@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,119 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { TileLayer } from '@aethervtt/shard-sprite';
3
+ import type { GridSource } from './components.js';
4
+ import { TriangleSoup } from './geometry.js';
5
+ import { GridSearch, type NavGridData } from './grid.js';
6
+ import type { NavMeshRuntime } from './navmesh.js';
7
+ import type { OffMeshLinkParams, Recast } from './recast.js';
8
+ /** A NavGrid entity's effective grid: its cells and where they sit in the world. */
9
+ export interface GridRecord {
10
+ entity: Entity;
11
+ source: GridSource;
12
+ /** The cells. Built from the tilemap or colliders, or the data asset itself. */
13
+ grid: NavGridData | null;
14
+ /** World XY of cell (0, 0)'s lower-left corner, and cell size. */
15
+ ox: number;
16
+ oy: number;
17
+ csx: number;
18
+ csy: number;
19
+ /** The entity's world z: grid corners get it. */
20
+ z: number;
21
+ diagonal: number;
22
+ /** Why the grid is empty, if it is. */
23
+ problem: string | null;
24
+ /** The component's fields when last built, to rebuild on edits. */
25
+ stamp: string;
26
+ /** tilemap: the layer and how far into its edit log the grid has read. */
27
+ layer: TileLayer | null;
28
+ editBase: number;
29
+ editCursor: number;
30
+ tilemapVersion: number;
31
+ /** tilemap: tile number → blocks (1). Empty: every non-empty tile blocks. */
32
+ blocking: Uint8Array | null;
33
+ /** colliders: physics collider count and tick when last rasterized. */
34
+ colliderStamp: string;
35
+ /** Bumps when cells change, so agents on the grid repath. */
36
+ version: number;
37
+ /** The grid's `version` last seen (data grids change underneath). */
38
+ seen: number;
39
+ }
40
+ /** A NavAgent's runtime side: its path, crowd slot, and repath bookkeeping. */
41
+ export interface AgentRecord {
42
+ entity: Entity;
43
+ /** The NavGrid or NavMesh entity it walks on, or -1. */
44
+ nav: Entity;
45
+ kind: 'grid' | 'mesh' | 'none';
46
+ /** Crowd agent index on a mesh, or -1. */
47
+ crowdIndex: number;
48
+ /** Corners of the current path (xyz), and how many; grid agents walk them. */
49
+ corners: Float32Array;
50
+ count: number;
51
+ /** Next corner to walk to (grid agents). */
52
+ next: number;
53
+ /** Where the current path goes. */
54
+ goal: Float64Array;
55
+ /** Navigation version the path was made on. */
56
+ navVersion: number;
57
+ sinceRepath: number;
58
+ /** The path reaches the goal (false: partial, heading for the closest point). */
59
+ reachable: boolean;
60
+ /** Last status sent, for events on changes. */
61
+ status: number;
62
+ /** The NavAgent tick the path was made for: writing NavAgent repaths. */
63
+ agentTick: number;
64
+ /** The drive actually used (character without a CharacterController drives the transform). */
65
+ drive: number;
66
+ /** On a framed navmesh: steering happens in its space, from this position (set each step). */
67
+ framed: boolean;
68
+ lpos: Float64Array;
69
+ /**
70
+ * Crossing an off-mesh link: where it started (x, z), the link's near and far surface
71
+ * heights and far end (x, z), how high the entity sits above the surface (a character's capsule
72
+ * center), and the height of the jump arc.
73
+ */
74
+ link: {
75
+ active: boolean;
76
+ lift: number;
77
+ x0: number;
78
+ z0: number;
79
+ y0: number;
80
+ x1: number;
81
+ z1: number;
82
+ y1: number;
83
+ arc: number;
84
+ };
85
+ }
86
+ export declare class NavState {
87
+ /** Recast and Detour, once loaded (the nav plugin loads them; nav/grid doesn't). */
88
+ R: Recast | null;
89
+ readonly grids: Map<number, GridRecord>;
90
+ readonly meshes: Map<number, NavMeshRuntime>;
91
+ readonly agents: Map<number, AgentRecord>;
92
+ /** `grids` and `meshes` as arrays, for per-frame loops (Map iteration allocates). */
93
+ gridList: GridRecord[];
94
+ meshList: NavMeshRuntime[];
95
+ readonly search: GridSearch;
96
+ readonly soup: TriangleSoup;
97
+ links: OffMeshLinkParams[];
98
+ /** Entities whose changes rebake navmeshes: sources, their descendants, and link ends. */
99
+ readonly watched: Set<number>;
100
+ /** `watched` as an array, for the per-frame change check. */
101
+ watchedList: Entity[];
102
+ /** Two 32-bit hash lanes per soup triangle, combined into tile keys. */
103
+ triHashes: Uint32Array<ArrayBuffer>;
104
+ /** Something structural changed (a source added or removed): gather and bake. */
105
+ dirty: boolean;
106
+ /** Signature of the nav/Areas costs last applied (NaN: none yet). */
107
+ areas: number;
108
+ /** Colliders: count and tick of the last change seen (grids rasterized from them rebuild). */
109
+ colliderCount: number;
110
+ colliderTick: number;
111
+ /** Source entities skipped by the last gather, with why. */
112
+ skipped: {
113
+ entity: Entity;
114
+ reason: string;
115
+ }[];
116
+ }
117
+ export declare const Nav: import("@aethervtt/shard-core").ResourceDef<NavState>;
118
+ /** The navigation state, or `nav/not-ready` without a nav plugin. */
119
+ export declare function navState(world: World): NavState;
package/dist/state.js ADDED
@@ -0,0 +1,49 @@
1
+ import { defineResource, ShardError } from "@aethervtt/shard-core";
2
+ import { TriangleSoup } from "./geometry.js";
3
+ import { GridSearch } from "./grid.js";
4
+ class NavState {
5
+ /** Recast and Detour, once loaded (the nav plugin loads them; nav/grid doesn't). */
6
+ R = null;
7
+ grids = /* @__PURE__ */ new Map();
8
+ meshes = /* @__PURE__ */ new Map();
9
+ agents = /* @__PURE__ */ new Map();
10
+ /** `grids` and `meshes` as arrays, for per-frame loops (Map iteration allocates). */
11
+ gridList = [];
12
+ meshList = [];
13
+ search = new GridSearch();
14
+ soup = new TriangleSoup();
15
+ links = [];
16
+ /** Entities whose changes rebake navmeshes: sources, their descendants, and link ends. */
17
+ watched = /* @__PURE__ */ new Set();
18
+ /** `watched` as an array, for the per-frame change check. */
19
+ watchedList = [];
20
+ /** Two 32-bit hash lanes per soup triangle, combined into tile keys. */
21
+ triHashes = new Uint32Array(0);
22
+ /** Something structural changed (a source added or removed): gather and bake. */
23
+ dirty = true;
24
+ /** Signature of the nav/Areas costs last applied (NaN: none yet). */
25
+ areas = Number.NaN;
26
+ /** Colliders: count and tick of the last change seen (grids rasterized from them rebuild). */
27
+ colliderCount = -1;
28
+ colliderTick = 0;
29
+ /** Source entities skipped by the last gather, with why. */
30
+ skipped = [];
31
+ }
32
+ const Nav = defineResource("nav/State", {
33
+ description: "Navigation runtime: grids, navmeshes, agents.",
34
+ init: () => new NavState()
35
+ });
36
+ function navState(world) {
37
+ const s = world.tryResource(Nav);
38
+ if (!s) {
39
+ throw new ShardError("nav/not-ready", "Navigation is not set up", {
40
+ hint: "Add the nav plugin (or nav/grid for grids only) and await app.init()."
41
+ });
42
+ }
43
+ return s;
44
+ }
45
+ export {
46
+ Nav,
47
+ NavState,
48
+ navState
49
+ };
@@ -0,0 +1,19 @@
1
+ import type { Entity, World } from '@aethervtt/shard-core';
2
+ import { App, type Plugin } from '@aethervtt/shard-runtime';
3
+ export declare function navApp(...plugins: Plugin[]): Promise<App>;
4
+ export declare function frames(a: App, n: number): void;
5
+ /** A box whose top face is at `top`, centered on (x, z). */
6
+ export declare function slab(world: World, x: number, z: number, hx: number, hz: number, top: number, area?: number): Entity;
7
+ /**
8
+ * A ramp `width` wide rising toward -z from (x, 0, z0) at `degrees`, until it reaches `rise`.
9
+ * Its top surface passes through (x, 0, z0).
10
+ */
11
+ export declare function ramp(world: World, x: number, z0: number, degrees: number, rise: number, width?: number): Entity;
12
+ /**
13
+ * Two floors: ground (40 × 40, top y = 0) and an upper floor (top y = 3) north of it, joined by a
14
+ * 27° ramp. A second platform (also y = 3) is only reachable by a 60° ramp.
15
+ */
16
+ export declare function level(world: World): {
17
+ upper: [number, number, number];
18
+ steep: [number, number, number];
19
+ };
@@ -0,0 +1,55 @@
1
+ import { Collider } from "@aethervtt/shard-physics";
2
+ import { App } from "@aethervtt/shard-runtime";
3
+ import { Transform, TransformPlugin } from "@aethervtt/shard-transform";
4
+ import { NavSource } from "./components.js";
5
+ import { navPlugin } from "./plugin.js";
6
+ const DT = 1 / 60;
7
+ async function navApp(...plugins) {
8
+ const a = new App().addPlugin(TransformPlugin, navPlugin, ...plugins);
9
+ await a.init();
10
+ return a;
11
+ }
12
+ function frames(a, n) {
13
+ for (let i = 0; i < n; i++) a.update(DT);
14
+ }
15
+ function slab(world, x, z, hx, hz, top, area = 0) {
16
+ return world.spawn(
17
+ [NavSource, { area }],
18
+ [Collider, { shape: "cuboid", halfExtents: [hx, 0.5, hz] }],
19
+ [Transform, { translation: [x, top - 0.5, z] }]
20
+ );
21
+ }
22
+ function ramp(world, x, z0, degrees, rise, width = 3) {
23
+ const a = degrees * Math.PI / 180;
24
+ const half = rise / Math.sin(a) / 2;
25
+ return world.spawn(
26
+ [NavSource, {}],
27
+ [Collider, { shape: "cuboid", halfExtents: [width / 2, 0.25, half] }],
28
+ [
29
+ Transform,
30
+ {
31
+ translation: [
32
+ x,
33
+ Math.sin(a) * half - Math.cos(a) * 0.25,
34
+ z0 - Math.cos(a) * half - Math.sin(a) * 0.25
35
+ ],
36
+ rotation: [Math.sin(a / 2), 0, 0, Math.cos(a / 2)]
37
+ }
38
+ ]
39
+ );
40
+ }
41
+ function level(world) {
42
+ slab(world, 0, 0, 20, 20, 0);
43
+ slab(world, 6, -12, 4, 4, 3);
44
+ ramp(world, 6, -2, 26.57, 3);
45
+ slab(world, -8, -12, 3, 3, 3);
46
+ ramp(world, -8, -6, 60, 3);
47
+ return { upper: [6, 3, -12], steep: [-8, 3, -12] };
48
+ }
49
+ export {
50
+ frames,
51
+ level,
52
+ navApp,
53
+ ramp,
54
+ slab
55
+ };
@@ -0,0 +1,8 @@
1
+ import { NavGridData } from './grid.js';
2
+ /** A small deterministic PRNG (mulberry32). */
3
+ export declare function rng(seed: number): () => number;
4
+ /**
5
+ * A size × size maze: rooms on odd cells, carved by a randomized depth-first search, then
6
+ * `loops` extra walls knocked out so there's more than one way through. Some corridors cost 3.
7
+ */
8
+ export declare function maze(size: number, seed: number, loops?: number): NavGridData;
@@ -0,0 +1,56 @@
1
+ import { NavGridData } from "./grid.js";
2
+ function rng(seed) {
3
+ let s = seed >>> 0;
4
+ return () => {
5
+ s = s + 1831565813 >>> 0;
6
+ let x = s;
7
+ x = Math.imul(x ^ x >>> 15, x | 1);
8
+ x ^= x + Math.imul(x ^ x >>> 7, x | 61);
9
+ return ((x ^ x >>> 14) >>> 0) / 4294967296;
10
+ };
11
+ }
12
+ function maze(size, seed, loops = 0.08) {
13
+ const grid = new NavGridData(size, size, new Uint8Array(size * size));
14
+ const random = rng(seed);
15
+ const stack = [1 + size];
16
+ grid.costs[1 + size] = 1;
17
+ while (stack.length > 0) {
18
+ const cur = stack[stack.length - 1];
19
+ const x = cur % size;
20
+ const y = (cur - x) / size;
21
+ const options = [];
22
+ for (const [dx2, dy2] of [
23
+ [2, 0],
24
+ [-2, 0],
25
+ [0, 2],
26
+ [0, -2]
27
+ ]) {
28
+ const nx = x + dx2;
29
+ const ny = y + dy2;
30
+ if (nx > 0 && ny > 0 && nx < size - 1 && ny < size - 1 && grid.costs[ny * size + nx] === 0)
31
+ options.push(dx2, dy2);
32
+ }
33
+ if (options.length === 0) {
34
+ stack.pop();
35
+ continue;
36
+ }
37
+ const k = Math.floor(random() * (options.length / 2)) * 2;
38
+ const dx = options[k];
39
+ const dy = options[k + 1];
40
+ const cost = random() < 0.1 ? 3 : 1;
41
+ grid.costs[(y + dy / 2) * size + x + dx / 2] = cost;
42
+ grid.costs[(y + dy) * size + x + dx] = cost;
43
+ stack.push((y + dy) * size + x + dx);
44
+ }
45
+ for (let y = 1; y < size - 1; y++) {
46
+ for (let x = 1; x < size - 1; x++) {
47
+ if (grid.costs[y * size + x] === 0 && (x + y) % 2 === 1 && random() < loops)
48
+ grid.costs[y * size + x] = 1;
49
+ }
50
+ }
51
+ return grid;
52
+ }
53
+ export {
54
+ maze,
55
+ rng
56
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@aethervtt/shard-nav",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "LICENSE",
16
+ "README.md"
17
+ ],
18
+ "dependencies": {
19
+ "@aethervtt/shard-assets": "0.0.1",
20
+ "@aethervtt/shard-core": "0.0.1",
21
+ "@aethervtt/shard-mesh": "0.0.1",
22
+ "@aethervtt/shard-physics": "0.0.1",
23
+ "@aethervtt/shard-platform": "0.0.1",
24
+ "@aethervtt/shard-render": "0.0.1",
25
+ "@aethervtt/shard-runtime": "0.0.1",
26
+ "@aethervtt/shard-sprite": "0.0.1",
27
+ "@aethervtt/shard-transform": "0.0.1",
28
+ "recast-navigation": "0.43.1"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/aethervtt/shard.git",
33
+ "directory": "packages/nav"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "provenance": true
38
+ },
39
+ "shard": {
40
+ "buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
41
+ }
42
+ }