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