Item entities
This commit is contained in:
+173
-6
@@ -5,6 +5,9 @@ import {
|
||||
FACE_OFFSETS,
|
||||
Faces,
|
||||
faces,
|
||||
PLAYER_EYE_HEIGHT,
|
||||
PLAYER_HEIGHT,
|
||||
PLAYER_WIDTH,
|
||||
TICK_DELTA,
|
||||
TICKS_PER_SECOND,
|
||||
} from "$/common/constants.ts";
|
||||
@@ -29,10 +32,22 @@ import type { GameLoop } from "./game_loop.ts";
|
||||
import { consume_recipe_items, update_crafting_result } from "./crafting.ts";
|
||||
import { OpenScreen, SavedPlayer, ServerPlayer } from "./player.ts";
|
||||
import { ServerWorld, Tile } from "./world.ts";
|
||||
import {
|
||||
BLOCK_DROP_PICKUP_DELAY,
|
||||
DESPAWN_TICKS,
|
||||
ITEM_SIZE,
|
||||
ItemEntity,
|
||||
PLAYER_DROP_PICKUP_DELAY,
|
||||
SavedItemEntity,
|
||||
} from "./item_entity.ts";
|
||||
|
||||
// how far from a player's eyes a block can be changed, a bit more than the client's reach
|
||||
const MAX_REACH = 8;
|
||||
const EYE_HEIGHT = 1.69;
|
||||
// how far past a player's box items get picked up from, sideways and up or down, like minecraft
|
||||
const PICKUP_REACH_XZ = 1;
|
||||
const PICKUP_REACH_Y = 0.5;
|
||||
// items this close (past their own size) merge into one stack
|
||||
const MERGE_DISTANCE = 0.5;
|
||||
// tiles further than this many chunks from every player don't tick
|
||||
const SIMULATION_DISTANCE = 6;
|
||||
const MAX_GIVE = 64 * 36;
|
||||
@@ -60,6 +75,8 @@ export interface SavedWorld {
|
||||
mod_data?: unknown;
|
||||
}[];
|
||||
players: Record<string, SavedPlayer>;
|
||||
// items on the ground
|
||||
entities?: SavedItemEntity[];
|
||||
// ctx.storage of every mod, by mod id
|
||||
mod_storage?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
@@ -81,6 +98,9 @@ export class GameServer {
|
||||
#pending = new Map<number, { name: unknown; since: number }>();
|
||||
#saved_players: Record<string, SavedPlayer> = {};
|
||||
#tick = 0;
|
||||
// items on the ground, by id
|
||||
#entities = new Map<string, ItemEntity>();
|
||||
#next_entity_id = 1;
|
||||
#mods: GameMods;
|
||||
mods: ModRuntime;
|
||||
recipes: RecipeBook;
|
||||
@@ -106,6 +126,10 @@ export class GameServer {
|
||||
}
|
||||
this.world.tiles.set(`${tile.x},${tile.y},${tile.z}`, { ...tile, containers });
|
||||
}
|
||||
for (const entity of saved?.entities ?? []) {
|
||||
const item = ItemEntity.load(this.#new_entity_id(), entity);
|
||||
this.#entities.set(item.id, item);
|
||||
}
|
||||
this.#saved_players = saved?.players ?? {};
|
||||
this.world.dirty = saved?.version !== 2;
|
||||
|
||||
@@ -189,6 +213,8 @@ export class GameServer {
|
||||
this.world.dirty = true;
|
||||
}
|
||||
|
||||
this.#tick_items();
|
||||
|
||||
this.mods.tick();
|
||||
this.mods.check_watched_containers();
|
||||
|
||||
@@ -224,6 +250,7 @@ export class GameServer {
|
||||
mod_data: tile.mod_data,
|
||||
})),
|
||||
players: this.#saved_players,
|
||||
entities: [...this.#entities.values()].map((entity) => entity.save()),
|
||||
mod_storage: this.mods.storage,
|
||||
};
|
||||
this.world.dirty = false;
|
||||
@@ -248,10 +275,45 @@ export class GameServer {
|
||||
stack.amount = Math.min(left, stack.max_amount);
|
||||
if (data !== undefined) stack.data = structuredClone(data);
|
||||
left -= stack.amount;
|
||||
player.give(stack);
|
||||
this.give_stack(player, stack);
|
||||
}
|
||||
}
|
||||
|
||||
// into the inventory, whatever doesn't fit drops at the player's feet
|
||||
give_stack(player: ServerPlayer, stack: ItemStack) {
|
||||
if (player.inventory.add_item(stack) > 0) {
|
||||
this.spawn_item(player.x, player.y, player.z, stack, PLAYER_DROP_PICKUP_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
spawn_item(x: number, y: number, z: number, stack: ItemStack, pickup_delay = 0): ItemEntity {
|
||||
const entity = new ItemEntity(this.#new_entity_id(), stack, x, y, z, pickup_delay);
|
||||
this.#entities.set(entity.id, entity);
|
||||
this.#broadcast({ type: "add_entity", entity: entity.info() });
|
||||
this.world.dirty = true;
|
||||
return entity;
|
||||
}
|
||||
|
||||
// like minecraft's Block.popResource: from a bit off the block's middle, flying up and out a little
|
||||
pop_item(x: number, y: number, z: number, stack: ItemStack) {
|
||||
const spread = () => (Math.random() - 0.5) * 0.5;
|
||||
const entity = this.spawn_item(
|
||||
x + 0.5 + spread(),
|
||||
y + 0.5 - ITEM_SIZE / 2 + spread(),
|
||||
z + 0.5 + spread(),
|
||||
stack,
|
||||
BLOCK_DROP_PICKUP_DELAY,
|
||||
);
|
||||
entity.vx = (Math.random() - 0.5) * 4;
|
||||
entity.vy = 4;
|
||||
entity.vz = (Math.random() - 0.5) * 4;
|
||||
return entity;
|
||||
}
|
||||
|
||||
item_entities(): ItemEntity[] {
|
||||
return [...this.#entities.values()];
|
||||
}
|
||||
|
||||
send_chat(player: ServerPlayer, text: string) {
|
||||
this.#send(player, { type: "chat", text });
|
||||
}
|
||||
@@ -392,6 +454,7 @@ export class GameServer {
|
||||
name: player.name,
|
||||
players: [...this.#players.values()].map((p) => p.info()),
|
||||
changes: this.world.all_changes(),
|
||||
entities: [...this.#entities.values()].map((entity) => entity.info()),
|
||||
spawn: { x: player.x, y: player.y, z: player.z, yaw: player.yaw, pitch: player.pitch },
|
||||
selected_slot: player.selected_slot,
|
||||
});
|
||||
@@ -475,7 +538,7 @@ export class GameServer {
|
||||
|
||||
this.set_block(x, y, z, AIR_ID, player);
|
||||
if (drops) {
|
||||
player.give(new ItemStack(info.drop_table!));
|
||||
this.pop_item(x, y, z, new ItemStack(info.drop_table!));
|
||||
}
|
||||
this.mods.after.block_break.emit(event);
|
||||
}
|
||||
@@ -687,14 +750,118 @@ export class GameServer {
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const item = player.crafting.get_item(i);
|
||||
if (item) {
|
||||
player.give(item);
|
||||
player.crafting.set_item(i, undefined);
|
||||
this.give_stack(player, item);
|
||||
}
|
||||
}
|
||||
update_crafting_result(player.crafting, this.recipes.shaped);
|
||||
if (player.cursor.item) {
|
||||
player.give(player.cursor.item);
|
||||
const item = player.cursor.item;
|
||||
player.cursor.item = undefined;
|
||||
this.give_stack(player, item);
|
||||
}
|
||||
}
|
||||
|
||||
// items on the ground
|
||||
|
||||
#new_entity_id() {
|
||||
return `item${this.#next_entity_id++}`;
|
||||
}
|
||||
|
||||
#remove_item(entity: ItemEntity) {
|
||||
this.#entities.delete(entity.id);
|
||||
this.#broadcast({ type: "remove_entity", id: entity.id });
|
||||
}
|
||||
|
||||
// like minecraft's ItemEntity.tick: only near players, falling, merging, despawning, and getting picked up
|
||||
#tick_items() {
|
||||
if (this.#entities.size === 0) {
|
||||
return;
|
||||
}
|
||||
const is_solid = (x: number, y: number, z: number) => this.world.get_block_nid(x, y, z) !== AIR;
|
||||
|
||||
const active = [...this.#entities.values()].filter((entity) => this.#near_any_player(entity.x, entity.z));
|
||||
for (const entity of active) {
|
||||
entity.tick(is_solid);
|
||||
if (entity.age >= DESPAWN_TICKS) {
|
||||
this.#remove_item(entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#tick % 2 === 0) {
|
||||
this.#merge_items(active.filter((entity) => this.#entities.has(entity.id)));
|
||||
}
|
||||
|
||||
for (const player of this.#players.values()) {
|
||||
for (const entity of active) {
|
||||
if (this.#entities.has(entity.id) && entity.pickup_delay === 0 && this.#can_pick_up(player, entity)) {
|
||||
this.#pick_up(player, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const entity of active) {
|
||||
const moved = entity.x !== entity.sent_x || entity.y !== entity.sent_y || entity.z !== entity.sent_z;
|
||||
if (moved && this.#entities.has(entity.id)) {
|
||||
entity.sent_x = entity.x;
|
||||
entity.sent_y = entity.y;
|
||||
entity.sent_z = entity.z;
|
||||
this.#broadcast({ type: "move_entity", id: entity.id, x: entity.x, y: entity.y, z: entity.z });
|
||||
}
|
||||
}
|
||||
|
||||
this.world.dirty = true;
|
||||
}
|
||||
|
||||
// the smaller stack goes into the bigger one, like minecraft's ItemEntity.tryToMerge
|
||||
#merge_items(entities: ItemEntity[]) {
|
||||
for (const a of entities) {
|
||||
for (const b of entities) {
|
||||
if (!this.#entities.has(a.id) || !this.#entities.has(b.id) || !a.can_merge_with(b)) {
|
||||
continue;
|
||||
}
|
||||
const reach = MERGE_DISTANCE + ITEM_SIZE;
|
||||
if (Math.abs(a.x - b.x) > reach || Math.abs(a.z - b.z) > reach || Math.abs(a.y - b.y) > ITEM_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [into, from] = a.item.amount >= b.item.amount ? [a, b] : [b, a];
|
||||
const moving = Math.min(from.item.amount, into.item.max_amount - into.item.amount);
|
||||
into.item.amount += moving;
|
||||
from.item.amount -= moving;
|
||||
into.pickup_delay = Math.max(into.pickup_delay, from.pickup_delay);
|
||||
into.age = Math.min(into.age, from.age);
|
||||
|
||||
this.#broadcast({ type: "set_entity_item", id: into.id, item: into.item.to_data() });
|
||||
if (from.item.amount === 0) {
|
||||
this.#remove_item(from);
|
||||
} else {
|
||||
this.#broadcast({ type: "set_entity_item", id: from.id, item: from.item.to_data() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the player's box grown by the pickup reach touches the item's box
|
||||
#can_pick_up(player: ServerPlayer, entity: ItemEntity) {
|
||||
const reach_xz = PLAYER_WIDTH / 2 + PICKUP_REACH_XZ + ITEM_SIZE / 2;
|
||||
return Math.abs(player.x - entity.x) <= reach_xz && Math.abs(player.z - entity.z) <= reach_xz &&
|
||||
entity.y + ITEM_SIZE >= player.y - PICKUP_REACH_Y &&
|
||||
entity.y <= player.y + PLAYER_HEIGHT + PICKUP_REACH_Y;
|
||||
}
|
||||
|
||||
// as much as fits, the rest stays on the ground
|
||||
#pick_up(player: ServerPlayer, entity: ItemEntity) {
|
||||
const before = entity.item.amount;
|
||||
const left = player.inventory.add_item(entity.item);
|
||||
if (left === before) {
|
||||
return;
|
||||
}
|
||||
if (left === 0) {
|
||||
this.#entities.delete(entity.id);
|
||||
this.#broadcast({ type: "take_entity", id: entity.id, player: player.id });
|
||||
} else {
|
||||
this.#broadcast({ type: "set_entity_item", id: entity.id, item: entity.item.to_data() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,7 +918,7 @@ export class GameServer {
|
||||
|
||||
#in_reach(player: ServerPlayer, x: number, y: number, z: number) {
|
||||
const dx = x + 0.5 - player.x;
|
||||
const dy = y + 0.5 - (player.y + EYE_HEIGHT);
|
||||
const dy = y + 0.5 - (player.y + PLAYER_EYE_HEIGHT);
|
||||
const dz = z + 0.5 - player.z;
|
||||
return dx * dx + dy * dy + dz * dz <= MAX_REACH * MAX_REACH;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user