Add title load slot selection
This commit is contained in:
@@ -27,6 +27,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
|
|||||||
- Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview.
|
- Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview.
|
||||||
- Pre-battle Chapters overview shows story-arc progress and can open completed or current battles.
|
- Pre-battle Chapters overview shows story-arc progress and can open completed or current battles.
|
||||||
- Pre-battle Save menu can write and load one manual campaign checkpoint separate from the automatic save.
|
- Pre-battle Save menu can write and load one manual campaign checkpoint separate from the automatic save.
|
||||||
|
- The title screen exposes Start, Load, and Settings, and Load now lets the player choose between the automatic record and the manual checkpoint.
|
||||||
- Escort scenarios can include required, non-controllable protected units.
|
- Escort scenarios can include required, non-controllable protected units.
|
||||||
- Campaign saves joined officers, future deployments can require prior recruitment, and choices can make officers leave.
|
- Campaign saves joined officers, future deployments can require prior recruitment, and choices can make officers leave.
|
||||||
- Completed saves reconcile officer join/leave rewards on load without replaying gold or items.
|
- Completed saves reconcile officer join/leave rewards on load without replaying gold or items.
|
||||||
|
|||||||
@@ -191,10 +191,18 @@ func load_manual_game() -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func get_save_summary() -> Dictionary:
|
||||||
|
return _get_save_summary_from_path(SAVE_PATH)
|
||||||
|
|
||||||
|
|
||||||
func get_manual_save_summary() -> Dictionary:
|
func get_manual_save_summary() -> Dictionary:
|
||||||
if not has_manual_save():
|
return _get_save_summary_from_path(MANUAL_SAVE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_save_summary_from_path(path: String) -> Dictionary:
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
return {}
|
return {}
|
||||||
var parsed = JSON.parse_string(FileAccess.get_file_as_string(MANUAL_SAVE_PATH))
|
var parsed = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||||||
if typeof(parsed) != TYPE_DICTIONARY:
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
return {}
|
return {}
|
||||||
var completed_count := 0
|
var completed_count := 0
|
||||||
|
|||||||
@@ -507,6 +507,10 @@ var title_start_button: Button
|
|||||||
var title_load_button: Button
|
var title_load_button: Button
|
||||||
var title_settings_button: Button
|
var title_settings_button: Button
|
||||||
var title_status_label: Label
|
var title_status_label: Label
|
||||||
|
var title_load_panel: PanelContainer
|
||||||
|
var title_load_auto_button: Button
|
||||||
|
var title_load_manual_button: Button
|
||||||
|
var title_load_back_button: Button
|
||||||
var title_settings_panel: PanelContainer
|
var title_settings_panel: PanelContainer
|
||||||
var title_bgm_toggle: CheckButton
|
var title_bgm_toggle: CheckButton
|
||||||
var title_sfx_toggle: CheckButton
|
var title_sfx_toggle: CheckButton
|
||||||
@@ -574,10 +578,13 @@ func _ready() -> void:
|
|||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if _is_title_screen_visible():
|
if _is_title_screen_visible():
|
||||||
if event is InputEventKey and event.pressed and not event.echo:
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
if event.keycode == KEY_ENTER:
|
if event.keycode == KEY_ESCAPE:
|
||||||
|
if _is_title_load_panel_visible():
|
||||||
|
_on_title_load_back_pressed()
|
||||||
|
elif title_settings_panel != null and title_settings_panel.visible:
|
||||||
|
_on_title_settings_back_pressed()
|
||||||
|
elif event.keycode == KEY_ENTER and not _is_title_load_panel_visible() and not _is_title_settings_panel_visible():
|
||||||
_on_title_start_pressed()
|
_on_title_start_pressed()
|
||||||
elif event.keycode == KEY_ESCAPE and title_settings_panel != null and title_settings_panel.visible:
|
|
||||||
_on_title_settings_back_pressed()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if _is_opening_prologue_visible():
|
if _is_opening_prologue_visible():
|
||||||
@@ -3416,6 +3423,43 @@ func _create_hud() -> void:
|
|||||||
title_settings_button.pressed.connect(_on_title_settings_pressed)
|
title_settings_button.pressed.connect(_on_title_settings_pressed)
|
||||||
title_menu_column.add_child(title_settings_button)
|
title_menu_column.add_child(title_settings_button)
|
||||||
|
|
||||||
|
title_load_panel = PanelContainer.new()
|
||||||
|
title_load_panel.visible = false
|
||||||
|
title_load_panel.custom_minimum_size = Vector2(432, 188)
|
||||||
|
_apply_panel_style(title_load_panel, "hud_info")
|
||||||
|
title_menu_column.add_child(title_load_panel)
|
||||||
|
|
||||||
|
var title_load_column := VBoxContainer.new()
|
||||||
|
title_load_column.add_theme_constant_override("separation", 8)
|
||||||
|
title_load_panel.add_child(title_load_column)
|
||||||
|
|
||||||
|
var title_load_caption := Label.new()
|
||||||
|
title_load_caption.text = "불러올 기록"
|
||||||
|
title_load_caption.custom_minimum_size = Vector2(392, 24)
|
||||||
|
title_load_caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
title_load_caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
_apply_label_style(title_load_caption, UI_OLD_BRONZE, 15)
|
||||||
|
title_load_column.add_child(title_load_caption)
|
||||||
|
|
||||||
|
title_load_auto_button = Button.new()
|
||||||
|
title_load_auto_button.custom_minimum_size = Vector2(392, 38)
|
||||||
|
title_load_auto_button.tooltip_text = "자동으로 봉인된 진행 기록을 엽니다."
|
||||||
|
title_load_auto_button.pressed.connect(_on_title_load_auto_pressed)
|
||||||
|
title_load_column.add_child(title_load_auto_button)
|
||||||
|
|
||||||
|
title_load_manual_button = Button.new()
|
||||||
|
title_load_manual_button.custom_minimum_size = Vector2(392, 38)
|
||||||
|
title_load_manual_button.tooltip_text = "군막에서 직접 남긴 수동 봉인을 엽니다."
|
||||||
|
title_load_manual_button.pressed.connect(_on_title_load_manual_pressed)
|
||||||
|
title_load_column.add_child(title_load_manual_button)
|
||||||
|
|
||||||
|
title_load_back_button = Button.new()
|
||||||
|
title_load_back_button.text = "뒤로"
|
||||||
|
title_load_back_button.custom_minimum_size = Vector2(392, 34)
|
||||||
|
title_load_back_button.tooltip_text = "처음 화면 선택으로 돌아갑니다."
|
||||||
|
title_load_back_button.pressed.connect(_on_title_load_back_pressed)
|
||||||
|
title_load_column.add_child(title_load_back_button)
|
||||||
|
|
||||||
title_settings_panel = PanelContainer.new()
|
title_settings_panel = PanelContainer.new()
|
||||||
title_settings_panel.visible = false
|
title_settings_panel.visible = false
|
||||||
title_settings_panel.custom_minimum_size = Vector2(432, 236)
|
title_settings_panel.custom_minimum_size = Vector2(432, 236)
|
||||||
@@ -3572,6 +3616,9 @@ func _create_hud() -> void:
|
|||||||
title_start_button,
|
title_start_button,
|
||||||
title_load_button,
|
title_load_button,
|
||||||
title_settings_button,
|
title_settings_button,
|
||||||
|
title_load_auto_button,
|
||||||
|
title_load_manual_button,
|
||||||
|
title_load_back_button,
|
||||||
title_settings_back_button
|
title_settings_back_button
|
||||||
]:
|
]:
|
||||||
_apply_button_style(hud_button)
|
_apply_button_style(hud_button)
|
||||||
@@ -10451,6 +10498,7 @@ func _show_title_menu() -> void:
|
|||||||
if title_screen_root != null:
|
if title_screen_root != null:
|
||||||
title_screen_root.visible = true
|
title_screen_root.visible = true
|
||||||
title_screen_root.move_to_front()
|
title_screen_root.move_to_front()
|
||||||
|
_set_title_load_panel_visible(false)
|
||||||
_set_title_settings_visible(false)
|
_set_title_settings_visible(false)
|
||||||
_update_title_menu_state()
|
_update_title_menu_state()
|
||||||
_refresh_screen_backdrop_visibility()
|
_refresh_screen_backdrop_visibility()
|
||||||
@@ -10544,9 +10592,10 @@ func _update_title_menu_state() -> void:
|
|||||||
var has_load := campaign_state.has_save() or campaign_state.has_manual_save()
|
var has_load := campaign_state.has_save() or campaign_state.has_manual_save()
|
||||||
if title_load_button != null:
|
if title_load_button != null:
|
||||||
title_load_button.disabled = not has_load
|
title_load_button.disabled = not has_load
|
||||||
title_load_button.tooltip_text = "저장된 전기를 불러옵니다." if has_load else "저장된 전기가 없습니다."
|
title_load_button.tooltip_text = "불러올 기록을 선택합니다." if has_load else "저장된 전기가 없습니다."
|
||||||
if title_status_label != null:
|
if title_status_label != null:
|
||||||
title_status_label.text = _format_title_status_text(has_load)
|
title_status_label.text = _format_title_status_text(has_load)
|
||||||
|
_update_title_load_panel_state()
|
||||||
if title_settings_back_button != null:
|
if title_settings_back_button != null:
|
||||||
title_settings_back_button.visible = title_settings_panel != null and title_settings_panel.visible
|
title_settings_back_button.visible = title_settings_panel != null and title_settings_panel.visible
|
||||||
if title_bgm_toggle != null:
|
if title_bgm_toggle != null:
|
||||||
@@ -10558,20 +10607,55 @@ func _update_title_menu_state() -> void:
|
|||||||
_refresh_title_volume_controls()
|
_refresh_title_volume_controls()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_title_load_panel_state() -> void:
|
||||||
|
if title_load_auto_button != null:
|
||||||
|
var auto_summary := campaign_state.get_save_summary()
|
||||||
|
var has_auto := not auto_summary.is_empty()
|
||||||
|
title_load_auto_button.disabled = not has_auto
|
||||||
|
title_load_auto_button.text = _format_title_load_button_text(auto_summary, "자동 기록", "자동 기록 없음")
|
||||||
|
title_load_auto_button.tooltip_text = _format_title_load_button_tooltip(auto_summary, "자동으로 봉인된 진행 기록입니다.")
|
||||||
|
if title_load_manual_button != null:
|
||||||
|
var manual_summary := campaign_state.get_manual_save_summary()
|
||||||
|
var has_manual := not manual_summary.is_empty()
|
||||||
|
title_load_manual_button.disabled = not has_manual
|
||||||
|
title_load_manual_button.text = _format_title_load_button_text(manual_summary, "수동 봉인", "수동 봉인 없음")
|
||||||
|
title_load_manual_button.tooltip_text = _format_title_load_button_tooltip(manual_summary, "군막에서 직접 남긴 기록입니다.")
|
||||||
|
|
||||||
|
|
||||||
|
func _format_title_load_button_text(summary: Dictionary, label: String, empty_text: String) -> String:
|
||||||
|
if summary.is_empty():
|
||||||
|
return empty_text
|
||||||
|
var title := str(summary.get("current_scenario_title", "전기"))
|
||||||
|
if title.is_empty():
|
||||||
|
title = "전기"
|
||||||
|
return "%s: %s" % [label, title]
|
||||||
|
|
||||||
|
|
||||||
|
func _format_title_load_button_tooltip(summary: Dictionary, header: String) -> String:
|
||||||
|
if summary.is_empty():
|
||||||
|
return "%s\n저장된 기록이 없습니다." % header
|
||||||
|
var state_label := "전후 선택 대기" if bool(summary.get("pending_choice", false)) else "군막 또는 전장 진입 전"
|
||||||
|
return "%s\n진행 %d/%d · 군자금 %d냥 · %s" % [
|
||||||
|
header,
|
||||||
|
int(summary.get("completed_count", 0)),
|
||||||
|
int(summary.get("total_count", 0)),
|
||||||
|
int(summary.get("gold", 0)),
|
||||||
|
state_label
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
func _format_title_status_text(has_load: bool) -> String:
|
func _format_title_status_text(has_load: bool) -> String:
|
||||||
if not has_load:
|
if not has_load:
|
||||||
return "새 전기를 열어 조조와 하후돈의 첫 군령을 시작합니다."
|
return "새 전기를 열어 조조와 하후돈의 첫 군령을 시작합니다."
|
||||||
var summary := campaign_state.get_manual_save_summary()
|
var auto_summary := campaign_state.get_save_summary()
|
||||||
if campaign_state.has_save():
|
if not auto_summary.is_empty():
|
||||||
var title := campaign_state.get_scenario_title(campaign_state.current_scenario_id)
|
|
||||||
if title.is_empty():
|
|
||||||
title = campaign_state.get_scenario_title(campaign_state.get_start_scenario_id())
|
|
||||||
return "이어 할 전기: %s · %d/%d 봉인 · %d냥" % [
|
return "이어 할 전기: %s · %d/%d 봉인 · %d냥" % [
|
||||||
title,
|
str(auto_summary.get("current_scenario_title", "전기")),
|
||||||
campaign_state.completed_scenarios.size(),
|
int(auto_summary.get("completed_count", 0)),
|
||||||
campaign_state.get_scenario_count(),
|
int(auto_summary.get("total_count", 0)),
|
||||||
campaign_state.gold
|
int(auto_summary.get("gold", 0))
|
||||||
]
|
]
|
||||||
|
var summary := campaign_state.get_manual_save_summary()
|
||||||
if not summary.is_empty():
|
if not summary.is_empty():
|
||||||
return "수동 봉인: %s · %d/%d 봉인 · %d냥" % [
|
return "수동 봉인: %s · %d/%d 봉인 · %d냥" % [
|
||||||
str(summary.get("current_scenario_title", "전기")),
|
str(summary.get("current_scenario_title", "전기")),
|
||||||
@@ -10598,15 +10682,36 @@ func _on_title_load_pressed() -> void:
|
|||||||
if title_load_button != null and title_load_button.disabled:
|
if title_load_button != null and title_load_button.disabled:
|
||||||
return
|
return
|
||||||
_play_ui_confirm()
|
_play_ui_confirm()
|
||||||
var loaded := false
|
_set_title_load_panel_visible(true)
|
||||||
if campaign_state.has_save():
|
_update_title_menu_state()
|
||||||
loaded = campaign_state.load_game()
|
|
||||||
elif campaign_state.has_manual_save():
|
|
||||||
loaded = campaign_state.load_manual_game()
|
func _on_title_load_auto_pressed() -> void:
|
||||||
|
_load_title_slot(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_title_load_manual_pressed() -> void:
|
||||||
|
_load_title_slot(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_title_load_back_pressed() -> void:
|
||||||
|
_play_ui_cancel()
|
||||||
|
_set_title_load_panel_visible(false)
|
||||||
|
_update_title_menu_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _load_title_slot(manual: bool) -> void:
|
||||||
|
if manual and title_load_manual_button != null and title_load_manual_button.disabled:
|
||||||
|
return
|
||||||
|
if not manual and title_load_auto_button != null and title_load_auto_button.disabled:
|
||||||
|
return
|
||||||
|
_play_ui_confirm()
|
||||||
|
var loaded := campaign_state.load_manual_game() if manual else campaign_state.load_game()
|
||||||
if not loaded:
|
if not loaded:
|
||||||
if title_status_label != null:
|
if title_status_label != null:
|
||||||
title_status_label.text = "저장된 전기를 열지 못했습니다."
|
title_status_label.text = "저장된 전기를 열지 못했습니다."
|
||||||
_update_title_menu_state()
|
_set_title_load_panel_visible(false)
|
||||||
|
_update_title_load_panel_state()
|
||||||
return
|
return
|
||||||
_enter_campaign_from_title()
|
_enter_campaign_from_title()
|
||||||
|
|
||||||
@@ -10642,6 +10747,8 @@ func _on_title_settings_back_pressed() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _set_title_settings_visible(visible: bool) -> void:
|
func _set_title_settings_visible(visible: bool) -> void:
|
||||||
|
if visible and title_load_panel != null:
|
||||||
|
title_load_panel.visible = false
|
||||||
if title_status_label != null:
|
if title_status_label != null:
|
||||||
title_status_label.visible = not visible
|
title_status_label.visible = not visible
|
||||||
if title_start_button != null:
|
if title_start_button != null:
|
||||||
@@ -10656,6 +10763,31 @@ func _set_title_settings_visible(visible: bool) -> void:
|
|||||||
title_settings_back_button.visible = visible
|
title_settings_back_button.visible = visible
|
||||||
|
|
||||||
|
|
||||||
|
func _set_title_load_panel_visible(visible: bool) -> void:
|
||||||
|
if visible and title_settings_panel != null:
|
||||||
|
title_settings_panel.visible = false
|
||||||
|
if title_status_label != null:
|
||||||
|
title_status_label.visible = not visible
|
||||||
|
if title_start_button != null:
|
||||||
|
title_start_button.visible = not visible
|
||||||
|
if title_load_button != null:
|
||||||
|
title_load_button.visible = not visible
|
||||||
|
if title_settings_button != null:
|
||||||
|
title_settings_button.visible = not visible
|
||||||
|
if title_load_panel != null:
|
||||||
|
title_load_panel.visible = visible
|
||||||
|
if title_settings_back_button != null:
|
||||||
|
title_settings_back_button.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _is_title_load_panel_visible() -> bool:
|
||||||
|
return title_load_panel != null and title_load_panel.visible
|
||||||
|
|
||||||
|
|
||||||
|
func _is_title_settings_panel_visible() -> bool:
|
||||||
|
return title_settings_panel != null and title_settings_panel.visible
|
||||||
|
|
||||||
|
|
||||||
func _on_title_bgm_toggled(enabled: bool) -> void:
|
func _on_title_bgm_toggled(enabled: bool) -> void:
|
||||||
title_bgm_enabled = enabled
|
title_bgm_enabled = enabled
|
||||||
if title_bgm_enabled:
|
if title_bgm_enabled:
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void:
|
|||||||
failures.append("title screen should expose Game Load")
|
failures.append("title screen should expose Game Load")
|
||||||
elif not scene.title_load_button.disabled:
|
elif not scene.title_load_button.disabled:
|
||||||
failures.append("Game Load should be disabled when no save exists")
|
failures.append("Game Load should be disabled when no save exists")
|
||||||
|
if scene.title_load_panel == null:
|
||||||
|
failures.append("title screen should create a load-slot selection panel")
|
||||||
|
elif scene.title_load_panel.visible:
|
||||||
|
failures.append("load-slot selection panel should stay hidden until Game Load is pressed")
|
||||||
if scene.title_settings_button == null or scene.title_settings_button.text != "환경 설정":
|
if scene.title_settings_button == null or scene.title_settings_button.text != "환경 설정":
|
||||||
failures.append("title screen should expose Settings")
|
failures.append("title screen should expose Settings")
|
||||||
|
|
||||||
@@ -118,12 +122,35 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void:
|
|||||||
failures.append("finishing the opening prologue should enter the first briefing")
|
failures.append("finishing the opening prologue should enter the first briefing")
|
||||||
if not scene.campaign_state.has_save():
|
if not scene.campaign_state.has_save():
|
||||||
failures.append("starting a new game should create the main campaign save")
|
failures.append("starting a new game should create the main campaign save")
|
||||||
|
if not scene.campaign_state.save_manual_game():
|
||||||
|
failures.append("manual checkpoint should be writable for title load selection")
|
||||||
|
|
||||||
scene._show_title_menu()
|
scene._show_title_menu()
|
||||||
scene._update_title_menu_state()
|
scene._update_title_menu_state()
|
||||||
if scene.title_load_button == null or scene.title_load_button.disabled:
|
if scene.title_load_button == null or scene.title_load_button.disabled:
|
||||||
failures.append("Game Load should become available after a campaign save exists")
|
failures.append("Game Load should become available after a campaign save exists")
|
||||||
scene._on_title_load_pressed()
|
scene._on_title_load_pressed()
|
||||||
|
if scene.title_load_panel == null or not scene.title_load_panel.visible:
|
||||||
|
failures.append("Game Load should open a load-slot selection panel instead of loading immediately")
|
||||||
|
if scene.title_start_button != null and scene.title_start_button.visible:
|
||||||
|
failures.append("load-slot selection should replace the main title commands while open")
|
||||||
|
if scene.title_load_auto_button == null or scene.title_load_auto_button.disabled:
|
||||||
|
failures.append("load-slot selection should enable the automatic campaign save")
|
||||||
|
elif not scene.title_load_auto_button.text.contains("자동 기록"):
|
||||||
|
failures.append("automatic load slot should be labeled in Korean")
|
||||||
|
if scene.title_load_manual_button == null or scene.title_load_manual_button.disabled:
|
||||||
|
failures.append("load-slot selection should enable the manual checkpoint save")
|
||||||
|
elif not scene.title_load_manual_button.text.contains("수동 봉인"):
|
||||||
|
failures.append("manual load slot should be labeled in Korean")
|
||||||
|
scene._show_title_menu()
|
||||||
|
if scene.title_load_panel != null and scene.title_load_panel.visible:
|
||||||
|
failures.append("showing the title menu again should reset the load-slot panel")
|
||||||
|
scene._on_title_load_pressed()
|
||||||
|
scene._on_title_load_back_pressed()
|
||||||
|
if scene.title_load_panel != null and scene.title_load_panel.visible:
|
||||||
|
failures.append("load-slot back should close the title load panel")
|
||||||
|
scene._on_title_load_pressed()
|
||||||
|
scene._on_title_load_auto_pressed()
|
||||||
if scene.title_screen_root != null and scene.title_screen_root.visible:
|
if scene.title_screen_root != null and scene.title_screen_root.visible:
|
||||||
failures.append("loading a saved game should hide the title screen")
|
failures.append("loading a saved game should hide the title screen")
|
||||||
if scene._is_opening_prologue_visible():
|
if scene._is_opening_prologue_visible():
|
||||||
|
|||||||
Reference in New Issue
Block a user