import { Container, ItemStack } from "$/common/inventory.ts"; import { ScreenLayout } from "$/common/protocol.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 export interface FurnaceRecipe { input: string; output: ItemStack; cook_time: number; } export const FURNACE_RECIPES: FurnaceRecipe[] = [ { input: "bworld:log", output: new ItemStack("bworld:coal", 1), cook_time: 100, }, { input: "bworld:coal_ore", output: new ItemStack("bworld:coal", 1), cook_time: 100, }, { input: "bworld:iron_ore", output: new ItemStack("bworld:iron_ingot", 1), cook_time: 200, }, { input: "bworld:tin_ore", output: new ItemStack("bworld:tin_ingot", 1), cook_time: 200, }, { input: "bworld:copper_ore", output: new ItemStack("bworld:copper_ingot", 1), cook_time: 200, }, { input: "bworld:gold_ore", output: new ItemStack("bworld:gold_ingot", 1), cook_time: 200, }, ]; export const FUEL_VALUES: Record = { "bworld:coal": 1000, "bworld:log": 100, }; interface FurnaceData { progress: number; progress_max: number; fuel: number; fuel_max: number; } function get_recipe(input?: ItemStack | undefined): FurnaceRecipe | undefined { if (!input) { return; } return FURNACE_RECIPES.find((r) => r.input === 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.type_id) { return false; } return output.amount < output.max_amount; } function get_fuel_value(item?: ItemStack | undefined): number { if (!item) { return 0; } return FUEL_VALUES[item.type_id] ?? 0; } function has_fuel(container: Container) { return get_fuel_value(container.get_item(1)) > 0; } function consume_fuel(container: Container): number { const fuel = container.get_slot(1)!; const value = get_fuel_value(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, recipe.output.clone()); } else { output.amount += recipe.output.amount; } 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(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(container)) { data.fuel = consume_fuel(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); } } }, };