From 56e4b6d688c31d61a79fc0448b92411058cab156 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 00:59:39 +0300 Subject: [PATCH] feat(senses): walkers slide round still things, and lines of sight are the world's to answer - ludic.wildlife: WildlifeWorld.push / pushed_x / pushed_z (defaults change nothing); a ground walker's step is slid clear of a still thing with a body from its species' size and scale, and a step that gets nowhere turns away as the water does. Birds are not pushed; no dice added. - ludic.npc: NpcWorld.push / pushed_x / pushed_z (a person 0.35 m round) in npc_walk, and NpcWorld.clear (default true), which npc_line_ok asks at 1 m so a detour does not cut a trunk. - ludic.aim: AimView.clear replaces AimView.ground and the ground march; the pick asks the line a body's width short of where the ray enters the thing (aim_clear_at(t, back)), so a solid thing does not hide itself. probe.ludic (the ground-agreement probe) and AimView.dry are gone. - ludic.photo: PhotoLens.clear replaces PhotoLens.ground, PhotoLens.trunk and the march. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.aim/README.md | 15 ++--- packages/ludic.aim/index.ludic | 3 +- packages/ludic.aim/pick.ludic | 7 +-- packages/ludic.aim/ports.ludic | 10 ++-- packages/ludic.aim/probe.ludic | 58 ------------------- packages/ludic.aim/ray.ludic | 23 ++++---- packages/ludic.aim/tests/aim_test.ludic | 43 ++++++++------ packages/ludic.npc/README.md | 2 + packages/ludic.npc/ground.ludic | 3 +- packages/ludic.npc/person.ludic | 2 + packages/ludic.npc/port.ludic | 9 +++ packages/ludic.npc/tests/fake/index.ludic | 23 ++++++++ packages/ludic.npc/tests/npc_test.ludic | 19 ++++++ packages/ludic.npc/walk.ludic | 26 +++++++-- packages/ludic.photo/README.md | 4 +- packages/ludic.photo/ports.ludic | 10 ++-- packages/ludic.photo/sight.ludic | 26 +-------- packages/ludic.photo/tests/photo_test.ludic | 16 ++++- packages/ludic.wildlife/README.md | 1 + packages/ludic.wildlife/port.ludic | 7 +++ packages/ludic.wildlife/species.ludic | 4 ++ packages/ludic.wildlife/steer.ludic | 32 ++++++++-- .../ludic.wildlife/tests/fake/index.ludic | 17 ++++++ .../ludic.wildlife/tests/wildlife_test.ludic | 22 +++++++ 24 files changed, 230 insertions(+), 152 deletions(-) delete mode 100644 packages/ludic.aim/probe.ludic diff --git a/packages/ludic.aim/README.md b/packages/ludic.aim/README.md index 685c2d55..b3f711b5 100644 --- a/packages/ludic.aim/README.md +++ b/packages/ludic.aim/README.md @@ -3,7 +3,9 @@ What the crosshair is on, and which thing the use key means. The crosshair is the exact middle of the screen and the projection is symmetric, so the ray through it is the camera's position along its forward - no unprojection. Each candidate is an upright cylinder widened by a fixed angular -forgiveness, and the pick is the nearest one the ray enters that the ground does not hide. Uses +forgiveness, and the pick is the nearest one the ray enters that nothing hides: the line up to +where the ray enters it is asked of the game (`clear`), so a thing that is itself solid does not +hide itself. Uses [`ludic.base`](../ludic.base/README.md) and nothing else. ```ludic @@ -32,13 +34,12 @@ that asks "am I close enough to this" asks `aim_reach_of`. ```ludic export port AimView { x, y, z, fx, fy, fz, fov, # the camera (the ray) - ground: fn(float, float) -> float, - dry: fn(float, float) -> bool, # land, for the probe's viewpoints + clear: fn(ax, ay, az, bx, by, bz) -> bool, # nothing between: the ground and every still thing body_x, body_z # distances and reach are the body's, not the camera's } ``` -Every member has a default: a camera 1.5 m up at the origin looking down -z over flat ground. +Every member has a default: a camera 1.5 m up at the origin looking down -z at nothing in the way. ## API @@ -47,8 +48,7 @@ Every member has a default: a camera 1.5 m up at the origin looking down -z over | `aim_begin()`, `aim_offer(kind, id, x, y0, y1, z, r, reach, far) -> bool` | a frame's candidates: true when this one is now the pick (`reach` `AIM_NEVER` is never in reach) | | `aim_kind()` (`AIM_NONE` for nothing), `aim_id()`, `aim_dist()`, `aim_in_reach()`, `aim_t()`, `aim_off_centre()` | the pick | | `aim_use(kind)` -> `AIM_USE_THIS` / `_NOTHING` / `_NEAREST`, `aim_reach_of(reach)` | the use key | -| `aim_ray_cyl(cx, y0, y1, cz, r)`, `aim_slack_m(dist)`, `aim_clear_to(x, y, z)`, `aim_range(x, z)`, `aim_miss(cx, cz, r)` | the geometry, for a game's own probes | -| `aim_probe_ground(px, pz, look) -> bool`, `aim_probe_seen()`, `aim_probe_blocked()`, `aim_probe_disagree()` | from a point and a ring round it, lines at 60, 120 and 200 m on 36 bearings: the ground's own answer against `aim_clear_to`; `look(x, y, z)` puts the game's camera there | +| `aim_ray_cyl(cx, y0, y1, cz, r)`, `aim_slack_m(dist)`, `aim_clear_to(x, y, z)`, `aim_clear_at(t, back)`, `aim_range(x, z)`, `aim_miss(cx, cz, r)` | the geometry, for a game's own probes (`aim_clear_at`: the ray clear to `back` short of t along it) | ## Tests @@ -57,4 +57,5 @@ ludic test packages/ludic.aim ``` A camera over flat ground with a ridge: the ray against a cylinder, the forgiveness, near before -far, the ground hiding a thing, the reach floor and the three answers for the use key, and the probe. +far, the ridge hiding a thing and a solid thing not hiding itself, the reach floor and the three +answers for the use key. diff --git a/packages/ludic.aim/index.ludic b/packages/ludic.aim/index.ludic index 2a2a9ceb..8c1a9a0c 100644 --- a/packages/ludic.aim/index.ludic +++ b/packages/ludic.aim/index.ludic @@ -1,6 +1,6 @@ # ludic.aim - the crosshair is the exact middle of the screen, so the ray through it is the camera's # position along its forward. Each candidate is an upright cylinder widened by a fixed angular -# forgiveness; the pick is the nearest one the ray enters that the ground does not hide. +# forgiveness; the pick is the nearest one the ray enters that nothing hides. module ludic_aim uses ludic_base numbers float import "ludic.base" @@ -8,4 +8,3 @@ import "ports.ludic" import "ray.ludic" import "pick.ludic" import "use.ludic" -import "probe.ludic" diff --git a/packages/ludic.aim/pick.ludic b/packages/ludic.aim/pick.ludic index 4bd61251..f7c7f9bb 100644 --- a/packages/ludic.aim/pick.ludic +++ b/packages/ludic.aim/pick.ludic @@ -1,5 +1,5 @@ # pick.ludic - one target a frame: the game offers every candidate, and the nearest one the ray enters -# that the ground does not hide is the pick. Near things occlude far ones for free, which is what a +# that nothing hides is the pick. Near things occlude far ones for free, which is what a # player means by "I am looking at that one". export const AIM_NONE: int = 0 export const AIM_REACH_MIN: float = 3.0 # about as far as a hand goes with a step into it @@ -13,9 +13,6 @@ export state AimState { am_dist: float = 0.0 am_reach: bool = false am_off: float = 1.0 - am_seen: int = 0 - am_blocked: int = 0 - am_disagree: int = 0 } # The crosshair decides WHICH; the distance decides only WHETHER. Every reach is lifted to a floor of @@ -41,7 +38,7 @@ export function aim_offer(aim_st: mut AimState, kind: int, id: int, x: float, y0 let rr = r + aim_slack_m(d) let hit = aim_ray_cyl(x, y0, y1, z, rr) if hit < 0.0 or not (hit < aim_st.am_t) { return false } - if not aim_clear_to(x, (y0 + y1) * 0.5, z) { return false } + if not aim_clear_at(hit, rr) { return false } aim_st.am_t = hit aim_st.am_off = aim_miss(x, z, rr) aim_st.am_kind = kind diff --git a/packages/ludic.aim/ports.ludic b/packages/ludic.aim/ports.ludic index 5e071f43..0e88cdae 100644 --- a/packages/ludic.aim/ports.ludic +++ b/packages/ludic.aim/ports.ludic @@ -1,5 +1,5 @@ -# ports.ludic - what the aim asks the game: the camera, the ground, and where the body stands. -# Unbound, a camera 1.5 m up at the origin looks down -z over flat ground, with the body under it. +# ports.ludic - what the aim asks the game: the camera, whether a line is clear, and where the body +# stands. Unbound, a camera 1.5 m up at the origin looks down -z at nothing in the way, over the body. export port AimView { x: fn() -> float = fn aim__zero y: fn() -> float = fn aim__eye @@ -8,8 +8,7 @@ export port AimView { fy: fn() -> float = fn aim__zero fz: fn() -> float = fn aim__ahead fov: fn() -> float = fn aim__fov # vertical, radians - ground: fn(float, float) -> float = fn aim__flat - dry: fn(float, float) -> bool = fn aim__dry # land, for the probe's viewpoints + clear: fn(float, float, float, float, float, float) -> bool = fn aim__clear # nothing between two points body_x: fn() -> float = fn aim__zero # distances and reach are the body's, not the camera's body_z: fn() -> float = fn aim__zero } @@ -18,5 +17,4 @@ function aim__zero() -> float { return 0.0 } function aim__eye() -> float { return 1.5 } function aim__ahead() -> float { return -1.0 } function aim__fov() -> float { return 1.0 } -function aim__flat(x: float, z: float) -> float { return 0.0 } -function aim__dry(x: float, z: float) -> bool { return true } +function aim__clear(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return true } diff --git a/packages/ludic.aim/probe.ludic b/packages/ludic.aim/probe.ludic deleted file mode 100644 index ed8e68c7..00000000 --- a/packages/ludic.aim/probe.ludic +++ /dev/null @@ -1,58 +0,0 @@ -# probe.ludic - does the aim agree with the ground about what it hides? From (px, pz) and a ring of -# eight points 250 m round it, at 60, 120 and 200 m on 36 bearings, the answer worked out by walking -# the ground under each line against aim_clear_to. Lines the ground merely grazes are left out: that -# is the probe's sampling against the aim's, not a disagreement. Moves the camera (AimView's) through -# `look`, which the game binds to put its camera at a point. - -export function aim_probe_seen(aim_st: AimState) -> int { return aim_st.am_seen } -export function aim_probe_blocked(aim_st: AimState) -> int { return aim_st.am_blocked } -export function aim_probe_disagree(aim_st: AimState) -> int { return aim_st.am_disagree } - -# true when at least one line is hidden and the aim disagrees with the ground on at most one in fifty -export function aim_probe_ground(aim_st: mut AimState, px: float, pz: float, look: fn(float, float, float) -> void) -> bool { - aim_st.am_seen = 0 - aim_st.am_blocked = 0 - aim_st.am_disagree = 0 - for sp in 0 .. 9 { - var sx = px - var sz = pz - if sp > 0 { - let sa = float(sp - 1) / 8.0 * 6.2831853 - sx = px + Math.sin(sa) * 250.0 - sz = pz + Math.cos(sa) * 250.0 - } - if AimView.dry(sx, sz) { am_probe_from(aim_st, sx, AimView.ground(sx, sz) + 1.6, sz, look) } - } - return aim_st.am_blocked >= 1 and not (aim_st.am_disagree > aim_st.am_seen / 50) -} - -function am_probe_from(aim_st: mut AimState, sx: float, sy: float, sz: float, look: fn(float, float, float) -> void) -> void { - for di in 0 .. 3 { - var dist = 60.0 - if di == 1 { dist = 120.0 } - if di == 2 { dist = 200.0 } - for b in 0 .. 36 { - let bearing = float(b) / 36.0 * 6.2831853 - let ex = sx + Math.sin(bearing) * dist - let ez = sz + Math.cos(bearing) * dist - let ey = AimView.ground(ex, ez) + 0.4 - let truth = am_probe_gap(sx, sy, sz, ex, ey, ez, 0.5, 10000.0) - if am_probe_gap(sx, sy, sz, ex, ey, ez, -0.5, 0.5) and not truth { continue } - look(sx, sy, sz) - aim_st.am_seen += 1 - let clear = aim_clear_to(ex, ey, ez) - if not clear { aim_st.am_blocked += 1 } - if clear == truth { aim_st.am_disagree += 1 } - } - } -} - -# somewhere between the ends (excluded), the ground stands between lo and hi above the line -function am_probe_gap(sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, lo: float, hi: float) -> bool { - for k in 2 .. 38 { - let t = float(k) / 40.0 - let gap = AimView.ground(Math.lerp(sx, ex, t), Math.lerp(sz, ez, t)) - Math.lerp(sy, ey, t) - if gap > lo and gap < hi { return true } - } - return false -} diff --git a/packages/ludic.aim/ray.ludic b/packages/ludic.aim/ray.ludic index 102aaa73..20c7e320 100644 --- a/packages/ludic.aim/ray.ludic +++ b/packages/ludic.aim/ray.ludic @@ -1,5 +1,5 @@ # ray.ludic - the ray through the crosshair against an upright cylinder, the forgiveness around a -# body, and the ground in front of a point +# body, and whether anything stands in front of a point # t along the ray where it enters the cylinder at (cx, cz), radius r, from y0 up to y1; or -1 export function aim_ray_cyl(cx: float, y0: float, y1: float, cz: float, r: float) -> float { let ox = AimView.x() - cx @@ -42,18 +42,15 @@ export function aim_range(x: float, z: float) -> float { return Math.sqrt(dx * dx + dz * dz) } -# Is the line from the camera to a point clear of the ground? Without it the crosshair reads through a -# rise; within arm's length nothing is hidden, or a prop on a slope hides behind its own slope. -export function aim_clear_to(x: float, y: float, z: float) -> bool { - let cx = AimView.x() - let cy = AimView.y() - let cz = AimView.z() - if (x - cx) * (x - cx) + (z - cz) * (z - cz) < 9.0 { return true } - for s in 1 .. 10 { - let f = float(s) / 10.0 - if AimView.ground(Math.lerp(cx, x, f), Math.lerp(cz, z, f)) - 0.35 > Math.lerp(cy, y, f) { return false } - } - return true +# is the line from the camera to a point clear? (the ground and every still thing: the port's) +export function aim_clear_to(x: float, y: float, z: float) -> bool { return AimView.clear(AimView.x(), AimView.y(), AimView.z(), x, y, z) } + +# is the ray clear up to `back` short of t, where it enters a thing? Asked a body's width short, so +# a solid thing (a trunk, a tent) whose collider is wider than its aim does not hide itself +export function aim_clear_at(t: float, back: float) -> bool { + let u = t - Math.max(back, 0.05) + if u < 0.1 { return true } + return aim_clear_to(AimView.x() + AimView.fx() * u, AimView.y() + AimView.fy() * u, AimView.z() + AimView.fz() * u) } # how far the ray passes from a body's axis as a share of its width: 0 dead centre, 1 at the edge diff --git a/packages/ludic.aim/tests/aim_test.ludic b/packages/ludic.aim/tests/aim_test.ludic index 0c60c982..714bfe6c 100644 --- a/packages/ludic.aim/tests/aim_test.ludic +++ b/packages/ludic.aim/tests/aim_test.ludic @@ -1,6 +1,6 @@ # aim_test.ludic - a camera 1.5 m up looking down -z over flat ground with a ridge 42 m out: the ray -# against a cylinder, the forgiveness, near before far, the ground hiding a thing, the reach floor, -# which thing the use key means, and the probe agreeing with the ground +# against a cylinder, the forgiveness, near before far, the ridge hiding a thing, the reach floor, +# and which thing the use key means import "ludic.aim" import "ludic.base" program AimTest { @@ -11,6 +11,7 @@ program AimTest { cy: float = 1.5 cz: float = 0.0 fy: float = 0.0 + post: bool = false } function fk_x(aim_test_st: AimTestState) -> float { return aim_test_st.cx } function fk_y(aim_test_st: AimTestState) -> float { return aim_test_st.cy } @@ -19,18 +20,29 @@ program AimTest { function fk_fz(aim_test_st: AimTestState) -> float { return -Math.sqrt(1.0 - aim_test_st.fy * aim_test_st.fy) } # a 6 m ridge along z = -42, thirty metres across, the whole width of the view function fk_ground(x: float, z: float) -> float { return 6.0 * Math.max(0.0, 1.0 - Math.abs(z + 42.0) / 15.0) } - function fk_look(aim_test_st: mut AimTestState, x: float, y: float, z: float) -> void { - aim_test_st.cx = x - aim_test_st.cy = y - aim_test_st.cz = z + # the ground under ten points of the line, with 35 cm of slack, and (when it stands) a solid post + # 0.75 m round at (0, -20), wider than its aim: what a physics ray would say + function fk_clear(aim_test_st: AimTestState, ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { + let lx = bx - ax + let lz = bz - az + let t = Math.clamp(((0.0 - ax) * lx + (-20.0 - az) * lz) / Math.max(lx * lx + lz * lz, 0.0001), 0.0, 1.0) + let px = ax + lx * t + let pz = az + lz * t + if aim_test_st.post and px * px + (pz + 20.0) * (pz + 20.0) < 0.5625 { return false } + for s in 1 .. 10 { + let f = float(s) / 10.0 + if fk_ground(Math.lerp(ax, bx, f), Math.lerp(az, bz, f)) - 0.35 > Math.lerp(ay, by, f) { return false } + } + return true } - bind AimView { x: fn fk_x, y: fn fk_y, z: fn fk_z, fy: fn fk_fy, fz: fn fk_fz, ground: fn fk_ground } + bind AimView { x: fn fk_x, y: fn fk_y, z: fn fk_z, fy: fn fk_fy, fz: fn fk_fz, clear: fn fk_clear } function fresh(aim_st: mut AimState, aim_test_st: mut AimTestState) -> void { aim_test_st.cx = 0.0 aim_test_st.cy = 1.5 aim_test_st.cz = 0.0 aim_test_st.fy = 0.0 + aim_test_st.post = false aim_begin(aim_st) } @@ -63,13 +75,18 @@ program AimTest { expect(not aim_offer(aim_st, 2, 5, 0.0, 0.0, 2.0, -3.0, 0.5, 1.5, 2.0)) } - test "the ground in front hides a thing, and nothing within arm's length is hidden" (aim_st: mut AimState, aim_test_st: mut AimTestState) { + test "the ridge in front hides a thing, a thing does not hide itself, and nothing a hand's breadth off is hidden" (aim_st: mut AimState, aim_test_st: mut AimTestState) { fresh(aim_st, aim_test_st) expect(not aim_clear_to(0.0, 1.0, -60.0)) expect(aim_clear_to(0.0, 1.0, -30.0)) expect(not aim_offer(aim_st, 1, 1, 0.0, 0.0, 2.0, -60.0, 2.0, 1.5, 80.0)) + aim_test_st.post = true + expect(not aim_clear_to(0.0, 1.0, -20.0)) + expect(aim_offer(aim_st, 1, 2, 0.0, 0.0, 2.0, -20.0, 0.5, 1.5, 80.0)) + fresh(aim_st, aim_test_st) aim_test_st.cy = -5.0 - expect(aim_clear_to(0.0, -5.0, -2.0)) + expect(aim_clear_at(0.5, 0.45)) + expect(not aim_clear_at(50.0, 0.5)) } test "the crosshair decides which, the distance whether: every reach is floored at three metres" (aim_st: mut AimState, aim_test_st: mut AimTestState) { @@ -92,12 +109,4 @@ program AimTest { expect(not aim_in_reach(aim_st)) expect_eq(aim_kind(aim_st), 2) } - - test "the probe finds the bank and agrees with the ground about every line it hides" (aim_st: mut AimState, aim_test_st: mut AimTestState) { - fresh(aim_st, aim_test_st) - expect(aim_probe_ground(aim_st, 0.0, 0.0, fn fk_look)) - expect(aim_probe_seen(aim_st) > 100) - expect(aim_probe_blocked(aim_st) > 0) - expect(not (aim_probe_disagree(aim_st) > aim_probe_seen(aim_st) / 50)) - } } diff --git a/packages/ludic.npc/README.md b/packages/ludic.npc/README.md index 8f43868c..31daab20 100644 --- a/packages/ludic.npc/README.md +++ b/packages/ludic.npc/README.md @@ -78,6 +78,8 @@ a `fill` line's words are `NpcTalk.say(p, line)`, and "" means it has nothing tr export port NpcWorld { # every member has a default: a flat dry field at noon, one player, nobody wanted ground, water, slope: fn(float, float) -> float allowed: fn(float, float) -> bool # may a person stop here + push: fn(x, z, r, feet, head) -> bool, pushed_x, pushed_z # a walker slid clear of still things (r 0.35) + clear: fn(ax, ay, az, bx, by, bz) -> bool # no still thing between: npc_line_ok asks it at 1 m hour: fn() -> float, day: fn() -> int # the clock players: fn() -> int, player_on: fn(int) -> bool, player_x / player_z: fn(int) -> float want: fn() -> int, extra: fn() -> int # the census: how many, and a kind brought out regardless (-1) diff --git a/packages/ludic.npc/ground.ludic b/packages/ludic.npc/ground.ludic index 5ee6b103..1c7d7327 100644 --- a/packages/ludic.npc/ground.ludic +++ b/packages/ludic.npc/ground.ludic @@ -1,5 +1,5 @@ # ground.ludic - where a person can stand and walk: dry, gentle, allowed; a straight walk with no -# water and no scramble on it; a spot in a ring, a spot toward something, and the nearest shore. +# water, scramble or still thing on it; a spot in a ring, a spot toward something, the nearest shore. # A finder that succeeds leaves its answer in npc_px / npc_pz (and the water in npc_wx / npc_wz). export function npc_px(npc_st: NpcState) -> float { return npc_st.np_px } @@ -18,6 +18,7 @@ export function npc_line_ok(ax: float, az: float, bx: float, bz: float) -> bool var n = int(d / 1.5) if n < 1 { n = 1 } var prev = NpcWorld.ground(ax, az) + if not NpcWorld.clear(ax, prev + 1.0, az, bx, NpcWorld.ground(bx, bz) + 1.0, bz) { return false } # a trunk, a tent for i in 1 .. n + 1 { let t = float(i) / float(n) let x = ax + (bx - ax) * t diff --git a/packages/ludic.npc/person.ludic b/packages/ludic.npc/person.ludic index 538a2c56..0a255952 100644 --- a/packages/ludic.npc/person.ludic +++ b/packages/ludic.npc/person.ludic @@ -19,6 +19,8 @@ export state NpcState { nw_ids: int = 0 np_near_i: int = -1 np_said: string = "" + np_ask_x: float = 0.0 # the point handed to NpcWorld.push, then where it came out + np_ask_z: float = 0.0 } const NP_PI: float = 3.1415927 const NP_TAU: float = 6.2831853 diff --git a/packages/ludic.npc/port.ludic b/packages/ludic.npc/port.ludic index ca23bdce..3448ec87 100644 --- a/packages/ludic.npc/port.ludic +++ b/packages/ludic.npc/port.ludic @@ -5,6 +5,11 @@ export port NpcWorld { water: fn(float, float) -> float = fn nw_dry # the water's surface there slope: fn(float, float) -> float = fn nw_zero2 allowed: fn(float, float) -> bool = fn nw_yes2 # a person may stop here (not the player's camp) + # (x, z, r, feet, head): true when a still thing takes a body of radius r there; pushed_x / _z say where + push: fn(float, float, float, float, float) -> bool = fn nw_no_push + pushed_x: fn() -> float = fn nw_asked_x + pushed_z: fn() -> float = fn nw_asked_z + clear: fn(float, float, float, float, float, float) -> bool = fn nw_clear # nothing between two points hour: fn() -> float = fn nw_noon day: fn() -> int = fn nw_day_one players: fn() -> int = fn nw_one # player slots @@ -34,6 +39,10 @@ function nw_zero1(p: int) -> float { return 0.0 } function nw_zero2(x: float, z: float) -> float { return 0.0 } function nw_dry(x: float, z: float) -> float { return -1000.0 } function nw_yes2(x: float, z: float) -> bool { return true } +function nw_no_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { return false } +function nw_asked_x(npc_st: NpcState) -> float { return npc_st.np_ask_x } +function nw_asked_z(npc_st: NpcState) -> float { return npc_st.np_ask_z } +function nw_clear(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return true } function nw_noon() -> float { return 12.0 } function nw_day_one() -> int { return 1 } function nw_one() -> int { return 1 } diff --git a/packages/ludic.npc/tests/fake/index.ludic b/packages/ludic.npc/tests/fake/index.ludic index 75532b4c..0647af29 100644 --- a/packages/ludic.npc/tests/fake/index.ludic +++ b/packages/ludic.npc/tests/fake/index.ludic @@ -11,6 +11,9 @@ export state NpcTestsFakeState { fk_on: []bool = null fk_born: int = 0 fk_gone: int = 0 + fk_trunk: bool = false # a trunk 1 m round at (10, 0.3) + fk_out_x: float = 0.0 + fk_out_z: float = 0.0 } function fk_setup(npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) -> void { @@ -23,6 +26,7 @@ function fk_setup(npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState npc_tests_fake_st.fk_on = [true, false] npc_tests_fake_st.fk_born = 0 npc_tests_fake_st.fk_gone = 0 + npc_tests_fake_st.fk_trunk = false npc_clear(npc_st) npc_reset(npc_st) npc_seed(npc_st, 7) @@ -61,3 +65,22 @@ function fk_count(npc_st: mut NpcState, what: int) -> int { for i in 0 .. len(fs) { if fs[i].what == what { n += 1 } } return n } + +# the trunk, when it stands: a body inside it is pushed straight out from its axis, and a line +# passing within its radius is not clear +function fk_push(npc_tests_fake_st: mut NpcTestsFakeState, x: float, z: float, r: float, y0: float, y1: float) -> bool { + let d = npc_d(x, z, 10.0, 0.3) + if not npc_tests_fake_st.fk_trunk or d > 1.0 + r or d < 0.0001 { return false } + npc_tests_fake_st.fk_out_x = 10.0 + (x - 10.0) / d * (1.0 + r) + npc_tests_fake_st.fk_out_z = 0.3 + (z - 0.3) / d * (1.0 + r) + return true +} +function fk_pushed_x(npc_tests_fake_st: NpcTestsFakeState) -> float { return npc_tests_fake_st.fk_out_x } +function fk_pushed_z(npc_tests_fake_st: NpcTestsFakeState) -> float { return npc_tests_fake_st.fk_out_z } +function fk_clear(npc_tests_fake_st: NpcTestsFakeState, ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { + if not npc_tests_fake_st.fk_trunk { return true } + let lx = bx - ax + let lz = bz - az + let t = Math.clamp(((10.0 - ax) * lx + (0.3 - az) * lz) / Math.max(lx * lx + lz * lz, 0.0001), 0.0, 1.0) + return npc_d(ax + lx * t, az + lz * t, 10.0, 0.3) > 1.0 +} diff --git a/packages/ludic.npc/tests/npc_test.ludic b/packages/ludic.npc/tests/npc_test.ludic index 2030ef63..0b001a50 100644 --- a/packages/ludic.npc/tests/npc_test.ludic +++ b/packages/ludic.npc/tests/npc_test.ludic @@ -10,6 +10,10 @@ program NpcTest { ground: fn fk_ground water: fn fk_water allowed: fn fk_allowed + push: fn fk_push + pushed_x: fn fk_pushed_x + pushed_z: fn fk_pushed_z + clear: fn fk_clear hour: fn fk_hour_now day: fn fk_day_now players: fn fk_players @@ -95,6 +99,21 @@ program NpcTest { expect_near(p.fx, 30.0, 0.01) } + test "a trunk in the way is walked round, and a line through it is not a clear walk" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) { + let p = one(npc_st, npc_tests_fake_st, NPCK_HIKER) + npc_tests_fake_st.fk_trunk = true + expect(not npc_line_ok(0.0, 0.0, 20.0, 0.0)) + expect(npc_line_ok(0.0, 5.0, 20.0, 5.0)) + npc_go(npc_st, p, 20.0, 0.0, NPCA_SIT, 30.0) + var closest = 100.0 + for k in 0 .. 600 { + npc_tick(npc_st, 0.05) + closest = Math.min(closest, npc_d(p.x, p.z, 10.0, 0.3)) + } + expect(closest > 1.3) + expect_eq(p.act, NPCA_SIT) + } + test "nobody walks into the lake" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) { let p = one(npc_st, npc_tests_fake_st, NPCK_HIKER) npc_go(npc_st, p, -80.0, 0.0, NPCA_SIT, 30.0) diff --git a/packages/ludic.npc/walk.ludic b/packages/ludic.npc/walk.ludic index dbc1b4ea..230a7797 100644 --- a/packages/ludic.npc/walk.ludic +++ b/packages/ludic.npc/walk.ludic @@ -1,5 +1,8 @@ -# walk.ludic - walking to the target: turn toward it, step, never into the water and never up a -# scramble - step round it instead - and give up on a target that gets no closer for twenty seconds +# walk.ludic - walking to the target: turn toward it, step, slid round a trunk or a tent, never into +# the water and never up a scramble - step round it instead - and give up on a target that gets no +# closer for twenty seconds +const NP_BODY_R: float = 0.35 # a person's width, for the still things + export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void { var gx = p.tx var gz = p.tz @@ -29,10 +32,12 @@ export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void let r = 3.0 * dt p.yaw = p.yaw + Math.clamp(npc_wrap(want - p.yaw), -r, r) let step = sp * dt - let nx = p.x - Math.sin(p.yaw) * step - let nz = p.z - Math.cos(p.yaw) * step + np_slide(npc_st, p, p.x - Math.sin(p.yaw) * step, p.z - Math.cos(p.yaw) * step) + let nx = npc_st.np_ask_x + let nz = npc_st.np_ask_z let nh = NpcWorld.ground(nx, nz) - if nh < NpcWorld.water(nx, nz) + 0.4 or nh - p.y > step * 1.4 { + let slid = Math.abs(nx - p.x) + Math.abs(nz - p.z) + if nh < NpcWorld.water(nx, nz) + 0.4 or nh - p.y > step * 1.4 or slid < step * 0.2 { p.stall = p.stall + dt if p.stall > p.stall_max { p.stall_max = p.stall } if p.detours < 10 and np_detour(p, want) { @@ -49,6 +54,17 @@ export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void p.phase = p.phase + step * 2.6 } +# (x, z) slid clear of every still thing for a person; np_ask_x / _z hold where it came out +function np_slide(npc_st: mut NpcState, p: NpcPerson, x: float, z: float) -> void { + npc_st.np_ask_x = x + npc_st.np_ask_z = z + if not NpcWorld.push(x, z, NP_BODY_R, p.y + 0.1, p.y + 1.8) { return } + let ox = NpcWorld.pushed_x() + let oz = NpcWorld.pushed_z() + npc_st.np_ask_x = ox + npc_st.np_ask_z = oz +} + # there: a step round something ends, or the errand begins, facing what it came for function np_arrive(npc_st: mut NpcState, p: NpcPerson) -> void { if p.via_on { diff --git a/packages/ludic.photo/README.md b/packages/ludic.photo/README.md index 4565d4f1..61821d08 100644 --- a/packages/ludic.photo/README.md +++ b/packages/ludic.photo/README.md @@ -36,14 +36,14 @@ export port PhotoLens { x, y, z, fx, fy, fz, fov, # the camera project(x, y, z) -> bool, sx(), sy(), # into the picture, 0..1 visible(x, y, z, r) -> bool, # the frustum - ground(x, z), trunk(x, z, r) -> bool, # what a line of sight walks + clear(ax, ay, az, bx, by, bz) -> bool, # a line of sight: the ground and every still thing range(), steady() -> bool # the lens in the pack } export port PhotoLight { hour, night, moon, dim, bonus, sun_x, sun_z } export port PhotoRules { behaviour, subject_value(sp), legend_value(sp), mult } ``` -Every member has a default: a lens at the origin looking down -z on flat open ground at noon. +Every member has a default: a lens at the origin looking down -z with nothing in the way at noon. ## API diff --git a/packages/ludic.photo/ports.ludic b/packages/ludic.photo/ports.ludic index 0d662bcd..5822d8c1 100644 --- a/packages/ludic.photo/ports.ludic +++ b/packages/ludic.photo/ports.ludic @@ -1,6 +1,6 @@ # ports.ludic - what a photograph asks the game: where the lens is and what it can see, the light, -# and the rules of the market. Unbound, the lens sits at the origin looking down -z on flat open -# ground at noon under a clear sky, every point projects to the middle of the frame, and a +# and the rules of the market. Unbound, the lens sits at the origin looking down -z with nothing in +# the way at noon under a clear sky, every point projects to the middle of the frame, and a # subject's first frame is worth 12 and a legend 120. export port PhotoLens { x: fn() -> float = fn pht__zero # the camera's position @@ -14,8 +14,7 @@ export port PhotoLens { sx: fn() -> float = fn pht__half # 0..1 across sy: fn() -> float = fn pht__half # 0..1 down visible: fn(float, float, float, float) -> bool = fn pht__seen # a sphere inside the frustum - ground: fn(float, float) -> float = fn pht__flat - trunk: fn(float, float, float) -> bool = fn pht__clear # (x, z, r): a trunk there + clear: fn(float, float, float, float, float, float) -> bool = fn pht__clear # nothing between two points range: fn() -> float = fn pht__range # how far the lens reaches, m steady: fn() -> bool = fn pht__no # on a tripod } @@ -43,8 +42,7 @@ function pht__fov() -> float { return 1.0 } function pht__centred(x: float, y: float, z: float) -> bool { return true } function pht__half() -> float { return 0.5 } function pht__seen(x: float, y: float, z: float, r: float) -> bool { return true } -function pht__flat(x: float, z: float) -> float { return 0.0 } -function pht__clear(x: float, z: float, r: float) -> bool { return false } +function pht__clear(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return true } function pht__range() -> float { return 100.0 } function pht__no() -> bool { return false } function pht__noon() -> float { return 12.0 } diff --git a/packages/ludic.photo/sight.ludic b/packages/ludic.photo/sight.ludic index 42d1df43..2cec7c73 100644 --- a/packages/ludic.photo/sight.ludic +++ b/packages/ludic.photo/sight.ludic @@ -1,27 +1,7 @@ # sight.ludic - in the picture, within reach, and actually seen. A frustum says a subject is in FRONT -# of the lens, not that the lens can see it: the ray walks the ground and the trunks between. It is -# forgiving on purpose - the subject passes if its body or its head is clear. -const PHT_STEP: float = 3.0 # metres between the ray's samples - -function pht_ray_point(x: float, y: float, z: float) -> bool { - let cx = PhotoLens.x() - let cy = PhotoLens.y() - let cz = PhotoLens.z() - let d = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz)) - if d < 3.0 { return true } - let n = int(Math.clamp(d / PHT_STEP, 6.0, 120.0)) - for s in 1 .. n { - let f = float(s) / float(n) - let px = Math.lerp(cx, x, f) - let py = Math.lerp(cy, y, f) - let pz = Math.lerp(cz, z, f) - let g = PhotoLens.ground(px, pz) - # the ground with 35 cm of slack; a trunk once past the lens's own few metres - if g - 0.35 > py { return false } - if d * f > 6.0 and py - g < 7.0 and PhotoLens.trunk(px, pz, 0.45) { return false } - } - return true -} +# of the lens, not that the lens can see it: the line must be clear of the ground and the still +# things (the port's). It is forgiving on purpose - the subject passes if its body or its head is clear. +function pht_ray_point(x: float, y: float, z: float) -> bool { return PhotoLens.clear(PhotoLens.x(), PhotoLens.y(), PhotoLens.z(), x, y, z) } # is (x, y, z) in the picture, within `far`, and can the lens see it? export function photo_in_view(x: float, y: float, z: float, far: float) -> bool { diff --git a/packages/ludic.photo/tests/photo_test.ludic b/packages/ludic.photo/tests/photo_test.ludic index 0787558b..0bd99dac 100644 --- a/packages/ludic.photo/tests/photo_test.ludic +++ b/packages/ludic.photo/tests/photo_test.ludic @@ -34,7 +34,19 @@ program PhotoTest { if x < 0.0 and z < -45.0 and z > -50.0 { return 10.0 } return 0.0 } - function fk_trunk(x: float, z: float, r: float) -> bool { return Math.abs(x - 3.75) < 1.6 and Math.abs(z + 30.0) < 1.6 } + function fk_trunk(x: float, z: float) -> bool { return Math.abs(x - 3.75) < 1.6 and Math.abs(z + 30.0) < 1.6 } + # what a physics ray would say: forty points of the line against the ridge and the trunk + function fk_clear(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { + let n = 40 + for s in 1 .. n { + let f = float(s) / float(n) + let px = Math.lerp(ax, bx, f) + let pz = Math.lerp(az, bz, f) + let g = fk_ground(px, pz) + if g - 0.35 > Math.lerp(ay, by, f) or (Math.lerp(ay, by, f) - g < 7.0 and fk_trunk(px, pz)) { return false } + } + return true + } function fk_range() -> float { return 80.0 } function fk_hour(photo_test_st: PhotoTestState) -> float { return photo_test_st.hour } function fk_night(photo_test_st: PhotoTestState) -> bool { return photo_test_st.night } @@ -44,7 +56,7 @@ program PhotoTest { function fk_behaviour(photo_test_st: PhotoTestState) -> bool { return photo_test_st.behaviour } function fk_value(sp: int) -> int { return 10 + sp } - bind PhotoLens { y: fn fk_y, fz: fn fk_fz, fov: fn fk_fov, project: fn fk_project, sx: fn fk_sx, sy: fn fk_sy, ground: fn fk_ground, trunk: fn fk_trunk, range: fn fk_range } + bind PhotoLens { y: fn fk_y, fz: fn fk_fz, fov: fn fk_fov, project: fn fk_project, sx: fn fk_sx, sy: fn fk_sy, clear: fn fk_clear, range: fn fk_range } bind PhotoLight { hour: fn fk_hour, night: fn fk_night, moon: fn fk_moon, dim: fn fk_dim, sun_z: fn fk_sun_z } bind PhotoRules { behaviour: fn fk_behaviour, subject_value: fn fk_value } diff --git a/packages/ludic.wildlife/README.md b/packages/ludic.wildlife/README.md index 37e504ed..c9d5b67b 100644 --- a/packages/ludic.wildlife/README.md +++ b/packages/ludic.wildlife/README.md @@ -31,6 +31,7 @@ export port WildlifeWorld { # every member has a default: a flat dry mead ground, water, slope: fn(float, float) -> float # the ground, the water's surface, the gradient forest: fn(float, float) -> bool sight: fn(ax, ay, az, bx, by, bz) -> bool # nothing between the two points + push: fn(x, z, r, feet, head) -> bool, pushed_x, pushed_z # a walker slid clear of still things players: fn() -> int # slots; player_on(p), player_x / _y / _z(p) hidden, noise, scent, stillness # per player: in a hide, 0..1, 0..1, WILD_MOVING / _STILL / _SITTING hour, light, winterness, wind, wind_dir: fn() -> float # the clock, how well it sees now, the season, the weather diff --git a/packages/ludic.wildlife/port.ludic b/packages/ludic.wildlife/port.ludic index 6f8ac14f..5fb77d1c 100644 --- a/packages/ludic.wildlife/port.ludic +++ b/packages/ludic.wildlife/port.ludic @@ -15,6 +15,10 @@ export port WildlifeWorld { slope: fn(float, float) -> float = fn wl_zero2 forest: fn(float, float) -> bool = fn wl_no2 sight: fn(float, float, float, float, float, float) -> bool = fn wl_clear # nothing between + # (x, z, r, feet, head): true when a still thing takes a body of radius r there; pushed_x / _z say where + push: fn(float, float, float, float, float) -> bool = fn wl_no_push + pushed_x: fn() -> float = fn wl_asked_x + pushed_z: fn() -> float = fn wl_asked_z players: fn() -> int = fn wl_one # player slots player_on: fn(int) -> bool = fn wl_first player_x: fn(int) -> float = fn wl_zero1 @@ -52,6 +56,9 @@ function wl_no1(p: int) -> bool { return false } function wl_no2(x: float, z: float) -> bool { return false } function wl_yes1(u: int) -> bool { return true } function wl_clear(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> bool { return true } +function wl_no_push(x: float, z: float, r: float, y0: float, y1: float) -> bool { return false } +function wl_asked_x(wildlife_st: WildlifeState) -> float { return wildlife_st.wl_ask_x } +function wl_asked_z(wildlife_st: WildlifeState) -> float { return wildlife_st.wl_ask_z } function wl_one() -> int { return 1 } function wl_none0() -> int { return 0 } function wl_first(p: int) -> bool { return p == 0 } diff --git a/packages/ludic.wildlife/species.ludic b/packages/ludic.wildlife/species.ludic index 767d2bc0..273d12ad 100644 --- a/packages/ludic.wildlife/species.ludic +++ b/packages/ludic.wildlife/species.ludic @@ -42,6 +42,10 @@ export state WildlifeState { wl_seed_any: bool = false wl_seed_t: float = 0.0 wl_ids: int = 0 + wl_ask_x: float = 0.0 # the point last handed to WildlifeWorld.push, and where it came out + wl_ask_z: float = 0.0 + wl_out_x: float = 0.0 + wl_out_z: float = 0.0 wl_ranges: []WildRange = null wl_found: []int = null # found flags a load brought before the ranges were set out wl_seen: int = 0 diff --git a/packages/ludic.wildlife/steer.ludic b/packages/ludic.wildlife/steer.ludic index baf39b13..4b51c5a9 100644 --- a/packages/ludic.wildlife/steer.ludic +++ b/packages/ludic.wildlife/steer.ludic @@ -13,14 +13,20 @@ function wl_face(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) -> a.yaw = a.yaw + Math.clamp(wl_wrap(want - a.yaw), -r, r) } -# true when it moved +# true when it moved; a walker is slid round a still thing (a trunk, a tent), a bird is not function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool { let d = a.speed * dt if d < 0.0001 { return false } - let nx = a.x - Math.sin(a.yaw) * d - let nz = a.z - Math.cos(a.yaw) * d + var nx = a.x - Math.sin(a.yaw) * d + var nz = a.z - Math.cos(a.yaw) * d + let s = wl_sp(wildlife_st, a.sp) + if not s.flies and wl_push(wildlife_st, a, s, nx, nz) { + nx = wildlife_st.wl_out_x + nz = wildlife_st.wl_out_z + } let nh = WildlifeWorld.ground(nx, nz) - if nh < WildlifeWorld.water(nx, nz) + 0.6 or nh - a.y > d * 1.2 { + let moved = Math.abs(nx - a.x) + Math.abs(nz - a.z) + if nh < WildlifeWorld.water(nx, nz) + 0.6 or nh - a.y > d * 1.2 or moved < d * 0.2 { a.yaw = a.yaw + Math.deg_to_rad(75.0) a.timer = 0.0 return false @@ -29,13 +35,29 @@ function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo a.z = nz a.y = nh a.track_d = a.track_d + d - if wl_sp(wildlife_st, a.sp).tracks and a.track_d > 2.8 { + if s.tracks and a.track_d > 2.8 { a.track_d = 0.0 wl_fact(wildlife_st, WILD_PRINT, a) } return true } +# (x, z) pushed clear of every still thing for a body its size; wl_out_x / _z say where +function wl_push(wildlife_st: mut WildlifeState, a: WildAnimal, s: WildSpecies, x: float, z: float) -> bool { + wildlife_st.wl_ask_x = x + wildlife_st.wl_ask_z = z + let k = Math.clamp(float(s.size), 0.0, 2.0) + let r = (0.2 + 0.2 * k) * s.scale * a.scale + let hit = WildlifeWorld.push(x, z, r, a.y + 0.1, a.y + (0.5 + 0.7 * k) * s.scale * a.scale) + wildlife_st.wl_out_x = x + wildlife_st.wl_out_z = z + if hit { + wildlife_st.wl_out_x = WildlifeWorld.pushed_x() + wildlife_st.wl_out_z = WildlifeWorld.pushed_z() + } + return hit +} + function wl_pick_target(wildlife_st: mut WildlifeState, a: WildAnimal, radius: float) -> void { let ang = wl_rnd(wildlife_st) * WL_TAU let d = wl_rnd(wildlife_st) * radius diff --git a/packages/ludic.wildlife/tests/fake/index.ludic b/packages/ludic.wildlife/tests/fake/index.ludic index 05e036e6..12af7202 100644 --- a/packages/ludic.wildlife/tests/fake/index.ludic +++ b/packages/ludic.wildlife/tests/fake/index.ludic @@ -16,6 +16,9 @@ export state WildlifeTestsFakeState { fk_born: int = 0 fk_lures: []WildLure = null fk_gone: int = 0 # a uid the port says is no longer there + fk_trunk: bool = false # a trunk 1 m round at (0.3, -20) + fk_out_x: float = 0.0 + fk_out_z: float = 0.0 } function fk_setup(wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) -> void { @@ -27,6 +30,7 @@ function fk_setup(wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut Wi wildlife_tests_fake_st.fk_px[0] = 5000.0 wildlife_tests_fake_st.fk_pz[0] = 5000.0 wildlife_tests_fake_st.fk_lures = new []WildLure + wildlife_tests_fake_st.fk_trunk = false wildlife_clear(wildlife_st) wildlife_species_clear(wildlife_st) fk_species(wildlife_st) @@ -108,3 +112,16 @@ function fk_spot(a: WildAnimal, water: bool, s: WildSpot) -> bool { s.water = water return true } + +# the trunk, when it stands: a body inside it is pushed straight out from its axis +function fk_push(wildlife_tests_fake_st: mut WildlifeTestsFakeState, x: float, z: float, r: float, y0: float, y1: float) -> bool { + let dx = x - 0.3 + let dz = z + 20.0 + let d = Math.sqrt(dx * dx + dz * dz) + if not wildlife_tests_fake_st.fk_trunk or d > 1.0 + r or d < 0.0001 { return false } + wildlife_tests_fake_st.fk_out_x = 0.3 + dx / d * (1.0 + r) + wildlife_tests_fake_st.fk_out_z = -20.0 + dz / d * (1.0 + r) + return true +} +function fk_pushed_x(wildlife_tests_fake_st: WildlifeTestsFakeState) -> float { return wildlife_tests_fake_st.fk_out_x } +function fk_pushed_z(wildlife_tests_fake_st: WildlifeTestsFakeState) -> float { return wildlife_tests_fake_st.fk_out_z } diff --git a/packages/ludic.wildlife/tests/wildlife_test.ludic b/packages/ludic.wildlife/tests/wildlife_test.ludic index 259c95b5..bd8caae9 100644 --- a/packages/ludic.wildlife/tests/wildlife_test.ludic +++ b/packages/ludic.wildlife/tests/wildlife_test.ludic @@ -10,6 +10,9 @@ program WildlifeTest { 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 @@ -177,6 +180,25 @@ program WildlifeTest { expect(wildlife_seen(wildlife_st, 0)) } + test "a walker slides round a trunk rather than through it" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = deer_at(wildlife_st, 0.0, 0.0) + wildlife_tests_fake_st.fk_trunk = true + player_at(wildlife_tests_fake_st, 60.0, 0.0) + wildlife_tests_fake_st.fk_blocked = true + a.state = WILD_WANDER + a.tx = 0.0 + a.tz = -100.0 + a.timer = 100.0 + var closest = 100.0 + for k in 0 .. 800 { + wildlife_tick(wildlife_st, 0.05, 0.0) + closest = Math.min(closest, Math.sqrt((a.x - 0.3) * (a.x - 0.3) + (a.z + 20.0) * (a.z + 20.0))) + } + expect(closest > 1.35) + expect(a.z < -25.0) + } + test "the record and the found ranges come back from the save section" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { fk_setup(wildlife_st, wildlife_tests_fake_st) wildlife_spawn(wildlife_st, 0.0, 0.0)