feat: add NPC schedules with sleep, meal, and work periods
Add ACTION_SLEEP with home-position targeting and energy restoration. Embed schedule periods (SLEEP/MEAL/WORK/DISCRETIONARY) in ActionSelectionSystem with TimeOfDay from SimulationClock. NPCs sleep at night when energy is low, eat during meal periods when hungry, and work/discretionary the rest of the cycle. Home position stored per NPC and serialized through NPCStateRecord with backward-compatible fallback. Cycle duration persisted through save/restore. Includes headless test for sleep period, work period, meal hunger, energy restoration, serialization round-trip, and home targeting without world adapter.
This commit is contained in:
@@ -15,6 +15,7 @@ var village := SimVillage.new()
|
||||
var npcs: Array[SimNPC] = []
|
||||
@export var tick_interval := 1.2
|
||||
@export var simulation_seed: int = 1337
|
||||
@export var cycle_duration_seconds := 240.0
|
||||
@export var debug_logs := true
|
||||
@export var active_world_adapter: Node
|
||||
|
||||
@@ -42,6 +43,7 @@ func _ready() -> void:
|
||||
set_process(false)
|
||||
return
|
||||
clock = SimulationClock.new(tick_interval)
|
||||
clock.cycle_duration_seconds = cycle_duration_seconds
|
||||
village.debug_logs = debug_logs
|
||||
_initialize_storage()
|
||||
village.update_modifiers()
|
||||
@@ -112,7 +114,7 @@ func simulate_tick() -> void:
|
||||
and old_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
||||
and npc.task_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
||||
):
|
||||
var selection := action_selector.select_action(npc, village)
|
||||
var selection := action_selector.select_action(npc, village, clock.time_of_day())
|
||||
if selection != null:
|
||||
latest_decisions[npc.id] = selection
|
||||
npc_decision_recorded.emit(npc, selection)
|
||||
@@ -195,6 +197,11 @@ func simulate_tick() -> void:
|
||||
withdraw_to_npc(npc, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
elif completed_task == SimulationIds.ACTION_EAT:
|
||||
consume_npc_food(npc)
|
||||
elif completed_task == SimulationIds.ACTION_SLEEP:
|
||||
npc.energy = minf(npc.energy + 40.0, 100.0)
|
||||
npc.position = npc.home_position
|
||||
if debug_logs:
|
||||
print("[SimulationManager] ", npc.npc_name, " completed sleep at home")
|
||||
elif (
|
||||
completed_task
|
||||
not in [SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_GATHER_WOOD]
|
||||
@@ -354,14 +361,19 @@ func notify_npc_target_unavailable(npc_id: int) -> void:
|
||||
|
||||
|
||||
func resolve_npc_target(npc_id: int, origin: Vector3) -> bool:
|
||||
if active_world_adapter == null:
|
||||
push_error("SimulationManager: active_world_adapter is missing")
|
||||
return false
|
||||
for npc in npcs:
|
||||
if npc.id != npc_id:
|
||||
continue
|
||||
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_TRAVELING:
|
||||
return false
|
||||
if npc.current_task == SimulationIds.ACTION_SLEEP:
|
||||
npc.travel_target_position = npc.home_position
|
||||
npc.has_travel_target = true
|
||||
npc_travel_requested.emit(npc, npc.travel_target_position)
|
||||
return true
|
||||
if active_world_adapter == null:
|
||||
push_error("SimulationManager: active_world_adapter is missing")
|
||||
return false
|
||||
var result := target_resolver.resolve(npc, origin, self, active_world_adapter)
|
||||
if result.is_empty():
|
||||
notify_npc_target_unavailable(npc.id)
|
||||
@@ -738,7 +750,8 @@ func create_state_record() -> SimulationStateRecord:
|
||||
"clock_accumulator": clock.accumulator,
|
||||
"clock_elapsed_ticks": clock.elapsed_ticks,
|
||||
"wander_random_streams": wander_streams,
|
||||
"next_event_id": next_event_id
|
||||
"next_event_id": next_event_id,
|
||||
"cycle_duration_seconds": clock.cycle_duration_seconds
|
||||
}
|
||||
record.village = VillageStateRecord.capture(village)
|
||||
for npc in npcs:
|
||||
@@ -782,6 +795,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
clock = SimulationClock.new(tick_interval)
|
||||
clock.accumulator = float(record.simulation["clock_accumulator"])
|
||||
clock.elapsed_ticks = int(record.simulation["clock_elapsed_ticks"])
|
||||
clock.cycle_duration_seconds = float(record.simulation.get("cycle_duration_seconds", 240.0))
|
||||
village = record.village.restore(debug_logs)
|
||||
storage_states.clear()
|
||||
for storage_record in record.storages:
|
||||
|
||||
Reference in New Issue
Block a user