feat(wildlife): 17.5 - an animal walks by the world's way where it has one: WildlifeWorld.way (the next corner toward where it is going, asked every half second or on reaching it) steers wandering and going to water or forage, and goal (a spot it can reach) is a wander's target, seeded by the same two draws so the dice do not move; the defaults are the straight line and the old target. way_test: round a wall by its corners, and a wander's target where the world says
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
537d3a0ab3
commit
4add35b4ca
9 changed files with 168 additions and 10 deletions
|
|
@ -46,9 +46,18 @@ export port WildlifeWorld { # every member has a default: a flat dry mead
|
|||
next_id: fn() -> int # an animal's id across machines
|
||||
born: fn(WildAnimal) -> void # draw it
|
||||
placed: fn(WildRange, int) -> void # a range was set out: put its water and forage down
|
||||
way: fn(WildAnimal, float, float) -> bool # the next corner of its way toward (x, z), into way_x / _z
|
||||
goal: fn(WildAnimal, x, z, r, seed) -> bool # a spot about r from (x, z) it can walk to, into way_x / _z
|
||||
}
|
||||
```
|
||||
|
||||
**An animal walks by the world's way where the world has one.** Wandering and going to water or
|
||||
forage face the next corner `way` gives (asked again every half second, or on reaching the corner),
|
||||
and a wander's target is a spot `goal` says it can reach, seeded by the same two draws the old
|
||||
target took - so binding them changes where an animal walks and never the order of the dice. With
|
||||
no answer (the defaults) it walks straight and turns 75 degrees off water and a climb, as before.
|
||||
Maroon Lake binds both to its navmesh, walked by the species' habitat's taste.
|
||||
|
||||
## API
|
||||
|
||||
| | |
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ export property WildAnimal {
|
|||
home_z: float = 0.0
|
||||
tx: float = 0.0
|
||||
tz: float = 0.0
|
||||
wx: float = 0.0 # the corner of its way it is walking to now (WildlifeWorld.way)
|
||||
wz: float = 0.0
|
||||
way_t: float = 0.0 # seconds until it asks the way again; 0 asks at once
|
||||
thirst: float = 0.0 # 0..100: over 60 it goes looking for water
|
||||
hunger: float = 0.0
|
||||
scat_t: float = 0.0 # game minutes until it might leave sign
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function wildlife_tick_ground(wildlife_st: mut WildlifeState, a: WildAnim
|
|||
}
|
||||
return
|
||||
}
|
||||
wl_face(a, a.tx, a.tz, 2.0, dt)
|
||||
wl_head(a, a.tx, a.tz, 2.0, dt)
|
||||
a.speed = s.walk
|
||||
wl_step(wildlife_st, a, dt)
|
||||
if wl_at_target(a, 1.5) or a.timer < 0.0 {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import "ranges.ludic"
|
|||
import "migrate.ludic"
|
||||
import "senses.ludic"
|
||||
import "steer.ludic"
|
||||
import "way.ludic"
|
||||
import "lures.ludic"
|
||||
import "needs.ludic"
|
||||
import "ground.ludic"
|
||||
|
|
|
|||
|
|
@ -46,6 +46,13 @@ export port WildlifeWorld {
|
|||
next_id: fn() -> int = fn wl_next_id
|
||||
born: fn(WildAnimal) -> void = fn wl_nothing # a new animal: draw it
|
||||
placed: fn(WildRange, int) -> void = fn wl_range_nothing # a range was set out
|
||||
# (a, x, z): the next corner of this animal's way toward (x, z) into way_x / _z; false, the
|
||||
# straight line (a navmesh's path by its habitat's taste, in Maroon Lake)
|
||||
way: fn(WildAnimal, float, float) -> bool = fn wl_no_way
|
||||
# (a, x, z, r, seed): a spot about r from (x, z) this animal can walk to, into way_x / _z
|
||||
goal: fn(WildAnimal, float, float, float, int) -> bool = fn wl_no_goal
|
||||
way_x: fn() -> float = fn wl_asked_x
|
||||
way_z: fn() -> float = fn wl_asked_z
|
||||
}
|
||||
function wl_zero0() -> float { return 0.0 }
|
||||
function wl_zero1(p: int) -> float { return 0.0 }
|
||||
|
|
@ -73,6 +80,8 @@ function wl_no_spot(a: WildAnimal, water: bool, s: WildSpot) -> bool { return fa
|
|||
function wl_no_lure(i: int, l: WildLure) -> bool { return false }
|
||||
function wl_nothing(a: WildAnimal) -> void { }
|
||||
function wl_range_nothing(r: WildRange, i: int) -> void { }
|
||||
function wl_no_way(a: WildAnimal, x: float, z: float) -> bool { return false }
|
||||
function wl_no_goal(a: WildAnimal, x: float, z: float, r: float, seed: int) -> bool { return false }
|
||||
function wl_next_id(wildlife_st: mut WildlifeState) -> int {
|
||||
wildlife_st.wl_ids += 1
|
||||
return wildlife_st.wl_ids
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# steer.ludic - turning toward a point at a rate, a step along the facing that turns away from water
|
||||
# and a climb, and a target somewhere about home; a walker leaves a print every 2.8 m
|
||||
# and a climb; a walker leaves a print every 2.8 m (the way there and a target about home: way.ludic)
|
||||
function wl_wrap(a: float) -> float {
|
||||
var r = a
|
||||
while r > 3.1415927 { r = r - WL_TAU }
|
||||
|
|
@ -58,13 +58,6 @@ function wl_push(wildlife_st: mut WildlifeState, a: WildAnimal, s: WildSpecies,
|
|||
return hit
|
||||
}
|
||||
|
||||
function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float) -> void {
|
||||
let ang = wl_rnd(wildlife_st) * WL_TAU
|
||||
let d = wl_rnd(wildlife_st) * radius
|
||||
a.tx = a.home_x + Math.cos(ang) * d
|
||||
a.tz = a.home_z + Math.sin(ang) * d
|
||||
}
|
||||
|
||||
function wl_at_target(a: WildAnimal, r: float) -> bool {
|
||||
let dx = a.tx - a.x
|
||||
let dz = a.tz - a.z
|
||||
|
|
|
|||
106
packages/ludic.wildlife/tests/way_test.ludic
Normal file
106
packages/ludic.wildlife/tests/way_test.ludic
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# way_test.ludic - an animal walks by the world's way where the world has one: round a wall the
|
||||
# straight line runs into (by the corners WildlifeWorld.way gives), and to a wander target the world
|
||||
# says it can reach (goal); with no answer it walks straight, as before
|
||||
import "ludic.wildlife"
|
||||
import "ludic.base"
|
||||
import "fake"
|
||||
program WayTest {
|
||||
numbers float
|
||||
bind WildlifeWorld {
|
||||
ground: fn fk_ground
|
||||
water: fn fk_water
|
||||
forest: fn fk_forest
|
||||
sight: fn fk_sight
|
||||
push: fn fk_push
|
||||
pushed_x: fn fk_pushed_x
|
||||
pushed_z: fn fk_pushed_z
|
||||
players: fn fk_players
|
||||
player_on: fn fk_player_on
|
||||
player_x: fn fk_x
|
||||
player_y: fn fk_y
|
||||
player_z: fn fk_z
|
||||
hidden: fn fk_is_hidden
|
||||
noise: fn fk_noise_of
|
||||
scent: fn fk_scent_of
|
||||
stillness: fn fk_still_of
|
||||
wind: fn fk_wind_of
|
||||
wind_dir: fn fk_wind_to
|
||||
winterness: fn fk_winter_of
|
||||
about: fn fk_about_of
|
||||
per_range: fn fk_one
|
||||
lures: fn fk_count
|
||||
lure: fn fk_lure
|
||||
present: fn fk_present
|
||||
born: fn fk_on_born
|
||||
spot: fn fk_spot
|
||||
way: fn wt_way
|
||||
way_x: fn wt_x
|
||||
way_z: fn wt_z
|
||||
goal: fn wt_goal
|
||||
}
|
||||
|
||||
state WayTestState {
|
||||
wx: float = 0.0
|
||||
wz: float = 0.0
|
||||
asked: int = 0
|
||||
}
|
||||
# a wall across z = -40 for |x| < 30: the way goes round its east end, then on to the target
|
||||
function wt_way(way_test_st: mut WayTestState, a: WildAnimal, x: float, z: float) -> bool {
|
||||
way_test_st.asked += 1
|
||||
way_test_st.wx = x
|
||||
way_test_st.wz = z
|
||||
if a.z > -38.0 and a.x < 31.0 {
|
||||
way_test_st.wx = 32.0
|
||||
way_test_st.wz = -40.0
|
||||
}
|
||||
return true
|
||||
}
|
||||
# a wander target a known step from home, whatever the seed
|
||||
function wt_goal(way_test_st: mut WayTestState, a: WildAnimal, x: float, z: float, r: float, seed: int) -> bool {
|
||||
way_test_st.wx = x + 7.0
|
||||
way_test_st.wz = z + 3.0
|
||||
return true
|
||||
}
|
||||
function wt_x(way_test_st: WayTestState) -> float { return way_test_st.wx }
|
||||
function wt_z(way_test_st: WayTestState) -> float { return way_test_st.wz }
|
||||
|
||||
test "it walks round the wall by the corners it is given, and asks again as it goes" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) {
|
||||
fk_setup(wildlife_st, wildlife_tests_fake_st)
|
||||
let a = wildlife_make(wildlife_st, 0, 0.0, 0.0, false, 0.0, 0, 1.0, 1)
|
||||
wildlife_tests_fake_st.fk_px[0] = 400.0
|
||||
wildlife_tests_fake_st.fk_pz[0] = 0.0
|
||||
wildlife_tests_fake_st.fk_hidden = true
|
||||
a.state = WILD_WANDER
|
||||
a.tx = 0.0
|
||||
a.tz = -100.0
|
||||
a.timer = 300.0
|
||||
var east = -100.0
|
||||
for k in 0 .. 3000 {
|
||||
wildlife_tick(wildlife_st, 0.05, 0.0)
|
||||
if a.z > -42.0 and a.z < -38.0 { east = Math.max(east, a.x) }
|
||||
}
|
||||
expect(east > 29.0)
|
||||
expect(a.z < -60.0)
|
||||
expect(way_test_st.asked > 10)
|
||||
}
|
||||
|
||||
test "a wander target is where the world says it can walk" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) {
|
||||
fk_setup(wildlife_st, wildlife_tests_fake_st)
|
||||
let a = wildlife_make(wildlife_st, 0, 5.0, 5.0, false, 0.0, 0, 1.0, 1)
|
||||
wildlife_tests_fake_st.fk_px[0] = 400.0
|
||||
wildlife_tests_fake_st.fk_pz[0] = 0.0
|
||||
wildlife_tests_fake_st.fk_hidden = true
|
||||
a.hunger = 0.0
|
||||
a.thirst = 0.0
|
||||
a.state = WILD_IDLE
|
||||
a.timer = -1.0
|
||||
var k = 0
|
||||
while a.state != WILD_WANDER and k < 2000 {
|
||||
wildlife_tick(wildlife_st, 0.05, 0.0)
|
||||
k += 1
|
||||
}
|
||||
expect_eq(a.state, WILD_WANDER)
|
||||
expect_near(a.tx, a.home_x + 7.0, 0.001)
|
||||
expect_near(a.tz, a.home_z + 3.0, 0.001)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ function wl_seek(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo
|
|||
a.timer = 2.0
|
||||
return true
|
||||
}
|
||||
wl_face(a, a.spot_x, a.spot_z, 2.0, dt)
|
||||
wl_head(a, a.spot_x, a.spot_z, 2.0, dt)
|
||||
a.speed = wl_sp(wildlife_st, a.sp).walk
|
||||
a.tx = a.spot_x
|
||||
a.tz = a.spot_z
|
||||
|
|
@ -44,6 +44,7 @@ function wl_drink(wildlife_st: WildlifeState, a: WildAnimal, dt: float, gh: floa
|
|||
let water = a.thirst > a.hunger
|
||||
if not WildlifeWorld.spot(a, water, wildlife_st.wl_spot_ask) { return false }
|
||||
a.spot = wildlife_st.wl_spot_ask.uid
|
||||
a.way_t = 0.0
|
||||
a.spot_x = wildlife_st.wl_spot_ask.x
|
||||
a.spot_z = wildlife_st.wl_spot_ask.z
|
||||
a.spot_water = wildlife_st.wl_spot_ask.water
|
||||
|
|
|
|||
36
packages/ludic.wildlife/way.ludic
Normal file
36
packages/ludic.wildlife/way.ludic
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# way.ludic - where an animal walks: by the world's path to where it is going (WildlifeWorld.way, a
|
||||
# navmesh by its habitat's taste in Maroon Lake), to a target about home it can reach (goal), and
|
||||
# straight, as before, where the world has no answer
|
||||
|
||||
# face the way toward (tx, tz): the next corner of the world's path for it, asked again every
|
||||
# WL_WAY_EVERY seconds or on reaching the corner; without a path, the straight line as before
|
||||
const WL_WAY_EVERY: float = 0.5
|
||||
function wl_head(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) -> void {
|
||||
a.way_t = a.way_t - dt
|
||||
let cx = a.wx - a.x
|
||||
let cz = a.wz - a.z
|
||||
if a.way_t <= 0.0 or cx * cx + cz * cz < 1.0 {
|
||||
a.way_t = WL_WAY_EVERY
|
||||
a.wx = tx
|
||||
a.wz = tz
|
||||
if WildlifeWorld.way(a, tx, tz) {
|
||||
a.wx = WildlifeWorld.way_x()
|
||||
a.wz = WildlifeWorld.way_z()
|
||||
}
|
||||
}
|
||||
wl_face(a, a.wx, a.wz, rate, dt)
|
||||
}
|
||||
|
||||
# somewhere about home: a spot the world says it can walk to when it knows, seeded by the same two
|
||||
# draws the old target took, so the dice are the same whether it knows or not
|
||||
function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float) -> void {
|
||||
let ang = wl_rnd(wildlife_st) * WL_TAU
|
||||
let d = wl_rnd(wildlife_st) * radius
|
||||
a.tx = a.home_x + Math.cos(ang) * d
|
||||
a.tz = a.home_z + Math.sin(ang) * d
|
||||
a.way_t = 0.0
|
||||
if WildlifeWorld.goal(a, a.home_x, a.home_z, radius, int(ang * 100000.0) + int(d * 1000.0)) {
|
||||
a.tx = WildlifeWorld.way_x()
|
||||
a.tz = WildlifeWorld.way_z()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue