From c4d6a0c1cd6715a0729ee6dba456dc5d7b0893e4 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 18 Jun 2026 19:47:38 +0900 Subject: [PATCH] Enhance class attack VFX signatures --- scripts/scenes/battle_scene.gd | 93 +++++++++++++++++++++++++++++++++- tools/smoke_visual_assets.gd | 44 ++++++++++++++-- 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index ba0cb27..63ac83a 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -1620,7 +1620,11 @@ func _draw_attack_effects() -> void: var unit := state.get_unit(str(unit_id)) if not unit.is_empty() and unit.get("team", "") == BattleState.TEAM_ENEMY: team_color = ENEMY_COLOR - _draw_action_effect(str(motion.get("profile", "slash")), start, end, progress, peak, team_color) + var profile := str(motion.get("profile", "slash")) + var impact_cue := str(motion.get("impact_cue", _action_motion_impact_cue(profile))) + var impact_radius := float(motion.get("impact_radius", _action_motion_impact_radius(profile))) + _draw_action_effect(profile, start, end, progress, peak, team_color) + _draw_action_impact_cue(impact_cue, start, end, progress, peak, team_color, impact_radius) func _draw_action_effect(profile: String, start: Vector2, end: Vector2, progress: float, peak: float, team_color: Color) -> void: @@ -1710,6 +1714,48 @@ func _draw_slash_attack_effect(end: Vector2, peak: float, team_color: Color) -> draw_circle(end, 8.0 + 8.0 * peak, Color(1.0, 0.82, 0.34, 0.20 * peak)) +func _draw_action_impact_cue(cue: String, start: Vector2, end: Vector2, progress: float, peak: float, team_color: Color, radius: float) -> void: + var direction := end - start + if direction.length() <= 0.01: + direction = Vector2.RIGHT + else: + direction = direction.normalized() + var side := Vector2(-direction.y, direction.x) + var accent := team_color.lightened(0.55) + accent.a = 0.34 * peak + var hot := Color(1.0, 0.90, 0.42, 0.42 * peak) + if cue == "projectile": + draw_circle(end, 3.5 + radius * 0.22 * peak, Color(1.0, 0.96, 0.62, 0.28 * peak)) + draw_line(end - direction * radius * 0.55, end + direction * radius * 0.30, hot, 2.0) + draw_line(end - side * radius * 0.22, end + side * radius * 0.22, Color(0.92, 0.96, 1.0, 0.26 * peak), 1.5) + elif cue == "charge": + for index in range(3): + var offset := side * float(index - 1) * radius * 0.34 + draw_circle(end - direction * radius * (0.34 + float(index) * 0.10) + offset, 3.0 + radius * 0.12 * peak, Color(0.76, 0.58, 0.30, 0.18 * peak)) + draw_line(end - side * radius * 0.70, end + side * radius * 0.70, hot, 2.5) + draw_arc(end, radius * (0.58 + progress * 0.20), -PI * 0.12, PI * 1.12, 32, accent, 2.0) + elif cue == "guard": + draw_rect(Rect2(end - Vector2(radius * 0.32, radius * 0.42), Vector2(radius * 0.64, radius * 0.84)), Color(0.80, 0.90, 1.0, 0.14 * peak), false, 2.0) + draw_line(end - side * radius * 0.52 - direction * radius * 0.22, end + side * radius * 0.52 + direction * radius * 0.22, hot, 2.0) + draw_line(end - direction * radius * 0.48, end + direction * radius * 0.48, accent, 2.0) + elif cue == "command": + draw_arc(end, radius * (0.72 + 0.20 * peak), progress * TAU, progress * TAU + PI * 1.20, 40, Color(0.72, 0.94, 1.0, 0.34 * peak), 2.0) + draw_arc(end, radius * 0.44, -progress * TAU, -progress * TAU + PI * 1.55, 40, hot, 2.0) + for index in range(3): + var angle := progress * TAU + float(index) * TAU / 3.0 + var ray := Vector2(cos(angle), sin(angle)) + draw_line(end + ray * radius * 0.18, end + ray * radius * 0.58, accent, 1.5) + elif cue == "crush": + draw_arc(end, radius * (0.42 + peak * 0.42), 0.0, TAU, 48, Color(1.0, 0.68, 0.24, 0.30 * peak), 3.0) + for index in range(4): + var angle := float(index) * PI * 0.5 + progress * PI * 0.25 + var crack := Vector2(cos(angle), sin(angle)) + draw_line(end + crack * radius * 0.16, end + crack * radius * (0.54 + 0.16 * peak), Color(0.96, 0.82, 0.42, 0.34 * peak), 2.0) + else: + draw_line(end - side * radius * 0.48, end + side * radius * 0.48, hot, 2.0) + draw_circle(end, radius * 0.32 + radius * 0.18 * peak, Color(1.0, 0.82, 0.34, 0.18 * peak)) + + func _draw_floating_texts() -> void: if floating_texts.is_empty(): return @@ -2638,6 +2684,9 @@ func _on_unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_c "profile": profile, "attack_family": profile, "style": _action_motion_style(profile), + "vfx_signature": _action_motion_vfx_signature(profile), + "impact_cue": _action_motion_impact_cue(profile), + "impact_radius": _action_motion_impact_radius(profile), "age": 0.0, "duration": _action_motion_duration(profile) } @@ -2696,6 +2745,48 @@ func _action_motion_lunge_scale(profile: String) -> float: return 1.0 +func _action_motion_vfx_signature(profile: String) -> String: + if profile == "arrow": + return "piercing_trace" + if profile == "cavalry_charge": + return "charge_dust" + if profile == "infantry_strike": + return "shield_spark" + if profile == "command_strike": + return "command_ring" + if profile == "heavy_strike": + return "shock_crack" + return "slash_flash" + + +func _action_motion_impact_cue(profile: String) -> String: + if profile == "arrow": + return "projectile" + if profile == "cavalry_charge": + return "charge" + if profile == "infantry_strike": + return "guard" + if profile == "command_strike": + return "command" + if profile == "heavy_strike": + return "crush" + return "slash" + + +func _action_motion_impact_radius(profile: String) -> float: + if profile == "arrow": + return 14.0 + if profile == "cavalry_charge": + return 25.0 + if profile == "infantry_strike": + return 18.0 + if profile == "command_strike": + return 24.0 + if profile == "heavy_strike": + return 28.0 + return 16.0 + + func _action_motion_sfx(profile: String) -> String: if profile == "arrow": return "bow_release" diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index ff49c01..4dad7a0 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -233,13 +233,23 @@ func _check_attack_motion_profiles(failures: Array[String]) -> void: _check_synthetic_attack_profile(failures, scene, "champion", "heavy_strike") _check_synthetic_attack_profile(failures, scene, "bandit", "heavy_strike") scene._on_unit_action_motion_requested("xiahou_dun", Vector2i(1, 4), Vector2i(2, 4), "attack") - _check_stored_attack_motion(failures, scene, "xiahou_dun", "cavalry_charge", "melee", "attack") + _check_stored_attack_motion(failures, scene, "xiahou_dun", "cavalry_charge", "melee", "attack", "charge_dust") scene._on_unit_action_motion_requested("yellow_turban_3", Vector2i(7, 6), Vector2i(7, 4), "attack") - _check_stored_attack_motion(failures, scene, "yellow_turban_3", "arrow", "ranged", "attack") + _check_stored_attack_motion(failures, scene, "yellow_turban_3", "arrow", "ranged", "attack", "piercing_trace") + _check_attack_vfx_signature(failures, scene, "arrow", "piercing_trace") + _check_attack_vfx_signature(failures, scene, "cavalry_charge", "charge_dust") + _check_attack_vfx_signature(failures, scene, "infantry_strike", "shield_spark") + _check_attack_vfx_signature(failures, scene, "command_strike", "command_ring") + _check_attack_vfx_signature(failures, scene, "heavy_strike", "shock_crack") + _check_unique_attack_vfx_signatures(failures, scene) if scene._action_motion_duration("cavalry_charge") <= scene._action_motion_duration("slash"): failures.append("cavalry charge animation should last longer than default slash") if scene._action_motion_lunge_scale("arrow") >= scene._action_motion_lunge_scale("slash"): failures.append("arrow animation should keep the archer mostly planted") + if scene._action_motion_impact_radius("heavy_strike") <= scene._action_motion_impact_radius("infantry_strike"): + failures.append("heavy strike impact radius should exceed infantry strike") + if scene._action_motion_impact_radius("cavalry_charge") <= scene._action_motion_impact_radius("arrow"): + failures.append("cavalry charge impact radius should exceed arrow impact") if scene._action_motion_sfx("arrow") != "bow_release": failures.append("arrow animation should use bow SFX") if scene._action_motion_sfx("infantry_strike") != "slash": @@ -269,7 +279,28 @@ func _check_synthetic_attack_profile(failures: Array[String], scene, class_id: S failures.append("%s synthetic attack profile mismatch: expected %s got %s" % [class_id, expected_profile, profile]) -func _check_stored_attack_motion(failures: Array[String], scene, unit_id: String, expected_profile: String, expected_style: String, expected_kind: String) -> void: +func _check_attack_vfx_signature(failures: Array[String], scene, profile: String, expected_signature: String) -> void: + var signature: String = scene._action_motion_vfx_signature(profile) + if signature != expected_signature: + failures.append("%s VFX signature mismatch: expected %s got %s" % [profile, expected_signature, signature]) + if str(scene._action_motion_impact_cue(profile)).is_empty(): + failures.append("%s should expose an impact cue" % profile) + if float(scene._action_motion_impact_radius(profile)) <= 0.0: + failures.append("%s should expose a positive impact radius" % profile) + + +func _check_unique_attack_vfx_signatures(failures: Array[String], scene) -> void: + var seen := {} + for profile in ["arrow", "cavalry_charge", "infantry_strike", "command_strike", "heavy_strike"]: + var signature: String = scene._action_motion_vfx_signature(profile) + if seen.has(signature): + failures.append("attack VFX signature should be unique: %s" % signature) + seen[signature] = true + if scene._action_motion_vfx_signature("slash") == scene._action_motion_vfx_signature("heavy_strike"): + failures.append("default slash should not share heavy strike VFX signature") + + +func _check_stored_attack_motion(failures: Array[String], scene, unit_id: String, expected_profile: String, expected_style: String, expected_kind: String, expected_vfx_signature: String) -> void: if not scene.unit_action_motion_by_unit.has(unit_id): failures.append("%s should store an action motion" % unit_id) return @@ -278,12 +309,19 @@ func _check_stored_attack_motion(failures: Array[String], scene, unit_id: String var family := str(motion.get("attack_family", "")) var style := str(motion.get("style", "")) var kind := str(motion.get("kind", "")) + var signature := str(motion.get("vfx_signature", "")) if profile != expected_profile or family != expected_profile: failures.append("%s stored motion profile mismatch: %s" % [unit_id, str(motion)]) if style != expected_style: failures.append("%s stored motion style mismatch: %s" % [unit_id, str(motion)]) if kind != expected_kind: failures.append("%s stored motion kind mismatch: %s" % [unit_id, str(motion)]) + if signature != expected_vfx_signature: + failures.append("%s stored motion VFX signature mismatch: %s" % [unit_id, str(motion)]) + if str(motion.get("impact_cue", "")).is_empty(): + failures.append("%s stored motion should include an impact cue: %s" % [unit_id, str(motion)]) + if float(motion.get("impact_radius", 0.0)) <= 0.0: + failures.append("%s stored motion should include a positive impact radius: %s" % [unit_id, str(motion)]) if float(motion.get("duration", 0.0)) <= 0.0: failures.append("%s stored motion duration should be positive: %s" % [unit_id, str(motion)])