import { TICK_DELTA } from "$/common/constants.ts"; import type { ClientLevel } from "../level/client_level.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; // anything that exists in the level and moves, like minecraft's Entity. position is the middle of its feet. // it's simulated in fixed ticks (common/constants.ts), frames draw it between its last two positions export abstract class Entity { id: string; level: ClientLevel; x = 0; y = 0; z = 0; // where it was at the start of the tick, what frames interpolate from prev_x = 0; prev_y = 0; prev_z = 0; // blocks per second vx = 0; vy = 0; vz = 0; yaw = 0; pitch = 0; // the collision box, width is used for both x and z width: number; height: number; eye_height: number; gravity = -15.8; // which way it hit something on each axis in the last move: 1 or -1, 0 for nothing. // 1 on y means it's standing on something colliding_x = 0; colliding_y = 0; colliding_z = 0; constructor(level: ClientLevel, id: string, width: number, height: number, eye_height: number) { this.level = level; this.id = id; this.width = width; this.height = height; this.eye_height = eye_height; } get on_ground() { return this.colliding_y === 1; } // jumps there, without interpolating from where it was set_position(x: number, y: number, z: number) { this.x = this.prev_x = x; this.y = this.prev_y = y; this.z = this.prev_z = z; } save_previous_position() { this.prev_x = this.x; this.prev_y = this.y; this.prev_z = this.z; } // where to draw it, partial_tick is how far the frame is between the last tick and the next render_position(partial_tick: number) { return { x: this.prev_x + (this.x - this.prev_x) * partial_tick, y: this.prev_y + (this.y - this.prev_y) * partial_tick, z: this.prev_z + (this.z - this.prev_z) * partial_tick, }; } // one step of the game, TICK_DELTA seconds abstract tick(): void; // falls and moves 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 move() { // the average of this tick's start and end speed, so the arc is the same at any tick rate const wanted_y = (this.vy + this.gravity * TICK_DELTA / 2) * TICK_DELTA; this.vy += this.gravity * TICK_DELTA; const wanted = [this.vx * TICK_DELTA, wanted_y, this.vz * TICK_DELTA]; const half = this.width / 2; const min = [this.x - half, this.y, this.z - half]; const max = [this.x + half, this.y + this.height, this.z + half]; const moved = [0, 0, 0]; for (const axis of [1, 0, 2]) { const distance = this.#clip(min, max, axis, wanted[axis]); min[axis] += distance; max[axis] += distance; moved[axis] = distance; } const hit = (axis: number) => moved[axis] !== wanted[axis] ? -Math.sign(wanted[axis]) : 0; this.colliding_x = hit(0); this.colliding_y = hit(1); this.colliding_z = hit(2); if (this.colliding_x !== 0) this.vx = 0; if (this.colliding_y !== 0) this.vy = 0; if (this.colliding_z !== 0) this.vz = 0; this.x += moved[0]; this.y += moved[1]; this.z += moved[2]; } // how far the box can go along an axis before it runs into a block, unloaded chunks included. // blocks it's already inside don't stop it, so it can get out of them #clip(min: number[], max: number[], axis: number, distance: number) { 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 (!this.level.get_block(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; } }