Item entities

This commit is contained in:
2026-09-25 16:26:45 -03:00
parent 5fb23cf404
commit a254b96f65
18 changed files with 797 additions and 94 deletions
+5
View File
@@ -23,6 +23,11 @@ export const CHUNK_SIZE = 16;
export const CHUNK_HEIGHT = 128;
export const CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE;
// the player's collision box, position is the middle of its feet
export const PLAYER_WIDTH = 0.55;
export const PLAYER_HEIGHT = 1.79;
export const PLAYER_EYE_HEIGHT = 1.69;
// where a block placed against each face of another block goes
export const FACE_OFFSETS: Record<Faces, { x: number; y: number; z: number }> = {
top: { x: 0, y: 1, z: 0 },
+1
View File
@@ -134,6 +134,7 @@ export interface Player {
readonly inventory: Container; // 36 slots, hotbar is 0-8
readonly selected_slot: number;
readonly held_item: ItemStack | undefined;
// what doesn't fit in the inventory drops at their feet
give_item(id: Id, count?: number, data?: unknown): void;
send_message(text: string): void;
teleport(x: number, y: number, z: number): void;
+96
View File
@@ -0,0 +1,96 @@
// collision against blocks, shared by the client's entities and the server's, so both move things the same way
import { TICK_DELTA } from "./constants.ts";
// how far inside a block face still counts as touching it, so boxes resting exactly on a face don't snag
const EPSILON = 1e-7;
// the two axes that aren't the one being moved along
const OTHER_AXES = [[1, 2], [0, 2], [0, 1]] as const;
// something with a box that moves. position is the middle of its feet, velocity is in blocks per second
export interface Body {
x: number;
y: number;
z: number;
vx: number;
vy: number;
vz: number;
// width is used for both x and z
width: number;
height: number;
}
// which way the body hit something on each axis: 1 or -1, 0 for nothing. 1 on y means it landed on something
export interface Collisions {
x: number;
y: number;
z: number;
}
// falls and moves a body by its velocity for one tick. like minecraft it moves along y, then x, then z, each
// time only as far as it can before touching a block, and stops its velocity on the axes it hit something.
// blocks it's already inside don't stop it, so it can get out of them
export function move_body(body: Body, gravity: number, is_solid: (x: number, y: number, z: number) => boolean) {
// the average of this tick's start and end speed, so the arc is the same at any tick rate
const wanted_y = (body.vy + gravity * TICK_DELTA / 2) * TICK_DELTA;
body.vy += gravity * TICK_DELTA;
const wanted = [body.vx * TICK_DELTA, wanted_y, body.vz * TICK_DELTA];
const half = body.width / 2;
const min = [body.x - half, body.y, body.z - half];
const max = [body.x + half, body.y + body.height, body.z + half];
const moved = [0, 0, 0];
for (const axis of [1, 0, 2]) {
const distance = clip(min, max, axis, wanted[axis], is_solid);
min[axis] += distance;
max[axis] += distance;
moved[axis] = distance;
}
const hit = (axis: number) => moved[axis] !== wanted[axis] ? -Math.sign(wanted[axis]) : 0;
const collisions: Collisions = { x: hit(0), y: hit(1), z: hit(2) };
if (collisions.x !== 0) body.vx = 0;
if (collisions.y !== 0) body.vy = 0;
if (collisions.z !== 0) body.vz = 0;
body.x += moved[0];
body.y += moved[1];
body.z += moved[2];
return collisions;
}
// how far the box can go along an axis before it runs into a block
function clip(
min: number[],
max: number[],
axis: number,
distance: number,
is_solid: (x: number, y: number, z: number) => boolean,
) {
if (distance === 0) {
return 0;
}
const [a, b] = OTHER_AXES[axis];
const from = Math.floor(Math.min(min[axis], min[axis] + distance));
const to = Math.floor(Math.max(max[axis], max[axis] + distance));
const position = [0, 0, 0];
for (let i = from; i <= to; i++) {
for (let j = Math.floor(min[a] + EPSILON); j <= Math.floor(max[a] - EPSILON); j++) {
for (let k = Math.floor(min[b] + EPSILON); k <= Math.floor(max[b] - EPSILON); k++) {
position[axis] = i;
position[a] = j;
position[b] = k;
if (!is_solid(position[0], position[1], position[2])) {
continue;
}
if (distance > 0 && i >= max[axis] - EPSILON) {
distance = Math.min(distance, i - max[axis]);
} else if (distance < 0 && i + 1 <= min[axis] + EPSILON) {
distance = Math.max(distance, i + 1 - min[axis]);
}
}
}
}
return distance;
}
+20 -1
View File
@@ -7,7 +7,7 @@ export const AIR_ID = "bworld:air";
// bump when a client and server of different versions can't play together.
// the server rejects a different version before the client downloads anything
export const PROTOCOL_VERSION = 1;
export const PROTOCOL_VERSION = 2;
export interface PlayerInfo {
id: string;
@@ -19,6 +19,16 @@ export interface PlayerInfo {
pitch: number;
}
// an entity that isn't a player, as the server first sends it. players have their own messages
export interface EntityInfo {
kind: "item";
id: string;
x: number;
y: number;
z: number;
item: ItemData;
}
// x, y, z, block id, and its state bits when they aren't 0
export type BlockChange = [number, number, number, string, number?];
@@ -84,6 +94,8 @@ export type ServerMessage =
// the server may change the name asked for, like alice to alice2
name: string;
players: PlayerInfo[];
// items on the ground
entities: EntityInfo[];
changes: BlockChange[];
spawn: { x: number; y: number; z: number; yaw: number; pitch: number };
selected_slot: number;
@@ -91,6 +103,13 @@ export type ServerMessage =
| { type: "player_join"; player: PlayerInfo }
| { type: "player_leave"; id: string }
| { type: "player_move"; id: string; x: number; y: number; z: number; yaw: number; pitch: number }
| { type: "add_entity"; entity: EntityInfo }
| { type: "move_entity"; id: string; x: number; y: number; z: number }
// a dropped item's stack changed, like when two stacks merge
| { type: "set_entity_item"; id: string; item: ItemData }
| { type: "remove_entity"; id: string }
// a player picked up an item, it flies to them and goes away
| { type: "take_entity"; id: string; player: string }
// also sent to the player who caused it, which corrects anything their client predicted wrong
| { type: "set_block"; x: number; y: number; z: number; id: string; state?: number }
| { type: "chat"; from?: string; text: string }