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:
@@ -1,10 +1,56 @@
|
||||
class_name ActionSelectionSystem
|
||||
extends RefCounted
|
||||
|
||||
enum SchedulePeriod {
|
||||
WORK,
|
||||
SLEEP,
|
||||
MEAL,
|
||||
DISCRETIONARY
|
||||
}
|
||||
|
||||
func select_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
||||
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(
|
||||
@@ -37,6 +83,14 @@ func select_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
||||
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)
|
||||
|
||||
|
||||
@@ -129,3 +183,23 @@ func _calculate_score(
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user