openvibers/openvibe.live Commits
The Josh Vegas set: a jackpot pop on the name, casino chips, a top hat and an announcer voice (#314)
Changes
@@@ -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); }/* ══════════════════════════════════════════════════════════════@@@ -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@@@ -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@@@ -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');
@@ -176,18 +176,21 @@
@@ -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
/* 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 {
.name-fx-vegas {
background: linear-gradient(90deg, #fff1b8 0%, #ffcf3f 22%, #ff9d00 40%, #ff3d7f 52%, #ffcf3f 66%, #fff1b8 82%, #ffcf3f 100%);
background: linear-gradient(90deg, #fff1b8 0%, #ffcf3f 22%, #ff9d00 40%, #ff3d7f 52%, #ffcf3f 66%, #fff1b8 82%, #ffcf3f 100%);
background-size: 280% 100%;
background-size: 280% 100%;
-webkit-background-clip: text;
-webkit-background-clip: text;
background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
-webkit-text-fill-color: transparent;
+ display: inline-block;
font-weight: 800;
font-weight: 800;
letter-spacing: .02em;
letter-spacing: .02em;
white-space: nowrap;
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::before,
.name-fx-vegas::after {
.name-fx-vegas::after {
@@ -221,6 +224,13 @@
@@ -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)); }
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)); }
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). */
/* Each reel spins through the symbols, then lands on 7 and holds it (a browser that cannot animate `content` shows 7). */
@keyframes nameFxVegasReel {
@keyframes nameFxVegasReel {
0% { content: '🍒'; }
0% { content: '🍒'; }
@@ -266,6 +276,8 @@
@@ -266,6 +276,8 @@
.px-flames .chat-particle { color: #ff6600; }
.px-flames .chat-particle { color: #ff6600; }
.px-stars .chat-particle { color: #fbbf24; }
.px-stars .chat-particle { color: #fbbf24; }
.px-void .chat-particle { color: #9900ff; }
.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); }
/* ══════════════════════════════════════════════════════════════
/* ══════════════════════════════════════════════════════════════
@@ -199,18 +199,21 @@ body.no-shadow .msg { text-shadow: none; }
@@ -199,18 +199,21 @@ 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
/* 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 {
.name-fx-vegas {
background: linear-gradient(90deg, #fff1b8 0%, #ffcf3f 22%, #ff9d00 40%, #ff3d7f 52%, #ffcf3f 66%, #fff1b8 82%, #ffcf3f 100%);
background: linear-gradient(90deg, #fff1b8 0%, #ffcf3f 22%, #ff9d00 40%, #ff3d7f 52%, #ffcf3f 66%, #fff1b8 82%, #ffcf3f 100%);
background-size: 280% 100%;
background-size: 280% 100%;
-webkit-background-clip: text;
-webkit-background-clip: text;
background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
-webkit-text-fill-color: transparent;
+ display: inline-block;
font-weight: 800;
font-weight: 800;
letter-spacing: .02em;
letter-spacing: .02em;
white-space: nowrap;
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::before,
.name-fx-vegas::after {
.name-fx-vegas::after {
@@ -244,6 +247,13 @@ body.no-shadow .msg { text-shadow: none; }
@@ -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)); }
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)); }
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). */
/* Each reel spins through the symbols, then lands on 7 and holds it (a browser that cannot animate `content` shows 7). */
@keyframes nameFxVegasReel {
@keyframes nameFxVegasReel {
0% { content: '🍒'; }
0% { content: '🍒'; }
@@ -350,6 +360,8 @@ body.no-shadow .msg { text-shadow: none; }
@@ -350,6 +360,8 @@ body.no-shadow .msg { text-shadow: none; }
.px-flames .chat-particle { color: #ff6600; }
.px-flames .chat-particle { color: #ff6600; }
.px-stars .chat-particle { color: #fbbf24; }
.px-stars .chat-particle { color: #fbbf24; }
.px-void .chat-particle { color: #9900ff; }
.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
HAT EFFECTS
@@ -34,8 +34,8 @@ const COSMETICS = {
@@ -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_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.
+ // 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 },
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 ─────────────────────────────────
@@ -44,6 +44,7 @@ const COSMETICS = {
@@ -44,6 +44,7 @@ const COSMETICS = {
px_flames: { name: 'Flames', emoji: '🔥', category: 'particle', tier: 2, cssClass: 'px-flames', chars: '🔥🔸⚡', desc: 'Flame embers' },
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_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_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 ─────────────────────────────────────────────
// ── Hats ─────────────────────────────────────────────
hat_basic_cap: { name: 'Basic Cap', emoji: '🧢', category: 'hat', tier: 1, hatChar: '🧢', desc: 'Simple cap' },
hat_basic_cap: { name: 'Basic Cap', emoji: '🧢', category: 'hat', tier: 1, hatChar: '🧢', desc: 'Simple cap' },
@@ -52,6 +53,7 @@ const COSMETICS = {
@@ -52,6 +53,7 @@ const COSMETICS = {
hat_crown: { name: 'Royal Crown', emoji: '👑', category: 'hat', tier: 4, hatChar: '👑', animated: 'pulse', desc: 'Royalty' },
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_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_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) ──
// ── Voices (TTS style — cosmetic, actual TTS is client-side SpeechSynthesis) ──
voice_default: { name: 'Default Voice', emoji: '🔊', category: 'voice', tier: 0, desc: 'Standard TTS' },
voice_default: { name: 'Default Voice', emoji: '🔊', category: 'voice', tier: 0, desc: 'Standard TTS' },
@@ -102,6 +104,7 @@ const COSMETICS = {
@@ -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_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' },
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' },
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
// Category → slot name mapping
@@ -15,6 +15,16 @@ const cosmetics = require('../server/monetization/cosmetics');
@@ -15,6 +15,16 @@ const cosmetics = require('../server/monetization/cosmetics');
(async () => {
(async () => {
const vegas = cosmetics.COSMETICS.fx_vegas;
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.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');
assert.match(vegas.cssClass, /^name-fx-[a-z]{2,24}$/, 'its class passes the pattern Chat and Network accept');
const app = express();
const app = express();
@@ -25,14 +35,15 @@ const cosmetics = require('../server/monetization/cosmetics');
@@ -25,14 +35,15 @@ const cosmetics = require('../server/monetization/cosmetics');
const { catalog } = await res.json();
const { catalog } = await res.json();
const listed = Object.values(catalog).flat().map((c) => c.itemId);
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_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 {
} finally {
server.close();
server.close();
}
}
for (const file of ['public/css/cosmetics.css', 'public/obs/chat.html']) {
for (const file of ['public/css/cosmetics.css', 'public/obs/chat.html']) {
const css = fs.readFileSync(path.join(__dirname, '..', file), 'utf8');
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`);
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');
console.log('cosmetics exclusive: all checks passed');