Kitty merge test 1


<!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>

Leave a Reply