import { assert, assertEquals } from "@std/assert"; import { ItemStack } from "$/common/inventory.ts"; import { test_game } from "./helpers.ts"; // a stone floor at y 99 so drops have something to land on, high above the generated terrain function build_floor(game: Awaited>["game"]) { for (let x = -3; x <= 9; x++) { for (let z = -4; z <= 4; z++) { game.set_block(x, 99, z, "bworld:stone"); } } } const inventory_count = ( messages: { type: string; container?: string; items?: ({ id: string; count: number } | null)[] }[], id: string, ) => (messages.filter((m) => m.type === "container" && m.container === "inventory").at(-1)?.items ?? []) .reduce((sum, item) => sum + (item?.id === id ? item.count : 0), 0); Deno.test("breaking a block drops its item, which lands and gets picked up by walking to it", async () => { const { game, take, send, join } = await test_game("mods"); join(1, "alice"); build_floor(game); game.set_block(5, 100, 0, "bworld:dirt"); send(1, { type: "move", x: 0.5, y: 100, z: 0.5, yaw: 0, pitch: 0 }); take(1); send(1, { type: "break_block", x: 5, y: 100, z: 0 }); const messages = take(1); const added = messages.find((m) => m.type === "add_entity"); assertEquals(added?.entity.kind, "item"); assertEquals(added?.entity.item, { id: "bworld:dirt", count: 1 }); assertEquals(inventory_count(messages, "bworld:dirt"), 0, "it drops instead of going into the inventory"); for (let i = 0; i < 40; i++) game.tick(); const [item] = game.item_entities(); assert(item.on_ground, "it fell onto the floor"); assertEquals(item.y, 100); assert(Math.abs(item.x - 5.5) < 1.5 && Math.abs(item.z - 0.5) < 1.5, "it stays near the block"); assert(take(1).some((m) => m.type === "move_entity" && m.id === item.id)); send(1, { type: "move", x: item.x, y: 100, z: item.z, yaw: 0, pitch: 0 }); game.tick(); const picked = take(1); assert(picked.some((m) => m.type === "take_entity" && m.id === item.id)); assertEquals(inventory_count(picked, "bworld:dirt"), 1); assertEquals(game.item_entities(), []); }); Deno.test("items that don't fit in the inventory drop at the player's feet and stay there", async () => { const { game, take, send, join } = await test_game("mods"); join(1, "alice"); build_floor(game); send(1, { type: "move", x: 0.5, y: 100, z: 0.5, yaw: 0, pitch: 0 }); // a full inventory, then 10 more send(1, { type: "chat", text: `/give bworld:stone ${64 * 36}` }); assertEquals(game.item_entities(), []); send(1, { type: "chat", text: "/give bworld:stone 10" }); const [item] = game.item_entities(); assertEquals(item.item.amount, 10); for (let i = 0; i < 60; i++) game.tick(); assertEquals(game.item_entities().length, 1, "a full inventory can't pick it up"); assert(take(1).some((m) => m.type === "add_entity")); }); Deno.test("stacks next to each other merge, and items on the ground are saved with the world", async () => { const { game, join } = await test_game("mods"); join(1, "alice"); build_floor(game); for (let i = 0; i < 3; i++) { game.spawn_item(6.5, 100, 0.5, new ItemStack("bworld:dirt", 2)); } game.tick(); game.tick(); assertEquals(game.item_entities().map((entity) => entity.item.amount), [6]); const saved = game.save(); const reloaded = await test_game("mods", saved); assertEquals(reloaded.game.item_entities().map((entity) => [entity.item.type_id, entity.item.amount]), [[ "bworld:dirt", 6, ]]); const joined = reloaded.join(2, "bob"); assertEquals(joined.entities.map((entity: { item: unknown }) => entity.item), [{ id: "bworld:dirt", count: 6 }]); }); Deno.test("q throws one of an item the way the player looks, ctrl throws the whole stack", async () => { const { game, take, send, join } = await test_game("mods"); join(1, "alice"); build_floor(game); // looking straight along -z (yaw 0) send(1, { type: "move", x: 0.5, y: 100, z: 0.5, yaw: 0, pitch: 0 }); send(1, { type: "chat", text: "/give bworld:stone 5" }); take(1); send(1, { type: "drop_item", container: "inventory", index: 0, all: false }); const [thrown] = game.item_entities(); assertEquals(thrown.item.amount, 1); assert(thrown.vz < -5 && Math.abs(thrown.vx) < 1, "thrown forwards"); assertEquals(inventory_count(take(1), "bworld:stone"), 4); for (let i = 0; i < 20; i++) game.tick(); assert(thrown.z < -1, "it flew away from the player"); assertEquals(game.item_entities().length, 1, "and isn't picked straight back up"); send(1, { type: "drop_item", container: "inventory", index: 0, all: true }); assertEquals(game.item_entities().map((entity) => entity.item.amount).sort(), [1, 4]); assertEquals(inventory_count(take(1), "bworld:stone"), 0); // empty slots and the crafting result don't throw anything send(1, { type: "drop_item", container: "inventory", index: 0, all: true }); send(1, { type: "drop_item", container: "crafting", index: 9, all: true }); assertEquals(game.item_entities().length, 2); }); Deno.test("water has no collision, items sink through it to the floor", async () => { const { game, join } = await test_game("mods"); join(1, "alice"); build_floor(game); for (let y = 100; y <= 102; y++) { game.set_block(6, y, 0, "bworld:water"); } const item = game.spawn_item(6.5, 104, 0.5, new ItemStack("bworld:dirt")); for (let i = 0; i < 40; i++) game.tick(); assert(item.on_ground); assertEquals(item.y, 100, "on the stone under the water"); });