@wvdsh/sdk-js 1.3.19 → 1.3.21

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/dist/index.d.ts CHANGED
@@ -43,17 +43,24 @@ declare abstract class WavedashManager {
43
43
  * e.g. a PIXI/GDevelop intro video), force-muting it before playback begins
44
44
  * regardless of how it was created — the one path the DOM-based sources and
45
45
  * the `muted` setter all miss.
46
+ *
47
+ * Speech synthesis (`window.speechSynthesis`): bypasses both Web Audio and HTML
48
+ * media entirely, so it gets its own shim — `speak()` forces the utterance's
49
+ * native volume to 0 while muted.
46
50
  */
47
51
  declare class AudioManager extends WavedashManager {
48
52
  private _isMuted;
49
53
  private contexts;
50
54
  private elements;
51
55
  private intendedMuted;
56
+ private intendedUtteranceVolume;
52
57
  private originalAudioContext;
53
58
  private originalWebKitAudioContext;
54
59
  private originalAudio;
55
60
  private originalMutedDescriptor;
56
61
  private originalPlay;
62
+ private originalSpeak;
63
+ private originalUtteranceVolumeDescriptor;
57
64
  private mutationObserver;
58
65
  constructor(sdk: WavedashSDK);
59
66
  isMuted(): boolean;
@@ -78,6 +85,18 @@ declare class AudioManager extends WavedashManager {
78
85
  */
79
86
  private trackElement;
80
87
  private installShims;
88
+ /**
89
+ * Shim `window.speechSynthesis` so speech respects the SDK mute state.
90
+ *
91
+ * Never swallows speak(): utterances have a lifecycle the game may sequence
92
+ * off (onstart/onend, synth.speaking/pending checks), so every call is
93
+ * delegated and silenced via volume instead. Volume is sampled at speak()
94
+ * time, so forcing the native value to 0 right before delegating silences
95
+ * anything spoken while muted; in-flight speech at the mute edge is
96
+ * deliberately left to finish (can't be softened mid-utterance, and
97
+ * cancel() would discard the pending queue).
98
+ */
99
+ private shimSpeechSynthesis;
81
100
  private shimAudioContextClass;
82
101
  destroy(): void;
83
102
  }
@@ -665,16 +684,18 @@ declare class LobbyManager extends WavedashManager {
665
684
  * Owns the iframe ↔ parent interactions for the Wavedash overlay UI:
666
685
  * - Shift+Tab inside the iframe toggles the overlay on the host page
667
686
  * (the host owns the overlay, so we postMessage up).
668
- * - When the parent closes the overlay it sends TAKE_FOCUS so keyboard
669
- * input goes back to the game; we walk the DOM for a focusable target.
670
- * - `takeFocus()` is also called after load completes so the game starts
671
- * with keyboard focus without the player clicking first.
687
+ * - When the parent closes the overlay it sends TAKE_FOCUS, which hands
688
+ * keyboard focus back to the game (see `takeFocus`).
689
+ * - While the overlay is open we suspend pointer lock (the host broadcasts
690
+ * OVERLAY_CHANGED) so a game can't hold/re-grab the cursor behind it.
672
691
  */
673
692
  declare class OverlayManager extends WavedashManager {
693
+ private restorePointerLock;
674
694
  constructor(sdk: WavedashSDK);
695
+ private setOpen;
675
696
  toggleOverlay(): void;
676
- takeFocus(): void;
677
697
  private handleKeyDown;
698
+ destroy(): void;
678
699
  }
679
700
 
680
701
  /**
@@ -795,9 +816,13 @@ declare class P2PManager extends WavedashManager {
795
816
  }
796
817
 
797
818
  declare class PaidContentManager extends WavedashManager {
819
+ private paywallOpen;
820
+ private restorePointerLock;
798
821
  isEntitled(contentId: string): Promise<boolean>;
799
822
  getEntitlements(): Promise<string[]>;
800
823
  triggerPaywall(contentIdentifier: string): Promise<boolean>;
824
+ isPaywallOpen(): boolean;
825
+ destroy(): void;
801
826
  }
802
827
 
803
828
  declare class StatsManager extends WavedashManager {
package/dist/index.js CHANGED
@@ -124,12 +124,16 @@ var AudioManager = class extends WavedashManager {
124
124
  // HTML media elements we know about + their game-intended muted state
125
125
  this.elements = new WeakRefSet();
126
126
  this.intendedMuted = /* @__PURE__ */ new WeakMap();
127
+ // Speech synthesis utterances + their game-intended volume
128
+ this.intendedUtteranceVolume = /* @__PURE__ */ new WeakMap();
127
129
  // Originals (restored on destroy)
128
130
  this.originalAudioContext = null;
129
131
  this.originalWebKitAudioContext = null;
130
132
  this.originalAudio = null;
131
133
  this.originalMutedDescriptor = null;
132
134
  this.originalPlay = null;
135
+ this.originalSpeak = null;
136
+ this.originalUtteranceVolumeDescriptor = null;
133
137
  this.mutationObserver = null;
134
138
  this.handleMute = (data) => {
135
139
  if (this._isMuted === data.isMuted) return;
@@ -275,6 +279,66 @@ var AudioManager = class extends WavedashManager {
275
279
  return originalPlay.call(this);
276
280
  };
277
281
  })(this);
282
+ this.shimSpeechSynthesis();
283
+ }
284
+ /**
285
+ * Shim `window.speechSynthesis` so speech respects the SDK mute state.
286
+ *
287
+ * Never swallows speak(): utterances have a lifecycle the game may sequence
288
+ * off (onstart/onend, synth.speaking/pending checks), so every call is
289
+ * delegated and silenced via volume instead. Volume is sampled at speak()
290
+ * time, so forcing the native value to 0 right before delegating silences
291
+ * anything spoken while muted; in-flight speech at the mute edge is
292
+ * deliberately left to finish (can't be softened mid-utterance, and
293
+ * cancel() would discard the pending queue).
294
+ */
295
+ shimSpeechSynthesis() {
296
+ if (!window.speechSynthesis || typeof SpeechSynthesisUtterance === "undefined") {
297
+ return;
298
+ }
299
+ this.originalUtteranceVolumeDescriptor = Object.getOwnPropertyDescriptor(
300
+ SpeechSynthesisUtterance.prototype,
301
+ "volume"
302
+ ) ?? null;
303
+ const volDesc = this.originalUtteranceVolumeDescriptor;
304
+ if (volDesc?.get && volDesc?.set) {
305
+ ((manager) => {
306
+ Object.defineProperty(SpeechSynthesisUtterance.prototype, "volume", {
307
+ configurable: true,
308
+ get() {
309
+ const intended = manager.intendedUtteranceVolume.get(this);
310
+ return intended !== void 0 ? intended : volDesc.get.call(this);
311
+ },
312
+ set(value) {
313
+ manager.intendedUtteranceVolume.set(this, value);
314
+ volDesc.set.call(this, value);
315
+ }
316
+ });
317
+ })(this);
318
+ }
319
+ const speechSynthesis = window.speechSynthesis;
320
+ const originalSpeak = speechSynthesis.speak;
321
+ this.originalSpeak = originalSpeak;
322
+ ((manager) => {
323
+ speechSynthesis.speak = function(utterance) {
324
+ if (manager._isMuted) {
325
+ if (!manager.intendedUtteranceVolume.has(utterance)) {
326
+ const current = volDesc?.get ? volDesc.get.call(utterance) : utterance.volume;
327
+ manager.intendedUtteranceVolume.set(utterance, current);
328
+ }
329
+ if (volDesc?.set) volDesc.set.call(utterance, 0);
330
+ else utterance.volume = 0;
331
+ } else {
332
+ const intended = manager.intendedUtteranceVolume.get(utterance);
333
+ if (intended !== void 0) {
334
+ if (volDesc?.set) volDesc.set.call(utterance, intended);
335
+ else utterance.volume = intended;
336
+ manager.intendedUtteranceVolume.delete(utterance);
337
+ }
338
+ }
339
+ return originalSpeak.call(speechSynthesis, utterance);
340
+ };
341
+ })(this);
278
342
  }
279
343
  shimAudioContextClass(Original) {
280
344
  return /* @__PURE__ */ ((manager) => class extends Original {
@@ -320,6 +384,16 @@ var AudioManager = class extends WavedashManager {
320
384
  if (this.originalAudio) {
321
385
  window.Audio = this.originalAudio;
322
386
  }
387
+ if (this.originalSpeak && window.speechSynthesis) {
388
+ window.speechSynthesis.speak = this.originalSpeak;
389
+ }
390
+ }
391
+ if (this.originalUtteranceVolumeDescriptor && typeof SpeechSynthesisUtterance !== "undefined") {
392
+ Object.defineProperty(
393
+ SpeechSynthesisUtterance.prototype,
394
+ "volume",
395
+ this.originalUtteranceVolumeDescriptor
396
+ );
323
397
  }
324
398
  if (this.originalPlay) {
325
399
  HTMLMediaElement.prototype.play = this.originalPlay;
@@ -334,6 +408,7 @@ var AudioManager = class extends WavedashManager {
334
408
  this.contexts.clear();
335
409
  this.elements.clear();
336
410
  this.intendedMuted = /* @__PURE__ */ new WeakMap();
411
+ this.intendedUtteranceVolume = /* @__PURE__ */ new WeakMap();
337
412
  super.destroy();
338
413
  }
339
414
  };
@@ -1849,6 +1924,44 @@ var LobbyManager = _LobbyManager;
1849
1924
 
1850
1925
  // src/services/overlay.ts
1851
1926
  import { IFRAME_MESSAGE_TYPE as IFRAME_MESSAGE_TYPE5 } from "@wvdsh/api";
1927
+
1928
+ // src/utils/focus.ts
1929
+ function takeFocus() {
1930
+ if (typeof document === "undefined") return;
1931
+ const gameFocusTargets = document.getElementsByClassName("game-focus-target");
1932
+ if (gameFocusTargets.length > 0) {
1933
+ gameFocusTargets[0].focus();
1934
+ return;
1935
+ }
1936
+ const focusableElement = document.querySelector(
1937
+ "canvas, input, button, [tabindex]:not([tabindex='-1'])"
1938
+ );
1939
+ focusableElement?.focus();
1940
+ }
1941
+
1942
+ // src/utils/pointerLock.ts
1943
+ var hasDom = typeof Element !== "undefined" && typeof document !== "undefined";
1944
+ var nativeRequestPointerLock = hasDom ? Element.prototype.requestPointerLock : void 0;
1945
+ var depth = 0;
1946
+ function suspendPointerLock() {
1947
+ if (!hasDom || !nativeRequestPointerLock) return () => {
1948
+ };
1949
+ if (++depth === 1) {
1950
+ Element.prototype.requestPointerLock = function() {
1951
+ return Promise.resolve();
1952
+ };
1953
+ }
1954
+ document.exitPointerLock();
1955
+ let disposed = false;
1956
+ return () => {
1957
+ if (disposed) return;
1958
+ disposed = true;
1959
+ if (--depth === 0)
1960
+ Element.prototype.requestPointerLock = nativeRequestPointerLock;
1961
+ };
1962
+ }
1963
+
1964
+ // src/services/overlay.ts
1852
1965
  var OverlayManager = class extends WavedashManager {
1853
1966
  constructor(sdk) {
1854
1967
  super(sdk);
@@ -1860,29 +1973,36 @@ var OverlayManager = class extends WavedashManager {
1860
1973
  };
1861
1974
  this.sdk.iframeMessenger.addEventListener(
1862
1975
  IFRAME_MESSAGE_TYPE5.TAKE_FOCUS,
1863
- () => this.takeFocus()
1976
+ takeFocus
1977
+ );
1978
+ this.sdk.iframeMessenger.addEventListener(
1979
+ IFRAME_MESSAGE_TYPE5.OVERLAY_CHANGED,
1980
+ ({ isOpen }) => this.setOpen(isOpen)
1864
1981
  );
1865
1982
  if (typeof window !== "undefined") {
1866
1983
  window.addEventListener("keydown", this.handleKeyDown);
1867
1984
  }
1868
1985
  }
1986
+ setOpen(open) {
1987
+ if (open) {
1988
+ this.restorePointerLock ?? (this.restorePointerLock = suspendPointerLock());
1989
+ } else {
1990
+ this.restorePointerLock?.();
1991
+ this.restorePointerLock = void 0;
1992
+ }
1993
+ }
1869
1994
  toggleOverlay() {
1870
1995
  this.sdk.iframeMessenger.postToParent(
1871
1996
  IFRAME_MESSAGE_TYPE5.TOGGLE_OVERLAY,
1872
1997
  {}
1873
1998
  );
1874
1999
  }
1875
- takeFocus() {
1876
- if (typeof document === "undefined") return;
1877
- const gameFocusTargets = document.getElementsByClassName("game-focus-target");
1878
- if (gameFocusTargets.length > 0) {
1879
- gameFocusTargets[0].focus();
1880
- return;
2000
+ destroy() {
2001
+ this.restorePointerLock?.();
2002
+ this.restorePointerLock = void 0;
2003
+ if (typeof window !== "undefined") {
2004
+ window.removeEventListener("keydown", this.handleKeyDown);
1881
2005
  }
1882
- const focusableElement = document.querySelector(
1883
- "canvas, input, button, [tabindex]:not([tabindex='-1'])"
1884
- );
1885
- focusableElement?.focus();
1886
2006
  }
1887
2007
  };
1888
2008
 
@@ -3204,6 +3324,10 @@ function readEntitlementsFromJwt(jwt) {
3204
3324
  return ents.filter((e) => typeof e === "string");
3205
3325
  }
3206
3326
  var PaidContentManager = class extends WavedashManager {
3327
+ constructor() {
3328
+ super(...arguments);
3329
+ this.paywallOpen = false;
3330
+ }
3207
3331
  async isEntitled(contentId) {
3208
3332
  const jwt = await this.sdk.ensureGameplayJwt();
3209
3333
  return readEntitlementsFromJwt(jwt).includes(contentId);
@@ -3214,15 +3338,34 @@ var PaidContentManager = class extends WavedashManager {
3214
3338
  }
3215
3339
  async triggerPaywall(contentIdentifier) {
3216
3340
  if (await this.isEntitled(contentIdentifier)) return true;
3217
- const response = await this.sdk.iframeMessenger.requestFromParent(
3218
- IFRAME_MESSAGE_TYPE6.TRIGGER_PAYWALL,
3219
- { contentIdentifier },
3220
- PAYWALL_TIMEOUT_MS
3221
- );
3341
+ if (this.paywallOpen) {
3342
+ throw new Error("Paywall already in progress");
3343
+ }
3344
+ this.paywallOpen = true;
3345
+ this.restorePointerLock = suspendPointerLock();
3346
+ let response;
3347
+ try {
3348
+ response = await this.sdk.iframeMessenger.requestFromParent(
3349
+ IFRAME_MESSAGE_TYPE6.TRIGGER_PAYWALL,
3350
+ { contentIdentifier },
3351
+ PAYWALL_TIMEOUT_MS
3352
+ );
3353
+ } finally {
3354
+ this.restorePointerLock?.();
3355
+ this.restorePointerLock = void 0;
3356
+ this.paywallOpen = false;
3357
+ }
3222
3358
  if (!response.purchased) return false;
3223
3359
  await this.sdk.ensureGameplayJwt(true);
3224
3360
  return true;
3225
3361
  }
3362
+ isPaywallOpen() {
3363
+ return this.paywallOpen;
3364
+ }
3365
+ destroy() {
3366
+ this.restorePointerLock?.();
3367
+ this.restorePointerLock = void 0;
3368
+ }
3226
3369
  };
3227
3370
 
3228
3371
  // src/services/stats.ts
@@ -4033,7 +4176,6 @@ var WavedashSDK = class extends EventTarget {
4033
4176
  this.gameFinishedLoading = true;
4034
4177
  this.heartbeatManager.start();
4035
4178
  iframeMessenger.postToParent(IFRAME_MESSAGE_TYPE7.LOADING_COMPLETE, {});
4036
- this.overlayManager.takeFocus();
4037
4179
  }
4038
4180
  get gameLoaded() {
4039
4181
  return this.gameFinishedLoading;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wvdsh/sdk-js",
3
- "version": "1.3.19",
3
+ "version": "1.3.21",
4
4
  "type": "module",
5
5
  "description": "Wavedash JavaScript SDK",
6
6
  "main": "./dist/client.js",
@@ -49,7 +49,7 @@
49
49
  "typescript-eslint": "^8.52.0"
50
50
  },
51
51
  "dependencies": {
52
- "@wvdsh/api": "^0.1.34",
52
+ "@wvdsh/api": "^0.1.36",
53
53
  "convex": "^1.39.1",
54
54
  "lodash.throttle": "^4.1.1"
55
55
  }