// [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)
' +
'';
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);
});
})();