177 lines
8.6 KiB
GDScript
177 lines
8.6 KiB
GDScript
extends SceneTree
|
|
|
|
## Normal UI commands, default funds, and natural arrivals only. No demo fixture.
|
|
## Optional THE_TREE_OPENING_CAPTURE=1 renders the new-game and close screens.
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _runtime: Variant
|
|
var _clock: Variant
|
|
var _main: Control
|
|
var _guide: InnOpeningGuide
|
|
var _ledger: OperatingLedgerPanel
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_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() as Control
|
|
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) as InnOpeningGuide
|
|
_ledger = _main.find_child("OperatingLedgerPanel", true, false) as OperatingLedgerPanel
|
|
_expect(_guide != null and _ledger != null, "normal main scene mounts opening guide and real ledger")
|
|
if _guide == null or _ledger == null:
|
|
_finish()
|
|
return
|
|
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 5000, "ordinary initial funds are unchanged")
|
|
_expect(_guide.get_progress().get(&"stage") == &"reception", "new game starts with reception guidance")
|
|
await _capture("opening")
|
|
if not _build(&"reception", Vector3i(-8, 0, 0)):
|
|
_finish()
|
|
return
|
|
_expect(_guide.get_progress().get(&"action") == &"resume", "guide explains construction time while paused")
|
|
await _advance(120)
|
|
_expect(_guide.get_progress().get(&"stage") == &"room", "completed reception advances room guidance")
|
|
_build(&"standard_room", Vector3i(-8, 2, 0))
|
|
_build(&"standard_room", Vector3i(-6, 2, 0))
|
|
_build(&"guest_corridor", Vector3i(-4, 1, 0))
|
|
await _advance(60)
|
|
_build(&"guest_corridor", Vector3i(-4, 2, 0))
|
|
await _advance(10)
|
|
for x: int in range(-8, -3):
|
|
_build(&"guest_corridor", Vector3i(x, 3, 0))
|
|
await _advance(10)
|
|
var reception_id: StringName = &""
|
|
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
|
if facility.definition_id == &"reception":
|
|
reception_id = facility.instance_id
|
|
_expect(not reception_id.is_empty(), "normal construction created the reception")
|
|
_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")
|
|
var staff_panel := _main.find_child("StaffManagementPanel", true, false) as StaffManagementPanel
|
|
_expect(staff_panel != null, "staff assignment controls are part of the dashboard")
|
|
if staff_panel == null or staff_members.is_empty():
|
|
_finish()
|
|
return
|
|
staff_panel.select_staff(staff_members[0].staff_id)
|
|
var assigned := false
|
|
for index: int in staff_panel.facility_option.item_count:
|
|
if StringName(staff_panel.facility_option.get_item_metadata(index)) == reception_id:
|
|
staff_panel.facility_option.select(index)
|
|
staff_panel.facility_option.item_selected.emit(index)
|
|
staff_panel.apply_button.pressed.emit()
|
|
assigned = true
|
|
break
|
|
_expect(assigned, "reception can be selected through the assignment dropdown")
|
|
await _advance(2)
|
|
_expect(_runtime.staff_service.get_staff(staff_members[0].staff_id).assigned_facility_id == reception_id, "UI applies the receptionist assignment to the simulation")
|
|
_expect(_guide.get_progress().get(&"stage") == &"sale", "guide recognizes accessible rooms and staffing")
|
|
_main.call("_clear_active_tool")
|
|
_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)
|
|
var summaries: Array = _runtime.operations_coordinator.get_day_summaries()
|
|
_expect(summaries.size() == 1, "next 06:00 creates one authoritative operating-day close")
|
|
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)
|
|
if entry.category == &"SERVICE_SALE" and facility != null and facility.definition_id == &"standard_room":
|
|
lodging_sales += entry.amount
|
|
_expect(lodging_sales > 0, "naturally arriving guests complete paid lodging")
|
|
if not summaries.is_empty():
|
|
var first: Dictionary = summaries[0]
|
|
_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(_guide.get_progress().get(&"stage") == &"complete", "first lodging and following close complete opening guidance")
|
|
var digest: String = _runtime.latest_digest
|
|
_guide.refresh()
|
|
_ledger.refresh()
|
|
_expect(_runtime.latest_digest == digest, "guide and ledger refresh never mutate simulation")
|
|
_main.call("_activate_management_mode", &"ledger")
|
|
await _capture("first-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"})
|
|
_expect(bool(saved.get(&"ok", false)), "first operating day saves to a dedicated test slot: %s" % saved)
|
|
var close_before: Dictionary = _ledger.get_report()[&"selected_close"]
|
|
var restored: Dictionary = _runtime.load_from_path(save_path)
|
|
_expect(bool(restored.get(&"ok", false)), "first operating day can be restored: %s" % restored.get(&"code", &"UNKNOWN"))
|
|
# Coordinator snapshots intentionally normalize StringName keys and values
|
|
# 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())
|
|
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")
|
|
DirAccess.make_dir_recursive_absolute(evidence_path.get_base_dir())
|
|
var evidence_file := FileAccess.open(evidence_path, FileAccess.WRITE)
|
|
if evidence_file != null:
|
|
evidence_file.store_string(JSON.stringify(evidence, "\t") + "\n")
|
|
SaveService.delete_save(save_path)
|
|
_finish()
|
|
|
|
|
|
func _build(definition_id: StringName, origin: Vector3i) -> bool:
|
|
var before: int = _runtime.construction_service.get_jobs().size()
|
|
_main.call("_select_build_definition", definition_id)
|
|
_main.call("_on_map_cell_clicked", origin)
|
|
var built: bool = _runtime.construction_service.get_jobs().size() == before + 1
|
|
_expect(built, "UI starts %s at %s" % [definition_id, origin])
|
|
return built
|
|
|
|
|
|
func _advance(minutes: int) -> void:
|
|
for index: int in maxi(minutes, 0):
|
|
var before: int = _clock.absolute_minute
|
|
_clock.advance_minutes(1)
|
|
if _clock.absolute_minute != before + 1:
|
|
_expect(false, "normal operations must advance: %s" % _runtime.get_operations_report())
|
|
return
|
|
if index % 120 == 119:
|
|
print("OPENING_PROGRESS ", _clock.formatted_time())
|
|
await process_frame
|
|
|
|
|
|
func _capture(suffix: String) -> void:
|
|
if OS.get_environment("THE_TREE_OPENING_CAPTURE") != "1":
|
|
return
|
|
_main.call("_refresh_dashboard")
|
|
for frame: int in 5:
|
|
await process_frame
|
|
await RenderingServer.frame_post_draw
|
|
var image := root.get_texture().get_image()
|
|
var path := ProjectSettings.globalize_path("res://artifacts/first-operating-day/%s.png" % suffix)
|
|
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
|
_expect(image != null and image.save_png(path) == OK, "captures %s UI" % suffix)
|
|
|
|
|
|
func _expect(condition: bool, message: String) -> void:
|
|
_checks += 1
|
|
if not condition:
|
|
_failures += 1
|
|
push_error("FAIL: %s" % message)
|
|
|
|
|
|
func _finish() -> void:
|
|
print("First operating day: %d checks, %d failures" % [_checks, _failures])
|
|
quit(1 if _failures > 0 else 0)
|