feat: Add player entity with WASD/space controls and spawn functionality
This commit is contained in:
parent
d86baa8f99
commit
db5b49ee7f
@ -25,6 +25,7 @@
|
|||||||
<button id="circle-btn">Circle</button>
|
<button id="circle-btn">Circle</button>
|
||||||
<button id="triangle-btn">Triangle</button>
|
<button id="triangle-btn">Triangle</button>
|
||||||
<button id="eraser-btn">Eraser</button>
|
<button id="eraser-btn">Eraser</button>
|
||||||
|
<button id="spawn-player-btn">Spawn Player</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
<button id="move-left">←</button>
|
<button id="move-left">←</button>
|
||||||
@ -51,6 +52,7 @@
|
|||||||
<script src="js/elements/physics_objects.js"></script>
|
<script src="js/elements/physics_objects.js"></script>
|
||||||
<script src="js/entities/entity.js"></script>
|
<script src="js/entities/entity.js"></script>
|
||||||
<script src="js/entities/rabbit.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/render.js"></script>
|
||||||
<script src="js/input.js"></script>
|
<script src="js/input.js"></script>
|
||||||
<script src="js/physics.js"></script>
|
<script src="js/physics.js"></script>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Base entity system
|
// Base entity system
|
||||||
const ENTITY_TYPES = {
|
const ENTITY_TYPES = {
|
||||||
RABBIT: 'rabbit'
|
RABBIT: 'rabbit',
|
||||||
|
PLAYER: 'player'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store all entities
|
// Store all entities
|
||||||
@ -141,6 +142,9 @@ function createEntity(type, x, y, options = {}) {
|
|||||||
case ENTITY_TYPES.RABBIT:
|
case ENTITY_TYPES.RABBIT:
|
||||||
entity = new Rabbit(x, y, options);
|
entity = new Rabbit(x, y, options);
|
||||||
break;
|
break;
|
||||||
|
case ENTITY_TYPES.PLAYER:
|
||||||
|
entity = new Player(x, y, options);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown entity type: ${type}`);
|
console.error(`Unknown entity type: ${type}`);
|
||||||
return null;
|
return null;
|
||||||
|
187
js/entities/player.js
Normal file
187
js/entities/player.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
36
js/input.js
36
js/input.js
@ -4,6 +4,42 @@ let isDragging = false;
|
|||||||
let lastMouseX, lastMouseY;
|
let lastMouseX, lastMouseY;
|
||||||
let currentMouseX, currentMouseY;
|
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) {
|
function setTool(tool) {
|
||||||
currentTool = tool;
|
currentTool = tool;
|
||||||
document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active'));
|
||||||
|
32
js/main.js
32
js/main.js
@ -40,6 +40,9 @@ window.onload = function() {
|
|||||||
document.getElementById('triangle-btn').addEventListener('click', () => setTool(TRIANGLE));
|
document.getElementById('triangle-btn').addEventListener('click', () => setTool(TRIANGLE));
|
||||||
document.getElementById('eraser-btn').addEventListener('click', () => setTool(EMPTY));
|
document.getElementById('eraser-btn').addEventListener('click', () => setTool(EMPTY));
|
||||||
|
|
||||||
|
// Add player spawn button
|
||||||
|
document.getElementById('spawn-player-btn').addEventListener('click', spawnPlayer);
|
||||||
|
|
||||||
// Navigation controls
|
// Navigation controls
|
||||||
document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0));
|
document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0));
|
||||||
document.getElementById('move-right').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;
|
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) {
|
function simulationLoop(timestamp) {
|
||||||
// Calculate FPS
|
// Calculate FPS
|
||||||
const deltaTime = timestamp - lastFrameTime;
|
const deltaTime = timestamp - lastFrameTime;
|
||||||
@ -93,6 +120,11 @@ function simulationLoop(timestamp) {
|
|||||||
fps = Math.round(1000 / deltaTime);
|
fps = Math.round(1000 / deltaTime);
|
||||||
document.getElementById('fps').textContent = `FPS: ${fps}`;
|
document.getElementById('fps').textContent = `FPS: ${fps}`;
|
||||||
|
|
||||||
|
// Update player movement if player exists
|
||||||
|
if (player) {
|
||||||
|
updatePlayerMovement();
|
||||||
|
}
|
||||||
|
|
||||||
// Update physics with timestamp for rate limiting
|
// Update physics with timestamp for rate limiting
|
||||||
updatePhysics(timestamp);
|
updatePhysics(timestamp);
|
||||||
|
|
||||||
|
12
styles.css
12
styles.css
@ -39,6 +39,18 @@ body {
|
|||||||
background-color: #ff9800;
|
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 {
|
.navigation {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user