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="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) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
script = ExtResource("1_h2yge") script = ExtResource("1_h2yge")
camera_rig = NodePath("../CameraRig") camera_rig = NodePath("../CameraRig")
simulation_manager = NodePath("../SimulationManager") simulation_manager = NodePath("../SimulationManager")
world_view_manager = NodePath("../WorldViewManager") world_view_manager = NodePath("../WorldViewManager")
pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry") pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
woodpile_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillageWoodpile")
guard_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/GuardPost") guard_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/GuardPost")
study_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/StudyDesk") study_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/StudyDesk")
stomach_capacity_for_food = 10 stomach_capacity_for_food = 10
+1
View File
@@ -4,6 +4,7 @@ extends RefCounted
const KIND_ANIMAL := &"animal" const KIND_ANIMAL := &"animal"
const KIND_RESOURCE := &"resource" const KIND_RESOURCE := &"resource"
const KIND_PANTRY := &"pantry" const KIND_PANTRY := &"pantry"
const KIND_DEPOSIT := &"deposit"
const KIND_GUARD := &"guard" const KIND_GUARD := &"guard"
const KIND_STUDY := &"study" 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 world_view_manager: Node
@export var pantry_storage: StorageNode @export var pantry_storage: StorageNode
@export var woodpile_storage: StorageNode
@export var guard_site: ActivitySite @export var guard_site: ActivitySite
@export var study_site: ActivitySite @export var study_site: ActivitySite
@@ -69,6 +70,8 @@ func try_interact() -> void:
_execute_animal_interaction(context) _execute_animal_interaction(context)
PlayerInteractionResult.KIND_RESOURCE: PlayerInteractionResult.KIND_RESOURCE:
_execute_resource_interaction(context) _execute_resource_interaction(context)
PlayerInteractionResult.KIND_DEPOSIT:
_execute_deposit_interaction(context)
PlayerInteractionResult.KIND_GUARD: PlayerInteractionResult.KIND_GUARD:
simulation_manager.add_safety(3.0) simulation_manager.add_safety(3.0)
interaction_feedback.emit( interaction_feedback.emit(
@@ -112,6 +115,20 @@ func get_interaction_context() -> PlayerInteractionResult:
"Work here · +2 knowledge", "Work here · +2 knowledge",
study_site 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): if is_near_storage(pantry_storage):
return _build_pantry_context() return _build_pantry_context()
return null 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() definition.display_name if definition != null else String(node.action_id).capitalize()
) )
var display_name := String(node.node_id).replace("_", " ").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( return PlayerInteractionResult.new(
PlayerInteractionResult.KIND_RESOURCE, PlayerInteractionResult.KIND_RESOURCE,
node.action_id, node.action_id,
node.node_id, node.node_id,
display_name, display_name,
prompt, prompt,
"%s · %.1f remaining" % [display_name, node.get_amount_remaining()], detail,
node 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: func _execute_resource_interaction(context: PlayerInteractionResult) -> void:
var node := context.target_node as ResourceNode var node := context.target_node as ResourceNode
if node == null or not simulation_manager.has_method("harvest_resource_node"): if node == null or not simulation_manager.has_method("player_gather"):
push_error("Player: SimulationManager cannot harvest the resolved ResourceNode") push_error("Player: SimulationManager cannot gather the resolved ResourceNode")
return return
var extracted: float = simulation_manager.harvest_resource_node(node) var extracted: float = simulation_manager.player_gather(node)
if extracted <= 0.0: if extracted <= 0.0:
interaction_feedback.emit( 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 return
var destination := "pantry" if node.resource_id == SimulationIds.RESOURCE_FOOD else "woodpile" var carried := _get_player_carried(node.resource_id)
interaction_feedback.emit( interaction_feedback.emit(
"%s gathered" % String(node.resource_id).capitalize(), "%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 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: func _execute_pantry_interaction(context: PlayerInteractionResult) -> void:
if not context.is_available(): if not context.is_available():
interaction_feedback.emit("The pantry is empty", context.blocked_reason, false) 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 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: func is_near_storage(storage_node: StorageNode) -> bool:
if storage_node == null: if storage_node == null:
return false return false
+43 -29
View File
@@ -30,6 +30,7 @@ signal player_quest_expired(quest: PlayerQuestRecord)
signal player_standing_changed(standing: PlayerStandingRecord) signal player_standing_changed(standing: PlayerStandingRecord)
signal player_tier_advanced(tier: int, tier_name: String) signal player_tier_advanced(tier: int, tier_name: String)
signal player_needs_changed(state: PlayerStateRecord) signal player_needs_changed(state: PlayerStateRecord)
signal player_inventory_changed(state: PlayerStateRecord)
signal player_died(state: PlayerStateRecord) signal player_died(state: PlayerStateRecord)
var village := SimVillage.new() var village := SimVillage.new()
@@ -85,6 +86,8 @@ func _ready() -> void:
event_log.event_recorded.connect(_on_economic_event_recorded) event_log.event_recorded.connect(_on_economic_event_recorded)
event_recorder.configure(event_log, Callable(self, "get_tick_count_for_recording")) event_recorder.configure(event_log, Callable(self, "get_tick_count_for_recording"))
event_recorder.set_npcs(npcs) 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.inventory_changed.connect(_on_economy_inventory_changed)
economy.economic_event_requested.connect(event_recorder.record_economic) economy.economic_event_requested.connect(event_recorder.record_economic)
economy.narrative_event_requested.connect(event_recorder.record_narrative) 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: for learned_record in learned_records:
_apply_new_event_knowledge(learned_record, event) _apply_new_event_knowledge(learned_record, event)
if StringName(event.data["event_type"]) == SimulationIds.EVENT_ANIMAL_FED: 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) _update_opportunity_from_event(event)
economic_event_recorded.emit(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: func _finish_quest_resolution(resolution: Dictionary) -> void:
if not resolution.has("quest"): if not resolution.has("quest"):
return return
var quest: PlayerQuestRecord = resolution["quest"] var quest: PlayerQuestRecord = resolution["quest"]
if resolution["completed"]: if not resolution["completed"]:
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) 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())
func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bool: 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) var listener := _find_npc_by_id(listener_id)
if not _can_communicate_at_shared_activity(speaker, listener): if not _can_communicate_at_shared_activity(speaker, listener):
return false 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, speaker.id,
_get_lasting_event_ids(speaker.id), _get_lasting_event_ids(speaker.id),
opportunity_system.get_open_trigger_event_id() opportunity_system.get_open_trigger_event_id()
) ):
for event_id in known_event_ids:
var event := event_log.get_by_id(event_id) var event := event_log.get_by_id(event_id)
if event == null: if event == null:
continue continue
@@ -742,22 +742,21 @@ func get_active_player_quest() -> PlayerQuestRecord:
func _advance_player_needs() -> void: func _advance_player_needs() -> void:
if not player_needs.advance(village): if player_needs.advance(village):
player_needs_changed.emit(player_needs.state) 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 return
player_died.emit(player_needs.state) player_needs_changed.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)
func respawn_player() -> bool: func respawn_player() -> bool:
var was_dead := player_needs.state.is_dead() if not player_needs.state.is_dead():
return false
player_needs.respawn() player_needs.respawn()
if was_dead and not player_needs.state.is_dead(): player_needs_changed.emit(player_needs.state)
player_needs_changed.emit(player_needs.state) return true
return true
return false
func get_player_state() -> PlayerStateRecord: func get_player_state() -> PlayerStateRecord:
@@ -771,6 +770,21 @@ func player_eat(amount: float) -> float:
return eaten 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: func _consider_animal_care_quests() -> void:
var pantry: StorageStateRecord = get_pantry() var pantry: StorageStateRecord = get_pantry()
for animal_state in animal_care.get_all_states(): 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 event_type := StringName(event.data["event_type"])
var actor_id := int(event.data["actor_id"]) var actor_id := int(event.data["actor_id"])
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED: if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
if actor_id < 0:
return StringName(event.data["source_id"]) == &"player_carry"
return ( return (
_find_npc(actor_id, npcs) != null _find_npc(actor_id, npcs) != null
and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id) 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 extends RefCounted
var state := PlayerStateRecord.create() 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: func advance(village: SimVillage) -> bool:
@@ -15,8 +27,80 @@ func advance(village: SimVillage) -> bool:
func eat(amount: float) -> float: func eat(amount: float) -> float:
var eaten := state.eat(amount) return state.eat(amount)
return eaten
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: func respawn() -> void:
+68
View File
@@ -7,6 +7,7 @@ const STACK_THRESHOLD := 90.0
const DEATH_STANDING_PENALTY := 0.5 const DEATH_STANDING_PENALTY := 0.5
const RESET_HUNGER := 20.0 const RESET_HUNGER := 20.0
const RESET_ENERGY := 70.0 const RESET_ENERGY := 70.0
const DEFAULT_CARRY_CAPACITY := 4.0
var data: Dictionary var data: Dictionary
@@ -28,6 +29,8 @@ static func create() -> PlayerStateRecord:
"starvation_death_threshold": 600, "starvation_death_threshold": 600,
"is_dead": false, "is_dead": false,
"deaths": 0, "deaths": 0,
"inventory": {},
"carry_capacity": DEFAULT_CARRY_CAPACITY,
} }
) )
) )
@@ -68,6 +71,28 @@ static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord:
or deaths < 0 or deaths < 0
): ):
return null 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 ( return (
PlayerStateRecord PlayerStateRecord
. new( . new(
@@ -80,6 +105,8 @@ static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord:
"starvation_death_threshold": death_threshold, "starvation_death_threshold": death_threshold,
"is_dead": bool(record_data["is_dead"]), "is_dead": bool(record_data["is_dead"]),
"deaths": deaths, "deaths": deaths,
"inventory": inventory,
"carry_capacity": carry_capacity,
} }
) )
) )
@@ -109,6 +136,47 @@ func get_deaths() -> int:
return int(data["deaths"]) 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: func eat(amount: float) -> float:
if amount <= 0.0 or is_dead(): if amount <= 0.0 or is_dead():
return 0.0 return 0.0
@@ -772,6 +772,8 @@ static func _is_valid_supply_resolution(
): ):
return false return false
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED: if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
if actor_id < 0:
return StringName(event.data["source_id"]) == &"player_carry"
return ( return (
npc_ids.has(actor_id) npc_ids.has(actor_id)
and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(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) opportunity_bush_state.set_amount_remaining(1.0)
player.global_position = opportunity_bush.interaction_point.global_position player.global_position = opportunity_bush.interaction_point.global_position
player.call("try_interact") 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.selected_npc_index = contributor.id
village_ui.call("_refresh_npc_inspector") village_ui.call("_refresh_npc_inspector")
_check( _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 "Resolved village need" in inspector_label.text
and "◆ Player restocked the pantry" in inspector_label.text and "◆ Player restocked the pantry" in inspector_label.text
and whisper_kicker.text == "NEED MET" and whisper_kicker.text == "NEED MET"
and "The player restocked the pantry" in whisper_message.text 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() 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( _check(
is_equal_approx(bush_state.get_amount_remaining(), 0.0), 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( _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") _check(bush_state.get_reserved_by() == -1, "Depletion should release the NPC reservation")
var player_extraction: EconomicEventRecord var player_extraction: EconomicEventRecord
@@ -65,24 +71,41 @@ func _run() -> void:
"Player depletion should preserve the resource interaction position" "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 := ( var pantry := (
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
) )
var pantry_state: StorageStateRecord = simulation_manager.get_pantry() 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) pantry_state.deposit(SimulationIds.RESOURCE_FOOD, pantry_state.get_available_capacity() - 0.5)
simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD) simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
bush_state.set_amount_remaining(1.0) bush_state.set_amount_remaining(1.0)
@@ -90,20 +113,40 @@ func _run() -> void:
player.try_interact() player.try_interact()
_check( _check(
( (
is_equal_approx(bush_state.get_amount_remaining(), 0.5) 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 should leave overflow at its source when storage fills" "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) simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
var pantry_food_before := pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) var pantry_food_before := pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
player.global_position = pantry.get_interaction_position() player.global_position = pantry.get_interaction_position()
player.try_interact() 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( _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 := ( var guard_site := (
main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite