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
160 lines
4.8 KiB
JavaScript
160 lines
4.8 KiB
JavaScript
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' }, 'Who’s 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 = 'Who’s 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 = `It’s ${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', {}, 'Who’s that Pokémon?'),
|
||
el('p', {}, 'Name it from the silhouette. Three misses and it’s 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;
|
||
}
|