// PROCITY core loaders — promise-cached, fail-soft (house pattern from both parent games). // Missing asset ⇒ null / flat colour; callers keep their placeholder. The game runs asset-free. import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; export const DEPOT = 'https://digalot.fyi/3god'; // shared GLB CDN: GET /a/, /api/list // Local-depot mode (Lane E round-3): resolve depot: to GLBs served by the dev server // (web/assets/models/) instead of the CDN, so Lanes B/C/F can validate GLB loading with NO // network / before the depot publish. Enable per-load with ?localdepot=1 (or ?localdepot=), // or globally with window.PROCITY_LOCALDEPOT = true | ''. Staged GLBs = the normalized // library set + hero props (mirror of pipeline/_normalized; run pipeline/stage_local_depot.py). export const LOCAL_MODELS = 'assets/models/'; function localDepotBase() { try { const q = new URLSearchParams(location.search).get('localdepot'); if (q === '1' || q === '' || q === 'true') return LOCAL_MODELS; if (q) return q.endsWith('/') ? q : q + '/'; } catch { /* no location (non-browser import) */ } const g = (typeof window !== 'undefined') && window.PROCITY_LOCALDEPOT; if (g === true) return LOCAL_MODELS; if (typeof g === 'string' && g) return g.endsWith('/') ? g : g + '/'; return null; } const LOCAL_DEPOT = localDepotBase(); // resolve a depot: ref to a concrete URL (local mode → served copy, else the CDN) export function depotURL(file) { return LOCAL_DEPOT ? LOCAL_DEPOT + file : `${DEPOT}/a/${file}`; } const gltfLoader = new GLTFLoader(); const texLoader = new THREE.TextureLoader(); const GLB_CACHE = {}; // url → Promise const TEX_CACHE = {}; // url → THREE.Texture (may still be loading; fallback colour beneath) // loadGLB('gen/x.glb' | 'depot:file.glb' | full URL) → Promise, cached, never rejects export function loadGLB(ref) { const url = ref.startsWith('depot:') ? depotURL(ref.slice(6)) : ref; return (GLB_CACHE[url] ||= gltfLoader.loadAsync(url).catch(err => { console.warn('[loaders] GLB failed, placeholder stays:', url, err?.message || err); return null; })); } // loadTex(url, {repeat:[u,v]}) → texture immediately (fills in when loaded), sRGB, aniso 4 export function loadTex(url, opts = {}) { if (TEX_CACHE[url]) return TEX_CACHE[url]; const t = texLoader.load(url, undefined, undefined, () => console.warn('[loaders] tex failed, fallback colour stays:', url)); t.colorSpace = THREE.SRGBColorSpace; t.anisotropy = 4; if (opts.repeat) { t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(...opts.repeat); } TEX_CACHE[url] = t; return t; } // fetch + cache the depot listing (editor palettes, Lane E validation) let _depotList = null; export async function depotList() { return (_depotList ||= fetch(`${DEPOT}/api/list`).then(r => r.json()).catch(() => [])); }