From 61f802e9ad44b13853bed1fc04546aa00d91c5e4 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 22:31:07 +0300 Subject: [PATCH 01/14] ludic.wildlife (17.6): a fleeing or wary animal goes by the world's way It sets a target straight away from the threat (40 m when it flees, 20 m when it is wary) and heads for it with wl_head, so the way port takes it round water and cliffs. Where the world gives no way, it still runs straight away as before. way_test now has a flee round the wall's end. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.wildlife/react.ludic | 6 ++++-- packages/ludic.wildlife/tests/way_test.ludic | 19 +++++++++++++++++++ packages/ludic.wildlife/way.ludic | 17 +++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/ludic.wildlife/react.ludic b/packages/ludic.wildlife/react.ludic index 3f3580bc..e2172e5d 100644 --- a/packages/ludic.wildlife/react.ludic +++ b/packages/ludic.wildlife/react.ludic @@ -9,14 +9,16 @@ function wl_react(wildlife_st: mut WildlifeState, a: WildAnimal, np: int, dt: fl return true } if a.state == WILD_WARY { - wl_face(a, a.x + (a.x - a.sus_x), a.z + (a.z - a.sus_z), 2.6, dt) + wl_away(a, a.sus_x, a.sus_z, 20.0) + wl_head(a, a.tx, a.tz, 2.6, dt) a.speed = s.walk * 0.8 wl_step(wildlife_st, a, dt) if a.timer < 0.0 { wl_wander(wildlife_st, a, 3.0 + wl_rnd(wildlife_st) * 4.0) } return true } if a.state == WILD_FLEE { - wl_face(a, a.x + (a.x - WildlifeWorld.player_x(np)), a.z + (a.z - WildlifeWorld.player_z(np)), 5.0, dt) + wl_away(a, WildlifeWorld.player_x(np), WildlifeWorld.player_z(np), 40.0) + wl_head(a, a.tx, a.tz, 5.0, dt) a.speed = s.run wl_step(wildlife_st, a, dt) if a.timer < 0.0 { diff --git a/packages/ludic.wildlife/tests/way_test.ludic b/packages/ludic.wildlife/tests/way_test.ludic index b9624030..65b5ffbb 100644 --- a/packages/ludic.wildlife/tests/way_test.ludic +++ b/packages/ludic.wildlife/tests/way_test.ludic @@ -103,4 +103,23 @@ program WayTest { expect_near(a.tx, a.home_x + 7.0, 0.001) expect_near(a.tz, a.home_z + 3.0, 0.001) } + + test "it runs from the player by the way too: round the wall's end, not into it" (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] = 0.0 + wildlife_tests_fake_st.fk_pz[0] = 20.0 + wildlife_tests_fake_st.fk_hidden = true + a.state = WILD_FLEE + a.timer = 300.0 + var east = -100.0 + for k in 0 .. 1200 { + 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_eq(a.state, WILD_FLEE) + expect(east > 29.0) + expect(a.z < -45.0) + expect(way_test_st.asked > 10) + } } diff --git a/packages/ludic.wildlife/way.ludic b/packages/ludic.wildlife/way.ludic index ddc00e31..6cb5f9e9 100644 --- a/packages/ludic.wildlife/way.ludic +++ b/packages/ludic.wildlife/way.ludic @@ -34,3 +34,20 @@ function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float a.tz = WildlifeWorld.way_z() } } + +# a target d metres straight away from (fx, fz): where it runs or backs off to, by the world's way - +# round water and a cliff where there is one, and straight away as before where there is not +function wl_away(a: WildAnimal, fx: float, fz: float, d: float) -> void { + var dx = a.x - fx + var dz = a.z - fz + let l = Math.sqrt(dx * dx + dz * dz) + if l < 0.01 { + dx = -Math.sin(a.yaw) + dz = -Math.cos(a.yaw) + } else { + dx = dx / l + dz = dz / l + } + a.tx = a.x + dx * d + a.tz = a.z + dz * d +} From ab84a5f9368ac305eb890f8b376457a9daabdc8c Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 22:33:25 +0300 Subject: [PATCH 02/14] ludic.npc (17.7): a person's fallback is a fact A step round what is in the way, a target planned again after twenty seconds without progress, and a target given up are each pushed as NPC_F_STUCK (how, x, z), so the game can see where the way failed a person. The lake test expects one when a walker turns back from the water; the way test expects none when the world gives the way. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.npc/README.md | 2 +- packages/ludic.npc/defs.ludic | 4 ++++ packages/ludic.npc/tests/npc_test.ludic | 8 +++++++- packages/ludic.npc/tests/way_test.ludic | 2 ++ packages/ludic.npc/walk.ludic | 1 + packages/ludic.npc/way.ludic | 17 +++++++++++++++-- 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/ludic.npc/README.md b/packages/ludic.npc/README.md index 42263c87..dc46f886 100644 --- a/packages/ludic.npc/README.md +++ b/packages/ludic.npc/README.md @@ -114,7 +114,7 @@ by a person's taste. | `npc_pick_name(body)`, `npc_name_used(n)`, `npc_name_clashes()` | names | | `npc_talk(p, player)`, `npc_line_for(p)`, `npc_words(p, line)`, `npc_said()`, `npc_choose(p, line, c, player)`, `npc_talked_today(p)`, `npc_greeting(p)` | talk | | `npc_mirror(on)`, `npc_mirroring()`, `npc_put(slot, id, kind, look, seed, name, hx, hz)`, `npc_heard(slot, x, y, z, yaw, speed, act, stalk)`, `npc_heard_lately(p)`, `npc_mirror_tick(dt)` | a guest's copy of the host's people | -| `npc_facts() -> Queue` | `NPC_F_GREETED` (`player`), `NPC_F_TALKED` (`player`, `line`, `choice`), `NPC_F_ARRIVED` (`act`) | +| `npc_facts() -> Queue` | `NPC_F_GREETED` (`player`), `NPC_F_TALKED` (`player`, `line`, `choice`), `NPC_F_ARRIVED` (`act`), `NPC_F_STUCK` (`how` - `NPC_STUCK_DETOUR`, `_RETRY`, `_GAVE_UP` - at `x`, `z`: the way failed it and it fell back) | | `npc_system() -> System` | `"npc"`, `PH_SIMULATE`, a reset and no tick; nothing saved | ## Tests diff --git a/packages/ludic.npc/defs.ludic b/packages/ludic.npc/defs.ludic index 18e3121a..2352a657 100644 --- a/packages/ludic.npc/defs.ludic +++ b/packages/ludic.npc/defs.ludic @@ -131,6 +131,7 @@ export open registry NpcNames of NpcName as NPCN export const NPC_F_GREETED: int = 0 # a player came within notice export const NPC_F_TALKED: int = 1 # a player talked (line, choice -1 or the choice taken) export const NPC_F_ARRIVED: int = 2 # got where it was walking, and began the act +export const NPC_F_STUCK: int = 3 # the way failed it and it fell back (`how`, at `x`, `z`) export property NpcFact { what: int = 0 @@ -141,4 +142,7 @@ export property NpcFact { line: int = -1 choice: int = -1 act: int = -1 + how: int = 0 # NPC_STUCK_* + x: float = 0.0 + z: float = 0.0 } diff --git a/packages/ludic.npc/tests/npc_test.ludic b/packages/ludic.npc/tests/npc_test.ludic index 0b001a50..c9b5bbe0 100644 --- a/packages/ludic.npc/tests/npc_test.ludic +++ b/packages/ludic.npc/tests/npc_test.ludic @@ -114,7 +114,7 @@ program NpcTest { expect_eq(p.act, NPCA_SIT) } - test "nobody walks into the lake" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) { + test "nobody walks into the lake, and turning back from it is told as a fallback" (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) var west = 0.0 @@ -123,6 +123,12 @@ program NpcTest { west = Math.min(west, p.x) } expect(west > -50.5) + var fell = 0 + let fs = q_drain(npc_facts(npc_st)) + for i in 0 .. len(fs) { + if fs[i].what == NPC_F_STUCK and fs[i].x > -52.0 { fell += 1 } + } + expect(fell >= 1) } test "a step's spot is where its hours say: the shore by day, home by night" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState) { diff --git a/packages/ludic.npc/tests/way_test.ludic b/packages/ludic.npc/tests/way_test.ludic index 22b726b4..b76fda18 100644 --- a/packages/ludic.npc/tests/way_test.ludic +++ b/packages/ludic.npc/tests/way_test.ludic @@ -78,5 +78,7 @@ program NpcWayTest { expect_eq(p.act, NPCA_SIT) expect(npc_d(p.x, p.z, 38.0, -25.0) < 1.3) expect(way_test_st.asked > 5) + let fs = q_drain(npc_facts(npc_st)) + for i in 0 .. len(fs) { expect(fs[i].what != NPC_F_STUCK) } } } diff --git a/packages/ludic.npc/walk.ludic b/packages/ludic.npc/walk.ludic index 90758386..6f416cc4 100644 --- a/packages/ludic.npc/walk.ludic +++ b/packages/ludic.npc/walk.ludic @@ -46,6 +46,7 @@ export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void 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) { + np_stuck_fact(npc_st, p, NPC_STUCK_DETOUR) p.detours += 1 return } diff --git a/packages/ludic.npc/way.ludic b/packages/ludic.npc/way.ludic index 1108897f..d0ffa8ad 100644 --- a/packages/ludic.npc/way.ludic +++ b/packages/ludic.npc/way.ludic @@ -1,6 +1,15 @@ # way.ludic - where a person walks when the straight line will not do: the world's way (NpcWorld.way, # a navmesh by a person's taste in Maroon Lake), and without one a step round what is in the way, -# and giving the target up after three tries +# and giving the target up after three tries - each of those a NPC_F_STUCK fact, so the fallback is seen +export const NPC_STUCK_DETOUR: int = 0 # a step round what was in front of it +export const NPC_STUCK_RETRY: int = 1 # no closer for twenty seconds, or no step round: planned again +export const NPC_STUCK_GAVE_UP: int = 2 # the third of those: the target given up +function np_stuck_fact(npc_st: NpcState, p: NpcPerson, how: int) -> void { + let f = np_fact(npc_st, NPC_F_STUCK, p, -1) + f.how = how + f.x = p.x + f.z = p.z +} # the next corner toward the target, asked again every half second, on reaching it, or for a new target const NP_WAY_EVERY: float = 0.5 @@ -39,7 +48,11 @@ function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void { p.fails += 1 p.via_on = false if p.fails >= 3 { + np_stuck_fact(npc_st, p, NPC_STUCK_GAVE_UP) p.fails = 0 npc_wander(npc_st, p) - } else { npc_plan(npc_st, p) } + } else { + np_stuck_fact(npc_st, p, NPC_STUCK_RETRY) + npc_plan(npc_st, p) + } } From 297578705ec757d19c5c7db113bc91c21e07d62f Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 22:38:37 +0300 Subject: [PATCH 03/14] ludic.wildlife (17.10): count the steps refused and turned from The count is wildlife_turned(), the 75-degree fallback an animal takes when the way was not enough. It is a measure only and is not saved. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.wildlife/species.ludic | 1 + packages/ludic.wildlife/steer.ludic | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/ludic.wildlife/species.ludic b/packages/ludic.wildlife/species.ludic index df213e6b..64940c78 100644 --- a/packages/ludic.wildlife/species.ludic +++ b/packages/ludic.wildlife/species.ludic @@ -58,6 +58,7 @@ export state WildlifeState { wl_dice: Rng = rng_new(97) wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed wl_spot_ask: WildSpot = new WildSpot + wl_turned: int = 0 # steps refused and turned 75 degrees instead: the fallback, counted } function wl_sp_all(wildlife_st: WildlifeState) -> []WildSpecies { diff --git a/packages/ludic.wildlife/steer.ludic b/packages/ludic.wildlife/steer.ludic index 4e73371e..b3cba573 100644 --- a/packages/ludic.wildlife/steer.ludic +++ b/packages/ludic.wildlife/steer.ludic @@ -14,6 +14,9 @@ function wl_face(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) -> } # 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 } + function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bool { let d = a.speed * dt if d < 0.0001 { return false } @@ -29,6 +32,7 @@ function wl_step(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo 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 + wildlife_st.wl_turned += 1 return false } a.x = nx From f65eda8b47fa66c0d81cd778abbb044a88cb6a89 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 22:41:30 +0300 Subject: [PATCH 04/14] ludic.nav (17.10): the time paths take, counted in the shim nav_us() gives the microseconds spent in nav_path across the loaded meshes. The shim times each path with the OS's monotonic clock (clock_gettime, or QueryPerformanceCounter from KERNEL32), so the DLL still needs no C++ runtime. Both libraries are rebuilt, and 13 tests pass on the Mac and the PC. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.nav/README.md | 2 +- .../lib/macos-arm64/libludicnav.dylib | 4 ++-- .../ludic.nav/lib/windows-x64/ludicnav.dll | 4 ++-- .../ludic.nav/lib/windows-x64/ludicnav.lib | 4 ++-- packages/ludic.nav/native.ludic | 1 + packages/ludic.nav/native/shim/nav_query.inl | 12 +++++++++-- packages/ludic.nav/native/shim/nav_shim.cpp | 21 +++++++++++++++++++ packages/ludic.nav/query.ludic | 9 ++++++++ packages/ludic.nav/tests/nav_test.ludic | 10 +++++++++ 9 files changed, 58 insertions(+), 9 deletions(-) diff --git a/packages/ludic.nav/README.md b/packages/ludic.nav/README.md index 0f6eba1f..70d99108 100644 --- a/packages/ludic.nav/README.md +++ b/packages/ludic.nav/README.md @@ -51,7 +51,7 @@ import "ludic.nav" | `nav_next_corner(kind, filter, sx, sy, sz, ex, ey, ez) -> bool` | the next corner of that way, read back as the nearest point is (a walker's one question a frame) | | `nav_straight(kind, filter, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable | | `nav_random_near(kind, filter, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed | -| `nav_asked()`, `nav_found()`, `nav_short()` | paths asked for, found and cut short since the meshes loaded (a game's measure of how often its walkers had a way) | +| `nav_asked()`, `nav_found()`, `nav_short()`, `nav_us()` | paths asked for, found and cut short since the meshes loaded, and the microseconds they took (timed in the shim by the OS clock: a game's measure of how often its walkers had a way, and what it cost) | | `nav_area_cost(kind, filter, area, cost)` | how dear a kind of ground is to cross by that filter | ## Tests diff --git a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib index 3941b8a7..7d84075a 100755 --- a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib +++ b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10555b7baa5958700f0ce4599e85fc4fc18b9c92afbbacfa64f04b630f42f583 -size 176816 +oid sha256:969c1ee0010c0fe88abcdfceb1a166031a8f6519b9e77a27c27331cbe2036e0b +size 176912 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.dll b/packages/ludic.nav/lib/windows-x64/ludicnav.dll index 7a9c6695..0bdc9341 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.dll +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.dll @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99671e7df79802010fbf132f736d1b5e2681f0cd72ab71e3aa6e7026b837c97c -size 337408 +oid sha256:0ef49349d93dbc3b94c3743f80fa56f2acf4f5e02f16faba6ec13823b5705aaa +size 337920 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.lib b/packages/ludic.nav/lib/windows-x64/ludicnav.lib index 9e1bdfe3..6bcfe136 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.lib +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3e4f7ae19a6113829feffe2ed99e4a8f6b8fb2831c90115f489d1ecf646216a -size 3866 +oid sha256:0ea766bfe59613b41a1566f41de772d49602191ad7e188bef3bcf453c55d6114 +size 4022 diff --git a/packages/ludic.nav/native.ludic b/packages/ludic.nav/native.ludic index 71d02ce1..190af731 100644 --- a/packages/ludic.nav/native.ludic +++ b/packages/ludic.nav/native.ludic @@ -11,6 +11,7 @@ extern function nvc_nearest(h: pointer, x: float, y: float, z: float, out: point extern function nvc_area_cost(h: pointer, f: int, area: int, cost: float) -> void = "nav_area_cost" extern function nvc_path(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path" extern function nvc_partial(h: pointer) -> int = "nav_partial" +extern function nvc_us(h: pointer) -> int = "nav_us" extern function nvc_raycast(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast" extern function nvc_random_near(h: pointer, f: int, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near" extern function nvc_stage(size: int) -> pointer = "nav_stage" diff --git a/packages/ludic.nav/native/shim/nav_query.inl b/packages/ludic.nav/native/shim/nav_query.inl index c3a8fdf3..9eb476a6 100644 --- a/packages/ludic.nav/native/shim/nav_query.inl +++ b/packages/ludic.nav/native/shim/nav_query.inl @@ -22,8 +22,7 @@ NAV_SHIM void nav_area_cost(void *h, int f, int area, float cost) { // the corners of a path from one point to another, at most max of them, into out: the count, or // -1 when either end is off the mesh. One that cannot reach the end stops as near it as the mesh // allows, and nav_partial says so -NAV_SHIM int nav_path(void *h, int f, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) { - Nav *n = static_cast