Files
TheTree_mock/tests/ui/test_main_dashboard.gd
T
TakeshiandClaude Opus 5.5 8917c190cf P14-02: Split warps into guest FRONT and staff BACK service warp (A08)
The FRONT Mana warp is guest-only: its connectors carry the "!staff" access,
which path access checks treat as open to everyone except staff. A new 2x2
BACK service warp links only to other service warps for staff, with A07
values. Registry and palette now hold 25 definitions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:48:14 +09:00

634 lines
35 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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