Files
CodeLikeDemo/js/main.js
2026-03-27 18:21:29 +08:00

1358 lines
73 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* js/main.js
* 核心逻辑,包含屏幕流转 (Screen Flow)
*/
// --- 0. 屏幕管理与流转 ---
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(el => el.classList.add('hidden'));
document.getElementById(screenId).classList.remove('hidden');
}
// 被 HTML onclick 调用
window.selectClass = function(classId) {
state.selectedClass = classId;
state.cards = [];
state.sprintTotalTime = 180;
state.sprintTimeRemaining = 180;
state.integrationCapacityRemaining = state.integrationCapacityMax;
state.overdueDaysProcessed = 0;
state.crisisActive = false;
state.gameOver = false;
state.paused = false;
state.negativeEffects = createEmptyNegativeEffects();
state.lockedCardIds = [];
state.negativeTickCount = 0;
// 初始化职业特定数据 (5 大职业)
if(classId === 'pm') {
state.funds = 25000;
addCard('pm', 200, 200);
addCard('whiteboard', 200, 200);
addCard('endless_meeting', 200, 200);
}
else if(classId === 'qa') {
state.funds = 12000;
addCard('qa', 200, 200);
addCard('unit_test', 200, 200);
addCard('bug', 200, 200);
}
else if(classId === 'ops') {
state.funds = 20000;
addCard('ops', 200, 200);
addCard('server', 200, 200);
addCard('security_patch', 200, 200);
}
else if(classId === 'bystander') {
state.funds = 2000; // 穷
addCard('intern', 200, 200);
addCard('tech_debt', 200, 200);
}
else {
// default: dev
state.funds = 10000;
addCard('developer', 200, 200);
addCard('focus', 200, 200);
addCard('focus', 200, 200);
addCard('coffee', 200, 200);
}
state.reserve = 5000;
state.earnedValue = 0;
state.goalEV = 10000;
state.sprintNumber = 1;
state.maxFunds = state.funds;
showScreen('screen-game');
setHudStatus('动作完成后才扣项目天数', 'info');
updateHUD();
renderBoard();
// 启动主循环每100ms
if(window.gameInterval) clearInterval(window.gameInterval);
if(window.secondInterval) clearInterval(window.secondInterval);
if(window.negativeInterval) clearInterval(window.negativeInterval);
window.gameInterval = setInterval(gameTick, 100);
window.secondInterval = setInterval(secondTick, 1000);
window.negativeInterval = setInterval(negativeTick, 5000);
recalculateNegativeEffects();
}
// --- 1. 数据配置与类型定义模拟 ---
const CardTemplates = {
// === 基础手牌 (初始即可使用) ===
// --- 人员 ---
intern: { id: 'intern', name: '👶 实习生', type: 'actor', tags: ['human', 'dev'], desc: '执行域只能打杂极易产出Bug。', icon: '👶', colorClass: 'card-actor', unlocked: true },
developer: { id: 'developer', name: '👨‍💻 开发人员', type: 'actor', tags: ['human', 'dev'], desc: '执行域:将需求转化为代码。', icon: '👨‍💻', colorClass: 'card-actor', unlocked: true },
pm: { id: 'pm', name: '📊 产品经理', type: 'actor', tags: ['human', 'pm'], desc: '规划域:画大饼、搞需求。', icon: '📊', colorClass: 'card-actor', unlocked: true },
qa: { id: 'qa', name: '🕵️ 测试工程师', type: 'actor', tags: ['human', 'qa'], desc: '监控域:找出质量漏洞。', icon: '🕵️', colorClass: 'card-actor', unlocked: true },
ui_designer: { id: 'ui_designer', name: '🎨 UI设计师', type: 'actor', tags: ['human', 'design'], desc: '规划域:提供视觉切图。', icon: '🎨', colorClass: 'card-actor', unlocked: true },
ops: { id: 'ops', name: '🛠️ 运维工程师', type: 'actor', tags: ['human', 'ops'], desc: '收尾域:部署与监控服务器。', icon: '🛠️', colorClass: 'card-actor', unlocked: true },
// --- 资源与工具 ---
focus: { id: 'focus', name: '✨ 专注力', type: 'resource', tags: ['resource'], desc: '执行核心消耗品。', icon: '✨', colorClass: 'card-resource', unlocked: true },
coffee: { id: 'coffee', name: '☕ 冰美式', type: 'resource', tags: ['resource'], desc: '恢复专注力。', icon: '☕', colorClass: 'card-resource', unlocked: true },
server: { id: 'server', name: '☁️ 云服务器', type: 'resource', tags: ['infra'], desc: '代码运行的环境。', icon: '☁️', colorClass: 'card-resource', unlocked: true },
whiteboard: { id: 'whiteboard', name: '📝 白板', type: 'resource', tags: ['tool'], desc: '用于开会和头脑风暴。', icon: '📝', colorClass: 'card-resource', unlocked: true },
git_repo: { id: 'git_repo', name: '🗄️ Git 仓库', type: 'resource', tags: ['tool'], desc: '存放代码的地方。', icon: '🗄️', colorClass: 'card-resource', unlocked: true },
// --- 文档与产物 ---
prd: { id: 'prd', name: '📄 需求文档', type: 'resource', tags: ['doc'], desc: '开发指南,防止需求变更。', icon: '📄', colorClass: 'card-resource', unlocked: true },
design_draft: { id: 'design_draft', name: '🖼️ 设计稿', type: 'resource', tags: ['doc'], desc: '前端页面基础。', icon: '🖼️', colorClass: 'card-resource', unlocked: true },
api_doc: { id: 'api_doc', name: '📜 API接口文档', type: 'resource', tags: ['doc'], desc: '前后端联调必备。', icon: '📜', colorClass: 'card-resource', unlocked: true },
dirty_code: { id: 'dirty_code', name: '💩 脏代码', type: 'resource', tags: ['code'], desc: '缺乏设计的初稿。', icon: '💩', colorClass: 'card-resource', unlocked: true },
clean_code: { id: 'clean_code', name: '💎 优雅代码', type: 'resource', tags: ['code'], desc: '可维护的高质量代码。', icon: '💎', colorClass: 'card-resource', unlocked: true },
module: { id: 'module', name: '📦 可交付模块', type: 'module', tags: ['goal'], desc: '最终交付物。', icon: '📦', colorClass: 'card-module', unlocked: true },
user_feedback: { id: 'user_feedback', name: '💬 用户反馈', type: 'resource', tags: ['feedback'], desc: '线上系统的真实声音。', icon: '💬', colorClass: 'card-resource', unlocked: true },
// === 高级手牌 (需要通过随机事件或商店抽取解锁) ===
// --- 高级人员 ---
senior_dev: { id: 'senior_dev', name: '🥷 高级开发', type: 'actor', tags: ['human', 'dev', 'senior'], desc: '超强执行:合成速度快,自带优化。', icon: '🥷', colorClass: 'card-actor', unlocked: false },
architect: { id: 'architect', name: '🧙 架构师', type: 'actor', tags: ['human', 'senior'], desc: '规划域:输出高内聚蓝图,秒杀技术债。', icon: '🧙', colorClass: 'card-actor', unlocked: false },
tech_lead: { id: 'tech_lead', name: '👑 技术专家', type: 'actor', tags: ['human', 'senior'], desc: '兜底域所有Bug瞬间修复。', icon: '👑', colorClass: 'card-actor', unlocked: false },
agile_coach: { id: 'agile_coach', name: '🥋 敏捷教练', type: 'actor', tags: ['human', 'manage'], desc: '控制域消除所有废话会议产出极速Focus。', icon: '🥋', colorClass: 'card-actor', unlocked: false },
senior_pm: { id: 'senior_pm', name: '🧠 数据产品', type: 'actor', tags: ['human', 'pm', 'senior'], desc: '只做有用需求,彻底屏蔽范围蔓延。', icon: '🧠', colorClass: 'card-actor', unlocked: false },
// --- 高级资源/工具 ---
energy_drink: { id: 'energy_drink', name: '🧪 能量饮料', type: 'resource', tags: ['resource'], desc: '致死量咖啡因,极速爆气。', icon: '🧪', colorClass: 'card-resource', unlocked: false },
pizza: { id: 'pizza', name: '🍕 加班披萨', type: 'resource', tags: ['resource'], desc: '稳定军心,化解团队士气危机。', icon: '🍕', colorClass: 'card-resource', unlocked: false },
kanban: { id: 'kanban', name: '📋 敏捷看板', type: 'resource', tags: ['tool'], desc: '任务可视化,防止返工。', icon: '📋', colorClass: 'card-resource', unlocked: false },
budget: { id: 'budget', name: '💰 追加预算', type: 'resource', tags: ['finance'], desc: '万能的钞能力。', icon: '💰', colorClass: 'card-resource', unlocked: false },
database: { id: 'database', name: '💽 主从数据库', type: 'resource', tags: ['infra'], desc: '存放核心数据。', icon: '💽', colorClass: 'card-resource', unlocked: false },
cache_redis: { id: 'cache_redis', name: '⚡ Redis 缓存', type: 'resource', tags: ['infra'], desc: '性能飞升。', icon: '⚡', colorClass: 'card-resource', unlocked: false },
message_queue: { id: 'message_queue', name: '📬 消息队列', type: 'resource', tags: ['infra'], desc: '解耦架构,抗高并发。', icon: '📬', colorClass: 'card-resource', unlocked: false },
docker_container: { id: 'docker_container', name: '🐳 Docker 容器', type: 'resource', tags: ['infra'], desc: '统一环境,杜绝“在我电脑上能跑”。', icon: '🐳', colorClass: 'card-resource', unlocked: false },
k8s_cluster: { id: 'k8s_cluster', name: '☸️ K8s 集群', type: 'resource', tags: ['infra'], desc: '终极基建,高可用保证。', icon: '☸️', colorClass: 'card-resource', unlocked: false },
ci_cd: { id: 'ci_cd', name: '🔄 CI/CD 流水线', type: 'resource', tags: ['tool'], desc: '自动化打包发布。', icon: '🔄', colorClass: 'card-resource', unlocked: false },
unit_test: { id: 'unit_test', name: '🧪 单元测试', type: 'resource', tags: ['tool'], desc: '代码保护网免疫底层Bug。', icon: '🧪', colorClass: 'card-resource', unlocked: false },
e2e_test: { id: 'e2e_test', name: '🤖 E2E 自动化', type: 'resource', tags: ['tool'], desc: '代替QA的手工点点点。', icon: '🤖', colorClass: 'card-resource', unlocked: false },
// --- 高级产物/操作 ---
arch_diagram: { id: 'arch_diagram', name: '🗺️ 架构蓝图', type: 'resource', tags: ['doc'], desc: '防止代码变屎山的护身符。', icon: '🗺️', colorClass: 'card-resource', unlocked: false },
refactoring: { id: 'refactoring', name: '🔨 重构', type: 'resource', tags: ['action'], desc: '消除技术债的良药。', icon: '🔨', colorClass: 'card-resource', unlocked: false },
perf_tuning: { id: 'perf_tuning', name: '🚀 性能调优', type: 'resource', tags: ['action'], desc: '化解响应超时。', icon: '🚀', colorClass: 'card-resource', unlocked: false },
security_patch: { id: 'security_patch', name: '🛡️ 安全补丁', type: 'resource', tags: ['action'], desc: '防止黑客删库。', icon: '🛡️', colorClass: 'card-resource', unlocked: false },
data_report: { id: 'data_report', name: '📊 数据大盘', type: 'resource', tags: ['doc'], desc: '向上管理的利器。', icon: '📊', colorClass: 'card-resource', unlocked: false },
market_research: { id: 'market_research', name: '📈 竞品分析', type: 'resource', tags: ['doc'], desc: '老板最爱看的东西。', icon: '📈', colorClass: 'card-resource', unlocked: false },
scrum_meeting: { id: 'scrum_meeting', name: '🧍 每日站会', type: 'resource', tags: ['manage'], desc: '对齐颗粒度。', icon: '🧍', colorClass: 'card-resource', unlocked: false },
pair_programming: { id: 'pair_programming', name: '👯 结对编程', type: 'resource', tags: ['action'], desc: '两份工资一份产出但Bug清零。', icon: '👯', colorClass: 'card-resource', unlocked: false },
// === 系统风险/负面 (Red Cards - 10+) ===
bug: { id: 'bug', name: '🐛 代码Bug', type: 'negative', tags: ['bad'], desc: '质量缺陷,拖慢速度。', icon: '🐛', colorClass: 'card-negative', trigger: 'immediate', penalty: '持续占用桌面空间', cancelWith: ['qa', 'tech_lead'] },
tech_debt: { id: 'tech_debt', name: '💣 技术债', type: 'negative', tags: ['bad'], desc: '屎山堆积,减慢全员速度。', icon: '💣', colorClass: 'card-negative', trigger: 'immediate', penalty: '使所有合成耗时 +50%', cancelWith: ['architect', 'refactoring'] },
scope_creep: { id: 'scope_creep', name: '📈 范围蔓延', type: 'negative', tags: ['bad'], desc: '无底洞需求。', icon: '📈', colorClass: 'card-negative', trigger: 'immediate', penalty: '扣除 $500', cancelWith: ['senior_pm', 'pm'] },
endless_meeting: { id: 'endless_meeting', name: '🗣️ 无意义会议', type: 'negative', tags: ['bad'], desc: '消耗大量专注力。', icon: '🗣️', colorClass: 'card-negative', trigger: 'immediate', penalty: '吞噬周围的✨专注力', cancelWith: ['agile_coach', 'scrum_meeting'] },
bad_vibe: { id: 'bad_vibe', name: '🌩️ 士气低落', type: 'negative', tags: ['bad'], desc: '团队失去Vibe。', icon: '🌩️', colorClass: 'card-negative', trigger: 'immediate', penalty: '产出Bug率飙升', cancelWith: ['pizza', 'budget'] },
merge_conflict: { id: 'merge_conflict', name: '⚔️ 合并冲突', type: 'negative', tags: ['bad'], desc: 'Git 灾难。', icon: '⚔️', colorClass: 'card-negative', trigger: 'immediate', penalty: '锁死 👨‍💻 开发人员', cancelWith: ['senior_dev', 'tech_lead'] },
prod_bug: { id: 'prod_bug', name: '🔥 线上事故', type: 'crisis', tags: ['crisis'], desc: '致命危机:需立刻回滚!', icon: '🔥', colorClass: 'card-crisis', trigger: 'countdown', timeMs: 15000, penalty: '扣除 $10,000', cancelWith: ['ops', 'ci_cd'] },
server_down: { id: 'server_down', name: '💥 宕机', type: 'crisis', tags: ['crisis'], desc: '致命危机:运维快来!', icon: '💥', colorClass: 'card-crisis', trigger: 'countdown', timeMs: 10000, penalty: '项目破产 / 资金清零', cancelWith: ['ops', 'k8s_cluster'] },
memory_leak: { id: 'memory_leak', name: '🚨 内存泄漏', type: 'crisis', tags: ['crisis'], desc: '系统内存溢出,即将崩溃。', icon: '🚨', colorClass: 'card-crisis', trigger: 'countdown', timeMs: 15000, penalty: '扣除 $20,000', cancelWith: ['perf_tuning', 'qa', 'focus'] },
data_loss: { id: 'data_loss', name: '🗑️ 数据丢失', type: 'crisis', tags: ['crisis'], desc: '删库跑路危机!', icon: '🗑️', colorClass: 'card-crisis', trigger: 'countdown', timeMs: 12000, penalty: '项目破产 / Game Over', cancelWith: ['database', 'security_patch'] },
resignation: { id: 'resignation', name: '👋 核心提离职', type: 'crisis', tags: ['crisis'], desc: '必须要挽留!', icon: '👋', colorClass: 'card-crisis', trigger: 'countdown', timeMs: 20000, penalty: '永久失去一名 👨‍💻高级人员', cancelWith: ['budget', 'agile_coach'] },
audit_fail: { id: 'audit_fail', name: '⚖️ 审计失败', type: 'crisis', tags: ['crisis'], desc: '合规检查不通过,项目叫停。', icon: '⚖️', colorClass: 'card-crisis', trigger: 'countdown', timeMs: 30000, penalty: '直接破产', cancelWith: ['senior_pm', 'architect'] }
};
const Recipes = [
// --- 规划域 (Planning) ---
{ id: 'pm_focus_to_prd', inputs: ['pm', 'focus'], timeMs: 4000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'prd', chance: 1.0 }, { templateId: 'scope_creep', chance: 0.3 }], desc: "规划需求 (输出 PRD)..." },
{ id: 'arch_prd_to_diagram', inputs: ['architect', 'prd'], timeMs: 5000, tier: 2, dayCost: 3, capacityCost: 2, outputs: [{ templateId: 'arch_diagram', chance: 1.0 }], desc: "系统架构设计..." },
{ id: 'ui_prd_to_design', inputs: ['ui_designer', 'prd'], timeMs: 4000, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'design_draft', chance: 1.0 }], desc: "高保真UI绘制中..." },
{ id: 'intern_design_to_dirty', inputs: ['intern', 'design_draft'], timeMs: 6000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'dirty_code', chance: 1.0 }, { templateId: 'bug', chance: 0.8 }], desc: "实习生切图 (高Bug率)..." },
{ id: 'senior_pm_prd', inputs: ['senior_pm', 'whiteboard'], timeMs: 3000, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'prd', chance: 1.0 }, { templateId: 'data_report', chance: 1.0 }], desc: "数据驱动规划..." },
{ id: 'senior_pm_market', inputs: ['senior_pm', 'user_feedback'], timeMs: 4000, tier: 2, dayCost: 3, capacityCost: 2, outputs: [{ templateId: 'market_research', chance: 1.0 }, { templateId: 'prd', chance: 1.0 }], desc: "深挖用户反馈生成竞品分析..." },
{ id: 'pm_market_report', inputs: ['market_research', 'pm'], timeMs: 3000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'budget', chance: 1.0 }], desc: "拿竞品分析去忽悠追加预算..." },
// --- 执行域 (Executing) ---
{ id: 'dev_focus_to_dirty', inputs: ['developer', 'focus'], timeMs: 3000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'dirty_code', chance: 1.0 }, { templateId: 'bug', chance: 0.4 }, { templateId: 'tech_debt', chance: 0.2 }], desc: "无文档盲写(容易出Bug)..." },
{ id: 'dev_prd_to_clean', inputs: ['developer', 'prd'], timeMs: 5000, tier: 2, dayCost: 3, capacityCost: 2, outputs: [{ templateId: 'clean_code', chance: 1.0 }, { templateId: 'bug', chance: 0.1 }], desc: "按照PRD规范开发..." },
{ id: 'dev_arch_to_clean', inputs: ['developer', 'arch_diagram'], timeMs: 4000, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'clean_code', chance: 1.0 }, { templateId: 'api_doc', chance: 0.8 }], desc: "基于架构蓝图的高效开发..." },
{ id: 'senior_dev_clean', inputs: ['senior_dev', 'prd'], timeMs: 2500, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'clean_code', chance: 1.0 }], desc: "大佬出手,又快又好..." },
{ id: 'pair_prog', inputs: ['pair_programming', 'developer'], timeMs: 2000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'clean_code', chance: 1.0 }], desc: "结对编程中..." },
{ id: 'tech_lead_git', inputs: ['tech_lead', 'git_repo'], timeMs: 3000, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'api_doc', chance: 1.0 }, { templateId: 'clean_code', chance: 1.0 }], desc: "技术专家梳理全局代码..." },
{ id: 'dev_api_doc', inputs: ['developer', 'api_doc'], timeMs: 3000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'clean_code', chance: 1.0 }], desc: "前后端对齐接口开发..." },
{ id: 'intern_api_doc', inputs: ['intern', 'api_doc'], timeMs: 4000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'dirty_code', chance: 1.0 }, { templateId: 'bug', chance: 1.0 }], desc: "实习生看不懂接口乱写..." },
{ id: 'refactoring_clean', inputs: ['refactoring', 'dirty_code'], timeMs: 4000, tier: 2, dayCost: 3, capacityCost: 2, outputs: [{ templateId: 'clean_code', chance: 1.0 }], desc: "强力重构屎山代码..." },
// --- 监控与控制域 (Monitoring & Controlling) ---
{ id: 'dirty_to_clean_qa', inputs: ['dirty_code', 'qa'], timeMs: 4000, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'clean_code', chance: 1.0 }], desc: "抓虫并提纯代码..." },
{ id: 'e2e_clean', inputs: ['e2e_test', 'dirty_code'], timeMs: 1500, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'clean_code', chance: 1.0 }], desc: "自动化测试飞速过滤..." },
{ id: 'unit_test_protect', inputs: ['unit_test', 'developer'], timeMs: 2000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'focus', chance: 1.0 }, { templateId: 'focus', chance: 1.0 }], desc: "跑通单测,增强信心..." },
{ id: 'agile_kanban', inputs: ['agile_coach', 'kanban'], timeMs: 2000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'scrum_meeting', chance: 1.0 }, { templateId: 'focus', chance: 1.0 }], desc: "建立敏捷看板,生成站会..." },
{ id: 'scrum_dev', inputs: ['scrum_meeting', 'developer'], timeMs: 1500, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'focus', chance: 1.0 }, { templateId: 'tech_debt', chance: 0.2 }], desc: "每日站会对齐进度..." },
{ id: 'security_ops', inputs: ['security_patch', 'ops'], timeMs: 3000, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'clean_code', chance: 1.0 }, { templateId: 'server', chance: 1.0 }], desc: "运维打安全补丁..." },
// --- 收尾域 (Closing/Release) ---
{ id: 'clean_server_to_module', inputs: ['clean_code', 'server'], timeMs: 5000, tier: 3, dayCost: 5, capacityCost: 3, outputs: [{ templateId: 'module', chance: 1.0 }], desc: "手动部署上线打包..." },
{ id: 'ci_cd_module', inputs: ['clean_code', 'ci_cd'], timeMs: 1500, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'module', chance: 1.0 }], desc: "流水线全自动秒级发布..." },
{ id: 'pm_magic_bug', inputs: ['pm', 'bug'], timeMs: 2000, tier: 3, dayCost: 4, capacityCost: 3, outputs: [{ templateId: 'module', chance: 0.5 }, { templateId: 'prod_bug', chance: 0.5 }], desc: "带病强上(极高风险)..." },
{ id: 'module_feedback', inputs: ['module', 'kanban'], timeMs: 2000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'user_feedback', chance: 1.0 }], desc: "上线后收集用户反馈..." },
// --- 运维基建拓展 ---
{ id: 'ops_server', inputs: ['ops', 'server'], timeMs: 3000, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'docker_container', chance: 1.0 }], desc: "容器化服务器..." },
{ id: 'ops_docker_k8s', inputs: ['ops', 'docker_container'], timeMs: 4000, tier: 4, dayCost: 8, capacityCost: 5, outputs: [{ templateId: 'k8s_cluster', chance: 1.0 }], desc: "搭建 K8s 集群高可用..." },
{ id: 'dev_database', inputs: ['developer', 'database'], timeMs: 5000, tier: 2, dayCost: 3, capacityCost: 2, outputs: [{ templateId: 'api_doc', chance: 1.0 }, { templateId: 'dirty_code', chance: 1.0 }], desc: "连主从库写业务..." },
{ id: 'perf_redis', inputs: ['perf_tuning', 'cache_redis'], timeMs: 3000, tier: 2, dayCost: 2, capacityCost: 2, outputs: [{ templateId: 'server', chance: 1.0 }, { templateId: 'focus', chance: 1.0 }], desc: "接入Redis性能起飞..." },
{ id: 'mq_arch', inputs: ['message_queue', 'tech_lead'], timeMs: 4000, tier: 4, dayCost: 7, capacityCost: 5, outputs: [{ templateId: 'arch_diagram', chance: 1.0 }, { templateId: 'module', chance: 1.0 }], desc: "技术专家解耦高并发架构..." },
{ id: 'ops_db_report', inputs: ['ops', 'database'], timeMs: 2000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'data_report', chance: 1.0 }], desc: "运维捞取后台数据报表..." },
// --- 资源转换 ---
{ id: 'coffee_to_focus', inputs: ['developer', 'coffee'], timeMs: 1000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'developer', chance: 1.0 }, { templateId: 'focus', chance: 1.0 }, { templateId: 'focus', chance: 1.0 }], desc: "喝咖啡,暴气中..." },
{ id: 'energy_drink_focus', inputs: ['senior_dev', 'energy_drink'], timeMs: 500, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'senior_dev', chance: 1.0 }, { templateId: 'focus', chance: 1.0 }, { templateId: 'focus', chance: 1.0 }, { templateId: 'focus', chance: 1.0 }], desc: "致死量咖啡因..." },
{ id: 'pizza_team', inputs: ['pizza', 'developer'], timeMs: 2000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'focus', chance: 1.0 }, { templateId: 'focus', chance: 1.0 }, { templateId: 'bug', chance: 0.3 }], desc: "吃披萨通宵加班 (易出Bug)..." },
{ id: 'budget_pm', inputs: ['budget', 'pm'], timeMs: 1000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'pizza', chance: 1.0 }, { templateId: 'coffee', chance: 1.0 }, { templateId: 'energy_drink', chance: 1.0 }], desc: "产品经理挥霍预算买吃的..." },
{ id: 'budget_report', inputs: ['budget', 'data_report'], timeMs: 2000, tier: 1, dayCost: 1, capacityCost: 1, outputs: [{ templateId: 'budget', chance: 1.0 }, { templateId: 'budget', chance: 1.0 }], desc: "拿好报表去骗更多投资!..." }
];
const OVERDUE_STAGE_RULES = [
{ maxDays: 5, dailyPenalty: 500, label: '阶段1' },
{ maxDays: 15, dailyPenalty: 1000, label: '阶段2' },
{ maxDays: 30, dailyPenalty: 2000, label: '阶段3' },
{ maxDays: Infinity, dailyPenalty: 4000, label: '阶段4' }
];
// --- 2. 全局状态 (State) ---
let state = {
selectedClass: 'developer',
funds: 10000,
maxFunds: 10000,
reserve: 5000,
earnedValue: 0,
goalEV: 10000,
sprintNumber: 1,
sprintTimeRemaining: 180, // 180天项目周期 (动作结<E4BD9C><E7BB93>制)
sprintTotalTime: 180,
integrationCapacityMax: 20,
integrationCapacityRemaining: 20,
overdueDaysProcessed: 0,
cards: [],
maxZIndex: 100,
crisisActive: false,
gameOver: false,
paused: false,
negativeEffects: createEmptyNegativeEffects(),
lockedCardIds: [],
negativeTickCount: 0
};
// --- 核心方法 ---
function createEmptyNegativeEffects() {
return {
extraBugChance: 0,
extraNegativeChance: 0,
extraDayCostT2Plus: 0,
crisisChanceBonus: 0,
fundLossPerTick: 0,
lockedCardIds: [],
summary: '红卡压力:无'
};
}
function uuid() { return Math.random().toString(36).substr(2, 9); }
function isNegativeCard(card) {
const tpl = CardTemplates[card.templateId];
return tpl?.type === 'negative';
}
function isBlockedFromTrash(card) {
const tpl = CardTemplates[card.templateId];
return tpl?.type === 'negative' || tpl?.type === 'crisis' || card.status === 'warning' || card.status === 'crisis';
}
function isCardLocked(card) {
return !!card && typeof card.lockedUntil === 'number' && Date.now() < card.lockedUntil;
}
function cleanupExpiredLocks() {
const now = Date.now();
state.cards.forEach(card => {
if (card.lockedUntil && now >= card.lockedUntil) {
delete card.lockedUntil;
}
});
}
function recalculateNegativeEffects() {
cleanupExpiredLocks();
const negatives = state.cards.filter(card => card.status === 'idle' && isNegativeCard(card));
const countById = negatives.reduce((acc, card) => {
acc[card.templateId] = (acc[card.templateId] || 0) + 1;
return acc;
}, {});
const effects = createEmptyNegativeEffects();
const bugCount = countById.bug || 0;
const techDebtCount = countById.tech_debt || 0;
const badVibeCount = countById.bad_vibe || 0;
const lockedCardIds = state.cards.filter(card => isCardLocked(card)).map(card => card.uid);
effects.fundLossPerTick += bugCount * 200;
effects.extraBugChance += bugCount * 0.1;
effects.extraNegativeChance += badVibeCount * 0.1;
effects.extraBugChance += badVibeCount * 0.1;
effects.extraDayCostT2Plus += techDebtCount;
if (techDebtCount >= 3) effects.crisisChanceBonus += 0.02;
effects.lockedCardIds = lockedCardIds;
const summaryParts = [];
if (effects.fundLossPerTick > 0) summaryParts.push(`资金流失 -$${effects.fundLossPerTick}/5s`);
if (effects.extraDayCostT2Plus > 0) summaryParts.push(`T2+工期+${effects.extraDayCostT2Plus}`);
if (lockedCardIds.length > 0) summaryParts.push(`${lockedCardIds.length}人被锁定`);
if (effects.extraBugChance > 0 || effects.extraNegativeChance > 0) summaryParts.push('负面产物率上升');
effects.summary = summaryParts.length ? `红卡压力:${summaryParts.join(' / ')}` : '红卡压力:无';
state.negativeEffects = effects;
state.lockedCardIds = lockedCardIds;
return effects;
}
function negativeTick() {
if (state.gameOver || state.paused) return;
cleanupExpiredLocks();
const negatives = state.cards.filter(card => card.status === 'idle' && isNegativeCard(card));
if (!negatives.length) {
recalculateNegativeEffects();
updateHUD();
return;
}
state.negativeTickCount += 1;
const bugCount = negatives.filter(card => card.templateId === 'bug').length;
if (bugCount > 0) {
addFunds(-(bugCount * 200));
}
const meetingCards = negatives.filter(card => card.templateId === 'endless_meeting');
if (meetingCards.length > 0) {
const actors = state.cards.filter(card => card.status === 'idle' && CardTemplates[card.templateId]?.type === 'actor' && !isNegativeCard(card) && !isCardLocked(card));
if (actors.length > 0) {
const target = actors[Math.floor(Math.random() * actors.length)];
target.lockedUntil = Date.now() + (5000 + Math.floor(Math.random() * 3000));
}
}
recalculateNegativeEffects();
updateHUD();
}
function addEV(amount) {
state.earnedValue += amount;
updateHUD();
if(state.earnedValue >= state.goalEV) {
handleSprintEnd(true);
}
}
function addReserve(amount) {
state.reserve += amount;
updateHUD();
if(state.reserve < 0) {
checkGameOver("应急储备金耗尽!由于工期无底线延误,甲方撤资,项目强行叫停。");
}
}
function getOverdueStage(daysOverdue) {
if (daysOverdue <= 0) {
return { label: '正常', dailyPenalty: 0, colorClass: 'text-emerald-300' };
}
const rule = OVERDUE_STAGE_RULES.find(item => daysOverdue <= item.maxDays) || OVERDUE_STAGE_RULES[OVERDUE_STAGE_RULES.length - 1];
const colorClass = rule.label === '阶段1'
? 'text-amber-300'
: rule.label === '阶段2'
? 'text-orange-300'
: 'text-rose-300';
return { ...rule, colorClass };
}
function applyOverduePenalty(newOverdueDays) {
let penalty = 0;
for (let day = state.overdueDaysProcessed + 1; day <= newOverdueDays; day++) {
const stage = getOverdueStage(day);
penalty += stage.dailyPenalty;
}
state.overdueDaysProcessed = Math.max(state.overdueDaysProcessed, newOverdueDays);
if (penalty <= 0) return;
if (state.reserve >= penalty) {
addReserve(-penalty);
return;
}
const reserveUsed = Math.max(0, state.reserve);
if (reserveUsed > 0) {
addReserve(-reserveUsed);
}
addFunds(-(penalty - reserveUsed));
if (state.funds <= 0) {
checkGameOver("严重超期导致预算被彻底击穿,公司破产清算!");
}
}
function setHudStatus(text, tone = 'info') {
const statusEl = document.getElementById('hud-status');
if (!statusEl) return;
const classMap = {
info: 'text-slate-300 border-slate-600/50',
warning: 'text-amber-200 border-amber-700/50',
danger: 'text-rose-200 border-rose-700/50',
success: 'text-emerald-200 border-emerald-700/50'
};
statusEl.textContent = text;
statusEl.className = `hidden lg:block text-xs bg-slate-800/80 px-3 py-1.5 rounded border ${classMap[tone] || classMap.info}`;
}
// 核心:动作结算型时间推进
function advanceTime(days) {
if (days <= 0) return;
state.sprintTimeRemaining -= days;
const overdueDays = Math.max(0, -state.sprintTimeRemaining);
if (overdueDays > state.overdueDaysProcessed) {
applyOverduePenalty(overdueDays);
}
state.cards.slice().forEach(c => {
if (c.status === 'crisis' && typeof c.timeRemainingDays === 'number') {
c.timeRemainingDays -= days;
if (c.timeRemainingDays <= 0) {
addFunds(-20000);
removeCard(c.uid);
state.crisisActive = false;
checkGameOver(`由于未及时处理【${CardTemplates[c.templateId]?.name || '危机'}】,系统崩溃,面临巨额索赔...`);
}
}
});
updateHUD();
}
function findEmptySpot(startX, startY, width = 180, height = 250) {
let bestX = startX;
let bestY = startY;
let radius = 0;
let angle = 0;
let overlap = true;
// 防止飞出屏幕
const minX = 140; // 避开左侧工具栏
const minY = 80; // 避开顶部 HUD
// 螺旋向外查找空位
while (overlap && radius < 2000) {
overlap = false;
for (let c of state.cards) {
const cLeft = c.x;
const cRight = c.x + 180; // 稍微多留点间距
const cTop = c.y;
const cBottom = c.y + 250;
// 判断矩形重叠
if (bestX < cRight && bestX + width > cLeft && bestY < cBottom && bestY + height > cTop) {
overlap = true;
break;
}
}
if (overlap) {
angle += 0.8;
radius += 15;
bestX = startX + Math.cos(angle) * radius;
bestY = startY + Math.sin(angle) * radius;
// 边缘约束
const maxX = window.innerWidth ? window.innerWidth - width - 20 : 1200;
const maxY = window.innerHeight ? window.innerHeight - height - 20 : 800;
bestX = Math.max(minX, Math.min(maxX, bestX));
bestY = Math.max(minY, Math.min(maxY, bestY));
}
}
return { x: bestX, y: bestY };
}
function addCard(templateId, x, y, extraProps = {}) {
const spot = findEmptySpot(x, y);
state.cards.push({
uid: uuid(), templateId, x: spot.x, y: spot.y, status: 'idle', isNew: true, ...extraProps
});
recalculateNegativeEffects();
}
function removeCard(uid) {
state.cards = state.cards.filter(c => c.uid !== uid);
recalculateNegativeEffects();
}
function addFunds(amount) {
state.funds += amount;
if(state.funds > state.maxFunds) state.maxFunds = state.funds;
// 资金飘字反馈
const deltaEl = document.getElementById('hud-funds-delta');
if (deltaEl) {
deltaEl.textContent = amount > 0 ? `+$${amount}` : `-$${Math.abs(amount)}`;
deltaEl.className = `absolute left-1/2 -translate-x-1/2 text-lg font-mono font-bold transition-all duration-500 z-50 ${amount > 0 ? 'text-emerald-400 drop-shadow-[0_0_10px_#10b981]' : 'text-red-500 drop-shadow-[0_0_10px_#ef4444]'}`;
// 重置动画状态
deltaEl.style.transition = 'none';
deltaEl.style.opacity = '1';
deltaEl.style.top = '-20px';
// 触发重绘
void deltaEl.offsetWidth;
// 播放飘字
deltaEl.style.transition = 'all 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)';
deltaEl.style.top = '-60px';
deltaEl.style.opacity = '0';
// 资金文字震动和颜色闪烁
const fundsText = document.getElementById('hud-funds');
if (fundsText) {
fundsText.style.transition = 'all 0.1s';
fundsText.style.transform = 'scale(1.2)';
fundsText.style.color = amount > 0 ? '#34d399' : '#f87171';
setTimeout(() => {
fundsText.style.transform = 'scale(1)';
fundsText.style.color = state.funds < 0 ? '#ef4444' : '#34d399';
}, 300);
}
}
}
// --- 3. 游戏循环 (Game Loop) ---
function gameTick() {
if(state.gameOver || state.paused) return;
const now = Date.now();
let stateChanged = false;
// 处理状态演变
for (let i = 0; i < state.cards.length; i++) {
let card = state.cards[i];
// 合成完成
if (card.status === 'processing' && card.processEndTime && now >= card.processEndTime) {
const recipe = card.recipe;
recipe.outputs.forEach(out => {
if (Math.random() <= out.chance) {
addCard(out.templateId, card.x + Math.random()*20, card.y + Math.random()*20);
}
});
// 返还 Actor
recipe.inputs.forEach(inTemplate => {
const tpl = CardTemplates[inTemplate];
if(tpl.type === 'actor') {
addCard(inTemplate, card.x - 20, card.y + 100);
}
});
state.integrationCapacityRemaining = Math.max(0, state.integrationCapacityRemaining - (recipe.capacityCost || 0));
advanceTime(recipe.dayCost || 0);
setHudStatus(`完成 ${recipe.desc} · 项目推进 ${recipe.dayCost || 0}`, state.sprintTimeRemaining >= 0 ? 'success' : 'danger');
removeCard(card.uid);
stateChanged = true;
break;
}
// 危机预警转真实危机
if (card.status === 'warning' && card.processEndTime && now >= card.processEndTime) {
card.status = 'crisis';
card.processEndTime = now + 15000; // 15秒倒计时
card.templateId = 'CRISIS_MEM_LEAK';
stateChanged = true;
}
// 回合制危机爆发已经在 advanceTime 中处理
}
if (stateChanged) renderBoard();
else updateProgressBars(now);
}
function secondTick() {
if(state.gameOver || state.paused) return;
// 随机触发危机:在每天的随机时刻触发
if (!state.crisisActive && state.sprintTimeRemaining > 0 && Math.random() < (0.02 + (state.negativeEffects?.crisisChanceBonus || 0))) {
triggerLLMCrisis();
}
// 自动回血/利息机制预留
}
function checkGameOver(reasonText) {
if (state.funds < 0) {
state.gameOver = true;
document.getElementById('gameover-reason').textContent = `*** 致命异常 0x0000DEAD: ${reasonText}`;
document.getElementById('gameover-sprints').textContent = state.sprintNumber;
document.getElementById('gameover-maxfunds').textContent = `$ ${state.maxFunds.toLocaleString()}`;
showScreen('screen-gameover');
}
}
function handleSprintEnd(isWin) {
state.paused = true;
if(isWin) {
// 胜利结算:结余预算 + 储备金 + 提前完工奖励
const bonus = (state.sprintTimeRemaining > 0 ? state.sprintTimeRemaining * 500 : 0) + state.reserve;
addFunds(bonus);
document.getElementById('win-total-funds').textContent = `$ ${state.funds.toLocaleString()}`;
showScreen('screen-win');
}
}
window.selectReward = function(id) {
// 隐藏奖励选择,重置状态进入下一局
state.sprintNumber++;
state.goalEV = Math.floor(state.goalEV * 1.5); // 难度提升:要求挣值上升
state.earnedValue = 0; // 挣值归零重新计算
state.sprintTotalTime = 180;
state.sprintTimeRemaining = 180;
state.integrationCapacityRemaining = state.integrationCapacityMax;
state.overdueDaysProcessed = 0;
state.paused = false;
// 奖励逻辑模拟
if(id === 3) { // 保洁阿姨
state.cards = state.cards.filter(c => c.templateId !== 'dirty_code' && c.templateId !== 'bug');
} else if(id === 1) { // 送一个 QA
addCard('qa', 800, 200);
}
setHudStatus('新 Sprint 开始:周期固定 180 天', 'info');
showScreen('screen-game');
updateHUD();
renderBoard();
}
function triggerLLMCrisis() {
state.crisisActive = true;
state.cards.push({
uid: uuid(),
templateId: 'memory_leak', // 直接出危机卡
x: window.innerWidth ? window.innerWidth / 2 - 85 : 400,
y: window.innerHeight ? window.innerHeight / 2 - 120 : 300,
status: 'crisis',
timeRemainingDays: 15 // 15天必须解决
});
setHudStatus('出现危机15天内必须处理', 'danger');
renderBoard();
}
function updateHUD() {
const sprintEl = document.getElementById('hud-sprint');
if(sprintEl) sprintEl.textContent = state.sprintNumber;
const evEl = document.getElementById('hud-ev');
if(evEl) evEl.textContent = `$${state.earnedValue.toLocaleString()} / $${state.goalEV.toLocaleString()}`;
const reserveEl = document.getElementById('hud-reserve');
if(reserveEl) {
reserveEl.textContent = `$${state.reserve.toLocaleString()}`;
reserveEl.className = `text-sm font-mono font-bold px-2 py-0.5 rounded ${state.reserve < 2000 ? 'bg-red-900/30 text-red-400' : 'bg-blue-900/30 text-blue-400'}`;
}
const fundsEl = document.getElementById('hud-funds');
if(fundsEl) fundsEl.textContent = `$${state.funds.toLocaleString()}`;
const capacityEl = document.getElementById('hud-capacity');
if (capacityEl) {
capacityEl.textContent = `${state.integrationCapacityRemaining} / ${state.integrationCapacityMax}`;
capacityEl.className = `text-sm font-mono font-bold ${state.integrationCapacityRemaining <= 5 ? 'text-amber-300' : 'text-cyan-300'}`;
}
const overdueStageEl = document.getElementById('hud-overdue-stage');
if (overdueStageEl) {
const stage = getOverdueStage(Math.max(0, -state.sprintTimeRemaining));
overdueStageEl.textContent = state.sprintTimeRemaining >= 0 ? '正常' : `${stage.label} · -$${stage.dailyPenalty}/天`;
overdueStageEl.className = `text-sm font-bold ${stage.colorClass}`;
}
const timeEl = document.getElementById('hud-time');
if(timeEl) {
if(state.sprintTimeRemaining >= 0) {
timeEl.textContent = `${state.sprintTimeRemaining}`;
timeEl.className = 'text-xl font-mono font-bold text-emerald-400 drop-shadow-[0_0_8px_rgba(52,211,153,0.4)]';
} else {
timeEl.textContent = `延期${Math.abs(state.sprintTimeRemaining)}`;
timeEl.className = 'text-xl font-mono font-bold text-rose-500 drop-shadow-[0_0_8px_rgba(244,63,94,0.6)] animate-pulse';
}
}
// 更新竖向进度条 (高度计算)
const timeBar = document.getElementById('hud-time-bar');
if (timeBar) {
const timePct = Math.max(0, (state.sprintTimeRemaining / state.sprintTotalTime) * 100);
timeBar.style.height = `${timePct}%`;
timeBar.className = `w-full transition-all duration-1000 ease-linear bg-gradient-to-t ${state.sprintTimeRemaining >= 0 ? 'from-emerald-600 to-emerald-400' : 'from-rose-600 to-rose-400'}`;
}
const fundsBar = document.getElementById('hud-funds-bar');
if (fundsBar) {
const fundsPct = Math.max(0, Math.min(100, (state.funds / 50000) * 100));
fundsBar.style.height = `${fundsPct}%`;
if (state.funds < 5000) {
fundsBar.className = 'w-full transition-all duration-500 ease-out bg-gradient-to-t from-red-600 to-red-400';
} else if (state.funds < 15000) {
fundsBar.className = 'w-full transition-all duration-500 ease-out bg-gradient-to-t from-amber-600 to-amber-400';
} else {
fundsBar.className = 'w-full transition-all duration-500 ease-out bg-gradient-to-t from-emerald-600 to-emerald-400';
}
}
const negativeEl = document.getElementById('hud-negative');
if (negativeEl) {
negativeEl.textContent = state.negativeEffects?.summary || '红卡压力:无';
negativeEl.className = `hidden xl:block text-xs px-3 py-1.5 rounded border ${state.lockedCardIds.length || state.negativeEffects?.fundLossPerTick || state.negativeEffects?.extraDayCostT2Plus ? 'text-rose-200 bg-rose-950/40 border-rose-700/40' : 'text-slate-300 bg-slate-800/80 border-slate-600/50'}`;
}
if(state.funds < 0) {
document.getElementById('hud-funds').style.color = '#ef4444'; // red-500
} else {
document.getElementById('hud-funds').style.color = '#34d399'; // emerald-400
}
}
// --- 4. 渲染引擎 (Render) ---
function renderBoard() {
const board = document.getElementById('desktop-board');
board.innerHTML = '';
state.cards.forEach(cardData => {
const el = document.createElement('div');
el.className = 'draggable absolute group';
el.style.left = cardData.x + 'px';
el.style.top = cardData.y + 'px';
el.dataset.uid = cardData.uid;
const isLocked = isCardLocked(cardData);
if (cardData.status === 'processing') {
el.className += ' stack-group';
el.innerHTML = `
<div class="progress-container">
<div class="progress-bar" id="prog-${cardData.uid}" style="width: 0%"></div>
</div>
<!-- 顶卡模拟 -->
<div class="game-card card-actor shadow-blue-500/50 shadow-2xl z-10 w-full h-full" style="left:0;top:0;">
<div class="card-header">
<span class="font-bold">Working</span>
<span class="text-[10px] text-blue-300 animate-pulse">⚙️</span>
</div>
<div class="card-body">
<div class="text-xs text-blue-200 font-medium text-center bg-blue-900/50 p-2 rounded w-full border border-blue-500/30">
${cardData.recipe.desc}
</div>
</div>
</div>
`;
}
else if (cardData.status === 'warning') {
el.className += ' game-card card-warning z-20 shadow-2xl';
el.innerHTML = `
<div class="card-header bg-red-950/80 border-b border-red-800">
<span class="font-bold text-red-200">⚠️ 甲方正在输入...</span>
</div>
<div class="card-body gap-6 bg-red-900/20">
<div class="loader"></div>
<div class="text-sm font-bold text-red-300 text-center animate-pulse drop-shadow-md">API 请求中...<br>危机即将降临</div>
</div>
`;
}
else if (cardData.status === 'crisis') {
const crisisTpl = CardTemplates[cardData.templateId] || CardTemplates.memory_leak;
const remainingDays = Math.max(0, typeof cardData.timeRemainingDays === 'number' ? cardData.timeRemainingDays : 0);
const totalDays = Math.max(remainingDays, 15);
el.className += ' game-card card-crisis z-20 shadow-[0_0_30px_rgba(239,68,68,0.5)]';
el.innerHTML = `
<div class="progress-container shadow-[0_0_10px_rgba(239,68,68,0.5)] border-red-900">
<div class="progress-bar bg-gradient-to-r from-red-600 to-rose-400" id="prog-${cardData.uid}" style="width: 100%"></div>
</div>
<div class="card-header bg-red-950 border-b border-red-800 flex justify-between">
<span class="font-bold text-red-400">${crisisTpl.name}</span>
<span class="text-xs font-mono font-bold text-red-300 bg-red-900/50 px-2 py-0.5 rounded shadow-inner" id="timer-${cardData.uid}">${remainingDays}天</span>
</div>
<div class="card-body justify-start pt-3 bg-red-900/10 p-3">
<div class="text-xs text-red-200 leading-relaxed italic bg-black/40 p-2 rounded mb-3 border border-red-900/50">
"堆积如山,服务器马上炸了!"
</div>
<div class="w-full flex flex-col gap-2">
<div class="flex justify-between items-center bg-red-950/50 p-1 rounded border border-red-900/30">
<span class="text-[10px] text-red-400/80 uppercase">超时惩罚</span>
<span class="text-xs font-bold text-red-400">-$20k</span>
</div>
<div class="flex justify-between items-center bg-red-950/50 p-1 rounded border border-red-900/30">
<span class="text-[10px] text-red-400/80 uppercase">剩余工期</span>
<span class="text-xs font-bold text-red-300">${remainingDays}天</span>
</div>
<div class="mt-1 w-full">
<div class="text-[10px] text-slate-400 uppercase tracking-wider mb-1 text-center">拖入解题</div>
<div class="flex justify-center gap-3">
<div class="crisis-slot" data-need="qa" title="需测试">🕵️</div>
<div class="crisis-slot" data-need="focus" title="需专注力">✨</div>
</div>
</div>
</div>
</div>
`;
cardData.crisisTotalDays = totalDays;
}
else {
const tpl = CardTemplates[cardData.templateId];
if(!tpl) return;
el.className += ` game-card ${tpl.colorClass}${isLocked ? ' ring-2 ring-amber-400 opacity-70' : ''}`;
el.innerHTML = `
<div class="card-header">
<span class="font-bold">${tpl.name}</span>
${isLocked ? '<span class="text-[10px] text-amber-300">锁定中</span>' : ''}
</div>
<div class="card-body pointer-events-none">
<div class="text-5xl drop-shadow-md mb-2 group-hover:scale-110 transition-transform">${tpl.icon}</div>
<div class="text-[11px] text-slate-300 text-center leading-relaxed opacity-80">${tpl.desc}</div>
</div>
<div class="card-footer">
<span class="tag">${tpl.type}</span>
</div>
`;
}
board.appendChild(el);
// 新卡片生成时的弹现动效
if (cardData.isNew) {
el.style.animation = 'card-spawn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
cardData.isNew = false; // 只播放一次
}
bindDragEvent(el);
});
}
function updateProgressBars(now) {
state.cards.forEach(card => {
if (card.status === 'processing') {
const el = document.getElementById(`prog-${card.uid}`);
if (el) {
const total = card.recipe.timeMs;
const remain = card.processEndTime - now;
let pct = 100 - (remain / total) * 100;
if (pct > 100) pct = 100;
el.style.width = `${pct}%`;
}
}
else if (card.status === 'crisis') {
const el = document.getElementById(`prog-${card.uid}`);
const tEl = document.getElementById(`timer-${card.uid}`);
if (el && tEl) {
const total = Math.max(1, card.crisisTotalDays || card.timeRemainingDays || 15);
const remain = Math.max(0, card.timeRemainingDays || 0);
let pct = (remain / total) * 100;
if (pct < 0) pct = 0;
el.style.width = `${pct}%`;
tEl.textContent = `${remain}`;
}
}
});
}
// --- 5. 拖拽与碰撞事件 ---
function bindDragEvent(element) {
let initialX, initialY, startX, startY;
element.addEventListener('pointerdown', (e) => {
if(e.target.classList.contains('crisis-slot')) return;
if(state.paused || state.gameOver) return;
const draggedUid = element.dataset.uid;
const draggedCard = state.cards.find(c => c.uid === draggedUid);
if (isCardLocked(draggedCard)) {
setHudStatus('该卡被负面状态锁定,暂时无法操作', 'warning');
return;
}
state.maxZIndex++;
element.style.zIndex = state.maxZIndex;
initialX = e.clientX;
initialY = e.clientY;
startX = element.offsetLeft;
startY = element.offsetTop;
element.classList.add('dragging');
element.setPointerCapture(e.pointerId);
// Highlight connectable cards
// 提示全局放置区 (Delivery / Trash)
if (draggedCard) {
const acceptedDeliverables = ['module', 'data_report', 'market_research', 'api_doc', 'arch_diagram'];
if (acceptedDeliverables.includes(draggedCard.templateId)) {
const dz = document.getElementById('delivery-zone');
if(dz && dz.firstElementChild) dz.firstElementChild.classList.add('ring-4', 'ring-emerald-500', 'animate-pulse');
}
if (!isBlockedFromTrash(draggedCard)) {
const tz = document.getElementById('trash-zone');
if(tz && tz.firstElementChild) tz.firstElementChild.classList.add('ring-4', 'ring-rose-500', 'animate-pulse');
}
}
if (draggedCard && draggedCard.status === 'idle') {
const dTemplate = CardTemplates[draggedCard.templateId];
const canSolveCrisis = dTemplate && (dTemplate.id === 'qa' || dTemplate.id === 'focus' || dTemplate.id === 'pm');
state.cards.forEach(tCard => {
if (tCard.uid === draggedUid) return;
// 配方高亮
if (tCard.status === 'idle') {
const matchedRecipe = Recipes.find(r =>
(r.inputs.includes(draggedCard.templateId) && r.inputs.includes(tCard.templateId)) &&
r.inputs.length === 2
);
if (matchedRecipe) {
const tEl = document.querySelector(`[data-uid="${tCard.uid}"]`);
if(tEl) tEl.classList.add('highlight-connectable');
}
}
// 危机卡高亮提示
if (tCard.status === 'crisis' && canSolveCrisis) {
const tEl = document.querySelector(`[data-uid="${tCard.uid}"]`);
if(tEl) tEl.classList.add('highlight-connectable');
}
});
}
const onMove = (e) => {
e.preventDefault();
const dx = e.clientX - initialX;
const dy = e.clientY - initialY;
element.style.left = `${startX + dx}px`;
element.style.top = `${startY + dy}px`;
// Highlight hover target and draw tentative line
document.querySelectorAll('.highlight-hover').forEach(el => el.classList.remove('highlight-hover'));
document.getElementById('svg-connections').innerHTML = ''; // Clear line
const centerX = startX + dx + 85;
const centerY = startY + dy + 120;
// --- 动态反馈区:当拖拽卡牌悬浮在回收站或交付区时,触发图标动效 ---
const trashZone = document.getElementById('trash-zone');
if (trashZone && draggedCard) {
const tRect = trashZone.getBoundingClientRect();
const inner = trashZone.firstElementChild;
if (e.clientX > tRect.left && e.clientX < tRect.right && e.clientY > tRect.top && e.clientY < tRect.bottom) {
inner.classList.add('scale-110', 'bg-rose-900/80', 'border-rose-400', 'shadow-[0_0_20px_rgba(225,29,72,0.6)]');
} else {
inner.classList.remove('scale-110', 'bg-rose-900/80', 'border-rose-400', 'shadow-[0_0_20px_rgba(225,29,72,0.6)]');
}
}
const deliveryZone = document.getElementById('delivery-zone');
if (deliveryZone && draggedCard) {
const dRect = deliveryZone.getBoundingClientRect();
const inner = deliveryZone.firstElementChild;
const acceptedDeliverables = ['module', 'data_report', 'market_research', 'api_doc', 'arch_diagram'];
if (e.clientX > dRect.left && e.clientX < dRect.right && e.clientY > dRect.top && e.clientY < dRect.bottom) {
if (acceptedDeliverables.includes(draggedCard.templateId)) {
inner.classList.add('scale-110', 'bg-emerald-800/80', 'border-emerald-400', 'shadow-[0_0_20px_rgba(16,185,129,0.6)]');
}
} else {
inner.classList.remove('scale-110', 'bg-emerald-800/80', 'border-emerald-400', 'shadow-[0_0_20px_rgba(16,185,129,0.6)]');
}
}
// -------------------------------------------------------------
if (draggedCard && draggedCard.status === 'idle') {
document.querySelectorAll('.highlight-crisis-hover').forEach(el => el.classList.remove('highlight-crisis-hover'));
for (let tCard of state.cards) {
if (tCard.uid === draggedUid) continue;
const tRect = {
left: tCard.x, top: tCard.y,
right: tCard.x + 170, bottom: tCard.y + 240
};
if (centerX > tRect.left && centerX < tRect.right && centerY > tRect.top && centerY < tRect.bottom) {
const tEl = document.querySelector(`[data-uid="${tCard.uid}"]`);
if(tEl && tEl.classList.contains('highlight-connectable')) {
// 危机解决悬停
if (tCard.status === 'crisis') {
tEl.classList.add('highlight-crisis-hover');
document.getElementById('svg-connections').innerHTML = `
<line x1="${centerX}" y1="${centerY}" x2="${tCard.x + 95}" y2="${tCard.y + 130}"
stroke="#10b981" stroke-width="8" stroke-dasharray="20,10"
style="filter: drop-shadow(0 0 20px #10b981); animation: pulse-border 0.3s linear infinite;" />
`;
}
// 正常合成悬停
else if (tCard.status === 'idle') {
tEl.classList.add('highlight-hover');
document.getElementById('svg-connections').innerHTML = `
<line x1="${centerX}" y1="${centerY}" x2="${tCard.x + 85}" y2="${tCard.y + 120}"
stroke="#c084fc" stroke-width="6" stroke-dasharray="15,10"
style="filter: drop-shadow(0 0 10px #c084fc); animation: pulse-border 0.5s linear infinite;" />
`;
}
}
}
}
}
};
const onUp = (e) => {
element.classList.remove('dragging');
element.releasePointerCapture(e.pointerId);
element.removeEventListener('pointermove', onMove);
element.removeEventListener('pointerup', onUp);
// Remove all highlights and lines
document.querySelectorAll('.highlight-connectable').forEach(el => el.classList.remove('highlight-connectable'));
document.querySelectorAll('.highlight-hover').forEach(el => el.classList.remove('highlight-hover'));
document.querySelectorAll('.highlight-crisis-hover').forEach(el => el.classList.remove('highlight-crisis-hover'));
document.getElementById('svg-connections').innerHTML = '';
// 恢复图标区的原始状态
const tz = document.getElementById('trash-zone');
if(tz && tz.firstElementChild) tz.firstElementChild.classList.remove('scale-110', 'bg-rose-900/80', 'border-rose-400', 'shadow-[0_0_20px_rgba(225,29,72,0.6)]', 'ring-4', 'ring-rose-500', 'animate-pulse');
const dz = document.getElementById('delivery-zone');
if(dz && dz.firstElementChild) dz.firstElementChild.classList.remove('scale-110', 'bg-emerald-800/80', 'border-emerald-400', 'shadow-[0_0_20px_rgba(16,185,129,0.6)]', 'ring-4', 'ring-emerald-500', 'animate-pulse');
const uid = element.dataset.uid;
const cData = state.cards.find(c => c.uid === uid);
if(cData) {
cData.x = parseInt(element.style.left);
cData.y = parseInt(element.style.top);
// Delivery Zone Check (交付发版)
const deliveryZone = document.getElementById('delivery-zone');
const acceptedDeliverables = ['module', 'data_report', 'market_research', 'api_doc', 'arch_diagram'];
if (deliveryZone && acceptedDeliverables.includes(cData.templateId)) {
const dRect = deliveryZone.getBoundingClientRect();
if (e.clientX > dRect.left && e.clientX < dRect.right && e.clientY > dRect.top && e.clientY < dRect.bottom) {
if (cData.templateId === 'module') {
addEV(5000);
} else if (cData.templateId === 'data_report') {
addEV(2000);
} else if (cData.templateId === 'market_research') {
addEV(3000);
} else if (cData.templateId === 'api_doc') {
addEV(1000);
} else if (cData.templateId === 'arch_diagram') {
addEV(2000);
}
// 立即从状态中移除,防止引擎下一帧重新渲染覆盖它
removeCard(uid);
renderBoard();
updateHUD();
// 把它放回 DOM 里单独跑一次动画
document.getElementById('desktop-board').appendChild(element);
// 交付成功动画 (吸入缩小)
// 确保触发重绘
void element.offsetWidth;
element.style.transition = 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
element.style.transform = 'scale(0.1) rotate(15deg)';
element.style.opacity = '0';
setTimeout(() => {
element.remove();
}, 400);
return;
}
}
// Trash / Sell Zone Check
const trashZone = document.getElementById('trash-zone');
if (trashZone) {
const rect = trashZone.getBoundingClientRect();
if (e.clientX > rect.left && e.clientX < rect.right && e.clientY > rect.top && e.clientY < rect.bottom) {
// Prevent selling negative cards (e.g. bug, dirty_code, crisis, warning)
if (!isBlockedFromTrash(cData)) {
addFunds(10); // get $10 for selling
removeCard(uid);
renderBoard();
document.getElementById('desktop-board').appendChild(element);
void element.offsetWidth;
// 回收站吸入动画
element.style.transition = 'all 0.3s ease-in';
element.style.transform = 'scale(0) translateY(50px)';
element.style.opacity = '0';
setTimeout(() => {
element.remove();
}, 300);
return;
} else {
setHudStatus('红卡和危机卡不能出售,只能治理', 'warning');
element.style.transform = 'translateX(20px)';
setTimeout(() => { element.style.transform = ''; }, 200);
}
}
}
checkOverlap(cData, element);
}
};
element.addEventListener('pointermove', onMove);
element.addEventListener('pointerup', onUp);
});
}
function checkOverlap(draggedCard, dragElement) {
if(draggedCard.status !== 'idle') return;
const dragRect = {
left: draggedCard.x, top: draggedCard.y,
right: draggedCard.x + 170, bottom: draggedCard.y + 240
};
const centerX = dragRect.left + 85;
const centerY = dragRect.top + 120;
for (let targetCard of state.cards) {
if (targetCard.uid === draggedCard.uid) continue;
// 拖向危机卡
if (targetCard.status === 'crisis') {
const tRect = {
left: targetCard.x, top: targetCard.y,
right: targetCard.x + 170, bottom: targetCard.y + 240
};
if (centerX > tRect.left && centerX < tRect.right && centerY > tRect.top && centerY < tRect.bottom) {
const dTemplate = CardTemplates[draggedCard.templateId];
if (dTemplate && (dTemplate.id === 'qa' || dTemplate.id === 'focus' || dTemplate.id === 'pm')) {
addFunds(5000);
// 先在状态里移除它们,让引擎不要重绘
removeCard(targetCard.uid);
if(dTemplate.id === 'focus' || dTemplate.id === 'pm') removeCard(draggedCard.uid);
state.crisisActive = false;
renderBoard(); // 重新渲染桌面,此时这两个卡片已经不在 DOM 里了
// 重新把它们塞回 DOM 以便播放单独的动画
const board = document.getElementById('desktop-board');
// 危机解决特效 (光芒爆发 + 缩小消失)
const targetElement = document.createElement('div');
targetElement.className = 'draggable absolute group';
targetElement.style.left = targetCard.x + 'px';
targetElement.style.top = targetCard.y + 'px';
targetElement.innerHTML = `
<div class="game-card card-crisis z-10 w-full h-full">
<div class="card-header"><span class="font-bold text-red-100">CRISIS CLEARED</span></div>
<div class="card-body">✨</div>
</div>`;
board.appendChild(targetElement);
if (dragElement) {
board.appendChild(dragElement);
void dragElement.offsetWidth;
dragElement.style.transition = 'all 0.5s ease';
dragElement.style.transform = 'scale(0) rotate(90deg)';
dragElement.style.filter = 'brightness(2) drop-shadow(0 0 20px #fff)';
dragElement.style.opacity = '0';
}
void targetElement.offsetWidth;
targetElement.style.transition = 'all 0.6s ease';
targetElement.style.transform = 'scale(1.2) translateY(-20px)';
targetElement.style.filter = 'brightness(3) saturate(0) drop-shadow(0 0 50px #10b981)';
targetElement.style.opacity = '0';
setTimeout(() => {
targetElement.remove();
if (dragElement) dragElement.remove();
}, 500);
return;
}
}
}
if (targetCard.status !== 'idle') continue;
if (isCardLocked(targetCard)) continue;
const targetTpl = CardTemplates[targetCard.templateId];
const draggedTpl = CardTemplates[draggedCard.templateId];
if (targetTpl?.type === 'negative' && targetTpl.cancelWith?.includes(draggedCard.templateId)) {
removeCard(targetCard.uid);
if (draggedTpl?.type !== 'actor') removeCard(draggedCard.uid);
recalculateNegativeEffects();
setHudStatus(`已治理 ${targetTpl.name}`, 'success');
renderBoard();
return;
}
const tRect = {
left: targetCard.x, top: targetCard.y,
right: targetCard.x + 170, bottom: targetCard.y + 240
};
if (centerX > tRect.left && centerX < tRect.right && centerY > tRect.top && centerY < tRect.bottom) {
const matchedRecipe = Recipes.find(r =>
(r.inputs.includes(draggedCard.templateId) && r.inputs.includes(targetCard.templateId)) &&
r.inputs.length === 2
);
if (matchedRecipe) {
const runtimeRecipe = getModifiedRecipe(matchedRecipe);
if (state.integrationCapacityRemaining < (runtimeRecipe.capacityCost || 0)) {
setHudStatus(`复杂度额度不足T${runtimeRecipe.tier} 需要 ${runtimeRecipe.capacityCost}`, 'warning');
if (dragElement) {
dragElement.style.transform = 'translateX(20px)';
setTimeout(() => { dragElement.style.transform = ''; }, 200);
}
return;
}
removeCard(draggedCard.uid);
removeCard(targetCard.uid);
state.cards.push({
uid: uuid(),
templateId: 'processing_stack',
x: targetCard.x, y: targetCard.y,
status: 'processing',
recipe: runtimeRecipe,
processEndTime: Date.now() + runtimeRecipe.timeMs
});
setHudStatus(`开始 ${runtimeRecipe.desc} · T${runtimeRecipe.tier} / ${runtimeRecipe.dayCost}天 / 额度-${runtimeRecipe.capacityCost}`, 'info');
renderBoard();
return;
}
}
}
}
function getModifiedRecipe(recipe) {
const effects = state.negativeEffects || createEmptyNegativeEffects();
const runtimeRecipe = {
...recipe,
outputs: recipe.outputs.map(out => ({ ...out }))
};
if (runtimeRecipe.tier >= 2) {
runtimeRecipe.dayCost += effects.extraDayCostT2Plus || 0;
}
runtimeRecipe.outputs = runtimeRecipe.outputs.map(out => {
let chance = out.chance;
if (out.templateId === 'bug') {
chance = Math.min(1, chance + (effects.extraBugChance || 0));
}
if (out.templateId === 'bug' || out.templateId === 'tech_debt') {
chance = Math.min(1, chance + (effects.extraNegativeChance || 0));
}
return { ...out, chance };
});
return runtimeRecipe;
}
// 按钮控制与图鉴模块
window.toggleRecipeModal = function() {
const modal = document.getElementById('recipe-modal');
if (!modal) return;
if (modal.classList.contains('hidden')) {
modal.classList.remove('hidden');
renderRecipeList();
} else {
modal.classList.add('hidden');
}
};
function renderRecipeList() {
const container = document.getElementById('recipe-list-container');
if (!container) return;
container.innerHTML = '';
Recipes.forEach(r => {
const recipeBlock = document.createElement('div');
recipeBlock.className = 'bg-slate-800 p-4 rounded-xl border border-slate-700 shadow flex items-center gap-4 hover:border-slate-500 transition-colors';
let inputsHtml = r.inputs.map((inId) => {
const tpl = CardTemplates[inId];
return `
<div class="flex flex-col items-center gap-1">
<div class="w-12 h-12 rounded-lg flex items-center justify-center text-2xl shadow bg-slate-700 border border-slate-600" title="${tpl.name}">
${tpl.icon}
</div>
</div>
`;
}).join('<span class="text-slate-500 font-bold px-2">+</span>');
let outputsHtml = r.outputs.map(out => {
const tpl = CardTemplates[out.templateId];
const isProbable = out.chance < 1.0;
return `
<div class="relative flex flex-col items-center gap-1">
<div class="relative w-14 h-14 rounded-lg flex flex-col items-center justify-center text-3xl shadow bg-emerald-900/40 border border-emerald-500/50" title="${tpl.name}">
${tpl.icon}
${isProbable ? `<span class="absolute -bottom-2 -right-2 text-[10px] bg-rose-900 text-white px-1.5 rounded shadow shadow-rose-900/50 font-mono">${out.chance*100}%</span>` : ''}
</div>
</div>
`;
}).join('');
recipeBlock.innerHTML = `
<div class="flex-1 flex flex-col justify-center">
<div class="text-sm font-bold text-blue-300 mb-3 flex items-center gap-2 flex-wrap">
<span class="bg-blue-900/50 px-2 py-0.5 rounded text-blue-200 border border-blue-700/50">${r.desc}</span>
<span class="text-xs font-mono text-slate-400 bg-slate-900/50 px-2 py-0.5 rounded">🕒 ${r.timeMs / 1000}s</span>
<span class="text-xs font-mono text-cyan-300 bg-cyan-950/40 px-2 py-0.5 rounded border border-cyan-900/40">📅 ${r.dayCost}天</span>
<span class="text-xs font-mono text-violet-300 bg-violet-950/40 px-2 py-0.5 rounded border border-violet-900/40">⚙️ T${r.tier}</span>
<span class="text-xs font-mono text-amber-300 bg-amber-950/40 px-2 py-0.5 rounded border border-amber-900/40">🔋 -${r.capacityCost}</span>
</div>
<div class="flex items-center gap-6">
<div class="flex items-center bg-slate-900/50 p-3 rounded-xl border border-slate-700/50">
${inputsHtml}
</div>
<span class="text-2xl text-slate-500 animate-pulse">➔</span>
<div class="flex items-center bg-slate-900/50 p-3 rounded-xl border border-slate-700/50 gap-2">
${outputsHtml}
</div>
</div>
</div>
`;
container.appendChild(recipeBlock);
});
}
document.getElementById('btn-pause')?.addEventListener('click', () => {
state.paused = !state.paused;
const btn = document.getElementById('btn-pause');
if(btn) btn.textContent = state.paused ? '▶' : '⏸';
});
document.getElementById('btn-fast')?.addEventListener('click', () => {
setHudStatus('快进已禁用:项目天数仅在融合完成后结算', 'warning');
});