feat: Add block breaking, HUD, and inventory system for player
This commit is contained in:
parent
755be2f5a4
commit
d765defa9d
@ -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;
|
||||
|
12
js/input.js
12
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() {
|
||||
|
16
js/main.js
16
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) {
|
||||
|
32
js/render.js
32
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);
|
||||
|
54
styles.css
54
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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user