refactor: separate action system responsibilities

This commit is contained in:
2026-07-05 14:17:45 +02:00
parent 81df7b5939
commit 4673f0698d
18 changed files with 458 additions and 383 deletions
@@ -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