256 lines
8.8 KiB
JavaScript
256 lines
8.8 KiB
JavaScript
// Player entity class
|
|
class Player extends Entity {
|
|
constructor(x, y, options = {}) {
|
|
super(ENTITY_TYPES.PLAYER, x, y, {
|
|
width: 8, // Adjusted collision box size
|
|
height: 16,
|
|
...options
|
|
});
|
|
|
|
// Load player sprite
|
|
this.sprite = new Image();
|
|
this.sprite.src = 'sprites/citizen.png';
|
|
|
|
// Movement properties
|
|
this.moveSpeed = 0.03;
|
|
this.jumpForce = -0.2;
|
|
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();
|
|
this.lastDirection = 1; // Track last direction to prevent unnecessary flipping
|
|
|
|
// 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;
|
|
this.animationTimer = 0; // Consistent timer for animation
|
|
}
|
|
|
|
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 timer consistently
|
|
this.animationTimer += deltaTime;
|
|
|
|
// Only change animation frame at fixed intervals to prevent blinking
|
|
if (this.animationTimer >= this.animationSpeed) {
|
|
// Only update animation if actually moving horizontally
|
|
if (Math.abs(this.vx) > 0.005 && this.isMoving) {
|
|
this.currentFrame = (this.currentFrame + 1) % this.frameCount;
|
|
} else if (!this.isMoving || Math.abs(this.vx) < 0.005) {
|
|
// Reset to standing frame when not moving
|
|
this.currentFrame = 0;
|
|
}
|
|
|
|
// Reset timer but keep remainder to maintain smooth timing
|
|
this.animationTimer %= this.animationSpeed;
|
|
}
|
|
|
|
// Only update direction when it actually changes to prevent flipping
|
|
if (Math.abs(this.vx) > 0.005) {
|
|
const newDirection = this.vx > 0 ? 1 : -1;
|
|
if (newDirection !== this.lastDirection) {
|
|
this.direction = newDirection;
|
|
this.lastDirection = newDirection;
|
|
}
|
|
}
|
|
}
|
|
|
|
render(ctx, offsetX, offsetY) {
|
|
const screenX = (this.x - offsetX) * PIXEL_SIZE;
|
|
const screenY = (this.y - offsetY) * PIXEL_SIZE;
|
|
|
|
if (this.sprite && this.sprite.complete) {
|
|
// Set pixelated rendering (nearest neighbor)
|
|
ctx.imageSmoothingEnabled = false;
|
|
|
|
ctx.save();
|
|
ctx.translate(screenX, screenY);
|
|
|
|
// Use smaller dimensions for the sprite
|
|
const spriteDisplayWidth = 24 * (PIXEL_SIZE / 2); // Smaller sprite (was 32)
|
|
const spriteDisplayHeight = 24 * (PIXEL_SIZE / 2); // Smaller sprite (was 32)
|
|
|
|
// Flip horizontally based on direction
|
|
if (this.direction < 0) {
|
|
ctx.scale(-1, 1);
|
|
}
|
|
|
|
// Draw the correct sprite frame
|
|
// Center the sprite on the entity position, with slight y-offset to align feet
|
|
ctx.drawImage(
|
|
this.sprite,
|
|
this.currentFrame * this.frameWidth, 0,
|
|
this.frameWidth, this.frameHeight,
|
|
-spriteDisplayWidth / 2, -spriteDisplayHeight / 2 + 2, // Reduced offset for smaller sprite
|
|
spriteDisplayWidth, spriteDisplayHeight
|
|
);
|
|
|
|
ctx.restore();
|
|
// Reset image smoothing for other rendering
|
|
ctx.imageSmoothingEnabled = true;
|
|
|
|
// 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
|
|
);
|
|
|
|
// Also draw sprite boundary in debug mode
|
|
ctx.strokeStyle = '#ff00ff';
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeRect(
|
|
screenX - spriteDisplayWidth / 2,
|
|
screenY - spriteDisplayHeight / 2 + 4,
|
|
spriteDisplayWidth,
|
|
spriteDisplayHeight
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
// Get current camera center in world coordinates
|
|
const cameraWidth = canvas.width / PIXEL_SIZE;
|
|
const cameraHeight = canvas.height / PIXEL_SIZE;
|
|
const cameraCenterX = worldOffsetX + cameraWidth / 2;
|
|
const cameraCenterY = worldOffsetY + cameraHeight / 2;
|
|
|
|
// Calculate distance from player to camera center
|
|
const distanceX = Math.abs(this.x - cameraCenterX);
|
|
const distanceY = Math.abs(this.y - cameraCenterY);
|
|
|
|
// Define thresholds for camera movement (percentage of screen size)
|
|
const thresholdX = cameraWidth * 0.3; // Move when player is 30% away from center
|
|
const thresholdY = cameraHeight * 0.3;
|
|
|
|
// Only move camera when player gets close to the edge of current view
|
|
let needsUpdate = false;
|
|
|
|
if (distanceX > thresholdX) {
|
|
// Calculate target position with chunk-based snapping
|
|
const chunkSize = CHUNK_SIZE;
|
|
const playerChunkX = Math.floor(this.x / chunkSize);
|
|
const targetX = this.x - (canvas.width / PIXEL_SIZE / 2);
|
|
|
|
// Smooth transition to the target position
|
|
worldOffsetX += (targetX - worldOffsetX) * 0.1;
|
|
needsUpdate = true;
|
|
}
|
|
|
|
if (distanceY > thresholdY) {
|
|
// Calculate target position with chunk-based snapping
|
|
const chunkSize = CHUNK_SIZE;
|
|
const playerChunkY = Math.floor(this.y / chunkSize);
|
|
const targetY = this.y - (canvas.height / PIXEL_SIZE / 2);
|
|
|
|
// Smooth transition to the target position
|
|
worldOffsetY += (targetY - worldOffsetY) * 0.1;
|
|
needsUpdate = true;
|
|
}
|
|
|
|
// Only mark world as moved if we actually updated the camera
|
|
if (needsUpdate) {
|
|
worldMoved = true;
|
|
}
|
|
}
|
|
}
|