From b1fff7c9215f807b5918eb3966d06b5927469250 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 23:52:49 +0300 Subject: [PATCH] ludic.wildlife: a climb is judged over half a metre ahead, not one frame's step 17.10 found almost every animal turn-back was for a climb, where the navmesh (40-45 degrees) is stricter than the step's 1.2 per metre. So the frame-sized test was refusing a few centimetres of steep ground: a pebble a walker steps across. It also refused sooner the faster the frame rate. wl_climb takes the ground half a metre ahead along the way the step actually goes. climb_test holds it at 60 and 240 Hz: a pebble is crossed and a wall is not. Both tests fail under the old rule. wildlife 24/24. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.wildlife/steer.ludic | 12 +++- .../ludic.wildlife/tests/climb_test.ludic | 71 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 packages/ludic.wildlife/tests/climb_test.ludic diff --git a/packages/ludic.wildlife/steer.ludic b/packages/ludic.wildlife/steer.ludic index 14a803ec..0d6e5588 100644 --- a/packages/ludic.wildlife/steer.ludic +++ b/packages/ludic.wildlife/steer.ludic @@ -20,6 +20,16 @@ export function wildlife_turned(wildlife_st: WildlifeState) -> int { return wild 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 +const WL_LOOK: float = 0.5 +function wl_climb(a: WildAnimal, nx: float, nz: float) -> float { + let dx = nx - a.x + let dz = nz - a.z + let l = Math.sqrt(dx * dx + dz * dz) + return WildlifeWorld.ground(a.x + dx / l * WL_LOOK, a.z + dz / l * WL_LOOK) - a.y +} + function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool { let d = a.speed * dt if d < 0.0001 { return false } @@ -33,7 +43,7 @@ function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo let nh = WildlifeWorld.ground(nx, nz) let moved = Math.abs(nx - a.x) + Math.abs(nz - a.z) let wet = nh < WildlifeWorld.water(nx, nz) + 0.6 - let steep = nh - a.y > d * 1.2 + let steep = moved > 0.0001 and wl_climb(a, nx, nz) > WL_LOOK * 1.2 if wet or steep or moved < d * 0.2 { if wet { wildlife_st.wl_turned_wet += 1 } else if steep { wildlife_st.wl_turned_steep += 1 } a.yaw = a.yaw + Math.deg_to_rad(75.0) diff --git a/packages/ludic.wildlife/tests/climb_test.ludic b/packages/ludic.wildlife/tests/climb_test.ludic new file mode 100644 index 00000000..c07db6a4 --- /dev/null +++ b/packages/ludic.wildlife/tests/climb_test.ludic @@ -0,0 +1,71 @@ +# climb_test.ludic - a climb is judged over half a metre ahead: a pebble's few centimetres of steep +# ground are walked over, at any frame rate, and a wall is still turned from +import "ludic.wildlife" +import "ludic.base" +import "fake" +program ClimbTest { + numbers float + bind WildlifeWorld { + ground: fn ct_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 + } + + # flat at 10 m, a pebble 4 cm high and 2 cm across at z = -10, a 3 m wall at z = -30 + function ct_ground(x: float, z: float) -> float { + if z < -30.0 { return 13.0 } + if z < -9.99 and z > -10.01 { return 10.04 } + return 10.0 + } + function walk(wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, dt: float) -> float { + 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 south = 0.0 + for k in 0 .. int(40.0 / dt) { + wildlife_tick(wildlife_st, dt, 0.0) + south = Math.min(south, a.z) + } + return south + } + + test "a pebble is walked over and a wall is not, at 60 Hz" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + let s = walk(wildlife_st, wildlife_tests_fake_st, 1.0 / 60.0) + expect(s < -12.0) + expect(s > -30.5) + } + test "and the same at 240 Hz" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + let s = walk(wildlife_st, wildlife_tests_fake_st, 1.0 / 240.0) + expect(s < -12.0) + expect(s > -30.5) + } +}