class_name ConstructionJob extends RefCounted enum State { PLANNED, BUILDING, COMPLETED, CANCELLED, BLOCKED, } const MAX_PROGRESS := 10_000 const NORMAL_WORK_RATE := 10_000 var job_id: StringName = &"" var definition_id: StringName = &"" var origin := Vector3i.ZERO var rotation := 0 var layer := 0 var reserved_cells: Array[Vector3i] = [] var progress := 0 var state: State = State.PLANNED var state_reason := "" var assigned_builder_ids: Array[StringName] = [] var construction_cost: Dictionary = {} var build_minutes := 60 var work_units_done := 0 var created_at := 0 var started_at := -1 var completed_at := -1 var cancelled_at := -1 var facility_instance_id: StringName = &"" func configure( p_job_id: StringName, p_definition_id: StringName, p_origin: Vector3i, p_rotation: int, p_layer: int, p_reserved_cells: Array[Vector3i], p_construction_cost: Dictionary, p_build_minutes: int, p_created_at: int ) -> ConstructionJob: job_id = p_job_id definition_id = p_definition_id origin = p_origin rotation = _normalize_rotation(p_rotation) layer = p_layer reserved_cells.assign(p_reserved_cells) construction_cost = _normalize_cost(p_construction_cost) build_minutes = maxi(p_build_minutes, 1) created_at = maxi(p_created_at, 0) return self func advance_one_minute(work_rate_basis_points: int = NORMAL_WORK_RATE, absolute_minute: int = -1) -> bool: if state == State.COMPLETED or state == State.CANCELLED: return false if progress >= MAX_PROGRESS: return true if work_rate_basis_points <= 0: mark_blocked("No effective construction work was supplied.") return false if started_at < 0: started_at = absolute_minute state = State.BUILDING state_reason = "" work_units_done += work_rate_basis_points progress = mini(MAX_PROGRESS, work_units_done / build_minutes) return progress >= MAX_PROGRESS func mark_blocked(reason: String) -> void: if state == State.COMPLETED or state == State.CANCELLED: return state = State.BLOCKED state_reason = reason func mark_completed(instance_id: StringName, absolute_minute: int = -1) -> void: progress = MAX_PROGRESS state = State.COMPLETED state_reason = "" facility_instance_id = instance_id completed_at = absolute_minute func mark_cancelled(absolute_minute: int = -1) -> void: state = State.CANCELLED state_reason = "" cancelled_at = absolute_minute func is_terminal() -> bool: return state == State.COMPLETED or state == State.CANCELLED func refundable_cost() -> Dictionary: var refund: Dictionary = {} var remaining_basis_points := maxi(MAX_PROGRESS - progress, 0) for resource_name: Variant in construction_cost: var amount := int(construction_cost[resource_name]) refund[StringName(resource_name)] = amount * remaining_basis_points / MAX_PROGRESS return refund func create_snapshot() -> Dictionary: return { &"job_id": String(job_id), &"definition_id": String(definition_id), &"origin": [origin.x, origin.y, origin.z], &"rotation": rotation, &"layer": layer, &"reserved_cells": _serialize_cells(reserved_cells), &"progress": progress, &"state": State.keys()[state], &"state_reason": state_reason, &"assigned_builder_ids": _string_names_to_strings(assigned_builder_ids), &"construction_cost": construction_cost.duplicate(true), &"build_minutes": build_minutes, &"work_units_done": work_units_done, &"created_at": created_at, &"started_at": started_at, &"completed_at": completed_at, &"cancelled_at": cancelled_at, &"facility_instance_id": String(facility_instance_id), } static func from_snapshot(snapshot: Dictionary) -> ConstructionJob: if not _is_valid_snapshot(snapshot): return null var job := ConstructionJob.new() job.job_id = StringName(snapshot.get(&"job_id", "")) job.definition_id = StringName(snapshot.get(&"definition_id", "")) job.origin = _parse_cell(snapshot.get(&"origin", [])) job.rotation = _normalize_rotation(int(snapshot.get(&"rotation", 0))) job.layer = int(snapshot.get(&"layer", job.origin.z)) job.reserved_cells = _parse_cells(snapshot.get(&"reserved_cells", [])) job.progress = clampi(int(snapshot.get(&"progress", 0)), 0, MAX_PROGRESS) var state_index := State.keys().find(String(snapshot.get(&"state", "PLANNED"))) if state_index < 0: return null job.state = state_index as State job.state_reason = String(snapshot.get(&"state_reason", "")) var saved_builders: Variant = snapshot.get(&"assigned_builder_ids", []) if saved_builders is Array: for builder_id: Variant in saved_builders: job.assigned_builder_ids.append(StringName(builder_id)) job.construction_cost = _normalize_cost(snapshot.get(&"construction_cost", {})) job.build_minutes = maxi(int(snapshot.get(&"build_minutes", 60)), 1) job.work_units_done = maxi(int(snapshot.get(&"work_units_done", 0)), 0) job.created_at = maxi(int(snapshot.get(&"created_at", 0)), 0) job.started_at = int(snapshot.get(&"started_at", -1)) job.completed_at = int(snapshot.get(&"completed_at", -1)) job.cancelled_at = int(snapshot.get(&"cancelled_at", -1)) job.facility_instance_id = StringName(snapshot.get(&"facility_instance_id", "")) return job static func _is_valid_snapshot(snapshot: Dictionary) -> bool: if String(snapshot.get(&"job_id", "")).is_empty(): return false if String(snapshot.get(&"definition_id", "")).is_empty(): return false if not snapshot.get(&"origin", null) is Array: return false if not snapshot.get(&"reserved_cells", null) is Array: return false if not snapshot.get(&"construction_cost", null) is Dictionary: return false return true static func _normalize_cost(source: Variant) -> Dictionary: var normalized: Dictionary = {} if not source is Dictionary: return normalized for resource_name: Variant in source: normalized[StringName(resource_name)] = maxi(int(source[resource_name]), 0) return normalized static func _normalize_rotation(value: int) -> int: var normalized := value % 360 if normalized < 0: normalized += 360 return (normalized / 90) * 90 static func _parse_cell(source: Variant) -> Vector3i: if not source is Array or source.size() != 3: return Vector3i.ZERO return Vector3i(int(source[0]), int(source[1]), int(source[2])) static func _parse_cells(source: Variant) -> Array[Vector3i]: var cells: Array[Vector3i] = [] if not source is Array: return cells for value: Variant in source: if value is Array and value.size() == 3: cells.append(_parse_cell(value)) return cells static func _serialize_cells(source: Array[Vector3i]) -> Array[Array]: var serialized: Array[Array] = [] for cell: Vector3i in source: serialized.append([cell.x, cell.y, cell.z]) return serialized static func _string_names_to_strings(source: Array[StringName]) -> Array[String]: var serialized: Array[String] = [] for value: StringName in source: serialized.append(String(value)) return serialized