Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bed2599f5 | ||
|
|
174d1227c0 | ||
|
|
774ddaf9d1 | ||
|
|
22588425cf | ||
|
|
96748d726f | ||
|
|
20dc1f252f | ||
|
|
17820b1f28 | ||
|
|
1701a908c9 | ||
|
|
71ca5b48a9 | ||
|
|
a3aa381656 | ||
|
|
494bc5ff64 | ||
|
|
43dfc2d41b | ||
|
|
3d916a1574 | ||
|
|
4545fe380b | ||
|
|
8474f4caa7 | ||
|
|
c0535e6ebe | ||
|
|
7111e154b2 | ||
|
|
72e1f3fc3c | ||
|
|
fafa2ee20a | ||
|
|
fcf94b6c3f | ||
|
|
296c609479 | ||
|
|
bd28219b65 | ||
|
|
8605f9bb5f | ||
|
|
00a46a0bf9 | ||
|
|
084387174b | ||
|
|
ab511fe9e0 | ||
|
|
60445abd42 | ||
|
|
04f6a398f4 |
@@ -147,3 +147,4 @@ dev.out.log
|
|||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.vercel
|
.vercel
|
||||||
skills/
|
skills/
|
||||||
|
qa-*.png
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
959
|
1061
|
||||||
|
|||||||
@@ -1,35 +1,197 @@
|
|||||||
# 回响星核 / Echo Nexus
|
# 回响星核 / Echo Nexus
|
||||||
|
|
||||||
> 原创深空考古放置策略游戏 · Original deep-space archaeology idle strategy game
|
> 原创深空考古 **放置策略**游戏 · Original deep-space archaeology **idle strategy** game
|
||||||
|
|
||||||
你是一名深空考古指挥官,驾驶自治勘探舰回收已「飞升」的以太文明遗留的记忆晶体,解码碎片拼凑出他们的故事,并最终跨越维度发起「接触」。
|
你是一名深空考古指挥官,驾驶自治勘探舰回收已「飞升」的以太文明遗留的记忆晶体,解码碎片拼凑出他们的故事,并最终跨越维度发起「接触」。
|
||||||
|
|
||||||
|
**关掉浏览器,星核仍在为你工作**——采矿无人机舰队 24h 不间断采矿,放置工程在离线时持续推进,回来后一键领取积累的奖励。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎮 在线游玩
|
||||||
|
|
||||||
|
**👉 [https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/](https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/)**
|
||||||
|
|
||||||
|
无需安装,打开即玩。支持桌面(鼠标 + 键盘)与移动端(触控)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 项目状态
|
## 项目状态
|
||||||
|
|
||||||
- **版本**:v0.1(MVP 开发中)
|
- **版本**:**v0.16** — 解码阈值叙事事件(全屏碎片释放叠层)+ loreLayer 已读标记 + idleHistory 详情展开 + 移动端 375px 适配审计
|
||||||
- **栈**:Next.js 16 · TypeScript · Tailwind · shadcn/ui · Canvas · Zustand · Prisma
|
- **栈**:Next.js 16 (App Router) · TypeScript · Tailwind CSS 4 · shadcn/ui · Canvas 2D · Zustand · Web Audio API
|
||||||
- **仓库**:https://git.atdunbg.xyz/Super_Z/echo-nexus
|
- **仓库**:[https://git.atdunbg.xyz/Super_Z/echo-nexus](https://git.atdunbg.xyz/Super_Z/echo-nexus)
|
||||||
|
- **在线版**:[https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/](https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus/)
|
||||||
|
- **部署**:Gitea Pages(静态导出,`BUILD_EXPORT=true next build` → `out/` 推送 `gh-pages` 分支)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心特色
|
||||||
|
|
||||||
|
- **📡 采矿无人机舰队**:每解锁一个采矿分支技术,就有一台自动采矿无人机加入舰队,24h 持续产出晶体(无需点击)
|
||||||
|
- **⚙️ 放置工程项目**:6 种工程,派遣后**离线/在线自动推进**,完成领取奖励(晶体/洞见/能量/永久产能加成/随机碎片)
|
||||||
|
- **🔓 离线收益**:关闭浏览器后产能按 `offlineEff × crystalsPerSec` 持续累积(上限 8h),重新打开弹窗结算「沉睡期间」收益
|
||||||
|
- **🎯 始终可见的放置徽章**:Header 永远显示「放置中 +X/s」徽章,悬停查看产能构成明细——一眼就知道这是个 idle game
|
||||||
|
- **🔍 解码共振谜题**:把放置游戏的「等待」变成有节奏的主动小谜题(路径构造法生成器,100% 可解)
|
||||||
|
- **📜 碎片涌现叙事**:故事从解码中自然浮现,5 纪元循环命名 + 模板化 lore,不打断玩法
|
||||||
|
- **💎 深空全息美学**:晶体 + Canvas 粒子 + 共振波纹的差异化视觉(严格 emerald/rose/amber/fuchsia 四色全息色谱,零蓝色/靛色)
|
||||||
|
- **🔄 飞升多周目**:终局「接触」后保留蓝图开启新周目,星图天文台 18 个天赋跨周目永久生效
|
||||||
|
- **🚀 实时巡航玩法**:Canvas 2D 飞船操控 + 8 种实体 + 3 层视差星空,打破纯放置节奏
|
||||||
|
- **🧬 角色 RPG 深度**:四维属性(探索力/智慧/勇气/灵感)影响所有游戏系统
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 游戏系统(11 大系统)
|
||||||
|
|
||||||
|
| # | 系统 | 简介 | 类型 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | **📡 放置采矿** | 中央晶体球 Canvas 粒子系统 + 主动脉冲 + 连击加成 + **自动采矿无人机舰队** | 放置 |
|
||||||
|
| 2 | **⚙️ 放置工程** | 6 种工程 × 3 槽位,**离线/在线自动推进**,完成领取奖励 | **放置** |
|
||||||
|
| 3 | **🔓 离线收益** | 关闭浏览器后产能按 `eff × cps` 累积(8h 上限),重新打开结算 | **放置** |
|
||||||
|
| 4 | 谐振解码 | 原创路径谜题,100% 可解率 | 主动 |
|
||||||
|
| 5 | 技术树 | 三层科技解锁,提升产能/解码/探险 | 主动 |
|
||||||
|
| 6 | 遗迹探险 | 肉鸽系统,6 种节点 + 程序化路径 + BOSS | 主动 |
|
||||||
|
| 7 | 星潮事件 | 6 种动态事件,全屏叠层 + 程序化音效 | 被动 |
|
||||||
|
| 8 | 星图天文台 | 飞升后 3 选 1 天赋 draft,18 个跨周目天赋 | 元进程 |
|
||||||
|
| 9 | 回响编年史 | 跨周目叙事时间轴,5 纪元循环命名 | 元进程 |
|
||||||
|
| 10 | 深空信标 | 每日挑战 + 本地排行榜 Top20 | 每日 |
|
||||||
|
| 11 | 深空巡航 | Canvas 2D 实时玩法,飞船操控/陨石躲避/星门通关 | 实时 |
|
||||||
|
| 12 | 角色属性 | 四维属性(探索力/智慧/勇气/灵感),飞升获得属性点 | 元进程 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 放置系统设计哲学(v0.14 + v0.15 平衡)
|
||||||
|
|
||||||
|
回响星核的「放置」不是简单地把数字挂机增长,而是分层呈现 **3 种 idle 节奏**:
|
||||||
|
|
||||||
|
### 节奏 1:被动产能(最慢,最持续)
|
||||||
|
- 采矿无人机舰队 = 你已解锁的采矿技术节点
|
||||||
|
- 每台无人机 24h 持续产出晶体,无需任何操作
|
||||||
|
- 晶体进入仓库(受 `crystalCap` 限制),仓库满后停止产出(防溢出)
|
||||||
|
- **永远在线,离线也累积**
|
||||||
|
|
||||||
|
### 节奏 2:放置工程(中等周期,需要决策)
|
||||||
|
- 6 种工程,时长 1min ~ 1h 不等
|
||||||
|
- 玩家选择派遣到 3 个槽位之一 → 启动后**离线/在线自动推进**
|
||||||
|
- 完成后必须**手动领取**(防玩家错过奖励时机)
|
||||||
|
- 奖励类型多样:晶体 / 洞见 / 能量 / 永久产能加成 / 随机碎片
|
||||||
|
- **核心 idle loop:派遣 → 离开 → 回来 → 领取 → 再派遣**
|
||||||
|
|
||||||
|
### 节奏 3:离线收益(最长周期,回归奖励)
|
||||||
|
- 关闭浏览器后,被动产能按 `offlineEff`(默认 50%,可通过技术提升至 100%)继续累积
|
||||||
|
- 上限 8 小时(防极端挂机)
|
||||||
|
- 重新打开页面弹窗结算:「你离开了 X 小时,星核为你产出 Y 颗晶体」
|
||||||
|
- **回归玩家的"啊我回来了"瞬间**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开发路线
|
||||||
|
|
||||||
|
| 版本 | 核心内容 | 状态 |
|
||||||
|
|------|---------|------|
|
||||||
|
| v0.1 | 放置 + 解码 + 技术树 + 图谱 + 飞升 | ✅ |
|
||||||
|
| v0.1.1 | 解码可解性修复(路径构造法) | ✅ |
|
||||||
|
| v0.2 | 遗迹探险肉鸽系统 | ✅ |
|
||||||
|
| v0.2.1 | 程序化音频 + 14 项成就 | ✅ |
|
||||||
|
| v0.3 | 星潮事件系统 | ✅ |
|
||||||
|
| v0.3.1 | 星图天文台元进程 | ✅ |
|
||||||
|
| v0.4 | 回响编年史系统 | ✅ |
|
||||||
|
| v0.5 | 深空信标系统 | ✅ |
|
||||||
|
| v0.5.1 | Gitea Pages 静态部署 + 新手教程 | ✅ |
|
||||||
|
| v0.5.2 | 离线收益报告 + 标签页 UI 重设计 | ✅ |
|
||||||
|
| v0.6 | 深空巡航 Canvas 2D 实时玩法 | ✅ |
|
||||||
|
| v0.7 | CrystalOrb Canvas 粒子系统 + 四维角色属性 | ✅ |
|
||||||
|
| v0.8 | 巡航玩法增强 / BOSS 关卡 / 道具掉落 | ✅ |
|
||||||
|
| v0.8.1 | 探险平衡 + 信标系统扩展(周挑战 + 信标链) | ✅ |
|
||||||
|
| **v0.14** | **放置系统:采矿无人机舰队 + 6 种放置工程 + 永久产能加成 + 始终可见 idle 徽章** | ✅ |
|
||||||
|
| **v0.15** | **叙事深化(8 碎片 × 2 层 = 16 层 loreLayers)+ 永久加成 softcap + 批量领取 + 完成 toast + StatsPanel idle 统计** | ✅ |
|
||||||
|
| **v0.16** | **解码阈值叙事事件(5 里程碑全屏叠层)+ loreLayer 已读标记 + idleHistory 详情展开 + 移动端 375px 适配审计** | ✅ |
|
||||||
|
| v0.17 | 星潮三频段切换 + 健康设计(每日软上限 + 星核建议休眠) | 🔜 |
|
||||||
|
| v1.0 | 全 5 纪元手写叙事 + 多人同步星潮 | 🔜 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- [`docs/01-调研报告.md`](docs/01-调研报告.md) — 游戏市场调研
|
- [`docs/01-调研报告.md`](docs/01-调研报告.md) — 游戏市场调研
|
||||||
- [`docs/02-游戏设计文档.md`](docs/02-游戏设计文档.md) — GDD 策划设计
|
- [`docs/02-游戏设计文档.md`](docs/02-游戏设计文档.md) — GDD 策划设计
|
||||||
- [`docs/03-技术架构.md`](docs/03-技术架构.md) — 技术架构
|
- [`docs/03-技术架构.md`](docs/03-技术架构.md) — 技术架构
|
||||||
|
- [`docs/04-解码系统修复-v0.1.1.md`](docs/04-解码系统修复-v0.1.1.md) — v0.1.1 解码修复
|
||||||
## 核心特色
|
- [`docs/05-遗迹探险系统-v0.2.md`](docs/05-遗迹探险系统-v0.2.md) — v0.2 探险系统
|
||||||
|
- [`docs/06-音频与成就系统-v0.2.1.md`](docs/06-音频与成就系统-v0.2.1.md) — v0.2.1 音频与成就
|
||||||
- **解码共振谜题**:把放置游戏的「等待」变成有节奏的主动小谜题
|
- [`docs/07-星潮事件系统-v0.3.md`](docs/07-星潮事件系统-v0.3.md) — v0.3 星潮
|
||||||
- **碎片涌现叙事**:故事从解码中自然浮现,不打断玩法
|
- [`docs/08-星图天文台系统-v0.3.1.md`](docs/08-星图天文台系统-v0.3.1.md) — v0.3.1 天文台
|
||||||
- **深空全息美学**:晶体 + 粒子 + 共振波纹的差异化视觉
|
- [`docs/09-回响编年史系统-v0.4.md`](docs/09-回响编年史系统-v0.4.md) — v0.4 编年史
|
||||||
- **飞升多周目**:终局「接触」后保留蓝图开启新周目
|
- [`docs/10-深空信标系统-v0.5.md`](docs/10-深空信标系统-v0.5.md) — v0.5 信标
|
||||||
|
- [`docs/11-放置系统-v0.14.md`](docs/11-放置系统-v0.14.md) — **v0.14 放置系统(idle operations)**
|
||||||
## 开发路线
|
- [`docs/12-叙事深化-v0.15.md`](docs/12-叙事深化-v0.15.md) — **v0.15 loreLayers 多层碎片文本 + softcap + 批量领取**
|
||||||
|
- [`docs/13-叙事事件-v0.16.md`](docs/13-叙事事件-v0.16.md) — **v0.16 解码阈值里程碑叠层 + loreLayer 已读 + 移动端适配**
|
||||||
- v0.1 放置 + 解码 + 技术树 + 图谱 + 飞升(当前)
|
|
||||||
- v0.2 遗迹探险肉鸽
|
|
||||||
- v0.3 音频 + 全局星潮 socket 事件
|
|
||||||
- v0.4 云存档 + 排行榜
|
|
||||||
- v0.5 全 5 纪元叙事 + 隐藏内容
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# 启动 dev 服务器(http://localhost:3000)
|
||||||
|
bun run dev
|
||||||
|
|
||||||
|
# 静态导出(用于 Gitea Pages 部署)
|
||||||
|
BUILD_EXPORT=true bun run build:static
|
||||||
|
# 产物在 out/ 目录,basePath 为 /Super_Z/echo-nexus
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
bun run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 部署到 Gitea Pages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 静态导出
|
||||||
|
BUILD_EXPORT=true bun run build:static
|
||||||
|
|
||||||
|
# 2. 推送到 gh-pages 分支
|
||||||
|
cd out
|
||||||
|
git init && git checkout -b gh-pages
|
||||||
|
git add -A
|
||||||
|
git commit -m "deploy: v0.16"
|
||||||
|
git remote add origin https://git.atdunbg.xyz/Super_Z/echo-nexus.git
|
||||||
|
git push -f origin gh-pages
|
||||||
|
```
|
||||||
|
|
||||||
|
Gitea Pages 会自动从 `gh-pages` 分支提供静态网站服务。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 灵感来源(Inspired by)
|
||||||
|
|
||||||
|
- **Antimatter Dimensions** — 数值膨胀美学 + 终端 UI
|
||||||
|
- **Universal Paperclips** — 叙事驱动的放置游戏(Frank Lantz)
|
||||||
|
- **A Dark Room** — 极简文字放置游戏(Michael Townsend)
|
||||||
|
- **Melvor Idle** — 离线收益 + 多技能树
|
||||||
|
- **Outer Wilds** — 知识解锁式考古
|
||||||
|
- **Cookie Clicker** — Orteil 的放置游戏范式定义
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 健康游戏设计声明
|
||||||
|
|
||||||
|
放置游戏天然具有"低投入高回报"的成瘾性,可能对学业/工作压力大的青年造成时间碎片化风险。回响星核主动采取以下健康设计:
|
||||||
|
|
||||||
|
- ✅ **离线收益上限 8h**——防止 24h 挂机
|
||||||
|
- ✅ **工程完成需手动领取**——避免"打开就有奖励"的条件反射
|
||||||
|
- ✅ **无广告、无内购**——纯粹的单机体验
|
||||||
|
- 🔜 **v0.17 计划**:每日软上限 + "星核建议休眠"提醒(每 1 小时主动提示休息)
|
||||||
|
|
||||||
|
参考学术研究:
|
||||||
|
- Frank Lantz GDC 2018 "This Is Your Brain on Games"
|
||||||
|
- Reuters 2025 "Cozy video games can quell stress and anxiety"
|
||||||
|
- DOI:10.3389/fpsyg.2026.1774030 TikTok 时代学术倦怠研究
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
© Super_Z · 持续开发中
|
© Super_Z · 持续开发中
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
Task ID: 2-b
|
||||||
|
Agent: full-stack-developer
|
||||||
|
Task: v0.16 完善 — loreLayer 已读标记 + idleProjectHistory 详情展开 + 移动端 375px 适配审计
|
||||||
|
|
||||||
|
Work Log:
|
||||||
|
- 必读上下文:worklog v0.15-release 段 + Codex.tsx(v0.15 loreLayers 分层渲染)+ StatsPanel.tsx(v0.15 抽出独立组件)+ types.ts GameState + config.ts FRAGMENTS/loreLayers/isLoreLayerUnlocked + gameStore.ts(actions + persist migrate)+ IdleOperationsPanel.tsx + BeaconPanel.tsx + ExpeditionPanel.tsx
|
||||||
|
- 子任务 1:loreLayer 已读标记
|
||||||
|
- 新建 `src/lib/game/format.ts` 导出 `formatRelativeTime(ts)` 工具函数(<60s "刚刚" / <60m "X 分钟前" / <24h "X 小时前" / <7d "X 天前" / 否则具体日期)
|
||||||
|
- `types.ts`:GameState 接口新增 `readLoreLayers: string[]`(位置:v0.14 idle 字段之后、元字段之前,注释标 v0.16 loreLayer 已读)
|
||||||
|
- `engine.ts`:
|
||||||
|
* `createInitialState` 末尾加 `readLoreLayers: []`(在 idleStats 之后)
|
||||||
|
* `performPrestige` 返回对象加 `readLoreLayers: Array.isArray(state.readLoreLayers) ? state.readLoreLayers : []`(跨周目永久保留,与 idle 永久字段同一区域)
|
||||||
|
- `gameStore.ts`:
|
||||||
|
* GameActions 接口加 `markLoreLayerRead: (fragmentId: string, layer: 1 | 2) => void`(在 tickIdleProjects 之后)
|
||||||
|
* 实现该 action:构造 key `${fragmentId}_L${layer}`,已存在则 no-op,否则 set 新数组
|
||||||
|
* persist migrate 函数末尾加 `readLoreLayers: Array.isArray(s.readLoreLayers) ? s.readLoreLayers : []`(不改 version,仍是 1)
|
||||||
|
- `Codex.tsx`:
|
||||||
|
* useShallow 订阅新增 `readLoreLayers` 与 `markLoreLayerRead`
|
||||||
|
* LoreLayerBlock 接收 `isRead` + `onMarkRead` props:
|
||||||
|
- 已读:层标题旁加 muted "✓ 已读" 标记 + 边框从 amber/fuchsia 高亮降级为 `border-white/10`
|
||||||
|
- 未读但已解锁:保持 v0.15 高亮 + 标题旁加 `animate-pulse` 脉冲小圆点 + "新" 字提示
|
||||||
|
- 文本区域包成 `<button>` 元素,点击调用 `onMarkRead`(自然交互,不另设按钮)
|
||||||
|
- 未读时下方加一行 muted "▸ 点击文本标记已读" 引导文字
|
||||||
|
* 列表卡片标题旁小圆点:未读时 `animate-pulse` + glow,已读时 `bg-*-400/30` 静态
|
||||||
|
* 顶部统计从单行"深层叙事 X/Y 已解锁"扩展为两行(移动端 stack / 桌面并排):深层叙事 X/Y 已解锁 + 已读 A/B(A=已读数,B=已解锁层数)
|
||||||
|
* 弹层 padding 从 `p-5` 改 `p-4 sm:p-5`,标题 `text-lg` 改 `text-base sm:text-lg`,外层 backdrop padding 从 `p-4` 改 `p-3 sm:p-4`(375px 适配)
|
||||||
|
|
||||||
|
- 子任务 2:idleProjectHistory 详情展开
|
||||||
|
- `StatsPanel.tsx`:
|
||||||
|
* useShallow 订阅新增 `idleProjectHistory` 数组与 `idleProjectSlots`(用于详情中潜在"进行中"状态参考)
|
||||||
|
* 删除原"放置工程历史 N 条"的简单行,改为 `col-span-2` 可展开按钮:
|
||||||
|
- 左侧 label + "(最近 5 条可展开)" 提示(仅 historyLen > 0 时显示)
|
||||||
|
- 右侧 "N 条" + ChevronDown/ChevronUp 图标(lucide-react)
|
||||||
|
* 展开后渲染 `col-span-2` 卡片:
|
||||||
|
- 空历史 → "暂无放置工程完成记录 · 前往「放置」标签派遣工程"
|
||||||
|
- 非空 → 倒序最近 5 条:每行 = 工程图标 + 工程名(truncate)+ 相对时间(formatRelativeTime)
|
||||||
|
- 最近一条用 `bg-fuchsia-500/5` 高亮
|
||||||
|
- 历史超 5 条时底部加 "仅展示最近 5 条 · 共 N 条历史" 提示
|
||||||
|
* 展开状态用本地 `useState<boolean>`
|
||||||
|
* useMemo 派生 `recentHistory`(避免每次渲染重切)
|
||||||
|
|
||||||
|
- 子任务 3:移动端 375px 适配审计
|
||||||
|
- 审计范围:Codex / IdleOperationsPanel / StatsPanel / BeaconPanel / ExpeditionPanel
|
||||||
|
- 修复点:
|
||||||
|
* `IdleOperationsPanel.tsx`:所有 `text-[9px]` → `text-[10px]`(11 处,符合"移动端字号最小 10px"规范)
|
||||||
|
* `BeaconPanel.tsx`:所有 `text-[9px]` → `text-[10px]`(7 处);信标链底部统计行 `flex items-center justify-between gap-2` → `flex flex-wrap items-center justify-between gap-x-2 gap-y-1`(防 3 组数据在 375px 溢出)
|
||||||
|
* `Codex.tsx`:弹层 padding + 标题字号响应式(见子任务 1);顶部统计区 `flex flex-col sm:flex-row`(移动端纵向堆叠)
|
||||||
|
* `StatsPanel.tsx`:detail 区 `text-[9px]` → `text-[10px]`(2 处)
|
||||||
|
- 审计通过(无需改):
|
||||||
|
* `ExpeditionPanel.tsx`:节点路径已用 `overflow-x-auto` 自适应窄屏;事件卡 p-3 padding 合理;按钮组 `flex-1` + abort 收起;能量条文字 `text-[10px]` 已达标;路径节点 `h-9 w-9` 36px 足够小且可横向滚动
|
||||||
|
* `BeaconPanel.tsx`:4 个里程碑节点 `w-12 h-12`(48px)× 4 = 192px + flex-1 分布,375px 下可容纳;日/周挑战卡 padding p-3 合理;周挑战+信标链已用 `grid-cols-1 lg:grid-cols-2` 移动单列
|
||||||
|
* `IdleOperationsPanel.tsx`:4 个 section 已用 `grid-cols-1 sm:grid-cols-N` / `grid-cols-2 sm:grid-cols-3` 响应式降级;DispatchCard 槽位按钮 `h-5 w-7` × 3 在单列布局下不溢出
|
||||||
|
* `Codex.tsx` 列表 `grid-cols-2 gap-2`:每卡片约 165px 宽,aspect-[4/3] + p-2 + text-[11px]/text-[9px](已升 text-[10px])在 375px 下可读
|
||||||
|
|
||||||
|
- 严格 4 色全息:emerald / rose / amber / fuchsia,未引入 blue / indigo
|
||||||
|
- useShallow:所有 store 订阅用 useShallow(Codex / StatsPanel 均已用)
|
||||||
|
- 持久化兼容:persist version 仍为 1,新字段 `readLoreLayers` 靠 migrate 兜底,旧存档加载时自动补 `[]`
|
||||||
|
- 与子代理 A 文件边界:types.ts/engine.ts/gameStore.ts 三个共享文件,A 的 `triggeredMilestones` / `pendingMilestone` / `dismissMilestone` 字段已落地,与本子代理的 `readLoreLayers` / `markLoreLayerRead` 完全无冲突(不同字段、不同 action、不同 migrate 行)
|
||||||
|
|
||||||
|
Stage Summary:
|
||||||
|
- ✅ 子任务 1:loreLayer 已读标记完整实现
|
||||||
|
- 类型 + store + UI 三层联动;点击层文本即可标记已读
|
||||||
|
- 视觉区分:已读 = muted "✓ 已读" + 边框降级为 border-white/10;未读 = 高亮 + 脉冲小圆点 + "新" 字
|
||||||
|
- 顶部统计新增"已读 A/B"行(A=已读数 / B=已解锁层数)
|
||||||
|
- 跨周目永久保留(performPrestige 不重置)
|
||||||
|
- ✅ 子任务 2:idleProjectHistory 详情展开完整实现
|
||||||
|
- "放置工程历史" 行可点击展开/收起
|
||||||
|
- 展开后显示最近 5 条倒序历史(图标 + 工程名 + 相对时间)
|
||||||
|
- 空态友好提示,超 5 条底部加"仅展示最近 5 条"说明
|
||||||
|
- ✅ 子任务 3:移动端 375px 适配审计完成
|
||||||
|
- 审 5 个组件,修 4 个(Codex / IdleOps / StatsPanel / Beacon),ExpeditionPanel 无需改
|
||||||
|
- 主要修复:text-[9px] → text-[10px](共 20 处,符合"移动端字号最小 10px"规范)
|
||||||
|
- 信标链底部统计加 flex-wrap 防 3 组数据溢出
|
||||||
|
- Codex 弹层 padding + 标题字号响应式
|
||||||
|
- Codex 顶部统计区移动端纵向堆叠
|
||||||
|
- ✅ 新建文件:`/home/z/my-project/src/lib/game/format.ts`
|
||||||
|
- ✅ 修改文件清单(绝对路径):
|
||||||
|
- `/home/z/my-project/src/lib/game/types.ts`(+readLoreLayers 字段)
|
||||||
|
- `/home/z/my-project/src/lib/game/engine.ts`(createInitialState 默认 + performPrestige 保留)
|
||||||
|
- `/home/z/my-project/src/store/gameStore.ts`(GameActions 签名 + action 实现 + migrate 补全)
|
||||||
|
- `/home/z/my-project/src/components/game/Codex.tsx`(已读标记 + 点击逻辑 + 顶部统计 + 弹层响应式)
|
||||||
|
- `/home/z/my-project/src/components/game/StatsPanel.tsx`(idleHistory 展开 + 相对时间 + 字号升级)
|
||||||
|
- `/home/z/my-project/src/components/game/IdleOperationsPanel.tsx`(字号 9→10px 升级)
|
||||||
|
- `/home/z/my-project/src/components/game/BeaconPanel.tsx`(字号 9→10px 升级 + 信标链底部 flex-wrap)
|
||||||
|
- ✅ bun run lint 零错误零警告
|
||||||
|
- ✅ HTTP 200,dev.log 无运行时错误(compile 11ms / render 33ms)
|
||||||
|
- ✅ 与子代理 A 文件边界完全无冲突(不同字段/action/migrate 行)
|
||||||
|
- ⚠️ 未做 agent-browser 端到端验证(按工单由主控完成)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Task 6: 开发 Canvas 2D 深空巡航实时玩法
|
||||||
|
|
||||||
|
## Agent: full-stack-developer
|
||||||
|
|
||||||
|
## 完成状态: ✅ 全部完成
|
||||||
|
|
||||||
|
## 创建的文件
|
||||||
|
1. `/home/z/my-project/src/lib/game/cruise.ts` — 逻辑层(~520 行纯 TS)
|
||||||
|
2. `/home/z/my-project/src/components/game/CruiseMode.tsx` — Canvas 2D 渲染层(~830 行)
|
||||||
|
|
||||||
|
## 修改的文件
|
||||||
|
1. `/home/z/my-project/src/app/page.tsx` — 新增巡航入口按钮 + CruiseMode 渲染
|
||||||
|
|
||||||
|
## 关键实现细节
|
||||||
|
|
||||||
|
### cruise.ts 逻辑层
|
||||||
|
- **类型系统**:CruiseEntity 联合类型(8 种实体:ship/asteroid/storm/crystal/insight/beacon/stargate/particle),完整 CruiseState/Phase/RunResult/Stats/Input
|
||||||
|
- **关卡生成**:mulberry32 + FNV-1a 种子化 RNG;陨石(8+level×2)、风暴(1+level/2)、晶体(5+level)、洞见(2+level/3)、信标(1+level/4)、星门;时长 60-90s
|
||||||
|
- **物理**:飞船 8 方向加速度+摩擦+限速+边界反弹;陨石碰撞扣盾+击退+600ms 无敌;风暴持续 DPS;收集物吸引半径 90px 自动吸入
|
||||||
|
- **奖励**:crystals=碎片×(8+lvl×2)、insights=洞见×(2+lvl×0.5)、contact=信标×(1.5+lvl×0.3);通关×1.5、失败×0.5
|
||||||
|
- **localStorage**:独立 key `echo-nexus-cruise-v1`,记录最高分/通关数/最高关卡/累计奖励/最近 20 局
|
||||||
|
|
||||||
|
### CruiseMode.tsx 渲染层
|
||||||
|
- **全屏 Canvas**:fixed inset-0 z-50,DPR cap 2,resize 监听
|
||||||
|
- **3 层视差星空**:远/中/近 100/60/30 星,独立漂移+飞船速度视差+闪烁
|
||||||
|
- **实体辉光绘制**:全部使用 ctx.shadowBlur;飞船三角形+尾焰渐变;陨石不规则多边形;风暴 5 层云团+闪电;晶体菱形+脉冲环;洞见球体+漩涡;信标光束+旋转环;星门 4 层旋涡+吸入粒子
|
||||||
|
- **粒子系统**:尾焰/收集/碰撞/烟花,上限 200
|
||||||
|
- **屏幕震动**:受击 280ms 衰减抖动
|
||||||
|
- **低护盾警告**:shield<30% 屏幕边缘 rose 脉冲边框
|
||||||
|
- **HUD**:HTML 叠层(非 canvas),glass+backdrop-blur;护盾/能量/分数/用时/收集计数;80ms 节流更新
|
||||||
|
- **控制**:桌面 WASD/方向键;移动端 floating 虚拟摇杆;P 暂停;Esc 退出;失焦自动暂停
|
||||||
|
- **奖励同步**:结算时 grantCruiseReward + recordRun,rewardGrantedRef 防重
|
||||||
|
|
||||||
|
### page.tsx 集成
|
||||||
|
- header 按钮区新增「巡航」按钮(Navigation 图标,amber 主题)
|
||||||
|
- 新增 cruiseOpen state
|
||||||
|
- 组件末尾渲染 {cruiseOpen && <CruiseMode onClose={...} />}
|
||||||
|
- 未破坏现有 7 标签页和其他功能
|
||||||
|
|
||||||
|
## QA 验证结果
|
||||||
|
- ✅ lint 零错误
|
||||||
|
- ✅ 巡航按钮出现在顶部 header
|
||||||
|
- ✅ 点击进入全屏 Canvas,ready 界面完整
|
||||||
|
- ✅ WASD 控制飞船移动,按 W 直冲星门通关
|
||||||
|
- ✅ 收集 2 晶体 → 奖励 +30(2×10×1.5=30 计算正确)
|
||||||
|
- ✅ localStorage 正确记录 highScore/totalWins/bestLevel
|
||||||
|
- ✅ 奖励同步到 gameStore
|
||||||
|
- ✅ 退出返回主界面,7 标签页完好
|
||||||
|
- ✅ 移动端 viewport 测试通过
|
||||||
|
|
||||||
|
## 色彩遵循
|
||||||
|
emerald(#34d399) / rose(#fb7185) / amber(#fbbf24) / fuchsia(#e879f9) 四色全息色谱,零蓝色/靛色
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Task 7: 角色属性系统(探索力/智慧/勇气/灵感)
|
||||||
|
|
||||||
|
## Agent: full-stack-developer
|
||||||
|
|
||||||
|
## 完成状态: ✅ 全部完成
|
||||||
|
|
||||||
|
## 创建的文件
|
||||||
|
1. `/home/z/my-project/src/lib/game/attributes.ts` — 属性逻辑层(~330 行)
|
||||||
|
2. `/home/z/my-project/src/components/game/AttributesPanel.tsx` — 属性 UI 面板(~330 行)
|
||||||
|
|
||||||
|
## 修改的文件
|
||||||
|
1. `/home/z/my-project/src/lib/game/types.ts` — GameState 新增 attributes/attributeProgress/pendingAttrPoints
|
||||||
|
2. `/home/z/my-project/src/lib/game/config.ts` — INITIAL_STATE 补全新字段默认值
|
||||||
|
3. `/home/z/my-project/src/lib/game/engine.ts` — recomputeStats 聚合属性加成;performPrestige 发放属性点
|
||||||
|
4. `/home/z/my-project/src/store/gameStore.ts` — 新增 allocateAttribute/gainAttributeExp action;pulse/clickNode/autoDecodeTick/resolveCurrentNode/grantCruiseReward/tickTide/init 接入属性逻辑;旧存档兼容
|
||||||
|
5. `/home/z/my-project/src/lib/game/achievements.ts` — 新增 2 项属性成就
|
||||||
|
6. `/home/z/my-project/src/app/page.tsx` — 新增第 8 个「角色」标签页 + grid-cols-7→8 + 红点提示 + 统计面板新增属性行 + 版本号 v0.6→v0.7
|
||||||
|
|
||||||
|
## 关键实现细节
|
||||||
|
|
||||||
|
### attributes.ts 逻辑层
|
||||||
|
- **类型系统**:AttributeKey(4 个键)/ CharacterAttributes(0-100 数值)/ AttributeProgressEntry(exp + level)
|
||||||
|
- **加成公式**:
|
||||||
|
- 0-50 线性区:每点 +0.5% 加成(0..50 → 0..25%)
|
||||||
|
- 50-100 递减区:每点 +0.2% 加成(50..100 → 25..35%)
|
||||||
|
- 超过 100 仍按 100 计算加成(软上限)
|
||||||
|
- **getAttributeBonus(attr)** 单属性加成百分比
|
||||||
|
- **getAllBonuses(attrs)** 返回 12 个修饰器:探险力倍率/巡航速度/解码步数/洞见倍率/自动解码周期/探险生命/BOSS 胜率/巡航护盾/接触率/星潮触发/脉冲连击/产能加成
|
||||||
|
- **levelUpCheck(progress)** 自动跨多级升级,安全上限 200 次循环
|
||||||
|
- **expRequiredForLevel(level) = 10 × level**(最低 10)
|
||||||
|
- **computePrestigeAttrPoints(ascensions) = ascensions × 2 + 1**(飞升前次数计算)
|
||||||
|
- **migrateAttributes(state)** 旧存档兼容:补全缺失字段、夹紧越界值、同步 level 与 attributes
|
||||||
|
|
||||||
|
### engine.ts 改造
|
||||||
|
- recomputeStats 末尾追加 `getAllBonuses(state.attributes ?? {})` 聚合:
|
||||||
|
- `crystalsPerSec *= am.crystalsPerSecMult`
|
||||||
|
- `insightMult += am.insightMultAdd`
|
||||||
|
- `contactRateMult *= am.contactRateMult`
|
||||||
|
- `decodeStepsBonus += am.decodeStepsBonus`
|
||||||
|
- performPrestige:
|
||||||
|
- 保留 attributes 数值(跨周目永久)
|
||||||
|
- 清空 attributeProgress(新周目重新累积经验)
|
||||||
|
- pendingAttrPoints += computePrestigeAttrPoints(ascensions) = ascensions × 2 + 1
|
||||||
|
- createInitialState:每次返回全新 attributes/attributeProgress 对象,避免引用共享
|
||||||
|
|
||||||
|
### gameStore.ts 集成(核心)
|
||||||
|
- **pulse**:连击 ≥3 给灵感经验(expGain = 1 + floor(combo/2));脉冲威力乘以灵感连击加成
|
||||||
|
- **clickNode** 完成:给智慧经验(expGain = puzzle.tier × 2)
|
||||||
|
- **autoDecodeTick**:自动解码也给智慧经验 +1;自动解码周期受智慧 am.autoDecodeIntervalMult 影响
|
||||||
|
- **startExpedition**:探险力乘 am.expeditionPowerMult;探险生命加 am.expeditionHpBonus
|
||||||
|
- **resolveCurrentNode**:
|
||||||
|
- BOSS 节点用包装 RNG 提升 +am.bossWinRateBonus 胜率(单次 rng 调用,B% 概率返回 0,其余返回 r-B 保持均匀分布)
|
||||||
|
- 战斗胜利给勇气+探索力经验(中途战斗 +2/+1,BOSS 击破 +8/+6,探险胜利 +4)
|
||||||
|
- **grantCruiseReward**:按总奖励量缩放给探索力+勇气经验(expBase = max(2, totalReward/30))
|
||||||
|
- **tickTide**:灵感 am.tideTriggerBonus 缩短星潮间隙(gap × (1 - bonus)),上限 30%
|
||||||
|
- **doPrestige**:performPrestige 后用 next.attributes 重算 stats
|
||||||
|
- **allocateAttribute(attr, points=1)**:分配属性点,同步 attributeProgress[attr].level
|
||||||
|
- **gainAttributeExp(attr, amount)**:通用经验获取(自动升级)
|
||||||
|
- **init()**:调用 migrateAttributes 补全旧存档字段,并传 attributes 到 syncStats
|
||||||
|
- 所有 syncStats 调用点(10+ 处)都补充 `attributes: ...` 参数
|
||||||
|
|
||||||
|
### AttributesPanel.tsx UI
|
||||||
|
- 四维属性卡片网格(小屏 2×2,大屏 1×4):
|
||||||
|
- 图标(Compass/Brain/Swords/Sparkles)+ 中文名 + 英文名 + Lv.{value} badge
|
||||||
|
- 数值 /100 + 加成百分比
|
||||||
|
- 经验进度条(gradient + glow)+ "递减区"标记
|
||||||
|
- 加成影响列表(3 条)
|
||||||
|
- 「+分配」按钮(pendingAttrPoints > 0 时可点,hover scale 105)
|
||||||
|
- 顶部:标题 + 待分配点数 badge(pendingAttrPoints > 0 时 echo-pending-pulse 动画)
|
||||||
|
- 底部:总等级 + 总加成概览 + 12 个修饰器明细行
|
||||||
|
- 配色:4 色全息(emerald/fuchsia/amber/rose),辉光边框 + 顶角光晕装饰
|
||||||
|
- 完全响应式(2 列 → 4 列),overflow-y-auto + 自定义 scrollbar
|
||||||
|
|
||||||
|
### page.tsx 集成
|
||||||
|
- 新增 `User` 图标导入
|
||||||
|
- 新增 `pendingAttrPoints` store selector
|
||||||
|
- TabsList: grid-cols-7 → grid-cols-8
|
||||||
|
- 新增第 8 个 TabsTrigger「角色」(value="attributes"),主题色用 emerald→fuchsia→rose 渐变
|
||||||
|
- pendingAttrPoints > 0 时显示 rose 红点
|
||||||
|
- 新增 TabsContent 渲染 AttributesPanel
|
||||||
|
- StatsPanel 新增 5 行属性行(探索力/智慧/勇气/灵感/待分配属性点)
|
||||||
|
- 版本号 v0.6 → v0.7
|
||||||
|
|
||||||
|
### achievements.ts 新增
|
||||||
|
- `ach_attr_total_50`(四维觉醒):四维属性总和 ≥ 50 → 产能+6%/洞见+6%
|
||||||
|
- `ach_attr_max_100`(维度精通):任一属性 ≥ 100 → 产能+12%/洞见+10%
|
||||||
|
|
||||||
|
## QA 验证结果
|
||||||
|
- ✅ `bun run lint` 零错误
|
||||||
|
- ✅ dev 服务器 HTTP 200
|
||||||
|
- ✅ 编译 < 250ms(177ms 实测)
|
||||||
|
- ✅ 7 标签页 + 巡航按钮完好保留,新增第 8 个「角色」标签页
|
||||||
|
- ✅ data-tut 锚点(tab-expedition/tab-tech/prestige-btn/crystal-orb/decode-panel)保留
|
||||||
|
- ✅ 色彩规范:严格 emerald/fuchsia/amber/rose 四色全息,零蓝色/靛色
|
||||||
|
- ✅ 旧存档兼容:migrateAttributes 补全 attributes/attributeProgress/pendingAttrPoints 字段
|
||||||
|
- ✅ 飞升后 pendingAttrPoints = ascensions × 2 + 1(飞升前次数)
|
||||||
|
- ✅ pulse/clickNode/resolveCurrentNode/grantCruiseReward/autoDecodeTick 均接入属性经验获取
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
- BOSS 胜率 RNG 包装:仅在 boss 节点生效,单次 rng 调用保持均匀分布;+am.bossWinRateBonus 上限 +30%
|
||||||
|
- 灵感星潮触发:通过缩短 gap 间接提升触发频率(上限 30%)
|
||||||
|
- 属性加成与现有所有系统(技术树/蓝图/成就/星图/星潮)叠加,不冲突
|
||||||
|
- migrateAttributes 同步 attributeProgress[attr].level 与 attributes[attr] 数值,避免漂移
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Task 9-b · full-stack-developer · 信标系统扩展(周挑战 + 信标链)
|
||||||
|
|
||||||
|
> 本文件为本 agent 在 Task 9-b 的工作记录,供后续 agent 查阅。
|
||||||
|
|
||||||
|
## 任务概述
|
||||||
|
|
||||||
|
为「回响星核 / Echo Nexus」v0.8 扩展深空信标系统,新增两大功能:
|
||||||
|
1. **周挑战(Weekly Challenge)** — 每周一 UTC 0 点刷新,目标更大、奖励更好,与日挑战并行
|
||||||
|
2. **信标链(Beacon Chain)** — 连续完成日挑战形成"链",达成里程碑(3/7/14/30 天)领取递增大奖,含 grace 续命机制
|
||||||
|
|
||||||
|
## 阅读的前置工作
|
||||||
|
|
||||||
|
- `/home/z/my-project/worklog.md`(v0.8 项目状态,10 大系统,四色全息规范)
|
||||||
|
- `/home/z/my-project/src/lib/game/beacon.ts`(v0.5 原版 358 行:每日挑战 + 本地排行榜)
|
||||||
|
- `/home/z/my-project/src/components/game/BeaconPanel.tsx`(原版 301 行)
|
||||||
|
- `/home/z/my-project/src/store/gameStore.ts` 第 173 行 `trackBeacon` 函数 + 7 处调用点(pulse/clickNode/autoDecodeTick/resolveCurrentNode 等)
|
||||||
|
|
||||||
|
## 实现细节
|
||||||
|
|
||||||
|
### 1. beacon.ts 扩展(358 → 873 行)
|
||||||
|
|
||||||
|
**周挑战(WEEKLY CHALLENGE)section**:
|
||||||
|
- `BeaconWeeklyChallenge` 接口 + `BeaconWeeklyProgress` 接口
|
||||||
|
- `getWeekKey(now)`:ISO 8601 周键(YYYY-Www,周一为起点,含首个周四的周为第一周)
|
||||||
|
- `weekKeyToSeed`:FNV-1a 哈希
|
||||||
|
- `generateWeeklyChallenge(now)`:基于 weekKey 种子确定性生成
|
||||||
|
- 难度加权:anomaly 60% / singular 40%
|
||||||
|
- mult = 3 + floor(rng() * 3) → 3-5 倍
|
||||||
|
- goal 范围:decode 18-75 / expedition 3-15 / pulse 60-245 / boss 3-12 / insight 120-595
|
||||||
|
- `loadWeeklyProgress` / `saveWeeklyProgress` / `addWeeklyProgress` / `claimWeeklyReward`
|
||||||
|
- `msUntilNextWeek(now)`
|
||||||
|
- `BEACON_WEEKLY_KEY = "echo-nexus-beacon-weekly-v1"`
|
||||||
|
|
||||||
|
**信标链(BEACON CHAIN)section**:
|
||||||
|
- `BeaconChainState` 接口(lastCompletedDateKey / currentStreak / longestStreak / totalCompletions / graceUsed / milestonesClaimed)
|
||||||
|
- `BEACON_CHAIN_KEY = "echo-nexus-beacon-chain-v1"`
|
||||||
|
- `BEACON_CHAIN_MILESTONES = [3, 7, 14, 30] as const`
|
||||||
|
- `BEACON_CHAIN_REWARDS`:4 个里程碑
|
||||||
|
- 3 天:+50 洞见 / +5 接触 / "三日谐振"
|
||||||
|
- 7 天:+120 洞见 / +12 接触 / "七日回响"
|
||||||
|
- 14 天:+280 洞见 / +28 接触 / "半月星潮"
|
||||||
|
- 30 天:+680 洞见 / +68 接触 / "满月飞升"
|
||||||
|
- `loadChainState` / `saveChainState`(每次返回新对象避免引用共享 bug)
|
||||||
|
- `recordChainCompletion(dateKey)` 核心逻辑:
|
||||||
|
- 同日重复完成 → 忽略
|
||||||
|
- 次日 → streak++
|
||||||
|
- 隔一天 miss 且 graceUsed<1 → 续命 streak++ graceUsed++
|
||||||
|
- 其他 → 断链 streak=1 graceUsed=0
|
||||||
|
- 返回 `{ state, newMilestones }`
|
||||||
|
- `claimChainMilestone(milestone)` / `getNextMilestone(streak)` / `getChainProgress(streak)`
|
||||||
|
- `dateKeyToTimestamp` / `dateKeyDiffDays` 工具
|
||||||
|
|
||||||
|
`BeaconScoreEntry` 新增可选 `isWeekly?: boolean` 字段,向后兼容。
|
||||||
|
|
||||||
|
### 2. gameStore.ts 集成
|
||||||
|
|
||||||
|
- import 扩展:新增 generateWeeklyChallenge / loadWeeklyProgress / loadChainState / addWeeklyProgress / recordChainCompletion / claimWeeklyReward / claimChainMilestone / getTodayKey + 类型
|
||||||
|
- `trackBeacon(type, delta)` 返回值从 `boolean` 升级为 `{ dailyJustCompleted, weeklyJustCompleted, newChainMilestones }`:
|
||||||
|
- 同时更新日挑战进度 + 周挑战进度
|
||||||
|
- 日挑战刚完成时调用 `recordChainCompletion(getTodayKey())`
|
||||||
|
- 所有 7 处调用点原本忽略返回值,向后兼容
|
||||||
|
- 新增 action `claimWeeklyBeacon()`:调用 claimWeeklyReward → 发放奖励到 state
|
||||||
|
- 新增 action `claimChainReward(milestone)`:前置校验 → claimChainMilestone → 发放奖励
|
||||||
|
- GameActions 接口同步扩展
|
||||||
|
|
||||||
|
### 3. BeaconPanel.tsx 重写(301 → 638 行)
|
||||||
|
|
||||||
|
- 头部 + 每日挑战卡片(v0.5 保留)+ 难度色按钮主题
|
||||||
|
- **周挑战区块**(fuchsia 主题):标题 + weekKey + 倒计时 + 卡片(标签/标题/描述/进度/奖励/领取按钮 emerald)+ weekly-glow 动画
|
||||||
|
- **信标链区块**(amber→rose 渐变):
|
||||||
|
- 标题"信标链 · CHAIN" + 大字号 streak(chain-streak-text 流动渐变动画)
|
||||||
|
- 今日完成状态徽章
|
||||||
|
- 4 个里程碑节点(w-12 h-12 圆形):
|
||||||
|
- claimed: emerald 实心 + ✓
|
||||||
|
- reachable: rose 脉冲动画 + "领取"按钮
|
||||||
|
- inProgress (next milestone): amber 半亮
|
||||||
|
- 未到达: muted 灰
|
||||||
|
- 节点间连线:背景灰 + 已达成部分 amber→rose 渐变 + 辉光,基于 prev→next 插值定位
|
||||||
|
- 进度条 + 底部统计(最长链/累计完成/续命状态)
|
||||||
|
- 桌面端 lg:grid-cols-2 让周挑战 + 信标链并排,移动端单列
|
||||||
|
- 排行榜区分日/周:周挑战 entry 显示 "WEEK" 徽章 + fuchsia 高亮背景
|
||||||
|
- 4 个新 CSS 动画:weekly-glow / chain-milestone-pulse / chain-streak-flux
|
||||||
|
|
||||||
|
## QA 验证结果
|
||||||
|
|
||||||
|
### 1. lint
|
||||||
|
- `bun run lint` 零错误(每次修改后均验证)
|
||||||
|
|
||||||
|
### 2. dev 服务器
|
||||||
|
- dev.log 全程无错误,所有编译 < 300ms,HTTP 200
|
||||||
|
|
||||||
|
### 3. 信标链逻辑测试(bun 直接运行 TS,5 个场景全 PASS)
|
||||||
|
1. ✅ 昨日 streak=1 → 今日完成 → streak=2(normal increment)
|
||||||
|
2. ✅ 同日重复完成 → 忽略
|
||||||
|
3. ✅ 明日完成 → streak=3,无需 grace
|
||||||
|
4. ✅ 隔一天 miss(lastCompleted + 2 天)→ streak=4,graceUsed=1(续命触发)
|
||||||
|
5. ✅ 再次 miss(grace 已用)→ 断链 streak=1,graceUsed=0
|
||||||
|
|
||||||
|
### 4. UI 集成测试(agent-browser)
|
||||||
|
- localStorage 模拟设置 chain state(lastCompletedDateKey=yesterday, currentStreak=1)→ reload → UI 正确显示 streak=1
|
||||||
|
- 设置 streak=3 + milestonesClaimed=[] → milestone 3 节点显示 rose 脉冲动画 + "领取"按钮
|
||||||
|
- 点击"领取"→ milestonesClaimed=[3] + Toast "✦ 三日谐振 已领取 +50 洞见 · +5.0 接触" + 按钮消失 ✅
|
||||||
|
|
||||||
|
### 5. VLM 视觉评分(目标 ≥7/10)
|
||||||
|
- 首屏截图:**8/10**(四色一致、布局合理、信标链清晰)
|
||||||
|
- 里程碑可领取状态:**8/10**(amber→rose 渐变醒目、里程碑层次分明、fuchsia 主题清晰)
|
||||||
|
- 重置后干净状态:**8/10**(WEEKLY 字体对比度可优化,但整体可读性强)
|
||||||
|
|
||||||
|
## 修改的文件
|
||||||
|
|
||||||
|
1. `src/lib/game/beacon.ts` — 358 → 873 行(+515 行)
|
||||||
|
2. `src/store/gameStore.ts` — trackBeacon 升级 + 2 个新 action(+~80 行)
|
||||||
|
3. `src/components/game/BeaconPanel.tsx` — 301 → 638 行(+337 行,重写)
|
||||||
|
|
||||||
|
## 不破坏的现有功能
|
||||||
|
|
||||||
|
- 日挑战进度追踪与领奖流程
|
||||||
|
- 本地排行榜 Top20
|
||||||
|
- 现有 5 种挑战类型 + 3 档难度
|
||||||
|
- 现有 localStorage keys(echo-nexus-beacon-lb-v1 / echo-nexus-beacon-prog-v1)
|
||||||
|
|
||||||
|
## 新增的 localStorage keys
|
||||||
|
|
||||||
|
- `echo-nexus-beacon-weekly-v1`(周挑战进度)
|
||||||
|
- `echo-nexus-beacon-chain-v1`(信标链状态)
|
||||||
|
|
||||||
|
## 四色全息规范遵循
|
||||||
|
|
||||||
|
- 周挑战主题:**fuchsia** (#e879f9)
|
||||||
|
- 信标链主题:**amber → rose** 渐变 (#fbbf24 → #fb7185)
|
||||||
|
- 领取按钮:**emerald** (#34d399)
|
||||||
|
- 难度色:routine emerald / anomaly amber / singular rose
|
||||||
|
- **零蓝色/靛色违规**
|
||||||
|
|
||||||
|
## 截图资产
|
||||||
|
|
||||||
|
- `/home/z/my-project/agent-ctx/beacon-panel-v0.8.png` — 初次进入信标页
|
||||||
|
- `/home/z/my-project/agent-ctx/beacon-chain-streak1.png` — chain streak=1 状态
|
||||||
|
- `/home/z/my-project/agent-ctx/beacon-chain-milestone3.png` — chain streak=3 milestone 可领取
|
||||||
|
- `/home/z/my-project/agent-ctx/beacon-milestone3-claimable.png` — milestone 3 领取前
|
||||||
|
- `/home/z/my-project/agent-ctx/beacon-final-fresh.png` — 重置后干净状态
|
||||||
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 383 KiB |
|
After Width: | Height: | Size: 387 KiB |
@@ -0,0 +1,139 @@
|
|||||||
|
# Task ID: v0.14-reimpl
|
||||||
|
# Agent: full-stack-developer
|
||||||
|
# Task: 放置系统 (Idle Operations) — 采矿无人机舰队 + 6放置工程 + 永久产能加成 + idle徽章
|
||||||
|
|
||||||
|
## Work Log
|
||||||
|
|
||||||
|
### 1. 类型扩展 (`src/lib/game/types.ts`)
|
||||||
|
- 新增 `IdleProjectDef` 接口(id/name/desc/durationSec/reward/minAscensions/minCrystalsPerSec/icon/color/order)
|
||||||
|
- 新增 `IdleProjectSlot` 接口(projectId/startedAt/finishesAt/remainingSec/completed)
|
||||||
|
- 在 `GameState` 接口 `pendingAttrPoints` 后新增 4 个字段:
|
||||||
|
- `idleProjectSlots: (IdleProjectSlot | null)[]`
|
||||||
|
- `idleProjectHistory: { projectId: string; finishedAt: number }[]`
|
||||||
|
- `idlePermanentBonus: number`
|
||||||
|
- `idleStats: { projectsCompleted: number; crystalsFromIdle: number }`
|
||||||
|
|
||||||
|
### 2. 新建 `src/lib/game/idle.ts` (~290 行)
|
||||||
|
- 定义 6 个放置工程(按 order 排序):
|
||||||
|
| id | name | duration | reward | icon | color | unlock |
|
||||||
|
|----|------|----------|--------|------|-------|--------|
|
||||||
|
| idle_scan | 深空勘探扫描 | 60s | insights +8 | 🛰️ | emerald | cps≥0.5 |
|
||||||
|
| idle_refine | 晶体精炼阵列校准 | 180s | permBonus +0.3/s | ⚙️ | rose | cps≥1 |
|
||||||
|
| idle_archive | 遗迹碎片整理 | 600s | randomFragment + insights +20 | 📜 | amber | asc≥1 OR cps≥5 |
|
||||||
|
| idle_anchor | 维度锚点部署 | 1200s | energy +2 + contact +5 | ⚓ | fuchsia | asc≥1 |
|
||||||
|
| idle_drones | 无人机群扩编 | 1800s | permBonus +2/s + crystals +500 | 🛸 | emerald | cps≥10 |
|
||||||
|
| idle_resonance | 跨维度谐振标定 | 3600s | permBonus +8/s + contact +15 + insights +100 | 🌌 | fuchsia | asc≥2 |
|
||||||
|
- 导出:`IDLE_PROJECTS`, `IDLE_SLOT_COUNT=3`, `getIdleProject`, `getUnlockedIdleProjects`, `getLockedIdleProjects`, `isIdleProjectUnlocked` (OR 逻辑:满足任一已设置条件即可解锁), `formatRemaining` (12s/3m 45s/1h 12m), `formatDuration`, `formatReward`, `deriveMinerFleet` (派生 min_1..min_5 采矿无人机,无任何采矿技术时返回休眠 min_1)
|
||||||
|
- 导出颜色映射 `IDLE_COLOR_CLASSES` (text/border/bg/bgSoft/glow/ring/dot) + `COLOR_HEX` (用于 inline style,绕过 Tailwind 动态类限制)
|
||||||
|
|
||||||
|
### 3. 引擎集成 (`src/lib/game/engine.ts`)
|
||||||
|
- `recomputeStats()` 末尾 `crystalsPerSec += state.idlePermanentBonus ?? 0` (放置永久加成叠加到产能)
|
||||||
|
- `performPrestige()` 在返回对象中:
|
||||||
|
- 清空 `idleProjectSlots: [null, null, null]` (飞升中断当前周目放置工程)
|
||||||
|
- 保留 `idleProjectHistory`, `idlePermanentBonus`, `idleStats` (跨周目永久)
|
||||||
|
- `recomputeStats` 调用传入 `idlePermanentBonus` 以确保飞升后立即应用永久加成
|
||||||
|
- `createInitialState()` 新增 4 字段默认值
|
||||||
|
|
||||||
|
### 4. Store actions (`src/store/gameStore.ts`)
|
||||||
|
- 新增 4 个 action:
|
||||||
|
- `startIdleProject(slotIndex, projectId)`: 校验槽位空 + 项目已解锁 + 未在他处运行;写入 `{ projectId, startedAt, finishesAt, remainingSec, completed:false }`
|
||||||
|
- `cancelIdleProject(slotIndex)`: 置槽位为 null
|
||||||
|
- `claimIdleProject(slotIndex)`: 校验已完成 → 应用奖励(crystals 限 crystalCap,energy 限 energyMax,contact 限 100;permBonus 累加到 idlePermanentBonus + 重算 stats;randomFragment 从未解锁碎片中随机一个)→ 追加 history (slice -50) → idleStats.projectsCompleted++ → Toast 提示
|
||||||
|
- `tickIdleProjects(now)`: 遍历非空非完成槽位,更新 remainingSec,归零时标记 completed
|
||||||
|
- **persist migrate 函数**:persist 配置增加 `version: 1` + `migrate` 函数,自动补全 idleProjectSlots/idleProjectHistory/idlePermanentBonus/idleStats 4 字段,旧存档加载不崩溃
|
||||||
|
- 全部 14 处 syncStats 调用更新为传入 `idlePermanentBonus: s.idlePermanentBonus ?? 0`,确保 recomputeStats 计算时纳入永久加成
|
||||||
|
- `init()` 中也补全 4 个 idle 字段(defense in depth)
|
||||||
|
|
||||||
|
### 5. 主循环 (`src/hooks/useGameLoop.ts`)
|
||||||
|
- 新增 `tickIdleProjects` 选择器
|
||||||
|
- setInterval 回调中 `autoDecodeTick()` 后调用 `tickIdleProjects(now)`
|
||||||
|
- visibilitychange 回调中也调用 `tickIdleProjects(now)`
|
||||||
|
|
||||||
|
### 6. 新建 `src/components/game/IdleOperationsPanel.tsx` (~490 行)
|
||||||
|
- 主「放置」标签内容,4 个 section 在 `max-h-[520px] overflow-y-auto` 容器中:
|
||||||
|
1. **放置收益概览**:3 stat tiles (放置产能 / 永久加成 / 完成工程) + 离线效率 progress bar + 累计放置晶体统计
|
||||||
|
2. **采矿无人机舰队**:grid 展示 deriveMinerFleet(state),每张卡:emoji + name + Lv + output/s + 状态点(active=emerald ping 脉冲,dormant=灰)
|
||||||
|
3. **放置工程槽位**:3 张槽位卡(空槽=虚线占位,运行中=大字号倒计时+自定义进度条+取消按钮,已完成=奖励预览+领取按钮带 glow 辉光)
|
||||||
|
4. **可派遣工程**:2 列 grid 展示已解锁工程(icon+name+duration+desc+reward+3 个数字派遣按钮 1/2/3),下方列出未解锁工程及解锁条件
|
||||||
|
- 使用 `useShallow` 订阅多个 store 字段
|
||||||
|
- 本地 `now` state 每 1s 刷新倒计时
|
||||||
|
- 严格 4 色全息(emerald/rose/amber/fuchsia),无蓝/靛
|
||||||
|
- 自定义滚动条样式 (fuchsia 主题)
|
||||||
|
|
||||||
|
### 7. 新建 `src/components/game/IdleStatusBadge.tsx` (~100 行)
|
||||||
|
- 紧凑徽章:脉冲点 (emerald 若 cps>0,否则灰) + "放置中 +X/s" (或 "休眠中")
|
||||||
|
- Tooltip 悬停展示分解:基础产能 / 永久加成 / 运行中工程 / 待领取工程数
|
||||||
|
- `h-8 px-2.5 text-[11px]`,点击切换到放置标签
|
||||||
|
- `data-tut="idle-status-badge"` 锚点
|
||||||
|
|
||||||
|
### 8. 新建 `src/components/game/IdleProjectBar.tsx` (~90 行)
|
||||||
|
- 晶体球下方的细长进度条
|
||||||
|
- 显示最多 3 个运行/已完成工程为 mini pill:`[icon] name 12s ▓▓▓░░`
|
||||||
|
- 完成时显示 "✓ 待领取" + glow 辉光
|
||||||
|
- 无工程时返回 null
|
||||||
|
- 本地 `now` state 每 1s 刷新倒计时
|
||||||
|
- 进度条颜色用 inline style 控制(COLOR_HEX 映射)
|
||||||
|
|
||||||
|
### 9. 主页面集成 (`src/app/page.tsx`)
|
||||||
|
- 引入 3 个新组件 + `Clock` 图标 + `getUnlockedIdleProjects`
|
||||||
|
- 新增 `idleProjectSlots` / `createdAt` store 订阅
|
||||||
|
- 新增 `activeTab` / `tabInited` state,controlled Tabs:`<Tabs value={activeTab} onValueChange={setActiveTab}>`
|
||||||
|
- 挂载后 useEffect 一次性设置默认 tab:hasActiveExpedition → "expedition",hasPendingPerk → "constellation",否则 → "idle"
|
||||||
|
- 版本号 v0.8 → v0.14
|
||||||
|
- Header 在 StarTideIndicator 后加 IdleStatusBadge
|
||||||
|
- 左侧 CrystalOrb 后加 IdleProjectBar
|
||||||
|
- TabsList grid-cols-8 → grid-cols-9,新增 `value="idle"` 的 TabsTrigger 作为第一个 tab(Clock 图标,emerald 主题,待领取时显示数量红点)
|
||||||
|
- 新增 TabsContent value="idle" 渲染 IdleOperationsPanel
|
||||||
|
- 新增 idle 相关 goal 提示(高优先级):
|
||||||
|
- 待领取 > 0 → "✦ 放置工程已完成 X 项,请前往「放置」标签领取奖励"
|
||||||
|
- 全空 + 有解锁 + 时长 > 60s → "「放置」标签可派遣工程项目,离线自动产出"
|
||||||
|
- StatsPanel 新增 3 行:放置永久加成 / 完成放置工程 / 放置产出晶体
|
||||||
|
|
||||||
|
### 10. 教程更新 (`src/lib/game/tutorial.ts`)
|
||||||
|
- TUTORIAL_STEPS 在 decode 与 tech 之间插入新步骤:
|
||||||
|
- id: "idle", target: "tab-idle", placement: "top"
|
||||||
|
- 标题 "③ 放置工程 · 离线产出"
|
||||||
|
- 介绍放置标签的工程派遣 + 顶部徽章 + 晶体球下方进度条
|
||||||
|
- 原 tech/expedition/prestige 步骤编号顺延为 ④⑤⑥
|
||||||
|
|
||||||
|
## QA 验证
|
||||||
|
|
||||||
|
### Lint
|
||||||
|
- `bun run lint` → 零错误零警告 ✅
|
||||||
|
|
||||||
|
### HTTP
|
||||||
|
- `curl http://localhost:3000/` → 200 OK ✅
|
||||||
|
|
||||||
|
### Dev log
|
||||||
|
- 全程无 runtime error,所有请求 200 OK,编译 < 250ms ✅
|
||||||
|
|
||||||
|
### agent-browser 烟雾测试
|
||||||
|
1. ✅ 版本标签 "v0.14" 可见
|
||||||
|
2. ✅ 默认 tab 是 "放置"(data-state="active" 在第一个 tab)
|
||||||
|
3. ✅ 头部 idle 徽章可见("放置中 +0.4/s" 或 "休眠中")
|
||||||
|
4. ✅ IdleOperationsPanel 4 个 section 全部可见(放置收益概览 / 采矿无人机舰队 / 放置工程槽位 / 可派遣工程 / 未解锁工程)
|
||||||
|
5. ✅ 采矿无人机舰队在无采矿技术时显示休眠 min_1(emoji ⛏️ + 灰色状态点)
|
||||||
|
6. ✅ 设置 insights=50 → buyTech('min_1') → crystalsPerSec 0.4→1.0 → 解锁 idle_scan + idle_refine
|
||||||
|
7. ✅ 点击 idle_scan 派遣按钮 → 槽位 0 写入 { projectId: "idle_scan", finishesAt: now+60000 } → 倒计时正确递减
|
||||||
|
8. ✅ 60s 后自动标记 completed=true → 槽位卡显示 "✓ 完成" + 领取按钮
|
||||||
|
9. ✅ 点击领取 → Toast "✦ 工程奖励已领取 · 深空勘探扫描 · +8 洞见" → insights 8→16 → history 追加 → idleStats.projectsCompleted 0→1
|
||||||
|
10. ✅ 派遣 idle_refine (180s) → 强制完成 → 领取 → crystalsPerSec 1.0→1.3 → idlePermanentBonus 0→0.3 ✅ 永久产能加成正确应用
|
||||||
|
11. ✅ IdleProjectBar 在晶体球下方显示:`🛰️ 深空勘探扫描 55s ▓▓▓░░`
|
||||||
|
12. ✅ Footer goal 文字根据状态动态切换为放置相关提示
|
||||||
|
13. ✅ Footer 产能显示 "产能 1.3/s" (含 idle 永久加成)
|
||||||
|
|
||||||
|
### Migration 测试
|
||||||
|
- 旧存档(无 idle 字段)通过 migrate 函数自动补全为默认值,加载不崩溃 ✅
|
||||||
|
|
||||||
|
## Stage Summary
|
||||||
|
- ✅ 类型层:新增 IdleProjectDef + IdleProjectSlot + 4 GameState 字段
|
||||||
|
- ✅ 逻辑层:新建 idle.ts (~290 行),6 工程定义 + 派生函数 + 颜色映射
|
||||||
|
- ✅ 引擎层:recomputeStats / performPrestige / createInitialState 全部接入 idle 永久加成
|
||||||
|
- ✅ Store 层:4 新 action + persist migrate (version 1) + 14 处 syncStats 调用全部传 idlePermanentBonus
|
||||||
|
- ✅ 主循环:tickIdleProjects 接入 setInterval + visibilitychange
|
||||||
|
- ✅ UI 层:3 新组件 (IdleOperationsPanel ~490 行 / IdleStatusBadge ~100 行 / IdleProjectBar ~90 行)
|
||||||
|
- ✅ 主页面:默认放置 tab + 头部徽章 + 晶体球下方进度条 + 版本 v0.14 + 9 标签页
|
||||||
|
- ✅ 教程:新增 idle 步骤 (③ 放置工程)
|
||||||
|
- ✅ 严格 4 色全息 (emerald/rose/amber/fuchsia),零蓝/靛
|
||||||
|
- ✅ lint 零错误 + HTTP 200 + agent-browser 全流程烟雾测试通过
|
||||||
|
- ✅ 5 秒内可见三要素:头部徽章 + 默认放置 tab + 晶体球下方进度条(派遣后)
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
# 回响星核 v0.14 — 放置系统(Idle Operations)设计文档
|
||||||
|
|
||||||
|
> 版本:v0.14 · 日期:2026-01-25
|
||||||
|
> 核心目标:让玩家打开游戏 **5 秒内** 就能看出这是个 idle game
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 设计动机
|
||||||
|
|
||||||
|
### 1.1 用户反馈
|
||||||
|
> "你的这个我没有看到任何可放置的功能" —— 玩家反馈
|
||||||
|
|
||||||
|
虽然 v0.8 已有 `crystalsPerSec` 在后台自动累积,但 **没有任何 UI 元素明示这一事实**:
|
||||||
|
- 没有「放置中」徽章
|
||||||
|
- 没有可视化自动采矿机
|
||||||
|
- 没有定时工程项目
|
||||||
|
- 离线收益弹窗只在重新加载页面时触发,玩家日常游玩看不到
|
||||||
|
|
||||||
|
放置游戏的灵魂是「关掉也在进步」。如果玩家看不到这一点,就等于没有放置玩法。
|
||||||
|
|
||||||
|
### 1.2 设计目标
|
||||||
|
1. **可见性**:5 秒内让玩家意识到 idle 属性
|
||||||
|
2. **决策性**:放置不只是数字增长,要有玩家选择
|
||||||
|
3. **节奏感**:3 层 idle 节奏(被动产能 / 放置工程 / 离线收益)协同
|
||||||
|
4. **健康性**:避免 24h 挂机的极端沉迷
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 三层 Idle 节奏
|
||||||
|
|
||||||
|
### 节奏 1:被动产能(Passive Generation)
|
||||||
|
- **载体**:采矿无人机舰队(Miner Fleet)
|
||||||
|
- **来源**:每解锁一个采矿分支技术节点 → 加入一架无人机
|
||||||
|
- **行为**:24h 持续产出晶体,无需任何操作
|
||||||
|
- **限制**:受 `crystalCap` 限制,仓库满后停止
|
||||||
|
- **离线**:按 `offlineEff`(默认 50%)持续累积,上限 8h
|
||||||
|
|
||||||
|
#### 采矿分支技术 → 无人机映射
|
||||||
|
|
||||||
|
| 技术节点 | 无人机名 | 输出/s | 解锁成本(洞见) |
|
||||||
|
|---------|---------|--------|-----------------|
|
||||||
|
| `min_1` | ⛏️ 谐振钻头 | +0.6 | 5 |
|
||||||
|
| `min_2` | 🌋 深层矿脉 | +1.6 | 24 |
|
||||||
|
| `min_3` | 🤖 自治机群 | +5 | 120 |
|
||||||
|
| `min_4` | 💠 量子采掘矩阵 | +12 | 400 |
|
||||||
|
| `min_5` | ☀️ 戴森星核 | +30 | 1200 |
|
||||||
|
|
||||||
|
无采矿技术时显示「谐振钻头(休眠中)」占位卡片,引导玩家解锁采矿分支。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 节奏 2:放置工程(Idle Projects)
|
||||||
|
|
||||||
|
#### 2.1 工程列表
|
||||||
|
|
||||||
|
| ID | 名称 | 时长 | 奖励 | 解锁条件 | 图标 | 色 |
|
||||||
|
|----|------|------|------|---------|------|-----|
|
||||||
|
| `idle_scan` | 深空勘探扫描 | 60s | ✦ +8 洞见 | 产能 ≥ 0.5/s | 🛰️ | emerald |
|
||||||
|
| `idle_refine` | 晶体精炼阵列校准 | 180s | ⇪ +0.3/s 永久 | 产能 ≥ 1/s | ⚙️ | rose |
|
||||||
|
| `idle_archive` | 遗迹碎片整理 | 600s | ✦ +20 洞见 + 📜 随机碎片 | 飞升 ≥ 1 OR 产能 ≥ 5/s | 📜 | amber |
|
||||||
|
| `idle_anchor` | 维度锚点部署 | 1200s | ⚡ +2 能量 + ☄ +5 接触进度 | 飞升 ≥ 1 | ⚓ | fuchsia |
|
||||||
|
| `idle_drones` | 无人机群扩编 | 1800s | 💎 +500 晶体 + ⇪ +2/s 永久 | 产能 ≥ 10/s | 🛸 | emerald |
|
||||||
|
| `idle_resonance` | 跨维度谐振标定 | 3600s | ✦ +100 洞见 + ☄ +15 接触 + ⇪ +8/s 永久 | 飞升 ≥ 2 | 🌌 | fuchsia |
|
||||||
|
|
||||||
|
#### 2.2 槽位机制
|
||||||
|
- **3 个并行槽位**:可同时运行 3 个工程
|
||||||
|
- **不重复**:同一工程不能在多个槽位同时运行
|
||||||
|
- **完成需手动领取**:避免"打开就有奖励"的条件反射
|
||||||
|
- **取消不退时间**:误取消的代价
|
||||||
|
- **离线推进**:关闭浏览器后工程继续倒计时
|
||||||
|
|
||||||
|
#### 2.3 奖励类型说明
|
||||||
|
|
||||||
|
| 奖励 | 含义 | 设计意图 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 晶体 | 立即加到 `crystals` | 短期反馈 |
|
||||||
|
| 洞见 | 立即加到 `insights` | 用于解锁技术 |
|
||||||
|
| 能量 | 加到 `energy`(受 `energyMax` 上限) | 探险资源 |
|
||||||
|
| 接触进度 | 加到 `contact`(0-100) | 推进飞升 |
|
||||||
|
| **永久产能加成** | 加到 `idlePermanentBonus`,影响 `recomputeStats` | 长期投资 |
|
||||||
|
| 随机碎片 | 从 `FRAGMENTS` 中随机解锁一个未拥有 | 叙事推进 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 节奏 3:离线收益(Offline Gains)
|
||||||
|
|
||||||
|
#### 3.1 公式
|
||||||
|
```
|
||||||
|
gain = crystalsPerSec × elapsed × offlineEff
|
||||||
|
elapsed = min(realElapsed, 8h)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 解锁路径
|
||||||
|
- 初始 `offlineEff = 0.5`(50% 离线效率)
|
||||||
|
- 通过技术树 `offlineEff` 类效果提升
|
||||||
|
- 通过星图天赋 `offlineEffBonus` 提升
|
||||||
|
- 上限 100%
|
||||||
|
|
||||||
|
#### 3.3 8h 上限理由
|
||||||
|
- 参考 GDC 2016 Anthony Pecorella "Quest for Progress":Egg Inc 2h 硬 cap 导致玩家流失
|
||||||
|
- 但完全无 cap 会导致 24h 挂机成为最优策略,破坏短会话体验
|
||||||
|
- 8h 折中:覆盖一个工作日/一夜,但不鼓励全天挂机
|
||||||
|
|
||||||
|
#### 3.4 离线报告弹窗
|
||||||
|
重新加载页面时,若 `elapsed > 60s`,弹出 `OfflineReportDialog`:
|
||||||
|
- 「你离开了 X 小时 Y 分」
|
||||||
|
- 「星核为你产出 Z 颗晶体」
|
||||||
|
- 「产能 /s · 离线效率 N%」
|
||||||
|
- 「是否触达 8h 上限」标志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. UI 设计
|
||||||
|
|
||||||
|
### 3.1 始终可见的 Idle 徽章(Header)
|
||||||
|
位置:顶部 Header,紧邻「巡航」按钮左侧
|
||||||
|
状态:
|
||||||
|
- 产能 > 0:`● 放置中 +X.X/s`(绿点脉动)
|
||||||
|
- 产能 = 0:`○ 休眠中`(灰点静止)
|
||||||
|
|
||||||
|
悬停 Tooltip 显示明细:
|
||||||
|
```
|
||||||
|
基础产能 8.2/s
|
||||||
|
永久加成 +1.5/s
|
||||||
|
离线效率 75%
|
||||||
|
完成工程 12 次
|
||||||
|
```
|
||||||
|
|
||||||
|
点击徽章 → 切换到「放置」标签。
|
||||||
|
|
||||||
|
### 3.2 默认标签页 = 放置
|
||||||
|
首次打开游戏,默认 tab 是「放置」(v0.13 前是「探险」或「技术」)。
|
||||||
|
返回玩家若有进行中探险或待选天赋,自动切回原默认。
|
||||||
|
|
||||||
|
### 3.3 放置标签页四段式布局
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Section 1: 放置收益概览 │
|
||||||
|
│ [放置产能] [永久加成] [完成工程] │
|
||||||
|
│ [━━━━━━━ 离线效率 50% · 上限 8h ━━━━━━] │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ Section 2: 采矿无人机舰队 │
|
||||||
|
│ [⛏️ 钻头] [🌋 矿脉] [🤖 机群] ... │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ Section 3: 放置工程槽位 │
|
||||||
|
│ [槽位1: 运行中 47s ▓▓▓░] [槽位2: 空] [槽位3: 空] │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ Section 4: 可派遣工程 │
|
||||||
|
│ [🛰️ 深空勘探扫描 1m +8] [⚙️ 精炼校准 3m +0.3/s] │
|
||||||
|
│ [📜 碎片整理 10m +20] │
|
||||||
|
│ │
|
||||||
|
│ 未解锁工程(3): │
|
||||||
|
│ ⚓ 维度锚点部署 · 需 飞升≥1 │
|
||||||
|
│ 🛸 无人机群扩编 · 需 产能≥10/s │
|
||||||
|
│ 🌌 跨维度谐振标定 · 需 飞升≥2 │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 IdleProjectBar(晶体球下方)
|
||||||
|
位置:左侧晶体球区,`CrystalYieldPanel` 下方
|
||||||
|
显示:当前运行的工程迷你 pill
|
||||||
|
```
|
||||||
|
🛰️ 深空勘探扫描 47s ▓▓▓░░░░░
|
||||||
|
⚙️ 晶体精炼校准 2m 13s ▓▓░░░░░░
|
||||||
|
```
|
||||||
|
无运行工程时返回 `null`(不占位)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 技术实现
|
||||||
|
|
||||||
|
### 4.1 数据结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// types.ts
|
||||||
|
interface IdleProjectDef {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
durationSec: number;
|
||||||
|
reward: { crystals?; insights?; energy?; contact?; crystalsPerSecPermanent?; randomFragment?; };
|
||||||
|
minAscensions?: number;
|
||||||
|
minCrystalsPerSec?: number;
|
||||||
|
icon: string;
|
||||||
|
color: "emerald" | "rose" | "amber" | "fuchsia";
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdleProjectSlot {
|
||||||
|
projectId: string;
|
||||||
|
startedAt: number;
|
||||||
|
finishesAt: number;
|
||||||
|
remainingSec: number;
|
||||||
|
completed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GameState 新增 4 字段
|
||||||
|
idleProjectSlots: (IdleProjectSlot | null)[]; // 3 槽
|
||||||
|
idleProjectHistory: { projectId: string; finishedAt: number }[]; // 最近 50 条
|
||||||
|
idlePermanentBonus: number;
|
||||||
|
idleStats: { projectsCompleted: number; crystalsFromIdle: number };
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Store actions
|
||||||
|
- `startIdleProject(slotIndex, projectId)` — 校验 + 创建槽位
|
||||||
|
- `cancelIdleProject(slotIndex)` — 置空槽位
|
||||||
|
- `claimIdleProject(slotIndex)` — 应用奖励 + toast + 置空槽位
|
||||||
|
- `tickIdleProjects(now)` — 推进剩余时间,标记完成
|
||||||
|
|
||||||
|
### 4.3 引擎集成
|
||||||
|
`recomputeStats()` 末尾添加:
|
||||||
|
```typescript
|
||||||
|
crystalsPerSec += state.idlePermanentBonus ?? 0;
|
||||||
|
```
|
||||||
|
这让永久产能加成真正生效。
|
||||||
|
|
||||||
|
### 4.4 飞升处理
|
||||||
|
`performPrestige()` 重置 `idleProjectSlots = [null, null, null]`(不跨周目继承进行中工程),但保留 `idlePermanentBonus` / `idleProjectHistory` / `idleStats`(跨周目永久)。
|
||||||
|
|
||||||
|
### 4.5 存档迁移
|
||||||
|
persist 添加 `version: 1` + `migrate` 函数,为旧存档补全 4 个新字段:
|
||||||
|
```typescript
|
||||||
|
idleProjectSlots: (s.idleProjectSlots as unknown[]) ?? [null, null, null],
|
||||||
|
idleProjectHistory: (s.idleProjectHistory as unknown[]) ?? [],
|
||||||
|
idlePermanentBonus: (s.idlePermanentBonus as number) ?? 0,
|
||||||
|
idleStats: (s.idleStats as unknown) ?? { projectsCompleted: 0, crystalsFromIdle: 0 },
|
||||||
|
```
|
||||||
|
旧存档零崩溃风险。
|
||||||
|
|
||||||
|
### 4.6 主循环集成
|
||||||
|
`useGameLoop` 每 250ms tick:
|
||||||
|
1. `tick(now)` — 产能 + 自动产晶体
|
||||||
|
2. `autoDecodeTick()` — 自动解码
|
||||||
|
3. `tickTide(now)` — 星潮事件
|
||||||
|
4. **`tickIdleProjects(now)` — v0.14 放置工程进度**(新增)
|
||||||
|
5. `checkAchievements()` — 每秒一次
|
||||||
|
|
||||||
|
页面可见性切回时也调用 `tickIdleProjects(now)`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 平衡考虑
|
||||||
|
|
||||||
|
### 5.1 永久产能加成可能过强
|
||||||
|
- `idle_resonance` +8/s 永久,多次完成可能让玩家飞快积累
|
||||||
|
- **v0.15 待办**:加飞升次数 softcap,例如 `effectiveBonus = idlePermanentBonus × sqrt(ascensions + 1) / (ascensions + 1)`
|
||||||
|
|
||||||
|
### 5.2 工程时长曲线
|
||||||
|
- 60s / 180s / 600s / 1200s / 1800s / 3600s
|
||||||
|
- 短工程奖励低但灵活(适合短会话)
|
||||||
|
- 长工程奖励高但需等待(适合过夜挂机)
|
||||||
|
- 玩家自主选择节奏
|
||||||
|
|
||||||
|
### 5.3 解锁条件
|
||||||
|
- 0 飞升 + 0.5/s:新手 5 分钟内可解锁第一个
|
||||||
|
- 1 飞升:玩家完成首次飞升后解锁中段
|
||||||
|
- 2 飞升 + 10/s:长线目标
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 验证记录
|
||||||
|
|
||||||
|
### 6.1 Lint
|
||||||
|
- ✅ 零错误零警告
|
||||||
|
|
||||||
|
### 6.2 Dev 服务器
|
||||||
|
- ✅ HTTP 200
|
||||||
|
- ✅ 无运行时错误
|
||||||
|
|
||||||
|
### 6.3 Agent Browser 端到端测试
|
||||||
|
1. ✅ 默认 tab 是「放置」
|
||||||
|
2. ✅ Header 显示「放置中 +0.4/s」徽章
|
||||||
|
3. ✅ 4 段面板完整渲染
|
||||||
|
4. ✅ 买 min_1+min_2 后产能 2.6/s,解锁 idle_scan + idle_refine
|
||||||
|
5. ✅ 点击「派遣至槽位 1」→ 槽位显示运行中 60s + 进度条 + 取消按钮
|
||||||
|
6. ✅ IdleProjectBar 同步显示迷你 pill
|
||||||
|
7. ✅ 快进完成后标记 completed=true
|
||||||
|
8. ✅ 领取后 insights +8、projectsCompleted 0→1、槽位清空
|
||||||
|
|
||||||
|
### 6.4 Sticky Footer 验证
|
||||||
|
- ✅ Header `position: sticky; top: 0`
|
||||||
|
- ✅ Footer `position: sticky; bottom: 0; margin-top: auto`
|
||||||
|
- ✅ 内容超出视口时 footer 被自然推下(无浮动空隙)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 未解决问题 / 后续
|
||||||
|
|
||||||
|
- 🟡 `crystalsFromIdle` / `idleProjectHistory` 已在 store 累加但 UI 未展示——v0.15 可在统计 tab 补
|
||||||
|
- 🟡 永久产能加成可能需要飞升次数 softcap
|
||||||
|
- 🟡 缺「批量领取全部」按钮(玩家有 3 个槽位都完成时方便)
|
||||||
|
- 🟡 缺工程完成 toast 通知(目前只能在面板内看到「待领取」标志)
|
||||||
|
- 🟢 v0.15 可加 Dark Souls 式 loreFragments 多层文本,让"遗迹碎片整理"工程的随机碎片奖励更有叙事感
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 设计哲学总结
|
||||||
|
|
||||||
|
> "Incremental games are very raw. They're about relating to a process and becoming obsessed with its growth, like watching something you planted in the ground, through a time-lapse camera, grow into a tree." —— Clark (via Frank Lantz, New Yorker 2019)
|
||||||
|
|
||||||
|
回响星核 v0.14 的放置系统不只是把数字挂机增长,而是让玩家**看到**这个过程:
|
||||||
|
- 采矿无人机像植物在土里生长
|
||||||
|
- 放置工程像种子在发芽
|
||||||
|
- 离线收益像收获时节
|
||||||
|
|
||||||
|
玩家不再只是「数字变大」,而是「亲手布置了一个会自动运转的考古基地,然后离开,让它在自己不在的时候继续工作」。
|
||||||
|
|
||||||
|
这是 idle game 的本质:**让玩家信任系统会在他不在时为他工作**。
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
# v0.15 — 叙事深化(loreLayers)+ 放置平衡(softcap)+ UX 增强
|
||||||
|
|
||||||
|
> 回响星核 / Echo Nexus · v0.15 设计文档
|
||||||
|
> 对应 worklog `Task ID: v0.15-release`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、动机
|
||||||
|
|
||||||
|
v0.14 上线了完整的放置系统(6 工程 + 永久加成 + idle 徽章),但留下两个问题:
|
||||||
|
|
||||||
|
1. **平衡性漏洞**:`idle_resonance` 工程每次完成 +8/s 永久加成,玩家可反复派遣导致滚雪球,几小时后产能爆炸。
|
||||||
|
2. **叙事单薄**:8 个记忆碎片只有一层 `echo` 文本,解锁后即"读完",缺乏 Dark Souls / Outer Wilds 式的碎片叙事深度——这是项目的**差异化竞争力**。
|
||||||
|
|
||||||
|
v0.15 用两个并行子代理一次性解决:
|
||||||
|
|
||||||
|
- **2-a(平衡 + UX)**:永久加成 softcap + 批量领取 + 完成 toast + StatsPanel idle 统计
|
||||||
|
- **2-b(叙事深化)**:8 碎片 × 2 层 = 16 层 loreLayers 多层文本 + Codex 分层渲染
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、平衡:永久加成 Softcap
|
||||||
|
|
||||||
|
### 公式
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function applyIdleSoftcap(bonus: number): number {
|
||||||
|
if (bonus <= 30) return bonus; // 全额
|
||||||
|
if (bonus <= 100) return 30 + (bonus - 30) * 0.5; // 超出部分 ×0.5
|
||||||
|
return 65 + (bonus - 100) * 0.25; // 超出部分 ×0.25
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 临界值
|
||||||
|
|
||||||
|
| 原始 bonus | 有效 bonus | 衰减率 |
|
||||||
|
|-----------|-----------|--------|
|
||||||
|
| 30 | 30 | 0% |
|
||||||
|
| 50 | 40 | 20% |
|
||||||
|
| 100 | 65 | 35% |
|
||||||
|
| 200 | 90 | 55% |
|
||||||
|
| 500 | 152.5 | 69.5% |
|
||||||
|
|
||||||
|
### 设计意图
|
||||||
|
|
||||||
|
- **30 以下不衰减**:新手期 + 偶尔完成 `idle_refine`(+0.3/s)不受惩罚
|
||||||
|
- **30-100 线性半衰**:中段玩家反复完成 `idle_drones`(+2/s)开始感到边际递减
|
||||||
|
- **100 以上严重衰减**:硬核玩家刷 `idle_resonance`(+8/s)收益大幅缩水,但仍非零(保留进度感)
|
||||||
|
- **永不归零**:`applyIdleSoftcap(bonus) → ∞` 时仍趋向无穷,只是增速极慢——避免"刷到上限"的挫败感
|
||||||
|
|
||||||
|
### UI 展示
|
||||||
|
|
||||||
|
`IdleOperationsPanel` Section 1「永久加成」StatTile:
|
||||||
|
- 有效值永远显示(如 `+40/s`)
|
||||||
|
- 当 `有效 < 原始` 时,下方加 amber 副行:`有效 +40/s(原始 +50/s · 衰减中)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、叙事深化:loreLayers 多层碎片文本
|
||||||
|
|
||||||
|
### 类型扩展
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface FragmentLoreLayer {
|
||||||
|
layer: 1 | 2; // 1=中层,2=深层
|
||||||
|
title: string; // "隐秘注脚" / "深层回响"
|
||||||
|
text: string; // 60-150 字叙事文本
|
||||||
|
unlockHint: string; // "飞升 1 次后揭示"
|
||||||
|
unlock: FragmentLoreUnlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FragmentLoreUnlock {
|
||||||
|
kind: "ascensions" | "totalDecoded" | "contact" | "fragmentCount" | "bossKills";
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5 种解锁条件
|
||||||
|
|
||||||
|
| kind | 含义 | 典型阈值 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `ascensions` | 飞升次数 | 1 / 2 / 3 |
|
||||||
|
| `totalDecoded` | 累计解码晶体数 | 20 / 40 / 60 |
|
||||||
|
| `contact` | 接触进度 | 50 / 80 |
|
||||||
|
| `fragmentCount` | 已解锁碎片数 | 4 / 6 |
|
||||||
|
| `bossKills` | 累计击杀 BOSS | 3 / 8 |
|
||||||
|
|
||||||
|
### 5 条叙事暗线(贯穿 16 层)
|
||||||
|
|
||||||
|
1. **追逐者**:以太族并非自然消亡,而是被虚空中"某种东西"追逐而集体飞升
|
||||||
|
2. **回响同源**:玩家舰载 AI"回响"的频段直接复制自以太族议会档案——玩家其实是以太族的延续/入口
|
||||||
|
3. **信任即引诱**:第一纪元的"信任后来者"实为"引诱后来者替他们完成未竟之事"
|
||||||
|
4. **飞升是单向门**:每次飞升都在消耗什么,不是奖赏
|
||||||
|
5. **回归的不是他们**:接触进度满 80 时回归的,是"借他们形状的某种东西",请玩家别回应
|
||||||
|
|
||||||
|
### 示例(f1_1 第一缕谐振)
|
||||||
|
|
||||||
|
**L0 表层**(默认):
|
||||||
|
> 在被称作「寂灭前夜」的年代,以太族第一次捕捉到来自虚空深处的谐振——那不是声音,而是存在的回声。他们将之记录在第一颗记忆晶体里。
|
||||||
|
|
||||||
|
**L1 隐秘注脚**(飞升 ≥1):
|
||||||
|
> 档案的边缘有一行几乎被擦去的批注:「那并非来自虚空——而是来自我们身后。我们一直不敢回头。」
|
||||||
|
|
||||||
|
**L2 深层回响**(累计解码 ≥40):
|
||||||
|
> 你终于明白:那缕谐振的频率,与你的舰载 AI「回响」的核心频段完全一致。你不是在考古——你是在被召唤。而你身后的东西,一直在等。
|
||||||
|
|
||||||
|
### Codex 分层渲染
|
||||||
|
|
||||||
|
| 层级 | 颜色 | 标题 | 未解锁状态 |
|
||||||
|
|------|------|------|-----------|
|
||||||
|
| L0 表层 | emerald | 表层回响 | 永远显示 |
|
||||||
|
| L1 中层 | amber | 隐秘注脚 | `???` + `blur-sm` 全息乱码 + `🔒 unlockHint` |
|
||||||
|
| L2 深层 | fuchsia | 深层回响 | `???` + `blur-sm` 全息乱码 + `🔒 unlockHint` |
|
||||||
|
|
||||||
|
顶部统计:`深层叙事 X / 16 已解锁`
|
||||||
|
列表卡片:已解锁深层文本的碎片标题旁显示 amber/fuchsia glow 小圆点
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、UX 增强
|
||||||
|
|
||||||
|
### 4.1 批量领取全部
|
||||||
|
|
||||||
|
`claimAllIdleProjects()` action:
|
||||||
|
- 遍历 3 槽位,对所有 `completed === true` 执行奖励逻辑
|
||||||
|
- 单次 `set()` + 单次 `syncStats()`(避免多次 re-render)
|
||||||
|
- 无可领取 → 直接 return(不弹 toast)
|
||||||
|
- 成功 → 汇总 toast `✦ 批量领取完成 · 共领取 N 项工程 · +X 晶体 · +Y 洞见 · ...`
|
||||||
|
|
||||||
|
UI:Section 3 标题右侧,当存在 completed slot 时显示 emerald「✓ 领取全部」按钮(h-5 px-2 text-[10px])
|
||||||
|
|
||||||
|
### 4.2 工程完成 toast
|
||||||
|
|
||||||
|
`tickIdleProjects` 检测状态转变瞬间(旧 `!completed` → 新 `completed`):
|
||||||
|
- 收集 `justCompletedProjects` 数组
|
||||||
|
- 循环外单次 `toast({ title: "✦ 放置工程完成", description: "${icon} ${name} · 可前往「放置」标签领取" })`
|
||||||
|
- 避免每 250ms tick 重复弹窗
|
||||||
|
|
||||||
|
### 4.3 StatsPanel idle 统计
|
||||||
|
|
||||||
|
从 `page.tsx` 内联函数抽出为独立组件 `StatsPanel.tsx`:
|
||||||
|
- `useShallow` 精细订阅 24 字段
|
||||||
|
- 新增 idle 统计行:
|
||||||
|
- 放置永久加成 `+X /s`
|
||||||
|
- 完成放置工程 `N 项`
|
||||||
|
- 放置产出晶体 `formatNum(N)`
|
||||||
|
- 放置工程历史 `N 条`(最近 50 条,跨周目)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、技术实现
|
||||||
|
|
||||||
|
### 修改文件清单(8 个)
|
||||||
|
|
||||||
|
**子代理 2-a(5 文件):**
|
||||||
|
- `src/lib/game/engine.ts` — 新增 `applyIdleSoftcap` 导出 + `recomputeStats` 调用
|
||||||
|
- `src/store/gameStore.ts` — `GameActions` 加 `claimAllIdleProjects` 签名 + `tickIdleProjects` 改写 + 新增 action
|
||||||
|
- `src/components/game/IdleOperationsPanel.tsx` — softcap 副行 + 领取全部按钮 + StatTile 扩展
|
||||||
|
- `src/components/game/StatsPanel.tsx` — 新建独立组件
|
||||||
|
- `src/app/page.tsx` — 最小集成(删除内联 + import)
|
||||||
|
|
||||||
|
**子代理 2-b(3 文件):**
|
||||||
|
- `src/lib/game/types.ts` — `FragmentLoreLayer` / `FragmentLoreUnlock` 接口 + `Fragment.loreLayers` 字段
|
||||||
|
- `src/lib/game/config.ts` — FRAGMENTS 全部填充 loreLayers + `isLoreLayerUnlocked` 函数
|
||||||
|
- `src/components/game/Codex.tsx` — useShallow + 顶部统计 + 列表小圆点 + 3 层分层渲染弹层
|
||||||
|
|
||||||
|
### 并行零冲突
|
||||||
|
|
||||||
|
两个子代理修改文件严格不重叠:
|
||||||
|
- 2-a 不碰 `types.ts` / `config.ts` / `Codex.tsx`
|
||||||
|
- 2-b 不碰 `engine.ts` / `gameStore.ts` / `IdleOperationsPanel.tsx` / `StatsPanel.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、验证
|
||||||
|
|
||||||
|
- ✅ `bun run lint` 零错误零警告
|
||||||
|
- ✅ HTTP 200,dev.log 无运行时错误
|
||||||
|
- ✅ agent-browser 端到端 smoke test:
|
||||||
|
- softcap:permBonus=50 → 有效 40,UI 显示"衰减中" ✓
|
||||||
|
- 批量领取:permBonus 0→0.3, projectsCompleted 1→2 ✓
|
||||||
|
- 完成 toast:tickIdleProjects 触发 ✓
|
||||||
|
- Codex 分层:L0 显示 / L1·L2 模糊 → ascensions=1 解锁 L1 → totalDecoded=50 解锁 L2 ✓
|
||||||
|
- 深层叙事计数 0→2→6 随条件递增 ✓
|
||||||
|
- StatsPanel idle 4 行统计全显示 ✓
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、哲学
|
||||||
|
|
||||||
|
### 碎片叙事的留白
|
||||||
|
|
||||||
|
回响星核的叙事不靠过场动画或长篇对话,而是**让玩家自己拼凑**。每个碎片的多层文本设计遵循:
|
||||||
|
|
||||||
|
1. **表层温和**:第一遍读像抒情散文,不带压迫感
|
||||||
|
2. **中层矛盾**:揭示表层没说的东西,制造认知裂缝
|
||||||
|
3. **深层颠覆**:推翻表层认知,但留有解读空间(不直接说"怪物是谁")
|
||||||
|
|
||||||
|
### 平衡与保留
|
||||||
|
|
||||||
|
softcap 的设计哲学:**衰减但不封顶**。
|
||||||
|
- 衰减:防止滚雪球破坏游戏节奏
|
||||||
|
- 不封顶:尊重硬核玩家的投入,即使边际收益递减,进度感始终存在
|
||||||
|
|
||||||
|
这与飞升系统的"单向门"叙事呼应——每次进步都有代价,但绝非徒劳。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、下一步(v0.16+)
|
||||||
|
|
||||||
|
- 🟡 loreLayer "已读"标记(需改 gameStore,本轮避开边界冲突)
|
||||||
|
- 🟡 解码阈值释放叙事碎片 + 星潮三频段切换
|
||||||
|
- 🟡 健康设计:每日软上限 + "星核建议休眠"提醒
|
||||||
|
- 🟡 移动端 375px Codex 弹层可读性实测
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# v0.16 — 解码阈值叙事事件 + loreLayer 已读 + idleHistory 详情 + 移动端适配
|
||||||
|
|
||||||
|
> 回响星核 / Echo Nexus · v0.16 设计文档
|
||||||
|
> 对应 worklog `Task ID: 2-a` + `2-b` + `v0.16-release`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、动机
|
||||||
|
|
||||||
|
v0.15 上线了 loreLayers 多层碎片文本,但存在三个未完善点:
|
||||||
|
|
||||||
|
1. **解锁瞬间缺乏仪式感** — 碎片静默出现在图谱,没有"叙事涌现"的高光时刻
|
||||||
|
2. **loreLayer 没有已读标记** — 玩家无法区分"新解锁未读"和"已读",缺乏进度感
|
||||||
|
3. **idleProjectHistory 只显示计数** — 无法查看具体完成了哪些工程、何时完成
|
||||||
|
4. **移动端 375px 未实测** — v0.15 的 Codex 分层弹层在小屏可读性未知
|
||||||
|
|
||||||
|
v0.16 用两个并行子代理一次性解决:
|
||||||
|
|
||||||
|
- **2-a**:5 个解码阈值叙事里程碑 + 全屏"碎片释放"叠层
|
||||||
|
- **2-b**:loreLayer 已读标记 + idleHistory 详情展开 + 移动端适配审计
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、解码阈值叙事里程碑(2-a)
|
||||||
|
|
||||||
|
### 设计哲学
|
||||||
|
|
||||||
|
回响星核的核心卖点是**"解码驱动叙事涌现"**。v0.15 的 loreLayers 让每个碎片有 3 层文本,但解锁是静默的。v0.16 在 `totalDecoded` 达到 5/12/25/40/60 时触发**全屏叙事叠层**——不是解锁新碎片,而是对已解锁碎片的**回顾性咏叹**,类似游戏中的"过场回忆"。
|
||||||
|
|
||||||
|
5 个阈值与 FRAGMENTS 的 threshold(1/3/6/10/15/22/32/45)**错开**,是额外的叙事节奏点。
|
||||||
|
|
||||||
|
### 5 个里程碑
|
||||||
|
|
||||||
|
| threshold | id | title | color | 主题 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 5 | ms_5 | 回响 · 初鸣 | emerald | 身份觉醒(你是回响) |
|
||||||
|
| 12 | ms_12 | 代价 · 锚痕渐浅 | rose | 代价感知(解码在消耗锚痕) |
|
||||||
|
| 25 | ms_25 | 信任 · 钩上的光 | amber | 信任的重量("信任"实为引诱) |
|
||||||
|
| 40 | ms_40 | 身后 · 整齐的虚空 | fuchsia | 身后的东西(追逐者逼近) |
|
||||||
|
| 60 | ms_60 | 回归 · 借形之物 | emerald | 回归的预兆(接触满溢借形回归) |
|
||||||
|
|
||||||
|
每段 body 文本 150-250 字,与 v0.15 loreLayers 的 5 条暗线呼应:
|
||||||
|
1. 追逐者(以太族被虚空追逐者逼迫飞升)
|
||||||
|
2. 回响同源(玩家 AI 频段与以太族议会档案同源)
|
||||||
|
3. 信任即引诱("信任后来者"实为"引诱替他们完成未竟之事")
|
||||||
|
4. 飞升单向门(每次飞升都在消耗什么)
|
||||||
|
5. 回归非他们(接触满 80 时回归的是"借形之物")
|
||||||
|
|
||||||
|
### 技术实现
|
||||||
|
|
||||||
|
**新建 `src/lib/game/narrativeMilestones.ts`**:
|
||||||
|
- `NarrativeMilestone` 接口(threshold/id/title/subtitle/body/color)
|
||||||
|
- `NARRATIVE_MILESTONES` 数组(5 个里程碑)
|
||||||
|
- `findMilestoneForTotal(total, triggered)` 纯函数(返回未触发的里程碑)
|
||||||
|
|
||||||
|
**GameState 扩展**:
|
||||||
|
- `triggeredMilestones: string[]` — 已触发的 milestone id 集合(跨周目永久)
|
||||||
|
- `pendingMilestone: NarrativeMilestone | null` — 待展示的里程碑(叠层读取)
|
||||||
|
|
||||||
|
**Store 集成**:
|
||||||
|
- 两个解码完成点(`clickNode` line 596 + `autoDecodeTick` line 711)检测 `findMilestoneForTotal(newTotal, triggered)`
|
||||||
|
- 命中则加入 `triggeredMilestones` + 设置 `pendingMilestone`
|
||||||
|
- `dismissMilestone()` action 清空 `pendingMilestone`
|
||||||
|
- persist migrate 补全两字段(version 仍为 1)
|
||||||
|
- `performPrestige` 保留 `triggeredMilestones`(不重复看过场)+ 清空 `pendingMilestone`
|
||||||
|
|
||||||
|
### 全屏叠层组件
|
||||||
|
|
||||||
|
**新建 `src/components/game/NarrativeMilestoneOverlay.tsx`**:
|
||||||
|
|
||||||
|
- `fixed inset-0 z-50` + `bg-black/80 backdrop-blur-sm`(高于 StarTideOverlay 的 z-0)
|
||||||
|
- **粒子边框**:全屏 4 色主题色 inset box-shadow(120px + 240px 双层)+ `narrative-breathe` 3.5s 呼吸循环
|
||||||
|
- **中央卡片**:max-w-2xl,4 色主题渐变背景 + 主题色 border + 60px glow
|
||||||
|
- **排版层级**:
|
||||||
|
- 副标题 `text-[10px] sm:text-xs font-mono tracking-[0.3em] uppercase`
|
||||||
|
- 大标题 `text-3xl sm:text-4xl md:text-5xl font-serif font-bold` + textShadow glow
|
||||||
|
- 装饰分隔线 `linear-gradient`
|
||||||
|
- 主体文本 `text-sm sm:text-base md:text-lg leading-relaxed whitespace-pre-line`
|
||||||
|
- **入场动画**:`narrative-fade-in` 300ms(opacity 0→1)+ `narrative-scale-in` 300ms cubic-bezier(scale 1.05→1)
|
||||||
|
- **逐字浮现**:30ms/字 setTimeout 推进 + inline-block 光标 `narrative-cursor` 1s steps(2) 闪烁
|
||||||
|
- **交互**:
|
||||||
|
- 未完全显示时:点击 → skip 到全文(底部提示"点击跳过逐字显示")
|
||||||
|
- 完全显示后:点击/Esc → `dismissMilestone()`(底部提示"点击任意处或按 Esc 继续")
|
||||||
|
- **移动端 375px**:卡片 px-4 py-6 / 标题 text-3xl / 正文 text-sm / 光标 h-4
|
||||||
|
- **结构巧思**:外层订阅 + 内层 `key={milestone.id}` 重挂,state lazy init 为 0,规避 React 19 `react-hooks/set-state-in-effect` 规则
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、loreLayer 已读标记(2-b)
|
||||||
|
|
||||||
|
### 类型扩展
|
||||||
|
|
||||||
|
`GameState.readLoreLayers: string[]` — 格式 `["f1_1_L1", "f1_1_L2", ...]`
|
||||||
|
|
||||||
|
### Store
|
||||||
|
|
||||||
|
- `markLoreLayerRead(fragmentId, layer)` action:构造 key `${fragmentId}_L${layer}`,已存在则 no-op
|
||||||
|
- persist migrate 补全 `readLoreLayers: []`
|
||||||
|
- `performPrestige` 保留(跨周目永久,已读不重置)
|
||||||
|
|
||||||
|
### Codex 分层渲染增强
|
||||||
|
|
||||||
|
- **已读层**:标题旁 muted "✓ 已读" + 边框降级为 `border-white/10`
|
||||||
|
- **未读但已解锁层**:保持 v0.15 高亮(amber/fuchsia 边框 + glow)+ 标题旁 `animate-pulse` 脉冲小圆点 + "新" 字 + "▸ 点击文本标记已读" 引导
|
||||||
|
- **触发方式**:点击层文本区域(`<button>` 元素)即调用 `markLoreLayerRead`,无需额外按钮
|
||||||
|
- **顶部统计**:从单行扩展为两行(移动端 stack / 桌面并排):`深层叙事 X/Y 已解锁` + `已读 A/B`
|
||||||
|
- **列表卡片小圆点**:未读 pulse + glow,已读静态降亮
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、idleProjectHistory 详情展开(2-b)
|
||||||
|
|
||||||
|
### StatsPanel 增强
|
||||||
|
|
||||||
|
- "放置工程历史" 行改为 `col-span-2` 可展开按钮:左侧 label + "(最近 N 条可展开)",右侧 "N 条" + ChevronDown/ChevronUp
|
||||||
|
- 展开后渲染倒序最近 5 条:每行 = 工程 icon + 工程名(truncate)+ 相对时间
|
||||||
|
- 最近一条用 `bg-fuchsia-500/5` 高亮
|
||||||
|
- 超 5 条时底部加"仅展示最近 5 条 · 共 N 条历史"
|
||||||
|
- 空态友好文案
|
||||||
|
|
||||||
|
### 新建 `src/lib/game/format.ts`
|
||||||
|
|
||||||
|
`formatRelativeTime(ts: number): string`:
|
||||||
|
- `< 60s` → "刚刚"
|
||||||
|
- `< 60m` → "X 分钟前"
|
||||||
|
- `< 24h` → "X 小时前"
|
||||||
|
- `< 7d` → "X 天前"
|
||||||
|
- 否则 → 具体日期
|
||||||
|
|
||||||
|
独立文件便于后续复用(编年史时间轴、探险日志等)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、移动端 375px 适配审计(2-b)
|
||||||
|
|
||||||
|
### 审计结果
|
||||||
|
|
||||||
|
审了 5 个组件,修了 4 个:
|
||||||
|
|
||||||
|
| 组件 | 问题 | 修复 |
|
||||||
|
|------|------|------|
|
||||||
|
| `IdleOperationsPanel.tsx` | 11 处 `text-[9px]` 低于最小字号 | `text-[9px]` → `text-[10px]` |
|
||||||
|
| `BeaconPanel.tsx` | 7 处 `text-[9px]` + 信标链底部统计行 375px 溢出 | 字号升级 + `flex-wrap + gap-y-1` |
|
||||||
|
| `Codex.tsx` | 弹层 padding + 标题 + 顶部统计在小屏拥挤 | `p-5` → `p-4 sm:p-5` / 标题 `text-lg` → `text-base sm:text-lg` / 顶部统计 `flex flex-col sm:flex-row` |
|
||||||
|
| `StatsPanel.tsx` | 2 处 `text-[9px]` | `text-[10px]` |
|
||||||
|
|
||||||
|
**无需改**:
|
||||||
|
- `ExpeditionPanel.tsx` — 节点路径已用 `overflow-x-auto` 自适应,按钮 `flex-1`,路径节点 36px 足够小
|
||||||
|
- BeaconPanel 4 里程碑节点 `w-12 h-12` × 4 = 192px 在 375px 可容纳
|
||||||
|
- IdleOperationsPanel 4 section 已用 `grid-cols-1 sm:grid-cols-N` 响应式降级
|
||||||
|
|
||||||
|
### 移动端字号规范
|
||||||
|
|
||||||
|
- 最小 `text-[10px]`(9px 在高 DPI 屏可读性差)
|
||||||
|
- 标题最大 `text-2xl`(移动端)/ `text-3xl`(桌面)
|
||||||
|
- 优先用 `sm:` `md:` 响应式前缀,不删桌面端样式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、技术实现
|
||||||
|
|
||||||
|
### 修改文件清单(11 个)
|
||||||
|
|
||||||
|
**子代理 2-a(6 文件):**
|
||||||
|
- 新建 `src/lib/game/narrativeMilestones.ts`
|
||||||
|
- 新建 `src/components/game/NarrativeMilestoneOverlay.tsx`
|
||||||
|
- 修改 `src/lib/game/types.ts`(加 triggeredMilestones + pendingMilestone)
|
||||||
|
- 修改 `src/lib/game/engine.ts`(createInitialState + performPrestige)
|
||||||
|
- 修改 `src/store/gameStore.ts`(2 解码点检测 + dismissMilestone + migrate)
|
||||||
|
- 修改 `src/app/page.tsx`(挂载叠层 + 版本号)
|
||||||
|
|
||||||
|
**子代理 2-b(8 文件,2 个与 2-a 共享):**
|
||||||
|
- 新建 `src/lib/game/format.ts`
|
||||||
|
- 修改 `src/lib/game/types.ts`(加 readLoreLayers,与 2-a 共存)
|
||||||
|
- 修改 `src/lib/game/engine.ts`(createInitialState + performPrestige,与 2-a 共存)
|
||||||
|
- 修改 `src/store/gameStore.ts`(markLoreLayerRead + migrate,与 2-a 共存)
|
||||||
|
- 修改 `src/components/game/Codex.tsx`(已读标记 + 点击逻辑 + 顶部统计)
|
||||||
|
- 修改 `src/components/game/StatsPanel.tsx`(idleHistory 展开 + 字号)
|
||||||
|
- 修改 `src/components/game/IdleOperationsPanel.tsx`(字号 9→10px)
|
||||||
|
- 修改 `src/components/game/BeaconPanel.tsx`(字号 + flex-wrap)
|
||||||
|
|
||||||
|
### 并行零冲突
|
||||||
|
|
||||||
|
两个子代理在 3 个共享文件(types.ts / engine.ts / gameStore.ts)中:
|
||||||
|
- 字段名不冲突(`triggeredMilestones`/`pendingMilestone` vs `readLoreLayers`)
|
||||||
|
- action 不冲突(`dismissMilestone` vs `markLoreLayerRead`)
|
||||||
|
- migrate 补全各行独立
|
||||||
|
- 2-b 不动解码逻辑(line 575-720),那是 2-a 的领地
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、验证
|
||||||
|
|
||||||
|
- ✅ `bun run lint` 零错误零警告
|
||||||
|
- ✅ HTTP 200,dev.log 无运行时错误
|
||||||
|
- ✅ agent-browser 端到端 smoke test:
|
||||||
|
- 里程碑叠层:setState pendingMilestone=ms_5 → 全屏 dialog "回响 · 初鸣" + "点击任意处或按 Esc 继续" → 点击 → pending 清空 + triggered 保留 ✓
|
||||||
|
- loreLayer 已读:解锁 f1_1 + ascensions=1 → L1 "隐秘注脚" 显示 "新" + "标记为已读" 按钮 → 点击 → readLoreLayers=["f1_1_L1"] + L1 显示 "已读" + 顶部统计更新 ✓
|
||||||
|
- idleHistory 展开:设置 3 条历史 → 统计 tab "放置工程历史(最近 3 条可展开)" → 点击展开 → 倒序显示 3 行(icon + 工程名 + 相对时间)✓
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、哲学
|
||||||
|
|
||||||
|
### 仪式感与节奏
|
||||||
|
|
||||||
|
v0.16 的里程碑叠层不是"奖励",而是**节奏标记**。放置游戏容易陷入"数字增长麻木",5 个阈值(5/12/25/40/60)在玩家的解码旅程中设置了 5 个"停下来看一看"的节点:
|
||||||
|
|
||||||
|
- **ms_5**:新手期结束,确认身份
|
||||||
|
- **ms_12**:开始感知代价
|
||||||
|
- **ms_25**:中期转折,信任的重量
|
||||||
|
- **ms_40**:后期紧迫,追逐者逼近
|
||||||
|
- **ms_60**:终局预兆,回归的借形之物
|
||||||
|
|
||||||
|
每个里程碑的 body 文本都是**回顾性咏叹**——不是新剧情,而是对已解锁碎片的重新诠释。这让玩家在数字增长中保持对叙事的敏感。
|
||||||
|
|
||||||
|
### 已读标记的心理学
|
||||||
|
|
||||||
|
loreLayer 已读标记解决了一个细微的 UX 问题:玩家解锁深层文本后,如果不标记"已读",每次打开图谱都被高亮提示淹没,反而会**忽略真正的新内容**。已读标记让"新"保持稀缺,让玩家的注意力聚焦在真正未读的层上。
|
||||||
|
|
||||||
|
这与健康游戏设计呼应——**进度感不等于信息过载**。标记已读是一种"我处理过了"的仪式,让玩家心安。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、下一步(v0.17+)
|
||||||
|
|
||||||
|
- 🟡 星潮三频段切换(星潮系统增强)
|
||||||
|
- 🟡 健康设计:每日软上限 + "星核建议休眠"提醒
|
||||||
|
- 🟡 loreLayer 已读标记可扩展到碎片本身(fragment 级别的已读)
|
||||||
|
- 🟡 里程碑叠层的音效(程序化生成,与星潮音效区分)
|
||||||
|
- 🟡 format.ts 复用到编年史时间轴 + 探险日志
|
||||||
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 234 KiB |
@@ -1,7 +1,21 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
// 静态导出开关:BUILD_EXPORT=true 时启用 output:export + basePath
|
||||||
|
// 这样 dev 服务器不受影响,仅构建静态资源时切换配置
|
||||||
|
const isExport = process.env.BUILD_EXPORT === "true";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: isExport ? "export" : "standalone",
|
||||||
|
// 部署到 https://gitea-pages.atdunbg.xyz/Super_Z/echo-nexus 时需要 basePath
|
||||||
|
...(isExport
|
||||||
|
? {
|
||||||
|
basePath: "/Super_Z/echo-nexus",
|
||||||
|
trailingSlash: true,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
/* config options here */
|
/* config options here */
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000 2>&1 | tee dev.log",
|
"dev": "next dev -p 3000 2>&1 | tee dev.log",
|
||||||
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
|
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
|
||||||
|
"build:static": "BUILD_EXPORT=true next build",
|
||||||
"start": "NODE_ENV=production bun .next/standalone/server.js 2>&1 | tee server.log",
|
"start": "NODE_ENV=production bun .next/standalone/server.js 2>&1 | tee server.log",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,5 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
return NextResponse.json({ message: "Hello, world!" });
|
|
||||||
}
|
|
||||||
@@ -149,3 +149,19 @@
|
|||||||
0%, 100% { opacity: 0.6; }
|
0%, 100% { opacity: 0.6; }
|
||||||
50% { opacity: 1; }
|
50% { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 教程系统动画 */
|
||||||
|
@keyframes tut-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 0 2px rgba(232,121,249,0.9), 0 0 24px 4px rgba(232,121,249,0.4);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 3px rgba(232,121,249,1), 0 0 32px 8px rgba(232,121,249,0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 教程提示气泡入场 */
|
||||||
|
@keyframes tut-pop-in {
|
||||||
|
0% { opacity: 0; transform: scale(0.92) translateY(8px); }
|
||||||
|
100% { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
// 回响星核 / Echo Nexus — 游戏主入口
|
// 回响星核 / Echo Nexus — 游戏主入口
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { StarfieldCanvas } from "@/components/game/StarfieldCanvas";
|
import { StarfieldCanvas } from "@/components/game/StarfieldCanvas";
|
||||||
import { ResourceBar } from "@/components/game/ResourceBar";
|
import { ResourceBar } from "@/components/game/ResourceBar";
|
||||||
import { CrystalOrb } from "@/components/game/CrystalOrb";
|
import { CrystalOrb } from "@/components/game/CrystalOrb";
|
||||||
@@ -15,6 +15,16 @@ import { AchievementNotifier } from "@/components/game/AchievementNotifier";
|
|||||||
import { ConstellationPanel } from "@/components/game/ConstellationPanel";
|
import { ConstellationPanel } from "@/components/game/ConstellationPanel";
|
||||||
import { ConstellationDialog } from "@/components/game/ConstellationDialog";
|
import { ConstellationDialog } from "@/components/game/ConstellationDialog";
|
||||||
import { ChronicleDialog } from "@/components/game/ChronicleDialog";
|
import { ChronicleDialog } from "@/components/game/ChronicleDialog";
|
||||||
|
import { BeaconPanel } from "@/components/game/BeaconPanel";
|
||||||
|
import { TutorialOverlay } from "@/components/game/TutorialOverlay";
|
||||||
|
import { OfflineReportDialog } from "@/components/game/OfflineReportDialog";
|
||||||
|
import { CruiseMode } from "@/components/game/CruiseMode";
|
||||||
|
import { AttributesPanel } from "@/components/game/AttributesPanel";
|
||||||
|
import { IdleOperationsPanel } from "@/components/game/IdleOperationsPanel";
|
||||||
|
import { IdleStatusBadge } from "@/components/game/IdleStatusBadge";
|
||||||
|
import { IdleProjectBar } from "@/components/game/IdleProjectBar";
|
||||||
|
import { StatsPanel } from "@/components/game/StatsPanel";
|
||||||
|
import { NarrativeMilestoneOverlay } from "@/components/game/NarrativeMilestoneOverlay";
|
||||||
import {
|
import {
|
||||||
StarTideNotifier,
|
StarTideNotifier,
|
||||||
StarTideIndicator,
|
StarTideIndicator,
|
||||||
@@ -36,11 +46,16 @@ import {
|
|||||||
Github,
|
Github,
|
||||||
Trophy,
|
Trophy,
|
||||||
Star,
|
Star,
|
||||||
|
Radio,
|
||||||
|
Navigation,
|
||||||
|
User,
|
||||||
|
Clock,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
|
import { FRAGMENTS, formatNum } from "@/lib/game/config";
|
||||||
import { ACHIEVEMENTS } from "@/lib/game/achievements";
|
import { ACHIEVEMENTS } from "@/lib/game/achievements";
|
||||||
import { TIDE_EVENTS } from "@/lib/game/starTide";
|
import { TIDE_EVENTS } from "@/lib/game/starTide";
|
||||||
import { CONSTELLATION_PERKS } from "@/lib/game/constellation";
|
import { generateDailyChallenge, loadDailyProgress } from "@/lib/game/beacon";
|
||||||
|
import { getUnlockedIdleProjects } from "@/lib/game/idle";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
useGameLoop();
|
useGameLoop();
|
||||||
@@ -49,6 +64,7 @@ export default function Page() {
|
|||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [constellationOpen, setConstellationOpen] = useState(false);
|
const [constellationOpen, setConstellationOpen] = useState(false);
|
||||||
const [chronicleOpen, setChronicleOpen] = useState(false);
|
const [chronicleOpen, setChronicleOpen] = useState(false);
|
||||||
|
const [cruiseOpen, setCruiseOpen] = useState(false);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
const contact = useGameStore((s) => s.contact);
|
const contact = useGameStore((s) => s.contact);
|
||||||
@@ -66,13 +82,61 @@ export default function Page() {
|
|||||||
const energy = useGameStore((s) => s.energy);
|
const energy = useGameStore((s) => s.energy);
|
||||||
const hasActiveExpedition = useGameStore((s) => !!s.activeExpedition && !s.activeExpedition.finished);
|
const hasActiveExpedition = useGameStore((s) => !!s.activeExpedition && !s.activeExpedition.finished);
|
||||||
const chronicleCount = useGameStore((s) => s.chronicle?.length ?? 0);
|
const chronicleCount = useGameStore((s) => s.chronicle?.length ?? 0);
|
||||||
|
const pendingAttrPoints = useGameStore((s) => s.pendingAttrPoints ?? 0);
|
||||||
|
// v0.14 放置系统:用于驱动 goal 提示 + 标签默认值
|
||||||
|
const idleProjectSlots = useGameStore(
|
||||||
|
(s) => (s.idleProjectSlots ?? [null, null, null]) as (
|
||||||
|
| { projectId: string; completed: boolean }
|
||||||
|
| null
|
||||||
|
)[]
|
||||||
|
);
|
||||||
|
const createdAt = useGameStore((s) => s.createdAt);
|
||||||
|
|
||||||
|
// 深空信标:检测是否有可领取的奖励(独立 localStorage)
|
||||||
|
const [beaconClaimable, setBeaconClaimable] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
const check = () => {
|
||||||
|
try {
|
||||||
|
const c = generateDailyChallenge();
|
||||||
|
const p = loadDailyProgress();
|
||||||
|
if (!cancelled) {
|
||||||
|
setBeaconClaimable(p.completedAt !== null && !p.claimed && p.dateKey === c.dateKey);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setBeaconClaimable(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
const id = setInterval(check, 2000);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
clearInterval(id);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致
|
// 挂载检测:避免持久化 store 在 SSR/CSR 间产生 hydration 不一致
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// v0.14 放置系统:默认放置标签(在挂载后按优先级切换一次)
|
||||||
|
// 注意:所有 hooks 必须在早期 return 之前调用,保持调用顺序一致
|
||||||
|
const [activeTab, setActiveTab] = useState("idle");
|
||||||
|
const tabInited = useRef(false);
|
||||||
|
const hasPendingPerkEarly = !!(pendingPerkChoices && pendingPerkChoices.length > 0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (tabInited.current) return;
|
||||||
|
tabInited.current = true;
|
||||||
|
if (hasActiveExpedition) {
|
||||||
|
setActiveTab("expedition");
|
||||||
|
} else if (hasPendingPerkEarly) {
|
||||||
|
setActiveTab("constellation");
|
||||||
|
} else {
|
||||||
|
setActiveTab("idle");
|
||||||
|
}
|
||||||
|
}, [hasActiveExpedition, hasPendingPerkEarly]);
|
||||||
|
|
||||||
// 防止 SSR/CSR 不一致
|
// 防止 SSR/CSR 不一致
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
@@ -91,6 +155,19 @@ export default function Page() {
|
|||||||
// 仓库满仓警告
|
// 仓库满仓警告
|
||||||
const warehouseFull = crystals >= crystalCap * 0.98;
|
const warehouseFull = crystals >= crystalCap * 0.98;
|
||||||
|
|
||||||
|
// v0.14 放置系统相关 goal 提示
|
||||||
|
const idleClaimableCount = idleProjectSlots.filter(
|
||||||
|
(s) => s !== null && s.completed
|
||||||
|
).length;
|
||||||
|
const idleRunningCount = idleProjectSlots.filter(
|
||||||
|
(s) => s !== null && !s.completed
|
||||||
|
).length;
|
||||||
|
const playtimeSec = (Date.now() - (createdAt ?? Date.now())) / 1000;
|
||||||
|
const unlockedIdleCount = getUnlockedIdleProjects({
|
||||||
|
ascensions,
|
||||||
|
crystalsPerSec,
|
||||||
|
}).length;
|
||||||
|
|
||||||
// 目标提示
|
// 目标提示
|
||||||
let goal = "点击中央晶体发起脉冲,累积记忆晶体";
|
let goal = "点击中央晶体发起脉冲,累积记忆晶体";
|
||||||
if (totalDecoded === 0 && crystals >= 5) {
|
if (totalDecoded === 0 && crystals >= 5) {
|
||||||
@@ -117,6 +194,16 @@ export default function Page() {
|
|||||||
goal = `${tm.icon} 星潮「${tm.name}」进行中 · ${tm.desc}`;
|
goal = `${tm.icon} 星潮「${tm.name}」进行中 · ${tm.desc}`;
|
||||||
}
|
}
|
||||||
if (canPrestige) goal = "✦ 接触进度已满,可发起飞升进入新周目";
|
if (canPrestige) goal = "✦ 接触进度已满,可发起飞升进入新周目";
|
||||||
|
// v0.14 放置工程 goal 提示(高优先级,覆盖默认)
|
||||||
|
if (idleClaimableCount > 0) {
|
||||||
|
goal = `✦ 放置工程已完成 ${idleClaimableCount} 项,请前往「放置」标签领取奖励`;
|
||||||
|
} else if (
|
||||||
|
idleRunningCount === 0 &&
|
||||||
|
unlockedIdleCount > 0 &&
|
||||||
|
playtimeSec > 60
|
||||||
|
) {
|
||||||
|
goal = "「放置」标签可派遣工程项目,离线自动产出";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen flex flex-col bg-[#050410] text-foreground overflow-x-hidden">
|
<div className="relative min-h-screen flex flex-col bg-[#050410] text-foreground overflow-x-hidden">
|
||||||
@@ -138,12 +225,24 @@ export default function Page() {
|
|||||||
<div className="leading-tight">
|
<div className="leading-tight">
|
||||||
<h1 className="text-base sm:text-lg font-bold text-gradient">回响星核</h1>
|
<h1 className="text-base sm:text-lg font-bold text-gradient">回响星核</h1>
|
||||||
<p className="text-[9px] sm:text-[10px] text-muted-foreground/70 -mt-0.5 tracking-wider">
|
<p className="text-[9px] sm:text-[10px] text-muted-foreground/70 -mt-0.5 tracking-wider">
|
||||||
ECHO NEXUS · v0.4
|
ECHO NEXUS · v0.16
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex items-center gap-1.5">
|
<div className="ml-auto flex items-center gap-1.5">
|
||||||
<StarTideIndicator />
|
<StarTideIndicator />
|
||||||
|
<IdleStatusBadge onClick={() => setActiveTab("idle")} />
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setCruiseOpen(true)}
|
||||||
|
className="border-amber-400/50 text-amber-200 hover:bg-amber-500/10 h-8 px-2.5"
|
||||||
|
aria-label="深空巡航"
|
||||||
|
title="深空巡航 · 实时玩法"
|
||||||
|
>
|
||||||
|
<Navigation className="h-3.5 w-3.5 mr-1" />
|
||||||
|
巡航
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -175,6 +274,7 @@ export default function Page() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setPrestigeOpen(true)}
|
onClick={() => setPrestigeOpen(true)}
|
||||||
|
data-tut="prestige-btn"
|
||||||
className="border-fuchsia-400/50 text-fuchsia-200 hover:bg-fuchsia-500/10 h-8 px-2.5"
|
className="border-fuchsia-400/50 text-fuchsia-200 hover:bg-fuchsia-500/10 h-8 px-2.5"
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-3.5 w-3.5 mr-1" />
|
<RotateCcw className="h-3.5 w-3.5 mr-1" />
|
||||||
@@ -195,64 +295,94 @@ export default function Page() {
|
|||||||
<ResourceBar onPrestige={() => setPrestigeOpen(true)} />
|
<ResourceBar onPrestige={() => setPrestigeOpen(true)} />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主体 */}
|
{/* 主体 — 小屏允许自然滚动,避免 min-h 总和超过视口导致挤压重叠 */}
|
||||||
<main className="flex-1 px-3 sm:px-5 pb-3 min-h-0">
|
<main className="flex-1 px-3 sm:px-5 pb-3 min-h-0">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_minmax(360px,420px)] gap-3 h-full">
|
<div className="grid grid-cols-1 lg:grid-cols-[1fr_minmax(360px,420px)] gap-3 h-full">
|
||||||
{/* 左侧:脉冲晶体 */}
|
{/* 左侧:脉冲晶体 */}
|
||||||
<section className="glass rounded-2xl p-4 sm:p-6 flex flex-col items-center justify-center min-h-[420px] relative overflow-hidden">
|
<section className="glass rounded-2xl p-4 sm:p-6 flex flex-col items-center justify-center min-h-[340px] sm:min-h-[420px] relative overflow-hidden">
|
||||||
{/* 装饰光圈 */}
|
{/* 装饰光圈 */}
|
||||||
<div className="pointer-events-none absolute -top-20 -left-20 h-60 w-60 rounded-full bg-emerald-500/10 blur-3xl" />
|
<div className="pointer-events-none absolute -top-20 -left-20 h-60 w-60 rounded-full bg-emerald-500/10 blur-3xl" />
|
||||||
<div className="pointer-events-none absolute -bottom-20 -right-20 h-60 w-60 rounded-full bg-fuchsia-500/10 blur-3xl" />
|
<div className="pointer-events-none absolute -bottom-20 -right-20 h-60 w-60 rounded-full bg-fuchsia-500/10 blur-3xl" />
|
||||||
<CrystalOrb />
|
<div data-tut="crystal-orb" className="contents">
|
||||||
|
<CrystalOrb />
|
||||||
|
</div>
|
||||||
|
{/* v0.14 放置工程进度条(晶体球下方) */}
|
||||||
|
<IdleProjectBar />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* 右侧:解码 + 标签面板 */}
|
{/* 右侧:解码 + 标签面板 */}
|
||||||
<section className="flex flex-col gap-3 min-h-0">
|
<section className="flex flex-col gap-3 min-h-0">
|
||||||
{/* 解码面板 */}
|
{/* 解码面板 */}
|
||||||
<div className="glass rounded-2xl p-4 flex-1 min-h-[360px] max-h-[560px]">
|
<div data-tut="decode-panel" className="glass rounded-2xl p-4 flex-1 min-h-[300px] sm:min-h-[360px] max-h-[560px]">
|
||||||
<DecodePanel />
|
<DecodePanel />
|
||||||
</div>
|
</div>
|
||||||
{/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 统计 */}
|
{/* 标签面板:放置 / 探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */}
|
||||||
<div className="glass rounded-2xl p-3 min-h-[320px] max-h-[440px]">
|
<div className="glass rounded-2xl p-3 min-h-[280px] sm:min-h-[320px] max-h-[440px] flex flex-col">
|
||||||
<Tabs defaultValue={hasActiveExpedition ? "expedition" : hasPendingPerk ? "constellation" : "tech"} className="h-full flex flex-col">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
|
||||||
<TabsList className="grid grid-cols-6 h-8 bg-black/30">
|
<TabsList className="grid grid-cols-9 h-9 bg-black/30 gap-0.5 p-1">
|
||||||
<TabsTrigger value="expedition" className="text-[11px] gap-0.5 relative px-1">
|
<TabsTrigger value="idle" data-tut="tab-idle" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-emerald-500/15 data-[state=active]:shadow-[0_0_12px_rgba(52,211,153,0.3)] transition-all">
|
||||||
<Rocket className="h-3 w-3" />
|
<Clock className="h-3.5 w-3.5" />
|
||||||
探险
|
<span className="leading-none">放置</span>
|
||||||
|
{idleClaimableCount > 0 && (
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 min-w-[14px] h-[14px] px-1 rounded-full bg-emerald-500 text-[9px] font-mono font-bold text-white flex items-center justify-center border border-emerald-300/50 animate-pulse">
|
||||||
|
{idleClaimableCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="expedition" data-tut="tab-expedition" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
|
||||||
|
<Rocket className="h-3.5 w-3.5" />
|
||||||
|
<span className="leading-none">探险</span>
|
||||||
{energy >= 1 && !hasActiveExpedition && (
|
{energy >= 1 && !hasActiveExpedition && (
|
||||||
<span className="absolute -top-0.5 -right-0.5 h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse" />
|
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-amber-400 animate-pulse ring-1 ring-black/50" />
|
||||||
)}
|
)}
|
||||||
{hasActiveExpedition && (
|
{hasActiveExpedition && (
|
||||||
<span className="absolute -top-0.5 -right-0.5 h-1.5 w-1.5 rounded-full bg-rose-400 animate-pulse" />
|
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-rose-400 animate-pulse ring-1 ring-black/50" />
|
||||||
)}
|
)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="tech" className="text-[11px] gap-0.5 px-1">
|
<TabsTrigger value="tech" data-tut="tab-tech" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-emerald-500/15 data-[state=active]:shadow-[0_0_12px_rgba(52,211,153,0.3)] transition-all">
|
||||||
<Cpu className="h-3 w-3" />
|
<Cpu className="h-3.5 w-3.5" />
|
||||||
技术
|
<span className="leading-none">技术</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="constellation" className="text-[11px] gap-0.5 relative px-1">
|
<TabsTrigger value="constellation" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-fuchsia-500/15 data-[state=active]:shadow-[0_0_12px_rgba(232,121,249,0.3)] transition-all">
|
||||||
<Star className="h-3 w-3" />
|
<Star className="h-3.5 w-3.5" />
|
||||||
星图
|
<span className="leading-none">星图</span>
|
||||||
{hasPendingPerk && (
|
{hasPendingPerk && (
|
||||||
<span className="absolute -top-0.5 -right-0.5 h-1.5 w-1.5 rounded-full bg-fuchsia-400 animate-pulse" />
|
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-fuchsia-400 animate-pulse ring-1 ring-black/50" />
|
||||||
)}
|
)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="codex" className="text-[11px] gap-0.5 px-1">
|
<TabsTrigger value="codex" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-cyan-500/15 data-[state=active]:shadow-[0_0_12px_rgba(34,211,238,0.3)] transition-all">
|
||||||
<BookOpen className="h-3 w-3" />
|
<BookOpen className="h-3.5 w-3.5" />
|
||||||
图谱
|
<span className="leading-none">图谱</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="ach" className="text-[11px] gap-0.5 relative px-1">
|
<TabsTrigger value="ach" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
|
||||||
<Trophy className="h-3 w-3" />
|
<Trophy className="h-3.5 w-3.5" />
|
||||||
成就
|
<span className="leading-none">成就</span>
|
||||||
{ownedAchCount < ACHIEVEMENTS.length && (
|
{ownedAchCount < ACHIEVEMENTS.length && (
|
||||||
<span className="absolute -top-0.5 -right-0.5 h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse" />
|
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-amber-400 animate-pulse ring-1 ring-black/50" />
|
||||||
)}
|
)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="stats" className="text-[11px] gap-0.5 px-1">
|
<TabsTrigger value="beacon" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-emerald-500/15 data-[state=active]:shadow-[0_0_12px_rgba(52,211,153,0.3)] transition-all">
|
||||||
<BarChart3 className="h-3 w-3" />
|
<Radio className="h-3.5 w-3.5" />
|
||||||
统计
|
<span className="leading-none">信标</span>
|
||||||
|
{beaconClaimable && (
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-emerald-400 animate-pulse ring-1 ring-black/50" />
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="stats" className="text-[11px] gap-1 px-0.5 flex-col h-7 data-[state=active]:bg-slate-500/15 data-[state=active]:shadow-[0_0_12px_rgba(148,163,184,0.3)] transition-all">
|
||||||
|
<BarChart3 className="h-3.5 w-3.5" />
|
||||||
|
<span className="leading-none">统计</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="attributes" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-gradient-to-br data-[state=active]:from-emerald-500/15 data-[state=active]:via-fuchsia-500/15 data-[state=active]:to-rose-500/15 data-[state=active]:shadow-[0_0_12px_rgba(232,121,249,0.3)] transition-all">
|
||||||
|
<User className="h-3.5 w-3.5" />
|
||||||
|
<span className="leading-none">角色</span>
|
||||||
|
{pendingAttrPoints > 0 && (
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-rose-400 animate-pulse ring-1 ring-black/50" />
|
||||||
|
)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
<TabsContent value="idle" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
||||||
|
<IdleOperationsPanel />
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="expedition" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
<TabsContent value="expedition" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
||||||
<ExpeditionPanel />
|
<ExpeditionPanel />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -268,9 +398,15 @@ export default function Page() {
|
|||||||
<TabsContent value="ach" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
<TabsContent value="ach" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
||||||
<AchievementsPanel />
|
<AchievementsPanel />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="beacon" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
||||||
|
<BeaconPanel />
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="stats" className="flex-1 mt-2 min-h-0">
|
<TabsContent value="stats" className="flex-1 mt-2 min-h-0">
|
||||||
<StatsPanel />
|
<StatsPanel />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="attributes" className="flex-1 mt-2 min-h-0 data-[state=active]:flex data-[state=active]:flex-col">
|
||||||
|
<AttributesPanel />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -330,42 +466,13 @@ export default function Page() {
|
|||||||
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
||||||
<AchievementNotifier />
|
<AchievementNotifier />
|
||||||
<StarTideNotifier />
|
<StarTideNotifier />
|
||||||
|
<TutorialOverlay />
|
||||||
|
<OfflineReportDialog />
|
||||||
|
{cruiseOpen && <CruiseMode onClose={() => setCruiseOpen(false)} />}
|
||||||
|
{/* v0.16 叙事里程碑全屏叠层(pendingMilestone 非空时渲染,模态盖住一切) */}
|
||||||
|
<NarrativeMilestoneOverlay />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatsPanel() {
|
|
||||||
const s = useGameStore();
|
|
||||||
const achCount = Object.values(s.achievements).filter(Boolean).length;
|
|
||||||
const rows = [
|
|
||||||
{ label: "累计解码晶体", value: `${s.totalDecoded} 颗` },
|
|
||||||
{ label: "飞升周目", value: `${s.ascensions}` },
|
|
||||||
{ label: "持有蓝图", value: `${s.blueprints.length} / 6` },
|
|
||||||
{ label: "已学技术", value: `${Object.values(s.tech).filter((v) => v > 0).length} / ${TECH_TREE.length}` },
|
|
||||||
{ label: "已获碎片", value: `${Object.values(s.fragments).filter(Boolean).length} / ${FRAGMENTS.length}` },
|
|
||||||
{ label: "已解锁成就", value: `${achCount} / ${ACHIEVEMENTS.length}` },
|
|
||||||
{ label: "星图天赋", value: `${s.constellation?.length ?? 0} / ${CONSTELLATION_PERKS.length}` },
|
|
||||||
{ label: "接触进度", value: `${s.contact.toFixed(1)}%` },
|
|
||||||
{ label: "晶体产能", value: `${s.crystalsPerSec.toFixed(2)} /s` },
|
|
||||||
{ label: "仓库上限", value: `${formatNum(s.crystalCap)}` },
|
|
||||||
{ label: "洞见倍率", value: `×${s.insightMult.toFixed(2)}` },
|
|
||||||
{ label: "累计探险", value: `${s.totalExpeditions} 次` },
|
|
||||||
{ label: "BOSS 击破", value: `${s.bossKills ?? 0} 次` },
|
|
||||||
{ label: "星潮亲历", value: `${(s.starTidesEncountered ?? []).length} / 6` },
|
|
||||||
{ label: "编年史条目", value: `${(s.chronicle ?? []).length} 纪元` },
|
|
||||||
{ label: "探险能量", value: `${Math.floor(s.energy)} / ${s.energyMax}` },
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-2 gap-1.5 text-xs">
|
|
||||||
{rows.map((r) => (
|
|
||||||
<div
|
|
||||||
key={r.label}
|
|
||||||
className="flex items-center justify-between rounded-md bg-black/25 border border-white/5 px-2 py-1.5"
|
|
||||||
>
|
|
||||||
<span className="text-muted-foreground">{r.label}</span>
|
|
||||||
<span className="font-mono text-foreground">{r.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export function AchievementsPanel() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 成就网格 */}
|
{/* 成就网格 — 移除固定 max-h,用 flex-1 min-h-0 自适应父容器,避免小屏挤压重叠 */}
|
||||||
<div className="flex-1 grid grid-cols-1 sm:grid-cols-2 gap-1.5 content-start overflow-y-auto echo-scroll pr-1 max-h-[300px]">
|
<div className="flex-1 min-h-0 grid grid-cols-1 sm:grid-cols-2 gap-1.5 content-start overflow-y-auto echo-scroll pr-1">
|
||||||
{ACHIEVEMENTS.map((a) => {
|
{ACHIEVEMENTS.map((a) => {
|
||||||
const unlocked = !!achievements[a.id];
|
const unlocked = !!achievements[a.id];
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
"use client";
|
||||||
|
// 回响星核 / Echo Nexus — 角色属性面板(v0.7 P1)
|
||||||
|
//
|
||||||
|
// 无人机驾驶员四维属性可视化:
|
||||||
|
// • 探索力 emerald / 智慧 fuchsia / 勇气 amber / 灵感 rose
|
||||||
|
// • 显示数值(0-100)、加成百分比、经验进度条、等级 badge
|
||||||
|
// • 待分配属性点时高亮 + 闪烁提示,「+」按钮可分配 1 点
|
||||||
|
// • 顶部待分配点数提示;底部总览
|
||||||
|
// • 小屏 2×2 网格,大屏 1×4 横排
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useGameStore } from "@/store/gameStore";
|
||||||
|
import {
|
||||||
|
ATTRIBUTE_CONFIG,
|
||||||
|
ATTRIBUTE_KEYS,
|
||||||
|
ATTRIBUTE_HARD_CAP,
|
||||||
|
ATTRIBUTE_LINEAR_CAP,
|
||||||
|
getAttributeBonus,
|
||||||
|
getAllBonuses,
|
||||||
|
totalAttributeLevel,
|
||||||
|
totalAttributeBonusPct,
|
||||||
|
expRequiredForLevel,
|
||||||
|
type AttributeKey,
|
||||||
|
} from "@/lib/game/attributes";
|
||||||
|
import {
|
||||||
|
Compass,
|
||||||
|
Brain,
|
||||||
|
Swords,
|
||||||
|
Sparkles,
|
||||||
|
Plus,
|
||||||
|
ChevronUp,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
const ICON_MAP: Record<string, LucideIcon> = {
|
||||||
|
Compass,
|
||||||
|
Brain,
|
||||||
|
Swords,
|
||||||
|
Sparkles,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 主题色 → Tailwind/CSS 颜色映射 */
|
||||||
|
const COLOR_STYLES: Record<
|
||||||
|
AttributeKey,
|
||||||
|
{
|
||||||
|
text: string;
|
||||||
|
border: string;
|
||||||
|
borderActive: string;
|
||||||
|
bg: string;
|
||||||
|
ring: string;
|
||||||
|
barFrom: string;
|
||||||
|
barTo: string;
|
||||||
|
glow: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
exploration: {
|
||||||
|
text: "text-emerald-300",
|
||||||
|
border: "border-emerald-400/30",
|
||||||
|
borderActive: "border-emerald-400/70",
|
||||||
|
bg: "bg-emerald-500/10",
|
||||||
|
ring: "ring-emerald-400/40",
|
||||||
|
barFrom: "from-emerald-400",
|
||||||
|
barTo: "to-emerald-500",
|
||||||
|
glow: "rgba(52,211,153,0.45)",
|
||||||
|
},
|
||||||
|
wisdom: {
|
||||||
|
text: "text-fuchsia-300",
|
||||||
|
border: "border-fuchsia-400/30",
|
||||||
|
borderActive: "border-fuchsia-400/70",
|
||||||
|
bg: "bg-fuchsia-500/10",
|
||||||
|
ring: "ring-fuchsia-400/40",
|
||||||
|
barFrom: "from-fuchsia-400",
|
||||||
|
barTo: "to-fuchsia-500",
|
||||||
|
glow: "rgba(232,121,249,0.45)",
|
||||||
|
},
|
||||||
|
courage: {
|
||||||
|
text: "text-amber-300",
|
||||||
|
border: "border-amber-400/30",
|
||||||
|
borderActive: "border-amber-400/70",
|
||||||
|
bg: "bg-amber-500/10",
|
||||||
|
ring: "ring-amber-400/40",
|
||||||
|
barFrom: "from-amber-400",
|
||||||
|
barTo: "to-amber-500",
|
||||||
|
glow: "rgba(251,191,36,0.45)",
|
||||||
|
},
|
||||||
|
inspiration: {
|
||||||
|
text: "text-rose-300",
|
||||||
|
border: "border-rose-400/30",
|
||||||
|
borderActive: "border-rose-400/70",
|
||||||
|
bg: "bg-rose-500/10",
|
||||||
|
ring: "ring-rose-400/40",
|
||||||
|
barFrom: "from-rose-400",
|
||||||
|
barTo: "to-rose-500",
|
||||||
|
glow: "rgba(251,113,133,0.45)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AttributesPanel() {
|
||||||
|
const attributes = useGameStore((s) => s.attributes);
|
||||||
|
const attributeProgress = useGameStore((s) => s.attributeProgress);
|
||||||
|
const pendingAttrPoints = useGameStore((s) => s.pendingAttrPoints ?? 0);
|
||||||
|
const allocateAttribute = useGameStore((s) => s.allocateAttribute);
|
||||||
|
|
||||||
|
const bonuses = useMemo(() => getAllBonuses(attributes), [attributes]);
|
||||||
|
const totalLvl = useMemo(() => totalAttributeLevel(attributes), [attributes]);
|
||||||
|
const totalBonus = useMemo(
|
||||||
|
() => totalAttributeBonusPct(attributes),
|
||||||
|
[attributes]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2.5 h-full overflow-y-auto echo-scroll pr-0.5">
|
||||||
|
<style jsx global>{`
|
||||||
|
.echo-scroll::-webkit-scrollbar { width: 4px; }
|
||||||
|
.echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
|
||||||
|
.echo-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
@keyframes echo-pending-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(232,121,249,0.5); }
|
||||||
|
50% { box-shadow: 0 0 14px 4px rgba(232,121,249,0.65); }
|
||||||
|
}
|
||||||
|
.echo-pending-pulse {
|
||||||
|
animation: echo-pending-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes echo-card-glow {
|
||||||
|
0%, 100% { opacity: 0.5; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* 顶部:标题 + 待分配点数 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold flex items-center gap-1.5">
|
||||||
|
<span className="bg-gradient-to-br from-emerald-300 via-fuchsia-300 to-rose-300 bg-clip-text text-transparent">
|
||||||
|
驾驶员属性
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
className={`text-[10px] px-2 py-0.5 rounded-full border ${
|
||||||
|
pendingAttrPoints > 0
|
||||||
|
? "border-fuchsia-400/60 bg-fuchsia-500/15 text-fuchsia-200 echo-pending-pulse font-bold"
|
||||||
|
: "border-white/15 bg-black/25 text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
待分配 {pendingAttrPoints}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 属性卡片网格:小屏 2×2,大屏 1×4 */}
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
||||||
|
{ATTRIBUTE_KEYS.map((key) => {
|
||||||
|
const meta = ATTRIBUTE_CONFIG[key];
|
||||||
|
const cs = COLOR_STYLES[key];
|
||||||
|
const Icon = ICON_MAP[meta.icon] ?? Sparkles;
|
||||||
|
const value = attributes?.[key] ?? 0;
|
||||||
|
const prog = attributeProgress?.[key] ?? { exp: 0, level: value };
|
||||||
|
const bonusPct = getAttributeBonus(value) * 100;
|
||||||
|
const nextExp = expRequiredForLevel(value);
|
||||||
|
const curExp = prog.exp;
|
||||||
|
const expPct =
|
||||||
|
nextExp > 0 ? Math.min(100, (curExp / nextExp) * 100) : 100;
|
||||||
|
const isMax = value >= ATTRIBUTE_HARD_CAP;
|
||||||
|
const canAllocate = pendingAttrPoints > 0 && !isMax;
|
||||||
|
// 区域标记
|
||||||
|
const inDiminishing = value > ATTRIBUTE_LINEAR_CAP;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className={`relative rounded-xl border ${cs.border} ${cs.bg} p-2.5 flex flex-col gap-1.5 overflow-hidden transition-all duration-300`}
|
||||||
|
style={{
|
||||||
|
boxShadow: `inset 0 0 18px ${cs.glow}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 顶部光晕装饰 */}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute -top-8 -right-8 h-20 w-20 rounded-full blur-2xl opacity-50"
|
||||||
|
style={{ background: meta.hex }}
|
||||||
|
/>
|
||||||
|
{/* 标题行:图标 + 名称 + 等级 badge */}
|
||||||
|
<div className="relative flex items-center gap-1.5">
|
||||||
|
<div
|
||||||
|
className={`shrink-0 h-7 w-7 rounded-lg flex items-center justify-center ${cs.bg} border ${cs.border}`}
|
||||||
|
style={{ boxShadow: `0 0 8px ${cs.glow}` }}
|
||||||
|
>
|
||||||
|
<Icon className={`h-3.5 w-3.5 ${cs.text}`} />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1 leading-tight">
|
||||||
|
<div className={`text-[11px] font-semibold ${cs.text} truncate`}>
|
||||||
|
{meta.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-[8px] text-muted-foreground/70 uppercase tracking-wider truncate">
|
||||||
|
{meta.enName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`shrink-0 text-[9px] px-1.5 py-0.5 rounded font-mono font-bold ${
|
||||||
|
isMax
|
||||||
|
? "bg-amber-400/20 text-amber-200 border border-amber-400/40"
|
||||||
|
: "bg-black/30 text-white/80 border border-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Lv.{value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数值 + 加成 */}
|
||||||
|
<div className="relative flex items-end justify-between">
|
||||||
|
<span className={`text-lg font-mono font-bold ${cs.text} leading-none`}>
|
||||||
|
{value}
|
||||||
|
<span className="text-[9px] text-muted-foreground/60 ml-0.5">
|
||||||
|
/{ATTRIBUTE_HARD_CAP}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className={`text-[10px] ${cs.text} font-mono`}>
|
||||||
|
+{bonusPct.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 经验进度条 */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="h-1.5 rounded-full bg-black/40 overflow-hidden border border-white/5">
|
||||||
|
<div
|
||||||
|
className={`h-full bg-gradient-to-r ${cs.barFrom} ${cs.barTo} transition-all duration-500`}
|
||||||
|
style={{
|
||||||
|
width: `${isMax ? 100 : expPct}%`,
|
||||||
|
boxShadow: `0 0 6px ${cs.glow}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mt-0.5 text-[8px] text-muted-foreground/70 font-mono">
|
||||||
|
<span>
|
||||||
|
{isMax ? "已满级" : `${curExp} / ${nextExp} EXP`}
|
||||||
|
</span>
|
||||||
|
{inDiminishing && !isMax && (
|
||||||
|
<span className="text-amber-300/70">递减区</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 加成列表 */}
|
||||||
|
<div className="relative flex flex-col gap-0.5 mt-0.5">
|
||||||
|
{meta.effects.map((eff, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="text-[8.5px] text-muted-foreground/70 leading-tight flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span className={cs.text}>·</span>
|
||||||
|
<span className="truncate">{eff}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分配按钮 */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!canAllocate}
|
||||||
|
onClick={() => allocateAttribute(key, 1)}
|
||||||
|
aria-label={`分配 1 点到 ${meta.name}`}
|
||||||
|
className={`relative mt-auto rounded-md border px-1.5 py-1 flex items-center justify-center gap-0.5 text-[10px] font-bold transition-all ${
|
||||||
|
canAllocate
|
||||||
|
? `${cs.borderActive} ${cs.bg} ${cs.text} hover:scale-105 cursor-pointer`
|
||||||
|
: "border-white/5 bg-black/20 text-muted-foreground/30 cursor-not-allowed"
|
||||||
|
}`}
|
||||||
|
style={
|
||||||
|
canAllocate
|
||||||
|
? { boxShadow: `0 0 8px ${cs.glow}` }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
<span>分配</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部:总览 */}
|
||||||
|
<div className="mt-auto rounded-lg border border-white/10 bg-black/30 px-2.5 py-1.5 flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-1.5 text-[10px]">
|
||||||
|
<ChevronUp className="h-3 w-3 text-fuchsia-300" />
|
||||||
|
<span className="text-muted-foreground">总等级</span>
|
||||||
|
<span className="font-mono font-bold text-fuchsia-200">
|
||||||
|
{totalLvl}
|
||||||
|
<span className="text-muted-foreground/60 text-[9px]">/{ATTRIBUTE_HARD_CAP * 4}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-[10px]">
|
||||||
|
<span className="text-muted-foreground">总加成</span>
|
||||||
|
<span className="font-mono font-bold text-emerald-200">
|
||||||
|
+{totalBonus.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 当前生效修饰器(细节展示) */}
|
||||||
|
<div className="grid grid-cols-2 gap-1 text-[9px]">
|
||||||
|
<DetailLine label="探险力" value={`×${bonuses.expeditionPowerMult.toFixed(2)}`} color="text-emerald-300" />
|
||||||
|
<DetailLine label="解码步数" value={`+${bonuses.decodeStepsBonus}`} color="text-fuchsia-300" />
|
||||||
|
<DetailLine label="洞见倍率" value={`+${(bonuses.insightMultAdd * 100).toFixed(1)}%`} color="text-fuchsia-300" />
|
||||||
|
<DetailLine label="自动解码周期" value={`×${bonuses.autoDecodeIntervalMult.toFixed(2)}`} color="text-fuchsia-300" />
|
||||||
|
<DetailLine label="探险生命" value={`+${bonuses.expeditionHpBonus}`} color="text-amber-300" />
|
||||||
|
<DetailLine label="BOSS 胜率" value={`+${(bonuses.bossWinRateBonus * 100).toFixed(1)}%`} color="text-amber-300" />
|
||||||
|
<DetailLine label="巡航护盾" value={`+${bonuses.cruiseShieldBonus}`} color="text-amber-300" />
|
||||||
|
<DetailLine label="接触率" value={`×${bonuses.contactRateMult.toFixed(2)}`} color="text-rose-300" />
|
||||||
|
<DetailLine label="星潮触发" value={`+${(bonuses.tideTriggerBonus * 100).toFixed(1)}%`} color="text-rose-300" />
|
||||||
|
<DetailLine label="脉冲连击" value={`+${(bonuses.pulseComboBonus * 100).toFixed(1)}%`} color="text-rose-300" />
|
||||||
|
<DetailLine label="巡航速度" value={`×${bonuses.cruiseShipSpeedMult.toFixed(2)}`} color="text-emerald-300" />
|
||||||
|
<DetailLine label="产能加成" value={`×${bonuses.crystalsPerSecMult.toFixed(2)}`} color="text-emerald-300" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 飞升获得属性点提示 */}
|
||||||
|
<div className="text-[9px] text-muted-foreground/60 text-center leading-tight">
|
||||||
|
每次飞升获得 <span className="text-fuchsia-300 font-mono">飞升次数 × 2 + 1</span> 点属性点;完成对应活动自动累积经验
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailLine({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
color: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between rounded bg-black/25 border border-white/5 px-1.5 py-0.5">
|
||||||
|
<span className="text-muted-foreground/80 truncate">{label}</span>
|
||||||
|
<span className={`font-mono ${color}`}>{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,753 @@
|
|||||||
|
"use client";
|
||||||
|
// 回响星核 / Echo Nexus — 深空信标面板
|
||||||
|
// v0.5:每日挑战 + 本地排行榜
|
||||||
|
// v0.8:周挑战 + 信标链(连续完成奖励)
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { useGameStore } from "@/store/gameStore";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { sfx } from "@/hooks/useAudio";
|
||||||
|
import {
|
||||||
|
generateDailyChallenge,
|
||||||
|
generateWeeklyChallenge,
|
||||||
|
loadDailyProgress,
|
||||||
|
loadWeeklyProgress,
|
||||||
|
loadLeaderboard,
|
||||||
|
loadChainState,
|
||||||
|
claimBeaconReward,
|
||||||
|
BEACON_DIFFICULTY,
|
||||||
|
BEACON_TYPE_META,
|
||||||
|
BEACON_CHAIN_MILESTONES,
|
||||||
|
BEACON_CHAIN_REWARDS,
|
||||||
|
getWeekKey,
|
||||||
|
getTodayKey,
|
||||||
|
getNextMilestone,
|
||||||
|
getChainProgress,
|
||||||
|
msUntilNextDay,
|
||||||
|
msUntilNextWeek,
|
||||||
|
formatCountdown,
|
||||||
|
type BeaconDailyChallenge,
|
||||||
|
type BeaconDailyProgress,
|
||||||
|
type BeaconWeeklyChallenge,
|
||||||
|
type BeaconWeeklyProgress,
|
||||||
|
type BeaconScoreEntry,
|
||||||
|
type BeaconChallengeType,
|
||||||
|
type BeaconDifficulty,
|
||||||
|
type BeaconChainState,
|
||||||
|
} from "@/lib/game/beacon";
|
||||||
|
import { formatNum } from "@/lib/game/config";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import {
|
||||||
|
Radio,
|
||||||
|
Clock,
|
||||||
|
Trophy,
|
||||||
|
Sparkles,
|
||||||
|
Award,
|
||||||
|
Crown,
|
||||||
|
Medal,
|
||||||
|
Link2,
|
||||||
|
Flame,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
const DIFFICULTY_ORDER: BeaconDifficulty[] = ["routine", "anomaly", "singular"];
|
||||||
|
|
||||||
|
function rankBadge(rank: number) {
|
||||||
|
if (rank === 0) return { icon: Crown, color: "#fbbf24", label: "1st" };
|
||||||
|
if (rank === 1) return { icon: Medal, color: "#cbd5e1", label: "2nd" };
|
||||||
|
if (rank === 2) return { icon: Award, color: "#f97316", label: "3rd" };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BeaconPanel() {
|
||||||
|
const grantBeaconReward = useGameStore((s) => s.grantBeaconReward);
|
||||||
|
const claimWeeklyBeacon = useGameStore((s) => s.claimWeeklyBeacon);
|
||||||
|
const claimChainReward = useGameStore((s) => s.claimChainReward);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [challenge, setChallenge] = useState<BeaconDailyChallenge | null>(null);
|
||||||
|
const [progress, setProgress] = useState<BeaconDailyProgress | null>(null);
|
||||||
|
const [weeklyChallenge, setWeeklyChallenge] =
|
||||||
|
useState<BeaconWeeklyChallenge | null>(null);
|
||||||
|
const [weeklyProgress, setWeeklyProgress] =
|
||||||
|
useState<BeaconWeeklyProgress | null>(null);
|
||||||
|
const [chainState, setChainState] = useState<BeaconChainState | null>(null);
|
||||||
|
const [leaderboard, setLeaderboard] = useState<BeaconScoreEntry[]>([]);
|
||||||
|
const [dayCountdown, setDayCountdown] = useState("00:00:00");
|
||||||
|
const [weekCountdown, setWeekCountdown] = useState("00:00:00");
|
||||||
|
const [now, setNow] = useState(Date.now());
|
||||||
|
|
||||||
|
// 初始化 + 每秒刷新(进度 + 倒计时)
|
||||||
|
useEffect(() => {
|
||||||
|
setChallenge(generateDailyChallenge());
|
||||||
|
setProgress(loadDailyProgress());
|
||||||
|
setWeeklyChallenge(generateWeeklyChallenge());
|
||||||
|
setWeeklyProgress(loadWeeklyProgress());
|
||||||
|
setChainState(loadChainState());
|
||||||
|
setLeaderboard(loadLeaderboard());
|
||||||
|
const id = setInterval(() => {
|
||||||
|
setNow(Date.now());
|
||||||
|
setProgress(loadDailyProgress());
|
||||||
|
setWeeklyProgress(loadWeeklyProgress());
|
||||||
|
setChainState(loadChainState());
|
||||||
|
setChallenge((c) => c ?? generateDailyChallenge());
|
||||||
|
setWeeklyChallenge((c) => c ?? generateWeeklyChallenge());
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDayCountdown(formatCountdown(msUntilNextDay(new Date(now))));
|
||||||
|
setWeekCountdown(formatCountdown(msUntilNextWeek(new Date(now))));
|
||||||
|
}, [now]);
|
||||||
|
|
||||||
|
const handleClaimDaily = useCallback(() => {
|
||||||
|
if (!challenge || !progress) return;
|
||||||
|
if (progress.completedAt === null || progress.claimed) return;
|
||||||
|
const res = claimBeaconReward(challenge, progress);
|
||||||
|
setLeaderboard(res.leaderboard);
|
||||||
|
setProgress(loadDailyProgress());
|
||||||
|
// 发放奖励到游戏状态
|
||||||
|
grantBeaconReward(res.rewardInsight, res.rewardContact);
|
||||||
|
sfx("achievement");
|
||||||
|
toast({
|
||||||
|
title: "✦ 每日信标奖励已领取",
|
||||||
|
description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
|
||||||
|
1
|
||||||
|
)} 接触 · 得分 ${res.score}`,
|
||||||
|
});
|
||||||
|
}, [challenge, progress, toast, grantBeaconReward]);
|
||||||
|
|
||||||
|
const handleClaimWeekly = useCallback(() => {
|
||||||
|
if (!weeklyChallenge || !weeklyProgress) return;
|
||||||
|
if (weeklyProgress.completedAt === null || weeklyProgress.claimed) return;
|
||||||
|
const res = claimWeeklyBeacon();
|
||||||
|
setLeaderboard(loadLeaderboard());
|
||||||
|
setWeeklyProgress(loadWeeklyProgress());
|
||||||
|
if (res.rewardInsight > 0 || res.rewardContact > 0) {
|
||||||
|
sfx("achievement");
|
||||||
|
toast({
|
||||||
|
title: "✦ 周挑战奖励已领取",
|
||||||
|
description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
|
||||||
|
1
|
||||||
|
)} 接触 · 得分 ${res.score}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [weeklyChallenge, weeklyProgress, toast, claimWeeklyBeacon]);
|
||||||
|
|
||||||
|
const handleClaimChain = useCallback(
|
||||||
|
(milestone: number) => {
|
||||||
|
const reward = BEACON_CHAIN_REWARDS.find(
|
||||||
|
(r) => r.milestone === milestone
|
||||||
|
);
|
||||||
|
const res = claimChainReward(milestone);
|
||||||
|
setChainState(loadChainState());
|
||||||
|
if (res.ok) {
|
||||||
|
sfx("achievement");
|
||||||
|
toast({
|
||||||
|
title: `✦ ${res.label || reward?.label || "里程碑"} 已领取`,
|
||||||
|
description: `+${res.rewardInsight} 洞见 · +${res.rewardContact.toFixed(
|
||||||
|
1
|
||||||
|
)} 接触`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[toast, claimChainReward]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!challenge || !progress || !weeklyChallenge || !weeklyProgress || !chainState) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-xs text-muted-foreground/60">
|
||||||
|
正在校准深空信标…
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffMeta = BEACON_DIFFICULTY[challenge.difficulty];
|
||||||
|
const typeMeta = BEACON_TYPE_META[challenge.type];
|
||||||
|
const pct = Math.min(100, (progress.progress / challenge.goal) * 100);
|
||||||
|
const isCompleted = progress.completedAt !== null;
|
||||||
|
const isClaimed = progress.claimed;
|
||||||
|
const canClaim = isCompleted && !isClaimed;
|
||||||
|
|
||||||
|
// 周挑战派生量
|
||||||
|
const wDiffMeta = BEACON_DIFFICULTY[weeklyChallenge.difficulty];
|
||||||
|
const wTypeMeta = BEACON_TYPE_META[weeklyChallenge.type];
|
||||||
|
const wPct = Math.min(100, (weeklyProgress.progress / weeklyChallenge.goal) * 100);
|
||||||
|
const wCompleted = weeklyProgress.completedAt !== null;
|
||||||
|
const wClaimed = weeklyProgress.claimed;
|
||||||
|
const wCanClaim = wCompleted && !wClaimed;
|
||||||
|
|
||||||
|
// 信标链派生量
|
||||||
|
const chainProgress = getChainProgress(chainState.currentStreak);
|
||||||
|
const nextMilestone = getNextMilestone(chainState.currentStreak);
|
||||||
|
const todayKey = getTodayKey();
|
||||||
|
const completedToday = chainState.lastCompletedDateKey === todayKey;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2.5 h-full overflow-y-auto echo-scroll pr-0.5">
|
||||||
|
<style jsx global>{`
|
||||||
|
.echo-scroll::-webkit-scrollbar { width: 4px; }
|
||||||
|
.echo-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
|
||||||
|
.echo-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
@keyframes beacon-pulse-ring {
|
||||||
|
0% { transform: scale(0.8); opacity: 0.8; }
|
||||||
|
100% { transform: scale(2.2); opacity: 0; }
|
||||||
|
}
|
||||||
|
@keyframes beacon-glow {
|
||||||
|
0%, 100% { box-shadow: 0 0 18px ${diffMeta.glow}, inset 0 0 12px ${diffMeta.glow}; }
|
||||||
|
50% { box-shadow: 0 0 32px ${diffMeta.glow}, inset 0 0 20px ${diffMeta.glow}; }
|
||||||
|
}
|
||||||
|
@keyframes weekly-glow {
|
||||||
|
0%, 100% { box-shadow: 0 0 18px rgba(232,121,249,0.35), inset 0 0 12px rgba(232,121,249,0.25); }
|
||||||
|
50% { box-shadow: 0 0 32px rgba(232,121,249,0.5), inset 0 0 20px rgba(232,121,249,0.35); }
|
||||||
|
}
|
||||||
|
@keyframes chain-milestone-pulse {
|
||||||
|
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(251,113,133,0.55); }
|
||||||
|
50% { transform: scale(1.06); box-shadow: 0 0 0 8px rgba(251,113,133,0); }
|
||||||
|
}
|
||||||
|
.chain-milestone-reachable {
|
||||||
|
animation: chain-milestone-pulse 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes chain-streak-flux {
|
||||||
|
0%, 100% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
.chain-streak-text {
|
||||||
|
background: linear-gradient(90deg, #fbbf24, #fb7185, #fbbf24);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: chain-streak-flux 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* 头部:信标 + 倒计时 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold flex items-center gap-1.5">
|
||||||
|
<Radio className="h-4 w-4 text-fuchsia-400" />
|
||||||
|
深空信标
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
次日重置
|
||||||
|
<span className="font-mono text-fuchsia-300/90">{dayCountdown}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 每日挑战卡片 */}
|
||||||
|
<div
|
||||||
|
className="relative rounded-xl border p-3 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
borderColor: `${diffMeta.color}55`,
|
||||||
|
background: `linear-gradient(135deg, ${diffMeta.color}1f, rgba(0,0,0,0.45))`,
|
||||||
|
animation: isCompleted ? "none" : "beacon-glow 3s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 背景装饰:脉冲环 */}
|
||||||
|
{!isCompleted && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
|
||||||
|
style={{
|
||||||
|
border: `1.5px solid ${diffMeta.color}`,
|
||||||
|
animation: "beacon-pulse-ring 2.5s ease-out infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute top-4 right-4 h-10 w-10 rounded-full pointer-events-none"
|
||||||
|
style={{
|
||||||
|
border: `1.5px solid ${diffMeta.color}`,
|
||||||
|
animation: "beacon-pulse-ring 2.5s ease-out infinite 1.25s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
{/* 难度 + 类型 标签 */}
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
|
||||||
|
style={{ background: `${diffMeta.color}22`, color: diffMeta.color, border: `1px solid ${diffMeta.color}55` }}
|
||||||
|
>
|
||||||
|
<span>{diffMeta.icon}</span>
|
||||||
|
{diffMeta.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
|
||||||
|
{typeMeta.icon} {typeMeta.label}
|
||||||
|
</span>
|
||||||
|
{isCompleted && (
|
||||||
|
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
|
||||||
|
<Sparkles className="h-2.5 w-2.5" /> 已完成
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 挑战标题 */}
|
||||||
|
<h4 className="text-sm font-semibold mb-1" style={{ color: diffMeta.color, textShadow: `0 0 10px ${diffMeta.glow}` }}>
|
||||||
|
{challenge.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-[11px] text-muted-foreground/80 leading-relaxed mb-2.5">
|
||||||
|
{challenge.desc}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-[10px] text-muted-foreground/70">进度</span>
|
||||||
|
<span className="text-[11px] font-mono font-semibold" style={{ color: diffMeta.color }}>
|
||||||
|
{Math.min(progress.progress, challenge.goal)} / {challenge.goal} {typeMeta.unit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={pct}
|
||||||
|
className="h-2 bg-black/40"
|
||||||
|
style={{
|
||||||
|
["--progress-color" as string]: diffMeta.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 奖励 + 领取按钮 */}
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2 text-[10px]">
|
||||||
|
<span className="text-muted-foreground/60">奖励:</span>
|
||||||
|
{challenge.rewardInsight > 0 && (
|
||||||
|
<span className="text-amber-300 font-mono">+{formatNum(challenge.rewardInsight)}洞见</span>
|
||||||
|
)}
|
||||||
|
<span className="text-fuchsia-300 font-mono">+{challenge.rewardContact.toFixed(1)}接触</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleClaimDaily}
|
||||||
|
disabled={!canClaim}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-[11px] border-0"
|
||||||
|
style={{
|
||||||
|
background: canClaim
|
||||||
|
? `linear-gradient(90deg, ${diffMeta.color}, ${diffMeta.color}cc)`
|
||||||
|
: `${diffMeta.color}1a`,
|
||||||
|
color: canClaim ? "#022c22" : `${diffMeta.color}99`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isClaimed ? "已领取" : canClaim ? "领取奖励" : isCompleted ? "已领取" : "进行中…"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 完成时长 */}
|
||||||
|
{isCompleted && progress.durationSec > 0 && (
|
||||||
|
<div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
|
||||||
|
完成用时 {Math.floor(progress.durationSec / 60)}分{progress.durationSec % 60}秒
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 周挑战 + 信标链(桌面端并排,移动端单列) */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2.5">
|
||||||
|
{/* ===== 周挑战卡片(fuchsia 主题) ===== */}
|
||||||
|
<div
|
||||||
|
className="relative rounded-xl border p-3 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
borderColor: "rgba(232,121,249,0.4)",
|
||||||
|
background: `linear-gradient(135deg, rgba(232,121,249,0.12), rgba(0,0,0,0.45))`,
|
||||||
|
animation: wCompleted ? "none" : "weekly-glow 3.5s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 头部:标题 + weekKey + 倒计时 */}
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Zap className="h-3.5 w-3.5 text-fuchsia-400" />
|
||||||
|
<span className="text-xs font-semibold text-fuchsia-200">
|
||||||
|
周挑战 · WEEKLY
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
|
||||||
|
<span className="font-mono text-fuchsia-300/80">
|
||||||
|
{weeklyChallenge.weekKey}
|
||||||
|
</span>
|
||||||
|
<Clock className="h-2.5 w-2.5" />
|
||||||
|
<span className="font-mono text-fuchsia-300/90">{weekCountdown}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 难度 + 类型 标签 */}
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-mono px-1.5 py-0.5 rounded flex items-center gap-1"
|
||||||
|
style={{ background: `${wDiffMeta.color}22`, color: wDiffMeta.color, border: `1px solid ${wDiffMeta.color}55` }}
|
||||||
|
>
|
||||||
|
<span>{wDiffMeta.icon}</span>
|
||||||
|
{wDiffMeta.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-white/5 text-muted-foreground border border-white/10">
|
||||||
|
{wTypeMeta.icon} {wTypeMeta.label}
|
||||||
|
</span>
|
||||||
|
{wCompleted && (
|
||||||
|
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 flex items-center gap-1">
|
||||||
|
<Sparkles className="h-2.5 w-2.5" /> 已完成
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<h4
|
||||||
|
className="text-sm font-semibold mb-1"
|
||||||
|
style={{ color: wDiffMeta.color, textShadow: `0 0 10px ${wDiffMeta.glow}` }}
|
||||||
|
>
|
||||||
|
{weeklyChallenge.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-[10px] text-muted-foreground/80 leading-relaxed mb-2">
|
||||||
|
{weeklyChallenge.desc}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-[10px] text-muted-foreground/70">进度</span>
|
||||||
|
<span className="text-[11px] font-mono font-semibold" style={{ color: wDiffMeta.color }}>
|
||||||
|
{Math.min(weeklyProgress.progress, weeklyChallenge.goal)} / {weeklyChallenge.goal} {wTypeMeta.unit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={wPct}
|
||||||
|
className="h-2 bg-black/40"
|
||||||
|
style={{
|
||||||
|
["--progress-color" as string]: wDiffMeta.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 奖励 + 领取按钮 */}
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-1.5 text-[10px]">
|
||||||
|
<span className="text-muted-foreground/60">奖励:</span>
|
||||||
|
{weeklyChallenge.rewardInsight > 0 && (
|
||||||
|
<span className="text-amber-300 font-mono">
|
||||||
|
+{formatNum(weeklyChallenge.rewardInsight)}洞见
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-fuchsia-300 font-mono">
|
||||||
|
+{weeklyChallenge.rewardContact.toFixed(1)}接触
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleClaimWeekly}
|
||||||
|
disabled={!wCanClaim}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-[11px] border-0"
|
||||||
|
style={{
|
||||||
|
background: wCanClaim
|
||||||
|
? `linear-gradient(90deg, #34d399, #34d399cc)`
|
||||||
|
: "rgba(232,121,249,0.10)",
|
||||||
|
color: wCanClaim
|
||||||
|
? "#022c22"
|
||||||
|
: "rgba(232,121,249,0.55)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{wClaimed
|
||||||
|
? "已领取"
|
||||||
|
: wCanClaim
|
||||||
|
? "领取奖励"
|
||||||
|
: wCompleted
|
||||||
|
? "已领取"
|
||||||
|
: "进行中…"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{wCompleted && weeklyProgress.durationSec > 0 && (
|
||||||
|
<div className="mt-1.5 text-[10px] text-muted-foreground/50 text-center">
|
||||||
|
完成用时 {Math.floor(weeklyProgress.durationSec / 60)}分{weeklyProgress.durationSec % 60}秒
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ===== 信标链卡片(amber→rose 渐变) ===== */}
|
||||||
|
<div
|
||||||
|
className="relative rounded-xl border p-3 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
borderColor: "rgba(251,191,36,0.35)",
|
||||||
|
background: `linear-gradient(135deg, rgba(251,191,36,0.10), rgba(251,113,133,0.10), rgba(0,0,0,0.4))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 头部:标题 + 当前连续天数 */}
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Link2 className="h-3.5 w-3.5 text-amber-400" />
|
||||||
|
<span className="text-xs font-semibold text-amber-200">
|
||||||
|
信标链 · CHAIN
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<Flame className="h-3 w-3 text-rose-400" />
|
||||||
|
<span className="text-[10px] text-muted-foreground/60">连续</span>
|
||||||
|
<span className="chain-streak-text text-2xl font-bold font-mono leading-none">
|
||||||
|
{chainState.currentStreak}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground/60">天</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 今日完成状态 */}
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<span className="text-[10px] text-muted-foreground/70">
|
||||||
|
{completedToday
|
||||||
|
? "今日已贡献"
|
||||||
|
: "今日尚未完成日挑战"}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${
|
||||||
|
completedToday
|
||||||
|
? "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
|
||||||
|
: "bg-white/5 text-muted-foreground/70 border-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{completedToday ? "✓ 已记录" : "○ 待完成"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 4 个里程碑节点横向排列 */}
|
||||||
|
<div className="flex items-center justify-between mb-2 relative">
|
||||||
|
{/* 节点之间的连线(背景灰) */}
|
||||||
|
<div className="absolute top-6 left-[12.5%] right-[12.5%] h-[2px] bg-white/10" />
|
||||||
|
{/* 已达成部分高亮(基于 prev→next 插值) */}
|
||||||
|
{(() => {
|
||||||
|
// 节点圆心水平位置(百分比)
|
||||||
|
const MILESTONE_POS: Record<number, number> = {
|
||||||
|
0: 12.5,
|
||||||
|
3: 12.5,
|
||||||
|
7: 37.5,
|
||||||
|
14: 62.5,
|
||||||
|
30: 87.5,
|
||||||
|
};
|
||||||
|
const prevPos =
|
||||||
|
MILESTONE_POS[chainProgress.prev] ?? 12.5;
|
||||||
|
const nextPos =
|
||||||
|
chainProgress.next !== null
|
||||||
|
? MILESTONE_POS[chainProgress.next] ?? 87.5
|
||||||
|
: 87.5;
|
||||||
|
const pct = chainProgress.progressPct / 100;
|
||||||
|
const activeEndPos = prevPos + (nextPos - prevPos) * pct;
|
||||||
|
const widthPct = Math.max(0, activeEndPos - 12.5);
|
||||||
|
if (widthPct <= 0) return null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute top-6 h-[2px]"
|
||||||
|
style={{
|
||||||
|
left: "12.5%",
|
||||||
|
width: `${widthPct}%`,
|
||||||
|
background:
|
||||||
|
"linear-gradient(90deg, #fbbf24, #fb7185)",
|
||||||
|
boxShadow: "0 0 8px rgba(251,113,133,0.5)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{BEACON_CHAIN_MILESTONES.map((m) => {
|
||||||
|
const reward = BEACON_CHAIN_REWARDS.find((r) => r.milestone === m);
|
||||||
|
const claimed = chainState.milestonesClaimed.includes(m);
|
||||||
|
const reachable =
|
||||||
|
chainState.currentStreak >= m && !claimed;
|
||||||
|
const inProgress = chainState.currentStreak > 0 && nextMilestone === m;
|
||||||
|
// 节点配色
|
||||||
|
let nodeBg = "rgba(255,255,255,0.05)";
|
||||||
|
let nodeBorder = "rgba(255,255,255,0.15)";
|
||||||
|
let textColor = "rgba(255,255,255,0.4)";
|
||||||
|
if (claimed) {
|
||||||
|
nodeBg = "rgba(52,211,153,0.25)";
|
||||||
|
nodeBorder = "#34d399";
|
||||||
|
textColor = "#34d399";
|
||||||
|
} else if (reachable) {
|
||||||
|
nodeBg = "rgba(251,113,133,0.20)";
|
||||||
|
nodeBorder = "#fb7185";
|
||||||
|
textColor = "#fb7185";
|
||||||
|
} else if (inProgress) {
|
||||||
|
nodeBg = "rgba(251,191,36,0.20)";
|
||||||
|
nodeBorder = "#fbbf24";
|
||||||
|
textColor = "#fbbf24";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={m}
|
||||||
|
className="flex flex-col items-center gap-1 z-10 flex-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-12 h-12 rounded-full flex items-center justify-center font-mono font-bold text-sm border-2 ${
|
||||||
|
reachable ? "chain-milestone-reachable" : ""
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
background: nodeBg,
|
||||||
|
borderColor: nodeBorder,
|
||||||
|
color: textColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{claimed ? (
|
||||||
|
<Sparkles className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
m
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="text-[10px] text-center leading-tight"
|
||||||
|
style={{ color: textColor }}
|
||||||
|
>
|
||||||
|
{reward?.label}
|
||||||
|
</span>
|
||||||
|
{claimed ? (
|
||||||
|
<span className="text-[10px] text-emerald-400/80 font-mono">
|
||||||
|
已领
|
||||||
|
</span>
|
||||||
|
) : reachable ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => handleClaimChain(m)}
|
||||||
|
size="sm"
|
||||||
|
className="h-5 px-2 text-[10px] py-0 border-0"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(90deg, #fb7185, #f43f5e)",
|
||||||
|
color: "#1c0608",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
领取
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<span className="text-[10px] text-muted-foreground/40 font-mono">
|
||||||
|
+{reward?.rewardInsight}洞
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 进度条 */}
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-[10px] text-muted-foreground/70">
|
||||||
|
{chainProgress.next === null
|
||||||
|
? "已通关全部里程碑"
|
||||||
|
: `下一目标:${chainProgress.next} 天`}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-mono text-amber-300">
|
||||||
|
{chainProgress.current}
|
||||||
|
{chainProgress.next !== null && ` / ${chainProgress.next}`} 天
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={chainProgress.progressPct}
|
||||||
|
className="h-1.5 bg-black/40"
|
||||||
|
style={{
|
||||||
|
["--progress-color" as string]: "#fbbf24",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部统计 */}
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-x-2 gap-y-1 text-[10px]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground/60">最长</span>
|
||||||
|
<span className="font-mono text-amber-300">
|
||||||
|
{chainState.longestStreak}天
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground/60">累计</span>
|
||||||
|
<span className="font-mono text-rose-300">
|
||||||
|
{chainState.totalCompletions}次
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-muted-foreground/60">续命</span>
|
||||||
|
{chainState.graceUsed >= 1 ? (
|
||||||
|
<span className="font-mono text-rose-400/80">已用</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-mono text-emerald-400/80">可用</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 本地排行榜 */}
|
||||||
|
<div className="rounded-xl border border-white/10 bg-black/30 p-2.5">
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<Trophy className="h-3.5 w-3.5 text-amber-400" />
|
||||||
|
<span className="text-xs font-semibold">深空排行榜</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground/50">本地 · Top {leaderboard.length || 0}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{leaderboard.length === 0 ? (
|
||||||
|
<div className="text-center py-4 text-[11px] text-muted-foreground/40">
|
||||||
|
<Trophy className="h-6 w-6 mx-auto mb-1 opacity-30" />
|
||||||
|
尚无记录。完成今日或本周信标即可登榜。
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-0.5 max-h-[180px] overflow-y-auto echo-scroll">
|
||||||
|
{leaderboard.map((entry, i) => {
|
||||||
|
const rb = rankBadge(i);
|
||||||
|
const eDiff = BEACON_DIFFICULTY[entry.difficulty];
|
||||||
|
const eType = BEACON_TYPE_META[entry.challenge as BeaconChallengeType];
|
||||||
|
const isMine = entry.dateKey === getTodayKey() || entry.dateKey === getWeekKey();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${entry.timestamp}-${i}`}
|
||||||
|
className={`flex items-center gap-2 px-2 py-1 rounded-lg text-[11px] ${
|
||||||
|
entry.isWeekly
|
||||||
|
? "bg-fuchsia-500/10 border border-fuchsia-500/20"
|
||||||
|
: isMine
|
||||||
|
? "bg-fuchsia-500/10 border border-fuchsia-500/20"
|
||||||
|
: "hover:bg-white/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* 排名 */}
|
||||||
|
<span className="w-6 flex items-center justify-center">
|
||||||
|
{rb ? (
|
||||||
|
<rb.icon className="h-3.5 w-3.5" style={{ color: rb.color }} />
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground/50 font-mono">{i + 1}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{/* 类型 + 难度 + 周挑战标记 */}
|
||||||
|
<span className="flex items-center gap-1 flex-1 min-w-0">
|
||||||
|
<span style={{ color: eDiff.color }} className="font-mono">{eDiff.icon}</span>
|
||||||
|
<span className="text-muted-foreground/80 truncate">{eType.label}</span>
|
||||||
|
{entry.isWeekly && (
|
||||||
|
<span className="text-[10px] font-mono px-1 rounded bg-fuchsia-500/20 text-fuchsia-300 border border-fuchsia-500/40">
|
||||||
|
WEEK
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{entry.progress >= 1 && <span className="text-emerald-400/70">✓</span>}
|
||||||
|
</span>
|
||||||
|
{/* 用时 */}
|
||||||
|
<span className="text-muted-foreground/50 font-mono text-[10px] w-16 text-right">
|
||||||
|
{Math.floor(entry.durationSec / 60)}m{entry.durationSec % 60}s
|
||||||
|
</span>
|
||||||
|
{/* 分数 */}
|
||||||
|
<span className="font-mono font-semibold w-12 text-right" style={{ color: eDiff.color }}>
|
||||||
|
{formatNum(entry.score)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 难度图例 */}
|
||||||
|
<div className="flex items-center justify-center gap-3 text-[10px] text-muted-foreground/50">
|
||||||
|
{DIFFICULTY_ORDER.map((d) => {
|
||||||
|
const m = BEACON_DIFFICULTY[d];
|
||||||
|
return (
|
||||||
|
<span key={d} className="flex items-center gap-1">
|
||||||
|
<span style={{ color: m.color }}>{m.icon}</span>
|
||||||
|
{m.label} ×{m.mult}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||