feat: give the player a real carried inventory and deposit transactions

This commit is contained in:
2026-08-08 23:59:19 +02:00
parent 9678526c5a
commit 5e6c963d23
12 changed files with 565 additions and 65 deletions
+2 -1
View File
@@ -25,13 +25,14 @@ height = 1.7
[node name="JajceWorld" parent="." unique_id=1023795383 instance=ExtResource("11_jajce")]
[node name="Player" type="CharacterBody3D" parent="." unique_id=2022843760 node_paths=PackedStringArray("camera_rig", "simulation_manager", "world_view_manager", "pantry_storage", "guard_site", "study_site")]
[node name="Player" type="CharacterBody3D" parent="." unique_id=2022843760 node_paths=PackedStringArray("camera_rig", "simulation_manager", "world_view_manager", "pantry_storage", "woodpile_storage", "guard_site", "study_site")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
script = ExtResource("1_h2yge")
camera_rig = NodePath("../CameraRig")
simulation_manager = NodePath("../SimulationManager")
world_view_manager = NodePath("../WorldViewManager")
pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
woodpile_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillageWoodpile")
guard_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/GuardPost")
study_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/StudyDesk")
stomach_capacity_for_food = 10
+1
View File
@@ -4,6 +4,7 @@ extends RefCounted
const KIND_ANIMAL := &"animal"
const KIND_RESOURCE := &"resource"
const KIND_PANTRY := &"pantry"
const KIND_DEPOSIT := &"deposit"
const KIND_GUARD := &"guard"
const KIND_STUDY := &"study"
+93 -8
View File
@@ -11,6 +11,7 @@ signal interaction_feedback(heading: String, message: String, succeeded: bool)
@export var world_view_manager: Node
@export var pantry_storage: StorageNode
@export var woodpile_storage: StorageNode
@export var guard_site: ActivitySite
@export var study_site: ActivitySite
@@ -69,6 +70,8 @@ func try_interact() -> void:
_execute_animal_interaction(context)
PlayerInteractionResult.KIND_RESOURCE:
_execute_resource_interaction(context)
PlayerInteractionResult.KIND_DEPOSIT:
_execute_deposit_interaction(context)
PlayerInteractionResult.KIND_GUARD:
simulation_manager.add_safety(3.0)
interaction_feedback.emit(
@@ -112,6 +115,20 @@ func get_interaction_context() -> PlayerInteractionResult:
"Work here · +2 knowledge",
study_site
)
if is_near_storage(woodpile_storage) and _get_player_carried(SimulationIds.RESOURCE_WOOD) > 0.0:
return _build_deposit_context(
SimulationIds.RESOURCE_WOOD,
SimulationIds.STORAGE_VILLAGE_WOODPILE,
woodpile_storage,
"Village woodpile"
)
if is_near_storage(pantry_storage) and _get_player_carried(SimulationIds.RESOURCE_FOOD) > 0.0:
return _build_deposit_context(
SimulationIds.RESOURCE_FOOD,
SimulationIds.STORAGE_VILLAGE_PANTRY,
pantry_storage,
"Village pantry"
)
if is_near_storage(pantry_storage):
return _build_pantry_context()
return null
@@ -290,14 +307,40 @@ func _build_resource_context(node: ResourceNode) -> PlayerInteractionResult:
definition.display_name if definition != null else String(node.action_id).capitalize()
)
var display_name := String(node.node_id).replace("_", " ").capitalize()
var carried := _get_player_carried(node.resource_id)
var detail := (
"%s · %.1f remaining · carrying %.1f" % [display_name, node.get_amount_remaining(), carried]
)
if _get_available_carry() <= 0.0:
detail = "Hands full — deposit before gathering"
return PlayerInteractionResult.new(
PlayerInteractionResult.KIND_RESOURCE,
node.action_id,
node.node_id,
display_name,
prompt,
"%s · %.1f remaining" % [display_name, node.get_amount_remaining()],
node
detail,
node,
"" if _get_available_carry() > 0.0 else "You cannot carry more right now."
)
func _build_deposit_context(
resource_id: StringName, target_id: StringName, target_node: StorageNode, display_name: String
) -> PlayerInteractionResult:
var carried := _get_player_carried(resource_id)
return PlayerInteractionResult.new(
PlayerInteractionResult.KIND_DEPOSIT,
(
SimulationIds.ACTION_DEPOSIT_FOOD
if resource_id == SimulationIds.RESOURCE_FOOD
else SimulationIds.ACTION_DEPOSIT_WOOD
),
target_id,
display_name,
"Deposit %s" % String(resource_id).capitalize(),
"%.1f %s in your hands" % [carried, String(resource_id).to_lower()],
target_node
)
@@ -349,23 +392,51 @@ func _execute_animal_interaction(context: PlayerInteractionResult) -> void:
func _execute_resource_interaction(context: PlayerInteractionResult) -> void:
var node := context.target_node as ResourceNode
if node == null or not simulation_manager.has_method("harvest_resource_node"):
push_error("Player: SimulationManager cannot harvest the resolved ResourceNode")
if node == null or not simulation_manager.has_method("player_gather"):
push_error("Player: SimulationManager cannot gather the resolved ResourceNode")
return
var extracted: float = simulation_manager.harvest_resource_node(node)
var extracted: float = simulation_manager.player_gather(node)
if extracted <= 0.0:
interaction_feedback.emit(
"%s is unavailable" % context.display_name, "Nothing could be gathered.", false
"%s is unavailable" % context.display_name,
(
context.blocked_reason
if not context.blocked_reason.is_empty()
else "Nothing could be gathered."
),
false
)
return
var destination := "pantry" if node.resource_id == SimulationIds.RESOURCE_FOOD else "woodpile"
var carried := _get_player_carried(node.resource_id)
interaction_feedback.emit(
"%s gathered" % String(node.resource_id).capitalize(),
"%.1f moved to the village %s." % [extracted, destination],
"%.1f added to your hands · carrying %.1f" % [extracted, carried],
true
)
func _deposit_carried(resource_id: StringName) -> float:
if not simulation_manager.has_method("player_deposit"):
push_error("Player: SimulationManager cannot deposit carried resources")
return 0.0
return simulation_manager.player_deposit(resource_id)
func _execute_deposit_interaction(context: PlayerInteractionResult) -> void:
var resource_id := (
SimulationIds.RESOURCE_FOOD
if context.action_id == SimulationIds.ACTION_DEPOSIT_FOOD
else SimulationIds.RESOURCE_WOOD
)
var deposited: float = _deposit_carried(resource_id)
if deposited <= 0.0:
interaction_feedback.emit("Deposit blocked", "You are not carrying that here.", false)
return
interaction_feedback.emit(
"Deposited to %s" % context.display_name, "%.1f moved into the village." % deposited, true
)
func _execute_pantry_interaction(context: PlayerInteractionResult) -> void:
if not context.is_available():
interaction_feedback.emit("The pantry is empty", context.blocked_reason, false)
@@ -424,6 +495,20 @@ func _get_pantry_food() -> float:
return pantry.get_amount(SimulationIds.RESOURCE_FOOD) if pantry != null else 0.0
func _get_player_carried(resource_id: StringName) -> float:
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
return 0.0
var state: PlayerStateRecord = simulation_manager.get_player_state()
return state.get_inventory_amount(resource_id) if state != null else 0.0
func _get_available_carry() -> float:
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
return 0.0
var state: PlayerStateRecord = simulation_manager.get_player_state()
return state.get_available_carry() if state != null else 0.0
func is_near_storage(storage_node: StorageNode) -> bool:
if storage_node == null:
return false
+33 -19
View File
@@ -30,6 +30,7 @@ signal player_quest_expired(quest: PlayerQuestRecord)
signal player_standing_changed(standing: PlayerStandingRecord)
signal player_tier_advanced(tier: int, tier_name: String)
signal player_needs_changed(state: PlayerStateRecord)
signal player_inventory_changed(state: PlayerStateRecord)
signal player_died(state: PlayerStateRecord)
var village := SimVillage.new()
@@ -85,6 +86,8 @@ func _ready() -> void:
event_log.event_recorded.connect(_on_economic_event_recorded)
event_recorder.configure(event_log, Callable(self, "get_tick_count_for_recording"))
event_recorder.set_npcs(npcs)
player_needs.configure(economy, event_recorder)
player_needs.set_resource_states(resource_states)
economy.inventory_changed.connect(_on_economy_inventory_changed)
economy.economic_event_requested.connect(event_recorder.record_economic)
economy.narrative_event_requested.connect(event_recorder.record_narrative)
@@ -578,30 +581,28 @@ func _on_economic_event_recorded(event: EconomicEventRecord) -> void:
for learned_record in learned_records:
_apply_new_event_knowledge(learned_record, event)
if StringName(event.data["event_type"]) == SimulationIds.EVENT_ANIMAL_FED:
_resolve_animal_care_quest(event)
_finish_quest_resolution(
player_quest_system.on_animal_fed(
animal_care.get_state(StringName(event.data["destination_id"])), event, tick_count
)
)
_update_opportunity_from_event(event)
economic_event_recorded.emit(event)
func _resolve_animal_care_quest(event: EconomicEventRecord) -> void:
var animal_state := animal_care.get_state(StringName(event.data["destination_id"]))
var resolution := player_quest_system.on_animal_fed(animal_state, event, tick_count)
_finish_quest_resolution(resolution)
func _finish_quest_resolution(resolution: Dictionary) -> void:
if not resolution.has("quest"):
return
var quest: PlayerQuestRecord = resolution["quest"]
if resolution["completed"]:
if not resolution["completed"]:
player_quest_expired.emit(quest)
return
player_quest_completed.emit(quest)
var standing := player_quest_system.standing
player_standing_changed.emit(standing)
if standing.get_tier() > _last_player_tier:
_last_player_tier = standing.get_tier()
player_tier_advanced.emit(standing.get_tier(), standing.get_tier_name())
else:
player_quest_expired.emit(quest)
func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bool:
@@ -609,12 +610,11 @@ func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bo
var listener := _find_npc_by_id(listener_id)
if not _can_communicate_at_shared_activity(speaker, listener):
return false
var known_event_ids := event_knowledge_system.get_communicable_event_ids(
for event_id in event_knowledge_system.get_communicable_event_ids(
speaker.id,
_get_lasting_event_ids(speaker.id),
opportunity_system.get_open_trigger_event_id()
)
for event_id in known_event_ids:
):
var event := event_log.get_by_id(event_id)
if event == null:
continue
@@ -742,22 +742,21 @@ func get_active_player_quest() -> PlayerQuestRecord:
func _advance_player_needs() -> void:
if not player_needs.advance(village):
player_needs_changed.emit(player_needs.state)
return
if player_needs.advance(village):
player_died.emit(player_needs.state)
player_needs.apply_death_consequence(player_quest_system.standing)
_last_player_tier = player_quest_system.standing.get_tier()
player_standing_changed.emit(player_quest_system.standing)
return
player_needs_changed.emit(player_needs.state)
func respawn_player() -> bool:
var was_dead := player_needs.state.is_dead()
if not player_needs.state.is_dead():
return false
player_needs.respawn()
if was_dead and not player_needs.state.is_dead():
player_needs_changed.emit(player_needs.state)
return true
return false
func get_player_state() -> PlayerStateRecord:
@@ -771,6 +770,21 @@ func player_eat(amount: float) -> float:
return eaten
func player_gather(node: ResourceNode) -> float:
var gathered := player_needs.gather_node(node)
if gathered > 0.0:
player_inventory_changed.emit(player_needs.state)
return gathered
func player_deposit(resource_id: StringName) -> float:
var deposited := player_needs.deposit_resource(resource_id)
if deposited > 0.0:
village_changed.emit(village)
player_inventory_changed.emit(player_needs.state)
return deposited
func _consider_animal_care_quests() -> void:
var pantry: StorageStateRecord = get_pantry()
for animal_state in animal_care.get_all_states():
@@ -537,6 +537,8 @@ static func _is_valid_supply(
var event_type := StringName(event.data["event_type"])
var actor_id := int(event.data["actor_id"])
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
if actor_id < 0:
return StringName(event.data["source_id"]) == &"player_carry"
return (
_find_npc(actor_id, npcs) != null
and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id)
+86 -2
View File
@@ -2,6 +2,18 @@ class_name PlayerNeedsSystem
extends RefCounted
var state := PlayerStateRecord.create()
var economy: RefCounted
var event_recorder: RefCounted
var resource_states: Dictionary = {}
func configure(economy_service: RefCounted, recorder: RefCounted) -> void:
economy = economy_service
event_recorder = recorder
func set_resource_states(states: Dictionary) -> void:
resource_states = states
func advance(village: SimVillage) -> bool:
@@ -15,8 +27,80 @@ func advance(village: SimVillage) -> bool:
func eat(amount: float) -> float:
var eaten := state.eat(amount)
return eaten
return state.eat(amount)
func gather_node(node: ResourceNode) -> float:
if node == null:
return 0.0
var resource_state := resource_states.get(node.node_id) as ResourceStateRecord
if resource_state == null or not resource_state.can_player_use_resource():
return 0.0
var available := state.get_available_carry()
if available <= 0.0:
return 0.0
var extracted := resource_state.extract(minf(resource_state.get_yield_per_action(), available))
if extracted <= 0.0:
return 0.0
var carried := state.add_inventory(resource_state.get_resource_id(), extracted)
if carried <= 0.0:
resource_state.data["amount_remaining"] = (
resource_state.get_amount_remaining() + extracted
)
return 0.0
var event_position := (
node.interaction_point.global_position
if node.interaction_point != null
else node.global_position
)
event_recorder.record_economic_at(
SimulationIds.EVENT_RESOURCE_EXTRACTED,
-1,
resource_state.get_node_id(),
&"player_carry",
resource_state.get_resource_id(),
carried,
event_position
)
if resource_state.get_amount_remaining() <= 0.0:
event_recorder.record_narrative_at(
SimulationIds.EVENT_RESOURCE_DEPLETED,
-1,
resource_state.get_node_id(),
"",
event_position
)
return carried
func deposit_resource(resource_id: StringName) -> float:
if resource_id.is_empty():
return 0.0
var carried := state.get_inventory_amount(resource_id)
if carried <= 0.0:
return 0.0
if economy == null or not economy.has_method("get_storage_for_resource"):
return 0.0
var storage: StorageStateRecord = economy.get_storage_for_resource(resource_id)
if storage == null:
return 0.0
var deposited: float = economy.deposit_resource(resource_id, carried)
if deposited <= 0.0:
return 0.0
state.remove_inventory(resource_id, deposited)
event_recorder.record_economic(
SimulationIds.EVENT_STORAGE_DEPOSITED,
-1,
&"player_carry",
storage.get_storage_id(),
resource_id,
deposited
)
return deposited
func get_carried_amount(item_id: StringName) -> float:
return state.get_inventory_amount(item_id)
func respawn() -> void:
+68
View File
@@ -7,6 +7,7 @@ const STACK_THRESHOLD := 90.0
const DEATH_STANDING_PENALTY := 0.5
const RESET_HUNGER := 20.0
const RESET_ENERGY := 70.0
const DEFAULT_CARRY_CAPACITY := 4.0
var data: Dictionary
@@ -28,6 +29,8 @@ static func create() -> PlayerStateRecord:
"starvation_death_threshold": 600,
"is_dead": false,
"deaths": 0,
"inventory": {},
"carry_capacity": DEFAULT_CARRY_CAPACITY,
}
)
)
@@ -68,6 +71,28 @@ static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord:
or deaths < 0
):
return null
var inventory := {}
var carry_capacity := DEFAULT_CARRY_CAPACITY
if record_data.has("inventory"):
if not record_data["inventory"] is Dictionary:
return null
var normalized := {}
for raw_item_id in record_data["inventory"]:
var item_id := String(raw_item_id)
var amount := float(record_data["inventory"][raw_item_id])
if (
item_id.is_empty()
or normalized.has(item_id)
or not is_finite(amount)
or amount < 0.0
):
return null
normalized[item_id] = amount
inventory = normalized
if record_data.has("carry_capacity"):
carry_capacity = float(record_data["carry_capacity"])
if not is_finite(carry_capacity) or carry_capacity < 0.0:
return null
return (
PlayerStateRecord
. new(
@@ -80,6 +105,8 @@ static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord:
"starvation_death_threshold": death_threshold,
"is_dead": bool(record_data["is_dead"]),
"deaths": deaths,
"inventory": inventory,
"carry_capacity": carry_capacity,
}
)
)
@@ -109,6 +136,47 @@ func get_deaths() -> int:
return int(data["deaths"])
func get_inventory_amount(item_id: StringName) -> float:
return float(data["inventory"].get(String(item_id), 0.0))
func get_carry_capacity() -> float:
return float(data["carry_capacity"])
func get_carried_total() -> float:
var total := 0.0
for amount in data["inventory"].values():
total += float(amount)
return total
func get_available_carry() -> float:
return maxf(get_carry_capacity() - get_carried_total(), 0.0)
func add_inventory(item_id: StringName, amount: float) -> float:
if item_id.is_empty() or not is_finite(amount) or amount <= 0.0 or is_dead():
return 0.0
var accepted := minf(amount, get_available_carry())
if accepted <= 0.0:
return 0.0
data["inventory"][String(item_id)] = get_inventory_amount(item_id) + accepted
return accepted
func remove_inventory(item_id: StringName, requested_amount: float) -> float:
if item_id.is_empty() or not is_finite(requested_amount) or requested_amount <= 0.0:
return 0.0
var removed := minf(requested_amount, get_inventory_amount(item_id))
if removed <= 0.0:
return 0.0
data["inventory"][String(item_id)] = get_inventory_amount(item_id) - removed
if is_equal_approx(get_inventory_amount(item_id), 0.0):
data["inventory"].erase(String(item_id))
return removed
func eat(amount: float) -> float:
if amount <= 0.0 or is_dead():
return 0.0
@@ -772,6 +772,8 @@ static func _is_valid_supply_resolution(
):
return false
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
if actor_id < 0:
return StringName(event.data["source_id"]) == &"player_carry"
return (
npc_ids.has(actor_id)
and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id)
+13 -2
View File
@@ -298,17 +298,28 @@ func _run() -> void:
opportunity_bush_state.set_amount_remaining(1.0)
player.global_position = opportunity_bush.interaction_point.global_position
player.call("try_interact")
var carried_after_gather: float = simulation_manager.get_player_state().get_inventory_amount(
SimulationIds.RESOURCE_FOOD
)
var pantry_position: Vector3 = (
main_scene
. get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry")
. get_interaction_position()
)
player.global_position = pantry_position
player.call("try_interact")
village_ui.selected_npc_index = contributor.id
village_ui.call("_refresh_npc_inspector")
_check(
(
player_opportunity.get_status() == OpportunityStateRecord.STATUS_RESOLVED
is_equal_approx(carried_after_gather, 1.0)
and player_opportunity.get_status() == OpportunityStateRecord.STATUS_RESOLVED
and "Resolved village need" in inspector_label.text
and "◆ Player restocked the pantry" in inspector_label.text
and whisper_kicker.text == "NEED MET"
and "The player restocked the pantry" in whisper_message.text
),
"Player gathering should resolve the restored need in debug detail and transient HUD"
"Player gathering and depositing should resolve the restored need in debug detail and transient HUD"
)
var woodpile_state: StorageStateRecord = simulation_manager.get_woodpile()
+188
View File
@@ -0,0 +1,188 @@
extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var manager := _create_manager()
var actor: SimNPC = manager.npcs[0]
var interested: SimNPC = manager.npcs[2]
var berry := _register_resource(
manager,
&"carry_berry_bush",
SimulationIds.ACTION_GATHER_FOOD,
SimulationIds.RESOURCE_FOOD,
3.0
)
var pantry: StorageStateRecord = manager.get_pantry()
_set_pantry_amount(manager, 1.0)
for npc in manager.npcs:
npc.position = Vector3(40.0 + npc.id * 10.0, 0.0, 0.0)
actor.position = Vector3.ZERO
interested.position = Vector3(4.0, 0.0, 0.0)
actor.hunger = 60.0
interested.hunger = 90.0
manager.economy.withdraw_to_inventory(actor, SimulationIds.RESOURCE_FOOD, 1.0)
var quest: PlayerQuestRecord = manager.get_active_player_quest()
_check(quest != null, "The carry loop needs an open player quest")
if quest == null:
berry.free()
manager.free()
_finish()
return
var state: PlayerStateRecord = manager.get_player_state()
var carried: float = manager.player_gather(berry)
_check(
(
is_equal_approx(carried, 2.0)
and is_equal_approx(state.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 2.0)
and is_equal_approx(manager.get_pantry().get_amount(SimulationIds.RESOURCE_FOOD), 0.0)
),
"Gathering should fill the player's hands instead of the pantry directly",
)
var capacity_limited: float = manager.player_gather(berry)
_check(
(
is_equal_approx(capacity_limited, 1.0)
and is_equal_approx(state.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 3.0)
),
"Remaining resource amount should cap the second gather",
)
var hands_full: float = manager.player_gather(berry)
_check(
(
is_equal_approx(hands_full, 0.0)
and is_equal_approx(state.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 3.0)
),
"An empty resource should refuse further gathering",
)
var pantry_before: float = manager.get_pantry().get_amount(SimulationIds.RESOURCE_FOOD)
var deposited: float = manager.player_deposit(SimulationIds.RESOURCE_FOOD)
_check(
(
is_equal_approx(deposited, 3.0)
and is_equal_approx(
manager.get_pantry().get_amount(SimulationIds.RESOURCE_FOOD), pantry_before + 3.0
)
and is_equal_approx(state.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0)
and manager.get_active_player_quest() == null
and (
manager.get_latest_player_quest_for_requester(interested.id).get_status()
== PlayerQuestRecord.STATUS_COMPLETED
)
),
"Depositing carried food should resolve the quest through the real pantry transaction",
)
var saved_json: String = manager.serialize_state()
var restored := _create_manager(902)
_check(
restored.restore_state_from_json(saved_json),
"The emptied carry state should restore through the current schema"
)
_check(
(
restored.get_state_checksum() == manager.get_state_checksum()
and is_equal_approx(
restored.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0
)
),
"Restore should preserve the player's empty carried inventory and checksum",
)
var carry_state_saved: String = manager.serialize_state()
manager.player_gather(berry)
var mid_carry: String = manager.serialize_state()
var mid_restored := _create_manager(903)
_check(
mid_restored.restore_state_from_json(mid_carry),
"A partially carried inventory should restore through the current schema"
)
_check(
(
mid_restored.get_state_checksum() == manager.get_state_checksum()
and is_equal_approx(
mid_restored.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD),
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD)
)
),
"Restore should preserve the player's carried inventory mid-transport",
)
berry.free()
manager.free()
restored.free()
mid_restored.free()
_finish()
func _register_resource(
manager: Node,
node_id: StringName,
action_id: StringName,
resource_id: StringName,
amount: float
) -> ResourceNode:
var node := ResourceNode.new()
node.name = "CarryResource"
node.node_id = node_id
node.action_id = action_id
node.resource_id = resource_id
node.initial_amount = amount
node.yield_per_action = 2.0
node.debug_label_enabled = false
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
node.add_child(interaction_point)
root.add_child(node)
_check(
manager.register_resource_node(node),
"The carry loop should bind a real finite ResourceNode"
)
return node
func _create_manager(seed_value: int = 901) -> 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 _set_pantry_amount(manager: Node, amount: float) -> void:
var pantry: StorageStateRecord = manager.get_pantry()
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
pantry.deposit(SimulationIds.RESOURCE_FOOD, amount)
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
func _finish() -> void:
if failures.is_empty():
print("[TEST] Player carry and deposit loop passed")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)
+1
View File
@@ -0,0 +1 @@
uid://bwyg38d3ieiex
+66 -23
View File
@@ -35,11 +35,17 @@ func _run() -> void:
_check(
is_equal_approx(bush_state.get_amount_remaining(), 0.0),
"Player should deplete the nearby bush"
"Player should deplete the nearby bush into their hands"
)
var carried_after_gather: float = simulation_manager.get_player_state().get_inventory_amount(
SimulationIds.RESOURCE_FOOD
)
_check(
is_equal_approx(simulation_manager.village.food, food_before + 1.0),
"Village should receive exactly the bush's remaining food"
(
is_equal_approx(carried_after_gather, 1.0)
and is_equal_approx(simulation_manager.village.food, food_before)
),
"Gathering should fill the player's hands without bypassing the village store",
)
_check(bush_state.get_reserved_by() == -1, "Depletion should release the NPC reservation")
var player_extraction: EconomicEventRecord
@@ -65,24 +71,41 @@ 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()
player.global_position = pantry.get_interaction_position()
player.try_interact()
var food_after_deposit: float = simulation_manager.village.food
_check(
(
is_equal_approx(food_after_deposit, food_before + 1.0)
and is_equal_approx(
simulation_manager.get_player_state().get_inventory_amount(
SimulationIds.RESOURCE_FOOD
),
0.0
)
),
"Depositing at the typed pantry should deliver the carried food to the village",
)
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()
var carried_wood: float = simulation_manager.get_player_state().get_inventory_amount(
SimulationIds.RESOURCE_WOOD
)
_check(
(
is_equal_approx(tree_state.get_amount_remaining(), tree_before - tree.yield_per_action)
and is_equal_approx(carried_wood, tree.yield_per_action)
),
"Player should extract the tree's configured yield into their hands",
)
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, pantry_state.get_available_capacity() - 0.5)
simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
bush_state.set_amount_remaining(1.0)
@@ -90,20 +113,40 @@ func _run() -> void:
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)
is_equal_approx(bush_state.get_amount_remaining(), 0.0)
and is_equal_approx(
simulation_manager.get_player_state().get_inventory_amount(
SimulationIds.RESOURCE_FOOD
),
"Player harvesting should leave overflow at its source when storage fills"
1.0
)
),
"Player gathering should still extract into their hands regardless of storage capacity",
)
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, 3.0)
pantry_state.withdraw(
SimulationIds.RESOURCE_FOOD, pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
)
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, 5.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()
var food_after_first_deposit := pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
var food_carried_after_first: float = (
simulation_manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD)
)
_check(
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < pantry_food_before,
"Player should eat from the typed pantry StorageNode"
(
food_after_first_deposit > pantry_food_before
and is_equal_approx(food_carried_after_first, 0.0)
),
"Player should deposit carried food at the typed pantry",
)
player.try_interact()
_check(
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < food_after_first_deposit,
"Player should eat from the pantry once their hands are empty",
)
var guard_site := (
main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite