<!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”>
<title>Fruit Merge Game</title>
<script src=”https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js”></script>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #fff8e1;
font-family: ‘Segoe UI’, Tahoma, Geneva, Verdana, sans-serif;
touch-action: none;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: 100dvh;
user-select: none;
-webkit-user-select: none;
}
#evolution-guide {
width: 100%;
max-width: 450px;
height: 5dvh;
display: flex;
justify-content: space-evenly;
align-items: center;
padding: 0 5px;
box-sizing: border-box;
font-size: min(4vw, 2.5dvh);
color: #f57c00;
}
#evolution-guide .arrow { opacity: 0.5; font-size: 0.8em; font-weight: bold; }
#game-container {
position: relative;
width: 100vw;
max-width: 450px;
height: 95dvh;
max-height: 900px;
background-color: #ffecb3;
border-left: 12px solid #ffb300;
border-right: 12px solid #ffb300;
border-bottom: 12px solid #ffb300;
border-radius: 0 0 20px 20px;
box-sizing: border-box;
box-shadow: 0 15px 35px rgba(255, 160, 0, 0.2);
overflow: hidden;
}
canvas { display: block; width: 100%; height: 100%; }
#ui {
position: absolute; top: 20px; left: 20px; color: #333; z-index: 10;
pointer-events: none;
}
.ui-row {
display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
}
.ui-box {
background: rgba(255, 255, 255, 0.9); padding: 8px 15px; border-radius: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1); border: 2px solid #ffca28;
display: inline-block;
}
#score { font-size: 24px; font-weight: 800; }
#high-score { font-size: 16px; font-weight: 700; color: #e65100; margin-bottom: 8px; }
#btn-toggle-tools {
pointer-events: auto;
background: rgba(255, 255, 255, 0.9);
border: 2px solid #ffca28;
border-radius: 50%;
width: 42px;
height: 42px;
font-size: 22px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.2s, background-color 0.2s;
}
#btn-toggle-tools:active { transform: scale(0.9); }
#btn-toggle-tools:hover { background: #fff3e0; }
#powerups {
pointer-events: auto; display: flex; flex-wrap: wrap; gap: 8px;
transition: opacity 0.3s ease, transform 0.3s ease, max-height 0.3s ease;
transform-origin: top left;
max-height: 100px;
opacity: 1;
}
#powerups.hidden {
opacity: 0;
transform: scaleY(0);
max-height: 0;
pointer-events: none;
}
.powerup-btn {
color: white; border-radius: 15px; padding: 8px 12px; font-size: 14px;
font-weight: bold; cursor: pointer; box-shadow: 0 4px 6px rgba(0,0,0,0.2);
transition: all 0.2s;
}
.powerup-btn:active { transform: scale(0.95); }
.powerup-btn:disabled { background: #9e9e9e !important; border-color: #757575 !important; color: #e0e0e0 !important; cursor: not-allowed; transform: none; box-shadow: none; }
/* Button Colors */
.slice-btn { background: #e91e63; border: 2px solid #c2185b; }
.slice-btn.active { background: #ff4081; box-shadow: 0 0 15px #ff4081; transform: scale(1.05); }
.quake-btn { background: #ff9800; border: 2px solid #e65100; }
.rainbow-btn { background: #9c27b0; border: 2px solid #7b1fa2; }
.rainbow-btn.active { background: #e040fb; box-shadow: 0 0 15px #e040fb; transform: scale(1.05); }
.auto-btn { background: #2196f3; border: 2px solid #1976d2; }
.auto-btn.active { background: #64b5f6; box-shadow: 0 0 15px #64b5f6; transform: scale(1.05); }
#next-piece {
position: absolute; top: 20px; right: 20px; background: rgba(255, 255, 255, 0.9);
padding: 10px 15px; border-radius: 20px; text-align: center; font-size: 14px; font-weight: bold;
box-shadow: 0 4px 6px rgba(0,0,0,0.1); pointer-events: none; z-index: 10; border: 2px solid #ffca28;
}
#next-emoji { font-size: 32px; margin-top: 2px; }
#game-over {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95); padding: 30px 40px; border-radius: 20px;
text-align: center; display: none; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 20;
width: 70%; border: 3px solid #ffb300;
}
#game-over h1 { margin-top: 0; color: #e65100; font-size: 32px; }
#final-score { font-size: 24px; font-weight: bold; }
#game-over button {
padding: 12px 24px; font-size: 20px; background: #4caf50; color: white; border: none;
border-radius: 12px; cursor: pointer; margin-top: 15px; font-weight: bold;
box-shadow: 0 4px 6px rgba(76, 175, 80, 0.4); transition: transform 0.1s; width: 100%;
}
#game-over button:active { transform: scale(0.95); }
</style>
</head>
<body>
<div id=”evolution-guide”></div>
<div id=”game-container”>
<div id=”ui”>
<div class=”ui-row”>
<div id=”score” class=”ui-box”>Score: 0</div>
<button id=”btn-toggle-tools” onclick=”toggleTools()” title=”Toggle Tools”>βοΈ</button>
</div>
<div id=”high-score” class=”ui-box”>Best: 0</div>
<!– Added the “hidden” class here so it starts closed –>
<div id=”powerups” class=”hidden”>
<button id=”btn-slice” class=”powerup-btn slice-btn” onclick=”toggleSlice()”>πͺ Slice (2)</button>
<button id=”btn-quake” class=”powerup-btn quake-btn” onclick=”triggerQuake()”>π Quake (3)</button>
<button id=”btn-rainbow” class=”powerup-btn rainbow-btn” onclick=”toggleRainbow()”>π Rainbow (2)</button>
<button id=”btn-auto” class=”powerup-btn auto-btn” onclick=”toggleAutoPlay()”>π€ Auto</button>
</div>
</div>
<div id=”next-piece”>
Next
<div id=”next-emoji”>π</div>
</div>
<div id=”game-over”>
<h1>Basket Full! π</h1>
<p id=”final-score”>Score: 0</p>
<button onclick=”resetGame()”>Play Again</button>
</div>
<canvas id=”gameCanvas”></canvas>
</div>
<script type=”module”>
import { initializeApp } from “https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js”;
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from “https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js”;
import { getFirestore, doc, getDoc, setDoc } from “https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js”;
let manualFirebaseConfig = null;
const firebaseConfig = typeof __firebase_config !== ‘undefined’ ? JSON.parse(__firebase_config) : manualFirebaseConfig;
const appId = typeof __app_id !== ‘undefined’ ? __app_id : ‘standalone-app’;
let db, auth, currentUser;
let bestScore = 0;
if (firebaseConfig) {
const app = initializeApp(firebaseConfig);
auth = getAuth(app); db = getFirestore(app);
const initAuth = async () => {
if (typeof __initial_auth_token !== ‘undefined’ && __initial_auth_token) await signInWithCustomToken(auth, __initial_auth_token);
else await signInAnonymously(auth);
};
initAuth();
onAuthStateChanged(auth, async (user) => {
if (user) { currentUser = user; await loadBestScore(); }
});
}
async function loadBestScore() {
if (!currentUser || !db) return;
try {
const scoreRef = doc(db, ‘artifacts’, appId, ‘users’, currentUser.uid, ‘gameData’, ‘highScore’);
const snap = await getDoc(scoreRef);
if (snap.exists()) { bestScore = snap.data().score || 0; document.getElementById(‘high-score’).innerText = ‘Best: ‘ + bestScore; }
} catch (e) { console.error(“Failed to load best score”, e); }
}
async function saveBestScore(newScore) {
if (!currentUser || !db) return;
try {
const scoreRef = doc(db, ‘artifacts’, appId, ‘users’, currentUser.uid, ‘gameData’, ‘highScore’);
await setDoc(scoreRef, { score: newScore });
} catch (e) { console.error(“Failed to save best score”, e); }
}
// ==========================================
// GAME LOGIC & CONFIG
// ==========================================
const { Engine, Runner, Bodies, Composite, Events, Query } = window.Matter;
const engine = Engine.create();
const world = engine.world;
const container = document.getElementById(‘game-container’);
const canvas = document.getElementById(‘gameCanvas’);
const ctx = canvas.getContext(‘2d’);
const dpr = window.devicePixelRatio || 1;
let width, height, ground, leftWall, rightWall;
const particles = [];
const floatingTexts = [];
let shakeFrames = 0;
let shakeIntensity = 0;
const orbTypes = [
{ level: 0, scale: 1.0, emoji: ‘π’, score: 2, color: ‘#e53935’ },
{ level: 1, scale: 1.3, emoji: ‘π’, score: 4, color: ‘#d81b60’ },
{ level: 2, scale: 1.7, emoji: ‘π’, score: 8, color: ‘#8e24aa’ },
{ level: 3, scale: 2.2, emoji: ‘π’, score: 16, color: ‘#fdd835’ },
{ level: 4, scale: 2.8, emoji: ‘π’, score: 32, color: ‘#fb8c00’ },
{ level: 5, scale: 3.5, emoji: ‘π’, score: 64, color: ‘#e53935’ },
{ level: 6, scale: 4.3, emoji: ‘π’, score: 128, color: ‘#f48fb1’ },
{ level: 7, scale: 5.2, emoji: ‘π₯₯’, score: 256, color: ‘#8d6e63’ },
{ level: 8, scale: 6.2, emoji: ‘π’, score: 512, color: ‘#c0ca33’ },
{ level: 9, scale: 7.3, emoji: ‘π’, score: 1024, color: ‘#fbc02d’ },
{ level: 10, scale: 8.5, emoji: ‘π’, score: 2048, color: ‘#43a047’ }
];
const guideDiv = document.getElementById(‘evolution-guide’);
orbTypes.forEach((type, index) => {
const span = document.createElement(‘span’); span.innerText = type.emoji; guideDiv.appendChild(span);
if (index < orbTypes.length – 1) {
const arrow = document.createElement(‘span’); arrow.className = ‘arrow’; arrow.innerText = ‘βΊ’; guideDiv.appendChild(arrow);
}
});
let score = 0, isGameOver = false, canDrop = true;
let nextDropLevel = Math.floor(Math.random() * 3);
const DROP_COOLDOWN = 600, DEATH_LINE_Y = 120;
let baseR = 20, isAiming = false, aimX = 0;
// Powerup Tracking
let sliceUses = 2, isSliceActive = false;
let quakeUses = 3;
let rainbowUses = 2, isHoldingRainbow = false;
let isAutoPlay = false, autoPlayTimer = null;
let lineBreachStartTime = null;
const GRACE_PERIOD_MS = 3000;
document.getElementById(‘next-emoji’).innerText = orbTypes[nextDropLevel].emoji;
// — UI VISIBILITY TOGGLE —
window.toggleTools = function() {
const powerups = document.getElementById(‘powerups’);
powerups.classList.toggle(‘hidden’);
};
// — FX FUNCTIONS —
function spawnParticles(x, y, color, count) {
for(let i=0; i<count; i++) {
particles.push({
x: x, y: y,
vx: (Math.random() – 0.5) * 12, vy: (Math.random() – 0.5) * 12 – 2,
life: 1.0, color: color, size: Math.random() * 6 + 3
});
}
}
function spawnFloatingText(x, y, text, color, size) {
floatingTexts.push({ x: x, y: y, vy: -1.5, life: 1.0, text: text, color: color, size: size });
}
function triggerShake(level) {
if (level < 3) return;
shakeFrames = 10 + (level * 2); shakeIntensity = level * 1.5;
}
// — POWERUP BUTTONS —
window.toggleSlice = function() {
if (sliceUses <= 0 || isGameOver || isAutoPlay) return;
if (isHoldingRainbow) toggleRainbow(); // Mutually exclusive
isSliceActive = !isSliceActive;
const btn = document.getElementById(‘btn-slice’);
if (isSliceActive) { btn.classList.add(‘active’); btn.innerText = `CANCEL SLICE`; }
else { btn.classList.remove(‘active’); btn.innerText = `πͺ Slice (${sliceUses})`; }
};
window.toggleRainbow = function() {
if (rainbowUses <= 0 || isGameOver || isAutoPlay) return;
if (isSliceActive) toggleSlice(); // Mutually exclusive
isHoldingRainbow = !isHoldingRainbow;
const btn = document.getElementById(‘btn-rainbow’);
if (isHoldingRainbow) {
btn.classList.add(‘active’); btn.innerText = `CANCEL RAINBOW`;
document.getElementById(‘next-emoji’).innerText = ‘π’;
} else {
btn.classList.remove(‘active’); btn.innerText = `π Rainbow (${rainbowUses})`;
document.getElementById(‘next-emoji’).innerText = orbTypes[nextDropLevel].emoji;
}
}
window.triggerQuake = function() {
if (quakeUses <= 0 || isGameOver || isAutoPlay) return;
quakeUses–;
const btn = document.getElementById(‘btn-quake’);
btn.innerText = `π Quake (${quakeUses})`;
if (quakeUses <= 0) btn.disabled = true;
const orbs = Composite.allBodies(world).filter(b => b.label === ‘orb’ || b.label === ‘rainbow’);
orbs.forEach(orb => {
const vx = orb.velocity.x + (Math.random() – 0.5) * 6;
const vy = – (Math.random() * 8 + 6);
window.Matter.Body.setVelocity(orb, { x: vx, y: vy });
});
triggerShake(12);
spawnFloatingText(width / 2, height / 2, “EARTHQUAKE!”, “#ff9800”, 42);
};
window.toggleAutoPlay = function() {
if (isGameOver) return;
isAutoPlay = !isAutoPlay;
const btn = document.getElementById(‘btn-auto’);
if (isAutoPlay) {
btn.classList.add(‘active’);
btn.innerText = `π Stop Auto`;
if (isSliceActive) toggleSlice();
if (isHoldingRainbow) toggleRainbow(); // Cancel rainbow to keep AI simple
performAutoPlayStep();
} else {
btn.classList.remove(‘active’);
btn.innerText = `π€ Auto`;
clearTimeout(autoPlayTimer);
isAiming = false;
}
};
// — AUTO PLAY AI LOGIC —
function performAutoPlayStep() {
if (!isAutoPlay || isGameOver) return;
const orbs = Composite.allBodies(world).filter(b => b.label === ‘orb’);
let highestTopEdge = height;
for (let orb of orbs) {
if (Date.now() – orb.createdAt > 1000) {
const topEdge = orb.position.y – getRadius(orb.orbLevel);
if (topEdge < highestTopEdge) highestTopEdge = topEdge;
}
}
if (highestTopEdge < DEATH_LINE_Y + 100) {
toggleAutoPlay();
spawnFloatingText(width / 2, height / 2, “AI PAUSED: DANGER!”, “#e91e63”, 28);
return;
}
if (canDrop && !isAiming) {
let targetX = width / 2;
const matchingOrbs = orbs.filter(b => b.orbLevel === nextDropLevel);
if (matchingOrbs.length > 0) {
matchingOrbs.sort((a, b) => a.position.y – b.position.y);
targetX = matchingOrbs[0].position.x;
targetX += (Math.random() – 0.5) * 30;
} else {
const radius = getRadius(nextDropLevel);
targetX = radius + Math.random() * (width – radius * 2);
}
const radius = getRadius(nextDropLevel);
targetX = getSafeX(targetX, radius);
isAiming = true;
aimX = targetX;
autoPlayTimer = setTimeout(() => {
if (!isAutoPlay || isGameOver) { isAiming = false; return; }
isAiming = false;
dropOrb(aimX);
autoPlayTimer = setTimeout(performAutoPlayStep, 800 + Math.random() * 400);
}, 400);
} else {
autoPlayTimer = setTimeout(performAutoPlayStep, 200);
}
}
function getRadius(levelId) { return baseR * orbTypes[levelId].scale; }
function getSafeX(x, radius) { return Math.max(radius, Math.min(width – radius, x)); }
function createOrb(x, y, levelId) {
const radius = getRadius(levelId);
const orb = Bodies.circle(x, y, radius, { restitution: 0.2, friction: 0.1, density: 0.001 * (levelId + 1), label: ‘orb’ });
orb.orbLevel = levelId; orb.createdAt = Date.now(); return orb;
}
function dropOrb(x) {
if (!canDrop || isGameOver) return;
let droppedRainbow = false;
if (isHoldingRainbow) {
droppedRainbow = true;
isHoldingRainbow = false;
rainbowUses–;
const btn = document.getElementById(‘btn-rainbow’);
btn.classList.remove(‘active’); btn.innerText = `π Rainbow (${rainbowUses})`;
if (rainbowUses <= 0) btn.disabled = true;
}
const radius = droppedRainbow ? getRadius(2) : getRadius(nextDropLevel);
const safeX = getSafeX(x, radius);
let orb;
if (droppedRainbow) {
orb = Bodies.circle(safeX, 40, radius, { restitution: 0.4, friction: 0.1, density: 0.003, label: ‘rainbow’ });
orb.createdAt = Date.now();
} else {
orb = createOrb(safeX, 40, nextDropLevel);
nextDropLevel = Math.floor(Math.random() * 3);
}
Composite.add(world, orb);
canDrop = false;
document.getElementById(‘next-emoji’).innerText = orbTypes[nextDropLevel].emoji;
setTimeout(() => { canDrop = true; }, DROP_COOLDOWN);
}
function updateScore(points) {
score += points;
document.getElementById(‘score’).innerText = ‘Score: ‘ + score;
if (score > bestScore) { bestScore = score; document.getElementById(‘high-score’).innerText = ‘Best: ‘ + bestScore; }
}
function triggerGameOver() {
if (isGameOver) return;
isGameOver = true; isAiming = false; isSliceActive = false; isHoldingRainbow = false; lineBreachStartTime = null;
if (isAutoPlay) toggleAutoPlay();
saveBestScore(bestScore);
document.getElementById(‘final-score’).innerText = ‘Score: ‘ + score;
document.getElementById(‘game-over’).style.display = ‘block’;
}
window.resetGame = function() {
Composite.remove(world, Composite.allBodies(world).filter(b => b.label === ‘orb’ || b.label === ‘rainbow’));
score = 0; document.getElementById(‘score’).innerText = ‘Score: 0’;
isGameOver = false; canDrop = true; isAiming = false; lineBreachStartTime = null;
// Reset Powerups
sliceUses = 2; isSliceActive = false;
const btnSlice = document.getElementById(‘btn-slice’);
btnSlice.innerText = `πͺ Slice (${sliceUses})`; btnSlice.classList.remove(‘active’); btnSlice.disabled = false;
quakeUses = 3;
const btnQuake = document.getElementById(‘btn-quake’);
btnQuake.innerText = `π Quake (${quakeUses})`; btnQuake.disabled = false;
rainbowUses = 2; isHoldingRainbow = false;
const btnRainbow = document.getElementById(‘btn-rainbow’);
btnRainbow.innerText = `π Rainbow (${rainbowUses})`; btnRainbow.classList.remove(‘active’); btnRainbow.disabled = false;
nextDropLevel = Math.floor(Math.random() * 3);
document.getElementById(‘next-emoji’).innerText = orbTypes[nextDropLevel].emoji;
document.getElementById(‘game-over’).style.display = ‘none’;
}
// ==========================================
// INPUT HANDLING
// ==========================================
const getEventX = (e) => { const rect = canvas.getBoundingClientRect(); return (e.touches ? e.touches[0].clientX : e.clientX) – rect.left; };
const getEventY = (e) => { const rect = canvas.getBoundingClientRect(); return (e.touches ? e.touches[0].clientY : e.clientY) – rect.top; };
const handleStart = (e) => {
if (isGameOver || !canDrop || isAutoPlay) return;
if (e.target.tagName.toLowerCase() === ‘button’) return;
e.preventDefault();
if (isSliceActive) {
const touchX = getEventX(e), touchY = getEventY(e);
const clickedBodies = Query.point(Composite.allBodies(world), { x: touchX, y: touchY });
const orbToDestroy = clickedBodies.find(b => b.label === ‘orb’ || b.label === ‘rainbow’);
if (orbToDestroy) {
const color = orbToDestroy.label === ‘rainbow’ ? ‘#ffd700’ : orbTypes[orbToDestroy.orbLevel].color;
spawnParticles(orbToDestroy.position.x, orbToDestroy.position.y, color, 30);
Composite.remove(world, orbToDestroy);
sliceUses–;
}
isSliceActive = false;
const btn = document.getElementById(‘btn-slice’);
btn.classList.remove(‘active’); btn.innerText = `πͺ Slice (${sliceUses})`;
if (sliceUses <= 0) btn.disabled = true;
return;
}
isAiming = true; aimX = getEventX(e);
};
const handleMove = (e) => { if (!isAiming || isGameOver || isSliceActive || isAutoPlay) return; e.preventDefault(); aimX = getEventX(e); };
const handleEnd = (e) => { if (!isAiming || isGameOver || isSliceActive || isAutoPlay) return; e.preventDefault(); isAiming = false; dropOrb(aimX); };
canvas.addEventListener(‘touchstart’, handleStart, { passive: false }); canvas.addEventListener(‘touchmove’, handleMove, { passive: false });
canvas.addEventListener(‘touchend’, handleEnd, { passive: false }); canvas.addEventListener(‘mousedown’, handleStart);
canvas.addEventListener(‘mousemove’, handleMove); canvas.addEventListener(‘mouseup’, handleEnd);
// ==========================================
// COLLISION & LOOP LOGIC
// ==========================================
Events.on(engine, ‘collisionStart’, (event) => {
const pairs = event.pairs;
for (let i = 0; i < pairs.length; i++) {
const bodyA = pairs[i].bodyA;
const bodyB = pairs[i].bodyB;
if (bodyA.label === ‘orb’ && bodyB.label === ‘orb’) {
if (bodyA.orbLevel === bodyB.orbLevel && !bodyA.isMerging && !bodyB.isMerging) {
bodyA.isMerging = true; bodyB.isMerging = true;
executeMerge(bodyA, bodyB, bodyA.orbLevel);
}
}
else {
const isRainbowA = bodyA.label === ‘rainbow’ && bodyB.label === ‘orb’;
const isRainbowB = bodyB.label === ‘rainbow’ && bodyA.label === ‘orb’;
if ((isRainbowA || isRainbowB) && !bodyA.isMerging && !bodyB.isMerging) {
bodyA.isMerging = true; bodyB.isMerging = true;
const normalBody = isRainbowA ? bodyB : bodyA;
executeMerge(bodyA, bodyB, normalBody.orbLevel, true);
}
else if (bodyA.label === ‘rainbow’ && bodyB.label === ‘rainbow’ && !bodyA.isMerging && !bodyB.isMerging) {
bodyA.isMerging = true; bodyB.isMerging = true;
setTimeout(() => {
Composite.remove(world, [bodyA, bodyB]);
spawnFloatingText(bodyA.position.x, bodyA.position.y, “POOF!”, “#ffd700”, 30);
spawnParticles(bodyA.position.x, bodyA.position.y, “#ffd700”, 20);
}, 0);
}
}
}
});
function executeMerge(bodyA, bodyB, level, isRainbowMerge = false) {
const newLevel = level + 1;
const midX = (bodyA.position.x + bodyB.position.x) / 2;
const midY = (bodyA.position.y + bodyB.position.y) / 2;
setTimeout(() => {
Composite.remove(world, [bodyA, bodyB]);
const points = orbTypes[level].score;
updateScore(points);
triggerShake(level);
const textOutput = isRainbowMerge ? `π +${points}` : `+${points}`;
spawnFloatingText(midX, midY, textOutput, orbTypes[level].color, Math.min(20 + level * 2, 50));
if (newLevel < orbTypes.length) {
spawnParticles(midX, midY, orbTypes[level].color, 10 + (level * 3));
if(isRainbowMerge) spawnParticles(midX, midY, “#ffd700”, 15);
const newOrb = createOrb(midX, midY, newLevel);
Composite.add(world, newOrb);
} else {
spawnFloatingText(midX, midY – 40, “MAX MERGE!”, “#ffd700”, 40);
spawnParticles(midX, midY, “#ffd700”, 60);
triggerShake(15);
}
}, 0);
}
Events.on(engine, ‘beforeUpdate’, () => {
if (isGameOver) return;
const orbs = Composite.allBodies(world).filter(b => b.label === ‘orb’ || b.label === ‘rainbow’);
const now = Date.now();
let isBreachingLine = false;
for (let orb of orbs) {
if (now – orb.createdAt > 2000) {
const radius = orb.label === ‘rainbow’ ? getRadius(2) : getRadius(orb.orbLevel);
if (orb.position.y – radius < DEATH_LINE_Y && Math.abs(orb.velocity.y) < 1.0 && Math.abs(orb.velocity.x) < 1.0) {
isBreachingLine = true; break;
}
}
}
if (isBreachingLine) {
if (lineBreachStartTime === null) lineBreachStartTime = now;
else if (now – lineBreachStartTime > GRACE_PERIOD_MS) triggerGameOver();
} else {
lineBreachStartTime = null;
}
});
function resizeCanvas() {
width = container.clientWidth; height = container.clientHeight; baseR = width * 0.05;
canvas.width = width * dpr; canvas.height = height * dpr; ctx.scale(dpr, dpr);
if (!ground) {
ground = Bodies.rectangle(width / 2, height + 25, 2000, 50, { isStatic: true });
leftWall = Bodies.rectangle(-25, height / 2, 50, 4000, { isStatic: true });
rightWall = Bodies.rectangle(width + 25, height / 2, 50, 4000, { isStatic: true });
Composite.add(world, [ground, leftWall, rightWall]);
} else {
Matter.Body.setPosition(ground, { x: width / 2, y: height + 25 });
Matter.Body.setPosition(leftWall, { x: -25, y: height / 2 });
Matter.Body.setPosition(rightWall, { x: width + 25, y: height / 2 });
}
}
window.addEventListener(‘resize’, resizeCanvas); resizeCanvas();
function renderLoop() {
ctx.clearRect(0, 0, width, height);
ctx.save();
if (shakeFrames > 0 && !isGameOver) {
const sx = (Math.random() – 0.5) * shakeIntensity;
const sy = (Math.random() – 0.5) * shakeIntensity;
ctx.translate(sx, sy);
shakeFrames–;
}
if (lineBreachStartTime && !isGameOver) {
const timeLeft = Math.max(0, GRACE_PERIOD_MS – (Date.now() – lineBreachStartTime));
const secondsLeft = (timeLeft / 1000).toFixed(1);
const pulse = Math.abs(Math.sin(Date.now() / 150));
ctx.fillStyle = `rgba(255, 0, 0, ${0.1 + (pulse * 0.15)})`; ctx.fillRect(0, 0, width, height);
ctx.fillStyle = “rgba(211, 47, 47, 0.9)”; ctx.font = “bold 28px Arial”; ctx.textAlign = “center”;
ctx.fillText(`β οΈ ${secondsLeft}s`, width / 2, DEATH_LINE_Y – 20);
ctx.beginPath(); ctx.setLineDash([10, 10]); ctx.moveTo(0, DEATH_LINE_Y); ctx.lineTo(width, DEATH_LINE_Y);
ctx.strokeStyle = `rgba(255, 0, 0, ${0.5 + pulse})`; ctx.lineWidth = 6; ctx.stroke(); ctx.setLineDash([]);
} else {
ctx.beginPath(); ctx.setLineDash([10, 10]); ctx.moveTo(0, DEATH_LINE_Y); ctx.lineTo(width, DEATH_LINE_Y);
ctx.strokeStyle = ‘rgba(244, 67, 54, 0.6)’; ctx.lineWidth = 4; ctx.stroke(); ctx.setLineDash([]);
}
if (isSliceActive) {
ctx.fillStyle = “rgba(233, 30, 99, 0.1)”; ctx.fillRect(0, 0, width, height);
ctx.fillStyle = “rgba(194, 24, 91, 0.8)”; ctx.font = “bold 24px Arial”; ctx.textAlign = “center”;
ctx.fillText(“πͺ TAP A FRUIT TO SLICE IT”, width / 2, height / 2);
}
if (isAiming && canDrop && !isGameOver && !isSliceActive) {
const isR = isHoldingRainbow;
const radius = isR ? getRadius(2) : getRadius(nextDropLevel);
const safeX = getSafeX(aimX, radius);
const emojiToDraw = isR ? ‘π’ : orbTypes[nextDropLevel].emoji;
ctx.beginPath(); ctx.setLineDash([8, 8]); ctx.moveTo(safeX, 40); ctx.lineTo(safeX, height);
ctx.strokeStyle = isAutoPlay ? ‘rgba(33, 150, 243, 0.6)’ : ‘rgba(255, 179, 0, 0.5)’;
ctx.lineWidth = 2; ctx.stroke(); ctx.setLineDash([]);
ctx.save(); ctx.globalAlpha = 0.5; const fontSize = radius * 1.8; ctx.font = `${fontSize}px Arial`;
ctx.textAlign = ‘center’; ctx.textBaseline = ‘middle’; ctx.fillText(emojiToDraw, safeX, 40 + (radius * 0.1)); ctx.restore();
}
const bodies = Composite.allBodies(world).filter(b => b.label === ‘orb’ || b.label === ‘rainbow’);
for (let body of bodies) {
const isR = body.label === ‘rainbow’;
const type = isR ? { emoji: ‘π’ } : orbTypes[body.orbLevel];
const radius = isR ? getRadius(2) : getRadius(body.orbLevel);
ctx.save(); ctx.translate(body.position.x, body.position.y); ctx.rotate(body.angle);
if (isR) {
ctx.beginPath(); ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fillStyle = “rgba(255, 255, 255, 0.5)”; ctx.fill();
}
const fontSize = radius * 1.8; ctx.font = `${fontSize}px Arial`; ctx.textAlign = ‘center’;
ctx.textBaseline = ‘middle’; ctx.fillText(type.emoji, 0, radius * 0.1); ctx.restore();
}
for(let i = particles.length – 1; i >= 0; i–) {
let p = particles[i];
p.x += p.vx; p.y += p.vy; p.vy += 0.3;
p.life -= 0.02;
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
if(p.life <= 0) particles.splice(i, 1);
}
ctx.globalAlpha = 1.0;
for(let i = floatingTexts.length – 1; i >= 0; i–) {
let t = floatingTexts[i];
t.y += t.vy; t.life -= 0.015;
ctx.globalAlpha = Math.max(0, t.life);
ctx.fillStyle = t.color;
ctx.font = `900 ${t.size}px Arial`;
ctx.textAlign = “center”;
ctx.lineWidth = 4;
ctx.strokeStyle = “white”;
ctx.strokeText(t.text, t.x, t.y);
ctx.fillText(t.text, t.x, t.y);
if(t.life <= 0) floatingTexts.splice(i, 1);
}
ctx.globalAlpha = 1.0;
ctx.restore();
requestAnimationFrame(renderLoop);
}
Runner.run(Runner.create(), engine);
renderLoop();
</script>
</body>
</html>
