Files
2026-09-25 16:26:45 -03:00

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;
}
}