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.
- package/README.md +1498 -1659
- package/dist/types/index.d.ts +4 -1
- package/dist/types/src/Animator.d.ts +1 -1
- package/dist/types/src/CollisionLayers.d.ts +2 -2
- package/dist/types/src/Color.d.ts +1 -0
- package/dist/types/src/Drawable.d.ts +1 -1
- package/dist/types/src/EmeraldDB.d.ts +2 -2
- package/dist/types/src/InstancedTexture.d.ts +17 -2
- package/dist/types/src/Material.d.ts +2 -2
- package/dist/types/src/MathUtils.d.ts +2 -1
- package/dist/types/src/ParticleEmitter.d.ts +1 -1
- package/dist/types/src/Physics.d.ts +148 -18
- package/dist/types/src/Scene.d.ts +1 -1
- package/dist/types/src/Shaders.d.ts +2 -2
- package/dist/types/src/Tilemap.d.ts +1 -1
- package/dist/types/src/UI.d.ts +1 -1
- package/dist/types/src/components/Behaviour.d.ts +2 -2
- package/dist/types/src/components/Collider.d.ts +7 -1
- package/dist/types/src/components/GameObject.d.ts +2 -2
- package/dist/types/src/components/PolygonCollider.d.ts +33 -0
- package/dist/types/src/components/RigidBody.d.ts +281 -8
- package/dist/types/src/importers/Aseprite.d.ts +2 -2
- package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
- package/dist/types/src/importers/TiledMap.d.ts +1 -1
- package/dist/types/src/managers/EventManager.d.ts +1 -1
- package/dist/types/src/managers/Gamepad.d.ts +102 -0
- package/dist/types/src/managers/InputManager.d.ts +93 -2
- package/dist/types/src/managers/NetworkManager.d.ts +2 -2
- package/dist/types/src/managers/RenderStats.d.ts +1 -1
- package/dist/types/src/managers/TextureManager.d.ts +1 -1
- package/dist/types/src/physics/AABB.d.ts +92 -0
- package/dist/types/src/physics/Body.d.ts +435 -0
- package/dist/types/src/physics/BodyType.d.ts +6 -0
- package/dist/types/src/physics/BroadPhase.d.ts +210 -0
- package/dist/types/src/physics/Collision.d.ts +102 -0
- package/dist/types/src/physics/Contact.d.ts +206 -0
- package/dist/types/src/physics/ContactSolver.d.ts +108 -0
- package/dist/types/src/physics/Distance.d.ts +54 -0
- package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
- package/dist/types/src/physics/Fixture.d.ts +221 -0
- package/dist/types/src/physics/Island.d.ts +52 -0
- package/dist/types/src/physics/Joint.d.ts +59 -0
- package/dist/types/src/physics/Math2D.d.ts +371 -0
- package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
- package/dist/types/src/physics/Settings.d.ts +22 -0
- package/dist/types/src/physics/Shapes.d.ts +207 -0
- package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
- package/dist/types/src/physics/World.d.ts +274 -0
- package/dist/types/src/physics/index.d.ts +34 -0
- package/index.js +6 -0
- package/package.json +2 -3
- package/src/Animator.js +1 -1
- package/src/CollisionLayers.js +3 -3
- package/src/Color.js +8 -0
- package/src/Drawable.js +1 -1
- package/src/Emerald.js +1 -1
- package/src/EmeraldDB.js +2 -2
- package/src/InstancedTexture.js +57 -9
- package/src/Material.js +2 -2
- package/src/MathUtils.js +2 -1
- package/src/ParticleEmitter.js +1 -1
- package/src/Physics.js +270 -60
- package/src/Scene.js +1 -1
- package/src/Shaders.js +20 -20
- package/src/Tilemap.js +1 -1
- package/src/UI.js +1 -1
- package/src/components/Behaviour.js +2 -2
- package/src/components/BoxCollider.js +7 -9
- package/src/components/BoxColliderDebug.js +3 -4
- package/src/components/CircleCollider.js +7 -9
- package/src/components/CircleColliderDebug.js +3 -2
- package/src/components/Collider.js +13 -3
- package/src/components/GameObject.js +2 -2
- package/src/components/PolygonCollider.js +55 -0
- package/src/components/RigidBody.js +441 -14
- package/src/importers/Aseprite.js +2 -2
- package/src/importers/ForgeLevel.js +581 -0
- package/src/importers/TiledMap.js +1 -1
- package/src/managers/EventManager.js +1 -1
- package/src/managers/Gamepad.js +126 -0
- package/src/managers/InputManager.js +129 -3
- package/src/managers/NetworkManager.js +2 -2
- package/src/managers/RenderStats.js +1 -1
- package/src/managers/TextureManager.js +1 -1
- package/src/physics/AABB.js +207 -0
- package/src/physics/Body.js +862 -0
- package/src/physics/BodyType.js +16 -0
- package/src/physics/BroadPhase.js +641 -0
- package/src/physics/Collision.js +534 -0
- package/src/physics/Contact.js +500 -0
- package/src/physics/ContactSolver.js +526 -0
- package/src/physics/Distance.js +403 -0
- package/src/physics/DistanceJoint.js +227 -0
- package/src/physics/Fixture.js +346 -0
- package/src/physics/Island.js +203 -0
- package/src/physics/Joint.js +78 -0
- package/src/physics/Math2D.js +573 -0
- package/src/physics/RevoluteJoint.js +278 -0
- package/src/physics/Settings.js +78 -0
- package/src/physics/Shapes.js +549 -0
- package/src/physics/TimeOfImpact.js +87 -0
- package/src/physics/World.js +731 -0
- 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
|
|
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
|
|
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}
|