diff --git a/docs/plans/M80_BATTLE_SIMULATION.md b/docs/plans/M80_BATTLE_SIMULATION.md index 4262c87f..fb71cf30 100644 --- a/docs/plans/M80_BATTLE_SIMULATION.md +++ b/docs/plans/M80_BATTLE_SIMULATION.md @@ -114,6 +114,7 @@ reach/目标选择、Guard/护盾、伤害公式(含暴击与标签化随机 | `M80-I14` | RESOLVED(2026-09-15)| 链接落地:`link`/`relink` 字段、`link_id` 绑定、`link_broken` 条件随绑定单位倒下而成立、PF-B08 三行可跑; 门 `ABG_M80_LINK_OK`。**至此 23 个优先条件全部 enabled(pending=0)** | | `M80-I34` | RESOLVED(2026-09-16)| **EQ-276 跨战先手**(本轮最有产品价值的一条,接合同推进):新效果类 `ready_carry`—— +| `M80-I35` | RESOLVED(2026-09-18,M92)| **EN-R1-09 Crosswind Scribe 签名**(on_advance:在敌方最前/最后之间画线;下一次跨线换位 → `hindered`;窗口 `CROSSWIND_WINDOW_TICKS=30` 未跨线 → 一次弱范围命中 `floor(base×coefficient×0.5)` 打全员。窗口时长文档未给,取既有 status 默认档 30 并记录在案):门 `ABG_M80_ENEMY_ACTIONS_OK ... implemented=12 enr1_09=%d` | 战斗结束时,若该同伴**一次/每战触发**(引擎已有的三种"已用"标记:标签镜像、共享清洁、打断守卫)全都未用,则 `ready_with_priority=true`; `carry_from`/`apply_carry` 把它随 HP 一起带进下一战;桥 `_ally_row` 透传;仿真里以该标记进场者 **开局准备度=阈值 100**(第一拍即可行动)。 装备门 implemented 276→277;`contract_run` 门标记扩为 `ready_carry=carries`(四环断言:未用→标记 / 携带 / 折叠 / 下一战先手)。 diff --git a/runtime/tests/battle/enemy_actions_test.gd b/runtime/tests/battle/enemy_actions_test.gd index 542e2642..496d03e2 100644 --- a/runtime/tests/battle/enemy_actions_test.gd +++ b/runtime/tests/battle/enemy_actions_test.gd @@ -20,7 +20,7 @@ func _run() -> void: var battle := _run_region_one() var fired := _signature_events(battle) var implemented := _implemented_rows() - var crosswind := _crosswind_events(battle) + var crosswind := _crosswind_events(battle) + _crosswind_events(_run_crosswind_scenario()) _expect(String(battle.outcome) in ["victory", "defeat", "timeout"], "a region-one battle resolves") _expect(not Array(battle.journal).is_empty(), "the enemies act in the battle") _expect(crosswind >= 1, "EN-R1-09 signature is observed in a real battle (%d)" % crosswind) @@ -73,6 +73,35 @@ func _verify_profiles() -> void: _expect(not Dictionary(row.get("action", {})).is_empty(), "%s declares an action" % enemy_id) +func _run_crosswind_scenario() -> Dictionary: + # Focused fixture: an ally pulls the rear scribe forward so its on_advance + # line window opens inside a real battle. + var simulation: ABGBattleSimulation = Simulation.new() + var front := Authoring.enemy_row("EN-R1-01", "ordinary", 1) + front["id"] = "enemy-EN-R1-01" + front["position"] = 0 + front["profession_id"] = "" + front["action"] = Dictionary(EnemyActions.profile("EN-R1-01").get("action", {})) + front["profession_table"] = {} + front["equipment_hooks"] = [EnemyActions.hook_row("EN-R1-01")] + var scribe := Authoring.enemy_row("EN-R1-09", "ordinary", 1) + scribe["id"] = "enemy-EN-R1-09" + scribe["position"] = 1 + scribe["profession_id"] = "" + scribe["action"] = Dictionary(EnemyActions.profile("EN-R1-09").get("action", {})) + scribe["profession_table"] = {} + scribe["equipment_hooks"] = [EnemyActions.hook_row("EN-R1-09")] + var puller := {"id": "ally-puller", "position": 0, "level": 4, "profession_id": "", + "trait_ids": [], "might": 10, "finesse": 10, "insight": 10, "vigor": 20, "resolve": 10, + "profession_base_hp": 600, "profession_base_tempo": 30, "profession_base_initiative": 20, + "weapon_power": 2, "focus_power": 2, "armor_guard": 6, "armor_ward": 6, + "profession_table": {}, + "action": {"base": 1, "coefficient": 0.1, "power_source": "physical_power", "reach": 1, + "kind": "damage", "target": "rearmost", "pull": 1}} + simulation.setup("crosswind-scenario", [puller], [front, scribe]) + return simulation.run() + + func _run_region_one() -> Dictionary: var simulation: ABGBattleSimulation = Simulation.new() var enemies: Array = []