[P0][CR-F4] 投射物图集加载失败被静默吞掉,渲染退化为代码绘制的色块或借用别的图 #530

Closed
opened 2026-09-29 06:43:59 +00:00 by troytt · 1 comment
Owner

来源:#516|优先级 P0|评审编号 F4|基线 e475c2e

问题描述

投射物、爆炸、施法特效的图集加载失败时,既没有日志,也没有提示。渲染层随后会用代码绘制的色块代替,或者借用另一个投射物的图。

结果是:美术资源缺失或打包出错时,画面上仍然"有东西",看起来像是能跑,问题很难被发现。这同时违反了 AGENTS.md 的两条规则:

  • §3:禁止宽容吞错;
  • §4.3:严禁长期使用代码绘制的方块、纯几何色块或程序化图形代替技能投射物和特效。

证据

1. 加载失败被吞掉

  • act-scene 启动三个图集加载,都是 void promise.then(...),没有任何 .catch:act-scene.ts:191-201。三个加载分别是:
    • loadMissileArtMap
    • loadItemsAtlas
    • loadOverlayArtMap
  • loadMissileArtMap 内部依次尝试多个候选路径:
    • 响应不是 ok 时直接跳过;
    • 抛出的异常被 catch { // ignore candidate error ... } 吞掉(missile-overlay-renderer.ts:95-97);
    • 所有候选都失败时,这个投射物只是不出现在 map 里,没有任何记录。

2. 缺图时退化为代码绘制或借图

  • 投射物:在 world-renderer.ts:874-887 里:

    • 找不到图集时,画一个 8×8 的黄色实心方块(renderer.drawSolid(... [0.9, 0.9, 0.2, 1]));
    • 闪电类传入 undefined,走程序化绘制。
  • drawMissileProjectile 的缺图分支:从 missile-overlay-renderer.ts:275 起,没有图集时用 drawSolid 拼出程序化图形,例如:

    • 陨石的地面准星和下落火球;
    • 九头蛇的岩浆池和蛇颈。

    这个文件里共有 31 处 drawSolid。

  • 爆炸:

  • 手写的借图表:world-renderer 里还有一张手写的别名表:

    • frozenorbbolt / frozenorbnova → icebolt
    • blizzard4 → blizzard3
    • blizzardexplode3 → blizzardexplode2
    • 闪电类 → lightningstrike / chainlightning

    对照 Missiles.txt,这几组在 CelFile 上确实相同,比如 frozenorbbolt 和 icebolt 都是 IceBolt,chainlightning 和 lightningstrike 都是 LightningStrike,所以借用本身没错。但这种对应关系应该在打包时由 CelFile 推导出来,而不是在渲染代码里手写。而且表里的 'lightning' 在 Missiles.txt 中并没有这一行。

根因

  • 加载器和渲染器都把"缺图"当作可以容忍的情况:一边吞掉错误,一边准备好替代画法。
  • 打包脚本和运行时之间,没有一份"应该有哪些图集"的清单可以校验(AGENTS.md §4.1 要求两者全链路对齐)。

修复指南

  1. 加载失败要可见:
    • loadMissileArtMap / loadOverlayArtMap / loadItemsAtlas 收集每个失败项的名字、候选路径和原因(HTTP 状态或异常);
    • 汇总后 console.error 一次,并在状态栏提示"N 个投射物图集缺失";
    • act-scene 的三个 void ...then() 补上 .catch,走统一的错误上报;
    • 开发模式(import.meta.env.DEV)下直接 fail-fast,抛出带缺失清单的错误。
  2. 删除代码绘制的替代品:
    • 删除投射物的黄色方块、drawMissileProjectile 和 drawExplosion 中的程序化缺图分支,以及"借用 fireexplode"的兜底;
    • 缺图属于资源错误,应该在加载阶段就暴露出来(第 1 步),而不是在渲染阶段掩盖;
    • 不要改成别的占位图形,这同样违反 §4.3。
    • 例外:如果 Missiles.txt 中某行的 CelFile 本来就是空值(例如 lightningbolt),说明原版由客户端特殊绘制。这类情况按 1.13c 的客户端逻辑实现,并注明出处,不算缺图。
  3. 别名由数据推导:
    • scripts/pack-missiles.ts 按 CelFile 烘焙图集;
    • 同时输出一张"投射物名 → 图集"的映射,覆盖 Missiles.txt 中每一个 CelFile 非空的行;
    • 运行时只查这张表,删除 world-renderer 里的手写别名。
  4. 打包与运行时对齐:
    • 打包脚本对每个 CelFile 非空的投射物做校验:文件在 MPQ 中存在,并且烘焙成功,否则让打包失败;
    • 运行时启动时,用同一份清单比对实际加载到的图集。

验收标准

  • 删除一个投射物图集(例如把 firebolt 的 json 改名)后:
    • 开发模式下启动报错,错误信息列出缺失项;
    • 生产模式下控制台有一次汇总的 console.error,状态栏有提示。
  • world-renderer.ts 和 missile-overlay-renderer.ts 中不再有投射物、爆炸的 drawSolid 缺图分支和手写别名表。
  • 打包脚本对 Missiles.txt 中所有 CelFile 非空的行都能产出图集或明确报错;新增测试校验映射表覆盖完整。
  • 涉及渲染,按惯例跑一遍全部 136 个关卡的无头审计。
  • npm run typecheck 0 error;npx vitest run 全部通过。

相关

  • #529:同一批加载函数的取消与过期处理。
  • #524:静默兜底的整体清理。
  • #532:missiles-meta.ts 的生成方式。
> 来源:#516|优先级 P0|评审编号 F4|基线 `e475c2e` ## 问题描述 投射物、爆炸、施法特效的图集加载失败时,既没有日志,也没有提示。渲染层随后会用代码绘制的色块代替,或者借用另一个投射物的图。 结果是:美术资源缺失或打包出错时,画面上仍然"有东西",看起来像是能跑,问题很难被发现。这同时违反了 `AGENTS.md` 的两条规则: - **§3**:禁止宽容吞错; - **§4.3**:严禁长期使用代码绘制的方块、纯几何色块或程序化图形代替技能投射物和特效。 ## 证据 ### 1. 加载失败被吞掉 - act-scene 启动三个图集加载,都是 `void promise.then(...)`,没有任何 `.catch`:[act-scene.ts:191-201](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/act-scene.ts#L191-L201)。三个加载分别是: - `loadMissileArtMap` - `loadItemsAtlas` - `loadOverlayArtMap` - `loadMissileArtMap` 内部依次尝试多个候选路径: - 响应不是 ok 时直接跳过; - 抛出的异常被 `catch { // ignore candidate error ... }` 吞掉([missile-overlay-renderer.ts:95-97](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/missile-overlay-renderer.ts#L95-L97)); - 所有候选都失败时,这个投射物只是不出现在 map 里,没有任何记录。 ### 2. 缺图时退化为代码绘制或借图 - **投射物**:在 [world-renderer.ts:874-887](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/world-renderer.ts#L874-L887) 里: - 找不到图集时,画一个 8×8 的黄色实心方块(`renderer.drawSolid(... [0.9, 0.9, 0.2, 1])`); - 闪电类传入 `undefined`,走程序化绘制。 - **`drawMissileProjectile` 的缺图分支**:从 [missile-overlay-renderer.ts:275](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/missile-overlay-renderer.ts#L275) 起,没有图集时用 `drawSolid` 拼出程序化图形,例如: - 陨石的地面准星和下落火球; - 九头蛇的岩浆池和蛇颈。 这个文件里共有 31 处 `drawSolid`。 - **爆炸**: - 找不到对应图集时,一律借用 `fireexplode` 的图:[world-renderer.ts:893-901](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/world-renderer.ts#L893-L901); - `fireexplode` 也没有时,`drawExplosion` 用 `drawSolid` 画一个程序化的火球:[missile-overlay-renderer.ts:451-464](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/missile-overlay-renderer.ts#L451-L464)。 - **手写的借图表**:world-renderer 里还有一张手写的别名表: - `frozenorbbolt` / `frozenorbnova` → `icebolt` - `blizzard4` → `blizzard3` - `blizzardexplode3` → `blizzardexplode2` - 闪电类 → `lightningstrike` / `chainlightning` 对照 Missiles.txt,这几组在 `CelFile` 上确实相同,比如 `frozenorbbolt` 和 `icebolt` 都是 IceBolt,`chainlightning` 和 `lightningstrike` 都是 LightningStrike,所以借用本身没错。但这种对应关系应该在打包时由 `CelFile` 推导出来,而不是在渲染代码里手写。而且表里的 `'lightning'` 在 Missiles.txt 中并没有这一行。 ## 根因 - 加载器和渲染器都把"缺图"当作可以容忍的情况:一边吞掉错误,一边准备好替代画法。 - 打包脚本和运行时之间,没有一份"应该有哪些图集"的清单可以校验(`AGENTS.md` §4.1 要求两者全链路对齐)。 ## 修复指南 1. **加载失败要可见**: - `loadMissileArtMap` / `loadOverlayArtMap` / `loadItemsAtlas` 收集每个失败项的名字、候选路径和原因(HTTP 状态或异常); - 汇总后 `console.error` 一次,并在状态栏提示"N 个投射物图集缺失"; - act-scene 的三个 `void ...then()` 补上 `.catch`,走统一的错误上报; - 开发模式(`import.meta.env.DEV`)下直接 fail-fast,抛出带缺失清单的错误。 2. **删除代码绘制的替代品**: - 删除投射物的黄色方块、`drawMissileProjectile` 和 `drawExplosion` 中的程序化缺图分支,以及"借用 `fireexplode`"的兜底; - 缺图属于资源错误,应该在加载阶段就暴露出来(第 1 步),而不是在渲染阶段掩盖; - **不要**改成别的占位图形,这同样违反 §4.3。 - 例外:如果 Missiles.txt 中某行的 `CelFile` 本来就是空值(例如 `lightningbolt`),说明原版由客户端特殊绘制。这类情况按 1.13c 的客户端逻辑实现,并注明出处,不算缺图。 3. **别名由数据推导**: - `scripts/pack-missiles.ts` 按 `CelFile` 烘焙图集; - 同时输出一张"投射物名 → 图集"的映射,覆盖 Missiles.txt 中每一个 `CelFile` 非空的行; - 运行时只查这张表,删除 world-renderer 里的手写别名。 4. **打包与运行时对齐**: - 打包脚本对每个 `CelFile` 非空的投射物做校验:文件在 MPQ 中存在,并且烘焙成功,否则让打包失败; - 运行时启动时,用同一份清单比对实际加载到的图集。 ## 验收标准 - [ ] 删除一个投射物图集(例如把 `firebolt` 的 json 改名)后: - 开发模式下启动报错,错误信息列出缺失项; - 生产模式下控制台有一次汇总的 `console.error`,状态栏有提示。 - [ ] `world-renderer.ts` 和 `missile-overlay-renderer.ts` 中不再有投射物、爆炸的 `drawSolid` 缺图分支和手写别名表。 - [ ] 打包脚本对 Missiles.txt 中所有 `CelFile` 非空的行都能产出图集或明确报错;新增测试校验映射表覆盖完整。 - [ ] 涉及渲染,按惯例跑一遍全部 136 个关卡的无头审计。 - [ ] `npm run typecheck` 0 error;`npx vitest run` 全部通过。 ## 相关 - #529:同一批加载函数的取消与过期处理。 - #524:静默兜底的整体清理。 - #532:`missiles-meta.ts` 的生成方式。
Author
Owner

已在提交 3465c80(fix(render): fail fast on missing missile sprite atlases without fallback art (#530))中完成修复,并在 5de27f8 中补充端到端与 136 关全量验证套件。

修复与验证摘要

  • 修复内容:移除投射物/覆盖层图集预加载的 .catch(() => {}) 静默吞错,移除 world-renderer.ts 与 missile-overlay-renderer.ts 中的程序化 drawSolid 色块与借用 fireexplode 兜底,补齐 Missiles.txt CelFile 映射并在缺失必需图集时显式 fail-fast 报错。
  • 专项回归测试:tests/p0-530-missile-art-failfast.test.ts
  • 总体验收门禁:npm run build 0 错误、14 个 P0/E2E 测试套件(190/190 用例)100% 通过、全仓 Vitest 6,460/6,460 通过、136/136 关无头浏览器巡检通过。
已在提交 [`3465c80`](https://git.projectdiablo2.cn/troytt/diablo2-web/commit/3465c8052b318746ce330acb60f0c406294def2f)(`fix(render): fail fast on missing missile sprite atlases without fallback art (#530)`)中完成修复,并在 [`5de27f8`](https://git.projectdiablo2.cn/troytt/diablo2-web/commit/5de27f8177d43524444de7aa301cc470c4f0844e) 中补充端到端与 136 关全量验证套件。 ### 修复与验证摘要 - **修复内容**:移除投射物/覆盖层图集预加载的 `.catch(() => {})` 静默吞错,移除 `world-renderer.ts` 与 `missile-overlay-renderer.ts` 中的程序化 `drawSolid` 色块与借用 `fireexplode` 兜底,补齐 `Missiles.txt` `CelFile` 映射并在缺失必需图集时显式 fail-fast 报错。 - **专项回归测试**:[`tests/p0-530-missile-art-failfast.test.ts`](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/5de27f8177d43524444de7aa301cc470c4f0844e/tests/p0-530-missile-art-failfast.test.ts) - **总体验收门禁**:`npm run build` 0 错误、14 个 P0/E2E 测试套件(190/190 用例)100% 通过、全仓 Vitest 6,460/6,460 通过、136/136 关无头浏览器巡检通过。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#530
No description provided.