Basic crops system
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { bake_model, block_variant, BUILTIN_MODELS, type ModelJson, resolve_texture } from "$/common/block_models.ts";
|
||||
import { block_from_json } from "$/common/mod_data.ts";
|
||||
import { set_state_value } from "$/common/utils.ts";
|
||||
|
||||
const cube = BUILTIN_MODELS["engine:cube"];
|
||||
|
||||
Deno.test("cubes resolve the old texture shapes", () => {
|
||||
assertEquals(resolve_texture("#top", "a:all", cube), "a:all");
|
||||
const pillar = { top: "a:top", bottom: "a:bottom", side: "a:side" };
|
||||
assertEquals(["#top", "#bottom", "#front", "#side"].map((t) => resolve_texture(t, pillar, cube)), [
|
||||
"a:top",
|
||||
"a:bottom",
|
||||
"a:side",
|
||||
"a:side",
|
||||
]);
|
||||
const furnace = { front: "a:front", side: "a:side" };
|
||||
assertEquals(["#top", "#bottom", "#front"].map((t) => resolve_texture(t, furnace, cube)), [
|
||||
"a:side",
|
||||
"a:side",
|
||||
"a:front",
|
||||
]);
|
||||
assertEquals(resolve_texture("#nothing", { crop: "a:crop" }), "engine:missing");
|
||||
});
|
||||
|
||||
Deno.test("a cube bakes into six full faces that cull against their neighbors", () => {
|
||||
const quads = bake_model(cube, "a:all");
|
||||
assertEquals(quads.length, 6);
|
||||
assert(quads.every((q) => q.flush && q.cull === q.face && q.texture === "a:all"));
|
||||
// the top face, corners and uvs like the mesher always had
|
||||
assertEquals(quads[0].positions, [0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0]);
|
||||
assertEquals(quads[0].uvs, [0, 16, 16, 16, 16, 0, 0, 0]);
|
||||
});
|
||||
|
||||
Deno.test("crop and cross models are planes inside the block, seen from both sides", () => {
|
||||
const crop = bake_model(BUILTIN_MODELS["engine:crop"], { crop: "a:wheat" });
|
||||
assertEquals(crop.length, 8);
|
||||
assert(crop.every((q) => !q.flush && q.cull === -1 && !q.shade && q.texture === "a:wheat"));
|
||||
assertEquals(crop[0].positions.filter((_, i) => i % 3 === 0), [0.25, 0.25, 0.25, 0.25]);
|
||||
|
||||
const cross = bake_model(BUILTIN_MODELS["engine:cross"], { cross: "a:flower" });
|
||||
assertEquals(cross.length, 4);
|
||||
// rescaled so the diagonal planes stay as wide as the block, like minecraft's (0.8 to 15.2 pixels)
|
||||
for (const quad of cross) {
|
||||
for (const v of quad.positions) assert(v > -1e-6 && v < 1 + 1e-6, `${v} is outside the block`);
|
||||
}
|
||||
const xs = cross[0].positions.filter((_, i) => i % 3 === 0).map((v) => Math.round(v * 1000) / 1000);
|
||||
assertEquals(new Set(xs), new Set([0.05, 0.95]));
|
||||
});
|
||||
|
||||
Deno.test("turning a model moves its faces and cullfaces", () => {
|
||||
const model: ModelJson = {
|
||||
id: "a:half",
|
||||
elements: [{
|
||||
from: [0, 0, 0],
|
||||
to: [16, 16, 8],
|
||||
faces: { north: { texture: "a:x", cullface: "north" } },
|
||||
}],
|
||||
};
|
||||
const [north] = bake_model(model, undefined, 0);
|
||||
const [east] = bake_model(model, undefined, 90);
|
||||
assertEquals([north.face, north.cull], [3, 3]);
|
||||
assertEquals([east.face, east.cull], [5, 5]);
|
||||
assert(east.flush);
|
||||
});
|
||||
|
||||
Deno.test("variants pick textures by state", () => {
|
||||
const { block } = block_from_json(JSON.parse(Deno.readTextFileSync("mods/bworld/blocks/wheat.json")).block);
|
||||
assertEquals(block_variant(block, { age: 5 }).textures, { crop: "bworld:wheat_stage_5" });
|
||||
assertEquals(block_variant(block, { age: 5 }).model, "engine:crop");
|
||||
assertEquals(block_variant(block).textures, { crop: "bworld:wheat_stage_0" });
|
||||
// states live above the numeric id
|
||||
assertEquals(set_state_value(7, block, "age", 3), (3 << 16) | 7);
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { AIR, CHUNK_HEIGHT } from "$/common/constants.ts";
|
||||
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
import { block_from_json, block_to_json, item_from_json, item_to_json } from "$/common/mod_data.ts";
|
||||
import { generate_raw_chunk } from "$/common/generation.ts";
|
||||
import { get_state_value } from "$/common/utils.ts";
|
||||
import { load_worldgen } from "$/common/worldgen_loader.ts";
|
||||
import { create_mod } from "$/tools/new_mod.ts";
|
||||
import { PROTOCOL_VERSION } from "$/common/protocol.ts";
|
||||
@@ -23,9 +24,9 @@ const inventory_of = (messages: Msg[]) =>
|
||||
|
||||
Deno.test("the base game loads from mods/bworld", async () => {
|
||||
const { game } = await test_game("mods");
|
||||
assertEquals(EverythingRegistry.entries("blocks").length, 19);
|
||||
// 11 items plus the item forms of the 16 blocks that have one
|
||||
assertEquals(EverythingRegistry.entries("items").length, 27);
|
||||
assertEquals(EverythingRegistry.entries("blocks").length, 20);
|
||||
// 14 items plus the item forms of the 17 blocks that have one
|
||||
assertEquals(EverythingRegistry.entries("items").length, 31);
|
||||
assertEquals(game.recipes.shaped.length, 5);
|
||||
assertEquals(game.recipes.furnace.size, 6);
|
||||
assertEquals(game.recipes.fuel.size, 2);
|
||||
@@ -34,6 +35,37 @@ Deno.test("the base game loads from mods/bworld", async () => {
|
||||
assert(EverythingRegistry.get<ItemRegistry>("items", "bworld:watering_can")?.on_create);
|
||||
});
|
||||
|
||||
Deno.test("bone meal grows wheat until it's fully grown", async () => {
|
||||
const { game, take, send } = await test_game("mods");
|
||||
game.on_connect(1);
|
||||
send(1, { type: "hello", name: "alice", protocol: PROTOCOL_VERSION });
|
||||
send(1, { type: "ready" });
|
||||
|
||||
let y = CHUNK_HEIGHT - 1;
|
||||
while (game.world.get_block_nid(1, y, 1) === AIR) y--;
|
||||
y += 1;
|
||||
game.set_block(1, y, 1, "bworld:wheat");
|
||||
const wheat = EverythingRegistry.get<BlockRegistry>("blocks", "bworld:wheat")!;
|
||||
const age = () => get_state_value(game.world.get_block_value(1, y, 1), wheat, "age")!;
|
||||
assertEquals(age(), 0);
|
||||
|
||||
send(1, { type: "chat", text: "/give bworld:bone_meal 8" });
|
||||
const slot = inventory_of(take(1)).findIndex((i: Msg) => i?.id === "bworld:bone_meal");
|
||||
send(1, { type: "select_slot", slot });
|
||||
send(1, { type: "move", x: 2.5, y, z: 2.5, yaw: 0, pitch: 0 });
|
||||
|
||||
// 2 to 5 stages at a time
|
||||
send(1, { type: "use_block", x: 1, y, z: 1, face: "top" });
|
||||
assert(age() >= 2 && age() <= 5, `age ${age()}`);
|
||||
// at most 3 more uses to reach 7, then it stops using bone meal
|
||||
for (let i = 0; i < 5; i++) send(1, { type: "use_block", x: 1, y, z: 1, face: "top" });
|
||||
assertEquals(age(), 7);
|
||||
const left = inventory_of(take(1))[slot]?.count;
|
||||
assert(left >= 4 && left <= 6, `${left} bone meal left`);
|
||||
// nothing got placed on top
|
||||
assertEquals(game.world.get_block_id(1, y + 1, 1), "bworld:air");
|
||||
});
|
||||
|
||||
Deno.test("mods/bworld json survives going to the registry and back", async () => {
|
||||
await test_game("mods");
|
||||
for (const file of Deno.readDirSync("mods/bworld/blocks")) {
|
||||
|
||||
@@ -65,6 +65,20 @@ Deno.test("check-mods finds broken mods", async () => {
|
||||
format_version: 1,
|
||||
block: { id: "broken:x", textures: "bworld:stone" },
|
||||
});
|
||||
// a model that doesn't exist, a variant for a state the block doesn't have, and a broken model
|
||||
write_json(`${mod}/blocks/plant.json`, {
|
||||
format_version: 1,
|
||||
block: { id: "broken:plant", textures: { cross: "broken:plant" }, model: "broken:no_such_model" },
|
||||
});
|
||||
write_json(`${mod}/blocks/grows.json`, {
|
||||
format_version: 1,
|
||||
block: { id: "broken:grows", textures: "broken:plant", variants: { "size=1": { y: 90 } } },
|
||||
});
|
||||
Deno.mkdirSync(`${mod}/models`);
|
||||
write_json(`${mod}/models/bad.json`, {
|
||||
format_version: 1,
|
||||
model: { id: "broken:bad", elements: [{ from: [0, 0], to: [16, 16, 16], faces: { up: { texture: "#all" } } }] },
|
||||
});
|
||||
// not json
|
||||
Deno.writeTextFileSync(`${mod}/items/bad.json`, "{ nope");
|
||||
// uses a letter the key doesn't define
|
||||
@@ -93,6 +107,10 @@ Deno.test("check-mods finds broken mods", async () => {
|
||||
has(report.errors, "item broken:no_such_item doesn't exist");
|
||||
has(report.errors, "block broken:x is also defined by broken, broken");
|
||||
has(report.errors, "items/bad.json: isn't valid json");
|
||||
has(report.errors, "model broken:no_such_model doesn't exist");
|
||||
has(report.errors, `"size=1" must be a state of this block`);
|
||||
has(report.errors, "elements[0].from must be [x, y, z]");
|
||||
has(report.errors, `elements[0].faces: "up" isn't one of`);
|
||||
has(report.errors, `pattern uses "B" but key doesn't define it`);
|
||||
has(report.errors, "textures/huge.png: is");
|
||||
has(report.errors, "scripts don't typecheck");
|
||||
|
||||
Reference in New Issue
Block a user