diff --git a/game/presentation/views/battle_stage_view.gd b/game/presentation/views/battle_stage_view.gd index e792d188..aa89d201 100644 --- a/game/presentation/views/battle_stage_view.gd +++ b/game/presentation/views/battle_stage_view.gd @@ -291,9 +291,21 @@ func on_step_row_fx(row: Dictionary) -> void: var card: Node2D = _card_by_name(target) if card == null: return - card.flash_hit() - if bool(row.get("killed", false)): - card.mark_dead() + # 弹道到达拍对齐(迭代 6 T4 终稿):闪白/打击火花/击杀塌陷延迟 ≈弹道飞行时长 + # (play_projectile 0.16+0.19)——呈现链序=弹道→闪白+火花→血条(投影刷新)→灰化塌陷。 + # 行突发同拍多定时器互不干扰(flash 重启语义/死亡幂等守卫);卡片提前释放=守卫跳过; + # 树外(防御)=即时执行不调度。 + var arrived := func(): + if not is_instance_valid(card): + return + card.play_hit_impact() + card.flash_hit() + if bool(row.get("killed", false)): + card.mark_dead() + if get_tree() != null: + get_tree().create_timer(UiKit.motion(0.33)).timeout.connect(arrived) + else: + arrived.call() func _card_by_name(display_name: String) -> Node2D: diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index 2ea2d6c0..748433c5 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -261,7 +261,9 @@ func mark_dead() -> void: # ---- 演出密度终稿(样张 v2 冻结批 T3——程序化 tween,零新帧资产) ---- const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值) +const HIT_SPARK_N := 6 # 命中火花数(迭代 6 T4 终稿——打击瞬间可读性一拍) var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数) +var _hit_sparks := 0 # 探针出口计数(命中火花累计) # 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机) @@ -285,9 +287,30 @@ func _spawn_death_shards() -> void: stw.tween_callback(shard.queue_free) -# 演出探针出口(fx_probe——死亡碎裂断言面) +# 演出探针出口(fx_probe——死亡碎裂/命中火花断言面) func fx_probe() -> Dictionary: - return {"death_shards": _death_shards} + return {"death_shards": _death_shards, "hit_sparks": _hit_sparks} + + +# 命中瞬间打击特效(迭代 6 T4 终稿):立绘盒中心六枚短火花放射(程序化色块,确定性方向 +# i*60°——禁全局随机;亮金/暖白交替=物理打击语义,不占 危险红=雷 与 奖励金=收益 的语义位)。 +# 弹道到达拍由 battle_stage_view 调度(on_step_row_fx 延迟 0.33s≈弹道飞行)。 +func play_hit_impact() -> void: + var center := _portrait_rect().get_center() + for i in HIT_SPARK_N: + _hit_sparks += 1 + var spark := ColorRect.new() + spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM + spark.size = Vector2(4, 3) + spark.mouse_filter = Control.MOUSE_FILTER_IGNORE + spark.position = center - spark.size * 0.5 + add_child(spark) + var ang := TAU * float(i) / float(HIT_SPARK_N) + var dir := Vector2(cos(ang), sin(ang)) * 16.0 + var stw := spark.create_tween() + stw.tween_property(spark, "position", spark.position + dir, UiKit.motion(0.12)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + stw.parallel().tween_property(spark, "modulate:a", 0.0, UiKit.motion(0.12)) + stw.tween_callback(spark.queue_free) func is_dead() -> bool: diff --git a/game/smoke/fx_probe.gd b/game/smoke/fx_probe.gd index 65e3428c..47ceb5fd 100644 --- a/game/smoke/fx_probe.gd +++ b/game/smoke/fx_probe.gd @@ -36,6 +36,16 @@ func _ready() -> void: 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) @@ -98,7 +108,7 @@ func _ready() -> void: # ---- 汇总 ---- if problems.is_empty(): - print("[fx_probe] PASS — 死亡碎裂5块/意图三档+零表示/胜利纹章10/失败掸灰8+旗光8/结算光带+缘线/主角三态切换 全链路通过") + print("[fx_probe] PASS — 死亡碎裂5块/命中火花6/意图三档+零表示/胜利纹章10/失败掸灰8+旗光8/结算光带+缘线/主角三态切换 全链路通过") get_tree().quit(0) else: for p in problems: