// 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; } } }