Enable first operating day with staff assignments, opening guide, and ledger
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
extends SceneTree
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
var _clock: Variant
|
||||
var _state: Variant
|
||||
var _runtime: Variant
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("P1: Staff reassignment, repeating shifts, and save compatibility")
|
||||
_test_service_shift_history_and_recurrence()
|
||||
_test_night_boundary_and_unassignment()
|
||||
_test_service_snapshot_guards()
|
||||
_test_runtime_assignment_boundary()
|
||||
_cleanup()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d / %d checks" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _worker(service: StaffService, id: StringName = &"worker") -> StaffRuntime:
|
||||
_expect(service.create_staff(id, &"receptionist", {&"daily_wage": 120}).is_success(), "test employee registers")
|
||||
return service.get_staff(id)
|
||||
|
||||
|
||||
func _test_service_shift_history_and_recurrence() -> void:
|
||||
var service := StaffService.new()
|
||||
var worker := _worker(service)
|
||||
service.advance_one_minute(360)
|
||||
_expect(service.set_work_assignment(worker.staff_id, &"west", &"DAY").is_success(), "first assignment starts a recurring DAY shift")
|
||||
var first_id := worker.shift_id
|
||||
for minute: int in range(361, 600):
|
||||
service.advance_one_minute(minute)
|
||||
service.advance_one_minute(600)
|
||||
var total_before := service.get_shift(first_id).worked_minutes
|
||||
var result := service.set_work_assignment(worker.staff_id, &"east", &"DAY")
|
||||
_expect(result.is_success() and int(result.details[&"effective_from"]) == 600, "daytime reassignment takes effect now")
|
||||
var first := service.get_shift(first_id)
|
||||
var second := service.get_shift(worker.shift_id)
|
||||
_expect(first.assigned_facility_id == &"west" and first.start_at == 360 and first.end_at == 600 and first.actual_ended_at == 600, "elapsed work retains its original facility and exact half-open interval")
|
||||
_expect(second.assigned_facility_id == &"east" and second.start_at == 600 and second.end_at == 1080, "replacement starts exactly at the split boundary")
|
||||
_expect(first.worked_minutes + second.worked_minutes == total_before, "reassignment neither duplicates nor loses the current duty minute")
|
||||
_expect(worker.current_cell == Vector3i.ZERO, "assignment never teleports the employee")
|
||||
var stable := service.create_snapshot()
|
||||
result = service.set_work_assignment(worker.staff_id, &"east", &"DAY")
|
||||
_expect(result.details[&"reason"] == &"ASSIGNMENT_UNCHANGED" and service.create_snapshot() == stable, "same plan replay is an exact no-op")
|
||||
for minute: int in range(601, 1081):
|
||||
service.advance_one_minute(minute)
|
||||
_expect(worker.state == StaffRuntime.State.OFF_DUTY and worker.assigned_facility_id.is_empty(), "DAY ends at 18:00 and clears active facility")
|
||||
_expect(first.worked_minutes + second.worked_minutes == 720, "all 720 daytime minutes remain attributed exactly once")
|
||||
var historical := first.create_snapshot()
|
||||
for minute: int in range(1081, 3241):
|
||||
service.advance_one_minute(minute)
|
||||
_expect(worker.state == StaffRuntime.State.AVAILABLE and worker.assigned_facility_id == &"east", "the same assignment works on the third day without another command")
|
||||
_expect(service.get_shift(first_id).create_snapshot() == historical, "later daily shifts leave historical work unchanged")
|
||||
_expect(service.validate_invariants()[&"ok"], "repeating shifts preserve the Staff graph")
|
||||
var allocation := _wage_allocation(service, 0, 1440)
|
||||
_expect(int(allocation[&"allocated_total"]) == 120 and int(allocation[&"unallocated_total"]) == 0, "split daily wages are allocated once with no lost amount")
|
||||
_expect(allocation[&"allocated_by_facility"] == {&"west": 40, &"east": 80}, "historical wages follow the 240/480 minute split after later days run")
|
||||
|
||||
|
||||
func _test_night_boundary_and_unassignment() -> void:
|
||||
var service := StaffService.new()
|
||||
var worker := _worker(service)
|
||||
service.advance_one_minute(1410)
|
||||
_expect(service.set_work_assignment(worker.staff_id, &"west", &"NIGHT").is_success(), "late-night assignment begins immediately")
|
||||
var first_id := worker.shift_id
|
||||
for minute: int in range(1411, 1451):
|
||||
service.advance_one_minute(minute)
|
||||
_expect(service.set_work_assignment(worker.staff_id, &"east", &"NIGHT").is_success(), "NIGHT reassignment works after midnight")
|
||||
var first := service.get_shift(first_id)
|
||||
var next := service.get_shift(worker.shift_id)
|
||||
_expect(first.start_at == 1410 and first.end_at == 1450 and next.start_at == 1450 and next.end_at == 1800, "midnight split keeps one exact historical interval and the remaining night")
|
||||
_expect(first.worked_minutes + next.worked_minutes == 41, "cross-midnight reassignment preserves recorded duty total")
|
||||
_expect(_wage_allocation(service, 0, 1440)[&"allocated_by_facility"] == {&"west": 120}, "reassignment after midnight does not move the prior day's wage expense")
|
||||
var restored := StaffService.new()
|
||||
_expect(restored.restore_snapshot(service.create_snapshot()), "a split NIGHT snapshot restores")
|
||||
for minute: int in range(1451, 2521):
|
||||
service.advance_one_minute(minute)
|
||||
restored.advance_one_minute(minute)
|
||||
_expect(service.create_snapshot() == restored.create_snapshot(), "save replay reaches the next NIGHT deterministically")
|
||||
_expect(worker.state == StaffRuntime.State.AVAILABLE and worker.assigned_facility_id == &"east", "next evening starts a new NIGHT automatically")
|
||||
var day_result := service.set_work_assignment(worker.staff_id, &"east", &"DAY")
|
||||
_expect(int(day_result.details[&"effective_from"]) == 3240 and worker.state == StaffRuntime.State.OFF_DUTY, "changing night to DAY waits for the next 06:00")
|
||||
_expect(service.set_work_assignment(worker.staff_id, &"", &"DAY").is_success(), "empty facility explicitly unassigns")
|
||||
service.advance_one_minute(4680)
|
||||
_expect(worker.assigned_facility_id.is_empty() and worker.shift_id.is_empty(), "unassigned Staff never return to an old future shift")
|
||||
_expect(service.get_daily_payroll_total() == 120, "unassigning does not silently dismiss Staff or remove their wage")
|
||||
_expect(service.remove_staff(worker.staff_id).is_success() and service.get_work_assignments().is_empty(), "staff removal cleans the recurring plan")
|
||||
|
||||
|
||||
func _wage_allocation(service: StaffService, start: int, end: int) -> Dictionary:
|
||||
var allocator = load("res://autoload/simulation_runtime.gd").new()
|
||||
allocator.staff_service = service
|
||||
for id: StringName in [&"west", &"east"]:
|
||||
allocator.facility_service.create_instance({&"instance_id": id, &"definition_id": &"reception"})
|
||||
var result: Dictionary = allocator._allocate_operations_wages_to_facilities(start, end, 120, [{&"staff_id": &"worker", &"new_wages": 120}], {})
|
||||
allocator.free()
|
||||
return result
|
||||
|
||||
|
||||
func _test_service_snapshot_guards() -> void:
|
||||
var service := StaffService.new()
|
||||
_worker(service)
|
||||
service.advance_one_minute(400)
|
||||
service.set_work_assignment(&"worker", &"west", &"DAY")
|
||||
var current := service.create_snapshot()
|
||||
var restored := StaffService.new()
|
||||
_expect(restored.restore_snapshot(current) and restored.create_snapshot() == current, "current recurring-plan snapshot round trip is exact")
|
||||
var legacy := current.duplicate(true)
|
||||
legacy[&"version"] = 3
|
||||
legacy.erase(&"work_assignments")
|
||||
_expect(restored.restore_snapshot(legacy), "pre-roster v3 snapshot migrates")
|
||||
_expect(restored.get_work_assignments().is_empty() and restored.get_shifts().size() == service.get_shifts().size(), "legacy migration preserves concrete shifts without inventing a plan")
|
||||
var pristine := restored.create_snapshot()
|
||||
for change: Dictionary in [
|
||||
{&"staff_id": "missing"}, {&"template_id": "FLEX"},
|
||||
{&"effective_from": 1}, {&"next_start_at": 1}, {&"changed_at": 99999},
|
||||
{&"next_start_at": 1800 + 1440 * 10},
|
||||
{&"facility_id": 17}, {&"unexpected": true},
|
||||
]:
|
||||
var bad := current.duplicate(true)
|
||||
for key: Variant in change:
|
||||
bad[&"work_assignments"][0][key] = change[key]
|
||||
_expect(not restored.restore_snapshot(bad) and restored.create_snapshot() == pristine, "malformed plan rejects atomically: %s" % change)
|
||||
var downgrade := current.duplicate(true)
|
||||
downgrade[&"version"] = 3
|
||||
_expect(not restored.restore_snapshot(downgrade), "outer-only downgrade cannot hide a roster")
|
||||
|
||||
|
||||
func _test_runtime_assignment_boundary() -> void:
|
||||
_clock = load("res://autoload/game_clock.gd").new()
|
||||
_state = load("res://autoload/game_state.gd").new()
|
||||
_runtime = load("res://autoload/simulation_runtime.gd").new()
|
||||
_clock.reset(1, 6, 0)
|
||||
_state.reset()
|
||||
_expect(_runtime.initialize(_clock, _state, 91003)[&"valid"], "normal Runtime initializes")
|
||||
_clock.set_paused(true)
|
||||
for origin: Vector3i in [Vector3i(-8, 0, 0), Vector3i(2, 0, 0)]:
|
||||
_expect(_runtime.submit_command(&"build_facility", {&"definition_id": &"reception", &"origin": origin}).is_success(), "reception is built through the normal command")
|
||||
_clock.advance_minutes(120)
|
||||
var buildings: Array = _runtime.facility_service.get_instances()
|
||||
_expect(buildings.size() == 2, "both receptions complete")
|
||||
if buildings.size() != 2:
|
||||
return
|
||||
var west: StringName = buildings[0].instance_id
|
||||
var east: StringName = buildings[1].instance_id
|
||||
for id: StringName in [&"staff_akane", &"staff_bardo", &"staff_chika"]:
|
||||
_expect(_runtime.submit_command(&"hire_staff_candidate", {&"candidate_id": id}).is_success(), "catalog employee is hired: %s" % id)
|
||||
var money_before: int = _runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT)
|
||||
_expect(_assign(&"staff_akane", west, &"DAY").is_success(), "new command assigns hired receptionist")
|
||||
_expect(buildings[0].active_staff_ids == [&"staff_akane"] and buildings[0].state == FacilityInstance.State.OPEN, "paused assignment immediately opens the facility")
|
||||
var old_cell: Vector3i = _runtime.staff_service.get_staff(&"staff_akane").current_cell
|
||||
var before: Dictionary = _runtime.staff_service.create_snapshot()
|
||||
for row: Array in [
|
||||
[&"absent", west, &"DAY", &"STAFF_NOT_FOUND"],
|
||||
[&"staff_akane", &"absent", &"DAY", &"FACILITY_NOT_FOUND"],
|
||||
[&"staff_chika", west, &"DAY", &"ROLE_MISMATCH"],
|
||||
[&"staff_akane", west, &"FLEX", &"INVALID_TEMPLATE"],
|
||||
]:
|
||||
var result = _assign(row[0], row[1], row[2])
|
||||
_expect(not result.is_success() and result.details.get(&"reason") == row[3], "invalid assignment explains: %s" % row[3])
|
||||
_expect(_runtime.staff_service.create_snapshot() == before, "rejected command leaves Staff and schedule unchanged")
|
||||
_runtime.path_service.set_cell_temporary_block(old_cell, true)
|
||||
var blocked = _assign(&"staff_akane", east, &"DAY")
|
||||
_expect(not blocked.is_success() and blocked.details.get(&"reason") == &"NO_ROUTE", "inaccessible assignment is rejected")
|
||||
_expect(_runtime.staff_service.create_snapshot() == before, "route failure preserves the old assignment")
|
||||
_runtime.path_service.set_cell_temporary_block(old_cell, false)
|
||||
_clock.advance_minutes(20)
|
||||
_expect(_assign(&"staff_akane", east, &"DAY").is_success(), "employee can move to another compatible facility")
|
||||
_expect(buildings[0].active_staff_ids.is_empty() and buildings[1].active_staff_ids == [&"staff_akane"], "reassignment removes the old service staffing and activates the new facility")
|
||||
_expect(_runtime.staff_service.get_staff(&"staff_akane").current_cell == old_cell, "Runtime assignment leaves Path position intact")
|
||||
_expect(_runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT) == money_before, "assignment and shift changes charge no second signing fee")
|
||||
var report: Dictionary = _runtime.get_staff_management_report()
|
||||
_expect(report[&"staff"][0][&"recurring"] and report[&"staff"][0][&"assigned_facility_id"] == east, "management report exposes the committed recurring plan")
|
||||
report[&"staff"][0][&"assigned_facility_id"] = &"tampered"
|
||||
_expect(_runtime.staff_service.get_work_assignment(&"staff_akane")[&"facility_id"] == String(east), "management report is a defensive copy")
|
||||
_expect(_assign(&"staff_bardo", east, &"NIGHT").is_success(), "another employee can cover the opposite shift")
|
||||
var snapshot: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.restore_snapshot(snapshot), "Runtime save validates repeating assignments and split historical work")
|
||||
_clock.advance_minutes(1440)
|
||||
var replay_expected: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.restore_snapshot(snapshot), "Runtime rewinds to recurring-plan checkpoint")
|
||||
_clock.advance_minutes(1440)
|
||||
_expect(_runtime.create_snapshot() == replay_expected, "next-day scheduling and actor projection replay exactly after restore")
|
||||
var saved_staff: Dictionary = _runtime.staff_service.create_snapshot()
|
||||
var bad_snapshot: Dictionary = _runtime.create_snapshot().duplicate(true)
|
||||
bad_snapshot[&"systems"][&"runtime"][&"staff"][&"work_assignments"][0][&"facility_id"] = "missing"
|
||||
_expect(not _runtime.restore_snapshot(bad_snapshot), "Runtime rejects a plan targeting a missing facility")
|
||||
_expect(_runtime.staff_service.create_snapshot() == saved_staff, "invalid Runtime roster restore is atomic")
|
||||
var remove = _runtime.submit_command(&"demolish_facility", {&"instance_id": east})
|
||||
_expect(not remove.is_success() and remove.details.get(&"reason") == &"STAFF_ASSIGNED", "demolition requires releasing saved staff assignments")
|
||||
var queued_at: int = _clock.absolute_minute
|
||||
_clock.set_paused(false)
|
||||
var queued = _assign(&"staff_akane", west, &"DAY")
|
||||
_expect(queued.is_success(), "running game accepts the assignment into its command queue")
|
||||
_clock.advance_one_minute()
|
||||
_clock.set_paused(true)
|
||||
var applied: Dictionary = _runtime.staff_service.get_work_assignment(&"staff_akane")
|
||||
var applied_shift: ShiftInstance = _runtime.staff_service.get_shift(_runtime.staff_service.get_staff(&"staff_akane").shift_id)
|
||||
_expect(int(applied[&"changed_at"]) == queued_at + 1 and int(applied[&"effective_from"]) == queued_at + 1, "queued assignment uses the execution minute, not the previous Staff tick")
|
||||
_expect(applied_shift.worked_minutes == 1, "command and AI phases record the execution minute only once")
|
||||
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "queued assignment leaves a coherent save at the end of its tick")
|
||||
_test_active_service_reassignment(west, east)
|
||||
_test_busy_staff_and_capacity(west)
|
||||
|
||||
|
||||
func _test_active_service_reassignment(west: StringName, east: StringName) -> void:
|
||||
_expect(_assign(&"staff_akane", east, &"DAY").is_success(), "service test keeps receptionist on DAY")
|
||||
var spawned = _runtime.submit_command(&"spawn_guest", {
|
||||
&"guest_id": &"reassignment_guest", &"guest_type_id": &"traveler", &"gold": 100,
|
||||
&"options": {&"state": GuestRuntime.State.DECIDING, &"current_cell": Vector3i.ZERO},
|
||||
})
|
||||
_expect(spawned.is_success(), "a real Guest and wallet enter the service test")
|
||||
var request = _runtime.submit_command(&"create_service_request", {
|
||||
&"facility_id": east, &"guest_ids": [&"reassignment_guest"], &"service_type": &"check_in",
|
||||
&"options": {&"service_minutes": 30},
|
||||
})
|
||||
_expect(request.is_success(), "Guest enters reception through normal request ownership")
|
||||
_clock.advance_one_minute()
|
||||
var active: Array = _runtime.facility_service.get_active_services(east)
|
||||
_expect(active.size() == 1, "reception begins the Guest service")
|
||||
if active.size() != 1:
|
||||
return
|
||||
var service: ServiceInstance = active[0]
|
||||
var balance_before: int = _runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT)
|
||||
_expect(_assign(&"staff_akane", west, &"DAY").is_success(), "reassignment during facility service is allowed")
|
||||
_clock.advance_one_minute()
|
||||
_expect(service.state == ServiceInstance.State.WAITING_FOR_STAFF, "existing service enters the normal staff-replacement grace period")
|
||||
_expect(_runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT) == balance_before, "waiting service posts no phantom revenue")
|
||||
_expect(_assign(&"staff_akane", east, &"DAY").is_success(), "replacement can return during grace period")
|
||||
_clock.advance_one_minute()
|
||||
_expect(service.state == ServiceInstance.State.ACTIVE, "staff return resumes the same service instead of duplicating it")
|
||||
_expect(_assign(&"staff_akane", &"", &"DAY").is_success(), "employee can be unassigned during service")
|
||||
_clock.advance_minutes(6)
|
||||
_expect(service.state == ServiceInstance.State.FAILED, "uncovered service fails at the existing five-minute grace deadline")
|
||||
_expect(_runtime.economy_service.get_balance(_runtime.PLAYER_CASH_ACCOUNT) == balance_before, "failed uncovered service never posts completed-sale revenue")
|
||||
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "Guest, service, staff, and repeating plan save consistently after interruption")
|
||||
|
||||
|
||||
func _test_busy_staff_and_capacity(west: StringName) -> void:
|
||||
_expect(_assign(&"staff_akane", west, &"DAY").is_success(), "busy-task test starts with a working employee")
|
||||
var task = _runtime.submit_command(&"create_staff_task", {
|
||||
&"task_id": &"assignment_guard_task", &"task_type": &"reception", &"priority": 99,
|
||||
&"options": {&"target_cell": Vector3i.ZERO, &"required_role_tags": [&"receptionist"],
|
||||
&"task_tags": [&"reception"], &"estimated_minutes": 20},
|
||||
})
|
||||
_expect(task.is_success(), "staff task enters normal dispatcher")
|
||||
_clock.advance_one_minute()
|
||||
var before: Dictionary = _runtime.staff_service.create_snapshot()
|
||||
var busy = _assign(&"staff_akane", &"", &"DAY")
|
||||
_expect(not busy.is_success() and busy.details.get(&"reason") == &"STAFF_BUSY", "active task blocks a schedule edit with an explicit reason")
|
||||
_expect(_runtime.staff_service.create_snapshot() == before, "busy rejection preserves Task ownership and progress")
|
||||
_expect(_runtime.staff_service.cancel_task(&"assignment_guard_task", &"TEST_COMPLETE", _clock.absolute_minute).is_success(), "task releases its employee through the service boundary")
|
||||
_expect(_assign(&"staff_bardo", west, &"DAY").is_success(), "second receptionist shares the day shift")
|
||||
for id: StringName in [&"capacity_extra", &"capacity_overflow"]:
|
||||
_expect(_runtime.submit_command(&"register_staff", {&"staff_id": id, &"role_id": &"receptionist"}).is_success(), "additional employee registers for the capacity boundary")
|
||||
_expect(_assign(&"capacity_extra", west, &"DAY").is_success(), "third receptionist reaches the exact staffing limit")
|
||||
before = _runtime.staff_service.create_snapshot()
|
||||
var overflow = _assign(&"capacity_overflow", west, &"DAY")
|
||||
_expect(not overflow.is_success() and overflow.details.get(&"reason") == &"FACILITY_FULL", "fourth simultaneous receptionist is rejected")
|
||||
_expect(_runtime.staff_service.create_snapshot() == before, "capacity rejection leaves all assignments unchanged")
|
||||
_expect(_assign(&"capacity_overflow", west, &"NIGHT").is_success(), "opposite shift has its own staffing capacity")
|
||||
|
||||
|
||||
func _assign(staff_id: StringName, facility_id: StringName, template_id: StringName) -> CommandResult:
|
||||
return _runtime.submit_command(&"set_staff_work_assignment", {&"staff_id": staff_id, &"facility_id": facility_id, &"template_id": template_id})
|
||||
|
||||
|
||||
func _cleanup() -> void:
|
||||
for node: Variant in [_runtime, _clock, _state]:
|
||||
if node != null:
|
||||
node.free()
|
||||
_runtime = null
|
||||
_clock = null
|
||||
_state = null
|
||||
|
||||
|
||||
func _expect(condition: bool, description: String) -> void:
|
||||
_checks += 1
|
||||
if not condition:
|
||||
_failures += 1
|
||||
push_error(description)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cedumilwe1o4f
|
||||
@@ -208,6 +208,7 @@ func _test_snapshot_contract() -> void:
|
||||
|
||||
var pre_effective_v2 := snapshot.duplicate(true)
|
||||
pre_effective_v2[&"version"] = 2
|
||||
pre_effective_v2.erase(&"work_assignments")
|
||||
for staff_snapshot: Dictionary in pre_effective_v2[&"staff"]:
|
||||
staff_snapshot.erase(&"snapshot_version")
|
||||
staff_snapshot.erase(&"employment_effective_minute")
|
||||
@@ -215,13 +216,14 @@ func _test_snapshot_contract() -> void:
|
||||
_expect(migrated_v2.restore_snapshot(pre_effective_v2), "pre-effective-minute v2 Staff snapshot migrates")
|
||||
_expect(migrated_v2.get_staff(&"a").employment_effective_minute == 0, "v2 legacy Staff employment starts at minute zero")
|
||||
_expect(
|
||||
migrated_v2.create_snapshot()[&"version"] == 3
|
||||
migrated_v2.create_snapshot()[&"version"] == StaffServiceType.SNAPSHOT_VERSION
|
||||
and migrated_v2.create_snapshot()[&"staff"][0][&"snapshot_version"] == 1,
|
||||
"v2 legacy Staff row is re-emitted in the current service and row schemas"
|
||||
)
|
||||
|
||||
var legacy := snapshot.duplicate(true)
|
||||
legacy[&"version"] = 1
|
||||
legacy.erase(&"work_assignments")
|
||||
legacy.erase(&"last_advanced_minute")
|
||||
legacy.erase(&"payroll_settlements")
|
||||
for staff_snapshot: Dictionary in legacy[&"staff"]:
|
||||
|
||||
@@ -608,6 +608,7 @@ func _test_payroll_snapshot_migration_and_tamper() -> void:
|
||||
legacy_source.remove_staff(&"departed_debt", &"LEFT", true, 0)
|
||||
var legacy: Dictionary = legacy_source.create_snapshot()
|
||||
legacy[&"version"] = 1
|
||||
legacy.erase(&"work_assignments")
|
||||
legacy.erase(&"last_advanced_minute")
|
||||
legacy.erase(&"payroll_settlements")
|
||||
for staff_snapshot: Dictionary in legacy[&"staff"]:
|
||||
@@ -621,7 +622,7 @@ func _test_payroll_snapshot_migration_and_tamper() -> void:
|
||||
"v1 migration preserves active and departed unpaid wages"
|
||||
)
|
||||
var migrated_snapshot := migrated.create_snapshot()
|
||||
_expect(migrated_snapshot[&"version"] == 3 and migrated_snapshot[&"payroll_settlements"].is_empty(), "v1 migration emits canonical v3 with empty history")
|
||||
_expect(migrated_snapshot[&"version"] == StaffServiceType.SNAPSHOT_VERSION and migrated_snapshot[&"payroll_settlements"].is_empty(), "v1 migration emits canonical current schema with empty history")
|
||||
|
||||
var service := StaffServiceType.new()
|
||||
service.register_staff(_staff(&"staff_a", 10, {&"daily_wage": 100, &"unpaid_wages": 20}))
|
||||
@@ -630,19 +631,20 @@ func _test_payroll_snapshot_migration_and_tamper() -> void:
|
||||
service.apply_daily_payroll_settlement(&"snapshot.day.2", 2, {&"staff_a": 50, &"staff_b": 100})
|
||||
var snapshot: Dictionary = service.create_snapshot()
|
||||
var restored := StaffServiceType.new()
|
||||
_expect(restored.restore_snapshot(snapshot), "canonical v3 payroll history restores")
|
||||
_expect(restored.create_snapshot() == snapshot, "v3 payroll settlement history round trip is exact")
|
||||
_expect(restored.restore_snapshot(snapshot), "canonical current schema payroll history restores")
|
||||
_expect(restored.create_snapshot() == snapshot, "current payroll settlement history round trip is exact")
|
||||
var v2_snapshot: Dictionary = snapshot.duplicate(true)
|
||||
v2_snapshot[&"version"] = 2
|
||||
v2_snapshot.erase(&"work_assignments")
|
||||
for staff_snapshot: Dictionary in v2_snapshot[&"staff"]:
|
||||
staff_snapshot.erase(&"snapshot_version")
|
||||
staff_snapshot.erase(&"employment_effective_minute")
|
||||
var v2_migrated := StaffServiceType.new()
|
||||
_expect(v2_migrated.restore_snapshot(v2_snapshot), "pre-effective-minute v2 payroll history migrates")
|
||||
_expect(
|
||||
v2_migrated.create_snapshot()[&"version"] == 3
|
||||
v2_migrated.create_snapshot()[&"version"] == StaffServiceType.SNAPSHOT_VERSION
|
||||
and v2_migrated.get_staff(&"staff_a").employment_effective_minute == 0,
|
||||
"v2 migration emits canonical v3 Staff rows at effective minute zero"
|
||||
"v2 migration emits canonical current schema Staff rows at effective minute zero"
|
||||
)
|
||||
var before_tamper := restored.create_snapshot()
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0lupcx1bx00j
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3t73d1gjiac7
|
||||
Reference in New Issue
Block a user