@indiegems/gem-web-sdk 13.3.3 → 13.4.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/CHANGELOG.md +14 -0
- package/dist/game.d.ts +17 -1
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +3 -3
- package/dist/gem-web-sdk.function.js +1 -1
- package/dist/gem-web-sdk.game.js +3 -3
- package/dist/gem-web-sdk.min.js +3 -3
- package/dist/gem-web-sdk.sdk.js +3 -3
- package/dist/gem-web-sdk.server.js +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/sdk.d.ts +18 -1
- package/dist/server-api.d.ts +1 -0
- package/dist/server.d.ts +1 -0
- package/docs/analytics.md +44 -1
- package/docs/api/api-reference-function.md +1 -1
- package/docs/api/api-reference-game.md +130 -3
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +132 -4
- package/docs/changelog.md +14 -0
- package/docs/getting-started.md +19 -13
- package/docs/inventory-and-catalogs.md +21 -0
- package/docs/manifest.json +18 -18
- package/docs/p2p-and-parties.md +32 -3
- package/docs/rewarded-ads.md +53 -32
- package/docs/rooms-and-matches.md +49 -1
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +115 -41
- package/package.json +1 -1
- package/schema/bridge.v1.json +22 -1
package/docs/rewarded-ads.md
CHANGED
|
@@ -6,7 +6,8 @@ for 3 dice rolls". Pressing it is the player's choice to watch, so the Arcade st
|
|
|
6
6
|
straight away, labelled as an ad, with no prompt of its own first. The platform then decides
|
|
7
7
|
whether the player is rewarded, and writes the reward into **your game's own inventory** for that
|
|
8
8
|
player. Your game never grants the reward, and never learns from the answer whether one was
|
|
9
|
-
granted. It reads its inventory.
|
|
9
|
+
granted. It reads its inventory. The Arcade shows no result once the ad closes, either: it hands
|
|
10
|
+
straight back, and **your game shows the reward** (§3).
|
|
10
11
|
|
|
11
12
|
```js
|
|
12
13
|
watchAdButton.addEventListener('click', async () => {
|
|
@@ -210,19 +211,35 @@ after a press:
|
|
|
210
211
|
|
|
211
212
|
| what happened | was the reward granted? |
|
|
212
213
|
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
213
|
-
| the ad played for its full length | yes, before the answer
|
|
214
|
+
| the ad played for its full length | yes, and usually before the answer; a provider's confirmation can land after it (below) |
|
|
214
215
|
| the player closed the ad early | no: closing early is a skip, and a skip pays nothing |
|
|
215
216
|
| 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" |
|
|
216
217
|
| no ad source serves the channel (§8) | your `without_ads` decides, with no ad |
|
|
217
218
|
| a cap, a cooldown, `max_held`, the platform's spacing (§6), or something about the player | no |
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
The answer is `shown` whenever the Arcade put its ad stage up, however the ad then ended: watched
|
|
221
|
+
to the end, closed early, or no ad could play and `without_ads` decided. It is `no_ad` when no
|
|
222
|
+
stage went up. In some cases the platform also grants with no ad for reasons your game is not
|
|
223
|
+
told. So **branch on your inventory, never on the outcome.** Re-read it once the ask settles,
|
|
224
|
+
whatever it settled as.
|
|
225
|
+
|
|
226
|
+
**The Arcade shows no result: your game shows the reward.** When the ad closes, the Arcade hands
|
|
227
|
+
straight back to your game, for a test ad and a real one alike. It draws no "you received" line and
|
|
228
|
+
no Continue button of its own. While the ad plays it is labelled "Ad", or **TEST AD** for a test
|
|
229
|
+
ad, and after it the only thing the Arcade ever draws is "No ad available right now", when no ad
|
|
230
|
+
could play and nothing was granted. Telling the player what they got is yours: re-read the
|
|
231
|
+
inventory, then show the new token your own way, such as "+1 revive", or use it at once as §5's
|
|
232
|
+
revive does. When the ad played to the end and the grant landed at once, it is already in the
|
|
233
|
+
inventory when the answer arrives.
|
|
221
234
|
|
|
222
235
|
**A reward can also arrive after that re-read.** Some ad providers confirm a finished ad from their
|
|
223
|
-
own server, a little after the player closes it
|
|
224
|
-
|
|
225
|
-
|
|
236
|
+
own server, a little after the player closes it. The Arcade waits a few seconds for that
|
|
237
|
+
confirmation before it hands back, so the reward has usually landed by the time the ask answers,
|
|
238
|
+
but it can land after. So pick the reward up whenever a later read finds it, not only on the
|
|
239
|
+
re-read right after the ask: re-read when the screen that spends the reward opens, show the reward
|
|
240
|
+
then if it is new, and spend a token the player already holds before asking for another (§5's code
|
|
241
|
+
does both). Your game hears of no grant on its own: the SDK's change event fires only for your own
|
|
242
|
+
writes, so a later read is how you find it.
|
|
226
243
|
[`inventory-and-catalogs.md` §6](inventory-and-catalogs.md#6-when-to-re-read) is the general rule.
|
|
227
244
|
|
|
228
245
|
A granted reward shows in your order history like any other change, as an order the platform made
|
|
@@ -413,32 +430,35 @@ The caps are **yours to declare and the platform's to enforce.** The reward is y
|
|
|
413
430
|
economy, so you choose its limits, and the platform holds them on its side, where a modified client
|
|
414
431
|
cannot reach them. They apply the same with an ad and without one.
|
|
415
432
|
|
|
416
|
-
| limit
|
|
417
|
-
|
|
|
418
|
-
| `max_per_day` — grants per player per UTC day
|
|
419
|
-
| `cooldown_seconds` — the time between two grants
|
|
420
|
-
| `max_held` — no grant while the player holds this many
|
|
421
|
-
| `min_watch_secs` — how long a test ad plays before it pays
|
|
422
|
-
|
|
|
423
|
-
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
433
|
+
| limit | declared by | enforced by |
|
|
434
|
+
| ---------------------------------------------------------------------------------------------- | --------------------- | ------------- |
|
|
435
|
+
| `max_per_day` — grants per player per UTC day | you, optionally | the platform |
|
|
436
|
+
| `cooldown_seconds` — the time between two grants | you | the platform |
|
|
437
|
+
| `max_held` — no grant while the player holds this many | you, for item rewards | the platform |
|
|
438
|
+
| `min_watch_secs` — how long a test ad plays before it pays | you, optionally | the platform |
|
|
439
|
+
| a gap between two rewards granted with no ad, only where the platform turns one on (off today) | the platform | the platform |
|
|
440
|
+
| once per run, only on the death screen, never in ranked | you | **your game** |
|
|
441
|
+
|
|
442
|
+
- **The platform makes no player wait between rewarded ads.** A player who has just watched one
|
|
443
|
+
to the end and presses again is offered the next at once. Your placement's own limits are the
|
|
444
|
+
only spacing there is.
|
|
445
|
+
- **A no-ad gap applies only where the platform turns one on, and it is off today.** Set, it
|
|
446
|
+
spaces two rewards granted with no ad (§8) to one player, across all of their games: an ask in
|
|
447
|
+
yours that would grant with no ad inside it answers `no_ad` with nothing granted, even though
|
|
448
|
+
your game has offered nothing. It never touches an ask that plays an ad.
|
|
429
449
|
- **What counts toward a cap** is a grant, after an ad or with none, and an ad that is still
|
|
430
450
|
playing. A grant `without_ads: grant` makes because no ad could play counts the same as any
|
|
431
451
|
other. An ad the player closed early, and a press that paid nothing, do not count, so a skip
|
|
432
|
-
costs the player nothing against your caps
|
|
433
|
-
|
|
452
|
+
costs the player nothing against your caps, and a player who closes the ad early can press
|
|
453
|
+
again at once.
|
|
434
454
|
- **The platform may hold a daily cap lower than the one you declared, never higher.** It sets no
|
|
435
455
|
daily limit of its own unless one is configured, and a configured one applies to every placement
|
|
436
456
|
from the next ask, with no upload from you.
|
|
437
457
|
- ⚠️ **Nothing protects your economy by default but your own caps.** A placement with no
|
|
438
|
-
`max_per_day` and no `cooldown_seconds`
|
|
439
|
-
and under `without_ads: grant`
|
|
440
|
-
banking a second token; it does not stop them spending one and
|
|
441
|
-
daily cap and cooldown your economy needs.
|
|
458
|
+
`max_per_day` and no `cooldown_seconds` pays after every ad watched to the end, as fast as the
|
|
459
|
+
player can watch them, and under `without_ads: grant` on every press, wherever no ad plays.
|
|
460
|
+
`max_held: 1` stops a player banking a second token; it does not stop them spending one and
|
|
461
|
+
earning the next. Declare the daily cap and cooldown your economy needs.
|
|
442
462
|
- **`without_ads: grant` means the reward is fine to give free, within its caps.** Every press
|
|
443
463
|
that finds no ad pays under it, so the caps are all that limit it. Leave the field out, which is
|
|
444
464
|
`nothing`, for a reward that should only ever follow an ad.
|
|
@@ -455,11 +475,11 @@ platform sets them all. A rewarded reward is your economy, so the caps are yours
|
|
|
455
475
|
- **A rewarded ad does not replace the break's platform moment.** Signal the moment from the press
|
|
456
476
|
that leaves the break as usual ([`platform-moments.md`](platform-moments.md)); whether the Arcade
|
|
457
477
|
shows anything then is its decision.
|
|
458
|
-
- **Rewarded has its own spacing:** your placement's `cooldown_seconds
|
|
459
|
-
|
|
460
|
-
a timer. Never ask on a timer at all: the ad would
|
|
461
|
-
ask only when no input came in the last few
|
|
462
|
-
presses again later gets a fresh ask.
|
|
478
|
+
- **Rewarded has its own spacing:** your placement's `cooldown_seconds` and `max_per_day` (§6),
|
|
479
|
+
and nothing of the platform's. An ask inside a cooldown, or past the daily cap, answers `no_ad`
|
|
480
|
+
with nothing granted, so do not re-ask on a timer. Never ask on a timer at all: the ad would
|
|
481
|
+
start with no press. The SDK refuses a timer's ask only when no input came in the last few
|
|
482
|
+
seconds (§3), so do not count on it. A player who presses again later gets a fresh ask.
|
|
463
483
|
- A grant made with no ad holds nothing off, because nothing was shown.
|
|
464
484
|
|
|
465
485
|
## 8. Where an ad plays, and where it does not
|
|
@@ -553,7 +573,8 @@ Play your game on its `dev` or `test` channel. `dev` is open to your team and `t
|
|
|
553
573
|
testers. Pressing the button starts a test ad at once, marked **TEST AD**, which counts down the
|
|
554
574
|
placement's `min_watch_secs` (5 seconds unless you set it). Watch it to the end and the reward
|
|
555
575
|
lands in that channel's inventory. Close it before then and that is a skip, which pays nothing.
|
|
556
|
-
Your game is paused while it plays, and the ask answers when it closes
|
|
576
|
+
Your game is paused while it plays, and the ask answers when it closes, with no result shown by
|
|
577
|
+
the Arcade: your game's re-read finds the reward and shows it.
|
|
557
578
|
|
|
558
579
|
To pick a behaviour before you press, add `gem_test_ads=<mode>` to the play page's address:
|
|
559
580
|
|
|
@@ -983,6 +983,53 @@ template either way.
|
|
|
983
983
|
This is still quick-join: the player is in a room at once, possibly alone. For "any of these modes,
|
|
984
984
|
but never start short", name the same slugs on `queueForMatch()` instead.
|
|
985
985
|
|
|
986
|
+
## 5e. Landing in a friend's room: play-with
|
|
987
|
+
|
|
988
|
+
The Arcade can open your game **to play with someone**: when the player presses Join on a friend
|
|
989
|
+
who is playing it, or answers the prompt the Arcade shows a party member when their leader starts
|
|
990
|
+
it. Your game makes no call for this. The Arcade adds a
|
|
991
|
+
launch key of its own, `gemPlayWith`: an opaque ticket the platform issued for this player, which
|
|
992
|
+
names nobody your game can recognise. The SDK reads it once as `createGem` runs, and every
|
|
993
|
+
`quickJoin()` of the session passes it on: the platform tries the room of the person the ticket
|
|
994
|
+
stands for first, and otherwise quick-joins exactly as it always does.
|
|
995
|
+
|
|
996
|
+
```js
|
|
997
|
+
const room = await gem.rooms.quickJoin({ templateSlug: 'hub' });
|
|
998
|
+
if (gem.rooms.playWith === 'joined') showToast('You joined your friend');
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
- **Their room is tried first, when the player may play with them** — an accepted friend whose
|
|
1002
|
+
presence shows the player that they are in your game, or a member of the same party. The room
|
|
1003
|
+
must be a public one of a template the quick-join named, on the channel being played, that
|
|
1004
|
+
admits a join now (open, or a running match whose template admits late joiners), with a free
|
|
1005
|
+
seat. When it is not, the quick-join goes on as usual: a fuller room, or a new one.
|
|
1006
|
+
- **`gem.rooms.playWith` says which.** `'joined'` — seated in that player's room; `'unavailable'`
|
|
1007
|
+
— their room could not be joined, or the ticket was no longer good, so the quick-join went on as
|
|
1008
|
+
usual; `null` — this quick-join did not ask, because the Arcade did not open the game to play
|
|
1009
|
+
with anyone. It never says why, and it is read-only: there is no option for naming a player
|
|
1010
|
+
yourself. A game that could name one could ask the platform, one friend at a time, where each of
|
|
1011
|
+
them is.
|
|
1012
|
+
- **Who it was stays with the platform.** Neither the launch key, the answer nor anything else of
|
|
1013
|
+
this mechanism names the person: your game learns that it was opened to play with somebody, and
|
|
1014
|
+
whether the quick-join seated the player with them. When it was `'joined'`, the people in the room are its members, as they
|
|
1015
|
+
always are.
|
|
1016
|
+
- **The two share a `groupId`.** A member who came to play with someone carries the same
|
|
1017
|
+
`RoomMember.groupId` as them, as a party's members do, so a roster grouped by it already shows
|
|
1018
|
+
them together. It is not a party: either of them leaves alone.
|
|
1019
|
+
- **No `seated` fires for it.** `seated` reports a seat your game did not make; this one your own
|
|
1020
|
+
`quickJoin()` made, and its answer is the room.
|
|
1021
|
+
- **Only quick-join takes part.** `create()`, `join()`, `joinByCode()` and `queueForMatch()`
|
|
1022
|
+
never try a friend's room, and a party leader's quick-join brings the party into a room of its
|
|
1023
|
+
own rather than joining anyone else's.
|
|
1024
|
+
- **Never read `gemPlayWith` from the URL yourself, and never define a key of that name.** It is
|
|
1025
|
+
the platform's, like `gemChallenge`
|
|
1026
|
+
([`getting-started.md`](getting-started.md#linking-to-your-game)).
|
|
1027
|
+
|
|
1028
|
+
A game that quick-joins as it starts — a hub, a drop-in lobby — gets all of this with no change.
|
|
1029
|
+
One that shows a mode picker first keeps it: the friend's room is tried for the modes the player
|
|
1030
|
+
picks, and a friend playing another mode reads `'unavailable'`. Naming several modes in one
|
|
1031
|
+
quick-join (§5d) tries the friend's room for any of them.
|
|
1032
|
+
|
|
986
1033
|
## 6. Scope
|
|
987
1034
|
|
|
988
1035
|
**In:** rooms, membership, host, ready state, the match lifecycle, and the dedicated-server connect
|
|
@@ -997,7 +1044,8 @@ skill rating, and inventing one is not a small addition to this.
|
|
|
997
1044
|
**Elsewhere:** P2P _connection_ and parties. Both are
|
|
998
1045
|
[`p2p-and-parties.md`](p2p-and-parties.md). Three things surface here:
|
|
999
1046
|
|
|
1000
|
-
- **A member's group, not their party.** `RoomMember.groupId` says who arrived together
|
|
1047
|
+
- **A member's group, not their party.** `RoomMember.groupId` says who arrived together — as a
|
|
1048
|
+
party, or one coming to play with the other (§5e) — as an opaque
|
|
1001
1049
|
label that is stable for the life of this room and unrelated to the label the same people carry in
|
|
1002
1050
|
any other room. It is gem-api's, read off the wire; it replaced a pass-through of the real
|
|
1003
1051
|
`party_id`, which is account-level and cross-game and therefore a social-graph key rather than a
|
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.4.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v13.4.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -729,7 +729,7 @@ the `var()` above, or read `gem.viewport`. The release checklist looks for the b
|
|
|
729
729
|
The Arcade keeps **one control of its own over every game**, on every device — a phone's full-bleed
|
|
730
730
|
game, a framed or full-window game on a desktop, a tablet or a TV in native fullscreen:
|
|
731
731
|
|
|
732
|
-
- **The Gem button**, the Gem Arcade's gem, always there (a touch target
|
|
732
|
+
- **The Gem button**, the Gem Arcade's gem, always there (a 44px square touch target), in
|
|
733
733
|
the **top-left corner** unless your game [asks for another](#moving-the-gem-gemsetgemcorner). It
|
|
734
734
|
opens [the Arcade's menu](#the-arcades-menu): Return to the Arcade, the player's Gems, Tickets and
|
|
735
735
|
achievement score, mute, the player's Arcade-wide settings and the rest. Not your settings: those
|
|
@@ -756,7 +756,8 @@ where "back" and "settings" are once, across every game:
|
|
|
756
756
|
Arcade's menu, so a player always knows which control does which.
|
|
757
757
|
- **Whichever of your controls shares the Gem's corner sits level with the Gem, on the side toward
|
|
758
758
|
the centre.** With the Gem top-left, nothing of yours does. With the Gem top-right, your Back and
|
|
759
|
-
settings sit just left of it.
|
|
759
|
+
settings sit just left of it. "Level" means centred on the Gem button's own middle, which the
|
|
760
|
+
SDK gives you as `--gem-reserved-tl-center-y` (below), never on the middle of its reserved area.
|
|
760
761
|
- **Your settings button is there on every screen.** It goes top-right even where no Arcade control
|
|
761
762
|
is: another embedder may keep one in that corner, so place it from the reserved areas below and it
|
|
762
763
|
moves aside for whatever is there.
|
|
@@ -769,32 +770,72 @@ into the camera band, but that edge is somewhere else on every device — read t
|
|
|
769
770
|
An inset cannot say that — as a left or top inset, a button a few dozen pixels wide would take a whole
|
|
770
771
|
strip of your screen. So `gem.viewport` also carries the rectangles, in the same CSS pixels of your
|
|
771
772
|
frame, measured from its top-left corner — one for the Gem button, and one for the frame-rate
|
|
772
|
-
readout while the player shows it.
|
|
773
|
-
|
|
774
|
-
|
|
773
|
+
readout while the player shows it.
|
|
774
|
+
|
|
775
|
+
**Each rectangle is a keep-out area, not the control.** It is grown out to the frame edges it sits
|
|
776
|
+
nearest, because the sliver between the Gem and the edge is no place for a button either. So its
|
|
777
|
+
middle is not the button's middle, and nothing should be lined up with it. Beside it, `control`
|
|
778
|
+
is the control's own box, as drawn and not grown: that is what you line up with. On a notched phone
|
|
779
|
+
in portrait, with the Gem top-left, the Gem sits 8px in from the left and at the foot of the
|
|
780
|
+
47px inset:
|
|
775
781
|
|
|
776
782
|
```json
|
|
777
783
|
{
|
|
778
784
|
"insets": { "top": 47, "right": 0, "bottom": 34, "left": 0 },
|
|
779
|
-
"reserved": [
|
|
785
|
+
"reserved": [
|
|
786
|
+
{
|
|
787
|
+
"x": 0,
|
|
788
|
+
"y": 0,
|
|
789
|
+
"width": 52,
|
|
790
|
+
"height": 91,
|
|
791
|
+
"control": { "x": 8, "y": 47, "width": 44, "height": 44 }
|
|
792
|
+
}
|
|
793
|
+
]
|
|
780
794
|
}
|
|
781
795
|
```
|
|
782
796
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
797
|
+
Here the area's middle is about 46px down and the button's is 69px down, some 23px apart: a top bar
|
|
798
|
+
centred on the area would have its middle level with the top of the Gem. The numbers change with the phone, the
|
|
799
|
+
orientation, a letterboxed game and a television, so read them rather than copying these.
|
|
800
|
+
`control` is absent under an arcade or embedder that does not send it yet; then you have the
|
|
801
|
+
keep-out area alone.
|
|
802
|
+
|
|
803
|
+
**The SDK turns them into custom properties per corner**, written beside `--gem-safe-*` before
|
|
804
|
+
anything else hears the push. A rectangle belongs to the corner its centre lies nearest to.
|
|
805
|
+
|
|
806
|
+
| Property (and the same for `tr`, `bl`, `br`) | What it is |
|
|
807
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
808
|
+
| `--gem-reserved-tl-width` | How far that corner's keep-out areas reach in from the side edge. Keep your controls outside it. |
|
|
809
|
+
| `--gem-reserved-tl-height` | How far they reach down from the top edge (up from the bottom edge, for `bl` and `br`). |
|
|
810
|
+
| `--gem-reserved-tl-center-y` | The vertical middle of that corner's controls themselves, from your frame's top edge. Line things up on it. |
|
|
811
|
+
| `--gem-reserved-tl-control-height` | Those controls' height: the Gem button's, or the Gem and the frame-rate readout together when both are there. |
|
|
812
|
+
|
|
813
|
+
To line a top bar up with the Gem, give it the Gem's height and centre it on the Gem's middle. Put
|
|
814
|
+
the bar between the two top corners' keep-out areas, and it is right wherever the Gem is:
|
|
788
815
|
|
|
789
816
|
```css
|
|
790
|
-
/* The
|
|
817
|
+
/* The Gem's middle and height, from whichever top corner holds it. The fallback is the Arcade's
|
|
818
|
+
own placement on a full-bleed game, for an embedder that does not send the control's box.
|
|
819
|
+
On :root, where the SDK writes its properties, so every element below can use them. */
|
|
820
|
+
:root {
|
|
821
|
+
--gem-mid: var(
|
|
822
|
+
--gem-reserved-tl-center-y,
|
|
823
|
+
var(
|
|
824
|
+
--gem-reserved-tr-center-y,
|
|
825
|
+
calc(max(8px, var(--gem-safe-top, env(safe-area-inset-top, 0px))) + 22px)
|
|
826
|
+
)
|
|
827
|
+
);
|
|
828
|
+
--gem-size: var(--gem-reserved-tl-control-height, var(--gem-reserved-tr-control-height, 44px));
|
|
829
|
+
}
|
|
830
|
+
/* The top row: level with the Gem, clear of whatever the Arcade reserves in either top corner */
|
|
791
831
|
#top-row {
|
|
792
832
|
position: absolute;
|
|
793
|
-
top: var(--gem-
|
|
833
|
+
top: calc(var(--gem-mid) - var(--gem-size) / 2);
|
|
834
|
+
height: var(--gem-size);
|
|
794
835
|
left: max(var(--gem-safe-left, 0px), var(--gem-reserved-tl-width, 0px));
|
|
795
836
|
right: max(var(--gem-safe-right, 0px), var(--gem-reserved-tr-width, 0px));
|
|
796
837
|
display: flex;
|
|
797
|
-
align-items: center;
|
|
838
|
+
align-items: center; /* anything taller than the Gem still centres on its middle */
|
|
798
839
|
gap: 16px;
|
|
799
840
|
}
|
|
800
841
|
/* your information takes the middle; Back, then settings, at the right end */
|
|
@@ -804,6 +845,18 @@ nearest to. Lay the top row out between the two top corners, and it is right whe
|
|
|
804
845
|
}
|
|
805
846
|
```
|
|
806
847
|
|
|
848
|
+
With the Gem in a bottom corner, neither top corner has a control, and the fallback puts the row
|
|
849
|
+
where the Gem would sit at the top. From script, read the box itself:
|
|
850
|
+
|
|
851
|
+
```js
|
|
852
|
+
// The Gem's own box: the tallest control on the top edge (the frame-rate readout is smaller)
|
|
853
|
+
const boxes = (gem.viewport?.reserved ?? []).filter((r) => r.y === 0 && r.control);
|
|
854
|
+
const gemBox = boxes.map((r) => r.control).sort((a, b) => b.height - a.height)[0];
|
|
855
|
+
const middle = gemBox
|
|
856
|
+
? gemBox.y + gemBox.height / 2
|
|
857
|
+
: Math.max(8, gem.viewport?.insets.top ?? 0) + 22;
|
|
858
|
+
```
|
|
859
|
+
|
|
807
860
|
The per-corner properties say how far a corner's rectangles reach, not how much room is left beside
|
|
808
861
|
them. To know whether your top row fits between the Arcade's controls, read the rectangles from
|
|
809
862
|
script: the room is what lies between the top rectangles on the left and those on the right.
|
|
@@ -817,9 +870,12 @@ const to = Math.min(w, ...top.filter((r) => !isLeft(r)).map((r) => r.x));
|
|
|
817
870
|
row.classList.toggle('below-arcade-row', to - from < row.scrollWidth + 8);
|
|
818
871
|
```
|
|
819
872
|
|
|
820
|
-
|
|
821
|
-
a stale value; they are never set under an embedder that sends no viewport,
|
|
822
|
-
|
|
873
|
+
The eight width and height properties are written on every push, `0px` for a clear corner, so a
|
|
874
|
+
rotation never leaves a stale value; they are never set under an embedder that sends no viewport,
|
|
875
|
+
which is what the `0px` fallback is for. The centre and control height are different: a corner with
|
|
876
|
+
no control in it, or one whose embedder sends no control box, has them **removed** rather than set to
|
|
877
|
+
`0px` (a middle of 0 would put your bar half off the screen), so your `var()` fallback stands.
|
|
878
|
+
`gem.viewport.reserved` is `[]` when nothing is covered.
|
|
823
879
|
|
|
824
880
|
**Lay out against it whenever it changes.** The Gem button's rectangle is there for the whole
|
|
825
881
|
session, and it moves with a rotation, a resize, the frame-rate readout and a change of corner. Read
|
|
@@ -978,12 +1034,14 @@ This is the whole model, and every other page that mentions settings links here:
|
|
|
978
1034
|
**The button goes in the top-right corner**, the same corner in every game, so a player never has
|
|
979
1035
|
to look for it, with your Back button just to its left when a screen has one. Place it with the
|
|
980
1036
|
reserved-area properties so it never sits under an Arcade control: if the Gem is in that corner, the
|
|
981
|
-
button sits level with it, just inside it.
|
|
1037
|
+
button sits level with it, just inside it. Inside [the top row](#the-top-of-the-screen) it is
|
|
1038
|
+
level already; placed on its own, centre it on the same middle:
|
|
982
1039
|
|
|
983
1040
|
```css
|
|
984
1041
|
#settings-button {
|
|
985
1042
|
position: absolute;
|
|
986
|
-
top: var(--gem-
|
|
1043
|
+
top: var(--gem-mid); /* defined on :root, as for the top row */
|
|
1044
|
+
transform: translateY(-50%);
|
|
987
1045
|
right: max(var(--gem-safe-right, 0px), var(--gem-reserved-tr-width, 0px));
|
|
988
1046
|
}
|
|
989
1047
|
```
|
|
@@ -994,19 +1052,19 @@ The player sets their defaults once, in the Arcade's Settings, separately for ea
|
|
|
994
1052
|
and a television can say different things. They reach your game as `gem.prefs.recommended`, in the
|
|
995
1053
|
same push as `muted` and `reducedMotion`, and again whenever the player changes one:
|
|
996
1054
|
|
|
997
|
-
| Field
|
|
998
|
-
|
|
|
999
|
-
| `volume`
|
|
1000
|
-
| `batterySaver`
|
|
1001
|
-
| `keyboardLayout`
|
|
1002
|
-
| `gamepadConfirm`
|
|
1003
|
-
| `vibration`
|
|
1004
|
-
| `lookSensitivity`
|
|
1005
|
-
| `gamepadLookSensitivity` | `20` to `400`
|
|
1006
|
-
| `invertLookY`
|
|
1007
|
-
| `touchMovement`
|
|
1008
|
-
| `graphicsQuality`
|
|
1009
|
-
| `graphicsQualityAuto`
|
|
1055
|
+
| Field | Values | What it suggests |
|
|
1056
|
+
| ------------------------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
1057
|
+
| `volume` | `0` to `100` | Your master volume's starting level, as a percentage — divide by 100 for a gain. A level, never a mute. |
|
|
1058
|
+
| `batterySaver` | `true` / `false` | Spend less power: a lower frame rate, fewer effects, a lower resolution. |
|
|
1059
|
+
| `keyboardLayout` | `'wasd'` / `'arrows'` | Which keys move by default. |
|
|
1060
|
+
| `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. |
|
|
1061
|
+
| `vibration` | `true` / `false` | Your vibration toggle's starting state ([if your game vibrates](#if-your-game-vibrates)). |
|
|
1062
|
+
| `lookSensitivity` | `20` to `400` | Look speed for touch, mouse and pen, as a percentage of the base speeds: `100` is 1×. |
|
|
1063
|
+
| `gamepadLookSensitivity` | `20` to `400` | Look speed for a gamepad stick, as a percentage: `100` is 1×. |
|
|
1064
|
+
| `invertLookY` | `true` / `false` | Up/down look inverted: pushing up or dragging up looks down. Pitch only, never yaw. |
|
|
1065
|
+
| `touchMovement` | `'floating'` / `'fixed'` | On a touch screen, a movement stick that appears where the thumb lands, or one fixed in place. |
|
|
1066
|
+
| `graphicsQuality` | `'low'` / `'medium'` / `'high'` | The graphics quality to start at on this device. |
|
|
1067
|
+
| `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. |
|
|
1010
1068
|
|
|
1011
1069
|
`lookSensitivity` to `touchMovement` are the camera and controls settings: see
|
|
1012
1070
|
[camera and look controls](#camera-and-look-controls) for `gem.controls.look()`, which applies the
|
|
@@ -1034,8 +1092,15 @@ sends neither; the SDK then fills `graphicsQuality` from `batterySaver` (`'low'`
|
|
|
1034
1092
|
|
|
1035
1093
|
```js
|
|
1036
1094
|
const DEFAULTS = {
|
|
1037
|
-
volume: 100,
|
|
1038
|
-
|
|
1095
|
+
volume: 100,
|
|
1096
|
+
batterySaver: false,
|
|
1097
|
+
keyboardLayout: 'wasd',
|
|
1098
|
+
gamepadConfirm: 'south',
|
|
1099
|
+
vibration: true,
|
|
1100
|
+
lookSensitivity: 100,
|
|
1101
|
+
gamepadLookSensitivity: 100,
|
|
1102
|
+
invertLookY: false,
|
|
1103
|
+
touchMovement: 'floating',
|
|
1039
1104
|
graphicsQuality: 'medium',
|
|
1040
1105
|
};
|
|
1041
1106
|
let chosen = {}; // ONLY what the player picked in your settings screen
|
|
@@ -1106,11 +1171,16 @@ function turn({ yaw: dYaw, pitch: dPitch }) {
|
|
|
1106
1171
|
|
|
1107
1172
|
// Touch and pen: the change since the last event. A mouse: movementX/Y under pointer lock.
|
|
1108
1173
|
let last = null;
|
|
1109
|
-
canvas.addEventListener('pointerdown', (e) => {
|
|
1110
|
-
|
|
1174
|
+
canvas.addEventListener('pointerdown', (e) => {
|
|
1175
|
+
last = { x: e.clientX, y: e.clientY };
|
|
1176
|
+
});
|
|
1177
|
+
canvas.addEventListener('pointerup', () => {
|
|
1178
|
+
last = null;
|
|
1179
|
+
});
|
|
1111
1180
|
canvas.addEventListener('pointermove', (e) => {
|
|
1112
1181
|
if (e.pointerType === 'mouse') {
|
|
1113
|
-
if (document.pointerLockElement === canvas)
|
|
1182
|
+
if (document.pointerLockElement === canvas)
|
|
1183
|
+
turn(look.fromPointer(e.movementX, e.movementY, 'mouse'));
|
|
1114
1184
|
return;
|
|
1115
1185
|
}
|
|
1116
1186
|
if (!last) return;
|
|
@@ -1400,6 +1470,7 @@ Arcade, which draws a small readout in its own chrome. **Your game draws nothing
|
|
|
1400
1470
|
a second call for one frame counts it twice. It is free while the overlay is off — a flag check,
|
|
1401
1471
|
nothing sent — so leave it in your loop. A worker renderer posts to the main thread when it draws
|
|
1402
1472
|
and calls it there.
|
|
1473
|
+
|
|
1403
1474
|
- **Do not draw an FPS counter of your own over the Arcade's.** Two readouts that disagree — and
|
|
1404
1475
|
they will, since they count different things — teach a player that one of them is lying. A debug
|
|
1405
1476
|
overlay in your own builds is fine; keep it off in an Arcade build, or at least off by default.
|
|
@@ -1557,7 +1628,9 @@ first:
|
|
|
1557
1628
|
const services = context.sourceCode.parserServices;
|
|
1558
1629
|
const checker = services.program.getTypeChecker();
|
|
1559
1630
|
const isPlayerText = (type) =>
|
|
1560
|
-
(type.isUnion() ? type.types : [type]).some(
|
|
1631
|
+
(type.isUnion() ? type.types : [type]).some(
|
|
1632
|
+
(t) => t.getSymbol()?.getName() === 'PlayerText',
|
|
1633
|
+
);
|
|
1561
1634
|
const typeOf = (node) => checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(node));
|
|
1562
1635
|
const check = (node) => {
|
|
1563
1636
|
if (node && isPlayerText(typeOf(node))) context.report({ node, messageId: 'stringified' });
|
|
@@ -1607,6 +1680,7 @@ first:
|
|
|
1607
1680
|
|
|
1608
1681
|
It needs type information, so it lints TypeScript only. In plain JavaScript, rely on the console
|
|
1609
1682
|
warning.
|
|
1683
|
+
|
|
1610
1684
|
- **The field list.** The package carries `docs/player-text-fields.json`: every field that holds
|
|
1611
1685
|
`PlayerText`, by entry (`sdk` is `@indiegems/gem-web-sdk/sdk`), each with the first release in
|
|
1612
1686
|
which that entry shipped it as `PlayerText`. Search your code for every field it lists, not only
|
|
@@ -1651,9 +1725,9 @@ accepts, uploading and selling your own, the purchase flow and loading the model
|
|
|
1651
1725
|
has **two tabs**, and they are **two separate worn choices** — wearing one never changes the
|
|
1652
1726
|
other:
|
|
1653
1727
|
|
|
1654
|
-
| Tab
|
|
1655
|
-
|
|
1656
|
-
| **2D** | a portrait — a picture
|
|
1728
|
+
| Tab | What the player wears | On `gem.player` and every room member |
|
|
1729
|
+
| ------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
1730
|
+
| **2D** | a portrait — a picture | `avatarIconId`, `avatarIconUrl` (`avatar_icon_id`, `avatar_icon_url` on the wire) |
|
|
1657
1731
|
| **3D** | a 3D character — a rigged model | `avatar3dId`, `avatar3dModelUrl`, `avatar3dIconUrl` (`avatar_3d_id`, `avatar_3d_model_url`, `avatar_3d_icon_url`) |
|
|
1658
1732
|
|
|
1659
1733
|
- **`avatar3dId`** is the worn 3D character's item id. Absent means none is worn — draw your
|
package/package.json
CHANGED
package/schema/bridge.v1.json
CHANGED
|
@@ -1250,7 +1250,28 @@
|
|
|
1250
1250
|
"x": { "kind": "int", "min": 0, "max": 16384 },
|
|
1251
1251
|
"y": { "kind": "int", "min": 0, "max": 16384 },
|
|
1252
1252
|
"width": { "kind": "int", "min": 0, "max": 16384 },
|
|
1253
|
-
"height": { "kind": "int", "min": 0, "max": 16384 }
|
|
1253
|
+
"height": { "kind": "int", "min": 0, "max": 16384 },
|
|
1254
|
+
"control": {
|
|
1255
|
+
"kind": "object",
|
|
1256
|
+
"optional": true,
|
|
1257
|
+
"$comment": [
|
|
1258
|
+
"The box of the control this area protects, as drawn: NOT grown to",
|
|
1259
|
+
"the frame's edges, clipped to the frame, in the same space and",
|
|
1260
|
+
"rounded outward like the area. The area is a keep-out zone, grown",
|
|
1261
|
+
"to the nearest edges so nothing lands in the sliver between the",
|
|
1262
|
+
"control and the edge; its middle is not the control's middle. This",
|
|
1263
|
+
"is what a game lines its own controls up with, such as a top bar",
|
|
1264
|
+
"centred on the Gem button. Additive: absent from an older parent,",
|
|
1265
|
+
"which leaves the game knowing only the keep-out area, and ignored",
|
|
1266
|
+
"by an older SDK, which never reads it."
|
|
1267
|
+
],
|
|
1268
|
+
"fields": {
|
|
1269
|
+
"x": { "kind": "int", "min": 0, "max": 16384 },
|
|
1270
|
+
"y": { "kind": "int", "min": 0, "max": 16384 },
|
|
1271
|
+
"width": { "kind": "int", "min": 0, "max": 16384 },
|
|
1272
|
+
"height": { "kind": "int", "min": 0, "max": 16384 }
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1254
1275
|
}
|
|
1255
1276
|
}
|
|
1256
1277
|
}
|