Basic crops system

This commit is contained in:
2026-09-25 22:49:27 -03:00
parent ef25e66f08
commit 099811670f
22 changed files with 985 additions and 178 deletions
+111 -5
View File
@@ -7,6 +7,7 @@ import {
RENDER_LAYERS,
type RenderLayer,
} from "./everything_registry.ts";
import { type BlockVariant, MODEL_FACES, type ModelFace } from "./block_models.ts";
export const FORMAT_VERSION = 1;
export const ID_PATTERN = /^[a-z0-9_]+:[a-z0-9_]+$/;
@@ -31,6 +32,8 @@ export interface ManifestJson {
export interface BlockJson {
id: string;
textures: BlockTextures;
model?: string;
variants?: Record<string, BlockVariant>;
render_layer?: RenderLayer;
cull_same?: boolean;
light_emission?: number;
@@ -89,6 +92,8 @@ 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.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;
if (block.cull_same !== undefined) json.cull_same = block.cull_same;
if (block.light_emission !== undefined) json.light_emission = block.light_emission;
@@ -115,6 +120,8 @@ export function block_from_json(json: BlockJson): { block: BlockRegistry; has_it
textures: json.textures,
has_collision: json.collision ?? true,
};
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);
if (render_layer && render_layer !== "solid") block.render_layer = render_layer;
if (json.cull_same !== undefined) block.cull_same = json.cull_same;
@@ -255,12 +262,45 @@ export function validate_manifest(json: unknown, folder_name: string): Problems
export function validate_block(json: unknown): Problems {
if (!is_object(json)) return ["block must be an object"];
const problems = validate_id(json.id, "id");
if (json.model !== undefined) problems.push(...validate_id(json.model, "model"));
const is_cube = json.model === undefined || json.model === "engine:cube";
const textures = json.textures;
const texture_ok = typeof textures === "string" ||
(is_object(textures) &&
(["top", "bottom", "side"].every((k) => typeof textures[k] === "string") ||
["front", "side"].every((k) => typeof textures[k] === "string")));
if (!texture_ok) problems.push("textures must be a texture id, { top, bottom, side } or { front, side }");
if (is_cube) {
const texture_ok = typeof textures === "string" ||
(is_object(textures) &&
(["top", "bottom", "side"].every((k) => typeof textures[k] === "string") ||
["front", "side"].every((k) => typeof textures[k] === "string")));
if (!texture_ok) problems.push("textures must be a texture id, { top, bottom, side } or { front, side }");
} else {
problems.push(...validate_textures(textures, "textures"));
}
if (json.variants !== undefined) {
if (!is_object(json.variants)) {
problems.push('variants must map conditions like "age=3" to { model, textures, y }');
} else {
const names = new Set(Array.isArray(json.states) ? json.states.map((s) => is_object(s) ? s.name : "") : []);
for (const [condition, variant] of Object.entries(json.variants)) {
const where = `variants["${condition}"]`;
for (const part of condition === "" ? [] : condition.split(",")) {
const [name, value] = part.split("=").map((p) => p.trim());
if (!names.has(name) || !/^\d+$/.test(value ?? "")) {
problems.push(`${where}: "${part}" must be a state of this block and a number, like age=3`);
}
}
if (!is_object(variant)) {
problems.push(`${where} must be { model, textures, y }`);
continue;
}
if (variant.model !== undefined) problems.push(...validate_id(variant.model, `${where}.model`));
if (variant.textures !== undefined) {
problems.push(...validate_textures(variant.textures, `${where}.textures`));
}
if (variant.y !== undefined && ![0, 90, 180, 270].includes(variant.y as number)) {
problems.push(`${where}.y must be 0, 90, 180 or 270`);
}
}
}
}
if (json.render_layer !== undefined && !RENDER_LAYERS.includes(json.render_layer as RenderLayer)) {
problems.push(`render_layer must be one of ${RENDER_LAYERS.join(", ")}`);
}
@@ -294,6 +334,72 @@ export function validate_block(json: unknown): Problems {
return problems;
}
export function validate_model(json: unknown): Problems {
if (!is_object(json)) return ["model must be an object"];
const problems = validate_id(json.id, "id");
if (json.textures !== undefined) problems.push(...validate_textures(json.textures, "textures", true));
if (!Array.isArray(json.elements) || json.elements.length === 0) {
problems.push("elements must be a list of boxes");
return problems;
}
json.elements.forEach((element, i) => {
const where = `elements[${i}]`;
if (!is_object(element)) {
problems.push(`${where} must be { from, to, faces }`);
return;
}
for (const key of ["from", "to"]) {
const point = element[key];
if (!Array.isArray(point) || point.length !== 3 || !point.every((v) => typeof v === "number")) {
problems.push(`${where}.${key} must be [x, y, z] in pixels`);
}
}
const rotation = element.rotation;
if (rotation !== undefined) {
if (
!is_object(rotation) || !["x", "y", "z"].includes(rotation.axis as string) ||
![-45, -22.5, 0, 22.5, 45].includes(rotation.angle as number) || !Array.isArray(rotation.origin)
) {
problems.push(
`${where}.rotation must be { origin, axis: x, y or z, angle: -45, -22.5, 0, 22.5 or 45 }`,
);
}
}
if (element.shade !== undefined && typeof element.shade !== "boolean") {
problems.push(`${where}.shade must be true or false`);
}
if (!is_object(element.faces)) {
problems.push(`${where}.faces must map sides to { texture }`);
return;
}
for (const [side, face] of Object.entries(element.faces)) {
if (!MODEL_FACES.includes(side as ModelFace)) {
problems.push(`${where}.faces: "${side}" isn't one of ${MODEL_FACES.join(", ")}`);
}
if (!is_object(face) || typeof face.texture !== "string") {
problems.push(`${where}.faces.${side} needs a texture, like "#side"`);
continue;
}
if (face.uv !== undefined && (!Array.isArray(face.uv) || face.uv.length !== 4)) {
problems.push(`${where}.faces.${side}.uv must be [u1, v1, u2, v2] in pixels`);
}
if (face.cullface !== undefined && !MODEL_FACES.includes(face.cullface as ModelFace)) {
problems.push(`${where}.faces.${side}.cullface isn't one of ${MODEL_FACES.join(", ")}`);
}
}
});
return problems;
}
// a texture id, or texture variables mapped to ids. a model's own defaults may also point at variables ("#side")
function validate_textures(value: unknown, field: string, allow_variables = false): Problems {
if (typeof value === "string") return validate_id(value, field);
if (!is_object(value)) return [`${field} must be a texture id or { variable: texture id }`];
return Object.entries(value).flatMap(([name, id]) =>
allow_variables && typeof id === "string" && id.startsWith("#") ? [] : validate_id(id, `${field}.${name}`)
);
}
export function validate_item(json: unknown): Problems {
if (!is_object(json)) return ["item must be an object"];
const problems = validate_id(json.id, "id");