diff --git a/index.html b/index.html index 865f321..71d5f0d 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,8 @@ + + diff --git a/js/entities/entity.js b/js/entities/entity.js new file mode 100644 index 0000000..053153f --- /dev/null +++ b/js/entities/entity.js @@ -0,0 +1,154 @@ +// Base entity system +const ENTITY_TYPES = { + RABBIT: 'rabbit' +}; + +// Store all entities +const entities = []; + +// Base Entity class +class Entity { + constructor(type, x, y, options = {}) { + this.type = type; + this.x = x; + this.y = y; + this.vx = 0; + this.vy = 0; + this.width = options.width || 10; + this.height = options.height || 10; + this.rotation = 0; + this.sprite = null; + this.flipped = false; + this.isStatic = false; + this.lastUpdate = performance.now(); + this.id = Entity.nextId++; + } + + static nextId = 1; + + update() { + // Override in subclasses + return false; + } + + render(ctx, offsetX, offsetY) { + // Default rendering - override in subclasses + const screenX = (this.x - offsetX) * PIXEL_SIZE; + const screenY = (this.y - offsetY) * PIXEL_SIZE; + + ctx.save(); + ctx.translate(screenX, screenY); + ctx.rotate(this.rotation); + + if (this.sprite && this.sprite.complete) { + const width = this.width * PIXEL_SIZE; + const height = this.height * PIXEL_SIZE; + + if (this.flipped) { + ctx.scale(-1, 1); + ctx.drawImage(this.sprite, -width/2, -height/2, width, height); + } else { + ctx.drawImage(this.sprite, -width/2, -height/2, width, height); + } + } else { + // Fallback if sprite not loaded + ctx.fillStyle = '#FF00FF'; + ctx.fillRect( + -this.width/2 * PIXEL_SIZE, + -this.height/2 * PIXEL_SIZE, + this.width * PIXEL_SIZE, + this.height * PIXEL_SIZE + ); + } + + ctx.restore(); + } + + checkCollisions(newX, newY) { + const result = { + collision: false, + horizontal: false, + vertical: false, + ground: false + }; + + // Check points around the entity + const halfWidth = this.width / 2; + const halfHeight = this.height / 2; + + // Check bottom points for ground collision + const bottomLeft = { x: newX - halfWidth * 0.8, y: newY + halfHeight }; + const bottomRight = { x: newX + halfWidth * 0.8, y: newY + halfHeight }; + + if (this.isPixelSolid(bottomLeft.x, bottomLeft.y) || + this.isPixelSolid(bottomRight.x, bottomRight.y)) { + result.collision = true; + result.vertical = true; + result.ground = true; + } + + // Check side points for horizontal collision + const leftMiddle = { x: newX - halfWidth, y: newY }; + const rightMiddle = { x: newX + halfWidth, y: newY }; + + if (this.isPixelSolid(leftMiddle.x, leftMiddle.y)) { + result.collision = true; + result.horizontal = true; + } + + if (this.isPixelSolid(rightMiddle.x, rightMiddle.y)) { + result.collision = true; + result.horizontal = true; + } + + // Check top for ceiling collision + const topMiddle = { x: newX, y: newY - halfHeight }; + if (this.isPixelSolid(topMiddle.x, topMiddle.y)) { + result.collision = true; + result.vertical = true; + } + + return result; + } + + isPixelSolid(x, y) { + const pixel = getPixel(Math.floor(x), Math.floor(y)); + return pixel !== EMPTY && + pixel !== WATER && + pixel !== FIRE && + pixel !== SQUARE && + pixel !== CIRCLE && + pixel !== TRIANGLE; + } +} + +// Function to create and register an entity +function createEntity(type, x, y, options = {}) { + let entity; + + switch(type) { + case ENTITY_TYPES.RABBIT: + entity = new Rabbit(x, y, options); + break; + default: + console.error(`Unknown entity type: ${type}`); + return null; + } + + entities.push(entity); + return entity; +} + +// Update all entities +function updateEntities() { + for (let i = entities.length - 1; i >= 0; i--) { + entities[i].update(); + } +} + +// Render all entities +function renderEntities(ctx, offsetX, offsetY) { + for (const entity of entities) { + entity.render(ctx, offsetX, offsetY); + } +} diff --git a/js/entities/rabbit.js b/js/entities/rabbit.js new file mode 100644 index 0000000..80a512b --- /dev/null +++ b/js/entities/rabbit.js @@ -0,0 +1,141 @@ +// Rabbit entity +class Rabbit extends Entity { + constructor(x, y, options = {}) { + super(ENTITY_TYPES.RABBIT, x, y, { + width: 6, // Smaller size for rabbit + height: 6, + ...options + }); + + // Load rabbit sprite + this.sprite = new Image(); + this.sprite.src = 'sprites/rabbit.png'; + + // Rabbit specific properties + this.jumpCooldown = 0; + this.jumpForce = -3.5; + this.moveSpeed = 0.8; + this.direction = Math.random() > 0.5 ? 1 : -1; // 1 for right, -1 for left + this.isJumping = false; + this.thinkTimer = 0; + this.actionDuration = 0; + this.currentAction = 'idle'; + + // Apply gravity + this.gravity = 0.2; + } + + update() { + const now = performance.now(); + const deltaTime = Math.min(50, now - this.lastUpdate); + this.lastUpdate = now; + + // Apply gravity + this.vy += this.gravity; + + // Calculate new position + let newX = this.x + this.vx; + let newY = this.y + this.vy; + + // Check for collisions + const collisionResult = this.checkCollisions(newX, newY); + + if (collisionResult.collision) { + if (collisionResult.horizontal) { + // Hit a wall, reverse direction + this.direction *= -1; + this.vx = this.moveSpeed * this.direction; + newX = this.x; // Don't move horizontally this frame + } + + if (collisionResult.vertical) { + if (collisionResult.ground) { + // Landed on ground + this.vy = 0; + this.isJumping = false; + + // Find exact ground position + while (this.isPixelSolid(this.x, newY)) { + newY--; + } + newY = Math.floor(newY) + 0.99; // Position just above ground + } else { + // Hit ceiling + this.vy = 0; + newY = this.y; + } + } + } + + // Update position + this.x = newX; + this.y = newY; + + // Update jump cooldown + if (this.jumpCooldown > 0) { + this.jumpCooldown--; + } + + // AI behavior + this.thinkTimer++; + if (this.thinkTimer >= 30) { // Think every 30 frames + this.thinkTimer = 0; + this.think(); + } + + // Update action duration + if (this.actionDuration > 0) { + this.actionDuration--; + } else if (this.currentAction !== 'idle') { + this.currentAction = 'idle'; + this.vx = 0; + } + + // Update sprite direction + this.flipped = this.direction < 0; + + return true; + } + + think() { + // Only make decisions when on the ground and not already in an action + if (!this.isJumping && this.actionDuration <= 0) { + const decision = Math.random(); + + if (decision < 0.2) { + // Jump + this.jump(); + } else if (decision < 0.6) { + // Move + this.move(); + } else { + // Idle + this.idle(); + } + } + } + + jump() { + if (!this.isJumping && this.jumpCooldown <= 0) { + this.vy = this.jumpForce; + this.vx = this.moveSpeed * this.direction; + this.isJumping = true; + this.jumpCooldown = 20; + this.currentAction = 'jump'; + this.actionDuration = 30; + } + } + + move() { + this.direction = Math.random() > 0.5 ? 1 : -1; + this.vx = this.moveSpeed * this.direction; + this.currentAction = 'move'; + this.actionDuration = 60 + Math.floor(Math.random() * 60); + } + + idle() { + this.vx = 0; + this.currentAction = 'idle'; + this.actionDuration = 30 + Math.floor(Math.random() * 30); + } +} diff --git a/js/physics.js b/js/physics.js index 75a0ed4..0476856 100644 --- a/js/physics.js +++ b/js/physics.js @@ -12,6 +12,11 @@ function updatePhysics(timestamp) { updatePhysicsObjects(); } + // Update entities + if (typeof updateEntities === 'function') { + updateEntities(); + } + // Get visible chunks const visibleChunks = getVisibleChunks(); diff --git a/js/render.js b/js/render.js index c67d51c..d9cf3b0 100644 --- a/js/render.js +++ b/js/render.js @@ -66,6 +66,11 @@ function render() { // Render physics objects renderPhysicsObjects(ctx, worldOffsetX, worldOffsetY); + // Render entities + if (typeof renderEntities === 'function') { + renderEntities(ctx, worldOffsetX, worldOffsetY); + } + // Draw cursor position and update debug info if (currentMouseX !== undefined && currentMouseY !== undefined) { const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX; diff --git a/sprites/rabbit.png b/sprites/rabbit.png index 0f5269b..178a10c 100644 Binary files a/sprites/rabbit.png and b/sprites/rabbit.png differ