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:
@@ -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():
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bwvp6pp1gmkb5
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1bcyrfalhjc8
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dwknbl8i0a7vt
|
||||
@@ -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
|
||||
@@ -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}],
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dlgto3iitns6f
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfactoxtr4j7o
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://5lpeq4dfpkqh
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://7pjvl52ebeqi
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://lv72d8fmyhdl
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://12qlnwu4ldpw
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpy8opqu4mjhu
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://6bjsv1fuy0ol
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bcv2y1ne4ik7b
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwsl5k2mr1e4f
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxlhwwgj5vv38
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://deoqe6b11a40o
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c5u745h6rgnis
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bxmabisl8kpou
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://beivol4d4b5al
|
||||
Reference in New Issue
Block a user