feat: Add rabbit entity with dynamic movement and sprite rendering
This commit is contained in:
parent
f9cb363b37
commit
f1a18f9168
@ -48,6 +48,8 @@
|
|||||||
<script src="js/elements/trees.js"></script>
|
<script src="js/elements/trees.js"></script>
|
||||||
<script src="js/elements/fire.js"></script>
|
<script src="js/elements/fire.js"></script>
|
||||||
<script src="js/elements/physics_objects.js"></script>
|
<script src="js/elements/physics_objects.js"></script>
|
||||||
|
<script src="js/entities/entity.js"></script>
|
||||||
|
<script src="js/entities/rabbit.js"></script>
|
||||||
<script src="js/render.js"></script>
|
<script src="js/render.js"></script>
|
||||||
<script src="js/input.js"></script>
|
<script src="js/input.js"></script>
|
||||||
<script src="js/physics.js"></script>
|
<script src="js/physics.js"></script>
|
||||||
|
154
js/entities/entity.js
Normal file
154
js/entities/entity.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
141
js/entities/rabbit.js
Normal file
141
js/entities/rabbit.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,11 @@ function updatePhysics(timestamp) {
|
|||||||
updatePhysicsObjects();
|
updatePhysicsObjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update entities
|
||||||
|
if (typeof updateEntities === 'function') {
|
||||||
|
updateEntities();
|
||||||
|
}
|
||||||
|
|
||||||
// Get visible chunks
|
// Get visible chunks
|
||||||
const visibleChunks = getVisibleChunks();
|
const visibleChunks = getVisibleChunks();
|
||||||
|
|
||||||
|
@ -66,6 +66,11 @@ function render() {
|
|||||||
// Render physics objects
|
// Render physics objects
|
||||||
renderPhysicsObjects(ctx, worldOffsetX, worldOffsetY);
|
renderPhysicsObjects(ctx, worldOffsetX, worldOffsetY);
|
||||||
|
|
||||||
|
// Render entities
|
||||||
|
if (typeof renderEntities === 'function') {
|
||||||
|
renderEntities(ctx, worldOffsetX, worldOffsetY);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw cursor position and update debug info
|
// Draw cursor position and update debug info
|
||||||
if (currentMouseX !== undefined && currentMouseY !== undefined) {
|
if (currentMouseX !== undefined && currentMouseY !== undefined) {
|
||||||
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
|
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 532 B |
Loading…
x
Reference in New Issue
Block a user