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 = &"gather_food" @export var resource_id: StringName = &"food" @export var amount_remaining: float = 10.0 @export var yield_per_action: float = 2.0 @export var 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 reserved_by: int = -1 func _ready(): if node_id.is_empty(): push_error("ResourceNode at %s has empty node_id" % get_path()) _all.append(self) add_to_group("resource_nodes") amount_changed.connect(_update_debug_label) reservation_changed.connect(_update_debug_label) _update_debug_label() func _exit_tree(): _all.erase(self) func is_depleted() -> bool: return amount_remaining <= 0.0 func can_extract() -> bool: return enabled and not is_depleted() func is_available_for(agent_id: int) -> bool: return enabled and not is_depleted() and (reserved_by == -1 or reserved_by == agent_id) func reserve(agent_id: int) -> bool: if not is_available_for(agent_id): return false reserved_by = agent_id reservation_changed.emit(node_id, agent_id) return true func release(agent_id: int) -> void: if reserved_by == agent_id: reserved_by = -1 reservation_changed.emit(node_id, -1) func extract(requested_amount: float = yield_per_action) -> float: if not can_extract(): return 0.0 var extracted := minf(requested_amount, amount_remaining) amount_remaining -= extracted amount_changed.emit(node_id, amount_remaining) if is_depleted(): depleted.emit(node_id) if hide_visual_when_depleted and has_node("Visual"): $Visual.visible = false return extracted func _update_debug_label(_a = null, _b = null) -> void: if not has_node("DebugLabel"): return var label := $DebugLabel as Label3D var text := "%s\n%s %.1f" % [node_id, resource_id, amount_remaining] if reserved_by >= 0: text += "\nheld:%d" % reserved_by if is_depleted(): text += "\nDEPLETED" label.text = text static func get_by_id(node_id: StringName) -> ResourceNode: for node in _all: if node.node_id == node_id: return node return null static func find_available(action: StringName, agent_id: int, from_position: Vector3) -> ResourceNode: var best: ResourceNode var best_dist := INF for node in _all: if node.action_id != action: continue if not node.can_npcs_use: continue if not node.is_available_for(agent_id): continue var dist := from_position.distance_squared_to(node.global_position) if dist < best_dist: best_dist = dist best = node return best