@oasiz/sdk 1.5.6 → 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
@@ -13,7 +13,7 @@ Both talk to the same host bridges (`window.submitScore`, `__oasizLeaveGame`, la
13
13
 
14
14
  ## HTML5 / TypeScript (`@oasiz/sdk`)
15
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.
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
17
 
18
18
  ### Install
19
19
 
@@ -44,13 +44,17 @@ document.documentElement.style.setProperty(
44
44
  `${oasiz.safeAreaTop}vh`,
45
45
  );
46
46
 
47
- // 5. Submit score when the game ends
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
48
52
  oasiz.submitScore(score);
49
53
 
50
- // 6. Optionally hide the leaderboard while a custom overlay is open
54
+ // 7. Optionally hide the leaderboard while a custom overlay is open
51
55
  oasiz.setLeaderboardVisible(false);
52
56
 
53
- // 7. Optionally surface console logs in-game while debugging
57
+ // 8. Optionally surface console logs in-game while debugging
54
58
  oasiz.enableLogOverlay({
55
59
  enabled: new URLSearchParams(window.location.search).has("oasizLogs"),
56
60
  collapsed: true,
@@ -264,6 +268,40 @@ function closeCustomOverlay(): void {
264
268
 
265
269
  Unsupported hosts safely no-op.
266
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`
302
+
303
+ Getter alias for `getGraphicsPerformance()`.
304
+
267
305
  ### Lifecycle
268
306
 
269
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.
@@ -315,6 +353,29 @@ const offBack = oasiz.onBackButton(() => {
315
353
  offBack();
316
354
  ```
317
355
 
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
+
318
379
  #### `oasiz.leaveGame(): void`
319
380
 
320
381
  Programmatically request the host to close the current game (for example, from a Quit button inside your game UI).
@@ -421,6 +482,8 @@ import {
421
482
  shareRoomCode,
422
483
  openInviteModal,
423
484
  enableLogOverlay,
485
+ enableBackButtonTesting,
486
+ getGraphicsPerformance,
424
487
  getSafeAreaTop,
425
488
  getViewportInsets,
426
489
  setLeaderboardVisible,
@@ -440,7 +503,11 @@ import {
440
503
 
441
504
  ```ts
442
505
  import type {
506
+ BackButtonTestingHandle,
507
+ BackButtonTestingOptions,
443
508
  GameState,
509
+ GraphicsPerformanceMetric,
510
+ GraphicsPerformanceTier,
444
511
  HapticType,
445
512
  LogOverlayEntry,
446
513
  LogOverlayHandle,
@@ -486,6 +553,10 @@ public class GameManager : MonoBehaviour
486
553
  float safeTopPx = safeTopPct / 100f * Screen.height;
487
554
  Debug.Log($"Safe area top: {safeTopPx}px ({safeTopPct}% of height)");
488
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
+
489
560
  // Emit score normalization anchors
490
561
  OasizSDK.EmitScoreConfig(new ScoreConfig(
491
562
  new ScoreAnchor(10, 100),
@@ -530,8 +601,10 @@ public class GameManager : MonoBehaviour
530
601
  | `oasiz.getViewportInsets()` / `viewportInsets` | `OasizSDK.GetViewportInsets()` (`ViewportInsets`, each side 0–100, % of matching viewport axis) |
531
602
  | `oasiz.getSafeAreaTop()` / `safeAreaTop` | `OasizSDK.GetSafeAreaTop()` / `OasizSDK.SafeAreaTop` (`float`, 0–100, % of viewport height; legacy alias for top viewport inset) |
532
603
  | `oasiz.setLeaderboardVisible(v)` | `OasizSDK.SetLeaderboardVisible(bool)` |
604
+ | `oasiz.getGraphicsPerformance()` / `graphicsPerformance` | `OasizSDK.GetGraphicsPerformance()` / `OasizSDK.GraphicsPerformance` (`GraphicsPerformanceMetric`, recommended FPS plus `minimal` / `low` / `medium` / `high`) |
533
605
  | `oasiz.onPause` / `onResume` | `OasizSDK.OnPause` / `OnResume` static events |
534
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) |
535
608
  | `oasiz.onLeaveGame` | `OasizSDK.OnLeaveGame` |
536
609
  | `oasiz.leaveGame()` | `OasizSDK.LeaveGame()` |
537
610
  | `oasiz.share(request)` | `OasizSDK.Share(ShareRequest)` |
@@ -542,6 +615,23 @@ public class GameManager : MonoBehaviour
542
615
  | `oasiz.enableLogOverlay` | `OasizSDK.EnableLogOverlay(LogOverlayOptions)` (see note below) |
543
616
  | -- | `OasizSDK.AppendLogOverlay(level, message, stackTrace)` (see note below) |
544
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
+ ```
632
+
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
+
545
635
  ### Share (Unity)
546
636
 
547
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.
package/dist/index.cjs CHANGED
@@ -21,9 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  addScore: () => addScore,
24
+ enableBackButtonTesting: () => enableBackButtonTesting,
24
25
  enableLogOverlay: () => enableLogOverlay,
25
26
  flushGameState: () => flushGameState,
26
27
  getGameId: () => getGameId,
28
+ getGraphicsPerformance: () => getGraphicsPerformance,
27
29
  getPlayerAvatar: () => getPlayerAvatar,
28
30
  getPlayerCharacter: () => getPlayerCharacter,
29
31
  getPlayerId: () => getPlayerId,
@@ -1628,7 +1630,9 @@ function setLeaderboardVisible(visible) {
1628
1630
  }
1629
1631
 
1630
1632
  // src/navigation.ts
1633
+ var BACK_BUTTON_TEST_STATE_KEY = "__oasizBackButtonTest";
1631
1634
  var activeBackListeners = 0;
1635
+ var activeBackButtonTestingHandle;
1632
1636
  function isDevelopment10() {
1633
1637
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
1634
1638
  return nodeEnv !== "production";
@@ -1654,6 +1658,16 @@ function normalizeNavigationError(error) {
1654
1658
  typeof error === "string" ? error : "Back button callback failed."
1655
1659
  );
1656
1660
  }
1661
+ function isRecord2(value) {
1662
+ return typeof value === "object" && value !== null;
1663
+ }
1664
+ function dispatchNavigationEvent(eventName) {
1665
+ const bridge = getBridgeWindow9();
1666
+ if (!bridge || typeof bridge.dispatchEvent !== "function") {
1667
+ return;
1668
+ }
1669
+ bridge.dispatchEvent(new Event(eventName));
1670
+ }
1657
1671
  function addNavigationListener(eventName, callback) {
1658
1672
  if (typeof window === "undefined") {
1659
1673
  if (isDevelopment10()) {
@@ -1668,6 +1682,151 @@ function addNavigationListener(eventName, callback) {
1668
1682
  window.addEventListener(eventName, handler);
1669
1683
  return () => window.removeEventListener(eventName, handler);
1670
1684
  }
1685
+ function enableBackButtonTesting(options = {}) {
1686
+ activeBackButtonTestingHandle?.destroy();
1687
+ const bridge = getBridgeWindow9();
1688
+ if (!bridge) {
1689
+ if (isDevelopment10()) {
1690
+ console.warn(
1691
+ "[oasiz/sdk] enableBackButtonTesting requires a browser window."
1692
+ );
1693
+ }
1694
+ return {
1695
+ destroy: () => {
1696
+ },
1697
+ isBackOverrideActive: () => false,
1698
+ triggerBack: () => {
1699
+ },
1700
+ triggerLeave: () => {
1701
+ }
1702
+ };
1703
+ }
1704
+ const bridgeWindow = bridge;
1705
+ const keyboard = options.keyboard ?? true;
1706
+ const browserHistory = options.browserHistory ?? true;
1707
+ const log = options.log === true;
1708
+ const previousSetBackOverride = bridgeWindow.__oasizSetBackOverride;
1709
+ const previousLeaveGame = bridgeWindow.__oasizLeaveGame;
1710
+ let destroyed = false;
1711
+ let backOverrideActive = false;
1712
+ let historyTrapArmed = false;
1713
+ function maybeLog(message) {
1714
+ if (log) {
1715
+ console.info("[oasiz/sdk] " + message);
1716
+ }
1717
+ }
1718
+ function canUseHistoryTrap() {
1719
+ return browserHistory && typeof bridgeWindow.history?.pushState === "function" && typeof bridgeWindow.history?.replaceState === "function" && typeof bridgeWindow.location?.href === "string";
1720
+ }
1721
+ function ensureHistoryTrap() {
1722
+ if (!backOverrideActive || historyTrapArmed || !canUseHistoryTrap()) {
1723
+ return;
1724
+ }
1725
+ try {
1726
+ const currentState = isRecord2(bridgeWindow.history.state) ? bridgeWindow.history.state : {};
1727
+ bridgeWindow.history.replaceState(
1728
+ { ...currentState, [BACK_BUTTON_TEST_STATE_KEY]: "base" },
1729
+ "",
1730
+ bridgeWindow.location.href
1731
+ );
1732
+ bridgeWindow.history.pushState(
1733
+ { [BACK_BUTTON_TEST_STATE_KEY]: "trap" },
1734
+ "",
1735
+ bridgeWindow.location.href
1736
+ );
1737
+ historyTrapArmed = true;
1738
+ maybeLog("Local browser Back testing is armed.");
1739
+ } catch (error) {
1740
+ historyTrapArmed = false;
1741
+ if (log) {
1742
+ console.warn("[oasiz/sdk] Failed to arm browser Back testing:", error);
1743
+ }
1744
+ }
1745
+ }
1746
+ function triggerBack() {
1747
+ dispatchNavigationEvent("oasiz:back");
1748
+ }
1749
+ function triggerLeave() {
1750
+ dispatchNavigationEvent("oasiz:leave");
1751
+ }
1752
+ function setBackOverride(active) {
1753
+ backOverrideActive = active;
1754
+ if (active) {
1755
+ ensureHistoryTrap();
1756
+ }
1757
+ if (typeof previousSetBackOverride === "function") {
1758
+ previousSetBackOverride(active);
1759
+ }
1760
+ maybeLog("Back override " + (active ? "enabled" : "disabled") + ".");
1761
+ }
1762
+ function stopBackEvent(event) {
1763
+ event.preventDefault();
1764
+ event.stopPropagation();
1765
+ event.stopImmediatePropagation?.();
1766
+ }
1767
+ const handleKeyDown = (event) => {
1768
+ if (!backOverrideActive || event.key !== "Escape") {
1769
+ return;
1770
+ }
1771
+ stopBackEvent(event);
1772
+ triggerBack();
1773
+ };
1774
+ const handlePopState = (event) => {
1775
+ if (!backOverrideActive) {
1776
+ historyTrapArmed = false;
1777
+ return;
1778
+ }
1779
+ stopBackEvent(event);
1780
+ triggerBack();
1781
+ historyTrapArmed = false;
1782
+ ensureHistoryTrap();
1783
+ };
1784
+ const testLeaveGame = () => {
1785
+ triggerLeave();
1786
+ if (typeof previousLeaveGame === "function") {
1787
+ previousLeaveGame();
1788
+ }
1789
+ };
1790
+ bridgeWindow.__oasizSetBackOverride = setBackOverride;
1791
+ bridgeWindow.__oasizLeaveGame = testLeaveGame;
1792
+ if (keyboard) {
1793
+ bridgeWindow.addEventListener("keydown", handleKeyDown);
1794
+ }
1795
+ if (browserHistory) {
1796
+ bridgeWindow.addEventListener("popstate", handlePopState);
1797
+ }
1798
+ if (activeBackListeners > 0) {
1799
+ setBackOverride(true);
1800
+ }
1801
+ maybeLog("Back button testing bridge installed.");
1802
+ const handle = {
1803
+ destroy: () => {
1804
+ if (destroyed) return;
1805
+ destroyed = true;
1806
+ if (keyboard) {
1807
+ bridgeWindow.removeEventListener("keydown", handleKeyDown);
1808
+ }
1809
+ if (browserHistory) {
1810
+ bridgeWindow.removeEventListener("popstate", handlePopState);
1811
+ }
1812
+ if (bridgeWindow.__oasizSetBackOverride === setBackOverride) {
1813
+ bridgeWindow.__oasizSetBackOverride = previousSetBackOverride;
1814
+ }
1815
+ if (bridgeWindow.__oasizLeaveGame === testLeaveGame) {
1816
+ bridgeWindow.__oasizLeaveGame = previousLeaveGame;
1817
+ }
1818
+ if (activeBackButtonTestingHandle === handle) {
1819
+ activeBackButtonTestingHandle = void 0;
1820
+ }
1821
+ maybeLog("Back button testing bridge removed.");
1822
+ },
1823
+ isBackOverrideActive: () => backOverrideActive,
1824
+ triggerBack,
1825
+ triggerLeave
1826
+ };
1827
+ activeBackButtonTestingHandle = handle;
1828
+ return handle;
1829
+ }
1671
1830
  function onBackButton(callback) {
1672
1831
  const off = addNavigationListener("oasiz:back", () => {
1673
1832
  try {
@@ -1711,6 +1870,329 @@ function leaveGame() {
1711
1870
  warnMissingBridge7("__oasizLeaveGame");
1712
1871
  }
1713
1872
 
1873
+ // src/performance.ts
1874
+ var DEFAULT_GRAPHICS_PERFORMANCE = {
1875
+ fps: 45,
1876
+ tier: "medium"
1877
+ };
1878
+ var TIER_DEFAULT_FPS = {
1879
+ minimal: 24,
1880
+ low: 30,
1881
+ medium: 45,
1882
+ high: 60
1883
+ };
1884
+ var cachedEstimatedGraphicsPerformance;
1885
+ function getBridgeWindow10() {
1886
+ if (typeof window === "undefined") {
1887
+ return void 0;
1888
+ }
1889
+ return window;
1890
+ }
1891
+ function isRecord3(value) {
1892
+ return typeof value === "object" && value !== null;
1893
+ }
1894
+ function toFiniteNumber2(value) {
1895
+ if (typeof value === "number" && Number.isFinite(value)) {
1896
+ return value;
1897
+ }
1898
+ if (typeof value === "string") {
1899
+ const parsed = Number.parseFloat(value.trim());
1900
+ if (Number.isFinite(parsed)) {
1901
+ return parsed;
1902
+ }
1903
+ }
1904
+ return void 0;
1905
+ }
1906
+ function clampScore(value) {
1907
+ return Math.min(100, Math.max(0, Math.round(value)));
1908
+ }
1909
+ function clampFps(value) {
1910
+ return Math.min(240, Math.max(1, Math.round(value)));
1911
+ }
1912
+ function tierFromScore(score) {
1913
+ if (score < 25) return "minimal";
1914
+ if (score < 40) return "low";
1915
+ if (score < 70) return "medium";
1916
+ return "high";
1917
+ }
1918
+ function tierFromFps(fps) {
1919
+ if (fps < 30) return "minimal";
1920
+ if (fps < 45) return "low";
1921
+ if (fps < 58) return "medium";
1922
+ return "high";
1923
+ }
1924
+ function fpsFromScore(score) {
1925
+ return TIER_DEFAULT_FPS[tierFromScore(score)];
1926
+ }
1927
+ function normalizeTier(value) {
1928
+ if (typeof value !== "string") {
1929
+ return void 0;
1930
+ }
1931
+ const normalized = value.trim().toLowerCase();
1932
+ if (normalized === "minimal" || normalized === "safe") return "minimal";
1933
+ if (normalized === "high") return "high";
1934
+ if (normalized === "medium") return "medium";
1935
+ if (normalized === "low") return "low";
1936
+ return void 0;
1937
+ }
1938
+ function firstDefined2(...values) {
1939
+ return values.find((value) => typeof value !== "undefined");
1940
+ }
1941
+ function normalizeMetric(value) {
1942
+ if (typeof value === "undefined" || value === null) {
1943
+ return void 0;
1944
+ }
1945
+ if (typeof value === "number" || typeof value === "string") {
1946
+ const numeric = toFiniteNumber2(value);
1947
+ if (typeof numeric !== "undefined") {
1948
+ const fps2 = clampFps(numeric);
1949
+ return { fps: fps2, tier: tierFromFps(fps2) };
1950
+ }
1951
+ const tier2 = normalizeTier(value);
1952
+ if (tier2) {
1953
+ return { fps: TIER_DEFAULT_FPS[tier2], tier: tier2 };
1954
+ }
1955
+ return void 0;
1956
+ }
1957
+ if (!isRecord3(value)) {
1958
+ return void 0;
1959
+ }
1960
+ const fpsCandidate = firstDefined2(
1961
+ value.fps,
1962
+ value.targetFps,
1963
+ value.frameRate,
1964
+ value.framesPerSecond
1965
+ );
1966
+ const legacyScoreCandidate = firstDefined2(
1967
+ value.score,
1968
+ value.metric,
1969
+ value.value,
1970
+ value.performance,
1971
+ value.performanceScore,
1972
+ value.graphicsScore
1973
+ );
1974
+ const tierCandidate = firstDefined2(
1975
+ value.tier,
1976
+ value.graphicsTier,
1977
+ value.performanceTier,
1978
+ value.qualityTier,
1979
+ value.recommendedTier
1980
+ );
1981
+ const tier = normalizeTier(tierCandidate);
1982
+ const fpsNumeric = toFiniteNumber2(fpsCandidate);
1983
+ const scoreNumeric = toFiniteNumber2(legacyScoreCandidate);
1984
+ if (typeof fpsNumeric === "undefined" && typeof scoreNumeric === "undefined" && !tier) {
1985
+ return void 0;
1986
+ }
1987
+ const fps = clampFps(
1988
+ typeof fpsNumeric !== "undefined" ? fpsNumeric : typeof scoreNumeric !== "undefined" ? fpsFromScore(clampScore(scoreNumeric)) : TIER_DEFAULT_FPS[tier]
1989
+ );
1990
+ return {
1991
+ fps,
1992
+ tier: tier ?? tierFromFps(fps)
1993
+ };
1994
+ }
1995
+ function callBridgeMetric(bridge, name) {
1996
+ const fn = bridge[name];
1997
+ if (typeof fn !== "function") {
1998
+ return void 0;
1999
+ }
2000
+ try {
2001
+ return fn.call(bridge);
2002
+ } catch (error) {
2003
+ console.error("[oasiz/sdk] " + String(name) + " failed:", error);
2004
+ return void 0;
2005
+ }
2006
+ }
2007
+ function readHostGraphicsPerformance(bridge) {
2008
+ const candidates = [
2009
+ callBridgeMetric(bridge, "getGraphicsPerformance"),
2010
+ callBridgeMetric(bridge, "getGraphicsPerformanceMetric"),
2011
+ callBridgeMetric(bridge, "getPerformanceMetric"),
2012
+ bridge.__OASIZ_GRAPHICS_PERFORMANCE__,
2013
+ bridge.__OASIZ_PERFORMANCE_METRIC__
2014
+ ];
2015
+ for (const candidate of candidates) {
2016
+ const metric = normalizeMetric(candidate);
2017
+ if (metric) {
2018
+ return metric;
2019
+ }
2020
+ }
2021
+ return void 0;
2022
+ }
2023
+ function getDevicePixelRatio2(bridge) {
2024
+ const dpr = bridge.devicePixelRatio;
2025
+ return typeof dpr === "number" && Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
2026
+ }
2027
+ function getNavigatorValue(bridge) {
2028
+ return bridge.navigator;
2029
+ }
2030
+ function parseIosMajorVersion(userAgent) {
2031
+ const match = /\bOS (\d+)_/i.exec(userAgent);
2032
+ if (!match) {
2033
+ return void 0;
2034
+ }
2035
+ const parsed = Number.parseInt(match[1] ?? "", 10);
2036
+ return Number.isFinite(parsed) ? parsed : void 0;
2037
+ }
2038
+ function isAppleMobileDevice(bridge) {
2039
+ const userAgent = bridge.navigator?.userAgent ?? "";
2040
+ return /iP(hone|ad|od)/i.test(userAgent) || /Macintosh/i.test(userAgent) && (bridge.navigator?.maxTouchPoints ?? 0) > 1;
2041
+ }
2042
+ function getLongestScreenEdge(bridge) {
2043
+ const screenWidth = bridge.screen?.width;
2044
+ const screenHeight = bridge.screen?.height;
2045
+ const innerWidth = bridge.innerWidth;
2046
+ const innerHeight = bridge.innerHeight;
2047
+ return Math.max(
2048
+ typeof screenWidth === "number" ? screenWidth : 0,
2049
+ typeof screenHeight === "number" ? screenHeight : 0,
2050
+ typeof innerWidth === "number" ? innerWidth : 0,
2051
+ typeof innerHeight === "number" ? innerHeight : 0
2052
+ );
2053
+ }
2054
+ function isCoarsePointer(bridge) {
2055
+ try {
2056
+ return bridge.matchMedia?.("(pointer: coarse)").matches === true;
2057
+ } catch {
2058
+ return false;
2059
+ }
2060
+ }
2061
+ function getWebGlInfo(bridge) {
2062
+ const canvas = bridge.document?.createElement?.("canvas");
2063
+ if (!canvas || typeof canvas.getContext !== "function") {
2064
+ return { webglVersion: 0 };
2065
+ }
2066
+ const gl2 = canvas.getContext("webgl2");
2067
+ const gl = gl2 ?? canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
2068
+ if (!gl) {
2069
+ return { webglVersion: 0 };
2070
+ }
2071
+ const context = gl;
2072
+ let maxTextureSize;
2073
+ let renderer;
2074
+ try {
2075
+ const value = context.getParameter(context.MAX_TEXTURE_SIZE);
2076
+ if (typeof value === "number" && Number.isFinite(value)) {
2077
+ maxTextureSize = value;
2078
+ }
2079
+ } catch {
2080
+ }
2081
+ try {
2082
+ const debugInfo = context.getExtension("WEBGL_debug_renderer_info");
2083
+ const rendererParam = debugInfo?.UNMASKED_RENDERER_WEBGL ?? context.RENDERER;
2084
+ const value = context.getParameter(rendererParam);
2085
+ if (typeof value === "string") {
2086
+ renderer = value;
2087
+ }
2088
+ } catch {
2089
+ }
2090
+ return {
2091
+ maxTextureSize,
2092
+ renderer,
2093
+ webglVersion: gl2 ? 2 : 1
2094
+ };
2095
+ }
2096
+ function applyMemoryScore(score, memory) {
2097
+ if (typeof memory !== "number" || !Number.isFinite(memory) || memory <= 0) {
2098
+ return score;
2099
+ }
2100
+ if (memory <= 1) return score - 18;
2101
+ if (memory <= 2) return score - 10;
2102
+ if (memory >= 8) return score + 16;
2103
+ if (memory >= 6) return score + 10;
2104
+ return score;
2105
+ }
2106
+ function applyCoreScore(score, cores) {
2107
+ if (typeof cores !== "number" || !Number.isFinite(cores) || cores <= 0) {
2108
+ return score;
2109
+ }
2110
+ if (cores <= 2) return score - 12;
2111
+ if (cores <= 4) return score;
2112
+ if (cores >= 8) return score + 12;
2113
+ return score + 8;
2114
+ }
2115
+ function applyWebGlScore(score, info) {
2116
+ let next = score;
2117
+ if (info.webglVersion === 2) {
2118
+ next += 16;
2119
+ } else if (info.webglVersion === 1) {
2120
+ next += 6;
2121
+ } else {
2122
+ next -= 25;
2123
+ }
2124
+ if (typeof info.maxTextureSize === "number") {
2125
+ if (info.maxTextureSize >= 8192) next += 10;
2126
+ else if (info.maxTextureSize >= 4096) next += 3;
2127
+ else next -= 8;
2128
+ }
2129
+ const renderer = info.renderer?.toLowerCase() ?? "";
2130
+ if (/swiftshader|llvmpipe|software/.test(renderer)) {
2131
+ next = Math.min(next, 35);
2132
+ } else if (/\bm[1-9]\b|apple gpu|a1[6-9]|rtx|radeon|adreno 7|adreno 8/.test(renderer)) {
2133
+ next += 8;
2134
+ }
2135
+ return next;
2136
+ }
2137
+ function applyMobileCostScore(score, bridge) {
2138
+ if (!isCoarsePointer(bridge)) {
2139
+ return score;
2140
+ }
2141
+ const dpr = getDevicePixelRatio2(bridge);
2142
+ const width = bridge.screen?.width ?? bridge.innerWidth ?? 0;
2143
+ const height = bridge.screen?.height ?? bridge.innerHeight ?? 0;
2144
+ const physicalPixels = width * height * dpr * dpr;
2145
+ if (physicalPixels > 4e6) {
2146
+ return score - 5;
2147
+ }
2148
+ return score;
2149
+ }
2150
+ function applyAppleMobileRules(score, bridge) {
2151
+ if (!isAppleMobileDevice(bridge)) {
2152
+ return score;
2153
+ }
2154
+ const userAgent = bridge.navigator?.userAgent ?? "";
2155
+ const iosMajorVersion = parseIosMajorVersion(userAgent);
2156
+ const highDensityDisplay = getDevicePixelRatio2(bridge) >= 3;
2157
+ const longestScreenEdge = getLongestScreenEdge(bridge);
2158
+ if (typeof iosMajorVersion === "number" && iosMajorVersion < 15) {
2159
+ return Math.min(score, 35);
2160
+ }
2161
+ if (highDensityDisplay && longestScreenEdge >= 430 && (typeof iosMajorVersion === "undefined" || iosMajorVersion >= 17)) {
2162
+ return Math.max(score, 78);
2163
+ }
2164
+ if (highDensityDisplay && longestScreenEdge >= 414 && (typeof iosMajorVersion === "undefined" || iosMajorVersion >= 16)) {
2165
+ return Math.max(score, 62);
2166
+ }
2167
+ return Math.min(score, 48);
2168
+ }
2169
+ function estimateGraphicsPerformance(bridge) {
2170
+ const navigatorValue = getNavigatorValue(bridge);
2171
+ let score = 45;
2172
+ score = applyMemoryScore(score, navigatorValue?.deviceMemory);
2173
+ score = applyCoreScore(score, navigatorValue?.hardwareConcurrency);
2174
+ score = applyWebGlScore(score, getWebGlInfo(bridge));
2175
+ score = applyMobileCostScore(score, bridge);
2176
+ score = applyAppleMobileRules(score, bridge);
2177
+ const normalizedScore = clampScore(score);
2178
+ return {
2179
+ fps: fpsFromScore(normalizedScore),
2180
+ tier: tierFromScore(normalizedScore)
2181
+ };
2182
+ }
2183
+ function getGraphicsPerformance() {
2184
+ const bridge = getBridgeWindow10();
2185
+ if (!bridge) {
2186
+ return { ...DEFAULT_GRAPHICS_PERFORMANCE };
2187
+ }
2188
+ const hostMetric = readHostGraphicsPerformance(bridge);
2189
+ if (hostMetric) {
2190
+ return hostMetric;
2191
+ }
2192
+ cachedEstimatedGraphicsPerformance ??= estimateGraphicsPerformance(bridge);
2193
+ return { ...cachedEstimatedGraphicsPerformance };
2194
+ }
2195
+
1714
2196
  // src/index.ts
1715
2197
  var oasiz = {
1716
2198
  submitScore,
@@ -1730,6 +2212,8 @@ var oasiz = {
1730
2212
  getSafeAreaTop,
1731
2213
  getViewportInsets,
1732
2214
  setLeaderboardVisible,
2215
+ getGraphicsPerformance,
2216
+ enableBackButtonTesting,
1733
2217
  onBackButton,
1734
2218
  onLeaveGame,
1735
2219
  leaveGame,
@@ -1753,14 +2237,19 @@ var oasiz = {
1753
2237
  },
1754
2238
  get viewportInsets() {
1755
2239
  return getViewportInsets();
2240
+ },
2241
+ get graphicsPerformance() {
2242
+ return getGraphicsPerformance();
1756
2243
  }
1757
2244
  };
1758
2245
  // Annotate the CommonJS export names for ESM import in node:
1759
2246
  0 && (module.exports = {
1760
2247
  addScore,
2248
+ enableBackButtonTesting,
1761
2249
  enableLogOverlay,
1762
2250
  flushGameState,
1763
2251
  getGameId,
2252
+ getGraphicsPerformance,
1764
2253
  getPlayerAvatar,
1765
2254
  getPlayerCharacter,
1766
2255
  getPlayerId,
package/dist/index.d.cts CHANGED
@@ -1,3 +1,21 @@
1
+ type GraphicsPerformanceTier = "minimal" | "low" | "medium" | "high";
2
+ interface GraphicsPerformanceMetric {
3
+ /** Recommended graphics/rendering frame-rate target for the current device. */
4
+ fps: number;
5
+ /** Suggested graphics/rendering tier for the current device. */
6
+ tier: GraphicsPerformanceTier;
7
+ }
8
+ /**
9
+ * Return the current device's recommended graphics performance profile.
10
+ *
11
+ * Hosts can provide an FPS/tier recommendation through
12
+ * `window.getGraphicsPerformance()` or `window.__OASIZ_GRAPHICS_PERFORMANCE__`.
13
+ * When no host value exists, the SDK estimates from browser/device/WebGL
14
+ * capability signals so games still get a usable recommendation in local
15
+ * development and unsupported hosts.
16
+ */
17
+ declare function getGraphicsPerformance(): GraphicsPerformanceMetric;
18
+
1
19
  type ViewportInsetSide = "top" | "right" | "bottom" | "left";
2
20
  interface ViewportInsetEdges {
3
21
  top: number;
@@ -221,6 +239,28 @@ type Unsubscribe = () => void;
221
239
  declare function onPause(callback: () => void): Unsubscribe;
222
240
  declare function onResume(callback: () => void): Unsubscribe;
223
241
 
242
+ interface BackButtonTestingOptions {
243
+ /**
244
+ * When true, pressing Escape dispatches the same event the host app sends for
245
+ * back actions. Defaults to true.
246
+ */
247
+ keyboard?: boolean;
248
+ /**
249
+ * When true, the SDK traps one browser-history entry while back override is
250
+ * active, so the browser Back button can be used for local testing. Defaults
251
+ * to true.
252
+ */
253
+ browserHistory?: boolean;
254
+ /** Log setup details to the console. Defaults to false. */
255
+ log?: boolean;
256
+ }
257
+ interface BackButtonTestingHandle {
258
+ destroy: () => void;
259
+ isBackOverrideActive: () => boolean;
260
+ triggerBack: () => void;
261
+ triggerLeave: () => void;
262
+ }
263
+ declare function enableBackButtonTesting(options?: BackButtonTestingOptions): BackButtonTestingHandle;
224
264
  declare function onBackButton(callback: () => void): Unsubscribe;
225
265
  declare function onLeaveGame(callback: () => void): Unsubscribe;
226
266
  declare function leaveGame(): void;
@@ -243,6 +283,8 @@ declare const oasiz: {
243
283
  getSafeAreaTop: typeof getSafeAreaTop;
244
284
  getViewportInsets: typeof getViewportInsets;
245
285
  setLeaderboardVisible: typeof setLeaderboardVisible;
286
+ getGraphicsPerformance: typeof getGraphicsPerformance;
287
+ enableBackButtonTesting: typeof enableBackButtonTesting;
246
288
  onBackButton: typeof onBackButton;
247
289
  onLeaveGame: typeof onLeaveGame;
248
290
  leaveGame: typeof leaveGame;
@@ -253,6 +295,7 @@ declare const oasiz: {
253
295
  readonly playerAvatar: string | undefined;
254
296
  readonly safeAreaTop: number;
255
297
  readonly viewportInsets: ViewportInsets;
298
+ readonly graphicsPerformance: GraphicsPerformanceMetric;
256
299
  };
257
300
 
258
- export { type FacingFrameMap, type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type PlayerCharacter, type ScoreEditResult, type ShareRequest, type ShareRoomCodeOptions, type TextureAtlas, type TextureAtlasAnimation, type TextureAtlasFrame, type Unsubscribe, type ViewportInsetEdges, type ViewportInsetSide, type ViewportInsets, addScore, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerCharacter, getPlayerId, getPlayerName, getRoomCode, getSafeAreaTop, getViewportInsets, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, setScore, share, shareRoomCode, submitScore, triggerHaptic };
301
+ export { type BackButtonTestingHandle, type BackButtonTestingOptions, type FacingFrameMap, type GameState, type GraphicsPerformanceMetric, type GraphicsPerformanceTier, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type PlayerCharacter, type ScoreEditResult, type ShareRequest, type ShareRoomCodeOptions, type TextureAtlas, type TextureAtlasAnimation, type TextureAtlasFrame, type Unsubscribe, type ViewportInsetEdges, type ViewportInsetSide, type ViewportInsets, addScore, enableBackButtonTesting, enableLogOverlay, flushGameState, getGameId, getGraphicsPerformance, getPlayerAvatar, getPlayerCharacter, getPlayerId, getPlayerName, getRoomCode, getSafeAreaTop, getViewportInsets, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, setScore, share, shareRoomCode, submitScore, triggerHaptic };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,21 @@
1
+ type GraphicsPerformanceTier = "minimal" | "low" | "medium" | "high";
2
+ interface GraphicsPerformanceMetric {
3
+ /** Recommended graphics/rendering frame-rate target for the current device. */
4
+ fps: number;
5
+ /** Suggested graphics/rendering tier for the current device. */
6
+ tier: GraphicsPerformanceTier;
7
+ }
8
+ /**
9
+ * Return the current device's recommended graphics performance profile.
10
+ *
11
+ * Hosts can provide an FPS/tier recommendation through
12
+ * `window.getGraphicsPerformance()` or `window.__OASIZ_GRAPHICS_PERFORMANCE__`.
13
+ * When no host value exists, the SDK estimates from browser/device/WebGL
14
+ * capability signals so games still get a usable recommendation in local
15
+ * development and unsupported hosts.
16
+ */
17
+ declare function getGraphicsPerformance(): GraphicsPerformanceMetric;
18
+
1
19
  type ViewportInsetSide = "top" | "right" | "bottom" | "left";
2
20
  interface ViewportInsetEdges {
3
21
  top: number;
@@ -221,6 +239,28 @@ type Unsubscribe = () => void;
221
239
  declare function onPause(callback: () => void): Unsubscribe;
222
240
  declare function onResume(callback: () => void): Unsubscribe;
223
241
 
242
+ interface BackButtonTestingOptions {
243
+ /**
244
+ * When true, pressing Escape dispatches the same event the host app sends for
245
+ * back actions. Defaults to true.
246
+ */
247
+ keyboard?: boolean;
248
+ /**
249
+ * When true, the SDK traps one browser-history entry while back override is
250
+ * active, so the browser Back button can be used for local testing. Defaults
251
+ * to true.
252
+ */
253
+ browserHistory?: boolean;
254
+ /** Log setup details to the console. Defaults to false. */
255
+ log?: boolean;
256
+ }
257
+ interface BackButtonTestingHandle {
258
+ destroy: () => void;
259
+ isBackOverrideActive: () => boolean;
260
+ triggerBack: () => void;
261
+ triggerLeave: () => void;
262
+ }
263
+ declare function enableBackButtonTesting(options?: BackButtonTestingOptions): BackButtonTestingHandle;
224
264
  declare function onBackButton(callback: () => void): Unsubscribe;
225
265
  declare function onLeaveGame(callback: () => void): Unsubscribe;
226
266
  declare function leaveGame(): void;
@@ -243,6 +283,8 @@ declare const oasiz: {
243
283
  getSafeAreaTop: typeof getSafeAreaTop;
244
284
  getViewportInsets: typeof getViewportInsets;
245
285
  setLeaderboardVisible: typeof setLeaderboardVisible;
286
+ getGraphicsPerformance: typeof getGraphicsPerformance;
287
+ enableBackButtonTesting: typeof enableBackButtonTesting;
246
288
  onBackButton: typeof onBackButton;
247
289
  onLeaveGame: typeof onLeaveGame;
248
290
  leaveGame: typeof leaveGame;
@@ -253,6 +295,7 @@ declare const oasiz: {
253
295
  readonly playerAvatar: string | undefined;
254
296
  readonly safeAreaTop: number;
255
297
  readonly viewportInsets: ViewportInsets;
298
+ readonly graphicsPerformance: GraphicsPerformanceMetric;
256
299
  };
257
300
 
258
- export { type FacingFrameMap, type GameState, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type PlayerCharacter, type ScoreEditResult, type ShareRequest, type ShareRoomCodeOptions, type TextureAtlas, type TextureAtlasAnimation, type TextureAtlasFrame, type Unsubscribe, type ViewportInsetEdges, type ViewportInsetSide, type ViewportInsets, addScore, enableLogOverlay, flushGameState, getGameId, getPlayerAvatar, getPlayerCharacter, getPlayerId, getPlayerName, getRoomCode, getSafeAreaTop, getViewportInsets, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, setScore, share, shareRoomCode, submitScore, triggerHaptic };
301
+ export { type BackButtonTestingHandle, type BackButtonTestingOptions, type FacingFrameMap, type GameState, type GraphicsPerformanceMetric, type GraphicsPerformanceTier, type HapticType, type LogOverlayEntry, type LogOverlayHandle, type LogOverlayLevel, type LogOverlayOptions, type PlayerCharacter, type ScoreEditResult, type ShareRequest, type ShareRoomCodeOptions, type TextureAtlas, type TextureAtlasAnimation, type TextureAtlasFrame, type Unsubscribe, type ViewportInsetEdges, type ViewportInsetSide, type ViewportInsets, addScore, enableBackButtonTesting, enableLogOverlay, flushGameState, getGameId, getGraphicsPerformance, getPlayerAvatar, getPlayerCharacter, getPlayerId, getPlayerName, getRoomCode, getSafeAreaTop, getViewportInsets, leaveGame, loadGameState, oasiz, onBackButton, onLeaveGame, onPause, onResume, openInviteModal, saveGameState, setLeaderboardVisible, setScore, share, shareRoomCode, submitScore, triggerHaptic };
package/dist/index.js CHANGED
@@ -1577,7 +1577,9 @@ function setLeaderboardVisible(visible) {
1577
1577
  }
1578
1578
 
1579
1579
  // src/navigation.ts
1580
+ var BACK_BUTTON_TEST_STATE_KEY = "__oasizBackButtonTest";
1580
1581
  var activeBackListeners = 0;
1582
+ var activeBackButtonTestingHandle;
1581
1583
  function isDevelopment10() {
1582
1584
  const nodeEnv = globalThis.process?.env?.NODE_ENV;
1583
1585
  return nodeEnv !== "production";
@@ -1603,6 +1605,16 @@ function normalizeNavigationError(error) {
1603
1605
  typeof error === "string" ? error : "Back button callback failed."
1604
1606
  );
1605
1607
  }
1608
+ function isRecord2(value) {
1609
+ return typeof value === "object" && value !== null;
1610
+ }
1611
+ function dispatchNavigationEvent(eventName) {
1612
+ const bridge = getBridgeWindow9();
1613
+ if (!bridge || typeof bridge.dispatchEvent !== "function") {
1614
+ return;
1615
+ }
1616
+ bridge.dispatchEvent(new Event(eventName));
1617
+ }
1606
1618
  function addNavigationListener(eventName, callback) {
1607
1619
  if (typeof window === "undefined") {
1608
1620
  if (isDevelopment10()) {
@@ -1617,6 +1629,151 @@ function addNavigationListener(eventName, callback) {
1617
1629
  window.addEventListener(eventName, handler);
1618
1630
  return () => window.removeEventListener(eventName, handler);
1619
1631
  }
1632
+ function enableBackButtonTesting(options = {}) {
1633
+ activeBackButtonTestingHandle?.destroy();
1634
+ const bridge = getBridgeWindow9();
1635
+ if (!bridge) {
1636
+ if (isDevelopment10()) {
1637
+ console.warn(
1638
+ "[oasiz/sdk] enableBackButtonTesting requires a browser window."
1639
+ );
1640
+ }
1641
+ return {
1642
+ destroy: () => {
1643
+ },
1644
+ isBackOverrideActive: () => false,
1645
+ triggerBack: () => {
1646
+ },
1647
+ triggerLeave: () => {
1648
+ }
1649
+ };
1650
+ }
1651
+ const bridgeWindow = bridge;
1652
+ const keyboard = options.keyboard ?? true;
1653
+ const browserHistory = options.browserHistory ?? true;
1654
+ const log = options.log === true;
1655
+ const previousSetBackOverride = bridgeWindow.__oasizSetBackOverride;
1656
+ const previousLeaveGame = bridgeWindow.__oasizLeaveGame;
1657
+ let destroyed = false;
1658
+ let backOverrideActive = false;
1659
+ let historyTrapArmed = false;
1660
+ function maybeLog(message) {
1661
+ if (log) {
1662
+ console.info("[oasiz/sdk] " + message);
1663
+ }
1664
+ }
1665
+ function canUseHistoryTrap() {
1666
+ return browserHistory && typeof bridgeWindow.history?.pushState === "function" && typeof bridgeWindow.history?.replaceState === "function" && typeof bridgeWindow.location?.href === "string";
1667
+ }
1668
+ function ensureHistoryTrap() {
1669
+ if (!backOverrideActive || historyTrapArmed || !canUseHistoryTrap()) {
1670
+ return;
1671
+ }
1672
+ try {
1673
+ const currentState = isRecord2(bridgeWindow.history.state) ? bridgeWindow.history.state : {};
1674
+ bridgeWindow.history.replaceState(
1675
+ { ...currentState, [BACK_BUTTON_TEST_STATE_KEY]: "base" },
1676
+ "",
1677
+ bridgeWindow.location.href
1678
+ );
1679
+ bridgeWindow.history.pushState(
1680
+ { [BACK_BUTTON_TEST_STATE_KEY]: "trap" },
1681
+ "",
1682
+ bridgeWindow.location.href
1683
+ );
1684
+ historyTrapArmed = true;
1685
+ maybeLog("Local browser Back testing is armed.");
1686
+ } catch (error) {
1687
+ historyTrapArmed = false;
1688
+ if (log) {
1689
+ console.warn("[oasiz/sdk] Failed to arm browser Back testing:", error);
1690
+ }
1691
+ }
1692
+ }
1693
+ function triggerBack() {
1694
+ dispatchNavigationEvent("oasiz:back");
1695
+ }
1696
+ function triggerLeave() {
1697
+ dispatchNavigationEvent("oasiz:leave");
1698
+ }
1699
+ function setBackOverride(active) {
1700
+ backOverrideActive = active;
1701
+ if (active) {
1702
+ ensureHistoryTrap();
1703
+ }
1704
+ if (typeof previousSetBackOverride === "function") {
1705
+ previousSetBackOverride(active);
1706
+ }
1707
+ maybeLog("Back override " + (active ? "enabled" : "disabled") + ".");
1708
+ }
1709
+ function stopBackEvent(event) {
1710
+ event.preventDefault();
1711
+ event.stopPropagation();
1712
+ event.stopImmediatePropagation?.();
1713
+ }
1714
+ const handleKeyDown = (event) => {
1715
+ if (!backOverrideActive || event.key !== "Escape") {
1716
+ return;
1717
+ }
1718
+ stopBackEvent(event);
1719
+ triggerBack();
1720
+ };
1721
+ const handlePopState = (event) => {
1722
+ if (!backOverrideActive) {
1723
+ historyTrapArmed = false;
1724
+ return;
1725
+ }
1726
+ stopBackEvent(event);
1727
+ triggerBack();
1728
+ historyTrapArmed = false;
1729
+ ensureHistoryTrap();
1730
+ };
1731
+ const testLeaveGame = () => {
1732
+ triggerLeave();
1733
+ if (typeof previousLeaveGame === "function") {
1734
+ previousLeaveGame();
1735
+ }
1736
+ };
1737
+ bridgeWindow.__oasizSetBackOverride = setBackOverride;
1738
+ bridgeWindow.__oasizLeaveGame = testLeaveGame;
1739
+ if (keyboard) {
1740
+ bridgeWindow.addEventListener("keydown", handleKeyDown);
1741
+ }
1742
+ if (browserHistory) {
1743
+ bridgeWindow.addEventListener("popstate", handlePopState);
1744
+ }
1745
+ if (activeBackListeners > 0) {
1746
+ setBackOverride(true);
1747
+ }
1748
+ maybeLog("Back button testing bridge installed.");
1749
+ const handle = {
1750
+ destroy: () => {
1751
+ if (destroyed) return;
1752
+ destroyed = true;
1753
+ if (keyboard) {
1754
+ bridgeWindow.removeEventListener("keydown", handleKeyDown);
1755
+ }
1756
+ if (browserHistory) {
1757
+ bridgeWindow.removeEventListener("popstate", handlePopState);
1758
+ }
1759
+ if (bridgeWindow.__oasizSetBackOverride === setBackOverride) {
1760
+ bridgeWindow.__oasizSetBackOverride = previousSetBackOverride;
1761
+ }
1762
+ if (bridgeWindow.__oasizLeaveGame === testLeaveGame) {
1763
+ bridgeWindow.__oasizLeaveGame = previousLeaveGame;
1764
+ }
1765
+ if (activeBackButtonTestingHandle === handle) {
1766
+ activeBackButtonTestingHandle = void 0;
1767
+ }
1768
+ maybeLog("Back button testing bridge removed.");
1769
+ },
1770
+ isBackOverrideActive: () => backOverrideActive,
1771
+ triggerBack,
1772
+ triggerLeave
1773
+ };
1774
+ activeBackButtonTestingHandle = handle;
1775
+ return handle;
1776
+ }
1620
1777
  function onBackButton(callback) {
1621
1778
  const off = addNavigationListener("oasiz:back", () => {
1622
1779
  try {
@@ -1660,6 +1817,329 @@ function leaveGame() {
1660
1817
  warnMissingBridge7("__oasizLeaveGame");
1661
1818
  }
1662
1819
 
1820
+ // src/performance.ts
1821
+ var DEFAULT_GRAPHICS_PERFORMANCE = {
1822
+ fps: 45,
1823
+ tier: "medium"
1824
+ };
1825
+ var TIER_DEFAULT_FPS = {
1826
+ minimal: 24,
1827
+ low: 30,
1828
+ medium: 45,
1829
+ high: 60
1830
+ };
1831
+ var cachedEstimatedGraphicsPerformance;
1832
+ function getBridgeWindow10() {
1833
+ if (typeof window === "undefined") {
1834
+ return void 0;
1835
+ }
1836
+ return window;
1837
+ }
1838
+ function isRecord3(value) {
1839
+ return typeof value === "object" && value !== null;
1840
+ }
1841
+ function toFiniteNumber2(value) {
1842
+ if (typeof value === "number" && Number.isFinite(value)) {
1843
+ return value;
1844
+ }
1845
+ if (typeof value === "string") {
1846
+ const parsed = Number.parseFloat(value.trim());
1847
+ if (Number.isFinite(parsed)) {
1848
+ return parsed;
1849
+ }
1850
+ }
1851
+ return void 0;
1852
+ }
1853
+ function clampScore(value) {
1854
+ return Math.min(100, Math.max(0, Math.round(value)));
1855
+ }
1856
+ function clampFps(value) {
1857
+ return Math.min(240, Math.max(1, Math.round(value)));
1858
+ }
1859
+ function tierFromScore(score) {
1860
+ if (score < 25) return "minimal";
1861
+ if (score < 40) return "low";
1862
+ if (score < 70) return "medium";
1863
+ return "high";
1864
+ }
1865
+ function tierFromFps(fps) {
1866
+ if (fps < 30) return "minimal";
1867
+ if (fps < 45) return "low";
1868
+ if (fps < 58) return "medium";
1869
+ return "high";
1870
+ }
1871
+ function fpsFromScore(score) {
1872
+ return TIER_DEFAULT_FPS[tierFromScore(score)];
1873
+ }
1874
+ function normalizeTier(value) {
1875
+ if (typeof value !== "string") {
1876
+ return void 0;
1877
+ }
1878
+ const normalized = value.trim().toLowerCase();
1879
+ if (normalized === "minimal" || normalized === "safe") return "minimal";
1880
+ if (normalized === "high") return "high";
1881
+ if (normalized === "medium") return "medium";
1882
+ if (normalized === "low") return "low";
1883
+ return void 0;
1884
+ }
1885
+ function firstDefined2(...values) {
1886
+ return values.find((value) => typeof value !== "undefined");
1887
+ }
1888
+ function normalizeMetric(value) {
1889
+ if (typeof value === "undefined" || value === null) {
1890
+ return void 0;
1891
+ }
1892
+ if (typeof value === "number" || typeof value === "string") {
1893
+ const numeric = toFiniteNumber2(value);
1894
+ if (typeof numeric !== "undefined") {
1895
+ const fps2 = clampFps(numeric);
1896
+ return { fps: fps2, tier: tierFromFps(fps2) };
1897
+ }
1898
+ const tier2 = normalizeTier(value);
1899
+ if (tier2) {
1900
+ return { fps: TIER_DEFAULT_FPS[tier2], tier: tier2 };
1901
+ }
1902
+ return void 0;
1903
+ }
1904
+ if (!isRecord3(value)) {
1905
+ return void 0;
1906
+ }
1907
+ const fpsCandidate = firstDefined2(
1908
+ value.fps,
1909
+ value.targetFps,
1910
+ value.frameRate,
1911
+ value.framesPerSecond
1912
+ );
1913
+ const legacyScoreCandidate = firstDefined2(
1914
+ value.score,
1915
+ value.metric,
1916
+ value.value,
1917
+ value.performance,
1918
+ value.performanceScore,
1919
+ value.graphicsScore
1920
+ );
1921
+ const tierCandidate = firstDefined2(
1922
+ value.tier,
1923
+ value.graphicsTier,
1924
+ value.performanceTier,
1925
+ value.qualityTier,
1926
+ value.recommendedTier
1927
+ );
1928
+ const tier = normalizeTier(tierCandidate);
1929
+ const fpsNumeric = toFiniteNumber2(fpsCandidate);
1930
+ const scoreNumeric = toFiniteNumber2(legacyScoreCandidate);
1931
+ if (typeof fpsNumeric === "undefined" && typeof scoreNumeric === "undefined" && !tier) {
1932
+ return void 0;
1933
+ }
1934
+ const fps = clampFps(
1935
+ typeof fpsNumeric !== "undefined" ? fpsNumeric : typeof scoreNumeric !== "undefined" ? fpsFromScore(clampScore(scoreNumeric)) : TIER_DEFAULT_FPS[tier]
1936
+ );
1937
+ return {
1938
+ fps,
1939
+ tier: tier ?? tierFromFps(fps)
1940
+ };
1941
+ }
1942
+ function callBridgeMetric(bridge, name) {
1943
+ const fn = bridge[name];
1944
+ if (typeof fn !== "function") {
1945
+ return void 0;
1946
+ }
1947
+ try {
1948
+ return fn.call(bridge);
1949
+ } catch (error) {
1950
+ console.error("[oasiz/sdk] " + String(name) + " failed:", error);
1951
+ return void 0;
1952
+ }
1953
+ }
1954
+ function readHostGraphicsPerformance(bridge) {
1955
+ const candidates = [
1956
+ callBridgeMetric(bridge, "getGraphicsPerformance"),
1957
+ callBridgeMetric(bridge, "getGraphicsPerformanceMetric"),
1958
+ callBridgeMetric(bridge, "getPerformanceMetric"),
1959
+ bridge.__OASIZ_GRAPHICS_PERFORMANCE__,
1960
+ bridge.__OASIZ_PERFORMANCE_METRIC__
1961
+ ];
1962
+ for (const candidate of candidates) {
1963
+ const metric = normalizeMetric(candidate);
1964
+ if (metric) {
1965
+ return metric;
1966
+ }
1967
+ }
1968
+ return void 0;
1969
+ }
1970
+ function getDevicePixelRatio2(bridge) {
1971
+ const dpr = bridge.devicePixelRatio;
1972
+ return typeof dpr === "number" && Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
1973
+ }
1974
+ function getNavigatorValue(bridge) {
1975
+ return bridge.navigator;
1976
+ }
1977
+ function parseIosMajorVersion(userAgent) {
1978
+ const match = /\bOS (\d+)_/i.exec(userAgent);
1979
+ if (!match) {
1980
+ return void 0;
1981
+ }
1982
+ const parsed = Number.parseInt(match[1] ?? "", 10);
1983
+ return Number.isFinite(parsed) ? parsed : void 0;
1984
+ }
1985
+ function isAppleMobileDevice(bridge) {
1986
+ const userAgent = bridge.navigator?.userAgent ?? "";
1987
+ return /iP(hone|ad|od)/i.test(userAgent) || /Macintosh/i.test(userAgent) && (bridge.navigator?.maxTouchPoints ?? 0) > 1;
1988
+ }
1989
+ function getLongestScreenEdge(bridge) {
1990
+ const screenWidth = bridge.screen?.width;
1991
+ const screenHeight = bridge.screen?.height;
1992
+ const innerWidth = bridge.innerWidth;
1993
+ const innerHeight = bridge.innerHeight;
1994
+ return Math.max(
1995
+ typeof screenWidth === "number" ? screenWidth : 0,
1996
+ typeof screenHeight === "number" ? screenHeight : 0,
1997
+ typeof innerWidth === "number" ? innerWidth : 0,
1998
+ typeof innerHeight === "number" ? innerHeight : 0
1999
+ );
2000
+ }
2001
+ function isCoarsePointer(bridge) {
2002
+ try {
2003
+ return bridge.matchMedia?.("(pointer: coarse)").matches === true;
2004
+ } catch {
2005
+ return false;
2006
+ }
2007
+ }
2008
+ function getWebGlInfo(bridge) {
2009
+ const canvas = bridge.document?.createElement?.("canvas");
2010
+ if (!canvas || typeof canvas.getContext !== "function") {
2011
+ return { webglVersion: 0 };
2012
+ }
2013
+ const gl2 = canvas.getContext("webgl2");
2014
+ const gl = gl2 ?? canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
2015
+ if (!gl) {
2016
+ return { webglVersion: 0 };
2017
+ }
2018
+ const context = gl;
2019
+ let maxTextureSize;
2020
+ let renderer;
2021
+ try {
2022
+ const value = context.getParameter(context.MAX_TEXTURE_SIZE);
2023
+ if (typeof value === "number" && Number.isFinite(value)) {
2024
+ maxTextureSize = value;
2025
+ }
2026
+ } catch {
2027
+ }
2028
+ try {
2029
+ const debugInfo = context.getExtension("WEBGL_debug_renderer_info");
2030
+ const rendererParam = debugInfo?.UNMASKED_RENDERER_WEBGL ?? context.RENDERER;
2031
+ const value = context.getParameter(rendererParam);
2032
+ if (typeof value === "string") {
2033
+ renderer = value;
2034
+ }
2035
+ } catch {
2036
+ }
2037
+ return {
2038
+ maxTextureSize,
2039
+ renderer,
2040
+ webglVersion: gl2 ? 2 : 1
2041
+ };
2042
+ }
2043
+ function applyMemoryScore(score, memory) {
2044
+ if (typeof memory !== "number" || !Number.isFinite(memory) || memory <= 0) {
2045
+ return score;
2046
+ }
2047
+ if (memory <= 1) return score - 18;
2048
+ if (memory <= 2) return score - 10;
2049
+ if (memory >= 8) return score + 16;
2050
+ if (memory >= 6) return score + 10;
2051
+ return score;
2052
+ }
2053
+ function applyCoreScore(score, cores) {
2054
+ if (typeof cores !== "number" || !Number.isFinite(cores) || cores <= 0) {
2055
+ return score;
2056
+ }
2057
+ if (cores <= 2) return score - 12;
2058
+ if (cores <= 4) return score;
2059
+ if (cores >= 8) return score + 12;
2060
+ return score + 8;
2061
+ }
2062
+ function applyWebGlScore(score, info) {
2063
+ let next = score;
2064
+ if (info.webglVersion === 2) {
2065
+ next += 16;
2066
+ } else if (info.webglVersion === 1) {
2067
+ next += 6;
2068
+ } else {
2069
+ next -= 25;
2070
+ }
2071
+ if (typeof info.maxTextureSize === "number") {
2072
+ if (info.maxTextureSize >= 8192) next += 10;
2073
+ else if (info.maxTextureSize >= 4096) next += 3;
2074
+ else next -= 8;
2075
+ }
2076
+ const renderer = info.renderer?.toLowerCase() ?? "";
2077
+ if (/swiftshader|llvmpipe|software/.test(renderer)) {
2078
+ next = Math.min(next, 35);
2079
+ } else if (/\bm[1-9]\b|apple gpu|a1[6-9]|rtx|radeon|adreno 7|adreno 8/.test(renderer)) {
2080
+ next += 8;
2081
+ }
2082
+ return next;
2083
+ }
2084
+ function applyMobileCostScore(score, bridge) {
2085
+ if (!isCoarsePointer(bridge)) {
2086
+ return score;
2087
+ }
2088
+ const dpr = getDevicePixelRatio2(bridge);
2089
+ const width = bridge.screen?.width ?? bridge.innerWidth ?? 0;
2090
+ const height = bridge.screen?.height ?? bridge.innerHeight ?? 0;
2091
+ const physicalPixels = width * height * dpr * dpr;
2092
+ if (physicalPixels > 4e6) {
2093
+ return score - 5;
2094
+ }
2095
+ return score;
2096
+ }
2097
+ function applyAppleMobileRules(score, bridge) {
2098
+ if (!isAppleMobileDevice(bridge)) {
2099
+ return score;
2100
+ }
2101
+ const userAgent = bridge.navigator?.userAgent ?? "";
2102
+ const iosMajorVersion = parseIosMajorVersion(userAgent);
2103
+ const highDensityDisplay = getDevicePixelRatio2(bridge) >= 3;
2104
+ const longestScreenEdge = getLongestScreenEdge(bridge);
2105
+ if (typeof iosMajorVersion === "number" && iosMajorVersion < 15) {
2106
+ return Math.min(score, 35);
2107
+ }
2108
+ if (highDensityDisplay && longestScreenEdge >= 430 && (typeof iosMajorVersion === "undefined" || iosMajorVersion >= 17)) {
2109
+ return Math.max(score, 78);
2110
+ }
2111
+ if (highDensityDisplay && longestScreenEdge >= 414 && (typeof iosMajorVersion === "undefined" || iosMajorVersion >= 16)) {
2112
+ return Math.max(score, 62);
2113
+ }
2114
+ return Math.min(score, 48);
2115
+ }
2116
+ function estimateGraphicsPerformance(bridge) {
2117
+ const navigatorValue = getNavigatorValue(bridge);
2118
+ let score = 45;
2119
+ score = applyMemoryScore(score, navigatorValue?.deviceMemory);
2120
+ score = applyCoreScore(score, navigatorValue?.hardwareConcurrency);
2121
+ score = applyWebGlScore(score, getWebGlInfo(bridge));
2122
+ score = applyMobileCostScore(score, bridge);
2123
+ score = applyAppleMobileRules(score, bridge);
2124
+ const normalizedScore = clampScore(score);
2125
+ return {
2126
+ fps: fpsFromScore(normalizedScore),
2127
+ tier: tierFromScore(normalizedScore)
2128
+ };
2129
+ }
2130
+ function getGraphicsPerformance() {
2131
+ const bridge = getBridgeWindow10();
2132
+ if (!bridge) {
2133
+ return { ...DEFAULT_GRAPHICS_PERFORMANCE };
2134
+ }
2135
+ const hostMetric = readHostGraphicsPerformance(bridge);
2136
+ if (hostMetric) {
2137
+ return hostMetric;
2138
+ }
2139
+ cachedEstimatedGraphicsPerformance ??= estimateGraphicsPerformance(bridge);
2140
+ return { ...cachedEstimatedGraphicsPerformance };
2141
+ }
2142
+
1663
2143
  // src/index.ts
1664
2144
  var oasiz = {
1665
2145
  submitScore,
@@ -1679,6 +2159,8 @@ var oasiz = {
1679
2159
  getSafeAreaTop,
1680
2160
  getViewportInsets,
1681
2161
  setLeaderboardVisible,
2162
+ getGraphicsPerformance,
2163
+ enableBackButtonTesting,
1682
2164
  onBackButton,
1683
2165
  onLeaveGame,
1684
2166
  leaveGame,
@@ -1702,13 +2184,18 @@ var oasiz = {
1702
2184
  },
1703
2185
  get viewportInsets() {
1704
2186
  return getViewportInsets();
2187
+ },
2188
+ get graphicsPerformance() {
2189
+ return getGraphicsPerformance();
1705
2190
  }
1706
2191
  };
1707
2192
  export {
1708
2193
  addScore,
2194
+ enableBackButtonTesting,
1709
2195
  enableLogOverlay,
1710
2196
  flushGameState,
1711
2197
  getGameId,
2198
+ getGraphicsPerformance,
1712
2199
  getPlayerAvatar,
1713
2200
  getPlayerCharacter,
1714
2201
  getPlayerId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oasiz/sdk",
3
- "version": "1.5.6",
3
+ "version": "1.6.1",
4
4
  "description": "Typed SDK for Oasiz game platform bridge APIs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",