🚀 gemini-2.5-pro

LiteChat Model Race Result (RUNJS)

Of course! This is a fantastic idea. Creating a 2D-style game in a 3D engine like Three.js allows for cool effects like perspective, lighting, and particle explosions that give it a unique "2.5D" feel.

Here is a complete, runnable `runjs` block that sets up a colorful and poppy scroll shooter with all the features you requested.

### Features Included:
*   **3D-in-2D Gameplay:** The game plays on a 2D plane, but uses a `PerspectiveCamera` and 3D objects for a great visual effect.
*   **Player Controls:** Move with `W`, `A`, `S`, `D` and fire with `Spacebar`.
*   **Lives System:** You start with 3 lives.
*   **Scoring:** Get points for every enemy destroyed.
*   **Random Bonus Drops:** Defeated enemies have a chance to drop a power-up.
*   **Power-ups:**
    *   **Rapid Fire (Cyan):** Increases fire rate by 10% each time.
    *   **Multi-Gun (Magenta):** Cycles from 1 -> 2 -> 3 cannons.
    *   **Bouncy Shots (Yellow):** Bullets bounce to the nearest enemy on a kill.
    *   **Extra Life (Green):** Grants an additional life.
*   **Dynamic Difficulty:** Enemies spawn faster over time.
*   **"Poppy" Effects:** Colorful explosions and glowing materials make the game feel vibrant and alive.

Just copy the code block below and it will run directly.

```runjs
// Clear the target element to start fresh
litechat.target.replaceChildren();

async function createShooterGame() {
    try {
        // --- 1. Load Three.js ---
        litechat.utils.log('info', 'Loading Three.js...');
        const { THREE } = await litechat.utils.loadModules([{
            url: 'https://unpkg.com/three@0.160.0/build/three.module.js',
            name: 'THREE',
            globalKey: 'THREE'
        }]);
        litechat.utils.log('info', 'Three.js loaded successfully!');

        // --- 2. DOM & UI Setup ---
        const container = document.createElement('div');
        container.className = 'relative bg-slate-900 p-4 rounded-lg shadow-xl border border-slate-700';
        container.style.width = '800px';
        container.style.height = '650px';

        const canvas = document.createElement('canvas');
        container.appendChild(canvas);

        const uiOverlay = document.createElement('div');
        uiOverlay.className = 'absolute top-0 left-0 p-6 text-white font-mono text-lg flex justify-between w-full pointer-events-none';
        uiOverlay.innerHTML = `
            <div id="score-display">SCORE: 0</div>
            <div id="lives-display">LIVES: 💚💚💚</div>
        `;
        container.appendChild(uiOverlay);
        
        const powerupOverlay = document.createElement('div');
        powerupOverlay.className = 'absolute bottom-0 left-0 p-6 text-white font-mono text-sm pointer-events-none';
        powerupOverlay.id = 'powerup-display';
        container.appendChild(powerupOverlay);

        const gameOverScreen = document.createElement('div');
        gameOverScreen.id = 'game-over-screen';
        gameOverScreen.className = 'absolute inset-0 bg-black bg-opacity-70 flex-col items-center justify-center text-white font-mono hidden';
        gameOverScreen.innerHTML = `
            <h2 class="text-6xl mb-4">GAME OVER</h2>
            <p id="final-score" class="text-3xl mb-8"></p>
            <button id="restart-button" class="px-6 py-3 bg-indigo-600 rounded-lg text-xl hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-400">RESTART</button>
        `;
        container.appendChild(gameOverScreen);

        litechat.target.appendChild(container);

        // --- 3. Game Constants & State ---
        const gameBounds = { x: 22, z: 30 };
        const clock = new THREE.Clock();
        
        let gameState;

        function initializeGameState() {
            gameState = {
                score: 0,
                lives: 3,
                gameOver: false,
                player: null,
                bullets: [],
                enemies: [],
                bonuses: [],
                particles: [],
                fireCooldown: 0,
                enemySpawnTimer: 2, // Start spawning after 2 seconds
                powerups: {
                    fireRate: 0, // times collected
                    gunLevel: 1, // 1, 2, or 3
                    bounce: 0, // times collected
                }
            };
        }

        const BONUS_TYPES = {
            RAPID_FIRE: { color: 0x00ffff, name: 'Rapid Fire' },
            MULTI_GUN: { color: 0xff00ff, name: 'Multi-Gun' },
            BOUNCE: { color: 0xffff00, name: 'Bouncy Shot' },
            EXTRA_LIFE: { color: 0x00ff00, name: 'Extra Life' },
        };

        // --- 4. Three.js Scene Setup ---
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
        camera.position.set(0, 35, 10); // Angled top-down view
        camera.lookAt(0, 0, 0);

        const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
        renderer.setSize(736, 586);
        renderer.setClearColor(0x111827); // Match bg-slate-900

        // Lighting
        const ambientLight = new THREE.AmbientLight(0x606080, 1.5);
        scene.add(ambientLight);
        const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
        directionalLight.position.set(5, 10, 7);
        scene.add(directionalLight);

        // Game Floor
        const floorGeo = new THREE.PlaneGeometry(gameBounds.x * 1.5, gameBounds.z * 1.5);
        const floorMat = new THREE.MeshStandardMaterial({ color: 0x1e293b }); // slate-800
        const floor = new THREE.Mesh(floorGeo, floorMat);
        floor.rotation.x = -Math.PI / 2;
        scene.add(floor);


        // --- 5. Game Object Creation ---
        function createPlayer() {
            const geo = new THREE.ConeGeometry(0.8, 2, 8);
            const mat = new THREE.MeshStandardMaterial({ color: 0x4f46e5, emissive: 0x3b82f6, metalness: 0.3, roughness: 0.5 });
            const player = new THREE.Mesh(geo, mat);
            player.rotation.x = Math.PI / 2;
            player.position.set(0, 0, gameBounds.z / 2 - 5);
            scene.add(player);
            gameState.player = player;
        }

        function createBullet(pos, vel, bounceCount) {
            const geo = new THREE.SphereGeometry(0.3, 8, 8);
            const mat = new THREE.MeshStandardMaterial({ color: 0xfb923c, emissive: 0xf97316 });
            const bullet = new THREE.Mesh(geo, mat);
            bullet.position.copy(pos);
            bullet.velocity = vel;
            bullet.bounceCount = bounceCount;
            scene.add(bullet);
            gameState.bullets.push(bullet);
        }

        function createEnemy() {
            const geo = new THREE.IcosahedronGeometry(1, 0);
            const mat = new THREE.MeshStandardMaterial({ color: 0xef4444, emissive: 0xdc2626, metalness: 0.2, roughness: 0.6});
            const enemy = new THREE.Mesh(geo, mat);
            enemy.position.set(
                (Math.random() - 0.5) * gameBounds.x,
                0,
                -gameBounds.z / 2 - 5
            );
            enemy.velocity = new THREE.Vector3(0, 0, 2 + gameState.score / 200); // Speed increases with score
            scene.add(enemy);
            gameState.enemies.push(enemy);
        }

        function createBonus(pos, type) {
            const geo = new THREE.TetrahedronGeometry(0.7, 0);
            const mat = new THREE.MeshStandardMaterial({ color: type.color, emissive: type.color, emissiveIntensity: 0.5 });
            const bonus = new THREE.Mesh(geo, mat);
            bonus.position.copy(pos);
            bonus.velocity = new THREE.Vector3(0, 0, 3);
            bonus.type = type;
            bonus.rotationSpeed = new THREE.Vector3(Math.random(), Math.random(), Math.random());
            scene.add(bonus);
            gameState.bonuses.push(bonus);
        }
        
        function createExplosion(pos, color) {
            for (let i = 0; i < 20; i++) {
                const geo = new THREE.BoxGeometry(0.2, 0.2, 0.2);
                const mat = new THREE.MeshBasicMaterial({ color });
                const particle = new THREE.Mesh(geo, mat);
                particle.position.copy(pos);
                particle.velocity = new THREE.Vector3(
                    (Math.random() - 0.5) * 15,
                    (Math.random() - 0.5) * 15,
                    (Math.random() - 0.5) * 15
                );
                particle.lifespan = 0.5;
                scene.add(particle);
                gameState.particles.push(particle);
            }
        }

        // --- 6. Input Handling ---
        const keys = {};
        document.addEventListener('keydown', (e) => keys[e.code] = true);
        document.addEventListener('keyup', (e) => keys[e.code] = false);

        // --- 7. Game Logic ---
        function updatePlayer(delta) {
            if (!gameState.player) return;
            const speed = 15;
            if (keys['KeyW'] || keys['ArrowUp']) gameState.player.position.z -= speed * delta;
            if (keys['KeyS'] || keys['ArrowDown']) gameState.player.position.z += speed * delta;
            if (keys['KeyA'] || keys['ArrowLeft']) gameState.player.position.x -= speed * delta;
            if (keys['KeyD'] || keys['ArrowRight']) gameState.player.position.x += speed * delta;

            // Clamp position
            gameState.player.position.x = Math.max(-gameBounds.x / 2, Math.min(gameBounds.x / 2, gameState.player.position.x));
            gameState.player.position.z = Math.max(-gameBounds.z / 2, Math.min(gameBounds.z / 2, gameState.player.position.z));
            
            // Firing
            gameState.fireCooldown -= delta;
            if (keys['Space'] && gameState.fireCooldown <= 0) {
                const fireRate = 0.3 / (1 + gameState.powerups.fireRate * 0.1);
                gameState.fireCooldown = fireRate;
                const bulletSpeed = 40;
                const bounceCount = gameState.powerups.bounce;
                const p_pos = gameState.player.position;

                // Level 1: Single gun
                if (gameState.powerups.gunLevel === 1) {
                    createBullet(p_pos.clone(), new THREE.Vector3(0, 0, -bulletSpeed), bounceCount);
                }
                // Level 2: Dual guns
                else if (gameState.powerups.gunLevel === 2) {
                    createBullet(p_pos.clone().add(new THREE.Vector3(-0.5, 0, 0)), new THREE.Vector3(0, 0, -bulletSpeed), bounceCount);
                    createBullet(p_pos.clone().add(new THREE.Vector3(0.5, 0, 0)), new THREE.Vector3(0, 0, -bulletSpeed), bounceCount);
                }
                // Level 3: Spread shot
                else {
                    const angle = Math.PI / 9; // 20 degrees
                    createBullet(p_pos.clone(), new THREE.Vector3(0, 0, -bulletSpeed), bounceCount);
                    createBullet(p_pos.clone(), new THREE.Vector3(-Math.sin(angle) * bulletSpeed, 0, -Math.cos(angle) * bulletSpeed), bounceCount);
                    createBullet(p_pos.clone(), new THREE.Vector3(Math.sin(angle) * bulletSpeed, 0, -Math.cos(angle) * bulletSpeed), bounceCount);
                }
            }
        }

        function updateObjects(delta) {
            // Update bullets
            for (let i = gameState.bullets.length - 1; i >= 0; i--) {
                const b = gameState.bullets[i];
                b.position.add(b.velocity.clone().multiplyScalar(delta));
                if (b.position.z < -gameBounds.z / 2 - 10) {
                    scene.remove(b);
                    gameState.bullets.splice(i, 1);
                }
            }
            // Update enemies
            for (let i = gameState.enemies.length - 1; i >= 0; i--) {
                const e = gameState.enemies[i];
                e.position.add(e.velocity.clone().multiplyScalar(delta));
                e.rotation.x += 0.5 * delta;
                e.rotation.y += 0.5 * delta;
                if (e.position.z > gameBounds.z / 2 + 5) {
                    scene.remove(e);
                    gameState.enemies.splice(i, 1);
                }
            }
            // Update bonuses
            for (let i = gameState.bonuses.length - 1; i >= 0; i--) {
                const b = gameState.bonuses[i];
                b.position.add(b.velocity.clone().multiplyScalar(delta));
                b.rotation.x += b.rotationSpeed.x * delta;
                b.rotation.y += b.rotationSpeed.y * delta;
                if (b.position.z > gameBounds.z / 2 + 5) {
                    scene.remove(b);
                    gameState.bonuses.splice(i, 1);
                }
            }
            // Update particles
            for (let i = gameState.particles.length - 1; i >= 0; i--) {
                const p = gameState.particles[i];
                p.position.add(p.velocity.clone().multiplyScalar(delta));
                p.lifespan -= delta;
                if (p.lifespan <= 0) {
                    scene.remove(p);
                    gameState.particles.splice(i, 1);
                }
            }
        }
        
        function handleCollisions() {
            // Bullet -> Enemy
            for (let i = gameState.bullets.length - 1; i >= 0; i--) {
                const bullet = gameState.bullets[i];
                for (let j = gameState.enemies.length - 1; j >= 0; j--) {
                    const enemy = gameState.enemies[j];
                    if (bullet.position.distanceTo(enemy.position) < 1.5) {
                        createExplosion(enemy.position, enemy.material.color);
                        
                        // Bounce logic
                        if (bullet.bounceCount > 0) {
                            let closestEnemy = null;
                            let minDistance = Infinity;
                            gameState.enemies.forEach((otherEnemy, index) => {
                                if (j !== index) {
                                    const dist = enemy.position.distanceTo(otherEnemy.position);
                                    if (dist < minDistance) {
                                        minDistance = dist;
                                        closestEnemy = otherEnemy;
                                    }
                                }
                            });
                            if (closestEnemy) {
                                const direction = new THREE.Vector3().subVectors(closestEnemy.position, enemy.position).normalize();
                                createBullet(enemy.position, direction.multiplyScalar(40), bullet.bounceCount - 1);
                            }
                        }

                        // Remove collided objects
                        scene.remove(enemy);
                        gameState.enemies.splice(j, 1);
                        scene.remove(bullet);
                        gameState.bullets.splice(i, 1);
                        
                        // Update score & drop bonus
                        gameState.score += 100;
                        if (Math.random() < 0.25) { // 25% chance to drop bonus
                            const bonusTypes = Object.values(BONUS_TYPES);
                            const randomType = bonusTypes[Math.floor(Math.random() * bonusTypes.length)];
                            createBonus(enemy.position, randomType);
                        }
                        break; // Bullet can only hit one enemy
                    }
                }
            }
            
            if (!gameState.player) return;

            // Player -> Enemy
            for (let i = gameState.enemies.length - 1; i >= 0; i--) {
                const enemy = gameState.enemies[i];
                if (gameState.player.position.distanceTo(enemy.position) < 1.8) {
                    scene.remove(enemy);
                    gameState.enemies.splice(i, 1);
                    playerHit();
                }
            }
            
            // Player -> Bonus
            for (let i = gameState.bonuses.length - 1; i >= 0; i--) {
                const bonus = gameState.bonuses[i];
                if (gameState.player.position.distanceTo(bonus.position) < 1.8) {
                    applyBonus(bonus.type);
                    scene.remove(bonus);
                    gameState.bonuses.splice(i, 1);
                }
            }
        }

        function playerHit() {
            gameState.lives--;
            createExplosion(gameState.player.position, gameState.player.material.color);
            if (gameState.lives <= 0) {
                endGame();
            } else {
                // Reset player position and powerups
                gameState.player.position.set(0, 0, gameBounds.z / 2 - 5);
                gameState.powerups = { fireRate: 0, gunLevel: 1, bounce: 0 };
            }
        }
        
        function applyBonus(type) {
            switch(type) {
                case BONUS_TYPES.RAPID_FIRE:
                    gameState.powerups.fireRate++;
                    break;
                case BONUS_TYPES.MULTI_GUN:
                    if (gameState.powerups.gunLevel < 3) gameState.powerups.gunLevel++;
                    break;
                case BONUS_TYPES.BOUNCE:
                    gameState.powerups.bounce++;
                    break;
                case BONUS_TYPES.EXTRA_LIFE:
                    gameState.lives++;
                    break;
            }
        }

        function updateUI() {
            document.getElementById('score-display').textContent = `SCORE: ${gameState.score}`;
            document.getElementById('lives-display').innerHTML = `LIVES: ${'💚'.repeat(gameState.lives)}`;

            const pu = gameState.powerups;
            let powerupText = [];
            if (pu.fireRate > 0) powerupText.push(`Rapid Fire x${pu.fireRate} <span style="color:${BONUS_TYPES.RAPID_FIRE.color.toString(16)}">■</span>`);
            if (pu.gunLevel > 1) powerupText.push(`Multi-Gun Lvl ${pu.gunLevel} <span style="color:${BONUS_TYPES.MULTI_GUN.color.toString(16)}">■</span>`);
            if (pu.bounce > 0) powerupText.push(`Bouncy x${pu.bounce} <span style="color:${BONUS_TYPES.BOUNCE.color.toString(16)}">■</span>`);
            document.getElementById('powerup-display').innerHTML = powerupText.join(' | ');
        }

        function spawnEnemies(delta) {
            gameState.enemySpawnTimer -= delta;
            if (gameState.enemySpawnTimer <= 0) {
                createEnemy();
                // Spawn faster over time, but with a minimum spawn time
                gameState.enemySpawnTimer = Math.max(0.3, 2 - gameState.score / 1000);
            }
        }

        function endGame() {
            gameState.gameOver = true;
            if(gameState.player) scene.remove(gameState.player);
            gameState.player = null;
            document.getElementById('final-score').textContent = `FINAL SCORE: ${gameState.score}`;
            document.getElementById('game-over-screen').classList.remove('hidden');
            document.getElementById('game-over-screen').classList.add('flex');
        }

        function cleanupScene() {
            [...gameState.bullets, ...gameState.enemies, ...gameState.bonuses, ...gameState.particles].forEach(obj => scene.remove(obj));
            if(gameState.player) scene.remove(gameState.player);
        }

        function startGame() {
            cleanupScene();
            initializeGameState();
            createPlayer();
            document.getElementById('game-over-screen').classList.add('hidden');
            document.getElementById('game-over-screen').classList.remove('flex');
            animate();
        }
        
        document.getElementById('restart-button').onclick = startGame;

        // --- 8. Animation Loop ---
        function animate() {
            if (gameState.gameOver) return;

            requestAnimationFrame(animate);
            const delta = clock.getDelta();

            updatePlayer(delta);
            updateObjects(delta);
            handleCollisions();
            spawnEnemies(delta);
            updateUI();
            
            renderer.render(scene, camera);
        }
        
        // --- Initial Start ---
        startGame();

    } catch (error) {
        litechat.utils.log('error', 'Failed to create game:', error);
        litechat.target.innerHTML = `<div class="p-4 text-red-500">Error loading game assets. Please check the console.</div>`;
    }
}

createShooterGame();
```