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,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
|
+
}
|