diff options
-rw-r--r-- | html/fps/game.js | 468 | ||||
-rw-r--r-- | html/fps/index.html | 23 | ||||
-rw-r--r-- | html/space/game.js | 8 | ||||
-rw-r--r-- | html/space/gameState.js | 71 | ||||
-rw-r--r-- | html/space/index.html | 1 | ||||
-rw-r--r-- | html/space/input.js | 110 | ||||
-rw-r--r-- | html/space/physics.js | 279 | ||||
-rw-r--r-- | html/space/renderer.js | 202 |
8 files changed, 924 insertions, 238 deletions
diff --git a/html/fps/game.js b/html/fps/game.js new file mode 100644 index 0000000..0a18d79 --- /dev/null +++ b/html/fps/game.js @@ -0,0 +1,468 @@ +// Game state management +const GameState = { + player: { + x: 0, + y: 0, + angle: 0, + health: 100, + ammo: 10, + score: 0 + }, + level: { + width: 32, + height: 32, + map: [], + flag: { x: 0, y: 0 } + }, + enemies: [], + items: [], + isGameOver: false, + gun: { + recoil: 0, + lastShot: 0, + muzzleFlash: 0 + }, + shots: [] +}; + +// Level generation using a simple maze algorithm +const generateLevel = () => { + const map = Array(GameState.level.height).fill().map(() => + Array(GameState.level.width).fill(1) + ); + + // Create a larger starting room + const startRoomSize = 5; + for (let y = 1; y < startRoomSize; y++) { + for (let x = 1; x < startRoomSize; x++) { + map[y][x] = 0; + } + } + + // Simple maze generation using depth-first search + const carveMaze = (x, y) => { + map[y][x] = 0; + const directions = [ + [0, -2], [2, 0], [0, 2], [-2, 0] + ].sort(() => Math.random() - 0.5); + + for (const [dx, dy] of directions) { + const nx = x + dx; + const ny = y + dy; + if (nx > 0 && nx < GameState.level.width - 1 && + ny > 0 && ny < GameState.level.height - 1 && + map[ny][nx] === 1) { + map[y + dy/2][x + dx/2] = 0; + carveMaze(nx, ny); + } + } + }; + + // Start maze generation from the edge of the starting room + carveMaze(startRoomSize, startRoomSize); + + // Place flag in a random open space (not in starting room) + const openSpaces = []; + for (let y = 0; y < GameState.level.height; y++) { + for (let x = 0; x < GameState.level.width; x++) { + if (map[y][x] === 0 && (x >= startRoomSize || y >= startRoomSize)) { + openSpaces.push({x, y}); + } + } + } + const flagPos = openSpaces[Math.floor(Math.random() * openSpaces.length)]; + GameState.level.flag = flagPos; + + // Place enemies in open spaces (not in starting room) + GameState.enemies = []; + for (let i = 0; i < 5; i++) { + const pos = openSpaces[Math.floor(Math.random() * openSpaces.length)]; + GameState.enemies.push({ + x: pos.x, + y: pos.y, + health: Math.floor(Math.random() * 4) + 2 + }); + } + + // Place items in open spaces (not in starting room) + GameState.items = []; + for (let i = 0; i < 10; i++) { + const pos = openSpaces[Math.floor(Math.random() * openSpaces.length)]; + GameState.items.push({ + x: pos.x, + y: pos.y, + type: Math.random() > 0.5 ? 'ammo' : 'health' + }); + } + + GameState.level.map = map; + GameState.player.x = startRoomSize/2; + GameState.player.y = startRoomSize/2; + GameState.player.angle = 0; +}; + +// Player movement and controls +const handlePlayerMovement = (keys) => { + const moveSpeed = 0.1; + const rotateSpeed = 0.05; + + if (keys.w) { + GameState.player.x += Math.sin(GameState.player.angle) * moveSpeed; + GameState.player.y += Math.cos(GameState.player.angle) * moveSpeed; + } + if (keys.s) { + GameState.player.x -= Math.sin(GameState.player.angle) * moveSpeed; + GameState.player.y -= Math.cos(GameState.player.angle) * moveSpeed; + } + if (keys.a) { + GameState.player.x -= Math.sin(GameState.player.angle - Math.PI/2) * moveSpeed; + GameState.player.y -= Math.cos(GameState.player.angle - Math.PI/2) * moveSpeed; + } + if (keys.d) { + GameState.player.x += Math.sin(GameState.player.angle - Math.PI/2) * moveSpeed; + GameState.player.y += Math.cos(GameState.player.angle - Math.PI/2) * moveSpeed; + } +}; + +// Enemy AI +const updateEnemies = () => { + GameState.enemies = GameState.enemies.map(enemy => { + if (enemy.health <= 0) return null; + + const dx = GameState.player.x - enemy.x; + const dy = GameState.player.y - enemy.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 0.5) { + GameState.player.health -= 10; + if (GameState.player.health <= 0) { + GameState.isGameOver = true; + } + } else if (dist < 5) { + // Check if path to player is clear + const steps = 10; + let canMove = true; + for (let i = 1; i <= steps; i++) { + const testX = enemy.x + (dx / dist) * (i / steps); + const testY = enemy.y + (dy / dist) * (i / steps); + if (GameState.level.map[Math.floor(testY)][Math.floor(testX)] === 1) { + canMove = false; + break; + } + } + + if (canMove) { + enemy.x += dx / dist * 0.05; + enemy.y += dy / dist * 0.05; + } + } + + return enemy; + }).filter(Boolean); +}; + +// Collision detection +const checkCollisions = () => { + // Wall collisions with improved precision + const playerX = GameState.player.x; + const playerY = GameState.player.y; + const nextX = playerX + Math.sin(GameState.player.angle) * 0.1; + const nextY = playerY + Math.cos(GameState.player.angle) * 0.1; + + // Check all four corners of the player's collision box + const checkPoint = (x, y) => { + const gridX = Math.floor(x); + const gridY = Math.floor(y); + return GameState.level.map[gridY][gridX] === 1; + }; + + const collisionBox = 0.2; // Player collision box size + + if (checkPoint(playerX + collisionBox, playerY + collisionBox) || + checkPoint(playerX + collisionBox, playerY - collisionBox) || + checkPoint(playerX - collisionBox, playerY + collisionBox) || + checkPoint(playerX - collisionBox, playerY - collisionBox)) { + // Push player back to last valid position + GameState.player.x = Math.floor(GameState.player.x) + 0.5; + GameState.player.y = Math.floor(GameState.player.y) + 0.5; + } + + // Item collection + GameState.items = GameState.items.filter(item => { + const dx = GameState.player.x - item.x; + const dy = GameState.player.y - item.y; + if (Math.sqrt(dx * dx + dy * dy) < 0.5) { + if (item.type === 'ammo') { + GameState.player.ammo += 5; + } else { + GameState.player.health = Math.min(100, GameState.player.health + 25); + } + return false; + } + return true; + }); + + // Flag collection + const dx = GameState.player.x - GameState.level.flag.x; + const dy = GameState.player.y - GameState.level.flag.y; + if (Math.sqrt(dx * dx + dy * dy) < 0.5) { + GameState.player.score++; + generateLevel(); + } +}; + +// Rendering system +const render = (ctx) => { + const canvas = ctx.canvas; + const width = canvas.width; + const height = canvas.height; + + // Clear screen + ctx.fillStyle = '#000'; + ctx.fillRect(0, 0, width, height); + + // Draw ceiling + ctx.fillStyle = '#1a1a4a'; + ctx.fillRect(0, 0, width, height/2); + + // Draw floor + ctx.fillStyle = '#4a2a00'; + ctx.fillRect(0, height/2, width, height/2); + + // Draw walls and enemies using ray casting + const fov = Math.PI / 3; + const numRays = width; + const rayResults = []; + + for (let i = 0; i < numRays; i++) { + const rayAngle = GameState.player.angle - fov/2 + fov * i / numRays; + let distance = 0; + let hit = false; + let hitEnemy = null; + + while (!hit && distance < 20) { + distance += 0.1; + const testX = GameState.player.x + Math.sin(rayAngle) * distance; + const testY = GameState.player.y + Math.cos(rayAngle) * distance; + + // Check for wall hits + if (GameState.level.map[Math.floor(testY)][Math.floor(testX)] === 1) { + hit = true; + } + + // Check for enemy hits + if (!hit) { + hitEnemy = GameState.enemies.find(enemy => { + const dx = testX - enemy.x; + const dy = testY - enemy.y; + return Math.sqrt(dx * dx + dy * dy) < 0.5; + }); + if (hitEnemy) { + hit = true; + } + } + } + + rayResults.push({ + distance, + hitEnemy, + angle: rayAngle + }); + + const wallHeight = height / (distance * Math.cos(rayAngle - GameState.player.angle)); + const brightness = Math.max(0, 1 - distance / 20); + + if (hitEnemy) { + // Draw enemy + const enemyColor = Math.floor(brightness * 255); + ctx.fillStyle = `rgb(${enemyColor}, 0, 0)`; + ctx.fillRect(i, height/2 - wallHeight/2, 1, wallHeight); + } else { + // Draw wall + const wallColor = Math.floor(brightness * 100); + ctx.fillStyle = `rgb(0, ${wallColor}, 0)`; + ctx.fillRect(i, height/2 - wallHeight/2, 1, wallHeight); + } + } + + // Draw shots + const now = Date.now(); + GameState.shots = GameState.shots.filter(shot => { + const age = now - shot.time; + if (age > 200) return false; // Remove shots older than 200ms + + const progress = age / 200; + const distance = progress * 20; + const x = GameState.player.x + Math.sin(shot.angle) * distance; + const y = GameState.player.y + Math.cos(shot.angle) * distance; + + // Convert world position to screen position + const dx = x - GameState.player.x; + const dy = y - GameState.player.y; + const angle = Math.atan2(dx, dy) - GameState.player.angle; + const screenX = (angle / fov + 0.5) * width; + + if (screenX >= 0 && screenX < width) { + ctx.fillStyle = '#ffff00'; + ctx.fillRect(screenX, height/2, 2, 2); + } + + return true; + }); + + // Draw gun + const gunY = height - 150; + const gunX = width/2; + const recoilOffset = Math.sin(GameState.gun.recoil * Math.PI) * 50; // Increased recoil range + + // Only draw gun if it's not in full recoil + if (GameState.gun.recoil < 0.8) { + // Gun body + ctx.fillStyle = '#333'; + ctx.fillRect(gunX - 30, gunY + recoilOffset, 60, 90); + + // Gun barrel + ctx.fillStyle = '#222'; + ctx.fillRect(gunX - 8, gunY - 30 + recoilOffset, 16, 60); + + // Muzzle flash + if (GameState.gun.muzzleFlash > 0) { + const flashSize = GameState.gun.muzzleFlash * 30; + ctx.fillStyle = `rgba(255, 255, 0, ${GameState.gun.muzzleFlash})`; + ctx.beginPath(); + ctx.arc(gunX, gunY - 30 + recoilOffset, flashSize, 0, Math.PI * 2); + ctx.fill(); + } + } + + // Draw crosshair + ctx.fillStyle = '#fff'; + ctx.fillRect(width/2 - 5, height/2, 10, 1); + ctx.fillRect(width/2, height/2 - 5, 1, 10); + + // Draw HUD - only canvas-based + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(10, 10, 200, 100); + + ctx.fillStyle = '#fff'; + ctx.font = '24px monospace'; + ctx.fillText(`Health: ${GameState.player.health}`, 20, 45); + ctx.fillText(`Ammo: ${GameState.player.ammo}`, 20, 80); + ctx.fillText(`Score: ${GameState.player.score}`, 20, 115); + + // Draw enemy health bars + GameState.enemies.forEach(enemy => { + const dx = enemy.x - GameState.player.x; + const dy = enemy.y - GameState.player.y; + const angle = Math.atan2(dx, dy) - GameState.player.angle; + const screenX = (angle / fov + 0.5) * width; + + if (screenX >= 0 && screenX < width) { + const distance = Math.sqrt(dx * dx + dy * dy); + const height = canvas.height / (distance * Math.cos(angle)); + const y = canvas.height/2 - height/2; + + // Health bar background + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(screenX - 10, y - 20, 20, 5); + + // Health bar + const healthPercent = enemy.health / 5; // Assuming max health is 5 + ctx.fillStyle = `rgb(${255 * (1 - healthPercent)}, ${255 * healthPercent}, 0)`; + ctx.fillRect(screenX - 10, y - 20, 20 * healthPercent, 5); + } + }); +}; + +// Game loop +const gameLoop = (ctx) => { + if (GameState.isGameOver) { + ctx.fillStyle = '#fff'; + ctx.font = '48px monospace'; + ctx.fillText('GAME OVER', ctx.canvas.width/2 - 100, ctx.canvas.height/2); + return; + } + + // Update gun recoil and muzzle flash + if (GameState.gun.recoil > 0) { + GameState.gun.recoil -= 0.1; + } + if (GameState.gun.muzzleFlash > 0) { + GameState.gun.muzzleFlash -= 0.2; + } + + handlePlayerMovement(keys); + updateEnemies(); + checkCollisions(); + render(ctx); + + requestAnimationFrame(() => gameLoop(ctx)); +}; + +// Input handling +const keys = { w: false, a: false, s: false, d: false }; +document.addEventListener('keydown', e => { + if (e.key.toLowerCase() in keys) keys[e.key.toLowerCase()] = true; +}); +document.addEventListener('keyup', e => { + if (e.key.toLowerCase() in keys) keys[e.key.toLowerCase()] = false; +}); + +document.addEventListener('mousemove', e => { + GameState.player.angle += e.movementX * 0.01; +}); + +document.addEventListener('click', () => { + if (GameState.player.ammo > 0) { + GameState.player.ammo--; + GameState.gun.recoil = 1; + GameState.gun.muzzleFlash = 1; + GameState.shots.push({ + time: Date.now(), + angle: GameState.player.angle + }); + + // Check for enemy hits with wall collision + const fov = Math.PI / 3; + const rayAngle = GameState.player.angle; + let distance = 0; + let hitEnemy = null; + let hitWall = false; + + while (!hitEnemy && !hitWall && distance < 20) { + distance += 0.1; + const testX = GameState.player.x + Math.sin(rayAngle) * distance; + const testY = GameState.player.y + Math.cos(rayAngle) * distance; + + // Check for wall hit first + if (GameState.level.map[Math.floor(testY)][Math.floor(testX)] === 1) { + hitWall = true; + break; + } + + // Then check for enemy hit + hitEnemy = GameState.enemies.find(enemy => { + const dx = testX - enemy.x; + const dy = testY - enemy.y; + return Math.sqrt(dx * dx + dy * dy) < 0.5; + }); + } + + if (hitEnemy) { + hitEnemy.health--; + } + } +}); + +// Initialize game +const init = () => { + const canvas = document.getElementById('gameCanvas'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + const ctx = canvas.getContext('2d'); + + generateLevel(); + gameLoop(ctx); +}; + +init(); diff --git a/html/fps/index.html b/html/fps/index.html new file mode 100644 index 0000000..a025fae --- /dev/null +++ b/html/fps/index.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Procedural FPS</title> + <style> + body { + margin: 0; + padding: 0; + overflow: hidden; + background-color: #000; + } + canvas { + display: block; + } + </style> +</head> +<body> + <canvas id="gameCanvas"></canvas> + <script type="module" src="game.js"></script> +</body> +</html> diff --git a/html/space/game.js b/html/space/game.js index 0a106cf..ecd7abc 100644 --- a/html/space/game.js +++ b/html/space/game.js @@ -10,10 +10,12 @@ let isRunning = true; // Initialize all systems function init() { + console.log('Initializing game...'); initRenderer(); initInput(); initPhysics(); initGameState(); + console.log('Game initialized'); } // Main game loop using requestAnimationFrame @@ -29,9 +31,15 @@ function gameLoop(timestamp) { updateGameState(deltaTime); render(); + // Debug output + if (Math.random() < 0.01) { // Only log occasionally to avoid spam + console.log('Game loop running, deltaTime:', deltaTime); + } + requestAnimationFrame(gameLoop); } // Start the game +console.log('Starting game...'); init(); requestAnimationFrame(gameLoop); \ No newline at end of file diff --git a/html/space/gameState.js b/html/space/gameState.js index 85f56b0..620bc1f 100644 --- a/html/space/gameState.js +++ b/html/space/gameState.js @@ -6,11 +6,14 @@ import { getPlayerState } from './physics.js'; const planets = []; const enemyShips = []; const projectiles = []; +let lastEnemySpawn = 0; // Space dimensions const SPACE_SIZE = 10000; // Increased from implicit 2000 const PLANET_DISTANCE = 5000; // Increased from 1000 const ENEMY_SPAWN_DISTANCE = 3000; // Increased from 500 +const ENEMY_SPAWN_INTERVAL = 5000; // 5 seconds +const MAX_ENEMIES = 5; // Initialize game state export function initGameState() { @@ -27,6 +30,11 @@ export function initGameState() { color: '#e74c3c' }); + // Reset other state + enemyShips.length = 0; + projectiles.length = 0; + lastEnemySpawn = Date.now(); + // Create initial enemy ships for (let i = 0; i < 5; i++) { createEnemyShip(); @@ -56,8 +64,28 @@ function createEnemyShip() { // Update game state export function updateGameState(deltaTime) { + const currentTime = Date.now(); const player = getPlayerState(); + + // Spawn enemies + if (currentTime - lastEnemySpawn > ENEMY_SPAWN_INTERVAL && + enemyShips.length < MAX_ENEMIES) { + spawnEnemy(); + lastEnemySpawn = currentTime; + } + + // Update projectiles + projectiles.forEach((projectile, index) => { + projectile.position.x += projectile.velocity.x * deltaTime; + projectile.position.y += projectile.velocity.y * deltaTime; + projectile.position.z += projectile.velocity.z * deltaTime; + // Remove if too old + if (currentTime - projectile.createdAt > 5000) { + projectiles.splice(index, 1); + } + }); + // Update enemy ships enemyShips.forEach((ship, index) => { // Move ships @@ -85,28 +113,10 @@ export function updateGameState(deltaTime) { if (inputState.fireSecondary) { createProjectile('secondary'); } - - // Update projectiles - projectiles.forEach((projectile, index) => { - projectile.position.x += projectile.velocity.x * deltaTime; - projectile.position.y += projectile.velocity.y * deltaTime; - projectile.position.z += projectile.velocity.z * deltaTime; - - // Remove projectiles that are too far away - const distance = Math.sqrt( - Math.pow(projectile.position.x - player.position.x, 2) + - Math.pow(projectile.position.y - player.position.y, 2) + - Math.pow(projectile.position.z - player.position.z, 2) - ); - - if (distance > SPACE_SIZE/2) { - projectiles.splice(index, 1); - } - }); } // Create a new projectile -function createProjectile(type) { +export function createProjectile(type) { const player = getPlayerState(); const speed = type === 'primary' ? 10 : 7.5; // Reduced from 20/15 const damage = type === 'primary' ? 25 : 10; @@ -124,7 +134,28 @@ function createProjectile(type) { y: sinX * speed, z: cosY * cosX * speed }, - damage + damage, + createdAt: Date.now() + }); +} + +// Spawn a new enemy ship +function spawnEnemy() { + const angle = Math.random() * Math.PI * 2; + const distance = ENEMY_SPAWN_DISTANCE; + + enemyShips.push({ + position: { + x: Math.cos(angle) * distance, + y: 0, + z: Math.sin(angle) * distance + }, + velocity: { + x: (Math.random() - 0.5) * 0.5, + y: (Math.random() - 0.5) * 0.5, + z: (Math.random() - 0.5) * 0.5 + }, + health: 100 }); } diff --git a/html/space/index.html b/html/space/index.html index 52dffa0..9db977d 100644 --- a/html/space/index.html +++ b/html/space/index.html @@ -16,6 +16,7 @@ </style> </head> <body> + <canvas id="gameCanvas"></canvas> <script type="module" src="game.js"></script> </body> </html> \ No newline at end of file diff --git a/html/space/input.js b/html/space/input.js index dadecb9..19ea56c 100644 --- a/html/space/input.js +++ b/html/space/input.js @@ -1,16 +1,9 @@ -// Input module using keyboard controls -const keys = { - w: false, - a: false, - s: false, - d: false, - ArrowUp: false, - ArrowDown: false, - ArrowLeft: false, - ArrowRight: false, - ' ': false, // space - e: false -}; +// Input handling module +import { updatePlayerControls } from './physics.js'; + +let keys = {}; +let mouseX = 0; +let mouseY = 0; // Input state that other modules can read export const inputState = { @@ -24,48 +17,67 @@ export const inputState = { // Initialize input handlers export function initInput() { - window.addEventListener('keydown', handleKeyDown); - window.addEventListener('keyup', handleKeyUp); -} + // Keyboard event listeners + document.addEventListener('keydown', (e) => { + keys[e.key.toLowerCase()] = true; + }); -// Update input state based on current key presses -export function updateInput() { - // Reset input state - inputState.thrust = 0; - inputState.strafe = 0; - inputState.yaw = 0; - inputState.pitch = 0; - inputState.firePrimary = false; - inputState.fireSecondary = false; + document.addEventListener('keyup', (e) => { + keys[e.key.toLowerCase()] = false; + }); - // Thrust controls (W/S) - if (keys.w) inputState.thrust = 1; // Forward thrust - if (keys.s) inputState.thrust = -1; // Backward thrust + // Mouse movement for heading + document.addEventListener('mousemove', (e) => { + // Calculate mouse position relative to center of canvas + const canvas = document.querySelector('canvas'); + const rect = canvas.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + mouseX = (e.clientX - centerX) / (rect.width / 2); + mouseY = (e.clientY - centerY) / (rect.height / 2); + }); - // Strafe controls (A/D) - if (keys.a) inputState.strafe = -1; // Left strafe - if (keys.d) inputState.strafe = 1; // Right strafe + // Mouse click for primary weapon + document.addEventListener('mousedown', (e) => { + if (e.button === 0) { // Left click + keys['fire'] = true; + } + }); - // Rotation controls (Arrow keys) - if (keys.ArrowLeft) inputState.yaw = -1; - if (keys.ArrowRight) inputState.yaw = 1; - if (keys.ArrowUp) inputState.pitch = -1; - if (keys.ArrowDown) inputState.pitch = 1; + document.addEventListener('mouseup', (e) => { + if (e.button === 0) { // Left click + keys['fire'] = false; + } + }); - // Weapons - if (keys[' ']) inputState.firePrimary = true; - if (keys.e) inputState.fireSecondary = true; -} + // E key for secondary weapon + document.addEventListener('keydown', (e) => { + if (e.key.toLowerCase() === 'e') { + keys['secondary'] = true; + } + }); -// Event handlers -function handleKeyDown(e) { - if (keys.hasOwnProperty(e.key)) { - keys[e.key] = true; - } + document.addEventListener('keyup', (e) => { + if (e.key.toLowerCase() === 'e') { + keys['secondary'] = false; + } + }); } -function handleKeyUp(e) { - if (keys.hasOwnProperty(e.key)) { - keys[e.key] = false; - } +// Update controls based on current input state +export function updateInput() { + const controls = { + thrust: keys[' '] || false, // Space bar for thrust + up: keys['w'] || false, // W for upward strafe + down: keys['s'] || false, // S for downward strafe + left: keys['a'] || false, // A for left strafe + right: keys['d'] || false, // D for right strafe + fire: keys['fire'] || false, + secondary: keys['secondary'] || false, + mouseX, + mouseY + }; + + updatePlayerControls(controls); } \ No newline at end of file diff --git a/html/space/physics.js b/html/space/physics.js index 6b71601..d4dfe55 100644 --- a/html/space/physics.js +++ b/html/space/physics.js @@ -1,5 +1,6 @@ // Physics module for handling movement and collisions import { inputState } from './input.js'; +import { createProjectile } from './gameState.js'; // Constants const MAX_THRUST = 0.5; // Reduced from 2 @@ -8,113 +9,245 @@ const DECELERATION = 0.001; // Reduced from 0.01 const BASE_ROTATION_SPEED = 0.001; // Reduced from 0.005 const ROTATION_ACCELERATION = 0.0005; // Reduced from 0.002 const ROTATION_DECELERATION = 0.0002; // Reduced from 0.001 +const MOUSE_SENSITIVITY = 0.03; // Increased from 0.01 for sharper turns +const MAX_SPEED = 1.0; // Maximum speed in any direction + +// Weapon constants +export const PRIMARY_COOLDOWN = 100; // ms between primary shots +export const SECONDARY_COOLDOWN = 2000; // ms between secondary shots +export const PRIMARY_BURST_COUNT = 3; // Number of shots in primary burst +export const PRIMARY_BURST_DELAY = 50; // ms between burst shots // Player state -const player = { +let playerState = { position: { x: 0, y: 0, z: 0 }, velocity: { x: 0, y: 0, z: 0 }, - rotation: { x: 0, y: 0, z: 0 }, - rotationSpeed: { x: 0, y: 0 } // Added for smooth rotation + rotation: { x: 0, y: 0 }, + thrust: 0, + strafe: 0, + weapons: { + primary: { + lastFired: 0, + burstCount: 0, + burstTimer: 0 + }, + secondary: { + lastFired: 0 + } + } }; // Initialize physics export function initPhysics() { // Reset player state - player.position = { x: 0, y: 0, z: 0 }; - player.velocity = { x: 0, y: 0, z: 0 }; - player.rotation = { x: 0, y: 0, z: 0 }; - player.rotationSpeed = { x: 0, y: 0 }; + playerState.position = { x: 0, y: 0, z: 0 }; + playerState.velocity = { x: 0, y: 0, z: 0 }; + playerState.rotation = { x: 0, y: 0 }; + playerState.thrust = 0; + playerState.strafe = 0; } -// Helper function to apply force in a direction -function applyForce(direction, magnitude, deltaTime) { - const force = { - x: direction.x * magnitude * deltaTime, - y: direction.y * magnitude * deltaTime, - z: direction.z * magnitude * deltaTime - }; - - player.velocity.x += force.x; - player.velocity.y += force.y; - player.velocity.z += force.z; +// Helper function to limit speed in a direction +function limitSpeed(velocity, maxSpeed) { + const speed = Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z); + if (speed > maxSpeed) { + const scale = maxSpeed / speed; + velocity.x *= scale; + velocity.y *= scale; + velocity.z *= scale; + } } -// Update physics -export function updatePhysics(deltaTime) { - // Update rotation speed based on input - if (inputState.yaw !== 0) { - player.rotationSpeed.y += inputState.yaw * ROTATION_ACCELERATION * deltaTime; +// Update player controls +export function updatePlayerControls(controls) { + // Handle thrust (space bar) + if (controls.thrust) { + playerState.thrust = Math.min(playerState.thrust + THRUST_ACCELERATION, MAX_THRUST); } else { - // Smoothly decelerate rotation - player.rotationSpeed.y *= (1 - ROTATION_DECELERATION * deltaTime); + // Apply deceleration when no thrust input + if (playerState.thrust > 0) { + playerState.thrust = Math.max(playerState.thrust - DECELERATION, 0); + } } - if (inputState.pitch !== 0) { - player.rotationSpeed.x += inputState.pitch * ROTATION_ACCELERATION * deltaTime; + // Handle vertical strafing (W/S) + if (controls.up) { + playerState.verticalStrafe = Math.min(playerState.verticalStrafe + THRUST_ACCELERATION, MAX_THRUST); + } else if (controls.down) { + playerState.verticalStrafe = Math.max(playerState.verticalStrafe - THRUST_ACCELERATION, -MAX_THRUST); } else { - // Smoothly decelerate rotation - player.rotationSpeed.x *= (1 - ROTATION_DECELERATION * deltaTime); + // Apply deceleration when no vertical strafe input + if (playerState.verticalStrafe > 0) { + playerState.verticalStrafe = Math.max(playerState.verticalStrafe - DECELERATION, 0); + } else if (playerState.verticalStrafe < 0) { + playerState.verticalStrafe = Math.min(playerState.verticalStrafe + DECELERATION, 0); + } + } + + // Handle horizontal strafing (A/D) + if (controls.left) { + playerState.horizontalStrafe = Math.min(playerState.horizontalStrafe + THRUST_ACCELERATION, MAX_THRUST); + } else if (controls.right) { + playerState.horizontalStrafe = Math.max(playerState.horizontalStrafe - THRUST_ACCELERATION, -MAX_THRUST); + } else { + // Apply deceleration when no horizontal strafe input + if (playerState.horizontalStrafe > 0) { + playerState.horizontalStrafe = Math.max(playerState.horizontalStrafe - DECELERATION, 0); + } else if (playerState.horizontalStrafe < 0) { + playerState.horizontalStrafe = Math.min(playerState.horizontalStrafe + DECELERATION, 0); + } + } + + // Handle mouse-based rotation with smoothing + const targetRotationY = controls.mouseX * MOUSE_SENSITIVITY; + const targetRotationX = controls.mouseY * MOUSE_SENSITIVITY; + + // Smooth rotation using lerp with faster response + playerState.rotation.y += (targetRotationY - playerState.rotation.y) * 0.2; + playerState.rotation.x += (targetRotationX - playerState.rotation.x) * 0.2; + + // Clamp pitch rotation + playerState.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, playerState.rotation.x)); + + // Handle weapons with cooldowns + const currentTime = Date.now(); + + // Primary weapon (burst fire) + if (controls.fire) { + const primary = playerState.weapons.primary; + if (currentTime - primary.lastFired >= PRIMARY_COOLDOWN && primary.burstCount === 0) { + primary.burstCount = PRIMARY_BURST_COUNT; + primary.burstTimer = currentTime; + firePrimaryWeapon(); + primary.lastFired = currentTime; + } } - // Apply rotation based on current rotation speed - player.rotation.y += player.rotationSpeed.y * BASE_ROTATION_SPEED * deltaTime; - player.rotation.x += player.rotationSpeed.x * BASE_ROTATION_SPEED * deltaTime; + // Secondary weapon (single shot with cooldown) + if (controls.secondary && currentTime - playerState.weapons.secondary.lastFired >= SECONDARY_COOLDOWN) { + fireSecondaryWeapon(); + playerState.weapons.secondary.lastFired = currentTime; + } - // Calculate forward and right vectors based on current rotation - const cosY = Math.cos(player.rotation.y); - const sinY = Math.sin(player.rotation.y); - const cosX = Math.cos(player.rotation.x); - const sinX = Math.sin(player.rotation.x); + // Handle burst fire timing + const primary = playerState.weapons.primary; + if (primary.burstCount > 0 && currentTime - primary.burstTimer >= PRIMARY_BURST_DELAY) { + firePrimaryWeapon(); + primary.burstCount--; + primary.burstTimer = currentTime; + } +} - // Forward vector (in the direction the ship is facing) +// Update physics +export function updatePhysics(deltaTime) { + // Calculate forward and right vectors based on rotation const forward = { - x: sinY * cosX, - y: sinX, - z: cosY * cosX + x: Math.sin(playerState.rotation.y) * Math.cos(playerState.rotation.x), + y: -Math.sin(playerState.rotation.x), + z: Math.cos(playerState.rotation.y) * Math.cos(playerState.rotation.x) }; - // Right vector (perpendicular to forward) const right = { - x: cosY, + x: Math.cos(playerState.rotation.y), y: 0, - z: -sinY + z: -Math.sin(playerState.rotation.y) }; - // Apply thrust in the direction the ship is facing - if (inputState.thrust !== 0) { - applyForce(forward, inputState.thrust * THRUST_ACCELERATION, deltaTime); - } + const up = { x: 0, y: 1, z: 0 }; - // Apply strafing force - if (inputState.strafe !== 0) { - applyForce(right, inputState.strafe * THRUST_ACCELERATION, deltaTime); - } + // Apply thrust in forward direction + const thrustVelocity = { + x: forward.x * playerState.thrust * deltaTime, + y: forward.y * playerState.thrust * deltaTime, + z: forward.z * playerState.thrust * deltaTime + }; - // Apply damping to simulate space friction (very slight) - const currentSpeed = Math.sqrt( - player.velocity.x * player.velocity.x + - player.velocity.y * player.velocity.y + - player.velocity.z * player.velocity.z - ); - - if (currentSpeed > 0) { - const speedDamping = 1 - (DECELERATION * deltaTime); - player.velocity.x *= speedDamping; - player.velocity.y *= speedDamping; - player.velocity.z *= speedDamping; - } + // Apply horizontal strafe + const horizontalStrafeVelocity = { + x: right.x * playerState.horizontalStrafe * deltaTime, + y: 0, + z: right.z * playerState.horizontalStrafe * deltaTime + }; + + // Apply vertical strafe + const verticalStrafeVelocity = { + x: 0, + y: up.y * playerState.verticalStrafe * deltaTime, + z: 0 + }; + + // Add velocities + playerState.velocity.x += thrustVelocity.x + horizontalStrafeVelocity.x + verticalStrafeVelocity.x; + playerState.velocity.y += thrustVelocity.y + horizontalStrafeVelocity.y + verticalStrafeVelocity.y; + playerState.velocity.z += thrustVelocity.z + horizontalStrafeVelocity.z + verticalStrafeVelocity.z; + + // Limit total speed + limitSpeed(playerState.velocity, MAX_SPEED); + + // Apply velocity to position + playerState.position.x += playerState.velocity.x * deltaTime; + playerState.position.y += playerState.velocity.y * deltaTime; + playerState.position.z += playerState.velocity.z * deltaTime; + + // Apply friction/drag + const drag = 0.99; + playerState.velocity.x *= drag; + playerState.velocity.y *= drag; + playerState.velocity.z *= drag; +} + +// Weapon firing +function firePrimaryWeapon() { + const forward = { + x: Math.sin(playerState.rotation.y) * Math.cos(playerState.rotation.x), + y: -Math.sin(playerState.rotation.x), + z: Math.cos(playerState.rotation.y) * Math.cos(playerState.rotation.x) + }; - // Update position - player.position.x += player.velocity.x * deltaTime; - player.position.y += player.velocity.y * deltaTime; - player.position.z += player.velocity.z * deltaTime; + createProjectile({ + position: { ...playerState.position }, + velocity: { + x: forward.x * 10 + playerState.velocity.x, + y: forward.y * 10 + playerState.velocity.y, + z: forward.z * 10 + playerState.velocity.z + }, + type: 'primary' + }); } -// Get player state for rendering +function fireSecondaryWeapon() { + const forward = { + x: Math.sin(playerState.rotation.y) * Math.cos(playerState.rotation.x), + y: -Math.sin(playerState.rotation.x), + z: Math.cos(playerState.rotation.y) * Math.cos(playerState.rotation.x) + }; + + createProjectile({ + position: { ...playerState.position }, + velocity: { + x: forward.x * 5 + playerState.velocity.x, + y: forward.y * 5 + playerState.velocity.y, + z: forward.z * 5 + playerState.velocity.z + }, + type: 'secondary' + }); +} + +// Get current player state export function getPlayerState() { + return playerState; +} + +// Get weapon cooldown states +export function getWeaponStates() { + const currentTime = Date.now(); return { - position: { ...player.position }, - rotation: { ...player.rotation }, - velocity: { ...player.velocity } + primary: { + cooldown: Math.max(0, PRIMARY_COOLDOWN - (currentTime - playerState.weapons.primary.lastFired)), + burstCount: playerState.weapons.primary.burstCount + }, + secondary: { + cooldown: Math.max(0, SECONDARY_COOLDOWN - (currentTime - playerState.weapons.secondary.lastFired)) + } }; } \ No newline at end of file diff --git a/html/space/renderer.js b/html/space/renderer.js index 6af271d..04646cf 100644 --- a/html/space/renderer.js +++ b/html/space/renderer.js @@ -1,14 +1,22 @@ // Renderer module using HTML5 Canvas -import { getPlayerState } from './physics.js'; +import { getPlayerState, getWeaponStates } from './physics.js'; import { getGameState } from './gameState.js'; +// Import weapon constants +import { + PRIMARY_COOLDOWN, + SECONDARY_COOLDOWN, + PRIMARY_BURST_COUNT, + PRIMARY_BURST_DELAY +} from './physics.js'; + let canvas; let ctx; let width; let height; // Star field -const stars = []; +let starfield = []; // Declare starfield array const NUM_STARS = 2000; // Increased from 1000 const STAR_FIELD_DEPTH = 20000; // Increased from 2000 @@ -24,60 +32,33 @@ const TARGET_LOCK_THRESHOLD = 20; // Pixels from center to consider locked // Initialize the renderer export function initRenderer() { - canvas = document.createElement('canvas'); - document.body.appendChild(canvas); + console.log('Initializing renderer...'); + canvas = document.getElementById('gameCanvas'); ctx = canvas.getContext('2d'); - // Make canvas fullscreen - function resize() { - width = window.innerWidth; - height = window.innerHeight; - canvas.width = width; - canvas.height = height; - } - - window.addEventListener('resize', resize); - resize(); + // Set canvas size + width = canvas.width = window.innerWidth; + height = canvas.height = window.innerHeight; - // Initialize star field - for (let i = 0; i < NUM_STARS; i++) { - stars.push({ - x: (Math.random() - 0.5) * width * 4, // Increased spread - y: (Math.random() - 0.5) * height * 4, // Increased spread - z: Math.random() * STAR_FIELD_DEPTH, - size: Math.random() * 2 - }); - } + // Initialize starfield + console.log('Creating starfield with', NUM_STARS, 'stars...'); + starfield = Array.from({ length: NUM_STARS }, () => ({ + x: (Math.random() - 0.5) * STAR_FIELD_DEPTH, + y: (Math.random() - 0.5) * STAR_FIELD_DEPTH, + z: Math.random() * STAR_FIELD_DEPTH, + size: Math.random() * 2 + 1 + })); + console.log('Starfield initialized'); } // Project 3D point to 2D screen coordinates -function projectPoint(point, player) { - // Calculate relative position to player - const relativeX = point.x - player.position.x; - const relativeY = point.y - player.position.y; - const relativeZ = point.z - player.position.z; - - // Apply player rotation - const cosY = Math.cos(player.rotation.y); - const sinY = Math.sin(player.rotation.y); - const cosX = Math.cos(player.rotation.x); - const sinX = Math.sin(player.rotation.x); - - // Rotate around Y axis (yaw) - let rotatedX = relativeX * cosY - relativeZ * sinY; - let rotatedZ = relativeX * sinY + relativeZ * cosY; - - // Rotate around X axis (pitch) - let rotatedY = relativeY * cosX - rotatedZ * sinX; - rotatedZ = relativeY * sinX + rotatedZ * cosX; - - // Project to screen - if (rotatedZ <= 0) return null; // Behind camera - - const scale = 1000 / rotatedZ; +function projectPoint(x, y, z) { + if (z <= 0) return null; // Behind camera + + const scale = 2000 / z; // Increased scale factor return { - x: width/2 + rotatedX * scale, - y: height/2 + rotatedY * scale, + x: width/2 + x * scale, + y: height/2 + y * scale, scale }; } @@ -88,7 +69,7 @@ function getTargetLock(player, gameState) { const centerY = height / 2; for (const ship of gameState.enemyShips) { - const projected = projectPoint(ship.position, player); + const projected = projectPoint(ship.x - player.x, ship.y - player.y, ship.z - player.z); if (projected) { const distance = Math.sqrt( Math.pow(projected.x - centerX, 2) + @@ -135,8 +116,8 @@ function drawRadar(player, gameState, targetLock) { // Draw planets gameState.planets.forEach(planet => { - const dx = (planet.position.x - player.position.x) * RADAR_SCALE; - const dy = (planet.position.z - player.position.z) * RADAR_SCALE; + const dx = (planet.x - player.x) * RADAR_SCALE; + const dy = (planet.z - player.z) * RADAR_SCALE; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < RADAR_RADIUS) { @@ -154,8 +135,8 @@ function drawRadar(player, gameState, targetLock) { // Draw enemy ships gameState.enemyShips.forEach(ship => { - const dx = (ship.position.x - player.position.x) * RADAR_SCALE; - const dy = (ship.position.z - player.position.z) * RADAR_SCALE; + const dx = (ship.x - player.x) * RADAR_SCALE; + const dy = (ship.z - player.z) * RADAR_SCALE; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < RADAR_RADIUS) { @@ -181,18 +162,18 @@ function drawSpeedIndicator(player, targetLock) { ctx.fillStyle = targetLock ? TARGET_LOCK_COLOR : HUD_COLOR; ctx.font = '14px monospace'; - // Calculate speed + // Calculate speed from velocity components const speed = Math.sqrt( - player.velocity.x * player.velocity.x + - player.velocity.y * player.velocity.y + - player.velocity.z * player.velocity.z + player.vx * player.vx + + player.vy * player.vy + + player.vz * player.vz ); // Draw speed ctx.fillText(`Speed: ${speed.toFixed(2)}`, 20, height - 40); - // Draw direction - const direction = Math.atan2(player.velocity.x, player.velocity.z); + // Draw direction (using x and z components for heading) + const direction = Math.atan2(player.vx, player.vz); ctx.fillText(`Heading: ${(direction * 180 / Math.PI).toFixed(1)}°`, 20, height - 20); ctx.restore(); @@ -255,6 +236,39 @@ function drawTargetingReticle(player, gameState) { ctx.restore(); } +// Draw weapon cooldown indicators +function drawWeaponCooldowns() { + const weaponStates = getWeaponStates(); + ctx.save(); + ctx.fillStyle = HUD_COLOR; + ctx.font = '14px monospace'; + + // Primary weapon cooldown (bottom left) + const primaryCooldown = weaponStates.primary.cooldown / PRIMARY_COOLDOWN; + ctx.fillText('Primary:', 20, height - 80); + ctx.fillStyle = `rgba(0, 255, 0, ${primaryCooldown})`; + ctx.fillRect(20, height - 70, 100, 10); + ctx.strokeStyle = HUD_COLOR; + ctx.strokeRect(20, height - 70, 100, 10); + + // Secondary weapon cooldown (bottom left, below primary) + const secondaryCooldown = weaponStates.secondary.cooldown / SECONDARY_COOLDOWN; + ctx.fillStyle = HUD_COLOR; + ctx.fillText('Secondary:', 20, height - 50); + ctx.fillStyle = `rgba(0, 255, 0, ${secondaryCooldown})`; + ctx.fillRect(20, height - 40, 100, 10); + ctx.strokeStyle = HUD_COLOR; + ctx.strokeRect(20, height - 40, 100, 10); + + // Draw burst indicator for primary weapon + if (weaponStates.primary.burstCount > 0) { + ctx.fillStyle = HUD_COLOR; + ctx.fillText(`Burst: ${weaponStates.primary.burstCount}`, 20, height - 20); + } + + ctx.restore(); +} + // Main render function export function render() { const player = getPlayerState(); @@ -262,49 +276,44 @@ export function render() { const targetLock = getTargetLock(player, gameState); // Clear canvas - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, width, height); + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); - // Draw star field - ctx.fillStyle = 'white'; - stars.forEach(star => { + // Draw starfield + let starsRendered = 0; + starfield.forEach(star => { // Calculate star position relative to player - const relativeX = star.x - player.position.x; - const relativeY = star.y - player.position.y; - const relativeZ = star.z - player.position.z; - + let relativeX = star.x - player.x; + let relativeY = star.y - player.y; + let relativeZ = star.z - player.z; + // Apply player rotation - const cosY = Math.cos(player.rotation.y); - const sinY = Math.sin(player.rotation.y); - const cosX = Math.cos(player.rotation.x); - const sinX = Math.sin(player.rotation.x); - - // Rotate around Y axis (yaw) - let rotatedX = relativeX * cosY - relativeZ * sinY; - let rotatedZ = relativeX * sinY + relativeZ * cosY; - - // Rotate around X axis (pitch) - let rotatedY = relativeY * cosX - rotatedZ * sinX; - rotatedZ = relativeY * sinX + rotatedZ * cosX; - - // Project to screen - if (rotatedZ <= 0) return; // Behind camera - - const scale = STAR_FIELD_DEPTH / rotatedZ; - const x = width/2 + rotatedX * scale; - const y = height/2 + rotatedY * scale; - const size = star.size * scale; + let rotatedX = relativeX * Math.cos(player.rotation) - relativeY * Math.sin(player.rotation); + let rotatedY = relativeX * Math.sin(player.rotation) + relativeY * Math.cos(player.rotation); + let rotatedZ = relativeZ; - if (x >= 0 && x <= width && y >= 0 && y <= height) { - ctx.beginPath(); - ctx.arc(x, y, size, 0, Math.PI * 2); - ctx.fill(); + // Project to screen coordinates + if (rotatedZ > 0) { + const projected = projectPoint(rotatedX, rotatedY, rotatedZ); + if (projected) { + const brightness = Math.min(1, 2000 / rotatedZ); + ctx.fillStyle = `rgba(255, 255, 255, ${brightness})`; + ctx.beginPath(); + ctx.arc(projected.x, projected.y, star.size * brightness, 0, Math.PI * 2); + ctx.fill(); + starsRendered++; + } } }); + + // Debug output + if (Math.random() < 0.01) { // Only log occasionally to avoid spam + console.log('Stars rendered:', starsRendered); + } // Draw planets gameState.planets.forEach(planet => { - const projected = projectPoint(planet.position, player); + const projected = projectPoint(planet.x - player.x, planet.y - player.y, planet.z - player.z); if (projected) { const radius = planet.radius * projected.scale; ctx.fillStyle = planet.color; @@ -316,7 +325,7 @@ export function render() { // Draw enemy ships gameState.enemyShips.forEach(ship => { - const projected = projectPoint(ship.position, player); + const projected = projectPoint(ship.x - player.x, ship.y - player.y, ship.z - player.z); if (projected) { const size = 20 * projected.scale; ctx.fillStyle = '#ff0000'; @@ -331,7 +340,7 @@ export function render() { // Draw projectiles gameState.projectiles.forEach(projectile => { - const projected = projectPoint(projectile.position, player); + const projected = projectPoint(projectile.x - player.x, projectile.y - player.y, projectile.z - player.z); if (projected) { const size = 3 * projected.scale; ctx.fillStyle = projectile.type === 'primary' ? '#ffff00' : '#00ffff'; @@ -345,4 +354,5 @@ export function render() { drawRadar(player, gameState, targetLock); drawSpeedIndicator(player, targetLock); drawTargetingReticle(player, gameState); + drawWeaponCooldowns(); } \ No newline at end of file |