mirror of
https://github.com/motajs/template.git
synced 2026-11-01 22:24:31 +08:00
449 lines
21 KiB
Markdown
449 lines
21 KiB
Markdown
# 新增勇士属性
|
||
|
||
勇士属性不只是数据表里多一列。一个属性会同时被类型常量、内容配置、修饰器、伤害计算、装备与界面读取;只在接口里加字段,读取处会拿到 `undefined`,参与运算后变成 `NaN`。新增属性真正要补的,是这条链路上的每一处。
|
||
|
||
本篇以攻速(`atkSpeed`,每回合造成的伤害按百分比缩放)与护甲(`armor`,按百分比减免最终伤害)为例,走完「改接口 → 补默认值与初始值 → 读取与修饰 → 接入伤害计算」的数据端流程。编辑器负责新增属性项与内容配置,状态栏负责显示;这些都属于对应端的实现,需要编辑器的部分只留占位。
|
||
|
||
## 新增属性要改哪五处
|
||
|
||
| 位置 | 文件 | 要做什么 |
|
||
| -------- | ---------------------------------------- | -------------------------------------- |
|
||
| 类型 | `packages-user/data-common/src/types.ts` | 在 `IHeroAttr` 中增加字段 |
|
||
| 默认值 | `packages-user/data-state/src/shared.ts` | 补齐 `HERO_DEFAULT_ATTRIBUTE` |
|
||
| 初始值 | `src/content/core.jsonc` | 由编辑器补齐 `init.hero.attribute` |
|
||
| 计算接入 | `src/data/enemy/calculator.ts` 等读取处 | 让属性真正影响伤害或其他运算 |
|
||
| 显示 | 编辑器与渲染端 | 编辑器属性项、状态栏显示(本篇不实现) |
|
||
|
||
前四处缺一不可。接口决定编译期可见性,`core.jsonc` 决定运行期取值,计算接入决定属性是否真的起作用;`HERO_DEFAULT_ATTRIBUTE` 只保证类型形状完整,不是运行时兜底。显示可以后补,但数据端必须先跑通。
|
||
|
||
中级教程的[勇士属性](../../intermediate/hero-attribute.md)已经讲过基础属性、最终属性与修饰器的常用调用,这里补齐「新增一个属性」的完整改动面。
|
||
|
||
## 改接口
|
||
|
||
`IHeroAttr` 位于 `packages-user/data-common/src/types.ts`。要给攻速与护甲留位置,就在接口中增加两个数值字段:
|
||
|
||
```ts
|
||
export interface IHeroAttr {
|
||
/** 勇士名称 */
|
||
name: string;
|
||
/** 勇士等级 */
|
||
level: number;
|
||
/** 勇士生命值 */
|
||
hp: number;
|
||
/** 勇士生命值上限 */
|
||
hpmax: number;
|
||
/** 勇士攻击力 */
|
||
atk: number;
|
||
/** 勇士防御力 */
|
||
def: number;
|
||
/** 勇士护盾 */
|
||
mdef: number;
|
||
/** 勇士魔法值 */
|
||
mana: number;
|
||
/** 勇士魔法上限 */
|
||
manamax: number;
|
||
/** 勇士拥有的金币 */
|
||
money: number;
|
||
/** 勇士拥有的经验 */
|
||
exp: number;
|
||
/** 勇士攻速,百分比。100 表示正常速度,115 表示每回合造成 115% 伤害 */
|
||
atkSpeed: number;
|
||
/** 勇士护甲,百分比减伤。15 表示受到的最终伤害降低 15% */
|
||
armor: number;
|
||
}
|
||
```
|
||
|
||
字段名一旦定下,就会被存档、内容配置、装备与插件引用,旧存档里也会以这个键保存数值。按项目的命名约定,新增公共字段要先确认名字再落笔;改名相当于改存档格式。
|
||
|
||
两个字段都参与伤害计算,因此都是 `number`。如果某个属性只用于展示、不参与数值运算,也仍然建议放在 `IHeroAttr` 之外,避免被存档与装备系统当成可修饰属性。
|
||
|
||
## 补默认值与初始值
|
||
|
||
`HERO_DEFAULT_ATTRIBUTE` 是属性对象的完整形状,只用于类型完整与示例,不会进入运行时。接口增加字段后,这里必须同步补齐,否则 `IHeroAttr` 的类型检查会报缺键:
|
||
|
||
```ts
|
||
/**
|
||
* 勇士属性的完整默认形状。
|
||
* 注释里的「最终会从旧样板读取」是历史说法:当前运行时不会读取这个常量,
|
||
* 它只用于保证接口字段在类型层面齐全;真正的新游戏初始值来自 core.jsonc。
|
||
*/
|
||
export const HERO_DEFAULT_ATTRIBUTE: IHeroAttr = {
|
||
name: '',
|
||
level: 0,
|
||
hp: 1,
|
||
hpmax: 0,
|
||
atk: 0,
|
||
def: 0,
|
||
mdef: 0,
|
||
mana: 0,
|
||
manamax: 0,
|
||
money: 0,
|
||
exp: 0,
|
||
// 100 表示正常攻速,低于 100 变慢,高于 100 变快
|
||
atkSpeed: 100,
|
||
// 0 表示没有减伤;15 表示减伤 15%
|
||
armor: 0
|
||
};
|
||
```
|
||
|
||
实际进入游戏的初始值来自内容配置 `src/content/core.jsonc` 的 `init.hero.attribute`,由 `src/data/initialize.ts` 的 `afterLoad` 调用 `state.hero.createAttribute(coreConfig.init.hero.attribute)`,直接交给 `HeroAttribute` 构造。它不会与默认值合并,缺键就是缺键;`HERO_DEFAULT_ATTRIBUTE` 不会在运行时兜底。
|
||
|
||
> **需要编辑器**:在 `core.jsonc` 的 `init.hero.attribute` 中补齐 `atkSpeed` 与 `armor`。
|
||
|
||
这两项不是可选项。`core.jsonc` 的 `init.hero.attribute` 由编辑器维护,必须补齐这两个键。
|
||
|
||
若只在接口与默认值里加、忘了内容配置,`getBaseAttribute('atkSpeed')` 会返回 `undefined`,`atkSpeed / 100` 随之变成 `NaN`,伤害与回合数都会失真。
|
||
|
||
### 旧存档迁移
|
||
|
||
新游戏只需要改内容配置;旧存档不会和它合并。`state.loadState(save, compression)` 会把 `@system/hero` 里的 `attribute.values` 原样交给 `HeroAttribute.loadState`,缺的键不会被补。需要兼容旧档时,**建议实现一个 `ISaveableExecutor` 并绑定到 `@system/hero`**:它在读档完成后执行,正是为这类迁移准备的。
|
||
|
||
```ts
|
||
import { IHeroAttr } from '@user/data-common';
|
||
import { IHeroState } from '@user/data-base';
|
||
import { ISaveableExecutor } from '@user/data-state';
|
||
|
||
/**
|
||
* 读档后补齐旧档缺失的勇士属性键。
|
||
* Executor 在对应可存档对象读档完成后执行,不包在录像禁用范围内。
|
||
*/
|
||
export class HeroAttributeSaveExecutor implements ISaveableExecutor<
|
||
IHeroState<IHeroAttr>
|
||
> {
|
||
afterLoad(data: IHeroState<IHeroAttr>): void {
|
||
const attribute = data.getModifiableAttribute();
|
||
const values = attribute.toStructured();
|
||
|
||
// 只在旧档确实缺键时补默认值;set 会顺带重算最终值
|
||
if (!Object.hasOwn(values, 'atkSpeed')) attribute.set('atkSpeed', 100);
|
||
if (!Object.hasOwn(values, 'armor')) attribute.set('armor', 0);
|
||
}
|
||
}
|
||
```
|
||
|
||
把执行器绑定在可存档对象注册之后(例如 `initialize` 的 `awake` 里):
|
||
|
||
```ts
|
||
state.addSaveableContent('@system/hero', state.hero);
|
||
state.bindSaveableExecuter('@system/hero', new HeroAttributeSaveExecutor());
|
||
```
|
||
|
||
读档后 `set` 会立刻重算最终值,不需要再手动刷新。新增属性需要存档迁移时,优先用 Executor,而不是在 `loadState` 前后手工改存档映射。
|
||
|
||
## 读取与修饰
|
||
|
||
属性对象有两套读取方式:
|
||
|
||
- `getBaseAttribute(name)` 读基础值,也就是存档里保存的数值;
|
||
- `getFinalAttribute(name)` 读最终值,也就是基础值依次经过全部修饰器后的结果。
|
||
|
||
写基础值要用 `state.hero.getModifiableAttribute()`;只读场景用 `state.hero.getAttribute()`。直接改基础值会触发重算,最终值随之更新。
|
||
|
||
修饰器通过 `addModifier(name, modifier, save?)` 挂到某个属性上。内置的 `ValueModifier` 做数值加减,`PercentageModifier` 按基础值计算百分比:
|
||
|
||
```ts
|
||
import { ICoreState } from '@user/data-state';
|
||
import { PercentageModifier } from '@user/data-base';
|
||
|
||
/** 给勇士挂一个临时的 +15% 攻速。 */
|
||
function addTemporaryAtkSpeed(state: ICoreState): void {
|
||
const attribute = state.hero.getModifiableAttribute();
|
||
const modifier = new PercentageModifier(0.15);
|
||
|
||
// 第三个参数为 false:这个修饰器不进属性存档,由拥有它的系统负责恢复
|
||
attribute.addModifier('atkSpeed', modifier, false);
|
||
|
||
// 需要结束时用同一个修饰器对象移除
|
||
// attribute.deleteModifier('atkSpeed', modifier);
|
||
}
|
||
```
|
||
|
||
`PercentageModifier` 的参数是倍率,`0.15` 表示基础值的 15%。基础攻速为 `100` 时,最终攻速是 `100 + 100 * 0.15 = 115`;它乘的是基础值,不会把其他修饰器的结果再乘一遍。
|
||
|
||
`save` 的取舍决定了读档后由谁恢复这个修饰器:
|
||
|
||
- 装备产生的加成用 `false`。装备槽在读档时会重新穿戴,实例修饰器随之重挂;属性存档再存一份会重复计算。详见[勇士的装备系统](./equipment-system.md)。
|
||
- 由某个系统长期维护的加成,也建议用 `false`,让该系统在自己的读档流程里恢复,避免两套来源各恢复一次。
|
||
- 确实需要属性自己保存的修饰器用 `true`,但它的 `type` 必须已经通过 `registerModifier` 注册;读档时找不到构造器会静默跳过该修饰器。
|
||
|
||
## 接入伤害计算
|
||
|
||
`IDamageCalculator.calculate(handler)` 接收只读的怪物与勇士信息,返回 `{ damage, turn }`:`damage` 是这场战斗勇士会损失的总生命,`turn` 是打完怪物需要的回合数。内置实现在 `src/data/enemy/calculator.ts`,由 `src/data/enemy/initialize.ts` 通过 `damageSystem.useCalculator(new MainDamageCalculator())` 注册。
|
||
|
||
攻速与护甲的接入点不同:
|
||
|
||
- **攻速影响「每回合伤害」**。先算 `atk - monDef`,再乘 `atkSpeed / 100`,最后用缩放后的每回合伤害计算回合数。
|
||
- **护甲影响「最终伤害」**。所有回合伤害汇总后,再乘 `1 - armor / 100`,并把护甲夹在 `0` 到 `100` 之间,避免负伤害或反向治疗。
|
||
|
||
顺序不能反:攻速要在算回合数之前生效,护甲要在最终伤害上生效。下面给出的是**完整**的 `MainDamageCalculator`,吸血、支援、先攻、破甲、反击、净化、魔防、负伤归零、固伤、仇恨、自爆全部保留,只插入攻速与护甲两处:
|
||
|
||
```ts
|
||
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
|
||
import {
|
||
CriticalableHeroStatus,
|
||
IDamageCalculator,
|
||
IEnemyDamageInfoBase,
|
||
IReadonlyEnemyHandler
|
||
} from '@user/data-system';
|
||
import { ISpecial9Value } from './special';
|
||
|
||
/** 带攻速与护甲的勇士伤害计算器。 */
|
||
export class MainDamageCalculator implements IDamageCalculator<
|
||
IEnemyAttr,
|
||
IHeroAttr
|
||
> {
|
||
/** 当前是否正在计算支援怪的伤害 */
|
||
private inGuard: boolean = false;
|
||
|
||
calculate(
|
||
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
|
||
): IEnemyDamageInfoBase {
|
||
const { enemy, locator, hero } = handler;
|
||
const hp = hero.getBaseAttribute('hp');
|
||
const atk = hero.getFinalAttribute('atk');
|
||
const def = hero.getFinalAttribute('def');
|
||
// 支援中魔防只会被计算一次
|
||
const mdef = this.inGuard ? 0 : hero.getFinalAttribute('mdef');
|
||
const monAtk = enemy.getAttribute('atk');
|
||
const monDef = enemy.getAttribute('def');
|
||
let monHp = enemy.getAttribute('hp');
|
||
|
||
// 无敌
|
||
if (
|
||
enemy.hasSpecial(18) &&
|
||
handler.state.hero.items.itemCount('cross') < 1
|
||
) {
|
||
return { damage: Infinity, turn: 0 };
|
||
}
|
||
|
||
let damage = 0;
|
||
let heroPerDamage = 0;
|
||
let enemyPerDamage = 0;
|
||
|
||
// 攻速缩放勇士每回合伤害。
|
||
// 插入点就是原来的 `heroPerDamage += atk - monDef`:
|
||
// 必须在 `turn = Math.ceil(monHp / heroPerDamage)` 之前,
|
||
// 也要在下面的 `<= 0` 早退之前,否则攻速为 0 时会除出 Infinity。
|
||
const atkSpeed = hero.getFinalAttribute('atkSpeed');
|
||
heroPerDamage += ((atk - monDef) * atkSpeed) / 100;
|
||
if (heroPerDamage <= 0) {
|
||
return { damage: Infinity, turn: 0 };
|
||
}
|
||
|
||
// 吸血
|
||
const vampire = enemy.getSpecial<ISpecial9Value>(9);
|
||
if (vampire) {
|
||
const value = (vampire.value.vampire / 100) * hp;
|
||
damage += value;
|
||
// 如果吸血加到自身
|
||
if (vampire.value.add) {
|
||
monHp += value;
|
||
}
|
||
}
|
||
|
||
// 魔攻
|
||
if (enemy.hasSpecial(2)) {
|
||
enemyPerDamage = monAtk;
|
||
} else {
|
||
enemyPerDamage = monAtk - def;
|
||
}
|
||
|
||
// 连击
|
||
const multiHit = enemy.getSpecial<number>(4);
|
||
if (multiHit) {
|
||
enemyPerDamage *= multiHit.value;
|
||
}
|
||
if (enemyPerDamage < 0) enemyPerDamage = 0;
|
||
|
||
// 回合数必须使用缩放后的每回合伤害
|
||
let turn = Math.ceil(monHp / heroPerDamage);
|
||
|
||
// 支援
|
||
const guards = enemy.getAttribute('guard');
|
||
if (guards.size > 0 && !this.inGuard) {
|
||
this.inGuard = true;
|
||
// 打支援怪时当前怪物也会打你,因此回合数与伤害都要累加
|
||
for (const view of guards) {
|
||
const extraInfo = this.calculate({
|
||
enemy: view.getComputedEnemy(),
|
||
context: handler.context,
|
||
locator,
|
||
hero,
|
||
state: handler.state
|
||
});
|
||
turn += extraInfo.turn;
|
||
damage += extraInfo.damage;
|
||
}
|
||
this.inGuard = false;
|
||
}
|
||
|
||
// 先攻
|
||
if (enemy.hasSpecial(1)) {
|
||
damage += enemyPerDamage;
|
||
}
|
||
|
||
// 破甲
|
||
const breakArmor = enemy.getSpecial<number>(5);
|
||
if (breakArmor) {
|
||
damage += (breakArmor.value / 100) * def;
|
||
}
|
||
|
||
// 反击
|
||
const counterAttack = enemy.getSpecial<number>(6);
|
||
if (counterAttack) {
|
||
// 反击是每回合生效,因此加到 enemyPerDamage 上
|
||
enemyPerDamage += (counterAttack.value / 100) * atk;
|
||
}
|
||
|
||
// 净化
|
||
const purify = enemy.getSpecial<number>(7);
|
||
if (purify) {
|
||
damage += purify.value * mdef;
|
||
}
|
||
|
||
damage += (turn - 1) * enemyPerDamage;
|
||
|
||
// 魔防
|
||
damage -= mdef;
|
||
|
||
// 未开启负伤时,如果伤害为负,则设为 0
|
||
if (
|
||
!handler.state.flags.getFieldValueDefaults(
|
||
'enableNegativeDamage',
|
||
false
|
||
) &&
|
||
damage < 0
|
||
) {
|
||
damage = 0;
|
||
}
|
||
|
||
// 固伤,无法被魔防减伤
|
||
const fixedDamage = enemy.getSpecial<number>(20);
|
||
if (fixedDamage) {
|
||
damage += fixedDamage.value;
|
||
}
|
||
|
||
// 仇恨,无法被魔防减伤
|
||
if (enemy.hasSpecial(15)) {
|
||
damage += handler.state.flags.getFieldValueDefaults('hatred', 0);
|
||
}
|
||
|
||
// 护甲按最终伤害百分比减伤。
|
||
// 放在魔防、负伤归零、固伤、仇恨之后,因此这些部分都会被护甲缩放;
|
||
// 放在自爆之前:自爆只把低于 hp - 1 的伤害抬到下限,下限本身不缩放。
|
||
// 如果你希望固伤/仇恨绕过护甲,把这段移到固伤之前。
|
||
const armor = Math.min(
|
||
Math.max(hero.getFinalAttribute('armor'), 0),
|
||
100
|
||
);
|
||
damage *= 1 - armor / 100;
|
||
|
||
// 自爆
|
||
if (enemy.hasSpecial(17)) {
|
||
damage = Math.max(damage, hp - 1);
|
||
}
|
||
|
||
return { damage: Math.floor(damage), turn };
|
||
}
|
||
|
||
getCriticalLimit(
|
||
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
|
||
attribute: CriticalableHeroStatus<IHeroAttr>
|
||
): number {
|
||
switch (attribute) {
|
||
case 'atk': {
|
||
if (handler.enemy.hasSpecial(3)) {
|
||
return Infinity;
|
||
}
|
||
return (
|
||
handler.enemy.getAttribute('def') +
|
||
handler.enemy.getAttribute('hp')
|
||
);
|
||
}
|
||
}
|
||
return handler.hero.getFinalAttribute(attribute);
|
||
}
|
||
}
|
||
```
|
||
|
||
两处插入点的含义:
|
||
|
||
- **攻速**:替换的是「每回合伤害」的初值,位置在吸血、连击、支援之前,但仍在 `turn` 计算与 `<= 0` 早退之前。不能等 `turn` 算完再乘——那样回合数不会变,只是把总伤害整体缩小。
|
||
- **护甲**:放在所有伤害分量汇总之后,缩放的是最终伤害。魔防已经先减掉,固伤与仇恨随后加上,所以它们也会被护甲缩放;自爆在其后执行 `damage = max(damage, hp - 1)`。注意自爆只保证 `hp - 1` 这个**下限**不被护甲缩放:护甲仍会先缩放之前的伤害,只有缩放后低于下限时才被抬回;缩放后仍高于下限时不会恢复。因此不能说「自爆完全不受护甲影响」。不要把护甲乘进 `enemyPerDamage`:那样只减免每回合伤害,先攻、破甲、净化、固伤等一次性伤害会漏掉护甲。
|
||
|
||
`getCriticalLimit` 也要跟着新属性考虑。它决定临界计算的搜索上界;自定义属性若参与伤害,至少要让读取该属性的方式与计算器一致,避免界面显示一个值、战斗按另一个值结算。
|
||
|
||
## 与装备和道具的关系
|
||
|
||
装备的 `value` 与 `percentage` 是按属性名索引的记录,新属性不需要额外注册。只要 `IHeroAttr` 里有这个数值字段,装备定义就能直接配置(`category: 4` 即 `ItemCategory.Equipment`)。
|
||
|
||
> **需要编辑器**:在装备内容里把 `percentage.atkSpeed` 设为 `0.15`。
|
||
|
||
装备实例会把这些记录转成 `ValueModifier` 与 `PercentageModifier`,穿戴时挂到勇士属性,卸下时移除。基础攻速 `100` 配上 `percentage.atkSpeed = 0.15`,最终攻速就是 `115`。注意属性本身按百分数存储(`15` 表示 15%),而装备的 `percentage` 是倍率(`0.15` 表示 15%),两者写法不同。
|
||
|
||
道具效果若要直接改攻速,也可以调用 `state.hero.getModifiableAttribute().add('atkSpeed', value)` 或挂修饰器;道具效果属于内容层,这里不额外展开。
|
||
|
||
## 编辑器与状态栏
|
||
|
||
编辑器需要在英雄属性面板增加攻速与护甲两个输入项,并负责维护 `core.jsonc` 里的内容配置;状态栏需要在合适的位置显示它们的当前值。这些都属于对应端的实现,不在本篇范围内:
|
||
|
||
- `需要编辑器 · 新增英雄属性项`
|
||
- `需要编辑器 · 内容配置`
|
||
- `渲染端 · 状态栏`
|
||
|
||
<!-- TODO(advanced-II): 编辑器属性项、内容配置与状态栏显示 -->
|
||
|
||
数据端完成后,编辑器与状态栏只需要读取 `getFinalAttribute('atkSpeed')` 与 `getFinalAttribute('armor')`。
|
||
|
||
## 怎么验证
|
||
|
||
先准备一只属性固定的怪物,避免光环、支援或随机因素干扰。在编辑器里把内容配置的 `atkSpeed` 设为 `100`、`armor` 设为 `0`,读取最终属性,再穿上一件 +15% 攻速的装备,最后观察伤害变化:
|
||
|
||
```ts
|
||
import { IEnemyAttr } from '@user/data-common';
|
||
import { ICoreState } from '@user/data-state';
|
||
import { IEnemyView } from '@user/data-system';
|
||
|
||
/** 打印攻速、护甲与对指定怪物的伤害。 */
|
||
function verifyHeroAttribute(
|
||
state: ICoreState,
|
||
enemy: IEnemyView<IEnemyAttr>
|
||
): void {
|
||
const attribute = state.hero.getAttribute();
|
||
console.log('攻速', attribute.getFinalAttribute('atkSpeed'));
|
||
console.log('护甲', attribute.getFinalAttribute('armor'));
|
||
|
||
const damageSystem = state.enemyContext.getDamageSystem();
|
||
if (!damageSystem) return;
|
||
|
||
const info = damageSystem.getDamageInfo(enemy);
|
||
if (!info) return;
|
||
|
||
console.log('伤害', info.damage, '回合', info.turn);
|
||
}
|
||
```
|
||
|
||
预期现象:
|
||
|
||
- 初始攻速为 `100`、护甲为 `0`;
|
||
- 穿上 +15% 攻速的装备后,最终攻速变为 `115`,同一只怪物的战斗回合数下降;
|
||
- 把护甲调高后,`damage` 随护甲上升而下降,`turn` 不变。
|
||
|
||
若最终攻速是 `NaN`,先检查编辑器里 `core.jsonc` 的 `init.hero.attribute` 是否补了键;`HERO_DEFAULT_ATTRIBUTE` 只影响类型检查,补了它也不会在运行时兜底。若装备穿上后数值不变,检查装备定义的键名是否与接口字段完全一致。
|
||
|
||
## 边界与注意
|
||
|
||
- **所有创建英雄属性的位置都要补键**:类型常量(`HERO_DEFAULT_ATTRIBUTE`)、内容配置、测试夹具各一处。`Partial<IHeroAttr>` 不会报缺键,但运行时仍会读到 `undefined`。
|
||
- **内容配置不会与默认值合并**:`createAttribute` 直接使用传入对象,缺键就是 `undefined`;不要指望默认值兜底。
|
||
- **百分数属性统一用百分数存储**:`15` 表示 15%,计算时再除以 `100`。装备的 `percentage` 则是倍率,`0.15` 表示 15%。
|
||
- **不要把新属性塞进既有汇总字段**:例如不要把攻速加进 `money` 或 `exp` 的统计逻辑,读这些字段的系统并不知道新属性的语义。
|
||
- **新增字段会影响存档结构**:读旧档时缺失的键不会自动补齐。迁移入口是在调用 `state.loadState(save, compression)` 之前修改 `@system/hero` 的 `attribute.values`(见上文「旧存档迁移」),不要指望 `core.jsonc` 或 `HERO_DEFAULT_ATTRIBUTE` 兜底;写入新档后,旧版本读到多出的键通常可忽略,但不能假设旧版本认识它。
|
||
- **`save = true` 的修饰器要注册类型**:读档按 `type` 查构造器,未注册的修饰器会被静默跳过,数值看起来「读档后消失了」。
|
||
- **护甲要夹在有效区间**:计算器先把护甲夹到 `0`–`100`;夹紧后 `≥ 100` 时最终伤害为 `0`,不是治疗,负值会被夹到 `0`,不会放大伤害。
|
||
- **攻速改动会影响回合数**:凡是依赖 `turn` 的逻辑(支援、先攻、状态持续时间)都要跟着复核,不能只改伤害数字。
|
||
- **编辑器与状态栏读取最终属性**:它们不属于本篇的数据端流程。
|
||
|
||
渲染端如何展示这两个属性不在本篇范围内。 <!-- TODO(renderer): 攻速与护甲的状态栏显示 -->
|
||
|
||
返回[高级 II 导览](./index.md)。
|