From 0da74fb2dcaaca0f3c34c3665aeab7f867bb183c Mon Sep 17 00:00:00 2001 From: jessikitty Date: Thu, 18 Jun 2026 08:39:37 +1000 Subject: [PATCH] Fix iOS gyro: explicit Enable Motion gate; spawn ghosts near center, reveal on spawn --- public/js/game.js | 95 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 20 deletions(-) diff --git a/public/js/game.js b/public/js/game.js index 026cea1..0b68f53 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -159,10 +159,27 @@ const hunt = { await this.initCamera(); this.initScene(); this.bindControls(); + this.maybePromptMotion(); this.spawnNext(); this.loop(); }, + // On iOS, surface an explicit "Enable Motion" button. Tapping it is the clean + // user gesture iOS needs to show its Motion & Orientation Access prompt. + maybePromptMotion() { + const overlay = $('#motion-gate'); + if (!this.gyroNeedsPermission()) { overlay.classList.add('hidden'); return; } + overlay.classList.remove('hidden'); + const btn = $('#motion-enable'); + const onTap = async () => { + const ok = await this.requestGyro(); + overlay.classList.add('hidden'); + if (!ok) this.toast('Motion blocked — enable it in Settings › Safari', 2200); + }; + btn.onclick = onTap; + this._motionEls = { overlay, btn }; + }, + async initCamera() { this.video = $('#ar-video'); try { @@ -196,18 +213,40 @@ const hunt = { }; addEventListener('resize', this._onResize); - // Gyro look (iOS Safari needs a permission gesture; we request on first blast/scan tap) + // Gyro look. iOS Safari requires DeviceOrientationEvent.requestPermission() + // from an explicit user gesture (handled by the "Enable Motion" button in + // start()). Other browsers can attach the listener immediately. this._onOrient = (e) => { if (e.alpha == null) return; this.orientation = { alpha: e.alpha, beta: e.beta, gamma: e.gamma, active: true }; }; - addEventListener('deviceorientation', this._onOrient); + if (!this.gyroNeedsPermission()) { + addEventListener('deviceorientation', this._onOrient); + this._gyroAttached = true; + } }, - async requestGyro() { + gyroNeedsPermission() { const DOE = window.DeviceOrientationEvent; - if (DOE && typeof DOE.requestPermission === 'function') { - try { await DOE.requestPermission(); } catch { /* ignore */ } + return !!(DOE && typeof DOE.requestPermission === 'function'); + }, + + // Called from a clean tap (the Enable Motion overlay). On iOS this triggers + // the system "Motion & Orientation Access" prompt; once granted we attach the + // listener. Safe to call more than once. + async requestGyro() { + if (this._gyroAttached) return true; + const DOE = window.DeviceOrientationEvent; + try { + if (DOE && typeof DOE.requestPermission === 'function') { + const res = await DOE.requestPermission(); + if (res !== 'granted') return false; + } + addEventListener('deviceorientation', this._onOrient); + this._gyroAttached = true; + return true; + } catch { + return false; } }, @@ -225,9 +264,8 @@ const hunt = { }); const blast = $('#btn-blast'); - const startBlast = async (e) => { + const startBlast = (e) => { e.preventDefault(); - await this.requestGyro(); this.blasting = true; }; const endBlast = () => { this.blasting = false; }; @@ -278,32 +316,45 @@ const hunt = { } group.add(mesh); - // place at random yaw around the player, slight height + distance - const yaw = Math.random() * Math.PI * 2; - const dist = 4 + Math.random() * 2; - group.position.set(Math.sin(yaw) * dist, (Math.random() - 0.4) * 1.5, -Math.cos(yaw) * dist); + // Place in a forward-facing arc so ghosts are findable without perfect + // aiming: yaw within ±60° of straight ahead, modest pitch, closer in. + const yaw = (Math.random() - 0.5) * (Math.PI * 2 / 3); + const pitch = (Math.random() - 0.5) * 0.5; + const dist = 3 + Math.random() * 1.5; + group.position.set( + Math.sin(yaw) * dist, + Math.sin(pitch) * dist, + -Math.cos(yaw) * dist + ); group.userData = { ...group.userData, data, isBoss, hp: data.health, maxHp: data.health, bobPhase: Math.random() * Math.PI * 2, + revealed: true, // visible on spawn; the wheel now aggros/tints rather than uncloaks }; this.scene.add(group); this.ghosts.push(group); }, scanGloom(type) { - // "deglooming": reveal/aggro the nearest ghost matching this color. - // Mirrors the source loop: scan a color → uncover a gloom spot of that type. - const match = this.ghosts.find((g) => g.userData.data.type === type && !g.userData.revealed); - if (match) { - match.userData.revealed = true; - this.toast(`${type.toUpperCase()} gloom uncovered`, 700); + // Ghosts are visible on spawn now, so the wheel acts as a "lure": scanning a + // colour pulls the nearest matching ghost toward your aim and charges gloom. + // Mismatched colour risks a jumpscare. + const matches = this.ghosts.filter((g) => g.userData.data.type === type); + if (matches.length) { + // pull the nearest match into a forward, easy-to-aim position + const g = matches[0]; + const dist = 3; + g.position.set((Math.random() - 0.5) * 1.2, (Math.random() - 0.3) * 0.8, -dist); + g.userData.lured = true; + this.gloom += 5; this.updateGloom(); + this.toast(`${type.toUpperCase()} ghost lured in`, 800); } else { - // small chance of a Nagging-Nathan-style jumpscare on an empty scan + // no ghost of that colour present — small chance of a jumpscare if (Math.random() < 0.12) this.jumpscare(); - else { this.gloom += 5; this.updateGloom(); this.toast('+5 gloom', 600); } + else { this.gloom += 2; this.updateGloom(); this.toast('+2 gloom', 600); } } }, @@ -482,7 +533,11 @@ const hunt = { if (this.raf) cancelAnimationFrame(this.raf); if (this.stream) { this.stream.getTracks().forEach((t) => t.stop()); this.stream = null; } if (this._onResize) removeEventListener('resize', this._onResize); - if (this._onOrient) removeEventListener('deviceorientation', this._onOrient); + if (this._onOrient && this._gyroAttached) { + removeEventListener('deviceorientation', this._onOrient); + this._gyroAttached = false; + } + if (this._motionEls) { this._motionEls.overlay.classList.add('hidden'); } if (this._blastEls) { removeEventListener('touchend', this._blastEls.endBlast); removeEventListener('mouseup', this._blastEls.endBlast);