feat(nav): 17.4 - a mesh has eight filters (a cost per kind of ground each) and every path, straight line and random point names the one it walks by, so a deer, a marmot and a person each take their own way over the same mesh; tested with a band of thicket walked round by the filter that dislikes it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 21:34:23 +03:00
parent f0485a20a8
commit d4b6979426
10 changed files with 82 additions and 57 deletions

View file

@ -29,7 +29,10 @@ import "ludic.nav"
- **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.
- **A kind of ground has a cost, and a walker has a taste.** A mesh carries `NAV_FILTERS` (8) sets of
costs per area (`nav_area_cost(kind, filter, area, cost)`), and every path, straight line and
random point names the filter it walks by: a deer's dislikes scree, a marmot's likes it, a
person's prefers the town. The nearest point uses filter 0.
- **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.
@ -37,17 +40,17 @@ import "ludic.nav"
| | |
| --- | --- |
| `NAV_PERSON`, `NAV_LARGE`, `NAV_SMALL`, `NAV_CORNERS` | the kinds of walker, and a path's most corners (256) |
| `NAV_PERSON`, `NAV_LARGE`, `NAV_SMALL`, `NAV_FILTERS`, `NAV_CORNERS` | the kinds of walker, the filters a mesh has (8), 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, 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 |
| `nav_path(kind, filter, 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, 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_area_cost(kind, filter, area, cost)` | how dear a kind of ground is to cross by that filter |
## Tests
@ -55,7 +58,8 @@ import "ludic.nav"
ludic test packages/ludic.nav
```
A 40 m meadow built by hand: a path round a post and a boulder's footprint, over a river's ford,
A 40 m meadow built by hand: a path round a post and a boulder's footprint, round a band of thicket
by a filter that dislikes it and straight through by the plain one, 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.

Binary file not shown.

View file

@ -8,8 +8,8 @@ extern function nvc_load(buf: pointer, size: int) -> pointer = "nav_load"
extern function nvc_free(h: pointer) -> void = "nav_free"
extern function nvc_polys(h: pointer) -> int = "nav_polys"
extern function nvc_nearest(h: pointer, x: float, y: float, z: float, out: pointer) -> int = "nav_nearest"
extern function nvc_area_cost(h: pointer, area: int, cost: float) -> void = "nav_area_cost"
extern function nvc_path(h: pointer, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path"
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_raycast(h: pointer, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast"
extern function nvc_random_near(h: pointer, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near"
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"

View file

@ -3,7 +3,7 @@
static dtPolyRef nav_poly_at(Nav *n, const float *p, float *on) {
dtPolyRef r = 0;
if (dtStatusFailed(n->query->findNearestPoly(p, n->ext, &n->filter, &r, on))) return 0;
if (dtStatusFailed(n->query->findNearestPoly(p, n->ext, nav_filter(n, 0), &r, on))) return 0;
return r;
}
@ -13,23 +13,23 @@ NAV_SHIM int nav_nearest(void *h, float x, float y, float z, float *out) {
return nav_poly_at(static_cast<Nav *>(h), p, out) ? 1 : 0;
}
// a cost per kind of ground (area 1..62): the filter every query uses
NAV_SHIM void nav_area_cost(void *h, int area, float cost) {
// filter f's cost for a kind of ground (area 1..62); a query names the filter it walks by
NAV_SHIM void nav_area_cost(void *h, int f, int area, float cost) {
Nav *n = static_cast<Nav *>(h);
if (area > 0 && area < DT_MAX_AREAS) n->filter.setAreaCost(area, cost);
if (f >= 0 && f < NAV_FILTERS && area > 0 && area < DT_MAX_AREAS) n->filters[f].setAreaCost(area, 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, float sx, float sy, float sz, float ex, float ey, float ez, float *out, int max) {
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<Nav *>(h);
float s[3] = {sx, sy, sz}, e[3] = {ex, ey, ez}, so[3], eo[3];
dtPolyRef a = nav_poly_at(n, s, so), b = nav_poly_at(n, e, eo);
n->partial = 0;
if (!a || !b) return -1;
int np = 0;
dtStatus st = n->query->findPath(a, b, so, eo, &n->filter, n->polys, &np, NAV_MAX_POLYS);
dtStatus st = n->query->findPath(a, b, so, eo, nav_filter(n, f), n->polys, &np, NAV_MAX_POLYS);
if (dtStatusFailed(st) || np == 0) return -1;
if (dtStatusDetail(st, DT_PARTIAL_RESULT) || n->polys[np - 1] != b) n->partial = 1;
float end[3];
@ -42,13 +42,13 @@ NAV_SHIM int nav_path(void *h, float sx, float sy, float sz, float ex, float ey,
NAV_SHIM int nav_partial(void *h) { return static_cast<Nav *>(h)->partial; }
// does the straight line from one point to another stay on the mesh? 1 yes, 0 no, -1 off it
NAV_SHIM int nav_raycast(void *h, float sx, float sy, float sz, float ex, float ez) {
NAV_SHIM int nav_raycast(void *h, int f, float sx, float sy, float sz, float ex, float ez) {
Nav *n = static_cast<Nav *>(h);
float s[3] = {sx, sy, sz}, so[3], e[3] = {ex, sy, ez}, t = 0, nrm[3];
dtPolyRef a = nav_poly_at(n, s, so);
if (!a) return -1;
int np = 0;
if (dtStatusFailed(n->query->raycast(a, so, e, &n->filter, &t, nrm, n->polys, &np, NAV_MAX_POLYS))) return -1;
if (dtStatusFailed(n->query->raycast(a, so, e, nav_filter(n, f), &t, nrm, n->polys, &np, NAV_MAX_POLYS))) return -1;
return t >= 1.0f ? 1 : 0;
}
@ -60,13 +60,13 @@ static float nav_frand() {
nav_rs = nav_rs * 1664525u + 1013904223u;
return (float)(nav_rs >> 8) * (1.0f / 16777216.0f);
}
NAV_SHIM int nav_random_near(void *h, float x, float y, float z, float r, int seed, float *out) {
NAV_SHIM int nav_random_near(void *h, int f, float x, float y, float z, float r, int seed, float *out) {
Nav *n = static_cast<Nav *>(h);
float p[3] = {x, y, z}, on[3];
dtPolyRef start = nav_poly_at(n, p, on);
if (!start) return 0;
nav_rs = (unsigned int)seed * 2654435761u + 1u;
dtPolyRef ref = 0;
if (dtStatusFailed(n->query->findRandomPointAroundCircle(start, on, r, &n->filter, nav_frand, &ref, out))) return 0;
if (dtStatusFailed(n->query->findRandomPointAroundCircle(start, on, r, nav_filter(n, f), nav_frand, &ref, out))) return 0;
return ref ? 1 : 0;
}

View file

@ -19,12 +19,13 @@
namespace {
const int NAV_MAX_POLYS = 512; // polygons a path may cross; a longer one is cut there
const int NAV_MAX_NODES = 4096; // the search's open list
const int NAV_FILTERS = 8; // costs per kind of ground: one set per kind of walker's taste
// a navmesh and the one query that walks it, with the scratch a path needs
struct Nav {
dtNavMesh *mesh = nullptr;
dtNavMeshQuery *query = nullptr;
dtQueryFilter filter;
dtQueryFilter filters[NAV_FILTERS];
float ext[3] = {2.0f, 4.0f, 2.0f}; // how far to look for the mesh around a point
dtPolyRef polys[NAV_MAX_POLYS];
int partial = 0; // the last path stopped short of its end
@ -36,8 +37,10 @@ Nav *nav_open(dtNavMesh *mesh) {
n->mesh = mesh;
n->query = dtAllocNavMeshQuery();
if (!n->query || dtStatusFailed(n->query->init(n->mesh, NAV_MAX_NODES))) { dtFreeNavMeshQuery(n->query); dtFreeNavMesh(n->mesh); delete n; return nullptr; }
n->filter.setIncludeFlags(0xffff);
n->filter.setExcludeFlags(0);
for (int f = 0; f < NAV_FILTERS; ++f) {
n->filters[f].setIncludeFlags(0xffff);
n->filters[f].setExcludeFlags(0);
}
return n;
}
Nav *nav_from(unsigned char *data, int size) {
@ -45,6 +48,8 @@ Nav *nav_from(unsigned char *data, int size) {
if (!mesh || dtStatusFailed(mesh->init(data, size, DT_TILE_FREE_DATA))) { dtFree(data); dtFreeNavMesh(mesh); return nullptr; }
return nav_open(mesh);
}
// filter f, the first when f is out of range
const dtQueryFilter *nav_filter(Nav *n, int f) { return &n->filters[f >= 0 && f < NAV_FILTERS ? f : 0]; }
} // namespace
#include "nav_build.inl"

View file

@ -13,19 +13,19 @@ export function nav_near_z(nav_st: NavState) -> float { return nav_st.nv_near[2]
# a point about r from (x, y, z) that a walker standing there can reach (a wander's goal), read back
# as the nearest is; the seed is the caller's dice, so the same seed is the same point everywhere
export function nav_random_near(nav_st: NavState, kind: int, x: float, y: float, z: float, r: float, seed: int) -> bool {
export function nav_random_near(nav_st: NavState, kind: int, filter: int, x: float, y: float, z: float, r: float, seed: int) -> bool {
let h = nv_mesh(nav_st, kind)
if h == null { return false }
return nvc_random_near(h, x, y, z, r, seed, nav_st.nv_near) == 1
return nvc_random_near(h, filter, x, y, z, r, seed, nav_st.nv_near) == 1
}
# a way from one point to another: its corners, first the start and last the end (or as near the end
# as the mesh reaches - nav_partial says which); -1 when either end is off the mesh
export function nav_path(nav_st: mut NavState, kind: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> int {
export function nav_path(nav_st: mut NavState, kind: int, filter: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> int {
let h = nv_mesh(nav_st, kind)
nav_st.nv_count = 0
if h == null { return -1 }
let n = nvc_path(h, sx, sy, sz, ex, ey, ez, nav_st.nv_out, NAV_CORNERS)
let n = nvc_path(h, filter, sx, sy, sz, ex, ey, ez, nav_st.nv_out, NAV_CORNERS)
if n > 0 { nav_st.nv_count = n }
return n
}
@ -39,13 +39,14 @@ export function nav_corner_y(nav_st: NavState, i: int) -> float { return nav_st.
export function nav_corner_z(nav_st: NavState, i: int) -> float { return nav_st.nv_out[i * 3 + 2] }
# does the straight line from (sx, sy, sz) to (ex, ez) stay walkable?
export function nav_straight(nav_st: NavState, kind: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> bool {
export function nav_straight(nav_st: NavState, kind: int, filter: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> bool {
let h = nv_mesh(nav_st, kind)
return h != null and nvc_raycast(h, sx, sy, sz, ex, ez) == 1
return h != null and nvc_raycast(h, filter, sx, sy, sz, ex, ez) == 1
}
# how dear a kind of ground is to cross (1 is plain; a trail cheaper, scree dearer)
export function nav_area_cost(nav_st: NavState, kind: int, area: int, cost: float) -> void {
# how dear a kind of ground is to cross by filter f (1 is plain; a town cheaper for a person, scree
# dearer for a deer): a query names the filter it walks by, 0 .. NAV_FILTERS - 1
export function nav_area_cost(nav_st: NavState, kind: int, filter: int, area: int, cost: float) -> void {
let h = nv_mesh(nav_st, kind)
if h != null { nvc_area_cost(h, area, cost) }
if h != null { nvc_area_cost(h, filter, area, cost) }
}

View file

@ -4,6 +4,7 @@ export const NAV_PERSON: int = 0 # 0.35 m wide, steps what a hiker steps (C
export const NAV_LARGE: int = 1 # an elk, a bear, a horse
export const NAV_SMALL: int = 2 # a hare, a marmot
export const NAV_KINDS: int = 3
export const NAV_FILTERS: int = 8 # costs per kind of ground, one set per taste (nav_area_cost)
export const NAV_CORNERS: int = 256 # a path's corners at most; a longer one is cut there
# what a mesh is built for: its voxels (cell across, cell high) and its walker (height, radius,

View file

@ -1,5 +1,6 @@
# fakes/meadow.ludic - ground built by hand for ludic.nav's tests: n metre squares a side, flat at
# y = 0; river 1 crosses x 18..22 with a ford at z 30..34, river 2 without one, river 0 is none
# y = 0; river 1 crosses x 18..22 with a ford at z 30..34, river 2 without one, river 0 is none;
# 3 is no river but a band of area 2 (thicket) over the same x for z under 30
function ground(n: int, river: int) -> NavGround {
let g = new NavGround
g.nv = (n + 1) * (n + 1)
@ -24,7 +25,8 @@ function ground(n: int, river: int) -> NavGround {
push(g.t, a + n + 1)
push(g.t, a + n + 2)
var k = 1
if river > 0 and i >= 18 and i < 22 and not (river == 1 and j >= 30 and j < 34) { k = 0 }
if river > 0 and river < 3 and i >= 18 and i < 22 and not (river == 1 and j >= 30 and j < 34) { k = 0 }
if river == 3 and i >= 18 and i < 22 and j < 30 { k = 2 }
g.area[(j * n + i) * 2] = k
g.area[(j * n + i) * 2 + 1] = k
}

View file

@ -19,7 +19,7 @@ program NavTest {
test "a path goes round a post, and the straight line through it is not walkable" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 0, true))
expect(nav_polygons(nav_st, NAV_PERSON) > 0)
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0)
let n = nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0)
expect(n >= 3)
expect(not nav_partial(nav_st, NAV_PERSON))
expect(Math.abs(last_x(nav_st) - 35.0) < 0.1)
@ -28,13 +28,13 @@ program NavTest {
let dz = nav_corner_z(nav_st, i) - 20.0
expect(dx * dx + dz * dz > 2.0 * 2.0)
}
expect(not nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 20.0))
expect(nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 5.0, 35.0, 5.0))
expect(not nav_straight(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 20.0))
expect(nav_straight(nav_st, NAV_PERSON, 0, 5.0, 0.0, 5.0, 35.0, 5.0))
}
test "a river is crossed at its ford" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 1, false))
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
let n = nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
expect(n >= 3)
expect(not nav_partial(nav_st, NAV_PERSON))
var ford = false
@ -44,7 +44,7 @@ program NavTest {
test "without a ford the path stops at the near bank and says so" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 2, false))
let n = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
let n = nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
expect(n >= 1)
expect(nav_partial(nav_st, NAV_PERSON))
expect(last_x(nav_st) < 18.0)
@ -56,33 +56,45 @@ program NavTest {
expect(Math.abs(nav_near_y(nav_st)) < 0.3)
expect(Math.abs(nav_near_x(nav_st) - 10.0) < 0.1)
expect(not nav_nearest(nav_st, NAV_PERSON, -50.0, 0.0, -50.0))
expect_eq(nav_path(nav_st, NAV_PERSON, -50.0, 0.0, -50.0, 10.0, 0.0, 10.0), -1)
expect_eq(nav_path(nav_st, NAV_LARGE, 5.0, 0.0, 5.0, 10.0, 0.0, 10.0), -1)
expect_eq(nav_path(nav_st, NAV_PERSON, 0, -50.0, 0.0, -50.0, 10.0, 0.0, 10.0), -1)
expect_eq(nav_path(nav_st, NAV_LARGE, 0, 5.0, 0.0, 5.0, 10.0, 0.0, 10.0), -1)
}
test "a random point near is always one a walker there can reach, and a seed is always the same point" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 2, false))
for seed in 0 .. 50 {
expect(nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, seed))
expect(nav_random_near(nav_st, NAV_PERSON, 0, 5.0, 0.0, 10.0, 30.0, seed))
expect(nav_near_x(nav_st) < 18.0)
}
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
nav_random_near(nav_st, NAV_PERSON, 0, 5.0, 0.0, 10.0, 30.0, 7)
let x7 = nav_near_x(nav_st)
let z7 = nav_near_z(nav_st)
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 8)
nav_random_near(nav_st, NAV_PERSON, 0, 5.0, 0.0, 10.0, 30.0, 8)
expect(Math.abs(nav_near_x(nav_st) - x7) + Math.abs(nav_near_z(nav_st) - z7) > 0.01)
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
nav_random_near(nav_st, NAV_PERSON, 0, 5.0, 0.0, 10.0, 30.0, 7)
expect_eq(nav_near_x(nav_st), x7)
expect_eq(nav_near_z(nav_st), z7)
}
test "a filter that dislikes a band of thicket walks round it; the plain one goes through" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 3, false))
nav_area_cost(nav_st, NAV_PERSON, 1, 2, 20.0)
nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
expect_eq(nav_corners(nav_st), 2)
let n = nav_path(nav_st, NAV_PERSON, 1, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
var round = false
for i in 0 .. n { if nav_corner_z(nav_st, i) > 29.0 { round = true } }
expect(round)
expect(not nav_partial(nav_st, NAV_PERSON))
}
test "a boulder's footprint is walked round" (nav_st: mut NavState) {
let g = ground(40, 0)
g.foot = square_at(20.0, 20.0, 3.0)
g.nf = len(g.foot)
expect(nav_build(nav_st, NAV_PERSON, g, new NavConfig))
expect(not nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 20.0))
expect(nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0) >= 3)
expect(not nav_straight(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 20.0))
expect(nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0) >= 3)
}
test "a mesh saved and loaded answers as the built one did" (nav_st: mut NavState) {
@ -90,10 +102,10 @@ program NavTest {
let path = Os.temp_dir() + "/ludic_nav_test.navmesh"
expect(nav_save_file(nav_st, NAV_PERSON, path))
expect(nav_load_file(nav_st, NAV_SMALL, path))
let a = nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
let a = nav_path(nav_st, NAV_PERSON, 0, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
let ax = last_x(nav_st)
let az = last_z(nav_st)
let b = nav_path(nav_st, NAV_SMALL, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
let b = nav_path(nav_st, NAV_SMALL, 0, 5.0, 0.0, 10.0, 35.0, 0.0, 10.0)
expect_eq(a, b)
expect_near(last_x(nav_st), ax, 0.0001)
expect_near(last_z(nav_st), az, 0.0001)

View file

@ -33,11 +33,11 @@ program TilesTest {
test "a path crosses the tiles' seams and goes round the post in the far tile" (nav_st: mut NavState) {
expect_eq(map(nav_st, false), 0)
expect(nav_polygons(nav_st, NAV_PERSON) > 0)
let n = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
let n = nav_path(nav_st, NAV_PERSON, 0, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
expect(n >= 3)
expect(not nav_partial(nav_st, NAV_PERSON))
expect(Math.abs(last_x(nav_st) - 120.0) < 0.1)
let d = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 120.0, 0.0, 120.0)
let d = nav_path(nav_st, NAV_PERSON, 0, 10.0, 0.0, 10.0, 120.0, 0.0, 120.0)
expect(d >= 2)
expect(not nav_partial(nav_st, NAV_PERSON))
}
@ -45,8 +45,8 @@ program TilesTest {
test "a tile under water has no polygons: a way into it is none, the shore beside it is reached" (nav_st: mut NavState) {
expect_eq(map(nav_st, true), 1)
expect(not nav_nearest(nav_st, NAV_PERSON, 100.0, 0.0, 100.0))
expect_eq(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 70.0, 0.0, 70.0), -1)
expect(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 62.0, 0.0, 70.0) >= 2)
expect_eq(nav_path(nav_st, NAV_PERSON, 0, 10.0, 0.0, 10.0, 70.0, 0.0, 70.0), -1)
expect(nav_path(nav_st, NAV_PERSON, 0, 10.0, 0.0, 10.0, 62.0, 0.0, 70.0) >= 2)
expect(not nav_partial(nav_st, NAV_PERSON))
expect(Math.abs(last_z(nav_st) - 70.0) < 0.1)
}
@ -57,9 +57,9 @@ program TilesTest {
expect(nav_save_file(nav_st, NAV_PERSON, path))
expect(nav_load_file(nav_st, NAV_LARGE, path))
expect_eq(nav_polygons(nav_st, NAV_LARGE), nav_polygons(nav_st, NAV_PERSON))
let a = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
let a = nav_path(nav_st, NAV_PERSON, 0, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
let ax = last_x(nav_st)
let b = nav_path(nav_st, NAV_LARGE, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
let b = nav_path(nav_st, NAV_LARGE, 0, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
expect_eq(a, b)
expect_near(last_x(nav_st), ax, 0.0001)
}