1(function() { 2 var container = document.getElementById('js-footer-ducks'); 3 if (!container) return; 4 5 // ââ Constants ââ 6 var DUCK_SIZE = 36; 7 var DUCK_SVG = '<svg width="40" height="36" viewBox="0 0 40 36" fill="none" xmlns="http://www.w3.org/2000/svg">' + 8 '<ellipse cx="22" cy="26" rx="16" ry="10" fill="#FFD700"/>' + 9 '<ellipse cx="24" cy="24" rx="8" ry="6" fill="#FFC107"/>' + 10 '<ellipse cx="10" cy="18" rx="5" ry="7" fill="#FFD700"/>' + 11 '<circle cx="9" cy="11" r="7" fill="#FFD700"/>' + 12 '<circle cx="7" cy="10" r="1.5" fill="#2D2D2D"/>' + 13 '<path d="M2 13 L0 11.5 L2 10 Q4 11.5 2 13Z" fill="#FF8F0B"/>' + 14 '<path d="M36 20 L40 16 L38 22Z" fill="#FFC107"/>' + 15 '</svg>'; 16 17 var GOLDEN_DUCK_SVG = '<svg width="40" height="36" viewBox="0 0 40 36" fill="none" xmlns="http://www.w3.org/2000/svg">' + 18 '<ellipse cx="22" cy="26" rx="16" ry="10" fill="#FFD700"/>' + 19 '<ellipse cx="24" cy="24" rx="8" ry="6" fill="#FFEA00"/>' + 20 '<ellipse cx="10" cy="18" rx="5" ry="7" fill="#FFD700"/>' + 21 '<circle cx="9" cy="11" r="7" fill="#FFD700"/>' + 22 '<circle cx="7" cy="10" r="1.5" fill="#2D2D2D"/>' + 23 '<path d="M2 13 L0 11.5 L2 10 Q4 11.5 2 13Z" fill="#FF6B00"/>' + 24 '<path d="M36 20 L40 16 L38 22Z" fill="#FFEA00"/>' + 25 '<circle cx="14" cy="22" r="2" fill="#FFF8E1" opacity="0.6"/>' + 26 '<circle cx="28" cy="28" r="1.5" fill="#FFF8E1" opacity="0.4"/>' + 27 '</svg>'; 28 29 var IDLE_CONFIG = { spawnMin: 2500, spawnMax: 5000, swimMin: 12000, swimMax: 20000, maxDucks: 5 }; 30 31 var LEVELS = [ 32 { duration: 15000, spawnMin: 2500, spawnMax: 5000, swimMin: 14000, swimMax: 20000, maxDucks: 3 }, 33 { duration: 15000, spawnMin: 2000, spawnMax: 4000, swimMin: 11000, swimMax: 17000, maxDucks: 4 }, 34 { duration: 15000, spawnMin: 1500, spawnMax: 3500, swimMin: 9000, swimMax: 14000, maxDucks: 5 }, 35 { duration: 15000, spawnMin: 1200, spawnMax: 3000, swimMin: 7000, swimMax: 12000, maxDucks: 5 }, 36 { duration: 20000, spawnMin: 1000, spawnMax: 2500, swimMin: 5500, swimMax: 10000, maxDucks: 6 }, 37 { duration: 20000, spawnMin: 800, spawnMax: 2000, swimMin: 4000, swimMax: 8000, maxDucks: 7 } 38 ]; 39 40 var POINTS_PER_DUCK = 10; 41 var GOLDEN_POINTS = 50; 42 var GOLDEN_CHANCE = 0.08; // 8% chance per spawn 43 var GOLDEN_SPEED_MULTIPLIER = 0.5; // swims twice as fast 44 var STREAK_BONUS_THRESHOLD = 3; 45 var STREAK_BONUS_MULTIPLIER = 5; 46 var LEVEL_BONUS = 25; 47 var STREAK_TIMEOUT = 3000; 48 var GAME_OVER_IDLE_TIMEOUT = 8000; 49 50 // ââ State ââ 51 var state = 'IDLE'; // IDLE | PLAYING | GAME_OVER 52 var score = 0; 53 var level = 0; 54 var streak = 0; 55 var highScore = loadHighScore(); 56 var activeDucks = []; 57 var spawnTimer = null; 58 var levelTimer = null; 59 var streakTimer = null; 60 var idleReturnTimer = null; 61 var gamePaused = false; 62 var pauseTime = 0; 63 var hudEl = null; 64 var gameOverEl = null; 65 66 // ââ Audio (Web Audio API quack) ââ 67 var audioCtx = null; 68 69 function getAudioContext() { 70 if (!audioCtx) { 71 try { 72 audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 73 } catch(e) { 74 return null; 75 } 76 } 77 return audioCtx; 78 } 79 80 function playQuack(isGolden) { 81 var ctx = getAudioContext(); 82 if (!ctx) return; 83 84 var now = ctx.currentTime; 85 var duration = isGolden ? 0.18 : 0.15; 86 var freq = isGolden ? 900 : 680; 87 88 // Oscillator for the quack tone 89 var osc = ctx.createOscillator(); 90 var gain = ctx.createGain(); 91 osc.type = 'square'; 92 osc.frequency.setValueAtTime(freq, now); 93 osc.frequency.exponentialRampToValueAtTime(freq * 0.6, now + duration); 94 95 gain.gain.setValueAtTime(0.12, now); 96 gain.gain.exponentialRampToValueAtTime(0.001, now + duration); 97 98 osc.connect(gain); 99 gain.connect(ctx.destination); 100 osc.start(now); 101 osc.stop(now + duration); 102 103 // Second quack for golden duck 104 if (isGolden) { 105 var osc2 = ctx.createOscillator(); 106 var gain2 = ctx.createGain(); 107 osc2.type = 'square'; 108 osc2.frequency.setValueAtTime(freq * 1.2, now + 0.12); 109 osc2.frequency.exponentialRampToValueAtTime(freq * 0.7, now + 0.12 + duration); 110 gain2.gain.setValueAtTime(0.1, now + 0.12); 111 gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.12 + duration); 112 osc2.connect(gain2); 113 gain2.connect(ctx.destination); 114 osc2.start(now + 0.12); 115 osc2.stop(now + 0.12 + duration); 116 } 117 } 118 119 // ââ Helpers ââ 120 function randomBetween(min, max) { 121 return Math.floor(Math.random() * (max - min + 1)) + min; 122 } 123 124 function getConfig() { 125 if (state !== 'PLAYING') return IDLE_CONFIG; 126 var idx = Math.min(level, LEVELS.length - 1); 127 return LEVELS[idx]; 128 } 129 130 function loadHighScore() { 131 try { return parseInt(localStorage.getItem('duckGameHighScore'), 10) || 0; } 132 catch(e) { return 0; } 133 } 134 135 function saveHighScore(val) { 136 try { localStorage.setItem('duckGameHighScore', val); } 137 catch(e) {} 138 } 139 140 // ââ HUD ââ 141 function createHUD() { 142 hudEl = document.createElement('div'); 143 hudEl.className = 'duck-game-hud'; 144 hudEl.innerHTML = 145 '<span class="duck-game-hud__score">Score: 0</span>' + 146 '<span class="duck-game-hud__level">Level 1</span>' + 147 '<span class="duck-game-hud__streak"></span>'; 148 container.appendChild(hudEl); 149 hudEl.offsetHeight; 150 hudEl.classList.add('duck-game-hud--visible'); 151 } 152 153 function updateHUD() { 154 if (!hudEl) return; 155 hudEl.querySelector('.duck-game-hud__score').textContent = 'Score: ' + score; 156 hudEl.querySelector('.duck-game-hud__level').textContent = 'Level ' + (level + 1); 157 var streakEl = hudEl.querySelector('.duck-game-hud__streak'); 158 streakEl.textContent = streak >= STREAK_BONUS_THRESHOLD ? streak + 'x streak!' : ''; 159 } 160 161 function flashLevel() { 162 if (!hudEl) return; 163 var levelEl = hudEl.querySelector('.duck-game-hud__level'); 164 levelEl.classList.add('duck-game-hud__level--flash'); 165 setTimeout(function() { 166 levelEl.classList.remove('duck-game-hud__level--flash'); 167 }, 400); 168 } 169 170 function removeHUD() { 171 if (hudEl && hudEl.parentNode) { 172 hudEl.parentNode.removeChild(hudEl); 173 hudEl = null; 174 } 175 } 176 177 // ââ Game Over Overlay ââ 178 function showGameOver() { 179 var isNewHighScore = score > highScore; 180 if (isNewHighScore) { 181 highScore = score; 182 saveHighScore(highScore); 183 } 184 185 gameOverEl = document.createElement('div'); 186 gameOverEl.className = 'duck-game-over'; 187 gameOverEl.innerHTML = 188 '<span>Game Over!</span>' + 189 '<span>
189Score: ' + score + '</span>' + 190 '<span>' + (isNewHighScore ? 'Nieuw record!' : 'Record: ' + highScore) + '</span>' + 191 '<button class="duck-game-over__btn">Opnieuw!</button>'; 192 container.appendChild(gameOverEl); 193 194 gameOverEl.querySelector('.duck-game-over__btn').addEventListener('click', function(e) { 195 e.stopPropagation(); 196 removeGameOver(); 197 startGame(0); 198 }); 199 200 idleReturnTimer = setTimeout(function() { 201 returnToIdle(); 202 }, GAME_OVER_IDLE_TIMEOUT); 203 } 204 205 function removeGameOver() { 206 clearTimeout(idleReturnTimer); 207 idleReturnTimer = null; 208 if (gameOverEl && gameOverEl.parentNode) { 209 gameOverEl.parentNode.removeChild(gameOverEl); 210 gameOverEl = null; 211 } 212 } 213 214 // ââ Points Popup ââ 215 function showPointsPopup(x, y, points, isGolden) { 216 var popup = document.createElement('div'); 217 popup.className = 'duck-points-popup'; 218 popup.textContent = '+' + points; 219 if (isGolden) { 220 popup.style.color = '#FFD700'; 221 popup.style.fontSize = '18px'; 222 } 223 popup.style.left = x + 'px'; 224 popup.style.bottom = (container.offsetHeight - y) + 'px'; 225 container.appendChild(popup); 226 setTimeout(function() { 227 if (popup.parentNode) popup.parentNode.removeChild(popup); 228 }, 850); 229 } 230 231 // ââ Bubbles ââ 232 function spawnBubbles(x, bottomY) { 233 var count = randomBetween(3, 6); 234 for (var i = 0; i < count; i++) { 235 var bubble = document.createElement('div'); 236 bubble.className = 'duck-bubble'; 237 bubble.style.left = (x + randomBetween(-12, 12)) + 'px'; 238 bubble.style.bottom = (bottomY + randomBetween(0, 8)) + 'px'; 239 bubble.style.width = bubble.style.height = randomBetween(4, 8) + 'px'; 240 bubble.style.animationDelay = (i * 0.08) + 's'; 241 container.appendChild(bubble); 242 (function(b) { 243 setTimeout(function() { 244 if (b.parentNode) b.parentNode.removeChild(b); 245 }, 800); 246 })(bubble); 247 } 248 } 249 250 // ââ Duck Spawning ââ 251 function spawnDuck() { 252 var config = getConfig(); 253 var gameActiveDucks = activeDucks.filter(function(d) { return !d.caught; }); 254 if (gameActiveDucks.length >= config.maxDucks) return; 255 256 var isGolden = state === 'PLAYING' && Math.random() < GOLDEN_CHANCE; 257 258 var duck = document.createElement('div'); 259 duck.className = 'footer-duck'; 260 if (isGolden) duck.classList.add('footer-duck--golden'); 261 duck.innerHTML = isGolden ? GOLDEN_DUCK_SVG : DUCK_SVG; 262 263 var swimDuration = randomBetween(config.swimMin, config.swimMax); 264 if (isGolden) swimDuration = Math.round(swimDuration * GOLDEN_SPEED_MULTIPLIER); 265 266 var containerWidth = container.offsetWidth; 267 var startX = -DUCK_SIZE; 268 var endX = containerWidth + DUCK_SIZE; 269 270 duck.style.left = startX + 'px'; 271 duck.style.animationDelay = randomBetween(0, 2000) + 'ms'; 272 273 container.appendChild(duck); 274 275 var duckData = { 276 el: duck, 277 startTime: null, 278 swimDuration: swimDuration, 279 startX: startX, 280 endX: endX, 281 caught: false, 282 golden: isGolden, 283 animId: null 284 }; 285 activeDucks.push(duckData); 286 287 function animate(timestamp) { 288 if (gamePaused) { 289 duckData.animId = requestAnimationFrame(animate); 290 return; 291 } 292 if (!duckData.startTime) duckData.startTime = timestamp; 293 var progress = (timestamp - duckData.startTime) / swimDuration; 294 295 if (duckData.caught || !duck.parentNode) { 296 removeDuckData(duckData); 297 return; 298 } 299 300 if (progress >= 1) { 301 duck.parentNode.removeChild(duck); 302 removeDuckData(duckData); 303 if (state === 'PLAYING') { 304 endGame(); 305 } 306 return; 307 } 308 309 duck.style.left = (startX + (endX - startX) * progress) + 'px'; 310 duckData.animId = requestAnimationFrame(animate); 311 } 312 313 duckData.animId = requestAnimationFrame(animate); 314 315 duck.addEventListener('click', function(e) { 316 e.preventDefault(); 317 e.stopPropagation(); 318 if (duckData.caught) return; 319 duckData.caught = true; 320 duck.classList.add('footer-duck--caught'); 321 322 playQuack(duckData.golden); 323 324 if (state === 'IDLE') { 325 startGame(duckData.golden ? GOLDEN_POINTS : POINTS_PER_DUCK); 326 } else if (state === 'PLAYING') { 327 catchDuck(duckData, e); 328 } 329 330 playCatchAnimation(duck, duckData); 331 }); 332 } 333 334 function catchDuck(duckData, event) { 335 streak++; 336 clearTimeout(streakTimer); 337 streakTimer = setTimeout(function() { 338 streak = 0; 339 updateHUD(); 340 }, STREAK_TIMEOUT); 341 342 var points = duckData.golden ? GOLDEN_POINTS : POINTS_PER_DUCK; 343 if (streak >= STREAK_BONUS_THRESHOLD) { 344 points += streak * STREAK_BONUS_MULTIPLIER; 345 } 346 score += points; 347 updateHUD(); 348 349 var rect = duckData.el.getBoundingClientRect(); 350 var containerRect = container.getBoundingClientRect(); 351 showPointsPopup( 352 rect.left - containerRect.left + DUCK_SIZE / 2, 353 rect.top - containerRect.top, 354 points, 355 duckData.golden 356 ); 357 } 358 359 function playCatchAnimation(duck, duckData) { 360 var catchStart = null; 361 var startBottom = parseFloat(getComputedStyle(duck).bottom) || -14; 362 363 function catchAnimate(timestamp) { 364 if (!catchStart) catchStart = timestamp;
365 var elapsed = timestamp - catchStart; 366 var duration = 900; 367 var progress = Math.min(elapsed / duration, 1); 368 369 var y; 370 if (progress < 0.35) { 371 var p = progress / 0.35; 372 y = startBottom + (28 * p); 373 } else { 374 var p = (progress - 0.35) / 0.65; 375 y = (startBottom + 28) - (70 * p * p); 376 } 377 378 duck.style.bottom = y + 'px'; 379 380 // Spawn bubbles when duck goes below water line 381 if (progress > 0.6 && progress < 0.65) { 382 var duckLeft = parseFloat(duck.style.left) || 0; 383 spawnBubbles(duckLeft + DUCK_SIZE / 2, 8); 384 } 385 386 if (progress < 1) { 387 requestAnimationFrame(catchAnimate); 388 } else { 389 if (duck.parentNode) duck.parentNode.removeChild(duck); 390 removeDuckData(duckData); 391 } 392 } 393 394 requestAnimationFrame(catchAnimate); 395 } 396 397 function removeDuckData(duckData) { 398 var idx = activeDucks.indexOf(duckData); 399 if (idx !== -1) activeDucks.splice(idx, 1); 400 } 401 402 function removeAllDucks() { 403 for (var i = activeDucks.length - 1; i >= 0; i--) { 404 var d = activeDucks[i]; 405 if (d.animId) cancelAnimationFrame(d.animId); 406 if (d.el.parentNode) d.el.parentNode.removeChild(d.el); 407 } 408 activeDucks = []; 409 } 410 411 function scheduleSpawn() { 412 clearTimeout(spawnTimer); 413 var config = getConfig(); 414 var delay = randomBetween(config.spawnMin, config.spawnMax); 415 spawnTimer = setTimeout(function() { 416 if (state !== 'GAME_OVER') { 417 spawnDuck(); 418 scheduleSpawn(); 419 } 420 }, delay); 421 } 422 423 // ââ Game Flow ââ 424 function startGame(initialScore) { 425 state = 'PLAYING'; 426 score = initialScore; 427 level = 0; 428 streak = 0; 429 430 removeGameOver(); 431 createHUD(); 432 updateHUD(); 433 scheduleSpawn(); 434 scheduleLevelUp(); 435 } 436 437 function scheduleLevelUp() { 438 clearTimeout(levelTimer); 439 var config = getConfig(); 440 levelTimer = setTimeout(function() { 441 if (state !== 'PLAYING') return; 442 level++; 443 score += LEVEL_BONUS; 444 updateHUD(); 445 flashLevel(); 446 scheduleSpawn(); 447 scheduleLevelUp(); 448 }, config.duration); 449 } 450 451 function endGame() { 452 state = 'GAME_OVER'; 453 454 clearTimeout(spawnTimer); 455 clearTimeout(levelTimer); 456 clearTimeout(streakTimer); 457 spawnTimer = null; 458 levelTimer = null; 459 streakTimer = null; 460 461 removeAllDucks(); 462 removeHUD(); 463 showGameOver(); 464 } 465 466 function returnToIdle() { 467 state = 'IDLE'; 468 removeGameOver(); 469 removeAllDucks(); 470 removeHUD(); 471 472 setTimeout(function() { 473 spawnDuck(); 474 scheduleSpawn(); 475 }, 1000); 476 } 477 478 // ââ Visibility change (pause/resume) ââ 479 document.addEventListener('visibilitychange', function() { 480 if (state !== 'PLAYING') return; 481 482 if (document.hidden) { 483 gamePaused = true; 484 pauseTime = performance.now(); 485 clearTimeout(spawnTimer); 486 clearTimeout(levelTimer); 487 clearTimeout(streakTimer); 488 } else { 489 gamePaused = false; 490 var pausedFor = performance.now() - pauseTime; 491 for (var i = 0; i < activeDucks.length; i++) { 492 if (activeDucks[i].startTime) { 493 activeDucks[i].startTime += pausedFor; 494 } 495 } 496 scheduleSpawn(); 497 scheduleLevelUp(); 498 } 499 }); 500 501 // ââ Init (IDLE) ââ 502 setTimeout(function() { 503 spawnDuck(); 504 scheduleSpawn(); 505 }, 2000); 506})();
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.