/** * 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天项目周期 (动作结��制) 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 = `
Working ⚙️
${cardData.recipe.desc}
`; } else if (cardData.status === 'warning') { el.className += ' game-card card-warning z-20 shadow-2xl'; el.innerHTML = `
⚠️ 甲方正在输入...
API 请求中...
危机即将降临
`; } 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 = `
${crisisTpl.name} ${remainingDays}天
"堆积如山,服务器马上炸了!"
超时惩罚 -$20k
剩余工期 ${remainingDays}天
拖入解题
🕵️
`; 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 = `
${tpl.name} ${isLocked ? '锁定中' : ''}
${tpl.icon}
${tpl.desc}
`; } 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 = ` `; } // 正常合成悬停 else if (tCard.status === 'idle') { tEl.classList.add('highlight-hover'); document.getElementById('svg-connections').innerHTML = ` `; } } } } } }; 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 = `
CRISIS CLEARED
`; 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 `
${tpl.icon}
`; }).join('+'); let outputsHtml = r.outputs.map(out => { const tpl = CardTemplates[out.templateId]; const isProbable = out.chance < 1.0; return `
${tpl.icon} ${isProbable ? `${out.chance*100}%` : ''}
`; }).join(''); recipeBlock.innerHTML = `
${r.desc} 🕒 ${r.timeMs / 1000}s 📅 ${r.dayCost}天 ⚙️ T${r.tier} 🔋 -${r.capacityCost}
${inputsHtml}
${outputsHtml}
`; 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'); });