Sequence enemy turn presentation

This commit is contained in:
2026-06-19 18:21:06 +09:00
parent c40bccf044
commit 0fed16fe32
2 changed files with 204 additions and 18 deletions

View File

@@ -56,6 +56,8 @@ const MINIMAP_PADDING := 8.0
const MINIMAP_TITLE_HEIGHT := 18.0
const MINIMAP_MARGIN := Vector2(18, 18)
const MAP_FOCUS_MARGIN := 96.0
const BATTLE_PRESENTATION_STEP_GAP := 0.08
const BATTLE_PRESENTATION_FEEDBACK_LAG := 0.10
const UNIT_MOVE_ANIMATION_DURATION := 0.18
const TARGET_PREVIEW_BADGE_SIZE := Vector2(104, 22)
const LOW_HP_WARNING_RATIO := 0.35
@@ -431,6 +433,9 @@ var floating_texts: Array[Dictionary] = []
var objective_notice_timer := 0.0
var unit_motion_by_unit: Dictionary = {}
var unit_action_motion_by_unit: Dictionary = {}
var battle_presentation_sequence_active := false
var battle_presentation_delay_cursor := 0.0
var battle_presentation_feedback_delay_by_unit := {}
var battle_background_path := ""
var battle_background_texture: Texture2D
var ui_regular_font: Font
@@ -2628,6 +2633,7 @@ func _handle_key(event: InputEventKey) -> void:
func _process(delta: float) -> void:
var needs_redraw := false
var had_sequence_presentation := _has_active_battle_presentation_sequence()
if battle_started and state.battle_status == BattleState.STATUS_ACTIVE and not _is_dialogue_visible():
needs_redraw = true
if _update_edge_scroll(delta):
@@ -2642,6 +2648,7 @@ func _process(delta: float) -> void:
for popup in floating_texts:
var next_popup := popup.duplicate(true)
next_popup["age"] = float(next_popup.get("age", 0.0)) + delta
_activate_presentation_focus_if_ready(next_popup)
if float(next_popup["age"]) < float(next_popup.get("duration", FLOATING_TEXT_LIFETIME)):
active_texts.append(next_popup)
floating_texts = active_texts
@@ -2653,6 +2660,7 @@ func _process(delta: float) -> void:
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
_activate_presentation_focus_if_ready(next_motion)
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
@@ -2664,6 +2672,7 @@ func _process(delta: float) -> void:
var motion: Dictionary = unit_action_motion_by_unit[unit_id]
var next_motion := motion.duplicate(true)
next_motion["age"] = float(next_motion.get("age", 0.0)) + delta
_activate_presentation_focus_if_ready(next_motion)
if float(next_motion["age"]) < float(next_motion.get("duration", UNIT_ATTACK_ANIMATION_DURATION)):
active_actions[unit_id] = next_motion
unit_action_motion_by_unit = active_actions
@@ -2671,6 +2680,10 @@ func _process(delta: float) -> void:
if needs_redraw:
queue_redraw()
if had_sequence_presentation and not _has_active_battle_presentation_sequence():
if status_label != null:
_update_hud()
queue_redraw()
func _can_select(unit: Dictionary) -> bool:
@@ -3586,12 +3599,13 @@ func _unit_pixel_facing(unit: Dictionary) -> int:
var unit_id := str(unit.get("id", ""))
if not unit_id.is_empty() and unit_action_motion_by_unit.has(unit_id):
var action_motion: Dictionary = unit_action_motion_by_unit[unit_id]
var from_cell: Vector2i = action_motion.get("from", unit.get("pos", Vector2i.ZERO))
var to_cell: Vector2i = action_motion.get("to", from_cell)
if to_cell.x < from_cell.x:
return -1
if to_cell.x > from_cell.x:
return 1
if float(action_motion.get("age", 0.0)) >= 0.0:
var from_cell: Vector2i = action_motion.get("from", unit.get("pos", Vector2i.ZERO))
var to_cell: Vector2i = action_motion.get("to", from_cell)
if to_cell.x < from_cell.x:
return -1
if to_cell.x > from_cell.x:
return 1
if unit.has("facing_x"):
return state.unit_facing_x(unit)
if unit.get("team", "") == BattleState.TEAM_ENEMY:
@@ -4098,6 +4112,8 @@ func _draw_attack_effects() -> void:
var motion: Dictionary = unit_action_motion_by_unit[unit_id]
var duration: float = maxf(0.01, float(motion.get("duration", UNIT_ATTACK_ANIMATION_DURATION)))
var age := float(motion.get("age", 0.0))
if age < 0.0:
continue
var progress := clampf(age / duration, 0.0, 1.0)
var profile := str(motion.get("profile", "slash"))
var peak := _action_motion_effect_peak(profile, progress)
@@ -4311,6 +4327,8 @@ func _draw_floating_texts() -> void:
for popup in floating_texts:
var duration: float = maxf(0.01, float(popup.get("duration", FLOATING_TEXT_LIFETIME)))
var age := float(popup.get("age", 0.0))
if age < 0.0:
continue
var progress := clampf(age / duration, 0.0, 1.0)
var alpha := 1.0 - progress
var color := _floating_text_color(str(popup.get("kind", "")))
@@ -4844,6 +4862,67 @@ func _focus_board_on_cell(cell: Vector2i, force_center := false) -> bool:
return true
func _activate_presentation_focus_if_ready(entry: Dictionary) -> void:
if bool(entry.get("focused", false)):
return
if float(entry.get("age", 0.0)) < 0.0:
return
var focus_cell: Vector2i = entry.get("focus_cell", Vector2i(-1, -1))
if state.is_inside(focus_cell):
_focus_board_on_cell(focus_cell, bool(entry.get("force_focus", false)))
var sfx_key := str(entry.get("sfx", ""))
if not sfx_key.is_empty() and not bool(entry.get("sfx_played", false)) and is_inside_tree():
_play_sfx(sfx_key)
entry["sfx_played"] = true
entry["focused"] = true
func _begin_battle_presentation_sequence() -> void:
battle_presentation_sequence_active = true
battle_presentation_delay_cursor = 0.0
battle_presentation_feedback_delay_by_unit.clear()
func _end_battle_presentation_sequence() -> void:
battle_presentation_sequence_active = false
battle_presentation_delay_cursor = 0.0
func _presentation_delay_for_step(duration: float) -> float:
if not battle_presentation_sequence_active:
return 0.0
var delay := battle_presentation_delay_cursor
battle_presentation_delay_cursor += maxf(0.01, duration) + BATTLE_PRESENTATION_STEP_GAP
return delay
func _presentation_feedback_delay_for_unit(unit_id: String) -> float:
if battle_presentation_feedback_delay_by_unit.has(unit_id):
return maxf(0.0, float(battle_presentation_feedback_delay_by_unit[unit_id]))
if battle_presentation_sequence_active:
return maxf(0.0, battle_presentation_delay_cursor)
return 0.0
func _clear_presentation_state() -> void:
battle_presentation_sequence_active = false
battle_presentation_delay_cursor = 0.0
battle_presentation_feedback_delay_by_unit.clear()
func _has_active_battle_presentation_sequence() -> bool:
for motion in unit_motion_by_unit.values():
if typeof(motion) == TYPE_DICTIONARY and bool((motion as Dictionary).get("sequence", false)):
return true
for motion in unit_action_motion_by_unit.values():
if typeof(motion) == TYPE_DICTIONARY and bool((motion as Dictionary).get("sequence", false)):
return true
for popup in floating_texts:
if typeof(popup) == TYPE_DICTIONARY and bool((popup as Dictionary).get("sequence", false)):
return true
return false
func _is_minimap_visible() -> bool:
return battle_started and state.map_size.x > 0 and state.map_size.y > 0 and state.battle_status == BattleState.STATUS_ACTIVE
@@ -5788,14 +5867,20 @@ func _on_combat_feedback_requested(unit_id: String, text: String, kind: String)
var cell: Vector2i = unit.get("pos", Vector2i(-1, -1))
if not state.is_inside(cell):
return
_focus_board_on_cell(cell, true)
var delay := _presentation_feedback_delay_for_unit(unit_id)
if delay <= 0.0:
_focus_board_on_cell(cell, true)
var jitter := float((floating_texts.size() % 3) - 1) * 12.0
floating_texts.append({
"text": text,
"kind": kind,
"pos": _floating_text_origin(cell) + Vector2(jitter, 0),
"age": 0.0,
"duration": FLOATING_TEXT_LIFETIME
"age": -delay,
"duration": FLOATING_TEXT_LIFETIME,
"focus_cell": cell,
"force_focus": true,
"focused": delay <= 0.0,
"sequence": delay > 0.0
})
queue_redraw()
@@ -5803,12 +5888,20 @@ func _on_combat_feedback_requested(unit_id: String, text: String, kind: String)
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
_focus_board_on_cell(to_cell)
var duration := UNIT_MOVE_ANIMATION_DURATION
var delay := _presentation_delay_for_step(duration)
battle_presentation_feedback_delay_by_unit[unit_id] = delay + duration + BATTLE_PRESENTATION_FEEDBACK_LAG
if delay <= 0.0:
_focus_board_on_cell(to_cell)
unit_motion_by_unit[unit_id] = {
"from": from_cell,
"to": to_cell,
"age": 0.0,
"duration": UNIT_MOVE_ANIMATION_DURATION
"age": -delay,
"duration": duration,
"focus_cell": to_cell,
"force_focus": false,
"focused": delay <= 0.0,
"sequence": battle_presentation_sequence_active or delay > 0.0
}
queue_redraw()
@@ -5821,7 +5914,12 @@ func _on_unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_c
if from_cell == to_cell and not _action_motion_allows_same_cell(profile):
return
var focus_cell := to_cell if state.is_inside(to_cell) else from_cell
_focus_board_on_cell(focus_cell, true)
var duration := _action_motion_duration(profile)
var delay := _presentation_delay_for_step(duration)
battle_presentation_feedback_delay_by_unit[unit_id] = delay + duration * 0.58
if delay <= 0.0:
_focus_board_on_cell(focus_cell, true)
var sfx_key := _action_motion_sfx(profile)
unit_action_motion_by_unit[unit_id] = {
"from": from_cell,
"to": to_cell,
@@ -5832,11 +5930,17 @@ func _on_unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_c
"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)
"age": -delay,
"duration": duration,
"focus_cell": focus_cell,
"force_focus": true,
"focused": delay <= 0.0,
"sfx": sfx_key,
"sfx_played": delay <= 0.0,
"sequence": battle_presentation_sequence_active or delay > 0.0
}
if is_inside_tree():
_play_sfx(_action_motion_sfx(profile))
if delay <= 0.0 and is_inside_tree():
_play_sfx(sfx_key)
queue_redraw()
@@ -6313,7 +6417,9 @@ func _on_end_turn_pressed() -> void:
_hide_item_menu()
_hide_equip_menu()
_hide_post_move_picker()
_begin_battle_presentation_sequence()
state.end_player_turn()
_end_battle_presentation_sequence()
func _on_tactic_pressed() -> void:
@@ -6428,6 +6534,8 @@ func _on_restart_pressed() -> void:
post_battle_dialogue_started = false
floating_texts.clear()
unit_motion_by_unit.clear()
unit_action_motion_by_unit.clear()
_clear_presentation_state()
shop_sell_mode = false
selected_skill_id = ""
selected_item_id = ""
@@ -6474,6 +6582,8 @@ func _load_scenario(scenario_id: String) -> void:
post_battle_dialogue_started = false
floating_texts.clear()
unit_motion_by_unit.clear()
unit_action_motion_by_unit.clear()
_clear_presentation_state()
selected_skill_id = ""
selected_item_id = ""
_clear_pending_move_state()
@@ -7525,6 +7635,8 @@ func _reload_from_campaign_state() -> void:
post_battle_dialogue_started = false
floating_texts.clear()
unit_motion_by_unit.clear()
unit_action_motion_by_unit.clear()
_clear_presentation_state()
selected_skill_id = ""
selected_item_id = ""
_clear_pending_move_state()
@@ -8278,7 +8390,13 @@ func _sale_price_for_item(item: Dictionary) -> int:
func _is_input_locked() -> bool:
return campaign_complete_screen or _is_dialogue_visible() or not battle_started or state.battle_status != BattleState.STATUS_ACTIVE
return (
campaign_complete_screen
or _is_dialogue_visible()
or _has_active_battle_presentation_sequence()
or not battle_started
or state.battle_status != BattleState.STATUS_ACTIVE
)
func _apply_battle_result_once() -> void:
@@ -8760,6 +8878,8 @@ func _show_campaign_complete() -> void:
post_battle_dialogue_started = true
floating_texts.clear()
unit_motion_by_unit.clear()
unit_action_motion_by_unit.clear()
_clear_presentation_state()
move_cells.clear()
attack_cells.clear()
skill_cells.clear()