class_name ActionSelectionSystem extends RefCounted enum SchedulePeriod { WORK, SLEEP, MEAL, DISCRETIONARY } const SLEEP_BEGIN := 0.88 const SLEEP_END := 0.15 const BREAKFAST_BEGIN := 0.25 const BREAKFAST_END := 0.35 const DINNER_BEGIN := 0.7 const DINNER_END := 0.8 const WORK_BEGIN := 0.28 const WORK_END := 0.85 func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5) -> ActionSelectionResult: if npc.is_dead: return null var period := _get_period(time_of_day) if period == SchedulePeriod.SLEEP: if npc.energy < 60.0: return ActionSelectionResult.new( SimulationIds.ACTION_SLEEP, -1.0, "Night-time sleep; energy is low" ) if npc.hunger > 75.0 and npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0: return ActionSelectionResult.new( SimulationIds.ACTION_EAT, -1.0, "Night-time hunger; eating from inventory" ) return ActionSelectionResult.new( SimulationIds.ACTION_SLEEP, -1.0, "Night-time; going home" ) if period == SchedulePeriod.MEAL: if npc.hunger > 50.0: if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0: return ActionSelectionResult.new( SimulationIds.ACTION_EAT, -1.0, "Meal-time hunger; eating from inventory" ) if village.food > 0: return ActionSelectionResult.new( SimulationIds.ACTION_WITHDRAW_FOOD, -1.0, "Meal-time; pantry has food" ) return ActionSelectionResult.new( SimulationIds.ACTION_GATHER_FOOD, -1.0, "Meal-time hunger; pantry is empty" ) if npc.is_starving: if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0: return ActionSelectionResult.new( SimulationIds.ACTION_EAT, 1.0, "Starving and carrying food" ) if village.food > 0: return ActionSelectionResult.new( SimulationIds.ACTION_WITHDRAW_FOOD, 1.0, "Starving; pantry has food" ) return ActionSelectionResult.new( SimulationIds.ACTION_GATHER_FOOD, 4.0, "Starving; pantry is empty" ) if npc.hunger > 75.0: if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0: return ActionSelectionResult.new( SimulationIds.ACTION_EAT, -1.0, "Hungry and carrying food" ) if village.food > 0: return ActionSelectionResult.new( SimulationIds.ACTION_WITHDRAW_FOOD, -1.0, "Hungry; pantry has food" ) return ActionSelectionResult.new( SimulationIds.ACTION_GATHER_FOOD, -1.0, "Hungry; pantry is empty" ) if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0: return ActionSelectionResult.new( SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage" ) if npc.energy < 25.0: return ActionSelectionResult.new( SimulationIds.ACTION_REST, -1.0, "Energy is below the rest threshold" ) if period == SchedulePeriod.DISCRETIONARY: var roll := npc.random_source.randf() if roll < 0.3: return ActionSelectionResult.new( SimulationIds.ACTION_WANDER, -1.0, "Discretionary wander" ) return _choose_best_work_action(npc, village) func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult: 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 ActionSelectionResult.new( best_action, -1.0, "Highest current work utility", scores ) 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 static func _get_period(time_of_day: float) -> int: if _in_wrap_range(time_of_day, SLEEP_BEGIN, SLEEP_END): return SchedulePeriod.SLEEP if _in_range(time_of_day, BREAKFAST_BEGIN, BREAKFAST_END): return SchedulePeriod.MEAL if _in_range(time_of_day, DINNER_BEGIN, DINNER_END): return SchedulePeriod.MEAL if _in_range(time_of_day, WORK_BEGIN, WORK_END): return SchedulePeriod.WORK return SchedulePeriod.DISCRETIONARY static func _in_range(value: float, begin: float, end: float) -> bool: return value >= begin and value <= end static func _in_wrap_range(value: float, begin: float, end: float) -> bool: return value >= begin or value <= end