Enable first operating day with staff assignments, opening guide, and ledger

This commit is contained in:
Codex
2026-09-09 08:26:20 +09:00
parent 423fd6dd2e
commit 29a660aed0
28 changed files with 2451 additions and 156 deletions
+176
View File
@@ -0,0 +1,176 @@
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)
+1
View File
@@ -0,0 +1 @@
uid://b0lupcx1bx00j
+4 -4
View File
@@ -1,7 +1,6 @@
extends SceneTree
const MANUAL_SAVE_PATH := "user://saves/manual_ui"
var _manual_save_path := "user://tests/main_dashboard_%d/manual_ui" % OS.get_process_id()
var _checks := 0
var _failures := 0
var _main: Control
@@ -31,6 +30,7 @@ func _run() -> void:
"project uses DPI-aware canvas scaling for high-resolution displays"
)
_main = load("res://scenes/main/main.tscn").instantiate() as Control
_main.set("manual_save_path", _manual_save_path)
root.add_child(_main)
await process_frame
await process_frame
@@ -150,7 +150,7 @@ func _run() -> void:
if is_instance_valid(_main):
_main.queue_free()
SaveService.delete_save(MANUAL_SAVE_PATH)
SaveService.delete_save(_manual_save_path)
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
@@ -416,7 +416,7 @@ func _promote_rank_two() -> bool:
func _test_manual_save_and_load() -> void:
SaveService.delete_save(MANUAL_SAVE_PATH)
SaveService.delete_save(_manual_save_path)
_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")
+287
View File
@@ -0,0 +1,287 @@
extends SceneTree
## Real Runtime commands through the staff panel controls, including queued
## assignment, draft preservation, a rejected role, and pending Save restoration.
var _checks := 0
var _failures := 0
var _main: Control
var _panel: StaffManagementPanel
var _runtime: Variant
var _clock: Variant
var _receptions: Array[StringName] = []
var _kitchen: StringName = &""
var _save_path := ""
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, 824242)
_clock.set_paused(false)
root.size = Vector2i(1280, 720)
_main = load("res://scenes/main/main.tscn").instantiate() as Control
root.add_child(_main)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
_expect(_clock.paused, "a truly new opening pauses before the first construction")
await process_frame
await process_frame
_panel = _main.find_child("StaffManagementPanel", true, false) as StaffManagementPanel
_expect(_panel != null, "normal UI includes a separate staff management panel")
if _panel == null:
_finish()
return
for request: Array in [
[&"reception", Vector3i(-8, 0, 0)],
[&"reception", Vector3i(2, 0, 0)],
[&"kitchen", Vector3i(2, 0, 1)],
]:
_main.call("_select_build_definition", request[0])
_main.call("_on_map_cell_clicked", request[1])
_clock.advance_minutes(180)
for facility: FacilityInstance in _runtime.facility_service.get_instances():
if facility.definition_id == &"reception":
_receptions.append(facility.instance_id)
elif facility.definition_id == &"kitchen":
_kitchen = facility.instance_id
_expect(_receptions.size() == 2 and not _kitchen.is_empty(), "three real facilities are ready for assignment tests")
if _receptions.size() != 2 or _kitchen.is_empty():
_finish()
return
_main.call("_unhandled_key_input", _key(KEY_H))
_expect((_main.get("staff_candidate_panel") as Control).visible, "H opens the unchanged hiring candidates")
for candidate: StringName in [&"staff_akane", &"staff_bardo"]:
var hire := (_main.get("staff_candidate_buttons") as Dictionary).get(candidate) as Button
hire.pressed.emit()
(_main.get("staff_management_toggle_button") as Button).pressed.emit()
_expect(_panel.visible and not (_main.get("staff_candidate_panel") as Control).visible,
"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")
_panel.select_staff(&"staff_akane")
_choose_facility(_receptions[0])
_panel.apply_button.pressed.emit()
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0],
"paused Apply assigns the receptionist to the chosen completed facility")
_expect("変更しました" in _panel.result_label.text and "毎日" in _panel.staff_detail_label.text,
"assignment success and recurring schedule are visible in Japanese")
_test_rejected_assignment()
_test_queued_assignment_preserves_other_draft()
_test_save_pending_assignment()
_test_guide_respects_current_assignments()
await _test_navigation_and_layout()
await _test_restored_opening_does_not_pause()
_finish()
func _test_rejected_assignment() -> void:
var before: Dictionary = _assignment(&"staff_akane").duplicate(true)
_choose_facility(_kitchen)
_panel.apply_button.pressed.emit()
_expect(_assignment(&"staff_akane") == before, "wrong-role assignment never mutates the existing reception schedule")
_expect("役割" in _panel.result_label.text and "役割" in (_main.get("build_status_label") as Label).text,
"rejected assignment shows its actionable Japanese reason in both panel and status")
_expect(_chosen_facility() == _kitchen, "a rejected assignment preserves the attempted facility for correction")
func _test_queued_assignment_preserves_other_draft() -> void:
_choose_facility(_receptions[1])
_clock.set_paused(false)
_panel.apply_button.pressed.emit()
var queued: Array = _runtime.command_queue.create_snapshot().get("pending", [])
_expect(queued.size() == 1 and _panel.apply_button.disabled, "running-clock Apply queues one assignment and disables repeated submission")
_panel.submit_assignment()
_expect((_runtime.command_queue.create_snapshot().get("pending", []) as Array).size() == 1,
"repeated submit cannot create a second assignment for the same employee")
_panel.select_staff(&"staff_bardo")
_choose_facility(_receptions[0])
_panel.shift_option.select(1)
_panel.shift_option.item_selected.emit(1)
_clock.advance_minutes(1)
_clock.set_paused(true)
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[1], "queued reassignment commits on the command tick")
_expect(_panel.selected_staff_id == &"staff_bardo" and _chosen_facility() == _receptions[0]
and _panel.shift_option.selected == 1, "another employee's pending completion preserves the active facility and night-shift draft")
_panel.apply_button.pressed.emit()
_expect(_assignment(&"staff_bardo").get(&"template_id") == &"NIGHT", "night selector creates an actual recurring night schedule")
_expect("18:00" in _panel.result_label.text and "反映" in _panel.staff_detail_label.text,
"future night shift clearly reports when it begins")
_panel.select_staff(&"staff_akane")
_choose_facility(&"")
_panel.apply_button.pressed.emit()
_expect(StringName(_assignment(&"staff_akane").get(&"facility_id", &"missing")).is_empty(),
"unassigned option removes the facility duty through Runtime")
func _test_save_pending_assignment() -> void:
_choose_facility(_receptions[0])
_clock.set_paused(false)
_panel.apply_button.pressed.emit()
_save_path = "user://tests/staff-management-%d" % OS.get_process_id()
var saved: Dictionary = _runtime.save_to_path(_save_path)
_expect(bool(saved.get(&"ok", false)), "pending UI assignment saves to an isolated test slot")
_clock.advance_minutes(1)
var restored: Dictionary = _runtime.load_from_path(_save_path)
_expect(bool(restored.get(&"ok", false)), "pending UI assignment can be restored")
_panel.refresh()
_expect(_panel.apply_button.disabled and not (_panel.get("_pending_commands") as Dictionary).is_empty(),
"restored command metadata restores disabled pending controls")
var sequence_before := int(_panel.get("_sequence"))
_clock.advance_minutes(1)
_clock.set_paused(true)
_panel.refresh()
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0]
and (_panel.get("_pending_commands") as Dictionary).is_empty(),
"restored assignment completes once and releases its UI pending state")
_choose_facility(_receptions[1])
_panel.apply_button.pressed.emit()
_expect(int(_panel.get("_sequence")) > sequence_before
and _assignment(&"staff_akane").get(&"facility_id") == _receptions[1],
"post-load Apply uses a fresh command key and changes the real assignment")
func _test_guide_respects_current_assignments() -> void:
# Remove both saved plans so an unrelated receptionist cannot satisfy the
# guide while this employee's assignment is being changed.
for staff_id: StringName in [&"staff_akane", &"staff_bardo"]:
_panel.select_staff(staff_id)
_choose_facility(&"")
_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))
_clock.advance_minutes(60)
_build_guide_part(&"guest_corridor", Vector3i(-4, 2, 0))
_clock.advance_minutes(10)
for x: int in range(-8, -3):
_build_guide_part(&"guest_corridor", Vector3i(x, 3, 0))
_clock.advance_minutes(10)
var accessible_room := false
for facility: FacilityInstance in _runtime.facility_service.get_instances():
accessible_room = accessible_room or (facility.definition_id == &"standard_room" and facility.access_available)
var guide := _main.find_child("InnOpeningGuide", true, false) as InnOpeningGuide
_expect(accessible_room and guide.get_progress().get(&"stage") == &"staff",
"real accessible room still requires a reception assignment before first sale")
_main.call("_activate_management_mode", &"staff")
_panel.select_staff(&"staff_akane")
_choose_facility(_receptions[0])
_panel.shift_option.select(0)
_panel.shift_option.item_selected.emit(0)
_panel.apply_button.pressed.emit()
_expect(guide.get_progress().get(&"stage") == &"sale",
"day assignment advances the accessible inn to waiting for its first sale")
_choose_facility(&"")
_panel.apply_button.pressed.emit()
var historical_reception_shift := false
for shift: ShiftInstance in _runtime.staff_service.get_shifts():
if shift.staff_id == &"staff_akane" and shift.assigned_facility_id == _receptions[0]:
historical_reception_shift = historical_reception_shift or shift.cancelled or shift.end_at <= _clock.absolute_minute
_expect(historical_reception_shift and guide.get_progress().get(&"stage") == &"staff",
"unassigning reception returns guidance to staffing despite past or cancelled reception shifts")
_choose_facility(_receptions[0])
_panel.shift_option.select(1)
_panel.shift_option.item_selected.emit(1)
_panel.apply_button.pressed.emit()
var night_plan: Dictionary = _assignment(&"staff_akane")
var reception: FacilityInstance = _runtime.facility_service.get_instance(_receptions[0])
_expect(night_plan.get(&"template_id") == &"NIGHT" and int(night_plan.get(&"effective_from", -1)) > _clock.absolute_minute
and reception.active_staff_ids.is_empty(), "night assignment is genuinely future duty with nobody staffing reception during daytime")
_expect(guide.get_progress().get(&"stage") == &"sale",
"saved future night duty satisfies staffing guidance before its shift starts")
func _build_guide_part(definition_id: StringName, origin: Vector3i) -> void:
var before: int = _runtime.construction_service.get_jobs().size()
_main.call("_select_build_definition", definition_id)
_main.call("_on_map_cell_clicked", origin)
_expect(_runtime.construction_service.get_jobs().size() == before + 1,
"guide fixture builds %s through UI at %s" % [definition_id, origin])
func _test_navigation_and_layout() -> void:
_main.call("_clear_active_tool")
_expect(not _panel.visible, "Escape behavior closes the hired staff pane")
_main.call("_activate_management_mode", &"ledger")
var ledger := _main.find_child("OperatingLedgerPanel", true, false) as Control
_expect(ledger.visible and not (_main.get("operations_controls") as Control).visible,
"ledger navigation displays the actual ledger while hiding assignment controls")
_main.call("_unhandled_key_input", _key(KEY_H))
_expect(not ledger.visible and (_main.get("staff_candidate_panel") as Control).visible,
"H returns from the ledger to hiring candidates")
_main.call("_activate_management_mode", &"staff")
await process_frame
await process_frame
var guide := _main.find_child("InnOpeningGuide", true, false) as Control
var side := _main.find_child("SidePanel", true, false) as Control
_expect(not guide.get_global_rect().intersects(side.get_global_rect()),
"1280x720 keeps opening guidance and staff inspector apart")
_expect(_panel.get_global_rect().position.x >= side.get_global_rect().position.x
and _panel.get_global_rect().end.x <= side.get_global_rect().end.x,
"staff roster and assignment controls fit the compact sidebar width")
if OS.get_environment("THE_TREE_STAFF_UI_CAPTURE") == "1":
await RenderingServer.frame_post_draw
root.get_texture().get_image().save_png("/tmp/the-tree-staff-management.png")
func _test_restored_opening_does_not_pause() -> void:
_main.queue_free()
await process_frame
_clock.reset(1, 6, 0)
var state: Variant = root.get_node("GameState")
state.reset()
_runtime.initialize(_clock, state, 824242)
_clock.set_paused(false)
var snapshot: Dictionary = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(snapshot), "restored opening fixture accepts its empty initial save")
_main = load("res://scenes/main/main.tscn").instantiate() as Control
root.add_child(_main)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
_expect(not _clock.paused and int(_clock.absolute_minute) == 360,
"mounting a restored empty opening preserves its saved running state and time")
_clock.set_paused(true)
func _assignment(staff_id: StringName) -> Dictionary:
return _runtime.staff_service.get_work_assignment(staff_id)
func _choose_facility(facility_id: StringName) -> void:
for index: int in _panel.facility_option.item_count:
if StringName(_panel.facility_option.get_item_metadata(index)) == facility_id:
_panel.facility_option.select(index)
_panel.facility_option.item_selected.emit(index)
return
_expect(false, "requested facility is available in the UI: %s" % facility_id)
func _chosen_facility() -> StringName:
return StringName(_panel.facility_option.get_item_metadata(_panel.facility_option.selected))
func _key(code: Key) -> InputEventKey:
var event := InputEventKey.new()
event.keycode = code
event.pressed = true
return event
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
func _finish() -> void:
if not _save_path.is_empty():
SaveService.delete_save(_save_path)
if is_instance_valid(_main):
_main.queue_free()
print("Staff management UI: %d checks, %d failures" % [_checks, _failures])
quit(1 if _failures > 0 else 0)
+1
View File
@@ -0,0 +1 @@
uid://b3t73d1gjiac7