// assets.js const jsonCache = new Map(); /** * Resolves a namespace:path ID to a file path. * Defaults to the 'minecraft' namespace if none is provided. */ export function resolveResourceLocation(id, type, ext = 'json') { const parts = id.split(':'); const namespace = parts.length > 1 ? parts[0] : 'minecraft'; const path = parts.length > 1 ? parts[1] : parts[0]; return `assets/${namespace}/${type}/${path}.${ext}`; } async function fetchJSON(url) { if (jsonCache.has(url)) return jsonCache.get(url); const res = await fetch(url); if (!res.ok) throw new Error(`Failed to load asset: ${url}`); const data = await res.json(); jsonCache.set(url, data); return data; } export async function loadBlockstate(id) { const url = resolveResourceLocation(id, 'blockstates'); return fetchJSON(url); } export async function loadModel(id) { const url = resolveResourceLocation(id, 'models'); const model = await fetchJSON(url); // Recursively resolve and merge parent models if (model.parent) { // Parents sometimes use block/ or item/ prefixes directly const parentId = model.parent.includes(':') ? model.parent : `minecraft:${model.parent}`; const parent = await loadModel(parentId); // Merge textures model.textures = {...parent.textures, ...model.textures}; // Inherit elements if child doesn't override them if (!model.elements) model.elements = parent.elements; } return model; } export class TextureAtlas { constructor(size = 512) { this.canvas = document.createElement('canvas'); this.canvas.width = size; this.canvas.height = size; this.ctx = this.canvas.getContext('2d', {willReadFrequently: true}); this.ctx.imageSmoothingEnabled = false; this.map = new Map(); this.cellSize = 16; this.x = 0; this.y = 0; this.rowHeight = 0; } async load(id) { if (this.map.has(id)) return this.map.get(id); const url = resolveResourceLocation(id, 'textures', 'png'); // Calculate position before trying to load, so we can draw a fallback if it fails if (this.x + this.cellSize > this.canvas.width) { this.x = 0; this.y += this.rowHeight; this.rowHeight = 0; } const currentX = this.x; const currentY = this.y; try { const img = await new Promise((resolve, reject) => { const i = new Image(); i.crossOrigin = 'anonymous'; i.onload = () => resolve(i); i.onerror = () => reject(new Error(url)); i.src = url; }); this.ctx.drawImage(img, currentX, currentY, this.cellSize, this.cellSize); } catch (e) { console.warn(`Texture missing: ${id}`); // Draw magenta square ONLY for missing textures this.ctx.fillStyle = '#ff00ff'; this.ctx.fillRect(currentX, currentY, this.cellSize, this.cellSize); } const epsU = 0.1 / this.canvas.width; const epsV = 0.1 / this.canvas.height; const uvData = { // Push the start coordinate slightly inward u: (currentX / this.canvas.width) + epsU, v: (currentY / this.canvas.height) + epsV, // Shrink the total width/height to account for the inset on both sides du: (this.cellSize / this.canvas.width) - (epsU * 2), dv: (this.cellSize / this.canvas.height) - (epsV * 2) }; this.map.set(id, uvData); this.map.set(id, uvData); this.x += this.cellSize; this.rowHeight = Math.max(this.rowHeight, this.cellSize); return uvData; } }