Files
bworld/tests/base_game_test.ts
T

198 lines
9.1 KiB
TypeScript

// the base game's block and item behavior: hoeing, chests, furnaces, crops and the watering can
import { assert, assertEquals } from "@std/assert";
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { get_state_value } from "$/common/utils.ts";
import { test_game } from "./helpers.ts";
// deno-lint-ignore no-explicit-any
type Msg = any;
type Game = Awaited<ReturnType<typeof test_game>>;
const last_container = (messages: Msg[], container: string) =>
messages.filter((m) => m.type === "container" && m.container === container).at(-1)?.items;
// a player standing on a stone floor at y 99, high above the terrain, with the items given in their inventory
async function setup(items: [string, number][] = []) {
const t = await test_game("mods");
t.join(1, "alice");
for (let x = -3; x <= 3; x++) {
for (let z = -3; z <= 3; z++) t.game.set_block(x, 99, z, "bworld:stone");
}
t.send(1, { type: "move", x: 0.5, y: 100, z: 0.5, yaw: 0, pitch: 0 });
for (const [id, count] of items) t.send(1, { type: "chat", text: `/give ${id} ${count}` });
return t;
}
// the inventory slot holding an item, from what the server sent last
function slot_of(messages: Msg[], id: string) {
const slot = (last_container(messages, "inventory") ?? []).findIndex((i: Msg) => i?.id === id);
assert(slot >= 0, `no ${id} in the inventory`);
return slot;
}
// picks a whole inventory stack up and puts it into a slot of the open screen
function move_to_screen(t: Game, from: number, to: number) {
t.send(1, { type: "click", container: "inventory", index: from, button: 0 });
t.send(1, { type: "click", container: "screen", index: to, button: 0 });
}
Deno.test("a hoe turns grass and dirt into hoed dirt, anything else places nothing", async () => {
const t = await setup([["bworld:hoe", 1]]);
t.game.set_block(1, 100, 0, "bworld:grass");
t.game.set_block(2, 100, 0, "bworld:dirt");
t.send(1, { type: "select_slot", slot: slot_of(t.take(1), "bworld:hoe") });
t.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
t.send(1, { type: "use_block", x: 2, y: 100, z: 0, face: "top" });
assertEquals(t.game.world.get_block_id(1, 100, 0), "bworld:hoed_dirt");
assertEquals(t.game.world.get_block_id(2, 100, 0), "bworld:hoed_dirt");
// hoed dirt stays hoed dirt, and the hand doesn't do it
t.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
assertEquals(t.game.world.get_block_id(1, 100, 0), "bworld:hoed_dirt");
t.game.set_block(1, 100, 1, "bworld:grass");
t.send(1, { type: "select_slot", slot: 8 });
t.send(1, { type: "use_block", x: 1, y: 100, z: 1, face: "top" });
assertEquals(t.game.world.get_block_id(1, 100, 1), "bworld:grass");
});
Deno.test("a chest keeps items, shows them to everyone, and drops them when broken", async () => {
const t = await setup([["bworld:chest", 1], ["bworld:coal", 10]]);
t.game.set_block(1, 100, 0, "bworld:chest");
const coal = slot_of(t.take(1), "bworld:coal");
t.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
const opened = t.take(1).find((m) => m.type === "open_screen");
assertEquals(opened.layout.rows, 3);
assertEquals(opened.layout.slots.length, 27);
// nothing got placed on top
assertEquals(t.game.world.get_block_id(1, 101, 0), "bworld:air");
move_to_screen(t, coal, 4);
assertEquals(last_container(t.take(1), "screen")[4], { id: "bworld:coal", count: 10 });
t.send(1, { type: "close_screen" });
// still there after opening it again, and after saving and loading
const loaded = await test_game("mods", t.game.save());
loaded.join(1, "alice");
loaded.send(1, { type: "move", x: 0.5, y: 100, z: 0.5, yaw: 0, pitch: 0 });
loaded.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
assertEquals(last_container(loaded.take(1), "screen")[4], { id: "bworld:coal", count: 10 });
// breaking it closes the screen and drops the chest and what was inside
loaded.send(1, { type: "break_block", x: 1, y: 100, z: 0 });
const messages = loaded.take(1);
assert(messages.some((m) => m.type === "close_screen"));
const dropped = loaded.game.item_entities().map((e) => e.item.to_data());
assertEquals(
dropped.sort((a, b) => a.id.localeCompare(b.id)),
[{ id: "bworld:chest", count: 1 }, { id: "bworld:coal", count: 10 }],
);
});
Deno.test("a furnace burns fuel to smelt, shows its progress, and only lets the result be taken", async () => {
const t = await setup([["bworld:iron_ore", 2], ["bworld:coal", 1]]);
t.game.set_block(1, 100, 0, "bworld:furnace");
const messages = t.take(1);
const ore = slot_of(messages, "bworld:iron_ore");
const coal = slot_of(messages, "bworld:coal");
t.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
const opened = t.take(1).find((m) => m.type === "open_screen");
assertEquals(opened.layout.slots.map((s: Msg) => s.index), [0, 1, 2]);
assertEquals(opened.layout.slots[2].output, true);
assertEquals(opened.layout.bars.map((b: Msg) => b.value), ["fuel", "progress"]);
move_to_screen(t, ore, 0);
move_to_screen(t, coal, 1);
t.take(1);
// iron takes 200 ticks
for (let i = 0; i < 100; i++) t.game.tick();
const halfway = t.take(1).filter((m) => m.type === "screen_properties").at(-1).properties;
assertEquals(halfway.progress_max, 200);
assert(halfway.progress >= 98 && halfway.progress <= 100, `progress ${halfway.progress}`);
assertEquals(halfway.fuel_max, 1000);
for (let i = 0; i < 110; i++) t.game.tick();
const screen = last_container(t.take(1), "screen");
assertEquals(screen[0], { id: "bworld:iron_ore", count: 1 });
assertEquals(screen[1], null, "the coal got used");
assertEquals(screen[2], { id: "bworld:iron_ingot", count: 1 });
// nothing can be put into the result slot, but the result can be taken
t.send(1, { type: "click", container: "screen", index: 0, button: 0 });
t.send(1, { type: "click", container: "screen", index: 2, button: 0 });
assertEquals(last_container(t.take(1), "screen")[2], { id: "bworld:iron_ingot", count: 1 });
t.send(1, { type: "click", container: "screen", index: 0, button: 0 });
t.send(1, { type: "click", container: "screen", index: 2, button: 0 });
const after = t.take(1);
assertEquals(last_container(after, "screen")[2], null);
assertEquals(after.filter((m) => m.type === "cursor").at(-1).item, { id: "bworld:iron_ingot", count: 1 });
});
Deno.test("bone meal grows wheat until it's fully grown", async () => {
const t = await setup([["bworld:bone_meal", 8]]);
t.game.set_block(1, 100, 0, "bworld:wheat");
const wheat = EverythingRegistry.get<BlockRegistry>("blocks", "bworld:wheat")!;
const age = () => get_state_value(t.game.world.get_block_value(1, 100, 0), wheat, "age")!;
assertEquals(age(), 0);
const slot = slot_of(t.take(1), "bworld:bone_meal");
t.send(1, { type: "select_slot", slot });
// one stage at a time, and once it's fully grown it stops using bone meal
t.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
assertEquals(age(), 1);
for (let i = 0; i < 7; i++) t.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
assertEquals(age(), 7);
assertEquals(last_container(t.take(1), "inventory")[slot]?.count, 1);
assertEquals(t.game.world.get_block_id(1, 101, 0), "bworld:air");
});
Deno.test("the last bone meal is used up", async () => {
const t = await setup([["bworld:bone_meal", 1]]);
t.game.set_block(1, 100, 0, "bworld:wheat");
const slot = slot_of(t.take(1), "bworld:bone_meal");
t.send(1, { type: "select_slot", slot });
t.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
assertEquals(last_container(t.take(1), "inventory")[slot], null);
});
Deno.test("a new watering can starts empty", async () => {
const t = await setup([["bworld:watering_can", 1]]);
const can = last_container(t.take(1), "inventory").find((i: Msg) => i?.id === "bworld:watering_can");
assertEquals(can.data, { water: 0, max_water: 32 });
});
Deno.test("chests and furnaces from a world saved before they were mod code keep their items and keep smelting", async () => {
const t = await test_game("mods", Deno.readTextFileSync("tests/fixtures/world_v2.json"), "fixture-seed");
t.join(1, "alice");
t.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
const chest = last_container(t.take(1), "screen");
assertEquals(chest.length, 27);
assertEquals(chest[4], { id: "bworld:coal", count: 6 });
t.send(1, { type: "close_screen" });
t.send(1, { type: "use_block", x: 2, y: 100, z: 0, face: "top" });
let messages = t.take(1);
assertEquals(last_container(messages, "screen"), [
{ id: "bworld:iron_ore", count: 2 },
{ id: "bworld:coal", count: 2 },
{ id: "bworld:iron_ingot", count: 1 },
]);
const properties = messages.filter((m) => m.type === "screen_properties").at(-1).properties;
assertEquals(properties, { progress: 50, progress_max: 200, fuel: 751, fuel_max: 1000 });
// 150 more ticks finish the second ingot
for (let i = 0; i < 150; i++) t.game.tick();
messages = t.take(1);
assertEquals(last_container(messages, "screen")[2], { id: "bworld:iron_ingot", count: 2 });
// saved in the new format, nothing lost
const saved = JSON.parse(t.game.save());
assertEquals(saved.version, 3);
assert(saved.tiles.every((tile: Msg) => tile.data === undefined && tile.containers === undefined));
assertEquals(Object.keys(saved.containers).length, 2);
});