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