LiteChat Race Result
This is an ambitious and exciting project! Combining 2D scroll shooter mechanics with 3D effects using Three.js within a `runjs` block is a fantastic challenge. I'll create a vibrant, poppy, and comfy game experience with the requested features.
Here's a breakdown of how I'll approach this, followed by the `runjs` block:
### Game Concept & Features:
* **2D Scroll Shooter with 3D Effect:**
* The gameplay will be on a 2D plane (X-Z in Three.js terms, with Y as the "up" direction for depth).
* The "3D effect" will come from the models themselves, lighting, shadows, and camera perspective, making it feel like objects are popping out while still moving on a 2D plane.
* The background will scroll to simulate forward movement.
* **Colorful and Poppy:**
* Bright, contrasting colors for player, enemies, bullets, and power-ups.
* Simple, stylized geometry for a clean, "poppy" look.
* Particle effects for explosions and power-up collection.
* **Comfy Game Area:**
* A clear, uncluttered play space.
* Intuitive controls (mouse for movement, click for fire).
* Subtle background elements that don't distract.
* **Live Display:**
* A simple UI overlay for lives and score.
* **Random Bonus Drops:**
* Each enemy kill has a chance to drop a bonus.
* **Faster Fire Rate:** Increases player's bullet firing speed.
* **Bouncy Fire:** Bullets can bounce to hit additional enemies after an initial kill.
* **More Gun:**
* Level 1: Single shot (default)
* Level 2: Dual shots (left and right of player)
* Level 3: Triple shot (center, angled left, angled right)
* **Extra Life:** Grants one additional life.
* **Enemy Progression:**
* Enemies will appear in waves.
* Over time (or by wave progression), enemies will get faster and potentially have more health.
### Technical Implementation (within `runjs`):
1. **Three.js Setup:** Scene, camera (orthographic or perspective with a fixed Y for 2D feel), renderer, lights.
2. **Game Loop:** `requestAnimationFrame` for continuous updates.
3. **Player:**
* Mesh (e.g., a simple spaceship model).
* Movement based on mouse X/Y.
* Collision detection with enemies and enemy bullets.
* Firing mechanism.
4. **Bullets:**
* Player bullets: Simple meshes, move forward, collision with enemies.
* Enemy bullets (if implemented): Similar to player bullets, collision with player.
5. **Enemies:**
* Different types of enemy meshes.
* Spawn logic (randomized or wave-based).
* Movement patterns (e.g., straight down, simple zig-zag).
* Health system.
* Collision detection with player bullets.
6. **Power-ups:**
* Distinct meshes/colors for each type.
* Drop chance on enemy destruction.
* Collection logic when player touches them.
* Apply effects to player stats.
7. **Scrolling Background:**
* A large plane or multiple planes that move towards the camera and loop.
8. **Collision Detection:** Simple bounding box or sphere intersections for performance.
9. **UI:** DOM elements overlayed on the canvas for lives, score, and game over.
10. **Game State Management:** Variables for score, lives, power-up levels, game over state.
Let's build this! Due to the complexity, I'll start with a solid foundation and implement the core mechanics and a few power-ups. We can expand it further if you like.
```runjs
// Make sure to clear any previous content in the target
litechat.target.replaceChildren();
async function createShooterGame() {
try {
litechat.utils.log('info', '🚀 Starting 2D Shooter (3D Effect) creation...');
// Load Three.js and OrbitControls (though we won't use OrbitControls for gameplay, it's good for debugging initially)
const modules = await litechat.utils.loadModules([
{
url: 'https://unpkg.com/three@0.170.0/build/three.module.js',
name: 'THREE',
globalKey: 'THREE',
importMap: {
"three": "https://unpkg.com/three@0.170.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.170.0/examples/jsm/"
}
},
{
url: 'https://unpkg.com/three@0.170.0/examples/jsm/controls/OrbitControls.js',
name: 'OrbitControls', // We'll load it but not use it for actual gameplay
dependencies: ['THREE']
}
]);
const { THREE } = modules;
// OrbitControls is loaded but not directly used in the game loop for player control.
// --- Game Configuration & Constants ---
const GAME_WIDTH = 12; // Units
const GAME_HEIGHT = 20; // Units (Z-axis in Three.js)
const PLAYER_SPEED = 0.15; // How fast player moves with mouse
const PLAYER_BULLET_SPEED = 0.4;
const ENEMY_BULLET_SPEED = 0.2;
const ENEMY_SPEED_BASE = 0.05;
const ENEMY_SPAWN_INTERVAL = 1000; // ms
const BACKGROUND_SCROLL_SPEED = 0.08;
const MAX_PLAYER_BULLETS = 50;
const MAX_ENEMY_BULLETS = 100;
const POWERUP_DROP_CHANCE = 0.3; // 30% chance for an enemy to drop a power-up
const INVINCIBILITY_TIME = 2000; // ms after hit
// --- Game State Variables ---
let score = 0;
let lives = 3;
let gameOver = false;
let lastEnemySpawnTime = 0;
let lastPlayerFireTime = 0;
let playerFireRateModifier = 1.0; // 1.0 = normal, <1.0 = faster
let playerBulletBounces = 0; // Number of enemies a bouncy bullet can hit
let playerGunLevel = 1; // 1: single, 2: dual, 3: triple
let playerInvincible = false;
let playerInvincibleEndTime = 0;
let playerHitFlashTimer = 0;
const playerBullets = [];
const enemies = [];
const enemyBullets = [];
const powerups = [];
const particles = []; // For explosions
// --- DOM Elements for UI ---
const gameContainer = document.createElement('div');
gameContainer.className = 'relative w-full h-[600px] bg-black overflow-hidden rounded-lg shadow-xl m-auto flex flex-col items-center justify-center';
gameContainer.style.width = '800px'; // Fixed width for game area
gameContainer.style.height = '600px'; // Fixed height for game area
gameContainer.style.outline = '5px solid #FFD700'; // Gold border for comfy feel
gameContainer.style.boxShadow = '0 0 20px rgba(255,215,0,0.5)';
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
canvas.className = 'absolute top-0 left-0 z-0'; // Canvas below UI
gameContainer.appendChild(canvas);
const uiOverlay = document.createElement('div');
uiOverlay.className = 'absolute top-0 left-0 w-full h-full p-4 flex flex-col justify-between z-10 pointer-events-none';
gameContainer.appendChild(uiOverlay);
const topUI = document.createElement('div');
topUI.className = 'flex justify-between items-center text-white text-2xl font-bold bg-gradient-to-b from-gray-900/80 to-transparent p-2 rounded-lg';
uiOverlay.appendChild(topUI);
const scoreDisplay = document.createElement('span');
scoreDisplay.textContent = `SCORE: ${score}`;
scoreDisplay.className = 'text-green-400 drop-shadow-[0_0_5px_rgba(0,255,0,0.5)]';
topUI.appendChild(scoreDisplay);
const livesDisplay = document.createElement('span');
livesDisplay.textContent = `LIVES: ${lives}`;
livesDisplay.className = 'text-red-400 drop-shadow-[0_0_5px_rgba(255,0,0,0.5)]';
topUI.appendChild(livesDisplay);
const messageDisplay = document.createElement('div');
messageDisplay.className = 'text-center text-white text-5xl font-extrabold drop-shadow-[0_0_10px_rgba(255,255,255,0.8)] opacity-0 transition-opacity duration-500';
uiOverlay.appendChild(messageDisplay);
const restartButton = document.createElement('button');
restartButton.textContent = 'RESTART GAME';
restartButton.className = 'absolute bottom-1/4 left-1/2 -translate-x-1/2 px-8 py-4 bg-blue-600 hover:bg-blue-700 text-white text-3xl font-bold rounded-full shadow-lg transition-all duration-300 transform scale-0 pointer-events-none opacity-0';
restartButton.style.pointerEvents = 'none'; // Initially disabled
restartButton.onclick = () => resetGame();
gameContainer.appendChild(restartButton);
litechat.target.appendChild(gameContainer);
function updateUI() {
scoreDisplay.textContent = `SCORE: ${score}`;
livesDisplay.textContent = `LIVES: ${lives}`;
if (playerInvincible) {
playerHitFlashTimer = (playerHitFlashTimer + 1) % 10; // Simple flicker
player.mesh.visible = playerHitFlashTimer < 5;
} else {
player.mesh.visible = true;
}
}
function showMessage(text, fadeOut = true) {
messageDisplay.textContent = text;
messageDisplay.classList.remove('opacity-0');
messageDisplay.classList.add('opacity-100');
messageDisplay.style.pointerEvents = 'auto'; // Enable interaction for messages if needed
if (fadeOut) {
setTimeout(() => {
messageDisplay.classList.remove('opacity-100');
messageDisplay.classList.add('opacity-0');
}, 1500);
}
}
function showRestartButton() {
restartButton.classList.remove('scale-0', 'opacity-0');
restartButton.classList.add('scale-100', 'opacity-100');
restartButton.style.pointerEvents = 'auto';
}
function hideRestartButton() {
restartButton.classList.remove('scale-100', 'opacity-100');
restartButton.classList.add('scale-0', 'opacity-0');
restartButton.style.pointerEvents = 'none';
}
// --- Three.js Scene Setup ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x100020); // Dark purple for space
// Camera: Orthographic to emphasize 2D gameplay, but with a slight perspective feel
// We'll use a PerspectiveCamera but position it to simulate orthographic for the main action
const camera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);
camera.position.set(0, 15, GAME_HEIGHT / 2); // Y for height/depth, Z for background
camera.lookAt(0, 0, GAME_HEIGHT / 2 - 5); // Look slightly down the Z-axis
scene.add(camera);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setSize(canvas.width, canvas.height);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Lights
const ambientLight = new THREE.AmbientLight(0x404040, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -GAME_WIDTH / 2;
directionalLight.shadow.camera.right = GAME_WIDTH / 2;
directionalLight.shadow.camera.top = GAME_HEIGHT / 2;
directionalLight.shadow.camera.bottom = -GAME_HEIGHT / 2;
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0xffaaff, 0.5, 50); // Pinkish light
pointLight.position.set(0, 5, 0);
scene.add(pointLight);
// --- Game Objects ---
// Player
const player = {
mesh: null,
collider: new THREE.Box3(),
init: function () {
const geometry = new THREE.ConeGeometry(0.8, 2, 8);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00, emissive: 0x008800, emissiveIntensity: 0.5 });
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.rotation.x = Math.PI / 2; // Point forward
this.mesh.position.set(0, 0, -GAME_HEIGHT / 2 + 2); // Start at bottom center
this.mesh.castShadow = true;
this.mesh.receiveShadow = true;
scene.add(this.mesh);
this.updateCollider();
},
updateCollider: function () {
this.collider.setFromObject(this.mesh);
}
};
player.init();
// Background (Scrolling Stars/Nebula)
const backgroundMaterial = new THREE.MeshBasicMaterial({
map: new THREE.TextureLoader().load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/planets/earth_atmos_2048.jpg'), // Placeholder, ideally a seamless space texture
side: THREE.BackSide
});
backgroundMaterial.map.wrapS = THREE.RepeatWrapping;
backgroundMaterial.map.wrapT = THREE.RepeatWrapping;
backgroundMaterial.map.repeat.set(1, 2); // Repeat vertically
const backgroundGeometry = new THREE.PlaneGeometry(GAME_WIDTH * 2, GAME_HEIGHT * 4); // Larger plane
const backgroundMesh = new THREE.Mesh(backgroundGeometry, backgroundMaterial);
backgroundMesh.rotation.x = -Math.PI / 2; // Lay flat on XZ plane
backgroundMesh.position.set(0, -1, GAME_HEIGHT / 2); // Position behind game area
scene.add(backgroundMesh);
// --- Helper Functions ---
function createBullet(isPlayerBullet = true, position, color, initialRotation = 0) {
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.8);
const material = new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.8 });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(position);
mesh.rotation.y = initialRotation; // For angled shots
mesh.castShadow = true;
mesh.receiveShadow = false;
scene.add(mesh);
const bullet = { mesh, isPlayerBullet, collider: new THREE.Box3(), bounces: playerBulletBounces };
bullet.updateCollider = () => bullet.collider.setFromObject(bullet.mesh);
bullet.updateCollider();
return bullet;
}
function createEnemy(type = 'basic') {
let geometry, material, health, points;
switch (type) {
case 'basic':
geometry = new THREE.BoxGeometry(1, 1, 1);
material = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0x880000, emissiveIntensity: 0.5 });
health = 1;
points = 100;
break;
case 'fast':
geometry = new THREE.OctahedronGeometry(0.8);
material = new THREE.MeshStandardMaterial({ color: 0xff8800, emissive: 0xaa4400, emissiveIntensity: 0.5 });
health = 1;
points = 150;
break;
case 'strong':
geometry = new THREE.DodecahedronGeometry(1.2);
material = new THREE.MeshStandardMaterial({ color: 0x8800ff, emissive: 0x4400aa, emissiveIntensity: 0.5 });
health = 3;
points = 300;
break;
}
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(
(Math.random() - 0.5) * (GAME_WIDTH - 2), // Random X within bounds
0, // Y-axis for depth
GAME_HEIGHT / 2 // Start at top
);
mesh.castShadow = true;
mesh.receiveShadow = true;
scene.add(mesh);
const enemy = { mesh, health, points, type, collider: new THREE.Box3() };
enemy.updateCollider = () => enemy.collider.setFromObject(enemy.mesh);
enemy.updateCollider();
return enemy;
}
function createPowerup(type, position) {
let geometry, material, color;
switch (type) {
case 'fireRate':
geometry = new THREE.SphereGeometry(0.5, 16, 16);
color = 0xffd700; // Gold
break;
case 'bouncyFire':
geometry = new THREE.TorusGeometry(0.4, 0.2, 8, 16);
color = 0x00ffff; // Cyan
break;
case 'moreGun':
geometry = new THREE.ConeGeometry(0.5, 1, 4);
color = 0xff00ff; // Magenta
break;
case 'extraLife':
geometry = new THREE.TetrahedronGeometry(0.6);
color = 0x00ff00; // Green
break;
}
material = new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.6 });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(position);
mesh.position.y = 0; // Keep on the game plane
mesh.castShadow = true;
scene.add(mesh);
const powerup = { mesh, type, collider: new THREE.Box3() };
powerup.updateCollider = () => powerup.collider.setFromObject(powerup.mesh);
powerup.updateCollider();
return powerup;
}
function createParticle(position, color, scale = 0.1, speed = 0.1) {
const geometry = new THREE.SphereGeometry(scale, 8, 8);
const material = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 1.0 });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(position);
scene.add(mesh);
const velocity = new THREE.Vector3(
(Math.random() - 0.5) * speed,
(Math.random() - 0.5) * speed,
(Math.random() - 0.5) * speed
);
const particle = { mesh, velocity, life: 60 }; // 60 frames life
return particle;
}
function explode(position, color, numParticles = 10) {
for (let i = 0; i < numParticles; i++) {
particles.push(createParticle(position, color, 0.1 + Math.random() * 0.1, 0.1 + Math.random() * 0.1));
}
}
// --- Game Logic ---
function handlePlayerFire(currentTime) {
const fireDelay = 200 * playerFireRateModifier; // Base 200ms, faster with modifier
if (currentTime - lastPlayerFireTime > fireDelay) {
const bulletPos = player.mesh.position.clone();
bulletPos.z += 0.5; // Offset slightly in front of player
if (playerGunLevel === 1) {
playerBullets.push(createBullet(true, bulletPos, 0x00ff00)); // Green bullet
} else if (playerGunLevel === 2) {
playerBullets.push(createBullet(true, bulletPos.clone().add(new THREE.Vector3(-0.5, 0, 0)), 0x00ff00));
playerBullets.push(createBullet(true, bulletPos.clone().add(new THREE.Vector3(0.5, 0, 0)), 0x00ff00));
} else if (playerGunLevel === 3) {
playerBullets.push(createBullet(true, bulletPos, 0x00ff00));
playerBullets.push(createBullet(true, bulletPos.clone().add(new THREE.Vector3(-0.5, 0, 0)), 0x00ff00, -0.2)); // Angled left
playerBullets.push(createBullet(true, bulletPos.clone().add(new THREE.Vector3(0.5, 0, 0)), 0x00ff00, 0.2)); // Angled right
}
lastPlayerFireTime = currentTime;
}
}
function checkCollisions() {
// Player Bullets vs. Enemies
for (let i = playerBullets.length - 1; i >= 0; i--) {
const bullet = playerBullets[i];
bullet.updateCollider();
for (let j = enemies.length - 1; j >= 0; j--) {
const enemy = enemies[j];
enemy.updateCollider();
if (bullet.collider.intersectsBox(enemy.collider)) {
enemy.health--;
score += 10; // Hit bonus
explode(bullet.mesh.position, 0xffaaff, 3); // Small hit explosion
if (enemy.health <= 0) {
score += enemy.points;
explode(enemy.mesh.position, enemy.mesh.material.color, 20); // Big explosion
// Power-up Drop
if (Math.random() < POWERUP_DROP_CHANCE) {
const powerupTypes = ['fireRate', 'bouncyFire', 'moreGun', 'extraLife'];
const randomType = powerupTypes[Math.floor(Math.random() * powerupTypes.length)];
powerups.push(createPowerup(randomType, enemy.mesh.position));
}
scene.remove(enemy.mesh);
enemies.splice(j, 1);
if (bullet.bounces > 0) {
bullet.bounces--;
// Bullet continues, but loses a bounce charge
} else {
scene.remove(bullet.mesh);
playerBullets.splice(i, 1);
break; // Bullet is destroyed, move to next player bullet
}
} else {
// Bullet is destroyed if enemy is not defeated and it's not bouncy
if (bullet.bounces === 0) {
scene.remove(bullet.mesh);
playerBullets.splice(i, 1);
break;
}
}
}
}
}
// Player vs. Enemies
if (!playerInvincible) {
player.updateCollider();
for (let i = enemies.length - 1; i >= 0; i--) {
const enemy = enemies[i];
enemy.updateCollider();
if (player.collider.intersectsBox(enemy.collider)) {
takeDamage();
explode(player.mesh.position, 0xff0000, 30); // Player hit explosion
scene.remove(enemy.mesh);
enemies.splice(i, 1);
break;
}
}
}
// Player vs. Powerups
player.updateCollider();
for (let i = powerups.length - 1; i >= 0; i--) {
const powerup = powerups[i];
powerup.updateCollider();
if (player.collider.intersectsBox(powerup.collider)) {
applyPowerup(powerup.type);
explode(powerup.mesh.position, powerup.mesh.material.color, 10);
scene.remove(powerup.mesh);
powerups.splice(i, 1);
}
}
}
function takeDamage() {
if (playerInvincible) return; // Cannot take damage if invincible
lives--;
playerInvincible = true;
playerInvincibleEndTime = performance.now() + INVINCIBILITY_TIME;
showMessage('HIT!');
updateUI();
if (lives <= 0) {
endGame();
}
}
function applyPowerup(type) {
let message = '';
switch (type) {
case 'fireRate':
playerFireRateModifier *= 0.9; // 10% faster
if (playerFireRateModifier < 0.2) playerFireRateModifier = 0.2; // Cap max speed
message = 'FIRE RATE UP!';
break;
case 'bouncyFire':
playerBulletBounces++;
if (playerBulletBounces > 3) playerBulletBounces = 3; // Cap bounces
message = 'BOUNCY SHOTS!';
break;
case 'moreGun':
playerGunLevel++;
if (playerGunLevel > 3) playerGunLevel = 3; // Cap gun level
message = 'MORE GUNS!';
break;
case 'extraLife':
lives++;
message = 'EXTRA LIFE!';
break;
}
showMessage(message);
updateUI();
}
function endGame() {
gameOver = true;
showMessage('GAME OVER!', false); // Don't fade out
showRestartButton();
litechat.utils.toast('info', `Game Over! Final Score: ${score}`);
}
function resetGame() {
// Clear all game objects
playerBullets.forEach(b => scene.remove(b.mesh));
enemies.forEach(e => scene.remove(e.mesh));
powerups.forEach(p => scene.remove(p.mesh));
particles.forEach(p => scene.remove(p.mesh));
playerBullets.length = 0;
enemies.length = 0;
enemyBullets.length = 0; // If implemented
powerups.length = 0;
particles.length = 0;
// Reset game state
score = 0;
lives = 3;
gameOver = false;
lastEnemySpawnTime = 0;
lastPlayerFireTime = 0;
playerFireRateModifier = 1.0;
playerBulletBounces = 0;
playerGunLevel = 1;
playerInvincible = false;
playerInvincibleEndTime = 0;
playerHitFlashTimer = 0;
player.mesh.position.set(0, 0, -GAME_HEIGHT / 2 + 2); // Reset player position
player.mesh.visible = true; // Ensure player is visible
hideRestartButton();
messageDisplay.classList.remove('opacity-100'); // Hide "GAME OVER"
messageDisplay.classList.add('opacity-0');
updateUI();
showMessage('GET READY!', true);
}
// --- Event Listeners ---
const mouse = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // XZ plane at Y=0
// Handle mouse movement for player
canvas.addEventListener('mousemove', (event) => {
if (gameOver) return;
// Calculate mouse position in normalized device coordinates (-1 to +1)
mouse.x = (event.offsetX / canvas.width) * 2 - 1;
mouse.y = -(event.offsetY / canvas.height) * 2 + 1;
// Update the raycaster with the camera and mouse position
raycaster.setFromCamera(mouse, camera);
// Intersect the raycaster with the game plane
const intersectionPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersectionPoint);
// Clamp player position within game bounds
player.mesh.position.x = Math.max(-GAME_WIDTH / 2 + player.mesh.geometry.parameters.radiusTop, Math.min(GAME_WIDTH / 2 - player.mesh.geometry.parameters.radiusTop, intersectionPoint.x));
player.mesh.position.z = Math.max(-GAME_HEIGHT / 2 + 2, Math.min(GAME_HEIGHT / 2 - 2, intersectionPoint.z)); // Keep player in lower half
});
// Handle mouse click for firing
canvas.addEventListener('mousedown', (event) => {
if (gameOver) return;
if (event.button === 0) { // Left click
handlePlayerFire(performance.now());
}
});
// Prevent context menu on right click
canvas.addEventListener('contextmenu', (event) => {
event.preventDefault();
});
// --- Animation Loop ---
let lastFrameTime = performance.now();
function animate(currentTime) {
requestAnimationFrame(animate);
const deltaTime = (currentTime - lastFrameTime); // Milliseconds
lastFrameTime = currentTime;
if (gameOver) {
// Keep rendering but no game logic updates
renderer.render(scene, camera);
return;
}
// Update player invincibility
if (playerInvincible && currentTime > playerInvincibleEndTime) {
playerInvincible = false;
player.mesh.visible = true;
}
// Background scrolling
backgroundMaterial.map.offset.y -= BACKGROUND_SCROLL_SPEED * (deltaTime / 1000); // Adjust speed with deltaTime
if (backgroundMaterial.map.offset.y < -1) { // Reset offset to create seamless loop
backgroundMaterial.map.offset.y = 0;
}
// Player Bullet movement
for (let i = playerBullets.length - 1; i >= 0; i--) {
const bullet = playerBullets[i];
bullet.mesh.translateZ(PLAYER_BULLET_SPEED); // Move forward
if (bullet.mesh.position.z > GAME_HEIGHT / 2 ||
bullet.mesh.position.x < -GAME_WIDTH / 2 ||
bullet.mesh.position.x > GAME_WIDTH / 2) { // Out of bounds
scene.remove(bullet.mesh);
playerBullets.splice(i, 1);
}
}
// Enemy Spawning
if (currentTime - lastEnemySpawnTime > ENEMY_SPAWN_INTERVAL * (0.8 + Math.random() * 0.4)) { // Randomize slightly
let enemyType = 'basic';
if (score > 1000 && Math.random() < 0.3) enemyType = 'fast';
if (score > 3000 && Math.random() < 0.2) enemyType = 'strong';
enemies.push(createEnemy(enemyType));
lastEnemySpawnTime = currentTime;
}
// Enemy Movement
for (let i = enemies.length - 1; i >= 0; i--) {
const enemy = enemies[i];
const speed = ENEMY_SPEED_BASE + (score / 5000) * 0.01; // Enemies get faster with score
enemy.mesh.position.z -= speed; // Move down
if (enemy.mesh.position.z < -GAME_HEIGHT / 2 - 1) { // Off screen
scene.remove(enemy.mesh);
enemies.splice(i, 1);
// Optionally, penalize player for letting enemy escape
}
}
// Powerup Movement
for (let i = powerups.length - 1; i >= 0; i--) {
const powerup = powerups[i];
powerup.mesh.position.z -= 0.05; // Move down slower than enemies
powerup.mesh.rotation.y += 0.02; // Rotate for visual
if (powerup.mesh.position.z < -GAME_HEIGHT / 2 - 1) { // Off screen
scene.remove(powerup.mesh);
powerups.splice(i, 1);
}
}
// Particle Updates
for (let i = particles.length - 1; i >= 0; i--) {
const particle = particles[i];
particle.mesh.position.add(particle.velocity);
particle.mesh.material.opacity -= 0.015; // Fade out
particle.life--;
if (particle.life <= 0 || particle.mesh.material.opacity <= 0) {
scene.remove(particle.mesh);
particles.splice(i, 1);
}
}
checkCollisions();
updateUI();
renderer.render(scene, camera);
}
// Start the game!
animate(performance.now());
showMessage('Welcome to LiteShooter!', true);
} catch (error) {
litechat.utils.log('error', '❌ FAILED to create game:', error.message);
const errorDiv = document.createElement('div');
errorDiv.className = 'p-4 text-red-600 bg-red-100 border border-red-200 rounded-md';
errorDiv.textContent = `Error loading game: ${error.message}. Check console for details.`;
litechat.target.appendChild(errorDiv);
}
}
createShooterGame();
```
### How to play:
1. **Run the code block.**
2. **Mouse Movement:** Move your mouse cursor within the game area (the black box with the gold border) to control your spaceship's horizontal and vertical position.
3. **Fire:** Click the left mouse button to fire your bullets.
4. **Dodge:** Avoid colliding with enemies.
5. **Collect Power-ups:** Pick up the shiny drops from destroyed enemies to enhance your abilities!
### Features implemented:
* **2D Shooter with 3D Effect:** Yes, the camera angle and object models give a 3D feel while gameplay is confined to a 2D plane.
* **Colorful & Poppy:** Bright green player, red/orange/purple enemies, vibrant bullets and power-ups, particle explosions.
* **Comfy Game Area:** Gold border, clear UI, intuitive controls.
* **Live Display:** Score and Lives are displayed at the top.
* **Random Bonus Drop:**
* **Faster Fire Rate (Gold Sphere):** Makes you shoot more rapidly.
* **Bouncy Fire (Cyan Torus):** Bullets can hit multiple enemies after a kill.
* **More Gun (Magenta Cone):** Upgrades your gun from single to dual, then triple shot.
* **Extra Life (Green Tetrahedron):** Grants an additional life.
* **Enemy Progression:** Enemies get slightly faster as your score increases.
### Potential Improvements and Next Steps:
* **Sound Effects:** Add audio for firing, explosions, power-up collection.
* **Enemy Variety:** More complex enemy movement patterns, enemies that shoot back.
* **Boss Fights:** Introduce an occasional, more challenging enemy.
* **Better Background:** A more detailed, seamless scrolling space background.
* **Player Model:** A more elaborate spaceship model.
* **Score Persistence:** Save high scores.
* **Pause Functionality:** Add a pause button.
* **Mobile Controls:** Touch controls for mobile devices.
* **Performance:** Optimize object pooling for bullets, enemies, and particles to reduce garbage collection.
Enjoy playing your 2D scroll shooter! Let me know if you'd like any modifications or additional features.