This commit adds: 1. Player climbing mechanism for small steps 2. Collision avoidance with trees (WOOD and LEAF pixels) 3. Enhanced animation states for climbing and jumping 4. Improved player movement and collision detection
301 lines
11 KiB
JavaScript
301 lines
11 KiB
JavaScript
// Player entity class
|
|
class Player extends Entity {
|
|
constructor(x, y, options = {}) {
|
|
super(ENTITY_TYPES.PLAYER, x, y, {
|
|
width: 3, // 50% smaller collision box width
|
|
height: 6, // 50% smaller collision box height
|
|
...options
|
|
});
|
|
|
|
// Load player sprite
|
|
this.sprite = new Image();
|
|
this.sprite.src = 'sprites/rabbit.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
|
|
this.isClimbing = false; // Track climbing state
|
|
|
|
// Animation properties
|
|
this.frameWidth = 32;
|
|
this.frameHeight = 30;
|
|
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) {
|
|
// Try to climb up if there's a 1-pixel step
|
|
if (this.tryClimbing(newX, newY)) {
|
|
// Successfully climbed, continue with adjusted position
|
|
newY -= 1; // Move up one pixel to climb
|
|
} else {
|
|
// Can't climb, stop horizontal movement
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
// Update animation frame based on climbing state
|
|
if (this.isClimbing) {
|
|
// Use a specific frame for climbing
|
|
this.currentFrame = 1; // Use frame 1 for climbing animation
|
|
} else if (this.isJumping) {
|
|
// Use a specific frame for jumping
|
|
this.currentFrame = 2; // Use frame 2 for jumping animation
|
|
} else if (Math.abs(this.vx) > 0.01 && this.isMoving) {
|
|
// Animate walking
|
|
if (this.animationTimer >= this.animationSpeed) {
|
|
this.currentFrame = (this.currentFrame + 1) % this.frameCount;
|
|
this.animationTimer %= this.animationSpeed;
|
|
}
|
|
} else {
|
|
// Standing still
|
|
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) {
|
|
// Set pixelated rendering (nearest neighbor)
|
|
ctx.imageSmoothingEnabled = false;
|
|
|
|
ctx.save();
|
|
ctx.translate(screenX, screenY);
|
|
|
|
// Use 50% smaller dimensions for the sprite
|
|
const spriteDisplayWidth = 12 * (PIXEL_SIZE / 2); // 50% smaller sprite
|
|
const spriteDisplayHeight = 12 * (PIXEL_SIZE / 2); // 50% smaller sprite
|
|
|
|
// 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 y-offset to align feet with collision box
|
|
// Stretch the sprite vertically to match the collision box height
|
|
ctx.drawImage(
|
|
this.sprite,
|
|
this.currentFrame * this.frameWidth, 0,
|
|
this.frameWidth, this.frameHeight,
|
|
-spriteDisplayWidth / 2, -spriteDisplayHeight / 2, // Remove the negative offset that caused levitation
|
|
spriteDisplayWidth, spriteDisplayHeight * 1.2 // Stretch sprite vertically by 20% to match collision box
|
|
);
|
|
|
|
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, // Match the updated sprite drawing position
|
|
spriteDisplayWidth,
|
|
spriteDisplayHeight * 1.2 // Match the stretched sprite height
|
|
);
|
|
|
|
// Draw a dot at the entity's exact position
|
|
ctx.fillStyle = '#ffff00';
|
|
ctx.beginPath();
|
|
ctx.arc(screenX, screenY, 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Try to climb up a small step
|
|
tryClimbing(newX, newY) {
|
|
const halfWidth = this.width / 2;
|
|
|
|
// Check if there's a solid pixel in front of the player
|
|
const frontX = newX + (this.direction * halfWidth);
|
|
const frontY = newY;
|
|
|
|
// Check if there's a solid pixel at the current level
|
|
if (this.isPixelSolid(frontX, frontY)) {
|
|
// Check if there's empty space one pixel above
|
|
if (!this.isPixelSolid(frontX, frontY - 1) &&
|
|
!this.isPixelSolid(this.x, this.y - 1)) {
|
|
|
|
// Check if there's ground to stand on after climbing
|
|
if (this.isPixelSolid(frontX, frontY + 1)) {
|
|
this.isClimbing = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.isClimbing = false;
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|