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,675 @@
1
+ #!/usr/bin/env node
2
+ // Per-agent foreground prop sprite sheet.
3
+ // Output: pixel-office/public/props.png
4
+ // Layout: 1 column x 9 rows of 32x32 sprites.
5
+ // Order matches AGENTS in build-sprites.js so SPRITE_ROWS works for both.
6
+
7
+ const fs = require('fs');
8
+ const zlib = require('zlib');
9
+ const path = require('path');
10
+
11
+ // ---------- PNG encoder ----------
12
+
13
+ const CRC_TABLE = (() => {
14
+ const t = new Uint32Array(256);
15
+ for (let n = 0; n < 256; n++) {
16
+ let c = n;
17
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
18
+ t[n] = c >>> 0;
19
+ }
20
+ return t;
21
+ })();
22
+
23
+ function crc32(buf) {
24
+ let c = 0xffffffff;
25
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
26
+ return (c ^ 0xffffffff) >>> 0;
27
+ }
28
+
29
+ function chunk(type, data) {
30
+ const len = Buffer.alloc(4);
31
+ len.writeUInt32BE(data.length, 0);
32
+ const typeBuf = Buffer.from(type, 'ascii');
33
+ const crcBuf = Buffer.alloc(4);
34
+ crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
35
+ return Buffer.concat([len, typeBuf, data, crcBuf]);
36
+ }
37
+
38
+ function encodePNG(width, height, rgba) {
39
+ const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
40
+ const ihdr = Buffer.alloc(13);
41
+ ihdr.writeUInt32BE(width, 0);
42
+ ihdr.writeUInt32BE(height, 4);
43
+ ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
44
+ const stride = width * 4;
45
+ const raw = Buffer.alloc((stride + 1) * height);
46
+ for (let y = 0; y < height; y++) {
47
+ raw[y * (stride + 1)] = 0;
48
+ rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride);
49
+ }
50
+ const idat = zlib.deflateSync(raw, { level: 9 });
51
+ return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]);
52
+ }
53
+
54
+ // ---------- Canvas ----------
55
+
56
+ function hex(s) {
57
+ const h = s.replace('#', '');
58
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), 255];
59
+ }
60
+ function shade(c, mul) {
61
+ return [Math.max(0, Math.min(255, Math.round(c[0] * mul))),
62
+ Math.max(0, Math.min(255, Math.round(c[1] * mul))),
63
+ Math.max(0, Math.min(255, Math.round(c[2] * mul))),
64
+ c[3]];
65
+ }
66
+
67
+ class Canvas {
68
+ constructor(w, h) { this.w = w; this.h = h; this.buf = Buffer.alloc(w * h * 4); }
69
+ px(x, y, c) {
70
+ if (!c) return;
71
+ if (x < 0 || y < 0 || x >= this.w || y >= this.h) return;
72
+ const i = (y * this.w + x) * 4;
73
+ this.buf[i] = c[0]; this.buf[i+1] = c[1]; this.buf[i+2] = c[2]; this.buf[i+3] = c[3] != null ? c[3] : 255;
74
+ }
75
+ rect(x, y, w, h, c) { for (let yy = 0; yy < h; yy++) for (let xx = 0; xx < w; xx++) this.px(x+xx, y+yy, c); }
76
+ hline(x, y, w, c) { for (let i = 0; i < w; i++) this.px(x+i, y, c); }
77
+ vline(x, y, h, c) { for (let i = 0; i < h; i++) this.px(x, y+i, c); }
78
+ outlineRect(x, y, w, h, c) {
79
+ this.hline(x, y, w, c); this.hline(x, y+h-1, w, c);
80
+ this.vline(x, y, h, c); this.vline(x+w-1, y, h, c);
81
+ }
82
+ }
83
+
84
+ // ---------- Painters ----------
85
+
86
+ const OUTLINE = [10, 14, 26, 255];
87
+ const SIZE = 32;
88
+
89
+ const AGENTS = ['main', 'markethunting', 'sage', 'senku', 'shikamaru', 'tyrion', 'harvey', 'l', 'd', 'ephraim', 'house', 'markethunting2'];
90
+
91
+ function paint(c, ox, oy, agentId) {
92
+ switch (agentId) {
93
+ case 'main': paintWarMap(c, ox, oy); break;
94
+ case 'markethunting': paintTradingChart(c, ox, oy); break;
95
+ case 'sage': paintKettle(c, ox, oy); break;
96
+ case 'senku': paintFlaskRack(c, ox, oy); break;
97
+ case 'shikamaru': paintShogiBoard(c, ox, oy); break;
98
+ case 'tyrion': paintLedgerWine(c, ox, oy); break;
99
+ case 'harvey': paintFilingCabinet(c, ox, oy); break;
100
+ case 'l': paintCandyPile(c, ox, oy); break;
101
+ case 'd': paintCondenserMic(c, ox, oy); break;
102
+ case 'ephraim': paintGymRack(c, ox, oy); break;
103
+ case 'house': paintXrayViewer(c, ox, oy); break;
104
+ case 'markethunting2': paintTradingChart(c, ox, oy); break;
105
+ }
106
+ }
107
+
108
+ // ----- Ephraim: wall-mounted gym rack — pull-up bar (top) + 2-tier dumbbell shelf (below) -----
109
+ // 32x32 layout:
110
+ // y=1..7 pull-up bar mounted on wall brackets, with knurled grip tape sections
111
+ // y=10..30 steel rack backboard with 2 shelves of dumbbells
112
+ // shelf 1 (y≈14..20): large dumbbells — red plates, blue plates
113
+ // shelf 2 (y≈22..29): smaller dumbbells — black plates
114
+ function paintGymRack(c, ox, oy) {
115
+ const metal = hex('#7a7e8a');
116
+ const metalH = hex('#a8acb8');
117
+ const metalD = hex('#3a3e48');
118
+ const bracket = hex('#2a2e38');
119
+ const bar = hex('#1d2230');
120
+ const barH = hex('#3a3f4e');
121
+ const grip = hex('#5a3818');
122
+ const gripD = hex('#3a2410');
123
+ const handle = hex('#15182a');
124
+ const handleH = hex('#3a3f4e');
125
+ const plateR = hex('#c93838'); // red plates
126
+ const plateRD = hex('#7a1d1d');
127
+ const plateB = hex('#3a4a8c'); // blue plates
128
+ const plateBD = hex('#1f2a55');
129
+ const plateK = hex('#1d2230'); // black plates
130
+ const plateKH = hex('#3a3f4e');
131
+ const screw = hex('#d8d8d8');
132
+
133
+ // === Pull-up bar (top) ===
134
+ // Wall brackets (left + right): tall, dark, bolted to wall
135
+ c.rect(ox + 3, oy + 1, 3, 6, bracket);
136
+ c.outlineRect(ox + 3, oy + 1, 3, 6, OUTLINE);
137
+ c.px(ox + 4, oy + 2, screw);
138
+ c.px(ox + 4, oy + 5, screw);
139
+ c.rect(ox + 26, oy + 1, 3, 6, bracket);
140
+ c.outlineRect(ox + 26, oy + 1, 3, 6, OUTLINE);
141
+ c.px(ox + 27, oy + 2, screw);
142
+ c.px(ox + 27, oy + 5, screw);
143
+ // Horizontal bar (between brackets)
144
+ c.rect(ox + 5, oy + 2, 22, 3, bar);
145
+ c.outlineRect(ox + 5, oy + 2, 22, 3, OUTLINE);
146
+ c.hline(ox + 6, oy + 2, 20, barH);
147
+ // Two grip-tape sections (knurled chalk grips)
148
+ c.rect(ox + 9, oy + 2, 4, 3, grip);
149
+ c.outlineRect(ox + 9, oy + 2, 4, 3, OUTLINE);
150
+ c.hline(ox + 9, oy + 3, 4, gripD);
151
+ c.rect(ox + 19, oy + 2, 4, 3, grip);
152
+ c.outlineRect(ox + 19, oy + 2, 4, 3, OUTLINE);
153
+ c.hline(ox + 19, oy + 3, 4, gripD);
154
+
155
+ // === Dumbbell rack (lower) ===
156
+ // Backboard / mounting plate
157
+ c.rect(ox + 2, oy + 11, 28, 19, metal);
158
+ c.outlineRect(ox + 2, oy + 11, 28, 19, OUTLINE);
159
+ c.hline(ox + 3, oy + 12, 26, metalH);
160
+ c.hline(ox + 3, oy + 28, 26, metalD);
161
+ // Bolts at the four corners
162
+ c.px(ox + 4, oy + 12, screw);
163
+ c.px(ox + 27, oy + 12, screw);
164
+ c.px(ox + 4, oy + 28, screw);
165
+ c.px(ox + 27, oy + 28, screw);
166
+ // Shelf divider
167
+ c.hline(ox + 3, oy + 21, 26, metalD);
168
+
169
+ // --- Shelf 1: large dumbbells ---
170
+ // Left dumbbell — red plates
171
+ paintDumbbellLarge(c, ox + 3, oy + 14, plateR, plateRD, handle, handleH);
172
+ // Right dumbbell — blue plates
173
+ paintDumbbellLarge(c, ox + 17, oy + 14, plateB, plateBD, handle, handleH);
174
+
175
+ // --- Shelf 2: smaller dumbbells ---
176
+ paintDumbbellSmall(c, ox + 4, oy + 23, plateK, plateKH, handle);
177
+ paintDumbbellSmall(c, ox + 18, oy + 23, plateK, plateKH, handle);
178
+ }
179
+
180
+ // Helper: 12x6 dumbbell with chunky weight plates and a knurled handle.
181
+ function paintDumbbellLarge(c, ox, oy, plate, plateD, handle, handleH) {
182
+ // Left plate (chunky, 3x6)
183
+ c.rect(ox + 0, oy + 0, 3, 6, plate);
184
+ c.outlineRect(ox + 0, oy + 0, 3, 6, OUTLINE);
185
+ c.vline(ox + 0, oy + 1, 4, plateD);
186
+ // Right plate
187
+ c.rect(ox + 9, oy + 0, 3, 6, plate);
188
+ c.outlineRect(ox + 9, oy + 0, 3, 6, OUTLINE);
189
+ c.vline(ox + 11, oy + 1, 4, plateD);
190
+ // Handle between plates
191
+ c.rect(ox + 3, oy + 2, 6, 2, handle);
192
+ c.outlineRect(ox + 3, oy + 2, 6, 2, OUTLINE);
193
+ c.hline(ox + 3, oy + 2, 6, handleH);
194
+ }
195
+
196
+ // Helper: 10x4 mini dumbbell — round plates, slimmer handle.
197
+ function paintDumbbellSmall(c, ox, oy, plate, plateH, handle) {
198
+ // Left plate (compact, 2x4)
199
+ c.rect(ox + 0, oy + 0, 2, 4, plate);
200
+ c.outlineRect(ox + 0, oy + 0, 2, 4, OUTLINE);
201
+ c.px(ox + 0, oy + 1, plateH);
202
+ // Right plate
203
+ c.rect(ox + 8, oy + 0, 2, 4, plate);
204
+ c.outlineRect(ox + 8, oy + 0, 2, 4, OUTLINE);
205
+ c.px(ox + 9, oy + 1, plateH);
206
+ // Handle
207
+ c.rect(ox + 2, oy + 1, 6, 2, handle);
208
+ c.outlineRect(ox + 2, oy + 1, 6, 2, OUTLINE);
209
+ }
210
+
211
+ // ----- House: wall-mounted X-ray viewer light box with skull silhouette + clipboard hung beside -----
212
+ // 32x32 layout:
213
+ // Left (x≈3..23): glowing X-ray light box with skull X-ray pinned to it
214
+ // Right (x≈25..29): small clipboard with chart paper, hung from a peg
215
+ function paintXrayViewer(c, ox, oy) {
216
+ const frame = hex('#cfd6dc'); // light medical bezel
217
+ const frameD = hex('#7e8590');
218
+ const frameH = hex('#ecf0f4');
219
+ const glow = hex('#dff3ff'); // light box glow (cool white)
220
+ const glowH = hex('#ffffff');
221
+ const film = hex('#1f3140'); // x-ray film background (dark blue-black)
222
+ const filmH = hex('#2c4658');
223
+ const bone = hex('#dee5ee'); // bone luminance
224
+ const boneD = hex('#9aa6b4');
225
+ const screw = hex('#3a3f48');
226
+ const cord = hex('#1d2230');
227
+ const clipMet = hex('#aab2bd');
228
+ const clipBoa = hex('#8c6a3d'); // clipboard wood
229
+ const clipBoaD= hex('#5e4423');
230
+ const paper = hex('#f1ecdb');
231
+ const ink = hex('#243140');
232
+ const red = hex('#c92a2a');
233
+
234
+ // Power cord drooping from top of light box
235
+ c.vline(ox + 4, oy + 0, 3, cord);
236
+ c.px(ox + 5, oy + 2, cord);
237
+
238
+ // Light box outer frame
239
+ c.rect(ox + 2, oy + 3, 22, 26, frame);
240
+ c.outlineRect(ox + 2, oy + 3, 22, 26, OUTLINE);
241
+ c.hline(ox + 3, oy + 4, 20, frameH); // top inner highlight
242
+ c.vline(ox + 3, oy + 5, 23, frameH); // left inner highlight
243
+ c.hline(ox + 3, oy + 27, 20, frameD); // bottom shadow
244
+ c.vline(ox + 22, oy + 5, 22, frameD); // right shadow
245
+ // Mounting screws at the four corners
246
+ c.px(ox + 4, oy + 5, screw);
247
+ c.px(ox + 21, oy + 5, screw);
248
+ c.px(ox + 4, oy + 26, screw);
249
+ c.px(ox + 21, oy + 26, screw);
250
+
251
+ // Glowing light surface (back-lit panel)
252
+ c.rect(ox + 5, oy + 6, 16, 20, glow);
253
+ c.hline(ox + 6, oy + 6, 14, glowH); // top highlight band
254
+ c.hline(ox + 6, oy + 7, 14, glowH);
255
+
256
+ // X-ray film clipped to the panel
257
+ c.rect(ox + 6, oy + 7, 14, 18, film);
258
+ c.outlineRect(ox + 6, oy + 7, 14, 18, OUTLINE);
259
+ c.hline(ox + 7, oy + 8, 12, filmH); // subtle film tone band
260
+ // Tiny clips holding the film at the top
261
+ c.px(ox + 9, oy + 7, clipMet);
262
+ c.px(ox + 16, oy + 7, clipMet);
263
+
264
+ // Skull silhouette (cranium + jaw + eye/nasal cavities) — read as "x-ray" instantly
265
+ // Cranium dome
266
+ c.rect(ox + 9, oy + 10, 8, 6, bone);
267
+ c.px(ox + 8, oy + 11, bone);
268
+ c.px(ox + 17, oy + 11, bone);
269
+ c.px(ox + 8, oy + 12, bone);
270
+ c.px(ox + 17, oy + 12, bone);
271
+ c.px(ox + 9, oy + 9, bone);
272
+ c.px(ox + 16, oy + 9, bone);
273
+ // Cranium edge shading
274
+ c.hline(ox + 9, oy + 15, 8, boneD);
275
+ // Eye sockets (dark holes)
276
+ c.rect(ox + 10, oy + 12, 2, 2, film);
277
+ c.rect(ox + 14, oy + 12, 2, 2, film);
278
+ // Nasal cavity
279
+ c.px(ox + 12, oy + 14, film);
280
+ c.px(ox + 13, oy + 14, film);
281
+ c.px(ox + 12, oy + 15, film);
282
+ // Cheekbones / jaw
283
+ c.rect(ox + 9, oy + 17, 8, 1, bone);
284
+ c.rect(ox + 10, oy + 18, 6, 2, bone);
285
+ c.hline(ox + 10, oy + 19, 6, boneD);
286
+ // Teeth row (tiny vertical bone pips)
287
+ c.px(ox + 11, oy + 20, bone);
288
+ c.px(ox + 12, oy + 20, bone);
289
+ c.px(ox + 13, oy + 20, bone);
290
+ c.px(ox + 14, oy + 20, bone);
291
+ // Top of spine peeking below
292
+ c.rect(ox + 12, oy + 21, 2, 3, bone);
293
+ c.px(ox + 12, oy + 23, boneD);
294
+ c.px(ox + 13, oy + 23, boneD);
295
+
296
+ // === Clipboard hung beside the light box (right side) ===
297
+ // Peg / hook
298
+ c.px(ox + 27, oy + 4, screw);
299
+ c.vline(ox + 27, oy + 5, 1, screw);
300
+ // Clipboard body
301
+ c.rect(ox + 25, oy + 6, 5, 16, clipBoa);
302
+ c.outlineRect(ox + 25, oy + 6, 5, 16, OUTLINE);
303
+ c.vline(ox + 29, oy + 7, 14, clipBoaD);
304
+ // Metal clip at top
305
+ c.rect(ox + 26, oy + 5, 3, 2, clipMet);
306
+ c.outlineRect(ox + 26, oy + 5, 3, 2, OUTLINE);
307
+ // Paper
308
+ c.rect(ox + 26, oy + 8, 3, 12, paper);
309
+ c.outlineRect(ox + 26, oy + 8, 3, 12, OUTLINE);
310
+ // Chart lines on paper (notes / vitals)
311
+ c.hline(ox + 26, oy + 10, 3, ink);
312
+ c.hline(ox + 26, oy + 12, 3, ink);
313
+ c.hline(ox + 26, oy + 14, 3, ink);
314
+ c.hline(ox + 26, oy + 16, 3, ink);
315
+ // A red flag / urgent mark on the chart
316
+ c.px(ox + 28, oy + 18, red);
317
+ c.px(ox + 27, oy + 19, red);
318
+ }
319
+
320
+ // ----- Eagle: war map on a cork/wood board with red pins -----
321
+ function paintWarMap(c, ox, oy) {
322
+ const cork = hex('#a47148');
323
+ const corkD = hex('#7e5430');
324
+ const paper = hex('#e8dcb5');
325
+ const paperD = hex('#bca878');
326
+ const ink = hex('#1d2238');
327
+ const red = hex('#e84a4a');
328
+ // board
329
+ c.rect(ox+2, oy+3, 28, 26, cork);
330
+ // wood texture stripes
331
+ for (let y = oy+5; y < oy+28; y += 4) c.hline(ox+3, y, 26, corkD);
332
+ c.outlineRect(ox+2, oy+3, 28, 26, OUTLINE);
333
+ // paper map
334
+ c.rect(ox+5, oy+6, 22, 18, paper);
335
+ c.outlineRect(ox+5, oy+6, 22, 18, OUTLINE);
336
+ // continents
337
+ c.rect(ox+7, oy+9, 6, 4, paperD);
338
+ c.rect(ox+15, oy+8, 9, 5, paperD);
339
+ c.rect(ox+10, oy+15, 12, 6, paperD);
340
+ // routes
341
+ c.hline(ox+8, oy+11, 14, ink);
342
+ c.vline(ox+12, oy+11, 8, ink);
343
+ c.vline(ox+20, oy+10, 9, ink);
344
+ // red pins
345
+ c.rect(ox+11, oy+10, 2, 2, red);
346
+ c.rect(ox+19, oy+18, 2, 2, red);
347
+ c.rect(ox+24, oy+12, 2, 2, red);
348
+ }
349
+
350
+ // ----- MarketHunting: monitor with green candlestick chart -----
351
+ function paintTradingChart(c, ox, oy) {
352
+ const bezel = hex('#1c2129');
353
+ const screen = hex('#0a1a14');
354
+ const grid = hex('#143b2a');
355
+ const green = hex('#3ee27e');
356
+ const greenD = hex('#1f8a4a');
357
+ const red = hex('#e85b5b');
358
+ const stand = hex('#2a2f3a');
359
+ // monitor body
360
+ c.rect(ox+2, oy+4, 28, 20, bezel);
361
+ c.outlineRect(ox+2, oy+4, 28, 20, OUTLINE);
362
+ // screen
363
+ c.rect(ox+4, oy+6, 24, 16, screen);
364
+ // grid
365
+ for (let x = ox+6; x < ox+28; x += 4) c.vline(x, oy+7, 14, grid);
366
+ for (let y = oy+9; y < oy+22; y += 3) c.hline(ox+5, y, 22, grid);
367
+ // candlesticks (rising trend)
368
+ const candles = [
369
+ { x: 6, top: 17, bot: 20, dir: 'red' },
370
+ { x: 9, top: 14, bot: 18, dir: 'green' },
371
+ { x: 12, top: 12, bot: 16, dir: 'green' },
372
+ { x: 15, top: 13, bot: 15, dir: 'red' },
373
+ { x: 18, top: 10, bot: 14, dir: 'green' },
374
+ { x: 21, top: 8, bot: 12, dir: 'green' },
375
+ { x: 24, top: 9, bot: 11, dir: 'green' },
376
+ ];
377
+ candles.forEach(k => {
378
+ const col = k.dir === 'green' ? green : red;
379
+ const colD = k.dir === 'green' ? greenD : shade(red, 0.6);
380
+ c.vline(ox + k.x + 1, oy + k.top - 1, k.bot - k.top + 3, colD);
381
+ c.rect(ox + k.x, oy + k.top, 3, k.bot - k.top + 1, col);
382
+ });
383
+ // stand + base
384
+ c.rect(ox+14, oy+24, 4, 4, stand);
385
+ c.rect(ox+10, oy+27, 12, 2, stand);
386
+ c.outlineRect(ox+10, oy+27, 12, 2, OUTLINE);
387
+ }
388
+
389
+ // ----- Sage: kettle with steam -----
390
+ function paintKettle(c, ox, oy) {
391
+ const body = hex('#3d6c8a');
392
+ const bodyD = hex('#274c66');
393
+ const metal = hex('#cdd8e0');
394
+ const wood = hex('#7c5438');
395
+ const steam = [240, 240, 255, 200];
396
+ // body (round-ish rectangle)
397
+ c.rect(ox+8, oy+15, 16, 10, body);
398
+ c.rect(ox+9, oy+13, 14, 2, body);
399
+ c.rect(ox+10, oy+25, 12, 1, body);
400
+ c.outlineRect(ox+8, oy+15, 16, 10, OUTLINE);
401
+ c.outlineRect(ox+9, oy+13, 14, 2, OUTLINE);
402
+ c.hline(ox+10, oy+26, 12, OUTLINE);
403
+ // shading
404
+ c.vline(ox+22, oy+15, 10, bodyD);
405
+ c.vline(ox+23, oy+15, 10, bodyD);
406
+ // spout
407
+ c.rect(ox+24, oy+13, 5, 3, body);
408
+ c.outlineRect(ox+24, oy+13, 5, 3, OUTLINE);
409
+ c.rect(ox+27, oy+11, 2, 2, body);
410
+ c.outlineRect(ox+27, oy+11, 2, 2, OUTLINE);
411
+ // lid
412
+ c.rect(ox+12, oy+10, 8, 3, body);
413
+ c.outlineRect(ox+12, oy+10, 8, 3, OUTLINE);
414
+ c.rect(ox+15, oy+8, 2, 2, metal);
415
+ c.outlineRect(ox+15, oy+8, 2, 2, OUTLINE);
416
+ // handle
417
+ c.rect(ox+11, oy+6, 10, 2, wood);
418
+ c.vline(ox+11, oy+7, 4, wood);
419
+ c.vline(ox+20, oy+7, 4, wood);
420
+ c.outlineRect(ox+11, oy+6, 10, 2, OUTLINE);
421
+ // steam puffs (static — animated via CSS later)
422
+ c.rect(ox+13, oy+3, 2, 2, steam);
423
+ c.rect(ox+18, oy+1, 2, 2, steam);
424
+ c.rect(ox+22, oy+4, 2, 2, steam);
425
+ }
426
+
427
+ // ----- Senku: 3 flasks in a wooden rack with bubbles -----
428
+ function paintFlaskRack(c, ox, oy) {
429
+ const wood = hex('#7c5438');
430
+ const woodD = hex('#5a3c25');
431
+ const liquidA = hex('#7df0a0');
432
+ const liquidB = hex('#7ad7ff');
433
+ const liquidC = hex('#ffb863');
434
+ const glass = hex('#dff4ff');
435
+ // rack base
436
+ c.rect(ox+2, oy+24, 28, 4, wood);
437
+ c.outlineRect(ox+2, oy+24, 28, 4, OUTLINE);
438
+ c.hline(ox+3, oy+25, 26, woodD);
439
+ // rack top rail (with holes)
440
+ c.rect(ox+2, oy+13, 28, 3, wood);
441
+ c.outlineRect(ox+2, oy+13, 28, 3, OUTLINE);
442
+ // verticals
443
+ c.rect(ox+2, oy+13, 2, 12, wood);
444
+ c.rect(ox+28, oy+13, 2, 12, wood);
445
+ // 3 flasks: positions 7, 15, 23 (centered)
446
+ const flasks = [
447
+ { x: 7, liquid: liquidA },
448
+ { x: 15, liquid: liquidB },
449
+ { x: 23, liquid: liquidC },
450
+ ];
451
+ flasks.forEach(f => {
452
+ // neck
453
+ c.rect(ox+f.x+1, oy+10, 2, 4, glass);
454
+ // body (triangle-ish via stacked rects)
455
+ c.rect(ox+f.x-1, oy+18, 6, 5, glass);
456
+ c.rect(ox+f.x, oy+16, 4, 2, glass);
457
+ // liquid
458
+ c.rect(ox+f.x-1, oy+20, 6, 3, f.liquid);
459
+ c.rect(ox+f.x, oy+19, 4, 1, f.liquid);
460
+ // outlines
461
+ c.outlineRect(ox+f.x+1, oy+10, 2, 4, OUTLINE);
462
+ c.outlineRect(ox+f.x-1, oy+18, 6, 5, OUTLINE);
463
+ c.outlineRect(ox+f.x, oy+16, 4, 2, OUTLINE);
464
+ // stopper
465
+ c.rect(ox+f.x, oy+8, 4, 2, hex('#caa07a'));
466
+ c.outlineRect(ox+f.x, oy+8, 4, 2, OUTLINE);
467
+ // bubble
468
+ c.rect(ox+f.x+1, oy+18, 1, 1, [255,255,255,200]);
469
+ });
470
+ }
471
+
472
+ // ----- Shikamaru: shogi board on a low table -----
473
+ function paintShogiBoard(c, ox, oy) {
474
+ const board = hex('#d8b780');
475
+ const boardD = hex('#a37e4b');
476
+ const grid = hex('#3a2a1a');
477
+ const piece = hex('#c2924b');
478
+ const pieceK = hex('#403122');
479
+ // table legs
480
+ c.rect(ox+5, oy+22, 3, 8, hex('#5b3d24'));
481
+ c.rect(ox+24, oy+22, 3, 8, hex('#5b3d24'));
482
+ c.outlineRect(ox+5, oy+22, 3, 8, OUTLINE);
483
+ c.outlineRect(ox+24, oy+22, 3, 8, OUTLINE);
484
+ // board top
485
+ c.rect(ox+2, oy+8, 28, 16, board);
486
+ c.outlineRect(ox+2, oy+8, 28, 16, OUTLINE);
487
+ // bottom shadow
488
+ c.hline(ox+3, oy+22, 26, boardD);
489
+ c.hline(ox+3, oy+23, 26, boardD);
490
+ // grid lines
491
+ for (let x = ox+6; x <= ox+26; x += 4) c.vline(x, oy+10, 12, grid);
492
+ for (let y = oy+12; y <= oy+22; y += 4) c.hline(ox+4, y, 24, grid);
493
+ // pieces
494
+ [[8,11],[12,11],[20,15],[16,19],[24,19]].forEach(([px, py]) => {
495
+ c.rect(ox+px, oy+py, 4, 3, piece);
496
+ c.outlineRect(ox+px, oy+py, 4, 3, OUTLINE);
497
+ c.rect(ox+px+1, oy+py+1, 2, 1, pieceK);
498
+ });
499
+ }
500
+
501
+ // ----- Tyrion: stack of ledgers + wine bottle + goblet -----
502
+ function paintLedgerWine(c, ox, oy) {
503
+ const r = hex('#7a1f2c');
504
+ const g = hex('#3f6b3a');
505
+ const b = hex('#3a4a8c');
506
+ const pages = hex('#e8dcb5');
507
+ const gold = hex('#d4a64a');
508
+ const bottle = hex('#1f3a25');
509
+ const cork = hex('#a07050');
510
+ const goblet = hex('#caa54a');
511
+ // ledger 1 (bottom, red)
512
+ c.rect(ox+3, oy+22, 14, 6, r);
513
+ c.outlineRect(ox+3, oy+22, 14, 6, OUTLINE);
514
+ c.hline(ox+4, oy+23, 12, gold);
515
+ c.rect(ox+4, oy+25, 12, 1, pages);
516
+ // ledger 2 (middle, green)
517
+ c.rect(ox+5, oy+16, 13, 6, g);
518
+ c.outlineRect(ox+5, oy+16, 13, 6, OUTLINE);
519
+ c.hline(ox+6, oy+17, 11, gold);
520
+ c.rect(ox+6, oy+19, 11, 1, pages);
521
+ // ledger 3 (top, blue)
522
+ c.rect(ox+4, oy+10, 12, 6, b);
523
+ c.outlineRect(ox+4, oy+10, 12, 6, OUTLINE);
524
+ c.hline(ox+5, oy+11, 10, gold);
525
+ c.rect(ox+5, oy+13, 10, 1, pages);
526
+ // wine bottle on right
527
+ c.rect(ox+22, oy+8, 4, 14, bottle);
528
+ c.rect(ox+23, oy+5, 2, 4, bottle);
529
+ c.outlineRect(ox+22, oy+8, 4, 14, OUTLINE);
530
+ c.outlineRect(ox+23, oy+5, 2, 4, OUTLINE);
531
+ c.rect(ox+23, oy+3, 2, 2, cork);
532
+ c.outlineRect(ox+23, oy+3, 2, 2, OUTLINE);
533
+ // label
534
+ c.rect(ox+22, oy+13, 4, 4, hex('#e8dcb5'));
535
+ c.outlineRect(ox+22, oy+13, 4, 4, OUTLINE);
536
+ c.hline(ox+23, oy+15, 2, OUTLINE);
537
+ // goblet
538
+ c.rect(ox+19, oy+22, 4, 4, goblet);
539
+ c.outlineRect(ox+19, oy+22, 4, 4, OUTLINE);
540
+ c.rect(ox+20, oy+26, 2, 2, goblet);
541
+ c.rect(ox+19, oy+28, 4, 1, goblet);
542
+ c.outlineRect(ox+19, oy+28, 4, 1, OUTLINE);
543
+ // wine in goblet
544
+ c.rect(ox+20, oy+23, 2, 2, r);
545
+ }
546
+
547
+ // ----- Harvey: tall filing cabinet with 3 drawers -----
548
+ function paintFilingCabinet(c, ox, oy) {
549
+ const body = hex('#7a7e8a');
550
+ const bodyD = hex('#52555f');
551
+ const handle = hex('#1d2230');
552
+ const label = hex('#ecd58a');
553
+ // body
554
+ c.rect(ox+6, oy+3, 20, 26, body);
555
+ c.outlineRect(ox+6, oy+3, 20, 26, OUTLINE);
556
+ // top
557
+ c.rect(ox+5, oy+2, 22, 2, bodyD);
558
+ c.outlineRect(ox+5, oy+2, 22, 2, OUTLINE);
559
+ // drawers (3)
560
+ for (let i = 0; i < 3; i++) {
561
+ const yy = oy + 4 + i * 8;
562
+ c.outlineRect(ox+7, yy, 18, 8, OUTLINE);
563
+ // shading
564
+ c.hline(ox+8, yy+7, 16, bodyD);
565
+ // handle
566
+ c.rect(ox+13, yy+3, 6, 2, handle);
567
+ c.outlineRect(ox+13, yy+3, 6, 2, OUTLINE);
568
+ // label slot
569
+ c.rect(ox+9, yy+1, 5, 2, label);
570
+ c.outlineRect(ox+9, yy+1, 5, 2, OUTLINE);
571
+ }
572
+ // base
573
+ c.rect(ox+5, oy+28, 22, 2, bodyD);
574
+ c.outlineRect(ox+5, oy+28, 22, 2, OUTLINE);
575
+ }
576
+
577
+ // ----- L: pile of candy/sweets on the floor -----
578
+ function paintCandyPile(c, ox, oy) {
579
+ const stickColor = hex('#e8dcb5');
580
+ const lollyR = hex('#ff7eb1');
581
+ const lollyB = hex('#7adcff');
582
+ const lollyG = hex('#a6e873');
583
+ const wrap1 = hex('#ffd57a');
584
+ const wrap2 = hex('#ffaa42');
585
+ // floor shadow
586
+ c.rect(ox+2, oy+26, 28, 3, [0,0,0,80]);
587
+ // wrapped candies (twist on ends)
588
+ // candy 1
589
+ c.rect(ox+4, oy+22, 8, 5, wrap1);
590
+ c.outlineRect(ox+4, oy+22, 8, 5, OUTLINE);
591
+ c.rect(ox+2, oy+23, 2, 3, wrap1);
592
+ c.rect(ox+12, oy+23, 2, 3, wrap1);
593
+ // stripe
594
+ c.vline(ox+7, oy+23, 3, wrap2);
595
+ c.vline(ox+9, oy+23, 3, wrap2);
596
+ // candy 2
597
+ c.rect(ox+18, oy+24, 8, 4, hex('#a085ff'));
598
+ c.outlineRect(ox+18, oy+24, 8, 4, OUTLINE);
599
+ c.rect(ox+16, oy+25, 2, 2, hex('#a085ff'));
600
+ c.rect(ox+26, oy+25, 2, 2, hex('#a085ff'));
601
+ // lollipop 1 (red)
602
+ c.rect(ox+10, oy+12, 8, 8, lollyR);
603
+ c.outlineRect(ox+10, oy+12, 8, 8, OUTLINE);
604
+ c.rect(ox+12, oy+14, 2, 2, [255,255,255,200]);
605
+ c.rect(ox+13, oy+20, 2, 6, stickColor);
606
+ c.outlineRect(ox+13, oy+20, 2, 6, OUTLINE);
607
+ // lollipop 2 (blue, smaller, behind)
608
+ c.rect(ox+19, oy+14, 6, 6, lollyB);
609
+ c.outlineRect(ox+19, oy+14, 6, 6, OUTLINE);
610
+ c.rect(ox+21, oy+15, 1, 1, [255,255,255,220]);
611
+ c.rect(ox+21, oy+20, 2, 5, stickColor);
612
+ c.outlineRect(ox+21, oy+20, 2, 5, OUTLINE);
613
+ // lollipop 3 (green, top-left)
614
+ c.rect(ox+3, oy+15, 5, 5, lollyG);
615
+ c.outlineRect(ox+3, oy+15, 5, 5, OUTLINE);
616
+ c.rect(ox+5, oy+20, 1, 4, stickColor);
617
+ }
618
+
619
+ // ----- D: condenser mic with pop filter on a stand -----
620
+ function paintCondenserMic(c, ox, oy) {
621
+ const mic = hex('#dadde3');
622
+ const micD = hex('#888c97');
623
+ const grill = hex('#3a3f4a');
624
+ const stand = hex('#1d2230');
625
+ const filter = hex('#1f1730');
626
+ const filterMesh = hex('#3a2a52');
627
+ // base
628
+ c.rect(ox+10, oy+27, 12, 2, stand);
629
+ c.outlineRect(ox+10, oy+27, 12, 2, OUTLINE);
630
+ // pole
631
+ c.rect(ox+15, oy+12, 2, 15, stand);
632
+ c.outlineRect(ox+15, oy+12, 2, 15, OUTLINE);
633
+ // arm to mic
634
+ c.rect(ox+13, oy+10, 8, 2, stand);
635
+ c.outlineRect(ox+13, oy+10, 8, 2, OUTLINE);
636
+ // mic body (vertical capsule)
637
+ c.rect(ox+18, oy+5, 8, 10, mic);
638
+ c.outlineRect(ox+18, oy+5, 8, 10, OUTLINE);
639
+ // mic shading
640
+ c.vline(ox+24, oy+6, 8, micD);
641
+ // grill detail (horizontal stripes)
642
+ for (let y = oy+6; y < oy+13; y += 2) c.hline(ox+19, y, 6, grill);
643
+ // pop filter (circle to the left of mic)
644
+ c.rect(ox+5, oy+5, 9, 9, filter);
645
+ c.outlineRect(ox+5, oy+5, 9, 9, OUTLINE);
646
+ // mesh dots
647
+ for (let y = oy+6; y < oy+13; y++) {
648
+ for (let x = ox+6; x < ox+13; x++) {
649
+ if ((x + y) % 2 === 0) c.px(x, y, filterMesh);
650
+ }
651
+ }
652
+ // filter neck
653
+ c.rect(ox+13, oy+8, 2, 2, stand);
654
+ c.outlineRect(ox+13, oy+8, 2, 2, OUTLINE);
655
+ }
656
+
657
+ // ---------- Build ----------
658
+
659
+ function build() {
660
+ const W = SIZE;
661
+ const H = SIZE * AGENTS.length;
662
+ const c = new Canvas(W, H);
663
+ AGENTS.forEach((id, row) => paint(c, 0, row * SIZE, id));
664
+ return { png: encodePNG(W, H, c.buf), W, H };
665
+ }
666
+
667
+ const { png, W, H } = build();
668
+ const outPath = path.join(__dirname, '..', 'public', 'props.png');
669
+ fs.writeFileSync(outPath, png);
670
+ fs.writeFileSync(
671
+ path.join(__dirname, '..', 'public', 'props.json'),
672
+ JSON.stringify({ propWidth: SIZE, propHeight: SIZE, rows: AGENTS, sheetWidth: W, sheetHeight: H }, null, 2),
673
+ );
674
+ console.log(`Wrote ${png.length} bytes -> ${outPath}`);
675
+ console.log(`Sheet: ${W}x${H}, ${AGENTS.length} props`);