@indiegems/gem-web-sdk 23.2.0 → 24.0.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 +48 -0
- package/dist/conformance.d.ts +5 -2
- package/dist/function.d.ts +22 -1
- package/dist/game.d.ts +1590 -1466
- package/dist/gem-web-sdk.bridge.js +2 -2
- package/dist/gem-web-sdk.conformance.js +3 -3
- package/dist/gem-web-sdk.esm.js +3 -3
- package/dist/gem-web-sdk.function.js +3 -3
- 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 +2 -2
- package/dist/index.d.ts +8 -1
- package/dist/sdk.d.ts +1600 -1464
- package/dist/server.d.ts +44 -1
- package/docs/analytics.md +40 -0
- package/docs/api/api-reference-function.md +196 -45
- package/docs/api/api-reference-game.md +778 -33
- package/docs/api/api-reference-server.md +477 -3
- package/docs/api/reference.json +1833 -102
- package/docs/arcade-and-store.md +166 -61
- package/docs/arcade-economy.md +184 -184
- package/docs/changelog.md +48 -0
- package/docs/deprecation-policy.md +2 -0
- package/docs/getting-started.md +1 -1
- package/docs/inventory-and-catalogs.md +247 -76
- package/docs/manifest.json +24 -24
- package/docs/platform-rules.md +36 -27
- package/docs/player-sign-in.md +60 -3
- package/docs/rewarded-ads.md +32 -1
- package/docs/server-bundles.md +9 -7
- package/docs/what-a-game-must-do.md +7 -4
- package/package.json +1 -1
- package/schema/bridge.v1.json +97 -7
|
@@ -141,7 +141,7 @@ gem.me.inventory.onChanged(async () => {
|
|
|
141
141
|
|
|
142
142
|
It is a cue to re-read, not a receipt, and it arrives a moment after the change commits. It is
|
|
143
143
|
only ever about **this** game, on the channel you are playing: the Arcade never relays another
|
|
144
|
-
game's changes or the platform wallet's (
|
|
144
|
+
game's changes or the platform wallet's (Coins have their own hint,
|
|
145
145
|
`gem.platform.earnAllowance.on('changed')`). A burst — a function granting in a loop — reaches
|
|
146
146
|
you as one call now and at most one more every two seconds, and the hint itself makes no request.
|
|
147
147
|
|
|
@@ -334,6 +334,77 @@ fails. The platform checks prerequisites **before it charges anything**: a recip
|
|
|
334
334
|
currency (§16) that the player is locked out of is refused with the code `recipe_locked`, and no
|
|
335
335
|
currency moves.
|
|
336
336
|
|
|
337
|
+
### Requirements: `requires`
|
|
338
|
+
|
|
339
|
+
New config states what a player must hold or have done with one vocabulary, `requires`: a list
|
|
340
|
+
of requirements that must **all** hold. Items, recipes and progression tracks may carry one, and so
|
|
341
|
+
may each bonus (below).
|
|
342
|
+
|
|
343
|
+
| `kind` | Holds when the player… |
|
|
344
|
+
| ---------------------- | ------------------------------------------------------------------------------------ |
|
|
345
|
+
| `holds_pass` | holds the pass `pass` now, e.g. `{ kind: 'holds_pass', pass: 'vip' }` |
|
|
346
|
+
| `owns_platform_item` | owns the platform item `item_definition_id`; only one this game grants or sells |
|
|
347
|
+
| `owns_item` | holds at least `min` of the game item `item_definition_id` |
|
|
348
|
+
| `has_currency` | holds at least `min` of the **game** currency `currency_id`; a platform currency is refused |
|
|
349
|
+
| `progression_complete` | has completed the progression `progression_definition_id` |
|
|
350
|
+
| `progression_level` | is at or above `min` on the progression `progression_definition_id`: its track level, or its value when it has no tracks |
|
|
351
|
+
|
|
352
|
+
- **The platform judges it, when the recipe runs.** Not the browser: a requirement is the one place
|
|
353
|
+
a gate on a pass is enforced.
|
|
354
|
+
- **A refusal or a receipt names an unmet requirement by its index and kind**, never by the pass or
|
|
355
|
+
item it names.
|
|
356
|
+
- **The older shapes still work.** A recipe's `prerequisites`, a loot entry's exclusion conditions
|
|
357
|
+
and a track's `requires_item_id` are read as they always were; `gemctl` warns on `prerequisites`
|
|
358
|
+
in a new upload. Write `requires` in new config.
|
|
359
|
+
|
|
360
|
+
### Bonuses: more for a player who meets a requirement
|
|
361
|
+
|
|
362
|
+
A recipe or a loot table may carry `bonuses`: extra grants for a player who meets each bonus's own
|
|
363
|
+
`requires`. VIP is just `holds_pass: vip`; owning a charm, or reaching a level, works the same way.
|
|
364
|
+
|
|
365
|
+
```jsonc
|
|
366
|
+
// a recipe
|
|
367
|
+
{
|
|
368
|
+
"id": "open_chest",
|
|
369
|
+
"costs": [{ "type": "item_remove", "item_definition_id": "<chest_key>", "quantity": 1 }],
|
|
370
|
+
"loot_table_id": "<chest_common>",
|
|
371
|
+
"bonuses": [
|
|
372
|
+
{
|
|
373
|
+
"id": "vip_roll",
|
|
374
|
+
"requires": [{ "kind": "holds_pass", "pass": "vip" }],
|
|
375
|
+
"grants": [{ "type": "roll", "loot_table_id": "<chest_common>", "count": 1 }]
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"id": "lucky_charm",
|
|
379
|
+
"requires": [{ "kind": "owns_item", "item_definition_id": "<lucky_charm>", "min": 1 }],
|
|
380
|
+
"grants": [{ "type": "item_grant", "item_definition_id": "<rare_shard>", "quantity": 1, "account_bound": true }]
|
|
381
|
+
}
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
- **A bonus never fails the base.** The recipe runs exactly as it would without bonuses; each bonus
|
|
387
|
+
is then judged on the player's state from before the recipe ran, applied or skipped on its own,
|
|
388
|
+
and a skipped bonus changes nothing else. The order's `changes` carry one `bonus` entry per
|
|
389
|
+
bonus: `{ bonus_id, outcome: 'applied' | 'skipped', reason?, unmet: [{ index, kind }] }`, so the
|
|
390
|
+
game can say "VIP bonus: +1 roll".
|
|
391
|
+
- **What a bonus may grant:** game items, game currencies, and `roll`, which rolls a loot table
|
|
392
|
+
`count` times. Never a platform currency, and never a write to a progression.
|
|
393
|
+
- **A loot table's bonuses grant extra rolls only**, never extra entries, so the base roll's odds
|
|
394
|
+
never move. A `roll`'s table may not carry bonuses of its own: one level deep.
|
|
395
|
+
- **Limits:** at most 8 bonuses on a recipe or a table, and 10 rolls per bonus. A bonus that grants
|
|
396
|
+
what a bonus requires is refused, and a VIP bonus that grants a transferable item is warned about:
|
|
397
|
+
mark it `account_bound`.
|
|
398
|
+
- **Rolls are their own.** Each bonus rolls from its own random stream, after every base draw, and
|
|
399
|
+
never reads or moves pity, so adding, removing or skipping a bonus never changes the base's
|
|
400
|
+
outcome or another bonus's.
|
|
401
|
+
- **The platform holds nothing.** A bonus is judged when the recipe runs and never later: there is
|
|
402
|
+
no pending or missed bonus and no voucher. To sell VIP at that moment, offer it **before** the
|
|
403
|
+
recipe runs ([the upsell](#offering-vip-before-a-reward-the-upsell)).
|
|
404
|
+
- **A paid roll is gated.** On a recipe priced in Coins, a bonus that rolls is skipped with
|
|
405
|
+
`paid_chance_restricted` for a known minor and in restricted regions; the base of a free recipe is
|
|
406
|
+
never affected.
|
|
407
|
+
|
|
337
408
|
## 10. Orders — the record that something happened
|
|
338
409
|
|
|
339
410
|
Every change to a player's holdings produces an **order**. Order history is read-only and answers
|
|
@@ -410,6 +481,12 @@ because the config does not re-poll — `gem.gameConfig.refresh()` is the only t
|
|
|
410
481
|
it. Show this number as a "base chance," or not as a number at
|
|
411
482
|
all. The only authority on what a player actually receives is the result of executing the recipe.
|
|
412
483
|
|
|
484
|
+
**Show the odds before a player pays for a roll.** A recipe priced in Coins that rolls a table, or
|
|
485
|
+
carries a bonus that rolls one, is a paid random item: the arcade's purchase sheet shows the odds
|
|
486
|
+
from the published weights before the player pays, and your shop and any upsell show them too. The
|
|
487
|
+
platform refuses a paid roll with `paid_chance_restricted` for a known minor and in restricted
|
|
488
|
+
regions (a bonus roll is skipped instead), so hide such an offer rather than drawing a dead button.
|
|
489
|
+
|
|
413
490
|
## 13. Progressions
|
|
414
491
|
|
|
415
492
|
A **progression** is a track a player advances along — a battle pass, a level, a mastery. You read
|
|
@@ -469,7 +546,7 @@ is `gemctl platform config`'s answer.
|
|
|
469
546
|
so this page does not list them. Name one that is not permitted and the config upload is refused
|
|
470
547
|
with an error that enumerates the ones that are.
|
|
471
548
|
|
|
472
|
-
|
|
549
|
+
Coins can be your game's soft currency, awarded through a mapping. A game currency of your own suits
|
|
473
550
|
what lives inside one play loop — gold picked up and spent within a run, energy, crafting
|
|
474
551
|
materials: you control it completely, no other game can spend it, and none of the constraints below
|
|
475
552
|
apply to it.
|
|
@@ -496,7 +573,7 @@ What that means for game code:
|
|
|
496
573
|
happens to add to the source.
|
|
497
574
|
- **The channel does not change where a grant lands.** A mapping fires on `dev`, `test` and
|
|
498
575
|
`release` alike, into the player's real platform balance, against the real per-game cap — so
|
|
499
|
-
|
|
576
|
+
Coins paid while you test are real, and the allowance they spend is not given back.
|
|
500
577
|
- The order your server gets back carries one `platform_grant` entry in `changes` per mapping:
|
|
501
578
|
`fired`, `clamped` (and which ceiling bit — the per-firing ceiling, the platform's per-game
|
|
502
579
|
ceiling or its daily maximum across all games, or the player's holdings cap), or `suppressed`
|
|
@@ -526,13 +603,66 @@ What that means for game code:
|
|
|
526
603
|
hold** — within a window it only rises, resetting or lowering the source progression never takes
|
|
527
604
|
it back, and a player spending a granted currency does not refill your game's allowance.
|
|
528
605
|
|
|
606
|
+
### Showing a payout before and after: `preview` and `fromReceipt`
|
|
607
|
+
|
|
608
|
+
**These two are the only way to show a platform payout.** Never multiply by `gem.player.boost`, and
|
|
609
|
+
never copy a config amount into your game's code: the platform retunes amounts and multipliers
|
|
610
|
+
without a release of your game, and both go stale the moment it does. The release review flags
|
|
611
|
+
hand-written multiplier code.
|
|
612
|
+
|
|
613
|
+
Before the claim, give the preview a **config id** — never an amount, never a pass:
|
|
614
|
+
|
|
615
|
+
```js
|
|
616
|
+
const preview = await gem.gameConfig.preview({ recipe: 'open_chest' });
|
|
617
|
+
// or { progression: 'run_complete' } — an `on_complete` mapping
|
|
618
|
+
// or { progression: 'puzzles_cleared', add: 3 } — an `on_add` mapping, with the delta you will apply
|
|
619
|
+
|
|
620
|
+
for (const p of preview.platformGrants) {
|
|
621
|
+
// { currencySlug: 'ARCADE_TICKETS', base: 30, total: 45,
|
|
622
|
+
// breakdown: [{ source: 'pass', pass: 'vip', amount: 15 }], ifComplete?: true }
|
|
623
|
+
rewardRow.show(p.total, p.breakdown.map((b) => (b.source === 'pass' ? 'VIP' : 'Boost')));
|
|
624
|
+
}
|
|
625
|
+
for (const b of preview.bonuses) {
|
|
626
|
+
// { id, grants, eligible: true | false | 'unknown', unmet: [{ index, kind }] }
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
- **`grants`**: the items and game currencies the recipe gives, and `{ kind: 'roll', lootTableId,
|
|
631
|
+
count }` for each roll — "1 roll from Common Chest", never what it will land on.
|
|
632
|
+
- **`platformGrants`**: each `platform_grants` mapping that fires, with `base` (no pass, no Boost),
|
|
633
|
+
`total` (this player) and the `breakdown` of the difference. `ifComplete: true` marks an
|
|
634
|
+
`on_complete` payout: it pays only if this claim completes the progression.
|
|
635
|
+
- **`bonuses`**: each bonus with `eligible` `true`, `false` or `'unknown'` — unknown when the SDK
|
|
636
|
+
cannot see what the requirement names. Draw unknown as neither won nor lost.
|
|
637
|
+
- **`upsells`**: what each pass on sale here would add. Empty means show no upsell
|
|
638
|
+
([below](#offering-vip-before-a-reward-the-upsell)).
|
|
639
|
+
- **It is advisory.** The run decides: state can change between the preview and the claim, and the
|
|
640
|
+
platform's daily limits are not in it, because they are private.
|
|
641
|
+
- **Browser only.** It rejects a `PreviewError` with `not_found` for an id the config does not have,
|
|
642
|
+
`invalid_argument` for a target that is not one of the three shapes, and `unsupported` outside a
|
|
643
|
+
browser game.
|
|
644
|
+
|
|
645
|
+
After the claim, the order's `platform_grant` change is the receipt. `gem.earnings.fromReceipt`
|
|
646
|
+
turns it into the same shape, so one component draws "you will earn" and "you earned":
|
|
647
|
+
|
|
648
|
+
```js
|
|
649
|
+
for (const change of order.changes) {
|
|
650
|
+
const paid = await gem.earnings.fromReceipt(change); // null for any other kind of change
|
|
651
|
+
if (paid) rewardRow.show(paid.total, paid.breakdown);
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
`base` there is what a player with no pass and no Boost would have got under the same limits, so
|
|
656
|
+
the breakdown is exact even when a limit trimmed the payout. A function's `gem.earnings` and a
|
|
657
|
+
dedicated server's `gem.earnings` read receipts the same way.
|
|
658
|
+
|
|
529
659
|
### Achievements: the other kind of platform grant
|
|
530
660
|
|
|
531
661
|
A `platform_grants` entry can also be an **achievement** — a platform-wide recognition with points,
|
|
532
662
|
on the progression's completion or on one level of a track, that unlocks once per player when your
|
|
533
663
|
server, or a serverless function that player called, moves the progression past it. Everything
|
|
534
664
|
above about server-only writes applies — an achievement that awards points or rewards may not sit
|
|
535
|
-
on a progression any operation in your config can raise, so a recipe priced in
|
|
665
|
+
on a progression any operation in your config can raise, so a recipe priced in Coins can never sell
|
|
536
666
|
one — and more: an achievement unlocks only on `release`, it never re-fires, and what it credits is
|
|
537
667
|
fixed once released. Its points are never boosted. [achievements.md](achievements.md) is the whole of it.
|
|
538
668
|
|
|
@@ -581,9 +711,10 @@ nobody else can touch them. The arcade also has currencies that span every game,
|
|
|
581
711
|
|
|
582
712
|
That changes what a shop can be. A player earns in someone else's game and spends in yours.
|
|
583
713
|
|
|
584
|
-
|
|
585
|
-
**
|
|
586
|
-
[arcade-economy.md](arcade-economy.md)
|
|
714
|
+
That currency is **Coins** (`ARCADE_TICKETS`): play earns them, the Arcade sells them, and games
|
|
715
|
+
price in them. **Gems are retiring** — reprice any `GEMS` price in Coins at 5 Coins per Gem and move
|
|
716
|
+
any Gem grant to Coins; [arcade-economy.md](arcade-economy.md) has the date's consequences and the
|
|
717
|
+
rest of the rules.
|
|
587
718
|
|
|
588
719
|
### Awarding one
|
|
589
720
|
|
|
@@ -613,13 +744,12 @@ A browser-only game cannot award a platform currency without a server **or a ser
|
|
|
613
744
|
That is the price of the platform never trusting a client. A game with no dedicated server writes
|
|
614
745
|
the source progression from a function the player calls, on a progression that opts in with
|
|
615
746
|
`function_may_grant`, and the platform then pays that player only, in any listed platform currency
|
|
616
|
-
—
|
|
747
|
+
— Coins (`ARCADE_TICKETS`)
|
|
617
748
|
([serverless-functions.md](serverless-functions.md#paying-the-player-who-called)).
|
|
618
749
|
|
|
619
750
|
### Showing the reward land in the arcade's wallet
|
|
620
751
|
|
|
621
|
-
**Platform-currency display belongs to the arcade** (the creator's ruling, 2026-09-24) —
|
|
622
|
-
Coins alike. The arcade shows the player's platform currencies in its own chrome — in the Gem
|
|
752
|
+
**Platform-currency display belongs to the arcade** (the creator's ruling, 2026-09-24). The arcade shows the player's platform currencies in its own chrome — in the Gem
|
|
623
753
|
Arcade, the Gem menu the player opens from the Gem button over your frame; no wallet
|
|
624
754
|
is drawn over your game — and moves a total when the platform confirms a grant. Your game keeps no counter or balance
|
|
625
755
|
of a platform currency of its own, draws no flight or celebration of its own, and shows no
|
|
@@ -627,20 +757,19 @@ platform-currency amount at all when it is not connected to the arcade.
|
|
|
627
757
|
|
|
628
758
|
What your game does is ask for the payout to start on **your** screen, from the currency's icon (or
|
|
629
759
|
hero) beside the reward, so the currency flies from there into the arcade — in the Gem Arcade,
|
|
630
|
-
into the Gem button, which pops as each mark lands. A
|
|
760
|
+
into the Gem button, which pops as each mark lands. A Coins payout is cued from the coin:
|
|
631
761
|
|
|
632
762
|
```js
|
|
633
|
-
const { x, y, width, height } =
|
|
763
|
+
const { x, y, width, height } = rewardCoin.getBoundingClientRect(); // the coin element's own box
|
|
634
764
|
gem.cueReward({
|
|
635
|
-
currencySlug: '
|
|
765
|
+
currencySlug: 'ARCADE_TICKETS',
|
|
636
766
|
expected: 15, // if you know it; drawn as provisional until the platform confirms
|
|
637
767
|
origin: { x: x + width / 2, y: y + height / 2 }, // CSS px of your own viewport
|
|
638
768
|
});
|
|
639
769
|
```
|
|
640
770
|
|
|
641
|
-
`origin` is the **centre of the
|
|
642
|
-
panel around it.
|
|
643
|
-
coin: the slug decides what flies and which total moves. **A payout you do not cue still flies**
|
|
771
|
+
`origin` is the **centre of the coin on screen** — measure the coin element itself, not the row
|
|
772
|
+
or panel around it. The slug decides what flies and which total moves. **A payout you do not cue still flies**
|
|
644
773
|
in the Gem Arcade, from the middle of your frame, once the platform confirms it — cue it so the
|
|
645
774
|
flight starts where the player is looking. **If `cueReward` returns `false`, do nothing**: there is
|
|
646
775
|
no fallback celebration.
|
|
@@ -658,10 +787,10 @@ reward once, whichever reaches it first, so you may cue before the call or after
|
|
|
658
787
|
```js
|
|
659
788
|
const cueId = crypto.randomUUID(); // one per award, never reused
|
|
660
789
|
const result = await gem.functions.invoke({ claim: 'daily' }, { cueId });
|
|
661
|
-
gem.cueReward({ currencySlug: '
|
|
790
|
+
gem.cueReward({ currencySlug: 'ARCADE_TICKETS', expected: 45, origin, cueId });
|
|
662
791
|
```
|
|
663
792
|
|
|
664
|
-
A payout that pays
|
|
793
|
+
A payout that pays more than one platform currency in one call cues each with the same `cueId`. The id is
|
|
665
794
|
yours and opaque: 1 to 64 letters, digits, `_` or `-`, and the SDK leaves out anything else with a
|
|
666
795
|
console warning rather than fail the cue or the call. It is for the animation only. It moves no
|
|
667
796
|
balance, your function never sees it, and no receipt or order carries it.
|
|
@@ -721,14 +850,14 @@ gem.cueReward({ currencySlug: 'ARCADE_TICKETS', expected: 20, origin: claimCoinC
|
|
|
721
850
|
the bridge is up (held and sent on connect) and under an embedder that presents every payout its
|
|
722
851
|
own way; there, keep cueing as usual and nothing else changes.
|
|
723
852
|
|
|
724
|
-
To draw
|
|
725
|
-
**icon** beside an amount or price, the **hero** only on a reward presentation where the
|
|
726
|
-
subject. [brand-art.md](brand-art.md) has both, how to draw them, and the rest of the rules.
|
|
853
|
+
To draw a Coin on your own screen, use the platform's art, which ships in the package — the Coins
|
|
854
|
+
**icon** beside an amount or price, the **hero** only on a reward presentation where the coin is
|
|
855
|
+
the subject. [brand-art.md](brand-art.md) has both, how to draw them, and the rest of the rules.
|
|
727
856
|
|
|
728
857
|
### Being paid in one
|
|
729
858
|
|
|
730
|
-
**Spend platform currency with `gem.purchases.request`.** Every game sells for
|
|
731
|
-
other platform currency through the arcade's buy sheet: the platform confirms the purchase with the
|
|
859
|
+
**Spend platform currency with `gem.purchases.request`.** Every game sells for Coins, or any
|
|
860
|
+
other platform currency, through the arcade's buy sheet: the platform confirms the purchase with the
|
|
732
861
|
player, offers a guest an account, and processes the order. Build your shop's Buy button on it.
|
|
733
862
|
|
|
734
863
|
| Call | Who owns confirmation | Transport | Use it for |
|
|
@@ -754,7 +883,7 @@ const item = config.item('extra-life');
|
|
|
754
883
|
const res = await gem.purchases.request({
|
|
755
884
|
kind: 'item', // or 'recipe'
|
|
756
885
|
targetId: item.id, // UUID, never the slug
|
|
757
|
-
currencySlug: '
|
|
886
|
+
currencySlug: 'ARCADE_TICKETS',
|
|
758
887
|
quantity: 1,
|
|
759
888
|
});
|
|
760
889
|
|
|
@@ -768,11 +897,12 @@ The platform re-fetches the target and price, submits the order with the player'
|
|
|
768
897
|
and verifies an `ok` before returning it to the game. Whether the player sees a sheet first depends
|
|
769
898
|
on the currency:
|
|
770
899
|
|
|
771
|
-
- **
|
|
772
|
-
|
|
900
|
+
- **Any other platform currency:** the arcade opens its purchase sheet, and nothing is spent until
|
|
901
|
+
the player presses Buy.
|
|
773
902
|
- **Coins (`ARCADE_TICKETS`) the player can afford:** no sheet. The arcade spends them at once, does
|
|
774
903
|
not pause your game, and says so over it ("Lantern purchased · 180 Coins left"). Coins are earned
|
|
775
|
-
by playing
|
|
904
|
+
by playing as well as bought, and a game's Coin spend needs no confirmation: the platform's
|
|
905
|
+
decision, unchanged since Coins went on sale.
|
|
776
906
|
- **Coins the player cannot afford:** the arcade opens its sheet to say so, and answers `declined`
|
|
777
907
|
with reason `insufficient_funds`.
|
|
778
908
|
|
|
@@ -824,7 +954,7 @@ backend it answers `undefined` by design: there, your placeholder is what you se
|
|
|
824
954
|
The direct path submits an order from your game with no platform sheet. Its job is recipes paid in
|
|
825
955
|
your own game currency or items (`payment: { kind: 'configured' }`, below). It also accepts a
|
|
826
956
|
`platform_currency` payment, but **do not spend platform currency this way on gemarcade.com**: a
|
|
827
|
-
web guest is refused (below), and every game spends
|
|
957
|
+
web guest is refused (below), and every game spends Coins through
|
|
828
958
|
[the sheet](#platform-owned-purchase-sheet). The form is documented for games that have not moved
|
|
829
959
|
yet:
|
|
830
960
|
|
|
@@ -835,7 +965,7 @@ const result = await gem.me.mutations.purchase({
|
|
|
835
965
|
quantity: 1,
|
|
836
966
|
payment: {
|
|
837
967
|
kind: 'platform_currency',
|
|
838
|
-
currencySlug: '
|
|
968
|
+
currencySlug: 'ARCADE_TICKETS',
|
|
839
969
|
},
|
|
840
970
|
});
|
|
841
971
|
|
|
@@ -905,7 +1035,7 @@ Do not retry an unsafe write after a timeout; re-read inventory or use the retur
|
|
|
905
1035
|
ids for support.
|
|
906
1036
|
|
|
907
1037
|
**A guest is refused `guest_upgrade_required`, and you draw nothing.** The platform lets only a
|
|
908
|
-
player with an account spend
|
|
1038
|
+
player with an account spend Coins — which is why platform currency is spent through
|
|
909
1039
|
[the sheet](#platform-owned-purchase-sheet), where the arcade handles a guest itself. This is the
|
|
910
1040
|
safety net for a game that still spends directly. When a guest's direct purchase is refused, the SDK
|
|
911
1041
|
hands the attempt to the arcade, which offers the player an account over your game, and your call
|
|
@@ -943,7 +1073,7 @@ the same strings.
|
|
|
943
1073
|
|
|
944
1074
|
| Code | What happened | What to do |
|
|
945
1075
|
| --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
946
|
-
| `unknown_currency` | the slug is not an active platform currency | write `ARCADE_TICKETS
|
|
1076
|
+
| `unknown_currency` | the slug is not an active platform currency | write `ARCADE_TICKETS`; the direct path's message lists what is active |
|
|
947
1077
|
| `currency_not_in_game_payable` | the currency exists but cannot pay for game items | price in a currency `gemctl platform config` lists under `platform_prices` |
|
|
948
1078
|
| `not_purchasable` | the target is not in the purchase catalog on this channel, or cannot be bought | send the id (not the slug); promote the config version that prices it |
|
|
949
1079
|
| `no_price_in_currency` | the target has no price in that currency | ask in the currency it is priced in, or add the price |
|
|
@@ -964,24 +1094,21 @@ inventory, never on a reason, and do not retry because of one.
|
|
|
964
1094
|
⚠️ **Never draw a top-up or payment screen of your own** — no checkout, no wallet top-up, nothing
|
|
965
1095
|
that looks like the arcade's purchase sheet or wallet. A game screen shaped like platform payment
|
|
966
1096
|
chrome is an imitation of it. Your Buy button is yours, and taking a player's money is the
|
|
967
|
-
arcade's alone: a "Get
|
|
968
|
-
"Get VIP" button calls `gem.store.
|
|
1097
|
+
arcade's alone: a "Get Coins" button of yours calls `gem.store.purchase` with a Coin pack from the
|
|
1098
|
+
player's press, a "Get VIP" button calls `gem.store.showPass('vip')`, and the arcade store's own checkout does the rest
|
|
969
1099
|
([arcade-and-store.md](arcade-and-store.md)). An
|
|
970
1100
|
`insufficient_funds` refusal never opens it by itself.
|
|
971
1101
|
|
|
972
|
-
### What Coins
|
|
1102
|
+
### What Coins may buy: the spend scope
|
|
973
1103
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
each may price is decided by two settings: each platform currency's **spend scope**, which the
|
|
977
|
-
platform sets, and each of your items' **category**, which you set.
|
|
1104
|
+
What a platform currency may price is decided by two settings: the currency's **spend scope**, which
|
|
1105
|
+
the platform sets, and each of your items' **category**, which you set.
|
|
978
1106
|
|
|
979
|
-
- A currency whose spend scope is **`
|
|
980
|
-
|
|
981
|
-
- A currency whose spend scope is **`any`** may price anything. Gems' is.
|
|
1107
|
+
- A currency whose spend scope is **`any`** may price anything. Coins' is.
|
|
1108
|
+
- A currency whose spend scope is **`cosmetic_only`** may price cosmetics only.
|
|
982
1109
|
|
|
983
|
-
The platform owns each currency's scope and can change it, so do not hard-code
|
|
984
|
-
|
|
1110
|
+
The platform owns each currency's scope and can change it, so do not hard-code it: a refusal always
|
|
1111
|
+
names the scope that applied.
|
|
985
1112
|
|
|
986
1113
|
```yaml
|
|
987
1114
|
items:
|
|
@@ -990,27 +1117,28 @@ items:
|
|
|
990
1117
|
category: cosmetic # changes only how the player looks
|
|
991
1118
|
platform_prices:
|
|
992
1119
|
- currency: ARCADE_TICKETS
|
|
993
|
-
price:
|
|
1120
|
+
price: 1500
|
|
994
1121
|
- id: '…'
|
|
995
1122
|
slug: extra-life
|
|
996
1123
|
# category omitted: `progression`, the default
|
|
997
1124
|
platform_prices:
|
|
998
|
-
- currency:
|
|
999
|
-
price:
|
|
1125
|
+
- currency: ARCADE_TICKETS
|
|
1126
|
+
price: 125
|
|
1000
1127
|
```
|
|
1001
1128
|
|
|
1002
1129
|
- **`category`** is `cosmetic` — skins, outfits, visual effects, decorations, emotes, profile
|
|
1003
1130
|
cosmetics: anything that changes how the player looks and never what they can do — or
|
|
1004
1131
|
`progression`, anything that affects play. It defaults to `progression`, so an item is only ever a
|
|
1005
1132
|
cosmetic because you said so.
|
|
1006
|
-
- **
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1133
|
+
- **Under a `cosmetic_only` scope, a price is accepted only on a cosmetic.** On an item, that item
|
|
1134
|
+
must be `category: cosmetic` and a plain `item_type: item`; a recipe must grant at least one
|
|
1135
|
+
cosmetic item and nothing else — no currency, no progression, no non-cosmetic item, no loot table.
|
|
1136
|
+
Taking from the player as part of the recipe (`currency_consume`, `item_remove`) is fine.
|
|
1137
|
+
- **Under `any`, a price may go on anything**, cosmetic or not.
|
|
1138
|
+
- **A paid roll is gated.** A recipe priced in Coins that rolls a loot table, or has a bonus that
|
|
1139
|
+
rolls one, is a paid random item: the arcade's sheet shows the odds before the player pays, and
|
|
1140
|
+
the platform refuses it with `paid_chance_restricted` for a known minor and in restricted regions
|
|
1141
|
+
(§12).
|
|
1014
1142
|
|
|
1015
1143
|
The upload refuses a config that breaks the rule. The refusal names the currency and its scope, and
|
|
1016
1144
|
the offending item or operation, so you know what to change:
|
|
@@ -1025,8 +1153,8 @@ purchase catalog stops listing that currency's price on anything the scope does
|
|
|
1025
1153
|
purchase naming such a price is refused `not_purchasable` before anything is charged.
|
|
1026
1154
|
|
|
1027
1155
|
The category is a declaration the platform takes at your word, and the release review checks it.
|
|
1028
|
-
Labelling something that affects play `cosmetic`
|
|
1029
|
-
|
|
1156
|
+
Labelling something that affects play `cosmetic` breaks platform policy, whatever the scope: a play
|
|
1157
|
+
item sold for Coins keeps `category: progression`.
|
|
1030
1158
|
|
|
1031
1159
|
**To sell a cosmetic that needs game progress first, sell it through a recipe.** An item's own
|
|
1032
1160
|
`platform_prices` cannot carry prerequisites; a recipe's can. Grant the cosmetic from a recipe that
|
|
@@ -1048,27 +1176,70 @@ catalogs:
|
|
|
1048
1176
|
item_definition_slug: dragon-skin
|
|
1049
1177
|
platform_prices:
|
|
1050
1178
|
- currency: ARCADE_TICKETS
|
|
1051
|
-
price:
|
|
1179
|
+
price: 1500
|
|
1052
1180
|
```
|
|
1053
1181
|
|
|
1054
1182
|
A player below level 10 is refused `recipe_locked` before any Coins are taken, and
|
|
1055
1183
|
`recipeAvailability` tells your shop the recipe is locked and what it needs (§9) — so show the lock
|
|
1056
1184
|
("Reach level 10 to unlock") instead of offering the purchase.
|
|
1057
1185
|
|
|
1186
|
+
### Offering VIP before a reward: the upsell
|
|
1187
|
+
|
|
1188
|
+
The platform holds nothing back for later, so a game that wants to sell VIP does it **before** it
|
|
1189
|
+
grants: it holds the reward, shows what VIP would add, offers it, then runs the recipe.
|
|
1190
|
+
|
|
1191
|
+
```js
|
|
1192
|
+
async function openChest() {
|
|
1193
|
+
const preview = await gem.gameConfig.preview({ recipe: 'open_chest' });
|
|
1194
|
+
const upsell = preview.upsells[0]; // empty: no pass on sale here would add anything
|
|
1195
|
+
if (upsell && (await vipCard.ask(upsell)) === 'get') {
|
|
1196
|
+
// The card shows upsell.name and its locked extras: "+1 roll · 45 Coins".
|
|
1197
|
+
const { outcome, reason } = await gem.store.showPass(upsell.pass);
|
|
1198
|
+
if (outcome === 'ok' && reason === 'pending') note('VIP will apply once it is confirmed');
|
|
1199
|
+
}
|
|
1200
|
+
vipCard.focusReturnsTo(openButton);
|
|
1201
|
+
await runRecipe(); // the bonus applies if VIP is now held, and the base runs either way
|
|
1202
|
+
}
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
`showPass` answers `ok` only once a re-read shows the pass held, so a recipe run after it gets the
|
|
1206
|
+
bonus the player just paid for. `pending` (Ask to Buy, or a payment not yet settled) means run it
|
|
1207
|
+
now without the bonus. `declined` or `unavailable` means run it as it is — and `unavailable` or
|
|
1208
|
+
`error` may still mean the player paid, so read `gem.me.passes` again rather than telling them
|
|
1209
|
+
they did not.
|
|
1210
|
+
|
|
1211
|
+
`upsells` is empty for a minor, a store that is off, a pass the player declined twice today or
|
|
1212
|
+
already holds, and anywhere the store sells no pass: draw nothing then. A guest **does** get
|
|
1213
|
+
upsells, and buying takes them through sign-up first.
|
|
1214
|
+
|
|
1215
|
+
**The rules, every one a must:**
|
|
1216
|
+
|
|
1217
|
+
- **Upsell only before granting**, and at most once per reward moment. Never after.
|
|
1218
|
+
- **Give Get VIP and Continue equal weight.** Neither is the default, the larger or the brighter.
|
|
1219
|
+
- **Never show the outcome of a locked roll.** Show "+1 roll", never what it would land on.
|
|
1220
|
+
- **Disclose the odds** for a bonus roll, as for any roll ([§12](#12-loot-tables-and-odds)).
|
|
1221
|
+
- **No countdowns, no "expires" pressure, and no price** in your game. The Arcade's sheet shows the
|
|
1222
|
+
price.
|
|
1223
|
+
- **Show a lock as an icon plus text**, never colour alone.
|
|
1224
|
+
- **Return focus to the button** that opened the sheet when it closes.
|
|
1225
|
+
- **Announce an applied bonus in a polite live region** (`aria-live="polite"`).
|
|
1226
|
+
- **Use the preview's numbers and `upsell.name`**, never your own copy of them, and never a
|
|
1227
|
+
literal `'vip'`: pass `upsell.pass` straight to `showPass`.
|
|
1228
|
+
|
|
1058
1229
|
### Reading the player's balance
|
|
1059
1230
|
|
|
1060
1231
|
```js
|
|
1061
1232
|
const currencies = await gem.platform.currencies.get();
|
|
1062
|
-
const
|
|
1063
|
-
buyButton.disabled = !
|
|
1233
|
+
const coins = currencies.find((currency) => currency.slug === 'ARCADE_TICKETS');
|
|
1234
|
+
buyButton.disabled = !coins || coins.balance < price; // can the player afford it?
|
|
1064
1235
|
```
|
|
1065
1236
|
|
|
1066
1237
|
**Read it to decide, never to display.** Platform-currency display belongs to the arcade: its
|
|
1067
1238
|
wallet — the Gem menu, in the Gem Arcade, and beside the Gem for a game that asks with
|
|
1068
1239
|
`gem.setWalletVisible(true)` ([the Arcade's wallet](what-a-game-must-do.md#the-arcades-wallet-in-the-gem-menu-and-beside-the-gem-when-you-ask))
|
|
1069
|
-
— shows the player's
|
|
1070
|
-
|
|
1071
|
-
|
|
1240
|
+
— shows the player's Coins total while your game runs, and your game keeps no counter, balance or
|
|
1241
|
+
wallet total of its own ([brand-art.md](brand-art.md) §5). Use the balance to enable a purchase or
|
|
1242
|
+
to say "not enough Coins" — not to draw a number.
|
|
1072
1243
|
|
|
1073
1244
|
Every game receives every active platform currency. Each row has `access: 'spend'`; the field stays
|
|
1074
1245
|
in the response shape for existing consumers.
|
|
@@ -1084,27 +1255,27 @@ in the response shape for existing consumers.
|
|
|
1084
1255
|
|
|
1085
1256
|
```js
|
|
1086
1257
|
const allowances = await gem.platform.earnAllowance.get();
|
|
1087
|
-
const
|
|
1258
|
+
const coins = allowances.find((a) => a.currencySlug === 'ARCADE_TICKETS');
|
|
1088
1259
|
// 0 or less once `resetsAt` has passed: the read is stale, so say nothing about when.
|
|
1089
|
-
const hours =
|
|
1090
|
-
if (
|
|
1260
|
+
const hours = coins?.resetsAt ? Math.ceil((coins.resetsAt - Date.now()) / 3_600_000) : 0;
|
|
1261
|
+
if (coins && coins.remaining > 0) {
|
|
1091
1262
|
hud.setText(
|
|
1092
|
-
`You can still earn ${
|
|
1263
|
+
`You can still earn ${coins.remaining} Coins` + (hours > 0 ? `, more in ${hours}h` : ''),
|
|
1093
1264
|
);
|
|
1094
|
-
} else if (
|
|
1265
|
+
} else if (coins?.resetsAt) {
|
|
1095
1266
|
// Spent for now: say when more arrives, never "0 left".
|
|
1096
|
-
hud.setText(hours > 0 ? `More
|
|
1267
|
+
hud.setText(hours > 0 ? `More Coins in ${hours}h` : 'More Coins soon');
|
|
1097
1268
|
}
|
|
1098
1269
|
```
|
|
1099
1270
|
|
|
1100
|
-
One entry per earned platform currency (
|
|
1271
|
+
One entry per earned platform currency (Coins, today), for **this game and this player**:
|
|
1101
1272
|
how much more your game can still grant before the platform's ceilings trim the grant. It is the
|
|
1102
1273
|
lower of what is left of your game's own cap and what is left of the player's daily maximum across
|
|
1103
1274
|
all games, with the player's Boost applied — one figure, never a breakdown.
|
|
1104
1275
|
|
|
1105
1276
|
| Field | What it is |
|
|
1106
1277
|
| -------------- | ------------------------------------------------------------------------------------------------- |
|
|
1107
|
-
| `currencySlug` | `
|
|
1278
|
+
| `currencySlug` | `ARCADE_TICKETS`, … (`''` if the platform sent none) |
|
|
1108
1279
|
| `currencyId` | The permanent id; joins to `gem.platform.currencies` and `platform_prices[].platform_currency_id` |
|
|
1109
1280
|
| `limit` | The binding ceiling: your game's own allowance, or the daily maximum across every game |
|
|
1110
1281
|
| `earned` | How much of that ceiling has been paid, which may include other games' payouts |
|
|
@@ -1127,8 +1298,8 @@ end of the reported period when both of the platform's ceilings are spent.
|
|
|
1127
1298
|
|
|
1128
1299
|
**Display only. Never gate gameplay or a grant on it.**
|
|
1129
1300
|
|
|
1130
|
-
- ✅ ``hud.setText(`${
|
|
1131
|
-
- ❌ `if (
|
|
1301
|
+
- ✅ ``hud.setText(`${coins.remaining} Coins left to earn today`)``
|
|
1302
|
+
- ❌ `if (coins.remaining > 0) awardDailyBonus()`
|
|
1132
1303
|
|
|
1133
1304
|
Grant anyway: the platform trims every grant to what is left, and the receipt is the authority on
|
|
1134
1305
|
what landed. The figure can be stale the moment after you read it — another game may pay the player
|
|
@@ -1182,8 +1353,8 @@ remove it. A player spends it through the platform, by choosing to.
|
|
|
1182
1353
|
|
|
1183
1354
|
**You cannot sell for real money here.** A platform currency a player _earns_ is what this surface
|
|
1184
1355
|
reaches. Charging actual money is a separate platform capability and is not something a game config
|
|
1185
|
-
grants itself. What a game _can_ do is offer the arcade store's own
|
|
1186
|
-
bundles from a button of its own, through `gem.store`
|
|
1356
|
+
grants itself. What a game _can_ do is offer the arcade store's own Coin packs, passes such as VIP
|
|
1357
|
+
and bundles from a button of its own, through `gem.store`
|
|
1187
1358
|
([arcade-and-store.md](arcade-and-store.md)): the arcade prices them and takes the money.
|
|
1188
1359
|
|
|
1189
1360
|
## Errors
|
|
@@ -1201,8 +1372,8 @@ bundles from a button of its own, through `gem.store`
|
|
|
1201
1372
|
|
|
1202
1373
|
## What to read next
|
|
1203
1374
|
|
|
1204
|
-
- [`arcade-economy.md`](arcade-economy.md) —
|
|
1205
|
-
|
|
1375
|
+
- [`arcade-economy.md`](arcade-economy.md) — Coins and achievement points: which to use for what,
|
|
1376
|
+
the rules each carries, and Gems' retirement.
|
|
1206
1377
|
- [`achievements.md`](achievements.md) — platform achievements on your progressions' thresholds.
|
|
1207
1378
|
- [`game-economy-tutorial.md`](game-economy-tutorial.md) — build a shop, step by step, the safe way.
|
|
1208
1379
|
- [`trading-and-gifting.md`](trading-and-gifting.md) — move items and currency between players.
|