openvibers/openvibe.live Commits

The Josh Vegas set: a jackpot pop on the name, casino chips, a top hat and an announcer voice (#314)

OP OpenVibers committed 22 days ago · d3ce0e3605 · 4 files, +48 −10

Changes

ddiff --git a/public/css/cosmetics.css b/public/css/cosmetics.css
iindex 8b2d6419..adadc407 100644
---- a/public/css/cosmetics.css
++++ b/public/css/cosmetics.css
@@@ -176,18 +176,21 @@
}
/* 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. */
++ spin through cherries, bells, diamonds, slot machines and lemons and land on 7, and the name pops like a jackpot the
++ moment they do. 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;
++ display: inline-block;
font-weight: 800;
letter-spacing: .02em;
white-space: nowrap;
-- animation: nameFxVegasShine 2.6s linear infinite, nameFxVegasLights 1.4s steps(1, end) infinite;
++ transform-origin: 50% 60%;
++ animation: nameFxVegasShine 2.6s linear infinite, nameFxVegasLights 1.4s steps(1, end) infinite, nameFxVegasJackpot 3.2s ease-out infinite;
}
.name-fx-vegas::before,
.name-fx-vegas::after {
@@@ -221,6 +224,13 @@
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)); }
}
++/* Jackpot: as the reels land on 7, the whole name pops and settles (the reels' cycle, 3.2 s). */
++@keyframes nameFxVegasJackpot {
++ 0%, 34% { transform: scale(1); }
++ 38% { transform: scale(1.14) rotate(-2deg); }
++ 43% { transform: scale(.97) rotate(1deg); }
++ 48%, 100% { transform: scale(1); }
++}
/* 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: '🍒'; }
@@@ -266,6 +276,8 @@
.px-flames .chat-particle { color: #ff6600; }
.px-stars .chat-particle { color: #fbbf24; }
.px-void .chat-particle { color: #9900ff; }
++/* Josh Vegas particles: card suits, dice and coins thrown from the name in casino gold. */
++.px-vegas .chat-particle { color: #ffcf3f; text-shadow: 0 0 4px rgba(255, 61, 127, .75); }
/* ══════════════════════════════════════════════════════════════
ddiff --git a/public/obs/chat.html b/public/obs/chat.html
iindex 97a97992..97f47ca5 100644
---- a/public/obs/chat.html
++++ b/public/obs/chat.html
@@@ -199,18 +199,21 @@ 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. */
++ spin through cherries, bells, diamonds, slot machines and lemons and land on 7, and the name pops like a jackpot the
++ moment they do. 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;
++ display: inline-block;
font-weight: 800;
letter-spacing: .02em;
white-space: nowrap;
-- animation: nameFxVegasShine 2.6s linear infinite, nameFxVegasLights 1.4s steps(1, end) infinite;
++ transform-origin: 50% 60%;
++ animation: nameFxVegasShine 2.6s linear infinite, nameFxVegasLights 1.4s steps(1, end) infinite, nameFxVegasJackpot 3.2s ease-out infinite;
}
.name-fx-vegas::before,
.name-fx-vegas::after {
@@@ -244,6 +247,13 @@ body.no-shadow .msg { text-shadow: none; }
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)); }
}
++/* Jackpot: as the reels land on 7, the whole name pops and settles (the reels' cycle, 3.2 s). */
++@keyframes nameFxVegasJackpot {
++ 0%, 34% { transform: scale(1); }
++ 38% { transform: scale(1.14) rotate(-2deg); }
++ 43% { transform: scale(.97) rotate(1deg); }
++ 48%, 100% { transform: scale(1); }
++}
/* 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: '🍒'; }
@@@ -350,6 +360,8 @@ body.no-shadow .msg { text-shadow: none; }
.px-flames .chat-particle { color: #ff6600; }
.px-stars .chat-particle { color: #fbbf24; }
.px-void .chat-particle { color: #9900ff; }
++/* Josh Vegas particles: card suits, dice and coins thrown from the name in casino gold. */
++.px-vegas .chat-particle { color: #ffcf3f; text-shadow: 0 0 4px rgba(255, 61, 127, .75); }
/* ══════════════════════════════════════════════════════════════
HAT EFFECTS
ddiff --git a/server/monetization/cosmetics.js b/server/monetization/cosmetics.js
iindex c6c8cbd0..423e292b 100644
---- a/server/monetization/cosmetics.js
++++ b/server/monetization/cosmetics.js
@@@ -34,8 +34,8 @@ 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.
++ // One of a kind (2026-10-22, the owner's gift to his friend Josh, "Josh Vegas" since they were kids), with its set
++ // (px_vegas, hat_vegas, voice_vegas): granted to their 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 ─────────────────────────────────
@@@ -44,6 +44,7 @@ const COSMETICS = {
px_flames: { name: 'Flames', emoji: '🔥', category: 'particle', tier: 2, cssClass: 'px-flames', chars: '🔥🔸⚡', desc: 'Flame embers' },
px_stars: { name: 'Stars', emoji: '⭐', category: 'particle', tier: 2, cssClass: 'px-stars', chars: '★☆✩', desc: 'Orbiting stars' },
px_void: { name: 'Void', emoji: '🕳️', category: 'particle', tier: 3, cssClass: 'px-void', chars: '◉◎⊙', desc: 'Dark matter' },
++ px_vegas: { name: 'Josh Vegas Chips', emoji: '🎲', category: 'particle', tier: 6, cssClass: 'px-vegas', chars: '♠♥♦♣🎲🪙💰', desc: 'Cards, dice and coins', exclusive: true },
// ── Hats ─────────────────────────────────────────────
hat_basic_cap: { name: 'Basic Cap', emoji: '🧢', category: 'hat', tier: 1, hatChar: '🧢', desc: 'Simple cap' },
@@@ -52,6 +53,7 @@ const COSMETICS = {
hat_crown: { name: 'Royal Crown', emoji: '👑', category: 'hat', tier: 4, hatChar: '👑', animated: 'pulse', desc: 'Royalty' },
hat_halo: { name: 'Halo', emoji: '😇', category: 'hat', tier: 5, hatChar: '😇', animated: 'float', desc: 'Angelic' },
hat_void_crown: { name: 'Void Crown', emoji: '🕳️', category: 'hat', tier: 6, hatChar: '🕳️', animated: 'warp', desc: 'From the dungeon depths' },
++ hat_vegas: { name: 'Josh Vegas Top Hat', emoji: '🎩', category: 'hat', tier: 6, hatChar: '🎩', animated: 'float', desc: 'The showman on the Strip', exclusive: true },
// ── Voices (TTS style — cosmetic, actual TTS is client-side SpeechSynthesis) ──
voice_default: { name: 'Default Voice', emoji: '🔊', category: 'voice', tier: 0, desc: 'Standard TTS' },
@@@ -102,6 +104,7 @@ const COSMETICS = {
pl_ruth_long: { name: 'Ruth', emoji: '🎤', category: 'voice', tier: 4, pitch: 1.0, rate: 0.85, desc: 'Polly Long-form' },
pl_arthur_neural: { name: 'Arthur Neural', emoji: '🧠', category: 'voice', tier: 5, pitch: 0.9, rate: 0.85, desc: 'Polly Neural' },
vs_mine_dwarf_lord: { name: 'Dwarf Lord', emoji: '⛏️', category: 'voice', tier: 5, pitch: 0.5, rate: 0.7, desc: 'Skill mastery' },
++ voice_vegas: { name: 'Vegas Announcer', emoji: '🎙️', category: 'voice', tier: 6, pitch: 0.7, rate: 0.92, desc: 'Ladies and gentlemen…', exclusive: true },
};
// Category → slot name mapping
ddiff --git a/test/cosmetics-exclusive.test.js b/test/cosmetics-exclusive.test.js
iindex fd7441e0..a46eeb06 100644
---- a/test/cosmetics-exclusive.test.js
++++ b/test/cosmetics-exclusive.test.js
@@@ -15,6 +15,16 @@ 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');
++ // Its set: a particle, a hat and a voice, all one of a kind and valid for Inventory's kinds.
++ const SET = { px_vegas: 'particle', hat_vegas: 'hat', voice_vegas: 'voice' };
++ for (const [id, category] of Object.entries(SET)) {
++ const c = cosmetics.COSMETICS[id];
++ assert.ok(c && c.exclusive === true && c.category === category, `${id} is a one-of-a-kind ${category}`);
++ }
++ assert.ok(cosmetics.COSMETICS.px_vegas.chars.length <= 16 && /^px-[a-z]+$/.test(cosmetics.COSMETICS.px_vegas.cssClass), 'particle chars fit live.particle');
++ assert.ok(['float', 'pulse', 'warp'].includes(cosmetics.COSMETICS.hat_vegas.animated) && cosmetics.COSMETICS.hat_vegas.hatChar.length <= 8, 'the hat fits live.hat');
++ const v = cosmetics.COSMETICS.voice_vegas;
++ assert.ok(v.pitch >= 0 && v.pitch <= 2 && v.rate >= 0.1 && v.rate <= 3, 'the voice fits live.voice');
assert.match(vegas.cssClass, /^name-fx-[a-z]{2,24}$/, 'its class passes the pattern Chat and Network accept');
const app = express();
@@@ -25,14 +35,15 @@ const cosmetics = require('../server/monetization/cosmetics');
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');
++ for (const id of ['fx_vegas', 'px_vegas', 'hat_vegas', 'voice_vegas']) assert.ok(!listed.includes(id), `${id}, one of a kind, 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('.name-fx-vegas {') && css.includes('@keyframes nameFxVegasReel') && css.includes('@keyframes nameFxVegasJackpot'), `${file} draws the effect`);
++ assert.ok(css.includes('.px-vegas .chat-particle'), `${file} colours the Vegas particles`);
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');