Add tactic cast motion effects

This commit is contained in:
2026-06-18 22:34:39 +09:00
parent 3273ec33ec
commit 70a34cf262
4 changed files with 168 additions and 1 deletions

View File

@@ -122,6 +122,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
- Floating combat text for damage, recovery, misses, support effects, poison ticks, action locks, level-ups, and promotions. - Floating combat text for damage, recovery, misses, support effects, poison ticks, action locks, level-ups, and promotions.
- Short unit slide animation for board movement. - Short unit slide animation for board movement.
- Board attacks now request class-specific motion profiles, with infantry strikes, archer traces, cavalry charges, command flashes, and heavy warrior/bandit impacts drawn by the battle scene. - Board attacks now request class-specific motion profiles, with infantry strikes, archer traces, cavalry charges, command flashes, and heavy warrior/bandit impacts drawn by the battle scene.
- Damage, healing, and support tactics now request distinct cast motion profiles with flare, sigil, and banner-style battlefield effects.
- Battle maps can use a high-resolution background image with translucent terrain overlays, terrain-specific detail patterns, connected roads, shoreline/castle/forest/hill edge blends, transparent generic-enemy class art, and token marks. - Battle maps can use a high-resolution background image with translucent terrain overlays, terrain-specific detail patterns, connected roads, shoreline/castle/forest/hill edge blends, transparent generic-enemy class art, and token marks.
- Hover target preview badges for attacks, tactics, and items. - Hover target preview badges for attacks, tactics, and items.
- Low-HP board unit warning rings, HP bar color states, and board status pips plus HUD duration summaries for support, debuff, poison, seal, snare, and disarm effects. - Low-HP board unit warning rings, HP bar color states, and board status pips plus HUD duration summaries for support, debuff, poison, seal, snare, and disarm effects.

View File

@@ -754,6 +754,7 @@ func try_cast_selected_skill(skill_id: String, target_cell: Vector2i) -> bool:
if skill_kind == "support" and not _skill_has_support_effects(skill): if skill_kind == "support" and not _skill_has_support_effects(skill):
return false return false
_emit_skill_motion(caster, target_cell, skill_kind)
caster["mp"] = max(0, int(caster.get("mp", 0)) - int(skill.get("mp_cost", 0))) caster["mp"] = max(0, int(caster.get("mp", 0)) - int(skill.get("mp_cost", 0)))
if skill_kind == "heal": if skill_kind == "heal":
var total_healed := 0 var total_healed := 0
@@ -3153,6 +3154,7 @@ func _execute_ai_skill_action(caster: Dictionary, action: Dictionary) -> bool:
if skill_kind == "support" and not _skill_has_support_effects(skill): if skill_kind == "support" and not _skill_has_support_effects(skill):
return false return false
_emit_skill_motion(caster, target_cell, skill_kind)
caster["mp"] = max(0, int(caster.get("mp", 0)) - int(skill.get("mp_cost", 0))) caster["mp"] = max(0, int(caster.get("mp", 0)) - int(skill.get("mp_cost", 0)))
if skill_kind == "heal": if skill_kind == "heal":
var total_healed := 0 var total_healed := 0
@@ -3267,6 +3269,17 @@ func _resolve_skill_damage(caster: Dictionary, target: Dictionary, skill: Dictio
return false return false
func _emit_skill_motion(caster: Dictionary, target_cell: Vector2i, skill_kind: String) -> void:
if caster.is_empty() or not is_inside(target_cell):
return
unit_action_motion_requested.emit(
str(caster.get("id", "")),
caster.get("pos", Vector2i(-1, -1)),
target_cell,
"skill_%s" % skill_kind
)
func _handle_unit_defeated(unit: Dictionary) -> void: func _handle_unit_defeated(unit: Dictionary) -> void:
if unit.is_empty(): if unit.is_empty():
return return

View File

@@ -52,6 +52,7 @@ const UNIT_CAVALRY_ATTACK_DURATION := 0.42
const UNIT_ARROW_ATTACK_DURATION := 0.36 const UNIT_ARROW_ATTACK_DURATION := 0.36
const UNIT_COMMAND_ATTACK_DURATION := 0.38 const UNIT_COMMAND_ATTACK_DURATION := 0.38
const UNIT_HEAVY_ATTACK_DURATION := 0.34 const UNIT_HEAVY_ATTACK_DURATION := 0.34
const UNIT_TACTIC_CAST_DURATION := 0.42
const DIALOGUE_PANEL_POSITION := Vector2(96, 448) const DIALOGUE_PANEL_POSITION := Vector2(96, 448)
const DIALOGUE_PANEL_SIZE := Vector2(1040, 210) const DIALOGUE_PANEL_SIZE := Vector2(1040, 210)
const DIALOGUE_PORTRAIT_SIZE := Vector2(164, 186) const DIALOGUE_PORTRAIT_SIZE := Vector2(164, 186)
@@ -2051,6 +2052,12 @@ func _draw_action_effect(profile: String, start: Vector2, end: Vector2, progress
_draw_command_attack_effect(start, end, progress, peak, team_color) _draw_command_attack_effect(start, end, progress, peak, team_color)
elif profile == "heavy_strike": elif profile == "heavy_strike":
_draw_heavy_attack_effect(end, peak, team_color) _draw_heavy_attack_effect(end, peak, team_color)
elif profile == "tactic_damage":
_draw_tactic_damage_effect(start, end, progress, peak, team_color)
elif profile == "tactic_heal":
_draw_tactic_heal_effect(start, end, progress, peak, team_color)
elif profile == "tactic_support":
_draw_tactic_support_effect(start, end, progress, peak, team_color)
else: else:
_draw_slash_attack_effect(end, peak, team_color) _draw_slash_attack_effect(end, peak, team_color)
@@ -2119,6 +2126,47 @@ func _draw_heavy_attack_effect(end: Vector2, peak: float, team_color: Color) ->
draw_arc(end, 10.0 + 18.0 * peak, 0.0, TAU, 48, Color(1.0, 0.68, 0.24, 0.26 * peak), 3.0) draw_arc(end, 10.0 + 18.0 * peak, 0.0, TAU, 48, Color(1.0, 0.68, 0.24, 0.26 * peak), 3.0)
func _draw_tactic_damage_effect(start: Vector2, end: Vector2, progress: float, peak: float, team_color: Color) -> void:
var caster_color := team_color.lightened(0.58)
caster_color.a = 0.34 * peak
var fire_color := Color(1.0, 0.38, 0.18, 0.52 * peak)
draw_arc(start, 12.0 + 7.0 * peak, progress * TAU, progress * TAU + PI * 1.55, 36, caster_color, 2.0)
draw_line(start, end, Color(1.0, 0.62, 0.28, 0.30 * peak), 2.0)
for index in range(3):
var angle := progress * TAU + float(index) * TAU / 3.0
var spark := Vector2(cos(angle), sin(angle))
draw_line(end - spark * (5.0 + 4.0 * peak), end + spark * (12.0 + 6.0 * peak), fire_color, 2.0)
draw_circle(end, 6.0 + 10.0 * peak, Color(1.0, 0.50, 0.18, 0.18 * peak))
func _draw_tactic_heal_effect(start: Vector2, end: Vector2, progress: float, peak: float, team_color: Color) -> void:
var line_color := Color(0.48, 1.0, 0.60, 0.24 * peak)
if start.distance_to(end) > 0.01:
draw_line(start, end, line_color, 2.0)
var ring_color := Color(0.42, 1.0, 0.54, 0.42 * peak)
draw_arc(end, 9.0 + 12.0 * peak, -progress * TAU, -progress * TAU + PI * 1.70, 44, ring_color, 2.0)
draw_arc(end, 19.0 - 5.0 * peak, progress * TAU, progress * TAU + PI * 1.25, 40, Color(0.88, 1.0, 0.72, 0.32 * peak), 2.0)
draw_line(end + Vector2(-7, 0), end + Vector2(7, 0), Color(0.80, 1.0, 0.78, 0.54 * peak), 2.5)
draw_line(end + Vector2(0, -7), end + Vector2(0, 7), Color(0.80, 1.0, 0.78, 0.54 * peak), 2.5)
var caster_glow := team_color.lightened(0.62)
caster_glow.a = 0.20 * peak
draw_circle(start, 6.0 + 4.0 * peak, caster_glow)
func _draw_tactic_support_effect(start: Vector2, end: Vector2, progress: float, peak: float, team_color: Color) -> void:
var banner := team_color.lightened(0.48)
banner.a = 0.46 * peak
if start.distance_to(end) > 0.01:
draw_line(start, end, Color(1.0, 0.86, 0.30, 0.20 * peak), 1.5)
draw_line(end + Vector2(-10, -16), end + Vector2(-10, 12), banner, 2.5)
draw_rect(Rect2(end + Vector2(-8, -15), Vector2(20, 12)), Color(1.0, 0.82, 0.22, 0.28 * peak))
draw_arc(end, 12.0 + 8.0 * peak, progress * TAU, progress * TAU + PI * 1.45, 36, Color(1.0, 0.92, 0.38, 0.36 * peak), 2.0)
for index in range(3):
var angle := -PI * 0.5 + float(index - 1) * 0.52
var ray := Vector2(cos(angle), sin(angle))
draw_line(end + ray * 7.0, end + ray * (18.0 + 5.0 * peak), Color(1.0, 0.96, 0.62, 0.25 * peak), 1.5)
func _draw_slash_attack_effect(end: Vector2, peak: float, team_color: Color) -> void: func _draw_slash_attack_effect(end: Vector2, peak: float, team_color: Color) -> void:
var flash := team_color.lightened(0.45) var flash := team_color.lightened(0.45)
flash.a = 0.55 * peak flash.a = 0.55 * peak
@@ -2164,6 +2212,15 @@ func _draw_action_impact_cue(cue: String, start: Vector2, end: Vector2, progress
var angle := float(index) * PI * 0.5 + progress * PI * 0.25 var angle := float(index) * PI * 0.5 + progress * PI * 0.25
var crack := Vector2(cos(angle), sin(angle)) 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) 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)
elif cue == "tactic_damage":
draw_arc(end, radius * (0.35 + peak * 0.38), 0.0, TAU, 44, Color(1.0, 0.34, 0.18, 0.28 * peak), 3.0)
draw_arc(end, radius * 0.62, progress * TAU, progress * TAU + PI * 1.20, 36, Color(1.0, 0.82, 0.32, 0.34 * peak), 2.0)
elif cue == "tactic_heal":
draw_arc(end, radius * (0.40 + peak * 0.24), 0.0, TAU, 48, Color(0.48, 1.0, 0.54, 0.30 * peak), 2.5)
draw_circle(end, radius * 0.14 + radius * 0.10 * peak, Color(0.78, 1.0, 0.74, 0.22 * peak))
elif cue == "tactic_support":
draw_arc(end, radius * (0.48 + peak * 0.18), -PI * 0.10, PI * 1.10, 40, Color(1.0, 0.88, 0.28, 0.30 * peak), 2.5)
draw_line(end + Vector2(-radius * 0.38, radius * 0.26), end + Vector2(radius * 0.38, radius * 0.26), Color(1.0, 0.94, 0.50, 0.30 * peak), 2.0)
else: else:
draw_line(end - side * radius * 0.48, end + side * radius * 0.48, hot, 2.0) 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)) draw_circle(end, radius * 0.32 + radius * 0.18 * peak, Color(1.0, 0.82, 0.34, 0.18 * peak))
@@ -3219,10 +3276,12 @@ func _on_unit_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Ve
func _on_unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Vector2i, action_kind: String) -> void: func _on_unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Vector2i, action_kind: String) -> void:
if unit_id.is_empty() or from_cell == to_cell: if unit_id.is_empty():
return return
var unit := state.get_unit(unit_id) var unit := state.get_unit(unit_id)
var profile := _unit_action_motion_profile(unit, from_cell, to_cell, action_kind) var profile := _unit_action_motion_profile(unit, from_cell, to_cell, action_kind)
if from_cell == to_cell and not _action_motion_allows_same_cell(profile):
return
unit_action_motion_by_unit[unit_id] = { unit_action_motion_by_unit[unit_id] = {
"from": from_cell, "from": from_cell,
"to": to_cell, "to": to_cell,
@@ -3242,6 +3301,12 @@ func _on_unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_c
func _unit_action_motion_profile(unit: Dictionary, from_cell: Vector2i, to_cell: Vector2i, _action_kind: String) -> String: func _unit_action_motion_profile(unit: Dictionary, from_cell: Vector2i, to_cell: Vector2i, _action_kind: String) -> String:
if _action_kind == "skill_heal":
return "tactic_heal"
if _action_kind == "skill_support":
return "tactic_support"
if _action_kind.begins_with("skill_"):
return "tactic_damage"
var class_id := str(unit.get("class_id", "")) var class_id := str(unit.get("class_id", ""))
var distance := absi(from_cell.x - to_cell.x) + absi(from_cell.y - to_cell.y) var distance := absi(from_cell.x - to_cell.x) + absi(from_cell.y - to_cell.y)
if class_id.contains("archer") or class_id.contains("marksman") or (int(unit.get("range", 1)) > 1 and distance > 1): if class_id.contains("archer") or class_id.contains("marksman") or (int(unit.get("range", 1)) > 1 and distance > 1):
@@ -3260,6 +3325,8 @@ func _unit_action_motion_profile(unit: Dictionary, from_cell: Vector2i, to_cell:
func _action_motion_style(profile: String) -> String: func _action_motion_style(profile: String) -> String:
if profile == "arrow": if profile == "arrow":
return "ranged" return "ranged"
if profile.begins_with("tactic_"):
return "tactic"
return "melee" return "melee"
@@ -3274,6 +3341,8 @@ func _action_motion_duration(profile: String) -> float:
return UNIT_COMMAND_ATTACK_DURATION return UNIT_COMMAND_ATTACK_DURATION
if profile == "heavy_strike": if profile == "heavy_strike":
return UNIT_HEAVY_ATTACK_DURATION return UNIT_HEAVY_ATTACK_DURATION
if profile.begins_with("tactic_"):
return UNIT_TACTIC_CAST_DURATION
return UNIT_ATTACK_ANIMATION_DURATION return UNIT_ATTACK_ANIMATION_DURATION
@@ -3288,9 +3357,15 @@ func _action_motion_lunge_scale(profile: String) -> float:
return 0.20 return 0.20
if profile == "heavy_strike": if profile == "heavy_strike":
return 1.15 return 1.15
if profile.begins_with("tactic_"):
return 0.12
return 1.0 return 1.0
func _action_motion_allows_same_cell(profile: String) -> bool:
return profile.begins_with("tactic_")
func _action_motion_vfx_signature(profile: String) -> String: func _action_motion_vfx_signature(profile: String) -> String:
if profile == "arrow": if profile == "arrow":
return "piercing_trace" return "piercing_trace"
@@ -3302,6 +3377,12 @@ func _action_motion_vfx_signature(profile: String) -> String:
return "command_ring" return "command_ring"
if profile == "heavy_strike": if profile == "heavy_strike":
return "shock_crack" return "shock_crack"
if profile == "tactic_damage":
return "tactic_flare"
if profile == "tactic_heal":
return "healing_sigil"
if profile == "tactic_support":
return "order_banner"
return "slash_flash" return "slash_flash"
@@ -3316,6 +3397,12 @@ func _action_motion_impact_cue(profile: String) -> String:
return "command" return "command"
if profile == "heavy_strike": if profile == "heavy_strike":
return "crush" return "crush"
if profile == "tactic_damage":
return "tactic_damage"
if profile == "tactic_heal":
return "tactic_heal"
if profile == "tactic_support":
return "tactic_support"
return "slash" return "slash"
@@ -3330,6 +3417,12 @@ func _action_motion_impact_radius(profile: String) -> float:
return 24.0 return 24.0
if profile == "heavy_strike": if profile == "heavy_strike":
return 28.0 return 28.0
if profile == "tactic_damage":
return 26.0
if profile == "tactic_heal":
return 23.0
if profile == "tactic_support":
return 24.0
return 16.0 return 16.0
@@ -3344,6 +3437,8 @@ func _action_motion_sfx(profile: String) -> String:
return "skill_cast" return "skill_cast"
if profile == "heavy_strike": if profile == "heavy_strike":
return "slash" return "slash"
if profile.begins_with("tactic_"):
return "skill_cast"
return "slash" return "slash"

View File

@@ -449,20 +449,34 @@ func _check_attack_motion_profiles(failures: Array[String]) -> void:
_check_synthetic_attack_profile(failures, scene, "commander", "command_strike") _check_synthetic_attack_profile(failures, scene, "commander", "command_strike")
_check_synthetic_attack_profile(failures, scene, "champion", "heavy_strike") _check_synthetic_attack_profile(failures, scene, "champion", "heavy_strike")
_check_synthetic_attack_profile(failures, scene, "bandit", "heavy_strike") _check_synthetic_attack_profile(failures, scene, "bandit", "heavy_strike")
_check_skill_action_profile(failures, scene, "skill_damage", "tactic_damage", "tactic_flare")
_check_skill_action_profile(failures, scene, "skill_heal", "tactic_heal", "healing_sigil")
_check_skill_action_profile(failures, scene, "skill_support", "tactic_support", "order_banner")
scene._on_unit_action_motion_requested("xiahou_dun", Vector2i(1, 4), Vector2i(2, 4), "attack") 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", "charge_dust") _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") 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", "piercing_trace") _check_stored_attack_motion(failures, scene, "yellow_turban_3", "arrow", "ranged", "attack", "piercing_trace")
scene._on_unit_action_motion_requested("cao_cao", Vector2i(1, 3), Vector2i(3, 3), "skill_damage")
_check_stored_attack_motion(failures, scene, "cao_cao", "tactic_damage", "tactic", "skill_damage", "tactic_flare")
scene._on_unit_action_motion_requested("cao_cao", Vector2i(1, 3), Vector2i(1, 3), "skill_heal")
_check_stored_attack_motion(failures, scene, "cao_cao", "tactic_heal", "tactic", "skill_heal", "healing_sigil")
_check_attack_vfx_signature(failures, scene, "arrow", "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, "cavalry_charge", "charge_dust")
_check_attack_vfx_signature(failures, scene, "infantry_strike", "shield_spark") _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, "command_strike", "command_ring")
_check_attack_vfx_signature(failures, scene, "heavy_strike", "shock_crack") _check_attack_vfx_signature(failures, scene, "heavy_strike", "shock_crack")
_check_attack_vfx_signature(failures, scene, "tactic_damage", "tactic_flare")
_check_attack_vfx_signature(failures, scene, "tactic_heal", "healing_sigil")
_check_attack_vfx_signature(failures, scene, "tactic_support", "order_banner")
_check_unique_attack_vfx_signatures(failures, scene) _check_unique_attack_vfx_signatures(failures, scene)
if scene._action_motion_duration("cavalry_charge") <= scene._action_motion_duration("slash"): if scene._action_motion_duration("cavalry_charge") <= scene._action_motion_duration("slash"):
failures.append("cavalry charge animation should last longer than default slash") failures.append("cavalry charge animation should last longer than default slash")
if scene._action_motion_duration("tactic_damage") <= scene._action_motion_duration("command_strike"):
failures.append("tactic cast animation should linger longer than command physical strike")
if scene._action_motion_lunge_scale("arrow") >= scene._action_motion_lunge_scale("slash"): if scene._action_motion_lunge_scale("arrow") >= scene._action_motion_lunge_scale("slash"):
failures.append("arrow animation should keep the archer mostly planted") failures.append("arrow animation should keep the archer mostly planted")
if scene._action_motion_lunge_scale("tactic_damage") >= scene._action_motion_lunge_scale("arrow"):
failures.append("tactic animation should keep the caster planted like a spell cast")
if scene._action_motion_impact_radius("heavy_strike") <= scene._action_motion_impact_radius("infantry_strike"): 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") failures.append("heavy strike impact radius should exceed infantry strike")
if scene._action_motion_impact_radius("cavalry_charge") <= scene._action_motion_impact_radius("arrow"): if scene._action_motion_impact_radius("cavalry_charge") <= scene._action_motion_impact_radius("arrow"):
@@ -473,6 +487,8 @@ func _check_attack_motion_profiles(failures: Array[String]) -> void:
failures.append("infantry animation should use slash SFX") failures.append("infantry animation should use slash SFX")
if scene._action_motion_sfx("heavy_strike") != "slash": if scene._action_motion_sfx("heavy_strike") != "slash":
failures.append("heavy animation should use swing SFX before hit resolution") failures.append("heavy animation should use swing SFX before hit resolution")
if scene._action_motion_sfx("tactic_damage") != "skill_cast":
failures.append("tactic animation should use skill cast SFX")
scene.free() scene.free()
@@ -496,6 +512,16 @@ 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]) failures.append("%s synthetic attack profile mismatch: expected %s got %s" % [class_id, expected_profile, profile])
func _check_skill_action_profile(failures: Array[String], scene, action_kind: String, expected_profile: String, expected_signature: String) -> void:
var profile: String = scene._unit_action_motion_profile({"class_id": "hero", "range": 1}, Vector2i(0, 0), Vector2i(0, 0), action_kind)
if profile != expected_profile:
failures.append("%s action profile mismatch: expected %s got %s" % [action_kind, expected_profile, profile])
if not scene._action_motion_allows_same_cell(profile):
failures.append("%s should allow same-cell tactic motion" % profile)
if scene._action_motion_vfx_signature(profile) != expected_signature:
failures.append("%s action VFX mismatch: expected %s got %s" % [action_kind, expected_signature, scene._action_motion_vfx_signature(profile)])
func _check_attack_vfx_signature(failures: Array[String], scene, profile: String, expected_signature: 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) var signature: String = scene._action_motion_vfx_signature(profile)
if signature != expected_signature: if signature != expected_signature:
@@ -569,6 +595,38 @@ func _check_attack_motion_signal(failures: Array[String]) -> void:
return return
if not motions.has("cao_cao|attack|6,2|7,2"): if not motions.has("cao_cao|attack|6,2|7,2"):
failures.append("attack motion signal missing expected Cao Cao attack: %s" % str(motions)) failures.append("attack motion signal missing expected Cao Cao attack: %s" % str(motions))
_check_skill_motion_signal(failures, "spark", "skill_damage", Vector2i(3, 3), func(skill_state) -> void:
var enemy: Dictionary = skill_state.get_unit("yellow_turban_1")
enemy["pos"] = Vector2i(3, 3)
)
_check_skill_motion_signal(failures, "mend", "skill_heal", Vector2i(1, 3), func(skill_state) -> void:
var caster: Dictionary = skill_state.get_unit("cao_cao")
caster["hp"] = max(1, int(caster.get("hp", 1)) - 10)
)
_check_skill_motion_signal(failures, "guard_order", "skill_support", Vector2i(1, 3), func(_skill_state) -> void:
pass
)
func _check_skill_motion_signal(failures: Array[String], skill_id: String, expected_kind: String, target_cell: Vector2i, setup: Callable) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("could not load battle for %s motion" % skill_id)
return
setup.call(state)
var motions: Array[String] = []
state.unit_action_motion_requested.connect(func(unit_id: String, from_cell: Vector2i, to_cell: Vector2i, action_kind: String) -> void:
motions.append("%s|%s|%d,%d|%d,%d" % [unit_id, action_kind, from_cell.x, from_cell.y, to_cell.x, to_cell.y])
)
if not state.select_unit("cao_cao"):
failures.append("could not select Cao Cao for %s motion" % skill_id)
return
if not state.try_cast_selected_skill(skill_id, target_cell):
failures.append("%s should cast successfully for motion check" % skill_id)
return
var expected := "cao_cao|%s|1,3|%d,%d" % [expected_kind, target_cell.x, target_cell.y]
if not motions.has(expected):
failures.append("%s motion signal missing `%s`: %s" % [skill_id, expected, str(motions)])
func _check_camp_conversation_effect(failures: Array[String], conversation: Dictionary, expected_item_id: String, expected_count: int) -> void: func _check_camp_conversation_effect(failures: Array[String], conversation: Dictionary, expected_item_id: String, expected_count: int) -> void: