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.
- package/CHANGELOG.md +123 -0
- package/EDITOR.md +2 -2
- package/FAQ.md +55 -11
- package/README.md +6 -2
- package/REFERENCE.md +203 -57
- package/dist/littlejs.d.ts +933 -313
- package/dist/littlejs.esm.js +5143 -2419
- package/dist/littlejs.esm.min.js +38 -24
- package/dist/littlejs.js +5132 -2416
- package/dist/littlejs.min.js +37 -23
- package/dist/littlejs.release.js +4905 -2232
- package/package.json +4 -4
- package/plugins/box2d.js +141 -56
- package/plugins/drawUtilities.js +374 -380
- package/plugins/gltf.js +383 -60
- package/plugins/lightSystem.js +412 -30
- package/plugins/math3d.js +1015 -1014
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +5 -6
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +47 -14
- package/plugins/pathFinder.js +60 -38
- package/plugins/pluginExport.js +11 -0
- package/plugins/postProcess.js +200 -67
- package/plugins/render3d.js +493 -2391
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +84 -9
- package/plugins/render3dExtras.js +122 -33
- package/plugins/render3dLevel.js +67 -8
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +117 -87
- package/plugins/uiSystem.js +62 -28
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +192 -40
- package/src/engineAudio.js +47 -27
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +23 -162
- package/src/engineDraw.js +148 -43
- package/src/engineEditor.js +153 -40
- package/src/engineExport.js +0 -3
- package/src/engineInput.js +31 -25
- package/src/engineMath.js +38 -20
- package/src/engineObject.js +68 -50
- package/src/engineParticles.js +66 -18
- package/src/engineRelease.js +18 -15
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +44 -18
- package/src/engineUtilities.js +56 -11
- package/src/engineWebGL.js +68 -22
package/plugins/uiSystem.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 + '')
|
|
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
|
-
/**
|
|
1244
|
+
/** Is the mouse hovering over this element
|
|
1245
|
+
* @return {boolean} */
|
|
1216
1246
|
isHoverObject() { return uiSystem.hoverObject === this; }
|
|
1217
1247
|
|
|
1218
|
-
/**
|
|
1248
|
+
/** Is the mouse held onto this element
|
|
1249
|
+
* @return {boolean} */
|
|
1219
1250
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
1220
1251
|
|
|
1221
|
-
/**
|
|
1252
|
+
/** Is the gamepad or keyboard navigation object
|
|
1253
|
+
* @return {boolean} */
|
|
1222
1254
|
isNavigationObject() { return uiSystem.navigationObject === this; }
|
|
1223
1255
|
|
|
1224
|
-
/**
|
|
1256
|
+
/** Is this object in keyboard input mode
|
|
1257
|
+
* @return {boolean} */
|
|
1225
1258
|
isKeyInputObject() { return uiSystem.keyInputObject === this; }
|
|
1226
1259
|
|
|
1227
|
-
/**
|
|
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 =
|
|
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
|
|
1759
|
-
* @param {boolean} [loop
|
|
1760
|
-
* @param {number} [volume
|
|
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
|
|
1916
|
-
* @param {number} [gap
|
|
1917
|
-
* @param {number} [padding
|
|
1918
|
-
* @param {boolean} [transparent
|
|
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
|
+
}
|