Server authority
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
import { Container, Inventory } from "../inventory.ts";
|
||||
import { PlayerComponent } from "../player.ts";
|
||||
import { GuiChest } from "$/client/gui/gui_chest.ts";
|
||||
|
||||
interface TileChestData {
|
||||
inventory: Inventory;
|
||||
}
|
||||
|
||||
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,
|
||||
on_interact(dimension, block) {
|
||||
const [player] = dimension.world.get_tag("player")!;
|
||||
const player_component = player.get(PlayerComponent)!;
|
||||
const block_data = dimension.get_block_data<TileChestData>(block.x, block.y, block.z);
|
||||
if (block_data && block_data.data.inventory) {
|
||||
const gui = new GuiChest(block_data.data.inventory, player_component.player_inventory);
|
||||
player_component.screens.push(gui);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
on_create(dimension, block) {
|
||||
dimension.add_block_data({
|
||||
id: block.id,
|
||||
x: block.x,
|
||||
y: block.y,
|
||||
z: block.z,
|
||||
data: {
|
||||
inventory: new Inventory(new Container(9 * 3)),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,147 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { ItemStack } from "../inventory.ts";
|
||||
import { PlayerComponent } from "../player.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"),
|
||||
);
|
||||
*/
|
||||
@@ -1,27 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { register_block_item } from "$/common/utils.ts";
|
||||
import { PlayerComponent } from "../player.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",
|
||||
|
||||
on_interact(dimension, block) {
|
||||
const [player] = dimension.world.get_tag("player")!;
|
||||
const player_inventory = player.get(PlayerComponent)!.player_inventory;
|
||||
const maybe_item = player_inventory.container.get_item(player_inventory.hotbar_selected);
|
||||
if (maybe_item && maybe_item.type_id === "bworld:hoe") {
|
||||
dimension.add_block({ x: block.x, y: block.y, z: block.z, id: "bworld:hoed_dirt" });
|
||||
dimension.sync_block(block.x, block.y, block.z);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,200 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { Container, Inventory, ItemStack } from "../inventory.ts";
|
||||
import { PlayerComponent } from "../player.ts";
|
||||
import { GuiFurnace } from "$/client/gui/gui_furnace.ts";
|
||||
import { register_block_item } from "../../common/utils.ts";
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const FUEL_VALUES: Record<string, number> = {
|
||||
"bworld:coal": 1000,
|
||||
"bworld:log": 100,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
interface TileChestData {
|
||||
inventory: Inventory;
|
||||
progress: number;
|
||||
progress_max: number;
|
||||
fuel: number;
|
||||
fuel_max: number;
|
||||
}
|
||||
|
||||
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",
|
||||
|
||||
on_interact(dimension, block) {
|
||||
const [player] = dimension.world.get_tag("player")!;
|
||||
const player_component = player.get(PlayerComponent)!;
|
||||
const block_data = dimension.get_block_data<TileChestData>(block.x, block.y, block.z);
|
||||
if (block_data && block_data.data.inventory) {
|
||||
const gui = new GuiFurnace(block_data.data.inventory, player_component.player_inventory, block_data.data);
|
||||
player_component.screens.push(gui);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
on_create(dimension, block) {
|
||||
dimension.add_block_data({
|
||||
id: block.id,
|
||||
x: block.x,
|
||||
y: block.y,
|
||||
z: block.z,
|
||||
data: {
|
||||
inventory: new Inventory(new Container(3)),
|
||||
progress: 0,
|
||||
progress_max: 0,
|
||||
fuel: 0,
|
||||
fuel_max: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
on_break() {
|
||||
// TODO: remove block data
|
||||
},
|
||||
on_tick(dimension, block) {
|
||||
const block_data = dimension.get_block_data<TileChestData>(block.x, block.y, block.z);
|
||||
if (!block_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = block_data.data;
|
||||
const container = data.inventory.container;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
register_block_item(block);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,25 +0,0 @@
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { PlayerComponent } from "../player.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",
|
||||
|
||||
on_interact(dimension, block) {
|
||||
const [player] = dimension.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?.type_id === "bworld:hoe") {
|
||||
block.id = "bworld:hoed_dirt";
|
||||
dimension.add_block(block);
|
||||
dimension.sync_block(block.x, block.y, block.z);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
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);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,19 +0,0 @@
|
||||
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";
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,13 +0,0 @@
|
||||
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);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,9 +0,0 @@
|
||||
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,
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import { UIRenderSystem } from "$/client/systems/ui_render_system.ts";
|
||||
import { create_main_menu } from "./main_menu.ts";
|
||||
import { start_game } from "./game.ts";
|
||||
import { canvas, resize_canvas } from "./renderer/mod.ts";
|
||||
import { DimensionLogicSystem } from "./systems/dimension_logic.ts";
|
||||
import { Dimension } from "./components/dimension.ts";
|
||||
import { GuiRenderSystem, GuiTickSystem } from "./gui/gui_systems.ts";
|
||||
import { WorldGenerationSystem } from "./systems/world_generation_system.ts";
|
||||
@@ -27,11 +26,10 @@ export class ClientWorld extends World {
|
||||
|
||||
dimension!: Dimension;
|
||||
|
||||
// undefined when playing single player
|
||||
connection?: Connection;
|
||||
connection: Connection;
|
||||
chat_log: ChatLine[] = [];
|
||||
|
||||
constructor(connection?: Connection) {
|
||||
constructor(connection: Connection) {
|
||||
super("game");
|
||||
this.connection = connection;
|
||||
|
||||
@@ -55,7 +53,6 @@ export class ClientWorld extends World {
|
||||
this.add_system(new GuiTickSystem(), "game");
|
||||
this.add_system(new PlayerControlsSystem(), "game");
|
||||
this.add_system(new WorldGenerationSystem(), "game");
|
||||
this.add_system(new DimensionLogicSystem(), "game");
|
||||
this.add_system(new CollisionSystem(), "game");
|
||||
this.add_system(new MovementSystem(), "game");
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from "$/common/ecs/mod.ts";
|
||||
import { chunk_key } from "$/common/utils.ts";
|
||||
import { AIR_ID, BlockChange } from "$/common/protocol.ts";
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
import { AIR, CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE, Faces, ID_MASK, VOID } from "../../common/constants.ts";
|
||||
@@ -6,19 +7,9 @@ import { AssetManager } from "../assets.ts";
|
||||
import { ClientWorld } from "../client_world.ts";
|
||||
import { ChunkWorkerPool } from "../chunk_workers.ts";
|
||||
import type { FromChunkWorker } from "../workers/chunk_messages.ts";
|
||||
import { ItemStack } from "../inventory.ts";
|
||||
import { PlayerComponent } from "../player.ts";
|
||||
import { create_vertex_buffer, destroy_vertex_buffer, Texture } from "../renderer/mod.ts";
|
||||
import { Camera } from "./camera.ts";
|
||||
|
||||
export interface BlockData<T = unknown> {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface Block {
|
||||
id: string;
|
||||
x: number;
|
||||
@@ -32,7 +23,6 @@ export interface Chunk {
|
||||
x: number;
|
||||
z: number;
|
||||
blocks: Uint32Array;
|
||||
blocks_data: BlockData[];
|
||||
generated: boolean;
|
||||
dirty: boolean;
|
||||
// bumped on every mesh request so late results from older requests get ignored
|
||||
@@ -43,10 +33,7 @@ export interface Chunk {
|
||||
transparent_vertex_count?: number;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
export { chunk_key };
|
||||
|
||||
const NEIGHBOR_OFFSETS = [[-1, 0], [1, 0], [0, -1], [0, 1]] as const;
|
||||
|
||||
@@ -99,7 +86,6 @@ export class Dimension extends Component {
|
||||
x,
|
||||
z,
|
||||
blocks,
|
||||
blocks_data: [],
|
||||
dirty: true,
|
||||
generated: false,
|
||||
mesh_version: 0,
|
||||
@@ -120,7 +106,7 @@ export class Dimension extends Component {
|
||||
chunk = this.add_chunk(block_chunk_x, block_chunk_z);
|
||||
}
|
||||
|
||||
const [nid, block_info] = EverythingRegistry.get_full<BlockRegistry>("blocks", block.id)!;
|
||||
const nid = EverythingRegistry.get_id("blocks", block.id)!;
|
||||
|
||||
const lx = block.x - block_chunk_x * CHUNK_SIZE;
|
||||
const lz = block.z - block_chunk_z * CHUNK_SIZE;
|
||||
@@ -130,9 +116,7 @@ export class Dimension extends Component {
|
||||
|
||||
chunk.blocks[index] = nid;
|
||||
chunk.dirty = true;
|
||||
if (block_info?.on_create) {
|
||||
block_info?.on_create(this, block);
|
||||
}
|
||||
this.#mark_border_neighbors_dirty(block_chunk_x, block_chunk_z, lx, lz);
|
||||
}
|
||||
|
||||
get_block(x: number, y: number, z: number) {
|
||||
@@ -154,33 +138,8 @@ export class Dimension extends Component {
|
||||
return chunk.blocks[index] & ID_MASK;
|
||||
}
|
||||
|
||||
add_block_data(block_data: BlockData) {
|
||||
const block_chunk_x = Math.floor(block_data.x / CHUNK_SIZE);
|
||||
const block_chunk_z = Math.floor(block_data.z / CHUNK_SIZE);
|
||||
let chunk = this.get_chunk(block_chunk_x, block_chunk_z);
|
||||
if (!chunk) {
|
||||
chunk = this.add_chunk(block_chunk_x, block_chunk_z);
|
||||
}
|
||||
|
||||
chunk.blocks_data.push(block_data);
|
||||
}
|
||||
|
||||
get_block_data<T>(x: number, y: number, z: number) {
|
||||
const chunk_x = Math.floor(x / CHUNK_SIZE);
|
||||
const chunk_z = Math.floor(z / CHUNK_SIZE);
|
||||
|
||||
const chunk = this.get_chunk(chunk_x, chunk_z);
|
||||
|
||||
if (!chunk) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return chunk.blocks_data.find((block_data) =>
|
||||
block_data.x === x && block_data.y === y && block_data.z === z
|
||||
) as BlockData<T>;
|
||||
}
|
||||
|
||||
break_block(x: number, y: number, z: number, drop_item: boolean = true) {
|
||||
// only changes what this client shows, drops and everything else happen on the server
|
||||
break_block(x: number, y: number, z: number) {
|
||||
const block_chunk_x = Math.floor(x / CHUNK_SIZE);
|
||||
const block_chunk_z = Math.floor(z / CHUNK_SIZE);
|
||||
const chunk = this.get_chunk(block_chunk_x, block_chunk_z);
|
||||
@@ -188,14 +147,6 @@ export class Dimension extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const block_info = EverythingRegistry.get_by_id<BlockRegistry>("blocks", this.get_block(x, y, z))!;
|
||||
|
||||
if (drop_item && block_info.drop_table) {
|
||||
const [player] = this.world.get_tag("player")!;
|
||||
const player_component = player.get(PlayerComponent)!;
|
||||
player_component.player_inventory.container.add_item(new ItemStack(block_info.drop_table));
|
||||
}
|
||||
|
||||
const lx = x - block_chunk_x * CHUNK_SIZE;
|
||||
const lz = z - block_chunk_z * CHUNK_SIZE;
|
||||
const ly = y;
|
||||
@@ -203,7 +154,11 @@ export class Dimension extends Component {
|
||||
const index = ly * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx;
|
||||
chunk.blocks[index] = AIR;
|
||||
chunk.dirty = true;
|
||||
this.#mark_border_neighbors_dirty(block_chunk_x, block_chunk_z, lx, lz);
|
||||
}
|
||||
|
||||
// a block on a chunk's edge changes which faces its neighbor shows
|
||||
#mark_border_neighbors_dirty(block_chunk_x: number, block_chunk_z: number, lx: number, lz: number) {
|
||||
if (lx === 0) {
|
||||
const n = this.get_chunk(block_chunk_x - 1, block_chunk_z);
|
||||
if (n) {
|
||||
@@ -226,10 +181,6 @@ export class Dimension extends Component {
|
||||
n.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (block_info?.on_break) {
|
||||
block_info.on_break(this, { x, y, z, id: block_info.id });
|
||||
}
|
||||
}
|
||||
|
||||
index_to_xyz(index: number) {
|
||||
@@ -252,18 +203,7 @@ export class Dimension extends Component {
|
||||
chunk_changes.set(`${x},${y},${z}`, [x, y, z, id]);
|
||||
}
|
||||
|
||||
// call after the player changes a block so it gets saved and sent to the server
|
||||
sync_block(x: number, y: number, z: number) {
|
||||
const numeric_id = this.get_block(x, y, z);
|
||||
const id = numeric_id === AIR ? AIR_ID : EverythingRegistry.get_by_id<BlockRegistry>("blocks", numeric_id)?.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
this.record_change(x, y, z, id);
|
||||
this.world.connection?.send({ type: "set_block", x, y, z, id });
|
||||
}
|
||||
|
||||
// set a block without drops or syncing, for changes that came from somewhere else
|
||||
// set a block the server told us about
|
||||
apply_change(x: number, y: number, z: number, id: string) {
|
||||
const chunk = this.get_chunk(Math.floor(x / CHUNK_SIZE), Math.floor(z / CHUNK_SIZE));
|
||||
if (!chunk || !chunk.generated) {
|
||||
@@ -273,7 +213,7 @@ export class Dimension extends Component {
|
||||
const current = this.get_block(x, y, z);
|
||||
if (id === AIR_ID) {
|
||||
if (current !== AIR) {
|
||||
this.break_block(x, y, z, false);
|
||||
this.break_block(x, y, z);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -423,7 +363,8 @@ export class Dimension extends Component {
|
||||
chunk.transparent_vertex_count = message.transparent_count / 9;
|
||||
}
|
||||
|
||||
// sets a block from generation, no hooks, makes a placeholder chunk like add_block does
|
||||
// a neighbor's leaves, only fill air so the result doesn't depend on which chunk loaded first.
|
||||
// the server builds chunks the same way (server/game/world.ts)
|
||||
#set_block_raw(x: number, y: number, z: number, nid: number) {
|
||||
if (y < 0 || y >= CHUNK_HEIGHT) {
|
||||
return;
|
||||
@@ -433,8 +374,11 @@ export class Dimension extends Component {
|
||||
const chunk = this.get_chunk(chunk_x, chunk_z) ?? this.add_chunk(chunk_x, chunk_z);
|
||||
const lx = x - chunk_x * CHUNK_SIZE;
|
||||
const lz = z - chunk_z * CHUNK_SIZE;
|
||||
chunk.blocks[y * CHUNK_AREA + lz * CHUNK_SIZE + lx] = nid;
|
||||
chunk.dirty = true;
|
||||
const index = y * CHUNK_AREA + lz * CHUNK_SIZE + lx;
|
||||
if (chunk.blocks[index] === AIR) {
|
||||
chunk.blocks[index] = nid;
|
||||
chunk.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
delete_chunk_mesh(chunk: Chunk) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
let stopped = false;
|
||||
|
||||
// replaces the game with a message, for when it can't go on (no server, lost connection)
|
||||
export function show_fatal_error(message: string) {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
stopped = true;
|
||||
document.exitPointerLock?.();
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.style.cssText =
|
||||
"position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;" +
|
||||
"gap:12px;background:rgba(0,0,0,0.85);color:white;font:20px system-ui,sans-serif;text-align:center;";
|
||||
const text = document.createElement("div");
|
||||
text.textContent = message;
|
||||
const hint = document.createElement("div");
|
||||
hint.textContent = "Reload the page to try again.";
|
||||
hint.style.cssText = "font-size:14px;opacity:0.7;";
|
||||
overlay.append(text, hint);
|
||||
document.body.append(overlay);
|
||||
}
|
||||
|
||||
export function is_stopped() {
|
||||
return stopped;
|
||||
}
|
||||
+2
-2
@@ -13,8 +13,8 @@ export function start_game(world: ClientWorld) {
|
||||
|
||||
const dimension = new Entity("dimension");
|
||||
world.dimension?.dispose();
|
||||
world.dimension = new Dimension(world, world.connection?.seed);
|
||||
for (const [x, y, z, id] of world.connection?.initial_changes ?? []) {
|
||||
world.dimension = new Dimension(world, world.connection.seed);
|
||||
for (const [x, y, z, id] of world.connection.initial_changes) {
|
||||
world.dimension.record_change(x, y, z, id);
|
||||
}
|
||||
dimension.add(world.dimension);
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
import { Alea, create_noise_2d, create_noise_3d, NoiseFunction2D, NoiseFunction3D } from "@paulaboks/rng";
|
||||
import { 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-28
@@ -3,7 +3,6 @@ import { canvas, draw_rect, draw_text, measure_text } from "$/client/renderer/mo
|
||||
import { InputManager } from "../input_manager.ts";
|
||||
import { ClientWorld } from "../client_world.ts";
|
||||
import { PlayerComponent } from "../player.ts";
|
||||
import { ItemStack } from "../inventory.ts";
|
||||
import { MAX_CHAT_LENGTH } from "$/common/protocol.ts";
|
||||
import { render_chat_log } from "../systems/rendering/network.ts";
|
||||
|
||||
@@ -102,14 +101,9 @@ export class GuiChat extends GuiScreen {
|
||||
override on_close(): void {}
|
||||
|
||||
submit() {
|
||||
if (this.text_typed.startsWith("/")) {
|
||||
this.command();
|
||||
} else if (this.text_typed.trim().length > 0) {
|
||||
if (this.world.connection) {
|
||||
this.world.connection.send({ type: "chat", text: this.text_typed });
|
||||
} else {
|
||||
this.world.add_chat(this.text_typed);
|
||||
}
|
||||
// commands like /give run on the server too
|
||||
if (this.text_typed.trim().length > 0) {
|
||||
this.world.connection.send({ type: "chat", text: this.text_typed });
|
||||
}
|
||||
this.text_typed = "";
|
||||
|
||||
@@ -119,23 +113,4 @@ export class GuiChat extends GuiScreen {
|
||||
player_component.pop_screen();
|
||||
}
|
||||
}
|
||||
|
||||
command() {
|
||||
if (this.text_typed.startsWith("/give")) {
|
||||
let [_, item_id, quantity] = this.text_typed.split(" ");
|
||||
|
||||
if (!item_id.includes(":")) {
|
||||
item_id = `bworld:${item_id}`;
|
||||
}
|
||||
if (quantity === undefined) {
|
||||
quantity = "1";
|
||||
}
|
||||
|
||||
const [player] = this.world.get_tag("player")!;
|
||||
const player_component = player.get(PlayerComponent);
|
||||
if (player_component) {
|
||||
player_component.player_inventory.container.add_item(new ItemStack(item_id, Number(quantity)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Inventory, PlayerInventory } from "../inventory.ts";
|
||||
import { add_player_hotbar, add_player_inventory, GuiInventoryScreen, Slot } from "./gui_screen.ts";
|
||||
import { canvas, draw_rect, Texture } from "$/client/renderer/mod.ts";
|
||||
import { AssetManager } from "../assets.ts";
|
||||
import { SLOT_SIZE } from "../../common/constants.ts";
|
||||
import { draw_nine_slice } from "../systems/rendering/render_utils.ts";
|
||||
|
||||
const PADDING = 10;
|
||||
|
||||
export class GuiChest extends GuiInventoryScreen {
|
||||
override inventory_width = PADDING * 2 + SLOT_SIZE * 9;
|
||||
override inventory_height = PADDING * 4 + SLOT_SIZE * 7;
|
||||
|
||||
constructor(inventory: Inventory, player_inventory: PlayerInventory) {
|
||||
super(inventory, player_inventory, undefined);
|
||||
|
||||
add_player_hotbar(this, player_inventory, PADDING, PADDING);
|
||||
add_player_inventory(this, player_inventory, PADDING, PADDING * 2 + SLOT_SIZE);
|
||||
|
||||
const chest_x = PADDING;
|
||||
const chest_y = PADDING * 3 + SLOT_SIZE * 4;
|
||||
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
for (let l = 0; l < 9; ++l) {
|
||||
this.slots.push(
|
||||
new Slot(
|
||||
this.inventory,
|
||||
l + i * 9,
|
||||
chest_x + l * SLOT_SIZE,
|
||||
chest_y + i * SLOT_SIZE,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override on_render(): void {
|
||||
draw_rect(0, 0, canvas.width, canvas.height, [0, 0, 0, 0.8]);
|
||||
|
||||
const ui = AssetManager.instance.get<Texture>("bworld:ui");
|
||||
|
||||
draw_nine_slice(
|
||||
ui,
|
||||
160,
|
||||
0,
|
||||
16,
|
||||
16,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
this.x,
|
||||
this.y,
|
||||
this.inventory_width,
|
||||
this.inventory_height,
|
||||
);
|
||||
|
||||
super.on_render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { add_player_hotbar, add_player_inventory, GuiInventoryScreen, Slot } from "./gui_screen.ts";
|
||||
import { canvas, draw_rect, draw_texture_region, Texture } from "$/client/renderer/mod.ts";
|
||||
import { AssetManager } from "../assets.ts";
|
||||
import { SLOT_SIZE, TEXTURE_SIZE } from "$/common/constants.ts";
|
||||
import { ClientMessage, ScreenLayout } from "$/common/protocol.ts";
|
||||
import { draw_nine_slice } from "../systems/rendering/render_utils.ts";
|
||||
import { get_sprite_region } from "$/client/sprites.ts";
|
||||
import { ClientInventories } from "../inventory.ts";
|
||||
|
||||
const PADDING = 10;
|
||||
|
||||
// a screen opened by the server (chest, furnace, ...), drawn from the layout it sent
|
||||
export class GuiContainer extends GuiInventoryScreen {
|
||||
layout: ScreenLayout;
|
||||
properties: Record<string, number>;
|
||||
|
||||
constructor(
|
||||
inventories: ClientInventories,
|
||||
send: (message: ClientMessage) => void,
|
||||
layout: ScreenLayout,
|
||||
properties: Record<string, number>,
|
||||
) {
|
||||
super(inventories, send);
|
||||
this.layout = layout;
|
||||
this.properties = properties;
|
||||
|
||||
this.inventory_width = PADDING * 2 + SLOT_SIZE * 9;
|
||||
this.inventory_height = PADDING * 4 + SLOT_SIZE * (4 + layout.rows);
|
||||
|
||||
add_player_hotbar(this, PADDING, PADDING);
|
||||
add_player_inventory(this, PADDING, PADDING * 2 + SLOT_SIZE);
|
||||
|
||||
const area_y = PADDING * 3 + SLOT_SIZE * 4;
|
||||
for (const slot of layout.slots) {
|
||||
this.slots.push(
|
||||
new Slot("screen", slot.index, PADDING + slot.x * SLOT_SIZE, area_y + slot.y * SLOT_SIZE, slot.output),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
override on_render(): void {
|
||||
draw_rect(0, 0, canvas.width, canvas.height, [0, 0, 0, 0.8]);
|
||||
|
||||
const ui = AssetManager.instance.get<Texture>("bworld:ui");
|
||||
|
||||
draw_nine_slice(
|
||||
ui,
|
||||
160,
|
||||
0,
|
||||
16,
|
||||
16,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
this.x,
|
||||
this.y,
|
||||
this.inventory_width,
|
||||
this.inventory_height,
|
||||
);
|
||||
|
||||
this.draw_bars();
|
||||
|
||||
super.on_render();
|
||||
}
|
||||
|
||||
draw_bars() {
|
||||
const atlas = AssetManager.instance.get<Texture>("bworld:textures");
|
||||
const area_y = PADDING * 3 + SLOT_SIZE * 4;
|
||||
|
||||
for (const bar of this.layout.bars) {
|
||||
const x = this.x + PADDING + bar.x * SLOT_SIZE;
|
||||
const y = this.y + area_y + bar.y * SLOT_SIZE;
|
||||
const max = this.properties[bar.max] ?? 0;
|
||||
const pct = max > 0 ? Math.max(0, Math.min(1, (this.properties[bar.value] ?? 0) / max)) : 0;
|
||||
|
||||
const empty = get_sprite_region(bar.empty_texture);
|
||||
draw_texture_region(
|
||||
atlas,
|
||||
empty.x * TEXTURE_SIZE,
|
||||
empty.y * TEXTURE_SIZE,
|
||||
TEXTURE_SIZE,
|
||||
TEXTURE_SIZE,
|
||||
x,
|
||||
y,
|
||||
SLOT_SIZE,
|
||||
SLOT_SIZE,
|
||||
);
|
||||
|
||||
const full = get_sprite_region(bar.full_texture);
|
||||
if (bar.direction === "right") {
|
||||
draw_texture_region(
|
||||
atlas,
|
||||
full.x * TEXTURE_SIZE,
|
||||
full.y * TEXTURE_SIZE,
|
||||
TEXTURE_SIZE * pct,
|
||||
TEXTURE_SIZE,
|
||||
x,
|
||||
y,
|
||||
SLOT_SIZE * pct,
|
||||
SLOT_SIZE,
|
||||
);
|
||||
} else {
|
||||
// fills from the bottom up
|
||||
draw_texture_region(
|
||||
atlas,
|
||||
full.x * TEXTURE_SIZE,
|
||||
full.y * TEXTURE_SIZE + TEXTURE_SIZE * (1 - pct),
|
||||
TEXTURE_SIZE,
|
||||
TEXTURE_SIZE * pct,
|
||||
x,
|
||||
y + SLOT_SIZE * (1 - pct),
|
||||
SLOT_SIZE,
|
||||
SLOT_SIZE * pct,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
import { Inventory, PlayerInventory } from "../inventory.ts";
|
||||
import { add_player_hotbar, add_player_inventory, GuiInventoryScreen, Slot } from "./gui_screen.ts";
|
||||
import { canvas, draw_rect, draw_texture_region, Texture } from "$/client/renderer/mod.ts";
|
||||
import { AssetManager } from "../assets.ts";
|
||||
import { SLOT_SIZE, TEXTURE_SIZE } from "$/common/constants.ts";
|
||||
import { draw_nine_slice } from "../systems/rendering/render_utils.ts";
|
||||
import { get_sprite_region } from "$/common/utils.ts";
|
||||
|
||||
const PADDING = 10;
|
||||
|
||||
// crazy this is how i found out interface X {} is diffrent then type X = {}
|
||||
type TileChestData = {
|
||||
inventory: Inventory;
|
||||
progress: number;
|
||||
progress_max: number;
|
||||
fuel: number;
|
||||
fuel_max: number;
|
||||
};
|
||||
|
||||
export class GuiFurnace extends GuiInventoryScreen<Inventory, TileChestData> {
|
||||
override inventory_width = PADDING * 2 + SLOT_SIZE * 9;
|
||||
override inventory_height = PADDING * 4 + SLOT_SIZE * 7;
|
||||
|
||||
constructor(
|
||||
inventory: Inventory,
|
||||
player_inventory: PlayerInventory,
|
||||
properties: TileChestData,
|
||||
) {
|
||||
super(inventory, player_inventory, properties);
|
||||
|
||||
add_player_hotbar(this, player_inventory, PADDING, PADDING);
|
||||
add_player_inventory(this, player_inventory, PADDING, PADDING * 2 + SLOT_SIZE);
|
||||
|
||||
const furnace_y = PADDING * 3 + SLOT_SIZE * 4;
|
||||
|
||||
this.slots.push(
|
||||
new Slot(
|
||||
this.inventory,
|
||||
0,
|
||||
this.inventory_width / 2 - SLOT_SIZE,
|
||||
furnace_y,
|
||||
),
|
||||
);
|
||||
this.slots.push(
|
||||
new Slot(
|
||||
this.inventory,
|
||||
1,
|
||||
this.inventory_width / 2 - SLOT_SIZE,
|
||||
furnace_y + SLOT_SIZE * 2,
|
||||
),
|
||||
);
|
||||
this.slots.push(
|
||||
new Slot(
|
||||
this.inventory,
|
||||
2,
|
||||
this.inventory_width / 2 + SLOT_SIZE,
|
||||
furnace_y + SLOT_SIZE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
override on_render(): void {
|
||||
draw_rect(0, 0, canvas.width, canvas.height, [0, 0, 0, 0.8]);
|
||||
|
||||
const ui = AssetManager.instance.get<Texture>("bworld:ui");
|
||||
|
||||
draw_nine_slice(
|
||||
ui,
|
||||
160,
|
||||
0,
|
||||
16,
|
||||
16,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
this.x,
|
||||
this.y,
|
||||
this.inventory_width,
|
||||
this.inventory_height,
|
||||
);
|
||||
|
||||
this.draw_fire();
|
||||
this.draw_arrow();
|
||||
|
||||
super.on_render();
|
||||
}
|
||||
|
||||
override handle_left_click(slot: Slot): void {
|
||||
if (slot.inventory === this.inventory && slot.index === 2) {
|
||||
if (this.inventory.container.get_item(2)) {
|
||||
this.pickup();
|
||||
}
|
||||
} else {
|
||||
super.handle_left_click(slot);
|
||||
}
|
||||
}
|
||||
|
||||
override handle_right_click(slot: Slot): void {
|
||||
if (slot.inventory === this.inventory && slot.index === 2) {
|
||||
if (this.inventory.container.get_item(2)) {
|
||||
this.pickup();
|
||||
}
|
||||
} else {
|
||||
super.handle_right_click(slot);
|
||||
}
|
||||
}
|
||||
|
||||
pickup() {
|
||||
const holding_item = this.player_inventory.holding_item;
|
||||
const item = this.inventory.container.get_item(2)!;
|
||||
|
||||
if (holding_item) {
|
||||
if (holding_item.type_id === item.type_id) {
|
||||
const items_left = holding_item.max_amount - holding_item.amount;
|
||||
if (items_left >= item.amount) {
|
||||
holding_item.amount += item.amount;
|
||||
this.inventory.container.set_item(2, undefined);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.player_inventory.holding_item = item;
|
||||
this.inventory.container.set_item(2, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
draw_fire() {
|
||||
const atlas = AssetManager.instance.get<Texture>("bworld:textures");
|
||||
|
||||
const fire_empty_region = get_sprite_region("bworld:fire_empty");
|
||||
const fire_full_region = get_sprite_region("bworld:fire_full");
|
||||
|
||||
const fuel_pct = Math.max(0, Math.min(1, this.properties.fuel / this.properties.fuel_max));
|
||||
const full_height = SLOT_SIZE * fuel_pct;
|
||||
|
||||
const furnace_y = PADDING * 3 + SLOT_SIZE * 4;
|
||||
|
||||
draw_texture_region(
|
||||
atlas,
|
||||
fire_empty_region.x * TEXTURE_SIZE,
|
||||
fire_empty_region.y * TEXTURE_SIZE,
|
||||
TEXTURE_SIZE,
|
||||
TEXTURE_SIZE,
|
||||
this.x + this.inventory_width / 2 - SLOT_SIZE,
|
||||
this.y + furnace_y + SLOT_SIZE,
|
||||
SLOT_SIZE,
|
||||
SLOT_SIZE,
|
||||
);
|
||||
|
||||
const fire_draw_x = this.x + this.inventory_width / 2 - SLOT_SIZE;
|
||||
const fire_draw_y = this.y + furnace_y + SLOT_SIZE;
|
||||
|
||||
const fire_src_height = TEXTURE_SIZE * fuel_pct;
|
||||
|
||||
const fire_src_y_offset = TEXTURE_SIZE - fire_src_height;
|
||||
const fire_dst_y_offset = SLOT_SIZE - full_height;
|
||||
|
||||
draw_texture_region(
|
||||
atlas,
|
||||
fire_full_region.x * TEXTURE_SIZE,
|
||||
fire_full_region.y * TEXTURE_SIZE + fire_src_y_offset,
|
||||
TEXTURE_SIZE,
|
||||
fire_src_height,
|
||||
fire_draw_x,
|
||||
fire_draw_y + fire_dst_y_offset,
|
||||
SLOT_SIZE,
|
||||
full_height,
|
||||
);
|
||||
}
|
||||
|
||||
draw_arrow() {
|
||||
const atlas = AssetManager.instance.get<Texture>("bworld:textures");
|
||||
|
||||
const arrow_empty_region = get_sprite_region("bworld:arrow_empty");
|
||||
const arrow_full_region = get_sprite_region("bworld:arrow_full");
|
||||
|
||||
const progress_pct = Math.max(0, Math.min(1, this.properties.progress / this.properties.progress_max));
|
||||
const full_width = SLOT_SIZE * progress_pct;
|
||||
|
||||
const furnace_y = PADDING * 3 + SLOT_SIZE * 4;
|
||||
|
||||
draw_texture_region(
|
||||
atlas,
|
||||
arrow_empty_region.x * TEXTURE_SIZE,
|
||||
arrow_empty_region.y * TEXTURE_SIZE,
|
||||
TEXTURE_SIZE,
|
||||
TEXTURE_SIZE,
|
||||
this.x + this.inventory_width / 2,
|
||||
this.y + furnace_y + SLOT_SIZE,
|
||||
SLOT_SIZE,
|
||||
SLOT_SIZE,
|
||||
);
|
||||
|
||||
const draw_x = this.x + this.inventory_width / 2;
|
||||
const draw_y = this.y + furnace_y + SLOT_SIZE;
|
||||
|
||||
const src_width = TEXTURE_SIZE * progress_pct;
|
||||
|
||||
draw_texture_region(
|
||||
atlas,
|
||||
arrow_full_region.x * TEXTURE_SIZE,
|
||||
arrow_full_region.y * TEXTURE_SIZE,
|
||||
src_width,
|
||||
TEXTURE_SIZE,
|
||||
draw_x,
|
||||
draw_y,
|
||||
full_width,
|
||||
SLOT_SIZE,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,23 @@
|
||||
import { Container, Inventory, ItemStack, PlayerInventory } from "../inventory.ts";
|
||||
import { add_player_hotbar, add_player_inventory, GuiInventoryScreen, Slot } from "./gui_screen.ts";
|
||||
import { canvas, draw_rect, Texture } from "$/client/renderer/mod.ts";
|
||||
import { AssetManager } from "../assets.ts";
|
||||
import { SLOT_SIZE } from "../../common/constants.ts";
|
||||
import { draw_nine_slice } from "../systems/rendering/render_utils.ts";
|
||||
|
||||
export interface CraftingRecipe {
|
||||
width: number;
|
||||
height: number;
|
||||
pattern: (string | undefined)[];
|
||||
result: ItemStack;
|
||||
}
|
||||
|
||||
const recipes: CraftingRecipe[] = [
|
||||
{
|
||||
width: 3,
|
||||
height: 3,
|
||||
pattern: [
|
||||
"bworld:planks",
|
||||
"bworld:planks",
|
||||
"bworld:planks",
|
||||
"bworld:planks",
|
||||
undefined,
|
||||
"bworld:planks",
|
||||
"bworld:planks",
|
||||
"bworld:planks",
|
||||
"bworld:planks",
|
||||
],
|
||||
result: new ItemStack("bworld:chest", 1),
|
||||
},
|
||||
{
|
||||
width: 3,
|
||||
height: 3,
|
||||
pattern: [
|
||||
"bworld:stone",
|
||||
"bworld:stone",
|
||||
"bworld:stone",
|
||||
"bworld:stone",
|
||||
undefined,
|
||||
"bworld:stone",
|
||||
"bworld:stone",
|
||||
"bworld:stone",
|
||||
"bworld:stone",
|
||||
],
|
||||
result: new ItemStack("bworld:furnace", 1),
|
||||
},
|
||||
{
|
||||
width: 1,
|
||||
height: 1,
|
||||
pattern: [
|
||||
"bworld:log",
|
||||
],
|
||||
result: new ItemStack("bworld:planks", 2),
|
||||
},
|
||||
{
|
||||
width: 1,
|
||||
height: 2,
|
||||
pattern: [
|
||||
"bworld:planks",
|
||||
"bworld:planks",
|
||||
],
|
||||
result: new ItemStack("bworld:stick", 2),
|
||||
},
|
||||
{
|
||||
width: 3,
|
||||
height: 3,
|
||||
pattern: [
|
||||
"bworld:planks",
|
||||
"bworld:planks",
|
||||
"bworld:planks",
|
||||
undefined,
|
||||
"bworld:stick",
|
||||
undefined,
|
||||
undefined,
|
||||
"bworld:stick",
|
||||
undefined,
|
||||
],
|
||||
result: new ItemStack("bworld:wood_pickaxe", 1),
|
||||
},
|
||||
];
|
||||
import { ClientInventories } from "../inventory.ts";
|
||||
import { ClientMessage, CRAFTING_RESULT_SLOT } from "$/common/protocol.ts";
|
||||
|
||||
const PADDING = 10;
|
||||
|
||||
// the recipes live on the server, which fills in the result slot
|
||||
export class GuiPlayerInventory extends GuiInventoryScreen {
|
||||
override inventory_width = PADDING * 2 + SLOT_SIZE * 9;
|
||||
override inventory_height = PADDING * 2 + SLOT_SIZE * 4 + PADDING + SLOT_SIZE * 3 + PADDING;
|
||||
|
||||
constructor(player_inventory: PlayerInventory) {
|
||||
super(new Inventory(new Container(10)), player_inventory, undefined);
|
||||
constructor(inventories: ClientInventories, send: (message: ClientMessage) => void) {
|
||||
super(inventories, send);
|
||||
|
||||
add_player_hotbar(this, player_inventory, PADDING, PADDING);
|
||||
add_player_inventory(this, player_inventory, PADDING, PADDING * 2 + SLOT_SIZE);
|
||||
add_player_hotbar(this, PADDING, PADDING);
|
||||
add_player_inventory(this, PADDING, PADDING * 2 + SLOT_SIZE);
|
||||
|
||||
const crafting_x = PADDING;
|
||||
const crafting_y = PADDING * 3 + SLOT_SIZE * 4;
|
||||
@@ -98,17 +25,14 @@ export class GuiPlayerInventory extends GuiInventoryScreen {
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
for (let j = 0; j < 3; j += 1) {
|
||||
this.slots.push(
|
||||
new Slot(this.inventory, j * 3 + i, crafting_x + i * SLOT_SIZE, crafting_y + j * SLOT_SIZE),
|
||||
new Slot("crafting", j * 3 + i, crafting_x + i * SLOT_SIZE, crafting_y + j * SLOT_SIZE),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.slots.push(new Slot(this.inventory, 9, crafting_x + 5 * SLOT_SIZE, crafting_y + 1 * SLOT_SIZE));
|
||||
}
|
||||
|
||||
override on_tick(delta: number): void {
|
||||
super.on_tick(delta);
|
||||
this.update_crafting_result();
|
||||
this.slots.push(
|
||||
new Slot("crafting", CRAFTING_RESULT_SLOT, crafting_x + 5 * SLOT_SIZE, crafting_y + 1 * SLOT_SIZE, true),
|
||||
);
|
||||
}
|
||||
|
||||
override on_render(): void {
|
||||
@@ -134,136 +58,4 @@ export class GuiPlayerInventory extends GuiInventoryScreen {
|
||||
|
||||
super.on_render();
|
||||
}
|
||||
|
||||
override on_close(): void {
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
const item = this.inventory.container.get_item(i);
|
||||
if (item) {
|
||||
this.player_inventory.container.add_item(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_crafting_grid(): (string | undefined)[] {
|
||||
const grid: (string | undefined)[] = [];
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const item = this.inventory.container.get_item(i);
|
||||
grid.push(item?.type_id);
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
matches_recipe(
|
||||
grid: (string | undefined)[],
|
||||
recipe: CraftingRecipe,
|
||||
): boolean {
|
||||
for (let y = 0; y <= 3 - recipe.height; y++) {
|
||||
for (let x = 0; x <= 3 - recipe.width; x++) {
|
||||
let match = true;
|
||||
|
||||
for (let gy = 0; gy < 3; gy++) {
|
||||
for (let gx = 0; gx < 3; gx++) {
|
||||
const grid_index = gy * 3 + gx;
|
||||
|
||||
if (
|
||||
gx >= x &&
|
||||
gx < x + recipe.width &&
|
||||
gy >= y &&
|
||||
gy < y + recipe.height
|
||||
) {
|
||||
const rx = gx - x;
|
||||
const ry = gy - y;
|
||||
const recipe_index = ry * recipe.width + rx;
|
||||
|
||||
if (grid[grid_index] !== recipe.pattern[recipe_index]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (grid[grid_index] !== undefined) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!match) break;
|
||||
}
|
||||
|
||||
if (match) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
update_crafting_result() {
|
||||
const grid = this.get_crafting_grid();
|
||||
|
||||
for (const recipe of recipes) {
|
||||
if (this.matches_recipe(grid, recipe)) {
|
||||
this.inventory.container.set_item(
|
||||
9,
|
||||
recipe.result.clone(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.inventory.container.set_item(9, undefined);
|
||||
}
|
||||
|
||||
consume_recipe_items() {
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const item = this.inventory.container.get_item(i);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.amount -= 1;
|
||||
|
||||
if (item.amount <= 0) {
|
||||
this.inventory.container.set_item(i, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override handle_left_click(slot: Slot): void {
|
||||
if (slot.inventory === this.inventory && slot.index === 9) {
|
||||
if (this.inventory.container.get_item(9)) {
|
||||
this.craft();
|
||||
}
|
||||
} else {
|
||||
super.handle_left_click(slot);
|
||||
}
|
||||
}
|
||||
|
||||
override handle_right_click(slot: Slot): void {
|
||||
if (slot.inventory === this.inventory && slot.index === 9) {
|
||||
if (this.inventory.container.get_item(9)) {
|
||||
this.craft();
|
||||
}
|
||||
} else {
|
||||
super.handle_right_click(slot);
|
||||
}
|
||||
}
|
||||
|
||||
craft() {
|
||||
const holding_item = this.player_inventory.holding_item;
|
||||
const item = this.inventory.container.get_item(9)!;
|
||||
|
||||
if (holding_item) {
|
||||
if (holding_item.type_id === item.type_id) {
|
||||
const items_left = holding_item.max_amount - holding_item.amount;
|
||||
if (items_left >= item.amount) {
|
||||
this.consume_recipe_items();
|
||||
holding_item.amount += item.amount;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.player_inventory.holding_item = item;
|
||||
this.consume_recipe_items();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+75
-185
@@ -1,27 +1,28 @@
|
||||
import { SLOT_SIZE } from "$/common/constants.ts";
|
||||
import { click_slot, Container } from "$/common/inventory.ts";
|
||||
import { ClientMessage, ContainerKey, CRAFTING_RESULT_SLOT } from "$/common/protocol.ts";
|
||||
import { point_inside_rec } from "../../common/utils.ts";
|
||||
import { AssetManager } from "../assets.ts";
|
||||
import { InputManager } from "../input_manager.ts";
|
||||
import { Inventory, ItemStack, PlayerInventory } from "../inventory.ts";
|
||||
import { ClientInventories } from "../inventory.ts";
|
||||
import { canvas, Texture } from "../renderer/mod.ts";
|
||||
import { draw_item, draw_nine_slice } from "../systems/rendering/render_utils.ts";
|
||||
|
||||
export class Slot {
|
||||
inventory: Inventory;
|
||||
container: ContainerKey;
|
||||
index: number;
|
||||
// relative to screen top left
|
||||
x: number;
|
||||
y: number;
|
||||
// can only be taken from, like the crafting or furnace result
|
||||
output: boolean;
|
||||
|
||||
constructor(inventory: Inventory, index: number, x: number, y: number) {
|
||||
this.inventory = inventory;
|
||||
constructor(container: ContainerKey, index: number, x: number, y: number, output = false) {
|
||||
this.container = container;
|
||||
this.index = index;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
get item(): ItemStack | undefined {
|
||||
return this.inventory.container.get_item(this.index);
|
||||
this.output = output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,25 +35,32 @@ export abstract class GuiScreen {
|
||||
abstract on_close(): void;
|
||||
}
|
||||
|
||||
export class GuiInventoryScreen<Inv extends Inventory = Inventory, Properties = Record<string, unknown> | undefined>
|
||||
extends GuiScreen {
|
||||
inventory: Inv;
|
||||
player_inventory: PlayerInventory;
|
||||
properties: Properties;
|
||||
// a screen of slots. the server owns the contents: clicks are sent to it and guessed locally
|
||||
// so they feel instant, then whatever the server syncs back wins
|
||||
export class GuiInventoryScreen extends GuiScreen {
|
||||
inventories: ClientInventories;
|
||||
send: (message: ClientMessage) => void;
|
||||
|
||||
slots: Slot[] = [];
|
||||
hovering: Slot | undefined;
|
||||
inventory_width: number = 500;
|
||||
inventory_height: number = 500;
|
||||
|
||||
constructor(
|
||||
inventory: Inv,
|
||||
player_inventory: PlayerInventory,
|
||||
properties: Properties,
|
||||
) {
|
||||
constructor(inventories: ClientInventories, send: (message: ClientMessage) => void) {
|
||||
super();
|
||||
this.inventory = inventory;
|
||||
this.player_inventory = player_inventory;
|
||||
this.properties = properties;
|
||||
this.inventories = inventories;
|
||||
this.send = send;
|
||||
}
|
||||
|
||||
get_container(key: ContainerKey): Container | undefined {
|
||||
switch (key) {
|
||||
case "inventory":
|
||||
return this.inventories.inventory;
|
||||
case "crafting":
|
||||
return this.inventories.crafting;
|
||||
case "screen":
|
||||
return this.inventories.screen;
|
||||
}
|
||||
}
|
||||
|
||||
on_tick(_delta: number): void {
|
||||
@@ -60,20 +68,17 @@ export class GuiInventoryScreen<Inv extends Inventory = Inventory, Properties =
|
||||
this.y = canvas.height / 2 - (this.inventory_height / 2);
|
||||
|
||||
this.handle_interaction();
|
||||
|
||||
if (this.player_inventory.holding_item?.amount === 0) {
|
||||
this.player_inventory.holding_item = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
on_render(): void {
|
||||
const ui = AssetManager.instance.get<Texture>("bworld:ui");
|
||||
|
||||
for (const slot of this.slots) {
|
||||
const hovered = slot === this.hovering;
|
||||
draw_nine_slice(
|
||||
ui,
|
||||
slot.inventory.hovering_slot === slot.index ? 19 * 16 : 160 + 32,
|
||||
slot.inventory.hovering_slot === slot.index ? 16 : 0,
|
||||
hovered ? 19 * 16 : 160 + 32,
|
||||
hovered ? 16 : 0,
|
||||
16,
|
||||
16,
|
||||
4,
|
||||
@@ -88,198 +93,83 @@ export class GuiInventoryScreen<Inv extends Inventory = Inventory, Properties =
|
||||
}
|
||||
|
||||
for (const slot of this.slots) {
|
||||
const item = slot.item;
|
||||
const item = this.get_container(slot.container)?.get_item(slot.index);
|
||||
if (item) {
|
||||
draw_item(item, this.x + slot.x, this.y + slot.y);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.player_inventory.holding_item) {
|
||||
if (this.inventories.cursor.item) {
|
||||
const mouse = InputManager.get_mouse_position();
|
||||
draw_item(this.player_inventory.holding_item, mouse.x, mouse.y);
|
||||
draw_item(this.inventories.cursor.item, mouse.x, mouse.y);
|
||||
}
|
||||
}
|
||||
|
||||
on_close(): void {}
|
||||
on_close(): void {
|
||||
this.send({ type: "close_screen" });
|
||||
|
||||
// the server puts these back in the inventory, show it right away
|
||||
const { inventory, crafting, cursor } = this.inventories;
|
||||
for (let i = 0; i < CRAFTING_RESULT_SLOT; i++) {
|
||||
const item = crafting.get_item(i);
|
||||
if (item) {
|
||||
inventory.add_item(item);
|
||||
crafting.set_item(i, undefined);
|
||||
}
|
||||
}
|
||||
crafting.set_item(CRAFTING_RESULT_SLOT, undefined);
|
||||
if (cursor.item) {
|
||||
inventory.add_item(cursor.item);
|
||||
cursor.item = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
handle_interaction() {
|
||||
this.player_inventory.hovering_slot = -1;
|
||||
this.inventory.hovering_slot = -1;
|
||||
this.hovering = undefined;
|
||||
const mouse = InputManager.get_mouse_position();
|
||||
|
||||
for (const slot of this.slots) {
|
||||
const mouse = InputManager.get_mouse_position();
|
||||
const slot_x = slot.x + this.x;
|
||||
const slot_y = slot.y + this.y;
|
||||
const hovering = point_inside_rec(mouse.x, mouse.y, slot_x, slot_y, SLOT_SIZE, SLOT_SIZE);
|
||||
if (hovering) {
|
||||
slot.inventory.hovering_slot = slot.index;
|
||||
const hovering = point_inside_rec(mouse.x, mouse.y, this.x + slot.x, this.y + slot.y, SLOT_SIZE, SLOT_SIZE);
|
||||
if (!hovering) {
|
||||
continue;
|
||||
}
|
||||
this.hovering = slot;
|
||||
|
||||
if (InputManager.is_mouse_pressed(0)) {
|
||||
InputManager.consume_mouse(0);
|
||||
this.handle_left_click(slot);
|
||||
return;
|
||||
}
|
||||
|
||||
if (InputManager.is_mouse_pressed(2)) {
|
||||
InputManager.consume_mouse(2);
|
||||
this.handle_right_click(slot);
|
||||
for (const button of [0, 2]) {
|
||||
if (InputManager.is_mouse_pressed(button)) {
|
||||
InputManager.consume_mouse(button);
|
||||
this.click(slot, button);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch_item_with_holding(inventory: Inventory, index: number) {
|
||||
const container_slot = inventory.container.get_slot(index);
|
||||
const original_holding_item = this.player_inventory.holding_item;
|
||||
this.player_inventory.holding_item = container_slot.get_item();
|
||||
container_slot.set_item(original_holding_item);
|
||||
}
|
||||
click(slot: Slot, button: number) {
|
||||
this.send({ type: "click", container: slot.container, index: slot.index, button });
|
||||
|
||||
handle_left_click(slot: Slot) {
|
||||
const holding = this.player_inventory.holding_item;
|
||||
const inventory_slot = slot.inventory.container.get_slot(slot.index);
|
||||
const slot_item = inventory_slot.get_item();
|
||||
|
||||
// if you aren't holding anything
|
||||
// "swap" with nothing on your hand (pick it up)
|
||||
if (!holding) {
|
||||
this.switch_item_with_holding(slot.inventory, slot.index);
|
||||
return;
|
||||
}
|
||||
|
||||
// if you are holding something and slot type equals holding type
|
||||
// try to add to stack
|
||||
if (slot_item && inventory_slot.type_id === holding.type_id) {
|
||||
const space_left = inventory_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
|
||||
this.switch_item_with_holding(slot.inventory, slot.index);
|
||||
}
|
||||
|
||||
handle_right_click(slot: Slot) {
|
||||
const holding = this.player_inventory.holding_item;
|
||||
const inventory_slot = slot.inventory.container.get_slot(slot.index);
|
||||
const slot_item = inventory_slot.get_item();
|
||||
|
||||
// if holding something
|
||||
if (holding) {
|
||||
// and slot type equals holding type
|
||||
// add 1 to matching stack
|
||||
if (slot_item && inventory_slot.type_id === holding.type_id) {
|
||||
if (slot_item.amount < slot_item.max_amount!) {
|
||||
slot_item.amount += 1;
|
||||
holding.amount -= 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// (actually the same as the last one but creates a new one techinically)
|
||||
// place 1 into empty slot
|
||||
if (!slot_item) {
|
||||
const new_item = holding.clone();
|
||||
new_item.amount = 1;
|
||||
inventory_slot.set_item(new_item);
|
||||
holding.amount -= 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// if something on hand but not the same
|
||||
// swap
|
||||
this.switch_item_with_holding(slot.inventory, slot.index);
|
||||
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);
|
||||
|
||||
slot_item.amount = half;
|
||||
|
||||
const picked_up = slot_item.clone();
|
||||
picked_up.amount = original_amount - half;
|
||||
|
||||
this.player_inventory.holding_item = picked_up;
|
||||
|
||||
if (slot_item.amount === 0) {
|
||||
inventory_slot.set_item(undefined);
|
||||
// output slots depend on server side things like recipes, just wait for the server
|
||||
const container = this.get_container(slot.container);
|
||||
if (container && !slot.output) {
|
||||
click_slot(container, slot.index, this.inventories.cursor, button);
|
||||
}
|
||||
}
|
||||
|
||||
/*handle_hotbar(world: ClientWorld, player_inventory: PlayerInventory) {
|
||||
const [player, player_hand] = world.get_tag("player")!;
|
||||
|
||||
const player_position = player.get(Position);
|
||||
const player_sprite = player.get(AnimatedSprite);
|
||||
const hand_position = player_hand.get(Position);
|
||||
const hand_sprite = player_hand.get(Sprite);
|
||||
|
||||
if (!player_position || !hand_position || !hand_sprite || !player_sprite) {
|
||||
return;
|
||||
}
|
||||
|
||||
// sync position
|
||||
hand_position.x = player_position.x + (player_sprite.flip_x ? 34 : 4);
|
||||
hand_position.y = player_position.y + 34;
|
||||
hand_sprite.flip_x = player_sprite.flip_x;
|
||||
|
||||
const maybe_item = player_inventory.container.get_item(player_inventory.hotbar_selected);
|
||||
if (maybe_item) {
|
||||
const region = get_sprite_region(maybe_item.type_id);
|
||||
hand_sprite.source_x = region.x * 16;
|
||||
hand_sprite.source_y = region.y * 16;
|
||||
hand_sprite.source_width = 16;
|
||||
hand_sprite.source_height = 16;
|
||||
hand_sprite.width = 32;
|
||||
hand_sprite.height = 32;
|
||||
} else {
|
||||
hand_sprite.width = 0;
|
||||
hand_sprite.height = 0;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
// generic methods that is often needed
|
||||
|
||||
export function add_player_inventory(
|
||||
handler: GuiInventoryScreen,
|
||||
player_inventory: PlayerInventory,
|
||||
offset_x: number,
|
||||
offset_y: number,
|
||||
) {
|
||||
export function add_player_inventory(handler: GuiInventoryScreen, offset_x: number, offset_y: number) {
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
for (let l = 0; l < 9; ++l) {
|
||||
handler.slots.push(
|
||||
new Slot(
|
||||
player_inventory,
|
||||
l + i * 9 + 9,
|
||||
offset_x + l * SLOT_SIZE,
|
||||
offset_y + i * SLOT_SIZE,
|
||||
),
|
||||
new Slot("inventory", l + i * 9 + 9, offset_x + l * SLOT_SIZE, offset_y + i * SLOT_SIZE),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function add_player_hotbar(
|
||||
handler: GuiInventoryScreen,
|
||||
player_inventory: PlayerInventory,
|
||||
offset_x: number,
|
||||
offset_y: number,
|
||||
) {
|
||||
export function add_player_hotbar(handler: GuiInventoryScreen, offset_x: number, offset_y: number) {
|
||||
for (let i = 0; i < 9; ++i) {
|
||||
handler.slots.push(new Slot(player_inventory, i, offset_x + i * SLOT_SIZE, offset_y));
|
||||
handler.slots.push(new Slot("inventory", i, offset_x + i * SLOT_SIZE, offset_y));
|
||||
}
|
||||
}
|
||||
|
||||
+9
-136
@@ -1,138 +1,11 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
import { Container, Cursor } from "$/common/inventory.ts";
|
||||
|
||||
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 {
|
||||
return new ItemStack(this.type_id, this.amount, this.max_amount);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
add_item(item_stack: ItemStack) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const slot of this.#slots) {
|
||||
if (!slot.has_item()) {
|
||||
slot.set_item(item_stack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// TODO: drop item on ground
|
||||
console.error("Failed to place item in container");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ContainerLayout {
|
||||
slots: { type: string; x: number; y: number }[];
|
||||
offset_x: number;
|
||||
offset_y: number;
|
||||
}
|
||||
|
||||
export class Inventory {
|
||||
container: Container;
|
||||
|
||||
hovering_slot: number = -1;
|
||||
|
||||
constructor(container: Container) {
|
||||
this.container = container;
|
||||
}
|
||||
}
|
||||
|
||||
export class PlayerInventory extends Inventory {
|
||||
owner_id: string;
|
||||
holding_item: ItemStack | undefined;
|
||||
hotbar_selected: number = 0;
|
||||
|
||||
constructor(owner_id: string) {
|
||||
const container = new Container(9 * 4);
|
||||
super(container);
|
||||
this.owner_id = owner_id;
|
||||
}
|
||||
// the local copies of the containers the server syncs to this player
|
||||
export class ClientInventories {
|
||||
inventory = new Container(9 * 4);
|
||||
crafting = new Container(10);
|
||||
// the open server screen's container
|
||||
screen: Container | undefined;
|
||||
cursor: Cursor = { item: undefined };
|
||||
hotbar_selected = 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:axe", {
|
||||
texture_id: "bworld:axe",
|
||||
tool_type: "axe",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:coal", {
|
||||
texture_id: "bworld:coal",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:copper_ingot", {
|
||||
texture_id: "bworld:copper_ingot",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:gold_ingot", {
|
||||
texture_id: "bworld:gold_ingot",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:hoe", {
|
||||
texture_id: "bworld:hoe",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:iron_ingot", {
|
||||
texture_id: "bworld:iron_ingot",
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
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";
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:pickaxe", {
|
||||
texture_id: "bworld:pickaxe",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:stick", {
|
||||
texture_id: "bworld:stick",
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:tin_ingot", {
|
||||
texture_id: "bworld:tin_ingot",
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
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}`;
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
EverythingRegistry.register<ItemRegistry>("items", "bworld:wood_pickaxe", {
|
||||
texture_id: "bworld:wood_pickaxe",
|
||||
tool_type: "pickaxe",
|
||||
});
|
||||
+14
-12
@@ -3,9 +3,10 @@ import { ClientWorld } from "./client_world.ts";
|
||||
import { InputManager } from "./input_manager.ts";
|
||||
import { Connection, get_player_name, get_server_url } from "./network.ts";
|
||||
import { begin_drawing, clear_background, end_drawing, init_font, init_window } from "./renderer/mod.ts";
|
||||
import { is_stopped, show_fatal_error } from "./fatal.ts";
|
||||
|
||||
await import("./blocks/mod.ts");
|
||||
await import("./items/mod.ts");
|
||||
await import("$/common/blocks/mod.ts");
|
||||
await import("$/common/items/mod.ts");
|
||||
|
||||
export class ClientLoop {
|
||||
running = false;
|
||||
@@ -39,7 +40,7 @@ export class ClientLoop {
|
||||
}
|
||||
|
||||
loop(time: number) {
|
||||
if (!this.running) {
|
||||
if (!this.running || is_stopped()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,17 +92,18 @@ await AssetManager.instance.load_all();
|
||||
|
||||
init_font();
|
||||
|
||||
// the game only runs against a server, it owns the world and everything in it
|
||||
const connection = await Connection.open(get_server_url(), get_player_name());
|
||||
if (connection) {
|
||||
console.log(`Connected to ${get_server_url()}`);
|
||||
|
||||
const client_world = new ClientWorld(connection);
|
||||
client_world.add_chat("Connected to the server");
|
||||
|
||||
const loop = new ClientLoop(client_world);
|
||||
loop.start();
|
||||
|
||||
console.log("Game started");
|
||||
} else {
|
||||
console.log("Couldn't reach a server, playing single player");
|
||||
show_fatal_error(`Couldn't connect to the server at ${get_server_url()}`);
|
||||
}
|
||||
|
||||
const client_world = new ClientWorld(connection);
|
||||
client_world.add_chat(connection ? "Connected to the server" : "Playing single player");
|
||||
|
||||
const loop = new ClientLoop(client_world);
|
||||
loop.start();
|
||||
|
||||
console.log("Game started");
|
||||
|
||||
+10
-6
@@ -15,18 +15,22 @@ export class Connection {
|
||||
id: string;
|
||||
seed: string;
|
||||
initial_changes: BlockChange[];
|
||||
spawn: { x: number; y: number; z: number; yaw: number; pitch: number };
|
||||
selected_slot: number;
|
||||
players = new Map<string, RemotePlayer>();
|
||||
|
||||
// handled by the network system inside the game loop, not whenever the socket feels like it
|
||||
incoming: ServerMessage[] = [];
|
||||
closed = false;
|
||||
|
||||
constructor(socket: WebSocket, id: string, seed: string, players: PlayerInfo[], changes: BlockChange[]) {
|
||||
constructor(socket: WebSocket, welcome: Extract<ServerMessage, { type: "welcome" }>) {
|
||||
this.socket = socket;
|
||||
this.id = id;
|
||||
this.seed = seed;
|
||||
this.initial_changes = changes;
|
||||
for (const player of players) {
|
||||
this.id = welcome.id;
|
||||
this.seed = welcome.seed;
|
||||
this.initial_changes = welcome.changes;
|
||||
this.spawn = welcome.spawn;
|
||||
this.selected_slot = welcome.selected_slot;
|
||||
for (const player of welcome.players) {
|
||||
this.add_player(player);
|
||||
}
|
||||
|
||||
@@ -77,7 +81,7 @@ export class Connection {
|
||||
const message: ServerMessage = JSON.parse(event.data);
|
||||
if (message.type === "welcome") {
|
||||
clearTimeout(timeout);
|
||||
resolve(new Connection(socket, message.id, message.seed, message.players, message.changes));
|
||||
resolve(new Connection(socket, message));
|
||||
}
|
||||
}, { once: true });
|
||||
|
||||
|
||||
+9
-5
@@ -2,14 +2,14 @@ import { Position } from "$/common/components/position.ts";
|
||||
import { Velocity } from "$/common/components/velocity.ts";
|
||||
import { Component, Entity } from "$/common/ecs/mod.ts";
|
||||
import { Camera } from "$/client/components/camera.ts";
|
||||
import { PlayerInventory } from "./inventory.ts";
|
||||
import { ClientInventories } from "./inventory.ts";
|
||||
import { PlayerControls } from "$/client/components/player_controls.ts";
|
||||
import { ClientWorld } from "./client_world.ts";
|
||||
import { GuiScreen } from "./gui/gui_screen.ts";
|
||||
import { CollisionCuboid } from "./components/collision.ts";
|
||||
|
||||
export class PlayerComponent extends Component {
|
||||
player_inventory = new PlayerInventory("");
|
||||
inventories = new ClientInventories();
|
||||
screens: GuiScreen[] = [];
|
||||
render_distance = 6;
|
||||
|
||||
@@ -27,12 +27,16 @@ export class PlayerComponent extends Component {
|
||||
|
||||
export function create_player(world: ClientWorld) {
|
||||
const player = new Entity("player");
|
||||
player.add(new Position(0, 100, 0));
|
||||
const spawn = world.connection.spawn;
|
||||
player.add(new Position(spawn.x, spawn.y, spawn.z));
|
||||
player.add(new Velocity(0, 0, 0));
|
||||
player.add(new PlayerControls());
|
||||
|
||||
player.add(new PlayerComponent());
|
||||
player.add(new Camera());
|
||||
const player_component = player.add(new PlayerComponent());
|
||||
player_component.inventories.hotbar_selected = world.connection.selected_slot;
|
||||
const camera = player.add(new Camera());
|
||||
camera.yaw = spawn.yaw;
|
||||
camera.pitch = spawn.pitch;
|
||||
player.add(new CollisionCuboid(0.55, 1.79, 0.55));
|
||||
|
||||
world.add_entity(player);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SpriteRegion } from "$/common/constants.ts";
|
||||
import { AssetManager } from "./assets.ts";
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { System } from "$/common/ecs/mod.ts";
|
||||
import { ClientWorld } from "../client_world.ts";
|
||||
import { Dimension } from "../components/dimension.ts";
|
||||
import { TICK_DELTA } from "$/common/constants.ts";
|
||||
import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts";
|
||||
|
||||
export class DimensionLogicSystem extends System {
|
||||
update(world: ClientWorld, delta: number): void {
|
||||
const dimension = world.dimension;
|
||||
|
||||
dimension.second_timer += delta;
|
||||
dimension.tick_timer += delta;
|
||||
|
||||
if (dimension.second_timer >= 1) {
|
||||
this.handle_second(world, dimension);
|
||||
}
|
||||
if (dimension.tick_timer >= TICK_DELTA) {
|
||||
this.handle_tick(world, dimension);
|
||||
}
|
||||
}
|
||||
handle_second(world: ClientWorld, dimension: Dimension) {
|
||||
for (const chunk of dimension.chunks.values()) {
|
||||
for (const tickable of chunk.blocks_data) {
|
||||
const tile_info = EverythingRegistry.get<BlockRegistry>("blocks", tickable.id);
|
||||
if (tile_info && tile_info.on_second) {
|
||||
tile_info.on_second(world.dimension, tickable, dimension.second_timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
dimension.second_timer = 0;
|
||||
}
|
||||
|
||||
handle_tick(world: ClientWorld, dimension: Dimension) {
|
||||
for (const chunk of dimension.chunks.values()) {
|
||||
for (const tickable of chunk.blocks_data) {
|
||||
const tile_info = EverythingRegistry.get<BlockRegistry>("blocks", tickable.id);
|
||||
if (tile_info && tile_info.on_tick) {
|
||||
tile_info.on_tick(world.dimension, tickable, dimension.tick_timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
dimension.tick_timer = 0;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import { System } from "$/common/ecs/mod.ts";
|
||||
import { Position } from "$/common/components/position.ts";
|
||||
import { ClientWorld } from "../client_world.ts";
|
||||
import { Camera } from "../components/camera.ts";
|
||||
import { Container, ItemStack } from "$/common/inventory.ts";
|
||||
import { PlayerComponent } from "../player.ts";
|
||||
import { GuiContainer } from "../gui/gui_container.ts";
|
||||
import { show_fatal_error } from "../fatal.ts";
|
||||
|
||||
const MOVE_SEND_INTERVAL = 1 / 10;
|
||||
const REMOTE_PLAYER_SMOOTHING = 12;
|
||||
@@ -11,9 +15,9 @@ export class NetworkSystem extends System {
|
||||
|
||||
update(world: ClientWorld, delta: number): void {
|
||||
const connection = world.connection;
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
const [local_player] = world.get_tag("player")!;
|
||||
const player_component = local_player.get(PlayerComponent)!;
|
||||
const inventories = player_component.inventories;
|
||||
|
||||
for (const message of connection.incoming) {
|
||||
switch (message.type) {
|
||||
@@ -41,13 +45,47 @@ export class NetworkSystem extends System {
|
||||
case "chat":
|
||||
world.add_chat(message.from ? `<${message.from}> ${message.text}` : message.text);
|
||||
break;
|
||||
case "container": {
|
||||
let container = message.container === "screen"
|
||||
? inventories.screen
|
||||
: inventories[message.container];
|
||||
if (message.container === "screen" && container?.size !== message.items.length) {
|
||||
container = inventories.screen = new Container(message.items.length);
|
||||
}
|
||||
container?.load(message.items);
|
||||
break;
|
||||
}
|
||||
case "cursor":
|
||||
inventories.cursor.item = message.item ? ItemStack.from_data(message.item) : undefined;
|
||||
break;
|
||||
case "open_screen": {
|
||||
// replace anything open locally without telling the server, it just opened this one
|
||||
player_component.screens.length = 0;
|
||||
const size = Math.max(0, ...message.layout.slots.map((slot) => slot.index + 1));
|
||||
inventories.screen = new Container(size);
|
||||
player_component.screens.push(
|
||||
new GuiContainer(inventories, (m) => connection.send(m), message.layout, message.properties),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "screen_properties": {
|
||||
const screen = player_component.screens.at(-1);
|
||||
if (screen instanceof GuiContainer) {
|
||||
screen.properties = message.properties;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "close_screen":
|
||||
// closed by the server (the block broke), it already put everything back
|
||||
player_component.screens = player_component.screens.filter((s) => !(s instanceof GuiContainer));
|
||||
inventories.screen = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
connection.incoming.length = 0;
|
||||
|
||||
if (connection.closed) {
|
||||
world.add_chat("Lost connection to the server");
|
||||
world.connection = undefined;
|
||||
show_fatal_error("Lost connection to the server");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import { PlayerComponent } from "../player.ts";
|
||||
import { GuiPlayerInventory } from "../gui/gui_player_inventory.ts";
|
||||
import { CollisionCuboid } from "../components/collision.ts";
|
||||
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
import { AIR, FACE_OFFSETS } from "$/common/constants.ts";
|
||||
import { ClientMessage } from "$/common/protocol.ts";
|
||||
import { GuiChat } from "../gui/gui_chat.ts";
|
||||
import { GuiInventoryScreen } from "../gui/gui_screen.ts";
|
||||
|
||||
@@ -24,6 +26,7 @@ export class PlayerControlsSystem extends System {
|
||||
const player_component = player.get(PlayerComponent)!;
|
||||
const position = player.get(Position)!;
|
||||
const camera = player.get(Camera)!;
|
||||
const send = (message: ClientMessage) => world.connection.send(message);
|
||||
|
||||
if (player_component.screens.length === 0) {
|
||||
let input_x = 0;
|
||||
@@ -71,7 +74,7 @@ export class PlayerControlsSystem extends System {
|
||||
|
||||
if (InputManager.is_key_pressed(controls.open_inventory)) {
|
||||
if (player_component.screens.length === 0) {
|
||||
player_component.screens.push(new GuiPlayerInventory(player_component.player_inventory));
|
||||
player_component.screens.push(new GuiPlayerInventory(player_component.inventories, send));
|
||||
} else if (player_component.screens.at(-1) instanceof GuiInventoryScreen) {
|
||||
player_component.pop_screen();
|
||||
}
|
||||
@@ -119,10 +122,9 @@ export class PlayerControlsSystem extends System {
|
||||
}
|
||||
|
||||
const block = world.dimension.get_looked_block(world.dimension, camera);
|
||||
const holding_item = player_component.player_inventory.container.get_item(
|
||||
player_component.player_inventory.hotbar_selected,
|
||||
);
|
||||
const holding_item_info = EverythingRegistry.get<ItemRegistry>("items", holding_item?.type_id ?? "");
|
||||
const inventories = player_component.inventories;
|
||||
const hotbar_slot = inventories.inventory.get_slot(inventories.hotbar_selected);
|
||||
const holding_item_info = EverythingRegistry.get<ItemRegistry>("items", hotbar_slot.type_id ?? "");
|
||||
|
||||
if (block && player_component.screens.length === 0) {
|
||||
const block_info = EverythingRegistry.get_by_id<BlockRegistry>("blocks", block.block)!;
|
||||
@@ -135,47 +137,24 @@ export class PlayerControlsSystem extends System {
|
||||
}
|
||||
player_component.break_progress += delta * multiplier;
|
||||
if (player_component.break_progress >= player_component.break_progress_max) {
|
||||
const drop_item = !block_info.requires_tool ||
|
||||
holding_item_info?.tool_type === block_info.tool_to_break;
|
||||
world.dimension.break_block(block.x, block.y, block.z, drop_item);
|
||||
world.dimension.sync_block(block.x, block.y, block.z);
|
||||
// show it right away, the server decides drops and corrects us if it disagrees
|
||||
world.dimension.break_block(block.x, block.y, block.z);
|
||||
send({ type: "break_block", x: block.x, y: block.y, z: block.z });
|
||||
player_component.break_progress_max = 0;
|
||||
player_component.break_progress = 0;
|
||||
}
|
||||
} else if (InputManager.is_mouse_pressed(2)) {
|
||||
// interact if possible
|
||||
const interacted = block_info?.on_interact?.(world.dimension, {
|
||||
x: block.x,
|
||||
y: block.y,
|
||||
z: block.z,
|
||||
id: block_info.id,
|
||||
});
|
||||
if (!interacted) {
|
||||
const inventory = player_component.player_inventory;
|
||||
const hotbar_slot = inventory.container.get_slot(inventory.hotbar_selected);
|
||||
if (hotbar_slot && hotbar_slot.has_item()) {
|
||||
const item = hotbar_slot.get_item()!;
|
||||
const item_info = EverythingRegistry.get<ItemRegistry>("items", item.type_id);
|
||||
if (item_info && item_info.block_id) {
|
||||
const FACE_OFFSETS = {
|
||||
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 },
|
||||
};
|
||||
const offset = FACE_OFFSETS[block.face];
|
||||
world.dimension.add_block({
|
||||
x: block.x + offset.x,
|
||||
y: block.y + offset.y,
|
||||
z: block.z + offset.z,
|
||||
id: item_info.block_id,
|
||||
});
|
||||
world.dimension.sync_block(block.x + offset.x, block.y + offset.y, block.z + offset.z);
|
||||
hotbar_slot.amount! -= 1;
|
||||
}
|
||||
}
|
||||
send({ type: "use_block", x: block.x, y: block.y, z: block.z, face: block.face });
|
||||
|
||||
// guess that it places the held block, unless the block does something when used
|
||||
const offset = FACE_OFFSETS[block.face];
|
||||
const target = { x: block.x + offset.x, y: block.y + offset.y, z: block.z + offset.z };
|
||||
const target_id = world.dimension.get_block(target.x, target.y, target.z);
|
||||
const replaceable = target_id === AIR ||
|
||||
EverythingRegistry.get_by_id<BlockRegistry>("blocks", target_id)?.id === "bworld:water";
|
||||
if (!block_info.interactive && holding_item_info?.block_id && replaceable) {
|
||||
world.dimension.add_block({ ...target, id: holding_item_info.block_id });
|
||||
hotbar_slot.amount = hotbar_slot.amount! - 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -185,32 +164,32 @@ export class PlayerControlsSystem extends System {
|
||||
}
|
||||
|
||||
if (player_component.screens.length === 0) {
|
||||
const player_inventory = player_component.player_inventory;
|
||||
const previous = inventories.hotbar_selected;
|
||||
const scroll = InputManager.get_wheel_delta();
|
||||
if (scroll > 0) {
|
||||
player_inventory.hotbar_selected = Math.min(8, player_inventory.hotbar_selected + 1);
|
||||
inventories.hotbar_selected = Math.min(8, inventories.hotbar_selected + 1);
|
||||
} else if (scroll < 0) {
|
||||
player_inventory.hotbar_selected = Math.max(0, player_inventory.hotbar_selected - 1);
|
||||
inventories.hotbar_selected = Math.max(0, inventories.hotbar_selected - 1);
|
||||
}
|
||||
|
||||
if (InputManager.is_key_pressed(controls.hotbar_1)) {
|
||||
player_inventory.hotbar_selected = 0;
|
||||
} else if (InputManager.is_key_pressed(controls.hotbar_2)) {
|
||||
player_inventory.hotbar_selected = 1;
|
||||
} else if (InputManager.is_key_pressed(controls.hotbar_3)) {
|
||||
player_inventory.hotbar_selected = 2;
|
||||
} else if (InputManager.is_key_pressed(controls.hotbar_4)) {
|
||||
player_inventory.hotbar_selected = 3;
|
||||
} else if (InputManager.is_key_pressed(controls.hotbar_5)) {
|
||||
player_inventory.hotbar_selected = 4;
|
||||
} else if (InputManager.is_key_pressed(controls.hotbar_6)) {
|
||||
player_inventory.hotbar_selected = 5;
|
||||
} else if (InputManager.is_key_pressed(controls.hotbar_7)) {
|
||||
player_inventory.hotbar_selected = 6;
|
||||
} else if (InputManager.is_key_pressed(controls.hotbar_8)) {
|
||||
player_inventory.hotbar_selected = 7;
|
||||
} else if (InputManager.is_key_pressed(controls.hotbar_9)) {
|
||||
player_inventory.hotbar_selected = 8;
|
||||
const hotbar_keys = [
|
||||
controls.hotbar_1,
|
||||
controls.hotbar_2,
|
||||
controls.hotbar_3,
|
||||
controls.hotbar_4,
|
||||
controls.hotbar_5,
|
||||
controls.hotbar_6,
|
||||
controls.hotbar_7,
|
||||
controls.hotbar_8,
|
||||
controls.hotbar_9,
|
||||
];
|
||||
const pressed = hotbar_keys.findIndex((key) => InputManager.is_key_pressed(key));
|
||||
if (pressed !== -1) {
|
||||
inventories.hotbar_selected = pressed;
|
||||
}
|
||||
|
||||
if (inventories.hotbar_selected !== previous) {
|
||||
send({ type: "select_slot", slot: inventories.hotbar_selected });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class RenderSystem extends System {
|
||||
|
||||
const player_component = entity.get(PlayerComponent);
|
||||
if (player_component) {
|
||||
render_player_hotbar(player_component.player_inventory);
|
||||
render_player_hotbar(player_component.inventories);
|
||||
render_player_crosshair();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SLOT_SIZE, TEXTURE_SIZE } from "$/common/constants.ts";
|
||||
import { AssetManager } from "$/client/assets.ts";
|
||||
import { PlayerInventory } from "../../inventory.ts";
|
||||
import { ClientInventories } from "../../inventory.ts";
|
||||
import { draw_item, draw_nine_slice } from "./render_utils.ts";
|
||||
import {
|
||||
canvas,
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
Texture,
|
||||
} from "$/client/renderer/mod.ts";
|
||||
import { PlayerComponent } from "../../player.ts";
|
||||
import { get_sprite_region } from "../../../common/utils.ts";
|
||||
import { get_sprite_region } from "$/client/sprites.ts";
|
||||
|
||||
const PADDING = 10;
|
||||
|
||||
export function render_player_hotbar(player_inventory: PlayerInventory) {
|
||||
export function render_player_hotbar(inventories: ClientInventories) {
|
||||
const ui = AssetManager.instance.get<Texture>("bworld:ui");
|
||||
|
||||
const hotbar_width = PADDING * 2 + SLOT_SIZE * 9;
|
||||
@@ -46,8 +46,8 @@ export function render_player_hotbar(player_inventory: PlayerInventory) {
|
||||
for (let index = 0; index < 9; index += 1) {
|
||||
draw_nine_slice(
|
||||
ui,
|
||||
player_inventory.hotbar_selected === index ? 19 * 16 : 160 + 32,
|
||||
player_inventory.hotbar_selected === index ? 16 : 0,
|
||||
inventories.hotbar_selected === index ? 19 * 16 : 160 + 32,
|
||||
inventories.hotbar_selected === index ? 16 : 0,
|
||||
16,
|
||||
16,
|
||||
4,
|
||||
@@ -62,7 +62,7 @@ export function render_player_hotbar(player_inventory: PlayerInventory) {
|
||||
}
|
||||
|
||||
for (let index = 0; index < 9; index += 1) {
|
||||
const item = player_inventory.container.get_item(index);
|
||||
const item = inventories.inventory.get_item(index);
|
||||
if (item) {
|
||||
draw_item(item, x + PADDING + index * SLOT_SIZE, y + PADDING);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SLOT_SIZE, TEXTURE_SIZE } from "$/common/constants.ts";
|
||||
import { get_sprite_region } from "$/common/utils.ts";
|
||||
import { get_sprite_region } from "$/client/sprites.ts";
|
||||
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
import { AssetManager } from "$/client/assets.ts";
|
||||
import { ItemStack } from "../../inventory.ts";
|
||||
import { ItemStack } from "$/common/inventory.ts";
|
||||
import { draw_text, draw_texture_region, draw_texture_region_skewed, Texture } from "$/client/renderer/mod.ts";
|
||||
|
||||
export function draw_nine_slice(
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { BlockRegistry } from "$/common/everything_registry.ts";
|
||||
import type { SpriteRegion } from "$/common/constants.ts";
|
||||
import type { Texture } from "../renderer/types.ts";
|
||||
import type { FromChunkWorker, ToChunkWorker } from "./chunk_messages.ts";
|
||||
import { generate_chunk } from "../generation.ts";
|
||||
import { generate_raw_chunk } from "$/common/generation.ts";
|
||||
|
||||
const pad = 0.5;
|
||||
|
||||
@@ -311,37 +311,8 @@ function post(message: FromChunkWorker, transfer: Transferable[]) {
|
||||
}
|
||||
|
||||
function generate(chunk_x: number, chunk_z: number, seed: string) {
|
||||
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,
|
||||
);
|
||||
|
||||
const spills_array = new Int32Array(spills);
|
||||
post({ type: "generated", chunk_x, chunk_z, blocks, spills: spills_array }, [
|
||||
blocks.buffer,
|
||||
spills_array.buffer,
|
||||
]);
|
||||
const { blocks, spills } = generate_raw_chunk(chunk_x, chunk_z, seed, block_ids);
|
||||
post({ type: "generated", chunk_x, chunk_z, blocks, spills }, [blocks.buffer, spills.buffer]);
|
||||
}
|
||||
|
||||
function make_chunk_mesh(
|
||||
|
||||
Reference in New Issue
Block a user