Files
Takeshi b84bade045 Save The Tree implementation and acceptance work in progress
Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
2026-10-11 15:58:00 +09:00

813 lines
25 KiB
GDScript

class_name WorldService
extends RefCounted
## Deterministic logical-world storage for S1.
##
## Vector3i uses (x, y, layer), where layer 0 is FRONT and layer 1 is BACK.
## All collection-returning APIs use a stable y/x/layer or entity-id order.
const SNAPSHOT_SCHEMA_VERSION := 1
const LAYER_FRONT := 0
const LAYER_BACK := 1
const ZONE_INVALID := &"INVALID"
const ZONE_ABOVE_GROUND := &"ABOVE_GROUND"
const ZONE_BOUNDARY := &"BOUNDARY"
const ZONE_UNDERGROUND := &"UNDERGROUND"
const ERROR_EMPTY_CELLS := &"EMPTY_CELLS"
const ERROR_INVALID_CELL_TYPE := &"INVALID_CELL_TYPE"
const ERROR_OUT_OF_BOUNDS := &"OUT_OF_BOUNDS"
const ERROR_DUPLICATE_CELL := &"DUPLICATE_CELL"
const ERROR_LAYER_MISMATCH := &"LAYER_MISMATCH"
const ERROR_OCCUPIED := &"OCCUPIED"
const ERROR_ENTITY_EXISTS := &"ENTITY_EXISTS"
const ERROR_INVALID_ENTITY_ID := &"INVALID_ENTITY_ID"
const ERROR_INVALID_KIND := &"INVALID_KIND"
const ERROR_INVALID_WEIGHT := &"INVALID_WEIGHT"
const ERROR_UNSUPPORTED := &"UNSUPPORTED"
var revision := 0
var dirty_revision: int:
get:
return revision
var occupancy_revision := 0
var walkability_revision := 0
var excavation_revision := 0
var support_revision := 0
var _bounds := {
&"min_x": 0,
&"max_x": -1,
&"min_y": 0,
&"max_y": -1,
}
var _occupants: Dictionary = {}
var _entity_cells: Dictionary = {}
var _entity_kinds: Dictionary = {}
var _walkable: Dictionary = {}
var _excavated: Dictionary = {}
var _support_capacity: Dictionary = {}
var _support_anchors: Dictionary = {}
var _dirty_cells: Dictionary = {}
var _snapshot_geometry: Dictionary = {}
var _snapshot_geometry_revision := -1
func reset(bounds: Variant) -> bool:
var normalized := _normalize_bounds(bounds)
if normalized.is_empty():
return false
_snapshot_geometry.clear()
_snapshot_geometry_revision = -1
_bounds = normalized
_occupants.clear()
_entity_cells.clear()
_entity_kinds.clear()
_walkable.clear()
_excavated.clear()
_support_capacity.clear()
_support_anchors.clear()
_dirty_cells.clear()
revision = 0
occupancy_revision = 0
walkability_revision = 0
excavation_revision = 0
support_revision = 0
return true
func get_bounds() -> Dictionary:
return _bounds.duplicate(true)
func expand_bounds(bounds: Variant) -> bool:
var normalized := _normalize_bounds(bounds)
if normalized.is_empty():
return false
if (
int(normalized[&"min_x"]) > int(_bounds[&"min_x"])
or int(normalized[&"max_x"]) < int(_bounds[&"max_x"])
or int(normalized[&"min_y"]) > int(_bounds[&"min_y"])
or int(normalized[&"max_y"]) < int(_bounds[&"max_y"])
):
return false
if normalized == _bounds:
return true
_bounds = normalized
revision += 1
return true
func is_in_bounds(cell: Variant) -> bool:
return cell is Vector3i and _is_in_bounds_with(cell, _bounds)
func get_zone(cell_or_y: Variant) -> StringName:
var y: int
if cell_or_y is Vector3i or cell_or_y is Vector2i:
y = cell_or_y.y
elif cell_or_y is int:
y = int(cell_or_y)
else:
return ZONE_INVALID
if y > 0:
return ZONE_ABOVE_GROUND
if y < 0:
return ZONE_UNDERGROUND
return ZONE_BOUNDARY
func cells_for_footprint(origin: Vector3i, footprint: Variant) -> Array[Vector3i]:
var result: Array[Vector3i] = []
if origin.z not in [LAYER_FRONT, LAYER_BACK]:
return result
if footprint is Vector2i:
if footprint.x <= 0 or footprint.y <= 0:
return result
for y_offset in range(footprint.y):
for x_offset in range(footprint.x):
result.append(origin + Vector3i(x_offset, y_offset, 0))
return result
if footprint is Array:
for offset: Variant in footprint:
if not offset is Vector2i:
return []
result.append(origin + Vector3i(offset.x, offset.y, 0))
result.sort_custom(_cell_less)
return result
func validate_occupancy(cells: Array, entity_id: StringName = &"") -> Dictionary:
var report := _validate_cells(cells)
var occupied_cells: Array[Vector3i] = []
for cell: Vector3i in report[&"valid_cells"]:
if _occupants.has(cell):
occupied_cells.append(cell)
if not occupied_cells.is_empty():
_add_error(report[&"errors"], ERROR_OCCUPIED)
if not entity_id.is_empty() and _entity_cells.has(entity_id):
_add_error(report[&"errors"], ERROR_ENTITY_EXISTS)
report[&"occupied_cells"] = occupied_cells
report[&"existing_entity"] = not entity_id.is_empty() and _entity_cells.has(entity_id)
report[&"ok"] = report[&"errors"].is_empty()
return report
func occupy(entity_id: StringName, cells: Array, kind: StringName = &"facility") -> bool:
if entity_id.is_empty() or kind.is_empty():
return false
var report := validate_occupancy(cells, entity_id)
if not report[&"ok"]:
return false
var canonical_cells: Array[Vector3i] = report[&"valid_cells"].duplicate()
canonical_cells.sort_custom(_cell_less)
for cell: Vector3i in canonical_cells:
_occupants[cell] = {
&"entity_id": entity_id,
&"kind": kind,
}
_entity_cells[entity_id] = canonical_cells
_entity_kinds[entity_id] = kind
var structural := _kind_support_capacity(kind) > 0 or _kind_weight(kind) > 0 or _kind_is_anchor(kind)
_mark_dirty(canonical_cells, true, false, false, structural)
return true
func release(entity_id: StringName) -> bool:
if entity_id.is_empty() or not _entity_cells.has(entity_id):
return false
var cells: Array[Vector3i] = _copy_cell_array(_entity_cells[entity_id])
var kind: StringName = _entity_kinds.get(entity_id, &"")
for cell: Vector3i in cells:
var occupant: Dictionary = _occupants.get(cell, {})
if occupant.get(&"entity_id", &"") == entity_id:
_occupants.erase(cell)
_entity_cells.erase(entity_id)
_entity_kinds.erase(entity_id)
var structural := _kind_support_capacity(kind) > 0 or _kind_weight(kind) > 0 or _kind_is_anchor(kind)
_mark_dirty(cells, true, false, false, structural)
return true
func get_occupant(cell: Vector3i) -> StringName:
var occupant: Dictionary = _occupants.get(cell, {})
return occupant.get(&"entity_id", &"")
func get_occupant_kind(cell: Vector3i) -> StringName:
var occupant: Dictionary = _occupants.get(cell, {})
return occupant.get(&"kind", &"")
func get_entity_cells(entity_id: StringName) -> Array[Vector3i]:
if not _entity_cells.has(entity_id):
return []
return _copy_cell_array(_entity_cells[entity_id])
func get_entity_kind(entity_id: StringName) -> StringName:
return _entity_kinds.get(entity_id, &"")
func set_walkable(cell: Vector3i, value: bool) -> bool:
if not is_in_bounds(cell):
return false
var previous := bool(_walkable.get(cell, false))
if previous == value:
return true
if value:
_walkable[cell] = true
else:
_walkable.erase(cell)
_mark_dirty([cell], false, true, false, false)
return true
func is_walkable(cell: Vector3i) -> bool:
return is_in_bounds(cell) and bool(_walkable.get(cell, false))
func get_walkable_cells(sort_result := true) -> Array[Vector3i]:
return _sorted_dictionary_cells(_walkable, sort_result)
func set_excavated(cell: Vector3i, value: bool) -> bool:
if not is_in_bounds(cell):
return false
var spatial_key := Vector2i(cell.x, cell.y)
var previous := is_excavated(cell)
if previous == value:
return true
var default_value := cell.y >= 0
if value == default_value:
_excavated.erase(spatial_key)
else:
_excavated[spatial_key] = value
var affected: Array[Vector3i] = [
Vector3i(cell.x, cell.y, LAYER_FRONT),
Vector3i(cell.x, cell.y, LAYER_BACK),
]
_mark_dirty(affected, false, false, true, false)
return true
func is_excavated(cell: Vector3i) -> bool:
if not is_in_bounds(cell):
return false
var spatial_key := Vector2i(cell.x, cell.y)
return bool(_excavated.get(spatial_key, cell.y >= 0))
## Sets explicit support on a layer cell. Anchors have unlimited capacity.
## A capacity of zero with anchor=false removes the explicit support record.
func set_support(cell: Vector3i, capacity: int, anchor := false) -> bool:
if not is_in_bounds(cell) or capacity < 0:
return false
var previous_capacity := int(_support_capacity.get(cell, 0))
var previous_anchor := bool(_support_anchors.get(cell, false))
if previous_capacity == capacity and previous_anchor == anchor:
return true
if capacity == 0:
_support_capacity.erase(cell)
else:
_support_capacity[cell] = capacity
if anchor:
_support_anchors[cell] = true
else:
_support_anchors.erase(cell)
_mark_dirty([cell], false, false, false, true)
return true
func get_support_capacity(cell: Vector3i) -> int:
if not is_in_bounds(cell):
return 0
var capacity := int(_support_capacity.get(cell, 0))
if _occupants.has(cell):
capacity += _kind_support_capacity(get_occupant_kind(cell))
return capacity
func is_support_anchor(cell: Vector3i) -> bool:
return (
is_in_bounds(cell)
and (
bool(_support_anchors.get(cell, false))
or (_occupants.has(cell) and _kind_is_anchor(get_occupant_kind(cell)))
)
)
## Validates connected structural components touched by the proposed cells.
## Existing structural occupant kinds contribute their configured capacity/load.
func validate_support(cells: Array, weight_per_cell := 1, entity_weights: Dictionary = {}) -> Dictionary:
var base_report := _validate_cells(cells)
var errors: Array[StringName] = base_report[&"errors"].duplicate()
if weight_per_cell <= 0:
_add_error(errors, ERROR_INVALID_WEIGHT)
# Optional definition weights are read-only and apply to existing occupants.
for id: Variant in entity_weights:
if typeof(id) not in [TYPE_STRING, TYPE_STRING_NAME] or not _entity_cells.has(id) or typeof(entity_weights[id]) != TYPE_INT or int(entity_weights[id]) <= 0:
_add_error(errors, ERROR_INVALID_WEIGHT)
var proposed: Array[Vector3i] = base_report[&"valid_cells"].duplicate()
proposed.sort_custom(_cell_less)
var result := {
&"ok": false,
&"errors": errors,
&"unsupported_cells": [] as Array[Vector3i],
&"required_weight": 0,
&"available_capacity": 0,
&"components": [] as Array[Dictionary],
}
if not errors.is_empty():
return result
var structural_set: Dictionary = {}
for proposed_cell: Vector3i in proposed:
structural_set[proposed_cell] = true
for cell: Variant in _sorted_dictionary_cells(_support_capacity):
structural_set[cell] = true
for cell: Variant in _sorted_dictionary_cells(_support_anchors):
structural_set[cell] = true
for entity_id: StringName in _sorted_entity_ids():
var kind: StringName = _entity_kinds[entity_id]
if _kind_support_capacity(kind) <= 0 and int(entity_weights.get(entity_id, _kind_weight(kind))) <= 0 and not _kind_is_anchor(kind):
continue
for occupied_cell: Vector3i in _entity_cells[entity_id]:
structural_set[occupied_cell] = true
var proposed_set: Dictionary = {}
for proposed_cell: Vector3i in proposed:
proposed_set[proposed_cell] = true
var visited: Dictionary = {}
var unsupported: Array[Vector3i] = []
var total_required := 0
var total_capacity := 0
var all_components_anchored := true
for proposed_cell: Vector3i in proposed:
if visited.has(proposed_cell):
continue
var component := _collect_component(proposed_cell, structural_set, visited)
var component_proposed: Array[Vector3i] = []
var required := 0
var capacity := 0
var anchored := false
for component_cell: Vector3i in component:
if proposed_set.has(component_cell):
component_proposed.append(component_cell)
required += weight_per_cell
if _occupants.has(component_cell) and not proposed_set.has(component_cell):
required += int(entity_weights.get(get_occupant(component_cell), _kind_weight(get_occupant_kind(component_cell))))
capacity += get_support_capacity(component_cell)
anchored = anchored or is_support_anchor(component_cell)
component_proposed.sort_custom(_cell_less)
var supported := anchored or capacity >= required
if not supported:
unsupported.append_array(component_proposed)
result[&"components"].append({
&"cells": component_proposed,
&"required_weight": required,
&"available_capacity": -1 if anchored else capacity,
&"anchored": anchored,
&"supported": supported,
})
total_required += required
total_capacity += capacity
all_components_anchored = all_components_anchored and anchored
unsupported.sort_custom(_cell_less)
if not unsupported.is_empty():
_add_error(result[&"errors"], ERROR_UNSUPPORTED)
result[&"unsupported_cells"] = unsupported
result[&"required_weight"] = total_required
result[&"available_capacity"] = -1 if all_components_anchored else total_capacity
result[&"ok"] = result[&"errors"].is_empty()
return result
func get_dirty_revision() -> int:
return revision
func get_dirty_cells() -> Array[Vector3i]:
return _sorted_dictionary_cells(_dirty_cells)
func consume_dirty_cells() -> Array[Vector3i]:
var result := get_dirty_cells()
_dirty_cells.clear()
return result
func create_snapshot() -> Dictionary:
# Geometry only changes through world mutations. Dirty-cell consumption and
# public revision counters are sampled separately on every read.
if _snapshot_geometry_revision != revision or _snapshot_geometry.is_empty():
_snapshot_geometry = _create_geometry_snapshot()
_snapshot_geometry_revision = revision
var snapshot := _snapshot_geometry.duplicate(true)
snapshot[&"dirty_cells"] = get_dirty_cells()
snapshot[&"revisions"] = {
&"world": revision, &"occupancy": occupancy_revision,
&"walkability": walkability_revision, &"excavation": excavation_revision,
&"support": support_revision,
}
return snapshot
func _create_geometry_snapshot() -> Dictionary:
var occupancies: Array[Dictionary] = []
for entity_id: StringName in _sorted_entity_ids():
occupancies.append({
&"entity_id": entity_id,
&"kind": _entity_kinds[entity_id],
&"cells": get_entity_cells(entity_id),
})
var excavation_overrides: Array[Dictionary] = []
var spatial_keys: Array[Vector2i] = []
for key: Variant in _excavated:
spatial_keys.append(key)
spatial_keys.sort_custom(_spatial_less)
for key: Vector2i in spatial_keys:
excavation_overrides.append({&"position": key, &"value": bool(_excavated[key])})
var support_entries: Array[Dictionary] = []
var support_cell_set: Dictionary = {}
for cell: Variant in _support_capacity:
support_cell_set[cell] = true
for cell: Variant in _support_anchors:
support_cell_set[cell] = true
for cell: Vector3i in _sorted_dictionary_cells(support_cell_set):
support_entries.append({
&"cell": cell,
&"capacity": int(_support_capacity.get(cell, 0)),
&"anchor": bool(_support_anchors.get(cell, false)),
})
return {
&"schema_version": SNAPSHOT_SCHEMA_VERSION,
&"bounds": get_bounds(),
&"occupancies": occupancies,
&"walkable_cells": _sorted_dictionary_cells(_walkable),
&"excavation_overrides": excavation_overrides,
&"support": support_entries,
}
func restore_snapshot(snapshot: Dictionary) -> bool:
if int(snapshot.get(&"schema_version", -1)) != SNAPSHOT_SCHEMA_VERSION:
return false
var new_bounds := _normalize_bounds(snapshot.get(&"bounds", {}))
if new_bounds.is_empty():
return false
var new_occupants: Dictionary = {}
var new_entity_cells: Dictionary = {}
var new_entity_kinds: Dictionary = {}
var occupancies: Variant = snapshot.get(&"occupancies", null)
if not occupancies is Array:
return false
for entry: Variant in occupancies:
if not entry is Dictionary:
return false
var entity_id := StringName(entry.get(&"entity_id", &""))
var kind := StringName(entry.get(&"kind", &""))
var cells: Variant = entry.get(&"cells", null)
if entity_id.is_empty() or kind.is_empty() or not cells is Array or new_entity_cells.has(entity_id):
return false
var validation := _validate_cells_against_bounds(cells, new_bounds)
if not validation[&"errors"].is_empty():
return false
var canonical_cells: Array[Vector3i] = validation[&"valid_cells"].duplicate()
for cell: Vector3i in canonical_cells:
if new_occupants.has(cell):
return false
new_occupants[cell] = {&"entity_id": entity_id, &"kind": kind}
new_entity_cells[entity_id] = canonical_cells
new_entity_kinds[entity_id] = kind
var new_walkable: Dictionary = {}
var walkable_cells: Variant = snapshot.get(&"walkable_cells", null)
if not walkable_cells is Array:
return false
for cell: Variant in walkable_cells:
if not cell is Vector3i or not _is_in_bounds_with(cell, new_bounds) or new_walkable.has(cell):
return false
new_walkable[cell] = true
var new_excavated: Dictionary = {}
var excavation_entries: Variant = snapshot.get(&"excavation_overrides", null)
if not excavation_entries is Array:
return false
for entry: Variant in excavation_entries:
if not entry is Dictionary:
return false
var position: Variant = entry.get(&"position", null)
var value: Variant = entry.get(&"value", null)
if not position is Vector2i or not value is bool or new_excavated.has(position):
return false
if not _is_spatial_in_bounds(position, new_bounds):
return false
if bool(value) == (position.y >= 0):
return false
new_excavated[position] = value
var new_support_capacity: Dictionary = {}
var new_support_anchors: Dictionary = {}
var support_entries: Variant = snapshot.get(&"support", null)
if not support_entries is Array:
return false
var seen_support: Dictionary = {}
for entry: Variant in support_entries:
if not entry is Dictionary:
return false
var cell: Variant = entry.get(&"cell", null)
var capacity: Variant = entry.get(&"capacity", null)
var anchor: Variant = entry.get(&"anchor", null)
if (
not cell is Vector3i
or not _is_in_bounds_with(cell, new_bounds)
or not capacity is int
or int(capacity) < 0
or not anchor is bool
or (int(capacity) == 0 and not bool(anchor))
or seen_support.has(cell)
):
return false
seen_support[cell] = true
if int(capacity) > 0:
new_support_capacity[cell] = int(capacity)
if bool(anchor):
new_support_anchors[cell] = true
var new_dirty: Dictionary = {}
var dirty_cells: Variant = snapshot.get(&"dirty_cells", null)
if not dirty_cells is Array:
return false
for cell: Variant in dirty_cells:
if not cell is Vector3i or not _is_in_bounds_with(cell, new_bounds) or new_dirty.has(cell):
return false
new_dirty[cell] = true
var revisions: Variant = snapshot.get(&"revisions", null)
if not revisions is Dictionary:
return false
var revision_names: Array[StringName] = [
&"world", &"occupancy", &"walkability", &"excavation", &"support",
]
for revision_name: StringName in revision_names:
if not revisions.get(revision_name, null) is int or int(revisions[revision_name]) < 0:
return false
if (
int(revisions[&"occupancy"]) > int(revisions[&"world"])
or int(revisions[&"walkability"]) > int(revisions[&"world"])
or int(revisions[&"excavation"]) > int(revisions[&"world"])
or int(revisions[&"support"]) > int(revisions[&"world"])
):
return false
_snapshot_geometry.clear()
_snapshot_geometry_revision = -1
_bounds = new_bounds
_occupants = new_occupants
_entity_cells = new_entity_cells
_entity_kinds = new_entity_kinds
_walkable = new_walkable
_excavated = new_excavated
_support_capacity = new_support_capacity
_support_anchors = new_support_anchors
_dirty_cells = new_dirty
revision = int(revisions[&"world"])
occupancy_revision = int(revisions[&"occupancy"])
walkability_revision = int(revisions[&"walkability"])
excavation_revision = int(revisions[&"excavation"])
support_revision = int(revisions[&"support"])
return true
func _validate_cells(cells: Array) -> Dictionary:
return _validate_cells_against_bounds(cells, _bounds)
func _validate_cells_against_bounds(cells: Array, bounds: Dictionary) -> Dictionary:
var errors: Array[StringName] = []
var valid_cells: Array[Vector3i] = []
var invalid_cells: Array[Vector3i] = []
var duplicate_cells: Array[Vector3i] = []
var seen: Dictionary = {}
var layers: Dictionary = {}
if cells.is_empty():
_add_error(errors, ERROR_EMPTY_CELLS)
for value: Variant in cells:
if not value is Vector3i:
_add_error(errors, ERROR_INVALID_CELL_TYPE)
continue
var cell: Vector3i = value
layers[cell.z] = true
if seen.has(cell):
duplicate_cells.append(cell)
_add_error(errors, ERROR_DUPLICATE_CELL)
continue
seen[cell] = true
valid_cells.append(cell)
if not _is_in_bounds_with(cell, bounds):
invalid_cells.append(cell)
_add_error(errors, ERROR_OUT_OF_BOUNDS)
if layers.size() > 1:
_add_error(errors, ERROR_LAYER_MISMATCH)
valid_cells.sort_custom(_cell_less)
invalid_cells.sort_custom(_cell_less)
duplicate_cells.sort_custom(_cell_less)
return {
&"ok": errors.is_empty(),
&"errors": errors,
&"valid_cells": valid_cells,
&"invalid_cells": invalid_cells,
&"duplicate_cells": duplicate_cells,
&"layer_mismatch": layers.size() > 1,
}
func _collect_component(start: Vector3i, structural_set: Dictionary, visited: Dictionary) -> Array[Vector3i]:
var queue: Array[Vector3i] = [start]
var component: Array[Vector3i] = []
visited[start] = true
var index := 0
while index < queue.size():
var current := queue[index]
index += 1
component.append(current)
var neighbors: Array[Vector3i] = [
current + Vector3i(-1, 0, 0),
current + Vector3i(0, -1, 0),
current + Vector3i(0, 1, 0),
current + Vector3i(1, 0, 0),
]
for neighbor: Vector3i in neighbors:
if structural_set.has(neighbor) and not visited.has(neighbor):
visited[neighbor] = true
queue.append(neighbor)
component.sort_custom(_cell_less)
return component
func _mark_dirty(
cells: Array,
occupancy_changed: bool,
walkability_changed: bool,
excavation_changed: bool,
support_changed: bool
) -> void:
revision += 1
if occupancy_changed:
occupancy_revision = revision
if walkability_changed:
walkability_revision = revision
if excavation_changed:
excavation_revision = revision
if support_changed:
support_revision = revision
for cell: Variant in cells:
if cell is Vector3i and is_in_bounds(cell):
_dirty_cells[cell] = true
func _normalize_bounds(value: Variant) -> Dictionary:
var min_x: int
var max_x: int
var min_y: int
var max_y: int
if value is Rect2i:
if value.size.x <= 0 or value.size.y <= 0:
return {}
min_x = value.position.x
max_x = value.position.x + value.size.x - 1
min_y = value.position.y
max_y = value.position.y + value.size.y - 1
elif value is Dictionary:
if value.has(&"min") and value.has(&"max"):
var minimum: Variant = value[&"min"]
var maximum: Variant = value[&"max"]
if not minimum is Vector2i or not maximum is Vector2i:
return {}
min_x = minimum.x
max_x = maximum.x
min_y = minimum.y
max_y = maximum.y
else:
var names: Array[StringName] = [&"min_x", &"max_x", &"min_y", &"max_y"]
for name: StringName in names:
if not value.get(name, null) is int:
return {}
min_x = int(value[&"min_x"])
max_x = int(value[&"max_x"])
min_y = int(value[&"min_y"])
max_y = int(value[&"max_y"])
elif value is Vector4i:
min_x = value.x
max_x = value.y
min_y = value.z
max_y = value.w
else:
return {}
if min_x > max_x or min_y > max_y:
return {}
return {
&"min_x": min_x,
&"max_x": max_x,
&"min_y": min_y,
&"max_y": max_y,
}
func _sorted_entity_ids() -> Array[StringName]:
var result: Array[StringName] = []
for entity_id: Variant in _entity_cells:
result.append(StringName(entity_id))
result.sort_custom(func(left: StringName, right: StringName) -> bool:
return String(left) < String(right)
)
return result
func _sorted_dictionary_cells(source: Dictionary, sort_result := true) -> Array[Vector3i]:
var result: Array[Vector3i] = []
for cell: Variant in source:
if cell is Vector3i:
result.append(cell)
if sort_result:
result.sort_custom(_cell_less)
return result
func _copy_cell_array(source: Array) -> Array[Vector3i]:
var result: Array[Vector3i] = []
for cell: Variant in source:
result.append(cell)
result.sort_custom(_cell_less)
return result
func _kind_support_capacity(kind: StringName) -> int:
match String(kind).to_lower():
"artificial_floor", "floor":
return 2
"reinforced_floor":
return 5
"support_column", "column":
return 10
return 0
func _kind_weight(kind: StringName) -> int:
match String(kind).to_lower():
"facility", "facility_small", "small":
return 1
"facility_medium", "medium":
return 2
"facility_large", "facility_heavy", "large", "heavy":
return 3
return 0
func _kind_is_anchor(kind: StringName) -> bool:
return String(kind).to_lower() in [
"anchor", "structural_anchor", "natural_branch", "world_tree", "world_tree_anchor",
]
static func _is_in_bounds_with(cell: Vector3i, bounds: Dictionary) -> bool:
return (
cell.z in [LAYER_FRONT, LAYER_BACK]
and cell.x >= int(bounds.get(&"min_x", 0))
and cell.x <= int(bounds.get(&"max_x", -1))
and cell.y >= int(bounds.get(&"min_y", 0))
and cell.y <= int(bounds.get(&"max_y", -1))
)
static func _is_spatial_in_bounds(position: Vector2i, bounds: Dictionary) -> bool:
return (
position.x >= int(bounds.get(&"min_x", 0))
and position.x <= int(bounds.get(&"max_x", -1))
and position.y >= int(bounds.get(&"min_y", 0))
and position.y <= int(bounds.get(&"max_y", -1))
)
static func _cell_less(left: Vector3i, right: Vector3i) -> bool:
if left.y != right.y:
return left.y < right.y
if left.x != right.x:
return left.x < right.x
return left.z < right.z
static func _spatial_less(left: Vector2i, right: Vector2i) -> bool:
if left.y != right.y:
return left.y < right.y
return left.x < right.x
static func _add_error(errors: Array[StringName], error_code: StringName) -> void:
if not errors.has(error_code):
errors.append(error_code)