diff options
author | elioat <elioat@tilde.institute> | 2025-04-09 22:47:53 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2025-04-09 22:47:53 -0400 |
commit | a96cbb4bdec31d07d53c618d4f8e824dddb06538 (patch) | |
tree | 28bc9cb7f71aa57b498c6eb78bda6235aaa50ddf /html/fps/game.js | |
parent | 6bab5e8daf7c09cc31882b0018c59ac0b9356c67 (diff) | |
download | tour-a96cbb4bdec31d07d53c618d4f8e824dddb06538.tar.gz |
*
Diffstat (limited to 'html/fps/game.js')
-rw-r--r-- | html/fps/game.js | 201 |
1 files changed, 149 insertions, 52 deletions
diff --git a/html/fps/game.js b/html/fps/game.js index bac8f9b..67cebc8 100644 --- a/html/fps/game.js +++ b/html/fps/game.js @@ -25,7 +25,9 @@ const GameState = { tilt: 0 // 0 = normal, 1 = tilted }, shots: [], - isStarted: false + isStarted: false, + gradients: {}, // Cache for wall gradients + lastGradientUpdate: 0 }; // Level generation using a simple maze algorithm @@ -329,19 +331,39 @@ const render = (ctx) => { // Draw walls and sprites using ray casting const fov = Math.PI / 3; - const numRays = width; + const numRays = Math.floor(width / 2); // Reduce ray count by half const rayResults = []; const sprites = []; - for (let i = 0; i < numRays; i++) { - const rayAngle = GameState.player.angle - fov/2 + fov * i / numRays; + // Pre-calculate common values + const halfHeight = height / 2; + const brightnessSteps = 20; // Number of brightness levels to cache + const gradientCache = {}; + + // Create cached gradients for different brightness levels + for (let b = 0; b <= brightnessSteps; b++) { + const brightness = b / brightnessSteps; + const baseColor = Math.floor(brightness * 100); + const distanceColor = Math.floor(brightness * 50); + + gradientCache[b] = { + top: `rgb(${distanceColor}, ${baseColor + 20}, ${distanceColor})`, + middle: `rgb(${distanceColor}, ${baseColor}, ${distanceColor})`, + bottom: `rgb(${distanceColor}, ${baseColor - 20}, ${distanceColor})`, + line: `rgba(0, ${baseColor - 30}, 0, 0.3)` + }; + } + + for (let i = 0; i < width; i += 2) { // Draw every other pixel + const rayIndex = Math.floor(i / 2); + const rayAngle = GameState.player.angle - fov/2 + fov * rayIndex / numRays; let distance = 0; let hit = false; let hitEnemy = null; let hitItem = null; while (!hit && distance < 20) { - distance += 0.1; + distance += 0.2; // Increase ray step size const testX = GameState.player.x + Math.sin(rayAngle) * distance; const testY = GameState.player.y + Math.cos(rayAngle) * distance; @@ -361,7 +383,7 @@ const render = (ctx) => { hit = true; sprites.push({ type: 'enemy', - x: i, + x: rayIndex, distance, angle: rayAngle, data: hitEnemy @@ -380,7 +402,7 @@ const render = (ctx) => { hit = true; sprites.push({ type: 'item', - x: i, + x: rayIndex, distance, angle: rayAngle, data: hitItem @@ -389,32 +411,48 @@ const render = (ctx) => { } } - rayResults.push({ + rayResults[rayIndex] = { distance, hitEnemy, angle: rayAngle - }); + }; const wallHeight = height / (distance * Math.cos(rayAngle - GameState.player.angle)); const brightness = Math.max(0, 1 - distance / 20); + const brightnessIndex = Math.floor(brightness * brightnessSteps); + const colors = gradientCache[brightnessIndex]; 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); + ctx.fillRect(i, halfHeight - wallHeight/2, 2, wallHeight); } else if (hitItem) { // Draw item const itemColor = hitItem.type === 'health' ? `rgb(0, ${Math.floor(brightness * 255)}, 0)` : `rgb(0, ${Math.floor(brightness * 255)}, ${Math.floor(brightness * 255)})`; ctx.fillStyle = itemColor; - ctx.fillRect(i, height/2 - wallHeight/2, 1, wallHeight); + ctx.fillRect(i, halfHeight - wallHeight/2, 2, 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); + const wallTop = halfHeight - wallHeight/2; + const wallBottom = halfHeight + wallHeight/2; + + // Draw wall with cached colors + ctx.fillStyle = colors.middle; + ctx.fillRect(i, wallTop, 2, wallHeight); // Draw 2 pixels wide + + // Draw top and bottom highlights + ctx.fillStyle = colors.top; + ctx.fillRect(i, wallTop, 2, 1); + ctx.fillStyle = colors.bottom; + ctx.fillRect(i, wallBottom - 1, 2, 1); + + // Draw vertical lines less frequently + if (i % 8 === 0) { + ctx.fillStyle = colors.line; + ctx.fillRect(i, wallTop, 2, wallHeight); + } } } @@ -437,7 +475,7 @@ const render = (ctx) => { if (screenX >= 0 && screenX < width) { ctx.fillStyle = '#ffff00'; - ctx.fillRect(screenX, height/2, 2, 2); + ctx.fillRect(screenX, halfHeight, 2, 2); } return true; @@ -447,7 +485,7 @@ const render = (ctx) => { const gunY = height - 150; const gunX = width/2; const recoilOffset = Math.sin(GameState.gun.recoil * Math.PI) * 50; - const tiltAngle = GameState.gun.tilt * Math.PI / 6; // 30 degrees max tilt + const tiltAngle = GameState.gun.tilt * Math.PI / 6; // Only draw gun if it's not in full recoil if (GameState.gun.recoil < 0.8) { @@ -455,21 +493,22 @@ const render = (ctx) => { ctx.translate(gunX, gunY + recoilOffset); ctx.rotate(tiltAngle); - // Gun body (larger rectangle) + // Gun body (larger rectangle) extending below screen ctx.fillStyle = '#333'; - ctx.fillRect(-30, 0, 60, 90); + ctx.fillRect(-30, 0, 60, height); // Gun slide (smaller rectangle) with sliding animation const slideOffset = GameState.gun.slidePosition * 20; ctx.fillStyle = '#222'; - ctx.fillRect(-8, -30 - slideOffset, 16, 60); + // Adjusted slide dimensions: shorter above, longer below + ctx.fillRect(-8, -15 - slideOffset, 16, 90); // Changed from -30 to -15 for top, and 60 to 90 for height // 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(0, -30 - slideOffset, flashSize, 0, Math.PI * 2); + ctx.arc(0, -15 - slideOffset, flashSize, 0, Math.PI * 2); // Adjusted to match new slide position ctx.fill(); } @@ -478,25 +517,25 @@ const render = (ctx) => { // Draw crosshair ctx.fillStyle = '#fff'; - ctx.fillRect(width/2 - 5, height/2, 10, 1); - ctx.fillRect(width/2, height/2 - 5, 1, 10); + ctx.fillRect(width/2 - 5, halfHeight, 10, 1); + ctx.fillRect(width/2, halfHeight - 5, 1, 10); // Draw HUD - only canvas-based ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; - ctx.fillRect(10, 10, 200, 100); + ctx.fillRect(10, 10, 200, 120); 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); + ctx.fillText(`Health: ${GameState.player.health}`, 100, 45); + ctx.fillText(`Ammo: ${GameState.player.ammo}`, 80, 80); + ctx.fillText(`Score: ${GameState.player.score}`, 80, 115); // Draw sprites with proper wall occlusion sprites.forEach(sprite => { const spriteHeight = height / (sprite.distance * Math.cos(sprite.angle - GameState.player.angle)); const spriteWidth = spriteHeight; const spriteX = sprite.x - spriteWidth/2; - const spriteY = height/2 - spriteHeight/2; + const spriteY = halfHeight - spriteHeight/2; // Only draw if not occluded by a wall if (sprite.distance < rayResults[sprite.x].distance) { @@ -535,7 +574,7 @@ const render = (ctx) => { }); // Draw mini-map - const miniMapSize = 150; + const miniMapSize = 200; // Increased from 150 const miniMapX = width - miniMapSize - 10; const miniMapY = 10; const cellSize = miniMapSize / GameState.level.width; @@ -571,12 +610,12 @@ const render = (ctx) => { ); ctx.fill(); - // Draw items in mini-map with matching colors + // Draw items GameState.items.forEach(item => { if (item.type === 'health') { - ctx.fillStyle = '#ffd700'; // Golden - } else if (item.type === 'ammo') { - ctx.fillStyle = '#0ff'; // Teal + ctx.fillStyle = '#0f0'; + } else { + ctx.fillStyle = '#0ff'; } ctx.beginPath(); ctx.arc( @@ -589,28 +628,22 @@ const render = (ctx) => { ctx.fill(); }); - // Draw enemies in mini-map - ctx.fillStyle = '#f00'; // Red - GameState.enemies.forEach(enemy => { - ctx.beginPath(); - ctx.arc( - miniMapX + enemy.x * cellSize + cellSize/2, - miniMapY + enemy.y * cellSize + cellSize/2, - cellSize/2, - 0, - Math.PI * 2 - ); - ctx.fill(); - }); - - // Draw player as a triangle - ctx.fillStyle = '#00f'; + // Draw player with enhanced visibility const playerX = miniMapX + GameState.player.x * cellSize; const playerY = miniMapY + GameState.player.y * cellSize; - const size = cellSize; + const size = cellSize * 1.5; // Increased player size + // Draw player outline + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(playerX + cellSize/2, playerY + cellSize/2, size/2 + 2, 0, Math.PI * 2); + ctx.stroke(); + + // Draw player triangle + ctx.fillStyle = '#00f'; ctx.save(); - ctx.translate(playerX, playerY); + ctx.translate(playerX + cellSize/2, playerY + cellSize/2); ctx.rotate(GameState.player.angle); ctx.beginPath(); ctx.moveTo(0, -size/2); @@ -619,6 +652,20 @@ const render = (ctx) => { ctx.closePath(); ctx.fill(); ctx.restore(); + + // Draw enemies + ctx.fillStyle = '#f00'; + GameState.enemies.forEach(enemy => { + ctx.beginPath(); + ctx.arc( + miniMapX + enemy.x * cellSize + cellSize/2, + miniMapY + enemy.y * cellSize + cellSize/2, + cellSize/2, + 0, + Math.PI * 2 + ); + ctx.fill(); + }); }; // Game loop @@ -718,6 +765,12 @@ const keys = { }; document.addEventListener('keydown', e => { + if (!GameState.isStarted && (e.key === ' ' || e.key === 'Enter')) { + GameState.isStarted = true; + generateLevel(); + return; + } + if (e.key.toLowerCase() in keys) keys[e.key.toLowerCase()] = true; if (e.key in keys) keys[e.key] = true; }); @@ -728,9 +781,12 @@ document.addEventListener('keyup', e => { }); document.addEventListener('mousemove', e => { - GameState.player.angle += e.movementX * 0.01; + if (GameState.isStarted) { + GameState.player.angle += e.movementX * 0.01; + } }); +// Update click handler to handle both start screen and firing document.addEventListener('click', (e) => { if (!GameState.isStarted) { const canvas = document.getElementById('gameCanvas'); @@ -748,6 +804,47 @@ document.addEventListener('click', (e) => { GameState.isStarted = true; generateLevel(); } + return; + } + + // Handle firing on click during gameplay + if (GameState.player.ammo > 0 && Date.now() - GameState.gun.lastShot > 200) { + GameState.player.ammo--; + GameState.gun.recoil = 1; + GameState.gun.muzzleFlash = 1; + GameState.gun.lastShot = Date.now(); + GameState.shots.push({ + time: Date.now(), + angle: GameState.player.angle + }); + + // Check for enemy hits + 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; + + if (GameState.level.map[Math.floor(testY)][Math.floor(testX)] === 1) { + hitWall = true; + break; + } + + 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--; + } } }); |