diff --git a/src/main.ts b/src/main.ts index 7a4b66f..17c8053 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,9 +4,18 @@ import './style.css'; const app = document.getElementById('app')!; -let currentView: 'quiz' | 'stats' = 'quiz'; +let currentView: 'quiz' | 'stats' | 'info' = 'quiz'; let typeChartView: 'attacking' | 'defending' = 'attacking'; const game = new Game(render); +const INFO_SEEN_KEY = 'pokemon-type-quiz-info-seen'; + +// Check if user has seen info screen before +const hasSeenInfo = localStorage.getItem(INFO_SEEN_KEY) === 'true'; + +if (!hasSeenInfo) { + currentView = 'info'; +} + game.nextQuestion(); function renderTypeBadge(name: TypeName, size: 'small' | 'large' = 'small'): string { @@ -15,12 +24,83 @@ function renderTypeBadge(name: TypeName, size: 'small' | 'large' = 'small'): str return `${info.name}`; } +function renderInfoScreen(): void { + app.innerHTML = ` +
+
+

Pokemon Type Quiz

+

Test your knowledge of type effectiveness!

+ +
+

How to Play

+
    +
  • A random attacking type will be shown
  • +
  • You'll see one or two defending type(s)
  • +
  • Select how effective the attack is against the defender
  • +
+
+ +
+

Effectiveness Options

+
+
+ 0x + No effect (e.g., Ghost vs Normal) +
+
+ 1/4x + Not very effective (e.g., Fire vs Rock/Water) +
+
+ 1/2x + Not very effective (e.g., Water vs Fire) +
+
+ 1x + Normal effectiveness +
+
+ 2x + Super effective (e.g., Water vs Fire) +
+
+ 4x + Super effective (e.g., Grass vs Water/Ground) +
+
+
+ +
+

Controls

+

+ 1-6 to select answer + Enter to continue +

+
+ + +
+
+ `; + + document.getElementById('startBtn')?.addEventListener('click', () => { + localStorage.setItem(INFO_SEEN_KEY, 'true'); + currentView = 'quiz'; + render(); + }); +} + function render(): void { + if (currentView === 'info') { + renderInfoScreen(); + return; + } + if (currentView === 'stats') { renderStatsPage(); return; } - + const state = game.getState(); const { options, startKey } = game.getOptions(); const stats = game.getStats(); @@ -31,7 +111,7 @@ function render(): void { } const q = state.currentQuestion; - const defenderHtml = q.defenderTypes.length === 1 + const defenderHtml = q.defenderTypes.length === 1 ? renderTypeBadge(q.defenderTypes[0], 'large') : `${renderTypeBadge(q.defenderTypes[0], 'large')} ${renderTypeBadge(q.defenderTypes[1], 'large')}`; @@ -40,9 +120,9 @@ function render(): void { if (i === state.selectedIndex) { cls += ' selected'; } - + const shortcut = `${startKey + i}`; - + return ` ' : ''} ` : ''; @@ -66,11 +146,14 @@ function render(): void {

Pokemon Type Quiz

-
- Streak: ${state.streak} - Accuracy: ${stats.accuracy.toFixed(1)}% - Total: ${stats.total} - +
+ +
+ Streak: ${state.streak} + Accuracy: ${stats.accuracy.toFixed(1)}% + Total: ${stats.total} + +
@@ -109,9 +192,9 @@ function renderCombinationItem(combo: CombinationStats): string { const defenderHtml = combo.defenderTypes.length === 1 ? renderTypeBadge(combo.defenderTypes[0], 'small') : `${renderTypeBadge(combo.defenderTypes[0], 'small')} ${renderTypeBadge(combo.defenderTypes[1], 'small')}`; - + const accuracyClass = combo.accuracy >= 80 ? 'high' : combo.accuracy >= 50 ? 'medium' : 'low'; - + return `
@@ -131,7 +214,7 @@ function renderTypeChartCell(typeStat: TypeChartStats): string { const info = getTypeInfo(typeStat.attackType); const accuracyClass = typeStat.total === 0 ? 'no-data' : typeStat.accuracy >= 80 ? 'high' : typeStat.accuracy >= 50 ? 'medium' : 'low'; const display = typeStat.total === 0 ? '-' : `${typeStat.accuracy.toFixed(0)}%`; - + return `
${typeStat.attackType.slice(0, 3)} @@ -142,7 +225,7 @@ function renderTypeChartCell(typeStat: TypeChartStats): string { function renderFullTypeChart(cells: FullTypeChartCell[]): string { const defenderTypes = ['Normal', 'Fire', 'Water', 'Electric', 'Grass', 'Ice', 'Fighting', 'Poison', 'Ground', 'Flying', 'Psychic', 'Bug', 'Rock', 'Ghost', 'Dragon', 'Dark', 'Steel', 'Fairy']; - + // Create a map for quick cell lookup const cellMap = new Map(); for (const cell of cells) { @@ -150,7 +233,7 @@ function renderFullTypeChart(cells: FullTypeChartCell[]): string { const key = `${cell.attackType}:${defenderKey}`; cellMap.set(key, cell); } - + // Generate header row const headerCells = defenderTypes.map(def => { const info = getTypeInfo(def as TypeName); @@ -158,7 +241,7 @@ function renderFullTypeChart(cells: FullTypeChartCell[]): string { ${def.slice(0, 3)}
`; }).join(''); - + // Generate rows const rows = defenderTypes.map(rowType => { const rowInfo = getTypeInfo(rowType as TypeName); @@ -166,11 +249,11 @@ function renderFullTypeChart(cells: FullTypeChartCell[]): string { const cell = cellMap.get(`${rowType}:${colType}`); const total = cell?.total || 0; const accuracy = cell?.accuracy || 0; - + let bgColor = 'var(--bg-accent)'; let textColor = 'var(--text-secondary)'; let display = '-'; - + if (total > 0) { display = `${accuracy.toFixed(0)}`; if (accuracy >= 80) { @@ -184,12 +267,12 @@ function renderFullTypeChart(cells: FullTypeChartCell[]): string { textColor = 'var(--error)'; } } - + return `
${display}
`; }).join(''); - + return `
@@ -199,7 +282,7 @@ function renderFullTypeChart(cells: FullTypeChartCell[]): string {
`; }).join(''); - + return `
@@ -217,19 +300,19 @@ function renderStatsPage(): void { const worstCombos = game.getWorstCombinations(5); const typeChartStats = typeChartView === 'attacking' ? game.getTypeChartStats() : game.getDefendingTypeStats(); const fullTypeChartCells = game.getFullTypeChartStats(); - + const hasData = stats.total > 0; - - const bestCombosHtml = bestCombos.length > 0 + + const bestCombosHtml = bestCombos.length > 0 ? bestCombos.map(renderCombinationItem).join('') : '

Answer at least 3 questions to see stats

'; - + const worstCombosHtml = worstCombos.length > 0 ? worstCombos.map(renderCombinationItem).join('') : '

Answer at least 3 questions to see stats

'; - + const typeChartHtml = typeChartStats.map(renderTypeChartCell).join(''); - + app.innerHTML = `
@@ -293,12 +376,12 @@ function renderStatsPage(): void { `}
`; - + document.getElementById('backBtn')?.addEventListener('click', () => { currentView = 'quiz'; render(); }); - + document.querySelectorAll('.chart-toggle').forEach(btn => { btn.addEventListener('click', () => { const view = btn.getAttribute('data-view') as 'attacking' | 'defending'; @@ -337,6 +420,14 @@ function attachEventListeners(): void { render(); }); } + + const infoBtn = document.getElementById('infoBtn'); + if (infoBtn) { + infoBtn.addEventListener('click', () => { + currentView = 'info'; + render(); + }); + } } document.addEventListener('keydown', (e) => { diff --git a/src/style.css b/src/style.css index 11fbe15..fd7ffef 100644 --- a/src/style.css +++ b/src/style.css @@ -663,6 +663,206 @@ body { position: relative; } +/* Info Button */ +.info-btn { + width: 32px; + height: 32px; + background: var(--bg-accent); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 50%; + color: var(--text-primary); + font-family: 'Outfit', sans-serif; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: var(--transition); +} + +.info-btn:hover { + background: var(--accent); + border-color: var(--accent); +} + +.header-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +/* Info Screen */ +.info-screen { + min-height: calc(100vh - 40px); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.info-content { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 40px 32px; + max-width: 480px; + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.05); + animation: fadeIn 0.4s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.info-title { + font-size: 2rem; + font-weight: 700; + text-align: center; + background: linear-gradient(135deg, var(--accent), #ff6b6b); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 8px; +} + +.info-subtitle { + text-align: center; + color: var(--text-secondary); + font-size: 1.1rem; + margin-bottom: 32px; +} + +.info-section { + margin-bottom: 28px; +} + +.info-section h2 { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 12px; + color: var(--text-primary); +} + +.info-list { + list-style: none; + padding: 0; +} + +.info-list li { + padding: 8px 0; + color: var(--text-secondary); + font-size: 0.95rem; + line-height: 1.5; +} + +.info-list li::before { + content: "→"; + color: var(--accent); + margin-right: 10px; +} + +.info-list li strong { + color: var(--text-primary); +} + +.effectiveness-examples { + display: flex; + flex-direction: column; + gap: 8px; +} + +.effectiveness-item { + display: flex; + align-items: center; + gap: 12px; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.eff-label { + display: inline-block; + width: 50px; + padding: 4px 8px; + border-radius: 4px; + text-align: center; + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + font-size: 0.8rem; +} + +.eff-0 { + background: rgba(248, 113, 113, 0.2); + color: var(--error); +} + +.eff-1q { + background: rgba(251, 191, 36, 0.2); + color: #fbbf24; +} + +.eff-2q { + background: rgba(251, 191, 36, 0.2); + color: #fbbf24; +} + +.eff-1 { + background: rgba(160, 160, 160, 0.2); + color: var(--text-secondary); +} + +.eff-2 { + background: rgba(74, 222, 128, 0.2); + color: var(--success); +} + +.eff-4 { + background: rgba(74, 222, 128, 0.2); + color: var(--success); +} + +.info-controls { + font-size: 0.9rem; + color: var(--text-secondary); +} + +.key-hint { + display: inline-block; + padding: 4px 10px; + background: var(--bg-accent); + border-radius: 4px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.8rem; + margin: 0 4px; + color: var(--text-primary); +} + +.start-btn { + display: block; + width: 100%; + padding: 16px 32px; + background: var(--accent); + border: none; + border-radius: var(--border-radius); + color: white; + font-family: 'Outfit', sans-serif; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + margin-top: 32px; +} + +.start-btn:hover { + background: #ff6b6b; + transform: scale(1.02); +} + @media (max-width: 480px) { body { padding: 12px; @@ -700,4 +900,20 @@ body { .option-label { font-size: 0.9rem; } + + .info-content { + padding: 28px 20px; + } + + .info-title { + font-size: 1.6rem; + } + + .info-subtitle { + font-size: 1rem; + } + + .effectiveness-item { + font-size: 0.8rem; + } }