littlejsengine 1.18.15 → 1.18.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlejsengine",
3
- "version": "1.18.15",
3
+ "version": "1.18.18",
4
4
  "description": "LittleJS - Tiny and Fast HTML5 Game Engine",
5
5
  "main": "dist/littlejs.esm.js",
6
6
  "types": "dist/littlejs.d.ts",
package/plugins/box2d.js CHANGED
@@ -207,23 +207,19 @@ class Box2dObject extends EngineObject
207
207
 
208
208
  function box2dCreatePolygonShape(points)
209
209
  {
210
- function box2dCreatePointList(points)
210
+ ASSERT(3 <= points.length && points.length <= 8);
211
+ const buffer = box2d.instance._malloc(points.length * 8);
212
+ for (let i=0, offset=0; i<points.length; ++i)
211
213
  {
212
- const buffer = box2d.instance._malloc(points.length * 8);
213
- for (let i=0, offset=0; i<points.length; ++i)
214
- {
215
- box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x;
216
- offset += 4;
217
- box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
218
- offset += 4;
219
- }
220
- return box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
214
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].x;
215
+ offset += 4;
216
+ box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
217
+ offset += 4;
221
218
  }
222
-
223
- ASSERT(3 <= points.length && points.length <= 8);
219
+ const box2dPoints = box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
224
220
  const shape = new box2d.instance.b2PolygonShape();
225
- const box2dPoints = box2dCreatePointList(points);
226
221
  shape.Set(box2dPoints, points.length);
222
+ box2d.instance._free(buffer);
227
223
  return shape;
228
224
  }
229
225
 
@@ -13,8 +13,8 @@
13
13
  * This function can not apply color because it draws using the 2d context
14
14
  * @param {Vector2} pos - Screen space position
15
15
  * @param {Vector2} size - Screen space size
16
- * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
17
- * @param {number} [borderSize] - Width of the border sections
16
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample (see drawNineSlice)
17
+ * @param {number} [borderSize] - Rendered thickness of the border sections
18
18
  * @param {number} [extraSpace] - Extra spacing adjustment
19
19
  * @param {number} [angle] - Angle to rotate by
20
20
  * @memberof DrawUtilities */
@@ -25,11 +25,16 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
25
25
 
26
26
  /** Draw a scalable nine-slice UI element in world space
27
27
  * This function can apply color and additive color if WebGL is enabled
28
+ * The nine-slice samples a 3x3 block of tiles from the tilesheet, it does not
29
+ * subdivide a single tile. Pass the top-left tile of that block as startTile;
30
+ * the other 8 tiles (edges, corners, and center) are taken automatically from
31
+ * the 3x3 grid of tiles extending right and down from it. borderSize only sets
32
+ * the rendered thickness of the edges and corners, not how the texture is cut.
28
33
  * @param {Vector2} pos - World space position
29
34
  * @param {Vector2} size - World space size
30
- * @param {TileInfo} startTile - Starting tile for the nine-slice pattern
35
+ * @param {TileInfo} startTile - Top-left tile of the 3x3 block to sample the nine-slice from
31
36
  * @param {Color} [color] - Color to modulate with
32
- * @param {number} [borderSize] - Width of the border sections
37
+ * @param {number} [borderSize] - Rendered thickness of the border sections
33
38
  * @param {Color} [additiveColor] - Additive color
34
39
  * @param {number} [extraSpace] - Extra spacing adjustment
35
40
  * @param {number} [angle] - Angle to rotate by
@@ -39,7 +44,8 @@ function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
39
44
  * @memberof DrawUtilities */
40
45
  function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
41
46
  {
42
- // setup nine slice tiles
47
+ // setup nine slice tiles - startTile is the top-left of a 3x3 tile block,
48
+ // so the center tile is one tile down and right from it
43
49
  const centerTile = startTile.offset(startTile.size);
44
50
  const centerSize = size.add(vec2(extraSpace-borderSize*2));
45
51
  const cornerSize = vec2(borderSize);
@@ -73,8 +79,8 @@ function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor,
73
79
  * This function can not apply color because it draws using the 2d context
74
80
  * @param {Vector2} pos - Screen space position
75
81
  * @param {Vector2} size - Screen space size
76
- * @param {TileInfo} startTile - Starting tile for the three-slice pattern
77
- * @param {number} [borderSize] - Width of the border sections
82
+ * @param {TileInfo} startTile - First of 3 consecutive tiles: corner, side, center (see drawThreeSlice)
83
+ * @param {number} [borderSize] - Rendered thickness of the border sections
78
84
  * @param {number} [extraSpace] - Extra spacing adjustment
79
85
  * @param {number} [angle] - Angle to rotate by
80
86
  * @memberof DrawUtilities */
@@ -85,11 +91,15 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
85
91
 
86
92
  /** Draw a scalable three-slice UI element in world space
87
93
  * This function can apply color and additive color if WebGL is enabled
94
+ * The three-slice samples 3 consecutive tiles from the tilesheet, it does not
95
+ * subdivide a single tile. Pass the first tile as startTile; the three tiles
96
+ * are used in order as corner, side, and center, then rotated and mirrored to
97
+ * build all four edges and corners. borderSize only sets the rendered thickness.
88
98
  * @param {Vector2} pos - World space position
89
99
  * @param {Vector2} size - World space size
90
- * @param {TileInfo} startTile - Starting tile for the three-slice pattern
100
+ * @param {TileInfo} startTile - First of 3 consecutive tiles (corner, side, center) for the three-slice
91
101
  * @param {Color} [color] - Color to modulate with
92
- * @param {number} [borderSize] - Width of the border sections
102
+ * @param {number} [borderSize] - Rendered thickness of the border sections
93
103
  * @param {Color} [additiveColor] - Additive color
94
104
  * @param {number} [extraSpace] - Extra spacing adjustment
95
105
  * @param {number} [angle] - Angle to rotate by
@@ -99,7 +109,7 @@ function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2,
99
109
  * @memberof DrawUtilities */
100
110
  function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context)
101
111
  {
102
- // setup three slice tiles
112
+ // setup three slice tiles - 3 tiles in a row starting at startTile
103
113
  const cornerTile = startTile.frame(0);
104
114
  const sideTile = startTile.frame(1);
105
115
  const centerTile = startTile.frame(2);
@@ -129,4 +139,68 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
129
139
  const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip));
130
140
  drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
131
141
  }
142
+ }
143
+
144
+ /** Draw a crescent / moon-phase shape built from a polygon
145
+ * Routes through drawPoly, so it supports WebGL, screen space, color, and outlines
146
+ * @param {Vector2} pos - Center position
147
+ * @param {number} [size] - Diameter
148
+ * @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
149
+ * @param {Color} [color] - Fill color
150
+ * @param {number} [angle] - Angle to rotate by
151
+ * @param {boolean} [invert] - Flip which side is illuminated
152
+ * @param {number} [lineWidth] - Outline width, 0 for no outline
153
+ * @param {Color} [lineColor] - Outline color
154
+ * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
155
+ * @param {boolean} [screenSpace] - Use screen space coordinates
156
+ * @param {CanvasRenderingContext2D} [context] - Canvas context to use
157
+ * @memberof DrawUtilities */
158
+ function drawCrescent(pos, size=1, percent=0, color=WHITE, angle=0, invert=false, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
159
+ {
160
+ // build local-space points and let drawPoly apply pos/angle so screen space works
161
+ const points = getCrescentPoints(vec2(), size, percent, 0, invert);
162
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
163
+ }
164
+
165
+ /** Get the list of points that make up a crescent / moon-phase shape
166
+ * Returns world-space points with pos and angle baked in, ready for drawPoly or other use
167
+ * @param {Vector2} pos - Center position
168
+ * @param {number} [size] - Diameter
169
+ * @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
170
+ * @param {number} [angle] - Angle to rotate by
171
+ * @param {boolean} [invert] - Flip which side is illuminated
172
+ * @param {number} [sides=glCircleSides] - Number of sides for a full circle (halved per arc)
173
+ * @return {Array<Vector2>} - List of points making up the crescent
174
+ * @memberof DrawUtilities */
175
+ function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=glCircleSides)
176
+ {
177
+ ASSERT(isVector2(pos), 'pos must be a vec2');
178
+ ASSERT(isNumber(size) && isNumber(percent), 'size and percent must be numbers');
179
+
180
+ // map phase to a signed terminator curve: -1 new, 0 half, 1 full
181
+ let p = mod(percent*4, 4); // quarter phase 0..4
182
+ if (p >= 2) // second half of cycle flips orientation
183
+ angle += PI;
184
+ p = p <= 2 ? p-1 : 3-p;
185
+ if (invert) // flip the illuminated side
186
+ {
187
+ p = -p;
188
+ angle += PI;
189
+ }
190
+
191
+ // build the crescent: outer semicircle, then inner half-ellipse traced back
192
+ const points = [];
193
+ const segs = max(3, sides>>1);
194
+ const radius = size/2;
195
+ for (let i=0; i<=segs; i++)
196
+ {
197
+ const t = i/segs*PI;
198
+ points.push(vec2(radius*cos(t), radius*sin(t)).rotate(angle).add(pos));
199
+ }
200
+ for (let i=segs; i>=0; i--)
201
+ {
202
+ const t = i/segs*PI;
203
+ points.push(vec2(radius*cos(t), -radius*p*sin(t)).rotate(angle).add(pos));
204
+ }
205
+ return points;
132
206
  }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * LittleJS Light System Plugin
3
+ * - Adds 2D dynamic lighting to the scene
4
+ * - Lights are first-class EngineObjects (the Light class)
5
+ * - Each Light draws a soft falloff blob of its color into a shared lightmap
6
+ * - Lights accumulate ADDITIVELY in the lightmap (red + blue = magenta)
7
+ * - The lightmap is then MULTIPLIED with the scene during composite, so unlit
8
+ * areas go to the ambient color and lit areas show the scene tinted by the
9
+ * accumulated light color
10
+ * - Draw the world at full brightness — the lightmap does the darkening
11
+ * - Any EngineObject may override renderLight() to additively contribute to the
12
+ * lightmap (e.g. emissive lava tiles, weapon flashes, glowing crystals)
13
+ * - Must be constructed BEFORE PostProcessPlugin so post-process sees lit pixels
14
+ * @namespace LightSystem
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ ///////////////////////////////////////////////////////////////////////////////
20
+
21
+ /** Global Light System plugin object
22
+ * @type {LightSystemPlugin}
23
+ * @memberof LightSystem */
24
+ let lightSystem;
25
+
26
+ ///////////////////////////////////////////////////////////////////////////////
27
+
28
+ /**
29
+ * LightSystemPlugin
30
+ * - Owns the offscreen lightmap texture, falloff/composite shaders, and the
31
+ * per-frame render pass that multiplies the lightmap onto the WebGL scene
32
+ * - The composite is MULTIPLICATIVE: unlit areas get the ambient color, lit
33
+ * areas show the scene tinted by the accumulated light color. So you should
34
+ * draw your world at full brightness — the lightmap handles the darkening.
35
+ * @memberof LightSystem
36
+ */
37
+ class LightSystemPlugin
38
+ {
39
+ /** Create the global light system plugin.
40
+ * @param {Vector2} [textureSize] - Size of the lightmap texture (defaults to mainCanvasSize)
41
+ * @param {Color} [ambientColor] - Color applied to unlit areas of the scene (defaults to BLACK = pitch dark). Set a small RGB like rgb(0.1,0.1,0.15) for a faint "moonlight" baseline so unlit areas aren't fully black.
42
+ * @example
43
+ * // simplest usage
44
+ * new LightSystemPlugin();
45
+ */
46
+ constructor(textureSize, ambientColor)
47
+ {
48
+ ASSERT(!lightSystem, 'LightSystemPlugin already initialized');
49
+ ASSERT(!postProcess, 'LightSystemPlugin must be created before PostProcessPlugin');
50
+ lightSystem = this;
51
+
52
+ /** @property {boolean} - When false, the render pass is skipped entirely */
53
+ this.enabled = true;
54
+ /** @property {Color} - Baseline color applied to unlit areas of the scene. Defaults to BLACK (pitch dark). Set to a small RGB for a faint ambient. The lightmap is cleared to this color each frame, then lights add on top, then the result multiplies the scene. */
55
+ this.ambientColor = (ambientColor || BLACK).copy();
56
+ /** @property {Vector2} - Size of the lightmap texture (set at construction; falls back to mainCanvasSize at init time) */
57
+ this.textureSize = textureSize ? textureSize.copy() : undefined;
58
+
59
+ /** @property {WebGLTexture} - The lightmap texture */
60
+ this.texture = undefined;
61
+ /** @property {WebGLProgram} - Shader for drawing per-Light falloff blobs into the lightmap */
62
+ this.lightShader = undefined;
63
+ /** @property {WebGLProgram} - Shader for compositing the lightmap over the main scene */
64
+ this.compositeShader = undefined;
65
+ /** @property {WebGLVertexArrayObject} - Vertex array object for the light shader */
66
+ this.lightVAO = undefined;
67
+ /** @property {WebGLVertexArrayObject} - Vertex array object for the composite shader */
68
+ this.compositeVAO = undefined;
69
+
70
+ initLightSystem();
71
+ engineAddPlugin(undefined, lightSystemRender,
72
+ lightSystemContextLost, lightSystemContextRestored);
73
+
74
+ function initLightSystem()
75
+ {
76
+ if (headlessMode) return;
77
+ if (!glEnable)
78
+ {
79
+ console.warn('LightSystemPlugin: WebGL not enabled!');
80
+ return;
81
+ }
82
+
83
+ // resolve texture size default at init time (mainCanvasSize may
84
+ // not be set yet at the moment the constructor first ran)
85
+ if (!lightSystem.textureSize)
86
+ lightSystem.textureSize = mainCanvasSize.copy();
87
+
88
+ // allocate the lightmap texture with null data at textureSize
89
+ lightSystem.texture = glContext.createTexture();
90
+ glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
91
+ glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
92
+ lightSystem.textureSize.x, lightSystem.textureSize.y, 0,
93
+ glContext.RGBA, glContext.UNSIGNED_BYTE, null);
94
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, glContext.LINEAR);
95
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, glContext.LINEAR);
96
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, glContext.CLAMP_TO_EDGE);
97
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, glContext.CLAMP_TO_EDGE);
98
+
99
+ // light falloff shader: one quad per Light, fragment computes radial falloff
100
+ lightSystem.lightShader = glCreateProgram(
101
+ '#version 300 es\n' +
102
+ 'precision highp float;'+
103
+ 'uniform mat4 m;'+
104
+ 'uniform vec2 lightPos;'+
105
+ 'uniform float radius;'+
106
+ 'in vec2 g;'+ // unit quad geometry [0..1]
107
+ 'out vec2 vWorldPos;'+
108
+ 'void main(){'+
109
+ 'vec2 worldP=lightPos+(g-.5)*2.*radius;'+
110
+ 'gl_Position=m*vec4(worldP,1,1);'+
111
+ 'vWorldPos=worldP;'+
112
+ '}'
113
+ ,
114
+ '#version 300 es\n' +
115
+ 'precision highp float;'+
116
+ 'uniform vec2 lightPos;'+
117
+ 'uniform float radius;'+
118
+ 'uniform float fadeRange;'+
119
+ 'uniform vec4 color;'+
120
+ 'in vec2 vWorldPos;'+
121
+ 'out vec4 c;'+
122
+ 'void main(){'+
123
+ 'float dist=distance(vWorldPos,lightPos);'+
124
+ 'float t=clamp((radius-dist)/max(fadeRange,1e-6),0.,1.);'+
125
+ 'c=vec4(color.rgb*t*color.a,1.);'+
126
+ '}'
127
+ );
128
+
129
+ // composite shader: fullscreen quad, samples the lightmap
130
+ lightSystem.compositeShader = glCreateProgram(
131
+ '#version 300 es\n' +
132
+ 'precision highp float;'+
133
+ 'in vec2 p;'+
134
+ 'void main(){'+
135
+ 'gl_Position=vec4(p+p-1.,1,1);'+
136
+ '}'
137
+ ,
138
+ '#version 300 es\n' +
139
+ 'precision highp float;'+
140
+ 'uniform sampler2D s;'+
141
+ 'uniform vec3 iResolution;'+
142
+ 'out vec4 c;'+
143
+ 'void main(){'+
144
+ 'vec2 uv=gl_FragCoord.xy/iResolution.xy;'+
145
+ 'c=vec4(texture(s,uv).rgb,1.);'+
146
+ '}'
147
+ );
148
+
149
+ // VAO for the per-Light quad — reuses the engine unit triangle-strip
150
+ lightSystem.lightVAO = glContext.createVertexArray();
151
+ glContext.bindVertexArray(lightSystem.lightVAO);
152
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
153
+ const gLight = glContext.getAttribLocation(lightSystem.lightShader, 'g');
154
+ glContext.enableVertexAttribArray(gLight);
155
+ glContext.vertexAttribPointer(gLight, 2, glContext.FLOAT, false, 8, 0);
156
+
157
+ // VAO for the composite fullscreen quad — same buffer, attribute named 'p'
158
+ lightSystem.compositeVAO = glContext.createVertexArray();
159
+ glContext.bindVertexArray(lightSystem.compositeVAO);
160
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer);
161
+ const pComp = glContext.getAttribLocation(lightSystem.compositeShader, 'p');
162
+ glContext.enableVertexAttribArray(pComp);
163
+ glContext.vertexAttribPointer(pComp, 2, glContext.FLOAT, false, 8, 0);
164
+ }
165
+ function lightSystemRender()
166
+ {
167
+ if (headlessMode || !glEnable) return;
168
+ if (!lightSystem.enabled) return;
169
+ if (!lightSystem.texture) return; // init failed or context lost
170
+
171
+ // 1. flush any in-flight sprite batch from earlier render passes
172
+ glFlush();
173
+ const prevAdditive = glAdditive;
174
+
175
+ // 2. bind lightmap as render target, clear to ambientColor
176
+ const ac = lightSystem.ambientColor;
177
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
178
+ glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
179
+ glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, lightSystem.texture, 0);
180
+ glContext.viewport(0, 0, lightSystem.textureSize.x, lightSystem.textureSize.y);
181
+ glContext.clearColor(ac.r, ac.g, ac.b, ac.a);
182
+ glContext.clear(glContext.COLOR_BUFFER_BIT);
183
+
184
+ // 3. walk engineObjects calling renderLight() — additive blend
185
+ // (lightmap accumulates raw additive color contributions)
186
+ setAdditiveBlendMode();
187
+ glContext.enable(glContext.BLEND);
188
+ glContext.blendFunc(glContext.ONE, glContext.ONE);
189
+
190
+ for (const o of engineObjects)
191
+ o.destroyed || o.renderLight();
192
+
193
+ // 4. drain any sprite-batched draws (e.g. drawTile inside a
194
+ // custom renderLight override) so they hit the FBO, not the
195
+ // canvas after we unbind
196
+ glFlush();
197
+ glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
198
+ glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
199
+
200
+ // 5. composite: fullscreen quad, multiplicative blend onto glCanvas
201
+ // (scene * lightmap — unlit areas go to black, lit areas are
202
+ // the scene tinted by the accumulated light color)
203
+ glContext.useProgram(lightSystem.compositeShader);
204
+ glContext.bindVertexArray(lightSystem.compositeVAO);
205
+ glContext.activeTexture(glContext.TEXTURE0);
206
+ glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
207
+ const cs = lightSystem.compositeShader;
208
+ glContext.uniform1i(glContext.getUniformLocation(cs, 's'), 0);
209
+ glContext.uniform3f(glContext.getUniformLocation(cs, 'iResolution'),
210
+ mainCanvas.width, mainCanvas.height, 1);
211
+ glContext.blendFunc(glContext.DST_COLOR, glContext.ZERO);
212
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
213
+
214
+ // 6. restore engine state so subsequent draws use the engine's
215
+ // tracked texture binding (otherwise glSetTexture would think
216
+ // the prior texture was still bound when actually the lightmap
217
+ // is, and any debug text / future draw could sample the lightmap)
218
+ if (glActiveTexture)
219
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
220
+ setAdditiveBlendMode(prevAdditive);
221
+ glSetInstancedMode(true);
222
+ }
223
+ function lightSystemContextLost()
224
+ {
225
+ lightSystem.texture = undefined;
226
+ lightSystem.lightShader = undefined;
227
+ lightSystem.compositeShader = undefined;
228
+ lightSystem.lightVAO = undefined;
229
+ lightSystem.compositeVAO = undefined;
230
+ LOG('LightSystemPlugin: WebGL context lost');
231
+ }
232
+ function lightSystemContextRestored()
233
+ {
234
+ initLightSystem();
235
+ LOG('LightSystemPlugin: WebGL context restored');
236
+ }
237
+ }
238
+
239
+ /** Draw a single Light's falloff blob into the currently bound lightmap.
240
+ * Called by Light.renderLight() during the plugin's render pass.
241
+ * @param {Light} light */
242
+ drawLight(light)
243
+ {
244
+ if (headlessMode || !glEnable || !this.lightShader) return;
245
+
246
+ // drain any sprite-batched draws queued by a previous custom
247
+ // renderLight() override (e.g. drawRect inside a LavaTile). They were
248
+ // queued in the engine's instanced-vertex format and must flush with
249
+ // the engine's shader+VAO bound — NOT this plugin's light shader.
250
+ glFlush();
251
+
252
+ glContext.useProgram(this.lightShader);
253
+ glContext.bindVertexArray(this.lightVAO);
254
+
255
+ // re-apply the engine camera transform onto this shader. Divide by
256
+ // mainCanvasSize (not textureSize) so world→NDC matches the main
257
+ // pass; the viewport handles the lightmap's actual resolution.
258
+ // No y-flip here: the composite samples this FBO with
259
+ // gl_FragCoord/iResolution (origin bottom-left), so storing world
260
+ // +Y at the top of the texture lines up with the canvas convention.
261
+ const s = vec2(2*cameraScale).divide(mainCanvasSize);
262
+ const rotatedCam = cameraPos.rotate(-cameraAngle);
263
+ const p = vec2(-1).subtract(rotatedCam.multiply(s));
264
+ const ca = cos(cameraAngle);
265
+ const sa = sin(cameraAngle);
266
+ const transform = [
267
+ s.x * ca, s.y * sa, 0, 0,
268
+ -s.x * sa, s.y * ca, 0, 0,
269
+ 1, 1, 1, 0,
270
+ p.x, p.y, 0, 1];
271
+
272
+ const ls = this.lightShader;
273
+ glContext.uniformMatrix4fv(glContext.getUniformLocation(ls, 'm'), false, transform);
274
+ glContext.uniform2f(glContext.getUniformLocation(ls, 'lightPos'), light.pos.x, light.pos.y);
275
+ glContext.uniform1f(glContext.getUniformLocation(ls, 'radius'), light.radius);
276
+ glContext.uniform1f(glContext.getUniformLocation(ls, 'fadeRange'), light.fadeRange);
277
+ const c = light.color;
278
+ glContext.uniform4f(glContext.getUniformLocation(ls, 'color'), c.r, c.g, c.b, c.a);
279
+
280
+ glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4);
281
+
282
+ // restore engine's instanced shader+VAO so subsequent renderLight()
283
+ // overrides that batch through drawRect/drawTile work correctly
284
+ glSetInstancedMode(true);
285
+ }
286
+ }
287
+
288
+ ///////////////////////////////////////////////////////////////////////////////
289
+
290
+ /**
291
+ * A Light is an EngineObject that contributes a soft additive blob of color
292
+ * to the LightSystem plugin's lightmap.
293
+ * @extends EngineObject
294
+ * @memberof LightSystem
295
+ * @example
296
+ * new Light(vec2(5, 5), 4, rgb(1, 0.5, 0)); // orange light, full soft blob
297
+ * new Light(vec2(0, 0), 8, rgb(1, 1, 1), 2); // white core with 2-unit soft halo
298
+ */
299
+ class Light extends EngineObject
300
+ {
301
+ /** Create a light object and add it to the engine object list
302
+ * @param {Vector2} pos - World space position
303
+ * @param {number} radius - Total extent of the light in world units
304
+ * @param {Color} [color] - Color of the light; alpha modulates intensity
305
+ * @param {number} [fadeRange] - Width of the soft edge in world units (defaults to radius) */
306
+ constructor(pos, radius, color, fadeRange)
307
+ {
308
+ super(pos, vec2(1), undefined, 0, color);
309
+ ASSERT(isNumber(radius) && radius >= 0, 'Light radius must be a non-negative number');
310
+ ASSERT(fadeRange === undefined || (isNumber(fadeRange) && fadeRange >= 0),
311
+ 'Light fadeRange must be a non-negative number when provided');
312
+
313
+ /** @property {number} - Total extent of the light in world units */
314
+ this.radius = radius;
315
+ /** @property {number} - Width of the soft edge in world units */
316
+ this.fadeRange = fadeRange === undefined ? radius : fadeRange;
317
+ }
318
+
319
+ /** Lights are invisible in the main render pass — they only contribute
320
+ * to the lightmap via renderLight(). */
321
+ render() {}
322
+
323
+ /** Draw this light's falloff blob into the lightmap.
324
+ * Called by LightSystemPlugin during its render pass. No-op when the
325
+ * plugin or WebGL is unavailable. */
326
+ renderLight()
327
+ {
328
+ lightSystem && lightSystem.drawLight(this);
329
+ }
330
+ }
@@ -60,7 +60,15 @@ function medalsInit(saveName)
60
60
  // check if medals are unlocked
61
61
  medalsSaveName = saveName;
62
62
  if (!debugMedals)
63
- medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]);
63
+ {
64
+ let saved = {};
65
+ try { saved = JSON.parse(localStorage[saveName] || '{}'); }
66
+ catch (e) { saved = {}; }
67
+ medalsForEach(medal => {
68
+ medal.unlocked = !!(saved[medal.id] && saved[medal.id].unlocked);
69
+ });
70
+ medalsSave();
71
+ }
64
72
 
65
73
  // engine automatically renders medals
66
74
  engineAddPlugin(undefined, medalsRender);
@@ -104,6 +112,31 @@ function medalsInit(saveName)
104
112
  function medalsForEach(callback)
105
113
  { Object.values(medals).forEach(medal=>callback(medal)); }
106
114
 
115
+ /** Reset all medals to locked and persist the cleared catalog
116
+ * @memberof Medals */
117
+ function medalsReset()
118
+ {
119
+ medalsForEach(medal => medal.unlocked = false);
120
+ medalsSave();
121
+ }
122
+
123
+ function medalsSave()
124
+ {
125
+ if (!medalsSaveName) return;
126
+ const data = {};
127
+ medalsForEach(medal => {
128
+ const entry = {
129
+ name: medal.name,
130
+ description: medal.description,
131
+ icon: medal.icon,
132
+ unlocked: medal.unlocked,
133
+ };
134
+ if (medal.image) entry.src = medal.image.src;
135
+ data[medal.id] = entry;
136
+ });
137
+ localStorage[medalsSaveName] = JSON.stringify(data);
138
+ }
139
+
107
140
  ///////////////////////////////////////////////////////////////////////////////
108
141
 
109
142
  /**
@@ -161,9 +194,9 @@ class Medal
161
194
  {
162
195
  if (medalsPreventUnlock || this.unlocked) return;
163
196
 
164
- // save the medal
165
197
  ASSERT(medalsSaveName, 'save name must be set');
166
- localStorage[this.storageKey()] = this.unlocked = true;
198
+ this.unlocked = true;
199
+ medalsSave();
167
200
  medalsDisplayQueue.push(this);
168
201
  }
169
202
 
@@ -221,8 +254,6 @@ class Medal
221
254
  drawTextScreen(this.icon, pos, size*.7, BLACK);
222
255
  }
223
256
 
224
- // Get local storage key used by the medal
225
- storageKey() { return medalsSaveName + '_' + this.id; }
226
257
  }
227
258
 
228
259
  ///////////////////////////////////////////////////////////////////////////////
@@ -12,6 +12,7 @@ export
12
12
  medalDisplaySize,
13
13
  medalsInit,
14
14
  medalsForEach,
15
+ medalsReset,
15
16
  setMedalDisplayTime,
16
17
  setMedalDisplaySlideTime,
17
18
  setMedalDisplaySize,
@@ -27,6 +28,11 @@ export
27
28
  postProcess,
28
29
  PostProcessPlugin,
29
30
 
31
+ // Light System
32
+ lightSystem,
33
+ LightSystemPlugin,
34
+ Light,
35
+
30
36
  // ZzFXMusic
31
37
  ZzFXMusic,
32
38
  zzfxM,
@@ -76,6 +82,8 @@ export
76
82
  drawNineSliceScreen,
77
83
  drawThreeSlice,
78
84
  drawThreeSliceScreen,
85
+ drawCrescent,
86
+ getCrescentPoints,
79
87
 
80
88
  // Tween System
81
89
  Tween,