From f66730b7aeb9494c266260df7eeba8a8e26beb4e Mon Sep 17 00:00:00 2001 From: elioat <{ID}+{username}@users.noreply.github.com> Date: Tue, 17 Dec 2024 07:14:01 -0500 Subject: * --- html/plains/game.js | 175 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 165 insertions(+), 10 deletions(-) (limited to 'html/plains/game.js') diff --git a/html/plains/game.js b/html/plains/game.js index 9e91353..5923b16 100644 --- a/html/plains/game.js +++ b/html/plains/game.js @@ -286,6 +286,43 @@ const CONFIG = { sizeMultiplier: 1.0 } } + }, + enemies: { + size: { + min: 15, + max: 20 + }, + colors: { + active: { + min: { + red: 150, + green: 0, + blue: 0 + }, + max: { + red: 255, + green: 100, + blue: 0 + } + }, + defeated: 'rgb(100, 100, 100)' + }, + patrol: { + radius: { + min: 100, + max: 200 + }, + speed: { + base: 100 // pixels per second + } + }, + chase: { + range: 2, + speedMultiplier: 1.5 // 50% faster than base speed + }, + return: { + speedMultiplier: 1.25 // 25% faster than base speed + } } }; @@ -307,24 +344,29 @@ const CAMERA_DEADZONE_Y = GAME_HEIGHT * CONFIG.display.camera.deadzoneMultiplier // ============= State Management ============= const createInitialState = () => ({ player: { - x: CONFIG.player.size, // A bit offset from the edge - y: CONFIG.player.size, // A bit offset from the edge + x: CONFIG.player.size, + y: CONFIG.player.size, isDefending: false, direction: { x: 0, y: -1 }, swordAngle: 0, isSwinging: false, - equipment: 'unarmed', // Start without sword + equipment: 'unarmed', bubbles: [], bubbleParticles: [], lastBubbleTime: 0, - dashStartTime: 0, // When the current dash started - isDashing: false, // Currently dashing? - dashExhausted: false, // Is dash on cooldown? - lastInputTime: 0, // Track when the last input occurred + dashStartTime: 0, + isDashing: false, + dashExhausted: false, + lastInputTime: 0, baseDirection: { x: 0, y: -1 }, lastDashEnd: 0, swordUnlocked: false, - rescuedCount: 0 + rescuedCount: 0, + hp: 15, + maxHp: 15, + isInvulnerable: false, + invulnerableUntil: 0, + isDead: false }, particles: [], footprints: [], @@ -338,7 +380,8 @@ const createInitialState = () => ({ collisionMap: new Map(), villagers: [], gameComplete: false, - gameCompleteMessageShown: false + gameCompleteMessageShown: false, + enemies: [] }); let state = createInitialState(); @@ -348,6 +391,14 @@ let state = createInitialState(); const keys = new Set(); const handleKeyDown = (e) => { + // If player is dead, only handle restart + if (state.player.isDead) { + if (e.code === 'Space') { + window.location.reload(); + } + return; + } + keys.add(e.key); if (e.key === 'z' && !state.player.isDefending) { @@ -1022,6 +1073,53 @@ const renderPlayer = () => { ctx.restore(); }; +const renderPlayerHUD = (ctx) => { + // Save the current context state + ctx.save(); + + // Switch to screen coordinates + ctx.setTransform(1, 0, 0, 1, 0, 0); + + // HP Bar container + const barWidth = 200; + const barHeight = 20; + const barX = 20; + const barY = 20; + const borderWidth = 2; + + // Draw border + ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; + ctx.fillRect( + barX - borderWidth, + barY - borderWidth, + barWidth + borderWidth * 2, + barHeight + borderWidth * 2 + ); + + // Background + ctx.fillStyle = 'rgba(50, 50, 50, 0.8)'; + ctx.fillRect(barX, barY, barWidth, barHeight); + + // HP + const hpRatio = state.player.hp / state.player.maxHp; + ctx.fillStyle = `rgb(${Math.floor(255 * (1 - hpRatio))}, ${Math.floor(255 * hpRatio)}, 0)`; + ctx.fillRect(barX, barY, barWidth * hpRatio, barHeight); + + // HP Text + ctx.fillStyle = 'white'; + ctx.font = '14px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText( + `HP: ${state.player.hp}/${state.player.maxHp}`, + barX + barWidth/2, + barY + barHeight/2 + ); + + // Restore the context state + ctx.restore(); +}; + const render = () => { ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT); @@ -1207,7 +1305,7 @@ const render = () => { // Choose color for this dot const colorIndex = useSingleColor ? cellColorIndex : - Math.floor(seededRandom(cellX * i * j, cellY * i * j) * config.colors.length); + Math.floor(seededRandom(cellX * i, cellY * j) * config.colors.length); ctx.fillStyle = config.colors[colorIndex]; ctx.beginPath(); @@ -1332,6 +1430,9 @@ const render = () => { ctx.restore(); }); + // Render enemies + enemySystem.renderEnemies(ctx, state.enemies); + // Draw player renderPlayer(); @@ -1361,6 +1462,33 @@ const render = () => { ctx.restore(); }); + // Render particles + state.particles = state.particles.filter(particle => { + const age = animationTime - particle.createdAt; + if (age >= particle.lifetime) return false; + + const alpha = 1 - (age / particle.lifetime); + ctx.beginPath(); + ctx.arc( + particle.x + particle.dx * age * 0.1, + particle.y + particle.dy * age * 0.1, + particle.size, + 0, Math.PI * 2 + ); + ctx.fillStyle = particle.color.replace('rgb', 'rgba').replace(')', `, ${alpha})`); + ctx.fill(); + + return true; + }); + + // Render HUD elements + renderPlayerHUD(ctx); + + // Update player invulnerability + if (state.player.isInvulnerable && animationTime >= state.player.invulnerableUntil) { + state.player.isInvulnerable = false; + } + ctx.restore(); }; @@ -1462,7 +1590,14 @@ const gameLoop = (currentTime) => { if (deltaTime >= FRAME_TIME) { animationTime += FRAME_TIME; + // Check for player death + if (state.player.hp <= 0 && !state.player.isDead) { + state.player.isDead = true; + showGameOver(); + } + updatePlayer(); + state.enemies = enemySystem.updateEnemies(state.enemies, deltaTime); render(); lastFrameTime = currentTime; @@ -1495,6 +1630,7 @@ resizeCanvas(); // Initialize villagers after collision map is populated state.villagers = generateVillagers(); +state.enemies = enemySystem.generateEnemies(state.villagers, state.collisionMap); // Start the game loop requestAnimationFrame(gameLoop); @@ -1696,4 +1832,23 @@ const showSwordUnlockAnimation = () => { document.body.removeChild(container); document.head.removeChild(style); }, CONFIG.player.equipment.unlockAnimation.duration); +}; + +const showGameOver = () => { + const gameOverDiv = document.createElement('div'); + gameOverDiv.style.position = 'fixed'; + gameOverDiv.style.top = '50%'; + gameOverDiv.style.left = '50%'; + gameOverDiv.style.transform = 'translate(-50%, -50%)'; + gameOverDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; + gameOverDiv.style.padding = '20px'; + gameOverDiv.style.borderRadius = '10px'; + gameOverDiv.style.color = 'white'; + gameOverDiv.style.textAlign = 'center'; + gameOverDiv.style.fontSize = '24px'; + gameOverDiv.innerHTML = ` +
Press Space to restart
+ `; + document.body.appendChild(gameOverDiv); }; \ No newline at end of file -- cgit 1.4.1-2-gfad0