- 5 个解码阈值叙事里程碑(ms_5/12/25/40/60)+ 全屏碎片释放叠层 - loreLayer 已读标记(readLoreLayers + 点击文本标记 + 已读/未读视觉区分) - idleProjectHistory 详情展开(倒序最近 5 条 + 相对时间) - 新建 format.ts formatRelativeTime 工具函数 - 移动端 375px 适配审计(4 组件字号 9→10px + flex-wrap + 响应式 padding) - 严格 4 色全息,lint 零错误,agent-browser 端到端验证通过
232 lines
11 KiB
Markdown
232 lines
11 KiB
Markdown
# 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 复用到编年史时间轴 + 探险日志
|