[M6] 收敛到 act-scene 单轨:舍弃 map-scene.ts #21
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
⚠️ 方案已变更(2026-09-15)
本 issue 最初的方案是「抽出第三个共享层,两个页面都变薄」,并断言
act-scene.ts无战斗 / 无物品 / 无技能 / 无存档。经逐行核查,那个断言是错的。 下面是修正后的事实。
核查结论一:GameEngine 已经就是共享层
src/game/engine.ts(350 行)只依赖:完全与地图实现无关。 它已经封装了战斗、物品/背包、技能/投射物、任务/NPC、
存读档、掉落/拾取的全部逻辑。
原计划要新建的
src/scene/shared/gameplay.ts不需要了。核查结论二:act-scene.ts 早就接入了玩法
act-scene.ts:34import { GameEngine, syncEngineState }act-scene.ts:777new GameEngine(...)act-scene.ts:820engine.tick({...})act-scene.ts:962engine.world.monstersact-scene.ts:968engine.npcEntitiesact-scene.ts:978engine.projectilesact-scene.ts:984engine.groundact-scene.ts:826-827saving: input.saving, loading: input.loadingact-scene.ts:809-815之前 grep 不到是因为查的是
tickCombat/spawnMonsters的直接导入,而 act-scene 是通过
GameEngine间接获得这些能力的。「真地图上没有怪物」的全部原因只有一行 ——
act-scene.ts:785:核查结论三:map-scene.ts 的逻辑不正确
缺陷 1 · 孤儿世界 + 死代码
前一个世界从头到尾没有被 tick 过。
缺陷 2 · 怪物动画冻结(真实 bug,不只是死代码)
读的是孤儿世界的
tick。而world.tick += 1只在combat.ts:518的tickCombat里发生,map-scene 从不直接调用它(只调engine.tick,那个 tick 的是engine.world)。world.tick恒为 0,表达式退化为group[(monster.index * 3) % group.length]—— 每只怪一个固定帧。怪物在追击和攻击时动画是静止的。
缺陷 3 · 存读档绕过引擎
act-scene 的做法(826-827 行传入真实标志)才是对的。在 tick 外做存读档对确���性是隐患。
附带 · 12 个死导入
tickCombat、rebindPlayer、damageMonster、castSkill、tickProjectiles、QuestLog、npcDialog、captureSnapshot、parseSnapshot、restoreSnapshot、serializeSnapshot、Rng—— 全部 import 但零使用。是「逻辑搬进
GameEngine后未清理外壳」留下的残骸。修正后的范围
1. 打通
stats: []act-scene.ts:785改为接收真实怪物定义。真实数据接入属于 #22,M6 只需打通链路并用占位数据验证。
2. 修复碰撞语义退化
collides(act-scene.ts:1120)对脚底盒采样 9 点,任一阻挡即true。但
combat.ts的脱困规则是terrain.overlap(x + dx, y) <= current:有计数:陷在墙里 5 格深 → 可向 3 格深移动(沿梯度爬出),向 7 格深被拒
布尔化:
1 <= 1恒成立 → 在墙里随机游走,能出来但不可靠、无方向性[NEW]
src/game/iso-terrain.ts—— 等距CollisionGrid→ 计数型WorldMapProvider3. 删除 map-scene.ts 与 map.html
src/scene/map-scene.ts(1055 行)map.htmlsrc/game/map.ts—— 保留仍被d2map.ts依赖的SUB_TILES_PER_TILE等4. GameEngine 的硬编码表格化
engine.ts:83——spawnMonsters(this.world, opts.stats, 8, opts.spawn, 260, this.terrain)怪物数量写死 8、散布半径写死 260。改为由调用方传入
(#22 会用
Levels.txt的NumMon/MonDen填充)。engine.ts:89-99—— NPC 用angle = attempt * 1.7 + 0.6、半径 56 的合成圆环放置。act-scene.ts:809-815已用真实 DS1 坐标覆盖,但这段兜底应下沉为可选项,避免误用。必须遵守的铁律
pushEntity),不能另起一层,否则穿墙穿屋顶CombatWorld.player与CombatWorld.players[i]必须是同一个对象(用rebindPlayer())renderer.flush()Date.now()/Math.random()验收
acts.html第一幕荒野上能被怪物追、能打死怪、能捡掉落、能 K/L 存读档tests/iso-terrain.test.ts验证等距碰撞计数的各向异性map.html与map-scene.ts已删除,且verify:all仍全绿npm run typecheck0 错误、npm test不低于 442 passed 基线[M6] 两轨合流:抽出共享玩法层,让真实地图跑起玩法to [M6] 收敛到 act-scene 单轨:舍弃 map-scene.ts已完成 · 提交
ae94755真正的根因和原来写的不一样
真地图上一直没有玩法,原因不是「两轨没合流」。
act-scene.ts早就通过GameEngine接入了战斗、物品、技能、任务、存档。真正的原因只有一行:而
combat.ts的spawnMonsters第一句就是if (stats.length === 0) return 0。所以这个里程碑没有新建共享层(原计划的
src/scene/shared/gameplay.ts不需要,GameEngine本身就是那一层),而是直接把act-scene.ts接上,并删掉map-scene.ts。改了什么
1. 碰撞语义从布尔改为计数 —— 新增
src/game/iso-terrain.tscombat.ts的脱困规则是if (overlap(next) <= current) 才走。原来传的是collides(...) ? 1 : 0,在墙里读到的永远是 1,1 <= 1恒真,角色在墙里随机游走。改成计数之后,同一条规则变成梯度下降:陷进去 5 格深可以走到 3 格深,朝 7 格深的方向
会被拒绝。
不能复用
map.ts的blockedOverlap:那个函数遍历一个矩形的格索引区间,只有在sub-tile 是屏幕矩形时才成立。等距下 sub-tile 是 16×8 的菱形,屏幕矩形映射过去是
错切且交错的,没有矩形索引区间可遍历。所以改为在包围盒上采样一个固定点阵,逐点问
isBlockedAt。点阵固定而非自适应 —— 计数必须是位置的纯函数,存档与 lockstep 都依赖这点。2. 引擎硬编码下沉为选项 ——
monsterCount(原写死 8)、monsterSpread(原写死 260)、npcRingFallback(NPC 合成圆环;act-scene 本来就用真实 DS1 坐标覆盖掉了,现在干脆不构造)。M7 会用
Levels.txt的NumMon/MonDen填充,目前是占位的 12。3. 顺带补上一个死接口 ——
EngineInput.saving/loading这两个字段声明了,但
tick()从不读它们:存读档是map-scene.ts在 tick 之外自己调
localStorage做的。也就是说act-scene.ts虽然老老实实把input.saving传了进去,K/L 其实什么也不会发生。
现在引擎新增
SnapshotStore端口(read()/write()),在 tick 的最末尾按上升沿处理。这样存档点永远是一个已结算的 tick;按住键不会每 tick 存一次;
localStorage仍留在场景层,模拟层不碰宿主环境;配额满 / 隐私窗口 / 旧格式存档都记进
metrics.saveError。4. 删除
map-scene.ts(1054 行)与map.html它的三个确凿缺陷(孤儿 world / 怪物动画帧恒为 0 / 存读档绕过引擎)加 12 个死 import,
不值得保留。
scripts/browser/checks/map-save-load.js改写为纯逻辑的tests/engine-save-load.test.ts:同样的断言(走位砍怪 → 存档 → 继续跑 → 读档,比对位置/击杀/金币/地面/等级),但不需要 headless Chromium 和 WebGL 上下文,而且顺带覆盖了
浏览器版从未走到的 in-tick 存读档路径。
verify-m4/verify-m5只依赖src/game与src/net,原样保留、仍然全绿。顺手修的工具缺陷
scripts/browser/inspect-page.mjs按「第一个type === 'page'的目标」附着,而 Chrome在打开命令行 URL 之前会先建一个空白标签页,200 ms 的轮询往往抢到空白页,之后的断言就
���「没有场景状态」—— 这和页面启动失败完全无法区分,很容易让人去应用代码里找根本不存在
的 bug。已改为等待 URL 匹配的目标。
验证
npx tsc --noEmitnpm testnpm run verify:allnpm run build新增测试:
tests/iso-terrain.test.ts(8 项)—— 空地为 0、完全陷入时饱和、越界视为阻挡、沿出墙方向单调递减且出现中间值(布尔谓词永远做不到这点)、身体边缘先于中心计入、
y 轴采样密度是 x 轴的两倍(16×8 各向异性)
tests/engine-save-load.test.ts(6 项)—— 漂移后能恢复到存档点、按住键只存一次、按住读档键不会把角色钉住、空槽位不算错误、store 抛异常不掀翻主循环、不配 store 时输入惰性
验收对照
tests/iso-terrain.test.ts验证等距碰撞计数的各向异性map.html及map-scene.ts已删除,且verify:all仍全绿npm run typecheck0 错误、npm test不低于 442 passed 基线acts.html上能被怪物追、能打死怪、能捡掉落、能 K/L 存读档 —— 代码路径已接通,由
tests/engine-save-load.test.ts覆盖下一步:#22(真实怪物 —— 从
Levels.txt/MonStats.txt/SuperUniques.txt读取怪物池、密度、精英怪与超级唯一怪)。