littlejsengine 1.18.29 → 1.20.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 (51) hide show
  1. package/FAQ.md +841 -815
  2. package/README.md +211 -199
  3. package/dist/littlejs.d.ts +4383 -812
  4. package/dist/littlejs.esm.js +31848 -16680
  5. package/dist/littlejs.esm.min.js +14 -1
  6. package/dist/littlejs.js +31230 -16166
  7. package/dist/littlejs.min.js +13 -1
  8. package/dist/littlejs.release.js +27419 -15457
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +375 -0
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +740 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -330
  16. package/plugins/math3d.js +911 -0
  17. package/plugins/medalSystem.js +118 -72
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +196 -115
  21. package/plugins/postProcess.js +284 -166
  22. package/plugins/render3d.js +4072 -0
  23. package/plugins/render3dDebug.js +109 -0
  24. package/plugins/render3dExtras.js +1754 -0
  25. package/plugins/render3dVoxels.js +502 -0
  26. package/plugins/textureSheet.js +86 -48
  27. package/plugins/threejs.js +183 -158
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +646 -526
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +8 -15
  32. package/src/engine.js +345 -194
  33. package/src/engineAudio.js +1022 -777
  34. package/src/engineBuild.mjs +289 -253
  35. package/src/engineDebug.js +912 -735
  36. package/src/engineDraw.js +1774 -1510
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +419 -396
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineLogo.js +4 -3
  41. package/src/engineMath.js +1274 -1183
  42. package/src/engineObject.js +674 -551
  43. package/src/engineParticles.js +583 -532
  44. package/src/engineRelease.js +67 -45
  45. package/src/engineSettings.js +808 -726
  46. package/src/engineTileLayer.js +995 -741
  47. package/src/engineUtilities.js +32 -13
  48. package/src/engineWebGL.js +1162 -941
  49. package/src/jsconfig.json +17 -13
  50. package/plugins/box2d.wasm.js +0 -630
  51. package/plugins/box2d.wasm.wasm +0 -0
@@ -2,16 +2,16 @@
2
2
  * LittleJS Medal System
3
3
  * - Achievement/trophy system for games
4
4
  * - Medal class with name, description, icon, and unlock tracking
5
- * - Automatic saving to local storage
5
+ * - Automatic saving to local storage, unless a service like Newgrounds holds the medal (see Medal.isLocal)
6
6
  * - Visual display queue with slide-in notifications
7
- * - Newgrounds API integration for online achievements
8
- * - Debug mode to unlock/reset medals during development
7
+ * - The Newgrounds plugin extends it with NewgroundsMedal, held on the server while logged in
8
+ * - Setting debugMedals = true in the game code before medalsInit skips the load and the save, and in the debug build logs the Newgrounds traffic; it is not exported, so only a script tag build can set it
9
9
  * @namespace Medals
10
10
  */
11
11
 
12
12
  'use strict';
13
13
 
14
- let debugMedals = false;
14
+ let debugMedals = false; // see the header
15
15
 
16
16
  ///////////////////////////////////////////////////////////////////////////////
17
17
  // Medals settings
@@ -41,62 +41,75 @@ let medalDisplaySize = vec2(640, 80);
41
41
  let medalsPreventUnlock = false;
42
42
 
43
43
  /** List of all medals
44
- * @type {Object}
44
+ * @type {Object<number, Medal>}
45
45
  * @memberof Medals */
46
46
  const medals = {};
47
47
 
48
48
  // Engine internal variables not exposed to documentation
49
- let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
49
+ let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast, medalsRenderAdded;
50
+ let medalsLoadWaiting = false; // medalsInit came before any medal, each one made reads its own unlock
50
51
 
51
52
  ///////////////////////////////////////////////////////////////////////////////
52
53
 
53
54
  /** Initialize medals with a save name used for storage
54
55
  * - Call this after creating all medals
55
- * - Checks if medals are unlocked
56
- * @param {string} saveName
56
+ * - Loads which medals are unlocked from the save, and writes the catalog back
57
+ * - A medal a service like Newgrounds holds is left as it is, see Medal.isLocal
58
+ * @param {string} saveName - The localStorage key the medals are kept under, a different one from the game's own
59
+ * readSaveData and writeSaveData, or each would overwrite the other
57
60
  * @memberof Medals */
58
61
  function medalsInit(saveName)
59
62
  {
60
- // check if medals are unlocked
61
63
  medalsSaveName = saveName;
62
- if (!debugMedals)
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
+ medalsLoad();
65
+
66
+ // add the medal display once, however often this is called
67
+ if (!medalsRenderAdded)
68
+ engineAddPlugin(undefined, medalsRender);
69
+ medalsRenderAdded = true;
70
+ }
72
71
 
73
- // engine automatically renders medals
74
- engineAddPlugin(undefined, medalsRender);
72
+ // check which local medals are unlocked in the save, and write the catalog back
73
+ function medalsLoad()
74
+ {
75
+ // with no medals made yet, the save is left as it is for them, a game that calls medalsInit first keeps its unlocks;
76
+ // it keeps waiting from then on, so loading again (a dropped Newgrounds session) does not drop medals still to come
77
+ medalsLoadWaiting ||= !Object.keys(medals).length;
78
+ if (debugMedals || !medalsSaveName) return;
79
+ const saved = readSaveData(medalsSaveName);
80
+ ASSERT(Object.keys(saved).every(key=> isNumber(+key)),
81
+ 'the medals save name holds other data, give medalsInit a name of its own');
82
+ medalsForEach(medal=> {
83
+ if (medal.isLocal())
84
+ medal.unlocked = !!saved[medal.id]?.unlocked;
85
+ });
86
+ medalsSave();
87
+ }
75
88
 
76
- // plugin functions
77
- function medalsRender()
89
+ // show the first medal in the queue, sliding it on and off
90
+ function medalsRender()
91
+ {
92
+ if (!medalsDisplayQueue.length) return;
93
+
94
+ // update first medal in queue
95
+ const medal = medalsDisplayQueue[0];
96
+ const elapsed = timeReal - medalsDisplayTimeLast;
97
+ if (!medalsDisplayTimeLast)
98
+ medalsDisplayTimeLast = timeReal;
99
+ else if (elapsed > medalDisplayTime)
78
100
  {
79
- if (!medalsDisplayQueue.length) return;
80
-
81
- // update first medal in queue
82
- const medal = medalsDisplayQueue[0];
83
- const time = timeReal - medalsDisplayTimeLast;
84
- if (!medalsDisplayTimeLast)
85
- medalsDisplayTimeLast = timeReal;
86
- else if (time > medalDisplayTime)
87
- {
88
- medalsDisplayTimeLast = 0;
89
- medalsDisplayQueue.shift();
90
- }
91
- else
92
- {
93
- // slide on/off medals
94
- const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
95
- const hidePercent =
96
- time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
97
- time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
98
- medal.render(hidePercent);
99
- }
101
+ medalsDisplayTimeLast = 0;
102
+ medalsDisplayQueue.shift();
103
+ }
104
+ else
105
+ {
106
+ // slide on/off medals, the slides share the display time when it is short
107
+ const slideTime = min(medalDisplaySlideTime, medalDisplayTime/2);
108
+ const slideOffTime = medalDisplayTime - slideTime;
109
+ const hidePercent =
110
+ elapsed < slideTime ? 1 - elapsed / slideTime :
111
+ elapsed > slideOffTime ? (elapsed - slideOffTime) / slideTime : 0;
112
+ medal.render(hidePercent);
100
113
  }
101
114
  }
102
115
 
@@ -110,21 +123,42 @@ function medalsInit(saveName)
110
123
  * @param {MedalCallbackFunction} callback
111
124
  * @memberof Medals */
112
125
  function medalsForEach(callback)
113
- { Object.values(medals).forEach(medal=>callback(medal)); }
126
+ { Object.values(medals).forEach(medal=> callback(medal)); }
114
127
 
115
128
  /** Reset all medals to locked and persist the cleared catalog
129
+ * - A medal a service like Newgrounds holds is left alone, the service has it
116
130
  * @memberof Medals */
117
131
  function medalsReset()
118
132
  {
119
- medalsForEach(medal => medal.unlocked = false);
133
+ medalsForEach(medal=> medal.isLocal() && (medal.unlocked = false));
134
+ if (medalsLoadWaiting && medalsSaveName && !debugMedals)
135
+ {
136
+ // the saved unlocks of medals not made yet are cleared too, they are read when those medals are made
137
+ const saved = readSaveData(medalsSaveName);
138
+ for (const id in saved)
139
+ if (!medals[id] && saved[id] && typeof saved[id] === 'object')
140
+ saved[id].unlocked = false;
141
+ writeSaveData(medalsSaveName, saved);
142
+ }
120
143
  medalsSave();
121
144
  }
122
145
 
146
+ // write the local medals to the save, keeping the entries of medals a service holds
123
147
  function medalsSave()
124
148
  {
125
- if (!medalsSaveName) return;
126
- const data = {};
127
- medalsForEach(medal => {
149
+ if (debugMedals || !medalsSaveName) return;
150
+ // while medalsInit waits for medals made later, their saved entries are kept for them
151
+ const saved = readSaveData(medalsSaveName);
152
+ const data = medalsLoadWaiting ? {...saved} : {};
153
+ for (const key in saved) // what is not a medal is the game's own, saved under the same name, and stays
154
+ isNumber(+key) || (data[key] = saved[key]);
155
+ medalsForEach(medal=> {
156
+ if (!medal.isLocal())
157
+ {
158
+ // a service holds this medal, its entry stays as it was for when it is local again
159
+ if (saved[medal.id]) data[medal.id] = saved[medal.id];
160
+ return;
161
+ }
128
162
  const entry = {
129
163
  name: medal.name,
130
164
  description: medal.description,
@@ -134,7 +168,7 @@ function medalsSave()
134
168
  if (medal.image) entry.src = medal.image.src;
135
169
  data[medal.id] = entry;
136
170
  });
137
- localStorage[medalsSaveName] = JSON.stringify(data);
171
+ writeSaveData(medalsSaveName, data);
138
172
  }
139
173
 
140
174
  ///////////////////////////////////////////////////////////////////////////////
@@ -154,7 +188,7 @@ function medalsSave()
154
188
  */
155
189
  class Medal
156
190
  {
157
- /** Create a medal object and adds it to the list of medals
191
+ /** Create a medal and add it to the list of medals
158
192
  * @param {number} id - The unique identifier of the medal
159
193
  * @param {string} name - Name of the medal
160
194
  * @param {string} [description] - Description of the medal
@@ -163,7 +197,7 @@ class Medal
163
197
  */
164
198
  constructor(id, name, description='', icon='🏆', src)
165
199
  {
166
- ASSERT(id >= 0 && !medals[id]);
200
+ ASSERT(isNumber(id) && id >= 0 && !medals[id], 'medal id must be a unique number of 0 or more');
167
201
 
168
202
  /** @property {number} - The unique identifier of the medal */
169
203
  this.id = id;
@@ -180,35 +214,46 @@ class Medal
180
214
  /** @property {boolean} - Is the medal unlocked? */
181
215
  this.unlocked = false;
182
216
 
183
- /** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */
217
+ /** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any
218
+ * @type {HTMLImageElement|undefined} */
184
219
  this.image = undefined;
185
220
  if (src)
186
221
  (this.image = new Image).src = src;
187
222
 
188
- // add this to list of medals
223
+ // add this to list of medals, unlocked if the save says so when medalsInit came before any medal
189
224
  medals[id] = this;
225
+ if (medalsLoadWaiting && !debugMedals && this.isLocal())
226
+ this.unlocked = !!readSaveData(medalsSaveName)[id]?.unlocked;
190
227
  }
191
228
 
192
- /** Unlocks a medal if not already unlocked */
229
+ /** Unlocks a medal if not already unlocked
230
+ * - The promise is optional, for when a game wants to know the outcome
231
+ * @return {Promise<boolean>} - Whether the medal is unlocked, right away unless a service like Newgrounds has to confirm */
193
232
  unlock()
194
233
  {
195
- if (medalsPreventUnlock || this.unlocked) return;
196
-
197
- ASSERT(medalsSaveName, 'save name must be set');
198
- this.unlocked = true;
199
- medalsSave();
200
- medalsDisplayQueue.push(this);
234
+ if (!medalsPreventUnlock && !this.unlocked)
235
+ {
236
+ ASSERT(medalsSaveName, 'save name must be set');
237
+ this.unlocked = true;
238
+ medalsSave();
239
+ medalsDisplayQueue.push(this);
240
+ }
241
+ return Promise.resolve(this.unlocked);
201
242
  }
202
243
 
244
+ /** Whether the local save holds this medal, it is neither loaded nor written while a service like Newgrounds holds it
245
+ * @return {boolean} */
246
+ isLocal() { return true; }
247
+
203
248
  /** Render a medal
204
249
  * @param {number} [hidePercent] - How much to slide the medal off screen
205
250
  */
206
251
  render(hidePercent=0)
207
252
  {
208
253
  const context = mainContext;
209
- const width = min(medalDisplaySize.x, mainCanvas.width);
254
+ const width = min(medalDisplaySize.x, mainCanvasSize.x);
210
255
  const height = medalDisplaySize.y;
211
- const x = mainCanvas.width - width;
256
+ const x = mainCanvasSize.x - width;
212
257
  const y = -height*hidePercent;
213
258
  const backgroundColor = hsl(0,0,.9);
214
259
 
@@ -217,22 +262,22 @@ class Medal
217
262
  context.beginPath();
218
263
  context.fillStyle = backgroundColor.toString();
219
264
  context.strokeStyle = BLACK.toString();
220
- context.lineWidth = 3;
221
- context.rect(x, y, width, height);
265
+ const lineWidth = context.lineWidth = 3;
266
+ context.rect(x + lineWidth/2, y + lineWidth/2, width - lineWidth, height - lineWidth); // the whole border shows
222
267
  context.fill();
223
268
  context.stroke();
224
269
  context.clip();
225
270
 
226
271
  // draw the icon
227
272
  const gap = vec2(.1, .05).scale(height);
228
- const medalDisplayIconSize = height - 2*gap.x;
229
- this.renderIcon(vec2(x + gap.x + medalDisplayIconSize/2, y + height/2), medalDisplayIconSize);
273
+ const iconSize = height - 2*gap.x;
274
+ this.renderIcon(vec2(x + gap.x + iconSize/2, y + height/2), iconSize);
230
275
 
231
276
  // draw the name
232
277
  const nameSize = height*.5;
233
278
  const descriptionSize = height*.3;
234
- const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
235
- const textWidth = width - medalDisplayIconSize - 3*gap.x;
279
+ const pos = vec2(x + iconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
280
+ const textWidth = width - iconSize - 3*gap.x;
236
281
  drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
237
282
 
238
283
  // draw the description
@@ -247,9 +292,10 @@ class Medal
247
292
  */
248
293
  renderIcon(pos, size)
249
294
  {
250
- // draw the image or icon
251
- if (this.image)
252
- mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
295
+ // draw the image once it has loaded, or the icon; a broken image would throw
296
+ const image = this.image;
297
+ if (image && image.complete && image.naturalWidth)
298
+ mainContext.drawImage(image, pos.x-size/2, pos.y-size/2, size, size);
253
299
  else
254
300
  drawTextScreen(this.icon, pos, size*.7, BLACK);
255
301
  }