diff --git a/README.md b/README.md index d81ebb6..ba79c71 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index b71db8e..ce74dd4 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -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: diff --git a/tools/smoke_title_menu.gd b/tools/smoke_title_menu.gd index a36ff72..dbfb596 100644 --- a/tools/smoke_title_menu.gd +++ b/tools/smoke_title_menu.gd @@ -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("작은 군세"):