@onnie81/murdoku-spor 1.2.2 → 1.2.4

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.
@@ -7,6 +7,12 @@ let master, sfxBus, musicBus, analyser;
7
7
  let enabled = { sfx: true, music: true };
8
8
  let musicOn = false;
9
9
  let schedTimer = null;
10
+ let outEl = null; // iOS: audio leaves through a media element, not ctx.destination
11
+
12
+ // iPadOS pretends to be MacIntel, hence the maxTouchPoints check
13
+ const IS_IOS = typeof navigator !== 'undefined' &&
14
+ (/iP(hone|ad|od)/.test(navigator.userAgent) ||
15
+ (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1));
10
16
 
11
17
  function ensureCtx() {
12
18
  if (ctx) return ctx;
@@ -15,7 +21,21 @@ function ensureCtx() {
15
21
  ctx = new AC();
16
22
  master = ctx.createGain();
17
23
  master.gain.value = 0.9;
18
- master.connect(ctx.destination);
24
+ if (IS_IOS) {
25
+ // Direct WebAudio output is unreliable on iOS (silent-switch category and
26
+ // sample-rate mismatches can mute it entirely while the graph keeps
27
+ // "playing"). Routing through a MediaStream + <audio> element uses the
28
+ // real media playback path: audible, and immune to the ring switch.
29
+ const dest = ctx.createMediaStreamDestination();
30
+ master.connect(dest);
31
+ outEl = document.createElement('audio');
32
+ outEl.setAttribute('playsinline', '');
33
+ outEl.autoplay = true;
34
+ outEl.srcObject = dest.stream;
35
+ document.body.appendChild(outEl);
36
+ } else {
37
+ master.connect(ctx.destination);
38
+ }
19
39
  analyser = ctx.createAnalyser();
20
40
  analyser.fftSize = 2048;
21
41
  master.connect(analyser);
@@ -40,22 +60,8 @@ function ensureCtx() {
40
60
  return ctx;
41
61
  }
42
62
 
43
- // iOS: playing a (nearly) silent media element inside the gesture switches the
44
- // audio session to "playback", so WebAudio stays audible even with the ring
45
- // switch on silent. One-time, invisible.
46
- const SILENT_WAV = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQAAAAA=';
47
- let mediaUnlocked = false;
48
- function unlockMediaSession() {
49
- if (mediaUnlocked) return;
50
- mediaUnlocked = true;
51
- try {
52
- const a = document.createElement('audio');
53
- a.setAttribute('playsinline', '');
54
- a.src = SILENT_WAV;
55
- a.volume = 0.01;
56
- const p = a.play();
57
- if (p && p.catch) p.catch(() => { mediaUnlocked = false; });
58
- } catch { mediaUnlocked = false; }
63
+ function unlocked() {
64
+ return ctx && ctx.state === 'running' && (!IS_IOS || (outEl && !outEl.paused));
59
65
  }
60
66
 
61
67
  export function initAudioOnGesture() {
@@ -63,22 +69,31 @@ export function initAudioOnGesture() {
63
69
  const kick = () => {
64
70
  const c = ensureCtx();
65
71
  if (!c) return;
66
- unlockMediaSession();
72
+ // both calls must happen inside the user gesture
73
+ if (outEl && outEl.paused) {
74
+ const p = outEl.play();
75
+ if (p && p.catch) p.catch(() => {});
76
+ }
67
77
  const go = () => {
68
- if (c.state !== 'running') return; // keep listeners until truly unlocked
78
+ if (!unlocked()) return; // keep listening until truly audible
69
79
  EVENTS.forEach(ev => window.removeEventListener(ev, kick));
70
80
  if (enabled.music) startMusic();
71
81
  };
72
82
  if (c.state === 'suspended') c.resume().then(go).catch(() => {});
73
- else go();
83
+ else setTimeout(go, 50);
74
84
  };
75
85
  EVENTS.forEach(ev => window.addEventListener(ev, kick));
76
86
 
77
- // phones suspend the context when the tab goes to background — resume on return
87
+ // phones suspend audio when the tab goes to background — restore on return
78
88
  document.addEventListener('visibilitychange', () => {
79
- if (!document.hidden && ctx && ctx.state === 'suspended') {
89
+ if (document.hidden || !ctx) return;
90
+ if (ctx.state === 'suspended') {
80
91
  ctx.resume().then(() => { if (enabled.music) startMusic(); }).catch(() => {});
81
92
  }
93
+ if (outEl && outEl.paused) {
94
+ const p = outEl.play();
95
+ if (p && p.catch) p.catch(() => {});
96
+ }
82
97
  });
83
98
  }
84
99
 
@@ -144,6 +159,10 @@ export function sfx(name) {
144
159
  const c = ensureCtx();
145
160
  if (!c) return;
146
161
  if (c.state === 'suspended') c.resume();
162
+ if (outEl && outEl.paused) {
163
+ const p = outEl.play();
164
+ if (p && p.catch) p.catch(() => {});
165
+ }
147
166
  switch (name) {
148
167
  case 'place': // soft wooden tap + warm blip
149
168
  noise({ d: 0.05, peak: 0.25, freq: 900, q: 0.8, type: 'lowpass' });
@@ -302,6 +321,8 @@ if (typeof window !== 'undefined') {
302
321
  state: () => (ctx ? ctx.state : 'none'),
303
322
  musicOn: () => musicOn,
304
323
  enabled: () => ({ ...enabled }),
324
+ route: () => (IS_IOS ? (outEl ? (outEl.paused ? 'ios-media:paused' : 'ios-media:playing') : 'ios-media:missing') : 'direct'),
325
+ rate: () => (ctx ? ctx.sampleRate : 0),
305
326
  rms() {
306
327
  if (!analyser) return 0;
307
328
  const a = new Float32Array(analyser.fftSize);
package/murdoku.bundle CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onnie81/murdoku-spor",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "type": "module",
5
5
  "description": "Murdoku — online murder-mystery logic puzzle (sudoku-style placement deduction). Deploy artifact for games.onniehex.dev.",
6
6
  "license": "MIT",
@@ -32,17 +32,29 @@ mkdir -p "$APP"
32
32
  find "$APP" -mindepth 1 -maxdepth 1 ! -name .git ! -name .version -exec rm -rf {} +
33
33
  cp -a "$src"/. "$APP"/
34
34
 
35
- # the deploy runner updates itself from the package (effective next run)
35
+ # the deploy runner updates itself from the package and re-executes so the
36
+ # rest of THIS deploy runs with the fresh logic (cmp guard prevents looping)
36
37
  if [ -f "$APP/scripts/npm-server-deploy.sh" ] && ! cmp -s "$APP/scripts/npm-server-deploy.sh" /usr/local/bin/murdoku-deploy; then
37
38
  install -m 755 "$APP/scripts/npm-server-deploy.sh" /usr/local/bin/murdoku-deploy || true
38
- echo "$(date -Is) deploy runner self-updated"
39
+ echo "$(date -Is) deploy runner self-updated — re-executing"
40
+ rm -rf "$tmp"
41
+ exec env FORCE=1 /usr/local/bin/murdoku-deploy "$APP" "$BRANCH"
39
42
  fi
40
43
 
41
- # refresh real git history from the bundle carried in the package (optional)
44
+ # refresh real git history from the bundle carried in the package.
45
+ # NOTE: fetch into a side ref — git refuses to fetch into the checked-out
46
+ # branch, which is what froze the reported sha at the first deploy.
42
47
  if command -v git >/dev/null 2>&1 && [ -f "$src/murdoku.bundle" ]; then
43
48
  if [ ! -d "$APP/.git" ]; then git -C "$APP" init -q; fi
44
- git -C "$APP" fetch -f "$src/murdoku.bundle" 'refs/heads/main:refs/heads/main' || true
45
- git -C "$APP" symbolic-ref HEAD refs/heads/main 2>/dev/null || true
49
+ if git -C "$APP" fetch -f "$src/murdoku.bundle" 'refs/heads/main:refs/heads/incoming' 2>/dev/null; then
50
+ NEWREF=$(git -C "$APP" rev-parse --verify incoming 2>/dev/null || true)
51
+ if [ -n "$NEWREF" ]; then
52
+ git -C "$APP" update-ref refs/heads/main "$NEWREF"
53
+ git -C "$APP" symbolic-ref HEAD refs/heads/main 2>/dev/null || true
54
+ git -C "$APP" reset --hard main >/dev/null 2>&1 || true
55
+ git -C "$APP" update-ref -d refs/heads/incoming 2>/dev/null || true
56
+ fi
57
+ fi
46
58
  fi
47
59
 
48
60
  cd "$APP"