feat: add ResourceNode system with target selection and reservation
Phase 1-2 of the ResourceNode migration plan: - Create ResourceNode class (world/resource_nodes/) with extraction, reservation, nearest-available selection, and debug label - Place 4 resource instances in main.tscn (2 berry bushes, 2 trees) - Add target_id to SimNPC for tracking which node an NPC is using - Add set_npc_target_id and release_npc_reservation to SimulationManager with cleanup on task change, completion, and death - Update WorldViewManager to query available ResourceNodes first, falling back to zone markers with a logged warning
This commit is contained in:
@@ -48,6 +48,7 @@ crash_handler*
|
||||
*.temp
|
||||
*.tmp
|
||||
*~
|
||||
~*.dll
|
||||
.#*
|
||||
\#*\#
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[ext_resource type="PackedScene" uid="uid://q0w3en4bfk42" path="res://assets/foliage/tree.glb" id="6_5vw27"]
|
||||
[ext_resource type="Script" uid="uid://cjvbgqx8rao0t" path="res://world/ui/ui.gd" id="6_7mycd"]
|
||||
[ext_resource type="PackedScene" uid="uid://dyu3r0cl0judq" path="res://assets/foliage/tree-high.glb" id="7_kek77"]
|
||||
[ext_resource type="PackedScene" uid="uid://dvd3qjpjor4sq" path="res://world/resource_nodes/ResourceNode.tscn" id="8_r3s0u"]
|
||||
|
||||
[sub_resource type="NavigationMesh" id="NavigationMesh_lquwl"]
|
||||
vertices = PackedVector3Array(-14.5, 0.5, 0, -1, 0.5, 0, -1, 0.5, -0.75, 0, 0.5, -1, 0, 0.5, -14.5, -14.5, 0.5, -14.5, 0.75, 0.5, -1, 0.75, 0.75, -0.5, 14.5, 0.5, -0.5, 14.5, 0.5, -14.5, 0.75, 0.75, 0, 0, 1, 0, 0, 0.75, 0.75, -0.5, 0.75, 0.75, -0.5, 0.5, 14.5, 14.5, 0.5, 14.5, -1, 0.5, 0.75, -14.5, 0.5, 14.5)
|
||||
@@ -211,3 +212,30 @@ theme_override_constants/margin_bottom = 4
|
||||
[node name="VillageStatsLabel" type="Label" parent="UI/VillagePanel/MarginContainer" unique_id=1554319259]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 10
|
||||
|
||||
[node name="ResourceNodes" type="Node3D" parent="." unique_id=1364178452]
|
||||
|
||||
[node name="BerryBush_01" parent="ResourceNodes" unique_id=42005298 instance=ExtResource("8_r3s0u")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -8)
|
||||
node_id = &"berry_bush_01"
|
||||
|
||||
[node name="BerryBush_02" parent="ResourceNodes" unique_id=2100422483 instance=ExtResource("8_r3s0u")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -10, 0, -4)
|
||||
node_id = &"berry_bush_02"
|
||||
amount_remaining = 8.0
|
||||
|
||||
[node name="Tree_01" parent="ResourceNodes" unique_id=1162125845 instance=ExtResource("8_r3s0u")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0, -8)
|
||||
node_id = &"tree_01"
|
||||
action_id = &"gather_wood"
|
||||
resource_id = &"wood"
|
||||
amount_remaining = 15.0
|
||||
yield_per_action = 3.0
|
||||
|
||||
[node name="Tree_02" parent="ResourceNodes" unique_id=1626186397 instance=ExtResource("8_r3s0u")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 10, 0, -4)
|
||||
node_id = &"tree_02"
|
||||
action_id = &"gather_wood"
|
||||
resource_id = &"wood"
|
||||
amount_remaining = 12.0
|
||||
yield_per_action = 3.0
|
||||
|
||||
@@ -27,6 +27,7 @@ var is_dead := false
|
||||
var task_duration := 0.0
|
||||
var task_progress := 0.0
|
||||
var task_complete := true
|
||||
var target_id: StringName = &""
|
||||
|
||||
func _init(
|
||||
_id: int,
|
||||
|
||||
@@ -59,13 +59,19 @@ func simulate_tick() -> void:
|
||||
|
||||
for npc in npcs:
|
||||
var old_task := npc.current_task
|
||||
var old_target := npc.target_id
|
||||
var old_state := npc.task_state
|
||||
var was_complete := npc.task_complete
|
||||
var was_dead := npc.is_dead
|
||||
|
||||
npc.simulate_tick(village)
|
||||
|
||||
if old_task != npc.current_task and old_target != &"":
|
||||
release_npc_reservation(npc.id)
|
||||
|
||||
if not was_dead and npc.is_dead:
|
||||
if old_target != &"":
|
||||
release_npc_reservation(npc.id)
|
||||
npc_died.emit(npc)
|
||||
npc_task_changed.emit(npc, old_task, npc.current_task)
|
||||
|
||||
@@ -79,6 +85,9 @@ func simulate_tick() -> void:
|
||||
var completed_task := npc.current_task
|
||||
var needs_immediate_food_after_gather := npc.should_eat_immediately_after_task()
|
||||
|
||||
if npc.target_id != &"":
|
||||
release_npc_reservation(npc.id)
|
||||
|
||||
village.apply_npc_task(npc)
|
||||
village_was_changed = true
|
||||
|
||||
@@ -129,6 +138,22 @@ func simulate_tick() -> void:
|
||||
if DEBUG_LOGS:
|
||||
print(village.get_summary())
|
||||
|
||||
func set_npc_target_id(npc_id: int, target_id: StringName) -> void:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
npc.target_id = target_id
|
||||
return
|
||||
|
||||
func release_npc_reservation(npc_id: int) -> void:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
if npc.target_id != &"":
|
||||
var node := ResourceNode.get_by_id(npc.target_id)
|
||||
if node != null:
|
||||
node.release(npc.id)
|
||||
npc.target_id = &""
|
||||
return
|
||||
|
||||
func notify_npc_arrived(npc_id: int) -> void:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://r3s0urc3n0d3v1
|
||||
@@ -0,0 +1,25 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://r3s0urc3tscn01"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://r3s0urc3n0d3v1" path="res://world/resource_nodes/ResourceNode.gd" id="1_abcde"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_visual"]
|
||||
|
||||
[node name="ResourceNode" type="Node3D"]
|
||||
script = ExtResource("1_abcde")
|
||||
|
||||
[node name="Visual" type="Node3D" parent="."]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Visual"]
|
||||
mesh = SubResource("BoxMesh_visual")
|
||||
|
||||
[node name="InteractionPoint" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2.0)
|
||||
|
||||
[node name="DebugLabel" type="Label3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.0, 0)
|
||||
billboard = 1
|
||||
no_depth_test = true
|
||||
font_size = 24
|
||||
outline_modulate = Color(0, 0, 0, 1)
|
||||
outline_size = 4
|
||||
text = "INIT"
|
||||
@@ -83,6 +83,11 @@ func update_npc_targets() -> void:
|
||||
continue
|
||||
|
||||
var visual = active_npc_visuals[npc.id]
|
||||
var resource_pos := _try_get_resource_target(npc, npc.current_task)
|
||||
if resource_pos != null:
|
||||
visual.set_target_position(resource_pos)
|
||||
continue
|
||||
|
||||
var target := get_target_for_task(npc.current_task)
|
||||
|
||||
if target == null:
|
||||
@@ -108,6 +113,32 @@ func get_target_for_task(task: String) -> Marker3D:
|
||||
_:
|
||||
return rest_zone
|
||||
|
||||
func _try_get_resource_target(npc: SimNPC, task: String):
|
||||
var action_map := {
|
||||
"gather_food": &"gather_food",
|
||||
"gather_wood": &"gather_wood"
|
||||
}
|
||||
var action_id := action_map.get(task) as StringName
|
||||
if action_id == null:
|
||||
return null
|
||||
|
||||
var visual = active_npc_visuals.get(npc.id) as Node3D
|
||||
if visual == null:
|
||||
return null
|
||||
|
||||
var node := ResourceNode.find_available(action_id, npc.id, visual.global_position)
|
||||
if node == null:
|
||||
debug_log("No available resource node for %s, using zone fallback" % task)
|
||||
return null
|
||||
|
||||
if not node.reserve(npc.id):
|
||||
debug_log("Failed to reserve %s for %s" % [node.node_id, npc.npc_name])
|
||||
return null
|
||||
|
||||
npc.target_id = node.node_id
|
||||
debug_log("%s reserved %s for %s" % [npc.npc_name, node.node_id, task])
|
||||
return node.interaction_point.global_position
|
||||
|
||||
func _on_npc_task_changed(npc: SimNPC, old_task: String, new_task: String) -> void:
|
||||
if DEBUG_LOGS:
|
||||
debug_log("%s changed task: %s -> %s" % [npc.npc_name, old_task, new_task])
|
||||
@@ -116,6 +147,11 @@ func _on_npc_task_changed(npc: SimNPC, old_task: String, new_task: String) -> vo
|
||||
return
|
||||
|
||||
var visual = active_npc_visuals[npc.id]
|
||||
var resource_pos := _try_get_resource_target(npc, new_task)
|
||||
if resource_pos != null:
|
||||
visual.set_target_position(resource_pos)
|
||||
return
|
||||
|
||||
var target := get_target_for_task(new_task)
|
||||
|
||||
if target != null:
|
||||
|
||||
Reference in New Issue
Block a user