318 lines
20 KiB
GDScript
318 lines
20 KiB
GDScript
extends SceneTree
|
|
|
|
const CommuteType = preload("res://scripts/staff/staff_commute_service.gd")
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _fixtures: Array = []
|
|
|
|
|
|
## Movement is injected at Runtime's narrow boundary. Real Staff, Shift, Task,
|
|
## Facility and staff-only Path rights run unchanged in this focused suite.
|
|
class CommuteRuntime extends RefCounted:
|
|
var staff_service := StaffService.new()
|
|
var facility_service := FacilityService.new()
|
|
var path_service := PathService.new()
|
|
var commute = CommuteType.new()
|
|
var movements: Array[Dictionary] = []
|
|
var last_moved: Dictionary = {}
|
|
var fail_movement := false
|
|
var in_transit := false
|
|
|
|
func configure() -> void:
|
|
var cells: Array[Vector3i] = []
|
|
for layer: int in [0, 1]:
|
|
for x in range(13): cells.append(Vector3i(x, 0, layer))
|
|
path_service.rebuild_graph(cells)
|
|
path_service.configure_connector(&"staff_door", Vector3i.ZERO, Vector3i(0, 0, 1), 100, true, &"staff")
|
|
for row: Array in [[&"west", Vector3i(5, 0, 0)], [&"east", Vector3i(10, 0, 0)], [&"back", Vector3i(5, 0, 1)]]:
|
|
facility_service.create_instance({&"instance_id": row[0], &"definition_id": &"office", &"origin": row[1], &"occupied_cells": [row[1]], &"staff_min": 1, &"staff_max": 2})
|
|
commute.configure(self)
|
|
|
|
func advance_staff_commute(staff_id: StringName, target: Vector3i, minute: int, reason: StringName = &"") -> Dictionary:
|
|
var worker := staff_service.get_staff(staff_id)
|
|
if not reason.is_empty() or int(last_moved.get(staff_id, -1)) >= minute or fail_movement:
|
|
return {&"moved": false}
|
|
var path := path_service.find_path(worker.current_cell, target, [0, 1], [&"staff"])
|
|
if path.size() <= 1: return {&"moved": false}
|
|
var previous := worker.current_cell
|
|
worker.current_cell = path[1]
|
|
last_moved[staff_id] = minute
|
|
movements.append({&"minute": minute, &"staff_id": staff_id, &"from": previous, &"to": worker.current_cell})
|
|
return {&"moved": true}
|
|
|
|
func is_staff_in_transit(_staff_id: StringName) -> bool:
|
|
return in_transit
|
|
|
|
func advance(minute: int) -> void:
|
|
staff_service.advance_one_minute(minute)
|
|
commute.tick(minute)
|
|
for facility: FacilityInstance in facility_service.get_instances():
|
|
var active: Array[StringName] = []
|
|
for worker: StaffRuntime in staff_service.get_staff_members():
|
|
if commute.is_present(worker, facility): active.append(worker.staff_id)
|
|
facility_service.set_operational_status(facility.instance_id, {&"active_staff_ids": active}, minute)
|
|
|
|
func dispose() -> void:
|
|
staff_service.configure_attendance_driver()
|
|
commute._runtime = null
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("P2: physical attendance, recurring commutes and saved position replay")
|
|
_test_legacy_contract()
|
|
_test_walk_and_arrival_staffing()
|
|
_test_early_arrival_and_shift_end()
|
|
_test_late_route_repair_and_replay()
|
|
_test_reassignment_and_return_to_post()
|
|
_test_reassignment_after_late_arrival_counts_boundary_once()
|
|
_test_porter_dispatch_after_check_in()
|
|
_test_back_layer_permissions_and_unavailable()
|
|
_test_night_shift_and_missing_workplace()
|
|
_test_boarded_reassignment_does_not_staff_origin()
|
|
for fixture: CommuteRuntime in _fixtures: fixture.dispose()
|
|
print("Staff commute: %d checks, %d failures" % [_checks, _failures])
|
|
quit(_failures)
|
|
|
|
|
|
func _fixture(role: StringName = &"receptionist", minute := 360) -> CommuteRuntime:
|
|
var fixture := CommuteRuntime.new()
|
|
fixture.configure()
|
|
fixture.staff_service.create_staff(&"worker", role)
|
|
fixture.staff_service.advance_one_minute(minute)
|
|
_fixtures.append(fixture)
|
|
return fixture
|
|
|
|
|
|
func _plan(fixture: CommuteRuntime, facility: StringName = &"west", template: StringName = &"DAY") -> StaffRuntime:
|
|
_expect(fixture.staff_service.set_work_assignment(&"worker", facility, template).is_success(), "repeating work plan is accepted")
|
|
return fixture.staff_service.get_staff(&"worker")
|
|
|
|
|
|
func _test_legacy_contract() -> void:
|
|
var legacy := StaffService.new()
|
|
legacy.create_staff(&"legacy", &"receptionist")
|
|
legacy.advance_one_minute(360)
|
|
legacy.set_work_assignment(&"legacy", &"west", &"DAY")
|
|
_expect(legacy.get_staff(&"legacy").state == StaffRuntime.State.AVAILABLE, "component-only recurring plans keep their original shift contract")
|
|
var fixture := _fixture()
|
|
fixture.staff_service.create_standard_shift(&"worker", &"DAY", 0, &"west", &"manual")
|
|
var worker := fixture.staff_service.get_staff(&"worker")
|
|
_expect(worker.state == StaffRuntime.State.AVAILABLE and not fixture.staff_service.requires_attendance(&"worker"), "manual legacy shifts remain available without a persistent plan")
|
|
_expect(fixture.commute.is_present(worker, fixture.facility_service.get_instance(&"west")), "legacy manual-shift staffing keeps compatibility")
|
|
|
|
|
|
func _test_walk_and_arrival_staffing() -> void:
|
|
var fixture := _fixture()
|
|
var worker := _plan(fixture)
|
|
var shift := fixture.staff_service.get_shift(worker.shift_id)
|
|
var west := fixture.facility_service.get_instance(&"west")
|
|
_expect(worker.state == StaffRuntime.State.COMMUTING and worker.current_cell == Vector3i.ZERO, "paused assignment starts a real commute without teleporting")
|
|
_expect(shift.worked_minutes == 0 and shift.actual_started_at == -1, "assignment time is not falsely counted as work")
|
|
_expect(not fixture.commute.is_present(worker, west), "a remote assignment cannot staff a facility")
|
|
for minute in range(361, 365):
|
|
var previous := worker.current_cell
|
|
fixture.advance(minute)
|
|
_expect(worker.current_cell.distance_to(previous) <= 1.0, "commute advances at most one route edge per minute")
|
|
_expect(worker.current_cell == Vector3i(4, 0, 0), "worker reaches the actual facility access cell")
|
|
_expect(worker.state == StaffRuntime.State.COMMUTING and west.active_staff_ids.is_empty(), "movement arrival activates at the next Staff AI boundary")
|
|
fixture.advance(365)
|
|
_expect(worker.state == StaffRuntime.State.AVAILABLE and west.active_staff_ids == [&"worker"], "only physical arrival contributes the facility staff capacity")
|
|
_expect(shift.actual_started_at == 365 and shift.worked_minutes == 1, "first recorded work minute is the arrival activation minute")
|
|
var snapshot := fixture.staff_service.create_snapshot()
|
|
fixture.commute.get_report()
|
|
_expect(fixture.staff_service.create_snapshot() == snapshot, "attendance report reads do not mutate saved Staff state")
|
|
_expect(fixture.staff_service.validate_invariants()[&"ok"], "physical commute preserves Staff invariants")
|
|
|
|
|
|
func _test_early_arrival_and_shift_end() -> void:
|
|
var fixture := _fixture(&"receptionist", 330)
|
|
var worker := _plan(fixture)
|
|
var shift := fixture.staff_service.get_shift(worker.shift_id)
|
|
for minute in range(331, 360): fixture.advance(minute)
|
|
_expect(worker.current_cell == Vector3i(4, 0, 0) and worker.state == StaffRuntime.State.COMMUTING, "early arrival waits at the workplace through the commute lead window")
|
|
_expect(shift.actual_started_at == -1 and shift.worked_minutes == 0, "early arrival does not start paid duty before 06:00")
|
|
fixture.advance(360)
|
|
_expect(worker.state == StaffRuntime.State.AVAILABLE and shift.actual_started_at == 360, "an employee already at the workplace starts at exactly 06:00")
|
|
fixture.advance(1080)
|
|
_expect(worker.state == StaffRuntime.State.OFF_DUTY and worker.shift_id.is_empty() and shift.actual_ended_at == 1080, "18:00 closes the work shift and removes attendance")
|
|
_expect(worker.current_cell == Vector3i(4, 0, 0), "shift end keeps the real position rather than teleporting home")
|
|
fixture.advance(1770)
|
|
_expect(worker.state == StaffRuntime.State.COMMUTING, "next day's recurring commute begins automatically at 05:30")
|
|
fixture.advance(1800)
|
|
_expect(worker.state == StaffRuntime.State.AVAILABLE, "next day's saved plan activates only at its new shift boundary")
|
|
|
|
|
|
func _test_late_route_repair_and_replay() -> void:
|
|
var fixture := _fixture()
|
|
fixture.path_service.set_cell_temporary_block(Vector3i(2, 0, 0), true)
|
|
var worker := _plan(fixture)
|
|
for minute in range(361, 379): fixture.advance(minute)
|
|
var shift := fixture.staff_service.get_shift(worker.shift_id)
|
|
_expect(worker.current_cell == Vector3i.ZERO and worker.state == StaffRuntime.State.COMMUTING, "a blocked route never teleports or invents attendance")
|
|
_expect(shift.late_minutes == 18 and shift.late_reason == CommuteType.ROUTE_BLOCKED, "lateness records the real route blockage after the grace threshold")
|
|
_expect(shift.worked_minutes == 0 and shift.actual_started_at == -1, "unreachable staff do not accumulate fictional worked minutes")
|
|
var checkpoint := fixture.staff_service.create_snapshot()
|
|
var restored := _fixture()
|
|
_expect(restored.staff_service.restore_snapshot(checkpoint), "a blocked commute snapshot restores using existing Staff and Shift data")
|
|
restored.path_service.set_cell_temporary_block(Vector3i(2, 0, 0), true)
|
|
_expect(restored.staff_service.create_snapshot() == checkpoint, "restored blocked attendance is exact without new service state")
|
|
fixture.path_service.set_cell_temporary_block(Vector3i(2, 0, 0), false)
|
|
restored.path_service.set_cell_temporary_block(Vector3i(2, 0, 0), false)
|
|
for minute in range(379, 390):
|
|
fixture.advance(minute)
|
|
restored.advance(minute)
|
|
_expect(fixture.staff_service.create_snapshot() == restored.staff_service.create_snapshot(), "route repair replays to identical work, lateness and positions after restore")
|
|
_expect(worker.state == StaffRuntime.State.AVAILABLE and worker.absence_reason.is_empty(), "repairing the route allows physical arrival and clears the active absence")
|
|
_expect(shift.actual_started_at == 383 and shift.late_minutes == 22, "arrival keeps historical late minutes while beginning real duty")
|
|
var midwalk := _fixture()
|
|
var walker := _plan(midwalk)
|
|
midwalk.advance(361)
|
|
midwalk.advance(362)
|
|
var moving := midwalk.staff_service.create_snapshot()
|
|
var replay := _fixture()
|
|
_expect(replay.staff_service.restore_snapshot(moving), "mid-walk save restores the exact current cell")
|
|
for minute in range(363, 368):
|
|
midwalk.advance(minute)
|
|
replay.advance(minute)
|
|
_expect(midwalk.staff_service.create_snapshot() == replay.staff_service.create_snapshot() and walker.current_cell == Vector3i(4, 0, 0), "mid-walk replay neither skips nor repeats route cells")
|
|
|
|
|
|
func _test_reassignment_and_return_to_post() -> void:
|
|
var fixture := _fixture()
|
|
var worker := _plan(fixture)
|
|
for minute in range(361, 367): fixture.advance(minute)
|
|
var first := fixture.staff_service.get_shift(worker.shift_id)
|
|
_expect(fixture.staff_service.set_work_assignment(&"worker", &"east", &"DAY").is_success(), "fixed staff can be reassigned after arriving")
|
|
first = fixture.staff_service.get_shift(first.shift_instance_id)
|
|
_expect(worker.state == StaffRuntime.State.COMMUTING and worker.current_cell == Vector3i(4, 0, 0), "reassignment walks from the current location")
|
|
_expect(first.assigned_facility_id == &"west" and first.actual_ended_at == 366, "historical duty remains attached to the old workplace")
|
|
for minute in range(367, 374): fixture.advance(minute)
|
|
var shift := fixture.staff_service.get_shift(worker.shift_id)
|
|
_expect(worker.current_cell == Vector3i(9, 0, 0) and worker.state == StaffRuntime.State.AVAILABLE, "reassignment opens the new workplace after arrival")
|
|
# Completion of a portable task leaves its employee at the work target.
|
|
worker.current_cell = Vector3i(6, 0, 0)
|
|
worker.transition_to(StaffRuntime.State.AVAILABLE, &"TASK_COMPLETED", 374)
|
|
fixture.advance(374)
|
|
_expect(worker.state == StaffRuntime.State.COMMUTING and worker.current_cell == Vector3i(7, 0, 0), "fixed roles return to their post after work elsewhere")
|
|
fixture.advance(375)
|
|
fixture.advance(376)
|
|
fixture.advance(377)
|
|
_expect(worker.state == StaffRuntime.State.AVAILABLE and worker.current_cell == Vector3i(9, 0, 0), "desk service resumes after returning physically")
|
|
_expect(shift.late_minutes == 0, "returning from a completed task does not rewrite arrival lateness")
|
|
_expect(fixture.staff_service.set_work_assignment(&"worker", &"", &"DAY").is_success(), "unassignment cancels the repeating commute")
|
|
var cell := worker.current_cell
|
|
fixture.advance(378)
|
|
_expect(worker.state == StaffRuntime.State.OFF_DUTY and worker.current_cell == cell, "unassigned staff keep position and stop commuting")
|
|
|
|
|
|
func _test_reassignment_after_late_arrival_counts_boundary_once() -> void:
|
|
var fixture := _fixture()
|
|
var worker := _plan(fixture)
|
|
for minute in range(361, 366): fixture.advance(minute)
|
|
var old_id := worker.shift_id
|
|
fixture.facility_service.create_instance({&"instance_id": &"nearby", &"definition_id": &"office", &"origin": Vector3i(3, 0, 0), &"occupied_cells": [Vector3i(3, 0, 0)]})
|
|
_expect(fixture.staff_service.get_shift(old_id).worked_minutes == 1, "late arrival has only one actual duty minute before reassignment")
|
|
_expect(fixture.staff_service.set_work_assignment(&"worker", &"nearby", &"DAY").is_success(), "paused reassignment may choose another workplace sharing this access cell")
|
|
var old := fixture.staff_service.get_shift(old_id)
|
|
var replacement := fixture.staff_service.get_shift(worker.shift_id)
|
|
_expect(old.worked_minutes == 0 and old.actual_started_at == -1 and replacement.worked_minutes == 1, "reassignment transfers the current duty minute exactly once despite earlier commute gaps")
|
|
_expect(fixture.staff_service.restore_snapshot(fixture.staff_service.create_snapshot()), "gap-aware historical shift split restores without extra saved attendance state")
|
|
|
|
|
|
func _test_porter_dispatch_after_check_in() -> void:
|
|
var fixture := _fixture(&"porter")
|
|
var worker := _plan(fixture)
|
|
_expect(not worker.is_available_for_dispatch(), "new porter cannot take a delivery before checking in")
|
|
for minute in range(361, 367): fixture.advance(minute)
|
|
worker.current_cell = Vector3i(10, 0, 0)
|
|
worker.transition_to(StaffRuntime.State.AVAILABLE, &"TASK_COMPLETED", 367)
|
|
fixture.advance(367)
|
|
_expect(worker.state == StaffRuntime.State.AVAILABLE and worker.current_cell == Vector3i(10, 0, 0), "checked-in porter can accept another job from a delivery destination")
|
|
_expect(not fixture.commute.is_present(worker, fixture.facility_service.get_instance(&"west")), "porter away from the warehouse does not falsely staff the warehouse")
|
|
var restored := _fixture(&"porter")
|
|
_expect(restored.staff_service.restore_snapshot(fixture.staff_service.create_snapshot()), "the porter's once-per-shift check-in survives save restoration")
|
|
restored.advance(368)
|
|
_expect(restored.staff_service.get_staff(&"worker").state == StaffRuntime.State.AVAILABLE, "loaded porter duty does not force an unnecessary return commute")
|
|
fixture.advance(1080)
|
|
fixture.advance(1770)
|
|
_expect(worker.state == StaffRuntime.State.COMMUTING and worker.current_cell == Vector3i(9, 0, 0), "the following shift requires a fresh real warehouse check-in")
|
|
|
|
|
|
func _test_back_layer_permissions_and_unavailable() -> void:
|
|
var fixture := _fixture()
|
|
var worker := _plan(fixture, &"back")
|
|
var access: Dictionary = fixture.commute.find_facility_access_path(Vector3i.ZERO, fixture.facility_service.get_instance(&"back"))
|
|
_expect(access[&"reachable"] and access[&"cell"].z == 1, "commute can reach BACK facilities through an authorized staff door")
|
|
_expect(fixture.path_service.find_path(Vector3i.ZERO, Vector3i(4, 0, 1), [0, 1], []).is_empty(), "the same staff door remains inaccessible without staff rights")
|
|
fixture.advance(361)
|
|
_expect(worker.current_cell == Vector3i(0, 0, 1), "cross-layer commute uses the actual connector endpoint")
|
|
worker.health = 0
|
|
worker.transition_to(StaffRuntime.State.INCAPACITATED, &"TEST", 362)
|
|
fixture.advance(362)
|
|
_expect(worker.current_cell == Vector3i(0, 0, 1) and worker.state == StaffRuntime.State.INCAPACITATED, "unavailable workers do not walk or get promoted to available")
|
|
worker.health = 100
|
|
worker.transition_to(StaffRuntime.State.COMMUTING, &"RECOVERED", 363)
|
|
fixture.fail_movement = true
|
|
fixture.advance(363)
|
|
_expect(worker.current_cell == Vector3i(0, 0, 1), "a refused Runtime movement leaves the commute position unchanged")
|
|
fixture.fail_movement = false
|
|
for minute in range(364, 371): fixture.advance(minute)
|
|
_expect(fixture.commute.is_present(worker, fixture.facility_service.get_instance(&"back")), "recovered staff finish the real BACK commute")
|
|
_expect(not CommuteType.is_at_facility(worker, fixture.facility_service.get_instance(&"west")), "identical screen coordinates on different layers never count as attendance")
|
|
var upper := FacilityInstance.new().configure(&"upper", &"office", {&"origin": Vector3i(4, 1, 1), &"occupied_cells": [Vector3i(4, 1, 1)]})
|
|
_expect(not CommuteType.is_at_facility(worker, upper), "standing below an upper room cannot bypass the route to its floor")
|
|
_expect(not fixture.commute.find_facility_access_path(worker.current_cell, upper)[&"reachable"], "a corridor underneath a room is not a valid workplace entrance")
|
|
|
|
|
|
func _test_night_shift_and_missing_workplace() -> void:
|
|
var fixture := _fixture(&"receptionist", 1410)
|
|
var worker := _plan(fixture, &"west", &"NIGHT")
|
|
for minute in range(1411, 1417): fixture.advance(minute)
|
|
var shift := fixture.staff_service.get_shift(worker.shift_id)
|
|
fixture.advance(1440)
|
|
_expect(worker.state == StaffRuntime.State.AVAILABLE and worker.shift_id == shift.shift_instance_id, "midnight retains the same physically attended NIGHT shift")
|
|
fixture.advance(1800)
|
|
_expect(worker.state == StaffRuntime.State.OFF_DUTY and shift.actual_ended_at == 1800, "NIGHT attendance ends at next-day 06:00")
|
|
var missing := _fixture()
|
|
var absent := _plan(missing, &"missing")
|
|
missing.advance(361)
|
|
_expect(absent.state == StaffRuntime.State.COMMUTING and absent.absence_reason == CommuteType.WORKPLACE_MISSING, "missing workplace remains a visible waiting condition")
|
|
_expect(missing.staff_service.get_shift(absent.shift_id).worked_minutes == 0, "a missing workplace creates no phantom duty")
|
|
|
|
|
|
func _test_boarded_reassignment_does_not_staff_origin() -> void:
|
|
var fixture := _fixture()
|
|
var worker := _plan(fixture)
|
|
fixture.facility_service.create_instance({&"instance_id": &"boarding_post", &"definition_id": &"office", &"origin": Vector3i.ZERO, &"occupied_cells": [Vector3i.ZERO]})
|
|
fixture.in_transit = true
|
|
_expect(fixture.staff_service.set_work_assignment(&"worker", &"boarding_post", &"DAY").is_success(), "a boarded commute can accept a new destination plan")
|
|
var shift := fixture.staff_service.get_shift(worker.shift_id)
|
|
fixture.advance(361)
|
|
_expect(worker.state == StaffRuntime.State.COMMUTING and shift.worked_minutes == 0, "the cabin's projected boarding cell cannot turn into attendance at the origin")
|
|
_expect(not fixture.commute.is_present(worker, fixture.facility_service.get_instance(&"boarding_post")), "a passenger never supplies staff capacity while still in transit")
|
|
_expect(fixture.commute.get_report()[&"staff"][0][&"reason"] == &"ELEVATOR_WAIT", "commute report distinguishes the owned ride from arrival at its projected origin")
|
|
_expect(worker.current_cell == Vector3i.ZERO and fixture.movements.is_empty(), "the commute driver waits for the owned ride rather than moving its projected anchor")
|
|
fixture.in_transit = false
|
|
worker.current_cell = Vector3i(3, 0, 0)
|
|
fixture.advance(362)
|
|
_expect(worker.state == StaffRuntime.State.COMMUTING and worker.current_cell == Vector3i(2, 0, 0), "after safe disembarkation the employee walks toward the latest assignment")
|
|
fixture.advance(363)
|
|
fixture.advance(364)
|
|
_expect(worker.state == StaffRuntime.State.AVAILABLE and shift.actual_started_at == 364, "new-post duty begins only after the completed ride and actual return")
|
|
fixture.in_transit = true
|
|
worker.current_task_id = &"owned_work"
|
|
_expect(fixture.commute.can_attend_shift(worker, shift, 365), "an already checked-in employee's owned portable work remains duty while riding")
|
|
worker.current_task_id = &""
|
|
_expect(not fixture.commute.can_attend_shift(worker, shift, 365), "an idle passenger cannot become dispatchable based on its old checked-in position")
|
|
|
|
|
|
func _expect(condition: bool, message: String) -> void:
|
|
_checks += 1
|
|
if not condition:
|
|
_failures += 1
|
|
push_error("FAIL: %s" % message)
|