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.
- package/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2455 -935
- package/dist/littlejs.esm.js +30344 -21110
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +30259 -21052
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +26567 -20455
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +58 -17
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +15 -0
- package/plugins/postProcess.js +56 -13
- package/plugins/render3d.js +1643 -1577
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +263 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +191 -75
- package/src/engineAudio.js +229 -125
- package/src/engineBuild.mjs +289 -256
- package/src/engineDebug.js +912 -742
- package/src/engineDraw.js +295 -193
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +12 -0
- package/src/engineInput.js +1462 -1293
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +194 -80
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +77 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/plugins/newgrounds.js
CHANGED
|
@@ -1,199 +1,400 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS Newgrounds Plugin
|
|
3
|
-
* - NewgroundsMedal extends Medal with Newgrounds API functionality
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
* @param {string}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
{
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|