docs(plan): E1-① 元素抗性逐任务实现计划(3 任务 · lambda-MCP 实测)
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
# 元素抗性(Element Resistance)Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 让敌人对不同元素有差异化抗性/弱点,激活当前的死代码抗性维度,使元素类伤害产生真实差异。
|
||||
|
||||
**Architecture:** `enemies.json` 每型可选 `resistances{元素名:-0.9~0.9}` → `EnemyManager._RESIST`(type→PackedFloat32Array[6]) 加载 → `apply_damage_from_context` 按 `ctx.damage_type` 查表传入 `calc_damage(0, res)`。护甲逻辑不动(仍在 `apply_damage` 内);物理(0) 无抗性;越界安全回退 0。设计器 enemy_tab 加 6×5 抗性网格。
|
||||
|
||||
**Tech Stack:** Godot 4.7.1 Mono / GDScript / 纯 JSON 数据驱动 / godot-mcp-pro 运行时实测(无 pytest:核心逻辑经运行中战斗场景 `execute_game_script` 断言,编辑器侧/设计器经 `execute_editor_script` + `validate_script`)。
|
||||
|
||||
> **规范**:所有 `.gd` 改动后 `validate_script`;纯数据驱动无硬编码;提交中文;分支 `feat/element-resistance`(已建)。spec:`docs_dev/specs/2026-07-24-element-resistance-design.md`。DamageType:PHYSICAL=0/FIRE=1/ICE=2/LIGHTNING=3/POISON=4/ARCANE=5。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 核心 —— EnemyManager 抗性加载 + 命中查表 + enemies.json 种子
|
||||
|
||||
**Files:**
|
||||
- Modify: `data/enemies.json`(给 "1" Fast 加示例抗性/弱点)
|
||||
- Modify: `scripts/autoloads/enemy_manager.gd`(成员区 :40-43、`_load_json_archetypes` :56-63、`apply_damage_from_context` :253-258)
|
||||
|
||||
- [ ] **Step 1: enemies.json 加种子抗性**
|
||||
|
||||
在 `data/enemies.json` 中,给 `"1"`(快速 Fast)项加 `resistances`(抗火、弱冰),用于实测。改为:
|
||||
```json
|
||||
"1": { "name": "快速 Fast", "speed": 140, "armor": 0, "size": 11, "color": [0.95, 0.72, 0.20], "resistances": { "fire": 0.5, "ice": -0.5 } },
|
||||
```
|
||||
(其余型不加,保持缺失=0 的回退路径可测。注意 JSON 逗号。)
|
||||
|
||||
- [ ] **Step 2: EnemyManager 增成员 + 元素映射常量**
|
||||
|
||||
在 `enemy_manager.gd:43`(`var _RENDER_COLOR...` 之后)新增:
|
||||
```gdscript
|
||||
var _RESIST: Dictionary = {} # type → PackedFloat32Array(6),index=damage_type,[0]物理恒0
|
||||
const _RESIST_ELEMENTS := {"fire": 1, "ice": 2, "lightning": 3, "poison": 4, "arcane": 5}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: `_load_json_archetypes` 加载抗性**
|
||||
|
||||
在 `enemy_manager.gd` 的 `_load_json_archetypes()` 每型循环内(`_RENDER_COLOR[t] = ...` 之后,`:63` 处)追加:
|
||||
```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
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 增查表辅助**
|
||||
|
||||
在 `apply_damage_from_context` 之前(或就近)新增:
|
||||
```gdscript
|
||||
## 元素抗性查表:越界/缺失回退 0(物理 index 0 恒 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]
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 改写 `apply_damage_from_context` 传入抗性**
|
||||
|
||||
将 `enemy_manager.gd:253-258` 的整个函数替换为:
|
||||
```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` 处理,勿动。)
|
||||
|
||||
- [ ] **Step 6: 校验语法**
|
||||
|
||||
Run: godot-mcp-pro `validate_script` on `res://scripts/autoloads/enemy_manager.gd`
|
||||
Expected: 无错误。
|
||||
|
||||
- [ ] **Step 7: 校验 JSON + 加载(编辑器脚本)**
|
||||
|
||||
Run: godot-mcp-pro `execute_editor_script`:
|
||||
```gdscript
|
||||
var d = JSON.parse_string(FileAccess.get_file_as_string("res://data/enemies.json"))
|
||||
var ok := d is Dictionary and d.has("1") and d["1"].has("resistances")
|
||||
ok = ok and abs(float(d["1"]["resistances"].get("fire", 0)) - 0.5) < 0.001
|
||||
ok = ok and abs(float(d["1"]["resistances"].get("ice", 0)) + 0.5) < 0.001
|
||||
print("FAILS=", 0 if ok else 1)
|
||||
```
|
||||
Expected: `FAILS=0`
|
||||
|
||||
- [ ] **Step 8: 提交**
|
||||
|
||||
```bash
|
||||
git add data/enemies.json scripts/autoloads/enemy_manager.gd
|
||||
git commit -m "feat(resist): EnemyManager 元素抗性加载+命中查表;enemies.json Fast 种子抗火弱冰"
|
||||
```
|
||||
(heredoc,body 末尾加 `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`。)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 设计器 —— enemy_tab 抗性网格 + _save 写入
|
||||
|
||||
**Files:**
|
||||
- Modify: `addons/game_designer/enemy_tab.gd`(成员 :9-11、`_ready` :24-40、`_save` :46-60)
|
||||
|
||||
- [ ] **Step 1: 增元素常量 + 行结构说明**
|
||||
|
||||
在 `enemy_tab.gd:7`(`const NAMES...` 之后)新增:
|
||||
```gdscript
|
||||
const ELEM_KEYS = ["fire", "ice", "lightning", "poison", "arcane"]
|
||||
const ELEM_LABELS = ["火 Fire", "冰 Ice", "雷 Lightning", "毒 Poison", "奥 Arcane"]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `_ready` 主表后加抗性网格**
|
||||
|
||||
在 `enemy_tab.gd` 的 `_ready()` 内、`add_child(HSeparator.new())`(现 :37,主表与保存按钮之间)**之后、保存按钮之前**插入:
|
||||
```gdscript
|
||||
add_child(UI.cell_label("元素抗性 Resistances(-0.9~0.9,负=弱点加伤)"))
|
||||
var rgrid := GridContainer.new()
|
||||
rgrid.columns = ELEM_KEYS.size() + 1
|
||||
add_child(rgrid)
|
||||
var rhead := Label.new(); rhead.text = "原型"; rhead.modulate = Color(0.7, 0.8, 1.0)
|
||||
rgrid.add_child(rhead)
|
||||
for el in ELEM_LABELS:
|
||||
var el_l := Label.new(); el_l.text = el; el_l.modulate = Color(0.7, 0.8, 1.0)
|
||||
rgrid.add_child(el_l)
|
||||
for i in 6:
|
||||
var d2: Dictionary = _data.get(str(i), {})
|
||||
var res_d: Dictionary = d2.get("resistances", {})
|
||||
rgrid.add_child(UI.cell_label(NAMES[i]))
|
||||
var res_spins := {}
|
||||
for ei in ELEM_KEYS.size():
|
||||
var ename: String = ELEM_KEYS[ei]
|
||||
var sp2 := UI.spin(-0.9, 0.9, 0.05, float(res_d.get(ename, 0.0)))
|
||||
rgrid.add_child(sp2)
|
||||
res_spins[ename] = sp2
|
||||
_rows[i]["resist"] = res_spins
|
||||
```
|
||||
(注意:此块必须在 6 行主表 `for i in 6` 之后运行,`_rows` 已填满 6 项——上面的 `_rows[i]["resist"] = ...` 给已存在的行字典追加键。)
|
||||
|
||||
- [ ] **Step 3: 改写 `_save` 写入 resistances**
|
||||
|
||||
将 `enemy_tab.gd:46-60` 的 `_save` 整体替换为:
|
||||
```gdscript
|
||||
func _save() -> void:
|
||||
var out: Dictionary = {}
|
||||
for i in 6:
|
||||
var r: Dictionary = _rows[i]
|
||||
var entry := {
|
||||
"name": NAMES[i],
|
||||
"speed": r["speed"].value,
|
||||
"armor": r["armor"].value,
|
||||
"size": r["size"].value,
|
||||
"color": [r["color"].color.r, r["color"].color.g, r["color"].color.b],
|
||||
}
|
||||
var rd := {}
|
||||
for ename in ELEM_KEYS:
|
||||
var v: float = r["resist"][ename].value
|
||||
if absf(v) > 0.0001:
|
||||
rd[ename] = v
|
||||
if not rd.is_empty():
|
||||
entry["resistances"] = rd
|
||||
out[str(i)] = entry
|
||||
if UI.save_json(PATH, out):
|
||||
UI.set_status(_status, "💾 已保存(重启游戏 F5 生效)")
|
||||
else:
|
||||
UI.set_status(_status, "✗ 保存失败", true)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 校验语法**
|
||||
|
||||
Run: godot-mcp-pro `validate_script` on `res://addons/game_designer/enemy_tab.gd`
|
||||
Expected: 无错误。
|
||||
|
||||
- [ ] **Step 5: 设计器读写往返实测**
|
||||
|
||||
> ⚠️ dock 内驻留旧类需 `EditorInterface.restart_editor(true)` 清缓存后新代码才生效(CLAUDE.md);若下方断言用旧类,先重启编辑器再跑。fresh `load().new()` 通常可避开。
|
||||
|
||||
Run: godot-mcp-pro `execute_editor_script`:
|
||||
```gdscript
|
||||
var Tab = load("res://addons/game_designer/enemy_tab.gd")
|
||||
var t = Tab.new(); add_child(t)
|
||||
var fails := 0
|
||||
# 读:Fast(1) fire 应=0.5, ice=-0.5
|
||||
if abs(t._rows[1]["resist"]["fire"].value - 0.5) > 0.001: fails += 1
|
||||
if abs(t._rows[1]["resist"]["ice"].value + 0.5) > 0.001: fails += 1
|
||||
# 写往返:改 Basic(0) poison=0.3,Fast(1) fire→0,重存重读
|
||||
t._rows[0]["resist"]["poison"].value = 0.3
|
||||
t._rows[1]["resist"]["fire"].value = 0.0
|
||||
t._save()
|
||||
var d = JSON.parse_string(FileAccess.get_file_as_string("res://data/enemies.json"))
|
||||
if abs(float(d["0"].get("resistances", {}).get("poison", -9)) - 0.3) > 0.001: fails += 1 # 写入非零
|
||||
if d["1"].get("resistances", {}).has("fire"): fails += 1 # 归零应省略键
|
||||
if not d["1"].get("resistances", {}).has("ice"): fails += 1 # ice=-0.5 仍在
|
||||
t.queue_free()
|
||||
print("FAILS=", fails)
|
||||
```
|
||||
Expected: `FAILS=0`。测试后用 `git checkout -- data/enemies.json` 还原到 Task 1 种子状态(`_save` 会 pretty-print 重排文件)。
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git checkout -- data/enemies.json # 还原往返测试对文件的改写/重排
|
||||
git add addons/game_designer/enemy_tab.gd
|
||||
git commit -m "feat(resist): 设计器敌人页增元素抗性网格(6×5) + _save 写 resistances(全0省略)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 验收 —— 运行时集成实测(MCP)+ 收尾
|
||||
|
||||
**Files:** 无(运行时断言)+ 收尾文档
|
||||
|
||||
- [ ] **Step 1: 启动战斗场景**
|
||||
|
||||
Run: godot-mcp-pro `play_scene` → `res://scenes/main/combat_s2.tscn`。确认运行、无 `get_editor_errors`。
|
||||
|
||||
- [ ] **Step 2: 抗性/弱点/物理对照(验收①②③④)**
|
||||
|
||||
> 方法:直接生成敌人 + 构造 DamageContext 命中,绕过子弹几何,精确对照。Fast(type=1) 抗火0.5/弱冰-0.5;Basic(type=0) 无抗性。
|
||||
|
||||
> `spawn_enemy(pos, hp, enemy_type)` 签名已确认(`enemy_manager.gd:121`)。用 lambda 辅助(`execute_game_script` 体内不能定义具名 func,须用 Callable)。
|
||||
|
||||
Run: godot-mcp-pro `execute_game_script`:
|
||||
```gdscript
|
||||
var fails := 0
|
||||
# lambda:对 (enemy_type, damage_type) 生成敌人+施加 base=100 伤害,返回实际掉血
|
||||
var probe := func(etype, dtype):
|
||||
var eid = EnemyManager.spawn_enemy(Vector2(300, 0), 1000.0, etype)
|
||||
var before = EnemyManager.get_hp_percent(eid) * 1000.0
|
||||
var cid = DamageContextPool.acquire(100.0, 1.0, dtype, -1, false, 0.0)
|
||||
EnemyManager.apply_damage_from_context(eid, cid)
|
||||
DamageContextPool.release(cid)
|
||||
return before - EnemyManager.get_hp_percent(eid) * 1000.0
|
||||
|
||||
var f1 = probe.call(1, 1) # Fast 抗火0.5 → ≈50
|
||||
if abs(f1 - 50.0) > 1.0: fails += 1
|
||||
var f2 = probe.call(1, 2) # Fast 弱冰-0.5 → ≈150
|
||||
if abs(f2 - 150.0) > 1.0: fails += 1
|
||||
var f3 = probe.call(1, 0) # Fast 物理无抗 → ≈100
|
||||
if abs(f3 - 100.0) > 1.0: fails += 1
|
||||
var b1 = probe.call(0, 1) # Basic 无 resistances 火 → 回退0 ≈100
|
||||
if abs(b1 - 100.0) > 1.0: fails += 1
|
||||
var e1 = probe.call(0, 99) # 越界 damage_type 不崩 → 回退0 ≈100
|
||||
if abs(e1 - 100.0) > 1.0: fails += 1
|
||||
print("FAILS=", fails, " fire_resist=", f1, " ice_weak=", f2, " phys=", f3, " basic_fire=", b1, " oob=", e1)
|
||||
```
|
||||
Expected: `FAILS=0`,fire_resist≈50 / ice_weak≈150 / phys≈100 / basic_fire≈100 / oob≈100。
|
||||
> 注:apply_damage 内 combo_bonus 默认 1.0(无 COMBO_MARK 层),armor 按型(Fast=0/Basic=0)故上述期望值成立。若某敌被一次伤害不足以在同一 slot 留存(如秒杀),大血量 1000 已规避。
|
||||
|
||||
- [ ] **Step 3: 护甲叠加对照(验收③)**
|
||||
|
||||
Run: godot-mcp-pro `execute_game_script`(复用上面 `_spawn`/`_hit` 或重新定义):
|
||||
```gdscript
|
||||
# Armored(2) armor=5,无 resistances:火(1) base100 → max(1, 100×1×1 −5)=95
|
||||
var fails := 0
|
||||
var cid = DamageContextPool.acquire(100.0, 1.0, 1, -1, false, 0.0)
|
||||
var eid = EnemyManager.spawn_enemy(Vector2(300, 0), 1000.0, 2) # (pos, hp, enemy_type=Armored)
|
||||
var before = EnemyManager.get_hp_percent(eid) * 1000.0
|
||||
EnemyManager.apply_damage_from_context(eid, cid)
|
||||
DamageContextPool.release(cid)
|
||||
var drop = before - EnemyManager.get_hp_percent(eid) * 1000.0
|
||||
if abs(drop - 95.0) > 1.0: fails += 1
|
||||
print("FAILS=", fails, " armored_drop=", drop) # 期望 ≈95(护甲减免生效、与抗性正交)
|
||||
```
|
||||
Expected: `FAILS=0`,armored_drop≈95。然后 `stop_scene`。
|
||||
|
||||
- [ ] **Step 4: 更新路线图 + memory + 提交**
|
||||
|
||||
- `docs_dev/plans/2026-07-23-missing-features-roadmap.md`:E1「现状」抗性行标记完成、切分①勾除。
|
||||
- memory `slice-progress-vs-plan.md`:记 E1-① 元素抗性完成(分支/commit)+ MEMORY.md 索引。
|
||||
|
||||
```bash
|
||||
git add docs_dev/plans/2026-07-23-missing-features-roadmap.md
|
||||
git commit -m "docs(roadmap): E1-① 元素抗性完成,勾除现状项"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收标准回溯(spec §4)
|
||||
|
||||
| # | 验收标准 | 覆盖任务 |
|
||||
| :- | :-- | :-- |
|
||||
| 1 | 抗火减半 / 物理不变 | Task 3 Step 2 (f1, f3) |
|
||||
| 2 | 负抗性放大 1.5× | Task 3 Step 2 (f2) |
|
||||
| 3 | 抗性与护甲叠加正确 | Task 3 Step 3 |
|
||||
| 4 | 缺失/未列元素回退 0 | Task 3 Step 2 (b1, oob) |
|
||||
| 5 | 设计器往返 FAILS=0(负值/全0省略) | Task 2 Step 5 |
|
||||
| 6 | validate 通过 / 越界不崩 | 各 validate 步 + Task 3 Step 2 (oob) |
|
||||
Reference in New Issue
Block a user