362 lines
12 KiB
GDScript
362 lines
12 KiB
GDScript
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 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))
|
|
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,
|
|
&"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),
|
|
&"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 (
|
|
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
|