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,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
|
+
}
|