feat: add deterministic goat routine
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
v1–v7 migrate to an empty opportunity list, while world schemas v8 and v9
|
||||
v1–v7 migrate to an empty opportunity list, while world schemas v8–v10
|
||||
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
|
||||
|
||||
@@ -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
@@ -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,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 v1–v9 layouts;
|
||||
supported v1–v10 layouts;
|
||||
- player inventory or player relationship records;
|
||||
- broader relationship dimensions, line-of-sight/hearing evidence,
|
||||
continuous or personalized memory decay, reinforcement, false beliefs, or
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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:
|
||||
animal_state.advance_tick()
|
||||
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:
|
||||
@@ -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"
|
||||
|
||||
@@ -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,7 +678,8 @@ 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
|
||||
migrated["animals"] = []
|
||||
if version <= ANIMAL_LEGACY_SCHEMA_VERSION:
|
||||
migrated["animals"] = []
|
||||
if version == LEGACY_SCHEMA_VERSION:
|
||||
var village_data: Dictionary = legacy_data.get("village", {})
|
||||
var initial_food := float(village_data.get("food", 0.0))
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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(
|
||||
(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user