feat: make pantry crises visible in the world
This commit is contained in:
@@ -18,9 +18,10 @@ SimulationClock
|
||||
-> EventKnowledgeSystem records, ranks, transfers, and retains bounded knowledge
|
||||
-> RelationshipSystem applies evidence-gated social consequences
|
||||
-> FoodShortageOpportunitySystem projects one known unresolved need
|
||||
-> WorldViewManager presents travel and NPC state
|
||||
-> WorldViewManager presents travel, NPC state, and world-state cues
|
||||
-> ActiveWorldAdapter supplies loaded-world positions/capacity
|
||||
-> NpcVisual performs local navigation, animation, and transient reactions
|
||||
-> PantryStockVisual derives physical stock arrangement from storage state
|
||||
```
|
||||
|
||||
`SimulationManager` is the scene-tree façade for the simulation. It owns the
|
||||
@@ -89,8 +90,10 @@ improving ownership.
|
||||
opportunity, or reservation state.
|
||||
- Inspector history is a read-only façade query over objective events and
|
||||
retained knowledge. Opportunity presentation is likewise query-only.
|
||||
Relationship cues consume state-change signals and are intentionally absent
|
||||
from saves and checksums.
|
||||
Relationship cues, interested-villager concern, physical pantry stock, and
|
||||
refill feedback consume authoritative state or state-change signals and are
|
||||
intentionally absent from saves and checksums. Stable visuals are rebuilt
|
||||
from state after restore; transient reactions are not replayed.
|
||||
- Resource changes go through `ResourceStateRecord`, NPC inventory, and
|
||||
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
||||
- New mutable features define serialization and deterministic continuation at
|
||||
|
||||
@@ -711,11 +711,20 @@ Completed:
|
||||
NPC or player supply actor. The repeatable debug capture stages this from
|
||||
live simulation state; world framing and the elevated follow camera remain
|
||||
unchanged.
|
||||
26. Physical pantry crisis payoff: the pantry now replaces its inherited
|
||||
generic crate with empty, low, and stocked arrangements driven by real
|
||||
storage state. Only the interested villager receives a small state-derived
|
||||
concern cue; a real deposit clears it and briefly animates the refilled
|
||||
pantry. The capture tool writes an edit-ready empty/restocked close pair
|
||||
while leaving gameplay camera defaults and simulation authority unchanged.
|
||||
|
||||
Next:
|
||||
|
||||
1. Prove the same opportunity lifecycle with one missing-wood `task_blocked`
|
||||
condition before extracting shared generation rules, as sequenced in
|
||||
1. Package one 20–30 second, repeatable pantry-crisis sequence that communicates
|
||||
shortage, purposeful gathering, visible carrying, deposit, and recovery
|
||||
without debug UI. Treat any art or timing work as support for that single
|
||||
readable cause-and-effect loop. Resume the second missing-wood opportunity
|
||||
proof after this public-demo exit test, as sequenced in
|
||||
`LEARNING_ROADMAP.md`.
|
||||
|
||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||
|
||||
@@ -730,11 +730,26 @@ The first bounded Milestone 7 opportunity proof is complete:
|
||||
quest-only event, acceptance, reward, dialogue, multi-hop rumour, or generic
|
||||
quest framework.
|
||||
|
||||
Milestone 7 is not complete. The practical next slice is a second bounded
|
||||
unresolved-condition consumer: surface missing-wood `task_blocked` facts from
|
||||
patrol or study as a cared-about woodpile-restock need, resolve it through real
|
||||
NPC/player wood supply, define deterministic interested-party death/staleness
|
||||
handling, and only then extract the lifecycle shared by two proven consumers.
|
||||
The first build-in-public presentation checkpoint for that proof is also
|
||||
complete:
|
||||
|
||||
- authoritative pantry food selects a physical empty, low, or stocked pantry
|
||||
arrangement instead of leaving a generic storage crate unchanged;
|
||||
- only the interested villager shows a restrained empty-bowl concern cue;
|
||||
- a real NPC deposit changes the pantry props, clears concern, and produces one
|
||||
brief refill response, while restore derives the stable presentation without
|
||||
replaying that transient effect;
|
||||
- the repeatable runtime capture now includes an empty/restocked close pair in
|
||||
the established elevated third-person visual language.
|
||||
|
||||
Milestone 7 is not complete. The immediate outcome-defined slice is now one
|
||||
short, repeatable pantry-crisis sequence that reads without debug UI: shortage,
|
||||
interested villager, finite-resource trip, visible carrying, deposit, and
|
||||
physical recovery. Improve route timing, work feedback, and village composition
|
||||
only where that sequence exposes a readability problem. Once that public-demo
|
||||
loop passes, prove a second bounded unresolved-condition consumer from a real
|
||||
missing-wood `task_blocked` fact, include deterministic interested-party
|
||||
death/staleness handling, and only then extract shared opportunity machinery.
|
||||
|
||||
Recently completed:
|
||||
|
||||
|
||||
+24
-13
@@ -2,7 +2,7 @@
|
||||
|
||||
> Agent-facing context for understanding the project quickly.
|
||||
>
|
||||
> Snapshot basis: repository state on July 13, 2026. Treat the code as the
|
||||
> Snapshot basis: repository state on July 14, 2026. Treat the code as the
|
||||
> source of truth when this document and the implementation differ.
|
||||
|
||||
See the [documentation map](README.md) for the authority and scope of each plan.
|
||||
@@ -200,8 +200,8 @@ study, and patrol target typed activity sites. The migration is documented in
|
||||
`docs/baselines/jajce_lookdev_01.png` with
|
||||
`tools/capture_jajce_lookdev.gd`
|
||||
- **Runtime baseline:** `Simulation Garden 01` is captured as paired debug and
|
||||
cinematic `main.tscn` images under `docs/baselines/` with
|
||||
`tools/capture_simulation_garden.gd`
|
||||
cinematic `main.tscn` images plus an empty/restocked pantry close pair under
|
||||
`docs/baselines/` with `tools/capture_simulation_garden.gd`
|
||||
- **Terrain compatibility floor:** Godot 4.4 according to the bundled extension
|
||||
- **Version control:** Git
|
||||
- **Primary branch:** `main`
|
||||
@@ -338,8 +338,9 @@ distance with resource `safety_risk`, `comfort_distance`, and
|
||||
- deterministic shaped terrain data and six game-owned surface layers;
|
||||
- fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke;
|
||||
- authored dirt path strips that make the current village task loop readable;
|
||||
- compact silhouette props for the ridge landmark, pantry, guard, study, and
|
||||
rest sites;
|
||||
- compact silhouette props for the ridge landmark, guard, study, and rest
|
||||
sites, plus a physical pantry whose empty/low/stocked arrangement follows
|
||||
authoritative food storage;
|
||||
- warm sky, fog, shadows, coherent wind-reactive foliage, and breeze-aligned
|
||||
chimney smoke;
|
||||
- one sparse terrain-aware calligraphic gust field with two or three soft
|
||||
@@ -382,7 +383,10 @@ A small village panel displays:
|
||||
NPC name/profession labels, definition-driven colors and props, carried-food
|
||||
visuals, compact task glyphs, and a brief amber blossom on real trust gains make
|
||||
active simulation state readable in the world. The F10 cinematic mode hides
|
||||
debug labels while preserving task glyphs and transient social reactions.
|
||||
debug labels while preserving task glyphs and transient social reactions. An
|
||||
open pantry opportunity adds a restrained empty-bowl concern cue only to its
|
||||
interested villager; a real refill changes the pantry props, clears concern,
|
||||
and produces one brief pantry response.
|
||||
|
||||
## Runtime architecture
|
||||
|
||||
@@ -441,7 +445,9 @@ than duplicating these responsibilities across additional manager nodes.
|
||||
- synchronizes successful visual movement into authoritative NPC position;
|
||||
- applies visual death state;
|
||||
- turns authoritative relationship-change signals into observer-only transient
|
||||
presentation cues without queuing or persisting them.
|
||||
presentation cues without queuing or persisting them;
|
||||
- derives pantry stock props and interested-villager concern from authoritative
|
||||
storage/opportunity state, including state-only rebuilding after restore.
|
||||
|
||||
### `player/npc/NpcVisual.gd`
|
||||
|
||||
@@ -452,7 +458,8 @@ than duplicating these responsibilities across additional manager nodes.
|
||||
- moves and rotates toward path points;
|
||||
- reports arrival;
|
||||
- presents definition-driven profession silhouettes, carried resources, task
|
||||
glyphs, walk motion, brief trust-gain blossoms, and death state.
|
||||
glyphs, walk motion, brief trust-gain blossoms, opportunity concern, and
|
||||
death state.
|
||||
|
||||
### `player/player.gd` and `player/camera_rig.gd`
|
||||
|
||||
@@ -870,12 +877,16 @@ stable trigger, interested villager, pantry/food target, status, and later
|
||||
resolution event; active evidence remains lasting. A real later NPC deposit or
|
||||
player harvest into the pantry resolves it without quest-only history, economy
|
||||
mutation by the generator, or AI assignment. The compact village/inspector UI
|
||||
shows this lifecycle without changing the elevated follow camera.
|
||||
shows this lifecycle without changing the elevated follow camera. The same
|
||||
facts now have a cardless world expression: empty/low/stocked pantry props, one
|
||||
interested-villager concern cue, and a brief refill response. Save/load rebuilds
|
||||
the stable state without replaying the response.
|
||||
|
||||
Milestone 7 remains in progress. The next systems slice should prove the same
|
||||
lifecycle with a missing-wood `task_blocked` condition and deterministic
|
||||
interested-party death/staleness handling before extracting common opportunity
|
||||
machinery.
|
||||
Milestone 7 remains in progress. The immediate next slice is an outcome-defined
|
||||
20–30 second pantry-crisis demo loop—shortage, finite-resource travel, visible
|
||||
carrying, deposit, and physical recovery—without debug UI. The second
|
||||
missing-wood consumer and deterministic interested-party death/staleness rules
|
||||
follow that visual exit test, before common opportunity machinery is extracted.
|
||||
|
||||
The remaining simulation-garden target still aims for:
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Simulation Garden 01
|
||||
|
||||
Captured: 2026-07-13
|
||||
Captured: 2026-07-14
|
||||
|
||||
## Files
|
||||
|
||||
- `docs/baselines/simulation_garden_01_debug.png`
|
||||
- `docs/baselines/simulation_garden_01_cinematic.png`
|
||||
- `docs/baselines/simulation_garden_02_empty_pantry.png`
|
||||
- `docs/baselines/simulation_garden_02_restocked_pantry.png`
|
||||
- `tools/capture_simulation_garden.gd`
|
||||
|
||||
## Capture Command
|
||||
@@ -32,9 +34,11 @@ The script warms up `main.tscn`, verifies that the simulation and active NPC
|
||||
visuals exist, stages a real food-deposit/trust consequence, a one-food pantry
|
||||
shortage, the hungry interested villager's ordinary gather-food replan, and one
|
||||
wind gust. It captures the F10 debug presentation, then the cinematic
|
||||
presentation with development labels and UI hidden. The trust blossom is
|
||||
restarted only for the second framing so both captures show the same real
|
||||
consequence clearly.
|
||||
presentation with development labels and UI hidden. It lets the earlier trust
|
||||
reaction finish so the crisis remains the single visual idea, then records a
|
||||
tighter empty-pantry view. A real NPC deposit resolves the opportunity and
|
||||
produces the matching restocked view; the capture-only close framing keeps the
|
||||
same elevated third-person perspective and does not change gameplay defaults.
|
||||
|
||||
## Review
|
||||
|
||||
@@ -124,5 +128,22 @@ find food` instead of a stale pre-shortage decision.
|
||||
|
||||
The inspector still fits the 1280×720 frame without adding another panel, and
|
||||
the cinematic capture remains free of UI while preserving the task glyph,
|
||||
trust blossom, and sparse calligraphic gust. Neither gameplay camera defaults
|
||||
nor the established capture framing changed.
|
||||
interested-villager concern, and sparse calligraphic gust. Gameplay camera
|
||||
defaults remain unchanged.
|
||||
|
||||
## July 14 physical-crisis follow-up
|
||||
|
||||
The pantry is no longer a generic crate whose appearance stays constant. Its
|
||||
presentation reads the authoritative food amount and swaps between a large
|
||||
empty terracotta bowl, a low basket with berries/sack, and a fuller stocked
|
||||
arrangement. The active opportunity adds one small empty-bowl concern mark to
|
||||
its interested villager only. A real NPC deposit clears that concern and emits
|
||||
one short amber refill ring/mote response; restoring the same state rebuilds
|
||||
the stock and concern without replaying the transient response.
|
||||
|
||||
The two `simulation_garden_02_*` images form a deterministic before/after pair
|
||||
for editing or public progress posts. They deliberately keep UI panels out and
|
||||
use a tighter version of the same elevated camera language. The broader
|
||||
environment remains prototype-grade; the next useful visual proof is the full
|
||||
20–30 second gather/carry/deposit/recovery loop rather than another isolated
|
||||
ornament pass.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 419 KiB After Width: | Height: | Size: 381 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 457 KiB After Width: | Height: | Size: 422 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://csdwcuh4q0n08"
|
||||
path="res://.godot/imported/simulation_garden_02_empty_pantry.png-8afdd59748244e4706aba1dd7c45f6ef.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/baselines/simulation_garden_02_empty_pantry.png"
|
||||
dest_files=["res://.godot/imported/simulation_garden_02_empty_pantry.png-8afdd59748244e4706aba1dd7c45f6ef.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bt0wgkeg8rhbf"
|
||||
path="res://.godot/imported/simulation_garden_02_restocked_pantry.png-e95422ae67d7940f919886822d5c8ab1.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/baselines/simulation_garden_02_restocked_pantry.png"
|
||||
dest_files=["res://.godot/imported/simulation_garden_02_restocked_pantry.png-e95422ae67d7940f919886822d5c8ab1.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
@@ -64,11 +64,12 @@ home_positions = Array[Vector3]([Vector3(-15, 2, 8.5), Vector3(-13.5, 2, 6.5), V
|
||||
script = ExtResource("10_save")
|
||||
simulation_manager = NodePath("../SimulationManager")
|
||||
|
||||
[node name="WorldViewManager" type="Node" parent="." unique_id=763982891 node_paths=PackedStringArray("simulation_manager", "active_npcs_parent")]
|
||||
[node name="WorldViewManager" type="Node" parent="." unique_id=763982891 node_paths=PackedStringArray("simulation_manager", "active_npcs_parent", "pantry_visual")]
|
||||
script = ExtResource("4_1bvp3")
|
||||
npc_visual_scene = ExtResource("5_lquwl")
|
||||
simulation_manager = NodePath("../SimulationManager")
|
||||
active_npcs_parent = NodePath("../ActiveNPCs")
|
||||
pantry_visual = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry/PantryPresentation")
|
||||
|
||||
[node name="ActiveNPCs" type="Node3D" parent="." unique_id=88374680]
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ signal position_changed(sim_id: int, active_position: Vector3)
|
||||
|
||||
const TRUST_REACTION_BASE_POSITION := Vector3(0.0, 2.72, 0.0)
|
||||
const TRUST_REACTION_START_SCALE := Vector3(0.24, 0.24, 0.24)
|
||||
const CONCERN_BASE_POSITION := Vector3(-0.4, 2.72, 0.0)
|
||||
|
||||
@export var debug_logs := false
|
||||
|
||||
@@ -28,6 +29,7 @@ const TRUST_REACTION_START_SCALE := Vector3(0.24, 0.24, 0.24)
|
||||
@onready var carried_wood_visual: Node3D = $CarriedWood
|
||||
@onready var task_glyph_root: Node3D = $TaskGlyphRoot
|
||||
@onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph
|
||||
@onready var opportunity_concern_root: Node3D = $OpportunityConcernRoot
|
||||
@onready var trust_reaction_root: Node3D = $TrustReactionRoot
|
||||
@onready var trust_reaction_visuals: Array[MeshInstance3D] = [
|
||||
$TrustReactionRoot/Halo,
|
||||
@@ -54,6 +56,7 @@ var dead_material := StandardMaterial3D.new()
|
||||
var debug_overlay_visible := true
|
||||
var walk_phase := 0.0
|
||||
var glyph_phase := 0.0
|
||||
var concern_phase := 0.0
|
||||
var trust_reaction_tween: Tween
|
||||
|
||||
|
||||
@@ -74,6 +77,12 @@ func _process(delta: float) -> void:
|
||||
if task_glyph_root.visible:
|
||||
task_glyph_root.position.y = 2.35 + sin(glyph_phase) * 0.045
|
||||
task_glyph_root.rotation.y = glyph_phase * 0.35
|
||||
if opportunity_concern_root.visible:
|
||||
concern_phase = fmod(concern_phase + delta * 1.65, TAU)
|
||||
opportunity_concern_root.position.y = (CONCERN_BASE_POSITION.y + sin(concern_phase) * 0.045)
|
||||
opportunity_concern_root.rotation.y = sin(concern_phase * 0.7) * 0.16
|
||||
var concern_scale := 0.96 + sin(concern_phase) * 0.04
|
||||
opportunity_concern_root.scale = Vector3.ONE * concern_scale
|
||||
|
||||
var horizontal_speed := Vector2(velocity.x, velocity.z).length()
|
||||
var blend := minf(delta * 9.0, 1.0)
|
||||
@@ -104,6 +113,7 @@ func setup_from_sim(npc: SimNPC) -> void:
|
||||
profession = npc.profession
|
||||
name = "NPC_%s_%s" % [sim_id, npc_name]
|
||||
_stop_trust_gain_reaction()
|
||||
set_opportunity_concern_visible(false)
|
||||
_apply_personal_details()
|
||||
_apply_profession_presentation()
|
||||
set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0)
|
||||
@@ -203,6 +213,17 @@ func set_task_presentation(action_id: StringName, task_state: StringName) -> voi
|
||||
task_glyph_root.visible = task_glyph.mesh != null
|
||||
|
||||
|
||||
func set_opportunity_concern_visible(is_visible: bool) -> void:
|
||||
if opportunity_concern_root == null:
|
||||
return
|
||||
opportunity_concern_root.visible = is_visible and not is_dead_visual
|
||||
opportunity_concern_root.position = CONCERN_BASE_POSITION
|
||||
opportunity_concern_root.rotation = Vector3.ZERO
|
||||
opportunity_concern_root.scale = Vector3.ONE
|
||||
if is_visible:
|
||||
concern_phase = 0.0
|
||||
|
||||
|
||||
func set_debug_overlay_visible(is_visible: bool) -> void:
|
||||
debug_overlay_visible = is_visible
|
||||
if profession_label != null:
|
||||
@@ -399,6 +420,7 @@ func _report_navigation_failure_once() -> void:
|
||||
func apply_dead_visual_state() -> void:
|
||||
is_dead_visual = true
|
||||
_stop_trust_gain_reaction()
|
||||
set_opportunity_concern_visible(false)
|
||||
carried_food_visual.visible = false
|
||||
carried_wood_visual.visible = false
|
||||
profession_prop.visible = false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=21 format=3 uid="uid://dhxxyprqflotq"]
|
||||
[gd_scene load_steps=24 format=3 uid="uid://dhxxyprqflotq"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bha8uf40p3sa0" path="res://player/npc/NpcVisual.gd" id="1_ixfoq"]
|
||||
|
||||
@@ -106,6 +106,28 @@ height = 0.14
|
||||
radial_segments = 8
|
||||
rings = 4
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_concern"]
|
||||
transparency = 1
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.74, 0.42, 0.3, 0.82)
|
||||
emission_enabled = true
|
||||
emission = Color(0.66, 0.24, 0.18, 1)
|
||||
emission_energy_multiplier = 0.28
|
||||
|
||||
[sub_resource type="TorusMesh" id="TorusMesh_concern"]
|
||||
material = SubResource("StandardMaterial3D_concern")
|
||||
inner_radius = 0.13
|
||||
outer_radius = 0.22
|
||||
rings = 14
|
||||
ring_segments = 8
|
||||
|
||||
[sub_resource type="SphereMesh" id="SphereMesh_concern"]
|
||||
material = SubResource("StandardMaterial3D_concern")
|
||||
radius = 0.06
|
||||
height = 0.12
|
||||
radial_segments = 8
|
||||
rings = 4
|
||||
|
||||
[node name="NpcVisual" type="CharacterBody3D"]
|
||||
collision_layer = 2
|
||||
collision_mask = 1
|
||||
@@ -205,6 +227,27 @@ position = Vector3(0, 2.35, 0)
|
||||
|
||||
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
|
||||
|
||||
[node name="OpportunityConcernRoot" type="Node3D" parent="."]
|
||||
visible = false
|
||||
position = Vector3(-0.4, 2.72, 0)
|
||||
|
||||
[node name="EmptyBowl" type="MeshInstance3D" parent="OpportunityConcernRoot"]
|
||||
rotation_degrees = Vector3(72, 0, 0)
|
||||
scale = Vector3(1.15, 0.72, 1.15)
|
||||
mesh = SubResource("TorusMesh_concern")
|
||||
|
||||
[node name="BreathLeft" type="MeshInstance3D" parent="OpportunityConcernRoot"]
|
||||
position = Vector3(-0.14, 0.18, 0)
|
||||
rotation_degrees = Vector3(0, 0, -24)
|
||||
scale = Vector3(0.7, 1.3, 0.7)
|
||||
mesh = SubResource("SphereMesh_concern")
|
||||
|
||||
[node name="BreathRight" type="MeshInstance3D" parent="OpportunityConcernRoot"]
|
||||
position = Vector3(0.08, 0.26, 0)
|
||||
rotation_degrees = Vector3(0, 0, 18)
|
||||
scale = Vector3(0.55, 1.05, 0.55)
|
||||
mesh = SubResource("SphereMesh_concern")
|
||||
|
||||
[node name="TrustReactionRoot" type="Node3D" parent="."]
|
||||
visible = false
|
||||
position = Vector3(0, 2.72, 0)
|
||||
|
||||
@@ -124,6 +124,19 @@ func _run() -> void:
|
||||
main_scene.get_node("UI/VillagePanel/MarginContainer/VillageStatsLabel") as Label
|
||||
)
|
||||
var pantry_state: StorageStateRecord = simulation_manager.get_pantry()
|
||||
var pantry_visual := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry/PantryPresentation")
|
||||
as PantryStockVisual
|
||||
)
|
||||
_check(
|
||||
(
|
||||
pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.STOCKED
|
||||
and pantry_visual.get_node("LowStock").visible
|
||||
and pantry_visual.get_node("FullStock").visible
|
||||
and not pantry_visual.get_node("EmptyStock").visible
|
||||
),
|
||||
"A stocked authoritative pantry should show its physical sacks and berry baskets"
|
||||
)
|
||||
contributor.hunger = 85.0
|
||||
var withdrawn_for_need: float = simulation_manager.economy.withdraw_to_inventory(
|
||||
contributor,
|
||||
@@ -133,6 +146,18 @@ func _run() -> void:
|
||||
_check(withdrawn_for_need > 0.0, "Runtime opportunity setup should empty the real pantry")
|
||||
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||
_check(opportunity != null, "Emptying the pantry should open one known shortage need")
|
||||
if pantry_visual.transition_tween != null:
|
||||
await pantry_visual.transition_tween.finished
|
||||
_check(
|
||||
(
|
||||
pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.EMPTY
|
||||
and pantry_visual.get_node("EmptyStock").visible
|
||||
and not pantry_visual.get_node("LowStock").visible
|
||||
and contributor_visual.get_node("OpportunityConcernRoot").visible
|
||||
and not listener_visual.get_node("OpportunityConcernRoot").visible
|
||||
),
|
||||
"The real shortage should empty the pantry prop and concern only its interested villager"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
"Village need" in village_stats_label.text
|
||||
@@ -169,6 +194,14 @@ func _run() -> void:
|
||||
),
|
||||
"Opportunity presentation should resolve from a real pantry deposit"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.STOCKED
|
||||
and pantry_visual.get_node("RefillResponse").visible
|
||||
and not contributor_visual.get_node("OpportunityConcernRoot").visible
|
||||
),
|
||||
"Real supply should restore visible stock, play one refill response, and clear concern"
|
||||
)
|
||||
_check(
|
||||
"Village need" not in village_stats_label.text,
|
||||
"Resolved opportunities should leave the compact village summary"
|
||||
@@ -198,6 +231,25 @@ func _run() -> void:
|
||||
_check(player_need_withdrawal > 0.0, "Player resolution setup should reopen a real need")
|
||||
var player_opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||
_check(player_opportunity != null, "A later shortage should open a new opportunity record")
|
||||
var active_need_json: String = simulation_manager.serialize_state()
|
||||
var old_contributor_visual := contributor_visual
|
||||
_check(
|
||||
simulation_manager.restore_state_from_json(active_need_json),
|
||||
"The visible shortage should survive a valid schema-v8 restore"
|
||||
)
|
||||
await process_frame
|
||||
contributor_visual = world_view.active_npc_visuals[contributor.id]
|
||||
player_opportunity = simulation_manager.get_active_opportunity()
|
||||
_check(
|
||||
(
|
||||
contributor_visual != old_contributor_visual
|
||||
and contributor_visual.get_node("OpportunityConcernRoot").visible
|
||||
and pantry_visual.get_stock_level() == PantryStockVisual.StockLevel.EMPTY
|
||||
and pantry_visual.get_node("EmptyStock").visible
|
||||
and not pantry_visual.get_node("RefillResponse").visible
|
||||
),
|
||||
"Restoration should rebuild concern and empty stock without replaying the refill response"
|
||||
)
|
||||
var player := main_scene.get_node("Player") as Node3D
|
||||
var opportunity_bush := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/BerryBush_01") as ResourceNode
|
||||
|
||||
@@ -3,6 +3,8 @@ extends SceneTree
|
||||
const SCENE_PATH := "res://main.tscn"
|
||||
const DEBUG_OUTPUT_PATH := "res://docs/baselines/simulation_garden_01_debug.png"
|
||||
const CINEMATIC_OUTPUT_PATH := "res://docs/baselines/simulation_garden_01_cinematic.png"
|
||||
const EMPTY_PANTRY_OUTPUT_PATH := "res://docs/baselines/simulation_garden_02_empty_pantry.png"
|
||||
const RESTOCKED_PANTRY_OUTPUT_PATH := "res://docs/baselines/simulation_garden_02_restocked_pantry.png"
|
||||
const CAPTURE_SIZE := Vector2i(1280, 720)
|
||||
const WARMUP_FRAMES := 540
|
||||
|
||||
@@ -45,7 +47,7 @@ func _run() -> void:
|
||||
quit(1)
|
||||
return
|
||||
await _stage_wind_gust(main_scene)
|
||||
await _settle_trust_reaction()
|
||||
await _settle_trust_reaction(main_scene)
|
||||
var debug_analysis := _capture(DEBUG_OUTPUT_PATH)
|
||||
if debug_analysis.is_empty():
|
||||
quit(1)
|
||||
@@ -53,21 +55,38 @@ func _run() -> void:
|
||||
|
||||
if demo_controller.has_method("toggle_debug_overlay") and demo_controller.debug_overlay_visible:
|
||||
demo_controller.toggle_debug_overlay()
|
||||
if not _restart_trust_reaction(main_scene):
|
||||
quit(1)
|
||||
return
|
||||
await _stage_wind_gust(main_scene)
|
||||
await _settle_trust_reaction()
|
||||
|
||||
var cinematic_analysis := _capture(CINEMATIC_OUTPUT_PATH)
|
||||
if cinematic_analysis.is_empty():
|
||||
quit(1)
|
||||
return
|
||||
_apply_pantry_capture_camera(main_scene)
|
||||
await process_frame
|
||||
await process_frame
|
||||
var empty_pantry_analysis := _capture(EMPTY_PANTRY_OUTPUT_PATH)
|
||||
if empty_pantry_analysis.is_empty() or not _stage_pantry_restock(main_scene):
|
||||
quit(1)
|
||||
return
|
||||
for _frame in 12:
|
||||
await process_frame
|
||||
var restocked_pantry_analysis := _capture(RESTOCKED_PANTRY_OUTPUT_PATH)
|
||||
if restocked_pantry_analysis.is_empty():
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print(
|
||||
(
|
||||
"[TOOL] Saved Simulation Garden captures | debug %s | cinematic %s"
|
||||
% [debug_analysis, cinematic_analysis]
|
||||
(
|
||||
"[TOOL] Saved Simulation Garden captures | debug %s | cinematic %s | "
|
||||
+ "empty pantry %s | restocked pantry %s"
|
||||
)
|
||||
% [
|
||||
debug_analysis,
|
||||
cinematic_analysis,
|
||||
empty_pantry_analysis,
|
||||
restocked_pantry_analysis,
|
||||
]
|
||||
)
|
||||
)
|
||||
quit(0)
|
||||
@@ -145,6 +164,20 @@ func _stage_social_history(main_scene: Node) -> bool:
|
||||
|
||||
func _stage_pantry_need(main_scene: Node, actor: SimNPC, interested: SimNPC) -> bool:
|
||||
var simulation_manager := main_scene.get_node("SimulationManager")
|
||||
var world_view := main_scene.get_node("WorldViewManager")
|
||||
var pantry_site := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||
)
|
||||
var actor_visual := world_view.active_npc_visuals.get(actor.id) as Node3D
|
||||
var interested_visual := world_view.active_npc_visuals.get(interested.id) as Node3D
|
||||
if actor_visual == null or interested_visual == null:
|
||||
push_error("Simulation Garden pantry staging needs both active NPC visuals")
|
||||
return false
|
||||
var pantry_arrival := pantry_site.get_interaction_position()
|
||||
actor_visual.global_position = pantry_arrival + Vector3(-0.65, 0.0, 0.15)
|
||||
interested_visual.global_position = pantry_arrival + Vector3(0.65, 0.0, 0.15)
|
||||
simulation_manager.synchronize_npc_position(actor.id, actor_visual.global_position)
|
||||
simulation_manager.synchronize_npc_position(interested.id, interested_visual.global_position)
|
||||
for npc in simulation_manager.npcs:
|
||||
npc.hunger = minf(npc.hunger, 70.0)
|
||||
actor.hunger = 60.0
|
||||
@@ -189,21 +222,6 @@ func _stage_pantry_need(main_scene: Node, actor: SimNPC, interested: SimNPC) ->
|
||||
return true
|
||||
|
||||
|
||||
func _restart_trust_reaction(main_scene: Node) -> bool:
|
||||
var simulation_manager := main_scene.get_node("SimulationManager")
|
||||
var world_view := main_scene.get_node("WorldViewManager")
|
||||
if simulation_manager.npcs.size() < 2:
|
||||
push_error("Simulation Garden trust-reaction staging needs two NPCs")
|
||||
return false
|
||||
var observer: SimNPC = simulation_manager.npcs[1]
|
||||
var observer_visual: Node3D = world_view.active_npc_visuals.get(observer.id)
|
||||
if observer_visual == null or not observer_visual.has_method("play_trust_gain_reaction"):
|
||||
push_error("Simulation Garden trust-reaction staging needs an active observer visual")
|
||||
return false
|
||||
observer_visual.play_trust_gain_reaction()
|
||||
return observer_visual.get_node("TrustReactionRoot").visible
|
||||
|
||||
|
||||
func _stage_wind_gust(main_scene: Node) -> void:
|
||||
var gust_field := (
|
||||
main_scene.get_node_or_null("JajceWorld/AtmosphereRoot/WindGustField") as WindGustField
|
||||
@@ -215,8 +233,44 @@ func _stage_wind_gust(main_scene: Node) -> void:
|
||||
await process_frame
|
||||
|
||||
|
||||
func _settle_trust_reaction() -> void:
|
||||
for _frame in 12:
|
||||
func _stage_pantry_restock(main_scene: Node) -> bool:
|
||||
var simulation_manager := main_scene.get_node("SimulationManager")
|
||||
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||
if opportunity == null:
|
||||
push_error("Simulation Garden restock staging needs an active pantry opportunity")
|
||||
return false
|
||||
var contributor: SimNPC = simulation_manager.npcs[0]
|
||||
var deposited: float = simulation_manager.economy.deposit_inventory(
|
||||
contributor, SimulationIds.RESOURCE_FOOD
|
||||
)
|
||||
var pantry_visual := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry/PantryPresentation")
|
||||
as PantryStockVisual
|
||||
)
|
||||
var world_view := main_scene.get_node("WorldViewManager")
|
||||
var interested_visual: Node3D = world_view.active_npc_visuals.get(
|
||||
opportunity.get_interested_npc_id()
|
||||
)
|
||||
if (
|
||||
deposited <= 0.0
|
||||
or simulation_manager.get_active_opportunity() != null
|
||||
or pantry_visual.get_stock_level() != PantryStockVisual.StockLevel.LOW
|
||||
or not pantry_visual.get_node("RefillResponse").visible
|
||||
or interested_visual.get_node("OpportunityConcernRoot").visible
|
||||
):
|
||||
push_error("Simulation Garden restock staging did not produce the real visual payoff")
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _settle_trust_reaction(main_scene: Node) -> void:
|
||||
var simulation_manager := main_scene.get_node("SimulationManager")
|
||||
var world_view := main_scene.get_node("WorldViewManager")
|
||||
var observer: SimNPC = simulation_manager.npcs[1]
|
||||
var observer_visual: Node3D = world_view.active_npc_visuals.get(observer.id)
|
||||
var reaction_tween: Tween = observer_visual.get("trust_reaction_tween")
|
||||
if reaction_tween != null and reaction_tween.is_running():
|
||||
await reaction_tween.finished
|
||||
await process_frame
|
||||
|
||||
|
||||
@@ -268,8 +322,20 @@ func _apply_capture_camera(main_scene: Node) -> void:
|
||||
var camera_rig := main_scene.get_node("CameraRig")
|
||||
if camera_rig.has_method("apply_presentation_preset"):
|
||||
camera_rig.pitch_degrees = -42.0
|
||||
camera_rig.apply_presentation_preset(Vector3(-2.0, 1.2, -0.5), 128.0, 0.95)
|
||||
camera_rig.apply_presentation_preset(Vector3(-1.5, 1.2, -4.0), 128.0, 0.7)
|
||||
camera_rig.set_physics_process(false)
|
||||
|
||||
var camera := main_scene.get_node("CameraRig/Camera3D") as Camera3D
|
||||
camera.fov = 62.0
|
||||
|
||||
|
||||
func _apply_pantry_capture_camera(main_scene: Node) -> void:
|
||||
var pantry := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||
)
|
||||
var camera_rig := main_scene.get_node("CameraRig")
|
||||
var pantry_focus := pantry.global_position + Vector3(0.0, 0.85, 0.72)
|
||||
camera_rig.apply_presentation_preset(pantry_focus, 128.0, 0.0)
|
||||
camera_rig.look_at(pantry_focus, Vector3.UP)
|
||||
var camera := main_scene.get_node("CameraRig/Camera3D") as Camera3D
|
||||
camera.fov = 44.0
|
||||
|
||||
+200
-7
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bj2aigpu1nug6
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user