clawhouse 0.1.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.
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ // Render each agent's idle pose as a square Telegram-friendly profile pic.
3
+ // Reuses the deterministic sprite painter from build-sprites.js.
4
+ //
5
+ // Output: pixel-office/public/profile-pics/<id>.png (768x768 PNG)
6
+ // Usage: node scripts/export-profile-pics.js [agentId] [agentId...]
7
+ // (no args = all agents)
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const {
12
+ Canvas,
13
+ encodePNG,
14
+ drawSprite,
15
+ AGENTS,
16
+ SPRITE_W,
17
+ SPRITE_H,
18
+ } = require('./build-sprites');
19
+
20
+ // Per-agent backdrop colors — pulled from each room's wall theme so the avatar
21
+ // reads as "the agent in their office," not "the agent against a flat color."
22
+ // 4 stops produce a soft top→bottom gradient: sky → wall1 → wall2 → floor.
23
+ const BACKDROPS = {
24
+ main: { sky: '#5a6cb0', top: '#344075', bot: '#252f5d', floor: '#65462f' },
25
+ markethunting: { sky: '#3a7a64', top: '#24483e', bot: '#18332d', floor: '#5a4330' },
26
+ sage: { sky: '#5a7a72', top: '#39514a', bot: '#263831', floor: '#685240' },
27
+ senku: { sky: '#56758a', top: '#37505d', bot: '#223740', floor: '#5a4839' },
28
+ shikamaru: { sky: '#605d8c', top: '#3f3d67', bot: '#292745', floor: '#5f4d3f' },
29
+ tyrion: { sky: '#7a6358', top: '#58473b', bot: '#3c3028', floor: '#5e452f' },
30
+ harvey: { sky: '#6e5666', top: '#4c3945', bot: '#32242c', floor: '#654b3d' },
31
+ l: { sky: '#4a4f6c', top: '#2f334d', bot: '#202334', floor: '#564a41' },
32
+ d: { sky: '#6e526f', top: '#4a364f', bot: '#2f2234', floor: '#593f32' },
33
+ ephraim: { sky: '#586374', top: '#3a4452', bot: '#252c38', floor: '#3e4146' },
34
+ house: { sky: '#5a6c7a', top: '#3e4f5a', bot: '#293841', floor: '#52575e' },
35
+ };
36
+
37
+ const SCALE = 16; // 32x48 → 512x768
38
+ const SPRITE_PX_W = SPRITE_W * SCALE; // 512
39
+ const SPRITE_PX_H = SPRITE_H * SCALE; // 768
40
+ const CANVAS_SIZE = SPRITE_PX_H; // 768x768 — square for Telegram
41
+ const SPRITE_X_OFFSET = (CANVAS_SIZE - SPRITE_PX_W) >> 1; // 128
42
+
43
+ // Idle, frame 0 — calm forward-facing pose, eyes open, no animation phase.
44
+ const STATE_IDX_IDLE = 1;
45
+ const FRAME_IDLE = 0;
46
+
47
+ function hexToRgb(hex) {
48
+ const s = hex.replace('#', '');
49
+ return [
50
+ parseInt(s.slice(0, 2), 16),
51
+ parseInt(s.slice(2, 4), 16),
52
+ parseInt(s.slice(4, 6), 16),
53
+ ];
54
+ }
55
+
56
+ function lerp(a, b, t) { return Math.round(a + (b - a) * t); }
57
+ function lerpRgb(a, b, t) { return [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)]; }
58
+
59
+ function paintBackdrop(out, backdrop) {
60
+ const sky = hexToRgb(backdrop.sky);
61
+ const top = hexToRgb(backdrop.top);
62
+ const bot = hexToRgb(backdrop.bot);
63
+ const floor = hexToRgb(backdrop.floor);
64
+ // 4-stop vertical gradient: sky (0..0.18), wall (0.18..0.78), floor (0.78..1.0)
65
+ for (let y = 0; y < CANVAS_SIZE; y++) {
66
+ const v = y / (CANVAS_SIZE - 1);
67
+ let rgb;
68
+ if (v < 0.18) rgb = lerpRgb(sky, top, v / 0.18);
69
+ else if (v < 0.78) rgb = lerpRgb(top, bot, (v - 0.18) / 0.60);
70
+ else rgb = lerpRgb(bot, floor, (v - 0.78) / 0.22);
71
+ for (let x = 0; x < CANVAS_SIZE; x++) {
72
+ const i = (y * CANVAS_SIZE + x) * 4;
73
+ out[i] = rgb[0];
74
+ out[i + 1] = rgb[1];
75
+ out[i + 2] = rgb[2];
76
+ out[i + 3] = 255;
77
+ }
78
+ }
79
+ }
80
+
81
+ // Draw a faint floor "platform" disc behind the sprite's feet — a soft oval
82
+ // shadow that grounds the character so it doesn't look like it's floating.
83
+ function paintGroundShadow(out) {
84
+ const cx = CANVAS_SIZE >> 1;
85
+ const cy = Math.round(CANVAS_SIZE * 0.82);
86
+ const rx = SPRITE_PX_W * 0.42;
87
+ const ry = SPRITE_PX_W * 0.10;
88
+ for (let dy = -ry; dy <= ry; dy++) {
89
+ for (let dx = -rx; dx <= rx; dx++) {
90
+ const v = (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry);
91
+ if (v > 1) continue;
92
+ const x = Math.round(cx + dx);
93
+ const y = Math.round(cy + dy);
94
+ if (x < 0 || x >= CANVAS_SIZE || y < 0 || y >= CANVAS_SIZE) continue;
95
+ const fall = 1 - v;
96
+ const alpha = fall * 0.45; // soft, peaks at ~45% darken
97
+ const i = (y * CANVAS_SIZE + x) * 4;
98
+ out[i] = Math.round(out[i] * (1 - alpha));
99
+ out[i + 1] = Math.round(out[i + 1] * (1 - alpha));
100
+ out[i + 2] = Math.round(out[i + 2] * (1 - alpha));
101
+ }
102
+ }
103
+ }
104
+
105
+ // Nearest-neighbor scale a 32x48 RGBA buffer into the larger canvas at the
106
+ // given top-left position, honoring source alpha (skip transparent pixels so
107
+ // the backdrop shows through around the sprite silhouette).
108
+ function blitScaled(spriteBuf, out, dstX, dstY) {
109
+ for (let sy = 0; sy < SPRITE_H; sy++) {
110
+ for (let sx = 0; sx < SPRITE_W; sx++) {
111
+ const si = (sy * SPRITE_W + sx) * 4;
112
+ const a = spriteBuf[si + 3];
113
+ if (a === 0) continue;
114
+ const r = spriteBuf[si], g = spriteBuf[si + 1], b = spriteBuf[si + 2];
115
+ // Paint a SCALE x SCALE block
116
+ for (let py = 0; py < SCALE; py++) {
117
+ for (let px = 0; px < SCALE; px++) {
118
+ const dx = dstX + sx * SCALE + px;
119
+ const dy = dstY + sy * SCALE + py;
120
+ if (dx < 0 || dx >= CANVAS_SIZE || dy < 0 || dy >= CANVAS_SIZE) continue;
121
+ const di = (dy * CANVAS_SIZE + dx) * 4;
122
+ if (a === 255) {
123
+ out[di] = r; out[di + 1] = g; out[di + 2] = b; out[di + 3] = 255;
124
+ } else {
125
+ // Premultiplied-style blend onto opaque backdrop
126
+ const t = a / 255;
127
+ out[di] = Math.round(r * t + out[di] * (1 - t));
128
+ out[di + 1] = Math.round(g * t + out[di + 1] * (1 - t));
129
+ out[di + 2] = Math.round(b * t + out[di + 2] * (1 - t));
130
+ out[di + 3] = 255;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ function exportAgent(agent) {
139
+ const backdrop = BACKDROPS[agent.id];
140
+ if (!backdrop) {
141
+ console.warn(`No backdrop for ${agent.id}, skipping`);
142
+ return;
143
+ }
144
+
145
+ // 1) Paint the sprite onto a fresh 32x48 canvas at (0, 0)
146
+ const spriteCanvas = new Canvas(SPRITE_W, SPRITE_H);
147
+ drawSprite(spriteCanvas, 0, 0, agent, 'idle', FRAME_IDLE);
148
+
149
+ // 2) Paint backdrop into the output canvas
150
+ const out = Buffer.alloc(CANVAS_SIZE * CANVAS_SIZE * 4);
151
+ paintBackdrop(out, backdrop);
152
+ paintGroundShadow(out);
153
+
154
+ // 3) Center the sprite horizontally; place it so its feet land near 86% down
155
+ const dstX = SPRITE_X_OFFSET;
156
+ const dstY = Math.round(CANVAS_SIZE * 0.86) - SPRITE_PX_H;
157
+ blitScaled(spriteCanvas.buf, out, dstX, dstY);
158
+
159
+ // 4) Encode and write
160
+ const png = encodePNG(CANVAS_SIZE, CANVAS_SIZE, out);
161
+ const outDir = path.join(__dirname, '..', 'public', 'profile-pics');
162
+ fs.mkdirSync(outDir, { recursive: true });
163
+ const outPath = path.join(outDir, `${agent.id}.png`);
164
+ fs.writeFileSync(outPath, png);
165
+ console.log(`Wrote ${png.length} bytes -> ${outPath}`);
166
+ }
167
+
168
+ // ---- Run ----
169
+ const requested = process.argv.slice(2);
170
+ const targets = requested.length
171
+ ? AGENTS.filter((a) => requested.includes(a.id))
172
+ : AGENTS;
173
+
174
+ if (requested.length && targets.length === 0) {
175
+ console.error(`No matching agents. Known: ${AGENTS.map((a) => a.id).join(', ')}`);
176
+ process.exit(1);
177
+ }
178
+
179
+ for (const agent of targets) exportAgent(agent);
180
+ console.log(`Done. ${targets.length} profile pic(s) generated.`);
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ // One-shot generator for PWA / Apple-touch icons. Emits
3
+ // public/icon-180.png, icon-192.png, icon-512.png, icon-maskable-512.png.
4
+ // Hand-encodes PNG so we don't pull in a dep.
5
+ const fs = require('fs');
6
+ const zlib = require('zlib');
7
+ const path = require('path');
8
+
9
+ const BG = [0x21, 0x27, 0x44, 0xff]; // deep navy (matches office theme)
10
+ const WIN = [0x7d, 0xf9, 0xd0, 0xff]; // cyan window glow (matches accent)
11
+ const ROOF = [0xcd, 0xb7, 0xff, 0xff]; // lavender roof (h2 color)
12
+ const FRAME = [0x0d, 0x10, 0x20, 0xff]; // near-black frame
13
+
14
+ // 24×24 pixelart of a 4-window office building.
15
+ // Legend: . bg, # frame, R roof, W window, F facade
16
+ const ART = [
17
+ '........................',
18
+ '........................',
19
+ '........................',
20
+ '........RRRRRRRRRR......',
21
+ '.......RRRRRRRRRRRR.....',
22
+ '......RRRRRRRRRRRRRR....',
23
+ '......##############....',
24
+ '......#FFFFFFFFFFFF#....',
25
+ '......#FWWFFFFFWWFF#....',
26
+ '......#FWWFFFFFWWFF#....',
27
+ '......#FFFFFFFFFFFF#....',
28
+ '......#FWWFFFFFWWFF#....',
29
+ '......#FWWFFFFFWWFF#....',
30
+ '......#FFFFFFFFFFFF#....',
31
+ '......#FWWFFFFFWWFF#....',
32
+ '......#FWWFFFFFWWFF#....',
33
+ '......#FFFFFFFFFFFF#....',
34
+ '......#FFFFFWWFFFFF#....',
35
+ '......#FFFFFWWFFFFF#....',
36
+ '......##############....',
37
+ '........................',
38
+ '........................',
39
+ '........................',
40
+ '........................',
41
+ ];
42
+ const PALETTE = { '.': BG, '#': FRAME, 'R': ROOF, 'W': WIN, 'F': [0x39, 0x41, 0x6f, 0xff] };
43
+
44
+ function crc32(buf) {
45
+ let c, table = [];
46
+ for (let n = 0; n < 256; n++) {
47
+ c = n;
48
+ for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
49
+ table[n] = c >>> 0;
50
+ }
51
+ let crc = 0xffffffff;
52
+ for (let i = 0; i < buf.length; i++) crc = (table[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8)) >>> 0;
53
+ return (crc ^ 0xffffffff) >>> 0;
54
+ }
55
+ function chunk(type, data) {
56
+ const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
57
+ const typeBuf = Buffer.from(type, 'ascii');
58
+ const crcBuf = Buffer.alloc(4);
59
+ crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
60
+ return Buffer.concat([len, typeBuf, data, crcBuf]);
61
+ }
62
+ function makePng(pixels, w, h) {
63
+ const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
64
+ const ihdr = Buffer.alloc(13);
65
+ ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4);
66
+ ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
67
+ const rows = [];
68
+ for (let y = 0; y < h; y++) {
69
+ rows.push(Buffer.from([0]));
70
+ rows.push(Buffer.from(pixels.slice(y * w * 4, (y + 1) * w * 4)));
71
+ }
72
+ const idat = zlib.deflateSync(Buffer.concat(rows));
73
+ return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]);
74
+ }
75
+
76
+ function renderIcon(targetSize, { padding = 0, maskable = false } = {}) {
77
+ const grid = ART.length;
78
+ const inner = targetSize - padding * 2;
79
+ const scale = Math.floor(inner / grid);
80
+ const renderedSize = scale * grid;
81
+ const offset = Math.floor((targetSize - renderedSize) / 2);
82
+ const pixels = Buffer.alloc(targetSize * targetSize * 4);
83
+ // Fill background first (different for maskable to make a safe-zone bg)
84
+ const bgFill = maskable ? BG : BG;
85
+ for (let i = 0; i < targetSize * targetSize; i++) {
86
+ pixels[i * 4 + 0] = bgFill[0];
87
+ pixels[i * 4 + 1] = bgFill[1];
88
+ pixels[i * 4 + 2] = bgFill[2];
89
+ pixels[i * 4 + 3] = bgFill[3];
90
+ }
91
+ for (let gy = 0; gy < grid; gy++) {
92
+ for (let gx = 0; gx < grid; gx++) {
93
+ const ch = ART[gy][gx];
94
+ const color = PALETTE[ch];
95
+ if (!color || ch === '.') continue;
96
+ for (let dy = 0; dy < scale; dy++) {
97
+ for (let dx = 0; dx < scale; dx++) {
98
+ const x = offset + gx * scale + dx;
99
+ const y = offset + gy * scale + dy;
100
+ const idx = (y * targetSize + x) * 4;
101
+ pixels[idx + 0] = color[0];
102
+ pixels[idx + 1] = color[1];
103
+ pixels[idx + 2] = color[2];
104
+ pixels[idx + 3] = color[3];
105
+ }
106
+ }
107
+ }
108
+ }
109
+ return makePng(pixels, targetSize, targetSize);
110
+ }
111
+
112
+ const outDir = path.resolve(__dirname, '..', 'public');
113
+ const targets = [
114
+ { name: 'icon-180.png', size: 180 }, // apple-touch-icon
115
+ { name: 'icon-192.png', size: 192 }, // PWA standard
116
+ { name: 'icon-512.png', size: 512 }, // PWA standard
117
+ { name: 'icon-maskable-512.png', size: 512, maskable: true }, // safe-zone padded
118
+ ];
119
+ for (const t of targets) {
120
+ const buf = renderIcon(t.size, { padding: t.maskable ? 60 : 0, maskable: t.maskable });
121
+ const out = path.join(outDir, t.name);
122
+ fs.writeFileSync(out, buf);
123
+ console.log(`wrote ${out} (${buf.length} bytes)`);
124
+ }