import { DenseNetwork } from './network.js'; /** Gated action residuals. Ordinary movement uses the frozen base. */ export class MovementNetwork { constructor(manifest, base, adapter, jumpAdapter=null) { if (!['superhq-motion-gated-v1','superhq-motion-branched-v1'].includes(manifest.format) || manifest.input_width !== 48 || manifest.output_width !== 143 || manifest.gate_indices?.join(',') !== '34,35,36,37' || base.layers[0].inputs !== 34 || adapter.layers[0].inputs !== 48 || base.layers.at(-1).outputs !== 143 || adapter.layers.at(-1).outputs !== 143) { throw new Error('Invalid movement action network contract.'); } this.base = base; this.adapter = adapter; this.branched=manifest.format==='superhq-motion-branched-v1';this.jumpAdapter=jumpAdapter; if(this.branched&&(!jumpAdapter||jumpAdapter.layers[0].inputs!==48||jumpAdapter.layers.at(-1).outputs!==143))throw Error('Missing jump residual.'); this.output = new Float32Array(143); } forward(input) { if (!(input instanceof Float32Array) || input.length !== 48 || !input.every(Number.isFinite)) { throw new Error('Expected 48 finite action features.'); } const gate = Math.max(0, Math.min(1, input[34] + input[35] + input[36] + input[37])); if(this.branched&&(Array.from(input.subarray(34,38)).some(v=>v<0||v>1)||(input[34]>0&&input.subarray(35,38).some(v=>v>0))))throw Error('Stance and jump gates must be exclusive.'); this.output.set(this.base.forward(input.subarray(0, 34))); if (gate > 0) { const delta = (this.branched&&input[34]===0?this.jumpAdapter:this.adapter).forward(input); for (let i = 0; i < this.output.length; i++) this.output[i] += gate * delta[i]; } if (!this.output.every(Number.isFinite)) throw new Error('Nonfinite movement prediction.'); return this.output; } static async load(baseUrl,{fetch:fetcher=globalThis.fetch}={}) { const root = new URL(baseUrl, globalThis.location?.href); const request = async url => { const response = await fetcher(url); if (!response.ok) throw new Error(`Movement asset failed (${response.status}).`); return response; }; const manifest = await (await request(new URL('movement.json', root))).json(); if (manifest.base !== 'base/' || manifest.adapter !== 'adapter/') throw new Error('Invalid action asset paths.'); if(manifest.format==='superhq-motion-branched-v1'&&manifest.jump_adapter!=='jump/')throw Error('Invalid jump asset path.'); const load = async name => { const folder = new URL(name, root); const data = await (await request(new URL('network.json', folder))).json(); if (data.weights !== 'weights.bin') throw new Error('Invalid weight path.'); const buffer = await (await request(new URL(data.weights, folder))).arrayBuffer(); const digest = await crypto.subtle.digest('SHA-256', buffer); const hash = Array.from(new Uint8Array(digest), b => b.toString(16).padStart(2, '0')).join(''); if (hash !== data.sha256) throw new Error('Movement weight checksum differs.'); return new DenseNetwork(data, buffer); }; const [base, adapter,jump] = await Promise.all([load(manifest.base), load(manifest.adapter),manifest.jump_adapter?load(manifest.jump_adapter):null]); return new MovementNetwork(manifest, base, adapter,jump); } }