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.
- package/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- 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
|
+
|