class_name StorageNode extends Node3D static var _all: Array = [] @export var storage_id: StringName = SimulationIds.STORAGE_VILLAGE_PANTRY @export var display_name := "Village Pantry" @export var accepted_items: Array[StringName] = [] @export var debug_label_enabled := true @onready var interaction_point: Marker3D = $InteractionPoint const PRESENTATION_UPDATE_INTERVAL_SECONDS := 0.25 var state: StorageStateRecord var _last_label_text := "" var _presentation_elapsed := 0.0 var _active_world_adapter: ActiveWorldAdapter func _ready() -> void: if accepted_items.is_empty(): accepted_items = [SimulationIds.RESOURCE_FOOD] if storage_id.is_empty(): push_error("StorageNode at %s has empty storage_id" % get_path()) _update_presentation() return _active_world_adapter = ActiveWorldAdapter.find_for_node(self) var existing: StorageNode = get_by_id_in_context(storage_id, _active_world_adapter) if existing != null: push_error( ( "Duplicate StorageNode storage_id '%s' at %s; first registered at %s" % [storage_id, get_path(), existing.get_path()] ) ) _update_presentation() return _all.append(self) add_to_group("storage_nodes") set_notify_transform(true) _try_register_with_simulation() _notify_world_adapter() _update_presentation() set_process(debug_label_enabled) func _process(delta: float) -> void: _presentation_elapsed += delta if _presentation_elapsed < PRESENTATION_UPDATE_INTERVAL_SECONDS: return _presentation_elapsed = fmod(_presentation_elapsed, PRESENTATION_UPDATE_INTERVAL_SECONDS) _update_presentation() func _exit_tree() -> void: _unregister_from_world_adapter() _all.erase(self) state = null func bind_state(storage_state: StorageStateRecord) -> bool: if storage_state == null or storage_state.get_storage_id() != storage_id: return false state = storage_state _update_presentation() _notify_world_adapter() return true func get_interaction_position() -> Vector3: return interaction_point.global_position if interaction_point != null else global_position func accepts_item(item_id: StringName) -> bool: return accepted_items.is_empty() or item_id in accepted_items func _try_register_with_simulation() -> void: var managers := get_tree().get_nodes_in_group("simulation_manager") if managers.size() != 1: return var manager := managers[0] if manager.has_method("register_storage_node"): manager.register_storage_node(self) func _notification(what: int) -> void: if what == NOTIFICATION_TRANSFORM_CHANGED and is_node_ready(): if is_instance_valid(_active_world_adapter): _active_world_adapter.refresh_world_target_position(self) func _notify_world_adapter() -> void: if not is_inside_tree(): return if ( not is_instance_valid(_active_world_adapter) or not _active_world_adapter.owns_target_node(self) ): _active_world_adapter = ActiveWorldAdapter.find_for_node(self) if _active_world_adapter != null: _active_world_adapter.register_world_target(self) func _bind_active_world_adapter(adapter: ActiveWorldAdapter) -> void: if adapter != null and adapter.owns_target_node(self): _active_world_adapter = adapter func _unregister_from_world_adapter() -> void: if not is_instance_valid(_active_world_adapter): _active_world_adapter = ActiveWorldAdapter.find_for_node(self) if is_instance_valid(_active_world_adapter): _active_world_adapter.unregister_world_target(self) _active_world_adapter = null func _update_presentation() -> void: if not has_node("DebugLabel"): return var label := $DebugLabel as Label3D label.visible = debug_label_enabled if not debug_label_enabled: return var text := "%s\n%s" % [display_name, storage_id] if state == null: text += "\nUNBOUND" else: for item_id in accepted_items: text += "\n%s: %.1f" % [item_id, state.get_amount(item_id)] if text == _last_label_text: return _last_label_text = text label.text = text func set_debug_label_enabled(is_enabled: bool) -> void: debug_label_enabled = is_enabled _presentation_elapsed = 0.0 set_process(is_enabled) _update_presentation() static func get_by_id(search_id: StringName): for node in _all: if node.storage_id == search_id: return node return null static func get_by_id_in_context(search_id: StringName, adapter: ActiveWorldAdapter) -> StorageNode: for node in _all: if node.storage_id == search_id and ActiveWorldAdapter.find_for_node(node) == adapter: return node as StorageNode return null static func get_all() -> Array: return _all.duplicate()