dex/src/views/WhosThatView.js
chris a57e516f16 Who's that Pokémon?: bright stage so the silhouette shows in dark mode
The stage used var(--surface), so a brightness(0) silhouette was
near-invisible on a dark theme. Make it a fixed light "TV screen"
(blue→white gradient) in every theme; move the caption/hint out onto the
page background so they stay readable; add a drop-shadow to the revealed
artwork.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
2026-09-10 12:52:56 -04:00

160 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { el, clear, onTeardown } from '../lib/dom.js';
import { loadSnapshot } from '../data/snapshot.js';
import { Sprite } from '../components/Sprite.js';
import { whosThat, recordResult } from '../store/whosThat.js';
import { prefersReducedMotion } from '../store/settings.js';
const loose = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
const pretty = (name) => name.replace(/-/g, ' ');
/**
* Hidden mini-game — reachable by the Konami code (↑↑↓↓←→←→BA) or seven
* quick taps on the nav wordmark, and by the bare #/whos-that hash once
* you know it. Guess the Pokémon from its silhouette.
*/
export async function WhosThatView() {
const view = el('section', { class: 'view wtp' });
const snap = await loadSnapshot();
const pool = snap.species;
let current = null;
let revealed = false;
let wrong = 0;
let streak = 0;
const art = el('div', { class: 'wtp__art' });
const caption = el('div', { class: 'wtp__caption' }, 'Whos that Pokémon?');
const hint = el('div', { class: 'wtp__hint' });
const feedback = el('div', { class: 'wtp__feedback' });
const scoreEl = el('div', { class: 'wtp__score' });
const input = el('input', {
class: 'wtp__input',
type: 'text',
placeholder: 'Your guess…',
autocomplete: 'off',
autocapitalize: 'off',
autocorrect: 'off',
spellcheck: 'false',
onkeydown: (e) => {
if (e.key === 'Enter') submitGuess();
},
});
const guessBtn = el(
'button',
{ class: 'button wtp__go', type: 'button', onclick: submitGuess },
'Guess',
);
const revealBtn = el(
'button',
{ class: 'button button--ghost', type: 'button', onclick: () => reveal(false) },
'Reveal',
);
const nextBtn = el('button', { class: 'button', type: 'button', onclick: next }, 'Next →');
function renderScore() {
const s = whosThat.get();
const pct = s.plays ? Math.round((s.wins / s.plays) * 100) : 0;
clear(scoreEl).append(
el('span', { class: 'wtp__pill' }, `Streak ${streak}`),
el('span', { class: 'wtp__pill' }, `Best ${s.best}`),
el('span', { class: 'wtp__pill wtp__pill--dim' }, `${s.wins}/${s.plays} · ${pct}%`),
);
}
function syncButtons() {
input.hidden = revealed;
guessBtn.hidden = revealed;
revealBtn.hidden = revealed;
nextBtn.hidden = !revealed;
}
function next() {
current = pool[Math.floor(Math.random() * pool.length)];
revealed = false;
wrong = 0;
art.classList.remove('is-revealed');
clear(art).append(Sprite(current.id, { style: 'official', size: 260, alt: 'Mystery Pokémon' }));
caption.textContent = 'Whos that Pokémon?';
hint.textContent = '';
feedback.textContent = '';
feedback.className = 'wtp__feedback';
input.value = '';
syncButtons();
input.focus();
}
function reveal(win) {
revealed = true;
art.classList.add('is-revealed');
caption.textContent = `Its ${pretty(current.name)}! #${String(current.id).padStart(4, '0')}`;
if (win) {
streak += 1;
feedback.textContent = wrong === 0 ? 'Nailed it!' : 'Got there!';
feedback.className = 'wtp__feedback is-good';
} else {
streak = 0;
feedback.textContent = 'That one got away.';
feedback.className = 'wtp__feedback is-bad';
}
recordResult(win, streak);
renderScore();
syncButtons();
if (win && !prefersReducedMotion()) {
art.animate(
[{ transform: 'scale(0.9)' }, { transform: 'scale(1.05)' }, { transform: 'scale(1)' }],
{ duration: 380, easing: 'cubic-bezier(.34,1.4,.64,1)' },
);
}
nextBtn.focus();
}
function submitGuess() {
if (revealed || !input.value.trim()) return;
if (loose(input.value) === loose(current.name)) {
reveal(true);
return;
}
wrong += 1;
input.value = '';
feedback.className = 'wtp__feedback is-bad';
if (wrong === 1) {
feedback.textContent = 'Nope — try again.';
hint.textContent = `${loose(current.name).length} letters`;
} else if (wrong === 2) {
feedback.textContent = 'Last clue…';
hint.textContent = `Starts with “${current.name[0].toUpperCase()}”, ${loose(current.name).length} letters`;
} else {
reveal(false); // three strikes
return;
}
input.focus();
}
view.append(
el('nav', { class: 'lookup__nav' }, el('a', { class: 'link', href: '#/' }, ' Dex')),
el(
'header',
{ class: 'view__header' },
el('h1', {}, 'Whos that Pokémon?'),
el('p', {}, 'Name it from the silhouette. Three misses and its revealed.'),
),
scoreEl,
el('div', { class: 'wtp__stage' }, art),
caption,
hint,
el('div', { class: 'wtp__controls' }, input, guessBtn, revealBtn, nextBtn),
feedback,
);
renderScore();
next();
const off = whosThat.subscribe(() => {
if (revealed) renderScore();
});
onTeardown(view, off);
return view;
}