diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt
index 1652803..cc69d6a 100644
--- a/docs/circular-deps.txt
+++ b/docs/circular-deps.txt
@@ -1,8 +1,8 @@
# Circular Dependencies
-Generated: 2026-05-19T10:51:45.659Z
+Generated: 2026-05-19T11:53:37.574Z
Found: 3 circular chain(s) — these MUST be fixed before modifying involved files.
-1. Processed 121 files (1.3s) (4 warnings)
+1. Processed 121 files (1.2s) (4 warnings)
2. 1) data/equipment/index.ts > data/equipment/utils.ts
3. 2) data/golems/index.ts > data/golems/utils.ts
diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json
index 149d345..7f0dec5 100644
--- a/docs/dependency-graph.json
+++ b/docs/dependency-graph.json
@@ -1,6 +1,6 @@
{
"_meta": {
- "generated": "2026-05-19T10:51:44.106Z",
+ "generated": "2026-05-19T11:53:36.198Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
},
@@ -409,7 +409,9 @@
],
"stores/combat-actions.ts": [
"constants.ts",
+ "effects/discipline-effects.ts",
"stores/combat-state.types.ts",
+ "stores/discipline-slice.ts",
"stores/prestigeStore.ts",
"types.ts",
"utils/index.ts"
@@ -451,7 +453,9 @@
"utils/discipline-math.ts"
],
"stores/gameActions.ts": [
+ "effects/discipline-effects.ts",
"stores/combatStore.ts",
+ "stores/discipline-slice.ts",
"stores/manaStore.ts",
"stores/prestigeStore.ts",
"stores/uiStore.ts",
@@ -460,8 +464,10 @@
"stores/gameHooks.ts": [
"constants.ts",
"effects.ts",
+ "effects/discipline-effects.ts",
"stores/combatStore.ts",
"stores/craftingStore.ts",
+ "stores/discipline-slice.ts",
"stores/gameStore.ts",
"stores/manaStore.ts",
"stores/prestigeStore.ts",
@@ -470,7 +476,9 @@
],
"stores/gameLoopActions.ts": [
"constants.ts",
+ "effects/discipline-effects.ts",
"stores/combatStore.ts",
+ "stores/discipline-slice.ts",
"stores/manaStore.ts",
"stores/prestigeStore.ts",
"stores/uiStore.ts",
@@ -500,6 +508,7 @@
"stores/combat-state.types.ts",
"stores/combatStore.ts",
"stores/craftingStore.ts",
+ "stores/discipline-slice.ts",
"stores/gameHooks.ts",
"stores/gameStore.ts",
"stores/manaStore.ts",
@@ -554,7 +563,8 @@
"utils/combat-utils.ts": [
"constants.ts",
"data/enchantment-effects.ts",
- "types.ts"
+ "types.ts",
+ "utils/mana-utils.ts"
],
"utils/discipline-math.ts": [
"types/disciplines.ts"
diff --git a/docs/project-structure.txt b/docs/project-structure.txt
index 39a5463..1d3a0c5 100644
--- a/docs/project-structure.txt
+++ b/docs/project-structure.txt
@@ -88,6 +88,15 @@ Mana-Loop/
│ │ │ │ └── index.tsx
│ │ │ ├── shared/
│ │ │ ├── tabs/
+│ │ │ │ ├── DebugTab/
+│ │ │ │ │ ├── AchievementDebugSection.tsx
+│ │ │ │ │ ├── AttunementDebugSection.tsx
+│ │ │ │ │ ├── DisciplineDebugSection.tsx
+│ │ │ │ │ ├── ElementDebugSection.tsx
+│ │ │ │ │ ├── GameStateDebugSection.tsx
+│ │ │ │ │ ├── GolemDebugSection.tsx
+│ │ │ │ │ ├── PactDebugSection.tsx
+│ │ │ │ │ └── SpireDebugSection.tsx
│ │ │ │ ├── StatsTab/
│ │ │ │ │ ├── CombatStatsSection.tsx
│ │ │ │ │ ├── ElementStatsSection.tsx
@@ -95,6 +104,7 @@ Mana-Loop/
│ │ │ │ │ ├── ManaStatsSection.tsx
│ │ │ │ │ ├── PactStatusSection.tsx
│ │ │ │ │ └── StudyStatsSection.tsx
+│ │ │ │ ├── AchievementsTab.tsx
│ │ │ │ ├── ActivityLog.tsx
│ │ │ │ ├── DisciplinesTab.tsx
│ │ │ │ ├── SpellsTab.tsx
@@ -147,6 +157,7 @@ Mana-Loop/
│ ├── game/
│ │ ├── __tests__/
│ │ │ ├── store-method-tests/
+│ │ │ ├── achievements.test.ts
│ │ │ ├── bug-fixes.test.ts
│ │ │ ├── computed-stats.test.ts
│ │ │ └── regression-fixes.test.ts
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 1677a0d..44809a5 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -44,6 +44,7 @@ import { LeftPanel } from './components/LeftPanel';
const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DisciplinesTab })));
const SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab })));
const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab })));
+const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab })));
const TabLoadingFallback = () =>
Loading...
;
@@ -233,6 +234,7 @@ export default function ManaLoopGame() {
📊 Stats
📚 Disciplines
📖 Grimoire
+ 🏆 Achievements
@@ -262,6 +264,14 @@ export default function ManaLoopGame() {
+
+
+ achievements tab failed to load.}>
+ }>
+
+
+
+
diff --git a/src/components/game/tabs/AchievementsTab.tsx b/src/components/game/tabs/AchievementsTab.tsx
new file mode 100644
index 0000000..4cad98f
--- /dev/null
+++ b/src/components/game/tabs/AchievementsTab.tsx
@@ -0,0 +1,250 @@
+'use client';
+
+import { useState, useMemo, useEffect } from 'react';
+import { useCombatStore } from '@/lib/game/stores';
+import {
+ ACHIEVEMENTS,
+ ACHIEVEMENT_CATEGORY_COLORS,
+ getAchievementsByCategory,
+ isAchievementRevealed,
+} from '@/lib/game/data/achievements';
+import type { AchievementDef } from '@/lib/game/types';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { SectionHeader } from '@/components/ui/section-header';
+import { DebugName } from '@/components/game/debug/debug-context';
+import { fmt } from '@/lib/game/stores';
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+const CATEGORY_LABELS: Record = {
+ combat: '⚔️ Combat',
+ progression: '📈 Progression',
+ crafting: '🔨 Crafting',
+ magic: '✨ Magic',
+ special: '🌟 Special',
+};
+
+function getProgressForAchievement(
+ achievement: AchievementDef,
+ progress: Record,
+): number {
+ return progress[achievement.id] ?? 0;
+}
+
+function getProgressPercent(achievement: AchievementDef, current: number): number {
+ if (achievement.requirement.value <= 0) return 100;
+ return Math.min(100, Math.round((current / achievement.requirement.value) * 100));
+}
+
+function formatReward(reward: AchievementDef['reward']): string {
+ const parts: string[] = [];
+ if (reward.insight) parts.push(`${reward.insight} insight`);
+ if (reward.manaBonus) parts.push(`+${reward.manaBonus} mana`);
+ if (reward.damageBonus) parts.push(`+${(reward.damageBonus * 100).toFixed(0)}% dmg`);
+ if (reward.regenBonus) parts.push(`+${reward.regenBonus} regen`);
+ if (reward.title) parts.push(`title: "${reward.title}"`);
+ if (reward.unlockEffect) parts.push(`unlock: ${reward.unlockEffect}`);
+ return parts.join(', ');
+}
+
+// ─── Category Section ────────────────────────────────────────────────────────
+
+interface CategorySectionProps {
+ category: string;
+ achievements: AchievementDef[];
+ unlocked: string[];
+ progress: Record;
+ collapsed: boolean;
+ onToggleCollapse: () => void;
+}
+
+function CategorySection({
+ category,
+ achievements,
+ unlocked,
+ progress,
+ collapsed,
+ onToggleCollapse,
+}: CategorySectionProps) {
+ const color = ACHIEVEMENT_CATEGORY_COLORS[category] ?? '#9CA3AF';
+ const label = CATEGORY_LABELS[category] ?? category;
+ const unlockedCount = achievements.filter((a) => unlocked.includes(a.id)).length;
+
+ return (
+
+
+ {collapsed ? 'Expand ▼' : 'Collapse ▲'}
+
+ }
+ />
+ {!collapsed && (
+
+ {achievements.map((achievement) => {
+ const isUnlocked = unlocked.includes(achievement.id);
+ const currentProgress = getProgressForAchievement(achievement, progress);
+ const percent = getProgressPercent(achievement, currentProgress);
+ const revealed = isAchievementRevealed(achievement, currentProgress);
+
+ if (!revealed) {
+ return (
+
+
+ ???
+
+ Hidden achievement — keep progressing to reveal
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {achievement.name}
+
+ {isUnlocked && (
+
+ ✓ Unlocked
+
+ )}
+
+
+ {fmt(currentProgress)} / {fmt(achievement.requirement.value)}
+
+
+
+
{achievement.desc}
+
+
+
+
+ Reward:{' '}
+ {formatReward(achievement.reward)}
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+// ─── Main Component ──────────────────────────────────────────────────────────
+
+export function AchievementsTab() {
+ const achievements = useCombatStore((s) => s.achievements);
+ const [mounted, setMounted] = useState(false);
+ const [collapsedCategories, setCollapsedCategories] = useState>({});
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const byCategory = useMemo(() => getAchievementsByCategory(), []);
+ const categories = useMemo(
+ () => Object.keys(byCategory).sort(),
+ [byCategory],
+ );
+
+ const totalAchievements = Object.keys(ACHIEVEMENTS).length;
+ const unlockedCount = achievements.unlocked.length;
+
+ const toggleCollapse = (category: string) => {
+ setCollapsedCategories((prev) => ({
+ ...prev,
+ [category]: !prev[category],
+ }));
+ };
+
+ if (!mounted) {
+ return (
+
+ Loading achievements…
+
+ );
+ }
+
+ return (
+
+
+ {/* Summary header */}
+
+
+
+
+
+ Achievements
+
+
+ Track your progress and unlock rewards
+
+
+
+
+ {unlockedCount}
+
+ /{totalAchievements}
+
+
+
+
+
+
+
+
+ {/* Category sections */}
+
+
+ {categories.map((category) => (
+ toggleCollapse(category)}
+ />
+ ))}
+
+
+
+
+ );
+}
+
+AchievementsTab.displayName = 'AchievementsTab';
diff --git a/src/components/game/tabs/DebugTab/AchievementDebugSection.tsx b/src/components/game/tabs/DebugTab/AchievementDebugSection.tsx
new file mode 100644
index 0000000..32cf986
--- /dev/null
+++ b/src/components/game/tabs/DebugTab/AchievementDebugSection.tsx
@@ -0,0 +1,83 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Trophy, CheckCircle, RotateCcw } from 'lucide-react';
+import { useCombatStore } from '@/lib/game/stores';
+import { ACHIEVEMENTS } from '@/lib/game/data/achievements';
+
+export function AchievementDebugSection() {
+ const achievements = useCombatStore((s) => s.achievements);
+
+ const unlockedCount = achievements?.unlocked?.length || 0;
+ const totalCount = Object.keys(ACHIEVEMENTS).length;
+
+ const handleUnlockAll = () => {
+ useCombatStore.setState({
+ achievements: {
+ unlocked: Object.keys(ACHIEVEMENTS),
+ progress: Object.fromEntries(
+ Object.keys(ACHIEVEMENTS).map((id) => [id, ACHIEVEMENTS[id].requirement.value])
+ ),
+ },
+ });
+ };
+
+ const handleResetAll = () => {
+ useCombatStore.setState({
+ achievements: {
+ unlocked: [],
+ progress: {},
+ },
+ });
+ };
+
+ return (
+
+
+
+
+ Achievement Debug
+
+
+
+
+ Unlocked: {unlockedCount} / {totalCount}
+
+
+
+
+
+
+
+
+ {Object.entries(ACHIEVEMENTS).map(([id, def]) => {
+ const isUnlocked = achievements?.unlocked?.includes(id);
+ return (
+
+
+ {def.name}
+ ({def.category})
+
+ {isUnlocked && (
+
+ )}
+
+ );
+ })}
+
+
+
+ );
+}
+
+AchievementDebugSection.displayName = "AchievementDebugSection";
diff --git a/src/components/game/tabs/DebugTab/AttunementDebugSection.tsx b/src/components/game/tabs/DebugTab/AttunementDebugSection.tsx
new file mode 100644
index 0000000..a1a46bf
--- /dev/null
+++ b/src/components/game/tabs/DebugTab/AttunementDebugSection.tsx
@@ -0,0 +1,88 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Sparkles, Unlock } from 'lucide-react';
+import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements';
+import { useAttunementStore } from '@/lib/game/stores';
+import { useManaStore } from '@/lib/game/stores';
+
+export function AttunementDebugSection() {
+ const attunements = useAttunementStore((s) => s.attunements);
+ const debugUnlockAttunement = useAttunementStore((s) => s.debugUnlockAttunement);
+ const addAttunementXP = useAttunementStore((s) => s.addAttunementXP);
+
+ const handleUnlockAttunement = (id: string) => {
+ if (debugUnlockAttunement) {
+ debugUnlockAttunement(id);
+ if (id === 'enchanter') {
+ useManaStore.getState().unlockElement('transference', 0);
+ }
+ }
+ };
+
+ const handleAddAttunementXP = (id: string, amount: number) => {
+ if (addAttunementXP) {
+ addAttunementXP(id, amount);
+ }
+ };
+
+ const handleUnlockAll = () => {
+ Object.keys(ATTUNEMENTS_DEF).forEach((id) => {
+ handleUnlockAttunement(id);
+ });
+ };
+
+ return (
+
+
+
+
+ Attunements
+
+
+
+
+ {Object.entries(ATTUNEMENTS_DEF).map(([id, def]) => {
+ const isActive = attunements?.[id]?.active;
+ const level = attunements?.[id]?.level || 1;
+ const xp = attunements?.[id]?.experience || 0;
+
+ return (
+
+
+
{def.icon}
+
+
{def.name}
+ {isActive && (
+
Lv.{level} • {xp} XP
+ )}
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+AttunementDebugSection.displayName = "AttunementDebugSection";
diff --git a/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx b/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx
new file mode 100644
index 0000000..3433afa
--- /dev/null
+++ b/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx
@@ -0,0 +1,135 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { BookOpen, Plus, Pause, Play } from 'lucide-react';
+import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
+import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines';
+
+export function DisciplineDebugSection() {
+ const disciplines = useDisciplineStore((s) => s.disciplines);
+ const activeIds = useDisciplineStore((s) => s.activeIds);
+ const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit);
+ const activate = useDisciplineStore((s) => s.activate);
+ const deactivate = useDisciplineStore((s) => s.deactivate);
+
+ const handleTogglePause = (id: string) => {
+ const disc = disciplines[id];
+ if (!disc) return;
+ if (disc.paused) {
+ activate(id);
+ } else {
+ deactivate(id);
+ // Re-activate with paused false — just activate again
+ activate(id);
+ }
+ };
+
+ const handleAddXP = (id: string, amount: number) => {
+ useDisciplineStore.setState((s) => {
+ const disc = s.disciplines[id];
+ if (!disc) return s;
+ return {
+ disciplines: {
+ ...s.disciplines,
+ [id]: { ...disc, xp: disc.xp + amount },
+ },
+ };
+ });
+ };
+
+ const handleActivateAll = () => {
+ ALL_DISCIPLINES.forEach((d) => {
+ if (!activeIds.includes(d.id)) {
+ activate(d.id);
+ }
+ });
+ };
+
+ const handleDeactivateAll = () => {
+ activeIds.forEach((id) => {
+ deactivate(id);
+ });
+ };
+
+ return (
+
+
+
+
+ Disciplines
+
+
+
+
+
+
+
+
+ Active: {activeIds.length} / {concurrentLimit}
+
+
+ {ALL_DISCIPLINES.map((def) => {
+ const disc = disciplines[def.id];
+ const isActive = activeIds.includes(def.id);
+ const xp = disc?.xp || 0;
+ const isPaused = disc?.paused ?? true;
+
+ return (
+
+
+
{def.name}
+
+ {isActive ? `XP: ${xp}` : 'Inactive'}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+DisciplineDebugSection.displayName = "DisciplineDebugSection";
diff --git a/src/components/game/tabs/DebugTab/ElementDebugSection.tsx b/src/components/game/tabs/DebugTab/ElementDebugSection.tsx
new file mode 100644
index 0000000..0f960e7
--- /dev/null
+++ b/src/components/game/tabs/DebugTab/ElementDebugSection.tsx
@@ -0,0 +1,92 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Star, Lock } from 'lucide-react';
+import { useManaStore } from '@/lib/game/stores';
+import { ELEMENTS } from '@/lib/game/constants';
+
+export function ElementDebugSection() {
+ const elements = useManaStore((s) => s.elements);
+
+ const handleUnlockElement = (element: string) => {
+ useManaStore.getState().unlockElement(element, 0);
+ };
+
+ const handleAddElementalMana = (element: string, amount: number) => {
+ const elem = elements?.[element];
+ if (elem?.unlocked) {
+ useManaStore.getState().addElementMana(element, amount, elem.max);
+ }
+ };
+
+ const handleUnlockAll = () => {
+ Object.keys(elements || {}).forEach((id) => {
+ if (!elements[id]?.unlocked) {
+ handleUnlockElement(id);
+ }
+ });
+ };
+
+ return (
+
+
+
+
+ Elemental Mana
+
+
+
+
+
+
+
+ {Object.entries(elements || {}).map(([id, elem]) => {
+ const def = ELEMENTS[id];
+ return (
+
+
{def?.sym}
+
{def?.name}
+
+ {elem.current}/{elem.max}
+
+ {!elem.unlocked && (
+
+ )}
+ {elem.unlocked && (
+
+ )}
+
+ );
+ })}
+
+
+
+ );
+}
+
+ElementDebugSection.displayName = "ElementDebugSection";
diff --git a/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx b/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx
new file mode 100644
index 0000000..ff7b77b
--- /dev/null
+++ b/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx
@@ -0,0 +1,235 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Separator } from '@/components/ui/separator';
+import { Switch } from '@/components/ui/switch';
+import { Label } from '@/components/ui/label';
+import {
+ RotateCcw, AlertTriangle, Zap, Clock, Eye,
+} from 'lucide-react';
+import { useDebug } from '@/components/game/debug/debug-context';
+import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores';
+import { computeMaxMana } from '@/lib/game/stores';
+
+export function GameStateDebugSection() {
+ const [confirmReset, setConfirmReset] = useState(false);
+ const { showComponentNames, toggleComponentNames } = useDebug();
+
+ const rawMana = useManaStore((s) => s.rawMana);
+ const day = useGameStore((s) => s.day);
+ const hour = useGameStore((s) => s.hour);
+ const paused = useUIStore((s) => s.paused);
+ const togglePause = useUIStore((s) => s.togglePause);
+
+ const resetGame = useGameStore((s) => s.resetGame);
+ const gatherMana = useGameStore((s) => s.gatherMana);
+ const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
+ const resetFloorHP = useCombatStore((s) => s.resetFloorHP);
+ const elements = useManaStore((s) => s.elements);
+ const unlockElement = useManaStore((s) => s.unlockElement);
+
+ const handleReset = () => {
+ if (confirmReset) {
+ resetGame();
+ setConfirmReset(false);
+ } else {
+ setConfirmReset(true);
+ setTimeout(() => setConfirmReset(false), 3000);
+ }
+ };
+
+ const handleAddMana = (amount: number) => {
+ for (let i = 0; i < amount; i++) {
+ gatherMana();
+ }
+ };
+
+ const getMaxMana = () => {
+ return computeMaxMana(
+ { skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
+ );
+ };
+
+ return (
+
+ {/* Display Options */}
+
+
+
+
+ Display Options
+
+
+
+
+
+
+
+ Display component names at the top of each component for debugging
+
+
+
+
+
+
+
+
+ {/* Game Reset */}
+
+
+
+
+ Game Reset
+
+
+
+
+ Reset all game progress and start fresh. This cannot be undone.
+
+
+
+
+
+ {/* Mana Debug */}
+
+
+
+
+ Mana Debug
+
+
+
+
+ Current: {rawMana} / {getMaxMana() || '?'}
+
+
+
+
+
+
+
+
+ Fill to max:
+
+
+
+
+ {/* Time Control */}
+
+
+
+
+ Time Control
+
+
+
+
+ Current: Day {day}, Hour {hour}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Quick Actions */}
+
+
+
+
+ Quick Actions
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+GameStateDebugSection.displayName = "GameStateDebugSection";
diff --git a/src/components/game/tabs/DebugTab/GolemDebugSection.tsx b/src/components/game/tabs/DebugTab/GolemDebugSection.tsx
new file mode 100644
index 0000000..0d5fe27
--- /dev/null
+++ b/src/components/game/tabs/DebugTab/GolemDebugSection.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Bug, Wand2 } from 'lucide-react';
+import { useCombatStore } from '@/lib/game/stores';
+import { GOLEMS_DEF } from '@/lib/game/data/golems';
+
+export function GolemDebugSection() {
+ const golemancy = useCombatStore((s) => s.golemancy);
+ const setEnabledGolems = useCombatStore((s) => s.setEnabledGolems);
+
+ const enabledGolems = golemancy?.enabledGolems || [];
+
+ const handleEnableAll = () => {
+ setEnabledGolems(Object.keys(GOLEMS_DEF));
+ };
+
+ const handleDisableAll = () => {
+ setEnabledGolems([]);
+ };
+
+ const handleToggleGolem = (golemId: string) => {
+ if (enabledGolems.includes(golemId)) {
+ setEnabledGolems(enabledGolems.filter(id => id !== golemId));
+ } else {
+ setEnabledGolems([...enabledGolems, golemId]);
+ }
+ };
+
+ return (
+
+
+
+
+ Golem Debug
+
+
+
+
+
+
+
+
+ Enabled: {enabledGolems.length} / {Object.keys(GOLEMS_DEF).length}
+
+
+ {Object.entries(GOLEMS_DEF).map(([id, def]) => {
+ const isEnabled = enabledGolems.includes(id);
+ return (
+
+
+
{def.name}
+
{def.element}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+GolemDebugSection.displayName = "GolemDebugSection";
diff --git a/src/components/game/tabs/DebugTab/PactDebugSection.tsx b/src/components/game/tabs/DebugTab/PactDebugSection.tsx
new file mode 100644
index 0000000..a4c4da2
--- /dev/null
+++ b/src/components/game/tabs/DebugTab/PactDebugSection.tsx
@@ -0,0 +1,161 @@
+'use client';
+
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Bug } from 'lucide-react';
+import { usePrestigeStore, useManaStore, useUIStore, useGameStore } from '@/lib/game/stores';
+import { GUARDIANS, ELEMENTS } from '@/lib/game/constants';
+
+export function PactDebugSection() {
+ const signedPacts = usePrestigeStore((s) => s.signedPacts);
+ const signedPactDetails = usePrestigeStore((s) => s.signedPactDetails);
+ const elements = useManaStore((s) => s.elements);
+ const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
+
+ const addSignedPact = usePrestigeStore((s) => s.addSignedPact);
+ const removePact = usePrestigeStore((s) => s.removePact);
+ const debugSetSignedPacts = usePrestigeStore((s) => s.debugSetSignedPacts);
+ const debugSetPactDetails = usePrestigeStore((s) => s.debugSetPactDetails);
+ const unlockElement = useManaStore((s) => s.unlockElement);
+
+ const addLog = useUIStore((s) => s.addLog);
+
+ const guardianFloors = Object.keys(GUARDIANS || {}).map(Number).sort((a, b) => a - b);
+
+ const forcePact = (floor: number) => {
+ const guardian = GUARDIANS[floor];
+ if (!guardian) return;
+
+ if (signedPacts.includes(floor)) {
+ addLog(`⚠️ Already signed pact with ${guardian.name}!`);
+ return;
+ }
+
+ const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0);
+ if (signedPacts.length >= maxPacts) {
+ addLog(`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`);
+ return;
+ }
+
+ addSignedPact(floor);
+
+ const newSignedPactDetails = {
+ ...signedPactDetails,
+ [floor]: {
+ floor,
+ guardianId: guardian.element,
+ signedAt: { day: useGameStore.getState().day, hour: useGameStore.getState().hour },
+ skillLevels: {} as Record,
+ },
+ };
+ debugSetPactDetails(newSignedPactDetails);
+
+ addLog(`📜 DEBUG: Pact with ${guardian.name} force-signed!`);
+ };
+
+ const removePactHandler = (floor: number) => {
+ const guardian = GUARDIANS[floor];
+
+ removePact(floor);
+
+ const newSignedPactDetails = { ...signedPactDetails };
+ delete newSignedPactDetails[floor];
+ debugSetPactDetails(newSignedPactDetails);
+
+ addLog(`📜 DEBUG: Removed pact with ${guardian?.name || 'Unknown'}!`);
+ };
+
+ const signAllPacts = () => {
+ guardianFloors.forEach((floor) => {
+ if (!signedPacts.includes(floor)) {
+ forcePact(floor);
+ }
+ });
+ };
+
+ const clearAllPacts = () => {
+ addLog(`📜 DEBUG: Cleared all pacts!`);
+ debugSetSignedPacts([]);
+ debugSetPactDetails({});
+ };
+
+ return (
+
+
+
+
+ Pact Debug
+
+
+
+
+
+
+
+
+
+
+ {guardianFloors.map((floor) => {
+ const guardian = GUARDIANS[floor];
+ const isSigned = signedPacts.includes(floor);
+
+ return (
+
+
+
+ {guardian.name}
+
+
+ Floor {floor} | {guardian.pact}x multiplier
+
+
+ Element: {ELEMENTS[guardian.element]?.name || guardian.element}
+
+
+
+ {isSigned ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+
+
+ Signed Pacts: {signedPacts.length} |
+ Max Pacts: {1 + (prestigeUpgrades?.pactCapacity || 0)}
+
+
+
+
+ );
+}
+
+PactDebugSection.displayName = "PactDebugSection";
diff --git a/src/components/game/tabs/DebugTab/SpireDebugSection.tsx b/src/components/game/tabs/DebugTab/SpireDebugSection.tsx
new file mode 100644
index 0000000..722829e
--- /dev/null
+++ b/src/components/game/tabs/DebugTab/SpireDebugSection.tsx
@@ -0,0 +1,106 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Castle, ArrowUp, Eye } from 'lucide-react';
+import { useCombatStore } from '@/lib/game/stores';
+
+export function SpireDebugSection() {
+ const [floorInput, setFloorInput] = useState('50');
+
+ const currentFloor = useCombatStore((s) => s.currentFloor);
+ const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
+ const spireMode = useCombatStore((s) => s.spireMode);
+ const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
+ const resetFloorHP = useCombatStore((s) => s.resetFloorHP);
+ const enterSpireMode = useCombatStore((s) => s.enterSpireMode);
+ const exitSpireMode = useCombatStore((s) => s.exitSpireMode);
+ const setMaxFloorReached = useCombatStore((s) => s.setMaxFloorReached);
+
+ const handleJumpToFloor = () => {
+ const floor = parseInt(floorInput, 10);
+ if (isNaN(floor) || floor < 1 || floor > 100) return;
+ debugSetFloor(floor);
+ setMaxFloorReached(floor);
+ };
+
+ const handleClearFloor = () => {
+ resetFloorHP();
+ };
+
+ const handleToggleSpireMode = () => {
+ if (spireMode) {
+ exitSpireMode();
+ } else {
+ enterSpireMode();
+ }
+ };
+
+ return (
+
+
+
+
+ Spire Debug
+
+
+
+
+ Current Floor: {currentFloor} | Max Reached: {maxFloorReached} | Spire Mode: {spireMode ? 'ON' : 'OFF'}
+
+
+
+
+
+ setFloorInput(e.target.value)}
+ className="h-8"
+ />
+
+
+
+
+
+
+
+
+
+
+ {[10, 25, 50, 75, 100].map((f) => (
+
+ ))}
+
+
+
+ );
+}
+
+SpireDebugSection.displayName = "SpireDebugSection";
diff --git a/src/components/game/tabs/index.ts b/src/components/game/tabs/index.ts
index 36a5d4d..f3cb11c 100644
--- a/src/components/game/tabs/index.ts
+++ b/src/components/game/tabs/index.ts
@@ -4,3 +4,4 @@
export { DisciplinesTab } from './DisciplinesTab';
export { SpellsTab } from './SpellsTab';
export { StatsTab } from './StatsTab';
+export { AchievementsTab } from './AchievementsTab';
diff --git a/src/lib/game/__tests__/achievements.test.ts b/src/lib/game/__tests__/achievements.test.ts
new file mode 100644
index 0000000..9a6bcb8
--- /dev/null
+++ b/src/lib/game/__tests__/achievements.test.ts
@@ -0,0 +1,129 @@
+import { describe, it, expect } from 'vitest';
+import {
+ ACHIEVEMENTS,
+ ACHIEVEMENT_CATEGORY_COLORS,
+ getAchievementsByCategory,
+ isAchievementRevealed,
+} from '../data/achievements';
+
+describe('Achievement Definitions', () => {
+ it('should have 24 achievements defined', () => {
+ expect(Object.keys(ACHIEVEMENTS).length).toBe(24);
+ });
+
+ it('should have achievements across 5 categories', () => {
+ const byCategory = getAchievementsByCategory();
+ const categories = Object.keys(byCategory).sort();
+ expect(categories).toEqual(['combat', 'crafting', 'magic', 'progression', 'special']);
+ });
+
+ it('should have valid structure for every achievement', () => {
+ Object.values(ACHIEVEMENTS).forEach((a) => {
+ expect(a.id).toBeTruthy();
+ expect(a.name).toBeTruthy();
+ expect(a.desc).toBeTruthy();
+ expect(a.category).toBeTruthy();
+ expect(a.requirement.type).toBeTruthy();
+ expect(a.requirement.value).toBeGreaterThan(0);
+ // At least one reward key must be present
+ const rewardKeys = Object.keys(a.reward);
+ expect(rewardKeys.length).toBeGreaterThan(0);
+ });
+ });
+
+ it('should have category colors for all used categories', () => {
+ const byCategory = getAchievementsByCategory();
+ Object.keys(byCategory).forEach((cat) => {
+ expect(ACHIEVEMENT_CATEGORY_COLORS[cat]).toBeDefined();
+ });
+ });
+
+ it('should have hidden flag only on special category achievements', () => {
+ Object.values(ACHIEVEMENTS).forEach((a) => {
+ if (a.hidden) {
+ expect(a.category).toBe('special');
+ }
+ });
+ });
+
+ it('should have exactly 2 hidden achievements', () => {
+ const hidden = Object.values(ACHIEVEMENTS).filter((a) => a.hidden);
+ expect(hidden.length).toBe(2); // speedRunner and perfectionist
+ });
+});
+
+describe('getAchievementsByCategory', () => {
+ it('should group achievements correctly', () => {
+ const byCategory = getAchievementsByCategory();
+ // Combat has 6 floor + 3 damage = 9
+ expect(byCategory['combat'].length).toBe(9);
+ // Progression has 3 pacts
+ expect(byCategory['progression'].length).toBe(3);
+ // Magic has 3 spells + 3 mana = 6
+ expect(byCategory['magic'].length).toBe(6);
+ // Crafting has 3
+ expect(byCategory['crafting'].length).toBe(3);
+ // Special has 3 (2 hidden + 1 survivor)
+ expect(byCategory['special'].length).toBe(3);
+ });
+
+ it('should return a new object each call (no mutation)', () => {
+ const result1 = getAchievementsByCategory();
+ const result2 = getAchievementsByCategory();
+ expect(result1).not.toBe(result2);
+ expect(result1).toEqual(result2);
+ });
+});
+
+describe('isAchievementRevealed', () => {
+ it('should always reveal non-hidden achievements', () => {
+ const nonHidden = Object.values(ACHIEVEMENTS).find((a) => !a.hidden);
+ expect(nonHidden).toBeDefined();
+ expect(isAchievementRevealed(nonHidden!, 0)).toBe(true);
+ expect(isAchievementRevealed(nonHidden!, 999)).toBe(true);
+ });
+
+ it('should hide hidden achievements below 50% progress', () => {
+ const hidden = Object.values(ACHIEVEMENTS).find((a) => a.hidden);
+ expect(hidden).toBeDefined();
+ // At 0% progress
+ expect(isAchievementRevealed(hidden!, 0)).toBe(false);
+ // At 25% progress
+ expect(isAchievementRevealed(hidden!, hidden!.requirement.value * 0.25)).toBe(false);
+ // At exactly 49% progress
+ expect(isAchievementRevealed(hidden!, hidden!.requirement.value * 0.49)).toBe(false);
+ });
+
+ it('should reveal hidden achievements at 50% progress', () => {
+ const hidden = Object.values(ACHIEVEMENTS).find((a) => a.hidden);
+ expect(hidden).toBeDefined();
+ // At exactly 50%
+ expect(isAchievementRevealed(hidden!, hidden!.requirement.value * 0.5)).toBe(true);
+ // At 75%
+ expect(isAchievementRevealed(hidden!, hidden!.requirement.value * 0.75)).toBe(true);
+ // At 100%
+ expect(isAchievementRevealed(hidden!, hidden!.requirement.value)).toBe(true);
+ });
+});
+
+describe('Achievement Rewards', () => {
+ it('should have insight rewards on all achievements', () => {
+ Object.values(ACHIEVEMENTS).forEach((a) => {
+ expect(a.reward.insight).toBeDefined();
+ expect(a.reward.insight).toBeGreaterThan(0);
+ });
+ });
+
+ it('should scale rewards with difficulty', () => {
+ const firstBlood = ACHIEVEMENTS['firstBlood'];
+ const apexReached = ACHIEVEMENTS['apexReached'];
+ expect(apexReached.reward.insight!).toBeGreaterThan(firstBlood.reward.insight!);
+ });
+
+ it('should have title rewards on top-tier achievements', () => {
+ const topTiers = ['apexReached', 'spireMaster', 'tenThousandDamage', 'spellStorm', 'manaOcean', 'pactMaster', 'legendaryEnchanter'];
+ topTiers.forEach((id) => {
+ expect(ACHIEVEMENTS[id].reward.title).toBeDefined();
+ });
+ });
+});