4604dc6d5a
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.
178 lines
5.2 KiB
GDScript
178 lines
5.2 KiB
GDScript
extends SceneTree
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
|
manager.debug_logs = false
|
|
root.add_child(manager)
|
|
manager.set_process(false)
|
|
manager.clock.elapsed_ticks = 100
|
|
|
|
var pantry: StorageStateRecord = manager.get_pantry()
|
|
pantry.withdraw(SimulationIds.RESOURCE_FOOD, 1000.0)
|
|
manager._sync_village_food()
|
|
var resource := ResourceStateRecord.from_dictionary(
|
|
{
|
|
"schema_version": ResourceStateRecord.SCHEMA_VERSION,
|
|
"node_id": "food_loop_bush",
|
|
"action_id": String(SimulationIds.ACTION_GATHER_FOOD),
|
|
"resource_id": String(SimulationIds.RESOURCE_FOOD),
|
|
"amount_remaining": 10.0,
|
|
"yield_per_action": 2.0,
|
|
"reserved_by": -1,
|
|
"enabled": true,
|
|
"can_npcs_use": true,
|
|
"can_player_use": true,
|
|
"safety_risk": 0.0,
|
|
"comfort_distance": 18.0,
|
|
"discovery_priority": 0.0
|
|
}
|
|
)
|
|
manager.resource_states[resource.get_node_id()] = resource
|
|
|
|
var npc: SimNPC = manager.npcs[0]
|
|
npc.hunger = 20.0
|
|
npc.energy = 100.0
|
|
npc.set_task(SimulationIds.ACTION_GATHER_FOOD, 1.0)
|
|
npc.target_id = resource.get_node_id()
|
|
resource.reserve(npc.id)
|
|
npc.start_working()
|
|
manager.simulate_tick()
|
|
|
|
_check(
|
|
(
|
|
is_equal_approx(resource.get_amount_remaining(), 8.0)
|
|
and is_equal_approx(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 2.0)
|
|
),
|
|
"Gathering should move food from the source into NPC inventory"
|
|
)
|
|
_check(
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0),
|
|
"Gathering should not teleport food into storage"
|
|
)
|
|
|
|
manager.simulate_tick()
|
|
_check(
|
|
npc.current_task == SimulationIds.ACTION_DEPOSIT_FOOD,
|
|
"Carried food should select the deposit action"
|
|
)
|
|
manager.notify_npc_arrived(npc.id)
|
|
manager.simulate_tick()
|
|
_check(
|
|
(
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 2.0)
|
|
and is_equal_approx(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
),
|
|
"Deposit should transfer carried food into the pantry"
|
|
)
|
|
|
|
npc.hunger = 80.0
|
|
manager.simulate_tick()
|
|
_check(
|
|
npc.current_task == SimulationIds.ACTION_WITHDRAW_FOOD,
|
|
"Hungry NPC should retrieve food from storage"
|
|
)
|
|
manager.notify_npc_arrived(npc.id)
|
|
manager.simulate_tick()
|
|
_check(
|
|
(
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
and is_equal_approx(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0)
|
|
),
|
|
"Withdraw should transfer pantry food into NPC inventory"
|
|
)
|
|
|
|
manager.simulate_tick()
|
|
_check(npc.current_task == SimulationIds.ACTION_EAT, "NPC carrying food should select eat")
|
|
manager.notify_npc_arrived(npc.id)
|
|
var hunger_before_eating := npc.hunger
|
|
manager.simulate_tick()
|
|
manager.simulate_tick()
|
|
_check(
|
|
(
|
|
npc.hunger < hunger_before_eating
|
|
and is_equal_approx(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
|
),
|
|
"Eating should consume carried food and reduce hunger"
|
|
)
|
|
|
|
var conserved_food := (
|
|
resource.get_amount_remaining()
|
|
+ pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
+ npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
|
)
|
|
_check(
|
|
is_equal_approx(conserved_food, 9.0),
|
|
"Exactly one food unit should leave the world when eaten"
|
|
)
|
|
_check_economic_event_chain(manager)
|
|
|
|
var restored: Node = load("res://simulation/SimulationManager.gd").new()
|
|
restored.debug_logs = false
|
|
root.add_child(restored)
|
|
restored.set_process(false)
|
|
_check(
|
|
restored.restore_state_from_json(manager.serialize_state()),
|
|
"Economic events should restore with the simulation"
|
|
)
|
|
_check(
|
|
(
|
|
restored.economic_events.size() == manager.economic_events.size()
|
|
and restored.next_event_id == manager.next_event_id
|
|
),
|
|
"Economic event history and its next ID should round-trip"
|
|
)
|
|
|
|
if failures.is_empty():
|
|
print("[TEST] Food storage loop passed")
|
|
quit(0)
|
|
return
|
|
for failure in failures:
|
|
push_error("[TEST] " + failure)
|
|
quit(1)
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|
|
|
|
|
|
func _check_economic_event_chain(manager: Node) -> void:
|
|
var expected_types: Array[StringName] = [
|
|
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
|
SimulationIds.EVENT_STORAGE_WITHDRAWN,
|
|
SimulationIds.EVENT_ITEM_CONSUMED
|
|
]
|
|
_check(
|
|
manager.economic_events.size() == expected_types.size(),
|
|
"Food transfer loop should emit one structured event per transfer"
|
|
)
|
|
if manager.economic_events.size() != expected_types.size():
|
|
return
|
|
for index in expected_types.size():
|
|
var event: EconomicEventRecord = manager.economic_events[index]
|
|
_check(
|
|
(
|
|
int(event.data["event_id"]) == index
|
|
and StringName(event.data["event_type"]) == expected_types[index]
|
|
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
|
),
|
|
"Economic event order, identity, and item should be deterministic"
|
|
)
|
|
_check(
|
|
(
|
|
is_equal_approx(float(manager.economic_events[0].data["amount"]), 2.0)
|
|
and is_equal_approx(float(manager.economic_events[1].data["amount"]), 2.0)
|
|
and is_equal_approx(float(manager.economic_events[2].data["amount"]), 1.0)
|
|
and is_equal_approx(float(manager.economic_events[3].data["amount"]), 1.0)
|
|
),
|
|
"Economic events should record exact transferred quantities"
|
|
)
|