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.
Files changed (208) hide show
  1. package/README.md +2198 -968
  2. package/dist/types/index.d.ts +75 -32
  3. package/dist/types/src/Animator.d.ts +50 -0
  4. package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
  5. package/dist/types/src/Camera.d.ts +122 -0
  6. package/dist/types/src/CameraController.d.ts +107 -0
  7. package/dist/types/src/CanvasText.d.ts +91 -0
  8. package/dist/types/src/CollisionLayers.d.ts +58 -0
  9. package/dist/types/{Color.d.ts → src/Color.d.ts} +6 -6
  10. package/dist/types/src/Coroutine.d.ts +111 -0
  11. package/dist/types/src/DebugOverlay.d.ts +86 -0
  12. package/dist/types/src/Drawable.d.ts +271 -0
  13. package/dist/types/src/Easing.d.ts +22 -0
  14. package/dist/types/src/Emerald.d.ts +420 -0
  15. package/dist/types/src/EmeraldDB.d.ts +159 -0
  16. package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
  17. package/dist/types/src/GLUtils.d.ts +4 -0
  18. package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
  19. package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +75 -23
  20. package/dist/types/src/Interpolator.d.ts +66 -0
  21. package/dist/types/src/Material.d.ts +87 -0
  22. package/dist/types/src/MathUtils.d.ts +81 -0
  23. package/dist/types/src/ParticleEmitter.d.ts +131 -0
  24. package/dist/types/src/Physics.d.ts +337 -0
  25. package/dist/types/src/Pool.d.ts +53 -0
  26. package/dist/types/src/PostEffects.d.ts +68 -0
  27. package/dist/types/src/PostProcessor.d.ts +124 -0
  28. package/dist/types/src/RenderTarget.d.ts +56 -0
  29. package/dist/types/src/Scene.d.ts +62 -0
  30. package/dist/types/src/ScreenEffects.d.ts +111 -0
  31. package/dist/types/src/Serializer.d.ts +86 -0
  32. package/dist/types/src/Shaders.d.ts +2 -0
  33. package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
  34. package/dist/types/src/SpatialGrid.d.ts +50 -0
  35. package/dist/types/src/SpriteBatch.d.ts +89 -0
  36. package/dist/types/src/StateMachine.d.ts +59 -0
  37. package/dist/types/src/Storage.d.ts +89 -0
  38. package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
  39. package/dist/types/src/TextureAtlas.d.ts +55 -0
  40. package/dist/types/src/Tilemap.d.ts +91 -0
  41. package/dist/types/src/Time.d.ts +53 -0
  42. package/dist/types/src/Timer.d.ts +54 -0
  43. package/dist/types/src/Transform.d.ts +94 -0
  44. package/dist/types/src/Tween.d.ts +81 -0
  45. package/dist/types/src/UI.d.ts +121 -0
  46. package/dist/types/src/components/Behaviour.d.ts +71 -0
  47. package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
  48. package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
  49. package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
  50. package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
  51. package/dist/types/src/components/Collider.d.ts +102 -0
  52. package/dist/types/src/components/GameObject.d.ts +135 -0
  53. package/dist/types/src/components/PolygonCollider.d.ts +33 -0
  54. package/dist/types/src/components/RigidBody.d.ts +456 -0
  55. package/dist/types/src/importers/Aseprite.d.ts +79 -0
  56. package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
  57. package/dist/types/src/importers/TiledMap.d.ts +62 -0
  58. package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
  59. package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
  60. package/dist/types/src/managers/AssetManager.d.ts +116 -0
  61. package/dist/types/src/managers/AudioManager.d.ts +259 -0
  62. package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
  63. package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
  64. package/dist/types/src/managers/GLManager.d.ts +84 -0
  65. package/dist/types/src/managers/GLState.d.ts +37 -0
  66. package/dist/types/src/managers/Gamepad.d.ts +102 -0
  67. package/dist/types/src/managers/IDManager.d.ts +31 -0
  68. package/dist/types/src/managers/InputManager.d.ts +381 -0
  69. package/dist/types/src/managers/NetworkManager.d.ts +93 -0
  70. package/dist/types/src/managers/RenderStats.d.ts +34 -0
  71. package/dist/types/src/managers/SceneManager.d.ts +44 -0
  72. package/dist/types/src/managers/ShaderManager.d.ts +55 -0
  73. package/dist/types/src/managers/TextureManager.d.ts +102 -0
  74. package/dist/types/src/particlesystem/Particle.d.ts +64 -0
  75. package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
  76. package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
  77. package/dist/types/src/physics/AABB.d.ts +92 -0
  78. package/dist/types/src/physics/Body.d.ts +435 -0
  79. package/dist/types/src/physics/BodyType.d.ts +6 -0
  80. package/dist/types/src/physics/BroadPhase.d.ts +210 -0
  81. package/dist/types/src/physics/Collision.d.ts +102 -0
  82. package/dist/types/src/physics/Contact.d.ts +206 -0
  83. package/dist/types/src/physics/ContactSolver.d.ts +108 -0
  84. package/dist/types/src/physics/Distance.d.ts +54 -0
  85. package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
  86. package/dist/types/src/physics/Fixture.d.ts +221 -0
  87. package/dist/types/src/physics/Island.d.ts +52 -0
  88. package/dist/types/src/physics/Joint.d.ts +59 -0
  89. package/dist/types/src/physics/Math2D.d.ts +371 -0
  90. package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
  91. package/dist/types/src/physics/Settings.d.ts +22 -0
  92. package/dist/types/src/physics/Shapes.d.ts +207 -0
  93. package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
  94. package/dist/types/src/physics/World.d.ts +274 -0
  95. package/dist/types/src/physics/index.d.ts +34 -0
  96. package/index.js +78 -0
  97. package/package.json +73 -60
  98. package/src/Animator.js +95 -0
  99. package/src/BitmapText.js +6 -5
  100. package/src/Camera.js +183 -0
  101. package/src/CameraController.js +192 -0
  102. package/src/CanvasText.js +281 -0
  103. package/src/CollisionLayers.js +86 -0
  104. package/src/Color.js +26 -18
  105. package/src/Coroutine.js +259 -0
  106. package/src/DebugOverlay.js +246 -0
  107. package/src/Drawable.js +842 -582
  108. package/src/Easing.js +57 -0
  109. package/src/Emerald.js +1150 -459
  110. package/src/EmeraldDB.js +328 -0
  111. package/src/FPSCounter.js +43 -43
  112. package/src/GLUtils.js +60 -67
  113. package/src/Instance.js +41 -5
  114. package/src/InstancedTexture.js +308 -127
  115. package/src/Interpolator.js +124 -0
  116. package/src/Material.js +202 -0
  117. package/src/MathUtils.js +134 -0
  118. package/src/ParticleEmitter.js +284 -0
  119. package/src/Physics.js +431 -40
  120. package/src/Pool.js +85 -0
  121. package/src/PostEffects.js +296 -0
  122. package/src/PostProcessor.js +304 -0
  123. package/src/RenderTarget.js +134 -0
  124. package/src/Scene.js +115 -83
  125. package/src/ScreenEffects.js +266 -0
  126. package/src/Serializer.js +131 -0
  127. package/src/Shaders.js +150 -165
  128. package/src/Shapes.js +118 -129
  129. package/src/SpatialGrid.js +111 -0
  130. package/src/SpriteBatch.js +299 -0
  131. package/src/StateMachine.js +82 -0
  132. package/src/Storage.js +175 -47
  133. package/src/Texture.js +58 -67
  134. package/src/TextureAtlas.js +96 -0
  135. package/src/Tilemap.js +274 -0
  136. package/src/Time.js +51 -6
  137. package/src/Timer.js +99 -0
  138. package/src/Transform.js +100 -7
  139. package/src/Tween.js +160 -0
  140. package/src/UI.js +394 -0
  141. package/src/components/Behaviour.js +90 -0
  142. package/src/components/BoxCollider.js +29 -12
  143. package/src/components/BoxColliderDebug.js +3 -4
  144. package/src/components/CircleCollider.js +26 -12
  145. package/src/components/CircleColliderDebug.js +25 -24
  146. package/src/components/Collider.js +150 -34
  147. package/src/components/GameObject.js +130 -21
  148. package/src/components/PolygonCollider.js +55 -0
  149. package/src/components/RigidBody.js +556 -8
  150. package/src/importers/Aseprite.js +142 -0
  151. package/src/importers/ForgeLevel.js +581 -0
  152. package/src/importers/TiledMap.js +158 -0
  153. package/src/lights/DirectionalLight.js +6 -15
  154. package/src/lights/PointLight.js +4 -4
  155. package/src/managers/AssetManager.js +239 -0
  156. package/src/managers/AudioManager.js +565 -146
  157. package/src/managers/EventManager.js +488 -477
  158. package/src/managers/GLManager.js +57 -0
  159. package/src/managers/GLState.js +70 -0
  160. package/src/managers/Gamepad.js +126 -0
  161. package/src/managers/IDManager.js +24 -2
  162. package/src/managers/InputManager.js +905 -0
  163. package/src/managers/NetworkManager.js +178 -0
  164. package/src/managers/RenderStats.js +34 -0
  165. package/src/managers/SceneManager.js +30 -0
  166. package/src/managers/ShaderManager.js +0 -2
  167. package/src/managers/TextureManager.js +218 -0
  168. package/src/particlesystem/Particle.js +82 -7
  169. package/src/particlesystem/ParticleSettings.js +21 -3
  170. package/src/particlesystem/Particles.js +80 -31
  171. package/src/physics/AABB.js +207 -0
  172. package/src/physics/Body.js +862 -0
  173. package/src/physics/BodyType.js +16 -0
  174. package/src/physics/BroadPhase.js +641 -0
  175. package/src/physics/Collision.js +534 -0
  176. package/src/physics/Contact.js +500 -0
  177. package/src/physics/ContactSolver.js +526 -0
  178. package/src/physics/Distance.js +403 -0
  179. package/src/physics/DistanceJoint.js +227 -0
  180. package/src/physics/Fixture.js +346 -0
  181. package/src/physics/Island.js +203 -0
  182. package/src/physics/Joint.js +78 -0
  183. package/src/physics/Math2D.js +573 -0
  184. package/src/physics/RevoluteJoint.js +278 -0
  185. package/src/physics/Settings.js +78 -0
  186. package/src/physics/Shapes.js +549 -0
  187. package/src/physics/TimeOfImpact.js +87 -0
  188. package/src/physics/World.js +731 -0
  189. package/src/physics/index.js +79 -0
  190. package/dist/types/Drawable.d.ts +0 -157
  191. package/dist/types/Emerald.d.ts +0 -73
  192. package/dist/types/GLUtils.d.ts +0 -6
  193. package/dist/types/Physics.d.ts +0 -139
  194. package/dist/types/Scene.d.ts +0 -39
  195. package/dist/types/Shaders.d.ts +0 -4
  196. package/dist/types/Storage.d.ts +0 -46
  197. package/dist/types/Time.d.ts +0 -22
  198. package/dist/types/Transform.d.ts +0 -41
  199. package/dist/types/components/BoxColliderDebug.d.ts +0 -19
  200. package/dist/types/components/CircleColliderDebug.d.ts +0 -19
  201. package/dist/types/components/Collider.d.ts +0 -53
  202. package/dist/types/components/GameObject.d.ts +0 -72
  203. package/dist/types/components/RigidBody.d.ts +0 -104
  204. package/dist/types/managers/AudioManager.d.ts +0 -60
  205. package/dist/types/managers/GLManager.d.ts +0 -47
  206. package/dist/types/managers/IDManager.d.ts +0 -21
  207. package/dist/types/managers/SceneManager.d.ts +0 -22
  208. 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;