diff options
author | elioat <elioat@tilde.institute> | 2025-04-09 22:00:15 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2025-04-09 22:00:15 -0400 |
commit | 0b4e014792b6969a0627fcf8e651f236102027af (patch) | |
tree | 9e6087241b00f211d75192e1f2d4f6d820fb2aa3 /html/fps/game.js | |
parent | a375a8bb1084bef90967734c6ec0233fca306145 (diff) | |
download | tour-0b4e014792b6969a0627fcf8e651f236102027af.tar.gz |
*
Diffstat (limited to 'html/fps/game.js')
-rw-r--r-- | html/fps/game.js | 468 |
1 files changed, 468 insertions, 0 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(); |