📱

Gire o celular para
o modo paisagem

Logo
Desvie dos tubarões, cardumes e águas-vivas
Pegue a Calça Jeans para nadar super rápido!
Colete os itens para ganhar pontos bônus
TEMPO
2:00
FASE
01
PONTOS
0000
JEANS POWER
🫧 BOLHAS DE AR
FASE 1
Recife Colorido
let W = window.innerWidth; let H = window.innerHeight; // ===== SCREEN DIMENSIONS ===== // Always use the actual window dimensions — no aspect-ratio clamping // The canvas fills the screen and the game renders to full size function getGameDimensions() { return { width: window.innerWidth, height: window.innerHeight }; } const gameCanvas = document.getElementById('canvas'); const ctx = gameCanvas.getContext('2d'); function updateCanvasSize() { const dims = getGameDimensions(); W = dims.width; H = dims.height; gameCanvas.width = W; gameCanvas.height = H; const winCanvas = document.getElementById('winCanvas'); if (winCanvas) { winCanvas.width = W; winCanvas.height = H; } } updateCanvasSize(); // ===== SEA SURFACE CANVAS (HUD background) ===== const seaSurfaceCanvas = document.getElementById('seaSurfaceCanvas'); const seaCtx = seaSurfaceCanvas.getContext('2d'); let seaWaveOffset = 0; const SEA_SURFACE_HEIGHT = 44; function updateSeaSurfaceSize() { seaSurfaceCanvas.width = window.innerWidth; seaSurfaceCanvas.height = SEA_SURFACE_HEIGHT + 32; } updateSeaSurfaceSize(); window.addEventListener('resize', updateSeaSurfaceSize); function drawSeaSurface() { const sw = seaSurfaceCanvas.width; const sh = seaSurfaceCanvas.height; seaCtx.clearRect(0, 0, sw, sh); const hudH = 32; const waveY = hudH; seaWaveOffset = (seaWaveOffset + 0.6) % sw; const seaGrad = seaCtx.createLinearGradient(0, waveY, 0, sh); seaGrad.addColorStop(0, 'rgba(0,140,220,0.82)'); seaGrad.addColorStop(0.4, 'rgba(0,80,160,0.65)'); seaGrad.addColorStop(1, 'rgba(0,40,100,0)'); seaCtx.fillStyle = seaGrad; seaCtx.beginPath(); seaCtx.moveTo(0, waveY); for (let x = 0; x <= sw; x += 6) { const w1 = Math.sin((x + seaWaveOffset) * 0.022) * 4; const w2 = Math.sin((x - seaWaveOffset * 0.7) * 0.05) * 2; seaCtx.lineTo(x, waveY + w1 + w2); } seaCtx.lineTo(sw, sh); seaCtx.lineTo(0, sh); seaCtx.closePath(); seaCtx.fill(); seaCtx.strokeStyle = 'rgba(180,240,255,0.55)'; seaCtx.lineWidth = 2; seaCtx.beginPath(); for (let x = 0; x <= sw; x += 6) { const w1 = Math.sin((x + seaWaveOffset) * 0.022) * 4; const w2 = Math.sin((x - seaWaveOffset * 0.7) * 0.05) * 2; if (x === 0) seaCtx.moveTo(x, waveY + w1 + w2); else seaCtx.lineTo(x, waveY + w1 + w2); } seaCtx.stroke(); for (let i = 0; i < 7; i++) { const bx = ((i * 167 + seaWaveOffset * 2) % sw); const by = waveY + Math.sin((bx + seaWaveOffset) * 0.022) * 4 + Math.sin((bx - seaWaveOffset * 0.7) * 0.05) * 2; const br = 1.5 + (i % 3) * 0.8; seaCtx.beginPath(); seaCtx.arc(bx, by - br, br, 0, Math.PI * 2); seaCtx.fillStyle = 'rgba(220,255,255,0.5)'; seaCtx.fill(); } } function animateSeaSurface() { drawSeaSurface(); requestAnimationFrame(animateSeaSurface); } animateSeaSurface(); function requestFullscreen() { const el = document.documentElement; if (el.requestFullscreen) el.requestFullscreen().catch(() => {}); else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen(); else if (el.mozRequestFullScreen) el.mozRequestFullScreen(); else if (el.msRequestFullscreen) el.msRequestFullscreen(); } // Try fullscreen on first interaction (required by browsers) document.getElementById('startBtn').addEventListener('click', () => { requestFullscreen(); }, { once: true }); // Resize canvas dynamically function resizeGame() { updateCanvasSize(); const winCanvas = document.getElementById('winCanvas'); if (winCanvas) { winCanvas.width = W; winCanvas.height = H; } // Recalculate sand height for new dimensions SAND_HEIGHT = getSandHeight(); SAND_Y = H - SAND_HEIGHT; } window.addEventListener('resize', resizeGame); window.addEventListener('orientationchange', () => { setTimeout(resizeGame, 200); }); // ===== MUSIC SYSTEM ===== const audioTracks = {}; let currentTrack = null; function loadAudio(name, path, loop = true) { const audio = new Audio(path); audio.loop = loop; audio.volume = 0.5; audioTracks[name] = audio; return audio; } function playTrack(name) { const track = audioTracks[name]; if (!track) return; if (currentTrack === track && !currentTrack.paused) return; if (currentTrack) { currentTrack.pause(); currentTrack.currentTime = 0; } currentTrack = track; currentTrack.currentTime = 0; currentTrack.play().catch(() => { // Retry on next user gesture const retry = () => { if (currentTrack === track) track.play().catch(()=>{}); }; document.addEventListener('click', retry, { once: true }); document.addEventListener('touchstart', retry, { once: true }); }); } // Load all tracks loadAudio('inicio', 'assets/musicas/musica_inicio.mp3', true); loadAudio('gameplay1', 'assets/musicas/musica_gameplay1.mp3', true); loadAudio('gameplay2', 'assets/musicas/musica_gameplay2.mp3', true); loadAudio('gameplay3', 'assets/musicas/musica_gameplay3.mp3', true); loadAudio('gameover', 'assets/musicas/musica_gameover.mp3', true); // game over plays once loadAudio('final', 'assets/musicas/musica_final.mp3', true); // Start screen music on first user interaction function tryPlayStartMusic() { if (gameState === 'start') { if (!currentTrack || currentTrack.paused) { playTrack('inicio'); } } } document.addEventListener('click', tryPlayStartMusic); document.addEventListener('touchstart', tryPlayStartMusic); // Also try to play when window gets focus window.addEventListener('focus', () => { if (gameState === 'start' && (!currentTrack || currentTrack.paused)) { playTrack('inicio'); } }); // ===== GAME STATE ===== let gameState = 'start'; let score = 0, stars = 0, phase = 1; let gameSpeed = 1; let phaseStartTime = 0; const keys = {}; let animFrame; let lastTime = 0; // Air bubble bar let airBubbles = 100; // 0-100 const AIR_DRAIN_RATE = 3.5; // per second const AIR_REFILL_AMOUNT = 22; // per bubble item collected const AIR_MAX = 100; let airWarningTriggered = false; // tracks if 5-second warning has been shown const PHASES = [ { name: 'FASE 1', sub: 'Recife Colorido', bgColors: ['#001830','#002a50','#003f70'], coralColors: ['#ff6b6b','#ffa07a','#ff7043','#4db6ac'], music: 'gameplay1' }, { name: 'FASE 2', sub: 'Cavernas Profundas', bgColors: ['#0a0020','#150035','#1a004a'], coralColors: ['#9c27b0','#673ab7','#e040fb','#7e57c2'], music: 'gameplay2' }, { name: 'FASE 3', sub: 'Abismo Bioluminescente', bgColors: ['#001a10','#003020','#004030'], coralColors: ['#00e676','#69f0ae','#00bfa5','#1de9b6'], music: 'gameplay3' }, ]; const player = { x: W * 0.2, y: H / 2, vx: 0, vy: 0, w: 40, h: 24, hasJeans: false, shielded: 0, tailAngle: 0, // Physics - inertia based maxSpeed: 120, jeansMaxSpeed: 280, accel: 260, jeansAccel: 700, friction: 0.18, jeansFriction: 0.04, // Blink state when jeans lost blinking: false, blinkTimer: 0, blinkVisible: true, // Sprite bubbles spriteBubbles: [], bubbleTimer: 0, }; let obstacles = []; let collectibles = []; let coralDecor = []; let bubbles = []; let sharkWarning = null; // Sand scrolling offset let sandOffset = 0; const spawnTimers = { school: 0, shark: 0, jellyfish: 0, jeans: 0, star: 0, coral: 0, bubble: 0, crab: 0, lobster: 0, octopus: 0, stingray: 0, anglerfish: 0, medusa: 0, eel: 0, bubbleItem: 0, goldenStar: 0 }; // ===== SAND FLOOR HEIGHT ===== // Sand floor starts at H - sandHeight and corals grow from there // Sand should be 10% of screen height for consistency function getSandHeight() { return Math.max(50, Math.floor(H * 0.10)); // Min 50px, 10% of height } let SAND_HEIGHT = getSandHeight(); let SAND_Y = H - SAND_HEIGHT; // Plants (corals) base is at SAND_Y function getSandY() { return H - getSandHeight(); } function createCoral(x) { return { x, y: getSandY(), w: 3 + Math.random() * 3, h: 40 + Math.random() * 50, color: PHASES[phase - 1].coralColors[Math.floor(Math.random() * PHASES[phase - 1].coralColors.length)], sway: Math.random() * Math.PI * 2, swaySpeed: 0.5 + Math.random() * 1, branches: 2 + Math.floor(Math.random() * 3), }; } function createBubble() { return { x: Math.random() * W, y: getSandY() + Math.random() * 60, r: 2 + Math.random() * 5, speed: 20 + Math.random() * 40, wobble: Math.random() * Math.PI * 2, }; } // ===== COLLECTIBLE CLASS ===== class Collectible { constructor(type, x, y, vx) { this.type = type; // 'jeans', 'star', 'golden_star', 'bubble_item' this.x = x; this.y = y; this.vx = vx; this.w = 24; this.h = 24; this.alive = true; this.spin = 0; this.floatPhase = Math.random() * Math.PI * 2; this.glowAngle = 0; // Movement for stars this.bobAmp = 8 + Math.random() * 8; this.bobSpeed = 1.5 + Math.random() * 1; // Horizontal wobble for golden star this.hWobble = 0; this.hWobbleSpeed = type === 'golden_star' ? (2 + Math.random() * 2) : 0; } update(dt) { this.x += this.vx * dt; this.spin += (this.type === 'golden_star' ? 4 : 2) * dt; this.floatPhase += this.bobSpeed * dt; this.hWobble += this.hWobbleSpeed * dt; if (this.x < -50 || this.x > W + 50) this.alive = false; } draw() { const yOff = Math.sin(this.floatPhase) * this.bobAmp; const xOff = this.type === 'golden_star' ? Math.sin(this.hWobble) * 12 : 0; ctx.save(); ctx.translate(this.x + xOff, this.y + yOff); ctx.rotate(this.spin); if (this.type === 'jeans') { this.glowAngle = (this.glowAngle || 0) + 0.05; const grd = ctx.createRadialGradient(0, 0, 5, 0, 0, 35); grd.addColorStop(0, 'rgba(0,150,255,0.3)'); grd.addColorStop(1, 'rgba(0,100,255,0)'); ctx.beginPath(); ctx.arc(0, 0, 35, 0, Math.PI * 2); ctx.fillStyle = grd; ctx.fill(); ctx.beginPath(); ctx.roundRect(-22, -16, 44, 32, 5); ctx.fillStyle = '#1565c0'; ctx.fill(); ctx.beginPath(); ctx.moveTo(0, -15); ctx.lineTo(0, 15); ctx.strokeStyle = '#90caf9'; ctx.lineWidth = 3; ctx.stroke(); for (let side of [-1, 1]) { ctx.beginPath(); ctx.roundRect(side * 3, -13, 16 * side > 0 ? 16 : -16, 12, 2); ctx.strokeStyle = '#90caf9'; ctx.lineWidth = 1.5; ctx.stroke(); } for (let i = 0; i < 4; i++) { const a = this.spin + i * Math.PI / 2; ctx.beginPath(); ctx.arc(Math.cos(a) * 28, Math.sin(a) * 20, 2.5, 0, Math.PI * 2); ctx.fillStyle = `rgba(100,200,255,${0.4 + Math.sin(this.spin * 2 + i) * 0.2})`; ctx.fill(); } } else if (this.type === 'star') { // Normal star - yellow ctx.shadowColor = 'rgba(255,215,0,0.8)'; ctx.shadowBlur = 10; ctx.fillStyle = '#ffd700'; ctx.beginPath(); for (let i = 0; i < 10; i++) { const r = i % 2 === 0 ? 13 : 6; const a = (i * Math.PI) / 5 - Math.PI / 2; if (i === 0) ctx.moveTo(Math.cos(a) * r, Math.sin(a) * r); else ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r); } ctx.closePath(); ctx.fill(); ctx.shadowBlur = 0; ctx.strokeStyle = '#ff8f00'; ctx.lineWidth = 1; ctx.stroke(); ctx.beginPath(); ctx.arc(-4, -4, 3, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.fill(); } else if (this.type === 'golden_star') { // Golden star - bigger, brighter, sparkles const pulse = Math.abs(Math.sin(this.floatPhase * 2)) * 0.4 + 0.8; ctx.shadowColor = 'rgba(255,180,0,1)'; ctx.shadowBlur = 20 * pulse; // Outer glow const grd = ctx.createRadialGradient(0, 0, 5, 0, 0, 28); grd.addColorStop(0, 'rgba(255,220,0,0.4)'); grd.addColorStop(1, 'rgba(255,150,0,0)'); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(0, 0, 28, 0, Math.PI * 2); ctx.fill(); // Star shape ctx.fillStyle = `rgb(255, ${Math.floor(180 + pulse * 40)}, 0)`; ctx.beginPath(); for (let i = 0; i < 10; i++) { const r = i % 2 === 0 ? 17 : 8; const a = (i * Math.PI) / 5 - Math.PI / 2; if (i === 0) ctx.moveTo(Math.cos(a) * r, Math.sin(a) * r); else ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r); } ctx.closePath(); ctx.fill(); ctx.shadowBlur = 0; ctx.strokeStyle = '#ffaa00'; ctx.lineWidth = 1.5; ctx.stroke(); // Sparkle dots for (let i = 0; i < 4; i++) { const sa = this.glowAngle + i * Math.PI / 2; ctx.beginPath(); ctx.arc(Math.cos(sa) * 22, Math.sin(sa) * 22, 2, 0, Math.PI * 2); ctx.fillStyle = `rgba(255,255,200,${0.5 + Math.sin(this.glowAngle * 3 + i) * 0.3})`; ctx.fill(); } this.glowAngle += 0.05; // "5x" label ctx.shadowBlur = 0; ctx.save(); ctx.rotate(-this.spin); ctx.font = 'bold 9px Roboto, sans-serif'; ctx.fillStyle = '#fff'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('5×', 0, 0); ctx.restore(); } else if (this.type === 'bubble_item') { // Collectible air bubble const s = 13; const bGlow = ctx.createRadialGradient(0, 0, 2, 0, 0, s + 6); bGlow.addColorStop(0, 'rgba(150,240,255,0.35)'); bGlow.addColorStop(1, 'rgba(0,200,255,0)'); ctx.fillStyle = bGlow; ctx.beginPath(); ctx.arc(0, 0, s + 6, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(0, 0, s, 0, Math.PI * 2); ctx.fillStyle = 'rgba(150,220,255,0.18)'; ctx.fill(); ctx.strokeStyle = 'rgba(200,240,255,0.75)'; ctx.lineWidth = 1.8; ctx.stroke(); ctx.beginPath(); ctx.arc(-s * 0.3, -s * 0.3, s * 0.28, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,0.55)'; ctx.fill(); // tiny inner reflection ctx.beginPath(); ctx.arc(s * 0.25, s * 0.25, s * 0.15, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.fill(); } ctx.restore(); } isCollidingWith(px, py, pw, ph) { const yOff = Math.sin(this.floatPhase) * this.bobAmp; const xOff = this.type === 'golden_star' ? Math.sin(this.hWobble) * 12 : 0; const cx = this.x + xOff; const cy = this.y + yOff; const hw = this.type === 'bubble_item' ? 14 : this.w / 2; const hh = this.type === 'bubble_item' ? 14 : this.h / 2; return !(px + pw < cx - hw || px > cx + hw || py + ph < cy - hh || py > cy + hh); } } // ===== OBSTACLE CLASS ===== class Obstacle { constructor(type, x, y, vx, vy, w, h) { this.type = type; this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.w = w; this.h = h; this.alive = true; this.tailAngle = 0; this.tentaclePhase = 0; } update(dt) { this.x += this.vx * dt; this.y += this.vy * dt; if (this.x < -150 || this.x > W + 150 || this.y < -100 || this.y > H + 100) { this.alive = false; } } draw() { ctx.save(); ctx.translate(this.x, this.y); if (this.type === 'shark') { this.tailAngle += 0.12; const tail = Math.sin(this.tailAngle) * 14; ctx.scale(-1, 1); ctx.beginPath(); ctx.moveTo(45, 0); ctx.bezierCurveTo(40, -22, -30, -22, -45, -4); ctx.bezierCurveTo(-50, 0, -45, 4, -40, 6); ctx.bezierCurveTo(-30, 22, 30, 20, 40, 6); ctx.closePath(); ctx.fillStyle = '#4a7c8f'; ctx.fill(); ctx.beginPath(); ctx.ellipse(0, 6, 36, 12, 0, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,0.15)'; ctx.fill(); ctx.beginPath(); ctx.moveTo(-38, 0); ctx.lineTo(-58, -18 + tail); ctx.lineTo(-52, 0); ctx.lineTo(-58, 16 - tail); ctx.closePath(); ctx.fillStyle = '#4a7c8f'; ctx.fill(); ctx.beginPath(); ctx.moveTo(0, -18); ctx.lineTo(-10, -38); ctx.lineTo(-22, -18); ctx.closePath(); ctx.fillStyle = '#4a7c8f'; ctx.fill(); ctx.beginPath(); ctx.moveTo(10, 2); ctx.lineTo(20, 22); ctx.lineTo(0, 10); ctx.closePath(); ctx.fillStyle = '#4a7c8f'; ctx.fill(); ctx.beginPath(); ctx.arc(28, -8, 6, 0, Math.PI * 2); ctx.fillStyle = '#eee'; ctx.fill(); ctx.beginPath(); ctx.arc(29, -8, 3.5, 0, Math.PI * 2); ctx.fillStyle = '#111'; ctx.fill(); ctx.beginPath(); ctx.arc(28, -9.5, 1.2, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); ctx.beginPath(); ctx.moveTo(40, -2); for (let i = 0; i < 5; i++) { ctx.lineTo(40 - i * 6, i % 2 === 0 ? 8 : 2); } ctx.strokeStyle = '#eee'; ctx.lineWidth = 1.5; ctx.stroke(); } else if (this.type === 'fish') { ctx.scale(-1, 1); this.tailAngle = (this.tailAngle || 0) + 0.1; const tail = Math.sin(this.tailAngle) * 10; ctx.beginPath(); ctx.ellipse(0, 0, 18, 10, 0, 0, Math.PI * 2); ctx.fillStyle = '#2d7a8a'; ctx.fill(); ctx.beginPath(); ctx.moveTo(16, 0); ctx.lineTo(26, -6 + tail * 0.5); ctx.lineTo(26, 6 - tail * 0.5); ctx.closePath(); ctx.fillStyle = '#1a4d58'; ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(6, -4, 3, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(6, -4, 1.5, 0, Math.PI * 2); ctx.fill(); } else if (this.type === 'jellyfish') { this.tentaclePhase += 0.06; ctx.beginPath(); ctx.arc(0, 0, 18, Math.PI, 0); ctx.fillStyle = '#e040fb'; ctx.fill(); ctx.beginPath(); ctx.arc(0, 0, 12, Math.PI, 0); ctx.fillStyle = 'rgba(255,255,255,0.18)'; ctx.fill(); ctx.beginPath(); ctx.arc(0, 0, 18, Math.PI, 0); ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 2; ctx.stroke(); for (let i = 0; i < 5; i++) { const tx = -14 + i * 7; const wave = Math.sin(this.tentaclePhase + i * 0.8) * 8; ctx.beginPath(); ctx.moveTo(tx, 2); ctx.bezierCurveTo(tx + wave, 8, tx - wave, 16, tx + wave * 0.5, 24); ctx.strokeStyle = '#e040fb'; ctx.lineWidth = 2.5; ctx.globalAlpha = 0.7; ctx.stroke(); ctx.globalAlpha = 1; } } else if (this.type === 'crab') { ctx.fillStyle = '#d97e2a'; ctx.beginPath(); ctx.ellipse(0, -2, 24, 16, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#c67427'; ctx.lineWidth = 1; for (let i = -2; i <= 2; i++) { ctx.beginPath(); ctx.moveTo(i * 8, -12); ctx.lineTo(i * 8, 12); ctx.stroke(); } ctx.strokeStyle = '#d97e2a'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-8, -12); ctx.lineTo(-8, -20); ctx.stroke(); ctx.beginPath(); ctx.moveTo(8, -12); ctx.lineTo(8, -20); ctx.stroke(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(-8, -22, 3, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(8, -22, 3, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#d97e2a'; ctx.lineWidth = 6; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(-16, 2); ctx.quadraticCurveTo(-28, -2, -32, 2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(16, 2); ctx.quadraticCurveTo(28, -2, 32, 2); ctx.stroke(); ctx.strokeStyle = '#c67427'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(-32, 2); ctx.lineTo(-37, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-32, 2); ctx.lineTo(-37, 4); ctx.stroke(); ctx.beginPath(); ctx.moveTo(32, 2); ctx.lineTo(37, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(32, 2); ctx.lineTo(37, 4); ctx.stroke(); ctx.lineWidth = 3; ctx.lineCap = 'round'; for (let i = 0; i < 6; i++) { const side = i < 3 ? -1 : 1; const y = -4 + (i % 3) * 4; ctx.beginPath(); ctx.moveTo(side * 20, y); ctx.lineTo(side * 32, y + 12); ctx.stroke(); } } else if (this.type === 'octopus') { this.tentaclePhase = (this.tentaclePhase || 0) + 0.06; ctx.fillStyle = 'rgba(180,80,180,0.8)'; ctx.beginPath(); ctx.arc(0, -4, 18, 0, Math.PI * 2); ctx.fill(); for (let i = 0; i < 4; i++) { const angle = i * Math.PI / 2; const tx = Math.cos(angle) * 12; const ty = Math.sin(angle) * 12 + 4; ctx.strokeStyle = 'rgba(180,80,180,0.7)'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(tx * 0.7, ty * 0.7); ctx.quadraticCurveTo(tx, ty + 8 + Math.sin(this.tentaclePhase + i) * 4, tx * 0.9, ty + 20); ctx.stroke(); } ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(-6, -4, 4, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(6, -4, 4, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(-6, -4, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(6, -4, 2, 0, Math.PI * 2); ctx.fill(); } else if (this.type === 'lobster') { ctx.fillStyle = '#b85c1a'; ctx.beginPath(); ctx.ellipse(0, 0, 22, 14, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#8b4513'; ctx.lineWidth = 3; for (let i = 1; i <= 3; i++) { ctx.beginPath(); ctx.arc(0, -i * 8, 6 + i, 0, Math.PI * 2); ctx.stroke(); } ctx.strokeStyle = '#b85c1a'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(-12, -6); ctx.lineTo(-24, -12); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-12, 6); ctx.lineTo(-24, 12); ctx.stroke(); } else if (this.type === 'medusa') { this.tentaclePhase = (this.tentaclePhase || 0) + 0.05; ctx.fillStyle = 'rgba(150,50,255,0.7)'; ctx.beginPath(); ctx.arc(0, -8, 14, 0, Math.PI * 2); ctx.fill(); for (let i = 0; i < 5; i++) { const tx = -14 + i * 7; const wave = Math.sin(this.tentaclePhase + i * 0.8) * 6; ctx.beginPath(); ctx.moveTo(tx, -8); ctx.bezierCurveTo(tx + wave, 4, tx - wave, 16, tx + wave * 0.5, 35); ctx.strokeStyle = 'rgba(150,50,255,0.6)'; ctx.lineWidth = 2; ctx.stroke(); } } else if (this.type === 'stingray') { ctx.fillStyle = '#5a7a7a'; ctx.beginPath(); ctx.ellipse(0, 0, 30, 20, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#5a7a7a'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(20, 0); ctx.quadraticCurveTo(40, -5, 45, 0); ctx.stroke(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(-8, -6, 4, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(8, -6, 4, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(-8, -6, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(8, -6, 2, 0, Math.PI * 2); ctx.fill(); } else if (this.type === 'anglerfish') { const glow = Math.abs(Math.sin(performance.now() * 0.006)) * 0.6 + 0.4; ctx.fillStyle = '#1a1a2e'; ctx.beginPath(); ctx.ellipse(0, 0, 24, 18, 0, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = `rgba(255,200,0,${glow * 0.8})`; ctx.beginPath(); ctx.arc(15, -10, 8, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = `rgba(255,200,0,${glow})`; ctx.beginPath(); ctx.arc(15, -10, 4, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#ffff00'; ctx.beginPath(); ctx.arc(8, -8, 5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(8, -8, 2.5, 0, Math.PI * 2); ctx.fill(); } else if (this.type === 'eel') { const glow = Math.sin(performance.now() * 0.01) * 0.5 + 0.5; ctx.shadowColor = `rgba(0,255,200,${glow * 0.7})`; ctx.shadowBlur = 15; ctx.fillStyle = `rgba(0,150,150,${0.7 + glow * 0.3})`; ctx.beginPath(); ctx.moveTo(-25, -8); ctx.quadraticCurveTo(0, -15, 15, -8); ctx.quadraticCurveTo(20, 0, 15, 8); ctx.quadraticCurveTo(0, 15, -25, 8); ctx.closePath(); ctx.fill(); ctx.fillStyle = `rgba(0,255,200,${glow})`; ctx.beginPath(); ctx.arc(20, 0, 5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(-15, -4, 4, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(-15, -4, 2, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } isCollidingWith(px, py, pw, ph) { return !(px + pw < this.x - this.w / 2 || px > this.x + this.w / 2 || py + ph < this.y - this.h / 2 || py > this.y + this.h / 2); } } // ===== SPAWN ===== // Shark intervals: phase1=10s, phase2=5s (2x), phase3=3.33s (3x) const SHARK_INTERVALS = [10, 5, 3.33]; // Shark speeds: phase1=-494, phase3=1.5x faster const SHARK_SPEEDS = [-494, -494, -741]; function spawnShark(phaseIdx) { // Fully random height: anywhere from top safe zone to bottom safe zone const minY = H * 0.12; const maxY = getSandY() - 40; const y = minY + Math.random() * (maxY - minY); sharkWarning = { y, timer: 1.5, flicker: 0 }; setTimeout(() => { if (gameState === 'playing' && sharkWarning) { obstacles.push(new Obstacle('shark', W + 60, sharkWarning.y, SHARK_SPEEDS[phaseIdx], 0, 90, 44)); sharkWarning = null; } }, 1500); spawnTimers.shark = 0; } function spawnSchool(yMin, yMax) { const count = 3 + Math.floor(Math.random() * 3); for (let i = 0; i < count; i++) { obstacles.push(new Obstacle('fish', W + 40, yMin + Math.random() * (yMax - yMin), -180, 0, 44, 24)); } } // Ceiling obstacle: a rocky/dark object near the top to force players toward center function spawnCeilingObstacle(type) { const topY = H * 0.08 + Math.random() * (H * 0.12); // top 8-20% of screen if (type === 'jellyfish') { obstacles.push(new Obstacle('jellyfish', W + 40, topY, -130, Math.random() * 30 - 15, 36, 60)); } else if (type === 'medusa') { obstacles.push(new Obstacle('medusa', W + 40, topY, -140, Math.random() * 30 - 15, 32, 80)); } else { obstacles.push(new Obstacle('stingray', W + 40, topY, -150, 0, 60, 40)); } } function spawnObstacles(dt) { const sharkInterval = SHARK_INTERVALS[phase - 1]; if (phase === 1) { spawnTimers.shark += dt; spawnTimers.school += dt; spawnTimers.jellyfish += dt; spawnTimers.crab += dt; if (spawnTimers.shark >= sharkInterval && !sharkWarning) spawnShark(0); if (spawnTimers.school > 2) { spawnSchool(60, getSandY() - 80); // Extra school near top ceiling if (Math.random() < 0.4) spawnSchool(H * 0.06, H * 0.22); spawnTimers.school = 0; } if (spawnTimers.jellyfish > 2.8) { obstacles.push(new Obstacle('jellyfish', W + 40, 100 + Math.random() * (getSandY() - 200), -130, Math.random() * 40 - 20, 36, 60)); // Ceiling jellyfish every other spawn if (Math.random() < 0.5) spawnCeilingObstacle('jellyfish'); spawnTimers.jellyfish = 0; } if (spawnTimers.crab > 3.2) { obstacles.push(new Obstacle('crab', W + 40, getSandY() - 30, -150, 0, 52, 48)); spawnTimers.crab = 0; } } else if (phase === 2) { spawnTimers.shark += dt; spawnTimers.school += dt; spawnTimers.octopus += dt; spawnTimers.stingray += dt; spawnTimers.lobster += dt; spawnTimers.jellyfish += dt; if (spawnTimers.shark >= sharkInterval && !sharkWarning) spawnShark(1); // Fish schools in phase 2 too if (spawnTimers.school > 2.5) { spawnSchool(60, getSandY() - 80); if (Math.random() < 0.55) spawnSchool(H * 0.06, H * 0.22); // ceiling schools spawnTimers.school = 0; } if (spawnTimers.jellyfish > 3) { spawnCeilingObstacle('jellyfish'); spawnTimers.jellyfish = 0; } if (spawnTimers.octopus > 2.6) { obstacles.push(new Obstacle('octopus', W + 40, 100 + Math.random() * (getSandY() - 200), -160, 0, 40, 50)); spawnTimers.octopus = 0; } if (spawnTimers.stingray > 2.8) { obstacles.push(new Obstacle('stingray', W + 40, H * 0.4 + Math.random() * 100, -170, 0, 60, 40)); spawnCeilingObstacle('stingray'); spawnTimers.stingray = 0; } if (spawnTimers.lobster > 3.2) { obstacles.push(new Obstacle('lobster', W + 40, getSandY() - 20, -140, 0, 48, 28)); spawnTimers.lobster = 0; } } else if (phase === 3) { spawnTimers.shark += dt; spawnTimers.school += dt; spawnTimers.eel += dt; spawnTimers.medusa += dt; spawnTimers.anglerfish += dt; spawnTimers.jellyfish += dt; if (spawnTimers.shark >= sharkInterval && !sharkWarning) spawnShark(2); // Fish schools in phase 3 if (spawnTimers.school > 2) { spawnSchool(60, getSandY() - 80); if (Math.random() < 0.65) spawnSchool(H * 0.06, H * 0.22); spawnTimers.school = 0; } if (spawnTimers.jellyfish > 2.4) { spawnCeilingObstacle('medusa'); spawnTimers.jellyfish = 0; } if (spawnTimers.eel > 3) { obstacles.push(new Obstacle('eel', W + 40, H / 2 + (Math.random() - 0.5) * 150, -200, (Math.random() - 0.5) * 80, 60, 40)); spawnTimers.eel = 0; } if (spawnTimers.medusa > 2.8) { obstacles.push(new Obstacle('medusa', W + 40, 120 + Math.random() * (getSandY() - 240), -140, Math.random() * 50 - 25, 32, 80)); spawnTimers.medusa = 0; } if (spawnTimers.anglerfish > 3.2) { obstacles.push(new Obstacle('anglerfish', W + 40, getSandY() - 40, -160, 0, 52, 44)); spawnTimers.anglerfish = 0; } } } function spawnCollectibles(dt) { spawnTimers.jeans += dt; spawnTimers.star += dt; spawnTimers.bubble += dt; spawnTimers.bubbleItem += dt; spawnTimers.goldenStar += dt; // Jeans if (Math.random() < 0.0008) { collectibles.push(new Collectible('jeans', W, 50 + Math.random() * (getSandY() - 100), -200)); } // Normal stars - more frequent, spawn in groups if (spawnTimers.star > 1.2) { const groupCount = 2 + Math.floor(Math.random() * 4); for (let i = 0; i < groupCount; i++) { const spreadY = 50 + Math.random() * (getSandY() - 100); const spreadX = W + i * 40; collectibles.push(new Collectible('star', spreadX, spreadY, -170 - Math.random() * 40)); } spawnTimers.star = 0; } // Golden stars - rarer, faster if (spawnTimers.goldenStar > 8 + Math.random() * 6) { collectibles.push(new Collectible('golden_star', W, 50 + Math.random() * (getSandY() - 100), -260 - Math.random() * 60)); spawnTimers.goldenStar = 0; } // Collectible air bubbles if (spawnTimers.bubbleItem > 2.0) { const count = 2 + Math.floor(Math.random() * 3); for (let i = 0; i < count; i++) { collectibles.push(new Collectible('bubble_item', W + i * 50, 60 + Math.random() * (getSandY() - 120), -150 - Math.random() * 30)); } spawnTimers.bubbleItem = 0; } // Background bubbles if (spawnTimers.bubble > 0.3) { bubbles.push(createBubble()); spawnTimers.bubble = 0; } } // ===== DRAW PLAYER ===== function drawPlayer(dt) { const { x, y, hasJeans, blinking, blinkVisible } = player; if (blinking && !blinkVisible) return; if (hasJeans) { player.auraAngle = (player.auraAngle || 0) + dt; const auraGrad = ctx.createRadialGradient(x, y, 8, x, y, 70); auraGrad.addColorStop(0, 'rgba(0,200,255,0.4)'); auraGrad.addColorStop(1, 'rgba(0,200,255,0)'); ctx.fillStyle = auraGrad; ctx.beginPath(); ctx.arc(x, y, 65, 0, Math.PI * 2); ctx.fill(); } ctx.save(); ctx.translate(x, y); player.tailAngle = (player.tailAngle || 0) + 0.08; // Subtle body wave animation - gentle squish/stretch player.bodyWave = (player.bodyWave || 0) + 0.07; const bodyScaleX = 1 + Math.sin(player.bodyWave) * 0.045; const bodyScaleY = 1 + Math.cos(player.bodyWave) * 0.03; const t = Math.sin(player.tailAngle) * 12; ctx.beginPath(); ctx.moveTo(-18, 0); ctx.lineTo(-38, -14 + t); ctx.lineTo(-35, 2); ctx.lineTo(-38, 14 - t); ctx.closePath(); ctx.fillStyle = '#ff7043'; ctx.fill(); ctx.save(); ctx.scale(bodyScaleX, bodyScaleY); ctx.beginPath(); ctx.ellipse(0, 0, 28, 16, 0, 0, Math.PI * 2); ctx.fillStyle = '#ff8a65'; ctx.fill(); ctx.beginPath(); ctx.ellipse(2, 5, 16, 9, 0.2, 0, Math.PI * 2); ctx.fillStyle = '#ffcc80'; ctx.fill(); if (hasJeans) { ctx.beginPath(); ctx.roundRect(-12, 0, 32, 18, 4); ctx.fillStyle = '#1565c0'; ctx.fill(); ctx.beginPath(); ctx.moveTo(10, 2); ctx.lineTo(10, 16); ctx.strokeStyle = '#90caf9'; ctx.lineWidth = 2.5; ctx.stroke(); } ctx.beginPath(); ctx.moveTo(8, -12); ctx.lineTo(12, -22); ctx.lineTo(22, -10); ctx.closePath(); ctx.fillStyle = '#e64a19'; ctx.fill(); ctx.beginPath(); ctx.arc(18, -6, 8, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); ctx.beginPath(); ctx.arc(19, -6, 5, 0, Math.PI * 2); ctx.fillStyle = '#333'; ctx.fill(); ctx.beginPath(); ctx.arc(17, -8, 2, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); ctx.restore(); // end body scale // Animated sprite bubbles above fish player.bubbleTimer = (player.bubbleTimer || 0) + dt; // Spawn new sprite bubbles if (player.bubbleTimer > 0.4) { player.spriteBubbles.push({ x: 20 + Math.random() * 16, y: -24, vy: -(18 + Math.random() * 12), vx: (player.vx / 200) * -3 + (Math.random() - 0.5) * 4, // physics: trail based on velocity r: 1.5 + Math.random() * 2, life: 1.0, wobble: Math.random() * Math.PI * 2, }); player.bubbleTimer = 0; } for (let i = player.spriteBubbles.length - 1; i >= 0; i--) { const sb = player.spriteBubbles[i]; sb.life -= dt * 0.9; sb.y += sb.vy * dt; sb.x += sb.vx * dt; sb.wobble += 3 * dt; sb.x += Math.sin(sb.wobble) * 0.3; if (sb.life <= 0) { player.spriteBubbles.splice(i, 1); continue; } ctx.globalAlpha = Math.min(1, sb.life * 2) * 0.7; ctx.beginPath(); ctx.arc(sb.x, sb.y, sb.r, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(160,230,255,0.9)'; ctx.lineWidth = 1; ctx.stroke(); ctx.beginPath(); ctx.arc(sb.x - sb.r * 0.3, sb.y - sb.r * 0.3, sb.r * 0.25, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.fill(); ctx.globalAlpha = 1; } ctx.restore(); } // ===== DRAW BACKGROUND ===== function drawBackground() { const p = PHASES[phase - 1]; const grad = ctx.createLinearGradient(0, 0, 0, H); grad.addColorStop(0, p.bgColors[0]); grad.addColorStop(0.5, p.bgColors[1]); grad.addColorStop(1, p.bgColors[2]); ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H); if (phase === 1) { for (let i = 0; i < 5; i++) { const x = (i * 180 + Date.now() * 0.01) % (W + 100) - 50; const grd = ctx.createLinearGradient(x, 0, x + 30, H); grd.addColorStop(0, 'rgba(0,180,255,0.06)'); grd.addColorStop(1, 'rgba(0,180,255,0)'); ctx.fillStyle = grd; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x + 60, 0); ctx.lineTo(x + 120, H); ctx.lineTo(x + 60, H); ctx.closePath(); ctx.fill(); } } if (phase === 2) { // Cave stalactites silhouette at top ctx.fillStyle = 'rgba(5,0,15,0.7)'; for (let i = 0; i < 10; i++) { const sx = (i * 130 + sandOffset * 0.5) % (W + 60) - 30; const sh = 30 + Math.sin(i * 1.7) * 20; ctx.beginPath(); ctx.moveTo(sx - 25, 0); ctx.lineTo(sx + 25, 0); ctx.lineTo(sx, sh); ctx.closePath(); ctx.fill(); } // Purple glow blobs for (let i = 0; i < 5; i++) { const x = (i * 210 + Date.now() * 0.008) % (W + 100) - 50; const y = 80 + Math.sin(Date.now() * 0.0008 + i * 1.2) * 60; const grd = ctx.createRadialGradient(x, y, 0, x, y, 80); grd.addColorStop(0, 'rgba(120,0,200,0.08)'); grd.addColorStop(1, 'rgba(80,0,150,0)'); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(x, y, 80, 0, Math.PI * 2); ctx.fill(); } } if (phase === 3) { for (let i = 0; i < 8; i++) { const x = (i * 120 + Date.now() * 0.015) % (W + 80); const y = 100 + Math.sin(Date.now() * 0.001 + i) * 150; const grd = ctx.createRadialGradient(x, y, 0, x, y, 50); grd.addColorStop(0, 'rgba(0,255,150,0.07)'); grd.addColorStop(1, 'rgba(0,255,150,0)'); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(x, y, 50, 0, Math.PI * 2); ctx.fill(); } // Deep abyss column streaks for (let i = 0; i < 4; i++) { const x = (i * 260 + Date.now() * 0.012) % (W + 100) - 50; const grd = ctx.createLinearGradient(x, 0, x + 20, H); grd.addColorStop(0, 'rgba(0,200,100,0.04)'); grd.addColorStop(1, 'rgba(0,200,100,0)'); ctx.fillStyle = grd; ctx.fillRect(x, 0, 20, H); } } } // ===== DRAW SAND FLOOR ===== function drawSandFloor(dt) { sandOffset = (sandOffset + 30 * gameSpeed * (dt || 0.016)) % 40; const sandY = getSandY(); const p = PHASES[phase - 1]; // Main sand gradient const sandGrad = ctx.createLinearGradient(0, sandY, 0, H); if (phase === 1) { sandGrad.addColorStop(0, '#c2a06b'); sandGrad.addColorStop(0.3, '#b8955a'); sandGrad.addColorStop(1, '#8b6914'); } else if (phase === 2) { sandGrad.addColorStop(0, '#4a3060'); sandGrad.addColorStop(0.3, '#38245a'); sandGrad.addColorStop(1, '#1a0a30'); } else { sandGrad.addColorStop(0, '#0a3020'); sandGrad.addColorStop(0.3, '#052515'); sandGrad.addColorStop(1, '#001508'); } ctx.fillStyle = sandGrad; ctx.fillRect(0, sandY, W, H - sandY); // Wavy sand top edge ctx.beginPath(); ctx.moveTo(0, sandY); for (let x = 0; x <= W; x += 8) { const wave = Math.sin((x + sandOffset * 4) * 0.025 + Date.now() * 0.001) * 5 + Math.sin((x + sandOffset * 2) * 0.06 + Date.now() * 0.0007) * 2; ctx.lineTo(x, sandY + wave); } ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath(); const topGrad = ctx.createLinearGradient(0, sandY - 8, 0, sandY + 20); if (phase === 1) { topGrad.addColorStop(0, 'rgba(220,185,110,0)'); topGrad.addColorStop(0.4, 'rgba(220,185,110,0.6)'); topGrad.addColorStop(1, 'rgba(200,160,80,0.3)'); } else if (phase === 2) { topGrad.addColorStop(0, 'rgba(80,50,110,0)'); topGrad.addColorStop(0.4, 'rgba(80,50,110,0.6)'); topGrad.addColorStop(1, 'rgba(60,30,90,0.3)'); } else { topGrad.addColorStop(0, 'rgba(20,70,40,0)'); topGrad.addColorStop(0.4, 'rgba(20,70,40,0.6)'); topGrad.addColorStop(1, 'rgba(10,50,25,0.3)'); } ctx.fillStyle = topGrad; ctx.fill(); // Sand ripple texture if (phase === 1) { ctx.strokeStyle = 'rgba(180,150,80,0.2)'; ctx.lineWidth = 1; for (let i = 0; i < 6; i++) { const ry = sandY + 8 + i * 10; ctx.beginPath(); ctx.moveTo(0, ry); for (let x = 0; x <= W; x += 12) { ctx.lineTo(x, ry + Math.sin((x - sandOffset * 8 + i * 30) * 0.04) * 2); } ctx.stroke(); } // Small pebbles ctx.fillStyle = 'rgba(160,130,70,0.35)'; for (let i = 0; i < 20; i++) { const px = ((i * 137 + sandOffset * 2) % W); const py = sandY + 6 + (i * 7) % 20; ctx.beginPath(); ctx.ellipse(px, py, 3 + (i % 3), 2, (i * 0.3) % Math.PI, 0, Math.PI * 2); ctx.fill(); } } } // ===== DRAW CORALS ===== function drawCorals(dt) { const speed = 50 * gameSpeed; for (let i = coralDecor.length - 1; i >= 0; i--) { const c = coralDecor[i]; c.x -= speed * dt; c.sway += c.swaySpeed * dt; if (c.x < -80) { coralDecor.splice(i, 1); // Spawn new one at the right if (Math.random() < 0.7) coralDecor.push(createCoral(W + 80)); continue; } ctx.save(); ctx.translate(c.x, getSandY()); ctx.strokeStyle = c.color; ctx.lineWidth = c.w; ctx.lineCap = 'round'; const sway = Math.sin(c.sway) * 0.05; ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(sway * c.h * 2, -c.h / 2, sway * c.h * 3, -c.h); ctx.stroke(); for (let b = 0; b < c.branches; b++) { const frac = 0.4 + (b / c.branches) * 0.5; const bx = sway * c.h * frac * 3; const by = -c.h * frac; const dir = b % 2 === 0 ? 1 : -1; ctx.lineWidth = c.w * 0.5; ctx.beginPath(); ctx.moveTo(bx, by); ctx.lineTo(bx + dir * 15, by - 20); ctx.stroke(); } ctx.restore(); } } // ===== DRAW BACKGROUND BUBBLES ===== function drawBubbles(dt) { for (let i = bubbles.length - 1; i >= 0; i--) { const b = bubbles[i]; b.y -= b.speed * dt; b.wobble += 1.5 * dt; b.x += Math.sin(b.wobble) * 0.4; if (b.y < -20) { bubbles.splice(i, 1); continue; } ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(200,240,255,0.3)'; ctx.lineWidth = 1; ctx.stroke(); ctx.beginPath(); ctx.arc(b.x - b.r * 0.3, b.y - b.r * 0.3, b.r * 0.25, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fill(); } } function drawSharkWarning() { if (!sharkWarning) return; sharkWarning.flicker += 0.2; if (Math.sin(sharkWarning.flicker * 5) > 0) { ctx.save(); ctx.translate(W - 35, sharkWarning.y); ctx.fillStyle = 'rgba(255,100,100,0.7)'; ctx.font = 'bold 40px Roboto'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⚠️', 0, 0); ctx.restore(); } } // ===== UPDATE ===== function update(dt) { const hasJeans = player.hasJeans; const maxSpeed = hasJeans ? player.jeansMaxSpeed : player.maxSpeed; const accel = hasJeans ? player.jeansAccel : player.accel; const friction = hasJeans ? player.jeansFriction : player.friction; let ax = 0, ay = 0; if (keys['ArrowUp'] || keys['w'] || keys['W']) ay = -accel; if (keys['ArrowDown'] || keys['s'] || keys['S']) ay = accel; if (keys['ArrowLeft'] || keys['a'] || keys['A']) ax = -accel; if (keys['ArrowRight'] || keys['d'] || keys['D']) ax = accel; player.vx += ax * dt; player.vy += ay * dt; // Friction (deceleration) when no key if (ax === 0) player.vx *= Math.pow(1 - friction, dt * 60); if (ay === 0) player.vy *= Math.pow(1 - friction, dt * 60); // Clamp const spd = Math.sqrt(player.vx * player.vx + player.vy * player.vy); if (spd > maxSpeed) { player.vx = (player.vx / spd) * maxSpeed; player.vy = (player.vy / spd) * maxSpeed; } player.x += player.vx * dt; player.y += player.vy * dt; // Bounds: top limit below sea surface strip, bottom above sand, left/right const TOP_BOUNDARY = 62 + player.h / 2; // below sea surface HUD if (player.x < player.w / 2) player.x = player.w / 2; if (player.x > W - player.w / 2) player.x = W - player.w / 2; if (player.y < TOP_BOUNDARY) player.y = TOP_BOUNDARY; if (player.y > getSandY() - player.h / 2) player.y = getSandY() - player.h / 2; // Jeans timer if (player.hasJeans) { player.shielded -= dt * 50; if (player.shielded <= 0) { player.hasJeans = false; document.getElementById('jeansBg').style.display = 'none'; } else { document.getElementById('jeansFill').style.width = (player.shielded / 500) * 100 + '%'; } } // Blink timer if (player.blinking) { player.blinkTimer -= dt; player.blinkVisible = Math.sin(player.blinkTimer * 20) > 0; if (player.blinkTimer <= 0) { player.blinking = false; player.blinkVisible = true; } } // Air bubbles drain airBubbles -= AIR_DRAIN_RATE * dt; // Low air warning: ~5 seconds before depletion (5 * 3.5 = 17.5) // Only trigger once and only while air is in danger zone if (airBubbles <= 17.5 && airBubbles > 0) { if (!airWarningTriggered) { airWarningTriggered = true; player.blinking = true; player.blinkTimer = 5; // Blink for exactly 5 seconds const lbl = document.getElementById('airBubbleLabel'); lbl.style.color = '#ff4444'; lbl.style.animation = 'airLabelPulse 0.4s ease-in-out infinite'; } // Keep updating blink timer while in danger zone player.blinkTimer = Math.max(0, player.blinkTimer - dt); if (player.blinkTimer <= 0) { player.blinking = false; player.blinkVisible = true; } } else if (airBubbles > 17.5 && airWarningTriggered) { // Air was refilled above threshold, cancel warning airWarningTriggered = false; player.blinking = false; player.blinkVisible = true; const lbl = document.getElementById('airBubbleLabel'); lbl.style.color = '#a0e8ff'; lbl.style.animation = ''; } if (airBubbles < 0) { airBubbles = 0; triggerGameOver(); return; } updateAirBubbleHUD(); spawnObstacles(dt); spawnCollectibles(dt); for (let o of obstacles) o.update(dt); for (let c of collectibles) c.update(dt); obstacles = obstacles.filter(o => o.alive); collectibles = collectibles.filter(c => c.alive); // Collisions - obstacles if (!player.blinking) { for (let o of obstacles) { if (o.isCollidingWith(player.x - player.w / 2, player.y - player.h / 2, player.w, player.h)) { if (player.hasJeans) { player.hasJeans = false; player.shielded = 0; document.getElementById('jeansBg').style.display = 'none'; // Start blink player.blinking = true; player.blinkTimer = 3; player.blinkVisible = true; } else { triggerGameOver(); return; } } } } // Collisions - collectibles for (let c of collectibles) { if (c.isCollidingWith(player.x - player.w / 2, player.y - player.h / 2, player.w, player.h)) { if (c.type === 'jeans') { player.hasJeans = true; player.shielded = 500; document.getElementById('jeansBg').style.display = 'block'; } else if (c.type === 'star') { stars++; score += 50; } else if (c.type === 'golden_star') { stars += 5; score += 250; } else if (c.type === 'bubble_item') { airBubbles = Math.min(AIR_MAX, airBubbles + AIR_REFILL_AMOUNT); updateAirBubbleHUD(); } c.alive = false; } } const elapsedTime = performance.now() - phaseStartTime; const remainingTime = Math.max(0, 120000 - elapsedTime); const minutes = Math.floor(remainingTime / 60000); const seconds = Math.floor((remainingTime % 60000) / 1000); document.getElementById('timerVal').textContent = minutes + ':' + (seconds < 10 ? '0' : '') + seconds; if (remainingTime <= 0) { if (phase === 3) { triggerWin(); } else { // Show phase transition screen gameState = 'transitioning'; cancelAnimationFrame(animFrame); document.getElementById('hud').classList.add('hidden'); document.getElementById('airBubbleHud').classList.add('hidden'); const transitionScreen = document.getElementById('phaseTransition'); document.getElementById('transitionPhaseNum').textContent = `FASE ${phase + 1}`; document.getElementById('transitionPhaseDesc').textContent = PHASES[phase].sub; transitionScreen.classList.remove('hidden'); // After 3 seconds, start next phase setTimeout(() => { transitionScreen.classList.add('hidden'); phase++; gameSpeed = 1 + phase * 0.2; phaseStartTime = performance.now(); score += 500; document.getElementById('phaseVal').textContent = String(phase).padStart(2, '0'); playTrack(PHASES[phase - 1].music); // Reset map elements obstacles = []; collectibles = []; coralDecor = []; bubbles = []; sharkWarning = null; sandOffset = 0; Object.keys(spawnTimers).forEach(k => spawnTimers[k] = 0); // Reset air to 100 airBubbles = 100; airWarningTriggered = false; updateAirBubbleHUD(); // Add new corals for (let i = 0; i < 25; i++) { coralDecor.push(createCoral(Math.random() * W)); } // Add background bubbles for (let i = 0; i < 40; i++) { bubbles.push(createBubble()); } document.getElementById('hud').classList.remove('hidden'); document.getElementById('airBubbleHud').classList.remove('hidden'); gameState = 'playing'; lastTime = 0; animFrame = requestAnimationFrame(gameLoop); }, 3000); } } score += 0.05 * dt * 10; document.getElementById('scoreVal').textContent = String(Math.floor(score)).padStart(4, '0'); if (sharkWarning) sharkWarning.timer -= dt; } function updateAirBubbleHUD() { const fill = document.getElementById('airBubbleFill'); fill.style.width = airBubbles + '%'; if (airBubbles < 25) { fill.classList.add('low'); } else { fill.classList.remove('low'); } } function draw(dt) { drawBackground(); drawSandFloor(dt); drawCorals(dt); drawBubbles(dt); obstacles.forEach(o => o.draw()); collectibles.forEach(c => c.draw()); drawPlayer(dt); drawSharkWarning(); } function gameLoop(now) { if (gameState !== 'playing') return; const dt = lastTime ? Math.min((now - lastTime) / 1000, 0.05) : 0.016; lastTime = now; update(dt); draw(dt); animFrame = requestAnimationFrame(gameLoop); } const MESSAGES = [ '"Nem todo herói usa capa, alguns usam calça jeans."', '"O peixe não desiste."', '"Tente novamente!"', '"A coragem vem em calças azuis."', '"Nem todos podem ser rápidos como um peixe em jeans."', '"A vida é feita de tentativas!"', '"Todo peixe grande um dia foi pequeno."', ]; function triggerGameOver() { if (gameState === 'gameover') return; gameState = 'gameover'; cancelAnimationFrame(animFrame); document.getElementById('hud').classList.add('hidden'); document.getElementById('airBubbleHud').classList.add('hidden'); document.getElementById('jeansBg').style.display = 'none'; document.getElementById('joystickZone').style.display = 'none'; document.getElementById('finalScore').textContent = Math.floor(score); document.getElementById('finalStars').textContent = stars; document.getElementById('gameoverMsg').textContent = MESSAGES[Math.floor(Math.random() * MESSAGES.length)]; document.getElementById('gameOverScreen').classList.remove('hidden'); playTrack('gameover'); } function triggerWin() { gameState = 'win'; cancelAnimationFrame(animFrame); document.getElementById('hud').classList.add('hidden'); document.getElementById('airBubbleHud').classList.add('hidden'); document.getElementById('jeansBg').style.display = 'none'; document.getElementById('joystickZone').style.display = 'none'; document.getElementById('winScore').textContent = Math.floor(score); document.getElementById('winStars').textContent = stars; document.getElementById('winScreen').classList.remove('hidden'); playTrack('final'); startWinAnimation(); } function initGame() { score = 0; stars = 0; phase = 1; gameSpeed = 1; phaseStartTime = performance.now(); obstacles = []; collectibles = []; coralDecor = []; bubbles = []; sharkWarning = null; airBubbles = 100; airWarningTriggered = false; sandOffset = 0; Object.keys(spawnTimers).forEach(k => spawnTimers[k] = 0); // Reset air label style const airLbl = document.getElementById('airBubbleLabel'); airLbl.style.color = '#a0e8ff'; airLbl.style.animation = ''; player.x = W * 0.2; player.y = H * 0.55; // center of play area below sea surface strip player.vx = 0; player.vy = 0; player.hasJeans = false; player.shielded = 0; player.tailAngle = 0; player.bodyWave = 0; player.blinking = false; player.blinkTimer = 0; player.blinkVisible = true; player.spriteBubbles = []; player.bubbleTimer = 0; for (let i = 0; i < 25; i++) { coralDecor.push(createCoral(Math.random() * W)); } for (let i = 0; i < 40; i++) { bubbles.push(createBubble()); } document.getElementById('scoreVal').textContent = '0000'; document.getElementById('phaseVal').textContent = '01'; document.getElementById('timerVal').textContent = '2:00'; document.getElementById('jeansBg').style.display = 'none'; document.getElementById('hud').classList.remove('hidden'); document.getElementById('airBubbleHud').classList.remove('hidden'); updateAirBubbleHUD(); gameState = 'playing'; lastTime = 0; // Show joystick on touch devices if (isTouchDevice()) document.getElementById('joystickZone').style.display = 'block'; // Play music BEFORE starting game loop playTrack(PHASES[0].music); // Show phase banner phaseText.textContent = 'FASE 1'; phaseSub.textContent = PHASES[0].sub; phaseBanner.classList.add('show'); setTimeout(() => phaseBanner.classList.remove('show'), 2200); animFrame = requestAnimationFrame(gameLoop); } // ===== WIN ANIMATION ===== const winCanvas = document.getElementById('winCanvas'); const wCtx = winCanvas.getContext('2d'); winCanvas.width = W; winCanvas.height = H; let winAnim; let winFishPhase = 0; let winFishX = -80; function startWinAnimation() { winFishX = -80; winFishPhase = 0; animateWin(); } function animateWin() { wCtx.clearRect(0, 0, W, H); winFishPhase += 0.03; winFishX += 1.5; if (winFishX > W + 80) winFishX = -80; const fy = H / 2 + 60 + Math.sin(winFishPhase) * 18; wCtx.save(); wCtx.translate(winFishX, fy); const grd = wCtx.createRadialGradient(0, 0, 10, 0, 0, 50); grd.addColorStop(0, 'rgba(255,215,0,0.3)'); grd.addColorStop(1, 'rgba(255,215,0,0)'); wCtx.beginPath(); wCtx.arc(0, 0, 50, 0, Math.PI * 2); wCtx.fillStyle = grd; wCtx.fill(); const t = Math.sin(winFishPhase * 2) * 14; wCtx.beginPath(); wCtx.moveTo(-22, 0); wCtx.lineTo(-44, -14 + t); wCtx.lineTo(-40, 2); wCtx.lineTo(-44, 14 - t); wCtx.closePath(); wCtx.fillStyle = '#1565c0'; wCtx.fill(); wCtx.beginPath(); wCtx.ellipse(2, 0, 30, 18, 0, 0, Math.PI * 2); wCtx.fillStyle = '#1976d2'; wCtx.fill(); wCtx.beginPath(); wCtx.roundRect(-8, 2, 30, 16, 3); wCtx.fillStyle = '#1565c0'; wCtx.fill(); wCtx.beginPath(); wCtx.moveTo(10, 3); wCtx.lineTo(10, 17); wCtx.strokeStyle = '#90caf9'; wCtx.lineWidth = 2; wCtx.stroke(); wCtx.beginPath(); wCtx.moveTo(10, -12); wCtx.lineTo(20, -26); wCtx.lineTo(28, -14); wCtx.closePath(); wCtx.fillStyle = '#0d47a1'; wCtx.fill(); wCtx.beginPath(); wCtx.arc(20, -5, 9, 0, Math.PI * 2); wCtx.fillStyle = '#fff'; wCtx.fill(); wCtx.beginPath(); wCtx.arc(21, -5, 5.5, 0, Math.PI * 2); wCtx.fillStyle = '#1565c0'; wCtx.fill(); wCtx.beginPath(); wCtx.arc(19, -7, 2, 0, Math.PI * 2); wCtx.fillStyle = '#fff'; wCtx.fill(); wCtx.restore(); for (let i = 0; i < 3; i++) { const sx = winFishX - 30 - i * 25; const sy = fy + Math.sin(winFishPhase + i) * 10; wCtx.beginPath(); wCtx.arc(sx, sy, 3, 0, Math.PI * 2); wCtx.fillStyle = `rgba(255,215,0,${0.7 - i * 0.2})`; wCtx.fill(); } winAnim = requestAnimationFrame(animateWin); } // ===== INPUT ===== document.addEventListener('keydown', e => { keys[e.key] = true; if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault(); }); document.addEventListener('keyup', e => { keys[e.key] = false; }); // ===== PORTRAIT / LANDSCAPE DETECTION ===== function isTouchDevice() { return ('ontouchstart' in window) || navigator.maxTouchPoints > 0; } function checkOrientation() { const isPortrait = window.innerHeight > window.innerWidth; const overlay = document.getElementById('rotateOverlay'); if (isTouchDevice() && isPortrait) { overlay.classList.add('show'); } else { overlay.classList.remove('show'); } } window.addEventListener('resize', checkOrientation); window.addEventListener('orientationchange', () => setTimeout(checkOrientation, 200)); checkOrientation(); // Show joystick only on touch devices if (isTouchDevice()) { document.getElementById('joystickZone').style.display = 'block'; } // ===== VIRTUAL JOYSTICK ===== const joystickZone = document.getElementById('joystickZone'); const joystickKnob = document.getElementById('joystickKnob'); const JOYSTICK_RADIUS = 130 / 2; // half of joystickBase width const KNOB_DEADZONE = 12; // px before registering direction let joystickActive = false; let joystickOriginX = 0; let joystickOriginY = 0; let joystickTouchId = null; function getJoystickCenter() { const rect = joystickZone.getBoundingClientRect(); return { cx: rect.left + rect.width / 2, cy: rect.top + rect.height / 2 }; } function updateJoystick(touchX, touchY) { const { cx, cy } = getJoystickCenter(); let dx = touchX - cx; let dy = touchY - cy; const dist = Math.sqrt(dx * dx + dy * dy); const maxDist = JOYSTICK_RADIUS - 10; if (dist > maxDist) { dx = (dx / dist) * maxDist; dy = (dy / dist) * maxDist; } // Move knob visually joystickKnob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`; // Map to keys with deadzone keys['ArrowRight'] = dx > KNOB_DEADZONE; keys['ArrowLeft'] = dx < -KNOB_DEADZONE; keys['ArrowDown'] = dy > KNOB_DEADZONE; keys['ArrowUp'] = dy < -KNOB_DEADZONE; } function resetJoystick() { joystickKnob.style.transform = 'translate(-50%, -50%)'; keys['ArrowRight'] = keys['ArrowLeft'] = keys['ArrowDown'] = keys['ArrowUp'] = false; joystickActive = false; joystickTouchId = null; } joystickZone.addEventListener('touchstart', e => { e.preventDefault(); const t = e.changedTouches[0]; joystickTouchId = t.identifier; joystickActive = true; updateJoystick(t.clientX, t.clientY); }, { passive: false }); joystickZone.addEventListener('touchmove', e => { e.preventDefault(); for (let t of e.changedTouches) { if (t.identifier === joystickTouchId) { updateJoystick(t.clientX, t.clientY); } } }, { passive: false }); joystickZone.addEventListener('touchend', e => { e.preventDefault(); for (let t of e.changedTouches) { if (t.identifier === joystickTouchId) resetJoystick(); } }, { passive: false }); joystickZone.addEventListener('touchcancel', e => { e.preventDefault(); resetJoystick(); }, { passive: false }); // Fallback swipe on canvas (for non-joystick area) let touchStart = null; let canvasTouchId = null; canvas.addEventListener('touchstart', e => { // Ignore if touch is on joystick zone const t = e.changedTouches[0]; const rect = joystickZone.getBoundingClientRect(); const onJoystick = t.clientX >= rect.left && t.clientX <= rect.right && t.clientY >= rect.top && t.clientY <= rect.bottom; if (onJoystick) return; touchStart = { x: t.clientX, y: t.clientY }; canvasTouchId = t.identifier; e.preventDefault(); }, { passive: false }); canvas.addEventListener('touchmove', e => { if (!touchStart) return; for (let t of e.changedTouches) { if (t.identifier === canvasTouchId) { const dx = t.clientX - touchStart.x; const dy = t.clientY - touchStart.y; keys['ArrowRight'] = dx > 20; keys['ArrowLeft'] = dx < -20; keys['ArrowDown'] = dy > 20; keys['ArrowUp'] = dy < -20; } } e.preventDefault(); }, { passive: false }); canvas.addEventListener('touchend', e => { for (let t of e.changedTouches) { if (t.identifier === canvasTouchId) { touchStart = null; canvasTouchId = null; keys['ArrowRight'] = keys['ArrowLeft'] = keys['ArrowDown'] = keys['ArrowUp'] = false; } } e.preventDefault(); }, { passive: false }); // ===== BUTTONS ===== document.getElementById('startBtn').addEventListener('click', () => { document.getElementById('startScreen').classList.add('hidden'); initGame(); }); document.getElementById('restartBtn').addEventListener('click', () => { document.getElementById('gameOverScreen').classList.add('hidden'); initGame(); }); document.getElementById('exitBtn').addEventListener('click', () => { document.getElementById('gameOverScreen').classList.add('hidden'); document.getElementById('startScreen').classList.remove('hidden'); gameState = 'start'; playTrack('inicio'); }); document.getElementById('playAgainBtn').addEventListener('click', () => { cancelAnimationFrame(winAnim); document.getElementById('winScreen').classList.add('hidden'); initGame(); }); const phaseText = document.getElementById('phaseText'); const phaseSub = document.getElementById('phaseSub'); const phaseBanner = document.getElementById('phaseBanner'); const startVideo = document.getElementById('startVideo'); if (startVideo) { startVideo.addEventListener('error', () => { console.log('Vídeo não encontrado. Use: assets/videos/background_inicio.mp4'); }); startVideo.play().catch(() => {}); } // Draw initial frame const initGrad = ctx.createLinearGradient(0, 0, 0, H); initGrad.addColorStop(0, '#001830'); initGrad.addColorStop(1, '#003f70'); ctx.fillStyle = initGrad; ctx.fillRect(0, 0, W, H);