Record original-alignment implementation and traceability checkpoint

Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
This commit is contained in:
Takeshi
2026-09-30 23:52:24 +09:00
parent 6077e02ebe
commit 5dde0c5ec2
490 changed files with 54051 additions and 809 deletions
+4
View File
@@ -72,6 +72,10 @@ func _run() -> void:
# main enables its production save policy in _ready; immediately turn it off
# for this disposable scenario and make the manual save/load controls inert.
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
var load_menu := main.find_child("SaveLoadMenu", true, false) as MenuButton
if load_menu != null:
load_menu.disabled = true
load_menu.tooltip_text = "この見学モードは保存・読込を行いません。"
for button: Node in main.find_children("*", "Button", true, false):
var save_control := button as Button
for connection: Dictionary in save_control.pressed.get_connections():
+5
View File
@@ -0,0 +1,5 @@
extends "res://scenes/main/main.gd"
var dashboard_refresh_count := 0
func _refresh_dashboard() -> void:
dashboard_refresh_count += 1
super._refresh_dashboard()
@@ -0,0 +1 @@
uid://erbvbvnrtc84
+119
View File
@@ -0,0 +1,119 @@
extends "res://tests/ui/test_first_operating_day.gd"
## Two-process natural opening/three-close debug flow. The Python runner supplies
## an isolated slot and evidence directory; never use ordinary player saves.
var _handoff_done := false
func _run() -> void:
if OS.get_environment("THE_TREE_OPENING_RESTART_SLOT").is_empty():
push_error("Restart runner must supply its isolated save slot")
quit(1)
return
if "--read" not in OS.get_cmdline_user_args():
await super._run()
return
_clock = root.get_node("GameClock")
_runtime = root.get_node("SimulationRuntime")
var state: Variant = root.get_node("GameState")
_clock.reset(1, 6, 0)
state.reset()
_runtime.initialize(_clock, state, 424242)
_clock.set_paused(true)
root.size = Vector2i(1440, 810)
_main = load("res://scenes/main/main.tscn").instantiate()
_main.set("manual_save_path", OS.get_environment("THE_TREE_OPENING_RESTART_SLOT"))
root.add_child(_main)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
await process_frame
await process_frame
_guide = _main.find_child("InnOpeningGuide", true, false)
_ledger = _main.find_child("OperatingLedgerPanel", true, false)
_load_handoff()
await _continue_to_third_close()
var expected: Variant = JSON.parse_string(FileAccess.get_file_as_string(_evidence_path("expected.json")))
_expect(expected is Dictionary, "previous process retained expected continuation evidence")
if expected is Dictionary:
_expect(_gameplay_digest() == expected.get("gameplay_digest"), "fresh process reproduces full gameplay snapshot through third close")
_expect(JSON.stringify(_runtime.operations_coordinator.get_day_summaries()) == expected.get("closes_json"), "all three financial reports exactly match uninterrupted continuation")
await _capture_restart("restarted-third-close")
if _failures == 0:
SaveService.delete_save(OS.get_environment("THE_TREE_OPENING_RESTART_SLOT"))
super._finish()
func _finish() -> void:
if _failures > 0 or _handoff_done:
super._finish()
return
_handoff_done = true
_main.set("manual_save_path", OS.get_environment("THE_TREE_OPENING_RESTART_SLOT"))
var selected_id: StringName = _runtime.facility_service.get_instances()[0].instance_id
_main.call("_on_facility_selected", selected_id)
var selection_file := FileAccess.open(_evidence_path("selection.json"), FileAccess.WRITE)
selection_file.store_string(JSON.stringify({"id": String(selected_id)}))
selection_file.close()
var found_save := false
for node: Node in _main.find_children("*", "Button", true, false):
if node.text == "保存":
node.pressed.emit()
found_save = true
break
_expect(found_save, "save through the normal dashboard button")
_load_handoff()
await _capture_restart("saved-second-morning")
await _continue_to_third_close()
var file := FileAccess.open(_evidence_path("expected.json"), FileAccess.WRITE)
_expect(file != null, "continuation evidence can be retained")
if file != null:
file.store_string(JSON.stringify({"gameplay_digest": _gameplay_digest(), "closes_json": JSON.stringify(_runtime.operations_coordinator.get_day_summaries())}, "\t") + "\n")
file.close()
await _capture_restart("uninterrupted-third-close")
super._finish()
func _load_handoff() -> void:
var menu := _main.find_child("SaveLoadMenu", true, false) as MenuButton
_expect(menu != null and not menu.disabled, "normal load menu is available")
if menu != null and not menu.disabled:
menu.get_popup().id_pressed.emit(0)
_expect(_clock.absolute_minute == 1920, "disk load restores the second morning at08:00")
_expect(_guide.get_progress().get(&"stage") == &"complete", "restarted UI recognizes completed opening")
var selection: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(_evidence_path("selection.json")))
_expect(String(_main.get("_selected_entity_id")) == selection["id"], "disk load restores the facility selected when saving")
_expect(_main.get("_selected_entity_kind") == &"facility", "disk load restores the selection type")
_expect(String(_main.get("world_preview").selected_entity_id) == selection["id"], "disk load restores the selected facility highlight")
func _continue_to_third_close() -> void:
await _advance(4320 - _clock.absolute_minute)
_expect(_clock.absolute_minute == 4320, "natural operation reaches third midnight")
var summaries: Array = _runtime.operations_coordinator.get_day_summaries()
_expect(summaries.size() == 3, "three calendar closes exist exactly once")
for summary: Dictionary in summaries:
_expect(summary.get(&"balanced", false), "every operating close reconciles with the ledger")
var report: Dictionary = _runtime.get_operations_report()
_expect(not report.get(&"runtime_failure_latched", true), "no simulation failure after multi-day continuation")
_expect(_ledger.get_report().get(&"selected_close") == summaries.back(), "dashboard presents latest third-day report")
var detail: String = _ledger.get("_facility_label").text
_expect(not detail.contains("facility_"), "completed infrastructure has human-readable ledger names")
for definition_id: StringName in [&"guest_stairs", &"guest_corridor"]:
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(definition_id)
_expect(detail.contains(definition.display_name), "ledger identifies charged infrastructure: %s" % definition.display_name)
func _gameplay_digest() -> String:
var snapshot: Dictionary = _runtime.create_snapshot()
# SAVE_LOADED contains measured elapsed time, which differs by process.
# Preserve ID counters, RNG streams and every other gameplay subsystem.
snapshot[&"systems"][&"runtime"].erase(&"domain_events")
var phase := "read" if "--read" in OS.get_cmdline_user_args() else "write"
var evidence := FileAccess.open(_evidence_path(phase + "-gameplay.json"), FileAccess.WRITE)
if evidence != null:
evidence.store_string(JSON.stringify(snapshot, "\t") + "\n")
evidence.close()
return SimulationDigest.compute(snapshot)
func _evidence_path(name: String) -> String:
return OS.get_environment("THE_TREE_OPENING_RESTART_OUTPUT").path_join(name)
func _capture_restart(name: String) -> void:
if DisplayServer.get_name() == "headless": return
_main.call("_refresh_dashboard")
await process_frame
await RenderingServer.frame_post_draw
_expect(root.get_texture().get_image().save_png(_evidence_path(name + ".png")) == OK, "capture restart flow: " + name)
@@ -0,0 +1 @@
uid://b0wbovftoj0yu
+184
View File
@@ -0,0 +1,184 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _runtime: Variant
var _clock: Variant
var _main: Control
var _staff: StaffManagementPanel
var _guest: GuestObservationPanel
var _path := "user://tests/actor_selection_%d/slot" % OS.get_process_id()
var _slots: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _expect(ok: bool, message: String) -> void:
_checks += 1
if not ok:
_failures += 1
push_error(message)
func _mount() -> void:
_main = load("res://scenes/main/main.tscn").instantiate()
_main.set("manual_save_path", _path)
root.add_child(_main)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
await process_frame
await process_frame
_staff = _main.find_child("StaffManagementPanel", true, false)
_guest = null
func _choose(staff_id: StringName, guest_id: StringName, opened := true) -> void:
_main.call("_activate_management_mode", &"staff")
_staff.refresh()
_staff.roster_buttons[staff_id].pressed.emit()
_main.get("guest_count_label").pressed.emit()
_guest = _main.get("guest_observer").find_child("GuestObservationPanel", true, false)
for i: int in _guest.guests.item_count:
if _guest.guests.get_item_metadata(i) == guest_id:
_guest.guests.select(i)
_guest.guests.item_selected.emit(i)
if not opened:
_main.call("_set_staff_management_panel_open", false)
_main.get("guest_observer").hide()
func _check_selected(staff_id: StringName, guest_id: StringName, opened: bool, context: String) -> void:
var observer: AcceptDialog = _main.get("guest_observer")
_expect(observer != null, context + " creates a real guest inspector when required")
if observer == null:
return
_guest = observer.find_child("GuestObservationPanel", true, false)
_expect(_staff.selected_staff_id == staff_id, context + " restores saved staff instead of first roster entry")
_expect(_guest.selected_guest_id == guest_id and _guest.guests.get_item_metadata(_guest.guests.selected) == guest_id, context + " restores saved guest and dropdown")
_expect(_staff.visible == opened and _main.get("guest_observer").visible == opened, context + " restores inspector visibility")
var amount: int = _runtime.guest_service.get_guest(guest_id).wallet_gold
_expect("%dG" % amount in _guest.details.text, context + " guest details show the actual restored wallet")
var name := "バルド" if staff_id == &"staff_bardo" else "アカネ"
_expect(_runtime.staff_service.has_staff(staff_id) and name in _staff.staff_detail_label.text, context + " staff detail name matches the restored employee")
_expect(_guest.bars[&"hunger"].value == _runtime.guest_service.get_guest(guest_id).needs[&"hunger"], context + " need bar matches the independently saved guest")
func _gameplay_snapshot() -> Dictionary:
var snapshot: Dictionary = _runtime.create_snapshot()
# Save/load observations contain measured duration and intentionally differ.
snapshot[&"systems"][&"runtime"].erase(&"domain_events")
return snapshot
func _gameplay_matches(saved: Dictionary, context: String, observation_count := 0) -> bool:
var expected := saved.duplicate(true)
# Runtime reports every load attempt using one new diagnostic event ID.
# Assert that exact increment; retain every other allocator and subsystem.
var counters: Dictionary = expected[&"systems"][&"runtime"][&"id_generator"][&"counters"]
counters["domain_event"] = int(counters.get("domain_event", 0)) + observation_count
var actual := _gameplay_snapshot()
var equal := SimulationDigest.compute(expected) == SimulationDigest.compute(actual)
if not equal:
var file := FileAccess.open("res://artifacts/actor-selection-2026-09-30/" + context + "-gameplay-diff.json", FileAccess.WRITE)
file.store_string(JSON.stringify({"expected": expected, "actual": actual}, "\t"))
return equal
func _write_fixture(suffix: String, selection: Variant) -> String:
var path := _path + suffix
_slots.append(path)
var metadata := {} if selection == null else {&"interface_selection": selection}
_expect(SaveService.save_snapshot(path, _runtime.create_snapshot(), metadata).get(&"ok", false), "write gameplay-valid optional presentation fixture " + suffix)
return path
func _run() -> void:
root.gui_embed_subwindows = true
root.size = Vector2i(1440, 810)
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_clock.reset(1, 6, 0)
root.get_node("GameState").reset()
_runtime.initialize(_clock, root.get_node("GameState"), 42532)
_clock.set_paused(true)
await _mount()
_main.get("staff_candidate_toggle_button").pressed.emit()
for id: StringName in [&"staff_akane", &"staff_bardo"]:
_main.get("staff_candidate_buttons")[id].pressed.emit()
_expect(_runtime.staff_service.has_staff(&"staff_akane") and _runtime.staff_service.has_staff(&"staff_bardo"), "ordinary candidate buttons hire both real employees")
# Controlled arrival fixtures isolate observer persistence, not natural arrivals.
var definition: GuestDefinition = _runtime.guest_service.get_definitions()[0]
for row: Array in [[&"selection_guest_a", 510, 17], [&"selection_guest_b", 910, 83]]:
_expect(_runtime.submit_command(&"spawn_guest", {&"guest_id": row[0], &"guest_type_id": definition.guest_type_id, &"gold": row[1], &"options": {&"planned_exit_at": _clock.absolute_minute + 1000, &"needs": {&"hunger": row[2]}}}).is_success(), "spawn an actual saved visitor " + String(row[0]))
_choose(&"staff_bardo", &"selection_guest_b")
var captured_before: Dictionary = _runtime.create_snapshot()
_main.call("_on_checkpoint_presentation_requested", {})
_expect(captured_before == _runtime.create_snapshot(), "collecting actor presentation does not mutate any Runtime subsystem")
_main.call("_save_manual")
var before := _gameplay_snapshot()
_slots.append(_path)
var package := SaveService.load_snapshot(_path)
_expect(package.get(&"ok", false), "normal manual save creates a gameplay package")
var selection: Dictionary = package.get(&"manifest", {}).get("interface_selection", {})
_expect(selection.get("staff_id") == "staff_bardo" and selection.get("guest_id") == "selection_guest_b", "normal save captures both independently chosen actor IDs")
_choose(&"staff_akane", &"selection_guest_a", false)
_main.call("_load_manual", true)
_expect(_main.get("build_status_label").text == "手動セーブを読み込みました。", "normal strict load accepts the complete actor gameplay package")
_check_selected(&"staff_bardo", &"selection_guest_b", true, "manual strict load")
_expect(_gameplay_matches(before, "manual"), "actor restoration does not change loaded gameplay")
var capture := OS.get_environment("THE_TREE_ACTOR_SELECTION_CAPTURE")
if not capture.is_empty():
for frame: int in 5:
await process_frame
await RenderingServer.frame_post_draw
_expect(root.get_texture().get_image().save_png(capture) == OK, "capture loaded guest inspector over saved staff panel")
_main.get("guest_observer").hide()
_main.call("_set_staff_management_panel_open", false)
var hidden_path := _path + "_hidden"
_slots.append(hidden_path)
_expect(_runtime.save_to_path(hidden_path).get(&"ok", false), "direct Runtime save collects hidden inspector selections")
_main.queue_free()
await process_frame
await _mount()
_expect(_main.get("guest_observer") == null, "new main has no pre-existing guest popup")
_expect(_runtime.load_from_path(hidden_path).get(&"ok", false), "new main accepts hidden actor selection package")
_check_selected(&"staff_bardo", &"selection_guest_b", false, "fresh main hidden load")
_choose(&"staff_bardo", &"selection_guest_b")
var intact := _gameplay_snapshot()
_expect(not _runtime.load_from_path(_path + "_missing").get(&"ok", true), "missing package is rejected")
_check_selected(&"staff_bardo", &"selection_guest_b", true, "failed load")
_expect(_gameplay_matches(intact, "failed", 1), "failed load preserves gameplay")
var generations := _path + "_generations"
_slots.append(generations)
for pair: Array in [[&"staff_bardo", &"selection_guest_b"], [&"staff_akane", &"selection_guest_a"]]:
_choose(pair[0], pair[1])
_expect(_runtime.queue_checkpoint(generations, {}, true).get(&"ok", false), "queue actual rotating checkpoint")
_clock.advance_minutes(1)
_expect(_runtime.load_recovery_candidate(generations, &"autosave", 1).get(&"ok", false), "load older actor checkpoint generation")
_check_selected(&"staff_bardo", &"selection_guest_b", true, "older recovery candidate")
_expect(_runtime.load_recovery_candidate(generations, &"autosave", 0).get(&"ok", false), "load newest actor checkpoint generation")
_check_selected(&"staff_akane", &"selection_guest_a", true, "newest recovery candidate")
# Invalid primary map metadata must not discard valid actor presentation.
var independent := {&"version": 1, &"kind": [], &"id": {}, &"staff_id": "staff_bardo", &"staff_open": true, &"guest_id": "selection_guest_b", &"guest_open": true}
var independent_path := _write_fixture("_independent", independent)
_expect(_runtime.load_from_path(independent_path).get(&"ok", false), "invalid map presentation does not invalidate gameplay")
_check_selected(&"staff_bardo", &"selection_guest_b", true, "independent actor metadata")
for entry: Array in [
["_missing_ids", {&"version": 1, &"staff_id": "nonexistent_staff", &"staff_open": true, &"guest_id": "nonexistent_guest", &"guest_open": true}],
["_wrong_types", {&"version": 1, &"staff_id": [], &"staff_open": true, &"guest_id": 17, &"guest_open": true}],
["_long_ids", {&"version": 1, &"staff_id": "a".repeat(257), &"staff_open": true, &"guest_id": "b".repeat(257), &"guest_open": true}],
["_wrong_version", {&"version": 9, &"staff_id": "staff_bardo", &"staff_open": true, &"guest_id": "selection_guest_b", &"guest_open": true}],
["_legacy", {&"version": 1, &"kind": "", &"id": ""}],
["_none", null],
]:
_choose(&"staff_bardo", &"selection_guest_b")
var path := _write_fixture(entry[0], entry[1])
var expected := _gameplay_snapshot()
_expect(_runtime.load_from_path(path).get(&"ok", false), "optional invalid or old metadata still loads " + entry[0])
_expect(not _staff.visible and not _main.get("guest_observer").visible, "invalid or absent actor metadata does not reopen inspectors " + entry[0])
_expect(_staff.selected_staff_id != &"nonexistent_staff" and _guest.selected_guest_id != &"nonexistent_guest", "missing actor IDs never become active selections " + entry[0])
_expect(_gameplay_matches(expected, entry[0], 1), "presentation fallback preserves gameplay " + entry[0])
var wrong_flags := {&"version": 1, &"staff_id": "staff_bardo", &"staff_open": "true", &"guest_id": "selection_guest_b", &"guest_open": 1}
_expect(_runtime.load_from_path(_write_fixture("_flags", wrong_flags)).get(&"ok", false), "wrong visibility types do not block valid actor selection")
_check_selected(&"staff_bardo", &"selection_guest_b", false, "strict visibility flags")
_finish()
func _finish() -> void:
if is_instance_valid(_main):
_main.queue_free()
for path: String in _slots:
SaveService.delete_save(path)
print("Actor selection: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://bwvp6pp1gmkb5
+105
View File
@@ -0,0 +1,105 @@
extends SceneTree
var checks := 0
var failures := 0
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok: failures += 1; push_error(message)
func _initialize() -> void: call_deferred("run")
func run() -> void:
var runtime: Variant = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1,6,0); state.reset(); runtime.initialize(clock,state,42601); clock.set_paused(true)
root.size = Vector2i(1280,720)
var main: Control = load("res://scenes/main/main.tscn").instantiate()
var path := "user://tests/anri-recruitment-%d" % Time.get_ticks_usec()
main.manual_save_path = path
root.add_child(main)
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
await process_frame; await process_frame
var before: Dictionary = runtime.create_snapshot()
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
main.staff_candidate_toggle_button.pressed.emit()
var button: Button = main.staff_candidate_buttons.get(&"heroine_anri")
expect(button != null and button.text == "無料で加入" and button.is_visible_in_tree(), "normal staff screen offers Anri free recruitment")
clock.set_paused(false)
button.pressed.emit()
clock.set_paused(true)
expect(button.disabled and not runtime.staff_service.has_staff(&"heroine_anri"), "running game queues recruitment without premature joining")
button.pressed.emit()
expect(runtime.command_queue.create_snapshot()["pending"].size() == 1, "repeated UI action does not duplicate recruitment")
main.call("_save_manual")
expect(SaveService.load_snapshot(path)[&"ok"], "pending Anri recruitment saves through normal UI")
expect(runtime.restore_snapshot(before), "restore control before loading pending recruitment")
main.call("_load_manual")
clock.advance_one_minute()
var anri: StaffRuntime = runtime.staff_service.get_staff(&"heroine_anri")
expect(anri != null and anri.role_id == &"innkeeper" and anri.employment_kind == StaffRuntime.EmploymentKind.UNIQUE, "restored request creates canonical unique innkeeper")
expect(runtime.economy_service.create_snapshot() == economy_before and anri.daily_wage == 0, "joining costs no cash and creates no daily wage")
expect(button.disabled and button.text == "加入済み", "joined unique cannot be recruited twice in UI")
main.staff_management_toggle_button.pressed.emit()
var panel: StaffManagementPanel = main.find_child("StaffManagementPanel",true,false)
panel.select_staff(&"heroine_anri")
expect(panel.staff_detail_label.text.contains("アンリ") and panel.staff_detail_label.text.contains("女将") and panel.staff_detail_label.text.contains("日給 G0"), "staff screen uses unique name role and zero upkeep")
main.call("_save_manual")
var joined: Dictionary = runtime.create_snapshot()
expect(runtime.restore_snapshot(before), "restore pre-join control")
main.call("_load_manual")
expect(runtime.staff_service.has_staff(&"heroine_anri") and runtime.get_hired_staff_candidate_ids().has(&"heroine_anri"), "normal load restores unique identity and recruitment history")
expect(not runtime.staff_service.retire_staff(&"heroine_anri").is_success(), "recruited Anri cannot retire")
var invalid := joined.duplicate(true)
for row: Dictionary in invalid[&"systems"][&"runtime"][&"staff"][&"staff"]:
if row[&"staff_id"] == "heroine_anri": row[&"employment_kind"] = "FREE_HIRE"
var intact: Dictionary = runtime.create_snapshot()
expect(not runtime.restore_snapshot(invalid) and runtime.create_snapshot() == intact, "save cannot downgrade Anri to removable generic employment")
# Place the recruited manager through the normal staff controls.
for build: Array in [[&"reception",Vector3i(-6,0,0)],[&"public_bath",Vector3i(1,0,0)]]:
main.call("_select_build_definition", build[0])
main.call("_on_map_cell_clicked", build[1])
clock.advance_minutes(runtime.definition_registry.get_facility(build[0]).build_minutes)
var reception: StringName = &""
var bath: StringName = &""
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id==&"reception": reception=facility.instance_id
if facility.definition_id==&"public_bath": bath=facility.instance_id
expect(not reception.is_empty() and not bath.is_empty(), "normal UI builds manager workplace and same-floor service")
panel.refresh(); panel.select_staff(&"heroine_anri")
for i in panel.facility_option.item_count:
if panel.facility_option.get_item_metadata(i)==reception:
panel.facility_option.select(i); panel.facility_option.item_selected.emit(i)
panel.apply_button.pressed.emit()
expect(runtime.staff_service.get_work_assignment(&"heroine_anri").get(&"facility_id")==reception, "normal staff controls place unique innkeeper")
var reception_instance: FacilityInstance=runtime.facility_service.get_instance(reception)
expect(reception_instance.active_staff_ids.is_empty(), "Anri does not meet the receptionist staffing requirement")
expect(not main.opening_guide._has_reception_assignment(reception_instance), "opening guide does not mistake innkeeper placement for receptionist coverage")
var manager_only: Dictionary=runtime.create_snapshot()
main.call("_hire_staff_candidate", &"staff_akane")
panel.refresh();panel.select_staff(&"staff_akane")
for i in panel.facility_option.item_count:
if panel.facility_option.get_item_metadata(i)==reception:
panel.facility_option.select(i);panel.facility_option.item_selected.emit(i)
panel.apply_button.pressed.emit()
expect(main.opening_guide._has_reception_assignment(reception_instance), "actual receptionist placement satisfies opening guide")
expect(runtime.restore_snapshot(manager_only), "restore manager-only workplace after receptionist comparison")
expect(not main.opening_guide._has_reception_assignment(runtime.facility_service.get_instance(reception)), "loading manager-only placement restores unsatisfied receptionist guidance")
main.navigation_buttons[&"business"].pressed.emit()
var business: BusinessManagementPanel = main.find_child("BusinessManagementPanel",true,false)
business.refresh()
for i in business.facility_option.item_count:
if business.facility_option.get_item_metadata(i)==bath:
business.facility_option.select(i); business.facility_option.item_selected.emit(i)
expect("24G" in business.quote_label.text and "アンリ配置フロア" in business.effect_label.text, "normal business screen explains current20percent floor price")
for i in business.price_option.item_count:
if business.price_option.get_item_metadata(i)==&"expensive":
business.price_option.select(i); business.price_option.item_selected.emit(i)
business.refresh()
expect("設定後 36G" in business.quote_label.text, "draft150percent policy combines with Anri120percent consistently")
if OS.get_environment("THE_TREE_ANRI_CAPTURE") == "1":
if not panel.visible: main.staff_management_toggle_button.pressed.emit()
panel.select_staff(&"heroine_anri")
expect(panel.is_visible_in_tree(), "recruited staff panel visible for rendered inspection")
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/anri-floor-assigned.png")) == OK, "capture recruited Anri")
SaveService.delete_save(path)
main.queue_free(); await process_frame
print("Anri recruitment UI: %d checks, %d failures" % [checks,failures]); quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://cvc6rmlmj0ak4
@@ -0,0 +1,164 @@
extends SceneTree
## Candidate only, not compiled/executed. Normal construction, staff, save and
## load controls are exercised; the sole money fixture is an explicitly labelled
## ledger spend at 23:59 using existing valid accounts/category. No tuning edits.
## Invoke -- --read in a second process with the SAME unique test save slot.
var checks := 0
var failures := 0
var main: Control
var runtime: Variant
var clock: Variant
var slot := ""
func _initialize() -> void:
call_deferred("run")
func expect(ok: bool, label: String) -> void:
checks += 1
if not ok:
failures += 1
push_error("FREE_UNIQUE_UI: " + label)
func save_button() -> void:
for button: Node in main.find_children("*","Button",true,false):
if button.text == "保存":
expect(not button.disabled,"normal save button enabled")
button.pressed.emit()
return
expect(false,"normal save button exists")
func load_menu() -> void:
var menu := main.find_child("SaveLoadMenu",true,false) as MenuButton
expect(menu != null and not menu.disabled,"normal load menu enabled")
if menu != null and not menu.disabled:
menu.get_popup().id_pressed.emit(0)
func unique_hire_result() -> CommandResult:
for saved: Dictionary in runtime.command_queue.create_snapshot()["processed"]:
var details: Dictionary = saved.get("details",{})
if StringName(details.get(&"staff_id",&"")) == &"heroine_anri":
return CommandResult.from_dictionary(saved)
return null
func verify_joined() -> void:
var anri: StaffRuntime = runtime.staff_service.get_staff(&"heroine_anri")
expect(anri != null and anri.employment_kind == StaffRuntime.EmploymentKind.UNIQUE and anri.role_id == &"innkeeper","normal read has canonical unique innkeeper")
if anri == null:
return
expect(runtime.economy_service.get_balance(runtime.PLAYER_CASH_ACCOUNT) == 0 and runtime.upkeep_settlement.get_arrears_total() > 0,"joining preserves zero funds and actual unpaid upkeep")
expect(anri.daily_wage == 0 and runtime.staff_service.get_daily_payroll_total() == 0,"unique upkeep remains zero")
main.call("_set_staff_candidate_panel_open",true)
var button: Button = main.staff_candidate_buttons.get(&"heroine_anri")
expect(button != null and button.disabled and button.text == "加入済み","loaded normal UI cannot join unique twice")
main.staff_management_toggle_button.pressed.emit()
var panel := main.find_child("StaffManagementPanel",true,false) as StaffManagementPanel
expect(panel != null,"normal hired staff panel present")
if panel != null:
panel.select_staff(&"heroine_anri")
expect(panel.staff_detail_label.text.contains("アンリ") and panel.staff_detail_label.text.contains("日給 G0"),"normal management shows named character and no upkeep")
var completed := unique_hire_result()
expect(completed != null and completed.is_success() and completed.details.get(&"employment_kind") == &"UNIQUE","disk-persisted completed UI command has actual UNIQUE mode")
var events := 0
for event: SimulationDomainEvent in runtime.domain_events.recent_events():
if event.event_type == &"STAFF_CANDIDATE_HIRED" and &"heroine_anri" in event.entity_ids:
events += 1
expect(event.payload.get(&"employment_kind") == &"UNIQUE","disk-persisted joining event has corrected mode")
expect(events == 1,"UI load contains one successful joining event")
var before: Dictionary = runtime.create_snapshot()
expect(not runtime.staff_service.retire_staff(&"heroine_anri").is_success() and not runtime.staff_service.remove_staff(&"heroine_anri",&"SOLD").is_success() and runtime.create_snapshot() == before,"loaded unique remains unsellable and non-retirable")
var corrupt := before.duplicate(true)
for row: Dictionary in corrupt[&"systems"][&"runtime"][&"staff"][&"staff"]:
if row[&"staff_id"] == "heroine_anri":
row[&"employment_kind"] = "FREE_HIRE"
expect(not runtime.restore_snapshot(corrupt) and runtime.create_snapshot() == before,"save corruption cannot downgrade unique protection")
func run() -> void:
var explicit_slot: bool = OS.has_environment("THE_TREE_UNIQUE_FREE_UI_SLOT")
slot = OS.get_environment("THE_TREE_UNIQUE_FREE_UI_SLOT")
if not explicit_slot:
slot = "user://tests/approved_free_unique_%d_%d/manual" % [OS.get_process_id(), Time.get_ticks_usec()]
if DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(slot)):
push_error("Generated test slot already exists; refuse overwrite.")
quit(90)
return
var relative_slot := slot.trim_prefix("user://tests/")
var valid_slot: bool = slot.begins_with("user://tests/") and not relative_slot.is_empty() and not relative_slot.ends_with("/") and not relative_slot.contains("\\")
for segment: String in relative_slot.split("/", true):
valid_slot = valid_slot and segment not in ["", ".", ".."]
if not valid_slot:
push_error("Explicit test slot is invalid; no production fallback.")
quit(90)
return
runtime = root.get_node("SimulationRuntime")
clock = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1,6,0)
state.reset()
expect(runtime.initialize(clock,state,424242)[&"valid"],"canonical real Runtime initializes")
clock.set_paused(true)
root.size = Vector2i(1440,900)
main = load("res://scenes/main/main.tscn").instantiate()
main.manual_save_path = slot
root.add_child(main)
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
await process_frame
await process_frame
if "--read" in OS.get_cmdline_user_args():
load_menu()
expect(clock.absolute_minute == 1441,"second-process normal load restores exact joined minute")
verify_joined()
else:
# Actual build palette and map handler; no direct completed-room insertion.
main.call("_select_build_definition",&"reception")
main.call("_on_map_cell_clicked",Vector3i(-6,0,0))
expect(runtime.construction_service.get_jobs().size() == 1,"normal UI submits reception construction")
clock.advance_minutes(runtime.definition_registry.get_facility(&"reception").build_minutes)
clock.advance_minutes(1439-clock.absolute_minute)
var cash: int = runtime.economy_service.get_balance(runtime.PLAYER_CASH_ACCOUNT)
# Explicit boundary fixture only; not a natural-spending/balance assertion.
expect(runtime.economy_service.transfer(runtime.PLAYER_CASH_ACCOUNT,&"CONSTRUCTION_EXPENSE",cash,clock.absolute_minute,&"CONSTRUCTION",&"",&"acceptance.free_unique.ui_cash_fixture").is_success(),"controlled ledger spend before real upkeep close")
clock.advance_one_minute()
expect(clock.absolute_minute == 1440 and not runtime._operations_should_block_tick(),"ordinary first unpaid close completes without terminal/pause/failure")
expect(runtime.upkeep_settlement.get_arrears_total() > 0,"real ownership upkeep supplies test debt")
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
var arrears_before: int = runtime.upkeep_settlement.get_arrears_total()
main.staff_candidate_toggle_button.pressed.emit()
var paid: Button = main.staff_candidate_buttons.get(&"staff_tessa")
expect(paid != null and not paid.disabled,"ordinary paid candidate still present")
if paid != null:
paid.pressed.emit()
expect(not runtime.staff_service.has_staff(&"staff_tessa") and runtime.economy_service.create_snapshot() == economy_before,"normal paid hire still denied without ledger changes")
var button: Button = main.staff_candidate_buttons.get(&"heroine_anri")
expect(button != null and button.is_visible_in_tree() and not button.disabled and button.text == "無料で加入","normal staff pane offers already-approved free Anri despite unrelated debt")
if button == null:
main.queue_free()
quit(failures)
return
var before: Dictionary = runtime.create_snapshot()
clock.set_paused(false)
button.pressed.emit()
clock.set_paused(true)
expect(not runtime.staff_service.has_staff(&"heroine_anri") and button.disabled and runtime.command_queue.pending_count() == 1,"running UI queues exactly one free joining request")
button.pressed.emit()
expect(runtime.command_queue.pending_count() == 1,"repeated UI press cannot enqueue twice")
save_button()
expect(SaveService.load_snapshot(slot)[&"ok"],"actual UI stores pending free request")
expect(runtime.restore_snapshot(before),"restore control before normal pending load")
load_menu()
expect(runtime.command_queue.pending_count() == 1 and not runtime.staff_service.has_staff(&"heroine_anri"),"normal disk load preserves pending request without premature joining")
clock.advance_one_minute()
expect(runtime.economy_service.create_snapshot() == economy_before and runtime.upkeep_settlement.get_arrears_total() == arrears_before,"executing restored free request does not spend or erase debt")
verify_joined()
save_button() # intentionally replace this explicit disposable test slot
expect(SaveService.load_snapshot(slot)[&"ok"],"joined state saved for independent process read")
var capture := OS.get_environment("THE_TREE_UNIQUE_FREE_UI_CAPTURE")
if not capture.is_empty() and DisplayServer.get_name() != "headless":
expect(capture.is_absolute_path() and not FileAccess.file_exists(capture),"new absolute rendered evidence path required")
if capture.is_absolute_path() and not FileAccess.file_exists(capture):
await process_frame
await RenderingServer.frame_post_draw
expect(root.get_texture().get_image().save_png(capture) == OK,"rendered named character and unpaid HUD captured")
main.queue_free()
await process_frame
print("APPROVED_FREE_UNIQUE_UI phase=%s checks=%d failures=%d" % ["read" if "--read" in OS.get_cmdline_user_args() else "write",checks,failures])
quit(failures)
@@ -0,0 +1 @@
uid://bnpq58ue07874
+70 -3
View File
@@ -14,6 +14,22 @@ var _warehouse: StringName = &""
var _save_path := "user://tests/business_ui_%d/queued_policy" % OS.get_process_id()
func _test_layer_palette() -> void:
_main.call("_set_layer", 0)
var before: Color = _panel.facility_status_label.get_theme_color("font_color")
var selected: bool = _panel.business_open_button.button_pressed
_main.call("_set_layer", 1)
_panel.refresh()
_expect(_panel.facility_status_label.get_theme_color("font_color") == Color("d8eeea"), "BACK panel text survives refresh")
_expect(_panel.business_open_button.get_theme_color("font_hover_color") == Color("d8eeea") and _panel.business_open_button.get_theme_color("font_disabled_color") == Color("8da4a4"), "BACK toggle remains readable for hover and disabled states")
_panel.call("_set_result", "条件未達", false)
_expect(_panel.result_label.get_theme_color("font_color") == Color("ffb498"), "BACK error retains warning colour")
_main.call("_set_layer", 0)
_expect(_panel.facility_status_label.get_theme_color("font_color") == before and _panel.result_label.get_theme_color("font_color") == Color("a7402e"), "FRONT restores panel and warning colours")
_expect(_panel.business_open_button.button_pressed == selected, "palette does not toggle policy")
_panel.call("_set_result", "", true)
func _initialize() -> void:
call_deferred("_run")
@@ -42,6 +58,7 @@ func _run() -> void:
_expect(_panel.visible and _panel.apply_button.disabled and _panel.facility_option.disabled,
"business nav opens a safe empty state before completed construction")
_expect("完成" in _panel.facility_status_label.text, "empty state explains completed facility prerequisite")
_test_layer_palette()
for request: Array in [
[&"reception", Vector3i(-8, 0, 0)],
[&"standard_room", Vector3i(-8, 2, 0)],
@@ -68,13 +85,30 @@ func _run() -> void:
return
_main.call("_activate_management_mode", &"business")
_panel.refresh()
_test_related_facility_disk_save()
_test_prices_and_drafts()
_test_open_close()
_test_saved_pending_edit()
_test_customer_counts()
await _test_navigation_and_layout()
_finish()
func _test_related_facility_disk_save() -> void:
_clock.set_paused(true)
var linked: CommandResult = _runtime.submit_command(&"set_facility_conditions", {&"facility_id": _rooms[0], &"conditions": {&"related_facility_id": _rooms[1]}})
_expect(linked.is_success(), "built room accepts related facility through Runtime")
var snapshot: Dictionary = _runtime.create_snapshot()
var saved: Dictionary = _runtime.save_to_path(_save_path)
_expect(bool(saved.get(&"ok", false)), "related facility saves to real disk slot")
_runtime.submit_command(&"set_facility_conditions", {&"facility_id": _rooms[0], &"conditions": {&"related_facility_id": &""}})
var loaded: Dictionary = _runtime.load_from_path(_save_path)
_expect(bool(loaded.get(&"ok", false)), "related facility loads through Runtime crosslink validation")
_expect(_runtime.facility_service.get_instance(_rooms[0]).related_facility_id == _rooms[1], "restored room retains the selected dependency")
_expect(_runtime.facility_service.create_snapshot() == snapshot[&"systems"][&"runtime"][&"facilities"], "facility state is exact across disk roundtrip")
_runtime.submit_command(&"set_facility_conditions", {&"facility_id": _rooms[0], &"conditions": {&"related_facility_id": &""}})
func _test_prices_and_drafts() -> void:
_choose(_panel.facility_option, _rooms[0])
_expect(_panel.price_option.item_count == 5 and not _panel.price_option.disabled, "five price modes are available for paid guest rooms")
@@ -124,8 +158,9 @@ func _test_open_close() -> void:
_expect(not bool(_row(_rooms[1]).get(&"business_open", true)) and "停止" in _panel.facility_status_label.text,
"close reaches actual facility operator state and status text")
_expect("受付済みの料金は変わりません" in _panel.explanation_label.text
and "開始済みのサービス" in _panel.explanation_label.text and "給与は発生" in _panel.explanation_label.text,
"UI explains fixed accepted prices, service completion, and continued wages during closure")
and "開始済みのサービス" in _panel.explanation_label.text
and "休業中も給与と施設維持費は発生" in _panel.explanation_label.text,
"UI explains fixed accepted prices, service completion, and continued wages plus upkeep during closure")
_panel.business_open_button.button_pressed = true
_panel.apply_button.pressed.emit()
_expect(bool(_row(_rooms[1]).get(&"business_open", false)), "admission can be reopened through the same controls")
@@ -204,7 +239,8 @@ func _test_navigation_and_layout() -> void:
_expect(control.get_global_rect().position.x >= side.get_global_rect().position.x
and control.get_global_rect().end.x <= side.get_global_rect().end.x, "%s fits inside narrow sidebar" % control.name)
if OS.get_environment("THE_TREE_BUSINESS_UI_CAPTURE") == "1":
var capture_dir := ProjectSettings.globalize_path("res://artifacts/business-management")
var requested_capture := OS.get_environment("THE_TREE_BUSINESS_CAPTURE_DIR")
var capture_dir := ProjectSettings.globalize_path(requested_capture if not requested_capture.is_empty() else "res://artifacts/business-management")
DirAccess.make_dir_recursive_absolute(capture_dir)
await RenderingServer.frame_post_draw
root.get_texture().get_image().save_png(capture_dir.path_join("business-panel.png"))
@@ -213,6 +249,10 @@ func _test_navigation_and_layout() -> void:
await process_frame
await RenderingServer.frame_post_draw
root.get_texture().get_image().save_png(capture_dir.path_join("warehouse-policy.png"))
if OS.get_environment("THE_TREE_PANEL_SKIN_CAPTURE") == "1":
_main.call("_set_layer", 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/business-skin-back.png")) == OK, "capture BACK management skin")
_main.call("_unhandled_key_input", _key(KEY_ESCAPE))
_expect(not _panel.visible and not bool(_main.get("_management_panel_open")), "Escape closes business inspector and restores full map")
@@ -257,3 +297,30 @@ func _finish() -> void:
_main.queue_free()
print("Business management UI: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)
func _test_customer_counts() -> void:
# Controlled multi-person read-model fixture; no simulated time advances.
var saved: Dictionary = _runtime.facility_service.create_snapshot()
var room: FacilityInstance = _runtime.facility_service.get_instance(_rooms[1])
room.active_services.clear()
room.customer_capacity = 3
var group := ServiceInstance.new().configure(&"group", &"group_request", room.instance_id, _clock.absolute_minute, 60, [&"group_a", &"group_b"], [])
room.active_services[&"group"] = group
var request := ServiceRequest.new().configure(&"waiting_group", room.instance_id, [&"waiting_a", &"waiting_b", &"waiting_c"], &"default", _clock.absolute_minute)
_expect(_runtime.facility_service.enqueue_request(request).is_success(), "queue a three-person request for the occupancy read model")
var row := _row(room.instance_id)
_expect(row[&"active_count"] == 1 and row[&"active_customer_count"] == 2, "one grouped service reports two users separately from service count")
_expect(row[&"queued_count"] == 1 and row[&"queued_customer_count"] == 3, "one queued request reports three waiting people")
_panel.refresh()
_choose(_panel.facility_option, room.instance_id)
_expect("待ち 3人 / 利用中 2人" in _panel.facility_status_label.text and "上限 3人" in _panel.facility_status_label.text, "business screen shows people and explicit capacity")
room.customer_capacity = -1
_panel.refresh()
_expect("上限 未設定" in _panel.facility_status_label.text, "unset customer limit is never replaced by generic capacity")
room.active_services.clear()
room.customer_capacity = 0
_panel.refresh()
_expect("利用中 0人" in _panel.facility_status_label.text and "上限 0人" in _panel.facility_status_label.text, "zero limit remains distinct from an unspecified limit")
_expect(_runtime.facility_service.restore_snapshot(saved), "discard controlled occupancy fixture without changing later gameplay tests")
_panel.refresh()
+85
View File
@@ -0,0 +1,85 @@
extends SceneTree
var checks:=0
var failures:=0
func expect(ok:bool,label:String)->void:
checks+=1
if not ok:failures+=1;push_error(label)
func _initialize()->void:call_deferred("run")
func cleric_id() -> StringName: return &"staff_uma"
func donation_price() -> int: return 20
func recruit_cleric(main: Control, _runtime: Variant) -> void:
main.staff_candidate_buttons[cleric_id()].pressed.emit()
func verify_effect(_runtime: Variant, _chapel: FacilityInstance) -> void: pass
func verify_after_start(_runtime: Variant, _chapel: FacilityInstance) -> void: pass
func verify_completion(_runtime: Variant, _chapel: FacilityInstance, _service_id: StringName) -> void: pass
func run()->void:
var runtime:Variant=root.get_node("SimulationRuntime")
var clock:Variant=root.get_node("GameClock")
var state:Variant=root.get_node("GameState")
clock.reset(1,6,0);state.reset();runtime.initialize(clock,state,42607);clock.set_paused(true)
var original:Dictionary=runtime.create_snapshot()
var legacy:=original.duplicate(true)
legacy[&"systems"][&"runtime"][&"catalog_manifest"][&"manifest_version"]=1
legacy[&"systems"][&"runtime"][&"catalog_manifest"][&"staff_candidate_ids"].erase("staff_uma")
expect(runtime.restore_snapshot(legacy),"exact old twenty-candidate manifest loads without losing progress")
var malformed:=legacy.duplicate(true)
malformed[&"systems"][&"runtime"][&"catalog_manifest"][&"staff_candidate_ids"].erase("staff_akane")
var intact:Dictionary=runtime.create_snapshot()
expect(not runtime.restore_snapshot(malformed) and runtime.create_snapshot()==intact,"arbitrary incomplete legacy catalog is rejected atomically")
malformed=legacy.duplicate(true)
malformed[&"systems"][&"runtime"][&"staff"][&"staff"]=[42]
expect(not runtime.restore_snapshot(malformed) and runtime.create_snapshot()==intact,"legacy malformed staff rows are rejected without a script type error")
var main:Control=load("res://scenes/main/main.tscn").instantiate();root.add_child(main)
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
await process_frame;await process_frame
expect(main.MVP_FACILITY_IDS.has(&"chapel"),"chapel is exposed in normal construction palette")
main.call("_select_build_definition",&"chapel");main.call("_on_map_cell_clicked",Vector3i(-6,0,0));clock.advance_minutes(180)
var chapel:FacilityInstance
for f:FacilityInstance in runtime.facility_service.get_instances():
if f.definition_id==&"chapel":chapel=f
expect(chapel!=null,"normal construction UI creates chapel")
if chapel==null:main.queue_free();quit(failures);return
expect(chapel.state==FacilityInstance.State.NO_STAFF,"chapel waits for cleric")
recruit_cleric(main, runtime)
expect(runtime.staff_service.has_staff(cleric_id()),"normal hire control recruits cleric")
var panel:StaffManagementPanel=main.find_child("StaffManagementPanel",true,false)
panel.refresh();panel.select_staff(cleric_id())
expect(panel.staff_detail_label.text.contains("聖職者"),"staff UI labels new role")
for i in panel.facility_option.item_count:
if panel.facility_option.get_item_metadata(i)==chapel.instance_id:
panel.facility_option.select(i);panel.facility_option.item_selected.emit(i)
panel.apply_button.pressed.emit();clock.advance_minutes(20)
expect(chapel.active_staff_ids.has(cleric_id()) and chapel.state==FacilityInstance.State.OPEN,"assigned cleric walks to workplace and opens chapel")
verify_effect(runtime, chapel)
expect(runtime.submit_command(&"spawn_guest",{&"guest_id":&"worship_walker",&"guest_type_id":&"traveler",&"gold":100,&"options":{&"current_cell":Vector3i(-7,0,0),&"state":GuestRuntime.State.DECIDING,&"planned_exit_at":clock.absolute_minute+300,&"needs":{&"sleep":0,&"hunger":0,&"hygiene":0,&"medical":0,&"leisure":100,&"intimacy":0}}}).is_success(),"controlled arrival without forced destination or service")
var service_id:StringName=&""
for minute in 30:
clock.advance_one_minute()
var g:GuestRuntime=runtime.guest_service.get_guest(&"worship_walker")
if g!=null and not g.current_service_id.is_empty():service_id=g.current_service_id;break
expect(not service_id.is_empty(),"guest autonomously selects, reaches and begins worship")
if not service_id.is_empty():
verify_after_start(runtime, chapel)
var midpoint:Dictionary=runtime.create_snapshot()
expect(runtime.restore_snapshot(midpoint),"normal cleric, visitor and worship save together")
for minute in 61:
clock.advance_one_minute()
if runtime.facility_service.get_service(service_id).state==ServiceInstance.State.COMPLETED:break
var completed := int(runtime.facility_service.get_instance(chapel.instance_id).lifetime_statistics.get(&"services_completed", 0))
expect(runtime.facility_service.get_service(service_id).state==ServiceInstance.State.COMPLETED and completed>0 and state.get_resource(&"faith")==completed and runtime.progression_service.metrics[&"faith_lifetime"]==completed,"each completed worship contributes exactly one Faith, including other autonomous visitors")
expect(runtime.economy_service.get_balance(&"GUEST_WALLET:worship_walker")==100-donation_price(),"actual worshipper pays quoted donation exactly once")
verify_completion(runtime, chapel, service_id)
expect(runtime.restore_snapshot(runtime.create_snapshot()),"completed chapel gameplay remains saveable")
main.navigation_buttons[&"business"].pressed.emit()
var business:BusinessManagementPanel=main.find_child("BusinessManagementPanel",true,false)
business.refresh()
for i in business.facility_option.item_count:
if business.facility_option.get_item_metadata(i)==chapel.instance_id:
business.facility_option.select(i);business.facility_option.item_selected.emit(i)
expect(business.quote_label.text.contains("%dG" % donation_price()) and business.effect_label.text.contains("信仰+1"),"business UI explains donation price and per-completion faith")
if OS.get_environment("THE_TREE_CHAPEL_CAPTURE")=="1":
root.size=Vector2i(1440,900)
await process_frame;await process_frame;await RenderingServer.frame_post_draw
expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path(OS.get_environment("THE_TREE_CHAPEL_CAPTURE_PATH") if not OS.get_environment("THE_TREE_CHAPEL_CAPTURE_PATH").is_empty() else "res://artifacts/facility-dimensions-2026-09-29/chapel-gameplay.png"))==OK,"capture native chapel business screen")
main.queue_free();await process_frame
print("Chapel gameplay: %d checks, %d failures"%[checks,failures]);quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://d1bcyrfalhjc8
+129
View File
@@ -0,0 +1,129 @@
extends SceneTree
var checks:=0
var failures:=0
var runtime: Variant
var clock: Variant
func expect(ok: bool,label: String)->void:
checks+=1
if not ok:failures+=1;push_error(label)
func _initialize()->void:call_deferred("run")
func active_job(id: StringName)->StaffTask:
for task: StaffTask in runtime.staff_service.get_tasks():
if task.task_type==&"facility_clean" and task.target_entity_id==id and not task.is_terminal():return task
return null
func cleaner_id() -> StringName: return &"staff_eri"
func work_ticks() -> int: return 8
func hire_cleaner(main: Control) -> void:
main.staff_candidate_buttons[cleaner_id()].pressed.emit()
func run()->void:
runtime=root.get_node("SimulationRuntime");clock=root.get_node("GameClock")
var state: Variant=root.get_node("GameState")
clock.reset(1,6,0);state.reset()
expect(runtime.initialize(clock,state,42631)[&"valid"],"initialize normal cleaning catalog")
clock.set_paused(true)
var main: Control=load("res://scenes/main/main.tscn").instantiate();root.add_child(main)
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
await process_frame;await process_frame
expect(main.MVP_FACILITY_IDS.has(&"cleaning_room"),"normal palette contains cleaning room")
for build: Array in [[&"cleaning_room",Vector3i(-6,0,1)],[&"reception",Vector3i(1,0,0)]]:
main.call("_select_build_definition",build[0]);main.call("_on_map_cell_clicked",build[1]);clock.advance_minutes(runtime.definition_registry.get_facility(build[0]).build_minutes)
var room: FacilityInstance
var reception: FacilityInstance
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id==&"cleaning_room":room=facility
if facility.definition_id==&"reception":reception=facility
expect(room!=null and reception!=null,"normal UI builds both completed facilities")
if room==null or reception==null:main.queue_free();await process_frame;quit(failures);return
var preview: WorldPreview=main.world_preview
var room_id:=room.instance_id
var id:=reception.instance_id
expect(room.customer_capacity==0 and room.state==FacilityInstance.State.NO_STAFF,"cleaning room excludes customers and waits for cleaner")
var panel: StaffManagementPanel=main.find_child("StaffManagementPanel",true,false)
for assignment: Array in [[cleaner_id(),room_id],[&"staff_akane",id]]:
if assignment[0]==cleaner_id(): hire_cleaner(main)
else: main.staff_candidate_buttons[assignment[0]].pressed.emit()
panel.refresh();panel.select_staff(assignment[0])
for i in panel.facility_option.item_count:
if panel.facility_option.get_item_metadata(i)==assignment[1]:panel.facility_option.select(i);panel.facility_option.item_selected.emit(i)
panel.apply_button.pressed.emit()
clock.advance_minutes(30)
expect(room.operation_state==FacilityInstance.OperationState.OPERATING and reception.operation_state==FacilityInstance.OperationState.OPERATING,"actual cleaner and receptionist commute to assigned workplaces")
expect(runtime.restore_snapshot(runtime.create_snapshot()),"staffed cleaning room save/load")
expect(preview._staff_work_animation(runtime.staff_service.get_staff(cleaner_id()))==&"idle","cleaner at staffed base does not mime cleaning")
var count:=0
for batch in 4:
var size:=8 if batch<3 else 1
for index in size:
var guest:=StringName("cleaning_payer_%d"%count)
expect(runtime.submit_command(&"register_guest_wallet",{&"guest_id":guest,&"gold":100}).is_success(),"fund controlled visit")
expect(runtime.submit_command(&"create_service_request",{&"request_id":StringName("cleaning_visit_%d"%count),&"facility_id":id,&"guest_ids":[guest],&"service_type":&"check_in"}).is_success(),"request real reception service")
count+=1
for minute in 25:
clock.advance_one_minute()
if int(runtime.facility_cleanliness.get_facility(id)["current"])==100-count:break
expect(runtime.facility_cleanliness.get_facility(id)["current"]==75,"25 completed defined visits reach cleaning threshold")
var task:=active_job(id)
expect(task!=null,"threshold schedules an actual staff task")
if task==null:main.queue_free();await process_frame;quit(failures);return
var task_id:=task.task_id
var walking_seen:=false
for minute in 80:
clock.advance_one_minute();task=runtime.staff_service.get_task(task_id)
if task.state==StaffTask.State.MOVING:
walking_seen=true
expect(preview._staff_work_animation(runtime.staff_service.get_staff(cleaner_id()))!=&"clean","moving cleaner does not mop before arrival")
expect(runtime.facility_cleanliness.get_facility(id)["current"]==75,"walking does not clean")
if task.state==StaffTask.State.WORKING:break
expect(walking_seen and task.state==StaffTask.State.WORKING,"assigned cleaner physically reaches work site")
var worker: StaffRuntime=runtime.staff_service.get_staff(cleaner_id())
expect(worker.current_cell==task.target_cell and task.progress_minutes==0,"arrival starts work with zero elapsed minutes")
expect(runtime.facility_service.get_instance(room_id).operation_state==FacilityInstance.OperationState.OPERATING,"deployed cleaner continues staffing cleaning room")
if work_ticks() < 8:
expect(runtime.cleaning_bridge.status(id).contains("3倍速・残り3分"),"cleaning UI describes real remaining time at triple rate")
var working: Dictionary=runtime.create_snapshot()
expect(runtime.restore_snapshot(working),"cleaner away from room and working task restore together")
expect(preview._staff_work_animation(runtime.staff_service.get_staff(cleaner_id()))==&"clean","restored on-site active task selects mop animation")
expect(not runtime.submit_command(&"progress_staff_task",{&"task_id":task_id,&"minutes":8}).is_success(),"manual progress cannot bypass actual work time")
# Save this command receipt too, so both replay paths have identical command history.
working=runtime.create_snapshot()
clock.advance_minutes(work_ticks()-1)
expect(runtime.facility_cleanliness.get_facility(id)["current"]==75 and runtime.staff_service.get_task(task_id).progress_minutes==(work_ticks()-1)*FacilityCleaningBridge.work_units_per_minute(runtime.staff_service.get_staff(cleaner_id())),"work before final tick is insufficient")
clock.advance_one_minute()
expect(runtime.facility_cleanliness.get_facility(id)["current"]==100 and runtime.staff_service.get_task(task_id).state==StaffTask.State.COMPLETED,"final tick completes one25-point recovery")
expect(preview._staff_work_animation(runtime.staff_service.get_staff(cleaner_id()))!=&"clean","completed job stops mop animation")
var finished: Dictionary=runtime.create_snapshot()
expect(runtime.restore_snapshot(working),"rewind actual cleaning work")
clock.advance_minutes(work_ticks())
expect(runtime.create_snapshot()==finished,"cleaning replay matches all Runtime state")
# Stop an in-progress job through normal business controls. No partial recovery.
expect(runtime.restore_snapshot(working),"rewind for closed cleaning room")
clock.advance_minutes(work_ticks()-1)
expect(runtime.submit_command(&"set_facility_policy",{&"facility_id":room_id,&"patch":{&"business_open":false}}).is_success(),"close cleaning room through business policy")
clock.advance_minutes(10)
expect(runtime.facility_cleanliness.get_facility(id)["current"]<=75 and runtime.staff_service.get_task(task_id).state==StaffTask.State.CANCELLED,"closed source cancels partial work without recovery")
expect(preview._staff_work_animation(runtime.staff_service.get_staff(cleaner_id()))!=&"clean","closed base cancels visible cleaning")
expect(runtime.restore_snapshot(runtime.create_snapshot()),"cancelled work plus waiting replacement job restore")
expect(runtime.restore_snapshot(working),"rewind for power interruption")
clock.advance_minutes(work_ticks()-1)
state.set_resource(&"mana",0);clock.advance_minutes(10)
expect(runtime.facility_cleanliness.get_facility(id)["current"]<=75 and runtime.staff_service.get_task(task_id).state==StaffTask.State.CANCELLED,"power loss stops cleaning without recovery")
expect(runtime.restore_snapshot(runtime.create_snapshot()),"unpowered cleaning checkpoint restores")
expect(runtime.restore_snapshot(finished),"completed cleaning receipt restores")
clock.advance_minutes(5)
var cleaning_receipts:=0
for receipt: Dictionary in runtime.facility_cleanliness.create_snapshot()["receipts"]:
if receipt["kind"]=="clean" and StringName(receipt["source_id"])==task_id:cleaning_receipts+=1
expect(cleaning_receipts==1 and active_job(id)==null,"completion is not repeated; later normal guest visits may soil again")
main.navigation_buttons[&"business"].pressed.emit()
var business: BusinessManagementPanel=main.find_child("BusinessManagementPanel",true,false);business.refresh()
for i in business.facility_option.item_count:
if business.facility_option.get_item_metadata(i)==id:business.facility_option.select(i);business.facility_option.item_selected.emit(i)
expect(business.facility_status_label.text.contains("清潔度 %d / 100" % runtime.facility_cleanliness.get_facility(id)["current"]),"normal business UI shows actual cleanliness")
business.select_facility(room_id)
expect(business.business_open_button.text=="清掃業務を有効にする" and business.facility_status_label.text.contains("顧客利用なし") and not business.effect_label.text.contains("受付"),"cleaning room controls describe staff work rather than customer reception")
if OS.get_environment("THE_TREE_CLEANING_CAPTURE")=="1":
root.size=Vector2i(1280,720)
await process_frame;await process_frame;await RenderingServer.frame_post_draw
expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/cleanliness-runtime-2026-09-29/cleaning-gameplay.png"))==OK,"capture actual cleaning UI")
main.queue_free();await process_frame
print("Cleaning gameplay: %d checks, %d failures"%[checks,failures]);quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://dwknbl8i0a7vt
+40
View File
@@ -0,0 +1,40 @@
extends SceneTree
var checks:=0
var failures:=0
func expect(ok: bool,label: String) -> void:
checks+=1
if not ok: failures+=1;push_error(label)
func _initialize() -> void:call_deferred("run")
func run() -> void:
var runtime: Variant=root.get_node("SimulationRuntime")
var clock: Variant=root.get_node("GameClock")
var state: Variant=root.get_node("GameState")
clock.reset(1,6,0);state.reset();runtime.initialize(clock,state,42605);clock.set_paused(true)
var main: Control=load("res://scenes/main/main.tscn").instantiate()
main.set_script(load("res://tests/ui/fixtures/counted_dashboard.gd"))
root.add_child(main)
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
await process_frame;await process_frame
var before: Dictionary=runtime.create_snapshot()
var initial: int=main.dashboard_refresh_count
clock.advance_minutes(3)
expect(clock.absolute_minute==363,"three simulation minutes still advance")
expect(main.dashboard_refresh_count==initial,"tick notifications do not rebuild dashboard synchronously")
expect(main._dashboard_refresh_pending,"batch schedules a dashboard refresh")
var after: Dictionary=runtime.create_snapshot()
await process_frame;await process_frame
expect(main.dashboard_refresh_count==initial+1,"three publications produce one completed display refresh")
expect(not main._dashboard_refresh_pending,"display refresh clears pending flag")
expect(runtime.create_snapshot()==after,"deferred display refresh cannot mutate simulation state")
main._flush_dashboard_refresh()
expect(main.dashboard_refresh_count==initial+1,"empty flush does not rebuild dashboard")
clock.advance_one_minute()
await process_frame;await process_frame
expect(main.dashboard_refresh_count==initial+2,"next frame gets a fresh display refresh")
expect(runtime.restore_snapshot(before),"restore initial simulation checkpoint")
await process_frame;await process_frame
clock.advance_minutes(3)
expect(runtime.create_snapshot()==after,"coalesced display preserves deterministic simulation replay")
main.queue_free()
await process_frame;await process_frame
print("Dashboard coalescing: %d checks, %d failures" % [checks,failures]);quit(failures)
@@ -0,0 +1 @@
uid://djxfmyj5xengi
+323
View File
@@ -0,0 +1,323 @@
extends SceneTree
## Command-backed UI acceptance. Incidents are disclosed threat fixtures;
## combat progression uses the ordinary game clock.
var _checks := 0
var _failures := 0
var _main: Control
var _runtime: Variant
var _clock: Variant
var _panel: DefenseManagementPanel
var _facility_id: StringName = &""
var _path := "user://tests/defense_management_%d/slot" % OS.get_process_id()
var _slots: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _expect(ok: bool, message: String) -> void:
_checks += 1
if not ok:
_failures += 1
push_error(message)
func _run() -> void:
root.gui_embed_subwindows = true
root.size = Vector2i(1440, 810)
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_clock.reset(1, 6, 0)
root.get_node("GameState").reset()
_runtime.initialize(_clock, root.get_node("GameState"), 42540)
_clock.set_paused(true)
_main = load("res://scenes/main/main.tscn").instantiate()
_main.set("manual_save_path", _path)
root.add_child(_main)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
await process_frame
await process_frame
_main.call("_set_management_panel_open", true)
_main.find_child("DefenseManagementButton", true, false).pressed.emit()
_panel = _main.get("defense_manager").find_child("DefenseManagementPanel", true, false)
_expect(_main.get("defense_manager").visible and _panel.selected_raid_id.is_empty(), "ordinary management button opens empty defense screen")
_expect("ありません" in _panel.summary_label.text and _panel.start_button.disabled, "no incident has no actionable start")
# Paid normal construction and hiring establish real target/defender links.
_main.get("defense_manager").hide()
_main.call("_select_build_definition", &"reception")
_main.call("_on_map_cell_clicked", Vector3i(-8, 0, 0))
_main.call("_clear_build_selection")
_clock.advance_minutes(120)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"reception": _facility_id = facility.instance_id
_expect(not _facility_id.is_empty(), "normal paid reception construction provides a real target")
_main.get("staff_candidate_toggle_button").pressed.emit()
_main.get("staff_candidate_buttons")[&"staff_klaus"].pressed.emit()
_expect(_runtime.staff_service.has_staff(&"staff_klaus"), "ordinary recruitment hires guard with existing combat data")
if _facility_id.is_empty() or not _runtime.staff_service.has_staff(&"staff_klaus"):
_finish()
return
_expect(_create_incident(&"ui.defense.fixture").is_success(), "disclosed threat fixture creates an existing preparing incident")
_main.find_child("DefenseManagementButton", true, false).pressed.emit()
_expect(_panel.selected_raid_id == &"ui.defense.fixture" and "準備中" in _panel.summary_label.text, "real popup observes Runtime incident publication")
_expect(_panel.find_child("DefenseCancelPreparation", true, false) == null and &"cancel_defense_raid" not in DefenseManagementPanel.COMMANDS, "preparation has no free incident-cancellation button or allowed UI command")
var preparing_before: Dictionary = _runtime.create_snapshot()
_panel.call("_submit", &"cancel_defense_raid")
_panel.retreat_button.pressed.emit()
_expect(not _panel.confirmation.visible and (_panel.get("_confirmation_context") as Dictionary).is_empty(), "preparation cannot open a withdrawal confirmation even through a stale hidden button signal")
_expect(preparing_before == _runtime.create_snapshot(), "forbidden preparation cancellation and withdrawal do not enqueue commands or mutate any Runtime state")
var before: Dictionary = _runtime.create_snapshot()
var report: Dictionary = _runtime.get_defense_management_report(&"ui.defense.fixture")
_expect(before == _runtime.create_snapshot(), "management report does not mutate gameplay or RNG")
report[&"selected"][&"objective_ids"].clear()
report[&"staff"][0][&"combat"] = 999
_expect(before == _runtime.create_snapshot(), "compact projections are detached from Defense and Staff")
_expect(not _panel.register_staff_button.disabled and _panel.start_button.disabled, "unregistered objective explains and prevents start")
_panel.register_staff_button.pressed.emit()
var raid: Dictionary = _runtime.defense_service.get_raid(&"ui.defense.fixture")
var guard: StaffRuntime = _runtime.staff_service.get_staff(&"staff_klaus")
_expect(raid[&"combatants"].has(&"staff_klaus"), "real participation button registers defender through Runtime command")
_expect(raid[&"combatants"][&"staff_klaus"][&"combat"] == guard.skills[&"combat"] and guard.state == StaffRuntime.State.UNAVAILABLE, "defender uses actual stored skill and becomes unavailable for work")
_panel.register_objective_button.pressed.emit()
raid = _runtime.defense_service.get_raid(&"ui.defense.fixture")
_expect(raid[&"assets"].has(_facility_id) and not _panel.start_button.disabled, "real target button registers existing facility and enables start")
_expect(raid[&"assets"][_facility_id][&"integrity"] == _runtime.facility_service.get_instance(_facility_id).durability, "objective reflects current facility durability")
# Policy edits survive publication until explicitly applied, and do not
# change current policy before the player presses the apply button.
_panel.policy_option.select(2)
_panel.policy_option.item_selected.emit(2)
_panel.threshold_spin.value = 7
_panel.refresh()
_expect(_panel.policy_option.selected == 2 and _panel.threshold_spin.value == 7, "uncommitted retreat draft survives UI refresh")
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"retreat_policy"] == &"HOLD", "editing a draft alone does not alter defense")
_panel.policy_button.pressed.emit()
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"retreat_policy"] == &"THRESHOLD" and _runtime.defense_service.get_raid(&"ui.defense.fixture")[&"defender_retreat_threshold"] == 7, "apply button updates existing retreat policy contract")
_main.call("_save_manual")
_slots.append(_path)
var package := SaveService.load_snapshot(_path)
_expect(package.get(&"ok", false) and package.get(&"manifest", {}).get("interface_selection", {}).get("defense_id") == "ui.defense.fixture" and package.get(&"manifest", {}).get("interface_selection", {}).get("defense_open") == true, "normal save captures selected incident and open popup")
# Change a real policy after saving so the following read proves restoration.
_panel.policy_option.select(0)
_panel.policy_option.item_selected.emit(0)
_panel.policy_button.pressed.emit()
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"retreat_policy"] == &"HOLD", "ordinary policy button changes live preparation after the saved checkpoint")
_main.get("defense_manager").hide()
_main.call("_load_manual", true)
_expect(_main.get("defense_manager").visible and _panel.selected_raid_id == &"ui.defense.fixture", "manual load restores actual saved incident and visibility")
var restored_preparing: Dictionary = _runtime.defense_service.get_raid(&"ui.defense.fixture")
_expect(restored_preparing[&"state"] == DefenseService.RaidState.PREPARING and _runtime.staff_service.get_staff(&"staff_klaus").state == StaffRuntime.State.UNAVAILABLE, "saved preparation remains a live threat and keeps its actual defender locked")
_expect(restored_preparing[&"retreat_policy"] == &"THRESHOLD" and restored_preparing[&"defender_retreat_threshold"] == 7, "manual load restores the saved policy instead of the later live edit")
_clock.set_paused(false)
_panel.start_button.pressed.emit()
_panel.start_button.pressed.emit()
var queued := 0
for command: Dictionary in _runtime.command_queue.create_snapshot().get("pending", []):
if command.get("command_type", "") == "start_defense_raid": queued += 1
_expect(queued == 1 and _panel.start_button.disabled, "playing simulation accepts a single pending start despite repeated button activation")
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.PREPARING, "accepted running command has not mutated the incident before its tick")
_clock.advance_minutes(1)
_clock.set_paused(true)
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.ACTIVE and _panel.retreat_button.visible, "start button enters actual battle and exposes withdrawal")
# Stale start invocation is rejected by Runtime and never restarts combat.
var active: Dictionary = _runtime.defense_service.create_snapshot()
_panel.start_button.pressed.emit()
_expect(active == _runtime.defense_service.create_snapshot(), "invalid repeated start preserves active battle state")
# Withdrawal is available only after the existing incident starts. Backing
# out changes presentation alone and retains the threat and defender locks.
var active_before_return: Dictionary = _runtime.create_snapshot()
_panel.retreat_button.pressed.emit()
_expect(_panel.confirmation.visible and _panel.confirmation.title == "撤退の確認", "active battle opens only a withdrawal confirmation")
_panel.confirmation.canceled.emit()
_panel.confirmation.hide()
_expect((_panel.get("_confirmation_context") as Dictionary).is_empty() and active_before_return == _runtime.create_snapshot(), "returning from active withdrawal clears confirmation without changing gameplay or queued commands")
# Save the ACTIVE incident, then replace its queue through an actual load.
_main.call("_save_manual")
var active_package := SaveService.load_snapshot(_path)
_expect(active_package.get(&"ok", false), "normal save writes the active battle before stale-context testing")
_panel.retreat_button.pressed.emit()
var context: Dictionary = _panel.get("_confirmation_context").duplicate(true)
_expect(_panel.confirmation.visible and not context.is_empty(), "active withdrawal captures the current command queue before load")
_main.call("_load_manual", true)
_expect(_main.get("build_status_label").text == "手動セーブを読み込みました。" and _main.get("defense_manager").visible and _panel.selected_raid_id == &"ui.defense.fixture", "successful strict read restores active incident selection and popup")
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.ACTIVE, "active save restores actual battle state")
_expect(not _panel.confirmation.visible and (_panel.get("_confirmation_context") as Dictionary).is_empty(), "successful active load dismisses and invalidates the old withdrawal confirmation")
_expect(_runtime.command_queue.get_instance_id() != int(context.get(&"queue_instance_id", 0)), "successful load uses a different command queue than the pending confirmation")
var loaded_before_replay: Dictionary = _runtime.create_snapshot()
_panel.set("_confirmation_context", context)
_panel.confirmation.confirmed.emit()
_expect(loaded_before_replay == _runtime.create_snapshot(), "replayed old-queue withdrawal context leaves every loaded Runtime subsystem and command unchanged")
_expect("対象が変わりました" in _panel.result_label.text and _runtime.staff_service.get_staff(&"staff_klaus").state == StaffRuntime.State.UNAVAILABLE, "stale withdrawal reports rejection and leaves the loaded defender assigned")
# The disclosed fixture creates only the threat. Combat now progresses from
# ordinary game minutes without a round button or a synthetic player command.
var interval := int(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"round_minutes"])
_clock.advance_minutes(interval - 1)
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"current_round"] == 0, "ordinary battle waits for its persisted round interval")
_clock.advance_minutes(1)
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"current_round"] == 1, "ordinary clock automatically resolves the first attack")
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.BETWEEN_WAVES and _panel.next_wave_button.visible, "service wave outcome exposes ordinary next-wave control")
_panel.next_wave_button.pressed.emit()
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.ACTIVE, "player next-wave button advances existing incident through Runtime")
_panel.retreat_button.pressed.emit()
_panel.confirmation.confirmed.emit()
_panel.confirmation.hide()
_expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.DEFENDERS_RETREATED, "confirmed withdrawal terminates battle through Runtime")
_expect(_runtime.staff_service.get_staff(&"staff_klaus").state == StaffRuntime.State.FLEEING and _panel.policy_button.disabled, "retreat reconciles staff and terminal UI disables commands")
# Failed gameplay loads retain current selection. Optional bad presentation
# must not invalidate a valid gameplay load or reopen this inspector.
var selected := _panel.selected_raid_id
_expect(not _runtime.load_from_path(_path + "_absent").get(&"ok", true), "missing package load fails explicitly")
_expect(_panel.selected_raid_id == selected and _main.get("defense_manager").visible, "failed load preserves visible defense selection")
for entry: Array in [["types", []], ["missing", "missing.raid"], ["long", "x".repeat(257)]]:
var slot: String = _path + "_" + entry[0]
_slots.append(slot)
_expect(SaveService.save_snapshot(slot, _runtime.create_snapshot(), {&"interface_selection": {&"version": 1, &"defense_id": entry[1], &"defense_open": true}}).get(&"ok", false), "write malformed optional UI metadata fixture " + entry[0])
_expect(_runtime.load_from_path(slot).get(&"ok", false), "valid gameplay still loads with optional invalid UI metadata " + entry[0])
_expect(not _main.get("defense_manager").visible, "invalid incident selection does not reopen popup " + entry[0])
_main.find_child("DefenseManagementButton", true, false).pressed.emit()
await _test_incident_presentation()
var capture := OS.get_environment("THE_TREE_DEFENSE_MANAGEMENT_CAPTURE")
if not capture.is_empty():
for frame: int in 4: await process_frame
await RenderingServer.frame_post_draw
_expect(root.get_texture().get_image().save_png(capture) == OK, "capture actual defense popup after source-label and saved-selection checks")
_finish()
func _create_incident(id: StringName) -> CommandResult:
return _runtime.submit_command(&"create_defense_raid", {&"raid_id": id, &"definition": {
&"entry_points": [&"fixture.gate"], &"objective_ids": [_facility_id],
&"waves": [
{&"wave_id": &"fixture.wave.one", &"threat_strength": 5, &"max_rounds": 1},
{&"wave_id": &"fixture.wave.two", &"threat_strength": 5, &"max_rounds": 1},
],
}}, &"test.fixture")
func _finish() -> void:
if is_instance_valid(_main): _main.queue_free()
for path: String in _slots:
SaveService.delete_save(path)
if _failures == 0: print("PASS: %d checks" % _checks)
else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
## Display acceptance uses explicit disclosed causes through the actual Event
## queue. These cases do not claim normal threat generation or approve balance.
func _test_incident_presentation() -> void:
var expected := {"THIEF": "盗賊の襲撃", "BANKRUPTCY_RIOT": "客の暴動", "BANK_DELINQUENCY": "銀行による取り立て", "AUTHORITY_ESCALATION": "監査による摘発"}
var event_ids: Dictionary = {}
for kind: String in expected:
var event_id := StringName("ui.presentation.event." + kind)
var instance_id := StringName("ui.presentation.instance." + kind)
var action_id := &"ui.presentation.threat"
var action := EventAction.new().configure(action_id, EventAction.Type.CREATE_DEFENSE_RAID, {
&"template_id": &"ui.presentation.disclosed.template", &"source_kind": kind,
&"definition": _presentation_raid_definition(),
})
var registered: CommandResult = _runtime.submit_command(&"register_event_definition", {&"definition": {
&"event_id": event_id, &"category": &"DEFENSE", &"foreground": false,
&"transaction_policy": &"ROLLBACK_ALL", &"automatic_actions": [action.create_snapshot()],
}}, &"test.fixture")
_expect(registered.is_success(), "disclosed presentation source registers " + kind)
var queued: CommandResult = _runtime.submit_command(&"enqueue_event", {
&"event_id": event_id, &"actor_ids": [], &"options": {
&"event_instance_id": instance_id,
&"context": {&"threat_source_id": StringName("ui.presentation.cause." + kind)},
},
}, &"test.fixture")
_expect(queued.is_success(), "disclosed presentation cause enters the real event queue " + kind)
_clock.advance_minutes(1)
var raid_id := StringName("event_defense_raid." + JSON.stringify([String(instance_id), String(action_id)], "", true, true).sha256_text())
event_ids[kind] = raid_id
var raid: Dictionary = _runtime.defense_service.get_raid(raid_id)
_expect(not raid.is_empty() and raid.get(&"event_source", {}).get("source_kind", "") == kind, "ordinary event processing supplies explicit source provenance " + kind)
# The two manual incidents deliberately resemble a source enum. They have
# no event provenance and must remain generic defense records.
var manual_one := &"THIEF"
var manual_two := &"ui.presentation.manual.thief.source"
for id: StringName in [manual_one, manual_two]:
var definition := _presentation_raid_definition()
definition[&"source_id"] = &"THIEF"
var created: CommandResult = _runtime.submit_command(&"create_defense_raid", {&"raid_id": id, &"definition": definition}, &"test.fixture")
_expect(created.is_success(), "manual same-minute presentation incident is disclosed and created")
_main.find_child("DefenseManagementButton", true, false).pressed.emit()
_panel.refresh()
var before: Dictionary = _runtime.create_snapshot()
var report: Dictionary = _runtime.get_defense_management_report(event_ids["THIEF"])
var selected: Dictionary = report.get(&"selected", {})
_expect(selected.get(&"source_kind", "") == "THIEF" and typeof(selected.get(&"created_at")) == TYPE_INT, "compact management report exposes only explicit cause and persisted creation time")
selected[&"source_kind"] = "BANK_DELINQUENCY"
selected[&"created_at"] = -1
_expect(_runtime.create_snapshot() == before, "display projection fields remain owned and cannot mutate gameplay, source or RNG")
for kind: String in expected:
var raid_id := StringName(event_ids[kind])
_select_presentation_option(raid_id)
var index := _panel.raid_option.selected
_expect(_panel.selected_raid_id == raid_id and _panel.raid_option.get_item_metadata(index) == raid_id, "cause label never replaces internal selection metadata " + kind)
_expect(expected[kind] in _panel.raid_option.get_item_text(index) and "日目" in _panel.raid_option.get_item_text(index), "option shows readable cause and occurrence time " + kind)
_expect(expected[kind] in _panel.summary_label.text and String(raid_id) not in _panel.summary_label.text and "event_defense_raid." not in _panel.raid_option.get_item_text(index), "summary and option keep the SHA out of user text " + kind)
var again: Dictionary = _runtime.create_snapshot()
_panel.refresh()
_expect(_runtime.create_snapshot() == again and _panel.selected_raid_id == raid_id, "repeated label refresh preserves every gameplay system and selected record " + kind)
var manual_labels: Array[String] = []
for id: StringName in [manual_one, manual_two]:
_select_presentation_option(id)
manual_labels.append(_panel.raid_option.get_item_text(_panel.raid_option.selected))
_expect(_panel.selected_raid_id == id and "防衛戦" in _panel.summary_label.text and "盗賊" not in _panel.summary_label.text, "manual ID and source_id never imply a thief cause")
_expect(manual_labels[0] != manual_labels[1] and "(1)" in manual_labels[0] and "(2)" in manual_labels[1], "same cause/time manual incidents remain separately readable")
_expect(_panel.call("_raid_base_label", {&"raid_id": &"THIEF", &"source_id": &"THIEF"}) == "防衛戦 · 時刻不明", "legacy projection without cause/time never fabricates a source or midnight timestamp")
_expect("防衛戦" in String(_panel.call("_raid_base_label", {&"source_kind": "FUTURE_KIND", &"created_at": 0})), "unknown cause text receives a neutral label")
var saved_id := StringName(event_ids["THIEF"])
_select_presentation_option(saved_id)
var saved_text := _panel.summary_label.text
_panel.policy_option.select(2)
_panel.policy_option.item_selected.emit(2)
_panel.threshold_spin.value = 17
_panel.policy_button.pressed.emit()
_expect(_runtime.defense_service.get_raid(saved_id)[&"retreat_policy"] == &"THRESHOLD" and _runtime.defense_service.get_raid(saved_id)[&"defender_retreat_threshold"] == 17, "readable event selection sends the ordinary policy command to its actual internal raid")
_expect(_runtime.defense_service.get_raid(event_ids["BANK_DELINQUENCY"])[&"retreat_policy"] == &"HOLD", "selected policy action never affects another similarly displayed incident")
_main.call("_save_manual")
var package := SaveService.load_snapshot(_path)
_expect(package.get(&"ok", false) and package.get(&"manifest", {}).get("interface_selection", {}).get("defense_id") == String(saved_id), "normal UI save retains the full internal event raid ID")
_select_presentation_option(manual_two)
_main.get("defense_manager").hide()
_main.call("_load_manual", true)
_expect(_main.get("build_status_label").text == "手動セーブを読み込みました。" and _main.get("defense_manager").visible, "strict normal load restores the saved readable defense panel")
_expect(_panel.selected_raid_id == saved_id and _panel.summary_label.text == saved_text, "cause, creation time, state and exact selected event raid survive normal save/load")
_expect(_runtime.defense_service.get_raid(saved_id)[&"event_source"]["source_kind"] == "THIEF", "display labels never overwrite restored event provenance")
_expect(_runtime.defense_service.get_raid(saved_id)[&"retreat_policy"] == &"THRESHOLD" and _panel.threshold_spin.value == 17, "saved user command and policy controls restore on the same readable incident")
var restored_labels: Array[String] = []
for id: StringName in [manual_one, manual_two]:
_select_presentation_option(id)
restored_labels.append(_panel.raid_option.get_item_text(_panel.raid_option.selected))
_expect(restored_labels == manual_labels, "duplicate occurrence labels retain the same order after restoration")
_select_presentation_option(saved_id)
var title_before_failed_load := _panel.summary_label.text
_expect(not _runtime.load_from_path(_path + "_presentation_absent").get(&"ok", true), "failed load remains explicit during source-label acceptance")
_expect(_panel.selected_raid_id == saved_id and _panel.summary_label.text == title_before_failed_load, "failed read preserves readable selection and title")
for frame: int in 4: await process_frame
_expect(not _panel.raid_option.fit_to_longest_item and _panel.raid_option.clip_text, "raid selector cannot force window width from an internal or future long label")
_expect(_main.get("defense_manager").size.x <= 520 and _panel.raid_option.get_global_rect().size.x <= _main.get("defense_manager").size.x, "actual 520px defense dialog and selector stay within their intended width")
_expect(_panel.raid_option.get_item_tooltip(_panel.raid_option.selected) == _panel.raid_option.get_item_text(_panel.raid_option.selected), "clipped selector offers the full human-readable text instead of a technical ID")
func _select_presentation_option(raid_id: StringName) -> void:
for index: int in _panel.raid_option.item_count:
if _panel.raid_option.get_item_metadata(index) == raid_id:
_panel.raid_option.select(index)
_panel.raid_option.item_selected.emit(index)
return
_expect(false, "requested presentation raid exists in actual option metadata")
func _presentation_raid_definition() -> Dictionary:
return {
&"entry_points": [&"ui.presentation.disclosed.gate"], &"objective_ids": [_facility_id],
&"round_minutes": 5, &"max_duration_minutes": 120,
&"retreat_policy": &"HOLD", &"defender_retreat_threshold": 0,
&"terrain_bonus": 0, &"command_bonus": 0, &"surprise_penalty": 0, &"multiple_entry_penalty": 0,
&"waves": [{&"wave_id": &"ui.presentation.disclosed.wave", &"threat_strength": 5, &"max_rounds": 1, &"retreat_threshold": 0}],
}
+1
View File
@@ -0,0 +1 @@
uid://dlgto3iitns6f
+54
View File
@@ -0,0 +1,54 @@
extends SceneTree
var checks:=0
var failures:=0
func expect(ok: bool, message: String) -> void:
checks+=1
if not ok: failures+=1;push_error(message)
func _initialize() -> void: call_deferred("run")
func run() -> void:
var runtime: Variant=root.get_node("SimulationRuntime")
var clock: Variant=root.get_node("GameClock")
var state: Variant=root.get_node("GameState")
clock.reset(1,6,0);state.reset();runtime.initialize(clock,state,42507);clock.set_paused(true)
var paths: Array[String]=FacilityCatalog.FACILITY_PATHS+FacilityCatalog.INFRASTRUCTURE_FACILITY_PATHS+FacilityCatalog.RESCUE_FACILITY_PATHS+FacilityCatalog.ORIGINAL_FACILITY_PATHS
paths.erase("res://data/facilities/expansion/premium_room.tres");paths.append("res://tests/fixtures/legacy_premium_room.tres")
expect(runtime.definition_registry.rebuild_from_paths(paths)[&"valid"],"load legacy fixture definition")
expect(runtime.submit_command(&"build_facility",{&"definition_id":&"reception",&"origin":Vector3i(-6,0,0)}).is_success(),"build fixture reception")
clock.advance_minutes(120)
expect(runtime.submit_command(&"build_facility",{&"definition_id":&"premium_room",&"origin":Vector3i(-6,2,0)}).is_success(),"build old-size room")
clock.advance_minutes(150)
paths.erase("res://tests/fixtures/legacy_premium_room.tres");paths.append("res://data/facilities/expansion/premium_room.tres");runtime.definition_registry.rebuild_from_paths(paths)
var source: Dictionary=runtime.create_snapshot()
var path := "user://saves/migration_ui_%d" % Time.get_ticks_usec()
expect(SaveService.save_snapshot(path,source)[&"ok"],"persist disposable old-size game")
var main: Control=load("res://scenes/main/main.tscn").instantiate()
main.set("manual_save_path",path);root.size=Vector2i(1440,810);root.add_child(main)
await process_frame;await process_frame
var live: Dictionary=runtime.create_snapshot()
var menu: MenuButton=main.find_child("SaveLoadMenu",true,false)
expect(menu!=null and menu.get_popup().get_item_count()==2,"load menu exposes ordinary and dimension-migration actions")
menu.get_popup().id_pressed.emit(1)
var dialog: ConfirmationDialog=main.get("dimension_migration_dialog")
expect(dialog!=null and dialog.visible and "上級客室" in dialog.dialog_text and "4×1" in dialog.dialog_text and "未保存" in dialog.dialog_text,"review shows concrete change and unsaved-game consequence")
expect(runtime.create_snapshot()==live and SaveService.load_snapshot(path)[&"snapshot"]==source,"review leaves live and disk unchanged")
if OS.get_environment("THE_TREE_MIGRATION_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/migration-dialog.png"))==OK,"capture migration review UI")
dialog.canceled.emit();dialog.hide()
expect(String(main.get("_dimension_migration_digest")).is_empty(),"cancel clears pending migration token")
menu.get_popup().id_pressed.emit(1)
var changed: Dictionary=source.duplicate(true);changed[&"clock"][&"speed"]=2
SaveService.save_snapshot(path,changed)
dialog.confirmed.emit();dialog.hide()
expect(runtime.create_snapshot()==live and SaveService.load_snapshot(path)[&"snapshot"]==changed,"changed save rejects confirmation without loading or overwriting")
SaveService.save_snapshot(path,source);menu.get_popup().id_pressed.emit(1);dialog.confirmed.emit();dialog.hide()
var found:=false
for room: FacilityInstance in runtime.facility_service.get_instances():
if room.definition_id==&"premium_room": found=room.occupied_cells.size()==4
expect(found and "修正して読み込みました" in (main.get("build_status_label") as Label).text,"confirmation migrates and loads corrected room through UI")
expect(SaveService.load_snapshot(path+SaveService.MIGRATION_BACKUP_SUFFIX)[&"snapshot"]==source,"UI migration retains exact original backup")
menu.get_popup().id_pressed.emit(1)
expect("旧寸法の施設はありません" in (main.get("build_status_label") as Label).text,"already-current save reports no migration needed")
main.queue_free();await process_frame
SaveService.delete_save(path);SaveService.delete_save(path+SaveService.MIGRATION_BACKUP_SUFFIX)
print("Dimension migration UI: %d checks, %d failures" % [checks,failures]);quit(failures)
@@ -0,0 +1 @@
uid://bnt710u2gxgi6
+70 -6
View File
@@ -63,6 +63,20 @@ func _run() -> void:
if facility.definition_id == &"reception":
reception_id = facility.instance_id
_expect(not reception_id.is_empty(), "normal construction created the reception")
var before_manager: Dictionary=_runtime.create_snapshot()
_main.call("_hire_staff_candidate", &"heroine_anri")
var manager_panel := _main.find_child("StaffManagementPanel", true, false) as StaffManagementPanel
manager_panel.refresh();manager_panel.select_staff(&"heroine_anri")
for index: int in manager_panel.facility_option.item_count:
if StringName(manager_panel.facility_option.get_item_metadata(index))==reception_id:
manager_panel.facility_option.select(index)
manager_panel.facility_option.item_selected.emit(index)
manager_panel.apply_button.pressed.emit()
_expect(_runtime.staff_service.get_work_assignment(&"heroine_anri").get(&"facility_id")==reception_id, "normal manager placement established for opening guidance boundary")
_guide.refresh()
_expect(_guide.get_progress().get(&"stage")==&"staff", "manager-only reception keeps actual opening guide on staff recruitment")
_expect(_runtime.get_floor_price_multiplier(0)==12000, "manager passive remains enabled while receptionist guidance is unmet")
_expect(_runtime.restore_snapshot(before_manager), "restore ordinary opening scenario after manager-only boundary")
_main.call("_hire_staff_candidate", &"staff_akane")
var staff_members: Array = _runtime.staff_service.get_staff_members()
_expect(staff_members.size() == 1, "normal hire button employs one receptionist")
@@ -88,12 +102,27 @@ func _run() -> void:
_main.call("_refresh_dashboard")
await _capture("staffed")
# The night is simulated naturally. No service request or guest is inserted.
await _advance(1440 - _clock.absolute_minute)
_expect(int(_ledger.get_report()[&"business_day"]) == 1, "midnight remains part of the first 06:00-based operating day")
_expect(int(_ledger.get_report()[&"construction_cash_out"]) > 0, "building cash outflow remains visible at midnight")
await _advance(1800 - _clock.absolute_minute)
await _advance(1439 - _clock.absolute_minute)
_expect(int(_ledger.get_report()[&"business_day"]) == 1, "23:59 remains part of the first calendar day")
_expect(int(_ledger.get_report()[&"construction_cash_out"]) > 0, "building cash outflow remains visible before midnight")
await _advance(1)
_expect(int(_ledger.get_report()[&"business_day"]) == 2, "midnight rolls the cash-flow period")
_expect(int(_ledger.get_report()[&"construction_cash_out"]) == 0, "prior-day construction is excluded from new-day cash flow")
var summaries: Array = _runtime.operations_coordinator.get_day_summaries()
_expect(summaries.size() == 1, "next 06:00 creates one authoritative operating-day close")
_expect(summaries.size() == 1, "midnight creates one authoritative operating-day close")
var full_operations: Dictionary = _runtime.get_operations_report()
var compact_operations: Dictionary = _runtime.get_operations_dashboard_report()
for key: StringName in [&"enabled", &"arrears_total", &"tick_blocked", &"runtime_failure_latched", &"personal_action_points", &"deferred_runtime_minute"]:
_expect(compact_operations[key] == full_operations[key], "compact HUD matches live %s" % key)
var compact_coordinator: Dictionary = compact_operations[&"coordinator"]
var full_coordinator: Dictionary = full_operations[&"coordinator"]
for key: StringName in [&"terminal_state", &"consecutive_insolvency_count", &"legacy_history_last_day"]:
_expect(compact_coordinator[key] == full_coordinator[key], "compact HUD matches coordinator %s" % key)
_expect(compact_coordinator[&"day_summaries"].back()[&"net_profit"] == summaries.back()[&"net_profit"], "compact HUD preserves latest operating profit")
compact_coordinator[&"day_summaries"].back()[&"net_profit"] = -999999
_expect(_runtime.operations_coordinator.get_day_summaries() == summaries, "editing HUD projection cannot mutate authoritative day close")
_expect(_ledger.is_visible_in_tree(), "daily settlement automatically presents the ledger report")
var lodging_sales := 0
for entry: LedgerEntry in _runtime.economy_service.get_entries():
var facility: FacilityInstance = _runtime.facility_service.get_instance(entry.source_entity_id)
@@ -105,7 +134,7 @@ func _run() -> void:
_expect(int(first[&"category_totals"][&"wages"]) > 0, "the close includes real employee wages")
_expect(bool(first[&"balanced"]), "daily close reconciles the ledger")
_expect(_ledger.get_report()[&"selected_close"] == first, "ledger panel shows the authoritative close without reclassification")
_expect(int(_ledger.get_report()[&"business_day"]) == 2, "cash-flow period rolls at 06:00 rather than midnight")
_expect(int(_ledger.get_report()[&"business_day"]) == 2, "cash-flow period rolls at midnight")
_expect(_guide.get_progress().get(&"stage") == &"complete", "first lodging and following close complete opening guidance")
var digest: String = _runtime.latest_digest
_guide.refresh()
@@ -113,6 +142,8 @@ func _run() -> void:
_expect(_runtime.latest_digest == digest, "guide and ledger refresh never mutate simulation")
_main.call("_activate_management_mode", &"ledger")
await _capture("first-close")
await _advance(1800 - _clock.absolute_minute)
_expect(_runtime.operations_coordinator.get_day_summaries().size() == 1, "06:00 morning cannot duplicate the midnight close")
# A dedicated temp slot avoids every normal player save path.
var save_path := "user://tests/first_operating_day_%d/restore-check" % OS.get_process_id()
var saved: Dictionary = _runtime.save_to_path(save_path, {&"display_name": "First operating day regression"})
@@ -124,6 +155,18 @@ func _run() -> void:
# to String. Compare the JSON-shaped public report, preserving every value.
_expect(JSON.stringify(_ledger.get_report()[&"selected_close"]) == JSON.stringify(close_before), "restored close preserves every financial value")
_expect(_guide.get_progress().get(&"stage") == &"complete", "restored state preserves guide progress: %s" % _guide.get_progress())
# Continue the loaded second morning, then replay the same complete state.
# This checks active work after load, not only a static financial report.
var loaded_checkpoint: Dictionary = _runtime.create_snapshot()
await _advance(120)
var continued_digest := SimulationDigest.compute(_runtime.create_snapshot())
_expect(_clock.absolute_minute == 1920, "disk-loaded inn continues through the second morning")
_expect(_runtime.restore_snapshot(loaded_checkpoint), "second-morning checkpoint restores before replay")
await _advance(120)
_expect(SimulationDigest.compute(_runtime.create_snapshot()) == continued_digest,
"second-morning guests, staff, ledger and events replay exactly after load")
_expect(_guide.get_progress().get(&"stage") == &"complete", "continued second morning preserves completed guidance")
await _capture("second-morning")
var evidence := {"lodging_sales": lodging_sales, "report": _ledger.get_report(), "staff_count": _runtime.staff_service.get_staff_members().size()}
print("FIRST_OPERATING_DAY_REPORT ", JSON.stringify(evidence))
var evidence_path := ProjectSettings.globalize_path("res://artifacts/first-operating-day/operating-report.json")
@@ -132,9 +175,30 @@ func _run() -> void:
if evidence_file != null:
evidence_file.store_string(JSON.stringify(evidence, "\t") + "\n")
SaveService.delete_save(save_path)
_test_new_report_selection()
_finish()
func _test_new_report_selection() -> void:
# Projection-only fixture: test navigation between two closed reports without
# fabricating transactions in the real inn or changing its saved history.
var original: OperationsCoordinator = _runtime.operations_coordinator
var reports := OperationsCoordinator.new()
reports.advance_to(2880)
_runtime.operations_coordinator = reports
_ledger._selected_day = 1
_ledger.refresh()
_expect(int(_ledger.get_report()[&"selected_close"][&"day"]) == 1, "player can inspect an earlier close")
_main.set("_last_presented_close_minute", 1440)
var projection := {&"systems": {&"runtime": {&"operations": {&"coordinator": reports.create_snapshot()}}}}
_main.call("_on_snapshot_published", projection, "")
_expect(int(_ledger.get_report()[&"selected_close"][&"day"]) == 2, "new close selects the latest report even after an earlier day was selected")
_ledger._selected_day = 1
_main.call("_on_snapshot_published", projection, "")
_expect(int(_ledger.get_report()[&"selected_close"][&"day"]) == 1, "ordinary refresh preserves the player's historical selection")
_runtime.operations_coordinator = original
func _build(definition_id: StringName, origin: Vector3i) -> bool:
var before: int = _runtime.construction_service.get_jobs().size()
_main.call("_select_build_definition", definition_id)
+91
View File
@@ -0,0 +1,91 @@
extends SceneTree
const Demo := preload("res://scripts/ui/pixel_tower_demo.gd")
var checks := 0
var failures := 0
func expect(ok: bool, label: String) -> void:
checks += 1
if not ok: failures += 1; push_error(label)
func _initialize() -> void: call_deferred("run")
func run() -> void:
root.gui_embed_subwindows = true
root.size = Vector2i(1280,720)
var runtime: Variant = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1,6,0);state.reset();runtime.initialize(clock,state,424242);clock.set_paused(true)
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
var demo := Demo.new()
expect(await demo.prepare(runtime,clock,self),"prepare normal construction and autonomous visitors")
var guest: GuestRuntime
var record: Dictionary
for minute in 120:
for candidate: GuestRuntime in runtime.guest_service.get_guests():
var found: Dictionary = runtime.get_guest_decision_record(candidate.guest_id)
if found.get(&"action", &"") == &"MOVE_TO_FACILITY" and candidate.target_facility_id == StringName(found.get(&"facility_id", &"")):
guest=candidate;record=found;break
if guest != null: break
clock.advance_one_minute()
expect(guest != null,"autonomous decision produces matching recorded destination")
if guest == null: quit(failures);return
expect(record.has(&"needs") and record.has(&"travel_minutes") and record.has(&"estimated_total_gold"),"decision evidence includes actual scoring inputs")
var id := guest.guest_id
var original := record.duplicate(true)
record[&"needs"].clear()
expect(runtime.get_guest_decision_record(id)==original,"returned log does not expose saved event data")
var saved: Dictionary = runtime.create_snapshot()
expect(runtime.restore_snapshot(saved),"Runtime restores decision event with moving visitors")
expect(runtime.get_guest_decision_record(id)==original,"saved decision inputs survive restoration exactly")
var main: Control = load("res://scenes/main/main.tscn").instantiate();root.add_child(main)
await process_frame
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
main.guest_count_label.pressed.emit();await process_frame
var panel: GuestObservationPanel = main.guest_observer.find_child("GuestObservationPanel", true, false)
panel.selected_guest_id=id;panel.refresh()
expect(panel.decision_label.text.contains("判断時の最大欲求") and panel.decision_label.text.contains("見込料金"),"normal HUD observer exposes actual decision history")
expect(main.guest_observer.size.y <= root.size.y and main.guest_observer.get_ok_button().is_visible_in_tree(), "observer fits viewport with visible close action")
var displayed := panel.decision_label.text
var restored_guest: GuestRuntime = runtime.guest_service.get_guest(id)
restored_guest.needs[&"hunger"]=0;restored_guest.needs[&"sleep"]=100
panel.refresh()
expect(panel.decision_label.text==displayed,"current needs cannot rewrite past reasoning")
expect(panel._decision_text({}).contains("記録なし"),"legacy or expired history is explicitly unavailable")
var before: Dictionary=runtime.create_snapshot()
panel.refresh();panel.refresh()
expect(runtime.create_snapshot()==before,"opening and refreshing history is read-only")
if OS.get_environment("THE_TREE_DECISION_CAPTURE")!="":
await process_frame;await RenderingServer.frame_post_draw
expect(root.get_texture().get_image().save_png(OS.get_environment("THE_TREE_DECISION_CAPTURE"))==OK,"capture rendered decision UI")
expect(not runtime.submit_command(&"decide_guest", {&"guest_id": &"missing"}).is_success(), "failed decision is rejected")
expect(runtime.get_guest_decision_record(&"missing").is_empty(), "failure cannot fabricate a decision record")
expect(runtime.submit_command(&"spawn_guest", {&"guest_id": &"decision_leaver", &"guest_type_id": &"traveler", &"gold": 100, &"options": {&"state": GuestRuntime.State.DECIDING, &"planned_exit_at": clock.absolute_minute + 100}}).is_success(), "spawn controlled boundary visitor")
expect(runtime.submit_command(&"decide_guest", {&"guest_id": &"decision_leaver", &"context": {&"exit_threshold": 2147483647}}).is_success(), "no candidate exceeds departure threshold")
var leaving: Dictionary = runtime.get_guest_decision_record(&"decision_leaver")
expect(leaving.get(&"action", &"") == &"LEAVE" and leaving.get(&"facility_id", &"") == &"", "departure is recorded without a fictitious destination")
expect(panel._decision_text(leaving).contains("退館を選びました"), "departure reason is visible")
expect(runtime.submit_command(&"spawn_guest", {&"guest_id": &"decision_price", &"guest_type_id": &"traveler", &"gold": 100, &"options": {&"state": GuestRuntime.State.DECIDING, &"planned_exit_at": clock.absolute_minute + 100}}).is_success(), "spawn price alias fixture")
expect(runtime.submit_command(&"decide_guest", {&"guest_id": &"decision_price", &"candidates": [{&"facility_id": original[&"facility_id"], &"price_gold": 37, &"expected_quality": 100, &"respect_survival_reserve": false, &"need_relief": {&"sleep": 80}, &"travel_minutes": 5}], &"context": {&"exit_threshold": -10000}}).is_success(), "decision accepts supported price alias")
expect(runtime.get_guest_decision_record(&"decision_price").get(&"estimated_total_gold", -1) == 37, "history preserves effective quoted price rather than default zero")
expect(runtime.submit_command(&"spawn_guest", {&"guest_id": &"decision_rejected", &"guest_type_id": &"traveler", &"gold": 100, &"options": {&"state": GuestRuntime.State.DECIDING, &"planned_exit_at": clock.absolute_minute + 100}}).is_success(), "spawn rejection fixture")
var rejected_candidates: Array = []
for index in 9: rejected_candidates.append({&"facility_id": StringName("closed_%02d" % index), &"is_open": false})
rejected_candidates.append({&"facility_id": &"no_route", &"reachable": false})
rejected_candidates.append({&"facility_id": &"too_expensive", &"price_gold": 10000})
rejected_candidates.append({&"facility_id": &"missing_food", &"inputs_available": false})
expect(runtime.submit_command(&"decide_guest", {&"guest_id": &"decision_rejected", &"candidates": rejected_candidates}).is_success(), "actual filtering rejects twelve candidates")
var rejected: Dictionary = runtime.get_guest_decision_record(&"decision_rejected")
expect(rejected.get(&"rejection_counts", {}) == {"FACILITY_CLOSED": 9, "UNREACHABLE": 1, "CANNOT_PAY": 1, "MISSING_INPUTS": 1}, "reason counts match actual closed unreachable and unaffordable candidates")
expect(rejected.get(&"rejection_count", 0) == 12 and rejected.get(&"rejection_examples", []).size() == 8, "bounded examples retain full total")
panel.selected_guest_id = &"decision_rejected";panel.refresh()
expect(panel.decision_label.text.contains("閉店・稼働停止 9施設") and panel.decision_label.text.contains("経路なし 1施設") and panel.decision_label.text.contains("ほか4施設"), "normal observer renders translated actual causes and explicit truncation")
var event_saved: Dictionary = runtime.domain_events.create_snapshot()
expect(runtime.domain_events.restore_snapshot(event_saved), "saved event stream with rejection evidence restores")
expect(runtime.get_guest_decision_record(&"decision_rejected") == rejected, "all recorded reasons survive event restoration")
expect(panel._rejection_text({}).contains("この記録にはありません"), "old decision records do not fabricate zero rejections")
if OS.get_environment("THE_TREE_REJECTION_CAPTURE") != "":
var scroll: ScrollContainer = main.guest_observer.get_node("GuestObservationScroll")
await process_frame;await process_frame
scroll.scroll_vertical = 250
await process_frame;await RenderingServer.frame_post_draw
expect(root.get_texture().get_image().save_png(OS.get_environment("THE_TREE_REJECTION_CAPTURE")) == OK, "capture scrolled rejection details")
main.queue_free();await process_frame
print("Guest decision log: %d checks, %d failures" % [checks,failures]);quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://cfactoxtr4j7o
+68
View File
@@ -0,0 +1,68 @@
extends SceneTree
var checks := 0
var failures := 0
func _initialize() -> void: call_deferred("run")
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok:
failures += 1
push_error(message)
func run() -> void:
root.gui_embed_subwindows = true
root.size = Vector2i(1280,720)
var runtime: Variant = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
clock.reset(1,6,0)
root.get_node("GameState").reset()
runtime.initialize(clock,root.get_node("GameState"),424242)
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false})
var main: Control = load("res://scenes/main/main.tscn").instantiate()
root.add_child(main)
await process_frame
runtime.configure_save_policy({&"enabled": false})
for initial: GuestRuntime in runtime.guest_service.get_guests():
initial.state = GuestRuntime.State.LEAVING
runtime.guest_service.despawn_guest(initial.guest_id,clock.absolute_minute)
main.guest_count_label.pressed.emit()
await process_frame
var panel: GuestObservationPanel = main.guest_observer.find_child("GuestObservationPanel", true, false)
expect(main.guest_observer.visible and panel.guests.disabled, "HUD opens an empty observer without inventing visitors")
var service: GuestService = runtime.guest_service
var definition: GuestDefinition = service.get_definitions()[0]
var guest := service.spawn_guest(&"observer_guest", definition.guest_type_id, clock.absolute_minute, {&"wallet_gold": 500, &"needs": {&"hunger": 90, &"sleep": 70}, &"planned_exit_at": clock.absolute_minute+1000})
panel.refresh()
expect(panel.selected_guest_id == guest.guest_id and not panel.guests.disabled, "new real visitor appears in the observer")
expect(panel.bars[&"hunger"].value == 90 and "食欲" in panel.priority_label.text, "bars and highest need match actual state")
var before := SimulationDigest.compute(runtime.create_snapshot())
for frame: int in 3: panel.refresh()
expect(before == SimulationDigest.compute(runtime.create_snapshot()), "observing never changes the simulation")
guest.needs[&"sleep"] = 100
panel.refresh()
expect(panel.bars[&"sleep"].value == 100 and "睡眠欲" in panel.priority_label.text, "observer refreshes after needs change")
for state: int in GuestRuntime.State.values():
guest.state = state
panel.refresh()
expect(not panel.details.text.is_empty(), "every real state has a label")
guest.state = GuestRuntime.State.DECIDING
var saved := service.create_snapshot()
service.begin_leaving(guest.guest_id,clock.absolute_minute+1,&"TEST")
service.despawn_guest(guest.guest_id,clock.absolute_minute+2)
panel.refresh()
expect(panel.guests.disabled and panel.selected_guest_id.is_empty(), "despawn removes stale selection")
expect(service.restore_snapshot(saved), "guest snapshot restores")
panel.refresh()
expect(panel.selected_guest_id == &"observer_guest" and panel.bars[&"sleep"].value == 100, "read model follows restored service objects")
main.guest_observer.hide()
main.guest_count_label.pressed.emit()
await process_frame
expect(main.guest_observer.visible and clock.paused, "reopening preserves pause state")
main._refresh_dashboard()
if OS.get_environment("THE_TREE_GUEST_CAPTURE") == "1":
await process_frame
await RenderingServer.frame_post_draw
root.get_texture().get_image().save_png("res://artifacts/guest-ai-2026-09-29/observer.png")
main.queue_free()
await process_frame
print("Guest observer: %d checks, %d failures" % [checks, failures])
quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://5lpeq4dfpkqh
+93
View File
@@ -0,0 +1,93 @@
extends SceneTree
var checks := 0
var failures := 0
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok: failures += 1;push_error(message)
func _initialize() -> void: call_deferred("run")
func run() -> void:
var runtime: Variant=root.get_node("SimulationRuntime")
var clock: Variant=root.get_node("GameClock")
var state: Variant=root.get_node("GameState")
clock.reset(1,6,0);state.reset();runtime.initialize(clock,state,42509);clock.set_paused(true)
var ids := {}
for spec in [[&"reception",Vector3i(-6,0,0)],[&"warehouse",Vector3i(-6,0,1)],[&"staff_waiting_room",Vector3i(-1,0,1)]]:
var result: CommandResult=runtime.submit_command(&"build_facility",{&"definition_id":spec[0],&"origin":spec[1]})
expect(result.is_success(),"build layer fixture %s" % spec[0])
clock.advance_minutes(runtime.definition_registry.get_facility(spec[0]).build_minutes)
for room: Dictionary in runtime.construction_service.get_facilities(): ids[room[&"definition_id"]]=room[&"instance_id"]
var before: Dictionary=runtime.create_snapshot()
root.size=Vector2i(1280,720)
var preview:=WorldPreview.new()
preview.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
preview.set_runtime(runtime)
preview.set_visual_theme(load("res://assets/themes/japanese_fantasy/visual-theme-v1.tres"))
root.add_child(preview);preview.set_process(false)
await process_frame;await process_frame
preview.set_zoom(2)
preview.pan_offset += preview.size*0.5-preview.cell_to_canvas(Vector3i(-3,1,0))
var point:=preview.cell_to_canvas(Vector3i(-5,0,0))
expect(preview._other_layer_rooms().size()==2,"front view includes both visible backyard footprints")
expect(preview.facility_at_canvas(point)==ids[&"reception"],"overlapping backyard ghost does not replace front hit target")
expect(preview.facility_at_canvas(preview.cell_to_canvas(Vector3i(-1,0,0))).is_empty(),"backyard-only ghost cannot be selected from front")
await capture("front")
preview.layer_mode=WorldPreview.LayerMode.BACK
var lens: ColorRect=preview.get_node("LayerLens")
expect(lens.visible and lens.mouse_filter==Control.MOUSE_FILTER_IGNORE,"switch starts noninteractive lens overlay")
expect(preview._other_layer_rooms().size()==1,"back view shows front footprint as wireframe")
expect(preview.facility_at_canvas(point)==ids[&"warehouse"],"switch changes hit target to actual backyard occupant")
expect(preview.cell_to_canvas(Vector3i(-5,0,1))==point,"layer switch preserves zoom and coordinate alignment")
var actor_time: float=preview.get("_animation_time")
preview._process(0.2)
expect(lens.visible and preview.get("_animation_time")==actor_time,"paused game animates lens without animating actors")
await capture("ripple")
preview._process(1.0)
expect(not lens.visible,"transition disables screen-sampling overlay after completion")
await capture("back")
for i in 16:
preview.layer_mode=1-preview.layer_mode
preview._process(0.02)
expect(preview.get_node("LayerLens")==lens,"rapid toggles reuse a single lens instead of accumulating overlays")
preview._process(1)
expect(not lens.visible,"rapid toggles settle into the final layer")
expect(runtime.create_snapshot()==before,"layer visualization and animation leave full simulation snapshot unchanged")
for spec in [[&"premium_room",Vector3i(-6,2,0)],[&"staff_dormitory",Vector3i(-6,2,1)]]:
expect(runtime.submit_command(&"build_facility",{&"definition_id":spec[0],&"origin":spec[1]}).is_success(),"start opposite-layer construction %s" % spec[0])
clock.advance_minutes(5)
var construction_snapshot: Dictionary=runtime.create_snapshot()
preview.layer_mode=WorldPreview.LayerMode.FRONT;preview._process(1)
var back_job := {}
for item: Dictionary in preview._other_layer_rooms():
if item.get(&"building",false): back_job=item
expect(not back_job.is_empty() and back_job[&"definition_id"]==&"staff_dormitory" and back_job[&"progress"]>0,"front view exposes real backyard construction progress")
if back_job.is_empty(): quit(1);return
var back_cells: Array[Vector3i]=runtime.construction_service.get_job(back_job[&"id"]).reserved_cells
expect(back_job[&"rect"]==preview._cell_bounds_rect(back_cells),"ghost construction uses exact reserved geometry")
expect(preview.facility_at_canvas(preview.cell_to_canvas(Vector3i(-6,2,0))).is_empty(),"opposite construction does not become a completed selectable facility")
await capture("front-construction")
preview.layer_mode=WorldPreview.LayerMode.BACK;preview._process(1)
var front_job := {}
for item: Dictionary in preview._other_layer_rooms():
if item.get(&"building",false):front_job=item
expect(not front_job.is_empty() and front_job[&"definition_id"]==&"premium_room","back view exposes front construction")
await capture("back-construction")
expect(runtime.create_snapshot()==construction_snapshot,"construction overlays leave simulation unchanged")
expect(runtime.restore_snapshot(construction_snapshot),"partially built opposite layers restore")
preview.layer_mode=WorldPreview.LayerMode.FRONT;preview._process(1)
expect(preview._other_layer_rooms().any(func(item: Dictionary) -> bool: return item==back_job),"restored construction presentation matches exact prior state")
expect(runtime.submit_command(&"cancel_construction",{&"job_id":back_job[&"id"]}).is_success(),"cancel opposite construction normally")
expect(not preview._other_layer_rooms().any(func(item: Dictionary) -> bool: return item[&"id"]==back_job[&"id"]),"cancelled opposite construction vanishes")
clock.advance_minutes(150)
preview.layer_mode=WorldPreview.LayerMode.BACK;preview._process(1)
var completed_premium:=false
var still_building:=false
for item: Dictionary in preview._other_layer_rooms():
if item[&"definition_id"]==&"premium_room":
completed_premium=true;still_building=bool(item.get(&"building",false))
expect(completed_premium and not still_building,"completion replaces job hatch with finished facility outline")
preview.queue_free();await process_frame
print("Layer presentation: %d checks, %d failures" % [checks,failures]);quit(failures)
func capture(suffix: String) -> void:
if OS.get_environment("THE_TREE_LAYER_CAPTURE")!="1":return
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/layer-%s.png" % suffix))==OK,"capture %s layer presentation" % suffix)
+1
View File
@@ -0,0 +1 @@
uid://7pjvl52ebeqi
+70
View File
@@ -0,0 +1,70 @@
extends SceneTree
var checks := 0
var failures := 0
func _initialize() -> void: call_deferred("run")
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok: failures += 1;push_error(message)
func run() -> void:
var runtime: Variant=root.get_node("SimulationRuntime")
var clock: Variant=root.get_node("GameClock")
var state: Variant=root.get_node("GameState")
clock.reset(1,6,0);state.reset();runtime.initialize(clock,state,42511);clock.set_paused(true)
root.size=Vector2i(1440,810)
var main: Control=load("res://scenes/main/main.tscn").instantiate()
root.add_child(main)
main.call("_set_management_panel_open",true)
await process_frame;await process_frame
var side: PanelContainer=main.find_child("SidePanel",true,false)
var guide: PanelContainer = main.opening_guide
var guide_front: Color = guide.get_theme_stylebox("panel").bg_color
var guide_progress: Dictionary = guide.get_progress()
var front: Color=side.get_theme_stylebox("panel").bg_color
var detail: Label=main.get("selected_detail_label")
var front_text:=detail.get_theme_color("font_color")
var position: Vector2=main.world_preview.cell_to_canvas(Vector3i.ZERO)
await capture("front")
main.back_button.pressed.emit()
await process_frame;await process_frame
expect(state.active_layer==1 and main.world_preview.layer_mode==1,"normal back button switches layer")
expect(side.get_theme_stylebox("panel").bg_color==Color("202e34") and detail.get_theme_color("font_color")==Color("d8eeea"),"back panel and text adopt monitor palette")
var bottom: Control=main.find_child("BottomBar",true,false)
expect(bottom.position.y+bottom.size.y<=main.size.y+1,"back skin preserves toolbar bounds")
expect(main.world_preview.cell_to_canvas(Vector3i(0,0,1))==position,"skin does not change map coordinate mapping")
main.world_preview._process(1.0)
expect(guide.get_theme_stylebox("panel").bg_color == Color("172229"), "opening guide follows BACK skin")
expect(guide.get_progress() == guide_progress, "skin does not change opening instructions or action")
var expanded_height := guide.size.y
guide.get("_heading").pressed.emit()
await process_frame;await process_frame
expect(not guide.get("_body").visible and guide.size.y < expanded_height, "paused guide collapses immediately")
guide.get("_heading").pressed.emit()
await process_frame;await process_frame
expect(guide.get("_body").visible and guide.get("_action").visible, "guide reopens with normal action")
await capture("back")
for i in 4:
main.front_button.pressed.emit();main.back_button.pressed.emit()
main.front_button.pressed.emit()
await process_frame;await process_frame
expect(side.get_theme_stylebox("panel").bg_color==front and detail.get_theme_color("font_color")==front_text,"repeated toggles restore exact front palette")
expect(guide.get_theme_stylebox("panel").bg_color == guide_front, "guide restores FRONT skin")
expect(clock.paused and clock.absolute_minute==360,"skin changes never advance simulation")
main.queue_free();await process_frame
state.active_layer=1
main=load("res://scenes/main/main.tscn").instantiate()
root.add_child(main)
await process_frame;await process_frame
side=main.find_child("SidePanel",true,false)
bottom=main.find_child("BottomBar",true,false)
expect(side.get_theme_stylebox("panel").bg_color==Color("202e34") and main.opening_guide.get_theme_stylebox("panel").bg_color == Color("172229"),"new screen honors existing BACK selection including guide")
expect(bottom.position.y+bottom.size.y<=main.size.y+1,"BACK startup preserves toolbar bounds")
expect(main.rank_dialog.get_label().get_theme_color("font_color") == Color("d8eeea") and main.rank_dialog.theme.get_stylebox("panel", "AcceptDialog").bg_color == Color("202e34"), "rank dialog initializes in saved BACK skin")
expect(main.operating_ledger_panel.get("_cash_label").get_theme_color("font_color") == Color("d8eeea"), "ledger initialized before scene attachment honors saved BACK layer")
main.front_button.pressed.emit()
expect(side.get_theme_stylebox("panel").bg_color==front,"BACK startup still restores FRONT palette")
main.queue_free();await process_frame
print("Layer skin: %d checks, %d failures" % [checks,failures]);quit(failures)
func capture(label: String) -> void:
if DisplayServer.get_name()=="headless":return
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/guide-skin-%s.png" % label))==OK,"capture "+label)
+1
View File
@@ -0,0 +1 @@
uid://lv72d8fmyhdl
+155 -9
View File
@@ -35,6 +35,40 @@ func _run() -> void:
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")
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
@@ -49,7 +83,7 @@ func _run() -> void:
"world preview consumes the scene theme instead of hard-coded asset paths"
)
_expect(
visual_theme.facility_icons.size() == 18
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,
@@ -88,16 +122,36 @@ func _run() -> void:
_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,
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() == 18, "all sixteen MVP facilities and two stairs appear in the build palette")
_expect(palette.get_child_count() == 23, "all23 facilities including the cleaning room appear in the build 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 == 18, "all facility catalog buttons consume theme-provided icons")
_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),
@@ -124,7 +178,7 @@ func _run() -> void:
_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 Label
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(
@@ -187,9 +241,9 @@ func _test_staff_candidate_hiring_ui() -> void:
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() == 20, "all twenty validated Staff candidates have UI cards")
_expect(toggle != null and "20" in toggle.text, "collapsed hiring control exposes the available candidate count")
_expect(buttons.size() == 20 and buttons.has(&"staff_akane"), "every candidate has a stable hire control")
_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
@@ -264,6 +318,21 @@ func _test_excavation_rank_gate_and_tool_exclusion() -> void:
_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:
@@ -274,7 +343,7 @@ func _test_coordinate_round_trip() -> void:
_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(Vector2(-10, -10)) == null, "points outside the map do not produce build cells")
_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:
@@ -417,14 +486,54 @@ func _promote_rank_two() -> bool:
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:
@@ -467,6 +576,43 @@ func _test_event_choice_ui() -> void:
_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:
+67
View File
@@ -0,0 +1,67 @@
extends SceneTree
const Guide = preload("res://scripts/ui/inn_opening_guide.gd")
class Registry extends RefCounted:
var definition := FacilityDefinition.new()
func get_facility(_id): return definition
class Staff extends RefCounted:
var members: Array[StaffRuntime] = []
var shifts: Array[ShiftInstance] = []
var plans := {}
var reads := 0
func get_staff_members(): return members
func get_work_assignment(id): return plans.get(id, {}).duplicate(true)
func get_shifts():
reads += 1
return shifts.duplicate()
class Runtime extends RefCounted:
var definition_registry := Registry.new()
var staff_service := Staff.new()
class Clock extends RefCounted:
var absolute_minute := 60
var checks := 0
var failures := 0
func _initialize(): call_deferred("run")
func expect(value: bool, label: String):
checks += 1
if not value:
failures += 1
push_error(label)
func run():
var runtime := Runtime.new()
var clock := Clock.new()
var reception := FacilityInstance.new()
reception.instance_id = &"desk"
runtime.definition_registry.definition.required_roles = [&"receptionist"]
var guide := Guide.new();guide._runtime=runtime;guide._clock=clock
expect(not guide._has_reception_assignment(reception), "no employees cannot open reception")
var staff := StaffRuntime.new();staff.staff_id=&"worker";staff.role_id=&"manager"
runtime.staff_service.members.append(staff);reception.active_staff_ids.append(staff.staff_id)
expect(not guide._has_reception_assignment(reception), "active manager is not a receptionist")
staff.role_id=&"receptionist"
expect(guide._has_reception_assignment(reception), "active receptionist qualifies")
reception.active_staff_ids.clear()
runtime.staff_service.plans[staff.staff_id]={&"facility_id":&"desk"}
expect(guide._has_reception_assignment(reception), "off-duty repeating assignment qualifies")
var shift := ShiftInstance.new();shift.staff_id=staff.staff_id;shift.end_at=61;shift.assigned_facility_id=&"desk"
runtime.staff_service.shifts.append(shift)
runtime.staff_service.plans[staff.staff_id]={&"facility_id":&"elsewhere"}
expect(not guide._has_reception_assignment(reception), "current reassignment overrides old shifts")
runtime.staff_service.plans.clear()
expect(guide._has_reception_assignment(reception), "legacy future shift qualifies without repeating plan")
shift.end_at=60
expect(not guide._has_reception_assignment(reception), "shift ending now is expired")
shift.end_at=61;shift.cancelled=true
expect(not guide._has_reception_assignment(reception), "cancelled future shift cannot qualify")
shift.cancelled=false;shift.assigned_facility_id=&"elsewhere"
expect(not guide._has_reception_assignment(reception), "other facility shift cannot qualify")
shift.assigned_facility_id=&"desk";shift.staff_id=&"missing"
expect(not guide._has_reception_assignment(reception), "shift for absent employee cannot qualify")
for n in 49:
var extra := StaffRuntime.new();extra.staff_id=StringName("extra%d"%n);extra.role_id=&"receptionist";runtime.staff_service.members.append(extra)
runtime.staff_service.reads=0
expect(not guide._has_reception_assignment(reception), "unassigned team remains incomplete")
expect(runtime.staff_service.reads==1, "historical shifts enumerated once for whole team")
shift.staff_id=&"extra48"
expect(guide._has_reception_assignment(reception), "last employee legacy assignment remains discoverable")
guide.free()
print("Opening assignment: %d checks, %d failures"%[checks,failures]);quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://12qlnwu4ldpw
+166
View File
@@ -0,0 +1,166 @@
extends SceneTree
# Exercise viewport GUI hit testing; do not emit button signals or call map handlers.
var checks := 0
var failures := 0
var main: Control
var runtime: Variant
var clock: Variant
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok:
failures += 1
push_error(message)
func _initialize() -> void: call_deferred("run")
func click_at(point: Vector2) -> void:
var motion := InputEventMouseMotion.new()
motion.position = point
motion.global_position = point
root.push_input(motion, true)
for pressed: bool in [true, false]:
var event := InputEventMouseButton.new()
event.position = point
event.global_position = point
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = pressed
event.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0
root.push_input(event, true)
await process_frame
func run() -> void:
runtime = root.get_node("SimulationRuntime")
clock = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1,6,0);state.reset();runtime.initialize(clock,state,42508);clock.set_paused(true)
root.size = Vector2i(1440,810)
main = load("res://scenes/main/main.tscn").instantiate()
main.set("manual_save_path", "user://tests/build_pointer_%d" % Time.get_ticks_usec())
root.add_child(main)
runtime.configure_save_policy({&"enabled":false, &"daily_checkpoint_enabled":false})
await process_frame;await process_frame
await click_at((main.get("palette_toggle_button") as Button).get_global_rect().get_center())
expect(main.get("palette_scroll").visible, "viewport click opens construction palette")
for item: Dictionary in [
{&"id": &"reception", &"cell": Vector3i(-6,0,0), &"cells":8},
{&"id": &"premium_room", &"cell": Vector3i(-6,2,0), &"cells":4},
{&"id": &"guest_stairs", &"cell": Vector3i(-7,1,0), &"cells":1},
{&"id": &"guest_stairs", &"cell": Vector3i(-7,2,0), &"cells":1},
]:
var button: Button = main.get("build_buttons")[item[&"id"]]
var scroll: ScrollContainer = main.get("palette_scroll")
for attempt in 8:
if scroll.get_global_rect().has_point(button.get_global_rect().get_center()): break
var bar := scroll.get_h_scroll_bar()
await click_at(bar.global_position + Vector2(bar.size.x-3,bar.size.y/2))
# Clicking an already selected toggle would deselect the tool.
if not button.button_pressed: await click_at(button.get_global_rect().get_center())
expect(main.get("_selected_definition_id") == item[&"id"], "viewport click selects %s" % item[&"id"])
var preview: WorldPreview = main.get("world_preview")
var point := preview.global_position + preview.cell_to_canvas(item[&"cell"])
expect(preview.get_global_rect().has_point(point), "placement target is visible")
var before: int = runtime.construction_service.get_jobs().size()
await click_at(point)
expect(runtime.construction_service.get_jobs().size() == before+1, "viewport placement creates exactly one %s job" % item[&"id"])
if runtime.construction_service.get_jobs().size() != before+1:
print("Placement diagnostic: ", main.get("build_status_label").text)
finish();return
if item[&"id"] == &"premium_room":
expect((main.find_child("InnOpeningGuide", true, false) as InnOpeningGuide).get_progress()[&"action"] == &"resume", "guide recognizes premium construction in progress")
clock.advance_minutes(runtime.definition_registry.get_facility(item[&"id"]).build_minutes)
var found := false
for room: Dictionary in runtime.construction_service.get_facilities():
if room[&"definition_id"] == item[&"id"] and room[&"origin"] == item[&"cell"]: found = room[&"occupied_cells"].size() == item[&"cells"]
expect(found, "clicked construction completes with original geometry")
expect((main.find_child("InnOpeningGuide", true, false) as InnOpeningGuide).get_progress()[&"stage"] == &"staff", "premium room advances opening guide to staff instead of requesting a standard room")
await operate_day()
if OS.get_environment("THE_TREE_POINTER_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/original-pointer.png")) == OK, "capture viewport-built room")
finish()
func finish() -> void:
SaveService.delete_save(main.get("manual_save_path"))
main.queue_free()
print("Original build viewport input: %d checks, %d failures" % [checks,failures])
quit(failures)
func press_key(code: Key, target: Window = root) -> void:
for pressed: bool in [true,false]:
var event := InputEventKey.new()
event.keycode = code
event.physical_keycode = code
event.window_id = target.get_window_id()
event.pressed = pressed
Input.parse_input_event(event)
await process_frame
func operate_day() -> void:
await press_key(KEY_ESCAPE)
await press_key(KEY_H)
var hire: Button = main.get("staff_candidate_buttons")[&"staff_akane"]
await click_at(hire.get_global_rect().get_center())
var staff: Array = runtime.staff_service.get_staff_members()
expect(staff.size() == 1, "pointer hires ordinary receptionist from candidates")
if staff.is_empty(): return
await click_at((main.get("staff_management_toggle_button") as Button).get_global_rect().get_center())
var panel: StaffManagementPanel = main.get("staff_management_panel")
await click_at((panel.roster_buttons[staff[0].staff_id] as Button).get_global_rect().get_center())
var reception_id: StringName = &""
var room_id: StringName = &""
for room: FacilityInstance in runtime.facility_service.get_instances():
if room.definition_id == &"reception": reception_id = room.instance_id
if room.definition_id == &"premium_room": room_id = room.instance_id
var option_index := -1
for i in panel.facility_option.item_count:
if StringName(panel.facility_option.get_item_metadata(i)) == reception_id: option_index = i
expect(option_index >= 0, "reception appears in assignment options")
if option_index < 0: return
await click_at(panel.facility_option.get_global_rect().get_center())
var popup := panel.facility_option.get_popup()
await press_key(KEY_HOME, popup)
for attempt in panel.facility_option.item_count + 1:
if popup.get_focused_item() == option_index: break
await press_key(KEY_DOWN, popup)
await press_key(KEY_ENTER, popup)
await click_at(panel.apply_button.get_global_rect().get_center())
clock.advance_minutes(2)
expect(runtime.staff_service.get_staff(staff[0].staff_id).assigned_facility_id == reception_id, "pointer/dropdown keyboard assigns receptionist")
print("ASSIGNMENT_UI selected=",panel.facility_option.selected," expected=",option_index," result=",panel.result_label.text," popup=",popup.visible)
if runtime.staff_service.get_staff(staff[0].staff_id).assigned_facility_id != reception_id: return
await press_key(KEY_ESCAPE)
expect(runtime.get_facility_access_report(room_id)[&"reachable"], "clicked stairs connect the premium room")
while clock.absolute_minute < 1440:
var before: int = clock.absolute_minute
clock.advance_minutes(mini(30,1440-clock.absolute_minute))
if clock.absolute_minute == before:
expect(false, "ordinary clock did not advance: %s" % runtime.get_operations_report());return
if clock.absolute_minute % 120 == 52: print("PREMIUM_DAY minute=",clock.absolute_minute)
await process_frame
var sales := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.category == &"SERVICE_SALE" and entry.source_entity_id == room_id: sales += entry.amount
expect(sales > 0, "natural arrivals complete paid premium lodging before first midnight")
expect(runtime.operations_coordinator.get_day_summaries().size() == 1 and runtime.economy_service.validate_ledger()[&"ok"], "natural premium day closes with balanced ledger")
expect((main.find_child("InnOpeningGuide", true, false) as InnOpeningGuide).get_progress()[&"stage"] == &"complete", "premium revenue and midnight close complete the opening guide")
print("PREMIUM_POINTER_DAY sales=", sales, " minute=", clock.absolute_minute)
# Close the daily ledger before clicking the ordinary save control.
await press_key(KEY_ESCAPE)
for button: Node in main.find_children("*", "Button", true, false):
if button.text == "保存":
await click_at(button.get_global_rect().get_center())
break
var saved: Dictionary = SaveService.load_snapshot(main.get("manual_save_path"))
expect(saved[&"ok"], "pointer saves normal premium-day progress")
if saved[&"ok"]:
clock.advance_minutes(2)
await click_at((main.find_child("SaveLoadMenu",true,false) as MenuButton).get_global_rect().get_center())
var load_popup := (main.find_child("SaveLoadMenu",true,false) as MenuButton).get_popup()
await press_key(KEY_HOME,load_popup)
await press_key(KEY_ENTER,load_popup)
expect(clock.absolute_minute == 1440, "load-menu input restores saved day boundary")
expect((main.find_child("InnOpeningGuide", true, false) as InnOpeningGuide).get_progress()[&"stage"] == &"complete", "load preserves completed premium opening guidance")
var loaded: Dictionary = runtime.create_snapshot()
for section: StringName in [&"facilities", &"economy", &"guests", &"staff", &"construction"]:
expect(loaded[&"systems"][&"runtime"][section] == saved[&"snapshot"][&"systems"][&"runtime"][section], "load-menu input restores %s state" % section)
clock.advance_minutes(30)
var continued: Dictionary = runtime.create_snapshot()
expect(runtime.restore_snapshot(loaded), "loaded next-day state can be replayed")
clock.advance_minutes(30)
expect(runtime.create_snapshot() == continued, "loaded inn continues thirty minutes with deterministic guest/staff/economy replay")
@@ -0,0 +1 @@
uid://bq3jlsfqgcw4e
@@ -0,0 +1,73 @@
extends SceneTree
var checks := 0
var failures := 0
func _initialize() -> void:
call_deferred("_run")
func expect(value: bool, label: String) -> void:
checks += 1
if not value:
failures += 1
push_error(label)
func _run() -> void:
var clock: Variant = root.get_node("GameClock")
var runtime: Variant = root.get_node("SimulationRuntime")
var state: Variant = root.get_node("GameState")
clock.reset()
state.reset()
runtime.initialize(clock, state, 927)
clock.set_paused(true)
var legacy_snapshot: Dictionary = runtime.create_snapshot()
legacy_snapshot[&"clock"][&"speed"] = 4
expect(runtime.restore_snapshot(legacy_snapshot) and clock.speed == 3, "full Runtime migrates legacy x4 saves")
var main: Control = load("res://scenes/main/main.tscn").instantiate()
main.set("manual_save_path", "user://tests/original_controls_%d" % OS.get_process_id())
root.add_child(main)
await process_frame
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
for id: StringName in [&"a", &"b"]:
runtime.facility_service.create_instance({&"instance_id": id, &"definition_id": &"standard_room"})
main.set("_selected_entity_id", &"a")
main.set("_selected_entity_kind", &"facility")
main.call("_refresh_selected_entity")
var picker := main.get("related_facility_picker") as OptionButton
expect(picker.visible and picker.item_count == 2, "inspector exposes one related facility")
picker.item_selected.emit(1)
expect(runtime.facility_service.get_instance(&"a").related_facility_id == &"b", "UI submits real paused command")
runtime.facility_service.set_operational_status(&"b", {&"power_available": false})
main.call("_refresh_selected_entity")
expect("未成立" in (main.get("selected_detail_label") as Label).text, "inspector displays unavailable dependency")
expect("未設定" in (main.get("selected_detail_label") as Label).text, "unknown customer capacity is not fabricated")
var buttons: Dictionary = main.get("speed_buttons")
expect(buttons.has(3) and not buttons.has(4), "original maximum speed has a real UI button")
(buttons[3] as Button).pressed.emit()
expect(clock.speed == 3, "x3 button controls clock")
clock.set_paused(true)
# Test the transactional Authority closure boundary with actual discovered evidence.
var evidence := AuthorityEvidence.new().configure(&"closure", &"audit", 60, {&"facility_id": &"a", &"created_at": 360})
expect(runtime.authority_service.register_evidence(evidence).is_success(), "closure evidence registered")
expect(runtime.authority_service.attempt_discover_evidence(&"closure", &"inspector", Vector3i.ZERO, 360).is_success(), "inspection discovers reached evidence")
var payload := {&"resolution_id": &"test", &"inspection_id": &"inspection", &"absolute_minute": 360, &"fine_amount": 0, &"result": &"TEMPORARY_CLOSURE", &"evidence_ids": ["closure"]}
var before: Dictionary = runtime.facility_service.create_snapshot()
var events_before: Dictionary = runtime.domain_events.create_snapshot()
var ids_before: Dictionary = runtime.id_generator.create_snapshot()
var validation: Dictionary = runtime.call("_apply_authority_resolution_boundary", payload, &"validate", null)
expect(validation[&"ok"], "zero-fine closure validates")
var applied: Dictionary = runtime.call("_apply_authority_resolution_boundary", payload, &"apply", validation[&"token"])
expect(applied[&"ok"] and runtime.facility_service.get_instance(&"a").operation_state == FacilityInstance.OperationState.SUSPENDED, "zero-fine inspection still suspends target")
expect(runtime.facility_service.get_instance(&"b").suspension_reason.is_empty(), "unrelated facility is not suspended")
var rolled_back: Dictionary = runtime.call("_apply_authority_resolution_boundary", payload, &"rollback", validation[&"token"])
expect(rolled_back[&"ok"] and runtime.facility_service.create_snapshot() == before, "rollback restores facility and financial state")
expect(runtime.domain_events.create_snapshot() == events_before and runtime.id_generator.create_snapshot() == ids_before, "rollback restores emitted events and stable IDs")
if DisplayServer.get_name() != "headless":
root.size = Vector2i(1440, 900)
main.call("_set_management_panel_open", true)
main.call("_refresh_selected_entity")
for frame in 6:
await process_frame
await RenderingServer.frame_post_draw
var capture_path := "res://artifacts/original-2026-09-27/facility-controls.png"
expect(root.get_texture().get_image().save_png(capture_path) == OK, "real inspector capture saved")
main.queue_free()
await process_frame
print("%s: %d checks" % ["PASS" if failures == 0 else "FAIL", checks])
quit(failures)
@@ -0,0 +1 @@
uid://cg5t3a3ly3213
+20
View File
@@ -234,6 +234,7 @@ func _test_animation_pause_and_simulation_isolation() -> void:
_preview.call("_gui_input", press)
var drag := InputEventMouseMotion.new()
drag.relative = Vector2(43, 27)
drag.button_mask = MOUSE_BUTTON_MASK_MIDDLE
_preview.call("_gui_input", drag)
press.pressed = false
_preview.call("_gui_input", press)
@@ -242,6 +243,25 @@ func _test_animation_pause_and_simulation_isolation() -> void:
"middle-button drag translates the visible room by the pointer movement")
_click(moved_point)
_expect(not _last_facility.is_empty(), "a room remains selectable immediately after dragging")
_preview.set_pan_mode(true)
_click(moved_point)
_expect(_last_facility.is_empty() and _last_cell == null,
"left drag in pan mode does not select or issue a placement click")
drag.button_mask = MOUSE_BUTTON_MASK_LEFT
_preview.call("_gui_input", drag)
_expect(_preview.cell_to_canvas(cell).distance_to(moved_point + drag.relative) <= 2.0,
"explicit pan mode moves the room with the left mouse button")
var pan_before := _preview.pan_offset
drag.button_mask = 0
_preview.call("_gui_input", drag)
_expect(_preview.pan_offset == pan_before and not bool(_preview.get("_dragging")),
"returning after releasing outside the viewport does not leave a stuck drag")
_click(_preview.cell_to_canvas(cell))
_preview.notification(Control.NOTIFICATION_APPLICATION_FOCUS_OUT)
_expect(not bool(_preview.get("_dragging")), "application focus loss cancels the camera drag")
_preview.set_pan_mode(false)
_click(_preview.cell_to_canvas(cell))
_expect(not _last_facility.is_empty(), "leaving pan mode restores normal facility selection")
_preview.reset_view()
_expect(int(_clock.absolute_minute) == before_minute,
"animation frames and camera input do not advance simulation minutes")
+91
View File
@@ -0,0 +1,91 @@
extends SceneTree
# Viewport input routing, including the embedded modal; not OS input automation.
var checks := 0
var failures := 0
func _initialize() -> void: call_deferred("run")
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok:
failures += 1
push_error(message)
func click(control: Control) -> void:
var viewport := control.get_viewport()
var point := control.get_global_rect().get_center()
if viewport is Window and viewport != root and viewport.is_embedded():
point += Vector2(viewport.position)
viewport = root
var motion := InputEventMouseMotion.new()
motion.position = point
motion.global_position = point
viewport.push_input(motion, true)
for pressed: bool in [true, false]:
var event := InputEventMouseButton.new()
event.position = point
event.global_position = point
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = pressed
event.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0
viewport.push_input(event, true)
await process_frame
func key(code: Key, window: Window) -> void:
for pressed: bool in [true, false]:
var event := InputEventKey.new()
event.keycode = code
event.physical_keycode = code
event.window_id = window.get_window_id()
event.pressed = pressed
Input.parse_input_event(event)
await process_frame
func run() -> void:
var runtime: Variant = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1,6,0); state.reset(); runtime.initialize(clock,state,42511); clock.set_paused(true)
root.size = Vector2i(1280,720)
var main: Control = load("res://scenes/main/main.tscn").instantiate()
var path := "user://tests/rank_pointer_%d" % Time.get_ticks_usec()
main.set("manual_save_path",path)
root.add_child(main)
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
await process_frame; await process_frame
var button: Button = main.get("rank_button")
var dialog: ConfirmationDialog = main.get("rank_dialog")
print("RANK_RECT ",button.get_global_rect()," root ",root.size)
await click(button)
print("HOVER ",root.gui_get_hovered_control())
expect(dialog.visible,"pointer opens rank conditions")
await click(dialog.get_ok_button())
expect(dialog.visible and runtime.progression_service.current_rank == 1,"disabled promotion cannot activate by pointer")
await key(KEY_ESCAPE,dialog)
expect(not dialog.visible,"Escape closes rank modal")
# Controlled fixture isolates interaction from natural campaign progression.
expect(runtime.submit_command(&"update_progression_metrics",{&"metrics":{&"reputation":25}}).is_success(),"prepare reputation boundary")
expect(runtime.submit_command(&"change_resources",{&"deltas":{&"gold":5000}}).is_success(),"prepare money boundary")
await click(button)
await click(dialog.get_cancel_button())
expect(not dialog.visible and runtime.progression_service.current_rank == 1,"cancel does not promote eligible game")
await click(button)
await click(dialog.get_ok_button())
expect(not dialog.visible and runtime.progression_service.current_rank == 2,"pointer confirmation promotes and closes modal")
expect(button.text == "世界樹ランク 2","promotion updates visible HUD")
for node: Node in main.find_children("*","Button",true,false):
if node.text == "保存":
await click(node)
break
expect(SaveService.load_snapshot(path)[&"ok"],"pointer saves promoted game")
clock.advance_one_minute()
var menu: MenuButton = main.find_child("SaveLoadMenu",true,false)
await click(menu)
await key(KEY_HOME,menu.get_popup())
await key(KEY_ENTER,menu.get_popup())
expect(clock.absolute_minute == 360 and runtime.progression_service.current_rank == 2,"load menu keyboard restores saved promoted game")
await click(button)
expect(dialog.visible and "ランク3への条件" in dialog.dialog_text,"restored rank opens next requirements")
await key(KEY_ESCAPE,dialog)
if OS.get_environment("THE_TREE_RANK_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/rank-pointer.png")) == OK,"capture promoted HUD")
SaveService.delete_save(path)
main.queue_free(); await process_frame
print("Rank viewport input: %d checks, %d failures" % [checks,failures])
quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://cpy8opqu4mjhu
+153
View File
@@ -0,0 +1,153 @@
extends SceneTree
const Presentation := preload("res://scripts/ui/room_presentation.gd")
var failures := 0
var checks := 0
class Store extends RefCounted:
var rooms: Array = []
var requests: Dictionary = {}
func get_instances() -> Array: return rooms
func get_instance(id: StringName) -> Variant:
for room: Variant in rooms:
if room.instance_id == id: return room
return null
func get_request(id: StringName) -> Variant: return requests.get(id)
class Guests extends RefCounted:
var people: Dictionary = {}
func get_guest(id: StringName) -> Variant: return people.get(id)
class Fixture extends RefCounted:
var facility_service := Store.new()
var guest_service := Guests.new()
var world_service := WorldService.new()
var elevators := ElevatorService.new()
func _initialize() -> void:
call_deferred("run")
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok:
failures += 1
push_error(message)
func run() -> void:
root.get_node("GameClock").set_paused(true)
var room := FacilityInstance.new().configure(&"room", &"cafeteria", {&"capacity": 99, &"customer_capacity": 6})
var service := ServiceInstance.new()
service.guest_ids.assign([&"f", &"a", &"c", &"e", &"b", &"d"])
room.active_services[&"service"] = service
var data := Presentation.describe(room)
expect(data[&"count"] == 6 and data[&"limit"] == 6, "people capacity is independent of storage/service capacity 99")
expect(data[&"people"][0] == &"a" and "満員" in Presentation.label(data, 3) and "他3人" in Presentation.label(data, 3), "stable slots and honest overflow count")
var before := room.create_snapshot()
for id: StringName in [&"standard_room", &"cafeteria", &"reception"]:
for extent: Vector2 in [Vector2(16,24), Vector2(32,48), Vector2(96,48)]:
var rect := Rect2(Vector2(4,8), extent)
var points := Presentation.slots(rect, 1, id)
for i: int in points.size():
var body := Rect2(points[i]-Vector2(8,18), Vector2(16,18))
expect(rect.encloses(body), "person stays inside narrow/large room")
for j: int in range(i):
expect(not body.intersects(Rect2(points[j]-Vector2(8,18), Vector2(16,18))), "no overlapping or repeated use slots")
for scale: float in [0.5,1.0,2.0,4.0]:
var chapel_rect := Rect2(Vector2(16,32),Vector2(64,72)*scale)
var chapel_slots := Presentation.slots(chapel_rect,scale,&"chapel")
expect(chapel_slots.size()==8,"chapel reserves exactly eight visible visitor positions")
for i in chapel_slots.size():
var body := Rect2(chapel_slots[i]-Vector2(8,18)*scale,Vector2(16,18)*scale)
expect(chapel_rect.encloses(body) and body.position.y>=chapel_rect.position.y+30*scale,"chapel visitors remain inside room and below altar")
for j in range(i):
expect(not body.intersects(Rect2(chapel_slots[j]-Vector2(8,18)*scale,Vector2(16,18)*scale)),"chapel visitor bodies do not overlap at zoom levels")
var rotated_chapel := Rect2(Vector2.ZERO,Vector2(48,96))
var rotated_slots := Presentation.slots(rotated_chapel,1,&"chapel")
expect(rotated_slots.size()==8,"rotated chapel still accommodates eight visible visitors")
for i in rotated_slots.size():
var body := Rect2(rotated_slots[i]-Vector2(8,18),Vector2(16,18))
expect(rotated_chapel.encloses(body) and body.position.y>=30,"rotated visitor stays below altar and inside room")
for j in range(i):
expect(not body.intersects(Rect2(rotated_slots[j]-Vector2(8,18),Vector2(16,18))),"rotated chapel visitors do not overlap")
expect(room.create_snapshot() == before, "presentation leaves authoritative room data untouched")
room.set_operational_conditions({&"operator_open": false}, 0)
expect(Presentation.label(Presentation.describe(room), 3) == "未稼働", "closed room is inactive")
room.set_operational_conditions({&"suspension_reason": &"RAIDED"}, 0)
room.set_operational_conditions({&"operator_open": true}, 0)
expect(Presentation.label(Presentation.describe(room), 3) == "休止・摘発", "reopening cannot clear raid suspension")
var fixture := Fixture.new()
fixture.facility_service.rooms = [room]
fixture.world_service.reset({&"min_x": -3, &"max_x": 3, &"min_y": 0, &"max_y": 3})
for x: int in range(-3, 4): fixture.world_service.set_walkable(Vector3i(x,0,0), true)
for i: int in 9:
var id := StringName("guest_%02d" % i)
var guest := GuestRuntime.new()
guest.guest_id = id
guest.current_cell = Vector3i.ZERO
guest.state = GuestRuntime.State.QUEUING
fixture.guest_service.people[id] = guest
var request := ServiceRequest.new()
request.guest_ids.assign([id])
fixture.facility_service.requests[id] = request
room.queue_entries.append(id)
var preview := WorldPreview.new()
preview.size = Vector2(1100,600)
preview.set_runtime(fixture)
preview._prepare_room_presentations()
preview._prepare_queues()
var positions: Dictionary = preview._queue_positions.duplicate()
expect(positions.size() == 7 and preview._queue_hidden.size() == 2, "bounded walkable lane uses seven distinct positions and summarizes overflow")
var distinct: Dictionary = {}
for at: Vector2 in positions.values(): distinct[at] = true
expect(distinct.size() == positions.size(), "waiting people never repeat the same visible position")
preview._prepare_queues()
expect(positions == preview._queue_positions, "repeated frame preserves queue order and positions")
for i: int in 9:
fixture.guest_service.people[StringName("guest_%02d" % i)].current_cell = Vector3i(-1 if i < 4 else 1,0,0)
preview._prepare_queues()
distinct.clear()
for at: Vector2 in preview._queue_positions.values(): distinct[at] = true
expect(distinct.size() == preview._queue_positions.size(), "neighboring entrances share a corridor without overlapping their queues")
expect(preview._queue_positions.size()+preview._queue_hidden.size() == 9, "neighboring queues retain every actual waiting guest")
for guest: Variant in fixture.guest_service.people.values(): guest.current_cell = Vector3i.ZERO
preview.layer_mode = WorldPreview.LayerMode.BACK
preview._prepare_queues()
expect(preview._queue_positions.is_empty() and preview._queue_hidden.is_empty(), "layer switch removes front queues")
preview.layer_mode = WorldPreview.LayerMode.FRONT
room.queue_entries.clear()
var path := PathService.new()
var walkable: Array[Vector3i] = fixture.world_service.get_walkable_cells()
walkable.append(Vector3i(0,1,0))
path.rebuild_graph(walkable)
fixture.elevators.configure(path)
var config := {&"shaft_id": &"transport.shaft.0.0.0.2", &"module_ids": [&"module"], &"stops": [Vector3i.ZERO,Vector3i(0,1,0)], &"width": 2, &"capacity": 4, &"travel_minutes_per_floor": 10, &"access_id": &""}
expect(fixture.elevators.synchronize([config],0)[&"ok"], "real elevator fixture initializes")
for i: int in 9:
expect(fixture.elevators.request_trip(StringName("guest/guest_%02d" % i),Vector3i.ZERO,Vector3i(0,1,0),0)[&"ok"], "real trip enters landing queue")
preview._prepare_queues()
expect(preview._queue_positions.size()+preview._queue_hidden.size() == 9, "landing represents every queued rider")
expect(not preview._queue_positions.values().has(preview.cell_to_canvas(Vector3i.ZERO)), "waiting people stay outside the car footprint")
fixture.elevators.advance_one_minute(1)
preview._prepare_queues()
var report: Dictionary = fixture.elevators.get_report()
if report[&"shafts"].is_empty():
preview.free()
quit(1)
return
var car: Dictionary = report[&"shafts"][0]
expect(car[&"boarded"].size() == 4, "real elevator boards up to its capacity")
for actor: StringName in car[&"boarded"]:
expect(not preview._queue_positions.has(String(actor)) and not preview._queue_hidden.has(String(actor)), "boarded person is absent from landing display")
var snapshot := fixture.elevators.create_snapshot()
var queue_before: Dictionary = preview._queue_positions.duplicate()
expect(fixture.elevators.restore_snapshot(snapshot), "elevator snapshot restores")
preview._prepare_queues()
expect(queue_before == preview._queue_positions, "restored elevator preserves visible landing positions")
var initial_zoom := preview.zoom_level
preview.focus_operating_area()
expect(preview.zoom_level > initial_zoom and preview.canvas_to_cell(preview.cell_to_canvas(Vector3i.ZERO)) == Vector3i.ZERO, "initial framing enlarges the opening area without changing hit testing")
preview.reset_view()
expect(preview.zoom_level == 1.0 and preview.pan_offset == Vector2.ZERO, "Home overview remains available after opening focus")
preview.free()
print("PASS: %d room presentation checks; failures=%d" % [checks, failures])
quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://6bjsv1fuy0ol
+50
View File
@@ -0,0 +1,50 @@
extends "res://tests/ui/test_first_operating_day.gd"
var _testing_autosave := false
func _finish() -> void:
if _failures > 0 or _testing_autosave:
super._finish()
return
_testing_autosave = true
var path := "user://tests/selection_autosave_%d/slot" % OS.get_process_id()
var queued_path := path + "_queued"
SaveService.delete_save(path)
SaveService.delete_save(queued_path)
var facilities: Array = _runtime.facility_service.get_instances()
_expect(facilities.size() >= 2, "real opening provides two selection targets")
if facilities.size() < 2:
super._finish()
return
var first_id: StringName = facilities[0].instance_id
var second_id: StringName = facilities[1].instance_id
_main.call("_on_facility_selected", first_id)
_runtime.configure_save_policy({&"enabled": true, &"daily_checkpoint_enabled": true, &"autosave_path": path, &"autosave_generations": 3})
await _advance(3240 - _clock.absolute_minute)
var generation := SaveService.load_snapshot(SaveService.get_autosave_path(path, 0))
_expect(generation.get(&"ok", false), "06:00 checkpoint writes an actual autosave package")
_expect(generation.get(&"manifest", {}).get("interface_selection", {}).get("id", "") == String(first_id), "daily autosave captures the selected facility")
_main.call("_on_facility_selected", second_id)
var loaded: Dictionary = _runtime.load_recovery_candidate(path, &"autosave", 0)
_expect(loaded.get(&"ok", false), "explicit autosave candidate restores successfully")
_expect(_main.get("_selected_entity_id") == first_id, "direct autosave load restores selection through the common notification")
_expect(_main.get("world_preview").selected_entity_id == first_id, "direct load restores map highlight")
if OS.get_environment("THE_TREE_AUTOSAVE_CAPTURE") != "":
for frame: int in 5:
await process_frame
await RenderingServer.frame_post_draw
var screenshot := root.get_texture().get_image()
_expect(screenshot != null and screenshot.save_png(OS.get_environment("THE_TREE_AUTOSAVE_CAPTURE")) == OK, "capture actual autosave recovery selection")
_expect(_runtime.queue_manual_checkpoint(queued_path).get(&"ok", false), "queue a checkpoint while first target is selected")
_main.call("_on_facility_selected", second_id)
await _advance(1)
var queued := SaveService.load_snapshot(queued_path)
_expect(queued.get(&"manifest", {}).get("interface_selection", {}).get("id", "") == String(second_id), "queued checkpoint uses selection at actual save time")
_main.call("_on_facility_selected", first_id)
_expect(_runtime.load_from_path(queued_path).get(&"ok", false), "direct strict load succeeds")
_expect(_main.get("_selected_entity_id") == second_id, "direct strict load restores its own selection")
_expect(not _runtime.load_from_path(path + "_missing").get(&"ok", true), "missing direct load fails")
_expect(_main.get("_selected_entity_id") == second_id, "failed direct load preserves selection")
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
SaveService.delete_save(path)
SaveService.delete_save(queued_path)
super._finish()
+1
View File
@@ -0,0 +1 @@
uid://bcv2y1ne4ik7b
+150
View File
@@ -0,0 +1,150 @@
extends SceneTree
var _checks := 0
var _failures := 0
var _runtime: Variant
var _clock: Variant
var _state: Variant
var _main: Control
var _path := "user://tests/selection_lifecycle_%d/slot" % OS.get_process_id()
func _initialize() -> void:
call_deferred("_run")
func _expect(ok: bool, message: String) -> void:
_checks += 1
if not ok:
_failures += 1
push_error(message)
func _mount() -> void:
_main = load("res://scenes/main/main.tscn").instantiate()
_main.set("manual_save_path", _path)
root.add_child(_main)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
await process_frame
await process_frame
func _build(id: StringName, cell: Vector3i) -> StringName:
_main.call("_set_layer", cell.z)
_main.call("_select_build_definition", id)
var before: int = _runtime.construction_service.get_jobs().size()
_main.call("_on_map_cell_clicked", cell)
var jobs: Array = _runtime.construction_service.get_jobs()
_expect(jobs.size() == before + 1, "normal palette creates %s construction" % id)
return jobs.back().job_id if jobs.size() == before + 1 else &""
func _round_trip(kind: StringName, id: StringName, heading: String) -> void:
_main.call("_save_manual")
var package := SaveService.load_snapshot(_path)
_expect(package.get(&"ok", false), "%s selection has a valid package" % kind)
_main.set("_selected_entity_id", &"")
_main.set("_selected_entity_kind", &"")
_main.call("_load_manual", true)
_expect(_main.get("_selected_entity_id") == id and _main.get("_selected_entity_kind") == kind, "%s selection restores its actual target" % kind)
_expect(heading in _main.get("selected_heading_label").text, "%s inspector restores the target details" % kind)
var expected_highlight := &"" if kind == &"excavation" else id
_expect(_main.get("world_preview").selected_entity_id == expected_highlight, "%s map selection uses the appropriate highlight" % kind)
func _run() -> void:
_runtime = root.get_node("SimulationRuntime")
_clock = root.get_node("GameClock")
_state = root.get_node("GameState")
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, 42530)
_clock.set_paused(true)
root.size = Vector2i(1440, 810)
await _mount()
var reception_job := _build(&"reception", Vector3i(-8, 0, 0))
if reception_job.is_empty():
_finish()
return
_main.call("_clear_build_selection")
_main.call("_on_construction_selected", reception_job)
_round_trip(&"construction", reception_job, "工事")
_clock.advance_minutes(120)
var reception_id: StringName = _runtime.construction_service.get_facilities()[0][&"instance_id"]
_main.call("_on_facility_selected", reception_id)
_round_trip(&"facility", reception_id, "受付")
var stair_job := _build(&"guest_stairs", Vector3i(0, 1, 0))
_clock.advance_minutes(60)
_main.call("_clear_build_selection")
var stair_id := &""
for row: Dictionary in _runtime.construction_service.get_facilities():
if row[&"definition_id"] == &"guest_stairs":
stair_id = row[&"instance_id"]
_expect(not stair_job.is_empty() and not stair_id.is_empty(), "paid stair construction finishes without a FacilityService instance")
if not stair_id.is_empty():
_main.call("_on_facility_selected", stair_id)
_round_trip(&"facility", stair_id, "客用階段")
# Rank is an explicit boundary fixture; this suite does not claim natural rank progression.
_runtime.submit_command(&"update_progression_metrics", {&"metrics": {&"reputation": 25}}, &"player", &"", 100, -1, &"selection.rank.reputation")
_runtime.submit_command(&"change_resources", {&"deltas": {&"gold": 10000}}, &"player", &"", 100, -1, &"selection.rank.capital")
_expect(_runtime.submit_command(&"set_world_tree_rank", {&"rank": 2}, &"player", &"", 100, -1, &"selection.rank.promote").is_success(), "boundary fixture unlocks excavation")
_main.call("_set_excavation_mode", true)
_main.call("_on_map_cell_clicked", Vector3i(0, -1, 0))
var excavations: Array = _runtime.excavation_service.get_jobs()
_expect(not excavations.is_empty(), "normal excavation UI creates a saved job")
if not excavations.is_empty():
_round_trip(&"excavation", excavations.back()[&"job_id"], "地下掘削")
_main.call("_on_facility_selected", reception_id)
_expect(_runtime.save_to_path(_path).get(&"ok", false), "save selection through Runtime without explicit metadata")
_main.queue_free()
await process_frame
_expect(_runtime.get_signal_connection_list(&"checkpoint_presentation_requested").is_empty(), "destroyed UI leaves no presentation callback")
_expect(_runtime.save_to_path(_path + "_headless").get(&"ok", false), "Runtime still saves with no main scene")
var headless := SaveService.load_snapshot(_path + "_headless")
_expect(not headless.get(&"manifest", {}).has("interface_selection"), "UI-free save has no stale presentation")
await _mount()
_expect(_runtime.get_signal_connection_list(&"checkpoint_presentation_requested").size() == 1, "new main scene has exactly one presentation callback")
_expect(_runtime.load_from_path(_path).get(&"ok", false) and _main.get("_selected_entity_id") == reception_id, "new main scene receives direct-load selection")
# Rotation must preserve each candidate's own selection, rather than the newest UI state.
for selected: StringName in [reception_id, stair_id]:
_main.call("_on_facility_selected", selected)
_expect(_runtime.queue_checkpoint(_path + "_generations", {}, true).get(&"ok", false), "queue an autosave generation")
_clock.advance_minutes(1)
_expect(_runtime.load_recovery_candidate(_path + "_generations", &"autosave", 1).get(&"ok", false) and _main.get("_selected_entity_id") == reception_id, "older generation restores its reception selection")
_expect(_runtime.load_recovery_candidate(_path + "_generations", &"autosave", 0).get(&"ok", false) and _main.get("_selected_entity_id") == stair_id, "newest generation restores its staircase selection")
await _test_legacy()
_finish()
func _test_legacy() -> void:
# Materialize a genuine pre-policy world connection through the normal migration contract.
_clock.reset(1, 6, 0)
_state.reset()
_runtime.initialize(_clock, _state, 42531)
_clock.set_paused(true)
_runtime.world_service.set_walkable(Vector3i(0, 1, 0), true)
_expect(_runtime.rebuild_path_graph_from_world(), "old-world boundary fixture has two adjacent floors")
var old: Dictionary = _runtime.create_snapshot()
var data: Dictionary = old[&"systems"][&"runtime"]
data[&"runtime_schema_version"] = 7
data.erase(&"elevators")
data.erase(&"legacy_stairs")
preload("res://tests/fixtures/pre_upkeep_runtime.gd").strip_empty_upkeep(data)
data[&"path"][&"schema_version"] = 3
for field: StringName in [&"vertical_walk_blockers", &"vertical_walk_requires_connector", &"transit_actor_ids"]:
data[&"path"].erase(field)
_expect(_runtime.restore_snapshot(old), "genuine old world migrates into owned legacy stairs")
_main.get("world_preview").set_runtime(_runtime)
var records: Array = _main.get("world_preview").get_legacy_stair_records()
_expect(not records.is_empty(), "migrated legacy connection is selectable")
if not records.is_empty():
var id: StringName = records[0][&"id"]
_main.call("_on_legacy_stair_selected", id)
_round_trip(&"legacy_stairs", id, "既設の階段")
var capture := OS.get_environment("THE_TREE_SELECTION_LIFECYCLE_CAPTURE")
if not capture.is_empty():
for frame: int in 5:
await process_frame
await RenderingServer.frame_post_draw
_expect(root.get_texture().get_image().save_png(capture) == OK, "capture migrated staircase selection after actual load")
func _finish() -> void:
if is_instance_valid(_main):
_main.queue_free()
for suffix: String in ["", "_headless", "_generations"]:
SaveService.delete_save(_path + suffix)
print("Selection lifecycle: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://cwsl5k2mr1e4f
+314 -3
View File
@@ -13,6 +13,29 @@ var _kitchen: StringName = &""
var _save_path := ""
func _test_staff_layer_skin() -> void:
_main.call("_set_layer", 0)
var front_color := _panel.staff_detail_label.get_theme_color("font_color")
_panel.select_staff(&"staff_akane")
var before: Dictionary = _runtime.staff_service.get_work_assignment(&"staff_akane")
_main.call("_set_layer", 1)
_panel.refresh()
_expect(_panel.staff_detail_label.get_theme_color("font_color") == Color("d8eeea") and _panel.summary_label.get_theme_color("font_color") == Color("d8eeea"), "staff text stays readable in BACK after refresh")
_panel.call("_set_result", "配属できません", false)
_expect(_panel.result_label.get_theme_color("font_color") == Color("ffb498"), "BACK rejection retains a readable warning colour")
_main.call("_set_layer", 0)
_expect(_panel.staff_detail_label.get_theme_color("font_color") == front_color and _panel.result_label.get_theme_color("font_color") == Color("a7402e"), "FRONT restores staff text and rejection colours")
_main.call("_set_layer", 1)
_panel.call("_set_result", "配属を確認しました", true)
_expect(_panel.result_label.get_theme_color("font_color") == Color("b5e7ba"), "BACK success retains distinct readable colour")
_expect(_runtime.staff_service.get_work_assignment(&"staff_akane") == before, "skin changes do not submit an assignment")
if OS.get_environment("THE_TREE_STAFF_SKIN_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/staff-skin-back.png")) == OK, "capture staff BACK skin")
_main.call("_set_layer", 0)
_panel.call("_set_result", "", true)
func _initialize() -> void:
call_deferred("_run")
@@ -64,6 +87,14 @@ func _run() -> void:
"hired-staff tab replaces the candidates without hiding the management panel")
_expect(_panel.roster_buttons.size() == 2 and "未配属 2人" in _panel.summary_label.text,
"roster shows both real employees and unassigned count")
await _test_staff_layer_skin()
await _test_drag_assignment()
await _test_drag_rejection_boundaries()
await _test_drag_state_change()
await _test_drag_across_load()
await _test_drag_target_demolition()
await _test_drag_defense_lock()
await _test_drag_defense_lock(false)
_panel.select_staff(&"staff_akane")
_choose_facility(_receptions[0])
_panel.apply_button.pressed.emit()
@@ -159,12 +190,15 @@ func _test_guide_respects_current_assignments() -> void:
_panel.apply_button.pressed.emit()
_build_guide_part(&"standard_room", Vector3i(-8, 2, 0))
_build_guide_part(&"standard_room", Vector3i(-6, 2, 0))
_build_guide_part(&"guest_corridor", Vector3i(-4, 1, 0))
_build_guide_part(&"guest_stairs", Vector3i(-4, 1, 0))
_clock.advance_minutes(60)
_build_guide_part(&"guest_corridor", Vector3i(-4, 2, 0))
var incomplete_guide := _main.find_child("InnOpeningGuide", true, false) as InnOpeningGuide
_expect(incomplete_guide.get_progress().get(&"stage") == &"access",
"incomplete stairs cannot advance guidance to staffing")
_build_guide_part(&"guest_stairs", Vector3i(-4, 2, 0))
_clock.advance_minutes(10)
for x: int in range(-8, -3):
_build_guide_part(&"guest_corridor", Vector3i(x, 3, 0))
_build_guide_part(&"guest_stairs" if x == -4 else &"guest_corridor", Vector3i(x, 3, 0))
_clock.advance_minutes(10)
var accessible_room := false
for facility: FacilityInstance in _runtime.facility_service.get_instances():
@@ -289,3 +323,280 @@ func _finish() -> void:
_main.queue_free()
print("Staff management UI: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)
func _test_drag_assignment() -> void:
var before: Dictionary = _runtime.create_snapshot()
var preview: WorldPreview = _main.get("world_preview")
_main.call("_set_layer", 0)
_main.call("_clear_build_selection")
await process_frame
var target: Vector3i
for room: Dictionary in _runtime.construction_service.get_facilities():
if room[&"instance_id"] == _receptions[0]: target = room[&"origin"]
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "viewport drag assigns selected employee to completed reception")
_expect("変更しました" in _panel.result_label.text, "drag reports authoritative assignment success")
_expect(_runtime.restore_snapshot(before), "restore drag fixture before invalid target")
_panel.refresh()
var unchanged: Dictionary = _runtime.create_snapshot()
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(Vector3i(-4,4,0)), Vector3i(-4,4,0))
_expect(_runtime.create_snapshot() == unchanged, "empty-map drop preserves all simulation state")
_expect("完成した配属先施設" in _panel.result_label.text, "invalid drop explains target requirement")
_expect(not preview._can_drop_data(Vector2.ZERO,{&"kind":&"unrelated"}), "map refuses unrelated drag payload")
_panel.refresh()
var schedule_before := _assignment(&"staff_akane").duplicate(true)
_main.call("_set_layer", 1)
await process_frame
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(Vector3i(2,0,1)), Vector3i(2,0,1))
_expect(_assignment(&"staff_akane") == schedule_before, "wrong-role drag preserves staff schedule")
_expect("役割" in _panel.result_label.text, "wrong-role drag displays authoritative refusal")
_main.call("_set_layer", 0)
_panel.select_staff(&"staff_akane")
_panel.shift_option.select(1)
_panel.shift_option.item_selected.emit(1)
await process_frame
_clock.set_paused(false)
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
_clock.set_paused(true)
_expect(_panel.apply_button.disabled and (_runtime.command_queue.create_snapshot()["pending"] as Array).size() == 1, "night drag queues exactly one assignment")
var drag_path := "user://tests/staff-drag-%d" % Time.get_ticks_usec()
_expect(_runtime.save_to_path(drag_path).get(&"ok", false), "save pending night drag")
_expect(_runtime.load_from_path(drag_path).get(&"ok", false), "restore pending night drag")
_panel.refresh()
_expect(_panel.apply_button.disabled, "loaded drag remains pending")
_clock.advance_one_minute()
_expect(_assignment(&"staff_akane").get(&"template_id") == &"NIGHT" and _assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "restored drag applies selected night shift to exact facility")
_expect((_panel.get("_pending_commands") as Dictionary).is_empty(), "restored drag clears pending feedback after execution")
if OS.get_environment("THE_TREE_STAFF_DRAG_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/staff-drag-night.png")) == OK, "capture restored night drag assignment")
SaveService.delete_save(drag_path)
_expect(_runtime.restore_snapshot(before), "restore drag boundary fixture for existing regression")
_panel.refresh()
func _drag_staff_to(target: Vector2, cell: Variant = null, before_release := Callable()) -> void:
var source: Button = _panel.roster_buttons[&"staff_akane"]
var start := source.get_global_rect().get_center()
var motion := InputEventMouseMotion.new()
motion.position=start; motion.global_position=start
motion.window_id = root.get_window_id()
_send_drag_input(motion)
var down := InputEventMouseButton.new()
down.position=start; down.global_position=start; down.button_index=MOUSE_BUTTON_LEFT
down.pressed=true; down.button_mask=MOUSE_BUTTON_MASK_LEFT
down.window_id = root.get_window_id()
_send_drag_input(down)
await process_frame
for point: Vector2 in [start+Vector2(18,0),target]:
var move := InputEventMouseMotion.new()
if point == target and cell is Vector3i:
var map: WorldPreview = _main.get("world_preview")
point = map.global_position + map.cell_to_canvas(cell)
target = point
move.position=point; move.global_position=point; move.relative=point-start
move.button_mask=MOUSE_BUTTON_MASK_LEFT
move.window_id = root.get_window_id()
_send_drag_input(move)
await process_frame
if before_release.is_valid():
_expect(root.gui_is_dragging(), "state changes only after the viewport has started dragging")
before_release.call()
var up := InputEventMouseButton.new()
up.position=target; up.global_position=target; up.button_index=MOUSE_BUTTON_LEFT; up.pressed=false
up.window_id = root.get_window_id()
_send_drag_input(up)
await process_frame
func _send_drag_input(event: InputEventMouse) -> void:
if DisplayServer.get_name() == "headless":
event.position *= Vector2(root.size) / root.get_visible_rect().size
event.global_position = event.position
Input.parse_input_event(event)
else:
if event is InputEventMouseButton and not event.pressed:
Input.warp_mouse(event.position * Vector2(root.size) / root.get_visible_rect().size)
root.push_input(event, true)
func _test_drag_rejection_boundaries() -> void:
# Controlled boundary fixtures; the action under test uses viewport input.
var before: Dictionary = _runtime.create_snapshot()
var preview: WorldPreview = _main.get("world_preview")
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
_main.call("_set_layer", 0)
_panel.select_staff(&"staff_akane")
_panel.shift_option.select(0)
_panel.shift_option.item_selected.emit(0)
var role: StringName = _runtime.staff_service.get_staff(&"staff_akane").role_id
var maximum: int = _runtime.definition_registry.get_facility(&"reception").staff_max
for i in maximum:
var id := StringName("drag_capacity_%d" % i)
_expect(_runtime.staff_service.create_staff(id, role).is_success() and _runtime.staff_service.set_work_assignment(id, _receptions[0], &"DAY", _clock.absolute_minute).is_success(), "fill actual reception shift slots for capacity boundary")
var staff_before: Dictionary = _runtime.staff_service.create_snapshot()
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
_expect(_runtime.staff_service.create_snapshot() == staff_before and "人員上限" in _panel.result_label.text, "full-facility drag preserves all staff and explains capacity limit")
await _capture_drag_rejection("capacity")
_expect(_runtime.restore_snapshot(before), "restore after capacity drag fixture")
_panel.refresh()
_panel.select_staff(&"staff_akane")
_runtime.staff_service.get_staff(&"staff_akane").current_cell = Vector3i(999,999,0)
staff_before = _runtime.staff_service.create_snapshot()
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
_expect(_runtime.staff_service.create_snapshot() == staff_before and "通路と従業員ドア" in _panel.result_label.text, "unreachable drag preserves staff and explains route failure")
await _capture_drag_rejection("route")
_expect(_runtime.restore_snapshot(before), "restore after inaccessible route fixture")
_panel.refresh()
_panel.select_staff(&"staff_akane")
# Inject only the lock boundary, not a complete narrative event.
_runtime.event_service._actor_locks[&"staff_akane"] = &"drag_lock_fixture"
_panel.refresh()
staff_before = _runtime.staff_service.create_snapshot()
var queue_before: Dictionary = _runtime.command_queue.create_snapshot()
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
_expect(_runtime.staff_service.create_snapshot() == staff_before and _runtime.command_queue.create_snapshot() == queue_before, "locked employee cannot start an assignment drag or submit a command")
_expect("変更できません" in _panel.result_label.text, "locked drag explains why assignment is unavailable")
await _capture_drag_rejection("locked")
_runtime.event_service._actor_locks.erase(&"staff_akane")
_expect(_runtime.restore_snapshot(before), "restore after locked employee fixture")
_panel.refresh()
func _capture_drag_rejection(kind: String) -> void:
if OS.get_environment("THE_TREE_DRAG_REJECTION_CAPTURE") != "1": return
await process_frame
await RenderingServer.frame_post_draw
var path := "res://artifacts/facility-dimensions-2026-09-29/staff-drag-rejection-%s.png" % kind
_expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path(path)) == OK, "capture drag rejection %s" % kind)
func _test_drag_state_change() -> void:
var before: Dictionary = _runtime.create_snapshot()
var preview: WorldPreview = _main.get("world_preview")
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
var feedback: Array[CommandResult] = []
var listener := func(result: CommandResult) -> void: feedback.append(result)
_runtime.command_completed.connect(listener)
for reason: StringName in [&"EVENT_LOCKED", &"STAFF_BUSY"]:
_panel.select_staff(&"staff_akane")
var assignment := _assignment(&"staff_akane").duplicate(true)
feedback.clear()
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
if reason == &"EVENT_LOCKED":
_runtime.event_service._actor_locks[&"staff_akane"] = &"drag_race_fixture"
else:
_runtime.staff_service.get_staff(&"staff_akane").current_task_id = &"drag_race_task"
)
_expect(not feedback.is_empty() and not feedback.back().is_success() and feedback.back().details.get(&"reason") == reason, "drop revalidates latest %s state through real command" % reason)
_expect(_assignment(&"staff_akane") == assignment and _panel.apply_button.disabled, "rejected race preserves assignment and reflects unavailable state")
var explanation := "イベント参加中" if reason == &"EVENT_LOCKED" else "作業中"
_expect(explanation in _panel.staff_detail_label.text and explanation in _panel.apply_button.tooltip_text and explanation in _panel.result_label.text and (_panel.get("_pending_commands") as Dictionary).is_empty(), "race rejection shows precise reason and clears pending UI")
_runtime.event_service._actor_locks.erase(&"staff_akane")
_runtime.staff_service.get_staff(&"staff_akane").current_task_id = &""
_expect(_runtime.restore_snapshot(before), "restore state after drag race")
_panel.refresh()
_runtime.command_completed.disconnect(listener)
func _test_drag_across_load() -> void:
var before: Dictionary = _runtime.create_snapshot()
var path := "user://tests/staff-drag-load-%d" % Time.get_ticks_usec()
_expect(_runtime.save_to_path(path).get(&"ok", false), "save before live drag reload")
var preview: WorldPreview = _main.get("world_preview")
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
var loaded_staff: Array[Dictionary] = []
var loaded_queue: Array[Dictionary] = []
_panel.select_staff(&"staff_akane")
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
_expect(_runtime.load_from_path(path).get(&"ok", false), "load real save while pointer is dragging")
loaded_staff.append(_runtime.staff_service.create_snapshot())
loaded_queue.append(_runtime.command_queue.create_snapshot())
)
_expect(_runtime.staff_service.create_snapshot() == loaded_staff[0], "pre-load drag cannot alter restored staff or assignment")
_expect(_runtime.command_queue.create_snapshot() == loaded_queue[0], "pre-load drag cannot enqueue a command in restored world")
_expect("読み込み" in _panel.result_label.text, "stale drag explains that load requires a new drag")
if OS.get_environment("THE_TREE_DRAG_LOAD_CAPTURE") == "1":
await process_frame
await RenderingServer.frame_post_draw
_expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/staff-drag-load-2026-09-29/rejected.png")) == OK, "capture load cancellation feedback")
_panel.select_staff(&"staff_akane")
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "fresh drag after load still assigns normally")
SaveService.delete_save(path)
_expect(_runtime.restore_snapshot(before), "restore fixture after live drag reload")
_panel.refresh()
_panel.select_staff(&"staff_akane")
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
_expect(not _runtime.restore_snapshot({}), "invalid snapshot is refused during drag")
)
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "failed restore preserves current world and live drag")
_expect(_runtime.restore_snapshot(before), "restore fixture after refused load")
_panel.refresh()
func _test_drag_target_demolition() -> void:
var before: Dictionary = _runtime.create_snapshot()
var preview: WorldPreview = _main.get("world_preview")
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
var staff_before: Dictionary = _runtime.staff_service.create_snapshot()
_panel.select_staff(&"staff_akane")
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
var removed: CommandResult = _runtime.submit_command(&"demolish_facility", {&"instance_id":_receptions[0]}, &"player", &"", 30, -1, &"ui.drag.target.demolition")
_expect(removed.is_success(), "normal demolition command succeeds while dragging toward unstaffed facility")
)
_expect(not _runtime.facility_service.has_instance(_receptions[0]), "drop cannot resurrect demolished facility")
_expect(_runtime.staff_service.create_snapshot() == staff_before, "demolition during drag preserves staff state and schedules")
_expect("完成した配属先施設" in _panel.result_label.text or "見つかりません" in _panel.result_label.text, "removed drop target has actionable rejection feedback")
_expect((_panel.get("_pending_commands") as Dictionary).is_empty(), "removed target leaves no stuck assignment UI")
_expect(_runtime.restore_snapshot(before), "restore after live demolition boundary")
_panel.refresh()
func _test_drag_defense_lock(register_objective := true) -> void:
var before: Dictionary = _runtime.create_snapshot()
var preview: WorldPreview = _main.get("world_preview")
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
var assignment := _assignment(&"staff_akane").duplicate(true)
_panel.select_staff(&"staff_akane")
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
var created: CommandResult = _runtime.submit_command(&"create_defense_raid", {
&"raid_id": &"ui.drag.defense", &"definition": {
&"source_id": &"runtime_test", &"entry_points": [&"gate.north"],
&"objective_ids": [_receptions[0]], &"round_minutes": 5,
&"max_duration_minutes": 180, &"retreat_policy": &"HOLD", &"defender_retreat_threshold": 0,
&"waves": [{&"wave_id": &"wave.one", &"threat_strength": 15,
&"retreat_threshold": 0, &"max_rounds": 1, &"entry_points": [&"gate.north"]}],
}}, &"defense_controller")
_expect(created.is_success(), "create real defense raid during assignment drag")
if register_objective:
var asset: CommandResult = _runtime.submit_command(&"register_defense_asset", {
&"raid_id": &"ui.drag.defense", &"asset_id": _receptions[0], &"kind": &"objective",
&"max_integrity": 10000, &"defense": 40, &"options": {}}, &"defense_controller")
_expect(asset.is_success(), "register real facility as defense objective")
var registered: CommandResult = _runtime.submit_command(&"register_defense_combatant", {
&"raid_id": &"ui.drag.defense", &"actor_id": &"staff_akane", &"side": &"DEFENDER",
&"combat": 90, &"max_health": 100, &"options": {}}, &"defense_controller")
_expect(registered.is_success(), "register dragged employee as defender")
)
_expect(_assignment(&"staff_akane") == assignment and "防衛参加中" in _panel.result_label.text, "defense registration during drag rejects assignment with precise feedback")
_expect(_panel.apply_button.disabled and "防衛参加中" in _panel.staff_detail_label.text and "防衛参加中" in _panel.apply_button.tooltip_text, "locked editor explains defense restriction before retry")
var path := "user://tests/staff-defense-lock-%d" % Time.get_ticks_usec()
_expect(_runtime.save_to_path(path).get(&"ok", false), "save registered defense lock")
_expect(_runtime.load_from_path(path).get(&"ok", false), "restore registered defense lock")
_panel.refresh()
_panel.select_staff(&"staff_akane")
var queue_before: Dictionary = _runtime.command_queue.create_snapshot()
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
_expect(_runtime.command_queue.create_snapshot() == queue_before and "防衛参加中" in _panel.result_label.text, "restored defender cannot begin new drag and sees the reason")
var cancelled: CommandResult = _runtime.submit_command(&"cancel_defense_raid", {&"raid_id": &"ui.drag.defense"}, &"defense_controller")
_expect(cancelled.is_success(), "cancel raid through normal command: %s %s" % [cancelled.message, cancelled.details])
_panel.refresh()
_expect(not _panel.apply_button.disabled and not "防衛参加中" in _panel.staff_detail_label.text, "terminal raid releases editor lock and stale explanation")
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "new drag after defense cancellation assigns normally")
SaveService.delete_save(path)
_expect(_runtime.restore_snapshot(before), "restore fixture after defense boundary")
_panel.refresh()
+87
View File
@@ -0,0 +1,87 @@
extends SceneTree
var checks := 0
var failures := 0
var runtime: Variant
var clock: Variant
var main: Control
var elevator_mode := OS.get_environment("THE_TREE_RESCUE_ELEVATOR") == "1"
func _initialize() -> void: call_deferred("run")
func expect(ok: bool, text: String) -> void:
checks += 1
if not ok: failures += 1; push_error(text)
func command(kind: StringName, payload: Dictionary) -> bool:
var result: CommandResult = runtime.submit_command(kind,payload,&"player")
expect(result.is_success(), "normal command succeeds: %s (%s)" % [kind,result.message])
return result.is_success()
func run() -> void:
root.size = Vector2i(1440,810)
runtime = root.get_node("SimulationRuntime")
clock = root.get_node("GameClock")
clock.reset(1,6,0)
root.get_node("GameState").reset()
runtime.initialize(clock,root.get_node("GameState"),711)
clock.set_paused(true)
main = load("res://scenes/main/main.tscn").instantiate()
root.add_child(main)
await process_frame
runtime.configure_save_policy({&"enabled": false})
main._set_layer(1)
expect(main.build_buttons.has(&"infirmary"), "infirmary is selectable in the real build palette")
main.build_buttons[&"infirmary"].button_pressed = true
main._on_map_cell_clicked(Vector3i(-6,0,1))
expect(runtime.construction_service.get_jobs().size() == 1, "map click starts infirmary construction")
if not command(&"build_facility",{&"definition_id":&"staff_door",&"origin":Vector3i(-1,0,1) if elevator_mode else Vector3i(0,0,1)}): quit(1); return
if elevator_mode:
if not command(&"build_facility",{&"definition_id":&"service_elevator",&"origin":Vector3i(0,0,1)}): quit(1); return
clock.advance_minutes(400 if elevator_mode else 180)
var clinic: FacilityInstance
for room: FacilityInstance in runtime.facility_service.get_instances():
if room.definition_id == &"infirmary": clinic = room
expect(clinic != null and clinic.operation_state == FacilityInstance.OperationState.OPERATING, "paid infirmary completes and operates")
if clinic == null: quit(1); return
if not command(&"hire_staff_candidate",{&"candidate_id":&"staff_tessa"}): quit(1); return
if not command(&"set_staff_work_assignment",{&"staff_id":&"staff_tessa",&"facility_id":clinic.instance_id,&"template_id":&"DAY"}): quit(1); return
# Injury is the only fixture injection; the rescuer is hired and commutes normally.
if not command(&"register_staff",{&"staff_id":&"injured_worker",&"role_id":&"porter",&"options":{&"health":0,&"current_cell":Vector3i(0,1,1) if elevator_mode else Vector3i(3,0,1)}}): quit(1); return
main._activate_management_mode(&"staff")
main.staff_management_panel.select_staff(&"injured_worker")
main.world_preview.focus_operating_area()
var carried := false
var treated := false
var saw_cabin := false
for minute: int in 150:
clock.advance_minutes(1)
main.staff_management_panel.refresh()
var report: Dictionary = runtime.get_staff_rescue_report()
if elevator_mode and not saw_cabin:
var trip: Dictionary = runtime.elevators.get_actor_trip(&"staff:injured_worker")
for shaft: Dictionary in runtime.elevators.get_report()[&"shafts"]:
if trip.get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED and shaft[&"phase"] == &"TRAVEL":
saw_cabin = true
expect(shaft[&"passenger_count"] == 2, "normally hired medic and patient occupy two cabin seats")
await capture("paired-cabin")
if not carried and report[&"transporting"] > 0 and runtime.staff_service.get_staff(&"injured_worker").current_cell.x <= -1:
carried = true
expect("搬送中" in main.staff_management_panel.staff_detail_label.text, "staff UI shows actual transport status")
await capture("transport")
if not treated and report[&"treating"] > 0:
treated = true
expect("治療中" in main.staff_management_panel.staff_detail_label.text, "staff UI shows actual treatment status")
await capture("treatment")
if carried and treated and runtime.staff_service.get_staff(&"injured_worker").health > 0: break
if minute%10 == 0: await process_frame
if elevator_mode: expect(saw_cabin, "normal medical assignment uses the elevator for rescue")
expect(carried and treated, "a normally hired medical worker picks up and treats the patient")
expect(runtime.staff_service.get_staff(&"injured_worker").health == 100, "treatment completes through normal Runtime ticks")
expect(runtime.economy_service.get_balance(&"PLAYER_CASH") < 5000, "medical building and hiring are actually paid")
expect(runtime.staff_service.validate_invariants()[&"ok"], "normal rescue preserves ownership")
main.queue_free()
await process_frame
print("Rescue UI: %d checks, %d failures" % [checks,failures])
quit(failures)
func capture(label: String) -> void:
if OS.get_environment("THE_TREE_RESCUE_CAPTURE") != "1": return
main.world_preview.queue_redraw()
await process_frame
await RenderingServer.frame_post_draw
root.get_texture().get_image().save_png(("res://artifacts/remaining-2026-09-29/%s.png" if elevator_mode else "res://artifacts/rescue-2026-09-29/%s.png") % label)
+1
View File
@@ -0,0 +1 @@
uid://cxlhwwgj5vv38
+39
View File
@@ -0,0 +1,39 @@
extends SceneTree
var checks := 0
var failures := 0
func _initialize() -> void: call_deferred("run")
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok: failures += 1;push_error(message)
func run() -> void:
root.size = Vector2i(1440,810)
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
var runtime: Variant = root.get_node("SimulationRuntime")
clock.reset(1,6,0);state.reset();runtime.initialize(clock,state,718);clock.set_paused(true)
var main: Control = load("res://scenes/main/main.tscn").instantiate();root.add_child(main);await process_frame
runtime.configure_save_policy({&"enabled":false})
main._set_layer(1)
main.build_buttons[&"staff_waiting_room"].button_pressed = true
main._on_map_cell_clicked(Vector3i(-6,0,1))
expect(runtime.construction_service.get_jobs().size() == 1,"waiting room builds through the real map palette")
clock.advance_minutes(120)
# Fatigue/health fixture, normal public registration and authoritative movement.
expect(runtime.submit_command(&"register_staff",{&"staff_id":&"resting",&"role_id":&"porter",&"options":{&"health":20,&"fatigue":20,&"state":StaffRuntime.State.OFF_DUTY,&"current_cell":Vector3i(5,0,0)}},&"player").is_success(),"tired off-duty staff registers")
main._activate_management_mode(&"staff");main.staff_management_panel.select_staff(&"resting")
main.staff_management_panel.refresh()
expect(main.staff_management_panel.staff_detail_label.text.contains("休憩先へ移動中"),"panel exposes actual journey to rest")
clock.advance_minutes(60);main.staff_management_panel.refresh();main.world_preview.focus_operating_area()
await process_frame
expect(main.staff_management_panel.staff_detail_label.text.contains("待機室で休憩中"),"arrival updates the panel to resting")
expect(runtime.staff_service.get_staff(&"resting").health > 20,"resting health recovers in normal operations")
main.world_preview._prepare_room_presentations()
expect(main.world_preview._resting_staff.has(&"resting"),"room presentation uses actual resting staff ownership")
var room_id: StringName = main.world_preview._resting_staff.get(&"resting",&"")
expect(main.world_preview._room_presentations.get(room_id,{}).get(&"count",0) == 1,"rest room count is a real person, not cosmetic occupancy")
if OS.get_environment("THE_TREE_REST_CAPTURE") == "1":
main.world_preview.queue_redraw();await process_frame;await RenderingServer.frame_post_draw
expect(root.get_texture().get_image().save_png("res://artifacts/remaining-2026-09-29/staff-rest.png") == OK,"resting staff screenshot saved")
expect(runtime.restore_snapshot(runtime.create_snapshot()),"visible rest state remains saveable")
main.queue_free();await process_frame
print("Staff rest UI: %d checks, %d failures" % [checks,failures]);quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://deoqe6b11a40o
+13
View File
@@ -116,14 +116,27 @@ func _test_legacy_stair_selection() -> void:
var rect: Rect2 = _preview.call("_legacy_stair_rect", Vector3i(0,1,0))
var stair_id := _preview.legacy_stair_at_canvas(rect.get_center())
_expect(stair_id == &"legacy.stairs:0:0:0", "legacy stairs are hit-tested on the upper landing")
var inactive_front: Array = _preview._other_layer_rooms()
_expect(inactive_front.size()==1 and inactive_front[0][&"id"]==&"legacy.stairs:1:0:0" and inactive_front[0][&"rect"]==rect, "opposite legacy stairs appear once at their real compact footprint without construction service")
_expect(bool(inactive_front[0].get(&"legacy_stair",false)), "legacy ghost uses compact stair art rather than a completed facility room")
_expect(_preview.legacy_stair_at_canvas(_preview.get_cell_rect(Vector3i(0,1,0)).position+Vector2.ONE).is_empty(), "small legacy stair target preserves the rest of an existing room's click area")
_expect(_preview.legacy_stair_at_canvas(_preview.cell_to_canvas(Vector3i.ZERO)).is_empty(), "lower floor cannot select the stair glyph drawn upstairs")
_click(rect.get_center())
_expect(_main.get("_selected_entity_kind") == &"legacy_stairs" and _main.get("_selected_entity_id") == stair_id, "legacy glyph click opens its dedicated selection without clearing its ID")
_expect("既設の階段" == _main.get("selected_heading_label").text and "1F ↔ 2F" in _main.get("selected_detail_label").text, "legacy inspector describes the preserved connection")
_preview.layer_mode = WorldPreview.LayerMode.BACK
var inactive_back: Array = _preview._other_layer_rooms()
_expect(inactive_back.size()==1 and inactive_back[0][&"id"]==&"legacy.stairs:0:0:0", "back view exposes front legacy stair without duplicating selected stair")
_click(rect.get_center())
_expect(_main.get("_selected_entity_id") == &"legacy.stairs:1:0:0" and "客・従業員用" in _main.get("selected_detail_label").text and "BACK" in _main.get("selected_detail_label").text, "legacy BACK stairs preserve the old public access instead of falsely claiming staff-only access")
if OS.get_environment("THE_TREE_LAYER_LEGACY_CAPTURE") == "1":
_preview.set_zoom(3.0)
_preview.pan_offset += _preview.size*0.5-_preview.cell_to_canvas(Vector3i(0,1,0))
for layer: int in [0,1]:
_preview.layer_mode=layer
_preview._process(1.0)
await process_frame;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/layer-legacy-%d.png" % layer))==OK,"capture opposite legacy stairs")
var records := _preview.get_legacy_stair_records()
records[0][&"id"] = &"changed.view.copy"
_expect(not str(fixture.legacy_stairs.create_snapshot()).contains("changed.view.copy"), "visual records cannot mutate saved stair ownership")
+23
View File
@@ -14,6 +14,22 @@ var _item: StringName = &""
var _save_path := "user://tests/supply_ui_%d/queued_purchase" % OS.get_process_id()
func _test_layer_palette() -> void:
_main.call("_set_layer", 0)
var before: Color = _panel.warehouse_stock_label.get_theme_color("font_color")
var selected: bool = _panel.production_enabled.button_pressed
_main.call("_set_layer", 1)
_panel.refresh()
_expect(_panel.warehouse_stock_label.get_theme_color("font_color") == Color("d8eeea"), "BACK panel text survives refresh")
_expect(_panel.production_enabled.get_theme_color("font_hover_color") == Color("d8eeea") and _panel.production_enabled.get_theme_color("font_disabled_color") == Color("8da4a4"), "BACK toggle remains readable for hover and disabled states")
_panel.call("_set_result", "条件未達", false)
_expect(_panel.result_label.get_theme_color("font_color") == Color("ffb498"), "BACK error retains warning colour")
_main.call("_set_layer", 0)
_expect(_panel.warehouse_stock_label.get_theme_color("font_color") == before and _panel.result_label.get_theme_color("font_color") == Color("a7402e"), "FRONT restores panel and warning colours")
_expect(_panel.production_enabled.button_pressed == selected, "palette does not toggle policy")
_panel.call("_set_result", "", true)
func _initialize() -> void:
call_deferred("_run")
@@ -41,6 +57,7 @@ func _run() -> void:
(_main.get("navigation_buttons")[&"product"] as Button).pressed.emit()
_expect(_panel.visible and _panel.purchase_button.disabled, "product opens a safe empty state before any warehouse exists")
_expect("倉庫" in _panel.warehouse_stock_label.text, "empty purchase page explains the warehouse prerequisite")
_test_layer_palette()
for request: Array in [
[&"warehouse", Vector3i(-8, 0, 1)],
[&"kitchen", Vector3i(0, 0, 1)],
@@ -195,6 +212,12 @@ func _test_navigation_and_layout() -> void:
captured.save_png("/tmp/the-tree-supply-%s.png" % tab)
if tab == &"purchase":
captured.save_png("/tmp/the-tree-supply-management.png")
if OS.get_environment("THE_TREE_PANEL_SKIN_CAPTURE") == "1":
_main.call("_set_layer", 1)
_panel.select_tab(&"kitchen")
_choose(_panel.kitchen_option, _kitchens[0])
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/supply-skin-back.png")) == OK, "capture BACK management skin")
_main.call("_unhandled_key_input", _key(KEY_ESCAPE))
_expect(not _panel.visible and not bool(_main.get("_management_panel_open")), "Escape closes supply management and restores the map")
+135
View File
@@ -0,0 +1,135 @@
extends SceneTree
## Runs the real UI command handlers, a normal clock and disk save. Invoke again
## with -- --read and the same THE_TREE_UPKEEP_UI_SAVE for fresh-process restore.
var checks := 0
var failures := 0
var main: Control
var runtime: Variant
var clock: Variant
func _initialize() -> void:
call_deferred("_run")
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok:
failures += 1
push_error(message)
func _run() -> void:
runtime = root.get_node("SimulationRuntime")
clock = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1, 6, 0)
state.reset()
runtime.initialize(clock, state, 424242)
clock.set_paused(true)
root.size = Vector2i(1440, 900)
main = load("res://scenes/main/main.tscn").instantiate()
var path := OS.get_environment("THE_TREE_UPKEEP_UI_SAVE")
if path.is_empty():
path = "user://tests/upkeep_closed_%d/manual" % OS.get_process_id()
main.set("manual_save_path", path)
root.add_child(main)
await process_frame
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
if "--read" in OS.get_cmdline_user_args():
_press_text("読込")
expect(clock.absolute_minute == 1440, "fresh-process UI load restores midnight")
else:
(main.get("build_buttons")[&"reception"] as Button).button_pressed = true
main.call("_on_map_cell_clicked", Vector3i(-6, 0, 0))
expect(runtime.construction_service.get_jobs().size() == 1, "palette and map handler create one construction job")
clock.advance_minutes(120)
var rooms: Array = runtime.facility_service.get_instances()
expect(rooms.size() == 1, "normal clock finishes selected room")
if rooms.is_empty():
_finish()
return
var panel: BusinessManagementPanel = main.get("business_management_panel")
panel.selected_facility_id = rooms[0].instance_id
panel.refresh()
panel.business_open_button.button_pressed = false
panel.apply_button.pressed.emit()
expect(not rooms[0].operator_open, "real business controls close room while paused")
expect("維持費" in panel.explanation_label.text, "business controls explain continued upkeep")
clock.advance_minutes(959)
expect(runtime.economy_service.get_balance(FacilityUpkeepSettlement.EXPENSE) == 0, "23:59 is not settled early")
_press_text("保存")
var before: Dictionary = runtime.create_snapshot()
_press_text("読込")
# Save/load deliberately emit distinct audit events and measured durations.
# Compare authoritative clock, ownership and money rather than those events.
var after: Dictionary = runtime.create_snapshot()
expect(after[&"clock"] == before[&"clock"], "UI disk load restores exact pre-midnight clock")
for field: StringName in [&"economy", &"facilities", &"facility_upkeep", &"upkeep_settlement"]:
expect(after[&"systems"][&"runtime"][field] == before[&"systems"][&"runtime"][field], "UI disk load preserves " + String(field))
clock.advance_minutes(1)
_verify_closed()
await _test_ledger_skin()
if "--read" not in OS.get_cmdline_user_args():
_press_text("保存")
await _capture()
_finish()
func _test_ledger_skin() -> void:
main.call("_set_layer", 0)
main.call("_set_ledger_panel_open", true)
var ledger: OperatingLedgerPanel = main.get("operating_ledger_panel")
var report := ledger.get_report()
var cash_label: Label = ledger.get("_cash_label")
var closed_label: Label = ledger.get("_closed_label")
var before_text := closed_label.text
var before_color := cash_label.get_theme_color("font_color")
var economy: Dictionary = runtime.economy_service.create_snapshot()
main.call("_set_layer", 1)
ledger.refresh()
expect(cash_label.get_theme_color("font_color") == Color("d8eeea") and closed_label.get_theme_color("font_color") == Color("d8eeea"), "BACK ledger cash and close remain readable after refresh")
expect(ledger.get_report() == report and closed_label.text == before_text, "skin preserves selected close and every displayed amount")
expect(runtime.economy_service.create_snapshot() == economy, "ledger skin does not post transactions")
if DisplayServer.get_name() != "headless":
await process_frame;await RenderingServer.frame_post_draw
var phase := "read" if "--read" in OS.get_cmdline_user_args() else "write"
expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/facility-dimensions-2026-09-29/ledger-skin-back-%s.png" % phase)) == OK, "capture ledger BACK skin")
main.call("_set_layer", 0)
expect(cash_label.get_theme_color("font_color") == before_color and ledger.get_report() == report, "FRONT restoration retains ledger data")
func _verify_closed() -> void:
var rooms: Array = runtime.facility_service.get_instances()
expect(rooms.size() == 1, "saved room remains present")
if rooms.is_empty():
return
var room: FacilityInstance = rooms[0]
expect(not room.operator_open, "closed policy survives midnight and restore")
# 08:00 completion to 00:00 = 960 minutes at the production 3G/day.
expect(runtime.economy_service.get_balance(FacilityUpkeepSettlement.EXPENSE) == 2, "closed room incurs 3 * 960 / 1440 = 2G")
expect(runtime.upkeep_settlement.get_facility_balance(room.instance_id)[&"fraction_units"] == 0, "whole-gold settlement leaves no fractional remainder")
expect(runtime.upkeep_settlement.get_arrears_total() == 0, "upkeep is paid with no arrears")
main.get("world_preview").facility_selected.emit(room.instance_id)
expect("3G / 日" in (main.get("selected_detail_label") as Label).text, "inspector uses production daily rate")
var midnight: Dictionary = runtime.create_snapshot()
var count: int = runtime.upkeep_settlement.get_settled_period_count()
clock.advance_minutes(1)
expect(runtime.upkeep_settlement.get_settled_period_count() == count, "next minute never repeats midnight settlement")
# Restore exact midnight for the independent read phase.
expect(runtime.restore_snapshot(midnight), "restore verified midnight for disk handoff")
func _press_text(text: String) -> void:
if text == "読込":
var menu := main.find_child("SaveLoadMenu", true, false) as MenuButton
expect(menu != null and not menu.disabled, "normal load menu is available")
if menu != null and not menu.disabled:
menu.get_popup().id_pressed.emit(0)
return
for button: Node in main.find_children("*", "Button", true, false):
if button.text == text:
button.pressed.emit()
return
expect(false, "UI button found: " + text)
func _capture() -> void:
if DisplayServer.get_name() == "headless":
return
for frame in 6:
await process_frame
await RenderingServer.frame_post_draw
var suffix := "read" if "--read" in OS.get_cmdline_user_args() else "write"
expect(root.get_texture().get_image().save_png("res://artifacts/upkeep-2026-09-29/closed-ui-%s.png" % suffix) == OK, "native rendered evidence saved")
func _finish() -> void:
main.queue_free()
print("%s: %d checks (phase %s)" % ["PASS" if failures == 0 else "FAIL", checks, "read" if "--read" in OS.get_cmdline_user_args() else "write"])
quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://c5u745h6rgnis
+54
View File
@@ -0,0 +1,54 @@
extends SceneTree
var checks := 0
var failures := 0
func _initialize() -> void:
call_deferred("_run")
func expect(ok: bool, message: String) -> void:
checks += 1
if not ok:
failures += 1
push_error(message)
func _run() -> void:
var runtime: Variant = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1, 6, 0)
state.reset()
runtime.initialize(clock, state, 424242)
clock.set_paused(true)
root.size = Vector2i(1440, 900)
var main: Control = load("res://scenes/main/main.tscn").instantiate()
main.set("manual_save_path", "user://tests/upkeep_inspector_%d" % OS.get_process_id())
root.add_child(main)
await process_frame
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
var definition: FacilityDefinition = runtime.definition_registry.get_facility(&"guest_corridor")
var old_rate := definition.maintenance_gold
definition.maintenance_gold = 1440
expect(runtime.submit_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(4, 1, 0)}).is_success(), "public construction accepts infrastructure")
clock.advance_minutes(10)
var facilities: Array = runtime.construction_service.get_facilities()
expect(facilities.size() == 1, "one completed corridor")
if not facilities.is_empty():
var id: StringName = facilities[0][&"instance_id"]
clock.advance_minutes(3)
main.get("world_preview").facility_selected.emit(id)
var report: Dictionary = runtime.get_facility_upkeep_report(id)
expect(report[&"owned"] and report[&"daily_rate"] == 1440 and report[&"unsettled_units"] == 4320, "inspector projection preserves exact ownership accrual")
var text: String = main.get("selected_detail_label").text
expect("設備完成" in text and "1440G / 日" in text and "約3.00G" in text and "未払い 0G" in text, "infrastructure inspector displays daily pending and unpaid amounts")
expect("閉店・休止中" in text and "同期しています" not in text, "completed infrastructure explains ownership instead of a permanent sync placeholder")
var before: Dictionary = runtime.create_snapshot()
main.call("_refresh_selected_entity")
expect(runtime.create_snapshot() == before, "refresh is read only")
if DisplayServer.get_name() != "headless":
for frame in 6:
await process_frame
await RenderingServer.frame_post_draw
DirAccess.make_dir_recursive_absolute("res://artifacts/upkeep-2026-09-29")
expect(root.get_texture().get_image().save_png("res://artifacts/upkeep-2026-09-29/upkeep-inspector.png") == OK, "rendered inspector captured")
definition.maintenance_gold = old_rate
main.queue_free()
await process_frame
print("%s: %d checks" % ["PASS" if failures == 0 else "FAIL", checks])
quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://bxmabisl8kpou
+89
View File
@@ -0,0 +1,89 @@
extends SceneTree
var checks:=0
var failures:=0
func expect(ok: bool, message: String) -> void:
checks+=1
if not ok: failures+=1;push_error(message)
func _initialize() -> void:call_deferred("run")
func run() -> void:
var runtime: Variant=root.get_node("SimulationRuntime")
var clock: Variant=root.get_node("GameClock")
var state: Variant=root.get_node("GameState")
clock.reset(1,6,0);state.reset();runtime.initialize(clock,state,42510);clock.set_paused(true)
root.size=Vector2i(1280,720)
var main: Control=load("res://scenes/main/main.tscn").instantiate()
var save_path:="user://tests/rank_ui_%d" % Time.get_ticks_usec()
main.set("manual_save_path",save_path);root.add_child(main)
runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
await process_frame;await process_frame
var button: Button=main.get("rank_button")
var dialog: ConfirmationDialog=main.get("rank_dialog")
expect(button.text=="世界樹ランク 1" and button.is_visible_in_tree(),"rank appears in normal HUD")
var command_results: Array[CommandResult]=[]
runtime.command_completed.connect(func(result: CommandResult) -> void: command_results.append(result))
var original: Dictionary=runtime.create_snapshot()
button.pressed.emit()
expect(dialog.visible and dialog.get_ok_button().disabled,"unmet rank conditions cannot be confirmed")
expect("5000 / 10000" in dialog.dialog_text and "評判" in dialog.dialog_text,"rank conditions show current facts and deficits")
expect(runtime.create_snapshot()==original,"viewing live eligibility does not mutate progression or simulation")
var front_text := dialog.get_label().get_theme_color("font_color")
var front_panel: Color = dialog.theme.get_stylebox("panel", "AcceptDialog").bg_color
var conditions := dialog.dialog_text
main.call("_set_layer", 1)
dialog.refresh()
expect(dialog.get_label().get_theme_color("font_color") == Color("d8eeea") and dialog.theme.get_stylebox("panel", "AcceptDialog").bg_color == Color("202e34"), "rank dialog uses readable BACK palette")
expect(dialog.get_ok_button().get_theme_color("font_disabled_color") == Color("8da4a4"), "disabled promotion remains legible in BACK")
expect(dialog.dialog_text == conditions and dialog.get_ok_button().disabled, "skin change preserves conditions and disabled gate")
if OS.get_environment("THE_TREE_RANK_SKIN_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/rank-skin-back.png")) == OK, "capture BACK rank conditions")
main.call("_set_layer", 0)
expect(dialog.get_label().get_theme_color("font_color") == front_text and dialog.theme.get_stylebox("panel", "AcceptDialog").bg_color == front_panel, "rank dialog restores FRONT palette")
expect(runtime.create_snapshot() == original, "round trip skin change preserves complete simulation snapshot")
if OS.get_environment("THE_TREE_RANK_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/rank-conditions.png"))==OK,"capture rank HUD and unmet conditions")
dialog.hide()
# Controlled boundary inputs verify UI wiring, not natural campaign completion.
expect(runtime.submit_command(&"update_progression_metrics",{&"metrics":{&"reputation":25}}).is_success(),"fund reputation boundary fixture")
expect(runtime.submit_command(&"change_resources",{&"deltas":{&"gold":5000}}).is_success(),"fund current cash boundary through ledger command")
button.pressed.emit()
expect(not dialog.get_ok_button().disabled and "10000 / 10000" in dialog.dialog_text,"eligibility uses live cash before another tick")
var eligible_snapshot: Dictionary=runtime.create_snapshot()
# Explicit recovery-failure injection tests the pre-existing operations gate.
runtime.operations_coordinator._enter_external_quarantine(&"tick",clock.absolute_minute,&"test_phase",&"TEST_FAILURE")
dialog.refresh()
expect(dialog.get_ok_button().disabled and "営業状態の復旧" in dialog.dialog_text,"otherwise eligible rank is blocked and explained during operations quarantine")
expect(runtime.restore_snapshot(eligible_snapshot),"recover valid pre-fault game")
dialog.refresh()
runtime.submit_command(&"change_resources",{&"deltas":{&"gold":-1}})
dialog.confirmed.emit();dialog.hide()
expect(runtime.progression_service.current_rank==1 and "昇格できません" in main.get("build_status_label").text,"confirmation revalidates changed conditions instead of trusting prior preview")
expect(not command_results.is_empty() and command_results.back().details.get(&"reason",&"")==&"REQUIREMENTS_NOT_MET","changed-cash rejection is eligibility failure, not a malformed UI request")
runtime.submit_command(&"change_resources",{&"deltas":{&"gold":1}})
button.pressed.emit()
clock.set_paused(false)
dialog.confirmed.emit();dialog.hide()
clock.set_paused(true)
expect(runtime.progression_service.current_rank==1 and int(dialog.get("_pending_id"))!=0,"running-game confirmation queues promotion without pretending it already happened")
main.call("_save_manual")
expect(SaveService.load_snapshot(save_path)[&"ok"],"pending rank request saves before execution")
expect(runtime.restore_snapshot(original),"restore control game while promotion is pending")
main.call("_load_manual")
button.pressed.emit()
expect(int(dialog.get("_pending_id"))!=0 and dialog.get_ok_button().disabled,"loaded pending promotion stays visible and cannot be submitted twice")
dialog.hide()
clock.advance_one_minute()
expect(runtime.progression_service.current_rank==2 and state.world_tree_rank==2,"queued UI promotion commits real rank")
expect(runtime.world_service.get_bounds()==ProgressionService.RANK_BOUNDS[2],"promotion expands actual construction bounds")
expect(button.text=="世界樹ランク 2" and int(dialog.get("_pending_id"))==0,"completed command updates HUD and clears pending UI state")
main.call("_save_manual")
expect(SaveService.load_snapshot(save_path)[&"ok"],"promoted game saves through normal UI")
expect(runtime.restore_snapshot(original),"restore pre-promotion control state")
main.call("_refresh_dashboard")
expect(button.text=="世界樹ランク 1","HUD follows restored rank")
main.call("_load_manual")
expect(button.text=="世界樹ランク 2" and runtime.world_service.get_bounds()==ProgressionService.RANK_BOUNDS[2],"normal load restores promoted HUD and map bounds")
SaveService.delete_save(save_path)
main.queue_free();await process_frame
print("World tree rank UI: %d checks, %d failures" % [checks,failures]);quit(failures)
+1
View File
@@ -0,0 +1 @@
uid://beivol4d4b5al