feat: give the player a real carried inventory and deposit transactions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"]:
|
||||
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:
|
||||
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())
|
||||
|
||||
|
||||
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)
|
||||
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_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)
|
||||
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
|
||||
player_needs_changed.emit(player_needs.state)
|
||||
return true
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bwyg38d3ieiex
|
||||
@@ -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
|
||||
),
|
||||
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)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user