diff --git a/js/entities/player.js b/js/entities/player.js index ce5a736..f9043db 100644 --- a/js/entities/player.js +++ b/js/entities/player.js @@ -25,6 +25,26 @@ class Player extends Entity { this.lastDirection = 1; // Track last direction to prevent unnecessary flipping this.isClimbing = false; // Track climbing state + // Player stats + this.maxHealth = 100; + this.health = 100; + this.breakingPower = 1; + this.breakingRange = 3; + this.isBreaking = false; + this.breakingCooldown = 0; + this.breakingCooldownMax = 10; + + // Inventory + this.inventory = { + sand: 0, + water: 0, + dirt: 0, + stone: 0, + wood: 0, + grass: 0, + seed: 0 + }; + // Animation properties this.frameWidth = 32; this.frameHeight = 30; @@ -87,12 +107,26 @@ class Player extends Entity { this.x = newX; this.y = newY; + // Update breaking cooldown + if (this.breakingCooldown > 0) { + this.breakingCooldown--; + } + + // Handle breaking action + if (this.isBreaking && this.breakingCooldown <= 0) { + this.breakBlock(); + this.breakingCooldown = this.breakingCooldownMax; + } + // Update animation this.updateAnimation(deltaTime); // Center camera on player this.centerCamera(); + // Update HUD + this.updateHUD(); + return true; } @@ -209,6 +243,224 @@ class Player extends Entity { } } + startBreaking() { + this.isBreaking = true; + } + + stopBreaking() { + this.isBreaking = false; + } + + breakBlock() { + // Calculate the position in front of the player + const halfWidth = this.width / 2; + const breakX = Math.floor(this.x + (this.direction * this.breakingRange)); + const breakY = Math.floor(this.y); + + // Get the block type at that position + const blockType = getPixel(breakX, breakY); + + // Only break non-empty blocks that aren't special entities + if (blockType !== EMPTY && + blockType !== WATER && + blockType !== FIRE && + blockType !== SQUARE && + blockType !== CIRCLE && + blockType !== TRIANGLE) { + + // Add to inventory based on block type + this.addToInventory(blockType); + + // Replace with empty space + setPixel(breakX, breakY, EMPTY); + + // Create a breaking effect (particles) + this.createBreakingEffect(breakX, breakY, blockType); + } + } + + addToInventory(blockType) { + // Map block type to inventory item + switch(blockType) { + case SAND: + this.inventory.sand++; + break; + case DIRT: + this.inventory.dirt++; + break; + case STONE: + this.inventory.stone++; + break; + case GRASS: + this.inventory.grass++; + break; + case WOOD: + this.inventory.wood++; + break; + case SEED: + case TREE_SEED: + this.inventory.seed++; + break; + } + } + + createBreakingEffect(x, y, blockType) { + // Create a simple particle effect at the breaking location + // This could be expanded with a proper particle system + const numParticles = 5; + + // For now, we'll just create a visual feedback by setting nearby pixels + // to a different color briefly, then clearing them + for (let dy = -1; dy <= 1; dy++) { + for (let dx = -1; dx <= 1; dx++) { + if (dx === 0 && dy === 0) continue; + + // Skip if the pixel is not empty + if (getPixel(x + dx, y + dy) !== EMPTY) continue; + + // Set a temporary pixel + setPixel(x + dx, y + dy, EMPTY); + + // Mark the chunk as dirty for rendering + const { chunkX, chunkY } = getChunkCoordinates(x + dx, y + dy); + const key = getChunkKey(chunkX, chunkY); + dirtyChunks.add(key); + } + } + } + + updateHUD() { + // Get or create the HUD container + let hudContainer = document.getElementById('player-hud'); + if (!hudContainer) { + hudContainer = document.createElement('div'); + hudContainer.id = 'player-hud'; + hudContainer.style.position = 'fixed'; + hudContainer.style.bottom = '10px'; + hudContainer.style.left = '10px'; + hudContainer.style.width = '300px'; + hudContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; + hudContainer.style.color = 'white'; + hudContainer.style.padding = '10px'; + hudContainer.style.borderRadius = '5px'; + hudContainer.style.fontFamily = 'Arial, sans-serif'; + hudContainer.style.zIndex = '1000'; + document.body.appendChild(hudContainer); + + // Create health bar container + const healthBarContainer = document.createElement('div'); + healthBarContainer.id = 'health-bar-container'; + healthBarContainer.style.width = '100%'; + healthBarContainer.style.height = '20px'; + healthBarContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.3)'; + healthBarContainer.style.marginBottom = '10px'; + healthBarContainer.style.borderRadius = '3px'; + + // Create health bar + const healthBar = document.createElement('div'); + healthBar.id = 'health-bar'; + healthBar.style.width = '100%'; + healthBar.style.height = '100%'; + healthBar.style.backgroundColor = '#4CAF50'; + healthBar.style.borderRadius = '3px'; + healthBar.style.transition = 'width 0.3s'; + + healthBarContainer.appendChild(healthBar); + hudContainer.appendChild(healthBarContainer); + + // Create inventory container + const inventoryContainer = document.createElement('div'); + inventoryContainer.id = 'inventory-container'; + inventoryContainer.style.display = 'grid'; + inventoryContainer.style.gridTemplateColumns = 'repeat(7, 1fr)'; + inventoryContainer.style.gap = '5px'; + + // Create inventory slots + const inventoryItems = ['sand', 'dirt', 'stone', 'grass', 'wood', 'water', 'seed']; + inventoryItems.forEach(item => { + const slot = document.createElement('div'); + slot.id = `inventory-${item}`; + slot.className = 'inventory-slot'; + slot.style.width = '30px'; + slot.style.height = '30px'; + slot.style.backgroundColor = 'rgba(255, 255, 255, 0.2)'; + slot.style.borderRadius = '3px'; + slot.style.display = 'flex'; + slot.style.flexDirection = 'column'; + slot.style.alignItems = 'center'; + slot.style.justifyContent = 'center'; + slot.style.fontSize = '10px'; + slot.style.position = 'relative'; + + // Create item icon + const icon = document.createElement('div'); + icon.style.width = '20px'; + icon.style.height = '20px'; + icon.style.borderRadius = '3px'; + + // Set color based on item type + switch(item) { + case 'sand': icon.style.backgroundColor = '#e6c588'; break; + case 'dirt': icon.style.backgroundColor = '#8B4513'; break; + case 'stone': icon.style.backgroundColor = '#A9A9A9'; break; + case 'grass': icon.style.backgroundColor = '#7CFC00'; break; + case 'wood': icon.style.backgroundColor = '#8B5A2B'; break; + case 'water': icon.style.backgroundColor = '#4a80f5'; break; + case 'seed': icon.style.backgroundColor = '#654321'; break; + } + + // Create count label + const count = document.createElement('div'); + count.id = `${item}-count`; + count.style.position = 'absolute'; + count.style.bottom = '2px'; + count.style.right = '2px'; + count.style.fontSize = '8px'; + count.style.fontWeight = 'bold'; + count.textContent = '0'; + + slot.appendChild(icon); + slot.appendChild(count); + inventoryContainer.appendChild(slot); + }); + + hudContainer.appendChild(inventoryContainer); + + // Create controls help text + const controlsHelp = document.createElement('div'); + controlsHelp.style.marginTop = '10px'; + controlsHelp.style.fontSize = '10px'; + controlsHelp.style.color = '#aaa'; + controlsHelp.innerHTML = 'Controls: A/D - Move, W/Space - Jump, E - Break blocks'; + + hudContainer.appendChild(controlsHelp); + } + + // Update health bar + const healthBar = document.getElementById('health-bar'); + if (healthBar) { + const healthPercent = (this.health / this.maxHealth) * 100; + healthBar.style.width = `${healthPercent}%`; + + // Change color based on health + if (healthPercent > 60) { + healthBar.style.backgroundColor = '#4CAF50'; // Green + } else if (healthPercent > 30) { + healthBar.style.backgroundColor = '#FFC107'; // Yellow + } else { + healthBar.style.backgroundColor = '#F44336'; // Red + } + } + + // Update inventory counts + for (const [item, count] of Object.entries(this.inventory)) { + const countElement = document.getElementById(`${item}-count`); + if (countElement) { + countElement.textContent = count; + } + } + } + // Try to climb up a small step tryClimbing(newX, newY) { const halfWidth = this.width / 2; diff --git a/js/input.js b/js/input.js index 3dde0b7..6971f35 100644 --- a/js/input.js +++ b/js/input.js @@ -18,14 +18,24 @@ window.addEventListener('keydown', (e) => { e.preventDefault(); } + // Start breaking blocks with E key + if (e.code === 'KeyE' && player) { + player.startBreaking(); + } + // Prevent default behavior for game control keys - if (['KeyW', 'KeyA', 'KeyS', 'KeyD', 'Space'].includes(e.code)) { + if (['KeyW', 'KeyA', 'KeyS', 'KeyD', 'Space', 'KeyE'].includes(e.code)) { e.preventDefault(); } }); window.addEventListener('keyup', (e) => { keyState[e.code] = false; + + // Stop breaking blocks when E key is released + if (e.code === 'KeyE' && player) { + player.stopBreaking(); + } }); function updatePlayerMovement() { diff --git a/js/main.js b/js/main.js index c543ca4..aa0808a 100644 --- a/js/main.js +++ b/js/main.js @@ -115,6 +115,22 @@ function spawnPlayer() { // Remove the event listener for the spawn button to prevent multiple spawns document.getElementById('spawn-player-btn').removeEventListener('click', spawnPlayer); + + // Create CSS for player HUD + const style = document.createElement('style'); + style.textContent = ` + #player-hud { + transition: opacity 0.3s; + } + .inventory-slot { + transition: transform 0.1s; + } + .inventory-slot:hover { + transform: scale(1.1); + background-color: rgba(255, 255, 255, 0.3) !important; + } + `; + document.head.appendChild(style); } function simulationLoop(timestamp) { diff --git a/js/render.js b/js/render.js index f6ac1a2..6caea5e 100644 --- a/js/render.js +++ b/js/render.js @@ -86,6 +86,11 @@ function render() { renderEntities(ctx, worldOffsetX, worldOffsetY); } + // Render breaking range indicator if player is breaking + if (player && player.isBreaking) { + renderBreakingIndicator(ctx, worldOffsetX, worldOffsetY); + } + // Draw cursor position and update debug info if (currentMouseX !== undefined && currentMouseY !== undefined) { const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX; @@ -168,6 +173,33 @@ function displayDebugInfo() { ctx.restore(); } +// Render breaking indicator for player +function renderBreakingIndicator(ctx, offsetX, offsetY) { + // Calculate position in front of player + const breakX = Math.floor(player.x + (player.direction * player.breakingRange)); + const breakY = Math.floor(player.y); + + // Convert to screen coordinates + const screenX = (breakX - offsetX) * PIXEL_SIZE; + const screenY = (breakY - offsetY) * PIXEL_SIZE; + + // Draw breaking indicator + ctx.strokeStyle = '#ff0000'; + ctx.lineWidth = 2; + ctx.strokeRect( + screenX - PIXEL_SIZE/2, + screenY - PIXEL_SIZE/2, + PIXEL_SIZE, + PIXEL_SIZE + ); + + // Draw line from player to breaking point + ctx.beginPath(); + ctx.moveTo((player.x - offsetX) * PIXEL_SIZE, (player.y - offsetY) * PIXEL_SIZE); + ctx.lineTo(screenX, screenY); + ctx.stroke(); +} + // Render a chunk to an offscreen canvas and cache it function renderChunkToCache(chunkX, chunkY, key) { const chunk = chunks.get(key); diff --git a/styles.css b/styles.css index a834ee4..018e446 100644 --- a/styles.css +++ b/styles.css @@ -75,3 +75,57 @@ body { background-color: #000; cursor: crosshair; } + +#player-hud { + position: fixed; + bottom: 10px; + left: 10px; + width: 300px; + background-color: rgba(0, 0, 0, 0.7); + color: white; + padding: 10px; + border-radius: 5px; + font-family: Arial, sans-serif; + z-index: 1000; +} + +#health-bar-container { + width: 100%; + height: 20px; + background-color: rgba(255, 255, 255, 0.3); + margin-bottom: 10px; + border-radius: 3px; +} + +#health-bar { + width: 100%; + height: 100%; + background-color: #4CAF50; + border-radius: 3px; + transition: width 0.3s; +} + +#inventory-container { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 5px; +} + +.inventory-slot { + width: 30px; + height: 30px; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 3px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 10px; + position: relative; + transition: transform 0.1s; +} + +.inventory-slot:hover { + transform: scale(1.1); + background-color: rgba(255, 255, 255, 0.3); +}