Compare commits
5
Commits
7bc83a782d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
185845f969 | ||
|
|
3a4c2579a5 | ||
|
|
ac69e456d9 | ||
|
|
5385e10a41 | ||
|
|
762a698db4 |
@@ -20,7 +20,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
"Always maintain a minimum distance of 10 feet (3 meters) from sea turtles on land.",
|
"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.",
|
"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.",
|
"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 = ['🦀', '🦅', '🦝'];
|
const PREDATOR_EMOJIS = ['🦀', '🦅', '🦝'];
|
||||||
@@ -61,6 +61,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let beachingProgress = 0; // 0 to 100
|
let beachingProgress = 0; // 0 to 100
|
||||||
let beachingInterval = null;
|
let beachingInterval = null;
|
||||||
let beachingTouristsInterval = null;
|
let beachingTouristsInterval = null;
|
||||||
|
// Earliest time (ms) the next beaching event may trigger. Keeps beaching to
|
||||||
|
// an occasional event rather than something that interrupts every minute.
|
||||||
|
const BEACHING_COOLDOWN_MS = 5 * 60 * 1000;
|
||||||
|
let nextBeachingAllowedAt = 0;
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 2. DOM ELEMENTS
|
// 2. DOM ELEMENTS
|
||||||
@@ -119,6 +123,75 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
sad: document.getElementById('turtle-mouth-sad')
|
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',
|
||||||
|
heehee: 'turtvox006_heeheehee.m4a',
|
||||||
|
morePlease: 'turtvox008_more_please.m4a',
|
||||||
|
hehe: 'turtvox09_hehe_that_tickles.m4a',
|
||||||
|
yummySeagrass: 'turtvox011_yummy_seagrass.m4a',
|
||||||
|
letsPlay: 'turtvox012_another_turtle_lets_play.m4a',
|
||||||
|
swimSafe: 'turtvox013_swim_safe_friend.m4a',
|
||||||
|
applyingSalve: 'turtvox014_applying_salve.m4a',
|
||||||
|
grewALittle: 'turtvox15_I_think_I_grew_a_little.m4a',
|
||||||
|
crunchyCrab: 'turtvox016_crunchy_crab.m4a',
|
||||||
|
softJellyfish: 'turtvox017_soft_jellyfish.m4a'
|
||||||
|
};
|
||||||
|
|
||||||
|
const WATER_SFX = [
|
||||||
|
'water_sfx_001.m4a',
|
||||||
|
'water_sfx_002.m4a',
|
||||||
|
'water_sfx_003.m4a',
|
||||||
|
'turtvox008_glub_glub_glub.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
|
// Buttons
|
||||||
const btnStartGame = document.getElementById('btn-start-game');
|
const btnStartGame = document.getElementById('btn-start-game');
|
||||||
const btnShowFactsMain = document.getElementById('btn-show-facts-main');
|
const btnShowFactsMain = document.getElementById('btn-show-facts-main');
|
||||||
@@ -136,6 +209,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const btnHeal = document.getElementById('btn-action-heal');
|
const btnHeal = document.getElementById('btn-action-heal');
|
||||||
const feedDrawer = document.getElementById('feed-options-drawer');
|
const feedDrawer = document.getElementById('feed-options-drawer');
|
||||||
|
|
||||||
|
// Name inputs / dynamic name labels
|
||||||
|
const inputTurtleNameStart = document.getElementById('input-turtle-name-start');
|
||||||
|
const beachingTurtleName = document.getElementById('beaching-turtle-name');
|
||||||
|
const bonusTurtleName = document.getElementById('bonus-turtle-name');
|
||||||
|
|
||||||
// HUD Outputs
|
// HUD Outputs
|
||||||
const displayTurtleName = document.getElementById('display-turtle-name');
|
const displayTurtleName = document.getElementById('display-turtle-name');
|
||||||
const displayTurtleSpecies = document.getElementById('display-turtle-species');
|
const displayTurtleSpecies = document.getElementById('display-turtle-species');
|
||||||
@@ -491,15 +569,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
turtleAgeVal.textContent = formatAge(turtleAge);
|
turtleAgeVal.textContent = formatAge(turtleAge);
|
||||||
applyGrowthStage();
|
applyGrowthStage();
|
||||||
if (wholeDays > 0 && Math.random() > 0.7) {
|
if (wholeDays > 0 && Math.random() > 0.7) {
|
||||||
triggerThoughtBubble(getGrowthThought(), 2200);
|
const growth = getGrowthThought();
|
||||||
|
triggerThoughtBubble(growth.text, 2200, growth.voice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveProgress();
|
saveProgress();
|
||||||
|
|
||||||
// Beaching happens on a regular cycle for healthy adults — roughly
|
// Beaching is an occasional event for healthy adults. A cooldown plus a
|
||||||
// every few minutes of active play.
|
// low per-tick chance keeps it to roughly once every several minutes
|
||||||
if (turtleAge > 7 && stats.health > 50 && Math.random() < 0.18) {
|
// rather than interrupting play every minute.
|
||||||
|
if (turtleAge > 7 && stats.health > 50 &&
|
||||||
|
Date.now() >= nextBeachingAllowedAt && Math.random() < 0.04) {
|
||||||
|
nextBeachingAllowedAt = Date.now() + BEACHING_COOLDOWN_MS;
|
||||||
changeState(STATES.BEACHING);
|
changeState(STATES.BEACHING);
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@@ -533,10 +615,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (currentState !== STATES.OCEAN) return;
|
if (currentState !== STATES.OCEAN) return;
|
||||||
spawnAmbientBubble();
|
spawnAmbientBubble();
|
||||||
}, 1200);
|
}, 1200);
|
||||||
|
|
||||||
|
// 7. Ambient underwater sound for the ocean scene
|
||||||
|
startAmbientWater();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearOceanLoops() {
|
function clearOceanLoops() {
|
||||||
Object.values(oceanLoops).forEach(loop => clearInterval(loop));
|
Object.values(oceanLoops).forEach(loop => clearInterval(loop));
|
||||||
|
stopAmbientWater();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -568,7 +654,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
function sanitizeName(n) {
|
function sanitizeName(n) {
|
||||||
if (typeof n !== 'string') return 'Shelly';
|
if (typeof n !== 'string') return 'Shelly';
|
||||||
// Strip control chars and HTML-relevant punctuation; cap at 12 like the input.
|
// Strip control chars and HTML-relevant punctuation; cap at 12 like the input.
|
||||||
const cleaned = n.replace(/[ | |||||||