Compare commits
No commits in common. "b82f415f4feb61f29582d2f0bd07dd9ef598dedd" and "ebb96846ed3e430eaa8e0841eaa1f410d99556be" have entirely different histories.
b82f415f4f
...
ebb96846ed
@ -20,12 +20,7 @@
|
|||||||
<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>
|
||||||
@ -49,10 +44,6 @@
|
|||||||
<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;
|
||||||
let PIXEL_SIZE = 4;
|
const PIXEL_SIZE = 4;
|
||||||
const GRAVITY = 1.5; // Increased gravity (3x stronger)
|
const GRAVITY = 0.5;
|
||||||
const WATER_SPREAD = 3;
|
const WATER_SPREAD = 3;
|
||||||
|
|
||||||
// Base Colors
|
// Base Colors
|
||||||
@ -60,9 +60,6 @@ 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,22 +1,9 @@
|
|||||||
// Basic element behaviors (sand, water, dirt)
|
// Basic element behaviors (sand, water, dirt)
|
||||||
function updateSand(x, y) {
|
function updateSand(x, y) {
|
||||||
// Try to move down with stronger gravity (up to 5 pixels at once)
|
// Try to move down
|
||||||
let maxFall = 5;
|
if (getPixel(x, y + 1) === EMPTY) {
|
||||||
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, newY, SAND);
|
setPixel(x, y + 1, SAND);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Try to move down-left or down-right
|
// Try to move down-left or down-right
|
||||||
@ -62,24 +49,11 @@ function updateWater(x, y) {
|
|||||||
setMetadata(x, y, metadata);
|
setMetadata(x, y, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to move down with stronger gravity (up to 4 pixels at once)
|
// Try to move down
|
||||||
let maxFall = 4;
|
if (getPixel(x, y + 1) === EMPTY) {
|
||||||
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, newY, WATER);
|
setPixel(x, y + 1, WATER);
|
||||||
moveMetadata(x, y, x, newY);
|
moveMetadata(x, y, x, y + 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Try to move down-left or down-right
|
// Try to move down-left or down-right
|
||||||
@ -154,23 +128,10 @@ function updateWater(x, y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateDirt(x, y) {
|
function updateDirt(x, y) {
|
||||||
// Try to move down with stronger gravity (up to 5 pixels at once)
|
// Try to move down
|
||||||
let maxFall = 5;
|
if (getPixel(x, y + 1) === EMPTY) {
|
||||||
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, newY, DIRT);
|
setPixel(x, y + 1, DIRT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Try to move down-left or down-right
|
// Try to move down-left or down-right
|
||||||
|
@ -1,265 +0,0 @@
|
|||||||
// 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,25 +1,11 @@
|
|||||||
// 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 with stronger gravity
|
// Grass behaves like dirt for physics
|
||||||
let maxFall = 5;
|
if (getPixel(x, y + 1) === EMPTY) {
|
||||||
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, newY, GRASS);
|
setPixel(x, y + 1, 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);
|
||||||
@ -60,27 +46,13 @@ function updateGrass(x, y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateSeed(x, y) {
|
function updateSeed(x, y) {
|
||||||
// Seeds fall like sand with stronger gravity
|
// Seeds fall like sand
|
||||||
let maxFall = 5;
|
if (getPixel(x, y + 1) === EMPTY) {
|
||||||
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, newY, SEED);
|
setPixel(x, y + 1, SEED);
|
||||||
moveMetadata(x, y, x, newY);
|
moveMetadata(x, y, x, y + 1);
|
||||||
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,26 +1,12 @@
|
|||||||
// Tree element behaviors
|
// Tree element behaviors
|
||||||
function updateTreeSeed(x, y) {
|
function updateTreeSeed(x, y) {
|
||||||
// Tree seeds fall like other seeds with stronger gravity
|
// Tree seeds fall like other seeds
|
||||||
let maxFall = 5;
|
if (getPixel(x, y + 1) === EMPTY) {
|
||||||
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, newY, TREE_SEED);
|
setPixel(x, y + 1, TREE_SEED);
|
||||||
moveMetadata(x, y, x, newY);
|
moveMetadata(x, y, x, y + 1);
|
||||||
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);
|
||||||
|
@ -1,207 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,543 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,148 +0,0 @@
|
|||||||
// 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,58 +4,6 @@ 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'));
|
||||||
@ -80,14 +28,6 @@ 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');
|
||||||
}
|
}
|
||||||
@ -106,17 +46,11 @@ function handleMouseDown(e) {
|
|||||||
worldOffsetXBeforeDrag = worldOffsetX;
|
worldOffsetXBeforeDrag = worldOffsetX;
|
||||||
worldOffsetYBeforeDrag = worldOffsetY;
|
worldOffsetYBeforeDrag = worldOffsetY;
|
||||||
} else {
|
} else {
|
||||||
// Left mouse button for drawing or breaking blocks
|
// Left mouse button for drawing
|
||||||
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) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
@ -155,11 +89,6 @@ 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) {
|
||||||
@ -169,14 +98,6 @@ 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++) {
|
||||||
@ -193,11 +114,6 @@ 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);
|
||||||
|
|
||||||
@ -269,26 +185,4 @@ 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,15 +5,6 @@ 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');
|
||||||
@ -34,15 +25,8 @@ 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));
|
||||||
@ -77,11 +61,6 @@ 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() {
|
||||||
@ -89,50 +68,6 @@ 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;
|
||||||
@ -140,18 +75,11 @@ 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 - skip rendering if FPS is too low to prevent death spiral
|
// Render
|
||||||
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,16 +7,6 @@ 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,19 +6,8 @@ function render() {
|
|||||||
// Clear the canvas
|
// Clear the canvas
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// Set pixelated rendering for the entire canvas
|
// Get visible chunks
|
||||||
ctx.imageSmoothingEnabled = false;
|
const visibleChunks = getVisibleChunks();
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -74,23 +63,6 @@ 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;
|
||||||
@ -124,108 +96,6 @@ 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);
|
||||||
|
Before Width: | Height: | Size: 526 B |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 644 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 335 B |
66
styles.css
@ -39,18 +39,6 @@ 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;
|
||||||
}
|
}
|
||||||
@ -75,57 +63,3 @@ 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);
|
|
||||||
}
|
|
||||||
|