@indiegems/gem-web-sdk 7.3.1 → 7.4.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.
Files changed (42) hide show
  1. package/README.md +54 -0
  2. package/dist/game.d.ts +156 -22
  3. package/dist/gem-web-sdk.bridge.js +1 -1
  4. package/dist/gem-web-sdk.esm.js +2 -2
  5. package/dist/gem-web-sdk.game.js +2 -2
  6. package/dist/gem-web-sdk.min.js +2 -2
  7. package/dist/gem-web-sdk.sdk.js +2 -2
  8. package/dist/gem-web-sdk.server.js +2 -2
  9. package/dist/index.d.ts +89 -6
  10. package/dist/sdk.d.ts +156 -22
  11. package/docs/achievements.md +232 -0
  12. package/docs/analytics.md +271 -0
  13. package/docs/api/api-reference-game.md +15308 -0
  14. package/docs/api/api-reference-server.md +4397 -0
  15. package/docs/api/reference.json +24130 -0
  16. package/docs/arcade-economy.md +263 -0
  17. package/docs/async-opponents.md +396 -0
  18. package/docs/brand-art.md +334 -0
  19. package/docs/cross-promotion.md +137 -0
  20. package/docs/daily-challenges.md +530 -0
  21. package/docs/dedicated-server-flow.md +879 -0
  22. package/docs/deprecation-policy.md +124 -0
  23. package/docs/friends.md +233 -0
  24. package/docs/game-economy-tutorial.md +326 -0
  25. package/docs/getting-started.md +552 -0
  26. package/docs/inventory-and-catalogs.md +1035 -0
  27. package/docs/leaderboards.md +277 -0
  28. package/docs/manifest.json +135 -0
  29. package/docs/match-session.md +288 -0
  30. package/docs/p2p-and-parties.md +783 -0
  31. package/docs/platform-rules.md +372 -0
  32. package/docs/player-sign-in.md +89 -0
  33. package/docs/room-shapes.md +195 -0
  34. package/docs/rooms-and-matches.md +779 -0
  35. package/docs/saving-player-progress.md +457 -0
  36. package/docs/server-bundles.md +1041 -0
  37. package/docs/serverless-functions.md +748 -0
  38. package/docs/trading-and-gifting.md +98 -0
  39. package/docs/what-a-game-must-do.md +1231 -0
  40. package/package.json +7 -2
  41. package/schema/bridge.v1.json +440 -7
  42. package/schema/capabilities.public.json +189 -0
package/dist/sdk.d.ts CHANGED
@@ -205,6 +205,7 @@ type GameConfigSnapshot = {
205
205
  "async_opponent_types"?: SnapshotAsyncOpponentType[];
206
206
  "catalogs": SnapshotCatalog[];
207
207
  "currencies": SnapshotCurrency[];
208
+ "friend_challenge_templates"?: SnapshotFriendChallengeTemplate[];
208
209
  "game_room_templates": SnapshotGameRoomTemplate[];
209
210
  "hash": string;
210
211
  "items": SnapshotItem[];
@@ -212,6 +213,7 @@ type GameConfigSnapshot = {
212
213
  "leaderboards"?: SnapshotLeaderboard[];
213
214
  "loot_tables": SnapshotLootTable[];
214
215
  "progressions": SnapshotProgression[];
216
+ "rewarded_placements"?: SnapshotRewardedPlacement[];
215
217
  "run_types"?: SnapshotRunType[];
216
218
  "version": number;
217
219
  };
@@ -419,6 +421,21 @@ type SnapshotCurrency = {
419
421
  "min_balance": number;
420
422
  "slug"?: string | null;
421
423
  };
424
+ type SnapshotFriendChallengeParam = {
425
+ "key": string;
426
+ "localized"?: Record<string, unknown> | null;
427
+ "max"?: unknown;
428
+ "min"?: unknown;
429
+ "pattern"?: string | null;
430
+ "type": string;
431
+ "values"?: Record<string, unknown> | null;
432
+ };
433
+ type SnapshotFriendChallengeTemplate = {
434
+ "key": string;
435
+ "localized"?: Record<string, unknown>;
436
+ "params"?: SnapshotFriendChallengeParam[];
437
+ "text": string;
438
+ };
422
439
  type SnapshotGameRoomTemplate = {
423
440
  "allow_host_transfer": boolean;
424
441
  "allow_join_in_progress"?: boolean;
@@ -526,6 +543,25 @@ type SnapshotRecipe = {
526
543
  "slug"?: string | null;
527
544
  "sort_order": number;
528
545
  };
546
+ type SnapshotRewardGrant = {
547
+ "item_definition_id": string;
548
+ "quantity": number;
549
+ "type": "item_grant";
550
+ } | {
551
+ "amount": number;
552
+ "currency_id": string;
553
+ "type": "currency_grant";
554
+ };
555
+ type SnapshotRewardedPlacement = {
556
+ "allowance_currency_id"?: string | null;
557
+ "cooldown_seconds": number;
558
+ "grants": SnapshotRewardGrant[];
559
+ "max_held"?: number | null;
560
+ "max_per_day": number;
561
+ "placement": string;
562
+ "reward_class": string;
563
+ "without_ads": string;
564
+ };
529
565
  type SnapshotRunType = {
530
566
  "bucket_1_allowed_values"?: string[] | null;
531
567
  "bucket_1_name"?: string | null;
@@ -1211,10 +1247,35 @@ interface ListFriendsOptions {
1211
1247
  readonly cursor?: string;
1212
1248
  readonly signal?: AbortSignal;
1213
1249
  }
1250
+ interface FriendChallengeRequest {
1251
+ readonly playerId: string;
1252
+ readonly template: string;
1253
+ readonly params?: Readonly<Record<string, string | number>>;
1254
+ }
1255
+ type FriendChallengeOutcomeKind = "accepted" | "cancelled" | "refused" | "unavailable";
1256
+ interface FriendChallengeOutcome {
1257
+ readonly outcome: FriendChallengeOutcomeKind;
1258
+ }
1259
+ type FriendChallengeErrorCode = "unknown_template" | "template_not_on_channel" | "param_invalid" | "param_undeclared" | "recipient_invalid" | (string & {});
1260
+ declare class FriendChallengeError extends GemApiError {
1261
+ readonly code?: FriendChallengeErrorCode;
1262
+ readonly param?: string;
1263
+ readonly channel?: string;
1264
+ readonly configVersion?: number;
1265
+ constructor(source: GemApiError, extra?: {
1266
+ readonly param?: string;
1267
+ readonly channel?: string;
1268
+ readonly configVersion?: number;
1269
+ });
1270
+ }
1271
+ interface FriendsApiDependencies {
1272
+ readonly challenge?: (request: FriendChallengeRequest) => Promise<FriendChallengeOutcome>;
1273
+ }
1214
1274
  declare class FriendsApi {
1215
1275
  #private;
1216
- constructor(api: GameApi$1);
1276
+ constructor(api: GameApi$1, dependencies?: FriendsApiDependencies);
1217
1277
  list(options?: ListFriendsOptions): Promise<FriendsPage>;
1278
+ challenge(request: FriendChallengeRequest): Promise<FriendChallengeOutcome>;
1218
1279
  }
1219
1280
  type PlatformCurrencyAccess = "read" | "spend";
1220
1281
  interface PlatformCurrency {
@@ -1231,9 +1292,35 @@ declare class PlatformCurrenciesApi {
1231
1292
  constructor(api: GameApi$1);
1232
1293
  get(options?: PlatformCurrencyReadOptions): Promise<readonly PlatformCurrency[]>;
1233
1294
  }
1295
+ type EarnAllowancePeriod = "day" | "window";
1296
+ interface EarnAllowance {
1297
+ readonly currencySlug: string;
1298
+ readonly currencyId: string;
1299
+ readonly limit: number;
1300
+ readonly earned: number;
1301
+ readonly remaining: number;
1302
+ readonly period: EarnAllowancePeriod;
1303
+ readonly periodDays: number;
1304
+ readonly resetsAt?: number;
1305
+ }
1306
+ interface EarnAllowanceReadOptions {
1307
+ readonly signal?: AbortSignal;
1308
+ }
1309
+ interface EarnAllowanceSignal {
1310
+ on(listener: () => void): () => void;
1311
+ emit(): void;
1312
+ }
1313
+ type EarnAllowanceEvent = "changed";
1314
+ declare class PlatformEarnAllowanceApi {
1315
+ #private;
1316
+ constructor(api: GameApi$1, signal?: EarnAllowanceSignal, log?: ((line: string) => void) | null);
1317
+ get(options?: EarnAllowanceReadOptions): Promise<readonly EarnAllowance[]>;
1318
+ on(event: EarnAllowanceEvent, listener: () => void): () => void;
1319
+ }
1234
1320
  declare class PlatformApi {
1235
1321
  readonly currencies: PlatformCurrenciesApi;
1236
- constructor(api: GameApi$1);
1322
+ readonly earnAllowance: PlatformEarnAllowanceApi;
1323
+ constructor(api: GameApi$1, earnAllowanceSignal?: EarnAllowanceSignal, log?: ((line: string) => void) | null);
1237
1324
  }
1238
1325
  interface ProfileShowcaseEntry$1 {
1239
1326
  readonly key: string;
@@ -1856,6 +1943,12 @@ interface GemViewport$1 {
1856
1943
  bottom: number;
1857
1944
  left: number;
1858
1945
  };
1946
+ readonly reserved?: {
1947
+ x: number;
1948
+ y: number;
1949
+ width: number;
1950
+ height: number;
1951
+ }[];
1859
1952
  }
1860
1953
  interface GemDeviceSettings {
1861
1954
  readonly gem: typeof PROTOCOL;
@@ -1977,6 +2070,18 @@ interface GemSignInResult {
1977
2070
  readonly requestId: string;
1978
2071
  readonly outcome: "signed_in" | "declined" | "unavailable";
1979
2072
  }
2073
+ interface GemFriendChallengeResult {
2074
+ readonly gem: typeof PROTOCOL;
2075
+ readonly type: "gem.friendChallengeResult";
2076
+ readonly requestId: string;
2077
+ readonly outcome: "accepted" | "cancelled" | "refused" | "unavailable" | "rate_limited" | "invalid";
2078
+ readonly code?: string;
2079
+ readonly param?: string;
2080
+ readonly channel?: string;
2081
+ readonly configVersion?: number;
2082
+ readonly retryAfterMs?: number;
2083
+ readonly detail?: string;
2084
+ }
1980
2085
  interface ConnectOptions {
1981
2086
  readonly clusterTargetId?: ClusterTargetLike;
1982
2087
  readonly retry?: StartMatchRetryOptions;
@@ -3967,25 +4072,6 @@ interface SignInOutcome {
3967
4072
  interface AccountApi {
3968
4073
  requestSignIn(): Promise<SignInOutcome>;
3969
4074
  }
3970
- type AdFormat = "interstitial" | "rewarded";
3971
- interface AdRequest {
3972
- placement: string;
3973
- format: AdFormat;
3974
- }
3975
- type AdOutcomeKind = "shown" | "no_ad" | "unavailable";
3976
- interface AdOutcome {
3977
- readonly outcome: AdOutcomeKind;
3978
- }
3979
- interface AdsApi {
3980
- requestAd(req: AdRequest): Promise<AdOutcome>;
3981
- }
3982
- type DeviceSettings = Readonly<Record<string, unknown>>;
3983
- type DeviceSettingsPutOutcome = "stored" | "too_large" | "unavailable";
3984
- interface DeviceSettingsApi {
3985
- readonly current: DeviceSettings | null;
3986
- wait(timeoutMs?: number): Promise<DeviceSettings | null>;
3987
- put(settings: DeviceSettings): Promise<DeviceSettingsPutOutcome>;
3988
- }
3989
4075
  interface PlayerBoostMultiplier {
3990
4076
  readonly currencyId: string;
3991
4077
  readonly currencySlug: string;
@@ -4016,6 +4102,7 @@ interface FrameCallbacks {
4016
4102
  onPause?(msg: GemPause): void;
4017
4103
  onPrefs?(msg: GemPrefs$1): void;
4018
4104
  onCohorts?(msg: GemCohorts$1): void;
4105
+ onEarnAllowanceChanged?(): void;
4019
4106
  onViewport?(msg: GemViewport$1): void;
4020
4107
  onWillTerminate?(msg: GemWillTerminate): void;
4021
4108
  onDevtools?(action: "open" | "close" | "toggle"): void;
@@ -4041,6 +4128,7 @@ interface FrameCallbacks {
4041
4128
  onDeviceSettingsResult?(msg: GemDeviceSettingsResult): void;
4042
4129
  onAdResult?(msg: GemAdResult): void;
4043
4130
  onSignInResult?(msg: GemSignInResult): void;
4131
+ onFriendChallengeResult?(msg: GemFriendChallengeResult): void;
4044
4132
  onError?(code: GemErrorCode, message?: string): void;
4045
4133
  onDropped?(code: GemErrorCode, message?: string): void;
4046
4134
  onStateChange?(state: FrameState): void;
@@ -4082,6 +4170,13 @@ declare class GemFrame {
4082
4170
  requestFullscreen(enabled: boolean): void;
4083
4171
  reportMuted(muted: boolean): boolean;
4084
4172
  cueReward(cue: RewardCue): boolean;
4173
+ setWalletVisible(visible: boolean, options?: {
4174
+ only?: readonly string[];
4175
+ }): boolean;
4176
+ requestShare(params: ReadonlyArray<{
4177
+ readonly key: string;
4178
+ readonly value: string;
4179
+ }>): boolean;
4085
4180
  reportFrameStats(stats: {
4086
4181
  fps: number;
4087
4182
  frameTimeP95Ms: number;
@@ -4094,6 +4189,7 @@ declare class GemFrame {
4094
4189
  putDeviceSettings(requestId: string, settings: Readonly<Record<string, unknown>>): boolean;
4095
4190
  requestAd(requestId: string, placement: string, format: "interstitial" | "rewarded"): boolean;
4096
4191
  requestSignIn(requestId: string): boolean;
4192
+ requestFriendChallenge(requestId: string, playerId: string, template: string, params?: Readonly<Record<string, string | number>>): boolean;
4097
4193
  close(): void;
4098
4194
  private transition;
4099
4195
  private refuseMalformed;
@@ -4105,6 +4201,32 @@ declare class GemFrame {
4105
4201
  private dispatchInbound;
4106
4202
  private screenshotDenied;
4107
4203
  }
4204
+ interface ShareRequest {
4205
+ readonly params?: Readonly<Record<string, string>>;
4206
+ }
4207
+ interface ShareApi {
4208
+ readonly available: boolean;
4209
+ request(request?: ShareRequest): void;
4210
+ }
4211
+ type AdFormat = "interstitial" | "rewarded";
4212
+ interface AdRequest {
4213
+ placement: string;
4214
+ format: AdFormat;
4215
+ }
4216
+ type AdOutcomeKind = "shown" | "no_ad" | "unavailable";
4217
+ interface AdOutcome {
4218
+ readonly outcome: AdOutcomeKind;
4219
+ }
4220
+ interface AdsApi {
4221
+ requestAd(req: AdRequest): Promise<AdOutcome>;
4222
+ }
4223
+ type DeviceSettings = Readonly<Record<string, unknown>>;
4224
+ type DeviceSettingsPutOutcome = "stored" | "too_large" | "unavailable";
4225
+ interface DeviceSettingsApi {
4226
+ readonly current: DeviceSettings | null;
4227
+ wait(timeoutMs?: number): Promise<DeviceSettings | null>;
4228
+ put(settings: DeviceSettings): Promise<DeviceSettingsPutOutcome>;
4229
+ }
4108
4230
  declare const DEVICE_SETTINGS_MAX_BYTES = 16384;
4109
4231
  declare const ARCADE_EMBEDDERS: readonly string[];
4110
4232
  interface GemOptions {
@@ -4165,6 +4287,16 @@ interface GemInsets {
4165
4287
  }
4166
4288
  interface GemViewport {
4167
4289
  readonly insets: GemInsets;
4290
+ readonly reserved: readonly GemReservedArea[];
4291
+ }
4292
+ interface GemReservedArea {
4293
+ readonly x: number;
4294
+ readonly y: number;
4295
+ readonly width: number;
4296
+ readonly height: number;
4297
+ }
4298
+ interface WalletVisibleOptions {
4299
+ readonly only?: readonly string[];
4168
4300
  }
4169
4301
  interface GemTerminationNotice {
4170
4302
  readonly reason: GemWillTerminate["reason"];
@@ -4189,6 +4321,7 @@ interface Gem {
4189
4321
  readonly deviceSettings: DeviceSettingsApi;
4190
4322
  readonly ads: AdsApi;
4191
4323
  readonly account: AccountApi;
4324
+ readonly share: ShareApi;
4192
4325
  readonly analytics: AnalyticsApi;
4193
4326
  readonly leaderboards: LeaderboardsApi;
4194
4327
  readonly challenges: ChallengesApi;
@@ -4204,6 +4337,7 @@ interface Gem {
4204
4337
  awaitCohorts(timeoutMs?: number): Promise<GemCohorts | null>;
4205
4338
  reportMuted(muted: boolean): void;
4206
4339
  cueReward(cue: RewardCue): boolean;
4340
+ setWalletVisible(visible: boolean, options?: WalletVisibleOptions): boolean;
4207
4341
  exit(reason?: string): Promise<void>;
4208
4342
  readonly boot: BootTimeline;
4209
4343
  readonly frame: GemFrame;
@@ -4229,4 +4363,4 @@ interface ExitAnalytics {
4229
4363
  }
4230
4364
  declare function leaveAndExit(rooms: ExitRooms, bridge: ExitBridge | null, reason?: string, analytics?: ExitAnalytics): Promise<void>;
4231
4365
  declare function createGem(options?: GemOptions): Gem;
4232
- export { ARCADE_EMBEDDERS, type AccountApi, type AdFormat, type AdOutcome, type AdOutcomeKind, type AdRequest, type AdsApi, AnalyticsApi, type AnalyticsFlushOptions, type AnalyticsFlushResult, type AnalyticsOptions, type AnalyticsRejection$1 as AnalyticsRejection, type AnalyticsValue, AuthError, type AuthFailure, type AuthState, type Base64String, type BootMark, type BootTimeline, type BroadcastOptions, type BytesResponse, CONDITIONAL_HEADERS, type Challenge, type ChallengeWindow, ChallengesApi, type ClientOptions, type Clock, type ConditionalRequestHeaders, type ConfiguredPurchasePayment, type ConfiguredPurchaseRequest, type ConfiguredPurchaseResult, DEVICE_SETTINGS_MAX_BYTES, DedicatedServerError, type DedicatedServerFailure, type DedicatedServerSession, type DevToolsOpener, type DeviceSettings, type DeviceSettingsApi, type DeviceSettingsPutOutcome, type DirectPurchaseRequest, type DirectPurchaseResult, type EntityLeaderboard, type EntityLeaderboardEntry, type EntityLeaderboardOptions, type EntityType$1 as EntityType, type ExitAnalytics, type ExitBridge, type ExitRooms, type FrameState, type Friend, FriendsApi, type FriendsPage, type FunctionFailure, type FunctionInvokeOptions, type FunctionResult, FunctionsApi, type GameApi, type GameConfig, type GameServerLink, type Gem, GemApiError, GemClient, type GemCohort, type GemCohorts, GemConfigError, type GemDevTools, type GemErrorReason, GemFrame, GemFunctionError, type GemIdentity, type GemInsets, type GemMatchApi, type GemMatchServer, type GemMatchServerModule, type GemOptions, type GemPlayer, type GemPrefs, GemSaveError, type GemServer, type GemServerBundle, type GemServerFactory, type GemServerHost, type GemServerHostProfile, type GemServerIdentity, type GemServerLifecycle, type GemServerOptions, type GemServerProfile, type GemTerminationNotice, type GemViewport, type GetLeaderboardOptions, type HostTokenSource, type HostTokenSourceOptions, type JoinWelcome, type Leaderboard, type LeaderboardEntry, LeaderboardsApi, type ListChallengesOptions, type ListFriendsOptions, MAX_PERIODS_BACK, type MatchConnectOptions, type MatchConnection, type MatchHosting, type MatchHostingMode, type MatchServeOptions, type MatchTransport, type MessageMeta, type MultiTransportSession, PLATFORM_EVENTS, PlatformApi, PlatformCurrenciesApi, type PlatformCurrency, type PlatformCurrencyAccess, type PlatformCurrencyPurchasePayment, type PlatformCurrencyPurchaseRequest, type PlatformCurrencyPurchaseResult, type PlatformCurrencyReadOptions, type PlatformEventName, type PlayerBadge$1 as PlayerBadge, type PlayerBoost, type PlayerBoostMultiplier, type PlayerIdentity, type PlayerProfile, PlayerText, type ProfileCallOptions, type ProfileShowcaseEntry$1 as ProfileShowcaseEntry, ProfilesApi, type PurchaseApi, type PurchaseOptions, type PurchaseOutcome, PurchaseRefusal, type PurchaseRefusalCode, type PurchaseRequest, type PutBytesOptions, type PutSaveOptions, type RankedRow, type RecipeAvailability, type RecipeLock, type RequestOptions, type RewardCue, type RoomArgValue, type SavePage, type SaveRecord, type SendOptions$1 as SendOptions, type ServerApi, type ServerBundleHost, type ServerTokenGrant, type SessionEndReason, ShowcaseApi, type ShowcaseCallOptions, type ShowcaseFriendEntry, type ShowcaseFriends, type ShowcaseValue, type SignInOutcome, type SignInOutcomeKind, type SnapshotCurrency, type SnapshotItem, type TokenGrant, type Transport, type TransportAdvert, type TransportKind, TransportRefusal, type TransportRequest, type TransportResponse, UNRELIABLE_CHANNEL_LABEL, type VersionedGetOptions, type VersionedResponse, asPlayerText, createBootTimeline, createGem, createGemServer, createHostTokenSource, createMatchServer, defineServer, leaveAndExit, mapIdentity, markOnResolve, platformIconUrl, presentJoinCredential, readTemplateTransports, readTransportAdvert, reportOnResolve, systemClock, text, toBase64, webTransportAvailable };
4366
+ export { ARCADE_EMBEDDERS, type AccountApi, type AdFormat, type AdOutcome, type AdOutcomeKind, type AdRequest, type AdsApi, AnalyticsApi, type AnalyticsFlushOptions, type AnalyticsFlushResult, type AnalyticsOptions, type AnalyticsRejection$1 as AnalyticsRejection, type AnalyticsValue, AuthError, type AuthFailure, type AuthState, type Base64String, type BootMark, type BootTimeline, type BroadcastOptions, type BytesResponse, CONDITIONAL_HEADERS, type Challenge, type ChallengeWindow, ChallengesApi, type ClientOptions, type Clock, type ConditionalRequestHeaders, type ConfiguredPurchasePayment, type ConfiguredPurchaseRequest, type ConfiguredPurchaseResult, DEVICE_SETTINGS_MAX_BYTES, DedicatedServerError, type DedicatedServerFailure, type DedicatedServerSession, type DevToolsOpener, type DeviceSettings, type DeviceSettingsApi, type DeviceSettingsPutOutcome, type DirectPurchaseRequest, type DirectPurchaseResult, type EarnAllowance, type EarnAllowanceEvent, type EarnAllowancePeriod, type EarnAllowanceReadOptions, type EntityLeaderboard, type EntityLeaderboardEntry, type EntityLeaderboardOptions, type EntityType$1 as EntityType, type ExitAnalytics, type ExitBridge, type ExitRooms, type FrameState, type Friend, FriendChallengeError, type FriendChallengeErrorCode, type FriendChallengeOutcome, type FriendChallengeOutcomeKind, type FriendChallengeRequest, FriendsApi, type FriendsPage, type FunctionFailure, type FunctionInvokeOptions, type FunctionResult, FunctionsApi, type GameApi, type GameConfig, type GameServerLink, type Gem, GemApiError, GemClient, type GemCohort, type GemCohorts, GemConfigError, type GemDevTools, type GemErrorReason, GemFrame, GemFunctionError, type GemIdentity, type GemInsets, type GemMatchApi, type GemMatchServer, type GemMatchServerModule, type GemOptions, type GemPlayer, type GemPrefs, type GemReservedArea, GemSaveError, type GemServer, type GemServerBundle, type GemServerFactory, type GemServerHost, type GemServerHostProfile, type GemServerIdentity, type GemServerLifecycle, type GemServerOptions, type GemServerProfile, type GemTerminationNotice, type GemViewport, type GetLeaderboardOptions, type HostTokenSource, type HostTokenSourceOptions, type JoinWelcome, type Leaderboard, type LeaderboardEntry, LeaderboardsApi, type ListChallengesOptions, type ListFriendsOptions, MAX_PERIODS_BACK, type MatchConnectOptions, type MatchConnection, type MatchHosting, type MatchHostingMode, type MatchServeOptions, type MatchTransport, type MessageMeta, type MultiTransportSession, PLATFORM_EVENTS, PlatformApi, PlatformCurrenciesApi, type PlatformCurrency, type PlatformCurrencyAccess, type PlatformCurrencyPurchasePayment, type PlatformCurrencyPurchaseRequest, type PlatformCurrencyPurchaseResult, type PlatformCurrencyReadOptions, PlatformEarnAllowanceApi, type PlatformEventName, type PlayerBadge$1 as PlayerBadge, type PlayerBoost, type PlayerBoostMultiplier, type PlayerIdentity, type PlayerProfile, PlayerText, type ProfileCallOptions, type ProfileShowcaseEntry$1 as ProfileShowcaseEntry, ProfilesApi, type PurchaseApi, type PurchaseOptions, type PurchaseOutcome, PurchaseRefusal, type PurchaseRefusalCode, type PurchaseRequest, type PutBytesOptions, type PutSaveOptions, type RankedRow, type RecipeAvailability, type RecipeLock, type RequestOptions, type RewardCue, type RoomArgValue, type SavePage, type SaveRecord, type SendOptions$1 as SendOptions, type ServerApi, type ServerBundleHost, type ServerTokenGrant, type SessionEndReason, type ShareApi, type ShareRequest, ShowcaseApi, type ShowcaseCallOptions, type ShowcaseFriendEntry, type ShowcaseFriends, type ShowcaseValue, type SignInOutcome, type SignInOutcomeKind, type SnapshotCurrency, type SnapshotItem, type TokenGrant, type Transport, type TransportAdvert, type TransportKind, TransportRefusal, type TransportRequest, type TransportResponse, UNRELIABLE_CHANNEL_LABEL, type VersionedGetOptions, type VersionedResponse, type WalletVisibleOptions, asPlayerText, createBootTimeline, createGem, createGemServer, createHostTokenSource, createMatchServer, defineServer, leaveAndExit, mapIdentity, markOnResolve, platformIconUrl, presentJoinCredential, readTemplateTransports, readTransportAdvert, reportOnResolve, systemClock, text, toBase64, webTransportAvailable };
@@ -0,0 +1,232 @@
1
+ # Achievements
2
+
3
+ An **achievement** is a platform-wide recognition a player earns in your game: a name, a
4
+ description and some **achievement points**. The arcade lists your achievements on your store page, shows each player what they have unlocked, and
5
+ adds their points to their **Achievement Score**, a total that spans every game they play (§5).
6
+
7
+ There is no achievements API to call. An achievement sits on a **progression threshold your game
8
+ already has**, and it unlocks when your server, or a serverless function the player called, moves
9
+ that progression past it. Everything here is
10
+ built from things [inventory-and-catalogs.md](inventory-and-catalogs.md) §13 already covers — read
11
+ that first.
12
+
13
+ ## 1. Declaring one
14
+
15
+ An achievement is a kind of `platform_grants` entry, on either of the two thresholds a progression
16
+ has: its `completion_threshold`, or one level of a track. The full schema and its validation rules
17
+ are in the gemctl `game-config` skill; the shape is:
18
+
19
+ ```yaml
20
+ progressions:
21
+ - slug: boards_cleared
22
+ display_name: Boards cleared
23
+ config:
24
+ tracks:
25
+ - track_id: main
26
+ levels:
27
+ - level: 1
28
+ threshold: 10
29
+ platform_grants:
30
+ - achievement:
31
+ id: 91ac… # its own id, stable forever
32
+ points: 20
33
+ display_name: Getting the Hang of It
34
+ description: Clear 10 boards.
35
+ - level: 2
36
+ threshold: 100
37
+ platform_grants:
38
+ - achievement:
39
+ id: c47d…
40
+ points: 150
41
+ visibility: hidden
42
+ hidden_hint: See it through.
43
+ display_name: Every Door Opened
44
+ description: Clear all 100 boards.
45
+ ```
46
+
47
+ One counter with a level per tier is the usual shape: "clear 10, 50, 100 boards" is three levels on
48
+ one progression, not three progressions.
49
+
50
+ What the upload enforces, so you are not surprised by it:
51
+
52
+ - **A budget.** Your game's achievements may be worth at most **1,000 points** in total, and your
53
+ game may have at most **200 achievements** over its life — both counting ones you have since
54
+ removed, so retiring an achievement never frees room for another. Points are identity, not
55
+ currency — a player cannot spend them, and a game cannot mint them freely.
56
+ - **Nothing in your config may put value into its progression.** An achievement that awards
57
+ points or rewards may not sit on a progression that a recipe's cost or grant, a loot-table
58
+ entry, another progression's `rewards_on_complete` or a track level's `rewards` can raise — a
59
+ `progression_add` of a positive amount, or a `progression_set`. Unlocking counts the stored value
60
+ (§2), so any such write decides what your server's next write unlocks: a recipe priced in Gems
61
+ that adds 100 to `boards_cleared` would sell "Clear all 100 boards" to anyone with the Gems. A
62
+ reset or a spend (a negative add) is fine — it cannot buy an unlock that happens once. The
63
+ refusal is anchored at the achievement and names the operation that writes it:
64
+
65
+ ```text
66
+ progressions[0] (boards_cleared): platform_grants[0]: source is written by an operation in this
67
+ config — catalogs[0] (shop) recipes[0] (board_pack) grants[0]. An achievement that awards points
68
+ or rewards is earned only by the game server's own progression_add or progression_max, …
69
+ ```
70
+
71
+ Keep the counter your server advances separate from anything a player can buy, open or claim.
72
+ A display-only achievement (`points: 0`, no rewards) may sit on such a progression.
73
+
74
+ - **Released achievements are fixed.** Once a release carries an achievement, its `points`,
75
+ `rewards`, `visibility` and _where it sits_ — which progression, which level, and that threshold —
76
+ cannot change. Its copy can. Removing it retires it: players who hold it keep it, and its points
77
+ still count against your budget.
78
+ - **The text is screened.** Copy that impersonates the platform or its staff is refused, and the
79
+ refusal names the rule it broke.
80
+
81
+ ## 2. Who unlocks it
82
+
83
+ An achievement unlocks when an **eligible order** leaves the progression's value at or above the
84
+ threshold it sits on. Eligible means:
85
+
86
+ - written by **your game's server**, running as a platform-allocated server with its game-server
87
+ credential, **or by your serverless function, for the player who called it**. A browser write
88
+ never unlocks one, and neither does a server you started yourself with a developer credential —
89
+ its receipt says `suppressed`, with the reason `principal=developer-minted server token`;
90
+ - when a function wrote it, **on a progression that opts in** with `function_may_grant: true`
91
+ (otherwise `suppressed`, `functions_not_opted_in`), **for the player who called the function**,
92
+ and not for a guest (`function_invoker_is_guest`). A function a player called cannot write any
93
+ other player at all (`403 function_foreign_entity_write`), and a character never unlocks one.
94
+ A function your dedicated server called acts for your game, like the server. Every receipt a
95
+ function's order produces carries `principal: "function"`
96
+ ([serverless-functions.md](serverless-functions.md#paying-the-player-who-called));
97
+ - on the **`release` channel**. Writes on `dev` and `test` do cross the threshold, and the receipt
98
+ says so as `suppressed` with the channel as the reason, but nothing unlocks: an unlock is
99
+ permanent, so testing must not spend one.
100
+
101
+ **The platform checks which credential wrote the progression. It cannot check why.** Your server's
102
+ or your function's own logic is the whole defence, and a function is the easier one to lean on: a
103
+ player can call it with any input they like, as often as their per-minute budget allows. Advance a progression that carries achievements only from what your
104
+ server observed or simulated itself — a match it ran, a board it scored — never from a player's
105
+ claim ("I cleared board 50"), and never through a handler that passes a client's request straight
106
+ through to a progression write. Apply the same plausibility and rate checks you would apply before
107
+ paying currency.
108
+
109
+ It unlocks **once per player, ever**. Resetting the progression and crossing the threshold again
110
+ does nothing, and neither the completion flag nor claiming a level's rewards plays any part. A player
111
+ whose value was already past the threshold before your release unlocks it on your server's **next
112
+ write** to that progression.
113
+
114
+ ⚠️ **That catch-up counts the stored value, whoever wrote it.** That is why nothing in your config
115
+ may write the progression (§1), and why an achievement that pays points or rewards belongs on a
116
+ progression the browser has never been able to write. If a progression used to be
117
+ `client_writable`, or used to be written by a recipe, a loot table or a reward, its players' values
118
+ may not be your server's; make a new progression for the achievement rather than removing the other
119
+ writer from the old one. A release uploaded before this rule that still has such a writer unlocks
120
+ nothing there: the receipt says `suppressed`, with the reason `written_by_config`, until you upload
121
+ a config without it.
122
+
123
+ A progression the browser may write (`client_writable: true`) can carry only **display-only**
124
+ achievements: `points: 0` and no rewards. They never unlock on the platform — no record, no
125
+ receipt, nothing on the arcade — so a game that lists one works out its state itself, from
126
+ `currentValue >= threshold`.
127
+
128
+ The platform can switch achievements off for your game. When it has, the receipt says
129
+ `suppressed` with the reason `achievements_disabled` — read that before suspecting your config. It
130
+ can also switch off only what serverless functions pay and unlock; a function's unlock then reads
131
+ `suppressed` with `functions_disabled`.
132
+
133
+ ## 3. On your server: what the order unlocked
134
+
135
+ Your server writes the progression as it always has, and the order it gets back carries a receipt for
136
+ each achievement threshold the write crossed, plus any achievement it unlocked by catching up. The
137
+ server entry's `achievementReceipts` reads them off; the package itself is installed as
138
+ [getting-started.md](getting-started.md) describes.
139
+
140
+ ```js
141
+ import { achievementReceipts } from '@indiegems/gem-web-sdk/server';
142
+
143
+ const order = await gem.api.addEntityProgression('players', playerId, 'boards_cleared', {
144
+ amount: 1,
145
+ });
146
+ for (const r of achievementReceipts(order)) {
147
+ if (r.outcome === 'unlocked') log(`${playerId} unlocked ${r.achievementId} (+${r.points})`);
148
+ if (r.outcome === 'suppressed') log(`${r.achievementId} not unlocked: ${r.reason}`);
149
+ }
150
+ ```
151
+
152
+ | `outcome` | Means |
153
+ | ------------------ | -------------------------------------------------------------------------------- |
154
+ | `unlocked` | This order unlocked it, and `points` is what it credited |
155
+ | `already_unlocked` | The write crossed it again — after a reset — and the player already holds it |
156
+ | `suppressed` | It was not unlocked, and `reason` says why — for your logs, never for the player |
157
+
158
+ The list is empty when the write crossed no threshold, which is almost every write. For a batch,
159
+ read each entry of the result's `orders`. For a "best score" progression, use the `progression_max`
160
+ operation on `gem.api.createEntityOrder`; its result reads the same way.
161
+
162
+ **A receipt is a report, not a trigger.** A retried or replayed order reports nothing — the
163
+ threshold is already behind it — so a game-side reward keyed off `unlocked` is lost whenever the
164
+ first response never arrived. Put a game reward in the same threshold's `rewards`, which the
165
+ platform pays on the crossing itself, and read the player's standing from the progression. Those
166
+ rewards pay again if the progression resets and is crossed again; the achievement does not.
167
+
168
+ ## 4. In the game: showing the player's standing
169
+
170
+ The progression reads your client already makes carry each achievement on the progression and
171
+ whether this player has it:
172
+
173
+ ```js
174
+ const p = await gem.me.progressions.get('boards_cleared');
175
+ for (const a of p?.achievements ?? []) {
176
+ const state = a.revoked ? 'removed' : a.unlockedAt !== undefined ? 'unlocked' : 'locked';
177
+ // a.id, a.threshold, a.points, a.hidden
178
+ }
179
+ if (p?.achievementsUnavailable) {
180
+ // The list is right, but whether each is unlocked could not be read just now. Show no
181
+ // "locked" state from this read. A later get() asks the backend again rather than the cache.
182
+ }
183
+ ```
184
+
185
+ The standing comes from the platform's unlock record, not from the progression's value — so after a
186
+ reset, `isCompleted` is `false` and `currentValue` is back at zero, and the achievement still reads
187
+ as unlocked. `revoked` means the platform removed an unlock; it cannot be earned again. `points` is
188
+ what the achievement awards while locked, and what the unlock credited once held.
189
+
190
+ `achievements` is absent on a progression with none, and on a character's progressions: an
191
+ achievement belongs to the player. A player who has never written the progression has no record of
192
+ it, so `get` returns `null` and there is no list to read — to show locked achievements before the
193
+ first write, list them from your own game config.
194
+
195
+ The client caches what it reads, and an unlock happens on your server, so the client does not see
196
+ it by itself. When your server tells the game a player unlocked something, call
197
+ `gem.me.inventory.refresh()` before reading the progression again.
198
+
199
+ **This read is for display.** Never send it to your server as proof that a player holds something;
200
+ a modified client can report anything. Your server reads the standing itself.
201
+
202
+ **`hidden` is a presentation rule, not a secret.** Your game already knows a hidden achievement's
203
+ threshold — it is in your config. The arcade hides its name and description from players until they
204
+ unlock it; if your game shows its own achievement list, do the same.
205
+
206
+ ## 5. Points are prestige: the Achievement Score
207
+
208
+ Achievement points are a player's permanent standing on the arcade. They are **never spent,
209
+ bought, boosted, decayed or expired**, and they are **removed only for fraud or moderation**.
210
+
211
+ - **Never bought.** There is no achievement write API, and nothing a player pays for may write an
212
+ achievement's progression (§1) — so no price, pack or recipe reaches a point.
213
+ - **Never boosted.** A Boost multiplies what some platform-currency grants pay. It never touches
214
+ achievement points: an unlock credits exactly the points your release registered for that
215
+ achievement, for every player, boosted or not.
216
+ - **Never spent, decayed or expired.** Nothing consumes points, and resetting the progression,
217
+ retiring the achievement in a later release or a player's time away never takes them back.
218
+ - **Removed only for fraud or moderation.** The platform can revoke an unlock — a player who
219
+ cheated, or a game whose server was compromised — and that removes its points. A revoked
220
+ achievement cannot be earned again. This is the platform's decision, never a game's.
221
+
222
+ The **Achievement Score** is the one number the arcade shows for them: every point a player holds
223
+ from every game's achievements, plus the points of the platform's own achievements. Your game never
224
+ computes it, and should not restate it as a currency of its own: it moves only when an unlock
225
+ credits points or a revocation removes them.
226
+
227
+ ## What to read next
228
+
229
+ - [`inventory-and-catalogs.md`](inventory-and-catalogs.md) §13 — progressions and platform grants,
230
+ which this page builds on.
231
+ - [`server-bundles.md`](server-bundles.md) — how your server gets the credential that makes a write
232
+ eligible.