@indiegems/gem-web-sdk 23.0.6 → 23.1.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/docs/changelog.md CHANGED
@@ -16,6 +16,32 @@ Its entries are grouped under up to five headings:
16
16
  - **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
17
17
  - **New capability**: something a game can now do. Nothing changes unless you use it.
18
18
 
19
+ ## 23.1.1
20
+
21
+ Upgrading from 23.1.0. Impact: none.
22
+
23
+ Nothing in this release asks anything of a game.
24
+
25
+ ## 23.1.0
26
+
27
+ *Written from the API facts alone; no summary was generated for this release.*
28
+
29
+ Upgrading from 23.0.6. Impact: required.
30
+
31
+ ### Must change
32
+
33
+ - PARENT_CONFORMANCE_VERSION changed its signature.
34
+ - GemCapability changed its signature.
35
+ - GemHost.sendPrefs changed its signature.
36
+ - GemPrefs.recommended changed its signature.
37
+
38
+ ### New capability
39
+
40
+ - ParentUnderTest.setTextScale was added.
41
+ - Gem.textScale was added.
42
+ - GemOptions.onTextScale was added.
43
+ - GemRecommendedSettings.textScale was added.
44
+
19
45
  ## 23.0.6
20
46
 
21
47
  Upgrading from 23.0.5. Impact: none.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "23.0.6"
53
+ "@indiegems/gem-web-sdk": "23.1.1"
54
54
  }
55
55
  ```
56
56
 
@@ -48,8 +48,8 @@
48
48
  },
49
49
  {
50
50
  "file": "changelog.md",
51
- "sha256": "412ce6f6db6c2df07e8b360f71b13635f770453fcd616b4578723a6e85906d5c",
52
- "updated": "2026-11-07T03:38:38Z"
51
+ "sha256": "ffe343dbeb0820cb452ac8395245fa39ae3d09e1b2252eaa3730b566f215a879",
52
+ "updated": "2026-11-07T17:43:13Z"
53
53
  },
54
54
  {
55
55
  "file": "daily-challenges.md",
@@ -78,8 +78,8 @@
78
78
  },
79
79
  {
80
80
  "file": "getting-started.md",
81
- "sha256": "2cba7161b4896dd4a3f448a8ace3f0f2038b9a81de20b69ac8c000b3f50676e3",
82
- "updated": "2026-11-07T03:38:38Z"
81
+ "sha256": "28ab09ea55162912ff25ae8ccb6f6cdc35e494d65ebf5e2b0ee8952ea0265a81",
82
+ "updated": "2026-11-07T17:43:13Z"
83
83
  },
84
84
  {
85
85
  "file": "graphics-route.md",
@@ -123,8 +123,8 @@
123
123
  },
124
124
  {
125
125
  "file": "rewarded-ads.md",
126
- "sha256": "0658ec71ae9938040c2a493385cd9e360224b51309b3254438950124ae444743",
127
- "updated": "2026-11-07T02:38:17Z"
126
+ "sha256": "81b421d4bde3f71b40c7a5a173e6a2c114a315215a11491e488872ff9e1909dc",
127
+ "updated": "2026-11-07T16:40:01Z"
128
128
  },
129
129
  {
130
130
  "file": "room-shapes.md",
@@ -143,8 +143,8 @@
143
143
  },
144
144
  {
145
145
  "file": "server-bundles.md",
146
- "sha256": "58cca5bfe80a301e3b0a8fb35f38668ce8515be38eefcb13b6b2f78c9106280b",
147
- "updated": "2026-11-07T03:38:38Z"
146
+ "sha256": "c6b46f9b4628385858f0834dc4f44b6e737adba5805f3faefda8d4db44878b44",
147
+ "updated": "2026-11-07T17:43:13Z"
148
148
  },
149
149
  {
150
150
  "file": "serverless-functions-js.md",
@@ -168,8 +168,8 @@
168
168
  },
169
169
  {
170
170
  "file": "what-a-game-must-do.md",
171
- "sha256": "341a5fa4bec0435ece65183ea2edd601037767cc58cf7f0e5929547ad2253161",
172
- "updated": "2026-11-05T16:22:23Z"
171
+ "sha256": "f70fe9fad67c6b386bddc53f1f876bb78f6a5e11c35f565bb93e925d65a99874",
172
+ "updated": "2026-11-06T22:48:07Z"
173
173
  }
174
174
  ]
175
175
  }
@@ -110,7 +110,23 @@ rewarded_placements:
110
110
  | `cooldown_seconds` | Seconds between two grants to one player, 0 to 86400. Defaults to 0. |
111
111
  | `max_held` | No grant while the player already holds at least this many of the granted item. 1 to 100, and only on a placement whose grants are all items. |
112
112
  | `without_ads` | **Optional:** `grant` or `nothing`, and `nothing` when you leave it out. What a press gets when no ad plays: where no ad serves your channel at all, and where one does but no ad was available for this press or it failed to play (§8). `grant` pays the reward with no ad, within your caps, so write it out only for a reward that is fine to give free. |
113
- | `allowance_currency_slug` | Optional: one of your own currencies that each grant also spends 1 of. Use it when another path in your game must share the same daily cap. It must not be `client_writable`, must have `transfer_policy: none`, and needs a bucket that resets (daily, say) or it never refills. |
113
+ | `allowance_currency_slug` | Optional: one of your own currencies that each grant also spends 1 of. Use it when another path in your game must share the same daily cap. It must not be `client_writable` and must have `transfer_policy: none`. A grant is refused while it is empty (§3). ⚠️ For an allowance your game grants per use, put it in a bucket that never resets (`reset_interval: never`) for now, even though the upload warns `rewarded_placement_allowance_no_reset` (below). |
114
+
115
+ ⚠️ **Until a gem-api fix ships, an allowance in a resetting bucket refuses a player's first
116
+ press.** A grant that creates the player's balance of the allowance currency is not stamped with
117
+ the bucket's current period, so the next order on that player's inventory resets it, and the
118
+ press's own check is that order. A bucket that resets to `reset_amount: 0` (`daily`, say) therefore
119
+ empties the first grant before it can be spent, and the press answers `no_ad` with nothing granted.
120
+ Meanwhile:
121
+
122
+ - **An allowance your game grants per use** (one per level chest, say) needs no refill. Put its
123
+ currency in a bucket with `reset_interval: never`. The upload's
124
+ `rewarded_placement_allowance_no_reset` warning is about refilling, which such an allowance does
125
+ not need, so keep the bucket that never resets when the upload warns.
126
+ - **For a daily cap on this placement alone**, use its own `max_per_day` (§6), which needs no
127
+ allowance.
128
+
129
+ This advice will change once the fix is out.
114
130
 
115
131
  The placement only **selects** a reward your own config declared. Your game sends no reward, no
116
132
  amount, no watch time and no item in the request — nothing from your frame is trusted for that — so
@@ -216,6 +232,7 @@ after a press:
216
232
  | an ad source serves, but no ad was available for this press, or it failed to play | your `without_ads` decides: `grant` grants within your caps, with no ad; `nothing` grants nothing, and the Arcade shows "No ad available right now" |
217
233
  | no ad source serves the channel (§8) | your `without_ads` decides, with no ad |
218
234
  | a cap, a cooldown, `max_held`, the platform's spacing (§6), or something about the player | no |
235
+ | the placement's `allowance_currency_slug` (§2) is empty | no: the answer is `no_ad`, and on `dev` and `test` the console line says the allowance is spent (§10) |
219
236
 
220
237
  The answer is `shown` whenever the Arcade put its ad stage up, however the ad then ended: watched
221
238
  to the end, closed early, or no ad could play and `without_ads` decided. It is `no_ad` when no
@@ -312,8 +329,10 @@ never in a ranked mode — because the platform cannot see a run (§6).
312
329
  **Within those rules, draw it only when `gem.ads.rewardedStatus` says it can pay** (SDK 8.1.0 and
313
330
  later). The status reads the placement's caps as the platform holds them: `declared`, `grantable`,
314
331
  `remainingToday`, `nextEligibleAt`, `heldLimitReached` and `resetsAt`. `grantable` is `false` when
315
- a cap is spent, a cooldown runs, the player holds `max_held`, or rewarded is off on the channel, so
316
- drawing on it never offers a press that is certain to pay nothing. Hide or grey the button out
332
+ a cap is spent, a cooldown runs, the player holds `max_held`, or rewarded is off on the channel.
333
+ It does **not** read the placement's `allowance_currency_slug`: with the allowance empty,
334
+ `grantable` is still `true` and the press answers `no_ad`, so read the allowance currency's balance
335
+ yourself before you draw a placement that declares one (§2). Hide or grey the button out
317
336
  otherwise, and say when it is back from `nextEligibleAt` or `resetsAt`. `grantable` is not a
318
337
  promise: a press can still pay nothing for reasons the read never reports. The status reserves
319
338
  nothing, and it never rejects:
@@ -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/v23.0.6/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v23.0.6/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v23.1.1/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v23.1.1/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -1149,20 +1149,21 @@ The player sets their defaults once, in the Arcade's Settings, separately for ea
1149
1149
  and a television can say different things. They reach your game as `gem.prefs.recommended`, in the
1150
1150
  same push as `muted` and `reducedMotion`, and again whenever the player changes one:
1151
1151
 
1152
- | Field | Values | What it suggests |
1153
- | ------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
1154
- | `volume` | `0` to `100` | Your master volume's starting level, as a percentage — divide by 100 for a gain. A level, never a mute. |
1155
- | `batterySaver` | `true` / `false` | Spend less power: a lower frame rate, fewer effects, a lower resolution. |
1156
- | `keyboardLayout` | `'wasd'` / `'arrows'` | Which keys move by default. |
1157
- | `gamepadConfirm` | `'south'` / `'east'` | Which face button confirms, by position: the bottom one (A on an Xbox controller) or the right one (B). The other one goes back. |
1158
- | `vibration` | `true` / `false` | Your vibration toggle's starting state ([if your game vibrates](#if-your-game-vibrates)). |
1159
- | `lookSensitivity` | `20` to `400` | Look speed for touch, mouse and pen, as a percentage of the base speeds: `100` is 1×. |
1160
- | `gamepadLookSensitivity` | `20` to `400` | Look speed for a gamepad stick, as a percentage: `100` is 1×. |
1161
- | `invertLookY` | `true` / `false` | Up/down look inverted: pushing up or dragging up looks down. Pitch only, never yaw. |
1162
- | `touchMovement` | `'floating'` / `'fixed'` | On a touch screen, a movement stick that appears where the thumb lands, or one fixed in place. |
1163
- | `graphicsQuality` | `'low'` / `'medium'` / `'high'` | The graphics quality to start at on this device. |
1164
- | `graphicsQualityAuto` | `true` / `false` | `true`: the Arcade picked that tier for this device, so you may adapt from it. `false`: the player picked it, so hold it. |
1165
- | `colorHelp` | `true` / `false` / `null` | Color help: start your colour-blind aids (patterns or symbols beside colours, a colour-blind palette) on, or off. `null`: no advice. |
1152
+ | Field | Values | What it suggests |
1153
+ | ------------------------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1154
+ | `volume` | `0` to `100` | Your master volume's starting level, as a percentage — divide by 100 for a gain. A level, never a mute. |
1155
+ | `batterySaver` | `true` / `false` | Spend less power: a lower frame rate, fewer effects, a lower resolution. |
1156
+ | `keyboardLayout` | `'wasd'` / `'arrows'` | Which keys move by default. |
1157
+ | `gamepadConfirm` | `'south'` / `'east'` | Which face button confirms, by position: the bottom one (A on an Xbox controller) or the right one (B). The other one goes back. |
1158
+ | `vibration` | `true` / `false` | Your vibration toggle's starting state ([if your game vibrates](#if-your-game-vibrates)). |
1159
+ | `lookSensitivity` | `20` to `400` | Look speed for touch, mouse and pen, as a percentage of the base speeds: `100` is 1×. |
1160
+ | `gamepadLookSensitivity` | `20` to `400` | Look speed for a gamepad stick, as a percentage: `100` is 1×. |
1161
+ | `invertLookY` | `true` / `false` | Up/down look inverted: pushing up or dragging up looks down. Pitch only, never yaw. |
1162
+ | `touchMovement` | `'floating'` / `'fixed'` | On a touch screen, a movement stick that appears where the thumb lands, or one fixed in place. |
1163
+ | `graphicsQuality` | `'low'` / `'medium'` / `'high'` | The graphics quality to start at on this device. |
1164
+ | `graphicsQualityAuto` | `true` / `false` | `true`: the Arcade picked that tier for this device, so you may adapt from it. `false`: the player picked it, so hold it. |
1165
+ | `colorHelp` | `true` / `false` / `null` | Color help: start your colour-blind aids (patterns or symbols beside colours, a colour-blind palette) on, or off. `null`: no advice. |
1166
+ | `textScale` | `100` to `200` / `null` | Text size, as a percent: scale your whole interface by it. Sent only to a game that passes `onTextScale`: see [text size](#the-arcades-text-size-size-in-rem-apply-ontextscale). |
1166
1167
 
1167
1168
  `lookSensitivity` to `touchMovement` are the camera and controls settings: see
1168
1169
  [camera and look controls](#camera-and-look-controls) for `gem.controls.look()`, which applies the
@@ -1242,6 +1243,96 @@ owns — starts from `recommended` the same way and is kept in `gem.me.settings`
1242
1243
  ([sound](#sound-your-own-mute-control-and-the-arcades)), and `reducedMotion` is the player's
1243
1244
  accessibility setting, which your game honours.
1244
1245
 
1246
+ ### The Arcade's Text size: size in `rem`, apply `onTextScale`
1247
+
1248
+ The player picks a **Text size** in the Arcade's Settings, per device: 100, 115, 130, 150, 180 or
1249
+ 200%, or System, which follows the phone's own font size. The Arcade never enlarges your game's
1250
+ text for you — inside your frame, text renders at the size you chose — so a game that wants to
1251
+ honour it applies it, and applies it to **the whole interface**, not to text alone. Text that grows
1252
+ inside boxes that do not is exactly what breaks: labels overflow, overlap and clip.
1253
+
1254
+ **Pass `onTextScale` and set your root font size from it.** Passing the handler is what asks the
1255
+ Arcade for the value; a game without one is never sent it, and `gem.textScale` stays `null`:
1256
+
1257
+ ```js
1258
+ const MAX = 150; // the largest size your layout supports: at least 130, ideally 150
1259
+ let chosen = null; // the size the player picked in YOUR settings, or null: "Arcade's size"
1260
+
1261
+ const applyTextSize = (p) => {
1262
+ document.documentElement.style.fontSize = (chosen ?? Math.min(p ?? 100, MAX)) + '%';
1263
+ };
1264
+
1265
+ const gem = createGem({ allowedEmbedders, onTextScale: applyTextSize });
1266
+ ```
1267
+
1268
+ It is called on the first push — with `null` when the Arcade gave no advice — and again whenever
1269
+ the value changes. The player's own choice in your game beats the Arcade's, the Arcade's is clamped
1270
+ to what your layout supports, and with neither you render at 100%. The SDK applies nothing itself.
1271
+
1272
+ **This works only if your interface is sized in `rem`:**
1273
+
1274
+ - **Every length in your DOM UI is `rem`, `em` or `%`, never `px`:** font sizes, padding, gaps,
1275
+ widths, heights, radii and borders. Then the root font size scales them all together.
1276
+ - **Leave the root alone.** The root font size is the browser's default (`100%`), set only by the
1277
+ handler above. A `font` or `font-size` rule on `html` or `:root` breaks every `rem`: put your base
1278
+ font on `body` instead.
1279
+ - **Media queries stay in `px`**, and so do values you measure at runtime (positions, safe-area
1280
+ insets). A `rem` in a media query does not follow your root anyway.
1281
+ - **Panels that hold text are capped to the screen:** `width: min(22rem, 100% - 2rem)`, so a panel
1282
+ that grows never runs off a phone.
1283
+ - **Boxes that hold text grow with it:** `min-height` rather than `height`, and wrapping (or a
1284
+ fit-text routine whose floor you check) rather than `white-space: nowrap` with
1285
+ `overflow: hidden` or an ellipsis on an action label.
1286
+ - **Lay out against measured chrome, not constants.** Code that reserves room for a HUD bar or a
1287
+ gear button measures it (`getBoundingClientRect`, a `ResizeObserver`) instead of hard-coding
1288
+ pixels.
1289
+ - **Canvas text counts too.** A game that draws interface text on a canvas scales it from the same
1290
+ value.
1291
+ - **`text-size-adjust` is not a fix.** It does not protect your layout from any text scaling. Keep
1292
+ `100%` only as cheap protection against iOS's own inflation.
1293
+
1294
+ A board or playfield sized from the viewport (a `1/390` unit, say) may keep that unit: it follows
1295
+ the screen, which is right for the playfield. Menus, the HUD, settings and dialogs use `rem`.
1296
+
1297
+ **Support at least 130%,** with 150% the target and 200% for a game that is mostly text. Check every
1298
+ screen at 100%, 130% and your maximum on a phone held upright (390×844), a phone held sideways
1299
+ (844×390) and a desktop (1280×800), in your longest language: nothing clipped, overlapping or off
1300
+ screen. A phone held sideways runs out of height first, so every screen must fit or scroll
1301
+ vertically at your maximum. Rendered text is at least **12 CSS px**, and at least **14 CSS px** for
1302
+ what the player must read to act (costs, instructions, buttons), at a 360 CSS px wide viewport.
1303
+
1304
+ **A text-size setting of your own** is kept per device, like the Arcade setting it overrides (in
1305
+ [device settings](#device-settings-what-this-device-remembers-about-your-game)): a size chosen on a
1306
+ phone should not follow the player to a television. Declare it as a `choice` with `icon: 'text'`
1307
+ whose **first option is the Arcade's size** — the `null` state, which a slider cannot express:
1308
+
1309
+ ```js
1310
+ {
1311
+ id: 'text',
1312
+ label: t('Text size'),
1313
+ kind: 'choice',
1314
+ icon: 'text',
1315
+ value: chosen === null ? 'arcade' : String(chosen),
1316
+ options: [
1317
+ { id: 'arcade', label: t("Arcade's size") },
1318
+ { id: '100', label: '100%' },
1319
+ { id: '130', label: '130%' },
1320
+ { id: '150', label: '150%' },
1321
+ ],
1322
+ }
1323
+ ```
1324
+
1325
+ Your choice overrides the Arcade's **for your game only**; nothing you do changes the Arcade's
1326
+ setting.
1327
+
1328
+ **Two rules hold however your game uses it, because a text size hints at a player's eyesight:**
1329
+
1330
+ - **Never put a text size in analytics.** No event, dimension or measure carries `textScale`, your
1331
+ resolved size, or the value of a text-size change. Recording that the player opened your text
1332
+ settings, without the value, is fine.
1333
+ - **Save only the player's own choice.** Never store the Arcade's value as if the player had picked
1334
+ it, and a `null` choice stays `null`, so a later change to the Arcade setting still reaches them.
1335
+
1245
1336
  ### Mirror your settings into the Arcade's menu: `gem.settings`
1246
1337
 
1247
1338
  Optional, and worth doing. The Arcade's Gem menu shows your game's own settings beside the
@@ -1304,8 +1395,24 @@ const declared = () => [
1304
1395
  { id: 'music', label: t('Music'), kind: 'toggle', value: music.on, icon: 'music' }, // the quick row's Music
1305
1396
  { id: 'sfx', label: t('Sound effects'), kind: 'toggle', value: sfx.on, icon: 'sfx' },
1306
1397
  { id: 'colors', label: t('Color help'), kind: 'toggle', value: colorHelp, icon: 'eye' }, // its Color
1307
- { id: 'musicVolume', label: t('Music volume'), kind: 'slider', value: musicVolume, min: 0, max: 100, step: 5 },
1308
- { id: 'speed', label: t('Game speed'), kind: 'slider', value: speed, min: 0.5, max: 2, step: 0.25 },
1398
+ {
1399
+ id: 'musicVolume',
1400
+ label: t('Music volume'),
1401
+ kind: 'slider',
1402
+ value: musicVolume,
1403
+ min: 0,
1404
+ max: 100,
1405
+ step: 5,
1406
+ },
1407
+ {
1408
+ id: 'speed',
1409
+ label: t('Game speed'),
1410
+ kind: 'slider',
1411
+ value: speed,
1412
+ min: 0.5,
1413
+ max: 2,
1414
+ step: 0.25,
1415
+ },
1309
1416
  {
1310
1417
  id: 'difficulty',
1311
1418
  label: t('Difficulty'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "23.0.6",
3
+ "version": "23.1.1",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1007,6 +1007,10 @@
1007
1007
  "2026-10-23): the SDK does NOT fill it. An older parent omits it and the",
1008
1008
  "game reads `null`, \"no recommendation\", never false.",
1009
1009
  "",
1010
+ "`textScale` IS OPTIONAL INSIDE IT AND NOT FILLED either (owner",
1011
+ "decision 2026-11-05), and a parent sends it only where the game",
1012
+ "advertised `textScale`; an absent one is no advice.",
1013
+ "",
1010
1014
  "`muted`, `reducedMotion` and `showFrameStats` stay where they are, at",
1011
1015
  "the top of this message, and keep their meaning: they are the Arcade's",
1012
1016
  "own state the frame acts on, not a recommendation. The Arcade starts",
@@ -1127,6 +1131,42 @@
1127
1131
  "reports `null` and the game keeps its own default. Unlike the",
1128
1132
  "camera and controls fields there is no neutral value to invent."
1129
1133
  ]
1134
+ },
1135
+ "textScale": {
1136
+ "kind": "int",
1137
+ "min": 100,
1138
+ "max": 200,
1139
+ "optional": true,
1140
+ "$comment": [
1141
+ "The player's Arcade Text size for this device, as a percent (owner",
1142
+ "decision 2026-11-05): 100 is the size",
1143
+ "the game was designed at, 200 is twice that. A game sized in `rem`",
1144
+ "applies it by setting its ROOT font size, so its whole interface",
1145
+ "-- text, padding, boxes and icons -- scales together. The Arcade",
1146
+ "never applies it for a game: inside a game's frame, text renders at",
1147
+ "the size the game chose.",
1148
+ "",
1149
+ "A DEFAULT ONLY, like every field here: the player's own text size in",
1150
+ "the game wins, and a game clamps this to the largest size its layout",
1151
+ "supports. A game never saves it as the player's choice and never",
1152
+ "records it in analytics.",
1153
+ "",
1154
+ "BOUNDED 100..200 PERMANENTLY. Any integer in range is valid; a",
1155
+ "parent sends only the six sizes of the Arcade's own control (100,",
1156
+ "115, 130, 150, 180, 200 -- Android's font-scale ladder), its System",
1157
+ "choice resolved to one of them. Nothing below 100: games design to",
1158
+ "a 12 CSS px floor.",
1159
+ "",
1160
+ "SENT ONLY TO A FRAME THAT ADVERTISED `textScale`, which the SDK does",
1161
+ "when the game passes `onTextScale` to `createGem`. That limits which",
1162
+ "games receive the value; it is not a security control. A parent",
1163
+ "whose `recommended` would fail validation with this field in it",
1164
+ "sends `recommended` again without it, so a bad text size never costs",
1165
+ "a game its volume, Color help or graphics.",
1166
+ "",
1167
+ "ABSENT IS NO ADVICE. A parent built against an older SDK, or one",
1168
+ "that keeps no text size, omits it, and the SDK reports `null`."
1169
+ ]
1130
1170
  }
1131
1171
  }
1132
1172
  }
@@ -195,6 +195,14 @@
195
195
  }
196
196
  ]
197
197
  },
198
+ "textScale": {
199
+ "implemented": true,
200
+ "gates": [
201
+ {
202
+ "namespace": "gem.textScale"
203
+ }
204
+ ]
205
+ },
198
206
  "walletDisplay": {
199
207
  "implemented": true,
200
208
  "gates": [