Compact chapter overview rows

This commit is contained in:
2026-06-20 00:48:30 +09:00
parent 82dc5d1f2c
commit fc8ae7a0dd
3 changed files with 239 additions and 42 deletions

View File

@@ -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.

View File

@@ -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:

View File

@@ -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)