extends SceneTree const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd") const SAVE_PATH := "user://campaign_save.json" const MANUAL_SAVE_PATH := "user://campaign_manual_save.json" const SETTINGS_PATH := "user://heros_settings.json" func _init() -> void: var save_backup := _backup_user_file(SAVE_PATH) var manual_backup := _backup_user_file(MANUAL_SAVE_PATH) var settings_backup := _backup_user_file(SETTINGS_PATH) var failures: Array[String] = [] _clear_user_file(SAVE_PATH) _clear_user_file(MANUAL_SAVE_PATH) _clear_user_file(SETTINGS_PATH) _check_title_menu_start_load_and_settings(failures) if not _restore_user_file(SAVE_PATH, save_backup): failures.append("could not restore campaign save after title menu smoke") if not _restore_user_file(MANUAL_SAVE_PATH, manual_backup): failures.append("could not restore manual campaign save after title menu smoke") if not _restore_user_file(SETTINGS_PATH, settings_backup): failures.append("could not restore title settings after title menu smoke") if failures.is_empty(): print("title menu smoke ok") quit(0) return for failure in failures: push_error(failure) quit(1) func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._ready() if scene.title_screen_root == null or not scene.title_screen_root.visible: failures.append("title screen should be visible on startup") if scene.bgm_player == null or scene.bgm_player.stream == null or scene.current_bgm_key != "menu": failures.append("title screen should start with an audible menu BGM stream") elif scene.bgm_player.volume_db < -12.0: failures.append("title menu BGM should be loud enough to hear by default: %.1fdb" % scene.bgm_player.volume_db) if BattleSceneScript.BGM_MENU_PATH.contains("placeholder") or BattleSceneScript.BGM_BATTLE_PATH.contains("placeholder"): failures.append("menu and battle BGM should use named mood tracks instead of placeholder assets") if not BattleSceneScript.BGM_MENU_PATH.ends_with("menu_camp_dawn_theme.wav"): failures.append("title menu should use the camp dawn theme, got %s" % BattleSceneScript.BGM_MENU_PATH) if not BattleSceneScript.BGM_BATTLE_PATH.ends_with("battle_yingchuan_march.wav"): failures.append("battle start should use the Yingchuan march theme, got %s" % BattleSceneScript.BGM_BATTLE_PATH) _check_bgm_stream_quality(failures, scene._bgm_stream_for("menu"), "menu", 20.0) _check_bgm_stream_quality(failures, scene._bgm_stream_for("battle"), "battle", 16.0) for sfx_key in [ "ui_click", "menu_confirm", "menu_cancel", "footstep", "slash", "bow_release", "hit_heavy", "guard_block", "skill_cast", "fire_skill", "item_pickup", "victory_sting", "defeat_sting" ]: if scene._sfx_stream_for(sfx_key) == null: failures.append("SFX stream should load: %s" % sfx_key) if scene.title_background_texture_rect == null or scene.title_background_texture_rect.texture == null: 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_portrait_texture_rect == null or scene.title_portrait_texture_rect.texture == null: failures.append("title screen should show Cao Cao portrait art") elif scene.title_portrait_texture_rect.material == null or not bool(scene.title_portrait_texture_rect.material.get_meta("title_portrait_feather_material", false)): failures.append("title Cao Cao portrait should feather into the background instead of reading as a hard rectangle") var portrait_shadow := scene.title_screen_root.find_child("TitlePortraitLacquerField", true, false) as ColorRect if scene.title_screen_root != null else null if portrait_shadow == null: failures.append("title screen should keep a subtle portrait grounding field") elif portrait_shadow.color.a > 0.08: failures.append("title portrait grounding field should not read as a black rectangle: %.2f alpha" % portrait_shadow.color.a) if scene.title_panel == null or scene.title_panel.position.x > 100.0: failures.append("title menu panel should sit on the left side of the cinematic title screen") elif scene.title_portrait_texture_rect != null and scene.title_panel.position.x + scene.title_panel.size.x >= scene.title_portrait_texture_rect.position.x: failures.append("title menu commands should stay clearly to the left of Cao Cao portrait") if scene.title_menu_surface_texture_rect == null or scene.title_menu_surface_texture_rect.texture == null: failures.append("title screen should use a generated command-board surface behind the menu") else: if not bool(scene.title_menu_surface_texture_rect.get_meta("generated_title_menu_surface", false)): 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") 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_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 not crowd the first menu with opening story feature artwork") var title_story_strip: Node = null if scene.title_screen_root != null: title_story_strip = scene.title_screen_root.find_child("TitleStoryPreviewStrip", true, false) if title_story_strip != null: failures.append("title screen should keep opening story previews inside the prologue, not the first menu") if scene.briefing_panel == null or scene.briefing_panel.visible: failures.append("briefing should stay hidden behind the title screen") if scene.title_start_button == null or scene.title_start_button.text != "처음부터": failures.append("title screen should expose `처음부터`") else: if not scene.title_start_button.visible: failures.append("title start command should be visible on first screen") if scene.title_start_button.custom_minimum_size.y < 54.0: failures.append("title start command should be tall enough to read clearly") _check_title_command_fit(failures, scene.title_start_button, "title start") _check_title_menu_button_art(failures, scene.title_start_button, "new", "title start") if scene.title_load_button == null or scene.title_load_button.text != "로드하기": failures.append("title screen should expose `로드하기`") elif not scene.title_load_button.disabled: failures.append("Load should be disabled when no save exists") else: if not scene.title_load_button.visible: failures.append("title load command should be visible on first screen") if scene.title_load_button.custom_minimum_size.y < 54.0: failures.append("title load command should be tall enough to read clearly") _check_title_command_fit(failures, scene.title_load_button, "title load") _check_title_menu_button_art(failures, scene.title_load_button, "load", "title load") if scene.title_load_panel == null: failures.append("title screen should create a load-slot selection panel") elif scene.title_load_panel.visible: failures.append("load-slot selection panel should stay hidden until Game Load is pressed") if scene.title_settings_button == null or scene.title_settings_button.text != "환경설정": failures.append("title screen should expose `환경설정`") else: if not scene.title_settings_button.visible: failures.append("title settings command should be visible on first screen") if scene.title_settings_button.custom_minimum_size.y < 54.0: failures.append("title settings command should be tall enough to read clearly") _check_title_command_fit(failures, scene.title_settings_button, "title settings") _check_title_menu_button_art(failures, scene.title_settings_button, "settings", "title settings") if scene.title_settings_back_button != null and scene.title_settings_back_button.visible: failures.append("title settings back command should stay hidden until settings are open") scene._on_title_settings_pressed() if scene.title_settings_panel == null or not scene.title_settings_panel.visible: failures.append("title settings panel should open from the title screen") if scene.title_start_button != null and scene.title_start_button.visible: failures.append("title settings should replace the main title commands while open") if scene.title_bgm_toggle == null or scene.title_sfx_toggle == null or scene.title_edge_scroll_toggle == null: failures.append("title settings should expose BGM, SFX, and edge-scroll toggles") else: if not scene.title_bgm_toggle.text.contains("군막의 새벽"): failures.append("title BGM toggle should name the active menu theme") if not scene.title_sfx_toggle.text.contains("클릭") or not scene.title_sfx_toggle.text.contains("발소리") or not scene.title_sfx_toggle.text.contains("전투"): failures.append("title SFX toggle should clarify menu, movement, and battle effects") if scene.title_bgm_volume_slider == null or scene.title_sfx_volume_slider == null: failures.append("title settings should expose BGM and SFX volume sliders") scene._on_title_bgm_toggled(false) if scene.title_bgm_enabled: failures.append("BGM toggle should disable background music") scene._on_title_bgm_volume_changed(45.0) if scene.title_bgm_volume_percent != 45: failures.append("BGM volume slider should update the saved BGM volume percent") scene._on_title_sfx_toggled(false) if scene.title_sfx_enabled: failures.append("SFX toggle should disable sound effects") scene._on_title_sfx_volume_changed(35.0) if scene.title_sfx_volume_percent != 35: failures.append("SFX volume slider should update the saved SFX volume percent") scene._on_title_edge_scroll_toggled(false) if scene.title_edge_scroll_enabled: failures.append("edge-scroll toggle should disable edge scrolling") if scene._update_edge_scroll(0.25): failures.append("edge scrolling should not move while disabled in settings") var persisted_scene = BattleSceneScript.new() persisted_scene._ready() if persisted_scene.title_bgm_enabled or persisted_scene.title_sfx_enabled or persisted_scene.title_edge_scroll_enabled: failures.append("title settings toggles should persist into a new scene instance") if persisted_scene.title_bgm_volume_percent != 45 or persisted_scene.title_sfx_volume_percent != 35: failures.append("title settings volume values should persist into a new scene instance") persisted_scene.free() if scene.title_audio_reset_button == null or scene.title_audio_reset_button.text != "기본값 듣기": failures.append("title settings should expose a clear audio defaults preview button") else: _check_title_menu_button_art(failures, scene.title_audio_reset_button, "status", "title audio reset") if not scene.title_audio_reset_button.tooltip_text.contains("확인음") or not scene.title_audio_reset_button.tooltip_text.contains("발소리") or not scene.title_audio_reset_button.tooltip_text.contains("타격음"): failures.append("title audio preview should explain the confirmation, movement, and strike samples") scene.recent_sfx_keys.clear() scene._on_title_audio_reset_pressed() if not scene.title_bgm_enabled or not scene.title_sfx_enabled: failures.append("title audio recovery should re-enable BGM and SFX") if scene.title_bgm_volume_percent != BattleSceneScript.DEFAULT_AUDIO_VOLUME_PERCENT or scene.title_sfx_volume_percent != BattleSceneScript.DEFAULT_AUDIO_VOLUME_PERCENT: failures.append("title audio recovery should restore audible volume defaults") if scene.bgm_player == null or scene.bgm_player.stream == null or scene.current_bgm_key.is_empty(): failures.append("title audio recovery should leave a playable BGM stream selected") if not scene.recent_sfx_keys.has("menu_confirm"): failures.append("title audio preview should play an audible confirmation SFX") if not scene.recent_sfx_keys.has("footstep"): failures.append("title audio preview should play a movement footstep SFX") if not scene.recent_sfx_keys.has("slash"): failures.append("title audio preview should play a strike SFX") scene._on_title_settings_back_pressed() if scene.title_settings_panel != null and scene.title_settings_panel.visible: failures.append("settings back should hide the title settings panel") scene._on_title_start_pressed() if scene.title_screen_root != null and scene.title_screen_root.visible: failures.append("starting a new game should hide the title screen") if not scene._is_opening_prologue_visible(): failures.append("starting a new campaign should show the opening prologue before briefing") if scene.briefing_panel != null and scene.briefing_panel.visible: failures.append("opening prologue should hold back the first briefing until advanced") if scene.opening_prologue_title_label == null or scene.opening_prologue_body_label == null: failures.append("opening prologue should expose title and story text") elif not scene.opening_prologue_body_label.text.contains("조조") or not scene.opening_prologue_body_label.text.contains("황건의 난"): failures.append("opening prologue should first explain Cao Cao before the Yellow Turban battle") if scene.opening_prologue_step_label == null or not scene.opening_prologue_step_label.text.begins_with("장면 ") or not scene.opening_prologue_step_label.text.ends_with("/4"): 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_frame := card_node.find_child("OpeningStoryBeatImageFrame", true, false) as PanelContainer var beat_seal := card_node.find_child("OpeningStoryBeatSeal", true, false) as PanelContainer 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_frame == null or not bool(beat_frame.get_meta("generated_panel_texture", false)): failures.append("opening story beat image should sit inside a generated frame") if beat_seal == null or not bool(beat_seal.get_meta("generated_panel_texture", false)): failures.append("opening story beat should use a generated seal number") if beat_image == null or beat_image.texture == null: failures.append("opening story beat card should load generated artwork") elif beat_image.custom_minimum_size.x < 116.0: failures.append("opening story beat artwork should stay readable in the strip: %s" % str(beat_image.custom_minimum_size)) 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: 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_body_label != null and scene.opening_prologue_body_label.custom_minimum_size.x < 680: failures.append("opening prologue body text should have enough caption width for Korean copy") var opening_caption: PanelContainer = null if scene.opening_prologue_root != null: opening_caption = scene.opening_prologue_root.find_child("OpeningCaptionScroll", true, false) as PanelContainer if opening_caption == null: failures.append("opening prologue should keep a bottom scroll caption over the artwork") elif opening_caption.custom_minimum_size.y > 124.0 or opening_caption.position.y < 540.0: failures.append("opening prologue caption should stay low and compact so the image leads: %s at %s" % [str(opening_caption.custom_minimum_size), str(opening_caption.position)]) var first_story_texture: Texture2D = null if scene.opening_prologue_scene_texture_rect != null: first_story_texture = scene.opening_prologue_scene_texture_rect.texture 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("작은 군세"): failures.append("opening prologue decision page should explain why Cao Cao raises troops") scene._on_opening_prologue_next_pressed() if scene.opening_prologue_title_label == null or scene.opening_prologue_title_label.text != "하후돈의 합류": failures.append("opening prologue should then show Xiahou Dun joining Cao Cao") elif not scene.opening_prologue_body_label.text.contains("하후돈") or not scene.opening_prologue_body_label.text.contains("첫 군령"): failures.append("opening prologue Xiahou Dun page should link the relationship to the first command") scene._on_opening_prologue_next_pressed() if scene.opening_prologue_title_label == null or scene.opening_prologue_title_label.text != "영천으로": failures.append("opening prologue should end by pointing toward Yingchuan") var prologue_guard := 0 while scene._is_opening_prologue_visible() and prologue_guard < 8: scene._on_opening_prologue_next_pressed() prologue_guard += 1 if scene._is_opening_prologue_visible(): failures.append("opening prologue should close after advancing through its pages") if scene.briefing_panel == null or not scene.briefing_panel.visible: failures.append("finishing the opening prologue should enter the first briefing") var audio_scene = BattleSceneScript.new() audio_scene._ready() audio_scene._enter_campaign_from_title() audio_scene._start_battle_from_briefing() if audio_scene.current_bgm_key != "battle" or audio_scene.bgm_player == null or audio_scene.bgm_player.stream == null: failures.append("battle start should switch from menu music to an audible battle BGM stream") audio_scene._show_title_menu() if audio_scene.current_bgm_key != "menu" or audio_scene.bgm_player == null or audio_scene.bgm_player.stream == null: failures.append("returning to title should restore the menu BGM stream") audio_scene.free() if not scene.campaign_state.has_save(): failures.append("starting a new game should create the main campaign save") if not scene.campaign_state.save_manual_game(): failures.append("manual checkpoint should be writable for title load selection") scene._show_title_menu() scene._update_title_menu_state() if scene.title_load_button == null or scene.title_load_button.disabled: failures.append("Game Load should become available after a campaign save exists") scene._on_title_load_pressed() if scene.title_load_panel == null or not scene.title_load_panel.visible: failures.append("Game Load should open a load-slot selection panel instead of loading immediately") if scene.title_start_button != null and scene.title_start_button.visible: failures.append("load-slot selection should replace the main title commands while open") if scene.title_load_auto_button == null or scene.title_load_auto_button.disabled: failures.append("load-slot selection should enable the automatic campaign save") elif not scene.title_load_auto_button.text.contains("자동 기록"): failures.append("automatic load slot should be labeled in Korean") else: _check_title_menu_button_art(failures, scene.title_load_auto_button, "campaign", "title automatic load slot") if scene.title_load_manual_button == null or scene.title_load_manual_button.disabled: failures.append("load-slot selection should enable the manual checkpoint save") elif not scene.title_load_manual_button.text.contains("수동 봉인"): failures.append("manual load slot should be labeled in Korean") else: _check_title_menu_button_art(failures, scene.title_load_manual_button, "save", "title manual load slot") scene._show_title_menu() if scene.title_load_panel != null and scene.title_load_panel.visible: failures.append("showing the title menu again should reset the load-slot panel") scene._on_title_load_pressed() scene._on_title_load_back_pressed() if scene.title_load_panel != null and scene.title_load_panel.visible: failures.append("load-slot back should close the title load panel") scene._on_title_load_pressed() scene._on_title_load_auto_pressed() if scene.title_screen_root != null and scene.title_screen_root.visible: failures.append("loading a saved game should hide the title screen") if scene._is_opening_prologue_visible(): failures.append("loading a saved game should skip the new-campaign opening prologue") if scene.briefing_panel == null or not scene.briefing_panel.visible: failures.append("loading a saved game should enter the current campaign briefing") if scene.top_settings_button == null: failures.append("top toolbar should expose an in-game Settings icon") scene._on_settings_pressed() if scene.settings_panel == null or not scene.settings_panel.visible: failures.append("top Settings icon should open the in-game settings panel") if scene.settings_bgm_toggle == null or scene.settings_sfx_toggle == null or scene.settings_edge_scroll_toggle == null: failures.append("in-game settings panel should expose BGM, SFX, and edge-scroll toggles") else: if not scene.settings_bgm_toggle.text.contains("군막") or not scene.settings_bgm_toggle.text.contains("전투"): failures.append("in-game BGM toggle should clarify menu and battle music") if not scene.settings_sfx_toggle.text.contains("선택") or not scene.settings_sfx_toggle.text.contains("발소리") or not scene.settings_sfx_toggle.text.contains("전투"): failures.append("in-game SFX toggle should clarify menu, movement, and battle effects") if scene.settings_bgm_volume_slider == null or scene.settings_sfx_volume_slider == null: failures.append("in-game settings panel should expose BGM and SFX volume sliders") scene._on_settings_bgm_toggled(true) scene._on_settings_sfx_toggled(true) scene._on_settings_edge_scroll_toggled(true) scene._on_settings_bgm_volume_changed(70.0) scene._on_settings_sfx_volume_changed(60.0) if not scene.title_bgm_enabled or not scene.title_sfx_enabled or not scene.title_edge_scroll_enabled: failures.append("in-game settings toggles should update the shared settings state") if scene.title_bgm_volume_percent != 70 or scene.title_sfx_volume_percent != 60: failures.append("in-game settings sliders should update the shared volume state") if scene.settings_audio_reset_button == null or scene.settings_audio_reset_button.text != "기본값 듣기": failures.append("in-game settings should expose a clear audio defaults preview button") elif not scene.settings_audio_reset_button.tooltip_text.contains("현재 음악"): failures.append("in-game audio preview should explain that it plays the current music") scene._on_settings_bgm_toggled(false) scene._on_settings_sfx_toggled(false) scene._on_settings_bgm_volume_changed(20.0) scene._on_settings_sfx_volume_changed(15.0) scene.recent_sfx_keys.clear() scene._on_settings_audio_reset_pressed() if not scene.title_bgm_enabled or not scene.title_sfx_enabled: failures.append("in-game audio recovery should re-enable BGM and SFX") if scene.title_bgm_volume_percent != BattleSceneScript.DEFAULT_AUDIO_VOLUME_PERCENT or scene.title_sfx_volume_percent != BattleSceneScript.DEFAULT_AUDIO_VOLUME_PERCENT: failures.append("in-game audio recovery should restore audible volume defaults") if not scene.recent_sfx_keys.has("menu_confirm"): failures.append("in-game audio preview should play an audible confirmation SFX") if not scene.recent_sfx_keys.has("footstep"): failures.append("in-game audio preview should play a movement footstep SFX") if not scene.recent_sfx_keys.has("slash"): failures.append("in-game audio preview should play a strike SFX") scene._hide_settings_panel() scene._on_new_campaign_pressed() if not scene._is_opening_prologue_visible(): failures.append("top New Campaign should also show the opening prologue") scene._on_opening_prologue_skip_pressed() if scene.briefing_panel == null or not scene.briefing_panel.visible: failures.append("skipping the top New Campaign prologue should enter the first briefing") scene.free() func _check_title_menu_button_art(failures: Array[String], button: Button, expected_icon: String, context: String) -> void: if button == null: failures.append("%s button missing" % context) 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 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: failures.append("%s should expose generated icon art" % context) elif button.icon.get_width() > BattleSceneScript.TITLE_MENU_ICON_MAX_WIDTH or button.icon.get_height() > BattleSceneScript.TITLE_MENU_ICON_MAX_WIDTH: failures.append("%s icon should render at title-menu size, got %dx%d" % [context, button.icon.get_width(), button.icon.get_height()]) var stylebox := button.get_theme_stylebox("normal") if not (stylebox is StyleBoxTexture): failures.append("%s should use a generated button texture surface" % context) return var style := stylebox as StyleBoxTexture if style.texture == null: failures.append("%s generated button surface is missing its texture" % context) elif style.texture.get_width() < 512 or style.texture.get_height() < 192: failures.append("%s generated button texture should keep high-resolution source pixels: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()]) if style.texture_margin_top > 24 or style.texture_margin_bottom > 24: failures.append("%s generated button slice should stay shallow enough for the 720p title menu: %d/%d" % [context, style.texture_margin_top, style.texture_margin_bottom]) if str(button.tooltip_text).strip_edges().is_empty(): failures.append("%s should describe its command through a tooltip" % context) func _check_title_command_fit(failures: Array[String], button: Button, context: String) -> void: if button == null: return if button.expand_icon: failures.append("%s icon should not expand into an oversized title command" % context) var icon_limit := button.get_theme_constant("icon_max_width") if icon_limit <= 0 or icon_limit > BattleSceneScript.TITLE_MENU_ICON_MAX_WIDTH: failures.append("%s icon should be capped at title-menu size, got %d" % [context, icon_limit]) var combined := button.get_combined_minimum_size() if combined.y > 76.0: failures.append("%s combined height should fit inside the title menu, got %.1f" % [context, combined.y]) if combined.x > 420.0: failures.append("%s combined width should fit inside the title menu, got %.1f" % [context, combined.x]) 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) return var length := stream.get_length() if length < minimum_length: failures.append("%s BGM should be a substantial loop, got %.2fs expected %.2fs+" % [label, length, minimum_length]) func _backup_user_file(path: String) -> Dictionary: if not FileAccess.file_exists(path): return {"exists": false, "text": ""} return { "exists": true, "text": FileAccess.get_file_as_string(path) } func _restore_user_file(path: String, backup: Dictionary) -> bool: if bool(backup.get("exists", false)): var file := FileAccess.open(path, FileAccess.WRITE) if file == null: return false file.store_string(str(backup.get("text", ""))) return true return _clear_user_file(path) func _clear_user_file(path: String) -> bool: if not FileAccess.file_exists(path): return true return DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) == OK