From fc8ae7a0dd960e1771471ba67dd4e4df7417e765 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 00:48:30 +0900 Subject: [PATCH] Compact chapter overview rows --- README.md | 2 +- scripts/scenes/battle_scene.gd | 261 +++++++++++++++++++++++++++------ tools/smoke_visual_assets.gd | 18 +++ 3 files changed, 239 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 2642ccf..b3d2dad 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Pre-battle Roster can move optional officers between sortie and reserve within scenario limits, using portrait rows, compact status badges, and hover details. - 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 and can open completed or current battles. +- 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. - 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. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 29a1938..3b88164 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -120,6 +120,9 @@ const LOCAL_COMMAND_TITLE_MIN_FONT_SIZE := 10 const LOCAL_COMMAND_DETAIL_FONT_SIZE := 12 const LOCAL_COMMAND_DETAIL_MIN_FONT_SIZE := 10 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 CAMP_TALK_PORTRAIT_SIZE := Vector2(58, 58) const SHOP_MERCHANT_BADGE_SIZE := Vector2(50, 50) const SHOP_ITEM_ICON_SIZE := Vector2(48, 48) @@ -9870,7 +9873,7 @@ func _rebuild_chapter_menu() -> void: child.queue_free() var entries := campaign_state.get_chapter_progress_entries() if chapter_status_label != null: - chapter_status_label.text = "%s 병서 · 봉인 전장 %d/%d" % [ + chapter_status_label.text = "%s · 진행 %d/%d" % [ campaign_state.campaign_title, campaign_state.completed_scenarios.size(), campaign_state.get_scenario_count() @@ -9882,55 +9885,231 @@ func _rebuild_chapter_menu() -> void: chapter_list.add_child(empty_label) return for entry in entries: - var completed := int(entry.get("completed", 0)) - var total := int(entry.get("total", 0)) - var current := bool(entry.get("current", false)) - var status := "현 전장" if current else ("봉인 완료" if total > 0 and completed >= total else "미개봉") - var current_title := str(entry.get("current_scenario_title", "")) - var line := "병서 %02d/%02d · %s [%s] %d/%d" % [ - int(entry.get("position", 0)), - int(entry.get("chapter_count", 0)), - entry.get("title", ""), - status, - completed, - total - ] - if current and not current_title.is_empty(): - line += "\n현 전장: %s" % current_title - var chapter_label := Label.new() - chapter_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - chapter_label.custom_minimum_size = Vector2(560, 0) - chapter_label.text = line - _apply_label_style(chapter_label, UI_AGED_INK) - chapter_list.add_child(chapter_label) - for scenario in entry.get("scenarios", []): - _add_chapter_scenario_entry(scenario) + _add_chapter_progress_entry(entry) + if bool(entry.get("current", false)) or int(entry.get("completed", 0)) > 0: + for scenario in entry.get("scenarios", []): + _add_chapter_scenario_entry(scenario) + + +func _add_chapter_progress_entry(entry: Dictionary) -> void: + var tooltip_text := _format_chapter_progress_tooltip(entry) + var row := HBoxContainer.new() + row.custom_minimum_size = Vector2(580, 58) + row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_theme_constant_override("separation", 8) + row.tooltip_text = tooltip_text + chapter_list.add_child(row) + + row.add_child(_make_initials_badge(str(entry.get("title", "병서")), CHAPTER_SEAL_BADGE_SIZE, 16)) + + var column := VBoxContainer.new() + column.custom_minimum_size = Vector2(518, 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(518, 26) + title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title_label.text = "%02d/%02d %s" % [ + int(entry.get("position", 0)), + int(entry.get("chapter_count", 0)), + str(entry.get("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(518, 22) + badge_row.add_theme_constant_override("separation", 5) + badge_row.tooltip_text = tooltip_text + column.add_child(badge_row) + + for badge_text in _format_chapter_progress_badges(entry): + var text := str(badge_text).strip_edges() + if text.is_empty(): + continue + badge_row.add_child(_make_chapter_info_badge(text, tooltip_text)) func _add_chapter_scenario_entry(scenario: Dictionary) -> void: var scenario_id := str(scenario.get("id", "")) var title := str(scenario.get("title", scenario_id)) - var position := int(scenario.get("position", 0)) - var total := int(scenario.get("total", 0)) - var completed := bool(scenario.get("completed", false)) - var current := bool(scenario.get("current", false)) var available := bool(scenario.get("available", false)) - var status := "현 전장" if current else ("회상 가능" if completed else "미개봉") - var text := " %02d/%02d %s [%s]" % [position, total, title, status] + var tooltip_text := _format_chapter_scenario_tooltip(scenario) + var row := HBoxContainer.new() + row.custom_minimum_size = Vector2(580, 58) + row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_theme_constant_override("separation", 8) + row.tooltip_text = tooltip_text + chapter_list.add_child(row) + + row.add_child(_make_chapter_scenario_thumbnail(scenario, tooltip_text)) + + var column := VBoxContainer.new() + column.custom_minimum_size = Vector2(500, 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 scenario_button := Button.new() + scenario_button.custom_minimum_size = Vector2(500, 28) + scenario_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scenario_button.text = title + scenario_button.tooltip_text = tooltip_text + scenario_button.disabled = not available + scenario_button.add_theme_font_size_override("font_size", 12) + _apply_button_style(scenario_button) if available: - var scenario_button := Button.new() - scenario_button.custom_minimum_size = Vector2(560, 0) - scenario_button.text = text - _apply_button_style(scenario_button) scenario_button.pressed.connect(_on_chapter_scenario_pressed.bind(scenario_id)) - chapter_list.add_child(scenario_button) + column.add_child(scenario_button) + + var badge_row := HBoxContainer.new() + badge_row.custom_minimum_size = Vector2(500, 22) + badge_row.add_theme_constant_override("separation", 5) + badge_row.tooltip_text = tooltip_text + column.add_child(badge_row) + + for badge_text in _format_chapter_scenario_badges(scenario): + var text := str(badge_text).strip_edges() + if text.is_empty(): + continue + badge_row.add_child(_make_chapter_info_badge(text, tooltip_text)) + + +func _format_chapter_progress_badges(entry: Dictionary) -> Array: + var completed := int(entry.get("completed", 0)) + var total := int(entry.get("total", 0)) + return [ + _chapter_progress_status_text(entry), + "전장 %d/%d" % [completed, total] + ] + + +func _format_chapter_scenario_badges(scenario: Dictionary) -> Array: + return [ + "%02d/%02d" % [int(scenario.get("position", 0)), int(scenario.get("total", 0))], + _chapter_scenario_status_text(scenario), + "열람" if bool(scenario.get("available", false)) else "잠김" + ] + + +func _chapter_progress_status_text(entry: Dictionary) -> String: + var completed := int(entry.get("completed", 0)) + var total := int(entry.get("total", 0)) + if bool(entry.get("current", false)): + return "진행 중" + if total > 0 and completed >= total: + return "완료" + return "미개방" + + +func _chapter_scenario_status_text(scenario: Dictionary) -> String: + if bool(scenario.get("current", false)): + return "현 전장" + if bool(scenario.get("completed", false)): + return "회상" + return "미개방" + + +func _format_chapter_progress_tooltip(entry: Dictionary) -> String: + var lines := [ + "병서 %02d/%02d · %s" % [ + int(entry.get("position", 0)), + int(entry.get("chapter_count", 0)), + str(entry.get("title", "")) + ], + "상태: %s" % _chapter_progress_status_text(entry), + "전장 진행: %d/%d" % [int(entry.get("completed", 0)), int(entry.get("total", 0))] + ] + var current_title := str(entry.get("current_scenario_title", "")).strip_edges() + if bool(entry.get("current", false)) and not current_title.is_empty(): + lines.append("현 전장: %s" % current_title) + return _join_strings(lines, "\n") + + +func _format_chapter_scenario_tooltip(scenario: Dictionary) -> String: + var title := str(scenario.get("title", scenario.get("id", ""))) + var status := _chapter_scenario_status_text(scenario) + var lines := [ + "%02d/%02d · %s" % [int(scenario.get("position", 0)), int(scenario.get("total", 0)), title], + "상태: %s" % status + ] + if bool(scenario.get("current", false)): + lines.append("클릭하면 이 전장의 군막으로 이동합니다.") + elif bool(scenario.get("completed", false)): + lines.append("클릭하면 회상 전투를 엽니다.") else: - var scenario_label := Label.new() - scenario_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - scenario_label.custom_minimum_size = Vector2(560, 0) - scenario_label.text = text - _apply_label_style(scenario_label, UI_AGED_INK) - chapter_list.add_child(scenario_label) + lines.append("앞 전장을 마치면 열립니다.") + return _join_strings(lines, "\n") + + +func _make_chapter_info_badge(text: String, tooltip_text: String) -> PanelContainer: + var panel := PanelContainer.new() + panel.custom_minimum_size = CHAPTER_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 _make_chapter_scenario_thumbnail(scenario: Dictionary, tooltip_text: String) -> PanelContainer: + var panel := PanelContainer.new() + panel.custom_minimum_size = CHAPTER_SCENARIO_THUMBNAIL_SIZE + panel.tooltip_text = tooltip_text + _apply_panel_style(panel, "map") + + var stack := Control.new() + stack.custom_minimum_size = CHAPTER_SCENARIO_THUMBNAIL_SIZE + stack.tooltip_text = tooltip_text + panel.add_child(stack) + + var texture_rect := TextureRect.new() + texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT) + texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED + texture_rect.modulate = Color(0.92, 0.82, 0.62, 0.96) + texture_rect.tooltip_text = tooltip_text + var texture := _load_art_texture(_chapter_scenario_background_path(str(scenario.get("id", "")))) + texture_rect.texture = texture + texture_rect.visible = texture != null + stack.add_child(texture_rect) + + var fallback_label := Label.new() + fallback_label.set_anchors_preset(Control.PRESET_FULL_RECT) + fallback_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + fallback_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + fallback_label.text = _dialogue_portrait_initials(str(scenario.get("title", scenario.get("id", "")))) if texture == null else "" + fallback_label.visible = texture == null + fallback_label.tooltip_text = tooltip_text + _apply_label_style(fallback_label, UI_OLD_BRONZE, 16) + stack.add_child(fallback_label) + stack.add_child(_make_map_lattice_overlay(CHAPTER_SCENARIO_THUMBNAIL_SIZE)) + return panel + + +func _chapter_scenario_background_path(scenario_id: String) -> String: + var scenario_path := campaign_state.get_scenario_path(scenario_id) + if scenario_path.is_empty() or not FileAccess.file_exists(scenario_path): + return "" + var parsed = JSON.parse_string(FileAccess.get_file_as_string(scenario_path)) + if typeof(parsed) != TYPE_DICTIONARY: + return "" + var map_data = parsed.get("map", {}) + if typeof(map_data) != TYPE_DICTIONARY: + return "" + return str(map_data.get("background", "")) func _on_chapter_scenario_pressed(scenario_id: String) -> void: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 6e683f9..7f03d6a 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -2994,6 +2994,8 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: ) if not bean_tooltip.contains("\n") or not bean_tooltip.contains("보유 0점"): failures.append("shop item tooltip should preserve full item detail") + scene.campaign_state.load_campaign(BattleSceneScript.CAMPAIGN_PATH) + scene.campaign_state.start_new("001_yellow_turbans") scene._create_hud() scene.campaign_state.gold = 320 scene.shop_sell_mode = false @@ -3032,6 +3034,22 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: failures.append("formation rows should reuse officer portrait artwork") if scene._format_formation_unit_button_text(scene.state.get_unit("cao_cao")).contains(","): failures.append("formation unit button text should stay compact") + scene._rebuild_chapter_menu() + var chapter_status_text: String = scene.chapter_status_label.text if scene.chapter_status_label != null else "" + var chapter_visible_text := _collect_child_text(scene.chapter_list) + var chapter_tooltips := _collect_child_tooltips(scene.chapter_list) + if not chapter_status_text.contains("조조전") or not chapter_status_text.contains("진행"): + failures.append("chapter status should stay compact and campaign-aware: %s" % chapter_status_text) + if not chapter_visible_text.contains("난세의 기치") or not chapter_visible_text.contains("영천 소전") or not chapter_visible_text.contains("현 전장"): + failures.append("chapter rows should show compact titles and state badges: %s" % chapter_visible_text) + if chapter_visible_text.contains("[") or chapter_visible_text.contains("현 전장:") or chapter_visible_text.contains("앞 전장을 마치면"): + failures.append("chapter rows should keep dense state detail in hover text: %s" % chapter_visible_text) + if chapter_visible_text.contains("백마 구원전"): + failures.append("chapter rows should not dump unopened future chapter battles: %s" % chapter_visible_text) + if not chapter_tooltips.contains("현 전장: 영천 소전") or not chapter_tooltips.contains("클릭하면 이 전장의 군막으로 이동합니다.") or not chapter_tooltips.contains("앞 전장을 마치면 열립니다."): + 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") scene.free() _check_scenario_background_texture_loading(failures)