@oasiz/sdk 1.6.0 → 1.6.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.
package/README.md CHANGED
@@ -1,47 +1,69 @@
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), graphics performance, 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
 
24
+ The published package includes ESM, CommonJS, and TypeScript declarations.
25
+
11
26
  ## Quick start
12
27
 
13
28
  ```ts
14
29
  import { oasiz } from "@oasiz/sdk";
15
30
 
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
31
+ // 1. Load persisted state at the start of each session
27
32
  const state = oasiz.loadGameState();
28
33
  let level = typeof state.level === "number" ? state.level : 1;
29
34
 
30
- // 3. Save state at checkpoints
35
+ // 2. Save state at checkpoints
31
36
  oasiz.saveGameState({ level, coins: 42 });
32
37
 
33
- // 4. Trigger haptics on key events
38
+ // 3. Trigger haptics on key events
34
39
  oasiz.triggerHaptic("medium");
35
40
 
36
- // 5. Submit score when the game ends
41
+ // 4. Respect the host's top safe area (percent of viewport height → CSS vh)
42
+ document.documentElement.style.setProperty(
43
+ "--safe-top",
44
+ `${oasiz.safeAreaTop}vh`,
45
+ );
46
+
47
+ // 5. Pick graphics settings for this device
48
+ const graphics = oasiz.getGraphicsPerformance();
49
+ renderer.setPixelRatio(graphics.tier === "high" ? 1.5 : graphics.tier === "medium" ? 1.25 : 1);
50
+
51
+ // 6. Submit score when the game ends
37
52
  oasiz.submitScore(score);
38
- ```
39
53
 
40
- ---
54
+ // 7. Optionally hide the leaderboard while a custom overlay is open
55
+ oasiz.setLeaderboardVisible(false);
56
+
57
+ // 8. Optionally surface console logs in-game while debugging
58
+ oasiz.enableLogOverlay({
59
+ enabled: new URLSearchParams(window.location.search).has("oasizLogs"),
60
+ collapsed: true,
61
+ });
62
+ ```
41
63
 
42
- ## Score
64
+ ### Score
43
65
 
44
- ### `oasiz.submitScore(score: number)`
66
+ #### `oasiz.submitScore(score: number)`
45
67
 
46
68
  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.
47
69
 
@@ -54,39 +76,9 @@ private onGameOver(): void {
54
76
  - `score` must be a non-negative integer. Floats are floored automatically.
55
77
  - Do not call on intermediate scores or level completions, only on final game over.
56
78
 
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`
79
+ ### Haptics
84
80
 
85
- ---
86
-
87
- ## Haptics
88
-
89
- ### `oasiz.triggerHaptic(type: HapticType)`
81
+ #### `oasiz.triggerHaptic(type: HapticType)`
90
82
 
91
83
  Trigger native haptic feedback. Always guard with the user's haptics setting.
92
84
 
@@ -95,7 +87,7 @@ type HapticType = "light" | "medium" | "heavy" | "success" | "error";
95
87
  ```
96
88
 
97
89
  | Type | When to use |
98
- |---|---|
90
+ | --- | --- |
99
91
  | `"light"` | UI button taps, menu navigation, D-pad press |
100
92
  | `"medium"` | Collecting items, standard collisions, scoring |
101
93
  | `"heavy"` | Explosions, major impacts, screen shake |
@@ -126,20 +118,45 @@ private onGameOver(): void {
126
118
 
127
119
  Haptics are throttled internally (50ms cooldown) to prevent spam.
128
120
 
129
- ---
121
+ ### Debugging
122
+
123
+ #### `oasiz.enableLogOverlay(options?: LogOverlayOptions)`
124
+
125
+ 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.
126
+
127
+ ```ts
128
+ const logOverlay = oasiz.enableLogOverlay({
129
+ enabled: new URLSearchParams(window.location.search).has("oasizLogs"),
130
+ collapsed: true,
131
+ });
132
+
133
+ console.log("[Boot] Scene ready");
134
+
135
+ // Optional cleanup if your game tears down and remounts
136
+ logOverlay.destroy();
137
+ ```
138
+
139
+ Options:
140
+
141
+ - `enabled`: defaults to `true`. Pass your own flag or query-param check here.
142
+ - `collapsed`: start with only the toggle pill visible.
143
+ - `maxEntries`: cap retained log lines. Defaults to `200`.
144
+ - `title`: optional label shown at the top of the panel. Defaults to `SDK Logs`.
145
+
146
+ The returned handle supports `show()`, `hide()`, `clear()`, `isVisible()`, and `destroy()`.
130
147
 
131
- ## Game state persistence
148
+ ### Game state persistence
132
149
 
133
150
  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.
134
151
 
135
- ### `oasiz.loadGameState(): Record<string, unknown>`
152
+ #### `oasiz.loadGameState(): Record<string, unknown>`
136
153
 
137
154
  Returns the player's saved state synchronously. Returns `{}` if no state has been saved yet. Call once at the start of the game.
138
155
 
139
156
  ```ts
140
157
  private initFromSavedState(): void {
141
158
  const state = oasiz.loadGameState();
142
- this.level = typeof state.level === "number" ? state.level : 1;
159
+ this.level = typeof state.level === "number" ? state.level : 1;
143
160
  this.lifetimeHits = typeof state.lifetimeHits === "number" ? state.lifetimeHits : 0;
144
161
  this.unlockedSkins = Array.isArray(state.unlockedSkins) ? state.unlockedSkins : [];
145
162
  }
@@ -147,7 +164,7 @@ private initFromSavedState(): void {
147
164
 
148
165
  Always validate the shape of loaded data — it may be `{}` on first play.
149
166
 
150
- ### `oasiz.saveGameState(state: Record<string, unknown>)`
167
+ #### `oasiz.saveGameState(state: Record<string, unknown>)`
151
168
 
152
169
  Queues a debounced save. Saves are batched automatically — call freely at checkpoints without worrying about request spam.
153
170
 
@@ -164,11 +181,12 @@ private onLevelComplete(): void {
164
181
  ```
165
182
 
166
183
  **Rules:**
184
+
167
185
  - State must be a plain JSON object (not an array or primitive).
168
186
  - Do not use `localStorage` for cross-session progress — use `saveGameState` so data syncs across platforms.
169
187
  - Do not store scores here — scores are submitted via `submitScore`.
170
188
 
171
- ### `oasiz.flushGameState()`
189
+ #### `oasiz.flushGameState()`
172
190
 
173
191
  Forces an immediate write, bypassing the debounce. Use at important checkpoints like game over or before the page unloads.
174
192
 
@@ -180,19 +198,122 @@ private onGameOver(): void {
180
198
  }
181
199
  ```
182
200
 
183
- ---
201
+ ### Layout
202
+
203
+ Use runtime viewport insets instead of direct CSS `env(safe-area-inset-*)` reads or hardcoded offsets. The SDK resolves host-provided Oasiz values first, then browser CSS `env(safe-area-inset-*)`, then legacy `constant(safe-area-inset-*)`, and finally `0`.
204
+
205
+ The top inset preserves the existing Oasiz game-safe top behavior: host chrome, invite UI, and leaderboard clearance can contribute to it. Left, right, and bottom are device safe-area insets today and may include future host UI obstructions.
206
+
207
+ #### `oasiz.getViewportInsets(): ViewportInsets`
208
+
209
+ Returns effective viewport insets in both CSS pixels and normalized percentages:
210
+
211
+ ```ts
212
+ const insets = oasiz.getViewportInsets();
213
+ hud.style.paddingTop = `${insets.pixels.top}px`;
214
+ hud.style.paddingRight = `${insets.pixels.right}px`;
215
+ hud.style.paddingBottom = `${insets.pixels.bottom}px`;
216
+ hud.style.paddingLeft = `${insets.pixels.left}px`;
217
+ ```
218
+
219
+ Percentages use the matching active viewport axis:
220
+
221
+ - `top` / `bottom` are percentages of viewport height
222
+ - `left` / `right` are percentages of viewport width
223
+
224
+ Hosts may expose pixels via `window.getViewportInsets()` or `window.__OASIZ_VIEWPORT_INSETS__`, for example `{ top, right, bottom, left }` or `{ pixels: { top, right, bottom, left } }`. Hosts may expose percentages via `window.getViewportInsetsPercent()`, `window.__OASIZ_VIEWPORT_INSETS_PERCENT__`, or a `percent` object. Per-side globals such as `window.__OASIZ_SAFE_AREA_BOTTOM__` are also supported.
225
+
226
+ #### `oasiz.getSafeAreaTop(): number`
227
+
228
+ Legacy alias for `oasiz.getViewportInsets().percent.top`. Returns the top inset as a percentage of viewport height (0–100). To get pixels in JavaScript, prefer `oasiz.getViewportInsets().pixels.top`. In CSS, the percent value matches **`vh`** units (for example `12.5vh` for 12.5% of the viewport height). Unsupported hosts return `0`.
229
+
230
+ ```ts
231
+ const safeTopPct = oasiz.getSafeAreaTop();
232
+ document.documentElement.style.setProperty("--safe-top", `${safeTopPct}vh`);
233
+ ```
234
+
235
+ #### `oasiz.safeAreaTop`
236
+
237
+ Getter alias for `getSafeAreaTop()`.
238
+
239
+ #### `oasiz.viewportInsets`
240
+
241
+ Getter alias for `getViewportInsets()`.
242
+
243
+ Recommended CSS pattern:
244
+
245
+ ```css
246
+ :root {
247
+ --safe-top: 0px;
248
+ }
249
+
250
+ #top-bar {
251
+ padding-top: var(--safe-top);
252
+ }
253
+ ```
254
+
255
+ #### `oasiz.setLeaderboardVisible(visible: boolean): void`
256
+
257
+ 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.
258
+
259
+ ```ts
260
+ function openCustomOverlay(): void {
261
+ oasiz.setLeaderboardVisible(false);
262
+ }
263
+
264
+ function closeCustomOverlay(): void {
265
+ oasiz.setLeaderboardVisible(true);
266
+ }
267
+ ```
268
+
269
+ Unsupported hosts safely no-op.
270
+
271
+ ### Graphics performance
272
+
273
+ #### `oasiz.getGraphicsPerformance(): GraphicsPerformanceMetric`
274
+
275
+ Returns a recommended FPS target and suggested rendering tier:
276
+
277
+ ```ts
278
+ const graphics = oasiz.getGraphicsPerformance();
279
+
280
+ switch (graphics.tier) {
281
+ case "high":
282
+ enablePostProcessing();
283
+ renderer.setPixelRatio(1.5);
284
+ break;
285
+ case "medium":
286
+ renderer.setPixelRatio(1.25);
287
+ break;
288
+ case "low":
289
+ disableHeavyParticles();
290
+ renderer.setPixelRatio(1);
291
+ break;
292
+ case "minimal":
293
+ disableOptionalEffects();
294
+ renderer.setPixelRatio(0.75);
295
+ break;
296
+ }
297
+ ```
298
+
299
+ The returned object is `{ fps, tier }`, where `fps` is the recommended render target and `tier` is `"minimal"`, `"low"`, `"medium"`, or `"high"`. Hosts can inject measured values with `window.getGraphicsPerformance()` or `window.__OASIZ_GRAPHICS_PERFORMANCE__`; otherwise the SDK estimates from browser, device, and WebGL capability signals.
300
+
301
+ #### `oasiz.graphicsPerformance`
184
302
 
185
- ## Lifecycle
303
+ Getter alias for `getGraphicsPerformance()`.
304
+
305
+ ### Lifecycle
186
306
 
187
307
  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
308
 
189
- ### `oasiz.onPause(callback: () => void): Unsubscribe`
190
- ### `oasiz.onResume(callback: () => void): Unsubscribe`
309
+ #### `oasiz.onPause(callback: () => void): Unsubscribe`
310
+
311
+ #### `oasiz.onResume(callback: () => void): Unsubscribe`
191
312
 
192
313
  Both return an unsubscribe function.
193
314
 
194
315
  ```ts
195
- const offPause = oasiz.onPause(() => {
316
+ const offPause = oasiz.onPause(() => {
196
317
  this.gameLoop.stop();
197
318
  this.bgMusic.pause();
198
319
  });
@@ -207,19 +328,17 @@ offPause();
207
328
  offResume();
208
329
  ```
209
330
 
210
- ---
211
-
212
- ## Navigation
331
+ ### Navigation
213
332
 
214
333
  Use navigation hooks when your game needs to control back behavior (Android back / web Escape) or participate in host-driven close events.
215
334
 
216
- ### `oasiz.onBackButton(callback: () => void): Unsubscribe`
335
+ #### `oasiz.onBackButton(callback: () => void): Unsubscribe`
217
336
 
218
337
  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
338
 
220
339
  Use this for pause menus, in-game overlays, or custom back-stack behavior.
221
340
 
222
- If your callback throws, Oasiz falls back to closing the game and returning the player to Oasiz home before rethrowing the error for debugging/reporting.
341
+ **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."`).
223
342
 
224
343
  ```ts
225
344
  const offBack = oasiz.onBackButton(() => {
@@ -234,7 +353,30 @@ const offBack = oasiz.onBackButton(() => {
234
353
  offBack();
235
354
  ```
236
355
 
237
- ### `oasiz.leaveGame(): void`
356
+ #### `oasiz.enableBackButtonTesting(options?: BackButtonTestingOptions): BackButtonTestingHandle`
357
+
358
+ Opt-in local web helper for testing back override behavior without the Oasiz app bridge. Call it before registering `onBackButton` in local development:
359
+
360
+ ```ts
361
+ if (import.meta.env.DEV) {
362
+ oasiz.enableBackButtonTesting();
363
+ }
364
+
365
+ const offBack = oasiz.onBackButton(() => {
366
+ closePauseMenuOrOpenIt();
367
+ });
368
+ ```
369
+
370
+ While a back listener is active, the helper maps Escape to the same `oasiz:back` event the app sends. By default it also traps one browser-history entry so the browser Back button dispatches `oasiz:back` instead of leaving the page.
371
+
372
+ ```ts
373
+ const backTest = oasiz.enableBackButtonTesting({ browserHistory: false });
374
+ testBackButton.onclick = () => backTest.triggerBack();
375
+ ```
376
+
377
+ The returned handle also exposes `triggerLeave()` and `destroy()`. This helper is for local/dev web testing; in the app, the real bridge still owns back behavior.
378
+
379
+ #### `oasiz.leaveGame(): void`
238
380
 
239
381
  Programmatically request the host to close the current game (for example, from a Quit button inside your game UI).
240
382
 
@@ -269,14 +411,14 @@ const offLeave = oasiz.onLeaveGame(() => {
269
411
  offLeave();
270
412
  ```
271
413
 
272
- ---
273
-
274
- ## Multiplayer
414
+ ### Multiplayer
275
415
 
276
- ### `oasiz.shareRoomCode(code: string | null)`
416
+ #### `oasiz.shareRoomCode(code: string | null, options?: { inviteOverride?: boolean })`
277
417
 
278
418
  Notify the platform of the active multiplayer room so friends can join via the invite system. Pass `null` when leaving a room.
279
419
 
420
+ 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.
421
+
280
422
  ```ts
281
423
  import { insertCoin, getRoomCode } from "playroomkit";
282
424
  import { oasiz } from "@oasiz/sdk";
@@ -288,7 +430,26 @@ oasiz.shareRoomCode(getRoomCode());
288
430
  oasiz.shareRoomCode(null);
289
431
  ```
290
432
 
291
- ### Read-only injected values
433
+ ```ts
434
+ // Game-owned invite UI: hide the platform pill, keep room tracking
435
+ oasiz.shareRoomCode(getRoomCode(), { inviteOverride: true });
436
+ ```
437
+
438
+ #### `oasiz.openInviteModal(): void`
439
+
440
+ Opens the platform invite-friends UI when the bridge is available. Typically used together with `shareRoomCode` (for example, your own invite button calls this).
441
+
442
+ ```ts
443
+ import { openInviteModal, shareRoomCode } from "@oasiz/sdk";
444
+
445
+ shareRoomCode("ABCD", { inviteOverride: true });
446
+
447
+ inviteButton.addEventListener("click", () => {
448
+ openInviteModal();
449
+ });
450
+ ```
451
+
452
+ #### Read-only injected values
292
453
 
293
454
  These are populated by the platform before the game loads. Always check for `undefined` before using.
294
455
 
@@ -302,25 +463,30 @@ if (oasiz.roomCode) {
302
463
  }
303
464
 
304
465
  // Player identity for multiplayer games
305
- const name = oasiz.playerName;
466
+ const name = oasiz.playerName;
306
467
  const avatar = oasiz.playerAvatar;
307
468
  ```
308
469
 
309
- ---
310
-
311
- ## Named exports
470
+ ### Named exports
312
471
 
313
472
  All methods are also available as named exports if you prefer not to use the `oasiz` namespace object:
314
473
 
315
474
  ```ts
316
475
  import {
317
476
  submitScore,
318
- emitScoreConfig,
477
+ share,
319
478
  triggerHaptic,
320
479
  loadGameState,
321
480
  saveGameState,
322
481
  flushGameState,
323
482
  shareRoomCode,
483
+ openInviteModal,
484
+ enableLogOverlay,
485
+ enableBackButtonTesting,
486
+ getGraphicsPerformance,
487
+ getSafeAreaTop,
488
+ getViewportInsets,
489
+ setLeaderboardVisible,
324
490
  onPause,
325
491
  onResume,
326
492
  onBackButton,
@@ -333,18 +499,227 @@ import {
333
499
  } from "@oasiz/sdk";
334
500
  ```
335
501
 
502
+ ### TypeScript types
503
+
504
+ ```ts
505
+ import type {
506
+ BackButtonTestingHandle,
507
+ BackButtonTestingOptions,
508
+ GameState,
509
+ GraphicsPerformanceMetric,
510
+ GraphicsPerformanceTier,
511
+ HapticType,
512
+ LogOverlayEntry,
513
+ LogOverlayHandle,
514
+ LogOverlayLevel,
515
+ LogOverlayOptions,
516
+ ShareRequest,
517
+ ShareRoomCodeOptions,
518
+ Unsubscribe,
519
+ } from "@oasiz/sdk";
520
+ ```
521
+
336
522
  ---
337
523
 
338
- ## TypeScript types
524
+ ## Unity WebGL SDK
525
+
526
+ 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`).
527
+
528
+ ### Setup
529
+
530
+ 1. Copy `packages/OasizSDK` from this repo into `Assets/OasizSDK`.
531
+ 2. Ensure the **WebGL** platform is selected for release builds; the `.jslib` under `Runtime/Plugins/WebGL/` is included automatically for WebGL.
532
+ 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`.
533
+
534
+ ### Quick start
535
+
536
+ ```csharp
537
+ using Oasiz;
538
+ using UnityEngine;
539
+
540
+ public class GameManager : MonoBehaviour
541
+ {
542
+ void Start()
543
+ {
544
+ // Ensure the singleton is initialized early
545
+ _ = OasizSDK.Instance;
546
+
547
+ // Subscribe to lifecycle events
548
+ OasizSDK.OnPause += OnPause;
549
+ OasizSDK.OnResume += OnResume;
550
+
551
+ // Offset UI for the host's top safe area (0–100 percent of Screen.height)
552
+ float safeTopPct = OasizSDK.SafeAreaTop;
553
+ float safeTopPx = safeTopPct / 100f * Screen.height;
554
+ Debug.Log($"Safe area top: {safeTopPx}px ({safeTopPct}% of height)");
555
+
556
+ // Pick visual settings for the current device
557
+ GraphicsPerformanceMetric graphics = OasizSDK.GetGraphicsPerformance();
558
+ Debug.Log($"Graphics tier: {graphics.tier} ({graphics.fps} FPS target)");
559
+
560
+ // Emit score normalization anchors
561
+ OasizSDK.EmitScoreConfig(new ScoreConfig(
562
+ new ScoreAnchor(10, 100),
563
+ new ScoreAnchor(30, 300),
564
+ new ScoreAnchor(75, 600),
565
+ new ScoreAnchor(200, 950)
566
+ ));
567
+ }
568
+
569
+ void OnGameOver(int finalScore)
570
+ {
571
+ OasizSDK.SubmitScore(finalScore);
572
+ OasizSDK.FlushGameState();
573
+ OasizSDK.SetLeaderboardVisible(true);
574
+ }
575
+
576
+ void OnGameplayStart()
577
+ {
578
+ OasizSDK.SetLeaderboardVisible(false);
579
+ }
580
+
581
+ void OnPause() => Time.timeScale = 0f;
582
+ void OnResume() => Time.timeScale = 1f;
583
+
584
+ void OnDestroy()
585
+ {
586
+ OasizSDK.OnPause -= OnPause;
587
+ OasizSDK.OnResume -= OnResume;
588
+ }
589
+ }
590
+ ```
591
+
592
+ ### API parity (TypeScript → C#)
593
+
594
+ | HTML5 (`@oasiz/sdk`) | Unity (`Oasiz` namespace) |
595
+ | --- | --- |
596
+ | `oasiz.submitScore(n)` | `OasizSDK.SubmitScore(int)` |
597
+ | `oasiz.triggerHaptic(type)` | `OasizSDK.TriggerHaptic(HapticType)` |
598
+ | `oasiz.loadGameState()` | `OasizSDK.LoadGameState()` → `Dictionary<string, object>` |
599
+ | `oasiz.saveGameState(obj)` | `OasizSDK.SaveGameState(Dictionary<string, object>)` |
600
+ | `oasiz.flushGameState()` | `OasizSDK.FlushGameState()` |
601
+ | `oasiz.getViewportInsets()` / `viewportInsets` | `OasizSDK.GetViewportInsets()` (`ViewportInsets`, each side 0–100, % of matching viewport axis) |
602
+ | `oasiz.getSafeAreaTop()` / `safeAreaTop` | `OasizSDK.GetSafeAreaTop()` / `OasizSDK.SafeAreaTop` (`float`, 0–100, % of viewport height; legacy alias for top viewport inset) |
603
+ | `oasiz.setLeaderboardVisible(v)` | `OasizSDK.SetLeaderboardVisible(bool)` |
604
+ | `oasiz.getGraphicsPerformance()` / `graphicsPerformance` | `OasizSDK.GetGraphicsPerformance()` / `OasizSDK.GraphicsPerformance` (`GraphicsPerformanceMetric`, recommended FPS plus `minimal` / `low` / `medium` / `high`) |
605
+ | `oasiz.onPause` / `onResume` | `OasizSDK.OnPause` / `OnResume` static events |
606
+ | `oasiz.onBackButton` | `OasizSDK.OnBackButton` or `SubscribeBackButton(Action)` (reference-counts `__oasizSetBackOverride`) |
607
+ | `oasiz.enableBackButtonTesting()` | `OasizSDK.EnableBackButtonTesting()` (WebGL local/dev helper for Escape/browser Back testing) |
608
+ | `oasiz.onLeaveGame` | `OasizSDK.OnLeaveGame` |
609
+ | `oasiz.leaveGame()` | `OasizSDK.LeaveGame()` |
610
+ | `oasiz.share(request)` | `OasizSDK.Share(ShareRequest)` |
611
+ | `oasiz.shareRoomCode` | `OasizSDK.ShareRoomCode(string, ShareRoomCodeOptions)` |
612
+ | `oasiz.openInviteModal()` | `OasizSDK.OpenInviteModal()` |
613
+ | `oasiz.gameId` / `roomCode` / ... | `OasizSDK.GameId` / `RoomCode` / `PlayerName` / `PlayerAvatar` |
614
+ | -- | `OasizSDK.EmitScoreConfig(ScoreConfig)` → `window.emitScoreConfig` (Unity-only helper for normalized score UI) |
615
+ | `oasiz.enableLogOverlay` | `OasizSDK.EnableLogOverlay(LogOverlayOptions)` (see note below) |
616
+ | -- | `OasizSDK.AppendLogOverlay(level, message, stackTrace)` (see note below) |
617
+
618
+ ### Local back-button testing (Unity WebGL)
619
+
620
+ For local WebGL builds outside the Oasiz app, install the dev bridge before testing your subscribed back handler:
621
+
622
+ ```csharp
623
+ #if DEVELOPMENT_BUILD || UNITY_EDITOR
624
+ OasizSDK.EnableBackButtonTesting();
625
+ #endif
626
+
627
+ Action unsubscribeBack = OasizSDK.SubscribeBackButton(() =>
628
+ {
629
+ TogglePauseMenu();
630
+ });
631
+ ```
339
632
 
340
- ```ts
341
- import type { HapticType, ScoreConfig, ScoreAnchor, GameState } from "@oasiz/sdk";
633
+ While the override is active, Escape and the browser Back button dispatch the same `oasiz:back` event that the app bridge sends. You can disable either input with `OasizSDK.EnableBackButtonTesting(keyboard: false)` or `OasizSDK.EnableBackButtonTesting(browserHistory: false)`.
634
+
635
+ ### Share (Unity)
636
+
637
+ HTML5 **`oasiz.share`** returns a **Promise** you can `await`. Unity **`OasizSDK.Share(ShareRequest)`** returns **`void`**: C# validation throws **`ArgumentException`** with the same rules as TypeScript (at least one of text, score, or image; non-negative integer score; `http(s)` or `data:image/...;base64,...` image). The call forwards JSON to **`window.__oasizShareRequest`**. If the host promise rejects, the **WebGL `.jslib` logs the error** to the browser console.
638
+
639
+ ```csharp
640
+ OasizSDK.Share(new ShareRequest
641
+ {
642
+ Text = "Beat this run!",
643
+ Score = 1200,
644
+ Image = "https://example.com/card.png",
645
+ });
646
+ ```
647
+
648
+ ### Types
649
+
650
+ ```csharp
651
+ // Haptic feedback intensity
652
+ public enum HapticType { Light, Medium, Heavy, Success, Error }
653
+
654
+ // Score normalization (exactly 4 anchors required)
655
+ public struct ScoreAnchor { public int raw; public int normalized; }
656
+ public struct ScoreConfig { public ScoreAnchor[] anchors; }
657
+
658
+ // Host share sheet (text / score / image URL or data URL)
659
+ public class ShareRequest
660
+ {
661
+ public string Text { get; set; }
662
+ public int? Score { get; set; }
663
+ public string Image { get; set; }
664
+ }
665
+
666
+ // Multiplayer invite options
667
+ public class ShareRoomCodeOptions { public bool InviteOverride { get; set; } }
668
+
669
+ // Log overlay configuration
670
+ public class LogOverlayOptions
671
+ {
672
+ public bool Enabled { get; set; } = true;
673
+ public bool Collapsed { get; set; } = false;
674
+ public int MaxEntries { get; set; } = 200;
675
+ public string Title { get; set; } = "SDK Logs";
676
+ }
677
+
678
+ // Log overlay lifecycle handle
679
+ public class LogOverlayHandle
680
+ {
681
+ public void Clear();
682
+ public void Hide();
683
+ public void Show();
684
+ public bool IsVisible();
685
+ public void Destroy();
686
+ }
687
+ ```
688
+
689
+ ### Back button and errors
690
+
691
+ 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.
692
+
693
+ ```csharp
694
+ // Subscribe with automatic unsubscribe support
695
+ var offBack = OasizSDK.SubscribeBackButton(() =>
696
+ {
697
+ if (isPaused)
698
+ Resume();
699
+ else
700
+ Pause();
701
+ });
702
+
703
+ // Unsubscribe when no longer needed
704
+ offBack();
342
705
  ```
343
706
 
707
+ ### Editor vs WebGL builds
708
+
709
+ 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.
710
+
711
+ ### Log overlay (Unity)
712
+
713
+ 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.
714
+
715
+ `AppendLogOverlay(level, message, stackTrace)` lets you pipe `Debug.Log` output into the overlay manually, since many embedded WebViews do not route Unity player logs through `console.log`. Valid levels: `"debug"`, `"log"`, `"info"`, `"warn"`, `"error"`.
716
+
344
717
  ---
345
718
 
346
719
  ## Local development
347
720
 
721
+ ### HTML5 / TypeScript
722
+
348
723
  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:
349
724
 
350
725
  ```
@@ -352,3 +727,7 @@ All methods safely no-op when the platform bridges are not injected. In developm
352
727
  ```
353
728
 
354
729
  No crashes, no special setup required for local dev.
730
+
731
+ ### Unity WebGL
732
+
733
+ 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.