下载 App
修改于10/203 浏览综合 包含 AI 合成内容
【聚光灯21天】DAY 13 · H5 版一天做完六个页面,和一次「资产先瘦身再上线」

DAY 12 封存了 Lua 版、开了 H5 的壳。今天把整个游戏在浏览器里重建了一遍。
一天之内:六个页面(主菜单 / 选择页 / 对局页 / 设置 / 存档 / 结算)、
朝代罗盘、配音与 BGM 两套音频系统、七套浏览器验收测试、四条资产管线。
朝代罗盘、配音与 BGM 两套音频系统、七套浏览器验收测试、四条资产管线。
一、为什么能一天做完
因为规则早就写好了,今天只做"渲染层"。
DAY 12 说清了这件事的来龙去脉:这个项目的规则真源一直是 JS(prot/engine.js),
Lua 是它的逐位移植。所以 H5 版:
Lua 是它的逐位移植。所以 H5 版:
ts复制
ts// h5/vite.config.ts alias: { '@engine': resolve(repoRoot, 'prot/engine.js'), // 规则:原样复用 '@data': resolve(repoRoot, 'prot/lua-data'), // 数据:原样 import }
不拷贝、不转译。 prot/ 的测试因此继续守护同一份代码 —— H5 与 Node 跑的是同一个 engine.js。
于是今天的活就变成了纯粹的**“把状态渲染成 DOM”**。game/session.ts 933 行,
控制流严格对齐已封存的 scripts/qingshi/ui/game.lua 的 handleRoute。
控制流严格对齐已封存的 scripts/qingshi/ui/game.lua 的 handleRoute。
二、六个页面 + 罗盘
| 模块 | 行数 | 说明 |
|---|---|---|
| ui/menu.ts | 263 | 主菜单 |
| ui/select.ts | 286 | 选择页(剧本卡 + 难度 + 入局漩涡) |
| ui/wheel.ts | 299 | 朝代罗盘 |
| ui/settings.ts | 115 | 设置页 |
| ui/savepage.ts | 107 | 存档页 |
| ui/cast.ts | 77 | 立绘列 |
| game/session.ts | 933 | 会话与流程 |
| game/audio.ts | 155 | BGM |
| game/voice.ts | 188 | 配音 |
| game/saves.ts | 201 | 存档 |
| game/settings.ts | 126 | 设置 |
样式 2,481 行(art / base / game / menu / save / select / settings / stage)。
罗盘:DOM 移植,手感参数逐项对齐
罗盘是最难的一块。wheel.ts 头注写得很清楚 —— 模型与手感参数与 Lua 完全一致:
- 朝代名固定在自己的方位(第 1 项在正上方),不随盘转
- 盘面(环带 + 烘焙在顶部的指针)旋转,由指针去指当前选中的那一格
- 拖拽跟手 → 松手惯性 → 欠阻尼弹簧吸附(冲过再回摆)→ 整体归位
- 拖拽位移极小视为点击;中心盘为中枢死区
与 Lua 的差异只在渲染层:NanoVG 即时绘制换成 DOM + CSS transform,
每帧只写 transform/位置,不重排布局。
每帧只写 transform/位置,不重排布局。
这块的手感在 Lua 版调了很多轮(DAY 8 的"罗盘四版"),今天照搬结论,不重新调。
这就是封存说明里"已验证的视觉规则照搬"那条的实际兑现。
三、资产:先瘦身,再上线

这是今天最"硬"的一块。美术给的原件不能直接上网页 —— 差了两个数量级。
三条管线,三个不同的理由
① 美术图:build-assets.py —— 74 张源图 131MB → WebP
源图是 2848×1602 的 PNG,单张背景 6–8MB。直接引用的话,对局页光背景就要下 6MB,
首屏根本没法看。统一转 WebP + 限制最大边(如背景 1920px、立绘 620×860),
整站美术预算压到几 MB 量级。
首屏根本没法看。统一转 WebP + 限制最大边(如背景 1920px、立绘 620×860),
整站美术预算压到几 MB 量级。
每张图单独配质量参数(背景 72 / 卡片 84 / 标题 90),不是一刀切。
② BGM:build-audio.py —— 41.3MB → 每曲约 1.4MB
源是 vorbis 160kb/s、3分23秒的完整曲子,单个 3.2–4.0MB,十一个合计 41.3MB。
转码这里有个踩出来的结论:
同码率下 opus 比 aac 省 17%(48k vs 64k),省下的比换格式更重要。
还有一条反直觉的:固定码率对 libopus 意义重大 —— 加 -ac 1(强制单声道)不会变小,
因为比特率是总量,单声道只会让每声道分到更多码,听起来更糟。要省只能降码率。
因为比特率是总量,单声道只会让每声道分到更多码,听起来更糟。要省只能降码率。
为什么要出两种格式:
- opus 只能装进 webm/ogg —— 而且这个 ffmpeg 构建往 m4a 里塞 opus 会写出一个 0 字节的坏文件(加 -strict -2 也不行)
- opus 在 Safari 上支持不稳,aac/m4a 才是通用兜底
所以两套都出,运行时用 canPlayType 探测,只会下载其中一个。
③ 配音:build-voice.py —— 249MB → 试点单章 2.5MB
这条最讲究。先做试点,不一次转全量:
全量 opus 后约 100MB,一轮全流程出问题就得整体回退。
单章约 2.5MB、41 条,足够验证切片/预取/排队这套机制。
码率取舍也写明了:源是 mono vorbis 96kb/s;opus 32k mono 降到约 40%
(24k 能到 50%,但齿音上能听出来,不值),aac 48k mono 兜底。
(24k 能到 50%,但齿音上能听出来,不值),aac 48k mono 兜底。
四、音频的两条"物理约束"
写 audio.ts / voice.ts 时,真正决定代码形状的不是功能,是浏览器的物理约束:
BGM:自动播放策略
没有用户手势之前 play() 会被拒。 所以音乐不在页面加载时启动,
而是在玩家点「入局」之后 —— 那一下 click 就是合法手势。
而是在玩家点「入局」之后 —— 那一下 click 就是合法手势。
另外两条:只下纯音乐版(vocal 版 ~1.5MB 做成可选开关,默认不预载);
不阻塞交互(<audio> 由浏览器流式取,加载期间游戏照常玩)。
不阻塞交互(<audio> 由浏览器流式取,加载期间游戏照常玩)。
配音:缓冲窗口只有几百毫秒
点完选项到下一句播出来,中间没有留给你下载的时间。 所以必须提前把本章切片拉下来,
而不是播到哪下到哪。
而不是播到哪下到哪。
还有一条:串行播放,不能并发 —— 一章 41 条如果一次性并发拉,会把连接池打满。
一个"不维护清单"的设计
配音切片名纯靠推导得出:
intro_<N> → chapter.intro[N]
<事件id>_s<N> → event.segs[N-1]
<事件id>_o<N> → event.options[N-1].res
所以不需要额外维护一份清单文件 —— src 侧用 import.meta.glob 建索引,
查不到就是没有这条配音,天然降级。
查不到就是没有这条配音,天然降级。
用 glob 而不是「预生成一份 index.json」的理由:
清单会漂移,glob 不会。
五、测试:七套浏览器验收 + 一套纯 Node
prot/ 的测试验规则(引擎/数据/契约);h5/tests/ 验**“浏览器里真的能玩、真的不塌”**。
| 测试 | 验什么 |
|---|---|
| chain.cjs | 李斯第一章全链路(序章→词条→行动卡→关键事件→选项→属性增减→章末) |
| viewports.cjs | 8 种视口下布局不塌 |
| backtrack.cjs | 回溯按钮 → 确认浮层 → 回章首 |
| save.cjs | 存读档 |
| audio.cjs | BGM 格式选择与加载 |
| voice.cjs | 配音切片与降级 |
| reveal.cjs | 渐显动线 |
| scenarios.cjs | 纯 Node:剧本轻量表漂移 |
七套浏览器 + 一套纯 Node = 1,325 行,全部自建 CDP,不引 Playwright/Puppeteer。
dev-check.cjs 专门盯 dev 模式 —— 因为三套测试都跑 dist/ 构建产物,
vite dev 路径平时没人碰。而已知历史问题就是 dev 下 @engine 的 UMD 拿不到 default 导出而白屏。
vite dev 路径平时没人碰。而已知历史问题就是 dev 下 @engine 的 UMD 拿不到 default 导出而白屏。
六、今天的数据
| 项 | 值 |
|---|---|
| H5 源码 | 6,192 行(14 个 .ts + 8 个 .css) |
| 浏览器验收 | 1,325 行 / 7 套 + 1 套纯 Node |
| 资产管线 | 4 条(build-assets / build-audio / build-voice / shot.cjs) |
| 美术 | 131MB PNG → WebP(整站压到几 MB) |
| BGM | 41.3MB → 每曲 ~1.4MB(webm/m4a 双格式) |
| 配音 | 249MB → 试点单章 2.5MB |
| H5 资源总量 | 45MB(bgm 35M / voice 7.1M / char 837K / lua 485K) |
| Lua 版 | 已封存,prot 三门禁仍全绿(SYNTAX 27 / API OK / REPLAY 100/100) |
今天最值钱的一条
"换载体"之所以能一天做完六个页面,不是因为写得快,是因为规则不在渲染层里。
DAY 6 把规则抽成 JS 真源 + Lua 移植,当时只是为了做对拍网。
但 DAY 12 换载体、DAY 13 一天重建六个页面 —— 这份"规则与渲染分离"的架构,
决定了项目的迁移成本。
但 DAY 12 换载体、DAY 13 一天重建六个页面 —— 这份"规则与渲染分离"的架构,
决定了项目的迁移成本。
同样地,今天资产管线的价值也不在"转了个格式",而在把物理约束想清楚再写代码:
自动播放策略决定了音乐何时启动;几百毫秒的缓冲窗口决定了配音必须预取;
opus 在 Safari 不稳决定了必须出双格式。
自动播放策略决定了音乐何时启动;几百毫秒的缓冲窗口决定了配音必须预取;
opus 在 Safari 不稳决定了必须出双格式。
先想清楚"约束是什么",代码形状自己就出来了。
明天
- 配音全量转码 —— 试点已验证机制,--all 跑全量(约 100MB,谨慎)
- 浏览器验收常跑 —— 七套进日常,尤其 dev-check(盯 dev 白屏)
- 首屏体积预算 —— 美术已瘦身,再核一遍首屏实际下载量
- 成就图标 64 缺失 —— P3 美术批次待补(见 docs/已知问题-成就图标契约.md)


载体:H5(Vite + TypeScript),规则引擎复用 prot/engine.js · 状态:六页面 + 罗盘 + 双音频系统 + 七套浏览器验收;资产管线四条 · Lua 版封存中(prot 三门禁全绿)












