[P1][CR-B7/B9] 超大函数与重复代码:按数据表的函数号分发、抽取公共实现 #527

Open
opened 2026-09-29 06:43:58 +00:00 by troytt · 0 comments
Owner

来源:#516|优先级 P1|评审编号 B7、B9(含 P2 接口层 any)|基线 e475c2e

问题描述

核心模拟和渲染逻辑集中在少数几个上千行的函数里,靠一长串字面量 skillId === N 分支来分发。另外,同一段逻辑有多份手写副本,而且各份的策略并不一致。

这些函数没法单独测试,review 时也看不清一次改动的影响范围。#525 里的多条管线,大部分分支就堆在这些函数中。

证据

1. 超大函数

函数 位置 行数 / 近似圈复杂度
executeSkillCore113c registry.ts:317-2386 2070 / 536
castSkill skill-caster.ts:107-1770 1664 / 462
MissileEngine.tick missile-engine.ts:973-2062 1090 / 337
tickProjectiles skills.ts:4795-5570 776 / 172
executeSUnitDmg combat-pipeline.ts:414-1127 714 / 251
GameEngine.tick engine.ts:498-1134 637 / 272
runScene act-scene.ts:158-743 586 / —
renderWorldLayers world-renderer.ts:533-1095 563 / —

按字面量技能 id 分发的次数:

  • skills.ts:skillId === N 共 197 处;
  • registry.ts:skill.id === N 共 117 处;
  • skill-caster.ts:skillId === N 共 83 处。

2. 重复代码

3. any 的根源在接口层

src 中约 620 处 any,其中 skill-caster.ts 占 113 处。很多下游的 as any 都是因为接口这一层没有写类型。

根因

  • 没有利用数据表自带的分发键。1.13c 用 Skills.txt 的 srvstfunc / srvdofunc 和 Missiles.txt 的 pSrvDoFunc / pSrvHitFunc / pSrvDmgFunc 把技能和投射物映射到少量服务端函数;这里却按技能 id 逐个写分支。
  • 新增功能时,习惯往现有的大函数里加一个分支,或者复制一份相近的代码。

修复指南

  1. 按数据表的函数号建分发表(与 #525 的内核收敛一起做):
    const SRV_START_FUNCS: Record<number, SrvStartFunc> = { /* 按 1.13c srvstfunc 编号 */ }
    const SRV_DO_FUNCS: Record<number, SrvDoFunc> = { /* 按 1.13c srvdofunc 编号 */ }
    const MISSILE_SRV_DO: Record<number, MissileDoFunc> = { /* Missiles.txt pSrvDoFunc */ }
    const MISSILE_SRV_HIT: Record<number, MissileHitFunc> = { /* Missiles.txt pSrvHitFunc */ }
    
    • 每个 handler 是一个独立的小函数,可以单独测试。
    • 没有注册 handler 的函数号直接 throw,不要静默跳过(见 #524)。
    • 同一个函数号下,不同技能之间的差异来自数据表的 Param* 和 calc 列,不在代码里按 id 特判。
  2. 拆 GameEngine.tick:按阶段拆成 applyCommands / tickPlayer / tickMonsters / tickMissiles / resolveDeaths / tickPickups 等方法,每个阶段都能单独测试。
  3. 拆 renderWorldLayers 和 runScene:runScene 的做法见 #526(SceneController);renderWorldLayers 按图层拆开,热路径的对象复用见 #531。
  4. 抽取公共实现:
    • src/game/hash.ts 导出 fnv1a32(bytes | string) 和一个增量版的 Fnv32 类,替换 8 份手写实现;
    • monsterInitSeed(seed, index) 只保留一份;
    • SuperUnique / TC 解析只保留单机版(throw 的那一份),联机也调用它;
    • 投射物范围伤害抽成 applySplashDamage(center, radius, damage, filter),命中和撞墙两处共用。
  5. 先补接口类型:给 grid、getEquippedItems、equippedWeapon、waypointNetwork 等写出具体类型,再清理下游的 as any。可以配合 #537 的 no-explicit-any:先 warn,然后逐个目录收紧。
  6. 拆分前先有测试:每拆一个函数,先用固定种子和固定输入录一份状态快照作为基准,拆完后结果必须一致。

验收标准

  • 上表中每个函数都不超过 200 行,或者已经拆成分发表加独立 handler。
  • skills.ts、registry.ts、skill-caster.ts 中按字面量技能 id 分发的分支减少到只剩确有必要的特例,每个特例都注明 1.13c 出处。
  • FNV-1a、怪物种子、SuperUnique / TC 解析、范围伤害各只有一份实现。
  • engine.ts 和 skill-caster.ts 的公开接口中没有 any。
  • npm run typecheck 0 error;npx vitest run 全部通过。

相关

  • #525:内核收敛(先确定保留哪条管线,再拆函数)。
  • #524:未注册的函数号要 fail-fast。
  • #526:runScene → SceneController。
  • #531:渲染热路径。
  • #537:no-explicit-any。
> 来源:#516|优先级 P1|评审编号 B7、B9(含 P2 接口层 `any`)|基线 `e475c2e` ## 问题描述 核心模拟和渲染逻辑集中在少数几个上千行的函数里,靠一长串字面量 `skillId === N` 分支来分发。另外,同一段逻辑有多份手写副本,而且各份的策略并不一致。 这些函数没法单独测试,review 时也看不清一次改动的影响范围。#525 里的多条管线,大部分分支就堆在这些函数中。 ## 证据 ### 1. 超大函数 | 函数 | 位置 | 行数 / 近似圈复杂度 | |---|---|---| | `executeSkillCore113c` | [registry.ts:317-2386](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/skills/registry.ts#L317-L2386) | 2070 / 536 | | `castSkill` | [skill-caster.ts:107-1770](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/skill-caster.ts#L107-L1770) | 1664 / 462 | | `MissileEngine.tick` | [missile-engine.ts:973-2062](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine/missile-engine.ts#L973-L2062) | 1090 / 337 | | `tickProjectiles` | [skills.ts:4795-5570](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/skills.ts#L4795-L5570) | 776 / 172 | | `executeSUnitDmg` | [combat-pipeline.ts:414-1127](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine/combat-pipeline.ts#L414-L1127) | 714 / 251 | | `GameEngine.tick` | [engine.ts:498-1134](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L498-L1134) | 637 / 272 | | `runScene` | [act-scene.ts:158-743](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/act-scene.ts#L158-L743) | 586 / — | | `renderWorldLayers` | [world-renderer.ts:533-1095](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/world-renderer.ts#L533-L1095) | 563 / — | 按字面量技能 id 分发的次数: - `skills.ts`:`skillId === N` 共 197 处; - `registry.ts`:`skill.id === N` 共 117 处; - `skill-caster.ts`:`skillId === N` 共 83 处。 ### 2. 重复代码 - **FNV-1a 手写了 8 份**:`d2map.ts` 1 份、`rng.ts` 2 份、`lockstep.ts` 5 份(例如 [lockstep.ts:267](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/net/lockstep.ts#L267)、[lockstep.ts:307](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/net/lockstep.ts#L307))。 - **怪物种子公式有 5 份**: - `combat.ts` 3 份:[combat.ts:575](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/combat.ts#L575)、[combat.ts:653](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/combat.ts#L653)、[combat.ts:731](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/combat.ts#L731); - `net-simulation.ts` 2 份:[net-simulation.ts:55](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/net-simulation.ts#L55)、[net-simulation.ts:138](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/net-simulation.ts#L138)。 - **SuperUnique / 宝物类(TC)解析有两份,且策略相反**: - 单机版 [engine.ts:1247-1298](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L1247-L1298):查不到就 throw; - 联机版 [net-simulation.ts:98-128](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/net-simulation.ts#L98-L128):查不到就编造 TC `'Act 1 H2H A'`。 - **投射物命中范围伤害与撞墙爆炸**:两段代码几乎逐行重复,见 [engine.ts:605-667](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L605-L667) 与 [engine.ts:670-725](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L670-L725)。 ### 3. `any` 的根源在接口层 - `WorldMapProvider.grid?: any`:[engine.ts:37](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L37)。 - `getEquippedItems?: () => (Item | PlacedItem | any)[]`:[engine.ts:219](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine.ts#L219)。 - `equippedWeapon`、`equippedAmmo`、`waypointNetwork`、`openStash` 都是 `any`:[skill-caster.ts:65-70](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/skill-caster.ts#L65-L70)。 `src` 中约 620 处 `any`,其中 `skill-caster.ts` 占 113 处。很多下游的 `as any` 都是因为接口这一层没有写类型。 ## 根因 - 没有利用数据表自带的分发键。1.13c 用 Skills.txt 的 `srvstfunc` / `srvdofunc` 和 Missiles.txt 的 `pSrvDoFunc` / `pSrvHitFunc` / `pSrvDmgFunc` 把技能和投射物映射到少量服务端函数;这里却按技能 id 逐个写分支。 - 新增功能时,习惯往现有的大函数里加一个分支,或者复制一份相近的代码。 ## 修复指南 1. **按数据表的函数号建分发表**(与 #525 的内核收敛一起做): ```ts const SRV_START_FUNCS: Record<number, SrvStartFunc> = { /* 按 1.13c srvstfunc 编号 */ } const SRV_DO_FUNCS: Record<number, SrvDoFunc> = { /* 按 1.13c srvdofunc 编号 */ } const MISSILE_SRV_DO: Record<number, MissileDoFunc> = { /* Missiles.txt pSrvDoFunc */ } const MISSILE_SRV_HIT: Record<number, MissileHitFunc> = { /* Missiles.txt pSrvHitFunc */ } ``` - 每个 handler 是一个独立的小函数,可以单独测试。 - 没有注册 handler 的函数号直接 throw,不要静默跳过(见 #524)。 - 同一个函数号下,不同技能之间的差异来自数据表的 `Param*` 和 calc 列,不在代码里按 id 特判。 2. **拆 `GameEngine.tick`**:按阶段拆成 `applyCommands` / `tickPlayer` / `tickMonsters` / `tickMissiles` / `resolveDeaths` / `tickPickups` 等方法,每个阶段都能单独测试。 3. **拆 `renderWorldLayers` 和 `runScene`**:`runScene` 的做法见 #526(`SceneController`);`renderWorldLayers` 按图层拆开,热路径的对象复用见 #531。 4. **抽取公共实现**: - `src/game/hash.ts` 导出 `fnv1a32(bytes | string)` 和一个增量版的 `Fnv32` 类,替换 8 份手写实现; - `monsterInitSeed(seed, index)` 只保留一份; - SuperUnique / TC 解析只保留单机版(throw 的那一份),联机也调用它; - 投射物范围伤害抽成 `applySplashDamage(center, radius, damage, filter)`,命中和撞墙两处共用。 5. **先补接口类型**:给 `grid`、`getEquippedItems`、`equippedWeapon`、`waypointNetwork` 等写出具体类型,再清理下游的 `as any`。可以配合 #537 的 `no-explicit-any`:先 warn,然后逐个目录收紧。 6. **拆分前先有测试**:每拆一个函数,先用固定种子和固定输入录一份状态快照作为基准,拆完后结果必须一致。 ## 验收标准 - [ ] 上表中每个函数都不超过 200 行,或者已经拆成分发表加独立 handler。 - [ ] `skills.ts`、`registry.ts`、`skill-caster.ts` 中按字面量技能 id 分发的分支减少到只剩确有必要的特例,每个特例都注明 1.13c 出处。 - [ ] FNV-1a、怪物种子、SuperUnique / TC 解析、范围伤害各只有一份实现。 - [ ] `engine.ts` 和 `skill-caster.ts` 的公开接口中没有 `any`。 - [ ] `npm run typecheck` 0 error;`npx vitest run` 全部通过。 ## 相关 - #525:内核收敛(先确定保留哪条管线,再拆函数)。 - #524:未注册的函数号要 fail-fast。 - #526:`runScene` → `SceneController`。 - #531:渲染热路径。 - #537:`no-explicit-any`。
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#527
No description provided.