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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/bin/clawhouse.js +29 -0
- package/lib/usage.js +568 -0
- package/package.json +30 -0
- package/public/furniture.json +128 -0
- package/public/furniture.png +0 -0
- package/public/icon-180.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon-maskable-512.png +0 -0
- package/public/index.html +3335 -0
- package/public/manifest.webmanifest +16 -0
- package/public/monitors.json +20 -0
- package/public/monitors.png +0 -0
- package/public/props.json +20 -0
- package/public/props.png +0 -0
- package/public/sprites.json +27 -0
- package/public/sprites.png +0 -0
- package/scripts/build-furniture.js +762 -0
- package/scripts/build-monitors.js +496 -0
- package/scripts/build-props.js +675 -0
- package/scripts/build-sprites.js +1065 -0
- package/scripts/export-profile-pics.js +180 -0
- package/scripts/generate-icons.js +124 -0
- package/server.js +944 -0
|
@@ -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`);
|