<style>
* {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
touch-action: none;
box-sizing: border-box;
}
body {
font-family: 'Zen Maru Gothic', sans-serif;
background: radial-gradient(circle at center, #2e1065 0%, #0f172a 100%);
color: #ffffff;
overflow: hidden;
position: fixed;
width: 100%;
height: 100%;
}
.glass-card {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.15);
}
.glass-btn {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.glass-btn:active {
transform: scale(0.94);
background: rgba(255, 255, 255, 0.25);
}
canvas {
display: block;
}
</style>
<!-- Top Bar HUD -->
<header class="w-full max-w-md px-4 pt-3 pb-1 flex justify-between items-center z-10">
<!-- Score Box -->
<div class="glass-card px-3.5 py-1.5 rounded-2xl flex flex-col items-center min-w-[95px]">
<span class="text-[10px] font-bold uppercase tracking-wider text-purple-300">SCORE</span>
<span id="scoreText" class="text-2xl font-black text-amber-300 tracking-tight">0</span>
</div>
<!-- Next Item Preview -->
<div class="glass-card px-4 py-1.5 rounded-2xl flex flex-col items-center min-w-[85px]">
<span class="text-[10px] font-bold uppercase tracking-wider text-purple-300">NEXT</span>
<div id="nextPreview" class="w-8 h-8 flex items-center justify-center text-2xl mt-0.5">π</div>
</div>
<!-- High Score Box & Sound Toggle -->
<div class="flex gap-2">
<div class="glass-card px-3 py-1.5 rounded-2xl flex flex-col items-center min-w-[80px]">
<span class="text-[10px] font-bold uppercase tracking-wider text-purple-300">BEST</span>
<span id="bestText" class="text-lg font-bold text-emerald-400">0</span>
</div>
<button id="soundToggleBtn" class="glass-btn w-10 h-10 rounded-2xl flex items-center justify-center text-base text-purple-200 hover:text-white transition-colors">
<i id="soundIcon" class="fa-solid fa-volume-high"></i>
</button>
</div>
</header>
<!-- Main Game Box Container -->
<main class="relative flex-1 w-full max-w-md flex flex-col items-center justify-center px-3 pb-2">
<div id="canvasContainer" class="relative w-full h-full rounded-3xl overflow-hidden glass-card shadow-2xl flex justify-center items-center border border-white/20">
<canvas id="gameCanvas" class="w-full h-full"></canvas>
<!-- Aim Guide Line & Preview Dropper -->
<div id="dropGuide" class="absolute top-0 bottom-0 pointer-events-none hidden flex flex-col items-center">
<div class="w-0.5 h-full border-r-2 border-dashed border-white/50"></div>
</div>
<div id="droppingEmoji" class="absolute pointer-events-none transform -translate-x-1/2 -translate-y-1/2 hidden text-center flex items-center justify-center">
</div>
<!-- Red Deadline Indicator (Clean red dashed line with NO text) -->
<div id="dangerLine" class="absolute w-full border-b-2 border-dashed border-red-500/80 top-[13%] pointer-events-none hidden transition-opacity duration-300 shadow-[0_0_12px_rgba(239,68,68,0.7)]">
</div>
</div>
</main>
<!-- Evolution Hierarchy Bottom Bar -->
<footer class="w-full max-w-md px-3 pb-3 z-10">
<div class="glass-card p-2 rounded-2xl flex justify-between items-center overflow-x-auto gap-1 text-xs" id="evolutionBar">
<!-- Populated via JS -->
</div>
</footer>
<!-- Game Over Modal Overlay -->
<div id="gameOverModal" class="fixed inset-0 bg-black/80 backdrop-blur-md z-50 flex items-center justify-center p-6 hidden">
<div class="glass-card bg-slate-900/95 border border-slate-700/80 p-6 rounded-3xl max-w-xs w-full text-center flex flex-col items-center shadow-2xl">
<div class="w-16 h-16 mb-3 rounded-2xl bg-gradient-to-br from-red-500 to-pink-600 flex items-center justify-center shadow-lg shadow-red-500/40 text-3xl">
π₯
</div>
<h2 class="text-3xl font-black text-white mb-1">GAME OVER</h2>
<p class="text-xs text-slate-300 mb-5">γ³γ³γγγγγγ΅γγ¦γγΎγγΎγγοΌ</p>
<div class="w-full space-y-2 mb-6">
<div class="p-3 rounded-2xl bg-slate-800/80 border border-slate-700/60 flex justify-between items-center">
<span class="text-xs text-slate-400 font-semibold">FINAL SCORE</span>
<span id="finalScoreText" class="text-2xl font-black text-amber-300">0</span>
</div>
<div class="p-3 rounded-2xl bg-slate-800/80 border border-slate-700/60 flex justify-between items-center">
<span class="text-xs text-slate-400 font-semibold">BEST SCORE</span>
<span id="finalBestText" class="text-lg font-bold text-emerald-400">0</span>
</div>
</div>
<button id="restartBtn" class="w-full py-3.5 rounded-2xl bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 font-black text-white text-base shadow-lg shadow-purple-500/40 active:scale-95 transition-all">
γγδΈεΊ¦γγγΆ
</button>
</div>
</div>
<script>
// Web Audio Procedural Synthesizer
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let isMuted = false;
const audio = {
init() {
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
},
playPop(tierIndex) {
if (isMuted) return;
this.init();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
// Frequency rises smoothly with higher fruit stages
const baseFreq = 420 - (tierIndex * 24);
osc.type = 'sine';
osc.frequency.setValueAtTime(Math.max(130, baseFreq), audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(Math.max(180, baseFreq * 1.6), audioCtx.currentTime + 0.12);
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.12);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.12);
},
playGameOver() {
if (isMuted) return;
this.init();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(220, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 0.6);
gain.gain.setValueAtTime(0.35, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.6);
}
};
// 12 Tier Fruit Hierarchy
const TIERS = [
{ level: 0, name: 'γγγγγΌ', radius: 16, points: 2, emoji: 'π', color: '#ef4444', shape: 'circle' },
{ level: 1, name: 'γγ‘γ', radius: 22, points: 4, emoji: 'π', color: '#f43f5e', shape: 'strawberry' },
{ level: 2, name: 'γΆγ©γ', radius: 29, points: 8, emoji: 'π', color: '#8b5cf6', shape: 'bunch' },
{ level: 3, name: 'γγ³γγ³', radius: 36, points: 16, emoji: 'π', color: '#f97316', shape: 'dekopon' },
{ level: 4, name: 'γͺγ¬γ³γΈ', radius: 43, points: 32, emoji: 'π', color: '#f97316', shape: 'circle' },
{ level: 5, name: 'γγγ', radius: 52, points: 64, emoji: 'π', color: '#dc2626', shape: 'apple' },
{ level: 6, name: 'ζ΄γͺγ', radius: 61, points: 128, emoji: 'π', color: '#84cc16', shape: 'pear' },
{ level: 7, name: 'γγ', radius: 71, points: 256, emoji: 'π', color: '#f472b6', shape: 'peach' },
{ level: 8, name: 'γγ€γγγγ«', radius: 82, points: 512, emoji: 'π', color: '#eab308', shape: 'pineapple' },
{ level: 9, name: 'γ‘γγ³', radius: 94, points: 1024,emoji: 'π', color: '#22c55e', shape: 'circle' },
{ level: 10, name: 'γΉγ€γ«', radius: 108,points: 2048,emoji: 'π', color: '#15803d', shape: 'circle' },
{ level: 11, name: 'γγ³γ°ηε ', radius: 120,points: 4096,emoji: 'π', color: '#f59e0b', shape: 'crown' }
];
// Matter.js Engine Aliases
const { Engine, Bodies, Composite, Events, Vertices } = Matter;
// DOM Element Cache
const canvasContainer = document.getElementById('canvasContainer');
const gameCanvas = document.getElementById('gameCanvas');
const scoreText = document.getElementById('scoreText');
const bestText = document.getElementById('bestText');
const nextPreview = document.getElementById('nextPreview');
const dropGuide = document.getElementById('dropGuide');
const droppingEmoji = document.getElementById('droppingEmoji');
const dangerLine = document.getElementById('dangerLine');
const evolutionBar = document.getElementById('evolutionBar');
const gameOverModal = document.getElementById('gameOverModal');
const restartBtn = document.getElementById('restartBtn');
const finalScoreText = document.getElementById('finalScoreText');
const finalBestText = document.getElementById('finalBestText');
const soundToggleBtn = document.getElementById('soundToggleBtn');
const soundIcon = document.getElementById('soundIcon');
// State Flags & Variables
let engine;
let score = 0;
let bestScore = parseInt(localStorage.getItem('suika_best_score') || '0');
let currentNextTierIndex = 0;
let currentDroppingTierIndex = 0;
let isAiming = false;
let canDrop = true;
let aimX = 0;
let isGameOver = false;
let dangerTimer = null;
let particles = [];
let floatingScores = [];
let width, height;
let wallThickness = 60;
function initPhysics() {
width = canvasContainer.clientWidth;
height = canvasContainer.clientHeight;
gameCanvas.width = width;
gameCanvas.height = height;
engine = Engine.create({
gravity: { x: 0, y: 1.25 }
});
createWalls();
Events.on(engine, 'collisionStart', handleCollisions);
requestAnimationFrame(gameLoop);
prepareNextDrop();
renderEvolutionBar();
bestText.textContent = bestScore;
}
function createWalls() {
const ground = Bodies.rectangle(width / 2, height + wallThickness / 2 - 8, width * 2, wallThickness, {
isStatic: true,
friction: 0.6,
restitution: 0.2
});
const leftWall = Bodies.rectangle(-wallThickness / 2 + 6, height / 2, wallThickness, height * 2, {
isStatic: true,
friction: 0.6,
restitution: 0.2
});
const rightWall = Bodies.rectangle(width + wallThickness / 2 - 6, height / 2, wallThickness, height * 2, {
isStatic: true,
friction: 0.6,
restitution: 0.2
});
Composite.add(engine.world, [ground, leftWall, rightWall]);
}
function createFruitVertices(r, shape) {
const points = 16;
const verts = [];
for (let i = 0; i < points; i++) {
const angle = (i / points) * Math.PI * 2;
let scaleR = 1.0;
switch (shape) {
case 'strawberry': // Heart / tapered cone
if (angle > Math.PI * 0.25 && angle < Math.PI * 0.75) {
scaleR = 0.72 + 0.28 * Math.abs(Math.cos(angle));
} else if (angle > Math.PI * 1.15 && angle < Math.PI * 1.85) {
scaleR = 1.08;
}
break;
case 'bunch': // Bumpy organic cluster
scaleR = 0.86 + 0.14 * Math.sin(angle * 3);
break;
case 'dekopon': // Round with top bump
if (angle > Math.PI * 1.35 && angle < Math.PI * 1.65) {
scaleR = 1.22;
} else {
scaleR = 0.96;
}
break;
case 'apple': // Top stem indentation & bottom taper
if (angle > Math.PI * 0.35 && angle < Math.PI * 0.65) {
scaleR = 0.88;
} else if (angle > Math.PI * 1.35 && angle < Math.PI * 1.65) {
scaleR = 0.82;
}
break;
case 'pear': // Bottom-heavy teardrop
if (angle > Math.PI * 0.25 && angle < Math.PI * 0.75) {
scaleR = 1.15;
} else if (angle > Math.PI * 1.25 && angle < Math.PI * 1.75) {
scaleR = 0.78;
}
break;
case 'peach': // Top crease notch
if (angle > Math.PI * 1.35 && angle < Math.PI * 1.65) {
scaleR = 0.82;
}
break;
case 'pineapple': // Cylindrical barrel with flared top
if (angle > Math.PI * 1.25 && angle < Math.PI * 1.75) {
scaleR = 1.18 + 0.08 * Math.sin(angle * 4);
} else {
scaleR = 0.88 + 0.2 * Math.sin(angle) ** 2;
}
break;
case 'crown': // Wide flared top points
if (angle > Math.PI * 0.25 && angle < Math.PI * 0.75) {
scaleR = 0.85;
} else {
scaleR = 1.12;
}
break;
case 'circle':
default:
scaleR = 1.0;
break;
}
const x = Math.cos(angle) * r * scaleR;
const y = Math.sin(angle) * r * scaleR;
verts.push({ x, y });
}
return Vertices.create(verts);
}
function createFruitBody(x, y, tierIndex) {
const tier = TIERS[tierIndex];
const r = tier.radius;
let body;
if (tier.shape !== 'circle') {
const verts = createFruitVertices(r, tier.shape);
body = Bodies.fromVertices(x, y, [verts], {
restitution: 0.18,
friction: 0.45,
density: 0.002 * (1 + tierIndex * 0.08),
label: `fruit_${tierIndex}`
});
} else {
body = Bodies.circle(x, y, r, {
restitution: 0.18,
friction: 0.45,
density: 0.002 * (1 + tierIndex * 0.08),
label: `fruit_${tierIndex}`
});
}
body.tierIndex = tierIndex;
return body;
}
function handleCollisions(event) {
if (isGameOver) return;
const pairs = event.pairs;
for (let i = 0; i < pairs.length; i++) {
const { bodyA, bodyB } = pairs[i];
if (bodyA.tierIndex !== undefined && bodyB.tierIndex !== undefined) {
if (bodyA.tierIndex === bodyB.tierIndex && !bodyA.isMerging && !bodyB.isMerging) {
const tierIdx = bodyA.tierIndex;
if (tierIdx < TIERS.length - 1) {
bodyA.isMerging = true;
bodyB.isMerging = true;
const midX = (bodyA.position.x + bodyB.position.x) / 2;
const midY = (bodyA.position.y + bodyB.position.y) / 2;
Composite.remove(engine.world, [bodyA, bodyB]);
const nextTierIdx = tierIdx + 1;
const mergedFruit = createFruitBody(midX, midY, nextTierIdx);
Composite.add(engine.world, mergedFruit);
const gainedPoints = TIERS[nextTierIdx].points;
score += gainedPoints;
scoreText.textContent = score;
if (score > bestScore) {
bestScore = score;
bestText.textContent = bestScore;
localStorage.setItem('suika_best_score', bestScore.toString());
}
spawnParticles(midX, midY, TIERS[nextTierIdx].color);
addFloatingScore(midX, midY, `+${gainedPoints}`);
audio.playPop(nextTierIdx);
}
}
}
}
}
function prepareNextDrop() {
currentDroppingTierIndex = currentNextTierIndex;
currentNextTierIndex = Math.floor(Math.random() * 5); // Random fruit (Cherry to Dekopon)
const nextTier = TIERS[currentNextTierIndex];
nextPreview.textContent = nextTier.emoji;
}
function updateAimPosition(clientX) {
if (!canDrop || isGameOver) return;
const rect = canvasContainer.getBoundingClientRect();
const rawX = clientX - rect.left;
const radius = TIERS[currentDroppingTierIndex].radius;
aimX = Math.max(radius + 12, Math.min(width - radius - 12, rawX));
dropGuide.style.left = `${aimX}px`;
dropGuide.classList.remove('hidden');
const topY = 42;
droppingEmoji.style.left = `${aimX}px`;
droppingEmoji.style.top = `${topY}px`;
const droppingTier = TIERS[currentDroppingTierIndex];
droppingEmoji.style.fontSize = `${droppingTier.radius * 1.3}px`;
droppingEmoji.textContent = droppingTier.emoji;
droppingEmoji.classList.remove('hidden');
}
function executeDrop() {
if (!canDrop || !isAiming || isGameOver) return;
canDrop = false;
isAiming = false;
dropGuide.classList.add('hidden');
droppingEmoji.classList.add('hidden');
const topY = 42;
const fruitBody = createFruitBody(aimX, topY, currentDroppingTierIndex);
Composite.add(engine.world, fruitBody);
audio.init();
setTimeout(() => {
if (!isGameOver) {
prepareNextDrop();
canDrop = true;
}
}, 550);
}
// Touch Event Listeners for iPhone
canvasContainer.addEventListener('touchstart', (e) => {
if (e.touches.length > 0) {
isAiming = true;
updateAimPosition(e.touches[0].clientX);
}
}, { passive: true });
canvasContainer.addEventListener('touchmove', (e) => {
if (isAiming && e.touches.length > 0) {
updateAimPosition(e.touches[0].clientX);
}
}, { passive: true });
canvasContainer.addEventListener('touchend', () => {
executeDrop();
});
// Mouse Listeners for desktop preview
canvasContainer.addEventListener('mousedown', (e) => {
isAiming = true;
updateAimPosition(e.clientX);
});
canvasContainer.addEventListener('mousemove', (e) => {
if (isAiming) updateAimPosition(e.clientX);
});
canvasContainer.addEventListener('mouseup', () => {
executeDrop();
});
function spawnParticles(x, y, color) {
for (let i = 0; i < 16; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 6 + 2;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
radius: Math.random() * 4 + 2,
color: color,
alpha: 1.0
});
}
}
function addFloatingScore(x, y, text) {
floatingScores.push({
x, y,
text,
vy: -1.8,
alpha: 1.0
});
}
const ctx = gameCanvas.getContext('2d');
function gameLoop() {
Engine.update(engine, 1000 / 60);
ctx.clearRect(0, 0, width, height);
const dangerY = height * 0.13;
dangerLine.style.top = `${dangerY}px`;
dangerLine.classList.remove('hidden');
let isAnyFruitOverDanger = false;
const bodies = Composite.allBodies(engine.world);
bodies.forEach(body => {
if (body.tierIndex !== undefined) {
const { x, y } = body.position;
const tier = TIERS[body.tierIndex];
if (y - tier.radius < dangerY && body.velocity.y > -0.5 && body.speed < 1.0) {
isAnyFruitOverDanger = true;
}
ctx.save();
ctx.translate(x, y);
ctx.rotate(body.angle);
ctx.font = `${tier.radius * 1.3}px 'Zen Maru Gothic', sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(tier.emoji, 0, 2);
ctx.restore();
}
});
// Danger Timer Trigger
if (isAnyFruitOverDanger && !isGameOver) {
if (!dangerTimer) {
dangerTimer = setTimeout(() => {
triggerGameOver();
}, 2200);
}
} else if (!isAnyFruitOverDanger && dangerTimer) {
clearTimeout(dangerTimer);
dangerTimer = null;
}
// Render Juice Particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.alpha -= 0.03;
ctx.save();
ctx.globalAlpha = Math.max(p.alpha, 0);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
if (p.alpha <= 0) particles.splice(i, 1);
}
// Render Floating Score Text
for (let i = floatingScores.length - 1; i >= 0; i--) {
const fs = floatingScores[i];
fs.y += fs.vy;
fs.alpha -= 0.025;
ctx.save();
ctx.globalAlpha = Math.max(fs.alpha, 0);
ctx.font = '900 20px "Zen Maru Gothic", sans-serif';
ctx.fillStyle = '#fef08a';
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 6;
ctx.textAlign = 'center';
ctx.fillText(fs.text, fs.x, fs.y);
ctx.restore();
if (fs.alpha <= 0) floatingScores.splice(i, 1);
}
requestAnimationFrame(gameLoop);
}
function triggerGameOver() {
isGameOver = true;
audio.playGameOver();
finalScoreText.textContent = score;
finalBestText.textContent = bestScore;
gameOverModal.classList.remove('hidden');
}
function restartGame() {
const bodies = Composite.allBodies(engine.world);
bodies.forEach(body => {
if (body.tierIndex !== undefined) {
Composite.remove(engine.world, body);
}
});
score = 0;
scoreText.textContent = '0';
isGameOver = false;
canDrop = true;
if (dangerTimer) {
clearTimeout(dangerTimer);
dangerTimer = null;
}
gameOverModal.classList.add('hidden');
prepareNextDrop();
}
function renderEvolutionBar() {
evolutionBar.innerHTML = '';
TIERS.forEach((tier, i) => {
const item = document.createElement('div');
item.className = 'flex flex-col items-center min-w-[26px]';
item.innerHTML = `
<span class="text-base">${tier.emoji}</span>
<span class="text-[9px] text-purple-300 font-bold mt-0.5">${i + 1}</span>
`;
evolutionBar.appendChild(item);
});
}
soundToggleBtn.addEventListener('click', () => {
isMuted = !isMuted;
soundIcon.className = isMuted ? 'fa-solid fa-volume-xmark text-red-400' : 'fa-solid fa-volume-high text-purple-200';
});
restartBtn.addEventListener('click', restartGame);
window.onload = () => {
initPhysics();
};
window.addEventListener('resize', () => {
if (canvasContainer) {
width = canvasContainer.clientWidth;
height = canvasContainer.clientHeight;
gameCanvas.width = width;
gameCanvas.height = height;
}
});
</script>