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
|
-> EventKnowledgeSystem records, ranks, transfers, and retains bounded knowledge
|
||||||
-> RelationshipSystem applies evidence-gated social consequences
|
-> RelationshipSystem applies evidence-gated social consequences
|
||||||
-> FoodShortageOpportunitySystem projects one known unresolved need
|
-> 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
|
-> ActiveWorldAdapter supplies loaded-world positions/capacity
|
||||||
-> NpcVisual performs local navigation, animation, and transient reactions
|
-> 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
|
`SimulationManager` is the scene-tree façade for the simulation. It owns the
|
||||||
@@ -89,8 +90,10 @@ improving ownership.
|
|||||||
opportunity, or reservation state.
|
opportunity, or reservation state.
|
||||||
- Inspector history is a read-only façade query over objective events and
|
- Inspector history is a read-only façade query over objective events and
|
||||||
retained knowledge. Opportunity presentation is likewise query-only.
|
retained knowledge. Opportunity presentation is likewise query-only.
|
||||||
Relationship cues consume state-change signals and are intentionally absent
|
Relationship cues, interested-villager concern, physical pantry stock, and
|
||||||
from saves and checksums.
|
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
|
- Resource changes go through `ResourceStateRecord`, NPC inventory, and
|
||||||
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
||||||
- New mutable features define serialization and deterministic continuation at
|
- 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
|
NPC or player supply actor. The repeatable debug capture stages this from
|
||||||
live simulation state; world framing and the elevated follow camera remain
|
live simulation state; world framing and the elevated follow camera remain
|
||||||
unchanged.
|
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:
|
Next:
|
||||||
|
|
||||||
1. Prove the same opportunity lifecycle with one missing-wood `task_blocked`
|
1. Package one 20–30 second, repeatable pantry-crisis sequence that communicates
|
||||||
condition before extracting shared generation rules, as sequenced in
|
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`.
|
`LEARNING_ROADMAP.md`.
|
||||||
|
|
||||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
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-only event, acceptance, reward, dialogue, multi-hop rumour, or generic
|
||||||
quest framework.
|
quest framework.
|
||||||
|
|
||||||
Milestone 7 is not complete. The practical next slice is a second bounded
|
The first build-in-public presentation checkpoint for that proof is also
|
||||||
unresolved-condition consumer: surface missing-wood `task_blocked` facts from
|
complete:
|
||||||
patrol or study as a cared-about woodpile-restock need, resolve it through real
|
|
||||||
NPC/player wood supply, define deterministic interested-party death/staleness
|
- authoritative pantry food selects a physical empty, low, or stocked pantry
|
||||||
handling, and only then extract the lifecycle shared by two proven consumers.
|
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:
|
Recently completed:
|
||||||
|
|
||||||
|
|||||||
+24
-13
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Agent-facing context for understanding the project quickly.
|
> 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.
|
> source of truth when this document and the implementation differ.
|
||||||
|
|
||||||
See the [documentation map](README.md) for the authority and scope of each plan.
|
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
|
`docs/baselines/jajce_lookdev_01.png` with
|
||||||
`tools/capture_jajce_lookdev.gd`
|
`tools/capture_jajce_lookdev.gd`
|
||||||
- **Runtime baseline:** `Simulation Garden 01` is captured as paired debug and
|
- **Runtime baseline:** `Simulation Garden 01` is captured as paired debug and
|
||||||
cinematic `main.tscn` images under `docs/baselines/` with
|
cinematic `main.tscn` images plus an empty/restocked pantry close pair under
|
||||||
`tools/capture_simulation_garden.gd`
|
`docs/baselines/` with `tools/capture_simulation_garden.gd`
|
||||||
- **Terrain compatibility floor:** Godot 4.4 according to the bundled extension
|
- **Terrain compatibility floor:** Godot 4.4 according to the bundled extension
|
||||||
- **Version control:** Git
|
- **Version control:** Git
|
||||||
- **Primary branch:** `main`
|
- **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;
|
- deterministic shaped terrain data and six game-owned surface layers;
|
||||||
- fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke;
|
- fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke;
|
||||||
- authored dirt path strips that make the current village task loop readable;
|
- authored dirt path strips that make the current village task loop readable;
|
||||||
- compact silhouette props for the ridge landmark, pantry, guard, study, and
|
- compact silhouette props for the ridge landmark, guard, study, and rest
|
||||||
rest sites;
|
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
|
- warm sky, fog, shadows, coherent wind-reactive foliage, and breeze-aligned
|
||||||
chimney smoke;
|
chimney smoke;
|
||||||
- one sparse terrain-aware calligraphic gust field with two or three soft
|
- 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
|
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
|
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
|
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
|
## Runtime architecture
|
||||||
|
|
||||||
@@ -441,7 +445,9 @@ than duplicating these responsibilities across additional manager nodes.
|
|||||||
- synchronizes successful visual movement into authoritative NPC position;
|
- synchronizes successful visual movement into authoritative NPC position;
|
||||||
- applies visual death state;
|
- applies visual death state;
|
||||||
- turns authoritative relationship-change signals into observer-only transient
|
- 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`
|
### `player/npc/NpcVisual.gd`
|
||||||
|
|
||||||
@@ -452,7 +458,8 @@ than duplicating these responsibilities across additional manager nodes.
|
|||||||
- moves and rotates toward path points;
|
- moves and rotates toward path points;
|
||||||
- reports arrival;
|
- reports arrival;
|
||||||
- presents definition-driven profession silhouettes, carried resources, task
|
- 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`
|
### `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
|
resolution event; active evidence remains lasting. A real later NPC deposit or
|
||||||
player harvest into the pantry resolves it without quest-only history, economy
|
player harvest into the pantry resolves it without quest-only history, economy
|
||||||
mutation by the generator, or AI assignment. The compact village/inspector UI
|
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
|
Milestone 7 remains in progress. The immediate next slice is an outcome-defined
|
||||||
lifecycle with a missing-wood `task_blocked` condition and deterministic
|
20–30 second pantry-crisis demo loop—shortage, finite-resource travel, visible
|
||||||
interested-party death/staleness handling before extracting common opportunity
|
carrying, deposit, and physical recovery—without debug UI. The second
|
||||||
machinery.
|
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:
|
The remaining simulation-garden target still aims for:
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# Simulation Garden 01
|
# Simulation Garden 01
|
||||||
|
|
||||||
Captured: 2026-07-13
|
Captured: 2026-07-14
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
- `docs/baselines/simulation_garden_01_debug.png`
|
- `docs/baselines/simulation_garden_01_debug.png`
|
||||||
- `docs/baselines/simulation_garden_01_cinematic.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`
|
- `tools/capture_simulation_garden.gd`
|
||||||
|
|
||||||
## Capture Command
|
## 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
|
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
|
shortage, the hungry interested villager's ordinary gather-food replan, and one
|
||||||
wind gust. It captures the F10 debug presentation, then the cinematic
|
wind gust. It captures the F10 debug presentation, then the cinematic
|
||||||
presentation with development labels and UI hidden. The trust blossom is
|
presentation with development labels and UI hidden. It lets the earlier trust
|
||||||
restarted only for the second framing so both captures show the same real
|
reaction finish so the crisis remains the single visual idea, then records a
|
||||||
consequence clearly.
|
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
|
## 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 inspector still fits the 1280×720 frame without adding another panel, and
|
||||||
the cinematic capture remains free of UI while preserving the task glyph,
|
the cinematic capture remains free of UI while preserving the task glyph,
|
||||||
trust blossom, and sparse calligraphic gust. Neither gameplay camera defaults
|
interested-villager concern, and sparse calligraphic gust. Gameplay camera
|
||||||
nor the established capture framing changed.
|
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")
|
script = ExtResource("10_save")
|
||||||
simulation_manager = NodePath("../SimulationManager")
|
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")
|
script = ExtResource("4_1bvp3")
|
||||||
npc_visual_scene = ExtResource("5_lquwl")
|
npc_visual_scene = ExtResource("5_lquwl")
|
||||||
simulation_manager = NodePath("../SimulationManager")
|
simulation_manager = NodePath("../SimulationManager")
|
||||||
active_npcs_parent = NodePath("../ActiveNPCs")
|
active_npcs_parent = NodePath("../ActiveNPCs")
|
||||||
|
pantry_visual = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry/PantryPresentation")
|
||||||
|
|
||||||
[node name="ActiveNPCs" type="Node3D" parent="." unique_id=88374680]
|
[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_BASE_POSITION := Vector3(0.0, 2.72, 0.0)
|
||||||
const TRUST_REACTION_START_SCALE := Vector3(0.24, 0.24, 0.24)
|
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
|
@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 carried_wood_visual: Node3D = $CarriedWood
|
||||||
@onready var task_glyph_root: Node3D = $TaskGlyphRoot
|
@onready var task_glyph_root: Node3D = $TaskGlyphRoot
|
||||||
@onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph
|
@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_root: Node3D = $TrustReactionRoot
|
||||||
@onready var trust_reaction_visuals: Array[MeshInstance3D] = [
|
@onready var trust_reaction_visuals: Array[MeshInstance3D] = [
|
||||||
$TrustReactionRoot/Halo,
|
$TrustReactionRoot/Halo,
|
||||||
@@ -54,6 +56,7 @@ var dead_material := StandardMaterial3D.new()
|
|||||||
var debug_overlay_visible := true
|
var debug_overlay_visible := true
|
||||||
var walk_phase := 0.0
|
var walk_phase := 0.0
|
||||||
var glyph_phase := 0.0
|
var glyph_phase := 0.0
|
||||||
|
var concern_phase := 0.0
|
||||||
var trust_reaction_tween: Tween
|
var trust_reaction_tween: Tween
|
||||||
|
|
||||||
|
|
||||||
@@ -74,6 +77,12 @@ func _process(delta: float) -> void:
|
|||||||
if task_glyph_root.visible:
|
if task_glyph_root.visible:
|
||||||
task_glyph_root.position.y = 2.35 + sin(glyph_phase) * 0.045
|
task_glyph_root.position.y = 2.35 + sin(glyph_phase) * 0.045
|
||||||
task_glyph_root.rotation.y = glyph_phase * 0.35
|
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 horizontal_speed := Vector2(velocity.x, velocity.z).length()
|
||||||
var blend := minf(delta * 9.0, 1.0)
|
var blend := minf(delta * 9.0, 1.0)
|
||||||
@@ -104,6 +113,7 @@ func setup_from_sim(npc: SimNPC) -> void:
|
|||||||
profession = npc.profession
|
profession = npc.profession
|
||||||
name = "NPC_%s_%s" % [sim_id, npc_name]
|
name = "NPC_%s_%s" % [sim_id, npc_name]
|
||||||
_stop_trust_gain_reaction()
|
_stop_trust_gain_reaction()
|
||||||
|
set_opportunity_concern_visible(false)
|
||||||
_apply_personal_details()
|
_apply_personal_details()
|
||||||
_apply_profession_presentation()
|
_apply_profession_presentation()
|
||||||
set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0)
|
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
|
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:
|
func set_debug_overlay_visible(is_visible: bool) -> void:
|
||||||
debug_overlay_visible = is_visible
|
debug_overlay_visible = is_visible
|
||||||
if profession_label != null:
|
if profession_label != null:
|
||||||
@@ -399,6 +420,7 @@ func _report_navigation_failure_once() -> void:
|
|||||||
func apply_dead_visual_state() -> void:
|
func apply_dead_visual_state() -> void:
|
||||||
is_dead_visual = true
|
is_dead_visual = true
|
||||||
_stop_trust_gain_reaction()
|
_stop_trust_gain_reaction()
|
||||||
|
set_opportunity_concern_visible(false)
|
||||||
carried_food_visual.visible = false
|
carried_food_visual.visible = false
|
||||||
carried_wood_visual.visible = false
|
carried_wood_visual.visible = false
|
||||||
profession_prop.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"]
|
[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
|
radial_segments = 8
|
||||||
rings = 4
|
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"]
|
[node name="NpcVisual" type="CharacterBody3D"]
|
||||||
collision_layer = 2
|
collision_layer = 2
|
||||||
collision_mask = 1
|
collision_mask = 1
|
||||||
@@ -205,6 +227,27 @@ position = Vector3(0, 2.35, 0)
|
|||||||
|
|
||||||
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
|
[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="."]
|
[node name="TrustReactionRoot" type="Node3D" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
position = Vector3(0, 2.72, 0)
|
position = Vector3(0, 2.72, 0)
|
||||||
|
|||||||
@@ -124,6 +124,19 @@ func _run() -> void:
|
|||||||
main_scene.get_node("UI/VillagePanel/MarginContainer/VillageStatsLabel") as Label
|
main_scene.get_node("UI/VillagePanel/MarginContainer/VillageStatsLabel") as Label
|
||||||
)
|
)
|
||||||
var pantry_state: StorageStateRecord = simulation_manager.get_pantry()
|
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
|
contributor.hunger = 85.0
|
||||||
var withdrawn_for_need: float = simulation_manager.economy.withdraw_to_inventory(
|
var withdrawn_for_need: float = simulation_manager.economy.withdraw_to_inventory(
|
||||||
contributor,
|
contributor,
|
||||||
@@ -133,6 +146,18 @@ func _run() -> void:
|
|||||||
_check(withdrawn_for_need > 0.0, "Runtime opportunity setup should empty the real pantry")
|
_check(withdrawn_for_need > 0.0, "Runtime opportunity setup should empty the real pantry")
|
||||||
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||||
_check(opportunity != null, "Emptying the pantry should open one known shortage need")
|
_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(
|
_check(
|
||||||
(
|
(
|
||||||
"Village need" in village_stats_label.text
|
"Village need" in village_stats_label.text
|
||||||
@@ -169,6 +194,14 @@ func _run() -> void:
|
|||||||
),
|
),
|
||||||
"Opportunity presentation should resolve from a real pantry deposit"
|
"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(
|
_check(
|
||||||
"Village need" not in village_stats_label.text,
|
"Village need" not in village_stats_label.text,
|
||||||
"Resolved opportunities should leave the compact village summary"
|
"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")
|
_check(player_need_withdrawal > 0.0, "Player resolution setup should reopen a real need")
|
||||||
var player_opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
var player_opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
|
||||||
_check(player_opportunity != null, "A later shortage should open a new opportunity record")
|
_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 player := main_scene.get_node("Player") as Node3D
|
||||||
var opportunity_bush := (
|
var opportunity_bush := (
|
||||||
main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/BerryBush_01") as ResourceNode
|
main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/BerryBush_01") as ResourceNode
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ extends SceneTree
|
|||||||
const SCENE_PATH := "res://main.tscn"
|
const SCENE_PATH := "res://main.tscn"
|
||||||
const DEBUG_OUTPUT_PATH := "res://docs/baselines/simulation_garden_01_debug.png"
|
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 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 CAPTURE_SIZE := Vector2i(1280, 720)
|
||||||
const WARMUP_FRAMES := 540
|
const WARMUP_FRAMES := 540
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ func _run() -> void:
|
|||||||
quit(1)
|
quit(1)
|
||||||
return
|
return
|
||||||
await _stage_wind_gust(main_scene)
|
await _stage_wind_gust(main_scene)
|
||||||
await _settle_trust_reaction()
|
await _settle_trust_reaction(main_scene)
|
||||||
var debug_analysis := _capture(DEBUG_OUTPUT_PATH)
|
var debug_analysis := _capture(DEBUG_OUTPUT_PATH)
|
||||||
if debug_analysis.is_empty():
|
if debug_analysis.is_empty():
|
||||||
quit(1)
|
quit(1)
|
||||||
@@ -53,21 +55,38 @@ func _run() -> void:
|
|||||||
|
|
||||||
if demo_controller.has_method("toggle_debug_overlay") and demo_controller.debug_overlay_visible:
|
if demo_controller.has_method("toggle_debug_overlay") and demo_controller.debug_overlay_visible:
|
||||||
demo_controller.toggle_debug_overlay()
|
demo_controller.toggle_debug_overlay()
|
||||||
if not _restart_trust_reaction(main_scene):
|
|
||||||
quit(1)
|
|
||||||
return
|
|
||||||
await _stage_wind_gust(main_scene)
|
await _stage_wind_gust(main_scene)
|
||||||
await _settle_trust_reaction()
|
|
||||||
|
|
||||||
var cinematic_analysis := _capture(CINEMATIC_OUTPUT_PATH)
|
var cinematic_analysis := _capture(CINEMATIC_OUTPUT_PATH)
|
||||||
if cinematic_analysis.is_empty():
|
if cinematic_analysis.is_empty():
|
||||||
quit(1)
|
quit(1)
|
||||||
return
|
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(
|
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)
|
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:
|
func _stage_pantry_need(main_scene: Node, actor: SimNPC, interested: SimNPC) -> bool:
|
||||||
var simulation_manager := main_scene.get_node("SimulationManager")
|
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:
|
for npc in simulation_manager.npcs:
|
||||||
npc.hunger = minf(npc.hunger, 70.0)
|
npc.hunger = minf(npc.hunger, 70.0)
|
||||||
actor.hunger = 60.0
|
actor.hunger = 60.0
|
||||||
@@ -189,21 +222,6 @@ func _stage_pantry_need(main_scene: Node, actor: SimNPC, interested: SimNPC) ->
|
|||||||
return true
|
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:
|
func _stage_wind_gust(main_scene: Node) -> void:
|
||||||
var gust_field := (
|
var gust_field := (
|
||||||
main_scene.get_node_or_null("JajceWorld/AtmosphereRoot/WindGustField") as WindGustField
|
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
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
func _settle_trust_reaction() -> void:
|
func _stage_pantry_restock(main_scene: Node) -> bool:
|
||||||
for _frame in 12:
|
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
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
@@ -268,8 +322,20 @@ func _apply_capture_camera(main_scene: Node) -> void:
|
|||||||
var camera_rig := main_scene.get_node("CameraRig")
|
var camera_rig := main_scene.get_node("CameraRig")
|
||||||
if camera_rig.has_method("apply_presentation_preset"):
|
if camera_rig.has_method("apply_presentation_preset"):
|
||||||
camera_rig.pitch_degrees = -42.0
|
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)
|
camera_rig.set_physics_process(false)
|
||||||
|
|
||||||
var camera := main_scene.get_node("CameraRig/Camera3D") as Camera3D
|
var camera := main_scene.get_node("CameraRig/Camera3D") as Camera3D
|
||||||
camera.fov = 62.0
|
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="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
||||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
[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="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="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="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"]
|
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||||
_shader_parameters = {
|
_shader_parameters = {
|
||||||
@@ -74,6 +75,83 @@ size = Vector3(1.0, 0.08, 0.5)
|
|||||||
material = SubResource("Material_site_canvas")
|
material = SubResource("Material_site_canvas")
|
||||||
size = Vector3(0.5, 0.62, 0.42)
|
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"]
|
[sub_resource type="BoxMesh" id="Mesh_study_book"]
|
||||||
material = SubResource("Material_site_book")
|
material = SubResource("Material_site_book")
|
||||||
size = Vector3(0.42, 0.08, 0.3)
|
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")]
|
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
|
||||||
position = Vector3(0, 0, -8)
|
position = Vector3(0, 0, -8)
|
||||||
|
|
||||||
|
[node name="Visual" parent="WorldObjects/StorageSites/VillagePantry"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
[node name="VillageWoodpile" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
|
[node name="VillageWoodpile" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
|
||||||
position = Vector3(8, 0, -12)
|
position = Vector3(8, 0, -12)
|
||||||
storage_id = &"village_woodpile"
|
storage_id = &"village_woodpile"
|
||||||
@@ -471,16 +552,128 @@ position = Vector3(-1.05, 1.85, 1.25)
|
|||||||
rotation_degrees = Vector3(0, 90, 0)
|
rotation_degrees = Vector3(0, 90, 0)
|
||||||
mesh = SubResource("Mesh_site_sign")
|
mesh = SubResource("Mesh_site_sign")
|
||||||
|
|
||||||
[node name="PantrySack_A" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
|
[node name="PantryPresentation" type="Node3D" parent="WorldObjects/StorageSites/VillagePantry"]
|
||||||
position = Vector3(-0.95, 0.3, -0.65)
|
position = Vector3(0, 0, -0.48)
|
||||||
scale = Vector3(0.65, 1.0, 0.65)
|
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")
|
mesh = SubResource("Mesh_site_sack")
|
||||||
|
|
||||||
[node name="PantrySack_B" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
|
[node name="Basket" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry/PantryPresentation/LowStock"]
|
||||||
position = Vector3(-1.35, 0.22, -0.25)
|
position = Vector3(0.36, 0.32, -0.02)
|
||||||
scale = Vector3(0.5, 0.75, 0.5)
|
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")
|
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="ActivitySites" type="Node3D" parent="WorldObjects"]
|
||||||
|
|
||||||
[node name="GuardPost" type="Node3D" parent="WorldObjects/ActivitySites"]
|
[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"]
|
[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")
|
material = SubResource("Material_cloth")
|
||||||
size = Vector3(1.75, 0.12, 1.75)
|
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"]
|
[node name="StorageNode" type="Node3D"]
|
||||||
script = ExtResource("1_storage")
|
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)
|
transform = Transform3D(1, 0, 0, 0, 0.7, 0.7173561, 0, -0.7173561, 0.7, 0, 1.15, -0.06)
|
||||||
mesh = SubResource("Mesh_lid")
|
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="."]
|
[node name="InteractionPoint" type="Marker3D" parent="."]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2.0)
|
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 npc_visual_scene: PackedScene
|
||||||
@export var simulation_manager: Node
|
@export var simulation_manager: Node
|
||||||
@export var active_npcs_parent: Node3D
|
@export var active_npcs_parent: Node3D
|
||||||
|
@export var pantry_visual: Node3D
|
||||||
|
|
||||||
var active_npc_visuals := {}
|
var active_npc_visuals := {}
|
||||||
|
|
||||||
@@ -44,11 +45,24 @@ func initialize_world_view() -> void:
|
|||||||
simulation_manager.relationship_changed.connect(_on_relationship_changed)
|
simulation_manager.relationship_changed.connect(_on_relationship_changed)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: SimulationManager has no relationship_changed signal")
|
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"):
|
if simulation_manager.has_signal("state_restored"):
|
||||||
simulation_manager.state_restored.connect(_on_simulation_state_restored)
|
simulation_manager.state_restored.connect(_on_simulation_state_restored)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: SimulationManager has no state_restored signal")
|
push_error("WorldViewManager: SimulationManager has no state_restored signal")
|
||||||
spawn_initial_npcs()
|
spawn_initial_npcs()
|
||||||
|
_refresh_world_state_presentation(false)
|
||||||
|
|
||||||
|
|
||||||
func spawn_initial_npcs() -> void:
|
func spawn_initial_npcs() -> void:
|
||||||
@@ -86,6 +100,7 @@ func spawn_npc_visual(npc: SimNPC) -> void:
|
|||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: NPCVisual has no position_changed signal")
|
push_error("WorldViewManager: NPCVisual has no position_changed signal")
|
||||||
active_npc_visuals[npc.id] = visual
|
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])
|
debug_log("Spawned visual for %s at %s" % [npc.npc_name, npc.position])
|
||||||
if npc.is_dead:
|
if npc.is_dead:
|
||||||
visual.apply_dead_visual_state()
|
visual.apply_dead_visual_state()
|
||||||
@@ -200,8 +215,62 @@ func _on_relationship_changed(
|
|||||||
push_error("WorldViewManager: NPCVisual has no play_trust_gain_reaction method")
|
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:
|
func _on_simulation_state_restored() -> void:
|
||||||
for visual in active_npc_visuals.values():
|
for visual in active_npc_visuals.values():
|
||||||
visual.queue_free()
|
visual.queue_free()
|
||||||
active_npc_visuals.clear()
|
active_npc_visuals.clear()
|
||||||
spawn_initial_npcs()
|
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