缺失功能路线图 E1 子计划①。元素链路已通,仅补敌人抗性数据+命中查表:
enemies.json 每型可选 resistances{元素名:-0.9~0.9},EnemyManager 按 ctx.damage_type
查表传入 calc_damage(0,res);护甲不动;物理无抗性;设计器加抗性网格。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
5.5 KiB
Markdown
110 lines
5.5 KiB
Markdown
# 元素抗性(Element Resistance / 1-Res)设计
|
||
|
||
> **日期**:2026-07-24
|
||
> **Epic**:E1 战斗深度 · 子计划 ①(缺失功能路线图 `docs_dev/plans/2026-07-23-missing-features-roadmap.md`)
|
||
> **优先级**:P0 · 工作量 S
|
||
> **目标**:让敌人对不同元素有差异化抗性/弱点,激活当前的死代码抗性维度,使元素类词条/核心产生真实伤害差异。
|
||
|
||
---
|
||
|
||
## 1. 背景与现状(代码复核 2026-07-24)
|
||
|
||
元素链路**已完整贯通**,仅缺敌人抗性数据 + 命中时查表:
|
||
- 法术 `meta.damage_type`(int) → 子弹 SoA slot+8(`bullet_manager.gd:145`)→ 命中时 `DamageContextPool.acquire(b_dmg, b_mult, b_type, ...)`(`bullet_manager.gd:77`)→ `ctx.damage_type`。
|
||
- `DamageContext.calc_damage(armor, resistance)`(`damage_context.gd:26`)公式**已含** `(1−resistance)`:`max(0, base×mult×(1−res) − armor×(1−pierce))`。
|
||
- **断点**:`enemy_manager.apply_damage_from_context`(`enemy_manager.gd:253`)调 `ctx.calc_damage()` **不传参** → resistance 恒 0,抗性维度是死代码。
|
||
- 护甲在 `apply_damage`(`enemy_manager.gd:268`)内单独应用(`_ARMOR` 查表),与本设计**互不干扰**。
|
||
|
||
`DamageType`(autoload):PHYSICAL=0, FIRE=1, ICE=2, LIGHTNING=3, POISON=4, ARCANE=5。
|
||
|
||
## 2. 设计
|
||
|
||
### 2.1 数据(enemies.json)
|
||
|
||
每个敌型可选新增 `resistances`,**元素名键**,值域 **-0.9 ~ 0.9**(负=弱点加伤,正=减抗),缺失键=0:
|
||
```json
|
||
"1": { "name": "快速 Fast", "speed": 140, ..., "resistances": { "fire": 0.5, "ice": -0.5 } }
|
||
```
|
||
- 仅覆盖 5 种元素:`fire` `ice` `lightning` `poison` `arcane`。**物理无抗性**(归护甲域)。
|
||
- 全 0 抗性的敌型可省略 `resistances` 键(保持 JSON 简洁)。
|
||
|
||
### 2.2 逻辑(EnemyManager)
|
||
|
||
- 新增成员 `var _RESIST: Dictionary = {}` —— `type_int → PackedFloat32Array`(长度 6,index=damage_type,`[0]` 物理恒 0)。
|
||
- 元素名→int 映射常量(就近 DamageType):
|
||
```gdscript
|
||
const _RESIST_ELEMENTS := {"fire": 1, "ice": 2, "lightning": 3, "poison": 4, "arcane": 5}
|
||
```
|
||
- 在 `_load_json_archetypes()` 的每型循环内追加加载:
|
||
```gdscript
|
||
var rf := PackedFloat32Array([0, 0, 0, 0, 0, 0])
|
||
var res_d: Dictionary = d.get("resistances", {})
|
||
for ename in _RESIST_ELEMENTS:
|
||
if res_d.has(ename):
|
||
rf[_RESIST_ELEMENTS[ename]] = clampf(float(res_d[ename]), -0.9, 0.9)
|
||
_RESIST[t] = rf
|
||
```
|
||
- 新增查表辅助(保底 0,越界安全):
|
||
```gdscript
|
||
func _resist_for(etype: int, dtype: int) -> float:
|
||
var rf: PackedFloat32Array = _RESIST.get(etype, PackedFloat32Array())
|
||
if dtype < 0 or dtype >= rf.size():
|
||
return 0.0
|
||
return rf[dtype]
|
||
```
|
||
- 改写 `apply_damage_from_context`:解析 entity→slot→敌型,查抗性,传入 `calc_damage`:
|
||
```gdscript
|
||
func apply_damage_from_context(entity_id: int, ctx_id: int) -> bool:
|
||
var ctx: DamageContext = DamageContextPool.get_context(ctx_id)
|
||
if ctx == null:
|
||
return false
|
||
var slot: int = _entity_index_map.get(entity_id, -1)
|
||
var res: float = 0.0
|
||
if slot >= 0:
|
||
var etype: int = int(_data[slot * ENEMY_STRIDE + 6])
|
||
res = _resist_for(etype, ctx.damage_type)
|
||
var final_dmg: float = ctx.calc_damage(0.0, res)
|
||
return apply_damage(entity_id, final_dmg, ctx.owner_id, true)
|
||
```
|
||
护甲仍由 `apply_damage` 内的 `_ARMOR` 处理,**不动**。次序:`base×mult×(1−res)` → `apply_damage` 内 `×combo − armor`(floor 1)。物理弹 res=0 → 行为完全不变。
|
||
|
||
### 2.3 设计器(enemy_tab)
|
||
|
||
- 主表(速度/护甲/尺寸/颜色)之后加一张 `HSeparator` + 第二个 `GridContainer`(columns=6):
|
||
- 表头:`原型` + `火 Fire` / `冰 Ice` / `雷 Lightning` / `毒 Poison` / `奥 Arcane`(中英双语中文在前)。
|
||
- 6 行 × 5 元素 `UI.spin(-0.9, 0.9, 0.05, 初值)`,初值读 `_data[str(i)].resistances.get(ename, 0.0)`。
|
||
- 保存到 `_rows[i]["resist"]`(Dictionary ename→SpinBox)。
|
||
- `_save()`:每型汇总非零抗性写入 `resistances`(全 0 则省略该键):
|
||
```gdscript
|
||
var rd := {}
|
||
for ename in ELEM_KEYS:
|
||
var v: float = r["resist"][ename].value
|
||
if absf(v) > 0.0001:
|
||
rd[ename] = v
|
||
# out[str(i)] 内:仅当 rd 非空时加 "resistances": rd
|
||
```
|
||
|
||
## 3. 影响文件
|
||
|
||
| 文件 | 改动 |
|
||
| :-- | :-- |
|
||
| `data/enemies.json` | 可选加 `resistances`(种子:给 1-2 型示例抗性/弱点,供实测) |
|
||
| `scripts/autoloads/enemy_manager.gd` | `_RESIST` + `_RESIST_ELEMENTS` + 加载 + `_resist_for` + 改写 `apply_damage_from_context` |
|
||
| `addons/game_designer/enemy_tab.gd` | 抗性网格 UI + `_save` 写 `resistances` |
|
||
|
||
## 4. 验收标准
|
||
|
||
1. 抗火 0.5 的敌人被**火弹**(damage_type=1)受伤减半;被**物理弹**(0)受伤不变(MCP:同 base_damage 两种 damage_type 对比 HP 降幅)。
|
||
2. 负抗性 `-0.5` 的敌人被对应元素放大 1.5×(实测)。
|
||
3. 抗性与护甲叠加正确:`final = max(1, base×mult×(1−res)×combo − armor)`。
|
||
4. `resistances` 缺失/未列元素 → 该元素抗性 0(回退安全,无报错)。
|
||
5. 设计器抗性网格读写往返 `FAILS=0`(含负值、全 0 省略键)。
|
||
6. `validate_script` 通过;纯数据驱动;越界 damage_type 不崩。
|
||
|
||
## 5. 非目标(YAGNI)
|
||
|
||
- 伤害飘字变色/抵抗提示(E7 DamageNumber 未建)。
|
||
- 状态削抗 / 催化引爆 / 元素反应(E2)。
|
||
- 物理抗性(护甲已覆盖物理减免)。
|
||
- Boss 分阶段动态抗性(后续可扩,本期静态按原型)。
|