From b5e2493c2a31cebf02abe17caa84b5c37d940d0e Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Sun, 21 Jun 2026 23:37:58 +0200 Subject: [PATCH] feat: add proper death from starvation and survival tasks urgency --- player/npc/NpcVisual.gd | 28 +++++++++++++ project.godot | 5 +++ simulation/SimNPC.gd | 73 +++++++++++++++++++++++++++------ simulation/SimVillage.gd | 24 +++++++++-- simulation/SimulationManager.gd | 38 +++++++++++++++-- world/world_view_manager.gd | 17 ++++++++ 6 files changed, 165 insertions(+), 20 deletions(-) diff --git a/player/npc/NpcVisual.gd b/player/npc/NpcVisual.gd index 4b22fb7..0aac532 100644 --- a/player/npc/NpcVisual.gd +++ b/player/npc/NpcVisual.gd @@ -20,10 +20,16 @@ var path_index := 0 var current_target := Vector3.INF var arrival_distance := 0.6 +var is_dead_visual := false +var dead_material := StandardMaterial3D.new() + func debug_log(message: String) -> void: if DEBUG_LOGS: print("[NPCVisual] ", name, " | ", message) +func _ready() -> void: + dead_material.albedo_color = Color(0.25, 0.25, 0.25, 1.0) + func setup_from_sim(npc: SimNPC) -> void: sim_id = npc.id npc_name = npc.npc_name @@ -59,6 +65,11 @@ func set_target_position(pos: Vector3) -> void: debug_log("New target: %s Path points: %s" % [pos, current_path.size()]) func _physics_process(delta: float) -> void: + if is_dead_visual: + velocity = Vector3.ZERO + move_and_slide() + return + if current_target == Vector3.INF: velocity = Vector3.ZERO move_and_slide() @@ -104,3 +115,20 @@ func _report_arrival_once() -> void: debug_log("Arrived at target") arrived_at_target.emit(sim_id) + +func apply_dead_visual_state() -> void: + is_dead_visual = true + velocity = Vector3.ZERO + current_path = PackedVector3Array() + path_index = 0 + current_target = Vector3.INF + has_reported_arrival = true + + scale = Vector3(0.8, 0.25, 1.2) + rotation_degrees.x = 90.0 + + for child in get_children(): + if child is MeshInstance3D: + child.material_override = dead_material + + debug_log("Applied dead visual state") diff --git a/project.godot b/project.godot index 3a4c829..80bab55 100644 --- a/project.godot +++ b/project.godot @@ -15,6 +15,11 @@ run/main_scene="uid://rs3hv73svbpa" config/features=PackedStringArray("4.5", "Forward Plus") config/icon="res://icon.svg" +[display] + +window/size/viewport_width=1920 +window/size/viewport_height=1020 + [input] move_forward={ diff --git a/simulation/SimNPC.gd b/simulation/SimNPC.gd index 84d04a7..e680364 100644 --- a/simulation/SimNPC.gd +++ b/simulation/SimNPC.gd @@ -1,7 +1,7 @@ class_name SimNPC extends RefCounted -@export var DEBUG_LOGS := true +const DEBUG_LOGS := true const TASK_STATE_IDLE := "idle" const TASK_STATE_TRAVELING := "traveling" @@ -20,6 +20,9 @@ var task_state: String = 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 @@ -42,18 +45,13 @@ func _init( position = Vector3(randf_range(-8.0, 8.0), 0.0, randf_range(-8.0, 8.0)) func simulate_tick(village: SimVillage) -> void: - hunger += 5.0 * village.food_modifier - energy -= 2.0 + if is_dead: + return - hunger = clamp(hunger, 0.0, 100.0) - energy = clamp(energy, 0.0, 100.0) + update_needs(village) - is_starving = hunger >= 90.0 - - if is_starving: - energy -= 8.0 - - energy = clamp(energy, 0.0, 100.0) + if is_dead: + return if task_state == TASK_STATE_IDLE or task_state == TASK_STATE_COMPLETE: choose_new_task(village) @@ -68,8 +66,56 @@ func simulate_tick(village: SimVillage) -> void: 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 != "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 = "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("eat", 1.0) + else: + set_task("gather_food", 4.0) + return + if hunger > 75.0: if village.food > 0: set_task("eat", 2.0) @@ -77,7 +123,7 @@ func choose_new_task(village: SimVillage) -> void: set_task("gather_food", 5.0) return - if energy < 30.0: + if energy < 25.0: set_task("rest", 4.0) return @@ -94,6 +140,9 @@ func choose_new_task(village: SimVillage) -> void: set_task("study", 6.0) _: set_task("wander", 4.0) + +func should_eat_immediately_after_task() -> bool: + return current_task == "gather_food" and is_starving and not is_dead func choose_best_work_task(village: SimVillage) -> String: var food_score := calculate_task_score( diff --git a/simulation/SimVillage.gd b/simulation/SimVillage.gd index 360bfd4..5bf64f4 100644 --- a/simulation/SimVillage.gd +++ b/simulation/SimVillage.gd @@ -70,9 +70,14 @@ func update_priorities() -> void: print(get_priority_summary()) func apply_npc_task(npc: SimNPC) -> void: + if npc.is_dead: + return + var productivity := 1.0 - if npc.energy < 20.0: + if npc.is_starving: + productivity = 0.35 + elif npc.energy < 20.0: productivity = 0.25 elif npc.energy < 40.0: productivity = 0.5 @@ -87,10 +92,21 @@ func apply_npc_task(npc: SimNPC) -> void: "study": knowledge += 1.0 * productivity "eat": - food -= 1.0 - npc.hunger = max(npc.hunger - 35.0, 0.0) + if food > 0: + food -= 1.0 + npc.hunger = max(npc.hunger - 55.0, 0.0) + npc.starvation_ticks = 0 + npc.is_starving = npc.hunger >= 90.0 + else: + npc.hunger = min(npc.hunger + 5.0, 100.0) + npc.is_starving = npc.hunger >= 90.0 "rest": - npc.energy = min(npc.energy + 35.0, 100.0) + if npc.is_starving: + npc.energy = min(npc.energy + 2.0, 100.0) + elif npc.hunger > 75.0: + npc.energy = min(npc.energy + 12.0, 100.0) + else: + npc.energy = min(npc.energy + 35.0, 100.0) "wander": safety -= 0.2 diff --git a/simulation/SimulationManager.gd b/simulation/SimulationManager.gd index 7b6cf84..06ca920 100644 --- a/simulation/SimulationManager.gd +++ b/simulation/SimulationManager.gd @@ -2,6 +2,7 @@ extends Node signal npc_task_changed(npc: SimNPC, old_task: String, new_task: String) signal village_changed(village: SimVillage) +signal npc_died(npc: SimNPC) var village := SimVillage.new() @@ -26,9 +27,11 @@ var professions := [ func _ready() -> void: randomize() + village.update_modifiers() + village.update_priorities() generate_npcs() print("--- Simulation started ---") - + func _process(delta: float) -> void: tick_timer += delta @@ -58,13 +61,24 @@ func simulate_tick() -> void: var old_task := npc.current_task var old_state := npc.task_state var was_complete := npc.task_complete + var was_dead := npc.is_dead npc.simulate_tick(village) - if old_task != npc.current_task: + if not was_dead and npc.is_dead: + npc_died.emit(npc) npc_task_changed.emit(npc, old_task, npc.current_task) - if not was_complete and npc.task_complete: + if DEBUG_LOGS: + print("[SimulationManager] NPC died: ", npc.npc_name) + + if old_task != npc.current_task and not npc.is_dead: + npc_task_changed.emit(npc, old_task, npc.current_task) + + if not was_complete and npc.task_complete and not npc.is_dead: + var completed_task := npc.current_task + var needs_immediate_food_after_gather := npc.should_eat_immediately_after_task() + village.apply_npc_task(npc) village_was_changed = true @@ -72,6 +86,13 @@ func simulate_tick() -> void: npc.task_state = SimNPC.TASK_STATE_IDLE npc.current_task = "idle" + if needs_immediate_food_after_gather and village.food > 0: + npc.set_task("eat", 1.0) + npc_task_changed.emit(npc, completed_task, npc.current_task) + + if DEBUG_LOGS: + print("[SimulationManager] ", npc.npc_name, " gathered food while starving and is going to eat immediately.") + if DEBUG_LOGS: print( npc.npc_name, @@ -90,7 +111,13 @@ func simulate_tick() -> void: " | hunger: ", round(npc.hunger), " | energy: ", - round(npc.energy) + round(npc.energy), + " | starving: ", + npc.is_starving, + " | starvation_ticks: ", + npc.starvation_ticks, + " | dead: ", + npc.is_dead ) if old_state != npc.task_state: @@ -105,6 +132,9 @@ func simulate_tick() -> void: func notify_npc_arrived(npc_id: int) -> void: for npc in npcs: if npc.id == npc_id: + if npc.is_dead: + return + if npc.task_state == SimNPC.TASK_STATE_TRAVELING: npc.start_working() diff --git a/world/world_view_manager.gd b/world/world_view_manager.gd index fce741a..b0ac94d 100644 --- a/world/world_view_manager.gd +++ b/world/world_view_manager.gd @@ -30,6 +30,11 @@ func initialize_world_view() -> void: simulation_manager.npc_task_changed.connect(_on_npc_task_changed) else: push_error("WorldViewManager: SimulationManager has no npc_task_changed signal") + + if simulation_manager.has_signal("npc_died"): + simulation_manager.npc_died.connect(_on_npc_died) + else: + push_error("WorldViewManager: SimulationManager has no npc_died signal") update_npc_targets() @@ -127,3 +132,15 @@ func _on_npc_visual_arrived(sim_id: int) -> void: simulation_manager.notify_npc_arrived(sim_id) else: push_error("WorldViewManager: SimulationManager has no notify_npc_arrived method") + + +func _on_npc_died(npc: SimNPC) -> void: + if not active_npc_visuals.has(npc.id): + return + + var visual = active_npc_visuals[npc.id] + + if visual.has_method("apply_dead_visual_state"): + visual.apply_dead_visual_state() + else: + push_error("WorldViewManager: NPCVisual has no apply_dead_visual_state method")