littlejsengine 1.24.3 → 1.26.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 (53) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/EDITOR.md +2 -2
  3. package/FAQ.md +55 -11
  4. package/README.md +6 -2
  5. package/REFERENCE.md +203 -57
  6. package/dist/littlejs.d.ts +933 -313
  7. package/dist/littlejs.esm.js +5143 -2419
  8. package/dist/littlejs.esm.min.js +38 -24
  9. package/dist/littlejs.js +5132 -2416
  10. package/dist/littlejs.min.js +37 -23
  11. package/dist/littlejs.release.js +4905 -2232
  12. package/package.json +4 -4
  13. package/plugins/box2d.js +141 -56
  14. package/plugins/drawUtilities.js +374 -380
  15. package/plugins/gltf.js +383 -60
  16. package/plugins/lightSystem.js +412 -30
  17. package/plugins/math3d.js +1015 -1014
  18. package/plugins/medalSystem.js +19 -7
  19. package/plugins/newgrounds.js +5 -6
  20. package/plugins/parallax.js +1 -0
  21. package/plugins/particleEffects.js +47 -14
  22. package/plugins/pathFinder.js +60 -38
  23. package/plugins/pluginExport.js +11 -0
  24. package/plugins/postProcess.js +200 -67
  25. package/plugins/render3d.js +493 -2391
  26. package/plugins/render3dDebug.js +5 -8
  27. package/plugins/render3dEditor.js +84 -9
  28. package/plugins/render3dExtras.js +122 -33
  29. package/plugins/render3dLevel.js +67 -8
  30. package/plugins/render3dMesh.js +1158 -0
  31. package/plugins/render3dObject.js +1451 -0
  32. package/plugins/render3dVoxels.js +15 -3
  33. package/plugins/textureSheet.js +9 -8
  34. package/plugins/threejs.js +1 -1
  35. package/plugins/tweenSystem.js +117 -87
  36. package/plugins/uiSystem.js +62 -28
  37. package/plugins/wavedash.js +229 -0
  38. package/src/engine.js +192 -40
  39. package/src/engineAudio.js +47 -27
  40. package/src/engineBuild.mjs +6 -1
  41. package/src/engineDebug.js +23 -162
  42. package/src/engineDraw.js +148 -43
  43. package/src/engineEditor.js +153 -40
  44. package/src/engineExport.js +0 -3
  45. package/src/engineInput.js +31 -25
  46. package/src/engineMath.js +38 -20
  47. package/src/engineObject.js +68 -50
  48. package/src/engineParticles.js +66 -18
  49. package/src/engineRelease.js +18 -15
  50. package/src/engineSettings.js +48 -9
  51. package/src/engineTileLayer.js +44 -18
  52. package/src/engineUtilities.js +56 -11
  53. package/src/engineWebGL.js +68 -22
@@ -28,6 +28,10 @@ let uiSystem;
28
28
  * @memberof UISystem */
29
29
  let uiDebug = 0;
30
30
 
31
+ // the active object's press was let go of in the frame it came, a tap, so it is clicked on the next with no release
32
+ // then; a click needs a release, which the mouse let go of by a window losing focus is not
33
+ let uiActiveReleased = false;
34
+
31
35
  /** Enable UI system debug drawing
32
36
  * 0=off, 1=normal, 2=show invisible
33
37
  * @param {number|boolean} debugMode
@@ -35,8 +39,14 @@ let uiDebug = 0;
35
39
  function uiSetDebug(debugMode)
36
40
  { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
37
41
 
42
+ /**
43
+ * @callback DragAndDropCallback - Callback for drag and drop events
44
+ * @param {DragEvent} event - The drag event
45
+ * @memberof UISystem
46
+ */
47
+
38
48
  ///////////////////////////////////////////////////////////////////////////////
39
- /**
49
+ /**
40
50
  * UI System Global Object
41
51
  * @memberof UISystem
42
52
  */
@@ -193,8 +203,10 @@ class UISystemPlugin
193
203
 
194
204
  // setup recursive update and render
195
205
  // update in reverse order to detect mouse enter/leave
206
+ let updatePass = 0; // marks the objects each update has reached, see updateObject
196
207
  function uiUpdate()
197
208
  {
209
+ ++updatePass;
198
210
  // a held or focused object that can no longer be used, itself or through a parent, lets go,
199
211
  // and one that was hidden or disabled is still released, a destroyed one stays silent
200
212
  const activeObject = uiSystem.activeObject;
@@ -339,7 +351,10 @@ class UISystemPlugin
339
351
 
340
352
  function updateObject(o)
341
353
  {
342
- if (o.destroyed || !o.visible) return;
354
+ // once a pass, though a callback may move it under a parent not yet reached, or detach it to the
355
+ // top level, where the loop reaches it again
356
+ if (o.destroyed || !o.visible || o.uiUpdatePass === updatePass) return;
357
+ o.uiUpdatePass = updatePass;
343
358
 
344
359
  // update in reverse order to detect mouse enter/leave, from a copy since a child may destroy
345
360
  // siblings mid-update (e.g. dialog close) and the ones after it would shift under the loop
@@ -513,7 +528,7 @@ class UISystemPlugin
513
528
  * @param {'left'|'center'|'right'} [align]
514
529
  * @param {string} [font=uiSystem.defaultFont]
515
530
  * @param {string} [fontStyle]
516
- * @param {boolean} [applyMaxWidth=true]
531
+ * @param {boolean} [applyMaxWidth]
517
532
  * @param {Vector2} [textShadow]
518
533
  * @param {Color} [shadowColor]
519
534
  * @param {number} [shadowBlur]
@@ -528,12 +543,6 @@ class UISystemPlugin
528
543
  context.shadowColor = '#0000';
529
544
  }
530
545
 
531
- /**
532
- * @callback DragAndDropCallback - Callback for drag and drop events
533
- * @param {DragEvent} event - The drag event
534
- * @memberof UISystem
535
- */
536
-
537
546
  /** Setup drag and drop event handlers
538
547
  * Automatically prevents defaults and calls the given functions
539
548
  * @param {DragAndDropCallback} [onDrop] - when a file is dropped
@@ -821,7 +830,7 @@ class UISystemPlugin
821
830
  // close menu and clear the input that closed it
822
831
  function closeMenu()
823
832
  {
824
- ASSERT(uiSystem.confirmDialog === confirmMenu);
833
+ ASSERT(uiSystem.confirmDialog === confirmMenu, 'the confirm dialog closing is not the one open');
825
834
  confirmMenu.destroy();
826
835
  inputClear();
827
836
  }
@@ -961,6 +970,7 @@ class UIObject
961
970
  this.textLineWidth = 0;
962
971
  /** @property {boolean} - Should this object be drawn */
963
972
  this.visible = true;
973
+ this.uiUpdatePass = 0; // the UI update that last reached it, so a pass updates it once
964
974
  /** @property {Array<UIObject>} - A list of this object's children
965
975
  * @type {Array<UIObject>} */
966
976
  this.children = [];
@@ -1023,7 +1033,8 @@ class UIObject
1023
1033
  * @param {UIObject} child */
1024
1034
  removeChild(child)
1025
1035
  {
1026
- ASSERT(child.parent === this && this.children.includes(child));
1036
+ ASSERT(child.parent === this && this.children.includes(child), 'removeChild: that object is not a child of this one',
1037
+ child);
1027
1038
  this.children.splice(this.children.indexOf(child), 1);
1028
1039
  child.parent = undefined;
1029
1040
  }
@@ -1095,10 +1106,24 @@ class UIObject
1095
1106
  return;
1096
1107
 
1097
1108
  const wasHover = uiSystem.lastHoverObject === this;
1098
- const isActive = this.isActiveObject();
1109
+ let isActive = this.isActiveObject();
1099
1110
  const mouseDown = mouseIsDown(0);
1100
1111
  // a press and release in one frame is a press too, as it is for a button that is not drag activated
1101
1112
  const mousePress = mouseWasPressed(0) || this.dragActivate && mouseDown;
1113
+
1114
+ // a new press while this is still active from the one before means that one was let go of in between, by a
1115
+ // second quick tap or a release missed in a hitch: it is clicked and released first, so neither is lost
1116
+ if (isActive && mouseWasPressed(0))
1117
+ {
1118
+ if (!uiSystem.activateOnPress && this.interactive && !disabled)
1119
+ this.click();
1120
+ if (this.destroyed) return;
1121
+ uiSystem.activeObject = undefined;
1122
+ isActive = false;
1123
+ this.onRelease();
1124
+ this.soundRelease && this.soundRelease.play();
1125
+ if (this.destroyed) return;
1126
+ }
1102
1127
  if (this.canBeHover)
1103
1128
  if (!uiSystem.navigationMode) // no mouse hover in navigation mode
1104
1129
  if (mousePress || isActive || (!mouseDown && !isTouchDevice))
@@ -1130,7 +1155,10 @@ class UIObject
1130
1155
  this.soundRelease && this.soundRelease.play();
1131
1156
  }
1132
1157
  else
1158
+ {
1133
1159
  uiSystem.activeObject = this;
1160
+ uiActiveReleased = !mouseDown;
1161
+ }
1134
1162
 
1135
1163
  if (newPress && uiSystem.activateOnPress)
1136
1164
  this.click(!this.soundPress);
@@ -1142,6 +1170,7 @@ class UIObject
1142
1170
  // frame is clicked on the next, with its release, not on both
1143
1171
  if (!uiSystem.activateOnPress)
1144
1172
  if (!mouseDown && isActive && this.isActiveObject() && this.interactive)
1173
+ if (mouseWasReleased(0) || uiActiveReleased)
1145
1174
  this.click();
1146
1175
  if (this.destroyed) return;
1147
1176
  }
@@ -1194,7 +1223,7 @@ class UIObject
1194
1223
  getTextSize()
1195
1224
  {
1196
1225
  // text fitted to the size shares its height between its lines, a set textHeight is the height of each line
1197
- const lines = this.textHeight ? 1 : (this.text + '').split('\n').length;
1226
+ const lines = this.textHeight ? 1 : textLineCount(this.text + '');
1198
1227
  return vec2(
1199
1228
  this.textWidth || this.textFitScale * this.size.x,
1200
1229
  this.textHeight || this.textFitScale * this.size.y / lines);
@@ -1212,19 +1241,24 @@ class UIObject
1212
1241
  /** Called when the navigation button is pressed on this object */
1213
1242
  navigatePressed() { this.click(); }
1214
1243
 
1215
- /** @return {boolean} - Is the mouse hovering over this element */
1244
+ /** Is the mouse hovering over this element
1245
+ * @return {boolean} */
1216
1246
  isHoverObject() { return uiSystem.hoverObject === this; }
1217
1247
 
1218
- /** @return {boolean} - Is the mouse held onto this element */
1248
+ /** Is the mouse held onto this element
1249
+ * @return {boolean} */
1219
1250
  isActiveObject() { return uiSystem.activeObject === this; }
1220
1251
 
1221
- /** @return {boolean} - Is the gamepad or keyboard navigation object */
1252
+ /** Is the gamepad or keyboard navigation object
1253
+ * @return {boolean} */
1222
1254
  isNavigationObject() { return uiSystem.navigationObject === this; }
1223
1255
 
1224
- /** @return {boolean} - Is this object in keyboard input mode */
1256
+ /** Is this object in keyboard input mode
1257
+ * @return {boolean} */
1225
1258
  isKeyInputObject() { return uiSystem.keyInputObject === this; }
1226
1259
 
1227
- /** @return {boolean} - Can it be interacted with, it and every parent visible and enabled */
1260
+ /** Can it be interacted with, it and every parent visible and enabled
1261
+ * @return {boolean} */
1228
1262
  isInteractive() { return this.interactive && uiObjectIsUsable(this); }
1229
1263
 
1230
1264
  /** Returns string containing info about this object for debugging
@@ -1371,7 +1405,7 @@ class UITextInput extends UIObject
1371
1405
 
1372
1406
  ASSERT(isStringLike(text), 'ui text must be a string');
1373
1407
 
1374
- /** @property {number} - Max length of input (0 = no limit) */
1408
+ /** @property {number} - Max length of input in characters as a reader counts them (0 = no limit) */
1375
1409
  this.maxLength = 0;
1376
1410
 
1377
1411
  // set properties, as a string, which typing adds to
@@ -1419,7 +1453,7 @@ class UITextInput extends UIObject
1419
1453
  return; // a key held when editing began repeats, it should not type or stop editing
1420
1454
  this.text += ''; // a game may have set a number
1421
1455
  if (key === 'Backspace')
1422
- this.text = [...this.text].slice(0, -1).join(''); // a whole character, an emoji is two code units
1456
+ this.text = textGraphemes(this.text).slice(0, -1).join(''); // a whole character, an emoji family too
1423
1457
  else if (key === 'Enter' || key === 'Escape')
1424
1458
  this.stopEditing();
1425
1459
  else if (key.length === 1) // printable characters
@@ -1427,7 +1461,7 @@ class UITextInput extends UIObject
1427
1461
  // ctrl and cmd shortcuts do not type, but AltGr reports ctrl and alt and types characters like @
1428
1462
  if ((e.ctrlKey || e.metaKey) && !e.getModifierState?.('AltGraph'))
1429
1463
  return;
1430
- if (!this.maxLength || this.text.length < this.maxLength)
1464
+ if (!this.maxLength || textGraphemes(this.text).length < this.maxLength)
1431
1465
  this.text += key;
1432
1466
  }
1433
1467
  }
@@ -1755,9 +1789,9 @@ class UIVideo extends UIObject
1755
1789
  * @param {Vector2} pos
1756
1790
  * @param {Vector2} size
1757
1791
  * @param {string} src - Video file path or URL
1758
- * @param {boolean} [autoplay=false] - Start playing immediately?
1759
- * @param {boolean} [loop=false] - Loop the video?
1760
- * @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
1792
+ * @param {boolean} [autoplay] - Start playing immediately?
1793
+ * @param {boolean} [loop] - Loop the video?
1794
+ * @param {number} [volume] - Volume percent scaled by global volume (0-1)
1761
1795
  */
1762
1796
  constructor(pos, size, src, autoplay=false, loop=false, volume=1)
1763
1797
  {
@@ -1912,10 +1946,10 @@ class UILayout extends UIObject
1912
1946
  {
1913
1947
  /** Create a UILayout container that auto-arranges children
1914
1948
  * @param {Vector2} [pos]
1915
- * @param {number} [columns=1] - Number of columns (1 = vertical list)
1916
- * @param {number} [gap=10] - Space between children
1917
- * @param {number} [padding=10] - Space between container border and children
1918
- * @param {boolean} [transparent=false] - If true, draws no background, outline, or shadow
1949
+ * @param {number} [columns] - Number of columns (1 = vertical list)
1950
+ * @param {number} [gap] - Space between children
1951
+ * @param {number} [padding] - Space between container border and children
1952
+ * @param {boolean} [transparent] - If true, draws no background, outline, or shadow
1919
1953
  */
1920
1954
  constructor(pos, columns=1, gap=10, padding=10, transparent=false)
1921
1955
  {
@@ -0,0 +1,229 @@
1
+ /**
2
+ * LittleJS Wavedash Plugin
3
+ * - The Wavedash twin of the Newgrounds plugin: achievements and leaderboards, the same shape so a game can switch
4
+ * - Wavedash serves the game's page and puts its SDK in window.Wavedash before the game runs, so nothing is bundled;
5
+ * off Wavedash (local, itch, GitHub Pages) there is none, and every call does nothing
6
+ * - Make the plugin when the game can draw, at the end of gameInit: Wavedash.init is called then, and until it is
7
+ * Wavedash keeps its loading screen over the game
8
+ * - WavedashMedal is a Medal with the identifier of its Wavedash achievement; on Wavedash an unlock is sent as that
9
+ * achievement and Wavedash shows its own toast in place of the engine's popup, off it the medal unlocks as any does
10
+ * - Wavedash refuses an achievement until it has loaded the player's, a moment after launch, so refused and earlier
11
+ * unlocks are sent again every two seconds until it takes them; medals unlocked before, in the save, are sent too
12
+ * - Leaderboards are made from code: give the plugin a table of them, each with how it sorts and shows its scores,
13
+ * and they are made at once, so each one exists from the first launch
14
+ * - The SDK checks its arguments' types and throws on a wrong one, so every call passes real booleans and whole
15
+ * numbers and is caught; a game built with its own minifier keeps the SDK's names, see REFERENCE
16
+ * @namespace Wavedash
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ /** Global Wavedash plugin object
22
+ * @type {WavedashPlugin}
23
+ * @memberof Wavedash */
24
+ let wavedash;
25
+
26
+ // Engine internal variables not exposed to documentation
27
+ const wavedashRetryMS = 2e3; // how long to wait before sending refused achievements again
28
+ const wavedashRetries = 30; // how many times one is sent before it is taken as never to be, about a minute
29
+ const wavedashTimeoutMS = 15e3; // how long a leaderboard call may take, as the Newgrounds plugin waits
30
+ const wavedashDisplayTypes = {number: 0, seconds: 1, milliseconds: 2, ticks: 3}; // the SDK's display types
31
+
32
+ // the SDK Wavedash put on the page, read at each call, undefined off Wavedash
33
+ const wavedashSDK = ()=> globalThis['Wavedash'];
34
+
35
+ // a call's answer, or undefined if it does not come in time or fails, with a warning; the time limit is let go of once
36
+ // the answer is in, so nothing waits on it after
37
+ function wavedashWait(name, answer)
38
+ {
39
+ let timer;
40
+ const limit = new Promise((resolve)=> timer = setTimeout(resolve, wavedashTimeoutMS));
41
+ return Promise.race([Promise.resolve(answer), limit])
42
+ .catch((error)=> { console.warn('Wavedash ' + name + ' failed: ' + error); })
43
+ .finally(()=> clearTimeout(timer));
44
+ }
45
+
46
+ ///////////////////////////////////////////////////////////////////////////////
47
+ /**
48
+ * WavedashMedal: a medal that is also a Wavedash achievement, unlocked on Wavedash by its identifier
49
+ * @extends Medal
50
+ * @memberof Wavedash
51
+ * @example
52
+ * const medal_finish = new WavedashMedal(0, 'ACH_01_FINISH', 'Finish', 'Finish a level');
53
+ * medal_finish.unlock(); // Wavedash's toast on Wavedash, the engine's popup anywhere else
54
+ */
55
+ class WavedashMedal extends Medal
56
+ {
57
+ /** Create a WavedashMedal and add it to the list of medals
58
+ * @param {number} id - The unique identifier of the medal, as for any Medal
59
+ * @param {string} achievement - The identifier of its Wavedash achievement, as made with the Wavedash CLI
60
+ * @param {string} name - Name of the medal
61
+ * @param {string} [description] - Description of the medal
62
+ * @param {string} [icon] - Icon for the medal
63
+ * @param {string} [src] - Image location for the medal
64
+ */
65
+ constructor(id, achievement, name, description, icon, src)
66
+ {
67
+ ASSERT(typeof achievement === 'string' && achievement !== '', 'WavedashMedal: achievement must be the identifier of a Wavedash achievement', achievement);
68
+ super(id, name, description, icon, src);
69
+
70
+ /** @property {string} - The identifier of its Wavedash achievement */
71
+ this.achievement = achievement;
72
+ }
73
+
74
+ /** Unlocks the medal if not already unlocked, saved as any medal is; on Wavedash its achievement is sent, until
75
+ * Wavedash takes it, and Wavedash shows its own toast in place of the engine's popup; one unlocked before the
76
+ * plugin is made is sent when it is
77
+ * @return {Promise<boolean>} - Whether the medal is unlocked */
78
+ unlock()
79
+ {
80
+ // off Wavedash, or testing medals with debugMedals, the engine's own popup and nothing sent
81
+ if (medalsPreventUnlock || this.unlocked || !wavedashSDK() || debugMedals)
82
+ return super.unlock();
83
+ this.unlocked = true;
84
+ medalsSave();
85
+ wavedash?.sendAchievements();
86
+ return Promise.resolve(true);
87
+ }
88
+ }
89
+
90
+ ///////////////////////////////////////////////////////////////////////////////
91
+ /**
92
+ * Wavedash plugin: starts the Wavedash SDK, sends achievements and posts and reads leaderboards
93
+ * @memberof Wavedash
94
+ * @example
95
+ * // at the end of gameInit, with the leaderboards the game posts to
96
+ * new WavedashPlugin({LEVEL_1: {lowerWins: true, display: 'milliseconds'}, HIGH_SCORE: {}});
97
+ * wavedash.postScore('LEVEL_1', timeMs);
98
+ */
99
+ class WavedashPlugin
100
+ {
101
+ /** Start the Wavedash SDK when the game is on Wavedash, and make its leaderboards
102
+ * @param {Object<string, {lowerWins?: boolean, display?: string}>} [leaderboards] - The game's leaderboards by
103
+ * name: lowerWins for times and golf, where a lower score is better, higher wins when left out; display how
104
+ * Wavedash shows a score, 'number', 'seconds', 'milliseconds' or 'ticks' (60 a second), 'number' when left out
105
+ */
106
+ constructor(leaderboards={})
107
+ {
108
+ ASSERT(!wavedash, 'WavedashPlugin already initialized');
109
+ ASSERT(!!leaderboards && typeof leaderboards === 'object', 'WavedashPlugin: leaderboards is an object of them by name');
110
+ wavedash = this;
111
+
112
+ /** @property {Object<string, Promise<string|undefined>>} - Each leaderboard's id by its name, once Wavedash
113
+ * has made or found it, undefined when it could not
114
+ * @type {Object<string, Promise<string|undefined>>} */
115
+ this.leaderboards = Object.create(null); // no inherited names, a board may be called constructor
116
+ /** @type {Object<string, {lowerWins?: boolean, display?: string}>} */
117
+ this.leaderboardSettings = leaderboards; // how each one sorts and shows its scores, read as its own entries
118
+ /** @type {Set<string>} */
119
+ this.achievementsSent = new Set; // the achievements Wavedash took this visit, or refused for good
120
+ /** @type {Object<string, number>} */
121
+ this.achievementTries = Object.create(null); // how many times each one not taken yet was sent
122
+ /** @type {number|undefined} */
123
+ this.achievementRetry = undefined; // the timer that sends refused ones again
124
+
125
+ // Wavedash keeps its loading screen until init, which is called once
126
+ wavedashWait('init', this.call('init'));
127
+ for (const name in leaderboards)
128
+ this.leaderboard(name);
129
+ this.sendAchievements(); // the medals the save already has, unlocked before or off Wavedash
130
+ }
131
+
132
+ /** Whether the game is on Wavedash, its SDK on the page
133
+ * @return {boolean} */
134
+ isActive() { return !!wavedashSDK(); }
135
+
136
+ /** Call the SDK, undefined off Wavedash; a call that throws, as on an argument of the wrong type, says so in
137
+ * the console and gives undefined
138
+ * @param {string} name - The SDK function
139
+ * @param {...*} args
140
+ * @return {*}
141
+ * @ignore */
142
+ call(name, ...args)
143
+ {
144
+ const sdk = wavedashSDK();
145
+ if (!sdk) return;
146
+ try { return sdk[name](...args); }
147
+ catch (error) { console.warn('Wavedash ' + name + ' failed: ' + error); }
148
+ }
149
+
150
+ /** A leaderboard's id by its name, made on Wavedash the first time it is asked for, with the settings it was given
151
+ * @param {string} name
152
+ * @return {Promise<string|undefined>}
153
+ * @ignore */
154
+ leaderboard(name)
155
+ {
156
+ ASSERT(typeof name === 'string' && name !== '', 'Wavedash: a leaderboard name must be a string', name);
157
+ if (this.leaderboards[name]) return this.leaderboards[name];
158
+ const settings = Object.prototype.hasOwnProperty.call(this.leaderboardSettings, name) ?
159
+ this.leaderboardSettings[name] : undefined; // its own entry, not an inherited one
160
+ const {lowerWins=false, display='number'} = settings || {};
161
+ const known = wavedashDisplayTypes.hasOwnProperty(display);
162
+ ASSERT(known, 'Wavedash: a leaderboard display is number, seconds, milliseconds or ticks', display);
163
+ const made = this.call('getOrCreateLeaderboard', name, lowerWins ? 0 : 1, known ? wavedashDisplayTypes[display] : 0);
164
+ const id = wavedashWait('leaderboard ' + name, made).then((result)=>
165
+ result?.['success'] ? result['data']?.['id'] : undefined);
166
+ // one that could not be made is asked for again next time
167
+ id.then((value)=> value === undefined && this.leaderboards[name] === id && delete this.leaderboards[name]);
168
+ return this.leaderboards[name] = id;
169
+ }
170
+
171
+ /** Post a score to a leaderboard, Wavedash keeping the player's best; off Wavedash it does nothing
172
+ * - A leaderboard not in the table given to the plugin is made the first time, higher wins and shown as a number
173
+ * @param {string} name - The leaderboard's name
174
+ * @param {number} score - A whole number, milliseconds for a time
175
+ * @return {Promise<boolean>} - Whether it was posted */
176
+ async postScore(name, score)
177
+ {
178
+ ASSERT(isNumber(score), 'Wavedash postScore: score must be a number', score);
179
+ if (!this.isActive()) return false;
180
+ const id = await this.leaderboard(name);
181
+ if (id === undefined) return false;
182
+ const result = await wavedashWait('postScore', this.call('uploadLeaderboardScore', id, round(score), true));
183
+ return !!result?.['success'];
184
+ }
185
+
186
+ /** Read a leaderboard's entries, undefined off Wavedash or when it could not be read
187
+ * @param {string} name - The leaderboard's name
188
+ * @param {number} [offset] - How many entries to skip over
189
+ * @param {number} [limit] - How many entries to read
190
+ * @param {boolean} [friendsOnly] - Only the player and their friends
191
+ * @return {Promise<Array<Object>|undefined>} - The entries, as Wavedash gives them */
192
+ async getScores(name, offset=0, limit=10, friendsOnly=false)
193
+ {
194
+ if (!this.isActive()) return;
195
+ const id = await this.leaderboard(name);
196
+ if (id === undefined) return;
197
+ const result = await wavedashWait('getScores',
198
+ this.call('listLeaderboardEntries', id, offset|0, limit|0, !!friendsOnly));
199
+ return result?.['success'] ? result['data'] : undefined;
200
+ }
201
+
202
+ /** Send every unlocked WavedashMedal's achievement Wavedash has not taken yet, again every two seconds while it
203
+ * refuses some, as it does until it has loaded the player's achievements; one refused for about a minute is
204
+ * taken as an identifier Wavedash does not have, said once in the console, and not sent again this visit
205
+ * @ignore */
206
+ sendAchievements()
207
+ {
208
+ if (!this.isActive() || debugMedals) return;
209
+ let refused = false;
210
+ medalsForEach((medal)=>
211
+ {
212
+ const achievement = medal instanceof WavedashMedal && medal.unlocked && medal.achievement;
213
+ if (!achievement || this.achievementsSent.has(achievement)) return;
214
+ const result = this.call('setAchievement', achievement, true);
215
+ if (result === true || result?.['success'] === true)
216
+ this.achievementsSent.add(achievement);
217
+ else if ((this.achievementTries[achievement] = (this.achievementTries[achievement] || 0) + 1) < wavedashRetries)
218
+ refused = true;
219
+ else
220
+ {
221
+ console.warn('Wavedash refused achievement ' + achievement + ' ' + wavedashRetries +
222
+ ' times; is it made, with that identifier?');
223
+ this.achievementsSent.add(achievement); // not sent again this visit
224
+ }
225
+ });
226
+ clearTimeout(this.achievementRetry);
227
+ this.achievementRetry = refused ? setTimeout(()=> this.sendAchievements(), wavedashRetryMS) : undefined;
228
+ }
229
+ }