1// ð Fish Food System w/ Bubble Burst 2const FISH_FOOD_SIZE = 12; // pixels 3const FISH_FOOD_SINK_SPEED = 0.55; // pixels per frame 4const FISH_FOOD_WIGGLE = 1.2; // horizontal wobble 5const FISH_FOOD_SPAWN_INTERVAL_MIN = 1500; // ms 6const FISH_FOOD_SPAWN_INTERVAL_MAX = 5000; // ms 7const NOM_SOUND_URLS = [ 8 "https://cdn.pixabay.com/audio/2024/01/03/audio_d3cebd844e.mp3", 9 "https://cdn.pixabay.com/audio/2024/01/16/audio_f0aa2ad2dc.mp3", 10 "https://cdn.pixabay.com/audio/2024/01/16/audio_fb4eef1715.mp3", 11 "https://cdn.pixabay.com/audio/2024/01/16/audio_c3e0358182.mp3" 12]; 13 14let fishFood = null; 15let fishFoodEnabled = true; 16let pendingSpawnTimer = null; 17 18function clearPendingSpawn() { 19 if (pendingSpawnTimer !== null) { 20 clearTimeout(pendingSpawnTimer); 21 pendingSpawnTimer = null; 22 } 23} 24 25function scheduleSpawn(delay) { 26 clearPendingSpawn(); 27 if (!fishFoodEnabled) return; 28 pendingSpawnTimer = setTimeout(function () { 29 pendingSpawnTimer = null; 30 spawnFishFood(); 31 }, delay); 32} 33 34function setFishFoodEnabled(enabled) { 35 fishFoodEnabled = !!enabled; 36 if (!fishFoodEnabled) { 37 clearPendingSpawn(); 38 if (fishFood) { 39 fishFood.remove(); 40 fishFood = null; 41 } 42 document.querySelectorAll('.fish-food, .bubble-pop').forEach(function (node) { 43 node.remove(); 44 }); 45 } else if (!fishFood && !pendingSpawnTimer) { 46 scheduleSpawn(randomSpawnInterval()); 47 } 48} 49 50// Cursor position (used for collection when there's no 3D fish) 51let cursorX = null; 52let cursorY = null; 53document.addEventListener("mousemove", (e) => { 54 cursorX = e.clientX; 55 cursorY = e.clientY; 56}); 57 58// Play one of the nom sounds at a slightly randomized volume (0.1 - 0.2) 59function playNomSound(){ 60 try { 61 const url = NOM_SOUND_URLS[Math.floor(Math.random() * NOM_SOUND_URLS.length)]; 62 const a = new Audio(url); 63 a.preload = 'auto'; 64 a.volume = Math.random() * 0.1 + 0.1; // 0.1 .. 0.2 65 // auto-cleanup when finished 66 a.addEventListener('ended', () => { try { a.remove(); } catch(e){} }); 67 a.play().catch(()=>{}); 68 } catch(e){ /* ignore playback errors */ } 69} 70let spawnTime = 0; 71 72// ð£ Helper: random position in viewport 73function randomPosition() { 74 const x = Math.random() * (window.innerWidth - FISH_FOOD_SIZE * 2) + FISH_FOOD_SIZE; 75 const y = Math.random() * (window.innerHeight / 3); 76 return { x, y }; 77} 78 79// ð£ Helper: random spawn interval 80function randomSpawnInterval() { 81 return Math.random() * (FISH_FOOD_SPAWN_INTERVAL_MAX - FISH_FOOD_SPAWN_INTERVAL_MIN) + FISH_FOOD_SPAWN_INTERVAL_MIN; 82} 83 84// ð¤ Spawn a pellet 85function spawnFishFood() { 86 if (!fishFoodEnabled) return; 87 if (fishFood) fishFood.remove(); 88 89 const { x, y } = randomPosition(); 90 const food = document.createElement("div"); 91 food.classList.add("fish-food"); 92 food.style.left = `${x}px`; 93 food.style.top = `${y}px`; 94 food.dataset.baseX = x; 95 food.dataset.offset = Math.random() * 1000; 96 document.body.appendChild(food); 97 fishFood = food; 98 spawnTime = Date.now(); 99} 100 101// 𪱠Check collision: cursor or 3D fish (fishBuddy sets window.fishX/fishY when present) 102const COLLECT_RADIUS = 12; // pixels 103 104function checkCollision() { 105 // Use cursor position if available; otherwise fall back to 3D fish position 106 const px = cursorX ?? window.fishX; 107 const py = cursorY ?? window.fishY; 108 if (!fishFood || px === undefined || py === undefined) return; 109 110 const rect = fishFood.getBoundingClientRect(); 111 const fx = rect.left + rect.width / 2; 112 const fy = rect.top + rect.height / 2; 113 const dx = fx - px; 114 const dy = fy - py; 115 const dist = Math.sqrt(dx * dx + dy * dy); 116 117 if (dist < COLLECT_RADIUS) collectFishFood(fx, fy); 118} 119 120// ð«§ Create bubble burst at location 121function createBubbleBurst(x, y) { 122 const bubbleCount = 2 + Math.floor(Math.random() * 2); 123 124 for (let i = 0; i < bubbleCount; i++) { 125 const bubble = document.createElement("div"); 126 bubble.classList.add("bubble-pop"); 127 128 // random small size + angle 129 const size = 6 + Math.random() * 6; 130 bubble.style.width = `${size}px`; 131 bubble.style.height = `${size}px`; 132 bubble.style.left = `${x}px`; 133 bubble.style.top = `${y}px`; 134 bubble.style.setProperty("--dx", (Math.random() - 0.5) * 60 + "px"); 135 bubble.style.setProperty("--dy", (Math.random() - 1.2) * 60 + "px"); 136 bubble.style.setProperty("--fade", (0.8 + Math.random() * 0.4).toFixed(2)); 137 138 document.body.appendChild(bubble); 139 140 // cleanup 141 setTimeout(() => bubble.remove(), 800); 142 } 143} 144 145// ð«§ Handle collection
146function collectFishFood(x, y) { 147 if (!fishFood) return; 148 // guard so a single piece of food only triggers one collection flow 149 if (fishFood.dataset && fishFood.dataset.collected === '1') return; 150 if (fishFood.dataset) fishFood.dataset.collected = '1'; 151 // play a randomized nom sound immediately 152 playNomSound(); 153 154 createBubbleBurst(x, y); 155 156 // Emit a global event so other scripts (food counter UI, analytics, etc.) can react 157 try { 158 window.dispatchEvent(new CustomEvent('fishfood:collected', { detail: { when: Date.now() } })); 159 } catch (e) { 160 // ignore if dispatch fails in some environments 161 } 162 163 fishFood.style.transition = "opacity 0.3s ease-out, transform 0.3s ease-out"; 164 fishFood.style.opacity = "0"; 165 fishFood.style.transform = "scale(0.5)"; 166 167 setTimeout(() => { 168 if (fishFood) { 169 fishFood.remove(); 170 fishFood = null; 171 scheduleSpawn(randomSpawnInterval()); 172 } 173 }, 300); 174} 175 176// ð«§ Animate sinking + wobble 177function animateFood() { 178 if (fishFood) { 179 let top = parseFloat(fishFood.style.top); 180 let baseX = parseFloat(fishFood.dataset.baseX); 181 let offset = parseFloat(fishFood.dataset.offset); 182 183 top += FISH_FOOD_SINK_SPEED; 184 const wobble = Math.sin((Date.now() + offset) / 400) * FISH_FOOD_WIGGLE; 185 186 fishFood.style.top = `${top}px`; 187 fishFood.style.left = `${baseX + wobble}px`; 188 189 if (top > window.innerHeight - 30) { 190 fishFood.style.transition = "opacity 0.5s ease-out"; 191 fishFood.style.opacity = "0"; 192 setTimeout(() => { 193 if (fishFood) { 194 fishFood.remove(); 195 fishFood = null; 196 scheduleSpawn(randomSpawnInterval()); 197 } 198 }, 500); 199 } 200 } 201 202 checkCollision(); 203 requestAnimationFrame(animateFood); 204} 205 206// ð Initialize 207// Start animation loop immediately 208animateFood(); 209 210// Spawn first food after random initial delay (5-10 seconds) 211const initialDelay = Math.random() * 5000 + 5000; // 5000ms to 10000ms 212scheduleSpawn(initialDelay); 213 214window.FishFood = { 215 setEnabled: setFishFoodEnabled, 216 isEnabled: function () { return fishFoodEnabled; } 217};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.