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();
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user