Add turtle voice lines and ambient water SFX
Wire turtvox voice clips to their matching speech bubbles (predator alert, shell bite, plastic ingestion, propeller strike, tickle reward, idle musing) and loop water SFX as ambient underwater sound throughout the ocean scene. Precache media in the service worker (v11 -> v12) for offline playback. Also fix raw control bytes in the name-sanitization regex.
This commit is contained in:
@@ -20,7 +20,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
"Always maintain a minimum distance of 10 feet (3 meters) from sea turtles on land.",
|
||||
"Crowding sea turtles causes them severe stress and can disrupt their natural nesting or resting behaviors.",
|
||||
"Do not use flash photography near sea turtles; the bright flash disorients them and can disrupt nesting mothers.",
|
||||
"Filing in beach holes and removing beach chairs prevents hatchlings from getting trapped on their way to the ocean."
|
||||
"Filling in beach holes and removing beach chairs prevents hatchlings from getting trapped on their way to the ocean."
|
||||
];
|
||||
|
||||
const PREDATOR_EMOJIS = ['🦀', '🦅', '🦝'];
|
||||
@@ -119,6 +119,64 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
sad: document.getElementById('turtle-mouth-sad')
|
||||
};
|
||||
|
||||
// ==========================================
|
||||
// AUDIO: Turtle voice lines + ambient water SFX
|
||||
// ==========================================
|
||||
const MEDIA_PATH = 'media/';
|
||||
|
||||
// Voice lines keyed to the speech bubble they accompany (file name → trigger).
|
||||
const TURT_VOX = {
|
||||
deepBlue: 'turtvox001_another_day_in_the_deep_blue.m4a',
|
||||
bestHuman: 'turtvox002_best_human_ever.m4a',
|
||||
bittenShell: 'turtvox003_bitten_shell_but_ok.m4a',
|
||||
somebodysComing: 'turtvox004_somebodys_coming.m4a',
|
||||
ateBagToxic: 'turtvox005_ate_a_bag_toxic.m4a',
|
||||
propellerStrike: 'turtvox006_propeller_strike_fatal_wound.m4a'
|
||||
};
|
||||
|
||||
const WATER_SFX = [
|
||||
'water_sfx_001.m4a',
|
||||
'water_sfx_002.m4a',
|
||||
'water_sfx_003.m4a'
|
||||
];
|
||||
|
||||
// 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 playVoice(key) {
|
||||
const file = TURT_VOX[key];
|
||||
if (!file) return;
|
||||
try {
|
||||
if (currentVoice) currentVoice.pause();
|
||||
currentVoice = new Audio(MEDIA_PATH + file);
|
||||
currentVoice.volume = 0.9;
|
||||
currentVoice.play().catch(() => {});
|
||||
} catch (e) { /* audio unsupported */ }
|
||||
}
|
||||
|
||||
// Ambient underwater sound — loops continuously through the water SFX clips
|
||||
// for the whole time the player is in the ocean scene.
|
||||
let ambientAudio = null;
|
||||
let ambientIndex = 0;
|
||||
function startAmbientWater() {
|
||||
if (ambientAudio) return;
|
||||
ambientIndex = 0;
|
||||
ambientAudio = new Audio(MEDIA_PATH + WATER_SFX[0]);
|
||||
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(() => {});
|
||||
});
|
||||
ambientAudio.play().catch(() => {});
|
||||
}
|
||||
function stopAmbientWater() {
|
||||
if (!ambientAudio) return;
|
||||
ambientAudio.pause();
|
||||
ambientAudio = null;
|
||||
}
|
||||
|
||||
// Buttons
|
||||
const btnStartGame = document.getElementById('btn-start-game');
|
||||
const btnShowFactsMain = document.getElementById('btn-show-facts-main');
|
||||
@@ -533,10 +591,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (currentState !== STATES.OCEAN) return;
|
||||
spawnAmbientBubble();
|
||||
}, 1200);
|
||||
|
||||
// 7. Ambient underwater sound for the ocean scene
|
||||
startAmbientWater();
|
||||
}
|
||||
|
||||
function clearOceanLoops() {
|
||||
Object.values(oceanLoops).forEach(loop => clearInterval(loop));
|
||||
stopAmbientWater();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@@ -568,7 +630,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function sanitizeName(n) {
|
||||
if (typeof n !== 'string') return 'Shelly';
|
||||
// Strip control chars and HTML-relevant punctuation; cap at 12 like the input.
|
||||
const cleaned = n.replace(/[ | ||||