openvibers/openvibe.live Commits
Josh Vegas (fx_vegas): a one-of-a-kind name effect, slot reels around the name under casino lights (#313)
Changes
@@@ -175,6 +175,68 @@50% { background-position: 300% 50%; }}++/* Josh Vegas: one of a kind (2026-10-22). The name in casino gold under marquee lights, between two slot reels that++ spin through cherries, bells, diamonds, slot machines and lemons and land on 7. Only its one holder wears it (fx_vegas is++ never in the catalog). The reels are pseudo-elements, so they carry their own fill, pause and reduced-motion rules. */++.name-fx-vegas {++ background: linear-gradient(90deg, #fff1b8 0%, #ffcf3f 22%, #ff9d00 40%, #ff3d7f 52%, #ffcf3f 66%, #fff1b8 82%, #ffcf3f 100%);++ background-size: 280% 100%;++ -webkit-background-clip: text;++ background-clip: text;++ -webkit-text-fill-color: transparent;++ font-weight: 800;++ letter-spacing: .02em;++ white-space: nowrap;++ animation: nameFxVegasShine 2.6s linear infinite, nameFxVegasLights 1.4s steps(1, end) infinite;++}++.name-fx-vegas::before,++.name-fx-vegas::after {++ content: '7';++ display: inline-block;++ box-sizing: border-box;++ width: 2em;++ margin-right: .4em;++ border: 1px solid #ffcf3f;++ border-radius: 3px;++ background: linear-gradient(180deg, #3d0512 0%, #8f0f2c 48%, #3d0512 100%);++ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .45), 0 0 5px rgba(255, 207, 63, .6);++ color: #ffe27a;++ -webkit-text-fill-color: #ffe27a;++ font-size: .72em;++ font-weight: 900;++ letter-spacing: 0;++ line-height: 1.35;++ text-align: center;++ overflow: hidden;++ vertical-align: .1em;++ animation: nameFxVegasReel 3.2s steps(1, end) infinite;++}++.name-fx-vegas::after { margin: 0 0 0 .4em; animation-delay: .35s; }++@keyframes nameFxVegasShine {++ 0% { background-position: 0% 50%; }++ 100% { background-position: 280% 50%; }++}++/* Marquee bulbs: the glow steps between gold and pink, like the lights around a casino sign. */++@keyframes nameFxVegasLights {++ 0% { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)) drop-shadow(0 0 9px rgba(255, 157, 0, .55)); }++ 50% { filter: drop-shadow(0 0 3px rgba(255, 61, 127, .9)) drop-shadow(0 0 9px rgba(255, 61, 127, .5)); }++}++/* Each reel spins through the symbols, then lands on 7 and holds it (a browser that cannot animate `content` shows 7). */++@keyframes nameFxVegasReel {++ 0% { content: '🍒'; }++ 7% { content: '🔔'; }++ 14% { content: '💎'; }++ 21% { content: '🎰'; }++ 28% { content: '🍋'; }++ 35%, 100% { content: '7'; }++}++.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,++.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }++@media (prefers-reduced-motion: reduce) {++ .name-fx-vegas, .name-fx-vegas::before, .name-fx-vegas::after { animation: none !important; }++ .name-fx-vegas { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)); }++}++/* ══════════════════════════════════════════════════════════════PARTICLE EFFECTS — spawned around chat usernames@@@ -198,6 +198,68 @@ body.no-shadow .msg { text-shadow: none; }}@keyframes nameFxGalaxy { 0%{background-position:0% 50%} 100%{background-position:400% 50%} }++/* Josh Vegas: one of a kind (2026-10-22). The name in casino gold under marquee lights, between two slot reels that++ spin through cherries, bells, diamonds, slot machines and lemons and land on 7. Only its one holder wears it (fx_vegas is++ never in the catalog). The reels are pseudo-elements, so they carry their own fill, pause and reduced-motion rules. */++.name-fx-vegas {++ background: linear-gradient(90deg, #fff1b8 0%, #ffcf3f 22%, #ff9d00 40%, #ff3d7f 52%, #ffcf3f 66%, #fff1b8 82%, #ffcf3f 100%);++ background-size: 280% 100%;++ -webkit-background-clip: text;++ background-clip: text;++ -webkit-text-fill-color: transparent;++ font-weight: 800;++ letter-spacing: .02em;++ white-space: nowrap;++ animation: nameFxVegasShine 2.6s linear infinite, nameFxVegasLights 1.4s steps(1, end) infinite;++}++.name-fx-vegas::before,++.name-fx-vegas::after {++ content: '7';++ display: inline-block;++ box-sizing: border-box;++ width: 2em;++ margin-right: .4em;++ border: 1px solid #ffcf3f;++ border-radius: 3px;++ background: linear-gradient(180deg, #3d0512 0%, #8f0f2c 48%, #3d0512 100%);++ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .45), 0 0 5px rgba(255, 207, 63, .6);++ color: #ffe27a;++ -webkit-text-fill-color: #ffe27a;++ font-size: .72em;++ font-weight: 900;++ letter-spacing: 0;++ line-height: 1.35;++ text-align: center;++ overflow: hidden;++ vertical-align: .1em;++ animation: nameFxVegasReel 3.2s steps(1, end) infinite;++}++.name-fx-vegas::after { margin: 0 0 0 .4em; animation-delay: .35s; }++@keyframes nameFxVegasShine {++ 0% { background-position: 0% 50%; }++ 100% { background-position: 280% 50%; }++}++/* Marquee bulbs: the glow steps between gold and pink, like the lights around a casino sign. */++@keyframes nameFxVegasLights {++ 0% { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)) drop-shadow(0 0 9px rgba(255, 157, 0, .55)); }++ 50% { filter: drop-shadow(0 0 3px rgba(255, 61, 127, .9)) drop-shadow(0 0 9px rgba(255, 61, 127, .5)); }++}++/* Each reel spins through the symbols, then lands on 7 and holds it (a browser that cannot animate `content` shows 7). */++@keyframes nameFxVegasReel {++ 0% { content: '🍒'; }++ 7% { content: '🔔'; }++ 14% { content: '💎'; }++ 21% { content: '🎰'; }++ 28% { content: '🍋'; }++ 35%, 100% { content: '7'; }++}++.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,++.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }++@media (prefers-reduced-motion: reduce) {++ .name-fx-vegas, .name-fx-vegas::before, .name-fx-vegas::after { animation: none !important; }++ .name-fx-vegas { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)); }++}++.name-fx-void {color: #9900ff !important;text-shadow: 0 0 8px #6600cc, 0 0 16px #33006688, 0 0 24px #11003344;@@@ -20,6 +20,7 @@ router.get('/catalog', async (req, res) => {const earn = await cosmetics.earnable();const catalog = {};for (const [id, c] of Object.entries(cosmetics.COSMETICS)) {++ if (c.exclusive) continue; // one of a kind: only its holder sees it, in their own inventoryif (!catalog[c.category]) catalog[c.category] = [];catalog[c.category].push({ itemId: id, ...c, ...(earn[id] ? { earn: earn[id] } : {}) });}@@@ -34,6 +34,9 @@ const COSMETICS = {fx_glitch: { name: 'Glitch Name', emoji: '📟', category: 'name_effect', tier: 3, cssClass: 'name-fx-glitch', desc: 'Digital glitch' },fx_hologram: { name: 'Hologram Name', emoji: '🔮', category: 'name_effect', tier: 4, cssClass: 'name-fx-hologram', desc: 'Holographic shimmer' },fx_divine: { name: 'Divine Name', emoji: '✝️', category: 'name_effect', tier: 5, cssClass: 'name-fx-divine', desc: 'Divine radiance' },++ // One of a kind (2026-10-22, the owner's gift to his friend Josh, "Josh Vegas" since they were kids): granted to++ // its one holder, never listed in the catalog nor earnable.++ fx_vegas: { name: 'Josh Vegas', emoji: '🎰', category: 'name_effect', tier: 6, cssClass: 'name-fx-vegas', desc: 'Slot reels and casino lights', exclusive: true },// ── Particle Effects ─────────────────────────────────px_sparkle: { name: 'Sparkle', emoji: '✨', category: 'particle', tier: 1, cssClass: 'px-sparkle', chars: '✦✧⋆', desc: 'Sparkle particles' },nnew file mode 100644@@@ -0,0 +1,39 @@++'use strict';++// A one-of-a-kind cosmetic (fx_vegas, "Josh Vegas", 2026-10-22): it is in Live's catalog of renderers, so the person++// who holds it shows it in chat (getCosmeticProfile maps a worn alias through the catalog), but GET++// /api/cosmetics/catalog never lists it, and its++// class is drawn by both of Live's stylesheets (the site's cosmetics.css and the OBS chat overlay).++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const http = require('http');++const express = require('express');++++console.warn = () => {};++const cosmetics = require('../server/monetization/cosmetics');++++(async () => {++ const vegas = cosmetics.COSMETICS.fx_vegas;++ assert.ok(vegas && vegas.exclusive === true && vegas.category === 'name_effect', 'fx_vegas is a one-of-a-kind name effect');++ assert.match(vegas.cssClass, /^name-fx-[a-z]{2,24}$/, 'its class passes the pattern Chat and Network accept');++++ const app = express();++ app.use('/api/cosmetics', require('../server/monetization/cosmetics-routes'));++ const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });++ try {++ const res = await fetch(`http://127.0.0.1:${server.address().port}/api/cosmetics/catalog`);++ const { catalog } = await res.json();++ const listed = Object.values(catalog).flat().map((c) => c.itemId);++ assert.ok(listed.includes('fx_galaxy'), 'the catalog still lists the others');++ assert.ok(!listed.includes('fx_vegas'), 'a one-of-a-kind item is never listed');++ } finally {++ server.close();++ }++++ for (const file of ['public/css/cosmetics.css', 'public/obs/chat.html']) {++ const css = fs.readFileSync(path.join(__dirname, '..', file), 'utf8');++ assert.ok(css.includes('.name-fx-vegas {') && css.includes('@keyframes nameFxVegasReel'), `${file} draws the effect`);++ assert.ok(css.includes('prefers-reduced-motion: reduce') && /\.name-fx-vegas::before, \.name-fx-vegas::after \{ animation: none/.test(css), `${file} stops the reels for reduced motion`);++ }++ console.log('cosmetics exclusive: all checks passed');++})().catch((e) => { console.error(e); process.exit(1); });
@@ -175,6 +175,68 @@
@@ -175,6 +175,68 @@
50% { background-position: 300% 50%; }
50% { background-position: 300% 50%; }
}
}
+/* Josh Vegas: one of a kind (2026-10-22). The name in casino gold under marquee lights, between two slot reels that
+ spin through cherries, bells, diamonds, slot machines and lemons and land on 7. Only its one holder wears it (fx_vegas is
+ never in the catalog). The reels are pseudo-elements, so they carry their own fill, pause and reduced-motion rules. */
+.name-fx-vegas {
+ background: linear-gradient(90deg, #fff1b8 0%, #ffcf3f 22%, #ff9d00 40%, #ff3d7f 52%, #ffcf3f 66%, #fff1b8 82%, #ffcf3f 100%);
+ background-size: 280% 100%;
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-weight: 800;
+ letter-spacing: .02em;
+ white-space: nowrap;
+ animation: nameFxVegasShine 2.6s linear infinite, nameFxVegasLights 1.4s steps(1, end) infinite;
+}
+.name-fx-vegas::before,
+.name-fx-vegas::after {
+ content: '7';
+ display: inline-block;
+ box-sizing: border-box;
+ width: 2em;
+ margin-right: .4em;
+ border: 1px solid #ffcf3f;
+ border-radius: 3px;
+ background: linear-gradient(180deg, #3d0512 0%, #8f0f2c 48%, #3d0512 100%);
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .45), 0 0 5px rgba(255, 207, 63, .6);
+ color: #ffe27a;
+ -webkit-text-fill-color: #ffe27a;
+ font-size: .72em;
+ font-weight: 900;
+ letter-spacing: 0;
+ line-height: 1.35;
+ text-align: center;
+ overflow: hidden;
+ vertical-align: .1em;
+ animation: nameFxVegasReel 3.2s steps(1, end) infinite;
+}
+.name-fx-vegas::after { margin: 0 0 0 .4em; animation-delay: .35s; }
+@keyframes nameFxVegasShine {
+ 0% { background-position: 0% 50%; }
+ 100% { background-position: 280% 50%; }
+}
+/* Marquee bulbs: the glow steps between gold and pink, like the lights around a casino sign. */
+@keyframes nameFxVegasLights {
+ 0% { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)) drop-shadow(0 0 9px rgba(255, 157, 0, .55)); }
+ 50% { filter: drop-shadow(0 0 3px rgba(255, 61, 127, .9)) drop-shadow(0 0 9px rgba(255, 61, 127, .5)); }
+}
+/* Each reel spins through the symbols, then lands on 7 and holds it (a browser that cannot animate `content` shows 7). */
+@keyframes nameFxVegasReel {
+ 0% { content: '🍒'; }
+ 7% { content: '🔔'; }
+ 14% { content: '💎'; }
+ 21% { content: '🎰'; }
+ 28% { content: '🍋'; }
+ 35%, 100% { content: '7'; }
+}
+.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,
+.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }
+@media (prefers-reduced-motion: reduce) {
+ .name-fx-vegas, .name-fx-vegas::before, .name-fx-vegas::after { animation: none !important; }
+ .name-fx-vegas { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)); }
+}
+
/* ══════════════════════════════════════════════════════════════
/* ══════════════════════════════════════════════════════════════
PARTICLE EFFECTS — spawned around chat usernames
PARTICLE EFFECTS — spawned around chat usernames
@@ -198,6 +198,68 @@ body.no-shadow .msg { text-shadow: none; }
@@ -198,6 +198,68 @@ body.no-shadow .msg { text-shadow: none; }
}
}
@keyframes nameFxGalaxy { 0%{background-position:0% 50%} 100%{background-position:400% 50%} }
@keyframes nameFxGalaxy { 0%{background-position:0% 50%} 100%{background-position:400% 50%} }
+/* Josh Vegas: one of a kind (2026-10-22). The name in casino gold under marquee lights, between two slot reels that
+ spin through cherries, bells, diamonds, slot machines and lemons and land on 7. Only its one holder wears it (fx_vegas is
+ never in the catalog). The reels are pseudo-elements, so they carry their own fill, pause and reduced-motion rules. */
+.name-fx-vegas {
+ background: linear-gradient(90deg, #fff1b8 0%, #ffcf3f 22%, #ff9d00 40%, #ff3d7f 52%, #ffcf3f 66%, #fff1b8 82%, #ffcf3f 100%);
+ background-size: 280% 100%;
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-weight: 800;
+ letter-spacing: .02em;
+ white-space: nowrap;
+ animation: nameFxVegasShine 2.6s linear infinite, nameFxVegasLights 1.4s steps(1, end) infinite;
+}
+.name-fx-vegas::before,
+.name-fx-vegas::after {
+ content: '7';
+ display: inline-block;
+ box-sizing: border-box;
+ width: 2em;
+ margin-right: .4em;
+ border: 1px solid #ffcf3f;
+ border-radius: 3px;
+ background: linear-gradient(180deg, #3d0512 0%, #8f0f2c 48%, #3d0512 100%);
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .45), 0 0 5px rgba(255, 207, 63, .6);
+ color: #ffe27a;
+ -webkit-text-fill-color: #ffe27a;
+ font-size: .72em;
+ font-weight: 900;
+ letter-spacing: 0;
+ line-height: 1.35;
+ text-align: center;
+ overflow: hidden;
+ vertical-align: .1em;
+ animation: nameFxVegasReel 3.2s steps(1, end) infinite;
+}
+.name-fx-vegas::after { margin: 0 0 0 .4em; animation-delay: .35s; }
+@keyframes nameFxVegasShine {
+ 0% { background-position: 0% 50%; }
+ 100% { background-position: 280% 50%; }
+}
+/* Marquee bulbs: the glow steps between gold and pink, like the lights around a casino sign. */
+@keyframes nameFxVegasLights {
+ 0% { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)) drop-shadow(0 0 9px rgba(255, 157, 0, .55)); }
+ 50% { filter: drop-shadow(0 0 3px rgba(255, 61, 127, .9)) drop-shadow(0 0 9px rgba(255, 61, 127, .5)); }
+}
+/* Each reel spins through the symbols, then lands on 7 and holds it (a browser that cannot animate `content` shows 7). */
+@keyframes nameFxVegasReel {
+ 0% { content: '🍒'; }
+ 7% { content: '🔔'; }
+ 14% { content: '💎'; }
+ 21% { content: '🎰'; }
+ 28% { content: '🍋'; }
+ 35%, 100% { content: '7'; }
+}
+.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,
+.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }
+@media (prefers-reduced-motion: reduce) {
+ .name-fx-vegas, .name-fx-vegas::before, .name-fx-vegas::after { animation: none !important; }
+ .name-fx-vegas { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)); }
+}
+
.name-fx-void {
.name-fx-void {
color: #9900ff !important;
color: #9900ff !important;
text-shadow: 0 0 8px #6600cc, 0 0 16px #33006688, 0 0 24px #11003344;
text-shadow: 0 0 8px #6600cc, 0 0 16px #33006688, 0 0 24px #11003344;
@@ -20,6 +20,7 @@ router.get('/catalog', async (req, res) => {
@@ -20,6 +20,7 @@ router.get('/catalog', async (req, res) => {
const earn = await cosmetics.earnable();
const earn = await cosmetics.earnable();
const catalog = {};
const catalog = {};
for (const [id, c] of Object.entries(cosmetics.COSMETICS)) {
for (const [id, c] of Object.entries(cosmetics.COSMETICS)) {
+ if (c.exclusive) continue; // one of a kind: only its holder sees it, in their own inventory
if (!catalog[c.category]) catalog[c.category] = [];
if (!catalog[c.category]) catalog[c.category] = [];
catalog[c.category].push({ itemId: id, ...c, ...(earn[id] ? { earn: earn[id] } : {}) });
catalog[c.category].push({ itemId: id, ...c, ...(earn[id] ? { earn: earn[id] } : {}) });
}
}
@@ -34,6 +34,9 @@ const COSMETICS = {
@@ -34,6 +34,9 @@ const COSMETICS = {
fx_glitch: { name: 'Glitch Name', emoji: '📟', category: 'name_effect', tier: 3, cssClass: 'name-fx-glitch', desc: 'Digital glitch' },
fx_glitch: { name: 'Glitch Name', emoji: '📟', category: 'name_effect', tier: 3, cssClass: 'name-fx-glitch', desc: 'Digital glitch' },
fx_hologram: { name: 'Hologram Name', emoji: '🔮', category: 'name_effect', tier: 4, cssClass: 'name-fx-hologram', desc: 'Holographic shimmer' },
fx_hologram: { name: 'Hologram Name', emoji: '🔮', category: 'name_effect', tier: 4, cssClass: 'name-fx-hologram', desc: 'Holographic shimmer' },
fx_divine: { name: 'Divine Name', emoji: '✝️', category: 'name_effect', tier: 5, cssClass: 'name-fx-divine', desc: 'Divine radiance' },
fx_divine: { name: 'Divine Name', emoji: '✝️', category: 'name_effect', tier: 5, cssClass: 'name-fx-divine', desc: 'Divine radiance' },
+ // One of a kind (2026-10-22, the owner's gift to his friend Josh, "Josh Vegas" since they were kids): granted to
+ // its one holder, never listed in the catalog nor earnable.
+ fx_vegas: { name: 'Josh Vegas', emoji: '🎰', category: 'name_effect', tier: 6, cssClass: 'name-fx-vegas', desc: 'Slot reels and casino lights', exclusive: true },
// ── Particle Effects ─────────────────────────────────
// ── Particle Effects ─────────────────────────────────
px_sparkle: { name: 'Sparkle', emoji: '✨', category: 'particle', tier: 1, cssClass: 'px-sparkle', chars: '✦✧⋆', desc: 'Sparkle particles' },
px_sparkle: { name: 'Sparkle', emoji: '✨', category: 'particle', tier: 1, cssClass: 'px-sparkle', chars: '✦✧⋆', desc: 'Sparkle particles' },
new file mode 100644
new file mode 100644
@@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
+'use strict';
+// A one-of-a-kind cosmetic (fx_vegas, "Josh Vegas", 2026-10-22): it is in Live's catalog of renderers, so the person
+// who holds it shows it in chat (getCosmeticProfile maps a worn alias through the catalog), but GET
+// /api/cosmetics/catalog never lists it, and its
+// class is drawn by both of Live's stylesheets (the site's cosmetics.css and the OBS chat overlay).
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const http = require('http');
+const express = require('express');
+
+console.warn = () => {};
+const cosmetics = require('../server/monetization/cosmetics');
+
+(async () => {
+ const vegas = cosmetics.COSMETICS.fx_vegas;
+ assert.ok(vegas && vegas.exclusive === true && vegas.category === 'name_effect', 'fx_vegas is a one-of-a-kind name effect');
+ assert.match(vegas.cssClass, /^name-fx-[a-z]{2,24}$/, 'its class passes the pattern Chat and Network accept');
+
+ const app = express();
+ app.use('/api/cosmetics', require('../server/monetization/cosmetics-routes'));
+ const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
+ try {
+ const res = await fetch(`http://127.0.0.1:${server.address().port}/api/cosmetics/catalog`);
+ const { catalog } = await res.json();
+ const listed = Object.values(catalog).flat().map((c) => c.itemId);
+ assert.ok(listed.includes('fx_galaxy'), 'the catalog still lists the others');
+ assert.ok(!listed.includes('fx_vegas'), 'a one-of-a-kind item is never listed');
+ } finally {
+ server.close();
+ }
+
+ for (const file of ['public/css/cosmetics.css', 'public/obs/chat.html']) {
+ const css = fs.readFileSync(path.join(__dirname, '..', file), 'utf8');
+ assert.ok(css.includes('.name-fx-vegas {') && css.includes('@keyframes nameFxVegasReel'), `${file} draws the effect`);
+ assert.ok(css.includes('prefers-reduced-motion: reduce') && /\.name-fx-vegas::before, \.name-fx-vegas::after \{ animation: none/.test(css), `${file} stops the reels for reduced motion`);
+ }
+ console.log('cosmetics exclusive: all checks passed');
+})().catch((e) => { console.error(e); process.exit(1); });