From 27fd364976613743e8602d80013c2ccfb9be2703 Mon Sep 17 00:00:00 2001 From: shiyue Date: Fri, 27 Mar 2026 18:21:29 +0800 Subject: [PATCH] Initial commit --- 01-需求与初始设计.md | 218 ++++++ 02-用户故事和测试用例说明.md | 57 ++ 03-技术架构关键路径.md | 132 ++++ css/main.css | 277 +++++++ dashboard.html | 157 ++++ index.html | 47 ++ js/main.js | 1357 ++++++++++++++++++++++++++++++++++ login.html | 38 + views/screen-game.html | 136 ++++ views/screen-gameover.html | 28 + views/screen-start.html | 121 +++ views/screen-win.html | 51 ++ 12 files changed, 2619 insertions(+) create mode 100644 01-需求与初始设计.md create mode 100644 02-用户故事和测试用例说明.md create mode 100644 03-技术架构关键路径.md create mode 100644 css/main.css create mode 100644 dashboard.html create mode 100644 index.html create mode 100644 js/main.js create mode 100644 login.html create mode 100644 views/screen-game.html create mode 100644 views/screen-gameover.html create mode 100644 views/screen-start.html create mode 100644 views/screen-win.html diff --git a/01-需求与初始设计.md b/01-需求与初始设计.md new file mode 100644 index 0000000..85fbad4 --- /dev/null +++ b/01-需求与初始设计.md @@ -0,0 +1,218 @@ + +# 🎮 产品需求文档 (PRD):《Project Vibe: Ship It!》 + +## 1. 产品愿景与核心体验 +* **游戏类型:** 桌面堆叠生存 (Desktop Stacking Survival) + 模拟经营 + Roguelike。 +* **目标受众:** 开发者、PM、QA、设计师,以及喜欢《Stacklands》或《密教模拟器》的策略玩家。 +* **核心体验:** 玩家在一个没有网格的“桌面”上,通过**拖拽合并卡牌**来推进项目进度(写代码、提需求、修Bug)。在有限的时间内(Sprint),一边应对由**真实大语言模型 (LLM) 动态生成的突发危机**,一边积攒资源完成交付物。 +* **VibeCoding 特色:** 游戏内的终极神卡是【Vibe助手】,它可以跨职业融合任何卡牌,大幅缩短进度条,但有概率引入“技术债”卡牌。 + +--- + +## 2. 核心游戏循环 (Core Loop & Roguelike) + +游戏采用经典的“局内战斗+局外成长”的 Roguelike 循环。 + +### 2.1 局内:Sprint 冲刺期(生存与合成) +* **周期时长:** 每个 Sprint 现实时间约为 3-5 分钟。 +* **胜利条件:** 在倒计时结束前,利用桌面卡牌合成出规定数量的【交付物】(如:3 个可用模块),且【项目资金】> 0。 +* **失败条件:** 【项目资金】归零(被 Bug 或超时危机扣光)。 +* **核心操作:** 拖拽卡牌重叠 -> 触发对应配方 -> 显示进度条 -> 倒计时结束产出新卡/资源。 + +### 2.2 局外:Retrospective 复盘大会(成长抉择) +* **结算与奖励:** Sprint 成功后,剩余的时间和资源按比例转化为【项目奖金】。 +* **肉鸽抉择(3选1):** 玩家从随机刷出的 3 个选项中选择 1 个强化自身: + * *新增高级卡牌:* 如【自动化测试脚本】(永久复用)。 + * *获取遗物 (Relics):* 如【敏捷教练】(全局 Buff:所有合成时间缩短 10%,但每回合扣除 5% 资金)。 + * *清理牌库:* 花费资金销毁桌面上难以处理的【技术债】或【屎山代码】卡。 + +--- + +## 3. 核心机制设计 + +### 3.1 桌面与状态机 (The Board & State) +* **无网格桌面:** 界面是一个类似操作系统的桌面。卡牌可以任意摆放和堆叠。 +* **全局资源:** 顶部 UI 显示当前 Sprint 的【倒计时】和【项目资金】。 +* **卡牌堆叠判定:** 当卡牌 A 被拖拽并释放 (Drop) 在卡牌 B 上时,系统检索 `Crafting_Recipes` (合成配方表)。若匹配,两张卡牌进入 `Processing` 状态锁定,头顶出现进度条。 + +### 3.2 基础卡牌与职业卡池 (Classes & Decks) +玩家开局需选择职业,决定初始手牌和专属配方路线。 + + + +#### 1. 基础公共卡池 (所有职业共用的环境底座) +* **【资源卡】** + * `专注力 (Focus)`:基础合成消耗品。随时间自然蒸发。 + * `咖啡 (Coffee)`:万能提神药。 + * `预算 (Budget)`:用于在商店购买服务器或外包。 + * `脑洞 (Idea)`:随机飘落在桌面的灵感。 +* **【建筑/设施卡】 (放在桌面上长期生效)** + * `咖啡机 (Coffee Machine)` + `预算` = 自动每 15 秒产出 1 杯 `咖啡`。 + * `云服务器 (Cloud Server)`:承载高级代码,产出项目进度,但每 10 秒消耗 1 点 `预算`。 +* **【负面/垃圾卡】 (需要想办法消除,否则占满桌面或引爆危机)** + * `Bug`:由脏代码变异,会吃掉附近的 `专注力`。 + * `技术债 (Tech Debt)`:无法移动,占据桌面空间。3个 `技术债` 聚在一起会召唤一次 【LLM 重大危机】。 + * `无意义的会议 (Pointless Meeting)`:随机抓取桌面上的一张 `实体卡(人)`,使其被锁定 20 秒,产出 `疲惫 (Burnout)`。 + +--- + +#### 2. 👨‍💻 资深开发 (Developer) 的深度合成树:架构师之路 +开发流派的乐趣在于**“从写屎山到重构成微服务”**的基建狂魔体验。 + +* **T1:造砖阶段 (脏与快)** + * `开发人员` + `专注力` = `脏代码 (Dirty Code)` (需 3 秒) + * `脏代码` + `脏代码` = `面条代码 (Spaghetti Code)` (只需 2 秒,但 30% 几率同时产出 `Bug`) +* **T2:重构阶段 (质量提升)** + * `面条代码` + `开发人员` + `咖啡` = `干净的代码 (Clean Code)` (需 15 秒) + * `脏代码` + `Vibe AI 助手` = `干净的代码` (只需 2 秒,极速重构!但 10% 几率产出 `开源协议侵权警告(危机卡)`) +* **T3:架构阶段 (质变)** + * `干净的代码` + `干净的代码` = `可用模块 (Working Module)` + * `可用模块` + `云服务器` = **【微服务节点 (Microservice)】**(建筑卡:每 5 秒自动产出 1 个 `项目进度` 和 100 `预算`) + * *隐藏配方:* `开发人员` + `无意义的会议` = `摸鱼打代码` -> 产出 `开源组件` (高价值卡牌) + `老板的怒火`。 + +--- + +#### 3. 📊 产品经理 (PM) 的深度合成树:空手套白狼 +产品流派没有直接产出代码的能力,乐趣在于**资源调度、画大饼和踢皮球**。 + +* **T1:需求制造** + * `产品经理` + `脑洞` = `一句话需求 (Vague Requirement)` (极快) + * `一句话需求` + `无意义的会议` = `PRD文档 (PRD)` + `画大饼 (Empty Promise)`卡片。 +* **T2:白嫖与外包** + * `画大饼` + `开发人员(NPC/队友)` = 临时将该开发者的合成速度提升 200%,但 10 秒后必定产出 `离职倾向 (Burnout)`(极度危险的负面卡)。 + * `PRD文档` + `预算(1000)` = `外包团队` -> 产出 `粗糙的模块` (可用但随时会坏)。 +* **T3:乾坤大挪移 (解决危机的独特方式)** + * `产品经理` + `Bug` = `特性 (Feature)`。*(神级配方:直接把 Bug 卡强行重命名为特性卡,变成可交付物,极其幽默)* + * `产品经理` + `技术债` = `排期延期卡`。*(把技术债塞进排期,消除占用,但会减少 Sprint 最终结算奖励)* + +--- + +#### 4. 🕵️ 测试工程师 (QA) 的深度合成树:陷阱与净化 +测试流派的乐趣在于**“找茬”、建立自动化流水线**,看着 Bug 灰飞烟灭。 + +* **T1:挑刺阶段** + * `测试员` + `脏代码` / `面条代码` = `Bug 报告` + `干净的代码` (提纯过程,需 10 秒)。 + * `Bug 报告` + `开发人员` = `争吵 (Argument)` + `重构好的代码`。 +* **T2:自动化防御** + * `测试员` + `专注力` + `咖啡` = `断言脚本 (Assert Script)` (消耗品武器卡)。 + * 将 `断言脚本` 拖到 `Bug` 或 `技术债` 上 = 瞬间将其秒杀,并掉落 `项目质量分`。 +* **T3:建立流水线 (质变)** + * `测试员` + `可用模块` + `云服务器` = **【CI/CD 流水线】**(自动化建筑)。 + * *作用:* 只要你把任何 `脏代码` 扔进 【CI/CD 流水线】,它会自动吃进去,5 秒后吐出 `干净的代码` 或者 `报警邮件`。玩家无需再手动派人去测试。 + +--- + +#### 5. 🎨 UI/UX 设计师 (Designer) 的深度合成树:美学的代价 +设计师流派主打**提升项目溢价(卖更多的钱)**,但要面对甲方的反复无常。 + +* **T1:原型设计** + * `设计师` + `专注力` = `线框图 (Wireframe)`。 + * `线框图` + `咖啡` = `高保真原型 (Figma Prototype)`。 +* **T2:前端融合** + * `高保真原型` + `可用模块(后端)` = `绚丽的 App (Gorgeous App)`(交付时获得 3 倍预算奖励)。 +* **T3:甲方的折磨 (特殊的卡牌连环演变)** + * 当你打出 `绚丽的 App` 时,有概率触发连环负面状态: + * `设计师` + `甲方反馈` = `设计稿_V2`。 + * `设计师` + `设计稿_V2` = `设计稿_最终版`。 + * `设计师` + `设计稿_最终版` = `设计稿_打死不改_绝对最终_V8.psd`。(只有这张卡才能真正交付)。 + +--- + +### 💡 VibeCoding 视角的 JSON 数据结构设计 + +有了这么庞大且幽默的配方树,如果你用传统的 IF-ELSE 来写代码绝对会疯掉。但对于 AI 辅助编程来说,你只需要让 AI 帮你搭建一个**基于 JSON 解析的合成引擎**。 + +你可以直接把这个 JSON 结构发给大模型(如 Cursor),让它生成核心逻辑: + +```json +// recipes.json (合成配方配置表) +[ + { + "id": "recipe_spaghetti_code", + "inputs": ["dirty_code", "dirty_code"], + "required_actor": null, + "time_ms": 2000, + "outputs": [ + { "card_id": "spaghetti_code", "chance": 1.0 }, + { "card_id": "bug", "chance": 0.3 } + ], + "description": "复制粘贴就是快,但也容易出Bug。" + }, + { + "id": "recipe_qa_purify", + "inputs": ["spaghetti_code"], + "required_actor": "qa_engineer", + "time_ms": 10000, + "outputs": [ + { "card_id": "clean_code", "chance": 1.0 }, + { "card_id": "bug_report", "chance": 1.0 } + ], + "description": "测试老哥强行帮你揪出Bug并提纯代码。" + }, + { + "id": "recipe_pm_magic", + "inputs": ["bug"], + "required_actor": "product_manager", + "time_ms": 5000, + "outputs": [ + { "card_id": "feature", "chance": 1.0 } + ], + "description": "这不是Bug,这是特性!" + } +] +``` + + +--- + +## 4. LLM 动态危机系统 (AI Director) + +这是游戏的核心亮点。LLM 将作为“甲方/系统”实时生成针对玩家当前桌面弱点的危机卡。 + +### 4.1 触发与生成机制 +1. **心跳检测:** 每隔 30 秒,游戏抓取当前桌面关键卡牌数量(如:资金 1w,脏代码 5,测试 0)。 +2. **异步请求:** 将状态发送给 LLM(如 OpenAI API),请求生成一个针对性的危机。 + +### 4.2 API 延迟处理策略(极简且优雅) +* **预警卡生成(立即执行):** API 请求发出的瞬间,桌面上立刻凭空掉落一张中立卡牌【⚠️ 甲方正在输入...】。 +* **UI 表现:** 卡牌显示 Loading 动画。玩家此时会有未知的压迫感,但无法对其进行操作。 +* **翻转与爆发(Promise Resolve):** LLM 返回 JSON 后,预警卡带有动画翻转为真正的【红色危机卡】,并开始倒计时。 +* **本地兜底 (Fallback):** 若请求超过 5 秒未返回,触发兜底逻辑,预警卡翻转为本地配置表中的随机通用危机(如【服务器宕机】)。 + +### 4.3 LLM 通信数据结构 (JSON 契约) +**System Prompt 要求 LLM 返回以下结构:** +```json +{ + "crisis_name": "内存泄漏灾难", + "description": "你的脏代码堆积如山,服务器马上就要炸了!", + "timer_seconds": 30, // 玩家解题的时间限制 + "penalty_type": "lose_money", // 超时惩罚:扣除资金 + "penalty_value": 20000, + "required_solution_tags": ["need_test_card", "need_focus_card"] // 解除危机需要的卡牌标签 +} +``` +**解题玩法:** 危机卡带有两个空槽(对应 `required_solution_tags`)。玩家必须在 30 秒内将具有相应 Tag 的卡牌(如 `测试员卡` 和 `专注力卡`)拖拽到危机卡上,读条 3 秒后危机解除,转化为奖励。超时则执行惩罚。 + +--- + +## 5. VibeCoding 开发任务拆解建议 + +为了充分利用 VibeCoding 的能力,建议您按照以下阶段向 AI (Cursor/Copilot) 提交 Prompt,逐步构建 MVP: + +### Phase 1: 核心前端框架与拖拽 (MVP) +* **技术栈选型:** React + TypeScript + Vite + TailwindCSS (或者 Zustand 用于全局状态管理)。 +* **核心任务 1:定义数据结构**。让 AI 帮您生成 `Card`, `Recipe`, `BoardState` 的 TypeScript Interface。 +* **核心任务 2:实现拖拽与碰撞判定 (Drag & Drop)**。不要用复杂的物理引擎,让 AI 写一个基于 HTML5 DnD API 或 `framer-motion` 的拖拽组件。判定逻辑为:“松开鼠标时,检查当前卡牌的坐标是否与另一张卡牌的包围盒重叠”。 +* **核心任务 3:实现倒计时状态机**。用 `useEffect` 或 `requestAnimationFrame` 实现两个重叠卡牌进入 `Processing` 状态,读条结束后触发回调生成新卡。 + +### Phase 2: 完善 JSON 配方表与职业系统 +* 将配方逻辑完全抽离为静态 JSON 配置。 +* **VibeCoding 优势:** 您只需写出 2 个示例配方,可以直接让大模型批量生成几十个符合“软件工程梗”的配方配置表。 + +### Phase 3: 接入 LLM 危机生成器 (API 联调) +* 编写 `useCrisisDirector` 自定义 Hook。 +* 实现“预警卡 -> API Fetch -> 解析 JSON -> 翻转为危机卡 -> 5秒超时本地 Fallback” 的完整异步逻辑链路。 + +### Phase 4: Roguelike 循环包装 +* 增加 Sprint 的总时间计时器。 +* 增加结算画面和 3 选 1 的 UI 组件。 diff --git a/02-用户故事和测试用例说明.md b/02-用户故事和测试用例说明.md new file mode 100644 index 0000000..66e543e --- /dev/null +++ b/02-用户故事和测试用例说明.md @@ -0,0 +1,57 @@ + + +# 📋 第一部分:用户故事 (User Stories) + +我们将整个游戏拆解为 4 个史诗任务 (Epics)。 + +### Epic 1: 桌面交互与核心卡牌系统 (The Board & Cards) +* **US1.1 自由拖拽:** 作为玩家,我希望能用鼠标在桌面上自由拖拽任意卡牌并放置在任何位置,以便我整理我的工作台。 +* **US1.2 职业与初始卡组:** 作为玩家,我希望在游戏开局时能选择职业(如开发、产品),以便获得该职业专属的初始手牌(如“开发人员”、“PRD文档”)和基础资金。 +* **US1.3 资源消耗与显示:** 作为玩家,我希望在屏幕顶部清晰看到当前 Sprint 的“剩余时间”和“项目资金”,以便我评估当前的生存压力。 + +### Epic 2: 堆叠合成与进度条 (Crafting & Processing) +* **US2.1 触发合成:** 作为玩家,我希望当我把一张卡牌(如“开发人员”)拖拽并覆盖到另一张有效卡牌(如“脏代码”)上时,它们会自动吸附并进入“合成中”状态。 +* **US2.2 进度条反馈:** 作为玩家,我希望在“合成中”的卡牌组上方看到一个倒计时进度条,以便我知道还需要等多久才能产出结果。 +* **US2.3 产出与概率:** 作为玩家,我希望进度条结束后,原有的消耗品卡牌消失,并在原位置弹射出新的产物卡(如“干净的代码”),并且支持概率掉落副产物(如 30% 掉落“Bug”卡)。 +* **US2.4 无效堆叠拒绝:** 作为玩家,我希望当我把两张没有配方关联的卡牌叠在一起时(如“咖啡”叠“咖啡”),它们不会发生任何反应,并在松开鼠标时弹开,防止误操作。 + +### Epic 3: LLM 动态危机系统 (The AI Director) +* **US3.1 预警卡掩盖延迟:** 作为玩家,我希望在系统即将降临危机时,桌面上会先掉落一张带有 Loading 动画的【⚠️ 甲方正在输入...】卡牌,以便在等待网络请求时给我提供真实的压迫感。 +* **US3.2 危机爆发:** 作为玩家,我希望 Loading 结束后,预警卡瞬间翻转为一张带有具体描述和倒计时的【红色危机卡】。 +* **US3.3 危机解除:** 作为玩家,我希望能在倒计时结束前,将带有对应 Tag 的卡牌(如带有 `need_test` 标签的“测试员卡”)拖入危机卡的解决槽位中,以消除危机并获得资金奖励。 +* **US3.4 危机惩罚:** 作为玩家,我希望如果危机卡倒计时归零我仍未解决,系统会立即执行惩罚(如扣除 50000 资金),若资金低于 0 则触发 Game Over。 + +### Epic 4: Roguelike 循环与结算 (The Loop) +* **US4.1 Sprint 结算:** 作为玩家,我希望在全局 Sprint 倒计时(如 3 分钟)结束且我存活时,系统暂停桌面,弹出胜利结算画面。 +* **US4.2 局外成长 (3选1):** 作为玩家,我希望在结算画面能看到 3 个随机的奖励选项(如新卡牌、永久遗物、删除垃圾卡),我可以选择其一加入下一轮,以获得肉鸽游戏的成长快感。 + +--- + +# 🧪 第二部分:核心测试用例 (Test Cases) + +这些测试用例主要针对**状态机逻辑**和**异步 API 处理**,这是 VibeCoding(AI 写代码)时最容易出 Bug 的地方。 + +### 模块一:合成状态机 (Crafting State Machine) + +| 用例编号 | 测试模块 | 测试标题 | 前置条件 | 操作步骤 | 预期结果 (Expected Result) | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **TC-1.1** | 合成引擎 | **基础配方成功合成** | 桌面上有一张“开发”和“脏代码”,配方需 5 秒。 | 1. 将“开发”拖拽到“脏代码”上并松开。
2. 等待 5 秒。 | 1. 两张卡被锁定,出现 5 秒进度条。
2. 5秒后,两张卡消失,原地生成一张“干净的代码”。 | +| **TC-1.2** | 合成引擎 | **合成中途强行打断 (Edge Case)** | “开发”与“脏代码”正在读条合成中(第 2 秒)。 | 1. 玩家用鼠标强行拖走处于底部的“脏代码”卡。 | 1. 进度条立即取消。
2. 两张卡恢复独立状态,不产出任何新卡,也不消耗原有卡牌。 | +| **TC-1.3** | 合成引擎 | **多重副产物概率掉落** | 触发“脏代码”+“Vibe助手”配方(设定 100%产出模块,20%产出Bug)。 | 1. 连续触发该配方 10 次并观察产出。 | 每次必定产出“可用模块”,大约有 2 次额外在旁边掉落一张“Bug”卡。 | + +### 模块二:LLM 危机触发与网络降级 (AI Director & Fallback) + +| 用例编号 | 测试模块 | 测试标题 | 前置条件 | 操作步骤 | 预期结果 (Expected Result) | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **TC-2.1** | LLM 系统 | **正常的危机生成链路** | 游戏运行到第 30 秒,触发危机心跳检测。网络正常,API 延迟为 2 秒。 | 1. 观察桌面变化。
2. 等待 2 秒后。 | 1. 第30秒时,桌面上生成一张【预警卡】并播放 Loading 动画。
2. 第32秒时,预警卡瞬间变成【危机卡】,显示 LLM 返回的 JSON 描述,倒计时开始。 | +| **TC-2.2** | LLM 系统 | **API 超时兜底机制 (断网测试)** | 拦截或断开本地网络,触发危机心跳检测。设定超时阈值为 5 秒。 | 1. 观察桌面出现【预警卡】。
2. 计时等待超过 5 秒。 | 1. 前 5 秒维持 Loading 状态。
2. 第 5.1 秒,触发兜底机制,预警卡变为本地 JSON 表中的随机通用危机(如“服务器宕机”),游戏不会崩溃卡死。 | +| **TC-2.3** | LLM 系统 | **危机成功解除逻辑** | 桌面上存在一张危机卡,要求 `need_pm` (需要产品) 标签。 | 1. 拖拽一张无标签的“咖啡”上去。
2. 拖拽一张带 `need_pm` 标签的“产品经理”上去。 | 1. 拖拽咖啡无效,弹开。
2. 拖拽产品经理后,危机卡被锁定读条 3 秒,随后危机卡消失,顶部资金增加。 | +| **TC-2.4** | LLM 系统 | **危机超时惩罚触发** | 桌面上存在一张“资金扣除”危机卡(倒计时 10 秒,惩罚 50000)。 | 1. 玩家不进行任何操作,等待 10 秒倒计时归零。 | 1. 倒计时变为 0,危机卡爆裂并消失。
2. 顶部面板的【项目资金】瞬间扣除 50000。 | + +### 模块三:核心循环与边界判定 (Core Loop Edge Cases) + +| 用例编号 | 测试模块 | 测试标题 | 前置条件 | 操作步骤 | 预期结果 (Expected Result) | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **TC-3.1** | 全局状态 | **资金归零触发 Game Over** | 玩家当前资金为 10000。桌面存在一张惩罚为扣除 20000 的危机卡。 | 1. 让该危机卡倒计时归零触发惩罚。 | 1. 资金被扣至 -10000。
2. 游戏立即暂停,弹出“项目破产 (Game Over)”结算弹窗,无法继续拖拽卡牌。 | +| **TC-3.2** | 全局状态 | **倒计时结束正常结算** | 当前 Sprint 全局倒计时剩余 3 秒,资金为 5000,且桌面有一张正在读条的危机卡。 | 1. 等待 3 秒全局倒计时归零。 | 1. 游戏立即暂停,危机卡倒计时冻结。
2. 弹出“Sprint 成功”弹窗,进入 3选1 的 Roguelike 奖励界面。 | + diff --git a/03-技术架构关键路径.md b/03-技术架构关键路径.md new file mode 100644 index 0000000..43665b8 --- /dev/null +++ b/03-技术架构关键路径.md @@ -0,0 +1,132 @@ + + +# 🏗️ 《Project Vibe: Ship It!》技术架构与关键路径文档 + +## 一、 技术栈选型 (Tech Stack) +*强烈建议在 Prompt 开头向 AI 明确这些技术栈,防止它自由发挥引入不兼容的库。* + +* **前端框架:** React 18 + Vite (纯前端单页应用,无需复杂后端)。 +* **开发语言:** TypeScript (*极其重要!严格的类型定义是 VibeCoding 不翻车的绝对保障*)。 +* **状态管理:** **Zustand** (*极其推荐!比 Redux 轻量,比 Context 性能好,非常适合做游戏的状态机更新*)。 +* **样式方案:** Tailwind CSS (*方便 AI 快速生成好看的 UI 卡片*)。 +* **拖拽与动画:** `framer-motion` (处理卡牌平滑移动和翻转动画) + 原生 Pointer Events (处理自由桌面坐标 x,y,**不要用**基于网格的 `dnd-kit`,因为我们要的是类似操作系统的无网格自由堆叠)。 + +--- + +## 二、 核心数据模型 (Data Models - TypeScript Interfaces) +*这是整个系统的灵魂。第一步请先让 AI 生成并完善这些类型定义文件 `types.ts`。有了它,AI 后续写的逻辑就不会乱。* + +```typescript +// 1. 卡牌基础模型 +export interface Card { + id: string; // 唯一实例ID (uuid) + templateId: string; // 对应配置表中的模板ID (如 'dirty_code', 'developer') + name: string; + type: 'resource' | 'actor' | 'module' | 'crisis' | 'warning'; + tags: string[]; // 用于危机判定 (如 ['need_test', 'human']) + position: { x: number; y: number }; // 桌面绝对坐标 + status: 'idle' | 'dragging' | 'processing' | 'locked'; + processEndTime?: number; // 如果在合成中/危机倒计时中,记录结束的绝对时间戳 +} + +// 2. 合成配方模型 (静态配置) +export interface Recipe { + id: string; + inputs: string[]; // 需要堆叠在一起的 templateId 列表 + timeMs: number; // 合成所需时间 (毫秒) + outputs: Array<{ templateId: string; chance: number }>; // 产物及概率 +} + +// 3. 全局状态模型 (Zustand Store) +export interface GameState { + // --- 资源状态 --- + funds: number; // 项目资金 + sprintTimeRemaining: number; // 当前 Sprint 倒计时 + sprintNumber: number; // 当前处于第几个 Sprint + + // --- 实体状态 --- + cards: Card[]; // 桌面上的所有卡牌 + + // --- 核心方法 (Actions) --- + moveCard: (id: string, x: number, y: number) => void; + checkCraftingOverlap: (cardId: string) => void; // 碰撞检测核心逻辑 + tick: () => void; // 游戏主循环 (Game Loop),每帧/每秒更新状态 + triggerCrisis: (crisisData: any) => void; // LLM 返回后触发危机 +} +``` + +--- + +## 三、 系统架构设计图 (单向数据流) + +游戏的架构必须是**数据驱动**的。UI 层只负责渲染 `cards` 数组和捕获鼠标事件。 + +```text + [ 玩家操作 (拖拽/松开) ] + │ + ▼ + [ 操作意图 (Pointer Events) ] ──发起请求──> [ LLM 危机生成器 (异步 Hook) ] + │ │ (返回JSON/超时Fallback) + ▼ ▼ + [ 碰撞检测算法 (Overlap Check) ] [ 触发预警卡/危机卡生成 ] + │ │ + ▼ (匹配 Recipe) │ + [ 状态机引擎 (Zustand Store) ] <─────────────────┘ + ├─ 更新卡牌坐标 (x, y) + ├─ 锁定卡牌状态 (status: 'processing') + └─ 结算资金/时间 (funds, sprintTime) + │ + ▼ (响应式数据) + [ React 渲染层 (UI Components) ] + ├─ (渲染桌面) + ├─ (渲染实体卡、倒计时进度条) + └─ (渲染顶部资金与时间) +``` + +--- + +## 四、 VibeCoding 关键开发路径 (Critical Paths) + +千万不要让 AI “一口气写一个游戏”。请严格按照以下 4 个阶段向 AI 提需求 (Prompting),每完成一个阶段就运行测试,确保无误后再进入下一阶段。 + +### 🚩 Path 1: 静态桌面与全局状态 (The Dumb Board) +* **目标:** 能在屏幕上看到几张卡牌,资金数值能够显示。 +* **AI 任务:** + 1. 初始化 Vite + React + TS 项目,安装 Zustand 和 Tailwind。 + 2. 创建 `types.ts`(使用上面提供的模型)。 + 3. 创建 `useGameStore.ts` (Zustand),初始化几个假数据卡牌 (Mock Data)。 + 4. 编写 `` 和 `` 组件。卡牌通过绝对定位 `absolute` 渲染在对应的 `x, y` 坐标上。 + +### 🚩 Path 2: 自由拖拽与物理碰撞 (Drag & Collision) +* **目标:** 卡牌可以被鼠标拖拽,松开鼠标时,能判断两张卡是否重叠。 +* **AI 任务:** + 1. 给 `` 添加 `onPointerDown`, `onPointerMove`, `onPointerUp` 事件。 + 2. 拖拽时,调用 Store 的 `moveCard` 更新坐标。 + 3. **核心算法要求:** 松开鼠标时,写一个 `checkOverlap(cardA, cardB)` 函数(通过判断两个矩形的边界 bounding box 是否相交)。如果重叠率超过 50%,则判定为“堆叠成功”。 + +### 🚩 Path 3: 核心主循环与合成引擎 (Game Loop & Crafting) +* **目标:** 两张配方卡重叠 -> 出现进度条 -> 读条结束产出新卡。 +* **AI 任务:** + 1. 创建一个静态的 `recipes.json`(包含 2-3 个基础配方,如 开发+脏代码=模块)。 + 2. 在 Store 中创建一个 `requestAnimationFrame` 或 `setInterval(..., 100)` 驱动的 `tick()` 函数(这就是游戏的心跳)。 + 3. **合成逻辑:** 当卡牌重叠,匹配 JSON 成功后,将这两张卡的 status 改为 `processing`,并设置 `processEndTime = Date.now() + timeMs`。 + 4. **心跳检测:** `tick()` 函数不断检查当前时间是否超过 `processEndTime`。如果超过,删除旧卡,在原坐标 `push` 新产物卡。 + +### 🚩 Path 4: LLM 异步危机总线 (The AI Director) +* **目标:** 每隔一段时间,根据桌面状态调用大模型,生成危机并处理延迟。 +* **AI 任务:** + 1. 编写一个独立的服务 `llmService.ts`。 + 2. 编写 `useCrisisDirector` 自定义 Hook。内部设置一个定时器(如每 30 秒触发一次)。 + 3. **时序逻辑编写:** + * 触发时:立刻在 Store 中推入一张 status 为 `processing` 的【预警卡】。 + * 发起 `fetch` 请求调用 OpenAI/DeepSeek API,附带当前桌面状态。 + * 使用 `Promise.race` 实现 5 秒超时控制。 + * 请求 Resolve (或超时 Fallback) 时:将那张【预警卡】的数据替换为真实的【危机卡】JSON 数据,并开始倒计时。 + +--- + +## 💡 给开发者的 VibeCoding 避坑指南 (Tips) + +1. 这是一个自由摆放的桌面,没有列表,只需要纯粹的 x,y 坐标更新和基于宽高的 AABB (Axis-Aligned Bounding Box) 碰撞检测。” +2. 在 Zustand store 里统一管理所有状态。**不要**在 `` 组件内部用 `useState` 去管理倒计时!所有的进度条渲染都应该基于 `processEndTime - Date.now()` 实时计算得出,这样即使组件卸载/重新渲染,进度也不会丢失。 +3. **Mock First:** 在写 LLM 接入时,用 `setTimeout` 模拟一个假的大模型返回。把游戏跑通了,最后再填入真实的 API Key 和 `fetch` 请求。 diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..7c21393 --- /dev/null +++ b/css/main.css @@ -0,0 +1,277 @@ +/* css/main.css */ + +/* 背景图案生成 */ +.bg-desktop-pattern { + background-image: radial-gradient(circle at 1px 1px, #334155 1px, transparent 0); + background-size: 24px 24px; +} + +/* 隐藏滚动条 */ +::-webkit-scrollbar { + display: none; +} + +/* 卡片基础样式 - 核心交互与视觉 */ +.game-card, .stack-group { + width: 170px; + height: 240px; + position: absolute; + border-radius: 14px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2); + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s ease; + display: flex; + flex-direction: column; + overflow: visible; /* 允许进度条等元素溢出 */ + border: 1px solid rgba(255, 255, 255, 0.1); + cursor: grab; + background-color: #1e293b; + color: #f8fafc; + will-change: transform, left, top; +} + +/* 正在被拖拽的状态 */ +.game-card.dragging, .stack-group.dragging { + cursor: grabbing; + transform: scale(1.08) rotate(2deg) translateY(-5px); + box-shadow: 0 25px 35px -5px rgba(0, 0, 0, 0.5), 0 10px 15px -5px rgba(0, 0, 0, 0.3); + z-index: 100 !important; + transition: none; /* 拖拽时取消缓动动画,跟随鼠标更紧密 */ +} + +/* 悬停时的微动效 */ +.game-card:hover:not(.dragging), .stack-group:hover:not(.dragging) { + transform: translateY(-4px) scale(1.02); + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2); + z-index: 50; +} + +/* 卡牌内部结构 */ +.card-header { + padding: 10px 14px; + font-size: 13px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255,255,255,0.08); + border-radius: 14px 14px 0 0; + backdrop-filter: blur(4px); +} + +.card-body { + flex: 1; + padding: 14px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0.2) 100%); +} + +.card-footer { + padding: 10px 12px; + font-size: 11px; + display: flex; + gap: 6px; + flex-wrap: wrap; + border-top: 1px solid rgba(255,255,255,0.08); + background: rgba(0,0,0,0.3); + border-radius: 0 0 14px 14px; +} + +.tag { + background: rgba(255,255,255,0.1); + padding: 3px 8px; + border-radius: 6px; + color: #cbd5e1; + font-weight: 500; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); +} + +/* ================= 职业卡系主题 ================= */ + +/* 实体角色 (Actor) */ +.card-actor { + background: linear-gradient(145deg, #1e3a8a, #1d4ed8); + border-color: rgba(59, 130, 246, 0.5); + box-shadow: 0 0 15px rgba(29, 78, 216, 0.2); +} + +/* 资源卡 (Resource) */ +.card-resource { + background: linear-gradient(145deg, #064e3b, #047857); + border-color: rgba(16, 185, 129, 0.4); +} + +/* 负面卡 (Negative) */ +.card-negative { + background: linear-gradient(145deg, #4c1d95, #6d28d9); + border-color: rgba(139, 92, 246, 0.4); +} + +/* 目标模块卡 (Module) */ +.card-module { + background: linear-gradient(145deg, #78350f, #92400e); + border-color: rgba(245, 158, 11, 0.5); +} + +/* 危机预警卡 (Warning) */ +.card-warning { + background: linear-gradient(145deg, #450a0a, #7f1d1d); + border-color: rgba(239, 68, 68, 0.6); + animation: pulse-border 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +/* 危机爆发卡 (Crisis) */ +.card-crisis { + background: linear-gradient(145deg, #2a0808, #5a1010); + border-color: #f87171; + box-shadow: 0 0 30px rgba(239, 68, 68, 0.4); +} + +@keyframes pulse-border { + 0%, 100% { + border-color: rgba(239, 68, 68, 0.3); + box-shadow: 0 0 15px rgba(239, 68, 68, 0.2); + } + 50% { + border-color: rgba(239, 68, 68, 0.9); + box-shadow: 0 0 25px rgba(239, 68, 68, 0.6); + } +} + +/* ================= 组件 ================= */ + +/* 进度条 (Processing Bar) */ +.progress-container { + position: absolute; + top: -18px; + left: 50%; + transform: translateX(-50%); + width: 110%; + height: 14px; + background: #0f172a; + border-radius: 8px; + border: 2px solid #334155; + overflow: hidden; + z-index: 20; + box-shadow: 0 4px 6px rgba(0,0,0,0.5); +} + +.progress-bar { + height: 100%; + background: linear-gradient(90deg, #3b82f6, #60a5fa); + border-radius: 6px; + box-shadow: inset 0 2px 4px rgba(255,255,255,0.3); + transition: width 0.1s linear; +} + +/* 危机槽位 (Slots) */ +.crisis-slot { + width: 44px; + height: 44px; + border: 2px dashed rgba(255,255,255,0.2); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + background: rgba(0,0,0,0.3); + transition: all 0.2s; +} + +.crisis-slot:hover { + border-color: rgba(255,255,255,0.5); + background: rgba(255,255,255,0.05); +} + +.crisis-slot.filled { + border: 2px solid #10b981; + background: rgba(16, 185, 129, 0.2); + box-shadow: 0 0 15px rgba(16, 185, 129, 0.3); +} + +/* Loading 动画 (API Requesting) */ +.loader { + border: 4px solid rgba(255,255,255,0.05); + border-top: 4px solid #f87171; + border-radius: 50%; + width: 42px; + height: 42px; + animation: spin 1s linear infinite; + box-shadow: 0 0 15px rgba(248, 113, 113, 0.2); +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* 栈 (Stack Group) 调整,取消单独卡牌样式因为外层是一个容器 */ +.stack-group { + background: transparent; + border: none; + box-shadow: none; +} + +/* Stack Group 内的卡牌取消自身悬停动效以防止冲突 */ +.stack-group .game-card { + position: absolute; + transition: none; +} + +.stack-group .game-card:hover { + transform: none; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4); +} + +/* 高亮可连接卡牌 (Highlight Connectable) */ +.highlight-connectable { + box-shadow: 0 0 20px 5px rgba(59, 130, 246, 0.6) !important; + border-color: #60a5fa !important; + transform: scale(1.05); + z-index: 80; +} + +/* 新卡牌生成弹现动效 */ +@keyframes card-spawn { + 0% { transform: scale(0) translateY(-20px); opacity: 0; filter: blur(10px); } + 60% { transform: scale(1.05) translateY(5px); opacity: 1; filter: blur(0); } + 100% { transform: scale(1) translateY(0); opacity: 1; filter: blur(0); } +} + +/* 正在被连线悬停 (Hovering Connection Target) */ +@keyframes magnetic-fusion { + 0% { transform: scale(1.1) rotate(0deg); box-shadow: 0 0 60px 20px rgba(59,130,246,0.9), inset 0 0 20px rgba(59,130,246,0.8); border-color: #60a5fa; filter: brightness(1.2); } + 33% { transform: scale(0.9) rotate(-3deg); box-shadow: 0 0 80px 30px rgba(139,92,246,0.9), inset 0 0 30px rgba(139,92,246,0.8); border-color: #c084fc; filter: brightness(1.5); } + 66% { transform: scale(1.1) rotate(3deg); box-shadow: 0 0 80px 30px rgba(236,72,153,0.9), inset 0 0 30px rgba(236,72,153,0.8); border-color: #f472b6; filter: brightness(1.5); } + 100% { transform: scale(1.1) rotate(0deg); box-shadow: 0 0 60px 20px rgba(59,130,246,0.9), inset 0 0 20px rgba(59,130,246,0.8); border-color: #60a5fa; filter: brightness(1.2); } +} + +.highlight-hover { + animation: magnetic-fusion 0.6s infinite ease-in-out !important; + border-width: 3px !important; + z-index: 90 !important; +} + +/* 危机卡悬停解救 (Hovering Crisis Resolution) */ +@keyframes crisis-resolve-hover { + 0%, 100% { transform: scale(1.15); box-shadow: 0 0 100px 40px rgba(16, 185, 129, 0.8); border-color: #34d399; filter: brightness(2) saturate(2); } + 50% { transform: scale(1.05); box-shadow: 0 0 60px 20px rgba(16, 185, 129, 0.6); border-color: #10b981; filter: brightness(1.5) saturate(1.5); } +} + +.highlight-crisis-hover { + animation: crisis-resolve-hover 0.5s infinite ease-in-out !important; + border-width: 4px !important; + z-index: 95 !important; +} + +/* 连线 SVG 画布 */ +#svg-connections { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; +} diff --git a/dashboard.html b/dashboard.html new file mode 100644 index 0000000..80491fa --- /dev/null +++ b/dashboard.html @@ -0,0 +1,157 @@ + + + + + + 工作台 - Project Vibe + + + + + + +
+

+ WORKSTATION 🚀 +

+
+ 工号: DEV-9527 + +
+
+ + +
+ + +
+
+
📝
+

新的 Sprint

+

你的上一个项目由于经费见底被公司强制终止。
准备好接手这个充满技术债的屎山新盘了吗?

+ +
+ +
+
🏆
+
+
历史最高纪录
+
总资金: $ 25,000
+
存活 Sprint: 3
+
+
+
+ + +
+
+

🗃️ 我的卡组 (Deck Library)

+ 当前已解锁 5/12 张 +
+ + +
+
加载卡池数据中...
+
+
+
+ + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..87c8d0d --- /dev/null +++ b/index.html @@ -0,0 +1,47 @@ + + + + + + Project Vibe: Ship It! - 完整流程高保真原型 + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..0c74621 --- /dev/null +++ b/js/main.js @@ -0,0 +1,1357 @@ +/** + * 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'); +}); diff --git a/login.html b/login.html new file mode 100644 index 0000000..f6a362e --- /dev/null +++ b/login.html @@ -0,0 +1,38 @@ + + + + + + 登录 - Project Vibe: Ship It! + + + + + +
+ +
+
+

+ Project Vibe +

+

>>> 研发协同终端 v2.0

+
+ +
+
+ + +
+
+ + +
+ + +
+
+ + \ No newline at end of file diff --git a/views/screen-game.html b/views/screen-game.html new file mode 100644 index 0000000..7386e76 --- /dev/null +++ b/views/screen-game.html @@ -0,0 +1,136 @@ + \ No newline at end of file diff --git a/views/screen-gameover.html b/views/screen-gameover.html new file mode 100644 index 0000000..dfb68a2 --- /dev/null +++ b/views/screen-gameover.html @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/views/screen-start.html b/views/screen-start.html new file mode 100644 index 0000000..f8c1967 --- /dev/null +++ b/views/screen-start.html @@ -0,0 +1,121 @@ +
+
+ +
+

+ Project Vibe: Ship It! +

+

>> 员工入职登记系统_ v1.0.4

+
+ +
+ +
+
👨‍💻
+

👨‍💻 开发人员

+

"一行代码,十个 Bug"

+
+
+ 资金 + $ 10,000 +
+
+ 初始牌组 +
+ 开发人员 + 专注力 x2 + 冰美式 +
+
+
+
+
+ + +
+
📊
+

📊 产品经理

+

"这个需求很简单,怎么实现我不管"

+
+
+ 资金 + $ 25,000 +
+
+ 初始牌组 +
+ 产品经理 + 白板 + 无意义会议 +
+
+
+
+
+ + +
+
🕵️
+

🕵️ 测试(QA)

+

"又重现不了了?这绝不是我的问题"

+
+
+ 资金 + $ 12,000 +
+
+ 初始牌组 +
+ 测试工程师 + 单元测试 + 系统 Bug +
+
+
+
+
+ + +
+
🛠️
+

🛠️ 运维(Ops)

+

"删库跑路?拔网线最快"

+
+
+ 资金 + $ 20,000 +
+
+ 初始牌组 +
+ 运维工程师 + 云服务器 + 安全补丁 +
+
+
+
+
+ + +
+
👶
+

👶 实习生(路人)

+

"大佬,我电脑蓝屏了..."

+
+
+ 资金 + $ 2,000 (真穷) +
+
+ 初始牌组 +
+ 实习生 + 技术债 +
+
+
+
+
+
+
\ No newline at end of file diff --git a/views/screen-win.html b/views/screen-win.html new file mode 100644 index 0000000..1b336bf --- /dev/null +++ b/views/screen-win.html @@ -0,0 +1,51 @@ + \ No newline at end of file