feat: let the player live as a citizen with needs and carried inventory
This commit is contained in:
@@ -249,7 +249,11 @@ func _run() -> void:
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(player_manager.harvest_resource_node(berry), 1.0),
|
||||
"Player harvesting should supply the real pantry through the existing command"
|
||||
"Player harvesting should carry the real finite yield into player inventory"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(player_manager.deposit_player_inventory(SimulationIds.RESOURCE_FOOD), 1.0),
|
||||
"Player deposit should supply the real pantry through the carried-inventory command"
|
||||
)
|
||||
var player_resolved: OpportunityStateRecord = player_manager.get_latest_opportunity_for_npc(
|
||||
player_interested.id
|
||||
@@ -264,9 +268,12 @@ func _run() -> void:
|
||||
and player_resolution != null
|
||||
and (
|
||||
StringName(player_resolution.data["event_type"])
|
||||
== SimulationIds.EVENT_RESOURCE_EXTRACTED
|
||||
== SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||
)
|
||||
and int(player_resolution.data["actor_id"]) == SimulationIds.PLAYER_ACTOR_ID
|
||||
and (
|
||||
StringName(player_resolution.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID
|
||||
)
|
||||
and int(player_resolution.data["actor_id"]) == -1
|
||||
and (
|
||||
StringName(player_resolution.data["destination_id"])
|
||||
== SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
|
||||
@@ -322,6 +322,18 @@ func _run() -> void:
|
||||
opportunity_bush_state.set_amount_remaining(1.0)
|
||||
player.global_position = opportunity_bush.interaction_point.global_position
|
||||
player.call("try_interact")
|
||||
_check(
|
||||
(
|
||||
simulation_manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
||||
>= 1.0
|
||||
),
|
||||
"Player gathering should first carry the finite yield"
|
||||
)
|
||||
var player_pantry := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||
)
|
||||
player.global_position = player_pantry.get_interaction_position()
|
||||
player.call("try_interact")
|
||||
village_ui.selected_npc_index = contributor.id
|
||||
village_ui.call("_refresh_npc_inspector")
|
||||
_check(
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var manager := _create_manager(913)
|
||||
manager.set_process(false)
|
||||
var pantry: StorageStateRecord = manager.get_pantry()
|
||||
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
||||
pantry.deposit(SimulationIds.RESOURCE_FOOD, 2.0)
|
||||
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||
|
||||
var berry := ResourceNode.new()
|
||||
berry.name = "CitizenBerry"
|
||||
berry.node_id = &"citizen_berry"
|
||||
berry.action_id = SimulationIds.ACTION_GATHER_FOOD
|
||||
berry.resource_id = SimulationIds.RESOURCE_FOOD
|
||||
berry.initial_amount = 2.0
|
||||
berry.yield_per_action = 1.0
|
||||
berry.debug_label_enabled = false
|
||||
var interaction_point := Marker3D.new()
|
||||
interaction_point.name = "InteractionPoint"
|
||||
interaction_point.position = Vector3.ZERO
|
||||
berry.add_child(interaction_point)
|
||||
root.add_child(berry)
|
||||
await process_frame
|
||||
_check(
|
||||
manager.register_resource_node(berry),
|
||||
"Player citizen test should bind a real finite ResourceNode"
|
||||
)
|
||||
|
||||
var initial_hunger: float = manager.get_player_state().get_hunger()
|
||||
manager.simulate_tick()
|
||||
_check(
|
||||
is_equal_approx(manager.get_player_state().get_hunger(), initial_hunger + 0.125),
|
||||
"Player hunger should advance deterministically on each simulation tick"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(manager.get_player_state().get_energy(), 100.0 - 0.0625),
|
||||
"Player energy should drain by an exact binary fraction each tick"
|
||||
)
|
||||
|
||||
_check(
|
||||
is_equal_approx(manager.harvest_resource_node(berry), 1.0),
|
||||
"The player should carry the first finite yield"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(manager.harvest_resource_node(berry), 1.0),
|
||||
"The player should carry the second finite yield"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(
|
||||
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 2.0
|
||||
),
|
||||
"Carried player food should be conserved across harvests"
|
||||
)
|
||||
|
||||
var food_before: float = manager.get_player_state().get_hunger()
|
||||
_check(manager.eat_player_food(), "The player should eat carried food")
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(manager.get_player_state().get_hunger(), maxf(food_before - 55.0, 0.0))
|
||||
and is_equal_approx(
|
||||
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0
|
||||
)
|
||||
),
|
||||
"Eating carried food should ease hunger and consume exactly one unit"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(manager.deposit_player_inventory(SimulationIds.RESOURCE_FOOD), 1.0),
|
||||
"The player should deposit the remaining carried food"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(
|
||||
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0
|
||||
)
|
||||
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 3.0)
|
||||
),
|
||||
"Deposit should move carried food into the pantry without creating or destroying it"
|
||||
)
|
||||
|
||||
var food_before_pantry: float = pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
var hunger_before: float = manager.get_player_state().get_hunger()
|
||||
_check(manager.eat_player_food(), "The player should eat from the pantry when carrying nothing")
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(
|
||||
pantry.get_amount(SimulationIds.RESOURCE_FOOD), food_before_pantry - 1.0
|
||||
)
|
||||
and is_equal_approx(
|
||||
manager.get_player_state().get_hunger(), maxf(hunger_before - 55.0, 0.0)
|
||||
)
|
||||
),
|
||||
"Pantry eating should consume one village food and ease player hunger"
|
||||
)
|
||||
|
||||
manager.simulate_tick()
|
||||
var mid_hunger: float = manager.get_player_state().get_hunger()
|
||||
var saved_json: String = manager.serialize_state()
|
||||
var restored := _create_manager(914)
|
||||
_check(
|
||||
restored.restore_state_from_json(saved_json),
|
||||
"Player state should survive schema-v13 save and restore"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
restored.get_state_checksum() == manager.get_state_checksum()
|
||||
and is_equal_approx(restored.get_player_state().get_hunger(), mid_hunger)
|
||||
),
|
||||
"Restore should preserve the deterministic player needs and checksum"
|
||||
)
|
||||
restored.free()
|
||||
|
||||
var v12_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
v12_data["schema_version"] = SimulationStateRecord.PLAYER_NEEDS_SCHEMA_VERSION
|
||||
v12_data.erase("player")
|
||||
var migrated := SimulationStateRecord.from_dictionary(v12_data)
|
||||
_check(
|
||||
(
|
||||
migrated != null
|
||||
and migrated.player != null
|
||||
and is_equal_approx(migrated.player.get_hunger(), 40.0)
|
||||
and is_equal_approx(migrated.player.get_energy(), 100.0)
|
||||
and migrated.player.get_carried_total() == 0.0
|
||||
),
|
||||
"World schema v12 should migrate to v13 with a fresh default player record"
|
||||
)
|
||||
|
||||
berry.queue_free()
|
||||
manager.free()
|
||||
_finish()
|
||||
|
||||
|
||||
func _create_manager(seed_value: int = 913) -> Node:
|
||||
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||
manager.simulation_seed = seed_value
|
||||
manager.debug_logs = false
|
||||
var home_positions: Array[Vector3] = [
|
||||
Vector3(0.0, 0.0, 0.0),
|
||||
Vector3(2.0, 0.0, 0.0),
|
||||
Vector3(10.0, 0.0, 0.0),
|
||||
Vector3(12.0, 0.0, 0.0),
|
||||
Vector3(30.0, 0.0, 30.0),
|
||||
Vector3(40.0, 0.0, 40.0),
|
||||
]
|
||||
manager.home_positions = home_positions
|
||||
root.add_child(manager)
|
||||
manager.set_process(false)
|
||||
return manager
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("[TEST] Player citizen loop passed: needs -> carry -> eat -> deposit")
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bxh6g2basq762
|
||||
@@ -38,20 +38,32 @@ func _run() -> void:
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(manager.harvest_resource_node(berry), 1.0),
|
||||
"Player harvesting should supply the real pantry"
|
||||
"Player harvesting should carry the real finite yield"
|
||||
)
|
||||
var carried_food: float = manager.get_player_state().get_inventory_amount(
|
||||
SimulationIds.RESOURCE_FOOD
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(carried_food, 1.0),
|
||||
"Player harvesting should move the yield into carried player inventory"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(manager.deposit_player_inventory(SimulationIds.RESOURCE_FOOD), 1.0),
|
||||
"Player deposit should supply the real pantry"
|
||||
)
|
||||
|
||||
var supply_event := _latest_event_of_type(manager, SimulationIds.EVENT_RESOURCE_EXTRACTED)
|
||||
var supply_event := _latest_event_of_type(manager, SimulationIds.EVENT_STORAGE_DEPOSITED)
|
||||
var supply_event_id := int(supply_event.data["event_id"])
|
||||
_check(
|
||||
(
|
||||
int(supply_event.data["actor_id"]) == SimulationIds.PLAYER_ACTOR_ID
|
||||
and (StringName(supply_event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID)
|
||||
and (
|
||||
StringName(supply_event.data["destination_id"])
|
||||
== SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
)
|
||||
),
|
||||
"The player supply should be recorded as a pantry food extraction by the player actor"
|
||||
"The player supply should be recorded as a pantry food deposit from player inventory"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
@@ -102,6 +114,7 @@ func _run() -> void:
|
||||
)
|
||||
restored.free()
|
||||
|
||||
var v12_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
berry.queue_free()
|
||||
manager.free()
|
||||
|
||||
@@ -113,7 +126,21 @@ func _run() -> void:
|
||||
and migrated.simulation.get("next_opportunity_id", -1) >= 0
|
||||
and not migrated.opportunities.is_empty()
|
||||
),
|
||||
"World schema v11 should migrate to v12 without dropping opportunity history"
|
||||
"World schema v11 should migrate to v13 without dropping opportunity history"
|
||||
)
|
||||
|
||||
v12_data["schema_version"] = SimulationStateRecord.PLAYER_NEEDS_SCHEMA_VERSION
|
||||
v12_data.erase("player")
|
||||
var v12_migrated := SimulationStateRecord.from_dictionary(v12_data)
|
||||
_check(
|
||||
(
|
||||
v12_migrated != null
|
||||
and v12_migrated.player != null
|
||||
and is_equal_approx(v12_migrated.player.get_hunger(), 40.0)
|
||||
and is_equal_approx(v12_migrated.player.get_energy(), 100.0)
|
||||
and v12_migrated.player.get_carried_total() == 0.0
|
||||
),
|
||||
"World schema v12 should migrate to v13 with a fresh default player record"
|
||||
)
|
||||
|
||||
_finish()
|
||||
|
||||
@@ -19,11 +19,9 @@ func _run() -> void:
|
||||
var bush := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/BerryBush_01") as ResourceNode
|
||||
)
|
||||
var tree := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/Tree_01") as ResourceNode
|
||||
simulation_manager.set_process(false)
|
||||
|
||||
var bush_state: ResourceStateRecord = simulation_manager.get_resource_state(bush.node_id)
|
||||
var tree_state: ResourceStateRecord = simulation_manager.get_resource_state(tree.node_id)
|
||||
bush_state.set_amount_remaining(1.0)
|
||||
_check(
|
||||
simulation_manager.reserve_resource(bush.node_id, 999),
|
||||
@@ -38,8 +36,11 @@ func _run() -> void:
|
||||
"Player should deplete the nearby bush"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(simulation_manager.village.food, food_before + 1.0),
|
||||
"Village should receive exactly the bush's remaining food"
|
||||
is_equal_approx(
|
||||
simulation_manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
||||
1.0
|
||||
),
|
||||
"Player harvesting should carry exactly the bush's remaining food"
|
||||
)
|
||||
_check(bush_state.get_reserved_by() == -1, "Depletion should release the NPC reservation")
|
||||
var player_extraction: EconomicEventRecord
|
||||
@@ -56,8 +57,14 @@ func _run() -> void:
|
||||
_check(player_depletion != null, "Player depletion should create a structured event")
|
||||
if player_extraction != null:
|
||||
_check(
|
||||
player_extraction.get_world_position().is_equal_approx(bush_event_position),
|
||||
"Player extraction should preserve the resource interaction position"
|
||||
(
|
||||
player_extraction.get_world_position().is_equal_approx(bush_event_position)
|
||||
and (
|
||||
StringName(player_extraction.data["destination_id"])
|
||||
== SimulationIds.PLAYER_INVENTORY_ID
|
||||
)
|
||||
),
|
||||
"Player extraction should preserve the interaction position and name the carried inventory"
|
||||
)
|
||||
if player_depletion != null:
|
||||
_check(
|
||||
@@ -65,45 +72,54 @@ func _run() -> void:
|
||||
"Player depletion should preserve the resource interaction position"
|
||||
)
|
||||
|
||||
player.global_position = tree.interaction_point.global_position
|
||||
var wood_before: float = simulation_manager.village.wood
|
||||
var tree_before := tree_state.get_amount_remaining()
|
||||
player.try_interact()
|
||||
|
||||
_check(
|
||||
is_equal_approx(tree_state.get_amount_remaining(), tree_before - tree.yield_per_action),
|
||||
"Player should extract the tree's configured yield"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(simulation_manager.village.wood, wood_before + tree.yield_per_action),
|
||||
"Village should receive exactly the extracted wood"
|
||||
)
|
||||
|
||||
var pantry := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||
)
|
||||
var pantry_state: StorageStateRecord = simulation_manager.get_pantry()
|
||||
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, pantry_state.get_available_capacity() - 0.5)
|
||||
player.global_position = pantry.get_interaction_position()
|
||||
player.try_interact()
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(simulation_manager.village.food, food_before + 1.0)
|
||||
and is_equal_approx(
|
||||
simulation_manager.get_player_state().get_inventory_amount(
|
||||
SimulationIds.RESOURCE_FOOD
|
||||
),
|
||||
0.0
|
||||
)
|
||||
),
|
||||
"Depositing at the pantry should move carried food into the village and empty the player"
|
||||
)
|
||||
|
||||
var pantry_food_before := pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
player.try_interact()
|
||||
_check(
|
||||
(
|
||||
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < pantry_food_before
|
||||
and simulation_manager.get_player_state().get_hunger() < 40.0
|
||||
),
|
||||
"Player should eat from the typed pantry StorageNode and ease their hunger"
|
||||
)
|
||||
|
||||
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, pantry_state.get_available_capacity())
|
||||
simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||
bush_state.set_amount_remaining(1.0)
|
||||
player.global_position = bush.interaction_point.global_position
|
||||
player.try_interact()
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(bush_state.get_amount_remaining(), 0.5)
|
||||
and is_equal_approx(pantry_state.get_available_capacity(), 0.0)
|
||||
),
|
||||
"Player harvesting should leave overflow at its source when storage fills"
|
||||
)
|
||||
|
||||
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, 3.0)
|
||||
simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||
var pantry_food_before := pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
player.global_position = pantry.get_interaction_position()
|
||||
player.try_interact()
|
||||
_check(
|
||||
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < pantry_food_before,
|
||||
"Player should eat from the typed pantry StorageNode"
|
||||
(
|
||||
is_equal_approx(bush_state.get_amount_remaining(), 0.0)
|
||||
and is_equal_approx(pantry_state.get_available_capacity(), 0.0)
|
||||
and is_equal_approx(
|
||||
simulation_manager.get_player_state().get_inventory_amount(
|
||||
SimulationIds.RESOURCE_FOOD
|
||||
),
|
||||
1.0
|
||||
)
|
||||
),
|
||||
"Player harvesting into a full pantry should leave the unaccepted overflow carried"
|
||||
)
|
||||
var guard_site := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite
|
||||
|
||||
@@ -246,7 +246,11 @@ func _run() -> void:
|
||||
player_active_restored.free()
|
||||
_check(
|
||||
is_equal_approx(player_manager.harvest_resource_node(tree), 1.0),
|
||||
"Player tree extraction should supply the authoritative woodpile"
|
||||
"Player tree extraction should carry the authoritative wood yield"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(player_manager.deposit_player_inventory(SimulationIds.RESOURCE_WOOD), 1.0),
|
||||
"Player wood deposit should supply the authoritative woodpile"
|
||||
)
|
||||
var player_resolved: OpportunityStateRecord = player_manager.get_latest_opportunity_for_npc(
|
||||
player_actor.id
|
||||
@@ -258,10 +262,10 @@ func _run() -> void:
|
||||
(
|
||||
player_resolved.get_status() == OpportunityStateRecord.STATUS_RESOLVED
|
||||
and (
|
||||
StringName(player_event.data["event_type"])
|
||||
== SimulationIds.EVENT_RESOURCE_EXTRACTED
|
||||
StringName(player_event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||
)
|
||||
and int(player_event.data["actor_id"]) == -1
|
||||
and int(player_event.data["actor_id"]) == SimulationIds.PLAYER_ACTOR_ID
|
||||
and (StringName(player_event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID)
|
||||
and (
|
||||
StringName(player_event.data["destination_id"])
|
||||
== SimulationIds.STORAGE_VILLAGE_WOODPILE
|
||||
|
||||
Reference in New Issue
Block a user