ai-control-center 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- package/templates/skill-template.md +142 -0
|
@@ -0,0 +1,1524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Office Layout — pixel art RPG style team zones with furniture.
|
|
3
|
+
* Expanded with warehouse, conveyor belt, break room, gym, and status board.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getPathfinder } from './pathfinding.js';
|
|
7
|
+
import { renderPixelText, PIXEL_SIZE } from './sprite-renderer.js';
|
|
8
|
+
|
|
9
|
+
const TILE = 32;
|
|
10
|
+
const TEAM_WIDTH = 22; // tiles (wider for new rooms)
|
|
11
|
+
const TEAM_HEIGHT = 22; // tiles (taller for warehouse area)
|
|
12
|
+
const TEAM_GAP = 3;
|
|
13
|
+
|
|
14
|
+
// Workstation positions within a team zone (in tiles)
|
|
15
|
+
const STATIONS = {
|
|
16
|
+
// ── Work area (top section) ──
|
|
17
|
+
statusBoard:{ x: 1, y: 2, w: 6, h: 3, label: 'Status Board' },
|
|
18
|
+
pmDesk: { x: 8, y: 2, w: 3, h: 2, label: 'PM Desk' },
|
|
19
|
+
whiteboard: { x: 12, y: 2, w: 5, h: 3, label: 'Whiteboard' },
|
|
20
|
+
coffee: { x: 18, y: 2, w: 2, h: 2, label: 'Coffee' },
|
|
21
|
+
codingDesk: { x: 1, y: 7, w: 6, h: 2, label: 'Coding Station' },
|
|
22
|
+
reviewTable:{ x: 8, y: 7, w: 3, h: 2, label: 'Review Table' },
|
|
23
|
+
// ── Social area (middle section) ──
|
|
24
|
+
breakRoom: { x: 12, y: 7, w: 4, h: 3, label: 'Break Room' },
|
|
25
|
+
gym: { x: 17, y: 7, w: 4, h: 3, label: 'Gym' },
|
|
26
|
+
// ── Deploy & warehouse (bottom section) ──
|
|
27
|
+
deployDock: { x: 1, y: 11, w: 4, h: 2, label: 'Deploy Dock' },
|
|
28
|
+
conveyor: { x: 5, y: 11, w: 5, h: 2, label: 'Conveyor Belt' },
|
|
29
|
+
warehouse: { x: 10, y: 10, w: 11, h: 5, label: 'Warehouse' },
|
|
30
|
+
// Gossip spots (open floor)
|
|
31
|
+
lounge1: { x: 1, y: 16, w: 3, h: 2, label: 'Lounge' },
|
|
32
|
+
lounge2: { x: 5, y: 16, w: 3, h: 2, label: 'Lounge' },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// RPG Pixel Palette
|
|
36
|
+
const PAL = {
|
|
37
|
+
floor1: '#3a3a5a', floor2: '#343454',
|
|
38
|
+
wall: '#4a4a7a', wallDk: '#38385a', wallTop: '#5a5a8a',
|
|
39
|
+
desk: '#8b6914', deskDk: '#6a5010', deskLt: '#a07820',
|
|
40
|
+
monitor: '#1a1a2e', screen: '#40a8f0', screenG: '#40d870',
|
|
41
|
+
wbBg: '#e8e8d8', wbFrame: '#8a8070',
|
|
42
|
+
dockBg: '#4a4a4a', dockYel: '#e8a020',
|
|
43
|
+
whGray: '#5a5a62', whGrayLt: '#6a6a72', whGrayDk: '#4a4a52',
|
|
44
|
+
convBelt: '#606068', convRoller: '#888890',
|
|
45
|
+
pkgTan: '#c8a050', pkgTan2: '#a88030',
|
|
46
|
+
prodGrn: '#2a7a3a', prodDk: '#1a5a2a', prodRf: '#3a9a4a',
|
|
47
|
+
road: '#505058', roadLn: '#707078',
|
|
48
|
+
carpet: '#3a2a4a', shadow: '#2a2a3a',
|
|
49
|
+
breakFloor: '#4a3a2a', breakTable: '#6a5030',
|
|
50
|
+
gymFloor: '#2a3a4a', gymMat: '#3050a0',
|
|
51
|
+
statusBg: '#1a2a1a', statusGreen: '#40d060', statusRed: '#d04040',
|
|
52
|
+
statusYellow: '#d8c020', statusBlue: '#4080d0',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export class OfficeLayout {
|
|
56
|
+
constructor() {
|
|
57
|
+
this._teamOrigins = new Map();
|
|
58
|
+
this._projectData = new Map(); // project → {feature, stage, packages}
|
|
59
|
+
this._conveyorOffset = 0; // animated conveyor belt offset
|
|
60
|
+
this._time = 0;
|
|
61
|
+
this._trucks = new Map(); // projectName → truck animation state
|
|
62
|
+
this._exhaustParticles = new Map(); // projectName → [{x,y,life,maxLife,vx,vy}]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setupTeams(projectConfigs) {
|
|
66
|
+
this._teamOrigins.clear();
|
|
67
|
+
const startX = 2;
|
|
68
|
+
const startY = 2;
|
|
69
|
+
|
|
70
|
+
projectConfigs.forEach((config, i) => {
|
|
71
|
+
const ox = (startX + i * (TEAM_WIDTH + TEAM_GAP)) * TILE;
|
|
72
|
+
const oy = startY * TILE;
|
|
73
|
+
this._teamOrigins.set(config.name, { x: ox, y: oy, config });
|
|
74
|
+
if (!this._projectData.has(config.name)) {
|
|
75
|
+
this._projectData.set(config.name, {
|
|
76
|
+
feature: '', stage: 'idle',
|
|
77
|
+
packages: [], // shipped features stored in warehouse
|
|
78
|
+
isDeploying: false,
|
|
79
|
+
conveyorOffset: 0,
|
|
80
|
+
conveyorTime: 0,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Build pathfinding grids
|
|
86
|
+
const pf = getPathfinder();
|
|
87
|
+
for (const [name, origin] of this._teamOrigins) {
|
|
88
|
+
const obstacles = [];
|
|
89
|
+
for (const [key, s] of Object.entries(STATIONS)) {
|
|
90
|
+
if (key !== 'lounge1' && key !== 'lounge2') {
|
|
91
|
+
// Pad obstacles by 1 tile so characters walk around furniture, not through it
|
|
92
|
+
obstacles.push({ x: Math.max(0, s.x - 1), y: Math.max(0, s.y - 1), w: s.w + 2, h: s.h + 2 });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
pf.buildGrid(name, { x: origin.x, y: origin.y }, TEAM_WIDTH, TEAM_HEIGHT, obstacles);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Update project state data for rendering */
|
|
100
|
+
setProjectState(projectName, feature, stage, isDeploying) {
|
|
101
|
+
const data = this._projectData.get(projectName);
|
|
102
|
+
if (!data) return;
|
|
103
|
+
data.feature = feature || '';
|
|
104
|
+
data.stage = stage || 'idle';
|
|
105
|
+
data.isDeploying = isDeploying || false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Start the deployer truck driving animation for a project */
|
|
109
|
+
startTruckAnimation(projectName) {
|
|
110
|
+
// Don't start if already running
|
|
111
|
+
if (this._trucks.has(projectName)) return;
|
|
112
|
+
const origin = this._teamOrigins.get(projectName);
|
|
113
|
+
if (!origin) return;
|
|
114
|
+
|
|
115
|
+
// Truck start position: center of deploy dock (in pixels, relative to team origin)
|
|
116
|
+
const dockX = STATIONS.deployDock.x * TILE + (STATIONS.deployDock.w * TILE) / 2 - 16;
|
|
117
|
+
const dockY = STATIONS.deployDock.y * TILE + 8;
|
|
118
|
+
// Warehouse entrance (right edge of conveyor / left edge of warehouse)
|
|
119
|
+
const warehouseX = STATIONS.warehouse.x * TILE - 4;
|
|
120
|
+
|
|
121
|
+
this._trucks.set(projectName, {
|
|
122
|
+
x: dockX,
|
|
123
|
+
y: dockY,
|
|
124
|
+
startX: dockX,
|
|
125
|
+
targetX: warehouseX,
|
|
126
|
+
phase: 'loading',
|
|
127
|
+
timer: 0,
|
|
128
|
+
featureName: this._projectData.get(projectName)?.feature || 'feature',
|
|
129
|
+
packageY: 0, // for unloading animation
|
|
130
|
+
packageAlpha: 1,
|
|
131
|
+
});
|
|
132
|
+
this._exhaustParticles.set(projectName, []);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Check if a truck animation is active for a project */
|
|
136
|
+
hasTruckAnimation(projectName) {
|
|
137
|
+
return this._trucks.has(projectName);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Update all active truck animations */
|
|
141
|
+
_updateTrucks(dt) {
|
|
142
|
+
for (const [name, truck] of this._trucks) {
|
|
143
|
+
truck.timer += dt;
|
|
144
|
+
const particles = this._exhaustParticles.get(name) || [];
|
|
145
|
+
|
|
146
|
+
switch (truck.phase) {
|
|
147
|
+
case 'loading':
|
|
148
|
+
// Truck vibrates at dock for 2s
|
|
149
|
+
if (truck.timer >= 2) {
|
|
150
|
+
truck.phase = 'driving';
|
|
151
|
+
truck.timer = 0;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case 'driving': {
|
|
156
|
+
// Drive from dock to warehouse entrance over 4s
|
|
157
|
+
const progress = Math.min(truck.timer / 4, 1);
|
|
158
|
+
// Ease-in-out for smooth movement
|
|
159
|
+
const eased = progress < 0.5
|
|
160
|
+
? 2 * progress * progress
|
|
161
|
+
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
|
162
|
+
truck.x = truck.startX + (truck.targetX - truck.startX) * eased;
|
|
163
|
+
|
|
164
|
+
// Spawn exhaust particles behind the truck (from the left/back side)
|
|
165
|
+
if (Math.random() < 0.4) {
|
|
166
|
+
particles.push({
|
|
167
|
+
x: truck.x - 4,
|
|
168
|
+
y: truck.y + 10 + (Math.random() - 0.5) * 6,
|
|
169
|
+
life: 0,
|
|
170
|
+
maxLife: 0.6 + Math.random() * 0.4,
|
|
171
|
+
vx: -8 - Math.random() * 6,
|
|
172
|
+
vy: -4 - Math.random() * 4,
|
|
173
|
+
size: 2 + Math.random() * 3,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (truck.timer >= 4) {
|
|
178
|
+
truck.phase = 'unloading';
|
|
179
|
+
truck.timer = 0;
|
|
180
|
+
truck.x = truck.targetX;
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case 'unloading':
|
|
186
|
+
// Package slides off into warehouse over 2s
|
|
187
|
+
truck.packageY = Math.min(truck.timer / 1.5, 1) * 20;
|
|
188
|
+
truck.packageAlpha = Math.max(1 - truck.timer / 2, 0);
|
|
189
|
+
if (truck.timer >= 2) {
|
|
190
|
+
truck.phase = 'returning';
|
|
191
|
+
truck.timer = 0;
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case 'returning': {
|
|
196
|
+
// Drive back to dock over 3s
|
|
197
|
+
const retProgress = Math.min(truck.timer / 3, 1);
|
|
198
|
+
const retEased = retProgress < 0.5
|
|
199
|
+
? 2 * retProgress * retProgress
|
|
200
|
+
: 1 - Math.pow(-2 * retProgress + 2, 2) / 2;
|
|
201
|
+
truck.x = truck.targetX + (truck.startX - truck.targetX) * retEased;
|
|
202
|
+
|
|
203
|
+
if (truck.timer >= 3) {
|
|
204
|
+
truck.phase = 'done';
|
|
205
|
+
truck.timer = 0;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'done':
|
|
211
|
+
this._trucks.delete(name);
|
|
212
|
+
this._exhaustParticles.delete(name);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Update exhaust particles
|
|
217
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
218
|
+
const p = particles[i];
|
|
219
|
+
p.life += dt;
|
|
220
|
+
p.x += p.vx * dt;
|
|
221
|
+
p.y += p.vy * dt;
|
|
222
|
+
p.vx *= 0.96;
|
|
223
|
+
p.vy *= 0.96;
|
|
224
|
+
if (p.life >= p.maxLife) {
|
|
225
|
+
particles.splice(i, 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this._exhaustParticles.set(name, particles);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Draw animated truck and effects for a project */
|
|
233
|
+
_drawAnimatedTruck(ctx, name, ox, oy) {
|
|
234
|
+
const truck = this._trucks.get(name);
|
|
235
|
+
if (!truck) return;
|
|
236
|
+
|
|
237
|
+
const tx = ox + truck.x;
|
|
238
|
+
const ty = ox !== undefined ? oy + truck.y : truck.y;
|
|
239
|
+
|
|
240
|
+
// Draw exhaust smoke particles (behind truck, so draw first)
|
|
241
|
+
const particles = this._exhaustParticles.get(name) || [];
|
|
242
|
+
for (const p of particles) {
|
|
243
|
+
const alpha = 0.5 * (1 - p.life / p.maxLife);
|
|
244
|
+
ctx.save();
|
|
245
|
+
ctx.globalAlpha = alpha;
|
|
246
|
+
ctx.fillStyle = '#888';
|
|
247
|
+
ctx.fillRect(ox + p.x, oy + p.y, p.size, p.size);
|
|
248
|
+
ctx.restore();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Draw the truck at animated position
|
|
252
|
+
const vibrate = truck.phase === 'loading'
|
|
253
|
+
? Math.sin(this._time * 20) * 1.5
|
|
254
|
+
: 0;
|
|
255
|
+
this._drawPixelTruck(ctx, tx + vibrate, ty, true, this._time);
|
|
256
|
+
|
|
257
|
+
// Draw unloading package
|
|
258
|
+
if (truck.phase === 'unloading') {
|
|
259
|
+
ctx.save();
|
|
260
|
+
ctx.globalAlpha = truck.packageAlpha;
|
|
261
|
+
// Small package box sliding right and down
|
|
262
|
+
const pkgX = tx + 24 + truck.packageY * 0.8;
|
|
263
|
+
const pkgY = ty + 4 + truck.packageY;
|
|
264
|
+
ctx.fillStyle = '#c8a050';
|
|
265
|
+
ctx.fillRect(pkgX, pkgY, 12, 10);
|
|
266
|
+
ctx.fillStyle = '#a88030';
|
|
267
|
+
ctx.fillRect(pkgX, pkgY, 12, 2);
|
|
268
|
+
ctx.fillRect(pkgX + 5, pkgY, 2, 10);
|
|
269
|
+
ctx.fillRect(pkgX, pkgY + 4, 12, 2);
|
|
270
|
+
ctx.restore();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// "VROOM" text while driving
|
|
274
|
+
if (truck.phase === 'driving') {
|
|
275
|
+
ctx.save();
|
|
276
|
+
ctx.globalAlpha = 0.7 + Math.sin(this._time * 5) * 0.3;
|
|
277
|
+
renderPixelText(ctx, 'VROOM!', tx + 10, ty - 6, '#ffcc00', 1);
|
|
278
|
+
ctx.restore();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Add a shipped package to the warehouse */
|
|
283
|
+
addShippedPackage(projectName, featureName) {
|
|
284
|
+
const data = this._projectData.get(projectName);
|
|
285
|
+
if (!data) return;
|
|
286
|
+
data.packages.push({
|
|
287
|
+
name: featureName,
|
|
288
|
+
time: Date.now(),
|
|
289
|
+
});
|
|
290
|
+
if (data.packages.length > 12) data.packages.shift(); // max 12 visible
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getCharacterPositions(projectName) {
|
|
294
|
+
const origin = this._teamOrigins.get(projectName);
|
|
295
|
+
if (!origin) return {};
|
|
296
|
+
const o = origin;
|
|
297
|
+
return {
|
|
298
|
+
pm: { x: o.x + (STATIONS.pmDesk.x + 1.5) * TILE, y: o.y + (STATIONS.pmDesk.y + 2.2) * TILE },
|
|
299
|
+
arch: { x: o.x + (STATIONS.whiteboard.x + 2.5) * TILE, y: o.y + (STATIONS.whiteboard.y + 3.5) * TILE },
|
|
300
|
+
coder: { x: o.x + (STATIONS.codingDesk.x + 3) * TILE, y: o.y + (STATIONS.codingDesk.y + 2.5) * TILE },
|
|
301
|
+
deployer: { x: o.x + (STATIONS.deployDock.x + 2) * TILE, y: o.y + (STATIONS.deployDock.y + 2.5) * TILE },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Get positions for idle activities */
|
|
306
|
+
getIdlePositions(projectName) {
|
|
307
|
+
const origin = this._teamOrigins.get(projectName);
|
|
308
|
+
if (!origin) return {};
|
|
309
|
+
const o = origin;
|
|
310
|
+
return {
|
|
311
|
+
breakRoom: { x: o.x + (STATIONS.breakRoom.x + 2) * TILE, y: o.y + (STATIONS.breakRoom.y + 2.5) * TILE },
|
|
312
|
+
gym: { x: o.x + (STATIONS.gym.x + 2) * TILE, y: o.y + (STATIONS.gym.y + 2.5) * TILE },
|
|
313
|
+
coffee: { x: o.x + (STATIONS.coffee.x + 1) * TILE, y: o.y + (STATIONS.coffee.y + 2.2) * TILE },
|
|
314
|
+
lounge1: { x: o.x + (STATIONS.lounge1.x + 1.5) * TILE, y: o.y + (STATIONS.lounge1.y + 1.5) * TILE },
|
|
315
|
+
lounge2: { x: o.x + (STATIONS.lounge2.x + 1.5) * TILE, y: o.y + (STATIONS.lounge2.y + 1.5) * TILE },
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
getWorkstation(projectName, role) {
|
|
320
|
+
const positions = this.getCharacterPositions(projectName);
|
|
321
|
+
return positions[role] || { x: 0, y: 0 };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
render(ctx, dt) {
|
|
325
|
+
this._time += (dt || 0.016);
|
|
326
|
+
// Update per-project conveyor offset independently
|
|
327
|
+
for (const data of this._projectData.values()) {
|
|
328
|
+
if (data.isDeploying) {
|
|
329
|
+
data.conveyorOffset = (data.conveyorOffset + (dt || 0.016) * 30) % 8;
|
|
330
|
+
data.conveyorTime = (data.conveyorTime || 0) + (dt || 0.016);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Update truck animations
|
|
335
|
+
this._updateTrucks(dt || 0.016);
|
|
336
|
+
|
|
337
|
+
for (const [name, origin] of this._teamOrigins) {
|
|
338
|
+
this._renderTeamZone(ctx, name, origin);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_renderTeamZone(ctx, name, origin) {
|
|
343
|
+
const ox = origin.x;
|
|
344
|
+
const oy = origin.y;
|
|
345
|
+
const w = TEAM_WIDTH * TILE;
|
|
346
|
+
const h = TEAM_HEIGHT * TILE;
|
|
347
|
+
const color = origin.config.color || '#4A90D9';
|
|
348
|
+
const data = this._projectData.get(name) || {};
|
|
349
|
+
|
|
350
|
+
// ── Floor ─────────────────────────────────────────────────
|
|
351
|
+
for (let ty = 0; ty < TEAM_HEIGHT; ty++) {
|
|
352
|
+
for (let tx = 0; tx < TEAM_WIDTH; tx++) {
|
|
353
|
+
const isLight = (tx + ty) % 2 === 0;
|
|
354
|
+
ctx.fillStyle = isLight ? PAL.floor1 : PAL.floor2;
|
|
355
|
+
ctx.fillRect(ox + tx * TILE, oy + ty * TILE, TILE, TILE);
|
|
356
|
+
ctx.strokeStyle = '#2e2e4e';
|
|
357
|
+
ctx.lineWidth = 0.5;
|
|
358
|
+
ctx.strokeRect(ox + tx * TILE, oy + ty * TILE, TILE, TILE);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── Room floors ───────────────────────────────────────────
|
|
363
|
+
// Break room floor
|
|
364
|
+
this._fillArea(ctx, ox, oy, STATIONS.breakRoom, PAL.breakFloor);
|
|
365
|
+
// Gym floor
|
|
366
|
+
this._fillArea(ctx, ox, oy, STATIONS.gym, PAL.gymFloor);
|
|
367
|
+
// Carpet under work area
|
|
368
|
+
this._drawCarpet(ctx, ox + 0.5 * TILE, oy + 5.5 * TILE, 7 * TILE, 3 * TILE);
|
|
369
|
+
|
|
370
|
+
// ── Walls ─────────────────────────────────────────────────
|
|
371
|
+
ctx.fillStyle = PAL.wallTop;
|
|
372
|
+
ctx.fillRect(ox, oy, w, TILE * 0.8);
|
|
373
|
+
ctx.fillStyle = PAL.wall;
|
|
374
|
+
ctx.fillRect(ox, oy + TILE * 0.8, w, TILE * 0.4);
|
|
375
|
+
ctx.fillStyle = PAL.wallDk;
|
|
376
|
+
ctx.fillRect(ox, oy + TILE * 1.2, w, 3);
|
|
377
|
+
ctx.fillStyle = PAL.wall;
|
|
378
|
+
ctx.fillRect(ox, oy, TILE * 0.4, h);
|
|
379
|
+
ctx.fillRect(ox + w - TILE * 0.4, oy, TILE * 0.4, h);
|
|
380
|
+
ctx.fillStyle = PAL.wallDk;
|
|
381
|
+
ctx.fillRect(ox + TILE * 0.4, oy, 2, h);
|
|
382
|
+
ctx.fillRect(ox + w - TILE * 0.4 - 2, oy, 2, h);
|
|
383
|
+
ctx.fillRect(ox, oy + h - 3, w, 3);
|
|
384
|
+
|
|
385
|
+
// ── Team banner ───────────────────────────────────────────
|
|
386
|
+
ctx.fillStyle = color;
|
|
387
|
+
ctx.fillRect(ox + TILE * 0.4, oy, w - TILE * 0.8, 22);
|
|
388
|
+
ctx.fillStyle = '#fff';
|
|
389
|
+
ctx.fillRect(ox + TILE * 0.4, oy + 21, w - TILE * 0.8, 1);
|
|
390
|
+
renderPixelText(ctx, name, ox + w / 2, oy + 6, '#fff', 2);
|
|
391
|
+
|
|
392
|
+
// ── Status Board (big LED-style) ──────────────────────────
|
|
393
|
+
this._drawStatusBoard(ctx, ox, oy, data);
|
|
394
|
+
|
|
395
|
+
// ── PM Desk ───────────────────────────────────────────────
|
|
396
|
+
this._drawPixelDesk(ctx,
|
|
397
|
+
ox + STATIONS.pmDesk.x * TILE, oy + STATIONS.pmDesk.y * TILE,
|
|
398
|
+
STATIONS.pmDesk.w * TILE, STATIONS.pmDesk.h * TILE);
|
|
399
|
+
this._drawPapers(ctx,
|
|
400
|
+
ox + (STATIONS.pmDesk.x + 0.5) * TILE, oy + (STATIONS.pmDesk.y + 0.3) * TILE);
|
|
401
|
+
// Phase 4: PM Desk Lamp
|
|
402
|
+
this._drawDeskLamp(ctx,
|
|
403
|
+
ox + (STATIONS.pmDesk.x + 2.2) * TILE, oy + (STATIONS.pmDesk.y + 0.1) * TILE,
|
|
404
|
+
data.stage, this._time);
|
|
405
|
+
|
|
406
|
+
// ── Whiteboard (shows current feature) ────────────────────
|
|
407
|
+
this._drawWhiteboardWithFeature(ctx,
|
|
408
|
+
ox + STATIONS.whiteboard.x * TILE, oy + STATIONS.whiteboard.y * TILE,
|
|
409
|
+
STATIONS.whiteboard.w * TILE, STATIONS.whiteboard.h * TILE,
|
|
410
|
+
data.feature, data.stage, this._time);
|
|
411
|
+
|
|
412
|
+
// ── Coffee ────────────────────────────────────────────────
|
|
413
|
+
this._drawCoffeeMachine(ctx, ox + STATIONS.coffee.x * TILE, oy + STATIONS.coffee.y * TILE);
|
|
414
|
+
|
|
415
|
+
// ── Coding Station ────────────────────────────────────────
|
|
416
|
+
this._drawCodingStation(ctx,
|
|
417
|
+
ox + STATIONS.codingDesk.x * TILE, oy + STATIONS.codingDesk.y * TILE,
|
|
418
|
+
STATIONS.codingDesk.w * TILE, STATIONS.codingDesk.h * TILE,
|
|
419
|
+
data.stage, this._time);
|
|
420
|
+
|
|
421
|
+
// ── Review Table ──────────────────────────────────────────
|
|
422
|
+
this._drawPixelDesk(ctx,
|
|
423
|
+
ox + STATIONS.reviewTable.x * TILE, oy + STATIONS.reviewTable.y * TILE,
|
|
424
|
+
STATIONS.reviewTable.w * TILE, STATIONS.reviewTable.h * TILE);
|
|
425
|
+
this._drawMagnifier(ctx,
|
|
426
|
+
ox + (STATIONS.reviewTable.x + 1) * TILE, oy + (STATIONS.reviewTable.y + 0.3) * TILE);
|
|
427
|
+
this._drawReviewTableAnim(ctx,
|
|
428
|
+
ox + STATIONS.reviewTable.x * TILE, oy + STATIONS.reviewTable.y * TILE,
|
|
429
|
+
STATIONS.reviewTable.w * TILE, STATIONS.reviewTable.h * TILE,
|
|
430
|
+
data.stage, this._time);
|
|
431
|
+
|
|
432
|
+
// ── Break Room ────────────────────────────────────────────
|
|
433
|
+
this._drawBreakRoom(ctx, ox + STATIONS.breakRoom.x * TILE, oy + STATIONS.breakRoom.y * TILE,
|
|
434
|
+
STATIONS.breakRoom.w * TILE, STATIONS.breakRoom.h * TILE);
|
|
435
|
+
|
|
436
|
+
// ── Gym ───────────────────────────────────────────────────
|
|
437
|
+
this._drawGym(ctx, ox + STATIONS.gym.x * TILE, oy + STATIONS.gym.y * TILE,
|
|
438
|
+
STATIONS.gym.w * TILE, STATIONS.gym.h * TILE);
|
|
439
|
+
|
|
440
|
+
// ── Deploy Dock ───────────────────────────────────────────
|
|
441
|
+
this._drawDeployDock(ctx,
|
|
442
|
+
ox + STATIONS.deployDock.x * TILE, oy + STATIONS.deployDock.y * TILE,
|
|
443
|
+
STATIONS.deployDock.w * TILE, STATIONS.deployDock.h * TILE,
|
|
444
|
+
data.stage, this._time, this._trucks.has(name));
|
|
445
|
+
|
|
446
|
+
// ── Conveyor Belt (animated) ──────────────────────────────
|
|
447
|
+
this._drawConveyorBelt(ctx,
|
|
448
|
+
ox + STATIONS.conveyor.x * TILE, oy + STATIONS.conveyor.y * TILE,
|
|
449
|
+
STATIONS.conveyor.w * TILE, STATIONS.conveyor.h * TILE,
|
|
450
|
+
data.isDeploying, data.conveyorOffset || 0, data.conveyorTime || 0);
|
|
451
|
+
|
|
452
|
+
// ── Warehouse ─────────────────────────────────────────────
|
|
453
|
+
this._drawWarehouse(ctx,
|
|
454
|
+
ox + STATIONS.warehouse.x * TILE, oy + STATIONS.warehouse.y * TILE,
|
|
455
|
+
STATIONS.warehouse.w * TILE, STATIONS.warehouse.h * TILE,
|
|
456
|
+
data.packages);
|
|
457
|
+
|
|
458
|
+
// ── Animated Deploy Truck ─────────────────────────────────
|
|
459
|
+
this._drawAnimatedTruck(ctx, name, ox, oy);
|
|
460
|
+
|
|
461
|
+
// ── Lounge areas (plants, benches) ────────────────────────
|
|
462
|
+
this._drawLounge(ctx, ox + STATIONS.lounge1.x * TILE, oy + STATIONS.lounge1.y * TILE);
|
|
463
|
+
this._drawLounge(ctx, ox + STATIONS.lounge2.x * TILE, oy + STATIONS.lounge2.y * TILE);
|
|
464
|
+
|
|
465
|
+
// ── Chairs ────────────────────────────────────────────────
|
|
466
|
+
this._drawChair(ctx, ox + (STATIONS.pmDesk.x + 1.5) * TILE, oy + (STATIONS.pmDesk.y + 1.8) * TILE);
|
|
467
|
+
this._drawChair(ctx, ox + (STATIONS.codingDesk.x + 1) * TILE, oy + (STATIONS.codingDesk.y + 1.8) * TILE);
|
|
468
|
+
this._drawChair(ctx, ox + (STATIONS.codingDesk.x + 3) * TILE, oy + (STATIONS.codingDesk.y + 1.8) * TILE);
|
|
469
|
+
this._drawChair(ctx, ox + (STATIONS.codingDesk.x + 5) * TILE, oy + (STATIONS.codingDesk.y + 1.8) * TILE);
|
|
470
|
+
this._drawChair(ctx, ox + (STATIONS.reviewTable.x + 1) * TILE, oy + (STATIONS.reviewTable.y + 1.8) * TILE);
|
|
471
|
+
this._drawChair(ctx, ox + (STATIONS.reviewTable.x + 2.5) * TILE, oy + (STATIONS.reviewTable.y + 1.8) * TILE);
|
|
472
|
+
this._drawChair(ctx, ox + (STATIONS.deployDock.x + 2) * TILE, oy + (STATIONS.deployDock.y + 1.8) * TILE);
|
|
473
|
+
this._drawChair(ctx, ox + (STATIONS.whiteboard.x + 2.5) * TILE, oy + (STATIONS.whiteboard.y + 3) * TILE);
|
|
474
|
+
|
|
475
|
+
// Room divider lines
|
|
476
|
+
ctx.fillStyle = PAL.wallDk;
|
|
477
|
+
// Between work and social
|
|
478
|
+
ctx.fillRect(ox + 11.5 * TILE, oy + 5.5 * TILE, 2, 4 * TILE);
|
|
479
|
+
// Between break room and gym
|
|
480
|
+
ctx.fillRect(ox + 16.5 * TILE, oy + 5.5 * TILE, 2, 3.5 * TILE);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ── Helper ──────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
_fillArea(ctx, ox, oy, station, color) {
|
|
486
|
+
ctx.fillStyle = color;
|
|
487
|
+
ctx.fillRect(ox + station.x * TILE, oy + station.y * TILE, station.w * TILE, station.h * TILE);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Status Board ────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
_drawStatusBoard(ctx, ox, oy, data) {
|
|
493
|
+
const s = STATIONS.statusBoard;
|
|
494
|
+
const x = ox + s.x * TILE;
|
|
495
|
+
const y = oy + s.y * TILE;
|
|
496
|
+
const w = s.w * TILE;
|
|
497
|
+
const h = s.h * TILE;
|
|
498
|
+
|
|
499
|
+
// Board background
|
|
500
|
+
ctx.fillStyle = PAL.statusBg;
|
|
501
|
+
ctx.fillRect(x, y, w, h);
|
|
502
|
+
ctx.strokeStyle = '#3a5a3a';
|
|
503
|
+
ctx.lineWidth = 2;
|
|
504
|
+
ctx.strokeRect(x, y, w, h);
|
|
505
|
+
|
|
506
|
+
// Scanline effect
|
|
507
|
+
ctx.fillStyle = '#ffffff08';
|
|
508
|
+
for (let sy = 0; sy < h; sy += 3) {
|
|
509
|
+
ctx.fillRect(x, y + sy, w, 1);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Title
|
|
513
|
+
renderPixelText(ctx, 'STATUS', x + w / 2, y + 4, '#40d060', 1);
|
|
514
|
+
|
|
515
|
+
// Status indicator
|
|
516
|
+
const stage = data.stage || 'idle';
|
|
517
|
+
const isWorking = stage && stage !== 'idle' && stage !== 'inbox' && stage !== '';
|
|
518
|
+
const statusColor = isWorking ? PAL.statusGreen : PAL.statusYellow;
|
|
519
|
+
const statusText = isWorking ? 'WORKING' : 'IDLE';
|
|
520
|
+
|
|
521
|
+
// Blinking dot
|
|
522
|
+
const blink = Math.sin(this._time * (isWorking ? 4 : 1.5)) > 0;
|
|
523
|
+
if (blink) {
|
|
524
|
+
ctx.fillStyle = statusColor;
|
|
525
|
+
ctx.fillRect(x + 6, y + 16, 6, 6);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
renderPixelText(ctx, statusText, x + w / 2, y + 16, statusColor, 1);
|
|
529
|
+
|
|
530
|
+
// Current stage
|
|
531
|
+
const stageShort = stage.replace(/_/g, ' ').toUpperCase().slice(0, 18);
|
|
532
|
+
if (isWorking) {
|
|
533
|
+
renderPixelText(ctx, stageShort, x + w / 2, y + 28, '#80c0f0', 1);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Feature name — use native canvas text for wider display
|
|
537
|
+
if (data.feature) {
|
|
538
|
+
const maxChars = Math.floor(w / 5);
|
|
539
|
+
const featLine1 = data.feature.slice(0, maxChars).toUpperCase();
|
|
540
|
+
ctx.fillStyle = '#e8d040';
|
|
541
|
+
ctx.font = 'bold 8px monospace';
|
|
542
|
+
ctx.textAlign = 'center';
|
|
543
|
+
ctx.fillText(featLine1, x + w / 2, y + 48);
|
|
544
|
+
// Second line if feature name is longer
|
|
545
|
+
if (data.feature.length > maxChars) {
|
|
546
|
+
const featLine2 = data.feature.slice(maxChars, maxChars * 2).toUpperCase();
|
|
547
|
+
ctx.fillStyle = '#c8b030';
|
|
548
|
+
ctx.fillText(featLine2, x + w / 2, y + 58);
|
|
549
|
+
}
|
|
550
|
+
// Short description below
|
|
551
|
+
ctx.fillStyle = '#708070';
|
|
552
|
+
ctx.font = '7px monospace';
|
|
553
|
+
ctx.fillText(stage.replace(/_/g, ' '), x + w / 2, y + 68);
|
|
554
|
+
ctx.textAlign = 'start';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Shipped count
|
|
558
|
+
const shipped = (data.packages || []).length;
|
|
559
|
+
renderPixelText(ctx, `SHIPPED:${shipped}`, x + w / 2, y + h - 12, '#808080', 1);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ── Whiteboard with Feature ─────────────────────────────────
|
|
563
|
+
|
|
564
|
+
_drawWhiteboardWithFeature(ctx, x, y, w, h, feature, stage = '', t = 0) {
|
|
565
|
+
// Frame
|
|
566
|
+
ctx.fillStyle = PAL.wbFrame;
|
|
567
|
+
ctx.fillRect(x + 2, y, w - 4, 3);
|
|
568
|
+
ctx.fillRect(x + 2, y + h - 10, w - 4, 3);
|
|
569
|
+
ctx.fillRect(x + 2, y, 3, h - 8);
|
|
570
|
+
ctx.fillRect(x + w - 5, y, 3, h - 8);
|
|
571
|
+
|
|
572
|
+
// White surface
|
|
573
|
+
ctx.fillStyle = PAL.wbBg;
|
|
574
|
+
ctx.fillRect(x + 5, y + 3, w - 10, h - 14);
|
|
575
|
+
|
|
576
|
+
// Title area
|
|
577
|
+
ctx.fillStyle = '#4060b0';
|
|
578
|
+
ctx.fillRect(x + 8, y + 6, w - 16, 12);
|
|
579
|
+
renderPixelText(ctx, 'ARCHITECTURE', x + w / 2, y + 8, '#fff', 1);
|
|
580
|
+
|
|
581
|
+
// Feature name (if any)
|
|
582
|
+
if (feature) {
|
|
583
|
+
const lines = feature.toUpperCase().match(/.{1,16}/g) || [];
|
|
584
|
+
for (let i = 0; i < Math.min(lines.length, 3); i++) {
|
|
585
|
+
renderPixelText(ctx, lines[i], x + w / 2, y + 22 + i * 10, '#d04040', 1);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Diagrams
|
|
590
|
+
ctx.fillStyle = '#4060b0';
|
|
591
|
+
ctx.fillRect(x + 10, y + 52, 16, 10);
|
|
592
|
+
ctx.fillStyle = '#e8e8d8';
|
|
593
|
+
ctx.fillRect(x + 12, y + 54, 12, 6);
|
|
594
|
+
ctx.fillStyle = '#d04040';
|
|
595
|
+
ctx.fillRect(x + 34, y + 52, 16, 10);
|
|
596
|
+
ctx.fillStyle = '#e8e8d8';
|
|
597
|
+
ctx.fillRect(x + 36, y + 54, 12, 6);
|
|
598
|
+
// Arrow
|
|
599
|
+
ctx.fillStyle = '#4060b0';
|
|
600
|
+
ctx.fillRect(x + 26, y + 56, 8, 2);
|
|
601
|
+
ctx.fillRect(x + 32, y + 54, 2, 6);
|
|
602
|
+
|
|
603
|
+
// Marker tray
|
|
604
|
+
ctx.fillStyle = PAL.wbFrame;
|
|
605
|
+
ctx.fillRect(x + 8, y + h - 8, w - 16, 4);
|
|
606
|
+
ctx.fillStyle = '#d04040';
|
|
607
|
+
ctx.fillRect(x + 12, y + h - 10, 3, 6);
|
|
608
|
+
ctx.fillStyle = '#4060b0';
|
|
609
|
+
ctx.fillRect(x + 18, y + h - 10, 3, 6);
|
|
610
|
+
ctx.fillStyle = '#40a040';
|
|
611
|
+
ctx.fillRect(x + 24, y + h - 10, 3, 6);
|
|
612
|
+
|
|
613
|
+
// Phase 5: Animated diagram drawing when architect is designing
|
|
614
|
+
if (stage === 'architecture' || stage === 'arch_complete') {
|
|
615
|
+
const boardX = x + 8;
|
|
616
|
+
const boardY = y + 38;
|
|
617
|
+
const boardW = w - 16;
|
|
618
|
+
const boardH = h - 52;
|
|
619
|
+
|
|
620
|
+
// Clear diagram area for animated content
|
|
621
|
+
ctx.fillStyle = PAL.wbBg;
|
|
622
|
+
ctx.fillRect(boardX, boardY, boardW, 38);
|
|
623
|
+
|
|
624
|
+
// Cycle through diagram layouts every ~8 seconds
|
|
625
|
+
const layoutPhase = Math.floor(t % 8);
|
|
626
|
+
const cycleT = t % 8; // time within current cycle
|
|
627
|
+
|
|
628
|
+
// Marker colors cycling
|
|
629
|
+
const markerColors = ['#4060b0', '#d04040', '#40a040'];
|
|
630
|
+
const activeMarker = markerColors[Math.floor(t) % 3];
|
|
631
|
+
|
|
632
|
+
// --- Diagram layout definitions ---
|
|
633
|
+
const layouts = [
|
|
634
|
+
// Layout 0-2: Three boxes in a row with arrows
|
|
635
|
+
() => {
|
|
636
|
+
const boxes = [
|
|
637
|
+
{ bx: boardX + 4, by: boardY + 8, bw: 22, bh: 12, label: 'API' },
|
|
638
|
+
{ bx: boardX + 38, by: boardY + 8, bw: 22, bh: 12, label: 'SVC' },
|
|
639
|
+
{ bx: boardX + 72, by: boardY + 8, bw: 22, bh: 12, label: 'DB' },
|
|
640
|
+
];
|
|
641
|
+
const progress = Math.min(cycleT / 2, 1); // grow over 2 seconds
|
|
642
|
+
for (let bi = 0; bi < boxes.length; bi++) {
|
|
643
|
+
const b = boxes[bi];
|
|
644
|
+
const drawW = Math.floor(b.bw * progress);
|
|
645
|
+
const drawH = Math.floor(b.bh * progress);
|
|
646
|
+
if (drawW > 2 && drawH > 2) {
|
|
647
|
+
ctx.strokeStyle = markerColors[bi % 3];
|
|
648
|
+
ctx.lineWidth = 1;
|
|
649
|
+
ctx.strokeRect(b.bx, b.by, drawW, drawH);
|
|
650
|
+
if (progress > 0.6) {
|
|
651
|
+
ctx.fillStyle = markerColors[bi % 3];
|
|
652
|
+
ctx.font = 'bold 5px monospace';
|
|
653
|
+
ctx.textAlign = 'center';
|
|
654
|
+
ctx.fillText(b.label, b.bx + b.bw / 2, b.by + b.bh / 2 + 2);
|
|
655
|
+
ctx.textAlign = 'start';
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Connection arrows (appear after boxes)
|
|
660
|
+
if (cycleT > 1.5) {
|
|
661
|
+
const arrowProg = Math.min((cycleT - 1.5) / 1, 1);
|
|
662
|
+
const arrowLen = Math.floor(12 * arrowProg);
|
|
663
|
+
ctx.fillStyle = activeMarker;
|
|
664
|
+
ctx.fillRect(boardX + 26, boardY + 13, arrowLen, 2);
|
|
665
|
+
if (arrowLen > 4) { ctx.fillRect(boardX + 26 + arrowLen - 2, boardY + 11, 2, 6); }
|
|
666
|
+
ctx.fillRect(boardX + 60, boardY + 13, arrowLen, 2);
|
|
667
|
+
if (arrowLen > 4) { ctx.fillRect(boardX + 60 + arrowLen - 2, boardY + 11, 2, 6); }
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
// Layout 3-5: Vertical flow chart
|
|
671
|
+
() => {
|
|
672
|
+
const boxes = [
|
|
673
|
+
{ bx: boardX + 30, by: boardY + 2, bw: 30, bh: 10, label: 'INPUT' },
|
|
674
|
+
{ bx: boardX + 30, by: boardY + 18, bw: 30, bh: 10, label: 'PROC' },
|
|
675
|
+
{ bx: boardX + 30, by: boardY + 34, bw: 30, bh: 10, label: 'OUT' },
|
|
676
|
+
];
|
|
677
|
+
const progress = Math.min(cycleT / 2, 1);
|
|
678
|
+
for (let bi = 0; bi < boxes.length; bi++) {
|
|
679
|
+
const b = boxes[bi];
|
|
680
|
+
const drawW = Math.floor(b.bw * progress);
|
|
681
|
+
if (drawW > 2) {
|
|
682
|
+
ctx.strokeStyle = markerColors[bi % 3];
|
|
683
|
+
ctx.lineWidth = 1;
|
|
684
|
+
ctx.strokeRect(b.bx, b.by, drawW, b.bh);
|
|
685
|
+
if (progress > 0.5) {
|
|
686
|
+
ctx.fillStyle = markerColors[bi % 3];
|
|
687
|
+
ctx.font = 'bold 5px monospace';
|
|
688
|
+
ctx.textAlign = 'center';
|
|
689
|
+
ctx.fillText(b.label, b.bx + b.bw / 2, b.by + 7);
|
|
690
|
+
ctx.textAlign = 'start';
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Vertical arrows
|
|
695
|
+
if (cycleT > 1.5) {
|
|
696
|
+
ctx.fillStyle = activeMarker;
|
|
697
|
+
ctx.fillRect(boardX + 44, boardY + 12, 2, 6);
|
|
698
|
+
ctx.fillRect(boardX + 42, boardY + 16, 6, 2);
|
|
699
|
+
ctx.fillRect(boardX + 44, boardY + 28, 2, 6);
|
|
700
|
+
ctx.fillRect(boardX + 42, boardY + 32, 6, 2);
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
// Layout 6-7: Diamond decision + branches
|
|
704
|
+
() => {
|
|
705
|
+
const progress = Math.min(cycleT / 2, 1);
|
|
706
|
+
// Diamond (decision)
|
|
707
|
+
const cx = boardX + boardW / 2;
|
|
708
|
+
const cy = boardY + 12;
|
|
709
|
+
const ds = Math.floor(8 * progress);
|
|
710
|
+
if (ds > 2) {
|
|
711
|
+
ctx.strokeStyle = '#d04040';
|
|
712
|
+
ctx.lineWidth = 1;
|
|
713
|
+
ctx.beginPath();
|
|
714
|
+
ctx.moveTo(cx, cy - ds);
|
|
715
|
+
ctx.lineTo(cx + ds, cy);
|
|
716
|
+
ctx.lineTo(cx, cy + ds);
|
|
717
|
+
ctx.lineTo(cx - ds, cy);
|
|
718
|
+
ctx.closePath();
|
|
719
|
+
ctx.stroke();
|
|
720
|
+
if (progress > 0.5) {
|
|
721
|
+
ctx.fillStyle = '#d04040';
|
|
722
|
+
ctx.font = 'bold 5px monospace';
|
|
723
|
+
ctx.textAlign = 'center';
|
|
724
|
+
ctx.fillText('?', cx, cy + 2);
|
|
725
|
+
ctx.textAlign = 'start';
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// Branch boxes
|
|
729
|
+
if (cycleT > 1.5) {
|
|
730
|
+
const brProg = Math.min((cycleT - 1.5) / 1.5, 1);
|
|
731
|
+
const brW = Math.floor(20 * brProg);
|
|
732
|
+
ctx.strokeStyle = '#4060b0';
|
|
733
|
+
ctx.lineWidth = 1;
|
|
734
|
+
if (brW > 4) {
|
|
735
|
+
ctx.strokeRect(boardX + 8, boardY + 28, brW, 8);
|
|
736
|
+
ctx.strokeRect(boardX + boardW - 8 - brW, boardY + 28, brW, 8);
|
|
737
|
+
ctx.fillStyle = '#4060b0';
|
|
738
|
+
ctx.font = 'bold 5px monospace';
|
|
739
|
+
ctx.textAlign = 'center';
|
|
740
|
+
ctx.fillText('Y', boardX + 18, boardY + 34);
|
|
741
|
+
ctx.fillText('N', boardX + boardW - 18, boardY + 34);
|
|
742
|
+
ctx.textAlign = 'start';
|
|
743
|
+
}
|
|
744
|
+
// Branch lines
|
|
745
|
+
ctx.fillStyle = '#40a040';
|
|
746
|
+
ctx.fillRect(cx - 12, cy + 8, 2, Math.floor(12 * brProg));
|
|
747
|
+
ctx.fillRect(cx + 10, cy + 8, 2, Math.floor(12 * brProg));
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
];
|
|
751
|
+
|
|
752
|
+
// Select layout based on phase (cycle through 3 layouts in 8-second windows)
|
|
753
|
+
const layoutIdx = Math.floor((t / 8) % layouts.length);
|
|
754
|
+
layouts[layoutIdx]();
|
|
755
|
+
|
|
756
|
+
// "Drawing hand" cursor moving across the board
|
|
757
|
+
const handX = boardX + 4 + ((t * 18) % (boardW - 8));
|
|
758
|
+
const handY = boardY + 4 + Math.sin(t * 5) * 6 + 12;
|
|
759
|
+
ctx.fillStyle = activeMarker;
|
|
760
|
+
ctx.fillRect(handX, handY, 3, 3);
|
|
761
|
+
ctx.fillRect(handX + 1, handY + 3, 2, 2);
|
|
762
|
+
|
|
763
|
+
// Glowing border around whiteboard
|
|
764
|
+
ctx.save();
|
|
765
|
+
ctx.globalAlpha = 0.25 + Math.sin(t * 2) * 0.15;
|
|
766
|
+
ctx.fillStyle = '#60c060';
|
|
767
|
+
ctx.fillRect(x + 3, y + 1, w - 6, 2);
|
|
768
|
+
ctx.fillRect(x + 3, y + h - 12, w - 6, 2);
|
|
769
|
+
ctx.restore();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ── Break Room ──────────────────────────────────────────────
|
|
774
|
+
|
|
775
|
+
_drawBreakRoom(ctx, x, y, w, h) {
|
|
776
|
+
// Room label
|
|
777
|
+
renderPixelText(ctx, 'BREAK ROOM', x + w / 2, y + 2, '#a08060', 1);
|
|
778
|
+
|
|
779
|
+
// Table
|
|
780
|
+
ctx.fillStyle = PAL.breakTable;
|
|
781
|
+
ctx.fillRect(x + w / 2 - 16, y + 16, 32, 20);
|
|
782
|
+
ctx.fillStyle = '#5a4020';
|
|
783
|
+
ctx.strokeStyle = '#4a3018';
|
|
784
|
+
ctx.lineWidth = 1;
|
|
785
|
+
ctx.strokeRect(x + w / 2 - 16, y + 16, 32, 20);
|
|
786
|
+
|
|
787
|
+
// Plates
|
|
788
|
+
ctx.fillStyle = '#e0e0d8';
|
|
789
|
+
ctx.beginPath();
|
|
790
|
+
ctx.arc(x + w / 2 - 6, y + 26, 4, 0, Math.PI * 2);
|
|
791
|
+
ctx.fill();
|
|
792
|
+
ctx.beginPath();
|
|
793
|
+
ctx.arc(x + w / 2 + 6, y + 26, 4, 0, Math.PI * 2);
|
|
794
|
+
ctx.fill();
|
|
795
|
+
|
|
796
|
+
// Food items
|
|
797
|
+
ctx.fillStyle = '#d04040'; // apple
|
|
798
|
+
ctx.fillRect(x + w / 2 - 8, y + 24, 4, 4);
|
|
799
|
+
ctx.fillStyle = '#e8d040'; // sandwich
|
|
800
|
+
ctx.fillRect(x + w / 2 + 4, y + 24, 6, 3);
|
|
801
|
+
|
|
802
|
+
// Chairs around table
|
|
803
|
+
this._drawChair(ctx, x + w / 2 - 20, y + 24);
|
|
804
|
+
this._drawChair(ctx, x + w / 2 + 20, y + 24);
|
|
805
|
+
this._drawChair(ctx, x + w / 2, y + 42);
|
|
806
|
+
|
|
807
|
+
// Vending machine
|
|
808
|
+
ctx.fillStyle = '#505060';
|
|
809
|
+
ctx.fillRect(x + 4, y + 10, 16, 24);
|
|
810
|
+
ctx.fillStyle = '#404050';
|
|
811
|
+
ctx.fillRect(x + 6, y + 12, 12, 18);
|
|
812
|
+
// Drinks
|
|
813
|
+
ctx.fillStyle = '#d04040';
|
|
814
|
+
ctx.fillRect(x + 8, y + 14, 3, 5);
|
|
815
|
+
ctx.fillStyle = '#4080d0';
|
|
816
|
+
ctx.fillRect(x + 12, y + 14, 3, 5);
|
|
817
|
+
ctx.fillStyle = '#40b060';
|
|
818
|
+
ctx.fillRect(x + 8, y + 21, 3, 5);
|
|
819
|
+
ctx.fillStyle = '#e8d040';
|
|
820
|
+
ctx.fillRect(x + 12, y + 21, 3, 5);
|
|
821
|
+
|
|
822
|
+
// Microwave
|
|
823
|
+
ctx.fillStyle = '#606068';
|
|
824
|
+
ctx.fillRect(x + w - 20, y + 10, 16, 12);
|
|
825
|
+
ctx.fillStyle = '#1a1a2a';
|
|
826
|
+
ctx.fillRect(x + w - 18, y + 12, 10, 8);
|
|
827
|
+
// Button
|
|
828
|
+
ctx.fillStyle = '#40d060';
|
|
829
|
+
ctx.fillRect(x + w - 6, y + 14, 2, 2);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ── Gym ─────────────────────────────────────────────────────
|
|
833
|
+
|
|
834
|
+
_drawGym(ctx, x, y, w, h) {
|
|
835
|
+
// Room label
|
|
836
|
+
renderPixelText(ctx, 'GYM', x + w / 2, y + 2, '#6080a0', 1);
|
|
837
|
+
|
|
838
|
+
// Yoga mat
|
|
839
|
+
ctx.fillStyle = PAL.gymMat;
|
|
840
|
+
ctx.fillRect(x + 8, y + 14, 24, 12);
|
|
841
|
+
ctx.fillStyle = '#3858b0';
|
|
842
|
+
ctx.fillRect(x + 10, y + 16, 20, 8);
|
|
843
|
+
|
|
844
|
+
// Treadmill
|
|
845
|
+
ctx.fillStyle = '#505058';
|
|
846
|
+
ctx.fillRect(x + 40, y + 12, 20, 26);
|
|
847
|
+
ctx.fillStyle = '#404048';
|
|
848
|
+
ctx.fillRect(x + 42, y + 14, 16, 20);
|
|
849
|
+
// Running belt
|
|
850
|
+
ctx.fillStyle = '#353540';
|
|
851
|
+
ctx.fillRect(x + 44, y + 24, 12, 8);
|
|
852
|
+
// Handles
|
|
853
|
+
ctx.fillStyle = '#707078';
|
|
854
|
+
ctx.fillRect(x + 44, y + 14, 2, 10);
|
|
855
|
+
ctx.fillRect(x + 56, y + 14, 2, 10);
|
|
856
|
+
// Display
|
|
857
|
+
ctx.fillStyle = '#40d060';
|
|
858
|
+
ctx.fillRect(x + 48, y + 16, 6, 4);
|
|
859
|
+
|
|
860
|
+
// Dumbbells
|
|
861
|
+
ctx.fillStyle = '#505058';
|
|
862
|
+
ctx.fillRect(x + 70, y + 30, 16, 4);
|
|
863
|
+
ctx.fillStyle = '#404048';
|
|
864
|
+
ctx.fillRect(x + 68, y + 28, 4, 8);
|
|
865
|
+
ctx.fillRect(x + 84, y + 28, 4, 8);
|
|
866
|
+
|
|
867
|
+
// Weight rack
|
|
868
|
+
ctx.fillStyle = '#404048';
|
|
869
|
+
ctx.fillRect(x + 68, y + 12, 22, 14);
|
|
870
|
+
ctx.fillStyle = '#505058';
|
|
871
|
+
for (let i = 0; i < 3; i++) {
|
|
872
|
+
ctx.fillRect(x + 70 + i * 7, y + 14, 5, 10);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Water cooler
|
|
876
|
+
ctx.fillStyle = '#a0d0e0';
|
|
877
|
+
ctx.fillRect(x + w - 14, y + 14, 8, 14);
|
|
878
|
+
ctx.fillStyle = '#80b0c8';
|
|
879
|
+
ctx.fillRect(x + w - 12, y + 16, 4, 8);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ── Conveyor Belt (animated) ────────────────────────────────
|
|
883
|
+
|
|
884
|
+
_drawConveyorBelt(ctx, x, y, w, h, isDeploying, conveyorOffset, conveyorTime) {
|
|
885
|
+
// Belt base
|
|
886
|
+
ctx.fillStyle = PAL.convBelt;
|
|
887
|
+
ctx.fillRect(x, y + 4, w, h - 8);
|
|
888
|
+
|
|
889
|
+
// Rollers (animated per-project)
|
|
890
|
+
ctx.fillStyle = PAL.convRoller;
|
|
891
|
+
const offset = Math.floor(conveyorOffset);
|
|
892
|
+
for (let rx = offset; rx < w; rx += 8) {
|
|
893
|
+
ctx.fillRect(x + rx, y + 6, 4, h - 12);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Belt edges
|
|
897
|
+
ctx.fillStyle = '#808088';
|
|
898
|
+
ctx.fillRect(x, y + 4, w, 2);
|
|
899
|
+
ctx.fillRect(x, y + h - 6, w, 2);
|
|
900
|
+
|
|
901
|
+
// Moving package on belt when deploying
|
|
902
|
+
if (isDeploying) {
|
|
903
|
+
const pkgX = x + (conveyorTime * 40 % w);
|
|
904
|
+
this._drawMiniPackage(ctx, pkgX, y + h / 2 - 4);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Direction arrows
|
|
908
|
+
ctx.fillStyle = '#ffffff30';
|
|
909
|
+
for (let ax = 0; ax < w - 10; ax += 20) {
|
|
910
|
+
const axx = x + ax + (conveyorOffset * 2 % 20);
|
|
911
|
+
ctx.fillRect(axx + 4, y + h / 2 - 1, 6, 2);
|
|
912
|
+
ctx.fillRect(axx + 8, y + h / 2 - 3, 2, 6);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ── Warehouse ───────────────────────────────────────────────
|
|
917
|
+
|
|
918
|
+
_drawWarehouse(ctx, x, y, w, h, packages) {
|
|
919
|
+
// Warehouse floor
|
|
920
|
+
ctx.fillStyle = PAL.whGrayDk;
|
|
921
|
+
ctx.fillRect(x, y, w, h);
|
|
922
|
+
|
|
923
|
+
// Floor markings (grid)
|
|
924
|
+
ctx.strokeStyle = '#5a5a5a';
|
|
925
|
+
ctx.lineWidth = 0.5;
|
|
926
|
+
for (let gx = 0; gx < w; gx += TILE) {
|
|
927
|
+
ctx.beginPath();
|
|
928
|
+
ctx.moveTo(x + gx, y);
|
|
929
|
+
ctx.lineTo(x + gx, y + h);
|
|
930
|
+
ctx.stroke();
|
|
931
|
+
}
|
|
932
|
+
for (let gy = 0; gy < h; gy += TILE) {
|
|
933
|
+
ctx.beginPath();
|
|
934
|
+
ctx.moveTo(x, y + gy);
|
|
935
|
+
ctx.lineTo(x + w, y + gy);
|
|
936
|
+
ctx.stroke();
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Warehouse walls (back and sides)
|
|
940
|
+
ctx.fillStyle = PAL.whGray;
|
|
941
|
+
ctx.fillRect(x, y, w, 6);
|
|
942
|
+
ctx.fillRect(x, y, 4, h);
|
|
943
|
+
ctx.fillRect(x + w - 4, y, 4, h);
|
|
944
|
+
|
|
945
|
+
// Sign
|
|
946
|
+
ctx.fillStyle = '#1a2a3a';
|
|
947
|
+
ctx.fillRect(x + w / 2 - 30, y - 4, 60, 12);
|
|
948
|
+
renderPixelText(ctx, 'WAREHOUSE', x + w / 2, y - 2, '#e8d040', 1);
|
|
949
|
+
|
|
950
|
+
// Shelving units (3 rows)
|
|
951
|
+
for (let row = 0; row < 3; row++) {
|
|
952
|
+
const sy = y + 14 + row * (TILE + 14);
|
|
953
|
+
// Shelf
|
|
954
|
+
ctx.fillStyle = '#6a6a72';
|
|
955
|
+
ctx.fillRect(x + 8, sy, w - 16, 4);
|
|
956
|
+
ctx.fillStyle = '#5a5a62';
|
|
957
|
+
ctx.fillRect(x + 8, sy + 4, w - 16, 2);
|
|
958
|
+
// Shelf supports
|
|
959
|
+
ctx.fillStyle = '#505058';
|
|
960
|
+
ctx.fillRect(x + 10, sy, 3, TILE + 8);
|
|
961
|
+
ctx.fillRect(x + w - 14, sy, 3, TILE + 8);
|
|
962
|
+
ctx.fillRect(x + w / 2, sy, 3, TILE + 8);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Draw shipped packages on shelves
|
|
966
|
+
if (packages && packages.length > 0) {
|
|
967
|
+
for (let i = 0; i < packages.length; i++) {
|
|
968
|
+
const pkg = packages[i];
|
|
969
|
+
const row = Math.floor(i / 4);
|
|
970
|
+
const col = i % 4;
|
|
971
|
+
if (row >= 3) break;
|
|
972
|
+
|
|
973
|
+
const px = x + 18 + col * ((w - 36) / 4);
|
|
974
|
+
const py = y + 14 + row * (TILE + 14) - 14;
|
|
975
|
+
this._drawShippedPackage(ctx, px, py, pkg.name);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Forklift (static decoration)
|
|
980
|
+
ctx.fillStyle = '#e8a020';
|
|
981
|
+
ctx.fillRect(x + w - 40, y + h - 24, 16, 10);
|
|
982
|
+
ctx.fillStyle = '#c88010';
|
|
983
|
+
ctx.fillRect(x + w - 38, y + h - 22, 12, 6);
|
|
984
|
+
// Forks
|
|
985
|
+
ctx.fillStyle = '#808088';
|
|
986
|
+
ctx.fillRect(x + w - 44, y + h - 18, 6, 2);
|
|
987
|
+
ctx.fillRect(x + w - 44, y + h - 14, 6, 2);
|
|
988
|
+
// Wheels
|
|
989
|
+
ctx.fillStyle = '#333';
|
|
990
|
+
ctx.fillRect(x + w - 40, y + h - 14, 5, 4);
|
|
991
|
+
ctx.fillRect(x + w - 28, y + h - 14, 5, 4);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
_drawShippedPackage(ctx, x, y, name) {
|
|
995
|
+
// Package box
|
|
996
|
+
ctx.fillStyle = PAL.pkgTan;
|
|
997
|
+
ctx.fillRect(x, y, 20, 14);
|
|
998
|
+
// Outline
|
|
999
|
+
ctx.fillStyle = PAL.pkgTan2;
|
|
1000
|
+
ctx.fillRect(x, y, 20, 2);
|
|
1001
|
+
ctx.fillRect(x, y, 2, 14);
|
|
1002
|
+
ctx.fillRect(x + 18, y, 2, 14);
|
|
1003
|
+
ctx.fillRect(x, y + 12, 20, 2);
|
|
1004
|
+
// Cross tape
|
|
1005
|
+
ctx.fillStyle = PAL.pkgTan2;
|
|
1006
|
+
ctx.fillRect(x + 9, y, 2, 14);
|
|
1007
|
+
ctx.fillRect(x, y + 6, 20, 2);
|
|
1008
|
+
// Checkmark (shipped!)
|
|
1009
|
+
ctx.fillStyle = '#40d060';
|
|
1010
|
+
ctx.fillRect(x + 14, y + 2, 2, 2);
|
|
1011
|
+
ctx.fillRect(x + 16, y + 1, 2, 2);
|
|
1012
|
+
ctx.fillRect(x + 12, y + 3, 2, 2);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
_drawMiniPackage(ctx, x, y) {
|
|
1016
|
+
ctx.fillStyle = PAL.pkgTan;
|
|
1017
|
+
ctx.fillRect(x, y, 12, 8);
|
|
1018
|
+
ctx.fillStyle = PAL.pkgTan2;
|
|
1019
|
+
ctx.fillRect(x + 5, y, 2, 8);
|
|
1020
|
+
ctx.fillRect(x, y + 3, 12, 2);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// ── Lounge ──────────────────────────────────────────────────
|
|
1024
|
+
|
|
1025
|
+
_drawLounge(ctx, x, y) {
|
|
1026
|
+
// Bench
|
|
1027
|
+
ctx.fillStyle = '#5a4a3a';
|
|
1028
|
+
ctx.fillRect(x + 4, y + 10, 28, 10);
|
|
1029
|
+
ctx.fillStyle = '#4a3a2a';
|
|
1030
|
+
ctx.fillRect(x + 6, y + 8, 24, 4);
|
|
1031
|
+
// Cushions
|
|
1032
|
+
ctx.fillStyle = '#6a4060';
|
|
1033
|
+
ctx.fillRect(x + 8, y + 12, 10, 6);
|
|
1034
|
+
ctx.fillStyle = '#4060a0';
|
|
1035
|
+
ctx.fillRect(x + 20, y + 12, 10, 6);
|
|
1036
|
+
|
|
1037
|
+
// Plant
|
|
1038
|
+
ctx.fillStyle = '#6a4030';
|
|
1039
|
+
ctx.fillRect(x + 38, y + 20, 8, 12);
|
|
1040
|
+
ctx.fillStyle = '#306028';
|
|
1041
|
+
ctx.beginPath();
|
|
1042
|
+
ctx.arc(x + 42, y + 16, 10, 0, Math.PI * 2);
|
|
1043
|
+
ctx.fill();
|
|
1044
|
+
ctx.fillStyle = '#408030';
|
|
1045
|
+
ctx.beginPath();
|
|
1046
|
+
ctx.arc(x + 40, y + 14, 6, 0, Math.PI * 2);
|
|
1047
|
+
ctx.fill();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ── Existing drawing methods (updated) ──────────────────────
|
|
1051
|
+
|
|
1052
|
+
_drawCarpet(ctx, x, y, w, h) {
|
|
1053
|
+
ctx.fillStyle = PAL.carpet;
|
|
1054
|
+
ctx.fillRect(x, y, w, h);
|
|
1055
|
+
ctx.fillStyle = '#3a2a52';
|
|
1056
|
+
for (let py = 0; py < h; py += 8) {
|
|
1057
|
+
for (let px = 0; px < w; px += 8) {
|
|
1058
|
+
if ((px / 8 + py / 8) % 2 === 0) {
|
|
1059
|
+
ctx.fillRect(x + px, y + py, 4, 4);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
_drawPixelDesk(ctx, x, y, w, h) {
|
|
1066
|
+
ctx.fillStyle = PAL.shadow;
|
|
1067
|
+
ctx.fillRect(x + 4, y + h - 2, w - 4, 4);
|
|
1068
|
+
ctx.fillStyle = PAL.deskLt;
|
|
1069
|
+
ctx.fillRect(x + 2, y + 2, w - 4, h - 8);
|
|
1070
|
+
ctx.fillStyle = PAL.deskDk;
|
|
1071
|
+
ctx.fillRect(x + 2, y + 2, w - 4, 2);
|
|
1072
|
+
ctx.fillRect(x + 2, y + h - 8, w - 4, 2);
|
|
1073
|
+
ctx.fillRect(x + 2, y + 2, 2, h - 8);
|
|
1074
|
+
ctx.fillRect(x + w - 4, y + 2, 2, h - 8);
|
|
1075
|
+
ctx.fillStyle = PAL.desk;
|
|
1076
|
+
ctx.fillRect(x + 2, y + h - 6, w - 4, 6);
|
|
1077
|
+
ctx.fillStyle = PAL.deskDk;
|
|
1078
|
+
ctx.fillRect(x + 4, y + h, 3, 4);
|
|
1079
|
+
ctx.fillRect(x + w - 7, y + h, 3, 4);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
_drawPapers(ctx, x, y) {
|
|
1083
|
+
ctx.fillStyle = '#f0f0e8';
|
|
1084
|
+
ctx.fillRect(x, y, 14, 18);
|
|
1085
|
+
ctx.fillStyle = '#e0e0d8';
|
|
1086
|
+
ctx.fillRect(x + 2, y + 2, 14, 18);
|
|
1087
|
+
ctx.fillStyle = '#bbb';
|
|
1088
|
+
for (let i = 0; i < 4; i++) {
|
|
1089
|
+
ctx.fillRect(x + 4, y + 6 + i * 4, 10, 1);
|
|
1090
|
+
}
|
|
1091
|
+
ctx.fillStyle = '#3050a0';
|
|
1092
|
+
ctx.fillRect(x + 16, y + 4, 2, 12);
|
|
1093
|
+
ctx.fillStyle = '#2040a0';
|
|
1094
|
+
ctx.fillRect(x + 16, y + 14, 2, 4);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
_drawMagnifier(ctx, x, y) {
|
|
1098
|
+
ctx.strokeStyle = '#888';
|
|
1099
|
+
ctx.lineWidth = 2;
|
|
1100
|
+
ctx.beginPath();
|
|
1101
|
+
ctx.arc(x + 8, y + 8, 6, 0, Math.PI * 2);
|
|
1102
|
+
ctx.stroke();
|
|
1103
|
+
ctx.fillStyle = '#c0e0ff80';
|
|
1104
|
+
ctx.beginPath();
|
|
1105
|
+
ctx.arc(x + 8, y + 8, 5, 0, Math.PI * 2);
|
|
1106
|
+
ctx.fill();
|
|
1107
|
+
ctx.fillStyle = '#6a5040';
|
|
1108
|
+
ctx.fillRect(x + 12, y + 12, 3, 8);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
_drawCodingStation(ctx, x, y, w, h, stage = '', t = 0) {
|
|
1112
|
+
this._drawPixelDesk(ctx, x, y, w, h);
|
|
1113
|
+
const monW = 30, monH = 22, gap = 8;
|
|
1114
|
+
const totalMonW = monW * 3 + gap * 2;
|
|
1115
|
+
const startX = x + (w - totalMonW) / 2;
|
|
1116
|
+
|
|
1117
|
+
// Determine monitor mode from stage
|
|
1118
|
+
const isCoding = ['implementation', 'implementation_complete'].includes(stage);
|
|
1119
|
+
const isReview = ['review', 'review_complete'].includes(stage);
|
|
1120
|
+
const isRejected = stage === 'rejected';
|
|
1121
|
+
const isIdle = !stage || stage === '' || stage === 'inbox';
|
|
1122
|
+
|
|
1123
|
+
// Phase 6: Pseudo-code lines for realistic code scrolling
|
|
1124
|
+
// Each line: [indentLevel, type] where type determines color
|
|
1125
|
+
// Types: 'kw' = keyword, 'str' = string, 'cmt' = comment, 'num' = number, 'code' = normal
|
|
1126
|
+
const CODE_LINES = [
|
|
1127
|
+
[0, 'kw', 10], [0, 'code', 14], [1, 'str', 12], [1, 'code', 8],
|
|
1128
|
+
[2, 'kw', 6], [2, 'num', 10], [2, 'code', 14], [1, 'code', 9],
|
|
1129
|
+
[0, 'cmt', 16], [0, 'kw', 8], [1, 'code', 12], [1, 'str', 10],
|
|
1130
|
+
[2, 'kw', 7], [2, 'num', 5], [3, 'code', 11], [3, 'str', 13],
|
|
1131
|
+
[2, 'code', 9], [1, 'kw', 8], [0, 'code', 15], [0, 'cmt', 18],
|
|
1132
|
+
[0, 'kw', 11], [1, 'code', 7], [1, 'num', 6], [2, 'str', 14],
|
|
1133
|
+
];
|
|
1134
|
+
const SYNTAX_COLORS = {
|
|
1135
|
+
kw: '#4080d0', // keywords in blue
|
|
1136
|
+
str: '#40a050', // strings in green
|
|
1137
|
+
cmt: '#606060', // comments in gray
|
|
1138
|
+
num: '#d08030', // numbers in orange
|
|
1139
|
+
code: '#c0c0d0', // normal code
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// Terminal output lines (green on black)
|
|
1143
|
+
const TERM_LINES = [
|
|
1144
|
+
[0, 8], [0, 14], [0, 10], [0, 6], [0, 16], [0, 12],
|
|
1145
|
+
[0, 9], [0, 11], [0, 7], [0, 15], [0, 13], [0, 5],
|
|
1146
|
+
];
|
|
1147
|
+
|
|
1148
|
+
// Compile error flash: brief red flash every ~20s for 0.3s
|
|
1149
|
+
const errorCycleT = t % 20;
|
|
1150
|
+
const isErrorFlash = isCoding && errorCycleT > 19.7;
|
|
1151
|
+
|
|
1152
|
+
for (let i = 0; i < 3; i++) {
|
|
1153
|
+
const mx = startX + i * (monW + gap);
|
|
1154
|
+
const my = y - 4;
|
|
1155
|
+
const screenX = mx + 2;
|
|
1156
|
+
const screenY = my + 2;
|
|
1157
|
+
const screenW = monW - 4;
|
|
1158
|
+
const screenH = monH - 6;
|
|
1159
|
+
|
|
1160
|
+
// Monitor bezel
|
|
1161
|
+
ctx.fillStyle = PAL.monitor;
|
|
1162
|
+
ctx.fillRect(mx, my, monW, monH);
|
|
1163
|
+
|
|
1164
|
+
// Screen background
|
|
1165
|
+
if (isIdle) {
|
|
1166
|
+
ctx.fillStyle = '#050d15';
|
|
1167
|
+
} else if (isRejected || isErrorFlash) {
|
|
1168
|
+
ctx.fillStyle = '#1a0808';
|
|
1169
|
+
} else if (isReview) {
|
|
1170
|
+
ctx.fillStyle = '#081418';
|
|
1171
|
+
} else if (i === 2 && isCoding) {
|
|
1172
|
+
ctx.fillStyle = '#0a0a0a'; // terminal: pure black
|
|
1173
|
+
} else {
|
|
1174
|
+
ctx.fillStyle = '#0a1a2a';
|
|
1175
|
+
}
|
|
1176
|
+
ctx.fillRect(screenX, screenY, screenW, screenH);
|
|
1177
|
+
|
|
1178
|
+
if (!isIdle) {
|
|
1179
|
+
ctx.save();
|
|
1180
|
+
ctx.beginPath();
|
|
1181
|
+
ctx.rect(screenX, screenY, screenW, screenH);
|
|
1182
|
+
ctx.clip();
|
|
1183
|
+
|
|
1184
|
+
if (isCoding) {
|
|
1185
|
+
// ── Phase 6: Realistic code / terminal scrolling ──
|
|
1186
|
+
const isTerminal = (i === 2); // third monitor is terminal
|
|
1187
|
+
const lineH = 3; // pixel height per code line
|
|
1188
|
+
const maxVisibleLines = Math.floor(screenH / lineH);
|
|
1189
|
+
const scrollOffset = (t * 6 + i * 7) % (CODE_LINES.length * lineH);
|
|
1190
|
+
const lines = isTerminal ? TERM_LINES : CODE_LINES;
|
|
1191
|
+
|
|
1192
|
+
for (let ln = 0; ln < maxVisibleLines + 2; ln++) {
|
|
1193
|
+
const lineIdx = (ln + Math.floor(scrollOffset / lineH)) % lines.length;
|
|
1194
|
+
const lineData = lines[lineIdx];
|
|
1195
|
+
const indentPx = isTerminal ? 1 : lineData[0] * 2; // indent in pixels
|
|
1196
|
+
const lineY = screenY + ln * lineH - (scrollOffset % lineH);
|
|
1197
|
+
|
|
1198
|
+
if (lineY < screenY - lineH || lineY > screenY + screenH) continue;
|
|
1199
|
+
|
|
1200
|
+
if (isTerminal) {
|
|
1201
|
+
// Terminal: green on black
|
|
1202
|
+
ctx.fillStyle = '#30d050';
|
|
1203
|
+
ctx.fillRect(screenX + 1, lineY, lineData[1], 2);
|
|
1204
|
+
} else {
|
|
1205
|
+
// Code: syntax colored
|
|
1206
|
+
const type = lineData[1];
|
|
1207
|
+
const lineW = lineData[2];
|
|
1208
|
+
ctx.fillStyle = (isErrorFlash) ? '#ff4040' : SYNTAX_COLORS[type];
|
|
1209
|
+
ctx.fillRect(screenX + 2 + indentPx, lineY, Math.min(lineW, screenW - 4 - indentPx), 2);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Blinking cursor at end of current line
|
|
1214
|
+
if (Math.sin(t * 4 + i) > 0) {
|
|
1215
|
+
const cursorLine = Math.floor(maxVisibleLines * 0.7);
|
|
1216
|
+
const cursorLineIdx = (cursorLine + Math.floor(scrollOffset / lineH)) % lines.length;
|
|
1217
|
+
const cLineData = lines[cursorLineIdx];
|
|
1218
|
+
const cIndent = isTerminal ? 1 : cLineData[0] * 2;
|
|
1219
|
+
const cLineW = isTerminal ? cLineData[1] : cLineData[2];
|
|
1220
|
+
ctx.fillStyle = isTerminal ? '#30ff50' : '#40ff80';
|
|
1221
|
+
ctx.fillRect(screenX + 2 + cIndent + cLineW + 1, screenY + cursorLine * lineH, 1, 2);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Scrollbar on right side
|
|
1225
|
+
ctx.fillStyle = '#303040';
|
|
1226
|
+
ctx.fillRect(screenX + screenW - 2, screenY, 2, screenH);
|
|
1227
|
+
const scrollFrac = (scrollOffset / (lines.length * lineH));
|
|
1228
|
+
const thumbH = Math.max(3, Math.floor(screenH * 0.3));
|
|
1229
|
+
ctx.fillStyle = '#606080';
|
|
1230
|
+
ctx.fillRect(screenX + screenW - 2, screenY + scrollFrac * (screenH - thumbH), 2, thumbH);
|
|
1231
|
+
|
|
1232
|
+
} else {
|
|
1233
|
+
// Non-coding states (review, rejected) — keep original simple rendering
|
|
1234
|
+
const lineColors = isRejected
|
|
1235
|
+
? ['#ff4040', '#ff8040', '#ff4060', '#e04040']
|
|
1236
|
+
: ['#40e0d0', '#40b0ff', '#80e0e0', '#40a8f0'];
|
|
1237
|
+
const LINE_WIDTHS = [14, 8, 12, 6, 10, 16, 7, 13, 9, 15, 11, 6];
|
|
1238
|
+
|
|
1239
|
+
for (let ln = 0; ln < 4; ln++) {
|
|
1240
|
+
ctx.fillStyle = lineColors[(i * 4 + ln) % lineColors.length];
|
|
1241
|
+
const lineW = LINE_WIDTHS[(i * 4 + ln) % LINE_WIDTHS.length];
|
|
1242
|
+
ctx.fillRect(mx + 4, my + 4 + ln * 4, lineW, 2);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
ctx.restore();
|
|
1247
|
+
|
|
1248
|
+
// Glow effect on active monitors
|
|
1249
|
+
ctx.save();
|
|
1250
|
+
if (isErrorFlash) {
|
|
1251
|
+
ctx.globalAlpha = 0.3;
|
|
1252
|
+
ctx.fillStyle = '#ff4040';
|
|
1253
|
+
} else {
|
|
1254
|
+
ctx.globalAlpha = 0.15 + Math.sin(t * 2 + i) * 0.08;
|
|
1255
|
+
ctx.fillStyle = isRejected ? '#ff4040' : isReview ? '#40d0d0' : '#40a8f0';
|
|
1256
|
+
}
|
|
1257
|
+
ctx.fillRect(mx - 2, my - 2, monW + 4, monH + 4);
|
|
1258
|
+
ctx.restore();
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Monitor stand
|
|
1262
|
+
ctx.fillStyle = '#444';
|
|
1263
|
+
ctx.fillRect(mx + monW / 2 - 3, my + monH, 6, 3);
|
|
1264
|
+
ctx.fillRect(mx + monW / 2 - 6, my + monH + 3, 12, 2);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Keyboard lightning spark when coding
|
|
1268
|
+
if (isCoding && Math.sin(t * 6) > 0.7) {
|
|
1269
|
+
ctx.fillStyle = '#60d0ff';
|
|
1270
|
+
ctx.fillRect(x + w / 2 - 2 + Math.floor(t * 5) % 6, y + h - 8, 2, 4);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
ctx.fillStyle = '#505058';
|
|
1274
|
+
ctx.fillRect(x + w / 2 - 20, y + h - 6, 40, 8);
|
|
1275
|
+
ctx.fillStyle = '#606068';
|
|
1276
|
+
for (let kx = 0; kx < 9; kx++) {
|
|
1277
|
+
for (let ky = 0; ky < 2; ky++) {
|
|
1278
|
+
ctx.fillRect(x + w / 2 - 18 + kx * 4, y + h - 5 + ky * 4, 3, 3);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
_drawReviewTableAnim(ctx, x, y, w, h, stage = '', t = 0) {
|
|
1284
|
+
if (stage !== 'review') return;
|
|
1285
|
+
// Pulsing glow around review table
|
|
1286
|
+
ctx.save();
|
|
1287
|
+
ctx.globalAlpha = 0.18 + Math.sin(t * 3) * 0.1;
|
|
1288
|
+
ctx.fillStyle = '#e0d040';
|
|
1289
|
+
ctx.fillRect(x - 2, y - 2, w + 4, h + 4);
|
|
1290
|
+
ctx.restore();
|
|
1291
|
+
// Animated magnifier movement
|
|
1292
|
+
const mx = x + w / 2 + Math.sin(t * 2) * 8;
|
|
1293
|
+
const my = y + 4 + Math.cos(t * 1.5) * 3;
|
|
1294
|
+
ctx.save();
|
|
1295
|
+
ctx.globalAlpha = 0.7;
|
|
1296
|
+
ctx.strokeStyle = '#e0d040';
|
|
1297
|
+
ctx.lineWidth = 1;
|
|
1298
|
+
ctx.beginPath();
|
|
1299
|
+
ctx.arc(mx, my, 5, 0, Math.PI * 2);
|
|
1300
|
+
ctx.stroke();
|
|
1301
|
+
ctx.restore();
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
_drawDeployDock(ctx, x, y, w, h, stage = '', t = 0, truckAnimActive = false) {
|
|
1305
|
+
ctx.fillStyle = PAL.dockBg;
|
|
1306
|
+
ctx.fillRect(x + 2, y + 2, w - 4, h + 4);
|
|
1307
|
+
ctx.fillStyle = PAL.dockYel;
|
|
1308
|
+
for (let i = 0; i < w - 4; i += 8) {
|
|
1309
|
+
ctx.fillRect(x + 2 + i, y + 2, 4, 2);
|
|
1310
|
+
ctx.fillRect(x + 2 + i + 4, y + h + 2, 4, 2);
|
|
1311
|
+
}
|
|
1312
|
+
for (let i = 0; i < h + 2; i += 8) {
|
|
1313
|
+
ctx.fillRect(x + 2, y + 2 + i, 2, 4);
|
|
1314
|
+
ctx.fillRect(x + w - 4, y + 2 + i + 4, 2, 4);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const isDeploying = stage === 'deploying';
|
|
1318
|
+
const isDeployed = stage === 'deployed';
|
|
1319
|
+
|
|
1320
|
+
// Hide static truck when animated truck is active
|
|
1321
|
+
if (!truckAnimActive) {
|
|
1322
|
+
// Truck drifts slightly when deploying
|
|
1323
|
+
const truckOffX = isDeploying ? Math.sin(t * 3) * 1.5 : 0;
|
|
1324
|
+
this._drawPixelTruck(ctx, x + w / 2 - 16 + truckOffX, y + 8, isDeploying || isDeployed, t);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Dock glow / warning when deploying
|
|
1328
|
+
if (isDeploying) {
|
|
1329
|
+
ctx.save();
|
|
1330
|
+
ctx.globalAlpha = 0.18 + Math.abs(Math.sin(t * 4)) * 0.15;
|
|
1331
|
+
ctx.fillStyle = '#ffcc00';
|
|
1332
|
+
ctx.fillRect(x, y, w, h + 6);
|
|
1333
|
+
ctx.restore();
|
|
1334
|
+
// VROOM label (only show when truck is not animating — it has its own label)
|
|
1335
|
+
if (!truckAnimActive) {
|
|
1336
|
+
ctx.save();
|
|
1337
|
+
ctx.globalAlpha = 0.7 + Math.sin(t * 5) * 0.3;
|
|
1338
|
+
renderPixelText(ctx, '🚀 DEPLOY!', x + w / 2, y + h - 4, '#ffcc00', 1);
|
|
1339
|
+
ctx.restore();
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
_drawPixelTruck(ctx, x, y, active = false, t = 0) {
|
|
1345
|
+
ctx.fillStyle = '#c8a050';
|
|
1346
|
+
ctx.fillRect(x, y, 20, 16);
|
|
1347
|
+
ctx.fillStyle = '#a88030';
|
|
1348
|
+
ctx.fillRect(x, y, 20, 2);
|
|
1349
|
+
ctx.fillRect(x, y, 2, 16);
|
|
1350
|
+
ctx.fillRect(x + 18, y, 2, 16);
|
|
1351
|
+
ctx.fillStyle = '#a88030';
|
|
1352
|
+
ctx.fillRect(x + 9, y, 2, 16);
|
|
1353
|
+
ctx.fillRect(x, y + 7, 20, 2);
|
|
1354
|
+
ctx.fillStyle = '#d04040';
|
|
1355
|
+
ctx.fillRect(x + 20, y + 4, 12, 12);
|
|
1356
|
+
ctx.fillStyle = '#80c0e0';
|
|
1357
|
+
ctx.fillRect(x + 22, y + 6, 8, 6);
|
|
1358
|
+
ctx.fillStyle = '#222';
|
|
1359
|
+
ctx.fillRect(x + 2, y + 16, 6, 4);
|
|
1360
|
+
ctx.fillRect(x + 24, y + 16, 6, 4);
|
|
1361
|
+
ctx.fillStyle = '#666';
|
|
1362
|
+
ctx.fillRect(x + 4, y + 17, 2, 2);
|
|
1363
|
+
ctx.fillRect(x + 26, y + 17, 2, 2);
|
|
1364
|
+
// Headlights flash when active
|
|
1365
|
+
if (active) {
|
|
1366
|
+
const on = Math.sin(t * 6) > 0;
|
|
1367
|
+
ctx.fillStyle = on ? '#ffffc0' : '#806030';
|
|
1368
|
+
ctx.fillRect(x + 2, y + 4, 4, 3);
|
|
1369
|
+
ctx.fillRect(x + 2, y + 9, 4, 3);
|
|
1370
|
+
if (on) {
|
|
1371
|
+
ctx.save();
|
|
1372
|
+
ctx.globalAlpha = 0.4;
|
|
1373
|
+
ctx.fillStyle = '#ffffa0';
|
|
1374
|
+
ctx.fillRect(x - 6, y + 3, 8, 10);
|
|
1375
|
+
ctx.restore();
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
_drawCoffeeMachine(ctx, x, y) {
|
|
1381
|
+
// Coffee table
|
|
1382
|
+
ctx.fillStyle = PAL.desk;
|
|
1383
|
+
ctx.fillRect(x + 2, y + 20, 28, 16);
|
|
1384
|
+
ctx.fillStyle = PAL.deskDk;
|
|
1385
|
+
ctx.fillRect(x + 2, y + 34, 28, 3);
|
|
1386
|
+
// Table legs
|
|
1387
|
+
ctx.fillStyle = PAL.deskDk;
|
|
1388
|
+
ctx.fillRect(x + 4, y + 36, 3, 8);
|
|
1389
|
+
ctx.fillRect(x + 25, y + 36, 3, 8);
|
|
1390
|
+
|
|
1391
|
+
// Coffee machine on table
|
|
1392
|
+
ctx.fillStyle = '#606068';
|
|
1393
|
+
ctx.fillRect(x + 6, y + 6, 20, 16);
|
|
1394
|
+
ctx.fillStyle = '#505058';
|
|
1395
|
+
ctx.fillRect(x + 8, y + 8, 16, 12);
|
|
1396
|
+
// Screen
|
|
1397
|
+
ctx.fillStyle = '#40a040';
|
|
1398
|
+
ctx.fillRect(x + 10, y + 9, 12, 5);
|
|
1399
|
+
// Label uses native text for clean rendering
|
|
1400
|
+
ctx.fillStyle = '#1a3a1a';
|
|
1401
|
+
ctx.font = 'bold 5px monospace';
|
|
1402
|
+
ctx.textAlign = 'center';
|
|
1403
|
+
ctx.fillText('COFFEE', x + 16, y + 13);
|
|
1404
|
+
ctx.textAlign = 'start';
|
|
1405
|
+
// Dispenser
|
|
1406
|
+
ctx.fillStyle = '#404048';
|
|
1407
|
+
ctx.fillRect(x + 10, y + 16, 12, 6);
|
|
1408
|
+
// Cup
|
|
1409
|
+
ctx.fillStyle = '#f0f0e8';
|
|
1410
|
+
ctx.fillRect(x + 13, y + 18, 6, 5);
|
|
1411
|
+
ctx.fillStyle = '#6a3a1a';
|
|
1412
|
+
ctx.fillRect(x + 14, y + 19, 4, 3);
|
|
1413
|
+
// Steam
|
|
1414
|
+
ctx.fillStyle = '#ffffff40';
|
|
1415
|
+
ctx.fillRect(x + 14, y + 15, 1, 3);
|
|
1416
|
+
ctx.fillRect(x + 17, y + 14, 1, 3);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// ── Phase 4: PM Desk Lamp ──────────────────────────────────
|
|
1420
|
+
|
|
1421
|
+
_drawDeskLamp(ctx, x, y, stage = '', t = 0) {
|
|
1422
|
+
const isActive = stage === 'spec' || stage === 'review';
|
|
1423
|
+
|
|
1424
|
+
// Lamp base (4x2 px dark gray)
|
|
1425
|
+
ctx.fillStyle = '#505058';
|
|
1426
|
+
ctx.fillRect(x, y + 18, 8, 4);
|
|
1427
|
+
|
|
1428
|
+
// Arm (angled, ~10px tall)
|
|
1429
|
+
ctx.fillStyle = '#606068';
|
|
1430
|
+
ctx.save();
|
|
1431
|
+
ctx.translate(x + 4, y + 18);
|
|
1432
|
+
ctx.rotate(-0.35); // slight angle
|
|
1433
|
+
ctx.fillRect(-1, -12, 3, 12);
|
|
1434
|
+
ctx.restore();
|
|
1435
|
+
|
|
1436
|
+
// Shade (trapezoid-ish rectangle at top of arm)
|
|
1437
|
+
const shadeX = x + 1;
|
|
1438
|
+
const shadeY = y + 2;
|
|
1439
|
+
ctx.fillStyle = '#707858';
|
|
1440
|
+
ctx.fillRect(shadeX - 2, shadeY, 14, 5);
|
|
1441
|
+
ctx.fillStyle = '#606850';
|
|
1442
|
+
ctx.fillRect(shadeX - 1, shadeY + 5, 12, 2);
|
|
1443
|
+
|
|
1444
|
+
if (isActive) {
|
|
1445
|
+
// Pulsing yellow glow under shade
|
|
1446
|
+
const pulse = 0.5 + Math.sin(t * 3) * 0.3;
|
|
1447
|
+
ctx.save();
|
|
1448
|
+
ctx.globalAlpha = pulse;
|
|
1449
|
+
// Bulb glow
|
|
1450
|
+
ctx.fillStyle = '#ffe860';
|
|
1451
|
+
ctx.fillRect(shadeX + 2, shadeY + 5, 6, 3);
|
|
1452
|
+
// Light cone (semi-transparent triangle pointing down)
|
|
1453
|
+
ctx.globalAlpha = pulse * 0.35;
|
|
1454
|
+
ctx.fillStyle = '#ffee80';
|
|
1455
|
+
ctx.beginPath();
|
|
1456
|
+
ctx.moveTo(shadeX + 1, shadeY + 7);
|
|
1457
|
+
ctx.lineTo(shadeX + 9, shadeY + 7);
|
|
1458
|
+
ctx.lineTo(shadeX + 12, y + 20);
|
|
1459
|
+
ctx.lineTo(shadeX - 2, y + 20);
|
|
1460
|
+
ctx.closePath();
|
|
1461
|
+
ctx.fill();
|
|
1462
|
+
// Soft glow circle
|
|
1463
|
+
ctx.globalAlpha = pulse * 0.15;
|
|
1464
|
+
ctx.fillStyle = '#ffee60';
|
|
1465
|
+
ctx.beginPath();
|
|
1466
|
+
ctx.arc(shadeX + 5, y + 14, 10, 0, Math.PI * 2);
|
|
1467
|
+
ctx.fill();
|
|
1468
|
+
ctx.restore();
|
|
1469
|
+
} else {
|
|
1470
|
+
// Inactive: dim bulb
|
|
1471
|
+
ctx.fillStyle = '#404040';
|
|
1472
|
+
ctx.fillRect(shadeX + 3, shadeY + 5, 4, 2);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
_drawChair(ctx, x, y) {
|
|
1477
|
+
ctx.fillStyle = '#505068';
|
|
1478
|
+
ctx.fillRect(x - 5, y - 3, 10, 8);
|
|
1479
|
+
ctx.fillStyle = '#404058';
|
|
1480
|
+
ctx.fillRect(x - 4, y - 6, 8, 4);
|
|
1481
|
+
ctx.fillStyle = '#606078';
|
|
1482
|
+
ctx.fillRect(x - 4, y - 2, 8, 6);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
hitTest(wx, wy) {
|
|
1486
|
+
for (const [name, origin] of this._teamOrigins) {
|
|
1487
|
+
const ox = origin.x;
|
|
1488
|
+
const oy = origin.y;
|
|
1489
|
+
if (wx >= ox && wx <= ox + TEAM_WIDTH * TILE && wy >= oy && wy <= oy + TEAM_HEIGHT * TILE) {
|
|
1490
|
+
return { project: name, config: origin.config };
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/** Hit-test warehouse packages; returns { project, pkg } or null */
|
|
1497
|
+
hitTestPackage(wx, wy) {
|
|
1498
|
+
for (const [name, origin] of this._teamOrigins) {
|
|
1499
|
+
const data = this._projectData.get(name);
|
|
1500
|
+
if (!data || !data.packages.length) continue;
|
|
1501
|
+
const wh = STATIONS.warehouse;
|
|
1502
|
+
const ox = origin.x + wh.x * TILE;
|
|
1503
|
+
const oy = origin.y + wh.y * TILE;
|
|
1504
|
+
const w = wh.w * TILE;
|
|
1505
|
+
for (let i = 0; i < data.packages.length; i++) {
|
|
1506
|
+
const row = Math.floor(i / 4);
|
|
1507
|
+
const col = i % 4;
|
|
1508
|
+
if (row >= 3) break;
|
|
1509
|
+
const px = ox + 18 + col * ((w - 36) / 4);
|
|
1510
|
+
const py = oy + 14 + row * (TILE + 14) - 14;
|
|
1511
|
+
if (wx >= px && wx <= px + 22 && wy >= py && wy <= py + 16) {
|
|
1512
|
+
return { project: name, pkg: data.packages[i], index: i, total: data.packages.length };
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
getTeamOrigin(projectName) {
|
|
1520
|
+
return this._teamOrigins.get(projectName);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
export { STATIONS, TILE, TEAM_WIDTH, TEAM_HEIGHT };
|