class_name ResourceNode extends Node3D signal amount_changed(node_id: StringName, amount_remaining: float) signal depleted(node_id: StringName) signal reservation_changed(node_id: StringName, agent_id: int) static var _all: Array[ResourceNode] = [] @export var node_id: StringName @export var action_id: StringName = SimulationIds.ACTION_GATHER_FOOD @export var resource_id: StringName = SimulationIds.RESOURCE_FOOD @export var initial_amount: float = 10.0 @export var yield_per_action: float = 2.0 @export var initial_enabled: bool = true @export var can_npcs_use: bool = true @export var can_player_use: bool = true @export var hide_visual_when_depleted: bool = false @onready var interaction_point: Marker3D = $InteractionPoint var state: ResourceStateRecord func _ready() -> void: if node_id.is_empty(): push_error("ResourceNode at %s has empty node_id" % get_path()) initial_enabled = false _update_presentation() return var existing := get_by_id(node_id) if existing != null: push_error( "Duplicate ResourceNode node_id '%s' at %s; first registered at %s" % [node_id, get_path(), existing.get_path()] ) initial_enabled = false _update_presentation() return _all.append(self) add_to_group("resource_nodes") _try_register_with_simulation() _update_presentation() func _exit_tree() -> void: _all.erase(self) if state != null and state.changed.is_connected(_on_state_changed): state.changed.disconnect(_on_state_changed) if state != null and state.depleted.is_connected(_on_state_depleted): state.depleted.disconnect(_on_state_depleted) state = null func bind_state(resource_state: ResourceStateRecord) -> bool: if resource_state == null or resource_state.get_node_id() != node_id: return false if state != null and state.changed.is_connected(_on_state_changed): state.changed.disconnect(_on_state_changed) if state != null and state.depleted.is_connected(_on_state_depleted): state.depleted.disconnect(_on_state_depleted) state = resource_state if not state.changed.is_connected(_on_state_changed): state.changed.connect(_on_state_changed) if not state.depleted.is_connected(_on_state_depleted): state.depleted.connect(_on_state_depleted) _update_presentation() return true 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_resource_node"): manager.register_resource_node(self) func get_amount_remaining() -> float: return state.get_amount_remaining() if state != null else initial_amount func get_reserved_by() -> int: return state.get_reserved_by() if state != null else -1 func is_enabled() -> bool: return state.is_enabled() if state != null else initial_enabled func is_depleted() -> bool: return state.is_depleted() if state != null else initial_amount <= 0.0 func can_extract() -> bool: return state.can_extract() if state != null else initial_enabled and not is_depleted() func _on_state_changed(changed_state: ResourceStateRecord) -> void: if changed_state != state: return amount_changed.emit(node_id, state.get_amount_remaining()) reservation_changed.emit(node_id, state.get_reserved_by()) _update_presentation() func _on_state_depleted(depleted_state: ResourceStateRecord) -> void: if depleted_state == state: depleted.emit(node_id) func _update_presentation() -> void: if has_node("Visual"): $Visual.visible = not (hide_visual_when_depleted and is_depleted()) if not has_node("DebugLabel"): return var label := $DebugLabel as Label3D var text := "%s\n%s %.1f" % [ node_id, resource_id, get_amount_remaining() ] if get_reserved_by() >= 0: text += "\nheld:%d" % get_reserved_by() if is_depleted(): text += "\nDEPLETED" if state == null: text += "\nUNBOUND" label.text = text static func get_by_id(search_id: StringName) -> ResourceNode: for node in _all: if node.node_id == search_id: return node return null static func get_all() -> Array[ResourceNode]: return _all.duplicate()