feat: let the player live as a citizen with needs and carried inventory
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"] = []
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var manager := _create_manager(913)
|
||||||
|
manager.set_process(false)
|
||||||
|
var pantry: StorageStateRecord = manager.get_pantry()
|
||||||
|
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
||||||
|
pantry.deposit(SimulationIds.RESOURCE_FOOD, 2.0)
|
||||||
|
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||||
|
|
||||||
|
var berry := ResourceNode.new()
|
||||||
|
berry.name = "CitizenBerry"
|
||||||
|
berry.node_id = &"citizen_berry"
|
||||||
|
berry.action_id = SimulationIds.ACTION_GATHER_FOOD
|
||||||
|
berry.resource_id = SimulationIds.RESOURCE_FOOD
|
||||||
|
berry.initial_amount = 2.0
|
||||||
|
berry.yield_per_action = 1.0
|
||||||
|
berry.debug_label_enabled = false
|
||||||
|
var interaction_point := Marker3D.new()
|
||||||
|
interaction_point.name = "InteractionPoint"
|
||||||
|
interaction_point.position = Vector3.ZERO
|
||||||
|
berry.add_child(interaction_point)
|
||||||
|
root.add_child(berry)
|
||||||
|
await process_frame
|
||||||
|
_check(
|
||||||
|
manager.register_resource_node(berry),
|
||||||
|
"Player citizen test should bind a real finite ResourceNode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var initial_hunger: float = manager.get_player_state().get_hunger()
|
||||||
|
manager.simulate_tick()
|
||||||
|
_check(
|
||||||
|
is_equal_approx(manager.get_player_state().get_hunger(), initial_hunger + 0.125),
|
||||||
|
"Player hunger should advance deterministically on each simulation tick"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(manager.get_player_state().get_energy(), 100.0 - 0.0625),
|
||||||
|
"Player energy should drain by an exact binary fraction each tick"
|
||||||
|
)
|
||||||
|
|
||||||
|
_check(
|
||||||
|
is_equal_approx(manager.harvest_resource_node(berry), 1.0),
|
||||||
|
"The player should carry the first finite yield"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(manager.harvest_resource_node(berry), 1.0),
|
||||||
|
"The player should carry the second finite yield"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(
|
||||||
|
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 2.0
|
||||||
|
),
|
||||||
|
"Carried player food should be conserved across harvests"
|
||||||
|
)
|
||||||
|
|
||||||
|
var food_before: float = manager.get_player_state().get_hunger()
|
||||||
|
_check(manager.eat_player_food(), "The player should eat carried food")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
is_equal_approx(manager.get_player_state().get_hunger(), maxf(food_before - 55.0, 0.0))
|
||||||
|
and is_equal_approx(
|
||||||
|
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"Eating carried food should ease hunger and consume exactly one unit"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(manager.deposit_player_inventory(SimulationIds.RESOURCE_FOOD), 1.0),
|
||||||
|
"The player should deposit the remaining carried food"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
is_equal_approx(
|
||||||
|
manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0
|
||||||
|
)
|
||||||
|
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 3.0)
|
||||||
|
),
|
||||||
|
"Deposit should move carried food into the pantry without creating or destroying it"
|
||||||
|
)
|
||||||
|
|
||||||
|
var food_before_pantry: float = pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||||
|
var hunger_before: float = manager.get_player_state().get_hunger()
|
||||||
|
_check(manager.eat_player_food(), "The player should eat from the pantry when carrying nothing")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
is_equal_approx(
|
||||||
|
pantry.get_amount(SimulationIds.RESOURCE_FOOD), food_before_pantry - 1.0
|
||||||
|
)
|
||||||
|
and is_equal_approx(
|
||||||
|
manager.get_player_state().get_hunger(), maxf(hunger_before - 55.0, 0.0)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"Pantry eating should consume one village food and ease player hunger"
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.simulate_tick()
|
||||||
|
var mid_hunger: float = manager.get_player_state().get_hunger()
|
||||||
|
var saved_json: String = manager.serialize_state()
|
||||||
|
var restored := _create_manager(914)
|
||||||
|
_check(
|
||||||
|
restored.restore_state_from_json(saved_json),
|
||||||
|
"Player state should survive schema-v13 save and restore"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
restored.get_state_checksum() == manager.get_state_checksum()
|
||||||
|
and is_equal_approx(restored.get_player_state().get_hunger(), mid_hunger)
|
||||||
|
),
|
||||||
|
"Restore should preserve the deterministic player needs and checksum"
|
||||||
|
)
|
||||||
|
restored.free()
|
||||||
|
|
||||||
|
var v12_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
|
v12_data["schema_version"] = SimulationStateRecord.PLAYER_NEEDS_SCHEMA_VERSION
|
||||||
|
v12_data.erase("player")
|
||||||
|
var migrated := SimulationStateRecord.from_dictionary(v12_data)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
migrated != null
|
||||||
|
and migrated.player != null
|
||||||
|
and is_equal_approx(migrated.player.get_hunger(), 40.0)
|
||||||
|
and is_equal_approx(migrated.player.get_energy(), 100.0)
|
||||||
|
and migrated.player.get_carried_total() == 0.0
|
||||||
|
),
|
||||||
|
"World schema v12 should migrate to v13 with a fresh default player record"
|
||||||
|
)
|
||||||
|
|
||||||
|
berry.queue_free()
|
||||||
|
manager.free()
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_manager(seed_value: int = 913) -> Node:
|
||||||
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
manager.simulation_seed = seed_value
|
||||||
|
manager.debug_logs = false
|
||||||
|
var home_positions: Array[Vector3] = [
|
||||||
|
Vector3(0.0, 0.0, 0.0),
|
||||||
|
Vector3(2.0, 0.0, 0.0),
|
||||||
|
Vector3(10.0, 0.0, 0.0),
|
||||||
|
Vector3(12.0, 0.0, 0.0),
|
||||||
|
Vector3(30.0, 0.0, 30.0),
|
||||||
|
Vector3(40.0, 0.0, 40.0),
|
||||||
|
]
|
||||||
|
manager.home_positions = home_positions
|
||||||
|
root.add_child(manager)
|
||||||
|
manager.set_process(false)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Player citizen loop passed: needs -> carry -> eat -> deposit")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bxh6g2basq762
|
||||||
@@ -38,20 +38,32 @@ func _run() -> void:
|
|||||||
)
|
)
|
||||||
_check(
|
_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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cgur3e3k1l3iy
|
||||||
Reference in New Issue
Block a user