extends SceneTree const AudioTeardown := preload("res://tests/helpers/ui_audio_teardown.gd") var _manual_save_path := "user://tests/main_dashboard_%d/manual_ui" % OS.get_process_id() var _checks := 0 var _failures := 0 var _main: Control var _preview: WorldPreview var _clock: Variant var _state: Variant var _runtime: Variant func _initialize() -> void: call_deferred("_run") func _run() -> void: _clock = root.get_node("GameClock") _state = root.get_node("GameState") _runtime = root.get_node("SimulationRuntime") _clock.reset(1, 6, 0) _state.reset() _runtime.initialize(_clock, _state, 424242) _clock.set_paused(true) root.size = Vector2i(1440, 810) _expect( ProjectSettings.get_setting("display/window/stretch/mode") == "canvas_items" and ProjectSettings.get_setting("display/window/stretch/aspect") == "expand" and bool(ProjectSettings.get_setting("display/window/dpi/allow_hidpi")), "project uses DPI-aware canvas scaling for high-resolution displays" ) _main = load("res://scenes/main/main.tscn").instantiate() as Control _main.set("manual_save_path", _manual_save_path) root.add_child(_main) await process_frame await process_frame _preview = _main.find_child("WorldPreview", true, false) as WorldPreview # A deferred tick must retain its retry UI through the compact projection. var previous_deferred: int = _runtime.get("_operations_deferred_runtime_minute") _runtime.set("_operations_deferred_runtime_minute", _clock.absolute_minute) _main.call("_update_pause_and_speed") _expect((_main.get("pause_button") as Button).text == "運営再試行", "compact projection preserves deferred tick retry button") _expect((_main.get("phase_label") as Label).text == "要確認", "compact projection preserves deferred tick warning") _runtime.set("_operations_deferred_runtime_minute", previous_deferred) _main.call("_update_pause_and_speed") _expect((_main.get("pause_button") as Button).text == "再開", "cleared deferred tick restores paused resume button") var before_weather: Dictionary = _runtime.create_snapshot() _expect(_runtime.weather.schedule(&"storm", _clock.absolute_minute + 1)[&"ok"], "schedule storm for visible weather boundary") _clock.advance_minutes(1) _main.call("_refresh_dashboard") _expect((_main.get("weather_label") as Label).text == "嵐", "HUD shows actual Runtime storm") if OS.get_environment("THE_TREE_WEATHER_CAPTURE") == "1": await process_frame await RenderingServer.frame_post_draw _expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/weather-2026-09-29/storm-hud.png")) == OK, "capture rendered storm HUD") _expect(_runtime.restore_snapshot(before_weather), "restore weather before UI drill") _main.call("_refresh_dashboard") _expect((_main.get("weather_label") as Label).text == "晴れ", "HUD follows restored weather state") var mana_label := (_main.get("resource_labels") as Dictionary)[&"mana"] as Label var original_mana_color := mana_label.get_theme_color("font_color") var original_mana: int = _state.get_resource(&"mana") _state.set_resource(&"mana", 0) _expect("停止" in mana_label.text and "0" in mana_label.text, "resource signal visibly identifies zero-reserve outage") _expect("残量と配電" in mana_label.tooltip_text and "休止は解除されません" in mana_label.tooltip_text, "mana HUD explains recovery without promising suspension reset") _expect("供給 +20" in mana_label.tooltip_text and "消費 −" in mana_label.tooltip_text and "蓄積 0/300" in mana_label.tooltip_text, "mana HUD shows A06 supply, consumption and storage") _expect(mana_label.text.ends_with("/300・停止"), "mana HUD shows capacity with outage") if OS.get_environment("THE_TREE_MANA_CAPTURE") == "1": await process_frame await RenderingServer.frame_post_draw var capture := ProjectSettings.globalize_path("res://artifacts/mana-2026-09-29/outage-hud.png") _expect(root.get_texture().get_image().save_png(capture) == OK, "capture rendered outage HUD") _state.set_resource(&"mana", original_mana) _expect(not "停止" in mana_label.text and mana_label.get_theme_color("font_color") == original_mana_color, "resource recovery clears outage label and alert color") _expect(_preview != null, "dashboard creates the live world preview") var visual_theme := _main.get("visual_theme") as VisualThemeProfile _expect( visual_theme != null and visual_theme.world_background_for_layer(WorldPreview.LayerMode.FRONT) != null and visual_theme.world_background_for_layer(WorldPreview.LayerMode.BACK) != null, "replaceable visual theme supplies independent FRONT and BACK art" ) _expect( _preview.visual_theme == visual_theme, "world preview consumes the scene theme instead of hard-coded asset paths" ) _expect( visual_theme.facility_icons.size() == 23 and visual_theme.navigation_icons.size() == 6 and visual_theme.resource_icons.size() == 4 and visual_theme.control_icons.size() == 6, "replaceable visual theme provides complete semantic icon assignments" ) _expect( visual_theme.guest_actor_marker(&"mage_elf") != null and visual_theme.guest_actor_marker(&"legacy_guest") != null and visual_theme.staff_actor_marker(&"guard") != null and visual_theme.staff_actor_marker(&"legacy_role") != null, "visual theme resolves class, role, and fallback actor markers" ) _expect( visual_theme.ui_skin_textures.size() == 16 and visual_theme.ui_skin(&"panel_parchment") != null and visual_theme.ui_skin(&"button_pressed") != null and visual_theme.ui_skin(&"queue_badge") != null and visual_theme.ui_skin(&"construction_hatch") != null, "visual theme provides replaceable panel, button, badge, route, and construction skins" ) for texture_path: String in [ "res://assets/themes/japanese_fantasy/world-front-v1.png.import", "res://assets/themes/japanese_fantasy/world-back-v1.png.import", "res://assets/themes/japanese_fantasy/facility-icons-v2.png.import", "res://assets/themes/japanese_fantasy/system-icons-v2.png.import", "res://assets/themes/japanese_fantasy/actor-markers-v2.png.import", "res://assets/themes/japanese_fantasy/ui-skin-v1.png.import", ]: var import_config := ConfigFile.new() _expect( import_config.load(texture_path) == OK and bool(import_config.get_value("params", "mipmaps/generate", false)), "high-resolution texture enables mipmaps: %s" % texture_path.get_file() ) var operations: Dictionary = _runtime.get_operations_report() _expect(bool(operations.get(&"enabled", false)), "dashboard enables the production Operations loop") var summary := _main.get("dashboard_summary_label") as Label _expect( summary != null and "行動点" in summary.text and "未払経費" in summary.text, "dashboard exposes action points and payroll arrears" ) await _test_defense_notice() var palette := _main.find_child("BuildPalette", true, false) as GridContainer _expect(palette.get_child_count() == 25, "all25 facilities including the cleaning room and both A07/A08 warps appear in the build palette") _expect((_main.get("build_buttons") as Dictionary).has(&"mana_warp") and (_main.get("build_buttons") as Dictionary).has(&"service_warp"), "guest and service Mana warps are buildable from the ordinary palette") var iconized_build_buttons := 0 for build_button_value: Variant in (_main.get("build_buttons") as Dictionary).values(): if (build_button_value as Button).icon != null: iconized_build_buttons += 1 _expect(iconized_build_buttons == 23, "all facility catalog buttons consume theme-provided icons") var premium_button: Button = (_main.get("build_buttons") as Dictionary)[&"premium_room"] _main.call("_set_build_palette_open", true) premium_button.button_pressed = true _expect(_main.get("_selected_definition_id") == &"premium_room" and _main.get("_selected_definition").footprint == Vector2i(4,1), "premium palette button selects original-size placement definition") if OS.get_environment("THE_TREE_PREMIUM_CAPTURE") == "1": await process_frame await RenderingServer.frame_post_draw _expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/facility-dimensions-2026-09-29/premium-palette.png")) == OK, "capture premium room palette") premium_button.button_pressed = false var dorm_button: Button = (_main.get("build_buttons") as Dictionary)[&"staff_dormitory"] dorm_button.button_pressed = true _expect(_main.get("_selected_definition_id") == &"staff_dormitory" and _main.get("_selected_definition").footprint == Vector2i(2,1) and _preview.layer_mode == WorldPreview.LayerMode.BACK, "dormitory palette selects original footprint and back layer") if OS.get_environment("THE_TREE_DORMITORY_CAPTURE") == "1": await process_frame await RenderingServer.frame_post_draw _expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/facility-dimensions-2026-09-29/dormitory-palette.png")) == OK, "capture dormitory palette") dorm_button.button_pressed = false _main.call("_set_layer", 0) _main.call("_set_build_palette_open", false) var palette_scroll := _main.find_child("BuildPaletteScroll", true, false) as ScrollContainer _expect( palette_scroll != null and palette_scroll.is_ancestor_of(palette), "construction catalog lives in the persistent bottom palette" ) var navigation := _main.find_child("NavigationRail", true, false) as Control _expect( navigation != null and (_main.get("navigation_buttons") as Dictionary).size() == 7, "dashboard exposes the seven Japanese management categories" ) var iconized_navigation_buttons := 0 for navigation_button_value: Variant in (_main.get("navigation_buttons") as Dictionary).values(): if (navigation_button_value as Button).icon != null: iconized_navigation_buttons += 1 _expect(iconized_navigation_buttons == 7, "all management categories consume theme-provided icons") var resource_icon_nodes := _main.get("resource_icon_nodes") as Dictionary _expect( resource_icon_nodes.size() == 4 and (resource_icon_nodes.get(&"gold") as TextureRect).texture != null and (resource_icon_nodes.get(&"mana") as TextureRect).texture != null, "top HUD consumes replaceable resource icons" ) var alert_icon := _main.find_child("AlertIcon", true, false) as TextureRect _expect(alert_icon != null and alert_icon.texture != null, "top HUD consumes the theme warning icon") var selected_icon := _main.find_child("SelectedEntityIcon", true, false) as TextureRect _expect(selected_icon != null, "facility inspector reserves a theme-driven identity icon") var guest_count := _main.get("guest_count_label") as Button var lodging_count := _main.get("lodging_count_label") as Label var alert_count := _main.get("alert_count_label") as Label _expect( guest_count != null and "来客" in guest_count.text and lodging_count != null and "宿泊" in lodging_count.text and alert_count != null and "警戒" in alert_count.text, "top HUD keeps visitor, lodging, and alert information visible" ) _expect((_main.get("build_buttons") as Dictionary).has(&"guest_elevator"), "connector proxy definitions are exposed for vertical-layout prototyping") _expect((_main.get("build_buttons") as Dictionary).has(&"adult_private_room"), "the full MVP catalog is reachable from the scrolling palette") await _check_layout(Vector2i(1440, 810)) await _check_layout(Vector2i(1280, 720)) await _check_layout(Vector2i(1920, 1080)) await _check_layout(Vector2i(2560, 1440)) await _check_layout(Vector2i(3840, 2160)) _test_staff_candidate_hiring_ui() _test_excavation_rank_gate_and_tool_exclusion() _test_coordinate_round_trip() _test_build_payload_and_hit_test() _test_excavation_preview_payload_and_start() _test_manual_save_and_load() _test_event_choice_ui() var teardown: Dictionary = await AudioTeardown.free_owner_and_wait(self, _main) _expect(bool(teardown.get(&"passed", false)) and not bool(teardown.get(&"deadline_reached", true)), "owned audio references release before bounded teardown deadline with whole Runtime unchanged") SaveService.delete_save(_manual_save_path) if _failures == 0: print("PASS: %d checks" % _checks) else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks]) quit(_failures) func _check_layout(window_size: Vector2i) -> void: root.size = window_size await process_frame await process_frame var side := _main.find_child("SidePanel", true, false) as Control var map_panel := _main.find_child("MapPanel", true, false) as Control var bottom := _main.find_child("BottomBar", true, false) as Control var navigation := _main.find_child("NavigationRail", true, false) as Control var excavation_button := _main.find_child("ExcavationButton", true, false) as Button _expect(side != null and side.size.x >= 300.0, "%dx%d keeps the operations sidebar readable" % [window_size.x, window_size.y]) _expect(map_panel != null and map_panel.size.x >= 600.0 and map_panel.size.y >= 380.0, "%dx%d keeps a usable map area" % [window_size.x, window_size.y]) _expect( navigation != null and navigation.position.x < map_panel.position.x and side.position.x > map_panel.position.x, "%dx%d keeps navigation left and facility inspection right of the living map" % [window_size.x, window_size.y] ) _expect(bottom != null and bottom.position.y + bottom.size.y <= _main.size.y + 1.0, "%dx%d keeps all controls inside the viewport" % [window_size.x, window_size.y]) _expect( excavation_button != null and excavation_button.position.x + excavation_button.size.x <= bottom.size.x + 1.0, "%dx%d keeps the excavation control inside the bottom bar" % [window_size.x, window_size.y] ) func _test_staff_candidate_hiring_ui() -> void: var panel := _main.find_child("StaffCandidatePanel", true, false) as VBoxContainer var list := _main.find_child("StaffCandidateList", true, false) as VBoxContainer var toggle := _main.find_child("StaffCandidateToggleButton", true, false) as Button var buttons := _main.get("staff_candidate_buttons") as Dictionary _expect(panel != null and not panel.visible, "staff candidate screen starts collapsed") _expect(list != null and list.get_child_count() == 22, "twenty-one general candidates plus approved Anri recruitment have UI cards") _expect(toggle != null and "22" in toggle.text, "collapsed hiring control exposes the available candidate count") _expect(buttons.size() == 22 and buttons.has(&"staff_akane") and buttons.has(&"heroine_anri"), "every candidate has a stable hire control") var akane_detail := _main.find_child("CandidateDetail_staff_akane", true, false) as Label _expect( akane_detail != null and "アカネ" in akane_detail.text and "契約 G180" in akane_detail.text and "日給 G120" in akane_detail.text, "candidate card shows Japanese identity and both signing and payroll costs" ) _main.call("_unhandled_key_input", _key_event(KEY_H)) _expect(panel.visible and bool(_main.get("_staff_candidate_panel_open")), "H opens the Staff candidate screen") var cash_before: int = int(_runtime.economy_service.get_balance(&"PLAYER_CASH")) _main.call("_hire_staff_candidate", &"staff_akane") _expect(_runtime.get_hired_staff_candidate_ids().has(&"staff_akane"), "paused UI hire commits the selected canonical candidate immediately") _expect(_runtime.staff_service.has_staff(&"staff_akane"), "UI hire creates the Staff member through Runtime") _expect( _runtime.economy_service.get_balance(&"PLAYER_CASH") == cash_before - 180, "UI hire charges the catalog signing cost exactly once" ) var akane_button := buttons.get(&"staff_akane") as Button _expect(akane_button != null and akane_button.disabled and akane_button.text == "雇用済み", "hired candidate is visibly final and cannot be clicked again") var after_first_hire: int = int(_runtime.economy_service.get_balance(&"PLAYER_CASH")) _main.call("_hire_staff_candidate", &"staff_akane") _expect( _runtime.economy_service.get_balance(&"PLAYER_CASH") == after_first_hire and _runtime.get_hired_staff_candidate_ids().count(&"staff_akane") == 1, "repeated UI hire does not double-charge or duplicate the employee" ) _clock.set_paused(false) _main.call("_hire_staff_candidate", &"staff_bardo") var pending := _runtime.command_queue.create_snapshot().get("pending", []) as Array _expect(pending.size() == 1, "running-clock Staff hire is queued before mutation") _main.call("_hire_staff_candidate", &"staff_bardo") _expect( (_runtime.command_queue.create_snapshot().get("pending", []) as Array).size() == 1, "pending Staff hire suppresses repeated clicks" ) _clock.advance_minutes(1) _expect( _runtime.get_hired_staff_candidate_ids().has(&"staff_bardo") and (_main.get("_pending_hire_commands") as Dictionary).is_empty(), "queued Staff hire completes and clears its UI pending state" ) _clock.set_paused(true) var candidate_summary := _main.find_child("StaffCandidateSummary", true, false) as Label _expect(candidate_summary != null and "雇用済み 2人" in candidate_summary.text, "candidate screen refreshes its hired count from Runtime state") _main.set("_hire_sequence", 1) _main.call("_synchronize_ui_command_state") _expect( int(_main.get("_hire_sequence")) >= 3, "saved Command metadata advances the UI hire-attempt sequence after reconstruction" ) _main.call("_unhandled_key_input", _key_event(KEY_ESCAPE)) _expect(not panel.visible and not bool(_main.get("_staff_candidate_panel_open")), "Escape closes the Staff candidate screen") func _test_excavation_rank_gate_and_tool_exclusion() -> void: _main.call("_unhandled_key_input", _key_event(KEY_E)) var status := _main.get("build_status_label") as Label var excavation_button := _main.get("excavation_button") as Button _expect(bool(_main.get("_excavation_mode")), "E enables the explicit excavation mode") _expect(_preview.excavation_mode, "dashboard and map share the excavation-mode state") _expect(excavation_button != null and excavation_button.button_pressed, "excavation toggle visibly reflects the keyboard shortcut") _expect(status != null and "ランク2" in status.text, "Rank 1 excavation gate is explained honestly in Japanese") _main.call("_select_build_definition", &"reception") _expect(not bool(_main.get("_excavation_mode")) and not _preview.excavation_mode, "choosing a facility exits excavation mode") _main.call("_unhandled_key_input", _key_event(KEY_E)) _expect(_main.get("_selected_definition") == null, "entering excavation mode clears the facility build selection") var reception_button := (_main.get("build_buttons") as Dictionary).get(&"reception") as Button _expect(reception_button != null and not reception_button.button_pressed, "exclusive excavation mode clears the palette toggle") _main.call("_unhandled_key_input", _key_event(KEY_ESCAPE)) _expect(not bool(_main.get("_excavation_mode")) and not _preview.excavation_mode, "Escape exits excavation mode") var pan_button := _main.find_child("PanModeButton", true, false) as Button _expect(pan_button != null, "camera movement has a visible text button") _main.call("_select_build_definition", &"reception") pan_button.pressed.emit() _expect(_preview.pan_mode and pan_button.button_pressed and _main.get("_selected_definition") == null, "pan button exits construction so a camera drag cannot place a facility") _main.call("_select_build_definition", &"reception") _expect(not _preview.pan_mode and not pan_button.button_pressed, "choosing construction exits pan mode") pan_button.pressed.emit() _main.call("_unhandled_key_input", _key_event(KEY_E)) _expect(not _preview.pan_mode and _preview.excavation_mode, "excavation exits pan mode") pan_button.pressed.emit() _expect(_preview.pan_mode and not _preview.excavation_mode, "pan mode exits excavation") _main.call("_unhandled_key_input", _key_event(KEY_ESCAPE)) _expect(not _preview.pan_mode and not pan_button.button_pressed, "Escape exits pan mode and resets its button") func _test_coordinate_round_trip() -> void: _preview.layer_mode = WorldPreview.LayerMode.FRONT for cell: Vector3i in [Vector3i(-12, 0, 0), Vector3i(0, 3, 0), Vector3i(12, 6, 0)]: var restored: Variant = _preview.canvas_to_cell(_preview.cell_to_canvas(cell)) _expect(restored == cell, "FRONT cell-to-canvas round trip preserves %s" % cell) _preview.layer_mode = WorldPreview.LayerMode.BACK var back_cell := Vector3i(4, 0, 1) _expect(_preview.canvas_to_cell(_preview.cell_to_canvas(back_cell)) == back_cell, "BACK layer coordinate round trip preserves the layer") _expect(_preview.canvas_to_cell(_preview.get_map_rect().position-Vector2(10, 10)) == null, "points outside the map do not produce build cells") func _test_build_payload_and_hit_test() -> void: _main.call("_select_build_definition", &"reception") var origin := Vector3i(-6, 0, WorldService.LAYER_FRONT) var preview_report: Dictionary = _preview.get_build_preview_report(origin) _expect(bool(preview_report.get(&"can_build", false)), "starter reception shows a valid authoritative placement preview") var jobs_before: int = _runtime.construction_service.get_jobs().size() _main.call("_on_map_cell_clicked", origin) var jobs: Array = _runtime.construction_service.get_jobs() _expect(jobs.size() == jobs_before + 1, "map click submits one build_facility command") var job: Variant = jobs.back() _expect(job.definition_id == &"reception" and job.origin == origin and job.rotation == 0, "build command payload preserves definition, cell, and rotation") _main.call("_on_map_cell_clicked", origin) _expect(_runtime.construction_service.get_jobs().size() == jobs.size(), "invalid overlap preview does not submit a second command") _clock.advance_minutes(120) var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities() _expect(facilities.size() == 1, "dashboard-created construction completes in simulation time") if facilities.is_empty(): return var instance_id := StringName(facilities[0].get(&"instance_id", &"")) var facility_point := _preview.cell_to_canvas(origin) _expect(_preview.facility_at_canvas(facility_point) == instance_id, "facility hit test resolves the completed world occupant") _main.call("_on_facility_selected", instance_id) var detail := _main.get("selected_detail_label") as Label _expect(detail != null and "待機列" in detail.text and "配置人員" in detail.text, "facility selection exposes queue, service, and staffing state") var selected_icon := _main.get("selected_entity_icon") as TextureRect _expect(selected_icon != null and selected_icon.texture != null, "facility selection exposes its theme icon in the inspector") func _test_excavation_preview_payload_and_start() -> void: _expect(_promote_rank_two(), "UI fixture reaches Rank 2 through public progression commands") _main.call("_set_layer", WorldPreview.LayerMode.FRONT) _main.call("_set_excavation_mode", true) var first_cell := Vector3i(0, -1, WorldPreview.LayerMode.FRONT) _preview.call("_set_hovered_cell", first_cell) var report := _preview.get_excavation_preview_report(first_cell) _expect(bool(report.get(&"ok", false)), "underground hover uses the authoritative excavation validator") var preview_cells: Variant = report.get(&"cells", []) _expect( preview_cells is Array and preview_cells.has(Vector3i(0, -1, WorldPreview.LayerMode.FRONT)) and preview_cells.has(Vector3i(0, -1, WorldPreview.LayerMode.BACK)), "excavation preview identifies both FRONT and BACK target cells" ) var status := _main.get("build_status_label") as Label _expect( status != null and "掘削可能" in status.text and "費用" in status.text and "所要" in status.text, "valid excavation preview shows Japanese cost and duration" ) _main.call("_set_layer", WorldPreview.LayerMode.BACK) _expect( (_preview.get("_last_excavation_preview_report") as Dictionary).get(&"ok", false), "changing layer recomputes the active-layer excavation preview" ) _main.call("_set_layer", WorldPreview.LayerMode.FRONT) _preview.call("_set_hovered_cell", first_cell) var jobs_before: int = _runtime.excavation_service.get_jobs().size() _clock.set_paused(false) _main.call("_on_map_cell_clicked", first_cell) var pending_snapshot: Dictionary = _runtime.command_queue.create_snapshot() var pending: Array = pending_snapshot.get("pending", []) _expect(_runtime.excavation_service.get_jobs().size() == jobs_before, "running clock queues the excavation command before mutation") _expect(pending.size() == 1, "map click queues exactly one start_excavation command") if not pending.is_empty(): var command: Dictionary = pending.back() var payload: Variant = command.get("payload", {}) _expect( StringName(command.get("command_type", "")) == &"start_excavation" and payload is Dictionary and payload.get(&"cell", null) == first_cell, "excavation command payload preserves the hovered layer cell" ) _expect( StringName(command.get("idempotency_key", "")) == &"ui.excavation.0.-1.00000001", "excavation uses a stable per-attempt idempotency key" ) _expect(status != null and "受理しました" in status.text, "queued excavation reports command acceptance") _main.call("_on_map_cell_clicked", first_cell) _expect( (_runtime.command_queue.create_snapshot().get("pending", []) as Array).size() == 1, "a repeated click cannot queue a second excavation attempt while the first is pending" ) _clock.advance_minutes(1) var first_jobs: Array[Dictionary] = _runtime.excavation_service.get_jobs() _expect(first_jobs.size() == jobs_before + 1, "queued excavation starts on the next Runtime command phase") _expect((_main.get("_pending_excavation_commands") as Dictionary).is_empty(), "queued completion clears the UI pending-command entry") _expect(status != null and "開始しました" in status.text, "queued command completion reports the actual start") if first_jobs.is_empty(): _clock.set_paused(true) return var first_job: Dictionary = first_jobs.back() _expect( first_job.get(&"position", Vector2i.ZERO) == Vector2i(0, -1) and int(first_job.get(&"progress", 0)) > 0, "started excavation is visible and progresses through simulation time" ) _clock.set_paused(true) _clock.advance_minutes(int(first_job.get(&"build_minutes", 0))) var second_cell := Vector3i(0, -2, WorldPreview.LayerMode.FRONT) _preview.call("_set_hovered_cell", second_cell) var second_report := _preview.get_excavation_preview_report(second_cell) _expect(bool(second_report.get(&"ok", false)), "completed excavation unlocks the next connected underground cell") var jobs_before_immediate: int = _runtime.excavation_service.get_jobs().size() _main.call("_on_map_cell_clicked", second_cell) _expect( _runtime.excavation_service.get_jobs().size() == jobs_before_immediate + 1, "paused clock executes an accepted excavation command immediately" ) var detail := _main.get("selected_detail_label") as Label _expect( detail != null and "進捗" in detail.text and "FRONT / 表層・BACK / 裏層" in detail.text, "started excavation exposes progress and both target layers in selection detail" ) _main.call("_unhandled_key_input", _key_event(KEY_ESCAPE)) _expect(not bool(_main.get("_excavation_mode")), "Escape still exits excavation mode after starting a job") func _promote_rank_two() -> bool: var reputation: Variant = _runtime.submit_command( &"update_progression_metrics", {&"metrics": {&"reputation": 25}}, &"player", &"", 100, -1, &"ui.excavation.rank.reputation" ) var capital: Variant = _runtime.submit_command( &"change_resources", {&"deltas": {&"gold": maxi(10_000 - _runtime.economy_service.get_balance(&"PLAYER_CASH"), 0)}}, &"player", &"", 100, -1, &"ui.excavation.rank.capital" ) var promoted: Variant = _runtime.submit_command( &"set_world_tree_rank", {&"rank": 2}, &"player", &"", 100, -1, &"ui.excavation.rank.promote" ) return reputation.is_success() and capital.is_success() and promoted.is_success() \ and _state.world_tree_rank == 2 func _test_manual_save_and_load() -> void: SaveService.delete_save(_manual_save_path) var facilities: Array = _runtime.facility_service.get_instances() _expect(not facilities.is_empty(), "selection save fixture has a completed facility") if facilities.is_empty(): return var selected_id: StringName = facilities[0].instance_id _main.call("_on_facility_selected", selected_id) _main.call("_set_layer", 1) _main.call("_save_manual") var status := _main.get("build_status_label") as Label _expect(status != null and "保存しました" in status.text, "manual save button writes the runtime snapshot") var expected_gold: int = _state.get_resource(&"gold") _state.set_resource(&"gold", expected_gold + 7) _main.set("_selected_entity_id", &"") _main.set("_selected_entity_kind", &"") _main.call("_set_layer", 0) _main.call("_load_manual") _expect(_state.get_resource(&"gold") == expected_gold, "manual load button restores the saved simulation") _expect(status != null and "読み込みました" in status.text, "manual load reports success in Japanese") _expect(_main.get("_selected_entity_id") == selected_id and _main.get("_selected_entity_kind") == &"facility", "manual load restores saved facility selection") _expect(_preview.selected_entity_id == selected_id, "loaded selection restores map highlight") _expect(_preview.layer_mode == 1 and _state.active_layer == 1, "load synchronizes saved layer and visible layer") var loaded: Dictionary = SaveService.load_snapshot(_manual_save_path) _expect(loaded[&"manifest"].get("interface_selection", {}).get("id", "") == String(selected_id), "selection persisted in the actual save package") # Missing or malformed optional UI metadata cannot break an otherwise valid save. for selection: Variant in [null, {"version": 99}, {"version": 1, "kind": [], "id": String(selected_id)}, {"version": 1, "kind": "facility", "id": "missing"}, {"version": 1, "kind": "unknown", "id": String(selected_id)}]: var saved: Dictionary = _runtime.save_to_path(_manual_save_path, {"interface_selection": selection}) _expect(saved.get(&"ok", false), "write fixture with optional presentation metadata") _main.call("_load_manual", true) _expect("読み込みました" in status.text and _main.get("_selected_entity_id") == &"", "invalid presentation selection is ignored without breaking gameplay load") var old_saved: Dictionary = SaveService.save_snapshot(_manual_save_path, _runtime.create_snapshot(), {}) _expect(old_saved.get(&"ok", false), "save without UI metadata remains supported") _main.call("_load_manual", true) _expect(_main.get("_selected_entity_id") == &"", "legacy save loads without inventing a selection") # Failed loading must preserve the currently selected facility. _main.call("_on_facility_selected", selected_id) _main.set("manual_save_path", _manual_save_path + "_missing") _main.call("_load_manual", true) _expect(_main.get("_selected_entity_id") == selected_id, "failed load preserves current selection") _main.set("manual_save_path", _manual_save_path) _main.call("_save_manual") _main.set("_selected_entity_id", &"") _main.set("_selected_entity_kind", &"") _main.call("_save_manual") var corrupt := FileAccess.open(_manual_save_path.path_join(SaveService.WORLD_FILE), FileAccess.WRITE) corrupt.store_string("invalid test package") corrupt.close() _main.call("_load_manual") _expect("読み込みました" in status.text and _main.get("_selected_entity_id") == selected_id, "backup recovery uses the selected candidate's UI metadata") func _test_event_choice_ui() -> void: var event_service: Variant = _runtime.event_service var queued: Variant = event_service.enqueue_candidate( &"relationship.threshold", [], _clock.absolute_minute, {&"deduplication_key": &"ui-event-choice-test"} ) _expect(queued.is_success(), "UI fixture enqueues a foreground relationship Event") var started: Variant = event_service.start_next_event(_clock.absolute_minute, {}) _expect( started.is_success() and bool(started.details.get(&"waiting_for_choice", false)), "UI fixture starts an Event waiting for a Choice" ) _clock.set_paused(true) _main.call("_refresh_dashboard") var panel := _main.find_child("ForegroundEventPanel", true, false) as VBoxContainer var choices := _main.find_child("ForegroundEventChoices", true, false) as VBoxContainer _expect(panel != null and panel.visible, "foreground Event opens the decision panel") _expect(choices != null and choices.get_child_count() == 2, "all two canonical Event choices are visible") var pause_button := _main.get("pause_button") as Button _expect( pause_button != null and pause_button.disabled and "イベント" in pause_button.text, "unresolved foreground Event cannot be bypassed with the pause control" ) if choices == null or choices.get_child_count() != 2: return var first_choice := choices.get_child(0) as Button _expect( first_choice != null and not first_choice.text.is_empty(), "Event choice uses a readable Japanese button" ) first_choice.emit_signal(&"pressed") _expect(event_service.get_foreground_instance() == null, "choice button resolves the foreground Event exactly once") _expect(not _clock.paused, "resolved foreground Event releases the simulation pause") _expect(not panel.visible and choices.get_child_count() == 0, "resolved Event removes the stale choice controls") _expect(not pause_button.disabled, "resolved Event restores the normal pause control") _clock.set_paused(true) func _test_defense_notice() -> void: var before: Dictionary = _runtime.create_snapshot() var notice: Label = _main.get("alert_count_label") var summary: Label = _main.get("dashboard_summary_label") _expect(not "襲撃" in notice.text, "empty defense keeps compact normal HUD") var created: CommandResult = _runtime.submit_command(&"create_defense_raid", { &"raid_id": &"ui.hud.defense", &"definition": { &"entry_points": [&"gate"], &"objective_ids": [&"core"], &"waves": [{&"wave_id": &"wave.one", &"threat_strength": 15}], }}, &"defense_controller") _expect(created.is_success(), "HUD fixture creates preparing raid through Runtime command") await process_frame await process_frame _expect("襲撃 1" in notice.text and "準備中 1" in summary.text, "Runtime publication shows preparing threat in HUD and management") _expect("配属変更できません" in notice.tooltip_text and "戦闘中 0" in notice.tooltip_text, "HUD distinguishes preparation and explains employee lock") if OS.get_environment("THE_TREE_DEFENSE_CAPTURE") == "1": _main.call("_set_management_panel_open", true) await process_frame await RenderingServer.frame_post_draw _expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/defense-hud-2026-09-29/preparing.png")) == OK, "capture rendered defense notice") _main.call("_set_management_panel_open", false) var saved: Dictionary = _runtime.create_snapshot() var cancelled: CommandResult = _runtime.submit_command(&"cancel_defense_raid", {&"raid_id": &"ui.hud.defense"}, &"defense_controller") _expect(cancelled.is_success(), "HUD fixture cancels incomplete preparation") await process_frame await process_frame _expect(not "襲撃" in notice.text and "準備中 0 / 戦闘中 0 / 次波待ち 0" in summary.text and "ありません" in notice.tooltip_text, "terminal history clears HUD warning and stale tooltip") _expect(_runtime.restore_snapshot(saved), "restore preparing raid snapshot") await process_frame await process_frame _expect("襲撃 1" in notice.text and "準備中 1" in summary.text, "HUD follows restored preparing raid without a new raid signal") _expect(_runtime.restore_snapshot(before), "restore pre-raid gameplay") await process_frame await process_frame _expect(not "襲撃" in notice.text, "restoring empty defense clears warning") func _expect(condition: bool, message: String) -> void: _checks += 1 if condition: return _failures += 1 push_error(message) func _key_event(keycode: Key) -> InputEventKey: var event := InputEventKey.new() event.keycode = keycode event.pressed = true return event