Preserve approved pixel tower prototype as development baseline
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
class_name FacilityInstance
|
||||
extends RefCounted
|
||||
|
||||
const ServiceInstanceType = preload("res://scripts/facility/service_instance.gd")
|
||||
|
||||
## Values are deliberately ordered by operational priority.
|
||||
enum State {
|
||||
BROKEN,
|
||||
NO_ACCESS,
|
||||
NO_POWER,
|
||||
NO_STAFF,
|
||||
CLOSED,
|
||||
DEGRADED,
|
||||
OPEN,
|
||||
}
|
||||
|
||||
const FULL_DAY_MINUTES := 1_440
|
||||
const DEFAULT_DURABILITY := 10_000
|
||||
const DEGRADED_DURABILITY_THRESHOLD := 3_000
|
||||
|
||||
var instance_id: StringName = &""
|
||||
var definition_id: StringName = &""
|
||||
var origin := Vector3i.ZERO
|
||||
var rotation := 0
|
||||
var occupied_cells: Array[Vector3i] = []
|
||||
var level := 1
|
||||
var state: State = State.OPEN
|
||||
var state_reasons: Array[StringName] = []
|
||||
var durability := DEFAULT_DURABILITY
|
||||
var local_inventory_id: StringName = &""
|
||||
var assigned_staff_by_shift: Dictionary = {}
|
||||
var active_staff_ids: Array[StringName] = []
|
||||
var queue_entries: Array[StringName] = []
|
||||
var active_services: Dictionary = {}
|
||||
var policy_current: Dictionary = {}
|
||||
var policy_pending: Dictionary = {}
|
||||
var policy_revision := 0
|
||||
var policy_pending_revision := 0
|
||||
var policy_pending_effective_at := -1
|
||||
var statistics_today: Dictionary = {}
|
||||
var lifetime_statistics: Dictionary = {}
|
||||
var created_at := 0
|
||||
|
||||
var capacity := 1
|
||||
var queue_capacity := 0
|
||||
var base_service_minutes := 1
|
||||
var base_quality := 0
|
||||
var base_price := 0
|
||||
var staff_min := 0
|
||||
var staff_max := 0
|
||||
var access_available := true
|
||||
var power_available := true
|
||||
var operator_open := true
|
||||
var opening_intervals: Array[Vector2i] = [Vector2i(0, FULL_DAY_MINUTES)]
|
||||
var degraded_reasons: Array[StringName] = []
|
||||
|
||||
|
||||
func configure(
|
||||
p_instance_id: StringName,
|
||||
p_definition_id: StringName,
|
||||
options: Dictionary = {}
|
||||
) -> FacilityInstance:
|
||||
instance_id = p_instance_id
|
||||
definition_id = p_definition_id
|
||||
origin = _vector3i(_value(options, &"origin", Vector3i.ZERO))
|
||||
rotation = _normalize_rotation(int(_value(options, &"rotation", 0)))
|
||||
occupied_cells = _cell_array(_value(options, &"occupied_cells", []))
|
||||
level = maxi(int(_value(options, &"level", 1)), 1)
|
||||
durability = clampi(int(_value(options, &"durability", DEFAULT_DURABILITY)), 0, DEFAULT_DURABILITY)
|
||||
local_inventory_id = StringName(_value(options, &"local_inventory_id", &""))
|
||||
capacity = maxi(int(_value(options, &"capacity", 1)), 1)
|
||||
queue_capacity = maxi(int(_value(options, &"queue_capacity", 0)), 0)
|
||||
base_service_minutes = maxi(int(_value(options, &"base_service_minutes", 1)), 1)
|
||||
base_quality = int(_value(options, &"base_quality", 0))
|
||||
base_price = maxi(int(_value(options, &"base_price", 0)), 0)
|
||||
staff_min = maxi(int(_value(options, &"staff_min", 0)), 0)
|
||||
staff_max = maxi(int(_value(options, &"staff_max", maxi(staff_min, 0))), staff_min)
|
||||
policy_current = _normalize_policy(_value(options, &"policy_current", {}))
|
||||
policy_revision = maxi(int(_value(options, &"policy_revision", 0)), 0)
|
||||
created_at = maxi(int(_value(options, &"created_at", 0)), 0)
|
||||
access_available = bool(_value(options, &"access_available", true))
|
||||
power_available = bool(_value(options, &"power_available", true))
|
||||
operator_open = bool(_value(options, &"operator_open", true))
|
||||
var intervals := _parse_intervals(_value(options, &"opening_intervals", []))
|
||||
if not intervals.is_empty():
|
||||
opening_intervals = intervals
|
||||
active_staff_ids = _sorted_unique_names(_value(options, &"active_staff_ids", []))
|
||||
degraded_reasons = _sorted_unique_names(_value(options, &"degraded_reasons", []))
|
||||
evaluate_state(created_at)
|
||||
return self
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return not instance_id.is_empty() \
|
||||
and not definition_id.is_empty() \
|
||||
and capacity > 0 \
|
||||
and queue_capacity >= 0 \
|
||||
and base_service_minutes > 0 \
|
||||
and staff_min >= 0 \
|
||||
and staff_max >= staff_min \
|
||||
and _intervals_are_valid(opening_intervals)
|
||||
|
||||
|
||||
func set_opening_hours(open_minute_of_day: int, close_minute_of_day: int) -> bool:
|
||||
if open_minute_of_day < 0 or open_minute_of_day >= FULL_DAY_MINUTES:
|
||||
return false
|
||||
if close_minute_of_day < 0 or close_minute_of_day > FULL_DAY_MINUTES:
|
||||
return false
|
||||
if open_minute_of_day == close_minute_of_day:
|
||||
opening_intervals = [Vector2i(0, FULL_DAY_MINUTES)]
|
||||
elif open_minute_of_day < close_minute_of_day:
|
||||
opening_intervals = [Vector2i(open_minute_of_day, close_minute_of_day)]
|
||||
else:
|
||||
opening_intervals = []
|
||||
if close_minute_of_day > 0:
|
||||
opening_intervals.append(Vector2i(0, close_minute_of_day))
|
||||
if open_minute_of_day < FULL_DAY_MINUTES:
|
||||
opening_intervals.append(Vector2i(open_minute_of_day, FULL_DAY_MINUTES))
|
||||
return true
|
||||
|
||||
|
||||
func set_opening_intervals(intervals: Variant) -> bool:
|
||||
var parsed := _parse_intervals(intervals)
|
||||
if parsed.is_empty() or not _intervals_are_valid(parsed):
|
||||
return false
|
||||
opening_intervals = parsed
|
||||
return true
|
||||
|
||||
|
||||
func is_within_opening_hours(absolute_minute: int) -> bool:
|
||||
var minute_of_day := posmod(absolute_minute, FULL_DAY_MINUTES)
|
||||
for interval: Vector2i in opening_intervals:
|
||||
if minute_of_day >= interval.x and minute_of_day < interval.y:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func set_operational_conditions(conditions: Dictionary, absolute_minute: int) -> bool:
|
||||
if _has_value(conditions, &"durability"):
|
||||
durability = clampi(int(_value(conditions, &"durability", durability)), 0, DEFAULT_DURABILITY)
|
||||
if _has_value(conditions, &"access_available"):
|
||||
access_available = bool(_value(conditions, &"access_available", access_available))
|
||||
if _has_value(conditions, &"power_available"):
|
||||
power_available = bool(_value(conditions, &"power_available", power_available))
|
||||
if _has_value(conditions, &"operator_open"):
|
||||
operator_open = bool(_value(conditions, &"operator_open", operator_open))
|
||||
if _has_value(conditions, &"active_staff_ids"):
|
||||
active_staff_ids = _sorted_unique_names(_value(conditions, &"active_staff_ids", []))
|
||||
if _has_value(conditions, &"degraded_reasons"):
|
||||
degraded_reasons = _sorted_unique_names(_value(conditions, &"degraded_reasons", []))
|
||||
return evaluate_state(absolute_minute)
|
||||
|
||||
|
||||
func evaluate_state(absolute_minute: int) -> bool:
|
||||
var next_reasons: Array[StringName] = []
|
||||
var next_state := State.OPEN
|
||||
if durability <= 0:
|
||||
next_reasons.append(&"BROKEN")
|
||||
next_state = State.BROKEN
|
||||
if not access_available:
|
||||
next_reasons.append(&"NO_ACCESS")
|
||||
if next_state > State.NO_ACCESS:
|
||||
next_state = State.NO_ACCESS
|
||||
if not power_available:
|
||||
next_reasons.append(&"NO_POWER")
|
||||
if next_state > State.NO_POWER:
|
||||
next_state = State.NO_POWER
|
||||
if active_staff_ids.size() < staff_min:
|
||||
next_reasons.append(&"NO_STAFF")
|
||||
if next_state > State.NO_STAFF:
|
||||
next_state = State.NO_STAFF
|
||||
if not operator_open:
|
||||
next_reasons.append(&"CLOSED_BY_OPERATOR")
|
||||
if next_state > State.CLOSED:
|
||||
next_state = State.CLOSED
|
||||
if not is_within_opening_hours(absolute_minute):
|
||||
next_reasons.append(&"OUTSIDE_OPENING_HOURS")
|
||||
if next_state > State.CLOSED:
|
||||
next_state = State.CLOSED
|
||||
if durability > 0 and durability < DEGRADED_DURABILITY_THRESHOLD:
|
||||
next_reasons.append(&"LOW_DURABILITY")
|
||||
if next_state > State.DEGRADED:
|
||||
next_state = State.DEGRADED
|
||||
for reason: StringName in degraded_reasons:
|
||||
if not next_reasons.has(reason):
|
||||
next_reasons.append(reason)
|
||||
if next_state > State.DEGRADED:
|
||||
next_state = State.DEGRADED
|
||||
var changed := state != next_state
|
||||
state = next_state
|
||||
state_reasons = next_reasons
|
||||
return changed
|
||||
|
||||
|
||||
func can_start_service(absolute_minute: int) -> bool:
|
||||
evaluate_state(absolute_minute)
|
||||
return state in [State.OPEN, State.DEGRADED] and available_slots() > 0
|
||||
|
||||
|
||||
func available_slots() -> int:
|
||||
return maxi(capacity - active_services.size(), 0)
|
||||
|
||||
|
||||
func has_queue_space() -> bool:
|
||||
return queue_entries.size() < queue_capacity
|
||||
|
||||
|
||||
func apply_policy_patch(patch: Dictionary) -> bool:
|
||||
var changed := false
|
||||
var keys: Array = patch.keys()
|
||||
keys.sort_custom(func(a: Variant, b: Variant) -> bool: return String(a) < String(b))
|
||||
for raw_key: Variant in keys:
|
||||
var key := StringName(raw_key)
|
||||
var value: Variant = patch[raw_key]
|
||||
var has_existing := _has_value(policy_current, key)
|
||||
var existing: Variant = _value(policy_current, key, null)
|
||||
if not has_existing or existing != value:
|
||||
policy_current.erase(String(key))
|
||||
policy_current[key] = _copy(value)
|
||||
changed = true
|
||||
if changed:
|
||||
policy_revision += 1
|
||||
return changed
|
||||
|
||||
|
||||
func schedule_policy_patch(patch: Dictionary, effective_at: int) -> bool:
|
||||
if effective_at < 0 or patch.is_empty():
|
||||
return false
|
||||
policy_pending = _normalize_policy(patch)
|
||||
policy_pending_effective_at = effective_at
|
||||
policy_pending_revision = policy_revision + 1
|
||||
return true
|
||||
|
||||
|
||||
func apply_pending_policy(absolute_minute: int) -> bool:
|
||||
if policy_pending.is_empty() or policy_pending_effective_at < 0:
|
||||
return false
|
||||
if absolute_minute < policy_pending_effective_at:
|
||||
return false
|
||||
var changed := apply_policy_patch(policy_pending)
|
||||
policy_pending.clear()
|
||||
policy_pending_effective_at = -1
|
||||
policy_pending_revision = 0
|
||||
return changed
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
var saved_services: Array[Dictionary] = []
|
||||
var service_ids := _sorted_keys(active_services)
|
||||
for service_id: StringName in service_ids:
|
||||
var service: ServiceInstance = active_services[service_id]
|
||||
saved_services.append(service.create_snapshot())
|
||||
return {
|
||||
&"instance_id": String(instance_id),
|
||||
&"definition_id": String(definition_id),
|
||||
&"origin": [origin.x, origin.y, origin.z],
|
||||
&"rotation": rotation,
|
||||
&"occupied_cells": _serialize_cells(occupied_cells),
|
||||
&"level": level,
|
||||
&"state": State.keys()[state],
|
||||
&"state_reasons": _names_to_strings(state_reasons),
|
||||
&"durability": durability,
|
||||
&"local_inventory_id": String(local_inventory_id),
|
||||
&"assigned_staff_by_shift": assigned_staff_by_shift.duplicate(true),
|
||||
&"active_staff_ids": _names_to_strings(active_staff_ids),
|
||||
&"queue_entries": _names_to_strings(queue_entries),
|
||||
&"active_services": saved_services,
|
||||
&"policy_current": policy_current.duplicate(true),
|
||||
&"policy_pending": policy_pending.duplicate(true),
|
||||
&"policy_revision": policy_revision,
|
||||
&"policy_pending_revision": policy_pending_revision,
|
||||
&"policy_pending_effective_at": policy_pending_effective_at,
|
||||
&"statistics_today": statistics_today.duplicate(true),
|
||||
&"lifetime_statistics": lifetime_statistics.duplicate(true),
|
||||
&"created_at": created_at,
|
||||
&"capacity": capacity,
|
||||
&"queue_capacity": queue_capacity,
|
||||
&"base_service_minutes": base_service_minutes,
|
||||
&"base_quality": base_quality,
|
||||
&"base_price": base_price,
|
||||
&"staff_min": staff_min,
|
||||
&"staff_max": staff_max,
|
||||
&"access_available": access_available,
|
||||
&"power_available": power_available,
|
||||
&"operator_open": operator_open,
|
||||
&"opening_intervals": _serialize_intervals(opening_intervals),
|
||||
&"degraded_reasons": _names_to_strings(degraded_reasons),
|
||||
}
|
||||
|
||||
|
||||
static func from_snapshot(snapshot: Dictionary) -> FacilityInstance:
|
||||
if not _snapshot_shapes_are_valid(snapshot):
|
||||
return null
|
||||
var instance := FacilityInstance.new().configure(
|
||||
StringName(snapshot.get(&"instance_id", "")),
|
||||
StringName(snapshot.get(&"definition_id", "")),
|
||||
{
|
||||
&"origin": snapshot.get(&"origin", []),
|
||||
&"rotation": snapshot.get(&"rotation", 0),
|
||||
&"occupied_cells": snapshot.get(&"occupied_cells", []),
|
||||
&"level": snapshot.get(&"level", 1),
|
||||
&"durability": snapshot.get(&"durability", DEFAULT_DURABILITY),
|
||||
&"local_inventory_id": snapshot.get(&"local_inventory_id", ""),
|
||||
&"capacity": snapshot.get(&"capacity", 1),
|
||||
&"queue_capacity": snapshot.get(&"queue_capacity", 0),
|
||||
&"base_service_minutes": snapshot.get(&"base_service_minutes", 1),
|
||||
&"base_quality": snapshot.get(&"base_quality", 0),
|
||||
&"base_price": snapshot.get(&"base_price", 0),
|
||||
&"staff_min": snapshot.get(&"staff_min", 0),
|
||||
&"staff_max": snapshot.get(&"staff_max", 0),
|
||||
&"policy_current": snapshot.get(&"policy_current", {}),
|
||||
&"policy_revision": snapshot.get(&"policy_revision", 0),
|
||||
&"created_at": snapshot.get(&"created_at", 0),
|
||||
&"access_available": snapshot.get(&"access_available", true),
|
||||
&"power_available": snapshot.get(&"power_available", true),
|
||||
&"operator_open": snapshot.get(&"operator_open", true),
|
||||
&"opening_intervals": snapshot.get(&"opening_intervals", []),
|
||||
&"active_staff_ids": snapshot.get(&"active_staff_ids", []),
|
||||
&"degraded_reasons": snapshot.get(&"degraded_reasons", []),
|
||||
}
|
||||
)
|
||||
var state_index := State.keys().find(String(snapshot.get(&"state", "OPEN")))
|
||||
if state_index < 0:
|
||||
return null
|
||||
instance.state = state_index as State
|
||||
instance.state_reasons = _string_name_array(snapshot.get(&"state_reasons", []))
|
||||
instance.assigned_staff_by_shift = snapshot.get(&"assigned_staff_by_shift", {}).duplicate(true)
|
||||
instance.queue_entries = _string_name_array(snapshot.get(&"queue_entries", []))
|
||||
instance.policy_pending = _normalize_policy(snapshot.get(&"policy_pending", {}))
|
||||
instance.policy_pending_revision = maxi(int(snapshot.get(&"policy_pending_revision", 0)), 0)
|
||||
instance.policy_pending_effective_at = int(snapshot.get(&"policy_pending_effective_at", -1))
|
||||
instance.statistics_today = snapshot.get(&"statistics_today", {}).duplicate(true)
|
||||
instance.lifetime_statistics = snapshot.get(&"lifetime_statistics", {}).duplicate(true)
|
||||
var seen_services: Dictionary = {}
|
||||
for saved_service: Variant in snapshot.get(&"active_services", []):
|
||||
var service := ServiceInstanceType.from_snapshot(saved_service)
|
||||
if service == null or service.is_terminal() or service.facility_id != instance.instance_id:
|
||||
return null
|
||||
if seen_services.has(service.service_id):
|
||||
return null
|
||||
seen_services[service.service_id] = true
|
||||
instance.active_services[service.service_id] = service
|
||||
if not instance.is_valid() or instance.active_services.size() > instance.capacity:
|
||||
return null
|
||||
return instance
|
||||
|
||||
|
||||
static func _snapshot_shapes_are_valid(snapshot: Dictionary) -> bool:
|
||||
var array_fields := [
|
||||
&"origin", &"occupied_cells", &"state_reasons", &"active_staff_ids",
|
||||
&"queue_entries", &"active_services", &"opening_intervals", &"degraded_reasons",
|
||||
]
|
||||
for field: StringName in array_fields:
|
||||
if not snapshot.get(field, null) is Array:
|
||||
return false
|
||||
var dictionary_fields := [
|
||||
&"assigned_staff_by_shift", &"policy_current", &"policy_pending",
|
||||
&"statistics_today", &"lifetime_statistics",
|
||||
]
|
||||
for field: StringName in dictionary_fields:
|
||||
if not snapshot.get(field, null) is Dictionary:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant:
|
||||
return source.get(key, source.get(String(key), fallback))
|
||||
|
||||
|
||||
static func _has_value(source: Dictionary, key: StringName) -> bool:
|
||||
return source.has(key) or source.has(String(key))
|
||||
|
||||
|
||||
static func _copy(value: Variant) -> Variant:
|
||||
return value.duplicate(true) if value is Dictionary or value is Array else value
|
||||
|
||||
|
||||
static func _dictionary_copy(source: Variant) -> Dictionary:
|
||||
return source.duplicate(true) if source is Dictionary else {}
|
||||
|
||||
|
||||
static func _normalize_policy(source: Variant) -> Dictionary:
|
||||
var normalized: Dictionary = {}
|
||||
if not source is Dictionary:
|
||||
return normalized
|
||||
for raw_key: Variant in source:
|
||||
normalized[StringName(raw_key)] = _copy(source[raw_key])
|
||||
return normalized
|
||||
|
||||
|
||||
static func _vector3i(source: Variant) -> Vector3i:
|
||||
if source is Vector3i:
|
||||
return source
|
||||
if source is Array and source.size() == 3:
|
||||
return Vector3i(int(source[0]), int(source[1]), int(source[2]))
|
||||
return Vector3i.ZERO
|
||||
|
||||
|
||||
static func _cell_array(source: Variant) -> Array[Vector3i]:
|
||||
var cells: Array[Vector3i] = []
|
||||
if not source is Array:
|
||||
return cells
|
||||
for raw_cell: Variant in source:
|
||||
if raw_cell is Vector3i:
|
||||
cells.append(raw_cell)
|
||||
elif raw_cell is Array and raw_cell.size() == 3:
|
||||
cells.append(_vector3i(raw_cell))
|
||||
return cells
|
||||
|
||||
|
||||
static func _parse_intervals(source: Variant) -> Array[Vector2i]:
|
||||
var intervals: Array[Vector2i] = []
|
||||
if not source is Array:
|
||||
return intervals
|
||||
for raw_interval: Variant in source:
|
||||
if raw_interval is Vector2i:
|
||||
intervals.append(raw_interval)
|
||||
elif raw_interval is Array and raw_interval.size() == 2:
|
||||
intervals.append(Vector2i(int(raw_interval[0]), int(raw_interval[1])))
|
||||
intervals.sort_custom(func(a: Vector2i, b: Vector2i) -> bool:
|
||||
return a.x < b.x or (a.x == b.x and a.y < b.y)
|
||||
)
|
||||
return intervals
|
||||
|
||||
|
||||
static func _intervals_are_valid(intervals: Array[Vector2i]) -> bool:
|
||||
if intervals.is_empty():
|
||||
return false
|
||||
var previous_end := -1
|
||||
for interval: Vector2i in intervals:
|
||||
if interval.x < 0 or interval.y > FULL_DAY_MINUTES or interval.x >= interval.y:
|
||||
return false
|
||||
if interval.x < previous_end:
|
||||
return false
|
||||
previous_end = interval.y
|
||||
return true
|
||||
|
||||
|
||||
static func _sorted_unique_names(source: Variant) -> Array[StringName]:
|
||||
var values := _string_name_array(source)
|
||||
var seen: Dictionary = {}
|
||||
var unique: Array[StringName] = []
|
||||
for value: StringName in values:
|
||||
if not value.is_empty() and not seen.has(value):
|
||||
seen[value] = true
|
||||
unique.append(value)
|
||||
unique.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
|
||||
return unique
|
||||
|
||||
|
||||
static func _string_name_array(source: Variant) -> Array[StringName]:
|
||||
var values: Array[StringName] = []
|
||||
if source is Array:
|
||||
for value: Variant in source:
|
||||
values.append(StringName(value))
|
||||
return values
|
||||
|
||||
|
||||
static func _sorted_keys(source: Dictionary) -> Array[StringName]:
|
||||
var values: Array[StringName] = []
|
||||
for value: Variant in source.keys():
|
||||
values.append(StringName(value))
|
||||
values.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b))
|
||||
return values
|
||||
|
||||
|
||||
static func _serialize_cells(source: Array[Vector3i]) -> Array[Array]:
|
||||
var cells: Array[Array] = []
|
||||
for cell: Vector3i in source:
|
||||
cells.append([cell.x, cell.y, cell.z])
|
||||
return cells
|
||||
|
||||
|
||||
static func _serialize_intervals(source: Array[Vector2i]) -> Array[Array]:
|
||||
var intervals: Array[Array] = []
|
||||
for interval: Vector2i in source:
|
||||
intervals.append([interval.x, interval.y])
|
||||
return intervals
|
||||
|
||||
|
||||
static func _names_to_strings(source: Array[StringName]) -> Array[String]:
|
||||
var values: Array[String] = []
|
||||
for value: StringName in source:
|
||||
values.append(String(value))
|
||||
return values
|
||||
|
||||
|
||||
static func _normalize_rotation(value: int) -> int:
|
||||
var normalized := posmod(value, 360)
|
||||
return (normalized / 90) * 90
|
||||
Reference in New Issue
Block a user