Forum Discussion

ArthaLearning01's avatar
ArthaLearning01
Community Member
2 months ago

Shrimp Racer

For this entry, I built a small racing game where learners control a shrimp around a race track with their mouse. They have to get as many laps as they can while dodging plastic waste.

Hitting plastic fills a “plastic meter,” and once it’s full, the shrimp dies. When that happens, the interaction surfaces a quick fact about ocean plastic, reinforcing the theme without interrupting the flow of play too early.

Shrimp Racer - Start

This idea came out of a team moment. I went from solo development to having two team member join and we landed on the team name “The Shrimps.” From there, this became a way to show how fast you can vibe code an idea into something playable. We built it together to have fun, but also to demonstrate the real back-and-forth required to refine and polish an LLM-generated idea. The model can get you moving quickly but human feedback is still what makes it better.

Shrimp Racer - Lap 3

The process started with brainstorming in ChatGPT, followed by a quick build spec. From there, I moved into Antigravity using Gemini to build the interaction with HTML/CSS/JS (using emojis for both the shrimp and the plastic). We used GitHub to track versions, Google Meet for live brainstorming and screen sharing, and FigJam to capture ideas as they came up.

A few things we learned along the way:

  • There are more pieces to a working racetrack game than expected—clear direction, visible borders, and a reliable “reset to track” all matter.
  • Letting the shrimp follow the visible cursor felt off, so hiding the cursor during gameplay made a big difference.
  • Checkpoints were necessary to prevent players from “cheating” the track.
  • As a team, we saw just how quickly a loose idea can turn into a working demo—often faster than traditional whiteboarding or planning.

Huge thanks to Ann Sze and Carlie V. for jumping in, brainstorming, and helping shape this one.

Shrimp Racer - Game Over

Course Link: 

https://360.articulate.com/review/content/38033791-2f52-4228-9f4c-537dcae11899/review 

I've included the code so everyone can poke around! 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Shrimp Survival — Plastic Lap Challenge</title>
    <style>
        :root {
            --bg-color: #0c1a2b;
            --accent-color: #ff6b6b;
            --track-color: rgba(236, 224, 191, 0.4);
            --water-gradient: linear-gradient(180deg, #1a365d 0%, #0c1a2b 100%);
            --hud-bg: rgba(0, 0, 0, 0.5);
            --text-color: #ffffff;
            --bar-bg: #333;
        }

        * {
            box-sizing: border-box;
            user-select: none;
        }

        body {
            margin: 0;
            padding: 0;
            background: #000;
            font-family: 'Inter', system-ui, -apple-system, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 200px;
            overflow: hidden;
        }

        /* Rise Container Constraints */
        #game-container {
            position: relative;
            width: 100%;
            max-width: 860px;
            height: 460px;
            background: var(--bg-color);
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        /* Logical Scaling Arena */
        #arena {
            position: relative;
            width: 800px;
            height: 450px;
            background: var(--water-gradient);
            box-shadow: 0 0 40px rgba(0,0,0,0.5);
            image-rendering: pixelated;
            cursor: none;
        }

        /* Caustic Overlay */
        .caustics {
            position: absolute;
            top: 0; left: 0; right: 0; bottom: 0;
            background-image: 
                radial-gradient(circle at 50% 50%, rgba(255,255,255,0.05) 0%, transparent 60%);
            pointer-events: none;
            z-index: 1;
        }

        /* HUD */
        #hud {
            position: absolute;
            top: 10px;
            left: 10px;
            right: 10px;
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            z-index: 10;
            pointer-events: none;
        }

        .hud-group {
            background: var(--hud-bg);
            padding: 8px 12px;
            border-radius: 8px;
            backdrop-filter: blur(4px);
            color: var(--text-color);
            font-size: 14px;
            font-weight: bold;
            display: flex;
            flex-direction: column;
            gap: 4px;
            pointer-events: auto;
        }

        #plastic-bar-container {
            width: 200px;
            height: 12px;
            background: var(--bar-bg);
            border-radius: 6px;
            overflow: hidden;
            margin-top: 4px;
        }

        #plastic-bar {
            width: 0%;
            height: 100%;
            background: linear-gradient(90deg, #ff6b6b, #ff4757);
            transition: width 0.3s ease-out;
        }

        /* Track SVG */
        #track-svg {
            position: absolute;
            top: 0; left: 0;
            width: 100%; height: 100%;
            z-index: 2;
        }

        #track-path {
            fill: none;
            stroke: var(--track-color);
            stroke-width: 70;
            stroke-linecap: round;
            stroke-linejoin: round;
        }

        .finish-line {
            stroke: #fff;
            stroke-width: 4;
            stroke-dasharray: 8 4;
        }

        .cp-label {
            fill: white;
            font-size: 16px;
            font-weight: bold;
            text-anchor: middle;
            dominant-baseline: central;
            pointer-events: none;
            text-shadow: 0 0 4px rgba(0,0,0,0.8);
        }

        #off-track-warning {
            position: absolute;
            bottom: 40px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(255, 107, 107, 0.9);
            color: white;
            padding: 8px 16px;
            border-radius: 20px;
            font-weight: bold;
            font-size: 14px;
            z-index: 20;
            display: none;
            animation: pulse-red 1s infinite;
        }

        @keyframes pulse-red {
            0% { opacity: 0.8; }
            50% { opacity: 1; }
            100% { opacity: 0.8; }
        }

        /* Entities */
        .entity {
            position: absolute;
            transform-origin: center;
            z-index: 10;
        }

        #plastic-layer {
            position: absolute;
            top: 0; left: 0;
            width: 100%; height: 100%;
            z-index: 8;
            pointer-events: none;
        }

        #shrimp {
            width: 32px;
            height: 32px;
            font-size: 28px;
            display: flex;
            justify-content: center;
            align-items: center;
            transition: filter 0.2s;
            z-index: 15;
        }

        .plastic-item {
            font-size: 20px;
        }

        /* Overlays */
        #overlay {
            position: absolute;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.8);
            z-index: 100;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: white;
            padding: 40px;
            text-align: center;
        }

        .btn {
            background: var(--accent-color);
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 24px;
            font-size: 18px;
            font-weight: bold;
            cursor: pointer;
            margin-top: 20px;
            transition: transform 0.2s;
        }

        .btn:hover {
            transform: scale(1.05);
        }

        /* Bubbles */
        .bubble {
            position: absolute;
            background: rgba(255,255,255,0.1);
            border: 1px solid rgba(255,255,255,0.2);
            border-radius: 50%;
            pointer-events: none;
            animation: rise 10s linear infinite;
        }

        @keyframes rise {
            from { transform: translateY(500px) translateX(0); }
            to { transform: translateY(-100px) translateX(20px); }
        }

        .hidden { display: none !important; }

        /* Start Prompt */
        #start-prompt {
            position: absolute;
            background: rgba(255, 255, 255, 0.9);
            color: #333;
            padding: 8px 12px;
            border-radius: 8px;
            font-size: 14px;
            font-weight: bold;
            pointer-events: none;
            white-space: nowrap;
            z-index: 20;
            transform: translate(20px, -50%); /* Start to the right of the shrimp */
            box-shadow: 0 4px 6px rgba(0,0,0,0.3);
            display: none;
        }

        #start-prompt::after {
            content: '';
            position: absolute;
            top: 50%;
            left: -6px;
            transform: translateY(-50%);
            border-width: 6px 6px 6px 0;
            border-style: solid;
            border-color: transparent rgba(255, 255, 255, 0.9) transparent transparent;
        }

        /* Pulse Animation */
        @keyframes pulse-scale {
            0% { transform: translate(-50%, -50%) scale(1); }
            50% { transform: translate(-50%, -50%) scale(1.3); }
            100% { transform: translate(-50%, -50%) scale(1); }
        }

        .shrimp-pulse {
            animation: pulse-scale 1.5s infinite ease-in-out;
            cursor: pointer;
            filter: drop-shadow(0 0 10px var(--accent-color));
        }
    </style>
</head>
<body>

    <div id="game-container">
        <div id="arena">
            <div class="caustics"></div>
            
            <!-- HUD -->
            <div id="hud">
                <div class="hud-group">
                    <div>LAPS: <span id="lap-count">0</span></div>
                    <div>PIECES: <span id="pieces-count">0</span></div>
                    <div>TIME: <span id="time-count">00:00</span></div>
                </div>
                <div class="hud-group" style="align-items: flex-end;">
                    <div>PLASTIC LOAD: <span id="plastic-percent">0%</span></div>
                    <div id="plastic-bar-container">
                        <div id="plastic-bar"></div>
                    </div>
                </div>
            </div>

            <!-- Overlays -->
            <div id="overlay">
                <h1 id="overlay-title">Shrimp Survival</h1>
                <p id="overlay-desc">Guide the shrimp with your mouse. Avoid plastic "food" to survive.</p>
                <div id="stats-display" class="hidden">
                    <p style="font-size: 1.2em; color: var(--accent-color); font-weight: bold;">TOTAL PLASTIC EATEN: <span id="final-total">0</span> pieces</p>
                    <div id="plastic-breakdown" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 15px 0; text-align: left; background: rgba(255,255,255,0.1); padding: 10px; border-radius: 8px;">
                        <!-- Breakdown will be injected here -->
                    </div>
                    <p>Laps: <span id="final-laps">0</span> | Time: <span id="final-time">0:00</span></p>
                    <p id="fact-line" style="font-style: italic; margin-top: 20px; font-size: 0.9em;"></p>
                </div>
                <button class="btn" id="start-btn">START SURVIVAL</button>
            </div>

            <!-- SVG Track -->
            <svg id="track-svg" viewBox="0 0 800 450">
                <defs>
                    <linearGradient id="track-grad" x1="0%" y1="0%" x2="100%" y2="0%">
                        <stop offset="0%" style="stop-color:#f8c291;stop-opacity:1.0" />
                        <stop offset="50%" style="stop-color:#e77f67;stop-opacity:1.0" />
                        <stop offset="100%" style="stop-color:#f8c291;stop-opacity:1.0" />
                    </linearGradient>
                </defs>
                <!-- More Characteristic Shrimp Silhouette -->
                <path id="track-border" d="M 200 380 C 50 380, 20 200, 150 100 C 250 20, 500 20, 650 100 C 800 180, 750 350, 600 380 C 500 400, 350 300, 350 200 C 350 100, 500 50, 600 150 C 650 200, 600 280, 450 250 C 350 230, 300 350, 200 380 Z" fill="none" stroke="#000" stroke-width="96" stroke-linecap="round" stroke-linejoin="round" />
                <path id="track-path" d="M 200 380 C 50 380, 20 200, 150 100 C 250 20, 500 20, 650 100 C 800 180, 750 350, 600 380 C 500 400, 350 300, 350 200 C 350 100, 500 50, 600 150 C 650 200, 600 280, 450 250 C 350 230, 300 350, 200 380 Z" fill="none" stroke="url(#track-grad)" stroke-width="90" stroke-linecap="round" stroke-linejoin="round" />
                <!-- Start/Finish Line -->
                <line id="finish-line-ui" x1="170" y1="380" x2="230" y2="380" class="finish-line" />
                
                <!-- Checkpoint Indicators (Generated by JS now for flexibility) -->
                <g id="checkpoint-layer"></g>
                
                <text id="start-label" class="cp-label" style="font-size: 12px;">START</text>
            </svg>

            <div id="off-track-warning">⚠️ OFF TRACK - RESET IN <span id="reset-timer">2.0</span>s</div>

            <!-- Game Entities -->
            <div id="shrimp" class="entity">🦐</div>
            <div id="start-prompt">Click to Start Racing!</div>
            <div id="plastic-layer"></div>
        </div>
    </div>

    <script>
        // CONFIG
        const WORLD_W = 800;
        const WORLD_H = 450;
        const TRACK_WIDTH = 90;
        const PLASTIC_MAX = 100;
        const TARGET_LAPS = 8;
        
        const ATTRACT_FORCE = 6000;
        const MAX_SPEED_ON = 500;
        const MAX_SPEED_OFF = 250;
        const DRAG_ON = 0.80; 
        const DRAG_OFF = 0.70; 
        
        const SPAWN_BASE = 1.8;
        const SPAWN_DECAY = 0.12;
        const SPAWN_MIN = 0.6;
        
        const DECAY_ENABLED = false;
        const DECAY_DELAY = 4.0;
        const DECAY_RATE = 0.8;
        const MIN_LOAD = 10;

        const PLASTIC_TYPES = [
            { type: 'microbead', emoji: '⚪', value: 2, radius: 6 },
            { type: 'fragment', emoji: '🔹', value: 6, radius: 8 },
            { type: 'cap', emoji: '🔴', value: 10, radius: 10 },
            { type: 'bag', emoji: '🗒️', value: 14, radius: 12 }
        ];

        const FACTS = [
            "Over 14 million tons of plastic end up in the ocean every year.",
            "Microplastics have been found in every part of the food chain.",
            "By 2050, there could be more plastic than fish in the sea.",
            "Plastic waste kills more than 100,000 marine mammals annually."
        ];

        // STATE
        let state = 'READY'; // READY, PLAYING, DEAD
        let shrimp = {
            pos: { x: 100, y: 225 },
            vel: { x: 0, y: 0 },
            radius: 12,
            plasticLoad: 0,
            lastEatTime: 0,
            offTrackTime: 0
        };
        let plasticItems = [];
        let plasticStats = { microbead: 0, fragment: 0, cap: 0, bag: 0 };
        let laps = 0;
        let startTime = 0;
        let gameTime = 0;
        let lastSpawn = 0;
        let cpIndex = 0;
        let pointerPos = { x: 100, y: 225 };
        let lastTime = 0;

        // ELEMENTS
        const arena = document.getElementById('arena');
        const shrimpEl = document.getElementById('shrimp');
        const trackPath = document.getElementById('track-path');
        const plasticLayer = document.getElementById('plastic-layer');
        const lapCountEl = document.getElementById('lap-count');
        const timeCountEl = document.getElementById('time-count');
        const plasticBar = document.getElementById('plastic-bar');
        const plasticPercentEl = document.getElementById('plastic-percent');
        const overlay = document.getElementById('overlay');
        const startBtn = document.getElementById('start-btn');
        const startPrompt = document.getElementById('start-prompt');

        // HELPERS
        function normalizeScale() {
            const container = document.getElementById('game-container');
            const scale = Math.min(container.clientWidth / WORLD_W, container.clientHeight / WORLD_H);
            arena.style.transform = `scale(${scale})`;
        }

        window.addEventListener('resize', normalizeScale);
        normalizeScale();

        // TRACK DETECTION
        const svg = document.getElementById('track-svg');
        const pt = svg.createSVGPoint();

        function isOnTrack(x, y) {
            pt.x = x;
            pt.y = y;
            return trackPath.isPointInStroke(pt);
        }

        // INPUT
        arena.addEventListener('mousemove', (e) => {
            const rect = arena.getBoundingClientRect();
            const scale = WORLD_W / rect.width;
            pointerPos.x = (e.clientX - rect.left) * scale;
            pointerPos.y = (e.clientY - rect.top) * scale;
        });

        arena.addEventListener('touchmove', (e) => {
            e.preventDefault();
            const rect = arena.getBoundingClientRect();
            const scale = WORLD_W / rect.width;
            pointerPos.x = (e.touches[0].clientX - rect.left) * scale;
            pointerPos.y = (e.touches[0].clientY - rect.top) * scale;
        }, { passive: false });

        // Click to Start on Shrimp
        shrimpEl.addEventListener('click', () => {
             if (state === 'READY') {
                 startGame();
             }
        });
        
        // Also allow spacebar to start if in READY state
        window.addEventListener('keydown', (e) => {
            if (e.code === 'Space' && state === 'READY') {
                startGame();
            }
        });

        startBtn.addEventListener('click', fullReset);

        // Initial Setup
        fullReset();

        function fullReset() {
            shrimp.plasticLoad = 0;
            laps = 0;
            gameTime = 0;
            cpIndex = 0;
            lastSpawn = 0;
            plasticItems = [];
            plasticStats = { microbead: 0, fragment: 0, cap: 0, bag: 0 };
            plasticLayer.innerHTML = '';
            
            enterReadyState();
        }

        function enterReadyState() {
            state = 'READY';
            shrimp.pos = { x: 200, y: 380 };
            pointerPos = { x: 200, y: 380 }; // Sync pointer
            shrimp.vel = { x: 0, y: 0 };
            shrimp.offTrackTime = 0;
            cpIndex = 0; // Reset checkpoint progress for the lap
            
            // UI Update
            overlay.classList.add('hidden');
            startPrompt.style.display = 'block';
            startPrompt.style.top = `${shrimp.pos.y}px`;
            startPrompt.style.left = `${shrimp.pos.x + 20}px`;
            arena.style.cursor = 'default'; 
            
            shrimpEl.classList.add('shrimp-pulse');
            shrimpEl.style.transform = `translate(${shrimp.pos.x}px, ${shrimp.pos.y}px) translate(-50%, -50%)`;
            shrimpEl.style.left = `${shrimp.pos.x}px`;
            shrimpEl.style.top = `${shrimp.pos.y}px`;
            
            updateCPIndicators();
            drawTrackArrows(); 
            
            // Render once
            render(); 
        }

        function drawTrackArrows() {
            // Remove existing arrows if any
            document.querySelectorAll('.track-arrow').forEach(el => el.remove());

            const length = trackPath.getTotalLength();
            // Place 4 arrows
            const positions = [0.125, 0.375, 0.625, 0.875];
            
            positions.forEach(p => {
                const pt = trackPath.getPointAtLength(length * p);
                // Calculate angle using a tiny delta
                const pt2 = trackPath.getPointAtLength(length * p + 1);
                const angle = Math.atan2(pt2.y - pt.y, pt2.x - pt.x) * 180 / Math.PI;

                const arrowText = document.createElementNS("http://www.w3.org/2000/svg", "text");
                arrowText.textContent = "➤";
                arrowText.setAttribute("x", pt.x);
                arrowText.setAttribute("y", pt.y);
                arrowText.setAttribute("class", "track-arrow");
                arrowText.setAttribute("fill", "rgba(0,0,0,0.4)");
                arrowText.setAttribute("font-size", "24px");
                arrowText.setAttribute("font-weight", "bold");
                arrowText.setAttribute("text-anchor", "middle");
                arrowText.setAttribute("dominant-baseline", "central");
                arrowText.setAttribute("transform", `rotate(${angle}, ${pt.x}, ${pt.y})`);
                arrowText.style.pointerEvents = "none";
                
                // Append before the start label so it's z-ordered correctly
                document.getElementById('track-svg').insertBefore(arrowText, document.getElementById('start-label'));
            });
        }

        function startGame() {
            state = 'PLAYING';
            
            // Clear Ready UI
            startPrompt.style.display = 'none';
            shrimpEl.classList.remove('shrimp-pulse');
            arena.style.cursor = 'none'; // Hide cursor for gameplay
            
            // NOTE: Data reset moved to fullReset(). strict resume here.
            
            lastTime = performance.now();
            // Adjust startTime so gameTime continues correctly if this was a pause?
            // Actually gameTime = (time - startTime) / 1000.
            // If we paused in Ready, `time` increased, but we want `gameTime` to NOT have jumped.
            // So we need to shift `startTime` forward by the duration of the pause.
            // Simplified: If it's a new game, fullReset already set gameTime=0.
            // We can just set startTime = performance.now() - (gameTime * 1000).
            startTime = performance.now() - (gameTime * 1000);
            
            updateCPIndicators();
            requestAnimationFrame(update);
        }

        function updateCPIndicators() {
            const layer = document.getElementById('checkpoint-layer');
            layer.innerHTML = '';
            const length = trackPath.getTotalLength();
            const count = 12; // 12 checkpoints for Anti-Cheat
            
            for (let i = 0; i < count; i++) {
                // Don't draw one at 0/1 (Start line)
                const p = (i + 1) / (count + 1); 
                const pt = trackPath.getPointAtLength(length * p);
                
                const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
                circle.setAttribute("cx", pt.x);
                circle.setAttribute("cy", pt.y);
                circle.setAttribute("r", "5"); // Small dot
                circle.setAttribute("fill", "rgba(255,255,255,0.3)");
                circle.setAttribute("id", `cp-${i}`);
                layer.appendChild(circle);
            }
            
            const startText = document.getElementById('start-label');
            startText.setAttribute("x", 200);
            startText.setAttribute("y", 395);
        }

        function gameOver() {
            state = 'DEAD';
            overlay.classList.remove('hidden');
            arena.style.cursor = 'default'; // SHOW CURSOR ON DEATH
            document.getElementById('overlay-title').innerText = "Plastic Overload";
            document.getElementById('stats-display').classList.remove('hidden');
            document.getElementById('final-laps').innerText = laps;
            document.getElementById('final-time').innerText = formatTime(gameTime);
            
            const totalEaten = Object.values(plasticStats).reduce((a, b) => a + b, 0);
            document.getElementById('final-total').innerText = totalEaten;

            const breakdown = document.getElementById('plastic-breakdown');
            breakdown.innerHTML = PLASTIC_TYPES.map(t => `
                <div>${t.emoji} ${t.type.toUpperCase()}: <strong>${plasticStats[t.type]}</strong></div>
            `).join('');

            document.getElementById('fact-line').innerText = FACTS[Math.floor(Math.random() * FACTS.length)];
            startBtn.innerText = "TRY AGAIN";
        }

        function formatTime(s) {
            const mins = Math.floor(s / 60);
            const secs = Math.floor(s % 60);
            return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
        }

        // UPDATE
        function update(time) {
            if (state !== 'PLAYING') return;

            const dt = Math.min((time - lastTime) / 1000, 0.1);
            lastTime = time;
            gameTime = (time - startTime) / 1000;

            // Direct Tracking Movement
            const lastPos = { ...shrimp.pos };
            shrimp.pos.x = pointerPos.x;
            shrimp.pos.y = pointerPos.y;

            const onTrack = isOnTrack(shrimp.pos.x, shrimp.pos.y);
            
            if (!onTrack) {
                shrimp.offTrackTime += dt;
                // User requested 2.0s
                if (shrimp.offTrackTime > 2.0) {
                    enterReadyState(); // Soft reset (keeps laps/score)
                    showToast("Returned to Start!");
                }
            } else {
                shrimp.offTrackTime = 0;
            }

            // Keep boundaries
            shrimp.pos.x = Math.max(15, Math.min(WORLD_W - 15, shrimp.pos.x));
            shrimp.pos.y = Math.max(15, Math.min(WORLD_H - 15, shrimp.pos.y));

            // Calculate "virtual" velocity for rotation only if moved significantly
            const distMoved = Math.sqrt((shrimp.pos.x - lastPos.x)**2 + (shrimp.pos.y - lastPos.y)**2);
            if (distMoved > 0.5) {
                shrimp.vel.x = (shrimp.pos.x - lastPos.x) / dt;
                shrimp.vel.y = (shrimp.pos.y - lastPos.y) / dt;
            } else {
                // If not moving, decay velocity so it stops wiggling but keeps last rotation
                shrimp.vel.x *= 0.9;
                shrimp.vel.y *= 0.9;
            }

            // Plastic Spawn
            const spawnInterval = Math.max(SPAWN_BASE - laps * SPAWN_DECAY, SPAWN_MIN);
            if (gameTime - lastSpawn > spawnInterval) {
                spawnPlastic();
                lastSpawn = gameTime;
            }

            // Plastic Update & Collision
            updatePlastic(dt);

            // Decay
            if (DECAY_ENABLED && gameTime - shrimp.lastEatTime > DECAY_DELAY) {
                shrimp.plasticLoad = Math.max(MIN_LOAD, shrimp.plasticLoad - DECAY_RATE * dt);
            }

            // Checkpoints (Simplistic lap logic based on path progress)
            checkLap();

            // Render
            render();

            if (shrimp.plasticLoad >= PLASTIC_MAX) {
                gameOver();
            } else {
                requestAnimationFrame(update);
            }
        }

        function spawnPlastic() {
            const length = trackPath.getTotalLength();
            const posOnPath = Math.random() * length;
            const pt = trackPath.getPointAtLength(posOnPath);
            
            // Random offset within track width
            const angle = Math.random() * Math.PI * 2;
            const offset = (Math.random() - 0.5) * TRACK_WIDTH;
            
            const x = pt.x + Math.cos(angle) * offset;
            const y = pt.y + Math.sin(angle) * offset;
            
            // Min distance check
            const dist = Math.sqrt((x - shrimp.pos.x)**2 + (y - shrimp.pos.y)**2);
            if (dist < 80) return;

            const type = PLASTIC_TYPES[Math.floor(Math.random() * PLASTIC_TYPES.length)];
            const id = Math.random().toString(36).substr(2, 9);
            
            const el = document.createElement('div');
            el.className = 'entity plastic-item';
            el.id = id;
            el.innerText = type.emoji;
            plasticLayer.appendChild(el);

            plasticItems.push({
                id,
                x, y,
                vel: { x: (Math.random() - 0.5) * 40, y: (Math.random() - 0.5) * 40 },
                ...type
            });
        }

        function updatePlastic(dt) {
            for (let i = plasticItems.length - 1; i >= 0; i--) {
                const item = plasticItems[i];
                item.x += item.vel.x * dt;
                item.y += item.vel.y * dt;

                // Collision
                const dist = Math.sqrt((item.x - shrimp.pos.x)**2 + (item.y - shrimp.pos.y)**2);
                if (dist < shrimp.radius + item.radius) {
                    const difficultyMultiplier = 1 + Math.floor(laps / 5) * 0.1;
                    shrimp.plasticLoad = Math.min(PLASTIC_MAX, shrimp.plasticLoad + item.value * difficultyMultiplier);
                    shrimp.lastEatTime = gameTime;
                    
                    // Stats
                    plasticStats[item.type]++;
                    
                    const el = document.getElementById(item.id);
                    if (el) {
                        // Animate pickup
                        el.style.transition = 'transform 0.2s, opacity 0.2s';
                        el.style.transform += ' scale(2)';
                        el.style.opacity = '0';
                        setTimeout(() => el.remove(), 200);
                    }
                    plasticItems.splice(i, 1);
                    continue;
                }

                // Bounds
                if (item.x < 0 || item.x > WORLD_W || item.y < 0 || item.y > WORLD_H) {
                    const el = document.getElementById(item.id);
                    if (el) el.remove();
                    plasticItems.splice(i, 1);
                }
            }
        }

        // Simplified Checkpoint Logic for this path
        // Checkpoints at 1/(count+1) intervals
        const CP_COUNT = 12; // Must match updateCPIndicators
        function checkLap() {
            const length = trackPath.getTotalLength();
            // We have CP_COUNT intermediate checkpoints + 1 final checkpoint (the finish line)
            // But let's map them simply: Indices 0 to CP_COUNT-1 are the dots. Index CP_COUNT is the finish line.
            
            // Current target index: cpIndex
            // If cpIndex < CP_COUNT, we look for dot.
            // If cpIndex == CP_COUNT, we look for Start/Finish.
            
            let targetP = 0;
            let isFinish = false;
            
            if (cpIndex < CP_COUNT) {
                 targetP = (cpIndex + 1) / (CP_COUNT + 1);
            } else {
                 targetP = 0.0; // Finish line (or 1.0)
                 isFinish = true;
            }
            
            const pt = trackPath.getPointAtLength(length * targetP);
            const dist = Math.sqrt((shrimp.pos.x - pt.x)**2 + (shrimp.pos.y - pt.y)**2);

            // Radius for checkpoint detection 
            const detectRadius = 80;  // Tightened slightly from 120 since there are more

            if (dist < detectRadius) {
                if (isFinish) {
                    if (cpIndex >= CP_COUNT) {
                        laps++;
                        cpIndex = 0;
                        showToast(`Lap ${laps}!`);
                        
                        // Reset all dots visually
                         for (let i = 0; i < CP_COUNT; i++) {
                             const el = document.getElementById(`cp-${i}`);
                             if (el) el.setAttribute('fill', 'rgba(255,255,255,0.3)');
                         }
                    }
                } else {
                    // Hit an intermediate checkpoint
                    // Flash it green or solid white
                    const cpEl = document.getElementById(`cp-${cpIndex}`);
                    if (cpEl) {
                        cpEl.setAttribute('fill', '#4cd137'); // Greenish
                        // Keep it lit so player knows they got it
                    }
                    cpIndex++;
                }
            }
        }

        function showToast(msg) {
            const toast = document.createElement('div');
            toast.innerText = msg;
            toast.style.position = 'absolute';
            toast.style.bottom = '20px';
            toast.style.left = '50%';
            toast.style.transform = 'translateX(-50%)';
            toast.style.background = 'rgba(255,255,255,0.8)';
            toast.style.padding = '10px 20px';
            toast.style.borderRadius = '20px';
            toast.style.color = '#000';
            toast.style.zIndex = '1000';
            arena.appendChild(toast);
            setTimeout(() => toast.remove(), 2000);
        }

        function render() {
            // Perfect centering using translate -50% -50%
            // Note: Rotation removed in READY state handled by CSS or specific logic if needed, 
            // but for simplicity we keep rotation based on velocity or default 0
            const rotation = state === 'PLAYING' ? Math.atan2(shrimp.vel.y, shrimp.vel.x) : 0;
            
            // We use specific transform in CSS pulse for Ready state so we must be careful not to override it completely 
            // BUT inline styles override CSS classes usually. 
            // Let's only update transform if playing to allow CSS pulse to work, OR we integrate pulse here.
            
            if (state === 'PLAYING') {
                 shrimpEl.style.left = '';
                 shrimpEl.style.top = '';
                 shrimpEl.style.transform = `translate(${shrimp.pos.x}px, ${shrimp.pos.y}px) translate(-50%, -50%) rotate(${rotation}rad)`;
            } else if (state === 'READY') {
                // Keep it positioned but allow the scale animation from CSS class to work? 
                // Creating a wrapper would be better but let's try to just set position
                // The CSS animation uses translate(-50%, -50%), so we need to set left/top instead of transform translate for position?
                // OR we update the keyframes to include the dynamic position... which is hard.
                // EASIEST: Update the CSS variable or just set the base transform and let animation scale.
                
                // Let's modify the pulse animation to NOT include translate, and handle translate here.
                // However, our pulse animation `pulse-scale` includes translate(-50%, -50%). 
                // So we just need to set the `left` and `top` properties of the shrimp?
                // Existing shrimp CSS: position: absolute.
                
                shrimpEl.style.left = `${shrimp.pos.x}px`;
                shrimpEl.style.top = `${shrimp.pos.y}px`;
                // transform is handled by class 'shrimp-pulse' which does translate(-50%, -50%) scale(...)
                // We need to clear the specific transform set during PLAYING
                shrimpEl.style.transform = ''; 
            }
            
            plasticItems.forEach(item => {
                const el = document.getElementById(item.id);
                if (el) {
                    el.style.transform = `translate(${item.x - 10}px, ${item.y - 10}px)`;
                }
            });

            lapCountEl.innerText = laps;
            const totalEatenHUD = Object.values(plasticStats).reduce((a, b) => a + b, 0);
            document.getElementById('pieces-count').innerText = totalEatenHUD;
            timeCountEl.innerText = formatTime(gameTime);
            plasticPercentEl.innerText = `${Math.floor(shrimp.plasticLoad)}%`;
            plasticBar.style.width = `${shrimp.plasticLoad}%`;
            
            // Visual feedback on track
            const onTrack = isOnTrack(shrimp.pos.x, shrimp.pos.y);
            shrimpEl.style.filter = onTrack ? 'none' : 'sepia(1) saturate(2) hue-rotate(-20deg)';
            
            const warning = document.getElementById('off-track-warning');
            const timerEl = document.getElementById('reset-timer');
            if (onTrack) {
                warning.style.display = 'none';
            } else {
                warning.style.display = 'block';
                timerEl.innerText = Math.max(0, 2.0 - shrimp.offTrackTime).toFixed(1);
            }
        }

        // Add some bubbles for atmosphere
        for (let i = 0; i < 15; i++) {
            const b = document.createElement('div');
            b.className = 'bubble';
            b.style.width = b.style.height = `${Math.random() * 10 + 5}px`;
            b.style.left = `${Math.random() * 100}%`;
            b.style.animationDelay = `${Math.random() * 10}s`;
            arena.appendChild(b);
        }

    </script>
</body>
</html>

 

1 Reply

  • D-Star's avatar
    D-Star
    Community Member

    Love the idea - a bit buggy (shrimpy?) but the concept is fun :)