ideaco 1.1.5

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.
Files changed (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,2281 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { useI18n } from '@/lib/i18n';
6
+ import AgentDetailModal from '@/components/AgentDetailModal';
7
+
8
+ // ===== Pixel Art Color Palettes (Stardew Valley inspired) =====
9
+ const ROOM_PALETTES = [
10
+ { floor: '#8B7355', wall: '#A0522D', wallTop: '#C4956A', accent: '#DEB887', name: 'wood' },
11
+ { floor: '#6B8E6B', wall: '#2E5A2E', wallTop: '#5A8A5A', accent: '#90C090', name: 'garden' },
12
+ { floor: '#7B7BA0', wall: '#3D3D6B', wallTop: '#6060A0', accent: '#9090D0', name: 'tech' },
13
+ { floor: '#A07070', wall: '#6B2D2D', wallTop: '#905050', accent: '#D09090', name: 'warm' },
14
+ { floor: '#70A0A0', wall: '#2D5A6B', wallTop: '#508090', accent: '#90C0D0', name: 'ocean' },
15
+ { floor: '#A0A070', wall: '#5A5A2D', wallTop: '#808050', accent: '#C0C090', name: 'sand' },
16
+ { floor: '#9070A0', wall: '#5A2D6B', wallTop: '#805090', accent: '#B090D0', name: 'violet' },
17
+ { floor: '#A08870', wall: '#6B4A2D', wallTop: '#906A50', accent: '#D0B090', name: 'coffee' },
18
+ ];
19
+
20
+ const SKIN_COLORS = ['#FFD5B8', '#E8B89A', '#C4956A', '#A07855', '#8B6544'];
21
+ const HAIR_COLORS = ['#2C1810', '#5A3520', '#8B4513', '#D4A574', '#E8C07A', '#C0392B', '#2C3E50', '#7D3C98', '#F39C12', '#ECF0F1'];
22
+ const SHIRT_COLORS = ['#3498DB', '#E74C3C', '#2ECC71', '#9B59B6', '#F39C12', '#1ABC9C', '#E67E22', '#34495E', '#E91E63', '#00BCD4'];
23
+
24
+ // ===== Hash helper =====
25
+ function hashStr(str) {
26
+ let h = 0;
27
+ for (let i = 0; i < (str || '').length; i++) {
28
+ h = ((h << 5) - h + str.charCodeAt(i)) | 0;
29
+ }
30
+ return Math.abs(h);
31
+ }
32
+
33
+ function getCharColors(name) {
34
+ const h = hashStr(name);
35
+ return {
36
+ skin: SKIN_COLORS[h % SKIN_COLORS.length],
37
+ hair: HAIR_COLORS[(h >> 3) % HAIR_COLORS.length],
38
+ shirt: SHIRT_COLORS[(h >> 6) % SHIRT_COLORS.length],
39
+ pants: ['#2C3E50', '#4A4A6A', '#3D5A3D', '#6B4A3D'][(h >> 9) % 4],
40
+ };
41
+ }
42
+
43
+ // ===== Draw pixel character (higher detail) =====
44
+ function drawPixelChar(ctx, x, y, name, scale = 3, frame = 0, direction = 0, sitting = false) {
45
+ const c = getCharColors(name);
46
+ const s = scale;
47
+ const px = (dx, dy, color) => {
48
+ ctx.fillStyle = color;
49
+ ctx.fillRect(x + dx * s, y + dy * s, s, s);
50
+ };
51
+
52
+ // Hair (fuller)
53
+ const hairDark = adjustColor(c.hair, -20);
54
+ for (let i = -2; i <= 2; i++) px(i, -7, c.hair);
55
+ for (let i = -2; i <= 2; i++) px(i, -6, c.hair);
56
+ px(-3, -6, hairDark); px(3, -6, hairDark);
57
+ px(-3, -5, c.hair); px(3, -5, c.hair);
58
+
59
+ // Head
60
+ for (let i = -2; i <= 2; i++) px(i, -5, c.skin);
61
+ for (let i = -2; i <= 2; i++) px(i, -4, c.skin);
62
+ for (let i = -2; i <= 2; i++) px(i, -3, c.skin);
63
+ // Side hair
64
+ px(-3, -5, c.hair); px(3, -5, c.hair);
65
+ px(-3, -4, c.hair); px(3, -4, c.hair);
66
+
67
+ // Eyes (blink)
68
+ const blinkFrame = frame % 150;
69
+ if (blinkFrame < 140) {
70
+ px(direction >= 0 ? -1 : 0, -4, '#2C1810');
71
+ px(direction <= 0 ? 1 : 0, -4, '#2C1810');
72
+ // Eye shine
73
+ px(direction >= 0 ? -1 : 0, -4, '#2C1810');
74
+ } else {
75
+ px(-1, -4, adjustColor(c.skin, -15));
76
+ px(1, -4, adjustColor(c.skin, -15));
77
+ }
78
+ // Nose
79
+ px(0, -3, adjustColor(c.skin, -10));
80
+ // Mouth
81
+ px(-1, -3, adjustColor(c.skin, -5));
82
+ px(0, -3, adjustColor(c.skin, -12));
83
+ px(1, -3, adjustColor(c.skin, -5));
84
+
85
+ // Neck
86
+ px(-1, -2, c.skin); px(0, -2, c.skin); px(1, -2, c.skin);
87
+
88
+ // Collar
89
+ const collarColor = adjustColor(c.shirt, 20);
90
+ px(-2, -2, collarColor); px(2, -2, collarColor);
91
+
92
+ // Torso / shirt (wider)
93
+ for (let row = -1; row <= 1; row++) {
94
+ for (let col = -3; col <= 3; col++) {
95
+ px(col, row, c.shirt);
96
+ }
97
+ }
98
+ // Shirt detail
99
+ px(0, -1, adjustColor(c.shirt, -15)); // button line
100
+ px(0, 0, adjustColor(c.shirt, -15));
101
+
102
+ if (sitting) {
103
+ // Arms resting on desk
104
+ px(-4, 0, c.skin); px(-4, 1, c.skin);
105
+ px(4, 0, c.skin); px(4, 1, c.skin);
106
+ // Lower body hidden in chair
107
+ for (let col = -2; col <= 2; col++) px(col, 2, c.pants);
108
+ } else {
109
+ // Arms (animate when walking)
110
+ const armSwing = Math.floor(frame / 12) % 4;
111
+ const armAnim = direction !== 0;
112
+ if (armAnim) {
113
+ const offL = armSwing < 2 ? 0 : 1;
114
+ const offR = armSwing < 2 ? 1 : 0;
115
+ px(-4, -1 + offL, c.skin); px(-4, offL, c.skin);
116
+ px(4, -1 + offR, c.skin); px(4, offR, c.skin);
117
+ } else {
118
+ px(-4, 0, c.skin); px(-4, 1, c.skin);
119
+ px(4, 0, c.skin); px(4, 1, c.skin);
120
+ }
121
+
122
+ // Belt
123
+ for (let col = -3; col <= 3; col++) px(col, 2, adjustColor(c.pants, -10));
124
+ px(0, 2, '#888'); // buckle
125
+
126
+ // Legs
127
+ const legFrame = direction !== 0 ? Math.floor(frame / 10) % 4 : 0;
128
+ if (legFrame === 0 || legFrame === 2) {
129
+ px(-2, 3, c.pants); px(-1, 3, c.pants);
130
+ px(1, 3, c.pants); px(2, 3, c.pants);
131
+ px(-2, 4, c.pants); px(-1, 4, c.pants);
132
+ px(1, 4, c.pants); px(2, 4, c.pants);
133
+ } else if (legFrame === 1) {
134
+ px(-3, 3, c.pants); px(-2, 3, c.pants);
135
+ px(1, 3, c.pants); px(2, 3, c.pants);
136
+ px(-3, 4, c.pants); px(-2, 4, c.pants);
137
+ px(1, 4, c.pants); px(2, 4, c.pants);
138
+ } else {
139
+ px(-2, 3, c.pants); px(-1, 3, c.pants);
140
+ px(2, 3, c.pants); px(3, 3, c.pants);
141
+ px(-2, 4, c.pants); px(-1, 4, c.pants);
142
+ px(2, 4, c.pants); px(3, 4, c.pants);
143
+ }
144
+
145
+ // Shoes (detailed)
146
+ px(-2, 5, '#1a1a1a'); px(-1, 5, '#1a1a1a');
147
+ px(1, 5, '#1a1a1a'); px(2, 5, '#1a1a1a');
148
+ px(-2, 5, '#222'); px(2, 5, '#222'); // sole highlight
149
+ }
150
+ }
151
+
152
+ // Darken/lighten a hex color
153
+ function adjustColor(hex, amount) {
154
+ const r = Math.max(0, Math.min(255, parseInt(hex.slice(1, 3), 16) + amount));
155
+ const g = Math.max(0, Math.min(255, parseInt(hex.slice(3, 5), 16) + amount));
156
+ const b = Math.max(0, Math.min(255, parseInt(hex.slice(5, 7), 16) + amount));
157
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
158
+ }
159
+
160
+ // ===== Draw chat bubble above character =====
161
+ function drawBubble(ctx, x, y, text, scale = 2) {
162
+ if (!text) return;
163
+ const maxChars = 20;
164
+ const displayText = text.length > maxChars ? text.slice(0, maxChars - 1) + 'โ€ฆ' : text;
165
+
166
+ ctx.font = `${10 * (scale / 2)}px "Courier New", monospace`;
167
+ const metrics = ctx.measureText(displayText);
168
+ const tw = metrics.width;
169
+ const bw = tw + 12 * (scale / 2);
170
+ const bh = 18 * (scale / 2);
171
+ const bx = x - bw / 2;
172
+ const by = y - 6 * scale - bh - 4;
173
+
174
+ ctx.fillStyle = 'rgba(255,255,255,0.95)';
175
+ ctx.strokeStyle = '#5A3520';
176
+ ctx.lineWidth = scale / 2;
177
+ roundRect(ctx, bx, by, bw, bh, 4 * (scale / 2));
178
+ ctx.fill();
179
+ ctx.stroke();
180
+
181
+ // Triangle pointer
182
+ ctx.fillStyle = 'rgba(255,255,255,0.95)';
183
+ ctx.beginPath();
184
+ ctx.moveTo(x - 3 * (scale / 2), by + bh);
185
+ ctx.lineTo(x + 3 * (scale / 2), by + bh);
186
+ ctx.lineTo(x, by + bh + 4 * (scale / 2));
187
+ ctx.closePath();
188
+ ctx.fill();
189
+ ctx.strokeStyle = '#5A3520';
190
+ ctx.beginPath();
191
+ ctx.moveTo(x - 3 * (scale / 2), by + bh);
192
+ ctx.lineTo(x, by + bh + 4 * (scale / 2));
193
+ ctx.lineTo(x + 3 * (scale / 2), by + bh);
194
+ ctx.stroke();
195
+
196
+ ctx.fillStyle = '#2C1810';
197
+ ctx.textAlign = 'center';
198
+ ctx.textBaseline = 'middle';
199
+ ctx.fillText(displayText, x, by + bh / 2);
200
+ }
201
+
202
+ function roundRect(ctx, x, y, w, h, r) {
203
+ ctx.beginPath();
204
+ ctx.moveTo(x + r, y);
205
+ ctx.lineTo(x + w - r, y);
206
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
207
+ ctx.lineTo(x + w, y + h - r);
208
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
209
+ ctx.lineTo(x + r, y + h);
210
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
211
+ ctx.lineTo(x, y + r);
212
+ ctx.quadraticCurveTo(x, y, x + r, y);
213
+ ctx.closePath();
214
+ }
215
+
216
+ // ===== Furniture Drawing Functions =====
217
+ function drawDesk(ctx, x, y, s) {
218
+ ctx.fillStyle = '#8B6914';
219
+ ctx.fillRect(x, y, 12 * s, 6 * s);
220
+ ctx.fillStyle = '#A07828';
221
+ ctx.fillRect(x + s, y + s, 10 * s, 4 * s);
222
+ // monitor
223
+ ctx.fillStyle = '#333';
224
+ ctx.fillRect(x + 3 * s, y - 5 * s, 6 * s, 5 * s);
225
+ ctx.fillStyle = '#4488AA';
226
+ ctx.fillRect(x + 4 * s, y - 4 * s, 4 * s, 3 * s);
227
+ // monitor stand
228
+ ctx.fillStyle = '#555';
229
+ ctx.fillRect(x + 5 * s, y, 2 * s, s);
230
+ }
231
+
232
+ function drawPlant(ctx, x, y, s) {
233
+ ctx.fillStyle = '#A0522D';
234
+ ctx.fillRect(x, y, 4 * s, 4 * s);
235
+ ctx.fillStyle = '#8B4513';
236
+ ctx.fillRect(x - s, y, 6 * s, s);
237
+ ctx.fillStyle = '#228B22';
238
+ ctx.fillRect(x - s, y - 4 * s, 6 * s, 4 * s);
239
+ ctx.fillStyle = '#32CD32';
240
+ ctx.fillRect(x, y - 5 * s, 4 * s, 2 * s);
241
+ ctx.fillRect(x - 2 * s, y - 3 * s, 2 * s, 2 * s);
242
+ ctx.fillRect(x + 4 * s, y - 2 * s, 2 * s, 2 * s);
243
+ }
244
+
245
+ function drawSmallPlant(ctx, x, y, s) {
246
+ ctx.fillStyle = '#8B4513';
247
+ ctx.fillRect(x, y, 3 * s, 3 * s);
248
+ ctx.fillStyle = '#228B22';
249
+ ctx.fillRect(x - s, y - 3 * s, 5 * s, 3 * s);
250
+ ctx.fillStyle = '#32CD32';
251
+ ctx.fillRect(x, y - 4 * s, 3 * s, 2 * s);
252
+ }
253
+
254
+ function drawWaterCooler(ctx, x, y, s) {
255
+ ctx.fillStyle = '#B0C4DE';
256
+ ctx.fillRect(x, y - 6 * s, 4 * s, 6 * s);
257
+ ctx.fillStyle = '#87CEEB';
258
+ ctx.fillRect(x + s, y - 5 * s, 2 * s, 3 * s);
259
+ ctx.fillStyle = '#666';
260
+ ctx.fillRect(x, y, 4 * s, 2 * s);
261
+ }
262
+
263
+ function drawBookshelf(ctx, x, y, s) {
264
+ ctx.fillStyle = '#6B4226';
265
+ ctx.fillRect(x, y - 10 * s, 10 * s, 12 * s);
266
+ ctx.fillStyle = '#8B5A2B';
267
+ ctx.fillRect(x + s, y - 9 * s, 8 * s, 3 * s);
268
+ ctx.fillRect(x + s, y - 5 * s, 8 * s, 3 * s);
269
+ ctx.fillRect(x + s, y - 1 * s, 8 * s, 2 * s);
270
+ const bookColors = ['#E74C3C', '#3498DB', '#2ECC71', '#F39C12', '#9B59B6', '#E67E22'];
271
+ for (let row = 0; row < 3; row++) {
272
+ const rowY = y - 9 * s + row * 4 * s;
273
+ for (let b = 0; b < 4; b++) {
274
+ ctx.fillStyle = bookColors[(row * 4 + b) % bookColors.length];
275
+ ctx.fillRect(x + (s + b * 2 * s), rowY, s, 2.5 * s);
276
+ }
277
+ }
278
+ }
279
+
280
+ function drawWhiteboard(ctx, x, y, s) {
281
+ // Frame
282
+ ctx.fillStyle = '#888';
283
+ ctx.fillRect(x, y, 18 * s, 12 * s);
284
+ // White surface
285
+ ctx.fillStyle = '#F5F5F5';
286
+ ctx.fillRect(x + s, y + s, 16 * s, 10 * s);
287
+ // Some scribbles
288
+ ctx.fillStyle = '#E74C3C';
289
+ ctx.fillRect(x + 3 * s, y + 3 * s, 5 * s, s);
290
+ ctx.fillStyle = '#3498DB';
291
+ ctx.fillRect(x + 3 * s, y + 5 * s, 8 * s, s);
292
+ ctx.fillStyle = '#2ECC71';
293
+ ctx.fillRect(x + 3 * s, y + 7 * s, 6 * s, s);
294
+ // Marker tray
295
+ ctx.fillStyle = '#666';
296
+ ctx.fillRect(x + 4 * s, y + 11 * s, 10 * s, s);
297
+ // Markers
298
+ ctx.fillStyle = '#E74C3C';
299
+ ctx.fillRect(x + 5 * s, y + 10.5 * s, s, s);
300
+ ctx.fillStyle = '#3498DB';
301
+ ctx.fillRect(x + 7 * s, y + 10.5 * s, s, s);
302
+ ctx.fillStyle = '#2ECC71';
303
+ ctx.fillRect(x + 9 * s, y + 10.5 * s, s, s);
304
+ }
305
+
306
+ function drawCoffeeMachine(ctx, x, y, s) {
307
+ // Body
308
+ ctx.fillStyle = '#333';
309
+ ctx.fillRect(x, y - 7 * s, 6 * s, 7 * s);
310
+ ctx.fillStyle = '#555';
311
+ ctx.fillRect(x + s, y - 6 * s, 4 * s, 4 * s);
312
+ // Cup
313
+ ctx.fillStyle = '#F5F5DC';
314
+ ctx.fillRect(x + 2 * s, y - s, 2 * s, 2 * s);
315
+ // Steam
316
+ ctx.fillStyle = 'rgba(200,200,200,0.6)';
317
+ ctx.fillRect(x + 2 * s, y - 3 * s, s, s);
318
+ ctx.fillRect(x + 3 * s, y - 4 * s, s, s);
319
+ // Light
320
+ ctx.fillStyle = '#00FF00';
321
+ ctx.fillRect(x + s, y - 2 * s, s, s);
322
+ }
323
+
324
+ function drawPrinter(ctx, x, y, s) {
325
+ // Body
326
+ ctx.fillStyle = '#666';
327
+ ctx.fillRect(x, y - 4 * s, 8 * s, 5 * s);
328
+ ctx.fillStyle = '#888';
329
+ ctx.fillRect(x + s, y - 3 * s, 6 * s, 2 * s);
330
+ // Paper tray
331
+ ctx.fillStyle = '#F5F5DC';
332
+ ctx.fillRect(x + 2 * s, y - 5 * s, 4 * s, 2 * s);
333
+ // Output paper
334
+ ctx.fillStyle = '#FFF';
335
+ ctx.fillRect(x + 2 * s, y, 4 * s, s);
336
+ // Buttons
337
+ ctx.fillStyle = '#00FF00';
338
+ ctx.fillRect(x + 6 * s, y - 3 * s, s, s);
339
+ ctx.fillStyle = '#FF0';
340
+ ctx.fillRect(x + 6 * s, y - 2 * s, s, s);
341
+ }
342
+
343
+ function drawClock(ctx, x, y, s) {
344
+ // Face
345
+ ctx.fillStyle = '#FFF';
346
+ ctx.beginPath();
347
+ ctx.arc(x + 3 * s, y + 3 * s, 3 * s, 0, Math.PI * 2);
348
+ ctx.fill();
349
+ ctx.strokeStyle = '#333';
350
+ ctx.lineWidth = s * 0.5;
351
+ ctx.stroke();
352
+ // Hour hand
353
+ ctx.strokeStyle = '#333';
354
+ ctx.lineWidth = s * 0.6;
355
+ ctx.beginPath();
356
+ ctx.moveTo(x + 3 * s, y + 3 * s);
357
+ ctx.lineTo(x + 3 * s, y + 1.5 * s);
358
+ ctx.stroke();
359
+ // Minute hand
360
+ ctx.lineWidth = s * 0.4;
361
+ ctx.beginPath();
362
+ ctx.moveTo(x + 3 * s, y + 3 * s);
363
+ ctx.lineTo(x + 4.5 * s, y + 2 * s);
364
+ ctx.stroke();
365
+ // Center dot
366
+ ctx.fillStyle = '#E74C3C';
367
+ ctx.beginPath();
368
+ ctx.arc(x + 3 * s, y + 3 * s, s * 0.4, 0, Math.PI * 2);
369
+ ctx.fill();
370
+ }
371
+
372
+ function drawTrashBin(ctx, x, y, s) {
373
+ ctx.fillStyle = '#666';
374
+ ctx.fillRect(x, y, 4 * s, 5 * s);
375
+ ctx.fillStyle = '#777';
376
+ ctx.fillRect(x - s * 0.5, y - s, 5 * s, s);
377
+ // Trash inside
378
+ ctx.fillStyle = '#888';
379
+ ctx.fillRect(x + s, y + s, 2 * s, 2 * s);
380
+ }
381
+
382
+ function drawRug(ctx, x, y, w, h, color) {
383
+ ctx.fillStyle = color;
384
+ ctx.fillRect(x, y, w, h);
385
+ // Border pattern
386
+ ctx.fillStyle = 'rgba(255,255,255,0.15)';
387
+ ctx.fillRect(x, y, w, 2);
388
+ ctx.fillRect(x, y + h - 2, w, 2);
389
+ ctx.fillRect(x, y, 2, h);
390
+ ctx.fillRect(x + w - 2, y, 2, h);
391
+ // Inner border
392
+ ctx.fillStyle = 'rgba(255,255,255,0.08)';
393
+ ctx.fillRect(x + 4, y + 4, w - 8, 1);
394
+ ctx.fillRect(x + 4, y + h - 5, w - 8, 1);
395
+ ctx.fillRect(x + 4, y + 4, 1, h - 8);
396
+ ctx.fillRect(x + w - 5, y + 4, 1, h - 8);
397
+ }
398
+
399
+ function drawWindow(ctx, x, y, s) {
400
+ // Frame
401
+ ctx.fillStyle = '#6B4226';
402
+ ctx.fillRect(x, y, 14 * s, 10 * s);
403
+ // Glass (sky blue)
404
+ ctx.fillStyle = '#87CEEB';
405
+ ctx.fillRect(x + s, y + s, 5.5 * s, 3.5 * s);
406
+ ctx.fillRect(x + 7.5 * s, y + s, 5.5 * s, 3.5 * s);
407
+ ctx.fillRect(x + s, y + 5.5 * s, 5.5 * s, 3.5 * s);
408
+ ctx.fillRect(x + 7.5 * s, y + 5.5 * s, 5.5 * s, 3.5 * s);
409
+ // Cross bar
410
+ ctx.fillStyle = '#6B4226';
411
+ ctx.fillRect(x + 6 * s, y, 2 * s, 10 * s);
412
+ ctx.fillRect(x, y + 4.5 * s, 14 * s, s);
413
+ // Sunlight effect
414
+ ctx.fillStyle = 'rgba(255,255,200,0.15)';
415
+ ctx.fillRect(x + 2 * s, y + 2 * s, 3 * s, 2 * s);
416
+ ctx.fillRect(x + 8.5 * s, y + 2 * s, 3 * s, 2 * s);
417
+ // Curtains
418
+ ctx.fillStyle = 'rgba(139,66,38,0.4)';
419
+ ctx.fillRect(x - s, y, 2 * s, 10 * s);
420
+ ctx.fillRect(x + 13 * s, y, 2 * s, 10 * s);
421
+ }
422
+
423
+ function drawHangingLamp(ctx, x, y, s) {
424
+ // Wire
425
+ ctx.fillStyle = '#555';
426
+ ctx.fillRect(x + 2 * s, y, s, 3 * s);
427
+ // Shade
428
+ ctx.fillStyle = '#D4A574';
429
+ ctx.fillRect(x, y + 3 * s, 5 * s, 3 * s);
430
+ // Light glow
431
+ ctx.fillStyle = 'rgba(255,255,200,0.3)';
432
+ ctx.fillRect(x + s, y + 4 * s, 3 * s, 2 * s);
433
+ // Warm glow below
434
+ ctx.fillStyle = 'rgba(255,255,200,0.06)';
435
+ ctx.beginPath();
436
+ ctx.arc(x + 2.5 * s, y + 6 * s, 8 * s, 0, Math.PI * 2);
437
+ ctx.fill();
438
+ }
439
+
440
+ function drawPictureFrame(ctx, x, y, s) {
441
+ // Frame
442
+ ctx.fillStyle = '#8B6914';
443
+ ctx.fillRect(x, y, 8 * s, 6 * s);
444
+ // Picture (landscape)
445
+ ctx.fillStyle = '#87CEEB';
446
+ ctx.fillRect(x + s, y + s, 6 * s, 4 * s);
447
+ // Mountains
448
+ ctx.fillStyle = '#228B22';
449
+ ctx.beginPath();
450
+ ctx.moveTo(x + s, y + 4 * s);
451
+ ctx.lineTo(x + 3 * s, y + 2 * s);
452
+ ctx.lineTo(x + 5 * s, y + 3 * s);
453
+ ctx.lineTo(x + 7 * s, y + 1.5 * s);
454
+ ctx.lineTo(x + 7 * s, y + 4 * s);
455
+ ctx.closePath();
456
+ ctx.fill();
457
+ // Sun
458
+ ctx.fillStyle = '#F39C12';
459
+ ctx.beginPath();
460
+ ctx.arc(x + 6 * s, y + 2 * s, s, 0, Math.PI * 2);
461
+ ctx.fill();
462
+ }
463
+
464
+ function drawFileCabinet(ctx, x, y, s) {
465
+ ctx.fillStyle = '#778899';
466
+ ctx.fillRect(x, y - 10 * s, 6 * s, 12 * s);
467
+ // Drawers
468
+ for (let i = 0; i < 3; i++) {
469
+ const dy = y - 9 * s + i * 4 * s;
470
+ ctx.fillStyle = '#8899AA';
471
+ ctx.fillRect(x + s * 0.5, dy, 5 * s, 3 * s);
472
+ // Handle
473
+ ctx.fillStyle = '#AAA';
474
+ ctx.fillRect(x + 2 * s, dy + s, 2 * s, s);
475
+ }
476
+ }
477
+
478
+ function drawCouch(ctx, x, y, s) {
479
+ // Back
480
+ ctx.fillStyle = '#6B4226';
481
+ ctx.fillRect(x, y - 2 * s, 16 * s, 3 * s);
482
+ // Seat
483
+ ctx.fillStyle = '#8B5A2B';
484
+ ctx.fillRect(x, y + s, 16 * s, 4 * s);
485
+ // Cushions
486
+ ctx.fillStyle = '#A0522D';
487
+ ctx.fillRect(x + s, y + 2 * s, 6 * s, 2 * s);
488
+ ctx.fillRect(x + 9 * s, y + 2 * s, 6 * s, 2 * s);
489
+ // Arms
490
+ ctx.fillStyle = '#6B4226';
491
+ ctx.fillRect(x - s, y, 2 * s, 5 * s);
492
+ ctx.fillRect(x + 15 * s, y, 2 * s, 5 * s);
493
+ }
494
+
495
+ // ===== Boss Office luxury furniture =====
496
+ function drawLuxuryCouch(ctx, x, y, s) {
497
+ // Big luxury sofa - dark leather
498
+ const cw = 24 * s, ch = 8 * s;
499
+ // Sofa back (tall, plush)
500
+ ctx.fillStyle = '#3D1F0F';
501
+ ctx.fillRect(x, y - 3 * s, cw, 4 * s);
502
+ ctx.fillStyle = '#4E2B14';
503
+ ctx.fillRect(x + s, y - 2.5 * s, cw - 2 * s, 3 * s);
504
+ // Seat
505
+ ctx.fillStyle = '#5C3317';
506
+ ctx.fillRect(x, y + s, cw, ch);
507
+ // Cushion segments (3 seats)
508
+ ctx.fillStyle = '#6B3D1E';
509
+ ctx.fillRect(x + s, y + 2 * s, 7 * s, 5 * s);
510
+ ctx.fillRect(x + 9 * s, y + 2 * s, 7 * s, 5 * s);
511
+ ctx.fillRect(x + 17 * s, y + 2 * s, 6 * s, 5 * s);
512
+ // Cushion highlights
513
+ ctx.fillStyle = 'rgba(255,255,255,0.08)';
514
+ ctx.fillRect(x + 2 * s, y + 3 * s, 5 * s, 2 * s);
515
+ ctx.fillRect(x + 10 * s, y + 3 * s, 5 * s, 2 * s);
516
+ ctx.fillRect(x + 18 * s, y + 3 * s, 4 * s, 2 * s);
517
+ // Arms (thick, rounded)
518
+ ctx.fillStyle = '#3D1F0F';
519
+ ctx.fillRect(x - 2 * s, y - s, 3 * s, ch + 2 * s);
520
+ ctx.fillRect(x + cw - s, y - s, 3 * s, ch + 2 * s);
521
+ // Arm top highlight
522
+ ctx.fillStyle = '#5C3317';
523
+ ctx.fillRect(x - 2 * s, y - s, 3 * s, 2 * s);
524
+ ctx.fillRect(x + cw - s, y - s, 3 * s, 2 * s);
525
+ // Decorative buttons on back
526
+ ctx.fillStyle = '#8B6914';
527
+ for (let i = 0; i < 3; i++) {
528
+ ctx.beginPath();
529
+ ctx.arc(x + 4 * s + i * 8 * s, y - s, s * 0.5, 0, Math.PI * 2);
530
+ ctx.fill();
531
+ }
532
+ }
533
+
534
+ function drawLuxuryRug(ctx, x, y, w, h) {
535
+ // Rich patterned rug
536
+ ctx.fillStyle = '#7B2D26';
537
+ ctx.fillRect(x, y, w, h);
538
+ // Border pattern (gold)
539
+ ctx.fillStyle = '#B8860B';
540
+ ctx.fillRect(x, y, w, 3);
541
+ ctx.fillRect(x, y + h - 3, w, 3);
542
+ ctx.fillRect(x, y, 3, h);
543
+ ctx.fillRect(x + w - 3, y, 3, h);
544
+ // Inner border
545
+ ctx.fillStyle = '#DAA520';
546
+ ctx.fillRect(x + 6, y + 6, w - 12, 2);
547
+ ctx.fillRect(x + 6, y + h - 8, w - 12, 2);
548
+ ctx.fillRect(x + 6, y + 6, 2, h - 12);
549
+ ctx.fillRect(x + w - 8, y + 6, 2, h - 12);
550
+ // Center medallion
551
+ ctx.fillStyle = '#8B4513';
552
+ const cx = x + w / 2, cy = y + h / 2;
553
+ ctx.beginPath();
554
+ ctx.ellipse(cx, cy, w / 5, h / 4, 0, 0, Math.PI * 2);
555
+ ctx.fill();
556
+ ctx.fillStyle = '#B8860B';
557
+ ctx.beginPath();
558
+ ctx.ellipse(cx, cy, w / 7, h / 6, 0, 0, Math.PI * 2);
559
+ ctx.fill();
560
+ }
561
+
562
+ function drawGoldPictureFrame(ctx, x, y, s) {
563
+ // Ornate gold frame
564
+ ctx.fillStyle = '#B8860B';
565
+ ctx.fillRect(x, y, 10 * s, 8 * s);
566
+ ctx.fillStyle = '#DAA520';
567
+ ctx.fillRect(x + s * 0.5, y + s * 0.5, 9 * s, 7 * s);
568
+ // Picture inside - abstract art
569
+ ctx.fillStyle = '#1a1a3a';
570
+ ctx.fillRect(x + s, y + s, 8 * s, 6 * s);
571
+ ctx.fillStyle = '#C0392B';
572
+ ctx.fillRect(x + 2 * s, y + 2 * s, 3 * s, 4 * s);
573
+ ctx.fillStyle = '#2980B9';
574
+ ctx.fillRect(x + 5 * s, y + 2 * s, 3 * s, 2 * s);
575
+ ctx.fillStyle = '#F39C12';
576
+ ctx.fillRect(x + 5 * s, y + 4 * s, 3 * s, 2 * s);
577
+ }
578
+
579
+ function drawCoffeeTable(ctx, x, y, s) {
580
+ // Glass coffee table
581
+ ctx.fillStyle = '#666';
582
+ ctx.fillRect(x, y, 10 * s, 6 * s);
583
+ ctx.fillStyle = 'rgba(200,230,255,0.3)';
584
+ ctx.fillRect(x + s * 0.5, y + s * 0.5, 9 * s, 5 * s);
585
+ // Reflection
586
+ ctx.fillStyle = 'rgba(255,255,255,0.1)';
587
+ ctx.fillRect(x + s, y + s, 4 * s, 2 * s);
588
+ // Legs
589
+ ctx.fillStyle = '#888';
590
+ ctx.fillRect(x + s, y + 5 * s, s, s);
591
+ ctx.fillRect(x + 8 * s, y + 5 * s, s, s);
592
+ }
593
+
594
+ // ===== Additional Boss Office Luxury Furniture =====
595
+ function drawWineCabinet(ctx, x, y, s) {
596
+ // Tall mahogany wine cabinet
597
+ ctx.fillStyle = '#3D1F0F';
598
+ ctx.fillRect(x, y - 14 * s, 8 * s, 16 * s);
599
+ // Inner shelf panels
600
+ ctx.fillStyle = '#5C3317';
601
+ ctx.fillRect(x + s, y - 13 * s, 6 * s, 14 * s);
602
+ // Glass front
603
+ ctx.fillStyle = 'rgba(200,230,255,0.12)';
604
+ ctx.fillRect(x + s, y - 13 * s, 6 * s, 8 * s);
605
+ // Shelves
606
+ ctx.fillStyle = '#4E2B14';
607
+ for (let i = 0; i < 4; i++) {
608
+ ctx.fillRect(x + s, y - 13 * s + i * 3.5 * s, 6 * s, s * 0.5);
609
+ }
610
+ // Wine bottles (colored)
611
+ const bottleColors = ['#4A0E0E', '#2E1A47', '#1A3A1A', '#4A0E0E', '#2E1A47', '#5A2020'];
612
+ for (let row = 0; row < 3; row++) {
613
+ for (let col = 0; col < 3; col++) {
614
+ ctx.fillStyle = bottleColors[(row * 3 + col) % bottleColors.length];
615
+ ctx.fillRect(x + (1.5 + col * 2) * s, y - 12.5 * s + row * 3.5 * s, s, 2.5 * s);
616
+ // Bottle cap
617
+ ctx.fillStyle = '#B8860B';
618
+ ctx.fillRect(x + (1.5 + col * 2) * s, y - 12.5 * s + row * 3.5 * s, s, s * 0.4);
619
+ }
620
+ }
621
+ // Gold handles
622
+ ctx.fillStyle = '#B8860B';
623
+ ctx.fillRect(x + 3 * s, y - 4 * s, s, s);
624
+ ctx.fillRect(x + 5 * s, y - 4 * s, s, s);
625
+ // Crown molding on top
626
+ ctx.fillStyle = '#B8860B';
627
+ ctx.fillRect(x - s * 0.5, y - 14.5 * s, 9 * s, s);
628
+ }
629
+
630
+ function drawGlobe(ctx, x, y, s) {
631
+ // Decorative globe on brass stand
632
+ // Stand base
633
+ ctx.fillStyle = '#8B6914';
634
+ ctx.fillRect(x - 2 * s, y + 3 * s, 6 * s, s);
635
+ ctx.fillStyle = '#A07828';
636
+ ctx.fillRect(x - s, y + 2 * s, 4 * s, s);
637
+ // Stand pole
638
+ ctx.fillStyle = '#B8860B';
639
+ ctx.fillRect(x + s * 0.5, y - s, s, 3 * s);
640
+ // Globe arc frame
641
+ ctx.fillStyle = '#DAA520';
642
+ ctx.beginPath();
643
+ ctx.arc(x + s, y - 3 * s, 3.5 * s, 0, Math.PI * 2);
644
+ ctx.fill();
645
+ // Globe body (blue/green)
646
+ ctx.fillStyle = '#1a5276';
647
+ ctx.beginPath();
648
+ ctx.arc(x + s, y - 3 * s, 3 * s, 0, Math.PI * 2);
649
+ ctx.fill();
650
+ // Continents (simplified green patches)
651
+ ctx.fillStyle = '#1e8449';
652
+ ctx.fillRect(x - s, y - 5 * s, 2 * s, 2 * s);
653
+ ctx.fillRect(x + s, y - 3 * s, 2 * s, s);
654
+ ctx.fillRect(x - 0.5 * s, y - 2 * s, s, s);
655
+ // Equator line
656
+ ctx.fillStyle = 'rgba(255,255,255,0.15)';
657
+ ctx.fillRect(x - 2.5 * s, y - 3.3 * s, 7 * s, s * 0.3);
658
+ // Shine
659
+ ctx.fillStyle = 'rgba(255,255,255,0.2)';
660
+ ctx.fillRect(x - s, y - 5 * s, s, s);
661
+ }
662
+
663
+ function drawFloorLamp(ctx, x, y, s) {
664
+ // Elegant brass floor lamp
665
+ // Base
666
+ ctx.fillStyle = '#8B6914';
667
+ ctx.fillRect(x - s, y + s, 4 * s, s);
668
+ ctx.fillStyle = '#A07828';
669
+ ctx.fillRect(x, y - 2 * s, 2 * s, 3 * s);
670
+ // Tall pole
671
+ ctx.fillStyle = '#B8860B';
672
+ ctx.fillRect(x + s * 0.5, y - 14 * s, s, 12 * s);
673
+ // Lamp shade (cream colored)
674
+ ctx.fillStyle = '#D4C5A9';
675
+ ctx.fillRect(x - 2 * s, y - 18 * s, 6 * s, 4 * s);
676
+ ctx.fillStyle = '#E8DCC8';
677
+ ctx.fillRect(x - s, y - 17 * s, 4 * s, 2 * s);
678
+ // Gold trim
679
+ ctx.fillStyle = '#B8860B';
680
+ ctx.fillRect(x - 2 * s, y - 18 * s, 6 * s, s * 0.5);
681
+ ctx.fillRect(x - 2 * s, y - 14 * s, 6 * s, s * 0.5);
682
+ // Warm glow
683
+ ctx.fillStyle = 'rgba(255,245,210,0.08)';
684
+ ctx.beginPath();
685
+ ctx.arc(x + s, y - 12 * s, 10 * s, 0, Math.PI * 2);
686
+ ctx.fill();
687
+ }
688
+
689
+ function drawSideTable(ctx, x, y, s) {
690
+ // Small round side table with items
691
+ // Table surface
692
+ ctx.fillStyle = '#5C3317';
693
+ ctx.fillRect(x, y, 6 * s, 4 * s);
694
+ ctx.fillStyle = '#6B3D1E';
695
+ ctx.fillRect(x + s * 0.5, y + s * 0.5, 5 * s, 3 * s);
696
+ // Legs
697
+ ctx.fillStyle = '#3D1F0F';
698
+ ctx.fillRect(x + s * 0.5, y + 4 * s, s, 2 * s);
699
+ ctx.fillRect(x + 4.5 * s, y + 4 * s, s, 2 * s);
700
+ // Whiskey glass on table
701
+ ctx.fillStyle = 'rgba(200,160,80,0.6)';
702
+ ctx.fillRect(x + s, y - s, 2 * s, s);
703
+ ctx.fillStyle = 'rgba(200,200,200,0.3)';
704
+ ctx.fillRect(x + s, y - 2 * s, 2 * s, 2 * s);
705
+ // Small lamp
706
+ ctx.fillStyle = '#B8860B';
707
+ ctx.fillRect(x + 4 * s, y - s, s, s);
708
+ ctx.fillStyle = '#D4C5A9';
709
+ ctx.fillRect(x + 3.5 * s, y - 3 * s, 2 * s, 2 * s);
710
+ ctx.fillStyle = 'rgba(255,245,210,0.1)';
711
+ ctx.beginPath();
712
+ ctx.arc(x + 4.5 * s, y - 2 * s, 4 * s, 0, Math.PI * 2);
713
+ ctx.fill();
714
+ }
715
+
716
+ function drawAwardShelf(ctx, x, y, s) {
717
+ // Wall-mounted award/trophy shelf
718
+ // Shelf bracket
719
+ ctx.fillStyle = '#5C3317';
720
+ ctx.fillRect(x, y, 10 * s, s);
721
+ ctx.fillStyle = '#3D1F0F';
722
+ ctx.fillRect(x + s, y + s, s, 2 * s);
723
+ ctx.fillRect(x + 8 * s, y + s, s, 2 * s);
724
+ // Trophies
725
+ // Gold trophy
726
+ ctx.fillStyle = '#FFD700';
727
+ ctx.fillRect(x + s, y - 4 * s, 2 * s, 4 * s);
728
+ ctx.fillRect(x + s * 0.5, y - 5 * s, 3 * s, s);
729
+ ctx.fillRect(x + s * 0.5, y - s, 3 * s, s);
730
+ // Silver trophy
731
+ ctx.fillStyle = '#C0C0C0';
732
+ ctx.fillRect(x + 4.5 * s, y - 3 * s, 2 * s, 3 * s);
733
+ ctx.fillRect(x + 4 * s, y - 4 * s, 3 * s, s);
734
+ ctx.fillRect(x + 4 * s, y - s, 3 * s, s);
735
+ // Bronze trophy
736
+ ctx.fillStyle = '#CD7F32';
737
+ ctx.fillRect(x + 8 * s, y - 3 * s, s, 3 * s);
738
+ ctx.fillRect(x + 7.5 * s, y - 4 * s, 2 * s, s);
739
+ ctx.fillRect(x + 7.5 * s, y - s, 2 * s, s);
740
+ }
741
+
742
+ function drawLuxuryChair(ctx, x, y, s) {
743
+ // Executive leather armchair (single, for guest seating)
744
+ // Seat
745
+ ctx.fillStyle = '#3D1F0F';
746
+ ctx.fillRect(x, y, 8 * s, 5 * s);
747
+ ctx.fillStyle = '#5C3317';
748
+ ctx.fillRect(x + s, y + s, 6 * s, 3 * s);
749
+ // Back
750
+ ctx.fillStyle = '#3D1F0F';
751
+ ctx.fillRect(x + s, y - 3 * s, 6 * s, 4 * s);
752
+ ctx.fillStyle = '#4E2B14';
753
+ ctx.fillRect(x + 2 * s, y - 2 * s, 4 * s, 2 * s);
754
+ // Button tufting
755
+ ctx.fillStyle = '#8B6914';
756
+ ctx.beginPath();
757
+ ctx.arc(x + 4 * s, y - s, s * 0.4, 0, Math.PI * 2);
758
+ ctx.fill();
759
+ // Arms
760
+ ctx.fillStyle = '#3D1F0F';
761
+ ctx.fillRect(x - s, y - s, 2 * s, 6 * s);
762
+ ctx.fillRect(x + 7 * s, y - s, 2 * s, 6 * s);
763
+ // Arm top
764
+ ctx.fillStyle = '#5C3317';
765
+ ctx.fillRect(x - s, y - s, 2 * s, s);
766
+ ctx.fillRect(x + 7 * s, y - s, 2 * s, s);
767
+ }
768
+
769
+ function drawBossRoom(ctx, room, s) {
770
+ // === Premium floor ===
771
+ ctx.fillStyle = '#4A3728';
772
+ ctx.fillRect(room.x, room.y, room.w, room.h);
773
+ // Parquet floor pattern (herringbone style)
774
+ ctx.fillStyle = '#5C4A3A';
775
+ const tileSize = 14;
776
+ for (let tx = room.x; tx < room.x + room.w; tx += tileSize) {
777
+ for (let ty = room.y; ty < room.y + room.h; ty += tileSize) {
778
+ if ((Math.floor((tx - room.x) / tileSize) + Math.floor((ty - room.y) / tileSize)) % 2 === 0) {
779
+ ctx.fillRect(tx, ty, tileSize, tileSize);
780
+ }
781
+ }
782
+ }
783
+ // Additional floor sheen
784
+ ctx.fillStyle = 'rgba(255,220,160,0.02)';
785
+ ctx.fillRect(room.x, room.y, room.w, room.h);
786
+
787
+ // ========================================
788
+ // ZONE LAYOUT (top to bottom):
789
+ // [0-56] Panoramic window (wall zone)
790
+ // [56-110] Boss desk area (center) + bookshelf(left) + wine cabinet(right)
791
+ // [110-210] Secretary desk(right) + globe(left) + award shelf(wall)
792
+ // [210-310] Lounge: sofa(left) + chairs(right) + coffee table(center)
793
+ // [310-360] Bottom: plants, floor lamps, rug extends
794
+ // ========================================
795
+
796
+ // === Large luxury rug covering central area ===
797
+ drawLuxuryRug(ctx, room.x + 20, room.y + 200, room.w - 40, 130);
798
+ // Smaller accent rug near desk
799
+ ctx.fillStyle = 'rgba(139,69,19,0.15)';
800
+ ctx.fillRect(room.x + room.w / 2 - 60, room.y + 70, 120, 50);
801
+ ctx.fillStyle = 'rgba(184,134,11,0.08)';
802
+ ctx.fillRect(room.x + room.w / 2 - 56, room.y + 74, 112, 42);
803
+
804
+ // === Walls (mahogany paneling) ===
805
+ ctx.fillStyle = '#3D1F0F';
806
+ ctx.fillRect(room.x, room.y - 26, room.w, 26);
807
+ ctx.fillStyle = '#5C3317';
808
+ ctx.fillRect(room.x, room.y - 32, room.w, 6);
809
+ // Gold crown molding
810
+ ctx.fillStyle = '#B8860B';
811
+ ctx.fillRect(room.x, room.y - 33, room.w, 2);
812
+ // Side walls
813
+ ctx.fillStyle = '#3D1F0F';
814
+ ctx.fillRect(room.x - 6, room.y - 32, 6, room.h + 38);
815
+ ctx.fillRect(room.x + room.w, room.y - 32, 6, room.h + 38);
816
+ ctx.fillRect(room.x - 6, room.y + room.h, room.w + 12, 6);
817
+ // Wainscoting on side walls
818
+ ctx.fillStyle = '#4E2B14';
819
+ ctx.fillRect(room.x - 5, room.y + room.h * 0.5, 4, room.h * 0.5);
820
+ ctx.fillRect(room.x + room.w + 1, room.y + room.h * 0.5, 4, room.h * 0.5);
821
+
822
+ // === Door (mahogany with gold handle) ===
823
+ ctx.fillStyle = '#4E2B14';
824
+ ctx.fillRect(room.x + room.w / 2 - 14, room.y + room.h - 3, 28, 9);
825
+ ctx.fillStyle = '#6B3D1E';
826
+ ctx.fillRect(room.x + room.w / 2 - 12, room.y + room.h, 24, 6);
827
+ ctx.fillStyle = '#FFD700';
828
+ ctx.fillRect(room.x + room.w / 2 + 7, room.y + room.h + 1, 4, 4);
829
+
830
+ // =========================================================
831
+ // === PANORAMIC FLOOR-TO-CEILING WINDOW โ€” Modern Skyline ===
832
+ // =========================================================
833
+ const winMargin = 12;
834
+ const winX = room.x + winMargin;
835
+ const winY = room.y - 24;
836
+ const winW = room.w - winMargin * 2;
837
+ const winH = 56;
838
+
839
+ // Window frame (sleek steel frame)
840
+ ctx.fillStyle = '#A0A0A0';
841
+ ctx.fillRect(winX, winY, winW, winH);
842
+ ctx.fillStyle = '#C8C8C8';
843
+ ctx.fillRect(winX + 1, winY + 1, winW - 2, winH - 2);
844
+
845
+ // Sky gradient (dusk / golden hour)
846
+ const skyGrad = ctx.createLinearGradient(winX, winY, winX, winY + winH);
847
+ skyGrad.addColorStop(0, '#1a1a3e');
848
+ skyGrad.addColorStop(0.25, '#2d2b55');
849
+ skyGrad.addColorStop(0.45, '#4a3f6b');
850
+ skyGrad.addColorStop(0.65, '#c97b4b');
851
+ skyGrad.addColorStop(0.85, '#e8a050');
852
+ skyGrad.addColorStop(1, '#f0c878');
853
+ ctx.fillStyle = skyGrad;
854
+ ctx.fillRect(winX + 2, winY + 2, winW - 4, winH - 4);
855
+
856
+ // Stars
857
+ ctx.fillStyle = 'rgba(255,255,255,0.6)';
858
+ const starSeed = hashStr(room.id + 'stars');
859
+ for (let i = 0; i < 12; i++) {
860
+ const sx = winX + 4 + ((starSeed * (i + 1) * 7) % (winW - 8));
861
+ const sy = winY + 3 + ((starSeed * (i + 3) * 13) % 14);
862
+ ctx.fillRect(sx, sy, 1, 1);
863
+ }
864
+
865
+ // Moon (crescent)
866
+ ctx.fillStyle = 'rgba(255,255,230,0.9)';
867
+ ctx.beginPath();
868
+ ctx.arc(winX + winW - 30, winY + 12, 6, 0, Math.PI * 2);
869
+ ctx.fill();
870
+ ctx.fillStyle = '#2d2b55';
871
+ ctx.beginPath();
872
+ ctx.arc(winX + winW - 27, winY + 11, 5, 0, Math.PI * 2);
873
+ ctx.fill();
874
+
875
+ // === CITY SKYLINE ===
876
+ const skylineY = winY + winH - 2;
877
+ const buildingBaseY = skylineY;
878
+
879
+ ctx.fillStyle = 'rgba(40,35,60,0.8)';
880
+ const bgBuildings = [
881
+ { x: 0.05, w: 0.04, h: 0.45 }, { x: 0.1, w: 0.03, h: 0.55 },
882
+ { x: 0.15, w: 0.05, h: 0.35 }, { x: 0.22, w: 0.03, h: 0.60 },
883
+ { x: 0.28, w: 0.04, h: 0.40 }, { x: 0.35, w: 0.03, h: 0.50 },
884
+ { x: 0.42, w: 0.06, h: 0.30 }, { x: 0.52, w: 0.03, h: 0.55 },
885
+ { x: 0.58, w: 0.04, h: 0.38 }, { x: 0.65, w: 0.03, h: 0.62 },
886
+ { x: 0.72, w: 0.05, h: 0.42 }, { x: 0.78, w: 0.03, h: 0.50 },
887
+ { x: 0.85, w: 0.04, h: 0.35 }, { x: 0.92, w: 0.03, h: 0.48 },
888
+ ];
889
+ bgBuildings.forEach(b => {
890
+ const bx = winX + 2 + b.x * (winW - 4);
891
+ const bw = b.w * (winW - 4);
892
+ const bh = b.h * (winH * 0.6);
893
+ ctx.fillRect(bx, buildingBaseY - bh, bw, bh);
894
+ });
895
+
896
+ const fgBuildings = [
897
+ { x: 0.02, w: 0.07, h: 0.75, color: '#2a2a4a' },
898
+ { x: 0.10, w: 0.05, h: 0.85, color: '#333355' },
899
+ { x: 0.17, w: 0.08, h: 0.60, color: '#2d2d50' },
900
+ { x: 0.27, w: 0.05, h: 0.90, color: '#353560' },
901
+ { x: 0.34, w: 0.06, h: 0.70, color: '#303050' },
902
+ { x: 0.42, w: 0.09, h: 0.55, color: '#282848' },
903
+ { x: 0.53, w: 0.05, h: 0.95, color: '#333360' },
904
+ { x: 0.60, w: 0.07, h: 0.65, color: '#2a2a50' },
905
+ { x: 0.69, w: 0.05, h: 0.80, color: '#353558' },
906
+ { x: 0.76, w: 0.08, h: 0.58, color: '#2d2d4a' },
907
+ { x: 0.86, w: 0.05, h: 0.72, color: '#303055' },
908
+ { x: 0.93, w: 0.06, h: 0.50, color: '#282845' },
909
+ ];
910
+ fgBuildings.forEach((b, bi) => {
911
+ const bx = winX + 2 + b.x * (winW - 4);
912
+ const bw = b.w * (winW - 4);
913
+ const bh = b.h * (winH * 0.65);
914
+ const by = buildingBaseY - bh;
915
+ ctx.fillStyle = b.color;
916
+ ctx.fillRect(bx, by, bw, bh);
917
+ ctx.fillStyle = 'rgba(255,255,255,0.08)';
918
+ ctx.fillRect(bx, by, bw, 2);
919
+ if (b.h > 0.8 && bw > 6) {
920
+ ctx.fillStyle = 'rgba(200,200,200,0.5)';
921
+ ctx.fillRect(bx + bw / 2, by - 5, 1, 5);
922
+ ctx.fillStyle = '#ff3333';
923
+ ctx.fillRect(bx + bw / 2, by - 6, 1, 1);
924
+ }
925
+ const winSpacingX = Math.max(3, Math.floor(bw / 4));
926
+ const winSpacingY = 4;
927
+ const wSeed = hashStr(room.id + 'bld' + bi);
928
+ for (let wy = by + 4; wy < buildingBaseY - 3; wy += winSpacingY) {
929
+ for (let wx = bx + 2; wx < bx + bw - 2; wx += winSpacingX) {
930
+ const lit = ((wSeed + wx * 7 + wy * 13) % 5) < 3;
931
+ if (lit) {
932
+ ctx.fillStyle = ((wSeed + wx + wy) % 3 === 0)
933
+ ? 'rgba(255,240,180,0.7)' : 'rgba(200,220,255,0.5)';
934
+ ctx.fillRect(wx, wy, 2, 2);
935
+ }
936
+ }
937
+ }
938
+ });
939
+
940
+ ctx.fillStyle = 'rgba(232,160,80,0.15)';
941
+ ctx.fillRect(winX + 2, buildingBaseY - 6, winW - 4, 6);
942
+
943
+ // Window dividers
944
+ ctx.fillStyle = '#B0B0B0';
945
+ const divCount = Math.max(2, Math.floor(winW / 80));
946
+ for (let d = 1; d < divCount; d++) {
947
+ ctx.fillRect(winX + d * (winW / divCount), winY, 1, winH);
948
+ }
949
+ ctx.fillRect(winX, winY + winH * 0.45, winW, 1);
950
+
951
+ // Warm light cast into room
952
+ ctx.fillStyle = 'rgba(255,240,200,0.04)';
953
+ ctx.beginPath();
954
+ ctx.moveTo(winX, winY + winH);
955
+ ctx.lineTo(winX - 10, room.y + room.h);
956
+ ctx.lineTo(winX + winW + 10, room.y + room.h);
957
+ ctx.lineTo(winX + winW, winY + winH);
958
+ ctx.closePath();
959
+ ctx.fill();
960
+
961
+ // === Crystal chandelier (centered) ===
962
+ const chandelierX = room.x + room.w / 2;
963
+ ctx.fillStyle = '#FFD700';
964
+ ctx.fillRect(chandelierX - s, room.y + 38, 2 * s, 2 * s);
965
+ ctx.fillStyle = '#DAA520';
966
+ ctx.fillRect(chandelierX - 5 * s, room.y + 40, 10 * s, s);
967
+ ctx.fillStyle = 'rgba(255,255,255,0.4)';
968
+ for (let i = -4; i <= 4; i++) {
969
+ ctx.fillRect(chandelierX + i * s, room.y + 41, s * 0.5, 2 * s);
970
+ }
971
+ ctx.fillStyle = 'rgba(255,255,200,0.06)';
972
+ ctx.beginPath();
973
+ ctx.arc(chandelierX, room.y + 44, 35 * s * 0.3, 0, Math.PI * 2);
974
+ ctx.fill();
975
+
976
+ // =====================================================
977
+ // ZONE 1: BOSS DESK AREA (y+65 to y+140)
978
+ // Left: File cabinet + Award shelf on wall
979
+ // Center: Executive desk with dual monitors
980
+ // Right: Bookshelf
981
+ // =====================================================
982
+
983
+ // Award shelf on left wall
984
+ drawAwardShelf(ctx, room.x + 12, room.y + 60, s * 0.7);
985
+
986
+ // File cabinet (left side)
987
+ drawFileCabinet(ctx, room.x + 10, room.y + 115, s * 0.85);
988
+
989
+ // === Boss desk (large executive desk, centered) ===
990
+ const deskX = room.x + room.w / 2 - 20 * s * 0.5;
991
+ const deskY = room.y + 105;
992
+ ctx.fillStyle = '#3D1F0F';
993
+ ctx.fillRect(deskX, deskY, 20 * s, 8 * s);
994
+ ctx.fillStyle = '#5C3317';
995
+ ctx.fillRect(deskX + s, deskY + s, 18 * s, 6 * s);
996
+ ctx.fillStyle = '#6B3D1E';
997
+ ctx.fillRect(deskX + 2 * s, deskY + 2 * s, 16 * s, 4 * s);
998
+ ctx.fillStyle = '#B8860B';
999
+ ctx.fillRect(deskX, deskY, 20 * s, s * 0.5);
1000
+ // Desk drawer handles
1001
+ ctx.fillStyle = '#DAA520';
1002
+ ctx.fillRect(deskX + 3 * s, deskY + 5 * s, 2 * s, s * 0.5);
1003
+ ctx.fillRect(deskX + 15 * s, deskY + 5 * s, 2 * s, s * 0.5);
1004
+
1005
+ // Dual monitors on desk
1006
+ ctx.fillStyle = '#222';
1007
+ ctx.fillRect(deskX + 4 * s, deskY - 5 * s, 5 * s, 5 * s);
1008
+ ctx.fillStyle = '#3a7a9a';
1009
+ ctx.fillRect(deskX + 5 * s, deskY - 4 * s, 3 * s, 3 * s);
1010
+ ctx.fillStyle = '#555';
1011
+ ctx.fillRect(deskX + 5.5 * s, deskY, 2 * s, s);
1012
+ ctx.fillStyle = '#222';
1013
+ ctx.fillRect(deskX + 11 * s, deskY - 5 * s, 5 * s, 5 * s);
1014
+ ctx.fillStyle = '#3a7a9a';
1015
+ ctx.fillRect(deskX + 12 * s, deskY - 4 * s, 3 * s, 3 * s);
1016
+ ctx.fillStyle = '#555';
1017
+ ctx.fillRect(deskX + 12.5 * s, deskY, 2 * s, s);
1018
+ // Pen holder on desk
1019
+ ctx.fillStyle = '#333';
1020
+ ctx.fillRect(deskX + 18 * s, deskY + s, 2 * s, 3 * s);
1021
+ ctx.fillStyle = '#E74C3C';
1022
+ ctx.fillRect(deskX + 18 * s, deskY - s, s * 0.5, 2 * s);
1023
+ ctx.fillStyle = '#3498DB';
1024
+ ctx.fillRect(deskX + 19 * s, deskY - s, s * 0.5, 2 * s);
1025
+
1026
+ // Bookshelf (right side, near wall)
1027
+ drawBookshelf(ctx, room.x + room.w - 42, room.y + 75, s * 0.9);
1028
+
1029
+ // =====================================================
1030
+ // ZONE 2: SECRETARY AREA (y+140 to y+210)
1031
+ // Right side: Secretary desk
1032
+ // Left side: Globe on stand
1033
+ // =====================================================
1034
+
1035
+ // Secretary desk (right side)
1036
+ if (room.members.length > 1) {
1037
+ const secDeskX = room.x + room.w - 85;
1038
+ const secDeskY = room.y + 140;
1039
+ drawDesk(ctx, secDeskX, secDeskY, s);
1040
+ }
1041
+
1042
+ // Globe (left side, mid area)
1043
+ drawGlobe(ctx, room.x + 30, room.y + 185, s * 1.1);
1044
+
1045
+ // Gold picture frame on left wall (between zones)
1046
+ drawGoldPictureFrame(ctx, room.x + 12, room.y + 148, s * 0.65);
1047
+
1048
+ // =====================================================
1049
+ // ZONE 3: LOUNGE AREA (y+210 to y+310)
1050
+ // Left: Luxury sofa
1051
+ // Center: Coffee table
1052
+ // Right: Two guest armchairs
1053
+ // =====================================================
1054
+
1055
+ // Luxury sofa (left side of lounge)
1056
+ drawLuxuryCouch(ctx, room.x + 16, room.y + room.h - 100, s * 0.8);
1057
+
1058
+ // Coffee table (center of lounge)
1059
+ drawCoffeeTable(ctx, room.x + room.w / 2 - 15, room.y + room.h - 80, s * 0.9);
1060
+
1061
+ // Two guest armchairs (right side, facing the sofa)
1062
+ drawLuxuryChair(ctx, room.x + room.w - 55, room.y + room.h - 105, s * 0.75);
1063
+ drawLuxuryChair(ctx, room.x + room.w - 55, room.y + room.h - 75, s * 0.75);
1064
+
1065
+ // Side table between the chairs
1066
+ drawSideTable(ctx, room.x + room.w - 42, room.y + room.h - 90, s * 0.65);
1067
+
1068
+ // Wine cabinet (right wall, between zone 2 and 3)
1069
+ drawWineCabinet(ctx, room.x + room.w - 36, room.y + 210, s * 0.85);
1070
+
1071
+ // =====================================================
1072
+ // ZONE 4: FLOOR PERIMETER (decorations)
1073
+ // =====================================================
1074
+
1075
+ // Floor lamps (left and right corners)
1076
+ drawFloorLamp(ctx, room.x + 12, room.y + room.h - 30, s * 0.7);
1077
+ drawFloorLamp(ctx, room.x + room.w - 18, room.y + room.h - 30, s * 0.7);
1078
+
1079
+ // Large plants at bottom corners
1080
+ drawPlant(ctx, room.x + room.w - 26, room.y + room.h - 28, s);
1081
+ drawPlant(ctx, room.x + 40, room.y + room.h - 28, s);
1082
+ // Small plant near desk
1083
+ drawSmallPlant(ctx, room.x + room.w / 2 + 45, room.y + 135, s * 0.8);
1084
+ // Small plant near sofa
1085
+ drawSmallPlant(ctx, room.x + 60, room.y + room.h - 40, s * 0.7);
1086
+
1087
+ // === Room name sign (gold) ===
1088
+ ctx.fillStyle = 'rgba(0,0,0,0.8)';
1089
+ roundRect(ctx, room.x + 10, room.y - 29, Math.min(room.w - 20, 220), 20, 4);
1090
+ ctx.fill();
1091
+ ctx.fillStyle = '#FFD700';
1092
+ ctx.font = 'bold 12px "Courier New", monospace';
1093
+ ctx.textAlign = 'left';
1094
+ ctx.textBaseline = 'middle';
1095
+ ctx.fillText(room.name, room.x + 18, room.y - 19);
1096
+ }
1097
+
1098
+ // ===== Desk pair for 2x2 employees (side-view style, like secretary desk) =====
1099
+ // Layout (s=3, all relative to pairX, pairY which is the top-left of the desk group):
1100
+ //
1101
+ // WORKSTATION_W = 14s per person, total pair width = 30s (2 workstations + 2s gap)
1102
+ //
1103
+ // [top-left person] [top-right person] <- y offset: -14s (characters, ~12s tall)
1104
+ // [chair top] [chair top] <- y offset: -2s to 0
1105
+ // [monitor + desk top] [monitor + desk top] <- y offset: 0 to 6s (desk=6s, monitor on desk -5s to 0)
1106
+ // ========= divider ========= <- y offset: 6s to 7s
1107
+ // [monitor + desk bottom] [monitor + desk bottom] <- y offset: 7s to 13s
1108
+ // [chair bottom] [chair bottom] <- y offset: 13s to 15s
1109
+ // [bottom-left person] [bottom-right person] <- y offset: 15s (characters sit here)
1110
+ //
1111
+ // Total height: from top-person (-14s) to bottom-person-feet (+20s) = 34s
1112
+ // Obstacle area: desks from y to y+13s
1113
+
1114
+ const DESK_PAIR_W_UNITS = 30; // total width in s units
1115
+ const DESK_PAIR_TOTAL_H_UNITS = 44; // full height including person space top(-14s) to bottom(+30s)
1116
+ const WORKSTATION_W = 14; // width of one workstation in s units
1117
+ const WORKSTATION_GAP = 2; // gap between left and right workstation
1118
+
1119
+ function drawDeskPair(ctx, x, y, s) {
1120
+ // y = top of the upper desk surface
1121
+ // Each side has 2 workstations side by side
1122
+
1123
+ const ws = WORKSTATION_W * s; // workstation pixel width
1124
+ const gap = WORKSTATION_GAP * s;
1125
+
1126
+ // Workstation X positions (left edge of each workstation)
1127
+ const wx0 = x; // left workstation
1128
+ const wx1 = x + ws + gap; // right workstation
1129
+
1130
+ // --- Draw each workstation (same style as secretary drawDesk) ---
1131
+ // TOP ROW (facing down, monitors face top-row people above)
1132
+ drawWorkstation(ctx, wx0, y, s, 'top');
1133
+ drawWorkstation(ctx, wx1, y, s, 'top');
1134
+
1135
+ // === DIVIDER between top and bottom rows ===
1136
+ ctx.fillStyle = '#666';
1137
+ ctx.fillRect(x, y + 6 * s, (DESK_PAIR_W_UNITS) * s, s);
1138
+ // Divider highlight
1139
+ ctx.fillStyle = '#888';
1140
+ ctx.fillRect(x, y + 6 * s, (DESK_PAIR_W_UNITS) * s, s * 0.3);
1141
+
1142
+ // BOTTOM ROW (facing up, monitors face bottom-row people below)
1143
+ drawWorkstation(ctx, wx0, y + 7 * s, s, 'bottom');
1144
+ drawWorkstation(ctx, wx1, y + 7 * s, s, 'bottom');
1145
+ }
1146
+
1147
+ // Draw a single workstation: desk surface + monitor + keyboard + chair
1148
+ // direction: 'top' = person sits above, 'bottom' = person sits below
1149
+ function drawWorkstation(ctx, x, y, s, direction) {
1150
+ // Desk surface: 12s wide, 6s tall (same as secretary)
1151
+ const deskW = 12 * s;
1152
+ const deskH = 6 * s;
1153
+ const deskX = x + s; // 1s margin from workstation edge
1154
+
1155
+ ctx.fillStyle = '#8B6914';
1156
+ ctx.fillRect(deskX, y, deskW, deskH);
1157
+ ctx.fillStyle = '#A07828';
1158
+ ctx.fillRect(deskX + s, y + s, (12 - 2) * s, (6 - 2) * s);
1159
+
1160
+ if (direction === 'top') {
1161
+ // Person sits ABOVE the desk, looking down at monitor
1162
+ // Monitor sits ON TOP of desk, visible above desk surface (like secretary)
1163
+ // Monitor body
1164
+ ctx.fillStyle = '#333';
1165
+ ctx.fillRect(deskX + 3 * s, y - 5 * s, 6 * s, 5 * s);
1166
+ // Screen
1167
+ ctx.fillStyle = '#4488AA';
1168
+ ctx.fillRect(deskX + 4 * s, y - 4 * s, 4 * s, 3 * s);
1169
+ // Monitor stand
1170
+ ctx.fillStyle = '#555';
1171
+ ctx.fillRect(deskX + 5 * s, y, 2 * s, s);
1172
+ // Screen glare
1173
+ ctx.fillStyle = 'rgba(255,255,255,0.15)';
1174
+ ctx.fillRect(deskX + 4 * s, y - 4 * s, 2 * s, s);
1175
+
1176
+ // Keyboard (above desk, between person and monitor)
1177
+ ctx.fillStyle = '#3a3a3a';
1178
+ ctx.fillRect(deskX + 2 * s, y - 7 * s, 5 * s, s);
1179
+ // Key dots
1180
+ ctx.fillStyle = '#555';
1181
+ for (let i = 0; i < 4; i++) {
1182
+ ctx.fillRect(deskX + (2.5 + i) * s, y - 6.7 * s, s * 0.5, s * 0.4);
1183
+ }
1184
+ // Mouse
1185
+ ctx.fillStyle = '#555';
1186
+ ctx.fillRect(deskX + 8 * s, y - 7 * s, 2 * s, s);
1187
+
1188
+ // Chair
1189
+ ctx.fillStyle = 'rgba(80,80,80,0.5)';
1190
+ ctx.fillRect(deskX + 2 * s, y - 9 * s, 8 * s, 2 * s);
1191
+ // Chair back
1192
+ ctx.fillStyle = 'rgba(60,60,60,0.4)';
1193
+ ctx.fillRect(deskX + 3 * s, y - 11 * s, 6 * s, 2 * s);
1194
+
1195
+ } else {
1196
+ // Person sits BELOW the desk, looking up at monitor
1197
+ // Monitor sits below desk surface, facing the bottom person
1198
+ // Monitor body
1199
+ ctx.fillStyle = '#333';
1200
+ ctx.fillRect(deskX + 3 * s, y + deskH, 6 * s, 5 * s);
1201
+ // Screen (facing the bottom person)
1202
+ ctx.fillStyle = '#4488AA';
1203
+ ctx.fillRect(deskX + 4 * s, y + deskH + s, 4 * s, 3 * s);
1204
+ // Monitor stand
1205
+ ctx.fillStyle = '#555';
1206
+ ctx.fillRect(deskX + 5 * s, y + deskH - s, 2 * s, s);
1207
+ // Screen glare
1208
+ ctx.fillStyle = 'rgba(255,255,255,0.15)';
1209
+ ctx.fillRect(deskX + 4 * s, y + deskH + s, 2 * s, s);
1210
+
1211
+ // Keyboard (further below monitor, more space for person)
1212
+ ctx.fillStyle = '#3a3a3a';
1213
+ ctx.fillRect(deskX + 2 * s, y + deskH + 8 * s, 5 * s, s);
1214
+ // Key dots
1215
+ ctx.fillStyle = '#555';
1216
+ for (let i = 0; i < 4; i++) {
1217
+ ctx.fillRect(deskX + (2.5 + i) * s, y + deskH + 8.3 * s, s * 0.5, s * 0.4);
1218
+ }
1219
+ // Mouse
1220
+ ctx.fillStyle = '#555';
1221
+ ctx.fillRect(deskX + 8 * s, y + deskH + 8 * s, 2 * s, s);
1222
+
1223
+ // Chair (further down)
1224
+ ctx.fillStyle = 'rgba(80,80,80,0.5)';
1225
+ ctx.fillRect(deskX + 2 * s, y + deskH + 10 * s, 8 * s, 2 * s);
1226
+ // Chair back
1227
+ ctx.fillStyle = 'rgba(60,60,60,0.4)';
1228
+ ctx.fillRect(deskX + 3 * s, y + deskH + 12 * s, 6 * s, 2 * s);
1229
+ }
1230
+ }
1231
+
1232
+ // ===== Room Layout Calculator =====
1233
+ // Helper: compute room dimensions for a department based on member count and available width
1234
+ function computeRoomSize(memberCount, availW) {
1235
+ const s = 3;
1236
+ const deskW = DESK_PAIR_W_UNITS * s; // 90px
1237
+ const SMALL_THRESHOLD = 4; // <=4 people โ†’ small room (1 desk pair)
1238
+ const MED_THRESHOLD = 8; // <=8 people โ†’ medium room
1239
+
1240
+ // Determine room width class based on member count
1241
+ let roomW;
1242
+ if (memberCount <= SMALL_THRESHOLD) {
1243
+ // Small office: compact width โ€” fits 1 desk pair comfortably
1244
+ roomW = Math.min(availW, Math.max(220, deskW + 100));
1245
+ } else if (memberCount <= MED_THRESHOLD) {
1246
+ // Medium office: standard width
1247
+ roomW = Math.min(availW, Math.max(320, deskW + 180));
1248
+ } else {
1249
+ // Large office: full width
1250
+ roomW = availW;
1251
+ }
1252
+
1253
+ // Compute height based on desk pair rows
1254
+ const pairsPerRow = Math.max(1, Math.min(2, Math.floor((roomW - 60) / (deskW + 30))));
1255
+ const totalPairs = Math.ceil(memberCount / 4);
1256
+ const deskRows = Math.max(1, Math.ceil(totalPairs / pairsPerRow));
1257
+
1258
+ // Small rooms get a more compact base height
1259
+ let baseH;
1260
+ if (memberCount <= SMALL_THRESHOLD) {
1261
+ baseH = 240;
1262
+ } else {
1263
+ baseH = 300;
1264
+ }
1265
+ const extraH = Math.max(0, deskRows - 1) * (DESK_PAIR_TOTAL_H_UNITS * s + 40);
1266
+ const roomH = baseH + extraH;
1267
+
1268
+ return { roomW, roomH, sizeClass: memberCount <= SMALL_THRESHOLD ? 'small' : memberCount <= MED_THRESHOLD ? 'medium' : 'large' };
1269
+ }
1270
+
1271
+ function calculateRoomLayout(departments, canvasWidth, secretary, boss) {
1272
+ const rooms = [];
1273
+ const PADDING = 20;
1274
+ const fullColW = Math.max(380, canvasWidth - PADDING * 2);
1275
+
1276
+ let startY = PADDING + 36; // extra top space so room title signs are not clipped
1277
+
1278
+ // Boss office (secretary sits inside with the boss)
1279
+ if (boss) {
1280
+ const maxBossW = Math.min(fullColW, 480);
1281
+ const bossW = Math.max(380, maxBossW);
1282
+ const rowH = 360; // spacious luxury office with full furniture
1283
+
1284
+ const bossMembers = [{ id: '__boss__', name: boss.name || 'Boss', role: 'CEO' }];
1285
+ bossMembers.push({ id: '__secretary__', name: secretary?.name || 'Secretary', role: 'Secretary' });
1286
+
1287
+ rooms.push({
1288
+ id: '__boss__',
1289
+ name: `๐Ÿ‘” ${boss.name || 'Boss'}'s Office`,
1290
+ x: PADDING,
1291
+ y: startY,
1292
+ w: bossW,
1293
+ h: rowH,
1294
+ palette: { floor: '#4A3728', wall: '#3D1F0F', wallTop: '#5C3317', accent: '#B8860B' },
1295
+ members: bossMembers,
1296
+ isBoss: true,
1297
+ });
1298
+
1299
+ startY += rowH + PADDING;
1300
+ } else if (secretary) {
1301
+ rooms.push({
1302
+ id: '__secretary__',
1303
+ name: '๐Ÿ›๏ธ Secretary Office',
1304
+ x: PADDING,
1305
+ y: startY,
1306
+ w: Math.min(fullColW, canvasWidth - PADDING * 2),
1307
+ h: 200,
1308
+ palette: { floor: '#9B8B7B', wall: '#705A48', wallTop: '#A0886A', accent: '#D4B896' },
1309
+ members: [{ id: '__secretary__', name: secretary?.name || 'Secretary', role: 'Secretary' }],
1310
+ });
1311
+
1312
+ startY += 200 + PADDING;
1313
+ }
1314
+
1315
+ // Pre-compute size info for all departments
1316
+ const deptSizes = departments.map(dept => {
1317
+ const memberCount = (dept.members || []).length;
1318
+ return { ...computeRoomSize(memberCount, fullColW), memberCount };
1319
+ });
1320
+
1321
+ // === Bin-packing: greedily fill rows with rooms (left to right) ===
1322
+ // Each row has a max width of canvasWidth - 2*PADDING
1323
+ const rowMaxW = canvasWidth - PADDING * 2;
1324
+ const rowAssignments = []; // [{deptIdx, x, rowIdx}]
1325
+ let currentRow = 0;
1326
+ let currentRowX = 0;
1327
+ let currentRowRoomCount = 0;
1328
+ const rowHeights = {};
1329
+
1330
+ // Sort departments: large first for better packing, but keep original palette assignment
1331
+ const sortedIndices = departments.map((_, i) => i);
1332
+ // Pack: put large rooms first in each row, then fill with small ones
1333
+ // Actually, keep original order for visual consistency (departments appear in order)
1334
+
1335
+ sortedIndices.forEach((origIdx) => {
1336
+ const sz = deptSizes[origIdx];
1337
+ const neededW = sz.roomW;
1338
+
1339
+ // Check if this room fits in the current row
1340
+ const spaceLeft = rowMaxW - currentRowX;
1341
+ if (currentRowRoomCount > 0 && neededW + PADDING > spaceLeft) {
1342
+ // Move to next row
1343
+ currentRow++;
1344
+ currentRowX = 0;
1345
+ currentRowRoomCount = 0;
1346
+ }
1347
+
1348
+ // Also limit to max 3 rooms per row for readability
1349
+ if (currentRowRoomCount >= 3) {
1350
+ currentRow++;
1351
+ currentRowX = 0;
1352
+ currentRowRoomCount = 0;
1353
+ }
1354
+
1355
+ rowAssignments.push({
1356
+ origIdx,
1357
+ rowIdx: currentRow,
1358
+ x: PADDING + currentRowX,
1359
+ });
1360
+
1361
+ rowHeights[currentRow] = Math.max(rowHeights[currentRow] || 0, sz.roomH);
1362
+ currentRowX += neededW + PADDING;
1363
+ currentRowRoomCount++;
1364
+ });
1365
+
1366
+ // Now compute actual widths: if rooms in a row don't fill the full width,
1367
+ // proportionally expand them to fill available space (looks better)
1368
+ const rowRoomGroups = {};
1369
+ rowAssignments.forEach(ra => {
1370
+ if (!rowRoomGroups[ra.rowIdx]) rowRoomGroups[ra.rowIdx] = [];
1371
+ rowRoomGroups[ra.rowIdx].push(ra);
1372
+ });
1373
+
1374
+ Object.keys(rowRoomGroups).forEach(rowIdx => {
1375
+ const group = rowRoomGroups[rowIdx];
1376
+ const totalDesiredW = group.reduce((sum, ra) => sum + deptSizes[ra.origIdx].roomW, 0);
1377
+ const totalGaps = (group.length - 1) * PADDING;
1378
+ const availableW = rowMaxW - totalGaps;
1379
+
1380
+ // Proportionally distribute available width
1381
+ let xCursor = PADDING;
1382
+ group.forEach((ra) => {
1383
+ const desiredW = deptSizes[ra.origIdx].roomW;
1384
+ const expandedW = Math.floor((desiredW / totalDesiredW) * availableW);
1385
+ ra.x = xCursor;
1386
+ ra.expandedW = expandedW;
1387
+ xCursor += expandedW + PADDING;
1388
+
1389
+ // Recompute height with expanded width (more width may reduce desk rows)
1390
+ const memberCount = deptSizes[ra.origIdx].memberCount;
1391
+ const recomputed = computeRoomSize(memberCount, expandedW);
1392
+ ra.finalH = recomputed.roomH;
1393
+ rowHeights[rowIdx] = Math.max(rowHeights[rowIdx] || 0, ra.finalH);
1394
+ });
1395
+ });
1396
+
1397
+ // Create room objects
1398
+ departments.forEach((dept, i) => {
1399
+ const ra = rowAssignments.find(r => r.origIdx === i);
1400
+ const sz = deptSizes[i];
1401
+
1402
+ // Compute Y from accumulated row heights
1403
+ let yOffset = startY;
1404
+ for (let r = 0; r < ra.rowIdx; r++) {
1405
+ yOffset += (rowHeights[r] || 240) + PADDING + 16;
1406
+ }
1407
+
1408
+ const finalW = ra.expandedW || sz.roomW;
1409
+ const finalH = rowHeights[ra.rowIdx] || sz.roomH;
1410
+
1411
+ rooms.push({
1412
+ id: dept.id,
1413
+ name: dept.name,
1414
+ x: ra.x,
1415
+ y: yOffset,
1416
+ w: finalW,
1417
+ h: finalH,
1418
+ palette: ROOM_PALETTES[i % ROOM_PALETTES.length],
1419
+ sizeClass: sz.sizeClass,
1420
+ members: (dept.members || []).map(m => ({
1421
+ id: m.id,
1422
+ name: m.name,
1423
+ role: m.role,
1424
+ avatar: m.avatar,
1425
+ })),
1426
+ });
1427
+ });
1428
+
1429
+ const lastRoom = rooms[rooms.length - 1];
1430
+ const totalH = lastRoom ? lastRoom.y + lastRoom.h + PADDING * 2 : 400;
1431
+
1432
+ return { rooms, totalH };
1433
+ }
1434
+
1435
+ // Calculate fixed desk seat positions for each member in a room.
1436
+ // Also returns desk obstacle rectangles for collision detection.
1437
+ //
1438
+ // New side-view desk pair layout (s=3, relative to pairX, pairY):
1439
+ // pairY = top of upper desk surface
1440
+ //
1441
+ // Top person: y - 16s (high enough to not overlap with monitor/keyboard)
1442
+ // Chair+keyboard: y - 11s to y - 7s
1443
+ // Monitor (top): y - 5s to y (on desk, facing top person)
1444
+ // Top desk: y to y+6s <- OBSTACLE
1445
+ // Divider: y+6s to y+7s <- OBSTACLE
1446
+ // Bottom desk: y+7s to y+13s <- OBSTACLE
1447
+ // Monitor (bot): y+13s to y+18s (on desk, facing bottom person)
1448
+ // Chair+keyboard: y+19s to y+23s
1449
+ // Bottom person: y + 28s
1450
+ //
1451
+ // Width: 30s (2 workstations of 14s + 2s gap)
1452
+ //
1453
+ // Seats:
1454
+ // Top-left: (pairX + 7s, pairY - 14s)
1455
+ // Top-right: (pairX + 23s, pairY - 14s)
1456
+ // Bot-left: (pairX + 7s, pairY + 30s)
1457
+ // Bot-right: (pairX + 23s, pairY + 30s)
1458
+
1459
+ function getSeatPositions(room) {
1460
+ const seats = [];
1461
+ const obstacles = []; // desk rectangles for collision
1462
+ const memberCount = room.members.length;
1463
+ if (memberCount === 0) return { seats, obstacles };
1464
+
1465
+ const s = 3; // must match pixel scale
1466
+
1467
+ if (room.id === '__boss__') {
1468
+ // Boss sits behind the executive desk (center, upper-mid area)
1469
+ seats.push({
1470
+ x: room.x + room.w / 2,
1471
+ y: room.y + 150,
1472
+ deskX: room.x + room.w / 2 - 30,
1473
+ deskY: room.y + 105,
1474
+ });
1475
+ obstacles.push({
1476
+ x: room.x + room.w / 2 - 30,
1477
+ y: room.y + 105,
1478
+ w: 20 * s,
1479
+ h: 8 * s,
1480
+ });
1481
+ // Secretary sits at a smaller desk on the right side, mid-height
1482
+ if (room.members.length > 1) {
1483
+ seats.push({
1484
+ x: room.x + room.w - 65,
1485
+ y: room.y + 175,
1486
+ deskX: room.x + room.w - 85,
1487
+ deskY: room.y + 140,
1488
+ });
1489
+ obstacles.push({
1490
+ x: room.x + room.w - 85,
1491
+ y: room.y + 140,
1492
+ w: 12 * s,
1493
+ h: 6 * s,
1494
+ });
1495
+ }
1496
+ return { seats, obstacles };
1497
+ }
1498
+
1499
+ if (room.id === '__secretary__') {
1500
+ seats.push({
1501
+ x: room.x + room.w / 2,
1502
+ y: room.y + 90,
1503
+ deskX: room.x + room.w / 2 - 18,
1504
+ deskY: room.y + 60,
1505
+ });
1506
+ // Secretary desk obstacle
1507
+ obstacles.push({
1508
+ x: room.x + room.w / 2 - 18,
1509
+ y: room.y + 60,
1510
+ w: 12 * s,
1511
+ h: 6 * s,
1512
+ });
1513
+ return { seats, obstacles };
1514
+ }
1515
+
1516
+ const DESK_W = DESK_PAIR_W_UNITS * s; // 90px
1517
+ const DESK_GAP_X = 30;
1518
+ const DESK_GAP_Y = 40; // vertical gap between desk pair rows
1519
+
1520
+ const pairsPerRow = Math.max(1, Math.min(2, Math.floor((room.w - 60) / (DESK_W + DESK_GAP_X))));
1521
+
1522
+ // Center desk pairs horizontally in the room
1523
+ const totalDeskRowW = pairsPerRow * DESK_W + (pairsPerRow - 1) * DESK_GAP_X;
1524
+ const DESK_AREA_START_X = room.x + Math.max(15, Math.floor((room.w - totalDeskRowW) / 2));
1525
+ const DESK_AREA_START_Y = room.y + (room.w < 300 ? 60 : 80); // small rooms: tighter top margin
1526
+
1527
+ // Seat X offsets: center of each workstation
1528
+ // Left workstation desk starts at pairX + 1s, width 12s โ†’ center at pairX + 7s
1529
+ // Right workstation desk starts at pairX + (14+2+1)s = pairX + 17s, width 12s โ†’ center at pairX + 23s
1530
+ const seatOffX0 = 7 * s; // left seat center
1531
+ const seatOffX1 = 23 * s; // right seat center
1532
+
1533
+ let seatIndex = 0;
1534
+ for (let mi = 0; mi < memberCount; mi++) {
1535
+ const pairIndex = Math.floor(seatIndex / 4);
1536
+ const seatInPair = seatIndex % 4;
1537
+
1538
+ const pairCol = pairIndex % pairsPerRow;
1539
+ const pairRow = Math.floor(pairIndex / pairsPerRow);
1540
+
1541
+ const pairX = DESK_AREA_START_X + pairCol * (DESK_W + DESK_GAP_X);
1542
+ const pairY = DESK_AREA_START_Y + pairRow * (DESK_PAIR_TOTAL_H_UNITS * s + DESK_GAP_Y);
1543
+
1544
+ // Register obstacle for this desk pair
1545
+ // Covers: monitors(top) + desks + divider + monitors(bottom) + keyboard area
1546
+ const obstKey = `${pairCol}-${pairRow}`;
1547
+ if (!obstacles.find(o => o._key === obstKey)) {
1548
+ obstacles.push({
1549
+ _key: obstKey,
1550
+ x: pairX - s,
1551
+ y: pairY - 6 * s,
1552
+ w: DESK_W + 2 * s,
1553
+ h: 30 * s, // from top-monitor to bottom-keyboard area
1554
+ });
1555
+ }
1556
+
1557
+ const seatCol = seatInPair % 2; // 0=left, 1=right
1558
+ const seatRow = Math.floor(seatInPair / 2); // 0=top, 1=bottom
1559
+
1560
+ const seatX = pairX + (seatCol === 0 ? seatOffX0 : seatOffX1);
1561
+ // Top seats: above desk, high enough to be fully visible
1562
+ // Bottom seats: below desk, with enough space
1563
+ const seatY = seatRow === 0
1564
+ ? pairY - 14 * s
1565
+ : pairY + 30 * s;
1566
+
1567
+ seats.push({
1568
+ x: seatX,
1569
+ y: seatY,
1570
+ deskX: pairX,
1571
+ deskY: pairY,
1572
+ facing: seatRow === 0 ? 'down' : 'up',
1573
+ });
1574
+
1575
+ seatIndex++;
1576
+ }
1577
+
1578
+ return { seats, obstacles };
1579
+ }
1580
+
1581
+ // Check if a point is inside any obstacle rectangle (with padding)
1582
+ function isInsideAnyObstacle(px, py, obstacles, pad = 0) {
1583
+ for (const o of obstacles) {
1584
+ if (px >= o.x - pad && px <= o.x + o.w + pad &&
1585
+ py >= o.y - pad && py <= o.y + o.h + pad) {
1586
+ return true;
1587
+ }
1588
+ }
1589
+ return false;
1590
+ }
1591
+
1592
+ // ===== Agent Walking State Manager =====
1593
+ function useAgentPositions(rooms, frame) {
1594
+ const stateRef = useRef({});
1595
+
1596
+ // Clamp position within room boundaries (with margin)
1597
+ const clampToRoom = (x, y, room) => {
1598
+ const margin = 15;
1599
+ return {
1600
+ x: Math.max(room.x + margin, Math.min(room.x + room.w - margin, x)),
1601
+ y: Math.max(room.y + margin, Math.min(room.y + room.h - margin, y)),
1602
+ };
1603
+ };
1604
+
1605
+ const positions = useMemo(() => {
1606
+ const state = stateRef.current;
1607
+
1608
+ // Build a room lookup so we can detect if agent is in wrong room
1609
+ const roomById = {};
1610
+ rooms.forEach(room => { roomById[room.id] = room; });
1611
+
1612
+ rooms.forEach(room => {
1613
+ const { seats, obstacles } = getSeatPositions(room);
1614
+
1615
+ room.members.forEach((member, mi) => {
1616
+ const key = member.id;
1617
+ const seat = seats[mi];
1618
+ if (!seat) return;
1619
+
1620
+ if (!state[key]) {
1621
+ // Initialize: start exactly at desk seat, sitting
1622
+ state[key] = {
1623
+ x: seat.x,
1624
+ y: seat.y,
1625
+ direction: 0,
1626
+ sitting: true,
1627
+ target: null,
1628
+ seatX: seat.x,
1629
+ seatY: seat.y,
1630
+ idleTimer: Math.floor(Math.random() * 2000) + 1500, // ~50-120 seconds before first stand
1631
+ wanderTimer: 0,
1632
+ roomId: room.id,
1633
+ };
1634
+ }
1635
+
1636
+ const s = state[key];
1637
+
1638
+ // If seat position changed (room recalculated), snap to new seat
1639
+ if (s.seatX !== seat.x || s.seatY !== seat.y) {
1640
+ const wasSitting = s.sitting;
1641
+ s.seatX = seat.x;
1642
+ s.seatY = seat.y;
1643
+ if (wasSitting) {
1644
+ // Snap to new seat position
1645
+ s.x = seat.x;
1646
+ s.y = seat.y;
1647
+ }
1648
+ }
1649
+
1650
+ // If room changed, reset to seat
1651
+ if (s.roomId !== room.id) {
1652
+ s.roomId = room.id;
1653
+ s.x = seat.x;
1654
+ s.y = seat.y;
1655
+ s.sitting = true;
1656
+ s.target = null;
1657
+ s.direction = 0;
1658
+ s.idleTimer = Math.floor(Math.random() * 1500) + 1000;
1659
+ }
1660
+
1661
+ // Ensure current position is within room bounds
1662
+ if (!s.sitting) {
1663
+ const clamped = clampToRoom(s.x, s.y, room);
1664
+ s.x = clamped.x;
1665
+ s.y = clamped.y;
1666
+ }
1667
+
1668
+ if (s.sitting) {
1669
+ // Sitting at desk โ€” rarely stand up
1670
+ s.direction = 0;
1671
+ s.x = s.seatX;
1672
+ s.y = s.seatY;
1673
+ s.idleTimer--;
1674
+ if (s.idleTimer <= 0) {
1675
+ // Stand up and wander briefly
1676
+ s.sitting = false;
1677
+ s.wanderTimer = Math.floor(Math.random() * 80) + 40; // short walk (1-3 seconds)
1678
+ // Pick ONE safe destination inside room (avoiding desks)
1679
+ const safeMargin = 30;
1680
+ const wanderTargets = [
1681
+ { x: room.x + room.w - 50, y: room.y + 40 }, // water cooler area
1682
+ { x: room.x + room.w - 50, y: room.y + room.h - 40 }, // plant area
1683
+ { x: room.x + safeMargin, y: room.y + room.h - 40 }, // bottom-left corner
1684
+ { x: room.x + room.w / 2, y: room.y + room.h - 30 }, // door area
1685
+ ];
1686
+ // Filter out targets that collide with desks
1687
+ const safeTgts = wanderTargets.filter(t => !isInsideAnyObstacle(t.x, t.y, obstacles, 10));
1688
+ s.target = safeTgts.length > 0
1689
+ ? safeTgts[Math.floor(Math.random() * safeTgts.length)]
1690
+ : wanderTargets[wanderTargets.length - 1]; // door area as fallback
1691
+ // Clamp target to room
1692
+ const ct = clampToRoom(s.target.x, s.target.y, room);
1693
+ s.target = ct;
1694
+ }
1695
+ } else {
1696
+ // Walking around
1697
+ s.wanderTimer--;
1698
+ if (s.wanderTimer <= 0 || !s.target) {
1699
+ // Time's up โ€” head back to seat
1700
+ s.target = { x: s.seatX, y: s.seatY };
1701
+ if (Math.abs(s.x - s.seatX) < 3 && Math.abs(s.y - s.seatY) < 3) {
1702
+ s.x = s.seatX;
1703
+ s.y = s.seatY;
1704
+ s.sitting = true;
1705
+ s.target = null;
1706
+ s.direction = 0;
1707
+ // Sit for a long time: 40-120 seconds (at ~30fps with frame/2 tick)
1708
+ s.idleTimer = Math.floor(Math.random() * 2400) + 1200;
1709
+ }
1710
+ }
1711
+
1712
+ if (s.target) {
1713
+ const dx = s.target.x - s.x;
1714
+ const dy = s.target.y - s.y;
1715
+ const dist = Math.sqrt(dx * dx + dy * dy);
1716
+ if (dist > 2) {
1717
+ const speed = 0.6;
1718
+ let nx = s.x + (dx / dist) * speed;
1719
+ let ny = s.y + (dy / dist) * speed;
1720
+ // Clamp movement to room bounds
1721
+ const clamped = clampToRoom(nx, ny, room);
1722
+ nx = clamped.x;
1723
+ ny = clamped.y;
1724
+
1725
+ // Desk collision avoidance: if next position is inside a desk, steer around it
1726
+ if (isInsideAnyObstacle(nx, ny, obstacles, 6)) {
1727
+ // Try sliding along X or Y axis only
1728
+ const cx = clampToRoom(s.x + (dx > 0 ? speed : -speed), s.y, room);
1729
+ const cy = clampToRoom(s.x, s.y + (dy > 0 ? speed : -speed), room);
1730
+ if (!isInsideAnyObstacle(cx.x, cx.y, obstacles, 6)) {
1731
+ nx = cx.x; ny = cx.y;
1732
+ } else if (!isInsideAnyObstacle(cy.x, cy.y, obstacles, 6)) {
1733
+ nx = cy.x; ny = cy.y;
1734
+ } else {
1735
+ // Stuck โ€” just stay put
1736
+ nx = s.x; ny = s.y;
1737
+ }
1738
+ }
1739
+
1740
+ s.x = nx;
1741
+ s.y = ny;
1742
+ s.direction = dx > 0 ? 1 : -1;
1743
+ } else {
1744
+ // Reached destination โ€” go back to seat (no second wander)
1745
+ s.target = { x: s.seatX, y: s.seatY };
1746
+ }
1747
+ }
1748
+ }
1749
+ });
1750
+ });
1751
+
1752
+ // Build output
1753
+ const result = {};
1754
+ for (const key in state) {
1755
+ result[key] = {
1756
+ x: state[key].x,
1757
+ y: state[key].y,
1758
+ direction: state[key].direction,
1759
+ sitting: state[key].sitting,
1760
+ };
1761
+ }
1762
+ return result;
1763
+ }, [rooms, frame]);
1764
+
1765
+ return positions;
1766
+ }
1767
+
1768
+ // ===== Main Component =====
1769
+ export default function PixelOffice({ embedded, groupChat, members: filterMembers }) {
1770
+ const { company, fetchMessages, setChatOpen, setChatMinimized } = useStore();
1771
+ const { t } = useI18n();
1772
+ const canvasRef = useRef(null);
1773
+ const containerRef = useRef(null);
1774
+ const frameRef = useRef(0);
1775
+ const [canvasSize, setCanvasSize] = useState({ w: 900, h: 600 });
1776
+ const [hoveredAgent, setHoveredAgent] = useState(null);
1777
+ const [selectedAgent, setSelectedAgent] = useState(null);
1778
+ const [chatBubbles, setChatBubbles] = useState({});
1779
+ const [zoom, setZoom] = useState(1);
1780
+ const animRef = useRef(null);
1781
+ const [frame, setFrame] = useState(0);
1782
+
1783
+ const allDepartments = company?.departments || [];
1784
+ const secretary = company?.secretary;
1785
+ const boss = company?.boss ? { name: company.boss } : null;
1786
+
1787
+ // In embedded mode with filterMembers, only show departments that have matching members
1788
+ const departments = useMemo(() => {
1789
+ if (!embedded || !filterMembers?.length) return allDepartments;
1790
+ const memberIds = new Set(filterMembers.map(m => m.id));
1791
+ return allDepartments
1792
+ .map(dept => ({
1793
+ ...dept,
1794
+ members: (dept.members || []).filter(m => memberIds.has(m.id)),
1795
+ }))
1796
+ .filter(dept => dept.members.length > 0);
1797
+ }, [embedded, filterMembers, allDepartments]);
1798
+
1799
+ const { rooms, totalH } = useMemo(
1800
+ () => calculateRoomLayout(departments, canvasSize.w / zoom, embedded ? null : secretary, embedded ? null : boss),
1801
+ [departments, canvasSize.w, zoom, secretary, embedded, boss]
1802
+ );
1803
+
1804
+ const positions = useAgentPositions(rooms, frame);
1805
+
1806
+ // Chat bubbles: from groupChat (embedded) or fetchMessages (standalone)
1807
+ // Bubbles auto-disappear after 30 seconds
1808
+ const prevGroupChatLenRef = useRef(0);
1809
+ useEffect(() => {
1810
+ if (embedded && groupChat?.length) {
1811
+ // Build bubbles from groupChat messages, with timestamps for auto-disappear
1812
+ const now = Date.now();
1813
+ const bubbles = {};
1814
+ // Process all messages, keep latest per sender
1815
+ for (const msg of groupChat) {
1816
+ const senderId = msg.from?.id || msg.from;
1817
+ const content = msg.content || msg.text;
1818
+ if (!senderId || !content || senderId === 'boss' || msg.type === 'system') continue;
1819
+ const msgTime = msg.timestamp || msg.time || new Date().toISOString();
1820
+ if (!bubbles[senderId] || new Date(msgTime) > new Date(bubbles[senderId].time)) {
1821
+ bubbles[senderId] = {
1822
+ text: content.replace(/\n/g, ' ').slice(0, 40),
1823
+ time: msgTime,
1824
+ showUntil: new Date(msgTime).getTime() + 30000, // 30s display
1825
+ };
1826
+ }
1827
+ }
1828
+ setChatBubbles(bubbles);
1829
+ }
1830
+ }, [embedded, groupChat?.length]);
1831
+
1832
+ // Auto-refresh bubbles from fetchMessages in standalone mode
1833
+ useEffect(() => {
1834
+ if (embedded) return;
1835
+ const loadBubbles = async () => {
1836
+ try {
1837
+ const msgs = await fetchMessages(30);
1838
+ if (!msgs) return;
1839
+ const bubbles = {};
1840
+ for (const msg of msgs) {
1841
+ const senderId = msg.from;
1842
+ if (senderId && msg.content) {
1843
+ const msgTime = msg.timestamp || msg.time;
1844
+ if (!bubbles[senderId] || new Date(msgTime) > new Date(bubbles[senderId].time)) {
1845
+ bubbles[senderId] = {
1846
+ text: msg.content.replace(/\n/g, ' ').slice(0, 40),
1847
+ time: msgTime,
1848
+ };
1849
+ }
1850
+ }
1851
+ }
1852
+ setChatBubbles(bubbles);
1853
+ } catch (e) { /* ignore */ }
1854
+ };
1855
+
1856
+ loadBubbles();
1857
+ const iv = setInterval(loadBubbles, 15000);
1858
+ return () => clearInterval(iv);
1859
+ }, [fetchMessages, embedded]);
1860
+
1861
+ // Resize observer
1862
+ useEffect(() => {
1863
+ const container = containerRef.current;
1864
+ if (!container) return;
1865
+ const ro = new ResizeObserver((entries) => {
1866
+ for (const entry of entries) {
1867
+ const w = entry.contentRect.width;
1868
+ setCanvasSize(prev => ({ ...prev, w: Math.max(600, w) }));
1869
+ }
1870
+ });
1871
+ ro.observe(container);
1872
+ return () => ro.disconnect();
1873
+ }, []);
1874
+
1875
+ useEffect(() => {
1876
+ setCanvasSize(prev => ({ ...prev, h: Math.max(400, totalH) }));
1877
+ }, [totalH]);
1878
+
1879
+ // Animation loop
1880
+ useEffect(() => {
1881
+ let running = true;
1882
+ const tick = () => {
1883
+ if (!running) return;
1884
+ frameRef.current += 1;
1885
+ if (frameRef.current % 2 === 0) {
1886
+ setFrame(frameRef.current);
1887
+ }
1888
+ animRef.current = requestAnimationFrame(tick);
1889
+ };
1890
+ animRef.current = requestAnimationFrame(tick);
1891
+ return () => {
1892
+ running = false;
1893
+ cancelAnimationFrame(animRef.current);
1894
+ };
1895
+ }, []);
1896
+
1897
+ // ===== Main canvas drawing =====
1898
+ useEffect(() => {
1899
+ const canvas = canvasRef.current;
1900
+ if (!canvas) return;
1901
+
1902
+ const ctx = canvas.getContext('2d');
1903
+ const w = canvasSize.w;
1904
+ const h = canvasSize.h;
1905
+ const dpr = window.devicePixelRatio || 1;
1906
+
1907
+ canvas.width = w * dpr;
1908
+ canvas.height = h * dpr;
1909
+ canvas.style.width = w + 'px';
1910
+ canvas.style.height = h + 'px';
1911
+ ctx.setTransform(dpr * zoom, 0, 0, dpr * zoom, 0, 0);
1912
+
1913
+ ctx.imageSmoothingEnabled = false;
1914
+ ctx.fillStyle = '#1a1a2e';
1915
+ ctx.fillRect(0, 0, w / zoom, h / zoom);
1916
+
1917
+ // Subtle grid pattern
1918
+ ctx.fillStyle = 'rgba(255,255,255,0.02)';
1919
+ const gridSize = 16;
1920
+ for (let gx = 0; gx < w / zoom; gx += gridSize) {
1921
+ for (let gy = 0; gy < h / zoom; gy += gridSize) {
1922
+ if ((Math.floor(gx / gridSize) + Math.floor(gy / gridSize)) % 2 === 0) {
1923
+ ctx.fillRect(gx, gy, gridSize, gridSize);
1924
+ }
1925
+ }
1926
+ }
1927
+
1928
+ const s = 3; // pixel scale (higher quality)
1929
+
1930
+ // Draw rooms
1931
+ rooms.forEach((room) => {
1932
+ const p = room.palette;
1933
+ const seed = hashStr(room.id);
1934
+
1935
+ // Boss office has its own luxury rendering
1936
+ if (room.isBoss) {
1937
+ drawBossRoom(ctx, room, s);
1938
+ return;
1939
+ }
1940
+
1941
+ // Floor
1942
+ ctx.fillStyle = p.floor;
1943
+ ctx.fillRect(room.x, room.y, room.w, room.h);
1944
+
1945
+ // Floor tile pattern
1946
+ ctx.fillStyle = 'rgba(0,0,0,0.05)';
1947
+ const tileSize = 16;
1948
+ for (let tx = room.x; tx < room.x + room.w; tx += tileSize) {
1949
+ for (let ty = room.y; ty < room.y + room.h; ty += tileSize) {
1950
+ if ((Math.floor((tx - room.x) / tileSize) + Math.floor((ty - room.y) / tileSize)) % 2 === 0) {
1951
+ ctx.fillRect(tx, ty, tileSize, tileSize);
1952
+ }
1953
+ }
1954
+ }
1955
+
1956
+ // Rug in center area (scale with room width)
1957
+ const rugColors = ['rgba(139,69,19,0.2)', 'rgba(70,130,70,0.2)', 'rgba(100,100,160,0.2)', 'rgba(160,100,100,0.2)'];
1958
+ const rugW = Math.min(90, room.w - 40);
1959
+ const rugH = Math.min(50, room.h - 80);
1960
+ if (rugW > 40 && rugH > 20) {
1961
+ drawRug(ctx, room.x + room.w / 2 - rugW / 2, room.y + room.h - rugH - 15, rugW, rugH, rugColors[seed % rugColors.length]);
1962
+ }
1963
+
1964
+ // Walls (thicker for scale 3)
1965
+ ctx.fillStyle = p.wall;
1966
+ ctx.fillRect(room.x, room.y - 22, room.w, 22);
1967
+ ctx.fillStyle = p.wallTop;
1968
+ ctx.fillRect(room.x, room.y - 28, room.w, 6);
1969
+ ctx.fillStyle = p.wall;
1970
+ ctx.fillRect(room.x - 6, room.y - 28, 6, room.h + 34);
1971
+ ctx.fillStyle = p.wall;
1972
+ ctx.fillRect(room.x + room.w, room.y - 28, 6, room.h + 34);
1973
+ ctx.fillStyle = p.wall;
1974
+ ctx.fillRect(room.x - 6, room.y + room.h, room.w + 12, 6);
1975
+
1976
+ // Door (bottom center, bigger)
1977
+ ctx.fillStyle = '#5A3A1A';
1978
+ ctx.fillRect(room.x + room.w / 2 - 12, room.y + room.h - 3, 24, 9);
1979
+ ctx.fillStyle = '#8B6914';
1980
+ ctx.fillRect(room.x + room.w / 2 - 10, room.y + room.h, 20, 6);
1981
+ ctx.fillStyle = '#FFD700';
1982
+ ctx.fillRect(room.x + room.w / 2 + 5, room.y + room.h + 2, 3, 3);
1983
+
1984
+ // === Wall decorations (adaptive) ===
1985
+ if (room.w > 250) {
1986
+ drawWindow(ctx, room.x + room.w - 70, room.y - 20, s * 0.8);
1987
+ }
1988
+ if (room.w > 180) {
1989
+ drawClock(ctx, room.x + 14, room.y - 20, s * 0.9);
1990
+ } else {
1991
+ drawClock(ctx, room.x + 10, room.y - 18, s * 0.6);
1992
+ }
1993
+ if (seed % 3 === 0 && room.w > 300) {
1994
+ drawPictureFrame(ctx, room.x + room.w / 2 - 12, room.y - 20, s * 0.7);
1995
+ }
1996
+ const lampCount = Math.max(1, Math.floor(room.w / 200));
1997
+ const lampSpacing = room.w / (lampCount + 1);
1998
+ for (let li = 0; li < lampCount; li++) {
1999
+ drawHangingLamp(ctx, room.x + lampSpacing * (li + 1), room.y + 4, s * (room.w < 280 ? 0.6 : 0.8));
2000
+ }
2001
+
2002
+ // Room name sign (adaptive width)
2003
+ ctx.fillStyle = 'rgba(0,0,0,0.7)';
2004
+ const labelMaxW = Math.min(room.w - 20, 220);
2005
+ roundRect(ctx, room.x + 8, room.y - 25, labelMaxW, 18, 4);
2006
+ ctx.fill();
2007
+ ctx.fillStyle = '#FFE4B5';
2008
+ ctx.font = `bold ${room.w < 280 ? 10 : 12}px "Courier New", monospace`;
2009
+ ctx.textAlign = 'left';
2010
+ ctx.textBaseline = 'middle';
2011
+ const maxChars = Math.floor(labelMaxW / 8);
2012
+ const roomLabel = room.name.length > maxChars ? room.name.slice(0, maxChars - 1) + 'โ€ฆ' : room.name;
2013
+ ctx.fillText(roomLabel, room.x + 14, room.y - 16);
2014
+
2015
+ // === Desk pairs ===
2016
+ const { seats } = getSeatPositions(room);
2017
+ const drawnPairs = new Set();
2018
+ seats.forEach(seat => {
2019
+ const pairKey = `${seat.deskX},${seat.deskY}`;
2020
+ if (!drawnPairs.has(pairKey)) {
2021
+ drawnPairs.add(pairKey);
2022
+ if (room.id === '__secretary__') {
2023
+ drawDesk(ctx, seat.deskX, seat.deskY, s);
2024
+ } else if (room.id !== '__boss__') {
2025
+ drawDeskPair(ctx, seat.deskX, seat.deskY, s);
2026
+ }
2027
+ }
2028
+ });
2029
+
2030
+ // === Room decorations (adaptive to room size) ===
2031
+ if (room.id === '__boss__') return;
2032
+ const isSmallRoom = room.w < 300;
2033
+ const rightEdge = room.x + room.w - (isSmallRoom ? 20 : 40);
2034
+ const decoScale = isSmallRoom ? 0.6 : 1;
2035
+
2036
+ // Whiteboard (only in medium+ rooms)
2037
+ if (!isSmallRoom && room.id !== '__secretary__' && seed % 2 === 0) {
2038
+ drawWhiteboard(ctx, room.x + 8, room.y + 24, s * 0.7);
2039
+ }
2040
+ // Bookshelf (only in medium+ rooms)
2041
+ if (!isSmallRoom && seed % 4 < 2) {
2042
+ drawBookshelf(ctx, rightEdge - 6, room.y + 50, s * 0.9 * decoScale);
2043
+ }
2044
+ // Plant (always, but scale down in small rooms)
2045
+ drawPlant(ctx, room.x + room.w - (isSmallRoom ? 16 : 26), room.y + room.h - 30, s * decoScale);
2046
+ if (seed % 3 === 0 && !isSmallRoom) {
2047
+ drawSmallPlant(ctx, room.x + 10, room.y + room.h - 22, s);
2048
+ }
2049
+ // Water cooler / coffee machine (skip in small rooms)
2050
+ if (!isSmallRoom) {
2051
+ if (seed % 5 < 2) {
2052
+ drawWaterCooler(ctx, rightEdge, room.y + 30, s);
2053
+ } else if (seed % 5 < 4) {
2054
+ drawCoffeeMachine(ctx, rightEdge + 3, room.y + 36, s);
2055
+ }
2056
+ }
2057
+ // Printer (only in medium+ rooms)
2058
+ if (!isSmallRoom && seed % 3 !== 1 && room.id !== '__secretary__') {
2059
+ drawPrinter(ctx, room.x + room.w / 2 + 30, room.y + room.h - 22, s * 0.9);
2060
+ }
2061
+ // Trash bin (always)
2062
+ drawTrashBin(ctx, room.x + (isSmallRoom ? 4 : 10), room.y + room.h - 24, s * 0.7);
2063
+ // File cabinet (skip in small rooms)
2064
+ if (!isSmallRoom && room.id !== '__secretary__' && seed % 3 === 1) {
2065
+ drawFileCabinet(ctx, room.x + 8, room.y + 50, s * 0.8);
2066
+ }
2067
+ if (room.id === '__secretary__' && room.w > 200) {
2068
+ drawCouch(ctx, room.x + room.w - 80, room.y + room.h - 40, s * 0.7);
2069
+ }
2070
+ });
2071
+
2072
+ // Draw hallway paths between rooms
2073
+ ctx.fillStyle = 'rgba(139,115,85,0.3)';
2074
+ for (let i = 1; i < rooms.length; i++) {
2075
+ const from = rooms[i - 1];
2076
+ const to = rooms[i];
2077
+ const fromDoorX = from.x + from.w / 2;
2078
+ const fromDoorY = from.y + from.h + 4;
2079
+ const toDoorX = to.x + to.w / 2;
2080
+ const toDoorY = to.y - 20;
2081
+
2082
+ const midY = (fromDoorY + toDoorY) / 2;
2083
+ ctx.fillRect(fromDoorX - 8, fromDoorY, 16, midY - fromDoorY);
2084
+ ctx.fillRect(Math.min(fromDoorX, toDoorX) - 8, midY - 4, Math.abs(toDoorX - fromDoorX) + 16, 8);
2085
+ ctx.fillRect(toDoorX - 8, midY, 16, toDoorY - midY);
2086
+ }
2087
+
2088
+ // Draw characters (sorted by Y for proper overlap)
2089
+ const allAgents = [];
2090
+ rooms.forEach(room => {
2091
+ room.members.forEach(member => {
2092
+ const pos = positions[member.id];
2093
+ if (!pos) return;
2094
+ allAgents.push({ ...member, x: pos.x, y: pos.y, direction: pos.direction, sitting: pos.sitting, roomId: room.id });
2095
+ });
2096
+ });
2097
+
2098
+ allAgents.sort((a, b) => a.y - b.y);
2099
+
2100
+ allAgents.forEach(agent => {
2101
+ // Shadow
2102
+ ctx.fillStyle = 'rgba(0,0,0,0.2)';
2103
+ ctx.beginPath();
2104
+ ctx.ellipse(agent.x, agent.y + (agent.sitting ? 3 : 6) * s, 5 * s, 2.5 * s, 0, 0, Math.PI * 2);
2105
+ ctx.fill();
2106
+
2107
+ // Character
2108
+ drawPixelChar(ctx, agent.x, agent.y, agent.name, s, frameRef.current, agent.direction, agent.sitting);
2109
+
2110
+ // Name tag (bigger font)
2111
+ ctx.fillStyle = 'rgba(0,0,0,0.65)';
2112
+ ctx.font = '10px "Courier New", monospace';
2113
+ ctx.textAlign = 'center';
2114
+ ctx.textBaseline = 'top';
2115
+ const nameTag = agent.name.length > 12 ? agent.name.slice(0, 11) + 'โ€ฆ' : agent.name;
2116
+ const nameW = ctx.measureText(nameTag).width + 8;
2117
+ const nameY = agent.sitting ? agent.y + 4 * s : agent.y + 7 * s;
2118
+ roundRect(ctx, agent.x - nameW / 2, nameY, nameW, 14, 3);
2119
+ ctx.fill();
2120
+ ctx.fillStyle = '#fff';
2121
+ ctx.fillText(nameTag, agent.x, nameY + 2);
2122
+
2123
+ // Chat bubble
2124
+ const bubble = chatBubbles[agent.id];
2125
+ if (bubble) {
2126
+ const now = Date.now();
2127
+ const showBubble = bubble.showUntil
2128
+ ? now < bubble.showUntil // embedded: auto-disappear after showUntil
2129
+ : (now - new Date(bubble.time).getTime()) / 1000 < 300; // standalone: 5min
2130
+ if (showBubble) {
2131
+ drawBubble(ctx, agent.x, agent.y - 8 * s, bubble.text, s);
2132
+ }
2133
+ }
2134
+
2135
+ // Hover highlight
2136
+ if (hoveredAgent === agent.id) {
2137
+ ctx.strokeStyle = '#FFD700';
2138
+ ctx.lineWidth = 2;
2139
+ ctx.strokeRect(agent.x - 6 * s, agent.y - 8 * s, 12 * s, 16 * s);
2140
+ }
2141
+
2142
+ // Selected highlight
2143
+ if (selectedAgent === agent.id) {
2144
+ ctx.strokeStyle = '#00FF00';
2145
+ ctx.lineWidth = 2;
2146
+ ctx.setLineDash([4, 4]);
2147
+ ctx.strokeRect(agent.x - 7 * s, agent.y - 9 * s, 14 * s, 18 * s);
2148
+ ctx.setLineDash([]);
2149
+ }
2150
+ });
2151
+
2152
+ // Title
2153
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
2154
+ roundRect(ctx, (w / zoom) / 2 - 140, 4, 280, 26, 6);
2155
+ ctx.fill();
2156
+ ctx.fillStyle = '#FFE4B5';
2157
+ ctx.font = 'bold 13px "Courier New", monospace';
2158
+ ctx.textAlign = 'center';
2159
+ ctx.textBaseline = 'middle';
2160
+ ctx.fillText(`๐Ÿข ${company?.name || 'AI Enterprise'} โ€” ${t('pixelOffice.title')}`, (w / zoom) / 2, 17);
2161
+
2162
+ }, [rooms, positions, frame, canvasSize, chatBubbles, hoveredAgent, selectedAgent, zoom, company?.name, embedded, t]);
2163
+
2164
+ // Mouse interaction
2165
+ const handleCanvasClick = useCallback((e) => {
2166
+ const canvas = canvasRef.current;
2167
+ if (!canvas) return;
2168
+ const rect = canvas.getBoundingClientRect();
2169
+ const mx = (e.clientX - rect.left) / zoom;
2170
+ const my = (e.clientY - rect.top) / zoom;
2171
+ const s = 3;
2172
+
2173
+ let clicked = null;
2174
+ rooms.forEach(room => {
2175
+ room.members.forEach(member => {
2176
+ if (member.id === '__boss__') return; // boss is not clickable
2177
+ const pos = positions[member.id];
2178
+ if (!pos) return;
2179
+ if (mx >= pos.x - 8 * s && mx <= pos.x + 8 * s && my >= pos.y - 10 * s && my <= pos.y + 10 * s) {
2180
+ clicked = member;
2181
+ }
2182
+ });
2183
+ });
2184
+
2185
+ if (clicked?.id === '__secretary__') {
2186
+ // Open the bottom-right chat panel with secretary
2187
+ setChatOpen(true);
2188
+ setChatMinimized(false);
2189
+ return;
2190
+ }
2191
+
2192
+ setSelectedAgent(clicked?.id || null);
2193
+ }, [rooms, positions, zoom, setChatOpen, setChatMinimized]);
2194
+
2195
+ const handleCanvasMove = useCallback((e) => {
2196
+ const canvas = canvasRef.current;
2197
+ if (!canvas) return;
2198
+ const rect = canvas.getBoundingClientRect();
2199
+ const mx = (e.clientX - rect.left) / zoom;
2200
+ const my = (e.clientY - rect.top) / zoom;
2201
+ const s = 3;
2202
+
2203
+ let found = null;
2204
+ rooms.forEach(room => {
2205
+ room.members.forEach(member => {
2206
+ if (member.id === '__boss__') return; // boss is not interactive
2207
+ const pos = positions[member.id];
2208
+ if (!pos) return;
2209
+ if (mx >= pos.x - 8 * s && mx <= pos.x + 8 * s && my >= pos.y - 10 * s && my <= pos.y + 10 * s) {
2210
+ found = member.id;
2211
+ }
2212
+ });
2213
+ });
2214
+
2215
+ setHoveredAgent(found);
2216
+ if (canvas) canvas.style.cursor = found ? 'pointer' : 'default';
2217
+ }, [rooms, positions, zoom]);
2218
+
2219
+ return (
2220
+ <div className={`h-full flex flex-col bg-[#0d0d14] ${embedded ? '' : 'py-2'}`}>
2221
+ {/* Toolbar */}
2222
+ {!embedded && (
2223
+ <div className="flex items-center justify-between px-4 py-2 border-b border-[var(--border)] bg-[#0d0d0d]">
2224
+ <div className="flex items-center gap-3">
2225
+ <h1 className="text-sm font-bold text-[#FFE4B5]" style={{ fontFamily: '"Courier New", monospace' }}>
2226
+ ๐Ÿข {t('pixelOffice.title')}
2227
+ </h1>
2228
+ <span className="text-xs text-[var(--muted)]" style={{ fontFamily: '"Courier New", monospace' }}>
2229
+ {departments.length} {t('pixelOffice.depts')} ยท {departments.reduce((s, d) => s + (d.members?.length || 0), 0)} {t('pixelOffice.agents')}
2230
+ </span>
2231
+ </div>
2232
+ <div className="flex items-center gap-2">
2233
+ <button
2234
+ onClick={() => setZoom(z => Math.max(0.5, z - 0.25))}
2235
+ className="px-2 py-1 text-xs bg-[var(--card)] border border-[var(--border)] rounded hover:bg-[var(--card-hover)] transition-colors"
2236
+ style={{ fontFamily: '"Courier New", monospace' }}
2237
+ >
2238
+ โž–
2239
+ </button>
2240
+ <span className="text-xs text-[var(--muted)] min-w-[40px] text-center" style={{ fontFamily: '"Courier New", monospace' }}>
2241
+ {Math.round(zoom * 100)}%
2242
+ </span>
2243
+ <button
2244
+ onClick={() => setZoom(z => Math.min(2, z + 0.25))}
2245
+ className="px-2 py-1 text-xs bg-[var(--card)] border border-[var(--border)] rounded hover:bg-[var(--card-hover)] transition-colors"
2246
+ style={{ fontFamily: '"Courier New", monospace' }}
2247
+ >
2248
+ โž•
2249
+ </button>
2250
+ </div>
2251
+ </div>
2252
+ )}
2253
+
2254
+ {/* Canvas area (full width now, no sidebar) */}
2255
+ <div ref={containerRef} className="flex-1 overflow-auto relative">
2256
+ <canvas
2257
+ ref={canvasRef}
2258
+ onClick={handleCanvasClick}
2259
+ onMouseMove={handleCanvasMove}
2260
+ className="block"
2261
+ style={{ imageRendering: 'pixelated' }}
2262
+ />
2263
+ </div>
2264
+
2265
+ {/* Legend */}
2266
+ {!embedded && (
2267
+ <div className="px-4 py-2 border-t border-[var(--border)] bg-[#0d0d0d] flex items-center gap-4 text-xs text-[var(--muted)]" style={{ fontFamily: '"Courier New", monospace' }}>
2268
+ <span>๐Ÿ–ฑ๏ธ {t('pixelOffice.clickAgent')}</span>
2269
+ <span>๐Ÿ’ฌ {t('pixelOffice.bubbleHint')}</span>
2270
+ <span>๐Ÿšถ {t('pixelOffice.walkHint')}</span>
2271
+ </div>
2272
+ )}
2273
+
2274
+ {/* Agent Detail Modal (reuses existing component) */}
2275
+ {selectedAgent && selectedAgent !== '__secretary__' && selectedAgent !== '__boss__' && (
2276
+ <AgentDetailModal agentId={selectedAgent} onClose={() => setSelectedAgent(null)} />
2277
+ )}
2278
+ </div>
2279
+ );
2280
+ }
2281
+