Actually implement mod code into server

This commit is contained in:
2026-09-25 23:13:46 -03:00
parent 099811670f
commit 9237152aa1
22 changed files with 1359 additions and 457 deletions
+6 -1
View File
@@ -8,6 +8,11 @@
"tool": "axe"
},
"drops": "bworld:chest",
"interactive": true
"interactive": true,
"components": {
"bworld:storage": {
"rows": 3
}
}
}
}
+7 -1
View File
@@ -7,6 +7,12 @@
"toughness": 2,
"tool": "shovel"
},
"drops": "bworld:dirt"
"drops": "bworld:dirt",
"components": {
"bworld:hoeable": {
"tool": "bworld:hoe",
"into": "bworld:hoed_dirt"
}
}
}
}
+4 -1
View File
@@ -12,6 +12,9 @@
"requires_tool": true
},
"drops": "bworld:furnace",
"interactive": true
"interactive": true,
"components": {
"bworld:furnace": {}
}
}
}
+7 -1
View File
@@ -12,6 +12,12 @@
"tool": "shovel"
},
"drops": "bworld:dirt",
"item": false
"item": false,
"components": {
"bworld:hoeable": {
"tool": "bworld:hoe",
"into": "bworld:hoed_dirt"
}
}
}
}
+7 -1
View File
@@ -61,6 +61,12 @@
"bits": 3,
"default": 0
}
]
],
"components": {
"bworld:crop": {
"fertilizer": "bworld:bone_meal",
"max_age": 7
}
}
}
}
+6 -1
View File
@@ -2,6 +2,11 @@
"format_version": 1,
"item": {
"id": "bworld:watering_can",
"texture": "bworld:watering_can"
"texture": "bworld:watering_can",
"components": {
"bworld:watering_can": {
"max_water": 32
}
}
}
}
+7 -2
View File
@@ -4,7 +4,12 @@
"name": "bworld",
"description": "The base game: its blocks, items, recipes and world.",
"version": "0.1.0",
"authors": ["paula"],
"authors": [
"paula"
],
"game_version": ">=0.1.0",
"credits": "CREDITS.md"
"credits": "CREDITS.md",
"scripts": {
"server": "scripts/server.ts"
}
}
+234
View File
@@ -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}`;
},
});
}