Files
2026-09-25 16:26:45 -03:00

257 lines
6.3 KiB
TypeScript

import { Container, ItemStack } from "$/common/inventory.ts";
import { ScreenLayout } from "$/common/protocol.ts";
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
import type { RecipeBook } from "$/common/mod_loader.ts";
import type { GameServer } from "./game_server.ts";
import type { ServerPlayer } from "./player.ts";
import type { Tile } from "./world.ts";
// what blocks do, only on the server. blocks without an entry just sit there
export interface BlockBehavior {
// set up a tile for this block. blocks with this get a tile when placed
create_tile?(tile: Tile): void;
// right click, return true if it did something so no block gets placed
on_interact?(
game: GameServer,
block: { x: number; y: number; z: number; id: string },
player: ServerPlayer,
): boolean;
on_break?(game: GameServer, tile: Tile, player: ServerPlayer | undefined): void;
on_tick?(game: GameServer, tile: Tile): void;
on_second?(game: GameServer, tile: Tile): void;
}
export const BLOCK_BEHAVIORS: Record<string, BlockBehavior> = {};
// whatever was inside falls out, like minecraft's Containers.dropContents
function drop_container_contents(game: GameServer, tile: Tile) {
for (const container of Object.values(tile.containers)) {
for (let i = 0; i < container.size; i++) {
const item = container.get_item(i);
if (item) {
container.set_item(i, undefined);
game.pop_item(tile.x, tile.y, tile.z, item);
}
}
}
}
function hoe_into_hoed_dirt(
game: GameServer,
block: { x: number; y: number; z: number },
player: ServerPlayer,
): boolean {
if (player.held_item?.type_id !== "bworld:hoe") {
return false;
}
game.set_block(block.x, block.y, block.z, "bworld:hoed_dirt", player);
return true;
}
BLOCK_BEHAVIORS["bworld:grass"] = { on_interact: hoe_into_hoed_dirt };
BLOCK_BEHAVIORS["bworld:dirt"] = { on_interact: hoe_into_hoed_dirt };
// chest
const CHEST_LAYOUT: ScreenLayout = {
rows: 3,
slots: Array.from({ length: 9 * 3 }, (_, index) => ({ index, x: index % 9, y: Math.floor(index / 9) })),
bars: [],
};
BLOCK_BEHAVIORS["bworld:chest"] = {
create_tile(tile) {
tile.containers.main = new Container(9 * 3);
},
on_interact(game, block, player) {
const tile = game.get_or_create_tile(block.x, block.y, block.z);
game.open_screen(player, {
tile,
container: tile.containers.main,
layout: CHEST_LAYOUT,
properties: () => ({}),
});
return true;
},
on_break(game, tile) {
drop_container_contents(game, tile);
},
};
// furnace
interface FurnaceData {
progress: number;
progress_max: number;
fuel: number;
fuel_max: number;
}
interface FurnaceRecipe {
output: { id: string; count: number };
cook_time: number;
}
function get_recipe(recipes: RecipeBook, input?: ItemStack | undefined): FurnaceRecipe | undefined {
if (!input) {
return;
}
return recipes.furnace.get(input.type_id);
}
function can_craft(container: Container, recipe?: FurnaceRecipe) {
if (!recipe) {
return false;
}
const output = container.get_item(2);
if (!output) {
return true;
}
if (output.type_id !== recipe.output.id) {
return false;
}
return output.amount < output.max_amount;
}
function get_fuel_value(recipes: RecipeBook, item?: ItemStack | undefined): number {
if (!item) {
return 0;
}
return recipes.fuel.get(item.type_id) ?? 0;
}
function has_fuel(recipes: RecipeBook, container: Container) {
return get_fuel_value(recipes, container.get_item(1)) > 0;
}
function consume_fuel(recipes: RecipeBook, container: Container): number {
const fuel = container.get_slot(1)!;
const value = get_fuel_value(recipes, fuel.get_item());
if (fuel.has_item()) {
fuel.amount! -= 1;
}
return value;
}
function craft(container: Container, recipe: FurnaceRecipe) {
const input = container.get_item(0)!;
const output = container.get_item(2);
if (!output) {
container.set_item(2, new ItemStack(recipe.output.id, recipe.output.count));
} else {
output.amount += recipe.output.count;
}
input.amount -= 1;
container.set_item(0, input.amount > 0 ? input : undefined);
}
const FURNACE_LAYOUT: ScreenLayout = {
rows: 3,
slots: [
{ index: 0, x: 3.5, y: 0 },
{ index: 1, x: 3.5, y: 2 },
{ index: 2, x: 5.5, y: 1, output: true },
],
bars: [
{
x: 3.5,
y: 1,
value: "fuel",
max: "fuel_max",
direction: "up",
empty_texture: "bworld:fire_empty",
full_texture: "bworld:fire_full",
},
{
x: 4.5,
y: 1,
value: "progress",
max: "progress_max",
direction: "right",
empty_texture: "bworld:arrow_empty",
full_texture: "bworld:arrow_full",
},
],
};
BLOCK_BEHAVIORS["bworld:furnace"] = {
create_tile(tile) {
tile.containers.main = new Container(3);
tile.data = { progress: 0, progress_max: 0, fuel: 0, fuel_max: 0 } satisfies FurnaceData;
},
on_interact(game, block, player) {
const tile = game.get_or_create_tile(block.x, block.y, block.z);
const data = tile.data as unknown as FurnaceData;
game.open_screen(player, {
tile,
container: tile.containers.main,
layout: FURNACE_LAYOUT,
properties: () => ({ ...data }),
});
return true;
},
on_break(game, tile) {
drop_container_contents(game, tile);
},
on_tick(game, tile) {
const data = tile.data as unknown as FurnaceData;
const container = tile.containers.main;
const input = container.get_item(0);
const recipe = get_recipe(game.recipes, input);
// burn fuel
if (data.fuel > 0) {
data.fuel -= 1;
}
if (!can_craft(container, recipe)) {
data.progress = 0;
return;
}
// refuel
if (data.fuel === 0 && has_fuel(game.recipes, container)) {
data.fuel = consume_fuel(game.recipes, container);
data.fuel_max = data.fuel;
}
// cook
if (data.fuel > 0 && recipe) {
data.progress_max = recipe.cook_time;
data.progress += 1;
if (data.progress >= recipe.cook_time) {
data.progress = 0;
craft(container, recipe);
}
}
},
};
// items that still need code, until they're components in mods/bworld (phase 2 in MODS.md)
export interface WateringCanData {
water: number;
max_water: number;
}
export function add_base_item_hooks() {
const watering_can = EverythingRegistry.get<ItemRegistry<WateringCanData>>("items", "bworld:watering_can");
if (watering_can) {
watering_can.on_create = (item) => {
item.data = { water: 0, max_water: 32 };
};
watering_can.get_lore = (item) => `Water: ${item.data?.water}/${item.data?.max_water}`;
}
}