@oasiz/sdk 1.3.0 → 1.5.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,53 +1,63 @@
1
- # @oasiz/sdk
1
+ # Oasiz game SDKs
2
2
 
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.
3
+ Games on Oasiz can integrate using either of these official SDKs:
4
4
 
5
- ## Install
5
+ | Platform | Package | Use for |
6
+ | --- | --- | --- |
7
+ | **HTML5 / TypeScript** | [`@oasiz/sdk`](#html5--typescript-oasizsdk) (npm) | Canvas, Phaser, custom JS/TS, any browser game |
8
+ | **Unity WebGL** | [Unity runtime](#unity-webgl-sdk) in this repo (`packages/OasizSDK/`) | Unity projects targeting WebGL |
9
+
10
+ Both talk to the same host bridges (`window.submitScore`, `__oasizLeaveGame`, layout APIs, custom DOM events such as `oasiz:pause`, etc.). Unsupported hosts no-op safely; local dev usually logs warnings instead of crashing.
11
+
12
+ ---
13
+
14
+ ## HTML5 / TypeScript (`@oasiz/sdk`)
15
+
16
+ Typed SDK for integrating browser games with the Oasiz platform: score, haptics, cross-session state, multiplayer hooks, layout (safe area, leaderboard visibility), navigation (back / leave), and lifecycle events.
17
+
18
+ ### Install
6
19
 
7
20
  ```bash
8
- npm install @oasiz/sdk@^0.1.0
21
+ npm install @oasiz/sdk
9
22
  ```
10
23
 
11
- ## Quick start
24
+ ### Quick start
12
25
 
13
26
  ```ts
14
27
  import { oasiz } from "@oasiz/sdk";
15
28
 
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
29
+ // 1. Load persisted state at the start of each session
27
30
  const state = oasiz.loadGameState();
28
31
  let level = typeof state.level === "number" ? state.level : 1;
29
32
 
30
- // 3. Save state at checkpoints
33
+ // 2. Save state at checkpoints
31
34
  oasiz.saveGameState({ level, coins: 42 });
32
35
 
33
- // 4. Trigger haptics on key events
36
+ // 3. Trigger haptics on key events
34
37
  oasiz.triggerHaptic("medium");
35
38
 
39
+ // 4. Respect the host's top safe area
40
+ document.documentElement.style.setProperty(
41
+ "--safe-top",
42
+ `${oasiz.safeAreaTop}px`,
43
+ );
44
+
36
45
  // 5. Submit score when the game ends
37
46
  oasiz.submitScore(score);
38
47
 
39
- // 6. Optionally surface console logs in-game while debugging
48
+ // 6. Optionally hide the leaderboard while a custom overlay is open
49
+ oasiz.setLeaderboardVisible(false);
50
+
51
+ // 7. Optionally surface console logs in-game while debugging
40
52
  oasiz.enableLogOverlay({
41
53
  enabled: new URLSearchParams(window.location.search).has("oasizLogs"),
42
54
  collapsed: true,
43
55
  });
44
56
  ```
45
57
 
46
- ---
58
+ ### Score
47
59
 
48
- ## Score
49
-
50
- ### `oasiz.submitScore(score: number)`
60
+ #### `oasiz.submitScore(score: number)`
51
61
 
52
62
  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.
53
63
 
@@ -60,39 +70,9 @@ private onGameOver(): void {
60
70
  - `score` must be a non-negative integer. Floats are floored automatically.
61
71
  - Do not call on intermediate scores or level completions, only on final game over.
62
72
 
63
- ---
64
-
65
- ### `oasiz.emitScoreConfig(config)`
66
-
67
- Maps raw score values to the platform's normalized 0–1000 scale. Call once during initialization, not every frame.
68
-
69
- ```ts
70
- oasiz.emitScoreConfig({
71
- anchors: [
72
- { raw: 10, normalized: 100 }, // beginner
73
- { raw: 30, normalized: 300 }, // good
74
- { raw: 75, normalized: 600 }, // great
75
- { raw: 200, normalized: 950 }, // godlike
76
- ],
77
- });
78
- ```
73
+ ### Haptics
79
74
 
80
- **Anchor rules:**
81
- - Exactly 4 anchors required.
82
- - `raw` values must be strictly increasing.
83
- - `normalized` values must end at exactly `950`.
84
- - Choose thresholds based on realistic player skill bands.
85
-
86
- **Practical guidance by game type:**
87
- - Survival / time games → use seconds survived as `raw`
88
- - Score accumulation games → use points as `raw`
89
- - Puzzle games → use level reached or stars earned as `raw`
90
-
91
- ---
92
-
93
- ## Haptics
94
-
95
- ### `oasiz.triggerHaptic(type: HapticType)`
75
+ #### `oasiz.triggerHaptic(type: HapticType)`
96
76
 
97
77
  Trigger native haptic feedback. Always guard with the user's haptics setting.
98
78
 
@@ -101,7 +81,7 @@ type HapticType = "light" | "medium" | "heavy" | "success" | "error";
101
81
  ```
102
82
 
103
83
  | Type | When to use |
104
- |---|---|
84
+ | --- | --- |
105
85
  | `"light"` | UI button taps, menu navigation, D-pad press |
106
86
  | `"medium"` | Collecting items, standard collisions, scoring |
107
87
  | `"heavy"` | Explosions, major impacts, screen shake |
@@ -132,11 +112,9 @@ private onGameOver(): void {
132
112
 
133
113
  Haptics are throttled internally (50ms cooldown) to prevent spam.
134
114
 
135
- ---
136
-
137
- ## Debugging
115
+ ### Debugging
138
116
 
139
- ### `oasiz.enableLogOverlay(options?: LogOverlayOptions)`
117
+ #### `oasiz.enableLogOverlay(options?: LogOverlayOptions)`
140
118
 
141
119
  Mount an opt-in in-game console viewer for local debugging, QA sessions, or creator support. It mirrors `console.log`, `console.info`, `console.warn`, `console.error`, and `console.debug` into a floating overlay inside the game iframe. The overlay can be collapsed, repositioned by dragging the top bar, and resized from the bottom-right corner while the action buttons remain clickable.
142
120
 
@@ -153,6 +131,7 @@ logOverlay.destroy();
153
131
  ```
154
132
 
155
133
  Options:
134
+
156
135
  - `enabled`: defaults to `true`. Pass your own flag or query-param check here.
157
136
  - `collapsed`: start with only the toggle pill visible.
158
137
  - `maxEntries`: cap retained log lines. Defaults to `200`.
@@ -160,20 +139,18 @@ Options:
160
139
 
161
140
  The returned handle supports `show()`, `hide()`, `clear()`, `isVisible()`, and `destroy()`.
162
141
 
163
- ---
164
-
165
- ## Game state persistence
142
+ ### Game state persistence
166
143
 
167
144
  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.
168
145
 
169
- ### `oasiz.loadGameState(): Record<string, unknown>`
146
+ #### `oasiz.loadGameState(): Record<string, unknown>`
170
147
 
171
148
  Returns the player's saved state synchronously. Returns `{}` if no state has been saved yet. Call once at the start of the game.
172
149
 
173
150
  ```ts
174
151
  private initFromSavedState(): void {
175
152
  const state = oasiz.loadGameState();
176
- this.level = typeof state.level === "number" ? state.level : 1;
153
+ this.level = typeof state.level === "number" ? state.level : 1;
177
154
  this.lifetimeHits = typeof state.lifetimeHits === "number" ? state.lifetimeHits : 0;
178
155
  this.unlockedSkins = Array.isArray(state.unlockedSkins) ? state.unlockedSkins : [];
179
156
  }
@@ -181,7 +158,7 @@ private initFromSavedState(): void {
181
158
 
182
159
  Always validate the shape of loaded data — it may be `{}` on first play.
183
160
 
184
- ### `oasiz.saveGameState(state: Record<string, unknown>)`
161
+ #### `oasiz.saveGameState(state: Record<string, unknown>)`
185
162
 
186
163
  Queues a debounced save. Saves are batched automatically — call freely at checkpoints without worrying about request spam.
187
164
 
@@ -198,11 +175,12 @@ private onLevelComplete(): void {
198
175
  ```
199
176
 
200
177
  **Rules:**
178
+
201
179
  - State must be a plain JSON object (not an array or primitive).
202
180
  - Do not use `localStorage` for cross-session progress — use `saveGameState` so data syncs across platforms.
203
181
  - Do not store scores here — scores are submitted via `submitScore`.
204
182
 
205
- ### `oasiz.flushGameState()`
183
+ #### `oasiz.flushGameState()`
206
184
 
207
185
  Forces an immediate write, bypassing the debounce. Use at important checkpoints like game over or before the page unloads.
208
186
 
@@ -214,19 +192,63 @@ private onGameOver(): void {
214
192
  }
215
193
  ```
216
194
 
217
- ---
195
+ ### Layout
196
+
197
+ Use the runtime safe-area value instead of hardcoded top offsets. The host reports the current top inset in **CSS pixels** for persistent chrome such as the back button and top controls.
198
+
199
+ #### `oasiz.getSafeAreaTop(): number`
200
+
201
+ Returns the current top inset. The host may expose `window.getSafeAreaTop()` or `window.__OASIZ_SAFE_AREA_TOP__`. Unsupported hosts return `0`.
202
+
203
+ ```ts
204
+ const safeTop = oasiz.getSafeAreaTop();
205
+ document.documentElement.style.setProperty("--safe-top", `${safeTop}px`);
206
+ ```
207
+
208
+ #### `oasiz.safeAreaTop`
209
+
210
+ Getter alias for `getSafeAreaTop()`.
211
+
212
+ Recommended CSS pattern:
213
+
214
+ ```css
215
+ :root {
216
+ --safe-top: 0px;
217
+ }
218
+
219
+ #top-bar {
220
+ padding-top: var(--safe-top);
221
+ }
222
+ ```
223
+
224
+ #### `oasiz.setLeaderboardVisible(visible: boolean): void`
225
+
226
+ Show or hide the host leaderboard UI from inside the game. This only affects the leaderboard; back and social controls remain visible. Calls `window.__oasizSetLeaderboardVisible` when present.
218
227
 
219
- ## Lifecycle
228
+ ```ts
229
+ function openCustomOverlay(): void {
230
+ oasiz.setLeaderboardVisible(false);
231
+ }
232
+
233
+ function closeCustomOverlay(): void {
234
+ oasiz.setLeaderboardVisible(true);
235
+ }
236
+ ```
237
+
238
+ Unsupported hosts safely no-op.
239
+
240
+ ### Lifecycle
220
241
 
221
242
  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.
222
243
 
223
- ### `oasiz.onPause(callback: () => void): Unsubscribe`
224
- ### `oasiz.onResume(callback: () => void): Unsubscribe`
244
+ #### `oasiz.onPause(callback: () => void): Unsubscribe`
245
+
246
+ #### `oasiz.onResume(callback: () => void): Unsubscribe`
225
247
 
226
248
  Both return an unsubscribe function.
227
249
 
228
250
  ```ts
229
- const offPause = oasiz.onPause(() => {
251
+ const offPause = oasiz.onPause(() => {
230
252
  this.gameLoop.stop();
231
253
  this.bgMusic.pause();
232
254
  });
@@ -241,18 +263,18 @@ offPause();
241
263
  offResume();
242
264
  ```
243
265
 
244
- ---
245
-
246
- ## Navigation
266
+ ### Navigation
247
267
 
248
268
  Use navigation hooks when your game needs to control back behavior (Android back / web Escape) or participate in host-driven close events.
249
269
 
250
- ### `oasiz.onBackButton(callback: () => void): Unsubscribe`
270
+ #### `oasiz.onBackButton(callback: () => void): Unsubscribe`
251
271
 
252
272
  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.
253
273
 
254
274
  Use this for pause menus, in-game overlays, or custom back-stack behavior.
255
275
 
276
+ **If your callback throws**, the SDK calls `leaveGame()` (host close) and **rethrows** the error so you still see it in devtools or error reporting. Non-`Error` throws are normalized to an `Error` (strings become the message; otherwise `"Back button callback failed."`).
277
+
256
278
  ```ts
257
279
  const offBack = oasiz.onBackButton(() => {
258
280
  if (this.isPauseMenuOpen) {
@@ -266,7 +288,7 @@ const offBack = oasiz.onBackButton(() => {
266
288
  offBack();
267
289
  ```
268
290
 
269
- ### `oasiz.leaveGame(): void`
291
+ #### `oasiz.leaveGame(): void`
270
292
 
271
293
  Programmatically request the host to close the current game (for example, from a Quit button inside your game UI).
272
294
 
@@ -276,7 +298,7 @@ quitButton.addEventListener("click", () => {
276
298
  });
277
299
  ```
278
300
 
279
- ### `oasiz.onLeaveGame(callback: () => void): Unsubscribe`
301
+ #### `oasiz.onLeaveGame(callback: () => void): Unsubscribe`
280
302
 
281
303
  Registers a callback fired when the host initiates closing the game (for example, close button, gesture, or host navigation). Use this for lightweight cleanup.
282
304
 
@@ -290,11 +312,9 @@ const offLeave = oasiz.onLeaveGame(() => {
290
312
  offLeave();
291
313
  ```
292
314
 
293
- ---
294
-
295
- ## Multiplayer
315
+ ### Multiplayer
296
316
 
297
- ### `oasiz.shareRoomCode(code: string | null, options?: { inviteOverride?: boolean })`
317
+ #### `oasiz.shareRoomCode(code: string | null, options?: { inviteOverride?: boolean })`
298
318
 
299
319
  Notify the platform of the active multiplayer room so friends can join via the invite system. Pass `null` when leaving a room.
300
320
 
@@ -316,7 +336,9 @@ oasiz.shareRoomCode(null);
316
336
  oasiz.shareRoomCode(getRoomCode(), { inviteOverride: true });
317
337
  ```
318
338
 
319
- If you still want to use the platform invite sheet from your own in-game button, combine it with `openInviteModal()`:
339
+ #### `oasiz.openInviteModal(): void`
340
+
341
+ Opens the platform invite-friends UI when the bridge is available. Typically used together with `shareRoomCode` (for example, your own invite button calls this).
320
342
 
321
343
  ```ts
322
344
  import { openInviteModal, shareRoomCode } from "@oasiz/sdk";
@@ -328,7 +350,7 @@ inviteButton.addEventListener("click", () => {
328
350
  });
329
351
  ```
330
352
 
331
- ### Read-only injected values
353
+ #### Read-only injected values
332
354
 
333
355
  These are populated by the platform before the game loads. Always check for `undefined` before using.
334
356
 
@@ -342,26 +364,26 @@ if (oasiz.roomCode) {
342
364
  }
343
365
 
344
366
  // Player identity for multiplayer games
345
- const name = oasiz.playerName;
367
+ const name = oasiz.playerName;
346
368
  const avatar = oasiz.playerAvatar;
347
369
  ```
348
370
 
349
- ---
350
-
351
- ## Named exports
371
+ ### Named exports
352
372
 
353
373
  All methods are also available as named exports if you prefer not to use the `oasiz` namespace object:
354
374
 
355
375
  ```ts
356
376
  import {
357
377
  submitScore,
358
- emitScoreConfig,
359
378
  triggerHaptic,
360
379
  loadGameState,
361
380
  saveGameState,
362
381
  flushGameState,
363
382
  shareRoomCode,
383
+ openInviteModal,
364
384
  enableLogOverlay,
385
+ getSafeAreaTop,
386
+ setLeaderboardVisible,
365
387
  onPause,
366
388
  onResume,
367
389
  onBackButton,
@@ -374,9 +396,7 @@ import {
374
396
  } from "@oasiz/sdk";
375
397
  ```
376
398
 
377
- ---
378
-
379
- ## TypeScript types
399
+ ### TypeScript types
380
400
 
381
401
  ```ts
382
402
  import type {
@@ -384,15 +404,62 @@ import type {
384
404
  HapticType,
385
405
  LogOverlayHandle,
386
406
  LogOverlayOptions,
387
- ScoreAnchor,
388
- ScoreConfig,
407
+ ShareRoomCodeOptions,
408
+ Unsubscribe,
389
409
  } from "@oasiz/sdk";
390
410
  ```
391
411
 
392
412
  ---
393
413
 
414
+ ## Unity WebGL SDK
415
+
416
+ C# API and **WebGL-only** `OasizBridge.jslib` live in this repository at **`packages/OasizSDK/`**. Copy the **`OasizSDK`** folder into your Unity project under **`Assets/`** (for example `Assets/OasizSDK`).
417
+
418
+ ### Setup
419
+
420
+ 1. Copy `packages/OasizSDK` from this repo into `Assets/OasizSDK`.
421
+ 2. Ensure the **WebGL** platform is selected for release builds; the `.jslib` under `Runtime/Plugins/WebGL/` is included automatically for WebGL.
422
+ 3. Add an **`OasizSDK`** component to a persistent GameObject early (for example a bootstrap scene), **or** rely on `OasizSDK.Instance` which creates a `DontDestroyOnLoad` object. The component registers listeners for `oasiz:pause`, `oasiz:resume`, `oasiz:back`, and `oasiz:leave` via `SendMessage`.
423
+
424
+ ### API parity (TypeScript → C#)
425
+
426
+ | HTML5 (`@oasiz/sdk`) | Unity (`Oasiz` namespace) |
427
+ | --- | --- |
428
+ | `oasiz.submitScore(n)` | `OasizSDK.SubmitScore(int)` |
429
+ | `oasiz.triggerHaptic(type)` | `OasizSDK.TriggerHaptic(HapticType)` |
430
+ | `oasiz.loadGameState()` | `OasizSDK.LoadGameState()` → `Dictionary<string, object>` |
431
+ | `oasiz.saveGameState(obj)` | `OasizSDK.SaveGameState(Dictionary<string, object>)` |
432
+ | `oasiz.flushGameState()` | `OasizSDK.FlushGameState()` |
433
+ | `oasiz.getSafeAreaTop()` / `safeAreaTop` | `OasizSDK.GetSafeAreaTop()` / `OasizSDK.SafeAreaTop` (`float`, CSS px) |
434
+ | `oasiz.setLeaderboardVisible(v)` | `OasizSDK.SetLeaderboardVisible(bool)` |
435
+ | `oasiz.onPause` / `onResume` | `OasizSDK.OnPause` / `OnResume` static events |
436
+ | `oasiz.onBackButton` | `OasizSDK.OnBackButton` or `SubscribeBackButton(Action)` (reference-counts `__oasizSetBackOverride`) |
437
+ | `oasiz.onLeaveGame` | `OasizSDK.OnLeaveGame` |
438
+ | `oasiz.leaveGame()` | `OasizSDK.LeaveGame()` |
439
+ | `oasiz.shareRoomCode` | `OasizSDK.ShareRoomCode(string, ShareRoomCodeOptions)` |
440
+ | `oasiz.openInviteModal()` | `OasizSDK.OpenInviteModal()` |
441
+ | `oasiz.gameId` / `roomCode` / … | `OasizSDK.GameId` / `RoomCode` / `PlayerName` / `PlayerAvatar` |
442
+ | — | `OasizSDK.EmitScoreConfig(ScoreConfig)` → `window.emitScoreConfig` (Unity-only helper for normalized score UI) |
443
+ | `oasiz.enableLogOverlay` | `OasizSDK.EnableLogOverlay(LogOverlayOptions)` (see note below) |
444
+
445
+ ### Back button and errors
446
+
447
+ Matching the HTML5 SDK: if any **`OnBackButton`** handler throws, **`OasizSDK.LeaveGame()`** is invoked and the **original exception is rethrown** (`throw;` preserves the stack trace). Use **`SubscribeBackButton`** when you want an unsubscribe delegate; you can also use `OnBackButton +=` / `-=` directly.
448
+
449
+ ### Editor vs WebGL builds
450
+
451
+ In the **Unity Editor**, bridge calls are mostly **logged** and return safe defaults (for example safe area `0`, `null` platform IDs). Real host integration applies to **WebGL player** builds running inside Oasiz.
452
+
453
+ ### Log overlay (Unity)
454
+
455
+ The C# API for the log overlay exists for API compatibility, but the **default `OasizBridge.jslib` in this repo does not inject DOM UI** — `EnableLogOverlay` / `AppendLogOverlay` are no-ops at the JavaScript layer. Use Unity’s console and device logs for debugging unless you replace or extend the `.jslib` on your side.
456
+
457
+ ---
458
+
394
459
  ## Local development
395
460
 
461
+ ### HTML5 / TypeScript
462
+
396
463
  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:
397
464
 
398
465
  ```
@@ -400,3 +467,7 @@ All methods safely no-op when the platform bridges are not injected. In developm
400
467
  ```
401
468
 
402
469
  No crashes, no special setup required for local dev.
470
+
471
+ ### Unity WebGL
472
+
473
+ The `.jslib` logs warnings when `window.*` bridges are missing (for example `submitScore`, `__oasizLeaveGame`). The Editor path avoids calling native plugins and prints `Debug.Log` for most operations instead.
package/dist/index.cjs CHANGED
@@ -20,13 +20,13 @@ 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,
24
23
  enableLogOverlay: () => enableLogOverlay,
25
24
  flushGameState: () => flushGameState,
26
25
  getGameId: () => getGameId,
27
26
  getPlayerAvatar: () => getPlayerAvatar,
28
27
  getPlayerName: () => getPlayerName,
29
28
  getRoomCode: () => getRoomCode,
29
+ getSafeAreaTop: () => getSafeAreaTop,
30
30
  leaveGame: () => leaveGame,
31
31
  loadGameState: () => loadGameState,
32
32
  oasiz: () => oasiz,
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  onResume: () => onResume,
37
37
  openInviteModal: () => openInviteModal,
38
38
  saveGameState: () => saveGameState,
39
+ setLeaderboardVisible: () => setLeaderboardVisible,
39
40
  shareRoomCode: () => shareRoomCode,
40
41
  submitScore: () => submitScore,
41
42
  triggerHaptic: () => triggerHaptic
@@ -1042,14 +1043,6 @@ function submitScore(score) {
1042
1043
  }
1043
1044
  warnMissingBridge("submitScore");
1044
1045
  }
1045
- function emitScoreConfig(config) {
1046
- const bridge = getBridgeWindow3();
1047
- if (typeof bridge?.emitScoreConfig === "function") {
1048
- bridge.emitScoreConfig(config);
1049
- return;
1050
- }
1051
- warnMissingBridge("emitScoreConfig");
1052
- }
1053
1046
 
1054
1047
  // src/state.ts
1055
1048
  function isDevelopment4() {
@@ -1142,8 +1135,7 @@ function onResume(callback) {
1142
1135
  return addLifecycleListener("oasiz:resume", callback);
1143
1136
  }
1144
1137
 
1145
- // src/navigation.ts
1146
- var activeBackListeners = 0;
1138
+ // src/layout.ts
1147
1139
  function isDevelopment6() {
1148
1140
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
1149
1141
  return nodeEnv !== "production";
@@ -1161,9 +1153,74 @@ function warnMissingBridge3(methodName) {
1161
1153
  );
1162
1154
  }
1163
1155
  }
1156
+ function normalizeSafeAreaTop(value) {
1157
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1158
+ return 0;
1159
+ }
1160
+ return Math.max(0, value);
1161
+ }
1162
+ function getSafeAreaTop() {
1163
+ const bridge = getBridgeWindow5();
1164
+ if (!bridge) {
1165
+ return 0;
1166
+ }
1167
+ if (typeof bridge.getSafeAreaTop === "function") {
1168
+ return normalizeSafeAreaTop(bridge.getSafeAreaTop());
1169
+ }
1170
+ if (typeof bridge.__OASIZ_SAFE_AREA_TOP__ !== "undefined") {
1171
+ return normalizeSafeAreaTop(bridge.__OASIZ_SAFE_AREA_TOP__);
1172
+ }
1173
+ warnMissingBridge3("getSafeAreaTop");
1174
+ return 0;
1175
+ }
1176
+ function setLeaderboardVisible(visible) {
1177
+ if (typeof visible !== "boolean") {
1178
+ if (isDevelopment6()) {
1179
+ console.warn(
1180
+ "[oasiz/sdk] setLeaderboardVisible expected a boolean:",
1181
+ visible
1182
+ );
1183
+ }
1184
+ return;
1185
+ }
1186
+ const bridge = getBridgeWindow5();
1187
+ if (typeof bridge?.__oasizSetLeaderboardVisible === "function") {
1188
+ bridge.__oasizSetLeaderboardVisible(visible);
1189
+ return;
1190
+ }
1191
+ warnMissingBridge3("__oasizSetLeaderboardVisible");
1192
+ }
1193
+
1194
+ // src/navigation.ts
1195
+ var activeBackListeners = 0;
1196
+ function isDevelopment7() {
1197
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
1198
+ return nodeEnv !== "production";
1199
+ }
1200
+ function getBridgeWindow6() {
1201
+ if (typeof window === "undefined") {
1202
+ return void 0;
1203
+ }
1204
+ return window;
1205
+ }
1206
+ function warnMissingBridge4(methodName) {
1207
+ if (isDevelopment7()) {
1208
+ console.warn(
1209
+ "[oasiz/sdk] " + methodName + " bridge is unavailable. This is expected in local development."
1210
+ );
1211
+ }
1212
+ }
1213
+ function normalizeNavigationError(error) {
1214
+ if (error instanceof Error) {
1215
+ return error;
1216
+ }
1217
+ return new Error(
1218
+ typeof error === "string" ? error : "Back button callback failed."
1219
+ );
1220
+ }
1164
1221
  function addNavigationListener(eventName, callback) {
1165
1222
  if (typeof window === "undefined") {
1166
- if (isDevelopment6()) {
1223
+ if (isDevelopment7()) {
1167
1224
  console.warn(
1168
1225
  "[oasiz/sdk] " + eventName + " listener registered without a browser window. This is expected in local development."
1169
1226
  );
@@ -1176,25 +1233,32 @@ function addNavigationListener(eventName, callback) {
1176
1233
  return () => window.removeEventListener(eventName, handler);
1177
1234
  }
1178
1235
  function onBackButton(callback) {
1179
- const off = addNavigationListener("oasiz:back", callback);
1180
- const bridge = getBridgeWindow5();
1236
+ const off = addNavigationListener("oasiz:back", () => {
1237
+ try {
1238
+ callback();
1239
+ } catch (error) {
1240
+ leaveGame();
1241
+ throw normalizeNavigationError(error);
1242
+ }
1243
+ });
1244
+ const bridge = getBridgeWindow6();
1181
1245
  activeBackListeners += 1;
1182
1246
  if (activeBackListeners === 1) {
1183
1247
  if (typeof bridge?.__oasizSetBackOverride === "function") {
1184
1248
  bridge.__oasizSetBackOverride(true);
1185
1249
  } else {
1186
- warnMissingBridge3("__oasizSetBackOverride");
1250
+ warnMissingBridge4("__oasizSetBackOverride");
1187
1251
  }
1188
1252
  }
1189
1253
  return () => {
1190
1254
  off();
1191
1255
  activeBackListeners = Math.max(0, activeBackListeners - 1);
1192
1256
  if (activeBackListeners === 0) {
1193
- const currentBridge = getBridgeWindow5();
1257
+ const currentBridge = getBridgeWindow6();
1194
1258
  if (typeof currentBridge?.__oasizSetBackOverride === "function") {
1195
1259
  currentBridge.__oasizSetBackOverride(false);
1196
1260
  } else {
1197
- warnMissingBridge3("__oasizSetBackOverride");
1261
+ warnMissingBridge4("__oasizSetBackOverride");
1198
1262
  }
1199
1263
  }
1200
1264
  };
@@ -1203,18 +1267,17 @@ function onLeaveGame(callback) {
1203
1267
  return addNavigationListener("oasiz:leave", callback);
1204
1268
  }
1205
1269
  function leaveGame() {
1206
- const bridge = getBridgeWindow5();
1270
+ const bridge = getBridgeWindow6();
1207
1271
  if (typeof bridge?.__oasizLeaveGame === "function") {
1208
1272
  bridge.__oasizLeaveGame();
1209
1273
  return;
1210
1274
  }
1211
- warnMissingBridge3("__oasizLeaveGame");
1275
+ warnMissingBridge4("__oasizLeaveGame");
1212
1276
  }
1213
1277
 
1214
1278
  // src/index.ts
1215
1279
  var oasiz = {
1216
1280
  submitScore,
1217
- emitScoreConfig,
1218
1281
  triggerHaptic,
1219
1282
  enableLogOverlay,
1220
1283
  loadGameState,
@@ -1224,6 +1287,8 @@ var oasiz = {
1224
1287
  openInviteModal,
1225
1288
  onPause,
1226
1289
  onResume,
1290
+ getSafeAreaTop,
1291
+ setLeaderboardVisible,
1227
1292
  onBackButton,
1228
1293
  onLeaveGame,
1229
1294
  leaveGame,
@@ -1238,17 +1303,20 @@ var oasiz = {
1238
1303
  },
1239
1304
  get playerAvatar() {
1240
1305
  return getPlayerAvatar();
1306
+ },
1307
+ get safeAreaTop() {
1308
+ return getSafeAreaTop();
1241
1309
  }
1242
1310
  };
1243
1311
  // Annotate the CommonJS export names for ESM import in node:
1244
1312
  0 && (module.exports = {
1245
- emitScoreConfig,
1246
1313
  enableLogOverlay,
1247
1314
  flushGameState,
1248
1315
  getGameId,
1249
1316
  getPlayerAvatar,
1250
1317
  getPlayerName,
1251
1318
  getRoomCode,
1319
+ getSafeAreaTop,
1252
1320
  leaveGame,
1253
1321
  loadGameState,
1254
1322
  oasiz,
@@ -1258,6 +1326,7 @@ var oasiz = {
1258
1326
  onResume,
1259
1327
  openInviteModal,
1260
1328
  saveGameState,
1329
+ setLeaderboardVisible,
1261
1330
  shareRoomCode,
1262
1331
  submitScore,
1263
1332
  triggerHaptic
package/dist/index.d.cts CHANGED
@@ -19,13 +19,6 @@ interface LogOverlayHandle {
19
19
  isVisible: () => boolean;
20
20
  show: () => void;
21
21
  }
22
- interface ScoreAnchor {
23
- raw: number;
24
- normalized: 100 | 300 | 600 | 950;
25
- }
26
- interface ScoreConfig {
27
- anchors: [ScoreAnchor, ScoreAnchor, ScoreAnchor, ScoreAnchor];
28
- }
29
22
  type GameState = Record<string, unknown>;
30
23
 
31
24
  declare function triggerHaptic(type: HapticType): void;
@@ -53,7 +46,6 @@ declare function getPlayerName(): string | undefined;
53
46
  declare function getPlayerAvatar(): string | undefined;
54
47
 
55
48
  declare function submitScore(score: number): void;
56
- declare function emitScoreConfig(config: ScoreConfig): void;
57
49
 
58
50
  declare function loadGameState(): GameState;
59
51
  declare function saveGameState(state: GameState): void;
@@ -63,13 +55,15 @@ type Unsubscribe = () => void;
63
55
  declare function onPause(callback: () => void): Unsubscribe;
64
56
  declare function onResume(callback: () => void): Unsubscribe;
65
57
 
58
+ declare function getSafeAreaTop(): number;
59
+ declare function setLeaderboardVisible(visible: boolean): void;
60
+
66
61
  declare function onBackButton(callback: () => void): Unsubscribe;
67
62
  declare function onLeaveGame(callback: () => void): Unsubscribe;
68
63
  declare function leaveGame(): void;
69
64
 
70
65
  declare const oasiz: {
71
66
  submitScore: typeof submitScore;
72
- emitScoreConfig: typeof emitScoreConfig;
73
67
  triggerHaptic: typeof triggerHaptic;
74
68
  enableLogOverlay: typeof enableLogOverlay;
75
69
  loadGameState: typeof loadGameState;
@@ -79,6 +73,8 @@ declare const oasiz: {
79
73
  openInviteModal: typeof openInviteModal;
80
74
  onPause: typeof onPause;
81
75
  onResume: typeof onResume;
76
+ getSafeAreaTop: typeof getSafeAreaTop;
77
+ setLeaderboardVisible: typeof setLeaderboardVisible;
82
78
  onBackButton: typeof onBackButton;
83
79
  onLeaveGame: typeof onLeaveGame;
84
80
  leaveGame: typeof leaveGame;
@@ -86,6 +82,7 @@ declare const oasiz: {
86
82
  readonly roomCode: string | undefined;
87
83
  readonly playerName: string | undefined;
88
84
  readonly playerAvatar: string | undefined;
85
+ readonly safeAreaTop: number;
89
86
  };
90
87
 
91
- export { type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type ScoreAnchor, type ScoreConfig, type ShareRoomCodeOptions, type Unsubscribe, emitScoreConfig, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerName, getRoomCode, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, shareRoomCode, submitScore, triggerHaptic };
88
+ export { type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type ShareRoomCodeOptions, type Unsubscribe, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerName, getRoomCode, getSafeAreaTop, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, shareRoomCode, submitScore, triggerHaptic };
package/dist/index.d.ts CHANGED
@@ -19,13 +19,6 @@ interface LogOverlayHandle {
19
19
  isVisible: () => boolean;
20
20
  show: () => void;
21
21
  }
22
- interface ScoreAnchor {
23
- raw: number;
24
- normalized: 100 | 300 | 600 | 950;
25
- }
26
- interface ScoreConfig {
27
- anchors: [ScoreAnchor, ScoreAnchor, ScoreAnchor, ScoreAnchor];
28
- }
29
22
  type GameState = Record<string, unknown>;
30
23
 
31
24
  declare function triggerHaptic(type: HapticType): void;
@@ -53,7 +46,6 @@ declare function getPlayerName(): string | undefined;
53
46
  declare function getPlayerAvatar(): string | undefined;
54
47
 
55
48
  declare function submitScore(score: number): void;
56
- declare function emitScoreConfig(config: ScoreConfig): void;
57
49
 
58
50
  declare function loadGameState(): GameState;
59
51
  declare function saveGameState(state: GameState): void;
@@ -63,13 +55,15 @@ type Unsubscribe = () => void;
63
55
  declare function onPause(callback: () => void): Unsubscribe;
64
56
  declare function onResume(callback: () => void): Unsubscribe;
65
57
 
58
+ declare function getSafeAreaTop(): number;
59
+ declare function setLeaderboardVisible(visible: boolean): void;
60
+
66
61
  declare function onBackButton(callback: () => void): Unsubscribe;
67
62
  declare function onLeaveGame(callback: () => void): Unsubscribe;
68
63
  declare function leaveGame(): void;
69
64
 
70
65
  declare const oasiz: {
71
66
  submitScore: typeof submitScore;
72
- emitScoreConfig: typeof emitScoreConfig;
73
67
  triggerHaptic: typeof triggerHaptic;
74
68
  enableLogOverlay: typeof enableLogOverlay;
75
69
  loadGameState: typeof loadGameState;
@@ -79,6 +73,8 @@ declare const oasiz: {
79
73
  openInviteModal: typeof openInviteModal;
80
74
  onPause: typeof onPause;
81
75
  onResume: typeof onResume;
76
+ getSafeAreaTop: typeof getSafeAreaTop;
77
+ setLeaderboardVisible: typeof setLeaderboardVisible;
82
78
  onBackButton: typeof onBackButton;
83
79
  onLeaveGame: typeof onLeaveGame;
84
80
  leaveGame: typeof leaveGame;
@@ -86,6 +82,7 @@ declare const oasiz: {
86
82
  readonly roomCode: string | undefined;
87
83
  readonly playerName: string | undefined;
88
84
  readonly playerAvatar: string | undefined;
85
+ readonly safeAreaTop: number;
89
86
  };
90
87
 
91
- export { type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type ScoreAnchor, type ScoreConfig, type ShareRoomCodeOptions, type Unsubscribe, emitScoreConfig, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerName, getRoomCode, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, shareRoomCode, submitScore, triggerHaptic };
88
+ export { type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type ShareRoomCodeOptions, type Unsubscribe, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerName, getRoomCode, getSafeAreaTop, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, shareRoomCode, submitScore, triggerHaptic };
package/dist/index.js CHANGED
@@ -998,14 +998,6 @@ function submitScore(score) {
998
998
  }
999
999
  warnMissingBridge("submitScore");
1000
1000
  }
1001
- function emitScoreConfig(config) {
1002
- const bridge = getBridgeWindow3();
1003
- if (typeof bridge?.emitScoreConfig === "function") {
1004
- bridge.emitScoreConfig(config);
1005
- return;
1006
- }
1007
- warnMissingBridge("emitScoreConfig");
1008
- }
1009
1001
 
1010
1002
  // src/state.ts
1011
1003
  function isDevelopment4() {
@@ -1098,8 +1090,7 @@ function onResume(callback) {
1098
1090
  return addLifecycleListener("oasiz:resume", callback);
1099
1091
  }
1100
1092
 
1101
- // src/navigation.ts
1102
- var activeBackListeners = 0;
1093
+ // src/layout.ts
1103
1094
  function isDevelopment6() {
1104
1095
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
1105
1096
  return nodeEnv !== "production";
@@ -1117,9 +1108,74 @@ function warnMissingBridge3(methodName) {
1117
1108
  );
1118
1109
  }
1119
1110
  }
1111
+ function normalizeSafeAreaTop(value) {
1112
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1113
+ return 0;
1114
+ }
1115
+ return Math.max(0, value);
1116
+ }
1117
+ function getSafeAreaTop() {
1118
+ const bridge = getBridgeWindow5();
1119
+ if (!bridge) {
1120
+ return 0;
1121
+ }
1122
+ if (typeof bridge.getSafeAreaTop === "function") {
1123
+ return normalizeSafeAreaTop(bridge.getSafeAreaTop());
1124
+ }
1125
+ if (typeof bridge.__OASIZ_SAFE_AREA_TOP__ !== "undefined") {
1126
+ return normalizeSafeAreaTop(bridge.__OASIZ_SAFE_AREA_TOP__);
1127
+ }
1128
+ warnMissingBridge3("getSafeAreaTop");
1129
+ return 0;
1130
+ }
1131
+ function setLeaderboardVisible(visible) {
1132
+ if (typeof visible !== "boolean") {
1133
+ if (isDevelopment6()) {
1134
+ console.warn(
1135
+ "[oasiz/sdk] setLeaderboardVisible expected a boolean:",
1136
+ visible
1137
+ );
1138
+ }
1139
+ return;
1140
+ }
1141
+ const bridge = getBridgeWindow5();
1142
+ if (typeof bridge?.__oasizSetLeaderboardVisible === "function") {
1143
+ bridge.__oasizSetLeaderboardVisible(visible);
1144
+ return;
1145
+ }
1146
+ warnMissingBridge3("__oasizSetLeaderboardVisible");
1147
+ }
1148
+
1149
+ // src/navigation.ts
1150
+ var activeBackListeners = 0;
1151
+ function isDevelopment7() {
1152
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
1153
+ return nodeEnv !== "production";
1154
+ }
1155
+ function getBridgeWindow6() {
1156
+ if (typeof window === "undefined") {
1157
+ return void 0;
1158
+ }
1159
+ return window;
1160
+ }
1161
+ function warnMissingBridge4(methodName) {
1162
+ if (isDevelopment7()) {
1163
+ console.warn(
1164
+ "[oasiz/sdk] " + methodName + " bridge is unavailable. This is expected in local development."
1165
+ );
1166
+ }
1167
+ }
1168
+ function normalizeNavigationError(error) {
1169
+ if (error instanceof Error) {
1170
+ return error;
1171
+ }
1172
+ return new Error(
1173
+ typeof error === "string" ? error : "Back button callback failed."
1174
+ );
1175
+ }
1120
1176
  function addNavigationListener(eventName, callback) {
1121
1177
  if (typeof window === "undefined") {
1122
- if (isDevelopment6()) {
1178
+ if (isDevelopment7()) {
1123
1179
  console.warn(
1124
1180
  "[oasiz/sdk] " + eventName + " listener registered without a browser window. This is expected in local development."
1125
1181
  );
@@ -1132,25 +1188,32 @@ function addNavigationListener(eventName, callback) {
1132
1188
  return () => window.removeEventListener(eventName, handler);
1133
1189
  }
1134
1190
  function onBackButton(callback) {
1135
- const off = addNavigationListener("oasiz:back", callback);
1136
- const bridge = getBridgeWindow5();
1191
+ const off = addNavigationListener("oasiz:back", () => {
1192
+ try {
1193
+ callback();
1194
+ } catch (error) {
1195
+ leaveGame();
1196
+ throw normalizeNavigationError(error);
1197
+ }
1198
+ });
1199
+ const bridge = getBridgeWindow6();
1137
1200
  activeBackListeners += 1;
1138
1201
  if (activeBackListeners === 1) {
1139
1202
  if (typeof bridge?.__oasizSetBackOverride === "function") {
1140
1203
  bridge.__oasizSetBackOverride(true);
1141
1204
  } else {
1142
- warnMissingBridge3("__oasizSetBackOverride");
1205
+ warnMissingBridge4("__oasizSetBackOverride");
1143
1206
  }
1144
1207
  }
1145
1208
  return () => {
1146
1209
  off();
1147
1210
  activeBackListeners = Math.max(0, activeBackListeners - 1);
1148
1211
  if (activeBackListeners === 0) {
1149
- const currentBridge = getBridgeWindow5();
1212
+ const currentBridge = getBridgeWindow6();
1150
1213
  if (typeof currentBridge?.__oasizSetBackOverride === "function") {
1151
1214
  currentBridge.__oasizSetBackOverride(false);
1152
1215
  } else {
1153
- warnMissingBridge3("__oasizSetBackOverride");
1216
+ warnMissingBridge4("__oasizSetBackOverride");
1154
1217
  }
1155
1218
  }
1156
1219
  };
@@ -1159,18 +1222,17 @@ function onLeaveGame(callback) {
1159
1222
  return addNavigationListener("oasiz:leave", callback);
1160
1223
  }
1161
1224
  function leaveGame() {
1162
- const bridge = getBridgeWindow5();
1225
+ const bridge = getBridgeWindow6();
1163
1226
  if (typeof bridge?.__oasizLeaveGame === "function") {
1164
1227
  bridge.__oasizLeaveGame();
1165
1228
  return;
1166
1229
  }
1167
- warnMissingBridge3("__oasizLeaveGame");
1230
+ warnMissingBridge4("__oasizLeaveGame");
1168
1231
  }
1169
1232
 
1170
1233
  // src/index.ts
1171
1234
  var oasiz = {
1172
1235
  submitScore,
1173
- emitScoreConfig,
1174
1236
  triggerHaptic,
1175
1237
  enableLogOverlay,
1176
1238
  loadGameState,
@@ -1180,6 +1242,8 @@ var oasiz = {
1180
1242
  openInviteModal,
1181
1243
  onPause,
1182
1244
  onResume,
1245
+ getSafeAreaTop,
1246
+ setLeaderboardVisible,
1183
1247
  onBackButton,
1184
1248
  onLeaveGame,
1185
1249
  leaveGame,
@@ -1194,16 +1258,19 @@ var oasiz = {
1194
1258
  },
1195
1259
  get playerAvatar() {
1196
1260
  return getPlayerAvatar();
1261
+ },
1262
+ get safeAreaTop() {
1263
+ return getSafeAreaTop();
1197
1264
  }
1198
1265
  };
1199
1266
  export {
1200
- emitScoreConfig,
1201
1267
  enableLogOverlay,
1202
1268
  flushGameState,
1203
1269
  getGameId,
1204
1270
  getPlayerAvatar,
1205
1271
  getPlayerName,
1206
1272
  getRoomCode,
1273
+ getSafeAreaTop,
1207
1274
  leaveGame,
1208
1275
  loadGameState,
1209
1276
  oasiz,
@@ -1213,6 +1280,7 @@ export {
1213
1280
  onResume,
1214
1281
  openInviteModal,
1215
1282
  saveGameState,
1283
+ setLeaderboardVisible,
1216
1284
  shareRoomCode,
1217
1285
  submitScore,
1218
1286
  triggerHaptic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oasiz/sdk",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Typed SDK for Oasiz game platform bridge APIs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -19,19 +19,25 @@
19
19
  "sideEffects": false,
20
20
  "scripts": {
21
21
  "build": "tsup src/index.ts --format esm,cjs --dts",
22
- "prepack": "bun run build",
22
+ "prepack": "npm run build",
23
23
  "test": "node --experimental-strip-types --test ./tests/**/*.test.ts"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"
27
27
  },
28
+ "tagFormat": "sdk-v${version}",
28
29
  "release": {
29
30
  "branches": [
30
31
  "main"
31
32
  ],
32
33
  "plugins": [
33
- "@semantic-release/commit-analyzer",
34
- "@semantic-release/release-notes-generator",
34
+ [
35
+ "../../scripts/semantic-release/workspace-release.mjs",
36
+ {
37
+ "packageName": "@oasiz/sdk",
38
+ "packagePath": "packages/sdk"
39
+ }
40
+ ],
35
41
  "@semantic-release/github",
36
42
  [
37
43
  "@semantic-release/exec",
@@ -48,5 +54,9 @@
48
54
  "semantic-release": "^25.0.3",
49
55
  "tsup": "^8.5.1",
50
56
  "typescript": "^5.9.3"
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/oasiz-ai/oasiz-tooling.git"
51
61
  }
52
62
  }