littlejsengine 1.19.3 → 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 (50) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2455 -935
  4. package/dist/littlejs.esm.js +30344 -21110
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +30259 -21052
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +26567 -20455
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  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 -332
  16. package/plugins/math3d.js +58 -17
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +15 -0
  21. package/plugins/postProcess.js +56 -13
  22. package/plugins/render3d.js +1643 -1577
  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 +496 -458
  27. package/plugins/threejs.js +183 -163
  28. package/plugins/tweakables.js +411 -0
  29. package/plugins/tweenSystem.js +263 -145
  30. package/plugins/uiSystem.js +2024 -1687
  31. package/plugins/zzfxm.js +159 -159
  32. package/src/engine.js +191 -75
  33. package/src/engineAudio.js +229 -125
  34. package/src/engineBuild.mjs +289 -256
  35. package/src/engineDebug.js +912 -742
  36. package/src/engineDraw.js +295 -193
  37. package/src/engineEditor.js +2431 -0
  38. package/src/engineExport.js +12 -0
  39. package/src/engineInput.js +1462 -1293
  40. package/src/engineMath.js +1274 -1252
  41. package/src/engineObject.js +194 -80
  42. package/src/engineParticles.js +583 -532
  43. package/src/engineRelease.js +67 -45
  44. package/src/engineSettings.js +77 -29
  45. package/src/engineTileLayer.js +995 -741
  46. package/src/engineUtilities.js +19 -13
  47. package/src/engineWebGL.js +239 -82
  48. package/src/jsconfig.json +17 -13
  49. package/plugins/box2d.wasm.js +0 -630
  50. package/plugins/box2d.wasm.wasm +0 -0
@@ -1,199 +1,400 @@
1
- /**
2
- * LittleJS Newgrounds Plugin
3
- * - NewgroundsMedal extends Medal with Newgrounds API functionality
4
- * - Call new NewgroundsPlugin(app_id) to setup Newgrounds
5
- * - Uses CryptoJS for encryption if optional cipher is provided
6
- * - provides functions to interact with medals scoreboards
7
- * - Keeps connection alive and logs views
8
- * @namespace Newgrounds
9
- */
10
-
11
- 'use strict';
12
-
13
- /** Global Newgrounds object
14
- * @type {NewgroundsPlugin}
15
- * @memberof Newgrounds */
16
- let newgrounds;
17
-
18
- ///////////////////////////////////////////////////////////////////////////////
19
- /**
20
- * Newgrounds medal auto unlocks in newgrounds API
21
- * @extends Medal
22
- * @memberof Newgrounds
23
- */
24
- class NewgroundsMedal extends Medal
25
- {
26
- /** Create a newgrounds medal object and adds it to the list of medals
27
- * @param {number} id - The unique identifier of the medal
28
- * @param {string} name - Name of the medal
29
- * @param {string} [description] - Description of the medal
30
- * @param {string} [icon] - Icon for the medal
31
- * @param {string} [src] - Image location for the medal
32
- */
33
- constructor(id, name, description, icon, src)
34
- { super(id, name, description, icon, src); }
35
-
36
- /** Unlocks a medal if not already unlocked */
37
- unlock()
38
- {
39
- super.unlock();
40
- newgrounds && newgrounds.unlockMedal(this.id);
41
- }
42
- }
43
-
44
- ///////////////////////////////////////////////////////////////////////////////
45
- /**
46
- * Newgrounds API object
47
- * @memberof Newgrounds
48
- */
49
- class NewgroundsPlugin
50
- {
51
- /** Create the global newgrounds object
52
- * @param {string} app_id - The newgrounds App ID
53
- * @param {string} [cipher] - The encryption Key (AES-128/Base64)
54
- * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher
55
- * @example
56
- * // create the newgrounds object, replace the app id with your own
57
- * const app_id = 'your_app_id_here';
58
- * new NewgroundsPlugin(app_id);
59
- */
60
- constructor(app_id, cipher, cryptoJS)
61
- {
62
- ASSERT(!newgrounds, 'there can only be one newgrounds object');
63
- ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
64
-
65
- newgrounds = this; // set global newgrounds object
66
- /** @property {string} - The newgrounds App ID */
67
- this.app_id = app_id;
68
- /** @property {string|undefined} - AES-128/Base64 encryption key, if any */
69
- this.cipher = cipher;
70
- /** @property {Object|undefined} - CryptoJS instance used when cipher is set */
71
- this.cryptoJS = cryptoJS;
72
- /** @property {string} - Hostname used when logging views */
73
- this.host = location ? location.hostname : '';
74
-
75
- // get session id from url search params
76
- const url = new URL(location.href);
77
- /** @property {string|null} - Newgrounds session id from the URL (null when not logged in) */
78
- this.session_id = url.searchParams.get('ngio_session_id');
79
-
80
- if (!this.session_id)
81
- return; // only use newgrounds when logged in
82
-
83
- // get medals
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
-
97
- /** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
98
- this.medals = medalsResult.result.data?.['medals'] || [];
99
- debugMedals && LOG(this.medals);
100
- for (const newgroundsMedal of this.medals)
101
- {
102
- const medal = medals[newgroundsMedal['id']];
103
- if (medal)
104
- {
105
- // copy newgrounds medal data
106
- medal.image = new Image;
107
- medal.image.src = newgroundsMedal['icon'];
108
- medal.name = newgroundsMedal['name'];
109
- medal.description = newgroundsMedal['description'];
110
- medal.unlocked = newgroundsMedal['unlocked'];
111
- medal.difficulty = newgroundsMedal['difficulty'];
112
- medal.value = newgroundsMedal['value'];
113
-
114
- if (medal.value) // add value to description
115
- medal.description = medal.description + ` (${ medal.value })`;
116
- }
117
- }
118
-
119
- // get scoreboards
120
- const scoreboardResult = this.call('ScoreBoard.getBoards');
121
- /** @property {Array} - Scoreboards fetched from Newgrounds */
122
- this.scoreboards = scoreboardResult?.result?.data?.scoreboards || [];
123
- debugMedals && LOG(this.scoreboards);
124
-
125
- // keep the session alive with a ping every minute
126
- const keepAliveMS = 60 * 1e3;
127
- setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS);
128
- }
129
-
130
- /** Send message to unlock a medal by id
131
- * @param {number} id - The medal id */
132
- unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); }
133
-
134
- /** Send message to post score
135
- * @param {number} id - The scoreboard id
136
- * @param {number} value - The score value */
137
- postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); }
138
-
139
- /** Get scores from a scoreboard
140
- * @param {number} id - The scoreboard id
141
- * @param {string} [user] - A user's id or name
142
- * @param {number} [social] - If true, only social scores will be loaded
143
- * @param {number} [skip] - Number of scores to skip over
144
- * @param {number} [limit] - Number of scores to include in the list
145
- * @return {Object} - The response JSON object
146
- */
147
- getScores(id, user, social=0, skip=0, limit=10)
148
- { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); }
149
-
150
- /** Send message to log a view */
151
- logView() { return this.call('App.logView', {'host':this.host}, true); }
152
-
153
- /** Send a message to call a component of the Newgrounds API
154
- * @param {string} component - Name of the component
155
- * @param {Object} [parameters] - Parameters to use for call
156
- * @param {boolean} [async] - If true, don't wait for response before continuing
157
- * @return {Object} - The response JSON object
158
- */
159
- call(component, parameters, async=false)
160
- {
161
- const call = {'component':component, 'parameters':parameters};
162
- if (this.cipher)
163
- {
164
- // encrypt using AES-128 Base64 with cryptoJS
165
- const cryptoJS = this.cryptoJS;
166
- const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher);
167
- const iv = cryptoJS['lib']['WordArray']['random'](16);
168
- const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv});
169
- call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext']));
170
- call['parameters'] = 0;
171
- }
172
-
173
- // build the input object
174
- const input =
175
- {
176
- 'app_id': this.app_id,
177
- 'session_id': this.session_id,
178
- 'call': call
179
- };
180
-
181
- // build post data
182
- const formData = new FormData();
183
- formData.append('input', JSON.stringify(input));
184
-
185
- // send post data
186
- const xmlHttp = new XMLHttpRequest();
187
- const url = 'https://newgrounds.io/gateway_v3.php';
188
- xmlHttp.open('POST', url, !debugMedals && async);
189
- try { xmlHttp.send(formData); }
190
- catch(e)
191
- {
192
- debugMedals && LOG('newgrounds call failed', e);
193
- return;
194
- }
195
- debugMedals && LOG(xmlHttp.responseText);
196
- try { return xmlHttp.responseText && JSON.parse(xmlHttp.responseText); }
197
- catch(e) { debugMedals && LOG('newgrounds response is not valid JSON', e); }
198
- }
199
- }
1
+ /**
2
+ * LittleJS Newgrounds Plugin
3
+ * - NewgroundsMedal extends Medal with Newgrounds API functionality
4
+ * - When logged in, Newgrounds holds the player's NewgroundsMedals: they unlock once the server confirms and the local save leaves them alone
5
+ * - A plain Medal is never touched, so a game can use the plugin for scoreboards alone
6
+ * - Without a session the medal and scoreboard lists still come in, so the medals get their names and icons; unlocking on the server and posting scores need a logged in player
7
+ * - Create the medals as NewgroundsMedals with their ids on Newgrounds, then new NewgroundsPlugin(app_id, cipher); medalsInit is still needed, before or after
8
+ * - Encrypts medal unlocks and posted scores, the calls Newgrounds secures, with the browser's own WebCrypto when the app has a cipher, no library needed
9
+ * - Logs a view when it starts, and provides functions to unlock medals and to post and read scoreboards
10
+ * - Tells the Newgrounds page around the game when a medal unlocks or a score posts, as the official client does
11
+ * - Checks the session every minute when logged in, which keeps it alive, and plays as not logged in once it is lost
12
+ * - Where the browser has AbortSignal.timeout, a request that takes longer than 15 seconds fails like one that could not reach the server
13
+ * - Every call is a fetch, so the functions return promises; await newgrounds.ready for the medals and scoreboards
14
+ * @namespace Newgrounds
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ /** Global Newgrounds object
20
+ * @type {NewgroundsPlugin}
21
+ * @memberof Newgrounds */
22
+ let newgrounds;
23
+
24
+ // Engine internal variables not exposed to documentation
25
+ const newgroundsUnlocksToResend = new Set; // pending medals whose request did not reach the server
26
+ const newgroundsUnlocksRefused = new Set; // medals the server refused this visit, asked again they answer no unsent
27
+ const newgroundsSecureComponents = ['Medal.unlock', 'ScoreBoard.postScore']; // the calls encrypted with a cipher
28
+ const newgroundsSessionErrors = [104, 110, 111]; // expired session, login required, session cancelled
29
+ const newgroundsTimeoutMS = 15e3; // how long a request may take before it fails
30
+
31
+ // whether the server answered that the session is gone, as opposed to a request that failed on the way
32
+ function newgroundsSessionLost(response)
33
+ { return newgroundsSessionErrors.includes(response?.result?.data?.error?.code ?? response?.error?.code); }
34
+
35
+ // tell the Newgrounds page around the game, with the message the official client sends
36
+ function newgroundsNotifyPage(component, id)
37
+ { globalThis.top?.postMessage(JSON.stringify({'ngioComponent':component, 'id':id}), '*'); }
38
+
39
+ ///////////////////////////////////////////////////////////////////////////////
40
+ /**
41
+ * NewgroundsMedal: its id is the medal's id on the Newgrounds API Tools page; when logged in it only unlocks once the server confirms
42
+ * @extends Medal
43
+ * @memberof Newgrounds
44
+ */
45
+ class NewgroundsMedal extends Medal
46
+ {
47
+ /** Create a NewgroundsMedal and add it to the list of medals
48
+ * @param {number} id - The unique identifier of the medal
49
+ * @param {string} name - Name of the medal
50
+ * @param {string} [description] - Description of the medal
51
+ * @param {string} [icon] - Icon for the medal
52
+ * @param {string} [src] - Image location for the medal
53
+ */
54
+ constructor(id, name, description, icon, src)
55
+ {
56
+ super(id, name, description, icon, src);
57
+
58
+ /** @property {number|undefined} - Difficulty from the server once ready, 1 easy to 5 brutal
59
+ * @type {number|undefined} */
60
+ this.difficulty = undefined;
61
+
62
+ /** @property {number|undefined} - Point value from the server once ready
63
+ * @type {number|undefined} */
64
+ this.value = undefined;
65
+ }
66
+
67
+ /** Whether the local save holds this medal, false while logged in when Newgrounds holds it
68
+ * @return {boolean} */
69
+ isLocal() { return !newgrounds?.session_id; }
70
+
71
+ /** Unlocks a medal if not already unlocked, once Newgrounds confirms it when logged in
72
+ * - The promise is optional, for when a game wants to know the outcome
73
+ * - A request that did not reach the server is sent again after the session check every minute, and calling
74
+ * unlock again while the medal is pending returns the same promise
75
+ * - One the server refused is not sent again this visit, calling unlock again resolves false without a request
76
+ * - An answer that the session is gone makes the game play as not logged in, and the pending medals, this one too,
77
+ * unlock locally unless unlocks are prevented; a refused one only unlocks if it is earned again
78
+ * @return {Promise<boolean>} - Whether the medal is unlocked, once the server has answered when logged in */
79
+ unlock()
80
+ {
81
+ if (medalsPreventUnlock || this.unlocked || this.isLocal())
82
+ return super.unlock(); // nothing to send, or not logged in and the local save holds the medal
83
+
84
+ // logged in, Newgrounds holds the medal: it unlocks once the server confirms, one request at a time
85
+ ASSERT(medalsSaveName, 'save name must be set');
86
+ if (newgroundsUnlocksRefused.has(this))
87
+ return Promise.resolve(false); // refused this visit, asking again will not change that
88
+ const pending = newgrounds.pendingUnlocks;
89
+ if (pending.has(this))
90
+ return pending.get(this);
91
+ const request = newgrounds.unlockMedal(this.id).then(response=>
92
+ {
93
+ if (newgroundsSessionLost(response))
94
+ newgrounds.dropSession(); // the pending unlocks, this one too, are local now
95
+ const serverMedal = response?.result?.data?.medal;
96
+ if (!serverMedal?.unlocked || medalsPreventUnlock)
97
+ {
98
+ debugMedals && LOG('Newgrounds did not unlock medal', this.id, response?.result?.data?.error || response?.error);
99
+ if (this.isLocal())
100
+ return this.unlocked; // the session dropped, the medal is local now
101
+ if (!response || serverMedal?.unlocked)
102
+ newgroundsUnlocksToResend.add(this); // did not reach the server, or confirmed while prevented: pending
103
+ else
104
+ {
105
+ // refused, which will not change: no longer pending, so a session drop does not unlock it
106
+ pending.delete(this);
107
+ newgroundsUnlocksRefused.add(this);
108
+ }
109
+ return false;
110
+ }
111
+ const listed = newgrounds.medals.find(m=> m['id'] == this.id);
112
+ listed && Object.assign(listed, serverMedal); // keep the fetched list in step
113
+ if (serverMedal['icon'] && serverMedal['icon'] != this.image?.src)
114
+ (this.image = new Image).src = serverMedal['icon']; // a secret medal shows its real icon once unlocked
115
+ pending.delete(this);
116
+ newgroundsNotifyPage('Medal.unlock', this.id);
117
+ return super.unlock();
118
+ });
119
+ pending.set(this, request);
120
+ return request;
121
+ }
122
+ }
123
+
124
+ ///////////////////////////////////////////////////////////////////////////////
125
+ /**
126
+ * Newgrounds API object
127
+ * @memberof Newgrounds
128
+ */
129
+ class NewgroundsPlugin
130
+ {
131
+ /** Create the global newgrounds object
132
+ * - Logs a view right away, for a guest and a logged in player alike, so a game does not have to
133
+ * - Create the medals first: they take their name and icon from the server once it answers, and when logged in they are locked here until it does
134
+ * - Call medalsInit too, before or after: an unlock asserts without it, and it keeps the medals while not logged in
135
+ * @param {string} app_id - The Newgrounds App ID
136
+ * @param {string} [cipher] - The encryption key from the app's settings, AES-128 as Base64; medal unlocks and posted
137
+ * scores are encrypted with the browser's WebCrypto, which needs a secure page, https or localhost
138
+ * @example
139
+ * // create the newgrounds object, replace the app id with your own
140
+ * const app_id = 'your_app_id_here';
141
+ * new NewgroundsPlugin(app_id);
142
+ */
143
+ constructor(app_id, cipher)
144
+ {
145
+ ASSERT(!newgrounds, 'there can only be one newgrounds object');
146
+ ASSERT(!cipher || typeof crypto != 'undefined' && crypto.subtle, 'a cipher needs WebCrypto, which the browser only has on a secure page');
147
+ ASSERT(!cipher || /^[A-Za-z0-9+/]{22}==$/.test(cipher), 'the cipher must be the Base64 AES-128 key from the app settings');
148
+
149
+ newgrounds = this; // set global newgrounds object
150
+ /** @property {string} - The Newgrounds App ID */
151
+ this.app_id = app_id;
152
+ /** @property {string|undefined} - AES-128/Base64 encryption key, if any
153
+ * @type {string|undefined} */
154
+ this.cipher = cipher;
155
+ /** @type {CryptoKey|undefined} */
156
+ this.cryptoKey = undefined; // the cipher imported for WebCrypto, on the first encrypted call
157
+ const hasLocation = typeof location != 'undefined';
158
+ /** @property {string} - Hostname sent with the view the plugin logs when it starts */
159
+ this.host = hasLocation ? location.hostname : '';
160
+ /** @property {Array} - Medals fetched from Newgrounds, empty until ready, with the unlocks only when logged in */
161
+ this.medals = [];
162
+ /** @property {Array} - Scoreboards fetched from Newgrounds, empty until ready */
163
+ this.scoreboards = [];
164
+ /** @property {{id: number, name: string, url: string, supporter: boolean}|null} - The logged in player once ready, null when not logged in
165
+ * @type {{id: number, name: string, url: string, supporter: boolean}|null} */
166
+ this.user = null;
167
+
168
+ /** @property {Map<NewgroundsMedal, Promise<boolean>>} - Medals whose unlock is in flight or waiting to be resent, with their request's promise; one the server refused leaves it
169
+ * @type {Map<NewgroundsMedal, Promise<boolean>>} */
170
+ this.pendingUnlocks = new Map;
171
+
172
+ // get session id from url search params
173
+ /** @property {string|null} - Newgrounds session id from the URL, null when not logged in or once the session is lost
174
+ * @type {string|null} */
175
+ this.session_id = hasLocation ? new URL(location.href).searchParams.get('ngio_session_id') : null;
176
+ // Newgrounds holds this player's NewgroundsMedals: locked until the server says otherwise, the local save leaves them alone
177
+ if (this.session_id)
178
+ medalsForEach(medal=> medal instanceof NewgroundsMedal && (medal.unlocked = false));
179
+
180
+ /** @property {Promise<NewgroundsPlugin>} - Resolves once the session is checked and the lists are in, empty if the server could not be reached */
181
+ this.ready = this.init();
182
+ }
183
+
184
+ /** @deprecated since 1.20, the view is logged when the plugin starts, so this does nothing */
185
+ logView() {}
186
+
187
+ /** Log the view, check the session, fetch the medals and scoreboards, then keep the session alive; the constructor runs it once
188
+ * @private */
189
+ async init()
190
+ {
191
+ this.call('App.logView', {'host':this.host}); // every view counts, guest or logged in
192
+
193
+ let medalList;
194
+ if (this.session_id)
195
+ {
196
+ // the player is logged in when the server knows the session, it has a user and the medals come in
197
+ const sessionResult = await this.call('App.checkSession');
198
+ const session = sessionResult?.result?.data?.['session'];
199
+ const user = session && !session['expired'] && session['user'];
200
+ medalList = user && (await this.call('Medal.getList'))?.result?.data?.['medals'];
201
+ if (medalList && this.session_id)
202
+ this.user = user;
203
+ else
204
+ {
205
+ this.dropSession(); // without the server (offline / bad session / server error), or lost meanwhile
206
+ medalList = undefined; // its unlocks belong to the lost session
207
+ }
208
+ }
209
+
210
+ // not logged in, the list comes too, without the unlocks
211
+ medalList = medalList || (await this.call('Medal.getList'))?.result?.data?.['medals'];
212
+ this.medals = medalList || [];
213
+ debugMedals && LOG(this.medals);
214
+ for (const newgroundsMedal of this.medals)
215
+ {
216
+ const medal = medals[newgroundsMedal['id']];
217
+ if (medal instanceof NewgroundsMedal) // a plain medal with the same id is left alone
218
+ {
219
+ // copy the server's medal data
220
+ medal.image = new Image;
221
+ medal.image.src = newgroundsMedal['icon'];
222
+ medal.name = newgroundsMedal['name'];
223
+ medal.description = newgroundsMedal['description'];
224
+ medal.unlocked = medal.unlocked || !!newgroundsMedal['unlocked']; // keeps a local unlock, or one that landed first
225
+ medal.difficulty = newgroundsMedal['difficulty'];
226
+ medal.value = newgroundsMedal['value'];
227
+ if (medal.value) // add value to description
228
+ medal.description += ` (${ medal.value })`;
229
+
230
+ if (this.session_id && medal.unlocked)
231
+ {
232
+ newgroundsMedal['unlocked'] = true; // the list says so too
233
+ this.pendingUnlocks.delete(medal); // and a request waiting to be resent, or refused, is done
234
+ newgroundsUnlocksToResend.delete(medal);
235
+ newgroundsUnlocksRefused.delete(medal);
236
+ }
237
+ }
238
+ }
239
+
240
+ const scoreboardResult = await this.call('ScoreBoard.getBoards');
241
+ this.scoreboards = scoreboardResult?.result?.data?.scoreboards || [];
242
+ debugMedals && LOG(this.scoreboards);
243
+ if (!this.session_id)
244
+ return this;
245
+
246
+ // logged in, check the session every minute, which keeps it alive, and resend the unlocks that did not reach the server
247
+ const keepAlive = setInterval(async ()=>
248
+ {
249
+ if (!this.session_id)
250
+ return clearInterval(keepAlive);
251
+ const response = await this.call('App.checkSession');
252
+ const session = response?.result?.data?.['session'];
253
+ if (newgroundsSessionLost(response) || session && (session['expired'] || !session['user']))
254
+ return this.dropSession();
255
+ this.resendUnlocks();
256
+ }, 60e3);
257
+ return this;
258
+ }
259
+
260
+ /** Play as not logged in from now on: the NewgroundsMedals come back from the local save, keeping the unlocks the
261
+ * server confirmed meanwhile, and the unlocks still pending unlock locally; a refused one only if it is earned again
262
+ * - internal, the medals call it too when the server says the session is gone
263
+ * @ignore */
264
+ dropSession()
265
+ {
266
+ if (!this.session_id) return;
267
+ debugMedals && LOG('Newgrounds session unavailable; medals are local');
268
+ const confirmed = Object.values(medals).filter(medal=> medal.unlocked);
269
+ this.session_id = null;
270
+ this.user = null;
271
+ medalsLoad(); // the NewgroundsMedals are local again, back from the save
272
+ confirmed.forEach(medal=> medal.unlocked = true);
273
+ medalsSave();
274
+
275
+ // the unlocks still out are local too, they unlock now, or are dropped like any local unlock while prevented
276
+ const pending = [...this.pendingUnlocks.keys()];
277
+ this.pendingUnlocks.clear();
278
+ newgroundsUnlocksToResend.clear();
279
+ newgroundsUnlocksRefused.clear();
280
+ for (const medal of pending)
281
+ medal.unlock();
282
+ }
283
+
284
+ /** Send the unlocks whose request did not reach the server again, which the session check does every minute
285
+ * - A request still out is left to answer, and while unlocks are prevented they wait */
286
+ resendUnlocks()
287
+ {
288
+ if (medalsPreventUnlock) return;
289
+ for (const medal of newgroundsUnlocksToResend)
290
+ {
291
+ this.pendingUnlocks.delete(medal);
292
+ medal.unlock(); // a failure is only added back once the new request answers
293
+ }
294
+ newgroundsUnlocksToResend.clear();
295
+ }
296
+
297
+ /** Send a request to unlock a medal by id, the local medal is not changed; NewgroundsMedal.unlock sends this and waits for the answer
298
+ * @param {number} id - The medal id
299
+ * @return {Promise<Object>} - The response JSON object, undefined when the call failed */
300
+ unlockMedal(id) { return this.call('Medal.unlock', {'id':id}); }
301
+
302
+ /** Send message to post score
303
+ * @param {number} id - The scoreboard id
304
+ * @param {number} value - The score value, a whole number
305
+ * @return {Promise<Object>} - The response JSON object, undefined when the call failed; result.data.success says whether
306
+ * it posted, which needs a logged in player; an answer that the session is gone makes the game play as not logged
307
+ * in, and one that timed out may still have posted */
308
+ postScore(id, value)
309
+ {
310
+ return this.call('ScoreBoard.postScore', {'id':id, 'value':value}).then(response=>
311
+ {
312
+ response?.result?.data?.success && newgroundsNotifyPage('ScoreBoard.postScore', id);
313
+ newgroundsSessionLost(response) && this.dropSession();
314
+ return response;
315
+ });
316
+ }
317
+
318
+ /** Get scores from a scoreboard
319
+ * @param {number} id - The scoreboard id
320
+ * @param {string|number} [user] - A user's id or name, to load only their scores
321
+ * @param {boolean} [social] - If true, only the scores of the user and their friends, the logged in player when user is left out
322
+ * @param {number} [skip] - Number of scores to skip over
323
+ * @param {number} [limit] - Number of scores to include in the list
324
+ * @param {string} [period] - 'D' today, which the server assumes when left out, 'W' this week, 'M' this month, 'Y' this year or 'A' all time
325
+ * @return {Promise<Object>} - The response JSON object, undefined when the call failed; the scores are in
326
+ * result.data.scores, each with user.name, value and formatted_value; without a user or social it is the whole board
327
+ */
328
+ getScores(id, user, social=false, skip=0, limit=10, period)
329
+ {
330
+ // the whole board goes without the session, which the server would narrow down to the logged in player
331
+ const session_id = user || social ? this.session_id : null;
332
+ const parameters = {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit, 'period':period};
333
+ return this.call('ScoreBoard.getScores', parameters, session_id);
334
+ }
335
+
336
+ /** Encrypt text the way the Newgrounds gateway expects, AES-128 CBC with a random iv in front, as Base64
337
+ * @param {string} text
338
+ * @return {Promise<string>} */
339
+ async encrypt(text)
340
+ {
341
+ if (!this.cryptoKey)
342
+ {
343
+ const keyBytes = Uint8Array.from(atob(this.cipher), c=> c.charCodeAt(0));
344
+ this.cryptoKey = await crypto.subtle.importKey('raw', keyBytes, 'AES-CBC', false, ['encrypt']);
345
+ }
346
+ const iv = crypto.getRandomValues(new Uint8Array(16));
347
+ const encrypted = new Uint8Array(await crypto.subtle.encrypt({'name':'AES-CBC', iv}, this.cryptoKey, new TextEncoder().encode(text)));
348
+ const bytes = new Uint8Array(iv.length + encrypted.length);
349
+ bytes.set(iv);
350
+ bytes.set(encrypted, iv.length);
351
+ let binary = '';
352
+ for (const b of bytes)
353
+ binary += String.fromCharCode(b);
354
+ return btoa(binary);
355
+ }
356
+
357
+ /** Send a message to call a component of the Newgrounds API
358
+ * @param {string} component - Name of the component
359
+ * @param {Object} [parameters] - Parameters to use for call
360
+ * @param {string|null} [session_id] - The session to send, the player's by default
361
+ * @return {Promise<Object>} - The response JSON object, undefined when the call failed or took over 15 seconds;
362
+ * a component's own success and error are in result.data, and a cipher that is not a key gives error 201
363
+ */
364
+ async call(component, parameters, session_id=this.session_id)
365
+ {
366
+ const url = 'https://www.newgrounds.io/gateway_v3.php';
367
+ try
368
+ {
369
+ /** @type {Object} */
370
+ let execute = {'component':component, 'parameters':parameters};
371
+ if (this.cipher && newgroundsSecureComponents.includes(component))
372
+ {
373
+ // only the encrypted call goes; a key that will not even import is refused here the way the server
374
+ // refuses a wrong one, since sending again cannot fix it
375
+ const secure = await this.encrypt(JSON.stringify(execute)).catch(e=> debugMedals && LOG('Newgrounds cipher failed', e));
376
+ if (!secure)
377
+ return {'success':false, 'error':{'code':201, 'message':'Invalid Encryption: the cipher is not a Base64 AES-128 key'}};
378
+ execute = {'secure': secure};
379
+ }
380
+
381
+ // build the request object, in the form the Newgrounds.io docs give
382
+ const request =
383
+ {
384
+ 'app_id': this.app_id,
385
+ 'session_id': session_id,
386
+ 'execute': execute
387
+ };
388
+
389
+ // send it as post data
390
+ const formData = new FormData();
391
+ formData.append('request', JSON.stringify(request));
392
+ const signal = globalThis.AbortSignal?.timeout?.(newgroundsTimeoutMS); // a stalled request fails
393
+ const response = await fetch(url, {'method':'POST', 'body':formData, 'signal':signal});
394
+ const text = await response.text();
395
+ debugMedals && LOG(text);
396
+ return text ? JSON.parse(text) : undefined;
397
+ }
398
+ catch(e) { debugMedals && LOG('newgrounds call failed', e); }
399
+ }
400
+ }