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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +584 -0
  3. package/bin/aicc.js +772 -0
  4. package/lib/actions/approve.js +71 -0
  5. package/lib/actions/assign-project.js +132 -0
  6. package/lib/actions/browser-test.js +64 -0
  7. package/lib/actions/cleanup.js +174 -0
  8. package/lib/actions/debug.js +298 -0
  9. package/lib/actions/deploy.js +1229 -0
  10. package/lib/actions/fix-bug.js +134 -0
  11. package/lib/actions/new-feature.js +255 -0
  12. package/lib/actions/reject.js +307 -0
  13. package/lib/actions/review.js +706 -0
  14. package/lib/actions/status.js +47 -0
  15. package/lib/agents/browser-qa-agent.js +611 -0
  16. package/lib/agents/payment-agent.js +116 -0
  17. package/lib/agents/suggestion-agent.js +88 -0
  18. package/lib/cli.js +303 -0
  19. package/lib/config.js +243 -0
  20. package/lib/hub/hub-server.js +440 -0
  21. package/lib/hub/project-poller.js +75 -0
  22. package/lib/hub/skill-registry.js +89 -0
  23. package/lib/hub/state-aggregator.js +204 -0
  24. package/lib/index.js +471 -0
  25. package/lib/init/doctor.js +523 -0
  26. package/lib/init/presets.js +222 -0
  27. package/lib/init/skill-fetcher.js +77 -0
  28. package/lib/init/wizard.js +973 -0
  29. package/lib/integrations/codex-runner.js +128 -0
  30. package/lib/integrations/github-actions.js +248 -0
  31. package/lib/integrations/github-reporter.js +229 -0
  32. package/lib/integrations/screenshot-store.js +102 -0
  33. package/lib/openclaw/bridge.js +650 -0
  34. package/lib/openclaw/generate-skill.js +235 -0
  35. package/lib/openclaw/openclaw.json +64 -0
  36. package/lib/orchestrator/autonomous-loop.js +429 -0
  37. package/lib/orchestrator/thread-triggers.js +63 -0
  38. package/lib/roleplay/agent-messenger.js +75 -0
  39. package/lib/roleplay/discussion-threads.js +303 -0
  40. package/lib/roleplay/health-monitor.js +121 -0
  41. package/lib/roleplay/pm-agent.js +513 -0
  42. package/lib/roleplay/roleplay-config.js +25 -0
  43. package/lib/roleplay/room.js +164 -0
  44. package/lib/shared/action-runner.js +2330 -0
  45. package/lib/shared/event-bus.js +185 -0
  46. package/lib/slack/bot.js +378 -0
  47. package/lib/telegram/bot.js +416 -0
  48. package/lib/telegram/commands.js +1267 -0
  49. package/lib/telegram/keyboards.js +113 -0
  50. package/lib/telegram/notifications.js +247 -0
  51. package/lib/twitch/bot.js +354 -0
  52. package/lib/twitch/commands.js +302 -0
  53. package/lib/twitch/notifications.js +63 -0
  54. package/lib/utils/achievements.js +191 -0
  55. package/lib/utils/activity-log.js +182 -0
  56. package/lib/utils/agent-leaderboard.js +119 -0
  57. package/lib/utils/audit-logger.js +232 -0
  58. package/lib/utils/codebase-context.js +288 -0
  59. package/lib/utils/codebase-indexer.js +381 -0
  60. package/lib/utils/config-schema.js +230 -0
  61. package/lib/utils/context-compressor.js +172 -0
  62. package/lib/utils/correlation.js +63 -0
  63. package/lib/utils/cost-tracker.js +423 -0
  64. package/lib/utils/cron-scheduler.js +53 -0
  65. package/lib/utils/db-adapter.js +293 -0
  66. package/lib/utils/display.js +272 -0
  67. package/lib/utils/errors.js +116 -0
  68. package/lib/utils/format.js +134 -0
  69. package/lib/utils/intent-engine.js +464 -0
  70. package/lib/utils/mcp-client.js +238 -0
  71. package/lib/utils/model-ab-test.js +164 -0
  72. package/lib/utils/notify.js +122 -0
  73. package/lib/utils/persona-loader.js +80 -0
  74. package/lib/utils/pipeline-lock.js +73 -0
  75. package/lib/utils/pipeline.js +214 -0
  76. package/lib/utils/plugin-runner.js +234 -0
  77. package/lib/utils/rate-limiter.js +84 -0
  78. package/lib/utils/rbac.js +74 -0
  79. package/lib/utils/runner.js +1809 -0
  80. package/lib/utils/security.js +191 -0
  81. package/lib/utils/self-healer.js +144 -0
  82. package/lib/utils/skill-loader.js +255 -0
  83. package/lib/utils/spinner.js +132 -0
  84. package/lib/utils/stage-queue.js +50 -0
  85. package/lib/utils/state-machine.js +89 -0
  86. package/lib/utils/status-bar.js +327 -0
  87. package/lib/utils/token-estimator.js +101 -0
  88. package/lib/utils/ux-analyzer.js +101 -0
  89. package/lib/utils/webhook-emitter.js +83 -0
  90. package/lib/web/public/css/styles.css +417 -0
  91. package/lib/web/public/dark-mode.js +44 -0
  92. package/lib/web/public/hub/kanban.html +206 -0
  93. package/lib/web/public/index.html +45 -0
  94. package/lib/web/public/js/app.js +71 -0
  95. package/lib/web/public/js/ask.js +110 -0
  96. package/lib/web/public/js/dashboard.js +165 -0
  97. package/lib/web/public/js/deploy.js +72 -0
  98. package/lib/web/public/js/feature.js +79 -0
  99. package/lib/web/public/js/health.js +65 -0
  100. package/lib/web/public/js/logs.js +93 -0
  101. package/lib/web/public/js/review.js +123 -0
  102. package/lib/web/public/js/ws-client.js +82 -0
  103. package/lib/web/public/office/css/office.css +678 -0
  104. package/lib/web/public/office/index.html +148 -0
  105. package/lib/web/public/office/js/achievements-ui.js +117 -0
  106. package/lib/web/public/office/js/character.js +1056 -0
  107. package/lib/web/public/office/js/chat-bubbles.js +177 -0
  108. package/lib/web/public/office/js/cost-overlay.js +123 -0
  109. package/lib/web/public/office/js/day-night.js +68 -0
  110. package/lib/web/public/office/js/effects.js +632 -0
  111. package/lib/web/public/office/js/engine.js +146 -0
  112. package/lib/web/public/office/js/feature-ticket.js +216 -0
  113. package/lib/web/public/office/js/hub-client.js +60 -0
  114. package/lib/web/public/office/js/main.js +1757 -0
  115. package/lib/web/public/office/js/office-layout.js +1524 -0
  116. package/lib/web/public/office/js/pathfinding.js +144 -0
  117. package/lib/web/public/office/js/pixel-sprites.js +1454 -0
  118. package/lib/web/public/office/js/progress-bars.js +117 -0
  119. package/lib/web/public/office/js/replay.js +191 -0
  120. package/lib/web/public/office/js/sound-effects.js +91 -0
  121. package/lib/web/public/office/js/sprite-renderer.js +211 -0
  122. package/lib/web/public/office/js/stamina-system.js +89 -0
  123. package/lib/web/public/office/js/ui.js +107 -0
  124. package/lib/web/public/onboarding/index.html +243 -0
  125. package/lib/web/public/timeline/index.html +195 -0
  126. package/lib/web/routes/api.js +499 -0
  127. package/lib/web/routes/logs.js +20 -0
  128. package/lib/web/routes/metrics.js +99 -0
  129. package/lib/web/server.js +183 -0
  130. package/lib/web/ws/handler.js +65 -0
  131. package/package.json +67 -0
  132. package/templates/agent-architect.md +69 -0
  133. package/templates/agent-gemini-pm.md +49 -0
  134. package/templates/agent-gemini-reviewer.md +52 -0
  135. package/templates/copilot-instructions.md +36 -0
  136. package/templates/pipelines/mobile.json +27 -0
  137. package/templates/pipelines/nodejs-api.json +27 -0
  138. package/templates/pipelines/python.json +27 -0
  139. package/templates/pipelines/react.json +27 -0
  140. package/templates/pipelines/salesforce.json +27 -0
  141. package/templates/role-gemini.md +97 -0
  142. package/templates/skill-architect.md +114 -0
  143. package/templates/skill-browser-qa.md +50 -0
  144. package/templates/skill-bug-from-qa.md +58 -0
  145. package/templates/skill-chatbot.md +93 -0
  146. package/templates/skill-implement.md +78 -0
  147. package/templates/skill-openclaw.md +174 -0
  148. package/templates/skill-payment.md +110 -0
  149. package/templates/skill-pm-spec.md +77 -0
  150. package/templates/skill-requirement-capture.md +97 -0
  151. package/templates/skill-review.md +108 -0
  152. package/templates/skill-reviewer-qa.md +44 -0
  153. package/templates/skill-suggestion.md +45 -0
  154. package/templates/skill-template.md +142 -0
@@ -0,0 +1,117 @@
1
+ export class ProgressBarSystem {
2
+ constructor() {
3
+ this.bars = new Map();
4
+ }
5
+
6
+ setProgress(stageId, progress, label) {
7
+ const colors = {
8
+ spec: '#4A90D9',
9
+ architect: '#27AE60',
10
+ implement: '#F39C12',
11
+ review: '#9B59B6',
12
+ deploy: '#E74C3C',
13
+ };
14
+ this.bars.set(stageId, {
15
+ progress: Math.max(0, Math.min(1, progress)),
16
+ label: label || stageId,
17
+ color: colors[stageId] || '#666',
18
+ startTime: this.bars.get(stageId)?.startTime || Date.now(),
19
+ });
20
+ }
21
+
22
+ clearProgress(stageId) {
23
+ this.bars.delete(stageId);
24
+ }
25
+
26
+ clearAll() {
27
+ this.bars.clear();
28
+ }
29
+
30
+ render(ctx, x, y, width) {
31
+ const barHeight = 20;
32
+ const gap = 6;
33
+ let offsetY = 0;
34
+
35
+ ctx.fillStyle = '#333';
36
+ ctx.font = 'bold 14px system-ui, sans-serif';
37
+ ctx.fillText('Pipeline Progress', x, y + 14);
38
+ offsetY += 24;
39
+
40
+ if (this.bars.size === 0) {
41
+ ctx.fillStyle = '#999';
42
+ ctx.font = '12px system-ui, sans-serif';
43
+ ctx.fillText('No active pipeline', x, y + offsetY + 12);
44
+ return;
45
+ }
46
+
47
+ for (const [id, bar] of this.bars) {
48
+ const barY = y + offsetY;
49
+
50
+ // Background
51
+ ctx.fillStyle = '#e0e0e0';
52
+ this._roundRect(ctx, x, barY, width, barHeight, 4);
53
+ ctx.fill();
54
+
55
+ // Progress fill
56
+ const fillWidth = width * bar.progress;
57
+ if (fillWidth > 0) {
58
+ ctx.fillStyle = bar.color;
59
+ this._roundRect(ctx, x, barY, Math.max(8, fillWidth), barHeight, 4);
60
+ ctx.fill();
61
+ }
62
+
63
+ // Label
64
+ ctx.fillStyle = bar.progress > 0.5 ? '#fff' : '#333';
65
+ ctx.font = '11px system-ui, sans-serif';
66
+ const labelText = `${bar.label} ${Math.round(bar.progress * 100)}%`;
67
+ ctx.fillText(labelText, x + 6, barY + 14);
68
+
69
+ // Elapsed time
70
+ const elapsed = Math.round((Date.now() - bar.startTime) / 1000);
71
+ const timeText =
72
+ elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m${elapsed % 60}s`;
73
+ const timeWidth = ctx.measureText(timeText).width;
74
+ ctx.fillStyle = bar.progress > 0.8 ? '#fff' : '#999';
75
+ ctx.fillText(timeText, x + width - timeWidth - 6, barY + 14);
76
+
77
+ offsetY += barHeight + gap;
78
+ }
79
+ }
80
+
81
+ _roundRect(ctx, x, y, w, h, r) {
82
+ ctx.beginPath();
83
+ ctx.moveTo(x + r, y);
84
+ ctx.lineTo(x + w - r, y);
85
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
86
+ ctx.lineTo(x + w, y + h - r);
87
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
88
+ ctx.lineTo(x + r, y + h);
89
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
90
+ ctx.lineTo(x, y + r);
91
+ ctx.quadraticCurveTo(x, y, x + r, y);
92
+ ctx.closePath();
93
+ }
94
+
95
+ handlePipelineEvent(event) {
96
+ const stageMap = {
97
+ 'spec-generation': 'spec',
98
+ architecture: 'architect',
99
+ implementation: 'implement',
100
+ 'code-review': 'review',
101
+ deployment: 'deploy',
102
+ };
103
+
104
+ if (event.type === 'stage-start') {
105
+ const stage = stageMap[event.stage] || event.stage;
106
+ this.setProgress(stage, 0.1, event.stage);
107
+ } else if (event.type === 'stage-progress') {
108
+ const stage = stageMap[event.stage] || event.stage;
109
+ this.setProgress(stage, event.progress, event.stage);
110
+ } else if (event.type === 'stage-complete') {
111
+ const stage = stageMap[event.stage] || event.stage;
112
+ this.setProgress(stage, 1.0, event.stage);
113
+ } else if (event.type === 'pipeline-complete') {
114
+ setTimeout(() => this.clearAll(), 3000);
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,191 @@
1
+ export class PipelineReplay {
2
+ constructor(engine) {
3
+ this.engine = engine;
4
+ this.events = [];
5
+ this.playing = false;
6
+ this.paused = false;
7
+ this.speed = 10;
8
+ this.currentIndex = 0;
9
+ this.timer = null;
10
+ this.panel = null;
11
+ }
12
+
13
+ async loadEvents(featureId) {
14
+ try {
15
+ const res = await fetch(`/api/replay/${featureId}`);
16
+ if (!res.ok) throw new Error('Failed to load replay data');
17
+ const data = await res.json();
18
+ this.events = data.events || [];
19
+ return this.events.length;
20
+ } catch (e) {
21
+ console.error('Replay load error:', e);
22
+ return 0;
23
+ }
24
+ }
25
+
26
+ play() {
27
+ if (this.events.length === 0) return;
28
+ this.playing = true;
29
+ this.paused = false;
30
+ this.currentIndex = 0;
31
+ this._playNext();
32
+ }
33
+
34
+ pause() {
35
+ this.paused = !this.paused;
36
+ if (!this.paused && this.playing) this._playNext();
37
+ }
38
+
39
+ stop() {
40
+ this.playing = false;
41
+ this.paused = false;
42
+ if (this.timer) clearTimeout(this.timer);
43
+ this.timer = null;
44
+ this.currentIndex = 0;
45
+ }
46
+
47
+ setSpeed(speed) {
48
+ this.speed = Math.max(1, Math.min(50, speed));
49
+ }
50
+
51
+ _playNext() {
52
+ if (!this.playing || this.paused) return;
53
+ if (this.currentIndex >= this.events.length) {
54
+ this.stop();
55
+ this._onComplete();
56
+ return;
57
+ }
58
+
59
+ const event = this.events[this.currentIndex];
60
+ this._applyEvent(event);
61
+ this.currentIndex++;
62
+
63
+ if (this.currentIndex < this.events.length) {
64
+ const nextEvent = this.events[this.currentIndex];
65
+ const delay = Math.min(
66
+ (nextEvent.timestamp - event.timestamp) / this.speed,
67
+ 2000
68
+ );
69
+ this.timer = setTimeout(() => this._playNext(), Math.max(50, delay));
70
+ } else {
71
+ this.timer = setTimeout(() => {
72
+ this.stop();
73
+ this._onComplete();
74
+ }, 1000);
75
+ }
76
+ }
77
+
78
+ _applyEvent(event) {
79
+ const stageMap = {
80
+ 'spec-start': 'spec',
81
+ 'spec-complete': 'spec_complete',
82
+ 'arch-start': 'arch',
83
+ 'arch-complete': 'arch_complete',
84
+ 'impl-start': 'impl',
85
+ 'impl-complete': 'impl_complete',
86
+ 'review-start': 'review',
87
+ 'review-complete': 'review_complete',
88
+ 'deploy-start': 'deploy',
89
+ 'deploy-complete': 'deployed',
90
+ 'pipeline-start': 'spec',
91
+ 'pipeline-complete': 'idle',
92
+ };
93
+
94
+ if (event.stage && stageMap[event.stage]) {
95
+ this.engine.updateState({ stage: stageMap[event.stage] });
96
+ }
97
+
98
+ if (event.message && this.engine.chatBubbles) {
99
+ const agentMap = { spec: 'pm', arch: 'architect', impl: 'coder', review: 'pm', deploy: 'deployer' };
100
+ const agentRole = agentMap[event.stage?.split('-')[0]] || 'pm';
101
+ this.engine.chatBubbles.addBubble(agentRole, event.message, event.type === 'error' ? 'error' : 'normal');
102
+ }
103
+
104
+ if (event.progress !== undefined && this.engine.progressBars) {
105
+ this.engine.progressBars.setProgress(event.stage?.split('-')[0] || 'spec', event.progress);
106
+ }
107
+ }
108
+
109
+ _onComplete() {
110
+ if (this.onComplete) this.onComplete();
111
+ }
112
+
113
+ getProgress() {
114
+ if (this.events.length === 0) return 0;
115
+ return this.currentIndex / this.events.length;
116
+ }
117
+
118
+ createControls(container) {
119
+ this.panel = document.createElement('div');
120
+ this.panel.style.cssText = `
121
+ position: absolute; bottom: 60px; left: 50%; transform: translateX(-50%);
122
+ background: rgba(15, 23, 42, 0.95); border: 1px solid #334155;
123
+ border-radius: 12px; padding: 12px 20px; color: #e2e8f0;
124
+ font-family: system-ui; font-size: 12px; display: none; z-index: 100;
125
+ backdrop-filter: blur(8px); display: flex; align-items: center; gap: 12px;
126
+ `;
127
+ this.panel.innerHTML = `
128
+ <button id="replay-play" style="background:#3b82f6;border:none;color:white;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:14px">▶ Play</button>
129
+ <button id="replay-pause" style="background:#334155;border:none;color:white;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:14px" disabled>⏸</button>
130
+ <button id="replay-stop" style="background:#334155;border:none;color:white;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:14px" disabled>⏹</button>
131
+ <div style="display:flex;align-items:center;gap:4px">
132
+ <span>Speed:</span>
133
+ <select id="replay-speed" style="background:#1e293b;border:1px solid #475569;color:#e2e8f0;padding:4px;border-radius:4px;font-size:11px">
134
+ <option value="1">1x</option>
135
+ <option value="5">5x</option>
136
+ <option value="10" selected>10x</option>
137
+ <option value="20">20x</option>
138
+ <option value="50">50x</option>
139
+ </select>
140
+ </div>
141
+ <div style="flex:1;background:#1e293b;height:4px;border-radius:2px;min-width:100px">
142
+ <div id="replay-progress" style="background:#3b82f6;height:100%;width:0%;border-radius:2px;transition:width 0.2s"></div>
143
+ </div>
144
+ <span id="replay-status" style="color:#94a3b8;font-size:11px">Ready</span>
145
+ `;
146
+ this.panel.style.display = 'none';
147
+ container.appendChild(this.panel);
148
+
149
+ this.panel.querySelector('#replay-play').onclick = () => {
150
+ this.play();
151
+ this._updateControls(true);
152
+ };
153
+ this.panel.querySelector('#replay-pause').onclick = () => {
154
+ this.pause();
155
+ this._updateControls(this.playing);
156
+ };
157
+ this.panel.querySelector('#replay-stop').onclick = () => {
158
+ this.stop();
159
+ this._updateControls(false);
160
+ };
161
+ this.panel.querySelector('#replay-speed').onchange = (e) => {
162
+ this.setSpeed(parseInt(e.target.value));
163
+ };
164
+
165
+ this._progressInterval = setInterval(() => {
166
+ if (this.playing) {
167
+ const pct = Math.round(this.getProgress() * 100);
168
+ const bar = this.panel.querySelector('#replay-progress');
169
+ if (bar) bar.style.width = pct + '%';
170
+ const status = this.panel.querySelector('#replay-status');
171
+ if (status) status.textContent = `${this.currentIndex}/${this.events.length}`;
172
+ }
173
+ }, 200);
174
+
175
+ this.onComplete = () => {
176
+ this._updateControls(false);
177
+ const status = this.panel.querySelector('#replay-status');
178
+ if (status) status.textContent = 'Complete';
179
+ };
180
+ }
181
+
182
+ _updateControls(isPlaying) {
183
+ if (!this.panel) return;
184
+ this.panel.querySelector('#replay-pause').disabled = !isPlaying;
185
+ this.panel.querySelector('#replay-stop').disabled = !isPlaying;
186
+ }
187
+
188
+ show() { if (this.panel) this.panel.style.display = 'flex'; }
189
+ hide() { if (this.panel) this.panel.style.display = 'none'; this.stop(); }
190
+ toggle() { if (this.panel?.style.display === 'none') this.show(); else this.hide(); }
191
+ }
@@ -0,0 +1,91 @@
1
+ export class SoundEffects {
2
+ constructor() {
3
+ this.enabled = false;
4
+ this.volume = 0.3;
5
+ this.ctx = null;
6
+ this._ambientOsc = null;
7
+ this._ambientGain = null;
8
+ }
9
+
10
+ init() {
11
+ this.ctx = new (window.AudioContext || window.webkitAudioContext)();
12
+ }
13
+
14
+ enable() {
15
+ this.enabled = true;
16
+ if (!this.ctx) this.init();
17
+ }
18
+
19
+ disable() {
20
+ this.enabled = false;
21
+ }
22
+
23
+ setVolume(v) {
24
+ this.volume = Math.max(0, Math.min(1, v));
25
+ }
26
+
27
+ _playTone(frequency, duration, type = 'sine') {
28
+ if (!this.enabled || !this.ctx) return;
29
+ const osc = this.ctx.createOscillator();
30
+ const gain = this.ctx.createGain();
31
+ osc.type = type;
32
+ osc.frequency.value = frequency;
33
+ gain.gain.value = this.volume;
34
+ gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
35
+ osc.connect(gain);
36
+ gain.connect(this.ctx.destination);
37
+ osc.start();
38
+ osc.stop(this.ctx.currentTime + duration);
39
+ }
40
+
41
+ playTaskStart() {
42
+ this._playTone(440, 0.15);
43
+ setTimeout(() => this._playTone(554, 0.15), 100);
44
+ }
45
+
46
+ playTaskComplete() {
47
+ this._playTone(523, 0.1);
48
+ setTimeout(() => this._playTone(659, 0.1), 100);
49
+ setTimeout(() => this._playTone(784, 0.2), 200);
50
+ }
51
+
52
+ playError() {
53
+ this._playTone(200, 0.3, 'sawtooth');
54
+ }
55
+
56
+ playNotification() {
57
+ this._playTone(880, 0.1);
58
+ setTimeout(() => this._playTone(880, 0.1), 150);
59
+ }
60
+
61
+ playAchievement() {
62
+ [523, 659, 784, 1047].forEach((f, i) =>
63
+ setTimeout(() => this._playTone(f, 0.2), i * 100)
64
+ );
65
+ }
66
+
67
+ playTyping() {
68
+ this._playTone(800 + Math.random() * 400, 0.05, 'square');
69
+ }
70
+
71
+ playAmbient() {
72
+ if (!this.enabled || !this.ctx) return;
73
+ const osc = this.ctx.createOscillator();
74
+ const gain = this.ctx.createGain();
75
+ osc.type = 'sine';
76
+ osc.frequency.value = 100;
77
+ gain.gain.value = this.volume * 0.05;
78
+ osc.connect(gain);
79
+ gain.connect(this.ctx.destination);
80
+ osc.start();
81
+ this._ambientOsc = osc;
82
+ this._ambientGain = gain;
83
+ }
84
+
85
+ stopAmbient() {
86
+ if (this._ambientOsc) {
87
+ this._ambientOsc.stop();
88
+ this._ambientOsc = null;
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Sprite Renderer — draws pixel art from data arrays onto canvas
3
+ * Each sprite is a 16×16 grid of color values (null = transparent)
4
+ */
5
+
6
+ const PIXEL_SIZE = 2; // Each logical pixel = 2×2 canvas pixels
7
+
8
+ /**
9
+ * Render a pixel sprite onto the canvas
10
+ * @param {CanvasRenderingContext2D} ctx
11
+ * @param {Array<Array<string|null>>} pixelData - 16×16 array of hex colors
12
+ * @param {number} x - world X position (center)
13
+ * @param {number} y - world Y position (bottom)
14
+ * @param {object} opts
15
+ * @param {boolean} opts.flipX - mirror horizontally
16
+ * @param {number} opts.offsetY - vertical offset (for bob/jump)
17
+ * @param {number} opts.scale - additional scale factor
18
+ * @param {number} opts.opacity - 0-1 opacity
19
+ */
20
+ export function renderSprite(ctx, pixelData, x, y, opts = {}) {
21
+ const { flipX = false, offsetY = 0, scale = 1, opacity = 1 } = opts;
22
+ if (!pixelData || pixelData.length === 0) return;
23
+
24
+ const rows = pixelData.length;
25
+ const cols = pixelData[0].length;
26
+ const pw = PIXEL_SIZE * scale;
27
+ const spriteW = cols * pw;
28
+ const spriteH = rows * pw;
29
+
30
+ // Position: x,y is center-bottom of sprite
31
+ const drawX = x - spriteW / 2;
32
+ const drawY = y - spriteH + offsetY;
33
+
34
+ ctx.save();
35
+ if (opacity < 1) ctx.globalAlpha = opacity;
36
+
37
+ if (flipX) {
38
+ ctx.translate(drawX + spriteW, drawY);
39
+ ctx.scale(-1, 1);
40
+ } else {
41
+ ctx.translate(drawX, drawY);
42
+ }
43
+
44
+ for (let row = 0; row < rows; row++) {
45
+ for (let col = 0; col < cols; col++) {
46
+ const color = pixelData[row][col];
47
+ if (!color) continue;
48
+ ctx.fillStyle = color;
49
+ ctx.fillRect(col * pw, row * pw, pw, pw);
50
+ }
51
+ }
52
+
53
+ ctx.restore();
54
+ }
55
+
56
+ /**
57
+ * Render a small pixel text string (3×5 pixel font)
58
+ * Used for labels, status text above characters
59
+ */
60
+ const PIXEL_FONT = {
61
+ 'A': ['010','101','111','101','101'],
62
+ 'B': ['110','101','110','101','110'],
63
+ 'C': ['011','100','100','100','011'],
64
+ 'D': ['110','101','101','101','110'],
65
+ 'E': ['111','100','110','100','111'],
66
+ 'F': ['111','100','110','100','100'],
67
+ 'G': ['011','100','101','101','011'],
68
+ 'H': ['101','101','111','101','101'],
69
+ 'I': ['111','010','010','010','111'],
70
+ 'J': ['001','001','001','101','010'],
71
+ 'K': ['101','110','100','110','101'],
72
+ 'L': ['100','100','100','100','111'],
73
+ 'M': ['101','111','111','101','101'],
74
+ 'N': ['101','111','111','111','101'],
75
+ 'O': ['010','101','101','101','010'],
76
+ 'P': ['110','101','110','100','100'],
77
+ 'Q': ['010','101','101','011','001'],
78
+ 'R': ['110','101','110','101','101'],
79
+ 'S': ['011','100','010','001','110'],
80
+ 'T': ['111','010','010','010','010'],
81
+ 'U': ['101','101','101','101','010'],
82
+ 'V': ['101','101','101','010','010'],
83
+ 'W': ['101','101','111','111','101'],
84
+ 'X': ['101','101','010','101','101'],
85
+ 'Y': ['101','101','010','010','010'],
86
+ 'Z': ['111','001','010','100','111'],
87
+ '0': ['010','101','101','101','010'],
88
+ '1': ['010','110','010','010','111'],
89
+ '2': ['110','001','010','100','111'],
90
+ '3': ['110','001','010','001','110'],
91
+ '4': ['101','101','111','001','001'],
92
+ '5': ['111','100','110','001','110'],
93
+ '6': ['011','100','110','101','010'],
94
+ '7': ['111','001','010','010','010'],
95
+ '8': ['010','101','010','101','010'],
96
+ '9': ['010','101','011','001','110'],
97
+ ' ': ['000','000','000','000','000'],
98
+ '.': ['000','000','000','000','010'],
99
+ '!': ['010','010','010','000','010'],
100
+ '?': ['110','001','010','000','010'],
101
+ ':': ['000','010','000','010','000'],
102
+ '-': ['000','000','111','000','000'],
103
+ '/': ['001','001','010','100','100'],
104
+ "'": ['010','010','000','000','000'],
105
+ ',': ['000','000','000','010','100'],
106
+ '(': ['001','010','010','010','001'],
107
+ ')': ['100','010','010','010','100'],
108
+ '+': ['000','010','111','010','000'],
109
+ '=': ['000','111','000','111','000'],
110
+ '#': ['101','111','101','111','101'],
111
+ '_': ['000','000','000','000','111'],
112
+ };
113
+
114
+ /**
115
+ * Render pixel text
116
+ * @param {CanvasRenderingContext2D} ctx
117
+ * @param {string} text
118
+ * @param {number} x - center X
119
+ * @param {number} y - top Y
120
+ * @param {string} color
121
+ * @param {number} pixelScale - size of each font pixel
122
+ */
123
+ export function renderPixelText(ctx, text, x, y, color = '#fff', pixelScale = 1) {
124
+ const chars = text.toUpperCase().split('');
125
+ const charWidth = 4 * pixelScale; // 3px char + 1px gap
126
+ // Only count renderable characters for centering
127
+ const renderableCount = chars.filter(ch => PIXEL_FONT[ch]).length;
128
+ const totalWidth = renderableCount * charWidth - pixelScale;
129
+ let cx = x - totalWidth / 2;
130
+
131
+ ctx.fillStyle = color;
132
+ for (const ch of chars) {
133
+ const glyph = PIXEL_FONT[ch];
134
+ if (!glyph) { cx += charWidth; continue; }
135
+ for (let row = 0; row < 5; row++) {
136
+ for (let col = 0; col < 3; col++) {
137
+ if (glyph[row][col] === '1') {
138
+ ctx.fillRect(
139
+ cx + col * pixelScale,
140
+ y + row * pixelScale,
141
+ pixelScale,
142
+ pixelScale
143
+ );
144
+ }
145
+ }
146
+ }
147
+ cx += charWidth;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Render a pixel art speech/thought bubble
153
+ */
154
+ export function renderBubble(ctx, x, y, width, height, type = 'speech') {
155
+ const bx = x - width / 2;
156
+ const by = y - height;
157
+ const p = PIXEL_SIZE;
158
+
159
+ // Bubble body
160
+ ctx.fillStyle = '#fff';
161
+ ctx.fillRect(bx + p, by, width - 2 * p, height);
162
+ ctx.fillRect(bx, by + p, width, height - 2 * p);
163
+
164
+ // Border
165
+ ctx.fillStyle = '#333';
166
+ ctx.fillRect(bx + p, by - 1, width - 2 * p, 1);
167
+ ctx.fillRect(bx + p, by + height, width - 2 * p, 1);
168
+ ctx.fillRect(bx - 1, by + p, 1, height - 2 * p);
169
+ ctx.fillRect(bx + width, by + p, 1, height - 2 * p);
170
+
171
+ // Tail
172
+ if (type === 'speech') {
173
+ ctx.fillStyle = '#fff';
174
+ ctx.fillRect(x - p, by + height, p, p * 2);
175
+ ctx.fillRect(x, by + height + p * 2, p, p);
176
+ ctx.fillStyle = '#333';
177
+ ctx.fillRect(x - p - 1, by + height, 1, p * 2);
178
+ ctx.fillRect(x, by + height + p * 2, p, 1);
179
+ } else {
180
+ // Thought dots
181
+ ctx.fillStyle = '#fff';
182
+ ctx.fillRect(x - p, by + height + 2, p, p);
183
+ ctx.fillRect(x + p, by + height + p + 4, p - 1, p - 1);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Create an offscreen canvas with a pre-rendered sprite for performance
189
+ */
190
+ export function cacheSprite(pixelData, scale = 1) {
191
+ if (!pixelData || pixelData.length === 0) return null;
192
+ const rows = pixelData.length;
193
+ const cols = pixelData[0].length;
194
+ const pw = PIXEL_SIZE * scale;
195
+ const canvas = document.createElement('canvas');
196
+ canvas.width = cols * pw;
197
+ canvas.height = rows * pw;
198
+ const ctx = canvas.getContext('2d');
199
+
200
+ for (let row = 0; row < rows; row++) {
201
+ for (let col = 0; col < cols; col++) {
202
+ const color = pixelData[row][col];
203
+ if (!color) continue;
204
+ ctx.fillStyle = color;
205
+ ctx.fillRect(col * pw, row * pw, pw, pw);
206
+ }
207
+ }
208
+ return canvas;
209
+ }
210
+
211
+ export { PIXEL_SIZE };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Stamina / Fatigue System — characters get tired while working
3
+ * and need breaks at the break room or gym to recharge.
4
+ */
5
+
6
+ /**
7
+ * Initialize stamina properties on a Character instance.
8
+ */
9
+ export function initStamina(character) {
10
+ character.stamina = 100;
11
+ character.maxStamina = 100;
12
+ character.staminaDrainRate = 1.2; // per second while working
13
+ character.staminaRegenRate = 4.0; // per second while resting
14
+ character.fatigueThreshold = 20;
15
+ character.restTarget = 80;
16
+ character.isFatigued = false;
17
+ character._preBreakState = null;
18
+ }
19
+
20
+ /**
21
+ * Per-frame stamina update.
22
+ * @returns {'ok' | 'needs_break' | 'resting' | 'recovered'}
23
+ */
24
+ export function updateStamina(char, dt, breakPositions) {
25
+ if (!('stamina' in char)) return 'ok';
26
+
27
+ // Working: drain stamina
28
+ if (char.isWorking && !char.isFatigued) {
29
+ char.stamina = Math.max(0, char.stamina - char.staminaDrainRate * dt);
30
+
31
+ if (char.stamina <= char.fatigueThreshold) {
32
+ char.isFatigued = true;
33
+ char._preBreakState = char.state;
34
+
35
+ // Pick gym if very tired, otherwise break room
36
+ const dest = char.stamina < 10 ? breakPositions.gym : breakPositions.breakRoom;
37
+ if (dest) {
38
+ char.moveTo(dest.x, dest.y);
39
+ char.state = 'coffee_break';
40
+ char.idleBehavior = char.stamina < 10 ? 'gym' : 'breakRoom';
41
+ }
42
+ return 'needs_break';
43
+ }
44
+ return 'ok';
45
+ }
46
+
47
+ // Resting: regenerate
48
+ if (char.isFatigued) {
49
+ char.stamina = Math.min(char.maxStamina, char.stamina + char.staminaRegenRate * dt);
50
+
51
+ if (char.stamina >= char.restTarget) {
52
+ char.isFatigued = false;
53
+ char.idleBehavior = null;
54
+ char.moveTo(char.homeX, char.homeY);
55
+ char.state = char._preBreakState || 'idle';
56
+ char._preBreakState = null;
57
+ return 'recovered';
58
+ }
59
+ return 'resting';
60
+ }
61
+
62
+ // Idle: slow passive regen
63
+ if (!char.isWorking) {
64
+ char.stamina = Math.min(char.maxStamina, char.stamina + 0.5 * dt);
65
+ }
66
+
67
+ return 'ok';
68
+ }
69
+
70
+ /**
71
+ * Draw stamina bar below progress bar.
72
+ */
73
+ export function drawStaminaBar(ctx, char, yAboveSprite) {
74
+ if (!('stamina' in char)) return;
75
+ if (char.stamina > 60 && !char.isFatigued) return; // don't clutter
76
+
77
+ const width = 22;
78
+ const height = 2;
79
+ const x = char.x - width / 2;
80
+ const y = yAboveSprite;
81
+
82
+ ctx.fillStyle = '#1a1a2e';
83
+ ctx.fillRect(x - 1, y - 1, width + 2, height + 2);
84
+
85
+ const p = char.stamina / char.maxStamina;
86
+ const fillW = Math.floor(width * p);
87
+ ctx.fillStyle = p < 0.2 ? '#d04040' : p < 0.5 ? '#d8c020' : '#40b8d0';
88
+ ctx.fillRect(x, y, fillW, height);
89
+ }