emeraldengine 3.0.0 → 3.1.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.
Files changed (103) hide show
  1. package/README.md +1498 -1659
  2. package/dist/types/index.d.ts +4 -1
  3. package/dist/types/src/Animator.d.ts +1 -1
  4. package/dist/types/src/CollisionLayers.d.ts +2 -2
  5. package/dist/types/src/Color.d.ts +1 -0
  6. package/dist/types/src/Drawable.d.ts +1 -1
  7. package/dist/types/src/EmeraldDB.d.ts +2 -2
  8. package/dist/types/src/InstancedTexture.d.ts +17 -2
  9. package/dist/types/src/Material.d.ts +2 -2
  10. package/dist/types/src/MathUtils.d.ts +2 -1
  11. package/dist/types/src/ParticleEmitter.d.ts +1 -1
  12. package/dist/types/src/Physics.d.ts +148 -18
  13. package/dist/types/src/Scene.d.ts +1 -1
  14. package/dist/types/src/Shaders.d.ts +2 -2
  15. package/dist/types/src/Tilemap.d.ts +1 -1
  16. package/dist/types/src/UI.d.ts +1 -1
  17. package/dist/types/src/components/Behaviour.d.ts +2 -2
  18. package/dist/types/src/components/Collider.d.ts +7 -1
  19. package/dist/types/src/components/GameObject.d.ts +2 -2
  20. package/dist/types/src/components/PolygonCollider.d.ts +33 -0
  21. package/dist/types/src/components/RigidBody.d.ts +281 -8
  22. package/dist/types/src/importers/Aseprite.d.ts +2 -2
  23. package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
  24. package/dist/types/src/importers/TiledMap.d.ts +1 -1
  25. package/dist/types/src/managers/EventManager.d.ts +1 -1
  26. package/dist/types/src/managers/Gamepad.d.ts +102 -0
  27. package/dist/types/src/managers/InputManager.d.ts +93 -2
  28. package/dist/types/src/managers/NetworkManager.d.ts +2 -2
  29. package/dist/types/src/managers/RenderStats.d.ts +1 -1
  30. package/dist/types/src/managers/TextureManager.d.ts +1 -1
  31. package/dist/types/src/physics/AABB.d.ts +92 -0
  32. package/dist/types/src/physics/Body.d.ts +435 -0
  33. package/dist/types/src/physics/BodyType.d.ts +6 -0
  34. package/dist/types/src/physics/BroadPhase.d.ts +210 -0
  35. package/dist/types/src/physics/Collision.d.ts +102 -0
  36. package/dist/types/src/physics/Contact.d.ts +206 -0
  37. package/dist/types/src/physics/ContactSolver.d.ts +108 -0
  38. package/dist/types/src/physics/Distance.d.ts +54 -0
  39. package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
  40. package/dist/types/src/physics/Fixture.d.ts +221 -0
  41. package/dist/types/src/physics/Island.d.ts +52 -0
  42. package/dist/types/src/physics/Joint.d.ts +59 -0
  43. package/dist/types/src/physics/Math2D.d.ts +371 -0
  44. package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
  45. package/dist/types/src/physics/Settings.d.ts +22 -0
  46. package/dist/types/src/physics/Shapes.d.ts +207 -0
  47. package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
  48. package/dist/types/src/physics/World.d.ts +274 -0
  49. package/dist/types/src/physics/index.d.ts +34 -0
  50. package/index.js +6 -0
  51. package/package.json +2 -3
  52. package/src/Animator.js +1 -1
  53. package/src/CollisionLayers.js +3 -3
  54. package/src/Color.js +8 -0
  55. package/src/Drawable.js +1 -1
  56. package/src/Emerald.js +1 -1
  57. package/src/EmeraldDB.js +2 -2
  58. package/src/InstancedTexture.js +57 -9
  59. package/src/Material.js +2 -2
  60. package/src/MathUtils.js +2 -1
  61. package/src/ParticleEmitter.js +1 -1
  62. package/src/Physics.js +270 -60
  63. package/src/Scene.js +1 -1
  64. package/src/Shaders.js +20 -20
  65. package/src/Tilemap.js +1 -1
  66. package/src/UI.js +1 -1
  67. package/src/components/Behaviour.js +2 -2
  68. package/src/components/BoxCollider.js +7 -9
  69. package/src/components/BoxColliderDebug.js +3 -4
  70. package/src/components/CircleCollider.js +7 -9
  71. package/src/components/CircleColliderDebug.js +3 -2
  72. package/src/components/Collider.js +13 -3
  73. package/src/components/GameObject.js +2 -2
  74. package/src/components/PolygonCollider.js +55 -0
  75. package/src/components/RigidBody.js +441 -14
  76. package/src/importers/Aseprite.js +2 -2
  77. package/src/importers/ForgeLevel.js +581 -0
  78. package/src/importers/TiledMap.js +1 -1
  79. package/src/managers/EventManager.js +1 -1
  80. package/src/managers/Gamepad.js +126 -0
  81. package/src/managers/InputManager.js +129 -3
  82. package/src/managers/NetworkManager.js +2 -2
  83. package/src/managers/RenderStats.js +1 -1
  84. package/src/managers/TextureManager.js +1 -1
  85. package/src/physics/AABB.js +207 -0
  86. package/src/physics/Body.js +862 -0
  87. package/src/physics/BodyType.js +16 -0
  88. package/src/physics/BroadPhase.js +641 -0
  89. package/src/physics/Collision.js +534 -0
  90. package/src/physics/Contact.js +500 -0
  91. package/src/physics/ContactSolver.js +526 -0
  92. package/src/physics/Distance.js +403 -0
  93. package/src/physics/DistanceJoint.js +227 -0
  94. package/src/physics/Fixture.js +346 -0
  95. package/src/physics/Island.js +203 -0
  96. package/src/physics/Joint.js +78 -0
  97. package/src/physics/Math2D.js +573 -0
  98. package/src/physics/RevoluteJoint.js +278 -0
  99. package/src/physics/Settings.js +78 -0
  100. package/src/physics/Shapes.js +549 -0
  101. package/src/physics/TimeOfImpact.js +87 -0
  102. package/src/physics/World.js +731 -0
  103. package/src/physics/index.js +79 -0
@@ -0,0 +1,581 @@
1
+ import GameObject from "../components/GameObject.js";
2
+ import Instance from "../Instance.js";
3
+ import InstancedTexture from "../InstancedTexture.js";
4
+ import RigidBody from "../components/RigidBody.js";
5
+ import BoxCollider from "../components/BoxCollider.js";
6
+ import PolygonCollider from "../components/PolygonCollider.js";
7
+ import { Vector2, Vector3 } from "../Physics.js";
8
+
9
+ /**
10
+ * @class ForgeLevel
11
+ * @description Imports an Emerald Tile Forge level export (`level.json`).
12
+ *
13
+ * Each tile layer is drawn as **one draw call**: a single {@link InstancedTexture}
14
+ * whose instances each carry their own UV quad via `Instance.setTexCoords`, so
15
+ * tiles can be sliced out of an atlas with margins, spacing and per-tile flips
16
+ * without ever switching textures.
17
+ *
18
+ * Solid tiles become static physics bodies. Runs of horizontally adjacent
19
+ * full-tile colliders are merged into one box each, so an 18-tile floor costs
20
+ * one body instead of eighteen. A tile whose collider is a shape other than
21
+ * the full tile (a ramp, a wedge) gets a real {@link PolygonCollider} built
22
+ * from that shape's own points, not a bounding-box approximation.
23
+ *
24
+ * @example
25
+ * import level from "./level.json";
26
+ * const map = ForgeLevel.load(level, { scene, physics, filter: LAYERS.ground });
27
+ * emerald.setBackgroundColor(Color.fromHex(map.background));
28
+ */
29
+
30
+ /** Tiled-compatible flip flags, packed into the high bits of a gid. */
31
+ const FLIP_H = 0x80000000;
32
+ const FLIP_V = 0x40000000;
33
+ const FLIP_D = 0x20000000;
34
+ const GID_MASK = 0x1fffffff;
35
+
36
+ class ForgeLevel {
37
+ /**
38
+ * @method load
39
+ * @description Builds renderable and collidable objects from a Forge export.
40
+ * @param {Object} data - The parsed `level.json`
41
+ * @param {Object} options
42
+ * @param {Scene} options.scene - Scene to add the layer objects to
43
+ * @param {Physics} [options.physics] - Physics engine; omit to skip colliders
44
+ * @param {Object} [options.filter] - Collision filter spec for the colliders
45
+ * @param {Object} [options.ownerObject] - Owner reported by collision events
46
+ * @param {boolean} [options.pixelart=true] - NEAREST filtering for the atlas
47
+ * @param {string} [options.layerOrder="top-first"] - Whether `layers[0]` is
48
+ * the topmost layer ("top-first") or the bottommost ("bottom-first")
49
+ * @returns {Object} - `{ tileSize, cols, rows, width, height, background,
50
+ * bounds, layers, colliders, objects, entityTypes, toWorld }`
51
+ */
52
+ static load(data, options = {}) {
53
+ const {
54
+ scene,
55
+ physics = null,
56
+ filter = null,
57
+ ownerObject = null,
58
+ pixelart = true,
59
+ layerOrder = "top-first",
60
+ } = options;
61
+
62
+ const tileSize = data.tileSize;
63
+ const cols = data.cols;
64
+ const rows = data.rows;
65
+
66
+ const originX = -(cols * tileSize) / 2;
67
+ const originY = (rows * tileSize) / 2;
68
+
69
+ /**
70
+ * Grid cell -> world centre. Forge counts rows downwards from the top;
71
+ * Emerald's y axis points up, so the row term is subtracted.
72
+ */
73
+ const toWorld = (col, row) => ({
74
+ x: originX + (col + 0.5) * tileSize,
75
+ y: originY - (row + 0.5) * tileSize,
76
+ });
77
+
78
+ const layers = [];
79
+ const colliders = [];
80
+ const objects = [];
81
+ let tileLayerIndex = 0;
82
+
83
+ for (const layer of data.layers) {
84
+ if (layer.visible === false) continue;
85
+
86
+ if (layer.type === "object") {
87
+ objects.push(...ForgeLevel._readObjects(layer, { toWorld, tileSize }));
88
+ continue;
89
+ }
90
+
91
+ if (layer.type !== "tile") {
92
+ console.warn(
93
+ `[ForgeLevel] Skipping layer "${layer.name}" of unsupported type "${layer.type}".`
94
+ );
95
+ continue;
96
+ }
97
+
98
+ const depth =
99
+ layerOrder === "bottom-first" ? tileLayerIndex : -tileLayerIndex;
100
+
101
+ const built = ForgeLevel._buildTileLayer(data, layer, {
102
+ cols,
103
+ rows,
104
+ tileSize,
105
+ toWorld,
106
+ pixelart,
107
+ depth,
108
+ });
109
+ tileLayerIndex++;
110
+ for (const object of built) {
111
+ if (scene) scene.add(object.gameObject);
112
+ layers.push(object);
113
+ }
114
+ }
115
+
116
+ if (physics) {
117
+ colliders.push(
118
+ ...ForgeLevel._buildColliders(data, {
119
+ cols,
120
+ rows,
121
+ tileSize,
122
+ physics,
123
+ filter,
124
+ ownerObject,
125
+ })
126
+ );
127
+ }
128
+
129
+ return {
130
+ tileSize,
131
+ cols,
132
+ rows,
133
+ width: cols * tileSize,
134
+ height: rows * tileSize,
135
+ background: data.background,
136
+ bounds: {
137
+ minX: originX,
138
+ maxX: originX + cols * tileSize,
139
+ minY: originY - rows * tileSize,
140
+ maxY: originY,
141
+ },
142
+ layers,
143
+ colliders,
144
+ objects,
145
+ entityTypes: data.entityTypes || [],
146
+ toWorld,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * @method _readObjects
152
+ * @description Converts one object layer's entries from grid cells to world
153
+ * space. An object's `x`/`y` is its top-left cell and `w`/`h` its size in
154
+ * cells, so a 1x1 object resolves to that cell's centre.
155
+ * @returns {Array<Object>} - `{ type, name, x, y, width, height, props, layer, cell }`
156
+ * @private
157
+ */
158
+ static _readObjects(layer, { toWorld, tileSize }) {
159
+ const out = [];
160
+ for (const object of layer.objects || []) {
161
+ const w = object.w ?? 1;
162
+ const h = object.h ?? 1;
163
+ const centre = toWorld(object.x + (w - 1) / 2, object.y + (h - 1) / 2);
164
+ out.push({
165
+ type: object.type || "",
166
+ name: object.name || object.type || "object",
167
+ x: centre.x,
168
+ y: centre.y,
169
+ width: w * tileSize,
170
+ height: h * tileSize,
171
+ props: object.props || {},
172
+ layer: layer.name,
173
+ cell: { col: object.x, row: object.y },
174
+ });
175
+ }
176
+ return out;
177
+ }
178
+
179
+ /**
180
+ * @method _buildTileLayer
181
+ * @description Turns one tile layer into an InstancedTexture per tileset it
182
+ * references (a texture switch is a new draw call, so tiles are grouped by
183
+ * the atlas they come from).
184
+ * @returns {Array<Object>} - `{ gameObject, instanced, tileset, layer, count }`
185
+ * @private
186
+ */
187
+ static _buildTileLayer(data, layer, ctx) {
188
+ const { cols, rows, tileSize, toWorld, pixelart, depth = 0 } = ctx;
189
+
190
+ const buckets = new Map();
191
+ for (let row = 0; row < rows; row++) {
192
+ for (let col = 0; col < cols; col++) {
193
+ const raw = layer.data[row * cols + col];
194
+ if (!raw) continue;
195
+
196
+ const gid = raw & GID_MASK;
197
+ const tileset = ForgeLevel._tilesetFor(data, gid);
198
+ if (!tileset) {
199
+ console.warn(`[ForgeLevel] gid ${gid} matches no tileset.`);
200
+ continue;
201
+ }
202
+ if (!buckets.has(tileset)) buckets.set(tileset, []);
203
+ buckets.get(tileset).push({
204
+ col,
205
+ row,
206
+ index: gid - tileset.firstgid,
207
+ flipH: (raw & FLIP_H) !== 0,
208
+ flipV: (raw & FLIP_V) !== 0,
209
+ flipD: (raw & FLIP_D) !== 0,
210
+ });
211
+ }
212
+ }
213
+
214
+ const built = [];
215
+ for (const [tileset, cells] of buckets) {
216
+ const instanced = new InstancedTexture(
217
+ tileset.image,
218
+ cells.length,
219
+ 0,
220
+ 0,
221
+ 1,
222
+ 1,
223
+ 0,
224
+ false,
225
+ pixelart,
226
+ false
227
+ );
228
+
229
+ const gameObject = new GameObject(
230
+ `${layer.name || "layer"}:${tileset.name}`,
231
+ new Vector3(0, 0, depth),
232
+ 0,
233
+ new Vector2(1, 1)
234
+ );
235
+ gameObject.addComponent(instanced);
236
+ if (layer.opacity != null) gameObject.setOpacity(layer.opacity);
237
+
238
+ const half = tileSize / 2;
239
+ for (const cell of cells) {
240
+ const position = toWorld(cell.col, cell.row);
241
+ const instance = new Instance(
242
+ "tile",
243
+ new Vector3(position.x, position.y, 0),
244
+ new Vector2(half, half),
245
+ 0
246
+ );
247
+ if (cell.flipD) {
248
+ console.warn(
249
+ "[ForgeLevel] Diagonal tile flips are not supported; tile left unrotated."
250
+ );
251
+ }
252
+ instance.setTexCoords(
253
+ ForgeLevel.tileTexCoords(tileset, cell.index, cell.flipH, cell.flipV)
254
+ );
255
+ instanced.addInstance(instance);
256
+ }
257
+
258
+ instanced.setStatic(true);
259
+
260
+ built.push({
261
+ gameObject,
262
+ instanced,
263
+ tileset,
264
+ layer,
265
+ count: cells.length,
266
+ depth,
267
+ parallax: { x: layer.parallaxX ?? 1, y: layer.parallaxY ?? 1 },
268
+ });
269
+
270
+ if ((layer.parallaxX ?? 1) !== 1 || (layer.parallaxY ?? 1) !== 1) {
271
+ console.warn(
272
+ `[ForgeLevel] Layer "${layer.name}" requests parallax ` +
273
+ `(${layer.parallaxX}, ${layer.parallaxY}); scroll it yourself against the camera.`
274
+ );
275
+ }
276
+ }
277
+
278
+ return built;
279
+ }
280
+
281
+ /**
282
+ * @method tileTexCoords
283
+ * @description UV quad for one tile of an atlas, in the corner order
284
+ * `Drawable.getFrameTexCoords` uses: (R,B) (L,B) (R,T) (L,T). Coordinates are
285
+ * inset by half a texel so neighbouring tiles never bleed into each other,
286
+ * and the v axis is flipped because GL samples from the bottom up.
287
+ * @param {Object} tileset - A Forge tileset entry
288
+ * @param {number} index - Tile index within the tileset
289
+ * @param {boolean} [flipH=false]
290
+ * @param {boolean} [flipV=false]
291
+ * @returns {number[]} - 8 UV floats
292
+ */
293
+ static tileTexCoords(tileset, index, flipH = false, flipV = false) {
294
+ const {
295
+ imageW,
296
+ imageH,
297
+ tileW,
298
+ tileH,
299
+ columns,
300
+ marginX = 0,
301
+ marginY = 0,
302
+ spacingX = 0,
303
+ spacingY = 0,
304
+ } = tileset;
305
+
306
+ const col = index % columns;
307
+ const row = Math.floor(index / columns);
308
+ const px = marginX + col * (tileW + spacingX);
309
+ const py = marginY + row * (tileH + spacingY);
310
+
311
+ const ix = 0.5 / imageW;
312
+ const iy = 0.5 / imageH;
313
+
314
+ let uRight = (px + tileW) / imageW - ix;
315
+ let uLeft = px / imageW + ix;
316
+ let vTop = (imageH - py - tileH) / imageH + iy;
317
+ let vBottom = (imageH - py) / imageH - iy;
318
+
319
+ if (flipH) {
320
+ const swap = uRight;
321
+ uRight = uLeft;
322
+ uLeft = swap;
323
+ }
324
+ if (flipV) {
325
+ const swap = vTop;
326
+ vTop = vBottom;
327
+ vBottom = swap;
328
+ }
329
+
330
+ return [uRight, vBottom, uLeft, vBottom, uRight, vTop, uLeft, vTop];
331
+ }
332
+
333
+ /**
334
+ * @method _tilesetFor
335
+ * @description Finds the tileset that owns a gid (the one with the largest
336
+ * firstgid not greater than it).
337
+ * @private
338
+ */
339
+ static _tilesetFor(data, gid) {
340
+ let best = null;
341
+ for (const tileset of data.tilesets) {
342
+ if (
343
+ gid >= tileset.firstgid &&
344
+ (!best || tileset.firstgid > best.firstgid)
345
+ ) {
346
+ best = tileset;
347
+ }
348
+ }
349
+ return best;
350
+ }
351
+
352
+ /**
353
+ * @method _buildColliders
354
+ * @description Creates static bodies for solid tiles. Full-tile colliders are
355
+ * merged into horizontal runs, since BoxCollider is cheaper than a polygon
356
+ * with the same 4 corners. A tile whose collider shape isn't the full tile
357
+ * gets its own {@link PolygonCollider} built from that shape's actual
358
+ * points, in world space, not a bounding-box stand-in.
359
+ * @returns {Array<RigidBody>}
360
+ * @private
361
+ */
362
+ static _buildColliders(data, ctx) {
363
+ const { cols, rows, tileSize, physics, filter, ownerObject } = ctx;
364
+ const scale = physics.getScale();
365
+ const originX = -(cols * tileSize) / 2;
366
+ const originY = (rows * tileSize) / 2;
367
+ const bodies = [];
368
+ let warnedDegenerate = false;
369
+
370
+ const shapeAt = [];
371
+ for (let row = 0; row < rows; row++) {
372
+ shapeAt[row] = [];
373
+ for (let col = 0; col < cols; col++) {
374
+ shapeAt[row][col] = null;
375
+ for (const layer of data.layers) {
376
+ if (layer.type !== "tile") continue;
377
+ const raw = layer.data[row * cols + col];
378
+ if (!raw) continue;
379
+ const gid = raw & GID_MASK;
380
+ const tileset = ForgeLevel._tilesetFor(data, gid);
381
+ if (!tileset || !tileset.colliders) continue;
382
+ const shape = tileset.colliders[String(gid - tileset.firstgid)];
383
+ if (!shape || !shape.wall) continue;
384
+ shapeAt[row][col] = {
385
+ bounds: ForgeLevel._boundsOf(shape.points),
386
+ points: shape.points,
387
+ };
388
+ break;
389
+ }
390
+ }
391
+ }
392
+
393
+ const isFullTile = (cell) =>
394
+ cell &&
395
+ cell.bounds.w > 0.99 &&
396
+ cell.bounds.h > 0.99 &&
397
+ cell.bounds.x < 0.01 &&
398
+ cell.bounds.y < 0.01;
399
+
400
+ for (let row = 0; row < rows; row++) {
401
+ let col = 0;
402
+ while (col < cols) {
403
+ const cell = shapeAt[row][col];
404
+ if (!cell) {
405
+ col++;
406
+ continue;
407
+ }
408
+
409
+ if (!isFullTile(cell)) {
410
+ try {
411
+ bodies.push(
412
+ ForgeLevel._staticPolygon(physics, {
413
+ points: cell.points,
414
+ originX,
415
+ originY,
416
+ col,
417
+ row,
418
+ tileSize,
419
+ scale,
420
+ filter,
421
+ ownerObject,
422
+ })
423
+ );
424
+ } catch (error) {
425
+ if (!warnedDegenerate) {
426
+ warnedDegenerate = true;
427
+ console.warn(
428
+ `[ForgeLevel] A tile collider shape is degenerate (${error.message}); using its bounding box instead.`
429
+ );
430
+ }
431
+ const centerX =
432
+ originX +
433
+ col * tileSize +
434
+ (cell.bounds.x + cell.bounds.w / 2) * tileSize;
435
+ const centerY =
436
+ originY -
437
+ row * tileSize -
438
+ (cell.bounds.y + cell.bounds.h / 2) * tileSize;
439
+ bodies.push(
440
+ ForgeLevel._staticBox(
441
+ physics,
442
+ centerX,
443
+ centerY,
444
+ (cell.bounds.w * tileSize) / 2,
445
+ (cell.bounds.h * tileSize) / 2,
446
+ scale,
447
+ filter,
448
+ ownerObject
449
+ )
450
+ );
451
+ }
452
+ col++;
453
+ continue;
454
+ }
455
+
456
+ const runStart = col;
457
+ while (col < cols && isFullTile(shapeAt[row][col])) col++;
458
+ const runLength = col - runStart;
459
+ const runWidth = runLength * tileSize;
460
+ const centerX = originX + runStart * tileSize + runWidth / 2;
461
+ const centerY = originY - (row + 0.5) * tileSize;
462
+
463
+ bodies.push(
464
+ ForgeLevel._staticBox(
465
+ physics,
466
+ centerX,
467
+ centerY,
468
+ runWidth / 2,
469
+ tileSize / 2,
470
+ scale,
471
+ filter,
472
+ ownerObject
473
+ )
474
+ );
475
+ }
476
+ }
477
+
478
+ return bodies;
479
+ }
480
+
481
+ /**
482
+ * @method _staticBox
483
+ * @description Creates one static body with a box collider, in world pixels.
484
+ * @private
485
+ */
486
+ static _staticBox(physics, x, y, halfW, halfH, scale, filter, ownerObject) {
487
+ const body = new RigidBody(
488
+ physics,
489
+ "static",
490
+ new Vector2(x, y),
491
+ true,
492
+ ownerObject
493
+ );
494
+ new BoxCollider(
495
+ body,
496
+ new Vector2(halfW / scale, halfH / scale),
497
+ 0,
498
+ 0.6,
499
+ 0,
500
+ false,
501
+ ownerObject,
502
+ filter
503
+ );
504
+ return body;
505
+ }
506
+
507
+ /**
508
+ * @method _staticPolygon
509
+ * @description Creates one static body with a polygon collider built from a
510
+ * tile's own collider shape. The body sits at the shape's bounding-box
511
+ * centre; the polygon's points are converted from Forge's normalised,
512
+ * top-down tile space into local physics units around that centre.
513
+ * @private
514
+ */
515
+ static _staticPolygon(physics, ctx) {
516
+ const {
517
+ points,
518
+ originX,
519
+ originY,
520
+ col,
521
+ row,
522
+ tileSize,
523
+ scale,
524
+ filter,
525
+ ownerObject,
526
+ } = ctx;
527
+ const bounds = ForgeLevel._boundsOf(points);
528
+ const centerX =
529
+ originX + col * tileSize + (bounds.x + bounds.w / 2) * tileSize;
530
+ const centerY =
531
+ originY - row * tileSize - (bounds.y + bounds.h / 2) * tileSize;
532
+
533
+ const body = new RigidBody(
534
+ physics,
535
+ "static",
536
+ new Vector2(centerX, centerY),
537
+ true,
538
+ ownerObject
539
+ );
540
+
541
+ const localPoints = points.map(([px, py]) => {
542
+ const worldX = originX + col * tileSize + px * tileSize;
543
+ const worldY = originY - row * tileSize - py * tileSize;
544
+ return { x: (worldX - centerX) / scale, y: (worldY - centerY) / scale };
545
+ });
546
+
547
+ new PolygonCollider(
548
+ body,
549
+ localPoints,
550
+ 0,
551
+ 0.6,
552
+ 0,
553
+ false,
554
+ ownerObject,
555
+ filter
556
+ );
557
+ return body;
558
+ }
559
+
560
+ /**
561
+ * @method _boundsOf
562
+ * @description Normalised bounding box of a collider polygon.
563
+ * @returns {{x:number, y:number, w:number, h:number}}
564
+ * @private
565
+ */
566
+ static _boundsOf(points) {
567
+ let minX = Infinity;
568
+ let minY = Infinity;
569
+ let maxX = -Infinity;
570
+ let maxY = -Infinity;
571
+ for (const [x, y] of points) {
572
+ if (x < minX) minX = x;
573
+ if (y < minY) minY = y;
574
+ if (x > maxX) maxX = x;
575
+ if (y > maxY) maxY = y;
576
+ }
577
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
578
+ }
579
+ }
580
+
581
+ export default ForgeLevel;
@@ -8,7 +8,7 @@ const GID_MASK = 0x1fffffff;
8
8
  * @description Imports orthogonal maps exported from the Tiled editor
9
9
  * (https://www.mapeditor.org) in JSON format into the engine's Tilemap, plus
10
10
  * helpers to pull object layers (spawn points, triggers, etc.) out as plain
11
- * data. Pure parsing pass it the already-parsed JSON object (load it with
11
+ * data. Pure parsing: pass it the already-parsed JSON object (load it with
12
12
  * AssetManager.json or fetch).
13
13
  *
14
14
  * @example
@@ -200,7 +200,7 @@ class EventManager {
200
200
  * @method getTopObjectAt
201
201
  * @description Returns the topmost active object under a world-space point
202
202
  * along with the instance hit (if the object is an InstancedTexture). Topmost
203
- * means highest z, with later scene order breaking ties matching draw order.
203
+ * means highest z, with later scene order breaking ties, matching draw order.
204
204
  * @param {number} worldX - The world-space x coordinate
205
205
  * @param {number} worldY - The world-space y coordinate
206
206
  * @returns {{object: GameObject, instance: Instance|null}|null}