Item entities
This commit is contained in:
+7
-67
@@ -1,11 +1,7 @@
|
||||
import { TICK_DELTA } from "$/common/constants.ts";
|
||||
import { AIR } from "$/common/constants.ts";
|
||||
import { move_body } from "$/common/physics.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 {
|
||||
@@ -75,67 +71,11 @@ export abstract class Entity {
|
||||
// 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
|
||||
// falls and moves by its velocity for one tick, see move_body
|
||||
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;
|
||||
const collisions = move_body(this, this.gravity, (x, y, z) => this.level.get_block(x, y, z) !== AIR);
|
||||
this.colliding_x = collisions.x;
|
||||
this.colliding_y = collisions.y;
|
||||
this.colliding_z = collisions.z;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ItemStack } from "$/common/inventory.ts";
|
||||
import type { EntityInfo } from "$/common/protocol.ts";
|
||||
import type { ClientLevel } from "../level/client_level.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
|
||||
// how many ticks a server position takes to reach, like minecraft's lerpTo
|
||||
const LERP_TICKS = 3;
|
||||
// how many ticks it takes to fly into whoever picked it up
|
||||
const PICKUP_TICKS = 3;
|
||||
|
||||
// an item lying on the ground. the server simulates it, this only moves where it's told and spins
|
||||
export class ItemEntity extends Entity {
|
||||
item: ItemStack;
|
||||
// ticks since it showed up, for spinning and bobbing
|
||||
age = 0;
|
||||
// so items dropped together don't spin in step
|
||||
readonly bob_offset = Math.random() * Math.PI * 2;
|
||||
|
||||
#target_x: number;
|
||||
#target_y: number;
|
||||
#target_z: number;
|
||||
#lerp_ticks = 0;
|
||||
|
||||
// who's picking it up, and for how many ticks it has been flying to them
|
||||
#picked_up_by: Entity | undefined;
|
||||
#pickup_age = 0;
|
||||
|
||||
constructor(level: ClientLevel, info: EntityInfo) {
|
||||
super(level, info.id, 0.25, 0.25, 0.125);
|
||||
this.item = ItemStack.from_data(info.item);
|
||||
this.set_position(info.x, info.y, info.z);
|
||||
this.#target_x = info.x;
|
||||
this.#target_y = info.y;
|
||||
this.#target_z = info.z;
|
||||
}
|
||||
|
||||
lerp_to(x: number, y: number, z: number) {
|
||||
this.#target_x = x;
|
||||
this.#target_y = y;
|
||||
this.#target_z = z;
|
||||
this.#lerp_ticks = LERP_TICKS;
|
||||
}
|
||||
|
||||
// it already went into their inventory on the server, this is only the animation
|
||||
pick_up(by: Entity) {
|
||||
this.#picked_up_by = by;
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.age += 1;
|
||||
|
||||
if (this.#picked_up_by) {
|
||||
this.#pickup_age += 1;
|
||||
if (this.#pickup_age >= PICKUP_TICKS) {
|
||||
this.level.remove_entity(this.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#lerp_ticks > 0) {
|
||||
this.x += (this.#target_x - this.x) / this.#lerp_ticks;
|
||||
this.y += (this.#target_y - this.y) / this.#lerp_ticks;
|
||||
this.z += (this.#target_z - this.z) / this.#lerp_ticks;
|
||||
this.#lerp_ticks -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// while being picked up it speeds towards the middle of whoever took it, like minecraft's ItemPickupParticle
|
||||
override render_position(partial_tick: number) {
|
||||
const position = super.render_position(partial_tick);
|
||||
const by = this.#picked_up_by;
|
||||
if (!by) {
|
||||
return position;
|
||||
}
|
||||
const target = by.render_position(partial_tick);
|
||||
const t = Math.min(1, (this.#pickup_age + partial_tick) / PICKUP_TICKS) ** 2;
|
||||
return {
|
||||
x: position.x + (target.x - position.x) * t,
|
||||
y: position.y + (target.y + 0.5 - position.y) * t,
|
||||
z: position.z + (target.z - position.z) * t,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PLAYER_EYE_HEIGHT, PLAYER_HEIGHT, PLAYER_WIDTH } from "$/common/constants.ts";
|
||||
import type { ClientLevel } from "../level/client_level.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
|
||||
@@ -5,7 +6,7 @@ export abstract class Player extends Entity {
|
||||
name: string;
|
||||
|
||||
constructor(level: ClientLevel, id: string, name: string) {
|
||||
super(level, id, 0.55, 1.79, 1.69);
|
||||
super(level, id, PLAYER_WIDTH, PLAYER_HEIGHT, PLAYER_EYE_HEIGHT);
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user