feat: add paired goat care
This commit is contained in:
@@ -811,15 +811,20 @@ Completed:
|
||||
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.
|
||||
42. `Jajce Goat Pair 09`: Zora now joins Dunja as a second named, stable animal
|
||||
with an authored private shelter and deterministic access to the shared
|
||||
pasture. Simultaneous hunger selects the nearest available loaded goat,
|
||||
preserves two exact reservations through save/restore, and feeds only the
|
||||
named target. Private shelter ownership remains loaded-world context; no
|
||||
herd system, animal index, or schema bump was introduced.
|
||||
|
||||
Next:
|
||||
|
||||
1. Add one second named goat with its own stable state, authored shelter
|
||||
context, and deterministic use of the shared pasture. Prove nearest
|
||||
feed-target selection and independent reservations when both animals are
|
||||
hungry.
|
||||
1. Playtest the named pair and record one concrete gameplay or scaling need
|
||||
before extending the animal model.
|
||||
2. 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 unless that evidence requires one
|
||||
of them; otherwise return to the broader milestone sequence.
|
||||
|
||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||
mechanics. The next proof is a beautiful stage for the systems that already
|
||||
|
||||
@@ -938,16 +938,28 @@ destination, and every NPC RNG stream. Unloading and re-instantiating the goat
|
||||
scene retains that same travel state, arrival is exact, and hunger stops
|
||||
movement before feeding.
|
||||
|
||||
The immediate next slice should add one second named goat with its own stable
|
||||
record and authored shelter context while sharing the existing pasture.
|
||||
Selection and reservation tests must prove that simultaneous hunger still
|
||||
chooses the nearest real animal and mutates only the exact target. Keep
|
||||
loaded-animal discovery linear and defer herd scheduling, breeding, products,
|
||||
animal indexing, and active/abstract simulation LOD until two identities
|
||||
demonstrate a concrete need.
|
||||
The first multi-animal care slice is complete. Zora adds a second stable record,
|
||||
loaded presentation, and private shelter while sharing Dunja's pasture. When
|
||||
both goats are hungry, loaded target resolution chooses the nearest available
|
||||
interaction point, two villagers can retain independent exact claims, and
|
||||
feeding Zora leaves Dunja's hunger, feed history, and reservation untouched.
|
||||
Schema-v11 restore preserves both claim pairings and checksum. Loaded routine
|
||||
sites now distinguish private shelter ownership from shared species context
|
||||
without adding that authored-world fact to saved simulation state.
|
||||
|
||||
The immediate next step is a playtest-and-measure gate, not a herd abstraction.
|
||||
Keep loaded-animal discovery linear and use the named pair to identify one
|
||||
concrete gameplay need before adding breeding, products, herd scheduling,
|
||||
animal indexing, or active/abstract animal LOD. If no such need appears, resume
|
||||
the broader milestone sequence instead of adding animal framework by momentum.
|
||||
|
||||
Recently completed:
|
||||
|
||||
- `Jajce Goat Pair 09`: named goat Zora adds a second independent animal
|
||||
record and private shelter while sharing Dunja's pasture. Stable nearest
|
||||
targeting, simultaneous exact reservations, isolated feed mutation and
|
||||
events, deterministic routine ownership, and active-claim save/restore prove
|
||||
the multi-animal contract without a schema bump or herd system.
|
||||
- `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
|
||||
|
||||
@@ -999,10 +999,14 @@ World-schema v10 restore reconstructs her persistent state without replaying
|
||||
the transient response. Dunja now also alternates between a stable shelter and
|
||||
pasture through real loaded navigation. World-schema v11 preserves exact
|
||||
mid-route position, destination, and schedule through restore or presentation
|
||||
unload/reload without consuming decision RNG. The next bounded animal slice
|
||||
should add one second named goat and prove independent nearest-target care
|
||||
before any herd abstraction. Do not yet generalize animal discovery or
|
||||
introduce active/abstract simulation LOD.
|
||||
unload/reload without consuming decision RNG. Zora now proves the bounded
|
||||
multi-animal contract: her independent stable record and private shelter share
|
||||
Dunja's pasture, nearest loaded care targeting supports two simultaneous exact
|
||||
claims, and feeding one goat cannot mutate the other. World-schema v11
|
||||
preserves both claim pairings without serializing shelter ownership. Do not
|
||||
generalize animal discovery or introduce herd scheduling, products, breeding,
|
||||
or active/abstract animal LOD until playtest or measurement exposes a concrete
|
||||
need.
|
||||
|
||||
The remaining simulation-garden target still aims for:
|
||||
|
||||
|
||||
@@ -37,10 +37,10 @@ both require one stored wood without duplicating that rule.
|
||||
farmer profession and declares an exact one-food completion cost. Selection,
|
||||
animal target reservation, and completion remain separate; NPC and player
|
||||
completion both use `AnimalCareSystem`'s same pantry-to-animal transaction.
|
||||
`SimulationIds` also names Dunja's shelter and pasture anchors. These IDs are
|
||||
saved routine vocabulary; their loaded positions come from
|
||||
`AnimalRoutineSite` rather than entering an action definition or resource
|
||||
registry.
|
||||
`SimulationIds` names Dunja and Zora, their private shelter anchors, and the
|
||||
shared pasture anchor. These IDs are saved routine vocabulary; their loaded
|
||||
positions and private/shared ownership come from `AnimalRoutineSite` rather
|
||||
than entering an action definition or resource registry.
|
||||
|
||||
## Current profession contract
|
||||
|
||||
|
||||
@@ -284,13 +284,23 @@ care operation: reserve or select the exact animal, withdraw one real food
|
||||
from `village_pantry`, reduce that animal's hunger, and append one exact
|
||||
`animal_fed` fact. Failed withdrawal changes neither animal nor history.
|
||||
|
||||
The first routine consumer alternates Dunja between stable-ID shelter and
|
||||
pasture markers without drawing RNG. `ActiveWorldAdapter` supplies the loaded
|
||||
marker positions, the selected ID and exact position enter state, and
|
||||
`AnimalNode` follows a real `NavigationServer3D` path while reporting factual
|
||||
position changes. A mid-route save restores the same position and target;
|
||||
unloading the scene node pauses presentation only, and reloading it resumes
|
||||
from those facts. Hunger cancels travel before care targeting.
|
||||
The first routine consumers alternate Dunja and Zora between stable-ID private
|
||||
shelters and one shared pasture without drawing RNG. `AnimalRoutineSite`
|
||||
declares optional loaded-world resident ownership: an empty resident ID is
|
||||
shared species context, while a named resident makes a shelter private.
|
||||
`ActiveWorldAdapter` filters those loaded markers for each stable animal; site
|
||||
ownership is not persistent state and does not require a schema bump. The
|
||||
selected site ID and exact position do enter state, and each `AnimalNode`
|
||||
follows a real `NavigationServer3D` path while reporting factual position
|
||||
changes. A mid-route save restores the same position and target; unloading a
|
||||
scene node pauses presentation only, and reloading it resumes from those facts.
|
||||
Hunger cancels travel before care targeting.
|
||||
|
||||
Multiple animal records remain independently authoritative. A schema-v11 save
|
||||
may contain concurrent feed reservations only when each reserving NPC targets
|
||||
that exact animal in a valid travel or work state. Restoring Dunja and Zora
|
||||
preserves both claim pairings and the checksum; completing one feed releases
|
||||
and mutates only its destination record.
|
||||
|
||||
The persistent hungry/content presentation derives from authoritative hunger.
|
||||
The short feed response is transient and is reset rather than serialized or
|
||||
|
||||
@@ -217,9 +217,9 @@ func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
|
||||
if npc.task_state not in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]:
|
||||
return
|
||||
var helper := get_active_opportunity_helper()
|
||||
var animal_feed_name := animal_care.get_available_feed_name(npc.id)
|
||||
var animal_feed_available := animal_care.has_available_feed_target(npc.id)
|
||||
var selection := action_selector.select_action(
|
||||
npc, village, clock.time_of_day(), npcs, helper, _population_view, animal_feed_name
|
||||
npc, village, clock.time_of_day(), npcs, helper, _population_view, animal_feed_available
|
||||
)
|
||||
if selection == null:
|
||||
return
|
||||
|
||||
@@ -24,7 +24,7 @@ func select_action(
|
||||
all_npcs: Array = [],
|
||||
opportunity_helper: OpportunityHelperResult = null,
|
||||
population_view: SimulationPopulationView = null,
|
||||
animal_feed_name: String = ""
|
||||
animal_feed_available: bool = false
|
||||
) -> ActionSelectionResult:
|
||||
if npc.is_dead:
|
||||
return null
|
||||
@@ -127,13 +127,13 @@ func select_action(
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_REST, -1.0, "Energy is below the rest threshold"
|
||||
)
|
||||
if not animal_feed_name.is_empty():
|
||||
if animal_feed_available:
|
||||
var feed_definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
|
||||
if _get_cost_rejection(feed_definition, village).is_empty():
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_FEED_ANIMAL,
|
||||
-1.0,
|
||||
"%s is hungry; pantry food is available" % animal_feed_name
|
||||
"A hungry animal is available; pantry food is available"
|
||||
)
|
||||
if village.food <= RELATIONSHIP_AID_PANTRY_THRESHOLD and relationship_system != null:
|
||||
var trusted_starving_npc: SimNPC = relationship_system.get_trusted_starving_subject(
|
||||
|
||||
@@ -119,9 +119,9 @@ func can_complete_feed(animal_id: StringName, agent_id: int) -> bool:
|
||||
)
|
||||
|
||||
|
||||
func get_available_feed_name(npc_id: int) -> String:
|
||||
func has_available_feed_target(npc_id: int) -> bool:
|
||||
if active_world_adapter == null or not active_world_adapter.has_method("get_animal_candidates"):
|
||||
return ""
|
||||
return false
|
||||
var candidates: Array[Dictionary] = active_world_adapter.get_animal_candidates(
|
||||
SimulationIds.ACTION_FEED_ANIMAL
|
||||
)
|
||||
@@ -132,8 +132,8 @@ func get_available_feed_name(npc_id: int) -> String:
|
||||
and animal_state.can_npc_feed()
|
||||
and animal_state.is_available_for(npc_id)
|
||||
):
|
||||
return animal_state.get_display_name()
|
||||
return ""
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func find_node_for_player(from_position: Vector3, max_distance: float) -> AnimalNode:
|
||||
@@ -250,7 +250,7 @@ func _begin_due_routine(animal_state: AnimalStateRecord) -> bool:
|
||||
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()
|
||||
animal_state.get_species_id(), animal_state.get_animal_id()
|
||||
)
|
||||
if candidates.is_empty():
|
||||
return false
|
||||
|
||||
@@ -31,9 +31,11 @@ const RESOURCE_FOOD := &"food"
|
||||
const RESOURCE_WOOD := &"wood"
|
||||
|
||||
const ANIMAL_DUNJA := &"goat_dunja"
|
||||
const ANIMAL_ZORA := &"goat_zora"
|
||||
const SPECIES_GOAT := &"goat"
|
||||
const ANIMAL_SITE_DUNJA_PASTURE := &"dunja_pasture"
|
||||
const ANIMAL_SITE_DUNJA_SHELTER := &"dunja_shelter"
|
||||
const ANIMAL_SITE_ZORA_SHELTER := &"zora_shelter"
|
||||
|
||||
const STORAGE_VILLAGE_PANTRY := &"village_pantry"
|
||||
const STORAGE_VILLAGE_WOODPILE := &"village_woodpile"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
action_id = &"feed_animal"
|
||||
display_name = "Feed Dunja"
|
||||
display_name = "Feed animal"
|
||||
default_duration = 2.0
|
||||
preferred_profession_id = &"farmer"
|
||||
target_type = &"animal"
|
||||
|
||||
@@ -20,10 +20,10 @@ func _run() -> void:
|
||||
var player := main_scene.get_node("Player") as Node3D
|
||||
var pantry: StorageStateRecord = manager.get_pantry()
|
||||
var state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||
_check(goat != null, "Jajce should load one named goat presentation")
|
||||
_check(goat != null, "Jajce should load Dunja's named presentation")
|
||||
_check(
|
||||
AnimalNode.get_all().size() == 1 and ResourceNode.get_all().size() == 18,
|
||||
"The goat should be one AnimalNode without changing the finite-resource count"
|
||||
AnimalNode.get_all().size() == 2 and ResourceNode.get_all().size() == 18,
|
||||
"Named goats should remain AnimalNodes without changing the finite-resource count"
|
||||
)
|
||||
_check(
|
||||
not goat.is_in_group("resource_nodes") and ResourceNode.get_by_id(goat.animal_id) == null,
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
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 dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA)
|
||||
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
|
||||
var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
||||
var shared_pasture := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
|
||||
var dunja_shelter := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_DUNJA_SHELTER)
|
||||
var zora_shelter := AnimalRoutineSite.get_by_id(SimulationIds.ANIMAL_SITE_ZORA_SHELTER)
|
||||
_check(
|
||||
(
|
||||
AnimalNode.get_all().size() == 2
|
||||
and manager.animal_care.states.size() == 2
|
||||
and dunja_node != null
|
||||
and zora_node != null
|
||||
and dunja_state != null
|
||||
and zora_state != null
|
||||
and dunja_node.state == dunja_state
|
||||
and zora_node.state == zora_state
|
||||
),
|
||||
"The real main scene should bind exactly two stable named-goat records",
|
||||
)
|
||||
_check(
|
||||
shared_pasture != null and dunja_shelter != null and zora_shelter != null,
|
||||
"The pair proof needs one shared pasture and one authored shelter per goat",
|
||||
)
|
||||
if (
|
||||
dunja_node == null
|
||||
or zora_node == null
|
||||
or dunja_state == null
|
||||
or zora_state == null
|
||||
or shared_pasture == null
|
||||
or dunja_shelter == null
|
||||
or zora_shelter == null
|
||||
):
|
||||
_finish()
|
||||
return
|
||||
|
||||
for npc in manager.npcs:
|
||||
_park_npc(npc)
|
||||
|
||||
var adapter := manager.active_world_adapter as ActiveWorldAdapter
|
||||
var dunja_sites: Array[Dictionary] = adapter.get_animal_routine_sites(
|
||||
SimulationIds.SPECIES_GOAT, SimulationIds.ANIMAL_DUNJA
|
||||
)
|
||||
var zora_sites: Array[Dictionary] = adapter.get_animal_routine_sites(
|
||||
SimulationIds.SPECIES_GOAT, SimulationIds.ANIMAL_ZORA
|
||||
)
|
||||
_check(
|
||||
(
|
||||
(
|
||||
_site_ids(dunja_sites)
|
||||
== [
|
||||
SimulationIds.ANIMAL_SITE_DUNJA_PASTURE,
|
||||
SimulationIds.ANIMAL_SITE_DUNJA_SHELTER,
|
||||
]
|
||||
)
|
||||
and (
|
||||
_site_ids(zora_sites)
|
||||
== [
|
||||
SimulationIds.ANIMAL_SITE_DUNJA_PASTURE,
|
||||
SimulationIds.ANIMAL_SITE_ZORA_SHELTER,
|
||||
]
|
||||
)
|
||||
),
|
||||
"Routine discovery should expose the shared pasture plus only each goat's own shelter",
|
||||
)
|
||||
|
||||
zora_state.set_hunger(30.0)
|
||||
_check(
|
||||
zora_state.schedule_routine_at(manager.tick_count),
|
||||
"Zora's routine should be schedulable immediately from her authored shelter",
|
||||
)
|
||||
manager.simulate_tick()
|
||||
_check(
|
||||
(
|
||||
zora_state.has_active_travel_target()
|
||||
and zora_state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_ZORA_SHELTER
|
||||
and (zora_state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
|
||||
and (zora_state.get_travel_target_position().is_equal_approx(
|
||||
shared_pasture.get_routine_position()
|
||||
))
|
||||
),
|
||||
"Zora should deterministically choose the shared pasture from her own shelter",
|
||||
)
|
||||
_check(
|
||||
zora_state.complete_travel(shared_pasture.get_routine_position(), manager.tick_count),
|
||||
"The routine proof should be able to complete Zora's pasture leg directly",
|
||||
)
|
||||
_check(
|
||||
zora_state.schedule_routine_at(manager.tick_count),
|
||||
"Zora's return leg should be schedulable without waiting for long navigation",
|
||||
)
|
||||
manager.simulate_tick()
|
||||
_check(
|
||||
(
|
||||
zora_state.has_active_travel_target()
|
||||
and (zora_state.get_routine_site_id() == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE)
|
||||
and (zora_state.get_travel_target_site_id() == SimulationIds.ANIMAL_SITE_ZORA_SHELTER)
|
||||
and (zora_state.get_travel_target_position().is_equal_approx(
|
||||
zora_shelter.get_routine_position()
|
||||
))
|
||||
),
|
||||
"Zora should return to her own shelter rather than entering Dunja's shelter",
|
||||
)
|
||||
zora_state.cancel_travel(manager.tick_count + AnimalCareSystem.ROUTINE_RETRY_TICKS)
|
||||
|
||||
dunja_state.set_hunger(72.0)
|
||||
zora_state.set_hunger(72.0)
|
||||
var zora_caretaker: SimNPC = manager.npcs[0]
|
||||
var dunja_caretaker: SimNPC = manager.npcs[1]
|
||||
_prepare_feed_task(manager, zora_caretaker)
|
||||
_prepare_feed_task(manager, dunja_caretaker)
|
||||
var shared_origin := zora_node.get_interaction_position()
|
||||
zora_caretaker.position = shared_origin
|
||||
dunja_caretaker.position = shared_origin
|
||||
_check(
|
||||
manager.resolve_npc_target(zora_caretaker.id, shared_origin),
|
||||
"The first caretaker should resolve a real hungry animal target",
|
||||
)
|
||||
_check(
|
||||
(
|
||||
zora_caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
||||
and zora_state.get_reserved_by() == zora_caretaker.id
|
||||
and dunja_state.get_reserved_by() == -1
|
||||
),
|
||||
"The first caretaker should claim the nearest hungry goat, Zora, by exact ID",
|
||||
)
|
||||
_check(
|
||||
manager.resolve_npc_target(dunja_caretaker.id, shared_origin),
|
||||
"The second caretaker should resolve the remaining available hungry goat",
|
||||
)
|
||||
_check(
|
||||
(
|
||||
dunja_caretaker.target_id == SimulationIds.ANIMAL_DUNJA
|
||||
and dunja_state.get_reserved_by() == dunja_caretaker.id
|
||||
and zora_state.get_reserved_by() == zora_caretaker.id
|
||||
),
|
||||
"Simultaneous hunger should retain two independent exact animal reservations",
|
||||
)
|
||||
|
||||
var claimed_json: String = manager.serialize_state()
|
||||
var claimed_checksum: String = manager.get_state_checksum()
|
||||
var zora_caretaker_id := zora_caretaker.id
|
||||
var dunja_caretaker_id := dunja_caretaker.id
|
||||
_check(
|
||||
manager.restore_state_from_json(claimed_json),
|
||||
"A save containing both exact feed claims should pass the complete schema",
|
||||
)
|
||||
dunja_state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
||||
zora_caretaker = _find_npc(manager.npcs, zora_caretaker_id)
|
||||
dunja_caretaker = _find_npc(manager.npcs, dunja_caretaker_id)
|
||||
_check(
|
||||
(
|
||||
manager.get_state_checksum() == claimed_checksum
|
||||
and zora_node.state == zora_state
|
||||
and dunja_node.state == dunja_state
|
||||
and zora_caretaker != null
|
||||
and dunja_caretaker != null
|
||||
and zora_caretaker.target_id == SimulationIds.ANIMAL_ZORA
|
||||
and dunja_caretaker.target_id == SimulationIds.ANIMAL_DUNJA
|
||||
and zora_state.get_reserved_by() == zora_caretaker_id
|
||||
and dunja_state.get_reserved_by() == dunja_caretaker_id
|
||||
),
|
||||
"Restore should retain both claims, bindings, target IDs, and the exact checksum",
|
||||
)
|
||||
if zora_caretaker == null or dunja_caretaker == null:
|
||||
_finish()
|
||||
return
|
||||
|
||||
var pantry: StorageStateRecord = manager.get_pantry()
|
||||
var pantry_food_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
var zora_hunger_before := zora_state.get_hunger()
|
||||
var zora_fed_tick_before := zora_state.get_last_fed_tick()
|
||||
var dunja_hunger_before := dunja_state.get_hunger()
|
||||
var dunja_fed_tick_before := dunja_state.get_last_fed_tick()
|
||||
var dunja_reservation_before := dunja_state.get_reserved_by()
|
||||
_check(
|
||||
manager.feed_animal(SimulationIds.ANIMAL_ZORA, zora_caretaker_id),
|
||||
"The restored exact Zora claim should complete one real feed operation",
|
||||
)
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(
|
||||
zora_state.get_hunger(), zora_hunger_before - AnimalStateRecord.HUNGER_RELIEF
|
||||
)
|
||||
and zora_state.get_last_fed_tick() == manager.tick_count
|
||||
and zora_state.get_last_fed_tick() != zora_fed_tick_before
|
||||
and zora_state.get_reserved_by() == -1
|
||||
and is_equal_approx(dunja_state.get_hunger(), dunja_hunger_before)
|
||||
and dunja_state.get_last_fed_tick() == dunja_fed_tick_before
|
||||
and dunja_state.get_reserved_by() == dunja_reservation_before
|
||||
),
|
||||
"Feeding Zora should mutate only Zora's hunger, feed tick, and reservation",
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_food_before - 1.0),
|
||||
"Feeding exactly one goat should consume exactly one pantry food",
|
||||
)
|
||||
var feed_event := _latest_feed_event(manager)
|
||||
_check(
|
||||
(
|
||||
feed_event != null
|
||||
and int(feed_event.data["actor_id"]) == zora_caretaker_id
|
||||
and StringName(feed_event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
and StringName(feed_event.data["destination_id"]) == SimulationIds.ANIMAL_ZORA
|
||||
and StringName(feed_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||
and is_equal_approx(float(feed_event.data["amount"]), 1.0)
|
||||
and feed_event.get_world_position().is_equal_approx(zora_state.get_position())
|
||||
),
|
||||
"The feed event should record the exact caretaker, pantry, Zora destination, and amount",
|
||||
)
|
||||
|
||||
_finish()
|
||||
|
||||
|
||||
func _park_npc(npc: SimNPC) -> void:
|
||||
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
|
||||
npc.start_working()
|
||||
|
||||
|
||||
func _prepare_feed_task(manager: Node, npc: SimNPC) -> void:
|
||||
manager.release_npc_reservation(npc.id)
|
||||
npc.target_id = &""
|
||||
npc.has_travel_target = false
|
||||
npc.set_task(SimulationIds.ACTION_FEED_ANIMAL)
|
||||
|
||||
|
||||
func _site_ids(candidates: Array[Dictionary]) -> Array[StringName]:
|
||||
var ids: Array[StringName] = []
|
||||
for candidate in candidates:
|
||||
ids.append(StringName(candidate["site_id"]))
|
||||
return ids
|
||||
|
||||
|
||||
func _find_npc(npcs: Array[SimNPC], npc_id: int) -> SimNPC:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
return npc
|
||||
return null
|
||||
|
||||
|
||||
func _latest_feed_event(manager: Node) -> EconomicEventRecord:
|
||||
for index in range(manager.economic_events.size() - 1, -1, -1):
|
||||
var event := manager.economic_events[index] as EconomicEventRecord
|
||||
if StringName(event.data["event_type"]) == SimulationIds.EVENT_ANIMAL_FED:
|
||||
return event
|
||||
return null
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("[TEST] Two-goat vertical slice passed")
|
||||
quit(0)
|
||||
return
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://drh502csu7pj6
|
||||
@@ -397,29 +397,39 @@ func _run() -> void:
|
||||
_check(outskirt_context_count >= 4, "Jajce discovery should include outskirts/river contexts")
|
||||
_check(farming_context_count >= 1, "Jajce food discovery should include farming contexts")
|
||||
var animal_root := world.get_node("WorldObjects/Animals")
|
||||
var goat := animal_root.get_node("Dunja") as AnimalNode
|
||||
var dunja := animal_root.get_node("Dunja") as AnimalNode
|
||||
var zora := animal_root.get_node("Zora") as AnimalNode
|
||||
_check(
|
||||
animal_root.get_child_count() == 1 and goat != null,
|
||||
"Jajce should contain one bounded loaded-animal presentation"
|
||||
animal_root.get_child_count() == 2 and dunja != null and zora != null,
|
||||
"Jajce should contain the bounded pair of loaded-animal presentations"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
goat.animal_id == SimulationIds.ANIMAL_DUNJA
|
||||
and goat.species_id == SimulationIds.SPECIES_GOAT
|
||||
and goat.display_name == "Dunja"
|
||||
dunja.animal_id == SimulationIds.ANIMAL_DUNJA
|
||||
and dunja.species_id == SimulationIds.SPECIES_GOAT
|
||||
and dunja.display_name == "Dunja"
|
||||
and zora.animal_id == SimulationIds.ANIMAL_ZORA
|
||||
and zora.species_id == SimulationIds.SPECIES_GOAT
|
||||
and zora.display_name == "Zora"
|
||||
),
|
||||
"The authored goat should preserve its stable identity and readable name"
|
||||
"The authored goats should preserve unique stable identities and readable names"
|
||||
)
|
||||
_check(
|
||||
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"
|
||||
(
|
||||
not dunja.is_in_group("resource_nodes")
|
||||
and not zora.is_in_group("resource_nodes")
|
||||
and ResourceNode.get_by_id(dunja.animal_id) == null
|
||||
and ResourceNode.get_by_id(zora.animal_id) == null
|
||||
),
|
||||
"The identity-backed goats should remain separate from finite resources"
|
||||
)
|
||||
_check(
|
||||
goat.is_in_group("grass_interactors"),
|
||||
"Dunja should bend camera-local grass through the shared presentation group"
|
||||
dunja.is_in_group("grass_interactors") and zora.is_in_group("grass_interactors"),
|
||||
"Both goats 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 dunja_habitat := habitat_root.get_node("DunjaHabitat")
|
||||
var zora_shelter_context := habitat_root.get_node("ZoraShelter")
|
||||
var routine_sites: Array[AnimalRoutineSite] = []
|
||||
for site in AnimalRoutineSite.get_all():
|
||||
routine_sites.append(site)
|
||||
@@ -428,16 +438,25 @@ func _run() -> void:
|
||||
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"
|
||||
(
|
||||
habitat_root.get_child_count() == 2
|
||||
and dunja_habitat != null
|
||||
and zora_shelter_context != null
|
||||
and routine_sites.size() == 3
|
||||
),
|
||||
"Jajce should author one shared pasture and a private shelter context for each goat"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
routine_sites.size() == 2
|
||||
routine_sites.size() == 3
|
||||
and routine_sites[0].site_id == SimulationIds.ANIMAL_SITE_DUNJA_PASTURE
|
||||
and routine_sites[1].site_id == SimulationIds.ANIMAL_SITE_DUNJA_SHELTER
|
||||
and routine_sites[2].site_id == SimulationIds.ANIMAL_SITE_ZORA_SHELTER
|
||||
and routine_sites[0].resident_animal_id.is_empty()
|
||||
and routine_sites[1].resident_animal_id == SimulationIds.ANIMAL_DUNJA
|
||||
and routine_sites[2].resident_animal_id == SimulationIds.ANIMAL_ZORA
|
||||
),
|
||||
"Dunja's pasture and shelter should expose deterministic stable IDs"
|
||||
"Routine sites should expose stable IDs with shared pasture and exact shelter ownership"
|
||||
)
|
||||
_check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites")
|
||||
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||
@@ -491,7 +510,8 @@ func _run() -> void:
|
||||
var destinations: Array[Vector3] = []
|
||||
for resource in resources:
|
||||
destinations.append((resource as ResourceNode).interaction_point.global_position)
|
||||
destinations.append(goat.get_interaction_position())
|
||||
destinations.append(dunja.get_interaction_position())
|
||||
destinations.append(zora.get_interaction_position())
|
||||
for site in routine_sites:
|
||||
destinations.append(site.get_routine_position())
|
||||
destinations.append(pantry.get_interaction_position())
|
||||
|
||||
@@ -81,10 +81,10 @@ func get_animal_candidates(action_id: StringName) -> Array[Dictionary]:
|
||||
return candidates
|
||||
|
||||
|
||||
func get_animal_routine_sites(species_id: StringName) -> Array[Dictionary]:
|
||||
func get_animal_routine_sites(species_id: StringName, animal_id: StringName) -> Array[Dictionary]:
|
||||
var candidates: Array[Dictionary] = []
|
||||
for site in AnimalRoutineSite.get_all():
|
||||
if not site.supports_species(species_id):
|
||||
if not site.supports_animal(animal_id, species_id):
|
||||
continue
|
||||
(
|
||||
candidates
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 resident_animal_id: StringName
|
||||
@export var debug_label_enabled := false
|
||||
|
||||
|
||||
@@ -48,6 +49,13 @@ func supports_species(candidate_species_id: StringName) -> bool:
|
||||
return candidate_species_id == species_id
|
||||
|
||||
|
||||
func supports_animal(candidate_animal_id: StringName, candidate_species_id: StringName) -> bool:
|
||||
return (
|
||||
supports_species(candidate_species_id)
|
||||
and (resident_animal_id.is_empty() or resident_animal_id == candidate_animal_id)
|
||||
)
|
||||
|
||||
|
||||
func get_routine_position() -> Vector3:
|
||||
return global_position
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ script = ExtResource("1_site")
|
||||
site_id = &"dunja_shelter"
|
||||
display_name = "Warm shelter"
|
||||
species_id = &"goat"
|
||||
resident_animal_id = &"goat_dunja"
|
||||
|
||||
[node name="DebugLabel" type="Label3D" parent="ShelterSite"]
|
||||
position = Vector3(0, 2.45, 0)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
[gd_scene load_steps=13 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.39, 0.22, 0.11, 1)
|
||||
roughness = 0.96
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_roof"]
|
||||
albedo_color = Color(0.48, 0.3, 0.2, 1)
|
||||
roughness = 0.98
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_bowl"]
|
||||
albedo_color = Color(0.58, 0.31, 0.19, 1)
|
||||
roughness = 0.82
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_straw"]
|
||||
albedo_color = Color(0.72, 0.54, 0.27, 1)
|
||||
roughness = 0.98
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_glow"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(1, 0.65, 0.28, 0.9)
|
||||
emission_enabled = true
|
||||
emission = Color(1, 0.38, 0.1, 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_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="ZoraShelter" 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 = &"zora_shelter"
|
||||
display_name = "Zora's shelter"
|
||||
species_id = &"goat"
|
||||
resident_animal_id = &"goat_zora"
|
||||
|
||||
[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.72, 0.5, 1)
|
||||
pixel_size = 0.006
|
||||
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=61 format=3]
|
||||
[gd_scene load_steps=62 format=3]
|
||||
|
||||
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||
@@ -29,6 +29,7 @@
|
||||
[ext_resource type="PackedScene" path="res://world/jajce/ForestEdgeResourceCluster.tscn" id="27_forest_cluster"]
|
||||
[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"]
|
||||
[ext_resource type="PackedScene" path="res://world/animals/goat/zora_shelter.tscn" id="30_zora_shelter"]
|
||||
|
||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||
_shader_parameters = {
|
||||
@@ -466,11 +467,24 @@ phase = 4.4
|
||||
position = Vector3(0, 0, -15.5)
|
||||
rotation_degrees = Vector3(0, 150, 0)
|
||||
|
||||
[node name="Zora" parent="WorldObjects/Animals" instance=ExtResource("28_goat")]
|
||||
position = Vector3(-8, 0, -15.5)
|
||||
rotation_degrees = Vector3(0, 210, 0)
|
||||
scale = Vector3(0.92, 0.92, 0.92)
|
||||
animal_id = &"goat_zora"
|
||||
display_name = "Zora"
|
||||
initial_hunger = 42.0
|
||||
initial_routine_site_id = &"zora_shelter"
|
||||
initial_next_routine_tick = 12
|
||||
|
||||
[node name="AnimalHabitats" type="Node3D" parent="WorldObjects"]
|
||||
|
||||
[node name="DunjaHabitat" parent="WorldObjects/AnimalHabitats" instance=ExtResource("29_habitat")]
|
||||
position = Vector3(0, 0, -17)
|
||||
|
||||
[node name="ZoraShelter" parent="WorldObjects/AnimalHabitats" instance=ExtResource("30_zora_shelter")]
|
||||
position = Vector3(-8, 0, -17)
|
||||
|
||||
[node name="ResourceNodes" type="Node3D" parent="WorldObjects"]
|
||||
|
||||
[node name="BerryBush_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||
|
||||
Reference in New Issue
Block a user