From f320124270912ede0a163833a225a2b0408d40ae Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 15:27:47 +0900 Subject: [PATCH] Polish QHD title story scaling --- README.md | 2 +- scripts/scenes/battle_scene.gd | 22 +++++++++++---- tools/smoke_title_menu.gd | 50 ++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5299ad2..562a9c9 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,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, Cao Cao portrait art, and a compact left-side command board with `처음부터`, `로드하기`, and `환경설정`; new campaigns then move into 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 compact Korean story-ledger strip instead of exposed numeric cards, and a bottom scroll caption panel. Title and in-game settings expose clearer background-music, combat-SFX, edge-scroll, and fullscreen display controls, include one-click audio recovery, and persist preferences in `user://heros_settings.json`. +- Cinematic title screen uses a dedicated ancient-war-camp dawn backdrop, Cao Cao portrait art, and a compact left-side command board with `처음부터`, `로드하기`, and `환경설정`; its generated command board, title buttons, and opening-story controls keep safer source-art proportions plus linear filtering for QHD fullscreen scaling. New campaigns then move into 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 compact Korean story-ledger strip instead of exposed numeric cards, and a bottom scroll caption panel. Title and in-game settings expose clearer background-music, combat-SFX, edge-scroll, and fullscreen display controls, include one-click audio recovery, and persist preferences 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 6fb4009..16dfc66 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -166,6 +166,8 @@ const TITLE_STORY_PREVIEW_IMAGE_FRAME_SIZE := Vector2(140, 74) const TITLE_STORY_PREVIEW_IMAGE_SIZE := Vector2(132, 66) const TITLE_STORY_PREVIEW_LABEL_SIZE := Vector2(140, 16) const TITLE_MENU_ICON_MAX_WIDTH := 42 +const TITLE_MENU_BOARD_SIZE := Vector2(520, 650) +const TITLE_MENU_PANEL_SIZE := Vector2(462, 536) const OPENING_STORY_PROGRESS_LEDGER_SIZE := Vector2(356, 56) const OPENING_STORY_PROGRESS_SEAL_SIZE := Vector2(38, 38) const COMMAND_NOTICE_ICON_SIZE := Vector2(32, 32) @@ -1132,6 +1134,7 @@ func _icon_button_texture_margin_for(button: Button) -> Vector2i: func _apply_generated_button_style(button: Button, important: bool = false) -> bool: + button.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR var icon_only := bool(button.get_meta("icon_only", false)) var title_button := bool(button.get_meta("title_menu_button", false)) var opening_story_button := bool(button.get_meta("opening_story_button", false)) @@ -3817,12 +3820,14 @@ func _create_hud() -> void: opening_prologue_root.name = "OpeningPrologue" opening_prologue_root.visible = false opening_prologue_root.mouse_filter = Control.MOUSE_FILTER_STOP + opening_prologue_root.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR opening_prologue_root.set_anchors_preset(Control.PRESET_FULL_RECT) root.add_child(opening_prologue_root) opening_prologue_background_texture_rect = TextureRect.new() opening_prologue_background_texture_rect.name = "OpeningStoryBackdrop" opening_prologue_background_texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + opening_prologue_background_texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR opening_prologue_background_texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT) opening_prologue_background_texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE opening_prologue_background_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED @@ -3833,6 +3838,7 @@ func _create_hud() -> void: opening_prologue_scene_texture_rect = TextureRect.new() opening_prologue_scene_texture_rect.name = "OpeningStoryImage" opening_prologue_scene_texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + opening_prologue_scene_texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR opening_prologue_scene_texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT) opening_prologue_scene_texture_rect.custom_minimum_size = Vector2(1280, 720) opening_prologue_scene_texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE @@ -4524,12 +4530,14 @@ func _create_hud() -> void: title_screen_root.name = "TitleScreen" title_screen_root.visible = false title_screen_root.mouse_filter = Control.MOUSE_FILTER_STOP + title_screen_root.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR title_screen_root.set_anchors_preset(Control.PRESET_FULL_RECT) root.add_child(title_screen_root) title_background_texture_rect = TextureRect.new() title_background_texture_rect.name = "TitleBattlefieldBackdrop" title_background_texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + title_background_texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR title_background_texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT) title_background_texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE title_background_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED @@ -4546,17 +4554,18 @@ func _create_hud() -> void: var title_menu_shadow := ColorRect.new() title_menu_shadow.name = "TitleMenuInkPanel" title_menu_shadow.position = Vector2(0, 28) - title_menu_shadow.size = Vector2(560, 608) + title_menu_shadow.size = Vector2(560, 650) title_menu_shadow.color = Color(0.030, 0.018, 0.010, 0.50) title_menu_shadow.mouse_filter = Control.MOUSE_FILTER_IGNORE title_screen_root.add_child(title_menu_shadow) title_menu_surface_texture_rect = TextureRect.new() title_menu_surface_texture_rect.name = "TitleGeneratedCommandBoard" - title_menu_surface_texture_rect.position = Vector2(28, 32) - title_menu_surface_texture_rect.size = Vector2(520, 626) - title_menu_surface_texture_rect.custom_minimum_size = Vector2(520, 626) + title_menu_surface_texture_rect.position = Vector2(28, 24) + title_menu_surface_texture_rect.size = TITLE_MENU_BOARD_SIZE + title_menu_surface_texture_rect.custom_minimum_size = TITLE_MENU_BOARD_SIZE title_menu_surface_texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + title_menu_surface_texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR title_menu_surface_texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE title_menu_surface_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED title_menu_surface_texture_rect.texture = _load_art_texture(TITLE_MENU_SURFACE_PATH) @@ -4577,6 +4586,7 @@ func _create_hud() -> void: title_portrait_texture_rect.position = Vector2(646, 42) title_portrait_texture_rect.size = Vector2(492, 626) title_portrait_texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + title_portrait_texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR title_portrait_texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE title_portrait_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED title_portrait_texture_rect.texture = _load_art_texture(TITLE_PORTRAIT_PATH) @@ -4598,8 +4608,8 @@ func _create_hud() -> void: title_panel = PanelContainer.new() title_panel.name = "TitleMenuPanel" title_panel.position = Vector2(58, 78) - title_panel.size = Vector2(462, 526) - title_panel.custom_minimum_size = Vector2(462, 526) + title_panel.size = TITLE_MENU_PANEL_SIZE + title_panel.custom_minimum_size = TITLE_MENU_PANEL_SIZE _apply_panel_style(title_panel, "transparent_overlay") title_screen_root.add_child(title_panel) diff --git a/tools/smoke_title_menu.gd b/tools/smoke_title_menu.gd index 6fcb272..4783730 100644 --- a/tools/smoke_title_menu.gd +++ b/tools/smoke_title_menu.gd @@ -74,6 +74,10 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("title screen should show a high-resolution battlefield backdrop") elif BattleSceneScript.TITLE_BACKGROUND_PATH == BattleSceneScript.DEFAULT_BATTLE_BACKGROUND_PATH: failures.append("title screen should use a dedicated cinematic background, not the first battle map backdrop") + if scene.title_screen_root == null or scene.title_screen_root.texture_filter != CanvasItem.TEXTURE_FILTER_LINEAR: + failures.append("title screen UI should use linear filtering for QHD fullscreen scaling") + if scene.title_background_texture_rect != null and scene.title_background_texture_rect.texture_filter != CanvasItem.TEXTURE_FILTER_LINEAR: + failures.append("title background should use linear filtering instead of jagged fullscreen scaling") for button_spec in [ {"button": scene.top_settings_button, "context": "top settings"}, {"button": scene.end_turn_button, "context": "top end turn"}, @@ -105,10 +109,16 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("title command board should carry generated surface metadata") if scene.title_menu_surface_texture_rect.texture.get_width() < 1024 or scene.title_menu_surface_texture_rect.texture.get_height() < 1024: failures.append("title command board should keep high-resolution generated pixels") + _check_texture_rect_aspect_match(failures, scene.title_menu_surface_texture_rect, "title command board", 0.015) + _check_control_inside_design_view(failures, scene.title_menu_surface_texture_rect, "title command board") + if scene.title_menu_surface_texture_rect.texture_filter != CanvasItem.TEXTURE_FILTER_LINEAR: + failures.append("title command board should use linear filtering at QHD") if scene.title_menu_surface_texture_rect.custom_minimum_size.x > 620.0 or scene.title_menu_surface_texture_rect.custom_minimum_size.y > 660.0: failures.append("title command board should stay compact so the three commands are not clipped: %s" % str(scene.title_menu_surface_texture_rect.custom_minimum_size)) if scene.title_panel != null and str(scene.title_panel.get_meta("panel_style_variant", "")) != "transparent_overlay": failures.append("title menu panel should be a transparent content layer over generated art") + if scene.title_panel != null: + _check_control_inside_design_view(failures, scene.title_panel, "title menu panel") if scene.title_caption_label == null or scene.title_caption_label.text != "조조전": failures.append("title screen should present the campaign title in Korean") else: @@ -256,6 +266,8 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("opening prologue should describe the current record without bare numeric counters") 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") + if scene.opening_prologue_root == null or scene.opening_prologue_root.texture_filter != CanvasItem.TEXTURE_FILTER_LINEAR: + failures.append("opening prologue UI should use linear filtering for QHD fullscreen scaling") var opening_story_strip: Node = null if scene.opening_prologue_root != null: opening_story_strip = scene.opening_prologue_root.find_child("OpeningStoryLedgerStrip", true, false) @@ -291,6 +303,7 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("opening story ledger should not expose bare numeric labels") if opening_story_strip.custom_minimum_size.x > 380.0 or opening_story_strip.custom_minimum_size.y > 62.0: failures.append("opening story ledger should stay compact in the upper-right corner: %s" % str(opening_story_strip.custom_minimum_size)) + _check_control_inside_design_view(failures, opening_story_strip as Control, "opening story ledger") if scene.opening_prologue_next_button == null or scene.opening_prologue_next_button.text != "전기 넘기기": failures.append("opening prologue next button should read as a story action") else: @@ -305,6 +318,8 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("opening prologue story artwork should use a wide cinematic frame") elif scene.opening_prologue_scene_texture_rect.custom_minimum_size.x < 1200 or scene.opening_prologue_scene_texture_rect.custom_minimum_size.y < 700: failures.append("opening prologue story artwork should fill the first screen instead of sitting in a small frame") + if scene.opening_prologue_scene_texture_rect != null and scene.opening_prologue_scene_texture_rect.texture_filter != CanvasItem.TEXTURE_FILTER_LINEAR: + failures.append("opening story artwork should use linear filtering at QHD") if scene.opening_prologue_body_label != null and scene.opening_prologue_body_label.custom_minimum_size.x < 730: failures.append("opening prologue body text should have enough caption width for Korean copy") if scene.opening_prologue_title_label != null: @@ -343,6 +358,8 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("opening prologue caption should use a flat high-contrast panel instead of a busy texture") elif opening_caption.z_index < 60: failures.append("opening prologue caption should render above the generated story image") + if opening_caption != null: + _check_control_inside_design_view(failures, opening_caption, "opening caption scroll") if opening_caption_backing == null: failures.append("opening prologue should keep a dedicated dark backing behind story text") elif opening_caption_backing.color.a < 0.55: @@ -498,6 +515,8 @@ func _check_title_menu_button_art(failures: Array[String], button: Button, expec return if not bool(button.get_meta("title_menu_button", false)): failures.append("%s should be configured as a generated title menu button" % context) + if button.texture_filter != CanvasItem.TEXTURE_FILTER_LINEAR: + failures.append("%s should use linear filtering so generated button art does not distort at QHD" % context) if str(button.get_meta("command_icon", "")) != expected_icon: failures.append("%s should keep command icon metadata `%s`, got `%s`" % [context, expected_icon, str(button.get_meta("command_icon", ""))]) if button.icon == null: @@ -526,6 +545,8 @@ func _check_opening_story_button_art(failures: Array[String], button: Button, co return if not bool(button.get_meta("opening_story_button", false)): failures.append("%s should use the compact opening-story button profile" % context) + if button.texture_filter != CanvasItem.TEXTURE_FILTER_LINEAR: + failures.append("%s should use linear filtering so generated button art stays smooth at QHD" % context) if button.expand_icon: failures.append("%s icon should not stretch in QHD fullscreen" % context) if button.get_theme_constant("icon_max_width") > 22: @@ -603,6 +624,35 @@ func _check_title_text_contrast( failures.append("%s font should be large enough to read, got %d" % [context, font_size]) +func _check_control_inside_design_view(failures: Array[String], control: Control, context: String, margin: float = 10.0) -> void: + if control == null: + failures.append("%s missing for design-view fit check" % context) + return + var size := control.custom_minimum_size + if size.x <= 0.0 or size.y <= 0.0: + size = control.size + var rect := Rect2(control.position, size) + var design_rect := Rect2(Vector2(margin, margin), Vector2(1280.0 - margin * 2.0, 720.0 - margin * 2.0)) + if not design_rect.encloses(rect): + failures.append("%s should fit inside the 1280x720 design viewport before QHD scaling: %s" % [context, str(rect)]) + + +func _check_texture_rect_aspect_match(failures: Array[String], rect: TextureRect, context: String, tolerance: float) -> void: + if rect == null or rect.texture == null: + failures.append("%s missing for aspect check" % context) + return + var target := rect.custom_minimum_size + if target.x <= 0.0 or target.y <= 0.0: + target = rect.size + if target.x <= 0.0 or target.y <= 0.0: + failures.append("%s has no target size for aspect check" % context) + return + var source_aspect := float(rect.texture.get_width()) / float(rect.texture.get_height()) + var target_aspect := target.x / target.y + if absf(source_aspect - target_aspect) > tolerance: + failures.append("%s should preserve generated-art aspect before QHD scaling: source %.3f target %.3f" % [context, source_aspect, target_aspect]) + + func _check_bgm_stream_quality(failures: Array[String], stream: AudioStream, label: String, minimum_length: float) -> void: if stream == null: failures.append("%s BGM stream should load before playback" % label)