diff --git a/packages/ludic.wildlife/README.md b/packages/ludic.wildlife/README.md index c9d5b67b..67cf0306 100644 --- a/packages/ludic.wildlife/README.md +++ b/packages/ludic.wildlife/README.md @@ -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 | | | diff --git a/packages/ludic.wildlife/animal.ludic b/packages/ludic.wildlife/animal.ludic index a5f3ef4c..7ebf36dd 100644 --- a/packages/ludic.wildlife/animal.ludic +++ b/packages/ludic.wildlife/animal.ludic @@ -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 diff --git a/packages/ludic.wildlife/ground.ludic b/packages/ludic.wildlife/ground.ludic index ae9339c0..26313853 100644 --- a/packages/ludic.wildlife/ground.ludic +++ b/packages/ludic.wildlife/ground.ludic @@ -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 { diff --git a/packages/ludic.wildlife/index.ludic b/packages/ludic.wildlife/index.ludic index 832aa192..f768c96b 100644 --- a/packages/ludic.wildlife/index.ludic +++ b/packages/ludic.wildlife/index.ludic @@ -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" diff --git a/packages/ludic.wildlife/port.ludic b/packages/ludic.wildlife/port.ludic index 5fb77d1c..7c599b5c 100644 --- a/packages/ludic.wildlife/port.ludic +++ b/packages/ludic.wildlife/port.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 diff --git a/packages/ludic.wildlife/steer.ludic b/packages/ludic.wildlife/steer.ludic index b561d5cc..4e73371e 100644 --- a/packages/ludic.wildlife/steer.ludic +++ b/packages/ludic.wildlife/steer.ludic @@ -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 diff --git a/packages/ludic.wildlife/tests/way_test.ludic b/packages/ludic.wildlife/tests/way_test.ludic new file mode 100644 index 00000000..b9624030 --- /dev/null +++ b/packages/ludic.wildlife/tests/way_test.ludic @@ -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) + } +} diff --git a/packages/ludic.wildlife/want.ludic b/packages/ludic.wildlife/want.ludic index 696d6005..ebe42f43 100644 --- a/packages/ludic.wildlife/want.ludic +++ b/packages/ludic.wildlife/want.ludic @@ -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 diff --git a/packages/ludic.wildlife/way.ludic b/packages/ludic.wildlife/way.ludic new file mode 100644 index 00000000..ddc00e31 --- /dev/null +++ b/packages/ludic.wildlife/way.ludic @@ -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() + } +}