Fix iOS gyro: explicit Enable Motion gate; spawn ghosts near center, reveal on spawn
This commit is contained in:
+75
-20
@@ -159,10 +159,27 @@ const hunt = {
|
|||||||
await this.initCamera();
|
await this.initCamera();
|
||||||
this.initScene();
|
this.initScene();
|
||||||
this.bindControls();
|
this.bindControls();
|
||||||
|
this.maybePromptMotion();
|
||||||
this.spawnNext();
|
this.spawnNext();
|
||||||
this.loop();
|
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() {
|
async initCamera() {
|
||||||
this.video = $('#ar-video');
|
this.video = $('#ar-video');
|
||||||
try {
|
try {
|
||||||
@@ -196,18 +213,40 @@ const hunt = {
|
|||||||
};
|
};
|
||||||
addEventListener('resize', this._onResize);
|
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) => {
|
this._onOrient = (e) => {
|
||||||
if (e.alpha == null) return;
|
if (e.alpha == null) return;
|
||||||
this.orientation = { alpha: e.alpha, beta: e.beta, gamma: e.gamma, active: true };
|
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;
|
const DOE = window.DeviceOrientationEvent;
|
||||||
if (DOE && typeof DOE.requestPermission === 'function') {
|
return !!(DOE && typeof DOE.requestPermission === 'function');
|
||||||
try { await DOE.requestPermission(); } catch { /* ignore */ }
|
},
|
||||||
|
|
||||||
|
// 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 blast = $('#btn-blast');
|
||||||
const startBlast = async (e) => {
|
const startBlast = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await this.requestGyro();
|
|
||||||
this.blasting = true;
|
this.blasting = true;
|
||||||
};
|
};
|
||||||
const endBlast = () => { this.blasting = false; };
|
const endBlast = () => { this.blasting = false; };
|
||||||
@@ -278,32 +316,45 @@ const hunt = {
|
|||||||
}
|
}
|
||||||
group.add(mesh);
|
group.add(mesh);
|
||||||
|
|
||||||
// place at random yaw around the player, slight height + distance
|
// Place in a forward-facing arc so ghosts are findable without perfect
|
||||||
const yaw = Math.random() * Math.PI * 2;
|
// aiming: yaw within ±60° of straight ahead, modest pitch, closer in.
|
||||||
const dist = 4 + Math.random() * 2;
|
const yaw = (Math.random() - 0.5) * (Math.PI * 2 / 3);
|
||||||
group.position.set(Math.sin(yaw) * dist, (Math.random() - 0.4) * 1.5, -Math.cos(yaw) * dist);
|
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 = {
|
||||||
...group.userData,
|
...group.userData,
|
||||||
data, isBoss,
|
data, isBoss,
|
||||||
hp: data.health, maxHp: data.health,
|
hp: data.health, maxHp: data.health,
|
||||||
bobPhase: Math.random() * Math.PI * 2,
|
bobPhase: Math.random() * Math.PI * 2,
|
||||||
|
revealed: true, // visible on spawn; the wheel now aggros/tints rather than uncloaks
|
||||||
};
|
};
|
||||||
this.scene.add(group);
|
this.scene.add(group);
|
||||||
this.ghosts.push(group);
|
this.ghosts.push(group);
|
||||||
},
|
},
|
||||||
|
|
||||||
scanGloom(type) {
|
scanGloom(type) {
|
||||||
// "deglooming": reveal/aggro the nearest ghost matching this color.
|
// Ghosts are visible on spawn now, so the wheel acts as a "lure": scanning a
|
||||||
// Mirrors the source loop: scan a color → uncover a gloom spot of that type.
|
// colour pulls the nearest matching ghost toward your aim and charges gloom.
|
||||||
const match = this.ghosts.find((g) => g.userData.data.type === type && !g.userData.revealed);
|
// Mismatched colour risks a jumpscare.
|
||||||
if (match) {
|
const matches = this.ghosts.filter((g) => g.userData.data.type === type);
|
||||||
match.userData.revealed = true;
|
if (matches.length) {
|
||||||
this.toast(`${type.toUpperCase()} gloom uncovered`, 700);
|
// 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 {
|
} 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();
|
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.raf) cancelAnimationFrame(this.raf);
|
||||||
if (this.stream) { this.stream.getTracks().forEach((t) => t.stop()); this.stream = null; }
|
if (this.stream) { this.stream.getTracks().forEach((t) => t.stop()); this.stream = null; }
|
||||||
if (this._onResize) removeEventListener('resize', this._onResize);
|
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) {
|
if (this._blastEls) {
|
||||||
removeEventListener('touchend', this._blastEls.endBlast);
|
removeEventListener('touchend', this._blastEls.endBlast);
|
||||||
removeEventListener('mouseup', this._blastEls.endBlast);
|
removeEventListener('mouseup', this._blastEls.endBlast);
|
||||||
|
|||||||
Reference in New Issue
Block a user