Feature opening story art on title screen

This commit is contained in:
2026-06-20 05:38:53 +09:00
parent 1f7afc9d1b
commit 197b3cb10d
3 changed files with 103 additions and 14 deletions

View File

@@ -126,7 +126,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
- Campaign completion screen.
- New campaign save reset.
- Manual campaign checkpoint save/load.
- Cinematic title screen uses a dedicated ancient-war-camp dawn backdrop and Cao Cao portrait art, offers start/load/settings, previews the four generated opening story panels on the title menu, leads new campaigns through a generated four-panel Yellow Turban rebellion, Cao Cao resolve, Xiahou Dun meeting, and Yingchuan contact prologue with full-screen story artwork, a chapter seal, a highlighted four-beat story strip, and a bottom scroll caption panel, exposes the same audio/edge-scroll settings from the top in-game toolbar, includes a one-click audio recovery control, and persists BGM/SFX volume, sound toggles, and edge-scroll preference in `user://heros_settings.json`.
- Cinematic title screen uses a dedicated ancient-war-camp dawn backdrop and Cao Cao portrait art, offers start/load/settings, opens with a large generated Yellow Turban story feature panel plus four generated opening story previews on the title menu, leads new campaigns through a generated four-panel Yellow Turban rebellion, Cao Cao resolve, Xiahou Dun meeting, and Yingchuan contact prologue with full-screen story artwork, a chapter seal, a highlighted four-beat story strip, and a bottom scroll caption panel, exposes the same audio/edge-scroll settings from the top in-game toolbar, includes a one-click audio recovery control, and persists BGM/SFX volume, sound toggles, and edge-scroll preference in `user://heros_settings.json`.
- Victory and defeat result overlay.
- Dialogue portrait slot stays fixed across speaker and narration lines, with officer default image paths, optional per-line overrides, and speaker-initial or record fallback.
- Initial AI-generated photorealistic officer portraits for Cao Cao, Xiahou Dun, Xiahou Yuan, Cao Ren, Dian Wei, Guo Jia, and Zhang He.

View File

@@ -142,6 +142,8 @@ const BRIEFING_TACTICAL_MARKER_MAX_COUNT := 3
const BRIEFING_FORCE_PREVIEW_ICON_SIZE := Vector2(32, 32)
const BRIEFING_FORCE_PREVIEW_MAX_ALLIES := 2
const BRIEFING_FORCE_PREVIEW_MAX_ENEMY_TYPES := 4
const TITLE_STORY_FEATURE_SIZE := Vector2(616, 122)
const TITLE_STORY_FEATURE_IMAGE_SIZE := Vector2(238, 96)
const OPENING_STORY_BEAT_CARD_SIZE := Vector2(122, 66)
const OPENING_STORY_BEAT_IMAGE_SIZE := Vector2(112, 42)
const COMMAND_NOTICE_ICON_SIZE := Vector2(32, 32)
@@ -1968,14 +1970,85 @@ func _make_seal_cluster(height: float) -> HBoxContainer:
return cluster
func _make_title_story_preview_strip() -> PanelContainer:
func _make_title_story_feature_panel() -> PanelContainer:
var page: Dictionary = OPENING_PROLOGUE_PAGES[0]
var title := str(page.get("title", "황건의 난"))
var body := str(page.get("body", ""))
var image_path := str(page.get("image", ""))
var tooltip := "%s\n%s" % [title, body]
var panel := PanelContainer.new()
panel.name = "TitleStoryPreviewStrip"
panel.custom_minimum_size = Vector2(616, 88)
panel.name = "TitleStoryFeaturePanel"
panel.custom_minimum_size = TITLE_STORY_FEATURE_SIZE
panel.tooltip_text = tooltip
_apply_panel_style(panel, "hud_info")
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(584, 68)
row.custom_minimum_size = Vector2(584, 104)
row.add_theme_constant_override("separation", 10)
row.tooltip_text = tooltip
panel.add_child(row)
var image_stack := PanelContainer.new()
image_stack.name = "TitleStoryFeatureImageFrame"
image_stack.custom_minimum_size = Vector2(252, 104)
image_stack.tooltip_text = tooltip
_apply_panel_style(image_stack, "caption")
row.add_child(image_stack)
var image := TextureRect.new()
image.name = "TitleStoryFeatureImage"
image.custom_minimum_size = TITLE_STORY_FEATURE_IMAGE_SIZE
image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
image.texture = _load_art_texture(image_path)
image.modulate = Color(0.98, 0.95, 0.88, 0.98)
image.tooltip_text = tooltip
image_stack.add_child(image)
var text_column := VBoxContainer.new()
text_column.custom_minimum_size = Vector2(320, 104)
text_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
text_column.add_theme_constant_override("separation", 4)
text_column.tooltip_text = tooltip
row.add_child(text_column)
var title_row := HBoxContainer.new()
title_row.custom_minimum_size = Vector2(320, 26)
title_row.add_theme_constant_override("separation", 6)
title_row.tooltip_text = tooltip
text_column.add_child(title_row)
title_row.add_child(_make_seal_tile("1", 24))
var title_label := Label.new()
title_label.name = "TitleStoryFeatureTitle"
title_label.text = title
title_label.custom_minimum_size = Vector2(284, 26)
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.tooltip_text = tooltip
_apply_label_style(title_label, UI_PARCHMENT_TEXT, 18)
title_row.add_child(title_label)
var body_label := Label.new()
body_label.name = "TitleStoryFeatureBody"
body_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
body_label.custom_minimum_size = Vector2(320, 68)
body_label.tooltip_text = tooltip
_apply_label_style(body_label, UI_AGED_INK, 13)
_set_fitted_label_text(body_label, body, 13, 10, body_label.custom_minimum_size)
text_column.add_child(body_label)
_set_control_tree_tooltip(panel, tooltip)
return panel
func _make_title_story_preview_strip() -> PanelContainer:
var panel := PanelContainer.new()
panel.name = "TitleStoryPreviewStrip"
panel.custom_minimum_size = Vector2(616, 78)
_apply_panel_style(panel, "hud_info")
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(584, 58)
row.add_theme_constant_override("separation", 8)
panel.add_child(row)
@@ -1988,13 +2061,13 @@ func _make_title_story_preview_strip() -> PanelContainer:
func _make_title_story_preview_card(title: String, image_path: String, index: int) -> VBoxContainer:
var card := VBoxContainer.new()
card.name = "TitleStoryPreviewCard%d" % index
card.custom_minimum_size = Vector2(140, 68)
card.custom_minimum_size = Vector2(140, 58)
card.add_theme_constant_override("separation", 3)
card.tooltip_text = "%d. %s" % [index, title]
var texture_rect := TextureRect.new()
texture_rect.name = "TitleStoryPreviewImage"
texture_rect.custom_minimum_size = Vector2(140, 48)
texture_rect.custom_minimum_size = Vector2(140, 38)
texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
texture_rect.texture = _load_art_texture(image_path)
@@ -3764,22 +3837,22 @@ func _create_hud() -> void:
title_panel = PanelContainer.new()
title_panel.name = "TitleMenuPanel"
title_panel.position = Vector2(58, 46)
title_panel.size = Vector2(708, 628)
title_panel.custom_minimum_size = Vector2(708, 628)
title_panel.position = Vector2(58, 24)
title_panel.size = Vector2(708, 670)
title_panel.custom_minimum_size = Vector2(708, 670)
_apply_panel_style(title_panel, "paper")
title_screen_root.add_child(title_panel)
var title_column := VBoxContainer.new()
title_column.alignment = BoxContainer.ALIGNMENT_CENTER
title_column.add_theme_constant_override("separation", 10)
title_column.add_theme_constant_override("separation", 7)
title_panel.add_child(title_column)
title_column.add_child(_make_scroll_rod(616, 5))
title_column.add_child(_make_seal_ribbon(616, 18))
title_caption_label = Label.new()
title_caption_label.text = "조조전"
title_caption_label.custom_minimum_size = Vector2(616, 58)
title_caption_label.custom_minimum_size = Vector2(616, 48)
title_caption_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_caption_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_apply_label_style(title_caption_label, UI_PARCHMENT_TEXT, 34)
@@ -3787,16 +3860,17 @@ func _create_hud() -> void:
title_subtitle_label = Label.new()
title_subtitle_label.text = "난세를 여는 첫 군령"
title_subtitle_label.custom_minimum_size = Vector2(616, 34)
title_subtitle_label.custom_minimum_size = Vector2(616, 26)
title_subtitle_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_subtitle_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_apply_label_style(title_subtitle_label, UI_OLD_BRONZE, 17)
title_column.add_child(title_subtitle_label)
title_column.add_child(_make_title_story_feature_panel())
title_column.add_child(_make_title_story_preview_strip())
var title_body_panel := PanelContainer.new()
title_body_panel.custom_minimum_size = Vector2(616, 314)
title_body_panel.custom_minimum_size = Vector2(616, 300)
_apply_panel_style(title_body_panel, "edict")
title_column.add_child(title_body_panel)

View File

@@ -74,6 +74,21 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void:
failures.append("title menu panel should sit on the left side of the cinematic title screen")
if scene.title_caption_label == null or scene.title_caption_label.text != "조조전":
failures.append("title screen should present the campaign title in Korean")
var title_story_feature: Node = null
if scene.title_screen_root != null:
title_story_feature = scene.title_screen_root.find_child("TitleStoryFeaturePanel", true, false)
if title_story_feature == null:
failures.append("title screen should feature a large generated opening story panel")
else:
var feature_image := title_story_feature.find_child("TitleStoryFeatureImage", true, false) as TextureRect
var feature_title := title_story_feature.find_child("TitleStoryFeatureTitle", true, false) as Label
var feature_body := title_story_feature.find_child("TitleStoryFeatureBody", true, false) as Label
if feature_image == null or feature_image.texture == null:
failures.append("title story feature should load generated opening artwork")
if feature_title == null or feature_title.text != "황건의 난":
failures.append("title story feature should name the first opening beat")
if feature_body == null or not feature_body.text.contains("조조") or not feature_body.text.contains("황건의 난"):
failures.append("title story feature should summarize the first opening beat in Korean")
var title_story_strip: Node = null
if scene.title_screen_root != null:
title_story_strip = scene.title_screen_root.find_child("TitleStoryPreviewStrip", true, false)