Add unit movement slide animation

This commit is contained in:
2026-06-18 03:28:40 +09:00
parent 889b5a3743
commit 05af755da0
5 changed files with 72 additions and 14 deletions

View File

@@ -7,6 +7,7 @@ signal changed
signal log_added(message: String)
signal dialogue_requested(lines: Array)
signal combat_feedback_requested(unit_id: String, text: String, kind: String)
signal unit_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Vector2i)
const TEAM_PLAYER := "player"
const TEAM_ENEMY := "enemy"
@@ -539,8 +540,10 @@ func try_move_selected(cell: Vector2i) -> bool:
if not get_unit_at(cell).is_empty():
return false
var from_cell: Vector2i = unit["pos"]
unit["pos"] = cell
unit["moved"] = true
unit_motion_requested.emit(str(unit.get("id", "")), from_cell, cell)
_emit_log("%s moved to %s." % [unit["name"], _format_cell(cell)])
_run_events("unit_reaches_tile", str(unit.get("team", "")), turn_number, unit)
_check_battle_status()
@@ -1837,8 +1840,10 @@ func _enemy_take_action(enemy: Dictionary) -> void:
best_cell = cell
if best_cell != enemy["pos"]:
var from_cell: Vector2i = enemy["pos"]
enemy["pos"] = best_cell
enemy["moved"] = true
unit_motion_requested.emit(str(enemy.get("id", "")), from_cell, best_cell)
_emit_log("%s advances to %s." % [enemy["name"], _format_cell(best_cell)])
_run_events("unit_reaches_tile", str(enemy.get("team", "")), turn_number, enemy)
if battle_status != STATUS_ACTIVE:

View File

@@ -33,6 +33,7 @@ const SFX_VOLUME_DB := -4.0
const UI_SFX_VOLUME_DB := -8.0
const FLOATING_TEXT_LIFETIME := 1.0
const FLOATING_TEXT_RISE := 42.0
const UNIT_MOVE_ANIMATION_DURATION := 0.18
var state: BattleState = BattleStateScript.new()
var campaign_state: CampaignState = CampaignStateScript.new()
@@ -110,6 +111,7 @@ var bgm_player: AudioStreamPlayer
var current_bgm_key := ""
var last_announced_battle_status := ""
var floating_texts: Array[Dictionary] = []
var unit_motion_by_unit: Dictionary = {}
func _ready() -> void:
@@ -119,6 +121,7 @@ func _ready() -> void:
state.log_added.connect(_on_log_added)
state.dialogue_requested.connect(_on_dialogue_requested)
state.combat_feedback_requested.connect(_on_combat_feedback_requested)
state.unit_motion_requested.connect(_on_unit_motion_requested)
campaign_state.load_campaign(CAMPAIGN_PATH)
campaign_state.load_or_start(campaign_state.get_start_scenario_id())
if campaign_state.has_pending_post_battle_choice():
@@ -738,16 +741,30 @@ func _handle_key(event: InputEventKey) -> void:
func _process(delta: float) -> void:
if floating_texts.is_empty():
return
var active_texts: Array[Dictionary] = []
for popup in floating_texts:
var next_popup := popup.duplicate(true)
next_popup["age"] = float(next_popup.get("age", 0.0)) + delta
if float(next_popup["age"]) < float(next_popup.get("duration", FLOATING_TEXT_LIFETIME)):
active_texts.append(next_popup)
floating_texts = active_texts
queue_redraw()
var needs_redraw := false
if not floating_texts.is_empty():
var active_texts: Array[Dictionary] = []
for popup in floating_texts:
var next_popup := popup.duplicate(true)
next_popup["age"] = float(next_popup.get("age", 0.0)) + delta
if float(next_popup["age"]) < float(next_popup.get("duration", FLOATING_TEXT_LIFETIME)):
active_texts.append(next_popup)
floating_texts = active_texts
needs_redraw = true
if not unit_motion_by_unit.is_empty():
var active_motion := {}
for unit_id in unit_motion_by_unit.keys():
var motion: Dictionary = unit_motion_by_unit[unit_id]
var next_motion := motion.duplicate(true)
next_motion["age"] = float(next_motion.get("age", 0.0)) + delta
if float(next_motion["age"]) < float(next_motion.get("duration", UNIT_MOVE_ANIMATION_DURATION)):
active_motion[unit_id] = next_motion
unit_motion_by_unit = active_motion
needs_redraw = true
if needs_redraw:
queue_redraw()
func _can_select(unit: Dictionary) -> bool:
@@ -815,7 +832,7 @@ func _draw_units() -> void:
if not unit.get("alive", false) or not unit.get("deployed", true):
continue
var rect := _rect_for_cell(unit["pos"])
var rect := _visual_rect_for_unit(unit)
var center := rect.position + rect.size * 0.5
var team_color := PLAYER_COLOR if unit.get("team", "") == BattleState.TEAM_PLAYER else ENEMY_COLOR
var body_color := team_color.darkened(0.35) if unit.get("acted", false) else team_color
@@ -836,6 +853,26 @@ func _draw_units() -> void:
draw_arc(center, 29, 0.0, TAU, 48, Color(1.0, 0.92, 0.25), 3.0)
func _visual_rect_for_unit(unit: Dictionary) -> Rect2:
var cell: Vector2i = unit.get("pos", Vector2i(-1, -1))
var rect := _rect_for_cell(cell)
var unit_id := str(unit.get("id", ""))
if unit_id.is_empty() or not unit_motion_by_unit.has(unit_id):
return rect
var motion: Dictionary = unit_motion_by_unit[unit_id]
var from_cell: Vector2i = motion.get("from", cell)
var to_cell: Vector2i = motion.get("to", cell)
var duration := max(0.01, float(motion.get("duration", UNIT_MOVE_ANIMATION_DURATION)))
var age := float(motion.get("age", 0.0))
var progress := clampf(age / duration, 0.0, 1.0)
var smooth_progress := progress * progress * (3.0 - 2.0 * progress)
var from_rect := _rect_for_cell(from_cell)
var to_rect := _rect_for_cell(to_cell)
rect.position = from_rect.position.lerp(to_rect.position, smooth_progress)
return rect
func _draw_floating_texts() -> void:
if floating_texts.is_empty():
return
@@ -1262,6 +1299,18 @@ func _on_combat_feedback_requested(unit_id: String, text: String, kind: String)
queue_redraw()
func _on_unit_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Vector2i) -> void:
if unit_id.is_empty() or from_cell == to_cell:
return
unit_motion_by_unit[unit_id] = {
"from": from_cell,
"to": to_cell,
"age": 0.0,
"duration": UNIT_MOVE_ANIMATION_DURATION
}
queue_redraw()
func _floating_text_origin(cell: Vector2i) -> Vector2:
var rect := _rect_for_cell(cell)
return rect.position + Vector2(TILE_SIZE * 0.5, 18.0)
@@ -1447,6 +1496,7 @@ func _on_restart_pressed() -> void:
battle_result_summary.clear()
post_battle_dialogue_started = false
floating_texts.clear()
unit_motion_by_unit.clear()
shop_sell_mode = false
selected_skill_id = ""
selected_item_id = ""
@@ -1491,6 +1541,7 @@ func _load_scenario(scenario_id: String) -> void:
battle_result_summary.clear()
post_battle_dialogue_started = false
floating_texts.clear()
unit_motion_by_unit.clear()
selected_skill_id = ""
selected_item_id = ""
_hide_tactic_menu()
@@ -2370,6 +2421,7 @@ func _show_campaign_complete() -> void:
battle_result_summary.clear()
post_battle_dialogue_started = true
floating_texts.clear()
unit_motion_by_unit.clear()
move_cells.clear()
attack_cells.clear()
skill_cells.clear()