Add opening story beat strip

This commit is contained in:
2026-06-20 05:20:32 +09:00
parent ed13f9bac6
commit 2723cdb8d7
3 changed files with 111 additions and 1 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, 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, 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`.
- 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

@@ -139,6 +139,8 @@ const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(188, 86)
const BRIEFING_TACTICAL_MARKER_CHIP_SIZE := Vector2(122, 26)
const BRIEFING_TACTICAL_MARKER_ICON_SIZE := Vector2(22, 22)
const BRIEFING_TACTICAL_MARKER_MAX_COUNT := 3
const OPENING_STORY_BEAT_CARD_SIZE := Vector2(122, 66)
const OPENING_STORY_BEAT_IMAGE_SIZE := Vector2(112, 42)
const CHAPTER_SEAL_BADGE_SIZE := Vector2(54, 54)
const CHAPTER_SCENARIO_THUMBNAIL_SIZE := Vector2(72, 50)
const CHAPTER_INFO_BADGE_SIZE := Vector2(86, 22)
@@ -533,6 +535,7 @@ var opening_prologue_scene_texture_rect: TextureRect
var opening_prologue_title_label: Label
var opening_prologue_body_label: Label
var opening_prologue_step_label: Label
var opening_prologue_story_strip: HBoxContainer
var opening_prologue_next_button: Button
var opening_prologue_skip_button: Button
var opening_prologue_index := 0
@@ -2004,6 +2007,81 @@ func _make_title_story_preview_card(title: String, image_path: String, index: in
return card
func _make_opening_story_beat_strip() -> PanelContainer:
var panel := PanelContainer.new()
panel.name = "OpeningStoryBeatStrip"
panel.position = Vector2(630, 32)
panel.size = Vector2(594, 86)
panel.custom_minimum_size = Vector2(594, 86)
_apply_panel_style(panel, "hud_info")
opening_prologue_story_strip = HBoxContainer.new()
opening_prologue_story_strip.name = "OpeningStoryBeatRow"
opening_prologue_story_strip.custom_minimum_size = Vector2(562, 66)
opening_prologue_story_strip.add_theme_constant_override("separation", 8)
panel.add_child(opening_prologue_story_strip)
return panel
func _rebuild_opening_story_beat_strip() -> void:
if opening_prologue_story_strip == null:
return
for child in opening_prologue_story_strip.get_children():
opening_prologue_story_strip.remove_child(child)
child.queue_free()
for index in range(OPENING_PROLOGUE_PAGES.size()):
var page: Dictionary = OPENING_PROLOGUE_PAGES[index]
opening_prologue_story_strip.add_child(_make_opening_story_beat_card(page, index, index == opening_prologue_index))
func _make_opening_story_beat_card(page: Dictionary, index: int, is_current: bool) -> PanelContainer:
var title := str(page.get("title", "장면"))
var image_path := str(page.get("image", ""))
var tooltip := "장면 %d. %s" % [index + 1, title]
var card := PanelContainer.new()
card.name = "OpeningStoryBeatCard%d" % (index + 1)
card.custom_minimum_size = OPENING_STORY_BEAT_CARD_SIZE
card.tooltip_text = tooltip
card.set_meta("current_story_beat", is_current)
_apply_panel_style(card, "edict_compact" if is_current else "caption")
var card_style := card.get_theme_stylebox("panel")
if card_style != null:
card_style.content_margin_left = 5
card_style.content_margin_right = 5
card_style.content_margin_top = 4
card_style.content_margin_bottom = 3
card.modulate = Color(1.0, 1.0, 1.0, 1.0) if is_current else Color(0.56, 0.55, 0.50, 0.88)
var column := VBoxContainer.new()
column.custom_minimum_size = Vector2(112, 59)
column.add_theme_constant_override("separation", 2)
column.tooltip_text = tooltip
card.add_child(column)
var image := TextureRect.new()
image.name = "OpeningStoryBeatImage"
image.custom_minimum_size = OPENING_STORY_BEAT_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(1.0, 0.96, 0.88, 0.98) if is_current else Color(0.82, 0.80, 0.74, 0.72)
image.tooltip_text = tooltip
column.add_child(image)
var label := Label.new()
label.name = "OpeningStoryBeatLabel"
label.text = "%d %s" % [index + 1, title]
label.custom_minimum_size = Vector2(112, 15)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.clip_text = true
label.tooltip_text = tooltip
_apply_label_style(label, UI_PARCHMENT_TEXT if is_current else UI_OLD_BRONZE, 9)
column.add_child(label)
_set_control_tree_tooltip(card, tooltip)
return card
func _make_map_lattice_overlay(size: Vector2) -> Control:
var overlay := Control.new()
overlay.name = "MapLatticeOverlay"
@@ -2998,6 +3076,8 @@ func _create_hud() -> void:
_apply_label_style(opening_chapter_label, UI_PARCHMENT_TEXT, 21)
opening_chapter_panel.add_child(opening_chapter_label)
opening_prologue_root.add_child(_make_opening_story_beat_strip())
var opening_caption_panel := PanelContainer.new()
opening_caption_panel.name = "OpeningCaptionScroll"
opening_caption_panel.position = Vector2(56, 500)
@@ -11927,6 +12007,7 @@ func _update_opening_prologue_page() -> void:
if opening_prologue_next_button != null:
opening_prologue_next_button.text = "군막으로" if opening_prologue_index >= page_count - 1 else "다음"
opening_prologue_next_button.tooltip_text = "군막으로 이동합니다." if opening_prologue_index >= page_count - 1 else "다음 이야기로 넘깁니다."
_rebuild_opening_story_beat_strip()
func _on_opening_prologue_next_pressed() -> void:

View File

@@ -166,6 +166,30 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void:
failures.append("opening prologue should give Cao Cao, decision, Xiahou Dun, and Yingchuan their own beats")
if scene.opening_prologue_root == null or scene.opening_prologue_root.find_child("OpeningChapterSeal", true, false) == null:
failures.append("opening prologue should anchor the generated story art with a chapter seal")
var opening_story_strip: Node = null
if scene.opening_prologue_root != null:
opening_story_strip = scene.opening_prologue_root.find_child("OpeningStoryBeatStrip", true, false)
if opening_story_strip == null:
failures.append("opening prologue should show a generated four-beat story strip")
else:
var opening_story_cards: Array = opening_story_strip.find_children("OpeningStoryBeatCard*", "", true, false)
if opening_story_cards.size() != 4:
failures.append("opening story strip should expose four generated story beats: %d" % opening_story_cards.size())
var current_count := 0
var current_index := 0
for opening_story_card in opening_story_cards:
var card_node := opening_story_card as Node
var beat_image := card_node.find_child("OpeningStoryBeatImage", true, false) as TextureRect
var beat_label := card_node.find_child("OpeningStoryBeatLabel", true, false) as Label
if beat_image == null or beat_image.texture == null:
failures.append("opening story beat card should load generated artwork")
if beat_label == null or beat_label.text.strip_edges().is_empty():
failures.append("opening story beat card should name its scene")
if bool(card_node.get_meta("current_story_beat", false)):
current_count += 1
current_index = opening_story_cards.find(opening_story_card) + 1
if current_count != 1 or current_index != 1:
failures.append("opening story strip should highlight the first story beat on entry")
if scene.opening_prologue_scene_texture_rect == null or scene.opening_prologue_scene_texture_rect.texture == null:
failures.append("opening prologue should show generated story artwork")
elif scene.opening_prologue_scene_texture_rect.custom_minimum_size.x <= scene.opening_prologue_scene_texture_rect.custom_minimum_size.y:
@@ -180,6 +204,11 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void:
scene._on_opening_prologue_next_pressed()
if scene.opening_prologue_scene_texture_rect == null or scene.opening_prologue_scene_texture_rect.texture == first_story_texture:
failures.append("opening prologue should change story artwork per page")
var second_story_card: Node = null
if scene.opening_prologue_root != null:
second_story_card = scene.opening_prologue_root.find_child("OpeningStoryBeatCard2", true, false)
if second_story_card == null or not bool(second_story_card.get_meta("current_story_beat", false)):
failures.append("opening story strip should highlight the second beat after advancing")
if scene.opening_prologue_title_label == null or scene.opening_prologue_title_label.text != "맹덕의 결심":
failures.append("opening prologue should move from Cao Cao background to his decision")
elif not scene.opening_prologue_body_label.text.contains("황건") or not scene.opening_prologue_body_label.text.contains("작은 군세"):