emeraldengine 2.2.1 → 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 +2198 -968
- package/dist/types/index.d.ts +75 -32
- package/dist/types/src/Animator.d.ts +50 -0
- package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
- package/dist/types/src/Camera.d.ts +122 -0
- package/dist/types/src/CameraController.d.ts +107 -0
- package/dist/types/src/CanvasText.d.ts +91 -0
- package/dist/types/src/CollisionLayers.d.ts +58 -0
- package/dist/types/{Color.d.ts → src/Color.d.ts} +6 -6
- package/dist/types/src/Coroutine.d.ts +111 -0
- package/dist/types/src/DebugOverlay.d.ts +86 -0
- package/dist/types/src/Drawable.d.ts +271 -0
- package/dist/types/src/Easing.d.ts +22 -0
- package/dist/types/src/Emerald.d.ts +420 -0
- package/dist/types/src/EmeraldDB.d.ts +159 -0
- package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
- package/dist/types/src/GLUtils.d.ts +4 -0
- package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
- package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +75 -23
- package/dist/types/src/Interpolator.d.ts +66 -0
- package/dist/types/src/Material.d.ts +87 -0
- package/dist/types/src/MathUtils.d.ts +81 -0
- package/dist/types/src/ParticleEmitter.d.ts +131 -0
- package/dist/types/src/Physics.d.ts +337 -0
- package/dist/types/src/Pool.d.ts +53 -0
- package/dist/types/src/PostEffects.d.ts +68 -0
- package/dist/types/src/PostProcessor.d.ts +124 -0
- package/dist/types/src/RenderTarget.d.ts +56 -0
- package/dist/types/src/Scene.d.ts +62 -0
- package/dist/types/src/ScreenEffects.d.ts +111 -0
- package/dist/types/src/Serializer.d.ts +86 -0
- package/dist/types/src/Shaders.d.ts +2 -0
- package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
- package/dist/types/src/SpatialGrid.d.ts +50 -0
- package/dist/types/src/SpriteBatch.d.ts +89 -0
- package/dist/types/src/StateMachine.d.ts +59 -0
- package/dist/types/src/Storage.d.ts +89 -0
- package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
- package/dist/types/src/TextureAtlas.d.ts +55 -0
- package/dist/types/src/Tilemap.d.ts +91 -0
- package/dist/types/src/Time.d.ts +53 -0
- package/dist/types/src/Timer.d.ts +54 -0
- package/dist/types/src/Transform.d.ts +94 -0
- package/dist/types/src/Tween.d.ts +81 -0
- package/dist/types/src/UI.d.ts +121 -0
- package/dist/types/src/components/Behaviour.d.ts +71 -0
- package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
- package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
- package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
- package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
- package/dist/types/src/components/Collider.d.ts +102 -0
- package/dist/types/src/components/GameObject.d.ts +135 -0
- package/dist/types/src/components/PolygonCollider.d.ts +33 -0
- package/dist/types/src/components/RigidBody.d.ts +456 -0
- package/dist/types/src/importers/Aseprite.d.ts +79 -0
- package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
- package/dist/types/src/importers/TiledMap.d.ts +62 -0
- package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
- package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
- package/dist/types/src/managers/AssetManager.d.ts +116 -0
- package/dist/types/src/managers/AudioManager.d.ts +259 -0
- package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
- package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
- package/dist/types/src/managers/GLManager.d.ts +84 -0
- package/dist/types/src/managers/GLState.d.ts +37 -0
- package/dist/types/src/managers/Gamepad.d.ts +102 -0
- package/dist/types/src/managers/IDManager.d.ts +31 -0
- package/dist/types/src/managers/InputManager.d.ts +381 -0
- package/dist/types/src/managers/NetworkManager.d.ts +93 -0
- package/dist/types/src/managers/RenderStats.d.ts +34 -0
- package/dist/types/src/managers/SceneManager.d.ts +44 -0
- package/dist/types/src/managers/ShaderManager.d.ts +55 -0
- package/dist/types/src/managers/TextureManager.d.ts +102 -0
- package/dist/types/src/particlesystem/Particle.d.ts +64 -0
- package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
- package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
- 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 +78 -0
- package/package.json +73 -60
- package/src/Animator.js +95 -0
- package/src/BitmapText.js +6 -5
- package/src/Camera.js +183 -0
- package/src/CameraController.js +192 -0
- package/src/CanvasText.js +281 -0
- package/src/CollisionLayers.js +86 -0
- package/src/Color.js +26 -18
- package/src/Coroutine.js +259 -0
- package/src/DebugOverlay.js +246 -0
- package/src/Drawable.js +842 -582
- package/src/Easing.js +57 -0
- package/src/Emerald.js +1150 -459
- package/src/EmeraldDB.js +328 -0
- package/src/FPSCounter.js +43 -43
- package/src/GLUtils.js +60 -67
- package/src/Instance.js +41 -5
- package/src/InstancedTexture.js +308 -127
- package/src/Interpolator.js +124 -0
- package/src/Material.js +202 -0
- package/src/MathUtils.js +134 -0
- package/src/ParticleEmitter.js +284 -0
- package/src/Physics.js +431 -40
- package/src/Pool.js +85 -0
- package/src/PostEffects.js +296 -0
- package/src/PostProcessor.js +304 -0
- package/src/RenderTarget.js +134 -0
- package/src/Scene.js +115 -83
- package/src/ScreenEffects.js +266 -0
- package/src/Serializer.js +131 -0
- package/src/Shaders.js +150 -165
- package/src/Shapes.js +118 -129
- package/src/SpatialGrid.js +111 -0
- package/src/SpriteBatch.js +299 -0
- package/src/StateMachine.js +82 -0
- package/src/Storage.js +175 -47
- package/src/Texture.js +58 -67
- package/src/TextureAtlas.js +96 -0
- package/src/Tilemap.js +274 -0
- package/src/Time.js +51 -6
- package/src/Timer.js +99 -0
- package/src/Transform.js +100 -7
- package/src/Tween.js +160 -0
- package/src/UI.js +394 -0
- package/src/components/Behaviour.js +90 -0
- package/src/components/BoxCollider.js +29 -12
- package/src/components/BoxColliderDebug.js +3 -4
- package/src/components/CircleCollider.js +26 -12
- package/src/components/CircleColliderDebug.js +25 -24
- package/src/components/Collider.js +150 -34
- package/src/components/GameObject.js +130 -21
- package/src/components/PolygonCollider.js +55 -0
- package/src/components/RigidBody.js +556 -8
- package/src/importers/Aseprite.js +142 -0
- package/src/importers/ForgeLevel.js +581 -0
- package/src/importers/TiledMap.js +158 -0
- package/src/lights/DirectionalLight.js +6 -15
- package/src/lights/PointLight.js +4 -4
- package/src/managers/AssetManager.js +239 -0
- package/src/managers/AudioManager.js +565 -146
- package/src/managers/EventManager.js +488 -477
- package/src/managers/GLManager.js +57 -0
- package/src/managers/GLState.js +70 -0
- package/src/managers/Gamepad.js +126 -0
- package/src/managers/IDManager.js +24 -2
- package/src/managers/InputManager.js +905 -0
- package/src/managers/NetworkManager.js +178 -0
- package/src/managers/RenderStats.js +34 -0
- package/src/managers/SceneManager.js +30 -0
- package/src/managers/ShaderManager.js +0 -2
- package/src/managers/TextureManager.js +218 -0
- package/src/particlesystem/Particle.js +82 -7
- package/src/particlesystem/ParticleSettings.js +21 -3
- package/src/particlesystem/Particles.js +80 -31
- 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
- package/dist/types/Drawable.d.ts +0 -157
- package/dist/types/Emerald.d.ts +0 -73
- package/dist/types/GLUtils.d.ts +0 -6
- package/dist/types/Physics.d.ts +0 -139
- package/dist/types/Scene.d.ts +0 -39
- package/dist/types/Shaders.d.ts +0 -4
- package/dist/types/Storage.d.ts +0 -46
- package/dist/types/Time.d.ts +0 -22
- package/dist/types/Transform.d.ts +0 -41
- package/dist/types/components/BoxColliderDebug.d.ts +0 -19
- package/dist/types/components/CircleColliderDebug.d.ts +0 -19
- package/dist/types/components/Collider.d.ts +0 -53
- package/dist/types/components/GameObject.d.ts +0 -72
- package/dist/types/components/RigidBody.d.ts +0 -104
- package/dist/types/managers/AudioManager.d.ts +0 -60
- package/dist/types/managers/GLManager.d.ts +0 -47
- package/dist/types/managers/IDManager.d.ts +0 -21
- package/dist/types/managers/SceneManager.d.ts +0 -22
- package/dist/types/particlesystem/Particle.d.ts +0 -42
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @class Aseprite
|
|
3
|
+
* @description Imports sprite-sheet metadata exported from Aseprite
|
|
4
|
+
* (File ▸ Export Sprite Sheet, with "JSON Data" on) and turns its frame tags
|
|
5
|
+
* into engine animation clips. Works with both the Hash and Array JSON layouts.
|
|
6
|
+
* Pure parsing: pass it the already-parsed JSON object.
|
|
7
|
+
*
|
|
8
|
+
* Assumes the sheet is a uniform grid in frame order (the common case), so the
|
|
9
|
+
* frame indices line up with the engine's Texture/Animator frame numbering.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const cfg = Aseprite.spriteConfig(sheetJson); // {frameWidth, framesPerRow, ...}
|
|
13
|
+
* const tex = new Texture("hero.png", cfg.frameWidth, cfg.frameHeight,
|
|
14
|
+
* cfg.framesPerRow, cfg.totalFrames, 0, false);
|
|
15
|
+
* obj.addComponent(tex);
|
|
16
|
+
* const anim = new Animator();
|
|
17
|
+
* obj.addComponent(anim);
|
|
18
|
+
* Aseprite.applyTo(anim, sheetJson); // registers "idle", "run", ... clips
|
|
19
|
+
* anim.play("run");
|
|
20
|
+
*/
|
|
21
|
+
class Aseprite {
|
|
22
|
+
/**
|
|
23
|
+
* @method frames
|
|
24
|
+
* @description Returns the frames as an ordered array regardless of whether
|
|
25
|
+
* the export used the Hash (object) or Array layout.
|
|
26
|
+
* @param {Object} sheet - Parsed Aseprite JSON
|
|
27
|
+
* @returns {Array<Object>}
|
|
28
|
+
*/
|
|
29
|
+
static frames(sheet) {
|
|
30
|
+
const f = sheet.frames;
|
|
31
|
+
if (Array.isArray(f)) return f;
|
|
32
|
+
return f ? Object.values(f) : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @method spriteConfig
|
|
37
|
+
* @description Derives the uniform-grid sprite config (for Texture) from the
|
|
38
|
+
* first frame and the sheet size.
|
|
39
|
+
* @param {Object} sheet - Parsed Aseprite JSON
|
|
40
|
+
* @returns {{frameWidth:number, frameHeight:number, framesPerRow:number, totalFrames:number}}
|
|
41
|
+
*/
|
|
42
|
+
static spriteConfig(sheet) {
|
|
43
|
+
const frames = Aseprite.frames(sheet);
|
|
44
|
+
const first =
|
|
45
|
+
frames[0] && frames[0].frame ? frames[0].frame : { w: 0, h: 0 };
|
|
46
|
+
const frameWidth = first.w || 0;
|
|
47
|
+
const frameHeight = first.h || 0;
|
|
48
|
+
const sheetW =
|
|
49
|
+
(sheet.meta && sheet.meta.size && sheet.meta.size.w) || frameWidth;
|
|
50
|
+
return {
|
|
51
|
+
frameWidth,
|
|
52
|
+
frameHeight,
|
|
53
|
+
framesPerRow: frameWidth
|
|
54
|
+
? Math.max(1, Math.round(sheetW / frameWidth))
|
|
55
|
+
: 1,
|
|
56
|
+
totalFrames: frames.length,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @method durations
|
|
62
|
+
* @description Per-frame durations in milliseconds, in frame order.
|
|
63
|
+
* @param {Object} sheet - Parsed Aseprite JSON
|
|
64
|
+
* @returns {number[]}
|
|
65
|
+
*/
|
|
66
|
+
static durations(sheet) {
|
|
67
|
+
return Aseprite.frames(sheet).map((f) => f.duration || 100);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @private */
|
|
71
|
+
static _expandTag(tag) {
|
|
72
|
+
const from = tag.from;
|
|
73
|
+
const to = tag.to;
|
|
74
|
+
const forward = [];
|
|
75
|
+
for (let i = from; i <= to; i++) forward.push(i);
|
|
76
|
+
const dir = (tag.direction || "forward").toLowerCase();
|
|
77
|
+
if (dir === "reverse") return forward.slice().reverse();
|
|
78
|
+
if (dir === "pingpong" || dir === "ping-pong") {
|
|
79
|
+
const back = forward.slice(1, -1).reverse();
|
|
80
|
+
return forward.concat(back);
|
|
81
|
+
}
|
|
82
|
+
return forward;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @method toClips
|
|
87
|
+
* @description Builds clip descriptors from the sheet's frame tags. Each clip
|
|
88
|
+
* is `{ name, frames, speed }` where `frames` are frame indices and `speed`
|
|
89
|
+
* is the average frame duration (ms), the per-clip speed the Animator uses.
|
|
90
|
+
* If the sheet has no tags, a single "default" clip spanning all frames is
|
|
91
|
+
* returned.
|
|
92
|
+
* @param {Object} sheet - Parsed Aseprite JSON
|
|
93
|
+
* @returns {Array<{name:string, frames:number[], speed:number}>}
|
|
94
|
+
*/
|
|
95
|
+
static toClips(sheet) {
|
|
96
|
+
const durations = Aseprite.durations(sheet);
|
|
97
|
+
const avg = (indices) => {
|
|
98
|
+
if (!indices.length) return 100;
|
|
99
|
+
let sum = 0;
|
|
100
|
+
let n = 0;
|
|
101
|
+
for (const i of indices) {
|
|
102
|
+
sum += durations[i] != null ? durations[i] : 100;
|
|
103
|
+
n++;
|
|
104
|
+
}
|
|
105
|
+
return Math.round(sum / n);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const tags = (sheet.meta && sheet.meta.frameTags) || [];
|
|
109
|
+
if (!tags.length) {
|
|
110
|
+
const all = durations.map((_, i) => i);
|
|
111
|
+
return [{ name: "default", frames: all, speed: avg(all) }];
|
|
112
|
+
}
|
|
113
|
+
return tags.map((tag) => {
|
|
114
|
+
const frames = Aseprite._expandTag(tag);
|
|
115
|
+
return { name: tag.name, frames, speed: avg(frames) };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @method applyTo
|
|
121
|
+
* @description Registers every clip from the sheet on an Animator. Clips loop
|
|
122
|
+
* by default; pass `loop` as a boolean (applies to all) or an object mapping
|
|
123
|
+
* clip name -> boolean to override individual clips.
|
|
124
|
+
* @param {Animator} animator - Target animator
|
|
125
|
+
* @param {Object} sheet - Parsed Aseprite JSON
|
|
126
|
+
* @param {Object} [options] - { loop = true }
|
|
127
|
+
* @returns {Animator} - The animator (for chaining)
|
|
128
|
+
*/
|
|
129
|
+
static applyTo(animator, sheet, options = {}) {
|
|
130
|
+
const loopOpt = options.loop ?? true;
|
|
131
|
+
for (const clip of Aseprite.toClips(sheet)) {
|
|
132
|
+
const loop =
|
|
133
|
+
typeof loopOpt === "object" && loopOpt !== null
|
|
134
|
+
? (loopOpt[clip.name] ?? true)
|
|
135
|
+
: !!loopOpt;
|
|
136
|
+
animator.addClip(clip.name, clip.frames, { speed: clip.speed, loop });
|
|
137
|
+
}
|
|
138
|
+
return animator;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default Aseprite;
|
|
@@ -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;
|