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