feat: add deterministic goat routine

This commit is contained in:
Rijad Zuzo
2026-07-26 23:52:56 +02:00
parent f79c2bb630
commit e3aa3440a2
61 changed files with 1393 additions and 1057 deletions
+1
View File
@@ -0,0 +1 @@
+29 -13
View File
@@ -16,7 +16,7 @@ SimulationClock
-> ActionSelectionSystem chooses an action
-> ActionTargetResolver resolves a stable target ID
-> VillageEconomy performs inventory/storage transactions
-> AnimalCareSystem advances animal needs and composes feeding
-> AnimalCareSystem advances animal needs, routines, and feeding
-> SimulationEventLog records completed facts
-> EventKnowledgeSystem records, ranks, transfers, and retains bounded knowledge
-> RelationshipSystem applies evidence-gated social consequences
@@ -25,6 +25,7 @@ SimulationClock
-> ActiveWorldAdapter supplies loaded-world positions/capacity
-> LoadedResourceSpatialIndex bounds finite-anchor discovery
-> NpcVisual performs local navigation, animation, and transient reactions
-> AnimalNode follows saved animal destinations through loaded navigation
-> PantryStockVisual derives physical stock arrangement from storage state
```
@@ -36,9 +37,10 @@ would otherwise obscure that lifecycle:
- `simulation/actions/` owns selection, progress, and target resolution;
- `simulation/economy/VillageEconomy.gd` owns storage/inventory transactions
and keeps village resource summaries synchronized;
- `simulation/animals/AnimalCareSystem.gd` owns animal records, hunger
advancement, loaded binding, feed reservations, and the exact conserved
pantry-to-animal operation used by NPCs and the player;
- `simulation/animals/animal_care_system.gd` owns animal records, hunger
advancement, deterministic routine selection, loaded binding, feed
reservations, and the exact conserved pantry-to-animal operation used by
NPCs and the player;
- `simulation/events/SimulationEventLog.gd` owns ordered event identity,
history queries, and rate calculations;
- `simulation/knowledge/EventKnowledgeSystem.gd` owns per-NPC references to
@@ -105,17 +107,29 @@ hard to read.
| `simulation/persistence/` | Validated local save-file storage |
| `simulation/benchmark/` | Reproducible simulation and loaded-world query workloads |
| `world/` | Loaded-world interaction geometry and presentation adapters |
| `world/animals/` | Animal presentation binding and interaction geometry |
| `world/animals/` | Animal presentation binding, routine sites, and navigation reporting |
| `world/animals/goat/` | Self-contained goat scene, visual script, habitat, and lookdev scene |
| `world/resource_nodes/` | Finite resource presentation and disposable loaded-anchor index |
| `world/storage/` | Storage interaction geometry, never stored quantities |
| `world/activity/` | Rest/study/patrol interaction sites and capacity facts |
| `player/` | Player input, camera, and active NPC presentation |
| `tests/` | Deterministic headless gameplay scenarios |
Top-level core model scripts keep their stable paths because Godot's global
class cache records `class_name` locations. Moving them solely for cosmetic
nesting can break editor and headless startup for existing workspaces without
improving ownership.
Unrelated established model scripts keep their stable paths because Godot's
global class cache records `class_name` locations. This slice moves only the
animal files whose ownership changed, preserves their `.uid` files, and proves
the new paths through editor import and headless startup rather than expanding
the cleanup into cosmetic repository-wide churn.
Project-owned files use `snake_case` when they are newly added or deliberately
moved, while scene node names remain `PascalCase`. Reusable scene assets stay
beside the scene that owns them, so the goat's script, habitat, and lookdev
scene no longer sit in the broad Jajce level folder. Non-runtime documentation
is hidden from Godot with `docs/.gdignore`; baseline images therefore remain
review artifacts without tracked import sidecars. This follows Godot's
[project organization](https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html)
and [GDScript naming](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html)
guidance without turning established-path cleanup into unrelated churn.
## Dependency rules
@@ -136,10 +150,12 @@ improving ownership.
reactions are not replayed.
- Resource changes go through `ResourceStateRecord`, NPC inventory, and
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
- Animal identity, position, hunger, last feed tick, and reservation live in
`AnimalStateRecord`. `AnimalNode` binds that state and contributes only its
loaded interaction point. Animal candidates are currently scanned linearly;
they do not enter the resource grid.
- Animal identity, position, hunger, last feed tick, reservation, routine site,
destination, and next due tick live in `AnimalStateRecord`. `AnimalNode`
binds that state, contributes its loaded interaction point, follows the
selected navigation path, and reports position/arrival facts. Routine and
care candidates are currently scanned linearly; they do not enter the
resource grid.
- New mutable features define serialization and deterministic continuation at
the same time as their first gameplay use. Cross-record causes use stable
event IDs rather than object references or prose.
+12 -6
View File
@@ -804,15 +804,21 @@ Completed:
exact transaction that withdraws pantry food, changes only Dunja's state,
and emits one economic fact. World-schema v10 save/restore preserves the
result without replaying the transient feed response.
41. `Jajce Dunja Habitat 08`: a self-contained timber shelter and clover
pasture now expose two stable routine sites in an open south-village
clearing. Dunja alternates between them without RNG, follows the real
Jajce navigation map, bends nearby grass, stops when hungry, and preserves
exact mid-route state through schema-v11 restore and presentation
unload/reload. The animal feature assets now live together under
`world/animals/goat/`, while ignored docs no longer create import sidecars.
Next:
1. Give Dunja one deterministic pasture/shelter routine using
simulation-owned destination and position, real loaded-world navigation,
and exact mid-move save/restore continuation.
2. Add a second goat only after movement proves that identity remains stable
across care, navigation, unloading, and restore.
3. Keep breeding, products, herd scheduling, animal spatial indexing, and
1. Add one second named goat with its own stable state, authored shelter
context, and deterministic use of the shared pasture. Prove nearest
feed-target selection and independent reservations when both animals are
hungry.
2. Keep breeding, products, herd scheduling, animal spatial indexing, and
active/abstract simulation LOD deferred until two real animals require them.
Do not start with GIS data, a full city, a large asset pack, or more NPC
+6 -6
View File
@@ -43,17 +43,17 @@ transfer occurred.
## Persistence and determinism
`SimulationStateRecord` schema v10 stores the ordered event stream,
`SimulationStateRecord` schema v11 stores the ordered event stream,
`next_event_id`, directed relationships that may reference an exact event, and
per-NPC known-event references with first-acquisition provenance, plus
opportunity records that reference exact trigger/resolution events. Schema v1
and v2 saves migrate to an empty stream beginning at ID zero; world schemas
v1v7 migrate to an empty opportunity list, while world schemas v8 and v9
v1v7 migrate to an empty opportunity list, while world schemas v8v10
preserve and normalize opportunity history. World v9 adds an empty animal list
during migration. Parsing rejects duplicate event
IDs, invalid or duplicate knowledge/opportunity references, impossible
communicator sources, relationship causes the observer does not know, and next
IDs that could collide with restored history.
during migration; world v10 preserves and upgrades its animal records. Parsing
rejects duplicate event IDs, invalid or duplicate knowledge/opportunity
references, impossible communicator sources, relationship causes the observer
does not know, and next IDs that could collide with restored history.
Animal-feed facts receive an additional cross-record check: actor is either an
existing NPC or the player sentinel, source is the real pantry, destination is
+20 -7
View File
@@ -930,16 +930,29 @@ serves both NPC and player feeding, withdraws real pantry food, mutates only the
target animal, and appends one economic fact. World-schema v10 save/restore
preserves the result without replaying the transient response.
The immediate next slice should give Dunja one deterministic
pasture/shelter routine. Her destination and position must remain
simulation-owned, loaded movement must use real navigation, and a mid-move
save/restore must continue exactly without consuming new decision RNG. Keep
loaded-animal discovery linear and do not add a second goat, herd scheduling,
breeding, products, or active/abstract simulation LOD until this movement
contract is honest.
The first loaded-animal routine slice is complete. Dunja alternates between
stable shelter and pasture IDs without RNG; her exact target and current
position stay simulation-owned while the loaded scene follows the Jajce
navigation map. Schema-v11 mid-route restore preserves the checksum,
destination, and every NPC RNG stream. Unloading and re-instantiating the goat
scene retains that same travel state, arrival is exact, and hunger stops
movement before feeding.
The immediate next slice should add one second named goat with its own stable
record and authored shelter context while sharing the existing pasture.
Selection and reservation tests must prove that simultaneous hunger still
chooses the nearest real animal and mutates only the exact target. Keep
loaded-animal discovery linear and defer herd scheduling, breeding, products,
animal indexing, and active/abstract simulation LOD until two identities
demonstrate a concrete need.
Recently completed:
- `Jajce Dunja Habitat 08`: the first deterministic animal routine alternates
shelter and pasture through real loaded navigation, preserves exact
mid-route state across save/restore and presentation unload/reload, stops
when hungry, bends grass while moving, and colocates goat-owned assets in a
focused feature folder.
- `Jajce Goat 07`: named goat Dunja proves stable animal identity, exact
pantry-conserving NPC/player feeding, deterministic hunger and save/restore,
loaded target reservation, a state-derived hungry cue, and a transient cozy
+13 -4
View File
@@ -553,6 +553,7 @@ NpcVisual navigates through the active world
│ ├── SimulationClock.gd
│ ├── SimulationManager.gd
│ ├── actions/ Selection, execution, and target resolution
│ ├── animals/ Animal needs, routines, reservations, and care
│ ├── benchmark/ Population/history and resource-query workloads
│ ├── definitions/ Stable IDs and custom definition resources
│ ├── economy/ Inventory and storage transactions
@@ -587,6 +588,10 @@ NpcVisual navigates through the active world
│ ├── benchmark_loaded_resource_discovery.gd
│ └── generate_jajce_terrain_seed.gd
├── world/
│ ├── animals/
│ │ ├── animal_node.gd
│ │ ├── animal_routine_site.gd
│ │ └── goat/ Goat scene, habitat, visual, and lookdev assets
│ ├── jajce/
│ │ ├── JajceWorld.tscn
│ │ ├── JajceLookdev.tscn
@@ -618,7 +623,7 @@ These are expected prototype constraints, not necessarily isolated bugs:
the typed pantry `StorageNode`, and patrol/study/rest use `ActivitySite`.
- Current NPC, village, resource, storage, animal, event, knowledge,
relationship, opportunity, clock, and RNG state serialize through world
schema v10. F5/F9 provide one validated local quicksave; a save menu,
schema v11. F5/F9 provide one validated local quicksave; a save menu,
metadata, and player-transform persistence remain deferred.
- Simulation-owned resource records retain live amounts, reservations, and
usage definitions while ResourceNode scenes are unloaded.
@@ -991,9 +996,13 @@ amount. Dunja now proves the first identity-backed animal contract: stable
animal identity, simulation-owned position and hunger, a loaded visual binding,
and the same exact pantry-conserving feed operation for NPC and player.
World-schema v10 restore reconstructs her persistent state without replaying
the transient response. The next bounded animal slice should give Dunja one
deterministic pasture/shelter routine before adding another goat. Do not yet
generalize animal discovery or introduce active/abstract simulation LOD.
the transient response. Dunja now also alternates between a stable shelter and
pasture through real loaded navigation. World-schema v11 preserves exact
mid-route position, destination, and schedule through restore or presentation
unload/reload without consuming decision RNG. The next bounded animal slice
should add one second named goat and prove independent nearest-target care
before any herd abstraction. Do not yet generalize animal discovery or
introduce active/abstract simulation LOD.
The remaining simulation-garden target still aims for:
+3
View File
@@ -3,6 +3,9 @@
The project uses a small hierarchy so overlapping plans do not become competing
sources of truth.
This directory is intentionally covered by `.gdignore`: plans and baseline
captures are review artifacts, not runtime Godot resources.
1. [`PROJECT_CONTEXT.md`](PROJECT_CONTEXT.md) is the canonical description of
the vision, current implementation, target architecture, and active
constraints.
+4
View File
@@ -37,6 +37,10 @@ both require one stored wood without duplicating that rule.
farmer profession and declares an exact one-food completion cost. Selection,
animal target reservation, and completion remain separate; NPC and player
completion both use `AnimalCareSystem`'s same pantry-to-animal transaction.
`SimulationIds` also names Dunja's shelter and pasture anchors. These IDs are
saved routine vocabulary; their loaded positions come from
`AnimalRoutineSite` rather than entering an action definition or resource
registry.
## Current profession contract
+24 -7
View File
@@ -3,7 +3,7 @@
## Current contract
`SimulationStateRecord` is the versioned JSON boundary for the current
simulation. The current world schema is v10 and captures:
simulation. The current world schema is v11 and captures:
- simulation seed, tick interval, tick count, clock remainder, and elapsed
clock ticks;
@@ -14,7 +14,8 @@ simulation. The current world schema is v10 and captures:
- scene-independent resource state plus the definition facts needed while its
ResourceNode is unloaded;
- scene-independent animal identity, position, hunger, feeding history,
reservation, and availability state;
reservation, availability, current routine site, active travel destination,
and next routine tick;
- pantry contents, carried NPC inventory, the ordered economic event stream,
and its next stable event ID;
- directed relationship records with familiarity, trust, and the stable event
@@ -31,7 +32,7 @@ The top-level identity is:
```json
{
"schema": "the_steward.simulation",
"schema_version": 10
"schema_version": 11
}
```
@@ -179,6 +180,13 @@ species IDs, display name, authoritative world position, hunger,
World schema v9 preserves opportunity history while migrating to an empty
animal list.
SimulationStateRecord v11 preserves world-v10 animals and upgrades nested
`AnimalStateRecord` v1 to v2. The nested schema adds `routine_site_id`,
`travel_target_site_id`, exact `travel_target_position`,
`has_travel_target`, and `next_routine_tick`. Migrated animals receive an
idle, immediately due routine without inventing a destination; loaded-world
selection establishes the first real target later.
Current-schema parsing requires unique animal IDs that do not collide with
resource or storage IDs. An animal reservation must belong to an existing NPC
whose active feed task targets that exact animal. Every `animal_fed` event
@@ -255,9 +263,10 @@ after a JSON round trip.
## Animal authority
`AnimalCareSystem` owns `AnimalStateRecord` instances independently of the
scene tree and advances their hunger in stable-ID order. `AnimalNode` binds a
loaded presentation and interaction point to that authority; the simulation
position remains canonical while the node is loaded or absent.
scene tree and advances hunger and due routine selection in stable-ID order.
`AnimalNode` binds a loaded presentation and interaction point to that
authority; the simulation position and selected destination remain canonical
while the node is loaded or absent.
Animals are not `ResourceNode` instances and are not inserted into the
resource discovery grid. The first bounded slice uses a deterministic linear
@@ -266,6 +275,14 @@ care operation: reserve or select the exact animal, withdraw one real food
from `village_pantry`, reduce that animal's hunger, and append one exact
`animal_fed` fact. Failed withdrawal changes neither animal nor history.
The first routine consumer alternates Dunja between stable-ID shelter and
pasture markers without drawing RNG. `ActiveWorldAdapter` supplies the loaded
marker positions, the selected ID and exact position enter state, and
`AnimalNode` follows a real `NavigationServer3D` path while reporting factual
position changes. A mid-route save restores the same position and target;
unloading the scene node pauses presentation only, and reloading it resumes
from those facts. Hunger cancels travel before care targeting.
The persistent hungry/content presentation derives from authoritative hunger.
The short feed response is transient and is reset rather than serialized or
replayed after restore.
@@ -288,7 +305,7 @@ This phase does not yet provide:
- a save-slot menu, metadata, thumbnails, autosaves, or multiple profiles;
- migrations from any historical world schema other than the explicitly
supported v1v9 layouts;
supported v1v10 layouts;
- player inventory or player relationship records;
- broader relationship dimensions, line-of-sight/hearing evidence,
continuous or personalized memory decay, reinforcement, false beliefs, or
+53
View File
@@ -0,0 +1,53 @@
# Jajce Dunja Habitat 08
Captured: 2026-07-26
## File
- [shelter and clover pasture](jajce_dunja_habitat_08.png)
The 1280×720 Metal frame shows Dunja's small timber shelter, fixed feed bowl,
straw bed, warm lantern, and adjacent fenced clover patch in the south-village
clearing. The bowl now belongs to the static habitat instead of moving with
the goat. Dunja also joins the existing `grass_interactors` group, so the
camera-local grass shader bends around her real loaded position.
## Routine proof
The two habitat markers expose stable `dunja_shelter` and `dunja_pasture` IDs.
When the saved routine tick becomes due, `AnimalCareSystem` alternates to the
other site in stable-ID order without drawing RNG. The selected site ID, exact
target position, current position, and next due tick remain in
`AnimalStateRecord`.
`tests/animal_routine_vertical_slice_test.gd` proves the complete loaded-world
contract:
- the authored endpoints are connected by the Jajce navigation map;
- Dunja follows the returned path and reports authoritative position;
- a mid-route schema-v11 save restores the same checksum, destination, and NPC
RNG streams;
- unloading and re-instantiating the goat scene rebinds that same moving state;
- arrival promotes the destination to the current site exactly; and
- hunger stops travel so feeding targets a stationary animal.
## Structure refinement
The reusable goat scene, visual script, habitat, and lookdev scene now live
together under `world/animals/goat/`. Their file names use `snake_case`, while
scene node names remain `PascalCase`. `docs/.gdignore` keeps documentation and
review captures out of Godot's runtime import database, so baseline PNGs no
longer need tracked `.import` sidecars.
## Capture command
```bash
HOME="$PWD/logs/quality/godot_profile" \
APPDATA="$PWD/logs/quality/godot_profile" \
LOCALAPPDATA="$PWD/logs/quality/godot_profile" \
/Applications/Godot.app/Contents/MacOS/Godot \
--path "$PWD" --script res://tools/capture_jajce_lookdev.gd -- \
--scene=res://world/animals/goat/dunja_lookdev.tscn \
--output=res://docs/baselines/jajce_dunja_habitat_08.png \
--hide-debug-labels
```
+3 -3
View File
@@ -18,8 +18,8 @@ This visual comparison sits on the gameplay proof:
- world-schema v10 restores position, hunger, and last-fed tick without
replaying presentation effects.
The next animal frame should prove a real pasture/shelter movement routine,
not multiply static livestock.
The follow-up [Jajce Dunja Habitat 08](JAJCE_DUNJA_HABITAT_08.md) now proves
the real pasture/shelter movement routine before livestock multiplies.
Capture command:
@@ -29,7 +29,7 @@ APPDATA="$PWD/logs/quality/godot_profile" \
LOCALAPPDATA="$PWD/logs/quality/godot_profile" \
/Applications/Godot.app/Contents/MacOS/Godot \
--path "$PWD" --script res://tools/capture_jajce_lookdev.gd -- \
--scene=res://world/jajce/JajceGoatLookdev.tscn \
--scene=res://world/animals/goat/dunja_lookdev.tscn \
--hide-debug-labels --animal-hunger=goat_dunja:90 \
--output=res://docs/baselines/jajce_goat_07_hungry.png
```
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dcg13u5wqr23m"
path="res://.godot/imported/jajce_center_02_after.png-be0a331c22c145d626fa09f682c7e379.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_center_02_after.png"
dest_files=["res://.godot/imported/jajce_center_02_after.png-be0a331c22c145d626fa09f682c7e379.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dipu1epmps356"
path="res://.godot/imported/jajce_center_02_before.png-a6dad6237a960278853b124b5c1180c2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_center_02_before.png"
dest_files=["res://.godot/imported/jajce_center_02_before.png-a6dad6237a960278853b124b5c1180c2.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: 382 KiB

@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://4tlahsqubofd"
path="res://.godot/imported/jajce_forest_edge_06_after.png-cc336e5231ac96e8d23247a881a0ec1c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_forest_edge_06_after.png"
dest_files=["res://.godot/imported/jajce_forest_edge_06_after.png-cc336e5231ac96e8d23247a881a0ec1c.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://gsie1efiik5"
path="res://.godot/imported/jajce_forest_edge_06_before.png-ecb42151d9a8db85404cc1153afa0bcf.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_forest_edge_06_before.png"
dest_files=["res://.godot/imported/jajce_forest_edge_06_before.png-ecb42151d9a8db85404cc1153afa0bcf.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dnu1owrmly7wf"
path="res://.godot/imported/jajce_forest_edge_06_depleted.png-08c916e5b04639615251b670dcd41f56.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_forest_edge_06_depleted.png"
dest_files=["res://.godot/imported/jajce_forest_edge_06_depleted.png-08c916e5b04639615251b670dcd41f56.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://0ldryrvw76q6"
path="res://.godot/imported/jajce_goat_07_content.png-138802d07281bb13d9d6edbbb359f838.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_goat_07_content.png"
dest_files=["res://.godot/imported/jajce_goat_07_content.png-138802d07281bb13d9d6edbbb359f838.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bexlabef1e1xa"
path="res://.godot/imported/jajce_goat_07_hungry.png-2474d252bb9b213aa9e6cc2457baacb4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_goat_07_hungry.png"
dest_files=["res://.godot/imported/jajce_goat_07_hungry.png-2474d252bb9b213aa9e6cc2457baacb4.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://du1awnpktb7q0"
path="res://.godot/imported/jajce_grass_gameplay_02.png-4b072b6e4f1c146d62492370903db793.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_grass_gameplay_02.png"
dest_files=["res://.godot/imported/jajce_grass_gameplay_02.png-4b072b6e4f1c146d62492370903db793.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://clc1x0yydurdk"
path="res://.godot/imported/jajce_lookdev_01.png-090cdbef692a973d175bce2e4bf10776.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_lookdev_01.png"
dest_files=["res://.godot/imported/jajce_lookdev_01.png-090cdbef692a973d175bce2e4bf10776.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://drip3uanxmac0"
path="res://.godot/imported/jajce_resource_grove_04_after.png-400fd71bda2bd7202805c1ba66aa8c28.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_resource_grove_04_after.png"
dest_files=["res://.godot/imported/jajce_resource_grove_04_after.png-400fd71bda2bd7202805c1ba66aa8c28.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ch6nx7rj4elit"
path="res://.godot/imported/jajce_resource_grove_04_before.png-153834f7053f143a34b090dc21ac0d16.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_resource_grove_04_before.png"
dest_files=["res://.godot/imported/jajce_resource_grove_04_before.png-153834f7053f143a34b090dc21ac0d16.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cnusf1uofgeam"
path="res://.godot/imported/jajce_resource_states_05_depleted.png-4da1f8d2357fb72267bc5e3167868d55.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_resource_states_05_depleted.png"
dest_files=["res://.godot/imported/jajce_resource_states_05_depleted.png-4da1f8d2357fb72267bc5e3167868d55.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c6fr86t0glm1p"
path="res://.godot/imported/jajce_resource_states_05_full.png-b7555dd39fe62146f66c1e731a0813d3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_resource_states_05_full.png"
dest_files=["res://.godot/imported/jajce_resource_states_05_full.png-b7555dd39fe62146f66c1e731a0813d3.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://lerebtc2de5v"
path="res://.godot/imported/jajce_resource_states_05_low.png-ca9e13189ad1a8dc22e32380ddf38b2f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_resource_states_05_low.png"
dest_files=["res://.godot/imported/jajce_resource_states_05_low.png-ca9e13189ad1a8dc22e32380ddf38b2f.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://r87qvthnulve"
path="res://.godot/imported/jajce_river_03_after.png-43f5610f2e4f42d23b2a77674f846c7e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_river_03_after.png"
dest_files=["res://.godot/imported/jajce_river_03_after.png-43f5610f2e4f42d23b2a77674f846c7e.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://2whbruuqbywc"
path="res://.godot/imported/jajce_river_03_before.png-1cca7b1c7659c390906ee3738cc8b95f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_river_03_before.png"
dest_files=["res://.godot/imported/jajce_river_03_before.png-1cca7b1c7659c390906ee3738cc8b95f.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dtr1bl6xe3pnk"
path="res://.godot/imported/simulation_garden_01_cinematic.png-39560816a2b107dac16e5ba4e2c5dbb8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/simulation_garden_01_cinematic.png"
dest_files=["res://.godot/imported/simulation_garden_01_cinematic.png-39560816a2b107dac16e5ba4e2c5dbb8.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
@@ -1,40 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://wmk4fykdlafb"
path="res://.godot/imported/simulation_garden_01_debug.png-c2c8d5bb4081c8fa4b5fa05e512fa49e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/simulation_garden_01_debug.png"
dest_files=["res://.godot/imported/simulation_garden_01_debug.png-c2c8d5bb4081c8fa4b5fa05e512fa49e.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
@@ -1,40 +0,0 @@
[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
@@ -1,40 +0,0 @@
[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
+3 -3
View File
@@ -2,7 +2,7 @@ extends Node
const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
const AnimalCareSystemScript := preload("res://simulation/animals/AnimalCareSystem.gd")
const AnimalCareSystemScript := preload("res://simulation/animals/animal_care_system.gd")
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
@@ -162,7 +162,7 @@ func simulate_tick() -> void:
tick_count += 1
if debug_logs:
print("--- Tick ", tick_count, " ---")
animal_care.advance()
animal_care.advance(tick_count)
var village_was_changed := false
_population_view.rebuild(npcs)
for npc in npcs:
@@ -1192,7 +1192,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
for resource_record in record.resources:
resource_states[resource_record.get_node_id()] = resource_record
register_loaded_resource_nodes()
animal_care.restore_state_records(record.animals)
animal_care.restore_state_records(record.animals, tick_count)
village_changed.emit(village)
state_restored.emit()
return true
@@ -19,10 +19,16 @@ signal narrative_event_requested(
item_id: StringName,
required_amount: float
)
signal routine_started(animal_id: StringName, site_id: StringName, target_position: Vector3)
signal routine_completed(animal_id: StringName, site_id: StringName, position: Vector3)
const ROUTINE_STAY_TICKS := 40
const ROUTINE_RETRY_TICKS := 10
var states: Dictionary = {}
var economy: RefCounted
var active_world_adapter: Node
var current_tick := 0
func configure(economy_service: RefCounted, world_adapter: Node) -> void:
@@ -30,13 +36,19 @@ func configure(economy_service: RefCounted, world_adapter: Node) -> void:
active_world_adapter = world_adapter
func advance() -> void:
func advance(simulation_tick: int) -> void:
current_tick = simulation_tick
var animal_ids: Array = states.keys()
animal_ids.sort()
for animal_id in animal_ids:
var animal_state := states[animal_id] as AnimalStateRecord
if animal_state != null:
if animal_state == null:
continue
animal_state.advance_tick()
if animal_state.has_active_travel_target() and animal_state.needs_feed():
animal_state.cancel_travel(current_tick + ROUTINE_RETRY_TICKS)
elif animal_state.is_routine_due(current_tick):
_begin_due_routine(animal_state)
func register_loaded_nodes() -> void:
@@ -74,6 +86,12 @@ func register_node(node: AnimalNode) -> bool:
elif not animal_state.apply_definition(node):
push_error("AnimalCareSystem: AnimalNode definition mismatch for '%s'" % node.animal_id)
return false
if not node.position_changed.is_connected(_on_node_position_changed):
node.position_changed.connect(_on_node_position_changed)
if not node.routine_arrived.is_connected(_on_node_routine_arrived):
node.routine_arrived.connect(_on_node_routine_arrived)
if not node.navigation_failed.is_connected(_on_node_navigation_failed):
node.navigation_failed.connect(_on_node_navigation_failed)
return node.bind_state(animal_state)
@@ -190,6 +208,10 @@ func feed(animal_id: StringName, actor_id: int, npcs: Array[SimNPC], current_tic
if not animal_state.feed(actor_id, current_tick):
economy.deposit_resource(resource_id, consumed)
return false
if animal_state.has_active_travel_target():
animal_state.cancel_travel(current_tick + ROUTINE_STAY_TICKS)
else:
animal_state.schedule_routine_at(current_tick + ROUTINE_STAY_TICKS)
economic_event_requested.emit(
SimulationIds.EVENT_ANIMAL_FED,
@@ -211,13 +233,70 @@ func append_state_records(record: SimulationStateRecord) -> void:
record.animals.append(animal_state)
func restore_state_records(records: Array[AnimalStateRecord]) -> void:
func restore_state_records(records: Array[AnimalStateRecord], restored_tick: int) -> void:
current_tick = restored_tick
states.clear()
for animal_record in records:
states[animal_record.get_animal_id()] = animal_record
register_loaded_nodes()
func _begin_due_routine(animal_state: AnimalStateRecord) -> bool:
if (
active_world_adapter == null
or not active_world_adapter.has_method("get_animal_routine_sites")
):
return false
if AnimalNode.get_by_id(animal_state.get_animal_id()) == null:
return false
var candidates: Array[Dictionary] = active_world_adapter.get_animal_routine_sites(
animal_state.get_species_id()
)
if candidates.is_empty():
return false
var current_site_id := animal_state.get_routine_site_id()
for candidate in candidates:
var candidate_id := StringName(candidate["site_id"])
if candidate_id == current_site_id:
continue
var target_position: Vector3 = candidate["position"]
if not animal_state.begin_travel(candidate_id, target_position):
return false
routine_started.emit(animal_state.get_animal_id(), candidate_id, target_position)
return true
return false
func _on_node_position_changed(animal_id: StringName, active_position: Vector3) -> void:
var animal_state := get_state(animal_id)
if animal_state != null and animal_state.has_active_travel_target():
animal_state.set_position(active_position)
func _on_node_routine_arrived(
animal_id: StringName, site_id: StringName, arrival_position: Vector3
) -> void:
var animal_state := get_state(animal_id)
if (
animal_state == null
or not animal_state.has_active_travel_target()
or animal_state.get_travel_target_site_id() != site_id
):
return
if animal_state.complete_travel(arrival_position, current_tick + ROUTINE_STAY_TICKS):
routine_completed.emit(animal_id, site_id, arrival_position)
func _on_node_navigation_failed(animal_id: StringName, site_id: StringName) -> void:
var animal_state := get_state(animal_id)
if (
animal_state != null
and animal_state.has_active_travel_target()
and animal_state.get_travel_target_site_id() == site_id
):
animal_state.cancel_travel(current_tick + ROUTINE_RETRY_TICKS)
static func _find_npc(npc_id: int, npcs: Array[SimNPC]) -> SimNPC:
for npc in npcs:
if npc.id == npc_id:
+2
View File
@@ -32,6 +32,8 @@ const RESOURCE_WOOD := &"wood"
const ANIMAL_DUNJA := &"goat_dunja"
const SPECIES_GOAT := &"goat"
const ANIMAL_SITE_DUNJA_PASTURE := &"dunja_pasture"
const ANIMAL_SITE_DUNJA_SHELTER := &"dunja_shelter"
const STORAGE_VILLAGE_PANTRY := &"village_pantry"
const STORAGE_VILLAGE_WOODPILE := &"village_woodpile"
+12 -2
View File
@@ -2,7 +2,7 @@ class_name SimulationStateRecord
extends RefCounted
const SCHEMA_NAME := "the_steward.simulation"
const SCHEMA_VERSION := 10
const SCHEMA_VERSION := 11
const LEGACY_SCHEMA_VERSION := 1
const EVENT_LEGACY_SCHEMA_VERSION := 2
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
@@ -11,6 +11,7 @@ const PROVENANCE_LEGACY_SCHEMA_VERSION := 5
const RETENTION_LEGACY_SCHEMA_VERSION := 6
const OPPORTUNITY_LEGACY_SCHEMA_VERSION := 8
const ANIMAL_LEGACY_SCHEMA_VERSION := 9
const ROUTINE_LEGACY_SCHEMA_VERSION := 10
const PREVIOUS_SCHEMA_VERSION := 7
var simulation: Dictionary
@@ -95,6 +96,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
PREVIOUS_SCHEMA_VERSION,
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
ANIMAL_LEGACY_SCHEMA_VERSION,
ROUTINE_LEGACY_SCHEMA_VERSION,
]
):
record_data = _migrate_legacy(record_data, version)
@@ -676,6 +678,7 @@ static func _is_valid_supply_resolution(
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
var migrated := legacy_data.duplicate(true)
migrated["schema_version"] = SCHEMA_VERSION
if version <= ANIMAL_LEGACY_SCHEMA_VERSION:
migrated["animals"] = []
if version == LEGACY_SCHEMA_VERSION:
var village_data: Dictionary = legacy_data.get("village", {})
@@ -727,7 +730,14 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
migrated.get("economic_events", []),
int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0))
)
if version not in [OPPORTUNITY_LEGACY_SCHEMA_VERSION, ANIMAL_LEGACY_SCHEMA_VERSION]:
if (
version
not in [
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
ANIMAL_LEGACY_SCHEMA_VERSION,
ROUTINE_LEGACY_SCHEMA_VERSION,
]
):
migrated["opportunities"] = []
var opportunity_simulation_data: Dictionary = migrated.get("simulation", {})
opportunity_simulation_data["next_opportunity_id"] = 0
@@ -4,7 +4,8 @@ extends RefCounted
signal changed(state: AnimalStateRecord)
signal fed(state: AnimalStateRecord, actor_id: int)
const SCHEMA_VERSION := 1
const SCHEMA_VERSION := 2
const LEGACY_SCHEMA_VERSION := 1
const HUNGER_PER_TICK := 0.125
const FEED_THRESHOLD := 65.0
const HUNGER_RELIEF := 55.0
@@ -34,13 +35,21 @@ static func create_from_node(node: AnimalNode) -> AnimalStateRecord:
"enabled": node.initial_enabled,
"can_npcs_feed": node.can_npcs_feed,
"can_player_feed": node.can_player_feed,
"routine_site_id": String(node.initial_routine_site_id),
"travel_target_site_id": "",
"travel_target_position": [0.0, 0.0, 0.0],
"has_travel_target": false,
"next_routine_tick": node.initial_next_routine_tick,
}
)
)
static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
var version := int(record_data.get("schema_version", -1))
if version == LEGACY_SCHEMA_VERSION:
record_data = _migrate_legacy(record_data)
elif version != SCHEMA_VERSION:
return null
if not (
record_data
@@ -56,6 +65,11 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
"enabled",
"can_npcs_feed",
"can_player_feed",
"routine_site_id",
"travel_target_site_id",
"travel_target_position",
"has_travel_target",
"next_routine_tick",
]
)
):
@@ -67,6 +81,12 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
var hunger := float(record_data["hunger"])
var last_fed_tick := int(record_data["last_fed_tick"])
var reserved_by := int(record_data["reserved_by"])
var routine_site_id := String(record_data["routine_site_id"])
var travel_target_site_id := String(record_data["travel_target_site_id"])
var saved_travel_target = record_data["travel_target_position"]
var has_travel_target := bool(record_data["has_travel_target"])
var next_routine_tick := int(record_data["next_routine_tick"])
var enabled := bool(record_data["enabled"])
if animal_id.is_empty() or display_name.is_empty() or species_id.is_empty():
return null
if StringName(species_id) != SimulationIds.SPECIES_GOAT:
@@ -76,11 +96,25 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
for component in saved_position:
if not is_finite(float(component)):
return null
if not saved_travel_target is Array or saved_travel_target.size() != 3:
return null
for component in saved_travel_target:
if not is_finite(float(component)):
return null
if not is_finite(hunger) or hunger < 0.0 or hunger > 100.0:
return null
if last_fed_tick < NEVER_FED_TICK or reserved_by < -1:
if last_fed_tick < NEVER_FED_TICK or reserved_by < -1 or next_routine_tick < 0:
return null
if reserved_by >= 0 and hunger < FEED_THRESHOLD:
if reserved_by >= 0 and (hunger < FEED_THRESHOLD or has_travel_target):
return null
if has_travel_target != (not travel_target_site_id.is_empty()):
return null
if has_travel_target and (not enabled or travel_target_site_id == routine_site_id):
return null
var travel_target_position := Vector3(
float(saved_travel_target[0]), float(saved_travel_target[1]), float(saved_travel_target[2])
)
if not has_travel_target and not travel_target_position.is_zero_approx():
return null
var normalized := record_data.duplicate(true)
@@ -94,9 +128,16 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
normalized["hunger"] = hunger
normalized["last_fed_tick"] = last_fed_tick
normalized["reserved_by"] = reserved_by
normalized["enabled"] = bool(record_data["enabled"])
normalized["enabled"] = enabled
normalized["can_npcs_feed"] = bool(record_data["can_npcs_feed"])
normalized["can_player_feed"] = bool(record_data["can_player_feed"])
normalized["routine_site_id"] = routine_site_id
normalized["travel_target_site_id"] = travel_target_site_id
normalized["travel_target_position"] = [
travel_target_position.x, travel_target_position.y, travel_target_position.z
]
normalized["has_travel_target"] = has_travel_target
normalized["next_routine_tick"] = next_routine_tick
return AnimalStateRecord.new(normalized)
@@ -152,6 +193,27 @@ func can_player_feed_animal() -> bool:
return bool(data["can_player_feed"])
func get_routine_site_id() -> StringName:
return StringName(data["routine_site_id"])
func get_travel_target_site_id() -> StringName:
return StringName(data["travel_target_site_id"])
func get_travel_target_position() -> Vector3:
var saved_position: Array = data["travel_target_position"]
return Vector3(float(saved_position[0]), float(saved_position[1]), float(saved_position[2]))
func has_active_travel_target() -> bool:
return bool(data["has_travel_target"])
func get_next_routine_tick() -> int:
return int(data["next_routine_tick"])
func needs_feed() -> bool:
return is_enabled() and get_hunger() >= FEED_THRESHOLD
@@ -160,6 +222,16 @@ func is_available_for(agent_id: int) -> bool:
return needs_feed() and (get_reserved_by() == -1 or get_reserved_by() == agent_id)
func is_routine_due(current_tick: int) -> bool:
return (
is_enabled()
and not needs_feed()
and get_reserved_by() == -1
and not has_active_travel_target()
and current_tick >= get_next_routine_tick()
)
func reserve(agent_id: int) -> bool:
if agent_id < 0 or not is_available_for(agent_id):
return false
@@ -196,6 +268,56 @@ func feed(actor_id: int, current_tick: int) -> bool:
return true
func begin_travel(site_id: StringName, target_position: Vector3) -> bool:
if (
site_id.is_empty()
or site_id == get_routine_site_id()
or not target_position.is_finite()
or not is_enabled()
or has_active_travel_target()
or needs_feed()
or get_reserved_by() >= 0
):
return false
data["travel_target_site_id"] = String(site_id)
data["travel_target_position"] = [target_position.x, target_position.y, target_position.z]
data["has_travel_target"] = true
changed.emit(self)
return true
func complete_travel(arrival_position: Vector3, next_routine_tick: int) -> bool:
if not has_active_travel_target() or not arrival_position.is_finite() or next_routine_tick < 0:
return false
data["position"] = [arrival_position.x, arrival_position.y, arrival_position.z]
data["routine_site_id"] = data["travel_target_site_id"]
data["travel_target_site_id"] = ""
data["travel_target_position"] = [0.0, 0.0, 0.0]
data["has_travel_target"] = false
data["next_routine_tick"] = next_routine_tick
changed.emit(self)
return true
func cancel_travel(next_routine_tick: int) -> bool:
if next_routine_tick < 0:
return false
data["travel_target_site_id"] = ""
data["travel_target_position"] = [0.0, 0.0, 0.0]
data["has_travel_target"] = false
data["next_routine_tick"] = next_routine_tick
changed.emit(self)
return true
func schedule_routine_at(next_routine_tick: int) -> bool:
if next_routine_tick < 0 or has_active_travel_target():
return false
data["next_routine_tick"] = next_routine_tick
changed.emit(self)
return true
func set_position(value: Vector3) -> void:
if not value.is_finite() or value.is_equal_approx(get_position()):
return
@@ -216,8 +338,22 @@ func set_enabled(value: bool) -> void:
data["enabled"] = value
if not value:
data["reserved_by"] = -1
data["travel_target_site_id"] = ""
data["travel_target_position"] = [0.0, 0.0, 0.0]
data["has_travel_target"] = false
changed.emit(self)
func to_dictionary() -> Dictionary:
return data.duplicate(true)
static func _migrate_legacy(record_data: Dictionary) -> Dictionary:
var migrated := record_data.duplicate(true)
migrated["schema_version"] = SCHEMA_VERSION
migrated["routine_site_id"] = ""
migrated["travel_target_site_id"] = ""
migrated["travel_target_position"] = [0.0, 0.0, 0.0]
migrated["has_travel_target"] = false
migrated["next_routine_tick"] = 0
return migrated
+235
View File
@@ -0,0 +1,235 @@
extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var main_scene: Node = load("res://main.tscn").instantiate()
root.add_child(main_scene)
await process_frame
var manager: Node = main_scene.get_node("SimulationManager")
manager.set_process(false)
for _frame in 3:
await physics_frame
var goat := main_scene.get_node("JajceWorld/WorldObjects/Animals/Dunja") as AnimalNode
var state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
var pasture := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
var shelter := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_SHELTER)
_check(
goat != null and state != null and pasture != null and shelter != null,
"The routine proof needs Dunja and both loaded stable-ID habitat sites"
)
if goat == null or state == null or pasture == null or shelter == null:
_finish()
return
_check(
not state.begin_travel(shelter.site_id, shelter.get_routine_position()),
"Animal state should reject travel back to its current stable site",
)
state.set_enabled(false)
_check(
not state.begin_travel(pasture.site_id, pasture.get_routine_position()),
"Animal state should reject travel while the animal is disabled",
)
state.set_enabled(true)
for npc in manager.npcs:
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
npc.start_working()
var navigation_map: RID = main_scene.get_world_3d().navigation_map
await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map)
var authored_path := NavigationServer3D.map_get_path(
navigation_map, shelter.get_routine_position(), pasture.get_routine_position(), true
)
_check(
not authored_path.is_empty(),
"The shelter and pasture should be connected by the real Jajce navigation map"
)
state.set_hunger(30.0)
_check(
state.schedule_routine_at(manager.tick_count),
"The test should be able to make the content goat's routine immediately due"
)
goat.move_speed = 4.0
manager.simulate_tick()
_check(
(
state.has_active_travel_target()
and state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_SHELTER
and state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE
and state.get_travel_target_position().is_equal_approx(pasture.get_routine_position())
),
"The due routine should deterministically choose pasture from shelter"
)
for _frame in 3:
await physics_frame
_check(
goat.is_navigating() and not goat.get_navigation_path().is_empty(),
"The loaded goat should follow a NavigationServer path rather than a straight state teleport"
)
var start_position := state.get_position()
for _frame in 22:
await physics_frame
var mid_route_position := state.get_position()
_check(
(
not mid_route_position.is_equal_approx(start_position)
and state.has_active_travel_target()
and goat.global_position.is_equal_approx(mid_route_position)
),
"Loaded movement should continuously report Dunja's authoritative mid-route position"
)
var mid_route_json: String = manager.serialize_state()
var mid_route_checksum: String = manager.get_state_checksum()
var random_streams_before_restore := JSON.stringify(
manager.create_state_record().simulation["wander_random_streams"]
)
var saved_target_position := state.get_travel_target_position()
_check(
manager.restore_state_from_json(mid_route_json),
"A schema-v11 save should accept Dunja's in-progress routine"
)
state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
_check(
(
manager.get_state_checksum() == mid_route_checksum
and state.get_position().is_equal_approx(mid_route_position)
and state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE
and state.get_travel_target_position().is_equal_approx(saved_target_position)
and goat.global_position.is_equal_approx(mid_route_position)
and (
JSON.stringify(manager.create_state_record().simulation["wander_random_streams"])
== random_streams_before_restore
)
),
"Mid-route restore should preserve exact position, target, checksum, and every NPC RNG stream"
)
var animals_parent := goat.get_parent()
goat.queue_free()
await process_frame
_check(
(
AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA) == null
and state.has_active_travel_target()
and state.get_position().is_equal_approx(mid_route_position)
and manager.get_state_checksum() == mid_route_checksum
),
"Unloading Dunja's presentation should leave her in-progress simulation state untouched"
)
var reloaded_goat := load("res://world/animals/goat/cozy_goat.tscn").instantiate() as AnimalNode
animals_parent.add_child(reloaded_goat)
await process_frame
goat = reloaded_goat
goat.move_speed = 4.0
_check(
(
goat.state == state
and goat.global_position.is_equal_approx(mid_route_position)
and manager.get_state_checksum() == mid_route_checksum
),
"Reloading the self-contained goat scene should bind the same state without resetting it"
)
for _frame in 3:
await physics_frame
_check(
goat.is_navigating() and not goat.get_navigation_path().is_empty(),
"Restore should rebuild the loaded navigation path from saved facts"
)
for _frame in 240:
if not state.has_active_travel_target():
break
await physics_frame
_check(
(
not state.has_active_travel_target()
and state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE
and state.get_position().is_equal_approx(pasture.get_routine_position())
and not goat.is_navigating()
),
(
"Arrival should atomically promote pasture and retain its exact position "
+ (
"(active=%s site=%s state=%s target=%s navigating=%s)"
% [
state.has_active_travel_target(),
state.get_routine_site_id(),
state.get_position(),
pasture.get_routine_position(),
goat.is_navigating(),
]
)
)
)
var arrived_json: String = manager.serialize_state()
var arrived_checksum: String = manager.get_state_checksum()
_check(
(
manager.restore_state_from_json(arrived_json)
and manager.get_state_checksum() == arrived_checksum
),
"An arrived routine should also restore without replaying movement"
)
state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
_check(
not state.has_active_travel_target() and not goat.is_navigating(),
"An arrived save should remain quietly at pasture after binding"
)
state.schedule_routine_at(manager.tick_count)
manager.simulate_tick()
_check(
(
state.has_active_travel_target()
and state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_SHELTER
and state.get_travel_target_position().is_equal_approx(shelter.get_routine_position())
),
"The next due routine should deterministically alternate back to shelter"
)
state.set_hunger(AnimalStateRecord.FEED_THRESHOLD - AnimalStateRecord.HUNGER_PER_TICK)
manager.simulate_tick()
_check(
not state.has_active_travel_target() and not goat.is_navigating() and state.needs_feed(),
"Becoming hungry should stop routine travel so NPC and player feeding target a still goat"
)
_finish()
func _wait_for_navigation_map(navigation_map: RID) -> void:
for _attempt in 30:
if (
NavigationServer3D.map_get_iteration_id(navigation_map) > 0
and not NavigationServer3D.map_get_regions(navigation_map).is_empty()
):
await physics_frame
return
await physics_frame
_check(false, "Jajce navigation map did not synchronize within 30 physics frames")
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
func _finish() -> void:
if failures.is_empty():
print("[TEST] Animal pasture/shelter routine passed")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)
@@ -0,0 +1 @@
uid://wpfu3ft008sm
+1 -1
View File
@@ -105,7 +105,7 @@ func _run() -> void:
var active_restored := _create_manager(902)
_check(
active_restored.restore_state_from_json(active_json),
"An active opportunity should survive schema-v10 save and restore"
"An active opportunity should survive schema-v11 save and restore"
)
_check(
(
+1 -1
View File
@@ -269,7 +269,7 @@ func _run() -> void:
var old_contributor_visual := contributor_visual
_check(
simulation_manager.restore_state_from_json(active_need_json),
"The visible shortage should survive a valid schema-v10 restore"
"The visible shortage should survive a valid schema-v11 restore"
)
await process_frame
contributor = simulation_manager.npcs[contributor.id]
+27
View File
@@ -414,6 +414,31 @@ func _run() -> void:
not goat.is_in_group("resource_nodes") and ResourceNode.get_by_id(goat.animal_id) == null,
"The identity-backed goat should remain separate from finite resources"
)
_check(
goat.is_in_group("grass_interactors"),
"Dunja should bend camera-local grass through the shared presentation group"
)
var habitat_root := world.get_node("WorldObjects/AnimalHabitats")
var habitat := habitat_root.get_node("DunjaHabitat")
var routine_sites: Array[AnimalRoutineSite] = []
for site in AnimalRoutineSite.get_all():
routine_sites.append(site)
routine_sites.sort_custom(
func(first: AnimalRoutineSite, second: AnimalRoutineSite) -> bool:
return String(first.site_id) < String(second.site_id)
)
_check(
habitat_root.get_child_count() == 1 and habitat != null and routine_sites.size() == 2,
"Jajce should keep Dunja's two routine anchors inside one self-contained habitat scene"
)
_check(
(
routine_sites.size() == 2
and routine_sites[0].site_id == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE
and routine_sites[1].site_id == SimulationIds.ANIMAL_SITE_DUNJA_SHELTER
),
"Dunja's pasture and shelter should expose deterministic stable IDs"
)
_check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites")
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
_check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode")
@@ -467,6 +492,8 @@ func _run() -> void:
for resource in resources:
destinations.append((resource as ResourceNode).interaction_point.global_position)
destinations.append(goat.get_interaction_position())
for site in routine_sites:
destinations.append(site.get_routine_position())
destinations.append(pantry.get_interaction_position())
destinations.append(woodpile.get_interaction_position())
for site in activity_root.get_children():
@@ -20,6 +20,7 @@ func _run() -> void:
_test_previous_world_retention_migration()
_test_previous_world_opportunity_migration()
_test_previous_world_animal_migration()
_test_previous_world_routine_migration()
_test_relationship_schema_rejection()
_test_opportunity_schema_rejection()
_test_animal_schema_rejection()
@@ -498,6 +499,41 @@ func _test_previous_world_animal_migration() -> void:
manager.free()
func _test_previous_world_routine_migration() -> void:
var manager := _create_manager(65)
var v10_data: Dictionary = manager.create_state_record().to_dictionary()
v10_data["schema_version"] = SimulationStateRecord.ROUTINE_LEGACY_SCHEMA_VERSION
v10_data["animals"] = [
{
"schema_version": AnimalStateRecord.LEGACY_SCHEMA_VERSION,
"animal_id": "legacy_goat",
"display_name": "Legacy",
"species_id": String(SimulationIds.SPECIES_GOAT),
"position": [-4.0, 0.0, -11.5],
"hunger": 40.0,
"last_fed_tick": AnimalStateRecord.NEVER_FED_TICK,
"reserved_by": -1,
"enabled": true,
"can_npcs_feed": true,
"can_player_feed": true,
}
]
var migrated := SimulationStateRecord.from_dictionary(v10_data)
_check(migrated != null, "World schema v10 should preserve and upgrade animal records")
if migrated != null:
var animal := migrated.animals[0]
_check(
(
animal.get_animal_id() == &"legacy_goat"
and animal.get_routine_site_id().is_empty()
and not animal.has_active_travel_target()
and animal.get_next_routine_tick() == 0
),
"World v10 migration should add deterministic idle routine defaults"
)
manager.free()
func _test_animal_schema_rejection() -> void:
var manager := _create_manager(64)
var missing_animals: Dictionary = manager.create_state_record().to_dictionary()
@@ -521,6 +557,11 @@ func _test_animal_schema_rejection() -> void:
"enabled": true,
"can_npcs_feed": true,
"can_player_feed": true,
"routine_site_id": "",
"travel_target_site_id": "",
"travel_target_position": [0.0, 0.0, 0.0],
"has_travel_target": false,
"next_routine_tick": 0,
}
)
)
+1 -1
View File
@@ -132,7 +132,7 @@ func _run() -> void:
var active_restored := _create_manager(1202)
_check(
active_restored.restore_state_from_json(active_json),
"An active missing-wood need should survive schema-v10 restore"
"An active missing-wood need should survive schema-v11 restore"
)
var restored_helper: OpportunityHelperResult = active_restored.get_active_opportunity_helper()
_check(
+2
View File
@@ -89,6 +89,8 @@ func _apply_resource_capture_overrides() -> bool:
if hide_animal_labels:
animal.set_debug_label_enabled(false)
if hide_all_labels:
for site in AnimalRoutineSite.get_all():
site.set_debug_label_enabled(false)
for storage in StorageNode.get_all():
storage.set_debug_label_enabled(false)
for site in ActivitySite.get_all():
+26
View File
@@ -74,6 +74,32 @@ func get_animal_candidates(action_id: StringName) -> Array[Dictionary]:
}
)
)
candidates.sort_custom(
func(first: Dictionary, second: Dictionary) -> bool:
return String(first["target_id"]) < String(second["target_id"])
)
return candidates
func get_animal_routine_sites(species_id: StringName) -> Array[Dictionary]:
var candidates: Array[Dictionary] = []
for site in AnimalRoutineSite.get_all():
if not site.supports_species(species_id):
continue
(
candidates
. append(
{
"site_id": String(site.site_id),
"position": site.get_routine_position(),
"display_name": site.display_name,
}
)
)
candidates.sort_custom(
func(first: Dictionary, second: Dictionary) -> bool:
return String(first["site_id"]) < String(second["site_id"])
)
return candidates
-165
View File
@@ -1,165 +0,0 @@
class_name AnimalNode
extends Node3D
signal hunger_changed(animal_id: StringName, hunger: float)
signal fed(animal_id: StringName, actor_id: int)
signal reservation_changed(animal_id: StringName, agent_id: int)
static var _all: Array[AnimalNode] = []
@export var animal_id: StringName
@export var display_name := "Animal"
@export var species_id: StringName = SimulationIds.SPECIES_GOAT
@export_range(0.0, 100.0, 0.5) var initial_hunger := 70.0
@export var initial_enabled := true
@export var can_npcs_feed := true
@export var can_player_feed := true
@export var debug_label_enabled := true
@onready var interaction_point: Marker3D = get_node_or_null("InteractionPoint") as Marker3D
var state: AnimalStateRecord
func _ready() -> void:
if animal_id.is_empty():
push_error("AnimalNode at %s has empty animal_id" % get_path())
initial_enabled = false
_update_presentation()
return
var existing := get_by_id(animal_id)
if existing != null:
push_error(
(
"Duplicate AnimalNode animal_id '%s' at %s; first registered at %s"
% [animal_id, get_path(), existing.get_path()]
)
)
initial_enabled = false
_update_presentation()
return
_all.append(self)
add_to_group("animal_nodes")
_try_register_with_simulation()
_update_presentation()
func _exit_tree() -> void:
_all.erase(self)
_disconnect_state()
state = null
func bind_state(animal_state: AnimalStateRecord) -> bool:
if animal_state == null or animal_state.get_animal_id() != animal_id:
return false
_disconnect_state()
state = animal_state
if not state.changed.is_connected(_on_state_changed):
state.changed.connect(_on_state_changed)
if not state.fed.is_connected(_on_state_fed):
state.fed.connect(_on_state_fed)
global_position = state.get_position()
var visual := get_node_or_null("Visual")
if visual != null and visual.has_method("reset_transient_feedback"):
visual.reset_transient_feedback()
_update_presentation()
hunger_changed.emit(animal_id, state.get_hunger())
return true
func _disconnect_state() -> void:
if state == null:
return
if state.changed.is_connected(_on_state_changed):
state.changed.disconnect(_on_state_changed)
if state.fed.is_connected(_on_state_fed):
state.fed.disconnect(_on_state_fed)
func _try_register_with_simulation() -> void:
var managers := get_tree().get_nodes_in_group("simulation_manager")
if managers.size() != 1:
return
var manager := managers[0]
if "animal_care" in manager and manager.animal_care != null:
manager.animal_care.register_node(self)
func get_interaction_position() -> Vector3:
return interaction_point.global_position if interaction_point != null else global_position
func supports_action(action_id: StringName) -> bool:
return action_id == SimulationIds.ACTION_FEED_ANIMAL
func get_hunger() -> float:
return state.get_hunger() if state != null else initial_hunger
func needs_feed() -> bool:
if state != null:
return state.needs_feed()
return initial_enabled and initial_hunger >= AnimalStateRecord.FEED_THRESHOLD
func is_enabled() -> bool:
return state.is_enabled() if state != null else initial_enabled
func _on_state_changed(changed_state: AnimalStateRecord) -> void:
if changed_state != state:
return
if not global_position.is_equal_approx(state.get_position()):
global_position = state.get_position()
hunger_changed.emit(animal_id, state.get_hunger())
reservation_changed.emit(animal_id, state.get_reserved_by())
_update_presentation()
func _on_state_fed(fed_state: AnimalStateRecord, actor_id: int) -> void:
if fed_state != state:
return
var visual := get_node_or_null("Visual")
if visual != null and visual.has_method("play_fed_response"):
visual.play_fed_response()
fed.emit(animal_id, actor_id)
func _update_presentation() -> void:
var visual := get_node_or_null("Visual")
if visual != null:
visual.visible = is_enabled()
if visual.has_method("set_hungry"):
visual.set_hungry(needs_feed())
if not has_node("DebugLabel"):
return
var label := $DebugLabel as Label3D
label.visible = debug_label_enabled
if not debug_label_enabled:
return
var text := "%s\n%s · hunger %.0f" % [display_name, animal_id, get_hunger()]
if needs_feed():
text += "\nHUNGRY"
if state != null and state.get_reserved_by() >= 0:
text += "\nheld:%d" % state.get_reserved_by()
if state == null:
text += "\nUNBOUND"
label.text = text
func set_debug_label_enabled(is_enabled: bool) -> void:
debug_label_enabled = is_enabled
_update_presentation()
static func get_by_id(search_id: StringName) -> AnimalNode:
for node in _all:
if node.animal_id == search_id:
return node
return null
static func get_all() -> Array[AnimalNode]:
return _all.duplicate()
+317
View File
@@ -0,0 +1,317 @@
class_name AnimalNode
extends Node3D
signal hunger_changed(animal_id: StringName, hunger: float)
signal fed(animal_id: StringName, actor_id: int)
signal reservation_changed(animal_id: StringName, agent_id: int)
signal position_changed(animal_id: StringName, active_position: Vector3)
signal routine_arrived(animal_id: StringName, site_id: StringName, arrival_position: Vector3)
signal navigation_failed(animal_id: StringName, site_id: StringName)
static var _all: Array[AnimalNode] = []
@export var animal_id: StringName
@export var display_name := "Animal"
@export var species_id: StringName = SimulationIds.SPECIES_GOAT
@export_range(0.0, 100.0, 0.5) var initial_hunger := 70.0
@export var initial_enabled := true
@export var can_npcs_feed := true
@export var can_player_feed := true
@export var debug_label_enabled := true
@export var initial_routine_site_id: StringName
@export_range(0, 10000, 1) var initial_next_routine_tick := 12
@export_range(0.1, 10.0, 0.1) var move_speed := 1.6
@export_range(0.05, 1.0, 0.05) var waypoint_reached_distance := 0.25
@export_range(0.05, 1.0, 0.05) var arrival_distance := 0.3
@export_range(0.1, 20.0, 0.1) var rotation_speed := 7.0
@onready var interaction_point: Marker3D = get_node_or_null("InteractionPoint") as Marker3D
var state: AnimalStateRecord
var current_path := PackedVector3Array()
var path_index := 0
var navigation_site_id: StringName
var navigation_target := Vector3.INF
var navigation_request_id := 0
var path_pending := false
var has_reported_navigation_result := true
func _ready() -> void:
if animal_id.is_empty():
push_error("AnimalNode at %s has empty animal_id" % get_path())
initial_enabled = false
_update_presentation()
return
var existing := get_by_id(animal_id)
if existing != null:
push_error(
(
"Duplicate AnimalNode animal_id '%s' at %s; first registered at %s"
% [animal_id, get_path(), existing.get_path()]
)
)
initial_enabled = false
_update_presentation()
return
_all.append(self)
add_to_group("animal_nodes")
add_to_group("grass_interactors")
_try_register_with_simulation()
_update_presentation()
func _exit_tree() -> void:
navigation_request_id += 1
_all.erase(self)
_disconnect_state()
state = null
func bind_state(animal_state: AnimalStateRecord) -> bool:
if animal_state == null or animal_state.get_animal_id() != animal_id:
return false
_disconnect_state()
state = animal_state
if not state.changed.is_connected(_on_state_changed):
state.changed.connect(_on_state_changed)
if not state.fed.is_connected(_on_state_fed):
state.fed.connect(_on_state_fed)
global_position = state.get_position()
var visual := get_node_or_null("Visual")
if visual != null and visual.has_method("reset_transient_feedback"):
visual.reset_transient_feedback()
_stop_navigation()
call_deferred("_sync_navigation_from_state")
_update_presentation()
hunger_changed.emit(animal_id, state.get_hunger())
return true
func _disconnect_state() -> void:
if state == null:
return
if state.changed.is_connected(_on_state_changed):
state.changed.disconnect(_on_state_changed)
if state.fed.is_connected(_on_state_fed):
state.fed.disconnect(_on_state_fed)
func _try_register_with_simulation() -> void:
var managers := get_tree().get_nodes_in_group("simulation_manager")
if managers.size() != 1:
return
var manager := managers[0]
if "animal_care" in manager and manager.animal_care != null:
manager.animal_care.register_node(self)
func get_interaction_position() -> Vector3:
return interaction_point.global_position if interaction_point != null else global_position
func supports_action(action_id: StringName) -> bool:
return action_id == SimulationIds.ACTION_FEED_ANIMAL
func get_hunger() -> float:
return state.get_hunger() if state != null else initial_hunger
func needs_feed() -> bool:
if state != null:
return state.needs_feed()
return initial_enabled and initial_hunger >= AnimalStateRecord.FEED_THRESHOLD
func is_enabled() -> bool:
return state.is_enabled() if state != null else initial_enabled
func is_navigating() -> bool:
return not navigation_site_id.is_empty() and not has_reported_navigation_result
func get_navigation_path() -> PackedVector3Array:
return current_path.duplicate()
func _physics_process(delta: float) -> void:
if (
state == null
or not state.has_active_travel_target()
or navigation_site_id.is_empty()
or has_reported_navigation_result
):
return
if path_pending:
return
if _horizontal_distance_to(navigation_target) <= arrival_distance:
_report_arrival_once()
return
if current_path.is_empty() or path_index >= current_path.size():
_report_navigation_failure_once()
return
var next_position := current_path[path_index]
var offset := next_position - global_position
if offset.length() <= waypoint_reached_distance:
path_index += 1
return
var direction := offset.normalized()
global_position = global_position.move_toward(next_position, move_speed * delta)
position_changed.emit(animal_id, global_position)
var target_angle := atan2(direction.x, direction.z)
rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta)
func _on_state_changed(changed_state: AnimalStateRecord) -> void:
if changed_state != state:
return
if not global_position.is_equal_approx(state.get_position()):
global_position = state.get_position()
hunger_changed.emit(animal_id, state.get_hunger())
reservation_changed.emit(animal_id, state.get_reserved_by())
_sync_navigation_from_state()
_update_presentation()
func _on_state_fed(fed_state: AnimalStateRecord, actor_id: int) -> void:
if fed_state != state:
return
var visual := get_node_or_null("Visual")
if visual != null and visual.has_method("play_fed_response"):
visual.play_fed_response()
fed.emit(animal_id, actor_id)
func _update_presentation() -> void:
var visual := get_node_or_null("Visual")
if visual != null:
visual.visible = is_enabled()
if visual.has_method("set_hungry"):
visual.set_hungry(needs_feed())
if visual.has_method("set_moving"):
visual.set_moving(is_navigating())
if not has_node("DebugLabel"):
return
var label := $DebugLabel as Label3D
label.visible = debug_label_enabled
if not debug_label_enabled:
return
var text := "%s\n%s · hunger %.0f" % [display_name, animal_id, get_hunger()]
if needs_feed():
text += "\nHUNGRY"
if state != null and state.get_reserved_by() >= 0:
text += "\nheld:%d" % state.get_reserved_by()
if state != null and state.has_active_travel_target():
text += "\n%s" % state.get_travel_target_site_id()
elif state != null and not state.get_routine_site_id().is_empty():
text += "\n@ %s" % state.get_routine_site_id()
if state == null:
text += "\nUNBOUND"
label.text = text
func set_debug_label_enabled(is_enabled: bool) -> void:
debug_label_enabled = is_enabled
_update_presentation()
func _sync_navigation_from_state() -> void:
if state == null or not state.has_active_travel_target():
if not navigation_site_id.is_empty():
_stop_navigation()
return
var target_site_id := state.get_travel_target_site_id()
var target_position := state.get_travel_target_position()
if (
navigation_site_id == target_site_id
and navigation_target.is_equal_approx(target_position)
and not has_reported_navigation_result
):
return
_request_navigation(target_site_id, target_position)
func _request_navigation(site_id: StringName, target_position: Vector3) -> void:
navigation_request_id += 1
var request_id := navigation_request_id
navigation_site_id = site_id
navigation_target = target_position
current_path = PackedVector3Array()
path_index = 0
path_pending = true
has_reported_navigation_result = false
_update_presentation()
await get_tree().physics_frame
if request_id != navigation_request_id or state == null:
return
current_path = NavigationServer3D.map_get_path(
get_world_3d().navigation_map, global_position, target_position, true
)
path_pending = false
if current_path.is_empty():
_report_navigation_failure_once()
func _stop_navigation() -> void:
navigation_request_id += 1
current_path = PackedVector3Array()
path_index = 0
navigation_site_id = &""
navigation_target = Vector3.INF
path_pending = false
has_reported_navigation_result = true
var visual := get_node_or_null("Visual")
if visual != null and visual.has_method("set_moving"):
visual.set_moving(false)
func _report_arrival_once() -> void:
if has_reported_navigation_result:
return
has_reported_navigation_result = true
global_position = navigation_target
position_changed.emit(animal_id, global_position)
var arrived_site_id := navigation_site_id
current_path = PackedVector3Array()
path_index = 0
path_pending = false
var visual := get_node_or_null("Visual")
if visual != null and visual.has_method("set_moving"):
visual.set_moving(false)
routine_arrived.emit(animal_id, arrived_site_id, global_position)
func _report_navigation_failure_once() -> void:
if has_reported_navigation_result:
return
has_reported_navigation_result = true
var failed_site_id := navigation_site_id
current_path = PackedVector3Array()
path_index = 0
path_pending = false
var visual := get_node_or_null("Visual")
if visual != null and visual.has_method("set_moving"):
visual.set_moving(false)
navigation_failed.emit(animal_id, failed_site_id)
func _horizontal_distance_to(target_position: Vector3) -> float:
return Vector2(global_position.x, global_position.z).distance_to(
Vector2(target_position.x, target_position.z)
)
static func get_by_id(search_id: StringName) -> AnimalNode:
for node in _all:
if node.animal_id == search_id:
return node
return null
static func get_all() -> Array[AnimalNode]:
return _all.duplicate()
+76
View File
@@ -0,0 +1,76 @@
@tool
class_name AnimalRoutineSite
extends Marker3D
static var _all: Array[AnimalRoutineSite] = []
@export var site_id: StringName
@export var display_name := "Animal site"
@export var species_id: StringName = SimulationIds.SPECIES_GOAT
@export var debug_label_enabled := false
func _ready() -> void:
_update_debug_label()
if Engine.is_editor_hint():
return
if site_id.is_empty():
push_error("AnimalRoutineSite at %s has an empty site_id" % get_path())
return
var existing := get_by_id(site_id)
if existing != null:
push_error(
(
"Duplicate AnimalRoutineSite site_id '%s' at %s; first registered at %s"
% [site_id, get_path(), existing.get_path()]
)
)
return
_all.append(self)
add_to_group("animal_routine_sites")
func _exit_tree() -> void:
if not Engine.is_editor_hint():
_all.erase(self)
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
if site_id.is_empty():
warnings.append("Assign a stable site_id before using this routine site.")
if species_id.is_empty():
warnings.append("Assign the species that can use this routine site.")
return warnings
func supports_species(candidate_species_id: StringName) -> bool:
return candidate_species_id == species_id
func get_routine_position() -> Vector3:
return global_position
func set_debug_label_enabled(is_enabled: bool) -> void:
debug_label_enabled = is_enabled
_update_debug_label()
func _update_debug_label() -> void:
var label := get_node_or_null("DebugLabel") as Label3D
if label == null:
return
label.visible = debug_label_enabled
label.text = "%s\n%s" % [display_name, site_id]
static func get_by_id(search_id: StringName) -> AnimalRoutineSite:
for site in _all:
if site.site_id == search_id:
return site
return null
static func get_all() -> Array[AnimalRoutineSite]:
return _all.duplicate()
+1
View File
@@ -0,0 +1 @@
uid://cpx5saq28qf4o
@@ -1,7 +1,7 @@
[gd_scene load_steps=26 format=3]
[gd_scene load_steps=24 format=3]
[ext_resource type="Script" path="res://world/animals/AnimalNode.gd" id="1_animal"]
[ext_resource type="Script" path="res://world/jajce/CozyGoat.gd" id="2_visual"]
[ext_resource type="Script" path="res://world/animals/animal_node.gd" id="1_animal"]
[ext_resource type="Script" path="res://world/animals/goat/cozy_goat_visual.gd" id="2_visual"]
[sub_resource type="StandardMaterial3D" id="Material_wool"]
albedo_color = Color(0.88, 0.82, 0.67, 1)
@@ -23,10 +23,6 @@ roughness = 0.86
albedo_color = Color(0.12, 0.5, 0.5, 1)
roughness = 0.72
[sub_resource type="StandardMaterial3D" id="Material_wood"]
albedo_color = Color(0.35, 0.19, 0.09, 1)
roughness = 0.96
[sub_resource type="StandardMaterial3D" id="Material_hunger"]
transparency = 1
shading_mode = 0
@@ -124,13 +120,6 @@ outer_radius = 0.32
rings = 14
ring_segments = 8
[sub_resource type="CylinderMesh" id="Mesh_bowl"]
material = SubResource("Material_wood")
top_radius = 0.36
bottom_radius = 0.28
height = 0.13
radial_segments = 12
[sub_resource type="TorusMesh" id="Mesh_hunger_bowl"]
material = SubResource("Material_hunger")
inner_radius = 0.18
@@ -164,7 +153,9 @@ script = ExtResource("1_animal")
animal_id = &"goat_dunja"
display_name = "Dunja"
species_id = &"goat"
initial_hunger = 72.0
initial_hunger = 40.0
initial_routine_site_id = &"dunja_shelter"
initial_next_routine_tick = 8
[node name="Visual" type="Node3D" parent="."]
script = ExtResource("2_visual")
@@ -276,10 +267,6 @@ rotation_degrees = Vector3(60, 0, 0)
position = Vector3(0, 0.18, 0)
mesh = SubResource("Mesh_tail")
[node name="FeedBowl" type="MeshInstance3D" parent="Visual"]
position = Vector3(0.72, 0.08, 1.43)
mesh = SubResource("Mesh_bowl")
[node name="HungerCueRoot" type="Node3D" parent="Visual"]
position = Vector3(0, 2.05, 0.18)
@@ -7,15 +7,22 @@ const FED_RESPONSE_BASE_Y := 1.8
@onready var tail_root: Node3D = $TailRoot
@onready var hunger_cue_root: Node3D = $HungerCueRoot
@onready var fed_response_root: Node3D = $FedResponseRoot
@onready var leg_front_left: Node3D = $LegFrontLeft
@onready var leg_front_right: Node3D = $LegFrontRight
@onready var leg_back_left: Node3D = $LegBackLeft
@onready var leg_back_right: Node3D = $LegBackRight
var idle_phase := 0.0
var is_hungry := false
var is_moving := false
var fed_tween: Tween
var head_base_position: Vector3
var visual_base_position: Vector3
func _ready() -> void:
head_base_position = head_root.position
visual_base_position = position
reset_transient_feedback()
@@ -23,11 +30,18 @@ func _process(delta: float) -> void:
idle_phase = fmod(idle_phase + delta, TAU)
var breath := sin(idle_phase * 1.7) * 0.025
var hungry_drop := -0.12 if is_hungry else 0.0
var walking_bob := absf(sin(idle_phase * 5.4)) * 0.045 if is_moving else 0.0
position.y = visual_base_position.y + walking_bob
head_root.position.y = head_base_position.y + breath + hungry_drop
head_root.rotation_degrees.x = (
8.0 + sin(idle_phase * 1.2) * 2.0 if is_hungry else -3.0 + sin(idle_phase * 1.4) * 2.5
)
tail_root.rotation_degrees.z = sin(idle_phase * 3.1) * (4.0 if is_hungry else 13.0)
var stride := sin(idle_phase * 5.4) * 14.0 if is_moving else 0.0
leg_front_left.rotation_degrees.x = stride
leg_back_right.rotation_degrees.x = stride
leg_front_right.rotation_degrees.x = -stride
leg_back_left.rotation_degrees.x = -stride
if hunger_cue_root.visible:
hunger_cue_root.position.y = HUNGER_CUE_BASE_Y + sin(idle_phase * 2.2) * 0.07
@@ -37,6 +51,16 @@ func set_hungry(value: bool) -> void:
hunger_cue_root.visible = value
func set_moving(value: bool) -> void:
is_moving = value
if not value:
position = visual_base_position
leg_front_left.rotation_degrees.x = 0.0
leg_front_right.rotation_degrees.x = 0.0
leg_back_left.rotation_degrees.x = 0.0
leg_back_right.rotation_degrees.x = 0.0
func reset_transient_feedback() -> void:
if fed_tween != null:
fed_tween.kill()
+203
View File
@@ -0,0 +1,203 @@
[gd_scene load_steps=19 format=3]
[ext_resource type="Script" path="res://world/animals/animal_routine_site.gd" id="1_site"]
[sub_resource type="StandardMaterial3D" id="Material_timber"]
albedo_color = Color(0.43, 0.26, 0.12, 1)
roughness = 0.96
[sub_resource type="StandardMaterial3D" id="Material_roof"]
albedo_color = Color(0.4, 0.38, 0.18, 1)
roughness = 0.98
[sub_resource type="StandardMaterial3D" id="Material_fence"]
albedo_color = Color(0.55, 0.38, 0.19, 1)
roughness = 0.95
[sub_resource type="StandardMaterial3D" id="Material_clover"]
albedo_color = Color(0.29, 0.55, 0.24, 1)
roughness = 0.92
[sub_resource type="StandardMaterial3D" id="Material_bowl"]
albedo_color = Color(0.24, 0.48, 0.48, 1)
roughness = 0.82
[sub_resource type="StandardMaterial3D" id="Material_straw"]
albedo_color = Color(0.7, 0.53, 0.25, 1)
roughness = 0.98
[sub_resource type="StandardMaterial3D" id="Material_glow"]
shading_mode = 0
albedo_color = Color(1, 0.72, 0.3, 0.9)
emission_enabled = true
emission = Color(1, 0.46, 0.12, 1)
emission_energy_multiplier = 0.42
[sub_resource type="BoxMesh" id="Mesh_post"]
material = SubResource("Material_timber")
size = Vector3(0.18, 2.05, 0.18)
[sub_resource type="BoxMesh" id="Mesh_beam"]
material = SubResource("Material_timber")
size = Vector3(2.75, 0.16, 0.18)
[sub_resource type="BoxMesh" id="Mesh_roof"]
material = SubResource("Material_roof")
size = Vector3(1.65, 0.13, 2.55)
[sub_resource type="CylinderMesh" id="Mesh_fence_post"]
material = SubResource("Material_fence")
top_radius = 0.08
bottom_radius = 0.11
height = 0.92
radial_segments = 8
[sub_resource type="BoxMesh" id="Mesh_fence_rail"]
material = SubResource("Material_fence")
size = Vector3(2.55, 0.1, 0.1)
[sub_resource type="SphereMesh" id="Mesh_clover"]
material = SubResource("Material_clover")
radius = 0.24
height = 0.16
radial_segments = 8
rings = 4
[sub_resource type="CylinderMesh" id="Mesh_bowl"]
material = SubResource("Material_bowl")
top_radius = 0.34
bottom_radius = 0.27
height = 0.13
radial_segments = 12
[sub_resource type="BoxMesh" id="Mesh_straw"]
material = SubResource("Material_straw")
size = Vector3(1.65, 0.09, 1.15)
[sub_resource type="SphereMesh" id="Mesh_glow"]
material = SubResource("Material_glow")
radius = 0.08
height = 0.13
radial_segments = 8
rings = 4
[node name="DunjaHabitat" type="Node3D"]
[node name="Shelter" type="Node3D" parent="."]
[node name="PostFrontLeft" type="MeshInstance3D" parent="Shelter"]
position = Vector3(-1.18, 1.02, 2.2)
mesh = SubResource("Mesh_post")
[node name="PostFrontRight" type="MeshInstance3D" parent="Shelter"]
position = Vector3(1.18, 1.02, 2.2)
mesh = SubResource("Mesh_post")
[node name="PostBackLeft" type="MeshInstance3D" parent="Shelter"]
position = Vector3(-1.18, 1.02, 0.25)
mesh = SubResource("Mesh_post")
[node name="PostBackRight" type="MeshInstance3D" parent="Shelter"]
position = Vector3(1.18, 1.02, 0.25)
mesh = SubResource("Mesh_post")
[node name="FrontBeam" type="MeshInstance3D" parent="Shelter"]
position = Vector3(0, 2.02, 2.2)
mesh = SubResource("Mesh_beam")
[node name="BackBeam" type="MeshInstance3D" parent="Shelter"]
position = Vector3(0, 2.02, 0.25)
mesh = SubResource("Mesh_beam")
[node name="RoofLeft" type="MeshInstance3D" parent="Shelter"]
position = Vector3(-0.72, 2.31, 1.22)
rotation_degrees = Vector3(0, 0, -19)
mesh = SubResource("Mesh_roof")
[node name="RoofRight" type="MeshInstance3D" parent="Shelter"]
position = Vector3(0.72, 2.31, 1.22)
rotation_degrees = Vector3(0, 0, 19)
mesh = SubResource("Mesh_roof")
[node name="StrawBed" type="MeshInstance3D" parent="Shelter"]
position = Vector3(0, 0.05, 1.15)
mesh = SubResource("Mesh_straw")
[node name="FeedBowl" type="MeshInstance3D" parent="Shelter"]
position = Vector3(0.72, 0.08, 2.52)
mesh = SubResource("Mesh_bowl")
[node name="Lantern" type="MeshInstance3D" parent="Shelter"]
position = Vector3(0, 1.87, 2.1)
mesh = SubResource("Mesh_glow")
[node name="ShelterSite" type="Marker3D" parent="."]
position = Vector3(0, 0, 1.5)
script = ExtResource("1_site")
site_id = &"dunja_shelter"
display_name = "Warm shelter"
species_id = &"goat"
[node name="DebugLabel" type="Label3D" parent="ShelterSite"]
position = Vector3(0, 2.45, 0)
billboard = 1
no_depth_test = true
font_size = 20
outline_size = 5
modulate = Color(1, 0.78, 0.45, 1)
pixel_size = 0.006
[node name="Pasture" type="Node3D" parent="."]
position = Vector3(-4, 0, -2.5)
[node name="PostNorthLeft" type="MeshInstance3D" parent="Pasture"]
position = Vector3(-1.3, 0.46, -1)
mesh = SubResource("Mesh_fence_post")
[node name="PostNorthRight" type="MeshInstance3D" parent="Pasture"]
position = Vector3(1.3, 0.46, -1)
mesh = SubResource("Mesh_fence_post")
[node name="NorthRail" type="MeshInstance3D" parent="Pasture"]
position = Vector3(0, 0.58, -1)
mesh = SubResource("Mesh_fence_rail")
[node name="PostWestSouth" type="MeshInstance3D" parent="Pasture"]
position = Vector3(-1.3, 0.46, 1)
mesh = SubResource("Mesh_fence_post")
[node name="WestRail" type="MeshInstance3D" parent="Pasture"]
position = Vector3(-1.3, 0.58, 0)
rotation_degrees = Vector3(0, 90, 0)
mesh = SubResource("Mesh_fence_rail")
[node name="CloverA" type="MeshInstance3D" parent="Pasture"]
position = Vector3(-0.45, 0.08, -0.3)
scale = Vector3(1.1, 1, 0.8)
mesh = SubResource("Mesh_clover")
[node name="CloverB" type="MeshInstance3D" parent="Pasture"]
position = Vector3(0.35, 0.08, 0.2)
scale = Vector3(0.85, 1, 1.15)
mesh = SubResource("Mesh_clover")
[node name="CloverC" type="MeshInstance3D" parent="Pasture"]
position = Vector3(0.78, 0.07, -0.48)
scale = Vector3(0.72, 0.9, 0.72)
mesh = SubResource("Mesh_clover")
[node name="PastureSite" type="Marker3D" parent="."]
position = Vector3(-4, 0, -2.5)
script = ExtResource("1_site")
site_id = &"dunja_pasture"
display_name = "Clover pasture"
species_id = &"goat"
[node name="DebugLabel" type="Label3D" parent="PastureSite"]
position = Vector3(0, 2.15, 0)
billboard = 1
no_depth_test = true
font_size = 20
outline_size = 5
modulate = Color(0.72, 0.95, 0.54, 1)
pixel_size = 0.006
@@ -3,14 +3,14 @@
[ext_resource type="PackedScene" path="res://world/jajce/JajceWorld.tscn" id="1_world"]
[ext_resource type="Script" path="res://world/jajce/beauty_camera.gd" id="2_camera"]
[node name="JajceGoatLookdev" type="Node3D"]
[node name="DunjaLookdev" type="Node3D"]
[node name="JajceWorld" parent="." instance=ExtResource("1_world")]
[node name="GoatCamera" type="Camera3D" parent="."]
position = Vector3(0.2, 2.9, -7)
position = Vector3(5, 4.5, -25)
current = true
fov = 32.0
fov = 38.0
script = ExtResource("2_camera")
focus_point = Vector3(-4, 1.15, -11.5)
focus_point = Vector3(-2, 1.05, -17.5)
animate_orbit = false
+4
View File
@@ -363,6 +363,10 @@ func _apply_debug_overlay_visibility() -> void:
if node.has_method("set_debug_label_enabled"):
node.set_debug_label_enabled(debug_overlay_visible)
for node in AnimalRoutineSite.get_all():
if node.has_method("set_debug_label_enabled"):
node.set_debug_label_enabled(debug_overlay_visible)
for node in StorageNode.get_all():
if node.has_method("set_debug_label_enabled"):
node.set_debug_label_enabled(debug_overlay_visible)
+10 -4
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=60 format=3]
[gd_scene load_steps=61 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"]
@@ -27,7 +27,8 @@
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_process_material.tres" id="25_grass_process"]
[ext_resource type="PackedScene" path="res://world/jajce/RiverbankResourceCluster.tscn" id="26_resource_cluster"]
[ext_resource type="PackedScene" path="res://world/jajce/ForestEdgeResourceCluster.tscn" id="27_forest_cluster"]
[ext_resource type="PackedScene" path="res://world/jajce/CozyGoat.tscn" id="28_goat"]
[ext_resource type="PackedScene" path="res://world/animals/goat/cozy_goat.tscn" id="28_goat"]
[ext_resource type="PackedScene" path="res://world/animals/goat/dunja_habitat.tscn" id="29_habitat"]
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
_shader_parameters = {
@@ -462,8 +463,13 @@ phase = 4.4
[node name="Animals" type="Node3D" parent="WorldObjects"]
[node name="Dunja" parent="WorldObjects/Animals" instance=ExtResource("28_goat")]
position = Vector3(-4, 0, -11.5)
rotation_degrees = Vector3(0, -18, 0)
position = Vector3(0, 0, -15.5)
rotation_degrees = Vector3(0, 150, 0)
[node name="AnimalHabitats" type="Node3D" parent="WorldObjects"]
[node name="DunjaHabitat" parent="WorldObjects/AnimalHabitats" instance=ExtResource("29_habitat")]
position = Vector3(0, 0, -17)
[node name="ResourceNodes" type="Node3D" parent="WorldObjects"]
+3 -1
View File
@@ -21,7 +21,9 @@ func _initialize_world_presentation() -> void:
groups_to_snap.append(child)
if has_node("WorldObjects"):
var wo := $WorldObjects
for group_name in ["Animals", "ResourceNodes", "StorageSites", "ActivitySites"]:
for group_name in [
"Animals", "AnimalHabitats", "ResourceNodes", "StorageSites", "ActivitySites"
]:
if wo.has_node(group_name):
groups_to_snap.append_array(wo.get_node(group_name).get_children())