refactor: separate action system responsibilities
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
[ext_resource type="PackedScene" uid="uid://dhxxyprqflotq" path="res://player/npc/NpcVisual.tscn" id="5_lquwl"]
|
[ext_resource type="PackedScene" uid="uid://dhxxyprqflotq" path="res://player/npc/NpcVisual.tscn" id="5_lquwl"]
|
||||||
[ext_resource type="Script" uid="uid://cjvbgqx8rao0t" path="res://world/ui/ui.gd" id="6_7mycd"]
|
[ext_resource type="Script" uid="uid://cjvbgqx8rao0t" path="res://world/ui/ui.gd" id="6_7mycd"]
|
||||||
[ext_resource type="PackedScene" uid="uid://dvd3qjpjor4sq" path="res://world/resource_nodes/ResourceNode.tscn" id="8_r3s0u"]
|
[ext_resource type="PackedScene" uid="uid://dvd3qjpjor4sq" path="res://world/resource_nodes/ResourceNode.tscn" id="8_r3s0u"]
|
||||||
|
[ext_resource type="Script" uid="uid://bbwol3mrjgn2x" path="res://world/active_world_adapter.gd" id="9_adapter"]
|
||||||
|
|
||||||
[sub_resource type="NavigationMesh" id="NavigationMesh_lquwl"]
|
[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)
|
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)
|
||||||
@@ -103,18 +104,22 @@ deadzone_forward = 2.0
|
|||||||
current = true
|
current = true
|
||||||
fov = 65.0
|
fov = 65.0
|
||||||
|
|
||||||
[node name="SimulationManager" type="Node" parent="." unique_id=1099676814]
|
[node name="ActiveWorldAdapter" type="Node" parent="." node_paths=PackedStringArray("guard_marker", "study_marker", "rest_marker", "food_marker")]
|
||||||
script = ExtResource("3_lquwl")
|
script = ExtResource("9_adapter")
|
||||||
|
guard_marker = NodePath("../ActivityMarkers/GuardZone")
|
||||||
|
study_marker = NodePath("../ActivityMarkers/StudyZone")
|
||||||
|
rest_marker = NodePath("../ActivityMarkers/RestZone")
|
||||||
|
food_marker = NodePath("../ActivityMarkers/FoodZone")
|
||||||
|
|
||||||
[node name="WorldViewManager" type="Node" parent="." unique_id=763982891 node_paths=PackedStringArray("simulation_manager", "active_npcs_parent", "guard_zone", "study_zone", "rest_zone", "food_zone")]
|
[node name="SimulationManager" type="Node" parent="." unique_id=1099676814 node_paths=PackedStringArray("active_world_adapter")]
|
||||||
|
script = ExtResource("3_lquwl")
|
||||||
|
active_world_adapter = NodePath("../ActiveWorldAdapter")
|
||||||
|
|
||||||
|
[node name="WorldViewManager" type="Node" parent="." unique_id=763982891 node_paths=PackedStringArray("simulation_manager", "active_npcs_parent")]
|
||||||
script = ExtResource("4_1bvp3")
|
script = ExtResource("4_1bvp3")
|
||||||
npc_visual_scene = ExtResource("5_lquwl")
|
npc_visual_scene = ExtResource("5_lquwl")
|
||||||
simulation_manager = NodePath("../SimulationManager")
|
simulation_manager = NodePath("../SimulationManager")
|
||||||
active_npcs_parent = NodePath("../ActiveNPCs")
|
active_npcs_parent = NodePath("../ActiveNPCs")
|
||||||
guard_zone = NodePath("../ActivityMarkers/GuardZone")
|
|
||||||
study_zone = NodePath("../ActivityMarkers/StudyZone")
|
|
||||||
rest_zone = NodePath("../ActivityMarkers/RestZone")
|
|
||||||
food_zone = NodePath("../ActivityMarkers/FoodZone")
|
|
||||||
|
|
||||||
[node name="EventBus" type="Node" parent="." unique_id=1149294963]
|
[node name="EventBus" type="Node" parent="." unique_id=1149294963]
|
||||||
|
|
||||||
|
|||||||
@@ -55,56 +55,6 @@ func _init(
|
|||||||
random_source.randf_range(-8.0, 8.0)
|
random_source.randf_range(-8.0, 8.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
func simulate_tick(village: SimVillage) -> void:
|
|
||||||
if is_dead:
|
|
||||||
return
|
|
||||||
|
|
||||||
update_needs(village)
|
|
||||||
|
|
||||||
if is_dead:
|
|
||||||
return
|
|
||||||
|
|
||||||
if task_state == TASK_STATE_IDLE or task_state == TASK_STATE_COMPLETE:
|
|
||||||
choose_new_task(village)
|
|
||||||
return
|
|
||||||
|
|
||||||
if task_state == TASK_STATE_TRAVELING:
|
|
||||||
return
|
|
||||||
|
|
||||||
if task_state == TASK_STATE_WORKING:
|
|
||||||
task_progress += 1.0
|
|
||||||
|
|
||||||
if task_progress >= task_duration:
|
|
||||||
task_complete = true
|
|
||||||
task_state = TASK_STATE_COMPLETE
|
|
||||||
|
|
||||||
func update_needs(village: SimVillage) -> void:
|
|
||||||
hunger += 3.0 * village.food_modifier
|
|
||||||
|
|
||||||
match task_state:
|
|
||||||
TASK_STATE_IDLE:
|
|
||||||
energy -= 0.5
|
|
||||||
TASK_STATE_TRAVELING:
|
|
||||||
energy -= 2.0
|
|
||||||
TASK_STATE_WORKING:
|
|
||||||
if current_task != SimulationIds.ACTION_REST:
|
|
||||||
energy -= 3.0
|
|
||||||
TASK_STATE_COMPLETE:
|
|
||||||
energy -= 0.25
|
|
||||||
|
|
||||||
hunger = clamp(hunger, 0.0, 100.0)
|
|
||||||
energy = clamp(energy, 0.0, 100.0)
|
|
||||||
|
|
||||||
is_starving = hunger >= 90.0
|
|
||||||
|
|
||||||
if is_starving:
|
|
||||||
starvation_ticks += 1
|
|
||||||
else:
|
|
||||||
starvation_ticks = 0
|
|
||||||
|
|
||||||
if starvation_ticks >= starvation_death_threshold:
|
|
||||||
die_from_starvation()
|
|
||||||
|
|
||||||
func die_from_starvation() -> void:
|
func die_from_starvation() -> void:
|
||||||
is_dead = true
|
is_dead = true
|
||||||
current_task = SimulationIds.ACTION_DEAD
|
current_task = SimulationIds.ACTION_DEAD
|
||||||
@@ -116,30 +66,6 @@ func die_from_starvation() -> void:
|
|||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("[SimNPC] ", npc_name, " died from starvation.")
|
print("[SimNPC] ", npc_name, " died from starvation.")
|
||||||
|
|
||||||
func choose_new_task(village: SimVillage) -> void:
|
|
||||||
if is_dead:
|
|
||||||
return
|
|
||||||
|
|
||||||
if is_starving:
|
|
||||||
if village.food > 0:
|
|
||||||
set_task(SimulationIds.ACTION_EAT, 1.0)
|
|
||||||
else:
|
|
||||||
set_task(SimulationIds.ACTION_GATHER_FOOD, 4.0)
|
|
||||||
return
|
|
||||||
|
|
||||||
if hunger > 75.0:
|
|
||||||
if village.food > 0:
|
|
||||||
set_task(SimulationIds.ACTION_EAT)
|
|
||||||
else:
|
|
||||||
set_task(SimulationIds.ACTION_GATHER_FOOD)
|
|
||||||
return
|
|
||||||
|
|
||||||
if energy < 25.0:
|
|
||||||
set_task(SimulationIds.ACTION_REST)
|
|
||||||
return
|
|
||||||
|
|
||||||
set_task(choose_best_work_task(village))
|
|
||||||
|
|
||||||
func should_eat_immediately_after_task() -> bool:
|
func should_eat_immediately_after_task() -> bool:
|
||||||
return (
|
return (
|
||||||
current_task == SimulationIds.ACTION_GATHER_FOOD
|
current_task == SimulationIds.ACTION_GATHER_FOOD
|
||||||
@@ -147,105 +73,6 @@ func should_eat_immediately_after_task() -> bool:
|
|||||||
and not is_dead
|
and not is_dead
|
||||||
)
|
)
|
||||||
|
|
||||||
func choose_best_work_task(village: SimVillage) -> StringName:
|
|
||||||
var food_score := calculate_task_score(
|
|
||||||
SimulationIds.ACTION_GATHER_FOOD,
|
|
||||||
village.food_priority,
|
|
||||||
village.food,
|
|
||||||
15.0,
|
|
||||||
30.0,
|
|
||||||
3.0,
|
|
||||||
1.5
|
|
||||||
)
|
|
||||||
|
|
||||||
var wood_score := calculate_task_score(
|
|
||||||
SimulationIds.ACTION_GATHER_WOOD,
|
|
||||||
village.wood_priority,
|
|
||||||
village.wood,
|
|
||||||
10.0,
|
|
||||||
20.0,
|
|
||||||
2.5,
|
|
||||||
1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
var safety_score := calculate_task_score(
|
|
||||||
SimulationIds.ACTION_PATROL,
|
|
||||||
village.safety_priority,
|
|
||||||
village.safety,
|
|
||||||
30.0,
|
|
||||||
50.0,
|
|
||||||
3.0,
|
|
||||||
1.5
|
|
||||||
)
|
|
||||||
|
|
||||||
var knowledge_score := calculate_task_score(
|
|
||||||
SimulationIds.ACTION_STUDY,
|
|
||||||
village.knowledge_priority,
|
|
||||||
village.knowledge,
|
|
||||||
10.0,
|
|
||||||
25.0,
|
|
||||||
1.5,
|
|
||||||
0.75
|
|
||||||
)
|
|
||||||
|
|
||||||
var best_task := SimulationIds.ACTION_GATHER_FOOD
|
|
||||||
var best_score := food_score
|
|
||||||
|
|
||||||
if wood_score > best_score:
|
|
||||||
best_task = SimulationIds.ACTION_GATHER_WOOD
|
|
||||||
best_score = wood_score
|
|
||||||
|
|
||||||
if safety_score > best_score:
|
|
||||||
best_task = SimulationIds.ACTION_PATROL
|
|
||||||
best_score = safety_score
|
|
||||||
|
|
||||||
if knowledge_score > best_score:
|
|
||||||
best_task = SimulationIds.ACTION_STUDY
|
|
||||||
best_score = knowledge_score
|
|
||||||
|
|
||||||
return best_task
|
|
||||||
|
|
||||||
func calculate_task_score(
|
|
||||||
action_id: StringName,
|
|
||||||
base_priority: float,
|
|
||||||
resource_amount: float,
|
|
||||||
critical_threshold: float,
|
|
||||||
low_threshold: float,
|
|
||||||
critical_bonus: float,
|
|
||||||
low_bonus: float
|
|
||||||
) -> float:
|
|
||||||
var score := base_priority
|
|
||||||
|
|
||||||
if resource_amount < critical_threshold:
|
|
||||||
score += critical_bonus
|
|
||||||
elif resource_amount < low_threshold:
|
|
||||||
score += low_bonus
|
|
||||||
|
|
||||||
var definition := SimulationDefinitions.get_action(action_id)
|
|
||||||
if (
|
|
||||||
definition != null
|
|
||||||
and not definition.preferred_profession_id.is_empty()
|
|
||||||
and profession == definition.preferred_profession_id
|
|
||||||
):
|
|
||||||
score += 2.0
|
|
||||||
|
|
||||||
if action_id == last_task:
|
|
||||||
score -= 1.5
|
|
||||||
|
|
||||||
score += random_source.randf_range(0.0, 0.5)
|
|
||||||
|
|
||||||
if debug_logs:
|
|
||||||
print(
|
|
||||||
"[SimNPC] ",
|
|
||||||
npc_name,
|
|
||||||
" score ",
|
|
||||||
action_id,
|
|
||||||
" = ",
|
|
||||||
score
|
|
||||||
)
|
|
||||||
|
|
||||||
return score
|
|
||||||
|
|
||||||
func set_task(action_id: StringName, duration: float = -1.0) -> void:
|
func set_task(action_id: StringName, duration: float = -1.0) -> void:
|
||||||
var definition := SimulationDefinitions.get_action(action_id)
|
var definition := SimulationDefinitions.get_action(action_id)
|
||||||
if definition == null:
|
if definition == null:
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ signal npc_task_changed(
|
|||||||
)
|
)
|
||||||
signal village_changed(village: SimVillage)
|
signal village_changed(village: SimVillage)
|
||||||
signal npc_died(npc: SimNPC)
|
signal npc_died(npc: SimNPC)
|
||||||
|
signal npc_target_requested(npc: SimNPC)
|
||||||
|
signal npc_travel_requested(npc: SimNPC, target_position: Vector3)
|
||||||
|
|
||||||
var village := SimVillage.new()
|
var village := SimVillage.new()
|
||||||
|
|
||||||
@@ -14,11 +16,15 @@ var npcs: Array[SimNPC] = []
|
|||||||
@export var tick_interval := 1.2
|
@export var tick_interval := 1.2
|
||||||
@export var simulation_seed: int = 1337
|
@export var simulation_seed: int = 1337
|
||||||
@export var debug_logs := true
|
@export var debug_logs := true
|
||||||
|
@export var active_world_adapter: Node
|
||||||
|
|
||||||
var clock: SimulationClock
|
var clock: SimulationClock
|
||||||
var tick_count := 0
|
var tick_count := 0
|
||||||
var wander_random_sources := {}
|
var wander_random_sources := {}
|
||||||
var resource_states: Dictionary = {}
|
var resource_states: Dictionary = {}
|
||||||
|
var action_selector := ActionSelectionSystem.new()
|
||||||
|
var action_executor := ActionExecutionSystem.new()
|
||||||
|
var target_resolver := ActionTargetResolver.new()
|
||||||
|
|
||||||
var names := [
|
var names := [
|
||||||
"Amina", "Tarik", "Jasmin"
|
"Amina", "Tarik", "Jasmin"
|
||||||
@@ -97,7 +103,24 @@ func simulate_tick() -> void:
|
|||||||
var was_complete := npc.task_complete
|
var was_complete := npc.task_complete
|
||||||
var was_dead := npc.is_dead
|
var was_dead := npc.is_dead
|
||||||
|
|
||||||
npc.simulate_tick(village)
|
action_executor.advance_npc(npc, village)
|
||||||
|
if (
|
||||||
|
not npc.is_dead
|
||||||
|
and old_state in [
|
||||||
|
SimNPC.TASK_STATE_IDLE,
|
||||||
|
SimNPC.TASK_STATE_COMPLETE
|
||||||
|
]
|
||||||
|
and npc.task_state in [
|
||||||
|
SimNPC.TASK_STATE_IDLE,
|
||||||
|
SimNPC.TASK_STATE_COMPLETE
|
||||||
|
]
|
||||||
|
):
|
||||||
|
var selection := action_selector.select_action(npc, village)
|
||||||
|
if selection != null:
|
||||||
|
npc.set_task(
|
||||||
|
selection.action_id,
|
||||||
|
selection.duration_override
|
||||||
|
)
|
||||||
|
|
||||||
if old_task != npc.current_task and old_target != &"":
|
if old_task != npc.current_task and old_target != &"":
|
||||||
release_npc_reservation(npc.id)
|
release_npc_reservation(npc.id)
|
||||||
@@ -113,6 +136,7 @@ func simulate_tick() -> void:
|
|||||||
|
|
||||||
if old_task != npc.current_task and not npc.is_dead:
|
if old_task != npc.current_task and not npc.is_dead:
|
||||||
npc_task_changed.emit(npc, old_task, npc.current_task)
|
npc_task_changed.emit(npc, old_task, npc.current_task)
|
||||||
|
npc_target_requested.emit(npc)
|
||||||
|
|
||||||
if not was_complete and npc.task_complete and not npc.is_dead:
|
if not was_complete and npc.task_complete and not npc.is_dead:
|
||||||
var completed_task := npc.current_task
|
var completed_task := npc.current_task
|
||||||
@@ -162,6 +186,7 @@ func simulate_tick() -> void:
|
|||||||
if needs_immediate_food_after_gather and village.food > 0:
|
if needs_immediate_food_after_gather and village.food > 0:
|
||||||
npc.set_task(SimulationIds.ACTION_EAT, 1.0)
|
npc.set_task(SimulationIds.ACTION_EAT, 1.0)
|
||||||
npc_task_changed.emit(npc, completed_task, npc.current_task)
|
npc_task_changed.emit(npc, completed_task, npc.current_task)
|
||||||
|
npc_target_requested.emit(npc)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("[SimulationManager] ", npc.npc_name, " gathered food while starving and is going to eat immediately.")
|
print("[SimulationManager] ", npc.npc_name, " gathered food while starving and is going to eat immediately.")
|
||||||
@@ -255,6 +280,7 @@ func notify_npc_navigation_failed(npc_id: int) -> void:
|
|||||||
npc.last_task = failed_task
|
npc.last_task = failed_task
|
||||||
npc.set_task(SimulationIds.ACTION_WANDER, 2.0)
|
npc.set_task(SimulationIds.ACTION_WANDER, 2.0)
|
||||||
npc_task_changed.emit(npc, failed_task, npc.current_task)
|
npc_task_changed.emit(npc, failed_task, npc.current_task)
|
||||||
|
npc_target_requested.emit(npc)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("[SimulationManager] ", npc.npc_name, " could not navigate for ", failed_task, " and is trying a wander target")
|
print("[SimulationManager] ", npc.npc_name, " could not navigate for ", failed_task, " and is trying a wander target")
|
||||||
@@ -263,6 +289,29 @@ func notify_npc_navigation_failed(npc_id: int) -> void:
|
|||||||
func notify_npc_target_unavailable(npc_id: int) -> void:
|
func notify_npc_target_unavailable(npc_id: int) -> void:
|
||||||
notify_npc_navigation_failed(npc_id)
|
notify_npc_navigation_failed(npc_id)
|
||||||
|
|
||||||
|
func resolve_npc_target(npc_id: int, origin: Vector3) -> bool:
|
||||||
|
if active_world_adapter == null:
|
||||||
|
push_error("SimulationManager: active_world_adapter is missing")
|
||||||
|
return false
|
||||||
|
for npc in npcs:
|
||||||
|
if npc.id != npc_id:
|
||||||
|
continue
|
||||||
|
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_TRAVELING:
|
||||||
|
return false
|
||||||
|
var result := target_resolver.resolve(
|
||||||
|
npc,
|
||||||
|
origin,
|
||||||
|
self,
|
||||||
|
active_world_adapter
|
||||||
|
)
|
||||||
|
if result.is_empty():
|
||||||
|
notify_npc_target_unavailable(npc.id)
|
||||||
|
return false
|
||||||
|
npc.target_id = StringName(result.get("target_id", ""))
|
||||||
|
npc_travel_requested.emit(npc, result["position"])
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
func register_loaded_resource_nodes() -> void:
|
func register_loaded_resource_nodes() -> void:
|
||||||
for node in ResourceNode.get_all():
|
for node in ResourceNode.get_all():
|
||||||
register_resource_node(node)
|
register_resource_node(node)
|
||||||
@@ -305,33 +354,6 @@ func release_resource(node_id: StringName, agent_id: int) -> void:
|
|||||||
if resource_state != null:
|
if resource_state != null:
|
||||||
resource_state.release(agent_id)
|
resource_state.release(agent_id)
|
||||||
|
|
||||||
func acquire_resource_target(
|
|
||||||
action_id: StringName,
|
|
||||||
agent_id: int,
|
|
||||||
from_position: Vector3
|
|
||||||
) -> ResourceNode:
|
|
||||||
var best: ResourceNode
|
|
||||||
var best_distance := INF
|
|
||||||
for node in ResourceNode.get_all():
|
|
||||||
var resource_state := get_resource_state(node.node_id)
|
|
||||||
if resource_state == null:
|
|
||||||
continue
|
|
||||||
if resource_state.get_action_id() != action_id:
|
|
||||||
continue
|
|
||||||
if not resource_state.can_npc_use():
|
|
||||||
continue
|
|
||||||
if not resource_state.is_available_for(agent_id):
|
|
||||||
continue
|
|
||||||
var distance := from_position.distance_squared_to(
|
|
||||||
node.interaction_point.global_position
|
|
||||||
)
|
|
||||||
if distance < best_distance:
|
|
||||||
best_distance = distance
|
|
||||||
best = node
|
|
||||||
if best == null or not reserve_resource(best.node_id, agent_id):
|
|
||||||
return null
|
|
||||||
return best
|
|
||||||
|
|
||||||
func find_resource_node_for_player(
|
func find_resource_node_for_player(
|
||||||
from_position: Vector3,
|
from_position: Vector3,
|
||||||
max_distance: float
|
max_distance: float
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
class_name ActionExecutionSystem
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
func advance_npc(npc: SimNPC, village: SimVillage) -> void:
|
||||||
|
if npc.is_dead:
|
||||||
|
return
|
||||||
|
_update_needs(npc, village)
|
||||||
|
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_WORKING:
|
||||||
|
return
|
||||||
|
npc.task_progress += 1.0
|
||||||
|
if npc.task_progress >= npc.task_duration:
|
||||||
|
npc.task_complete = true
|
||||||
|
npc.task_state = SimNPC.TASK_STATE_COMPLETE
|
||||||
|
|
||||||
|
func _update_needs(npc: SimNPC, village: SimVillage) -> void:
|
||||||
|
npc.hunger += 3.0 * village.food_modifier
|
||||||
|
match npc.task_state:
|
||||||
|
SimNPC.TASK_STATE_IDLE:
|
||||||
|
npc.energy -= 0.5
|
||||||
|
SimNPC.TASK_STATE_TRAVELING:
|
||||||
|
npc.energy -= 2.0
|
||||||
|
SimNPC.TASK_STATE_WORKING:
|
||||||
|
if npc.current_task != SimulationIds.ACTION_REST:
|
||||||
|
npc.energy -= 3.0
|
||||||
|
SimNPC.TASK_STATE_COMPLETE:
|
||||||
|
npc.energy -= 0.25
|
||||||
|
npc.hunger = clamp(npc.hunger, 0.0, 100.0)
|
||||||
|
npc.energy = clamp(npc.energy, 0.0, 100.0)
|
||||||
|
npc.is_starving = npc.hunger >= 90.0
|
||||||
|
npc.starvation_ticks = npc.starvation_ticks + 1 if npc.is_starving else 0
|
||||||
|
if npc.starvation_ticks >= npc.starvation_death_threshold:
|
||||||
|
npc.die_from_starvation()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bcoht6j0fvva3
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
class_name ActionSelectionResult
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
var action_id: StringName
|
||||||
|
var duration_override := -1.0
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
selected_action_id: StringName,
|
||||||
|
selected_duration_override: float = -1.0
|
||||||
|
) -> void:
|
||||||
|
action_id = selected_action_id
|
||||||
|
duration_override = selected_duration_override
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dy0n3lije1lec
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
class_name ActionSelectionSystem
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
func select_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
||||||
|
if npc.is_dead:
|
||||||
|
return null
|
||||||
|
if npc.is_starving:
|
||||||
|
if village.food > 0:
|
||||||
|
return ActionSelectionResult.new(SimulationIds.ACTION_EAT, 1.0)
|
||||||
|
return ActionSelectionResult.new(SimulationIds.ACTION_GATHER_FOOD, 4.0)
|
||||||
|
if npc.hunger > 75.0:
|
||||||
|
if village.food > 0:
|
||||||
|
return ActionSelectionResult.new(SimulationIds.ACTION_EAT)
|
||||||
|
return ActionSelectionResult.new(SimulationIds.ACTION_GATHER_FOOD)
|
||||||
|
if npc.energy < 25.0:
|
||||||
|
return ActionSelectionResult.new(SimulationIds.ACTION_REST)
|
||||||
|
return ActionSelectionResult.new(_choose_best_work_action(npc, village))
|
||||||
|
|
||||||
|
func _choose_best_work_action(
|
||||||
|
npc: SimNPC,
|
||||||
|
village: SimVillage
|
||||||
|
) -> StringName:
|
||||||
|
var scores := {
|
||||||
|
SimulationIds.ACTION_GATHER_FOOD: _calculate_score(
|
||||||
|
npc, SimulationIds.ACTION_GATHER_FOOD, village.food_priority,
|
||||||
|
village.food, 15.0, 30.0, 3.0, 1.5
|
||||||
|
),
|
||||||
|
SimulationIds.ACTION_GATHER_WOOD: _calculate_score(
|
||||||
|
npc, SimulationIds.ACTION_GATHER_WOOD, village.wood_priority,
|
||||||
|
village.wood, 10.0, 20.0, 2.5, 1.0
|
||||||
|
),
|
||||||
|
SimulationIds.ACTION_PATROL: _calculate_score(
|
||||||
|
npc, SimulationIds.ACTION_PATROL, village.safety_priority,
|
||||||
|
village.safety, 30.0, 50.0, 3.0, 1.5
|
||||||
|
),
|
||||||
|
SimulationIds.ACTION_STUDY: _calculate_score(
|
||||||
|
npc, SimulationIds.ACTION_STUDY, village.knowledge_priority,
|
||||||
|
village.knowledge, 10.0, 25.0, 1.5, 0.75
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var best_action := SimulationIds.ACTION_GATHER_FOOD
|
||||||
|
var best_score: float = scores[best_action]
|
||||||
|
for action_id in [
|
||||||
|
SimulationIds.ACTION_GATHER_WOOD,
|
||||||
|
SimulationIds.ACTION_PATROL,
|
||||||
|
SimulationIds.ACTION_STUDY
|
||||||
|
]:
|
||||||
|
var score: float = scores[action_id]
|
||||||
|
if score > best_score:
|
||||||
|
best_action = action_id
|
||||||
|
best_score = score
|
||||||
|
return best_action
|
||||||
|
|
||||||
|
func _calculate_score(
|
||||||
|
npc: SimNPC,
|
||||||
|
action_id: StringName,
|
||||||
|
base_priority: float,
|
||||||
|
resource_amount: float,
|
||||||
|
critical_threshold: float,
|
||||||
|
low_threshold: float,
|
||||||
|
critical_bonus: float,
|
||||||
|
low_bonus: float
|
||||||
|
) -> float:
|
||||||
|
var score := base_priority
|
||||||
|
if resource_amount < critical_threshold:
|
||||||
|
score += critical_bonus
|
||||||
|
elif resource_amount < low_threshold:
|
||||||
|
score += low_bonus
|
||||||
|
var definition := SimulationDefinitions.get_action(action_id)
|
||||||
|
if (
|
||||||
|
definition != null
|
||||||
|
and not definition.preferred_profession_id.is_empty()
|
||||||
|
and npc.profession == definition.preferred_profession_id
|
||||||
|
):
|
||||||
|
score += 2.0
|
||||||
|
if action_id == npc.last_task:
|
||||||
|
score -= 1.5
|
||||||
|
score += npc.random_source.randf_range(0.0, 0.5)
|
||||||
|
if npc.debug_logs:
|
||||||
|
print("[ActionSelection] ", npc.npc_name, " score ", action_id, " = ", score)
|
||||||
|
return score
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c550deaitwskn
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
class_name ActionTargetResolver
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
func resolve(
|
||||||
|
npc: SimNPC,
|
||||||
|
origin: Vector3,
|
||||||
|
simulation_manager: Node,
|
||||||
|
active_world_adapter: Node
|
||||||
|
) -> Dictionary:
|
||||||
|
var definition := SimulationDefinitions.get_action(npc.current_task)
|
||||||
|
if definition == null:
|
||||||
|
return {}
|
||||||
|
match definition.target_type:
|
||||||
|
SimulationIds.TARGET_RESOURCE:
|
||||||
|
return _resolve_resource(
|
||||||
|
npc, origin, definition, simulation_manager,
|
||||||
|
active_world_adapter
|
||||||
|
)
|
||||||
|
SimulationIds.TARGET_ACTIVITY:
|
||||||
|
return active_world_adapter.get_activity_target(npc.current_task)
|
||||||
|
SimulationIds.TARGET_FREE:
|
||||||
|
return {
|
||||||
|
"target_id": "",
|
||||||
|
"position": origin + simulation_manager.get_wander_offset(npc.id)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
func _resolve_resource(
|
||||||
|
npc: SimNPC,
|
||||||
|
origin: Vector3,
|
||||||
|
definition: ActionDefinition,
|
||||||
|
simulation_manager: Node,
|
||||||
|
active_world_adapter: Node
|
||||||
|
) -> Dictionary:
|
||||||
|
var best: Dictionary = {}
|
||||||
|
var best_distance := INF
|
||||||
|
for candidate in active_world_adapter.get_resource_candidates(
|
||||||
|
definition.resource_action_id
|
||||||
|
):
|
||||||
|
var node_id := StringName(candidate["target_id"])
|
||||||
|
var state: ResourceStateRecord = simulation_manager.get_resource_state(
|
||||||
|
node_id
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
state == null
|
||||||
|
or not state.can_npc_use()
|
||||||
|
or not state.is_available_for(npc.id)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var position: Vector3 = candidate["position"]
|
||||||
|
var distance := origin.distance_squared_to(position)
|
||||||
|
if distance < best_distance:
|
||||||
|
best_distance = distance
|
||||||
|
best = candidate
|
||||||
|
if best.is_empty():
|
||||||
|
return {}
|
||||||
|
var target_id := StringName(best["target_id"])
|
||||||
|
if not simulation_manager.reserve_resource(target_id, npc.id):
|
||||||
|
return {}
|
||||||
|
return best
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cq8srsm762mnx
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
class FakeActiveWorld:
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var resource_candidates: Array[Dictionary] = []
|
||||||
|
var activity_position := Vector3(4.0, 0.0, 7.0)
|
||||||
|
|
||||||
|
func get_resource_candidates(_action_id: StringName) -> Array[Dictionary]:
|
||||||
|
return resource_candidates
|
||||||
|
|
||||||
|
func get_activity_target(_action_id: StringName) -> Dictionary:
|
||||||
|
return {"target_id": "", "position": activity_position}
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
var travel_requests: Array[Vector3] = []
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_test_selection_and_execution_are_separate()
|
||||||
|
_test_target_resolution_and_travel_are_separate()
|
||||||
|
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Action system boundaries passed")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
func _test_selection_and_execution_are_separate() -> void:
|
||||||
|
var village := SimVillage.new()
|
||||||
|
village.debug_logs = false
|
||||||
|
var npc := SimNPC.new(
|
||||||
|
700,
|
||||||
|
"BoundaryWorker",
|
||||||
|
SimulationIds.PROFESSION_FARMER,
|
||||||
|
5.0,
|
||||||
|
5.0
|
||||||
|
)
|
||||||
|
npc.hunger = 80.0
|
||||||
|
var executor := ActionExecutionSystem.new()
|
||||||
|
executor.advance_npc(npc, village)
|
||||||
|
_check(
|
||||||
|
npc.current_task == SimulationIds.ACTION_IDLE,
|
||||||
|
"Execution should update state without selecting an action"
|
||||||
|
)
|
||||||
|
var selector := ActionSelectionSystem.new()
|
||||||
|
var selection := selector.select_action(npc, village)
|
||||||
|
_check(
|
||||||
|
selection.action_id == SimulationIds.ACTION_EAT,
|
||||||
|
"Selection should independently choose the urgent eat action"
|
||||||
|
)
|
||||||
|
|
||||||
|
func _test_target_resolution_and_travel_are_separate() -> void:
|
||||||
|
var adapter := FakeActiveWorld.new()
|
||||||
|
adapter.resource_candidates = [{
|
||||||
|
"target_id": "boundary_bush",
|
||||||
|
"position": Vector3(3.0, 0.0, 2.0)
|
||||||
|
}]
|
||||||
|
root.add_child(adapter)
|
||||||
|
|
||||||
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
manager.debug_logs = false
|
||||||
|
manager.active_world_adapter = adapter
|
||||||
|
root.add_child(manager)
|
||||||
|
manager.set_process(false)
|
||||||
|
manager.npc_travel_requested.connect(_on_travel_requested)
|
||||||
|
|
||||||
|
var resource_state := ResourceStateRecord.from_dictionary({
|
||||||
|
"schema_version": ResourceStateRecord.SCHEMA_VERSION,
|
||||||
|
"node_id": "boundary_bush",
|
||||||
|
"action_id": String(SimulationIds.ACTION_GATHER_FOOD),
|
||||||
|
"resource_id": String(SimulationIds.RESOURCE_FOOD),
|
||||||
|
"amount_remaining": 5.0,
|
||||||
|
"yield_per_action": 1.0,
|
||||||
|
"reserved_by": -1,
|
||||||
|
"enabled": true,
|
||||||
|
"can_npcs_use": true,
|
||||||
|
"can_player_use": true
|
||||||
|
})
|
||||||
|
manager.resource_states[resource_state.get_node_id()] = resource_state
|
||||||
|
|
||||||
|
var npc: SimNPC = manager.npcs[0]
|
||||||
|
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
|
||||||
|
_check(
|
||||||
|
manager.resolve_npc_target(npc.id, Vector3.ZERO),
|
||||||
|
"Target resolver should resolve a resource from adapter facts"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
npc.target_id == &"boundary_bush"
|
||||||
|
and resource_state.get_reserved_by() == npc.id,
|
||||||
|
"Target resolver should authoritatively assign and reserve the target"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
travel_requests == [Vector3(3.0, 0.0, 2.0)],
|
||||||
|
"Resolution should publish travel without moving a visual itself"
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.release_npc_reservation(npc.id)
|
||||||
|
npc.set_task(SimulationIds.ACTION_STUDY)
|
||||||
|
_check(
|
||||||
|
manager.resolve_npc_target(npc.id, Vector3.ZERO),
|
||||||
|
"Target resolver should resolve an activity through the adapter"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
travel_requests.back() == adapter.activity_position,
|
||||||
|
"Activity resolution should publish the adapter's position"
|
||||||
|
)
|
||||||
|
manager.free()
|
||||||
|
adapter.free()
|
||||||
|
|
||||||
|
func _on_travel_requested(
|
||||||
|
_npc: SimNPC,
|
||||||
|
target_position: Vector3
|
||||||
|
) -> void:
|
||||||
|
travel_requests.append(target_position)
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://brtbafjq4h1n4
|
||||||
@@ -61,8 +61,9 @@ func _run() -> void:
|
|||||||
)
|
)
|
||||||
worker.set_task(SimulationIds.ACTION_STUDY, 2.0)
|
worker.set_task(SimulationIds.ACTION_STUDY, 2.0)
|
||||||
worker.start_working()
|
worker.start_working()
|
||||||
worker.simulate_tick(village)
|
var executor := ActionExecutionSystem.new()
|
||||||
worker.simulate_tick(village)
|
executor.advance_npc(worker, village)
|
||||||
|
executor.advance_npc(worker, village)
|
||||||
_check(worker.task_complete, "A two-tick working task should complete")
|
_check(worker.task_complete, "A two-tick working task should complete")
|
||||||
|
|
||||||
var starving := SimNPC.new(
|
var starving := SimNPC.new(
|
||||||
@@ -74,7 +75,7 @@ func _run() -> void:
|
|||||||
)
|
)
|
||||||
starving.hunger = 100.0
|
starving.hunger = 100.0
|
||||||
starving.starvation_death_threshold = 1
|
starving.starvation_death_threshold = 1
|
||||||
starving.simulate_tick(village)
|
executor.advance_npc(starving, village)
|
||||||
_check(starving.is_dead, "Starvation threshold should still kill an NPC")
|
_check(starving.is_dead, "Starvation threshold should still kill an NPC")
|
||||||
|
|
||||||
if failures.is_empty():
|
if failures.is_empty():
|
||||||
|
|||||||
@@ -87,26 +87,30 @@ func _run() -> void:
|
|||||||
"Jajce navigation path should exist from %s to %s" % [origin, destination]
|
"Jajce navigation path should exist from %s to %s" % [origin, destination]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var adapter := ActiveWorldAdapter.new()
|
||||||
|
root.add_child(adapter)
|
||||||
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
manager.debug_logs = false
|
manager.debug_logs = false
|
||||||
|
manager.active_world_adapter = adapter
|
||||||
root.add_child(manager)
|
root.add_child(manager)
|
||||||
manager.set_process(false)
|
manager.set_process(false)
|
||||||
await process_frame
|
await process_frame
|
||||||
var selected: ResourceNode = manager.acquire_resource_target(
|
var npc: SimNPC = manager.npcs[0]
|
||||||
SimulationIds.ACTION_GATHER_FOOD,
|
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
|
||||||
777,
|
_check(
|
||||||
Vector3.ZERO
|
manager.resolve_npc_target(npc.id, Vector3.ZERO),
|
||||||
|
"Target resolver should find Jajce resources without a parent path"
|
||||||
)
|
)
|
||||||
_check(selected != null, "Target discovery should find Jajce resources without a parent path")
|
var selected := ResourceNode.get_by_id(npc.target_id)
|
||||||
if selected != null:
|
if selected != null:
|
||||||
var selected_state: ResourceStateRecord = manager.get_resource_state(
|
var selected_state: ResourceStateRecord = manager.get_resource_state(
|
||||||
selected.node_id
|
selected.node_id
|
||||||
)
|
)
|
||||||
_check(
|
_check(
|
||||||
selected_state.get_reserved_by() == 777,
|
selected_state.get_reserved_by() == npc.id,
|
||||||
"Selected Jajce resource should be authoritatively reserved"
|
"Selected Jajce resource should be authoritatively reserved"
|
||||||
)
|
)
|
||||||
manager.release_resource(selected.node_id, 777)
|
manager.release_resource(selected.node_id, npc.id)
|
||||||
|
|
||||||
if failures.is_empty():
|
if failures.is_empty():
|
||||||
print("[TEST] Jajce scaffold passed: Terrain3D, stable IDs, 24 navigation routes")
|
print("[TEST] Jajce scaffold passed: Terrain3D, stable IDs, 24 navigation routes")
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
class_name ActiveWorldAdapter
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
@export var guard_marker: Marker3D
|
||||||
|
@export var study_marker: Marker3D
|
||||||
|
@export var rest_marker: Marker3D
|
||||||
|
@export var food_marker: Marker3D
|
||||||
|
|
||||||
|
func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
|
||||||
|
var candidates: Array[Dictionary] = []
|
||||||
|
for node in ResourceNode.get_all():
|
||||||
|
if node.action_id != action_id or node.interaction_point == null:
|
||||||
|
continue
|
||||||
|
candidates.append({
|
||||||
|
"target_id": String(node.node_id),
|
||||||
|
"position": node.interaction_point.global_position
|
||||||
|
})
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
func get_activity_target(action_id: StringName) -> Dictionary:
|
||||||
|
var marker: Marker3D
|
||||||
|
match action_id:
|
||||||
|
SimulationIds.ACTION_PATROL:
|
||||||
|
marker = guard_marker
|
||||||
|
SimulationIds.ACTION_STUDY:
|
||||||
|
marker = study_marker
|
||||||
|
SimulationIds.ACTION_REST:
|
||||||
|
marker = rest_marker
|
||||||
|
SimulationIds.ACTION_EAT:
|
||||||
|
marker = food_marker
|
||||||
|
if marker == null:
|
||||||
|
return {}
|
||||||
|
return {"target_id": "", "position": marker.global_position}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bbwol3mrjgn2x
|
||||||
+30
-162
@@ -6,11 +6,6 @@ const DEBUG_LOGS := true
|
|||||||
@export var simulation_manager: Node
|
@export var simulation_manager: Node
|
||||||
@export var active_npcs_parent: Node3D
|
@export var active_npcs_parent: Node3D
|
||||||
|
|
||||||
@export var guard_zone: Marker3D
|
|
||||||
@export var study_zone: Marker3D
|
|
||||||
@export var rest_zone: Marker3D
|
|
||||||
@export var food_zone: Marker3D
|
|
||||||
|
|
||||||
var active_npc_visuals := {}
|
var active_npc_visuals := {}
|
||||||
|
|
||||||
func debug_log(message: String) -> void:
|
func debug_log(message: String) -> void:
|
||||||
@@ -23,227 +18,100 @@ func _ready() -> void:
|
|||||||
func initialize_world_view() -> void:
|
func initialize_world_view() -> void:
|
||||||
await get_tree().physics_frame
|
await get_tree().physics_frame
|
||||||
spawn_initial_npcs()
|
spawn_initial_npcs()
|
||||||
|
if simulation_manager.has_signal("npc_target_requested"):
|
||||||
if simulation_manager.has_signal("npc_task_changed"):
|
simulation_manager.npc_target_requested.connect(
|
||||||
simulation_manager.npc_task_changed.connect(_on_npc_task_changed)
|
_on_npc_target_requested
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: SimulationManager has no npc_task_changed signal")
|
push_error("WorldViewManager: missing npc_target_requested signal")
|
||||||
|
if simulation_manager.has_signal("npc_travel_requested"):
|
||||||
|
simulation_manager.npc_travel_requested.connect(
|
||||||
|
_on_npc_travel_requested
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
push_error("WorldViewManager: missing npc_travel_requested signal")
|
||||||
if simulation_manager.has_signal("npc_died"):
|
if simulation_manager.has_signal("npc_died"):
|
||||||
simulation_manager.npc_died.connect(_on_npc_died)
|
simulation_manager.npc_died.connect(_on_npc_died)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: SimulationManager has no npc_died signal")
|
push_error("WorldViewManager: SimulationManager has no npc_died signal")
|
||||||
|
request_current_targets()
|
||||||
update_npc_targets()
|
|
||||||
|
|
||||||
func spawn_initial_npcs() -> void:
|
func spawn_initial_npcs() -> void:
|
||||||
if simulation_manager == null:
|
if simulation_manager == null:
|
||||||
push_error("WorldViewManager: simulation_manager is missing")
|
push_error("WorldViewManager: simulation_manager is missing")
|
||||||
return
|
return
|
||||||
|
|
||||||
if npc_visual_scene == null:
|
if npc_visual_scene == null:
|
||||||
push_error("WorldViewManager: npc_visual_scene is missing")
|
push_error("WorldViewManager: npc_visual_scene is missing")
|
||||||
return
|
return
|
||||||
|
|
||||||
if active_npcs_parent == null:
|
if active_npcs_parent == null:
|
||||||
push_error("WorldViewManager: active_npcs_parent is missing")
|
push_error("WorldViewManager: active_npcs_parent is missing")
|
||||||
return
|
return
|
||||||
|
|
||||||
debug_log("Spawning NPC visuals. Count: %s" % simulation_manager.npcs.size())
|
debug_log("Spawning NPC visuals. Count: %s" % simulation_manager.npcs.size())
|
||||||
|
|
||||||
for npc in simulation_manager.npcs:
|
for npc in simulation_manager.npcs:
|
||||||
spawn_npc_visual(npc)
|
spawn_npc_visual(npc)
|
||||||
|
|
||||||
func spawn_npc_visual(npc: SimNPC) -> void:
|
func spawn_npc_visual(npc: SimNPC) -> void:
|
||||||
if active_npc_visuals.has(npc.id):
|
if active_npc_visuals.has(npc.id):
|
||||||
return
|
return
|
||||||
|
|
||||||
var visual = npc_visual_scene.instantiate()
|
var visual = npc_visual_scene.instantiate()
|
||||||
active_npcs_parent.add_child(visual)
|
active_npcs_parent.add_child(visual)
|
||||||
|
|
||||||
visual.global_position = npc.position
|
visual.global_position = npc.position
|
||||||
visual.setup_from_sim(npc)
|
visual.setup_from_sim(npc)
|
||||||
|
|
||||||
if visual.has_signal("arrived_at_target"):
|
if visual.has_signal("arrived_at_target"):
|
||||||
visual.arrived_at_target.connect(_on_npc_visual_arrived)
|
visual.arrived_at_target.connect(_on_npc_visual_arrived)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: NPCVisual has no arrived_at_target signal")
|
push_error("WorldViewManager: NPCVisual has no arrived_at_target signal")
|
||||||
|
|
||||||
if visual.has_signal("navigation_failed"):
|
if visual.has_signal("navigation_failed"):
|
||||||
visual.navigation_failed.connect(_on_npc_visual_navigation_failed)
|
visual.navigation_failed.connect(_on_npc_visual_navigation_failed)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: NPCVisual has no navigation_failed signal")
|
push_error("WorldViewManager: NPCVisual has no navigation_failed signal")
|
||||||
|
|
||||||
active_npc_visuals[npc.id] = visual
|
active_npc_visuals[npc.id] = visual
|
||||||
debug_log("Spawned visual for %s at %s" % [npc.npc_name, npc.position])
|
debug_log("Spawned visual for %s at %s" % [npc.npc_name, npc.position])
|
||||||
|
|
||||||
func update_npc_targets() -> void:
|
func request_current_targets() -> void:
|
||||||
if simulation_manager == null:
|
|
||||||
return
|
|
||||||
|
|
||||||
for npc in simulation_manager.npcs:
|
for npc in simulation_manager.npcs:
|
||||||
if not active_npc_visuals.has(npc.id):
|
if npc.task_state != SimNPC.TASK_STATE_TRAVELING:
|
||||||
continue
|
continue
|
||||||
|
_on_npc_target_requested(npc)
|
||||||
|
|
||||||
var visual = active_npc_visuals[npc.id]
|
func _on_npc_target_requested(npc: SimNPC) -> void:
|
||||||
var task = npc.current_task
|
|
||||||
if task in [
|
|
||||||
&"",
|
|
||||||
SimulationIds.ACTION_IDLE,
|
|
||||||
SimulationIds.ACTION_DEAD
|
|
||||||
]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
var resource_pos = _try_get_resource_target(npc, npc.current_task)
|
|
||||||
if resource_pos != null:
|
|
||||||
visual.set_target_position(resource_pos)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if task == SimulationIds.ACTION_WANDER:
|
|
||||||
var offset := _get_wander_offset(npc.id)
|
|
||||||
visual.set_target_position(visual.global_position + offset)
|
|
||||||
continue
|
|
||||||
|
|
||||||
var definition := SimulationDefinitions.get_action(task)
|
|
||||||
if (
|
|
||||||
definition != null
|
|
||||||
and definition.target_type == SimulationIds.TARGET_RESOURCE
|
|
||||||
):
|
|
||||||
if simulation_manager.has_method("notify_npc_target_unavailable"):
|
|
||||||
simulation_manager.notify_npc_target_unavailable(npc.id)
|
|
||||||
continue
|
|
||||||
|
|
||||||
var target = get_target_for_task(task)
|
|
||||||
|
|
||||||
if target == null:
|
|
||||||
debug_log("No target for task: %s" % task)
|
|
||||||
continue
|
|
||||||
|
|
||||||
visual.set_target_position(target.global_position)
|
|
||||||
|
|
||||||
func get_target_for_task(task: StringName) -> Marker3D:
|
|
||||||
match task:
|
|
||||||
SimulationIds.ACTION_PATROL:
|
|
||||||
return guard_zone
|
|
||||||
SimulationIds.ACTION_STUDY:
|
|
||||||
return study_zone
|
|
||||||
SimulationIds.ACTION_REST:
|
|
||||||
return rest_zone
|
|
||||||
SimulationIds.ACTION_EAT:
|
|
||||||
return food_zone
|
|
||||||
_:
|
|
||||||
return rest_zone
|
|
||||||
|
|
||||||
func _try_get_resource_target(npc: SimNPC, task: StringName) -> Variant:
|
|
||||||
var definition := SimulationDefinitions.get_action(task)
|
|
||||||
if (
|
|
||||||
definition == null
|
|
||||||
or definition.target_type != SimulationIds.TARGET_RESOURCE
|
|
||||||
):
|
|
||||||
return null
|
|
||||||
|
|
||||||
var visual = active_npc_visuals.get(npc.id) as Node3D
|
var visual = active_npc_visuals.get(npc.id) as Node3D
|
||||||
if visual == null:
|
if visual == null:
|
||||||
return null
|
return
|
||||||
|
if not simulation_manager.resolve_npc_target(
|
||||||
if not simulation_manager.has_method("acquire_resource_target"):
|
|
||||||
push_error("WorldViewManager: SimulationManager cannot acquire resources")
|
|
||||||
return null
|
|
||||||
var node: ResourceNode = simulation_manager.acquire_resource_target(
|
|
||||||
definition.resource_action_id,
|
|
||||||
npc.id,
|
npc.id,
|
||||||
visual.global_position
|
visual.global_position
|
||||||
)
|
|
||||||
if node == null:
|
|
||||||
debug_log("No available resource node for %s" % task)
|
|
||||||
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: StringName,
|
|
||||||
new_task: StringName
|
|
||||||
) -> void:
|
|
||||||
if DEBUG_LOGS:
|
|
||||||
debug_log("%s changed task: %s -> %s" % [npc.npc_name, old_task, new_task])
|
|
||||||
|
|
||||||
if not active_npc_visuals.has(npc.id):
|
|
||||||
return
|
|
||||||
|
|
||||||
if new_task in [
|
|
||||||
&"",
|
|
||||||
SimulationIds.ACTION_IDLE,
|
|
||||||
SimulationIds.ACTION_DEAD
|
|
||||||
]:
|
|
||||||
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
|
|
||||||
|
|
||||||
if new_task == SimulationIds.ACTION_WANDER:
|
|
||||||
var offset := _get_wander_offset(npc.id)
|
|
||||||
visual.set_target_position(visual.global_position + offset)
|
|
||||||
return
|
|
||||||
|
|
||||||
var definition := SimulationDefinitions.get_action(new_task)
|
|
||||||
if (
|
|
||||||
definition != null
|
|
||||||
and definition.target_type == SimulationIds.TARGET_RESOURCE
|
|
||||||
):
|
):
|
||||||
if simulation_manager.has_method("notify_npc_target_unavailable"):
|
debug_log("Target resolution failed for %s" % npc.npc_name)
|
||||||
simulation_manager.notify_npc_target_unavailable(npc.id)
|
|
||||||
else:
|
func _on_npc_travel_requested(
|
||||||
push_error("WorldViewManager: SimulationManager cannot handle unavailable targets")
|
npc: SimNPC,
|
||||||
|
target_position: Vector3
|
||||||
|
) -> void:
|
||||||
|
var visual = active_npc_visuals.get(npc.id)
|
||||||
|
if visual == null:
|
||||||
return
|
return
|
||||||
|
visual.set_target_position(target_position)
|
||||||
var target := get_target_for_task(new_task)
|
|
||||||
|
|
||||||
if target != null:
|
|
||||||
visual.set_target_position(target.global_position)
|
|
||||||
|
|
||||||
func _get_wander_offset(npc_id: int) -> Vector3:
|
|
||||||
if simulation_manager != null and simulation_manager.has_method("get_wander_offset"):
|
|
||||||
return simulation_manager.get_wander_offset(npc_id)
|
|
||||||
push_error("WorldViewManager: SimulationManager cannot provide deterministic wander offsets")
|
|
||||||
return Vector3.ZERO
|
|
||||||
|
|
||||||
func _on_npc_visual_arrived(sim_id: int) -> void:
|
func _on_npc_visual_arrived(sim_id: int) -> void:
|
||||||
debug_log("NPC visual arrived. sim_id=%s" % sim_id)
|
debug_log("NPC visual arrived. sim_id=%s" % sim_id)
|
||||||
|
|
||||||
if simulation_manager == null:
|
if simulation_manager == null:
|
||||||
push_error("WorldViewManager: simulation_manager missing during arrival report")
|
push_error("WorldViewManager: simulation_manager missing during arrival")
|
||||||
return
|
return
|
||||||
|
|
||||||
if simulation_manager.has_method("notify_npc_arrived"):
|
|
||||||
simulation_manager.notify_npc_arrived(sim_id)
|
simulation_manager.notify_npc_arrived(sim_id)
|
||||||
else:
|
|
||||||
push_error("WorldViewManager: SimulationManager has no notify_npc_arrived method")
|
|
||||||
|
|
||||||
func _on_npc_visual_navigation_failed(sim_id: int) -> void:
|
func _on_npc_visual_navigation_failed(sim_id: int) -> void:
|
||||||
debug_log("NPC visual navigation failed. sim_id=%s" % sim_id)
|
debug_log("NPC visual navigation failed. sim_id=%s" % sim_id)
|
||||||
|
|
||||||
if simulation_manager == null:
|
if simulation_manager == null:
|
||||||
push_error("WorldViewManager: simulation_manager missing during navigation failure")
|
push_error("WorldViewManager: simulation_manager missing during failure")
|
||||||
return
|
return
|
||||||
|
|
||||||
if simulation_manager.has_method("notify_npc_navigation_failed"):
|
|
||||||
simulation_manager.notify_npc_navigation_failed(sim_id)
|
simulation_manager.notify_npc_navigation_failed(sim_id)
|
||||||
else:
|
|
||||||
push_error("WorldViewManager: SimulationManager has no notify_npc_navigation_failed method")
|
|
||||||
|
|
||||||
func _on_npc_died(npc: SimNPC) -> void:
|
func _on_npc_died(npc: SimNPC) -> void:
|
||||||
if not active_npc_visuals.has(npc.id):
|
if not active_npc_visuals.has(npc.id):
|
||||||
return
|
return
|
||||||
|
|
||||||
var visual = active_npc_visuals[npc.id]
|
var visual = active_npc_visuals[npc.id]
|
||||||
|
|
||||||
if visual.has_method("apply_dead_visual_state"):
|
if visual.has_method("apply_dead_visual_state"):
|
||||||
visual.apply_dead_visual_state()
|
visual.apply_dead_visual_state()
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: NPCVisual has no apply_dead_visual_state method")
|
push_error("WorldViewManager: NPCVisual has no apply_dead_visual_state")
|
||||||
|
|||||||
Reference in New Issue
Block a user