Smithing table
This commit is contained in:
@@ -349,8 +349,23 @@ clients as part of the container it's in, so client screens can show it.
|
|||||||
| `type` | Fields | Notes |
|
| `type` | Fields | Notes |
|
||||||
| --------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
| --------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||||
| `shaped` | `pattern` rows (space is empty), `key` letter to item id, `result` `{ id, count }` | Fits anywhere in the 3×3 grid, like the recipes in `server/game/crafting.ts` |
|
| `shaped` | `pattern` rows (space is empty), `key` letter to item id, `result` `{ id, count }` | Fits anywhere in the 3×3 grid, like the recipes in `server/game/crafting.ts` |
|
||||||
| `furnace` | `input` item id, `output` `{ id, count }`, `cook_time` in ticks | Same as `FURNACE_RECIPES` in `server/game/blocks.ts` |
|
| `furnace` | `input` item id, `output` `{ id, count }`, `cook_time` in ticks | Used by the furnace |
|
||||||
| `fuel` | `item` id, `burn_time` in ticks | Same as `FUEL_VALUES` in `server/game/blocks.ts` |
|
| `fuel` | `item` id, `burn_time` in ticks | Used by the furnace |
|
||||||
|
| `smithing` | `tool` item id, `material` `{ id, count }`, optional `addition` item id, `result` item id | Used by the smithing table: upgrades the tool, keeping its data. Uses up the tool, `count` of the material and the addition |
|
||||||
|
|
||||||
|
A smithing recipe, `recipes/smithing_stone_pickaxe.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"recipe": {
|
||||||
|
"type": "smithing",
|
||||||
|
"tool": "bworld:wood_pickaxe",
|
||||||
|
"material": { "id": "bworld:stone", "count": 5 },
|
||||||
|
"result": "bworld:stone_pickaxe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
A shaped recipe, for the crafting grid in the player's inventory screen:
|
A shaped recipe, for the crafting grid in the player's inventory screen:
|
||||||
|
|
||||||
@@ -366,7 +381,8 @@ A shaped recipe, for the crafting grid in the player's inventory screen:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Two furnace recipes with the same `input`, or two shaped recipes with the same pattern and key, are a load error. Server
|
Two furnace recipes with the same `input`, two smithing recipes with the same tool, material and addition, or two
|
||||||
|
shaped recipes with the same pattern and key, are a load error. Server
|
||||||
scripts can also register recipe types of their own through `ctx.recipes`.
|
scripts can also register recipe types of their own through `ctx.recipes`.
|
||||||
|
|
||||||
## Server scripts
|
## Server scripts
|
||||||
@@ -655,7 +671,9 @@ ctx.components.register_block("copper_tools:smelter", {
|
|||||||
can be fractional. `rows` sets its height, by default it fits the slots and bars.
|
can be fractional. `rows` sets its height, by default it fits the slots and bars.
|
||||||
- `filter` is a function `(item) => boolean` or one of the built-in filters `"smeltable"` (has a furnace recipe) and
|
- `filter` is a function `(item) => boolean` or one of the built-in filters `"smeltable"` (has a furnace recipe) and
|
||||||
`"fuel"`. It and `output_only` run **on the server**, so players can't put the wrong items in by editing their client.
|
`"fuel"`. It and `output_only` run **on the server**, so players can't put the wrong items in by editing their client.
|
||||||
`output_only` slots can only be taken from, all at once, like the furnace result.
|
`output_only` slots can only be taken from, all at once, like the furnace result. Their `on_take(item)` runs when a
|
||||||
|
player takes from one, before they get it: return `false` to stop them, or use up the inputs there so they're gone in
|
||||||
|
the same step (the smithing table does this).
|
||||||
- `bars` are progress bars filled with `value / max`, both names of properties set with `screen.set_property(id, n)`.
|
- `bars` are progress bars filled with `value / max`, both names of properties set with `screen.set_property(id, n)`.
|
||||||
`direction` is `"up"` (like the furnace's fire) or `"right"` (like its arrow). Properties are synced when they change,
|
`direction` is `"up"` (like the furnace's fire) or `"right"` (like its arrow). Properties are synced when they change,
|
||||||
so a component can update them every tick.
|
so a component can update them every tick.
|
||||||
|
|||||||
@@ -258,6 +258,12 @@ function right_click(container: Container, index: number, cursor: Cursor) {
|
|||||||
slot.amount = half;
|
slot.amount = half;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// whether take_output would take it
|
||||||
|
export function can_take_output(item: ItemStack, cursor: Cursor): boolean {
|
||||||
|
const holding = cursor.item;
|
||||||
|
return !holding || (holding.type_id === item.type_id && holding.max_amount - holding.amount >= item.amount);
|
||||||
|
}
|
||||||
|
|
||||||
// output slots (furnace result, crafting result) can only be taken from, all at once
|
// output slots (furnace result, crafting result) can only be taken from, all at once
|
||||||
// returns whether it was taken
|
// returns whether it was taken
|
||||||
export function take_output(item: ItemStack, cursor: Cursor): boolean {
|
export function take_output(item: ItemStack, cursor: Cursor): boolean {
|
||||||
|
|||||||
@@ -191,6 +191,8 @@ export interface RecipeApi {
|
|||||||
fuel_value(item: Id): number; // burn time in ticks, 0 if it isn't fuel
|
fuel_value(item: Id): number; // burn time in ticks, 0 if it isn't fuel
|
||||||
is_fuel(item: Id): boolean;
|
is_fuel(item: Id): boolean;
|
||||||
is_smeltable(item: Id): boolean;
|
is_smeltable(item: Id): boolean;
|
||||||
|
// the smithing recipe for these items, whatever the material's count. addition is the optional third item
|
||||||
|
smithing(tool: Id, material: Id, addition?: Id): { result: Id; material_count: number } | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// guis
|
// guis
|
||||||
@@ -233,6 +235,8 @@ export interface ContainerScreenOptions {
|
|||||||
filter?: SlotFilter;
|
filter?: SlotFilter;
|
||||||
// can only be taken from, all at once, like the furnace result
|
// can only be taken from, all at once, like the furnace result
|
||||||
output_only?: boolean;
|
output_only?: boolean;
|
||||||
|
// runs when a player takes from an output_only slot, before they get it. false stops them taking it
|
||||||
|
on_take?: (item: ItemStack) => boolean;
|
||||||
}[];
|
}[];
|
||||||
// height of the screen's area in slots, fits the layout and bars when not set
|
// height of the screen's area in slots, fits the layout and bars when not set
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
|||||||
+18
-2
@@ -78,7 +78,15 @@ export type RecipeJson =
|
|||||||
result: { id: string; count: number };
|
result: { id: string; count: number };
|
||||||
}
|
}
|
||||||
| { type: "furnace"; input: string; output: { id: string; count: number }; cook_time: number }
|
| { type: "furnace"; input: string; output: { id: string; count: number }; cook_time: number }
|
||||||
| { type: "fuel"; item: string; burn_time: number };
|
| { type: "fuel"; item: string; burn_time: number }
|
||||||
|
// upgrades a tool at a smithing table, with some of a material and maybe one more item
|
||||||
|
| {
|
||||||
|
type: "smithing";
|
||||||
|
tool: string;
|
||||||
|
material: { id: string; count: number };
|
||||||
|
addition?: string;
|
||||||
|
result: string;
|
||||||
|
};
|
||||||
|
|
||||||
// the crafting grid's format, see server/game/crafting.ts
|
// the crafting grid's format, see server/game/crafting.ts
|
||||||
export interface GridRecipe {
|
export interface GridRecipe {
|
||||||
@@ -447,8 +455,16 @@ export function validate_recipe(json: unknown): Problems {
|
|||||||
problems.push("burn_time must be a positive number of ticks");
|
problems.push("burn_time must be a positive number of ticks");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "smithing":
|
||||||
|
problems.push(
|
||||||
|
...validate_id(json.tool, "tool"),
|
||||||
|
...validate_stack(json.material, "material"),
|
||||||
|
...validate_id(json.result, "result"),
|
||||||
|
);
|
||||||
|
if (json.addition !== undefined) problems.push(...validate_id(json.addition, "addition"));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
problems.push('type must be "shaped", "furnace" or "fuel"');
|
problems.push('type must be "shaped", "furnace", "fuel" or "smithing"');
|
||||||
}
|
}
|
||||||
return problems;
|
return problems;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export class RecipeBook {
|
|||||||
shaped: GridRecipe[] = [];
|
shaped: GridRecipe[] = [];
|
||||||
furnace = new Map<string, { output: { id: string; count: number }; cook_time: number }>();
|
furnace = new Map<string, { output: { id: string; count: number }; cook_time: number }>();
|
||||||
fuel = new Map<string, number>();
|
fuel = new Map<string, number>();
|
||||||
|
smithing: Extract<RecipeJson, { type: "smithing" }>[] = [];
|
||||||
ores: OreJson[] = [];
|
ores: OreJson[] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +99,17 @@ export function register_mod_data(mods: { id: string; data: ModData }[]): Recipe
|
|||||||
case "fuel":
|
case "fuel":
|
||||||
recipes.fuel.set(recipe.item, recipe.burn_time);
|
recipes.fuel.set(recipe.item, recipe.burn_time);
|
||||||
break;
|
break;
|
||||||
|
case "smithing":
|
||||||
|
if (
|
||||||
|
recipes.smithing.some((other) =>
|
||||||
|
other.tool === recipe.tool && other.material.id === recipe.material.id &&
|
||||||
|
other.addition === recipe.addition
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
fail(`two smithing recipes for ${recipe.tool} with ${recipe.material.id}`);
|
||||||
|
}
|
||||||
|
recipes.smithing.push(recipe);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recipes.ores.push(...data.ores);
|
recipes.ores.push(...data.ores);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"block": {
|
||||||
|
"id": "bworld:smithing_table",
|
||||||
|
"textures": {
|
||||||
|
"top": "bworld:smithing_table_top",
|
||||||
|
"bottom": "bworld:smithing_table_bottom",
|
||||||
|
"front": "bworld:smithing_table_front",
|
||||||
|
"side": "bworld:smithing_table_side"
|
||||||
|
},
|
||||||
|
"mining": {
|
||||||
|
"toughness": 5,
|
||||||
|
"tool": "axe"
|
||||||
|
},
|
||||||
|
"drops": "bworld:smithing_table",
|
||||||
|
"interactive": true,
|
||||||
|
"components": {
|
||||||
|
"bworld:smithing_table": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"item": {
|
||||||
|
"id": "bworld:stone_pickaxe",
|
||||||
|
"texture": "bworld:stone_pickaxe",
|
||||||
|
"tool": "pickaxe"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"recipe": {
|
||||||
|
"type": "smithing",
|
||||||
|
"tool": "bworld:wood_pickaxe",
|
||||||
|
"material": {
|
||||||
|
"id": "bworld:stone",
|
||||||
|
"count": 5
|
||||||
|
},
|
||||||
|
"result": "bworld:stone_pickaxe"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export function setup(ctx: ServerContext) {
|
|||||||
register_hoeable(ctx);
|
register_hoeable(ctx);
|
||||||
register_storage(ctx);
|
register_storage(ctx);
|
||||||
register_furnace(ctx);
|
register_furnace(ctx);
|
||||||
|
register_smithing_table(ctx);
|
||||||
register_crop(ctx);
|
register_crop(ctx);
|
||||||
register_watering_can(ctx);
|
register_watering_can(ctx);
|
||||||
}
|
}
|
||||||
@@ -196,6 +197,81 @@ function register_furnace(ctx: ServerContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// smithing table: a tool, some of a material and maybe a third item make a better tool, see the smithing recipes.
|
||||||
|
// every player gets their own slots, and what's left in them goes back to the player when they close it
|
||||||
|
|
||||||
|
const TOOL = 0;
|
||||||
|
const MATERIAL = 1;
|
||||||
|
const ADDITION = 2;
|
||||||
|
const RESULT = 3;
|
||||||
|
|
||||||
|
function register_smithing_table(ctx: ServerContext) {
|
||||||
|
ctx.components.register_block("bworld:smithing_table", {
|
||||||
|
on_interact(_block, _params, player) {
|
||||||
|
const container = ctx.containers.create(4);
|
||||||
|
|
||||||
|
// the recipe the inputs make, with enough material
|
||||||
|
const match = () => {
|
||||||
|
const tool = container.get(TOOL);
|
||||||
|
const material = container.get(MATERIAL);
|
||||||
|
const addition = container.get(ADDITION);
|
||||||
|
if (!tool || !material) return undefined;
|
||||||
|
const recipe = ctx.recipes.smithing(tool.id, material.id, addition?.id);
|
||||||
|
if (!recipe || material.count < recipe.material_count) return undefined;
|
||||||
|
return { ...recipe, tool, material, addition };
|
||||||
|
};
|
||||||
|
|
||||||
|
// the result slot shows what taking it would give, the upgraded tool keeps the old one's data
|
||||||
|
const show_result = () => {
|
||||||
|
const recipe = match();
|
||||||
|
const shown = container.get(RESULT);
|
||||||
|
if (!recipe) {
|
||||||
|
if (shown) container.set(RESULT, undefined);
|
||||||
|
} else if (shown?.id !== recipe.result) {
|
||||||
|
container.set(RESULT, { id: recipe.result, count: 1, data: recipe.tool.data });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const stop = container.on_change((slot) => {
|
||||||
|
if (slot !== RESULT) show_result();
|
||||||
|
});
|
||||||
|
|
||||||
|
const screen = ctx.ui.open_container(player, {
|
||||||
|
container,
|
||||||
|
layout: [
|
||||||
|
{ slot: TOOL, x: 1.5, y: 0.5 },
|
||||||
|
{ slot: MATERIAL, x: 2.5, y: 0.5 },
|
||||||
|
{ slot: ADDITION, x: 3.5, y: 0.5 },
|
||||||
|
{
|
||||||
|
slot: RESULT,
|
||||||
|
x: 5.5,
|
||||||
|
y: 0.5,
|
||||||
|
output_only: true,
|
||||||
|
// the inputs are used up in the same step, so they can't be taken out after
|
||||||
|
on_take(item) {
|
||||||
|
const recipe = match();
|
||||||
|
if (!recipe || recipe.result !== item.id) return false;
|
||||||
|
recipe.tool.count -= 1;
|
||||||
|
recipe.material.count -= recipe.material_count;
|
||||||
|
if (recipe.addition) recipe.addition.count -= 1;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rows: 2,
|
||||||
|
});
|
||||||
|
screen.on_close(() => {
|
||||||
|
stop();
|
||||||
|
for (const slot of [TOOL, MATERIAL, ADDITION]) {
|
||||||
|
const item = container.get(slot);
|
||||||
|
if (item) player.give_item(item.id, item.count, item.data);
|
||||||
|
}
|
||||||
|
ctx.containers.delete(container.id);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// grows one stage each time it's right clicked with the fertilizer, like bone meal on wheat
|
// grows one stage each time it's right clicked with the fertilizer, like bone meal on wheat
|
||||||
function register_crop(ctx: ServerContext) {
|
function register_crop(ctx: ServerContext) {
|
||||||
ctx.components.register_block<{ fertilizer: string; max_age: number }>("bworld:crop", {
|
ctx.components.register_block<{ fertilizer: string; max_age: number }>("bworld:crop", {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
TICKS_PER_SECOND,
|
TICKS_PER_SECOND,
|
||||||
} from "$/common/constants.ts";
|
} from "$/common/constants.ts";
|
||||||
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||||
import { click_slot, Container, ItemData, ItemStack, take_output } from "$/common/inventory.ts";
|
import { can_take_output, click_slot, Container, ItemData, ItemStack, take_output } from "$/common/inventory.ts";
|
||||||
import {
|
import {
|
||||||
AIR_ID,
|
AIR_ID,
|
||||||
BlockChange,
|
BlockChange,
|
||||||
@@ -744,7 +744,8 @@ export class GameServer {
|
|||||||
}
|
}
|
||||||
} else if (key === "screen" && player.screen?.layout.slots.find((s) => s.index === index)?.output) {
|
} else if (key === "screen" && player.screen?.layout.slots.find((s) => s.index === index)?.output) {
|
||||||
const item = container.get_item(index);
|
const item = container.get_item(index);
|
||||||
if (item && take_output(item, player.cursor)) {
|
const allowed = (item: ItemStack) => player.screen?.on_take.get(index)?.(item) ?? true;
|
||||||
|
if (item && can_take_output(item, player.cursor) && allowed(item) && take_output(item, player.cursor)) {
|
||||||
container.set_item(index, undefined);
|
container.set_item(index, undefined);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
|||||||
@@ -391,6 +391,12 @@ export class ModRuntime {
|
|||||||
fuel_value: (item) => game().recipes.fuel.get(item) ?? 0,
|
fuel_value: (item) => game().recipes.fuel.get(item) ?? 0,
|
||||||
is_fuel: (item) => game().recipes.fuel.has(item),
|
is_fuel: (item) => game().recipes.fuel.has(item),
|
||||||
is_smeltable: (item) => game().recipes.furnace.has(item),
|
is_smeltable: (item) => game().recipes.furnace.has(item),
|
||||||
|
smithing: (tool, material, addition) => {
|
||||||
|
const recipe = game().recipes.smithing.find((r) =>
|
||||||
|
r.tool === tool && r.material.id === material && r.addition === addition
|
||||||
|
);
|
||||||
|
return recipe && { result: recipe.result, material_count: recipe.material.count };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
containers: {
|
containers: {
|
||||||
create: (size) => {
|
create: (size) => {
|
||||||
@@ -442,7 +448,11 @@ export class ModRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filters = new Map<number, (item: EngineItemStack) => boolean>();
|
const filters = new Map<number, (item: EngineItemStack) => boolean>();
|
||||||
for (const { slot, filter } of options.layout) {
|
const on_take = new Map<number, (item: EngineItemStack) => boolean>();
|
||||||
|
for (const { slot, filter, on_take: take } of options.layout) {
|
||||||
|
if (take) {
|
||||||
|
on_take.set(slot, (item) => run_guarded(mod, "an on_take", () => take(item_api(item))) === true);
|
||||||
|
}
|
||||||
if (!Number.isInteger(slot) || slot < 0 || slot >= container.size) {
|
if (!Number.isInteger(slot) || slot < 0 || slot >= container.size) {
|
||||||
throw new Error(`slot ${slot} isn't in the container, it has ${container.size}`);
|
throw new Error(`slot ${slot} isn't in the container, it has ${container.size}`);
|
||||||
}
|
}
|
||||||
@@ -474,7 +484,14 @@ export class ModRuntime {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let open = true;
|
let open = true;
|
||||||
const screen: OpenScreen = { container, layout, properties: {}, filters, on_close: [() => open = false] };
|
const screen: OpenScreen = {
|
||||||
|
container,
|
||||||
|
layout,
|
||||||
|
properties: {},
|
||||||
|
filters,
|
||||||
|
on_take,
|
||||||
|
on_close: [() => open = false],
|
||||||
|
};
|
||||||
game.open_screen(server_player, screen);
|
game.open_screen(server_player, screen);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export interface OpenScreen {
|
|||||||
properties: Record<string, number>;
|
properties: Record<string, number>;
|
||||||
// what can go into each slot, checked when the player clicks
|
// what can go into each slot, checked when the player clicks
|
||||||
filters: Map<number, (item: ItemStack) => boolean>;
|
filters: Map<number, (item: ItemStack) => boolean>;
|
||||||
|
// asked before a player takes from an output slot
|
||||||
|
on_take: Map<number, (item: ItemStack) => boolean>;
|
||||||
on_close: (() => void)[];
|
on_close: (() => void)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,3 +195,70 @@ Deno.test("chests and furnaces from a world saved before they were mod code keep
|
|||||||
assert(saved.tiles.every((tile: Msg) => tile.data === undefined && tile.containers === undefined));
|
assert(saved.tiles.every((tile: Msg) => tile.data === undefined && tile.containers === undefined));
|
||||||
assertEquals(Object.keys(saved.containers).length, 2);
|
assertEquals(Object.keys(saved.containers).length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("a smithing table upgrades a wooden pickaxe with 5 stone, using up exactly that", async () => {
|
||||||
|
const t = await setup([["bworld:wood_pickaxe", 1], ["bworld:stone", 7]]);
|
||||||
|
t.game.set_block(1, 100, 0, "bworld:smithing_table");
|
||||||
|
let messages = t.take(1);
|
||||||
|
const pickaxe = slot_of(messages, "bworld:wood_pickaxe");
|
||||||
|
const stone = slot_of(messages, "bworld:stone");
|
||||||
|
|
||||||
|
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, s.output ?? false]), [
|
||||||
|
[0, false],
|
||||||
|
[1, false],
|
||||||
|
[2, false],
|
||||||
|
[3, true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// with only the tool there's nothing to take
|
||||||
|
move_to_screen(t, pickaxe, 0);
|
||||||
|
t.game.tick();
|
||||||
|
t.send(1, { type: "click", container: "screen", index: 3, button: 0 });
|
||||||
|
assertEquals(t.take(1).filter((m) => m.type === "cursor").at(-1)?.item ?? null, null);
|
||||||
|
|
||||||
|
// the result shows up once there's enough stone, and taking it uses up the pickaxe and 5 stone
|
||||||
|
move_to_screen(t, stone, 1);
|
||||||
|
t.game.tick();
|
||||||
|
assertEquals(last_container(t.take(1), "screen")[3], { id: "bworld:stone_pickaxe", count: 1 });
|
||||||
|
t.send(1, { type: "click", container: "screen", index: 3, button: 0 });
|
||||||
|
messages = t.take(1);
|
||||||
|
assertEquals(messages.filter((m) => m.type === "cursor").at(-1).item, { id: "bworld:stone_pickaxe", count: 1 });
|
||||||
|
assertEquals(last_container(messages, "screen"), [null, { id: "bworld:stone", count: 2 }, null, null]);
|
||||||
|
|
||||||
|
// closing gives back the leftover stone, and the cursor's pickaxe goes into the inventory
|
||||||
|
t.send(1, { type: "close_screen" });
|
||||||
|
const inventory = last_container(t.take(1), "inventory").filter((i: Msg) => i);
|
||||||
|
assertEquals(
|
||||||
|
inventory.sort((a: Msg, b: Msg) => a.id.localeCompare(b.id)),
|
||||||
|
[{ id: "bworld:stone", count: 2 }, { id: "bworld:stone_pickaxe", count: 1 }],
|
||||||
|
);
|
||||||
|
assertEquals(t.game.containers.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("a smithing result can't be taken once its inputs are gone, even before it updates", async () => {
|
||||||
|
const t = await setup([["bworld:wood_pickaxe", 1], ["bworld:stone", 5]]);
|
||||||
|
t.game.set_block(1, 100, 0, "bworld:smithing_table");
|
||||||
|
const messages = t.take(1);
|
||||||
|
t.send(1, { type: "use_block", x: 1, y: 100, z: 0, face: "top" });
|
||||||
|
move_to_screen(t, slot_of(messages, "bworld:wood_pickaxe"), 0);
|
||||||
|
move_to_screen(t, slot_of(messages, "bworld:stone"), 1);
|
||||||
|
t.game.tick();
|
||||||
|
|
||||||
|
// take the pickaxe back out and try the result in the same tick
|
||||||
|
t.send(1, { type: "click", container: "screen", index: 0, button: 0 });
|
||||||
|
t.send(1, { type: "click", container: "inventory", index: 20, button: 0 });
|
||||||
|
t.send(1, { type: "click", container: "screen", index: 3, button: 0 });
|
||||||
|
const cursor = t.take(1).filter((m) => m.type === "cursor").at(-1).item;
|
||||||
|
assertEquals(cursor, null);
|
||||||
|
|
||||||
|
// and an extra item in the third slot doesn't match a recipe without one
|
||||||
|
t.send(1, { type: "click", container: "inventory", index: 20, button: 0 });
|
||||||
|
t.send(1, { type: "click", container: "screen", index: 0, button: 0 });
|
||||||
|
t.send(1, { type: "chat", text: "/give bworld:coal 1" });
|
||||||
|
const coal = slot_of(t.take(1), "bworld:coal");
|
||||||
|
move_to_screen(t, coal, 2);
|
||||||
|
t.game.tick();
|
||||||
|
assertEquals(last_container(t.take(1), "screen")[3], null);
|
||||||
|
});
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ const inventory_of = (messages: Msg[]) =>
|
|||||||
|
|
||||||
Deno.test("the base game loads from mods/bworld", async () => {
|
Deno.test("the base game loads from mods/bworld", async () => {
|
||||||
const { game } = await test_game("mods");
|
const { game } = await test_game("mods");
|
||||||
assertEquals(EverythingRegistry.entries("blocks").length, 20);
|
assertEquals(EverythingRegistry.entries("blocks").length, 21);
|
||||||
// 14 items plus the item forms of the 17 blocks that have one
|
// 15 items plus the item forms of the 18 blocks that have one
|
||||||
assertEquals(EverythingRegistry.entries("items").length, 31);
|
assertEquals(EverythingRegistry.entries("items").length, 33);
|
||||||
assertEquals(game.recipes.shaped.length, 5);
|
assertEquals(game.recipes.shaped.length, 5);
|
||||||
assertEquals(game.recipes.furnace.size, 6);
|
assertEquals(game.recipes.furnace.size, 6);
|
||||||
assertEquals(game.recipes.fuel.size, 2);
|
assertEquals(game.recipes.fuel.size, 2);
|
||||||
|
|||||||
@@ -280,6 +280,12 @@ function check_references(mods: LoadedMod[]) {
|
|||||||
case "fuel":
|
case "fuel":
|
||||||
uses(file, json.item, "item");
|
uses(file, json.item, "item");
|
||||||
break;
|
break;
|
||||||
|
case "smithing":
|
||||||
|
uses(file, json.tool, "item");
|
||||||
|
uses(file, json.material.id, "item");
|
||||||
|
if (json.addition) uses(file, json.addition, "item");
|
||||||
|
uses(file, json.result, "item");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const { file, json } of mod.ores) {
|
for (const { file, json } of mod.ores) {
|
||||||
|
|||||||
Reference in New Issue
Block a user