diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt
index 7cc0189..6265bf2 100644
--- a/docs/circular-deps.txt
+++ b/docs/circular-deps.txt
@@ -1,8 +1,10 @@
# Circular Dependencies
-Generated: 2026-05-20T13:20:47.227Z
-Found: 1 circular chain(s) — these MUST be fixed before modifying involved files.
+Generated: 2026-05-20T15:46:48.123Z
+Found: 3 circular chain(s) — these MUST be fixed before modifying involved files.
-1. Processed 125 files (1.4s) (4 warnings)
+1. Processed 125 files (1.5s) (3 warnings)
+2. 1) stores/gameStore.ts > stores/gameActions.ts
+3. 2) stores/gameStore.ts > stores/gameLoopActions.ts
## How to fix
1. Identify which import in the chain can be extracted to a shared types/utils file.
diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json
index a1bb1dc..8712e9a 100644
--- a/docs/dependency-graph.json
+++ b/docs/dependency-graph.json
@@ -1,6 +1,6 @@
{
"_meta": {
- "generated": "2026-05-20T13:20:45.668Z",
+ "generated": "2026-05-20T15:46:46.373Z",
"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."
},
@@ -127,7 +127,7 @@
"data/attunements.ts",
"data/enchantment-effects.ts",
"effects/special-effects.ts",
- "effects/upgrade-effects.ts",
+ "effects/upgrade-effects.types.ts",
"types.ts"
],
"crafting-attunements.ts": [
@@ -139,7 +139,7 @@
"data/enchantment-effects.ts",
"data/equipment/index.ts",
"effects/special-effects.ts",
- "effects/upgrade-effects.ts",
+ "effects/upgrade-effects.types.ts",
"types.ts"
],
"crafting-equipment.ts": [
@@ -158,7 +158,6 @@
],
"crafting-utils.ts": [
"data/crafting-recipes.ts",
- "data/enchantment-effects.ts",
"data/equipment/index.ts",
"types.ts"
],
@@ -371,7 +370,7 @@
"effects/discipline-effects.ts": [
"data/disciplines/index.ts",
"stores/discipline-slice.ts",
- "types.ts",
+ "types/disciplines.ts",
"utils/discipline-math.ts"
],
"effects/dynamic-compute.ts": [
@@ -431,7 +430,8 @@
"stores/craftingStore.types.ts",
"stores/manaStore.ts",
"stores/uiStore.ts",
- "types.ts"
+ "types.ts",
+ "types/equipmentSlot.ts"
],
"stores/craftingStore.types.ts": [
"types.ts"
@@ -448,6 +448,7 @@
"effects/discipline-effects.ts",
"stores/combatStore.ts",
"stores/discipline-slice.ts",
+ "stores/gameStore.ts",
"stores/manaStore.ts",
"stores/prestigeStore.ts",
"stores/uiStore.ts",
@@ -471,6 +472,7 @@
"effects/discipline-effects.ts",
"stores/combatStore.ts",
"stores/discipline-slice.ts",
+ "stores/gameStore.ts",
"stores/manaStore.ts",
"stores/prestigeStore.ts",
"stores/uiStore.ts",
diff --git a/docs/project-structure.txt b/docs/project-structure.txt
index cec187f..0113bad 100644
--- a/docs/project-structure.txt
+++ b/docs/project-structure.txt
@@ -52,6 +52,7 @@ Mana-Loop/
│ ├── app/
│ │ ├── components/
│ │ │ ├── GameOverScreen.tsx
+│ │ │ ├── GrimoireTab.tsx
│ │ │ └── LeftPanel.tsx
│ │ ├── globals.css
│ │ ├── layout.tsx
@@ -140,6 +141,7 @@ Mana-Loop/
│ │ │ │ ├── SpireSummaryTab.test.ts
│ │ │ │ ├── SpireSummaryTab.tsx
│ │ │ │ ├── StatsTab.tsx
+│ │ │ │ ├── guardian-pacts-components.tsx
│ │ │ │ └── index.ts
│ │ │ ├── ActionButtons.tsx
│ │ │ ├── ActivityLogPanel.tsx
diff --git a/src/app/components/GrimoireTab.tsx b/src/app/components/GrimoireTab.tsx
new file mode 100644
index 0000000..d313970
--- /dev/null
+++ b/src/app/components/GrimoireTab.tsx
@@ -0,0 +1,77 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { Badge } from '@/components/ui/badge';
+import { DebugName } from '@/components/game/debug/debug-context';
+import { SPELLS_DEF } from '@/lib/game/constants';
+import type { SpellDef } from '@/lib/game/types';
+
+export function GrimoireTab() {
+ const [grimoireSpells, setGrimoireSpells] = useState<[string, SpellDef][]>([]);
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ if (typeof window !== 'undefined' && SPELLS_DEF) {
+ setGrimoireSpells(
+ Object.entries(SPELLS_DEF).filter((entry): entry is [string, SpellDef] => !!entry[1].grimoire)
+ );
+ }
+ setLoaded(true);
+ }, []);
+
+ if (!loaded) {
+ return
Loading grimoire...
;
+ }
+
+ if (grimoireSpells.length === 0) {
+ return (
+
+ No grimoire spells available yet. Defeat guardians to unlock spells.
+
+ );
+ }
+
+ const availablePages = Math.ceil(grimoireSpells.length / 12);
+
+ return (
+
+
+
+
A vast tome of arcane knowledge. Study carefully — each spell costs insight to transcribe into your repertoire.
+
Available pages: {availablePages}. Spells in grimoire: {grimoireSpells.length}.
+
+
+
+
+ {grimoireSpells.map(([id, spell]) => (
+
+
+ {spell.name}
+
+ {spell.elem}
+
+
+ {spell.desc &&
{spell.desc}
}
+
+
Cost: {spell.cost.amount} {
+ spell.cost.type === 'element'
+ ? spell.cost.element
+ : 'raw mana'
+ }
+
Power: {spell.dmg}
+ {spell.effects && spell.effects.length > 0 && (
+
Effects: {spell.effects.map(e => e.type).join(', ')}
+ )}
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 5dcd6b6..a266a34 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -3,7 +3,6 @@
import { useEffect, useState, lazy, Suspense } from 'react';
import { useShallow } from 'zustand/react/shallow';
-// Import from new modular stores
import {
useGameStore,
useUIStore,
@@ -20,148 +19,68 @@ import {
} from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
import { useGameLoop } from '@/lib/game/stores/gameHooks';
-import { getUnifiedEffects, type UnifiedEffects } from '@/lib/game/effects';
-import { SPELLS_DEF } from '@/lib/game/constants';
-import { TimeDisplay } from '@/components/game';
+import { getUnifiedEffects } from '@/lib/game/effects';
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
-import type { SpellDef } from '@/lib/game/types';
-
+import { TimeDisplay } from '@/components/game';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-
-import { Badge } from '@/components/ui/badge';
-import { ScrollArea } from '@/components/ui/scroll-area';
-
import { TooltipProvider } from '@/components/ui/tooltip';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { DebugName } from '@/components/game/debug/debug-context';
-// Import extracted components
import { GameOverScreen } from './components/GameOverScreen';
import { LeftPanel } from './components/LeftPanel';
+import { GrimoireTab } from './components/GrimoireTab';
// Lazy load tab components
-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 DebugTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DebugTab })));
-const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab })));
-const AttunementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AttunementsTab })));
-const PrestigeTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.PrestigeTab })));
-const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.EquipmentTab })));
-const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
-const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GuardianPactsTab })));
-const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireSummaryTab })));
-const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab })));
-const SpireCombatPage = lazy(() => import('@/components/game/tabs/SpireCombatPage').then(module => ({ default: module.SpireCombatPage })));
+const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.DisciplinesTab })));
+const SpellsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.SpellsTab })));
+const StatsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.StatsTab })));
+const DebugTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.DebugTab })));
+const AchievementsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.AchievementsTab })));
+const AttunementsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.AttunementsTab })));
+const PrestigeTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.PrestigeTab })));
+const EquipmentTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.EquipmentTab })));
+const GolemancyTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.GolemancyTab })));
+const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.GuardianPactsTab })));
+const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.SpireSummaryTab })));
+const CraftingTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.CraftingTab })));
+const SpireCombatPage = lazy(() => import('@/components/game/tabs/SpireCombatPage').then(m => ({ default: m.SpireCombatPage })));
-const TabLoadingFallback = () => Loading...
;
+const TabFallback = () => Loading...
;
-// ============================================================================
-// Grimoire Tab Component
-// ============================================================================
-
-function GrimoireTab() {
- const [grimoireSpells, setGrimoireSpells] = useState<[string, SpellDef][]>(() => {
- if (typeof window !== 'undefined' && SPELLS_DEF) {
- return Object.entries(SPELLS_DEF).filter((entry): entry is [string, SpellDef] => !!entry[1].grimoire);
- }
- return [];
- });
- const loaded = typeof window !== 'undefined';
-
- if (!loaded) {
- return Loading grimoire...
;
- }
-
- if (grimoireSpells.length === 0) {
- return (
-
- No grimoire spells available yet. Defeat guardians to unlock spells.
-
- );
- }
-
- const availablePages = Math.ceil(grimoireSpells.length / 12);
-
- return (
-
-
-
-
A vast tome of arcane knowledge. Study carefully — each spell costs insight to transcribe into your repertoire.
-
Available pages: {availablePages}. Spells in grimoire: {grimoireSpells.length}.
-
-
-
-
- {grimoireSpells.map(([id, spell]) => (
-
-
- {spell.name}
-
- {spell.elem}
-
-
- {spell.desc &&
{spell.desc}
}
-
-
Cost: {spell.cost.amount} {
- spell.cost.type === 'element'
- ? spell.cost.element
- : 'raw mana'
- }
-
Power: {spell.dmg}
- {spell.effects && spell.effects.length > 0 &&
Effects: {spell.effects.map(e => e.type).join(', ')}
}
-
-
- ))}
-
-
-
-
- );
+function TabErrorFallback({ name }: { name: string }) {
+ return {name} tab failed to load.
;
}
-// ============================================================================
-// Main Game Component
-// ============================================================================
+// ─── Derived Stats Hook ──────────────────────────────────────────────────────
-export default function ManaLoopGame() {
- const [selectedManaType, setSelectedManaType] = useState('');
- const [activeTab, setActiveTab] = useState('spells');
-
- // ALL hooks must be called before any conditional returns
- useGameLoop();
-
- // Use useShallow to combine multi-field subscriptions and reduce re-renders
- const { day, hour, initGame } = useGameStore(useShallow(s => ({ day: s.day, hour: s.hour, initGame: s.initGame })));
- const { prestigeUpgrades, insight, loopInsight } = usePrestigeStore(useShallow(s => ({ prestigeUpgrades: s.prestigeUpgrades, insight: s.insight, loopInsight: s.loopInsight })));
- const { rawMana, meditateTicks } = useManaStore(useShallow(s => ({ rawMana: s.rawMana, meditateTicks: s.meditateTicks })));
- const spireMode = useCombatStore((s) => s.spireMode);
- const gameOver = useUIStore((s) => s.gameOver);
-
- // Get equipment state from crafting store
+function useGameDerivedStats() {
+ const { prestigeUpgrades } = usePrestigeStore(useShallow(s => ({
+ prestigeUpgrades: s.prestigeUpgrades,
+ })));
+ const { meditateTicks } = useManaStore(useShallow(s => ({
+ meditateTicks: s.meditateTicks,
+ })));
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
+ const day = useGameStore((s) => s.day);
+ const hour = useGameStore((s) => s.hour);
- // Derived state
const upgradeEffects = getUnifiedEffects({
skillUpgrades: {},
skillTiers: {},
equippedInstances,
- equipmentInstances
+ equipmentInstances,
});
- // Compute discipline bonuses from active disciplines
const disciplineEffects = computeDisciplineEffects();
const maxMana = computeMaxMana({
skills: {},
prestigeUpgrades,
skillUpgrades: {},
- skillTiers: {}
+ skillTiers: {},
}, upgradeEffects, disciplineEffects);
const baseRegen = computeRegen({
@@ -172,30 +91,80 @@ export default function ManaLoopGame() {
attunements: {},
}, upgradeEffects, disciplineEffects);
- const clickMana = computeClickMana({
- skills: {},
- }, disciplineEffects);
-
+ const clickMana = computeClickMana({ skills: {} }, disciplineEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const incursionStrength = getIncursionStrength(day, hour);
- // Effective regen with incursion penalty
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
- // Mana Cascade bonus
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
? Math.floor(maxMana / 100) * 0.1
: 0;
- // Mana Waterfall bonus
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
? Math.floor(maxMana / 100) * 0.25
: 0;
- // Effective regen
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
- // Initialize game on mount
+ return { maxMana, effectiveRegen, clickMana, meditationMultiplier };
+}
+
+// ─── Tab Triggers ────────────────────────────────────────────────────────────
+
+function TabTriggers() {
+ return (
+
+ 🔮 Spells
+ 📊 Stats
+ 📚 Disciplines
+ 📖 Grimoire
+ 🐛 Debug
+ ⚗️ Attunements
+ 🏆 Achievements
+ ✨ Prestige
+ ⚔️ Equipment
+ 🗿 Golemancy
+ 📜 Pacts
+ 🏔️ Spire
+ ⚒️ Crafting
+
+ );
+}
+
+// ─── Lazy Tab Content ────────────────────────────────────────────────────────
+
+function LazyTab({ name, children }: { name: string; children: React.ReactNode }) {
+ return (
+ }>
+ }>
+ {children}
+
+
+ );
+}
+
+// ─── Main Game Component ─────────────────────────────────────────────────────
+
+export default function ManaLoopGame() {
+ const [activeTab, setActiveTab] = useState('spells');
+
+ useGameLoop();
+
+ const { day, hour, initGame } = useGameStore(useShallow(s => ({
+ day: s.day,
+ hour: s.hour,
+ initGame: s.initGame,
+ })));
+ const { insight, loopInsight } = usePrestigeStore(useShallow(s => ({
+ insight: s.insight,
+ loopInsight: s.loopInsight,
+ })));
+ const spireMode = useCombatStore((s) => s.spireMode);
+ const gameOver = useUIStore((s) => s.gameOver);
+
+ useGameDerivedStats();
+
useEffect(() => {
initGame();
}, [initGame]);
@@ -203,21 +172,18 @@ export default function ManaLoopGame() {
const [mounted, setMounted] = useState(false);
useEffect(() => { setMounted(true); }, []); // eslint-disable-line react-hooks/set-state-in-effect
- // React to spireMode changes from combat store
useEffect(() => {
if (spireMode) {
setActiveTab('spells'); // eslint-disable-line react-hooks/set-state-in-effect
}
}, [spireMode]);
- // Conditional returns AFTER all hooks
if (gameOver) {
return ;
}
if (!mounted) return Loading...
;
- // Spire mode: full-page replacement view
if (spireMode) {
return (
@@ -232,7 +198,6 @@ export default function ManaLoopGame() {
- {/* Header */}
- {/* Main Content */}
-
- 🔮 Spells
- 📊 Stats
- 📚 Disciplines
- 📖 Grimoire
- 🐛 Debug
- ⚗️ Attunements
- 🏆 Achievements
- ✨ Prestige
- ⚔️ Equipment
- 🗿 Golemancy
- 📜 Pacts
- 🏔️ Spire
- ⚒️ Crafting
-
+
-
- spells tab failed to load.
}>
- }>
-
-
-
-
-
-
- stats tab failed to load. }>
- }>
-
-
-
-
-
-
- disciplines tab failed to load.}>
- }>
-
-
-
-
-
-
-
-
-
-
- debug tab failed to load.}>
- }>
-
-
-
-
-
-
- attunements tab failed to load.}>
- }>
-
-
-
-
-
-
- achievements tab failed to load.}>
- }>
-
-
-
-
-
-
- prestige tab failed to load.}>
- }>
-
-
-
-
-
-
- equipment tab failed to load.}>
- }>
-
-
-
-
-
-
- golemancy tab failed to load.}>
- }>
-
-
-
-
-
-
- pacts tab failed to load.}>
- }>
-
-
-
-
-
-
- spire tab failed to load.}>
- }>
-
-
-
-
-
-
- crafting tab failed to load.}>
- }>
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/game/crafting/EquipmentCrafter.tsx b/src/components/game/crafting/EquipmentCrafter.tsx
index 511fdf9..0d7723f 100644
--- a/src/components/game/crafting/EquipmentCrafter.tsx
+++ b/src/components/game/crafting/EquipmentCrafter.tsx
@@ -9,22 +9,220 @@ import { Separator } from '@/components/ui/separator';
import { Package, Sparkles, Trash2, Anvil } from 'lucide-react';
import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes';
import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops';
-import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
+import type { LootInventory } from '@/lib/game/types';
import { fmt } from '@/lib/game/stores';
import { useCraftingStore, useCombatStore, useManaStore } from '@/lib/game/stores';
+// ─── Crafting Progress ───────────────────────────────────────────────────────
+
+function CraftingProgress({ progress }: { progress: { blueprintId: string; progress: number; required: number; manaSpent: number } }) {
+ const recipe = CRAFTING_RECIPES[progress.blueprintId];
+ const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
+
+ return (
+
+
+ Crafting: {recipe?.name}
+
+
+
+ {progress.progress.toFixed(1)}h / {progress.required.toFixed(1)}h
+ Mana spent: {fmt(progress.manaSpent)}
+
+
Cancel
+
+ );
+}
+
+// ─── Blueprint Card ───────────────────────────────────────────────────────────
+
+function BlueprintCard({ bpId, lootInventory, rawMana, isCrafting }: {
+ bpId: string;
+ lootInventory: LootInventory;
+ rawMana: number;
+ isCrafting: boolean;
+}) {
+ const recipe = CRAFTING_RECIPES[bpId];
+ if (!recipe) return null;
+
+ const { canCraft, missingMaterials } = canCraftRecipe(recipe, lootInventory.materials, rawMana);
+ const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity];
+ const startCraftingEquipment = useCraftingStore((s) => s.startCraftingEquipment);
+ const currentAction = useCombatStore((s) => s.currentAction);
+
+ return (
+
+
+
+
+ {recipe.name}
+
+
{recipe.rarity}
+
+
+ {recipe.equipmentTypeId ? 'Equipment' : 'Other'}
+
+
+
+
{recipe.description}
+
+
+
+
+
Materials:
+ {Object.entries(recipe.materials).map(([matId, amount]) => {
+ const available = lootInventory.materials[matId] || 0;
+ const matDrop = LOOT_DROPS[matId];
+ const hasEnough = available >= amount;
+
+ return (
+
+ {matDrop?.name || matId}
+
+ {available} / {amount}
+
+
+ );
+ })}
+
+
+ Mana Cost:
+ = recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
+ {fmt(recipe.manaCost)}
+
+
+
+
+ Craft Time:
+ {recipe.craftTime}h
+
+
+
+
startCraftingEquipment(bpId)}
+ >
+ {canCraft ? 'Craft Equipment' : 'Missing Resources'}
+
+
+ );
+}
+
+// ─── Blueprint List ───────────────────────────────────────────────────────────
+
+function BlueprintList({ lootInventory, rawMana }: { lootInventory: LootInventory; rawMana: number }) {
+ const currentAction = useCombatStore((s) => s.currentAction);
+
+ if (lootInventory.blueprints.length === 0) {
+ return (
+
+
+
No blueprints discovered yet.
+
Defeat guardians to find blueprints!
+
+ );
+ }
+
+ return (
+
+
+ {lootInventory.blueprints.map(bpId => (
+
+ ))}
+
+
+ );
+}
+
+// ─── Material Card ────────────────────────────────────────────────────────────
+
+function MaterialCard({ matId, count }: { matId: string; count: number }) {
+ const drop = LOOT_DROPS[matId];
+ if (!drop) return null;
+
+ const rarityStyle = LOOT_RARITY_COLORS[drop.rarity];
+ const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
+
+ return (
+
+
+
+
+ {drop.name}
+
+
x{count}
+
+
deleteMaterial(matId, count)}
+ >
+
+
+
+
+ );
+}
+
+// ─── Materials Inventory ─────────────────────────────────────────────────────
+
+function MaterialsInventory({ materials }: { materials: Record }) {
+ const totalCount = Object.values(materials).reduce((a, b) => a + b, 0);
+
+ return (
+
+
+
+
+ Materials ({totalCount})
+
+
+
+
+ {Object.keys(materials).length === 0 ? (
+
+
+
No materials collected yet.
+
Defeat floors to gather materials!
+
+ ) : (
+
+ {Object.entries(materials).map(([matId, count]) => {
+ if (count <= 0) return null;
+ return ;
+ })}
+
+ )}
+
+
+
+ );
+}
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
export function EquipmentCrafter() {
const lootInventory = useCraftingStore((s) => s.lootInventory);
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
const rawMana = useManaStore((s) => s.rawMana);
- const currentAction = useCombatStore((s) => s.currentAction);
- const startCraftingEquipment = useCraftingStore((s) => s.startCraftingEquipment);
- const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
- const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
return (
- {/* Blueprint Selection */}
@@ -34,166 +232,16 @@ export function EquipmentCrafter() {
{equipmentCraftingProgress ? (
-
-
- Crafting: {CRAFTING_RECIPES[equipmentCraftingProgress.blueprintId]?.name}
-
-
-
- {equipmentCraftingProgress.progress.toFixed(1)}h / {equipmentCraftingProgress.required.toFixed(1)}h
- Mana spent: {fmt(equipmentCraftingProgress.manaSpent)}
-
-
Cancel
-
+
) : (
-
-
- {lootInventory.blueprints.length === 0 ? (
-
-
-
No blueprints discovered yet.
-
Defeat guardians to find blueprints!
-
- ) : (
- lootInventory.blueprints.map(bpId => {
- const recipe = CRAFTING_RECIPES[bpId];
- if (!recipe) return null;
-
- const { canCraft, missingMaterials, missingMana } = canCraftRecipe(
- recipe,
- lootInventory.materials,
- rawMana
- );
-
- const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity];
-
- return (
-
-
-
-
- {recipe.name}
-
-
{recipe.rarity}
-
-
- {recipe.equipmentTypeId ? 'Equipment' : 'Other'}
-
-
-
-
{recipe.description}
-
-
-
-
-
Materials:
- {Object.entries(recipe.materials).map(([matId, amount]) => {
- const available = lootInventory.materials[matId] || 0;
- const matDrop = LOOT_DROPS[matId];
- const hasEnough = available >= amount;
-
- return (
-
- {matDrop?.name || matId}
-
- {available} / {amount}
-
-
- );
- })}
-
-
- Mana Cost:
- = recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
- {fmt(recipe.manaCost)}
-
-
-
-
- Craft Time:
- {recipe.craftTime}h
-
-
-
-
startCraftingEquipment(bpId)}
- >
- {canCraft ? 'Craft Equipment' : 'Missing Resources'}
-
-
- );
- })
- )}
-
-
+
)}
- {/* Materials Inventory */}
-
-
-
-
- Materials ({Object.values(lootInventory.materials).reduce((a, b) => a + b, 0)})
-
-
-
-
- {Object.keys(lootInventory.materials).length === 0 ? (
-
-
-
No materials collected yet.
-
Defeat floors to gather materials!
-
- ) : (
-
- {Object.entries(lootInventory.materials).map(([matId, count]) => {
- if (count <= 0) return null;
- const drop = LOOT_DROPS[matId];
- if (!drop) return null;
-
- const rarityStyle = LOOT_RARITY_COLORS[drop.rarity];
-
- return (
-
-
-
-
- {drop.name}
-
-
x{count}
-
-
deleteMaterial(matId, count)}
- >
-
-
-
-
- );
- })}
-
- )}
-
-
-
+
);
}
-EquipmentCrafter.displayName = "EquipmentCrafter";
+EquipmentCrafter.displayName = 'EquipmentCrafter';
diff --git a/src/components/game/debug/GameStateDebug.tsx b/src/components/game/debug/GameStateDebug.tsx
index bb957cc..7ffd74d 100644
--- a/src/components/game/debug/GameStateDebug.tsx
+++ b/src/components/game/debug/GameStateDebug.tsx
@@ -6,18 +6,227 @@ 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 {
+import {
RotateCcw, AlertTriangle, Zap, Clock, Settings, 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';
+// ─── Warning Banner ──────────────────────────────────────────────────────────
+
+function WarningBanner() {
+ return (
+
+
+
+
+ These tools are for development and testing. Using them may break game balance or save data.
+
+
+
+ );
+}
+
+// ─── Display Options ─────────────────────────────────────────────────────────
+
+function DisplayOptions() {
+ const { showComponentNames, toggleComponentNames } = useDebug();
+
+ return (
+
+
+
+
+ Display Options
+
+
+
+
+
+
Show Component Names
+
+ Display component names at the top of each component for debugging
+
+
+
+
+
+
+ );
+}
+
+// ─── Game Reset Section ──────────────────────────────────────────────────────
+
+function GameResetSection({ confirmReset, onReset }: { confirmReset: boolean; onReset: () => void }) {
+ return (
+
+
+
+
+ Game Reset
+
+
+
+
+ Reset all game progress and start fresh. This cannot be undone.
+
+
+ {confirmReset ? (
+ <>
+
+ Click Again to Confirm Reset
+ >
+ ) : (
+ <>
+
+ Reset Game
+ >
+ )}
+
+
+
+ );
+}
+
+// ─── Mana Debug Section ──────────────────────────────────────────────────────
+
+function ManaDebugSection({ rawMana, onAddMana, onFillMana }: {
+ rawMana: number;
+ onAddMana: (amount: number) => void;
+ onFillMana: () => void;
+}) {
+ const maxMana = computeMaxMana(
+ { skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
+ );
+
+ return (
+
+
+
+
+ Mana Debug
+
+
+
+
+ Current: {rawMana} / {maxMana || '?'}
+
+
+ onAddMana(10)}>
+ +10
+
+ onAddMana(100)}>
+ +100
+
+ onAddMana(1000)}>
+ +1K
+
+ onAddMana(10000)}>
+ +10K
+
+
+
+ Fill to max:
+
+ Fill Mana
+
+
+
+ );
+}
+
+// ─── Time Control Section ────────────────────────────────────────────────────
+
+function TimeControlSection({ day, hour, paused, onSetDay, onTogglePause }: {
+ day: number;
+ hour: number;
+ paused: boolean;
+ onSetDay: (day: number) => void;
+ onTogglePause: () => void;
+}) {
+ return (
+
+
+
+
+ Time Control
+
+
+
+
+ Current: Day {day}, Hour {hour}
+
+
+ onSetDay(1)}>Day 1
+ onSetDay(10)}>Day 10
+ onSetDay(20)}>Day 20
+ onSetDay(30)}>Day 30
+
+
+
+
+ {paused ? '▶ Resume' : '⏸ Pause'}
+
+
+
+
+ );
+}
+
+// ─── Quick Actions Section ───────────────────────────────────────────────────
+
+function QuickActionsSection({ elements, onUnlockBase, onUnlockUtility, onSkipToFloor, onResetFloorHP }: {
+ elements: Record;
+ onUnlockBase: () => void;
+ onUnlockUtility: () => void;
+ onSkipToFloor: () => void;
+ onResetFloorHP: () => void;
+}) {
+ return (
+
+
+
+
+ Quick Actions
+
+
+
+
+
+ Unlock All Base Elements
+
+
+ Unlock Utility Elements
+
+
+ Skip to Floor 100
+
+
+ Reset Floor HP
+
+
+
+
+ );
+}
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
export function GameStateDebug() {
const [confirmReset, setConfirmReset] = useState(false);
const { showComponentNames, toggleComponentNames } = useDebug();
-
- // Get state from modular stores
+
const rawMana = useManaStore((s) => s.rawMana);
const elements = useManaStore((s) => s.elements);
const unlockElement = useManaStore((s) => s.unlockElement);
@@ -26,8 +235,6 @@ export function GameStateDebug() {
const hour = useGameStore((s) => s.hour);
const paused = useUIStore((s) => s.paused);
const togglePause = useUIStore((s) => s.togglePause);
-
- // Get actions from stores
const resetGame = useGameStore((s) => s.resetGame);
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
const resetFloorHP = useCombatStore((s) => s.resetFloorHP);
@@ -48,222 +255,52 @@ export function GameStateDebug() {
}
};
- const getMaxMana = () => {
- return computeMaxMana(
+ const handleFillMana = () => {
+ const maxMana = computeMaxMana(
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
- );
+ ) || 100;
+ useManaStore.setState((s) => ({ rawMana: Math.max(s.rawMana, maxMana) }));
+ };
+
+ const handleSetDay = (d: number) => {
+ useGameStore.setState({ day: d, hour: 0 });
+ };
+
+ const handleUnlockBase = () => {
+ ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => {
+ if (!elements[e]?.unlocked) {
+ unlockElement(e, 500);
+ }
+ });
+ };
+
+ const handleUnlockUtility = () => {
+ ['transference'].forEach(e => {
+ if (!elements[e]?.unlocked) {
+ unlockElement(e, 500);
+ }
+ });
};
return (
- {/* Warning Banner */}
-
-
-
-
- These tools are for development and testing. Using them may break game balance or save data.
-
-
-
-
- {/* Display Options */}
-
-
-
-
- Display Options
-
-
-
-
-
-
Show Component Names
-
- 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.
-
-
- {confirmReset ? (
- <>
-
- Click Again to Confirm Reset
- >
- ) : (
- <>
-
- Reset Game
- >
- )}
-
-
-
-
- {/* Mana Debug */}
-
-
-
-
- Mana Debug
-
-
-
-
- Current: {rawMana} / {getMaxMana() || '?'}
-
-
- handleAddMana(10)}>
- +10
-
- handleAddMana(100)}>
- +100
-
- handleAddMana(1000)}>
- +1K
-
- handleAddMana(10000)}>
- +10K
-
-
-
- Fill to max:
- {
- const max = getMaxMana() || 100;
- useManaStore.setState((s) => ({ rawMana: Math.max(s.rawMana, max) }));
- }}
- >
- Fill Mana
-
-
-
-
- {/* Time Control */}
-
-
-
-
- Time Control
-
-
-
-
- Current: Day {day}, Hour {hour}
-
-
- useGameStore.setState({ day: 1, hour: 0 })}>
- Day 1
-
- useGameStore.setState({ day: 10, hour: 0 })}>
- Day 10
-
- useGameStore.setState({ day: 20, hour: 0 })}>
- Day 20
-
- useGameStore.setState({ day: 30, hour: 0 })}>
- Day 30
-
-
-
-
-
- {paused ? '▶ Resume' : '⏸ Pause'}
-
-
-
-
-
- {/* Skills Debug - Quick Actions */}
-
-
-
-
- Quick Actions
-
-
-
-
- {
- // Unlock all base elements
- ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => {
- if (!elements[e]?.unlocked) {
- unlockElement(e, 500);
- }
- });
- }}
- >
- Unlock All Base Elements
-
- {
- // Unlock utility elements
- ['transference'].forEach(e => {
- if (!elements[e]?.unlocked) {
- unlockElement(e, 500);
- }
- });
- }}
- >
- Unlock Utility Elements
-
- debugSetFloor?.(100)}
- >
- Skip to Floor 100
-
- resetFloorHP?.()}
- >
- Reset Floor HP
-
-
-
-
+
+
+
+
debugSetFloor?.(100)}
+ onResetFloorHP={() => resetFloorHP?.()}
+ />
);
}
-GameStateDebug.displayName = "GameStateDebug";
+GameStateDebug.displayName = 'GameStateDebug';
diff --git a/src/components/game/debug/PactDebug.tsx b/src/components/game/debug/PactDebug.tsx
index e279f96..667a24b 100644
--- a/src/components/game/debug/PactDebug.tsx
+++ b/src/components/game/debug/PactDebug.tsx
@@ -6,48 +6,106 @@ import { Bug } from 'lucide-react';
import { usePrestigeStore, useManaStore, useUIStore, useGameStore } from '@/lib/game/stores';
import { GUARDIANS, ELEMENTS } from '@/lib/game/constants';
+// ─── Guardian Pact Row ───────────────────────────────────────────────────────
+
+function GuardianPactRow({ floor, isSigned, onForceSign, onRemove }: {
+ floor: number;
+ isSigned: boolean;
+ onForceSign: () => void;
+ onRemove: () => void;
+}) {
+ const guardian = GUARDIANS[floor];
+
+ return (
+
+
+
+ {guardian.name}
+
+
+ Floor {floor} | {guardian.pact}x multiplier
+
+
+ Element: {ELEMENTS[guardian.element]?.name || guardian.element}
+
+
+
+ {isSigned ? (
+
+ Remove
+
+ ) : (
+
+ Force Sign
+
+ )}
+
+
+ );
+}
+
+// ─── Guardian Pact List ──────────────────────────────────────────────────────
+
+function GuardianPactList({ signedPacts, onForceSign, onRemove }: {
+ signedPacts: number[];
+ onForceSign: (floor: number) => void;
+ onRemove: (floor: number) => void;
+}) {
+ const guardianFloors = Object.keys(GUARDIANS || {}).map(Number).sort((a, b) => a - b);
+
+ return (
+
+ {guardianFloors.map((floor) => (
+ onForceSign(floor)}
+ onRemove={() => onRemove(floor)}
+ />
+ ))}
+
+ );
+}
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
export function PactDebug() {
- // Get state from modular stores
const signedPacts = usePrestigeStore((s) => s.signedPacts);
const signedPactDetails = usePrestigeStore((s) => s.signedPactDetails);
const elements = useManaStore((s) => s.elements);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
-
- // Get actions
+
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);
-
- // Get log function from uiStore
- const addLog = useUIStore((s) => s.addLog);
-
- // Get all guardian floors
- const guardianFloors = Object.keys(GUARDIANS || {}).map(Number).sort((a, b) => a - b);
- // Force sign a pact with a guardian (bypass costs and time)
+ const addLog = useUIStore((s) => s.addLog);
+
const forcePact = (floor: number) => {
const guardian = GUARDIANS[floor];
if (!guardian) return;
- // Check if already signed
if (signedPacts.includes(floor)) {
addLog(`⚠️ Already signed pact with ${guardian.name}!`);
return;
}
- // Check max pacts
const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0);
if (signedPacts.length >= maxPacts) {
addLog(`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`);
return;
}
- // Force sign the pact
addSignedPact(floor);
-
- // Add pact details
+
const newSignedPactDetails = {
...signedPactDetails,
[floor]: {
@@ -62,13 +120,11 @@ export function PactDebug() {
addLog(`📜 DEBUG: Pact with ${guardian.name} force-signed!`);
};
- // Remove a pact
const removePactHandler = (floor: number) => {
const guardian = GUARDIANS[floor];
-
+
removePact(floor);
-
- // Remove pact details
+
const newSignedPactDetails = { ...signedPactDetails };
delete newSignedPactDetails[floor];
debugSetPactDetails(newSignedPactDetails);
@@ -76,7 +132,6 @@ export function PactDebug() {
addLog(`📜 DEBUG: Removed pact with ${guardian?.name || 'Unknown'}!`);
};
- // Clear all pacts
const clearAllPacts = () => {
addLog(`📜 DEBUG: Cleared all pacts!`);
debugSetSignedPacts([]);
@@ -96,74 +151,23 @@ export function PactDebug() {
Force sign pacts with guardians (bypasses mana costs and signing time)
-
-
- {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 ? (
- removePactHandler(floor)}
- className="text-xs"
- >
- Remove
-
- ) : (
- forcePact(floor)}
- className="text-xs bg-amber-600 hover:bg-amber-700"
- >
- Force Sign
-
- )}
-
-
- );
- })}
-
- {/* Clear All Button */}
+
+
{signedPacts.length > 0 && (
-
+
Clear All Pacts ({signedPacts.length})
)}
- {/* Status */}
- Signed Pacts: {signedPacts.length} |
+ Signed Pacts: {signedPacts.length} |
Max Pacts: {1 + (prestigeUpgrades?.pactCapacity || 0)}
@@ -172,4 +176,4 @@ export function PactDebug() {
);
}
-PactDebug.displayName = "PactDebug";
+PactDebug.displayName = 'PactDebug';
diff --git a/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx b/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx
index ff7b77b..eb44d42 100644
--- a/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx
+++ b/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx
@@ -13,6 +13,194 @@ import { useDebug } from '@/components/game/debug/debug-context';
import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores';
import { computeMaxMana } from '@/lib/game/stores';
+// ─── Display Options ─────────────────────────────────────────────────────────
+
+function DisplayOptions() {
+ const { showComponentNames, toggleComponentNames } = useDebug();
+
+ return (
+
+
+
+
+ Display Options
+
+
+
+
+
+
Show Component Names
+
+ Display component names at the top of each component for debugging
+
+
+
+
+
+
+ );
+}
+
+// ─── Game Reset Section ──────────────────────────────────────────────────────
+
+function GameResetSection({ confirmReset, onReset }: { confirmReset: boolean; onReset: () => void }) {
+ return (
+
+
+
+
+ Game Reset
+
+
+
+
+ Reset all game progress and start fresh. This cannot be undone.
+
+
+ {confirmReset ? (
+ <>
+
+ Click Again to Confirm Reset
+ >
+ ) : (
+ <>
+
+ Reset Game
+ >
+ )}
+
+
+
+ );
+}
+
+// ─── Mana Debug Section ──────────────────────────────────────────────────────
+
+function ManaDebugSection({ rawMana, onAddMana, onFillMana }: {
+ rawMana: number;
+ onAddMana: (amount: number) => void;
+ onFillMana: () => void;
+}) {
+ const maxMana = computeMaxMana(
+ { skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
+ );
+
+ return (
+
+
+
+
+ Mana Debug
+
+
+
+
+ Current: {rawMana} / {maxMana || '?'}
+
+
+ onAddMana(10)}>
+ +10
+
+ onAddMana(100)}>
+ +100
+
+ onAddMana(1000)}>
+ +1K
+
+ onAddMana(10000)}>
+ +10K
+
+
+
+ Fill to max:
+
+ Fill Mana
+
+
+
+ );
+}
+
+// ─── Time Control Section ────────────────────────────────────────────────────
+
+function TimeControlSection({ day, hour, paused, onSetDay, onTogglePause }: {
+ day: number;
+ hour: number;
+ paused: boolean;
+ onSetDay: (day: number) => void;
+ onTogglePause: () => void;
+}) {
+ return (
+
+
+
+
+ Time Control
+
+
+
+
+ Current: Day {day}, Hour {hour}
+
+
+ onSetDay(1)}>Day 1
+ onSetDay(10)}>Day 10
+ onSetDay(20)}>Day 20
+ onSetDay(30)}>Day 30
+
+
+
+
+ {paused ? '▶ Resume' : '⏸ Pause'}
+
+
+
+
+ );
+}
+
+// ─── Quick Actions Section ───────────────────────────────────────────────────
+
+function QuickActionsSection({ elements, onUnlockBase, onSkipToFloor, onResetFloorHP }: {
+ elements: Record;
+ onUnlockBase: () => void;
+ onSkipToFloor: () => void;
+ onResetFloorHP: () => void;
+}) {
+ return (
+
+
+
+
+ Quick Actions
+
+
+
+
+
+ Unlock All Base Elements
+
+
+ Skip to Floor 100
+
+
+ Reset Floor HP
+
+
+
+
+ );
+}
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
export function GameStateDebugSection() {
const [confirmReset, setConfirmReset] = useState(false);
const { showComponentNames, toggleComponentNames } = useDebug();
@@ -22,7 +210,6 @@ export function GameStateDebugSection() {
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);
@@ -46,190 +233,42 @@ export function GameStateDebugSection() {
}
};
- const getMaxMana = () => {
- return computeMaxMana(
+ const handleFillMana = () => {
+ const maxMana = computeMaxMana(
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
- );
+ ) || 100;
+ useManaStore.setState((s) => ({ rawMana: Math.max(s.rawMana, maxMana) }));
+ };
+
+ const handleSetDay = (d: number) => {
+ useGameStore.setState({ day: d, hour: 0 });
+ };
+
+ const handleUnlockBase = () => {
+ ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => {
+ if (!elements[e]?.unlocked) {
+ unlockElement(e, 0);
+ }
+ });
};
return (
- {/* Display Options */}
-
-
-
-
- Display Options
-
-
-
-
-
-
Show Component Names
-
- 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.
-
-
- {confirmReset ? (
- <>
-
- Click Again to Confirm Reset
- >
- ) : (
- <>
-
- Reset Game
- >
- )}
-
-
-
-
- {/* Mana Debug */}
-
-
-
-
- Mana Debug
-
-
-
-
- Current: {rawMana} / {getMaxMana() || '?'}
-
-
- handleAddMana(10)}>
- +10
-
- handleAddMana(100)}>
- +100
-
- handleAddMana(1000)}>
- +1K
-
- handleAddMana(10000)}>
- +10K
-
-
-
- Fill to max:
- {
- const max = getMaxMana() || 100;
- useManaStore.setState((s) => ({ rawMana: Math.max(s.rawMana, max) }));
- }}
- >
- Fill Mana
-
-
-
-
- {/* Time Control */}
-
-
-
-
- Time Control
-
-
-
-
- Current: Day {day}, Hour {hour}
-
-
- useGameStore.setState({ day: 1, hour: 0 })}>
- Day 1
-
- useGameStore.setState({ day: 10, hour: 0 })}>
- Day 10
-
- useGameStore.setState({ day: 20, hour: 0 })}>
- Day 20
-
- useGameStore.setState({ day: 30, hour: 0 })}>
- Day 30
-
-
-
-
-
- {paused ? '▶ Resume' : '⏸ Pause'}
-
-
-
-
-
- {/* Quick Actions */}
-
-
-
-
- Quick Actions
-
-
-
-
- {
- ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => {
- if (!elements[e]?.unlocked) {
- unlockElement(e, 0);
- }
- });
- }}
- >
- Unlock All Base Elements
-
- debugSetFloor?.(100)}
- >
- Skip to Floor 100
-
- resetFloorHP?.()}
- >
- Reset Floor HP
-
-
-
-
+
+
+
+
debugSetFloor?.(100)}
+ onResetFloorHP={() => resetFloorHP?.()}
+ />
);
}
-GameStateDebugSection.displayName = "GameStateDebugSection";
+GameStateDebugSection.displayName = 'GameStateDebugSection';
diff --git a/src/components/game/tabs/DebugTab/PactDebugSection.tsx b/src/components/game/tabs/DebugTab/PactDebugSection.tsx
index a4c4da2..fb6e937 100644
--- a/src/components/game/tabs/DebugTab/PactDebugSection.tsx
+++ b/src/components/game/tabs/DebugTab/PactDebugSection.tsx
@@ -6,6 +6,51 @@ import { Bug } from 'lucide-react';
import { usePrestigeStore, useManaStore, useUIStore, useGameStore } from '@/lib/game/stores';
import { GUARDIANS, ELEMENTS } from '@/lib/game/constants';
+// ─── Guardian Pact Row ───────────────────────────────────────────────────────
+
+function GuardianPactRow({ floor, isSigned, onForceSign, onRemove }: {
+ floor: number;
+ isSigned: boolean;
+ onForceSign: () => void;
+ onRemove: () => void;
+}) {
+ const guardian = GUARDIANS[floor];
+
+ return (
+
+
+
+ {guardian.name}
+
+
+ Floor {floor} | {guardian.pact}x multiplier
+
+
+ Element: {ELEMENTS[guardian.element]?.name || guardian.element}
+
+
+
+ {isSigned ? (
+
+ Remove
+
+ ) : (
+
+ Force Sign
+
+ )}
+
+
+ );
+}
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
export function PactDebugSection() {
const signedPacts = usePrestigeStore((s) => s.signedPacts);
const signedPactDetails = usePrestigeStore((s) => s.signedPactDetails);
@@ -99,53 +144,15 @@ export function PactDebugSection() {
- {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 ? (
- removePactHandler(floor)}
- className="text-xs"
- >
- Remove
-
- ) : (
- forcePact(floor)}
- className="text-xs bg-amber-600 hover:bg-amber-700"
- >
- Force Sign
-
- )}
-
-
- );
- })}
+ {guardianFloors.map((floor) => (
+
forcePact(floor)}
+ onRemove={() => removePactHandler(floor)}
+ />
+ ))}
@@ -158,4 +165,4 @@ export function PactDebugSection() {
);
}
-PactDebugSection.displayName = "PactDebugSection";
+PactDebugSection.displayName = 'PactDebugSection';
diff --git a/src/components/game/tabs/GuardianPactsTab.tsx b/src/components/game/tabs/GuardianPactsTab.tsx
index b0b7e11..99f9170 100644
--- a/src/components/game/tabs/GuardianPactsTab.tsx
+++ b/src/components/game/tabs/GuardianPactsTab.tsx
@@ -5,14 +5,14 @@ import { useShallow } from 'zustand/react/shallow';
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
import { useManaStore } from '@/lib/game/stores/manaStore';
import { useUIStore } from '@/lib/game/stores/uiStore';
-import { GUARDIANS, ELEMENTS } from '@/lib/game/constants';
-import type { GuardianDef, GuardianBoon } from '@/lib/game/types';
+import { GUARDIANS } from '@/lib/game/constants';
import { DebugName } from '@/components/game/debug/debug-context';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
-import { Shield, Swords, Clock, Sparkles, Check, Lock, ChevronRight } from 'lucide-react';
-import clsx from 'clsx';
+import {
+ GuardianCard,
+ PactHeaderSummary,
+ TierFilter,
+} from './guardian-pacts-components';
// ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -24,172 +24,6 @@ function getGuardianStatus(floor: number, defeated: number[], signed: number[]):
return 'undefeated';
}
-function formatHours(hours: number): string {
- return `${hours}h`;
-}
-
-// ─── Guardian Card ───────────────────────────────────────────────────────────
-
-interface GuardianCardProps {
- floor: number;
- guardian: GuardianDef;
- status: GuardianStatus;
- canAfford: boolean;
- hasSlot: boolean;
- isRitualActive: boolean;
- ritualProgress: number;
- onStartRitual: (floor: number) => void;
-}
-
-const GuardianCard: React.FC
= React.memo(({
- floor,
- guardian,
- status,
- canAfford,
- hasSlot,
- isRitualActive,
- ritualProgress,
- onStartRitual,
-}) => {
- const elemDef = ELEMENTS[guardian.element];
- const elemColor = elemDef?.color ?? '#888';
- const elemSym = elemDef?.sym ?? '';
-
- const statusConfig: Record = {
- undefeated: { label: 'Undefeated', color: 'text-gray-400', bg: 'bg-gray-800/50' },
- defeated: { label: 'Pact Available', color: 'text-amber-400', bg: 'bg-amber-900/20' },
- signed: { label: 'Pact Signed', color: 'text-green-400', bg: 'bg-green-900/20' },
- };
-
- const sc = statusConfig[status];
- const ritualTime = guardian.pactTime;
- const ritualComplete = ritualProgress >= ritualTime;
-
- return (
-
-
-
-
-
- {elemSym}
- {guardian.name}
-
-
Floor {floor} · {elemDef?.name ?? guardian.element}
-
-
- {sc.label}
-
-
-
-
-
- {/* Stats */}
-
-
-
- HP: {guardian.hp.toLocaleString()}
-
-
-
- PWR: {guardian.power.toLocaleString()}
-
-
-
- ARM: {Math.round((guardian.armor ?? 0) * 100)}%
-
-
-
- {/* Boons */}
-
-
- Boons
-
-
- {guardian.boons.map((boon: GuardianBoon, i: number) => (
-
- {boon.desc}
-
- ))}
-
-
-
- {/* Unique Perk */}
-
- Perk: {guardian.uniquePerk}
-
-
- {/* Pact Cost */}
-
-
-
- {formatHours(guardian.pactTime)}
-
-
- Cost: {guardian.pactCost.toLocaleString()} mana
-
-
-
- {/* Ritual Progress */}
- {isRitualActive && (
-
-
- Ritual in progress…
- {ritualProgress}/{ritualTime}h
-
-
- {ritualComplete && (
-
- Ritual complete — pact will be signed on next tick
-
- )}
-
- )}
-
- {/* Action Button */}
- {status === 'defeated' && !isRitualActive && (
- onStartRitual(floor)}
- disabled={!canAfford || !hasSlot}
- className={clsx(
- 'w-full rounded px-3 py-1.5 text-xs font-medium transition-colors flex items-center justify-center gap-1',
- canAfford && hasSlot
- ? 'bg-amber-600/80 text-white hover:bg-amber-500'
- : 'bg-gray-700 text-gray-500 cursor-not-allowed',
- )}
- >
- {!canAfford ? (
- <> Not enough mana>
- ) : !hasSlot ? (
- <> No pact slots>
- ) : (
- <> Begin Pact Ritual>
- )}
-
- )}
-
-
- );
-});
-
-GuardianCard.displayName = 'GuardianCard';
-
-// ─── Floor Tier Groups ──────────────────────────────────────────────────────
-
interface FloorTier {
label: string;
floors: number[];
@@ -263,7 +97,6 @@ export const GuardianPactsTab: React.FC = () => {
}
}, [startPactRitual, rawMana, addLog]);
- // Cumulative boon summary from signed pacts
const cumulativeBoons = useMemo(() => {
const boonMap: Record = {};
for (const floor of signedPacts) {
@@ -287,95 +120,34 @@ export const GuardianPactsTab: React.FC = () => {
return (
- {/* Header Summary */}
-
-
-
-
-
- Pact Slots:
- {signedPacts.length} / {pactSlots}
-
-
-
- Signed:
- {signedPacts.length}
-
-
-
- Defeated:
- {defeatedGuardians.length}
-
-
+
- {/* Cumulative Boons */}
- {signedPacts.length > 0 && (
-
-
Active Boon Effects:
-
- {Object.entries(cumulativeBoons).map(([type, value]) => (
-
- {type}: +{value}
-
- ))}
-
-
- )}
-
-
+
- {/* Tier Filter */}
-
- setActiveTier('all')}
- className={clsx(
- 'rounded px-3 py-1 text-xs font-medium transition-colors',
- activeTier === 'all'
- ? 'bg-amber-600 text-white'
- : 'text-gray-400 hover:text-gray-200',
- )}
- >
- All ({guardianFloors.length})
-
- {tiers.map((tier) => (
- setActiveTier(tier.label)}
- className={clsx(
- 'rounded px-3 py-1 text-xs font-medium transition-colors',
- activeTier === tier.label
- ? 'bg-amber-600 text-white'
- : 'text-gray-400 hover:text-gray-200',
- )}
- >
- {tier.label} ({tier.floors.length})
-
- ))}
-
-
- {/* Guardian Cards */}
{filteredFloors.map((floor) => {
const guardian = GUARDIANS[floor];
if (!guardian) return null;
- const status = getGuardianStatus(floor, defeatedGuardians, signedPacts);
- const isRitualActive = pactRitualFloor === floor;
- const hasSlot = signedPacts.length < pactSlots;
- const canAfford = rawMana >= guardian.pactCost;
-
return (
= guardian.pactCost}
+ hasSlot={signedPacts.length < pactSlots}
+ isRitualActive={pactRitualFloor === floor}
ritualProgress={pactRitualProgress}
onStartRitual={handleStartRitual}
/>
diff --git a/src/components/game/tabs/PrestigeTab.tsx b/src/components/game/tabs/PrestigeTab.tsx
index 5387b30..9cc0b0b 100644
--- a/src/components/game/tabs/PrestigeTab.tsx
+++ b/src/components/game/tabs/PrestigeTab.tsx
@@ -23,6 +23,101 @@ import {
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
+// ─── Stat Cell ────────────────────────────────────────────────────────────────
+
+function PrestigeStatCell({ value, label, color }: { value: string | number; label: string; color: string }) {
+ return (
+
+ );
+}
+
+// ─── Insight Summary ──────────────────────────────────────────────────────────
+
+function InsightSummary({ insight, totalInsight, loopCount, loopInsight }: {
+ insight: number;
+ totalInsight: number;
+ loopCount: number;
+ loopInsight: number;
+}) {
+ return (
+
+
+
+
+
+ );
+}
+
+// ─── Memories Card ────────────────────────────────────────────────────────────
+
+function MemoriesCard({ memories, memorySlots }: { memories: { skillId: string; level: number; tier: number }[]; memorySlots: number }) {
+ return (
+
+
+
+
+ Skills carried between loops. Slots: {memories.length}/{memorySlots}
+
+ {memories.length === 0 ? (
+ No memories stored yet.
+ ) : (
+
+ {memories.map((m) => (
+
+ {m.skillId}
+ Lv.{m.level} T{m.tier}
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+// ─── Pacts Card ───────────────────────────────────────────────────────────────
+
+function PactsCard({ signedPacts, pactSlots, defeatedGuardians }: {
+ signedPacts: number[];
+ pactSlots: number;
+ defeatedGuardians: number[];
+}) {
+ return (
+
+
+
+
+ Guardian pacts signed. Slots: {signedPacts.length}/{pactSlots}
+
+ {defeatedGuardians.length > 0 && (
+
+ Defeated guardians: {defeatedGuardians.map((f) => `F${f}`).join(', ')}
+
+ )}
+ {signedPacts.length === 0 ? (
+ No pacts signed yet.
+ ) : (
+
+ {signedPacts.map((f) => (
+
+ ✓ Floor {f} — Pact signed
+
+ ))}
+
+ )}
+
+
+ );
+}
+
// ─── Upgrade Card ─────────────────────────────────────────────────────────────
interface UpgradeCardProps {
@@ -72,6 +167,49 @@ function UpgradeCard({ id, name, desc, max, cost, currentLevel, insight, onPurch
);
}
+// ─── Reset Loop Section ──────────────────────────────────────────────────────
+
+function ResetLoopSection({ loopInsight, onReset }: { loopInsight: number; onReset: () => void }) {
+ return (
+
+
+
+
+
Reset Loop
+
+ End the current loop and gain {fmt(loopInsight)} insight. Your prestige upgrades, memories, and pacts are preserved.
+
+
+
+
+
+ Reset Loop
+
+
+
+
+ Reset the Loop?
+
+ This will end your current loop and award you {fmt(loopInsight)} insight .
+ Your prestige upgrades, memories, and pacts will be preserved.
+
+ Day, hour, mana, floor progress, and combat state will be reset.
+
+
+
+ Cancel
+
+ Confirm Reset
+
+
+
+
+
+
+
+ );
+}
+
// ─── Main Component ───────────────────────────────────────────────────────────
export function PrestigeTab() {
@@ -130,80 +268,18 @@ export function PrestigeTab() {
return (
- {/* Insight & Loop Summary */}
-
-
-
-
-
{fmt(insight)}
-
Insight Available
-
-
-
{fmt(totalInsight)}
-
Total Insight Earned
-
-
-
{loopCount}
-
Loops Completed
-
-
-
{fmt(loopInsight)}
-
This Loop's Insight
-
-
-
-
+
- {/* Memories & Pacts */}
-
-
-
-
- Skills carried between loops. Slots: {memories.length}/{memorySlots}
-
- {memories.length === 0 ? (
- No memories stored yet.
- ) : (
-
- {memories.map((m) => (
-
- {m.skillId}
- Lv.{m.level} T{m.tier}
-
- ))}
-
- )}
-
-
-
-
-
-
-
- Guardian pacts signed. Slots: {signedPacts.length}/{pactSlots}
-
- {defeatedGuardians.length > 0 && (
-
- Defeated guardians: {defeatedGuardians.map((f) => `F${f}`).join(', ')}
-
- )}
- {signedPacts.length === 0 ? (
- No pacts signed yet.
- ) : (
-
- {signedPacts.map((f) => (
-
- ✓ Floor {f} — Pact signed
-
- ))}
-
- )}
-
-
+
+
- {/* Prestige Upgrades */}
@@ -227,43 +303,7 @@ export function PrestigeTab() {
- {/* Reset Loop */}
-
-
-
-
-
Reset Loop
-
- End the current loop and gain {fmt(loopInsight)} insight. Your prestige upgrades, memories, and pacts are preserved.
-
-
-
-
-
- Reset Loop
-
-
-
-
- Reset the Loop?
-
- This will end your current loop and award you {fmt(loopInsight)} insight .
- Your prestige upgrades, memories, and pacts will be preserved.
-
- Day, hour, mana, floor progress, and combat state will be reset.
-
-
-
- Cancel
-
- Confirm Reset
-
-
-
-
-
-
-
+
);
diff --git a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx
index 418f2a2..fe36088 100644
--- a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx
+++ b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx
@@ -5,82 +5,21 @@ import { useShallow } from 'zustand/react/shallow';
import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
import { getUnifiedEffects } from '@/lib/game/effects';
-import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
import { useCraftingStore } from '@/lib/game/stores/craftingStore';
import { GUARDIANS } from '@/lib/game/constants';
import { getExtendedGuardian, isGuardianFloor } from '@/lib/game/data/guardian-encounters';
-import { getRoomsForFloor, generateSpireFloorState, calcInsight } from '@/lib/game/utils/spire-utils';
+import { getRoomsForFloor, generateSpireFloorState } from '@/lib/game/utils/spire-utils';
import { SpireHeader } from './SpireHeader';
import { RoomDisplay } from './RoomDisplay';
import { SpireCombatControls } from './SpireCombatControls';
import { SpireActivityLog } from './SpireActivityLog';
import { SpireManaDisplay } from './SpireManaDisplay';
-export function SpireCombatPage() {
- const [mounted, setMounted] = useState(false);
- const [roomsCleared, setRoomsCleared] = useState(0);
+// ─── Derived Stats Hook ──────────────────────────────────────────────────────
- // Combat store
- const {
- currentFloor,
- floorHP,
- floorMaxHP,
- castProgress,
- clearedFloors,
- isDescending,
- currentRoom,
- activityLog,
- setCurrentRoom,
- setFloorHP,
- setClearedFloor,
- climbDownFloor,
- exitSpireMode,
- startClimbUp,
- startClimbDown,
- addActivityLog,
- processCombatTick,
- setAction,
- } = useCombatStore(useShallow((s) => ({
- currentFloor: s.currentFloor,
- floorHP: s.floorHP,
- floorMaxHP: s.floorMaxHP,
- castProgress: s.castProgress,
- clearedFloors: s.clearedFloors,
- isDescending: s.isDescending,
- currentRoom: s.currentRoom,
- activityLog: s.activityLog,
- setCurrentRoom: s.setCurrentRoom,
- setFloorHP: s.setFloorHP,
- setClearedFloor: s.setClearedFloor,
- climbDownFloor: s.climbDownFloor,
- exitSpireMode: s.exitSpireMode,
- startClimbUp: s.startClimbUp,
- startClimbDown: s.startClimbDown,
- addActivityLog: s.addActivityLog,
- processCombatTick: s.processCombatTick,
- setAction: s.setAction,
- })));
-
- // Mana store
- const { rawMana, elements } = useManaStore(useShallow((s) => ({
- rawMana: s.rawMana,
- elements: s.elements,
- })));
-
- // Prestige store
- const { prestigeUpgrades, insight } = usePrestigeStore(useShallow((s) => ({
- prestigeUpgrades: s.prestigeUpgrades,
- insight: s.insight,
- })));
-
- // Crafting store for equipment effects
- const equippedInstances = useCraftingStore((s) => s.equippedInstances);
- const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
-
- // Discipline effects
+function useSpireStats(prestigeUpgrades: Record, equippedInstances: unknown[], equipmentInstances: unknown[]) {
const disciplineEffects = computeDisciplineEffects();
- // Compute derived stats
const upgradeEffects = getUnifiedEffects({
skillUpgrades: {},
skillTiers: {},
@@ -103,10 +42,70 @@ export function SpireCombatPage() {
attunements: {},
}, upgradeEffects, disciplineEffects);
- // Total rooms for current floor
+ return { maxMana, baseRegen };
+}
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
+export function SpireCombatPage() {
+ const [mounted, setMounted] = useState(false);
+ const [roomsCleared, setRoomsCleared] = useState(0);
+
+ const {
+ currentFloor,
+ floorHP,
+ floorMaxHP,
+ castProgress,
+ clearedFloors,
+ isDescending,
+ currentRoom,
+ activityLog,
+ setCurrentRoom,
+ setFloorHP,
+ setClearedFloor,
+ climbDownFloor,
+ exitSpireMode,
+ startClimbUp,
+ startClimbDown,
+ addActivityLog,
+ setAction,
+ } = useCombatStore(useShallow((s) => ({
+ currentFloor: s.currentFloor,
+ floorHP: s.floorHP,
+ floorMaxHP: s.floorMaxHP,
+ castProgress: s.castProgress,
+ clearedFloors: s.clearedFloors,
+ isDescending: s.isDescending,
+ currentRoom: s.currentRoom,
+ activityLog: s.activityLog,
+ setCurrentRoom: s.setCurrentRoom,
+ setFloorHP: s.setFloorHP,
+ setClearedFloor: s.setClearedFloor,
+ climbDownFloor: s.climbDownFloor,
+ exitSpireMode: s.exitSpireMode,
+ startClimbUp: s.startClimbUp,
+ startClimbDown: s.startClimbDown,
+ addActivityLog: s.addActivityLog,
+ setAction: s.setAction,
+ })));
+
+ const { rawMana, elements } = useManaStore(useShallow((s) => ({
+ rawMana: s.rawMana,
+ elements: s.elements,
+ })));
+
+ const { prestigeUpgrades, insight } = usePrestigeStore(useShallow((s) => ({
+ prestigeUpgrades: s.prestigeUpgrades,
+ insight: s.insight,
+ })));
+
+ const equippedInstances = useCraftingStore((s) => s.equippedInstances);
+ const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
+
+ const { maxMana, baseRegen } = useSpireStats(prestigeUpgrades, equippedInstances, equipmentInstances);
+
const totalRooms = useMemo(() => getRoomsForFloor(currentFloor), [currentFloor]);
- // Initialize room on floor change
useEffect(() => {
setMounted(true);
setRoomsCleared(0);
@@ -115,12 +114,10 @@ export function SpireCombatPage() {
setAction('climb');
}, [currentFloor, totalRooms, setCurrentRoom, setAction]);
- // Handle room/floor transitions
const handleRoomCleared = () => {
const nextRoomIndex = roomsCleared + 1;
if (nextRoomIndex >= totalRooms) {
- // Floor cleared
const wasGuardian = isGuardianFloor(currentFloor);
setClearedFloor(currentFloor, true);
@@ -138,30 +135,26 @@ export function SpireCombatPage() {
floor: currentFloor,
});
- // Auto-advance to next floor
const newFloor = currentFloor + 1;
const newTotalRooms = getRoomsForFloor(newFloor);
const newRoom = generateSpireFloorState(newFloor, 0, newTotalRooms);
setCurrentRoom(newRoom);
- setFloorHP(floorMaxHP); // Reset HP for new floor
+ setFloorHP(floorMaxHP);
setClearedFloor(currentFloor, true);
setRoomsCleared(0);
} else {
- // Next room on same floor
const newRoom = generateSpireFloorState(currentFloor, nextRoomIndex, totalRooms);
setCurrentRoom(newRoom);
setRoomsCleared(nextRoomIndex);
}
};
- // Handle climb up
const handleClimbUp = () => {
startClimbUp();
addActivityLog('floor_transition', `⬆️ Climbing to floor ${currentFloor + 1}...`);
};
- // Handle climb down
const handleClimbDown = () => {
if (currentFloor <= 1) return;
startClimbDown();
@@ -170,7 +163,6 @@ export function SpireCombatPage() {
addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`);
};
- // Handle exit spire
const handleExitSpire = () => {
exitSpireMode();
addActivityLog('floor_transition', '🚪 Exited the Spire.');
@@ -186,7 +178,6 @@ export function SpireCombatPage() {
return (
- {/* Compact header */}
- {/* Main content */}
- {/* Top section: Header + Mana */}
- {/* Middle section: Room + Controls */}
@@ -228,7 +216,6 @@ export function SpireCombatPage() {
- {/* Bottom: Activity Log */}
diff --git a/src/components/game/tabs/SpireSummaryTab.tsx b/src/components/game/tabs/SpireSummaryTab.tsx
index 4ef9708..c5cb633 100644
--- a/src/components/game/tabs/SpireSummaryTab.tsx
+++ b/src/components/game/tabs/SpireSummaryTab.tsx
@@ -45,7 +45,6 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
const totalFloors = Math.min(maxFloor, 100);
const clearedSet = new Set(Object.entries(clearedFloors).filter(([, v]) => v).map(([k]) => Number(k)));
- // Group floors into rows of 10 for display
const rows: number[][] = [];
for (let i = 0; i < totalFloors; i += 10) {
rows.push(Array.from({ length: 10 }, (_, j) => i + j + 1).filter((f) => f <= totalFloors));
@@ -88,23 +87,219 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
})}
))}
-
-
+ );
+}
+
+function FloorLegend() {
+ return (
+
+ );
+}
+
+// ─── Top Stats Row ───────────────────────────────────────────────────────────
+
+function TopStatsRow({ maxFloorReached, totalFloorsCleared, defeatedCount, insight }: {
+ maxFloorReached: number;
+ totalFloorsCleared: number;
+ defeatedCount: number;
+ insight: number;
+}) {
+ return (
+
+
+
+
+
+
+
-
-
-
Uncleared
+
+
+ );
+}
+
+function StatCell({ value, label, color }: { value: number | string; label: string; color: string }) {
+ return (
+
+ );
+}
+
+// ─── Next Guardian Card ──────────────────────────────────────────────────────
+
+function NextGuardianCard({ nextGuardian, nextGuardianData }: { nextGuardian: number; nextGuardianData: (typeof GUARDIANS)[number] }) {
+ const counterElement = getCounterElement(nextGuardianData.element);
+ const nextFloorElement = FLOOR_ELEM_CYCLE[(nextGuardian - 1) % FLOOR_ELEM_CYCLE.length];
+
+ return (
+
+
+
+
+
+ {nextGuardian}
+
+
+
{nextGuardianData.name}
+
+
+ {nextGuardianData.element}
+
+ HP: {fmt(nextGuardianData.hp)}
+ {nextGuardianData.armor && (
+
+ Armor: {Math.round(nextGuardianData.armor * 100)}%
+
+ )}
+
+
-
-
-
Guardian
+
+
0.15)}
+ />
+
+
+ );
+}
+
+function PreparationTips({ counterElement, nextFloorElement, hasHighArmor }: { counterElement: string | null; nextFloorElement: string | null; hasHighArmor: boolean }) {
+ return (
+
+
Recommended Preparation:
+
+ {counterElement && (
+
+ ⚡
+
+ Use {counterElement} spells for super effective damage (+50%)
+
+
+ )}
+ {nextFloorElement && (
+
+ 🔄
+
+ Floor element: {nextFloorElement}
+
+
+ )}
+ {hasHighArmor && (
+
+ 🛡️
+ High armor — consider armor-piercing or raw damage spells
+
+ )}
+
+ 💡
+ Ensure mana pools are full before attempting
-
+
+ );
+}
+
+// ─── Guardian Roster ─────────────────────────────────────────────────────────
+
+function GuardianRoster({ clearedFloors }: { clearedFloors: Record
}) {
+ return (
+
+
+
+
+ {GUARDIAN_FLOORS.map((floor) => (
+
+ ))}
+
+
+ );
+}
+
+function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; guardian: (typeof GUARDIANS)[number]; isDefeated: boolean }) {
+ return (
+
+
+
+ {floor}
+
+
+
+ {guardian.name}
+
+
+
+ {guardian.element}
+
+ HP: {fmt(guardian.hp)}
+
+
+
+
+ {isDefeated ? (
+
+ ✓ Defeated
+
+ ) : (
+
+ Undefeated
+
+ )}
);
@@ -135,7 +330,6 @@ export function SpireSummaryTab() {
setMounted(true);
}, []);
- // Derived data
const defeatedGuardians = useMemo(() => {
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
}, [clearedFloors]);
@@ -146,9 +340,6 @@ export function SpireSummaryTab() {
const nextGuardianData = nextGuardian ? GUARDIANS[nextGuardian] : null;
- const counterElement = nextGuardianData ? getCounterElement(nextGuardianData.element) : null;
- const nextFloorElement = nextGuardian ? FLOOR_ELEM_CYCLE[(nextGuardian - 1) % FLOOR_ELEM_CYCLE.length] : null;
-
const totalFloorsCleared = useMemo(() => {
return Object.values(clearedFloors).filter(Boolean).length;
}, [clearedFloors]);
@@ -164,31 +355,13 @@ export function SpireSummaryTab() {
return (
- {/* ── Top Stats Row ─────────────────────────────────────────────── */}
-
-
-
-
-
{maxFloorReached}
-
Max Floor Reached
-
-
-
{totalFloorsCleared}
-
Floors Cleared
-
-
-
{defeatedGuardians.length}
-
Guardians Defeated
-
-
-
{fmt(insight)}
-
Insight Earned
-
-
-
-
+
- {/* ── Climb the Spire Button ────────────────────────────────────── */}
- {/* ── Next Guardian + Preparation ───────────────────────────────── */}
{nextGuardianData && nextGuardian && (
-
-
-
-
-
- {nextGuardian}
-
-
-
{nextGuardianData.name}
-
-
- {nextGuardianData.element}
-
- HP: {fmt(nextGuardianData.hp)}
- {nextGuardianData.armor && (
-
- Armor: {Math.round(nextGuardianData.armor * 100)}%
-
- )}
-
-
-
-
- {/* Preparation recommendations */}
-
-
Recommended Preparation:
-
- {counterElement && (
-
- ⚡
-
- Use {counterElement} spells for super effective damage (+50%)
-
-
- )}
- {nextFloorElement && (
-
- 🔄
-
- Floor element: {nextFloorElement}
-
-
- )}
- {nextGuardianData.armor && nextGuardianData.armor > 0.15 && (
-
- 🛡️
- High armor — consider armor-piercing or raw damage spells
-
- )}
-
- 💡
- Ensure mana pools are full before attempting
-
-
-
-
-
+
)}
- {/* ── All Guardians List ────────────────────────────────────────── */}
-
-
-
-
- {GUARDIAN_FLOORS.map((floor) => {
- const guardian = GUARDIANS[floor];
- const isDefeated = clearedFloors[floor];
+
- return (
-
-
-
- {floor}
-
-
-
- {guardian.name}
-
-
-
- {guardian.element}
-
- HP: {fmt(guardian.hp)}
-
-
-
-
- {isDefeated ? (
-
- ✓ Defeated
-
- ) : (
-
- Undefeated
-
- )}
-
-
- );
- })}
-
-
-
-
- {/* ── Floor Progress Map ────────────────────────────────────────── */}
diff --git a/src/components/game/tabs/guardian-pacts-components.tsx b/src/components/game/tabs/guardian-pacts-components.tsx
new file mode 100644
index 0000000..58864bc
--- /dev/null
+++ b/src/components/game/tabs/guardian-pacts-components.tsx
@@ -0,0 +1,306 @@
+'use client';
+
+import React from 'react';
+import { ELEMENTS } from '@/lib/game/constants';
+import type { GuardianDef, GuardianBoon } from '@/lib/game/types';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Shield, Swords, Clock, Sparkles, Check, Lock, ChevronRight } from 'lucide-react';
+import clsx from 'clsx';
+
+// ─── Types ───────────────────────────────────────────────────────────────────
+
+export type GuardianStatus = 'undefeated' | 'defeated' | 'signed';
+
+interface FloorTier {
+ label: string;
+ floors: number[];
+}
+
+// ─── Guardian Card ───────────────────────────────────────────────────────────
+
+interface GuardianCardProps {
+ floor: number;
+ guardian: GuardianDef;
+ status: GuardianStatus;
+ canAfford: boolean;
+ hasSlot: boolean;
+ isRitualActive: boolean;
+ ritualProgress: number;
+ onStartRitual: (floor: number) => void;
+}
+
+export const GuardianCard: React.FC = React.memo(({
+ floor,
+ guardian,
+ status,
+ canAfford,
+ hasSlot,
+ isRitualActive,
+ ritualProgress,
+ onStartRitual,
+}) => {
+ const elemDef = ELEMENTS[guardian.element];
+ const elemColor = elemDef?.color ?? '#888';
+ const elemSym = elemDef?.sym ?? '';
+
+ const statusConfig: Record = {
+ undefeated: { label: 'Undefeated', color: 'text-gray-400', bg: 'bg-gray-800/50' },
+ defeated: { label: 'Pact Available', color: 'text-amber-400', bg: 'bg-amber-900/20' },
+ signed: { label: 'Pact Signed', color: 'text-green-400', bg: 'bg-green-900/20' },
+ };
+
+ const sc = statusConfig[status];
+ const ritualTime = guardian.pactTime;
+ const ritualComplete = ritualProgress >= ritualTime;
+
+ return (
+
+
+
+
+
+ {elemSym}
+ {guardian.name}
+
+
Floor {floor} · {elemDef?.name ?? guardian.element}
+
+
+ {sc.label}
+
+
+
+
+
+
+
+
+
+ Perk: {guardian.uniquePerk}
+
+
+
+
+
+ {ritualTime}h
+
+
+ Cost: {guardian.pactCost.toLocaleString()} mana
+
+
+
+ {isRitualActive && (
+
+ )}
+
+ {status === 'defeated' && !isRitualActive && (
+ onStartRitual(floor)}
+ />
+ )}
+
+
+ );
+});
+
+GuardianCard.displayName = 'GuardianCard';
+
+// ─── Guardian Stats ──────────────────────────────────────────────────────────
+
+function GuardianStats({ guardian }: { guardian: GuardianDef }) {
+ return (
+
+
+
+ HP: {guardian.hp.toLocaleString()}
+
+
+
+ PWR: {guardian.power.toLocaleString()}
+
+
+
+ ARM: {Math.round((guardian.armor ?? 0) * 100)}%
+
+
+ );
+}
+
+// ─── Guardian Boons ──────────────────────────────────────────────────────────
+
+function GuardianBoons({ guardian }: { guardian: GuardianDef }) {
+ return (
+
+
+ Boons
+
+
+ {guardian.boons.map((boon: GuardianBoon, i: number) => (
+
+ {boon.desc}
+
+ ))}
+
+
+ );
+}
+
+// ─── Ritual Progress ─────────────────────────────────────────────────────────
+
+function RitualProgress({ ritualProgress, ritualTime, ritualComplete }: { ritualProgress: number; ritualTime: number; ritualComplete: boolean }) {
+ return (
+
+
+ Ritual in progress…
+ {ritualProgress}/{ritualTime}h
+
+
+ {ritualComplete && (
+
+ Ritual complete — pact will be signed on next tick
+
+ )}
+
+ );
+}
+
+// ─── Pact Action Button ──────────────────────────────────────────────────────
+
+function PactActionButton({ canAfford, hasSlot, onStartRitual }: { canAfford: boolean; hasSlot: boolean; onStartRitual: () => void }) {
+ const disabled = !canAfford || !hasSlot;
+ return (
+
+ {!canAfford ? (
+ <> Not enough mana>
+ ) : !hasSlot ? (
+ <> No pact slots>
+ ) : (
+ <> Begin Pact Ritual>
+ )}
+
+ );
+}
+
+// ─── Pact Header Summary ────────────────────────────────────────────────────
+
+export function PactHeaderSummary({
+ signedCount,
+ pactSlots,
+ defeatedCount,
+ cumulativeBoons,
+}: {
+ signedCount: number;
+ pactSlots: number;
+ defeatedCount: number;
+ cumulativeBoons: Record;
+}) {
+ return (
+
+
+
+
+
+ Pact Slots:
+ {signedCount} / {pactSlots}
+
+
+
+ Signed:
+ {signedCount}
+
+
+
+ Defeated:
+ {defeatedCount}
+
+
+
+ {signedCount > 0 && (
+
+
Active Boon Effects:
+
+ {Object.entries(cumulativeBoons).map(([type, value]) => (
+
+ {type}: +{value}
+
+ ))}
+
+
+ )}
+
+
+ );
+}
+
+// ─── Tier Filter ─────────────────────────────────────────────────────────────
+
+export function TierFilter({
+ tiers,
+ activeTier,
+ guardianFloors,
+ onSelectTier,
+}: {
+ tiers: FloorTier[];
+ activeTier: string;
+ guardianFloors: number[];
+ onSelectTier: (tier: string) => void;
+}) {
+ return (
+
+ onSelectTier('all')}
+ className={clsx(
+ 'rounded px-3 py-1 text-xs font-medium transition-colors',
+ activeTier === 'all'
+ ? 'bg-amber-600 text-white'
+ : 'text-gray-400 hover:text-gray-200',
+ )}
+ >
+ All ({guardianFloors.length})
+
+ {tiers.map((tier) => (
+ onSelectTier(tier.label)}
+ className={clsx(
+ 'rounded px-3 py-1 text-xs font-medium transition-colors',
+ activeTier === tier.label
+ ? 'bg-amber-600 text-white'
+ : 'text-gray-400 hover:text-gray-200',
+ )}
+ >
+ {tier.label} ({tier.floors.length})
+
+ ))}
+
+ );
+}