Actually implement mod code into server
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
// 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);
|
||||
});
|
||||
Vendored
+436
@@ -0,0 +1,436 @@
|
||||
{
|
||||
"version": 2,
|
||||
"seed": "fixture-seed",
|
||||
"changes": [
|
||||
[
|
||||
-3,
|
||||
99,
|
||||
-3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-3,
|
||||
99,
|
||||
-2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-3,
|
||||
99,
|
||||
-1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-2,
|
||||
99,
|
||||
-3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-2,
|
||||
99,
|
||||
-2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-2,
|
||||
99,
|
||||
-1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-1,
|
||||
99,
|
||||
-3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-1,
|
||||
99,
|
||||
-2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-1,
|
||||
99,
|
||||
-1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-3,
|
||||
99,
|
||||
0,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-3,
|
||||
99,
|
||||
1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-3,
|
||||
99,
|
||||
2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-3,
|
||||
99,
|
||||
3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-2,
|
||||
99,
|
||||
0,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-2,
|
||||
99,
|
||||
1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-2,
|
||||
99,
|
||||
2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-2,
|
||||
99,
|
||||
3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-1,
|
||||
99,
|
||||
0,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-1,
|
||||
99,
|
||||
1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-1,
|
||||
99,
|
||||
2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
-1,
|
||||
99,
|
||||
3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
0,
|
||||
99,
|
||||
-3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
0,
|
||||
99,
|
||||
-2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
0,
|
||||
99,
|
||||
-1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
1,
|
||||
99,
|
||||
-3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
1,
|
||||
99,
|
||||
-2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
1,
|
||||
99,
|
||||
-1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
2,
|
||||
99,
|
||||
-3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
2,
|
||||
99,
|
||||
-2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
2,
|
||||
99,
|
||||
-1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
3,
|
||||
99,
|
||||
-3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
3,
|
||||
99,
|
||||
-2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
3,
|
||||
99,
|
||||
-1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
0,
|
||||
99,
|
||||
0,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
0,
|
||||
99,
|
||||
1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
0,
|
||||
99,
|
||||
2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
0,
|
||||
99,
|
||||
3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
1,
|
||||
99,
|
||||
0,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
1,
|
||||
99,
|
||||
1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
1,
|
||||
99,
|
||||
2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
1,
|
||||
99,
|
||||
3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
2,
|
||||
99,
|
||||
0,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
2,
|
||||
99,
|
||||
1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
2,
|
||||
99,
|
||||
2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
2,
|
||||
99,
|
||||
3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
3,
|
||||
99,
|
||||
0,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
3,
|
||||
99,
|
||||
1,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
3,
|
||||
99,
|
||||
2,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
3,
|
||||
99,
|
||||
3,
|
||||
"bworld:stone"
|
||||
],
|
||||
[
|
||||
1,
|
||||
100,
|
||||
0,
|
||||
"bworld:chest"
|
||||
],
|
||||
[
|
||||
2,
|
||||
100,
|
||||
0,
|
||||
"bworld:furnace"
|
||||
]
|
||||
],
|
||||
"tiles": [
|
||||
{
|
||||
"id": "bworld:chest",
|
||||
"x": 1,
|
||||
"y": 100,
|
||||
"z": 0,
|
||||
"data": {},
|
||||
"containers": {
|
||||
"main": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"id": "bworld:coal",
|
||||
"count": 6
|
||||
},
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bworld:furnace",
|
||||
"x": 2,
|
||||
"y": 100,
|
||||
"z": 0,
|
||||
"data": {
|
||||
"progress": 50,
|
||||
"progress_max": 200,
|
||||
"fuel": 751,
|
||||
"fuel_max": 1000
|
||||
},
|
||||
"containers": {
|
||||
"main": [
|
||||
{
|
||||
"id": "bworld:iron_ore",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"id": "bworld:coal",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"id": "bworld:iron_ingot",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"players": {
|
||||
"alice": {
|
||||
"x": 0.5,
|
||||
"y": 100,
|
||||
"z": 0.5,
|
||||
"yaw": 0,
|
||||
"pitch": 0,
|
||||
"selected_slot": 0,
|
||||
"inventory": [
|
||||
{
|
||||
"id": "bworld:coal",
|
||||
"count": 2
|
||||
},
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
}
|
||||
},
|
||||
"entities": [],
|
||||
"mod_storage": {}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export function mod_sources(mods_dir: string): ServerModSource[] {
|
||||
},
|
||||
data: {
|
||||
blocks: mod.blocks.map((b) => b.json),
|
||||
models: mod.models.map((m) => m.json),
|
||||
items: mod.items.map((i) => i.json),
|
||||
recipes: mod.recipes.map((r) => r.json),
|
||||
ores: mod.ores.map((o) => o.json),
|
||||
|
||||
@@ -3,7 +3,6 @@ import { AIR, CHUNK_HEIGHT } from "$/common/constants.ts";
|
||||
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
import { block_from_json, block_to_json, item_from_json, item_to_json } from "$/common/mod_data.ts";
|
||||
import { generate_raw_chunk } from "$/common/generation.ts";
|
||||
import { get_state_value } from "$/common/utils.ts";
|
||||
import { load_worldgen } from "$/common/worldgen_loader.ts";
|
||||
import { create_mod } from "$/tools/new_mod.ts";
|
||||
import { PROTOCOL_VERSION } from "$/common/protocol.ts";
|
||||
@@ -35,37 +34,6 @@ Deno.test("the base game loads from mods/bworld", async () => {
|
||||
assert(EverythingRegistry.get<ItemRegistry>("items", "bworld:watering_can")?.on_create);
|
||||
});
|
||||
|
||||
Deno.test("bone meal grows wheat until it's fully grown", async () => {
|
||||
const { game, take, send } = await test_game("mods");
|
||||
game.on_connect(1);
|
||||
send(1, { type: "hello", name: "alice", protocol: PROTOCOL_VERSION });
|
||||
send(1, { type: "ready" });
|
||||
|
||||
let y = CHUNK_HEIGHT - 1;
|
||||
while (game.world.get_block_nid(1, y, 1) === AIR) y--;
|
||||
y += 1;
|
||||
game.set_block(1, y, 1, "bworld:wheat");
|
||||
const wheat = EverythingRegistry.get<BlockRegistry>("blocks", "bworld:wheat")!;
|
||||
const age = () => get_state_value(game.world.get_block_value(1, y, 1), wheat, "age")!;
|
||||
assertEquals(age(), 0);
|
||||
|
||||
send(1, { type: "chat", text: "/give bworld:bone_meal 8" });
|
||||
const slot = inventory_of(take(1)).findIndex((i: Msg) => i?.id === "bworld:bone_meal");
|
||||
send(1, { type: "select_slot", slot });
|
||||
send(1, { type: "move", x: 2.5, y, z: 2.5, yaw: 0, pitch: 0 });
|
||||
|
||||
// 2 to 5 stages at a time
|
||||
send(1, { type: "use_block", x: 1, y, z: 1, face: "top" });
|
||||
assert(age() >= 2 && age() <= 5, `age ${age()}`);
|
||||
// at most 3 more uses to reach 7, then it stops using bone meal
|
||||
for (let i = 0; i < 5; i++) send(1, { type: "use_block", x: 1, y, z: 1, face: "top" });
|
||||
assertEquals(age(), 7);
|
||||
const left = inventory_of(take(1))[slot]?.count;
|
||||
assert(left >= 4 && left <= 6, `${left} bone meal left`);
|
||||
// nothing got placed on top
|
||||
assertEquals(game.world.get_block_id(1, y + 1, 1), "bworld:air");
|
||||
});
|
||||
|
||||
Deno.test("mods/bworld json survives going to the registry and back", async () => {
|
||||
await test_game("mods");
|
||||
for (const file of Deno.readDirSync("mods/bworld/blocks")) {
|
||||
|
||||
@@ -24,6 +24,28 @@ export function setup(ctx: ServerContext) {
|
||||
on_tick() { throw new Error("broken on purpose"); },
|
||||
on_click(block, _params, player) { log("click", [block.id, player.name]); },
|
||||
});
|
||||
// a box that only takes coal, counts how often it's opened, and gives up its items when broken
|
||||
ctx.components.register_block("testmod:coal_box", {
|
||||
on_interact(block, _params, player) {
|
||||
block.data ??= { container: ctx.containers.create(2).id, opened: 0 };
|
||||
block.data.opened += 1;
|
||||
const screen = ctx.ui.open_container(player, {
|
||||
container: ctx.containers.get(block.data.container)!,
|
||||
layout: [{ slot: 0, x: 0, y: 0, filter: (item) => item.id === "bworld:coal" }, { slot: 1, x: 1, y: 0 }],
|
||||
});
|
||||
screen.set_property("opened", block.data.opened);
|
||||
screen.on_close(() => log("closed", screen.open));
|
||||
return true;
|
||||
},
|
||||
on_break(block) {
|
||||
const container = ctx.containers.get(block.data.container)!;
|
||||
for (let slot = 0; slot < container.size; slot++) {
|
||||
const item = container.get(slot);
|
||||
if (item) ctx.world.drop_item(block.x, block.y, block.z, item);
|
||||
}
|
||||
ctx.containers.delete(container.id);
|
||||
},
|
||||
});
|
||||
ctx.components.register_item("testmod:wand", {
|
||||
on_use(item, params: { power: number }, player) { log("use", [item.id, params.power, player.name]); },
|
||||
});
|
||||
@@ -84,6 +106,10 @@ function test_mods() {
|
||||
`${mod}/blocks/broken.json`,
|
||||
block("testmod:broken", { components: { "testmod:broken": {} } }),
|
||||
);
|
||||
Deno.writeTextFileSync(
|
||||
`${mod}/blocks/coal_box.json`,
|
||||
block("testmod:coal_box", { interactive: true, components: { "testmod:coal_box": {} } }),
|
||||
);
|
||||
Deno.writeTextFileSync(
|
||||
`${mod}/blocks/lamp.json`,
|
||||
block("testmod:lamp", {
|
||||
@@ -227,3 +253,50 @@ Deno.test("/tps reports the loop, and mods can't take engine commands", async ()
|
||||
assert(error.includes("/tps belongs to the engine"), error);
|
||||
Deno.removeSync(clash, { recursive: true });
|
||||
});
|
||||
|
||||
Deno.test("container screens: filters, properties, closing, saving, and dropping what's inside", async () => {
|
||||
const { game, send, take, log, dir } = await setup();
|
||||
game.set_block(1, 100, 0, "testmod:coal_box");
|
||||
send(1, { type: "chat", text: "/give bworld:coal 3" });
|
||||
send(1, { type: "chat", text: "/give bworld:stone 1" });
|
||||
take(1);
|
||||
|
||||
send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
|
||||
let messages = take(1);
|
||||
const opened = messages.find((m) => m.type === "open_screen");
|
||||
assertEquals(opened.layout, { rows: 1, slots: [{ index: 0, x: 0, y: 0 }, { index: 1, x: 1, y: 0 }], bars: [] });
|
||||
assertEquals(messages.find((m) => m.type === "screen_properties").properties, { opened: 1 });
|
||||
|
||||
// stone doesn't go into the coal slot, but does into the other one. coal goes in
|
||||
const screen = (messages: { type: string; container?: string; items?: unknown[] }[]) =>
|
||||
messages.filter((m) => m.type === "container" && m.container === "screen").at(-1)?.items;
|
||||
send(1, { type: "click", container: "inventory", index: 1, button: 0 });
|
||||
send(1, { type: "click", container: "screen", index: 0, button: 0 });
|
||||
send(1, { type: "click", container: "screen", index: 1, button: 0 });
|
||||
send(1, { type: "click", container: "inventory", index: 0, button: 0 });
|
||||
send(1, { type: "click", container: "screen", index: 0, button: 0 });
|
||||
messages = take(1);
|
||||
assertEquals(screen(messages), [{ id: "bworld:coal", count: 3 }, { id: "bworld:stone", count: 1 }]);
|
||||
|
||||
send(1, { type: "close_screen" });
|
||||
assertEquals(log().closed, [false]);
|
||||
|
||||
// the items and the block's data are saved
|
||||
const loaded = await test_game(dir, 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" });
|
||||
messages = loaded.take(1);
|
||||
assertEquals(screen(messages), [{ id: "bworld:coal", count: 3 }, { id: "bworld:stone", count: 1 }]);
|
||||
assertEquals(messages.find((m) => m.type === "screen_properties").properties, { opened: 2 });
|
||||
|
||||
// breaking it closes the screen, runs on_close, and drops what was inside
|
||||
loaded.send(1, { type: "break_block", x: 1, y: 100, z: 0 });
|
||||
assert(loaded.take(1).some((m) => m.type === "close_screen"));
|
||||
const logged = loaded.game.mods.storage.testmod.log as Record<string, unknown[]>;
|
||||
assertEquals(logged.closed, [false, false]);
|
||||
const dropped = loaded.game.item_entities().map((e) => e.item.to_data().id).sort();
|
||||
assertEquals(dropped, ["bworld:coal", "bworld:stone"]);
|
||||
assertEquals(loaded.game.containers.size, 0);
|
||||
Deno.removeSync(dir, { recursive: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user