@aethervtt/shard-nav 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ import { defineOverlay } from "@aethervtt/shard-render";
2
+ import { GlobalTransform } from "@aethervtt/shard-transform";
3
+ import { Nav } from "./state.js";
4
+ function areaColor(area, out) {
5
+ if (area === 0) {
6
+ out[0] = 0.2;
7
+ out[1] = 0.85;
8
+ out[2] = 0.75;
9
+ } else {
10
+ const h = area * 0.618034 % 1;
11
+ out[0] = 0.5 + 0.5 * Math.cos(6.2832 * h);
12
+ out[1] = 0.5 + 0.5 * Math.cos(6.2832 * (h - 0.333));
13
+ out[2] = 0.5 + 0.5 * Math.cos(6.2832 * (h - 0.667));
14
+ }
15
+ out[3] = 1;
16
+ return out;
17
+ }
18
+ const meshLines = /* @__PURE__ */ new WeakMap();
19
+ const gridLines = /* @__PURE__ */ new WeakMap();
20
+ function linesOf(rt) {
21
+ const cached = meshLines.get(rt);
22
+ if (cached && cached.version === rt.version) return cached;
23
+ const lines = [];
24
+ const areas = [];
25
+ const links = [];
26
+ for (const t of rt.tiles.values()) {
27
+ if (!t.ref) continue;
28
+ const tile = rt.navMesh.getTileByRef(t.ref);
29
+ const header = tile?.header();
30
+ if (!tile || !header) continue;
31
+ for (let p = 0; p < header.polyCount(); p++) {
32
+ const poly = tile.polys(p);
33
+ const n = poly.vertCount();
34
+ if (poly.getType() === 1) {
35
+ const a2 = poly.verts(0) * 3;
36
+ const b2 = poly.verts(1) * 3;
37
+ links.push(tile.verts(a2), tile.verts(a2 + 1), tile.verts(a2 + 2));
38
+ links.push(tile.verts(b2), tile.verts(b2 + 1), tile.verts(b2 + 2));
39
+ continue;
40
+ }
41
+ const area = poly.areaAndType() & 63;
42
+ for (let i = 0; i < n; i++) {
43
+ const a2 = poly.verts(i) * 3;
44
+ const b2 = poly.verts((i + 1) % n) * 3;
45
+ lines.push(tile.verts(a2), tile.verts(a2 + 1) + 0.05, tile.verts(a2 + 2));
46
+ lines.push(tile.verts(b2), tile.verts(b2 + 1) + 0.05, tile.verts(b2 + 2));
47
+ areas.push(area);
48
+ }
49
+ }
50
+ }
51
+ const out = {
52
+ version: rt.version,
53
+ lines: Float32Array.from(lines),
54
+ areas: Uint8Array.from(areas),
55
+ count: areas.length,
56
+ links: Float32Array.from(links),
57
+ linkCount: links.length / 6
58
+ };
59
+ meshLines.set(rt, out);
60
+ return out;
61
+ }
62
+ function gridLinesOf(g) {
63
+ const cached = gridLines.get(g);
64
+ if (cached && cached.version === g.version) return cached;
65
+ const grid = g.grid;
66
+ const w = grid.width;
67
+ const h = grid.height;
68
+ const lines = [];
69
+ const open = (x, y) => grid.get(x, y) > 0;
70
+ for (let y = 0; y < h; y++) {
71
+ for (let x = 0; x < w; x++) {
72
+ if (!open(x, y)) continue;
73
+ if (!open(x - 1, y)) lines.push(x, y, x, y + 1);
74
+ if (!open(x + 1, y)) lines.push(x + 1, y, x + 1, y + 1);
75
+ if (!open(x, y - 1)) lines.push(x, y, x + 1, y);
76
+ if (!open(x, y + 1)) lines.push(x, y + 1, x + 1, y + 1);
77
+ }
78
+ }
79
+ const out = { version: g.version, lines: Float32Array.from(lines), count: lines.length / 4 };
80
+ gridLines.set(g, out);
81
+ return out;
82
+ }
83
+ const color = new Float32Array(4);
84
+ const LINK = new Float32Array([1, 0.75, 0.2, 1]);
85
+ const GRID = new Float32Array([0.2, 0.85, 0.75, 1]);
86
+ const PATH = new Float32Array([1, 0.95, 0.3, 1]);
87
+ const UNREACHABLE = new Float32Array([1, 0.35, 0.3, 1]);
88
+ const a = new Float64Array(3);
89
+ const b = new Float64Array(3);
90
+ const mid = new Float64Array(3);
91
+ const MAX_SEGMENTS = 6e4;
92
+ const navOverlay = defineOverlay({
93
+ name: "navmesh",
94
+ description: "Navigation: navmesh polygon edges colored by area, off-mesh links as arcs, NavGrid walkable-area outlines, and every NavAgent\u2019s path (yellow; red when unreachable).",
95
+ draw(world, g, passes) {
96
+ const nav = world.tryResource(Nav);
97
+ if (!nav) return;
98
+ let budget = MAX_SEGMENTS;
99
+ for (const rt of nav.meshes.values()) {
100
+ if (!passes(rt.entity)) continue;
101
+ const m = linesOf(rt);
102
+ const l = m.lines;
103
+ for (let i = 0; i < m.count && budget > 0; i++, budget--) {
104
+ const o = i * 6;
105
+ a[0] = l[o];
106
+ a[1] = l[o + 1];
107
+ a[2] = l[o + 2];
108
+ b[0] = l[o + 3];
109
+ b[1] = l[o + 4];
110
+ b[2] = l[o + 5];
111
+ g.line(a, b, areaColor(m.areas[i], color));
112
+ }
113
+ for (let i = 0; i < m.linkCount; i++) {
114
+ const o = i * 6;
115
+ a[0] = m.links[o];
116
+ a[1] = m.links[o + 1];
117
+ a[2] = m.links[o + 2];
118
+ b[0] = m.links[o + 3];
119
+ b[1] = m.links[o + 4];
120
+ b[2] = m.links[o + 5];
121
+ mid[0] = (a[0] + b[0]) / 2;
122
+ mid[1] = Math.max(a[1], b[1]) + 0.5;
123
+ mid[2] = (a[2] + b[2]) / 2;
124
+ g.line(a, mid, LINK);
125
+ g.line(mid, b, LINK);
126
+ }
127
+ }
128
+ for (const rec of nav.grids.values()) {
129
+ if (!rec.grid || !passes(rec.entity)) continue;
130
+ const gl = gridLinesOf(rec);
131
+ const l = gl.lines;
132
+ for (let i = 0; i < gl.count && budget > 0; i++, budget--) {
133
+ const o = i * 4;
134
+ a[0] = rec.ox + l[o] * rec.csx;
135
+ a[1] = rec.oy + l[o + 1] * rec.csy;
136
+ a[2] = rec.z;
137
+ b[0] = rec.ox + l[o + 2] * rec.csx;
138
+ b[1] = rec.oy + l[o + 3] * rec.csy;
139
+ b[2] = rec.z;
140
+ g.line(a, b, GRID);
141
+ }
142
+ }
143
+ for (const rec of nav.agents.values()) {
144
+ if (rec.count < 2 || !passes(rec.entity) || !world.isAlive(rec.entity)) continue;
145
+ if (!world.has(rec.entity, GlobalTransform)) continue;
146
+ const m = world.get(rec.entity, GlobalTransform).matrix;
147
+ const c = rec.corners;
148
+ const col = rec.reachable ? PATH : UNREACHABLE;
149
+ a[0] = m[3];
150
+ a[1] = m[7] + (rec.kind === "mesh" ? 0.1 : 0);
151
+ a[2] = m[11];
152
+ for (let i = Math.max(1, rec.kind === "grid" ? rec.next : 1); i < rec.count; i++) {
153
+ b[0] = c[i * 3];
154
+ b[1] = c[i * 3 + 1] + (rec.kind === "mesh" ? 0.1 : 0);
155
+ b[2] = c[i * 3 + 2];
156
+ g.line(a, b, col);
157
+ a[0] = b[0];
158
+ a[1] = b[1];
159
+ a[2] = b[2];
160
+ }
161
+ }
162
+ }
163
+ });
164
+ export {
165
+ navOverlay
166
+ };
@@ -0,0 +1,11 @@
1
+ import { type Plugin } from '@aethervtt/shard-runtime';
2
+ /**
3
+ * Navigation (spec 0037): NavGrid A* for 2D, Recast navmeshes baked from NavSource geometry with
4
+ * off-mesh links, path queries, and NavAgents steered by Detour's crowd or grid steering.
5
+ */
6
+ export declare const navPlugin: Plugin;
7
+ /**
8
+ * Grids only: NavGrid, path queries on grids, and grid NavAgents, without loading the Recast
9
+ * WASM. NavMesh entities report `nav/no-navmesh`.
10
+ */
11
+ export declare const navGridPlugin: Plugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,79 @@
1
+ import {
2
+ ChildOf,
3
+ FixedUpdate,
4
+ onAdd,
5
+ onRemove,
6
+ onSet,
7
+ PostUpdate
8
+ } from "@aethervtt/shard-core";
9
+ import { Collider, PhysicsSystems } from "@aethervtt/shard-physics";
10
+ import { Mesh3d } from "@aethervtt/shard-render";
11
+ import { definePlugin } from "@aethervtt/shard-runtime";
12
+ import { TransformSystems } from "@aethervtt/shard-transform";
13
+ import { dropAgent, navAgents } from "./agents.js";
14
+ import { navBake } from "./bake.js";
15
+ import * as cacheModule from "./cache.js";
16
+ import { NavCache } from "./cache.js";
17
+ import * as componentsModule from "./components.js";
18
+ import { NavAgent, NavAreas, NavGridDatas, NavSource, OffMeshLink } from "./components.js";
19
+ import { navMethods } from "./methods.js";
20
+ import * as overlayModule from "./overlay.js";
21
+ import { loadRecast } from "./recast.js";
22
+ import * as stateModule from "./state.js";
23
+ import { Nav } from "./state.js";
24
+ function underSource(world, entity) {
25
+ for (let e = entity, depth = 0; e !== null && depth < 64; depth++) {
26
+ if (!world.isAlive(e)) return false;
27
+ if (world.has(e, NavSource)) return true;
28
+ e = world.tryGet(e, ChildOf)?.parent ?? null;
29
+ }
30
+ return false;
31
+ }
32
+ function observe(world, nav) {
33
+ const structural = () => {
34
+ nav.dirty = true;
35
+ };
36
+ const shape = ({ entity }) => {
37
+ if (nav.watched.has(entity) || underSource(world, entity)) nav.dirty = true;
38
+ };
39
+ for (const def of [NavSource, OffMeshLink]) {
40
+ world.observe(onAdd(def), structural);
41
+ world.observe(onRemove(def), structural);
42
+ }
43
+ for (const def of [Collider, Mesh3d, ChildOf]) {
44
+ world.observe(onAdd(def), shape);
45
+ world.observe(onRemove(def), shape);
46
+ }
47
+ world.observe(onSet(ChildOf), shape);
48
+ world.observe(onRemove(NavAgent), ({ entity }) => dropAgent(world, entity));
49
+ }
50
+ function build(app) {
51
+ const w = app.world;
52
+ const nav = w.initResource(Nav);
53
+ w.initResource(NavAreas);
54
+ w.initResource(NavCache);
55
+ w.initResource(NavGridDatas);
56
+ observe(w, nav);
57
+ app.addSystems(FixedUpdate, navAgents.before(PhysicsSystems));
58
+ app.addSystems(PostUpdate, navBake.after(TransformSystems));
59
+ app.addMethod(...navMethods);
60
+ }
61
+ const navPlugin = definePlugin({
62
+ name: "nav",
63
+ provides: [cacheModule, componentsModule, overlayModule, stateModule],
64
+ dependencies: ["core/transform"],
65
+ build,
66
+ async ready(app) {
67
+ app.world.resource(Nav).R = await loadRecast();
68
+ }
69
+ });
70
+ const navGridPlugin = definePlugin({
71
+ name: "nav/grid",
72
+ provides: [cacheModule, componentsModule, overlayModule, stateModule],
73
+ dependencies: ["core/transform"],
74
+ build
75
+ });
76
+ export {
77
+ navGridPlugin,
78
+ navPlugin
79
+ };
@@ -0,0 +1,83 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { NavMeshRuntime } from './navmesh.js';
3
+ import { type GridRecord } from './state.js';
4
+ export declare const PATH_STATUSES: readonly ['complete', 'partial', 'none'];
5
+ export type PathStatus = (typeof PATH_STATUSES)[number];
6
+ /** A path's corners, reused across queries: pass it as `out` and nothing allocates. */
7
+ export interface NavPath {
8
+ /** x, y, z per corner, start first. */
9
+ corners: Float32Array;
10
+ count: number;
11
+ /** Length along the corners. */
12
+ length: number;
13
+ /** complete: reaches `to`. partial: ends at the closest reachable point. none: no path. */
14
+ status: PathStatus;
15
+ /** The NavGrid or NavMesh entity the path is on. */
16
+ nav: Entity | null;
17
+ }
18
+ export declare function createNavPath(capacity?: number): NavPath;
19
+ export interface FindPathOptions {
20
+ /** Where to write the path (default: a new one). */
21
+ out?: NavPath;
22
+ /** Walk on this NavGrid or NavMesh entity instead of the one containing `from`. */
23
+ nav?: Entity | null;
24
+ /** Plan as this NavAgent: its `nav`, if set. */
25
+ agent?: Entity | null;
26
+ /** Navmesh area costs for this query only, over nav/Areas (0 excludes an area). */
27
+ areas?: Record<number, number>;
28
+ }
29
+ /** Which grid or navmesh a point is in. */
30
+ export type NavTarget = {
31
+ kind: 'grid';
32
+ entity: Entity;
33
+ grid: GridRecord;
34
+ } | {
35
+ kind: 'mesh';
36
+ entity: Entity;
37
+ mesh: NavMeshRuntime;
38
+ };
39
+ /**
40
+ * The grid or navmesh containing a point (grids first, then navmeshes), or undefined. `nav`
41
+ * picks one explicitly.
42
+ */
43
+ export declare function navAt(world: World, point: ArrayLike<number>, nav?: Entity | null): NavTarget | undefined;
44
+ /**
45
+ * A path from `from` to `to` (world space), as corners, on the NavGrid or NavMesh containing
46
+ * `from`. Grid paths are A* pulled straight; mesh paths are Detour's. A goal with no path gives
47
+ * a partial path to the closest reachable point. Writes into `options.out` when given.
48
+ */
49
+ export declare function findPath(world: World, from: ArrayLike<number>, to: ArrayLike<number>, options?: FindPathOptions): NavPath;
50
+ /**
51
+ * A path on a navmesh with every point in its own space (framed navmeshes: the frame's local
52
+ * space). For agents, which steer in that space.
53
+ */
54
+ export declare function meshPathLocal(m: NavMeshRuntime, from: ArrayLike<number>, to: ArrayLike<number>, out: NavPath, areas?: Record<number, number>): void;
55
+ /**
56
+ * The walkable point nearest to `point` on the grid or navmesh containing it, written into
57
+ * `out` (xyz). Returns false when nothing walkable is near.
58
+ */
59
+ export declare function nearestPoint(world: World, point: ArrayLike<number>, out: {
60
+ [i: number]: number;
61
+ }, options?: {
62
+ nav?: Entity | null;
63
+ }): boolean;
64
+ /** Where `navRaycast` stopped. */
65
+ export interface NavRayHit {
66
+ /** 0..1 along the segment; 1: clear to `to`. */
67
+ t: number;
68
+ point: Float64Array;
69
+ /** Edge normal of the wall hit (xyz), zero when clear. */
70
+ normal: Float64Array;
71
+ }
72
+ export declare function createNavRayHit(): NavRayHit;
73
+ /**
74
+ * Walks along the walkable surface from `from` toward `to` and stops at the first wall or
75
+ * blocked cell. Returns true when it hit something (`hit.t` < 1); `hit.point` is where it stopped.
76
+ */
77
+ export declare function navRaycast(world: World, from: ArrayLike<number>, to: ArrayLike<number>, hit?: NavRayHit, options?: {
78
+ nav?: Entity | null;
79
+ }): boolean;
80
+ /** An entity's world position (xyz) from its GlobalTransform. */
81
+ export declare function worldPos(world: World, e: Entity, out: {
82
+ [i: number]: number;
83
+ }): void;