124 lines
3.4 KiB
TypeScript
124 lines
3.4 KiB
TypeScript
import { ItemData, ItemStack } from "$/common/inventory.ts";
|
|
import { move_body } from "$/common/physics.ts";
|
|
import type { EntityInfo } from "$/common/protocol.ts";
|
|
|
|
// minecraft's item physics, its per tick numbers turned into per second ones where they're speeds
|
|
export const ITEM_SIZE = 0.25;
|
|
// 0.04 blocks per tick, per tick
|
|
const GRAVITY = -16;
|
|
// what's left of the speed after each tick
|
|
const AIR_DRAG = 0.98;
|
|
const GROUND_FRICTION = 0.6 * 0.98;
|
|
// slower than this counts as stopped, so items don't creep along forever
|
|
const MIN_SPEED = 0.01;
|
|
// how fast an item stuck inside a block floats out of it
|
|
const UNSTICK_SPEED = 2;
|
|
|
|
// 5 minutes
|
|
export const DESPAWN_TICKS = 6000;
|
|
// how long before a popped block drop can be picked up, and one a player drops
|
|
export const BLOCK_DROP_PICKUP_DELAY = 10;
|
|
export const PLAYER_DROP_PICKUP_DELAY = 40;
|
|
|
|
// what's saved about an item on the ground
|
|
export interface SavedItemEntity {
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
vx: number;
|
|
vy: number;
|
|
vz: number;
|
|
item: ItemData;
|
|
age: number;
|
|
pickup_delay: number;
|
|
}
|
|
|
|
// a stack of items lying in the world, like minecraft's ItemEntity. only the server simulates it,
|
|
// clients draw where they're told it is
|
|
export class ItemEntity {
|
|
readonly id: string;
|
|
item: ItemStack;
|
|
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
vx = 0;
|
|
vy = 0;
|
|
vz = 0;
|
|
readonly width = ITEM_SIZE;
|
|
readonly height = ITEM_SIZE;
|
|
|
|
age = 0;
|
|
// ticks until a player can pick it up
|
|
pickup_delay: number;
|
|
on_ground = false;
|
|
|
|
// the position clients were last sent, so it's only sent again when it moves
|
|
sent_x: number;
|
|
sent_y: number;
|
|
sent_z: number;
|
|
|
|
constructor(id: string, item: ItemStack, x: number, y: number, z: number, pickup_delay = 0) {
|
|
this.id = id;
|
|
this.item = item;
|
|
this.x = this.sent_x = x;
|
|
this.y = this.sent_y = y;
|
|
this.z = this.sent_z = z;
|
|
this.pickup_delay = pickup_delay;
|
|
}
|
|
|
|
tick(is_solid: (x: number, y: number, z: number) => boolean) {
|
|
this.age += 1;
|
|
if (this.pickup_delay > 0) {
|
|
this.pickup_delay -= 1;
|
|
}
|
|
|
|
// covered by a block placed on it, float out the top instead of being stuck inside
|
|
if (is_solid(Math.floor(this.x), Math.floor(this.y + this.height / 2), Math.floor(this.z))) {
|
|
this.vy = UNSTICK_SPEED;
|
|
}
|
|
|
|
const collisions = move_body(this, GRAVITY, is_solid);
|
|
this.on_ground = collisions.y === 1;
|
|
|
|
const friction = this.on_ground ? GROUND_FRICTION : AIR_DRAG;
|
|
this.vx *= friction;
|
|
this.vy *= AIR_DRAG;
|
|
this.vz *= friction;
|
|
if (Math.abs(this.vx) < MIN_SPEED) this.vx = 0;
|
|
if (Math.abs(this.vz) < MIN_SPEED) this.vz = 0;
|
|
}
|
|
|
|
// whether two stacks could be one
|
|
can_merge_with(other: ItemEntity) {
|
|
return other !== this && other.item.type_id === this.item.type_id &&
|
|
JSON.stringify(other.item.data) === JSON.stringify(this.item.data) &&
|
|
this.item.amount < this.item.max_amount && other.item.amount < other.item.max_amount;
|
|
}
|
|
|
|
info(): EntityInfo {
|
|
return { kind: "item", id: this.id, x: this.x, y: this.y, z: this.z, item: this.item.to_data() };
|
|
}
|
|
|
|
save(): SavedItemEntity {
|
|
const { x, y, z, vx, vy, vz, age, pickup_delay } = this;
|
|
return { x, y, z, vx, vy, vz, age, pickup_delay, item: this.item.to_data() };
|
|
}
|
|
|
|
static load(id: string, saved: SavedItemEntity) {
|
|
const entity = new ItemEntity(
|
|
id,
|
|
ItemStack.from_data(saved.item),
|
|
saved.x,
|
|
saved.y,
|
|
saved.z,
|
|
saved.pickup_delay,
|
|
);
|
|
entity.vx = saved.vx;
|
|
entity.vy = saved.vy;
|
|
entity.vz = saved.vz;
|
|
entity.age = saved.age;
|
|
return entity;
|
|
}
|
|
}
|