mirror of
https://github.com/shadoll/sLogos.git
synced 2025-12-20 03:26:59 +00:00
Refactor session management in quizzes to use centralized restoreSessionState function and improve flag loading logic with shared utilities
This commit is contained in:
@@ -9,14 +9,15 @@
|
|||||||
} from "../quizLogic/quizGlobalStats.js";
|
} from "../quizLogic/quizGlobalStats.js";
|
||||||
import {
|
import {
|
||||||
saveSessionState,
|
saveSessionState,
|
||||||
loadSessionState,
|
|
||||||
clearSessionState,
|
clearSessionState,
|
||||||
createNewSessionState,
|
createNewSessionState,
|
||||||
|
restoreSessionState,
|
||||||
} from "../quizLogic/quizSession.js";
|
} from "../quizLogic/quizSession.js";
|
||||||
import { createAdvanceTimer } from "../quizLogic/advanceTimer.js";
|
import { createAdvanceTimer } from "../quizLogic/advanceTimer.js";
|
||||||
import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js";
|
import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js";
|
||||||
import { quizInfo } from "../quizInfo/CapitalsQuizInfo.js";
|
import { quizInfo } from "../quizInfo/CapitalsQuizInfo.js";
|
||||||
import { onMount } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import { loadFlags as loadFlagsShared, getCountryName, getFlagImage, pickWeightedFlag } from "../quizLogic/flags.js";
|
||||||
import Header from "../components/Header.svelte";
|
import Header from "../components/Header.svelte";
|
||||||
import Footer from "../components/Footer.svelte";
|
import Footer from "../components/Footer.svelte";
|
||||||
import Achievements from "../components/Achievements.svelte";
|
import Achievements from "../components/Achievements.svelte";
|
||||||
@@ -36,10 +37,7 @@
|
|||||||
let quizSubpage = "welcome"; // 'welcome' or 'quiz'
|
let quizSubpage = "welcome"; // 'welcome' or 'quiz'
|
||||||
let selectedAnswer = null;
|
let selectedAnswer = null;
|
||||||
let answered = false;
|
let answered = false;
|
||||||
let isAnswered = false;
|
|
||||||
let resultMessage = "";
|
|
||||||
let showResult = false;
|
let showResult = false;
|
||||||
let timeoutId = null;
|
|
||||||
let showCountryInfo = false;
|
let showCountryInfo = false;
|
||||||
let showResultCountryInfo = false;
|
let showResultCountryInfo = false;
|
||||||
|
|
||||||
@@ -56,8 +54,8 @@
|
|||||||
// Scoring
|
// Scoring
|
||||||
let score = { correct: 0, total: 0, skipped: 0 };
|
let score = { correct: 0, total: 0, skipped: 0 };
|
||||||
let gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 };
|
let gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 };
|
||||||
let wrongAnswers = new Map(); // Track flags answered incorrectly: flag.name -> count
|
let wrongAnswers = new Map();
|
||||||
let correctAnswers = new Map(); // Track flags answered correctly: flag.name -> count
|
let correctAnswers = new Map();
|
||||||
|
|
||||||
// Achievement System
|
// Achievement System
|
||||||
let currentStreak = 0;
|
let currentStreak = 0;
|
||||||
@@ -90,7 +88,7 @@
|
|||||||
let showSessionResults = false;
|
let showSessionResults = false;
|
||||||
let sessionInProgress = false;
|
let sessionInProgress = false;
|
||||||
let sessionStartTime = null;
|
let sessionStartTime = null;
|
||||||
let sessionRestoredFromReload = false; // Track if session was restored from page reload
|
let sessionRestoredFromReload = false;
|
||||||
|
|
||||||
// Update achievement count when achievements component is available
|
// Update achievement count when achievements component is available
|
||||||
$: if (achievementsComponent) {
|
$: if (achievementsComponent) {
|
||||||
@@ -188,76 +186,39 @@
|
|||||||
await loadFlags();
|
await loadFlags();
|
||||||
settingsLoaded = true;
|
settingsLoaded = true;
|
||||||
|
|
||||||
// Load or initialize session
|
// Load or initialize session (centralized)
|
||||||
const loadedSession = loadSessionState("capitalsQuizSessionState", null);
|
const restored = restoreSessionState("capitalsQuizSessionState");
|
||||||
if (loadedSession) {
|
if (restored && restored.sessionInProgress) {
|
||||||
// Restore session
|
sessionInProgress = restored.sessionInProgress;
|
||||||
sessionInProgress = loadedSession.sessionInProgress;
|
currentSessionQuestions = restored.currentSessionQuestions;
|
||||||
currentSessionQuestions = loadedSession.currentSessionQuestions || 0;
|
sessionStats = restored.sessionStats;
|
||||||
sessionStats = loadedSession.sessionStats || {
|
score = restored.score;
|
||||||
correct: 0,
|
currentQuestion = restored.currentQuestion;
|
||||||
wrong: 0,
|
selectedAnswer = restored.selectedAnswer;
|
||||||
skipped: 0,
|
showResult = restored.showResult;
|
||||||
total: 0,
|
gameState = restored.gameState;
|
||||||
sessionLength,
|
quizSubpage = restored.quizSubpage;
|
||||||
};
|
sessionStartTime = restored.sessionStartTime;
|
||||||
score = loadedSession.score || { correct: 0, total: 0, skipped: 0 };
|
questionKey = restored.questionKey || 0;
|
||||||
currentQuestion = loadedSession.currentQuestion;
|
sessionRestoredFromReload = restored.sessionRestoredFromReload;
|
||||||
selectedAnswer = loadedSession.selectedAnswer;
|
|
||||||
showResult = loadedSession.showResult || false;
|
|
||||||
gameState = loadedSession.gameState || "question";
|
|
||||||
quizSubpage = "quiz";
|
|
||||||
sessionStartTime = loadedSession.sessionStartTime;
|
|
||||||
questionKey = loadedSession.questionKey || 0;
|
|
||||||
|
|
||||||
// Mark that session was restored from reload
|
|
||||||
sessionRestoredFromReload = true;
|
|
||||||
|
|
||||||
// If we don't have a current question, generate one
|
|
||||||
if (!currentQuestion) {
|
if (!currentQuestion) {
|
||||||
generateQuestion();
|
generateQuestion();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No saved state, show welcome page
|
|
||||||
quizSubpage = "welcome";
|
quizSubpage = "welcome";
|
||||||
gameState = "welcome";
|
gameState = "welcome";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cleanup on component destroy: cancel any running advance timer
|
||||||
|
onDestroy(() => {
|
||||||
|
if (advanceTimer) advanceTimer.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
async function loadFlags() {
|
async function loadFlags() {
|
||||||
try {
|
flags = await loadFlagsShared({ requireCapital: true });
|
||||||
const response = await fetch("/data/flags.json");
|
console.log(`Loaded ${flags.length} unique country flags for capitals quiz`);
|
||||||
const data = await response.json();
|
|
||||||
// Filter for only country flags (has "Country" tag) and ensure we have country name and capital
|
|
||||||
flags = data.filter(
|
|
||||||
(flag) =>
|
|
||||||
!flag.disable &&
|
|
||||||
flag.meta?.country &&
|
|
||||||
flag.meta?.capital &&
|
|
||||||
flag.tags &&
|
|
||||||
flag.tags.includes("Country"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove duplicates based on country name
|
|
||||||
const uniqueFlags = [];
|
|
||||||
const seenCountries = new Set();
|
|
||||||
|
|
||||||
for (const flag of flags) {
|
|
||||||
const countryName = flag.meta.country.toLowerCase().trim();
|
|
||||||
if (!seenCountries.has(countryName)) {
|
|
||||||
seenCountries.add(countryName);
|
|
||||||
uniqueFlags.push(flag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flags = uniqueFlags;
|
|
||||||
console.log(
|
|
||||||
`Loaded ${flags.length} unique country flags for capitals quiz`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading flags:", error);
|
|
||||||
flags = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateQuestion() {
|
function generateQuestion() {
|
||||||
@@ -297,48 +258,9 @@
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick correct answer with adaptive learning settings
|
// Pick correct answer using shared helper (handles adaptive weighting)
|
||||||
let correctFlag;
|
const pick = pickWeightedFlag(flags, { focusWrongAnswers, reduceCorrectAnswers }, wrongAnswers, correctAnswers);
|
||||||
|
const correctFlag = pick || flags[Math.floor(Math.random() * flags.length)];
|
||||||
// Simple fallback to avoid uninitialized variable errors
|
|
||||||
if (settingsLoaded && (focusWrongAnswers || reduceCorrectAnswers)) {
|
|
||||||
// Re-enable adaptive learning
|
|
||||||
// Create weighted array based on learning settings
|
|
||||||
const weightedFlags = [];
|
|
||||||
for (const flag of flags) {
|
|
||||||
const wrongCount = wrongAnswers.get(flag.name) || 0;
|
|
||||||
const correctCount = correctAnswers.get(flag.name) || 0;
|
|
||||||
|
|
||||||
let weight = 1; // Base weight
|
|
||||||
|
|
||||||
// Increase weight for flags with wrong answers (if setting enabled)
|
|
||||||
if (focusWrongAnswers && wrongCount > 0) {
|
|
||||||
weight = Math.min(wrongCount + 1, 4); // Max 4x weight for wrong answers
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrease weight for flags with correct answers (if setting enabled)
|
|
||||||
if (reduceCorrectAnswers && correctCount > 0) {
|
|
||||||
weight = weight / Math.min(correctCount + 1, 4); // Reduce weight, min 0.25x
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add flag to weighted array based on calculated weight
|
|
||||||
const finalWeight = Math.max(0.25, weight); // Minimum weight to ensure variety
|
|
||||||
const timesToAdd = Math.ceil(finalWeight);
|
|
||||||
for (let i = 0; i < timesToAdd; i++) {
|
|
||||||
weightedFlags.push(flag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weightedFlags.length > 0) {
|
|
||||||
correctFlag =
|
|
||||||
weightedFlags[Math.floor(Math.random() * weightedFlags.length)];
|
|
||||||
} else {
|
|
||||||
correctFlag = flags[Math.floor(Math.random() * flags.length)];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal random selection
|
|
||||||
correctFlag = flags[Math.floor(Math.random() * flags.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
const correctCapital = correctFlag.meta.capital.toLowerCase();
|
const correctCapital = correctFlag.meta.capital.toLowerCase();
|
||||||
|
|
||||||
@@ -687,18 +609,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCountryName(flag) {
|
// Use shared getCountryName/getFlagImage helpers from flags.js
|
||||||
return flag.meta?.country || flag.name || "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCapitalName(flag) {
|
function getCapitalName(flag) {
|
||||||
return flag.meta?.capital || "Unknown";
|
return flag.meta?.capital || "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFlagImage(flag) {
|
|
||||||
return `/images/flags/${flag.path}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAchievementsUnlocked() {
|
function handleAchievementsUnlocked() {
|
||||||
achievementCount = updateAchievementCount(achievementsComponent);
|
achievementCount = updateAchievementCount(achievementsComponent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,15 @@
|
|||||||
import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js";
|
import { playCorrectSound, playWrongSound } from "../quizLogic/quizSound.js";
|
||||||
import {
|
import {
|
||||||
saveSessionState,
|
saveSessionState,
|
||||||
loadSessionState,
|
|
||||||
clearSessionState,
|
clearSessionState,
|
||||||
createNewSessionState,
|
createNewSessionState,
|
||||||
} from "../quizLogic/quizSession.js";
|
} from "../quizLogic/quizSession.js";
|
||||||
|
import { restoreSessionState } from "../quizLogic/quizSession.js";
|
||||||
import { createAdvanceTimer } from "../quizLogic/advanceTimer.js";
|
import { createAdvanceTimer } from "../quizLogic/advanceTimer.js";
|
||||||
|
|
||||||
import { quizInfo } from "../quizInfo/FlagQuizInfo.js";
|
import { quizInfo } from "../quizInfo/FlagQuizInfo.js";
|
||||||
import { onMount } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import { loadFlags as loadFlagsShared, getCountryName, getFlagImage, pickWeightedFlag } from "../quizLogic/flags.js";
|
||||||
import Header from "../components/Header.svelte";
|
import Header from "../components/Header.svelte";
|
||||||
import Footer from "../components/Footer.svelte";
|
import Footer from "../components/Footer.svelte";
|
||||||
import InlineSvg from "../components/InlineSvg.svelte";
|
import InlineSvg from "../components/InlineSvg.svelte";
|
||||||
@@ -31,8 +32,6 @@
|
|||||||
let questionType = "flag-to-country"; // 'flag-to-country' or 'country-to-flag'
|
let questionType = "flag-to-country"; // 'flag-to-country' or 'country-to-flag'
|
||||||
|
|
||||||
// Question and answer arrays
|
// Question and answer arrays
|
||||||
let currentCountryOptions = [];
|
|
||||||
let currentFlagOptions = [];
|
|
||||||
let correctAnswer = "";
|
let correctAnswer = "";
|
||||||
|
|
||||||
// Game states
|
// Game states
|
||||||
@@ -40,10 +39,7 @@
|
|||||||
let quizSubpage = "welcome"; // 'welcome' or 'quiz'
|
let quizSubpage = "welcome"; // 'welcome' or 'quiz'
|
||||||
let selectedAnswer = null;
|
let selectedAnswer = null;
|
||||||
let answered = false;
|
let answered = false;
|
||||||
let isAnswered = false;
|
|
||||||
let resultMessage = "";
|
|
||||||
let showResult = false;
|
let showResult = false;
|
||||||
let timeoutId = null;
|
|
||||||
let showCountryInfo = false;
|
let showCountryInfo = false;
|
||||||
let showResultCountryInfo = false;
|
let showResultCountryInfo = false;
|
||||||
|
|
||||||
@@ -58,8 +54,8 @@
|
|||||||
// Scoring
|
// Scoring
|
||||||
let score = { correct: 0, total: 0, skipped: 0 };
|
let score = { correct: 0, total: 0, skipped: 0 };
|
||||||
let gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 };
|
let gameStats = { correct: 0, wrong: 0, total: 0, skipped: 0 };
|
||||||
let wrongAnswers = new Map(); // Track flags answered incorrectly: flag.name -> count
|
let wrongAnswers = new Map();
|
||||||
let correctAnswers = new Map(); // Track flags answered correctly: flag.name -> count
|
let correctAnswers = new Map();
|
||||||
|
|
||||||
// Achievement System
|
// Achievement System
|
||||||
let currentStreak = 0;
|
let currentStreak = 0;
|
||||||
@@ -89,7 +85,7 @@
|
|||||||
let showSessionResults = false;
|
let showSessionResults = false;
|
||||||
let sessionInProgress = false;
|
let sessionInProgress = false;
|
||||||
let sessionStartTime = null;
|
let sessionStartTime = null;
|
||||||
let sessionRestoredFromReload = false; // Track if session was restored from page reload
|
let sessionRestoredFromReload = false;
|
||||||
|
|
||||||
// Update achievement count when achievements component is available
|
// Update achievement count when achievements component is available
|
||||||
$: if (achievementsComponent) {
|
$: if (achievementsComponent) {
|
||||||
@@ -193,36 +189,24 @@
|
|||||||
await loadFlags();
|
await loadFlags();
|
||||||
settingsLoaded = true;
|
settingsLoaded = true;
|
||||||
|
|
||||||
// Load or initialize session
|
// Load or initialize session (centralized)
|
||||||
const loaded = loadSessionState("flagQuizSessionState", null);
|
const restored = restoreSessionState("flagQuizSessionState");
|
||||||
if (loaded) {
|
if (restored && restored.sessionInProgress) {
|
||||||
if (loaded.sessionInProgress) {
|
sessionInProgress = restored.sessionInProgress;
|
||||||
sessionInProgress = loaded.sessionInProgress;
|
currentSessionQuestions = restored.currentSessionQuestions;
|
||||||
currentSessionQuestions = loaded.currentSessionQuestions || 0;
|
sessionStats = restored.sessionStats;
|
||||||
sessionStats = loaded.sessionStats || {
|
score = restored.score;
|
||||||
correct: 0,
|
currentQuestion = restored.currentQuestion;
|
||||||
wrong: 0,
|
selectedAnswer = restored.selectedAnswer;
|
||||||
skipped: 0,
|
showResult = restored.showResult;
|
||||||
total: 0,
|
gameState = restored.gameState;
|
||||||
sessionLength,
|
quizSubpage = restored.quizSubpage;
|
||||||
};
|
sessionStartTime = restored.sessionStartTime;
|
||||||
score = loaded.score || { correct: 0, total: 0, skipped: 0 };
|
questionKey = restored.questionKey || 0;
|
||||||
currentQuestion = loaded.currentQuestion;
|
sessionRestoredFromReload = restored.sessionRestoredFromReload;
|
||||||
selectedAnswer = loaded.selectedAnswer;
|
|
||||||
showResult = loaded.showResult || false;
|
|
||||||
gameState = loaded.gameState || "question";
|
|
||||||
quizSubpage = "quiz";
|
|
||||||
sessionStartTime = loaded.sessionStartTime;
|
|
||||||
questionKey = loaded.questionKey || 0;
|
|
||||||
|
|
||||||
sessionRestoredFromReload = true;
|
if (!currentQuestion) {
|
||||||
|
generateQuestion();
|
||||||
if (!currentQuestion) {
|
|
||||||
generateQuestion();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quizSubpage = "welcome";
|
|
||||||
gameState = "welcome";
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
quizSubpage = "welcome";
|
quizSubpage = "welcome";
|
||||||
@@ -230,39 +214,16 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cleanup on component destroy: cancel any running advance timer
|
||||||
|
onDestroy(() => {
|
||||||
|
if (advanceTimer) advanceTimer.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
// use shared applyTheme from ../utils/theme.js
|
// use shared applyTheme from ../utils/theme.js
|
||||||
|
|
||||||
async function loadFlags() {
|
async function loadFlags() {
|
||||||
try {
|
flags = await loadFlagsShared();
|
||||||
const response = await fetch("/data/flags.json");
|
console.log(`Loaded ${flags.length} unique country flags for quiz`);
|
||||||
const data = await response.json();
|
|
||||||
// Filter for only country flags (has "Country" tag) and ensure we have country name
|
|
||||||
flags = data.filter(
|
|
||||||
(flag) =>
|
|
||||||
!flag.disable &&
|
|
||||||
flag.meta?.country &&
|
|
||||||
flag.tags &&
|
|
||||||
flag.tags.includes("Country"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove duplicates based on country name
|
|
||||||
const uniqueFlags = [];
|
|
||||||
const seenCountries = new Set();
|
|
||||||
|
|
||||||
for (const flag of flags) {
|
|
||||||
const countryName = flag.meta.country.toLowerCase().trim();
|
|
||||||
if (!seenCountries.has(countryName)) {
|
|
||||||
seenCountries.add(countryName);
|
|
||||||
uniqueFlags.push(flag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flags = uniqueFlags;
|
|
||||||
console.log(`Loaded ${flags.length} unique country flags for quiz`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading flags:", error);
|
|
||||||
flags = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateQuestion() {
|
function generateQuestion() {
|
||||||
@@ -305,50 +266,11 @@
|
|||||||
// Randomly choose question type
|
// Randomly choose question type
|
||||||
questionType = Math.random() < 0.5 ? "flag-to-country" : "country-to-flag";
|
questionType = Math.random() < 0.5 ? "flag-to-country" : "country-to-flag";
|
||||||
|
|
||||||
// Pick correct answer with adaptive learning settings
|
// Pick correct answer with shared helper (handles adaptive settings)
|
||||||
let correctFlag;
|
const pick = pickWeightedFlag(flags, { focusWrongAnswers, reduceCorrectAnswers }, wrongAnswers, correctAnswers);
|
||||||
|
const correctFlag = pick || flags[Math.floor(Math.random() * flags.length)];
|
||||||
|
|
||||||
// Simple fallback to avoid uninitialized variable errors
|
const correctCountry = getCountryName(correctFlag).toLowerCase();
|
||||||
if (settingsLoaded && (focusWrongAnswers || reduceCorrectAnswers)) {
|
|
||||||
// Re-enable adaptive learning
|
|
||||||
// Create weighted array based on learning settings
|
|
||||||
const weightedFlags = [];
|
|
||||||
for (const flag of flags) {
|
|
||||||
const wrongCount = wrongAnswers.get(flag.name) || 0;
|
|
||||||
const correctCount = correctAnswers.get(flag.name) || 0;
|
|
||||||
|
|
||||||
let weight = 1; // Base weight
|
|
||||||
|
|
||||||
// Increase weight for flags with wrong answers (if setting enabled)
|
|
||||||
if (focusWrongAnswers && wrongCount > 0) {
|
|
||||||
weight = Math.min(wrongCount + 1, 4); // Max 4x weight for wrong answers
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrease weight for flags with correct answers (if setting enabled)
|
|
||||||
if (reduceCorrectAnswers && correctCount > 0) {
|
|
||||||
weight = weight / Math.min(correctCount + 1, 4); // Reduce weight, min 0.25x
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add flag to weighted array based on calculated weight
|
|
||||||
const finalWeight = Math.max(0.25, weight); // Minimum weight to ensure variety
|
|
||||||
const timesToAdd = Math.ceil(finalWeight);
|
|
||||||
for (let i = 0; i < timesToAdd; i++) {
|
|
||||||
weightedFlags.push(flag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weightedFlags.length > 0) {
|
|
||||||
correctFlag =
|
|
||||||
weightedFlags[Math.floor(Math.random() * weightedFlags.length)];
|
|
||||||
} else {
|
|
||||||
correctFlag = flags[Math.floor(Math.random() * flags.length)];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal random selection
|
|
||||||
correctFlag = flags[Math.floor(Math.random() * flags.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
const correctCountry = getCountryName(correctFlag).toLowerCase();
|
|
||||||
|
|
||||||
// Generate 3 wrong answers ensuring no duplicate country names
|
// Generate 3 wrong answers ensuring no duplicate country names
|
||||||
const wrongOptions = [];
|
const wrongOptions = [];
|
||||||
@@ -356,7 +278,7 @@
|
|||||||
|
|
||||||
while (wrongOptions.length < 3 && wrongOptions.length < flags.length - 1) {
|
while (wrongOptions.length < 3 && wrongOptions.length < flags.length - 1) {
|
||||||
const randomFlag = flags[Math.floor(Math.random() * flags.length)];
|
const randomFlag = flags[Math.floor(Math.random() * flags.length)];
|
||||||
const randomCountry = getCountryName(randomFlag).toLowerCase();
|
const randomCountry = getCountryName(randomFlag).toLowerCase();
|
||||||
|
|
||||||
if (!usedCountries.has(randomCountry)) {
|
if (!usedCountries.has(randomCountry)) {
|
||||||
wrongOptions.push(randomFlag);
|
wrongOptions.push(randomFlag);
|
||||||
@@ -373,9 +295,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Combine correct and wrong answers
|
// Combine correct and wrong answers
|
||||||
const allOptions = [correctFlag, ...wrongOptions].sort(
|
const allOptions = [correctFlag, ...wrongOptions].sort(() => Math.random() - 0.5);
|
||||||
() => Math.random() - 0.5,
|
|
||||||
);
|
|
||||||
currentQuestion = {
|
currentQuestion = {
|
||||||
type: questionType,
|
type: questionType,
|
||||||
correct: correctFlag,
|
correct: correctFlag,
|
||||||
@@ -683,13 +603,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCountryName(flag) {
|
// use shared getCountryName from ../quizLogic/flags.js
|
||||||
return flag.meta?.country || flag.name || "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFlagImage(flag) {
|
|
||||||
return `/images/flags/${flag.path}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAchievementsUnlocked() {
|
function handleAchievementsUnlocked() {
|
||||||
achievementCount = updateAchievementCount(achievementsComponent);
|
achievementCount = updateAchievementCount(achievementsComponent);
|
||||||
|
|||||||
73
src/quizLogic/flags.js
Normal file
73
src/quizLogic/flags.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Utilities for loading and choosing flags used by quiz pages
|
||||||
|
export async function loadFlags({ requireCapital = false } = {}) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/data/flags.json');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
let flags = data.filter((flag) => {
|
||||||
|
if (flag.disable) return false;
|
||||||
|
if (!flag.meta?.country) return false;
|
||||||
|
if (requireCapital && !flag.meta?.capital) return false;
|
||||||
|
if (!flag.tags || !flag.tags.includes('Country')) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove duplicates based on country name
|
||||||
|
const uniqueFlags = [];
|
||||||
|
const seenCountries = new Set();
|
||||||
|
|
||||||
|
for (const flag of flags) {
|
||||||
|
const countryName = (flag.meta.country || flag.name || '').toLowerCase().trim();
|
||||||
|
if (!seenCountries.has(countryName)) {
|
||||||
|
seenCountries.add(countryName);
|
||||||
|
uniqueFlags.push(flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueFlags;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('flags.loadFlags error', err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCountryName(flag) {
|
||||||
|
return flag?.meta?.country || flag?.name || 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFlagImage(flag) {
|
||||||
|
return `/images/flags/${flag.path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a flag from a weighted list based on wrong/correct answer maps and settings
|
||||||
|
export function pickWeightedFlag(flags, { focusWrongAnswers = false, reduceCorrectAnswers = false } = {}, wrongAnswers = new Map(), correctAnswers = new Map()) {
|
||||||
|
if (!Array.isArray(flags) || flags.length === 0) return null;
|
||||||
|
|
||||||
|
// If no adaptive settings enabled, return a random flag
|
||||||
|
if (!focusWrongAnswers && !reduceCorrectAnswers) {
|
||||||
|
return flags[Math.floor(Math.random() * flags.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const weighted = [];
|
||||||
|
for (const flag of flags) {
|
||||||
|
const wrongCount = wrongAnswers.get(flag.name) || 0;
|
||||||
|
const correctCount = correctAnswers.get(flag.name) || 0;
|
||||||
|
|
||||||
|
let weight = 1;
|
||||||
|
if (focusWrongAnswers && wrongCount > 0) {
|
||||||
|
weight = Math.min(wrongCount + 1, 4);
|
||||||
|
}
|
||||||
|
if (reduceCorrectAnswers && correctCount > 0) {
|
||||||
|
weight = weight / Math.min(correctCount + 1, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalWeight = Math.max(0.25, weight);
|
||||||
|
const times = Math.ceil(finalWeight);
|
||||||
|
for (let i = 0; i < times; i++) weighted.push(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weighted.length > 0) {
|
||||||
|
return weighted[Math.floor(Math.random() * weighted.length)];
|
||||||
|
}
|
||||||
|
return flags[Math.floor(Math.random() * flags.length)];
|
||||||
|
}
|
||||||
@@ -40,3 +40,32 @@ export function createNewSessionState(sessionLength = 10) {
|
|||||||
sessionRestoredFromReload: false,
|
sessionRestoredFromReload: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore session helper: loads saved session state (if any) and returns a normalized object
|
||||||
|
export function restoreSessionState(key) {
|
||||||
|
const loaded = loadSessionState(key, null);
|
||||||
|
if (!loaded) return null;
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
sessionInProgress: loaded.sessionInProgress || false,
|
||||||
|
currentSessionQuestions: loaded.currentSessionQuestions || 0,
|
||||||
|
sessionStats: loaded.sessionStats || {
|
||||||
|
correct: 0,
|
||||||
|
wrong: 0,
|
||||||
|
skipped: 0,
|
||||||
|
total: 0,
|
||||||
|
sessionLength: loaded?.sessionStats?.sessionLength || 10,
|
||||||
|
},
|
||||||
|
score: loaded.score || { correct: 0, total: 0, skipped: 0 },
|
||||||
|
currentQuestion: loaded.currentQuestion || null,
|
||||||
|
selectedAnswer: loaded.selectedAnswer || null,
|
||||||
|
showResult: loaded.showResult || false,
|
||||||
|
gameState: loaded.gameState || "question",
|
||||||
|
quizSubpage: loaded.quizSubpage || "quiz",
|
||||||
|
sessionStartTime: loaded.sessionStartTime || null,
|
||||||
|
questionKey: loaded.questionKey || 0,
|
||||||
|
sessionRestoredFromReload: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export function applyTheme(theme) {
|
export function applyTheme(theme) {
|
||||||
let effectiveTheme = theme;
|
let effectiveTheme = theme;
|
||||||
if (theme === "system") {
|
if (theme === "system") {
|
||||||
@@ -14,6 +16,12 @@ export function setTheme(newTheme) {
|
|||||||
if (newTheme === "light" || newTheme === "dark" || newTheme === "system") {
|
if (newTheme === "light" || newTheme === "dark" || newTheme === "system") {
|
||||||
// Persist choice and apply immediately
|
// Persist choice and apply immediately
|
||||||
localStorage.setItem("theme", newTheme);
|
localStorage.setItem("theme", newTheme);
|
||||||
|
// Update reactive store so $themeStore updates everywhere immediately
|
||||||
|
try {
|
||||||
|
themeStore.set(newTheme);
|
||||||
|
} catch (e) {
|
||||||
|
// If themeStore isn't initialized yet, ignore — it will be set on module init
|
||||||
|
}
|
||||||
console.log("[Theme] setTheme:", newTheme);
|
console.log("[Theme] setTheme:", newTheme);
|
||||||
setTimeout(() => applyTheme(newTheme), 0);
|
setTimeout(() => applyTheme(newTheme), 0);
|
||||||
return newTheme;
|
return newTheme;
|
||||||
@@ -25,5 +33,4 @@ export function getStoredTheme(defaultTheme = "system") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Svelte store for reactive theme across components
|
// Svelte store for reactive theme across components
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
export const themeStore = writable(getStoredTheme());
|
export const themeStore = writable(getStoredTheme());
|
||||||
|
|||||||
Reference in New Issue
Block a user