LiteChat Model Race Result (RUNJS)
Absolutely! Hereβs a complete, colorful, and βcomfyβ 2D scroll shooter with 3D effects using Three.js, tailored to your specs:
- **Comfy game area** (rounded, soft colors, shadows)
- **Bonuses**:
- Faster fire rate
- Bouncy fire
- More guns
- Extra life
- **Enemies get harder with time**
- **Poppy visual effects**
You can play with both keyboard (`Arrow keys`/`A/D` to move, `Space` to shoot) and mouse (move and click/tap to shoot).
```runjs
// Clear previous content
litechat.target.replaceChildren();
(async () => {
// ========== LOAD THREE.JS ==========
const [{ THREE }] = await Promise.all([
litechat.utils.loadModules([
{
url: 'https://unpkg.com/three@0.151.3/build/three.module.js',
name: 'THREE',
globalKey: 'THREE'
}
])
]);
// ========== GAME CONFIG ==========
const WIDTH = 500;
const HEIGHT = 700;
const GAME_BG = 0x252a42;
const GAME_RADIUS = 30;
// Player
const PLAYER_SIZE = 0.8;
const PLAYER_Y = -6;
const PLAYER_SPEED = 0.15;
const PLAYER_COLOR = 0xfff0a5;
// Bullet
const BULLET_SPEED = 0.45;
const BULLET_SIZE = 0.25;
const BULLET_COLOR = 0x4ff4ff;
// Enemies
const ENEMY_SIZE = 0.9;
const ENEMY_COLOR = 0xff4f70;
const ENEMY_START_SPEED = 0.04;
const ENEMY_SPAWN_INTERVAL = 60; // frames
// Bonuses
const BONUS_COLOR = 0x6cbbff;
const BONUS_SIZE = 0.6;
const BONUS_CHANCE = 0.18;
// Game Area
const VISIBLE_W = 10;
const VISIBLE_H = 14;
// ========== DOM SETUP ==========
// Wrapper
const wrapper = document.createElement('div');
wrapper.className = 'flex flex-col items-center justify-center bg-gradient-to-b from-blue-100 to-purple-100 rounded-3xl shadow-xl p-3';
litechat.target.appendChild(wrapper);
// Title
const title = document.createElement('h3');
title.textContent = 'π 3D Poppy Scroll Shooter';
title.className = 'font-bold text-xl mb-2 text-center text-blue-600';
wrapper.appendChild(title);
// Canvas container
const canvasWrap = document.createElement('div');
canvasWrap.className = 'overflow-hidden rounded-3xl shadow-lg border-4 border-blue-200';
canvasWrap.style.width = WIDTH + 'px';
canvasWrap.style.height = HEIGHT + 'px';
canvasWrap.style.background = '#252a42';
canvasWrap.style.position = 'relative';
wrapper.appendChild(canvasWrap);
// Scoreboard
const scoreBar = document.createElement('div');
scoreBar.className = 'flex flex-row justify-between items-center w-full px-2 py-1 bg-white/70 text-blue-800 rounded-b-lg text-lg mt-2 font-mono';
wrapper.appendChild(scoreBar);
// ========== THREE.JS SCENE SETUP ==========
// Camera
const camera = new THREE.OrthographicCamera(
-VISIBLE_W / 2, VISIBLE_W / 2,
VISIBLE_H / 2, -VISIBLE_H / 2,
0.1, 100
);
camera.position.set(0, 0, 30);
// Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(GAME_BG);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(WIDTH, HEIGHT);
renderer.domElement.style.display = 'block';
renderer.domElement.style.borderRadius = `${GAME_RADIUS}px`;
renderer.domElement.tabIndex = 1; // for focus
canvasWrap.appendChild(renderer.domElement);
// Lighting (soft, comfy)
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambient);
const spot = new THREE.SpotLight(0xffffff, 0.7, 30, Math.PI / 3, 0.5, 1.5);
spot.position.set(0, 8, 25);
scene.add(spot);
// ========== GAME OBJECTS ==========
// Player
function makePlayerMesh() {
// Poppy, round spaceship
const geom = new THREE.TorusGeometry(PLAYER_SIZE, PLAYER_SIZE * 0.19, 16, 32);
const mat = new THREE.MeshStandardMaterial({ color: PLAYER_COLOR, emissive: 0xffeeb5, roughness: 0.28 });
const mesh = new THREE.Mesh(geom, mat);
mesh.castShadow = true;
mesh.receiveShadow = true;
// Add a sphere in the middle
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(PLAYER_SIZE * 0.33, 20, 20),
new THREE.MeshStandardMaterial({ color: 0xffc86c, emissive: 0xffa040, roughness: 0.18 })
);
mesh.add(sphere);
sphere.position.set(0, 0, 0.25);
return mesh;
}
// Bullet
function makeBulletMesh(color = BULLET_COLOR) {
const geom = new THREE.SphereGeometry(BULLET_SIZE, 10, 10);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, roughness: 0.2 });
return new THREE.Mesh(geom, mat);
}
// Enemy
function makeEnemyMesh(color = ENEMY_COLOR) {
const geom = new THREE.DodecahedronGeometry(ENEMY_SIZE, 1);
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, roughness: 0.4 });
return new THREE.Mesh(geom, mat);
}
// Bonus
function makeBonusMesh(type) {
let color, mesh;
switch (type) {
case 'fireRate':
color = 0x96f76d; // green
mesh = new THREE.Mesh(
new THREE.TorusGeometry(BONUS_SIZE, BONUS_SIZE * 0.17, 16, 28),
new THREE.MeshStandardMaterial({ color, emissive: color, metalness: 0.1 })
);
break;
case 'bouncy':
color = 0xffa7f7; // pink
mesh = new THREE.Mesh(
new THREE.IcosahedronGeometry(BONUS_SIZE * 0.6, 0),
new THREE.MeshStandardMaterial({ color, emissive: color, metalness: 0.18 })
);
break;
case 'moreGun':
color = 0x7ec7ff; // cyan
mesh = new THREE.Mesh(
new THREE.OctahedronGeometry(BONUS_SIZE * 0.65, 0),
new THREE.MeshStandardMaterial({ color, emissive: color, metalness: 0.18 })
);
break;
case 'life':
color = 0xff4f70; // pink-red
mesh = new THREE.Mesh(
new THREE.SphereGeometry(BONUS_SIZE * 0.6, 13, 13),
new THREE.MeshStandardMaterial({ color, emissive: color, metalness: 0.2 })
);
break;
}
return mesh;
}
// ========== GAME STATE ==========
let player = {
x: 0,
y: PLAYER_Y,
mesh: makePlayerMesh(),
fireCooldown: 0,
baseFireRate: 13,
fireRateBonus: 0,
guns: 1,
gunBonus: 0,
bouncy: 0,
lives: 3,
};
player.mesh.position.set(player.x, player.y, 0);
scene.add(player.mesh);
let bullets = [];
let enemies = [];
let bonuses = [];
let enemySpeed = ENEMY_START_SPEED;
let enemySpawnRate = ENEMY_SPAWN_INTERVAL;
let spawnTick = 0;
let score = 0;
let running = true;
let tick = 0;
// ========== INPUT ==========
let input = { left: false, right: false, shoot: false, mouseX: 0 };
// Keyboard
renderer.domElement.addEventListener('keydown', e => {
if (e.repeat) return;
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') input.left = true;
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') input.right = true;
if (e.key === ' ' || e.key === 'z' || e.key === 'Z') input.shoot = true;
});
renderer.domElement.addEventListener('keyup', e => {
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') input.left = false;
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') input.right = false;
if (e.key === ' ' || e.key === 'z' || e.key === 'Z') input.shoot = false;
});
// Mouse/touch move & click to shoot
renderer.domElement.addEventListener('mousemove', e => {
const rect = renderer.domElement.getBoundingClientRect();
const mx = ((e.clientX - rect.left) / rect.width - 0.5) * VISIBLE_W;
input.mouseX = Math.max(-VISIBLE_W / 2 + 1, Math.min(VISIBLE_W / 2 - 1, mx));
});
renderer.domElement.addEventListener('mousedown', e => {
input.shoot = true;
});
renderer.domElement.addEventListener('mouseup', e => {
input.shoot = false;
});
renderer.domElement.addEventListener('touchstart', e => {
input.shoot = true;
if (e.touches.length) {
const rect = renderer.domElement.getBoundingClientRect();
const tx = e.touches[0].clientX;
const mx = ((tx - rect.left) / rect.width - 0.5) * VISIBLE_W;
input.mouseX = Math.max(-VISIBLE_W / 2 + 1, Math.min(VISIBLE_W / 2 - 1, mx));
}
});
renderer.domElement.addEventListener('touchend', e => {
input.shoot = false;
});
// focus for keyboard controls
renderer.domElement.focus();
// ========== GAMEPLAY ==========
function updatePlayer() {
// Mouse or keyboard move
let targetX = player.x;
if (Math.abs(input.mouseX) > 0.001) targetX = input.mouseX;
if (input.left) targetX = player.x - PLAYER_SPEED * 1.7;
if (input.right) targetX = player.x + PLAYER_SPEED * 1.7;
targetX = Math.max(-VISIBLE_W / 2 + 1, Math.min(VISIBLE_W / 2 - 1, targetX));
player.x += (targetX - player.x) * 0.25; // smooth
player.mesh.position.x = player.x;
// Firing
if (player.fireCooldown > 0) player.fireCooldown--;
const fireRate = Math.max(4, Math.round(player.baseFireRate * Math.pow(0.9, player.fireRateBonus)));
if (input.shoot && player.fireCooldown === 0) {
// Gun layout
let gunCount = 1 + player.gunBonus;
if (gunCount > 3) gunCount = 3;
let gunPattern = [[0]];
if (gunCount === 2) gunPattern = [[-0.35], [0.35]];
if (gunCount === 3) gunPattern = [[0], [-0.45], [0.45]];
for (let i = 0; i < gunCount; ++i) {
let dx = gunPattern[i][0];
let bullet = {
x: player.x + dx,
y: player.y + PLAYER_SIZE * 0.85,
vy: BULLET_SPEED,
mesh: makeBulletMesh(),
bounces: player.bouncy,
};
bullet.mesh.position.set(bullet.x, bullet.y, 0.5);
scene.add(bullet.mesh);
bullets.push(bullet);
}
player.fireCooldown = fireRate;
}
}
function updateBullets() {
for (let bullet of bullets) {
bullet.y += bullet.vy;
bullet.mesh.position.y = bullet.y;
}
// Remove offscreen
for (let i = bullets.length - 1; i >= 0; --i) {
if (bullets[i].y > VISIBLE_H / 2 + 1) {
scene.remove(bullets[i].mesh);
bullets.splice(i, 1);
}
}
}
function spawnEnemy() {
let x = (Math.random() - 0.5) * (VISIBLE_W - ENEMY_SIZE * 2.5);
let y = VISIBLE_H / 2 + ENEMY_SIZE * 1.2;
let mesh = makeEnemyMesh();
mesh.position.set(x, y, 0.5);
scene.add(mesh);
let hp = 1 + Math.floor(tick / (60 * 16));
let speed = enemySpeed * (1 + Math.random() * 0.25);
enemies.push({ x, y, mesh, hp, speed });
}
function updateEnemies() {
for (let enemy of enemies) {
enemy.y -= enemy.speed;
enemy.mesh.position.y = enemy.y;
enemy.mesh.rotation.x += 0.01;
enemy.mesh.rotation.y += 0.007;
}
// Remove offscreen
for (let i = enemies.length - 1; i >= 0; --i) {
if (enemies[i].y < -VISIBLE_H / 2 - ENEMY_SIZE) {
scene.remove(enemies[i].mesh);
enemies.splice(i, 1);
loseLife();
}
}
}
function spawnBonus(x, y) {
// Pick random bonus
const types = ['fireRate', 'bouncy', 'moreGun', 'life'];
const type = types[Math.floor(Math.random() * types.length)];
const mesh = makeBonusMesh(type);
mesh.position.set(x, y, 1.2);
scene.add(mesh);
bonuses.push({ x, y, mesh, type, vy: 0.065 });
}
function updateBonuses() {
for (let bonus of bonuses) {
bonus.y -= bonus.vy;
bonus.mesh.position.y = bonus.y;
bonus.mesh.rotation.x += 0.03;
bonus.mesh.rotation.y += 0.01;
}
// Remove offscreen
for (let i = bonuses.length - 1; i >= 0; --i) {
if (bonuses[i].y < -VISIBLE_H / 2 - 1) {
scene.remove(bonuses[i].mesh);
bonuses.splice(i, 1);
}
}
}
function collide(a, b, dist) {
return Math.abs(a.x - b.x) < dist && Math.abs(a.y - b.y) < dist;
}
function loseLife() {
player.lives--;
popEffect(player.x, player.y, 0xffb6b6);
if (player.lives <= 0) {
running = false;
showGameOver();
}
}
function popEffect(x, y, color) {
// Fun pop
for (let i = 0; i < 7; ++i) {
let mesh = new THREE.Mesh(
new THREE.SphereGeometry(0.12, 8, 8),
new THREE.MeshStandardMaterial({ color, emissive: color, metalness: 0.1 })
);
mesh.position.set(x, y, 1.7);
scene.add(mesh);
let angle = Math.random() * Math.PI * 2;
let speed = 0.14 + Math.random() * 0.13;
let life = 22 + Math.random() * 12;
let obj = { mesh, dx: Math.cos(angle) * speed, dy: Math.sin(angle) * speed, life };
popParticles.push(obj);
}
}
let popParticles = [];
function updatePopParticles() {
for (let o of popParticles) {
o.mesh.position.x += o.dx;
o.mesh.position.y += o.dy;
o.mesh.material.opacity = Math.max(0, o.life / 30);
o.mesh.material.transparent = true;
o.life--;
}
for (let i = popParticles.length - 1; i >= 0; --i) {
if (popParticles[i].life < 0) {
scene.remove(popParticles[i].mesh);
popParticles.splice(i, 1);
}
}
}
function handleCollisions() {
// Bullet & Enemy
for (let bi = bullets.length - 1; bi >= 0; --bi) {
let bullet = bullets[bi];
for (let ei = enemies.length - 1; ei >= 0; --ei) {
let enemy = enemies[ei];
if (collide(bullet, enemy, ENEMY_SIZE * 0.7)) {
enemy.hp--;
if (enemy.hp <= 0) {
popEffect(enemy.x, enemy.y, ENEMY_COLOR);
scene.remove(enemy.mesh);
enemies.splice(ei, 1);
score += 10;
// Bonus drop
if (Math.random() < BONUS_CHANCE) spawnBonus(enemy.x, enemy.y);
} else {
// Minor pop
popEffect(enemy.x, enemy.y, 0xffcfcf);
}
if (bullet.bounces > 0) {
// Try to bounce to another enemy
let found = false;
for (let ej = 0; ej < enemies.length; ++ej) {
if (ej !== ei && Math.abs(enemies[ej].y - enemy.y) < 2 && Math.random() < 0.7) {
bullet.x = enemies[ej].x;
bullet.y = enemies[ej].y;
bullet.bounces--;
found = true;
break;
}
}
if (!found) {
scene.remove(bullet.mesh);
bullets.splice(bi, 1);
}
} else {
scene.remove(bullet.mesh);
bullets.splice(bi, 1);
}
break;
}
}
}
// Player & Bonus
for (let i = bonuses.length - 1; i >= 0; --i) {
if (collide(player, bonuses[i], 0.95)) {
popEffect(bonuses[i].x, bonuses[i].y, BONUS_COLOR);
applyBonus(bonuses[i].type);
scene.remove(bonuses[i].mesh);
bonuses.splice(i, 1);
}
}
// Player & Enemy
for (let i = enemies.length - 1; i >= 0; --i) {
if (collide(player, enemies[i], ENEMY_SIZE * 0.7)) {
popEffect(enemies[i].x, enemies[i].y, ENEMY_COLOR);
scene.remove(enemies[i].mesh);
enemies.splice(i, 1);
loseLife();
}
}
}
function applyBonus(type) {
switch (type) {
case 'fireRate':
player.fireRateBonus++;
break;
case 'bouncy':
player.bouncy++;
break;
case 'moreGun':
player.gunBonus++;
break;
case 'life':
player.lives++;
break;
}
}
function updateDifficulty() {
// Every 15 seconds, make enemies faster & spawn more
if (tick % (60 * 15) === 0) {
enemySpeed += 0.008;
if (enemySpawnRate > 22) enemySpawnRate -= 6;
}
}
function updateScoreBar() {
scoreBar.innerHTML = `
<span>Score: <b>${score}</b></span>
<span>Lives: <b style="color:#ff4f70">${player.lives}</b></span>
<span>Guns: <b>${1 + player.gunBonus}</b></span>
<span>Fire: <b>${Math.round(100 * Math.pow(0.9, player.fireRateBonus))}%</b></span>
<span>Bouncy: <b>${player.bouncy}</b></span>
`;
}
function showGameOver() {
const overDiv = document.createElement('div');
overDiv.className = 'absolute inset-0 flex flex-col items-center justify-center bg-black/70 text-white rounded-3xl z-10';
overDiv.innerHTML = `
<div class="font-bold text-3xl mb-2">Game Over</div>
<div class="mb-3">Score: <b>${score}</b></div>
<button class="px-4 py-2 rounded bg-pink-400 text-white font-bold shadow" tabindex="0">Restart</button>
`;
overDiv.querySelector('button').onclick = () => {
wrapper.removeChild(overDiv);
restart();
};
canvasWrap.appendChild(overDiv);
}
function restart() {
// Remove all meshes
for (let arr of [bullets, enemies, bonuses, popParticles]) {
for (let obj of arr) scene.remove(obj.mesh);
arr.length = 0;
}
// Reset player
player.x = 0;
player.lives = 3;
player.gunBonus = 0;
player.fireRateBonus = 0;
player.bouncy = 0;
player.fireCooldown = 0;
player.mesh.position.set(player.x, player.y, 0);
// Reset score/difficulty
score = 0;
enemySpeed = ENEMY_START_SPEED;
enemySpawnRate = ENEMY_SPAWN_INTERVAL;
running = true;
tick = 0;
spawnTick = 0;
}
// ========== GAME LOOP ==========
function animate() {
if (running) {
tick++;
updatePlayer();
updateBullets();
updateEnemies();
updateBonuses();
updatePopParticles();
handleCollisions();
updateDifficulty();
updateScoreBar();
// Enemy spawn
spawnTick++;
if (spawnTick >= enemySpawnRate) {
spawnEnemy();
spawnTick = 0;
}
}
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
})();
```
**How to play:**
- Move: Arrow keys, A/D, or mouse/touch
- Shoot: Spacebar, Z, or mouse/touch
- Collect bonuses for power-ups!
Let me know if you want to tweak colors, add sound, or more effects!