From d6029703fe2034257665b9c33370d46edbdb668e Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 00:53:13 +0900 Subject: [PATCH] Compact prebattle save slots --- README.md | 2 +- scripts/scenes/battle_scene.gd | 168 ++++++++++++++++++++++++++++----- tools/smoke_visual_assets.gd | 14 +++ 3 files changed, 161 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b3d2dad..f1c78e0 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells, with portrait rows, coordinate badges, and hover placement instructions. - 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 with map thumbnail rows, compact status badges, hover details, 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, using compact seal-style slot rows and hover checkpoint details. - The title screen exposes Start, Load, and Settings, and Load now lets the player choose between the automatic record and the manual checkpoint. - Victory results show compact reward icons for gold, items, and officer roster changes, with hover details for the full reward text. - Escort scenarios can include required, non-controllable protected units. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 3b88164..b301457 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -123,6 +123,8 @@ const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(188, 86) const CHAPTER_SEAL_BADGE_SIZE := Vector2(54, 54) const CHAPTER_SCENARIO_THUMBNAIL_SIZE := Vector2(72, 50) const CHAPTER_INFO_BADGE_SIZE := Vector2(86, 22) +const SAVE_SLOT_BADGE_SIZE := Vector2(54, 54) +const SAVE_INFO_BADGE_SIZE := Vector2(86, 22) const CAMP_TALK_PORTRAIT_SIZE := Vector2(58, 58) const SHOP_MERCHANT_BADGE_SIZE := Vector2(50, 50) const SHOP_ITEM_ICON_SIZE := Vector2(48, 48) @@ -419,6 +421,7 @@ var formation_unit_id := "" var save_button: Button var save_menu: VBoxContainer var save_status_label: Label +var save_slot_list: VBoxContainer var manual_save_button: Button var manual_load_button: Button var dialogue_row: HBoxContainer @@ -3116,23 +3119,14 @@ func _create_hud() -> void: save_status_label = Label.new() save_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - save_status_label.custom_minimum_size = Vector2(600, 64) + save_status_label.custom_minimum_size = Vector2(600, 22) _apply_label_style(save_status_label, UI_AGED_INK, 14) save_menu.add_child(save_status_label) - var save_action_row := HBoxContainer.new() - save_action_row.add_theme_constant_override("separation", 8) - save_menu.add_child(save_action_row) - - manual_save_button = Button.new() - manual_save_button.text = "기록 봉인" - manual_save_button.pressed.connect(_on_manual_save_pressed) - save_action_row.add_child(manual_save_button) - - manual_load_button = Button.new() - manual_load_button.text = "봉인 해제" - manual_load_button.pressed.connect(_on_manual_load_pressed) - save_action_row.add_child(manual_load_button) + save_slot_list = VBoxContainer.new() + save_slot_list.custom_minimum_size = Vector2(580, 0) + save_slot_list.add_theme_constant_override("separation", 5) + save_menu.add_child(save_slot_list) dialogue_panel = PanelContainer.new() dialogue_panel.visible = false @@ -11396,20 +11390,150 @@ func _hide_save_menu() -> void: func _rebuild_save_menu() -> void: if save_status_label != null: - save_status_label.text = "%s\n%s" % [ - _format_current_checkpoint_text(), - _format_manual_checkpoint_text() - ] - if manual_load_button != null: - manual_load_button.disabled = not campaign_state.has_manual_save() + save_status_label.text = "전기록 · 군막 봉인" + if save_slot_list == null: + return + for child in save_slot_list.get_children(): + save_slot_list.remove_child(child) + child.queue_free() + + manual_save_button = Button.new() + manual_save_button.text = "봉인" + manual_save_button.tooltip_text = "%s\n클릭하면 이 군막 준비를 수동 봉인합니다." % _format_current_checkpoint_text() + manual_save_button.pressed.connect(_on_manual_save_pressed) + _apply_button_style(manual_save_button) + _add_save_slot_row( + "현재 군막", + _current_checkpoint_title(), + "현", + manual_save_button, + _format_current_checkpoint_badges(), + manual_save_button.tooltip_text + ) + + var manual_summary := campaign_state.get_manual_save_summary() + var has_manual := not manual_summary.is_empty() + manual_load_button = Button.new() + manual_load_button.text = "열기" if has_manual else "비어 있음" + manual_load_button.disabled = not has_manual + manual_load_button.tooltip_text = _format_manual_checkpoint_tooltip(manual_summary) + manual_load_button.pressed.connect(_on_manual_load_pressed) + _apply_button_style(manual_load_button) + _add_save_slot_row( + "수동 봉인", + _manual_checkpoint_title(manual_summary), + "수", + manual_load_button, + _format_manual_checkpoint_badges(manual_summary), + manual_load_button.tooltip_text + ) -func _format_current_checkpoint_text() -> String: +func _add_save_slot_row(slot_label: String, title: String, badge_text: String, action_button: Button, visible_badges: Array, tooltip_text: String) -> void: + var row := HBoxContainer.new() + row.custom_minimum_size = Vector2(580, 62) + row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_theme_constant_override("separation", 8) + row.tooltip_text = tooltip_text + save_slot_list.add_child(row) + + row.add_child(_make_initials_badge(badge_text, SAVE_SLOT_BADGE_SIZE, 16)) + + var column := VBoxContainer.new() + column.custom_minimum_size = Vector2(404, 0) + column.size_flags_horizontal = Control.SIZE_EXPAND_FILL + column.add_theme_constant_override("separation", 4) + column.tooltip_text = tooltip_text + row.add_child(column) + + var title_label := Label.new() + title_label.custom_minimum_size = Vector2(404, 26) + title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title_label.text = "%s · %s" % [slot_label, title] + title_label.tooltip_text = tooltip_text + _apply_label_style(title_label, UI_AGED_INK, 13) + column.add_child(title_label) + + var badge_row := HBoxContainer.new() + badge_row.custom_minimum_size = Vector2(404, 22) + badge_row.add_theme_constant_override("separation", 5) + badge_row.tooltip_text = tooltip_text + column.add_child(badge_row) + + for badge in visible_badges: + var text := str(badge).strip_edges() + if text.is_empty(): + continue + badge_row.add_child(_make_save_info_badge(text, tooltip_text)) + + action_button.custom_minimum_size = Vector2(92, 34) + action_button.tooltip_text = tooltip_text + row.add_child(action_button) + + +func _make_save_info_badge(text: String, tooltip_text: String) -> PanelContainer: + var panel := PanelContainer.new() + panel.custom_minimum_size = SAVE_INFO_BADGE_SIZE + panel.tooltip_text = tooltip_text + _apply_panel_style(panel, "caption") + + var label := Label.new() + label.set_anchors_preset(Control.PRESET_FULL_RECT) + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + label.text = text + label.tooltip_text = tooltip_text + _apply_label_style(label, UI_PARCHMENT_TEXT, 10) + panel.add_child(label) + return panel + + +func _current_checkpoint_title() -> String: 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()) + if title.is_empty(): + return "전기" + return title + + +func _manual_checkpoint_title(summary: Dictionary) -> String: + if summary.is_empty(): + return "비어 있음" + var title := str(summary.get("current_scenario_title", "")) + if title.is_empty(): + title = str(summary.get("current_scenario_id", "전기")) + return title + + +func _format_current_checkpoint_badges() -> Array: + return [ + "진행 %d/%d" % [campaign_state.completed_scenarios.size(), campaign_state.get_scenario_count()], + "%d냥" % campaign_state.gold + ] + + +func _format_manual_checkpoint_badges(summary: Dictionary) -> Array: + if summary.is_empty(): + return ["비어 있음", "수동"] + var badges := [ + "진행 %d/%d" % [int(summary.get("completed_count", 0)), int(summary.get("total_count", 0))], + "%d냥" % int(summary.get("gold", 0)) + ] + if bool(summary.get("pending_choice", false)): + badges.append("회의") + return badges + + +func _format_manual_checkpoint_tooltip(summary: Dictionary) -> String: + if summary.is_empty(): + return "수동 봉인\n저장된 기록이 없습니다." + return "%s\n클릭하면 이 수동 봉인을 열어 군막으로 돌아갑니다." % _format_manual_checkpoint_text() + + +func _format_current_checkpoint_text() -> String: return "현재 봉인: %s · %d/%d 봉인 · 군자금 %d냥" % [ - title, + _current_checkpoint_title(), campaign_state.completed_scenarios.size(), campaign_state.get_scenario_count(), campaign_state.gold diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 7f03d6a..2ffa96f 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3050,6 +3050,20 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: failures.append("chapter row tooltips should retain current and locked battle details: %s" % chapter_tooltips) if not _has_descendant_texture(scene.chapter_list): failures.append("chapter scenario rows should use battlefield thumbnail artwork") + if not scene.campaign_state.save_manual_game(): + failures.append("visual smoke should be able to create a manual checkpoint for save menu checks") + scene._rebuild_save_menu() + var save_status_text: String = scene.save_status_label.text if scene.save_status_label != null else "" + var save_visible_text := _collect_child_text(scene.save_slot_list) + var save_tooltips := _collect_child_tooltips(scene.save_slot_list) + if not save_status_text.contains("전기록") or not save_status_text.contains("군막"): + failures.append("save status should stay compact and mode-aware: %s" % save_status_text) + if not save_visible_text.contains("현재 군막") or not save_visible_text.contains("수동 봉인") or not save_visible_text.contains("진행") or not save_visible_text.contains("열기"): + failures.append("save rows should show compact slot titles, action, and badges: %s" % save_visible_text) + if save_visible_text.contains("현재 봉인:") or save_visible_text.contains("군자금"): + failures.append("save rows should keep checkpoint detail in hover text: %s" % save_visible_text) + if not save_tooltips.contains("현재 봉인:") or not save_tooltips.contains("클릭하면 이 군막 준비를 수동 봉인합니다.") or not save_tooltips.contains("클릭하면 이 수동 봉인을 열어 군막으로 돌아갑니다."): + failures.append("save row tooltips should retain checkpoint detail and action hints: %s" % save_tooltips) scene.free() _check_scenario_background_texture_loading(failures)