docs(10-26): 修复高级 II 复审非阻断项

This commit is contained in:
unanmed 2026-10-21 20:48:08 +08:00
parent 6d9dc28c9e
commit 04aae346ca
7 changed files with 33 additions and 11 deletions

View File

@ -112,3 +112,17 @@
- `pathfinding.md`:`InvalidInput` 的原因补充「缺少方向处理器」。
**验证**:prettier 格式化通过;`pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build` 通过。
### 第 3 轮复查(2026-10-21)
复查第 3 轮提出的 5 个非阻断项,全部对照源码修正,构建通过。
**非阻断**
- `pathfinding.md`:`block.static` 原先写成「空格为 `null`」与 `ILayerLocation` / `IMapLayer.getLocationData` 不符——只要坐标在地图内就返回缓存的 `IStaticTile`,空格时 `num()` 为 `0`。改为说明 `static` 非 `null`、判断空格用 `block.tile === 0`,与 `static-vs-dynamic-tiles.md` 对齐。
- `custom-replay-command.md`:被动示例使用 `ReplayCode.SkillEffect`,但指令码枚举只列了 `UseSkill`,复制后无法编译。在枚举末尾补 `SkillEffect`,并说明它对应紧随其后的被动步、两个码都要注册。
- `equipment-system.md`:`compareEquip` 的逐键回退原先只有三条文字列表。改为 3 行表格(两侧都提供 / 仅 A 提供 / 仅 B 提供),并在表下给出「空槽模拟」与 `this.attribute.getFinalAttribute(key)` 回退的来源。
- `static-vs-dynamic-tiles.md`:「刚读出来的图块默认是干净的」不成立:`StaticTile.loadState` 先 `restoreDefaultEvents()`,再按存档 `clear()` + `set()`,后者不调用 `markPure()`;带自定义事件的存档读回后 `dirty()` 仍为 `true`。改为区分「新建」与「读档」。
- `adding-hero-attribute.md` / `adding-enemy-attribute.md`:护甲边界改为「夹紧后 `≥ 100`」——此时最终伤害为 `0`(勇士侧)或勇士每回合伤害为 `0`、怪物无法被击败(怪物侧),负值夹到 `0`;不再表述为「大于 100 会变成治疗」。
**验证**:prettier 格式化通过;`pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build` 通过。

View File

@ -459,7 +459,7 @@ function verifyEnemyAttribute(
- **新增字段会进入怪物存档**:读旧档时缺失的键不会自动补齐,需要按上文「旧存档迁移」在 `state.loadState` 之前给默认值。
- **光环、查询、最终效果改的都是计算后怪物**:刷新前不要把它当原始数据;也不要在这三个阶段改 `getBaseEnemy()`。
- **直接改原始怪物要标脏**:`getModifiableEnemy()` 的修改不会自动同步,必须 `markDirty()` 后调用 `state.enemyContext.requestRefresh(view)`,再重新读取计算后怪物与伤害。
- **护甲要夹在有效区间**:大于 `100` 会让怪物无法被击败,负数会放大伤害;在计算器入口统一夹一次。
- **护甲要夹在有效区间**:计算器先把护甲夹到 `0`–`100`;夹紧后 `≥ 100` 时勇士每回合伤害为 `0`、怪物无法被击败,负值会被夹到 `0`,不会放大伤害。
- **攻速改动会影响回合数**:凡是依赖 `turn` 的逻辑(支援、先攻、状态持续时间)都要跟着复核,不能只改伤害数字。
- **编辑器与图鉴尚未接入**:在对应端实现之前,不要假设属性会自动出现在界面上。

View File

@ -472,7 +472,7 @@ function verifyHeroAttribute(
- **不要把新属性塞进既有汇总字段**:例如不要把攻速加进 `money` 或 `exp` 的统计逻辑,读这些字段的系统并不知道新属性的语义。
- **新增字段会影响存档结构**:读旧档时缺失的键不会自动补齐。迁移入口是在调用 `state.loadState(save, compression)` 之前修改 `@system/hero` 的 `attribute.values`(见上文「旧存档迁移」),不要指望 `core.jsonc` 或 `HERO_DEFAULT_ATTRIBUTE` 兜底;写入新档后,旧版本读到多出的键通常可忽略,但不能假设旧版本认识它。
- **`save = true` 的修饰器要注册类型**:读档按 `type` 查构造器,未注册的修饰器会被静默跳过,数值看起来「读档后消失了」。
- **护甲要夹在有效区间**:大于 `100` 会变成治疗,负数会放大伤害;在计算器入口统一夹一次。
- **护甲要夹在有效区间**:计算器先把护甲夹到 `0`–`100`;夹紧后 `≥ 100` 时最终伤害为 `0`,不是治疗,负值会被夹到 `0`,不会放大伤害。
- **攻速改动会影响回合数**:凡是依赖 `turn` 的逻辑(支援、先攻、状态持续时间)都要跟着复核,不能只改伤害数字。
- **编辑器与状态栏尚未接入**:在对应端实现之前,不要假设属性会自动出现在界面上。

View File

@ -37,10 +37,14 @@ export const enum ReplayCode {
/** 卸下装备 */
Unequip,
/** 使用技能(本篇新增) */
UseSkill
UseSkill,
/** 技能特效的被动步(本篇新增) */
SkillEffect
}
```
本篇新增两个码:`UseSkill` 是主动步,`SkillEffect` 是紧跟其后的被动步;两者都要在 `registerCommand` 里注册,否则沙盒按指令码查表时会中止播放。
新增一项意味着修改 `packages-user` 的引擎内容,而不是只改 `src`。`registerCommand` 收的是普通 `number`,跳过枚举直接写数字也能注册;但引擎之后分配新码时可能撞号,所以正式做法仍然是在枚举末尾追加。
指令码的取值还受位宽限制。默认的 `ReplayCommandWidth.Uint8` 只支持 `0–255`,超出后写入会被截断。需要更多指令码时,在开始录制前切到 `ReplayCommandWidth.Uint16`(`0–65535`):

View File

@ -182,13 +182,15 @@ this.attribute.addModifier(name, modifier, false);
内部会克隆当前属性,先移除该槽现有装备的修饰器,再把 A、B 分别挂到这个克隆上模拟。真实属性不会被修改。返回值的键只包含 A 或 B 涉及的属性。
差值并不总是两份模拟直接相减:某一侧没有提供某个键时,该侧会回退到**当前真实最终属性**(仍包含目标槽上的现役装备)。因此:
差值并不总是两份模拟直接相减:某一侧没有提供某个键时,该侧会回退到**当前真实最终属性**(`this.attribute.getFinalAttribute(key)`,仍包含目标槽上的现役装备)。逐键规则如下:
- 两侧都提供同一个键:差值是「空槽 + A」减「空槽 + B」;
- 只有 A 提供该键:差值是「空槽 + A」减当前状态(含目标槽现役装备);
- 只有 B 提供该键:差值是当前状态减「空槽 + B」。
| A 提供该键 | B 提供该键 | 差值 |
| ---------- | ---------- | ---------------------------------- |
| 是 | 是 | 「空槽模拟 + A」−「空槽模拟 + B」 |
| 是 | 否 | 「空槽模拟 + A」− 当前真实最终属性 |
| 否 | 是 | 当前真实最终属性 −「空槽模拟 + B」 |
目标槽为空时,当前状态就是空槽基线,三种情况都退化为 A − B。传 `-1` 表示该侧没有装备:目标槽为空时,`compareEquip(a, -1, slot)` 是 A 相对空槽的提升;目标槽已有装备 X 时,它得到的是 A 相对 X 的差值(A − X),**不是**相对空槽。反过来 `compareEquip(-1, b, slot)` 会得到 X − B,符号容易看反,按需要的方向选择参数顺序。
表里的「空槽模拟」指克隆属性先移除目标槽现役装备的修饰器,再挂上 A 或 B。目标槽为空时,当前真实最终属性就是空槽基线,三种情况都退化为 A − B。传 `-1` 表示该侧没有装备:目标槽为空时,`compareEquip(a, -1, slot)` 是 A 相对空槽的提升;目标槽已有装备 X 时,它得到的是 A 相对 X 的差值(A − X),**不是**相对空槽。反过来 `compareEquip(-1, b, slot)` 会得到 X − B,符号容易看反,按需要的方向选择参数顺序。
A 与 B 要么就是目标槽上的现役装备,要么完全不装备。**不要把它们穿在另一个槽上**:那样它们的修饰器会同时留在基线(当前属性)和模拟里,被重复计入。

View File

@ -139,11 +139,13 @@ function replacePathCost(state: ICoreState, cost: PathCostFunction): void {
`PathCostFunction` 接收一个 `ILayerLocation`,返回进入该格的代价。不注入时每格代价为 `1`。损失函数只读取位置信息,不修改地图:
- `block.tile`:该格静态图块的数字;
- `block.tile`:该格静态图块的数字,空格为 `0`;
- `block.locator`:该格坐标;
- `block.static`:静态图块实例,空格为 `null`;
- `block.static`:该格静态图块实例,只要坐标在地图内就非 `null`,空格时实例的 `num()` 为 `0`;
- `block.dynamics`:该格的动态图块集合。
判断空格要用 `block.tile === 0`,不要用 `block.static === null`:图层按坐标缓存静态图块实例,空地上 `layer.getTile()` 同样返回实例。
返回值必须是有限数或 `Infinity`。`Infinity` 是合法的「不可进入」代价;`NaN` 与负数会被视为非法,告警后回退为 `1`。
```ts

View File

@ -183,7 +183,7 @@ async function shiftRight(
| `clear()` | 清空当前事件 |
| `markPure()` | 把当前事件标记为新的参考基准 |
`markPure()` 建立「干净基准」:此后 `dirty()` 只反映相对这一刻的变化。静态图块与动态图块在构造、读档、重置默认事件后都会调用它,所以刚读出来的图块默认是干净的。存档时只需要保存 `dirty()` 为 `true` 的图块事件;如果事件与基准一致,就没有必要写入存档。
`markPure()` 建立「干净基准」:此后 `dirty()` 只反映相对这一刻的变化。静态图块与动态图块在构造、重置默认事件后都会调用它,所以新建的图块默认是干净的。读档则不一定:`loadState` 先 `restoreDefaultEvents()`,再按存档 `clear()` 并逐条 `set()`,后者不会重新调用 `markPure()`;因此存档里带自定义图块事件时,读回后 `dirty()` 仍为 `true`,只有没有自定义事件的图块才是干净的。存档时只需要保存 `dirty()` 为 `true` 的图块事件;如果事件与基准一致,就没有必要写入存档。
`set` 的 `event` 参数是**事件 id**,不是事件对象。这个 id 必须已经在 `state.eventStore` 中注册过(`state.eventStore.addEvent(id, event)`);否则执行到这里时找不到对应事件。同一个优先级只能挂一个事件,重复设置会覆盖并告警 code 136。