From 81b6782c9d50c6b91ea545dd11387731fc74defd Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 00:27:12 +0300 Subject: [PATCH] ludic.wildlife (18.2): a walker a crowd moves wildlife_set_crowd(a, true) hands a walker to a crowd. Its step no longer moves it, and its want is two fields on it: goal_x / goal_z (the point it heads for or faces) and goal_speed (0 when it stands). A route reads those and hands back where the crowd put it through wildlife_moved(a, x, z, vx, vz). That sets its place, its ground, its heading from its velocity and its prints, and updates its row's columns so the spatial index finds it there. Nothing allocates per animal per frame: the want is the animal's own fields, and moved writes in place. The prints and the turn counters moved to walked.ludic (steer.ludic was at the 100-line limit). crowd_test holds it; with crowd off every old test is unchanged. wildlife 26/26. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.wildlife/animal.ludic | 4 + packages/ludic.wildlife/ground.ludic | 1 + packages/ludic.wildlife/index.ludic | 1 + packages/ludic.wildlife/steer.ludic | 14 +--- .../ludic.wildlife/tests/crowd_test.ludic | 83 +++++++++++++++++++ packages/ludic.wildlife/walked.ludic | 42 ++++++++++ packages/ludic.wildlife/way.ludic | 5 ++ 7 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 packages/ludic.wildlife/tests/crowd_test.ludic create mode 100644 packages/ludic.wildlife/walked.ludic diff --git a/packages/ludic.wildlife/animal.ludic b/packages/ludic.wildlife/animal.ludic index 56e92221..15ae0444 100644 --- a/packages/ludic.wildlife/animal.ludic +++ b/packages/ludic.wildlife/animal.ludic @@ -44,6 +44,10 @@ export property WildAnimal { 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 + crowd: bool = false # walked by the world's crowd (phase 18): it decides, the crowd moves it + goal_x: float = 0.0 # where it walks this tick and how fast: what a crowd is handed + goal_z: float = 0.0 + goal_speed: float = 0.0 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 26313853..05b8bac9 100644 --- a/packages/ludic.wildlife/ground.ludic +++ b/packages/ludic.wildlife/ground.ludic @@ -1,6 +1,7 @@ # ground.ludic - a walker's tick: its needs, the alert, then the first of charging, reacting, going # to its spot, walking to something set out, eating, idling and wandering that has it this tick export function wildlife_tick_ground(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void { + a.goal_speed = 0.0 wl_needs(wildlife_st, a, gh) let dh = wl_near(wildlife_st, a.x, a.z) let np = wildlife_st.wl_near_i diff --git a/packages/ludic.wildlife/index.ludic b/packages/ludic.wildlife/index.ludic index 37f48c0a..6ce99a14 100644 --- a/packages/ludic.wildlife/index.ludic +++ b/packages/ludic.wildlife/index.ludic @@ -28,3 +28,4 @@ import "birds.ludic" import "run.ludic" import "spawn.ludic" import "system.ludic" +import "walked.ludic" diff --git a/packages/ludic.wildlife/steer.ludic b/packages/ludic.wildlife/steer.ludic index 0d6e5588..9341c0e4 100644 --- a/packages/ludic.wildlife/steer.ludic +++ b/packages/ludic.wildlife/steer.ludic @@ -11,14 +11,11 @@ function wl_face(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) -> let want = Math.atan2(-(tx - a.x), -(tz - a.z)) let r = rate * dt a.yaw = a.yaw + Math.clamp(wl_wrap(want - a.yaw), -r, r) + a.goal_x = tx + a.goal_z = tz } # true when it moved; a walker is slid round a still thing (a trunk, a tent), a bird is not -# how many steps were refused and turned from since the start: how often the way was not enough -export function wildlife_turned(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned } -# of those, how many for water ahead and how many for a climb (the rest: pushed back by a still thing) -export function wildlife_turned_wet(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned_wet } -export function wildlife_turned_steep(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned_steep } # a climb is judged over half a metre ahead, not one frame's step: a few centimetres of the drawn # ground rise steeply over a pebble a walker steps across, and a frame's step shrinks with the rate @@ -31,6 +28,7 @@ function wl_climb(a: WildAnimal, nx: float, nz: float) -> float { } function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool { + if a.crowd { return wl_crowd_step(a) } let d = a.speed * dt if d < 0.0001 { return false } var nx = a.x - Math.sin(a.yaw) * d @@ -54,11 +52,7 @@ function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo a.x = nx a.z = nz a.y = nh - a.track_d = a.track_d + d - if s.tracks and a.track_d > 2.8 { - a.track_d = 0.0 - wl_fact(wildlife_st, WILD_PRINT, a) - } + wl_walked(wildlife_st, a, s, d) return true } diff --git a/packages/ludic.wildlife/tests/crowd_test.ludic b/packages/ludic.wildlife/tests/crowd_test.ludic new file mode 100644 index 00000000..ba9396a4 --- /dev/null +++ b/packages/ludic.wildlife/tests/crowd_test.ludic @@ -0,0 +1,83 @@ +# crowd_test.ludic - a walker a crowd moves (phase 18): it says where it is going and how fast and +# stays where it is, standing still is a speed of 0, and where the crowd puts it is its place, +# its heading and its prints, found there by the table's index +import "ludic.wildlife" +import "ludic.base" +import "fake" +program WildCrowdTest { + 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 + } + + + function walker(wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) -> WildAnimal { + 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 + wildlife_set_crowd(a, true) + return a + } + + test "a crowd walker says where and how fast, and does not move itself" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + let a = walker(wildlife_st, wildlife_tests_fake_st) + a.state = WILD_WANDER + a.tx = 0.0 + a.tz = -100.0 + a.timer = 300.0 + for k in 0 .. 120 { wildlife_tick(wildlife_st, 1.0 / 60.0, 0.0) } + expect_eq(a.x, 0.0) + expect_eq(a.z, 0.0) + expect_eq(a.goal_x, 0.0) + expect_eq(a.goal_z, -100.0) + expect(a.goal_speed > 0.5) + a.state = WILD_IDLE + a.timer = 300.0 + wildlife_tick(wildlife_st, 1.0 / 60.0, 0.0) + expect_eq(a.goal_speed, 0.0) + } + + test "where the crowd puts it is its place, its heading, its prints, and the index finds it there" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + let a = walker(wildlife_st, wildlife_tests_fake_st) + q_clear(wildlife_facts(wildlife_st)) + for k in 1 .. 41 { wildlife_moved(wildlife_st, a, 0.0, -0.1 * float(k), 0.0, -1.2) } + expect_near(a.z, -4.0, 0.0001) + expect_near(a.y, 10.0, 0.0001) + expect_near(Math.sin(a.yaw), 0.0, 0.0001) + expect(Math.cos(a.yaw) > 0.99) + var prints = 0 + let fs = q_drain(wildlife_facts(wildlife_st)) + for i in 0 .. len(fs) { if fs[i].what == WILD_PRINT { prints += 1 } } + expect_eq(prints, 1) + let out = new []WildAnimal + expect_eq(wildlife_near(wildlife_st, 0.0, -4.0, 1.0, out), 1) + expect_eq(wildlife_near(wildlife_st, 0.0, 0.0, 1.0, out), 0) + } +} diff --git a/packages/ludic.wildlife/walked.ludic b/packages/ludic.wildlife/walked.ludic new file mode 100644 index 00000000..58c8e2b6 --- /dev/null +++ b/packages/ludic.wildlife/walked.ludic @@ -0,0 +1,42 @@ +# walked.ludic - what a step leaves behind (a print every 2.8 m), how often a step was refused, and +# the crowd's side (phase 18): a crowd walker only says where and how fast, and is handed back where +# the crowd put it. Nothing here allocates: it runs for every walker every frame. + +# how many steps were refused and turned from since the start: how often the way was not enough +export function wildlife_turned(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned } +# of those, how many for water ahead and how many for a climb (the rest: pushed back by a still thing) +export function wildlife_turned_wet(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned_wet } +export function wildlife_turned_steep(wildlife_st: WildlifeState) -> int { return wildlife_st.wl_turned_steep } + +# d metres covered: a print when it has gone far enough since the last +function wl_walked(wildlife_st: mut WildlifeState, a: WildAnimal, s: WildSpecies, d: float) -> void { + a.track_d = a.track_d + d + if s.tracks and a.track_d > 2.8 { + a.track_d = 0.0 + wl_fact(wildlife_st, WILD_PRINT, a) + } +} + +# a crowd walker's step: its want is where it faces or heads (goal_x / _z) at this tick's speed +function wl_crowd_step(a: WildAnimal) -> bool { + a.goal_speed = a.speed + return true +} + +# walked by a crowd, or by its own step again (off, it keeps where the crowd left it) +export function wildlife_set_crowd(a: WildAnimal, on: bool) -> void { a.crowd = on } + +# where the crowd put a walker, and how fast it went: its place, its heading from its velocity, its +# ground from the world's, and its prints as it goes +export function wildlife_moved(wildlife_st: mut WildlifeState, a: WildAnimal, x: float, z: float, vx: float, vz: float) -> void { + let dx = x - a.x + let dz = z - a.z + let d = Math.sqrt(dx * dx + dz * dz) + a.x = x + a.z = z + a.y = WildlifeWorld.ground(x, z) + if vx * vx + vz * vz > 0.01 { a.yaw = Math.atan2(-vx, -vz) } + let r = wl_row(a) + if r >= 0 { tb_set_xz(a.tab, WA_X, WA_Z, r, x, z) } + wl_walked(wildlife_st, a, wl_sp(wildlife_st, a.sp), d) +} diff --git a/packages/ludic.wildlife/way.ludic b/packages/ludic.wildlife/way.ludic index 6cb5f9e9..da4dfe94 100644 --- a/packages/ludic.wildlife/way.ludic +++ b/packages/ludic.wildlife/way.ludic @@ -6,6 +6,11 @@ # 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 { + if a.crowd { + a.goal_x = tx + a.goal_z = tz + return + } a.way_t = a.way_t - dt let cx = a.wx - a.x let cz = a.wz - a.z