littlejsengine 1.19.3 → 1.20.0
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/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2455 -935
- package/dist/littlejs.esm.js +30344 -21110
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +30259 -21052
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +26567 -20455
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +58 -17
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +15 -0
- package/plugins/postProcess.js +56 -13
- package/plugins/render3d.js +1643 -1577
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +263 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +191 -75
- package/src/engineAudio.js +229 -125
- package/src/engineBuild.mjs +289 -256
- package/src/engineDebug.js +912 -742
- package/src/engineDraw.js +295 -193
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +12 -0
- package/src/engineInput.js +1462 -1293
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +194 -80
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +77 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/plugins/pathFinder.js
CHANGED
|
@@ -34,7 +34,7 @@ class PathFinderNode
|
|
|
34
34
|
this.pos = vec2(x, y);
|
|
35
35
|
/** @property {Vector2} - World-space center of this tile (set by buildNodeData) */
|
|
36
36
|
this.posWorld = vec2();
|
|
37
|
-
/** @property {boolean} - True if this cell is passable (
|
|
37
|
+
/** @property {boolean} - True if this cell is passable (set by buildNodeData) */
|
|
38
38
|
this.walkable = false;
|
|
39
39
|
/** @property {number} - Extra cost added to A* G-score for stepping on this cell */
|
|
40
40
|
this.cost = 0;
|
|
@@ -42,26 +42,30 @@ class PathFinderNode
|
|
|
42
42
|
this.g = 0;
|
|
43
43
|
/** @property {number} - A* F-score: G + heuristic */
|
|
44
44
|
this.f = 0;
|
|
45
|
-
/** @property {
|
|
45
|
+
/** @property {number} - A* heuristic: the estimated cost left to the goal, breaks ties between equal F */
|
|
46
|
+
this.h = 0;
|
|
47
|
+
/** @property {PathFinderNode|null} - Parent for path reconstruction
|
|
48
|
+
* @type {PathFinderNode|null} */
|
|
46
49
|
this.parent = null;
|
|
47
50
|
/** @property {boolean} - In the A* open list */
|
|
48
51
|
this.isOpen = false;
|
|
49
52
|
/** @property {boolean} - In the A* closed list */
|
|
50
53
|
this.isClosed = false;
|
|
54
|
+
/** @property {number} - Where it is in the A* open list's heap, while open */
|
|
55
|
+
this.heapIndex = 0;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
58
|
+
/** Clear what a search left on this node, keeping walkable and cost */
|
|
59
|
+
resetSearch()
|
|
55
60
|
{
|
|
56
|
-
this.
|
|
57
|
-
this.cost = 0;
|
|
58
|
-
this.g = 0;
|
|
59
|
-
this.f = 0;
|
|
61
|
+
this.g = this.f = this.h = 0;
|
|
60
62
|
this.parent = null;
|
|
61
|
-
this.isOpen = false;
|
|
62
|
-
this.isClosed = false;
|
|
63
|
+
this.isOpen = this.isClosed = false;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/** Reset per-search state and walkability (called by buildNodeData). */
|
|
67
|
+
reset() { this.walkable = false; this.cost = 0; this.resetSearch(); }
|
|
68
|
+
|
|
65
69
|
/** True if walkable and not blocked by cost. */
|
|
66
70
|
isClear()
|
|
67
71
|
{
|
|
@@ -89,29 +93,35 @@ class PathFinder
|
|
|
89
93
|
* overrides isWalkable). */
|
|
90
94
|
constructor(source)
|
|
91
95
|
{
|
|
92
|
-
// Accept
|
|
93
|
-
// We don't import TileCollisionLayer to avoid coupling; we duck-type on
|
|
94
|
-
// .size + .getCollisionData.
|
|
96
|
+
// Accept a Vector2 size, or a TileCollisionLayer or any object with its size and getCollisionData
|
|
95
97
|
if (isVector2(source))
|
|
96
98
|
{
|
|
97
|
-
/** @property {Vector2} - Grid dimensions in tiles
|
|
98
|
-
|
|
99
|
-
/** @
|
|
99
|
+
/** @property {Vector2} - Grid dimensions in tiles
|
|
100
|
+
* @type {Vector2} */
|
|
101
|
+
this.size = /** @type {Vector2} */ (source).floor();
|
|
102
|
+
/** @property {TileCollisionLayer|undefined} - Tile layer driving walkability, if any
|
|
103
|
+
* @type {TileCollisionLayer|undefined} */
|
|
100
104
|
this.tileLayer = undefined;
|
|
101
105
|
}
|
|
102
106
|
else
|
|
103
107
|
{
|
|
104
|
-
|
|
108
|
+
const layer = /** @type {TileCollisionLayer} */ (source);
|
|
109
|
+
ASSERT(layer && isVector2(layer.size) && typeof layer.getCollisionData === 'function',
|
|
105
110
|
'PathFinder requires a Vector2 size or a TileCollisionLayer');
|
|
106
|
-
this.size =
|
|
107
|
-
this.tileLayer =
|
|
111
|
+
this.size = layer.size;
|
|
112
|
+
this.tileLayer = layer;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
// Tunables (public, freely re-assignable).
|
|
111
116
|
/** @property {number} - A* heuristic multiplier (1 = admissible, higher = greedier) */
|
|
112
117
|
this.heuristicWeight = 1;
|
|
113
|
-
/** @property {number} -
|
|
114
|
-
|
|
118
|
+
/** @property {number|undefined} - Most A* expansions before giving up, undefined for the number of cells,
|
|
119
|
+
* so a search always finishes; a lower one caps the time a search takes, see searchGaveUp
|
|
120
|
+
* @type {number|undefined} */
|
|
121
|
+
this.maxLoop = undefined;
|
|
122
|
+
/** @property {boolean} - True when the last search stopped at maxLoop with no path, so it gave up rather
|
|
123
|
+
* than that there is no way through */
|
|
124
|
+
this.searchGaveUp = false;
|
|
115
125
|
/** @property {boolean} - If true, post-process paths with two-pass smoothing */
|
|
116
126
|
this.smoothPath = true;
|
|
117
127
|
/** @property {boolean} - If true, draw debug visualization during findPath */
|
|
@@ -119,18 +129,31 @@ class PathFinder
|
|
|
119
129
|
/** @property {number} - Debug primitive lifetime in seconds (0 disables drawing) */
|
|
120
130
|
this.debugTime = 1;
|
|
121
131
|
|
|
122
|
-
/** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes
|
|
132
|
+
/** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes
|
|
133
|
+
* @type {Array<PathFinderNode>} */
|
|
123
134
|
this.nodes = new Array(this.size.x * this.size.y);
|
|
124
135
|
for (let y = 0; y < this.size.y; ++y)
|
|
125
136
|
for (let x = 0; x < this.size.x; ++x)
|
|
126
137
|
this.nodes[x + y * this.size.x] = new PathFinderNode(x, y);
|
|
127
138
|
|
|
139
|
+
// Whether buildNodeData has run, a search without a rebuild builds it the first time.
|
|
140
|
+
/** @private */
|
|
141
|
+
this.nodeDataBuilt = false;
|
|
142
|
+
|
|
128
143
|
// Scratch Vector2 reused to avoid allocations in the isWalkable hot path.
|
|
144
|
+
/** @private */
|
|
129
145
|
this.collisionScratch = vec2();
|
|
146
|
+
|
|
147
|
+
// The nodes the last search changed, reset before the next one so a
|
|
148
|
+
// search without a rebuild starts as fresh as one after it.
|
|
149
|
+
/** @type {Array<PathFinderNode>}
|
|
150
|
+
* @private */
|
|
151
|
+
this.searchNodes = [];
|
|
130
152
|
}
|
|
131
153
|
|
|
132
154
|
/** Default walkability: if a tile layer was provided, returns true when the
|
|
133
|
-
* cell has no solid collision data
|
|
155
|
+
* cell has no solid (positive) collision data, so negative data is open
|
|
156
|
+
* like it is to the engine's collision; otherwise returns true. Override on
|
|
134
157
|
* the instance or via a subclass.
|
|
135
158
|
* @param {number} x - Tile x
|
|
136
159
|
* @param {number} y - Tile y
|
|
@@ -138,7 +161,7 @@ class PathFinder
|
|
|
138
161
|
isWalkable(x, y)
|
|
139
162
|
{
|
|
140
163
|
if (!this.tileLayer) return true;
|
|
141
|
-
return !this.tileLayer.getCollisionData(this.collisionScratch.set(x, y));
|
|
164
|
+
return !(this.tileLayer.getCollisionData(this.collisionScratch.set(x, y)) > 0);
|
|
142
165
|
}
|
|
143
166
|
|
|
144
167
|
/** Default extra cost for stepping on a cell. Returns 0 (free) by default.
|
|
@@ -163,8 +186,7 @@ class PathFinder
|
|
|
163
186
|
|
|
164
187
|
/** Convert a world-space position to integer tile coords (no clamping).
|
|
165
188
|
* @param {Vector2} worldPos
|
|
166
|
-
* @returns {Vector2}
|
|
167
|
-
* @memberof PathFinding */
|
|
189
|
+
* @returns {Vector2} */
|
|
168
190
|
worldToTile(worldPos)
|
|
169
191
|
{
|
|
170
192
|
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
@@ -175,8 +197,7 @@ class PathFinder
|
|
|
175
197
|
/** Convert integer tile coords to the world-space center of that tile.
|
|
176
198
|
* @param {number} x
|
|
177
199
|
* @param {number} y
|
|
178
|
-
* @returns {Vector2}
|
|
179
|
-
* @memberof PathFinding */
|
|
200
|
+
* @returns {Vector2} */
|
|
180
201
|
tileToWorld(x, y)
|
|
181
202
|
{
|
|
182
203
|
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
@@ -186,10 +207,10 @@ class PathFinder
|
|
|
186
207
|
|
|
187
208
|
/** Reset all nodes and re-populate walkable / cost / posWorld from the
|
|
188
209
|
* current isWalkable / getCost overrides. Called at the start of
|
|
189
|
-
* findPath;
|
|
190
|
-
* @private */
|
|
210
|
+
* findPath; call it directly before searches made with rebuild=false. */
|
|
191
211
|
buildNodeData()
|
|
192
212
|
{
|
|
213
|
+
this.nodeDataBuilt = true;
|
|
193
214
|
const w = this.size.x;
|
|
194
215
|
const h = this.size.y;
|
|
195
216
|
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
@@ -208,16 +229,16 @@ class PathFinder
|
|
|
208
229
|
if (this.debug && this.debugTime > 0)
|
|
209
230
|
{
|
|
210
231
|
if (!walkable)
|
|
211
|
-
debugRect(node.posWorld, PATHFINDER_TILE_VEC, rgb(1, 0, 0, 0.25), this.debugTime);
|
|
232
|
+
debugRect(node.posWorld, PATHFINDER_TILE_VEC, rgb(1, 0, 0, 0.25), this.debugTime, 0, false, false);
|
|
212
233
|
else if (cost > 0)
|
|
213
|
-
debugRect(node.posWorld, PATHFINDER_TILE_VEC, rgb(1, 0, 0, min(0.2, cost * 0.05)), this.debugTime);
|
|
234
|
+
debugRect(node.posWorld, PATHFINDER_TILE_VEC, rgb(1, 0, 0, min(0.2, cost * 0.05)), this.debugTime, 0, false, false);
|
|
214
235
|
}
|
|
215
236
|
}
|
|
216
237
|
}
|
|
217
238
|
|
|
218
239
|
/** Core A* search loop. Expects buildNodeData() to have been called first.
|
|
219
240
|
* Marks node.parent for path reconstruction. Returns true if endNode was
|
|
220
|
-
* reached; false on disconnected goal or maxLoop exhaustion.
|
|
241
|
+
* reached; false on disconnected goal or maxLoop exhaustion, which sets searchGaveUp.
|
|
221
242
|
* @param {PathFinderNode} startNode
|
|
222
243
|
* @param {PathFinderNode} endNode
|
|
223
244
|
* @returns {boolean}
|
|
@@ -228,36 +249,78 @@ class PathFinder
|
|
|
228
249
|
ASSERT(startNode !== endNode, 'aStarSearch: start and end must differ — caller should handle trivial case');
|
|
229
250
|
ASSERT(startNode.walkable && endNode.walkable, 'aStarSearch: endpoints must be walkable');
|
|
230
251
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
252
|
+
// Undo what the last search changed, so one without a rebuild starts
|
|
253
|
+
// from the same state; after buildNodeData these are fresh already.
|
|
254
|
+
const searchNodes = this.searchNodes;
|
|
255
|
+
for (const n of searchNodes) n.resetSearch();
|
|
256
|
+
searchNodes.length = 0;
|
|
257
|
+
searchNodes.push(startNode);
|
|
258
|
+
|
|
259
|
+
// The open list is a binary heap with the smallest f score on top, so a big map searches quickly.
|
|
260
|
+
// Equal scores go to the node nearer the goal, so open ground is
|
|
261
|
+
// crossed nearly straight instead of widening in a band of ties;
|
|
262
|
+
// the path is just as short, only which of equal paths can change.
|
|
263
|
+
// Scores are sums of diagonals, so equal is within a hair.
|
|
264
|
+
const openList = [];
|
|
265
|
+
const isBetter = (a, b)=> a.f < b.f - 1e-9 || a.f < b.f + 1e-9 && a.h < b.h;
|
|
266
|
+
const siftUp = (node)=>
|
|
267
|
+
{
|
|
268
|
+
// a new node, or one whose score went down, moves up past the ones it now beats
|
|
269
|
+
let i = node.heapIndex;
|
|
270
|
+
while (i)
|
|
271
|
+
{
|
|
272
|
+
const parent = (i - 1) >> 1;
|
|
273
|
+
if (!isBetter(node, openList[parent])) break;
|
|
274
|
+
(openList[i] = openList[parent]).heapIndex = i;
|
|
275
|
+
i = parent;
|
|
276
|
+
}
|
|
277
|
+
(openList[i] = node).heapIndex = i;
|
|
278
|
+
};
|
|
279
|
+
const popBest = ()=>
|
|
236
280
|
{
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
let bestF = openList[0].f;
|
|
241
|
-
for (let i = 1; i < openList.length; ++i)
|
|
281
|
+
// take the top, then the last node sinks down from the top to where it belongs
|
|
282
|
+
const best = openList[0], last = openList.pop();
|
|
283
|
+
if (last !== best)
|
|
242
284
|
{
|
|
243
|
-
|
|
285
|
+
let i = 0;
|
|
286
|
+
for (;;)
|
|
244
287
|
{
|
|
245
|
-
|
|
246
|
-
|
|
288
|
+
const left = 2*i + 1, right = left + 1;
|
|
289
|
+
if (left >= openList.length) break;
|
|
290
|
+
const child = right < openList.length && isBetter(openList[right], openList[left]) ? right : left;
|
|
291
|
+
if (!isBetter(openList[child], last)) break;
|
|
292
|
+
(openList[i] = openList[child]).heapIndex = i;
|
|
293
|
+
i = child;
|
|
247
294
|
}
|
|
295
|
+
(openList[i] = last).heapIndex = i;
|
|
248
296
|
}
|
|
249
|
-
|
|
297
|
+
return best;
|
|
298
|
+
};
|
|
299
|
+
startNode.isOpen = true;
|
|
300
|
+
startNode.heapIndex = openList.length;
|
|
301
|
+
siftUp(startNode);
|
|
302
|
+
const maxLoop = this.maxLoop ?? this.size.x * this.size.y;
|
|
303
|
+
let loopCount = 0;
|
|
304
|
+
this.searchGaveUp = false;
|
|
250
305
|
|
|
306
|
+
while (openList.length > 0)
|
|
307
|
+
{
|
|
308
|
+
const current = openList[0];
|
|
251
309
|
if (current === endNode) break;
|
|
252
|
-
if (++loopCount >
|
|
310
|
+
if (++loopCount > maxLoop)
|
|
311
|
+
{
|
|
312
|
+
// the goal may be found already, waiting its turn, and then the path to it comes back
|
|
313
|
+
this.searchGaveUp = !endNode.parent;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
253
316
|
|
|
254
317
|
// Move current from open to closed.
|
|
318
|
+
popBest();
|
|
255
319
|
current.isOpen = false;
|
|
256
|
-
openList.splice(bestIndex, 1);
|
|
257
320
|
current.isClosed = true;
|
|
258
321
|
|
|
259
322
|
if (this.debug && this.debugTime > 0)
|
|
260
|
-
debugRect(current.posWorld, PATHFINDER_TILE_VEC, rgb(1, 1, 1, 0.05), this.debugTime);
|
|
323
|
+
debugRect(current.posWorld, PATHFINDER_TILE_VEC, rgb(1, 1, 1, 0.05), this.debugTime, 0, false, false);
|
|
261
324
|
|
|
262
325
|
// Expand all 8 neighbors.
|
|
263
326
|
for (let dy = -1; dy <= 1; ++dy)
|
|
@@ -282,10 +345,13 @@ class PathFinder
|
|
|
282
345
|
}
|
|
283
346
|
|
|
284
347
|
const tentativeG = current.g + stepCost + neighbor.cost;
|
|
285
|
-
|
|
348
|
+
const wasOpen = neighbor.isOpen;
|
|
349
|
+
if (!wasOpen)
|
|
286
350
|
{
|
|
287
351
|
neighbor.isOpen = true;
|
|
352
|
+
neighbor.heapIndex = openList.length;
|
|
288
353
|
openList.push(neighbor);
|
|
354
|
+
searchNodes.push(neighbor);
|
|
289
355
|
}
|
|
290
356
|
else if (tentativeG >= neighbor.g)
|
|
291
357
|
{
|
|
@@ -300,7 +366,9 @@ class PathFinder
|
|
|
300
366
|
const adx = abs(endNode.pos.x - neighbor.pos.x);
|
|
301
367
|
const ady = abs(endNode.pos.y - neighbor.pos.y);
|
|
302
368
|
const h = max(adx, ady) + (Math.SQRT2 - 1) * min(adx, ady);
|
|
369
|
+
neighbor.h = h;
|
|
303
370
|
neighbor.f = neighbor.g + h * this.heuristicWeight;
|
|
371
|
+
siftUp(neighbor); // its place in the heap, new or with a lower score now
|
|
304
372
|
}
|
|
305
373
|
}
|
|
306
374
|
|
|
@@ -319,47 +387,12 @@ class PathFinder
|
|
|
319
387
|
* @param {Vector2} worldPos
|
|
320
388
|
* @param {number} [searchRange=10] - Max box-radius in tiles
|
|
321
389
|
* @param {boolean} [rebuild=true] - Whether to call buildNodeData first
|
|
322
|
-
* @returns {PathFinderNode|null}
|
|
323
|
-
* @memberof PathFinding */
|
|
390
|
+
* @returns {PathFinderNode|null} */
|
|
324
391
|
getNearestClearNode(worldPos, searchRange = 10, rebuild = true)
|
|
325
392
|
{
|
|
326
393
|
ASSERT(isVector2(worldPos), 'worldPos must be a Vector2');
|
|
327
|
-
if (rebuild) this.buildNodeData();
|
|
328
|
-
|
|
329
|
-
// Inline worldToTile to avoid a Vector2 allocation per call.
|
|
330
|
-
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
331
|
-
const oy = this.tileLayer ? this.tileLayer.pos.y : 0;
|
|
332
|
-
const centerX = floor(worldPos.x - ox);
|
|
333
|
-
const centerY = floor(worldPos.y - oy);
|
|
334
|
-
|
|
335
|
-
for (let offset = 0; offset <= searchRange; ++offset)
|
|
336
|
-
{
|
|
337
|
-
let nearest = null;
|
|
338
|
-
let nearestDistSq = 0;
|
|
339
|
-
|
|
340
|
-
for (let dy = -offset; dy <= offset; ++dy)
|
|
341
|
-
for (let dx = -offset; dx <= offset; ++dx)
|
|
342
|
-
{
|
|
343
|
-
// Only scan the perimeter of the current ring (skip the
|
|
344
|
-
// interior we've already searched in earlier iterations).
|
|
345
|
-
if (offset > 0 && abs(dx) !== offset && abs(dy) !== offset)
|
|
346
|
-
continue;
|
|
347
|
-
|
|
348
|
-
const node = this.getNode(centerX + dx, centerY + dy);
|
|
349
|
-
if (!node || !node.isClear()) continue;
|
|
350
|
-
|
|
351
|
-
const ddx = node.posWorld.x - worldPos.x;
|
|
352
|
-
const ddy = node.posWorld.y - worldPos.y;
|
|
353
|
-
const distSq = ddx * ddx + ddy * ddy;
|
|
354
|
-
if (!nearest || distSq < nearestDistSq)
|
|
355
|
-
{
|
|
356
|
-
nearest = node;
|
|
357
|
-
nearestDistSq = distSq;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (nearest) return nearest;
|
|
361
|
-
}
|
|
362
|
-
return null;
|
|
394
|
+
if (rebuild || !this.nodeDataBuilt) this.buildNodeData(); // a grid never built has nothing to walk yet
|
|
395
|
+
return pathFinderNearestNode(this, worldPos, searchRange, (node)=> node.isClear());
|
|
363
396
|
}
|
|
364
397
|
|
|
365
398
|
/** Smooth a node path by removing redundant turns and tightening corners
|
|
@@ -383,8 +416,9 @@ class PathFinder
|
|
|
383
416
|
const dy = next.pos.y - prev.pos.y;
|
|
384
417
|
const lenSq = dx * dx + dy * dy;
|
|
385
418
|
|
|
386
|
-
//
|
|
387
|
-
//
|
|
419
|
+
// stepDx,stepDy is the prev-to-node step and stepDxNext,stepDyNext the node-to-next step; the 135°
|
|
420
|
+
// case uses them to tell mostly vertical from mostly horizontal, the straight case to tell a line
|
|
421
|
+
// from a bump
|
|
388
422
|
const stepDx = node.pos.x - prev.pos.x;
|
|
389
423
|
const stepDy = node.pos.y - prev.pos.y;
|
|
390
424
|
const stepDxNext = next.pos.x - node.pos.x;
|
|
@@ -394,7 +428,7 @@ class PathFinder
|
|
|
394
428
|
{
|
|
395
429
|
// 45° angle — middle node is off the straight line. Drop it.
|
|
396
430
|
if (this.debug && this.debugTime > 0)
|
|
397
|
-
debugCircle(node.posWorld, 0.3, rgb(0.5, 0, 0.5, 0.5), this.debugTime);
|
|
431
|
+
debugCircle(node.posWorld, 0.3, rgb(0.5, 0, 0.5, 0.5), this.debugTime, false, false);
|
|
398
432
|
path.splice(i, 1);
|
|
399
433
|
i = max(1, i - 1);
|
|
400
434
|
continue;
|
|
@@ -403,7 +437,7 @@ class PathFinder
|
|
|
403
437
|
{
|
|
404
438
|
// 90° corner. Check the alternative-diagonal cell.
|
|
405
439
|
if (this.debug && this.debugTime > 0)
|
|
406
|
-
debugCircle(node.posWorld, 0.3, rgb(1, 0, 0, 0.5), this.debugTime);
|
|
440
|
+
debugCircle(node.posWorld, 0.3, rgb(1, 0, 0, 0.5), this.debugTime, false, false);
|
|
407
441
|
|
|
408
442
|
let sx, sy;
|
|
409
443
|
if (prev.pos.y === node.pos.y && next.pos.x === node.pos.x)
|
|
@@ -425,7 +459,7 @@ class PathFinder
|
|
|
425
459
|
// middle node to whichever of two candidate cells is closer
|
|
426
460
|
// to prev-of-prev, and only if the corner cut is also clear.
|
|
427
461
|
if (this.debug && this.debugTime > 0)
|
|
428
|
-
debugCircle(node.posWorld, 0.3, rgb(1, 1, 0, 0.5), this.debugTime);
|
|
462
|
+
debugCircle(node.posWorld, 0.3, rgb(1, 1, 0, 0.5), this.debugTime, false, false);
|
|
429
463
|
|
|
430
464
|
const prevPrev = i >= 2 ? path[i - 2] : prev;
|
|
431
465
|
let s1x, s1y, s2x, s2y;
|
|
@@ -447,8 +481,8 @@ class PathFinder
|
|
|
447
481
|
const dd2y = s2y - prevPrev.pos.y;
|
|
448
482
|
const dist1Sq = dd1x * dd1x + dd1y * dd1y;
|
|
449
483
|
const dist2Sq = dd2x * dd2x + dd2y * dd2y;
|
|
450
|
-
const
|
|
451
|
-
const
|
|
484
|
+
const useFirst = dist1Sq < dist2Sq;
|
|
485
|
+
const sx = useFirst ? s1x : s2x, sy = useFirst ? s1y : s2y;
|
|
452
486
|
|
|
453
487
|
const shortcut = this.getNode(sx, sy);
|
|
454
488
|
if (shortcut && shortcut !== node && shortcut.isClear())
|
|
@@ -469,7 +503,7 @@ class PathFinder
|
|
|
469
503
|
{
|
|
470
504
|
// Straight line or a 1-cell bump.
|
|
471
505
|
if (this.debug && this.debugTime > 0)
|
|
472
|
-
debugCircle(node.posWorld, 0.3, rgb(0, 1, 0, 0.5), this.debugTime);
|
|
506
|
+
debugCircle(node.posWorld, 0.3, rgb(0, 1, 0, 0.5), this.debugTime, false, false);
|
|
473
507
|
|
|
474
508
|
if (stepDx === stepDxNext && stepDy === stepDyNext)
|
|
475
509
|
{
|
|
@@ -504,89 +538,42 @@ class PathFinder
|
|
|
504
538
|
* isLineClear permits, so the result can leave grid centers and cut
|
|
505
539
|
* cleanly across open spaces.
|
|
506
540
|
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
541
|
+
* A node with a cost is kept, and a shortcut only runs between clear
|
|
542
|
+
* nodes: isLineClear passes only through clear cells, so a straight line
|
|
543
|
+
* it accepts costs no more than the grid path it replaces.
|
|
510
544
|
*
|
|
511
|
-
*
|
|
545
|
+
* Replaces the port of ShortenPath2() in pathFinding.cpp, which could
|
|
546
|
+
* add a segment it had not checked.
|
|
512
547
|
* @param {PathFinderNode[]} path
|
|
513
548
|
* @private */
|
|
514
549
|
smoothPathStringPull(path)
|
|
515
550
|
{
|
|
516
551
|
if (path.length <= 2) return;
|
|
517
|
-
for (const n of path)
|
|
518
|
-
{
|
|
519
|
-
if (!n.isClear()) return;
|
|
520
|
-
}
|
|
521
552
|
|
|
553
|
+
// Greedy: from each kept node, jump to the furthest node with a clear
|
|
554
|
+
// line to it, or else the next node. Every segment is one isLineClear
|
|
555
|
+
// accepted or one the path already had, so none can cross a wall.
|
|
522
556
|
const original = path.slice();
|
|
523
557
|
path.length = 0;
|
|
524
558
|
path.push(original[0]);
|
|
525
|
-
let
|
|
526
|
-
|
|
527
|
-
for (let i = 1; i < original.length; ++i)
|
|
559
|
+
for (let k = 0; k < original.length - 1;)
|
|
528
560
|
{
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (a !== b)
|
|
538
|
-
{
|
|
539
|
-
const cross =
|
|
540
|
-
(b.pos.x - a.pos.x) * (node.pos.y - a.pos.y) -
|
|
541
|
-
(b.pos.y - a.pos.y) * (node.pos.x - a.pos.x);
|
|
542
|
-
if (cross === 0) continue;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (!this.isLineClear(node.pos, path[path.length - 1].pos))
|
|
547
|
-
{
|
|
548
|
-
// Look ahead — if any later node has a clear shot to the
|
|
549
|
-
// back of our new path, skip this node and try later.
|
|
550
|
-
let foundClearAfter = false;
|
|
551
|
-
for (let j = i + 1; j < original.length; ++j)
|
|
552
|
-
{
|
|
553
|
-
if (this.isLineClear(original[j].pos, path[path.length - 1].pos))
|
|
554
|
-
{
|
|
555
|
-
foundClearAfter = true;
|
|
556
|
-
break;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
if (foundClearAfter)
|
|
560
|
-
{
|
|
561
|
-
if (this.debug && this.debugTime > 0)
|
|
562
|
-
debugLine(node.posWorld, path[path.length - 1].posWorld, rgb(0, 0, 1, 0.3), 0.02, this.debugTime);
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// No clear line ahead — fall back to the last waypoint we did
|
|
567
|
-
// have a clear line to. searchIndex tracks our scan position.
|
|
568
|
-
for (; searchIndex < original.length; ++searchIndex)
|
|
569
|
-
{
|
|
570
|
-
const cand = original[searchIndex];
|
|
571
|
-
if (this.isLineClear(node.pos, cand.pos))
|
|
572
|
-
{
|
|
573
|
-
path.push(cand);
|
|
574
|
-
i = searchIndex;
|
|
575
|
-
break;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
ASSERT(searchIndex < original.length, 'smoothPathStringPull: ran out of candidates');
|
|
579
|
-
}
|
|
561
|
+
let j = original.length - 1;
|
|
562
|
+
if (original[k].isClear()) // isLineClear needs both ends clear
|
|
563
|
+
while (j > k + 1 && !(original[j].isClear() && this.isLineClear(original[k].pos, original[j].pos)))
|
|
564
|
+
--j;
|
|
565
|
+
else
|
|
566
|
+
j = k + 1;
|
|
567
|
+
path.push(original[j]);
|
|
568
|
+
k = j;
|
|
580
569
|
}
|
|
581
|
-
|
|
582
|
-
path.push(original[original.length - 1]);
|
|
583
570
|
}
|
|
584
571
|
|
|
585
572
|
/** Drop any middle node that lies exactly on the line through its two
|
|
586
573
|
* neighbors. Backstop for the smoothing passes — the corners pass
|
|
587
574
|
* intentionally keeps truly-straight runs, and the string-pulling pass
|
|
588
|
-
*
|
|
589
|
-
*
|
|
575
|
+
* falls back to the next node where no longer line is clear, so it can
|
|
576
|
+
* leave 3+ collinear nodes in some edge cases.
|
|
590
577
|
* @param {PathFinderNode[]} path
|
|
591
578
|
* @private */
|
|
592
579
|
dropCollinearNodes(path)
|
|
@@ -733,22 +720,30 @@ class PathFinder
|
|
|
733
720
|
/** Find a path from startPos to endPos in world space. Returns an array
|
|
734
721
|
* of world-space Vector2 points; empty array if no path exists.
|
|
735
722
|
*
|
|
736
|
-
* Start and end are snapped to the nearest walkable tile
|
|
737
|
-
*
|
|
723
|
+
* Start and end are snapped to the nearest walkable tile (a costed one
|
|
724
|
+
* counts), within 10 tiles. Intermediate points are tile centers unless the
|
|
738
725
|
* string-pulling smoothing pass moves them off-grid.
|
|
726
|
+
*
|
|
727
|
+
* By default, calls `buildNodeData()` first, which asks isWalkable and
|
|
728
|
+
* getCost about every cell. When finding many paths with unchanged
|
|
729
|
+
* walkability, pass `rebuild=false` and call `buildNodeData()` once
|
|
730
|
+
* externally; the paths found are the same.
|
|
739
731
|
* @param {Vector2} startPos - World-space start
|
|
740
732
|
* @param {Vector2} endPos - World-space end
|
|
741
|
-
* @
|
|
742
|
-
* @
|
|
743
|
-
findPath(startPos, endPos)
|
|
733
|
+
* @param {boolean} [rebuild] - Whether to call buildNodeData first
|
|
734
|
+
* @returns {Vector2[]} */
|
|
735
|
+
findPath(startPos, endPos, rebuild = true)
|
|
744
736
|
{
|
|
745
737
|
ASSERT(isVector2(startPos) && isVector2(endPos), 'findPath needs Vector2 endpoints');
|
|
746
738
|
|
|
747
|
-
this.
|
|
739
|
+
this.searchGaveUp = false;
|
|
740
|
+
if (rebuild || !this.nodeDataBuilt) this.buildNodeData(); // a grid never built has nothing to walk yet
|
|
748
741
|
|
|
749
742
|
// rebuild=false because we just built — avoid redundant work per snap.
|
|
750
|
-
|
|
751
|
-
const
|
|
743
|
+
// the ends go to the nearest cell that can be walked, whatever it costs to cross
|
|
744
|
+
const walkable = (node)=> node.walkable;
|
|
745
|
+
const startNode = pathFinderNearestNode(this, startPos, 10, walkable);
|
|
746
|
+
const endNode = pathFinderNearestNode(this, endPos, 10, walkable);
|
|
752
747
|
if (!startNode || !endNode) return [];
|
|
753
748
|
|
|
754
749
|
// Trivial case: start and end snapped to the same tile.
|
|
@@ -777,13 +772,51 @@ class PathFinder
|
|
|
777
772
|
if (this.debug && this.debugTime > 0 && result.length > 0)
|
|
778
773
|
{
|
|
779
774
|
for (let i = 1; i < result.length; ++i)
|
|
780
|
-
debugLine(result[i - 1], result[i], RED, 0.1, this.debugTime);
|
|
775
|
+
debugLine(result[i - 1], result[i], RED, 0.1, this.debugTime, false);
|
|
781
776
|
for (const p of result)
|
|
782
|
-
debugCircle(p, 0.5, rgb(1, 0, 0, 0.3), this.debugTime);
|
|
783
|
-
debugCircle(result[0], 0.5, rgb(0, 1, 0, 0.5), this.debugTime);
|
|
784
|
-
debugCircle(result[result.length - 1], 0.5, rgb(0, 1, 0, 0.5), this.debugTime);
|
|
777
|
+
debugCircle(p, 0.5, rgb(1, 0, 0, 0.3), this.debugTime, false, false);
|
|
778
|
+
debugCircle(result[0], 0.5, rgb(0, 1, 0, 0.5), this.debugTime, false, false);
|
|
779
|
+
debugCircle(result[result.length - 1], 0.5, rgb(0, 1, 0, 0.5), this.debugTime, false, false);
|
|
785
780
|
}
|
|
786
781
|
|
|
787
782
|
return result;
|
|
788
783
|
}
|
|
789
784
|
}
|
|
785
|
+
|
|
786
|
+
// the node nearest a world position that passes a test, within a range of tiles, or null: the rings of cells around
|
|
787
|
+
// the position are searched outward until the next ring cannot hold anything nearer than the best found, since the
|
|
788
|
+
// best of one ring is not always the nearest, a cell one ring out can be closer
|
|
789
|
+
function pathFinderNearestNode(finder, worldPos, searchRange, test)
|
|
790
|
+
{
|
|
791
|
+
const center = finder.worldToTile(worldPos);
|
|
792
|
+
const centerX = center.x, centerY = center.y;
|
|
793
|
+
|
|
794
|
+
let nearest = null, nearestDistSq = 0;
|
|
795
|
+
for (let offset = 0; offset <= searchRange; ++offset)
|
|
796
|
+
{
|
|
797
|
+
// every cell of this ring is more than offset - .5 away along one axis
|
|
798
|
+
const bound = max(0, offset - .5);
|
|
799
|
+
if (nearest && bound * bound >= nearestDistSq) break;
|
|
800
|
+
|
|
801
|
+
for (let dy = -offset; dy <= offset; ++dy)
|
|
802
|
+
for (let dx = -offset; dx <= offset; ++dx)
|
|
803
|
+
{
|
|
804
|
+
// only the ring itself, the inside was searched already
|
|
805
|
+
if (offset > 0 && abs(dx) !== offset && abs(dy) !== offset)
|
|
806
|
+
continue;
|
|
807
|
+
|
|
808
|
+
const node = finder.getNode(centerX + dx, centerY + dy);
|
|
809
|
+
if (!node || !test(node)) continue;
|
|
810
|
+
|
|
811
|
+
const ddx = node.posWorld.x - worldPos.x;
|
|
812
|
+
const ddy = node.posWorld.y - worldPos.y;
|
|
813
|
+
const distSq = ddx * ddx + ddy * ddy;
|
|
814
|
+
if (!nearest || distSq < nearestDistSq)
|
|
815
|
+
{
|
|
816
|
+
nearest = node;
|
|
817
|
+
nearestDistSq = distSq;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return nearest;
|
|
822
|
+
}
|
package/plugins/pluginExport.js
CHANGED
|
@@ -92,6 +92,7 @@ export
|
|
|
92
92
|
drawNineSliceScreen,
|
|
93
93
|
drawThreeSlice,
|
|
94
94
|
drawThreeSliceScreen,
|
|
95
|
+
TileSlice,
|
|
95
96
|
drawCrescent,
|
|
96
97
|
getCrescentPoints,
|
|
97
98
|
|
|
@@ -106,6 +107,12 @@ export
|
|
|
106
107
|
PathFinder,
|
|
107
108
|
PathFinderNode,
|
|
108
109
|
|
|
110
|
+
// Tweakables
|
|
111
|
+
tweak,
|
|
112
|
+
tweakDivider,
|
|
113
|
+
tweakButton,
|
|
114
|
+
tweakEngineDefaults,
|
|
115
|
+
|
|
109
116
|
// Math3D
|
|
110
117
|
vec3,
|
|
111
118
|
isVector3,
|
|
@@ -132,6 +139,7 @@ export
|
|
|
132
139
|
Camera3D,
|
|
133
140
|
EngineObject3D,
|
|
134
141
|
Mesh,
|
|
142
|
+
InstancedMesh3D,
|
|
135
143
|
buildLathe,
|
|
136
144
|
buildCylinder,
|
|
137
145
|
buildSphere,
|
|
@@ -146,6 +154,7 @@ export
|
|
|
146
154
|
buildExtrude,
|
|
147
155
|
buildText3D,
|
|
148
156
|
HeightMap,
|
|
157
|
+
VoxelMap,
|
|
149
158
|
Light3D,
|
|
150
159
|
DirectionalLight3D,
|
|
151
160
|
CameraControl3D,
|
|
@@ -157,6 +166,12 @@ export
|
|
|
157
166
|
engineObjectsRaycast3D,
|
|
158
167
|
parseOBJ,
|
|
159
168
|
loadOBJ,
|
|
169
|
+
GLTFModel,
|
|
170
|
+
GLTFPart,
|
|
171
|
+
GLTFObject,
|
|
172
|
+
GLTFAnimation,
|
|
173
|
+
parseGLTF,
|
|
174
|
+
loadGLTF,
|
|
160
175
|
debugBox3D,
|
|
161
176
|
debugSphere3D,
|
|
162
177
|
debugLine3D,
|