Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> | |
| <title>Fruit Ninja Pro</title> | |
| <style> | |
| :root { | |
| --primary: #ffcc00; | |
| --secondary: #ff6b6b; | |
| --dark: #1a1a1a; | |
| --glass: rgba(255, 255, 255, 0.1); | |
| --glass-border: rgba(255, 255, 255, 0.2); | |
| } | |
| body { | |
| margin: 0; padding: 0; overflow: hidden; | |
| background-color: var(--dark); | |
| font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| touch-action: none; user-select: none; | |
| color: #fff; | |
| } | |
| #game-container { | |
| position: relative; width: 100vw; height: 100vh; | |
| background: radial-gradient(circle at center, #2c3e50 0%, #000000 100%); | |
| } | |
| canvas { display: block; width: 100%; height: 100%; } | |
| /* Modern Glass UI */ | |
| .overlay { | |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; | |
| display: flex; flex-direction: column; justify-content: center; | |
| align-items: center; z-index: 100; text-align: center; | |
| backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); | |
| background: rgba(0,0,0,0.7); | |
| } | |
| .hidden { display: none ; } | |
| .menu-card { | |
| background: var(--glass); | |
| border: 1px solid var(--glass-border); | |
| padding: 40px; border-radius: 32px; | |
| width: 85%; max-width: 400px; | |
| box-shadow: 0 20px 50px rgba(0,0,0,0.5); | |
| } | |
| h1 { | |
| font-size: 42px; font-weight: 900; margin: 0 0 10px; | |
| background: linear-gradient(45deg, #ffcc00, #ff6b6b); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| letter-spacing: -1px; | |
| } | |
| .stat-text { font-size: 14px; text-transform: uppercase; letter-spacing: 2px; opacity: 0.6; margin-bottom: 30px; } | |
| .difficulty-group { | |
| display: grid; grid-template-columns: repeat(3, 1fr); | |
| gap: 10px; margin-bottom: 30px; | |
| } | |
| .diff-btn { | |
| background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); | |
| color: #fff; padding: 12px 5px; border-radius: 12px; | |
| font-size: 12px; font-weight: 700; cursor: pointer; transition: 0.3s; | |
| } | |
| .diff-btn.active { background: var(--primary); color: #000; border-color: var(--primary); } | |
| .btn { | |
| width: 100%; padding: 18px; border-radius: 16px; border: none; | |
| font-size: 18px; font-weight: 800; cursor: pointer; | |
| margin-bottom: 12px; transition: transform 0.1s; | |
| text-transform: uppercase; letter-spacing: 1px; | |
| } | |
| .btn-primary { background: var(--primary); color: #000; } | |
| .btn-secondary { background: rgba(255,255,255,0.1); color: #fff; border: 1px solid var(--glass-border); } | |
| .btn:active { transform: scale(0.95); } | |
| #hud { | |
| position: absolute; top: 0; left: 0; width: 100%; padding: 25px; | |
| display: flex; justify-content: space-between; align-items: flex-start; | |
| pointer-events: none; z-index: 50; | |
| } | |
| .score-val { font-size: 42px; font-weight: 900; line-height: 1; text-shadow: 0 2px 10px rgba(0,0,0,0.5); } | |
| #pause-trigger { | |
| pointer-events: auto; background: var(--glass); border: 1px solid var(--glass-border); | |
| width: 50px; height: 50px; border-radius: 15px; display: flex; | |
| justify-content: center; align-items: center; cursor: pointer; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <canvas id="canvas"></canvas> | |
| <div id="hud" class="hidden"> | |
| <div class="score-container"> | |
| <div id="score-val" class="score-val">0</div> | |
| </div> | |
| <button id="pause-trigger"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="white"><rect x="5" y="4" width="4" height="16"/><rect x="15" y="4" width="4" height="16"/></svg> | |
| </button> | |
| </div> | |
| <div id="home-menu" class="overlay"> | |
| <div class="menu-card"> | |
| <h1>FRUIT NINJA</h1> | |
| <div class="stat-text">Best: <span id="best-score">0</span></div> | |
| <div class="difficulty-group"> | |
| <button class="diff-btn" data-diff="easy">EASY</button> | |
| <button class="diff-btn active" data-diff="medium">MEDIUM</button> | |
| <button class="diff-btn" data-diff="hard">HARD</button> | |
| </div> | |
| <button class="btn btn-primary" id="start-btn">New Game</button> | |
| <button class="btn btn-secondary hidden" id="resume-btn">Continue</button> | |
| </div> | |
| </div> | |
| <div id="pause-menu" class="overlay hidden"> | |
| <div class="menu-card"> | |
| <h1>PAUSED</h1> | |
| <div style="height: 20px"></div> | |
| <button class="btn btn-primary" id="unpause-btn">Resume</button> | |
| <button class="btn btn-secondary" id="save-exit-btn">Save & Exit</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const FRUIT_TYPES = [ | |
| { emoji: '🍎', juice: '#c0392b' }, | |
| { emoji: '🍊', juice: '#e67e22' }, | |
| { emoji: '🍉', juice: '#ff7675' }, | |
| { emoji: '🍌', juice: '#f1c40f' }, | |
| { emoji: '🍍', juice: '#f39c12' }, | |
| { emoji: '🥝', juice: '#2ecc71' }, | |
| { emoji: '💣', juice: '#e17055', isBomb: true } | |
| ]; | |
| const AudioEngine = (() => { | |
| let ctx = null; | |
| const init = () => { if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)(); }; | |
| const play = (f1, f2, type, dur, vol) => { | |
| if (!ctx) return; | |
| try { | |
| const o = ctx.createOscillator(), g = ctx.createGain(); | |
| o.type = type; o.frequency.setValueAtTime(f1, ctx.currentTime); | |
| o.frequency.exponentialRampToValueAtTime(f2, ctx.currentTime + dur); | |
| g.gain.setValueAtTime(vol, ctx.currentTime); g.gain.linearRampToValueAtTime(0, ctx.currentTime + dur); | |
| o.connect(g); g.connect(ctx.destination); | |
| o.start(); o.stop(ctx.currentTime + dur); | |
| } catch(e) {} | |
| }; | |
| return { | |
| init, | |
| swish: () => play(500, 1500, 'triangle', 0.1, 0.05), | |
| splat: () => play(600, 50, 'sine', 0.2, 0.1), | |
| boom: () => play(150, 20, 'sawtooth', 0.6, 0.3) | |
| }; | |
| })(); | |
| const canvas = document.getElementById('canvas'); | |
| const gctx = canvas.getContext('2d'); | |
| const scoreDisplay = document.getElementById('score-val'); | |
| let width, height, score = 0, gameActive = false, isPaused = false; | |
| let currentDifficulty = 'medium'; | |
| let fruits = [], particles = [], bladeTrail = []; | |
| let lastPointer = { x: 0, y: 0 }, isSlicing = false, spawnTimer = null; | |
| const DIFFICULTY = { | |
| easy: { rate: 1600, speed: 0.7, bomb: 0.05 }, | |
| medium: { rate: 1100, speed: 1.0, bomb: 0.12 }, | |
| hard: { rate: 700, speed: 1.4, bomb: 0.22 } | |
| }; | |
| function resize() { | |
| width = window.innerWidth; height = window.innerHeight; | |
| canvas.width = width; canvas.height = height; | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| class Fruit { | |
| constructor() { | |
| const set = DIFFICULTY[currentDifficulty]; | |
| const isBomb = Math.random() < set.bomb; | |
| this.config = isBomb ? FRUIT_TYPES[FRUIT_TYPES.length - 1] : FRUIT_TYPES[Math.floor(Math.random() * (FRUIT_TYPES.length - 1))]; | |
| this.x = Math.random() * (width - 100) + 50; | |
| this.y = height + 60; | |
| this.radius = 45; | |
| this.vx = (width / 2 - this.x) * 0.012 + (Math.random() - 0.5) * 6; | |
| this.vy = -(Math.random() * 7 + 12) * set.speed; | |
| this.rot = 0; | |
| this.rotVel = (Math.random() - 0.5) * 0.15; | |
| this.sliced = false; | |
| } | |
| update() { | |
| if (isPaused) return true; | |
| this.vy += 0.25; // Gravity | |
| this.x += this.vx; this.y += this.vy; | |
| this.rot += this.rotVel; | |
| return this.y < height + 100; | |
| } | |
| draw() { | |
| gctx.save(); | |
| gctx.translate(this.x, this.y); | |
| gctx.rotate(this.rot); | |
| gctx.font = '64px Arial'; | |
| gctx.textAlign = 'center'; | |
| gctx.textBaseline = 'middle'; | |
| // Shadow for polish | |
| gctx.shadowBlur = 10; | |
| gctx.shadowColor = 'rgba(0,0,0,0.4)'; | |
| gctx.fillText(this.config.emoji, 0, 0); | |
| gctx.restore(); | |
| } | |
| } | |
| class Splat { | |
| constructor(x, y, color) { | |
| this.x = x; this.y = y; this.color = color; | |
| this.vx = (Math.random() - 0.5) * 14; | |
| this.vy = (Math.random() - 0.5) * 14; | |
| this.life = 1.0; | |
| this.size = Math.random() * 8 + 4; | |
| } | |
| update() { | |
| this.x += this.vx; this.y += this.vy; | |
| this.life -= 0.04; | |
| return this.life > 0; | |
| } | |
| draw() { | |
| gctx.globalAlpha = this.life; | |
| gctx.fillStyle = this.color; | |
| gctx.beginPath(); | |
| gctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); | |
| gctx.fill(); | |
| gctx.globalAlpha = 1; | |
| } | |
| } | |
| function spawn() { | |
| if (!gameActive || isPaused) return; | |
| fruits.push(new Fruit()); | |
| spawnTimer = setTimeout(spawn, DIFFICULTY[currentDifficulty].rate + Math.random() * 800); | |
| } | |
| function handleSlice(x1, y1, x2, y2) { | |
| if (!gameActive || isPaused) return; | |
| fruits.forEach(f => { | |
| if (f.sliced) return; | |
| // Collision: Point to line distance | |
| const dist = Math.abs((y2 - y1) * f.x - (x2 - x1) * f.y + x2 * y1 - y2 * x1) / | |
| Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)); | |
| const dCenter = Math.hypot(f.x - (x1+x2)/2, f.y - (y1+y2)/2); | |
| if (dist < f.radius && dCenter < f.radius * 1.5) { | |
| f.sliced = true; | |
| if (f.config.isBomb) { | |
| AudioEngine.boom(); | |
| gameOver(); | |
| } else { | |
| score += (currentDifficulty === 'hard' ? 20 : 10); | |
| scoreDisplay.innerText = score; | |
| AudioEngine.splat(); | |
| for(let i=0; i<12; i++) particles.push(new Splat(f.x, f.y, f.config.juice)); | |
| setTimeout(() => fruits = fruits.filter(i => i !== f), 0); | |
| } | |
| } | |
| }); | |
| } | |
| function startGame(resume = false) { | |
| AudioEngine.init(); | |
| if (!resume) score = 0; | |
| scoreDisplay.innerText = score; | |
| fruits = []; particles = []; gameActive = true; isPaused = false; | |
| document.getElementById('home-menu').classList.add('hidden'); | |
| document.getElementById('pause-menu').classList.add('hidden'); | |
| document.getElementById('hud').classList.remove('hidden'); | |
| if (spawnTimer) clearTimeout(spawnTimer); | |
| spawn(); | |
| } | |
| function gameOver() { | |
| gameActive = false; | |
| const best = localStorage.getItem('fruit_best_v2') || 0; | |
| if (score > best) localStorage.setItem('fruit_best_v2', score); | |
| localStorage.removeItem('fruit_save_v2'); | |
| document.getElementById('hud').classList.add('hidden'); | |
| document.getElementById('home-menu').classList.remove('hidden'); | |
| document.getElementById('resume-btn').classList.add('hidden'); | |
| loadStats(); | |
| } | |
| function saveAndExit() { | |
| localStorage.setItem('fruit_save_v2', JSON.stringify({ score, diff: currentDifficulty })); | |
| gameActive = false; isPaused = false; | |
| document.getElementById('pause-menu').classList.add('hidden'); | |
| document.getElementById('hud').classList.add('hidden'); | |
| document.getElementById('home-menu').classList.remove('hidden'); | |
| loadStats(); | |
| } | |
| function loadStats() { | |
| const best = localStorage.getItem('fruit_best_v2') || 0; | |
| document.getElementById('best-score').innerText = best; | |
| const save = localStorage.getItem('fruit_save_v2'); | |
| if (save) document.getElementById('resume-btn').classList.remove('hidden'); | |
| } | |
| // UI Interactions | |
| document.getElementById('start-btn').addEventListener('click', (e) => { e.stopPropagation(); startGame(false); }); | |
| document.getElementById('resume-btn').addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const save = JSON.parse(localStorage.getItem('fruit_save_v2')); | |
| if (save) { score = save.score; currentDifficulty = save.diff; startGame(true); } | |
| }); | |
| document.getElementById('pause-trigger').addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| isPaused = true; | |
| document.getElementById('pause-menu').classList.remove('hidden'); | |
| }); | |
| document.getElementById('unpause-btn').addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| isPaused = false; | |
| document.getElementById('pause-menu').classList.add('hidden'); | |
| spawn(); | |
| }); | |
| document.getElementById('save-exit-btn').addEventListener('click', (e) => { e.stopPropagation(); saveAndExit(); }); | |
| document.querySelectorAll('.diff-btn').forEach(b => { | |
| b.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| document.querySelectorAll('.diff-btn').forEach(btn => btn.classList.remove('active')); | |
| b.classList.add('active'); | |
| currentDifficulty = b.dataset.diff; | |
| }); | |
| }); | |
| function handlePointer(e) { | |
| if (!gameActive || isPaused) return; | |
| // Don't draw slash if clicking buttons | |
| if (e.target.closest('button')) return; | |
| let x, y; | |
| if (e.touches && e.touches.length > 0) { | |
| x = e.touches[0].clientX; y = e.touches[0].clientY; | |
| } else { | |
| x = e.clientX; y = e.clientY; | |
| } | |
| if (x === undefined) return; | |
| if (e.type === 'mousedown' || e.type === 'touchstart') { | |
| isSlicing = true; lastPointer = { x, y }; | |
| bladeTrail = [{ x, y }]; | |
| AudioEngine.swish(); | |
| } else if (isSlicing && (e.type === 'mousemove' || e.type === 'touchmove')) { | |
| handleSlice(lastPointer.x, lastPointer.y, x, y); | |
| bladeTrail.push({ x, y }); | |
| if (bladeTrail.length > 10) bladeTrail.shift(); | |
| lastPointer = { x, y }; | |
| } else { | |
| isSlicing = false; bladeTrail = []; | |
| } | |
| } | |
| ['mousedown','mousemove','mouseup','touchstart','touchmove','touchend'].forEach(evt => { | |
| window.addEventListener(evt, (e) => { | |
| // Only prevent default on Canvas to avoid breaking menu buttons | |
| if (gameActive && !isPaused && e.target.tagName === 'CANVAS') { | |
| if (e.cancelable) e.preventDefault(); | |
| } | |
| handlePointer(e); | |
| }, { passive: false }); | |
| }); | |
| function animate() { | |
| gctx.clearRect(0, 0, width, height); | |
| // Render Trail | |
| if (bladeTrail.length > 1) { | |
| gctx.beginPath(); | |
| gctx.shadowBlur = 10; gctx.shadowColor = '#fff'; | |
| gctx.strokeStyle = '#fff'; gctx.lineWidth = 4; gctx.lineCap = 'round'; | |
| gctx.moveTo(bladeTrail[0].x, bladeTrail[0].y); | |
| bladeTrail.forEach(p => gctx.lineTo(p.x, p.y)); | |
| gctx.stroke(); | |
| gctx.shadowBlur = 0; | |
| } | |
| particles = particles.filter(p => { | |
| const a = p.update(); | |
| if (a) p.draw(); | |
| return a; | |
| }); | |
| fruits = fruits.filter(f => { | |
| const a = f.update(); | |
| f.draw(); | |
| return a; | |
| }); | |
| requestAnimationFrame(animate); | |
| } | |
| loadStats(); | |
| animate(); | |
| </script> | |
| </body> | |
| </html> |