@indiegems/gem-web-sdk 13.2.2 → 13.3.1
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 +19 -0
- package/dist/function.d.ts +2 -2
- package/dist/game.d.ts +4 -2
- package/dist/gem-web-sdk.bridge.js +2 -1
- package/dist/gem-web-sdk.esm.js +3 -2
- package/dist/gem-web-sdk.function.js +3 -2
- package/dist/gem-web-sdk.game.js +3 -2
- package/dist/gem-web-sdk.min.js +3 -2
- package/dist/gem-web-sdk.sdk.js +3 -2
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +6 -2
- package/dist/sdk.d.ts +4 -2
- package/dist/server.d.ts +2 -2
- package/docs/api/api-reference-function.md +1 -1
- package/docs/api/api-reference-game.md +36 -3
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +19 -3
- package/docs/arcade-economy.md +18 -0
- package/docs/changelog.md +19 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +10 -10
- package/docs/player-text-fields.json +219 -0
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +130 -9
- package/package.json +2 -1
- package/schema/bridge.v1.json +25 -4
package/docs/manifest.json
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"file": "arcade-economy.md",
|
|
26
|
-
"sha256": "
|
|
27
|
-
"updated": "2026-10-
|
|
26
|
+
"sha256": "338dcdfe4aaa90241f7132677a0135d0ec8e32336f4302413dc0f11391b37623",
|
|
27
|
+
"updated": "2026-10-15T23:20:43Z"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"file": "async-opponents.md",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"file": "changelog.md",
|
|
41
|
-
"sha256": "
|
|
42
|
-
"updated": "2026-10-
|
|
41
|
+
"sha256": "1f95ccc1c4e84075e6f8a7a7d36403593196c3d7bc271f5cd3a992c77becf3de",
|
|
42
|
+
"updated": "2026-10-15T23:30:59Z"
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"file": "daily-challenges.md",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"file": "getting-started.md",
|
|
71
|
-
"sha256": "
|
|
72
|
-
"updated": "2026-10-
|
|
71
|
+
"sha256": "d131f187f5a3146a6ec13289d889606c4de770378921dc3b56b7c1b678b5f162",
|
|
72
|
+
"updated": "2026-10-15T23:30:59Z"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"file": "inventory-and-catalogs.md",
|
|
@@ -128,8 +128,8 @@
|
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
"file": "server-bundles.md",
|
|
131
|
-
"sha256": "
|
|
132
|
-
"updated": "2026-10-
|
|
131
|
+
"sha256": "f6654b2865d04cbcb8ed1bb1e157107e27d96348d74e8bf1be040a747991b743",
|
|
132
|
+
"updated": "2026-10-15T23:30:59Z"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"file": "serverless-functions-js.md",
|
|
@@ -153,8 +153,8 @@
|
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
"file": "what-a-game-must-do.md",
|
|
156
|
-
"sha256": "
|
|
157
|
-
"updated": "2026-10-
|
|
156
|
+
"sha256": "e97ca7a5b01a40d57900eb1116e961768f62bccfd412afd02bca245cdb7beeab",
|
|
157
|
+
"updated": "2026-10-15T20:36:02Z"
|
|
158
158
|
}
|
|
159
159
|
]
|
|
160
160
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": [
|
|
3
|
+
"The published fields whose value is PlayerText (player-authored text), and the",
|
|
4
|
+
"first release from which each has been PlayerText in that entry. String(), a",
|
|
5
|
+
"template literal or JSON.stringify on one shows \"[gem: use gem.text.toPlain()]\";",
|
|
6
|
+
"convert it with text.toPlain(). `entry` is the package export (`sdk` is",
|
|
7
|
+
"@indiegems/gem-web-sdk/sdk), `field` is <Type>.<property>, and `since` is null",
|
|
8
|
+
"for a field no release has shipped yet. Written by",
|
|
9
|
+
"`node scripts/player-text-fields.mjs`."
|
|
10
|
+
],
|
|
11
|
+
"schema_version": 1,
|
|
12
|
+
"fields": [
|
|
13
|
+
{
|
|
14
|
+
"entry": "function",
|
|
15
|
+
"field": "Friend.displayName",
|
|
16
|
+
"since": "7.6.0"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"entry": "function",
|
|
20
|
+
"field": "Friend.displayTag",
|
|
21
|
+
"since": "7.6.0"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"entry": "function",
|
|
25
|
+
"field": "LeaderboardEntry.displayName",
|
|
26
|
+
"since": "7.6.0"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"entry": "function",
|
|
30
|
+
"field": "LeaderboardEntry.displayTag",
|
|
31
|
+
"since": "7.6.0"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"entry": "function",
|
|
35
|
+
"field": "PlayerIdentity.displayName",
|
|
36
|
+
"since": "7.6.0"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"entry": "function",
|
|
40
|
+
"field": "PlayerIdentity.displayTag",
|
|
41
|
+
"since": "7.6.0"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"entry": "function",
|
|
45
|
+
"field": "PlayerProfile.displayName",
|
|
46
|
+
"since": "7.6.0"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"entry": "function",
|
|
50
|
+
"field": "PlayerProfile.displayTag",
|
|
51
|
+
"since": "7.6.0"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"entry": "function",
|
|
55
|
+
"field": "ShowcaseFriendEntry.displayName",
|
|
56
|
+
"since": "7.6.0"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"entry": "function",
|
|
60
|
+
"field": "ShowcaseFriendEntry.displayTag",
|
|
61
|
+
"since": "7.6.0"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"entry": "function",
|
|
65
|
+
"field": "Transfer.message",
|
|
66
|
+
"since": "7.6.0"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"entry": "game",
|
|
70
|
+
"field": "Friend.displayName",
|
|
71
|
+
"since": "5.11.0"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"entry": "game",
|
|
75
|
+
"field": "Friend.displayTag",
|
|
76
|
+
"since": "5.11.0"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"entry": "game",
|
|
80
|
+
"field": "GemPlayer.displayHandle",
|
|
81
|
+
"since": "1.2.0"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"entry": "game",
|
|
85
|
+
"field": "GemPlayer.displayName",
|
|
86
|
+
"since": "1.2.0"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"entry": "game",
|
|
90
|
+
"field": "GemPlayer.displayTag",
|
|
91
|
+
"since": "5.11.0"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"entry": "game",
|
|
95
|
+
"field": "LeaderboardEntry.displayName",
|
|
96
|
+
"since": "5.11.0"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"entry": "game",
|
|
100
|
+
"field": "LeaderboardEntry.displayTag",
|
|
101
|
+
"since": "5.11.0"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"entry": "game",
|
|
105
|
+
"field": "PlayerIdentity.displayName",
|
|
106
|
+
"since": "5.11.0"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"entry": "game",
|
|
110
|
+
"field": "PlayerIdentity.displayTag",
|
|
111
|
+
"since": "5.11.0"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"entry": "game",
|
|
115
|
+
"field": "PlayerProfile.displayName",
|
|
116
|
+
"since": "5.11.0"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"entry": "game",
|
|
120
|
+
"field": "PlayerProfile.displayTag",
|
|
121
|
+
"since": "5.11.0"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"entry": "game",
|
|
125
|
+
"field": "ShowcaseFriendEntry.displayName",
|
|
126
|
+
"since": "5.11.0"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"entry": "game",
|
|
130
|
+
"field": "ShowcaseFriendEntry.displayTag",
|
|
131
|
+
"since": "5.11.0"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"entry": "sdk",
|
|
135
|
+
"field": "Friend.displayName",
|
|
136
|
+
"since": "5.11.0"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"entry": "sdk",
|
|
140
|
+
"field": "Friend.displayTag",
|
|
141
|
+
"since": "5.11.0"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"entry": "sdk",
|
|
145
|
+
"field": "GemPlayer.displayHandle",
|
|
146
|
+
"since": "3.2.0"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"entry": "sdk",
|
|
150
|
+
"field": "GemPlayer.displayName",
|
|
151
|
+
"since": "3.2.0"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"entry": "sdk",
|
|
155
|
+
"field": "GemPlayer.displayTag",
|
|
156
|
+
"since": "5.11.0"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"entry": "sdk",
|
|
160
|
+
"field": "GemServerProfile.displayName",
|
|
161
|
+
"since": "3.2.0"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"entry": "sdk",
|
|
165
|
+
"field": "GemServerProfile.displayTag",
|
|
166
|
+
"since": "5.11.0"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"entry": "sdk",
|
|
170
|
+
"field": "LeaderboardEntry.displayName",
|
|
171
|
+
"since": "5.11.0"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"entry": "sdk",
|
|
175
|
+
"field": "LeaderboardEntry.displayTag",
|
|
176
|
+
"since": "5.11.0"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"entry": "sdk",
|
|
180
|
+
"field": "PlayerIdentity.displayName",
|
|
181
|
+
"since": "5.11.0"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"entry": "sdk",
|
|
185
|
+
"field": "PlayerIdentity.displayTag",
|
|
186
|
+
"since": "5.11.0"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"entry": "sdk",
|
|
190
|
+
"field": "PlayerProfile.displayName",
|
|
191
|
+
"since": "5.11.0"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"entry": "sdk",
|
|
195
|
+
"field": "PlayerProfile.displayTag",
|
|
196
|
+
"since": "5.11.0"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"entry": "sdk",
|
|
200
|
+
"field": "ShowcaseFriendEntry.displayName",
|
|
201
|
+
"since": "5.11.0"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"entry": "sdk",
|
|
205
|
+
"field": "ShowcaseFriendEntry.displayTag",
|
|
206
|
+
"since": "5.11.0"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"entry": "server",
|
|
210
|
+
"field": "GemServerProfile.displayName",
|
|
211
|
+
"since": "2.2.0"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"entry": "server",
|
|
215
|
+
"field": "GemServerProfile.displayTag",
|
|
216
|
+
"since": "5.11.0"
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
package/docs/server-bundles.md
CHANGED
|
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
|
|
|
80
80
|
also publishes on its download host, with the hash beside it:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
curl -fsSL -O https://downloads.gemarcade.com/sdk/v13.
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/v13.
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v13.3.1/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v13.3.1/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -1189,6 +1189,10 @@ What it does, so you know what to expect:
|
|
|
1189
1189
|
- **Your budget is `1000 / targetFps`**, and `targetFps` is 60 unless you say. A game that caps
|
|
1190
1190
|
itself at 30 frames a second on battery saver passes `targetFps: 30`, or every frame reads as
|
|
1191
1191
|
slow.
|
|
1192
|
+
- **Capping your frame rate? Report your draws, or the Arcade's FPS overlay still reads 60.** The
|
|
1193
|
+
overlay counts the frames the browser offers your page, not the frames you draw. Call
|
|
1194
|
+
`gem.reportFrame()` each time you draw and it counts your draws instead, so a player can see
|
|
1195
|
+
Battery saver working. See [The Arcade's FPS overlay](#the-arcades-fps-overlay).
|
|
1192
1196
|
- **It never counts a paused game**: not while the Arcade has you paused (`gem.paused`), and not
|
|
1193
1197
|
while the tab is hidden. It also ignores the first 2 seconds, which are your game loading.
|
|
1194
1198
|
- **It holds a hand-picked tier.** While `graphicsQualityAuto` is `false` it stays at the start and
|
|
@@ -1371,15 +1375,31 @@ Arcade, which draws a small readout in its own chrome. **Your game draws nothing
|
|
|
1371
1375
|
is sent until `showFrameStats` arrives `true`; the counter stops when it goes `false`, while the
|
|
1372
1376
|
Arcade has your game paused (`onPause`), and when the bridge closes. A game whose player never
|
|
1373
1377
|
presses the toggle never runs any of it.
|
|
1374
|
-
- **What the number measures
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
frame on purpose:
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
- **If you
|
|
1381
|
-
|
|
1382
|
-
|
|
1378
|
+
- **What the number measures, by default: the frames your page is offered, not the frames you
|
|
1379
|
+
draw.** It counts the `requestAnimationFrame` ticks the browser delivers to your game's
|
|
1380
|
+
document. That is your render rate only if you draw on every one of them — three.js, Phaser and
|
|
1381
|
+
Babylon.js do unless you cap them — and it is measured inside your frame on purpose: the Arcade
|
|
1382
|
+
page cannot count your frames from outside, because a cross-origin iframe renders on its own
|
|
1383
|
+
schedule, and a page-side counter would read a steady 60 over a game hitching at 25.
|
|
1384
|
+
- **If you skip frames, report your draws.** A game that caps itself on Battery saver by drawing
|
|
1385
|
+
every other tick still receives 60 ticks a second, so the default figure reads about 60 and
|
|
1386
|
+
nobody can see the cap working. The same goes for a game that draws through an `OffscreenCanvas`
|
|
1387
|
+
in a worker, which the SDK can only time on its main thread. Call `gem.reportFrame()` each time
|
|
1388
|
+
your game draws a frame, and the overlay counts your draws instead, labelled as the game's own
|
|
1389
|
+
figure:
|
|
1390
|
+
|
|
1391
|
+
```js
|
|
1392
|
+
function render() {
|
|
1393
|
+
renderer.render(scene, camera);
|
|
1394
|
+
gem.reportFrame(); // once per frame you actually draw
|
|
1395
|
+
}
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
Call it on **every** frame you draw, from your first: the first call switches the overlay to your
|
|
1399
|
+
draws for the rest of the session, so a screen that draws without it reads as dropped frames, and
|
|
1400
|
+
a second call for one frame counts it twice. It is free while the overlay is off — a flag check,
|
|
1401
|
+
nothing sent — so leave it in your loop. A worker renderer posts to the main thread when it draws
|
|
1402
|
+
and calls it there.
|
|
1383
1403
|
- **Do not draw an FPS counter of your own over the Arcade's.** Two readouts that disagree — and
|
|
1384
1404
|
they will, since they count different things — teach a player that one of them is lying. A debug
|
|
1385
1405
|
overlay in your own builds is fine; keep it off in an Arcade build, or at least off by default.
|
|
@@ -1390,6 +1410,10 @@ Arcade, which draws a small readout in its own chrome. **Your game draws nothing
|
|
|
1390
1410
|
`showFrameStats` is also on `gem.prefs` for you to read, held like the rest, but the only reason to
|
|
1391
1411
|
read it is to know the Arcade is already showing one.
|
|
1392
1412
|
|
|
1413
|
+
The browser offers no way to count the frames it actually put on screen for your page, which is
|
|
1414
|
+
why your draws come from you: without `gem.reportFrame()` the SDK cannot tell a frame you drew from
|
|
1415
|
+
one you skipped.
|
|
1416
|
+
|
|
1393
1417
|
## The player's language: prefer what the page sends, detect the rest
|
|
1394
1418
|
|
|
1395
1419
|
**`gem.prefs.locale` is the language the Arcade page is RENDERING in.** That is the one fact a frame
|
|
@@ -1492,6 +1516,103 @@ and showcase rows used to be plain strings and are now `PlayerText`, as they alr
|
|
|
1492
1516
|
members. `gem.player.displayHandle` and `gem.player.avatarUrl` still work, and are
|
|
1493
1517
|
[deprecated](deprecation-policy.md) in favour of `displayTag` and `avatarIconUrl`.
|
|
1494
1518
|
|
|
1519
|
+
### A name turned into a string: what catches it
|
|
1520
|
+
|
|
1521
|
+
`PlayerText` is not a string, so TypeScript refuses `el.innerHTML = who.displayName`, a `string`
|
|
1522
|
+
variable or parameter, and string methods such as `.length` and `.toUpperCase()`. **Four uses
|
|
1523
|
+
still compile**, because each accepts any value, and each puts `[gem: use gem.text.toPlain()]` on
|
|
1524
|
+
screen where the name should be:
|
|
1525
|
+
|
|
1526
|
+
```js
|
|
1527
|
+
String(who.displayName); // and who.displayName.toString()
|
|
1528
|
+
`Hi ${who.displayName}`;
|
|
1529
|
+
'Hi ' + who.displayName;
|
|
1530
|
+
JSON.stringify({ name: who.displayName });
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
A game that moved to a release where a field became `PlayerText` and kept a line like these
|
|
1534
|
+
compiles, builds and uploads cleanly, and shows players the placeholder. Three things catch it
|
|
1535
|
+
first:
|
|
1536
|
+
|
|
1537
|
+
- **The console, while you develop.** On a page served from your own machine (your bundler's dev
|
|
1538
|
+
or preview server, `gemctl serve`, with or without `--dev`), the first time each field is turned
|
|
1539
|
+
into a string the SDK logs one `console.warn` naming the field and the fix, with a stack trace
|
|
1540
|
+
that points at your line. A published game logs nothing, and the placeholder is the same.
|
|
1541
|
+
- **A lint rule.** If your ESLint runs typescript-eslint with type information, its
|
|
1542
|
+
`restrict-template-expressions` and `restrict-plus-operands` rules already flag the template and
|
|
1543
|
+
the `+`. This rule flags all four; paste it into `eslint.config.js`:
|
|
1544
|
+
|
|
1545
|
+
```js
|
|
1546
|
+
// Flags String(x), x.toString(), `${x}`, '…' + x and JSON.stringify(x) on PlayerText.
|
|
1547
|
+
const noPlayerTextString = {
|
|
1548
|
+
meta: {
|
|
1549
|
+
type: 'problem',
|
|
1550
|
+
schema: [],
|
|
1551
|
+
messages: {
|
|
1552
|
+
stringified:
|
|
1553
|
+
'This is PlayerText: as a string it shows "[gem: use gem.text.toPlain()]". Convert it with text.toPlain().',
|
|
1554
|
+
},
|
|
1555
|
+
},
|
|
1556
|
+
create(context) {
|
|
1557
|
+
const services = context.sourceCode.parserServices;
|
|
1558
|
+
const checker = services.program.getTypeChecker();
|
|
1559
|
+
const isPlayerText = (type) =>
|
|
1560
|
+
(type.isUnion() ? type.types : [type]).some((t) => t.getSymbol()?.getName() === 'PlayerText');
|
|
1561
|
+
const typeOf = (node) => checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node));
|
|
1562
|
+
const check = (node) => {
|
|
1563
|
+
if (node && isPlayerText(typeOf(node))) context.report({ node, messageId: 'stringified' });
|
|
1564
|
+
};
|
|
1565
|
+
// JSON.stringify also looks one level into an object: { name: who.displayName }.
|
|
1566
|
+
const checkJson = (node) => {
|
|
1567
|
+
if (!node) return;
|
|
1568
|
+
const type = typeOf(node);
|
|
1569
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
1570
|
+
const inside = checker
|
|
1571
|
+
.getPropertiesOfType(type)
|
|
1572
|
+
.some((p) => isPlayerText(checker.getTypeOfSymbolAtLocation(p, tsNode)));
|
|
1573
|
+
if (isPlayerText(type) || inside) context.report({ node, messageId: 'stringified' });
|
|
1574
|
+
};
|
|
1575
|
+
return {
|
|
1576
|
+
CallExpression(node) {
|
|
1577
|
+
const { callee } = node;
|
|
1578
|
+
if (callee.type === 'Identifier' && callee.name === 'String') check(node.arguments[0]);
|
|
1579
|
+
if (callee.type !== 'MemberExpression' || callee.computed) return;
|
|
1580
|
+
const isJson = callee.object.type === 'Identifier' && callee.object.name === 'JSON';
|
|
1581
|
+
if (isJson && callee.property.name === 'stringify') checkJson(node.arguments[0]);
|
|
1582
|
+
if (callee.property.name === 'toString') check(callee.object);
|
|
1583
|
+
},
|
|
1584
|
+
TemplateLiteral(node) {
|
|
1585
|
+
// A tagged template is the tag's to handle, and an html`` tag escapes.
|
|
1586
|
+
if (node.parent.type !== 'TaggedTemplateExpression') node.expressions.forEach(check);
|
|
1587
|
+
},
|
|
1588
|
+
BinaryExpression(node) {
|
|
1589
|
+
if (node.operator === '+') [node.left, node.right].forEach(check);
|
|
1590
|
+
},
|
|
1591
|
+
AssignmentExpression(node) {
|
|
1592
|
+
if (node.operator === '+=') check(node.right);
|
|
1593
|
+
},
|
|
1594
|
+
};
|
|
1595
|
+
},
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
export default [
|
|
1599
|
+
// …your existing config, with type information on (parserOptions.projectService: true)
|
|
1600
|
+
{
|
|
1601
|
+
files: ['**/*.ts', '**/*.tsx', '**/*.mts'],
|
|
1602
|
+
plugins: { gem: { rules: { 'no-player-text-string': noPlayerTextString } } },
|
|
1603
|
+
rules: { 'gem/no-player-text-string': 'error' },
|
|
1604
|
+
},
|
|
1605
|
+
];
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
It needs type information, so it lints TypeScript only. In plain JavaScript, rely on the console
|
|
1609
|
+
warning.
|
|
1610
|
+
- **The field list.** The package carries `docs/player-text-fields.json`: every field that holds
|
|
1611
|
+
`PlayerText`, by entry (`sdk` is `@indiegems/gem-web-sdk/sdk`), each with the first release in
|
|
1612
|
+
which that entry shipped it as `PlayerText`. Search your code for every field it lists, not only
|
|
1613
|
+
the ones that are new since your last pin: a field that has been `PlayerText` for years
|
|
1614
|
+
stringifies to the placeholder just the same.
|
|
1615
|
+
|
|
1495
1616
|
### The signed-in player's Boost: show it, never gate on it
|
|
1496
1617
|
|
|
1497
1618
|
`gem.player` also carries **`boost`** — the player's Boost, which the Arcade sells for real money.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indiegems/gem-web-sdk",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.3.1",
|
|
4
4
|
"description": "postMessage bridge contract between gem-web and browser-playable games",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"docs/manifest.json",
|
|
59
59
|
"docs/api/*.md",
|
|
60
60
|
"docs/api/reference.json",
|
|
61
|
+
"docs/player-text-fields.json",
|
|
61
62
|
"CHANGELOG.md"
|
|
62
63
|
]
|
|
63
64
|
}
|
package/schema/bridge.v1.json
CHANGED
|
@@ -3302,9 +3302,17 @@
|
|
|
3302
3302
|
"WHAT IT COUNTS. The frame document's own requestAnimationFrame delivery,",
|
|
3303
3303
|
"which is exact for an engine that renders on that callback (three.js,",
|
|
3304
3304
|
"Phaser, Babylon.js, any engine's web export) and is what the SDK measures",
|
|
3305
|
-
"with no code from the game.
|
|
3306
|
-
"
|
|
3307
|
-
"
|
|
3305
|
+
"with no code from the game. It is NOT the frames the game draws: a game",
|
|
3306
|
+
"honouring Battery saver by drawing every other tick still reads about 60,",
|
|
3307
|
+
"and a game rendering in a worker through OffscreenCanvas is timed on its",
|
|
3308
|
+
"main thread, not at its renderer.",
|
|
3309
|
+
"",
|
|
3310
|
+
"OR WHAT THE GAME SAYS IT DREW. A game that calls `gem.reportFrame()` each",
|
|
3311
|
+
"time it draws is counted by its draws instead, from its first call for",
|
|
3312
|
+
"the rest of the session, and `counts` says which figure this is, so the",
|
|
3313
|
+
"Arcade labels it rather than calling draws page frames or the reverse.",
|
|
3314
|
+
"No web API reports the compositor's presented frames to a document, so",
|
|
3315
|
+
"the game's report is the only figure that counts draws.",
|
|
3308
3316
|
"",
|
|
3309
3317
|
"SENT ONLY WHILE ASKED. Nothing is counted and nothing is sent until",
|
|
3310
3318
|
"`gem.prefs.showFrameStats` arrives true, and the counter stops on false",
|
|
@@ -3326,7 +3334,20 @@
|
|
|
3326
3334
|
],
|
|
3327
3335
|
"fields": {
|
|
3328
3336
|
"fps": { "kind": "int", "min": 0, "max": 1000 },
|
|
3329
|
-
"frameTimeP95Ms": { "kind": "int", "min": 0, "max": 100000 }
|
|
3337
|
+
"frameTimeP95Ms": { "kind": "int", "min": 0, "max": 100000 },
|
|
3338
|
+
"counts": {
|
|
3339
|
+
"kind": "enum",
|
|
3340
|
+
"values": ["pageFrames", "gameDraws"],
|
|
3341
|
+
"optional": true,
|
|
3342
|
+
"$comment": [
|
|
3343
|
+
"Which figure `fps` and `frameTimeP95Ms` are. `pageFrames`: the",
|
|
3344
|
+
"requestAnimationFrame ticks delivered to the game's document, measured",
|
|
3345
|
+
"by the SDK. `gameDraws`: the frames the game reported drawing through",
|
|
3346
|
+
"`gem.reportFrame()`. ABSENT from an SDK before this field, which always",
|
|
3347
|
+
"measured page frames, so a receiver reads absent as `pageFrames`.",
|
|
3348
|
+
"Additive: a parent that ignores it draws the numbers as before."
|
|
3349
|
+
]
|
|
3350
|
+
}
|
|
3330
3351
|
},
|
|
3331
3352
|
"rateLimit": { "perMinute": 120 }
|
|
3332
3353
|
},
|