From 0a459267037027396beb0b70f22381f8ef4032fc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 8 Nov 2026 05:42:07 +0000 Subject: [PATCH] refactor(gamification): centralize endgame mission lookup Extract duplicated endgame mission ID parsing logic into a pure utility `getEndgameMissionDef` in `src/utils/gamification/missionLogic.ts`. Replaces brittle inline string splitting and arrays searches previously scattered across: - `src/utils/gamification/missionLogic.ts` - `src/utils/gamification/questionLogic.ts` - `src/components/gamification/MissionList/MissionList.tsx` This adheres to DRY and makes ID resolution safer. Co-authored-by: albertoivo <1276520+albertoivo@users.noreply.github.com> --- .agent/logs/refactor-journal.md | 11 +++++ .../gamification/MissionList/MissionList.tsx | 12 ++--- src/utils/gamification/missionLogic.ts | 44 ++++++++++++------- src/utils/gamification/questionLogic.ts | 11 ++--- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/.agent/logs/refactor-journal.md b/.agent/logs/refactor-journal.md index 030a65c..a8d851c 100644 --- a/.agent/logs/refactor-journal.md +++ b/.agent/logs/refactor-journal.md @@ -24,3 +24,14 @@ - `src/hooks/gamification/useGamificationStore.ts`: Replaced inline logic with the pure function call and handled side-effects cleanly. - **๐Ÿงน Architectural Gain:** SRP โ€” `useGamificationStore` now focuses solely on state management and side effects, while domain rules for post-load logic reside in purely testable utility functions. - **๐Ÿ”ฌ Verification:** Confirmed that `tsc -b --noEmit`, `npm test`, `npm run lint`, and `npm run build` all pass successfully. + +## 2025-02-14 โ€” Centralize Endgame Mission Lookup + +- **๐Ÿ’ก What:** Extracted duplicated logic for finding and mapping endgame missions by ID into a pure `getEndgameMissionDef` function. Replaced inline string-splitting parsing inside `missionLogic.ts`, `questionLogic.ts` and `MissionList.tsx`. +- **๐ŸŽฏ Why:** Code smell identified (DRY Violation, brittle string splitting). The logic for resolving endgame missions (e.g. `endgame_speedrun_0` to the base `Mission` definition) was duplicated across three different files, with inconsistent fallback mechanisms. +- **๐Ÿ“ Files Changed:** + - `src/utils/gamification/missionLogic.ts`: Created `getEndgameMissionDef` and refactored multiple spots to use it. + - `src/utils/gamification/questionLogic.ts`: Replaced find logic with `getEndgameMissionDef`. + - `src/components/gamification/MissionList/MissionList.tsx`: Simplified UI mapping. +- **๐Ÿงน Architectural Gain:** DRY principle โ€” centralized the ID parsing rules into a single source of truth that safely merges dynamic user mission IDs with static generic definitions. +- **๐Ÿ”ฌ Verification:** Confirmed that `npx tsc -b --noEmit`, `npm test`, `npm run lint`, and `CI=true npm run build` all pass successfully. diff --git a/src/components/gamification/MissionList/MissionList.tsx b/src/components/gamification/MissionList/MissionList.tsx index 9ce979c..8d2ed18 100644 --- a/src/components/gamification/MissionList/MissionList.tsx +++ b/src/components/gamification/MissionList/MissionList.tsx @@ -1,7 +1,7 @@ import type { Mission, UserMission } from '../../../types/gamification'; import './MissionList.css'; -import { ENDGAME_MISSIONS } from '../../../data/gamificationData'; +import { getEndgameMissionDef } from '../../../utils/gamification/missionLogic'; interface MissionListProps { dailyMissions: Mission[]; @@ -21,16 +21,10 @@ export function MissionList({ dailyMissions, weeklyMissions, userMissions }: Mis // Construct Mission objects for them const activeEndgameMissions: Mission[] = endgameUserMissions.map(um => { - // Try to find matching definition - // ID format: endgame_{objectiveType}_{index} - const typePart = um.missionId.split('_')[1]; - const def = ENDGAME_MISSIONS.find(m => m.objectiveType === typePart || (typePart === 'improve' && m.objectiveType === 'improve_stars') || (typePart === 'syntax' && m.objectiveType === 'syntax_master')); + const def = getEndgameMissionDef(um.missionId); if (def) { - return { - ...def, - id: um.missionId - }; + return def; } // Fallback if not found (should not happen if data is consistent) return { diff --git a/src/utils/gamification/missionLogic.ts b/src/utils/gamification/missionLogic.ts index 80d52a7..540f6f2 100644 --- a/src/utils/gamification/missionLogic.ts +++ b/src/utils/gamification/missionLogic.ts @@ -175,10 +175,8 @@ export function claimMissionRewardLogic( // Note: ENDGAME_MISSIONS do not have 'id' property in definitions, so we cast to any or handle specifically let missionDef = allMissions.find(m => 'id' in m && m.id === missionId); - // If not found, try to find in ENDGAME_MISSIONS by title matching (since we construct IDs dynamically) if (!missionDef) { - // Find endgame mission definition by checking if the missionId (from user state) matches the constructed pattern - missionDef = ENDGAME_MISSIONS.find(m => missionId.startsWith(`endgame_${m.objectiveType}`)) as Mission; + missionDef = getEndgameMissionDef(missionId); } if (!missionDef) { @@ -242,7 +240,7 @@ export function updateMissionProgress( let missionDef = allMissions.find(m => 'id' in m && m.id === userMission.missionId); if (!missionDef) { - missionDef = ENDGAME_MISSIONS.find(m => userMission.missionId.startsWith(`endgame_${m.objectiveType}`)) as Mission; + missionDef = getEndgameMissionDef(userMission.missionId); } if (!missionDef) return userMission; @@ -371,6 +369,29 @@ function processHydration(mission: Mission, originalId: string): Mission { return mission; } +export function getEndgameMissionDef(missionId: string): Mission | undefined { + if (!missionId.startsWith('endgame_')) return undefined; + + let found = ENDGAME_MISSIONS.find(m => missionId.startsWith(`endgame_${m.objectiveType}`)); + + // Fallback for legacy ID formats (e.g. endgame_improve_0) + if (!found) { + const typePart = missionId.split('_')[1]; + if (typePart) { + found = ENDGAME_MISSIONS.find(m => + m.objectiveType === typePart || + (typePart === 'improve' && m.objectiveType === 'improve_stars') || + (typePart === 'syntax' && m.objectiveType === 'syntax_master') + ); + } + } + + if (found) { + return { ...found, id: missionId } as Mission; + } + return undefined; +} + export function hydrateMissions(activeMissions: UserGamification['activeMissions']): { daily: Mission[], weekly: Mission[] } { const daily: Mission[] = []; const weekly: Mission[] = []; @@ -399,17 +420,10 @@ export function hydrateMissions(activeMissions: UserGamification['activeMissions if (found) weekly.push(isReview ? processHydration(found, um.missionId) : found); } } else if (baseId.startsWith('endgame_')) { - // Handle endgame hydration if needed (already static basically) - // But we need to match the specific endgame mission - // const typePart = baseId.split('_')[1]; - const found = ENDGAME_MISSIONS.find(m => baseId.startsWith(`endgame_${m.objectiveType}`)); - if (found) { - const hydrated = { ...found, id: um.missionId } as Mission; // Cast needed as ENDGAME lacks ID - daily.push(hydrated); // Endgame usually shown in daily or separate list? The UI splits daily/weekly. - // Ideally endgame is its own category. But for now put in Daily or Weekly based on... choice. - // Actually the UI currently only shows Daily/Weekly props. - // We might need to add 'endgame' prop to UI later. - // For now, let's just push to daily to ensure visibility. + const foundDef = getEndgameMissionDef(baseId); + if (foundDef) { + const hydrated = { ...foundDef, id: um.missionId }; + daily.push(isReview ? processHydration(hydrated, um.missionId) : hydrated); } } }); diff --git a/src/utils/gamification/questionLogic.ts b/src/utils/gamification/questionLogic.ts index 319933d..5518f6d 100644 --- a/src/utils/gamification/questionLogic.ts +++ b/src/utils/gamification/questionLogic.ts @@ -1,6 +1,7 @@ -import type { UserGamification, LevelInfo, Mission } from '../../types/gamification'; -import { getLevelFromXP, ENDGAME_MISSIONS, generateDailyMissions, generateWeeklyMissions } from '../../data/gamificationData'; +import type { UserGamification, LevelInfo } from '../../types/gamification'; +import { getLevelFromXP, generateDailyMissions, generateWeeklyMissions } from '../../data/gamificationData'; import { calculateStreak } from '../gamificationUtils'; +import { getEndgameMissionDef } from './missionLogic'; import { getInitialPet, gainPetXp } from '../petLogic'; export function recordQuestionLogic( @@ -114,11 +115,7 @@ export function recordQuestionLogic( let def = [...daily, ...weekly].find(m => m.id === mission.missionId); if (!def && mission.missionId.startsWith('endgame_')) { - const typePart = mission.missionId.split('_')[1]; - const found = ENDGAME_MISSIONS.find(m => m.objectiveType === typePart || (typePart === 'improve' && m.objectiveType === 'improve_stars') || (typePart === 'syntax' && m.objectiveType === 'syntax_master')); - if (found) { - def = { ...found, id: mission.missionId } as Mission; - } + def = getEndgameMissionDef(mission.missionId); } if (!def) return mission;