@kernel.chat/kbot 3.94.0 → 3.97.0
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/dist/agent.js +30 -0
- package/dist/coordinator.d.ts +132 -0
- package/dist/coordinator.js +682 -0
- package/dist/tools/audio-engine.d.ts +76 -0
- package/dist/tools/audio-engine.js +583 -24
- package/dist/tools/index.js +6 -0
- package/dist/tools/sprite-engine.d.ts +18 -0
- package/dist/tools/sprite-engine.js +435 -1
- package/dist/tools/stream-chat-ai.d.ts +56 -0
- package/dist/tools/stream-chat-ai.js +625 -0
- package/dist/tools/stream-commands.d.ts +91 -0
- package/dist/tools/stream-commands.js +911 -0
- package/dist/tools/stream-overlay.d.ts +53 -0
- package/dist/tools/stream-overlay.js +494 -0
- package/dist/tools/stream-renderer.js +676 -77
- package/dist/tools/stream-vod.d.ts +60 -0
- package/dist/tools/stream-vod.js +449 -0
- package/dist/tools/stream-weather.d.ts +79 -0
- package/dist/tools/stream-weather.js +811 -0
- package/dist/tools/tile-world.d.ts +6 -0
- package/dist/tools/tile-world.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type AlertType = 'follow' | 'raid' | 'sub' | 'donation' | 'achievement';
|
|
2
|
+
export interface StreamAlert {
|
|
3
|
+
type: AlertType;
|
|
4
|
+
username: string;
|
|
5
|
+
viewers?: number;
|
|
6
|
+
amount?: number;
|
|
7
|
+
title?: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GoalConfig {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
current: number;
|
|
14
|
+
target: number;
|
|
15
|
+
color?: string;
|
|
16
|
+
position?: 'top' | 'bottom';
|
|
17
|
+
}
|
|
18
|
+
export interface InfoBarData {
|
|
19
|
+
viewers: number;
|
|
20
|
+
uptime: string;
|
|
21
|
+
biome: string;
|
|
22
|
+
chatRate: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class StreamOverlay {
|
|
25
|
+
private alertQueue;
|
|
26
|
+
private activeAlert;
|
|
27
|
+
private goals;
|
|
28
|
+
private goalAnimations;
|
|
29
|
+
private tickerItems;
|
|
30
|
+
private tickerSpeed;
|
|
31
|
+
private highlight;
|
|
32
|
+
private infoBar;
|
|
33
|
+
queueAlert(alert: StreamAlert): void;
|
|
34
|
+
setGoal(goal: GoalConfig): void;
|
|
35
|
+
updateGoal(id: string, current: number): void;
|
|
36
|
+
addTicker(text: string): void;
|
|
37
|
+
highlightMessage(username: string, message: string, color?: string): void;
|
|
38
|
+
updateInfoBar(info: InfoBarData): void;
|
|
39
|
+
tick(_frame: number): void;
|
|
40
|
+
render(ctx: CanvasRenderingContext2D, width: number, height: number): void;
|
|
41
|
+
private tickAlerts;
|
|
42
|
+
private renderAlert;
|
|
43
|
+
private tickGoals;
|
|
44
|
+
private renderGoals;
|
|
45
|
+
private tickTicker;
|
|
46
|
+
private renderTicker;
|
|
47
|
+
private tickHighlight;
|
|
48
|
+
private renderHighlight;
|
|
49
|
+
private renderInfoBar;
|
|
50
|
+
}
|
|
51
|
+
export declare function getOverlay(): StreamOverlay;
|
|
52
|
+
export declare function registerOverlayTools(): void;
|
|
53
|
+
//# sourceMappingURL=stream-overlay.d.ts.map
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
// kbot Stream Overlay — Animated overlays for alerts, goals, notifications
|
|
2
|
+
//
|
|
3
|
+
// Renders on the stream canvas: alert popups (follow/raid/sub/donation/achievement),
|
|
4
|
+
// goal progress bars, scrolling ticker, chat highlights, and a persistent info bar.
|
|
5
|
+
// Imported by stream-renderer.ts — uses Canvas 2D API at 6fps.
|
|
6
|
+
//
|
|
7
|
+
// Tools: overlay_alert, overlay_goal, overlay_ticker, overlay_highlight
|
|
8
|
+
import { registerTool } from './index.js';
|
|
9
|
+
// ─── Constants ─────────────────────────────────────────────────
|
|
10
|
+
const FPS = 6;
|
|
11
|
+
const SEC = FPS;
|
|
12
|
+
const ALERT_HOLD = 3 * SEC;
|
|
13
|
+
const ALERT_ENTER = SEC;
|
|
14
|
+
const ALERT_EXIT = SEC;
|
|
15
|
+
const ALERT_TOTAL = ALERT_ENTER + ALERT_HOLD + ALERT_EXIT;
|
|
16
|
+
// ─── Palette ───────────────────────────────────────────────────
|
|
17
|
+
const C = {
|
|
18
|
+
bg: '#0d1117', bgPanel: '#161b22', accent: '#6B5B95',
|
|
19
|
+
green: '#3fb950', blue: '#58a6ff', orange: '#d29922',
|
|
20
|
+
red: '#f85149', purple: '#bc8cff', text: '#e6edf3',
|
|
21
|
+
textDim: '#8b949e', gold: '#ffd700', white: '#ffffff',
|
|
22
|
+
};
|
|
23
|
+
// ─── Helpers ───────────────────────────────────────────────────
|
|
24
|
+
function hexToRgba(hex, alpha) {
|
|
25
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
26
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
27
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
28
|
+
return `rgba(${r},${g},${b},${alpha})`;
|
|
29
|
+
}
|
|
30
|
+
function lerp(a, b, t) {
|
|
31
|
+
return a + (b - a) * Math.min(1, Math.max(0, t));
|
|
32
|
+
}
|
|
33
|
+
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
|
|
34
|
+
function easeIn(t) { return t * t * t; }
|
|
35
|
+
function rng(min, max) { return min + Math.random() * (max - min); }
|
|
36
|
+
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
|
|
37
|
+
function truncate(s, max) {
|
|
38
|
+
return s.length > max ? s.slice(0, max - 3) + '...' : s;
|
|
39
|
+
}
|
|
40
|
+
/** Spawn N particles with given config */
|
|
41
|
+
function spawnN(n, cfg) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (let i = 0; i < n; i++) {
|
|
44
|
+
const c = cfg(i);
|
|
45
|
+
const maxLife = Math.floor(rng(c.minLife, c.maxLife));
|
|
46
|
+
out.push({ x: c.x, y: c.y, vx: c.vx, vy: c.vy, life: maxLife, maxLife, color: c.color, size: c.size });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function spawnAlertParticles(type, cx, cy) {
|
|
51
|
+
if (type === 'raid') {
|
|
52
|
+
return spawnN(30, () => {
|
|
53
|
+
const a = Math.random() * Math.PI * 2, sp = rng(2, 8);
|
|
54
|
+
return { x: cx, y: cy, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, minLife: SEC, maxLife: 3 * SEC, color: pick([C.red, C.orange, C.gold, C.white]), size: rng(2, 5) };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (type === 'sub') {
|
|
58
|
+
return spawnN(25, () => ({
|
|
59
|
+
x: rng(cx - 200, cx + 200), y: cy - 40, vx: rng(-1, 1), vy: rng(1, 4),
|
|
60
|
+
minLife: 2 * SEC, maxLife: 4 * SEC, color: pick([C.accent, C.green, C.blue, C.purple, C.gold]), size: rng(3, 6),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
if (type === 'donation') {
|
|
64
|
+
return spawnN(20, () => ({
|
|
65
|
+
x: rng(cx - 150, cx + 150), y: cy - 60, vx: rng(-0.5, 0.5), vy: rng(1, 3),
|
|
66
|
+
minLife: 2 * SEC, maxLife: 3 * SEC, color: pick([C.gold, C.orange, '#ffed4a']), size: rng(3, 5),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
if (type === 'achievement') {
|
|
70
|
+
return spawnN(16, (i) => {
|
|
71
|
+
const a = (i / 16) * Math.PI * 2;
|
|
72
|
+
return { x: cx + Math.cos(a) * 60, y: cy + Math.sin(a) * 25, vx: Math.cos(a) * 0.5, vy: Math.sin(a) * 0.5 - 0.3, minLife: 2 * SEC, maxLife: 3 * SEC, color: C.gold, size: rng(2, 4) };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
function tickParticles(particles) {
|
|
78
|
+
return particles.filter(p => { p.x += p.vx; p.y += p.vy; p.vy += 0.15; return --p.life > 0; });
|
|
79
|
+
}
|
|
80
|
+
function drawParticles(ctx, particles) {
|
|
81
|
+
for (const p of particles) {
|
|
82
|
+
ctx.fillStyle = hexToRgba(p.color, Math.min(1, p.life / (p.maxLife * 0.3)));
|
|
83
|
+
ctx.fillRect(Math.round(p.x), Math.round(p.y), p.size, p.size);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ─── Alert Label Maps ──────────────────────────────────────────
|
|
87
|
+
const ALERT_TITLES = {
|
|
88
|
+
follow: 'NEW FOLLOWER', raid: 'RAID INCOMING', sub: 'NEW SUBSCRIBER',
|
|
89
|
+
donation: 'DONATION', achievement: 'ACHIEVEMENT UNLOCKED',
|
|
90
|
+
};
|
|
91
|
+
const ALERT_COLORS = {
|
|
92
|
+
follow: C.green, raid: C.red, sub: C.accent, donation: C.gold, achievement: C.gold,
|
|
93
|
+
};
|
|
94
|
+
function alertBody(a) {
|
|
95
|
+
switch (a.type) {
|
|
96
|
+
case 'follow': return `${a.username} just followed!`;
|
|
97
|
+
case 'raid': return `${a.username} raided with ${a.viewers ?? 0} viewers!`;
|
|
98
|
+
case 'sub': return `${a.username} subscribed!`;
|
|
99
|
+
case 'donation': return `${a.username} donated $${(a.amount ?? 0).toFixed(2)}!`;
|
|
100
|
+
case 'achievement': return a.title ?? 'Unknown Achievement';
|
|
101
|
+
default: return a.message ?? '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Draw L-shaped corner accent */
|
|
105
|
+
function drawCorner(ctx, x, y, dx, dy, len) {
|
|
106
|
+
ctx.beginPath();
|
|
107
|
+
ctx.moveTo(x, y + dy * len);
|
|
108
|
+
ctx.lineTo(x, y);
|
|
109
|
+
ctx.lineTo(x + dx * len, y);
|
|
110
|
+
ctx.stroke();
|
|
111
|
+
}
|
|
112
|
+
// ─── StreamOverlay Class ───────────────────────────────────────
|
|
113
|
+
export class StreamOverlay {
|
|
114
|
+
alertQueue = [];
|
|
115
|
+
activeAlert = null;
|
|
116
|
+
goals = new Map();
|
|
117
|
+
goalAnimations = new Map();
|
|
118
|
+
tickerItems = [];
|
|
119
|
+
tickerSpeed = 2;
|
|
120
|
+
highlight = null;
|
|
121
|
+
infoBar = { viewers: 0, uptime: '0:00', biome: 'default', chatRate: 0 };
|
|
122
|
+
queueAlert(alert) { this.alertQueue.push(alert); }
|
|
123
|
+
setGoal(goal) {
|
|
124
|
+
this.goals.set(goal.id, { ...goal });
|
|
125
|
+
if (!this.goalAnimations.has(goal.id))
|
|
126
|
+
this.goalAnimations.set(goal.id, 0);
|
|
127
|
+
}
|
|
128
|
+
updateGoal(id, current) {
|
|
129
|
+
const g = this.goals.get(id);
|
|
130
|
+
if (g)
|
|
131
|
+
g.current = current;
|
|
132
|
+
}
|
|
133
|
+
addTicker(text) { this.tickerItems.push({ text, x: -1 }); }
|
|
134
|
+
highlightMessage(username, message, color) {
|
|
135
|
+
this.highlight = { username, message, color: color ?? C.accent, frame: 0, totalFrames: 3 * SEC };
|
|
136
|
+
}
|
|
137
|
+
updateInfoBar(info) { this.infoBar = { ...info }; }
|
|
138
|
+
tick(_frame) {
|
|
139
|
+
this.tickAlerts();
|
|
140
|
+
this.tickGoals();
|
|
141
|
+
this.tickTicker();
|
|
142
|
+
this.tickHighlight();
|
|
143
|
+
}
|
|
144
|
+
render(ctx, width, height) {
|
|
145
|
+
this.renderGoals(ctx, width, height);
|
|
146
|
+
this.renderTicker(ctx, width, height);
|
|
147
|
+
this.renderInfoBar(ctx, width, height);
|
|
148
|
+
this.renderHighlight(ctx, width, height);
|
|
149
|
+
this.renderAlert(ctx, width, height);
|
|
150
|
+
}
|
|
151
|
+
// ── Alert ──────────────────────────────────────────────────
|
|
152
|
+
tickAlerts() {
|
|
153
|
+
if (!this.activeAlert && this.alertQueue.length > 0) {
|
|
154
|
+
this.activeAlert = { alert: this.alertQueue.shift(), frame: 0, particles: [] };
|
|
155
|
+
}
|
|
156
|
+
if (!this.activeAlert)
|
|
157
|
+
return;
|
|
158
|
+
const a = this.activeAlert;
|
|
159
|
+
a.frame++;
|
|
160
|
+
if (a.frame === ALERT_ENTER + 1 && a.particles.length === 0) {
|
|
161
|
+
a.particles = spawnAlertParticles(a.alert.type, 640, 200);
|
|
162
|
+
}
|
|
163
|
+
a.particles = tickParticles(a.particles);
|
|
164
|
+
if (a.frame >= ALERT_TOTAL && a.particles.length === 0)
|
|
165
|
+
this.activeAlert = null;
|
|
166
|
+
}
|
|
167
|
+
renderAlert(ctx, width, _height) {
|
|
168
|
+
if (!this.activeAlert)
|
|
169
|
+
return;
|
|
170
|
+
const a = this.activeAlert, frame = a.frame;
|
|
171
|
+
const color = ALERT_COLORS[a.alert.type] ?? C.blue;
|
|
172
|
+
let alpha = 1, offsetY = 0;
|
|
173
|
+
if (frame <= ALERT_ENTER) {
|
|
174
|
+
const t = easeOut(frame / ALERT_ENTER);
|
|
175
|
+
alpha = t;
|
|
176
|
+
offsetY = lerp(-50, 0, t);
|
|
177
|
+
}
|
|
178
|
+
else if (frame > ALERT_ENTER + ALERT_HOLD) {
|
|
179
|
+
const t = easeIn((frame - ALERT_ENTER - ALERT_HOLD) / ALERT_EXIT);
|
|
180
|
+
alpha = 1 - t;
|
|
181
|
+
offsetY = lerp(0, -30, t);
|
|
182
|
+
}
|
|
183
|
+
const boxW = 420, boxH = 80;
|
|
184
|
+
const boxX = Math.floor((width - boxW) / 2), boxY = 100 + offsetY;
|
|
185
|
+
ctx.save();
|
|
186
|
+
ctx.globalAlpha = alpha;
|
|
187
|
+
ctx.fillStyle = hexToRgba(C.bgPanel, 0.92);
|
|
188
|
+
ctx.fillRect(boxX, boxY, boxW, boxH);
|
|
189
|
+
ctx.strokeStyle = color;
|
|
190
|
+
ctx.lineWidth = 2;
|
|
191
|
+
ctx.strokeRect(boxX, boxY, boxW, boxH);
|
|
192
|
+
ctx.fillStyle = color;
|
|
193
|
+
ctx.fillRect(boxX, boxY, 4, boxH);
|
|
194
|
+
if (a.alert.type === 'achievement' || a.alert.type === 'donation') {
|
|
195
|
+
ctx.fillStyle = hexToRgba(C.gold, 0.15 + 0.1 * Math.sin(frame * 0.5));
|
|
196
|
+
ctx.fillRect(boxX - 3, boxY - 3, boxW + 6, boxH + 6);
|
|
197
|
+
}
|
|
198
|
+
ctx.fillStyle = color;
|
|
199
|
+
ctx.font = 'bold 14px "Courier Prime", monospace';
|
|
200
|
+
ctx.textAlign = 'center';
|
|
201
|
+
ctx.fillText(ALERT_TITLES[a.alert.type] ?? 'ALERT', boxX + boxW / 2, boxY + 24);
|
|
202
|
+
ctx.fillStyle = C.text;
|
|
203
|
+
ctx.font = '16px "Courier Prime", monospace';
|
|
204
|
+
ctx.fillText(truncate(alertBody(a.alert), 40), boxX + boxW / 2, boxY + 50);
|
|
205
|
+
if (a.alert.message && a.alert.type !== 'achievement') {
|
|
206
|
+
ctx.fillStyle = C.textDim;
|
|
207
|
+
ctx.font = '12px "Courier Prime", monospace';
|
|
208
|
+
ctx.fillText(truncate(a.alert.message, 50), boxX + boxW / 2, boxY + 68);
|
|
209
|
+
}
|
|
210
|
+
ctx.textAlign = 'left';
|
|
211
|
+
ctx.restore();
|
|
212
|
+
drawParticles(ctx, a.particles);
|
|
213
|
+
}
|
|
214
|
+
// ── Goals ──────────────────────────────────────────────────
|
|
215
|
+
tickGoals() {
|
|
216
|
+
for (const [id, goal] of this.goals) {
|
|
217
|
+
const target = Math.min(1, goal.current / goal.target);
|
|
218
|
+
this.goalAnimations.set(id, lerp(this.goalAnimations.get(id) ?? 0, target, 0.15));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
renderGoals(ctx, width, height) {
|
|
222
|
+
if (this.goals.size === 0)
|
|
223
|
+
return;
|
|
224
|
+
const barH = 22, barMargin = 8, barX = 20, barW = width - 40;
|
|
225
|
+
let idx = 0;
|
|
226
|
+
for (const [id, goal] of this.goals) {
|
|
227
|
+
const isTop = (goal.position ?? 'top') === 'top';
|
|
228
|
+
const barY = isTop ? 10 + idx * (barH + barMargin) : height - 60 - idx * (barH + barMargin);
|
|
229
|
+
const fill = this.goalAnimations.get(id) ?? 0;
|
|
230
|
+
ctx.save();
|
|
231
|
+
ctx.fillStyle = hexToRgba(C.bgPanel, 0.85);
|
|
232
|
+
ctx.fillRect(barX, barY, barW, barH);
|
|
233
|
+
ctx.strokeStyle = hexToRgba(C.accent, 0.5);
|
|
234
|
+
ctx.lineWidth = 1;
|
|
235
|
+
ctx.strokeRect(barX, barY, barW, barH);
|
|
236
|
+
const fillW = Math.floor(barW * fill);
|
|
237
|
+
if (fillW > 0) {
|
|
238
|
+
ctx.fillStyle = hexToRgba(goal.color ?? C.green, 0.8);
|
|
239
|
+
ctx.fillRect(barX, barY, fillW, barH);
|
|
240
|
+
if (fill < 1) {
|
|
241
|
+
ctx.fillStyle = hexToRgba(C.white, 0.3);
|
|
242
|
+
ctx.fillRect(barX + fillW - 3, barY, 3, barH);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (fill >= 0.999) {
|
|
246
|
+
ctx.fillStyle = hexToRgba(C.gold, 0.15 + 0.05 * Math.sin(Date.now() * 0.005));
|
|
247
|
+
ctx.fillRect(barX, barY, barW, barH);
|
|
248
|
+
}
|
|
249
|
+
ctx.fillStyle = C.text;
|
|
250
|
+
ctx.font = '12px "Courier Prime", monospace';
|
|
251
|
+
ctx.textAlign = 'left';
|
|
252
|
+
ctx.fillText(`${goal.label}: ${goal.current}/${goal.target}`, barX + 6, barY + 15);
|
|
253
|
+
const pct = Math.min(100, Math.round((goal.current / goal.target) * 100));
|
|
254
|
+
ctx.textAlign = 'right';
|
|
255
|
+
ctx.fillStyle = fill >= 0.999 ? C.gold : C.textDim;
|
|
256
|
+
ctx.fillText(`${pct}%`, barX + barW - 6, barY + 15);
|
|
257
|
+
ctx.textAlign = 'left';
|
|
258
|
+
ctx.restore();
|
|
259
|
+
idx++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// ── Ticker ─────────────────────────────────────────────────
|
|
263
|
+
tickTicker() {
|
|
264
|
+
for (const item of this.tickerItems) {
|
|
265
|
+
if (item.x === -1) {
|
|
266
|
+
let rightEdge = 1280;
|
|
267
|
+
for (const o of this.tickerItems) {
|
|
268
|
+
if (o !== item && o.x !== -1) {
|
|
269
|
+
const end = o.x + o.text.length * 8 + 40;
|
|
270
|
+
if (end > rightEdge)
|
|
271
|
+
rightEdge = end;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
item.x = rightEdge + 30;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const item of this.tickerItems)
|
|
278
|
+
item.x -= this.tickerSpeed;
|
|
279
|
+
this.tickerItems = this.tickerItems.filter(item => item.x + item.text.length * 8 + 40 > -10);
|
|
280
|
+
}
|
|
281
|
+
renderTicker(ctx, width, height) {
|
|
282
|
+
if (this.tickerItems.length === 0)
|
|
283
|
+
return;
|
|
284
|
+
const tickerH = 24, tickerY = height - 72;
|
|
285
|
+
ctx.save();
|
|
286
|
+
ctx.fillStyle = hexToRgba(C.bg, 0.85);
|
|
287
|
+
ctx.fillRect(0, tickerY, width, tickerH);
|
|
288
|
+
ctx.strokeStyle = hexToRgba(C.accent, 0.4);
|
|
289
|
+
ctx.lineWidth = 1;
|
|
290
|
+
ctx.beginPath();
|
|
291
|
+
ctx.moveTo(0, tickerY);
|
|
292
|
+
ctx.lineTo(width, tickerY);
|
|
293
|
+
ctx.moveTo(0, tickerY + tickerH);
|
|
294
|
+
ctx.lineTo(width, tickerY + tickerH);
|
|
295
|
+
ctx.stroke();
|
|
296
|
+
ctx.beginPath();
|
|
297
|
+
ctx.rect(0, tickerY, width, tickerH);
|
|
298
|
+
ctx.clip();
|
|
299
|
+
ctx.font = '12px "Courier Prime", monospace';
|
|
300
|
+
ctx.textAlign = 'left';
|
|
301
|
+
for (const item of this.tickerItems) {
|
|
302
|
+
ctx.fillStyle = C.accent;
|
|
303
|
+
ctx.fillRect(Math.round(item.x - 12), tickerY + 10, 4, 4);
|
|
304
|
+
ctx.fillStyle = C.text;
|
|
305
|
+
ctx.fillText(item.text, Math.round(item.x), tickerY + 16);
|
|
306
|
+
}
|
|
307
|
+
ctx.restore();
|
|
308
|
+
}
|
|
309
|
+
// ── Highlight ──────────────────────────────────────────────
|
|
310
|
+
tickHighlight() {
|
|
311
|
+
if (!this.highlight)
|
|
312
|
+
return;
|
|
313
|
+
if (++this.highlight.frame >= this.highlight.totalFrames)
|
|
314
|
+
this.highlight = null;
|
|
315
|
+
}
|
|
316
|
+
renderHighlight(ctx, width, height) {
|
|
317
|
+
if (!this.highlight)
|
|
318
|
+
return;
|
|
319
|
+
const h = this.highlight, progress = h.frame / h.totalFrames;
|
|
320
|
+
let alpha;
|
|
321
|
+
if (progress < 0.2)
|
|
322
|
+
alpha = easeOut(progress / 0.2);
|
|
323
|
+
else if (progress > 0.8)
|
|
324
|
+
alpha = 1 - easeIn((progress - 0.8) / 0.2);
|
|
325
|
+
else
|
|
326
|
+
alpha = 1;
|
|
327
|
+
const boxW = 500, boxH = 70;
|
|
328
|
+
const boxX = Math.floor((width - boxW) / 2), boxY = Math.floor(height / 2 - boxH / 2);
|
|
329
|
+
ctx.save();
|
|
330
|
+
ctx.globalAlpha = alpha;
|
|
331
|
+
ctx.fillStyle = hexToRgba(C.bgPanel, 0.95);
|
|
332
|
+
ctx.fillRect(boxX, boxY, boxW, boxH);
|
|
333
|
+
ctx.strokeStyle = hexToRgba(h.color, 0.9);
|
|
334
|
+
ctx.lineWidth = 2;
|
|
335
|
+
ctx.strokeRect(boxX, boxY, boxW, boxH);
|
|
336
|
+
ctx.strokeStyle = hexToRgba(h.color, 0.3);
|
|
337
|
+
ctx.lineWidth = 1;
|
|
338
|
+
ctx.strokeRect(boxX - 3, boxY - 3, boxW + 6, boxH + 6);
|
|
339
|
+
// Corner accents (4 corners via helper)
|
|
340
|
+
ctx.strokeStyle = h.color;
|
|
341
|
+
ctx.lineWidth = 2;
|
|
342
|
+
drawCorner(ctx, boxX, boxY, 1, 1, 12);
|
|
343
|
+
drawCorner(ctx, boxX + boxW, boxY, -1, 1, 12);
|
|
344
|
+
drawCorner(ctx, boxX, boxY + boxH, 1, -1, 12);
|
|
345
|
+
drawCorner(ctx, boxX + boxW, boxY + boxH, -1, -1, 12);
|
|
346
|
+
ctx.fillStyle = h.color;
|
|
347
|
+
ctx.font = 'bold 14px "Courier Prime", monospace';
|
|
348
|
+
ctx.textAlign = 'center';
|
|
349
|
+
ctx.fillText(h.username, boxX + boxW / 2, boxY + 24);
|
|
350
|
+
ctx.fillStyle = C.text;
|
|
351
|
+
ctx.font = '16px "Courier Prime", monospace';
|
|
352
|
+
ctx.fillText(truncate(h.message, 50), boxX + boxW / 2, boxY + 48);
|
|
353
|
+
ctx.textAlign = 'left';
|
|
354
|
+
ctx.restore();
|
|
355
|
+
}
|
|
356
|
+
// ── Info Bar ───────────────────────────────────────────────
|
|
357
|
+
renderInfoBar(ctx, width, height) {
|
|
358
|
+
const barH = 28, barY = height - barH;
|
|
359
|
+
ctx.save();
|
|
360
|
+
ctx.fillStyle = hexToRgba(C.bg, 0.92);
|
|
361
|
+
ctx.fillRect(0, barY, width, barH);
|
|
362
|
+
ctx.strokeStyle = hexToRgba(C.accent, 0.5);
|
|
363
|
+
ctx.lineWidth = 1;
|
|
364
|
+
ctx.beginPath();
|
|
365
|
+
ctx.moveTo(0, barY);
|
|
366
|
+
ctx.lineTo(width, barY);
|
|
367
|
+
ctx.stroke();
|
|
368
|
+
const info = this.infoBar;
|
|
369
|
+
const items = [
|
|
370
|
+
{ label: 'VIEWERS', value: String(info.viewers), color: C.green },
|
|
371
|
+
{ label: 'UPTIME', value: info.uptime, color: C.blue },
|
|
372
|
+
{ label: 'BIOME', value: info.biome, color: C.accent },
|
|
373
|
+
{ label: 'CHAT', value: `${info.chatRate}/min`, color: C.orange },
|
|
374
|
+
];
|
|
375
|
+
const sectionW = Math.floor(width / items.length);
|
|
376
|
+
ctx.font = '11px "Courier Prime", monospace';
|
|
377
|
+
ctx.textAlign = 'left';
|
|
378
|
+
for (let i = 0; i < items.length; i++) {
|
|
379
|
+
const x = i * sectionW + 12, it = items[i];
|
|
380
|
+
ctx.fillStyle = C.textDim;
|
|
381
|
+
ctx.fillText(it.label, x, barY + 12);
|
|
382
|
+
ctx.fillStyle = it.color;
|
|
383
|
+
ctx.font = 'bold 12px "Courier Prime", monospace';
|
|
384
|
+
ctx.fillText(it.value, x + it.label.length * 7 + 8, barY + 12);
|
|
385
|
+
ctx.font = '11px "Courier Prime", monospace';
|
|
386
|
+
if (i < items.length - 1) {
|
|
387
|
+
ctx.fillStyle = hexToRgba(C.accent, 0.3);
|
|
388
|
+
ctx.fillRect(i * sectionW + sectionW - 1, barY + 4, 1, barH - 8);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
ctx.textAlign = 'right';
|
|
392
|
+
ctx.fillStyle = hexToRgba(C.accent, 0.6);
|
|
393
|
+
ctx.font = '10px "Courier Prime", monospace';
|
|
394
|
+
ctx.fillText('kbot stream', width - 8, barY + 19);
|
|
395
|
+
ctx.textAlign = 'left';
|
|
396
|
+
ctx.restore();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// ─── Singleton ─────────────────────────────────────────────────
|
|
400
|
+
let overlayInstance = null;
|
|
401
|
+
export function getOverlay() {
|
|
402
|
+
if (!overlayInstance)
|
|
403
|
+
overlayInstance = new StreamOverlay();
|
|
404
|
+
return overlayInstance;
|
|
405
|
+
}
|
|
406
|
+
// ─── Tool Registration ─────────────────────────────────────────
|
|
407
|
+
export function registerOverlayTools() {
|
|
408
|
+
registerTool({
|
|
409
|
+
name: 'overlay_alert',
|
|
410
|
+
description: 'Queue a stream overlay alert. Types: follow, raid, sub, donation, achievement. ' +
|
|
411
|
+
'Alerts display one at a time with enter/hold/exit animations and particle effects.',
|
|
412
|
+
parameters: {
|
|
413
|
+
type: { type: 'string', description: 'Alert type: follow | raid | sub | donation | achievement', required: true },
|
|
414
|
+
username: { type: 'string', description: 'Username to display', required: true },
|
|
415
|
+
viewers: { type: 'number', description: 'Viewer count (for raid alerts)', required: false },
|
|
416
|
+
amount: { type: 'number', description: 'Dollar amount (for donation alerts)', required: false },
|
|
417
|
+
title: { type: 'string', description: 'Achievement title (for achievement alerts)', required: false },
|
|
418
|
+
message: { type: 'string', description: 'Optional custom message below the alert body', required: false },
|
|
419
|
+
},
|
|
420
|
+
tier: 'free',
|
|
421
|
+
execute: async (args) => {
|
|
422
|
+
const type = args.type;
|
|
423
|
+
const valid = ['follow', 'raid', 'sub', 'donation', 'achievement'];
|
|
424
|
+
if (!valid.includes(type))
|
|
425
|
+
return `Invalid alert type: ${type}. Must be one of: ${valid.join(', ')}`;
|
|
426
|
+
const alert = {
|
|
427
|
+
type, username: args.username || 'Anonymous',
|
|
428
|
+
viewers: args.viewers, amount: args.amount,
|
|
429
|
+
title: args.title, message: args.message,
|
|
430
|
+
};
|
|
431
|
+
getOverlay().queueAlert(alert);
|
|
432
|
+
return `Queued ${type} alert for ${alert.username}`;
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
registerTool({
|
|
436
|
+
name: 'overlay_goal',
|
|
437
|
+
description: 'Set or update a goal progress bar on the stream overlay. ' +
|
|
438
|
+
'Creates the goal if target is provided, otherwise updates current value.',
|
|
439
|
+
parameters: {
|
|
440
|
+
id: { type: 'string', description: 'Unique goal ID (e.g. "followers", "duration")', required: true },
|
|
441
|
+
label: { type: 'string', description: 'Display label (e.g. "Followers")', required: false },
|
|
442
|
+
current: { type: 'number', description: 'Current progress value', required: true },
|
|
443
|
+
target: { type: 'number', description: 'Target value (needed when creating a new goal)', required: false },
|
|
444
|
+
color: { type: 'string', description: 'Bar fill color as hex (default: #3fb950)', required: false },
|
|
445
|
+
position: { type: 'string', description: 'Position: top | bottom (default: top)', required: false },
|
|
446
|
+
},
|
|
447
|
+
tier: 'free',
|
|
448
|
+
execute: async (args) => {
|
|
449
|
+
const overlay = getOverlay(), id = args.id, current = args.current;
|
|
450
|
+
if (args.target !== undefined) {
|
|
451
|
+
overlay.setGoal({
|
|
452
|
+
id, label: args.label || id, current, target: args.target,
|
|
453
|
+
color: args.color, position: args.position || 'top',
|
|
454
|
+
});
|
|
455
|
+
return `Goal "${id}" set: ${current}/${args.target}`;
|
|
456
|
+
}
|
|
457
|
+
overlay.updateGoal(id, current);
|
|
458
|
+
return `Goal "${id}" updated to ${current}`;
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
registerTool({
|
|
462
|
+
name: 'overlay_ticker',
|
|
463
|
+
description: 'Add a message to the scrolling ticker at the bottom of the stream overlay. Messages scroll right-to-left continuously.',
|
|
464
|
+
parameters: {
|
|
465
|
+
text: { type: 'string', description: 'Text to add to the scrolling ticker', required: true },
|
|
466
|
+
},
|
|
467
|
+
tier: 'free',
|
|
468
|
+
execute: async (args) => {
|
|
469
|
+
const text = args.text;
|
|
470
|
+
if (!text)
|
|
471
|
+
return 'No text provided';
|
|
472
|
+
getOverlay().addTicker(text);
|
|
473
|
+
return `Added to ticker: "${text}"`;
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
registerTool({
|
|
477
|
+
name: 'overlay_highlight',
|
|
478
|
+
description: 'Highlight a chat message in the center of the stream for 3 seconds with fancy border and corner accents.',
|
|
479
|
+
parameters: {
|
|
480
|
+
username: { type: 'string', description: 'Username of the message author', required: true },
|
|
481
|
+
message: { type: 'string', description: 'The message to highlight', required: true },
|
|
482
|
+
color: { type: 'string', description: 'Username/border color as hex (default: #6B5B95)', required: false },
|
|
483
|
+
},
|
|
484
|
+
tier: 'free',
|
|
485
|
+
execute: async (args) => {
|
|
486
|
+
const username = args.username, message = args.message;
|
|
487
|
+
if (!username || !message)
|
|
488
|
+
return 'Username and message are required';
|
|
489
|
+
getOverlay().highlightMessage(username, message, args.color);
|
|
490
|
+
return `Highlighting message from ${username}`;
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
} // end registerOverlayTools
|
|
494
|
+
//# sourceMappingURL=stream-overlay.js.map
|