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