159 lines
4.3 KiB
TypeScript
159 lines
4.3 KiB
TypeScript
import { copy } from "@std/fs";
|
|
import { createCanvas, loadImage } from "@gfx/canvas-wasm";
|
|
|
|
const BUILD_FOLDER = "build";
|
|
|
|
function clear_folder() {
|
|
Deno.removeSync(BUILD_FOLDER, { recursive: true });
|
|
Deno.mkdirSync(BUILD_FOLDER);
|
|
}
|
|
|
|
async function build_fonts() {
|
|
// i used https://fonts.varg.dev/ to build the font, its confusing
|
|
const canvas_size = 256;
|
|
|
|
const font = await loadImage("assets/fonts/m6x11.png");
|
|
const canvas = createCanvas(canvas_size, canvas_size);
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
ctx.drawImage(font, 0, 0);
|
|
const image_data = ctx.getImageData(0, 0, canvas_size, canvas_size);
|
|
const data = image_data.data;
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const r = data[i];
|
|
const g = data[i + 1];
|
|
const b = data[i + 2];
|
|
|
|
if (r === 0 && g === 0 && b === 0) {
|
|
// black pixel, alpha = 0
|
|
data[i + 3] = 0;
|
|
} else {
|
|
// force white white
|
|
data[i] = 255;
|
|
data[i + 1] = 255;
|
|
data[i + 2] = 255;
|
|
data[i + 3] = 255;
|
|
}
|
|
}
|
|
ctx.putImageData(image_data, 0, 0);
|
|
|
|
Deno.mkdirSync(`${BUILD_FOLDER}/assets/fonts`);
|
|
Deno.writeFileSync(`${BUILD_FOLDER}/assets/fonts/m6x11.png`, canvas.toBuffer());
|
|
await copy("assets/fonts/m6x11.fnt", `${BUILD_FOLDER}/assets/fonts/m6x11.fnt`);
|
|
}
|
|
|
|
function next_power_of_two(value: number): number {
|
|
return Math.pow(2, Math.ceil(Math.log2(value)));
|
|
}
|
|
|
|
const SPRITE_SIZE = 16;
|
|
|
|
function calculate_atlas_size(count: number) {
|
|
const raw_sprites_per_side = Math.ceil(Math.sqrt(count));
|
|
const raw_size = raw_sprites_per_side * SPRITE_SIZE;
|
|
const size = next_power_of_two(raw_size);
|
|
|
|
return {
|
|
sprites_per_side: size / SPRITE_SIZE,
|
|
size,
|
|
};
|
|
}
|
|
|
|
async function build_atlas_from_folder(folder: string) {
|
|
let sprite_count = 0;
|
|
for (const entry of Deno.readDirSync(folder)) {
|
|
console.assert(entry.isFile && entry.name.endsWith(".png"));
|
|
sprite_count += 1;
|
|
}
|
|
|
|
const atlas = calculate_atlas_size(sprite_count);
|
|
|
|
const canvas = createCanvas(atlas.size, atlas.size);
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
const atlas_info: Record<string, { x: number; y: number }> = {};
|
|
|
|
// purple and black missing texture at x:0 y:0 wow !
|
|
ctx.fillStyle = "magenta";
|
|
ctx.fillRect(0, 0, 8, 8);
|
|
ctx.fillRect(8, 8, 8, 8);
|
|
ctx.fillStyle = "black";
|
|
ctx.fillRect(8, 0, 8, 8);
|
|
ctx.fillRect(0, 8, 8, 8);
|
|
|
|
let index = 1;
|
|
for (const entry of Deno.readDirSync(folder)) {
|
|
const sprite = await loadImage(`${folder}/${entry.name}`);
|
|
const row = Math.floor(index / atlas.sprites_per_side);
|
|
const column = index % atlas.sprites_per_side;
|
|
ctx.drawImage(sprite, column * SPRITE_SIZE, row * SPRITE_SIZE);
|
|
index += 1;
|
|
|
|
const id = `bworld:${entry.name.replace(".png", "")}`;
|
|
atlas_info[id] = { x: column, y: row };
|
|
}
|
|
|
|
Deno.writeFileSync(`${BUILD_FOLDER}/${folder}.png`, canvas.toBuffer());
|
|
Deno.writeTextFileSync(`${BUILD_FOLDER}/${folder}.json`, JSON.stringify(atlas_info));
|
|
}
|
|
|
|
async function build_sprites() {
|
|
for (const entry of Deno.readDirSync("assets/sprites")) {
|
|
if (entry.name.endsWith(".png")) {
|
|
await copy(`assets/sprites/${entry.name}`, `${BUILD_FOLDER}/assets/sprites/${entry.name}`);
|
|
} else if (entry.isDirectory) {
|
|
await build_atlas_from_folder(`assets/sprites/${entry.name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function build_assets() {
|
|
Deno.mkdirSync(`${BUILD_FOLDER}/assets`, { recursive: true });
|
|
await copy("assets/ASSETS.md", `${BUILD_FOLDER}/assets/ASSETS.md`);
|
|
|
|
build_fonts();
|
|
Deno.mkdirSync(`${BUILD_FOLDER}/assets/sprites`, { recursive: true });
|
|
await build_sprites();
|
|
}
|
|
|
|
async function build_client() {
|
|
const _result = await Deno.bundle({
|
|
entrypoints: ["./client/main.ts", "./client/workers/chunk_mesh_worker.ts"],
|
|
outputDir: `${BUILD_FOLDER}/client`,
|
|
platform: "browser",
|
|
minify: false,
|
|
keepNames: true,
|
|
});
|
|
await copy("./client/index.html", `${BUILD_FOLDER}/client/index.html`);
|
|
}
|
|
|
|
async function build() {
|
|
try {
|
|
const now = performance.now();
|
|
clear_folder();
|
|
await build_assets();
|
|
await build_client();
|
|
console.log(`Built in ${(performance.now() - now).toFixed(2)}ms`);
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
let last_build = 0;
|
|
|
|
if (import.meta.main) {
|
|
await build();
|
|
const watcher = Deno.watchFs(["assets", "client", "common"], { recursive: true });
|
|
for await (const event of watcher) {
|
|
const now = performance.now();
|
|
if (now - last_build < 500) {
|
|
continue;
|
|
}
|
|
if (["create", "modify", "rename", "remove"].includes(event.kind)) {
|
|
last_build = now;
|
|
console.log("Rebuilding...");
|
|
await build();
|
|
}
|
|
}
|
|
}
|