class_name StaffTask extends RefCounted enum State { OPEN, RESERVED, MOVING, WORKING, COMPLETED, BLOCKED, CANCELLED, FAILED, } const MAX_PRIORITY := 100 const MAX_INTERRUPT_LEVEL := 3 var task_id: StringName = &"" var task_type: StringName = &"" var priority := 0 var created_at := 0 var deadline_at := -1 var rescue_facility_id: StringName = &"" var target_entity_id: StringName = &"" var target_cell := Vector3i.ZERO var required_role_tags: Array[StringName] = [] var required_skill_min: Dictionary = {} var required_items: Dictionary = {} var estimated_minutes := 1 var interrupt_level := 3 var state: State = State.OPEN var assigned_staff_id: StringName = &"" var retry_count := 0 var task_tags: Array[StringName] = [] var reservation_ids: Array[StringName] = [] var progress_minutes := 0 var dangerous := false var emergency := false var preserve_progress_on_interrupt := true var state_reason: StringName = &"" var state_changed_at := 0 func configure( p_task_id: StringName, p_task_type: StringName, p_priority: int, p_created_at: int, options: Dictionary = {} ) -> StaffTask: task_id = p_task_id task_type = p_task_type priority = clampi(p_priority, 0, MAX_PRIORITY) created_at = maxi(p_created_at, 0) deadline_at = int(_value(options, &"deadline_at", -1)) rescue_facility_id = StringName(_value(options, &"rescue_facility_id", &"")) target_entity_id = StringName(_value(options, &"target_entity_id", &"")) target_cell = _vector3i(_value(options, &"target_cell", Vector3i.ZERO)) required_role_tags = _sorted_unique_names(_value(options, &"required_role_tags", [])) required_skill_min = _normalize_positive_dictionary(_value(options, &"required_skill_min", {}), 999) required_items = _normalize_positive_dictionary(_value(options, &"required_items", {})) estimated_minutes = maxi(int(_value(options, &"estimated_minutes", 1)), 1) interrupt_level = clampi(int(_value(options, &"interrupt_level", 3)), 0, MAX_INTERRUPT_LEVEL) state = _state_from_value(_value(options, &"state", State.OPEN), State.OPEN) assigned_staff_id = StringName(_value(options, &"assigned_staff_id", &"")) retry_count = maxi(int(_value(options, &"retry_count", 0)), 0) task_tags = _sorted_unique_names(_value(options, &"task_tags", [])) reservation_ids = _sorted_unique_names(_value(options, &"reservation_ids", [])) progress_minutes = clampi( int(_value(options, &"progress_minutes", 0)), 0, estimated_minutes ) dangerous = bool(_value(options, &"dangerous", false)) emergency = bool(_value(options, &"emergency", priority >= 90)) preserve_progress_on_interrupt = bool(_value(options, &"preserve_progress_on_interrupt", true)) state_reason = StringName(_value(options, &"state_reason", &"")) state_changed_at = maxi(int(_value(options, &"state_changed_at", created_at)), 0) return self func is_valid() -> bool: if task_id.is_empty() or task_type.is_empty(): return false if priority < 0 or priority > MAX_PRIORITY: return false if estimated_minutes <= 0 or progress_minutes < 0 or progress_minutes > estimated_minutes: return false if interrupt_level < 0 or interrupt_level > MAX_INTERRUPT_LEVEL: return false if state in [State.RESERVED, State.MOVING, State.WORKING] and assigned_staff_id.is_empty(): return false if state in [State.OPEN, State.BLOCKED, State.COMPLETED, State.CANCELLED, State.FAILED] \ and not assigned_staff_id.is_empty(): return false if state == State.OPEN and not reservation_ids.is_empty(): return false return true func is_terminal() -> bool: return state in [State.COMPLETED, State.CANCELLED, State.FAILED] func is_dispatchable() -> bool: return state == State.OPEN and assigned_staff_id.is_empty() func reserve( staff_id: StringName, p_reservation_ids: Array[StringName], absolute_minute: int ) -> bool: if not is_dispatchable() or staff_id.is_empty(): return false if not required_items.is_empty() and p_reservation_ids.is_empty(): return false assigned_staff_id = staff_id reservation_ids.assign(p_reservation_ids) state = State.RESERVED state_reason = &"" state_changed_at = maxi(absolute_minute, 0) return true func begin_moving(absolute_minute: int) -> bool: if state != State.RESERVED: return false state = State.MOVING state_changed_at = maxi(absolute_minute, 0) return true func begin_working(absolute_minute: int) -> bool: if state not in [State.RESERVED, State.MOVING]: return false state = State.WORKING state_changed_at = maxi(absolute_minute, 0) return true func advance_work(minutes: int, absolute_minute: int) -> bool: if state != State.WORKING or minutes <= 0: return false progress_minutes = mini(progress_minutes + minutes, estimated_minutes) state_changed_at = maxi(absolute_minute, 0) return progress_minutes >= estimated_minutes func complete(absolute_minute: int) -> bool: if state != State.WORKING or progress_minutes < estimated_minutes: return false assigned_staff_id = &"" reservation_ids.clear() state = State.COMPLETED state_changed_at = maxi(absolute_minute, 0) return true func block(reason: StringName, absolute_minute: int) -> bool: if is_terminal(): return false retry_count += 1 assigned_staff_id = &"" reservation_ids.clear() state = State.BLOCKED state_reason = reason state_changed_at = maxi(absolute_minute, 0) return true func reopen(reason: StringName, absolute_minute: int, preserve_progress := true) -> bool: if is_terminal(): return false assigned_staff_id = &"" reservation_ids.clear() if not preserve_progress: progress_minutes = 0 state = State.OPEN state_reason = reason state_changed_at = maxi(absolute_minute, 0) return true func cancel(reason: StringName, absolute_minute: int) -> bool: if is_terminal(): return false assigned_staff_id = &"" reservation_ids.clear() state = State.CANCELLED state_reason = reason state_changed_at = maxi(absolute_minute, 0) return true func fail(reason: StringName, absolute_minute: int) -> bool: if is_terminal(): return false assigned_staff_id = &"" reservation_ids.clear() state = State.FAILED state_reason = reason state_changed_at = maxi(absolute_minute, 0) return true func can_be_interrupted_by(candidate: StaffTask) -> bool: if candidate == null or candidate.task_id == task_id: return false match interrupt_level: 0: return false 1: return candidate.emergency 2: return candidate.priority >= priority _: return true func create_snapshot() -> Dictionary: return { &"task_id": String(task_id), &"task_type": String(task_type), &"priority": priority, &"created_at": created_at, &"deadline_at": deadline_at, &"rescue_facility_id": String(rescue_facility_id), &"target_entity_id": String(target_entity_id), &"target_cell": [target_cell.x, target_cell.y, target_cell.z], &"required_role_tags": _names_to_strings(required_role_tags), &"required_skill_min": _serialize_dictionary(required_skill_min), &"required_items": _serialize_dictionary(required_items), &"estimated_minutes": estimated_minutes, &"interrupt_level": interrupt_level, &"state": State.keys()[state], &"assigned_staff_id": String(assigned_staff_id), &"retry_count": retry_count, &"task_tags": _names_to_strings(task_tags), &"reservation_ids": _names_to_strings(reservation_ids), &"progress_minutes": progress_minutes, &"dangerous": dangerous, &"emergency": emergency, &"preserve_progress_on_interrupt": preserve_progress_on_interrupt, &"state_reason": String(state_reason), &"state_changed_at": state_changed_at, } static func from_snapshot(snapshot: Dictionary) -> StaffTask: if not _snapshot_shape_is_valid(snapshot): return null var state_index := State.keys().find(String(snapshot.get(&"state", ""))) if state_index < 0: return null var task := StaffTask.new().configure( StringName(snapshot.get(&"task_id", "")), StringName(snapshot.get(&"task_type", "")), int(snapshot.get(&"priority", 0)), int(snapshot.get(&"created_at", 0)), { &"deadline_at": snapshot.get(&"deadline_at", -1), &"rescue_facility_id": snapshot.get(&"rescue_facility_id", ""), &"target_entity_id": snapshot.get(&"target_entity_id", ""), &"target_cell": snapshot.get(&"target_cell", []), &"required_role_tags": snapshot.get(&"required_role_tags", []), &"required_skill_min": snapshot.get(&"required_skill_min", {}), &"required_items": snapshot.get(&"required_items", {}), &"estimated_minutes": snapshot.get(&"estimated_minutes", 1), &"interrupt_level": snapshot.get(&"interrupt_level", 3), &"state": state_index, &"assigned_staff_id": snapshot.get(&"assigned_staff_id", ""), &"retry_count": snapshot.get(&"retry_count", 0), &"task_tags": snapshot.get(&"task_tags", []), &"reservation_ids": snapshot.get(&"reservation_ids", []), &"progress_minutes": snapshot.get(&"progress_minutes", 0), &"dangerous": snapshot.get(&"dangerous", false), &"emergency": snapshot.get(&"emergency", false), &"preserve_progress_on_interrupt": snapshot.get(&"preserve_progress_on_interrupt", true), &"state_reason": snapshot.get(&"state_reason", ""), &"state_changed_at": snapshot.get(&"state_changed_at", 0), } ) return task if task.is_valid() else null static func _snapshot_shape_is_valid(snapshot: Dictionary) -> bool: return ( (snapshot.get(&"rescue_facility_id", "") is String or snapshot.get(&"rescue_facility_id", "") is StringName) and not String(snapshot.get(&"task_id", "")).is_empty() and not String(snapshot.get(&"task_type", "")).is_empty() and snapshot.get(&"target_cell", null) is Array and snapshot.get(&"required_role_tags", null) is Array and snapshot.get(&"required_skill_min", null) is Dictionary and snapshot.get(&"required_items", null) is Dictionary and snapshot.get(&"task_tags", null) is Array and snapshot.get(&"reservation_ids", null) is Array and int(snapshot.get(&"priority", -1)) >= 0 and int(snapshot.get(&"priority", -1)) <= MAX_PRIORITY and int(snapshot.get(&"estimated_minutes", 0)) > 0 and int(snapshot.get(&"interrupt_level", -1)) in range(0, MAX_INTERRUPT_LEVEL + 1) ) static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant: return source.get(key, source.get(String(key), fallback)) static func _state_from_value(value: Variant, fallback: State) -> State: if value is int: var numeric := int(value) if numeric >= 0 and numeric < State.size(): return numeric as State var index := State.keys().find(String(value)) return index as State if index >= 0 else fallback static func _vector3i(value: Variant) -> Vector3i: if value is Vector3i: return value if value is Array and value.size() == 3: return Vector3i(int(value[0]), int(value[1]), int(value[2])) return Vector3i.ZERO static func _normalize_positive_dictionary(source: Variant, maximum := 2_147_483_647) -> Dictionary: var result: Dictionary = {} if not source is Dictionary: return result var keys: Array = source.keys() keys.sort_custom(func(a: Variant, b: Variant) -> bool: return String(a) < String(b)) for raw_id: Variant in keys: var item_id := StringName(raw_id) var value := int(source[raw_id]) if not item_id.is_empty() and value > 0: result[item_id] = mini(value, maximum) return result static func _serialize_dictionary(source: Dictionary) -> Dictionary: var result: Dictionary = {} var keys: Array = source.keys() keys.sort_custom(func(a: Variant, b: Variant) -> bool: return String(a) < String(b)) for raw_id: Variant in keys: result[String(raw_id)] = int(source[raw_id]) return result static func _sorted_unique_names(source: Variant) -> Array[StringName]: var result: Array[StringName] = [] if not source is Array: return result for value: Variant in source: var name := StringName(value) if not name.is_empty() and not result.has(name): result.append(name) result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) return result static func _names_to_strings(source: Array[StringName]) -> Array[String]: var result: Array[String] = [] for value: StringName in source: result.append(String(value)) return result