feat: Add player entity with WASD/space controls and spawn functionality

This commit is contained in:
Kacper Kostka (aider) 2025-04-05 18:03:10 +02:00
parent d86baa8f99
commit db5b49ee7f
6 changed files with 274 additions and 1 deletions

View File

@ -25,6 +25,7 @@
<button id="circle-btn">Circle</button>
<button id="triangle-btn">Triangle</button>
<button id="eraser-btn">Eraser</button>
<button id="spawn-player-btn">Spawn Player</button>
</div>
<div class="navigation">
<button id="move-left"></button>
@ -51,6 +52,7 @@
<script src="js/elements/physics_objects.js"></script>
<script src="js/entities/entity.js"></script>
<script src="js/entities/rabbit.js"></script>
<script src="js/entities/player.js"></script>
<script src="js/render.js"></script>
<script src="js/input.js"></script>
<script src="js/physics.js"></script>

View File

@ -1,6 +1,7 @@
// Base entity system
const ENTITY_TYPES = {
RABBIT: 'rabbit'
RABBIT: 'rabbit',
PLAYER: 'player'
};
// Store all entities
@ -141,6 +142,9 @@ function createEntity(type, x, y, options = {}) {
case ENTITY_TYPES.RABBIT:
entity = new Rabbit(x, y, options);
break;
case ENTITY_TYPES.PLAYER:
entity = new Player(x, y, options);
break;
default:
console.error(`Unknown entity type: ${type}`);
return null;

187
js/entities/player.js Normal file
View File

@ -0,0 +1,187 @@
// Player entity class
class Player extends Entity {
constructor(x, y, options = {}) {
super(ENTITY_TYPES.PLAYER, x, y, {
width: 8, // Player size
height: 16,
...options
});
// Load player sprite
this.sprite = new Image();
this.sprite.src = 'sprites/citizen.png';
// Movement properties
this.moveSpeed = 0.15;
this.jumpForce = -0.5;
this.gravity = 0.02;
this.maxVelocity = 0.5;
this.friction = 0.9;
// State tracking
this.isJumping = false;
this.direction = 1; // 1 = right, -1 = left
this.lastUpdate = performance.now();
// Animation properties
this.frameWidth = 32;
this.frameHeight = 32;
this.frameCount = 4;
this.currentFrame = 0;
this.animationSpeed = 150; // ms per frame
this.lastFrameUpdate = 0;
this.isMoving = false;
}
update() {
const now = performance.now();
const deltaTime = Math.min(50, now - this.lastUpdate);
this.lastUpdate = now;
// Apply gravity
this.vy += this.gravity;
// Cap velocity
if (this.vx > this.maxVelocity) this.vx = this.maxVelocity;
if (this.vx < -this.maxVelocity) this.vx = -this.maxVelocity;
if (this.vy > this.maxVelocity * 2) this.vy = this.maxVelocity * 2;
// Apply friction when not actively moving
if (!this.isMoving) {
this.vx *= this.friction;
}
// Calculate new position
let newX = this.x + this.vx * deltaTime;
let newY = this.y + this.vy * deltaTime;
// Check for collisions
const collisionResult = this.checkCollisions(newX, newY);
if (collisionResult.collision) {
if (collisionResult.horizontal) {
newX = this.x;
this.vx = 0;
}
if (collisionResult.vertical) {
if (this.vy > 0) {
this.isJumping = false;
}
newY = this.y;
this.vy = 0;
}
}
// Update position
this.x = newX;
this.y = newY;
// Update animation
this.updateAnimation(deltaTime);
// Center camera on player
this.centerCamera();
return true;
}
updateAnimation(deltaTime) {
// Update animation frame if moving
if (this.isMoving || Math.abs(this.vy) > 0.1) {
this.lastFrameUpdate += deltaTime;
if (this.lastFrameUpdate >= this.animationSpeed) {
this.currentFrame = (this.currentFrame + 1) % this.frameCount;
this.lastFrameUpdate = 0;
}
} else {
// Reset to standing frame when not moving
this.currentFrame = 0;
}
}
render(ctx, offsetX, offsetY) {
const screenX = (this.x - offsetX) * PIXEL_SIZE;
const screenY = (this.y - offsetY) * PIXEL_SIZE;
if (this.sprite && this.sprite.complete) {
ctx.save();
ctx.translate(screenX, screenY);
// Flip horizontally based on direction
if (this.direction < 0) {
ctx.scale(-1, 1);
ctx.translate(-this.width * PIXEL_SIZE, 0);
}
// Draw the correct sprite frame
ctx.drawImage(
this.sprite,
this.currentFrame * this.frameWidth, 0,
this.frameWidth, this.frameHeight,
-this.width * PIXEL_SIZE / 2, -this.height * PIXEL_SIZE / 2,
this.width * PIXEL_SIZE, this.height * PIXEL_SIZE
);
ctx.restore();
// Draw collision box in debug mode
if (debugMode) {
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 1;
ctx.strokeRect(
screenX - this.width * PIXEL_SIZE / 2,
screenY - this.height * PIXEL_SIZE / 2,
this.width * PIXEL_SIZE,
this.height * PIXEL_SIZE
);
}
}
}
moveLeft() {
this.vx = -this.moveSpeed;
this.direction = -1;
this.isMoving = true;
}
moveRight() {
this.vx = this.moveSpeed;
this.direction = 1;
this.isMoving = true;
}
moveUp() {
this.vy = -this.moveSpeed;
this.isMoving = true;
}
moveDown() {
this.vy = this.moveSpeed;
this.isMoving = true;
}
stopMoving() {
this.isMoving = false;
}
jump() {
if (!this.isJumping) {
this.vy = this.jumpForce;
this.isJumping = true;
}
}
centerCamera() {
// Calculate the target world offset to center on player
const targetOffsetX = this.x - (canvas.width / PIXEL_SIZE / 2);
const targetOffsetY = this.y - (canvas.height / PIXEL_SIZE / 2);
// Smoothly move the camera to the target position
worldOffsetX += (targetOffsetX - worldOffsetX) * 0.1;
worldOffsetY += (targetOffsetY - worldOffsetY) * 0.1;
// Mark that the world has moved for rendering
worldMoved = true;
}
}

View File

@ -4,6 +4,42 @@ let isDragging = false;
let lastMouseX, lastMouseY;
let currentMouseX, currentMouseY;
// Keyboard state tracking
const keyState = {};
let player = null;
// Handle keyboard input for player movement
window.addEventListener('keydown', (e) => {
keyState[e.code] = true;
// Prevent default behavior for game control keys
if (['KeyW', 'KeyA', 'KeyS', 'KeyD', 'Space'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
keyState[e.code] = false;
});
function updatePlayerMovement() {
if (!player) return;
// Reset movement flag
player.stopMoving();
// Handle movement
if (keyState['KeyA']) {
player.moveLeft();
}
if (keyState['KeyD']) {
player.moveRight();
}
if (keyState['KeyW'] || keyState['Space']) {
player.jump();
}
}
function setTool(tool) {
currentTool = tool;
document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active'));

View File

@ -40,6 +40,9 @@ window.onload = function() {
document.getElementById('triangle-btn').addEventListener('click', () => setTool(TRIANGLE));
document.getElementById('eraser-btn').addEventListener('click', () => setTool(EMPTY));
// Add player spawn button
document.getElementById('spawn-player-btn').addEventListener('click', spawnPlayer);
// Navigation controls
document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0));
document.getElementById('move-right').addEventListener('click', () => moveWorld(CHUNK_SIZE/2, 0));
@ -86,6 +89,30 @@ function resizeCanvas() {
canvas.height = window.innerHeight - document.querySelector('.controls').offsetHeight;
}
// Function to spawn player
function spawnPlayer() {
// Hide HUD elements
document.querySelector('.controls').style.display = 'none';
// Set zoom level to 50%
PIXEL_SIZE = 2;
// Create player at specified coordinates
player = createEntity(ENTITY_TYPES.PLAYER, 229, 61);
// Focus camera on player
worldOffsetX = player.x - (canvas.width / PIXEL_SIZE / 2);
worldOffsetY = player.y - (canvas.height / PIXEL_SIZE / 2);
worldMoved = true;
// Resize canvas to full screen
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Remove the event listener for the spawn button to prevent multiple spawns
document.getElementById('spawn-player-btn').removeEventListener('click', spawnPlayer);
}
function simulationLoop(timestamp) {
// Calculate FPS
const deltaTime = timestamp - lastFrameTime;
@ -93,6 +120,11 @@ function simulationLoop(timestamp) {
fps = Math.round(1000 / deltaTime);
document.getElementById('fps').textContent = `FPS: ${fps}`;
// Update player movement if player exists
if (player) {
updatePlayerMovement();
}
// Update physics with timestamp for rate limiting
updatePhysics(timestamp);

View File

@ -39,6 +39,18 @@ body {
background-color: #ff9800;
}
#spawn-player-btn {
background-color: #4CAF50;
color: white;
font-weight: bold;
padding: 10px 15px;
margin-left: 10px;
}
#spawn-player-btn:hover {
background-color: #45a049;
}
.navigation {
display: flex;
}