Add class-specific attack presentation

This commit is contained in:
2026-06-18 17:37:47 +09:00
parent c928bb64b7
commit 84ccead01c
5 changed files with 244 additions and 23 deletions

View File

@@ -47,6 +47,11 @@ const UNIT_STATUS_MARKER_OFFSET_Y := 14.0
const MAX_UNIT_STATUS_MARKERS := 4
const UNIT_ATTACK_ANIMATION_DURATION := 0.30
const UNIT_ATTACK_LUNGE_DISTANCE := 15.0
const UNIT_INFANTRY_ATTACK_DURATION := 0.32
const UNIT_CAVALRY_ATTACK_DURATION := 0.42
const UNIT_ARROW_ATTACK_DURATION := 0.36
const UNIT_COMMAND_ATTACK_DURATION := 0.38
const UNIT_HEAVY_ATTACK_DURATION := 0.34
const DIALOGUE_PANEL_POSITION := Vector2(96, 448)
const DIALOGUE_PANEL_SIZE := Vector2(1040, 210)
const DIALOGUE_PORTRAIT_SIZE := Vector2(164, 186)
@@ -956,9 +961,6 @@ func _handle_board_click(screen_position: Vector2) -> void:
return
if not clicked_unit.is_empty() and clicked_unit.get("team", "") != selected_unit.get("team", ""):
var attack_sfx := "bow_release" if int(selected_unit.get("range", 1)) > 1 else "slash"
if attack_cells.has(clicked_unit.get("pos", Vector2i(-1, -1))):
_play_sfx(attack_sfx)
if state.try_attack_selected(clicked_unit["id"]):
selected_skill_id = ""
selected_item_id = ""
@@ -1406,7 +1408,7 @@ func _visual_rect_for_unit(unit: Dictionary) -> Rect2:
var action_duration: float = maxf(0.01, float(action_motion.get("duration", UNIT_ATTACK_ANIMATION_DURATION)))
var action_age := float(action_motion.get("age", 0.0))
var action_progress := clampf(action_age / action_duration, 0.0, 1.0)
var lunge_scale := 0.35 if str(action_motion.get("style", "melee")) == "ranged" else 1.0
var lunge_scale := _action_motion_lunge_scale(str(action_motion.get("profile", "slash")))
rect.position += direction * sin(action_progress * PI) * UNIT_ATTACK_LUNGE_DISTANCE * lunge_scale
return rect
@@ -1437,17 +1439,94 @@ 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
var flash := team_color.lightened(0.45)
flash.a = 0.55 * peak
if str(motion.get("style", "melee")) == "ranged":
var head := start.lerp(end, clampf(progress + 0.18, 0.0, 1.0))
var tail := start.lerp(end, clampf(progress - 0.18, 0.0, 1.0))
draw_line(tail, head, flash, 3.0)
draw_circle(head, 4.0 + 4.0 * peak, Color(1.0, 0.92, 0.52, 0.42 * peak))
else:
draw_line(end + Vector2(-18, -13), end + Vector2(18, 13), flash, 3.0)
draw_line(end + Vector2(16, -14), end + Vector2(-14, 15), Color(1.0, 0.88, 0.42, 0.38 * peak), 2.0)
draw_circle(end, 8.0 + 8.0 * peak, Color(1.0, 0.82, 0.34, 0.20 * peak))
_draw_action_effect(str(motion.get("profile", "slash")), start, end, progress, peak, team_color)
func _draw_action_effect(profile: String, start: Vector2, end: Vector2, progress: float, peak: float, team_color: Color) -> void:
if profile == "arrow":
_draw_arrow_attack_effect(start, end, progress, peak, team_color)
elif profile == "cavalry_charge":
_draw_cavalry_attack_effect(start, end, progress, peak, team_color)
elif profile == "infantry_strike":
_draw_infantry_attack_effect(end, peak, team_color)
elif profile == "command_strike":
_draw_command_attack_effect(start, end, progress, peak, team_color)
elif profile == "heavy_strike":
_draw_heavy_attack_effect(end, peak, team_color)
else:
_draw_slash_attack_effect(end, peak, team_color)
func _draw_arrow_attack_effect(start: Vector2, end: Vector2, progress: float, peak: float, team_color: Color) -> void:
var direction := end - start
if direction.length() <= 0.01:
return
direction = direction.normalized()
var side := Vector2(-direction.y, direction.x)
var head := start.lerp(end, clampf(progress + 0.20, 0.0, 1.0))
var tail := start.lerp(end, clampf(progress - 0.18, 0.0, 1.0))
var shaft := Color(1.0, 0.86, 0.42, 0.76 * peak)
draw_line(tail, head, shaft, 3.0)
draw_line(head, head - direction * 9.0 + side * 4.5, Color(1.0, 0.96, 0.66, 0.70 * peak), 2.0)
draw_line(head, head - direction * 9.0 - side * 4.5, Color(1.0, 0.96, 0.66, 0.70 * peak), 2.0)
var wake := team_color.lightened(0.48)
wake.a = 0.22 * peak
draw_line(tail - side * 5.0, head - side * 5.0, wake, 1.5)
draw_line(tail + side * 5.0, head + side * 5.0, wake, 1.5)
draw_circle(head, 3.5 + 4.0 * peak, Color(1.0, 0.92, 0.52, 0.36 * peak))
func _draw_cavalry_attack_effect(start: Vector2, end: Vector2, progress: float, peak: float, team_color: Color) -> void:
var direction := end - start
if direction.length() <= 0.01:
return
direction = direction.normalized()
var side := Vector2(-direction.y, direction.x)
for index in range(3):
var lane_offset := side * float(index - 1) * 7.0
var dust_head := start.lerp(end, clampf(progress - float(index) * 0.05, 0.0, 1.0))
var dust_tail := dust_head - direction * (16.0 + float(index) * 5.0)
draw_line(dust_tail + lane_offset, dust_head + lane_offset, Color(0.86, 0.72, 0.42, 0.24 * peak), 2.0)
var lance := team_color.lightened(0.52)
lance.a = 0.68 * peak
draw_line(end - direction * 22.0, end + direction * 12.0, lance, 4.0)
draw_line(end - side * 15.0, end + side * 15.0, Color(1.0, 0.88, 0.34, 0.34 * peak), 2.0)
draw_circle(end, 9.0 + 13.0 * peak, Color(1.0, 0.66, 0.22, 0.18 * peak))
func _draw_infantry_attack_effect(end: Vector2, peak: float, team_color: Color) -> void:
var guard_flash := team_color.lightened(0.48)
guard_flash.a = 0.58 * peak
draw_line(end + Vector2(-16, -10), end + Vector2(17, 11), guard_flash, 3.0)
draw_line(end + Vector2(-14, 10), end + Vector2(14, -10), Color(1.0, 0.88, 0.42, 0.32 * peak), 2.0)
draw_rect(Rect2(end + Vector2(-9, -14), Vector2(18, 20)), Color(0.72, 0.82, 0.94, 0.16 * peak), false, 2.0)
draw_arc(end, 8.0 + 9.0 * peak, -PI * 0.80, PI * 0.20, 24, Color(1.0, 0.92, 0.54, 0.34 * peak), 2.0)
func _draw_command_attack_effect(start: Vector2, end: Vector2, progress: float, peak: float, team_color: Color) -> void:
var beam := Color(0.72, 0.92, 1.0, 0.34 * peak)
draw_line(start, end, beam, 2.0)
var command_color := team_color.lightened(0.58)
command_color.a = 0.56 * peak
draw_arc(end, 10.0 + 12.0 * peak, 0.0, TAU, 48, command_color, 2.0)
draw_arc(end, 20.0 - 6.0 * peak, progress * TAU, progress * TAU + PI * 1.35, 36, Color(1.0, 0.96, 0.62, 0.42 * peak), 2.0)
draw_circle(end, 6.0 + 8.0 * peak, Color(0.72, 0.92, 1.0, 0.18 * peak))
func _draw_heavy_attack_effect(end: Vector2, peak: float, team_color: Color) -> void:
var heavy := team_color.lightened(0.42)
heavy.a = 0.62 * peak
draw_line(end + Vector2(-20, -16), end + Vector2(20, 16), heavy, 5.0)
draw_line(end + Vector2(18, -18), end + Vector2(-18, 18), Color(1.0, 0.82, 0.28, 0.48 * peak), 4.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_slash_attack_effect(end: Vector2, peak: float, team_color: Color) -> void:
var flash := team_color.lightened(0.45)
flash.a = 0.55 * peak
draw_line(end + Vector2(-18, -13), end + Vector2(18, 13), flash, 3.0)
draw_line(end + Vector2(16, -14), end + Vector2(-14, 15), Color(1.0, 0.88, 0.42, 0.38 * peak), 2.0)
draw_circle(end, 8.0 + 8.0 * peak, Color(1.0, 0.82, 0.34, 0.20 * peak))
func _draw_floating_texts() -> void:
@@ -2370,19 +2449,86 @@ func _on_unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_c
if unit_id.is_empty() or from_cell == to_cell:
return
var unit := state.get_unit(unit_id)
var distance := absi(from_cell.x - to_cell.x) + absi(from_cell.y - to_cell.y)
var style := "ranged" if int(unit.get("range", 1)) > 1 and distance > 1 else "melee"
var profile := _unit_action_motion_profile(unit, from_cell, to_cell, action_kind)
unit_action_motion_by_unit[unit_id] = {
"from": from_cell,
"to": to_cell,
"kind": action_kind,
"style": style,
"profile": profile,
"attack_family": profile,
"style": _action_motion_style(profile),
"age": 0.0,
"duration": UNIT_ATTACK_ANIMATION_DURATION
"duration": _action_motion_duration(profile)
}
if is_inside_tree():
_play_sfx(_action_motion_sfx(profile))
queue_redraw()
func _unit_action_motion_profile(unit: Dictionary, from_cell: Vector2i, to_cell: Vector2i, _action_kind: String) -> String:
var class_id := str(unit.get("class_id", ""))
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):
return "arrow"
if class_id.contains("cavalry"):
return "cavalry_charge"
if class_id == "infantry" or class_id == "guard_captain":
return "infantry_strike"
if class_id.contains("strategist") or class_id.contains("advisor") or class_id.contains("hero") or class_id.contains("commander"):
return "command_strike"
if class_id.contains("warrior") or class_id.contains("champion") or class_id.contains("bandit"):
return "heavy_strike"
return "slash"
func _action_motion_style(profile: String) -> String:
if profile == "arrow":
return "ranged"
return "melee"
func _action_motion_duration(profile: String) -> float:
if profile == "arrow":
return UNIT_ARROW_ATTACK_DURATION
if profile == "cavalry_charge":
return UNIT_CAVALRY_ATTACK_DURATION
if profile == "infantry_strike":
return UNIT_INFANTRY_ATTACK_DURATION
if profile == "command_strike":
return UNIT_COMMAND_ATTACK_DURATION
if profile == "heavy_strike":
return UNIT_HEAVY_ATTACK_DURATION
return UNIT_ATTACK_ANIMATION_DURATION
func _action_motion_lunge_scale(profile: String) -> float:
if profile == "arrow":
return 0.22
if profile == "cavalry_charge":
return 1.65
if profile == "infantry_strike":
return 0.95
if profile == "command_strike":
return 0.20
if profile == "heavy_strike":
return 1.15
return 1.0
func _action_motion_sfx(profile: String) -> String:
if profile == "arrow":
return "bow_release"
if profile == "cavalry_charge":
return "footstep"
if profile == "infantry_strike":
return "slash"
if profile == "command_strike":
return "skill_cast"
if profile == "heavy_strike":
return "slash"
return "slash"
func _floating_text_origin(cell: Vector2i) -> Vector2:
var rect := _rect_for_cell(cell)
return rect.position + Vector2(TILE_SIZE * 0.5, 18.0)