|
|
|
@@ -0,0 +1,234 @@
|
|
|
|
|
// the base game's block and item behavior, written against the same api as any other mod
|
|
|
|
|
import type { BlockRef, Container, ScreenHandle, ServerContext } from "bworld/server";
|
|
|
|
|
|
|
|
|
|
export function setup(ctx: ServerContext) {
|
|
|
|
|
register_hoeable(ctx);
|
|
|
|
|
register_storage(ctx);
|
|
|
|
|
register_furnace(ctx);
|
|
|
|
|
register_crop(ctx);
|
|
|
|
|
register_watering_can(ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// right clicking with a tool turns the block into another, like a hoe on grass
|
|
|
|
|
function register_hoeable(ctx: ServerContext) {
|
|
|
|
|
ctx.components.register_block<{ tool: string; into: string }>("bworld:hoeable", {
|
|
|
|
|
on_interact(block, params, player) {
|
|
|
|
|
if (player.held_item?.id !== params.tool) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
ctx.world.set_block(block.x, block.y, block.z, params.into);
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// the container a block keeps its items in, made the first time it's needed. blocks placed before tile data
|
|
|
|
|
// existed don't have one yet
|
|
|
|
|
function block_container(ctx: ServerContext, block: BlockRef, size: number): Container {
|
|
|
|
|
const existing = block.data?.container && ctx.containers.get(block.data.container);
|
|
|
|
|
if (existing) {
|
|
|
|
|
return existing;
|
|
|
|
|
}
|
|
|
|
|
const container = ctx.containers.create(size);
|
|
|
|
|
block.data = { ...block.data, container: container.id };
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// what was inside falls out, like minecraft's Containers.dropContents
|
|
|
|
|
function drop_contents(ctx: ServerContext, block: BlockRef) {
|
|
|
|
|
const container = block.data?.container && ctx.containers.get(block.data.container);
|
|
|
|
|
if (!container) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// a chest
|
|
|
|
|
function register_storage(ctx: ServerContext) {
|
|
|
|
|
ctx.components.register_block<{ rows: number }>("bworld:storage", {
|
|
|
|
|
on_create(block, params) {
|
|
|
|
|
block_container(ctx, block, params.rows * 9);
|
|
|
|
|
},
|
|
|
|
|
on_interact(block, params, player) {
|
|
|
|
|
const size = params.rows * 9;
|
|
|
|
|
ctx.ui.open_container(player, {
|
|
|
|
|
container: block_container(ctx, block, size),
|
|
|
|
|
layout: Array.from({ length: size }, (_, slot) => ({ slot, x: slot % 9, y: Math.floor(slot / 9) })),
|
|
|
|
|
rows: params.rows,
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
on_break(block) {
|
|
|
|
|
drop_contents(ctx, block);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// furnace
|
|
|
|
|
|
|
|
|
|
// slot 0 is what's smelting, 1 the fuel, 2 the result
|
|
|
|
|
interface FurnaceData {
|
|
|
|
|
container: string;
|
|
|
|
|
// ticks
|
|
|
|
|
progress: number;
|
|
|
|
|
progress_max: number;
|
|
|
|
|
fuel: number;
|
|
|
|
|
fuel_max: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FURNACE_BARS = [
|
|
|
|
|
{
|
|
|
|
|
x: 3.5,
|
|
|
|
|
y: 1,
|
|
|
|
|
value: "fuel",
|
|
|
|
|
max: "fuel_max",
|
|
|
|
|
direction: "up" as const,
|
|
|
|
|
empty_texture: "bworld:fire_empty",
|
|
|
|
|
full_texture: "bworld:fire_full",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
x: 4.5,
|
|
|
|
|
y: 1,
|
|
|
|
|
value: "progress",
|
|
|
|
|
max: "progress_max",
|
|
|
|
|
direction: "right" as const,
|
|
|
|
|
empty_texture: "bworld:arrow_empty",
|
|
|
|
|
full_texture: "bworld:arrow_full",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function register_furnace(ctx: ServerContext) {
|
|
|
|
|
// who's looking at each furnace, by position, to keep their bars moving
|
|
|
|
|
const viewers = new Map<string, Set<ScreenHandle>>();
|
|
|
|
|
const key = (block: BlockRef) => `${block.x},${block.y},${block.z}`;
|
|
|
|
|
|
|
|
|
|
const furnace_data = (block: BlockRef): FurnaceData => {
|
|
|
|
|
const container = block_container(ctx, block, 3);
|
|
|
|
|
block.data = { progress: 0, progress_max: 0, fuel: 0, fuel_max: 0, ...block.data, container: container.id };
|
|
|
|
|
return block.data;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const show = (screen: ScreenHandle, data: FurnaceData) => {
|
|
|
|
|
screen.set_property("progress", data.progress);
|
|
|
|
|
screen.set_property("progress_max", data.progress_max);
|
|
|
|
|
screen.set_property("fuel", data.fuel);
|
|
|
|
|
screen.set_property("fuel_max", data.fuel_max);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// there's somewhere for the result to go
|
|
|
|
|
const can_smelt = (container: Container, output: { id: string; count: number }) => {
|
|
|
|
|
const result = container.get(2);
|
|
|
|
|
return !result || (result.id === output.id && result.count < ctx.items.max_stack(result.id));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ctx.components.register_block("bworld:furnace", {
|
|
|
|
|
on_create(block) {
|
|
|
|
|
furnace_data(block);
|
|
|
|
|
},
|
|
|
|
|
on_interact(block, _params, player) {
|
|
|
|
|
const data = furnace_data(block);
|
|
|
|
|
const screen = ctx.ui.open_container(player, {
|
|
|
|
|
container: ctx.containers.get(data.container)!,
|
|
|
|
|
layout: [
|
|
|
|
|
{ slot: 0, x: 3.5, y: 0 },
|
|
|
|
|
{ slot: 1, x: 3.5, y: 2, filter: "fuel" },
|
|
|
|
|
{ slot: 2, x: 5.5, y: 1, output_only: true },
|
|
|
|
|
],
|
|
|
|
|
rows: 3,
|
|
|
|
|
bars: FURNACE_BARS,
|
|
|
|
|
});
|
|
|
|
|
show(screen, data);
|
|
|
|
|
const screens = viewers.get(key(block)) ?? new Set();
|
|
|
|
|
viewers.set(key(block), screens);
|
|
|
|
|
screens.add(screen);
|
|
|
|
|
screen.on_close(() => screens.delete(screen));
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
on_break(block) {
|
|
|
|
|
drop_contents(ctx, block);
|
|
|
|
|
},
|
|
|
|
|
on_tick(block) {
|
|
|
|
|
const data = furnace_data(block);
|
|
|
|
|
const container = ctx.containers.get(data.container)!;
|
|
|
|
|
const input = container.get(0);
|
|
|
|
|
const recipe = input && ctx.recipes.furnace_result(input.id);
|
|
|
|
|
|
|
|
|
|
if (data.fuel > 0) {
|
|
|
|
|
data.fuel -= 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!recipe || !can_smelt(container, recipe.output)) {
|
|
|
|
|
data.progress = 0;
|
|
|
|
|
} else {
|
|
|
|
|
// refuel
|
|
|
|
|
const fuel = container.get(1);
|
|
|
|
|
if (data.fuel === 0 && fuel && ctx.recipes.is_fuel(fuel.id)) {
|
|
|
|
|
data.fuel = ctx.recipes.fuel_value(fuel.id);
|
|
|
|
|
data.fuel_max = data.fuel;
|
|
|
|
|
fuel.count -= 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cook
|
|
|
|
|
if (data.fuel > 0) {
|
|
|
|
|
data.progress_max = recipe.cook_time;
|
|
|
|
|
data.progress += 1;
|
|
|
|
|
if (data.progress >= recipe.cook_time) {
|
|
|
|
|
data.progress = 0;
|
|
|
|
|
const result = container.get(2);
|
|
|
|
|
if (result) {
|
|
|
|
|
result.count += recipe.output.count;
|
|
|
|
|
} else {
|
|
|
|
|
container.set(2, recipe.output);
|
|
|
|
|
}
|
|
|
|
|
input.count -= 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const screen of viewers.get(key(block)) ?? []) show(screen, data);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// grows one stage each time it's right clicked with the fertilizer, like bone meal on wheat
|
|
|
|
|
function register_crop(ctx: ServerContext) {
|
|
|
|
|
ctx.components.register_block<{ fertilizer: string; max_age: number }>("bworld:crop", {
|
|
|
|
|
on_interact(block, params, player) {
|
|
|
|
|
const held = player.held_item;
|
|
|
|
|
if (held?.id !== params.fertilizer) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const age = ctx.world.get_state(block.x, block.y, block.z, "age") ?? 0;
|
|
|
|
|
// fully grown, keep the fertilizer
|
|
|
|
|
if (age >= params.max_age) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
ctx.world.set_state(block.x, block.y, block.z, "age", age + 1);
|
|
|
|
|
held.count -= 1;
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface WateringCanData {
|
|
|
|
|
water: number;
|
|
|
|
|
max_water: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function register_watering_can(ctx: ServerContext) {
|
|
|
|
|
ctx.components.register_item<{ max_water: number }>("bworld:watering_can", {
|
|
|
|
|
on_create(item, params) {
|
|
|
|
|
item.data = { water: 0, max_water: params.max_water } satisfies WateringCanData;
|
|
|
|
|
},
|
|
|
|
|
get_lore(item) {
|
|
|
|
|
const data = item.data as WateringCanData;
|
|
|
|
|
return `Water: ${data.water}/${data.max_water}`;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|