Files
spellforge/docs_dev/specs/2026-07-24-element-resistance-design.md
joywayerandClaude Opus 4.8 e87e453abf docs(spec): E1-① 元素抗性设计——apply_damage_from_context 查表 + enemies.json resistances + 弱点(负抗)
缺失功能路线图 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>
2026-07-24 11:45:28 +08:00

5.5 KiB
Raw Permalink Blame History

元素抗性(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+8bullet_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)公式已含 (1resistance)max(0, base×mult×(1res) armor×(1pierce))
  • 断点enemy_manager.apply_damage_from_contextenemy_manager.gd:253)调 ctx.calc_damage() 不传参 → resistance 恒 0,抗性维度是死代码。
  • 护甲在 apply_damageenemy_manager.gd:268)内单独应用(_ARMOR 查表),与本设计互不干扰

DamageTypeautoload):PHYSICAL=0, FIRE=1, ICE=2, LIGHTNING=3, POISON=4, ARCANE=5。

2. 设计

2.1 数据(enemies.json

每个敌型可选新增 resistances元素名键,值域 -0.9 ~ 0.9(负=弱点加伤,正=减抗),缺失键=0:

"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(长度 6index=damage_type[0] 物理恒 0)。
  • 元素名→int 映射常量(就近 DamageType):
    const _RESIST_ELEMENTS := {"fire": 1, "ice": 2, "lightning": 3, "poison": 4, "arcane": 5}
    
  • _load_json_archetypes() 的每型循环内追加加载:
    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,越界安全):
    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
    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×(1res)apply_damage×combo armorfloor 1)。物理弹 res=0 → 行为完全不变。

2.3 设计器(enemy_tab

  • 主表(速度/护甲/尺寸/颜色)之后加一张 HSeparator + 第二个 GridContainercolumns=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 则省略该键):
    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 + _saveresistances

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×(1res)×combo armor)
  4. resistances 缺失/未列元素 → 该元素抗性 0(回退安全,无报错)。
  5. 设计器抗性网格读写往返 FAILS=0(含负值、全 0 省略键)。
  6. validate_script 通过;纯数据驱动;越界 damage_type 不崩。

5. 非目标(YAGNI

  • 伤害飘字变色/抵抗提示(E7 DamageNumber 未建)。
  • 状态削抗 / 催化引爆 / 元素反应(E2)。
  • 物理抗性(护甲已覆盖物理减免)。
  • Boss 分阶段动态抗性(后续可扩,本期静态按原型)。