1358 lines
73 KiB
JavaScript
1358 lines
73 KiB
JavaScript
/**
|
||
* 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');
|
||
});
|