# 元素抗性（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 分阶段动态抗性（后续可扩，本期静态按原型）。
