fix(iter37①): T2 呈现/组合根缺陷修——蓄力档分档·庆祝句柄·互斥四口·模态 fail-fast·站桩按 id 解析
- d2 蓄力档 `_intent_charge` 布尔分档(颜色相等判据退役,深浅金同值不再误判);d4 庆祝浮动 tween 句柄 kill 纪律(close 必杀) - d5/d6/d7/d8 互斥四口:调试钮判据并入 `_sync_sim_button` 单点、组合根两守卫补模拟态/冒险态、`is_running()` 运行态查询面 - d9 ModalStack open/close fail-fast+遮挡账合并非覆盖(重复 open 不再吞旧账);d12 target_id 产发(damage_controller 四处)+站桩两口 id 先名兜底;d17 await 后实例守卫;d18 参数类型注解 - 断言:standee_fx_sweep_test(新增,18 项)/modal_flow_test V6-5(5 项 wd9,V6-1~4 原样同绿)/damage_controller_test target_id/pacing_queue_test 排播 target_id/system_test wd6+wd7 - 代拍板:d18 卡片字面 `lumas_out: Array[Variant] = null` 在 Godot 4.7 解析报错(typed array 不可默认 null),改用 `Variant = null`——唯一合法注法,零行为变化,随代拍板追认单留痕
This commit is contained in:
1 parent
1b9736f065
commit
0529e1691c
19 files changed
+348
-21
No files matched your search
@@ -59,6 +59,7 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口)
|
||||
{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值)
|
||||
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态
|
||||
{"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析)
|
||||
{"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏)
|
||||
{"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合
|
||||
{"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 工作间两区六路线 UI 直验(V13 自动化前置面)
|
||||
|
||||
@@ -40,6 +40,7 @@ func _split_tail() -> void:
|
||||
await _smoke_scene_split() # 协程函数必须 await(无 await=发射后不管,首个 await 后即被 quit 截断)
|
||||
await _smoke_boot_chain()
|
||||
await _smoke_run_lifecycle()
|
||||
await _smoke_sim_guards()
|
||||
await _smoke_lifecycle_matrix()
|
||||
else:
|
||||
check(false, "V2/V3 前置:autoload 链未就绪(ConfigDB/SaveManager)")
|
||||
@@ -269,6 +270,47 @@ func _smoke_run_lifecycle() -> void:
|
||||
DirAccess.remove_absolute(p)
|
||||
|
||||
|
||||
# ---- d6/d7 批量模拟互斥守卫(wd6/wd7——白盒置态直调入口,以守卫早退的可见后果为断言面)----
|
||||
func _smoke_sim_guards() -> void:
|
||||
print("--- 卡① T2:批量模拟互斥两守卫(d6/d7)---")
|
||||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
await process_frame
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
for p in ["user://smoke_sim_guards.json", "user://smoke_sim_guards.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.attach_path("user://smoke_sim_guards.json")
|
||||
sm.mark_first_adventure_done()
|
||||
# wd6:闲时态 + 模拟运行态 → 调试两层入口守卫早退(开始层不收起、无结算对象)
|
||||
check(main.run_scope.settlement == null and not main.run_scope.is_adventure,
|
||||
"前置:闲时会话(settlement 空 + is_adventure=false)")
|
||||
main._start_overlay.visible = true
|
||||
main._tune_sim._running = true
|
||||
main._start_debug_levels()
|
||||
check(main.run_scope.settlement == null and main._start_overlay.visible,
|
||||
"模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕")
|
||||
main._tune_sim._running = false
|
||||
# wd7:真实冒险关间态(is_adventure=true、settlement==null)→ 模拟入口守卫早退
|
||||
main.debug_overlay.seed_edit.text = "42"
|
||||
main._start_adventure_from_menu()
|
||||
await process_frame
|
||||
check(main.run_scope.is_adventure and main.run_scope.settlement == null,
|
||||
"前置:真实冒险关间态(选路停点,非战斗)")
|
||||
main._on_tune_simulate(3)
|
||||
check(not main._tune_sim.is_running(),
|
||||
"冒险关间态下批量模拟入口守卫早退(d7 补 is_adventure)〔wd7〕")
|
||||
main._tune_sim._running = false # 红态清理(守卫缺失时 run 已启动)
|
||||
main.run_scope.adventure.abandon()
|
||||
for i in range(8):
|
||||
await process_frame
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
for p in ["user://smoke_sim_guards.json", "user://smoke_sim_guards.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
|
||||
|
||||
# 引用面快照(五承载物的会话相关面+恒活面——逐个调用现值回调,与 run_scope 成员并采)
|
||||
# (自 game/smoke/smoke_all.gd 剪切迁入——label 与 cond 逐字原样,断言迁移批 B G1)
|
||||
func _face_snapshot(main: Node) -> Dictionary:
|
||||
|
||||
@@ -30,6 +30,7 @@ func _init() -> void:
|
||||
_test_policy_immediate_mode(db)
|
||||
_test_source_death_void(db)
|
||||
_test_identical_content_dual_enemy(db)
|
||||
_test_target_id_payload(db)
|
||||
_test_requires_gate_and_zero(db)
|
||||
_test_hero_death_halt(db)
|
||||
_test_ac_merge(db)
|
||||
@@ -254,6 +255,28 @@ func _test_identical_content_dual_enemy(db) -> void:
|
||||
|
||||
# ---- requires 前置门:前置单未实际有效兑付 → 后继单静默作废;无存活敌 blast = 合法零兑付 ----
|
||||
|
||||
# d12 产发端增键:目标身份随行/随载荷下发(同名敌按 id 精确解析的产发半)
|
||||
func _test_target_id_payload(db) -> void:
|
||||
_rows = []
|
||||
var h = _harness(db)
|
||||
if not h.spawn_ok:
|
||||
Kit.check("前置:units.spawn(1,0) 成功(target_id 断言)", false)
|
||||
return
|
||||
var target = h.units.default_target()
|
||||
var expect_id := String(target.get("enemy_id", ""))
|
||||
Kit.check("前置:目标敌携非空 enemy_id", expect_id != "")
|
||||
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack",
|
||||
"T6目标id", "战斗段", int(db.hero.atk), DamageControllerScript.TARGET_FRONT_ALIVE, 1)
|
||||
var fx_payload: Dictionary = ticket.context.get("fx_attack_payload", {})
|
||||
Kit.check("attack_commit fx 载荷携 target_id〔wd12〕",
|
||||
String(fx_payload.get("target_id", "")) == expect_id and String(fx_payload.get("target", "")) != "")
|
||||
h.ctrl.submit(ticket)
|
||||
h.ctrl.flush()
|
||||
var row := _row_by_display_prefix("T6目标id")
|
||||
Kit.check("伤害行携 target_id == 目标敌 enemy_id〔wd12〕",
|
||||
not row.is_empty() and String(row.extra.get("target_id", "")) == expect_id)
|
||||
|
||||
|
||||
func _test_requires_gate_and_zero(db) -> void:
|
||||
_rows = []
|
||||
var h = _harness(db)
|
||||
|
||||
@@ -87,6 +87,7 @@ func _init() -> void:
|
||||
# V6-4 演播后排窗挂起+边沿派发(T17 语义)
|
||||
var end_ov := Control.new()
|
||||
end_ov.visible = false
|
||||
flow.register(end_ov) # d9 fail-fast 后:未登记层不可进入编排(夹具按 register 先行契约登记)
|
||||
rows["empty"] = false
|
||||
var sync_before1: int = calls["open_sync"]
|
||||
flow.open_after_playback(end_ov)
|
||||
@@ -105,9 +106,34 @@ func _init() -> void:
|
||||
flow.open_after_playback(end_ov)
|
||||
rows["empty"] = true
|
||||
var other := Control.new()
|
||||
flow.register(other)
|
||||
var sync_before: int = calls["open_sync"]
|
||||
flow.open(other)
|
||||
Kit.check_quiet(end_ov.visible and flow._deferred == null and calls["open_sync"] == sync_before + 2,
|
||||
"V6 挂起兜底:开新层先结挂起(T17 防永不兑现——挂起层先开+新层后开两拍)〔迭代27 T4〕")
|
||||
|
||||
# V6-5 遮挡失败快+遮挡账合而非覆盖(d9)
|
||||
var ms = ModalStackScript.new()
|
||||
var ma := Control.new()
|
||||
var mb := Control.new()
|
||||
var stranger := Control.new()
|
||||
stranger.visible = false # Control 默认 visible=true——先隐,才能断言 fail-fast 未把它照上屏
|
||||
ms.register(ma)
|
||||
ms.register(mb)
|
||||
ms.open(ma)
|
||||
ms.open(stranger) # 未登记层:push_error + 早退(零副作用)
|
||||
Kit.check_quiet(not stranger.visible and ma.visible,
|
||||
"V6 未登记层 open fail-fast(该层不可见、已开层不变)〔wd9〕")
|
||||
ms.close(stranger)
|
||||
Kit.check_quiet(not stranger.visible and ma.visible,
|
||||
"V6 未登记层 close fail-fast(已开层不受影响)〔wd9〕")
|
||||
ms.open(mb)
|
||||
ms.open(ma) # 开低层隐其上方可见层
|
||||
Kit.check_quiet(not mb.visible, "V6 开低层隐其上方层(重复 open 前态)〔wd9〕")
|
||||
ms.open(ma) # 同层重复 open:上方已隐,hidden 空表——旧账须保留
|
||||
Kit.check_quiet(not mb.visible, "V6 重复 open 保持遮挡(V6 在役语义不变)〔wd9〕")
|
||||
ms.close(ma)
|
||||
Kit.check_quiet(mb.visible, "V6 重复 open 不吞遮挡账(close 低层恢复 mb——合并非覆盖)〔wd9〕")
|
||||
ms.close(mb)
|
||||
|
||||
quit(Kit.finish("modal_flow"))
|
||||
@@ -7,9 +7,35 @@
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/pacing_queue_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const RowPacerScript := preload("res://presentation/row_pacer.gd")
|
||||
|
||||
var _problems: Array = []
|
||||
|
||||
|
||||
# 记参假站桩(d12 排播半):继承真类以过类型面,只记解析入参
|
||||
class FakeStage extends BattleStageView:
|
||||
var calls: Array = []
|
||||
|
||||
func enemy_standee_center(target_id: String, display_name: String) -> Variant:
|
||||
calls.append([target_id, display_name])
|
||||
return Vector2(10, 10)
|
||||
|
||||
|
||||
# 记参假演出层(同上——play_* 全部只记不演)
|
||||
class FakeFx extends SettlementFxView:
|
||||
var calls: Array = []
|
||||
|
||||
func play_projectile(_from: Vector2, _to: Vector2, _glyph: String, _col: Color, _crit: bool = false) -> void:
|
||||
calls.append(["projectile", _to])
|
||||
|
||||
func play_damage_number(_pos: Vector2, _text: String, _col: Color, _crit: bool = false) -> void:
|
||||
calls.append(["mark", _pos])
|
||||
|
||||
func play_status_pulse(_pos: Vector2, _col: Color) -> void:
|
||||
calls.append(["pulse", _pos])
|
||||
|
||||
|
||||
|
||||
func check(cond: bool, msg: String) -> void:
|
||||
if not cond:
|
||||
_problems.append(msg)
|
||||
@@ -98,6 +124,21 @@ func _init() -> void:
|
||||
UiKit.motion_scale = 0.5
|
||||
check(main.pace_delay_for("damage", "heal") == 0.0, "减动效段间延迟归零(行不跳过——同帧直落快路)")
|
||||
UiKit.motion_scale = 1.0
|
||||
# ---- 断言④:排播两处取用按行 target_id 解析站桩(d12 排播半——假 stage 记参)----
|
||||
var rp = RowPacerScript.new()
|
||||
var fstage := FakeStage.new(null)
|
||||
var ffx := FakeFx.new()
|
||||
rp.stage = fstage
|
||||
rp.board_view = main.board_view
|
||||
rp.settlement_fx = ffx
|
||||
rp._play_settlement_mark({"type": "damage", "target": "哥布林", "target_id": "goblin_b", "value": 3})
|
||||
check(fstage.calls.size() == 1 and fstage.calls[0] == ["goblin_b", "哥布林"],
|
||||
"结算标出按行 target_id 解析站桩(id 与名同传——d12)")
|
||||
rp._play_cross_zone_fx({"type": "damage", "target": "哥布林", "target_id": "goblin_b", "value": 3})
|
||||
check(fstage.calls.size() == 2 and fstage.calls[1] == ["goblin_b", "哥布林"],
|
||||
"跨区弹道按行 target_id 解析站桩(id 与名同传——d12)")
|
||||
check(ffx.calls.size() == 2, "两路解析成功后演出层各接一拍(记参面非空)")
|
||||
rp.free()
|
||||
_finish()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 呈现域缺陷扫掠断言(行为缺陷定点修批卡① T2 五族):
|
||||
# ① 意图蓄力档可达(d2——_intent_charge 分档,旧 GOLD_DEEP 判据恒不可达);
|
||||
# ② 庆祝 tween 句柄纪律(d4——重开不叠加、close 必杀);
|
||||
# ③ 互斥四口(d5 调试钮同判据 / d8 控制器运行态可查);
|
||||
# ⑤ 站桩按敌 id 解析(d12 两口:on_step_row_fx 闪白落点 + enemy_standee_center 弹道落点)。
|
||||
# d6/d7 两守卫断言宿主 = system_test;d3/d17/d18 属非行为面(实现报告等价申报)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/standee_fx_sweep_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const StandeeBodyScript := preload("res://presentation/views/standee_body.gd")
|
||||
const BattleEndScript := preload("res://presentation/views/battle_end_overlay.gd")
|
||||
const DebugOverlayScript := preload("res://presentation/views/debug_overlay.gd")
|
||||
const TuneSimScript := preload("res://presentation/tune_sim_controller.gd")
|
||||
const StageScript := preload("res://presentation/views/battle_stage_view.gd")
|
||||
|
||||
|
||||
# 假调试面板(TuneSimController 所需 setter 面 + 树内节点供给 get_tree)
|
||||
class FakePanel extends Node:
|
||||
var running := false
|
||||
var progress := ""
|
||||
var report := ""
|
||||
var atk := 0
|
||||
var hp := 0
|
||||
|
||||
func atk_pct() -> int:
|
||||
return atk
|
||||
|
||||
func hp_pct() -> int:
|
||||
return hp
|
||||
|
||||
func set_sim_running(r: bool) -> void:
|
||||
running = r
|
||||
|
||||
func set_sim_progress(t: String) -> void:
|
||||
progress = t
|
||||
|
||||
func set_sim_report(t: String) -> void:
|
||||
report = t
|
||||
|
||||
|
||||
# 假自动策略(run_level 签名同 level_autoplay——回调内读控制器运行态)
|
||||
class FakeDriver extends RefCounted:
|
||||
var ctrl
|
||||
var seen: Array = []
|
||||
var calls := 0
|
||||
|
||||
func run_level(_db, _seed: int, _level: int, _atk: int, _hp: int, _other: bool, _logger):
|
||||
calls += 1
|
||||
seen.append(ctrl.is_running())
|
||||
if calls == 1:
|
||||
return {"hero_dmg": 12.0, "rounds": 4, "result": "victory", "hp_left": 3.0, "hp_at_boss": 5.0}
|
||||
return {"hero_dmg": 6.0, "rounds": 2, "result": "defeat",
|
||||
"defeat_node_index": 3, "defeat_node_type": "battle", "hp_left": 0.0, "hp_at_boss": 0.0}
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
UiKit.motion_scale = 1.0
|
||||
|
||||
# ---- ⑤ 站桩按 id 解析(两口共用同名字敌载体)----
|
||||
var stage = StageScript.new(null)
|
||||
root.add_child(stage)
|
||||
var s1 = StandeeBodyScript.new("enemy")
|
||||
var s2 = StandeeBodyScript.new("enemy")
|
||||
stage.world.add_child(s1)
|
||||
stage.world.add_child(s2)
|
||||
s1.set_data({"display_name": "哥布林", "enemy_id": "goblin_a", "hp": 3, "max_hp": 3, "order": 1})
|
||||
s2.set_data({"display_name": "哥布林", "enemy_id": "goblin_b", "hp": 3, "max_hp": 3, "order": 2})
|
||||
s1.position = Vector2(10, 20)
|
||||
s2.position = Vector2(200, 20)
|
||||
stage._cards.clear()
|
||||
stage._cards.append(s1)
|
||||
stage._cards.append(s2)
|
||||
Kit.check("站桩 enemy_id 访问器读装载值〔wd12〕", s1.enemy_id() == "goblin_a" and s2.enemy_id() == "goblin_b")
|
||||
Kit.check("同名敌按 id 精确解析(不做名子串撞车)〔wd12〕",
|
||||
stage._card_by_id("goblin_b") == s2 and stage._card_by_id("goblin_a") == s1)
|
||||
Kit.check("空 id / 未登记 id 恒不命中(名兜底归调用点)〔wd12〕",
|
||||
stage._card_by_id("") == null and stage._card_by_id("no_such") == null)
|
||||
# 口一:on_step_row_fx —— 伤害行携 target_id,闪白落点须在 id 指向的站桩
|
||||
stage.on_step_row_fx({"type": "damage", "target": "哥布林", "target_id": "goblin_b"})
|
||||
await create_timer(0.6).timeout # 弹道到达拍 0.33s + 余量
|
||||
Kit.check("伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕",
|
||||
s2._flash_count == 1 and s1._flash_count == 0)
|
||||
# 口二:enemy_standee_center —— id 先、display_name 兜底
|
||||
var c_b = stage.enemy_standee_center("goblin_b", "哥布林")
|
||||
var c_b_expect: Vector2 = stage.world_to_shell(s2.position + s2.box * 0.5)
|
||||
Kit.check("弹道落点按 target_id 取站桩中心〔wd12〕", c_b != null and c_b == c_b_expect)
|
||||
var c_fallback = stage.enemy_standee_center("", "哥布林")
|
||||
var c_fallback_expect: Vector2 = stage.world_to_shell(s1.position + s1.box * 0.5)
|
||||
Kit.check("无 target_id 时按名兜底同旧行为(首命中)〔wd12〕", c_fallback != null and c_fallback == c_fallback_expect)
|
||||
|
||||
# ---- ① 意图蓄力档可达(d2)----
|
||||
s1.set_pending("damage", "heavy")
|
||||
Kit.check("蓄力技能(heavy)置 _intent_charge=true〔wd2〕", s1._intent_charge)
|
||||
s1.set_pending("damage", "goblin_slash")
|
||||
Kit.check("非蓄力攻击技(goblin_slash)零置位〔wd2〕", not s1._intent_charge)
|
||||
s1.set_pending("buff", "goblin_rage")
|
||||
Kit.check("增益系零置位〔wd2〕", not s1._intent_charge)
|
||||
s1.set_pending("damage", "basic")
|
||||
Kit.check("普攻零表示路径复位 _intent_charge〔wd2〕", not s1._intent_charge)
|
||||
|
||||
# ---- ② 庆祝 tween 句柄纪律(d4)----
|
||||
var ov = BattleEndScript.new()
|
||||
root.add_child(ov)
|
||||
ov.open_victory("判定行", "继续", Rect2(0, 0, 100, 100))
|
||||
var first: Tween = ov._celebrate_tw
|
||||
Kit.check("首开胜利画面创建庆祝句柄〔wd4〕", first != null and first.is_valid())
|
||||
ov.open_victory("判定行", "继续", Rect2(0, 0, 100, 100))
|
||||
Kit.check("重开胜利画面:旧句柄已 kill、新句柄在位(至多一个活跃)〔wd4〕",
|
||||
first != null and not first.is_valid() and ov._celebrate_tw != null and ov._celebrate_tw.is_valid())
|
||||
ov.close()
|
||||
Kit.check("close 必杀庆祝句柄(句柄清空)〔wd4〕", ov._celebrate_tw == null)
|
||||
|
||||
# ---- ③ 互斥四口(d5/d8)----
|
||||
var dbg = DebugOverlayScript.new(null, 0, 0)
|
||||
dbg.set_sim_running(true)
|
||||
Kit.check("模拟运行期调试入口钮同置灰〔wd5〕", dbg._dbg_btn.disabled)
|
||||
dbg.set_sim_running(false)
|
||||
Kit.check("模拟收尾调试入口钮恢复(未锁定)〔wd5〕", not dbg._dbg_btn.disabled)
|
||||
dbg.free()
|
||||
|
||||
var panel := FakePanel.new()
|
||||
root.add_child(panel)
|
||||
var drv := FakeDriver.new()
|
||||
var ctrl = TuneSimScript.new({"action_economy": {"ap_per_round": 2}}, panel, 100, drv)
|
||||
drv.ctrl = ctrl
|
||||
await ctrl.run(2) # 假 driver 回调内读 is_running()(两局各一读)
|
||||
Kit.check("控制器运行中 is_running()=true(驱动回调内读数)〔wd8〕", drv.seen == [true, true])
|
||||
Kit.check("控制器收尾 is_running()=false〔wd8〕", not ctrl.is_running() and not panel.running)
|
||||
Kit.check("两局报告全文注入面板(run 收尾链路完整)〔wd8〕", String(panel.report) != "")
|
||||
|
||||
quit(Kit.finish("standee_fx_sweep"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3vqjio51r2nf
|
||||
Reference in new issue
Block a user