Server authority

This commit is contained in:
2026-09-24 19:34:07 -03:00
parent 1bef94ce0c
commit 79faa556de
75 changed files with 2247 additions and 1632 deletions
+16
View File
@@ -0,0 +1,16 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
// behavior lives in server/game/blocks.ts
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:chest", {
id: "bworld:chest",
textures: "bworld:planks",
toughness: 8,
requires_tool: false,
tool_to_break: "axe",
drop_table: "bworld:chest",
has_collision: false,
interactive: true,
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:coal_ore", {
id: "bworld:coal_ore",
textures: "bworld:stone_coal",
has_collision: true,
drop_table: "bworld:coal_ore",
toughness: 5,
requires_tool: true,
tool_to_break: "pickaxe",
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:copper_ore", {
id: "bworld:copper_ore",
textures: "bworld:stone_copper",
has_collision: true,
drop_table: "bworld:copper_ore",
toughness: 5,
requires_tool: true,
tool_to_break: "pickaxe",
});
register_block_item(block);
+145
View File
@@ -0,0 +1,145 @@
import { EverythingRegistry } from "$/common/everything_registry.ts";
interface CropsRegistry {
total_stages: number;
time_to_grow: number[];
item_drop: string;
sprite_ids: string[];
regrowable: boolean;
}
EverythingRegistry.register<CropsRegistry>("crops", "bworld:carrot", {
total_stages: 5,
time_to_grow: [60, 60 * 3, 60 * 3, 60 * 3],
sprite_ids: [
"bworld:carrot_seeds",
"bworld:carrot_stage_1",
"bworld:carrot_stage_2",
"bworld:carrot_stage_3",
"bworld:carrot_stage_4",
],
item_drop: "bworld:carrot",
regrowable: false,
});
EverythingRegistry.register<CropsRegistry>("crops", "bworld:potato", {
total_stages: 5,
time_to_grow: [45, 60 * 2, 60 * 2, 60 * 2],
sprite_ids: [
"bworld:potato_seeds",
"bworld:potato_stage_1",
"bworld:potato_stage_2",
"bworld:potato_stage_3",
"bworld:potato_stage_4",
],
item_drop: "bworld:potato",
regrowable: false,
});
EverythingRegistry.register<CropsRegistry>("crops", "bworld:tomato", {
total_stages: 5,
time_to_grow: [90, 60 * 2, 60 * 2, 60 * 3],
sprite_ids: [
"bworld:tomato_seeds",
"bworld:tomato_stage_1",
"bworld:tomato_stage_2",
"bworld:tomato_stage_3",
"bworld:tomato_stage_4",
],
item_drop: "bworld:tomato",
regrowable: true,
});
EverythingRegistry.register<CropsRegistry>("crops", "bworld:pumpkin", {
total_stages: 5,
time_to_grow: [60 * 2, 60 * 4, 60 * 4, 60 * 4],
sprite_ids: [
"bworld:pumpkin_seeds",
"bworld:pumpkin_stage_1",
"bworld:pumpkin_stage_2",
"bworld:pumpkin_stage_3",
"bworld:pumpkin_stage_4",
],
item_drop: "bworld:pumpkin",
regrowable: false,
});
interface TileCropData {
current_stage: number;
growth_time: number;
}
/*
function create_crop_tile(crop_id: string) {
const crop_info = EverythingRegistry.get<CropsRegistry>("crops", crop_id);
return {
has_collision: false,
on_create(_, tile) {
tile.data = {
current_stage: 0,
growth_time: 0,
};
},
texture_id(tile) {
return crop_info.sprite_ids[tile.data!.current_stage];
},
on_click(world, tile) {
const [player] = world.get_tag("player")!;
const player_inventory = player.get(PlayerComponent)!.player_inventory;
const item = player_inventory.container.get_item(player_inventory.hotbar_selected);
if (!item) {
return;
}
if (item.type_id === "bworld:pickaxe") {
world.dimension.delete_tile(world, tile);
}
},
on_interact(world, tile) {
if (tile.data!.current_stage + 1 !== crop_info.total_stages) {
return;
}
const [player] = world.get_tag("player")!;
const player_inventory = player.get(PlayerComponent)!.player_inventory;
player_inventory.container.add_item(new ItemStack(crop_info.item_drop));
if (crop_info.regrowable) {
tile.data!.current_stage -= 1;
} else {
world.dimension.delete_tile(world, tile);
}
},
on_second(_, tile, delta) {
const crop = tile.data!;
// finished growing
if (crop.current_stage + 1 === crop_info.total_stages) {
return;
}
// grow !
crop.growth_time += delta;
if (crop.growth_time >= crop_info.time_to_grow[crop.current_stage]) {
crop.current_stage += 1;
crop.growth_time = 0;
}
},
} as BlockRegistry<TileCropData>;
}
EverythingRegistry.register<BlockRegistry<TileCropData>>(
"blocks",
"bworld:tomato_crop",
create_crop_tile("bworld:tomato"),
);
EverythingRegistry.register<BlockRegistry<TileCropData>>(
"blocks",
"bworld:carrot_crop",
create_crop_tile("bworld:carrot"),
);
EverythingRegistry.register<BlockRegistry<TileCropData>>(
"blocks",
"bworld:potato_crop",
create_crop_tile("bworld:potato"),
);
EverythingRegistry.register<BlockRegistry<TileCropData>>(
"blocks",
"bworld:pumpkin_crop",
create_crop_tile("bworld:pumpkin"),
);
*/
+15
View File
@@ -0,0 +1,15 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
// hoeing it is handled in server/game/blocks.ts
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:dirt", {
id: "bworld:dirt",
textures: "bworld:dirt",
has_collision: false,
drop_table: "bworld:dirt",
toughness: 2,
requires_tool: false,
tool_to_break: "shovel",
});
register_block_item(block);
+16
View File
@@ -0,0 +1,16 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
// behavior lives in server/game/blocks.ts
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:furnace", {
id: "bworld:furnace",
textures: { front: "bworld:furnace", side: "bworld:stone" },
has_collision: true,
drop_table: "bworld:furnace",
toughness: 5,
requires_tool: true,
tool_to_break: "pickaxe",
interactive: true,
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:glass", {
id: "bworld:glass",
textures: "bworld:glass",
has_collision: true,
transparent: true,
toughness: 3,
requires_tool: false,
tool_to_break: "pickaxe",
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:gold_ore", {
id: "bworld:gold_ore",
textures: "bworld:stone_gold",
has_collision: true,
drop_table: "bworld:gold_ore",
toughness: 5,
requires_tool: true,
tool_to_break: "pickaxe",
});
register_block_item(block);
+12
View File
@@ -0,0 +1,12 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
// hoeing it is handled in server/game/blocks.ts
EverythingRegistry.register<BlockRegistry>("blocks", "bworld:grass", {
id: "bworld:grass",
textures: { top: "bworld:grass_top", bottom: "bworld:dirt", "side": "bworld:grass_side" },
has_collision: false,
drop_table: "bworld:dirt",
toughness: 2,
requires_tool: false,
tool_to_break: "shovel",
});
+18
View File
@@ -0,0 +1,18 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
// TODO: watered state
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:hoed_dirt", {
id: "bworld:hoed_dirt",
textures: { side: "bworld:dirt", top: "bworld:hoed_dirt", bottom: "bworld:dirt" },
has_collision: false,
drop_table: "bworld:dirt",
toughness: 5,
requires_tool: false,
tool_to_break: "shovel",
states: [
{ name: "watered", bits: 1, default: 0 },
],
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:iron_ore", {
id: "bworld:iron_ore",
textures: "bworld:stone_iron",
has_collision: true,
drop_table: "bworld:iron_ore",
toughness: 5,
requires_tool: true,
tool_to_break: "pickaxe",
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:leaves", {
id: "bworld:leaves",
textures: "bworld:leaves",
has_collision: true,
transparent: true,
toughness: 3,
requires_tool: false,
tool_to_break: "hoe",
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:log", {
id: "bworld:log",
textures: { side: "bworld:log_side", top: "bworld:log_top", bottom: "bworld:log_top" },
has_collision: true,
drop_table: "bworld:log",
toughness: 3,
requires_tool: false,
tool_to_break: "axe",
});
register_block_item(block);
+19
View File
@@ -0,0 +1,19 @@
import "./grass.ts";
import "./dirt.ts";
import "./crops.ts";
import "./water.ts";
import "./chest.ts";
import "./furnace.ts";
import "./stone.ts";
import "./log.ts";
import "./sand.ts";
import "./snow.ts";
import "./glass.ts";
import "./hoed_dirt.ts";
import "./leaves.ts";
import "./coal_ore.ts";
import "./copper_ore.ts";
import "./iron_ore.ts";
import "./tin_ore.ts";
import "./gold_ore.ts";
import "./planks.ts";
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:planks", {
id: "bworld:planks",
textures: "bworld:planks",
has_collision: true,
drop_table: "bworld:log",
toughness: 3,
requires_tool: false,
tool_to_break: "axe",
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:sand", {
id: "bworld:sand",
textures: "bworld:sand",
has_collision: true,
drop_table: "bworld:sand",
toughness: 3,
requires_tool: false,
tool_to_break: "shovel",
});
register_block_item(block);
+13
View File
@@ -0,0 +1,13 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:snow", {
id: "bworld:snow",
textures: "bworld:snow",
has_collision: true,
toughness: 2,
requires_tool: true,
tool_to_break: "shovel",
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:stone", {
id: "bworld:stone",
textures: "bworld:stone",
has_collision: true,
drop_table: "bworld:stone",
toughness: 3,
requires_tool: true,
tool_to_break: "pickaxe",
});
register_block_item(block);
+14
View File
@@ -0,0 +1,14 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
import { register_block_item } from "$/common/utils.ts";
const block = EverythingRegistry.register<BlockRegistry>("blocks", "bworld:tin_ore", {
id: "bworld:tin_ore",
textures: "bworld:stone_tin",
has_collision: true,
drop_table: "bworld:tin_ore",
toughness: 5,
requires_tool: true,
tool_to_break: "pickaxe",
});
register_block_item(block);
+9
View File
@@ -0,0 +1,9 @@
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<BlockRegistry>("blocks", "bworld:water", {
id: "bworld:water",
textures: "bworld:water",
has_collision: false,
transparent: true,
alpha: 0.8,
});
+10
View File
@@ -22,3 +22,13 @@ export const STATE_SHIFT = 16;
export const CHUNK_SIZE = 16;
export const CHUNK_HEIGHT = 128;
export const CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE;
// where a block placed against each face of another block goes
export const FACE_OFFSETS: Record<Faces, { x: number; y: number; z: number }> = {
top: { x: 0, y: 1, z: 0 },
bottom: { x: 0, y: -1, z: 0 },
north: { x: 0, y: 0, z: -1 },
south: { x: 0, y: 0, z: 1 },
west: { x: -1, y: 0, z: 0 },
east: { x: 1, y: 0, z: 0 },
};
+4 -10
View File
@@ -1,5 +1,4 @@
import { Block, Dimension } from "$/client/components/dimension.ts";
import { ItemStack } from "$/client/inventory.ts";
import type { ItemStack } from "./inventory.ts";
export class EverythingRegistry {
static #key_to_id = new Map<string, Map<string, number>>();
@@ -94,12 +93,9 @@ export interface BlockRegistry {
states?: BlockStateDefinition[];
variants?: Record<string, BlockStateVariant>;
on_create?(dimension: Dimension, block: Block): void;
on_break?(dimension: Dimension, block: Block): void;
on_click?(dimension: Dimension, block: Block): void;
on_interact?(dimension: Dimension, block: Block): boolean;
on_tick?(dimension: Dimension, block: Block, tick_delta: number): void;
on_second?(dimension: Dimension, block: Block, second_delta: number): void;
// right clicking it does something instead of placing a block, clients don't predict placing against it.
// behavior runs on the server, see server/game/blocks.ts
interactive?: boolean;
compiled_states?: CompiledStateDefinition[];
}
@@ -109,8 +105,6 @@ export interface ItemRegistry<T = unknown | undefined> {
block_id?: string;
tool_type?: string;
place?(dimension: Dimension, block: Block): void;
on_create?(item: ItemStack<T>): void;
get_lore?(item: ItemStack<T>): string;
+315
View File
@@ -0,0 +1,315 @@
import { Alea, create_noise_2d, create_noise_3d, NoiseFunction2D, NoiseFunction3D } from "@paulaboks/rng";
import { CHUNK_HEIGHT, CHUNK_SIZE } from "$/common/constants.ts";
// generation runs in a worker now, so it only needs somewhere to put blocks
export interface BlockSink {
add_block(block: { x: number; y: number; z: number; id: string }): void;
}
type Biome =
| "desert"
| "plains"
| "forest"
| "jungle"
| "tundra"
| "taiga"
| "snow"
| "savanna"
| "swamp";
type OreDef = {
id: string;
min_y: number;
max_y: number;
scale: number;
threshold: number;
};
const ORES: OreDef[] = [
{ id: "bworld:coal_ore", min_y: 20, max_y: 120, scale: 0.05, threshold: 0.55 },
{ id: "bworld:copper_ore", min_y: 10, max_y: 80, scale: 0.06, threshold: 0.6 },
{ id: "bworld:tin_ore", min_y: 5, max_y: 60, scale: 0.06, threshold: 0.62 },
{ id: "bworld:iron_ore", min_y: 5, max_y: 50, scale: 0.05, threshold: 0.65 },
{ id: "bworld:gold_ore", min_y: 0, max_y: 30, scale: 0.04, threshold: 0.7 },
];
function get_biome(temp: number, moisture: number): Biome {
if (temp > 0.6) {
if (moisture < -0.2) {
return "desert";
}
if (moisture > 0.4) {
return "jungle";
}
return "savanna";
}
if (temp > 0) {
if (moisture > 0.5) {
return "swamp";
}
if (moisture > 0) {
return "forest";
}
return "plains";
}
if (temp > -0.5) {
return "taiga";
}
return "tundra";
}
function get_surface_block(biome: Biome) {
if (biome === "desert") {
return "bworld:sand";
} else if (biome === "tundra") {
return "bworld:snow";
}
return "bworld:grass";
}
function biome_height_modifier(biome: Biome) {
if (biome === "desert") {
return 0.2;
}
if (biome === "plains") {
return 0.4;
}
if (biome === "forest") {
return 0.5;
}
if (biome === "jungle") {
return 0.45;
}
if (biome === "taiga") {
return 0.55;
}
if (biome === "tundra") {
return 0.35;
}
if (biome === "savanna") {
return 0.4;
}
if (biome === "swamp") {
return 0.35;
}
return 0.4;
}
function fractal_noise(noise: NoiseFunction2D, x: number, y: number, octaves = 2) {
let value = 0;
let amp = 1;
let freq = 1;
let max = 0;
for (let i = 0; i < octaves; i++) {
value += noise(x * freq, y * freq) * amp;
max += amp;
amp *= 0.5;
freq *= 2;
}
return value / max;
}
function get_terrain_height(base: number, biome: Biome, x: number, z: number, noise: NoiseFunction2D) {
const biomeMod = biome_height_modifier(biome);
const main = fractal_noise(noise, x * 0.003, z * 0.003) * 15;
const detail = fractal_noise(noise, x * 0.01, z * 0.01) * 3;
return Math.floor(base + biomeMod * 20 + main + detail);
}
function can_place_tree(tree_map: boolean[][], local_x: number, local_z: number) {
const TREE_SPACING = 4;
for (let dx = -TREE_SPACING; dx <= TREE_SPACING; dx++) {
for (let dz = -TREE_SPACING; dz <= TREE_SPACING; dz++) {
const nx = local_x + dx;
const nz = local_z + dz;
if (nx >= 0 && nx < CHUNK_SIZE && nz >= 0 && nz < CHUNK_SIZE && tree_map[nx][nz]) {
return false;
}
}
}
return true;
}
function place_tree(dimension: BlockSink, rng: Alea, x: number, y: number, z: number, biome: Biome) {
const height = Math.floor(rng.next() * 3) + (biome === "jungle" ? 8 : 4);
const trunk_block = "bworld:log";
const leaves_block = "bworld:leaves";
for (let i = 0; i < height; i++) {
dimension.add_block({ x, y: y + i, z, id: trunk_block });
}
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
for (let dy = -1; dy <= 1; dy++) {
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy) <= 3) {
dimension.add_block({
x: x + dx,
y: y + height + dy,
z: z + dz,
id: leaves_block,
});
}
}
}
}
}
const TREE_THRESHOLD: Record<Biome, number> = {
forest: 0.5,
jungle: 0.3,
taiga: 0.6,
plains: 0.95,
desert: 1,
tundra: 1,
savanna: 0.65,
swamp: 0.5,
snow: 0.8,
};
function should_place_tree(feature_noise: NoiseFunction2D, biome: Biome, x: number, z: number) {
const n = feature_noise(x * 0.1, z * 0.1);
return n > (TREE_THRESHOLD[biome] ?? 0.8);
}
interface SeedNoises {
height_noise: NoiseFunction2D;
temp_noise: NoiseFunction2D;
moisture_noise: NoiseFunction2D;
feature_noise: NoiseFunction2D;
ore_noises: NoiseFunction3D[];
}
// building the permutation tables is expensive, only do it once per seed
const noise_cache = new Map<string, SeedNoises>();
function get_noises(seed: string): SeedNoises {
let noises = noise_cache.get(seed);
if (!noises) {
noises = {
height_noise: create_noise_2d(new Alea(seed + "_height")),
temp_noise: create_noise_2d(new Alea(seed + "_temp")),
moisture_noise: create_noise_2d(new Alea(seed + "_moisture")),
feature_noise: create_noise_2d(new Alea(seed + "_feature")),
ore_noises: ORES.map((ore) => create_noise_3d(new Alea(seed + "_" + ore.id))),
};
noise_cache.set(seed, noises);
}
return noises;
}
export function generate_chunk(dimension: BlockSink, cx: number, cz: number, seed = "seed") {
const { height_noise, temp_noise, moisture_noise, feature_noise, ore_noises } = get_noises(seed);
// seeded per chunk so every client generates the exact same terrain
const rng = new Alea(`${seed}_chunk_${cx}_${cz}`);
const biome_scale = 0.003;
const terrain_scale = 0.01;
const tree_map: boolean[][] = Array.from({ length: CHUNK_SIZE }, () => Array(CHUNK_SIZE).fill(false));
for (let x = 0; x < CHUNK_SIZE; x++) {
for (let z = 0; z < CHUNK_SIZE; z++) {
const wx = cx * CHUNK_SIZE + x;
const wz = cz * CHUNK_SIZE + z;
const temp = temp_noise(wx * biome_scale, wz * biome_scale);
const moisture = moisture_noise(wx * biome_scale, wz * biome_scale);
const biome = get_biome(temp, moisture);
const height_noise_value = fractal_noise(height_noise, wx * terrain_scale, wz * terrain_scale);
const base_height = (height_noise_value + 1) * 15 + 50;
const height = get_terrain_height(base_height, biome, wx, wz, height_noise);
const surface_block = get_surface_block(biome);
for (let y = 0; y <= height; y++) {
let block = "bworld:stone";
if (y < height - 3) {
for (let i = 0; i < ORES.length; i++) {
const ore = ORES[i];
if (y >= ore.min_y && y <= ore.max_y) {
const noise = ore_noises[i](
wx * ore.scale,
y * ore.scale,
wz * ore.scale,
);
if (noise > ore.threshold) {
block = ore.id;
break;
}
}
}
}
if (y === height) {
block = surface_block;
} else if (y > height - 4) {
block = "bworld:dirt";
}
if (biome === "swamp" && y === height && rng.next() < 0.2) {
block = "bworld:water";
}
dimension.add_block({ x: wx, y, z: wz, id: block });
}
if (should_place_tree(feature_noise, biome, wx, wz) && can_place_tree(tree_map, x, z)) {
place_tree(dimension, rng, wx, height + 1, wz, biome);
tree_map[x][z] = true;
}
}
}
}
export interface RawChunk {
// numeric block ids, only what this chunk generated itself
blocks: Uint32Array;
// blocks it generated in other chunks (tree leaves), flattened as x, y, z, numeric id
spills: Int32Array;
}
// generates one chunk on its own. neighbors' spills get merged in by whoever assembles the world:
// a chunk's own blocks always win and spills only fill air, so the result doesn't depend on load order
export function generate_raw_chunk(
chunk_x: number,
chunk_z: number,
seed: string,
block_ids: Record<string, number>,
): RawChunk {
const blocks = new Uint32Array(CHUNK_SIZE * CHUNK_SIZE * CHUNK_HEIGHT);
const spills: number[] = [];
generate_chunk(
{
add_block(block) {
const nid = block_ids[block.id];
if (nid === undefined || block.y < 0 || block.y >= CHUNK_HEIGHT) {
return;
}
const block_chunk_x = Math.floor(block.x / CHUNK_SIZE);
const block_chunk_z = Math.floor(block.z / CHUNK_SIZE);
if (block_chunk_x !== chunk_x || block_chunk_z !== chunk_z) {
spills.push(block.x, block.y, block.z, nid);
return;
}
const lx = block.x - chunk_x * CHUNK_SIZE;
const lz = block.z - chunk_z * CHUNK_SIZE;
blocks[block.y * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx] = nid;
},
},
chunk_x,
chunk_z,
seed,
);
return { blocks, spills: new Int32Array(spills) };
}
+270
View File
@@ -0,0 +1,270 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
// how item stacks are saved and sent over the network
export interface ItemData {
id: string;
count: number;
data?: unknown;
}
export class ItemStack<T = unknown | undefined> {
type_id: string;
amount: number;
max_amount: number;
data?: T;
constructor(type_id: string | string, amount: number = 1, max_amount: number = 64) {
this.type_id = type_id;
this.amount = amount;
this.max_amount = max_amount;
this.data = undefined;
const item_info = EverythingRegistry.get<ItemRegistry>("items", type_id);
if (item_info?.on_create) {
item_info.on_create(this);
}
}
clone(): ItemStack {
const item = new ItemStack(this.type_id, this.amount, this.max_amount);
item.data = structuredClone(this.data);
return item;
}
to_data(): ItemData {
return this.data === undefined
? { id: this.type_id, count: this.amount }
: { id: this.type_id, count: this.amount, data: this.data };
}
static from_data(data: ItemData): ItemStack {
const item = new ItemStack(data.id, data.count);
if (data.data !== undefined) {
item.data = data.data;
}
return item;
}
}
export class ContainerSlot {
#item_stack: ItemStack | undefined;
has_item() {
return this.#item_stack !== undefined;
}
set_item(item_stack: ItemStack | undefined) {
if ((item_stack?.amount ?? 0) <= 0) {
item_stack = undefined;
}
this.#item_stack = item_stack;
}
get_item() {
return this.#item_stack;
}
get type_id() {
return this.#item_stack?.type_id;
}
set amount(new_amount: number) {
if (this.#item_stack) {
this.#item_stack.amount = new_amount;
if (this.#item_stack.amount <= 0) {
this.#item_stack = undefined;
}
}
}
get amount(): number | undefined {
return this.#item_stack?.amount;
}
get max_amount() {
return this.#item_stack?.max_amount;
}
}
export class Container {
#slots: ContainerSlot[] = [];
readonly size: number;
constructor(size: number) {
this.size = size;
for (let i = 0; i < size; i += 1) {
this.#slots.push(new ContainerSlot());
}
}
// returns how many items didn't fit
add_item(item_stack: ItemStack): number {
for (const slot of this.#slots.filter((slot) => slot.has_item())) {
const slot_item = slot.get_item()!;
if (slot_item.type_id === item_stack.type_id) {
const missing = slot_item.max_amount - slot_item.amount;
const adding = Math.min(missing, item_stack.amount);
slot_item.amount += adding;
item_stack.amount -= adding;
if (item_stack.amount === 0) {
return 0;
}
}
}
for (const slot of this.#slots) {
if (!slot.has_item()) {
slot.set_item(item_stack);
return 0;
}
}
// TODO: drop item on ground
return item_stack.amount;
}
get_item(slot: number): ItemStack | undefined {
return this.#slots[slot]?.get_item();
}
get_slot(slot: number): ContainerSlot {
return this.#slots[slot];
}
set_item(slot: number, item: ItemStack | undefined) {
this.#slots[slot].set_item(item);
}
to_data(): (ItemData | null)[] {
return this.#slots.map((slot) => slot.get_item()?.to_data() ?? null);
}
load(data: (ItemData | null)[]) {
for (let i = 0; i < this.size; i += 1) {
const item = data[i];
this.#slots[i].set_item(item ? ItemStack.from_data(item) : undefined);
}
}
clear() {
for (const slot of this.#slots) {
slot.set_item(undefined);
}
}
}
// the item a player is carrying around with the mouse in an inventory screen
export interface Cursor {
item: ItemStack | undefined;
}
export const LEFT_CLICK = 0;
export const RIGHT_CLICK = 2;
// what clicking a normal slot does, the server runs this for real and clients run it to predict
export function click_slot(container: Container, index: number, cursor: Cursor, button: number) {
if (button === LEFT_CLICK) {
left_click(container, index, cursor);
} else if (button === RIGHT_CLICK) {
right_click(container, index, cursor);
}
if (cursor.item && cursor.item.amount <= 0) {
cursor.item = undefined;
}
}
function swap_with_cursor(container: Container, index: number, cursor: Cursor) {
const slot = container.get_slot(index);
const original = cursor.item;
cursor.item = slot.get_item();
slot.set_item(original);
}
function left_click(container: Container, index: number, cursor: Cursor) {
const holding = cursor.item;
const slot = container.get_slot(index);
const slot_item = slot.get_item();
// if you aren't holding anything
// "swap" with nothing on your hand (pick it up)
if (!holding) {
swap_with_cursor(container, index, cursor);
return;
}
// if you are holding something and slot type equals holding type
// try to add to stack
if (slot_item && slot.type_id === holding.type_id) {
const space_left = slot.max_amount! - slot_item.amount;
const amount_to_add = Math.min(space_left, holding.amount);
slot_item.amount += amount_to_add;
holding.amount -= amount_to_add;
return;
}
// if something on hand but not the same
// swap
swap_with_cursor(container, index, cursor);
}
function right_click(container: Container, index: number, cursor: Cursor) {
const holding = cursor.item;
const slot = container.get_slot(index);
const slot_item = slot.get_item();
// if holding something
if (holding) {
// and slot type equals holding type
// add 1 to matching stack
if (slot_item && slot.type_id === holding.type_id) {
if (slot_item.amount < slot_item.max_amount) {
slot_item.amount += 1;
holding.amount -= 1;
}
return;
}
// place 1 into empty slot
if (!slot_item) {
const new_item = holding.clone();
new_item.amount = 1;
slot.set_item(new_item);
holding.amount -= 1;
return;
}
// if something on hand but not the same
// swap
swap_with_cursor(container, index, cursor);
return;
}
// if player is holding nothing and clicks nothing, nothing happens
if (!slot_item) {
return;
}
// pick up half of the stack
const original_amount = slot_item.amount;
const half = Math.floor(original_amount / 2);
const picked_up = slot_item.clone();
picked_up.amount = original_amount - half;
cursor.item = picked_up;
slot.amount = half;
}
// output slots (furnace result, crafting result) can only be taken from, all at once
// returns whether it was taken
export function take_output(item: ItemStack, cursor: Cursor): boolean {
const holding = cursor.item;
if (!holding) {
cursor.item = item.clone();
return true;
}
if (holding.type_id === item.type_id && holding.max_amount - holding.amount >= item.amount) {
holding.amount += item.amount;
return true;
}
return false;
}
+6
View File
@@ -0,0 +1,6 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:axe", {
texture_id: "bworld:axe",
tool_type: "axe",
});
+5
View File
@@ -0,0 +1,5 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:coal", {
texture_id: "bworld:coal",
});
+5
View File
@@ -0,0 +1,5 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:copper_ingot", {
texture_id: "bworld:copper_ingot",
});
+5
View File
@@ -0,0 +1,5 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:gold_ingot", {
texture_id: "bworld:gold_ingot",
});
+5
View File
@@ -0,0 +1,5 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:hoe", {
texture_id: "bworld:hoe",
});
+5
View File
@@ -0,0 +1,5 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:iron_ingot", {
texture_id: "bworld:iron_ingot",
});
+11
View File
@@ -0,0 +1,11 @@
import "./watering_can.ts";
import "./axe.ts";
import "./pickaxe.ts";
import "./hoe.ts";
import "./coal.ts";
import "./tin_ingot.ts";
import "./iron_ingot.ts";
import "./copper_ingot.ts";
import "./gold_ingot.ts";
import "./stick.ts";
import "./wood_pickaxe.ts";
+5
View File
@@ -0,0 +1,5 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:pickaxe", {
texture_id: "bworld:pickaxe",
});
+5
View File
@@ -0,0 +1,5 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:stick", {
texture_id: "bworld:stick",
});
+5
View File
@@ -0,0 +1,5 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:tin_ingot", {
texture_id: "bworld:tin_ingot",
});
+16
View File
@@ -0,0 +1,16 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
export interface WateringCanData {
water: number;
max_water: number;
}
EverythingRegistry.register<ItemRegistry<WateringCanData>>("items", "bworld:watering_can", {
texture_id: "bworld:watering_can",
on_create(item) {
item.data = { water: 0, max_water: 32 };
},
get_lore(item) {
return `Water: ${item.data?.water}/${item.data?.max_water}`;
},
});
+6
View File
@@ -0,0 +1,6 @@
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
EverythingRegistry.register<ItemRegistry>("items", "bworld:wood_pickaxe", {
texture_id: "bworld:wood_pickaxe",
tool_type: "pickaxe",
});
+50 -3
View File
@@ -1,4 +1,6 @@
// messages sent between the client and the server, as json over a websocket
import type { Faces } from "./constants.ts";
import type { ItemData } from "./inventory.ts";
export const AIR_ID = "bworld:air";
@@ -15,19 +17,64 @@ export interface PlayerInfo {
// x, y, z, block id
export type BlockChange = [number, number, number, string];
// the containers a client can see and click. "screen" is whatever the open server screen shows
export type ContainerKey = "inventory" | "crafting" | "screen";
export const CRAFTING_RESULT_SLOT = 9;
// a screen the server opens, drawn below the player's inventory and hotbar
export interface ScreenLayout {
// height of the screen's own area, in slots
rows: number;
// x and y in slots, can be fractional
// output slots can only be taken from, like the furnace result
slots: { index: number; x: number; y: number; output?: boolean }[];
// progress bars filled from properties[value] / properties[max]
bars: {
x: number;
y: number;
value: string;
max: string;
direction: "up" | "right";
empty_texture: string;
full_texture: string;
}[];
}
// clients send what the player is trying to do, the server decides what happens
export type ClientMessage =
| { type: "hello"; name: string }
| { type: "move"; x: number; y: number; z: number; yaw: number; pitch: number }
| { type: "set_block"; x: number; y: number; z: number; id: string }
| { type: "break_block"; x: number; y: number; z: number }
// right click on a block: interact with it, or place the held block against `face`
| { type: "use_block"; x: number; y: number; z: number; face: Faces }
| { type: "select_slot"; slot: number }
| { type: "click"; container: ContainerKey; index: number; button: number }
// closes the open screen, including the player's own inventory screen
| { type: "close_screen" }
| { type: "chat"; text: string };
export type ServerMessage =
| { type: "welcome"; id: string; seed: string; players: PlayerInfo[]; changes: BlockChange[] }
| {
type: "welcome";
id: string;
seed: string;
players: PlayerInfo[];
changes: BlockChange[];
spawn: { x: number; y: number; z: number; yaw: number; pitch: number };
selected_slot: number;
}
| { type: "player_join"; player: PlayerInfo }
| { type: "player_leave"; id: string }
| { type: "player_move"; id: string; x: number; y: number; z: number; yaw: number; pitch: number }
// also sent to the player who caused it, which corrects anything their client predicted wrong
| { type: "set_block"; x: number; y: number; z: number; id: string }
| { type: "chat"; from?: string; text: string };
| { type: "chat"; from?: string; text: string }
| { type: "container"; container: ContainerKey; items: (ItemData | null)[] }
| { type: "cursor"; item: ItemData | null }
| { type: "open_screen"; layout: ScreenLayout; properties: Record<string, number> }
| { type: "screen_properties"; properties: Record<string, number> }
| { type: "close_screen" };
export const MAX_NAME_LENGTH = 16;
export const MAX_CHAT_LENGTH = 256;
+6 -9
View File
@@ -1,5 +1,4 @@
import { AssetManager } from "../client/assets.ts";
import { ID_MASK, SpriteRegion, STATE_SHIFT } from "./constants.ts";
import { ID_MASK, STATE_SHIFT } from "./constants.ts";
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "./everything_registry.ts";
export function point_inside_rec(
@@ -16,13 +15,6 @@ export function point_inside_rec(
point_y < rec_y + rec_h;
}
type TexturesInfo = Record<string, SpriteRegion>;
export function get_sprite_region(id: string): SpriteRegion {
const textures_info = AssetManager.instance.get<TexturesInfo>("bworld:textures_info");
return textures_info?.[id] ?? { x: 0, y: 0 };
}
export function distance_point_rectangle(px: number, py: number, sqx: number, sqy: number, sqw: number, sqh: number) {
const x0 = sqx;
const y0 = sqy;
@@ -106,3 +98,8 @@ function compile_block_states(block_info: BlockRegistry) {
return { name: s.name, mask, shift };
});
}
// numeric so looking chunks up doesnt allocate a string every time, fine for |x|, |z| < 32768
export function chunk_key(x: number, z: number) {
return (x + 32768) * 65536 + (z + 32768);
}