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="tree-seed-btn">Tree Seed</button>
|
||||||
<button id="fire-btn">Fire</button>
|
<button id="fire-btn">Fire</button>
|
||||||
<button id="lava-btn">Lava</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="eraser-btn">Eraser</button>
|
||||||
|
<button id="spawn-player-btn">Spawn Player</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
<button id="move-left">←</button>
|
<button id="move-left">←</button>
|
||||||
@ -44,6 +49,10 @@
|
|||||||
<script src="js/elements/plants.js"></script>
|
<script src="js/elements/plants.js"></script>
|
||||||
<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/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/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>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Game constants
|
// Game constants
|
||||||
const CHUNK_SIZE = 200;
|
const CHUNK_SIZE = 200;
|
||||||
const PIXEL_SIZE = 4;
|
let PIXEL_SIZE = 4;
|
||||||
const GRAVITY = 0.5;
|
const GRAVITY = 1.5; // Increased gravity (3x stronger)
|
||||||
const WATER_SPREAD = 3;
|
const WATER_SPREAD = 3;
|
||||||
|
|
||||||
// Base Colors
|
// Base Colors
|
||||||
@ -60,6 +60,9 @@ const LEAF = 12;
|
|||||||
const FIRE = 13;
|
const FIRE = 13;
|
||||||
const LAVA = 14;
|
const LAVA = 14;
|
||||||
const RABBIT = 15;
|
const RABBIT = 15;
|
||||||
|
const SQUARE = 16;
|
||||||
|
const CIRCLE = 17;
|
||||||
|
const TRIANGLE = 18;
|
||||||
|
|
||||||
// Flammable materials
|
// Flammable materials
|
||||||
const FLAMMABLE_MATERIALS = [GRASS, WOOD, SEED, GRASS_BLADE, FLOWER, TREE_SEED, LEAF];
|
const FLAMMABLE_MATERIALS = [GRASS, WOOD, SEED, GRASS_BLADE, FLOWER, TREE_SEED, LEAF];
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
// Basic element behaviors (sand, water, dirt)
|
// Basic element behaviors (sand, water, dirt)
|
||||||
function updateSand(x, y) {
|
function updateSand(x, y) {
|
||||||
// Try to move down
|
// Try to move down with stronger gravity (up to 5 pixels at once)
|
||||||
if (getPixel(x, y + 1) === EMPTY) {
|
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, EMPTY);
|
||||||
setPixel(x, y + 1, SAND);
|
setPixel(x, newY, SAND);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Try to move down-left or down-right
|
// Try to move down-left or down-right
|
||||||
@ -49,11 +62,24 @@ function updateWater(x, y) {
|
|||||||
setMetadata(x, y, metadata);
|
setMetadata(x, y, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to move down
|
// Try to move down with stronger gravity (up to 4 pixels at once)
|
||||||
if (getPixel(x, y + 1) === EMPTY) {
|
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, EMPTY);
|
||||||
setPixel(x, y + 1, WATER);
|
setPixel(x, newY, WATER);
|
||||||
moveMetadata(x, y, x, y + 1);
|
moveMetadata(x, y, x, newY);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Try to move down-left or down-right
|
// Try to move down-left or down-right
|
||||||
@ -128,10 +154,23 @@ function updateWater(x, y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateDirt(x, y) {
|
function updateDirt(x, y) {
|
||||||
// Try to move down
|
// Try to move down with stronger gravity (up to 5 pixels at once)
|
||||||
if (getPixel(x, y + 1) === EMPTY) {
|
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, EMPTY);
|
||||||
setPixel(x, y + 1, DIRT);
|
setPixel(x, newY, DIRT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Try to move down-left or down-right
|
// Try to move down-left or down-right
|
||||||
|
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)
|
// Plant element behaviors (grass, seeds, trees)
|
||||||
function updateGrass(x, y) {
|
function updateGrass(x, y) {
|
||||||
// Grass behaves like dirt for physics
|
// Grass behaves like dirt for physics with stronger gravity
|
||||||
if (getPixel(x, y + 1) === EMPTY) {
|
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, EMPTY);
|
||||||
setPixel(x, y + 1, GRASS);
|
setPixel(x, newY, GRASS);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Try to move down-left or down-right
|
||||||
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
||||||
setPixel(x, y, EMPTY);
|
setPixel(x, y, EMPTY);
|
||||||
setPixel(x - 1, y + 1, GRASS);
|
setPixel(x - 1, y + 1, GRASS);
|
||||||
@ -46,13 +60,27 @@ function updateGrass(x, y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateSeed(x, y) {
|
function updateSeed(x, y) {
|
||||||
// Seeds fall like sand
|
// Seeds fall like sand with stronger gravity
|
||||||
if (getPixel(x, y + 1) === EMPTY) {
|
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, EMPTY);
|
||||||
setPixel(x, y + 1, SEED);
|
setPixel(x, newY, SEED);
|
||||||
moveMetadata(x, y, x, y + 1);
|
moveMetadata(x, y, x, newY);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Try to move down-left or down-right
|
||||||
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
||||||
setPixel(x, y, EMPTY);
|
setPixel(x, y, EMPTY);
|
||||||
setPixel(x - 1, y + 1, SEED);
|
setPixel(x - 1, y + 1, SEED);
|
||||||
|
@ -1,12 +1,26 @@
|
|||||||
// Tree element behaviors
|
// Tree element behaviors
|
||||||
function updateTreeSeed(x, y) {
|
function updateTreeSeed(x, y) {
|
||||||
// Tree seeds fall like other seeds
|
// Tree seeds fall like other seeds with stronger gravity
|
||||||
if (getPixel(x, y + 1) === EMPTY) {
|
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, EMPTY);
|
||||||
setPixel(x, y + 1, TREE_SEED);
|
setPixel(x, newY, TREE_SEED);
|
||||||
moveMetadata(x, y, x, y + 1);
|
moveMetadata(x, y, x, newY);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Try to move down-left or down-right
|
||||||
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
else if (getPixel(x - 1, y + 1) === EMPTY) {
|
||||||
setPixel(x, y, EMPTY);
|
setPixel(x, y, EMPTY);
|
||||||
setPixel(x - 1, y + 1, TREE_SEED);
|
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);
|
||||||
|
}
|
||||||
|
}
|
108
js/input.js
@ -4,6 +4,58 @@ let isDragging = false;
|
|||||||
let lastMouseX, lastMouseY;
|
let lastMouseX, lastMouseY;
|
||||||
let currentMouseX, currentMouseY;
|
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) {
|
function setTool(tool) {
|
||||||
currentTool = tool;
|
currentTool = tool;
|
||||||
document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.tools button').forEach(btn => btn.classList.remove('active'));
|
||||||
@ -28,6 +80,14 @@ function setTool(tool) {
|
|||||||
document.getElementById('fire-btn').classList.add('active');
|
document.getElementById('fire-btn').classList.add('active');
|
||||||
} else if (tool === LAVA) {
|
} else if (tool === LAVA) {
|
||||||
document.getElementById('lava-btn').classList.add('active');
|
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) {
|
} else if (tool === EMPTY) {
|
||||||
document.getElementById('eraser-btn').classList.add('active');
|
document.getElementById('eraser-btn').classList.add('active');
|
||||||
}
|
}
|
||||||
@ -46,10 +106,16 @@ function handleMouseDown(e) {
|
|||||||
worldOffsetXBeforeDrag = worldOffsetX;
|
worldOffsetXBeforeDrag = worldOffsetX;
|
||||||
worldOffsetYBeforeDrag = worldOffsetY;
|
worldOffsetYBeforeDrag = worldOffsetY;
|
||||||
} else {
|
} else {
|
||||||
// Left mouse button for drawing
|
// 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;
|
isDrawing = true;
|
||||||
draw(x, y);
|
draw(x, y);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseMove(e) {
|
function handleMouseMove(e) {
|
||||||
@ -89,6 +155,11 @@ function handleMouseUp(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
|
|
||||||
|
// Stop breaking blocks if player exists
|
||||||
|
if (player) {
|
||||||
|
player.stopBreaking();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw(x, y) {
|
function draw(x, y) {
|
||||||
@ -98,6 +169,14 @@ function draw(x, y) {
|
|||||||
const worldX = Math.floor(x / PIXEL_SIZE) + worldOffsetX;
|
const worldX = Math.floor(x / PIXEL_SIZE) + worldOffsetX;
|
||||||
const worldY = Math.floor(y / PIXEL_SIZE) + worldOffsetY;
|
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)
|
// Draw a small brush (3x3)
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
for (let dy = -1; dy <= 1; dy++) {
|
||||||
for (let dx = -1; dx <= 1; dx++) {
|
for (let dx = -1; dx <= 1; dx++) {
|
||||||
@ -114,6 +193,11 @@ function draw(x, y) {
|
|||||||
colorIndex: Math.floor(Math.random() * FIRE_COLORS.length)
|
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 {
|
} else {
|
||||||
setPixel(pixelX, pixelY, currentTool);
|
setPixel(pixelX, pixelY, currentTool);
|
||||||
|
|
||||||
@ -185,4 +269,26 @@ function handleTouchMove(e) {
|
|||||||
function toggleDebug() {
|
function toggleDebug() {
|
||||||
debugMode = !debugMode;
|
debugMode = !debugMode;
|
||||||
document.getElementById('debug-btn').classList.toggle('active');
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
74
js/main.js
@ -5,6 +5,15 @@ let lastFrameTime = 0;
|
|||||||
let fps = 0;
|
let fps = 0;
|
||||||
let debugMode = false;
|
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
|
// Initialize the simulation
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
canvas = document.getElementById('simulation-canvas');
|
canvas = document.getElementById('simulation-canvas');
|
||||||
@ -25,8 +34,15 @@ window.onload = function() {
|
|||||||
document.getElementById('tree-seed-btn').addEventListener('click', () => setTool(TREE_SEED));
|
document.getElementById('tree-seed-btn').addEventListener('click', () => setTool(TREE_SEED));
|
||||||
document.getElementById('fire-btn').addEventListener('click', () => setTool(FIRE));
|
document.getElementById('fire-btn').addEventListener('click', () => setTool(FIRE));
|
||||||
document.getElementById('lava-btn').addEventListener('click', () => setTool(LAVA));
|
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));
|
document.getElementById('eraser-btn').addEventListener('click', () => setTool(EMPTY));
|
||||||
|
|
||||||
|
// Add player spawn button
|
||||||
|
document.getElementById('spawn-player-btn').addEventListener('click', spawnPlayer);
|
||||||
|
|
||||||
// Navigation controls
|
// Navigation controls
|
||||||
document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0));
|
document.getElementById('move-left').addEventListener('click', () => moveWorld(-CHUNK_SIZE/2, 0));
|
||||||
document.getElementById('move-right').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
|
// Start the simulation loop
|
||||||
requestAnimationFrame(simulationLoop);
|
requestAnimationFrame(simulationLoop);
|
||||||
|
|
||||||
|
// Initialize physics variables
|
||||||
|
window.physicsUpdateRate = 16; // ms between physics updates
|
||||||
|
window.lastPhysicsTime = 0;
|
||||||
|
window.fireUpdateCounter = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
@ -68,6 +89,50 @@ function resizeCanvas() {
|
|||||||
canvas.height = window.innerHeight - document.querySelector('.controls').offsetHeight;
|
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) {
|
function simulationLoop(timestamp) {
|
||||||
// Calculate FPS
|
// Calculate FPS
|
||||||
const deltaTime = timestamp - lastFrameTime;
|
const deltaTime = timestamp - lastFrameTime;
|
||||||
@ -75,11 +140,18 @@ function simulationLoop(timestamp) {
|
|||||||
fps = Math.round(1000 / deltaTime);
|
fps = Math.round(1000 / deltaTime);
|
||||||
document.getElementById('fps').textContent = `FPS: ${fps}`;
|
document.getElementById('fps').textContent = `FPS: ${fps}`;
|
||||||
|
|
||||||
|
// Update player movement if player exists
|
||||||
|
if (player) {
|
||||||
|
updatePlayerMovement();
|
||||||
|
}
|
||||||
|
|
||||||
// Update physics with timestamp for rate limiting
|
// Update physics with timestamp for rate limiting
|
||||||
updatePhysics(timestamp);
|
updatePhysics(timestamp);
|
||||||
|
|
||||||
// Render
|
// Render - skip rendering if FPS is too low to prevent death spiral
|
||||||
|
if (fps > 10 || timestamp % 3 < 1) {
|
||||||
render();
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
// Memory management: Clean up chunk cache for chunks that are far away
|
// Memory management: Clean up chunk cache for chunks that are far away
|
||||||
if (timestamp % 5000 < 16) { // Run every ~5 seconds
|
if (timestamp % 5000 < 16) { // Run every ~5 seconds
|
||||||
|
@ -7,6 +7,16 @@ function updatePhysics(timestamp) {
|
|||||||
|
|
||||||
lastPhysicsTime = timestamp || 0;
|
lastPhysicsTime = timestamp || 0;
|
||||||
|
|
||||||
|
// Update physics objects
|
||||||
|
if (typeof updatePhysicsObjects === 'function') {
|
||||||
|
updatePhysicsObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update entities
|
||||||
|
if (typeof updateEntities === 'function') {
|
||||||
|
updateEntities();
|
||||||
|
}
|
||||||
|
|
||||||
// Get visible chunks
|
// Get visible chunks
|
||||||
const visibleChunks = getVisibleChunks();
|
const visibleChunks = getVisibleChunks();
|
||||||
|
|
||||||
|
134
js/render.js
@ -6,8 +6,19 @@ function render() {
|
|||||||
// Clear the canvas
|
// Clear the canvas
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// Get visible chunks
|
// Set pixelated rendering for the entire canvas
|
||||||
const visibleChunks = getVisibleChunks();
|
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
|
// Render each visible chunk
|
||||||
for (const { chunkX, chunkY, isVisible } of visibleChunks) {
|
for (const { chunkX, chunkY, isVisible } of visibleChunks) {
|
||||||
@ -63,6 +74,23 @@ function render() {
|
|||||||
// Reset world moved flag after rendering
|
// Reset world moved flag after rendering
|
||||||
worldMoved = false;
|
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
|
// 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;
|
||||||
@ -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
|
// Render a chunk to an offscreen canvas and cache it
|
||||||
function renderChunkToCache(chunkX, chunkY, key) {
|
function renderChunkToCache(chunkX, chunkY, key) {
|
||||||
const chunk = chunks.get(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;
|
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 {
|
.navigation {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@ -63,3 +75,57 @@ body {
|
|||||||
background-color: #000;
|
background-color: #000;
|
||||||
cursor: crosshair;
|
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);
|
||||||
|
}
|
||||||
|