Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added cat-stage-vegas-0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cat-stage-vegas-10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cat-stage-vegas-100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cat-stage-vegas-130.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cat-stage-vegas-25.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cat-stage-vegas-50.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added cat-stage-vegas-75.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions count.html
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,22 @@
linear-gradient(135deg, rgba(101, 67, 33, 0.18) 0%, rgba(212, 179, 107, 0.10) 100%),
linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
}

/* ── Viva Las Vegas theme: neon casino glow + gold progress bar ── */
.count-container.theme-cat-vegas {
background:
radial-gradient(ellipse at 20% 0%, rgba(255, 92, 138, 0.22) 0%, rgba(255, 92, 138, 0) 55%),
radial-gradient(ellipse at 80% 100%, rgba(92, 242, 255, 0.14) 0%, rgba(92, 242, 255, 0) 55%),
linear-gradient(135deg, #1a0f2e 0%, #24123e 50%, #0f1a40 100%);
}

.count-container.theme-cat-vegas .progress-bar-fill {
background: linear-gradient(90deg, #c48a12, #ffd34d, #fff1b0);
}

.count-container.theme-cat-vegas .cat-img {
filter: drop-shadow(0 0 14px rgba(255, 211, 77, 0.35));
}
</style>
</head>
<body>
Expand Down Expand Up @@ -728,6 +744,26 @@ <h2>Select Event</h2>
</div>
</div>

<!-- Viva Las Vegas Theme: 7 stages, broke alley cat to Elvis-level high roller -->
<div id="themeCatVegas" class="theme-view" style="display:none;">
<div class="counter-number cat-counter" id="catCounterVegasNumber"></div>
<div class="cat-container" id="catContainerVegas">
<img class="cat-img" data-stage="0" src="cat-stage-vegas-0.png" alt="Broke kitten in the rain holding a Will Purr For Chips sign" draggable="false"/>
<img class="cat-img" data-stage="10" src="cat-stage-vegas-10.png" alt="Kitten in a dealer visor with a cup of coins at the penny slots" draggable="false"/>
<img class="cat-img" data-stage="25" src="cat-stage-vegas-25.png" alt="Kitten in a casino vest hitting blackjack with stacks of chips" draggable="false"/>
<img class="cat-img" data-stage="50" src="cat-stage-vegas-50.png" alt="Chubby kitten in a purple suit fanning cash beside a roulette wheel" draggable="false"/>
<img class="cat-img" data-stage="75" src="cat-stage-vegas-75.png" alt="Kitten with dollar-sign eyes and a gold chain hitting a 777 jackpot" draggable="false"/>
<img class="cat-img" data-stage="100" src="cat-stage-vegas-100.png" alt="High-roller kitten in a white tux and top hat with champagne and a red sports car" draggable="false"/>
<img class="cat-img" data-stage="130" src="cat-stage-vegas-130.png" alt="Elvis kitten in a rhinestone jumpsuit on a throne of cash with a mansion and sports cars" draggable="false"/>
</div>
<div class="progress-bar-container" id="progressBarContainerVegas">
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progressBarFillVegas"></div>
</div>
<div class="progress-bar-label" id="progressBarLabelVegas"></div>
</div>
</div>

<div class="milestone-message" id="milestoneMessage"></div>
<div class="encouraging-message" id="encouragingMessage"></div>
</div>
Expand Down Expand Up @@ -781,6 +817,7 @@ <h2>Select Event</h2>
<option value="classic" selected>Classic</option>
<option value="cat">Feed the Cat</option>
<option value="cat-de">Feed the Cat (German)</option>
<option value="cat-vegas">Viva Las Vegas</option>
</select>
</div>
</div>
Expand Down
246 changes: 220 additions & 26 deletions count.js

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions scripts/vegas-cat-prompts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"generator": "Bing Image Creator (MAI-Image-2.5-Flash), aspect ratio 1:1, downloaded as 1024x1024 WebP and converted to PNG. Each prompt is self-contained and under Bing's 480-character limit.",
"stages": [
{
"stage": 0,
"filename": "cat-stage-vegas-0.png",
"prompt": "Cute chibi orange tabby kitten, big anime eyes, white chest, painterly digital art, dark navy background with soft Las Vegas neon glow, centered full body, square. Skinny broke kitten in a rainy back alley, scruffy patched fur, empty pockets turned inside out, holding a soggy cardboard sign \"WILL PURR FOR CHIPS\", one tear, sad hopeful eyes, a single penny on the wet ground, dim pink PAWN neon sign behind."
},
{
"stage": 10,
"filename": "cat-stage-vegas-10.png",
"prompt": "Cute chibi orange tabby kitten, big anime eyes, white chest, painterly digital art, dark navy background with soft Las Vegas neon glow, centered full body, square. Kitten in a green casino dealer visor beside a small red penny slot machine, proudly holding a paper cup with a few gold coins, small hopeful smile, coins on the casino carpet, \"PENNY SLOTS\" neon sign."
},
{
"stage": 25,
"filename": "cat-stage-vegas-25.png",
"prompt": "Cute chibi orange tabby kitten, big anime eyes, white chest, painterly digital art, dark navy background with soft Las Vegas neon glow, centered full body, square. Plumper kitten in a red casino vest, black bow tie and green visor at a blackjack table, holding up an Ace of spades and King of hearts, big excited grin, small stacks of red, blue and black poker chips."
},
{
"stage": 50,
"filename": "cat-stage-vegas-50.png",
"prompt": "Cute chibi orange tabby kitten, big anime eyes, white chest, painterly digital art, dark navy background with soft Las Vegas neon glow, centered full body, square. Chubby kitten in a sharp purple suit, gold bow tie, sunglasses pushed up on its head, eyes shut in joy, fanning a handful of dollar bills, roulette wheel behind, tall towers of poker chips and cash bundles."
},
{
"stage": 75,
"filename": "cat-stage-vegas-75.png",
"prompt": "Cute chibi orange tabby kitten, big anime eyes, white chest, painterly digital art, dark navy background with soft Las Vegas neon glow, centered full body, square. Very chubby kitten in a purple suit with a thick gold chain and dollar medallion, green dollar-sign eyes, waving a fan of hundred-dollar bills, red slot machine showing 7-7-7 spilling a waterfall of gold coins, \"JACKPOT\" neon."
},
{
"stage": 100,
"filename": "cat-stage-vegas-100.png",
"prompt": "Cute chibi orange tabby kitten, big anime eyes, white chest, painterly digital art, dark navy background with soft Las Vegas neon glow, centered full body, square. Chonky high-roller kitten in a white tuxedo with a red rose, black top hat and cool sunglasses, raising a glass of champagne, shiny red convertible on the Las Vegas Strip behind, money bag and stacks of cash and chips."
},
{
"stage": 130,
"filename": "cat-stage-vegas-130.png",
"prompt": "Cute chibi orange tabby kitten, big anime eyes, painterly digital art, dark navy background with Las Vegas neon, centered full body, square. Rich chonky kitten dressed as Vegas-era Elvis: white rhinestone jumpsuit, tall collar, gold belt buckle, white-and-gold cape, black pompadour, gold aviators, microphone, lip-curl grin, on a throne of cash. Mansion, gold and red sports cars, Welcome to Las Vegas sign, fireworks, chips and bills raining."
}
]
}
4 changes: 2 additions & 2 deletions tests/german-cat-theme.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ async function waitForCountReady(page) {
}

test.describe('Dropdown integration', () => {
test('Dropdown shows three theme options', async ({ page }) => {
test('Dropdown lists every theme option', async ({ page }) => {
await page.goto(COUNT_URL);
const values = await page.$$eval('#themeSelect option', opts => opts.map(o => o.value));
expect(values).toEqual(['classic', 'cat', 'cat-de']);
expect(values).toEqual(['classic', 'cat', 'cat-de', 'cat-vegas']);
});

test("Selecting 'cat-de' switches the view and persists across reload", async ({ page }) => {
Expand Down
189 changes: 189 additions & 0 deletions tests/vegas-cat-theme.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* Verification suite for the Viva Las Vegas cat theme: rags-to-riches stages,
* casino celebrations (cash + poker chips) and slot machine sounds.
*/
import { test, expect } from '@playwright/test';

const COUNT_URL = '/count.html?code=CSA1B2C3';
const VEGAS_FOOD_EMOJIS = ['💵', '💰', '🪙', '💎', '🎲', '🃏'];

async function setMockCount(page, count) {
await page.evaluate((n) => {
const rows = Array.from({ length: n }, (_, i) => ({
eventCode: 'CSA1B2C3',
eventModuleId: 1,
id: i
}));
localStorage.setItem('bootcampFeedback', JSON.stringify(rows));
}, count);
}

async function pickTheme(page, theme) {
await page.selectOption('#themeSelect', theme);
await page.waitForTimeout(150);
}

async function waitForCountReady(page) {
await page.waitForSelector('#countDisplay', { state: 'visible', timeout: 5000 });
await page.waitForFunction(() => {
const el = document.getElementById('lastUpdated');
return el && el.textContent.trim() !== '' && el.textContent.trim() !== '-';
}, { timeout: 5000 });
}

test.describe('Viva Las Vegas — theme switching', () => {
test("Selecting 'cat-vegas' switches the view and persists across reload", async ({ page }) => {
await page.goto(COUNT_URL);
const label = await page.$eval('#themeSelect option[value="cat-vegas"]', o => o.textContent);
expect(label).toBe('Viva Las Vegas');
await pickTheme(page, 'cat-vegas');
await expect(page.locator('#themeCatVegas')).toBeVisible();
await expect(page.locator('#themeCat')).toBeHidden();
await expect(page.locator('#themeCatDE')).toBeHidden();
await expect(page.locator('#themeClassic')).toBeHidden();
expect(await page.evaluate(() => sessionStorage.getItem('counterTheme'))).toBe('cat-vegas');
await page.reload();
await expect(page.locator('#themeCatVegas')).toBeVisible();
});

test('theme-cat-vegas class is applied only while the Vegas theme is active', async ({ page }) => {
await page.goto(COUNT_URL);
const container = page.locator('.count-container');
await pickTheme(page, 'cat-vegas');
await expect(container).toHaveClass(/theme-cat-vegas/);
await expect(container).not.toHaveClass(/theme-cat-de/);
await pickTheme(page, 'cat-de');
await expect(container).not.toHaveClass(/theme-cat-vegas/);
await expect(container).toHaveClass(/theme-cat-de/);
await pickTheme(page, 'classic');
await expect(container).not.toHaveClass(/theme-cat-/);
});
});

test.describe('Viva Las Vegas — stages', () => {
test('All 7 rags-to-riches stages render at the correct counts', async ({ page }) => {
for (const [n, stage] of [[0, 0], [9, 0], [10, 10], [25, 25], [50, 50], [75, 75], [100, 100], [129, 100], [130, 130], [400, 130]]) {
await page.goto(COUNT_URL);
await setMockCount(page, n);
await page.reload();
await waitForCountReady(page);
await pickTheme(page, 'cat-vegas');
const activeStages = await page.$$eval('#themeCatVegas .cat-img.active', els => els.map(e => e.dataset.stage));
expect(activeStages, `count ${n}`).toEqual([String(stage)]);
}
});

test('Counter digits and progress bar update for the Vegas view', async ({ page }) => {
await page.goto(COUNT_URL);
await setMockCount(page, 42);
await page.reload();
await waitForCountReady(page);
await pickTheme(page, 'cat-vegas');
const digits = await page.$$eval('#catCounterVegasNumber .counter-digit', els => els.map(e => e.dataset.digit).join(''));
expect(digits).toBe('42');
await expect(page.locator('#progressBarLabelVegas')).toHaveText('42 / 50 to next milestone');

await setMockCount(page, 135);
await page.reload();
await waitForCountReady(page);
await pickTheme(page, 'cat-vegas');
await expect(page.locator('#progressBarLabelVegas')).toContainText('The King of Vegas');
});

test('All 7 stage images load', async ({ page }) => {
await page.goto(COUNT_URL);
await pickTheme(page, 'cat-vegas');
const results = await page.$$eval('#themeCatVegas .cat-img', imgs => Promise.all(imgs.map(img =>
img.complete ? img.naturalWidth : new Promise(r => { img.onload = () => r(img.naturalWidth); img.onerror = () => r(0); })
)));
expect(results).toHaveLength(7);
for (const w of results) expect(w).toBeGreaterThan(0);
});
});

test.describe('Viva Las Vegas — content', () => {
test('Encouraging messages are casino-flavored', async ({ page }) => {
await page.goto(COUNT_URL);
await pickTheme(page, 'cat-vegas');
const messages = await page.evaluate(() => window._activeEncouragingMessagesForTest?.());
expect(messages.join(' ')).toMatch(/Viva Las Vegas|jackpot|slots/i);
});

test('Milestone messages are Vegas overrides', async ({ page }) => {
await page.goto(COUNT_URL);
await pickTheme(page, 'cat-vegas');
expect(await page.evaluate(() => window._activeMilestoneMessageForTest?.(75))).toMatch(/JACKPOT/);
expect(await page.evaluate(() => window._activeMilestoneMessageForTest?.(100))).toMatch(/High roller/i);
});

test('Food drops are money and casino items only', async ({ page }) => {
await page.goto(COUNT_URL);
await pickTheme(page, 'cat-vegas');
const list = await page.evaluate(() => window._activeFoodEmojisForTest?.());
expect(list).toEqual(VEGAS_FOOD_EMOJIS);
});
});

test.describe('Viva Las Vegas — celebrations', () => {
test('Celebrations play slot machine sounds instead of the default chimes', async ({ page }) => {
await page.goto(COUNT_URL);
await pickTheme(page, 'cat-vegas');
const played = await page.evaluate(async () => {
const urls = [];
const OrigAudio = window.Audio;
window.Audio = function (src) {
urls.push(src);
return { play: () => Promise.resolve(), volume: 1 };
};
window.playMilestoneSound();
window.Audio = OrigAudio;
return urls;
});
expect(played).toHaveLength(1);
// Same call on the plain cat theme must produce a different (default) sound
await pickTheme(page, 'cat');
const defaultPlayed = await page.evaluate(async () => {
const urls = [];
const OrigAudio = window.Audio;
window.Audio = function (src) { urls.push(src); return { play: () => Promise.resolve(), volume: 1 }; };
window.playMilestoneSound();
window.Audio = OrigAudio;
return urls;
});
expect(defaultPlayed).toHaveLength(1);
expect(defaultPlayed[0]).not.toBe(played[0]);
});

test('Milestone confetti is poker chips and banknotes', async ({ page }) => {
await page.goto(COUNT_URL);
await pickTheme(page, 'cat-vegas');
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.evaluate(() => window.checkMilestone(10, 9));
await page.waitForTimeout(600);
await expect(page.locator('#milestoneMessage')).toContainText('Penny slots');
expect(errors).toEqual([]);
});
});

test.describe('Viva Las Vegas — image assets', () => {
test('All 7 cat-stage-vegas-*.png files exist, are square, and ≤ 3500 KB', async ({ }) => {
const fs = await import('node:fs/promises');
const { fileURLToPath } = await import('node:url');
const root = fileURLToPath(new URL('..', import.meta.url));
for (const stage of [0, 10, 25, 50, 75, 100, 130]) {
const file = root + `cat-stage-vegas-${stage}.png`;
const stat = await fs.stat(file);
expect(stat.size, `${file} size`).toBeLessThanOrEqual(3500 * 1024);
const fd = await fs.open(file, 'r');
const buf = Buffer.alloc(8);
await fd.read(buf, 0, 8, 16);
await fd.close();
const width = buf.readUInt32BE(0);
const height = buf.readUInt32BE(4);
expect(width, `${file} width`).toBeGreaterThanOrEqual(700);
expect(width).toBeLessThanOrEqual(1200);
expect(height, `${file} height`).toEqual(width);
}
});
});
Loading