ugly-game 0.4.0 → 0.5.1

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.
package/dist/gpuShadow.js CHANGED
@@ -70,7 +70,7 @@ const MAIN_WGSL = COMMON_WGSL + /* wgsl */ `
70
70
  @group(0) @binding(2) var shadowMap: texture_depth_2d;
71
71
  @group(0) @binding(3) var shadowSamp: sampler_comparison;
72
72
 
73
- struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) hue: f32, @location(3) emis: f32 };
73
+ struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) hue: f32, @location(3) emis: f32, @location(4) blk: vec3<f32>, @location(5) objN: vec3<f32> };
74
74
 
75
75
  @vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
76
76
  let inst = insts[ii];
@@ -81,6 +81,11 @@ struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>,
81
81
  o.nrm = normalize(qrot(inst.rot, CUBE_NRM[vi] / inst.scl.xyz)); // normal under non-uniform scale + rotation
82
82
  o.hue = inst.pos.w;
83
83
  o.emis = inst.scl.w;
84
+ // OBJECT-space block coord (per-sub-block units, anchored to the model, NOT world) so per-block AO moves +
85
+ // rotates with the instance (ships/creatures/plants) instead of sliding over a world-anchored grid; scale
86
+ // by inst.scl so a merged LOD cube (scl>1) still shows its sub-blocks. objN = pre-rotation face normal.
87
+ o.blk = (CUBE_POS[vi] + vec3(0.5, 0.5, 0.5)) * inst.scl.xyz;
88
+ o.objN = CUBE_NRM[vi];
84
89
  return o;
85
90
  }
86
91
 
@@ -117,12 +122,13 @@ fn blockShade(fx: f32, fz: f32) -> f32 {
117
122
  let ndl = max(dot(N, -normalize(cam.sun.xyz)), 0.0);
118
123
  let base = select(mix(vec3(0.34), hsv(i.hue), 0.62), vec3(0.6, 0.62, 0.66), i.hue < 0.0);
119
124
  let sh = shadowFactor(i.world, ndl);
120
- // in-block coords = the two world axes not along the (axis-aligned) face normal
121
- let an = abs(N);
125
+ // in-block coords = the two OBJECT axes not along the (object-space, axis-aligned) face normal — so the AO
126
+ // pattern is anchored to the model's own block grid and translates/rotates with the instance.
127
+ let an = abs(i.objN);
122
128
  var fx: f32; var fz: f32;
123
- if (an.y >= an.x && an.y >= an.z) { fx = fract(i.world.x); fz = fract(i.world.z); }
124
- else if (an.x >= an.z) { fx = fract(i.world.y); fz = fract(i.world.z); }
125
- else { fx = fract(i.world.x); fz = fract(i.world.y); }
129
+ if (an.y >= an.x && an.y >= an.z) { fx = fract(i.blk.x); fz = fract(i.blk.z); }
130
+ else if (an.x >= an.z) { fx = fract(i.blk.y); fz = fract(i.blk.z); }
131
+ else { fx = fract(i.blk.x); fz = fract(i.blk.y); }
126
132
  let ao = blockShade(fx, fz);
127
133
  let lit = base * (cam.sun.w + cam.sunCol.xyz * ndl * sh) * ao;
128
134
  return vec4(pow(clamp(lit, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0); // gamma
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './dmath';
2
2
  export * from './mat4';
3
3
  export * from './noise';
4
4
  export * from './moat';
5
+ export * from './stateIR';
5
6
  export * from './gpuShadow';
6
7
  export * from './input/resolve';
7
8
  export * from './input/browserBackend';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export * from './dmath';
4
4
  export * from './mat4';
5
5
  export * from './noise';
6
6
  export * from './moat';
7
+ export * from './stateIR';
7
8
  export * from './gpuShadow';
8
9
  export * from './input/resolve';
9
10
  export * from './input/browserBackend';
@@ -0,0 +1,30 @@
1
+ /** A sim/scene that can explain itself to an agent from structured state (not pixels). */
2
+ export interface AgentDebuggable<Snap> {
3
+ /** Typed, JSON-able structured snapshot of the current state (a pure read). */
4
+ snapshot(): Snap;
5
+ /** One-screen play-by-play of the current state, highlighting the actionable "why" (built with Narrator). */
6
+ describe(): string;
7
+ }
8
+ /** Accumulates a readable, sectioned report with FIRST-CLASS warnings — the "why isn't it working?" view.
9
+ * Warnings are recorded both inline (so they appear in the narration) and separately (so an agent can
10
+ * branch on `warnings` without re-parsing the text). Chainable. */
11
+ export declare class Narrator {
12
+ private lines;
13
+ private warns;
14
+ /** Append a raw line. */
15
+ line(text: string): this;
16
+ /** A compact "label: k1 v1 · k2 v2 · …" status line. */
17
+ status(label: string, kv: Record<string, string | number>): this;
18
+ /** A ⚠ warning: shown inline AND collected in `warnings` so an agent can act on it programmatically. */
19
+ warn(text: string): this;
20
+ /** Warn only when `cond` holds. Returns this for chaining either way. */
21
+ warnIf(cond: boolean, text: string): this;
22
+ /** The bare warning strings (no ⚠ prefix), for agent branching. */
23
+ get warnings(): string[];
24
+ /** Was any warning raised? (a quick "is something wrong?" for an agent loop). */
25
+ get healthy(): boolean;
26
+ /** Render the whole report as one string. */
27
+ toString(): string;
28
+ }
29
+ /** Tally names into a "3 miner, 2 smelter" style histogram string, preserving first-seen order. */
30
+ export declare function histogram(names: Iterable<string>): string;
@@ -0,0 +1,43 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // ugly-game STATE IR — "debug from state, not screenshots"
3
+ //
4
+ // A general helper for turning ANY deterministic sim's CURRENT state into (a) a typed, JSON-able
5
+ // structured snapshot and (b) a one-screen, sectioned, WARNING-annotated narration that surfaces the
6
+ // actionable "why isn't it working?" — the view an AI (or a human) reads to diagnose without a picture.
7
+ //
8
+ // Complements the replay IR (ir.ts): that reconstructs state over TIME by re-running the sim; this
9
+ // explains a SINGLE state readably, right now. A game exposes `AgentDebuggable` (snapshot + describe);
10
+ // `Narrator` + `histogram` are the tools its describe() is built from. Graduated from the Cogsworth
11
+ // factory game, where it caught starved crafters, unpowered machines, and idle logistics at a glance.
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ /** Accumulates a readable, sectioned report with FIRST-CLASS warnings — the "why isn't it working?" view.
14
+ * Warnings are recorded both inline (so they appear in the narration) and separately (so an agent can
15
+ * branch on `warnings` without re-parsing the text). Chainable. */
16
+ export class Narrator {
17
+ lines = [];
18
+ warns = [];
19
+ /** Append a raw line. */
20
+ line(text) { this.lines.push(text); return this; }
21
+ /** A compact "label: k1 v1 · k2 v2 · …" status line. */
22
+ status(label, kv) {
23
+ this.lines.push(`${label}: ${Object.entries(kv).map(([k, v]) => `${k} ${v}`).join(' · ')}`);
24
+ return this;
25
+ }
26
+ /** A ⚠ warning: shown inline AND collected in `warnings` so an agent can act on it programmatically. */
27
+ warn(text) { this.warns.push(text); this.lines.push(`⚠ ${text}`); return this; }
28
+ /** Warn only when `cond` holds. Returns this for chaining either way. */
29
+ warnIf(cond, text) { return cond ? this.warn(text) : this; }
30
+ /** The bare warning strings (no ⚠ prefix), for agent branching. */
31
+ get warnings() { return this.warns.slice(); }
32
+ /** Was any warning raised? (a quick "is something wrong?" for an agent loop). */
33
+ get healthy() { return this.warns.length === 0; }
34
+ /** Render the whole report as one string. */
35
+ toString() { return this.lines.join('\n'); }
36
+ }
37
+ /** Tally names into a "3 miner, 2 smelter" style histogram string, preserving first-seen order. */
38
+ export function histogram(names) {
39
+ const m = new Map();
40
+ for (const n of names)
41
+ m.set(n, (m.get(n) ?? 0) + 1);
42
+ return [...m].map(([k, v]) => `${v} ${k}`).join(', ');
43
+ }
package/package.json CHANGED
@@ -1,14 +1,34 @@
1
1
  {
2
2
  "name": "ugly-game",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
7
- "./*": { "types": "./dist/*.d.ts", "import": "./dist/*.js" }
8
- },
9
- "files": ["dist"],
10
- "scripts": { "build": "tsc -p tsconfig.json", "prepublishOnly": "tsc -p tsconfig.json" },
11
- "dependencies": { "meshoptimizer": "^1.2.0" },
12
- "peerDependencies": { "react": ">=18" },
13
- "devDependencies": { "typescript": "^5.6.0", "@types/react": "^19.0.0", "@webgpu/types": "^0.1.40" }
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js"
9
+ },
10
+ "./*": {
11
+ "types": "./dist/*.d.ts",
12
+ "import": "./dist/*.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "preinstall": "npx only-allow pnpm",
20
+ "build": "tsc -p tsconfig.json",
21
+ "prepublishOnly": "tsc -p tsconfig.json"
22
+ },
23
+ "dependencies": {
24
+ "meshoptimizer": "^1.2.0"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.6.0",
31
+ "@types/react": "^19.0.0",
32
+ "@webgpu/types": "^0.1.40"
33
+ }
14
34
  }