feat: let the player live as a citizen with needs and carried inventory

This commit is contained in:
Rijad Zuzo
2026-08-11 00:18:39 +02:00
parent f72aa659d2
commit 37f262fb84
20 changed files with 746 additions and 90 deletions
+37 -1
View File
@@ -15,6 +15,7 @@
[ext_resource type="Script" uid="uid://dwwxux24jc6yq" path="res://world/ui/village_whisper_hud.gd" id="15_whisper"] [ext_resource type="Script" uid="uid://dwwxux24jc6yq" path="res://world/ui/village_whisper_hud.gd" id="15_whisper"]
[ext_resource type="Script" path="res://world/ui/player_interaction_hud.gd" id="16_interaction"] [ext_resource type="Script" path="res://world/ui/player_interaction_hud.gd" id="16_interaction"]
[ext_resource type="Script" path="res://world/ui/villager_field_note_hud.gd" id="17_villager_note"] [ext_resource type="Script" path="res://world/ui/villager_field_note_hud.gd" id="17_villager_note"]
[ext_resource type="Script" path="res://world/ui/player_status_hud.gd" id="18_status"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
radius = 0.4 radius = 0.4
@@ -24,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
@@ -195,6 +197,40 @@ theme_override_font_sizes/font_size = 13
text = "Context" text = "Context"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="PlayerStatusLayer" type="CanvasLayer" parent="."]
layer = 2
[node name="PlayerStatusHud" type="Control" parent="PlayerStatusLayer" node_paths=PackedStringArray("simulation_manager")]
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -240.0
offset_top = -170.0
offset_right = 240.0
offset_bottom = -134.0
grow_horizontal = 2
grow_vertical = 0
mouse_filter = 2
script = ExtResource("18_status")
simulation_manager = NodePath("../../SimulationManager")
[node name="StatusLabel" type="Label" parent="PlayerStatusLayer/PlayerStatusHud"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
theme_override_colors/font_color = Color(0.95, 0.88, 0.74, 1)
theme_override_colors/font_outline_color = Color(0.07, 0.045, 0.025, 0.92)
theme_override_constants/outline_size = 5
theme_override_font_sizes/font_size = 14
text = "Hunger 40 · Energy 100 · Carrying nothing"
horizontal_alignment = 1
[node name="VillagerInspectionLayer" type="CanvasLayer" parent="."] [node name="VillagerInspectionLayer" type="CanvasLayer" parent="."]
layer = 2 layer = 2
+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_STORAGE_DEPOSIT := &"storage_deposit"
const KIND_GUARD := &"guard" const KIND_GUARD := &"guard"
const KIND_STUDY := &"study" const KIND_STUDY := &"study"
+90 -10
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
@@ -81,6 +82,8 @@ func try_interact() -> void:
) )
PlayerInteractionResult.KIND_PANTRY: PlayerInteractionResult.KIND_PANTRY:
_execute_pantry_interaction(context) _execute_pantry_interaction(context)
PlayerInteractionResult.KIND_STORAGE_DEPOSIT:
_execute_deposit_interaction(context)
func get_interaction_context() -> PlayerInteractionResult: func get_interaction_context() -> PlayerInteractionResult:
@@ -113,7 +116,12 @@ func get_interaction_context() -> PlayerInteractionResult:
study_site study_site
) )
if is_near_storage(pantry_storage): if is_near_storage(pantry_storage):
if _get_carried_food() > 0.0:
return _build_deposit_context(SimulationIds.RESOURCE_FOOD, pantry_storage)
return _build_pantry_context() return _build_pantry_context()
if woodpile_storage != null and is_near_storage(woodpile_storage):
if _get_carried_wood() > 0.0:
return _build_deposit_context(SimulationIds.RESOURCE_WOOD, woodpile_storage)
return null return null
@@ -346,10 +354,15 @@ func _build_resource_context(node: ResourceNode) -> PlayerInteractionResult:
func _build_pantry_context() -> PlayerInteractionResult: func _build_pantry_context() -> PlayerInteractionResult:
var pantry_food := _get_pantry_food() var pantry_food := _get_pantry_food()
var blocked_reason := "" if pantry_food > 0.0 else "The village pantry is empty." var carried_food := _get_carried_food()
var blocked_reason := ""
var detail := "%.0f food available" % pantry_food var detail := "%.0f food available" % pantry_food
if not blocked_reason.is_empty(): if pantry_food <= 0.0 and carried_food < 1.0:
detail = "Pantry empty" blocked_reason = "The village pantry is empty and you carry no food."
detail = "Pantry empty · carry nothing"
elif pantry_food <= 0.0:
blocked_reason = "The village pantry is empty."
detail = "Pantry empty · carry %.0f food" % carried_food
return PlayerInteractionResult.new( return PlayerInteractionResult.new(
PlayerInteractionResult.KIND_PANTRY, PlayerInteractionResult.KIND_PANTRY,
SimulationIds.ACTION_EAT, SimulationIds.ACTION_EAT,
@@ -362,6 +375,29 @@ func _build_pantry_context() -> PlayerInteractionResult:
) )
func _build_deposit_context(
resource_id: StringName, storage_node: StorageNode
) -> PlayerInteractionResult:
var resource_name := String(resource_id).capitalize()
var carried := (
_get_carried_food() if resource_id == SimulationIds.RESOURCE_FOOD else _get_carried_wood()
)
var action_id := (
SimulationIds.ACTION_DEPOSIT_FOOD
if resource_id == SimulationIds.RESOURCE_FOOD
else SimulationIds.ACTION_DEPOSIT_WOOD
)
return PlayerInteractionResult.new(
PlayerInteractionResult.KIND_STORAGE_DEPOSIT,
action_id,
storage_node.storage_id,
storage_node.display_name,
"Deposit %s" % resource_name.to_lower(),
"%s · carrying %.0f" % [storage_node.display_name, carried],
storage_node
)
func _execute_animal_interaction(context: PlayerInteractionResult) -> void: func _execute_animal_interaction(context: PlayerInteractionResult) -> void:
var node := context.target_node as AnimalNode var node := context.target_node as AnimalNode
if node == null or not context.is_available(): if node == null or not context.is_available():
@@ -401,10 +437,12 @@ func _execute_resource_interaction(context: PlayerInteractionResult) -> void:
"%s is unavailable" % context.display_name, "Nothing could be gathered.", false "%s is unavailable" % context.display_name, "Nothing could be gathered.", false
) )
return return
var destination := "pantry" if node.resource_id == SimulationIds.RESOURCE_FOOD else "woodpile"
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 pack — deposit it at the %s."
% [extracted, _storage_name_for(node.resource_id)]
),
true true
) )
@@ -413,12 +451,42 @@ 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)
return return
var food_before := _get_pantry_food() if not simulation_manager.has_method("eat_player_food"):
simulation_manager.eat_food(stomach_capacity_for_food) push_error("Player: SimulationManager cannot consume player food")
var consumed := food_before - _get_pantry_food() return
interaction_feedback.emit( if simulation_manager.eat_player_food():
"Ate from the pantry", "%.0f village food consumed." % consumed, consumed > 0.0 interaction_feedback.emit("Ate", "Hunger eased from carried or pantry food.", true)
return
interaction_feedback.emit("Nothing to eat", "The village pantry is empty.", false)
func _execute_deposit_interaction(context: PlayerInteractionResult) -> void:
if not simulation_manager.has_method("deposit_player_inventory"):
push_error("Player: SimulationManager cannot deposit player inventory")
return
var resource_id := (
SimulationIds.RESOURCE_FOOD
if context.action_id == SimulationIds.ACTION_DEPOSIT_FOOD
else SimulationIds.RESOURCE_WOOD
) )
var deposited: float = simulation_manager.deposit_player_inventory(resource_id)
var resource_name := String(resource_id).capitalize()
if deposited <= 0.0:
interaction_feedback.emit(
"%s not stored" % resource_name,
"The %s is full." % context.display_name.to_lower(),
false
)
return
interaction_feedback.emit(
"%s stored" % resource_name,
"%.0f moved to the %s." % [deposited, context.display_name.to_lower()],
true
)
func _storage_name_for(resource_id: StringName) -> String:
return "village pantry" if resource_id == SimulationIds.RESOURCE_FOOD else "village woodpile"
func _get_animal_feed_cost() -> float: func _get_animal_feed_cost() -> float:
@@ -433,6 +501,18 @@ 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_carried_food() -> float:
if not simulation_manager.has_method("get_player_state"):
return 0.0
return simulation_manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD)
func _get_carried_wood() -> float:
if not simulation_manager.has_method("get_player_state"):
return 0.0
return simulation_manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_WOOD)
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
+62 -27
View File
@@ -1,5 +1,9 @@
extends Node extends Node
# gdlint: disable=max-file-lines
# The manager is the deliberate scene-tree facade for the simulation slice and
# intentionally keeps its orchestration readable in one authoritative file.
const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd") const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd") const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
const AnimalCareSystemScript := preload("res://simulation/animals/animal_care_system.gd") const AnimalCareSystemScript := preload("res://simulation/animals/animal_care_system.gd")
@@ -25,6 +29,7 @@ signal opportunity_resolved(opportunity: OpportunityStateRecord, cause_event: Ec
signal opportunity_invalidated(opportunity: OpportunityStateRecord) signal opportunity_invalidated(opportunity: OpportunityStateRecord)
var village := SimVillage.new() var village := SimVillage.new()
var player_system := PlayerCitizenSystem.new()
var npcs: Array[SimNPC] = [] var npcs: Array[SimNPC] = []
@export var tick_interval := 1.2 @export var tick_interval := 1.2
@@ -89,6 +94,7 @@ func _ready() -> void:
village.debug_logs = debug_logs village.debug_logs = debug_logs
economy.configure(village, debug_logs) economy.configure(village, debug_logs)
animal_care.configure(economy, active_world_adapter) animal_care.configure(economy, active_world_adapter)
player_system.configure(economy, _record_player_event_at)
economy.initialize_storage() economy.initialize_storage()
village.update_modifiers() village.update_modifiers()
village.update_priorities() village.update_priorities()
@@ -164,6 +170,7 @@ func simulate_tick() -> void:
if debug_logs: if debug_logs:
print("--- Tick ", tick_count, " ---") print("--- Tick ", tick_count, " ---")
animal_care.advance(tick_count) animal_care.advance(tick_count)
advance_player_needs()
var village_was_changed := false var village_was_changed := false
_population_view.rebuild(npcs) _population_view.rebuild(npcs)
for npc in npcs: for npc in npcs:
@@ -984,9 +991,6 @@ func find_resource_node_for_player(from_position: Vector3, max_distance: float)
or not resource_state.can_extract() or not resource_state.can_extract()
): ):
continue continue
var storage := economy.get_storage_for_resource(resource_state.get_resource_id())
if storage == null or storage.get_available_capacity() <= 0.0:
continue
var distance := from_position.distance_squared_to(node.interaction_point.global_position) var distance := from_position.distance_squared_to(node.interaction_point.global_position)
if distance <= best_distance: if distance <= best_distance:
best_distance = distance best_distance = distance
@@ -1000,16 +1004,12 @@ func harvest_resource_node(node: ResourceNode) -> float:
var resource_state := get_resource_state(node.node_id) var resource_state := get_resource_state(node.node_id)
if resource_state == null or not resource_state.can_player_use_resource(): if resource_state == null or not resource_state.can_player_use_resource():
return 0.0 return 0.0
var storage := economy.get_storage_for_resource(resource_state.get_resource_id()) var extracted := resource_state.extract(resource_state.get_yield_per_action())
if storage == null:
return 0.0
var extracted := resource_state.extract(
minf(resource_state.get_yield_per_action(), storage.get_available_capacity())
)
if extracted <= 0.0: if extracted <= 0.0:
return 0.0 return 0.0
var resource_id := resource_state.get_resource_id()
player_system.add_carried(resource_id, extracted)
var deposited := economy.deposit_resource(resource_state.get_resource_id(), extracted)
var event_position := ( var event_position := (
node.interaction_point.global_position node.interaction_point.global_position
if node.interaction_point != null if node.interaction_point != null
@@ -1017,25 +1017,17 @@ func harvest_resource_node(node: ResourceNode) -> float:
) )
_record_economic_event_at( _record_economic_event_at(
SimulationIds.EVENT_RESOURCE_EXTRACTED, SimulationIds.EVENT_RESOURCE_EXTRACTED,
-1, SimulationIds.PLAYER_ACTOR_ID,
resource_state.get_node_id(), resource_state.get_node_id(),
( SimulationIds.PLAYER_INVENTORY_ID,
SimulationIds.STORAGE_VILLAGE_PANTRY resource_id,
if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD extracted,
else (
SimulationIds.STORAGE_VILLAGE_WOODPILE
if resource_state.get_resource_id() == SimulationIds.RESOURCE_WOOD
else &"village"
)
),
resource_state.get_resource_id(),
deposited,
event_position event_position
) )
if resource_state.get_amount_remaining() <= 0.0: if resource_state.get_amount_remaining() <= 0.0:
_record_narrative_event_at( _record_narrative_event_at(
SimulationIds.EVENT_RESOURCE_DEPLETED, SimulationIds.EVENT_RESOURCE_DEPLETED,
-1, SimulationIds.PLAYER_ACTOR_ID,
resource_state.get_node_id(), resource_state.get_node_id(),
"", "",
event_position event_position
@@ -1044,20 +1036,60 @@ func harvest_resource_node(node: ResourceNode) -> float:
if debug_logs: if debug_logs:
print( print(
"[SimulationManager] Player extracted ", "[SimulationManager] Player carried ",
extracted, extracted,
" ", " ",
resource_state.get_resource_id(), resource_id,
" from ", " from ",
resource_state.get_node_id() resource_state.get_node_id()
) )
return extracted
func advance_player_needs() -> void:
player_system.advance_needs(village.food_modifier)
func get_player_state() -> PlayerStateRecord:
return player_system.player_state
func deposit_player_inventory(item_id: StringName) -> float:
var storage := economy.get_storage_for_resource(item_id)
var storage_node := (
StorageNode.get_by_id(storage.get_storage_id()) as StorageNode if storage != null else null
)
var deposited := player_system.deposit(
item_id, storage_node.get_interaction_position() if storage_node != null else Vector3.ZERO
)
village_changed.emit(village)
return deposited return deposited
func eat_food(amount: float) -> void: func eat_player_food() -> bool:
economy.withdraw_resource(SimulationIds.RESOURCE_FOOD, amount) var succeeded := player_system.eat(Vector3.ZERO)
village_changed.emit(village) village_changed.emit(village)
return succeeded
func _record_player_event_at(
event_type: StringName,
source_id: StringName,
destination_id: StringName,
item_id: StringName,
amount: float,
world_position: Vector3
) -> void:
_record_economic_event_at(
event_type,
SimulationIds.PLAYER_ACTOR_ID,
source_id,
destination_id,
item_id,
amount,
world_position
)
func add_safety(amount: float) -> void: func add_safety(amount: float) -> void:
@@ -1125,6 +1157,7 @@ func create_state_record() -> SimulationStateRecord:
record.event_knowledge.append(known_event) record.event_knowledge.append(known_event)
for opportunity in opportunity_system.get_all_sorted(): for opportunity in opportunity_system.get_all_sorted():
record.opportunities.append(opportunity) record.opportunities.append(opportunity)
record.player = player_system.player_state
return record return record
@@ -1175,6 +1208,8 @@ func restore_state(record: SimulationStateRecord) -> bool:
resource_states[resource_record.get_node_id()] = resource_record resource_states[resource_record.get_node_id()] = resource_record
register_loaded_resource_nodes() register_loaded_resource_nodes()
animal_care.restore_state_records(record.animals, tick_count) animal_care.restore_state_records(record.animals, tick_count)
if record.player != null:
player_system.player_state = record.player
village_changed.emit(village) village_changed.emit(village)
state_restored.emit() state_restored.emit()
return true return true
+1
View File
@@ -2,6 +2,7 @@ class_name SimulationIds
extends RefCounted extends RefCounted
const PLAYER_ACTOR_ID := -1 const PLAYER_ACTOR_ID := -1
const PLAYER_INVENTORY_ID := &"player_inventory"
const ACTION_IDLE := &"idle" const ACTION_IDLE := &"idle"
const ACTION_DEAD := &"dead" const ACTION_DEAD := &"dead"
@@ -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 == SimulationIds.PLAYER_ACTOR_ID:
return StringName(event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID
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)
@@ -0,0 +1,80 @@
class_name PlayerCitizenSystem
extends RefCounted
var player_state := PlayerStateRecord.create_default()
var economy: RefCounted
var _record_event: Callable
func configure(economy_state: RefCounted, record_event: Callable) -> void:
economy = economy_state
_record_event = record_event
func advance_needs(food_modifier: float) -> void:
player_state.set_hunger(player_state.get_hunger() + 0.125 * food_modifier)
player_state.set_energy(maxf(player_state.get_energy() - 0.0625, 0.0))
func add_carried(item_id: StringName, amount: float) -> void:
player_state.add_inventory(item_id, amount)
func get_carried(item_id: StringName) -> float:
return player_state.get_inventory_amount(item_id)
func deposit(item_id: StringName, event_position: Vector3) -> float:
if item_id.is_empty():
return 0.0
var storage := _storage_for(item_id)
if storage == null:
return 0.0
var deposited := storage.deposit(item_id, player_state.get_inventory_amount(item_id))
player_state.remove_inventory(item_id, deposited)
economy.sync_resource(item_id)
if deposited > 0.0:
_record_event.call(
SimulationIds.EVENT_STORAGE_DEPOSITED,
SimulationIds.PLAYER_INVENTORY_ID,
storage.get_storage_id(),
item_id,
deposited,
event_position
)
return deposited
func eat(event_position: Vector3) -> bool:
if player_state.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
player_state.remove_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
_apply_meal(event_position, SimulationIds.PLAYER_INVENTORY_ID)
return true
var pantry := _storage_for(SimulationIds.RESOURCE_FOOD)
if pantry != null and pantry.get_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
var withdrawn := pantry.withdraw(SimulationIds.RESOURCE_FOOD, 1.0)
economy.sync_resource(SimulationIds.RESOURCE_FOOD)
if withdrawn <= 0.0:
return false
_apply_meal(event_position, pantry.get_storage_id())
return true
return false
func _apply_meal(event_position: Vector3, source_id: StringName) -> void:
player_state.set_hunger(maxf(player_state.get_hunger() - 55.0, 0.0))
player_state.set_energy(minf(player_state.get_energy() + 10.0, 100.0))
_record_event.call(
SimulationIds.EVENT_ITEM_CONSUMED,
source_id,
&"consumed",
SimulationIds.RESOURCE_FOOD,
1.0,
event_position
)
func _storage_for(item_id: StringName) -> StorageStateRecord:
if economy == null or not economy.has_method("get_storage_for_resource"):
return null
return economy.get_storage_for_resource(item_id) as StorageStateRecord
@@ -0,0 +1 @@
uid://vqve206mikpt
+35 -7
View File
@@ -2,7 +2,7 @@ class_name SimulationStateRecord
extends RefCounted extends RefCounted
const SCHEMA_NAME := "the_steward.simulation" const SCHEMA_NAME := "the_steward.simulation"
const SCHEMA_VERSION := 12 const SCHEMA_VERSION := 13
const LEGACY_SCHEMA_VERSION := 1 const LEGACY_SCHEMA_VERSION := 1
const EVENT_LEGACY_SCHEMA_VERSION := 2 const EVENT_LEGACY_SCHEMA_VERSION := 2
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3 const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
@@ -13,6 +13,7 @@ const OPPORTUNITY_LEGACY_SCHEMA_VERSION := 8
const ANIMAL_LEGACY_SCHEMA_VERSION := 9 const ANIMAL_LEGACY_SCHEMA_VERSION := 9
const ROUTINE_LEGACY_SCHEMA_VERSION := 10 const ROUTINE_LEGACY_SCHEMA_VERSION := 10
const PLAYER_RELATIONSHIP_SCHEMA_VERSION := 11 const PLAYER_RELATIONSHIP_SCHEMA_VERSION := 11
const PLAYER_NEEDS_SCHEMA_VERSION := 12
const PREVIOUS_SCHEMA_VERSION := 7 const PREVIOUS_SCHEMA_VERSION := 7
var simulation: Dictionary var simulation: Dictionary
@@ -25,6 +26,7 @@ var economic_events: Array[EconomicEventRecord] = []
var relationships: Array[RelationshipStateRecord] = [] var relationships: Array[RelationshipStateRecord] = []
var event_knowledge: Array[KnownEventStateRecord] = [] var event_knowledge: Array[KnownEventStateRecord] = []
var opportunities: Array[OpportunityStateRecord] = [] var opportunities: Array[OpportunityStateRecord] = []
var player: PlayerStateRecord
func to_dictionary() -> Dictionary: func to_dictionary() -> Dictionary:
@@ -66,7 +68,8 @@ func to_dictionary() -> Dictionary:
"economic_events": event_data, "economic_events": event_data,
"relationships": relationship_data, "relationships": relationship_data,
"event_knowledge": knowledge_data, "event_knowledge": knowledge_data,
"opportunities": opportunity_data "opportunities": opportunity_data,
"player": player.to_dictionary()
} }
@@ -99,6 +102,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
ANIMAL_LEGACY_SCHEMA_VERSION, ANIMAL_LEGACY_SCHEMA_VERSION,
ROUTINE_LEGACY_SCHEMA_VERSION, ROUTINE_LEGACY_SCHEMA_VERSION,
PLAYER_RELATIONSHIP_SCHEMA_VERSION, PLAYER_RELATIONSHIP_SCHEMA_VERSION,
PLAYER_NEEDS_SCHEMA_VERSION,
] ]
): ):
record_data = _migrate_legacy(record_data, version) record_data = _migrate_legacy(record_data, version)
@@ -118,6 +122,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
"relationships", "relationships",
"event_knowledge", "event_knowledge",
"opportunities", "opportunities",
"player",
] ]
) )
): ):
@@ -200,6 +205,13 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
var record := SimulationStateRecord.new() var record := SimulationStateRecord.new()
record.simulation = simulation_data.duplicate(true) record.simulation = simulation_data.duplicate(true)
record.village = village_record record.village = village_record
var player_data = record_data["player"]
if not player_data is Dictionary:
return null
var player_record := PlayerStateRecord.from_dictionary(player_data)
if player_record == null:
return null
record.player = player_record
var npc_ids := {} var npc_ids := {}
var npc_records_by_id := {} var npc_records_by_id := {}
@@ -681,12 +693,23 @@ static func _is_valid_relationship_cause(
and StringName(cause_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD and StringName(cause_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
and float(cause_event.data["amount"]) > 0.0 and float(cause_event.data["amount"]) > 0.0
) )
if int(cause_event.data["actor_id"]) != SimulationIds.PLAYER_ACTOR_ID:
return false
if StringName(cause_event.data["item_id"]) != SimulationIds.RESOURCE_FOOD:
return false
if float(cause_event.data["amount"]) <= 0.0:
return false
if StringName(cause_event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED:
return ( return (
int(cause_event.data["actor_id"]) == SimulationIds.PLAYER_ACTOR_ID StringName(cause_event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID
and StringName(cause_event.data["event_type"]) == SimulationIds.EVENT_RESOURCE_EXTRACTED and (
and (StringName(cause_event.data["destination_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY) StringName(cause_event.data["destination_id"])
and StringName(cause_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD == SimulationIds.STORAGE_VILLAGE_PANTRY
and float(cause_event.data["amount"]) > 0.0 )
)
return (
StringName(cause_event.data["event_type"]) == SimulationIds.EVENT_RESOURCE_EXTRACTED
and StringName(cause_event.data["destination_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
) )
@@ -705,6 +728,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 == SimulationIds.PLAYER_ACTOR_ID:
return StringName(event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID
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)
@@ -724,6 +749,8 @@ static func _is_valid_supply_resolution(
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary: static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
var migrated := legacy_data.duplicate(true) var migrated := legacy_data.duplicate(true)
migrated["schema_version"] = SCHEMA_VERSION migrated["schema_version"] = SCHEMA_VERSION
if not migrated.has("player"):
migrated["player"] = PlayerStateRecord.create_default().to_dictionary()
if version <= ANIMAL_LEGACY_SCHEMA_VERSION: if version <= ANIMAL_LEGACY_SCHEMA_VERSION:
migrated["animals"] = [] migrated["animals"] = []
if version == LEGACY_SCHEMA_VERSION: if version == LEGACY_SCHEMA_VERSION:
@@ -785,6 +812,7 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
ANIMAL_LEGACY_SCHEMA_VERSION, ANIMAL_LEGACY_SCHEMA_VERSION,
ROUTINE_LEGACY_SCHEMA_VERSION, ROUTINE_LEGACY_SCHEMA_VERSION,
PLAYER_RELATIONSHIP_SCHEMA_VERSION, PLAYER_RELATIONSHIP_SCHEMA_VERSION,
PLAYER_NEEDS_SCHEMA_VERSION,
] ]
): ):
migrated["opportunities"] = [] migrated["opportunities"] = []
+107
View File
@@ -0,0 +1,107 @@
class_name PlayerStateRecord
extends RefCounted
const SCHEMA_VERSION := 1
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create_default() -> PlayerStateRecord:
return (
PlayerStateRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"hunger": 40.0,
"energy": 100.0,
"inventory": {},
}
)
)
static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return null
if not record_data.has_all(["hunger", "energy", "inventory"]):
return null
if not record_data["inventory"] is Dictionary:
return null
var hunger := float(record_data["hunger"])
var energy := float(record_data["energy"])
if not is_finite(hunger) or not is_finite(energy):
return null
var normalized_inventory := {}
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_inventory.has(item_id)
or not is_finite(amount)
or amount < 0.0
):
return null
normalized_inventory[item_id] = amount
return (
PlayerStateRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"hunger": clampf(hunger, 0.0, 100.0),
"energy": clampf(energy, 0.0, 100.0),
"inventory": normalized_inventory,
}
)
)
func get_hunger() -> float:
return clampf(float(data["hunger"]), 0.0, 100.0)
func get_energy() -> float:
return clampf(float(data["energy"]), 0.0, 100.0)
func set_hunger(amount: float) -> void:
data["hunger"] = clampf(amount, 0.0, 100.0)
func set_energy(amount: float) -> void:
data["energy"] = clampf(amount, 0.0, 100.0)
func get_inventory_amount(item_id: StringName) -> float:
return float(data["inventory"].get(String(item_id), 0.0))
func get_carried_total() -> float:
var total := 0.0
for amount in data["inventory"].values():
total += float(amount)
return total
func add_inventory(item_id: StringName, amount: float) -> void:
if item_id.is_empty() or not is_finite(amount) or amount <= 0.0:
return
data["inventory"][String(item_id)] = get_inventory_amount(item_id) + amount
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
return removed
func to_dictionary() -> Dictionary:
return data.duplicate(true)
@@ -0,0 +1 @@
uid://dynd78rglkeji
+10 -3
View File
@@ -249,7 +249,11 @@ func _run() -> void:
) )
_check( _check(
is_equal_approx(player_manager.harvest_resource_node(berry), 1.0), is_equal_approx(player_manager.harvest_resource_node(berry), 1.0),
"Player harvesting should supply the real pantry through the existing command" "Player harvesting should carry the real finite yield into player inventory"
)
_check(
is_equal_approx(player_manager.deposit_player_inventory(SimulationIds.RESOURCE_FOOD), 1.0),
"Player deposit should supply the real pantry through the carried-inventory command"
) )
var player_resolved: OpportunityStateRecord = player_manager.get_latest_opportunity_for_npc( var player_resolved: OpportunityStateRecord = player_manager.get_latest_opportunity_for_npc(
player_interested.id player_interested.id
@@ -264,9 +268,12 @@ func _run() -> void:
and player_resolution != null and player_resolution != null
and ( and (
StringName(player_resolution.data["event_type"]) StringName(player_resolution.data["event_type"])
== SimulationIds.EVENT_RESOURCE_EXTRACTED == SimulationIds.EVENT_STORAGE_DEPOSITED
)
and int(player_resolution.data["actor_id"]) == SimulationIds.PLAYER_ACTOR_ID
and (
StringName(player_resolution.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID
) )
and int(player_resolution.data["actor_id"]) == -1
and ( and (
StringName(player_resolution.data["destination_id"]) StringName(player_resolution.data["destination_id"])
== SimulationIds.STORAGE_VILLAGE_PANTRY == SimulationIds.STORAGE_VILLAGE_PANTRY
+12
View File
@@ -322,6 +322,18 @@ 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")
_check(
(
simulation_manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD)
>= 1.0
),
"Player gathering should first carry the finite yield"
)
var player_pantry := (
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
)
player.global_position = player_pantry.get_interaction_position()
player.call("try_interact")
village_ui.selected_npc_index = contributor.id village_ui.selected_npc_index = contributor.id
village_ui.call("_refresh_npc_inspector") village_ui.call("_refresh_npc_inspector")
_check( _check(
+170
View File
@@ -0,0 +1,170 @@
extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var manager := _create_manager(913)
manager.set_process(false)
var pantry: StorageStateRecord = manager.get_pantry()
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
pantry.deposit(SimulationIds.RESOURCE_FOOD, 2.0)
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
var berry := ResourceNode.new()
berry.name = "CitizenBerry"
berry.node_id = &"citizen_berry"
berry.action_id = SimulationIds.ACTION_GATHER_FOOD
berry.resource_id = SimulationIds.RESOURCE_FOOD
berry.initial_amount = 2.0
berry.yield_per_action = 1.0
berry.debug_label_enabled = false
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
interaction_point.position = Vector3.ZERO
berry.add_child(interaction_point)
root.add_child(berry)
await process_frame
_check(
manager.register_resource_node(berry),
"Player citizen test should bind a real finite ResourceNode"
)
var initial_hunger: float = manager.get_player_state().get_hunger()
manager.simulate_tick()
_check(
is_equal_approx(manager.get_player_state().get_hunger(), initial_hunger + 0.125),
"Player hunger should advance deterministically on each simulation tick"
)
_check(
is_equal_approx(manager.get_player_state().get_energy(), 100.0 - 0.0625),
"Player energy should drain by an exact binary fraction each tick"
)
_check(
is_equal_approx(manager.harvest_resource_node(berry), 1.0),
"The player should carry the first finite yield"
)
_check(
is_equal_approx(manager.harvest_resource_node(berry), 1.0),
"The player should carry the second finite yield"
)
_check(
is_equal_approx(
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 2.0
),
"Carried player food should be conserved across harvests"
)
var food_before: float = manager.get_player_state().get_hunger()
_check(manager.eat_player_food(), "The player should eat carried food")
_check(
(
is_equal_approx(manager.get_player_state().get_hunger(), maxf(food_before - 55.0, 0.0))
and is_equal_approx(
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0
)
),
"Eating carried food should ease hunger and consume exactly one unit"
)
_check(
is_equal_approx(manager.deposit_player_inventory(SimulationIds.RESOURCE_FOOD), 1.0),
"The player should deposit the remaining carried food"
)
_check(
(
is_equal_approx(
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0
)
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 3.0)
),
"Deposit should move carried food into the pantry without creating or destroying it"
)
var food_before_pantry: float = pantry.get_amount(SimulationIds.RESOURCE_FOOD)
var hunger_before: float = manager.get_player_state().get_hunger()
_check(manager.eat_player_food(), "The player should eat from the pantry when carrying nothing")
_check(
(
is_equal_approx(
pantry.get_amount(SimulationIds.RESOURCE_FOOD), food_before_pantry - 1.0
)
and is_equal_approx(
manager.get_player_state().get_hunger(), maxf(hunger_before - 55.0, 0.0)
)
),
"Pantry eating should consume one village food and ease player hunger"
)
manager.simulate_tick()
var mid_hunger: float = manager.get_player_state().get_hunger()
var saved_json: String = manager.serialize_state()
var restored := _create_manager(914)
_check(
restored.restore_state_from_json(saved_json),
"Player state should survive schema-v13 save and restore"
)
_check(
(
restored.get_state_checksum() == manager.get_state_checksum()
and is_equal_approx(restored.get_player_state().get_hunger(), mid_hunger)
),
"Restore should preserve the deterministic player needs and checksum"
)
restored.free()
var v12_data: Dictionary = manager.create_state_record().to_dictionary()
v12_data["schema_version"] = SimulationStateRecord.PLAYER_NEEDS_SCHEMA_VERSION
v12_data.erase("player")
var migrated := SimulationStateRecord.from_dictionary(v12_data)
_check(
(
migrated != null
and migrated.player != null
and is_equal_approx(migrated.player.get_hunger(), 40.0)
and is_equal_approx(migrated.player.get_energy(), 100.0)
and migrated.player.get_carried_total() == 0.0
),
"World schema v12 should migrate to v13 with a fresh default player record"
)
berry.queue_free()
manager.free()
_finish()
func _create_manager(seed_value: int = 913) -> Node:
var manager: Node = load("res://simulation/SimulationManager.gd").new()
manager.simulation_seed = seed_value
manager.debug_logs = false
var home_positions: Array[Vector3] = [
Vector3(0.0, 0.0, 0.0),
Vector3(2.0, 0.0, 0.0),
Vector3(10.0, 0.0, 0.0),
Vector3(12.0, 0.0, 0.0),
Vector3(30.0, 0.0, 30.0),
Vector3(40.0, 0.0, 40.0),
]
manager.home_positions = home_positions
root.add_child(manager)
manager.set_process(false)
return manager
func _finish() -> void:
if failures.is_empty():
print("[TEST] Player citizen loop passed: needs -> carry -> eat -> deposit")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
+1
View File
@@ -0,0 +1 @@
uid://bxh6g2basq762
+31 -4
View File
@@ -38,20 +38,32 @@ func _run() -> void:
) )
_check( _check(
is_equal_approx(manager.harvest_resource_node(berry), 1.0), is_equal_approx(manager.harvest_resource_node(berry), 1.0),
"Player harvesting should supply the real pantry" "Player harvesting should carry the real finite yield"
)
var carried_food: float = manager.get_player_state().get_inventory_amount(
SimulationIds.RESOURCE_FOOD
)
_check(
is_equal_approx(carried_food, 1.0),
"Player harvesting should move the yield into carried player inventory"
)
_check(
is_equal_approx(manager.deposit_player_inventory(SimulationIds.RESOURCE_FOOD), 1.0),
"Player deposit should supply the real pantry"
) )
var supply_event := _latest_event_of_type(manager, SimulationIds.EVENT_RESOURCE_EXTRACTED) var supply_event := _latest_event_of_type(manager, SimulationIds.EVENT_STORAGE_DEPOSITED)
var supply_event_id := int(supply_event.data["event_id"]) var supply_event_id := int(supply_event.data["event_id"])
_check( _check(
( (
int(supply_event.data["actor_id"]) == SimulationIds.PLAYER_ACTOR_ID int(supply_event.data["actor_id"]) == SimulationIds.PLAYER_ACTOR_ID
and (StringName(supply_event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID)
and ( and (
StringName(supply_event.data["destination_id"]) StringName(supply_event.data["destination_id"])
== SimulationIds.STORAGE_VILLAGE_PANTRY == SimulationIds.STORAGE_VILLAGE_PANTRY
) )
), ),
"The player supply should be recorded as a pantry food extraction by the player actor" "The player supply should be recorded as a pantry food deposit from player inventory"
) )
_check( _check(
( (
@@ -102,6 +114,7 @@ func _run() -> void:
) )
restored.free() restored.free()
var v12_data: Dictionary = manager.create_state_record().to_dictionary()
berry.queue_free() berry.queue_free()
manager.free() manager.free()
@@ -113,7 +126,21 @@ func _run() -> void:
and migrated.simulation.get("next_opportunity_id", -1) >= 0 and migrated.simulation.get("next_opportunity_id", -1) >= 0
and not migrated.opportunities.is_empty() and not migrated.opportunities.is_empty()
), ),
"World schema v11 should migrate to v12 without dropping opportunity history" "World schema v11 should migrate to v13 without dropping opportunity history"
)
v12_data["schema_version"] = SimulationStateRecord.PLAYER_NEEDS_SCHEMA_VERSION
v12_data.erase("player")
var v12_migrated := SimulationStateRecord.from_dictionary(v12_data)
_check(
(
v12_migrated != null
and v12_migrated.player != null
and is_equal_approx(v12_migrated.player.get_hunger(), 40.0)
and is_equal_approx(v12_migrated.player.get_energy(), 100.0)
and v12_migrated.player.get_carried_total() == 0.0
),
"World schema v12 should migrate to v13 with a fresh default player record"
) )
_finish() _finish()
+50 -34
View File
@@ -19,11 +19,9 @@ func _run() -> void:
var bush := ( var bush := (
main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/BerryBush_01") as ResourceNode main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/BerryBush_01") as ResourceNode
) )
var tree := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/Tree_01") as ResourceNode
simulation_manager.set_process(false) simulation_manager.set_process(false)
var bush_state: ResourceStateRecord = simulation_manager.get_resource_state(bush.node_id) var bush_state: ResourceStateRecord = simulation_manager.get_resource_state(bush.node_id)
var tree_state: ResourceStateRecord = simulation_manager.get_resource_state(tree.node_id)
bush_state.set_amount_remaining(1.0) bush_state.set_amount_remaining(1.0)
_check( _check(
simulation_manager.reserve_resource(bush.node_id, 999), simulation_manager.reserve_resource(bush.node_id, 999),
@@ -38,8 +36,11 @@ func _run() -> void:
"Player should deplete the nearby bush" "Player should deplete the nearby bush"
) )
_check( _check(
is_equal_approx(simulation_manager.village.food, food_before + 1.0), is_equal_approx(
"Village should receive exactly the bush's remaining food" simulation_manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD),
1.0
),
"Player harvesting should carry exactly the bush's remaining food"
) )
_check(bush_state.get_reserved_by() == -1, "Depletion should release the NPC reservation") _check(bush_state.get_reserved_by() == -1, "Depletion should release the NPC reservation")
var player_extraction: EconomicEventRecord var player_extraction: EconomicEventRecord
@@ -56,8 +57,14 @@ func _run() -> void:
_check(player_depletion != null, "Player depletion should create a structured event") _check(player_depletion != null, "Player depletion should create a structured event")
if player_extraction != null: if player_extraction != null:
_check( _check(
player_extraction.get_world_position().is_equal_approx(bush_event_position), (
"Player extraction should preserve the resource interaction position" player_extraction.get_world_position().is_equal_approx(bush_event_position)
and (
StringName(player_extraction.data["destination_id"])
== SimulationIds.PLAYER_INVENTORY_ID
)
),
"Player extraction should preserve the interaction position and name the carried inventory"
) )
if player_depletion != null: if player_depletion != null:
_check( _check(
@@ -65,45 +72,54 @@ 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()
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, pantry_state.get_available_capacity() - 0.5) player.global_position = pantry.get_interaction_position()
player.try_interact()
_check(
(
is_equal_approx(simulation_manager.village.food, food_before + 1.0)
and is_equal_approx(
simulation_manager.get_player_state().get_inventory_amount(
SimulationIds.RESOURCE_FOOD
),
0.0
)
),
"Depositing at the pantry should move carried food into the village and empty the player"
)
var pantry_food_before := pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
player.try_interact()
_check(
(
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < pantry_food_before
and simulation_manager.get_player_state().get_hunger() < 40.0
),
"Player should eat from the typed pantry StorageNode and ease their hunger"
)
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, pantry_state.get_available_capacity())
simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD) simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
bush_state.set_amount_remaining(1.0) bush_state.set_amount_remaining(1.0)
player.global_position = bush.interaction_point.global_position player.global_position = bush.interaction_point.global_position
player.try_interact() player.try_interact()
_check(
(
is_equal_approx(bush_state.get_amount_remaining(), 0.5)
and is_equal_approx(pantry_state.get_available_capacity(), 0.0)
),
"Player harvesting should leave overflow at its source when storage fills"
)
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, 3.0)
simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
var pantry_food_before := pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
player.global_position = pantry.get_interaction_position() player.global_position = pantry.get_interaction_position()
player.try_interact() player.try_interact()
_check( _check(
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < pantry_food_before, (
"Player should eat from the typed pantry StorageNode" is_equal_approx(bush_state.get_amount_remaining(), 0.0)
and is_equal_approx(pantry_state.get_available_capacity(), 0.0)
and is_equal_approx(
simulation_manager.get_player_state().get_inventory_amount(
SimulationIds.RESOURCE_FOOD
),
1.0
)
),
"Player harvesting into a full pantry should leave the unaccepted overflow carried"
) )
var guard_site := ( var guard_site := (
main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite
+8 -4
View File
@@ -246,7 +246,11 @@ func _run() -> void:
player_active_restored.free() player_active_restored.free()
_check( _check(
is_equal_approx(player_manager.harvest_resource_node(tree), 1.0), is_equal_approx(player_manager.harvest_resource_node(tree), 1.0),
"Player tree extraction should supply the authoritative woodpile" "Player tree extraction should carry the authoritative wood yield"
)
_check(
is_equal_approx(player_manager.deposit_player_inventory(SimulationIds.RESOURCE_WOOD), 1.0),
"Player wood deposit should supply the authoritative woodpile"
) )
var player_resolved: OpportunityStateRecord = player_manager.get_latest_opportunity_for_npc( var player_resolved: OpportunityStateRecord = player_manager.get_latest_opportunity_for_npc(
player_actor.id player_actor.id
@@ -258,10 +262,10 @@ func _run() -> void:
( (
player_resolved.get_status() == OpportunityStateRecord.STATUS_RESOLVED player_resolved.get_status() == OpportunityStateRecord.STATUS_RESOLVED
and ( and (
StringName(player_event.data["event_type"]) StringName(player_event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED
== SimulationIds.EVENT_RESOURCE_EXTRACTED
) )
and int(player_event.data["actor_id"]) == -1 and int(player_event.data["actor_id"]) == SimulationIds.PLAYER_ACTOR_ID
and (StringName(player_event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID)
and ( and (
StringName(player_event.data["destination_id"]) StringName(player_event.data["destination_id"])
== SimulationIds.STORAGE_VILLAGE_WOODPILE == SimulationIds.STORAGE_VILLAGE_WOODPILE
+46
View File
@@ -0,0 +1,46 @@
class_name PlayerStatusHud
extends Control
@export var simulation_manager: Node
@onready var status_label: Label = $StatusLabel
var cached_text := ""
func _ready() -> void:
visible = true
_refresh(true)
func _process(_delta: float) -> void:
_refresh(false)
func _refresh(force: bool = false) -> void:
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
return
var player_state: PlayerStateRecord = simulation_manager.get_player_state()
var carried := _carried_text(player_state)
var next_text := (
"Hunger %.0f · Energy %.0f · Carrying %s"
% [player_state.get_hunger(), player_state.get_energy(), carried]
)
if not force and next_text == cached_text:
return
cached_text = next_text
status_label.text = next_text
func _carried_text(player_state: PlayerStateRecord) -> String:
var entries: Array[String] = []
for raw_item_id in player_state.data["inventory"]:
var item_id := String(raw_item_id)
var amount := player_state.get_inventory_amount(StringName(item_id))
if amount <= 0.0:
continue
entries.append("%.0f %s" % [amount, StringName(item_id).to_lower()])
if entries.is_empty():
return "nothing"
entries.sort()
return " · ".join(entries)
+1
View File
@@ -0,0 +1 @@
uid://cgur3e3k1l3iy