// [e32 coach] Engine 32 RC UI glue (Grok Bot): // - E32Progress: loading progress bar on the game view (stages from engine/ui/model-import.js) + clear error / info toasts. // - E32Setup: save / load the whole setup as JSON (schema + validation: MuseSetup, engine/io/setup-schema.js, Muse Glimmer, reviewed). // - Drag-and-drop: drop models or a setup .json on the game view. Left half = scene, right half = character; a .json anywhere. (function () { var E = window.MuseEngine29, M = window.MuseLooks; if (!E) return; var stage = document.getElementById('stage') || document.body; function esc(t) { return String(t == null ? '' : t).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function P() { return window.E29Panel; } // ---------------- progress + toasts ---------------- var box = document.createElement('div'); box.id = 'e32-progress'; box.setAttribute('role', 'status'); box.innerHTML = '
'; stage.appendChild(box); var toastEl = document.createElement('div'); toastEl.id = 'e32-toast'; toastEl.setAttribute('role', 'alert'); stage.appendChild(toastEl); var jobs = [], seq = 0, hideT = 0, toastT = 0, log = []; var FR = { read: 0.05, download: 0.05, parse: 0.65, textures: 0.7, build: 0.88, upload: 0.96 }; var SLOTNAME = { scene: 'scene', character: 'character' }; function paint(j) { box.className = 'show' + (j.state === 'err' ? ' err' : j.state === 'ok' ? ' ok' : ''); box.querySelector('.t').textContent = j.title + (j.text ? ' — ' + j.text : ''); box.querySelector('.bar i').style.width = Math.round(100 * j.frac) + '%'; box.querySelector('.h').textContent = j.hint || ''; } function start(slot, name) { clearTimeout(hideT); var j = { id: ++seq, slot: slot, name: name, title: 'Loading ' + (SLOTNAME[slot] || slot) + (name ? ': ' + name : ''), text: 'starting', frac: 0.02, state: 'run', stages: [], t0: performance.now() }; jobs.push(j); log.push(j); paint(j); return { update: function (p, text) { if (j.state !== 'run') return; var f = FR[p.stage] != null ? FR[p.stage] : j.frac; if (p.stage === 'download' && p.total) f = 0.05 + 0.55 * Math.min(1, p.loaded / p.total); if (p.stage === 'textures' && p.total) f = 0.7 + 0.15 * Math.min(1, (p.done || 0) / p.total); j.frac = Math.max(j.frac, f); j.text = text || p.stage; if (j.stages[j.stages.length - 1] !== p.stage) j.stages.push(p.stage); if (jobs[jobs.length - 1] === j) paint(j); }, done: function (ok, msg, hint) { j.state = ok ? 'ok' : 'err'; j.ms = Math.round(performance.now() - j.t0); j.msg = msg || ''; j.hint = ok ? '' : (hint || ''); j.frac = ok ? 1 : j.frac; j.title = ok ? 'Loaded ' + (SLOTNAME[slot] || slot) + (name ? ': ' + name : '') : 'Could not load ' + (name || SLOTNAME[slot] || slot); j.text = ok ? (j.ms / 1000).toFixed(1) + ' s' : msg; jobs = jobs.filter(function (x) { return x !== j; }); var run = jobs.filter(function (x) { return x.state === 'run'; }); if (run.length && ok) { paint(run[run.length - 1]); return; } paint(j); clearTimeout(hideT); hideT = setTimeout(function () { if (!jobs.length) box.className = ''; }, ok ? 1400 : 9000); } }; } box.onclick = function () { if (!jobs.length) box.className = ''; }; function toast(msg, level, ms) { toastEl.className = 'show ' + (level || 'info'); toastEl.innerHTML = Array.isArray(msg) ? msg.map(function (m) { return '
' + esc(m) + '
'; }).join('') : esc(msg); clearTimeout(toastT); toastT = setTimeout(function () { toastEl.className = ''; }, ms || (level === 'err' ? 9000 : 4500)); toastLog.push({ level: level || 'info', msg: Array.isArray(msg) ? msg.join(' | ') : String(msg) }); } var toastLog = []; toastEl.onclick = function () { toastEl.className = ''; }; window.E32Progress = { start: start, toast: toast, log: log, toasts: toastLog, active: function () { return jobs.length; } }; // ---------------- setup save / load ---------------- var ROLES = ['idle', 'walk', 'run', 'shoot', 'death']; function schemaOpts() { return { configKeys: Object.keys(E.configDefaults ? E.configDefaults() : E.getConfig()), looks: M ? M.list().map(function (l) { return l.id; }) : ['A'] }; } function slotState(slot) { var p = P(), ls = p && p.lastSpec ? p.lastSpec(slot) : null, info = E.importInfo(slot); if (!info || !ls) return null; var o = { name: info.name || ls.file || '', source: ls.source, url: ls.url, units: ls.units || 'auto', up: ls.up || 'auto' }; if (slot === 'scene') o.asLevel = ls.asLevel !== false; else { o.facing = ls.facing || '+z'; o.clipPicks = {}; var ov = info.overrides || {}, clips = info.clips || []; ROLES.forEach(function (r) { if (r in ov) o.clipPicks[r] = ov[r] >= 0 && clips[ov[r]] != null ? clips[ov[r]] : null; }); } return o; } function current() { var st = { engine: E.version || '', savedAt: new Date().toISOString(), look: M ? M.active() : 'A', config: E.getConfig(), scene: slotState('scene'), character: slotState('character') }; if (window.MuseSetup) return window.MuseSetup.make(st); return Object.assign({ kind: 'muse-engine-setup', version: 1 }, st); } function save() { var text = JSON.stringify(current(), null, 2) + '\n'; try { var url = URL.createObjectURL(new Blob([text], { type: 'application/json' })); var a = document.createElement('a'); a.href = url; a.download = 'muse-engine-setup.json'; document.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(url); a.remove(); }, 1000); } catch (e) { console.warn('[e32] save', e); } window.E32Setup.lastSaved = text; return text; } function sameAsLoaded(slot, s, spec) { var p = P(), ls = p && p.lastSpec ? p.lastSpec(slot) : null; if (!ls || !E.importInfo(slot) || spec.files || s.source !== 'url' || ls.url !== s.url) return false; return ls.units === spec.units && ls.up === spec.up && (slot === 'scene' ? ls.asLevel === spec.asLevel : ls.facing === spec.facing); } function apply(v, files) { files = files || {}; var rep = { applied: [], needFiles: [], errors: [], warnings: [] }; if (M && v.look && M.active() !== v.look) { try { M.setActive(v.look); } catch (e) {} } if (v.config) E.setConfig(v.config); var chain = Promise.resolve(); ['scene', 'character'].forEach(function (slot) { chain = chain.then(function () { var s = v[slot], p = P(); if (!p) { rep.errors.push('config panel missing'); return; } if (!s) { if (E.importInfo(slot)) p.forget(slot); rep.applied.push(slot + ': built-in'); return; } var spec = slot === 'scene' ? { asLevel: s.asLevel !== false, units: s.units || 'auto', up: s.up || 'auto' } : { units: s.units || 'auto', up: s.up || 'auto', facing: s.facing || '+z' }; p.setOpts(slot, spec); if (files[slot] && files[slot].length) spec.files = files[slot]; else if (s.source === 'url' && s.url) spec.url = s.url; else { rep.needFiles.push(s.name); return; } var go = sameAsLoaded(slot, s, spec) ? Promise.resolve(E.importInfo(slot)) : p.importModel(slot, spec); return go.then(function () { rep.applied.push(slot + ': ' + s.name); if (slot === 'character') { var info = E.importInfo('character') || {}, clips = info.clips || [], picks = s.clipPicks || {}; if (info.mode === 'animated') ROLES.forEach(function (r) { if (!(r in picks)) { E.setClipOverride(r, null); return; } if (picks[r] === null) { E.setClipOverride(r, -1); return; } var k = clips.indexOf(picks[r]); if (k < 0) { var lo = String(picks[r]).toLowerCase(); k = clips.findIndex(function (c) { return String(c).toLowerCase() === lo; }); } if (k >= 0) E.setClipOverride(r, k); else { E.setClipOverride(r, null); rep.warnings.push('clip "' + picks[r] + '" (' + r + ') not in ' + info.name + ', using auto'); } }); } }, function (err) { rep.errors.push(slot + ' "' + s.name + '": ' + (err && err.message ? err.message : err)); }); }); }); return chain.then(function () { var p = P(); if (p) { p.refresh(); } return rep; }); } function report(rep, extraWarn) { var w = (extraWarn || []).concat(rep.warnings || []); var lines = []; if (rep.applied.length) lines.push('Setup loaded: ' + rep.applied.join(' · ')); rep.needFiles.forEach(function (n) { lines.push('"' + n + '" was loaded from a file: drop it on the game view (or pick it in Load setup) to load it.'); }); rep.errors.forEach(function (e) { lines.push('Error: ' + e); }); w.forEach(function (x) { lines.push('Note: ' + x); }); var lvl = rep.errors.length ? 'err' : (rep.needFiles.length || w.length) ? 'warn' : 'ok'; toast(lines.length ? lines : ['Setup loaded.'], lvl); var p = P(); if (p) p.setStatus('setup', lines.join(' '), lvl === 'err' ? 'err' : 'okc'); window.E32Setup.lastReport = { level: lvl, lines: lines, rep: rep }; return window.E32Setup.lastReport; } function loadText(text, files) { if (!window.MuseSetup) { toast('Setup module missing (engine/io/setup-schema.js).', 'err'); return Promise.resolve(null); } var r = window.MuseSetup.parse(text, schemaOpts()); if (!r.ok) { var lines = ['Not a usable setup file:'].concat(r.errors); toast(lines, 'err'); var p = P(); if (p) p.setStatus('setup', lines.join(' '), 'err'); window.E32Setup.lastReport = { level: 'err', lines: lines, rep: null }; return Promise.resolve(window.E32Setup.lastReport); } return apply(r.value, files).then(function (rep) { return report(rep, r.warnings.filter(function (w) { return !/was loaded from a file/.test(w) || !(files && ((files.scene && files.scene.length) || (files.character && files.character.length))); })); }); } // files dropped on the game view or picked in "Load setup…"; target = 'scene' | 'character' | null function loadFiles(list, target) { var files = Array.prototype.slice.call(list || []); if (!files.length) return Promise.resolve(null); var infos = files.map(function (f) { return { name: f.name, size: f.size }; }); var jsonIdx = infos.findIndex(function (f) { return /\.json$/i.test(f.name); }); var readSetup = jsonIdx >= 0 ? files[jsonIdx].text() : Promise.resolve(null); return readSetup.then(function (text) { var parsed = null; if (text != null && window.MuseSetup) { parsed = window.MuseSetup.parse(text, schemaOpts()); } var plan = window.MuseSetup ? window.MuseSetup.planDrop(infos, parsed && parsed.ok ? parsed.value : null, target || null) : null; if (!plan) { toast('Setup module missing (engine/io/setup-schema.js).', 'err'); return null; } window.E32Setup.lastPlan = plan; function pick(sp) { return sp && sp.main >= 0 ? [files[sp.main]].concat(sp.extras.map(function (k) { return files[k]; })) : null; } var byslot = { scene: pick(plan.scene), character: pick(plan.character) }; if (plan.errors.length) toast(plan.errors, 'err'); if (jsonIdx >= 0) { return loadText(text, byslot); } var chain = Promise.resolve(), done = []; ['scene', 'character'].forEach(function (slot) { if (!byslot[slot]) return; chain = chain.then(function () { return P().importModel(slot, { files: byslot[slot] }).then(function () { done.push(slot); }, function () {}); }); }); return chain.then(function () { window.E32Setup.lastReport = { level: plan.errors.length ? 'err' : 'ok', lines: plan.errors, rep: { applied: done } }; return window.E32Setup.lastReport; }); }); } window.E32Setup = { current: current, save: save, apply: apply, loadText: loadText, loadFiles: loadFiles, lastSaved: null, lastReport: null, lastPlan: null }; // ---------------- drag-and-drop on the game view ---------------- var drop = document.createElement('div'); drop.id = 'e32-drop'; drop.innerHTML = '
Drop here: SCENEloads as the level (FBX / glTF / GLB + textures)
' + '
Drop here: CHARACTERbecomes the enemies (skinned FBX / GLB)
' + '
A setup .json can go on either half · .obj / .blend / .max / .ma / .mb: export FBX or glTF first
'; stage.appendChild(drop); var dragT = 0, lastHalf = null; function hasFiles(e) { var t = e.dataTransfer && e.dataTransfer.types; return !!t && Array.prototype.indexOf.call(t, 'Files') >= 0; } function halfAt(x, y) { var r = stage.getBoundingClientRect(); if (x < r.left || x > r.right || y < r.top || y > r.bottom) return null; return x < r.left + r.width / 2 ? 'scene' : 'character'; } function showDrop(half) { drop.classList.add('show'); lastHalf = half; drop.querySelectorAll('.half').forEach(function (h) { h.classList.toggle('on', h.getAttribute('data-drop') === half); }); clearTimeout(dragT); dragT = setTimeout(hideDrop, 400); } function hideDrop() { drop.classList.remove('show'); lastHalf = null; } window.addEventListener('dragenter', function (e) { if (!hasFiles(e)) return; e.preventDefault(); showDrop(halfAt(e.clientX, e.clientY)); }); window.addEventListener('dragover', function (e) { if (!hasFiles(e)) return; e.preventDefault(); try { e.dataTransfer.dropEffect = 'copy'; } catch (x) {} showDrop(halfAt(e.clientX, e.clientY)); }); window.addEventListener('dragleave', function (e) { if (!e.relatedTarget) { clearTimeout(dragT); dragT = setTimeout(hideDrop, 120); } }); window.addEventListener('drop', function (e) { if (!e.dataTransfer || !e.dataTransfer.files || !e.dataTransfer.files.length) return; e.preventDefault(); clearTimeout(dragT); var half = halfAt(e.clientX, e.clientY); hideDrop(); if (E.setBlocked && P() && !P().isOpen()) { try { document.exitPointerLock && document.exitPointerLock(); } catch (x) {} } window.E32Setup.lastDrop = { target: half, files: Array.prototype.map.call(e.dataTransfer.files, function (f) { return f.name; }) }; loadFiles(e.dataTransfer.files, half); }); })();