ugly-game 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/assets/anim.d.ts +13 -0
  2. package/dist/assets/anim.js +55 -0
  3. package/dist/assets/animMoat.d.ts +36 -0
  4. package/dist/assets/animMoat.js +55 -0
  5. package/dist/assets/archetype.d.ts +20 -0
  6. package/dist/assets/archetype.js +8 -0
  7. package/dist/assets/assemble.d.ts +26 -0
  8. package/dist/assets/assemble.js +92 -0
  9. package/dist/assets/genome.d.ts +33 -0
  10. package/dist/assets/genome.js +55 -0
  11. package/dist/assets/genomeExplore.d.ts +28 -0
  12. package/dist/assets/genomeExplore.js +100 -0
  13. package/dist/assets/geometry.d.ts +23 -0
  14. package/dist/assets/geometry.js +134 -0
  15. package/dist/assets/gpuSkin.d.ts +21 -0
  16. package/dist/assets/gpuSkin.js +148 -0
  17. package/dist/assets/lod.d.ts +2 -0
  18. package/dist/assets/lod.js +18 -0
  19. package/dist/assets/meshlet.d.ts +23 -0
  20. package/dist/assets/meshlet.js +53 -0
  21. package/dist/assets/morph.d.ts +5 -0
  22. package/dist/assets/morph.js +75 -0
  23. package/dist/assets/partkit.d.ts +28 -0
  24. package/dist/assets/partkit.js +21 -0
  25. package/dist/assets/pose.d.ts +26 -0
  26. package/dist/assets/pose.js +208 -0
  27. package/dist/assets/renderAssets.d.ts +37 -0
  28. package/dist/assets/renderAssets.js +54 -0
  29. package/dist/assets/residency.d.ts +13 -0
  30. package/dist/assets/residency.js +45 -0
  31. package/dist/assets/retarget.d.ts +11 -0
  32. package/dist/assets/retarget.js +37 -0
  33. package/dist/assets/rig.d.ts +13 -0
  34. package/dist/assets/rig.js +93 -0
  35. package/dist/assets/select.d.ts +10 -0
  36. package/dist/assets/select.js +24 -0
  37. package/dist/assets/stream.d.ts +12 -0
  38. package/dist/assets/stream.js +32 -0
  39. package/dist/assets/style.d.ts +10 -0
  40. package/dist/assets/style.js +17 -0
  41. package/dist/assets/ugm.d.ts +53 -0
  42. package/dist/assets/ugm.js +100 -0
  43. package/dist/gpuDriven.d.ts +39 -0
  44. package/dist/gpuDriven.js +197 -0
  45. package/dist/gpuGI.d.ts +39 -0
  46. package/dist/gpuGI.js +253 -0
  47. package/dist/gpuGraph.d.ts +28 -0
  48. package/dist/gpuGraph.js +128 -0
  49. package/dist/gpuLit.d.ts +40 -0
  50. package/dist/gpuLit.js +193 -0
  51. package/dist/gpuPost.d.ts +33 -0
  52. package/dist/gpuPost.js +162 -0
  53. package/dist/gpuShadowMap.d.ts +11 -0
  54. package/dist/gpuShadowMap.js +41 -0
  55. package/dist/gpuSpot.d.ts +39 -0
  56. package/dist/gpuSpot.js +213 -0
  57. package/dist/gpuTaa.d.ts +34 -0
  58. package/dist/gpuTaa.js +247 -0
  59. package/dist/gpuTerrain.d.ts +25 -0
  60. package/dist/gpuTerrain.js +154 -0
  61. package/dist/gpuTiled.d.ts +55 -0
  62. package/dist/gpuTiled.js +247 -0
  63. package/dist/gpuWorld.d.ts +49 -0
  64. package/dist/gpuWorld.js +309 -0
  65. package/dist/shaderGraph.d.ts +84 -0
  66. package/dist/shaderGraph.js +231 -0
  67. package/package.json +2 -1
@@ -0,0 +1,13 @@
1
+ export interface AnimChannel {
2
+ jointIndex: number;
3
+ path: number;
4
+ times: Float32Array;
5
+ values: Float32Array;
6
+ }
7
+ export interface AnimClip {
8
+ name: string;
9
+ duration: number;
10
+ channels: AnimChannel[];
11
+ }
12
+ export declare function packAnim(clip: AnimClip): Uint8Array;
13
+ export declare function unpackAnim(b: Uint8Array): AnimClip;
@@ -0,0 +1,55 @@
1
+ const u16 = (v) => { const a = new Uint8Array(2); new DataView(a.buffer).setUint16(0, v, true); return a; };
2
+ const u32 = (v) => { const a = new Uint8Array(4); new DataView(a.buffer).setUint32(0, v, true); return a; };
3
+ const i16 = (v) => { const a = new Uint8Array(2); new DataView(a.buffer).setInt16(0, v, true); return a; };
4
+ const f32s = (arr) => new Uint8Array(arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength));
5
+ const f32 = (v) => f32s(Float32Array.of(v));
6
+ export function packAnim(clip) {
7
+ const parts = [];
8
+ const name = new TextEncoder().encode(clip.name);
9
+ parts.push(u16(name.length), name, f32(clip.duration), u16(clip.channels.length));
10
+ for (const ch of clip.channels)
11
+ parts.push(i16(ch.jointIndex), new Uint8Array([ch.path, 0]), u32(ch.times.length), u32(ch.values.length), f32s(ch.times), f32s(ch.values));
12
+ const total = parts.reduce((s, p) => s + p.length, 0);
13
+ const out = new Uint8Array(total);
14
+ let o = 0;
15
+ for (const p of parts) {
16
+ out.set(p, o);
17
+ o += p.length;
18
+ }
19
+ return out;
20
+ }
21
+ export function unpackAnim(b) {
22
+ const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
23
+ let o = 0;
24
+ const nameLen = dv.getUint16(o, true);
25
+ o += 2;
26
+ const name = new TextDecoder().decode(b.subarray(o, o + nameLen));
27
+ o += nameLen;
28
+ const duration = dv.getFloat32(o, true);
29
+ o += 4;
30
+ const channelCount = dv.getUint16(o, true);
31
+ o += 2;
32
+ const channels = [];
33
+ for (let c = 0; c < channelCount; c++) {
34
+ const jointIndex = dv.getInt16(o, true);
35
+ o += 2;
36
+ const path = dv.getUint8(o);
37
+ o += 2; // path + pad
38
+ const tc = dv.getUint32(o, true);
39
+ o += 4;
40
+ const vc = dv.getUint32(o, true);
41
+ o += 4;
42
+ const times = new Float32Array(tc);
43
+ for (let i = 0; i < tc; i++) {
44
+ times[i] = dv.getFloat32(o, true);
45
+ o += 4;
46
+ }
47
+ const values = new Float32Array(vc);
48
+ for (let i = 0; i < vc; i++) {
49
+ values[i] = dv.getFloat32(o, true);
50
+ o += 4;
51
+ }
52
+ channels.push({ jointIndex, path, times, values });
53
+ }
54
+ return { name, duration, channels };
55
+ }
@@ -0,0 +1,36 @@
1
+ import type { SkelData } from './rig';
2
+ import type { AnimClip } from './anim';
3
+ export interface Discontinuity {
4
+ tick: number;
5
+ index: number;
6
+ jump: number;
7
+ ratio: number;
8
+ }
9
+ export interface ContinuityResult {
10
+ ticks: number;
11
+ components: number;
12
+ violations: Discontinuity[];
13
+ clean: boolean;
14
+ }
15
+ /** Generic PATTERN-3 detector: over a deterministic per-tick numeric trace, flag any component whose
16
+ * SECOND difference (|x[t+1]-2x[t]+x[t-1]|, i.e. acceleration) spikes far above its own median — a
17
+ * pop/teleport. Smooth motion, even fast constant-velocity, has ~zero second difference, so this
18
+ * distinguishes a real discontinuity from fast-but-smooth animation. Localizes to tick + index. */
19
+ export declare function continuityScan(trace: Float32Array[], factor?: number, floor?: number): ContinuityResult;
20
+ /** The animation IR: the deterministic per-tick bone-matrix palette for a clip sampled over `ticks`. */
21
+ export declare function animationPaletteTrace(skel: Required<SkelData>, clip: AnimClip, ticks: number): Float32Array[];
22
+ export interface AnimDiscontinuity {
23
+ clipTime: number;
24
+ joint: number;
25
+ element: number;
26
+ ratio: number;
27
+ }
28
+ export interface AnimContinuity {
29
+ clean: boolean;
30
+ ticks: number;
31
+ duration: number;
32
+ violations: AnimDiscontinuity[];
33
+ }
34
+ /** Scan a clip for animation pops/blinks over its palette IR; each violation is localized to the exact
35
+ * clip-time + joint (+ which palette element). `clean` ⇒ the animation is smooth. */
36
+ export declare function scanAnimationContinuity(skel: Required<SkelData>, clip: AnimClip, ticks?: number, factor?: number, floor?: number): AnimContinuity;
@@ -0,0 +1,55 @@
1
+ // Animation IR + continuity oracle (the moat thesis applied to skinning — shared/moat.ts patterns
2
+ // 1 & 2 are oracle-equivalence and perturbation-sweep; this is PATTERN 3: TEMPORAL CONTINUITY). The
3
+ // per-tick bone palette is a deterministic function of (skeleton, clip, time) — the "animation IR".
4
+ // A well-authored clip is SMOOTH, so any tick whose palette jumps far above that joint's own local
5
+ // motion is a pop/blink — localized to an exact clip-time + joint, a reproducible verdict not a
6
+ // screenshot dispute. This is exactly the class of bug the antipodal-quaternion blink was; wiring it
7
+ // here means a reintroduction is caught automatically instead of on the deployed site.
8
+ import { sampleClipToLocals, forwardKinematics, buildPalette } from './pose';
9
+ /** Generic PATTERN-3 detector: over a deterministic per-tick numeric trace, flag any component whose
10
+ * SECOND difference (|x[t+1]-2x[t]+x[t-1]|, i.e. acceleration) spikes far above its own median — a
11
+ * pop/teleport. Smooth motion, even fast constant-velocity, has ~zero second difference, so this
12
+ * distinguishes a real discontinuity from fast-but-smooth animation. Localizes to tick + index. */
13
+ export function continuityScan(trace, factor = 8, floor = 0.02) {
14
+ const T = trace.length, C = T ? trace[0].length : 0;
15
+ const violations = [];
16
+ if (T < 4)
17
+ return { ticks: T, components: C, violations, clean: true };
18
+ for (let i = 0; i < C; i++) {
19
+ const d2 = new Float64Array(T - 2);
20
+ for (let t = 1; t < T - 1; t++)
21
+ d2[t - 1] = Math.abs(trace[t + 1][i] - 2 * trace[t][i] + trace[t - 1][i]);
22
+ const base = Math.max([...d2].sort((a, b) => a - b)[d2.length >> 1], floor);
23
+ for (let t = 1; t < T - 1; t++) {
24
+ const a = d2[t - 1];
25
+ if (a > floor && a > factor * base)
26
+ violations.push({ tick: t, index: i, jump: a, ratio: a / base });
27
+ }
28
+ }
29
+ return { ticks: T, components: C, violations, clean: violations.length === 0 };
30
+ }
31
+ /** The animation IR: the deterministic per-tick bone-matrix palette for a clip sampled over `ticks`. */
32
+ export function animationPaletteTrace(skel, clip, ticks) {
33
+ const seed = Float64Array.from(skel.rootWorld);
34
+ const out = [];
35
+ for (let t = 0; t < ticks; t++) {
36
+ const time = ticks > 1 ? (t / (ticks - 1)) * clip.duration : 0;
37
+ out.push(buildPalette(forwardKinematics(sampleClipToLocals(clip, time, skel.restLocals), skel.parents, seed), skel.inverseBind));
38
+ }
39
+ return out;
40
+ }
41
+ /** Scan a clip for animation pops/blinks over its palette IR; each violation is localized to the exact
42
+ * clip-time + joint (+ which palette element). `clean` ⇒ the animation is smooth. */
43
+ export function scanAnimationContinuity(skel, clip, ticks = 120, factor = 8, floor = 0.02) {
44
+ const r = continuityScan(animationPaletteTrace(skel, clip, ticks), factor, floor);
45
+ const seen = new Set();
46
+ const violations = [];
47
+ for (const v of r.violations) { // dedupe to one entry per (tick,joint)
48
+ const joint = Math.floor(v.index / 16), key = `${v.tick}:${joint}`;
49
+ if (seen.has(key))
50
+ continue;
51
+ seen.add(key);
52
+ violations.push({ clipTime: ticks > 1 ? (v.tick / (ticks - 1)) * clip.duration : 0, joint, element: v.index % 16, ratio: v.ratio });
53
+ }
54
+ return { clean: violations.length === 0, ticks, duration: clip.duration, violations };
55
+ }
@@ -0,0 +1,20 @@
1
+ import type { JointTRS } from './pose';
2
+ /** A named attachment point on the shared skeleton. A part tagged for this socket's region welds here. */
3
+ export interface Socket {
4
+ id: string;
5
+ jointIndex: number;
6
+ region: string;
7
+ restLocal: JointTRS;
8
+ allowedRegions: string[];
9
+ }
10
+ /** A shared skeleton blueprint. `restLocals`/`rootWorld` drive FK; `inverseBind` is the ORIGINAL bind. */
11
+ export interface Archetype {
12
+ id: string;
13
+ jointCount: number;
14
+ parents: Int16Array;
15
+ inverseBind: Float32Array;
16
+ restLocals: JointTRS[];
17
+ rootWorld: number[];
18
+ sockets: Socket[];
19
+ clipLibrary: string[];
20
+ }
@@ -0,0 +1,8 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // ARCHETYPE — a per-body-plan skeleton "blueprint" (PLATFORM, generic). Every part in a kit welds onto
3
+ // THIS shared skeleton at a named socket, so a finite part library × the shared rig × continuous
4
+ // deformation yields infinite rigged variety (the No Man's Sky method). The skeleton is the joint
5
+ // hierarchy (parents), the bind (inverseBind — kept ORIGINAL so deformed bones push the skin out), the
6
+ // rest pose, and the sockets. Archetypes are authored data; the assembler + retarget consume them.
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ export {};
@@ -0,0 +1,26 @@
1
+ import { type JointTRS } from './pose';
2
+ import type { RawMesh } from './geometry';
3
+ import type { Archetype } from './archetype';
4
+ import { type PartKit } from './partkit';
5
+ import type { Genome } from './genome';
6
+ import { type StyleGuide } from './style';
7
+ export interface RoleRange {
8
+ first: number;
9
+ count: number;
10
+ region: string;
11
+ role: string;
12
+ }
13
+ export interface AssembledMesh {
14
+ mesh: RawMesh;
15
+ jointCount: number;
16
+ parents: Int16Array;
17
+ inverseBind: Float32Array;
18
+ jointsPerVertex: Uint16Array;
19
+ weightsPerVertex: Float32Array;
20
+ restLocals: JointTRS[];
21
+ rootWorld: number[];
22
+ roles: RoleRange[];
23
+ bounds: [number, number, number, number, number, number];
24
+ }
25
+ /** Assemble a rigged creature mesh from a genome. Deterministic + style-conformant. */
26
+ export declare function assemble(arch: Archetype, kit: PartKit, genome: Genome, guide: StyleGuide): AssembledMesh;
@@ -0,0 +1,92 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // ASSEMBLER — the No Man's Sky core (PLATFORM, pure). Welds a genome's chosen parts onto the shared
3
+ // archetype skeleton, applies continuous deformation, style-conforms, and emits ONE rigged mesh. It is
4
+ // a pure function of (archetype, kit, genome, guide) using only +−×÷/sqrt + dmath, so the same genome
5
+ // yields a byte-identical mesh on any engine — cacheable + replayable (the moat). Key trick: proportion
6
+ // deforms the REST pose (bone lengths) while inverseBind stays the ORIGINAL bind, so FK over the
7
+ // deformed rest pushes the skinned mesh out along lengthened bones (and library clips retarget for free).
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ import { composeTRS, invert } from './pose';
10
+ import { mul } from '../mat4';
11
+ import { dsin, dcos } from '../dmath';
12
+ import { resolveKit } from './partkit';
13
+ import { resolveRole } from './style';
14
+ function applyPoint(M, x, y, z) {
15
+ return [M[0] * x + M[4] * y + M[8] * z + M[12], M[1] * x + M[5] * y + M[9] * z + M[13], M[2] * x + M[6] * y + M[10] * z + M[14]];
16
+ }
17
+ /** Assemble a rigged creature mesh from a genome. Deterministic + style-conformant. */
18
+ export function assemble(arch, kit, genome, guide) {
19
+ const selected = resolveKit(kit, genome.parts);
20
+ const prop = genome.proportion;
21
+ // 1) Deform the shared rig's rest translations (bone lengths). Default per-joint scale = [girth,height,
22
+ // girth]; limb sockets add limbLength, head sockets add headScale. inverseBind stays ORIGINAL.
23
+ const sv = arch.restLocals.map(() => [prop.girth, prop.height, prop.girth]);
24
+ for (const s of arch.sockets) {
25
+ const j = s.jointIndex;
26
+ if (j < 0 || j >= sv.length)
27
+ continue;
28
+ const f = s.region === 'limb' ? prop.limbLength : s.region === 'head' ? prop.headScale : 1;
29
+ sv[j] = [sv[j][0] * f, sv[j][1] * f, sv[j][2] * f];
30
+ }
31
+ const restLocals = arch.restLocals.map((r, j) => ({
32
+ t: [r.t[0] * sv[j][0], r.t[1] * sv[j][1], r.t[2] * sv[j][2]],
33
+ q: [r.q[0], r.q[1], r.q[2], r.q[3]], s: [r.s[0], r.s[1], r.s[2]],
34
+ }));
35
+ // 2) Weld each selected part (deterministic socket order) with its per-part deform, concatenating
36
+ // positions/indices/joints/weights into one mesh sharing the archetype's parents + inverseBind.
37
+ const positions = [], indices = [], joints = [], weights = [], roles = [];
38
+ let minx = Infinity, miny = Infinity, minz = Infinity, maxx = -Infinity, maxy = -Infinity, maxz = -Infinity;
39
+ const sockets = [...arch.sockets].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
40
+ for (const s of sockets) {
41
+ const part = selected[s.id];
42
+ if (!part)
43
+ continue;
44
+ const d = genome.partDeform[s.id] ?? { scale: [1, 1, 1], stretch: [1, 1, 1], twist: 0 };
45
+ // socket joint bind position (= translation of the inverse of its inverseBind) → deform pivots here
46
+ const j = s.jointIndex, ib = new Float64Array(16);
47
+ for (let i = 0; i < 16; i++)
48
+ ib[i] = arch.inverseBind[j * 16 + i];
49
+ const bind = invert(ib);
50
+ const bx = bind[12], by = bind[13], bz = bind[14];
51
+ const half = d.twist / 2, qy = [0, dsin(half), 0, dcos(half)];
52
+ const Tp = composeTRS([bx, by, bz], [0, 0, 0, 1], [1, 1, 1]);
53
+ const R = composeTRS([0, 0, 0], qy, [1, 1, 1]);
54
+ const S = composeTRS([0, 0, 0], [0, 0, 0, 1], [d.scale[0] * d.stretch[0], d.scale[1] * d.stretch[1], d.scale[2] * d.stretch[2]]);
55
+ const Tn = composeTRS([-bx, -by, -bz], [0, 0, 0, 1], [1, 1, 1]);
56
+ const W = mul(Tp, mul(R, mul(S, Tn)));
57
+ const base = positions.length / 3, pv = part.mesh.positions.length / 3;
58
+ for (let v = 0; v < pv; v++) {
59
+ const [x, y, z] = applyPoint(W, part.mesh.positions[v * 3], part.mesh.positions[v * 3 + 1], part.mesh.positions[v * 3 + 2]);
60
+ positions.push(x, y, z);
61
+ if (x < minx)
62
+ minx = x;
63
+ if (y < miny)
64
+ miny = y;
65
+ if (z < minz)
66
+ minz = z;
67
+ if (x > maxx)
68
+ maxx = x;
69
+ if (y > maxy)
70
+ maxy = y;
71
+ if (z > maxz)
72
+ maxz = z;
73
+ let ws = 0;
74
+ for (let k = 0; k < 4; k++)
75
+ ws += part.weights[v * 4 + k];
76
+ for (let k = 0; k < 4; k++) {
77
+ joints.push(part.joints[v * 4 + k]);
78
+ weights.push(ws > 0 ? part.weights[v * 4 + k] / ws : (k === 0 ? 1 : 0));
79
+ }
80
+ }
81
+ for (const idx of part.mesh.indices)
82
+ indices.push(idx + base);
83
+ roles.push({ first: base, count: pv, region: part.tag.region, role: resolveRole(guide, part.tag.region) });
84
+ }
85
+ return {
86
+ mesh: { positions: new Float32Array(positions), indices: new Uint32Array(indices) },
87
+ jointCount: arch.jointCount, parents: arch.parents, inverseBind: arch.inverseBind,
88
+ jointsPerVertex: new Uint16Array(joints), weightsPerVertex: new Float32Array(weights),
89
+ restLocals, rootWorld: arch.rootWorld, roles,
90
+ bounds: positions.length ? [minx, miny, minz, maxx, maxy, maxz] : [0, 0, 0, 0, 0, 0],
91
+ };
92
+ }
@@ -0,0 +1,33 @@
1
+ import type { Archetype } from './archetype';
2
+ import { type PartKit } from './partkit';
3
+ export interface PartDeform {
4
+ scale: [number, number, number];
5
+ stretch: [number, number, number];
6
+ twist: number;
7
+ }
8
+ export interface GlobalProportion {
9
+ height: number;
10
+ girth: number;
11
+ limbLength: number;
12
+ headScale: number;
13
+ }
14
+ export interface PaletteChoice {
15
+ guideId: string;
16
+ roleRamp: Record<string, number>;
17
+ }
18
+ export interface Genome {
19
+ archetypeId: string;
20
+ partKitVersion: number;
21
+ seed: number;
22
+ parts: Record<string, string>;
23
+ partDeform: Record<string, PartDeform>;
24
+ proportion: GlobalProportion;
25
+ palette: PaletteChoice;
26
+ }
27
+ /** Sample a full genome from a seed: pick a part per socket, deform it, set global proportion + palette.
28
+ * Per-field seed splits keep draws independent (adding a field can't perturb earlier ones). */
29
+ export declare function sampleGenome(arch: Archetype, kit: PartKit, seed: number): Genome;
30
+ /** Stable canonical string (sorted keys, fixed float precision) → the cache/replay identity. */
31
+ export declare function canonicalizeGenome(g: Genome): string;
32
+ /** Content hash including the kit version (a kit change invalidates caches deterministically). */
33
+ export declare function genomeHash(g: Genome): string;
@@ -0,0 +1,55 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // GENOME — the seeded recipe for one procedural creature (PLATFORM, generic). A genome is a small,
3
+ // canonical, JSON-serializable record referencing parts/roles BY ID (never geometry), so the assembler
4
+ // is a pure function of (genome, partKitVersion): content-hashable, cacheable, and replayable — the
5
+ // moat. Infinite variety = discrete part choice × continuous per-part deform × global proportion ×
6
+ // palette. sampleGenome derives every field from the seed via per-field splits so adding a field can't
7
+ // shift earlier draws. See design/content-architecture.md.
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ import { fnv1a } from '../stateTree';
10
+ import { partsForSocket } from './partkit';
11
+ /** Deterministic mulberry32 (local copy so the platform module has no game-code dependency). */
12
+ function rng(seed) {
13
+ let a = seed >>> 0;
14
+ return () => { a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
15
+ }
16
+ const lerp = (r, lo, hi) => lo + (hi - lo) * r;
17
+ function hashStr(s) { let h = 2166136261 >>> 0; for (let i = 0; i < s.length; i++) {
18
+ h ^= s.charCodeAt(i);
19
+ h = Math.imul(h, 16777619);
20
+ } return h >>> 0; }
21
+ /** Sample a full genome from a seed: pick a part per socket, deform it, set global proportion + palette.
22
+ * Per-field seed splits keep draws independent (adding a field can't perturb earlier ones). */
23
+ export function sampleGenome(arch, kit, seed) {
24
+ const parts = {}, partDeform = {};
25
+ for (const s of arch.sockets) {
26
+ const opts = partsForSocket(kit, s.id, s.allowedRegions);
27
+ const rp = rng(seed ^ Math.imul(hashStr(s.id), 0x9e3779b1) ^ 0x1001);
28
+ const rd = rng(seed ^ Math.imul(hashStr(s.id), 0x85ebca77) ^ 0x2002);
29
+ if (opts.length)
30
+ parts[s.id] = opts[Math.floor(rp() * opts.length) % opts.length].tag.id;
31
+ partDeform[s.id] = {
32
+ scale: [lerp(rd(), 0.85, 1.15), lerp(rd(), 0.85, 1.15), lerp(rd(), 0.85, 1.15)],
33
+ stretch: [lerp(rd(), 0.9, 1.1), lerp(rd(), 0.9, 1.1), lerp(rd(), 0.9, 1.1)],
34
+ twist: lerp(rd(), -0.3, 0.3),
35
+ };
36
+ }
37
+ const rprop = rng(seed ^ 0x3003);
38
+ const proportion = { height: lerp(rprop(), 0.8, 1.3), girth: lerp(rprop(), 0.85, 1.2), limbLength: lerp(rprop(), 0.85, 1.2), headScale: lerp(rprop(), 0.85, 1.25) };
39
+ const rpal = rng(seed ^ 0x4004);
40
+ const roleRamp = {};
41
+ for (const s of arch.sockets)
42
+ roleRamp[s.region] ??= Math.floor(rpal() * 6);
43
+ return { archetypeId: arch.id, partKitVersion: kit.version, seed, parts, partDeform, proportion, palette: { guideId: '', roleRamp } };
44
+ }
45
+ /** Stable canonical string (sorted keys, fixed float precision) → the cache/replay identity. */
46
+ export function canonicalizeGenome(g) {
47
+ const fx = (n) => n.toFixed(6);
48
+ const parts = Object.keys(g.parts).sort().map((k) => `${k}:${g.parts[k]}`).join(',');
49
+ const def = Object.keys(g.partDeform).sort().map((k) => { const d = g.partDeform[k]; return `${k}:${d.scale.map(fx)}|${d.stretch.map(fx)}|${fx(d.twist)}`; }).join(',');
50
+ const p = g.proportion, prop = [p.height, p.girth, p.limbLength, p.headScale].map(fx).join(',');
51
+ const ramp = Object.keys(g.palette.roleRamp).sort().map((k) => `${k}:${g.palette.roleRamp[k]}`).join(',');
52
+ return `${g.archetypeId}#${g.partKitVersion}#${parts}#${def}#${prop}#${g.palette.guideId}:${ramp}`;
53
+ }
54
+ /** Content hash including the kit version (a kit change invalidates caches deterministically). */
55
+ export function genomeHash(g) { return fnv1a(canonicalizeGenome(g) + '|' + String(g.partKitVersion)); }
@@ -0,0 +1,28 @@
1
+ import type { Archetype } from './archetype';
2
+ import type { PartKit } from './partkit';
3
+ import type { StyleGuide } from './style';
4
+ import { type Genome } from './genome';
5
+ import { type AssembledMesh } from './assemble';
6
+ /** The largest VERTICAL gap between an assembled creature's parts (by role vertex-range Y-extents). A
7
+ * big gap means a body region has NO geometry — e.g. dropped mesh primitives (a multi-material sub-mesh
8
+ * silently skipped) or a missing socket. Structural validity + bind correctness both MISS this; it is
9
+ * the mesh-COMPLETENESS check the pipeline lacked (it hid the "floating head / no chest" render bug). */
10
+ export declare function verticalCoverageGap(a: AssembledMesh): number;
11
+ /** How far an archetype's REST pose is from reconstructing its bind (rest palette vs identity). ~0 means
12
+ * skinning at rest leaves the mesh undistorted; a large value means parts will render DISPLACED (floating
13
+ * heads / gaps) even though the mesh is structurally valid. This is the render-correctness check the
14
+ * structural oracle lacked — the debuggability gap that hid the genome rendering bug. */
15
+ export declare function bindReconstructionError(a: Archetype): number;
16
+ export interface GenomeAudit {
17
+ seed: number;
18
+ ok: boolean;
19
+ failures: string[];
20
+ }
21
+ /** Audit a single genome against all three properties. */
22
+ export declare function auditGenome(arch: Archetype, kit: PartKit, guide: StyleGuide, g: Genome): GenomeAudit;
23
+ export interface GenomeSweep {
24
+ audits: GenomeAudit[];
25
+ firstBad: number;
26
+ }
27
+ /** Sample + audit a batch of seeds; firstBad is the index of the first invalid genome (−1 if all ok). */
28
+ export declare function sweep(arch: Archetype, kit: PartKit, guide: StyleGuide, seeds: number[]): GenomeSweep;
@@ -0,0 +1,100 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // GENOME EXPLORER / ORACLE — the moat for procedural creatures (PLATFORM; cf. worldExplore/fpsAiExplore).
3
+ // A headless sweep that asserts, per genome, the three properties that make procedural bulk safe:
4
+ // • DETERMINISM — two independent assembles yield the identical mesh (byte-for-byte).
5
+ // • VALIDITY — watertight-rigged-animatable: sockets filled, finite positions, indices <
6
+ // vertexCount, joints < jointCount, weights sum 1, roles cover every vertex.
7
+ // • CONFORMANCE — every assigned role exists in the StyleGuide (cohesion guaranteed).
8
+ // It localizes the FIRST offending genome, so a broken part (e.g. an out-of-range joint) is caught.
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ import { sampleGenome } from './genome';
11
+ import { assemble } from './assemble';
12
+ import { composeTRS, forwardKinematics, buildPalette } from './pose';
13
+ function toMat(arr) { const m = new Float64Array(16); for (let i = 0; i < 16; i++)
14
+ m[i] = arr[i] ?? 0; return m; }
15
+ /** The largest VERTICAL gap between an assembled creature's parts (by role vertex-range Y-extents). A
16
+ * big gap means a body region has NO geometry — e.g. dropped mesh primitives (a multi-material sub-mesh
17
+ * silently skipped) or a missing socket. Structural validity + bind correctness both MISS this; it is
18
+ * the mesh-COMPLETENESS check the pipeline lacked (it hid the "floating head / no chest" render bug). */
19
+ export function verticalCoverageGap(a) {
20
+ const pos = a.mesh.positions;
21
+ const ranges = a.roles.map((r) => { let lo = Infinity, hi = -Infinity; for (let v = r.first; v < r.first + r.count; v++) {
22
+ const y = pos[v * 3 + 1];
23
+ if (y < lo)
24
+ lo = y;
25
+ if (y > hi)
26
+ hi = y;
27
+ } return [lo, hi]; }).filter((r) => r[0] <= r[1]).sort((p, q) => p[0] - q[0]);
28
+ if (ranges.length < 2)
29
+ return 0;
30
+ let cover = ranges[0][1], maxGap = 0;
31
+ for (let i = 1; i < ranges.length; i++) {
32
+ const g = ranges[i][0] - cover;
33
+ if (g > maxGap)
34
+ maxGap = g;
35
+ if (ranges[i][1] > cover)
36
+ cover = ranges[i][1];
37
+ }
38
+ return maxGap;
39
+ }
40
+ /** How far an archetype's REST pose is from reconstructing its bind (rest palette vs identity). ~0 means
41
+ * skinning at rest leaves the mesh undistorted; a large value means parts will render DISPLACED (floating
42
+ * heads / gaps) even though the mesh is structurally valid. This is the render-correctness check the
43
+ * structural oracle lacked — the debuggability gap that hid the genome rendering bug. */
44
+ export function bindReconstructionError(a) {
45
+ const worlds = forwardKinematics(a.restLocals.map((r) => composeTRS(r.t, r.q, r.s)), a.parents, toMat(a.rootWorld));
46
+ const pal = buildPalette(worlds, a.inverseBind);
47
+ let d = 0;
48
+ for (let j = 0; j < a.jointCount; j++)
49
+ for (let i = 0; i < 16; i++) {
50
+ const id = i === 0 || i === 5 || i === 10 || i === 15 ? 1 : 0;
51
+ d = Math.max(d, Math.abs(pal[j * 16 + i] - id));
52
+ }
53
+ return d;
54
+ }
55
+ /** Audit a single genome against all three properties. */
56
+ export function auditGenome(arch, kit, guide, g) {
57
+ const failures = [];
58
+ try {
59
+ for (const s of arch.sockets)
60
+ if (g.parts[s.id] === undefined)
61
+ failures.push(`socket ${s.id} unfilled`);
62
+ const a = assemble(arch, kit, g, guide);
63
+ const vc = a.mesh.positions.length / 3;
64
+ if (![...a.mesh.positions].every(Number.isFinite))
65
+ failures.push('non-finite positions');
66
+ if ([...a.mesh.indices].some((i) => i >= vc))
67
+ failures.push('index >= vertexCount');
68
+ if ([...a.jointsPerVertex].some((j) => j >= a.jointCount))
69
+ failures.push('joint index out of range');
70
+ let wbad = false;
71
+ for (let v = 0; v < vc && !wbad; v++) {
72
+ let s = 0;
73
+ for (let k = 0; k < 4; k++)
74
+ s += a.weightsPerVertex[v * 4 + k];
75
+ if (Math.abs(s - 1) > 1e-4)
76
+ wbad = true;
77
+ }
78
+ if (wbad)
79
+ failures.push('skin weights do not sum to 1');
80
+ if (a.roles.reduce((t, r) => t + r.count, 0) !== vc)
81
+ failures.push('roles do not cover all vertices');
82
+ if (a.roles.some((r) => guide.roles[r.role] === undefined))
83
+ failures.push('assigned role not in StyleGuide');
84
+ const gap = verticalCoverageGap(a), height = a.bounds[4] - a.bounds[1];
85
+ if (height > 0 && gap > 0.12 * height)
86
+ failures.push(`vertical geometry gap ${gap.toFixed(2)} of ${height.toFixed(2)} tall — a body region has no mesh (dropped primitive / missing part?)`);
87
+ const b = assemble(arch, kit, g, guide);
88
+ if (JSON.stringify([...a.mesh.positions]) !== JSON.stringify([...b.mesh.positions]))
89
+ failures.push('non-deterministic assemble');
90
+ }
91
+ catch (e) {
92
+ failures.push('assemble threw: ' + e.message);
93
+ }
94
+ return { seed: g.seed, ok: failures.length === 0, failures };
95
+ }
96
+ /** Sample + audit a batch of seeds; firstBad is the index of the first invalid genome (−1 if all ok). */
97
+ export function sweep(arch, kit, guide, seeds) {
98
+ const audits = seeds.map((sd) => auditGenome(arch, kit, guide, sampleGenome(arch, kit, sd)));
99
+ return { audits, firstBad: audits.findIndex((a) => !a.ok) };
100
+ }
@@ -0,0 +1,23 @@
1
+ export interface RawMesh {
2
+ positions: Float32Array;
3
+ indices: Uint32Array;
4
+ }
5
+ export declare function compactMesh(positions: Float32Array, indices: Uint32Array, uvs?: Float32Array): {
6
+ positions: Float32Array;
7
+ indices: Uint32Array;
8
+ uvs?: Float32Array;
9
+ };
10
+ export declare function encodeGeometry(m: RawMesh): Promise<{
11
+ vtx: Uint8Array;
12
+ idx: Uint8Array;
13
+ vertexCount: number;
14
+ indexCount: number;
15
+ }>;
16
+ export declare function decodeGeometry(enc: {
17
+ vtx: Uint8Array;
18
+ idx: Uint8Array;
19
+ vertexCount: number;
20
+ indexCount: number;
21
+ }): Promise<RawMesh>;
22
+ export declare function encodeUV(uvs: Float32Array): Promise<Uint8Array>;
23
+ export declare function decodeUV(bytes: Uint8Array, vertexCount: number): Promise<Float32Array>;