# VERTICAL SLICE - NOT FOR PRODUCTION # 演出密度探针(样张 v2 冻结批 T3——演出四项可触发+探针断言覆盖): # ①死亡碎裂(standee mark_dead → 碎块计数)②意图三档动画(damage/buff/debuff/charge 摇定不炸+徽标可见) # ③胜利纹章+失败掸灰+旗光收回(battle_end open_victory/open_defeat → fx_probe 计数) # ④结算轮光带(settlement_fx play_round_sweep → 子节点落位)。 # 运行:Godot_console.exe --headless --path . res://smoke/fx_probe.tscn extends Node func _ready() -> void: await get_tree().process_frame var problems: Array = [] # ---- ①② 站桩侧:死亡碎裂 + 意图三档 ---- var sb_script := load("res://presentation/views/standee_body.gd") var sb = sb_script.new("enemy") sb.box = Vector2(150, 176) # 站桩盒几何(Node2D 自定义字段——非 Control.size) add_child(sb) sb.set_data({"display_name": "地精", "enemy_id": "goblin", "hp": 10, "max_hp": 10}) await get_tree().process_frame # 意图三档:特殊准备(damage/buff/debuff)与蓄力档(heavy)摇定——动画触发+徽标可见 for tier in [["damage", "slash"], ["buff", "atk_up"], ["debuff", "weaken_hit"], ["damage", "heavy"]]: sb.set_pending(tier[0], tier[1]) if not sb.intent_probe().visible: problems.append("意图徽标应可见(%s/%s)" % [tier[0], tier[1]]) await get_tree().process_frame sb.set_pending("basic", "") # 档①普通攻击=零表示 if sb.intent_probe().visible: problems.append("普通攻击徽标应零表示(§4.6.4 档①)") # 死亡碎裂 sb.mark_dead() await get_tree().process_frame var dp: Dictionary = sb.fx_probe() if int(dp.get("death_shards", 0)) != 5: problems.append("死亡碎块应 5 块(§8.4 冻结值),实得 %s" % [dp.get("death_shards")]) if not sb.is_dead(): problems.append("mark_dead 后 is_dead 应真") # ①b 命中火花(迭代 6 T4 终稿——打击瞬间特效计数) var sb2 = sb_script.new("enemy") sb2.box = Vector2(150, 176) add_child(sb2) sb2.play_hit_impact() await get_tree().process_frame var hp2: Dictionary = sb2.fx_probe() if int(hp2.get("hit_sparks", 0)) != 6: problems.append("命中火花应 6 枚(迭代 6 T4),实得 %s" % [hp2.get("hit_sparks")]) # ---- ③ 终局侧:胜利纹章 / 失败掸灰+旗光 ---- var end_ov := BattleEndOverlay.new() add_child(end_ov) var br := Rect2(300, 300, 400, 300) end_ov.open_victory("胜利", "继续", br) await get_tree().process_frame var vp: Dictionary = end_ov.fx_probe() if int(vp.get("crest_shards", 0)) != 10: problems.append("胜利纹章碎砖应 10 枚(§2.4),实得 %s" % [vp.get("crest_shards")]) end_ov.close() end_ov.open_defeat("失败", "重来", br) await get_tree().process_frame var fp: Dictionary = end_ov.fx_probe() if int(fp.get("dust_motes", 0)) != 8: problems.append("失败掸灰尘絮应 8 枚(§2.5),实得 %s" % [fp.get("dust_motes")]) if int(fp.get("flag_dots", 0)) != 8: problems.append("旗光收回应 8 点,实得 %s" % [fp.get("flag_dots")]) end_ov.close() # ---- ④ 结算轮光带(含前导亮缘) ---- var sfx := SettlementFxView.new() add_child(sfx) sfx.play_round_sweep(br) # 光带几何全由 board_rect 驱动——无需容器尺寸 await get_tree().process_frame var n_fx := 0 for ch in sfx.get_children(): if ch is ColorRect: n_fx += 1 if n_fx < 2: problems.append("结算光带应含带体+前导缘线 ≥2 节点,实得 %d" % n_fx) # ---- ④b 轮切换横幅双态(迭代 8 卡⑦——常态=淡入驻留消隐动画链;减动效=静态呈现零动画)---- sfx.play_round_banner(3) await get_tree().process_frame var banner_nodes := 0 for ch in sfx.get_children(): if ch is Panel: banner_nodes += 1 if banner_nodes != 1: problems.append("轮横幅应落位 1 面(卡⑦),实得 %d" % banner_nodes) if int(sfx.probe_banners()) != 1: problems.append("轮横幅计数失守(probe_banners=%s)" % sfx.probe_banners()) var saved_motion: float = UiKit.motion_scale UiKit.motion_scale = 0.5 sfx.play_round_banner(4) await get_tree().process_frame if int(sfx.probe_banners()) != 2: problems.append("减动效轮横幅未发射(卡⑦ AC③——静态化仍呈现不跳过)") UiKit.motion_scale = saved_motion # ---- ④c 逐条结算标出(三轮 #3——状态脉冲+伤害数字浮现断言面) ---- sfx.play_status_pulse(Vector2(120, 120), Color("7cc47c")) sfx.play_damage_number(Vector2(120, 120), "-7", Color("d9402a"), false) await get_tree().process_frame await get_tree().process_frame if int(sfx.probe_settlement_marks()) != 2: problems.append("逐条结算标出未落位(状态脉冲+数字浮现——三轮 #3),实得 %s" % sfx.probe_settlement_marks()) # ---- ⑤ 主角三态切换(迭代 6 T3——attack/hit 短驻自回 + 死亡复活态回 idle) ---- var hero_sb = sb_script.new("hero") hero_sb.box = Vector2(150, 176) add_child(hero_sb) var t_idle: Texture2D = UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) var t_atk: Texture2D = UiKit.load_tex(UiKit.TEX_HERO_ATTACK) var t_hit: Texture2D = UiKit.load_tex(UiKit.TEX_HERO_HIT) if t_idle == null or t_atk == null or t_hit == null: problems.append("主角三态素材缺位(portrait/attack/hit——迭代 6 T3)") else: hero_sb.set_portrait_texture(t_idle, true) hero_sb.register_hero_state("idle", t_idle) hero_sb.register_hero_state("attack", t_atk) hero_sb.register_hero_state("hit", t_hit) hero_sb.set_hero_state("attack") if String(hero_sb.hero_state_probe().get("state", "")) != "attack": problems.append("主角攻击态切换失败(set_hero_state attack)") await get_tree().create_timer(UiKit.motion(0.5)).timeout if String(hero_sb.hero_state_probe().get("state", "")) != "idle": problems.append("attack 态未短驻自回 idle(0.35s)") hero_sb.set_hero_state("hit") if String(hero_sb.hero_state_probe().get("state", "")) != "hit": problems.append("主角受击态切换失败(set_hero_state hit)") hero_sb.set_hero_state("idle") hero_sb.mark_dead() hero_sb.set_hero(6, 10) # 新战斗开场 HP 回正——死亡残位复活(敌侧 set_data 同款语义) if hero_sb.is_dead() or String(hero_sb.hero_state_probe().get("state", "")) != "idle": problems.append("主角死亡后 set_hero 复活失败(is_dead 应假+态回 idle)") # ---- ⑥ 主角受击/死亡路由(迭代 6 T5——battle_stage 主角早退拆除的呈现链断言) ---- var db2 = load("res://core/config_db.gd").new() if db2.load_tables("res://data/tables", false): var stage2 = BattleStageView.new(db2) add_child(stage2) var flash_before: int = int(stage2.hero_standee.fx_probe().get("flash_count", 0)) stage2.on_step_row_fx({"type": "damage", "target": "hero", "killed": false}) await get_tree().create_timer(UiKit.motion(0.45)).timeout # >到达拍 0.33s var hfx: Dictionary = stage2.hero_standee.fx_probe() if int(hfx.get("flash_count", 0)) <= flash_before: problems.append("主角受击闪白未触发(on_step_row_fx hero 路由——迭代 6 T5)") if int(hfx.get("hit_sparks", 0)) < 6: problems.append("主角受击火花未触发(演出对等——迭代 6 T5)") stage2.on_step_row_fx({"type": "damage", "target": "hero", "killed": true}) await get_tree().create_timer(UiKit.motion(0.45)).timeout if not stage2.hero_standee.is_dead(): problems.append("主角击杀塌陷未触发(killed 行——迭代 6 T5)") stage2.queue_free() else: problems.append("fx_probe⑥ 表加载失败(config_db)") # ---- 汇总 ---- if problems.is_empty(): print("[fx_probe] PASS — 死亡碎裂5块/命中火花6/意图三档+零表示/胜利纹章10/失败掸灰8+旗光8/结算光带+缘线/轮横幅双态/逐条结算标出/主角三态切换 全链路通过") get_tree().quit(0) else: for p in problems: print("[fx_probe] FAIL — " + p) print("[fx_probe] FAIL — %d 项断言未过" % problems.size()) get_tree().quit(1)