This commit is contained in:
2026-09-26 18:06:05 -03:00
parent 01a81b926e
commit e22f12c0b6
12 changed files with 215 additions and 35 deletions
+4
View File
@@ -99,6 +99,8 @@ export function block_light_emission(block: BlockRegistry | undefined): number {
export interface BlockRegistry {
id: string;
// what players see, like "Iron Ore". made from the id when not set, see display_name in common/utils.ts
name?: string;
textures: BlockTextures;
// the block model, engine:cube when not set. see common/block_models.ts
model?: string;
@@ -135,6 +137,8 @@ export interface BlockRegistry {
export interface ItemRegistry<T = unknown | undefined> {
texture_id: string | ((item: ItemStack<T>) => string);
// what players see, like "Iron Ingot". made from the id when not set, see display_name in common/utils.ts
name?: string;
block_id?: string;
tool_type?: string;
max_stack?: number;
+23
View File
@@ -5,6 +5,8 @@ export interface ItemData {
id: string;
count: number;
data?: unknown;
// what server scripts say about it (get_lore), only sent to players, never saved
lore?: string;
}
export class ItemStack<T = unknown | undefined> {
@@ -12,6 +14,8 @@ export class ItemStack<T = unknown | undefined> {
amount: number;
max_amount: number;
data?: T;
// on clients, the lore the server's scripts gave it when it was last synced
lore?: string;
constructor(type_id: string | string, amount: number = 1, max_amount?: number) {
const item_info = EverythingRegistry.get<ItemRegistry>("items", type_id);
@@ -27,6 +31,7 @@ export class ItemStack<T = unknown | undefined> {
clone(): ItemStack {
const item = new ItemStack(this.type_id, this.amount, this.max_amount);
item.data = structuredClone(this.data);
item.lore = this.lore;
return item;
}
@@ -36,11 +41,24 @@ export class ItemStack<T = unknown | undefined> {
: { id: this.type_id, count: this.amount, data: this.data };
}
// what players are sent: to_data with the lore from the item's get_lore, which only runs on the server
to_synced_data(): ItemData {
const data = this.to_data();
const lore = EverythingRegistry.get<ItemRegistry>("items", this.type_id)?.get_lore?.(this);
if (lore) {
data.lore = lore;
}
return data;
}
static from_data(data: ItemData): ItemStack {
const item = new ItemStack(data.id, data.count);
if (data.data !== undefined) {
item.data = data.data;
}
if (data.lore !== undefined) {
item.lore = data.lore;
}
return item;
}
}
@@ -140,6 +158,11 @@ export class Container {
return this.#slots.map((slot) => slot.get_item()?.to_data() ?? null);
}
// what players are sent, with lore, see ItemStack.to_synced_data
to_synced_data(): (ItemData | null)[] {
return this.#slots.map((slot) => slot.get_item()?.to_synced_data() ?? null);
}
load(data: (ItemData | null)[]) {
for (let i = 0; i < this.size; i += 1) {
const item = data[i];
+13
View File
@@ -31,6 +31,7 @@ export interface ManifestJson {
export interface BlockJson {
id: string;
name?: string;
textures: BlockTextures;
model?: string;
variants?: Record<string, BlockVariant>;
@@ -66,6 +67,7 @@ export interface ItemJson {
tool?: string;
places?: string;
max_stack?: number;
name?: string;
lore?: string;
components?: Record<string, unknown>;
}
@@ -100,6 +102,7 @@ export interface GridRecipe {
export function block_to_json(block: BlockRegistry, has_item: boolean): BlockJson {
const json: BlockJson = { id: block.id, textures: block.textures };
if (block.name !== undefined) json.name = block.name;
if (block.model !== undefined) json.model = block.model;
if (block.variants !== undefined) json.variants = block.variants;
if (block.render_layer && block.render_layer !== "solid") json.render_layer = block.render_layer;
@@ -128,6 +131,7 @@ export function block_from_json(json: BlockJson): { block: BlockRegistry; has_it
textures: json.textures,
has_collision: json.collision ?? true,
};
if (json.name !== undefined) block.name = json.name;
if (json.model !== undefined) block.model = json.model;
if (json.variants !== undefined) block.variants = json.variants;
const render_layer = json.render_layer ?? (json.transparent ? "translucent" : undefined);
@@ -159,6 +163,7 @@ export function item_to_json(id: string, item: ItemRegistry): ItemJson {
if (item.tool_type !== undefined) json.tool = item.tool_type;
if (item.block_id !== undefined) json.places = item.block_id;
if (item.max_stack !== undefined) json.max_stack = item.max_stack;
if (item.name !== undefined) json.name = item.name;
if (item.lore !== undefined) json.lore = item.lore;
if (item.components) json.components = item.components;
return json;
@@ -169,6 +174,7 @@ export function item_from_json(json: ItemJson): ItemRegistry {
if (json.tool !== undefined) item.tool_type = json.tool;
if (json.places !== undefined) item.block_id = json.places;
if (json.max_stack !== undefined) item.max_stack = json.max_stack;
if (json.name !== undefined) item.name = json.name;
if (json.lore !== undefined) item.lore = json.lore;
if (json.components) item.components = json.components;
return item;
@@ -318,6 +324,7 @@ export function validate_block(json: unknown): Problems {
problems.push(`${key} must be a whole number from 0 to 15`);
}
}
problems.push(...validate_text(json.name, "name"));
for (const key of ["cull_same", "transparent", "collision", "item", "interactive", "replaceable"]) {
if (json[key] !== undefined && typeof json[key] !== "boolean") problems.push(`${key} must be true or false`);
}
@@ -413,6 +420,7 @@ export function validate_item(json: unknown): Problems {
const problems = validate_id(json.id, "id");
problems.push(...validate_id(json.texture, "texture"));
if (json.places !== undefined) problems.push(...validate_id(json.places, "places"));
problems.push(...validate_text(json.name, "name"), ...validate_text(json.lore, "lore"));
if (json.max_stack !== undefined && (!Number.isInteger(json.max_stack) || (json.max_stack as number) < 1)) {
problems.push("max_stack must be a positive whole number");
}
@@ -481,6 +489,11 @@ export function validate_ore(json: unknown): Problems {
return problems;
}
// optional text players see
function validate_text(value: unknown, field: string): Problems {
return value === undefined || (typeof value === "string" && value.length > 0) ? [] : [`${field} must be text`];
}
function validate_id(value: unknown, field: string): Problems {
return typeof value === "string" && ID_PATTERN.test(value) ? [] : [`${field} must be an id like "my_mod:thing"`];
}
+11 -4
View File
@@ -41,10 +41,17 @@ export function distance_point_point(ax: number, ay: number, az: number, bx: num
export function register_block_item(block: BlockRegistry) {
// cubes are drawn from the block's textures, anything else as a sprite
const texture_id = block_item_texture(block) ?? cube_face_texture(block, "side");
EverythingRegistry.register<ItemRegistry>("items", block.id, {
texture_id,
block_id: block.id,
});
const item: ItemRegistry = { texture_id, block_id: block.id };
if (block.name !== undefined) item.name = block.name;
EverythingRegistry.register<ItemRegistry>("items", block.id, item);
}
// what players see an item called: its name, or one made from its id, so bworld:iron_ingot is "Iron Ingot"
export function display_name(item_id: string): string {
const name = EverythingRegistry.get<ItemRegistry>("items", item_id)?.name;
if (name) return name;
const words = (item_id.split(":")[1] ?? item_id).split("_").filter((word) => word.length > 0);
return words.map((word) => word[0].toUpperCase() + word.slice(1)).join(" ");
}
export function get_state_value(value: number, block_info: BlockRegistry, name: string) {