@oasiz/sdk 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @oasiz/sdk
2
2
 
3
- Typed SDK for integrating games with the Oasiz platform. Handles score submission, haptic feedback, cross-session state persistence, multiplayer room codes, and app lifecycle events.
3
+ Typed SDK for integrating games with the Oasiz platform. Handles score submission, haptic feedback, cross-session state persistence, multiplayer room codes, navigation hooks, and app lifecycle events for local development.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @oasiz/sdk
8
+ npm install @oasiz/sdk@^0.1.0
9
9
  ```
10
10
 
11
11
  ## Quick start
@@ -13,17 +13,27 @@ npm install @oasiz/sdk
13
13
  ```ts
14
14
  import { oasiz } from "@oasiz/sdk";
15
15
 
16
- // 1. Load persisted state at the start of each session
16
+ // 1. Emit score normalization config once on init
17
+ oasiz.emitScoreConfig({
18
+ anchors: [
19
+ { raw: 30, normalized: 100 },
20
+ { raw: 60, normalized: 300 },
21
+ { raw: 120, normalized: 600 },
22
+ { raw: 300, normalized: 950 },
23
+ ],
24
+ });
25
+
26
+ // 2. Load persisted state at the start of each session
17
27
  const state = oasiz.loadGameState();
18
28
  let level = typeof state.level === "number" ? state.level : 1;
19
29
 
20
- // 2. Save state at checkpoints
30
+ // 3. Save state at checkpoints
21
31
  oasiz.saveGameState({ level, coins: 42 });
22
32
 
23
- // 3. Trigger haptics on key events
33
+ // 4. Trigger haptics on key events
24
34
  oasiz.triggerHaptic("medium");
25
35
 
26
- // 4. Submit score when the game ends
36
+ // 5. Submit score when the game ends
27
37
  oasiz.submitScore(score);
28
38
  ```
29
39
 
@@ -33,14 +43,44 @@ oasiz.submitScore(score);
33
43
 
34
44
  ### `oasiz.submitScore(score: number)`
35
45
 
36
- Submit the player's score. The platform handles leaderboard persistence — do not track high scores locally.
46
+ Submit the player's final score at game over. Call this exactly once per session, when the game ends. The platform handles leaderboard persistence — do not track high scores locally.
37
47
 
38
48
  ```ts
39
- oasiz.submitScore(this.score);
49
+ private onGameOver(): void {
50
+ oasiz.submitScore(Math.floor(this.score));
51
+ }
40
52
  ```
41
53
 
42
54
  - `score` must be a non-negative integer. Floats are floored automatically.
43
- - When and how often you call this depends on your game type (once at game over, end of each level, or throttled for long sessions).
55
+ - Do not call on intermediate scores or level completions, only on final game over.
56
+
57
+ ---
58
+
59
+ ### `oasiz.emitScoreConfig(config)`
60
+
61
+ Maps raw score values to the platform's normalized 0–1000 scale. Call once during initialization, not every frame.
62
+
63
+ ```ts
64
+ oasiz.emitScoreConfig({
65
+ anchors: [
66
+ { raw: 10, normalized: 100 }, // beginner
67
+ { raw: 30, normalized: 300 }, // good
68
+ { raw: 75, normalized: 600 }, // great
69
+ { raw: 200, normalized: 950 }, // godlike
70
+ ],
71
+ });
72
+ ```
73
+
74
+ **Anchor rules:**
75
+ - Exactly 4 anchors required.
76
+ - `raw` values must be strictly increasing.
77
+ - `normalized` values must end at exactly `950`.
78
+ - Choose thresholds based on realistic player skill bands.
79
+
80
+ **Practical guidance by game type:**
81
+ - Survival / time games → use seconds survived as `raw`
82
+ - Score accumulation games → use points as `raw`
83
+ - Puzzle games → use level reached or stars earned as `raw`
44
84
 
45
85
  ---
46
86
 
@@ -59,14 +99,23 @@ type HapticType = "light" | "medium" | "heavy" | "success" | "error";
59
99
  | `"light"` | UI button taps, menu navigation, D-pad press |
60
100
  | `"medium"` | Collecting items, standard collisions, scoring |
61
101
  | `"heavy"` | Explosions, major impacts, screen shake |
62
- | `"success"` | Level complete, achievement unlocked |
102
+ | `"success"` | Level complete, new high score, achievement unlocked |
63
103
  | `"error"` | Damage taken, game over, invalid action |
64
104
 
65
105
  ```ts
106
+ // UI buttons — always light
66
107
  button.addEventListener("click", () => {
67
108
  oasiz.triggerHaptic("light");
68
109
  });
69
110
 
111
+ // Tiered hit feedback
112
+ private onBallHit(zone: "center" | "edge"): void {
113
+ if (this.settings.haptics) {
114
+ oasiz.triggerHaptic(zone === "center" ? "success" : "medium");
115
+ }
116
+ }
117
+
118
+ // Game over
70
119
  private onGameOver(): void {
71
120
  oasiz.submitScore(this.score);
72
121
  if (this.settings.haptics) {
@@ -75,54 +124,75 @@ private onGameOver(): void {
75
124
  }
76
125
  ```
77
126
 
127
+ Haptics are throttled internally (50ms cooldown) to prevent spam.
128
+
78
129
  ---
79
130
 
80
131
  ## Game state persistence
81
132
 
82
- Persist cross-session data such as unlocked levels, inventory, or lifetime stats. Available across devices and app reinstalls.
133
+ Persist cross-session data such as unlocked levels, inventory, or lifetime stats. State is stored per-user per-game in the Oasiz backend — available across devices and app reinstalls.
83
134
 
84
135
  ### `oasiz.loadGameState(): Record<string, unknown>`
85
136
 
86
- Returns the player's saved state synchronously. Returns `{}` if no state has been saved yet.
137
+ Returns the player's saved state synchronously. Returns `{}` if no state has been saved yet. Call once at the start of the game.
87
138
 
88
139
  ```ts
89
- const state = oasiz.loadGameState();
90
- this.level = typeof state.level === "number" ? state.level : 1;
140
+ private initFromSavedState(): void {
141
+ const state = oasiz.loadGameState();
142
+ this.level = typeof state.level === "number" ? state.level : 1;
143
+ this.lifetimeHits = typeof state.lifetimeHits === "number" ? state.lifetimeHits : 0;
144
+ this.unlockedSkins = Array.isArray(state.unlockedSkins) ? state.unlockedSkins : [];
145
+ }
91
146
  ```
92
147
 
148
+ Always validate the shape of loaded data — it may be `{}` on first play.
149
+
93
150
  ### `oasiz.saveGameState(state: Record<string, unknown>)`
94
151
 
95
- Queues a debounced save. Call freely at checkpoints.
152
+ Queues a debounced save. Saves are batched automatically — call freely at checkpoints without worrying about request spam.
96
153
 
97
154
  ```ts
98
- oasiz.saveGameState({ level: this.level, unlockedSkins: this.unlockedSkins });
155
+ // Save after each level completion
156
+ private onLevelComplete(): void {
157
+ this.level += 1;
158
+ oasiz.saveGameState({
159
+ level: this.level,
160
+ lifetimeHits: this.lifetimeHits,
161
+ unlockedSkins: this.unlockedSkins,
162
+ });
163
+ }
99
164
  ```
100
165
 
166
+ **Rules:**
101
167
  - State must be a plain JSON object (not an array or primitive).
102
- - Use `saveGameState` instead of `localStorage` for cross-session progress.
103
- - Do not store scores here — use `submitScore`.
168
+ - Do not use `localStorage` for cross-session progress — use `saveGameState` so data syncs across platforms.
169
+ - Do not store scores here — scores are submitted via `submitScore`.
104
170
 
105
171
  ### `oasiz.flushGameState()`
106
172
 
107
- Forces an immediate write, bypassing the debounce. Use at game over or before page unload.
173
+ Forces an immediate write, bypassing the debounce. Use at important checkpoints like game over or before the page unloads.
108
174
 
109
175
  ```ts
110
- oasiz.saveGameState({ level: this.level });
111
- oasiz.flushGameState();
112
- oasiz.submitScore(this.score);
176
+ private onGameOver(): void {
177
+ oasiz.saveGameState({ level: this.level, lifetimeHits: this.lifetimeHits });
178
+ oasiz.flushGameState(); // ensure it lands before the page closes
179
+ oasiz.submitScore(this.score);
180
+ }
113
181
  ```
114
182
 
115
183
  ---
116
184
 
117
185
  ## Lifecycle
118
186
 
187
+ The platform dispatches lifecycle events when the app goes to the background or returns to the foreground. Subscribe to pause game loops and audio accordingly.
188
+
119
189
  ### `oasiz.onPause(callback: () => void): Unsubscribe`
120
190
  ### `oasiz.onResume(callback: () => void): Unsubscribe`
121
191
 
122
192
  Both return an unsubscribe function.
123
193
 
124
194
  ```ts
125
- const offPause = oasiz.onPause(() => {
195
+ const offPause = oasiz.onPause(() => {
126
196
  this.gameLoop.stop();
127
197
  this.bgMusic.pause();
128
198
  });
@@ -132,39 +202,126 @@ const offResume = oasiz.onResume(() => {
132
202
  this.bgMusic.play();
133
203
  });
134
204
 
205
+ // Clean up when the game is destroyed
135
206
  offPause();
136
207
  offResume();
137
208
  ```
138
209
 
139
210
  ---
140
211
 
212
+ ## Navigation
213
+
214
+ Use navigation hooks when your game needs to control back behavior (Android back / web Escape) or participate in host-driven close events.
215
+
216
+ ### `oasiz.onBackButton(callback: () => void): Unsubscribe`
217
+
218
+ Registers a callback for platform back actions. While at least one back listener is subscribed, back actions are routed to your game instead of immediately closing it.
219
+
220
+ Use this for pause menus, in-game overlays, or custom back-stack behavior.
221
+
222
+ ```ts
223
+ const offBack = oasiz.onBackButton(() => {
224
+ if (this.isPauseMenuOpen) {
225
+ this.closePauseMenu();
226
+ return;
227
+ }
228
+ this.openPauseMenu();
229
+ });
230
+
231
+ // Restore default host back behavior when no longer needed
232
+ offBack();
233
+ ```
234
+
235
+ ### `oasiz.leaveGame(): void`
236
+
237
+ Programmatically request the host to close the current game (for example, from a Quit button inside your game UI).
238
+
239
+ ```ts
240
+ quitButton.addEventListener("click", () => {
241
+ oasiz.leaveGame();
242
+ });
243
+ ```
244
+
245
+ ### `oasiz.onLeaveGame(callback: () => void): Unsubscribe`
246
+
247
+ Registers a callback fired when the host initiates closing the game (for example, close button, gesture, or host navigation). Use this for lightweight cleanup.
248
+
249
+ ```ts
250
+ const offLeave = oasiz.onLeaveGame(() => {
251
+ oasiz.flushGameState();
252
+ this.bgMusic.pause();
253
+ });
254
+
255
+ // Clean up listener when destroyed
256
+ offLeave();
257
+ ```
258
+
259
+ ---
260
+
141
261
  ## Multiplayer
142
262
 
143
- ### `oasiz.shareRoomCode(code: string | null)`
263
+ ### `oasiz.shareRoomCode(code: string | null, options?: { inviteOverride?: boolean })`
264
+
265
+ Notify the platform of the active multiplayer room so friends can join via the invite system. Pass `null` when leaving a room.
144
266
 
145
- Notify the platform of the active multiplayer room. Pass `null` when leaving.
267
+ Set `inviteOverride: true` when your game wants to hide the platform invite pill and render its own invite button/UI. The platform still tracks the room code, but your game owns the invite entry point.
146
268
 
147
269
  ```ts
270
+ import { insertCoin, getRoomCode } from "playroomkit";
271
+ import { oasiz } from "@oasiz/sdk";
272
+
273
+ await insertCoin({ skipLobby: true });
148
274
  oasiz.shareRoomCode(getRoomCode());
275
+
276
+ // On disconnect
149
277
  oasiz.shareRoomCode(null);
150
278
  ```
151
279
 
152
- ### Read-only properties
280
+ ```ts
281
+ // Game-owned invite UI: hide the platform pill, keep room tracking
282
+ oasiz.shareRoomCode(getRoomCode(), { inviteOverride: true });
283
+ ```
284
+
285
+ If you still want to use the platform invite sheet from your own in-game button, combine it with `openInviteModal()`:
286
+
287
+ ```ts
288
+ import { openInviteModal, shareRoomCode } from "@oasiz/sdk";
289
+
290
+ shareRoomCode("ABCD", { inviteOverride: true });
291
+
292
+ inviteButton.addEventListener("click", () => {
293
+ openInviteModal();
294
+ });
295
+ ```
153
296
 
154
- | Property | Type | Description |
155
- |---|---|---|
156
- | `oasiz.gameId` | string \| undefined | The platform's game ID |
157
- | `oasiz.roomCode` | string \| undefined | Pre-filled room code from invite |
158
- | `oasiz.playerName` | string \| undefined | Player's display name |
159
- | `oasiz.playerAvatar` | string \| undefined | Player's profile picture URL |
297
+ ### Read-only injected values
298
+
299
+ These are populated by the platform before the game loads. Always check for `undefined` before using.
300
+
301
+ ```ts
302
+ // The platform's internal game ID
303
+ const gameId = oasiz.gameId;
304
+
305
+ // Pre-filled room code for auto-joining a friend's session
306
+ if (oasiz.roomCode) {
307
+ await connectToRoom(oasiz.roomCode);
308
+ }
309
+
310
+ // Player identity for multiplayer games
311
+ const name = oasiz.playerName;
312
+ const avatar = oasiz.playerAvatar;
313
+ ```
160
314
 
161
315
  ---
162
316
 
163
317
  ## Named exports
164
318
 
319
+ All methods are also available as named exports if you prefer not to use the `oasiz` namespace object:
320
+
165
321
  ```ts
166
322
  import {
167
323
  submitScore,
324
+ emitScoreConfig,
168
325
  triggerHaptic,
169
326
  loadGameState,
170
327
  saveGameState,
@@ -172,6 +329,9 @@ import {
172
329
  shareRoomCode,
173
330
  onPause,
174
331
  onResume,
332
+ onBackButton,
333
+ onLeaveGame,
334
+ leaveGame,
175
335
  getGameId,
176
336
  getRoomCode,
177
337
  getPlayerName,
@@ -184,15 +344,17 @@ import {
184
344
  ## TypeScript types
185
345
 
186
346
  ```ts
187
- import type { HapticType, GameState } from "@oasiz/sdk";
347
+ import type { HapticType, ScoreConfig, ScoreAnchor, GameState } from "@oasiz/sdk";
188
348
  ```
189
349
 
190
350
  ---
191
351
 
192
352
  ## Local development
193
353
 
194
- All methods safely no-op when the platform bridges are not injected. In development mode a console warning is logged:
354
+ All methods safely no-op when the platform bridges are not injected. In development mode a console warning is logged so you know the call was made:
195
355
 
196
356
  ```
197
357
  [oasiz/sdk] submitScore bridge is unavailable. This is expected in local development.
198
358
  ```
359
+
360
+ No crashes, no special setup required for local dev.
package/dist/index.cjs CHANGED
@@ -20,15 +20,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ emitScoreConfig: () => emitScoreConfig,
23
24
  flushGameState: () => flushGameState,
24
25
  getGameId: () => getGameId,
25
26
  getPlayerAvatar: () => getPlayerAvatar,
26
27
  getPlayerName: () => getPlayerName,
27
28
  getRoomCode: () => getRoomCode,
29
+ leaveGame: () => leaveGame,
28
30
  loadGameState: () => loadGameState,
29
31
  oasiz: () => oasiz,
32
+ onBackButton: () => onBackButton,
33
+ onLeaveGame: () => onLeaveGame,
30
34
  onPause: () => onPause,
31
35
  onResume: () => onResume,
36
+ openInviteModal: () => openInviteModal,
32
37
  saveGameState: () => saveGameState,
33
38
  shareRoomCode: () => shareRoomCode,
34
39
  submitScore: () => submitScore,
@@ -71,10 +76,10 @@ function getBridgeWindow2() {
71
76
  }
72
77
  return window;
73
78
  }
74
- function shareRoomCode(roomCode) {
79
+ function shareRoomCode(roomCode, options) {
75
80
  const bridge = getBridgeWindow2();
76
81
  if (typeof bridge?.shareRoomCode === "function") {
77
- bridge.shareRoomCode(roomCode);
82
+ bridge.shareRoomCode(roomCode, options);
78
83
  return;
79
84
  }
80
85
  if (isDevelopment2()) {
@@ -83,6 +88,18 @@ function shareRoomCode(roomCode) {
83
88
  );
84
89
  }
85
90
  }
91
+ function openInviteModal() {
92
+ const bridge = getBridgeWindow2();
93
+ if (typeof bridge?.openInviteModal === "function") {
94
+ bridge.openInviteModal();
95
+ return;
96
+ }
97
+ if (isDevelopment2()) {
98
+ console.warn(
99
+ "[oasiz/sdk] openInviteModal bridge is unavailable. This is expected in local development."
100
+ );
101
+ }
102
+ }
86
103
  function getGameId() {
87
104
  const bridge = getBridgeWindow2();
88
105
  return bridge?.__GAME_ID__;
@@ -133,6 +150,14 @@ function submitScore(score) {
133
150
  }
134
151
  warnMissingBridge("submitScore");
135
152
  }
153
+ function emitScoreConfig(config) {
154
+ const bridge = getBridgeWindow3();
155
+ if (typeof bridge?.emitScoreConfig === "function") {
156
+ bridge.emitScoreConfig(config);
157
+ return;
158
+ }
159
+ warnMissingBridge("emitScoreConfig");
160
+ }
136
161
 
137
162
  // src/state.ts
138
163
  function isDevelopment4() {
@@ -225,16 +250,90 @@ function onResume(callback) {
225
250
  return addLifecycleListener("oasiz:resume", callback);
226
251
  }
227
252
 
253
+ // src/navigation.ts
254
+ var activeBackListeners = 0;
255
+ function isDevelopment6() {
256
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
257
+ return nodeEnv !== "production";
258
+ }
259
+ function getBridgeWindow5() {
260
+ if (typeof window === "undefined") {
261
+ return void 0;
262
+ }
263
+ return window;
264
+ }
265
+ function warnMissingBridge3(methodName) {
266
+ if (isDevelopment6()) {
267
+ console.warn(
268
+ "[oasiz/sdk] " + methodName + " bridge is unavailable. This is expected in local development."
269
+ );
270
+ }
271
+ }
272
+ function addNavigationListener(eventName, callback) {
273
+ if (typeof window === "undefined") {
274
+ if (isDevelopment6()) {
275
+ console.warn(
276
+ "[oasiz/sdk] " + eventName + " listener registered without a browser window. This is expected in local development."
277
+ );
278
+ }
279
+ return () => {
280
+ };
281
+ }
282
+ const handler = () => callback();
283
+ window.addEventListener(eventName, handler);
284
+ return () => window.removeEventListener(eventName, handler);
285
+ }
286
+ function onBackButton(callback) {
287
+ const off = addNavigationListener("oasiz:back", callback);
288
+ const bridge = getBridgeWindow5();
289
+ activeBackListeners += 1;
290
+ if (activeBackListeners === 1) {
291
+ if (typeof bridge?.__oasizSetBackOverride === "function") {
292
+ bridge.__oasizSetBackOverride(true);
293
+ } else {
294
+ warnMissingBridge3("__oasizSetBackOverride");
295
+ }
296
+ }
297
+ return () => {
298
+ off();
299
+ activeBackListeners = Math.max(0, activeBackListeners - 1);
300
+ if (activeBackListeners === 0) {
301
+ const currentBridge = getBridgeWindow5();
302
+ if (typeof currentBridge?.__oasizSetBackOverride === "function") {
303
+ currentBridge.__oasizSetBackOverride(false);
304
+ } else {
305
+ warnMissingBridge3("__oasizSetBackOverride");
306
+ }
307
+ }
308
+ };
309
+ }
310
+ function onLeaveGame(callback) {
311
+ return addNavigationListener("oasiz:leave", callback);
312
+ }
313
+ function leaveGame() {
314
+ const bridge = getBridgeWindow5();
315
+ if (typeof bridge?.__oasizLeaveGame === "function") {
316
+ bridge.__oasizLeaveGame();
317
+ return;
318
+ }
319
+ warnMissingBridge3("__oasizLeaveGame");
320
+ }
321
+
228
322
  // src/index.ts
229
323
  var oasiz = {
230
324
  submitScore,
325
+ emitScoreConfig,
231
326
  triggerHaptic,
232
327
  loadGameState,
233
328
  saveGameState,
234
329
  flushGameState,
235
330
  shareRoomCode,
331
+ openInviteModal,
236
332
  onPause,
237
333
  onResume,
334
+ onBackButton,
335
+ onLeaveGame,
336
+ leaveGame,
238
337
  get gameId() {
239
338
  return getGameId();
240
339
  },
@@ -250,15 +349,20 @@ var oasiz = {
250
349
  };
251
350
  // Annotate the CommonJS export names for ESM import in node:
252
351
  0 && (module.exports = {
352
+ emitScoreConfig,
253
353
  flushGameState,
254
354
  getGameId,
255
355
  getPlayerAvatar,
256
356
  getPlayerName,
257
357
  getRoomCode,
358
+ leaveGame,
258
359
  loadGameState,
259
360
  oasiz,
361
+ onBackButton,
362
+ onLeaveGame,
260
363
  onPause,
261
364
  onResume,
365
+ openInviteModal,
262
366
  saveGameState,
263
367
  shareRoomCode,
264
368
  submitScore,