缺失功能路线图 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>
5.5 KiB
5.5 KiB
元素抗性(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:
"1": { "name": "快速 Fast", "speed": 140, ..., "resistances": { "fire": 0.5, "ice": -0.5 } }
- 仅覆盖 5 种元素:
fireicelightningpoisonarcane。物理无抗性(归护甲域)。 - 全 0 抗性的敌型可省略
resistances键(保持 JSON 简洁)。
2.2 逻辑(EnemyManager)
- 新增成员
var _RESIST: Dictionary = {}——type_int → PackedFloat32Array(长度 6,index=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×(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 则省略该键):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. 验收标准
- 抗火 0.5 的敌人被火弹(damage_type=1)受伤减半;被物理弹(0)受伤不变(MCP:同 base_damage 两种 damage_type 对比 HP 降幅)。
- 负抗性
-0.5的敌人被对应元素放大 1.5×(实测)。 - 抗性与护甲叠加正确:
final = max(1, base×mult×(1−res)×combo − armor)。 resistances缺失/未列元素 → 该元素抗性 0(回退安全,无报错)。- 设计器抗性网格读写往返
FAILS=0(含负值、全 0 省略键)。 validate_script通过;纯数据驱动;越界 damage_type 不崩。
5. 非目标(YAGNI)
- 伤害飘字变色/抵抗提示(E7 DamageNumber 未建)。
- 状态削抗 / 催化引爆 / 元素反应(E2)。
- 物理抗性(护甲已覆盖物理减免)。
- Boss 分阶段动态抗性(后续可扩,本期静态按原型)。