class_name SimNPC extends RefCounted const TASK_STATE_IDLE := &"idle" const TASK_STATE_TRAVELING := &"traveling" const TASK_STATE_WORKING := &"working" const TASK_STATE_COMPLETE := &"complete" var id: int var npc_name: String var profession: StringName var hunger: float var energy: float var strength: float var intelligence: float var current_task: StringName = SimulationIds.ACTION_IDLE var task_state: StringName = TASK_STATE_IDLE var position: Vector3 var is_starving := false var starvation_ticks := 0 var starvation_death_threshold := 20 var is_dead := false var task_duration := 0.0 var task_progress := 0.0 var task_complete := true var target_id: StringName = &"" var last_task: StringName var random_source: RandomNumberGenerator var debug_logs := true func _init( _id: int, _npc_name: String, _profession: StringName, _strength: float, _intelligence: float, _random_source: RandomNumberGenerator = null ) -> void: id = _id npc_name = _npc_name profession = _profession strength = _strength intelligence = _intelligence random_source = _random_source if random_source == null: random_source = RandomNumberGenerator.new() random_source.seed = id + 1 hunger = random_source.randf_range(20.0, 80.0) energy = random_source.randf_range(40.0, 100.0) position = Vector3( random_source.randf_range(-8.0, 8.0), 0.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: is_dead = true current_task = SimulationIds.ACTION_DEAD task_state = TASK_STATE_IDLE task_progress = 0.0 task_duration = 0.0 task_complete = true if debug_logs: 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: return ( current_task == SimulationIds.ACTION_GATHER_FOOD and is_starving 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: var definition := SimulationDefinitions.get_action(action_id) if definition == null: push_error("SimNPC: unknown action_id '%s'" % action_id) return current_task = action_id task_duration = ( duration if duration >= 0.0 else definition.default_duration ) task_progress = 0.0 task_complete = false task_state = TASK_STATE_TRAVELING func start_working() -> void: if task_state != TASK_STATE_TRAVELING: return task_state = TASK_STATE_WORKING task_progress = 0.0 task_complete = false