Files
echo-nexus/docs/13-叙事事件-v0.16.md
Super_Z 5bed2599f5 v0.16: 解码阈值叙事事件 + loreLayer 已读标记 + idleHistory 详情 + 移动端适配
- 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 端到端验证通过
2026-06-24 23:45:34 +00:00

11 KiB
Raw Permalink Blame History

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-bloreLayer 已读标记 + idleHistory 详情展开 + 移动端适配审计

二、解码阈值叙事里程碑(2-a

设计哲学

回响星核的核心卖点是**"解码驱动叙事涌现"。v0.15 的 loreLayers 让每个碎片有 3 层文本,但解锁是静默的。v0.16 在 totalDecoded 达到 5/12/25/40/60 时触发全屏叙事叠层**——不是解锁新碎片,而是对已解锁碎片的回顾性咏叹,类似游戏中的"过场回忆"。

5 个阈值与 FRAGMENTS 的 threshold1/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-shadow120px + 240px 双层)+ narrative-breathe 3.5s 呼吸循环
  • 中央卡片max-w-2xl4 色主题渐变背景 + 主题色 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 300msopacity 0→1+ narrative-scale-in 300ms cubic-bezierscale 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-5p-4 sm:p-5 / 标题 text-lgtext-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-a6 文件):

  • 新建 src/lib/game/narrativeMilestones.ts
  • 新建 src/components/game/NarrativeMilestoneOverlay.tsx
  • 修改 src/lib/game/types.ts(加 triggeredMilestones + pendingMilestone
  • 修改 src/lib/game/engine.tscreateInitialState + performPrestige
  • 修改 src/store/gameStore.ts2 解码点检测 + 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.tscreateInitialState + performPrestige,与 2-a 共存)
  • 修改 src/store/gameStore.tsmarkLoreLayerRead + migrate,与 2-a 共存)
  • 修改 src/components/game/Codex.tsx(已读标记 + 点击逻辑 + 顶部统计)
  • 修改 src/components/game/StatsPanel.tsxidleHistory 展开 + 字号)
  • 修改 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 200dev.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 复用到编年史时间轴 + 探险日志