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,178 @@
1
+ import Interpolator from "../Interpolator.js";
2
+
3
+ /**
4
+ * @class NetworkManager
5
+ * @description A thin, optional multiplayer layer over Colyseus. `colyseus.js`
6
+ * is an optional peer dependency and is imported dynamically, so the engine has
7
+ * no hard dependency on it, so games that don't use networking never load it.
8
+ *
9
+ * It wraps connection/room lifecycle, exposes a small event API
10
+ * (onStateChange / onMessage / onAdd / onRemove / onLeave), and bundles an
11
+ * {@link Interpolator} so remote entities can be rendered smoothly.
12
+ *
13
+ * @example
14
+ * const net = new NetworkManager();
15
+ * await net.connect("wss://my-server");
16
+ * const room = await net.join("arena", { name: "P1" });
17
+ * net.onMessage("hit", (msg) => applyHit(msg));
18
+ * net.onStateChange((state) => {
19
+ * for (const [id, p] of state.players) net.interpolator.push(id, p, net.now());
20
+ * });
21
+ * // each frame:
22
+ * const pos = net.interpolator.sample(remoteId, net.now());
23
+ */
24
+ class NetworkManager {
25
+ constructor(options = {}) {
26
+ this.client = null;
27
+ this.room = null;
28
+ this.interpolator = new Interpolator(options.interpolation || {});
29
+ /** @private */
30
+ this._messageHandlers = new Map();
31
+ /** @private */
32
+ this._stateHandlers = [];
33
+ /** @private */
34
+ this._leaveHandlers = [];
35
+ /** @private */
36
+ this._startTime = NetworkManager._now();
37
+ }
38
+
39
+ /**
40
+ * @method connect
41
+ * @description Dynamically imports colyseus.js and creates a Client.
42
+ * @param {string} endpoint - The server endpoint (e.g. "wss://host:2567")
43
+ * @returns {Promise<NetworkManager>} - this
44
+ */
45
+ async connect(endpoint) {
46
+ let colyseus;
47
+ try {
48
+ colyseus = await import("colyseus.js");
49
+ } catch (err) {
50
+ throw new Error(
51
+ "[NetworkManager] colyseus.js is not installed. Run `npm install colyseus.js` to use networking."
52
+ );
53
+ }
54
+ const Client =
55
+ colyseus.Client || (colyseus.default && colyseus.default.Client);
56
+ this.client = new Client(endpoint);
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * @method join
62
+ * @description Joins (or creates) a room by name and wires up the event hooks.
63
+ * @param {string} roomName - The room name
64
+ * @param {Object} [options] - Join options forwarded to the server
65
+ * @returns {Promise<Object>} - The joined room
66
+ */
67
+ async join(roomName, options = {}) {
68
+ if (!this.client) {
69
+ throw new Error("[NetworkManager] Call connect() before join().");
70
+ }
71
+ this.room = await this.client.joinOrCreate(roomName, options);
72
+ this._wireRoom(this.room);
73
+ return this.room;
74
+ }
75
+
76
+ /** @private */
77
+ _wireRoom(room) {
78
+ if (typeof room.onStateChange === "function") {
79
+ room.onStateChange((state) => {
80
+ for (const handler of this._stateHandlers) handler(state);
81
+ });
82
+ }
83
+ if (typeof room.onLeave === "function") {
84
+ room.onLeave((code) => {
85
+ for (const handler of this._leaveHandlers) handler(code);
86
+ });
87
+ }
88
+ if (typeof room.onMessage === "function") {
89
+ for (const [type, handlers] of this._messageHandlers) {
90
+ room.onMessage(type, (payload) => handlers.forEach((h) => h(payload)));
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * @method onStateChange
97
+ * @description Registers a handler for full room-state updates.
98
+ */
99
+ onStateChange(handler) {
100
+ this._stateHandlers.push(handler);
101
+ return this;
102
+ }
103
+
104
+ /**
105
+ * @method onMessage
106
+ * @description Registers a handler for a server message type.
107
+ */
108
+ onMessage(type, handler) {
109
+ if (!this._messageHandlers.has(type)) {
110
+ this._messageHandlers.set(type, []);
111
+ if (this.room && typeof this.room.onMessage === "function") {
112
+ this.room.onMessage(type, (payload) =>
113
+ this._messageHandlers.get(type).forEach((h) => h(payload))
114
+ );
115
+ }
116
+ }
117
+ this._messageHandlers.get(type).push(handler);
118
+ return this;
119
+ }
120
+
121
+ /**
122
+ * @method onLeave
123
+ * @description Registers a handler invoked when the room is left.
124
+ */
125
+ onLeave(handler) {
126
+ this._leaveHandlers.push(handler);
127
+ return this;
128
+ }
129
+
130
+ /**
131
+ * @method send
132
+ * @description Sends a typed message to the server.
133
+ */
134
+ send(type, payload) {
135
+ if (this.room && typeof this.room.send === "function") {
136
+ this.room.send(type, payload);
137
+ }
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * @method now
143
+ * @description Seconds since this manager was created, a convenient clock for
144
+ * feeding the interpolator.
145
+ * @returns {number}
146
+ */
147
+ now() {
148
+ return (NetworkManager._now() - this._startTime) / 1000;
149
+ }
150
+
151
+ /**
152
+ * @method leave
153
+ * @description Leaves the current room.
154
+ */
155
+ async leave() {
156
+ if (this.room && typeof this.room.leave === "function") {
157
+ await this.room.leave();
158
+ }
159
+ this.room = null;
160
+ }
161
+
162
+ /**
163
+ * @method sessionId
164
+ * @returns {string|null} - This client's session id, if joined.
165
+ */
166
+ get sessionId() {
167
+ return this.room ? this.room.sessionId : null;
168
+ }
169
+
170
+ /** @private */
171
+ static _now() {
172
+ return typeof performance !== "undefined" && performance.now
173
+ ? performance.now()
174
+ : Date.now();
175
+ }
176
+ }
177
+
178
+ export default NetworkManager;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @class RenderStats
3
+ * @description Frame-level render counters, incremented by every draw site in
4
+ * the engine (Drawable, InstancedTexture, SpriteBatch, post-processing) and
5
+ * reset at the start of each drawScene. Read the previous completed frame via
6
+ * `RenderStats.frame` or `emerald.getRenderStats()`. DebugOverlay shows it
7
+ * automatically.
8
+ *
9
+ * - drawCalls: GPU draw commands issued (the batching win shows up here)
10
+ * - quads: sprites/shapes drawn, counting every instance in a batch
11
+ * - textureBinds: texture switches (high numbers = poor batching order)
12
+ */
13
+ class RenderStats {
14
+ /**
15
+ * @method beginFrame
16
+ * @description Snapshots the counters gathered since the previous call into
17
+ * `frame` and zeroes the accumulators. Called by Emerald.drawScene.
18
+ */
19
+ static beginFrame() {
20
+ RenderStats.frame.drawCalls = RenderStats.drawCalls;
21
+ RenderStats.frame.quads = RenderStats.quads;
22
+ RenderStats.frame.textureBinds = RenderStats.textureBinds;
23
+ RenderStats.drawCalls = 0;
24
+ RenderStats.quads = 0;
25
+ RenderStats.textureBinds = 0;
26
+ }
27
+ }
28
+
29
+ RenderStats.drawCalls = 0;
30
+ RenderStats.quads = 0;
31
+ RenderStats.textureBinds = 0;
32
+ RenderStats.frame = { drawCalls: 0, quads: 0, textureBinds: 0 };
33
+
34
+ export default RenderStats;
@@ -20,6 +20,36 @@ class SceneManager {
20
20
  static getScene() {
21
21
  return SceneManager.scene;
22
22
  }
23
+
24
+ /**
25
+ * @method transitionTo
26
+ * @description Switches the active scene, optionally behind a ScreenEffects
27
+ * fade. With `screenEffects` provided the screen fades out, the scene is
28
+ * swapped (and your optional `onSwap` runs) while covered, then fades back in.
29
+ * Without it the swap happens immediately.
30
+ *
31
+ * @param {Scene} scene - The scene to make active
32
+ * @param {Object} [options]
33
+ * @param {ScreenEffects} [options.screenEffects] - Effects layer to fade with
34
+ * @param {Function} [options.onSwap] - `(scene) => void|Promise`, run during
35
+ * the covered swap (e.g. (re)build the new scene's contents)
36
+ * @param {number} [options.duration] - Fade duration (seconds)
37
+ * @param {Color} [options.color] - Fade color
38
+ * @returns {Promise<Scene>} - Resolves with the new scene
39
+ */
40
+ static async transitionTo(scene, options = {}) {
41
+ const { screenEffects, onSwap, ...fadeOptions } = options;
42
+ const swap = async () => {
43
+ SceneManager.setScene(scene);
44
+ if (typeof onSwap === "function") await onSwap(scene);
45
+ };
46
+ if (screenEffects && typeof screenEffects.transition === "function") {
47
+ await screenEffects.transition(swap, fadeOptions);
48
+ } else {
49
+ await swap();
50
+ }
51
+ return scene;
52
+ }
23
53
  }
24
54
 
25
55
  SceneManager.scene = null;
@@ -109,7 +109,6 @@ export class ShaderManager {
109
109
  return {
110
110
  vertexPosition: this.gl.getAttribLocation(program, "aVertexPosition"),
111
111
  textureCoord: this.gl.getAttribLocation(program, "aTextureCoord"),
112
- // Add other attributes as needed
113
112
  };
114
113
  }
115
114
 
@@ -127,7 +126,6 @@ export class ShaderManager {
127
126
  ),
128
127
  modelViewMatrix: this.gl.getUniformLocation(program, "uModelViewMatrix"),
129
128
  sampler: this.gl.getUniformLocation(program, "uSampler"),
130
- // Add other uniforms as needed
131
129
  };
132
130
  }
133
131
  }
@@ -0,0 +1,218 @@
1
+ import GLManager from "./GLManager.js";
2
+
3
+ /**
4
+ * @class TextureManager
5
+ * @description Caches loaded images and GL textures so that many drawables
6
+ * sharing the same texture path download and upload the image only once.
7
+ *
8
+ * Textures are immutable after upload in this engine, so the same GL texture
9
+ * object can safely be bound by any number of objects. The GL-texture cache is
10
+ * keyed by path *and* filter mode, because pixel-art (NEAREST) and smooth
11
+ * (LINEAR) sampling require separate texture objects.
12
+ */
13
+ class TextureManager {
14
+ /**
15
+ * @method loadImage
16
+ * @description Loads (and caches) an HTMLImageElement for a given path.
17
+ * Concurrent requests for the same path share a single in-flight load.
18
+ * @param {string} path - The path (or data URL) of the image
19
+ * @returns {Promise<HTMLImageElement>} - The loaded image
20
+ */
21
+ static loadImage(path) {
22
+ if (TextureManager.images.has(path)) {
23
+ return TextureManager.images.get(path);
24
+ }
25
+
26
+ const promise = new Promise((resolve, reject) => {
27
+ const image = new Image();
28
+ image.crossOrigin = "Anonymous";
29
+ image.addEventListener("load", () => resolve(image));
30
+ image.addEventListener("error", () => {
31
+ TextureManager.images.delete(path);
32
+ reject(
33
+ new Error(
34
+ `[TextureManager.js] > Failed to load image: ${path}. Check that the file exists and the path is correct.`
35
+ )
36
+ );
37
+ });
38
+ image.src = path;
39
+ });
40
+
41
+ TextureManager.images.set(path, promise);
42
+ return promise;
43
+ }
44
+
45
+ /**
46
+ * @method getTexture
47
+ * @description Returns a cached GL texture for the given path, creating and
48
+ * uploading it on first request. Subsequent calls reuse the same texture.
49
+ * @param {string} path - The path (or data URL) of the texture
50
+ * @param {boolean} pixelart - Whether to sample with NEAREST (pixel art) filtering
51
+ * @returns {Promise<{texture: WebGLTexture, width: number, height: number}>}
52
+ */
53
+ static getTexture(path, pixelart = false) {
54
+ const key = `${path}|${pixelart ? "nearest" : "linear"}`;
55
+ if (TextureManager.textures.has(key)) {
56
+ return TextureManager.textures.get(key);
57
+ }
58
+
59
+ const promise = TextureManager._upload(path, pixelart);
60
+ TextureManager.textures.set(key, promise);
61
+ return promise;
62
+ }
63
+
64
+ /**
65
+ * @method _upload
66
+ * @description Loads the image (via the shared image cache) and uploads it
67
+ * as a GL texture with the requested filtering.
68
+ * @private
69
+ */
70
+ static _upload(path, pixelart) {
71
+ return TextureManager.loadImage(path).then((image) => {
72
+ const gl = GLManager.getGL();
73
+ if (!gl) {
74
+ throw new Error(
75
+ "[TextureManager] > No GL context set. Create an Emerald instance before loading textures."
76
+ );
77
+ }
78
+
79
+ const texture = gl.createTexture();
80
+ gl.bindTexture(gl.TEXTURE_2D, texture);
81
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
82
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
83
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
84
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
85
+ const filter = pixelart ? gl.NEAREST : gl.LINEAR;
86
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
87
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
88
+ gl.texImage2D(
89
+ gl.TEXTURE_2D,
90
+ 0,
91
+ gl.RGBA,
92
+ gl.RGBA,
93
+ gl.UNSIGNED_BYTE,
94
+ image
95
+ );
96
+
97
+ return { texture, width: image.width, height: image.height };
98
+ });
99
+ }
100
+
101
+ /**
102
+ * @method restoreAll
103
+ * @description Re-uploads every cached texture after a WebGL context loss.
104
+ * The image cache survives the loss, so this is upload-only (no network).
105
+ * Reference counts are untouched: they track logical ownership by
106
+ * drawables, which still exist.
107
+ */
108
+ static restoreAll() {
109
+ for (const key of Array.from(TextureManager.textures.keys())) {
110
+ const sep = key.lastIndexOf("|");
111
+ const path = key.slice(0, sep);
112
+ const pixelart = key.slice(sep + 1) === "nearest";
113
+ TextureManager.textures.set(key, TextureManager._upload(path, pixelart));
114
+ }
115
+ }
116
+
117
+ /**
118
+ * @method preload
119
+ * @description Eagerly loads a list of texture paths so they are ready (and
120
+ * cached) before the first frame that needs them.
121
+ * @param {string[]} paths - The texture paths to preload
122
+ * @param {boolean} pixelart - Whether to use pixel-art filtering
123
+ * @returns {Promise<void>}
124
+ */
125
+ static async preload(paths, pixelart = false) {
126
+ await Promise.all(
127
+ paths.map((path) => TextureManager.getTexture(path, pixelart))
128
+ );
129
+ }
130
+
131
+ /**
132
+ * @method has
133
+ * @description Checks whether a GL texture is already cached for a path
134
+ * @param {string} path - The texture path
135
+ * @param {boolean} pixelart - Whether the pixel-art variant is meant
136
+ * @returns {boolean}
137
+ */
138
+ static has(path, pixelart = false) {
139
+ return TextureManager.textures.has(
140
+ `${path}|${pixelart ? "nearest" : "linear"}`
141
+ );
142
+ }
143
+
144
+ /**
145
+ * @method retain
146
+ * @description Increments the reference count for a cached texture. Every
147
+ * Drawable that resolves a texture through getTexture() retains it; when the
148
+ * last user releases it, the GL texture is freed. Call retain/release in
149
+ * pairs (Drawable.dispose does the release for you).
150
+ * @param {string} path - The texture path
151
+ * @param {boolean} pixelart - Whether the pixel-art variant is meant
152
+ */
153
+ static retain(path, pixelart = false) {
154
+ const key = `${path}|${pixelart ? "nearest" : "linear"}`;
155
+ TextureManager.refs.set(key, (TextureManager.refs.get(key) || 0) + 1);
156
+ }
157
+
158
+ /**
159
+ * @method release
160
+ * @description Decrements a texture's reference count; when it reaches zero
161
+ * the GL texture is deleted and dropped from the cache (the CPU-side image
162
+ * cache is kept so a later reload is cheap).
163
+ * @param {string} path - The texture path
164
+ * @param {boolean} pixelart - Whether the pixel-art variant is meant
165
+ */
166
+ static release(path, pixelart = false) {
167
+ const key = `${path}|${pixelart ? "nearest" : "linear"}`;
168
+ const count = TextureManager.refs.get(key);
169
+ if (count == null) return;
170
+ if (count > 1) {
171
+ TextureManager.refs.set(key, count - 1);
172
+ return;
173
+ }
174
+ TextureManager.refs.delete(key);
175
+ const promise = TextureManager.textures.get(key);
176
+ TextureManager.textures.delete(key);
177
+ const gl = GLManager.getGL();
178
+ if (promise && gl) {
179
+ promise.then(({ texture }) => gl.deleteTexture(texture)).catch(() => {});
180
+ }
181
+ }
182
+
183
+ /**
184
+ * @method refCount
185
+ * @description Current reference count for a texture (0 if uncached).
186
+ * @returns {number}
187
+ */
188
+ static refCount(path, pixelart = false) {
189
+ return (
190
+ TextureManager.refs.get(`${path}|${pixelart ? "nearest" : "linear"}`) || 0
191
+ );
192
+ }
193
+
194
+ /**
195
+ * @method clear
196
+ * @description Deletes all cached GL textures and clears the image cache.
197
+ * Useful when tearing down a context or freeing memory between levels.
198
+ */
199
+ static clear() {
200
+ const gl = GLManager.getGL();
201
+ if (gl) {
202
+ TextureManager.textures.forEach((promise) => {
203
+ promise
204
+ .then(({ texture }) => gl.deleteTexture(texture))
205
+ .catch(() => {});
206
+ });
207
+ }
208
+ TextureManager.textures.clear();
209
+ TextureManager.images.clear();
210
+ TextureManager.refs.clear();
211
+ }
212
+ }
213
+
214
+ TextureManager.images = new Map();
215
+ TextureManager.textures = new Map();
216
+ TextureManager.refs = new Map();
217
+
218
+ export default TextureManager;
@@ -10,12 +10,24 @@ import Instance from "../Instance.js";
10
10
  * @param {ParticleSettings} settings - The settings for the particle
11
11
  */
12
12
  class Particle {
13
- constructor(instancedTexture, position, rotation, scale, settings) {
13
+ constructor(instancedTexture, position, rotation, scale, settings, velocity) {
14
+ this.reset(instancedTexture, position, rotation, scale, settings, velocity);
15
+ }
16
+
17
+ /**
18
+ * @method reset
19
+ * @description (Re)initializes the particle. Used by the pool so particle
20
+ * objects can be reused without allocating. `velocity` overrides
21
+ * settings.velocity when provided (avoids a ParticleSettings allocation per
22
+ * particle).
23
+ */
24
+ reset(instancedTexture, position, rotation, scale, settings, velocity) {
14
25
  this.settings = settings;
15
26
  this.lifetime = settings.lifetime;
16
27
  this.age = 0;
17
- this.velocity = { ...settings.velocity };
18
- this.gravity = { ...settings.gravity };
28
+ const v = velocity || settings.velocity;
29
+ this.velocity = { x: v.x, y: v.y };
30
+ this.gravity = { x: settings.gravity.x, y: settings.gravity.y };
19
31
  this.instance = new Instance(
20
32
  `Particle at ${position.x}, ${position.y}`,
21
33
  position,
@@ -23,16 +35,68 @@ class Particle {
23
35
  rotation,
24
36
  settings.frame
25
37
  );
38
+ /** @private */
39
+ this._baseScaleX = scale ? scale.x : 1;
40
+ /** @private */
41
+ this._baseScaleY = scale ? scale.y : 1;
42
+
43
+ /** @private */
44
+ this._colFrom = null;
45
+ /** @private */
46
+ this._colTo = null;
47
+ if (settings.colorOverLife) {
48
+ this._colFrom = Particle._normColor(settings.colorOverLife.from);
49
+ this._colTo = Particle._normColor(settings.colorOverLife.to);
50
+ }
51
+
26
52
  this.instancedTexture = instancedTexture;
27
- if(this.settings.animation) {
53
+ if (this.settings.animation) {
28
54
  this.instance.playAnimation(
29
55
  this.settings.animation.frames,
30
56
  this.settings.animation.speed
31
57
  );
32
58
  }
59
+ this._applyOverLife(0);
33
60
  instancedTexture.addInstance(this.instance);
34
61
  }
35
62
 
63
+ /** @private */
64
+ static _normColor(c) {
65
+ if (!c) return [1, 1, 1];
66
+ return [(c.r ?? 255) / 255, (c.g ?? 255) / 255, (c.b ?? 255) / 255];
67
+ }
68
+
69
+ /** @private */
70
+ _applyOverLife(t) {
71
+ const s = this.settings;
72
+ const tr = this.instance.transform;
73
+
74
+ if (s.scaleOverLife) {
75
+ const m =
76
+ s.scaleOverLife.from + (s.scaleOverLife.to - s.scaleOverLife.from) * t;
77
+ tr.scale.x = this._baseScaleX * m;
78
+ tr.scale.y = this._baseScaleY * m;
79
+ }
80
+
81
+ if (this._colFrom || s.alphaOverLife) {
82
+ let r = 1;
83
+ let g = 1;
84
+ let b = 1;
85
+ let a = 1;
86
+ if (this._colFrom) {
87
+ r = this._colFrom[0] + (this._colTo[0] - this._colFrom[0]) * t;
88
+ g = this._colFrom[1] + (this._colTo[1] - this._colFrom[1]) * t;
89
+ b = this._colFrom[2] + (this._colTo[2] - this._colFrom[2]) * t;
90
+ }
91
+ if (s.alphaOverLife) {
92
+ a =
93
+ s.alphaOverLife.from +
94
+ (s.alphaOverLife.to - s.alphaOverLife.from) * t;
95
+ }
96
+ this.instance.setColor(r, g, b, a);
97
+ }
98
+ }
99
+
36
100
  /**
37
101
  * @method update
38
102
  * @description Updates the particle
@@ -40,12 +104,23 @@ class Particle {
40
104
  */
41
105
  update(dt) {
42
106
  this.age += dt;
43
- // Apply gravity
107
+ const s = this.settings;
108
+
44
109
  this.velocity.x += this.gravity.x * dt;
45
110
  this.velocity.y += this.gravity.y * dt;
46
- // Update position
111
+ if (s.drag) {
112
+ const damp = Math.max(0, 1 - s.drag * dt);
113
+ this.velocity.x *= damp;
114
+ this.velocity.y *= damp;
115
+ }
47
116
  this.instance.transform.position.x += this.velocity.x * dt;
48
117
  this.instance.transform.position.y += this.velocity.y * dt;
118
+ if (s.rotationSpeed) {
119
+ this.instance.transform.rotation += s.rotationSpeed * dt;
120
+ }
121
+
122
+ const t = this.lifetime > 0 ? Math.min(1, this.age / this.lifetime) : 1;
123
+ this._applyOverLife(t);
49
124
  }
50
125
 
51
126
  /**
@@ -64,7 +139,7 @@ class Particle {
64
139
  destroy() {
65
140
  if (this.instancedTexture && this.instance) {
66
141
  if (this.instancedTexture.instances.length > 0) {
67
- if(this.instancedTexture.getInstanceWithId(this.instance.id)) {
142
+ if (this.instancedTexture.getInstanceWithId(this.instance.id)) {
68
143
  this.instancedTexture.removeInstance(this.instance.id);
69
144
  }
70
145
  }
@@ -24,12 +24,20 @@ class ParticleSettings {
24
24
  amount = 10,
25
25
  direction = new Vector2(1, 0),
26
26
  spread = Math.PI / 4,
27
- emissionRate = Infinity, // particles per second
27
+ emissionRate = Infinity,
28
28
  frame,
29
29
  offset,
30
30
  rotation,
31
31
  scale,
32
- animation
32
+ animation,
33
+ shape = "cone",
34
+ shapeRadius = 0,
35
+ shapeSize = new Vector2(0, 0),
36
+ scaleOverLife = null,
37
+ alphaOverLife = null,
38
+ colorOverLife = null,
39
+ rotationSpeed = 0,
40
+ drag = 0,
33
41
  } = {}) {
34
42
  this.lifetime = lifetime;
35
43
  this.velocity = velocity;
@@ -43,7 +51,17 @@ class ParticleSettings {
43
51
  this.rotation = rotation;
44
52
  this.scale = scale;
45
53
  this.animation = animation;
54
+
55
+ this.shape = shape;
56
+ this.shapeRadius = shapeRadius;
57
+ this.shapeSize = shapeSize;
58
+
59
+ this.scaleOverLife = scaleOverLife;
60
+ this.alphaOverLife = alphaOverLife;
61
+ this.colorOverLife = colorOverLife;
62
+ this.rotationSpeed = rotationSpeed;
63
+ this.drag = drag;
46
64
  }
47
65
  }
48
66
 
49
- export default ParticleSettings;
67
+ export default ParticleSettings;