Item entities
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user