From eef2068747ae62192767e366cc6913fa8f7c78f1 Mon Sep 17 00:00:00 2001
From: Rijad Zuzo <rijad.zuzo@matriq.ch>
Date: Sun, 21 Jun 2026 14:41:19 +0200
Subject: [PATCH] feat: add better priorities setting

---
 player/npc/NpcVisual.gd  |  57 ++++++++++--------
 simulation/SimNPC.gd     | 124 ++++++++++++++++++++++++++++++++-------
 simulation/SimVillage.gd |  59 +++++++++++++++++--
 3 files changed, 191 insertions(+), 49 deletions(-)

diff --git a/player/npc/NpcVisual.gd b/player/npc/NpcVisual.gd
index b7cd605..4b22fb7 100644
--- a/player/npc/NpcVisual.gd
+++ b/player/npc/NpcVisual.gd
@@ -2,7 +2,7 @@ extends CharacterBody3D
 
 signal arrived_at_target(sim_id: int)
 
-const DEBUG_LOGS := false
+const DEBUG_LOGS := true
 
 @export var move_speed := 3.0
 @export var rotation_speed := 8.0
@@ -18,6 +18,7 @@ var profession: String = ""
 var current_path: PackedVector3Array = []
 var path_index := 0
 var current_target := Vector3.INF
+var arrival_distance := 0.6
 
 func debug_log(message: String) -> void:
 	if DEBUG_LOGS:
@@ -30,28 +31,15 @@ func setup_from_sim(npc: SimNPC) -> void:
 	name = "NPC_%s_%s" % [sim_id, npc_name]
 
 func set_target_position(pos: Vector3) -> void:
-	if current_target.distance_to(pos) < 0.1:
-		return
-
 	current_target = pos
 	has_reported_arrival = false
-	nav_agent.target_position = pos
 
-	await get_tree().physics_frame
-
-	current_path = NavigationServer3D.map_get_path(
-		get_world_3d().navigation_map,
-		global_position,
-		pos,
-		true
-	)
-
-	path_index = 0
-	debug_log("New target: %s Path points: %s" % [pos, current_path.size()])
-	if current_target.distance_to(pos) < 0.1:
+	if global_position.distance_to(pos) <= arrival_distance:
+		current_path = PackedVector3Array()
+		path_index = 0
+		debug_log("Already close to target, will report arrival")
 		return
 
-	current_target = pos
 	nav_agent.target_position = pos
 
 	await get_tree().physics_frame
@@ -65,18 +53,26 @@ func set_target_position(pos: Vector3) -> void:
 
 	path_index = 0
 
-	debug_log("New target: %s Path points: %s" % [pos, current_path.size()])
+	if current_path.is_empty():
+		debug_log("Path is empty. Will report arrival/failure fallback.")
+	else:
+		debug_log("New target: %s Path points: %s" % [pos, current_path.size()])
 
 func _physics_process(delta: float) -> void:
+	if current_target == Vector3.INF:
+		velocity = Vector3.ZERO
+		move_and_slide()
+		return
+
+	if global_position.distance_to(current_target) <= arrival_distance:
+		_report_arrival_once()
+		velocity = Vector3.ZERO
+		move_and_slide()
+		return
+
 	if current_path.is_empty() or path_index >= current_path.size():
 		velocity = Vector3.ZERO
 		move_and_slide()
-
-		if not has_reported_arrival and current_target != Vector3.INF:
-			has_reported_arrival = true
-			debug_log("Arrived at target")
-			arrived_at_target.emit(sim_id)
-
 		return
 
 	var next_pos := current_path[path_index]
@@ -97,3 +93,14 @@ func _physics_process(delta: float) -> void:
 
 	var target_angle := atan2(direction.x, direction.z)
 	rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta)
+	
+func _report_arrival_once() -> void:
+	if has_reported_arrival:
+		return
+
+	has_reported_arrival = true
+	current_path = PackedVector3Array()
+	path_index = 0
+
+	debug_log("Arrived at target")
+	arrived_at_target.emit(sim_id)
diff --git a/simulation/SimNPC.gd b/simulation/SimNPC.gd
index e218040..84d04a7 100644
--- a/simulation/SimNPC.gd
+++ b/simulation/SimNPC.gd
@@ -1,6 +1,8 @@
 class_name SimNPC
 extends RefCounted
 
+@export var DEBUG_LOGS := true
+
 const TASK_STATE_IDLE := "idle"
 const TASK_STATE_TRAVELING := "traveling"
 const TASK_STATE_WORKING := "working"
@@ -79,34 +81,116 @@ func choose_new_task(village: SimVillage) -> void:
 		set_task("rest", 4.0)
 		return
 
-	if village.food < 15.0:
-		set_task("gather_food", 5.0)
-		return
+	var best_task := choose_best_work_task(village)
 
-	if village.wood < 10.0:
-		set_task("gather_wood", 5.0)
-		return
-
-	if village.safety < 40.0 and profession == "guard":
-		set_task("patrol", 6.0)
-		return
-
-	if village.knowledge < 10.0 and profession == "scholar":
-		set_task("study", 6.0)
-		return
-
-	match profession:
-		"farmer":
+	match best_task:
+		"gather_food":
 			set_task("gather_food", 5.0)
-		"woodcutter":
+		"gather_wood":
 			set_task("gather_wood", 5.0)
-		"guard":
+		"patrol":
 			set_task("patrol", 6.0)
-		"scholar":
+		"study":
 			set_task("study", 6.0)
 		_:
 			set_task("wander", 4.0)
 
+func choose_best_work_task(village: SimVillage) -> String:
+	var food_score := calculate_task_score(
+		"gather_food",
+		village.food_priority,
+		village.food,
+		15.0,
+		30.0,
+		3.0,
+		1.5,
+		"farmer"
+	)
+
+	var wood_score := calculate_task_score(
+		"gather_wood",
+		village.wood_priority,
+		village.wood,
+		10.0,
+		20.0,
+		2.5,
+		1.0,
+		"woodcutter"
+	)
+
+	var safety_score := calculate_task_score(
+		"patrol",
+		village.safety_priority,
+		village.safety,
+		30.0,
+		50.0,
+		3.0,
+		1.5,
+		"guard"
+	)
+
+	var knowledge_score := calculate_task_score(
+		"study",
+		village.knowledge_priority,
+		village.knowledge,
+		10.0,
+		25.0,
+		1.5,
+		0.75,
+		"scholar"
+	)
+
+	var best_task := "gather_food"
+	var best_score := food_score
+
+	if wood_score > best_score:
+		best_task = "gather_wood"
+		best_score = wood_score
+
+	if safety_score > best_score:
+		best_task = "patrol"
+		best_score = safety_score
+
+	if knowledge_score > best_score:
+		best_task = "study"
+		best_score = knowledge_score
+
+	return best_task
+	
+func calculate_task_score(
+	task_name: String,
+	base_priority: float,
+	resource_amount: float,
+	critical_threshold: float,
+	low_threshold: float,
+	critical_bonus: float,
+	low_bonus: float,
+	preferred_profession: String
+) -> float:
+	var score := base_priority
+
+	if resource_amount < critical_threshold:
+		score += critical_bonus
+	elif resource_amount < low_threshold:
+		score += low_bonus
+
+	if profession == preferred_profession:
+		score += 2.0
+
+	score += randf_range(0.0, 0.5)
+
+	if DEBUG_LOGS:
+		print(
+			"[SimNPC] ",
+			npc_name,
+			" score ",
+			task_name,
+			" = ",
+			score
+		)
+
+	return score
+
 func set_task(task: String, duration: float) -> void:
 	current_task = task
 	task_duration = duration
diff --git a/simulation/SimVillage.gd b/simulation/SimVillage.gd
index 392f353..360bfd4 100644
--- a/simulation/SimVillage.gd
+++ b/simulation/SimVillage.gd
@@ -13,6 +13,11 @@ var wood_modifier := 1.0
 var safety_modifier := 1.0
 var knowledge_modifier := 1.0
 
+var food_priority := 1.0
+var wood_priority := 1.0
+var safety_priority := 1.0
+var knowledge_priority := 1.0
+
 func update_modifiers() -> void:
 	food_modifier = 1.0
 	wood_modifier = 1.0
@@ -34,15 +39,44 @@ func update_modifiers() -> void:
 			knowledge_modifier
 		]
 	)
+	
+func update_priorities() -> void:
+	food_priority = 1.0
+	wood_priority = 1.0
+	safety_priority = 1.0
+	knowledge_priority = 1.0
 
+	if food < 10.0:
+		food_priority = 3.0
+	elif food < 20.0:
+		food_priority = 2.0
+
+	if wood < 5.0:
+		wood_priority = 2.5
+	elif wood < 15.0:
+		wood_priority = 1.75
+
+	if safety < 25.0:
+		safety_priority = 3.0
+	elif safety < 50.0:
+		safety_priority = 1.5
+
+	if knowledge < 5.0:
+		knowledge_priority = 1.75
+	elif knowledge < 15.0:
+		knowledge_priority = 1.25
+
+	if DEBUG_LOGS:
+		print(get_priority_summary())
 
 func apply_npc_task(npc: SimNPC) -> void:
 	var productivity := 1.0
+
 	if npc.energy < 20.0:
 		productivity = 0.25
 	elif npc.energy < 40.0:
 		productivity = 0.5
-		
+
 	match npc.current_task:
 		"gather_food":
 			food += 2.0 * productivity
@@ -54,8 +88,9 @@ func apply_npc_task(npc: SimNPC) -> void:
 			knowledge += 1.0 * productivity
 		"eat":
 			food -= 1.0
+			npc.hunger = max(npc.hunger - 35.0, 0.0)
 		"rest":
-			pass
+			npc.energy = min(npc.energy + 35.0, 100.0)
 		"wander":
 			safety -= 0.2
 
@@ -63,14 +98,30 @@ func apply_npc_task(npc: SimNPC) -> void:
 	wood = max(wood, 0.0)
 	safety = clamp(safety, 0.0, 100.0)
 	knowledge = max(knowledge, 0.0)
+
 	update_modifiers()
+	update_priorities()
 
 func get_summary() -> String:
-	return "Village | food: %s | wood: %s | safety: %s | knowledge: %s" % [
+
+	return "Village | food: %s | wood: %s | safety: %s | knowledge: %s | priorities F/W/S/K: %s/%s/%s/%s" % [
 		round(food),
 		round(wood),
 		round(safety),
-		round(knowledge)
+		round(knowledge),
+		food_priority,
+		wood_priority,
+		safety_priority,
+		knowledge_priority
+	]
+
+
+func get_priority_summary() -> String:
+	return "Priorities | food: %s | wood: %s | safety: %s | knowledge: %s" % [
+		food_priority,
+		wood_priority,
+		safety_priority,
+		knowledge_priority
 	]
 
 func debug_log(message: String) -> void:
