Files
TheTree_mock/scripts/staff/staff_commute_service.gd

169 lines
7.0 KiB
GDScript

class_name StaffCommuteService
extends RefCounted
## Commutes derive from the saved work plan, Shift and current actor position.
## Runtime owns movement credit, Path projection, elevator rides and events.
## This adapter deliberately owns no additional snapshot payload.
const ROUTE_BLOCKED := &"COMMUTE_ROUTE_BLOCKED"
const WORKPLACE_MISSING := &"COMMUTE_WORKPLACE_MISSING"
const WAITING_SHIFT := &"WAITING_FOR_SHIFT"
const MOVING_TO_WORK := &"MOVING_TO_WORK"
var _runtime: Variant
func configure(runtime: Variant) -> StaffCommuteService:
_runtime = runtime
_runtime.staff_service.configure_attendance_driver(Callable(self, "can_attend_shift"))
return self
## A porter checks in once at the warehouse, then remains dispatchable at the
## end of a delivery. Fixed-post workers return before resuming desk service.
func can_attend_shift(staff: StaffRuntime, shift: ShiftInstance, _minute: int) -> bool:
if _runtime == null or staff == null or shift == null:
return false
if _in_transit(staff.staff_id) and (staff.current_task_id.is_empty() or shift.actual_started_at < 0):
return false
if shift.actual_started_at >= 0 and (staff.role_id == &"porter" or not staff.current_task_id.is_empty()):
return true
var facility: FacilityInstance = _runtime.facility_service.get_instance(shift.assigned_facility_id)
return is_at_facility(staff, facility)
## Physical attendance is separate from dispatch availability. A porter away
## with goods does not provide staff capacity to an empty warehouse.
func is_present(staff: StaffRuntime, facility: FacilityInstance) -> bool:
if _runtime == null or staff == null or facility == null \
or staff.assigned_facility_id != facility.instance_id \
or staff.state != StaffRuntime.State.AVAILABLE or not staff.can_take_shift():
return false
if _in_transit(staff.staff_id):
return false
if not _runtime.staff_service.requires_attendance(staff.staff_id):
return true
return is_at_facility(staff, facility)
func tick(absolute_minute: int) -> Dictionary:
if _runtime == null:
return {&"ok": false, &"reason": &"NOT_CONFIGURED"}
var moved: Array[StringName] = []
var waiting: Array[StringName] = []
for staff: StaffRuntime in _runtime.staff_service.get_staff_members():
if not _runtime.staff_service.requires_attendance(staff.staff_id) \
or staff.state != StaffRuntime.State.COMMUTING \
or not staff.can_take_shift() or not staff.current_task_id.is_empty():
continue
var shift: ShiftInstance = _runtime.staff_service.get_shift(staff.shift_id)
if shift == null or (not shift.contains_minute(absolute_minute) and not shift.is_commute_window(absolute_minute)):
continue
if _in_transit(staff.staff_id):
_wait(staff, absolute_minute, &"ELEVATOR_WAIT")
waiting.append(staff.staff_id)
continue
var facility: FacilityInstance = _runtime.facility_service.get_instance(shift.assigned_facility_id)
if facility == null:
_wait(staff, absolute_minute, WORKPLACE_MISSING)
waiting.append(staff.staff_id)
continue
if is_at_facility(staff, facility):
staff.absence_reason = &""
_wait(staff, absolute_minute, WAITING_SHIFT if shift.is_commute_window(absolute_minute) else &"COMMUTE_ARRIVED")
waiting.append(staff.staff_id)
continue
var access := find_facility_access_path(staff.current_cell, facility)
if not bool(access.get(&"reachable", false)):
_wait(staff, absolute_minute, ROUTE_BLOCKED)
waiting.append(staff.staff_id)
continue
staff.absence_reason = &""
var result: Dictionary = _runtime.advance_staff_commute(staff.staff_id, access[&"cell"], absolute_minute)
if bool(result.get(&"moved", false)):
moved.append(staff.staff_id)
else:
waiting.append(staff.staff_id)
return {&"ok": true, &"moved_staff_ids": moved, &"waiting_staff_ids": waiting}
func get_report() -> Dictionary:
var rows: Array[Dictionary] = []
if _runtime == null:
return {&"staff": rows}
for staff: StaffRuntime in _runtime.staff_service.get_staff_members():
if not _runtime.staff_service.requires_attendance(staff.staff_id):
continue
var shift: ShiftInstance = _runtime.staff_service.get_shift(staff.shift_id)
var facility: FacilityInstance = _runtime.facility_service.get_instance(staff.assigned_facility_id)
var reason := staff.absence_reason
var target := staff.current_cell
if _in_transit(staff.staff_id):
reason = &"ELEVATOR_WAIT"
if facility != null:
var access := find_facility_access_path(staff.current_cell, facility)
target = access.get(&"cell", facility.origin)
if staff.state == StaffRuntime.State.COMMUTING and reason.is_empty():
reason = WAITING_SHIFT if is_at_facility(staff, facility) else MOVING_TO_WORK
rows.append({
&"staff_id": staff.staff_id, &"state": StaffRuntime.State.keys()[staff.state],
&"reason": reason, &"facility_id": staff.assigned_facility_id,
&"current_cell": staff.current_cell, &"target_cell": target,
&"present": is_present(staff, facility),
&"late_minutes": shift.late_minutes if shift != null else 0,
})
return {&"staff": rows}
func find_facility_access_path(start: Vector3i, facility: FacilityInstance) -> Dictionary:
if _runtime == null or facility == null:
return {&"reachable": false}
var candidates: Dictionary = {}
var occupied := facility.occupied_cells.duplicate()
if occupied.is_empty():
occupied.append(facility.origin)
for cell: Vector3i in occupied:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
if _runtime.path_service.has_cell(cell + delta):
candidates[cell + delta] = true
var ordered: Array = candidates.keys()
ordered.sort_custom(func(a: Vector3i, b: Vector3i) -> bool:
if a.z != b.z: return a.z < b.z
if a.y != b.y: return a.y < b.y
return a.x < b.x)
var best: Array[Vector3i] = []
var best_cost := 9223372036854775807
for cell: Vector3i in ordered:
var path: Array[Vector3i] = _runtime.path_service.find_path(start, cell,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
var cost: int = _runtime.path_service.calculate_path_cost(path, [&"staff"]) if not path.is_empty() else -1
if cost >= 0 and (best.is_empty() or cost < best_cost):
best = path
best_cost = cost
return {&"reachable": not best.is_empty(), &"path": best,
&"cell": best[-1] if not best.is_empty() else facility.origin,
&"steps": maxi(0, best.size() - 1)}
static func is_at_facility(staff: StaffRuntime, facility: FacilityInstance) -> bool:
if staff == null or facility == null:
return false
var occupied := facility.occupied_cells.duplicate()
if occupied.is_empty():
occupied.append(facility.origin)
for cell: Vector3i in occupied:
if cell.z == staff.current_cell.z and cell.y == staff.current_cell.y \
and absi(cell.x - staff.current_cell.x) <= 1:
return true
return false
func _wait(staff: StaffRuntime, minute: int, reason: StringName) -> void:
if reason in [ROUTE_BLOCKED, WORKPLACE_MISSING]:
staff.absence_reason = reason
_runtime.advance_staff_commute(staff.staff_id, staff.current_cell, minute, reason)
func _in_transit(staff_id: StringName) -> bool:
return _runtime != null and _runtime.has_method("is_staff_in_transit") \
and bool(_runtime.is_staff_in_transit(staff_id))