Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
116 lines
6.9 KiB
GDScript
116 lines
6.9 KiB
GDScript
class_name FacilityCleaningBridge
|
|
extends RefCounted
|
|
|
|
## Jobs live in StaffService and their effects in FacilityCleanlinessService.
|
|
## No independent clock, movement, or hidden completion timer.
|
|
const TASK_TYPE := &"facility_clean"
|
|
var _runtime: Variant
|
|
|
|
func configure(runtime: Variant) -> void:
|
|
_runtime = runtime
|
|
|
|
static func is_deployed(worker: StaffRuntime, room: FacilityInstance, staff: StaffService) -> bool:
|
|
if worker == null or room == null or room.definition_id != &"cleaning_room" or worker.role_id != &"cleaner" or worker.assigned_facility_id != room.instance_id or not worker.can_take_shift(): return false
|
|
var task := staff.get_task(worker.current_task_id)
|
|
return task != null and task.task_type == TASK_TYPE and not task.is_terminal() and task.assigned_staff_id == worker.staff_id and worker.state in [StaffRuntime.State.MOVING_TO_TASK, StaffRuntime.State.WORKING]
|
|
|
|
func can_dispatch(worker: StaffRuntime) -> bool:
|
|
if worker == null or worker.role_id != &"cleaner": return false
|
|
var room: FacilityInstance = _runtime.facility_service.get_instance(worker.assigned_facility_id)
|
|
return room != null and room.definition_id == &"cleaning_room" and room.operation_state == FacilityInstance.OperationState.OPERATING and _runtime.staff_commute.is_present(worker, room)
|
|
|
|
func task_can_work(task: StaffTask, worker: StaffRuntime) -> bool:
|
|
if task == null or worker == null or not is_deployed(worker, _runtime.facility_service.get_instance(worker.assigned_facility_id), _runtime.staff_service): return false
|
|
if task.state == StaffTask.State.WORKING and (worker.current_cell != task.target_cell or not _runtime.path_service.has_cell(task.target_cell) or _runtime.is_staff_in_transit(worker.staff_id)): return false
|
|
var room: FacilityInstance = _runtime.facility_service.get_instance(worker.assigned_facility_id)
|
|
return room.operation_state == FacilityInstance.OperationState.OPERATING and _runtime.facility_service.has_instance(task.target_entity_id) and not _runtime.facility_cleanliness.get_facility(task.target_entity_id).is_empty()
|
|
|
|
func tick(minute: int) -> void:
|
|
var pending: Dictionary = {}
|
|
for task: StaffTask in _runtime.staff_service.get_tasks():
|
|
if task.task_type != TASK_TYPE or task.is_terminal(): continue
|
|
var worker: StaffRuntime = _runtime.staff_service.get_staff(task.assigned_staff_id)
|
|
if not _runtime.facility_service.has_instance(task.target_entity_id) or (worker != null and not task_can_work(task, worker)):
|
|
_runtime.staff_service.cancel_task(task.task_id, &"CLEANING_UNAVAILABLE", minute)
|
|
continue
|
|
if pending.has(task.target_entity_id):
|
|
_runtime.staff_service.cancel_task(task.task_id, &"DUPLICATE_CLEANING", minute)
|
|
continue
|
|
if task.is_dispatchable():
|
|
for candidate: StaffRuntime in _runtime.staff_service.get_staff_members():
|
|
if can_dispatch(candidate) and prepare_route(task, candidate): break
|
|
pending[task.target_entity_id] = true
|
|
var rules: Dictionary = _runtime.cleaning_policy.create_snapshot()
|
|
for id: StringName in _runtime.facility_cleanliness.get_dirty_facility_ids(rules["clean_at_or_below"]):
|
|
if pending.has(id): continue
|
|
var facility: FacilityInstance = _runtime.facility_service.get_instance(id)
|
|
var cells := access_cells(facility, _runtime.path_service)
|
|
if cells.is_empty(): continue
|
|
_runtime.staff_service.create_task(TASK_TYPE, 50, minute, {
|
|
&"target_entity_id": id, &"target_cell": cells[0],
|
|
&"required_role_tags": [&"cleaner"], &"task_tags": [&"cleaning"],
|
|
&"estimated_minutes": rules["work_minutes"], &"interrupt_level": 0,
|
|
&"preserve_progress_on_interrupt": false,
|
|
})
|
|
|
|
static func access_cells(facility: FacilityInstance, path: PathService, walkable_only := true) -> Array[Vector3i]:
|
|
var result: Array[Vector3i] = []
|
|
if facility == null: return result
|
|
var occupied := facility.occupied_cells.duplicate()
|
|
if occupied.is_empty(): occupied.append(facility.origin)
|
|
for cell: Vector3i in occupied:
|
|
for offset: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
|
|
var candidate := cell + offset
|
|
if (not walkable_only or path.has_cell(candidate)) and not result.has(candidate): result.append(candidate)
|
|
result.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)
|
|
return result
|
|
|
|
func completed(task: StaffTask, worker: StaffRuntime) -> bool:
|
|
if task == null or worker == null or task.state != StaffTask.State.COMPLETED or worker.current_cell != task.target_cell: return false
|
|
return _runtime.facility_cleanliness.record_cleaning(task.target_entity_id, task.task_id, _runtime.cleaning_policy.create_snapshot()["recovery_per_completed_task"])
|
|
|
|
func status(facility_id: StringName) -> String:
|
|
for task: StaffTask in _runtime.staff_service.get_tasks():
|
|
if task.task_type != TASK_TYPE or task.target_entity_id != facility_id or task.is_terminal(): continue
|
|
if task.state == StaffTask.State.WORKING:
|
|
var rate := work_units_per_minute(_runtime.staff_service.get_staff(task.assigned_staff_id))
|
|
if rate > 1:
|
|
return "清掃中(%d倍速・残り%d分)" % [rate,ceili(float(task.estimated_minutes-task.progress_minutes)/rate)]
|
|
return "清掃中 %d/%d分" % [task.progress_minutes,task.estimated_minutes]
|
|
if task.state in [StaffTask.State.MOVING,StaffTask.State.RESERVED]: return "清掃員が移動中"
|
|
return "清掃待ち(清掃員室・配置・経路を確認)"
|
|
var row: Dictionary = _runtime.facility_cleanliness.get_facility(facility_id)
|
|
if not row.is_empty() and row["current"] <= _runtime.cleaning_policy.create_snapshot()["clean_at_or_below"]: return "清掃待ち(作業経路を確認)"
|
|
return ""
|
|
|
|
|
|
## Preserve a reachable reserved destination; reselect only if it is cut off.
|
|
## A moving cleaner waits in place if every access is isolated, never teleports.
|
|
func prepare_route(task: StaffTask, worker: StaffRuntime) -> bool:
|
|
if task == null or worker == null: return false
|
|
var facility: FacilityInstance = _runtime.facility_service.get_instance(task.target_entity_id)
|
|
var cells := access_cells(facility, _runtime.path_service)
|
|
if task.target_cell in cells and not _runtime.path_service.find_path(worker.current_cell, task.target_cell, [0,1], [&"staff"]).is_empty(): return true
|
|
var best: Variant = null
|
|
var best_cost := 9223372036854775807
|
|
for cell: Vector3i in cells:
|
|
var route: Array[Vector3i] = _runtime.path_service.find_path(worker.current_cell, cell, [0,1], [&"staff"])
|
|
if route.is_empty(): continue
|
|
var cost: int = _runtime.path_service.calculate_path_cost(route, [&"staff"])
|
|
if cost >= 0 and cost < best_cost:
|
|
best = cell
|
|
best_cost = cost
|
|
if best == null: return false
|
|
task.target_cell = best
|
|
return true
|
|
|
|
## Original 6.2: Fine cleans three times as fast. Task progress remains work
|
|
## units, so an eight-unit task completes on the third one-minute tick.
|
|
static func work_units_per_minute(worker: StaffRuntime) -> int:
|
|
if worker != null and worker.staff_id == &"heroine_fine" and worker.role_id == &"cleaner" and worker.employment_kind == StaffRuntime.EmploymentKind.UNIQUE:
|
|
return 3
|
|
return 1
|