Mobile-emulator / fruitninja.html
ngiactcp
Add several new playable games to the application
61bdef8
<!DOCTYPE html>
<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 !important; }
.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>