Compare commits
47 Commits
ebb96846ed
...
b82f415f4f
Author | SHA1 | Date | |
---|---|---|---|
|
b82f415f4f | ||
|
80c243ebf8 | ||
|
d765defa9d | ||
|
755be2f5a4 | ||
|
c5b7f2f224 | ||
|
bb1e25e753 | ||
|
d2ab5094ab | ||
|
583544840b | ||
|
cb62097150 | ||
|
8e2575f6fc | ||
|
04295f9f9f | ||
|
0c8e13d630 | ||
|
f592c74412 | ||
|
84e08b397d | ||
|
c853738bbf | ||
|
0a13dfc0a3 | ||
|
afba547fce | ||
|
8562c86986 | ||
|
ad90b9320f | ||
|
85a96f153f | ||
|
db5b49ee7f | ||
|
d86baa8f99 | ||
|
f0b00c3ccb | ||
|
724c5907a1 | ||
|
288c4a8772 | ||
|
03b192ae0a | ||
|
da895b11df | ||
|
853da1a61d | ||
|
d2a4927577 | ||
|
90650fefdd | ||
|
61ee259f6b | ||
|
5ac6d205ad | ||
|
f1a18f9168 | ||
|
f9cb363b37 | ||
|
cf64b6db48 | ||
|
20f5036848 | ||
|
34dd7e2d62 | ||
|
2067dad1d3 | ||
|
7cabd79d5f | ||
|
349d38c04c | ||
|
b5d1a643bd | ||
|
b7d12114d3 | ||
|
a948fab619 | ||
|
bcd61b7433 | ||
|
bdab2974c6 | ||
|
032793292f | ||
|
ba4fa3eb37 |
@ -20,7 +20,12 @@
|
||||
<button id="tree-seed-btn">Tree Seed</button>
|
||||
<button id="fire-btn">Fire</button>
|
||||
<button id="lava-btn">Lava</button>
|
||||
<button id="rabbit-btn">Rabbit</button>
|
||||
<button id="square-btn">Square</button>
|
||||
<button id="circle-btn">Circle</button>
|
||||
<button id="triangle-btn">Triangle</button>
|
||||
<button id="eraser-btn">Eraser</button>
|
||||
<button id="spawn-player-btn">Spawn Player</button>
|
||||
</div>
|
||||
<div class="navigation">
|
||||
<button id="move-left">←</button>
|
||||
@ -44,6 +49,10 @@
|
||||
<script src="js/elements/plants.js"></script>
|
||||
<script src="js/elements/trees.js"></script>
|
||||
<script src="js/elements/fire.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/entities/player.js"></script>
|
||||
<script src="js/render.js"></script>
|
||||
<script src="js/input.js"></script>
|
||||
<script src="js/physics.js"></script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Game constants
|
||||
const CHUNK_SIZE = 200;
|
||||
const PIXEL_SIZE = 4;
|
||||
const GRAVITY = 0.5;
|
||||
let PIXEL_SIZE = 4;
|
||||
const GRAVITY = 1.5; // Increased gravity (3x stronger)
|
||||
const WATER_SPREAD = 3;
|
||||
|
||||
// Base Colors
|
||||
@ -60,6 +60,9 @@ const LEAF = 12;
|
||||
const FIRE = 13;
|
||||
const LAVA = 14;
|
||||
const RABBIT = 15;
|
||||
const SQUARE = 16;
|
||||
const CIRCLE = 17;
|
||||
const TRIANGLE = 18;
|
||||
|
||||
// Flammable materials
|
||||
const FLAMMABLE_MATERIALS = [GRASS, WOOD, SEED, GRASS_BLADE, FLOWER, TREE_SEED, LEAF];
|
||||
|
@ -1,11 +1,24 @@
|
||||
// Basic element behaviors (sand, water, dirt)
|
||||
function updateSand(x, y) {
|
||||
// Try to move down
|
||||
if (getPixel(x, y + 1) === EMPTY) {
|
||||
// Try to move down with stronger gravity (up to 5 pixels at once)
|
||||
let maxFall = 5;
|
||||
let newY = y;
|
||||
|
||||
// Check how far down we can fall
|
||||
for (let i = 1; i <= maxFall; i++) {
|
||||
if (getPixel(x, y + i) === EMPTY) {
|
||||
newY = y + i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newY > y) {
|
||||
// Fall straight down as far as possible
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x, y + 1, SAND);
|
||||
setPixel(x, newY, SAND);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try to move down-left or down-right
|
||||
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
@ -49,13 +62,26 @@ function updateWater(x, y) {
|
||||
setMetadata(x, y, metadata);
|
||||
}
|
||||
|
||||
// Try to move down
|
||||
if (getPixel(x, y + 1) === EMPTY) {
|
||||
// Try to move down with stronger gravity (up to 4 pixels at once)
|
||||
let maxFall = 4;
|
||||
let newY = y;
|
||||
|
||||
// Check how far down we can fall
|
||||
for (let i = 1; i <= maxFall; i++) {
|
||||
if (getPixel(x, y + i) === EMPTY) {
|
||||
newY = y + i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newY > y) {
|
||||
// Fall straight down as far as possible
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x, y + 1, WATER);
|
||||
moveMetadata(x, y, x, y + 1);
|
||||
setPixel(x, newY, WATER);
|
||||
moveMetadata(x, y, x, newY);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try to move down-left or down-right
|
||||
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
@ -128,12 +154,25 @@ function updateWater(x, y) {
|
||||
}
|
||||
|
||||
function updateDirt(x, y) {
|
||||
// Try to move down
|
||||
if (getPixel(x, y + 1) === EMPTY) {
|
||||
// Try to move down with stronger gravity (up to 5 pixels at once)
|
||||
let maxFall = 5;
|
||||
let newY = y;
|
||||
|
||||
// Check how far down we can fall
|
||||
for (let i = 1; i <= maxFall; i++) {
|
||||
if (getPixel(x, y + i) === EMPTY) {
|
||||
newY = y + i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newY > y) {
|
||||
// Fall straight down as far as possible
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x, y + 1, DIRT);
|
||||
setPixel(x, newY, DIRT);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try to move down-left or down-right
|
||||
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
|
265
js/elements/physics_objects.js
Normal file
@ -0,0 +1,265 @@
|
||||
// Physics objects (square, circle, triangle)
|
||||
// Constants are already defined in constants.js
|
||||
|
||||
// Physics object properties
|
||||
const PHYSICS_OBJECT_COLORS = ['#FF5733', '#33FF57', '#3357FF', '#F3FF33', '#FF33F3'];
|
||||
|
||||
// Physics constants
|
||||
const GRAVITY_ACCELERATION = 0.2;
|
||||
const BOUNCE_FACTOR = 0.7;
|
||||
const FRICTION = 0.98;
|
||||
const ROTATION_SPEED = 0.05;
|
||||
|
||||
// Store physics objects
|
||||
const physicsObjects = [];
|
||||
|
||||
// Physics object class
|
||||
class PhysicsObject {
|
||||
constructor(type, x, y, size) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.size = size || 10;
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
this.rotation = 0;
|
||||
this.angularVelocity = (Math.random() - 0.5) * ROTATION_SPEED;
|
||||
this.color = PHYSICS_OBJECT_COLORS[Math.floor(Math.random() * PHYSICS_OBJECT_COLORS.length)];
|
||||
this.isStatic = false;
|
||||
this.lastUpdate = performance.now();
|
||||
}
|
||||
|
||||
update() {
|
||||
const now = performance.now();
|
||||
const deltaTime = Math.min(50, now - this.lastUpdate); // Cap at 50ms to prevent huge jumps
|
||||
this.lastUpdate = now;
|
||||
|
||||
if (this.isStatic) return false;
|
||||
|
||||
// Apply gravity
|
||||
this.vy += GRAVITY_ACCELERATION;
|
||||
|
||||
// Calculate new position
|
||||
const newX = this.x + this.vx;
|
||||
const newY = this.y + this.vy;
|
||||
|
||||
// Check for collisions with world elements
|
||||
const collisionResult = this.checkCollisions(newX, newY);
|
||||
|
||||
if (collisionResult.collision) {
|
||||
// Handle collision response
|
||||
if (collisionResult.horizontal) {
|
||||
this.vx *= -BOUNCE_FACTOR;
|
||||
}
|
||||
if (collisionResult.vertical) {
|
||||
this.vy *= -BOUNCE_FACTOR;
|
||||
}
|
||||
|
||||
// Apply friction
|
||||
this.vx *= FRICTION;
|
||||
this.vy *= FRICTION;
|
||||
|
||||
// If object is almost stopped, make it static
|
||||
if (Math.abs(this.vx) < 0.1 && Math.abs(this.vy) < 0.1 && Math.abs(this.angularVelocity) < 0.01) {
|
||||
this.isStatic = true;
|
||||
}
|
||||
} else {
|
||||
// No collision, update position
|
||||
this.x = newX;
|
||||
this.y = newY;
|
||||
}
|
||||
|
||||
// Update rotation
|
||||
this.rotation += this.angularVelocity;
|
||||
this.angularVelocity *= FRICTION;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
checkCollisions(newX, newY) {
|
||||
const result = {
|
||||
collision: false,
|
||||
horizontal: false,
|
||||
vertical: false
|
||||
};
|
||||
|
||||
// Check points around the object based on its type
|
||||
const checkPoints = this.getCollisionCheckPoints(newX, newY);
|
||||
|
||||
for (const point of checkPoints) {
|
||||
const pixel = getPixel(Math.floor(point.x), Math.floor(point.y));
|
||||
if (pixel !== EMPTY && pixel !== WATER &&
|
||||
pixel !== SQUARE && pixel !== CIRCLE && pixel !== TRIANGLE) {
|
||||
result.collision = true;
|
||||
|
||||
// Determine collision direction
|
||||
if (point.type === 'horizontal') {
|
||||
result.horizontal = true;
|
||||
} else if (point.type === 'vertical') {
|
||||
result.vertical = true;
|
||||
} else {
|
||||
// Corner collision, check both directions
|
||||
result.horizontal = true;
|
||||
result.vertical = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getCollisionCheckPoints(x, y) {
|
||||
const points = [];
|
||||
const halfSize = this.size / 2;
|
||||
|
||||
if (this.type === SQUARE) {
|
||||
// For a square, check corners and edges
|
||||
const corners = [
|
||||
{ x: x - halfSize, y: y - halfSize },
|
||||
{ x: x + halfSize, y: y - halfSize },
|
||||
{ x: x - halfSize, y: y + halfSize },
|
||||
{ x: x + halfSize, y: y + halfSize }
|
||||
];
|
||||
|
||||
// Add rotated corners
|
||||
for (const corner of corners) {
|
||||
const rotatedCorner = this.rotatePoint(corner.x, corner.y, x, y, this.rotation);
|
||||
points.push({ x: rotatedCorner.x, y: rotatedCorner.y, type: 'corner' });
|
||||
}
|
||||
|
||||
// Add edge midpoints
|
||||
points.push({ x: x, y: y - halfSize, type: 'vertical' });
|
||||
points.push({ x: x, y: y + halfSize, type: 'vertical' });
|
||||
points.push({ x: x - halfSize, y: y, type: 'horizontal' });
|
||||
points.push({ x: x + halfSize, y: y, type: 'horizontal' });
|
||||
|
||||
} else if (this.type === CIRCLE) {
|
||||
// For a circle, check points around the circumference
|
||||
const numPoints = 12;
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const angle = (i / numPoints) * Math.PI * 2;
|
||||
points.push({
|
||||
x: x + Math.cos(angle) * halfSize,
|
||||
y: y + Math.sin(angle) * halfSize,
|
||||
type: angle < Math.PI / 4 || angle > Math.PI * 7/4 || (angle > Math.PI * 3/4 && angle < Math.PI * 5/4) ? 'horizontal' : 'vertical'
|
||||
});
|
||||
}
|
||||
|
||||
} else if (this.type === TRIANGLE) {
|
||||
// For a triangle, check vertices and edges
|
||||
const vertices = [
|
||||
{ x: x, y: y - halfSize },
|
||||
{ x: x - halfSize, y: y + halfSize },
|
||||
{ x: x + halfSize, y: y + halfSize }
|
||||
];
|
||||
|
||||
// Add rotated vertices
|
||||
for (const vertex of vertices) {
|
||||
const rotatedVertex = this.rotatePoint(vertex.x, vertex.y, x, y, this.rotation);
|
||||
points.push({ x: rotatedVertex.x, y: rotatedVertex.y, type: 'corner' });
|
||||
}
|
||||
|
||||
// Add edge midpoints
|
||||
const midpoints = [
|
||||
{ x: (vertices[0].x + vertices[1].x) / 2, y: (vertices[0].y + vertices[1].y) / 2, type: 'edge' },
|
||||
{ x: (vertices[1].x + vertices[2].x) / 2, y: (vertices[1].y + vertices[2].y) / 2, type: 'horizontal' },
|
||||
{ x: (vertices[2].x + vertices[0].x) / 2, y: (vertices[2].y + vertices[0].y) / 2, type: 'edge' }
|
||||
];
|
||||
|
||||
for (const midpoint of midpoints) {
|
||||
const rotatedMidpoint = this.rotatePoint(midpoint.x, midpoint.y, x, y, this.rotation);
|
||||
points.push({ x: rotatedMidpoint.x, y: rotatedMidpoint.y, type: midpoint.type });
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
rotatePoint(px, py, cx, cy, angle) {
|
||||
const s = Math.sin(angle);
|
||||
const c = Math.cos(angle);
|
||||
|
||||
// Translate point back to origin
|
||||
px -= cx;
|
||||
py -= cy;
|
||||
|
||||
// Rotate point
|
||||
const xnew = px * c - py * s;
|
||||
const ynew = px * s + py * c;
|
||||
|
||||
// Translate point back
|
||||
return {
|
||||
x: xnew + cx,
|
||||
y: ynew + cy
|
||||
};
|
||||
}
|
||||
|
||||
render(ctx, offsetX, offsetY) {
|
||||
const screenX = (this.x - offsetX) * PIXEL_SIZE;
|
||||
const screenY = (this.y - offsetY) * PIXEL_SIZE;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(screenX, screenY);
|
||||
ctx.rotate(this.rotation);
|
||||
ctx.fillStyle = this.color;
|
||||
|
||||
// Draw collision box in debug mode
|
||||
if (debugMode) {
|
||||
ctx.strokeStyle = '#ff0000';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
// Draw a circle for the collision radius
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size * PIXEL_SIZE / 2, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw a dot at the center
|
||||
ctx.fillStyle = '#ffff00';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Restore original fill color
|
||||
ctx.fillStyle = this.color;
|
||||
}
|
||||
|
||||
if (this.type === SQUARE) {
|
||||
const halfSize = this.size / 2 * PIXEL_SIZE;
|
||||
ctx.fillRect(-halfSize, -halfSize, this.size * PIXEL_SIZE, this.size * PIXEL_SIZE);
|
||||
} else if (this.type === CIRCLE) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size / 2 * PIXEL_SIZE, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (this.type === TRIANGLE) {
|
||||
const halfSize = this.size / 2 * PIXEL_SIZE;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -halfSize);
|
||||
ctx.lineTo(-halfSize, halfSize);
|
||||
ctx.lineTo(halfSize, halfSize);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function createPhysicsObject(type, x, y, size) {
|
||||
const obj = new PhysicsObject(type, x, y, size || 10);
|
||||
physicsObjects.push(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function updatePhysicsObjects() {
|
||||
// Update all physics objects
|
||||
for (let i = physicsObjects.length - 1; i >= 0; i--) {
|
||||
physicsObjects[i].update();
|
||||
}
|
||||
}
|
||||
|
||||
function renderPhysicsObjects(ctx, offsetX, offsetY) {
|
||||
// Render all physics objects
|
||||
for (const obj of physicsObjects) {
|
||||
obj.render(ctx, offsetX, offsetY);
|
||||
}
|
||||
}
|
@ -1,11 +1,25 @@
|
||||
// Plant element behaviors (grass, seeds, trees)
|
||||
function updateGrass(x, y) {
|
||||
// Grass behaves like dirt for physics
|
||||
if (getPixel(x, y + 1) === EMPTY) {
|
||||
// Grass behaves like dirt for physics with stronger gravity
|
||||
let maxFall = 5;
|
||||
let newY = y;
|
||||
|
||||
// Check how far down we can fall
|
||||
for (let i = 1; i <= maxFall; i++) {
|
||||
if (getPixel(x, y + i) === EMPTY) {
|
||||
newY = y + i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newY > y) {
|
||||
// Fall straight down as far as possible
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x, y + 1, GRASS);
|
||||
setPixel(x, newY, GRASS);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try to move down-left or down-right
|
||||
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x - 1, y + 1, GRASS);
|
||||
@ -46,13 +60,27 @@ function updateGrass(x, y) {
|
||||
}
|
||||
|
||||
function updateSeed(x, y) {
|
||||
// Seeds fall like sand
|
||||
if (getPixel(x, y + 1) === EMPTY) {
|
||||
// Seeds fall like sand with stronger gravity
|
||||
let maxFall = 5;
|
||||
let newY = y;
|
||||
|
||||
// Check how far down we can fall
|
||||
for (let i = 1; i <= maxFall; i++) {
|
||||
if (getPixel(x, y + i) === EMPTY) {
|
||||
newY = y + i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newY > y) {
|
||||
// Fall straight down as far as possible
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x, y + 1, SEED);
|
||||
moveMetadata(x, y, x, y + 1);
|
||||
setPixel(x, newY, SEED);
|
||||
moveMetadata(x, y, x, newY);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try to move down-left or down-right
|
||||
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x - 1, y + 1, SEED);
|
||||
|
@ -1,12 +1,26 @@
|
||||
// Tree element behaviors
|
||||
function updateTreeSeed(x, y) {
|
||||
// Tree seeds fall like other seeds
|
||||
if (getPixel(x, y + 1) === EMPTY) {
|
||||
// Tree seeds fall like other seeds with stronger gravity
|
||||
let maxFall = 5;
|
||||
let newY = y;
|
||||
|
||||
// Check how far down we can fall
|
||||
for (let i = 1; i <= maxFall; i++) {
|
||||
if (getPixel(x, y + i) === EMPTY) {
|
||||
newY = y + i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newY > y) {
|
||||
// Fall straight down as far as possible
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x, y + 1, TREE_SEED);
|
||||
moveMetadata(x, y, x, y + 1);
|
||||
setPixel(x, newY, TREE_SEED);
|
||||
moveMetadata(x, y, x, newY);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try to move down-left or down-right
|
||||
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
||||
setPixel(x, y, EMPTY);
|
||||
setPixel(x - 1, y + 1, TREE_SEED);
|
||||
|
207
js/entities/entity.js
Normal file
@ -0,0 +1,207 @@
|
||||
// Base entity system
|
||||
const ENTITY_TYPES = {
|
||||
RABBIT: 'rabbit',
|
||||
PLAYER: 'player'
|
||||
};
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
||||
|
||||
// Draw collision box in debug mode
|
||||
if (debugMode) {
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(
|
||||
-this.width/2 * PIXEL_SIZE,
|
||||
-this.height/2 * PIXEL_SIZE,
|
||||
this.width * PIXEL_SIZE,
|
||||
this.height * PIXEL_SIZE
|
||||
);
|
||||
|
||||
// Draw a dot at the entity's center
|
||||
ctx.fillStyle = '#ffff00';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
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 - check multiple points along the bottom
|
||||
const numBottomPoints = 5;
|
||||
let groundCollision = false;
|
||||
|
||||
// For player entity, adjust the collision detection to match sprite feet position
|
||||
const yOffset = this.type === ENTITY_TYPES.PLAYER ? 2 : 0;
|
||||
|
||||
for (let i = 0; i < numBottomPoints; i++) {
|
||||
const ratio = i / (numBottomPoints - 1);
|
||||
const bottomX = newX - halfWidth + (2 * halfWidth * ratio);
|
||||
const bottomY = newY + halfHeight + yOffset;
|
||||
|
||||
if (this.isPixelSolid(bottomX, bottomY)) {
|
||||
groundCollision = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (groundCollision) {
|
||||
result.collision = true;
|
||||
result.vertical = true;
|
||||
result.ground = true;
|
||||
}
|
||||
|
||||
// Check side points for horizontal collision
|
||||
// For player entity, adjust the collision detection to match sprite position
|
||||
const yAdjust = this.type === ENTITY_TYPES.PLAYER ? -1 : 0;
|
||||
|
||||
const leftMiddle = { x: newX - halfWidth, y: newY + yAdjust };
|
||||
const rightMiddle = { x: newX + halfWidth, y: newY + yAdjust };
|
||||
|
||||
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 + (this.type === ENTITY_TYPES.PLAYER ? -2 : 0) };
|
||||
if (this.isPixelSolid(topMiddle.x, topMiddle.y)) {
|
||||
result.collision = true;
|
||||
result.vertical = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
isPixelSolid(x, y) {
|
||||
// Use ceiling for y coordinate to better detect ground below
|
||||
const pixel = getPixel(Math.floor(x), Math.ceil(y));
|
||||
|
||||
// For player entity, don't collide with trees (WOOD and LEAF)
|
||||
if (this.type === ENTITY_TYPES.PLAYER) {
|
||||
return pixel !== EMPTY &&
|
||||
pixel !== WATER &&
|
||||
pixel !== FIRE &&
|
||||
pixel !== SQUARE &&
|
||||
pixel !== CIRCLE &&
|
||||
pixel !== TRIANGLE &&
|
||||
pixel !== WOOD &&
|
||||
pixel !== LEAF;
|
||||
}
|
||||
|
||||
// For other entities, use the original collision detection
|
||||
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;
|
||||
case ENTITY_TYPES.PLAYER:
|
||||
entity = new Player(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);
|
||||
}
|
||||
}
|
543
js/entities/player.js
Normal file
@ -0,0 +1,543 @@
|
||||
// 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/player.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
|
||||
|
||||
// Player stats
|
||||
this.maxHealth = 100;
|
||||
this.health = 100;
|
||||
this.breakingPower = 1;
|
||||
this.breakingRange = 10; // Increased from 3 to 10 pixels
|
||||
this.isBreaking = false;
|
||||
this.breakingCooldown = 0;
|
||||
this.breakingCooldownMax = 10;
|
||||
|
||||
// Inventory
|
||||
this.inventory = {
|
||||
sand: 0,
|
||||
water: 0,
|
||||
dirt: 0,
|
||||
stone: 0,
|
||||
wood: 0,
|
||||
grass: 0,
|
||||
seed: 0
|
||||
};
|
||||
|
||||
// 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 breaking cooldown
|
||||
if (this.breakingCooldown > 0) {
|
||||
this.breakingCooldown--;
|
||||
}
|
||||
|
||||
// Handle breaking action
|
||||
if (this.isBreaking && this.breakingCooldown <= 0) {
|
||||
this.breakBlock();
|
||||
this.breakingCooldown = this.breakingCooldownMax;
|
||||
}
|
||||
|
||||
// Update animation
|
||||
this.updateAnimation(deltaTime);
|
||||
|
||||
// Center camera on player
|
||||
this.centerCamera();
|
||||
|
||||
// Update HUD
|
||||
this.updateHUD();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
startBreaking() {
|
||||
this.isBreaking = true;
|
||||
}
|
||||
|
||||
stopBreaking() {
|
||||
this.isBreaking = false;
|
||||
}
|
||||
|
||||
breakBlock() {
|
||||
// Get mouse position in world coordinates
|
||||
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
|
||||
const worldY = Math.floor(currentMouseY / PIXEL_SIZE) + worldOffsetY;
|
||||
|
||||
// Calculate distance from player to target block
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(worldX - this.x, 2) +
|
||||
Math.pow(worldY - this.y, 2)
|
||||
);
|
||||
|
||||
// Only break blocks within range
|
||||
if (distance <= this.breakingRange) {
|
||||
// Get the block type at that position
|
||||
const blockType = getPixel(worldX, worldY);
|
||||
|
||||
// Only break non-empty blocks that aren't special entities
|
||||
if (blockType !== EMPTY &&
|
||||
blockType !== WATER &&
|
||||
blockType !== FIRE &&
|
||||
blockType !== SQUARE &&
|
||||
blockType !== CIRCLE &&
|
||||
blockType !== TRIANGLE) {
|
||||
|
||||
// Add to inventory based on block type
|
||||
this.addToInventory(blockType);
|
||||
|
||||
// Replace with empty space
|
||||
setPixel(worldX, worldY, EMPTY);
|
||||
|
||||
// Create a breaking effect (particles)
|
||||
this.createBreakingEffect(worldX, worldY, blockType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addToInventory(blockType) {
|
||||
// Map block type to inventory item
|
||||
switch(blockType) {
|
||||
case SAND:
|
||||
this.inventory.sand++;
|
||||
break;
|
||||
case DIRT:
|
||||
this.inventory.dirt++;
|
||||
break;
|
||||
case STONE:
|
||||
this.inventory.stone++;
|
||||
break;
|
||||
case GRASS:
|
||||
this.inventory.grass++;
|
||||
break;
|
||||
case WOOD:
|
||||
this.inventory.wood++;
|
||||
break;
|
||||
case SEED:
|
||||
case TREE_SEED:
|
||||
this.inventory.seed++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
createBreakingEffect(x, y, blockType) {
|
||||
// Create a simple particle effect at the breaking location
|
||||
// This could be expanded with a proper particle system
|
||||
const numParticles = 5;
|
||||
|
||||
// For now, we'll just create a visual feedback by setting nearby pixels
|
||||
// to a different color briefly, then clearing them
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
|
||||
// Skip if the pixel is not empty
|
||||
if (getPixel(x + dx, y + dy) !== EMPTY) continue;
|
||||
|
||||
// Set a temporary pixel
|
||||
setPixel(x + dx, y + dy, EMPTY);
|
||||
|
||||
// Mark the chunk as dirty for rendering
|
||||
const { chunkX, chunkY } = getChunkCoordinates(x + dx, y + dy);
|
||||
const key = getChunkKey(chunkX, chunkY);
|
||||
dirtyChunks.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateHUD() {
|
||||
// Get or create the HUD container
|
||||
let hudContainer = document.getElementById('player-hud');
|
||||
if (!hudContainer) {
|
||||
hudContainer = document.createElement('div');
|
||||
hudContainer.id = 'player-hud';
|
||||
hudContainer.style.position = 'fixed';
|
||||
hudContainer.style.bottom = '10px';
|
||||
hudContainer.style.left = '10px';
|
||||
hudContainer.style.width = '300px';
|
||||
hudContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
|
||||
hudContainer.style.color = 'white';
|
||||
hudContainer.style.padding = '10px';
|
||||
hudContainer.style.borderRadius = '5px';
|
||||
hudContainer.style.fontFamily = 'Arial, sans-serif';
|
||||
hudContainer.style.zIndex = '1000';
|
||||
document.body.appendChild(hudContainer);
|
||||
|
||||
// Create health bar container
|
||||
const healthBarContainer = document.createElement('div');
|
||||
healthBarContainer.id = 'health-bar-container';
|
||||
healthBarContainer.style.width = '100%';
|
||||
healthBarContainer.style.height = '20px';
|
||||
healthBarContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.3)';
|
||||
healthBarContainer.style.marginBottom = '10px';
|
||||
healthBarContainer.style.borderRadius = '3px';
|
||||
|
||||
// Create health bar
|
||||
const healthBar = document.createElement('div');
|
||||
healthBar.id = 'health-bar';
|
||||
healthBar.style.width = '100%';
|
||||
healthBar.style.height = '100%';
|
||||
healthBar.style.backgroundColor = '#4CAF50';
|
||||
healthBar.style.borderRadius = '3px';
|
||||
healthBar.style.transition = 'width 0.3s';
|
||||
|
||||
healthBarContainer.appendChild(healthBar);
|
||||
hudContainer.appendChild(healthBarContainer);
|
||||
|
||||
// Create inventory container
|
||||
const inventoryContainer = document.createElement('div');
|
||||
inventoryContainer.id = 'inventory-container';
|
||||
inventoryContainer.style.display = 'grid';
|
||||
inventoryContainer.style.gridTemplateColumns = 'repeat(7, 1fr)';
|
||||
inventoryContainer.style.gap = '5px';
|
||||
|
||||
// Create inventory slots
|
||||
const inventoryItems = ['sand', 'dirt', 'stone', 'grass', 'wood', 'water', 'seed'];
|
||||
inventoryItems.forEach(item => {
|
||||
const slot = document.createElement('div');
|
||||
slot.id = `inventory-${item}`;
|
||||
slot.className = 'inventory-slot';
|
||||
slot.style.width = '30px';
|
||||
slot.style.height = '30px';
|
||||
slot.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';
|
||||
slot.style.borderRadius = '3px';
|
||||
slot.style.display = 'flex';
|
||||
slot.style.flexDirection = 'column';
|
||||
slot.style.alignItems = 'center';
|
||||
slot.style.justifyContent = 'center';
|
||||
slot.style.fontSize = '10px';
|
||||
slot.style.position = 'relative';
|
||||
|
||||
// Create item icon
|
||||
const icon = document.createElement('div');
|
||||
icon.style.width = '20px';
|
||||
icon.style.height = '20px';
|
||||
icon.style.borderRadius = '3px';
|
||||
|
||||
// Set color based on item type
|
||||
switch(item) {
|
||||
case 'sand': icon.style.backgroundColor = '#e6c588'; break;
|
||||
case 'dirt': icon.style.backgroundColor = '#8B4513'; break;
|
||||
case 'stone': icon.style.backgroundColor = '#A9A9A9'; break;
|
||||
case 'grass': icon.style.backgroundColor = '#7CFC00'; break;
|
||||
case 'wood': icon.style.backgroundColor = '#8B5A2B'; break;
|
||||
case 'water': icon.style.backgroundColor = '#4a80f5'; break;
|
||||
case 'seed': icon.style.backgroundColor = '#654321'; break;
|
||||
}
|
||||
|
||||
// Create count label
|
||||
const count = document.createElement('div');
|
||||
count.id = `${item}-count`;
|
||||
count.style.position = 'absolute';
|
||||
count.style.bottom = '2px';
|
||||
count.style.right = '2px';
|
||||
count.style.fontSize = '8px';
|
||||
count.style.fontWeight = 'bold';
|
||||
count.textContent = '0';
|
||||
|
||||
slot.appendChild(icon);
|
||||
slot.appendChild(count);
|
||||
inventoryContainer.appendChild(slot);
|
||||
});
|
||||
|
||||
hudContainer.appendChild(inventoryContainer);
|
||||
|
||||
// Create controls help text
|
||||
const controlsHelp = document.createElement('div');
|
||||
controlsHelp.style.marginTop = '10px';
|
||||
controlsHelp.style.fontSize = '10px';
|
||||
controlsHelp.style.color = '#aaa';
|
||||
controlsHelp.innerHTML = 'Controls: A/D - Move, W/Space - Jump, E - Break blocks';
|
||||
|
||||
hudContainer.appendChild(controlsHelp);
|
||||
}
|
||||
|
||||
// Update health bar
|
||||
const healthBar = document.getElementById('health-bar');
|
||||
if (healthBar) {
|
||||
const healthPercent = (this.health / this.maxHealth) * 100;
|
||||
healthBar.style.width = `${healthPercent}%`;
|
||||
|
||||
// Change color based on health
|
||||
if (healthPercent > 60) {
|
||||
healthBar.style.backgroundColor = '#4CAF50'; // Green
|
||||
} else if (healthPercent > 30) {
|
||||
healthBar.style.backgroundColor = '#FFC107'; // Yellow
|
||||
} else {
|
||||
healthBar.style.backgroundColor = '#F44336'; // Red
|
||||
}
|
||||
}
|
||||
|
||||
// Update inventory counts
|
||||
for (const [item, count] of Object.entries(this.inventory)) {
|
||||
const countElement = document.getElementById(`${item}-count`);
|
||||
if (countElement) {
|
||||
countElement.textContent = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.2; // Move when player is 30% away from center
|
||||
const thresholdY = cameraHeight * 0.2;
|
||||
|
||||
// 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.2;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
148
js/entities/rabbit.js
Normal file
@ -0,0 +1,148 @@
|
||||
// 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.5; // Position just above ground (reduced from 0.99)
|
||||
} 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 but only flip the sprite, not rotate it
|
||||
this.flipped = this.direction < 0;
|
||||
|
||||
// Only apply rotation when jumping
|
||||
if (this.isJumping) {
|
||||
this.rotation = this.direction < 0 ? -0.2 : 0.2;
|
||||
} else {
|
||||
this.rotation = 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.5) { // Increased from 0.2 to 0.5 for more frequent jumping
|
||||
// Jump
|
||||
this.jump();
|
||||
} else if (decision < 0.8) { // Adjusted range
|
||||
// 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);
|
||||
}
|
||||
}
|
112
js/input.js
@ -4,6 +4,58 @@ let isDragging = false;
|
||||
let lastMouseX, lastMouseY;
|
||||
let currentMouseX, currentMouseY;
|
||||
|
||||
// Keyboard state tracking
|
||||
const keyState = {};
|
||||
let player = null;
|
||||
|
||||
// Handle keyboard input for player movement
|
||||
window.addEventListener('keydown', (e) => {
|
||||
keyState[e.code] = true;
|
||||
|
||||
// Toggle debug mode with F3
|
||||
if (e.code === 'F3') {
|
||||
toggleDebug();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Start breaking blocks with E key or left mouse button
|
||||
if (e.code === 'KeyE' && player) {
|
||||
player.startBreaking();
|
||||
}
|
||||
|
||||
// Prevent default behavior for game control keys
|
||||
if (['KeyW', 'KeyA', 'KeyS', 'KeyD', 'Space', 'KeyE'].includes(e.code)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', (e) => {
|
||||
keyState[e.code] = false;
|
||||
|
||||
// Stop breaking blocks when E key is released
|
||||
if (e.code === 'KeyE' && player) {
|
||||
player.stopBreaking();
|
||||
}
|
||||
});
|
||||
|
||||
function updatePlayerMovement() {
|
||||
if (!player) return;
|
||||
|
||||
// Reset movement flag
|
||||
player.stopMoving();
|
||||
|
||||
// Handle movement
|
||||
if (keyState['KeyA']) {
|
||||
player.moveLeft();
|
||||
}
|
||||
if (keyState['KeyD']) {
|
||||
player.moveRight();
|
||||
}
|
||||
if (keyState['KeyW'] || keyState['Space']) {
|
||||
player.jump();
|
||||
}
|
||||
}
|
||||
|
||||
function setTool(tool) {
|
||||
currentTool = tool;
|
||||
document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active'));
|
||||
@ -28,6 +80,14 @@ function setTool(tool) {
|
||||
document.getElementById('fire-btn').classList.add('active');
|
||||
} else if (tool === LAVA) {
|
||||
document.getElementById('lava-btn').classList.add('active');
|
||||
} else if (tool === RABBIT) {
|
||||
document.getElementById('rabbit-btn').classList.add('active');
|
||||
} else if (tool === SQUARE) {
|
||||
document.getElementById('square-btn').classList.add('active');
|
||||
} else if (tool === CIRCLE) {
|
||||
document.getElementById('circle-btn').classList.add('active');
|
||||
} else if (tool === TRIANGLE) {
|
||||
document.getElementById('triangle-btn').classList.add('active');
|
||||
} else if (tool === EMPTY) {
|
||||
document.getElementById('eraser-btn').classList.add('active');
|
||||
}
|
||||
@ -46,9 +106,15 @@ function handleMouseDown(e) {
|
||||
worldOffsetXBeforeDrag = worldOffsetX;
|
||||
worldOffsetYBeforeDrag = worldOffsetY;
|
||||
} else {
|
||||
// Left mouse button for drawing
|
||||
isDrawing = true;
|
||||
draw(x, y);
|
||||
// Left mouse button for drawing or breaking blocks
|
||||
if (player) {
|
||||
// If player exists, start breaking blocks
|
||||
player.startBreaking();
|
||||
} else {
|
||||
// Otherwise use normal drawing
|
||||
isDrawing = true;
|
||||
draw(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +155,11 @@ function handleMouseUp(e) {
|
||||
}
|
||||
}
|
||||
isDragging = false;
|
||||
|
||||
// Stop breaking blocks if player exists
|
||||
if (player) {
|
||||
player.stopBreaking();
|
||||
}
|
||||
}
|
||||
|
||||
function draw(x, y) {
|
||||
@ -98,6 +169,14 @@ function draw(x, y) {
|
||||
const worldX = Math.floor(x / PIXEL_SIZE) + worldOffsetX;
|
||||
const worldY = Math.floor(y / PIXEL_SIZE) + worldOffsetY;
|
||||
|
||||
// Special handling for physics objects
|
||||
if (currentTool === SQUARE || currentTool === CIRCLE || currentTool === TRIANGLE) {
|
||||
// Create a physics object at the cursor position
|
||||
const size = 10; // Default size
|
||||
createPhysicsObject(currentTool, worldX, worldY, size);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw a small brush (3x3)
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
@ -114,6 +193,11 @@ function draw(x, y) {
|
||||
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
|
||||
});
|
||||
}
|
||||
}
|
||||
// Special handling for rabbits - create rabbit entity
|
||||
else if (currentTool === RABBIT) {
|
||||
createEntity(ENTITY_TYPES.RABBIT, pixelX, pixelY);
|
||||
return; // Only create one rabbit per click
|
||||
} else {
|
||||
setPixel(pixelX, pixelY, currentTool);
|
||||
|
||||
@ -185,4 +269,26 @@ function handleTouchMove(e) {
|
||||
function toggleDebug() {
|
||||
debugMode = !debugMode;
|
||||
document.getElementById('debug-btn').classList.toggle('active');
|
||||
|
||||
// Update UI to show debug mode is active
|
||||
if (debugMode) {
|
||||
// Show a temporary notification
|
||||
const notification = document.createElement('div');
|
||||
notification.textContent = 'Debug Mode: ON';
|
||||
notification.style.position = 'fixed';
|
||||
notification.style.top = '10px';
|
||||
notification.style.left = '50%';
|
||||
notification.style.transform = 'translateX(-50%)';
|
||||
notification.style.backgroundColor = 'rgba(0, 255, 0, 0.7)';
|
||||
notification.style.color = 'white';
|
||||
notification.style.padding = '10px 20px';
|
||||
notification.style.borderRadius = '5px';
|
||||
notification.style.zIndex = '1000';
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Remove notification after 2 seconds
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
76
js/main.js
@ -5,6 +5,15 @@ let lastFrameTime = 0;
|
||||
let fps = 0;
|
||||
let debugMode = false;
|
||||
|
||||
// Sky background variables
|
||||
const SKY_COLORS = [
|
||||
'#4a90e2', // Brighter blue
|
||||
'#74b9ff', // Light blue
|
||||
'#81ecec', // Very light blue/cyan
|
||||
];
|
||||
let skyAnimationTime = 0;
|
||||
let skyAnimationSpeed = 0.0005; // Controls how fast the sky position animates (not color)
|
||||
|
||||
// Initialize the simulation
|
||||
window.onload = function() {
|
||||
canvas = document.getElementById('simulation-canvas');
|
||||
@ -25,8 +34,15 @@ window.onload = function() {
|
||||
document.getElementById('tree-seed-btn').addEventListener('click', () => setTool(TREE_SEED));
|
||||
document.getElementById('fire-btn').addEventListener('click', () => setTool(FIRE));
|
||||
document.getElementById('lava-btn').addEventListener('click', () => setTool(LAVA));
|
||||
document.getElementById('rabbit-btn').addEventListener('click', () => setTool(RABBIT));
|
||||
document.getElementById('square-btn').addEventListener('click', () => setTool(SQUARE));
|
||||
document.getElementById('circle-btn').addEventListener('click', () => setTool(CIRCLE));
|
||||
document.getElementById('triangle-btn').addEventListener('click', () => setTool(TRIANGLE));
|
||||
document.getElementById('eraser-btn').addEventListener('click', () => setTool(EMPTY));
|
||||
|
||||
// Add player spawn button
|
||||
document.getElementById('spawn-player-btn').addEventListener('click', spawnPlayer);
|
||||
|
||||
// Navigation controls
|
||||
document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0));
|
||||
document.getElementById('move-right').addEventListener('click', () => moveWorld(CHUNK_SIZE/2, 0));
|
||||
@ -61,6 +77,11 @@ window.onload = function() {
|
||||
|
||||
// Start the simulation loop
|
||||
requestAnimationFrame(simulationLoop);
|
||||
|
||||
// Initialize physics variables
|
||||
window.physicsUpdateRate = 16; // ms between physics updates
|
||||
window.lastPhysicsTime = 0;
|
||||
window.fireUpdateCounter = 0;
|
||||
};
|
||||
|
||||
function resizeCanvas() {
|
||||
@ -68,6 +89,50 @@ function resizeCanvas() {
|
||||
canvas.height = window.innerHeight - document.querySelector('.controls').offsetHeight;
|
||||
}
|
||||
|
||||
// Function to spawn player
|
||||
function spawnPlayer() {
|
||||
// Hide HUD elements
|
||||
document.querySelector('.controls').style.display = 'none';
|
||||
|
||||
// Resize canvas to full screen first
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
// Set zoom level to 50% more zoomed in (from default 4 to 6)
|
||||
PIXEL_SIZE = 6;
|
||||
|
||||
// Create player at specified coordinates
|
||||
// Position adjusted for proper sprite alignment with smaller sprite
|
||||
player = createEntity(ENTITY_TYPES.PLAYER, 229, 40); // Adjusted Y position for smaller player
|
||||
|
||||
// Focus camera on player
|
||||
worldOffsetX = player.x - (canvas.width / PIXEL_SIZE / 2);
|
||||
worldOffsetY = player.y - (canvas.height / PIXEL_SIZE / 2);
|
||||
worldMoved = true;
|
||||
|
||||
// Clear chunk cache to force redraw at new zoom level
|
||||
chunkCanvasCache.clear();
|
||||
|
||||
// Remove the event listener for the spawn button to prevent multiple spawns
|
||||
document.getElementById('spawn-player-btn').removeEventListener('click', spawnPlayer);
|
||||
|
||||
// Create CSS for player HUD
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#player-hud {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.inventory-slot {
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
.inventory-slot:hover {
|
||||
transform: scale(1.1);
|
||||
background-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function simulationLoop(timestamp) {
|
||||
// Calculate FPS
|
||||
const deltaTime = timestamp - lastFrameTime;
|
||||
@ -75,11 +140,18 @@ function simulationLoop(timestamp) {
|
||||
fps = Math.round(1000 / deltaTime);
|
||||
document.getElementById('fps').textContent = `FPS: ${fps}`;
|
||||
|
||||
// Update player movement if player exists
|
||||
if (player) {
|
||||
updatePlayerMovement();
|
||||
}
|
||||
|
||||
// Update physics with timestamp for rate limiting
|
||||
updatePhysics(timestamp);
|
||||
|
||||
// Render
|
||||
render();
|
||||
// Render - skip rendering if FPS is too low to prevent death spiral
|
||||
if (fps > 10 || timestamp % 3 < 1) {
|
||||
render();
|
||||
}
|
||||
|
||||
// Memory management: Clean up chunk cache for chunks that are far away
|
||||
if (timestamp % 5000 < 16) { // Run every ~5 seconds
|
||||
|
@ -7,6 +7,16 @@ function updatePhysics(timestamp) {
|
||||
|
||||
lastPhysicsTime = timestamp || 0;
|
||||
|
||||
// Update physics objects
|
||||
if (typeof updatePhysicsObjects === 'function') {
|
||||
updatePhysicsObjects();
|
||||
}
|
||||
|
||||
// Update entities
|
||||
if (typeof updateEntities === 'function') {
|
||||
updateEntities();
|
||||
}
|
||||
|
||||
// Get visible chunks
|
||||
const visibleChunks = getVisibleChunks();
|
||||
|
||||
|
134
js/render.js
@ -6,8 +6,19 @@ function render() {
|
||||
// Clear the canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Get visible chunks
|
||||
const visibleChunks = getVisibleChunks();
|
||||
// Set pixelated rendering for the entire canvas
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
// Draw animated sky background
|
||||
renderSky();
|
||||
|
||||
// Display FPS in debug mode
|
||||
if (debugMode) {
|
||||
displayDebugInfo();
|
||||
}
|
||||
|
||||
// Get visible chunks - limit the number of chunks processed per frame
|
||||
const visibleChunks = getVisibleChunks().slice(0, 20);
|
||||
|
||||
// Render each visible chunk
|
||||
for (const { chunkX, chunkY, isVisible } of visibleChunks) {
|
||||
@ -63,6 +74,23 @@ function render() {
|
||||
// Reset world moved flag after rendering
|
||||
worldMoved = false;
|
||||
|
||||
// Update cloud position animation only (not colors or shapes)
|
||||
skyAnimationTime += skyAnimationSpeed;
|
||||
if (skyAnimationTime > 1) skyAnimationTime -= 1;
|
||||
|
||||
// Render physics objects
|
||||
renderPhysicsObjects(ctx, worldOffsetX, worldOffsetY);
|
||||
|
||||
// Render entities
|
||||
if (typeof renderEntities === 'function') {
|
||||
renderEntities(ctx, worldOffsetX, worldOffsetY);
|
||||
}
|
||||
|
||||
// Render breaking range indicator if player is breaking
|
||||
if (player && player.isBreaking) {
|
||||
renderBreakingIndicator(ctx, worldOffsetX, worldOffsetY);
|
||||
}
|
||||
|
||||
// Draw cursor position and update debug info
|
||||
if (currentMouseX !== undefined && currentMouseY !== undefined) {
|
||||
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
|
||||
@ -96,6 +124,108 @@ function render() {
|
||||
}
|
||||
}
|
||||
|
||||
// Render the animated sky background
|
||||
function renderSky() {
|
||||
// Create a gradient with fixed colors (no animation of colors)
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||
|
||||
// Use fixed color positions for a brighter, static sky
|
||||
gradient.addColorStop(0, SKY_COLORS[0]);
|
||||
gradient.addColorStop(0.5, SKY_COLORS[1]);
|
||||
gradient.addColorStop(1, SKY_COLORS[2]);
|
||||
|
||||
// Fill the background with the gradient
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Add a subtle horizon line to separate sky from world
|
||||
const horizonGradient = ctx.createLinearGradient(0, canvas.height * 0.4, 0, canvas.height * 0.6);
|
||||
horizonGradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
|
||||
horizonGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
|
||||
horizonGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
||||
|
||||
ctx.fillStyle = horizonGradient;
|
||||
ctx.fillRect(0, canvas.height * 0.4, canvas.width, canvas.height * 0.2);
|
||||
}
|
||||
|
||||
// Display debug information
|
||||
function displayDebugInfo() {
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fillRect(10, 10, 200, 80);
|
||||
|
||||
ctx.font = '14px monospace';
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
// Display FPS
|
||||
ctx.fillText(`FPS: ${fps}`, 20, 20);
|
||||
|
||||
// Display world coordinates
|
||||
ctx.fillText(`World: ${Math.floor(worldOffsetX)}, ${Math.floor(worldOffsetY)}`, 20, 40);
|
||||
|
||||
// Display chunk coordinates
|
||||
const chunkX = Math.floor(worldOffsetX / CHUNK_SIZE);
|
||||
const chunkY = Math.floor(worldOffsetY / CHUNK_SIZE);
|
||||
ctx.fillText(`Chunk: ${chunkX}, ${chunkY}`, 20, 60);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Render breaking indicator for player
|
||||
function renderBreakingIndicator(ctx, offsetX, offsetY) {
|
||||
// Get mouse position in world coordinates
|
||||
const worldX = Math.floor(currentMouseX / PIXEL_SIZE) + worldOffsetX;
|
||||
const worldY = Math.floor(currentMouseY / PIXEL_SIZE) + worldOffsetY;
|
||||
|
||||
// Calculate distance from player to target block
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(worldX - player.x, 2) +
|
||||
Math.pow(worldY - player.y, 2)
|
||||
);
|
||||
|
||||
// Convert to screen coordinates
|
||||
const screenX = (worldX - offsetX) * PIXEL_SIZE;
|
||||
const screenY = (worldY - offsetY) * PIXEL_SIZE;
|
||||
const playerScreenX = (player.x - offsetX) * PIXEL_SIZE;
|
||||
const playerScreenY = (player.y - offsetY) * PIXEL_SIZE;
|
||||
|
||||
// Draw breaking range circle
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
playerScreenX,
|
||||
playerScreenY,
|
||||
player.breakingRange * PIXEL_SIZE,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw target indicator
|
||||
if (distance <= player.breakingRange) {
|
||||
ctx.strokeStyle = '#ff0000';
|
||||
} else {
|
||||
ctx.strokeStyle = '#888888'; // Gray when out of range
|
||||
}
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(
|
||||
screenX - PIXEL_SIZE/2,
|
||||
screenY - PIXEL_SIZE/2,
|
||||
PIXEL_SIZE,
|
||||
PIXEL_SIZE
|
||||
);
|
||||
|
||||
// Draw line from player to target point
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(playerScreenX, playerScreenY);
|
||||
ctx.lineTo(screenX, screenY);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Render a chunk to an offscreen canvas and cache it
|
||||
function renderChunkToCache(chunkX, chunkY, key) {
|
||||
const chunk = chunks.get(key);
|
||||
|
BIN
sprites/citizen.png
Normal file
After Width: | Height: | Size: 526 B |
BIN
sprites/farnel.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
sprites/pingwin.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
sprites/player.png
Normal file
After Width: | Height: | Size: 644 B |
BIN
sprites/postac.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
sprites/purplin.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
sprites/rabbit.png
Normal file
After Width: | Height: | Size: 335 B |
66
styles.css
@ -39,6 +39,18 @@ body {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
#spawn-player-btn {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 10px 15px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#spawn-player-btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
display: flex;
|
||||
}
|
||||
@ -63,3 +75,57 @@ body {
|
||||
background-color: #000;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
#player-hud {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
width: 300px;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-family: Arial, sans-serif;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#health-bar-container {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
margin-bottom: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#health-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #4CAF50;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
#inventory-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.inventory-slot {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
position: relative;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.inventory-slot:hover {
|
||||
transform: scale(1.1);
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|