Files
TheTree_mock/scripts/environment/facility_cleaning_bridge.gd
Takeshi 5dde0c5ec2 Record original-alignment implementation and traceability checkpoint
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.
2026-09-30 23:52:24 +09:00

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