<!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>Cat 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: #e0f7fa; /* Soft cyan background */
font-family: ‘Segoe UI’, Tahoma, Geneva, Verdana, sans-serif;
touch-action: none;
display: flex;
justify-content: center;
align-items: flex-end;
height: 100dvh;
user-select: none;
-webkit-user-select: none;
}
/* The Cardboard Box that holds the cats */
#game-container {
position: relative;
width: 100vw;
max-width: 450px;
height: 95dvh;
max-height: 900px;
background-color: #d7ccc8; /* Light cardboard color */
border-left: 12px solid #8d6e63; /* Darker cardboard edges */
border-right: 12px solid #8d6e63;
border-bottom: 12px solid #8d6e63;
border-radius: 0 0 10px 10px;
box-sizing: border-box;
box-shadow: 0 15px 35px rgba(0,0,0,0.3);
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
#ui {
position: absolute;
top: 20px;
left: 20px;
color: #333;
pointer-events: none;
z-index: 10;
}
#score {
font-size: 24px;
font-weight: 800;
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 #bcaaa4;
}
#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 #bcaaa4;
}
#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 #8d6e63;
}
#game-over h1 { margin-top: 0; color: #d84315; font-size: 32px; }
#final-score { font-size: 24px; font-weight: bold; }
button {
padding: 12px 24px;
font-size: 20px;
background: #ff7043;
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
margin-top: 15px;
font-weight: bold;
box-shadow: 0 4px 6px rgba(255, 112, 67, 0.4);
transition: transform 0.1s;
width: 100%;
}
button:active {
transform: scale(0.95);
}
</style>
</head>
<body>
<div id=”game-container”>
<div id=”ui”>
<div id=”score”>Score: 0</div>
</div>
<div id=”next-piece”>
Next
<div id=”next-emoji”>πΎ</div>
</div>
<div id=”game-over”>
<h1>Box Full! π</h1>
<p id=”final-score”>Score: 0</p>
<button onclick=”resetGame()”>Play Again</button>
</div>
<canvas id=”gameCanvas”></canvas>
</div>
<script>
// — Setup Matter.js & Canvas —
const Engine = Matter.Engine,
Runner = Matter.Runner,
Bodies = Matter.Bodies,
Composite = Matter.Composite,
Events = Matter.Events;
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;
let ground, leftWall, rightWall;
// — Game Configuration —
// Cat progression: Paws -> Kittens -> Cats -> Big Cats
const orbTypes = [
{ level: 0, scale: 1.0, emoji: ‘πΎ’, score: 2 },
{ level: 1, scale: 1.3, emoji: ‘π±’, score: 4 },
{ level: 2, scale: 1.7, emoji: ‘πΈ’, score: 8 },
{ level: 3, scale: 2.2, emoji: ‘π»’, score: 16 },
{ level: 4, scale: 2.8, emoji: ‘πΌ’, score: 32 },
{ level: 5, scale: 3.5, emoji: ‘π½’, score: 64 },
{ level: 6, scale: 4.3, emoji: ‘π’, score: 128 },
{ level: 7, scale: 5.2, emoji: ‘πΎ’, score: 256 },
{ level: 8, scale: 6.2, emoji: ‘π
’, score: 512 },
{ level: 9, scale: 7.3, emoji: ‘π’, score: 1024 },
{ level: 10, scale: 8.5, emoji: ‘π¦’, score: 2048 }
];
let score = 0;
let isGameOver = false;
let canDrop = true;
let nextDropLevel = Math.floor(Math.random() * 3);
const DROP_COOLDOWN = 600;
const DEATH_LINE_Y = 120;
let baseR = 20;
document.getElementById(‘next-emoji’).innerText = orbTypes[nextDropLevel].emoji;
// — Core Functions —
function getRadius(levelId) {
return baseR * orbTypes[levelId].scale;
}
function createOrb(x, y, levelId) {
const radius = getRadius(levelId);
const orb = Bodies.circle(x, y, radius, {
restitution: 0.2, // Bounciness
friction: 0.1, // Rolling friction
density: 0.001 * (levelId + 1), // Heavier objects as they get bigger
label: ‘orb’
});
orb.orbLevel = levelId;
orb.createdAt = Date.now();
return orb;
}
function dropOrb(x) {
if (!canDrop || isGameOver) return;
const radius = getRadius(nextDropLevel);
// Keep orb inside the walls when spawned
const safeX = Math.max(radius, Math.min(width – radius, x));
const orb = createOrb(safeX, 40, nextDropLevel);
Composite.add(world, orb);
canDrop = false;
nextDropLevel = Math.floor(Math.random() * 3);
document.getElementById(‘next-emoji’).innerText = orbTypes[nextDropLevel].emoji;
setTimeout(() => { canDrop = true; }, DROP_COOLDOWN);
}
function updateScore(points) {
score += points;
document.getElementById(‘score’).innerText = ‘Score: ‘ + score;
}
function triggerGameOver() {
if (isGameOver) return;
isGameOver = true;
document.getElementById(‘final-score’).innerText = ‘Score: ‘ + score;
document.getElementById(‘game-over’).style.display = ‘block’;
}
function resetGame() {
const orbs = Composite.allBodies(world).filter(b => b.label === ‘orb’);
Composite.remove(world, orbs);
score = 0;
document.getElementById(‘score’).innerText = ‘Score: 0’;
isGameOver = false;
canDrop = true;
nextDropLevel = Math.floor(Math.random() * 3);
document.getElementById(‘next-emoji’).innerText = orbTypes[nextDropLevel].emoji;
document.getElementById(‘game-over’).style.display = ‘none’;
}
// — Interaction —
const handleInput = (e) => {
if (isGameOver) return;
e.preventDefault();
// Get X coordinate relative to the game container
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const rect = canvas.getBoundingClientRect();
const x = clientX – rect.left;
dropOrb(x);
};
window.addEventListener(‘touchstart’, handleInput, { passive: false });
window.addEventListener(‘mousedown’, handleInput);
// — Collision Logic (Merging) —
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) {
if (bodyA.isMerging || bodyB.isMerging) continue;
bodyA.isMerging = true;
bodyB.isMerging = true;
const level = bodyA.orbLevel;
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]);
updateScore(orbTypes[level].score);
if (newLevel < orbTypes.length) {
const newOrb = createOrb(midX, midY, newLevel);
Composite.add(world, newOrb);
}
}, 0);
}
}
}
});
// — Game Over Check —
Events.on(engine, ‘beforeUpdate’, () => {
if (isGameOver) return;
const orbs = Composite.allBodies(world).filter(b => b.label === ‘orb’);
const now = Date.now();
for (let orb of orbs) {
if (now – orb.createdAt > 2000) {
const radius = getRadius(orb.orbLevel);
if (orb.position.y – radius < DEATH_LINE_Y &&
Math.abs(orb.velocity.y) < 0.5 &&
Math.abs(orb.velocity.x) < 0.5) {
triggerGameOver();
break;
}
}
}
});
// — Sizing and Resizing Logic —
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();
// — Custom Render Loop —
function renderLoop() {
ctx.clearRect(0, 0, width, height);
// Draw Death Line (red yarn color)
ctx.beginPath();
ctx.setLineDash([10, 10]);
ctx.moveTo(0, DEATH_LINE_Y);
ctx.lineTo(width, DEATH_LINE_Y);
ctx.strokeStyle = ‘rgba(216, 67, 21, 0.6)’; // Deeper red-orange
ctx.lineWidth = 4;
ctx.stroke();
ctx.setLineDash([]);
// Draw Cat Orbs
const bodies = Composite.allBodies(world);
for (let body of bodies) {
if (body.label === ‘orb’) {
const type = orbTypes[body.orbLevel];
const radius = getRadius(body.orbLevel);
ctx.save();
ctx.translate(body.position.x, body.position.y);
ctx.rotate(body.angle);
const fontSize = radius * 1.8;
ctx.font = `${fontSize}px Arial`;
ctx.textAlign = ‘center’;
ctx.textBaseline = ‘middle’;
// Emoji vertical alignment tweaks
ctx.fillText(type.emoji, 0, radius * 0.1);
ctx.restore();
}
}
requestAnimationFrame(renderLoop);
}
Runner.run(Runner.create(), engine);
renderLoop();
</script>
</body>
</html>
