first add of info pane
This commit is contained in:
157
src/main.ts
157
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 `<span class="type-badge ${sizeClass}" style="background: ${info.backgroundColor}; color: ${info.color}">${info.name}</span>`;
|
||||
}
|
||||
|
||||
function renderInfoScreen(): void {
|
||||
app.innerHTML = `
|
||||
<div class="info-screen">
|
||||
<div class="info-content">
|
||||
<h1 class="info-title">Pokemon Type Quiz</h1>
|
||||
<p class="info-subtitle">Test your knowledge of type effectiveness!</p>
|
||||
|
||||
<div class="info-section">
|
||||
<h2>How to Play</h2>
|
||||
<ul class="info-list">
|
||||
<li>A random <strong>attacking type</strong> will be shown</li>
|
||||
<li>You'll see one or two <strong>defending type(s)</strong></li>
|
||||
<li>Select how effective the attack is against the defender</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h2>Effectiveness Options</h2>
|
||||
<div class="effectiveness-examples">
|
||||
<div class="effectiveness-item">
|
||||
<span class="eff-label eff-0">0x</span>
|
||||
<span>No effect (e.g., Ghost vs Normal)</span>
|
||||
</div>
|
||||
<div class="effectiveness-item">
|
||||
<span class="eff-label eff-1q">1/4x</span>
|
||||
<span>Not very effective (e.g., Fire vs Rock/Water)</span>
|
||||
</div>
|
||||
<div class="effectiveness-item">
|
||||
<span class="eff-label eff-2q">1/2x</span>
|
||||
<span>Not very effective (e.g., Water vs Fire)</span>
|
||||
</div>
|
||||
<div class="effectiveness-item">
|
||||
<span class="eff-label eff-1">1x</span>
|
||||
<span>Normal effectiveness</span>
|
||||
</div>
|
||||
<div class="effectiveness-item">
|
||||
<span class="eff-label eff-2">2x</span>
|
||||
<span>Super effective (e.g., Water vs Fire)</span>
|
||||
</div>
|
||||
<div class="effectiveness-item">
|
||||
<span class="eff-label eff-4">4x</span>
|
||||
<span>Super effective (e.g., Grass vs Water/Ground)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h2>Controls</h2>
|
||||
<p class="info-controls">
|
||||
<span class="key-hint">1-6</span> to select answer
|
||||
<span class="key-hint">Enter</span> to continue
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button class="start-btn" id="startBtn">Start Quiz</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 `
|
||||
<button class="${cls}" data-index="${i}">
|
||||
<span class="option-key">${shortcut}</span>
|
||||
@@ -53,11 +133,11 @@ function render(): void {
|
||||
|
||||
const resultHtml = state.showResult ? `
|
||||
<div class="result ${state.isCorrect ? 'correct' : 'wrong'}">
|
||||
${state.isCorrect
|
||||
? '<div class="result-title">✓ Correct!</div>'
|
||||
: `<div class="result-title">✗ Wrong</div>
|
||||
${state.isCorrect
|
||||
? '<div class="result-title">✓ Correct!</div>'
|
||||
: `<div class="result-title">✗ Wrong</div>
|
||||
<div class="explanation">${game.getExplanation()}</div>`
|
||||
}
|
||||
}
|
||||
${!state.isCorrect ? '<button class="next-btn">Continue (Enter)</button>' : ''}
|
||||
</div>
|
||||
` : '';
|
||||
@@ -66,11 +146,14 @@ function render(): void {
|
||||
<div class="game-container">
|
||||
<header class="header">
|
||||
<h1>Pokemon Type Quiz</h1>
|
||||
<div class="stats">
|
||||
<span class="stat">Streak: ${state.streak}</span>
|
||||
<span class="stat">Accuracy: ${stats.accuracy.toFixed(1)}%</span>
|
||||
<span class="stat">Total: ${stats.total}</span>
|
||||
<button class="stats-btn" id="statsBtn">View Stats</button>
|
||||
<div class="header-actions">
|
||||
<button class="info-btn" id="infoBtn" title="How to play">?</button>
|
||||
<div class="stats">
|
||||
<span class="stat">Streak: ${state.streak}</span>
|
||||
<span class="stat">Accuracy: ${stats.accuracy.toFixed(1)}%</span>
|
||||
<span class="stat">Total: ${stats.total}</span>
|
||||
<button class="stats-btn" id="statsBtn">View Stats</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -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 `
|
||||
<div class="combo-item">
|
||||
<div class="combo-types">
|
||||
@@ -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 `
|
||||
<div class="type-cell" style="background: ${info.backgroundColor}; color: ${info.color};" title="${typeStat.attackType}: ${typeStat.correct}/${typeStat.total}">
|
||||
<span class="type-cell-name">${typeStat.attackType.slice(0, 3)}</span>
|
||||
@@ -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<string, FullTypeChartCell>();
|
||||
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)}
|
||||
</div>`;
|
||||
}).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 `<div class="full-chart-cell" style="background: ${bgColor}; color: ${textColor};" title="${rowType} vs ${colType}: ${cell?.correct || 0}/${total}">
|
||||
${display}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
|
||||
return `
|
||||
<div class="full-chart-row">
|
||||
<div class="full-chart-row-header" style="background: ${rowInfo.backgroundColor}; color: ${rowInfo.color};">
|
||||
@@ -199,7 +282,7 @@ function renderFullTypeChart(cells: FullTypeChartCell[]): string {
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
|
||||
return `
|
||||
<div class="full-type-chart">
|
||||
<div class="full-chart-header-row">
|
||||
@@ -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('')
|
||||
: '<p class="no-data-text">Answer at least 3 questions to see stats</p>';
|
||||
|
||||
|
||||
const worstCombosHtml = worstCombos.length > 0
|
||||
? worstCombos.map(renderCombinationItem).join('')
|
||||
: '<p class="no-data-text">Answer at least 3 questions to see stats</p>';
|
||||
|
||||
|
||||
const typeChartHtml = typeChartStats.map(renderTypeChartCell).join('');
|
||||
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="stats-container">
|
||||
<header class="stats-header">
|
||||
@@ -293,12 +376,12 @@ function renderStatsPage(): void {
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
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) => {
|
||||
|
||||
216
src/style.css
216
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user