feat(nav): 17.3 - a map in square tiles (nav_tiled, nav_tile_build; a tile must be a whole number of cells or its seams never join, and the shim refuses one that is not; 64-bit polygon refs for 16384 tiles), ground as a NavGround with boulders as convex footprints, one save format for one tile or many, and nav_random_near - a reachable point from the caller's seed, bit-identical on the Mac and the PC. 10 tests on both

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 15:42:37 +03:00
parent 9b8606483d
commit e6893f58be
16 changed files with 436 additions and 109 deletions

View file

@ -14,15 +14,21 @@ import "ludic.nav"
`NAV_LARGE` (an elk, a bear, a horse) and `NAV_SMALL` (a hare, a marmot) each have their own,
because what a hare slips between a bear walks round. A `NavConfig` says what a mesh is built
for: its voxels and its walker's height, radius, step and steepest slope.
- **Built from triangles, or loaded from a bake.** `nav_build` rasterizes triangles - each with an
area byte, 0 not walkable and 1..62 a kind of ground - and marks every cylinder given (a trunk, a
post) as somewhere nothing stands; ground steeper than the slope is cleared whatever its area. A
game bakes its maps once and loads the bytes (`nav_save_file` / `nav_load_file`): a map is never
built at start-up.
- **Built from ground, or loaded from a bake.** A `NavGround` is triangles - each with an area
byte, 0 not walkable and 1..62 a kind of ground - and what nothing stands in: cylinders (a trunk,
a post) and convex footprints (a boulder). Ground steeper than the slope is cleared whatever its
area. `nav_build` makes one mesh of it; a map is square tiles (`nav_tiled`, then `nav_tile_build`
for each tile from the ground under it and a few metres round), and a tile must be a whole number
of cells across or its seams never join - the shim refuses one that is not. A game bakes its maps
once and loads the bytes (`nav_save_file` / `nav_load_file`, one format for one tile or many): a
map is never built at start-up. Polygon refs are 64-bit, so a map may have 16384 tiles.
- **A path is corners.** `nav_path` answers the way from one point to another as the corners
where it turns, the first the start and the last the end. When the end cannot be reached the
path stops as near it as the mesh allows and `nav_partial` says so - a walker goes there and
gives up, never through a river. Either end off the mesh is `-1`.
- **A wander's goal is somewhere it can go.** `nav_random_near` answers a point about r away that a
walker standing at the start can reach - never across a river without a ford - from a seed the
caller draws from its own `Rng`, so the same seed is the same point on every machine.
- **A kind of ground has a cost** (`nav_area_cost`): a trail cheaper than a meadow, scree dearer.
- **The same question gets the same answer.** Detour is deterministic for the same mesh and the
same points, so co-op's order of dice is untouched by asking it.
@ -32,12 +38,15 @@ import "ludic.nav"
| | |
| --- | --- |
| `NAV_PERSON`, `NAV_LARGE`, `NAV_SMALL`, `NAV_CORNERS` | the kinds of walker, and a path's most corners (256) |
| `NavConfig { cell, cell_h, height, radius, climb, slope }` | what a mesh is built for (a person by default) |
| `nav_build(kind, v, nv, t, nt, area, cyl, nc, config) -> bool`, `nav_polygons(kind)` | a mesh from triangles and cylinders (`x, y, z, r, h` each) |
| `NavConfig { cell, cell_h, height, radius, climb, slope }` | what a mesh is built for (a person by default, 0.25 m cells) |
| `NavGround { v, nv, t, nt, area, cyl, nc, foot, nf }` | what it is built from: triangles, an area byte each, cylinders (`x, y, z, r, h`) and footprints (`n`, n points `x z`, `ymin`, `ymax`) |
| `nav_build(kind, ground, config) -> bool`, `nav_polygons(kind)` | one mesh over all of it |
| `nav_tiled(kind, ox, oz, tile, max_tiles, max_polys) -> bool`, `nav_tile_build(kind, tx, tz, ground, config) -> int` | a map of square tiles, a tile at a time (its polygons, 0 none, -1 failed) |
| `nav_save_file(kind, path)`, `nav_load_file(kind, path)`, `nav_reset()` | a baked mesh written and read; every mesh let go |
| `nav_nearest(kind, x, y, z) -> bool`, `nav_near_x/y/z()` | the nearest walkable point within a couple of metres |
| `nav_path(kind, sx, sy, sz, ex, ey, ez) -> int`, `nav_corners()`, `nav_corner_x/y/z(i)`, `nav_partial(kind)` | a way as corners |
| `nav_straight(kind, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable |
| `nav_random_near(kind, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed |
| `nav_area_cost(kind, area, cost)` | how dear a kind of ground is to cross |
## Tests
@ -46,8 +55,10 @@ import "ludic.nav"
ludic test packages/ludic.nav
```
A 40 m meadow built by hand: a path round a post, over a river's ford, stopping at the bank of a
river with none, the nearest point, and a saved mesh answering as the built one did.
A 40 m meadow built by hand: a path round a post and a boulder's footprint, over a river's ford,
stopping at the bank of a river with none, the nearest point, random points that never cross the
river, and a saved mesh answering as the built one did. And a 128 m meadow as four tiles: a path
across the seams, a tile under water with no polygons, and the set saved and loaded.
## The native library