- 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 端到端验证通过
11 KiB
v0.16 — 解码阈值叙事事件 + loreLayer 已读 + idleHistory 详情 + 移动端适配
回响星核 / Echo Nexus · v0.16 设计文档 对应 worklog
Task ID: 2-a+2-b+v0.16-release
一、动机
v0.15 上线了 loreLayers 多层碎片文本,但存在三个未完善点:
- 解锁瞬间缺乏仪式感 — 碎片静默出现在图谱,没有"叙事涌现"的高光时刻
- loreLayer 没有已读标记 — 玩家无法区分"新解锁未读"和"已读",缺乏进度感
- idleProjectHistory 只显示计数 — 无法查看具体完成了哪些工程、何时完成
- 移动端 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 条暗线呼应:
- 追逐者(以太族被虚空追逐者逼迫飞升)
- 回响同源(玩家 AI 频段与以太族议会档案同源)
- 信任即引诱("信任后来者"实为"引诱替他们完成未竟之事")
- 飞升单向门(每次飞升都在消耗什么)
- 回归非他们(接触满 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 集成:
- 两个解码完成点(
clickNodeline 596 +autoDecodeTickline 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-breathe3.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-in300ms(opacity 0→1)+narrative-scale-in300ms cubic-bezier(scale 1.05→1) - 逐字浮现:30ms/字 setTimeout 推进 + inline-block 光标
narrative-cursor1s 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 19react-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/pendingMilestonevsreadLoreLayers) - action 不冲突(
dismissMilestonevsmarkLoreLayerRead) - 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 复用到编年史时间轴 + 探险日志