From 326df51cbe63490196bc807578d9dcc0a96527e5 Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Sat, 4 Jul 2026 19:47:31 +0200 Subject: [PATCH] feat: add player resource node harvesting --- main.tscn | 4 +- player/player.gd | 39 +++++++++++---- simulation/SimulationManager.gd | 26 ++++++++-- tests/resource_node_player_parity_test.gd | 50 +++++++++++++++++++ tests/resource_node_player_parity_test.gd.uid | 1 + world/resource_nodes/ResourceNode.gd | 15 ++++++ 6 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 tests/resource_node_player_parity_test.gd create mode 100644 tests/resource_node_player_parity_test.gd.uid diff --git a/main.tscn b/main.tscn index 644b13d..fa6095b 100644 --- a/main.tscn +++ b/main.tscn @@ -77,13 +77,11 @@ skeleton = NodePath("../../..") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.18380833, 0) shape = SubResource("BoxShape3D_lquwl") -[node name="Player" type="CharacterBody3D" parent="." unique_id=2022843760 node_paths=PackedStringArray("camera_rig", "simulation_manager", "farm_zone", "forest_zone", "food_zone", "guard_zone", "study_zone")] +[node name="Player" type="CharacterBody3D" parent="." unique_id=2022843760 node_paths=PackedStringArray("camera_rig", "simulation_manager", "food_zone", "guard_zone", "study_zone")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0) script = ExtResource("1_h2yge") camera_rig = NodePath("../CameraRig") simulation_manager = NodePath("../SimulationManager") -farm_zone = NodePath("../TaskZone/FarmZone") -forest_zone = NodePath("../TaskZone/ForestZone") food_zone = NodePath("../TaskZone/FoodZone") guard_zone = NodePath("../TaskZone/GuardZone") study_zone = NodePath("../TaskZone/StudyZone") diff --git a/player/player.gd b/player/player.gd index f2c11eb..6bd7348 100644 --- a/player/player.gd +++ b/player/player.gd @@ -7,8 +7,6 @@ extends CharacterBody3D @export var simulation_manager: Node -@export var farm_zone: Marker3D -@export var forest_zone: Marker3D @export var food_zone: Marker3D @export var guard_zone: Marker3D @export var study_zone: Marker3D @@ -62,13 +60,10 @@ func try_interact() -> void: if simulation_manager == null: return - if is_near(farm_zone): - simulation_manager.add_food(5.0) - print("Player gathered food for the village.") - elif is_near(forest_zone): - simulation_manager.add_wood(5.0) - print("Player gathered wood for the village.") - elif is_near(guard_zone): + if try_harvest_resource_node(): + return + + if is_near(guard_zone): simulation_manager.add_safety(3.0) print("Player helped guard the village.") elif is_near(study_zone): @@ -76,7 +71,31 @@ func try_interact() -> void: print("Player shared knowledge.") elif is_near(food_zone): simulation_manager.eat_food(stomach_capacity_for_food) - print("Player eating shit up.") + print("Player ate food from the village supply.") + +func try_harvest_resource_node() -> bool: + var node := ResourceNode.find_nearest_for_player(global_position, interaction_range) + if node == null: + return false + + if not node.enabled: + print("%s is unavailable." % node.node_id) + return true + + if node.is_depleted(): + print("%s is depleted." % node.node_id) + return true + + if not simulation_manager.has_method("harvest_resource_node"): + push_error("Player: SimulationManager cannot harvest ResourceNodes") + return true + + var extracted: float = simulation_manager.harvest_resource_node(node) + if extracted > 0.0: + print("Player gathered %s %s from %s." % [extracted, node.resource_id, node.node_id]) + else: + print("%s could not be harvested." % node.node_id) + return true func is_near(zone: Marker3D) -> bool: if zone == null: diff --git a/simulation/SimulationManager.gd b/simulation/SimulationManager.gd index db3a94a..62be6bb 100644 --- a/simulation/SimulationManager.gd +++ b/simulation/SimulationManager.gd @@ -208,24 +208,40 @@ func notify_npc_navigation_failed(npc_id: int) -> void: func notify_npc_target_unavailable(npc_id: int) -> void: notify_npc_navigation_failed(npc_id) +func harvest_resource_node(node: ResourceNode) -> float: + if node == null or not node.can_player_use: + return 0.0 + + var extracted := node.extract() + if extracted <= 0.0: + return 0.0 + + village.apply_resource_delta(node.resource_id, extracted) + village_changed.emit(village) + + if DEBUG_LOGS: + print("[SimulationManager] Player extracted ", extracted, " ", node.resource_id, " from ", node.node_id) + + return extracted + func eat_food(amount: float) -> void: - village.food -= amount + village.apply_resource_delta(&"food", -amount) village_changed.emit(village) func add_food(amount: float) -> void: - village.food += amount + village.apply_resource_delta(&"food", amount) village_changed.emit(village) func add_wood(amount: float) -> void: - village.wood += amount + village.apply_resource_delta(&"wood", amount) village_changed.emit(village) func add_safety(amount: float) -> void: - village.safety = clamp(village.safety + amount, 0.0, 100.0) + village.apply_resource_delta(&"safety", amount) village_changed.emit(village) func add_knowledge(amount: float) -> void: - village.knowledge += amount + village.apply_resource_delta(&"knowledge", amount) village_changed.emit(village) func get_starving_count() -> int: diff --git a/tests/resource_node_player_parity_test.gd b/tests/resource_node_player_parity_test.gd new file mode 100644 index 0000000..56c6f9e --- /dev/null +++ b/tests/resource_node_player_parity_test.gd @@ -0,0 +1,50 @@ +extends SceneTree + +var main_scene: Node +var failures: Array[String] = [] + +func _initialize() -> void: + call_deferred("_run") + +func _run() -> void: + main_scene = load("res://main.tscn").instantiate() + root.add_child(main_scene) + await process_frame + await physics_frame + + var simulation_manager := main_scene.get_node("SimulationManager") + var player := main_scene.get_node("Player") + var bush := main_scene.get_node("ResourceNodes/BerryBush_01") as ResourceNode + var tree := main_scene.get_node("ResourceNodes/Tree_01") as ResourceNode + simulation_manager.set_process(false) + + bush.amount_remaining = 1.0 + _check(bush.reserve(999), "Player contention setup should reserve the bush") + player.global_position = bush.interaction_point.global_position + var food_before: float = simulation_manager.village.food + player.try_interact() + + _check(is_equal_approx(bush.amount_remaining, 0.0), "Player should deplete the nearby bush") + _check(is_equal_approx(simulation_manager.village.food, food_before + 1.0), "Village should receive exactly the bush's remaining food") + _check(bush.reserved_by == -1, "Depletion should release the NPC reservation") + + player.global_position = tree.interaction_point.global_position + var wood_before: float = simulation_manager.village.wood + var tree_before := tree.amount_remaining + player.try_interact() + + _check(is_equal_approx(tree.amount_remaining, tree_before - tree.yield_per_action), "Player should extract the tree's configured yield") + _check(is_equal_approx(simulation_manager.village.wood, wood_before + tree.yield_per_action), "Village should receive exactly the extracted wood") + + if failures.is_empty(): + print("[TEST] ResourceNode player parity passed") + quit(0) + return + + for failure in failures: + push_error("[TEST] " + failure) + quit(1) + +func _check(condition: bool, message: String) -> void: + if not condition: + failures.append(message) diff --git a/tests/resource_node_player_parity_test.gd.uid b/tests/resource_node_player_parity_test.gd.uid new file mode 100644 index 0000000..7b27afb --- /dev/null +++ b/tests/resource_node_player_parity_test.gd.uid @@ -0,0 +1 @@ +uid://dunn1mc6772fq diff --git a/world/resource_nodes/ResourceNode.gd b/world/resource_nodes/ResourceNode.gd index a58c9a9..1dc3dcc 100644 --- a/world/resource_nodes/ResourceNode.gd +++ b/world/resource_nodes/ResourceNode.gd @@ -67,6 +67,9 @@ func extract(requested_amount: float = yield_per_action) -> float: amount_remaining -= extracted amount_changed.emit(node_id, amount_remaining) if is_depleted(): + if reserved_by >= 0: + reserved_by = -1 + reservation_changed.emit(node_id, -1) depleted.emit(node_id) if hide_visual_when_depleted and has_node("Visual"): $Visual.visible = false @@ -104,3 +107,15 @@ static func find_available(action: StringName, agent_id: int, from_position: Vec best_dist = dist best = node return best + +static func find_nearest_for_player(from_position: Vector3, max_distance: float) -> ResourceNode: + var best: ResourceNode + var best_dist := max_distance * max_distance + for node in _all: + if not node.can_player_use: + continue + var dist := from_position.distance_squared_to(node.interaction_point.global_position) + if dist <= best_dist: + best_dist = dist + best = node + return best