@lumiastream/lumia-types 3.2.6 → 3.2.8
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/activity.types.d.ts +3 -0
- package/dist/activity.types.js +3 -0
- package/dist/alert.types.js +53 -0
- package/dist/custom-overlays/custom-overlays-cheatsheet.md +23 -30
- package/dist/custom-overlays/custom-overlays-documentation.md +103 -134
- package/dist/custom-overlays/custom-overlays-examples.md +670 -70
- package/dist/custom-overlays/custom-overlays.d.ts +20 -18
- package/dist/custom-overlays/gpt-instructions.md +111 -166
- package/dist/custom-overlays.d.ts +20 -18
- package/dist/event.types.d.ts +1 -0
- package/dist/event.types.js +1 -0
- package/dist/variables.types.d.ts +6 -0
- package/dist/variables.types.js +6 -0
- package/package.json +1 -1
|
@@ -2,45 +2,153 @@
|
|
|
2
2
|
|
|
3
3
|
> Need to inspect what an alert returns? Open the [alert explorer](/docs/variables#alert-explorer) on the Platform Variables page to browse `data.alert`, `data.extraSettings.*`, and the fields available for each alert.
|
|
4
4
|
|
|
5
|
+
## Index
|
|
6
|
+
|
|
7
|
+
Grouped by what the overlay does, so you can jump to the closest starting point:
|
|
8
|
+
|
|
9
|
+
**Alerts & chat**
|
|
10
|
+
|
|
11
|
+
- [Custom Alert](#custom-alert) — single listener branches on `data.alert` for donations, subs, follows, bits, raids.
|
|
12
|
+
- [Custom Chat Box](#custom-chat-box) — renders chat messages with DOM APIs (no `innerHTML`).
|
|
13
|
+
|
|
14
|
+
**Commands & games**
|
|
15
|
+
|
|
16
|
+
- [Roll a Dice](#roll-a-dice) — `!roll` chat command with animation.
|
|
17
|
+
- [Pokemon Catch Mini-Game Overlay](#pokemon-catch-mini-game-overlay) — full game with storage, chatbot, and SFX.
|
|
18
|
+
- [Calculator with overlaySendCustomContent from Lumia Stream](#calculator-with-overlaysendcustomcontent-from-lumia-stream) — receives content from a Lumia command via `overlaycontent` listener.
|
|
19
|
+
|
|
20
|
+
**External APIs (fetch)**
|
|
21
|
+
|
|
22
|
+
- [Anime Facts using Fetch](#anime-facts-using-fetch)
|
|
23
|
+
- [Pet Cam Random Dog/Cat Images using Fetch](#pet-cam-random-dogcat-images-using-fetch)
|
|
24
|
+
|
|
25
|
+
**HFX / lights / loyalty**
|
|
26
|
+
|
|
27
|
+
- [HFX Listener Banner](#hfx-listener-banner) — `Overlay.on("hfx", ...)` triggers an animated banner with the user, command, and message.
|
|
28
|
+
- [Virtual Light Monitor](#virtual-light-monitor) — `Overlay.on("virtuallight", ...)` shows the current light color/brightness/power; persists across reloads with `saveStorage` + first-load null-check.
|
|
29
|
+
- [Loyalty Points Leaderboard](#loyalty-points-leaderboard) — `Overlay.getLoyaltyPoints` / `addLoyaltyPoints` + `Overlay.chatbot` for `!points` and `!give` commands.
|
|
30
|
+
|
|
31
|
+
**Visual**
|
|
32
|
+
|
|
33
|
+
- [Art Canvas](#art-canvas) — canvas rendering.
|
|
34
|
+
- [Font Picker with Quoted Variable](#font-picker-with-quoted-variable) — small reference for how Google fonts must be quoted in CSS.
|
|
35
|
+
|
|
36
|
+
## Font Picker with Quoted Variable
|
|
37
|
+
|
|
38
|
+
Small reference overlay. The key pattern: `fontpicker` Configs produce a font-family string, and CSS needs that value inside quotes because `font-family` expects a string. Compare to colors/sizes, which must be unquoted.
|
|
39
|
+
|
|
40
|
+
### HTML
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<div id="label">Hello, streamer!</div>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### CSS
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
#label {
|
|
50
|
+
/* quoted — font-family needs a string */
|
|
51
|
+
font-family: "{{font}}";
|
|
52
|
+
/* unquoted — color/size/number values are not strings */
|
|
53
|
+
color: {{labelColor}};
|
|
54
|
+
font-size: {{fontSize}}px;
|
|
55
|
+
padding: 16px 24px;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### JS
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
// Nothing needed — CSS variable replacement renders the picked font automatically.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Configs
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"font": {
|
|
70
|
+
"type": "fontpicker",
|
|
71
|
+
"label": "Font",
|
|
72
|
+
"order": 1
|
|
73
|
+
},
|
|
74
|
+
"labelColor": {
|
|
75
|
+
"type": "colorpicker",
|
|
76
|
+
"label": "Label color",
|
|
77
|
+
"order": 2,
|
|
78
|
+
"value": "#ffffff"
|
|
79
|
+
},
|
|
80
|
+
"fontSize": {
|
|
81
|
+
"type": "slider",
|
|
82
|
+
"label": "Font size",
|
|
83
|
+
"order": 3,
|
|
84
|
+
"options": { "min": 10, "max": 120, "step": 2, "suffix": "px" },
|
|
85
|
+
"value": 48
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Data
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"font": "Roboto",
|
|
95
|
+
"labelColor": "#ffffff",
|
|
96
|
+
"fontSize": 48
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
5
100
|
## Custom Alert
|
|
6
101
|
|
|
7
102
|
### JS Code
|
|
8
103
|
|
|
9
104
|
```js
|
|
10
105
|
// Overlay.data is fetched from the sidebar input values
|
|
11
|
-
const container = document.getElementById(
|
|
106
|
+
const container = document.getElementById("container");
|
|
107
|
+
const alertContainer = document.getElementById("alert-container");
|
|
12
108
|
|
|
13
109
|
// Pass data to css variable to use it easily inside css
|
|
14
|
-
if (Overlay.data?.primaryColor) container.style.setProperty(
|
|
15
|
-
if (Overlay.data?.backgroundColor) container.style.setProperty(
|
|
16
|
-
if (Overlay.data?.messageBgColor) container.style.setProperty(
|
|
17
|
-
if (Overlay.data?.textColor) container.style.setProperty(
|
|
18
|
-
if (Overlay.data?.rounded) container.style.setProperty(
|
|
110
|
+
if (Overlay.data?.primaryColor) container.style.setProperty("--primary", Overlay.data.primaryColor);
|
|
111
|
+
if (Overlay.data?.backgroundColor) container.style.setProperty("--secondary", Overlay.data.backgroundColor);
|
|
112
|
+
if (Overlay.data?.messageBgColor) container.style.setProperty("--bg", Overlay.data.messageBgColor);
|
|
113
|
+
if (Overlay.data?.textColor) container.style.setProperty("--text", Overlay.data.textColor);
|
|
114
|
+
if (Overlay.data?.rounded) container.style.setProperty("--rounded", Overlay.data.rounded);
|
|
19
115
|
|
|
20
116
|
// Listen for alerts
|
|
21
|
-
Overlay.on(
|
|
117
|
+
Overlay.on("alert", (data) => {
|
|
22
118
|
const alertType = data.alert;
|
|
23
|
-
const settings = data.extraSettings;
|
|
24
|
-
let fullMessage =
|
|
25
|
-
|
|
26
|
-
if (alertType
|
|
27
|
-
fullMessage
|
|
28
|
-
} else if (alertType
|
|
29
|
-
fullMessage
|
|
30
|
-
} else if (alertType
|
|
31
|
-
fullMessage
|
|
32
|
-
} else if (alertType
|
|
33
|
-
fullMessage
|
|
34
|
-
} else if (alertType
|
|
35
|
-
fullMessage
|
|
36
|
-
} else
|
|
37
|
-
|
|
119
|
+
const settings = data.extraSettings || {};
|
|
120
|
+
let fullMessage = "";
|
|
121
|
+
|
|
122
|
+
if (alertType === "streamlabs-donation" || alertType === "streamelements-donation" || alertType === "lumiastream-donation") {
|
|
123
|
+
fullMessage = `${settings.username || "Someone"} just tipped ${settings.amount ?? data.dynamic?.value ?? ""} ${settings.currency ?? ""}`;
|
|
124
|
+
} else if (alertType === "twitch-follower" || alertType === "kick-follower") {
|
|
125
|
+
fullMessage = `${settings.username || "Someone"} is now following!`;
|
|
126
|
+
} else if (alertType === "twitch-subscriber" || alertType === "kick-subscriber") {
|
|
127
|
+
fullMessage = `${settings.username || "Someone"} just subscribed!`;
|
|
128
|
+
} else if (alertType === "twitch-bits") {
|
|
129
|
+
fullMessage = `${settings.username || "Someone"} cheered ${data.dynamic?.value ?? settings.amount ?? ""} bits`;
|
|
130
|
+
} else if (alertType === "twitch-raid") {
|
|
131
|
+
fullMessage = `${settings.username || "Someone"} raided with ${data.dynamic?.value ?? settings.viewers ?? 0} viewers`;
|
|
132
|
+
} else {
|
|
133
|
+
return;
|
|
38
134
|
}
|
|
39
135
|
|
|
40
136
|
if (settings?.message) fullMessage += ` They said ${settings?.message}`;
|
|
41
137
|
|
|
42
|
-
|
|
43
|
-
|
|
138
|
+
alertContainer.textContent = "";
|
|
139
|
+
const messageEl = document.createElement("div");
|
|
140
|
+
messageEl.className = "message-container";
|
|
141
|
+
|
|
142
|
+
const avatarEl = document.createElement("img");
|
|
143
|
+
avatarEl.className = "avatar";
|
|
144
|
+
avatarEl.src = settings.avatar || "https://storage.lumiastream.com/placeholderUserIcon.png";
|
|
145
|
+
avatarEl.alt = "";
|
|
146
|
+
|
|
147
|
+
const textEl = document.createElement("span");
|
|
148
|
+
textEl.textContent = fullMessage;
|
|
149
|
+
|
|
150
|
+
messageEl.append(avatarEl, textEl);
|
|
151
|
+
alertContainer.appendChild(messageEl);
|
|
44
152
|
});
|
|
45
153
|
```
|
|
46
154
|
|
|
@@ -96,7 +204,7 @@ Overlay.on('alert', (data) => {
|
|
|
96
204
|
align-items: center;
|
|
97
205
|
gap: 0.5rem;
|
|
98
206
|
}
|
|
99
|
-
|
|
207
|
+
.avatar {
|
|
100
208
|
background: var(--secondary);
|
|
101
209
|
border-radius: 100px;
|
|
102
210
|
height: 32px;
|
|
@@ -123,15 +231,20 @@ Overlay.on('alert', (data) => {
|
|
|
123
231
|
"label": "Text color:",
|
|
124
232
|
"order": 3
|
|
125
233
|
},
|
|
234
|
+
"messageBgColor": {
|
|
235
|
+
"type": "colorpicker",
|
|
236
|
+
"label": "Message background color:",
|
|
237
|
+
"order": 4
|
|
238
|
+
},
|
|
126
239
|
"rounded": {
|
|
127
240
|
"type": "input",
|
|
128
241
|
"label": "Rounded corners:",
|
|
129
|
-
"order":
|
|
242
|
+
"order": 5
|
|
130
243
|
},
|
|
131
244
|
"alertImage": {
|
|
132
245
|
"type": "input",
|
|
133
246
|
"label": "Alert image URL:",
|
|
134
|
-
"order":
|
|
247
|
+
"order": 6
|
|
135
248
|
}
|
|
136
249
|
}
|
|
137
250
|
```
|
|
@@ -155,26 +268,52 @@ Overlay.on('alert', (data) => {
|
|
|
155
268
|
|
|
156
269
|
```js
|
|
157
270
|
// Overlay.data is fetched from the sidebar input values
|
|
158
|
-
const messageContainer = document.getElementById(
|
|
271
|
+
const messageContainer = document.getElementById("container");
|
|
272
|
+
const messagesContainer = document.getElementById("messages-container");
|
|
159
273
|
|
|
160
274
|
// Pass data to css variable to use it easily inside css
|
|
161
|
-
if (Overlay.data?.primaryColor) messageContainer.style.setProperty(
|
|
162
|
-
if (Overlay.data?.backgroundColor) messageContainer.style.setProperty(
|
|
163
|
-
if (Overlay.data?.messageBgColor) messageContainer.style.setProperty(
|
|
164
|
-
if (Overlay.data?.textColor) messageContainer.style.setProperty(
|
|
165
|
-
if (Overlay.data?.rounded) messageContainer.style.setProperty(
|
|
275
|
+
if (Overlay.data?.primaryColor) messageContainer.style.setProperty("--primary", Overlay.data.primaryColor);
|
|
276
|
+
if (Overlay.data?.backgroundColor) messageContainer.style.setProperty("--secondary", Overlay.data.backgroundColor);
|
|
277
|
+
if (Overlay.data?.messageBgColor) messageContainer.style.setProperty("--bg", Overlay.data.messageBgColor);
|
|
278
|
+
if (Overlay.data?.textColor) messageContainer.style.setProperty("--text", Overlay.data.textColor);
|
|
279
|
+
if (Overlay.data?.rounded) messageContainer.style.setProperty("--rounded", Overlay.data.rounded);
|
|
166
280
|
|
|
167
281
|
// Listen for chat messages
|
|
168
|
-
Overlay.on(
|
|
169
|
-
const
|
|
282
|
+
Overlay.on("chat", (data) => {
|
|
283
|
+
const messageEl = document.createElement("div");
|
|
284
|
+
messageEl.className = "message";
|
|
285
|
+
|
|
286
|
+
const originEl = document.createElement("span");
|
|
287
|
+
originEl.className = "origin";
|
|
288
|
+
originEl.textContent = `From ${data.origin}`;
|
|
170
289
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
290
|
+
const avatarEl = document.createElement("img");
|
|
291
|
+
avatarEl.className = "avatar";
|
|
292
|
+
avatarEl.src = data.avatar || "https://storage.lumiastream.com/placeholderUserIcon.png";
|
|
293
|
+
avatarEl.alt = "";
|
|
294
|
+
|
|
295
|
+
const usernameEl = document.createElement("strong");
|
|
296
|
+
usernameEl.className = "username";
|
|
297
|
+
usernameEl.textContent = `${data.username}:`;
|
|
298
|
+
|
|
299
|
+
const contentEl = document.createElement("span");
|
|
300
|
+
contentEl.className = "content";
|
|
301
|
+
contentEl.textContent = data.message;
|
|
302
|
+
|
|
303
|
+
messageEl.append(originEl, avatarEl, usernameEl, contentEl);
|
|
304
|
+
messagesContainer.appendChild(messageEl);
|
|
175
305
|
});
|
|
176
306
|
```
|
|
177
307
|
|
|
308
|
+
### HTML
|
|
309
|
+
|
|
310
|
+
```html
|
|
311
|
+
<div id="container">
|
|
312
|
+
<h1>Custom Chat Box</h1>
|
|
313
|
+
<div id="messages-container" class="messages-container"></div>
|
|
314
|
+
</div>
|
|
315
|
+
```
|
|
316
|
+
|
|
178
317
|
### CSS Styling
|
|
179
318
|
|
|
180
319
|
```css
|
|
@@ -212,7 +351,7 @@ Overlay.on('chat', (data) => {
|
|
|
212
351
|
background: var(--bg);
|
|
213
352
|
border-radius: 10px;
|
|
214
353
|
}
|
|
215
|
-
|
|
354
|
+
.avatar {
|
|
216
355
|
background: var(--secondary);
|
|
217
356
|
border-radius: 100px;
|
|
218
357
|
height: 32px;
|
|
@@ -288,15 +427,16 @@ async function() {
|
|
|
288
427
|
const calcEl = document.getElementById('calc-container');
|
|
289
428
|
const exprEl = document.getElementById('expression');
|
|
290
429
|
const resultEl = document.getElementById('result');
|
|
430
|
+
const cfg = Overlay.data || {};
|
|
291
431
|
|
|
292
432
|
// Apply colors from sidebar
|
|
293
|
-
if (
|
|
294
|
-
if (
|
|
433
|
+
if (cfg.primaryColor) calcEl.style.setProperty('--primary', cfg.primaryColor);
|
|
434
|
+
if (cfg.backgroundColor) calcEl.style.setProperty('--background', cfg.backgroundColor);
|
|
295
435
|
|
|
296
436
|
let tokens = [];
|
|
297
437
|
let showingResult = false;
|
|
298
438
|
|
|
299
|
-
const IDLE_MS =
|
|
439
|
+
const IDLE_MS = cfg.idleTimeout ?? 20000;
|
|
300
440
|
let idleTimer = null;
|
|
301
441
|
|
|
302
442
|
function resetIdleTimer() {
|
|
@@ -309,7 +449,7 @@ function resetIdleTimer() {
|
|
|
309
449
|
}
|
|
310
450
|
|
|
311
451
|
function speak(text, opts = {}) {
|
|
312
|
-
if (!
|
|
452
|
+
if (!cfg.tts) return;
|
|
313
453
|
const u = new SpeechSynthesisUtterance(text);
|
|
314
454
|
Object.assign(u, opts); // voice, rate, pitch, lang, etc.
|
|
315
455
|
window.speechSynthesis.cancel(); // stop anything currently talking
|
|
@@ -334,7 +474,14 @@ function verbalize(token) {
|
|
|
334
474
|
}
|
|
335
475
|
|
|
336
476
|
function render() {
|
|
337
|
-
exprEl.
|
|
477
|
+
exprEl.textContent = '';
|
|
478
|
+
tokens.forEach((t, i) => {
|
|
479
|
+
if (i > 0) exprEl.appendChild(document.createTextNode(' '));
|
|
480
|
+
const span = document.createElement('span');
|
|
481
|
+
span.className = /^[0-9.]+$/.test(t) ? 'token number' : 'token op';
|
|
482
|
+
span.textContent = t;
|
|
483
|
+
exprEl.appendChild(span);
|
|
484
|
+
});
|
|
338
485
|
}
|
|
339
486
|
|
|
340
487
|
function evaluate() {
|
|
@@ -498,10 +645,11 @@ Overlay.on('overlaycontent', (data) => {
|
|
|
498
645
|
|
|
499
646
|
```json
|
|
500
647
|
{
|
|
501
|
-
"tts":
|
|
648
|
+
"tts": false,
|
|
649
|
+
"ttsVoice": "",
|
|
502
650
|
"idleTimeout": 20000,
|
|
503
|
-
"primaryColor": "#
|
|
504
|
-
"backgroundColor": "#
|
|
651
|
+
"primaryColor": "#ffffff",
|
|
652
|
+
"backgroundColor": "#00000080"
|
|
505
653
|
}
|
|
506
654
|
```
|
|
507
655
|
|
|
@@ -511,11 +659,15 @@ Overlay.on('overlaycontent', (data) => {
|
|
|
511
659
|
|
|
512
660
|
```js
|
|
513
661
|
const die = document.getElementById('die');
|
|
514
|
-
const
|
|
662
|
+
const cfg = Overlay.data || {};
|
|
663
|
+
const HIDE_MS = cfg.hideDelay ?? 5000;
|
|
515
664
|
let hideT = null;
|
|
516
665
|
let min = 1;
|
|
517
666
|
let max = 6;
|
|
518
667
|
|
|
668
|
+
if (cfg.dotColor) die.style.setProperty('--dot-color', cfg.dotColor);
|
|
669
|
+
die.style.setProperty('--roll-ms', `${cfg.rollDuration ?? 900}ms`);
|
|
670
|
+
|
|
519
671
|
function showDie() {
|
|
520
672
|
die.classList.remove('hidden');
|
|
521
673
|
clearTimeout(hideT);
|
|
@@ -537,7 +689,7 @@ Overlay.on('chat', (data) => {
|
|
|
537
689
|
// reveal value after spin
|
|
538
690
|
setTimeout(() => {
|
|
539
691
|
die.textContent = v;
|
|
540
|
-
},
|
|
692
|
+
}, cfg.rollDuration ?? 900);
|
|
541
693
|
|
|
542
694
|
// make it visible & schedule hide
|
|
543
695
|
showDie();
|
|
@@ -906,13 +1058,15 @@ async function showPokedex(username = 'Someone') {
|
|
|
906
1058
|
|
|
907
1059
|
dexMsgEl.textContent = list.length ? `${username} has caught ${list.length} / ${cfg.maxDex}` : `${username} hasn’t caught anything yet.`;
|
|
908
1060
|
|
|
909
|
-
dexImgsEl.
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
.
|
|
1061
|
+
dexImgsEl.textContent = '';
|
|
1062
|
+
list.forEach((p) => {
|
|
1063
|
+
const slug = p.name.toLowerCase().replace(/♀/g, 'f').replace(/♂/g, 'm').replace(/[.'’]/g, '').replace(/\s+/g, '-');
|
|
1064
|
+
const img = document.createElement('img');
|
|
1065
|
+
img.className = 'poke' + (p.shiny ? ' shiny' : '');
|
|
1066
|
+
img.src = `https://img.pokemondb.net/sprites/home/${p.shiny ? 'shiny' : 'normal'}/${slug}.png`;
|
|
1067
|
+
img.alt = p.name;
|
|
1068
|
+
dexImgsEl.appendChild(img);
|
|
1069
|
+
});
|
|
916
1070
|
|
|
917
1071
|
modalDex.classList.remove('hidden');
|
|
918
1072
|
setTimeout(() => modalDex.classList.add('hidden'), 10000);
|
|
@@ -925,17 +1079,25 @@ async function showLeaderboard() {
|
|
|
925
1079
|
.sort((a, b) => b[1].total - a[1].total)
|
|
926
1080
|
.slice(0, 10);
|
|
927
1081
|
|
|
928
|
-
boardList.
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1082
|
+
boardList.textContent = '';
|
|
1083
|
+
if (!list.length) {
|
|
1084
|
+
const li = document.createElement('li');
|
|
1085
|
+
li.textContent = 'No catches yet';
|
|
1086
|
+
boardList.appendChild(li);
|
|
1087
|
+
} else {
|
|
1088
|
+
list.forEach(([u, d]) => {
|
|
1089
|
+
const tip = d.list.map((p) => (p.shiny ? `${p.name} ⭐` : p.name)).join(', ');
|
|
1090
|
+
const li = document.createElement('li');
|
|
1091
|
+
const userSpan = document.createElement('span');
|
|
1092
|
+
userSpan.className = 'user';
|
|
1093
|
+
userSpan.title = tip;
|
|
1094
|
+
userSpan.textContent = u;
|
|
1095
|
+
const totalSpan = document.createElement('span');
|
|
1096
|
+
totalSpan.textContent = d.total + (d.shiny ? ` ⭐${d.shiny}` : '');
|
|
1097
|
+
li.append(userSpan, totalSpan);
|
|
1098
|
+
boardList.appendChild(li);
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
939
1101
|
|
|
940
1102
|
boardList.classList.remove('hidden');
|
|
941
1103
|
|
|
@@ -1343,6 +1505,12 @@ body {
|
|
|
1343
1505
|
"order": 7,
|
|
1344
1506
|
"value": 60
|
|
1345
1507
|
},
|
|
1508
|
+
"msgMs": {
|
|
1509
|
+
"type": "number",
|
|
1510
|
+
"label": "Capture message duration (ms)",
|
|
1511
|
+
"order": 8,
|
|
1512
|
+
"value": 7000
|
|
1513
|
+
},
|
|
1346
1514
|
"idleSec": {
|
|
1347
1515
|
"type": "number",
|
|
1348
1516
|
"label": "Flee time (s)",
|
|
@@ -1392,8 +1560,9 @@ body {
|
|
|
1392
1560
|
"fleePct": 15,
|
|
1393
1561
|
"idleSec": 30,
|
|
1394
1562
|
"catchPct": 40,
|
|
1395
|
-
"maxSpawn":
|
|
1396
|
-
"minSpawn":
|
|
1563
|
+
"maxSpawn": 60,
|
|
1564
|
+
"minSpawn": 40,
|
|
1565
|
+
"msgMs": 7000,
|
|
1397
1566
|
"shinyRate": 2,
|
|
1398
1567
|
"disableChat": false,
|
|
1399
1568
|
"catchCommand": "!catch",
|
|
@@ -2308,3 +2477,434 @@ html, body, #board {
|
|
|
2308
2477
|
"twitchPointsCommand": "art"
|
|
2309
2478
|
}
|
|
2310
2479
|
```
|
|
2480
|
+
|
|
2481
|
+
## HFX Listener Banner
|
|
2482
|
+
|
|
2483
|
+
Reacts to Lumia HFX triggers. Flashes a banner showing who triggered the HFX, which command, and their message. Uses `Overlay.on("hfx", ...)` — the handler receives the raw HFX payload (no `event.detail`).
|
|
2484
|
+
|
|
2485
|
+
### HTML
|
|
2486
|
+
|
|
2487
|
+
```html
|
|
2488
|
+
<div id="hfx-banner" class="hidden">
|
|
2489
|
+
<img id="hfx-avatar" alt="" />
|
|
2490
|
+
<div class="text">
|
|
2491
|
+
<div id="hfx-user"></div>
|
|
2492
|
+
<div id="hfx-command"></div>
|
|
2493
|
+
<div id="hfx-message"></div>
|
|
2494
|
+
</div>
|
|
2495
|
+
</div>
|
|
2496
|
+
```
|
|
2497
|
+
|
|
2498
|
+
### CSS
|
|
2499
|
+
|
|
2500
|
+
```css
|
|
2501
|
+
body {
|
|
2502
|
+
background: transparent;
|
|
2503
|
+
font-family: "{{font}}";
|
|
2504
|
+
color: {{textColor}};
|
|
2505
|
+
}
|
|
2506
|
+
#hfx-banner {
|
|
2507
|
+
position: absolute;
|
|
2508
|
+
bottom: 40px;
|
|
2509
|
+
left: 40px;
|
|
2510
|
+
display: flex;
|
|
2511
|
+
align-items: center;
|
|
2512
|
+
gap: 16px;
|
|
2513
|
+
padding: 16px 24px;
|
|
2514
|
+
background: {{bgColor}};
|
|
2515
|
+
border-radius: 16px;
|
|
2516
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
2517
|
+
transform: translateX(-120%);
|
|
2518
|
+
transition: transform 400ms cubic-bezier(0.2, 0.9, 0.3, 1);
|
|
2519
|
+
}
|
|
2520
|
+
#hfx-banner.show {
|
|
2521
|
+
transform: translateX(0);
|
|
2522
|
+
}
|
|
2523
|
+
#hfx-banner.hidden {
|
|
2524
|
+
display: none;
|
|
2525
|
+
}
|
|
2526
|
+
#hfx-avatar {
|
|
2527
|
+
width: 64px;
|
|
2528
|
+
height: 64px;
|
|
2529
|
+
border-radius: 50%;
|
|
2530
|
+
background: #222;
|
|
2531
|
+
object-fit: cover;
|
|
2532
|
+
}
|
|
2533
|
+
.text {
|
|
2534
|
+
display: flex;
|
|
2535
|
+
flex-direction: column;
|
|
2536
|
+
gap: 2px;
|
|
2537
|
+
font-size: {{fontSize}}px;
|
|
2538
|
+
}
|
|
2539
|
+
#hfx-user {
|
|
2540
|
+
font-weight: 700;
|
|
2541
|
+
}
|
|
2542
|
+
#hfx-command {
|
|
2543
|
+
opacity: 0.8;
|
|
2544
|
+
font-size: 0.8em;
|
|
2545
|
+
}
|
|
2546
|
+
#hfx-message {
|
|
2547
|
+
opacity: 0.9;
|
|
2548
|
+
font-style: italic;
|
|
2549
|
+
}
|
|
2550
|
+
```
|
|
2551
|
+
|
|
2552
|
+
### JS
|
|
2553
|
+
|
|
2554
|
+
```js
|
|
2555
|
+
const banner = document.getElementById("hfx-banner");
|
|
2556
|
+
const avatarEl = document.getElementById("hfx-avatar");
|
|
2557
|
+
const userEl = document.getElementById("hfx-user");
|
|
2558
|
+
const commandEl = document.getElementById("hfx-command");
|
|
2559
|
+
const messageEl = document.getElementById("hfx-message");
|
|
2560
|
+
let hideTimer = null;
|
|
2561
|
+
|
|
2562
|
+
function show(data) {
|
|
2563
|
+
avatarEl.src = data.avatar || "https://storage.lumiastream.com/placeholderUserIcon.png";
|
|
2564
|
+
userEl.textContent = data.username || "Someone";
|
|
2565
|
+
commandEl.textContent = data.command ? `triggered ${data.command}` : "triggered an HFX";
|
|
2566
|
+
messageEl.textContent = data.message || "";
|
|
2567
|
+
|
|
2568
|
+
banner.classList.remove("hidden");
|
|
2569
|
+
// allow the DOM to register the hidden→visible transition
|
|
2570
|
+
requestAnimationFrame(() => banner.classList.add("show"));
|
|
2571
|
+
|
|
2572
|
+
clearTimeout(hideTimer);
|
|
2573
|
+
const holdMs = Math.max(2000, Number(data.duration) || Number(Overlay.data?.holdMs) || 5000);
|
|
2574
|
+
hideTimer = setTimeout(() => {
|
|
2575
|
+
banner.classList.remove("show");
|
|
2576
|
+
setTimeout(() => banner.classList.add("hidden"), 500);
|
|
2577
|
+
}, holdMs);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
Overlay.on("hfx", (data) => {
|
|
2581
|
+
show(data);
|
|
2582
|
+
});
|
|
2583
|
+
```
|
|
2584
|
+
|
|
2585
|
+
### Configs
|
|
2586
|
+
|
|
2587
|
+
```json
|
|
2588
|
+
{
|
|
2589
|
+
"bgColor": {
|
|
2590
|
+
"type": "colorpicker",
|
|
2591
|
+
"label": "Banner background",
|
|
2592
|
+
"order": 1,
|
|
2593
|
+
"value": "#1a1a2ecc"
|
|
2594
|
+
},
|
|
2595
|
+
"textColor": {
|
|
2596
|
+
"type": "colorpicker",
|
|
2597
|
+
"label": "Text color",
|
|
2598
|
+
"order": 2,
|
|
2599
|
+
"value": "#ffffff"
|
|
2600
|
+
},
|
|
2601
|
+
"font": {
|
|
2602
|
+
"type": "fontpicker",
|
|
2603
|
+
"label": "Font",
|
|
2604
|
+
"order": 3,
|
|
2605
|
+
"value": "Inter"
|
|
2606
|
+
},
|
|
2607
|
+
"fontSize": {
|
|
2608
|
+
"type": "slider",
|
|
2609
|
+
"label": "Font size",
|
|
2610
|
+
"order": 4,
|
|
2611
|
+
"options": { "min": 12, "max": 40, "step": 2, "suffix": "px" },
|
|
2612
|
+
"value": 20
|
|
2613
|
+
},
|
|
2614
|
+
"holdMs": {
|
|
2615
|
+
"type": "number",
|
|
2616
|
+
"label": "Display time (ms) — used when HFX duration is missing",
|
|
2617
|
+
"order": 5,
|
|
2618
|
+
"value": 5000
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
```
|
|
2622
|
+
|
|
2623
|
+
### Data
|
|
2624
|
+
|
|
2625
|
+
```json
|
|
2626
|
+
{
|
|
2627
|
+
"bgColor": "#1a1a2ecc",
|
|
2628
|
+
"textColor": "#ffffff",
|
|
2629
|
+
"font": "Inter",
|
|
2630
|
+
"fontSize": 20,
|
|
2631
|
+
"holdMs": 5000
|
|
2632
|
+
}
|
|
2633
|
+
```
|
|
2634
|
+
|
|
2635
|
+
## Virtual Light Monitor
|
|
2636
|
+
|
|
2637
|
+
Shows an on-screen representation of a Lumia virtual light: a colored dot that matches the current color, brightness, and power state. Uses `Overlay.on("virtuallight", ...)`. Also demonstrates the first-load storage pattern so the last-known color persists across overlay reloads.
|
|
2638
|
+
|
|
2639
|
+
### HTML
|
|
2640
|
+
|
|
2641
|
+
```html
|
|
2642
|
+
<div id="wrap">
|
|
2643
|
+
<div id="dot"></div>
|
|
2644
|
+
<div id="label"></div>
|
|
2645
|
+
</div>
|
|
2646
|
+
```
|
|
2647
|
+
|
|
2648
|
+
### CSS
|
|
2649
|
+
|
|
2650
|
+
```css
|
|
2651
|
+
body {
|
|
2652
|
+
background: transparent;
|
|
2653
|
+
font-family: "{{font}}";
|
|
2654
|
+
color: #ffffff;
|
|
2655
|
+
}
|
|
2656
|
+
#wrap {
|
|
2657
|
+
display: flex;
|
|
2658
|
+
align-items: center;
|
|
2659
|
+
gap: 14px;
|
|
2660
|
+
padding: 14px 20px;
|
|
2661
|
+
background: rgba(0, 0, 0, 0.55);
|
|
2662
|
+
border-radius: 999px;
|
|
2663
|
+
width: fit-content;
|
|
2664
|
+
}
|
|
2665
|
+
#dot {
|
|
2666
|
+
width: 36px;
|
|
2667
|
+
height: 36px;
|
|
2668
|
+
border-radius: 50%;
|
|
2669
|
+
background: #000;
|
|
2670
|
+
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12);
|
|
2671
|
+
transition: background 200ms linear, opacity 200ms linear, transform 200ms ease;
|
|
2672
|
+
}
|
|
2673
|
+
#dot.off {
|
|
2674
|
+
opacity: 0.25;
|
|
2675
|
+
transform: scale(0.85);
|
|
2676
|
+
}
|
|
2677
|
+
#label {
|
|
2678
|
+
font-size: 16px;
|
|
2679
|
+
white-space: nowrap;
|
|
2680
|
+
}
|
|
2681
|
+
```
|
|
2682
|
+
|
|
2683
|
+
### JS
|
|
2684
|
+
|
|
2685
|
+
```js
|
|
2686
|
+
const dot = document.getElementById("dot");
|
|
2687
|
+
const label = document.getElementById("label");
|
|
2688
|
+
const STORAGE_KEY = "last_light_state";
|
|
2689
|
+
|
|
2690
|
+
// Default state (first load)
|
|
2691
|
+
let state = {
|
|
2692
|
+
uuid: "",
|
|
2693
|
+
r: 0,
|
|
2694
|
+
g: 0,
|
|
2695
|
+
b: 0,
|
|
2696
|
+
brightness: 100,
|
|
2697
|
+
power: true,
|
|
2698
|
+
};
|
|
2699
|
+
|
|
2700
|
+
// Seed storage on first load so we never see the red "no value" toast
|
|
2701
|
+
const saved = await Overlay.getStorage(STORAGE_KEY);
|
|
2702
|
+
if (saved == null) {
|
|
2703
|
+
await Overlay.saveStorage(STORAGE_KEY, state);
|
|
2704
|
+
} else {
|
|
2705
|
+
state = saved;
|
|
2706
|
+
}
|
|
2707
|
+
render();
|
|
2708
|
+
|
|
2709
|
+
function render() {
|
|
2710
|
+
const { r, g, b, brightness, power, uuid } = state;
|
|
2711
|
+
dot.style.background = `rgb(${r}, ${g}, ${b})`;
|
|
2712
|
+
dot.classList.toggle("off", power === false);
|
|
2713
|
+
const hex = `#${[r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
2714
|
+
const name = uuid || "No light yet";
|
|
2715
|
+
label.textContent = power === false ? `${name} — off` : `${name} — ${hex} @ ${brightness}%`;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
Overlay.on("virtuallight", async (data) => {
|
|
2719
|
+
state = {
|
|
2720
|
+
uuid: data.uuid || state.uuid,
|
|
2721
|
+
r: data.color?.r ?? state.r,
|
|
2722
|
+
g: data.color?.g ?? state.g,
|
|
2723
|
+
b: data.color?.b ?? state.b,
|
|
2724
|
+
brightness: data.brightness ?? state.brightness,
|
|
2725
|
+
power: data.power !== false,
|
|
2726
|
+
};
|
|
2727
|
+
render();
|
|
2728
|
+
await Overlay.saveStorage(STORAGE_KEY, state);
|
|
2729
|
+
});
|
|
2730
|
+
```
|
|
2731
|
+
|
|
2732
|
+
### Configs
|
|
2733
|
+
|
|
2734
|
+
```json
|
|
2735
|
+
{
|
|
2736
|
+
"font": {
|
|
2737
|
+
"type": "fontpicker",
|
|
2738
|
+
"label": "Font",
|
|
2739
|
+
"order": 1,
|
|
2740
|
+
"value": "Inter"
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
```
|
|
2744
|
+
|
|
2745
|
+
### Data
|
|
2746
|
+
|
|
2747
|
+
```json
|
|
2748
|
+
{
|
|
2749
|
+
"font": "Inter"
|
|
2750
|
+
}
|
|
2751
|
+
```
|
|
2752
|
+
|
|
2753
|
+
## Loyalty Points Leaderboard
|
|
2754
|
+
|
|
2755
|
+
Uses `Overlay.getLoyaltyPoints` + `Overlay.addLoyaltyPoints`. Chat commands:
|
|
2756
|
+
|
|
2757
|
+
- `!points` — the user's current balance is posted back to chat via `Overlay.chatbot`.
|
|
2758
|
+
- `!give @user <n>` — transfer N points from the sender to `@user` (moderator-only).
|
|
2759
|
+
|
|
2760
|
+
Also renders a top-10 leaderboard from a locally cached list of users who have been seen in chat (loyalty points themselves are managed by Lumia — we just display the cache). Cache is persisted with `Overlay.saveStorage`.
|
|
2761
|
+
|
|
2762
|
+
### HTML
|
|
2763
|
+
|
|
2764
|
+
```html
|
|
2765
|
+
<div id="board">
|
|
2766
|
+
<h2>Top Loyalty</h2>
|
|
2767
|
+
<ol id="list"></ol>
|
|
2768
|
+
</div>
|
|
2769
|
+
```
|
|
2770
|
+
|
|
2771
|
+
### CSS
|
|
2772
|
+
|
|
2773
|
+
```css
|
|
2774
|
+
body {
|
|
2775
|
+
background: transparent;
|
|
2776
|
+
font-family: "{{font}}";
|
|
2777
|
+
color: #ffffff;
|
|
2778
|
+
}
|
|
2779
|
+
#board {
|
|
2780
|
+
width: 360px;
|
|
2781
|
+
padding: 20px 24px;
|
|
2782
|
+
background: rgba(10, 10, 20, 0.85);
|
|
2783
|
+
border-radius: 16px;
|
|
2784
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
2785
|
+
}
|
|
2786
|
+
#board h2 {
|
|
2787
|
+
margin: 0 0 12px;
|
|
2788
|
+
font-size: 20px;
|
|
2789
|
+
color: {{accentColor}};
|
|
2790
|
+
}
|
|
2791
|
+
#list {
|
|
2792
|
+
margin: 0;
|
|
2793
|
+
padding: 0 0 0 24px;
|
|
2794
|
+
font-size: 16px;
|
|
2795
|
+
line-height: 1.6;
|
|
2796
|
+
}
|
|
2797
|
+
#list li .amount {
|
|
2798
|
+
float: right;
|
|
2799
|
+
font-weight: 700;
|
|
2800
|
+
color: {{accentColor}};
|
|
2801
|
+
}
|
|
2802
|
+
```
|
|
2803
|
+
|
|
2804
|
+
### JS
|
|
2805
|
+
|
|
2806
|
+
```js
|
|
2807
|
+
const listEl = document.getElementById("list");
|
|
2808
|
+
const STORAGE_KEY = "loyalty_seen_users";
|
|
2809
|
+
|
|
2810
|
+
// first-load seed
|
|
2811
|
+
let seen = await Overlay.getStorage(STORAGE_KEY);
|
|
2812
|
+
if (seen == null) {
|
|
2813
|
+
seen = [];
|
|
2814
|
+
await Overlay.saveStorage(STORAGE_KEY, seen);
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
async function refresh() {
|
|
2818
|
+
const rows = [];
|
|
2819
|
+
for (const u of seen) {
|
|
2820
|
+
const pts = Number(await Overlay.getLoyaltyPoints({ username: u.username, platform: u.platform })) || 0;
|
|
2821
|
+
rows.push({ ...u, pts });
|
|
2822
|
+
}
|
|
2823
|
+
rows.sort((a, b) => b.pts - a.pts);
|
|
2824
|
+
|
|
2825
|
+
listEl.textContent = "";
|
|
2826
|
+
rows.slice(0, 10).forEach((r) => {
|
|
2827
|
+
const li = document.createElement("li");
|
|
2828
|
+
const name = document.createElement("span");
|
|
2829
|
+
name.textContent = r.username;
|
|
2830
|
+
const amt = document.createElement("span");
|
|
2831
|
+
amt.className = "amount";
|
|
2832
|
+
amt.textContent = r.pts.toLocaleString();
|
|
2833
|
+
li.append(name, amt);
|
|
2834
|
+
listEl.appendChild(li);
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
async function remember(username, platform) {
|
|
2839
|
+
if (!username || !platform) return;
|
|
2840
|
+
const existing = seen.find((u) => u.username === username && u.platform === platform);
|
|
2841
|
+
if (existing) return;
|
|
2842
|
+
seen.push({ username, platform });
|
|
2843
|
+
await Overlay.saveStorage(STORAGE_KEY, seen);
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
Overlay.on("chat", async (data) => {
|
|
2847
|
+
const msg = (data.message || "").trim();
|
|
2848
|
+
const username = data.username;
|
|
2849
|
+
const platform = data.platform || "twitch";
|
|
2850
|
+
const isMod = data.userLevels?.mod || data.userLevels?.isSelf;
|
|
2851
|
+
|
|
2852
|
+
await remember(username, platform);
|
|
2853
|
+
|
|
2854
|
+
// !points — look up caller's balance
|
|
2855
|
+
if (msg.toLowerCase() === "!points") {
|
|
2856
|
+
const pts = Number(await Overlay.getLoyaltyPoints({ username, platform })) || 0;
|
|
2857
|
+
await Overlay.chatbot({ message: `${username} has ${pts} points`, platform });
|
|
2858
|
+
await refresh();
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// !give @user 50 — moderator transfer
|
|
2863
|
+
if (msg.toLowerCase().startsWith("!give ") && isMod) {
|
|
2864
|
+
const parts = msg.split(/\s+/);
|
|
2865
|
+
const target = (parts[1] || "").replace(/^@/, "");
|
|
2866
|
+
const amount = Math.max(0, Math.floor(Number(parts[2]) || 0));
|
|
2867
|
+
if (!target || !amount) {
|
|
2868
|
+
await Overlay.chatbot({ message: "Usage: !give @user 50", platform });
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
await Overlay.addLoyaltyPoints({ value: -amount, username, platform });
|
|
2872
|
+
await Overlay.addLoyaltyPoints({ value: amount, username: target, platform });
|
|
2873
|
+
await remember(target, platform);
|
|
2874
|
+
await Overlay.chatbot({ message: `${username} gave ${amount} points to ${target}`, platform });
|
|
2875
|
+
await refresh();
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
|
|
2879
|
+
// Refresh on load and every 30s so balances stay current
|
|
2880
|
+
await refresh();
|
|
2881
|
+
setInterval(refresh, 30000);
|
|
2882
|
+
```
|
|
2883
|
+
|
|
2884
|
+
### Configs
|
|
2885
|
+
|
|
2886
|
+
```json
|
|
2887
|
+
{
|
|
2888
|
+
"accentColor": {
|
|
2889
|
+
"type": "colorpicker",
|
|
2890
|
+
"label": "Accent color",
|
|
2891
|
+
"order": 1,
|
|
2892
|
+
"value": "#ffcc00"
|
|
2893
|
+
},
|
|
2894
|
+
"font": {
|
|
2895
|
+
"type": "fontpicker",
|
|
2896
|
+
"label": "Font",
|
|
2897
|
+
"order": 2,
|
|
2898
|
+
"value": "Inter"
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
```
|
|
2902
|
+
|
|
2903
|
+
### Data
|
|
2904
|
+
|
|
2905
|
+
```json
|
|
2906
|
+
{
|
|
2907
|
+
"accentColor": "#ffcc00",
|
|
2908
|
+
"font": "Inter"
|
|
2909
|
+
}
|
|
2910
|
+
```
|