feat: add proper death from starvation and survival tasks urgency
This commit is contained in:
@@ -20,10 +20,16 @@ var path_index := 0
|
|||||||
var current_target := Vector3.INF
|
var current_target := Vector3.INF
|
||||||
var arrival_distance := 0.6
|
var arrival_distance := 0.6
|
||||||
|
|
||||||
|
var is_dead_visual := false
|
||||||
|
var dead_material := StandardMaterial3D.new()
|
||||||
|
|
||||||
func debug_log(message: String) -> void:
|
func debug_log(message: String) -> void:
|
||||||
if DEBUG_LOGS:
|
if DEBUG_LOGS:
|
||||||
print("[NPCVisual] ", name, " | ", message)
|
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:
|
func setup_from_sim(npc: SimNPC) -> void:
|
||||||
sim_id = npc.id
|
sim_id = npc.id
|
||||||
npc_name = npc.npc_name
|
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()])
|
debug_log("New target: %s Path points: %s" % [pos, current_path.size()])
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
|
if is_dead_visual:
|
||||||
|
velocity = Vector3.ZERO
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
|
||||||
if current_target == Vector3.INF:
|
if current_target == Vector3.INF:
|
||||||
velocity = Vector3.ZERO
|
velocity = Vector3.ZERO
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
@@ -104,3 +115,20 @@ func _report_arrival_once() -> void:
|
|||||||
|
|
||||||
debug_log("Arrived at target")
|
debug_log("Arrived at target")
|
||||||
arrived_at_target.emit(sim_id)
|
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")
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ run/main_scene="uid://rs3hv73svbpa"
|
|||||||
config/features=PackedStringArray("4.5", "Forward Plus")
|
config/features=PackedStringArray("4.5", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=1920
|
||||||
|
window/size/viewport_height=1020
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
move_forward={
|
move_forward={
|
||||||
|
|||||||
+61
-12
@@ -1,7 +1,7 @@
|
|||||||
class_name SimNPC
|
class_name SimNPC
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
@export var DEBUG_LOGS := true
|
const DEBUG_LOGS := true
|
||||||
|
|
||||||
const TASK_STATE_IDLE := "idle"
|
const TASK_STATE_IDLE := "idle"
|
||||||
const TASK_STATE_TRAVELING := "traveling"
|
const TASK_STATE_TRAVELING := "traveling"
|
||||||
@@ -20,6 +20,9 @@ var task_state: String = TASK_STATE_IDLE
|
|||||||
var position: Vector3
|
var position: Vector3
|
||||||
|
|
||||||
var is_starving := false
|
var is_starving := false
|
||||||
|
var starvation_ticks := 0
|
||||||
|
var starvation_death_threshold := 20
|
||||||
|
var is_dead := false
|
||||||
|
|
||||||
var task_duration := 0.0
|
var task_duration := 0.0
|
||||||
var task_progress := 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))
|
position = Vector3(randf_range(-8.0, 8.0), 0.0, randf_range(-8.0, 8.0))
|
||||||
|
|
||||||
func simulate_tick(village: SimVillage) -> void:
|
func simulate_tick(village: SimVillage) -> void:
|
||||||
hunger += 5.0 * village.food_modifier
|
if is_dead:
|
||||||
energy -= 2.0
|
return
|
||||||
|
|
||||||
hunger = clamp(hunger, 0.0, 100.0)
|
update_needs(village)
|
||||||
energy = clamp(energy, 0.0, 100.0)
|
|
||||||
|
|
||||||
is_starving = hunger >= 90.0
|
if is_dead:
|
||||||
|
return
|
||||||
if is_starving:
|
|
||||||
energy -= 8.0
|
|
||||||
|
|
||||||
energy = clamp(energy, 0.0, 100.0)
|
|
||||||
|
|
||||||
if task_state == TASK_STATE_IDLE or task_state == TASK_STATE_COMPLETE:
|
if task_state == TASK_STATE_IDLE or task_state == TASK_STATE_COMPLETE:
|
||||||
choose_new_task(village)
|
choose_new_task(village)
|
||||||
@@ -68,8 +66,56 @@ func simulate_tick(village: SimVillage) -> void:
|
|||||||
if task_progress >= task_duration:
|
if task_progress >= task_duration:
|
||||||
task_complete = true
|
task_complete = true
|
||||||
task_state = TASK_STATE_COMPLETE
|
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:
|
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 hunger > 75.0:
|
||||||
if village.food > 0:
|
if village.food > 0:
|
||||||
set_task("eat", 2.0)
|
set_task("eat", 2.0)
|
||||||
@@ -77,7 +123,7 @@ func choose_new_task(village: SimVillage) -> void:
|
|||||||
set_task("gather_food", 5.0)
|
set_task("gather_food", 5.0)
|
||||||
return
|
return
|
||||||
|
|
||||||
if energy < 30.0:
|
if energy < 25.0:
|
||||||
set_task("rest", 4.0)
|
set_task("rest", 4.0)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -94,6 +140,9 @@ func choose_new_task(village: SimVillage) -> void:
|
|||||||
set_task("study", 6.0)
|
set_task("study", 6.0)
|
||||||
_:
|
_:
|
||||||
set_task("wander", 4.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:
|
func choose_best_work_task(village: SimVillage) -> String:
|
||||||
var food_score := calculate_task_score(
|
var food_score := calculate_task_score(
|
||||||
|
|||||||
@@ -70,9 +70,14 @@ func update_priorities() -> void:
|
|||||||
print(get_priority_summary())
|
print(get_priority_summary())
|
||||||
|
|
||||||
func apply_npc_task(npc: SimNPC) -> void:
|
func apply_npc_task(npc: SimNPC) -> void:
|
||||||
|
if npc.is_dead:
|
||||||
|
return
|
||||||
|
|
||||||
var productivity := 1.0
|
var productivity := 1.0
|
||||||
|
|
||||||
if npc.energy < 20.0:
|
if npc.is_starving:
|
||||||
|
productivity = 0.35
|
||||||
|
elif npc.energy < 20.0:
|
||||||
productivity = 0.25
|
productivity = 0.25
|
||||||
elif npc.energy < 40.0:
|
elif npc.energy < 40.0:
|
||||||
productivity = 0.5
|
productivity = 0.5
|
||||||
@@ -87,10 +92,21 @@ func apply_npc_task(npc: SimNPC) -> void:
|
|||||||
"study":
|
"study":
|
||||||
knowledge += 1.0 * productivity
|
knowledge += 1.0 * productivity
|
||||||
"eat":
|
"eat":
|
||||||
food -= 1.0
|
if food > 0:
|
||||||
npc.hunger = max(npc.hunger - 35.0, 0.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":
|
"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":
|
"wander":
|
||||||
safety -= 0.2
|
safety -= 0.2
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ extends Node
|
|||||||
|
|
||||||
signal npc_task_changed(npc: SimNPC, old_task: String, new_task: String)
|
signal npc_task_changed(npc: SimNPC, old_task: String, new_task: String)
|
||||||
signal village_changed(village: SimVillage)
|
signal village_changed(village: SimVillage)
|
||||||
|
signal npc_died(npc: SimNPC)
|
||||||
|
|
||||||
var village := SimVillage.new()
|
var village := SimVillage.new()
|
||||||
|
|
||||||
@@ -26,9 +27,11 @@ var professions := [
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
randomize()
|
randomize()
|
||||||
|
village.update_modifiers()
|
||||||
|
village.update_priorities()
|
||||||
generate_npcs()
|
generate_npcs()
|
||||||
print("--- Simulation started ---")
|
print("--- Simulation started ---")
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
tick_timer += delta
|
tick_timer += delta
|
||||||
|
|
||||||
@@ -58,13 +61,24 @@ func simulate_tick() -> void:
|
|||||||
var old_task := npc.current_task
|
var old_task := npc.current_task
|
||||||
var old_state := npc.task_state
|
var old_state := npc.task_state
|
||||||
var was_complete := npc.task_complete
|
var was_complete := npc.task_complete
|
||||||
|
var was_dead := npc.is_dead
|
||||||
|
|
||||||
npc.simulate_tick(village)
|
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)
|
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.apply_npc_task(npc)
|
||||||
village_was_changed = true
|
village_was_changed = true
|
||||||
|
|
||||||
@@ -72,6 +86,13 @@ func simulate_tick() -> void:
|
|||||||
npc.task_state = SimNPC.TASK_STATE_IDLE
|
npc.task_state = SimNPC.TASK_STATE_IDLE
|
||||||
npc.current_task = "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:
|
if DEBUG_LOGS:
|
||||||
print(
|
print(
|
||||||
npc.npc_name,
|
npc.npc_name,
|
||||||
@@ -90,7 +111,13 @@ func simulate_tick() -> void:
|
|||||||
" | hunger: ",
|
" | hunger: ",
|
||||||
round(npc.hunger),
|
round(npc.hunger),
|
||||||
" | energy: ",
|
" | 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:
|
if old_state != npc.task_state:
|
||||||
@@ -105,6 +132,9 @@ func simulate_tick() -> void:
|
|||||||
func notify_npc_arrived(npc_id: int) -> void:
|
func notify_npc_arrived(npc_id: int) -> void:
|
||||||
for npc in npcs:
|
for npc in npcs:
|
||||||
if npc.id == npc_id:
|
if npc.id == npc_id:
|
||||||
|
if npc.is_dead:
|
||||||
|
return
|
||||||
|
|
||||||
if npc.task_state == SimNPC.TASK_STATE_TRAVELING:
|
if npc.task_state == SimNPC.TASK_STATE_TRAVELING:
|
||||||
npc.start_working()
|
npc.start_working()
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ func initialize_world_view() -> void:
|
|||||||
simulation_manager.npc_task_changed.connect(_on_npc_task_changed)
|
simulation_manager.npc_task_changed.connect(_on_npc_task_changed)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: SimulationManager has no npc_task_changed signal")
|
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()
|
update_npc_targets()
|
||||||
|
|
||||||
@@ -127,3 +132,15 @@ func _on_npc_visual_arrived(sim_id: int) -> void:
|
|||||||
simulation_manager.notify_npc_arrived(sim_id)
|
simulation_manager.notify_npc_arrived(sim_id)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: SimulationManager has no notify_npc_arrived method")
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user