@clawlabz/clawskin 1.0.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/CHANGELOG.md +98 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/docs/ARCHITECTURE.md +132 -0
- package/docs/PROGRESS.md +15 -0
- package/docs/ROADMAP.md +130 -0
- package/package.json +27 -0
- package/public/_headers +13 -0
- package/public/_redirects +1 -0
- package/public/app.html +595 -0
- package/public/css/app.css +379 -0
- package/public/css/style.css +415 -0
- package/public/index.html +324 -0
- package/public/js/app/AgentSlot.js +596 -0
- package/public/js/app/AgentStateMapper.js +213 -0
- package/public/js/app/ClawSkinApp.js +551 -0
- package/public/js/app/ConnectionPanel.js +142 -0
- package/public/js/app/DeviceIdentity.js +118 -0
- package/public/js/app/GatewayClient.js +284 -0
- package/public/js/app/SettingsManager.js +47 -0
- package/public/js/character/AnimationManager.js +69 -0
- package/public/js/character/BubbleManager.js +157 -0
- package/public/js/character/CharacterSprite.js +98 -0
- package/public/js/game.js +116 -0
- package/public/js/pets/Pet.js +303 -0
- package/public/js/pets/PetManager.js +102 -0
- package/public/js/scenes/CafeScene.js +594 -0
- package/public/js/scenes/HackerScene.js +527 -0
- package/public/js/scenes/OfficeScene.js +404 -0
- package/public/js/sprites/SpriteGenerator.js +927 -0
- package/public/js/state/AgentStateSync.js +89 -0
- package/public/js/state/DemoMode.js +65 -0
- package/public/js/ui/CharacterEditor.js +145 -0
- package/public/js/ui/ScenePicker.js +29 -0
- package/serve.cjs +132 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HackerScene.js — Dark hacker den with dual monitors, code rain, neon glow
|
|
3
|
+
* Architecture matches OfficeScene: getWorkstations, renderChair, renderDesk
|
|
4
|
+
*
|
|
5
|
+
* Render order (back to front):
|
|
6
|
+
* 1. Background wall + code rain + neon pulse + decorations
|
|
7
|
+
* 2. Per agent: gaming chair → character → dark desk (hides legs) → monitors + energy drink
|
|
8
|
+
* 3. Name tags + bubbles on top
|
|
9
|
+
*/
|
|
10
|
+
class HackerScene {
|
|
11
|
+
constructor(canvas, ctx, spriteGen) {
|
|
12
|
+
this.canvas = canvas;
|
|
13
|
+
this.ctx = ctx;
|
|
14
|
+
this.gen = spriteGen;
|
|
15
|
+
this.bgCanvas = null;
|
|
16
|
+
this.codeRainDrops = [];
|
|
17
|
+
this.neonPulse = 0;
|
|
18
|
+
this.screenGlitch = 0;
|
|
19
|
+
this.name = 'hacker';
|
|
20
|
+
this.label = '💻 Hacker Den';
|
|
21
|
+
this.workstations = [];
|
|
22
|
+
this.poiList = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
init() {
|
|
26
|
+
this.bgCanvas = this.gen.generateSceneBg('hacker', this.canvas.width, this.canvas.height);
|
|
27
|
+
this.codeRainDrops = [];
|
|
28
|
+
for (let i = 0; i < 40; i++) {
|
|
29
|
+
this.codeRainDrops.push({
|
|
30
|
+
x: Math.random() * this.canvas.width,
|
|
31
|
+
y: Math.random() * this.canvas.height,
|
|
32
|
+
speed: 0.5 + Math.random() * 2,
|
|
33
|
+
char: String.fromCharCode(0x30A0 + Math.floor(Math.random() * 96)),
|
|
34
|
+
opacity: 0.1 + Math.random() * 0.3,
|
|
35
|
+
size: 8 + Math.floor(Math.random() * 4),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getCharacterPosition() {
|
|
41
|
+
return { x: this.canvas.width / 2 - 48, y: this.canvas.height * 0.38 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calculate workstation layout for N agents.
|
|
46
|
+
* Hacker desks: wide dark desks with dual monitors.
|
|
47
|
+
*/
|
|
48
|
+
getWorkstations(count) {
|
|
49
|
+
const w = this.canvas.width;
|
|
50
|
+
const h = this.canvas.height;
|
|
51
|
+
const floorY = Math.round(h * 0.40);
|
|
52
|
+
const stations = [];
|
|
53
|
+
|
|
54
|
+
const cs = 2.5;
|
|
55
|
+
const charH = Math.round(32 * cs);
|
|
56
|
+
const charW = Math.round(32 * cs);
|
|
57
|
+
|
|
58
|
+
const deskW = 130, deskH = 42;
|
|
59
|
+
const monW = 44, monH = 44;
|
|
60
|
+
const chairW = 36, chairH = 40;
|
|
61
|
+
const cupW = 16, cupH = 20;
|
|
62
|
+
|
|
63
|
+
const deskSurfaceY = floorY + 50;
|
|
64
|
+
|
|
65
|
+
if (count <= 0) return stations;
|
|
66
|
+
|
|
67
|
+
const positions = [];
|
|
68
|
+
if (count === 1) {
|
|
69
|
+
positions.push(w / 2);
|
|
70
|
+
} else if (count <= 4) {
|
|
71
|
+
for (let i = 0; i < count; i++) {
|
|
72
|
+
positions.push((w / (count + 1)) * (i + 1));
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
const topN = Math.ceil(count / 2);
|
|
76
|
+
const botN = count - topN;
|
|
77
|
+
for (let i = 0; i < topN; i++) positions.push((w / (topN + 1)) * (i + 1));
|
|
78
|
+
for (let i = 0; i < botN; i++) positions.push((w / (botN + 1)) * (i + 1));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < count; i++) {
|
|
82
|
+
const cx = Math.round(positions[i]);
|
|
83
|
+
const isBackRow = count > 4 && i >= Math.ceil(count / 2);
|
|
84
|
+
const rowDeskY = isBackRow ? deskSurfaceY + 70 : deskSurfaceY;
|
|
85
|
+
|
|
86
|
+
const charY = rowDeskY - charH + 10;
|
|
87
|
+
const deskY = rowDeskY;
|
|
88
|
+
|
|
89
|
+
stations.push({
|
|
90
|
+
charX: cx - charW / 2,
|
|
91
|
+
charY,
|
|
92
|
+
charScale: cs,
|
|
93
|
+
|
|
94
|
+
chairX: cx - chairW / 2,
|
|
95
|
+
chairY: charY + charH * 0.3,
|
|
96
|
+
chairW, chairH,
|
|
97
|
+
|
|
98
|
+
deskX: cx - deskW / 2,
|
|
99
|
+
deskY,
|
|
100
|
+
deskW, deskH,
|
|
101
|
+
|
|
102
|
+
monX: cx - deskW / 2 + 6,
|
|
103
|
+
monY: deskY - monH + 10,
|
|
104
|
+
monW, monH,
|
|
105
|
+
|
|
106
|
+
cupX: cx + deskW / 2 - cupW - 12,
|
|
107
|
+
cupY: deskY - cupH + 8,
|
|
108
|
+
cupW, cupH,
|
|
109
|
+
|
|
110
|
+
cx, cy: rowDeskY,
|
|
111
|
+
index: i,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Points of interest for agent wandering
|
|
116
|
+
const floorH = h - floorY;
|
|
117
|
+
this.poiList = [
|
|
118
|
+
{ x: 80, y: floorY + 20, label: 'server_rack' },
|
|
119
|
+
{ x: w - 90, y: floorY + 20, label: 'server_rack' },
|
|
120
|
+
{ x: w / 2, y: 30, label: 'led_wall' },
|
|
121
|
+
{ x: 80, y: floorY + floorH * 0.55, label: 'bean_bag' },
|
|
122
|
+
{ x: w - 90, y: floorY + floorH * 0.45, label: 'mini_fridge' },
|
|
123
|
+
{ x: w / 2, y: floorY + floorH * 0.70, label: 'corner' },
|
|
124
|
+
{ x: 90, y: floorY + floorH * 0.30, label: 'cable_corner' },
|
|
125
|
+
{ x: w - 90, y: floorY + floorH * 0.68, label: 'arcade' },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// Collision obstacles
|
|
129
|
+
this.obstacles = [
|
|
130
|
+
...stations.map(s => ({ x: s.deskX - 5, y: s.deskY - 5, w: s.deskW + 10, h: s.deskH + 10, deskIndex: s.index })),
|
|
131
|
+
{ x: 0, y: floorY + 5, w: 30, h: 90 }, // left server rack
|
|
132
|
+
{ x: 0, y: floorY + floorH * 0.46, w: 45, h: 45 }, // left bean bag
|
|
133
|
+
{ x: w-28, y: floorY + 5, w: 28, h: 55 }, // right fridge
|
|
134
|
+
{ x: w-30, y: floorY + floorH * 0.30, w: 30, h: 70 }, // right arcade
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
this.workstations = stations;
|
|
138
|
+
return stations;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
update(dt) {
|
|
142
|
+
this.neonPulse += dt * 0.003;
|
|
143
|
+
this.screenGlitch += dt;
|
|
144
|
+
this.codeRainDrops.forEach(d => {
|
|
145
|
+
d.y += d.speed;
|
|
146
|
+
if (d.y > this.canvas.height) {
|
|
147
|
+
d.y = -10;
|
|
148
|
+
d.x = Math.random() * this.canvas.width;
|
|
149
|
+
d.char = String.fromCharCode(0x30A0 + Math.floor(Math.random() * 96));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Render background + static decorations */
|
|
155
|
+
render(ctx) {
|
|
156
|
+
const w = this.canvas.width;
|
|
157
|
+
const h = this.canvas.height;
|
|
158
|
+
|
|
159
|
+
if (this.bgCanvas) ctx.drawImage(this.bgCanvas, 0, 0);
|
|
160
|
+
|
|
161
|
+
// ── Code rain (behind everything) ──
|
|
162
|
+
ctx.save();
|
|
163
|
+
ctx.globalAlpha = 0.5;
|
|
164
|
+
this.codeRainDrops.forEach(d => {
|
|
165
|
+
ctx.fillStyle = `rgba(0, 255, 65, ${d.opacity})`;
|
|
166
|
+
ctx.font = `${d.size}px monospace`;
|
|
167
|
+
ctx.fillText(d.char, d.x, d.y);
|
|
168
|
+
});
|
|
169
|
+
ctx.restore();
|
|
170
|
+
|
|
171
|
+
// ── Neon ambient pulse ──
|
|
172
|
+
const pulse = Math.sin(this.neonPulse) * 0.03 + 0.05;
|
|
173
|
+
ctx.fillStyle = `rgba(155, 89, 182, ${pulse})`;
|
|
174
|
+
ctx.fillRect(0, 0, w, h);
|
|
175
|
+
|
|
176
|
+
const floorY = Math.round(h * 0.40);
|
|
177
|
+
const floorH = h - floorY;
|
|
178
|
+
const wallSafe = Math.round(h * 0.12);
|
|
179
|
+
|
|
180
|
+
// ── Wall decorations ──
|
|
181
|
+
this._drawHexDisplay(ctx, 25, wallSafe, 65, 38);
|
|
182
|
+
this._drawNeonSign(ctx, w / 2 - 30, wallSafe - 5);
|
|
183
|
+
this._drawLEDPanel(ctx, w - 95, wallSafe, 60, 35);
|
|
184
|
+
this._drawClock(ctx, w - 30, wallSafe + 8);
|
|
185
|
+
|
|
186
|
+
// ── LEFT WALL: server rack + bean bag ──
|
|
187
|
+
this._drawSideServerRack(ctx, 0, floorY + 8, 25, 88, 'left');
|
|
188
|
+
this._drawSideBeanBag(ctx, 0, floorY + floorH * 0.48, 40, 38, 'left');
|
|
189
|
+
|
|
190
|
+
// ── RIGHT WALL: mini fridge + arcade cabinet ──
|
|
191
|
+
this._drawSideMiniFridge(ctx, w - 22, floorY + 5, 22, 52, 'right');
|
|
192
|
+
this._drawSideArcade(ctx, w - 28, floorY + floorH * 0.30, 28, 68, 'right');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Render gaming chair (behind character) */
|
|
196
|
+
renderChair(ctx, s) {
|
|
197
|
+
const x = s.chairX, y = s.chairY, cw = s.chairW, ch = s.chairH;
|
|
198
|
+
// Gaming chair — black with cyan accent stripe
|
|
199
|
+
// Seat base
|
|
200
|
+
ctx.fillStyle = '#1a1a2e';
|
|
201
|
+
ctx.fillRect(x + 2, y + ch * 0.35, cw - 4, ch * 0.45);
|
|
202
|
+
// Back rest (tall)
|
|
203
|
+
ctx.fillStyle = '#111128';
|
|
204
|
+
ctx.fillRect(x + 4, y, cw - 8, ch * 0.55);
|
|
205
|
+
// Top of back rest — rounded
|
|
206
|
+
ctx.fillStyle = '#111128';
|
|
207
|
+
ctx.fillRect(x + 6, y - 4, cw - 12, 6);
|
|
208
|
+
// Cyan racing stripe
|
|
209
|
+
ctx.fillStyle = '#00e5ff';
|
|
210
|
+
ctx.fillRect(x + cw / 2 - 2, y + 2, 4, ch * 0.50);
|
|
211
|
+
ctx.globalAlpha = 0.3;
|
|
212
|
+
ctx.fillStyle = '#00e5ff';
|
|
213
|
+
ctx.fillRect(x + cw / 2 - 4, y + 2, 8, ch * 0.50);
|
|
214
|
+
ctx.globalAlpha = 1;
|
|
215
|
+
// Arm rests
|
|
216
|
+
ctx.fillStyle = '#222240';
|
|
217
|
+
ctx.fillRect(x, y + ch * 0.35, 4, ch * 0.25);
|
|
218
|
+
ctx.fillRect(x + cw - 4, y + ch * 0.35, 4, ch * 0.25);
|
|
219
|
+
// Wheel base
|
|
220
|
+
ctx.fillStyle = '#333';
|
|
221
|
+
ctx.fillRect(x + 4, y + ch - 4, cw - 8, 4);
|
|
222
|
+
// Wheels
|
|
223
|
+
ctx.fillStyle = '#444';
|
|
224
|
+
ctx.fillRect(x + 2, y + ch - 2, 4, 3);
|
|
225
|
+
ctx.fillRect(x + cw - 6, y + ch - 2, 4, 3);
|
|
226
|
+
ctx.fillRect(x + cw / 2 - 2, y + ch - 1, 4, 2);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Render dark desk + dual monitors + RGB keyboard + energy drink */
|
|
230
|
+
renderDesk(ctx, s, agentState) {
|
|
231
|
+
const isWorking = ['typing', 'executing', 'browsing'].includes(agentState);
|
|
232
|
+
|
|
233
|
+
// ── Dark desk ──
|
|
234
|
+
// Top surface (carbon fiber look)
|
|
235
|
+
ctx.fillStyle = '#1a1a2e';
|
|
236
|
+
ctx.fillRect(s.deskX, s.deskY, s.deskW, 8);
|
|
237
|
+
// Highlight strip on top edge
|
|
238
|
+
ctx.fillStyle = '#252545';
|
|
239
|
+
ctx.fillRect(s.deskX, s.deskY, s.deskW, 2);
|
|
240
|
+
// Front face
|
|
241
|
+
ctx.fillStyle = '#111125';
|
|
242
|
+
ctx.fillRect(s.deskX, s.deskY + 8, s.deskW, s.deskH - 8);
|
|
243
|
+
// Side edges
|
|
244
|
+
ctx.fillStyle = '#0d0d1e';
|
|
245
|
+
ctx.fillRect(s.deskX, s.deskY + 8, 2, s.deskH - 8);
|
|
246
|
+
ctx.fillRect(s.deskX + s.deskW - 2, s.deskY + 8, 2, s.deskH - 8);
|
|
247
|
+
// Bottom edge
|
|
248
|
+
ctx.fillStyle = '#0a0a18';
|
|
249
|
+
ctx.fillRect(s.deskX + 2, s.deskY + s.deskH - 2, s.deskW - 4, 2);
|
|
250
|
+
// Legs
|
|
251
|
+
ctx.fillStyle = '#0d0d1e';
|
|
252
|
+
ctx.fillRect(s.deskX + 6, s.deskY + s.deskH, 4, 4);
|
|
253
|
+
ctx.fillRect(s.deskX + s.deskW - 10, s.deskY + s.deskH, 4, 4);
|
|
254
|
+
|
|
255
|
+
// ── Neon underglow ──
|
|
256
|
+
const neonAlpha = 0.35 + Math.sin(this.neonPulse) * 0.2;
|
|
257
|
+
ctx.fillStyle = `rgba(0, 229, 255, ${neonAlpha})`;
|
|
258
|
+
ctx.fillRect(s.deskX + 4, s.deskY + s.deskH - 1, s.deskW - 8, 1);
|
|
259
|
+
// Underglow reflection on floor
|
|
260
|
+
ctx.fillStyle = `rgba(0, 229, 255, ${neonAlpha * 0.15})`;
|
|
261
|
+
ctx.fillRect(s.deskX + 10, s.deskY + s.deskH + 2, s.deskW - 20, 6);
|
|
262
|
+
|
|
263
|
+
// ── Dual monitors ──
|
|
264
|
+
const monType = isWorking && Math.floor(this.screenGlitch / 400) % 2 === 0 ? 'laptop_active' : 'laptop';
|
|
265
|
+
// Left monitor
|
|
266
|
+
ctx.drawImage(this.gen.generateFurniture(monType), s.monX, s.monY, s.monW, s.monH);
|
|
267
|
+
// Right monitor
|
|
268
|
+
const mon2X = s.monX + 50;
|
|
269
|
+
ctx.drawImage(this.gen.generateFurniture(monType), mon2X, s.monY, s.monW, s.monH);
|
|
270
|
+
|
|
271
|
+
// Screen glow when working
|
|
272
|
+
if (isWorking) {
|
|
273
|
+
ctx.fillStyle = 'rgba(0,255,100,0.04)';
|
|
274
|
+
ctx.fillRect(s.monX - 6, s.monY - 3, s.monW + 60, s.monH + 6);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── RGB keyboard between monitors ──
|
|
278
|
+
const kbX = s.deskX + s.deskW / 2 - 22;
|
|
279
|
+
const kbY = s.deskY - 8;
|
|
280
|
+
ctx.fillStyle = '#111';
|
|
281
|
+
ctx.fillRect(kbX, kbY, 44, 10);
|
|
282
|
+
ctx.fillStyle = '#1a1a1a';
|
|
283
|
+
ctx.fillRect(kbX + 1, kbY + 1, 42, 8);
|
|
284
|
+
// Rainbow key LEDs
|
|
285
|
+
for (let k = 0; k < 9; k++) {
|
|
286
|
+
const hue = (this.neonPulse * 50 + k * 40) % 360;
|
|
287
|
+
ctx.fillStyle = `hsl(${hue}, 80%, 50%)`;
|
|
288
|
+
ctx.fillRect(kbX + 3 + k * 4.5, kbY + 3, 3, 2);
|
|
289
|
+
ctx.fillRect(kbX + 3 + k * 4.5, kbY + 6, 3, 2);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Energy drink (instead of coffee) ──
|
|
293
|
+
const edX = s.cupX, edY = s.cupY;
|
|
294
|
+
// Can body
|
|
295
|
+
ctx.fillStyle = '#111';
|
|
296
|
+
ctx.fillRect(edX, edY, s.cupW - 2, s.cupH);
|
|
297
|
+
ctx.fillStyle = '#00e676';
|
|
298
|
+
ctx.fillRect(edX + 1, edY + 3, s.cupW - 4, s.cupH - 6);
|
|
299
|
+
// Label
|
|
300
|
+
ctx.fillStyle = '#004d40';
|
|
301
|
+
ctx.fillRect(edX + 2, edY + 5, s.cupW - 6, 4);
|
|
302
|
+
// Lightning bolt
|
|
303
|
+
ctx.fillStyle = '#ffea00';
|
|
304
|
+
ctx.fillRect(edX + 5, edY + 4, 2, 3);
|
|
305
|
+
ctx.fillRect(edX + 4, edY + 7, 2, 3);
|
|
306
|
+
ctx.fillRect(edX + 6, edY + 6, 2, 2);
|
|
307
|
+
// Can top
|
|
308
|
+
ctx.fillStyle = '#333';
|
|
309
|
+
ctx.fillRect(edX + 1, edY, s.cupW - 4, 3);
|
|
310
|
+
ctx.fillStyle = '#555';
|
|
311
|
+
ctx.fillRect(edX + 3, edY + 1, 4, 1);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Wall decorations ──
|
|
315
|
+
|
|
316
|
+
_drawHexDisplay(ctx, x, y, w, h) {
|
|
317
|
+
// Dark monitor showing hex code on wall
|
|
318
|
+
ctx.fillStyle = '#111';
|
|
319
|
+
ctx.fillRect(x - 2, y - 2, w + 4, h + 4);
|
|
320
|
+
ctx.fillStyle = '#0a0a1a';
|
|
321
|
+
ctx.fillRect(x, y, w, h);
|
|
322
|
+
// Hex lines
|
|
323
|
+
ctx.font = '5px monospace';
|
|
324
|
+
const colors = ['#0f0', '#0a0', '#080', '#0d0'];
|
|
325
|
+
for (let line = 0; line < 6; line++) {
|
|
326
|
+
ctx.fillStyle = colors[line % colors.length];
|
|
327
|
+
let txt = '';
|
|
328
|
+
for (let c = 0; c < 8; c++) {
|
|
329
|
+
txt += Math.floor(Math.random() * 16).toString(16).toUpperCase();
|
|
330
|
+
}
|
|
331
|
+
ctx.fillText(txt, x + 3, y + 7 + line * 5);
|
|
332
|
+
}
|
|
333
|
+
// Scanline
|
|
334
|
+
const scanY = y + ((this.screenGlitch * 0.02) % h);
|
|
335
|
+
ctx.fillStyle = 'rgba(0,255,0,0.08)';
|
|
336
|
+
ctx.fillRect(x, scanY, w, 2);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_drawNeonSign(ctx, x, y) {
|
|
340
|
+
// Glowing neon "< / >" sign
|
|
341
|
+
const glow = 0.6 + Math.sin(this.neonPulse * 2) * 0.3;
|
|
342
|
+
ctx.save();
|
|
343
|
+
ctx.globalAlpha = glow;
|
|
344
|
+
ctx.font = 'bold 14px monospace';
|
|
345
|
+
ctx.fillStyle = '#ff00ff';
|
|
346
|
+
ctx.fillText('< / >', x, y + 14);
|
|
347
|
+
// Glow effect
|
|
348
|
+
ctx.shadowColor = '#ff00ff';
|
|
349
|
+
ctx.shadowBlur = 8;
|
|
350
|
+
ctx.fillText('< / >', x, y + 14);
|
|
351
|
+
ctx.restore();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
_drawLEDPanel(ctx, x, y, w, h) {
|
|
355
|
+
// LED dot matrix panel on wall
|
|
356
|
+
ctx.fillStyle = '#0a0a0a';
|
|
357
|
+
ctx.fillRect(x - 1, y - 1, w + 2, h + 2);
|
|
358
|
+
ctx.fillStyle = '#050510';
|
|
359
|
+
ctx.fillRect(x, y, w, h);
|
|
360
|
+
// LED dots — scrolling pattern
|
|
361
|
+
const dotSize = 3;
|
|
362
|
+
const cols = Math.floor(w / (dotSize + 1));
|
|
363
|
+
const rows = Math.floor(h / (dotSize + 1));
|
|
364
|
+
for (let r = 0; r < rows; r++) {
|
|
365
|
+
for (let c = 0; c < cols; c++) {
|
|
366
|
+
const offset = (this.screenGlitch * 0.01 + c * 0.3 + r * 0.2) % 6;
|
|
367
|
+
const on = Math.sin(offset) > 0.3;
|
|
368
|
+
if (on) {
|
|
369
|
+
const hue = (c * 20 + r * 30 + this.neonPulse * 30) % 360;
|
|
370
|
+
ctx.fillStyle = `hsla(${hue}, 90%, 55%, 0.7)`;
|
|
371
|
+
} else {
|
|
372
|
+
ctx.fillStyle = 'rgba(50,50,80,0.3)';
|
|
373
|
+
}
|
|
374
|
+
ctx.fillRect(x + 2 + c * (dotSize + 1), y + 2 + r * (dotSize + 1), dotSize, dotSize);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
_drawClock(ctx, x, y) {
|
|
380
|
+
// Digital clock (hacker style)
|
|
381
|
+
ctx.fillStyle = '#111';
|
|
382
|
+
ctx.fillRect(x - 14, y, 28, 14);
|
|
383
|
+
ctx.fillStyle = '#0a0a18';
|
|
384
|
+
ctx.fillRect(x - 13, y + 1, 26, 12);
|
|
385
|
+
const now = new Date();
|
|
386
|
+
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
387
|
+
ctx.font = '7px monospace';
|
|
388
|
+
ctx.fillStyle = '#0f0';
|
|
389
|
+
ctx.fillText(timeStr, x - 11, y + 10);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── Side-view floor furniture ──
|
|
393
|
+
|
|
394
|
+
_drawSideServerRack(ctx, x, y, depth, h, side) {
|
|
395
|
+
// Server rack against wall
|
|
396
|
+
ctx.fillStyle = '#1a1a2e';
|
|
397
|
+
ctx.fillRect(x, y, depth, h);
|
|
398
|
+
ctx.fillStyle = '#111125';
|
|
399
|
+
ctx.fillRect(x + 2, y + 2, depth - 4, h - 4);
|
|
400
|
+
// Front edge
|
|
401
|
+
const edgeX = side === 'left' ? x + depth - 3 : x;
|
|
402
|
+
ctx.fillStyle = '#0d0d1e';
|
|
403
|
+
ctx.fillRect(edgeX, y, 3, h);
|
|
404
|
+
// Server units with LEDs
|
|
405
|
+
for (let i = 0; i < 6; i++) {
|
|
406
|
+
const uy = y + 4 + i * 14;
|
|
407
|
+
ctx.fillStyle = '#252545';
|
|
408
|
+
ctx.fillRect(x + 3, uy, depth - 6, 11);
|
|
409
|
+
// Blinking LED
|
|
410
|
+
const ledOn = Math.floor(this.screenGlitch / (300 + i * 100)) % 2 === 0;
|
|
411
|
+
ctx.fillStyle = ledOn ? ['#0f0', '#0ff', '#f0f', '#ff0', '#0f0', '#0ff'][i] : '#222';
|
|
412
|
+
ctx.fillRect(x + 5, uy + 4, 3, 3);
|
|
413
|
+
// Ventilation lines
|
|
414
|
+
ctx.fillStyle = '#1a1a30';
|
|
415
|
+
ctx.fillRect(x + 10, uy + 2, depth - 14, 1);
|
|
416
|
+
ctx.fillRect(x + 10, uy + 5, depth - 14, 1);
|
|
417
|
+
ctx.fillRect(x + 10, uy + 8, depth - 14, 1);
|
|
418
|
+
}
|
|
419
|
+
// Top/bottom trim
|
|
420
|
+
ctx.fillStyle = '#333';
|
|
421
|
+
ctx.fillRect(x - 1, y - 1, depth + 2, 2);
|
|
422
|
+
ctx.fillRect(x - 1, y + h - 1, depth + 2, 2);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
_drawSideBeanBag(ctx, x, y, depth, h, side) {
|
|
426
|
+
// Bean bag chair — blob shape
|
|
427
|
+
ctx.fillStyle = '#2d1b4e';
|
|
428
|
+
ctx.fillRect(x + 4, y + 6, depth - 8, h - 8);
|
|
429
|
+
ctx.fillStyle = '#3a2463';
|
|
430
|
+
ctx.fillRect(x + 2, y + 4, depth - 4, h - 6);
|
|
431
|
+
// Round top
|
|
432
|
+
ctx.fillStyle = '#3a2463';
|
|
433
|
+
ctx.fillRect(x + 6, y, depth - 12, 8);
|
|
434
|
+
// Highlight
|
|
435
|
+
ctx.fillStyle = '#4a3478';
|
|
436
|
+
ctx.fillRect(x + 6, y + 6, depth - 14, h * 0.4);
|
|
437
|
+
// Crease
|
|
438
|
+
ctx.fillStyle = '#2d1b4e';
|
|
439
|
+
ctx.fillRect(x + 8, y + Math.floor(h * 0.5), depth - 16, 1);
|
|
440
|
+
// Small glow sticker
|
|
441
|
+
ctx.fillStyle = '#00e5ff';
|
|
442
|
+
ctx.fillRect(x + depth - 10, y + 8, 4, 4);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
_drawSideMiniFridge(ctx, x, y, depth, h, side) {
|
|
446
|
+
// Black mini-fridge with stickers
|
|
447
|
+
ctx.fillStyle = '#1a1a2e';
|
|
448
|
+
ctx.fillRect(x, y, depth, h);
|
|
449
|
+
ctx.fillStyle = '#222240';
|
|
450
|
+
ctx.fillRect(x + 2, y + 2, depth - 4, h - 4);
|
|
451
|
+
// Door edge
|
|
452
|
+
const edgeX = side === 'right' ? x : x + depth - 3;
|
|
453
|
+
ctx.fillStyle = '#111128';
|
|
454
|
+
ctx.fillRect(edgeX, y, 3, h);
|
|
455
|
+
// Handle
|
|
456
|
+
ctx.fillStyle = '#555';
|
|
457
|
+
ctx.fillRect(edgeX + 1, y + Math.floor(h * 0.3), 1, 8);
|
|
458
|
+
// Top
|
|
459
|
+
ctx.fillStyle = '#252545';
|
|
460
|
+
ctx.fillRect(x, y - 2, depth, 3);
|
|
461
|
+
// Stickers
|
|
462
|
+
ctx.fillStyle = '#ff00ff';
|
|
463
|
+
ctx.fillRect(x + 5, y + 8, 5, 5);
|
|
464
|
+
ctx.fillStyle = '#00e5ff';
|
|
465
|
+
ctx.fillRect(x + 5, y + 18, 6, 4);
|
|
466
|
+
ctx.fillStyle = '#ffea00';
|
|
467
|
+
ctx.fillRect(x + 12, y + 12, 4, 4);
|
|
468
|
+
// Energy drinks on top
|
|
469
|
+
ctx.fillStyle = '#00e676';
|
|
470
|
+
ctx.fillRect(x + 4, y - 10, 5, 8);
|
|
471
|
+
ctx.fillStyle = '#004d40';
|
|
472
|
+
ctx.fillRect(x + 4, y - 8, 5, 3);
|
|
473
|
+
ctx.fillStyle = '#00e676';
|
|
474
|
+
ctx.fillRect(x + 12, y - 8, 5, 6);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
_drawSideArcade(ctx, x, y, depth, h, side) {
|
|
478
|
+
// Retro arcade cabinet — side view
|
|
479
|
+
// Cabinet body
|
|
480
|
+
ctx.fillStyle = '#1a1a2e';
|
|
481
|
+
ctx.fillRect(x, y, depth, h);
|
|
482
|
+
ctx.fillStyle = '#222240';
|
|
483
|
+
ctx.fillRect(x + 2, y + 2, depth - 4, h - 4);
|
|
484
|
+
// Front edge
|
|
485
|
+
const edgeX = side === 'right' ? x : x + depth - 3;
|
|
486
|
+
ctx.fillStyle = '#0d0d1e';
|
|
487
|
+
ctx.fillRect(edgeX, y, 3, h);
|
|
488
|
+
// Screen area (visible from side as a bright strip)
|
|
489
|
+
const screenY = y + 8;
|
|
490
|
+
ctx.fillStyle = '#000';
|
|
491
|
+
ctx.fillRect(x + 3, screenY, depth - 6, 22);
|
|
492
|
+
// Screen glow (cycling colors)
|
|
493
|
+
const screenHue = (this.neonPulse * 40) % 360;
|
|
494
|
+
ctx.fillStyle = `hsla(${screenHue}, 80%, 40%, 0.6)`;
|
|
495
|
+
ctx.fillRect(x + 4, screenY + 1, depth - 8, 20);
|
|
496
|
+
// Pixel art on screen
|
|
497
|
+
ctx.fillStyle = `hsla(${screenHue + 120}, 80%, 60%, 0.8)`;
|
|
498
|
+
ctx.fillRect(x + 6, screenY + 5, 4, 4);
|
|
499
|
+
ctx.fillRect(x + 12, screenY + 8, 3, 6);
|
|
500
|
+
ctx.fillRect(x + 8, screenY + 14, 5, 3);
|
|
501
|
+
// Control panel area
|
|
502
|
+
ctx.fillStyle = '#333';
|
|
503
|
+
ctx.fillRect(x + 3, y + 34, depth - 6, 12);
|
|
504
|
+
// Joystick
|
|
505
|
+
ctx.fillStyle = '#e74c3c';
|
|
506
|
+
ctx.fillRect(x + 8, y + 36, 4, 8);
|
|
507
|
+
ctx.fillRect(x + 7, y + 35, 6, 3);
|
|
508
|
+
// Buttons
|
|
509
|
+
ctx.fillStyle = '#3498db';
|
|
510
|
+
ctx.fillRect(x + 16, y + 38, 4, 4);
|
|
511
|
+
ctx.fillStyle = '#2ecc71';
|
|
512
|
+
ctx.fillRect(x + 16, y + 44, 4, 4);
|
|
513
|
+
// Marquee on top
|
|
514
|
+
ctx.fillStyle = '#ff00ff';
|
|
515
|
+
ctx.fillRect(x + 2, y, depth - 4, 6);
|
|
516
|
+
ctx.fillStyle = '#ffea00';
|
|
517
|
+
ctx.fillRect(x + 4, y + 1, depth - 8, 4);
|
|
518
|
+
// Coin slot
|
|
519
|
+
ctx.fillStyle = '#555';
|
|
520
|
+
ctx.fillRect(x + 8, y + 50, 6, 2);
|
|
521
|
+
// Base/legs
|
|
522
|
+
ctx.fillStyle = '#111';
|
|
523
|
+
ctx.fillRect(x + 2, y + h - 4, depth - 4, 4);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (typeof window !== 'undefined') window.HackerScene = HackerScene;
|