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 = {}; // give the breaking player whatever was inside function give_container_contents(tile: Tile, player: ServerPlayer | undefined) { if (!player) { return; } for (const container of Object.values(tile.containers)) { for (let i = 0; i < container.size; i++) { const item = container.get_item(i); if (item) { player.give(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, player) { give_container_contents(tile, player); }, }; // 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, player) { give_container_contents(tile, player); }, 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>("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}`; } }