Preserve approved pixel tower prototype as development baseline
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://dg3e85o0gd2pd
|
||||
@@ -0,0 +1,170 @@
|
||||
class_name AuthorityEvidence
|
||||
extends RefCounted
|
||||
|
||||
enum DiscoveryMethod {
|
||||
LATENT,
|
||||
REACHED,
|
||||
LINE_OF_SIGHT,
|
||||
TRUSTED_SOURCE,
|
||||
KNOWN_ILLEGAL,
|
||||
}
|
||||
|
||||
var evidence_id: StringName = &""
|
||||
var incident_id: StringName = &""
|
||||
var source: StringName = &""
|
||||
var severity := 0
|
||||
var facility_id: StringName = &""
|
||||
var location_cell := Vector3i.ZERO
|
||||
var created_at := 0
|
||||
var expires_at := -1
|
||||
var discovered_at := -1
|
||||
var discovered_by: StringName = &""
|
||||
var discovery_method: DiscoveryMethod = DiscoveryMethod.LATENT
|
||||
var revision := 0
|
||||
|
||||
|
||||
func configure(
|
||||
p_evidence_id: StringName,
|
||||
p_source: StringName,
|
||||
p_severity: int,
|
||||
options: Dictionary = {}
|
||||
) -> AuthorityEvidence:
|
||||
evidence_id = p_evidence_id
|
||||
incident_id = StringName(_value(options, &"incident_id", &""))
|
||||
source = p_source
|
||||
severity = clampi(p_severity, 0, 100)
|
||||
facility_id = StringName(_value(options, &"facility_id", &""))
|
||||
location_cell = _vector3i(_value(options, &"location_cell", Vector3i.ZERO))
|
||||
created_at = maxi(int(_value(options, &"created_at", 0)), 0)
|
||||
expires_at = int(_value(options, &"expires_at", -1))
|
||||
discovered_at = int(_value(options, &"discovered_at", -1))
|
||||
discovered_by = StringName(_value(options, &"discovered_by", &""))
|
||||
discovery_method = _method_from_value(_value(options, &"discovery_method", DiscoveryMethod.LATENT))
|
||||
revision = maxi(int(_value(options, &"revision", 0)), 0)
|
||||
return self
|
||||
|
||||
|
||||
func is_discovered() -> bool:
|
||||
return discovered_at >= 0
|
||||
|
||||
|
||||
func is_expired(absolute_minute: int) -> bool:
|
||||
return expires_at >= 0 and absolute_minute >= expires_at
|
||||
|
||||
|
||||
func mark_discovered(
|
||||
inspector_id: StringName,
|
||||
absolute_minute: int,
|
||||
method: DiscoveryMethod
|
||||
) -> bool:
|
||||
if is_discovered() or inspector_id.is_empty() or absolute_minute < created_at:
|
||||
return false
|
||||
if method == DiscoveryMethod.LATENT:
|
||||
return false
|
||||
discovered_at = absolute_minute
|
||||
discovered_by = inspector_id
|
||||
discovery_method = method
|
||||
revision += 1
|
||||
return true
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
if evidence_id.is_empty() or source.is_empty() or severity < 0 or severity > 100:
|
||||
return false
|
||||
if created_at < 0 or expires_at < -1 or revision < 0:
|
||||
return false
|
||||
if expires_at >= 0 and expires_at < created_at:
|
||||
return false
|
||||
if discovered_at < -1:
|
||||
return false
|
||||
if discovered_at >= 0:
|
||||
if discovered_by.is_empty() or discovery_method == DiscoveryMethod.LATENT:
|
||||
return false
|
||||
if discovered_at < created_at:
|
||||
return false
|
||||
else:
|
||||
if not discovered_by.is_empty() or discovery_method != DiscoveryMethod.LATENT:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {
|
||||
&"evidence_id": String(evidence_id),
|
||||
&"incident_id": String(incident_id),
|
||||
&"source": String(source),
|
||||
&"severity": severity,
|
||||
&"facility_id": String(facility_id),
|
||||
&"location_cell": [location_cell.x, location_cell.y, location_cell.z],
|
||||
&"created_at": created_at,
|
||||
&"expires_at": expires_at,
|
||||
&"discovered_at": discovered_at,
|
||||
&"discovered_by": String(discovered_by),
|
||||
&"discovery_method": DiscoveryMethod.keys()[discovery_method],
|
||||
&"revision": revision,
|
||||
}
|
||||
|
||||
|
||||
static func from_snapshot(snapshot: Dictionary) -> AuthorityEvidence:
|
||||
if not _snapshot_is_valid(snapshot):
|
||||
return null
|
||||
var evidence := AuthorityEvidence.new().configure(
|
||||
StringName(_value(snapshot, &"evidence_id", &"")),
|
||||
StringName(_value(snapshot, &"source", &"")),
|
||||
int(_value(snapshot, &"severity", -1)),
|
||||
snapshot
|
||||
)
|
||||
return evidence if evidence.is_valid() else null
|
||||
|
||||
|
||||
static func _snapshot_is_valid(snapshot: Dictionary) -> bool:
|
||||
for key: StringName in [
|
||||
&"evidence_id", &"incident_id", &"source", &"severity", &"facility_id",
|
||||
&"location_cell", &"created_at", &"expires_at", &"discovered_at",
|
||||
&"discovered_by", &"discovery_method", &"revision",
|
||||
]:
|
||||
if not _has_key(snapshot, key):
|
||||
return false
|
||||
if StringName(_value(snapshot, &"evidence_id", &"")).is_empty() \
|
||||
or StringName(_value(snapshot, &"source", &"")).is_empty():
|
||||
return false
|
||||
if not _value(snapshot, &"location_cell", null) is Array \
|
||||
or (_value(snapshot, &"location_cell", []) as Array).size() != 3:
|
||||
return false
|
||||
var severity_value := int(_value(snapshot, &"severity", -1))
|
||||
if severity_value < 0 or severity_value > 100:
|
||||
return false
|
||||
if int(_value(snapshot, &"created_at", -1)) < 0 \
|
||||
or int(_value(snapshot, &"expires_at", -2)) < -1 \
|
||||
or int(_value(snapshot, &"discovered_at", -2)) < -1 \
|
||||
or int(_value(snapshot, &"revision", -1)) < 0:
|
||||
return false
|
||||
return DiscoveryMethod.keys().has(String(_value(snapshot, &"discovery_method", "")))
|
||||
|
||||
|
||||
static func _method_from_value(value: Variant) -> DiscoveryMethod:
|
||||
if value is String or value is StringName:
|
||||
var index := DiscoveryMethod.keys().find(String(value))
|
||||
return (index as DiscoveryMethod) if index >= 0 else DiscoveryMethod.LATENT
|
||||
var number := int(value)
|
||||
return (number as DiscoveryMethod) if number >= 0 and number < DiscoveryMethod.size() else DiscoveryMethod.LATENT
|
||||
|
||||
|
||||
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 _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))
|
||||
@@ -0,0 +1 @@
|
||||
uid://mnr0hrt63kc3
|
||||
@@ -0,0 +1,309 @@
|
||||
class_name HeatState
|
||||
extends RefCounted
|
||||
|
||||
## Authoritative short-term attention state for the Authority domain. Heat is
|
||||
## deliberately separate from suspicion and discovered evidence.
|
||||
|
||||
const MAX_HEAT := 100
|
||||
const MAX_SUSPICION := 100
|
||||
|
||||
var heat := 0
|
||||
var suspicion := 0
|
||||
var evidence_points := 0
|
||||
var inspection_level := 0
|
||||
var next_inspection_window := 360
|
||||
var active_case_ids: Array[StringName] = []
|
||||
var fines_due := 0
|
||||
var known_illegal_facility_ids: Array[StringName] = []
|
||||
var last_decay_day := 0
|
||||
var revision := 0
|
||||
var heat_history: Array[Dictionary] = []
|
||||
var _next_history_sequence := 1
|
||||
|
||||
|
||||
func configure(options: Dictionary = {}) -> HeatState:
|
||||
heat = clampi(int(_value(options, &"heat", 0)), 0, MAX_HEAT)
|
||||
suspicion = clampi(int(_value(options, &"suspicion", 0)), 0, MAX_SUSPICION)
|
||||
evidence_points = maxi(int(_value(options, &"evidence_points", 0)), 0)
|
||||
inspection_level = clampi(int(_value(options, &"inspection_level", 0)), 0, 5)
|
||||
next_inspection_window = maxi(int(_value(options, &"next_inspection_window", 360)), 0)
|
||||
active_case_ids = _sorted_unique_names(_value(options, &"active_case_ids", []))
|
||||
fines_due = maxi(int(_value(options, &"fines_due", 0)), 0)
|
||||
known_illegal_facility_ids = _sorted_unique_names(
|
||||
_value(options, &"known_illegal_facility_ids", [])
|
||||
)
|
||||
last_decay_day = maxi(int(_value(options, &"last_decay_day", 0)), 0)
|
||||
revision = maxi(int(_value(options, &"revision", 0)), 0)
|
||||
heat_history.clear()
|
||||
var saved_history: Variant = _value(options, &"heat_history", [])
|
||||
if saved_history is Array:
|
||||
for raw_entry: Variant in saved_history:
|
||||
if raw_entry is Dictionary:
|
||||
heat_history.append((raw_entry as Dictionary).duplicate(true))
|
||||
_next_history_sequence = maxi(
|
||||
int(_value(options, &"next_history_sequence", heat_history.size() + 1)),
|
||||
1
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
func apply_heat_delta(
|
||||
delta: int,
|
||||
source_id: StringName,
|
||||
absolute_minute: int,
|
||||
reason: StringName = &""
|
||||
) -> Dictionary:
|
||||
if absolute_minute < 0:
|
||||
return {&"ok": false, &"reason": &"INVALID_MINUTE"}
|
||||
var old_value := heat
|
||||
heat = clampi(heat + delta, 0, MAX_HEAT)
|
||||
var applied := heat - old_value
|
||||
if applied != 0:
|
||||
revision += 1
|
||||
heat_history.append({
|
||||
&"sequence": _next_history_sequence,
|
||||
&"absolute_minute": absolute_minute,
|
||||
&"source_id": String(source_id),
|
||||
&"reason": String(reason),
|
||||
&"requested_delta": delta,
|
||||
&"applied_delta": applied,
|
||||
&"old_value": old_value,
|
||||
&"new_value": heat,
|
||||
})
|
||||
_next_history_sequence += 1
|
||||
return {
|
||||
&"ok": true,
|
||||
&"old_value": old_value,
|
||||
&"new_value": heat,
|
||||
&"requested_delta": delta,
|
||||
&"applied_delta": applied,
|
||||
}
|
||||
|
||||
|
||||
## Applies a given day at most once. An active-case floor can stop decay, but
|
||||
## decay never raises an already-lower Heat value up to that floor.
|
||||
func apply_daily_decay(day_index: int, amount: int, active_case_floor: int) -> Dictionary:
|
||||
if day_index <= last_decay_day:
|
||||
return {&"ok": true, &"is_replay": true, &"old_value": heat, &"new_value": heat}
|
||||
if amount < 0:
|
||||
return {&"ok": false, &"reason": &"INVALID_DECAY"}
|
||||
var old_value := heat
|
||||
var floor_value := 0
|
||||
if not active_case_ids.is_empty():
|
||||
floor_value = clampi(active_case_floor, 0, MAX_HEAT)
|
||||
if floor_value > 0 and heat > floor_value:
|
||||
heat = maxi(heat - amount, floor_value)
|
||||
else:
|
||||
heat = maxi(heat - amount, 0) if floor_value == 0 else heat
|
||||
last_decay_day = day_index
|
||||
revision += 1
|
||||
if heat != old_value:
|
||||
heat_history.append({
|
||||
&"sequence": _next_history_sequence,
|
||||
&"absolute_minute": day_index * 1_440,
|
||||
&"source_id": "daily_decay",
|
||||
&"reason": "DAILY_DECAY",
|
||||
&"requested_delta": -amount,
|
||||
&"applied_delta": heat - old_value,
|
||||
&"old_value": old_value,
|
||||
&"new_value": heat,
|
||||
})
|
||||
_next_history_sequence += 1
|
||||
return {
|
||||
&"ok": true,
|
||||
&"is_replay": false,
|
||||
&"old_value": old_value,
|
||||
&"new_value": heat,
|
||||
&"floor": floor_value,
|
||||
}
|
||||
|
||||
|
||||
func set_suspicion(value: int) -> void:
|
||||
var normalized := clampi(value, 0, MAX_SUSPICION)
|
||||
if suspicion == normalized:
|
||||
return
|
||||
suspicion = normalized
|
||||
revision += 1
|
||||
|
||||
|
||||
func set_evidence_points(value: int) -> void:
|
||||
var normalized := maxi(value, 0)
|
||||
if evidence_points == normalized:
|
||||
return
|
||||
evidence_points = normalized
|
||||
revision += 1
|
||||
|
||||
|
||||
func add_active_case(case_id: StringName) -> bool:
|
||||
if case_id.is_empty() or active_case_ids.has(case_id):
|
||||
return false
|
||||
active_case_ids.append(case_id)
|
||||
active_case_ids.sort_custom(_name_less)
|
||||
revision += 1
|
||||
return true
|
||||
|
||||
|
||||
func remove_active_case(case_id: StringName) -> bool:
|
||||
var index := active_case_ids.find(case_id)
|
||||
if index < 0:
|
||||
return false
|
||||
active_case_ids.remove_at(index)
|
||||
revision += 1
|
||||
return true
|
||||
|
||||
|
||||
func add_known_illegal_facility(facility_id: StringName) -> bool:
|
||||
if facility_id.is_empty() or known_illegal_facility_ids.has(facility_id):
|
||||
return false
|
||||
known_illegal_facility_ids.append(facility_id)
|
||||
known_illegal_facility_ids.sort_custom(_name_less)
|
||||
revision += 1
|
||||
return true
|
||||
|
||||
|
||||
func add_fine_due(amount: int) -> bool:
|
||||
if amount <= 0:
|
||||
return false
|
||||
fines_due += amount
|
||||
revision += 1
|
||||
return true
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
if heat < 0 or heat > MAX_HEAT or suspicion < 0 or suspicion > MAX_SUSPICION:
|
||||
return false
|
||||
if evidence_points < 0 or inspection_level < 0 or inspection_level > 5:
|
||||
return false
|
||||
if next_inspection_window < 0 or fines_due < 0 or last_decay_day < 0 or revision < 0:
|
||||
return false
|
||||
if active_case_ids != _sorted_unique_names(active_case_ids):
|
||||
return false
|
||||
if known_illegal_facility_ids != _sorted_unique_names(known_illegal_facility_ids):
|
||||
return false
|
||||
var last_sequence := 0
|
||||
for entry: Dictionary in heat_history:
|
||||
var sequence := int(entry.get(&"sequence", 0))
|
||||
if sequence <= last_sequence:
|
||||
return false
|
||||
last_sequence = sequence
|
||||
return _next_history_sequence > last_sequence
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {
|
||||
&"heat": heat,
|
||||
&"suspicion": suspicion,
|
||||
&"evidence_points": evidence_points,
|
||||
&"inspection_level": inspection_level,
|
||||
&"next_inspection_window": next_inspection_window,
|
||||
&"active_case_ids": _names_to_strings(active_case_ids),
|
||||
&"fines_due": fines_due,
|
||||
&"known_illegal_facility_ids": _names_to_strings(known_illegal_facility_ids),
|
||||
&"last_decay_day": last_decay_day,
|
||||
&"revision": revision,
|
||||
&"heat_history": heat_history.duplicate(true),
|
||||
&"next_history_sequence": _next_history_sequence,
|
||||
}
|
||||
|
||||
|
||||
static func from_snapshot(snapshot: Dictionary) -> HeatState:
|
||||
if not _snapshot_is_valid(snapshot):
|
||||
return null
|
||||
var state := HeatState.new().configure(snapshot)
|
||||
return state if state.is_valid() else null
|
||||
|
||||
|
||||
static func _snapshot_is_valid(snapshot: Dictionary) -> bool:
|
||||
for key: StringName in [
|
||||
&"heat", &"suspicion", &"evidence_points", &"inspection_level",
|
||||
&"next_inspection_window", &"active_case_ids", &"fines_due",
|
||||
&"known_illegal_facility_ids", &"last_decay_day", &"revision",
|
||||
&"heat_history", &"next_history_sequence",
|
||||
]:
|
||||
if not _has_key(snapshot, key):
|
||||
return false
|
||||
if not _value(snapshot, &"active_case_ids", null) is Array \
|
||||
or not _value(snapshot, &"known_illegal_facility_ids", null) is Array \
|
||||
or not _value(snapshot, &"heat_history", null) is Array:
|
||||
return false
|
||||
if not _name_array_is_canonical(_value(snapshot, &"active_case_ids", [])) \
|
||||
or not _name_array_is_canonical(_value(snapshot, &"known_illegal_facility_ids", [])):
|
||||
return false
|
||||
var raw_heat := int(_value(snapshot, &"heat", -1))
|
||||
var raw_suspicion := int(_value(snapshot, &"suspicion", -1))
|
||||
var raw_level := int(_value(snapshot, &"inspection_level", -1))
|
||||
if raw_heat < 0 or raw_heat > MAX_HEAT or raw_suspicion < 0 or raw_suspicion > MAX_SUSPICION:
|
||||
return false
|
||||
if int(_value(snapshot, &"evidence_points", -1)) < 0 or raw_level < 0 or raw_level > 5:
|
||||
return false
|
||||
if int(_value(snapshot, &"next_inspection_window", -1)) < 0 \
|
||||
or int(_value(snapshot, &"fines_due", -1)) < 0 \
|
||||
or int(_value(snapshot, &"last_decay_day", -1)) < 0 \
|
||||
or int(_value(snapshot, &"revision", -1)) < 0:
|
||||
return false
|
||||
var last_sequence := 0
|
||||
for raw_entry: Variant in _value(snapshot, &"heat_history", []):
|
||||
if not raw_entry is Dictionary:
|
||||
return false
|
||||
for entry_key: StringName in [
|
||||
&"sequence", &"absolute_minute", &"source_id", &"reason",
|
||||
&"requested_delta", &"applied_delta", &"old_value", &"new_value",
|
||||
]:
|
||||
if not _has_key(raw_entry, entry_key):
|
||||
return false
|
||||
var sequence := int(_value(raw_entry, &"sequence", 0))
|
||||
var old_value := int(_value(raw_entry, &"old_value", -1))
|
||||
var new_value := int(_value(raw_entry, &"new_value", -1))
|
||||
var applied_delta := int(_value(raw_entry, &"applied_delta", 999))
|
||||
if sequence <= last_sequence or int(_value(raw_entry, &"absolute_minute", -1)) < 0:
|
||||
return false
|
||||
if old_value < 0 or old_value > MAX_HEAT or new_value < 0 or new_value > MAX_HEAT:
|
||||
return false
|
||||
if new_value - old_value != applied_delta:
|
||||
return false
|
||||
last_sequence = sequence
|
||||
return int(_value(snapshot, &"next_history_sequence", 0)) > last_sequence
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjymlwh4jkx64
|
||||
@@ -0,0 +1,226 @@
|
||||
class_name AuthorityIncident
|
||||
extends RefCounted
|
||||
|
||||
enum State {
|
||||
OPEN,
|
||||
RESOLVED,
|
||||
DISMISSED,
|
||||
}
|
||||
|
||||
var incident_id: StringName = &""
|
||||
var incident_type: StringName = &""
|
||||
var created_at := 0
|
||||
var location_cell := Vector3i.ZERO
|
||||
var facility_id: StringName = &""
|
||||
var actor_ids: Array[StringName] = []
|
||||
var severity := 0
|
||||
var heat_delta := 0
|
||||
var detector_ids: Array[StringName] = []
|
||||
var evidence_ids: Array[StringName] = []
|
||||
var state: State = State.OPEN
|
||||
var resolution_id: StringName = &""
|
||||
var state_reason: StringName = &""
|
||||
var revision := 0
|
||||
|
||||
|
||||
func configure(
|
||||
p_incident_id: StringName,
|
||||
p_incident_type: StringName,
|
||||
p_created_at: int,
|
||||
options: Dictionary = {}
|
||||
) -> AuthorityIncident:
|
||||
incident_id = p_incident_id
|
||||
incident_type = p_incident_type
|
||||
created_at = maxi(p_created_at, 0)
|
||||
location_cell = _vector3i(_value(options, &"location_cell", Vector3i.ZERO))
|
||||
facility_id = StringName(_value(options, &"facility_id", &""))
|
||||
actor_ids = _sorted_unique_names(_value(options, &"actor_ids", []))
|
||||
severity = clampi(int(_value(options, &"severity", 0)), 0, 100)
|
||||
heat_delta = clampi(int(_value(options, &"heat_delta", severity / 5)), 0, 100)
|
||||
detector_ids = _sorted_unique_names(_value(options, &"detector_ids", []))
|
||||
evidence_ids = _sorted_unique_names(_value(options, &"evidence_ids", []))
|
||||
state = _state_from_value(_value(options, &"state", State.OPEN))
|
||||
resolution_id = StringName(_value(options, &"resolution_id", &""))
|
||||
state_reason = StringName(_value(options, &"state_reason", &""))
|
||||
revision = maxi(int(_value(options, &"revision", 0)), 0)
|
||||
return self
|
||||
|
||||
|
||||
func add_detector(detector_id: StringName) -> bool:
|
||||
if detector_id.is_empty() or detector_ids.has(detector_id):
|
||||
return false
|
||||
detector_ids.append(detector_id)
|
||||
detector_ids.sort_custom(_name_less)
|
||||
revision += 1
|
||||
return true
|
||||
|
||||
|
||||
func add_evidence(evidence_id: StringName) -> bool:
|
||||
if evidence_id.is_empty() or evidence_ids.has(evidence_id):
|
||||
return false
|
||||
evidence_ids.append(evidence_id)
|
||||
evidence_ids.sort_custom(_name_less)
|
||||
revision += 1
|
||||
return true
|
||||
|
||||
|
||||
func resolve(p_resolution_id: StringName, reason: StringName = &"") -> bool:
|
||||
if state != State.OPEN or p_resolution_id.is_empty():
|
||||
return false
|
||||
state = State.RESOLVED
|
||||
resolution_id = p_resolution_id
|
||||
state_reason = reason
|
||||
revision += 1
|
||||
return true
|
||||
|
||||
|
||||
func dismiss(reason: StringName) -> bool:
|
||||
if state != State.OPEN:
|
||||
return false
|
||||
state = State.DISMISSED
|
||||
state_reason = reason
|
||||
revision += 1
|
||||
return true
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
if incident_id.is_empty() or incident_type.is_empty() or created_at < 0:
|
||||
return false
|
||||
if severity < 0 or severity > 100 or heat_delta < 0 or heat_delta > 100 or revision < 0:
|
||||
return false
|
||||
if actor_ids != _sorted_unique_names(actor_ids):
|
||||
return false
|
||||
if detector_ids != _sorted_unique_names(detector_ids):
|
||||
return false
|
||||
if evidence_ids != _sorted_unique_names(evidence_ids):
|
||||
return false
|
||||
if state == State.RESOLVED and resolution_id.is_empty():
|
||||
return false
|
||||
if state != State.RESOLVED and not resolution_id.is_empty():
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {
|
||||
&"incident_id": String(incident_id),
|
||||
&"incident_type": String(incident_type),
|
||||
&"created_at": created_at,
|
||||
&"location_cell": [location_cell.x, location_cell.y, location_cell.z],
|
||||
&"facility_id": String(facility_id),
|
||||
&"actor_ids": _names_to_strings(actor_ids),
|
||||
&"severity": severity,
|
||||
&"heat_delta": heat_delta,
|
||||
&"detector_ids": _names_to_strings(detector_ids),
|
||||
&"evidence_ids": _names_to_strings(evidence_ids),
|
||||
&"state": State.keys()[state],
|
||||
&"resolution_id": String(resolution_id),
|
||||
&"state_reason": String(state_reason),
|
||||
&"revision": revision,
|
||||
}
|
||||
|
||||
|
||||
static func from_snapshot(snapshot: Dictionary) -> AuthorityIncident:
|
||||
if not _snapshot_is_valid(snapshot):
|
||||
return null
|
||||
var incident := AuthorityIncident.new().configure(
|
||||
StringName(_value(snapshot, &"incident_id", &"")),
|
||||
StringName(_value(snapshot, &"incident_type", &"")),
|
||||
int(_value(snapshot, &"created_at", -1)),
|
||||
snapshot
|
||||
)
|
||||
return incident if incident.is_valid() else null
|
||||
|
||||
|
||||
static func _snapshot_is_valid(snapshot: Dictionary) -> bool:
|
||||
for key: StringName in [
|
||||
&"incident_id", &"incident_type", &"created_at", &"location_cell",
|
||||
&"facility_id", &"actor_ids", &"severity", &"heat_delta",
|
||||
&"detector_ids", &"evidence_ids", &"state", &"resolution_id",
|
||||
&"state_reason", &"revision",
|
||||
]:
|
||||
if not _has_key(snapshot, key):
|
||||
return false
|
||||
if StringName(_value(snapshot, &"incident_id", &"")).is_empty() \
|
||||
or StringName(_value(snapshot, &"incident_type", &"")).is_empty():
|
||||
return false
|
||||
if not _value(snapshot, &"location_cell", null) is Array \
|
||||
or (_value(snapshot, &"location_cell", []) as Array).size() != 3:
|
||||
return false
|
||||
if not _value(snapshot, &"actor_ids", null) is Array \
|
||||
or not _value(snapshot, &"detector_ids", null) is Array \
|
||||
or not _value(snapshot, &"evidence_ids", null) is Array:
|
||||
return false
|
||||
if not _name_array_is_canonical(_value(snapshot, &"actor_ids", [])) \
|
||||
or not _name_array_is_canonical(_value(snapshot, &"detector_ids", [])) \
|
||||
or not _name_array_is_canonical(_value(snapshot, &"evidence_ids", [])):
|
||||
return false
|
||||
var severity_value := int(_value(snapshot, &"severity", -1))
|
||||
var heat_value := int(_value(snapshot, &"heat_delta", -1))
|
||||
if int(_value(snapshot, &"created_at", -1)) < 0 or severity_value < 0 or severity_value > 100:
|
||||
return false
|
||||
if heat_value < 0 or heat_value > 100 or int(_value(snapshot, &"revision", -1)) < 0:
|
||||
return false
|
||||
return State.keys().has(String(_value(snapshot, &"state", "")))
|
||||
|
||||
|
||||
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.OPEN
|
||||
var number := int(value)
|
||||
return (number as State) if number >= 0 and number < State.size() else State.OPEN
|
||||
|
||||
|
||||
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 _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
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0ao5te7fi1i3
|
||||
@@ -0,0 +1,390 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://6n3jyvx6a6lq
|
||||
Reference in New Issue
Block a user