2546 lines
76 KiB
GDScript
2546 lines
76 KiB
GDScript
extends Node2D
|
|
|
|
const BattleStateScript := preload("res://scripts/core/battle_state.gd")
|
|
const CampaignStateScript := preload("res://scripts/core/campaign_state.gd")
|
|
|
|
const CAMPAIGN_PATH := "res://data/campaign/campaign.json"
|
|
const TILE_SIZE := 64
|
|
const BOARD_OFFSET := Vector2(48, 104)
|
|
const PLAYER_COLOR := Color(0.20, 0.42, 0.82)
|
|
const ENEMY_COLOR := Color(0.72, 0.18, 0.16)
|
|
const GRID_COLOR := Color(0.05, 0.06, 0.07, 0.45)
|
|
const SKILL_OVERLAY_COLOR := Color(0.56, 0.28, 0.95, 0.28)
|
|
const ITEM_OVERLAY_COLOR := Color(0.24, 0.74, 0.34, 0.30)
|
|
const FORMATION_OVERLAY_COLOR := Color(0.95, 0.76, 0.25, 0.34)
|
|
const OBJECTIVE_OVERLAY_COLOR := Color(1.0, 0.78, 0.18, 0.30)
|
|
const BGM_MENU := preload("res://audio/bgm/menu_theme_placeholder.wav")
|
|
const BGM_BATTLE := preload("res://audio/bgm/battle_loop_placeholder.wav")
|
|
const SFX_BOW_RELEASE := preload("res://audio/sfx/bow_release_01.wav")
|
|
const SFX_DEFEAT_STING := preload("res://audio/sfx/defeat_sting_01.wav")
|
|
const SFX_FIRE_SKILL := preload("res://audio/sfx/fire_skill_01.wav")
|
|
const SFX_FOOTSTEP := preload("res://audio/sfx/footstep_armor_01.wav")
|
|
const SFX_GUARD_BLOCK := preload("res://audio/sfx/guard_block_01.wav")
|
|
const SFX_HIT_HEAVY := preload("res://audio/sfx/hit_heavy_01.wav")
|
|
const SFX_ITEM_PICKUP := preload("res://audio/sfx/item_pickup_01.wav")
|
|
const SFX_MENU_CANCEL := preload("res://audio/sfx/menu_cancel_01.wav")
|
|
const SFX_MENU_CONFIRM := preload("res://audio/sfx/menu_confirm_01.wav")
|
|
const SFX_SKILL_CAST := preload("res://audio/sfx/skill_cast_01.wav")
|
|
const SFX_SLASH := preload("res://audio/sfx/slash_01.wav")
|
|
const SFX_UI_CLICK := preload("res://audio/sfx/ui_click_01.wav")
|
|
const SFX_VICTORY_STING := preload("res://audio/sfx/victory_sting_01.wav")
|
|
const BGM_VOLUME_DB := -14.0
|
|
const SFX_VOLUME_DB := -4.0
|
|
const UI_SFX_VOLUME_DB := -8.0
|
|
|
|
var state: BattleState = BattleStateScript.new()
|
|
var campaign_state: CampaignState = CampaignStateScript.new()
|
|
var hover_cell := Vector2i(-1, -1)
|
|
var move_cells: Array[Vector2i] = []
|
|
var attack_cells: Array[Vector2i] = []
|
|
var skill_cells: Array[Vector2i] = []
|
|
var item_cells: Array[Vector2i] = []
|
|
var selected_skill_id := ""
|
|
var selected_item_id := ""
|
|
var active_scenario_id := ""
|
|
var battle_started := false
|
|
var battle_result_applied := false
|
|
var battle_result_summary := {}
|
|
var post_battle_dialogue_started := false
|
|
var campaign_complete_screen := false
|
|
|
|
var status_label: Label
|
|
var objective_label: Label
|
|
var campaign_status_label: Label
|
|
var selected_label: Label
|
|
var cell_info_label: Label
|
|
var forecast_label: Label
|
|
var inventory_label: Label
|
|
var briefing_panel: PanelContainer
|
|
var briefing_label: Label
|
|
var shop_button: Button
|
|
var shop_menu: VBoxContainer
|
|
var shop_list: VBoxContainer
|
|
var shop_status_label: Label
|
|
var armory_button: Button
|
|
var armory_menu: VBoxContainer
|
|
var armory_list: VBoxContainer
|
|
var armory_status_label: Label
|
|
var armory_unit_id := ""
|
|
var roster_button: Button
|
|
var roster_menu: VBoxContainer
|
|
var roster_list: VBoxContainer
|
|
var roster_status_label: Label
|
|
var formation_button: Button
|
|
var formation_menu: VBoxContainer
|
|
var formation_list: VBoxContainer
|
|
var formation_status_label: Label
|
|
var formation_unit_id := ""
|
|
var dialogue_panel: PanelContainer
|
|
var dialogue_speaker_label: Label
|
|
var dialogue_text_label: Label
|
|
var dialogue_continue_button: Button
|
|
var result_panel: PanelContainer
|
|
var result_label: Label
|
|
var result_choice_list: VBoxContainer
|
|
var result_restart_button: Button
|
|
var next_battle_button: Button
|
|
var end_turn_button: Button
|
|
var wait_button: Button
|
|
var tactic_button: Button
|
|
var tactic_menu: VBoxContainer
|
|
var tactic_list: VBoxContainer
|
|
var item_button: Button
|
|
var item_menu: VBoxContainer
|
|
var item_list: VBoxContainer
|
|
var equip_button: Button
|
|
var equip_menu: VBoxContainer
|
|
var equip_list: VBoxContainer
|
|
var restart_button: Button
|
|
var new_campaign_button: Button
|
|
var log_box: RichTextLabel
|
|
var dialogue_queue: Array = []
|
|
var active_dialogue_lines: Array = []
|
|
var active_dialogue_index := 0
|
|
var bgm_player: AudioStreamPlayer
|
|
var current_bgm_key := ""
|
|
var last_announced_battle_status := ""
|
|
|
|
|
|
func _ready() -> void:
|
|
_create_hud()
|
|
_create_audio()
|
|
state.changed.connect(_on_state_changed)
|
|
state.log_added.connect(_on_log_added)
|
|
state.dialogue_requested.connect(_on_dialogue_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():
|
|
_load_pending_post_battle_choice()
|
|
elif campaign_state.is_campaign_complete():
|
|
_show_campaign_complete()
|
|
else:
|
|
_load_current_battle()
|
|
_show_briefing()
|
|
_update_hud()
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if _is_dialogue_visible():
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
|
_advance_dialogue()
|
|
elif event is InputEventKey and event.pressed and not event.echo:
|
|
if event.keycode == KEY_ENTER or event.keycode == KEY_SPACE or event.keycode == KEY_ESCAPE:
|
|
_advance_dialogue()
|
|
return
|
|
|
|
if event is InputEventMouseMotion:
|
|
var next_hover := _cell_from_screen(event.position)
|
|
if next_hover != hover_cell:
|
|
hover_cell = next_hover
|
|
_update_hud()
|
|
queue_redraw()
|
|
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
|
_handle_board_click(event.position)
|
|
elif event is InputEventKey and event.pressed and not event.echo:
|
|
_handle_key(event)
|
|
|
|
|
|
func _draw() -> void:
|
|
_draw_map()
|
|
_draw_overlays()
|
|
_draw_units()
|
|
|
|
|
|
func _create_hud() -> void:
|
|
var layer := CanvasLayer.new()
|
|
add_child(layer)
|
|
|
|
var root := Control.new()
|
|
root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
layer.add_child(root)
|
|
|
|
var top_bar := PanelContainer.new()
|
|
top_bar.position = Vector2(24, 16)
|
|
top_bar.size = Vector2(1120, 64)
|
|
root.add_child(top_bar)
|
|
|
|
var top_row := HBoxContainer.new()
|
|
top_row.add_theme_constant_override("separation", 12)
|
|
top_bar.add_child(top_row)
|
|
|
|
status_label = Label.new()
|
|
status_label.custom_minimum_size = Vector2(180, 40)
|
|
top_row.add_child(status_label)
|
|
|
|
objective_label = Label.new()
|
|
objective_label.custom_minimum_size = Vector2(300, 40)
|
|
top_row.add_child(objective_label)
|
|
|
|
campaign_status_label = Label.new()
|
|
campaign_status_label.custom_minimum_size = Vector2(300, 40)
|
|
top_row.add_child(campaign_status_label)
|
|
|
|
restart_button = Button.new()
|
|
restart_button.text = "Restart"
|
|
restart_button.pressed.connect(_on_restart_pressed)
|
|
top_row.add_child(restart_button)
|
|
|
|
new_campaign_button = Button.new()
|
|
new_campaign_button.text = "New Campaign"
|
|
new_campaign_button.pressed.connect(_on_new_campaign_pressed)
|
|
top_row.add_child(new_campaign_button)
|
|
|
|
var side_panel := PanelContainer.new()
|
|
side_panel.position = Vector2(760, 104)
|
|
side_panel.size = Vector2(480, 580)
|
|
root.add_child(side_panel)
|
|
|
|
var side_column := VBoxContainer.new()
|
|
side_column.add_theme_constant_override("separation", 10)
|
|
side_panel.add_child(side_column)
|
|
|
|
selected_label = Label.new()
|
|
selected_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
selected_label.custom_minimum_size = Vector2(420, 72)
|
|
side_column.add_child(selected_label)
|
|
|
|
cell_info_label = Label.new()
|
|
cell_info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
cell_info_label.custom_minimum_size = Vector2(420, 90)
|
|
side_column.add_child(cell_info_label)
|
|
|
|
forecast_label = Label.new()
|
|
forecast_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
forecast_label.custom_minimum_size = Vector2(420, 84)
|
|
side_column.add_child(forecast_label)
|
|
|
|
inventory_label = Label.new()
|
|
inventory_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
inventory_label.custom_minimum_size = Vector2(420, 40)
|
|
side_column.add_child(inventory_label)
|
|
|
|
var button_row := HBoxContainer.new()
|
|
button_row.add_theme_constant_override("separation", 8)
|
|
side_column.add_child(button_row)
|
|
|
|
wait_button = Button.new()
|
|
wait_button.text = "Wait"
|
|
wait_button.pressed.connect(_on_wait_pressed)
|
|
button_row.add_child(wait_button)
|
|
|
|
tactic_button = Button.new()
|
|
tactic_button.text = "Tactic"
|
|
tactic_button.pressed.connect(_on_tactic_pressed)
|
|
button_row.add_child(tactic_button)
|
|
|
|
item_button = Button.new()
|
|
item_button.text = "Item"
|
|
item_button.pressed.connect(_on_item_pressed)
|
|
button_row.add_child(item_button)
|
|
|
|
equip_button = Button.new()
|
|
equip_button.text = "Equip"
|
|
equip_button.pressed.connect(_on_equip_pressed)
|
|
button_row.add_child(equip_button)
|
|
|
|
end_turn_button = Button.new()
|
|
end_turn_button.text = "End Turn"
|
|
end_turn_button.pressed.connect(_on_end_turn_pressed)
|
|
button_row.add_child(end_turn_button)
|
|
|
|
tactic_menu = VBoxContainer.new()
|
|
tactic_menu.visible = false
|
|
tactic_menu.add_theme_constant_override("separation", 6)
|
|
side_column.add_child(tactic_menu)
|
|
|
|
var tactic_title := Label.new()
|
|
tactic_title.text = "Tactics"
|
|
tactic_menu.add_child(tactic_title)
|
|
|
|
tactic_list = VBoxContainer.new()
|
|
tactic_list.add_theme_constant_override("separation", 4)
|
|
tactic_menu.add_child(tactic_list)
|
|
|
|
item_menu = VBoxContainer.new()
|
|
item_menu.visible = false
|
|
item_menu.add_theme_constant_override("separation", 6)
|
|
side_column.add_child(item_menu)
|
|
|
|
var item_title := Label.new()
|
|
item_title.text = "Items"
|
|
item_menu.add_child(item_title)
|
|
|
|
item_list = VBoxContainer.new()
|
|
item_list.add_theme_constant_override("separation", 4)
|
|
item_menu.add_child(item_list)
|
|
|
|
equip_menu = VBoxContainer.new()
|
|
equip_menu.visible = false
|
|
equip_menu.add_theme_constant_override("separation", 6)
|
|
side_column.add_child(equip_menu)
|
|
|
|
var equip_title := Label.new()
|
|
equip_title.text = "Equipment"
|
|
equip_menu.add_child(equip_title)
|
|
|
|
equip_list = VBoxContainer.new()
|
|
equip_list.add_theme_constant_override("separation", 4)
|
|
equip_menu.add_child(equip_list)
|
|
|
|
log_box = RichTextLabel.new()
|
|
log_box.custom_minimum_size = Vector2(420, 170)
|
|
log_box.fit_content = false
|
|
side_column.add_child(log_box)
|
|
|
|
briefing_panel = PanelContainer.new()
|
|
briefing_panel.visible = false
|
|
briefing_panel.position = Vector2(284, 88)
|
|
briefing_panel.size = Vector2(640, 500)
|
|
root.add_child(briefing_panel)
|
|
|
|
var briefing_column := VBoxContainer.new()
|
|
briefing_column.add_theme_constant_override("separation", 10)
|
|
briefing_panel.add_child(briefing_column)
|
|
|
|
briefing_label = Label.new()
|
|
briefing_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
briefing_label.custom_minimum_size = Vector2(600, 170)
|
|
briefing_column.add_child(briefing_label)
|
|
|
|
var prep_button_row := HBoxContainer.new()
|
|
prep_button_row.add_theme_constant_override("separation", 8)
|
|
briefing_column.add_child(prep_button_row)
|
|
|
|
shop_button = Button.new()
|
|
shop_button.text = "Shop"
|
|
shop_button.pressed.connect(_on_shop_pressed)
|
|
prep_button_row.add_child(shop_button)
|
|
|
|
armory_button = Button.new()
|
|
armory_button.text = "Armory"
|
|
armory_button.pressed.connect(_on_armory_pressed)
|
|
prep_button_row.add_child(armory_button)
|
|
|
|
roster_button = Button.new()
|
|
roster_button.text = "Roster"
|
|
roster_button.pressed.connect(_on_roster_pressed)
|
|
prep_button_row.add_child(roster_button)
|
|
|
|
formation_button = Button.new()
|
|
formation_button.text = "Formation"
|
|
formation_button.pressed.connect(_on_formation_pressed)
|
|
prep_button_row.add_child(formation_button)
|
|
|
|
shop_menu = VBoxContainer.new()
|
|
shop_menu.visible = false
|
|
shop_menu.add_theme_constant_override("separation", 6)
|
|
briefing_column.add_child(shop_menu)
|
|
|
|
shop_status_label = Label.new()
|
|
shop_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
shop_status_label.custom_minimum_size = Vector2(600, 22)
|
|
shop_menu.add_child(shop_status_label)
|
|
|
|
var shop_scroll := ScrollContainer.new()
|
|
shop_scroll.custom_minimum_size = Vector2(600, 190)
|
|
shop_menu.add_child(shop_scroll)
|
|
|
|
shop_list = VBoxContainer.new()
|
|
shop_list.custom_minimum_size = Vector2(580, 0)
|
|
shop_list.add_theme_constant_override("separation", 4)
|
|
shop_scroll.add_child(shop_list)
|
|
|
|
armory_menu = VBoxContainer.new()
|
|
armory_menu.visible = false
|
|
armory_menu.add_theme_constant_override("separation", 6)
|
|
briefing_column.add_child(armory_menu)
|
|
|
|
armory_status_label = Label.new()
|
|
armory_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
armory_status_label.custom_minimum_size = Vector2(600, 22)
|
|
armory_menu.add_child(armory_status_label)
|
|
|
|
var armory_scroll := ScrollContainer.new()
|
|
armory_scroll.custom_minimum_size = Vector2(600, 190)
|
|
armory_menu.add_child(armory_scroll)
|
|
|
|
armory_list = VBoxContainer.new()
|
|
armory_list.custom_minimum_size = Vector2(580, 0)
|
|
armory_list.add_theme_constant_override("separation", 4)
|
|
armory_scroll.add_child(armory_list)
|
|
|
|
roster_menu = VBoxContainer.new()
|
|
roster_menu.visible = false
|
|
roster_menu.add_theme_constant_override("separation", 6)
|
|
briefing_column.add_child(roster_menu)
|
|
|
|
roster_status_label = Label.new()
|
|
roster_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
roster_status_label.custom_minimum_size = Vector2(600, 22)
|
|
roster_menu.add_child(roster_status_label)
|
|
|
|
var roster_scroll := ScrollContainer.new()
|
|
roster_scroll.custom_minimum_size = Vector2(600, 190)
|
|
roster_menu.add_child(roster_scroll)
|
|
|
|
roster_list = VBoxContainer.new()
|
|
roster_list.custom_minimum_size = Vector2(580, 0)
|
|
roster_list.add_theme_constant_override("separation", 4)
|
|
roster_scroll.add_child(roster_list)
|
|
|
|
formation_menu = VBoxContainer.new()
|
|
formation_menu.visible = false
|
|
formation_menu.add_theme_constant_override("separation", 6)
|
|
briefing_column.add_child(formation_menu)
|
|
|
|
formation_status_label = Label.new()
|
|
formation_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
formation_status_label.custom_minimum_size = Vector2(600, 22)
|
|
formation_menu.add_child(formation_status_label)
|
|
|
|
var formation_scroll := ScrollContainer.new()
|
|
formation_scroll.custom_minimum_size = Vector2(600, 190)
|
|
formation_menu.add_child(formation_scroll)
|
|
|
|
formation_list = VBoxContainer.new()
|
|
formation_list.custom_minimum_size = Vector2(580, 0)
|
|
formation_list.add_theme_constant_override("separation", 4)
|
|
formation_scroll.add_child(formation_list)
|
|
|
|
var begin_button := Button.new()
|
|
begin_button.text = "Begin Battle"
|
|
begin_button.pressed.connect(_on_begin_battle_pressed)
|
|
briefing_column.add_child(begin_button)
|
|
|
|
dialogue_panel = PanelContainer.new()
|
|
dialogue_panel.visible = false
|
|
dialogue_panel.position = Vector2(128, 474)
|
|
dialogue_panel.size = Vector2(760, 140)
|
|
root.add_child(dialogue_panel)
|
|
|
|
var dialogue_column := VBoxContainer.new()
|
|
dialogue_column.add_theme_constant_override("separation", 8)
|
|
dialogue_panel.add_child(dialogue_column)
|
|
|
|
dialogue_speaker_label = Label.new()
|
|
dialogue_speaker_label.custom_minimum_size = Vector2(720, 22)
|
|
dialogue_column.add_child(dialogue_speaker_label)
|
|
|
|
dialogue_text_label = Label.new()
|
|
dialogue_text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
dialogue_text_label.custom_minimum_size = Vector2(720, 56)
|
|
dialogue_column.add_child(dialogue_text_label)
|
|
|
|
dialogue_continue_button = Button.new()
|
|
dialogue_continue_button.text = "Next"
|
|
dialogue_continue_button.pressed.connect(_advance_dialogue)
|
|
dialogue_column.add_child(dialogue_continue_button)
|
|
|
|
result_panel = PanelContainer.new()
|
|
result_panel.visible = false
|
|
result_panel.position = Vector2(300, 182)
|
|
result_panel.size = Vector2(500, 340)
|
|
root.add_child(result_panel)
|
|
|
|
var result_column := VBoxContainer.new()
|
|
result_column.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
result_panel.add_child(result_column)
|
|
|
|
result_label = Label.new()
|
|
result_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
result_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
result_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
result_label.custom_minimum_size = Vector2(460, 176)
|
|
result_column.add_child(result_label)
|
|
|
|
result_choice_list = VBoxContainer.new()
|
|
result_choice_list.add_theme_constant_override("separation", 6)
|
|
result_column.add_child(result_choice_list)
|
|
|
|
result_restart_button = Button.new()
|
|
result_restart_button.text = "Restart Battle"
|
|
result_restart_button.pressed.connect(_on_restart_pressed)
|
|
result_column.add_child(result_restart_button)
|
|
|
|
next_battle_button = Button.new()
|
|
next_battle_button.text = "Next Battle"
|
|
next_battle_button.pressed.connect(_on_next_battle_pressed)
|
|
result_column.add_child(next_battle_button)
|
|
|
|
|
|
func _create_audio() -> void:
|
|
bgm_player = AudioStreamPlayer.new()
|
|
bgm_player.name = "BgmPlayer"
|
|
bgm_player.volume_db = BGM_VOLUME_DB
|
|
bgm_player.finished.connect(_on_bgm_finished)
|
|
add_child(bgm_player)
|
|
|
|
|
|
func _on_bgm_finished() -> void:
|
|
if current_bgm_key.is_empty() or bgm_player == null or bgm_player.stream == null:
|
|
return
|
|
bgm_player.play()
|
|
|
|
|
|
func _play_bgm(key: String) -> void:
|
|
if bgm_player == null:
|
|
return
|
|
if current_bgm_key == key and bgm_player.playing:
|
|
return
|
|
var stream = _bgm_stream_for(key)
|
|
if stream == null:
|
|
return
|
|
current_bgm_key = key
|
|
bgm_player.stop()
|
|
bgm_player.stream = stream
|
|
bgm_player.play()
|
|
|
|
|
|
func _bgm_stream_for(key: String):
|
|
if key == "battle":
|
|
return BGM_BATTLE
|
|
if key == "menu":
|
|
return BGM_MENU
|
|
return null
|
|
|
|
|
|
func _play_sfx(key: String, volume_db := SFX_VOLUME_DB) -> void:
|
|
var stream = _sfx_stream_for(key)
|
|
if stream == null:
|
|
return
|
|
var player := AudioStreamPlayer.new()
|
|
player.name = "Sfx_%s" % key
|
|
player.stream = stream
|
|
player.volume_db = volume_db
|
|
player.finished.connect(player.queue_free)
|
|
add_child(player)
|
|
player.play()
|
|
|
|
|
|
func _sfx_stream_for(key: String):
|
|
if key == "bow_release":
|
|
return SFX_BOW_RELEASE
|
|
if key == "defeat_sting":
|
|
return SFX_DEFEAT_STING
|
|
if key == "fire_skill":
|
|
return SFX_FIRE_SKILL
|
|
if key == "footstep":
|
|
return SFX_FOOTSTEP
|
|
if key == "guard_block":
|
|
return SFX_GUARD_BLOCK
|
|
if key == "hit_heavy":
|
|
return SFX_HIT_HEAVY
|
|
if key == "item_pickup":
|
|
return SFX_ITEM_PICKUP
|
|
if key == "menu_cancel":
|
|
return SFX_MENU_CANCEL
|
|
if key == "menu_confirm":
|
|
return SFX_MENU_CONFIRM
|
|
if key == "skill_cast":
|
|
return SFX_SKILL_CAST
|
|
if key == "slash":
|
|
return SFX_SLASH
|
|
if key == "ui_click":
|
|
return SFX_UI_CLICK
|
|
if key == "victory_sting":
|
|
return SFX_VICTORY_STING
|
|
return null
|
|
|
|
|
|
func _play_ui_click() -> void:
|
|
_play_sfx("ui_click", UI_SFX_VOLUME_DB)
|
|
|
|
|
|
func _play_ui_confirm() -> void:
|
|
_play_sfx("menu_confirm", UI_SFX_VOLUME_DB)
|
|
|
|
|
|
func _play_ui_cancel() -> void:
|
|
_play_sfx("menu_cancel", UI_SFX_VOLUME_DB)
|
|
|
|
|
|
func _play_log_sfx(message: String) -> void:
|
|
if message.contains(" but misses."):
|
|
_play_sfx("guard_block")
|
|
elif message.contains(" attacks ") or message.contains(" counterattacks "):
|
|
if message.contains(" for "):
|
|
_play_sfx("hit_heavy")
|
|
elif message.contains(" casts "):
|
|
if message.contains("restoring"):
|
|
_play_sfx("skill_cast")
|
|
else:
|
|
_play_sfx("fire_skill")
|
|
elif message.contains(" moved to ") or message.contains(" advances to ") or message.contains(" takes formation at ") or message.contains(" arrives at "):
|
|
_play_sfx("footstep")
|
|
elif message.contains(" uses ") or message.contains(" equips ") or message.begins_with("Bought "):
|
|
_play_sfx("item_pickup")
|
|
elif message.contains(" reaches Lv.") or message.contains(" promotes to "):
|
|
_play_sfx("menu_confirm")
|
|
|
|
|
|
func _announce_battle_status_audio() -> void:
|
|
if state.battle_status == BattleState.STATUS_ACTIVE:
|
|
last_announced_battle_status = ""
|
|
return
|
|
if last_announced_battle_status == state.battle_status:
|
|
return
|
|
last_announced_battle_status = state.battle_status
|
|
_play_bgm("menu")
|
|
if state.battle_status == BattleState.STATUS_VICTORY:
|
|
_play_sfx("victory_sting")
|
|
elif state.battle_status == BattleState.STATUS_DEFEAT:
|
|
_play_sfx("defeat_sting")
|
|
|
|
|
|
func _handle_board_click(screen_position: Vector2) -> void:
|
|
if formation_menu != null and formation_menu.visible:
|
|
_handle_formation_board_click(screen_position)
|
|
return
|
|
if _is_input_locked():
|
|
return
|
|
|
|
var cell := _cell_from_screen(screen_position)
|
|
if tactic_menu != null and tactic_menu.visible:
|
|
_hide_tactic_menu()
|
|
_update_hud()
|
|
queue_redraw()
|
|
if item_menu != null and item_menu.visible:
|
|
_hide_item_menu()
|
|
_update_hud()
|
|
queue_redraw()
|
|
if equip_menu != null and equip_menu.visible:
|
|
_hide_equip_menu()
|
|
_update_hud()
|
|
queue_redraw()
|
|
if not state.is_inside(cell) or not state.can_player_act():
|
|
return
|
|
|
|
var clicked_unit := state.get_unit_at(cell)
|
|
var selected_unit := state.get_selected_unit()
|
|
|
|
if selected_unit.is_empty():
|
|
if _can_select(clicked_unit):
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
state.select_unit(clicked_unit["id"])
|
|
return
|
|
|
|
if not selected_item_id.is_empty():
|
|
if state.try_use_selected_item_on_cell(selected_item_id, cell):
|
|
selected_item_id = ""
|
|
return
|
|
if not clicked_unit.is_empty() and clicked_unit.get("team", "") == selected_unit.get("team", "") and _can_select(clicked_unit):
|
|
selected_item_id = ""
|
|
state.select_unit(clicked_unit["id"])
|
|
return
|
|
|
|
if not selected_skill_id.is_empty():
|
|
if state.try_cast_selected_skill(selected_skill_id, cell):
|
|
selected_skill_id = ""
|
|
return
|
|
if not clicked_unit.is_empty() and clicked_unit.get("team", "") == selected_unit.get("team", "") and _can_select(clicked_unit):
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
state.select_unit(clicked_unit["id"])
|
|
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 = ""
|
|
return
|
|
|
|
if not clicked_unit.is_empty() and clicked_unit.get("team", "") == selected_unit.get("team", ""):
|
|
if _can_select(clicked_unit):
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
state.select_unit(clicked_unit["id"])
|
|
return
|
|
|
|
if state.try_move_selected(cell):
|
|
return
|
|
|
|
if _can_select(clicked_unit):
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
state.select_unit(clicked_unit["id"])
|
|
|
|
|
|
func _handle_key(event: InputEventKey) -> void:
|
|
if event.keycode == KEY_ENTER and briefing_panel != null and briefing_panel.visible:
|
|
if _is_prep_menu_visible():
|
|
return
|
|
_on_begin_battle_pressed()
|
|
elif event.keycode == KEY_ESCAPE:
|
|
if shop_menu != null and shop_menu.visible:
|
|
_hide_shop_menu()
|
|
elif armory_menu != null and armory_menu.visible:
|
|
_hide_armory_menu()
|
|
elif roster_menu != null and roster_menu.visible:
|
|
_hide_roster_menu()
|
|
elif formation_menu != null and formation_menu.visible:
|
|
_hide_formation_menu()
|
|
elif tactic_menu != null and tactic_menu.visible:
|
|
_hide_tactic_menu()
|
|
elif item_menu != null and item_menu.visible:
|
|
_hide_item_menu()
|
|
elif equip_menu != null and equip_menu.visible:
|
|
_hide_equip_menu()
|
|
elif selected_skill_id.is_empty() and selected_item_id.is_empty():
|
|
state.clear_selection()
|
|
else:
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
elif event.keycode == KEY_SPACE and not _is_input_locked():
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
state.end_player_turn()
|
|
elif event.keycode == KEY_R and not _is_prep_menu_visible():
|
|
_on_restart_pressed()
|
|
elif event.keycode == KEY_T and not _is_input_locked():
|
|
_on_tactic_pressed()
|
|
elif event.keycode == KEY_I and not _is_input_locked():
|
|
_on_item_pressed()
|
|
elif event.keycode == KEY_E and not _is_input_locked():
|
|
_on_equip_pressed()
|
|
|
|
|
|
func _can_select(unit: Dictionary) -> bool:
|
|
if unit.is_empty():
|
|
return false
|
|
return (
|
|
unit.get("team", "") == BattleState.TEAM_PLAYER
|
|
and bool(unit.get("controllable", true))
|
|
and not unit.get("acted", false)
|
|
)
|
|
|
|
|
|
func _draw_map() -> void:
|
|
for y in range(state.map_size.y):
|
|
for x in range(state.map_size.x):
|
|
var cell := Vector2i(x, y)
|
|
var rect := _rect_for_cell(cell)
|
|
draw_rect(rect, state.get_terrain_color(cell))
|
|
draw_rect(rect, GRID_COLOR, false, 1.0)
|
|
|
|
var board_rect := Rect2(BOARD_OFFSET, Vector2(state.map_size.x, state.map_size.y) * TILE_SIZE)
|
|
draw_rect(board_rect, Color(0.02, 0.025, 0.03), false, 3.0)
|
|
|
|
|
|
func _draw_overlays() -> void:
|
|
for cell in state.get_objective_cells():
|
|
var objective_rect := _rect_for_cell(cell)
|
|
draw_rect(objective_rect, OBJECTIVE_OVERLAY_COLOR)
|
|
draw_rect(objective_rect.grow(-5.0), Color(1.0, 0.88, 0.30, 0.82), false, 2.0)
|
|
|
|
for cell in move_cells:
|
|
draw_rect(_rect_for_cell(cell), Color(0.16, 0.55, 0.95, 0.28))
|
|
|
|
for cell in attack_cells:
|
|
draw_rect(_rect_for_cell(cell), Color(0.95, 0.22, 0.18, 0.24))
|
|
|
|
for cell in skill_cells:
|
|
draw_rect(_rect_for_cell(cell), SKILL_OVERLAY_COLOR)
|
|
|
|
for cell in item_cells:
|
|
draw_rect(_rect_for_cell(cell), ITEM_OVERLAY_COLOR)
|
|
|
|
if formation_menu != null and formation_menu.visible:
|
|
for cell in state.get_formation_cells():
|
|
draw_rect(_rect_for_cell(cell), FORMATION_OVERLAY_COLOR)
|
|
|
|
if state.is_inside(hover_cell):
|
|
var hover_color := Color(1.0, 1.0, 1.0, 0.22)
|
|
var hover_unit := state.get_unit_at(hover_cell)
|
|
var selected := state.get_selected_unit()
|
|
if formation_menu != null and formation_menu.visible and state.is_formation_cell(hover_cell):
|
|
hover_color = Color(1.0, 0.86, 0.35, 0.62)
|
|
elif not selected_skill_id.is_empty() and skill_cells.has(hover_cell):
|
|
hover_color = Color(0.72, 0.46, 1.0, 0.54)
|
|
elif not selected_item_id.is_empty() and item_cells.has(hover_cell):
|
|
hover_color = Color(0.36, 0.95, 0.42, 0.52)
|
|
elif not hover_unit.is_empty() and not selected.is_empty() and hover_unit.get("team", "") != selected.get("team", ""):
|
|
hover_color = Color(1.0, 0.34, 0.24, 0.45)
|
|
draw_rect(_rect_for_cell(hover_cell), hover_color, false, 3.0)
|
|
|
|
|
|
func _draw_units() -> void:
|
|
var font := ThemeDB.fallback_font
|
|
for unit in state.units:
|
|
if not unit.get("alive", false) or not unit.get("deployed", true):
|
|
continue
|
|
|
|
var rect := _rect_for_cell(unit["pos"])
|
|
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
|
|
|
|
draw_circle(center, 24, body_color)
|
|
draw_arc(center, 24, 0.0, TAU, 48, Color(0.03, 0.035, 0.04), 2.0)
|
|
|
|
var label := _short_name(String(unit["name"]))
|
|
draw_string(font, rect.position + Vector2(2, 25), label, HORIZONTAL_ALIGNMENT_CENTER, TILE_SIZE - 4, 13, Color.WHITE)
|
|
|
|
var hp_ratio := float(unit["hp"]) / float(unit["max_hp"])
|
|
var hp_back := Rect2(rect.position + Vector2(8, TILE_SIZE - 12), Vector2(TILE_SIZE - 16, 6))
|
|
var hp_front := Rect2(hp_back.position, Vector2(hp_back.size.x * hp_ratio, hp_back.size.y))
|
|
draw_rect(hp_back, Color(0.08, 0.08, 0.09))
|
|
draw_rect(hp_front, Color(0.28, 0.82, 0.36))
|
|
|
|
if unit.get("id", "") == state.selected_unit_id:
|
|
draw_arc(center, 29, 0.0, TAU, 48, Color(1.0, 0.92, 0.25), 3.0)
|
|
|
|
|
|
func _cell_from_screen(screen_position: Vector2) -> Vector2i:
|
|
var local := screen_position - BOARD_OFFSET
|
|
if local.x < 0 or local.y < 0:
|
|
return Vector2i(-1, -1)
|
|
return Vector2i(int(floor(local.x / TILE_SIZE)), int(floor(local.y / TILE_SIZE)))
|
|
|
|
|
|
func _rect_for_cell(cell: Vector2i) -> Rect2:
|
|
return Rect2(BOARD_OFFSET + Vector2(cell.x, cell.y) * TILE_SIZE, Vector2(TILE_SIZE, TILE_SIZE))
|
|
|
|
|
|
func _short_name(unit_name: String) -> String:
|
|
if unit_name.length() <= 9:
|
|
return unit_name
|
|
return unit_name.substr(0, 8) + "."
|
|
|
|
|
|
func _refresh_ranges() -> void:
|
|
move_cells.clear()
|
|
attack_cells.clear()
|
|
skill_cells.clear()
|
|
item_cells.clear()
|
|
var selected := state.get_selected_unit()
|
|
if selected.is_empty():
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
return
|
|
move_cells = state.get_movement_range(selected["id"])
|
|
if not selected_skill_id.is_empty():
|
|
skill_cells = state.get_skill_cells(selected["id"], selected_skill_id)
|
|
if skill_cells.is_empty():
|
|
selected_skill_id = ""
|
|
attack_cells = state.get_attack_cells(selected["id"])
|
|
elif not selected_item_id.is_empty():
|
|
item_cells = state.get_item_target_cells(selected["id"], selected_item_id)
|
|
if item_cells.is_empty():
|
|
selected_item_id = ""
|
|
attack_cells = state.get_attack_cells(selected["id"])
|
|
else:
|
|
attack_cells = state.get_attack_cells(selected["id"])
|
|
|
|
|
|
func _update_hud() -> void:
|
|
status_label.text = state.get_status_text()
|
|
objective_label.text = String(state.objectives.get("victory", "Defeat all enemies."))
|
|
campaign_status_label.text = campaign_state.get_progress_text()
|
|
_update_cell_info()
|
|
_update_forecast()
|
|
_update_result_panel()
|
|
if inventory_label != null:
|
|
inventory_label.text = _format_inventory_status_text()
|
|
|
|
var selected := state.get_selected_unit()
|
|
if selected.is_empty():
|
|
selected_label.text = "No unit selected."
|
|
wait_button.disabled = true
|
|
_update_tactic_button({})
|
|
_hide_tactic_menu()
|
|
_update_item_button({})
|
|
_hide_item_menu()
|
|
_update_equip_button({})
|
|
_hide_equip_menu()
|
|
else:
|
|
var roster_mark := " *" if selected.get("loaded_from_roster", false) else ""
|
|
selected_label.text = "%s%s Lv.%d %s EXP %d\nHP %d/%d MP %d/%d ATK %d DEF %d INT %d\nMOV %d RNG %d-%d" % [
|
|
selected["name"],
|
|
roster_mark,
|
|
selected["level"],
|
|
selected["class"],
|
|
selected.get("exp", 0),
|
|
selected["hp"],
|
|
selected["max_hp"],
|
|
selected.get("mp", 0),
|
|
selected.get("max_mp", 0),
|
|
selected["atk"],
|
|
selected["def"],
|
|
selected.get("int", 0),
|
|
selected["move"],
|
|
selected.get("min_range", 1),
|
|
selected["range"]
|
|
]
|
|
wait_button.disabled = selected.get("acted", false) or not state.can_player_act() or _is_input_locked()
|
|
_update_tactic_button(selected)
|
|
_update_item_button(selected)
|
|
_update_equip_button(selected)
|
|
if tactic_menu != null and tactic_menu.visible:
|
|
_rebuild_tactic_menu(selected)
|
|
if item_menu != null and item_menu.visible:
|
|
_rebuild_item_menu(selected)
|
|
if equip_menu != null and equip_menu.visible:
|
|
_rebuild_equip_menu(selected)
|
|
|
|
end_turn_button.disabled = not state.can_player_act() or _is_input_locked()
|
|
restart_button.disabled = campaign_complete_screen
|
|
|
|
|
|
func _update_cell_info() -> void:
|
|
if not state.is_inside(hover_cell):
|
|
cell_info_label.text = "Tile: -"
|
|
return
|
|
|
|
var summary := state.get_cell_summary(hover_cell)
|
|
var cell: Vector2i = summary["cell"]
|
|
var text := "Tile %d,%d %s\nMove cost %d DEF +%d AVO +%d%%" % [
|
|
cell.x + 1,
|
|
cell.y + 1,
|
|
summary["terrain"],
|
|
summary["move_cost"],
|
|
summary["defense"],
|
|
summary.get("avoid", 0)
|
|
]
|
|
if state.get_objective_cells().has(cell):
|
|
text += "\nObjective marker"
|
|
|
|
var unit: Dictionary = summary["unit"]
|
|
if not unit.is_empty():
|
|
var control_label := " Protected" if not bool(unit.get("controllable", true)) else ""
|
|
text += "\n%s%s %s Lv.%d\nHP %d/%d MP %d/%d ATK %d DEF %d INT %d RNG %d-%d" % [
|
|
unit["name"],
|
|
control_label,
|
|
str(unit["team"]).capitalize(),
|
|
unit["level"],
|
|
unit["hp"],
|
|
unit["max_hp"],
|
|
unit.get("mp", 0),
|
|
unit.get("max_mp", 0),
|
|
unit["atk"],
|
|
unit["def"],
|
|
unit.get("int", 0),
|
|
unit.get("min_range", 1),
|
|
unit["range"]
|
|
]
|
|
|
|
cell_info_label.text = text
|
|
|
|
|
|
func _update_forecast() -> void:
|
|
var selected := state.get_selected_unit()
|
|
if selected.is_empty() or not state.is_inside(hover_cell):
|
|
forecast_label.text = "Forecast: -"
|
|
return
|
|
|
|
if not selected_skill_id.is_empty():
|
|
_update_skill_forecast(selected)
|
|
return
|
|
if not selected_item_id.is_empty():
|
|
_update_item_forecast(selected)
|
|
return
|
|
|
|
var target := state.get_unit_at(hover_cell)
|
|
if target.is_empty() or target.get("team", "") == selected.get("team", ""):
|
|
forecast_label.text = "Forecast: -"
|
|
return
|
|
|
|
var preview := state.get_damage_preview(selected["id"], target["id"])
|
|
if preview.is_empty():
|
|
forecast_label.text = "Forecast: -"
|
|
return
|
|
|
|
var range_text := "ready" if preview["in_range"] else "out of range"
|
|
var defeat_text := " Defeat" if preview["would_defeat"] else ""
|
|
var counter_text := ""
|
|
if preview["counter_in_range"]:
|
|
var counter_defeat := " Defeat" if preview["counter_would_defeat"] else ""
|
|
counter_text = "\nCounter %d, Hit %d%%, your HP %d/%d.%s" % [
|
|
preview["counter_damage"],
|
|
preview.get("counter_hit_chance", 0),
|
|
preview["attacker_hp_after"],
|
|
selected["max_hp"],
|
|
counter_defeat
|
|
]
|
|
forecast_label.text = "Forecast: %s\n%d damage, Hit %d%%, target HP %d/%d.%s%s" % [
|
|
range_text,
|
|
preview["damage"],
|
|
preview.get("hit_chance", 100),
|
|
preview["target_hp_after"],
|
|
target["max_hp"],
|
|
defeat_text,
|
|
counter_text
|
|
]
|
|
|
|
|
|
func _update_skill_forecast(selected: Dictionary) -> void:
|
|
var preview := state.get_skill_preview(selected["id"], selected_skill_id, hover_cell)
|
|
if preview.is_empty():
|
|
forecast_label.text = "Forecast: -"
|
|
return
|
|
|
|
var target := state.get_unit_at(hover_cell)
|
|
var range_text := "ready" if preview["in_range"] else "out of range"
|
|
var mp_text := "" if preview["has_mp"] else " Not enough MP."
|
|
if target.is_empty() or not preview["valid_target"]:
|
|
forecast_label.text = "%s: %s, %d MP.%s\nNo valid target." % [
|
|
preview["skill_name"],
|
|
range_text,
|
|
preview["mp_cost"],
|
|
mp_text
|
|
]
|
|
return
|
|
|
|
if preview["kind"] == "heal":
|
|
forecast_label.text = "%s: %s, %d MP.%s\nRestore %d HP, target HP %d/%d." % [
|
|
preview["skill_name"],
|
|
range_text,
|
|
preview["mp_cost"],
|
|
mp_text,
|
|
preview["heal"],
|
|
preview["target_hp_after"],
|
|
target["max_hp"]
|
|
]
|
|
else:
|
|
var defeat_text := " Defeat" if preview["would_defeat"] else ""
|
|
forecast_label.text = "%s: %s, %d MP.%s\n%d damage, target HP %d/%d.%s" % [
|
|
preview["skill_name"],
|
|
range_text,
|
|
preview["mp_cost"],
|
|
mp_text,
|
|
preview["damage"],
|
|
preview["target_hp_after"],
|
|
target["max_hp"],
|
|
defeat_text
|
|
]
|
|
|
|
|
|
func _update_item_forecast(selected: Dictionary) -> void:
|
|
var preview := state.get_item_preview(selected["id"], selected_item_id, hover_cell)
|
|
if preview.is_empty():
|
|
forecast_label.text = "Forecast: -"
|
|
return
|
|
|
|
var target := state.get_unit_at(hover_cell)
|
|
var range_text := "ready" if preview["in_range"] else "out of range"
|
|
var count_text := "x%d" % int(preview.get("count", 0))
|
|
if target.is_empty() or not preview["valid_target"]:
|
|
forecast_label.text = "%s: %s, %s\nNo valid target." % [
|
|
preview["item_name"],
|
|
range_text,
|
|
count_text
|
|
]
|
|
return
|
|
|
|
var hp_heal := int(preview.get("heal", 0))
|
|
var mp_heal := int(preview.get("mp_heal", 0))
|
|
if hp_heal <= 0 and mp_heal <= 0:
|
|
forecast_label.text = "%s: %s, %s\nNo recovery needed." % [
|
|
preview["item_name"],
|
|
range_text,
|
|
count_text
|
|
]
|
|
return
|
|
|
|
var recovery_parts := []
|
|
if hp_heal > 0:
|
|
recovery_parts.append("Restore %d HP, target HP %d/%d" % [
|
|
hp_heal,
|
|
preview["target_hp_after"],
|
|
target["max_hp"]
|
|
])
|
|
if mp_heal > 0:
|
|
recovery_parts.append("Restore %d MP, target MP %d/%d" % [
|
|
mp_heal,
|
|
preview["target_mp_after"],
|
|
target.get("max_mp", 0)
|
|
])
|
|
|
|
forecast_label.text = "%s: %s, %s\n%s." % [
|
|
preview["item_name"],
|
|
range_text,
|
|
count_text,
|
|
_join_strings(recovery_parts, "; ")
|
|
]
|
|
|
|
|
|
func _update_result_panel() -> void:
|
|
if result_panel == null:
|
|
return
|
|
_announce_battle_status_audio()
|
|
if campaign_complete_screen:
|
|
_clear_dialogue()
|
|
_clear_result_choices()
|
|
result_panel.visible = true
|
|
if result_restart_button != null:
|
|
result_restart_button.visible = false
|
|
if next_battle_button != null:
|
|
next_battle_button.visible = false
|
|
result_label.text = "%s Complete\nAll current scenarios cleared.\nGold %d\nStart a new campaign to replay." % [
|
|
campaign_state.campaign_title,
|
|
campaign_state.gold
|
|
]
|
|
return
|
|
if state.battle_status == BattleState.STATUS_VICTORY:
|
|
if _try_show_post_battle_dialogue():
|
|
_clear_result_choices()
|
|
result_panel.visible = false
|
|
return
|
|
if _is_dialogue_visible():
|
|
_clear_result_choices()
|
|
result_panel.visible = false
|
|
return
|
|
_apply_battle_result_once()
|
|
result_panel.visible = true
|
|
_rebuild_result_choices()
|
|
if result_restart_button != null:
|
|
result_restart_button.visible = true
|
|
if next_battle_button != null:
|
|
next_battle_button.visible = not str(battle_result_summary.get("next_scenario_id", "")).is_empty() and bool(battle_result_summary.get("choice_applied", true)) and bool(battle_result_summary.get("saved", false))
|
|
result_label.text = "Victory\n%s\n%s" % [
|
|
state.objectives.get("victory", ""),
|
|
_format_battle_result_summary()
|
|
]
|
|
elif state.battle_status == BattleState.STATUS_DEFEAT:
|
|
_clear_dialogue()
|
|
_clear_result_choices()
|
|
result_panel.visible = true
|
|
if result_restart_button != null:
|
|
result_restart_button.visible = true
|
|
if next_battle_button != null:
|
|
next_battle_button.visible = false
|
|
result_label.text = "Defeat\n%s" % state.objectives.get("defeat", "")
|
|
else:
|
|
_clear_result_choices()
|
|
result_panel.visible = false
|
|
if result_restart_button != null:
|
|
result_restart_button.visible = true
|
|
if next_battle_button != null:
|
|
next_battle_button.visible = false
|
|
|
|
|
|
func _on_state_changed() -> void:
|
|
if state.get_selected_unit().is_empty():
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _on_log_added(message: String) -> void:
|
|
_play_log_sfx(message)
|
|
if log_box == null:
|
|
return
|
|
log_box.append_text(message + "\n")
|
|
log_box.scroll_to_line(max(0, log_box.get_line_count() - 1))
|
|
|
|
|
|
func _on_dialogue_requested(lines: Array) -> void:
|
|
var normalized_lines := _normalized_dialogue_lines(lines)
|
|
if normalized_lines.is_empty():
|
|
return
|
|
dialogue_queue.append(normalized_lines)
|
|
if _can_show_dialogue_now():
|
|
_show_next_dialogue()
|
|
|
|
|
|
func _normalized_dialogue_lines(lines: Array) -> Array:
|
|
var result := []
|
|
for line in lines:
|
|
if typeof(line) == TYPE_DICTIONARY:
|
|
var text := str(line.get("text", ""))
|
|
if text.is_empty():
|
|
continue
|
|
result.append({
|
|
"speaker": str(line.get("speaker", "")),
|
|
"text": text
|
|
})
|
|
elif typeof(line) == TYPE_STRING:
|
|
var text := str(line)
|
|
if text.is_empty():
|
|
continue
|
|
result.append({"speaker": "", "text": text})
|
|
return result
|
|
|
|
|
|
func _can_show_dialogue_now() -> bool:
|
|
if not battle_started or campaign_complete_screen:
|
|
return false
|
|
if state.battle_status != BattleState.STATUS_ACTIVE:
|
|
return false
|
|
if briefing_panel != null and briefing_panel.visible:
|
|
return false
|
|
if result_panel != null and result_panel.visible:
|
|
return false
|
|
return not _is_dialogue_visible()
|
|
|
|
|
|
func _show_next_dialogue() -> void:
|
|
if dialogue_panel == null or dialogue_queue.is_empty():
|
|
return
|
|
if _is_dialogue_visible():
|
|
return
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
_refresh_ranges()
|
|
active_dialogue_lines = dialogue_queue.pop_front()
|
|
active_dialogue_index = 0
|
|
_render_dialogue_line()
|
|
|
|
|
|
func _render_dialogue_line() -> void:
|
|
if active_dialogue_index < 0 or active_dialogue_index >= active_dialogue_lines.size():
|
|
_hide_dialogue_panel()
|
|
return
|
|
var line: Dictionary = active_dialogue_lines[active_dialogue_index]
|
|
var speaker := str(line.get("speaker", ""))
|
|
dialogue_speaker_label.text = speaker
|
|
dialogue_speaker_label.visible = not speaker.is_empty()
|
|
dialogue_text_label.text = str(line.get("text", ""))
|
|
dialogue_continue_button.text = "Close" if active_dialogue_index >= active_dialogue_lines.size() - 1 else "Next"
|
|
dialogue_panel.visible = true
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _advance_dialogue() -> void:
|
|
if not _is_dialogue_visible():
|
|
return
|
|
_play_ui_click()
|
|
active_dialogue_index += 1
|
|
if active_dialogue_index < active_dialogue_lines.size():
|
|
_render_dialogue_line()
|
|
return
|
|
_hide_dialogue_panel()
|
|
if _can_show_dialogue_now():
|
|
_show_next_dialogue()
|
|
else:
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _hide_dialogue_panel() -> void:
|
|
if dialogue_panel != null:
|
|
dialogue_panel.visible = false
|
|
active_dialogue_lines.clear()
|
|
active_dialogue_index = 0
|
|
|
|
|
|
func _clear_dialogue() -> void:
|
|
dialogue_queue.clear()
|
|
_hide_dialogue_panel()
|
|
|
|
|
|
func _is_dialogue_visible() -> bool:
|
|
return dialogue_panel != null and dialogue_panel.visible
|
|
|
|
|
|
func _on_wait_pressed() -> void:
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
if state.try_wait_selected():
|
|
_play_ui_confirm()
|
|
|
|
|
|
func _on_end_turn_pressed() -> void:
|
|
if state.can_player_act():
|
|
_play_ui_confirm()
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
state.end_player_turn()
|
|
|
|
|
|
func _on_tactic_pressed() -> void:
|
|
var selected := state.get_selected_unit()
|
|
if selected.is_empty():
|
|
return
|
|
_play_ui_click()
|
|
if tactic_menu != null and tactic_menu.visible:
|
|
_hide_tactic_menu()
|
|
else:
|
|
selected_item_id = ""
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
_show_tactic_menu(selected)
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _on_item_pressed() -> void:
|
|
var selected := state.get_selected_unit()
|
|
if selected.is_empty():
|
|
return
|
|
_play_ui_click()
|
|
if item_menu != null and item_menu.visible:
|
|
_hide_item_menu()
|
|
else:
|
|
selected_skill_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_equip_menu()
|
|
_show_item_menu(selected)
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _on_equip_pressed() -> void:
|
|
var selected := state.get_selected_unit()
|
|
if selected.is_empty():
|
|
return
|
|
if selected.get("moved", false) or selected.get("acted", false):
|
|
return
|
|
_play_ui_click()
|
|
if equip_menu != null and equip_menu.visible:
|
|
_hide_equip_menu()
|
|
else:
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_show_equip_menu(selected)
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _on_restart_pressed() -> void:
|
|
if campaign_complete_screen:
|
|
return
|
|
_play_ui_confirm()
|
|
if log_box != null:
|
|
log_box.clear()
|
|
battle_started = false
|
|
battle_result_applied = false
|
|
battle_result_summary.clear()
|
|
post_battle_dialogue_started = false
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
armory_unit_id = ""
|
|
formation_unit_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
_load_scenario(active_scenario_id)
|
|
_show_briefing()
|
|
|
|
|
|
func _load_current_battle() -> void:
|
|
_load_scenario(campaign_state.current_scenario_id)
|
|
|
|
|
|
func _load_pending_post_battle_choice() -> void:
|
|
_load_scenario(campaign_state.get_pending_post_battle_choice_scenario_id())
|
|
battle_started = true
|
|
battle_result_applied = true
|
|
post_battle_dialogue_started = true
|
|
battle_result_summary = campaign_state.get_pending_post_battle_choice_summary()
|
|
state.battle_status = BattleState.STATUS_VICTORY
|
|
last_announced_battle_status = BattleState.STATUS_VICTORY
|
|
_play_bgm("menu")
|
|
if briefing_panel != null:
|
|
briefing_panel.visible = false
|
|
if log_box != null:
|
|
log_box.append_text("Campaign choice pending.\n")
|
|
|
|
|
|
func _load_scenario(scenario_id: String) -> void:
|
|
campaign_complete_screen = false
|
|
active_scenario_id = scenario_id
|
|
last_announced_battle_status = ""
|
|
var scenario_path := campaign_state.get_scenario_path(active_scenario_id)
|
|
if scenario_path.is_empty():
|
|
push_error("Missing scenario path for %s." % active_scenario_id)
|
|
return
|
|
battle_started = false
|
|
battle_result_applied = false
|
|
battle_result_summary.clear()
|
|
post_battle_dialogue_started = false
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
_hide_shop_menu()
|
|
_hide_armory_menu()
|
|
_hide_roster_menu()
|
|
_hide_formation_menu()
|
|
_clear_dialogue()
|
|
state.load_battle(
|
|
scenario_path,
|
|
campaign_state.get_roster_overrides(),
|
|
campaign_state.get_inventory_snapshot(),
|
|
campaign_state.get_flags_snapshot(),
|
|
campaign_state.get_joined_officers_snapshot()
|
|
)
|
|
|
|
|
|
func _show_briefing() -> void:
|
|
_play_bgm("menu")
|
|
var briefing := state.get_briefing()
|
|
if briefing.is_empty():
|
|
_start_battle_from_briefing()
|
|
return
|
|
|
|
var body := ""
|
|
for line in briefing.get("lines", []):
|
|
if not body.is_empty():
|
|
body += "\n"
|
|
body += str(line)
|
|
|
|
briefing_label.text = "%s\n%s\n\n%s\n\nObjective: %s" % [
|
|
briefing.get("title", state.battle_name),
|
|
briefing.get("location", ""),
|
|
body,
|
|
state.objectives.get("victory", "")
|
|
]
|
|
briefing_panel.visible = true
|
|
battle_started = false
|
|
_hide_shop_menu()
|
|
_hide_armory_menu()
|
|
_hide_roster_menu()
|
|
_hide_formation_menu()
|
|
var prep_locked := _is_prebattle_prep_locked()
|
|
if shop_button != null:
|
|
shop_button.disabled = prep_locked or state.get_shop_item_ids().is_empty()
|
|
if armory_button != null:
|
|
armory_button.disabled = prep_locked or state.get_controllable_player_units().is_empty()
|
|
if roster_button != null:
|
|
roster_button.disabled = prep_locked or not state.has_deployment_roster() or state.get_player_units(true).is_empty()
|
|
if formation_button != null:
|
|
formation_button.disabled = prep_locked or state.get_formation_cells().is_empty()
|
|
|
|
|
|
func _on_begin_battle_pressed() -> void:
|
|
_play_ui_confirm()
|
|
_start_battle_from_briefing()
|
|
|
|
|
|
func _start_battle_from_briefing() -> void:
|
|
battle_started = true
|
|
_play_bgm("battle")
|
|
if briefing_panel != null:
|
|
briefing_panel.visible = false
|
|
_hide_shop_menu()
|
|
_hide_armory_menu()
|
|
_hide_roster_menu()
|
|
_hide_formation_menu()
|
|
state.run_battle_begin_events()
|
|
_show_next_dialogue()
|
|
_update_hud()
|
|
|
|
|
|
func _on_shop_pressed() -> void:
|
|
if battle_started or _is_prebattle_prep_locked() or briefing_panel == null or not briefing_panel.visible:
|
|
return
|
|
_play_ui_click()
|
|
if shop_menu != null and shop_menu.visible:
|
|
_hide_shop_menu()
|
|
else:
|
|
_hide_armory_menu()
|
|
_hide_roster_menu()
|
|
_hide_formation_menu()
|
|
_rebuild_shop_menu()
|
|
shop_menu.visible = true
|
|
_update_hud()
|
|
|
|
|
|
func _hide_shop_menu() -> void:
|
|
if shop_menu == null:
|
|
return
|
|
shop_menu.visible = false
|
|
|
|
|
|
func _on_armory_pressed() -> void:
|
|
if battle_started or _is_prebattle_prep_locked() or briefing_panel == null or not briefing_panel.visible:
|
|
return
|
|
_play_ui_click()
|
|
if armory_menu != null and armory_menu.visible:
|
|
_hide_armory_menu()
|
|
else:
|
|
_hide_shop_menu()
|
|
_hide_roster_menu()
|
|
_hide_formation_menu()
|
|
_rebuild_armory_menu()
|
|
armory_menu.visible = true
|
|
_update_hud()
|
|
|
|
|
|
func _hide_armory_menu() -> void:
|
|
if armory_menu == null:
|
|
return
|
|
armory_menu.visible = false
|
|
|
|
|
|
func _on_roster_pressed() -> void:
|
|
if battle_started or _is_prebattle_prep_locked() or not state.has_deployment_roster() or briefing_panel == null or not briefing_panel.visible:
|
|
return
|
|
_play_ui_click()
|
|
if roster_menu != null and roster_menu.visible:
|
|
_hide_roster_menu()
|
|
else:
|
|
_hide_shop_menu()
|
|
_hide_armory_menu()
|
|
_hide_formation_menu()
|
|
_rebuild_roster_menu()
|
|
roster_menu.visible = true
|
|
_update_hud()
|
|
|
|
|
|
func _hide_roster_menu() -> void:
|
|
if roster_menu == null:
|
|
return
|
|
roster_menu.visible = false
|
|
|
|
|
|
func _on_formation_pressed() -> void:
|
|
if battle_started or _is_prebattle_prep_locked() or briefing_panel == null or not briefing_panel.visible:
|
|
return
|
|
_play_ui_click()
|
|
if formation_menu != null and formation_menu.visible:
|
|
_hide_formation_menu()
|
|
else:
|
|
_hide_shop_menu()
|
|
_hide_armory_menu()
|
|
_hide_roster_menu()
|
|
_rebuild_formation_menu()
|
|
formation_menu.visible = true
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _hide_formation_menu() -> void:
|
|
if formation_menu == null:
|
|
return
|
|
formation_menu.visible = false
|
|
|
|
|
|
func _is_prep_menu_visible() -> bool:
|
|
return (
|
|
(shop_menu != null and shop_menu.visible)
|
|
or (armory_menu != null and armory_menu.visible)
|
|
or (roster_menu != null and roster_menu.visible)
|
|
or (formation_menu != null and formation_menu.visible)
|
|
)
|
|
|
|
|
|
func _is_prebattle_prep_locked() -> bool:
|
|
return campaign_state.is_scenario_completed(active_scenario_id)
|
|
|
|
|
|
func _rebuild_roster_menu() -> void:
|
|
if roster_list == null:
|
|
return
|
|
_clear_roster_list()
|
|
var units := state.get_player_units(true)
|
|
var max_units := state.get_deployment_max_units()
|
|
var deployed_count := state.get_deployed_player_count()
|
|
if roster_status_label != null:
|
|
roster_status_label.text = "Sortie %d/%d" % [deployed_count, max_units]
|
|
|
|
if units.is_empty():
|
|
var empty_label := Label.new()
|
|
empty_label.text = "No officers"
|
|
roster_list.add_child(empty_label)
|
|
return
|
|
|
|
for unit in units:
|
|
var unit_id := str(unit.get("id", ""))
|
|
var roster_unit_button := Button.new()
|
|
roster_unit_button.text = _format_roster_unit_button_text(unit)
|
|
var deployed := bool(unit.get("deployed", true))
|
|
roster_unit_button.disabled = (
|
|
bool(unit.get("required_deployment", false))
|
|
or (not deployed and deployed_count >= max_units)
|
|
)
|
|
roster_unit_button.pressed.connect(_on_roster_unit_pressed.bind(unit_id))
|
|
roster_list.add_child(roster_unit_button)
|
|
|
|
|
|
func _clear_roster_list() -> void:
|
|
for child in roster_list.get_children():
|
|
roster_list.remove_child(child)
|
|
child.queue_free()
|
|
|
|
|
|
func _format_roster_unit_button_text(unit: Dictionary) -> String:
|
|
var state_text := "Deploy" if bool(unit.get("deployed", true)) else "Reserve"
|
|
var required_text := " Required" if bool(unit.get("required_deployment", false)) else ""
|
|
var protected_text := " Protected" if not bool(unit.get("controllable", true)) else ""
|
|
return "%s%s Lv.%d %s %s%s" % [
|
|
str(unit.get("name", "Officer")),
|
|
protected_text,
|
|
int(unit.get("level", 1)),
|
|
str(unit.get("class", "")),
|
|
state_text,
|
|
required_text
|
|
]
|
|
|
|
|
|
func _on_roster_unit_pressed(unit_id: String) -> void:
|
|
var unit := state.get_unit(unit_id)
|
|
if unit.is_empty():
|
|
return
|
|
var next_deployed := not bool(unit.get("deployed", true))
|
|
if state.try_set_unit_deployed(unit_id, next_deployed):
|
|
_play_ui_confirm()
|
|
if not state.get_unit(formation_unit_id).get("deployed", false):
|
|
formation_unit_id = ""
|
|
if not state.get_unit(armory_unit_id).get("deployed", false):
|
|
armory_unit_id = ""
|
|
_rebuild_roster_menu()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _rebuild_formation_menu() -> void:
|
|
if formation_list == null:
|
|
return
|
|
_clear_formation_list()
|
|
var units := state.get_controllable_player_units()
|
|
if formation_unit_id.is_empty() or _find_unit_in_list(units, formation_unit_id).is_empty():
|
|
formation_unit_id = str(units[0].get("id", "")) if not units.is_empty() else ""
|
|
|
|
if formation_status_label != null:
|
|
var selected := state.get_unit(formation_unit_id)
|
|
if selected.is_empty():
|
|
formation_status_label.text = "Formation"
|
|
else:
|
|
formation_status_label.text = "Formation: %s at %s" % [
|
|
str(selected.get("name", "Officer")),
|
|
_format_cell_label(selected.get("pos", Vector2i.ZERO))
|
|
]
|
|
|
|
if units.is_empty():
|
|
var empty_label := Label.new()
|
|
empty_label.text = "No deployed officers"
|
|
formation_list.add_child(empty_label)
|
|
return
|
|
|
|
for unit in units:
|
|
var unit_id := str(unit.get("id", ""))
|
|
var unit_button := Button.new()
|
|
unit_button.text = _format_formation_unit_button_text(unit)
|
|
unit_button.disabled = unit_id == formation_unit_id
|
|
unit_button.pressed.connect(_on_formation_unit_pressed.bind(unit_id))
|
|
formation_list.add_child(unit_button)
|
|
|
|
|
|
func _clear_formation_list() -> void:
|
|
for child in formation_list.get_children():
|
|
formation_list.remove_child(child)
|
|
child.queue_free()
|
|
|
|
|
|
func _format_formation_unit_button_text(unit: Dictionary) -> String:
|
|
var marker := "> " if str(unit.get("id", "")) == formation_unit_id else ""
|
|
return "%s%s %s" % [
|
|
marker,
|
|
str(unit.get("name", "Officer")),
|
|
_format_cell_label(unit.get("pos", Vector2i.ZERO))
|
|
]
|
|
|
|
|
|
func _format_cell_label(value) -> String:
|
|
if typeof(value) == TYPE_VECTOR2I:
|
|
return "%d,%d" % [value.x + 1, value.y + 1]
|
|
return "-"
|
|
|
|
|
|
func _on_formation_unit_pressed(unit_id: String) -> void:
|
|
_play_ui_click()
|
|
formation_unit_id = unit_id
|
|
_rebuild_formation_menu()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _handle_formation_board_click(screen_position: Vector2) -> void:
|
|
if battle_started or _is_prebattle_prep_locked() or formation_unit_id.is_empty():
|
|
return
|
|
var cell := _cell_from_screen(screen_position)
|
|
if not state.is_inside(cell):
|
|
return
|
|
if state.try_set_prebattle_formation(formation_unit_id, cell):
|
|
_play_ui_confirm()
|
|
_rebuild_formation_menu()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _rebuild_armory_menu() -> void:
|
|
if armory_list == null:
|
|
return
|
|
_clear_armory_list()
|
|
var units := state.get_controllable_player_units()
|
|
if armory_unit_id.is_empty() or _find_unit_in_list(units, armory_unit_id).is_empty():
|
|
armory_unit_id = str(units[0].get("id", "")) if not units.is_empty() else ""
|
|
|
|
if armory_status_label != null:
|
|
armory_status_label.text = _format_inventory_status_text()
|
|
|
|
if units.is_empty():
|
|
var empty_label := Label.new()
|
|
empty_label.text = "No deployed officers"
|
|
armory_list.add_child(empty_label)
|
|
return
|
|
|
|
var unit_title := Label.new()
|
|
unit_title.text = "Officers"
|
|
armory_list.add_child(unit_title)
|
|
|
|
for unit in units:
|
|
var unit_id := str(unit.get("id", ""))
|
|
var unit_button := Button.new()
|
|
unit_button.text = _format_armory_unit_button_text(unit)
|
|
unit_button.disabled = unit_id == armory_unit_id
|
|
unit_button.pressed.connect(_on_armory_unit_pressed.bind(unit_id))
|
|
armory_list.add_child(unit_button)
|
|
|
|
var selected := state.get_unit(armory_unit_id)
|
|
if selected.is_empty():
|
|
return
|
|
|
|
var equipment_label := Label.new()
|
|
equipment_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
equipment_label.text = "Equipped: %s" % _format_unit_equipment(selected)
|
|
armory_list.add_child(equipment_label)
|
|
|
|
var item_ids := state.get_equippable_item_ids(armory_unit_id)
|
|
if item_ids.is_empty():
|
|
var empty_equipment_label := Label.new()
|
|
empty_equipment_label.text = "No compatible equipment in inventory"
|
|
armory_list.add_child(empty_equipment_label)
|
|
else:
|
|
var inventory := state.get_inventory_snapshot()
|
|
for item_id in item_ids:
|
|
var normalized_id := str(item_id)
|
|
var item := state.get_item_def(normalized_id)
|
|
var armory_equip_button := Button.new()
|
|
armory_equip_button.text = "%s x%d %s" % [
|
|
_item_display_name(normalized_id),
|
|
int(inventory.get(normalized_id, 0)),
|
|
_format_equipment_bonus_text(item)
|
|
]
|
|
armory_equip_button.pressed.connect(_on_armory_equip_pressed.bind(normalized_id))
|
|
armory_list.add_child(armory_equip_button)
|
|
|
|
|
|
func _clear_armory_list() -> void:
|
|
for child in armory_list.get_children():
|
|
armory_list.remove_child(child)
|
|
child.queue_free()
|
|
|
|
|
|
func _find_unit_in_list(units: Array, unit_id: String) -> Dictionary:
|
|
for unit in units:
|
|
if str(unit.get("id", "")) == unit_id:
|
|
return unit
|
|
return {}
|
|
|
|
|
|
func _format_armory_unit_button_text(unit: Dictionary) -> String:
|
|
var marker := "> " if str(unit.get("id", "")) == armory_unit_id else ""
|
|
return "%s%s Lv.%d %s" % [
|
|
marker,
|
|
str(unit.get("name", "Officer")),
|
|
int(unit.get("level", 1)),
|
|
_format_unit_equipment(unit)
|
|
]
|
|
|
|
|
|
func _on_armory_unit_pressed(unit_id: String) -> void:
|
|
_play_ui_click()
|
|
armory_unit_id = unit_id
|
|
_rebuild_armory_menu()
|
|
_update_hud()
|
|
|
|
|
|
func _on_armory_equip_pressed(item_id: String) -> void:
|
|
if battle_started or _is_prebattle_prep_locked() or armory_unit_id.is_empty():
|
|
return
|
|
if not state.try_equip_item(armory_unit_id, item_id):
|
|
_rebuild_armory_menu()
|
|
_update_hud()
|
|
return
|
|
if campaign_state.try_save_prebattle_loadout(state.get_player_roster_snapshot(), state.get_inventory_snapshot()):
|
|
_on_log_added("Saved pre-battle equipment.")
|
|
else:
|
|
_play_ui_cancel()
|
|
_on_log_added("Could not save pre-battle equipment.")
|
|
_load_scenario(active_scenario_id)
|
|
_show_briefing()
|
|
return
|
|
_rebuild_armory_menu()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _rebuild_shop_menu() -> void:
|
|
if shop_list == null:
|
|
return
|
|
_clear_shop_list()
|
|
if shop_status_label != null:
|
|
shop_status_label.text = "Gold %d | %s" % [
|
|
campaign_state.gold,
|
|
_format_inventory_status_text()
|
|
]
|
|
|
|
var item_ids := state.get_shop_item_ids()
|
|
if item_ids.is_empty():
|
|
var empty_label := Label.new()
|
|
empty_label.text = "No shop stock"
|
|
shop_list.add_child(empty_label)
|
|
return
|
|
|
|
var inventory := campaign_state.get_inventory_snapshot()
|
|
for item_id in item_ids:
|
|
var normalized_id := str(item_id)
|
|
var item := state.get_item_def(normalized_id)
|
|
var price := int(item.get("price", 0))
|
|
var owned := int(inventory.get(normalized_id, 0))
|
|
var buy_button := Button.new()
|
|
buy_button.text = _format_shop_item_button_text(normalized_id, item, price, owned)
|
|
buy_button.disabled = campaign_state.gold < price
|
|
buy_button.pressed.connect(_on_shop_item_pressed.bind(normalized_id))
|
|
shop_list.add_child(buy_button)
|
|
|
|
|
|
func _clear_shop_list() -> void:
|
|
for child in shop_list.get_children():
|
|
shop_list.remove_child(child)
|
|
child.queue_free()
|
|
|
|
|
|
func _format_shop_item_button_text(item_id: String, item: Dictionary, price: int, owned: int) -> String:
|
|
var unavailable := ""
|
|
if campaign_state.gold < price:
|
|
unavailable = " - need %dG" % (price - campaign_state.gold)
|
|
return "%s %dG %s Owned x%d%s" % [
|
|
_item_display_name(item_id),
|
|
price,
|
|
_format_shop_item_effect_text(item),
|
|
owned,
|
|
unavailable
|
|
]
|
|
|
|
|
|
func _format_shop_item_effect_text(item: Dictionary) -> String:
|
|
var kind := str(item.get("kind", "item"))
|
|
if kind == "consumable":
|
|
return _format_item_effect_text(item)
|
|
if kind == "weapon" or kind == "armor" or kind == "accessory":
|
|
var parts := []
|
|
var equipment_type := str(item.get("%s_type" % kind, ""))
|
|
if not equipment_type.is_empty():
|
|
parts.append(equipment_type.capitalize())
|
|
if item.has("range"):
|
|
var range_value: Array = item.get("range", [])
|
|
if range_value.size() >= 2:
|
|
parts.append("RNG %d-%d" % [int(range_value[0]), int(range_value[1])])
|
|
parts.append(_format_equipment_bonus_text(item))
|
|
return _join_strings(parts, ", ")
|
|
return kind.capitalize()
|
|
|
|
|
|
func _on_shop_item_pressed(item_id: String) -> void:
|
|
if battle_started or _is_prebattle_prep_locked():
|
|
return
|
|
var item := state.get_item_def(item_id)
|
|
var price := int(item.get("price", 0))
|
|
var item_name := _item_display_name(item_id)
|
|
if item.is_empty() or price <= 0:
|
|
return
|
|
if campaign_state.try_buy_item(item_id, price):
|
|
state.set_inventory_snapshot(campaign_state.get_inventory_snapshot())
|
|
_on_log_added("Bought %s for %d gold." % [item_name, price])
|
|
else:
|
|
_play_ui_cancel()
|
|
_on_log_added("Could not buy %s." % item_name)
|
|
_rebuild_shop_menu()
|
|
_update_hud()
|
|
|
|
|
|
func _is_input_locked() -> bool:
|
|
return campaign_complete_screen or _is_dialogue_visible() or not battle_started or state.battle_status != BattleState.STATUS_ACTIVE
|
|
|
|
|
|
func _apply_battle_result_once() -> void:
|
|
if battle_result_applied:
|
|
return
|
|
battle_result_summary = campaign_state.apply_battle_result(state)
|
|
battle_result_applied = true
|
|
|
|
|
|
func _try_show_post_battle_dialogue() -> bool:
|
|
if post_battle_dialogue_started:
|
|
return false
|
|
var lines := _normalized_dialogue_lines(state.get_post_battle_dialogue())
|
|
if lines.is_empty():
|
|
post_battle_dialogue_started = true
|
|
return false
|
|
post_battle_dialogue_started = true
|
|
dialogue_queue.clear()
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
_refresh_ranges()
|
|
active_dialogue_lines = lines
|
|
active_dialogue_index = 0
|
|
_render_dialogue_line()
|
|
return true
|
|
|
|
|
|
func _rebuild_result_choices() -> void:
|
|
if result_choice_list == null:
|
|
return
|
|
_clear_result_choices()
|
|
if bool(battle_result_summary.get("choice_applied", true)):
|
|
return
|
|
var choices: Array = battle_result_summary.get("post_battle_choices", [])
|
|
for choice in choices:
|
|
if typeof(choice) != TYPE_DICTIONARY:
|
|
continue
|
|
var choice_button := Button.new()
|
|
choice_button.text = _format_post_battle_choice_text(choice)
|
|
choice_button.pressed.connect(_on_post_battle_choice_pressed.bind(choice))
|
|
result_choice_list.add_child(choice_button)
|
|
|
|
|
|
func _clear_result_choices() -> void:
|
|
if result_choice_list == null:
|
|
return
|
|
for child in result_choice_list.get_children():
|
|
result_choice_list.remove_child(child)
|
|
child.queue_free()
|
|
|
|
|
|
func _format_post_battle_choice_text(choice: Dictionary) -> String:
|
|
var label := str(choice.get("label", choice.get("text", choice.get("id", "Choice"))))
|
|
var description := str(choice.get("description", ""))
|
|
if description.is_empty():
|
|
return label
|
|
return "%s - %s" % [label, description]
|
|
|
|
|
|
func _on_post_battle_choice_pressed(choice: Dictionary) -> void:
|
|
if battle_result_summary.is_empty() or bool(battle_result_summary.get("choice_applied", true)):
|
|
return
|
|
var scenario_id := str(battle_result_summary.get("scenario_id", state.battle_id))
|
|
if campaign_state.try_apply_post_battle_choice(choice, scenario_id):
|
|
_play_ui_confirm()
|
|
battle_result_summary["choice_applied"] = true
|
|
battle_result_summary["saved"] = true
|
|
battle_result_summary["choice_label"] = str(choice.get("label", choice.get("id", "")))
|
|
_append_unique_result_values("joined_officers", choice.get("join_officers", []))
|
|
_append_unique_result_values("left_officers", choice.get("leave_officers", []))
|
|
_on_log_added("Campaign choice saved: %s." % battle_result_summary["choice_label"])
|
|
else:
|
|
_play_ui_cancel()
|
|
_on_log_added("Could not save campaign choice.")
|
|
_rebuild_result_choices()
|
|
_update_hud()
|
|
|
|
|
|
func _append_unique_result_values(key: String, values) -> void:
|
|
if typeof(values) != TYPE_ARRAY:
|
|
return
|
|
var current: Array = battle_result_summary.get(key, [])
|
|
for value in values:
|
|
var normalized := str(value)
|
|
if normalized.is_empty() or current.has(normalized):
|
|
continue
|
|
current.append(normalized)
|
|
battle_result_summary[key] = current
|
|
|
|
|
|
func _format_battle_result_summary() -> String:
|
|
if battle_result_summary.is_empty():
|
|
return ""
|
|
if battle_result_summary.get("pending_choice", false):
|
|
var pending_next := str(battle_result_summary.get("next_scenario_title", ""))
|
|
var pending_text := "Campaign choice pending.\nCampaign saved."
|
|
if not pending_next.is_empty():
|
|
pending_text += "\nNext: %s" % pending_next
|
|
return pending_text
|
|
if battle_result_summary.get("already_completed", false):
|
|
var already_saved := "Campaign saved." if battle_result_summary.get("saved", false) else "Campaign save failed."
|
|
var already_next := str(battle_result_summary.get("next_scenario_title", ""))
|
|
if already_next.is_empty():
|
|
return "Rewards already claimed.\n%s" % already_saved
|
|
return "Rewards already claimed.\nNext: %s\n%s" % [already_next, already_saved]
|
|
|
|
var items: Array = battle_result_summary.get("items", [])
|
|
var item_text := _format_reward_items(items)
|
|
var joined_text := ""
|
|
var joined_officers: Array = battle_result_summary.get("joined_officers", [])
|
|
if not joined_officers.is_empty():
|
|
joined_text = "\nJoined: %s" % _join_strings(joined_officers, ", ")
|
|
var left_text := ""
|
|
var left_officers: Array = battle_result_summary.get("left_officers", [])
|
|
if not left_officers.is_empty():
|
|
left_text = "\nLeft: %s" % _join_strings(left_officers, ", ")
|
|
var progression_text := _format_progression_events(battle_result_summary.get("progression_events", []))
|
|
|
|
var saved_text := "Campaign saved." if battle_result_summary.get("saved", false) else "Campaign save failed."
|
|
var next_text := "Campaign complete."
|
|
var next_title := str(battle_result_summary.get("next_scenario_title", ""))
|
|
if not next_title.is_empty():
|
|
next_text = "Next: %s" % next_title
|
|
var choice_text := ""
|
|
if not bool(battle_result_summary.get("choice_applied", true)):
|
|
choice_text = "\nChoose a campaign response."
|
|
elif not str(battle_result_summary.get("choice_label", "")).is_empty():
|
|
choice_text = "\nChoice: %s" % str(battle_result_summary.get("choice_label", ""))
|
|
return "Rewards: %d gold, %s%s%s%s\nTotal gold: %d\n%s\n%s%s" % [
|
|
battle_result_summary.get("gold", 0),
|
|
item_text,
|
|
joined_text,
|
|
left_text,
|
|
progression_text,
|
|
campaign_state.gold,
|
|
saved_text,
|
|
next_text,
|
|
choice_text
|
|
]
|
|
|
|
|
|
func _format_progression_events(events) -> String:
|
|
if typeof(events) != TYPE_ARRAY:
|
|
return ""
|
|
var parts := []
|
|
var hidden_count := 0
|
|
for event in events:
|
|
if typeof(event) != TYPE_DICTIONARY:
|
|
continue
|
|
if parts.size() >= 4:
|
|
hidden_count += 1
|
|
continue
|
|
var event_type := str(event.get("type", ""))
|
|
var unit_name := str(event.get("name", "Unit"))
|
|
if event_type == "promotion":
|
|
parts.append("%s -> %s" % [unit_name, str(event.get("to_class", "Promoted"))])
|
|
elif event_type == "level_up":
|
|
parts.append("%s Lv.%d" % [unit_name, int(event.get("to_level", 1))])
|
|
if hidden_count > 0:
|
|
parts.append("+%d more" % hidden_count)
|
|
if parts.is_empty():
|
|
return ""
|
|
return "\nProgress: %s" % _join_strings(parts, "; ")
|
|
|
|
|
|
func _format_inventory_status_text() -> String:
|
|
var inventory := state.get_inventory_snapshot()
|
|
if campaign_complete_screen or state.battle_status != BattleState.STATUS_ACTIVE:
|
|
inventory = campaign_state.get_inventory_snapshot()
|
|
return "Inventory: %s" % _format_inventory_by_category(inventory)
|
|
|
|
|
|
func _format_reward_items(items: Array) -> String:
|
|
var counts := {}
|
|
for item_id in items:
|
|
var key := str(item_id)
|
|
if key.is_empty():
|
|
continue
|
|
counts[key] = int(counts.get(key, 0)) + 1
|
|
return _format_inventory_items(counts)
|
|
|
|
|
|
func _format_inventory_items(inventory: Dictionary) -> String:
|
|
var item_ids := []
|
|
for item_id in inventory.keys():
|
|
var key := str(item_id)
|
|
if key.is_empty() or int(inventory.get(key, 0)) <= 0:
|
|
continue
|
|
item_ids.append(key)
|
|
item_ids.sort()
|
|
if item_ids.is_empty():
|
|
return "none"
|
|
|
|
var parts := []
|
|
for item_id in item_ids:
|
|
var key := str(item_id)
|
|
parts.append("%s x%d" % [_item_display_name(key), int(inventory.get(key, 0))])
|
|
return _join_strings(parts, ", ")
|
|
|
|
|
|
func _format_inventory_by_category(inventory: Dictionary) -> String:
|
|
var consumables := {}
|
|
var equipment := {}
|
|
var other := {}
|
|
for item_id in inventory.keys():
|
|
var key := str(item_id)
|
|
var count := int(inventory.get(key, 0))
|
|
if key.is_empty() or count <= 0:
|
|
continue
|
|
var item := state.get_item_def(key)
|
|
var kind := str(item.get("kind", ""))
|
|
if kind == "consumable":
|
|
consumables[key] = count
|
|
elif kind == "weapon" or kind == "armor" or kind == "accessory":
|
|
equipment[key] = count
|
|
else:
|
|
other[key] = count
|
|
|
|
var sections := []
|
|
if not consumables.is_empty():
|
|
sections.append("Consumables: %s" % _format_inventory_items(consumables))
|
|
if not equipment.is_empty():
|
|
sections.append("Equipment: %s" % _format_inventory_items(equipment))
|
|
if not other.is_empty():
|
|
sections.append("Other: %s" % _format_inventory_items(other))
|
|
if sections.is_empty():
|
|
return "none"
|
|
return _join_strings(sections, " | ")
|
|
|
|
|
|
func _item_display_name(item_id: String) -> String:
|
|
var item := state.get_item_def(item_id)
|
|
if item.is_empty():
|
|
return item_id
|
|
return str(item.get("name", item_id))
|
|
|
|
|
|
func _join_strings(values: Array, delimiter: String) -> String:
|
|
var text := ""
|
|
for value in values:
|
|
if not text.is_empty():
|
|
text += delimiter
|
|
text += str(value)
|
|
return text
|
|
|
|
|
|
func _on_next_battle_pressed() -> void:
|
|
var next_scenario_id := str(battle_result_summary.get("next_scenario_id", ""))
|
|
if next_scenario_id.is_empty():
|
|
return
|
|
_play_ui_confirm()
|
|
if log_box != null:
|
|
log_box.clear()
|
|
_load_scenario(next_scenario_id)
|
|
_show_briefing()
|
|
_update_hud()
|
|
|
|
|
|
func _show_campaign_complete() -> void:
|
|
campaign_complete_screen = true
|
|
_play_bgm("menu")
|
|
battle_started = false
|
|
battle_result_applied = true
|
|
battle_result_summary.clear()
|
|
post_battle_dialogue_started = true
|
|
move_cells.clear()
|
|
attack_cells.clear()
|
|
skill_cells.clear()
|
|
item_cells.clear()
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
_hide_shop_menu()
|
|
_hide_armory_menu()
|
|
_hide_roster_menu()
|
|
_hide_formation_menu()
|
|
_clear_dialogue()
|
|
active_scenario_id = campaign_state.current_scenario_id
|
|
if briefing_panel != null:
|
|
briefing_panel.visible = false
|
|
if log_box != null:
|
|
log_box.append_text("Campaign complete.\n")
|
|
|
|
|
|
func _on_new_campaign_pressed() -> void:
|
|
_play_ui_confirm()
|
|
if log_box != null:
|
|
log_box.clear()
|
|
if not campaign_state.reset_save(campaign_state.get_start_scenario_id()):
|
|
result_panel.visible = true
|
|
result_label.text = "Unable to reset campaign save.\nClose the game and check file permissions."
|
|
if result_restart_button != null:
|
|
result_restart_button.visible = false
|
|
if next_battle_button != null:
|
|
next_battle_button.visible = false
|
|
return
|
|
campaign_complete_screen = false
|
|
battle_started = false
|
|
battle_result_applied = false
|
|
battle_result_summary.clear()
|
|
post_battle_dialogue_started = false
|
|
selected_skill_id = ""
|
|
selected_item_id = ""
|
|
formation_unit_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
_hide_roster_menu()
|
|
_load_current_battle()
|
|
_show_briefing()
|
|
_update_hud()
|
|
|
|
|
|
func _update_tactic_button(selected: Dictionary) -> void:
|
|
if tactic_button == null:
|
|
return
|
|
if selected.is_empty():
|
|
tactic_button.text = "Tactic"
|
|
tactic_button.disabled = true
|
|
return
|
|
|
|
var skill_ids := state.get_skill_ids(selected["id"])
|
|
if skill_ids.is_empty():
|
|
tactic_button.text = "Tactic"
|
|
tactic_button.disabled = true
|
|
return
|
|
|
|
if not selected_skill_id.is_empty():
|
|
var skill := state.get_skill_def(selected_skill_id)
|
|
tactic_button.text = "Tactic: %s" % str(skill.get("name", selected_skill_id))
|
|
elif tactic_menu != null and tactic_menu.visible:
|
|
tactic_button.text = "Close Tactic"
|
|
else:
|
|
tactic_button.text = "Tactic"
|
|
tactic_button.disabled = selected.get("acted", false) or not state.can_player_act() or _is_input_locked()
|
|
|
|
|
|
func _show_tactic_menu(selected: Dictionary) -> void:
|
|
if tactic_menu == null:
|
|
return
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
_rebuild_tactic_menu(selected)
|
|
tactic_menu.visible = true
|
|
|
|
|
|
func _hide_tactic_menu() -> void:
|
|
if tactic_menu == null:
|
|
return
|
|
tactic_menu.visible = false
|
|
|
|
|
|
func _rebuild_tactic_menu(selected: Dictionary) -> void:
|
|
if tactic_list == null:
|
|
return
|
|
_clear_tactic_list()
|
|
var skill_ids := state.get_skill_ids(selected["id"])
|
|
if skill_ids.is_empty():
|
|
var empty_label := Label.new()
|
|
empty_label.text = "No tactics"
|
|
tactic_list.add_child(empty_label)
|
|
return
|
|
|
|
for skill_id in skill_ids:
|
|
var skill := state.get_skill_def(str(skill_id))
|
|
var skill_button := Button.new()
|
|
skill_button.text = _format_tactic_button_text(str(skill_id), skill, selected)
|
|
skill_button.disabled = selected.get("acted", false) or _is_input_locked() or int(selected.get("mp", 0)) < int(skill.get("mp_cost", 0))
|
|
skill_button.pressed.connect(_on_tactic_skill_pressed.bind(str(skill_id)))
|
|
tactic_list.add_child(skill_button)
|
|
|
|
var cancel_button := Button.new()
|
|
cancel_button.text = "Cancel"
|
|
cancel_button.pressed.connect(_on_tactic_cancel_pressed)
|
|
tactic_list.add_child(cancel_button)
|
|
|
|
|
|
func _clear_tactic_list() -> void:
|
|
for child in tactic_list.get_children():
|
|
tactic_list.remove_child(child)
|
|
child.queue_free()
|
|
|
|
|
|
func _format_tactic_button_text(skill_id: String, skill: Dictionary, selected: Dictionary) -> String:
|
|
var marker := "> " if selected_skill_id == skill_id else ""
|
|
var skill_name := str(skill.get("name", skill_id))
|
|
var mp_cost := int(skill.get("mp_cost", 0))
|
|
var kind := str(skill.get("kind", "skill")).capitalize()
|
|
var range_text := _format_tactic_range_text(skill)
|
|
var power_text := int(skill.get("power", 0))
|
|
var disabled_text := " - MP" if int(selected.get("mp", 0)) < mp_cost else ""
|
|
return "%s%s %dMP %s R%s P%d%s" % [marker, skill_name, mp_cost, kind, range_text, power_text, disabled_text]
|
|
|
|
|
|
func _format_tactic_range_text(skill: Dictionary) -> String:
|
|
var value = skill.get("range", 1)
|
|
if typeof(value) == TYPE_ARRAY:
|
|
if value.size() >= 2:
|
|
return "%d-%d" % [int(value[0]), int(value[1])]
|
|
if value.size() == 1:
|
|
return "%d" % int(value[0])
|
|
return "1"
|
|
if typeof(value) == TYPE_INT or typeof(value) == TYPE_FLOAT:
|
|
return "%d" % int(value)
|
|
return "1"
|
|
|
|
|
|
func _on_tactic_skill_pressed(skill_id: String) -> void:
|
|
_play_ui_confirm()
|
|
selected_skill_id = skill_id
|
|
selected_item_id = ""
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_hide_equip_menu()
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _on_tactic_cancel_pressed() -> void:
|
|
_play_ui_cancel()
|
|
selected_skill_id = ""
|
|
_hide_tactic_menu()
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _update_item_button(selected: Dictionary) -> void:
|
|
if item_button == null:
|
|
return
|
|
if selected.is_empty():
|
|
item_button.text = "Item"
|
|
item_button.disabled = true
|
|
return
|
|
|
|
var item_ids := state.get_usable_item_ids()
|
|
if item_ids.is_empty():
|
|
item_button.text = "Item"
|
|
item_button.disabled = true
|
|
return
|
|
|
|
if not selected_item_id.is_empty():
|
|
var item := state.get_item_def(selected_item_id)
|
|
item_button.text = "Item: %s" % str(item.get("name", selected_item_id))
|
|
elif item_menu != null and item_menu.visible:
|
|
item_button.text = "Close Item"
|
|
else:
|
|
item_button.text = "Item"
|
|
item_button.disabled = selected.get("acted", false) or not state.can_player_act() or _is_input_locked()
|
|
|
|
|
|
func _show_item_menu(selected: Dictionary) -> void:
|
|
if item_menu == null:
|
|
return
|
|
_hide_tactic_menu()
|
|
_hide_equip_menu()
|
|
_rebuild_item_menu(selected)
|
|
item_menu.visible = true
|
|
|
|
|
|
func _hide_item_menu() -> void:
|
|
if item_menu == null:
|
|
return
|
|
item_menu.visible = false
|
|
|
|
|
|
func _rebuild_item_menu(selected: Dictionary) -> void:
|
|
if item_list == null:
|
|
return
|
|
_clear_item_list()
|
|
var item_ids := state.get_usable_item_ids()
|
|
if item_ids.is_empty():
|
|
var empty_label := Label.new()
|
|
empty_label.text = "No items"
|
|
item_list.add_child(empty_label)
|
|
return
|
|
|
|
var inventory := state.get_inventory_snapshot()
|
|
for item_id in item_ids:
|
|
var normalized_id := str(item_id)
|
|
var item := state.get_item_def(normalized_id)
|
|
var count := int(inventory.get(normalized_id, 0))
|
|
var use_button := Button.new()
|
|
use_button.text = _format_item_button_text(normalized_id, item, count)
|
|
use_button.disabled = selected.get("acted", false) or _is_input_locked() or count <= 0
|
|
use_button.pressed.connect(_on_item_selected_pressed.bind(normalized_id))
|
|
item_list.add_child(use_button)
|
|
|
|
var cancel_button := Button.new()
|
|
cancel_button.text = "Cancel"
|
|
cancel_button.pressed.connect(_on_item_cancel_pressed)
|
|
item_list.add_child(cancel_button)
|
|
|
|
|
|
func _clear_item_list() -> void:
|
|
for child in item_list.get_children():
|
|
item_list.remove_child(child)
|
|
child.queue_free()
|
|
|
|
|
|
func _format_item_button_text(item_id: String, item: Dictionary, count: int) -> String:
|
|
var marker := "> " if selected_item_id == item_id else ""
|
|
var item_name := str(item.get("name", item_id))
|
|
return "%s%s x%d %s" % [marker, item_name, count, _format_item_effect_text(item)]
|
|
|
|
|
|
func _format_item_effect_text(item: Dictionary) -> String:
|
|
var parts := []
|
|
for effect in item.get("effects", []):
|
|
if typeof(effect) != TYPE_DICTIONARY:
|
|
continue
|
|
if str(effect.get("type", "")) == "heal_hp":
|
|
parts.append("Heal %d" % int(effect.get("amount", 0)))
|
|
elif str(effect.get("type", "")) == "heal_mp":
|
|
parts.append("MP %d" % int(effect.get("amount", 0)))
|
|
if parts.is_empty():
|
|
return str(item.get("kind", "item")).capitalize()
|
|
return _join_strings(parts, ", ")
|
|
|
|
|
|
func _on_item_selected_pressed(item_id: String) -> void:
|
|
_play_ui_confirm()
|
|
selected_item_id = item_id
|
|
selected_skill_id = ""
|
|
_hide_item_menu()
|
|
_hide_tactic_menu()
|
|
_hide_equip_menu()
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _on_item_cancel_pressed() -> void:
|
|
_play_ui_cancel()
|
|
selected_item_id = ""
|
|
_hide_item_menu()
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _update_equip_button(selected: Dictionary) -> void:
|
|
if equip_button == null:
|
|
return
|
|
if selected.is_empty():
|
|
equip_button.text = "Equip"
|
|
equip_button.disabled = true
|
|
return
|
|
|
|
if equip_menu != null and equip_menu.visible:
|
|
equip_button.text = "Close Equip"
|
|
else:
|
|
equip_button.text = "Equip"
|
|
equip_button.disabled = selected.get("moved", false) or selected.get("acted", false) or not state.can_player_act() or _is_input_locked()
|
|
|
|
|
|
func _show_equip_menu(selected: Dictionary) -> void:
|
|
if equip_menu == null:
|
|
return
|
|
_hide_tactic_menu()
|
|
_hide_item_menu()
|
|
_rebuild_equip_menu(selected)
|
|
equip_menu.visible = true
|
|
|
|
|
|
func _hide_equip_menu() -> void:
|
|
if equip_menu == null:
|
|
return
|
|
equip_menu.visible = false
|
|
|
|
|
|
func _rebuild_equip_menu(selected: Dictionary) -> void:
|
|
if equip_list == null:
|
|
return
|
|
_clear_equip_list()
|
|
var equipment_label := Label.new()
|
|
equipment_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
equipment_label.text = "Equipped: %s" % _format_unit_equipment(selected)
|
|
equip_list.add_child(equipment_label)
|
|
|
|
var item_ids := state.get_equippable_item_ids(selected["id"])
|
|
if item_ids.is_empty():
|
|
var empty_label := Label.new()
|
|
empty_label.text = "No compatible equipment"
|
|
equip_list.add_child(empty_label)
|
|
else:
|
|
var inventory := state.get_inventory_snapshot()
|
|
for item_id in item_ids:
|
|
var normalized_id := str(item_id)
|
|
var item := state.get_item_def(normalized_id)
|
|
var equip_item_button := Button.new()
|
|
equip_item_button.text = "%s x%d %s" % [
|
|
_item_display_name(normalized_id),
|
|
int(inventory.get(normalized_id, 0)),
|
|
_format_equipment_bonus_text(item)
|
|
]
|
|
equip_item_button.disabled = _is_input_locked()
|
|
equip_item_button.pressed.connect(_on_equip_item_pressed.bind(normalized_id))
|
|
equip_list.add_child(equip_item_button)
|
|
|
|
var cancel_button := Button.new()
|
|
cancel_button.text = "Cancel"
|
|
cancel_button.pressed.connect(_on_equip_cancel_pressed)
|
|
equip_list.add_child(cancel_button)
|
|
|
|
|
|
func _clear_equip_list() -> void:
|
|
for child in equip_list.get_children():
|
|
equip_list.remove_child(child)
|
|
child.queue_free()
|
|
|
|
|
|
func _format_unit_equipment(unit: Dictionary) -> String:
|
|
var equipment := state.get_equipment_snapshot(unit["id"])
|
|
var parts := []
|
|
for slot in ["weapon", "armor", "accessory"]:
|
|
var item_id := str(equipment.get(slot, ""))
|
|
if item_id.is_empty():
|
|
parts.append("%s -" % slot.capitalize())
|
|
else:
|
|
parts.append("%s %s" % [slot.capitalize(), _item_display_name(item_id)])
|
|
return _join_strings(parts, ", ")
|
|
|
|
|
|
func _format_equipment_bonus_text(item: Dictionary) -> String:
|
|
var parts := []
|
|
var bonuses: Dictionary = item.get("bonuses", {})
|
|
for stat in ["hp", "mp", "atk", "def", "int", "agi"]:
|
|
var amount := int(bonuses.get(stat, 0))
|
|
if amount == 0:
|
|
continue
|
|
var sign := "+" if amount > 0 else ""
|
|
parts.append("%s %s%d" % [stat.to_upper(), sign, amount])
|
|
if parts.is_empty():
|
|
return str(item.get("kind", "equipment")).capitalize()
|
|
return _join_strings(parts, ", ")
|
|
|
|
|
|
func _on_equip_item_pressed(item_id: String) -> void:
|
|
var selected := state.get_selected_unit()
|
|
if selected.is_empty():
|
|
return
|
|
if state.try_equip_item(selected["id"], item_id):
|
|
_rebuild_equip_menu(state.get_selected_unit())
|
|
_refresh_ranges()
|
|
_update_hud()
|
|
queue_redraw()
|
|
|
|
|
|
func _on_equip_cancel_pressed() -> void:
|
|
_play_ui_cancel()
|
|
_hide_equip_menu()
|
|
_update_hud()
|
|
queue_redraw()
|