feat: make pantry crises visible in the world

This commit is contained in:
Rijad Zuzo
2026-07-14 23:54:22 +02:00
parent c722e32a88
commit c23f6887c8
21 changed files with 800 additions and 73 deletions
+200 -7
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=36 format=3]
[gd_scene load_steps=50 format=3]
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
@@ -17,6 +17,7 @@
[ext_resource type="Script" path="res://world/activity/ActivitySite.gd" id="15_activity"]
[ext_resource type="NavigationMesh" path="res://world/jajce/JajceNavigationMesh.tres" id="16_nav"]
[ext_resource type="PackedScene" path="res://world/jajce/vfx/WindGustField.tscn" id="17_wind_gust"]
[ext_resource type="Script" path="res://world/storage/PantryStockVisual.gd" id="18_pantry_visual"]
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
_shader_parameters = {
@@ -74,6 +75,83 @@ size = Vector3(1.0, 0.08, 0.5)
material = SubResource("Material_site_canvas")
size = Vector3(0.5, 0.62, 0.42)
[sub_resource type="StandardMaterial3D" id="Material_pantry_berry"]
albedo_color = Color(0.56, 0.16, 0.19, 1)
roughness = 0.82
[sub_resource type="StandardMaterial3D" id="Material_pantry_leaf"]
albedo_color = Color(0.35, 0.52, 0.25, 1)
roughness = 0.86
[sub_resource type="StandardMaterial3D" id="Material_pantry_empty"]
albedo_color = Color(0.48, 0.22, 0.09, 1)
roughness = 0.94
[sub_resource type="StandardMaterial3D" id="Material_pantry_glow"]
transparency = 1
shading_mode = 0
albedo_color = Color(0.96, 0.72, 0.3, 0.7)
emission_enabled = true
emission = Color(0.92, 0.5, 0.15, 1)
emission_energy_multiplier = 0.45
[sub_resource type="CylinderMesh" id="Mesh_pantry_basket"]
material = SubResource("Material_site_dark_wood")
top_radius = 0.46
bottom_radius = 0.38
height = 0.3
radial_segments = 12
[sub_resource type="SphereMesh" id="Mesh_pantry_berry"]
material = SubResource("Material_pantry_berry")
radius = 0.15
height = 0.26
radial_segments = 10
rings = 5
[sub_resource type="SphereMesh" id="Mesh_pantry_leaf"]
material = SubResource("Material_pantry_leaf")
radius = 0.12
height = 0.24
radial_segments = 8
rings = 4
[sub_resource type="TorusMesh" id="Mesh_pantry_bowl_rim"]
material = SubResource("Material_pantry_empty")
inner_radius = 0.3
outer_radius = 0.39
rings = 14
ring_segments = 8
[sub_resource type="CylinderMesh" id="Mesh_pantry_bowl_base"]
material = SubResource("Material_pantry_empty")
top_radius = 0.3
bottom_radius = 0.24
height = 0.08
radial_segments = 12
[sub_resource type="BoxMesh" id="Mesh_pantry_shelf"]
material = SubResource("Material_site_dark_wood")
size = Vector3(1.5, 0.12, 0.5)
[sub_resource type="BoxMesh" id="Mesh_pantry_backcloth"]
material = SubResource("Material_site_canvas")
size = Vector3(1.45, 1.05, 0.08)
[sub_resource type="TorusMesh" id="Mesh_pantry_refill_ring"]
material = SubResource("Material_pantry_glow")
inner_radius = 0.55
outer_radius = 0.66
rings = 18
ring_segments = 8
[sub_resource type="SphereMesh" id="Mesh_pantry_refill_mote"]
material = SubResource("Material_pantry_glow")
radius = 0.09
height = 0.17
radial_segments = 8
rings = 4
[sub_resource type="BoxMesh" id="Mesh_study_book"]
material = SubResource("Material_site_book")
size = Vector3(0.42, 0.08, 0.3)
@@ -458,6 +536,9 @@ discovery_priority = 0.45
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
position = Vector3(0, 0, -8)
[node name="Visual" parent="WorldObjects/StorageSites/VillagePantry"]
visible = false
[node name="VillageWoodpile" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
position = Vector3(8, 0, -12)
storage_id = &"village_woodpile"
@@ -471,16 +552,128 @@ position = Vector3(-1.05, 1.85, 1.25)
rotation_degrees = Vector3(0, 90, 0)
mesh = SubResource("Mesh_site_sign")
[node name="PantrySack_A" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
position = Vector3(-0.95, 0.3, -0.65)
scale = Vector3(0.65, 1.0, 0.65)
[node name="PantryPresentation" type="Node3D" parent="WorldObjects/StorageSites/VillagePantry"]
position = Vector3(0, 0, -0.48)
scale = Vector3(1.12, 1.12, 1.12)
script = ExtResource("18_pantry_visual")
[node name="Backcloth" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation"]
position = Vector3(0, 0.68, 0.22)
mesh = SubResource("Mesh_pantry_backcloth")
[node name="ShelfLower" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation"]
position = Vector3(0, 0.18, 0)
mesh = SubResource("Mesh_pantry_shelf")
[node name="ShelfUpper" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation"]
position = Vector3(0, 1.15, 0)
mesh = SubResource("Mesh_pantry_shelf")
[node name="PostLeft" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation"]
position = Vector3(-0.65, 0.66, 0.18)
scale = Vector3(0.08, 8, 0.12)
mesh = SubResource("Mesh_pantry_shelf")
[node name="PostRight" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation"]
position = Vector3(0.65, 0.66, 0.18)
scale = Vector3(0.08, 8, 0.12)
mesh = SubResource("Mesh_pantry_shelf")
[node name="EmptyStock" type="Node3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation"]
position = Vector3(0, 0, -0.38)
[node name="BowlBase" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/EmptyStock"]
position = Vector3(0, 0.27, -0.04)
mesh = SubResource("Mesh_pantry_bowl_base")
[node name="BowlRim" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/EmptyStock"]
position = Vector3(0, 0.34, -0.04)
mesh = SubResource("Mesh_pantry_bowl_rim")
[node name="LowStock" type="Node3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation"]
visible = false
position = Vector3(0, 0, -0.38)
[node name="Sack" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/LowStock"]
position = Vector3(-0.42, 0.49, 0)
scale = Vector3(0.72, 0.9, 0.72)
mesh = SubResource("Mesh_site_sack")
[node name="PantrySack_B" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
position = Vector3(-1.35, 0.22, -0.25)
scale = Vector3(0.5, 0.75, 0.5)
[node name="Basket" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/LowStock"]
position = Vector3(0.36, 0.32, -0.02)
mesh = SubResource("Mesh_pantry_basket")
[node name="BerryLeft" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/LowStock"]
position = Vector3(0.18, 0.52, -0.02)
mesh = SubResource("Mesh_pantry_berry")
[node name="BerryCenter" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/LowStock"]
position = Vector3(0.38, 0.57, 0.02)
mesh = SubResource("Mesh_pantry_berry")
[node name="BerryRight" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/LowStock"]
position = Vector3(0.55, 0.5, -0.03)
mesh = SubResource("Mesh_pantry_berry")
[node name="Leaf" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/LowStock"]
position = Vector3(0.36, 0.67, 0)
rotation_degrees = Vector3(20, 0, 58)
scale = Vector3(0.45, 1, 0.3)
mesh = SubResource("Mesh_pantry_leaf")
[node name="FullStock" type="Node3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation"]
visible = false
position = Vector3(0, 0, -0.38)
[node name="SackLeft" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/FullStock"]
position = Vector3(-0.48, 0.77, 0)
scale = Vector3(0.55, 0.72, 0.55)
mesh = SubResource("Mesh_site_sack")
[node name="SackRight" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/FullStock"]
position = Vector3(0.48, 0.78, 0)
scale = Vector3(0.5, 0.68, 0.5)
mesh = SubResource("Mesh_site_sack")
[node name="UpperBasket" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/FullStock"]
position = Vector3(0, 1.31, -0.02)
scale = Vector3(0.82, 0.82, 0.82)
mesh = SubResource("Mesh_pantry_basket")
[node name="UpperBerryLeft" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/FullStock"]
position = Vector3(-0.18, 1.48, -0.03)
mesh = SubResource("Mesh_pantry_berry")
[node name="UpperBerryCenter" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/FullStock"]
position = Vector3(0, 1.52, 0.02)
mesh = SubResource("Mesh_pantry_berry")
[node name="UpperBerryRight" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/FullStock"]
position = Vector3(0.18, 1.46, -0.02)
mesh = SubResource("Mesh_pantry_berry")
[node name="RefillResponse" type="Node3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation"]
visible = false
position = Vector3(0, 0.08, -0.5)
scale = Vector3(0.45, 0.45, 0.45)
[node name="Ring" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/RefillResponse"]
position = Vector3(0, 0.78, 0)
mesh = SubResource("Mesh_pantry_refill_ring")
[node name="MoteLeft" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/RefillResponse"]
position = Vector3(-0.48, 1.02, 0)
mesh = SubResource("Mesh_pantry_refill_mote")
[node name="MoteCenter" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/RefillResponse"]
position = Vector3(0, 1.18, 0)
scale = Vector3(0.8, 1.3, 0.8)
mesh = SubResource("Mesh_pantry_refill_mote")
[node name="MoteRight" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/RefillResponse"]
position = Vector3(0.48, 1.02, 0)
mesh = SubResource("Mesh_pantry_refill_mote")
[node name="ActivitySites" type="Node3D" parent="WorldObjects"]
[node name="GuardPost" type="Node3D" parent="WorldObjects/ActivitySites"]
+149
View File
@@ -0,0 +1,149 @@
class_name PantryStockVisual
extends Node3D
enum StockLevel { EMPTY, LOW, STOCKED }
const LOW_STOCK_THRESHOLD := 4.0
const REFILL_RESPONSE_START := Vector3(0.0, 0.08, -0.5)
const REFILL_RESPONSE_END := Vector3(0.0, 0.36, -0.5)
@onready var empty_root: Node3D = $EmptyStock
@onready var low_stock_root: Node3D = $LowStock
@onready var full_stock_root: Node3D = $FullStock
@onready var refill_response_root: Node3D = $RefillResponse
@onready var refill_response_visuals: Array[MeshInstance3D] = [
$RefillResponse/Ring,
$RefillResponse/MoteLeft,
$RefillResponse/MoteCenter,
$RefillResponse/MoteRight,
]
var displayed_amount := -1.0
var stock_level := StockLevel.EMPTY
var transition_tween: Tween
var refill_tween: Tween
func _ready() -> void:
refill_response_root.visible = false
func set_food_amount(amount: float, animate: bool = true) -> void:
var normalized_amount := maxf(amount, 0.0)
var next_level := _get_stock_level(normalized_amount)
if next_level == StockLevel.EMPTY:
_stop_refill_response()
var was_initialized := displayed_amount >= 0.0
var was_empty := was_initialized and displayed_amount <= 0.001
if was_initialized and next_level == stock_level:
displayed_amount = normalized_amount
return
var should_animate := animate and was_initialized
displayed_amount = normalized_amount
stock_level = next_level
if should_animate:
_transition_to_level(next_level)
else:
_apply_level_immediately(next_level)
if was_empty and normalized_amount > 0.001 and animate:
_play_refill_response()
func get_stock_level() -> StockLevel:
return stock_level
func _get_stock_level(amount: float) -> StockLevel:
if amount <= 0.001:
return StockLevel.EMPTY
if amount < LOW_STOCK_THRESHOLD:
return StockLevel.LOW
return StockLevel.STOCKED
func _apply_level_immediately(level: StockLevel) -> void:
_stop_transition()
_set_root_visible(empty_root, level == StockLevel.EMPTY)
_set_root_visible(low_stock_root, level in [StockLevel.LOW, StockLevel.STOCKED])
_set_root_visible(full_stock_root, level == StockLevel.STOCKED)
func _transition_to_level(level: StockLevel) -> void:
_stop_transition()
transition_tween = create_tween().set_parallel(true)
_transition_root(empty_root, level == StockLevel.EMPTY)
_transition_root(low_stock_root, level in [StockLevel.LOW, StockLevel.STOCKED])
_transition_root(full_stock_root, level == StockLevel.STOCKED)
transition_tween.chain().tween_callback(_finish_transition.bind(level))
func _transition_root(root: Node3D, should_show: bool) -> void:
if should_show:
if not root.visible:
root.visible = true
root.scale = Vector3.ONE * 0.62
(
transition_tween
. tween_property(root, "scale", Vector3.ONE, 0.28)
. set_trans(Tween.TRANS_BACK)
. set_ease(Tween.EASE_OUT)
)
return
if not root.visible:
return
(
transition_tween
. tween_property(root, "scale", Vector3.ONE * 0.18, 0.16)
. set_trans(Tween.TRANS_SINE)
. set_ease(Tween.EASE_IN)
)
func _finish_transition(level: StockLevel) -> void:
transition_tween = null
_set_root_visible(empty_root, level == StockLevel.EMPTY)
_set_root_visible(low_stock_root, level in [StockLevel.LOW, StockLevel.STOCKED])
_set_root_visible(full_stock_root, level == StockLevel.STOCKED)
func _set_root_visible(root: Node3D, is_visible: bool) -> void:
root.visible = is_visible
root.scale = Vector3.ONE
func _stop_transition() -> void:
if transition_tween != null:
transition_tween.kill()
transition_tween = null
func _play_refill_response() -> void:
_stop_refill_response()
refill_response_root.visible = true
refill_response_root.position = REFILL_RESPONSE_START
refill_response_root.scale = Vector3.ONE * 0.45
_set_refill_transparency(0.0)
refill_tween = create_tween()
refill_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
refill_tween.tween_property(refill_response_root, "scale", Vector3.ONE, 0.24)
refill_tween.parallel().tween_method(_set_refill_transparency, 0.0, 1.0, 0.18)
refill_tween.tween_property(refill_response_root, "position", REFILL_RESPONSE_END, 0.5)
refill_tween.parallel().tween_method(_set_refill_transparency, 1.0, 0.0, 0.5)
refill_tween.tween_callback(_stop_refill_response)
func _set_refill_transparency(value: float) -> void:
for visual in refill_response_visuals:
visual.transparency = clampf(1.0 - value, 0.0, 1.0)
func _stop_refill_response() -> void:
if refill_tween != null:
refill_tween.kill()
refill_tween = null
if refill_response_root == null:
return
refill_response_root.visible = false
refill_response_root.position = REFILL_RESPONSE_START
refill_response_root.scale = Vector3.ONE * 0.45
_set_refill_transparency(0.0)
+1
View File
@@ -0,0 +1 @@
uid://bj2aigpu1nug6
+1 -9
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=7 format=3]
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://world/storage/StorageNode.gd" id="1_storage"]
@@ -18,10 +18,6 @@ size = Vector3(1.6, 1.1, 1.6)
material = SubResource("Material_cloth")
size = Vector3(1.75, 0.12, 1.75)
[sub_resource type="BoxMesh" id="Mesh_sack"]
material = SubResource("Material_cloth")
size = Vector3(0.45, 0.65, 0.45)
[node name="StorageNode" type="Node3D"]
script = ExtResource("1_storage")
@@ -35,10 +31,6 @@ mesh = SubResource("Mesh_crate")
transform = Transform3D(1, 0, 0, 0, 0.7, 0.7173561, 0, -0.7173561, 0.7, 0, 1.15, -0.06)
mesh = SubResource("Mesh_lid")
[node name="FoodSack" type="MeshInstance3D" parent="Visual"]
transform = Transform3D(0.85, 0, 0, 0, 1, 0, 0, 0, 0.85, 0.8, 0.325, 0.55)
mesh = SubResource("Mesh_sack")
[node name="InteractionPoint" type="Marker3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2.0)
+69
View File
@@ -5,6 +5,7 @@ extends Node
@export var npc_visual_scene: PackedScene
@export var simulation_manager: Node
@export var active_npcs_parent: Node3D
@export var pantry_visual: Node3D
var active_npc_visuals := {}
@@ -44,11 +45,24 @@ func initialize_world_view() -> void:
simulation_manager.relationship_changed.connect(_on_relationship_changed)
else:
push_error("WorldViewManager: SimulationManager has no relationship_changed signal")
if simulation_manager.has_signal("economic_event_recorded"):
simulation_manager.economic_event_recorded.connect(_on_economic_event_recorded)
else:
push_error("WorldViewManager: SimulationManager has no economic_event_recorded signal")
if simulation_manager.has_signal("opportunity_opened"):
simulation_manager.opportunity_opened.connect(_on_opportunity_changed)
else:
push_error("WorldViewManager: SimulationManager has no opportunity_opened signal")
if simulation_manager.has_signal("opportunity_resolved"):
simulation_manager.opportunity_resolved.connect(_on_opportunity_resolved)
else:
push_error("WorldViewManager: SimulationManager has no opportunity_resolved signal")
if simulation_manager.has_signal("state_restored"):
simulation_manager.state_restored.connect(_on_simulation_state_restored)
else:
push_error("WorldViewManager: SimulationManager has no state_restored signal")
spawn_initial_npcs()
_refresh_world_state_presentation(false)
func spawn_initial_npcs() -> void:
@@ -86,6 +100,7 @@ func spawn_npc_visual(npc: SimNPC) -> void:
else:
push_error("WorldViewManager: NPCVisual has no position_changed signal")
active_npc_visuals[npc.id] = visual
_sync_npc_opportunity_concern(npc.id, visual)
debug_log("Spawned visual for %s at %s" % [npc.npc_name, npc.position])
if npc.is_dead:
visual.apply_dead_visual_state()
@@ -200,8 +215,62 @@ func _on_relationship_changed(
push_error("WorldViewManager: NPCVisual has no play_trust_gain_reaction method")
func _on_economic_event_recorded(_event: EconomicEventRecord) -> void:
_refresh_pantry_stock(true)
func _on_opportunity_changed(_opportunity: OpportunityStateRecord) -> void:
_refresh_world_state_presentation(true)
func _on_opportunity_resolved(
_opportunity: OpportunityStateRecord, _cause_event: EconomicEventRecord
) -> void:
_refresh_world_state_presentation(true)
func _on_simulation_state_restored() -> void:
for visual in active_npc_visuals.values():
visual.queue_free()
active_npc_visuals.clear()
spawn_initial_npcs()
_refresh_world_state_presentation(false)
func _refresh_world_state_presentation(animate: bool) -> void:
_refresh_pantry_stock(animate)
_refresh_opportunity_concern()
func _refresh_pantry_stock(animate: bool) -> void:
if pantry_visual == null or not pantry_visual.has_method("set_food_amount"):
return
var pantry: StorageStateRecord = simulation_manager.get_pantry()
var amount := 0.0
if pantry != null:
amount = pantry.get_amount(SimulationIds.RESOURCE_FOOD)
pantry_visual.set_food_amount(amount, animate)
func _refresh_opportunity_concern() -> void:
for visual in active_npc_visuals.values():
if visual.has_method("set_opportunity_concern_visible"):
visual.set_opportunity_concern_visible(false)
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
if opportunity == null:
return
var interested_visual = active_npc_visuals.get(opportunity.get_interested_npc_id())
if (
interested_visual != null
and interested_visual.has_method("set_opportunity_concern_visible")
):
interested_visual.set_opportunity_concern_visible(true)
func _sync_npc_opportunity_concern(npc_id: int, visual: Node3D) -> void:
if not visual.has_method("set_opportunity_concern_visible"):
return
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
visual.set_opportunity_concern_visible(
opportunity != null and opportunity.get_interested_npc_id() == npc_id
)