@indiegems/gem-web-sdk 8.0.0 → 8.1.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.
@@ -353,6 +353,117 @@ one refused orientation, in fullscreen, once they are already the right way roun
353
353
  That is best effort (iOS does not implement it, and the native app's webview declines
354
354
  it), which is why the panel exists and why it always offers a way past.
355
355
 
356
+ **The same declaration decides how a computer or a television shows your game.** Neither is held
357
+ two ways round, so the Arcade reads your phone and tablet rows instead and, for a portrait-only
358
+ game, draws a centred portrait column.
359
+ [Art beside a portrait game](#art-beside-a-portrait-game-on-a-wide-screen-portrait-borders) below
360
+ has the exact rule, and the art you can put beside that column.
361
+
362
+ ## Art beside a portrait game on a wide screen: portrait borders
363
+
364
+ **You need this only if your game is portrait-only and you want your own art beside it on a
365
+ computer or a television.** On a desktop browser, the Arcade's desktop app or a TV, the Arcade
366
+ draws a portrait-only game as a centred 9:16 column and fills the space either side with its own
367
+ bars. Portrait borders put your art there instead: you publish a named set of images with your
368
+ store page, and your game says which one goes with what is on screen.
369
+
370
+ ### When the Arcade letterboxes a game
371
+
372
+ **Your store page's `orientations` decide it, and nothing else does.** Declaring borders never
373
+ turns the letterbox on, and there is no desktop orientation to declare. The rule:
374
+
375
+ 1. Take `tablet` if you declared both its `portrait` and its `landscape`; otherwise `phone` if you
376
+ declared both of its; otherwise stop, and the game is not letterboxed.
377
+ 2. The game is letterboxed to portrait when, on that device, `portrait` is `full` or `partial` and
378
+ `landscape` is `none`.
379
+
380
+ | `orientations`, each pair portrait / landscape | Letterboxed on a computer or TV |
381
+ | ---------------------------------------------- | ------------------------------- |
382
+ | nothing declared | no |
383
+ | tablet `full` / `none` | yes |
384
+ | phone `full` / `none`, no tablet | yes |
385
+ | phone `partial` / `none`, no tablet | yes |
386
+ | phone `full` / `none`, tablet `full` / `full` | no — the tablet decides |
387
+ | phone `full` / `none`, tablet `none` / `full` | no — the tablet decides |
388
+ | tablet `none` / `full` | no |
389
+ | tablet `partial` / `partial` | no |
390
+ | tablet `full` / `partial` | no |
391
+
392
+ A phone or a tablet is never letterboxed: it gets the rotate panel from the section above. On a
393
+ computer or TV, a game the rule does not letterbox fills the play area and its borders never show.
394
+ **A store page with borders and orientations that never letterbox uploads with a
395
+ `portrait_borders_unused` warning** — the page is accepted, and the art will not appear anywhere.
396
+
397
+ ### Declaring the borders
398
+
399
+ Add them to your store page's `media`, one entry per image:
400
+
401
+ ```yaml
402
+ media:
403
+ portrait_borders:
404
+ - name: meadow # the first entry is the default
405
+ src: borders/meadow.jpg
406
+ - name: alpine
407
+ src: borders/alpine.jpg
408
+ ```
409
+
410
+ - **Up to 16 entries**, each with a `name` and a `src`. A name is 1 to 40 characters of `a-z`,
411
+ `0-9`, `_` and `-`, starts with a letter or digit, and is unique on the page.
412
+ - **The first entry is the default.** The Arcade shows it the moment play starts, before your game
413
+ has said anything, so put first the one most players should see.
414
+ - **16:9, at least 1920×1080; 2560×1440 is ideal.** PNG, JPEG or WebP. An AVIF, GIF or SVG uploads
415
+ without complaint and then fails the page's processing.
416
+ - **Opaque.** A transparent area shows the Arcade's own bars through it.
417
+ - **Keep the interesting part away from the middle.** The image is scaled to cover the whole play
418
+ area and centred, and your game covers the centre. How much shows either side changes with the
419
+ shape of the player's screen — in a window narrower than the column, the art shows above and
420
+ below instead — so nothing in the art can line up with your game's edge.
421
+ - **Upload with a current `gemctl`.** An older one refuses `portrait_borders` as an unknown key.
422
+
423
+ ### Choosing one from your game
424
+
425
+ Call `gem.showPortraitBorders` at start-up and on every theme change, with the name of the border
426
+ that goes with what is on screen, and `null` for a theme that has no art:
427
+
428
+ ```js
429
+ const BORDERS = new Set(['meadow', 'alpine']); // the names your store page declares
430
+
431
+ function applyTheme(id) {
432
+ document.body.dataset.theme = id;
433
+ gem.showPortraitBorders(BORDERS.has(id) ? id : null); // null: the Arcade's own bars show again
434
+ }
435
+ ```
436
+
437
+ - **Call it unconditionally.** On a phone, or on a screen where your game is not letterboxed, the
438
+ Arcade keeps your choice and shows nothing, and nothing tells your game which case it is in — do
439
+ not look for a signal. `gem.frame.parentSupports('portraitBorders')` says whether this Arcade can
440
+ paint borders at all, never whether it is painting them now.
441
+ - **It is safe at any time.** Before the connection exists your choice is held and sent when the
442
+ Arcade connects. A burst of calls — a theme picker previewing on hover — reaches the Arcade as
443
+ its final value, at most one a second, so it never trips a rate limit. An embedder that paints
444
+ no borders is not told, and nothing is raised.
445
+ - **The Arcade owns the change.** When and how the image swaps is the Arcade's; you get no
446
+ acknowledgement and no say in the timing. A game that restores a saved theme just after it
447
+ starts shows the default for a moment, then its own.
448
+ - **It shows only while your game is playing** — not before play starts, and not while your key
449
+ art covers the play area.
450
+ - **A string that is not a valid name is refused on your side**, with a console warning, and never
451
+ sent. The SDK does not trim it, lower-case it or shorten it.
452
+ - **A valid name your page does not declare is ignored**, and the Arcade keeps what it is showing.
453
+ On the dev and test channels the Arcade's own page logs a console warning naming the value, the
454
+ channel whose page it looked in, and the names that page declares; on release it says nothing.
455
+
456
+ ### Publishing order and experiments
457
+
458
+ - **Publish the store page before, or together with, the build that uses a new name.** A build
459
+ asking for a name its page does not carry yet keeps the current image until the page arrives.
460
+ Rolling a build back is harmless; rolling the page back can remove a name, and the Arcade then
461
+ keeps showing what it has.
462
+ - **Every arm of a store-page experiment must carry every name your build uses.** The set comes
463
+ from the page the player opened, so an arm without a name falls under the rule above.
464
+ - **The borders come from the store page on screen.** To see your dev-channel borders, open your
465
+ game's store page on the dev channel. `gemctl serve` does not letterbox and shows no borders.
466
+
356
467
  ## An achievement unlock — celebrate it yourself, or declare the Arcade's
357
468
 
358
469
  By default the Arcade draws no card when a player unlocks one of your game's
@@ -429,8 +540,10 @@ credit for it until you say so.
429
540
  entry fields few and ordinary — and never gate first play behind one. A player made to type on a
430
541
  remote before they may play mostly does not.
431
542
  - **A TV performance budget.** TV-browser SoCs are weak next to any desktop: ship a player-settable
432
- render-scale option (render at DPR 1 and upscale), bound memory, assume a fixed 16:9 frame — there
433
- is no `orientationchange` rescue coming — and keep your heights `dvh`-safe.
543
+ render-scale option (render at DPR 1 and upscale), bound memory, assume a fixed frame — 16:9, or
544
+ a centred 9:16 column when your store page makes you
545
+ [a letterboxed portrait game](#art-beside-a-portrait-game-on-a-wide-screen-portrait-borders) —
546
+ with no `orientationchange` rescue coming, and keep your heights `dvh`-safe.
434
547
  - **Reduced motion.** TV browsers rarely expose `prefers-reduced-motion`, so the pushed
435
548
  `gem.prefs.reducedMotion` is the only signal a TV player has; honor it. It arrives with
436
549
  [the prefs the parent pushes](#what-the-parent-says-about-the-session-prefs-pause-and-terminate)
@@ -1016,7 +1129,15 @@ renderer serves all of them.
1016
1129
  | `displayTag` | The precomposed `Name#0001` | `PlayerText` |
1017
1130
  | `avatarIconId` | The portrait item's id | `string` |
1018
1131
  | `avatarIconUrl` | The portrait's thumbnail | `string` |
1019
- | `badges` | Equipped badges in display order, each `{ itemId, iconUrl }` — always a list, empty for now | array |
1132
+ | `badges` | Equipped badges, at most two, in the player's order, each `{ itemId, iconUrl }` | array |
1133
+
1134
+ **A player chooses what they wear**: a portrait and up to two badges, each an icon they own — one
1135
+ an achievement in your game or any other may have awarded them ([achievements.md](achievements.md)
1136
+ §1). The portrait arrives as `avatarIconId` and `avatarIconUrl`, and the badges as `badges`, in the
1137
+ order the player put them in. `badges` is always a list — empty for a player who wears none, and on
1138
+ a row whose identity is withheld — so draw what is there, never pad it to two, and key on `itemId`,
1139
+ never on a position. A player can also equip a title, and colours and animations for their name and
1140
+ title. The SDK's identity does not carry those, so draw the name in your own style.
1020
1141
 
1021
1142
  **The name and the tag are player-authored, so both are `PlayerText`** on every surface, not only
1022
1143
  some. Convert with `text.toPlain()` where you render, never before.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "8.0.0",
3
+ "version": "8.1.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -590,7 +590,7 @@
590
590
  "kind": "array",
591
591
  "maxItems": 2,
592
592
  "optional": true,
593
- "$comment": "Equipped badges in display order — empty until platform achievements equip any, and never more than two. Each is an item id and a thumbnail URL, the same pin caveat as `avatarIconUrl`.",
593
+ "$comment": "Equipped badges in the order the player arranged them — never more than two, and empty when the player wears none. Each is an item id and a thumbnail URL, the same pin caveat as `avatarIconUrl`.",
594
594
  "items": {
595
595
  "kind": "object",
596
596
  "fields": {
@@ -2715,6 +2715,47 @@
2715
2715
  "rateLimit": { "perMinute": 120 }
2716
2716
  },
2717
2717
 
2718
+ {
2719
+ "type": "gem.portraitBorders",
2720
+ "direction": "frameToParent",
2721
+ "transport": "port",
2722
+ "$comment": [
2723
+ "Which of the store page's portrait borders the parent paints beside the",
2724
+ "game while it letterboxes a portrait game on a wide screen. `name` is one",
2725
+ "of the names the page declares under `media.portrait_borders`; absent",
2726
+ "means clear the sides to the parent's own bars, for a theme with no art.",
2727
+ "",
2728
+ "A KEY INTO PUBLISHED ART, NEVER THE ART. The images are the store page's,",
2729
+ "served and reviewed like every other store image; the frame chooses only",
2730
+ "which one and when. The parent looks `name` up by exact match in the list",
2731
+ "the page it is showing declares, and never renders it as text or builds a",
2732
+ "URL from it. A name that list does not hold is ignored and the current",
2733
+ "image stays. Transitions, their timing and any minimum dwell are the",
2734
+ "parent's, swaps wait while a trusted overlay is up, and the frame gets no",
2735
+ "acknowledgement.",
2736
+ "",
2737
+ "`name` is a plain bounded string with no format: a border name may carry",
2738
+ "digits and hyphens, which the `code` format refuses. The SDK sends only",
2739
+ "names matching `^[a-z0-9][a-z0-9_-]{0,39}$`, the store page's own rule, and",
2740
+ "a parent checks the same rule on receipt.",
2741
+ "",
2742
+ "STATE, NOT A MOMENT. The SDK holds the game's latest choice until the",
2743
+ "bridge connects and sends at most one a second, latest wins, so a burst",
2744
+ "(a theme picker previewing on hover) delivers its final value well inside",
2745
+ "the limit below.",
2746
+ "",
2747
+ "Gated by `portraitBorders`, which a parent that paints borders advertises",
2748
+ "to EVERY game on every device, whether or not the game declares borders or",
2749
+ "is letterboxed right now. It is never a signal of form factor or letterbox",
2750
+ "state, and the parent answers nothing, so the frame learns nothing about",
2751
+ "how it is being shown."
2752
+ ],
2753
+ "fields": {
2754
+ "name": { "kind": "string", "maxLength": 40, "optional": true }
2755
+ },
2756
+ "rateLimit": { "perMinute": 60 }
2757
+ },
2758
+
2718
2759
  {
2719
2760
  "type": "gem.error",
2720
2761
  "direction": "both",
@@ -123,6 +123,12 @@
123
123
  "gem.frameStats"
124
124
  ]
125
125
  },
126
+ "portraitBorders": {
127
+ "implemented": true,
128
+ "gates": [
129
+ "gem.portraitBorders"
130
+ ]
131
+ },
126
132
  "engagement": {
127
133
  "implemented": true,
128
134
  "gates": [