feat: add deterministic goat routine
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -16,7 +16,7 @@ SimulationClock
|
|||||||
-> ActionSelectionSystem chooses an action
|
-> ActionSelectionSystem chooses an action
|
||||||
-> ActionTargetResolver resolves a stable target ID
|
-> ActionTargetResolver resolves a stable target ID
|
||||||
-> VillageEconomy performs inventory/storage transactions
|
-> VillageEconomy performs inventory/storage transactions
|
||||||
-> AnimalCareSystem advances animal needs and composes feeding
|
-> AnimalCareSystem advances animal needs, routines, and feeding
|
||||||
-> SimulationEventLog records completed facts
|
-> SimulationEventLog records completed facts
|
||||||
-> EventKnowledgeSystem records, ranks, transfers, and retains bounded knowledge
|
-> EventKnowledgeSystem records, ranks, transfers, and retains bounded knowledge
|
||||||
-> RelationshipSystem applies evidence-gated social consequences
|
-> RelationshipSystem applies evidence-gated social consequences
|
||||||
@@ -25,6 +25,7 @@ SimulationClock
|
|||||||
-> ActiveWorldAdapter supplies loaded-world positions/capacity
|
-> ActiveWorldAdapter supplies loaded-world positions/capacity
|
||||||
-> LoadedResourceSpatialIndex bounds finite-anchor discovery
|
-> LoadedResourceSpatialIndex bounds finite-anchor discovery
|
||||||
-> NpcVisual performs local navigation, animation, and transient reactions
|
-> NpcVisual performs local navigation, animation, and transient reactions
|
||||||
|
-> AnimalNode follows saved animal destinations through loaded navigation
|
||||||
-> PantryStockVisual derives physical stock arrangement from storage state
|
-> 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/actions/` owns selection, progress, and target resolution;
|
||||||
- `simulation/economy/VillageEconomy.gd` owns storage/inventory transactions
|
- `simulation/economy/VillageEconomy.gd` owns storage/inventory transactions
|
||||||
and keeps village resource summaries synchronized;
|
and keeps village resource summaries synchronized;
|
||||||
- `simulation/animals/AnimalCareSystem.gd` owns animal records, hunger
|
- `simulation/animals/animal_care_system.gd` owns animal records, hunger
|
||||||
advancement, loaded binding, feed reservations, and the exact conserved
|
advancement, deterministic routine selection, loaded binding, feed
|
||||||
pantry-to-animal operation used by NPCs and the player;
|
reservations, and the exact conserved pantry-to-animal operation used by
|
||||||
|
NPCs and the player;
|
||||||
- `simulation/events/SimulationEventLog.gd` owns ordered event identity,
|
- `simulation/events/SimulationEventLog.gd` owns ordered event identity,
|
||||||
history queries, and rate calculations;
|
history queries, and rate calculations;
|
||||||
- `simulation/knowledge/EventKnowledgeSystem.gd` owns per-NPC references to
|
- `simulation/knowledge/EventKnowledgeSystem.gd` owns per-NPC references to
|
||||||
@@ -105,17 +107,29 @@ hard to read.
|
|||||||
| `simulation/persistence/` | Validated local save-file storage |
|
| `simulation/persistence/` | Validated local save-file storage |
|
||||||
| `simulation/benchmark/` | Reproducible simulation and loaded-world query workloads |
|
| `simulation/benchmark/` | Reproducible simulation and loaded-world query workloads |
|
||||||
| `world/` | Loaded-world interaction geometry and presentation adapters |
|
| `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/resource_nodes/` | Finite resource presentation and disposable loaded-anchor index |
|
||||||
| `world/storage/` | Storage interaction geometry, never stored quantities |
|
| `world/storage/` | Storage interaction geometry, never stored quantities |
|
||||||
| `world/activity/` | Rest/study/patrol interaction sites and capacity facts |
|
| `world/activity/` | Rest/study/patrol interaction sites and capacity facts |
|
||||||
| `player/` | Player input, camera, and active NPC presentation |
|
| `player/` | Player input, camera, and active NPC presentation |
|
||||||
| `tests/` | Deterministic headless gameplay scenarios |
|
| `tests/` | Deterministic headless gameplay scenarios |
|
||||||
|
|
||||||
Top-level core model scripts keep their stable paths because Godot's global
|
Unrelated established model scripts keep their stable paths because Godot's
|
||||||
class cache records `class_name` locations. Moving them solely for cosmetic
|
global class cache records `class_name` locations. This slice moves only the
|
||||||
nesting can break editor and headless startup for existing workspaces without
|
animal files whose ownership changed, preserves their `.uid` files, and proves
|
||||||
improving ownership.
|
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
|
## Dependency rules
|
||||||
|
|
||||||
@@ -136,10 +150,12 @@ improving ownership.
|
|||||||
reactions are not replayed.
|
reactions are not replayed.
|
||||||
- Resource changes go through `ResourceStateRecord`, NPC inventory, and
|
- Resource changes go through `ResourceStateRecord`, NPC inventory, and
|
||||||
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
||||||
- Animal identity, position, hunger, last feed tick, and reservation live in
|
- Animal identity, position, hunger, last feed tick, reservation, routine site,
|
||||||
`AnimalStateRecord`. `AnimalNode` binds that state and contributes only its
|
destination, and next due tick live in `AnimalStateRecord`. `AnimalNode`
|
||||||
loaded interaction point. Animal candidates are currently scanned linearly;
|
binds that state, contributes its loaded interaction point, follows the
|
||||||
they do not enter the resource grid.
|
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
|
- New mutable features define serialization and deterministic continuation at
|
||||||
the same time as their first gameplay use. Cross-record causes use stable
|
the same time as their first gameplay use. Cross-record causes use stable
|
||||||
event IDs rather than object references or prose.
|
event IDs rather than object references or prose.
|
||||||
|
|||||||
@@ -804,15 +804,21 @@ Completed:
|
|||||||
exact transaction that withdraws pantry food, changes only Dunja's state,
|
exact transaction that withdraws pantry food, changes only Dunja's state,
|
||||||
and emits one economic fact. World-schema v10 save/restore preserves the
|
and emits one economic fact. World-schema v10 save/restore preserves the
|
||||||
result without replaying the transient feed response.
|
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:
|
Next:
|
||||||
|
|
||||||
1. Give Dunja one deterministic pasture/shelter routine using
|
1. Add one second named goat with its own stable state, authored shelter
|
||||||
simulation-owned destination and position, real loaded-world navigation,
|
context, and deterministic use of the shared pasture. Prove nearest
|
||||||
and exact mid-move save/restore continuation.
|
feed-target selection and independent reservations when both animals are
|
||||||
2. Add a second goat only after movement proves that identity remains stable
|
hungry.
|
||||||
across care, navigation, unloading, and restore.
|
2. Keep breeding, products, herd scheduling, animal spatial indexing, and
|
||||||
3. Keep breeding, products, herd scheduling, animal spatial indexing, and
|
|
||||||
active/abstract simulation LOD deferred until two real animals require them.
|
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
|
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
|
## 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
|
`next_event_id`, directed relationships that may reference an exact event, and
|
||||||
per-NPC known-event references with first-acquisition provenance, plus
|
per-NPC known-event references with first-acquisition provenance, plus
|
||||||
opportunity records that reference exact trigger/resolution events. Schema v1
|
opportunity records that reference exact trigger/resolution events. Schema v1
|
||||||
and v2 saves migrate to an empty stream beginning at ID zero; world schemas
|
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
|
preserve and normalize opportunity history. World v9 adds an empty animal list
|
||||||
during migration. Parsing rejects duplicate event
|
during migration; world v10 preserves and upgrades its animal records. Parsing
|
||||||
IDs, invalid or duplicate knowledge/opportunity references, impossible
|
rejects duplicate event IDs, invalid or duplicate knowledge/opportunity
|
||||||
communicator sources, relationship causes the observer does not know, and next
|
references, impossible communicator sources, relationship causes the observer
|
||||||
IDs that could collide with restored history.
|
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
|
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
|
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
|
target animal, and appends one economic fact. World-schema v10 save/restore
|
||||||
preserves the result without replaying the transient response.
|
preserves the result without replaying the transient response.
|
||||||
|
|
||||||
The immediate next slice should give Dunja one deterministic
|
The first loaded-animal routine slice is complete. Dunja alternates between
|
||||||
pasture/shelter routine. Her destination and position must remain
|
stable shelter and pasture IDs without RNG; her exact target and current
|
||||||
simulation-owned, loaded movement must use real navigation, and a mid-move
|
position stay simulation-owned while the loaded scene follows the Jajce
|
||||||
save/restore must continue exactly without consuming new decision RNG. Keep
|
navigation map. Schema-v11 mid-route restore preserves the checksum,
|
||||||
loaded-animal discovery linear and do not add a second goat, herd scheduling,
|
destination, and every NPC RNG stream. Unloading and re-instantiating the goat
|
||||||
breeding, products, or active/abstract simulation LOD until this movement
|
scene retains that same travel state, arrival is exact, and hunger stops
|
||||||
contract is honest.
|
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:
|
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
|
- `Jajce Goat 07`: named goat Dunja proves stable animal identity, exact
|
||||||
pantry-conserving NPC/player feeding, deterministic hunger and save/restore,
|
pantry-conserving NPC/player feeding, deterministic hunger and save/restore,
|
||||||
loaded target reservation, a state-derived hungry cue, and a transient cozy
|
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
|
│ ├── SimulationClock.gd
|
||||||
│ ├── SimulationManager.gd
|
│ ├── SimulationManager.gd
|
||||||
│ ├── actions/ Selection, execution, and target resolution
|
│ ├── actions/ Selection, execution, and target resolution
|
||||||
|
│ ├── animals/ Animal needs, routines, reservations, and care
|
||||||
│ ├── benchmark/ Population/history and resource-query workloads
|
│ ├── benchmark/ Population/history and resource-query workloads
|
||||||
│ ├── definitions/ Stable IDs and custom definition resources
|
│ ├── definitions/ Stable IDs and custom definition resources
|
||||||
│ ├── economy/ Inventory and storage transactions
|
│ ├── economy/ Inventory and storage transactions
|
||||||
@@ -587,6 +588,10 @@ NpcVisual navigates through the active world
|
|||||||
│ ├── benchmark_loaded_resource_discovery.gd
|
│ ├── benchmark_loaded_resource_discovery.gd
|
||||||
│ └── generate_jajce_terrain_seed.gd
|
│ └── generate_jajce_terrain_seed.gd
|
||||||
├── world/
|
├── world/
|
||||||
|
│ ├── animals/
|
||||||
|
│ │ ├── animal_node.gd
|
||||||
|
│ │ ├── animal_routine_site.gd
|
||||||
|
│ │ └── goat/ Goat scene, habitat, visual, and lookdev assets
|
||||||
│ ├── jajce/
|
│ ├── jajce/
|
||||||
│ │ ├── JajceWorld.tscn
|
│ │ ├── JajceWorld.tscn
|
||||||
│ │ ├── JajceLookdev.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`.
|
the typed pantry `StorageNode`, and patrol/study/rest use `ActivitySite`.
|
||||||
- Current NPC, village, resource, storage, animal, event, knowledge,
|
- Current NPC, village, resource, storage, animal, event, knowledge,
|
||||||
relationship, opportunity, clock, and RNG state serialize through world
|
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.
|
metadata, and player-transform persistence remain deferred.
|
||||||
- Simulation-owned resource records retain live amounts, reservations, and
|
- Simulation-owned resource records retain live amounts, reservations, and
|
||||||
usage definitions while ResourceNode scenes are unloaded.
|
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,
|
animal identity, simulation-owned position and hunger, a loaded visual binding,
|
||||||
and the same exact pantry-conserving feed operation for NPC and player.
|
and the same exact pantry-conserving feed operation for NPC and player.
|
||||||
World-schema v10 restore reconstructs her persistent state without replaying
|
World-schema v10 restore reconstructs her persistent state without replaying
|
||||||
the transient response. The next bounded animal slice should give Dunja one
|
the transient response. Dunja now also alternates between a stable shelter and
|
||||||
deterministic pasture/shelter routine before adding another goat. Do not yet
|
pasture through real loaded navigation. World-schema v11 preserves exact
|
||||||
generalize animal discovery or introduce active/abstract simulation LOD.
|
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:
|
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
|
The project uses a small hierarchy so overlapping plans do not become competing
|
||||||
sources of truth.
|
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
|
1. [`PROJECT_CONTEXT.md`](PROJECT_CONTEXT.md) is the canonical description of
|
||||||
the vision, current implementation, target architecture, and active
|
the vision, current implementation, target architecture, and active
|
||||||
constraints.
|
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,
|
farmer profession and declares an exact one-food completion cost. Selection,
|
||||||
animal target reservation, and completion remain separate; NPC and player
|
animal target reservation, and completion remain separate; NPC and player
|
||||||
completion both use `AnimalCareSystem`'s same pantry-to-animal transaction.
|
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
|
## Current profession contract
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Current contract
|
## Current contract
|
||||||
|
|
||||||
`SimulationStateRecord` is the versioned JSON boundary for the current
|
`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
|
- simulation seed, tick interval, tick count, clock remainder, and elapsed
|
||||||
clock ticks;
|
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
|
- scene-independent resource state plus the definition facts needed while its
|
||||||
ResourceNode is unloaded;
|
ResourceNode is unloaded;
|
||||||
- scene-independent animal identity, position, hunger, feeding history,
|
- 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,
|
- pantry contents, carried NPC inventory, the ordered economic event stream,
|
||||||
and its next stable event ID;
|
and its next stable event ID;
|
||||||
- directed relationship records with familiarity, trust, and the stable event
|
- directed relationship records with familiarity, trust, and the stable event
|
||||||
@@ -31,7 +32,7 @@ The top-level identity is:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"schema": "the_steward.simulation",
|
"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
|
World schema v9 preserves opportunity history while migrating to an empty
|
||||||
animal list.
|
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
|
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
|
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
|
whose active feed task targets that exact animal. Every `animal_fed` event
|
||||||
@@ -255,9 +263,10 @@ after a JSON round trip.
|
|||||||
## Animal authority
|
## Animal authority
|
||||||
|
|
||||||
`AnimalCareSystem` owns `AnimalStateRecord` instances independently of the
|
`AnimalCareSystem` owns `AnimalStateRecord` instances independently of the
|
||||||
scene tree and advances their hunger in stable-ID order. `AnimalNode` binds a
|
scene tree and advances hunger and due routine selection in stable-ID order.
|
||||||
loaded presentation and interaction point to that authority; the simulation
|
`AnimalNode` binds a loaded presentation and interaction point to that
|
||||||
position remains canonical while the node is loaded or absent.
|
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
|
Animals are not `ResourceNode` instances and are not inserted into the
|
||||||
resource discovery grid. The first bounded slice uses a deterministic linear
|
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
|
from `village_pantry`, reduce that animal's hunger, and append one exact
|
||||||
`animal_fed` fact. Failed withdrawal changes neither animal nor history.
|
`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 persistent hungry/content presentation derives from authoritative hunger.
|
||||||
The short feed response is transient and is reset rather than serialized or
|
The short feed response is transient and is reset rather than serialized or
|
||||||
replayed after restore.
|
replayed after restore.
|
||||||
@@ -288,7 +305,7 @@ This phase does not yet provide:
|
|||||||
|
|
||||||
- a save-slot menu, metadata, thumbnails, autosaves, or multiple profiles;
|
- a save-slot menu, metadata, thumbnails, autosaves, or multiple profiles;
|
||||||
- migrations from any historical world schema other than the explicitly
|
- migrations from any historical world schema other than the explicitly
|
||||||
supported v1–v9 layouts;
|
supported v1–v10 layouts;
|
||||||
- player inventory or player relationship records;
|
- player inventory or player relationship records;
|
||||||
- broader relationship dimensions, line-of-sight/hearing evidence,
|
- broader relationship dimensions, line-of-sight/hearing evidence,
|
||||||
continuous or personalized memory decay, reinforcement, false beliefs, or
|
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
|
- world-schema v10 restores position, hunger, and last-fed tick without
|
||||||
replaying presentation effects.
|
replaying presentation effects.
|
||||||
|
|
||||||
The next animal frame should prove a real pasture/shelter movement routine,
|
The follow-up [Jajce Dunja Habitat 08](JAJCE_DUNJA_HABITAT_08.md) now proves
|
||||||
not multiply static livestock.
|
the real pasture/shelter movement routine before livestock multiplies.
|
||||||
|
|
||||||
Capture command:
|
Capture command:
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ APPDATA="$PWD/logs/quality/godot_profile" \
|
|||||||
LOCALAPPDATA="$PWD/logs/quality/godot_profile" \
|
LOCALAPPDATA="$PWD/logs/quality/godot_profile" \
|
||||||
/Applications/Godot.app/Contents/MacOS/Godot \
|
/Applications/Godot.app/Contents/MacOS/Godot \
|
||||||
--path "$PWD" --script res://tools/capture_jajce_lookdev.gd -- \
|
--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 \
|
--hide-debug-labels --animal-hunger=goat_dunja:90 \
|
||||||
--output=res://docs/baselines/jajce_goat_07_hungry.png
|
--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 SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
|
||||||
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.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 RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
||||||
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ func simulate_tick() -> void:
|
|||||||
tick_count += 1
|
tick_count += 1
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("--- Tick ", tick_count, " ---")
|
print("--- Tick ", tick_count, " ---")
|
||||||
animal_care.advance()
|
animal_care.advance(tick_count)
|
||||||
var village_was_changed := false
|
var village_was_changed := false
|
||||||
_population_view.rebuild(npcs)
|
_population_view.rebuild(npcs)
|
||||||
for npc in npcs:
|
for npc in npcs:
|
||||||
@@ -1192,7 +1192,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
|||||||
for resource_record in record.resources:
|
for resource_record in record.resources:
|
||||||
resource_states[resource_record.get_node_id()] = resource_record
|
resource_states[resource_record.get_node_id()] = resource_record
|
||||||
register_loaded_resource_nodes()
|
register_loaded_resource_nodes()
|
||||||
animal_care.restore_state_records(record.animals)
|
animal_care.restore_state_records(record.animals, tick_count)
|
||||||
village_changed.emit(village)
|
village_changed.emit(village)
|
||||||
state_restored.emit()
|
state_restored.emit()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -19,10 +19,16 @@ signal narrative_event_requested(
|
|||||||
item_id: StringName,
|
item_id: StringName,
|
||||||
required_amount: float
|
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 states: Dictionary = {}
|
||||||
var economy: RefCounted
|
var economy: RefCounted
|
||||||
var active_world_adapter: Node
|
var active_world_adapter: Node
|
||||||
|
var current_tick := 0
|
||||||
|
|
||||||
|
|
||||||
func configure(economy_service: RefCounted, world_adapter: Node) -> void:
|
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
|
active_world_adapter = world_adapter
|
||||||
|
|
||||||
|
|
||||||
func advance() -> void:
|
func advance(simulation_tick: int) -> void:
|
||||||
|
current_tick = simulation_tick
|
||||||
var animal_ids: Array = states.keys()
|
var animal_ids: Array = states.keys()
|
||||||
animal_ids.sort()
|
animal_ids.sort()
|
||||||
for animal_id in animal_ids:
|
for animal_id in animal_ids:
|
||||||
var animal_state := states[animal_id] as AnimalStateRecord
|
var animal_state := states[animal_id] as AnimalStateRecord
|
||||||
if animal_state != null:
|
if animal_state == null:
|
||||||
animal_state.advance_tick()
|
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:
|
func register_loaded_nodes() -> void:
|
||||||
@@ -74,6 +86,12 @@ func register_node(node: AnimalNode) -> bool:
|
|||||||
elif not animal_state.apply_definition(node):
|
elif not animal_state.apply_definition(node):
|
||||||
push_error("AnimalCareSystem: AnimalNode definition mismatch for '%s'" % node.animal_id)
|
push_error("AnimalCareSystem: AnimalNode definition mismatch for '%s'" % node.animal_id)
|
||||||
return false
|
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)
|
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):
|
if not animal_state.feed(actor_id, current_tick):
|
||||||
economy.deposit_resource(resource_id, consumed)
|
economy.deposit_resource(resource_id, consumed)
|
||||||
return false
|
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(
|
economic_event_requested.emit(
|
||||||
SimulationIds.EVENT_ANIMAL_FED,
|
SimulationIds.EVENT_ANIMAL_FED,
|
||||||
@@ -211,13 +233,70 @@ func append_state_records(record: SimulationStateRecord) -> void:
|
|||||||
record.animals.append(animal_state)
|
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()
|
states.clear()
|
||||||
for animal_record in records:
|
for animal_record in records:
|
||||||
states[animal_record.get_animal_id()] = animal_record
|
states[animal_record.get_animal_id()] = animal_record
|
||||||
register_loaded_nodes()
|
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:
|
static func _find_npc(npc_id: int, npcs: Array[SimNPC]) -> SimNPC:
|
||||||
for npc in npcs:
|
for npc in npcs:
|
||||||
if npc.id == npc_id:
|
if npc.id == npc_id:
|
||||||
@@ -32,6 +32,8 @@ const RESOURCE_WOOD := &"wood"
|
|||||||
|
|
||||||
const ANIMAL_DUNJA := &"goat_dunja"
|
const ANIMAL_DUNJA := &"goat_dunja"
|
||||||
const SPECIES_GOAT := &"goat"
|
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_PANTRY := &"village_pantry"
|
||||||
const STORAGE_VILLAGE_WOODPILE := &"village_woodpile"
|
const STORAGE_VILLAGE_WOODPILE := &"village_woodpile"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class_name SimulationStateRecord
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_NAME := "the_steward.simulation"
|
const SCHEMA_NAME := "the_steward.simulation"
|
||||||
const SCHEMA_VERSION := 10
|
const SCHEMA_VERSION := 11
|
||||||
const LEGACY_SCHEMA_VERSION := 1
|
const LEGACY_SCHEMA_VERSION := 1
|
||||||
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
||||||
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
||||||
@@ -11,6 +11,7 @@ const PROVENANCE_LEGACY_SCHEMA_VERSION := 5
|
|||||||
const RETENTION_LEGACY_SCHEMA_VERSION := 6
|
const RETENTION_LEGACY_SCHEMA_VERSION := 6
|
||||||
const OPPORTUNITY_LEGACY_SCHEMA_VERSION := 8
|
const OPPORTUNITY_LEGACY_SCHEMA_VERSION := 8
|
||||||
const ANIMAL_LEGACY_SCHEMA_VERSION := 9
|
const ANIMAL_LEGACY_SCHEMA_VERSION := 9
|
||||||
|
const ROUTINE_LEGACY_SCHEMA_VERSION := 10
|
||||||
const PREVIOUS_SCHEMA_VERSION := 7
|
const PREVIOUS_SCHEMA_VERSION := 7
|
||||||
|
|
||||||
var simulation: Dictionary
|
var simulation: Dictionary
|
||||||
@@ -95,6 +96,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
PREVIOUS_SCHEMA_VERSION,
|
PREVIOUS_SCHEMA_VERSION,
|
||||||
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
|
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
|
||||||
ANIMAL_LEGACY_SCHEMA_VERSION,
|
ANIMAL_LEGACY_SCHEMA_VERSION,
|
||||||
|
ROUTINE_LEGACY_SCHEMA_VERSION,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
record_data = _migrate_legacy(record_data, 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:
|
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
|
||||||
var migrated := legacy_data.duplicate(true)
|
var migrated := legacy_data.duplicate(true)
|
||||||
migrated["schema_version"] = SCHEMA_VERSION
|
migrated["schema_version"] = SCHEMA_VERSION
|
||||||
migrated["animals"] = []
|
if version <= ANIMAL_LEGACY_SCHEMA_VERSION:
|
||||||
|
migrated["animals"] = []
|
||||||
if version == LEGACY_SCHEMA_VERSION:
|
if version == LEGACY_SCHEMA_VERSION:
|
||||||
var village_data: Dictionary = legacy_data.get("village", {})
|
var village_data: Dictionary = legacy_data.get("village", {})
|
||||||
var initial_food := float(village_data.get("food", 0.0))
|
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", []),
|
migrated.get("economic_events", []),
|
||||||
int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0))
|
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"] = []
|
migrated["opportunities"] = []
|
||||||
var opportunity_simulation_data: Dictionary = migrated.get("simulation", {})
|
var opportunity_simulation_data: Dictionary = migrated.get("simulation", {})
|
||||||
opportunity_simulation_data["next_opportunity_id"] = 0
|
opportunity_simulation_data["next_opportunity_id"] = 0
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ extends RefCounted
|
|||||||
signal changed(state: AnimalStateRecord)
|
signal changed(state: AnimalStateRecord)
|
||||||
signal fed(state: AnimalStateRecord, actor_id: int)
|
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 HUNGER_PER_TICK := 0.125
|
||||||
const FEED_THRESHOLD := 65.0
|
const FEED_THRESHOLD := 65.0
|
||||||
const HUNGER_RELIEF := 55.0
|
const HUNGER_RELIEF := 55.0
|
||||||
@@ -34,13 +35,21 @@ static func create_from_node(node: AnimalNode) -> AnimalStateRecord:
|
|||||||
"enabled": node.initial_enabled,
|
"enabled": node.initial_enabled,
|
||||||
"can_npcs_feed": node.can_npcs_feed,
|
"can_npcs_feed": node.can_npcs_feed,
|
||||||
"can_player_feed": node.can_player_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:
|
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
|
return null
|
||||||
if not (
|
if not (
|
||||||
record_data
|
record_data
|
||||||
@@ -56,6 +65,11 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
|||||||
"enabled",
|
"enabled",
|
||||||
"can_npcs_feed",
|
"can_npcs_feed",
|
||||||
"can_player_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 hunger := float(record_data["hunger"])
|
||||||
var last_fed_tick := int(record_data["last_fed_tick"])
|
var last_fed_tick := int(record_data["last_fed_tick"])
|
||||||
var reserved_by := int(record_data["reserved_by"])
|
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():
|
if animal_id.is_empty() or display_name.is_empty() or species_id.is_empty():
|
||||||
return null
|
return null
|
||||||
if StringName(species_id) != SimulationIds.SPECIES_GOAT:
|
if StringName(species_id) != SimulationIds.SPECIES_GOAT:
|
||||||
@@ -76,11 +96,25 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
|||||||
for component in saved_position:
|
for component in saved_position:
|
||||||
if not is_finite(float(component)):
|
if not is_finite(float(component)):
|
||||||
return null
|
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:
|
if not is_finite(hunger) or hunger < 0.0 or hunger > 100.0:
|
||||||
return null
|
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
|
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
|
return null
|
||||||
|
|
||||||
var normalized := record_data.duplicate(true)
|
var normalized := record_data.duplicate(true)
|
||||||
@@ -94,9 +128,16 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
|||||||
normalized["hunger"] = hunger
|
normalized["hunger"] = hunger
|
||||||
normalized["last_fed_tick"] = last_fed_tick
|
normalized["last_fed_tick"] = last_fed_tick
|
||||||
normalized["reserved_by"] = reserved_by
|
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_npcs_feed"] = bool(record_data["can_npcs_feed"])
|
||||||
normalized["can_player_feed"] = bool(record_data["can_player_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)
|
return AnimalStateRecord.new(normalized)
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +193,27 @@ func can_player_feed_animal() -> bool:
|
|||||||
return bool(data["can_player_feed"])
|
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:
|
func needs_feed() -> bool:
|
||||||
return is_enabled() and get_hunger() >= FEED_THRESHOLD
|
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)
|
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:
|
func reserve(agent_id: int) -> bool:
|
||||||
if agent_id < 0 or not is_available_for(agent_id):
|
if agent_id < 0 or not is_available_for(agent_id):
|
||||||
return false
|
return false
|
||||||
@@ -196,6 +268,56 @@ func feed(actor_id: int, current_tick: int) -> bool:
|
|||||||
return true
|
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:
|
func set_position(value: Vector3) -> void:
|
||||||
if not value.is_finite() or value.is_equal_approx(get_position()):
|
if not value.is_finite() or value.is_equal_approx(get_position()):
|
||||||
return
|
return
|
||||||
@@ -216,8 +338,22 @@ func set_enabled(value: bool) -> void:
|
|||||||
data["enabled"] = value
|
data["enabled"] = value
|
||||||
if not value:
|
if not value:
|
||||||
data["reserved_by"] = -1
|
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)
|
changed.emit(self)
|
||||||
|
|
||||||
|
|
||||||
func to_dictionary() -> Dictionary:
|
func to_dictionary() -> Dictionary:
|
||||||
return data.duplicate(true)
|
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)
|
var active_restored := _create_manager(902)
|
||||||
_check(
|
_check(
|
||||||
active_restored.restore_state_from_json(active_json),
|
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(
|
_check(
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ func _run() -> void:
|
|||||||
var old_contributor_visual := contributor_visual
|
var old_contributor_visual := contributor_visual
|
||||||
_check(
|
_check(
|
||||||
simulation_manager.restore_state_from_json(active_need_json),
|
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
|
await process_frame
|
||||||
contributor = simulation_manager.npcs[contributor.id]
|
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,
|
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"
|
"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")
|
_check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites")
|
||||||
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
|
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||||
_check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode")
|
_check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode")
|
||||||
@@ -467,6 +492,8 @@ func _run() -> void:
|
|||||||
for resource in resources:
|
for resource in resources:
|
||||||
destinations.append((resource as ResourceNode).interaction_point.global_position)
|
destinations.append((resource as ResourceNode).interaction_point.global_position)
|
||||||
destinations.append(goat.get_interaction_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(pantry.get_interaction_position())
|
||||||
destinations.append(woodpile.get_interaction_position())
|
destinations.append(woodpile.get_interaction_position())
|
||||||
for site in activity_root.get_children():
|
for site in activity_root.get_children():
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func _run() -> void:
|
|||||||
_test_previous_world_retention_migration()
|
_test_previous_world_retention_migration()
|
||||||
_test_previous_world_opportunity_migration()
|
_test_previous_world_opportunity_migration()
|
||||||
_test_previous_world_animal_migration()
|
_test_previous_world_animal_migration()
|
||||||
|
_test_previous_world_routine_migration()
|
||||||
_test_relationship_schema_rejection()
|
_test_relationship_schema_rejection()
|
||||||
_test_opportunity_schema_rejection()
|
_test_opportunity_schema_rejection()
|
||||||
_test_animal_schema_rejection()
|
_test_animal_schema_rejection()
|
||||||
@@ -498,6 +499,41 @@ func _test_previous_world_animal_migration() -> void:
|
|||||||
manager.free()
|
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:
|
func _test_animal_schema_rejection() -> void:
|
||||||
var manager := _create_manager(64)
|
var manager := _create_manager(64)
|
||||||
var missing_animals: Dictionary = manager.create_state_record().to_dictionary()
|
var missing_animals: Dictionary = manager.create_state_record().to_dictionary()
|
||||||
@@ -521,6 +557,11 @@ func _test_animal_schema_rejection() -> void:
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"can_npcs_feed": true,
|
"can_npcs_feed": true,
|
||||||
"can_player_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)
|
var active_restored := _create_manager(1202)
|
||||||
_check(
|
_check(
|
||||||
active_restored.restore_state_from_json(active_json),
|
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()
|
var restored_helper: OpportunityHelperResult = active_restored.get_active_opportunity_helper()
|
||||||
_check(
|
_check(
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ func _apply_resource_capture_overrides() -> bool:
|
|||||||
if hide_animal_labels:
|
if hide_animal_labels:
|
||||||
animal.set_debug_label_enabled(false)
|
animal.set_debug_label_enabled(false)
|
||||||
if hide_all_labels:
|
if hide_all_labels:
|
||||||
|
for site in AnimalRoutineSite.get_all():
|
||||||
|
site.set_debug_label_enabled(false)
|
||||||
for storage in StorageNode.get_all():
|
for storage in StorageNode.get_all():
|
||||||
storage.set_debug_label_enabled(false)
|
storage.set_debug_label_enabled(false)
|
||||||
for site in ActivitySite.get_all():
|
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
|
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/animals/animal_node.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/goat/cozy_goat_visual.gd" id="2_visual"]
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_wool"]
|
[sub_resource type="StandardMaterial3D" id="Material_wool"]
|
||||||
albedo_color = Color(0.88, 0.82, 0.67, 1)
|
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)
|
albedo_color = Color(0.12, 0.5, 0.5, 1)
|
||||||
roughness = 0.72
|
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"]
|
[sub_resource type="StandardMaterial3D" id="Material_hunger"]
|
||||||
transparency = 1
|
transparency = 1
|
||||||
shading_mode = 0
|
shading_mode = 0
|
||||||
@@ -124,13 +120,6 @@ outer_radius = 0.32
|
|||||||
rings = 14
|
rings = 14
|
||||||
ring_segments = 8
|
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"]
|
[sub_resource type="TorusMesh" id="Mesh_hunger_bowl"]
|
||||||
material = SubResource("Material_hunger")
|
material = SubResource("Material_hunger")
|
||||||
inner_radius = 0.18
|
inner_radius = 0.18
|
||||||
@@ -164,7 +153,9 @@ script = ExtResource("1_animal")
|
|||||||
animal_id = &"goat_dunja"
|
animal_id = &"goat_dunja"
|
||||||
display_name = "Dunja"
|
display_name = "Dunja"
|
||||||
species_id = &"goat"
|
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="."]
|
[node name="Visual" type="Node3D" parent="."]
|
||||||
script = ExtResource("2_visual")
|
script = ExtResource("2_visual")
|
||||||
@@ -276,10 +267,6 @@ rotation_degrees = Vector3(60, 0, 0)
|
|||||||
position = Vector3(0, 0.18, 0)
|
position = Vector3(0, 0.18, 0)
|
||||||
mesh = SubResource("Mesh_tail")
|
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"]
|
[node name="HungerCueRoot" type="Node3D" parent="Visual"]
|
||||||
position = Vector3(0, 2.05, 0.18)
|
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 tail_root: Node3D = $TailRoot
|
||||||
@onready var hunger_cue_root: Node3D = $HungerCueRoot
|
@onready var hunger_cue_root: Node3D = $HungerCueRoot
|
||||||
@onready var fed_response_root: Node3D = $FedResponseRoot
|
@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 idle_phase := 0.0
|
||||||
var is_hungry := false
|
var is_hungry := false
|
||||||
|
var is_moving := false
|
||||||
var fed_tween: Tween
|
var fed_tween: Tween
|
||||||
var head_base_position: Vector3
|
var head_base_position: Vector3
|
||||||
|
var visual_base_position: Vector3
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
head_base_position = head_root.position
|
head_base_position = head_root.position
|
||||||
|
visual_base_position = position
|
||||||
reset_transient_feedback()
|
reset_transient_feedback()
|
||||||
|
|
||||||
|
|
||||||
@@ -23,11 +30,18 @@ func _process(delta: float) -> void:
|
|||||||
idle_phase = fmod(idle_phase + delta, TAU)
|
idle_phase = fmod(idle_phase + delta, TAU)
|
||||||
var breath := sin(idle_phase * 1.7) * 0.025
|
var breath := sin(idle_phase * 1.7) * 0.025
|
||||||
var hungry_drop := -0.12 if is_hungry else 0.0
|
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.position.y = head_base_position.y + breath + hungry_drop
|
||||||
head_root.rotation_degrees.x = (
|
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
|
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)
|
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:
|
if hunger_cue_root.visible:
|
||||||
hunger_cue_root.position.y = HUNGER_CUE_BASE_Y + sin(idle_phase * 2.2) * 0.07
|
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
|
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:
|
func reset_transient_feedback() -> void:
|
||||||
if fed_tween != null:
|
if fed_tween != null:
|
||||||
fed_tween.kill()
|
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="PackedScene" path="res://world/jajce/JajceWorld.tscn" id="1_world"]
|
||||||
[ext_resource type="Script" path="res://world/jajce/beauty_camera.gd" id="2_camera"]
|
[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="JajceWorld" parent="." instance=ExtResource("1_world")]
|
||||||
|
|
||||||
[node name="GoatCamera" type="Camera3D" parent="."]
|
[node name="GoatCamera" type="Camera3D" parent="."]
|
||||||
position = Vector3(0.2, 2.9, -7)
|
position = Vector3(5, 4.5, -25)
|
||||||
current = true
|
current = true
|
||||||
fov = 32.0
|
fov = 38.0
|
||||||
script = ExtResource("2_camera")
|
script = ExtResource("2_camera")
|
||||||
focus_point = Vector3(-4, 1.15, -11.5)
|
focus_point = Vector3(-2, 1.05, -17.5)
|
||||||
animate_orbit = false
|
animate_orbit = false
|
||||||
@@ -363,6 +363,10 @@ func _apply_debug_overlay_visibility() -> void:
|
|||||||
if node.has_method("set_debug_label_enabled"):
|
if node.has_method("set_debug_label_enabled"):
|
||||||
node.set_debug_label_enabled(debug_overlay_visible)
|
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():
|
for node in StorageNode.get_all():
|
||||||
if node.has_method("set_debug_label_enabled"):
|
if node.has_method("set_debug_label_enabled"):
|
||||||
node.set_debug_label_enabled(debug_overlay_visible)
|
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="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
||||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||||
@@ -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="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/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/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"]
|
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||||
_shader_parameters = {
|
_shader_parameters = {
|
||||||
@@ -462,8 +463,13 @@ phase = 4.4
|
|||||||
[node name="Animals" type="Node3D" parent="WorldObjects"]
|
[node name="Animals" type="Node3D" parent="WorldObjects"]
|
||||||
|
|
||||||
[node name="Dunja" parent="WorldObjects/Animals" instance=ExtResource("28_goat")]
|
[node name="Dunja" parent="WorldObjects/Animals" instance=ExtResource("28_goat")]
|
||||||
position = Vector3(-4, 0, -11.5)
|
position = Vector3(0, 0, -15.5)
|
||||||
rotation_degrees = Vector3(0, -18, 0)
|
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"]
|
[node name="ResourceNodes" type="Node3D" parent="WorldObjects"]
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ func _initialize_world_presentation() -> void:
|
|||||||
groups_to_snap.append(child)
|
groups_to_snap.append(child)
|
||||||
if has_node("WorldObjects"):
|
if has_node("WorldObjects"):
|
||||||
var wo := $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):
|
if wo.has_node(group_name):
|
||||||
groups_to_snap.append_array(wo.get_node(group_name).get_children())
|
groups_to_snap.append_array(wo.get_node(group_name).get_children())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user