391 lines
14 KiB
GDScript
391 lines
14 KiB
GDScript
class_name AuthorityInspection
|
|
extends RefCounted
|
|
|
|
enum State {
|
|
SCHEDULED,
|
|
ARRIVING,
|
|
CHECK_IN,
|
|
PATROLLING,
|
|
REVIEWING,
|
|
RESOLVING,
|
|
DEPARTING,
|
|
CLOSED,
|
|
BLOCKED_ENTRY,
|
|
INTERRUPTED,
|
|
HOSTILE,
|
|
CANCELLED,
|
|
}
|
|
|
|
enum Result {
|
|
PENDING,
|
|
NO_ACTION,
|
|
WARNING,
|
|
FINE,
|
|
TEMPORARY_CLOSURE,
|
|
CONFISCATION,
|
|
ARREST_ATTEMPT,
|
|
RAID_ESCALATION,
|
|
}
|
|
|
|
var inspection_id: StringName = &""
|
|
var inspector_id: StringName = &""
|
|
var scheduled_at := 0
|
|
var entrance_cell := Vector3i.ZERO
|
|
var state: State = State.SCHEDULED
|
|
var state_changed_at := 0
|
|
var time_budget_minutes := 120
|
|
var time_spent_minutes := 0
|
|
var waypoint_route: Array[Dictionary] = []
|
|
var current_waypoint_index := 0
|
|
var visited_waypoint_ids: Array[StringName] = []
|
|
var inaccessible_waypoint_ids: Array[StringName] = []
|
|
var discovered_evidence_ids: Array[StringName] = []
|
|
var evidence_score := 0
|
|
var result: Result = Result.PENDING
|
|
var result_reason: StringName = &""
|
|
var exit_reason: StringName = &""
|
|
var resolution_id: StringName = &""
|
|
var closed_at := -1
|
|
var revision := 0
|
|
|
|
|
|
func configure(
|
|
p_inspection_id: StringName,
|
|
p_inspector_id: StringName,
|
|
p_scheduled_at: int,
|
|
options: Dictionary = {}
|
|
) -> AuthorityInspection:
|
|
inspection_id = p_inspection_id
|
|
inspector_id = p_inspector_id
|
|
scheduled_at = maxi(p_scheduled_at, 0)
|
|
entrance_cell = _vector3i(_value(options, &"entrance_cell", Vector3i.ZERO))
|
|
state = _state_from_value(_value(options, &"state", State.SCHEDULED))
|
|
state_changed_at = maxi(int(_value(options, &"state_changed_at", scheduled_at)), 0)
|
|
time_budget_minutes = maxi(int(_value(options, &"time_budget_minutes", 120)), 1)
|
|
time_spent_minutes = maxi(int(_value(options, &"time_spent_minutes", 0)), 0)
|
|
waypoint_route.clear()
|
|
var raw_route: Variant = _value(options, &"waypoint_route", [])
|
|
if raw_route is Array:
|
|
for raw_waypoint: Variant in raw_route:
|
|
if raw_waypoint is Dictionary:
|
|
var normalized := normalize_waypoint(raw_waypoint)
|
|
if not normalized.is_empty():
|
|
waypoint_route.append(normalized)
|
|
current_waypoint_index = maxi(int(_value(options, &"current_waypoint_index", 0)), 0)
|
|
visited_waypoint_ids = _sorted_unique_names(_value(options, &"visited_waypoint_ids", []))
|
|
inaccessible_waypoint_ids = _sorted_unique_names(_value(options, &"inaccessible_waypoint_ids", []))
|
|
discovered_evidence_ids = _sorted_unique_names(_value(options, &"discovered_evidence_ids", []))
|
|
evidence_score = maxi(int(_value(options, &"evidence_score", 0)), 0)
|
|
result = _result_from_value(_value(options, &"result", Result.PENDING))
|
|
result_reason = StringName(_value(options, &"result_reason", &""))
|
|
exit_reason = StringName(_value(options, &"exit_reason", &""))
|
|
resolution_id = StringName(_value(options, &"resolution_id", &""))
|
|
closed_at = int(_value(options, &"closed_at", -1))
|
|
revision = maxi(int(_value(options, &"revision", 0)), 0)
|
|
return self
|
|
|
|
|
|
func set_state(new_state: State, absolute_minute: int, reason: StringName = &"") -> bool:
|
|
if absolute_minute < state_changed_at or state == new_state:
|
|
return false
|
|
state = new_state
|
|
state_changed_at = absolute_minute
|
|
if new_state == State.CLOSED:
|
|
closed_at = absolute_minute
|
|
if not reason.is_empty():
|
|
result_reason = reason
|
|
revision += 1
|
|
return true
|
|
|
|
|
|
func add_visited_waypoint(waypoint_id: StringName, travel_minutes: int) -> bool:
|
|
if waypoint_id.is_empty() or visited_waypoint_ids.has(waypoint_id):
|
|
return false
|
|
visited_waypoint_ids.append(waypoint_id)
|
|
visited_waypoint_ids.sort_custom(_name_less)
|
|
time_spent_minutes += maxi(travel_minutes, 0)
|
|
current_waypoint_index += 1
|
|
revision += 1
|
|
return true
|
|
|
|
|
|
func add_inaccessible_waypoint(waypoint_id: StringName) -> bool:
|
|
if waypoint_id.is_empty() or inaccessible_waypoint_ids.has(waypoint_id):
|
|
return false
|
|
inaccessible_waypoint_ids.append(waypoint_id)
|
|
inaccessible_waypoint_ids.sort_custom(_name_less)
|
|
revision += 1
|
|
return true
|
|
|
|
|
|
func add_discovered_evidence(evidence_id: StringName) -> bool:
|
|
if evidence_id.is_empty() or discovered_evidence_ids.has(evidence_id):
|
|
return false
|
|
discovered_evidence_ids.append(evidence_id)
|
|
discovered_evidence_ids.sort_custom(_name_less)
|
|
revision += 1
|
|
return true
|
|
|
|
|
|
func is_terminal() -> bool:
|
|
return state in [State.CLOSED, State.CANCELLED]
|
|
|
|
|
|
func is_valid() -> bool:
|
|
if inspection_id.is_empty() or inspector_id.is_empty() or scheduled_at < 0:
|
|
return false
|
|
if state_changed_at < scheduled_at or time_budget_minutes <= 0 or time_spent_minutes < 0:
|
|
return false
|
|
if current_waypoint_index < 0 or current_waypoint_index > waypoint_route.size():
|
|
return false
|
|
if evidence_score < 0 or closed_at < -1 or revision < 0:
|
|
return false
|
|
if visited_waypoint_ids != _sorted_unique_names(visited_waypoint_ids):
|
|
return false
|
|
if inaccessible_waypoint_ids != _sorted_unique_names(inaccessible_waypoint_ids):
|
|
return false
|
|
if discovered_evidence_ids != _sorted_unique_names(discovered_evidence_ids):
|
|
return false
|
|
var waypoint_ids: Dictionary = {}
|
|
var previous_waypoint: Dictionary = {}
|
|
for waypoint: Dictionary in waypoint_route:
|
|
var normalized := normalize_waypoint(waypoint)
|
|
if normalized.is_empty():
|
|
return false
|
|
var waypoint_id: StringName = normalized[&"waypoint_id"]
|
|
if waypoint_ids.has(waypoint_id):
|
|
return false
|
|
if not previous_waypoint.is_empty():
|
|
var previous_utility := int(previous_waypoint[&"utility"])
|
|
var current_utility := int(normalized[&"utility"])
|
|
if current_utility > previous_utility:
|
|
return false
|
|
if current_utility == previous_utility \
|
|
and String(normalized[&"waypoint_id"]) < String(previous_waypoint[&"waypoint_id"]):
|
|
return false
|
|
waypoint_ids[waypoint_id] = true
|
|
previous_waypoint = normalized
|
|
if current_waypoint_index != visited_waypoint_ids.size():
|
|
return false
|
|
for route_index: int in range(current_waypoint_index):
|
|
if not visited_waypoint_ids.has(StringName(waypoint_route[route_index][&"waypoint_id"])):
|
|
return false
|
|
for waypoint_id: StringName in visited_waypoint_ids:
|
|
if not waypoint_ids.has(waypoint_id):
|
|
return false
|
|
for waypoint_id: StringName in inaccessible_waypoint_ids:
|
|
if not waypoint_ids.has(waypoint_id):
|
|
return false
|
|
if result == Result.PENDING and not resolution_id.is_empty():
|
|
return false
|
|
if result != Result.PENDING and resolution_id.is_empty():
|
|
return false
|
|
if state == State.CLOSED and closed_at < state_changed_at:
|
|
return false
|
|
if state != State.CLOSED and closed_at >= 0:
|
|
return false
|
|
return true
|
|
|
|
|
|
func create_snapshot() -> Dictionary:
|
|
var saved_route: Array[Dictionary] = []
|
|
for waypoint: Dictionary in waypoint_route:
|
|
saved_route.append({
|
|
&"waypoint_id": String(waypoint[&"waypoint_id"]),
|
|
&"cell": _cell_to_array(waypoint[&"cell"]),
|
|
&"suspect_score": int(waypoint[&"suspect_score"]),
|
|
&"evidence_potential": int(waypoint[&"evidence_potential"]),
|
|
&"travel_minutes": int(waypoint[&"travel_minutes"]),
|
|
&"inspection_minutes": int(waypoint[&"inspection_minutes"]),
|
|
&"time_budget_penalty": int(waypoint[&"time_budget_penalty"]),
|
|
&"utility": int(waypoint[&"utility"]),
|
|
})
|
|
return {
|
|
&"inspection_id": String(inspection_id),
|
|
&"inspector_id": String(inspector_id),
|
|
&"scheduled_at": scheduled_at,
|
|
&"entrance_cell": [entrance_cell.x, entrance_cell.y, entrance_cell.z],
|
|
&"state": State.keys()[state],
|
|
&"state_changed_at": state_changed_at,
|
|
&"time_budget_minutes": time_budget_minutes,
|
|
&"time_spent_minutes": time_spent_minutes,
|
|
&"waypoint_route": saved_route,
|
|
&"current_waypoint_index": current_waypoint_index,
|
|
&"visited_waypoint_ids": _names_to_strings(visited_waypoint_ids),
|
|
&"inaccessible_waypoint_ids": _names_to_strings(inaccessible_waypoint_ids),
|
|
&"discovered_evidence_ids": _names_to_strings(discovered_evidence_ids),
|
|
&"evidence_score": evidence_score,
|
|
&"result": Result.keys()[result],
|
|
&"result_reason": String(result_reason),
|
|
&"exit_reason": String(exit_reason),
|
|
&"resolution_id": String(resolution_id),
|
|
&"closed_at": closed_at,
|
|
&"revision": revision,
|
|
}
|
|
|
|
|
|
static func from_snapshot(snapshot: Dictionary) -> AuthorityInspection:
|
|
if not _snapshot_is_valid(snapshot):
|
|
return null
|
|
var inspection := AuthorityInspection.new().configure(
|
|
StringName(_value(snapshot, &"inspection_id", &"")),
|
|
StringName(_value(snapshot, &"inspector_id", &"")),
|
|
int(_value(snapshot, &"scheduled_at", -1)),
|
|
snapshot
|
|
)
|
|
return inspection if inspection.is_valid() else null
|
|
|
|
|
|
static func _snapshot_is_valid(snapshot: Dictionary) -> bool:
|
|
for key: StringName in [
|
|
&"inspection_id", &"inspector_id", &"scheduled_at", &"entrance_cell",
|
|
&"state", &"state_changed_at", &"time_budget_minutes", &"time_spent_minutes",
|
|
&"waypoint_route", &"current_waypoint_index", &"visited_waypoint_ids",
|
|
&"inaccessible_waypoint_ids", &"discovered_evidence_ids", &"evidence_score",
|
|
&"result", &"result_reason", &"exit_reason", &"resolution_id",
|
|
&"closed_at", &"revision",
|
|
]:
|
|
if not _has_key(snapshot, key):
|
|
return false
|
|
if StringName(_value(snapshot, &"inspection_id", &"")).is_empty() \
|
|
or StringName(_value(snapshot, &"inspector_id", &"")).is_empty():
|
|
return false
|
|
if not _value(snapshot, &"entrance_cell", null) is Array \
|
|
or (_value(snapshot, &"entrance_cell", []) as Array).size() != 3:
|
|
return false
|
|
for array_key: StringName in [
|
|
&"waypoint_route", &"visited_waypoint_ids", &"inaccessible_waypoint_ids",
|
|
&"discovered_evidence_ids",
|
|
]:
|
|
if not _value(snapshot, array_key, null) is Array:
|
|
return false
|
|
for names_key: StringName in [
|
|
&"visited_waypoint_ids", &"inaccessible_waypoint_ids", &"discovered_evidence_ids",
|
|
]:
|
|
if not _name_array_is_canonical(_value(snapshot, names_key, [])):
|
|
return false
|
|
if not State.keys().has(String(_value(snapshot, &"state", ""))) \
|
|
or not Result.keys().has(String(_value(snapshot, &"result", ""))):
|
|
return false
|
|
if int(_value(snapshot, &"scheduled_at", -1)) < 0 \
|
|
or int(_value(snapshot, &"state_changed_at", -1)) < 0 \
|
|
or int(_value(snapshot, &"time_budget_minutes", 0)) <= 0 \
|
|
or int(_value(snapshot, &"time_spent_minutes", -1)) < 0 \
|
|
or int(_value(snapshot, &"current_waypoint_index", -1)) < 0 \
|
|
or int(_value(snapshot, &"evidence_score", -1)) < 0 \
|
|
or int(_value(snapshot, &"closed_at", -2)) < -1 \
|
|
or int(_value(snapshot, &"revision", -1)) < 0:
|
|
return false
|
|
for raw_waypoint: Variant in _value(snapshot, &"waypoint_route", []):
|
|
if not raw_waypoint is Dictionary:
|
|
return false
|
|
for waypoint_key: StringName in [
|
|
&"waypoint_id", &"cell", &"suspect_score", &"evidence_potential",
|
|
&"travel_minutes", &"inspection_minutes", &"time_budget_penalty", &"utility",
|
|
]:
|
|
if not _has_key(raw_waypoint, waypoint_key):
|
|
return false
|
|
if not _value(raw_waypoint, &"cell", null) is Array \
|
|
or (_value(raw_waypoint, &"cell", []) as Array).size() != 3:
|
|
return false
|
|
var normalized := normalize_waypoint(raw_waypoint)
|
|
if normalized.is_empty() or int(_value(raw_waypoint, &"utility", 2_147_483_647)) != int(normalized[&"utility"]):
|
|
return false
|
|
return true
|
|
|
|
|
|
static func normalize_waypoint(value: Dictionary) -> Dictionary:
|
|
var waypoint_id := StringName(_value(value, &"waypoint_id", &""))
|
|
if waypoint_id.is_empty():
|
|
return {}
|
|
var suspect_score := clampi(int(_value(value, &"suspect_score", 0)), 0, 100)
|
|
var evidence_potential := clampi(int(_value(value, &"evidence_potential", 0)), 0, 100)
|
|
var travel_minutes := maxi(int(_value(value, &"travel_minutes", 0)), 0)
|
|
var inspection_minutes := maxi(int(_value(value, &"inspection_minutes", 5)), 1)
|
|
var time_budget_penalty := maxi(int(_value(value, &"time_budget_penalty", 0)), 0)
|
|
return {
|
|
&"waypoint_id": waypoint_id,
|
|
&"cell": _vector3i(_value(value, &"cell", Vector3i.ZERO)),
|
|
&"suspect_score": suspect_score,
|
|
&"evidence_potential": evidence_potential,
|
|
&"travel_minutes": travel_minutes,
|
|
&"inspection_minutes": inspection_minutes,
|
|
&"time_budget_penalty": time_budget_penalty,
|
|
&"utility": suspect_score * 5 + evidence_potential * 4 - travel_minutes * 2 - time_budget_penalty,
|
|
}
|
|
|
|
|
|
static func _state_from_value(value: Variant) -> State:
|
|
if value is String or value is StringName:
|
|
var index := State.keys().find(String(value))
|
|
return (index as State) if index >= 0 else State.SCHEDULED
|
|
var number := int(value)
|
|
return (number as State) if number >= 0 and number < State.size() else State.SCHEDULED
|
|
|
|
|
|
static func _result_from_value(value: Variant) -> Result:
|
|
if value is String or value is StringName:
|
|
var index := Result.keys().find(String(value))
|
|
return (index as Result) if index >= 0 else Result.PENDING
|
|
var number := int(value)
|
|
return (number as Result) if number >= 0 and number < Result.size() else Result.PENDING
|
|
|
|
|
|
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]))
|
|
if value is Dictionary:
|
|
return Vector3i(int(value.get(&"x", 0)), int(value.get(&"y", 0)), int(value.get(&"z", 0)))
|
|
return Vector3i.ZERO
|
|
|
|
|
|
static func _cell_to_array(value: Variant) -> Array[int]:
|
|
var cell := _vector3i(value)
|
|
return [cell.x, cell.y, cell.z]
|
|
|
|
|
|
static func _sorted_unique_names(values: Variant) -> Array[StringName]:
|
|
var result: Array[StringName] = []
|
|
if values is Array:
|
|
for raw_value: Variant in values:
|
|
var value := StringName(raw_value)
|
|
if not value.is_empty() and not result.has(value):
|
|
result.append(value)
|
|
result.sort_custom(_name_less)
|
|
return result
|
|
|
|
|
|
static func _names_to_strings(values: Array[StringName]) -> Array[String]:
|
|
var result: Array[String] = []
|
|
for value: StringName in values:
|
|
result.append(String(value))
|
|
return result
|
|
|
|
|
|
static func _name_less(a: StringName, b: StringName) -> bool:
|
|
return String(a) < String(b)
|
|
|
|
|
|
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
|
|
if source.has(key):
|
|
return source[key]
|
|
return source.get(String(key), fallback)
|
|
|
|
|
|
static func _has_key(source: Dictionary, key: StringName) -> bool:
|
|
return source.has(key) or source.has(String(key))
|
|
|
|
|
|
static func _name_array_is_canonical(value: Variant) -> bool:
|
|
if not value is Array:
|
|
return false
|
|
var previous := ""
|
|
for index: int in range(value.size()):
|
|
var current := String(value[index])
|
|
if current.is_empty() or (index > 0 and current <= previous):
|
|
return false
|
|
previous = current
|
|
return true
|