// 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.promises = new Map(); // Track loading promises separately! this.cellSize = 16; this.padding = 1; this.x = 0; this.y = 0; this.rowHeight = 0; } async fill(id, currentX, currentY) { const url = resolveResourceLocation(id, 'textures', 'png'); 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}`); this.ctx.fillStyle = '#ff00ff'; this.ctx.fillRect(currentX, currentY, this.cellSize, this.cellSize); } } load(id) { // If already loading/loaded, just return the tracking promise if (this.promises.has(id)) return this.promises.get(id); if (this.x + this.cellSize > this.canvas.width) { this.x = 0; this.y += this.rowHeight + this.padding; this.rowHeight = 0; } const currentX = this.x; const currentY = this.y; this.x += this.cellSize + this.padding; this.rowHeight = Math.max(this.rowHeight, this.cellSize); const epsX = 0.1 / this.canvas.width; const epsY = 0.1 / this.canvas.height; // 1. SYNCHRONOUSLY allocate the UV coordinates for buildGeometry const uvData = { u: currentX / this.canvas.width + epsX, v: currentY / this.canvas.height + epsY, du: this.cellSize / this.canvas.width - 2 * epsX, dv: this.cellSize / this.canvas.height - 2 * epsY, }; this.map.set(id, uvData); // 2. ASYNCHRONOUSLY kick off the image fetch const loadTask = this.fill(id, currentX, currentY); this.promises.set(id, loadTask); return loadTask; } }