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.lastDirection = 1; // Track last direction to prevent unnecessary flipping
|
||||||
this.isClimbing = false; // Track climbing state
|
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
|
// Animation properties
|
||||||
this.frameWidth = 32;
|
this.frameWidth = 32;
|
||||||
this.frameHeight = 30;
|
this.frameHeight = 30;
|
||||||
@ -87,12 +107,26 @@ class Player extends Entity {
|
|||||||
this.x = newX;
|
this.x = newX;
|
||||||
this.y = newY;
|
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
|
// Update animation
|
||||||
this.updateAnimation(deltaTime);
|
this.updateAnimation(deltaTime);
|
||||||
|
|
||||||
// Center camera on player
|
// Center camera on player
|
||||||
this.centerCamera();
|
this.centerCamera();
|
||||||
|
|
||||||
|
// Update HUD
|
||||||
|
this.updateHUD();
|
||||||
|
|
||||||
return true;
|
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
|
// Try to climb up a small step
|
||||||
tryClimbing(newX, newY) {
|
tryClimbing(newX, newY) {
|
||||||
const halfWidth = this.width / 2;
|
const halfWidth = this.width / 2;
|
||||||
|
12
js/input.js
12
js/input.js
@ -18,14 +18,24 @@ window.addEventListener('keydown', (e) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start breaking blocks with E key
|
||||||
|
if (e.code === 'KeyE' && player) {
|
||||||
|
player.startBreaking();
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent default behavior for game control keys
|
// 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();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keyup', (e) => {
|
window.addEventListener('keyup', (e) => {
|
||||||
keyState[e.code] = false;
|
keyState[e.code] = false;
|
||||||
|
|
||||||
|
// Stop breaking blocks when E key is released
|
||||||
|
if (e.code === 'KeyE' && player) {
|
||||||
|
player.stopBreaking();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function updatePlayerMovement() {
|
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
|
// Remove the event listener for the spawn button to prevent multiple spawns
|
||||||
document.getElementById('spawn-player-btn').removeEventListener('click', spawnPlayer);
|
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) {
|
function simulationLoop(timestamp) {
|
||||||
|
32
js/render.js
32
js/render.js
@ -86,6 +86,11 @@ function render() {
|
|||||||
renderEntities(ctx, worldOffsetX, worldOffsetY);
|
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
|
// Draw cursor position and update debug info
|
||||||
if (currentMouseX !== undefined && currentMouseY !== undefined) {
|
if (currentMouseX !== undefined && currentMouseY !== undefined) {
|
||||||
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
|
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
|
||||||
@ -168,6 +173,33 @@ function displayDebugInfo() {
|
|||||||
ctx.restore();
|
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
|
// Render a chunk to an offscreen canvas and cache it
|
||||||
function renderChunkToCache(chunkX, chunkY, key) {
|
function renderChunkToCache(chunkX, chunkY, key) {
|
||||||
const chunk = chunks.get(key);
|
const chunk = chunks.get(key);
|
||||||
|
54
styles.css
54
styles.css
@ -75,3 +75,57 @@ body {
|
|||||||
background-color: #000;
|
background-color: #000;
|
||||||
cursor: crosshair;
|
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