Server authority
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
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<string, BlockBehavior> = {};
|
||||
|
||||
// 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 FurnaceRecipe {
|
||||
input: string;
|
||||
output: ItemStack;
|
||||
cook_time: number;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
const FUEL_VALUES: Record<string, number> = {
|
||||
"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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user