Suspend gameplay when unfocused and fix playmate voice mismatches
Pause the ocean decay/aging/hazard loops and ambient audio whenever the tab is hidden or the window loses focus, and resume untouched on return. Remove the offline catch-up decay so a turtle left alone freezes instead of dying, letting turtles live and grow over a long real-world span. Gate the playmate greeting/farewell voice clips (letsPlay/swimSafe) to the green species whose text they match; loggerhead and leatherback play silently. Also keep glub out of the ambient loop while a voice line plays. Bump SW to v17.
This commit is contained in:
@@ -148,16 +148,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
softJellyfish: 'turtvox017_soft_jellyfish.m4a'
|
||||
};
|
||||
|
||||
// The glub clip is a turtle vocalisation, so it must never overlap a voice
|
||||
// line; the plain water clips are fine to play underneath voices.
|
||||
const GLUB_SFX = 'turtvox008_glub_glub_glub.m4a';
|
||||
const WATER_SFX = [
|
||||
'water_sfx_001.m4a',
|
||||
'water_sfx_002.m4a',
|
||||
'water_sfx_003.m4a',
|
||||
'turtvox008_glub_glub_glub.m4a'
|
||||
GLUB_SFX
|
||||
];
|
||||
|
||||
// Play a single turtle voice line. Autoplay is allowed because the player has
|
||||
// already tapped through the start screen before any voice line fires.
|
||||
let currentVoice = null;
|
||||
function isVoicePlaying() {
|
||||
return !!(currentVoice && !currentVoice.paused && !currentVoice.ended);
|
||||
}
|
||||
function playVoice(key) {
|
||||
const file = TURT_VOX[key];
|
||||
if (!file) return;
|
||||
@@ -166,6 +172,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
currentVoice = new Audio(MEDIA_PATH + file);
|
||||
currentVoice.volume = 0.9;
|
||||
currentVoice.play().catch(() => {});
|
||||
// If the ambient loop is currently on the glub clip, skip it forward so
|
||||
// the glub never plays on top of this voice line.
|
||||
if (ambientAudio && WATER_SFX[ambientIndex] === GLUB_SFX) {
|
||||
ambientIndex = (ambientIndex + 1) % WATER_SFX.length;
|
||||
playAmbientClip();
|
||||
}
|
||||
} catch (e) { /* audio unsupported */ }
|
||||
}
|
||||
|
||||
@@ -173,18 +185,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// for the whole time the player is in the ocean scene.
|
||||
let ambientAudio = null;
|
||||
let ambientIndex = 0;
|
||||
function playAmbientClip() {
|
||||
if (!ambientAudio) return;
|
||||
// Never start the glub clip while a voice line is playing — advance past it.
|
||||
let guard = 0;
|
||||
while (WATER_SFX[ambientIndex] === GLUB_SFX && isVoicePlaying() && guard < WATER_SFX.length) {
|
||||
ambientIndex = (ambientIndex + 1) % WATER_SFX.length;
|
||||
guard++;
|
||||
}
|
||||
ambientAudio.src = MEDIA_PATH + WATER_SFX[ambientIndex];
|
||||
ambientAudio.play().catch(() => {});
|
||||
}
|
||||
function startAmbientWater() {
|
||||
if (ambientAudio) return;
|
||||
ambientIndex = 0;
|
||||
ambientAudio = new Audio(MEDIA_PATH + WATER_SFX[0]);
|
||||
ambientAudio = new Audio();
|
||||
ambientAudio.volume = 0.3;
|
||||
ambientAudio.addEventListener('ended', () => {
|
||||
if (!ambientAudio) return;
|
||||
ambientIndex = (ambientIndex + 1) % WATER_SFX.length;
|
||||
ambientAudio.src = MEDIA_PATH + WATER_SFX[ambientIndex];
|
||||
ambientAudio.play().catch(() => {});
|
||||
playAmbientClip();
|
||||
});
|
||||
ambientAudio.play().catch(() => {});
|
||||
playAmbientClip();
|
||||
}
|
||||
function stopAmbientWater() {
|
||||
if (!ambientAudio) return;
|
||||
@@ -645,6 +667,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.addEventListener('pagehide', saveProgress);
|
||||
window.addEventListener('beforeunload', saveProgress);
|
||||
|
||||
// Suspend gameplay whenever the tab/window loses focus or is hidden, so the
|
||||
// turtle never decays (or dies) while the player is away. Stats and growth
|
||||
// simply freeze and resume untouched when the player returns. Only the ocean
|
||||
// care phase runs the long-lived decay loops, so that is what we pause.
|
||||
let oceanSuspended = false;
|
||||
function suspendOcean() {
|
||||
if (currentState !== STATES.OCEAN || oceanSuspended) return;
|
||||
oceanSuspended = true;
|
||||
saveProgress();
|
||||
clearOceanLoops();
|
||||
}
|
||||
function resumeOcean() {
|
||||
if (!oceanSuspended) return;
|
||||
oceanSuspended = false;
|
||||
if (currentState === STATES.OCEAN) startOceanLoops();
|
||||
}
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) suspendOcean();
|
||||
else resumeOcean();
|
||||
});
|
||||
window.addEventListener('blur', suspendOcean);
|
||||
window.addEventListener('focus', resumeOcean);
|
||||
|
||||
const ALLOWED_SPECIES = ['green', 'loggerhead', 'leatherback'];
|
||||
function clampStat(n) {
|
||||
const v = Number(n);
|
||||
@@ -689,34 +734,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
isSick = data.isSick === true;
|
||||
|
||||
const savedAt = Number(data.savedAt);
|
||||
if (Number.isFinite(savedAt) && savedAt > 0 && savedAt <= Date.now()) {
|
||||
const elapsedSec = Math.max(0, (Date.now() - savedAt) / 1000);
|
||||
const cappedSec = Math.min(elapsedSec, 24 * 60 * 60);
|
||||
applyOfflineProgression(cappedSec);
|
||||
}
|
||||
// No offline catch-up decay: gameplay is suspended whenever the tab is not
|
||||
// focused (see the visibility/focus handlers), so a turtle left alone — for
|
||||
// minutes or months — is simply paused and resumes exactly as it was left.
|
||||
// This is what lets turtles live and grow over a very long real-world span.
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyOfflineProgression(seconds) {
|
||||
// Mirror the per-second rates from the decay loop
|
||||
stats.hunger = Math.max(0, stats.hunger - 0.15 * seconds);
|
||||
stats.joy = Math.max(0, stats.joy - 0.18 * seconds);
|
||||
stats.clean = Math.max(0, stats.clean - 0.08 * seconds);
|
||||
|
||||
// Approximate health behavior over the elapsed window
|
||||
const avgEnd = (stats.hunger + stats.joy + stats.clean) / 3;
|
||||
if (avgEnd < 20) {
|
||||
stats.health = Math.max(0, stats.health - 0.4 * seconds * 0.5); // gradual neglect damage
|
||||
} else if (avgEnd > 60) {
|
||||
stats.health = Math.min(100, stats.health + 0.05 * seconds * 0.2);
|
||||
}
|
||||
|
||||
// Award age progression for the healthy portion of the absence
|
||||
const offlineDays = computeAgeGain(seconds);
|
||||
turtleAge += Math.floor(offlineDays);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// AGING & GROWTH HELPERS
|
||||
// ==========================================
|
||||
@@ -1536,6 +1560,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
name: 'shrimp friend',
|
||||
greeting: "A shrimp friend! Let's chase! 🦐",
|
||||
farewell: "Bye shrimp pal! 👋",
|
||||
// No matching voice clip for the shrimp lines.
|
||||
greetingVoice: null,
|
||||
farewellVoice: null,
|
||||
color: 'rgba(255, 140, 120, 0.9)'
|
||||
};
|
||||
case 'leatherback':
|
||||
@@ -1544,6 +1571,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
name: 'squid buddy',
|
||||
greeting: "Glowing squid buddy! Deep dive! 🦑",
|
||||
farewell: "See you in the depths! 🌊",
|
||||
// No matching voice clip for the squid lines.
|
||||
greetingVoice: null,
|
||||
farewellVoice: null,
|
||||
color: 'rgba(180, 130, 255, 0.95)'
|
||||
};
|
||||
case 'green':
|
||||
@@ -1553,6 +1583,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
name: 'turtle pal',
|
||||
greeting: "Another turtle! Let's play! 🐢",
|
||||
farewell: "Swim safe, friend! 💚",
|
||||
// The letsPlay/swimSafe clips say "another turtle"/"swim safe friend".
|
||||
greetingVoice: 'letsPlay',
|
||||
farewellVoice: 'swimSafe',
|
||||
color: 'rgba(120, 200, 160, 0.95)'
|
||||
};
|
||||
}
|
||||
@@ -1571,7 +1604,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateStatBars();
|
||||
setEyeState('happy');
|
||||
setMouthState('normal');
|
||||
triggerThoughtBubble(info.greeting, 2200, 'letsPlay');
|
||||
triggerThoughtBubble(info.greeting, 2200, info.greetingVoice);
|
||||
|
||||
// Random darting motion around the playpen
|
||||
const pen = oceanPlaypen.getBoundingClientRect();
|
||||
@@ -1639,7 +1672,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
stats.joy = Math.min(100, stats.joy + Math.min(15, highFives * 2));
|
||||
updateStatBars();
|
||||
}
|
||||
triggerThoughtBubble(info.farewell, 1800, 'swimSafe');
|
||||
triggerThoughtBubble(info.farewell, 1800, info.farewellVoice);
|
||||
setTimeout(() => setEyeState('normal'), 1200);
|
||||
const m = activePlaymate;
|
||||
activePlaymate = null;
|
||||
|
||||
Reference in New Issue
Block a user