new help section explaining classes and their mechanics
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import sharp from "sharp";
|
||||
import { createGameAssetIO } from "./lib/ktx2.mjs";
|
||||
|
||||
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const sourcePath = path.join(repositoryRoot, "game_assets/models/claudecraft/chars/players/mage.glb");
|
||||
const outputPath = path.join(repositoryRoot, "game_assets/textures/claudecraft/chars/players/priest-vestments.webp");
|
||||
|
||||
function clamp01(value) {
|
||||
return Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
function luminance(red, green, blue) {
|
||||
return (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255;
|
||||
}
|
||||
|
||||
function paintSwatch(pixels, width, channels, rectangle, shadow, highlight) {
|
||||
const samples = [];
|
||||
for (let y = rectangle.y; y < rectangle.y + rectangle.height; y += 1) {
|
||||
for (let x = rectangle.x; x < rectangle.x + rectangle.width; x += 1) {
|
||||
const offset = (y * width + x) * channels;
|
||||
if (pixels[offset + 3] === 0) continue;
|
||||
samples.push(luminance(pixels[offset], pixels[offset + 1], pixels[offset + 2]));
|
||||
}
|
||||
}
|
||||
const minimum = Math.min(...samples);
|
||||
const maximum = Math.max(...samples);
|
||||
const range = Math.max(0.001, maximum - minimum);
|
||||
|
||||
for (let y = rectangle.y; y < rectangle.y + rectangle.height; y += 1) {
|
||||
for (let x = rectangle.x; x < rectangle.x + rectangle.width; x += 1) {
|
||||
const offset = (y * width + x) * channels;
|
||||
if (pixels[offset + 3] === 0) continue;
|
||||
const sourceLuminance = luminance(pixels[offset], pixels[offset + 1], pixels[offset + 2]);
|
||||
const mix = clamp01((sourceLuminance - minimum) / range);
|
||||
for (let channel = 0; channel < 3; channel += 1) {
|
||||
pixels[offset + channel] = Math.round(shadow[channel] + (highlight[channel] - shadow[channel]) * mix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const io = await createGameAssetIO();
|
||||
const document = await io.read(sourcePath);
|
||||
const sourceTexture = document.getRoot().listTextures().find((texture) => texture.getName() === "mage_texture");
|
||||
if (!sourceTexture?.getImage()) throw new Error("Mage source is missing mage_texture.");
|
||||
|
||||
const decoded = await sharp(sourceTexture.getImage()).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
||||
const { width, height, channels } = decoded.info;
|
||||
if (width !== 512 || height !== 512 || channels !== 4) {
|
||||
throw new Error(`Unexpected mage palette shape: ${width}x${height}x${channels}.`);
|
||||
}
|
||||
|
||||
const pixels = new Uint8Array(decoded.data);
|
||||
paintSwatch(pixels, width, channels, { x: 0, y: 128, width: 128, height: 128 }, [48, 58, 94], [244, 239, 211]);
|
||||
paintSwatch(pixels, width, channels, { x: 128, y: 128, width: 64, height: 128 }, [105, 66, 18], [255, 218, 109]);
|
||||
paintSwatch(pixels, width, channels, { x: 64, y: 256, width: 64, height: 128 }, [105, 66, 18], [255, 218, 109]);
|
||||
paintSwatch(pixels, width, channels, { x: 128, y: 256, width: 64, height: 128 }, [37, 71, 101], [157, 215, 221]);
|
||||
|
||||
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, await sharp(pixels, { raw: { width, height, channels } })
|
||||
.webp({ quality: 92, smartSubsample: true })
|
||||
.toBuffer());
|
||||
console.log(`Built ${path.relative(repositoryRoot, outputPath)}`);
|
||||
Reference in New Issue
Block a user