axonis22 commited on
Commit
2e214ba
·
verified ·
1 Parent(s): 50850fd

Muse Engine 32 WebGL2 release candidate

Browse files
game/muse-authored/phase32/harness/ref/fbx6-keys.ref.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Coach reference (Grok Bot) for the Phase 32 part C spec: engine/import/fbx6-keys.js. Used only by the harness.
2
+ (function (root) {
3
+ var KTIME = 46186158000, IM = { C: 'step', L: 'linear', U: 'cubic', T: 'cubic' };
4
+ function isN(v) { return typeof v === 'number'; }
5
+ function decode(tokens, keyCount) {
6
+ var r = { ok: true, error: '', count: 0, times: [], values: [], interp: [] };
7
+ if (!Array.isArray(tokens)) { r.ok = false; r.error = 'tokens must be an array'; return r; }
8
+ var errs = [], i = 0, n = tokens.length, stop = false;
9
+ while (i < n && !stop) {
10
+ if (!(isN(tokens[i]) && isN(tokens[i + 1]))) { errs.push('bad key at token ' + i); break; }
11
+ var t = tokens[i], v = tokens[i + 1], ip, j;
12
+ if (i + 2 >= n) { ip = 'linear'; j = n; }
13
+ else {
14
+ var c = tokens[i + 2];
15
+ if (typeof c !== 'string' || !IM[c]) { errs.push('unknown interpolation "' + c + '" at token ' + (i + 2)); break; }
16
+ ip = IM[c]; j = i + 3;
17
+ while (j < n && !((isN(tokens[j]) && isN(tokens[j + 1]) && (IM[tokens[j + 2]] !== undefined && typeof tokens[j + 2] === 'string' || j + 2 === n)))) j++;
18
+ }
19
+ r.times.push(t / KTIME); r.values.push(v); r.interp.push(ip); r.count++;
20
+ i = j;
21
+ }
22
+ if (typeof keyCount === 'number' && r.count !== keyCount) errs.push('expected ' + keyCount + ' keys, decoded ' + r.count);
23
+ for (var k = 1; k < r.times.length; k++) if (r.times[k] < r.times[k - 1]) { errs.push('times not ascending at key ' + k); break; }
24
+ if (errs.length) { r.ok = false; r.error = errs[0]; }
25
+ return r;
26
+ }
27
+ function stepToLinear(times, values, interp, eps) {
28
+ var e = typeof eps === 'number' && eps > 0 ? eps : 1e-4, T = [], V = [];
29
+ for (var i = 0; i < times.length; i++) {
30
+ T.push(times[i]); V.push(values[i]);
31
+ if (interp[i] === 'step' && i + 1 < times.length && values[i + 1] !== values[i] && times[i + 1] - times[i] > 2 * e) { T.push(times[i + 1] - e); V.push(values[i]); }
32
+ }
33
+ return { times: T, values: V };
34
+ }
35
+ root.MuseFbx6Keys = { KTIME: KTIME, decode: decode, stepToLinear: stepToLinear };
36
+ if (typeof module !== 'undefined' && module.exports) module.exports = root.MuseFbx6Keys;
37
+ })(typeof window !== 'undefined' ? window : globalThis);
game/muse-authored/phase32/harness/ref/model-check.ref.js ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // [e32 coach] Reference implementation of the p32a model-check spec (harness oracle, Grok Bot). Not shipped.
2
+ (function (root) {
3
+ var LIMITS = { sceneTrisWarn: 300000, sceneTrisError: 1000000, charTrisWarn: 100000, charTrisError: 400000, partsWarn: 400,
4
+ maxBonesDefault: 60, rootMotionMinM: 0.10, rootMotionFrac: 0.05, charHeightMin: 0.5, charHeightMax: 3.0,
5
+ sceneExtentMin: 2, sceneExtentMax: 5000, fpsLow: 15 };
6
+ var S = [12, 15, 23.976, 24, 25, 29.97, 30, 48, 50, 59.94, 60, 100, 120];
7
+ function estimateFps(times) {
8
+ var t = Array.prototype.slice.call(times || []), d = [];
9
+ for (var i = 1; i < t.length; i++) { var x = t[i] - t[i - 1]; if (x >= 1e-6) d.push(x); }
10
+ if (!d.length) return { fps: 0, frames: t.length, standard: false };
11
+ d.sort(function (a, b) { return a - b; });
12
+ var dt = d[(d.length - 1) >> 1], raw = 1 / dt, best = S[0];
13
+ for (var k = 1; k < S.length; k++) if (Math.abs(raw - S[k]) < Math.abs(raw - best)) best = S[k];
14
+ var fps, std;
15
+ if (Math.abs(raw - best) <= 0.02 * best) { fps = best; std = true; } else { fps = Math.round(raw * 100) / 100; std = false; }
16
+ return { fps: fps, frames: Math.round((t[t.length - 1] - t[0]) * fps) + 1, standard: std };
17
+ }
18
+ function num(v) { return typeof v === 'number' && isFinite(v) ? v : 0; }
19
+ function arr(v) { return Array.isArray(v) ? v : []; }
20
+ function obj(v) { return v && typeof v === 'object' && !Array.isArray(v) ? v : {}; }
21
+ function analyze(facts) {
22
+ var f = obj(facts), W = [];
23
+ function add(code, level, msg, fix) { W.push({ code: code, level: level, msg: msg, fix: fix }); }
24
+ var rig = obj(f.rig), units = obj(f.units), up = obj(f.up), tex = obj(f.textures), sc = obj(f.scene);
25
+ var clips = arr(rig.clips);
26
+ if (f.kind === 'character') {
27
+ if (rig.skinned !== true) add('NO_SKIN', 'warn', 'No skinned mesh / skeleton: drawn as a static model scaled into the enemy box (no animation).', 'Export with Skin/Skinning and Deformations enabled, bind the mesh to the skeleton.');
28
+ else {
29
+ var maxB = num(rig.maxBones) || LIMITS.maxBonesDefault, merged = arr(rig.merged), bones = num(rig.bones);
30
+ if (bones > maxB) add('BONES_OVER_LIMIT', 'warn', bones + ' bones > ' + maxB + ' GPU slots: ' + merged.length + ' minor leaf bones merged into their parents' + (merged.length ? ' (' + merged.slice(0, 8).join(', ') + (merged.length > 8 ? ', …' : '') + ')' : '') + '.', 'Remove or skip fingers/twist/helper bones on export, or accept the merge.');
31
+ if (clips.length === 0) add('NO_CLIPS', 'warn', 'Skinned but no animation takes: the character stands in its bind pose.', 'Bake animation on export (Maya: Bake Animation; Max: Bake Animation) and include the takes.');
32
+ var th = Math.max(LIMITS.rootMotionMinM, LIMITS.rootMotionFrac * (num(rig.heightM) || 1.8));
33
+ var moving = clips.filter(function (c) { return num(c.rootMotionM) > th; });
34
+ if (moving.length) {
35
+ var lv = moving.every(function (c) { return c.stripped; }) ? 'info' : 'warn';
36
+ add('ROOT_MOTION', lv, 'Root motion in ' + moving.length + ' clip(s): ' + moving.map(function (c) { return c.name + ' ' + num(c.rootMotionM).toFixed(2) + ' m'; }).join(', ') + (lv === 'info' ? ' (stripped: played in place).' : ' (not stripped: the character slides away from its capsule).'), 'Keep the toggle "Strip root motion" on, or export in-place animations.');
37
+ }
38
+ if (clips.length > 0) {
39
+ var roles = obj(rig.roles);
40
+ var miss = ['idle', 'walk', 'run', 'shoot', 'death'].filter(function (r) { return !(typeof roles[r] === 'number' && roles[r] >= 0); });
41
+ if (miss.length) add('UNMAPPED_ROLES', 'info', 'No clip found for: ' + miss.join(', ') + ' (pick one in the clip picker, or name takes e.g. Idle, Walk, Run, Shoot, Death).', 'Rename the takes or pick clips in the panel.');
42
+ }
43
+ var low = [], odd = [];
44
+ clips.forEach(function (c) { var e = estimateFps(c.times); if (e.fps > 0 && e.fps < LIMITS.fpsLow) low.push([c, e]); if (e.fps > 0 && !e.standard) odd.push([c, e]); });
45
+ function lst(a) { return a.map(function (x) { return x[0].name + ' ' + x[1].fps + ' fps'; }).join(', '); }
46
+ if (low.length) add('LOW_FPS', 'warn', 'Low key rate in: ' + lst(low) + '.', 'Bake at 30 fps (every frame).');
47
+ else if (odd.length) add('ODD_FPS', 'info', 'Unusual key rate in: ' + lst(odd) + '.', 'Bake every frame at the scene frame rate (24/25/30/60).');
48
+ }
49
+ var h = num(rig.heightM);
50
+ if (h > 0 && (h < LIMITS.charHeightMin || h > LIMITS.charHeightMax)) add('ODD_SIZE', 'warn', 'Character is ' + h.toFixed(2) + ' m tall after unit conversion: check the units / scale.', 'Set the units in the panel (Units) or export in centimetres with unit metadata.');
51
+ }
52
+ if (f.kind === 'scene') {
53
+ var ext = Math.max.apply(null, arr(f.sizeM).map(num).concat([0]));
54
+ if (ext > 0 && (ext < LIMITS.sceneExtentMin || ext > LIMITS.sceneExtentMax)) add('ODD_SIZE', 'warn', 'Scene is ' + ext.toFixed(1) + ' m across after unit conversion: check the units / scale.', 'Set the units in the panel (Units) or export in centimetres with unit metadata.');
55
+ }
56
+ if (units.source === 'guess') add('NO_UNITS_INFO', 'warn', 'No unit information in the file: guessed ' + units.value + ' from the size.', 'FBX: keep the file units (Maya: Preferences > Settings > Working units; export with Automatic units). glTF is always metres.');
57
+ if (up.source === 'guess') add('NO_UP_INFO', 'warn', 'No up-axis information in the file: assumed ' + String(up.value).toUpperCase() + '-up.', 'FBX: export with Up Axis set (Y or Z); the engine converts Z-up to Y-up.');
58
+ var miss2 = arr(tex.missing);
59
+ if (miss2.length) add('MISSING_TEXTURES', 'warn', miss2.length + ' texture(s) not found: ' + miss2.slice(0, 6).join(', ') + (miss2.length > 6 ? ', …' : '') + '. Material colours used instead.', 'Embed media in the FBX (Embed Media) or select the texture files together with the model; for .gltf select the .bin and images too.');
60
+ var tris = num(f.triangles), isS = f.kind === 'scene';
61
+ var wAt = isS ? LIMITS.sceneTrisWarn : LIMITS.charTrisWarn, eAt = isS ? LIMITS.sceneTrisError : LIMITS.charTrisError;
62
+ if (tris >= eAt || tris >= wAt) { var er = tris >= eAt; add('HEAVY_MESH', er ? 'error' : 'warn', tris.toLocaleString('en-US') + ' triangles: ' + (er ? 'very heavy for WebGL1, expect low frame rates.' : 'heavy, frame rate may drop on laptops.'), 'Reduce polygons (Maya: Mesh > Reduce; Max: ProOptimizer) or remove hidden detail.'); }
63
+ if (num(f.parts) > LIMITS.partsWarn) add('MANY_PARTS', 'info', num(f.parts) + ' draw parts (meshes x materials): the engine batches parts that share a material.', 'Combine meshes that share a material before export.');
64
+ if (isS && sc.mode === 'level') {
65
+ if (num(sc.walkableTris) === 0) add('NO_FLOOR', 'error', 'No walkable floor found (no upward-facing collision triangles): you will fall.', 'Check the up axis / units, and that floors are not named _nocol.');
66
+ if (sc.spawn !== 'node') add('NO_SPAWN', 'info', 'No PlayerStart node: spawning on the nearest open floor.', 'Add an empty/dummy named PlayerStart where the player should start.');
67
+ }
68
+ var rank = { error: 0, warn: 1, info: 2 };
69
+ var sorted = W.map(function (w, i) { return [w, i]; }).sort(function (a, b) { return (rank[a[0].level] - rank[b[0].level]) || (a[1] - b[1]); }).map(function (x) { return x[0]; });
70
+ var e = sorted.filter(function (w) { return w.level === 'error'; }).length, wn = sorted.filter(function (w) { return w.level === 'warn'; }).length;
71
+ return { level: e ? 'error' : wn ? 'warn' : 'ok', warnings: sorted, summary: { errors: e, warns: wn, infos: sorted.length - e - wn, codes: sorted.map(function (w) { return w.code; }) } };
72
+ }
73
+ root.MuseModelCheck = { analyze: analyze, estimateFps: estimateFps, LIMITS: LIMITS };
74
+ })(typeof window !== 'undefined' ? window : globalThis);
game/muse-authored/phase32/harness/ref/root-motion.ref.js ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // [e32 coach] Reference implementation of the p32a root-motion spec (harness oracle, Grok Bot). Not shipped.
2
+ (function (root) {
3
+ function trk(clip, node) { var t = clip && clip.tracks || []; for (var i = 0; i < t.length; i++) if (t[i].node === node && t[i].path === 'translation') return t[i]; return null; }
4
+ function dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
5
+ function h(v, up) { var d = dot(v, up); return [v[0] - d * up[0], v[1] - d * up[1], v[2] - d * up[2]]; }
6
+ function len(v) { return Math.hypot(v[0], v[1], v[2]); }
7
+ function key(t, i) { return [t.values[3 * i], t.values[3 * i + 1], t.values[3 * i + 2]]; }
8
+ function measure(clip, node, up) {
9
+ var t = trk(clip, node); if (!t) return null;
10
+ var n = t.times.length, first = key(t, 0), last = key(t, n - 1);
11
+ var net = [last[0] - first[0], last[1] - first[1], last[2] - first[2]], mh = 0;
12
+ for (var i = 0; i < n; i++) { var k = key(t, i); mh = Math.max(mh, len(h([k[0] - first[0], k[1] - first[1], k[2] - first[2]], up))); }
13
+ var hz = len(h(net, up));
14
+ return { keys: n, first: first, last: last, net: net, horiz: hz, vert: Math.abs(dot(net, up)), maxHoriz: mh, speed: clip.duration > 0 ? hz / clip.duration : 0 };
15
+ }
16
+ function strip(clip, node, up, opts) {
17
+ var mode = opts && opts.mode === 'lock' ? 'lock' : 'drift';
18
+ var t = trk(clip, node); if (!t) return { clip: clip, removed: [0, 0, 0], mode: mode };
19
+ var n = t.times.length, first = key(t, 0), last = key(t, n - 1);
20
+ var hn = h([last[0] - first[0], last[1] - first[1], last[2] - first[2]], up);
21
+ var v = new Float32Array(t.values.length), t0 = t.times[0], t1 = t.times[n - 1];
22
+ for (var i = 0; i < n; i++) {
23
+ var k = key(t, i), o;
24
+ if (mode === 'drift') { var u = t1 === t0 ? 0 : (t.times[i] - t0) / (t1 - t0); o = [u * hn[0], u * hn[1], u * hn[2]]; }
25
+ else o = h([k[0] - first[0], k[1] - first[1], k[2] - first[2]], up);
26
+ v[3 * i] = k[0] - o[0]; v[3 * i + 1] = k[1] - o[1]; v[3 * i + 2] = k[2] - o[2];
27
+ }
28
+ var nt = Object.assign({}, t, { values: v });
29
+ return { clip: { name: clip.name, duration: clip.duration, tracks: clip.tracks.map(function (x) { return x === t ? nt : x; }) }, removed: hn, mode: mode };
30
+ }
31
+ function moves(clips, node) {
32
+ for (var c = 0; c < clips.length; c++) { var t = trk(clips[c], node); if (!t) continue; var k0 = key(t, 0);
33
+ for (var i = 1; i < t.times.length; i++) { var k = key(t, i); if (len([k[0] - k0[0], k[1] - k0[1], k[2] - k0[2]]) > 1e-6) return true; } }
34
+ return false;
35
+ }
36
+ function pickRoot(skel, clips, hips) {
37
+ var nodes = skel && skel.nodes || [], cand = {};
38
+ (clips || []).forEach(function (c) { (c.tracks || []).forEach(function (t) { if (t.path === 'translation') cand[t.node] = true; }); });
39
+ if (hips >= 0) for (var n = hips, guard = 0; n >= 0 && guard < 10000; n = nodes[n] ? nodes[n].parent : -1, guard++) if (cand[n] && moves(clips, n)) return n;
40
+ var best = -1, bd = Infinity;
41
+ Object.keys(cand).map(Number).sort(function (a, b) { return a - b; }).forEach(function (i) {
42
+ if (!moves(clips, i)) return; var d = 0; for (var p = nodes[i] ? nodes[i].parent : -1; p >= 0 && d < 10000; p = nodes[p] ? nodes[p].parent : -1) d++;
43
+ if (d < bd) { bd = d; best = i; } });
44
+ return best;
45
+ }
46
+ root.MuseRootMotion = { measure: measure, strip: strip, pickRoot: pickRoot };
47
+ })(typeof window !== 'undefined' ? window : globalThis);
game/muse-authored/phase32/harness/ref/setup-schema.ref.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // [e32 coach] Reference implementation of the p32b setup-schema spec (harness oracle, Grok Bot). Not shipped.
2
+ // Revised after the Muse review: the 'loaded from a file' warning now names a non-string name as String(name || '') like make() (was '').
3
+ (function (root) {
4
+ var KIND = 'muse-engine-setup', VERSION = 1;
5
+ var UNITS = ['auto', 'm', 'cm', 'mm', 'in', 'ft'], UP = ['auto', 'y', 'z'], FACING = ['+z', '-z', '+x', '-x'], ROLES = ['idle', 'walk', 'run', 'shoot', 'death'], SOURCES = ['url', 'file'];
6
+ function isObj(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
7
+ function slot(s, which) {
8
+ if (s === null || s === undefined) return null;
9
+ var source = s.source === 'file' ? 'file' : 'url';
10
+ var o = { name: String(s.name || ''), source: source, url: source === 'url' ? String(s.url || '') : null };
11
+ if (which === 'scene') o.asLevel = s.asLevel !== false;
12
+ o.units = UNITS.indexOf(s.units) >= 0 ? s.units : 'auto';
13
+ o.up = UP.indexOf(s.up) >= 0 ? s.up : 'auto';
14
+ if (which === 'character') {
15
+ o.facing = FACING.indexOf(s.facing) >= 0 ? s.facing : '+z';
16
+ var cp = {}, src = isObj(s.clipPicks) ? s.clipPicks : {};
17
+ ROLES.forEach(function (r) { if (Object.prototype.hasOwnProperty.call(src, r) && (typeof src[r] === 'string' || src[r] === null)) cp[r] = src[r]; });
18
+ o.clipPicks = cp;
19
+ }
20
+ return o;
21
+ }
22
+ function make(st) {
23
+ st = st || {};
24
+ var cfg = {}, c = isObj(st.config) ? st.config : {};
25
+ Object.keys(c).sort().forEach(function (k) { if (typeof c[k] === 'boolean') cfg[k] = c[k]; });
26
+ return { kind: KIND, version: VERSION, engine: String(st.engine || ''), savedAt: String(st.savedAt || ''), look: String(st.look || 'A'), config: cfg,
27
+ scene: slot(st.scene, 'scene'), character: slot(st.character, 'character') };
28
+ }
29
+ function urlOk(u) {
30
+ var t = u.trim(); if (t.length > 2048) return false;
31
+ if (/^https?:\/\//.test(t)) return true;
32
+ var slash = t.indexOf('/'), colon = t.indexOf(':');
33
+ return colon < 0 || (slash >= 0 && colon > slash);
34
+ }
35
+ var TOP = ['kind', 'version', 'engine', 'savedAt', 'look', 'config', 'scene', 'character'];
36
+ function validate(o, opts) {
37
+ opts = opts || {};
38
+ var E = [], W = [];
39
+ if (!isObj(o)) return { ok: false, errors: ['not a JSON object'], warnings: [], value: null };
40
+ if (o.kind !== KIND) E.push('kind must be "muse-engine-setup"');
41
+ var v = o.version;
42
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 1) E.push('version must be a positive integer');
43
+ else if (v > VERSION) E.push('version ' + v + ' is newer than this engine (' + VERSION + ')');
44
+ Object.keys(o).forEach(function (k) { if (TOP.indexOf(k) < 0) W.push('unknown key "' + k + '" ignored'); });
45
+ var cfg = {};
46
+ if (o.config !== undefined) {
47
+ if (!isObj(o.config)) W.push('config ignored (not an object)');
48
+ else Object.keys(o.config).forEach(function (k) {
49
+ if (typeof o.config[k] !== 'boolean') W.push('config.' + k + ' ignored (not true/false)');
50
+ else if (opts.configKeys && opts.configKeys.indexOf(k) < 0) W.push('config.' + k + ' ignored (unknown toggle)');
51
+ else cfg[k] = o.config[k];
52
+ });
53
+ }
54
+ var look = o.look === undefined ? 'A' : String(o.look);
55
+ if (o.look !== undefined && opts.looks && opts.looks.indexOf(look) < 0) { W.push('look "' + look + '" unknown, using ' + opts.looks[0]); look = opts.looks[0]; }
56
+ var slots = {};
57
+ ['scene', 'character'].forEach(function (sn) {
58
+ var s = o[sn];
59
+ if (s === undefined || s === null) { slots[sn] = null; return; }
60
+ if (!isObj(s)) { E.push(sn + ' must be an object or null'); return; }
61
+ if (s.source !== undefined && SOURCES.indexOf(s.source) < 0) E.push(sn + '.source must be url or file');
62
+ if ((s.source === 'url' || s.source === undefined) && !(typeof s.url === 'string' && s.url.length > 0)) E.push(sn + '.url is required for source url');
63
+ if (typeof s.url === 'string' && !urlOk(s.url)) E.push(sn + '.url is not allowed: ' + s.url);
64
+ if (s.name !== undefined && typeof s.name !== 'string') E.push(sn + '.name must be a string');
65
+ var fields = sn === 'scene' ? ['units', 'up'] : ['units', 'up', 'facing'];
66
+ fields.forEach(function (fd) {
67
+ var en = fd === 'units' ? UNITS : fd === 'up' ? UP : FACING;
68
+ if (s[fd] !== undefined && en.indexOf(s[fd]) < 0) W.push(sn + '.' + fd + ' "' + s[fd] + '" invalid, using ' + (fd === 'facing' ? '+z' : 'auto'));
69
+ });
70
+ if (sn === 'character' && isObj(s.clipPicks)) Object.keys(s.clipPicks).forEach(function (k) {
71
+ if (ROLES.indexOf(k) < 0) W.push(sn + '.clipPicks.' + k + ' ignored');
72
+ else if (!(typeof s.clipPicks[k] === 'string' || s.clipPicks[k] === null)) W.push(sn + '.clipPicks.' + k + ' ignored (not a clip name)');
73
+ });
74
+ var c2 = Object.assign({}, s);
75
+ if (typeof c2.name === 'string' && c2.name.length > 260) { c2.name = c2.name.slice(0, 260); W.push(sn + '.name truncated'); }
76
+ if (s.source === 'file') W.push(sn + ' "' + String(c2.name || '') + '" was loaded from a file: drop that file on the game view to load it');
77
+ slots[sn] = c2;
78
+ });
79
+ if (E.length) return { ok: false, errors: E, warnings: W, value: null };
80
+ return { ok: true, errors: [], warnings: W, value: make({ engine: o.engine, savedAt: o.savedAt, look: look, config: cfg, scene: slots.scene, character: slots.character }) };
81
+ }
82
+ function parse(text, opts) {
83
+ if (typeof text === 'string' && text.length > 262144) return { ok: false, errors: ['file too large (max 256 KB)'], warnings: [], value: null };
84
+ var o; try { o = JSON.parse(text); } catch (e) { return { ok: false, errors: ['not valid JSON: ' + e.message], warnings: [], value: null }; }
85
+ return validate(o, opts);
86
+ }
87
+ var MODEL = ['glb', 'gltf', 'fbx'], TAKES = ['gltf', 'fbx'], UNSUP = ['obj', 'blend', 'max', 'mb', 'ma', '3ds', 'dae', 'usd', 'usdz', 'stl', 'c4d', 'skp'], EXTRA = ['bin', 'png', 'jpg', 'jpeg', 'webp', 'tga', 'ktx2'];
88
+ function ext(n) { var i = String(n).lastIndexOf('.'); return i < 0 ? '' : String(n).slice(i + 1).toLowerCase(); }
89
+ function planDrop(files, setup, target) {
90
+ files = files || [];
91
+ var r = { setup: -1, scene: { main: -1, extras: [] }, character: { main: -1, extras: [] }, ignored: [], errors: [] };
92
+ var models = [], extras = [];
93
+ files.forEach(function (f, i) {
94
+ var e = ext(f.name);
95
+ if (e === 'json') { if (r.setup < 0) r.setup = i; else r.ignored.push(i); }
96
+ else if (MODEL.indexOf(e) >= 0) models.push(i);
97
+ else if (UNSUP.indexOf(e) >= 0) { r.ignored.push(i); r.errors.push(f.name + ': .' + e + ' is not supported, export FBX or glTF (.glb) instead'); }
98
+ else if (EXTRA.indexOf(e) >= 0) extras.push(i);
99
+ else { r.ignored.push(i); r.errors.push(f.name + ': unknown file type'); }
100
+ });
101
+ var rest = [];
102
+ models.forEach(function (i) {
103
+ var n = String(files[i].name).toLowerCase();
104
+ if (setup && setup.scene && r.scene.main < 0 && n === String(setup.scene.name).toLowerCase()) r.scene.main = i;
105
+ else if (setup && setup.character && r.character.main < 0 && n === String(setup.character.name).toLowerCase()) r.character.main = i;
106
+ else rest.push(i);
107
+ });
108
+ var unassigned = [];
109
+ rest.forEach(function (i) {
110
+ if ((target === 'scene' || target === 'character') && r[target].main < 0) r[target].main = i;
111
+ else if (r.setup < 0 && target == null && r.scene.main < 0) r.scene.main = i;
112
+ else if (r.setup < 0 && target == null && r.character.main < 0) r.character.main = i;
113
+ else unassigned.push(i);
114
+ });
115
+ // error messages for unassigned models are emitted in file order, after the type errors collected above
116
+ unassigned.forEach(function (i) { r.ignored.push(i); r.errors.push(files[i].name + ': dropped with no free slot (drop one model on each half of the game view)'); });
117
+ var takes = ['scene', 'character'].filter(function (s) { var m = r[s].main; return m >= 0 && TAKES.indexOf(ext(files[m].name)) >= 0; }); // [scope change P32-9] registered extensions may take extras
118
+ extras.forEach(function (i) { if (takes.length) takes.forEach(function (s) { r[s].extras.push(i); }); else r.ignored.push(i); });
119
+ r.ignored.sort(function (a, b) { return a - b; });
120
+ return r;
121
+ }
122
+ function registerModelExt(e, o) { // [scope change P32-9]
123
+ e = String(e == null ? '' : e).toLowerCase().replace(/^\./, '');
124
+ if (!/^[a-z0-9]+$/.test(e) || e === 'json' || EXTRA.indexOf(e) >= 0 || MODEL.indexOf(e) >= 0) return false;
125
+ MODEL.push(e); if (UNSUP.indexOf(e) >= 0) UNSUP.splice(UNSUP.indexOf(e), 1); if (o && o.extras) TAKES.push(e); return true;
126
+ }
127
+ root.MuseSetup = { KIND: KIND, VERSION: VERSION, make: make, validate: validate, parse: parse, planDrop: planDrop, registerModelExt: registerModelExt };
128
+ })(typeof window !== 'undefined' ? window : globalThis);
game/muse-authored/phase32/harness/ref/texture-budget.ref.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Coach reference (Grok Bot) for the Phase 32 part C spec: engine/check/texture-budget.js. Used only by the harness.
2
+ (function (root) {
3
+ var LIMITS = { uploadMax: 1024, largeWarn: 2048, memWarnMB: 256 };
4
+ function pot(x) { return Math.pow(2, Math.round(Math.log2(Math.max(1, x)))); }
5
+ function isPot(x) { return x > 0 && (x & (x - 1)) === 0; }
6
+ function num(v) { return typeof v === 'number' && v > 0; }
7
+ function analyze(textures, opts) {
8
+ var o = {}; for (var k in LIMITS) o[k] = opts && num(opts[k]) ? opts[k] : LIMITS[k];
9
+ var list = (Array.isArray(textures) ? textures : []).filter(function (t) { return t && typeof t === 'object' && num(t.width) && num(t.height); });
10
+ var dec = 0, up = 0, largest = null, over = [], npot = [], L = null, firstNpot = null;
11
+ list.forEach(function (t) {
12
+ dec += t.width * t.height * 4 / 1048576;
13
+ up += Math.min(o.uploadMax, pot(t.width)) * Math.min(o.uploadMax, pot(t.height)) * 4 * 4 / 3 / 1048576;
14
+ if (!largest || t.width * t.height > largest.width * largest.height) largest = t;
15
+ if (Math.max(t.width, t.height) > o.largeWarn) { over.push(t.name); if (!L || t.width * t.height > L.width * L.height) L = t; }
16
+ if (!isPot(t.width) || !isPot(t.height)) { npot.push(t.name); if (!firstNpot) firstNpot = t; }
17
+ });
18
+ var w = [];
19
+ if (over.length) w.push({ code: 'LARGE_TEXTURES', level: 'warn', msg: over.length + ' texture(s) larger than ' + o.largeWarn + ' px (largest: ' + L.name + ' ' + L.width + 'x' + L.height + '): the engine shrinks every texture to ' + o.uploadMax + ' px, so the extra size only costs memory and loading time.', fix: 'Export 1024 or 2048 px maps (3ds Max: Render to Texture / resize the bitmaps; Maya: resize the file textures) before exporting.' });
20
+ if (dec > o.memWarnMB) w.push({ code: 'TEXTURE_MEMORY', level: 'warn', msg: 'Textures need about ' + Math.round(dec) + ' MB of memory while loading (' + list.length + ' images).', fix: 'Use fewer or smaller textures, or share one atlas between parts.' });
21
+ if (npot.length) w.push({ code: 'NPOT_TEXTURES', level: 'info', msg: npot.length + ' texture(s) are not a power of two (e.g. ' + firstNpot.name + ' ' + firstNpot.width + 'x' + firstNpot.height + '): they are resized on upload and may blur slightly.', fix: 'Use power-of-two sizes such as 512, 1024 or 2048.' });
22
+ return { count: list.length, totalDecodedMB: Math.round(dec * 10) / 10, totalUploadMB: Math.round(up * 10) / 10,
23
+ largest: largest ? { name: largest.name, width: largest.width, height: largest.height } : null, over: over, npot: npot, warnings: w };
24
+ }
25
+ root.MuseTextureBudget = { analyze: analyze, LIMITS: LIMITS };
26
+ if (typeof module !== 'undefined' && module.exports) module.exports = root.MuseTextureBudget;
27
+ })(typeof window !== 'undefined' ? window : globalThis);