littlejsengine 1.18.12 → 1.18.17
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/FAQ.md +197 -15
- package/README.md +31 -10
- package/dist/littlejs.d.ts +301 -92
- package/dist/littlejs.esm.js +1578 -518
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1554 -517
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1525 -494
- package/package.json +4 -1
- package/plugins/box2d.js +26 -19
- package/plugins/drawUtilities.js +63 -0
- package/plugins/lightSystem.js +330 -0
- package/plugins/medalSystem.js +36 -5
- package/plugins/newgrounds.js +15 -3
- package/plugins/pathFinder.js +5 -3
- package/plugins/pluginExport.js +12 -3
- package/plugins/postProcess.js +8 -1
- package/plugins/tweenSystem.js +25 -20
- package/plugins/uiSystem.js +21 -3
- package/src/engine.js +14 -3
- package/src/engineAudio.js +48 -16
- package/src/engineBuild.mjs +2 -1
- package/src/engineDebug.js +30 -23
- package/src/engineDraw.js +80 -46
- package/src/engineExport.js +16 -2
- package/src/engineInput.js +534 -190
- package/src/engineLogo.js +1 -1
- package/src/engineMath.js +21 -7
- package/src/engineObject.js +24 -13
- package/src/engineParticles.js +45 -42
- package/src/engineRelease.js +1 -0
- package/src/engineSettings.js +112 -10
- package/src/engineTileLayer.js +67 -43
- package/src/engineUtilities.js +24 -11
- package/src/engineWebGL.js +23 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlejsengine",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.17",
|
|
4
4
|
"description": "LittleJS - Tiny and Fast HTML5 Game Engine",
|
|
5
5
|
"main": "dist/littlejs.esm.js",
|
|
6
6
|
"types": "dist/littlejs.d.ts",
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"build": "node src/engineBuild.mjs",
|
|
41
41
|
"test": "node --test --import ./test/setup.mjs \"test/**/*.test.mjs\""
|
|
42
42
|
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"bestzip": "^2.2.1",
|
|
45
48
|
"electron": "^38.2.0",
|
package/plugins/box2d.js
CHANGED
|
@@ -83,6 +83,11 @@ class Box2dObject extends EngineObject
|
|
|
83
83
|
// destroy physics body, fixtures, and joints
|
|
84
84
|
ASSERT(this.body, 'Box2dObject has no body to destroy');
|
|
85
85
|
box2d.world.DestroyBody(this.body);
|
|
86
|
+
|
|
87
|
+
// remove from tracked list so paused / headless sessions don't leak
|
|
88
|
+
const i = box2d.objects.indexOf(this);
|
|
89
|
+
if (i >= 0)
|
|
90
|
+
box2d.objects.splice(i, 1);
|
|
86
91
|
super.destroy();
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -171,7 +176,9 @@ class Box2dObject extends EngineObject
|
|
|
171
176
|
/** Add a box shape to the body
|
|
172
177
|
* @param {Vector2} [size]
|
|
173
178
|
* @param {Vector2} [offset]
|
|
174
|
-
* @param {number} [angle]
|
|
179
|
+
* @param {number} [angle] - LittleJS convention (clockwise positive).
|
|
180
|
+
* Negated internally to match Box2D's CCW-positive convention so the
|
|
181
|
+
* fixture aligns with the same angle passed to drawRect/drawTile.
|
|
175
182
|
* @param {number} [density]
|
|
176
183
|
* @param {number} [friction]
|
|
177
184
|
* @param {number} [restitution]
|
|
@@ -184,7 +191,7 @@ class Box2dObject extends EngineObject
|
|
|
184
191
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
185
192
|
|
|
186
193
|
const shape = new box2d.instance.b2PolygonShape();
|
|
187
|
-
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
|
|
194
|
+
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), -angle);
|
|
188
195
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
189
196
|
}
|
|
190
197
|
|
|
@@ -200,23 +207,19 @@ class Box2dObject extends EngineObject
|
|
|
200
207
|
|
|
201
208
|
function box2dCreatePolygonShape(points)
|
|
202
209
|
{
|
|
203
|
-
|
|
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)
|
|
204
213
|
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
offset += 4;
|
|
210
|
-
box2d.instance.HEAPF32[buffer + offset >> 2] = points[i].y;
|
|
211
|
-
offset += 4;
|
|
212
|
-
}
|
|
213
|
-
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;
|
|
214
218
|
}
|
|
215
|
-
|
|
216
|
-
ASSERT(3 <= points.length && points.length <= 8);
|
|
219
|
+
const box2dPoints = box2d.instance.wrapPointer(buffer, box2d.instance.b2Vec2);
|
|
217
220
|
const shape = new box2d.instance.b2PolygonShape();
|
|
218
|
-
const box2dPoints = box2dCreatePointList(points);
|
|
219
221
|
shape.Set(box2dPoints, points.length);
|
|
222
|
+
box2d.instance._free(buffer);
|
|
220
223
|
return shape;
|
|
221
224
|
}
|
|
222
225
|
|
|
@@ -486,9 +489,10 @@ class Box2dObject extends EngineObject
|
|
|
486
489
|
{
|
|
487
490
|
const data = new box2d.instance.b2MassData();
|
|
488
491
|
this.body.GetMassData(data);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
+
// use !== undefined so setMass(0) (static-equivalent) isn't silently ignored
|
|
493
|
+
if (localCenter !== undefined) data.set_center(box2d.vec2dTo(localCenter));
|
|
494
|
+
if (mass !== undefined) data.set_mass(mass);
|
|
495
|
+
if (momentOfInertia !== undefined) data.set_I(momentOfInertia);
|
|
492
496
|
this.body.SetMassData(data);
|
|
493
497
|
}
|
|
494
498
|
|
|
@@ -1660,6 +1664,8 @@ class Box2dPlugin
|
|
|
1660
1664
|
const fixtureB = contact.GetFixtureB();
|
|
1661
1665
|
const objectA = fixtureA.GetBody().object;
|
|
1662
1666
|
const objectB = fixtureB.GetBody().object;
|
|
1667
|
+
// raw user-created b2Bodies may have no .object — skip those
|
|
1668
|
+
if (!objectA || !objectB) return;
|
|
1663
1669
|
objectA.beginContact(objectB);
|
|
1664
1670
|
objectB.beginContact(objectA);
|
|
1665
1671
|
}
|
|
@@ -1670,6 +1676,7 @@ class Box2dPlugin
|
|
|
1670
1676
|
const fixtureB = contact.GetFixtureB();
|
|
1671
1677
|
const objectA = fixtureA.GetBody().object;
|
|
1672
1678
|
const objectB = fixtureB.GetBody().object;
|
|
1679
|
+
if (!objectA || !objectB) return;
|
|
1673
1680
|
objectA.endContact(objectB);
|
|
1674
1681
|
objectB.endContact(objectA);
|
|
1675
1682
|
};
|
|
@@ -2048,7 +2055,7 @@ async function box2dInit()
|
|
|
2048
2055
|
debugDraw.DrawTransform = function(transform)
|
|
2049
2056
|
{
|
|
2050
2057
|
transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
|
|
2051
|
-
const pos =
|
|
2058
|
+
const pos = box2d.vec2From(transform.get_p());
|
|
2052
2059
|
const angle = -transform.get_q().GetAngle();
|
|
2053
2060
|
const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
|
|
2054
2061
|
const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
|
package/plugins/drawUtilities.js
CHANGED
|
@@ -129,4 +129,67 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
|
|
|
129
129
|
const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip));
|
|
130
130
|
drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
|
|
131
131
|
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Draw a crescent / moon-phase shape built from a polygon
|
|
135
|
+
* Routes through drawPoly, so it supports WebGL, screen space, color, and outlines
|
|
136
|
+
* @param {Vector2} pos - Center position
|
|
137
|
+
* @param {number} [size] - Diameter
|
|
138
|
+
* @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
|
|
139
|
+
* @param {Color} [color] - Fill color
|
|
140
|
+
* @param {number} [angle] - Angle to rotate by
|
|
141
|
+
* @param {boolean} [invert] - Flip which side is illuminated
|
|
142
|
+
* @param {number} [lineWidth] - Outline width, 0 for no outline
|
|
143
|
+
* @param {Color} [lineColor] - Outline color
|
|
144
|
+
* @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering
|
|
145
|
+
* @param {boolean} [screenSpace] - Use screen space coordinates
|
|
146
|
+
* @param {CanvasRenderingContext2D} [context] - Canvas context to use
|
|
147
|
+
* @memberof DrawUtilities */
|
|
148
|
+
function drawCrescent(pos, size=1, percent=0, color=WHITE, angle=0, invert=false, lineWidth=0, lineColor=BLACK, useWebGL=glEnable, screenSpace=false, context)
|
|
149
|
+
{
|
|
150
|
+
const points = getCrescentPoints(pos, size, percent, angle, invert);
|
|
151
|
+
drawPoly(points, color, lineWidth, lineColor, vec2(), 0, useWebGL, screenSpace, context);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Get the list of points that make up a crescent / moon-phase shape
|
|
155
|
+
* Returns world-space points with pos and angle baked in, ready for drawPoly or other use
|
|
156
|
+
* @param {Vector2} pos - Center position
|
|
157
|
+
* @param {number} [size] - Diameter
|
|
158
|
+
* @param {number} [percent] - Moon phase over a full cycle (0=new, .25=first quarter, .5=full, .75=last quarter), wraps
|
|
159
|
+
* @param {number} [angle] - Angle to rotate by
|
|
160
|
+
* @param {boolean} [invert] - Flip which side is illuminated
|
|
161
|
+
* @param {number} [sides=glCircleSides] - Number of sides for a full circle (halved per arc)
|
|
162
|
+
* @return {Array<Vector2>} - List of points making up the crescent
|
|
163
|
+
* @memberof DrawUtilities */
|
|
164
|
+
function getCrescentPoints(pos, size=1, percent=0, angle=0, invert=false, sides=glCircleSides)
|
|
165
|
+
{
|
|
166
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
167
|
+
ASSERT(isNumber(size) && isNumber(percent), 'size and percent must be numbers');
|
|
168
|
+
|
|
169
|
+
// map phase to a signed terminator curve: -1 new, 0 half, 1 full
|
|
170
|
+
let p = mod(percent*4, 4); // quarter phase 0..4
|
|
171
|
+
if (p >= 2) // second half of cycle flips orientation
|
|
172
|
+
angle += PI;
|
|
173
|
+
p = p <= 2 ? p-1 : 3-p;
|
|
174
|
+
if (invert) // flip the illuminated side
|
|
175
|
+
{
|
|
176
|
+
p = -p;
|
|
177
|
+
angle += PI;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// build the crescent: outer semicircle, then inner half-ellipse traced back
|
|
181
|
+
const points = [];
|
|
182
|
+
const segs = max(3, sides>>1);
|
|
183
|
+
const radius = size/2;
|
|
184
|
+
for (let i=0; i<=segs; i++)
|
|
185
|
+
{
|
|
186
|
+
const t = i/segs*PI;
|
|
187
|
+
points.push(vec2(radius*cos(t), radius*sin(t)).rotate(angle).add(pos));
|
|
188
|
+
}
|
|
189
|
+
for (let i=segs; i>=0; i--)
|
|
190
|
+
{
|
|
191
|
+
const t = i/segs*PI;
|
|
192
|
+
points.push(vec2(radius*cos(t), -radius*p*sin(t)).rotate(angle).add(pos));
|
|
193
|
+
}
|
|
194
|
+
return points;
|
|
132
195
|
}
|
|
@@ -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
|
+
setBlendMode(true);
|
|
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
|
+
setBlendMode(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
|
+
}
|
package/plugins/medalSystem.js
CHANGED
|
@@ -60,7 +60,15 @@ function medalsInit(saveName)
|
|
|
60
60
|
// check if medals are unlocked
|
|
61
61
|
medalsSaveName = saveName;
|
|
62
62
|
if (!debugMedals)
|
|
63
|
-
|
|
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
|
-
|
|
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
|
///////////////////////////////////////////////////////////////////////////////
|
package/plugins/newgrounds.js
CHANGED
|
@@ -82,8 +82,20 @@ class NewgroundsPlugin
|
|
|
82
82
|
|
|
83
83
|
// get medals
|
|
84
84
|
const medalsResult = this.call('Medal.getList');
|
|
85
|
+
|
|
86
|
+
// bail early if the first call failed (offline / bad session /
|
|
87
|
+
// server error) so we don't block the main thread on more sync
|
|
88
|
+
// XHRs that are guaranteed to also fail
|
|
89
|
+
if (!medalsResult || !medalsResult.result || medalsResult.result.error)
|
|
90
|
+
{
|
|
91
|
+
debugMedals && LOG('Newgrounds session unavailable; skipping plugin init');
|
|
92
|
+
this.medals = [];
|
|
93
|
+
this.scoreboards = [];
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
/** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
|
|
86
|
-
this.medals = medalsResult
|
|
98
|
+
this.medals = medalsResult.result.data?.['medals'] || [];
|
|
87
99
|
debugMedals && LOG(this.medals);
|
|
88
100
|
for (const newgroundsMedal of this.medals)
|
|
89
101
|
{
|
|
@@ -103,11 +115,11 @@ class NewgroundsPlugin
|
|
|
103
115
|
medal.description = medal.description + ` (${ medal.value })`;
|
|
104
116
|
}
|
|
105
117
|
}
|
|
106
|
-
|
|
118
|
+
|
|
107
119
|
// get scoreboards
|
|
108
120
|
const scoreboardResult = this.call('ScoreBoard.getBoards');
|
|
109
121
|
/** @property {Array} - Scoreboards fetched from Newgrounds */
|
|
110
|
-
this.scoreboards = scoreboardResult
|
|
122
|
+
this.scoreboards = scoreboardResult?.result?.data?.scoreboards || [];
|
|
111
123
|
debugMedals && LOG(this.scoreboards);
|
|
112
124
|
|
|
113
125
|
// keep the session alive with a ping every minute
|
package/plugins/pathFinder.js
CHANGED
|
@@ -271,11 +271,13 @@ class PathFinder
|
|
|
271
271
|
if (dx !== 0 && dy !== 0)
|
|
272
272
|
{
|
|
273
273
|
// Diagonal step: refuse if either cardinal neighbor is
|
|
274
|
-
// blocked
|
|
274
|
+
// blocked. Prevents cutting through walls at corners.
|
|
275
|
+
// (Costed-but-walkable cardinals do not block — diagonal
|
|
276
|
+
// movement around expensive terrain is standard A*.)
|
|
275
277
|
const card1 = this.getNode(current.pos.x + dx, current.pos.y);
|
|
276
|
-
if (!card1 ||
|
|
278
|
+
if (!card1 || !card1.walkable) continue;
|
|
277
279
|
const card2 = this.getNode(current.pos.x, current.pos.y + dy);
|
|
278
|
-
if (!card2 ||
|
|
280
|
+
if (!card2 || !card2.walkable) continue;
|
|
279
281
|
stepCost = PATHFINDER_DIAGONAL_COST;
|
|
280
282
|
}
|
|
281
283
|
|