@xtia/grid 0.0.9 → 0.0.11
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.
- package/README.md +31 -8
- package/lib/grid.d.ts +4 -3
- package/lib/grid.js +30 -33
- package/lib/utils.d.ts +0 -8
- package/lib/utils.js +0 -9
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ const mask = (x: number, y: number) => {
|
|
|
91
91
|
const dist = Math.hypot(x - centre.x, y - centre.y);
|
|
92
92
|
return dist > 12 && dist < 18; // mountain ring
|
|
93
93
|
};
|
|
94
|
-
world.fill("mountain"
|
|
94
|
+
world.writeMask(mask).fill("mountain");
|
|
95
95
|
|
|
96
96
|
// get a view of just the interesting area
|
|
97
97
|
const lakeRegion = world.region(25, 25, 30, 30);
|
|
@@ -130,6 +130,21 @@ const mountainCells = world.cells.toFlatArrayXY()
|
|
|
130
130
|
.filter(cell => cell.value === "mountain")
|
|
131
131
|
.map(cell => [cell.x, cell.y]);
|
|
132
132
|
|
|
133
|
+
// or get a live, renderable minimap
|
|
134
|
+
import { C } from "@xtia/rgba";
|
|
135
|
+
|
|
136
|
+
const colourMap = new Map(Object.entries({
|
|
137
|
+
grass: C.x0f0,
|
|
138
|
+
water: C.x00f,
|
|
139
|
+
forest: C.x080,
|
|
140
|
+
mountain: C.xa70
|
|
141
|
+
}));
|
|
142
|
+
const minimap = world.pipe
|
|
143
|
+
.map(colourMap)
|
|
144
|
+
.floorCoordinates()
|
|
145
|
+
.crop(viewX, viewY, vw, vh)
|
|
146
|
+
.stretch(canvas.width, canvas.height);
|
|
147
|
+
|
|
133
148
|
```
|
|
134
149
|
|
|
135
150
|
## Cell interface
|
|
@@ -188,9 +203,8 @@ const visibility = start.createVisibilityMap(
|
|
|
188
203
|
cell => cell.value === "grass" // anything except grass blocks vision
|
|
189
204
|
);
|
|
190
205
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
screenGrid.paste(spriteMap, visibility);
|
|
206
|
+
// writeMask() creates a live view that only allows writes at positions allowed by the mask Source2D/function
|
|
207
|
+
screenGrid.writeMask(visibility).paste(spriteMap, 0, 0);
|
|
194
208
|
```
|
|
195
209
|
|
|
196
210
|
### Reusing path data
|
|
@@ -219,8 +233,17 @@ We can perform batched updates, suppressing the 'change' event until a process c
|
|
|
219
233
|
Pipe2D makes it easy to save and restore grid data:
|
|
220
234
|
|
|
221
235
|
```ts
|
|
222
|
-
const
|
|
236
|
+
const snapshot = world.pipe.stash();
|
|
237
|
+
|
|
238
|
+
const restored = Grid.from(snapshot);
|
|
239
|
+
// or paste it to an existing grid
|
|
240
|
+
world.paste(snapshot);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
For persistent storage or transmission, Pipe2D can export as array:
|
|
223
244
|
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
245
|
+
```ts
|
|
246
|
+
const saved = snapshot.toFlatArrayXY(); // ["mountain", "forest", "grass", ...]
|
|
247
|
+
|
|
248
|
+
const restoredSnapshot = Pipe2D.fromFlatArrayXY(saved);
|
|
249
|
+
```
|
package/lib/grid.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ type PathFindOptions<T> = {
|
|
|
13
13
|
cell: Cell<T>;
|
|
14
14
|
cost: number;
|
|
15
15
|
}) => void;
|
|
16
|
-
shortcutMap?: Source2D<
|
|
16
|
+
shortcutMap?: Source2D<LocationSpec<T>[] | undefined> | GetXYFunc<LocationSpec<T>[] | undefined>;
|
|
17
17
|
};
|
|
18
18
|
type CostMapOptions<T> = PathFindOptions<T> & {
|
|
19
19
|
stopAt?: LocationSpec<T>;
|
|
@@ -64,8 +64,9 @@ export declare class Grid<T> {
|
|
|
64
64
|
get(x: number, y: number): T;
|
|
65
65
|
set(x: number, y: number, value: T): void;
|
|
66
66
|
trySet(x: number, y: number, value: T): boolean;
|
|
67
|
-
fill(value: T
|
|
68
|
-
paste(
|
|
67
|
+
fill(value: T): void;
|
|
68
|
+
paste(source: Source2D<T>, x?: number, y?: number): void;
|
|
69
|
+
writeMask(mask: Source2D<boolean> | GetXYFunc<boolean>): Grid<T>;
|
|
69
70
|
[Symbol.iterator](): IterableIterator<{
|
|
70
71
|
x: number;
|
|
71
72
|
y: number;
|
package/lib/grid.js
CHANGED
|
@@ -50,11 +50,7 @@ export class Cell {
|
|
|
50
50
|
return this.gridView.cells.get(xDelta + this.x, yDelta + this.y);
|
|
51
51
|
}
|
|
52
52
|
findPath(target, getCost, options = {}) {
|
|
53
|
-
const pathMap = this.calculateCosts(getCost, {
|
|
54
|
-
stopAt: target,
|
|
55
|
-
allowDiagonal: options.allowDiagonal,
|
|
56
|
-
maxCost: options.maxCost,
|
|
57
|
-
}, true);
|
|
53
|
+
const pathMap = this.calculateCosts(getCost, Object.assign(Object.assign({}, options), { stopAt: target }), true);
|
|
58
54
|
return pathMap === null || pathMap === void 0 ? void 0 : pathMap.get(...this.getSiblingXY(target));
|
|
59
55
|
}
|
|
60
56
|
getPathMap(getCost, options = {}) {
|
|
@@ -98,7 +94,7 @@ export class Cell {
|
|
|
98
94
|
? options.shortcutMap(current.x, current.y)
|
|
99
95
|
: (_b = options.shortcutMap) === null || _b === void 0 ? void 0 : _b.get(current.x, current.y);
|
|
100
96
|
if (shortcutCells) {
|
|
101
|
-
const shortcuts = shortcutCells.map(cell => ({
|
|
97
|
+
const shortcuts = shortcutCells.map(loc => this.getSiblingCell(loc)).map(cell => ({
|
|
102
98
|
traversalType: TraversalType.shortcut,
|
|
103
99
|
cell
|
|
104
100
|
}));
|
|
@@ -185,14 +181,14 @@ export class Cell {
|
|
|
185
181
|
}
|
|
186
182
|
}
|
|
187
183
|
createVisibilityMap(isClear, options = {}) {
|
|
188
|
-
var _a
|
|
189
|
-
const maxDistance =
|
|
190
|
-
const boundariesVisible = (
|
|
184
|
+
var _a;
|
|
185
|
+
const maxDistance = options.maxDistance;
|
|
186
|
+
const boundariesVisible = (_a = options.boundariesVisible) !== null && _a !== void 0 ? _a : true;
|
|
191
187
|
return new Pipe2D(this.gridView.width, this.gridView.height, (x, y) => {
|
|
192
188
|
if (!isClear(this)) {
|
|
193
189
|
return x === this.x && y === this.y ? boundariesVisible : false;
|
|
194
190
|
}
|
|
195
|
-
if (maxDistance
|
|
191
|
+
if (maxDistance !== undefined) {
|
|
196
192
|
if (getDistance(this, { x, y }) > maxDistance) {
|
|
197
193
|
return false;
|
|
198
194
|
}
|
|
@@ -239,46 +235,46 @@ export class Grid {
|
|
|
239
235
|
this.parentSet(x, y, value);
|
|
240
236
|
}
|
|
241
237
|
trySet(x, y, value) {
|
|
242
|
-
if (x < 0
|
|
238
|
+
if (x < 0
|
|
239
|
+
|| x >= this.width
|
|
240
|
+
|| y < 0
|
|
241
|
+
|| y >= this.height
|
|
242
|
+
|| !Number.isInteger(x)
|
|
243
|
+
|| !Number.isInteger(y))
|
|
243
244
|
return false;
|
|
244
245
|
this.set(x, y, value);
|
|
245
246
|
return true;
|
|
246
247
|
}
|
|
247
|
-
fill(value
|
|
248
|
-
const maskSource = typeof mask == "function" ? {
|
|
249
|
-
width: this.width,
|
|
250
|
-
height: this.height,
|
|
251
|
-
get: mask
|
|
252
|
-
} : mask;
|
|
248
|
+
fill(value) {
|
|
253
249
|
this.batchUpdate(() => {
|
|
254
250
|
for (let y = 0; y < this.height; y++) {
|
|
255
251
|
for (let x = 0; x < this.width; x++) {
|
|
256
|
-
if (maskSource && !maskSource.get(x, y))
|
|
257
|
-
continue;
|
|
258
252
|
this.trySet(x, y, value);
|
|
259
253
|
}
|
|
260
254
|
}
|
|
261
255
|
});
|
|
262
256
|
}
|
|
263
|
-
paste(x, y
|
|
264
|
-
const maskSource = typeof mask == "function" ? {
|
|
265
|
-
width: this.width,
|
|
266
|
-
height: this.height,
|
|
267
|
-
get: mask
|
|
268
|
-
} : mask;
|
|
257
|
+
paste(source, x = 0, y = 0) {
|
|
269
258
|
const cachedSource = new GridBase(source);
|
|
270
259
|
this.batchUpdate(() => {
|
|
271
260
|
for (let oy = 0; oy < source.height; oy++) {
|
|
272
261
|
for (let ox = 0; ox < source.width; ox++) {
|
|
273
|
-
|
|
274
|
-
const ty = oy + y;
|
|
275
|
-
if (maskSource && !maskSource.get(tx, ty))
|
|
276
|
-
continue;
|
|
277
|
-
this.trySet(tx, ty, cachedSource.get(ox, oy));
|
|
262
|
+
this.trySet(ox + x, oy + y, cachedSource.get(ox, oy));
|
|
278
263
|
}
|
|
279
264
|
}
|
|
280
265
|
});
|
|
281
266
|
}
|
|
267
|
+
writeMask(mask) {
|
|
268
|
+
const getMask = typeof mask == "function"
|
|
269
|
+
? mask
|
|
270
|
+
: (x, y) => mask.get(x, y);
|
|
271
|
+
return new Grid(this.width, this.height, this.parentGet, (x, y, v) => {
|
|
272
|
+
const writable = getMask(x, y);
|
|
273
|
+
if (writable) {
|
|
274
|
+
this.set(x, y, v);
|
|
275
|
+
}
|
|
276
|
+
}, cb => this.batch(cb));
|
|
277
|
+
}
|
|
282
278
|
*[Symbol.iterator]() {
|
|
283
279
|
for (let y = 0; y < this.height; y++) {
|
|
284
280
|
for (let x = 0; x < this.width; x++) {
|
|
@@ -311,8 +307,6 @@ export class Grid {
|
|
|
311
307
|
return new Grid(width, height, (tx, ty) => {
|
|
312
308
|
return this.parentGet(tx + x, ty + y);
|
|
313
309
|
}, (tx, ty, value) => {
|
|
314
|
-
if (tx < 0 || ty < 0 || tx >= width || ty >= height)
|
|
315
|
-
throw new Error("Out of bounds");
|
|
316
310
|
this.parentSet(tx + x, ty + y, value);
|
|
317
311
|
}, fn => this.batch(fn));
|
|
318
312
|
}
|
|
@@ -381,17 +375,20 @@ export class GridBase extends Grid {
|
|
|
381
375
|
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
382
376
|
throw new Error(`Coordinates out of bounds: (${x}, ${y})`);
|
|
383
377
|
}
|
|
378
|
+
if (!Number.isInteger(x) || !Number.isInteger(y)) {
|
|
379
|
+
throw new Error(`Coordinates must be integer; got (${x}, ${y})`);
|
|
380
|
+
}
|
|
384
381
|
return y * this.width + x;
|
|
385
382
|
}
|
|
386
383
|
_set(x, y, value) {
|
|
387
384
|
const idx = this.xyToIndex(x, y);
|
|
388
385
|
if (this.data[idx] === value)
|
|
389
386
|
return;
|
|
387
|
+
this.data[this.xyToIndex(x, y)] = value;
|
|
390
388
|
if (this.batchState) {
|
|
391
389
|
this.batchState.changedCells.add(this.cells.get(x, y));
|
|
392
390
|
return;
|
|
393
391
|
}
|
|
394
|
-
this.data[this.xyToIndex(x, y)] = value;
|
|
395
392
|
if (this.eventHandlers.change) {
|
|
396
393
|
this.triggerChange(new Set([this.cells.get(x, y)]));
|
|
397
394
|
}
|
package/lib/utils.d.ts
CHANGED
|
@@ -4,14 +4,6 @@ export type Source2D<T> = {
|
|
|
4
4
|
height: number;
|
|
5
5
|
get: GetXYFunc<T>;
|
|
6
6
|
};
|
|
7
|
-
export type Angle = {
|
|
8
|
-
asDegrees: number;
|
|
9
|
-
} | {
|
|
10
|
-
asRadians: number;
|
|
11
|
-
} | {
|
|
12
|
-
asTurns: number;
|
|
13
|
-
};
|
|
14
|
-
export declare function angleToRadians(angle: Angle): number;
|
|
15
7
|
export declare class OrderedQueue<T> {
|
|
16
8
|
private getCost;
|
|
17
9
|
private queue;
|
package/lib/utils.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
export function angleToRadians(angle) {
|
|
2
|
-
if ("asRadians" in angle)
|
|
3
|
-
return angle.asRadians;
|
|
4
|
-
if ("asDegrees" in angle)
|
|
5
|
-
return angle.asDegrees * (180 / Math.PI);
|
|
6
|
-
if ("asTurns" in angle)
|
|
7
|
-
return angle.asTurns * Math.PI * 2;
|
|
8
|
-
throw new Error("Invalid angle");
|
|
9
|
-
}
|
|
10
1
|
export class OrderedQueue {
|
|
11
2
|
constructor(getCost, ...values) {
|
|
12
3
|
this.getCost = getCost;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.11",
|
|
3
3
|
"description": "2D store and utilities, with zero-copy regions and transform layers, pathfinding, Pipe2D interop",
|
|
4
4
|
"name": "@xtia/grid",
|
|
5
5
|
"exports": {
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"./lib/*": null
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"grid",
|
|
14
|
+
"game-dev",
|
|
15
|
+
"pathfinding",
|
|
16
|
+
"cellular-automata"
|
|
17
|
+
],
|
|
13
18
|
"scripts": {
|
|
14
19
|
"build": "npx tsc",
|
|
15
20
|
"test": "npx vitest run"
|
|
@@ -18,10 +23,11 @@
|
|
|
18
23
|
"lib/"
|
|
19
24
|
],
|
|
20
25
|
"devDependencies": {
|
|
26
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
21
27
|
"typescript": "^7.0.2",
|
|
22
28
|
"vitest": "^4.1.10"
|
|
23
29
|
},
|
|
24
30
|
"dependencies": {
|
|
25
|
-
"@xtia/pipe2d": "^2.1
|
|
31
|
+
"@xtia/pipe2d": "^2.2.1"
|
|
26
32
|
}
|
|
27
33
|
}
|