Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"site:preview": "cd website && bun run preview",
"typecheck": "tsc --noEmit",
"check": "bun run typecheck",
"test": "bun tests/liquid.test.ts && bun tests/raycast.test.ts && bun tests/responsiveness.test.ts",
"test": "bun tests/liquid.test.ts && bun tests/raycast.test.ts && bun tests/responsiveness.test.ts && bun tests/recipes.test.ts",
"test:light": "bun scripts/lighttest.ts",
"screenshot": "bun scripts/screenshot.ts"
},
Expand Down
93 changes: 88 additions & 5 deletions src/engine/Input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ export class Input {
private readonly keys = new Set<string>();
private mouseDX = 0;
private mouseDY = 0;
private lastMouseMotionAt = -Infinity;
private lastPointerMotionAt = -Infinity;
private breakQueued = false;
private placeQueued = false;
private lastBreakQueueAt = -Infinity;
private lastPlaceQueueAt = -Infinity;
private lastBreakDownAt = -Infinity;
private lastPlaceDownAt = -Infinity;
private _leftHeld = false;
private _rightHeld = false;
private lastSpaceTap = 0;
Expand All @@ -37,6 +43,13 @@ export class Input {
window.addEventListener("keydown", this.handleKeyDown);
window.addEventListener("keyup", this.handleKeyUp);
window.addEventListener("mousemove", this.handleMouseMove);
// Pointer events are more reliable than legacy mouse events in some iframe
// / pointer-lock transitions. Survival mining depends on the held-button
// state, so mirror mouse down/up through pointer down/up too.
window.addEventListener("pointerdown", this.handlePointerDown, true);
window.addEventListener("pointermove", this.handlePointerMove);
window.addEventListener("pointerup", this.handlePointerUp);
window.addEventListener("pointercancel", this.handlePointerCancel);
// mousedown is bound to window (capture phase) — NOT the canvas — so clicks
// are caught even if an overlay element happens to sit above the canvas.
window.addEventListener("mousedown", this.handleMouseDown, true);
Expand All @@ -61,6 +74,10 @@ export class Input {
window.removeEventListener("keydown", this.handleKeyDown);
window.removeEventListener("keyup", this.handleKeyUp);
window.removeEventListener("mousemove", this.handleMouseMove);
window.removeEventListener("pointerdown", this.handlePointerDown, true);
window.removeEventListener("pointermove", this.handlePointerMove);
window.removeEventListener("pointerup", this.handlePointerUp);
window.removeEventListener("pointercancel", this.handlePointerCancel);
window.removeEventListener("mousedown", this.handleMouseDown, true);
window.removeEventListener("mouseup", this.handleMouseUp);
window.removeEventListener("click", this.handleClick, true);
Expand All @@ -77,7 +94,7 @@ export class Input {
if (t && t.closest && t.closest(".screen:not([hidden])")) return;
e.preventDefault();
dbg("contextmenu (robust right-click) -> placeQueued");
this.placeQueued = true;
if (performance.now() - this.lastPlaceDownAt > 700) this.queuePlace();
};

private handleClick = (e: MouseEvent): void => {
Expand All @@ -86,7 +103,7 @@ export class Input {
const t = e.target as Element | null;
if (t && t.closest && t.closest(".screen:not([hidden])")) return;
dbg("click (robust left-click) -> breakQueued");
this.breakQueued = true;
if (performance.now() - this.lastBreakDownAt > 700) this.queueBreak();
};

private handleAuxClick = (e: MouseEvent): void => {
Expand All @@ -97,10 +114,22 @@ export class Input {
if (t && t.closest && t.closest(".screen:not([hidden])")) return;
if (e.button === 2) {
dbg("auxclick (robust right-click) -> placeQueued");
this.placeQueued = true;
if (performance.now() - this.lastPlaceDownAt > 700) this.queuePlace();
}
};

private queueBreak(): void {
const now = performance.now();
if (now - this.lastBreakQueueAt > 180) this.breakQueued = true;
this.lastBreakQueueAt = now;
}

private queuePlace(): void {
const now = performance.now();
if (now - this.lastPlaceQueueAt > 180) this.placeQueued = true;
this.lastPlaceQueueAt = now;
}

private handleKeyDown = (e: KeyboardEvent): void => {
// Don't capture game keys while the user is typing in a form field.
const el = document.activeElement;
Expand Down Expand Up @@ -133,11 +162,62 @@ export class Input {
};

private handleMouseMove = (e: MouseEvent): void => {
// Some browsers emit a mouseup during pointer-lock/focus transitions. Keep
// the held-button state recoverable from the browser's current bitfield,
// but never clear it here: pointer-lock mousemove events may report
// buttons=0 even while the physical button is still held.
if ((e.buttons & 1) !== 0) this._leftHeld = true;
if ((e.buttons & 2) !== 0) this._rightHeld = true;
if (!this._locked) return;
const now = performance.now();
if (now - this.lastPointerMotionAt < 8) return;
this.lastMouseMotionAt = now;
this.mouseDX += e.movementX;
this.mouseDY += e.movementY;
};

private handlePointerDown = (e: PointerEvent): void => {
if (e.pointerType !== "mouse" && e.pointerType !== "pen") return;
const t = e.target as Element | null;
if (t && t.closest && t.closest(".screen:not([hidden])")) return;
e.preventDefault();
if (e.button === 0) {
this.lastBreakDownAt = performance.now();
this.queueBreak();
this._leftHeld = true;
} else if (e.button === 2) {
this.lastPlaceDownAt = performance.now();
this.queuePlace();
this._rightHeld = true;
}
if (!this._locked && e.button === 0) this.requestLock();
};

private handlePointerMove = (e: PointerEvent): void => {
if (e.pointerType !== "mouse" && e.pointerType !== "pen") return;
if ((e.buttons & 1) !== 0) this._leftHeld = true;
if ((e.buttons & 2) !== 0) this._rightHeld = true;
if (!this._locked) return;
// Some iframe/pointer-lock paths report movement on pointermove but not on
// mousemove while a button is held.
const now = performance.now();
if (now - this.lastMouseMotionAt < 8) return;
this.lastPointerMotionAt = now;
this.mouseDX += e.movementX;
this.mouseDY += e.movementY;
};

private handlePointerUp = (e: PointerEvent): void => {
if (e.pointerType !== "mouse" && e.pointerType !== "pen") return;
if (e.button === 0) this._leftHeld = false;
else if (e.button === 2) this._rightHeld = false;
};

private handlePointerCancel = (): void => {
this._leftHeld = false;
this._rightHeld = false;
};

private handleMouseDown = (e: MouseEvent): void => {
const t = e.target as Element | null;
const describe = (el: Element | null): string => {
Expand All @@ -154,13 +234,16 @@ export class Input {
return;
}
dbg(`mousedown button=${e.button} locked=${this._locked} target=${describe(t)} pointerLockElement=${document.pointerLockElement ? "yes" : "no"}`);
e.preventDefault();
// Register the interaction FIRST, unconditionally, so clicks always mine/
// place whether or not pointer lock is engaged.
if (e.button === 0) {
this.breakQueued = true;
this.lastBreakDownAt = performance.now();
this.queueBreak();
this._leftHeld = true;
} else if (e.button === 2) {
this.placeQueued = true;
this.lastPlaceDownAt = performance.now();
this.queuePlace();
this._rightHeld = true;
}
dbg(` queued break=${this.breakQueued} place=${this.placeQueued} leftHeld=${this._leftHeld}`);
Expand Down
32 changes: 32 additions & 0 deletions src/engine/Textures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,38 @@ export function createTextureAtlas(scene: Scene): AtlasResult {
}
}
}
// 43: crafting table top — dark wood with a 3x3 grid of lighter cells
{
const [ox, oy] = off(43);
paintSpeckled(ctx, ox, oy, [104, 70, 40], 14, 70, rand);
// grid lines dividing the tile into a 3x3 of ~5px cells
ctx.fillStyle = "rgb(54,36,20)";
for (const p of [5, 10]) {
ctx.fillRect(ox, oy + p, TILE_PX, 1);
ctx.fillRect(ox + p, oy, 1, TILE_PX);
}
// faint lighter inset on each cell to read as a work surface
ctx.fillStyle = "rgba(196,150,92,0.35)";
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
ctx.fillRect(ox + c * 5 + 1, oy + r * 5 + 1, 3, 3);
}
}
}
// 44: crafting table side — plank bands with a darker tool strip
{
const [ox, oy] = off(44);
paintSpeckled(ctx, ox, oy, [150, 104, 60], 12, 70, rand);
// horizontal plank seams
ctx.fillStyle = "rgb(96,64,34)";
for (const y of [4, 8, 12]) ctx.fillRect(ox, oy + y, TILE_PX, 1);
// a dark recessed strip (the "tool drawer" band) across the middle
ctx.fillStyle = "rgb(70,46,24)";
ctx.fillRect(ox, oy + 6, TILE_PX, 2);
ctx.fillStyle = "rgba(0,0,0,0.25)";
ctx.fillRect(ox + 2, oy + 6, 3, 1);
ctx.fillRect(ox + 9, oy + 6, 4, 1);
}

// Upload the painted canvas to the GPU.
texture.update(false);
Expand Down
20 changes: 20 additions & 0 deletions src/game/Blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const T = {
BIRCH_LEAVES: 40,
SPRUCE_LEAVES: 41,
SNOWY_LEAVES: 42,
CRAFTING_TABLE_TOP: 43,
CRAFTING_TABLE_SIDE: 44,
} as const;

/**
Expand Down Expand Up @@ -589,13 +591,31 @@ export const BLOCKS: readonly BlockDef[] = [
liquid: false,
light: { lightPassesThrough: true },
},
{
id: 38,
name: "Crafting Table",
tiles: [
T.CRAFTING_TABLE_SIDE,
T.CRAFTING_TABLE_SIDE,
T.CRAFTING_TABLE_TOP,
T.WOOD_TOP,
T.CRAFTING_TABLE_SIDE,
T.CRAFTING_TABLE_SIDE,
],
color: "#8a5a32",
solid: true,
opaque: true,
transparent: false,
liquid: false,
},
];

export const AIR_BLOCK = 0;
export const WATER_BLOCK = 7;
export const WATER_FLOWING_BLOCK = 29;
export const CACTUS_BLOCK = 19;
export const MUSHROOM_BLOCK = 23;
export const CRAFTING_TABLE_BLOCK = 38;

export function isAir(id: BlockId): boolean {
return id === AIR_BLOCK;
Expand Down
Loading
Loading