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 */}

MANA LOOP

@@ -242,127 +207,26 @@ export default function ManaLoopGame() {
- {/* 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)} +
+ +
+ ); +} + +// ─── 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 +
+
+ + +
+ ); +} + +// ─── 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}
+
+ +
+
+ ); +} + +// ─── 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)} -
- -
+ ) : ( - -
- {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 -
-
- - -
- ); - }) - )} -
-
+ )}
- {/* 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}
-
- -
-
- ); - })} -
- )} -
-
-
+
); } -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 ( + + +
+ + Debug Mode +
+

+ 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 + + + +
+
+ +

+ 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. +

+ +
+
+ ); +} + +// ─── 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 || '?'} +
+
+ + + + +
+ +
Fill to max:
+ +
+
+ ); +} + +// ─── 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} +
+
+ + + + +
+ +
+ +
+
+
+ ); +} + +// ─── Quick Actions Section ─────────────────────────────────────────────────── + +function QuickActionsSection({ elements, onUnlockBase, onUnlockUtility, onSkipToFloor, onResetFloorHP }: { + elements: Record; + onUnlockBase: () => void; + onUnlockUtility: () => void; + onSkipToFloor: () => void; + onResetFloorHP: () => void; +}) { + return ( + + + + + Quick Actions + + + +
+ + + + +
+
+
+ ); +} + +// ─── 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 */} - - -
- - Debug Mode -
-

- These tools are for development and testing. Using them may break game balance or save data. -

-
-
- - {/* 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} -
-
- - - - -
- -
- -
-
-
- - {/* Skills Debug - Quick Actions */} - - - - - Quick Actions - - - -
- - - - -
-
-
+ + + + 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 ? ( + + ) : ( + + )} +
+
+ ); +} + +// ─── 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 ? ( - - ) : ( - - )} -
-
- ); - })} -
- {/* Clear All Button */} + + {signedPacts.length > 0 && (
-
)} - {/* 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 + + + +
+
+ +

+ 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. +

+ +
+
+ ); +} + +// ─── 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 || '?'} +
+
+ + + + +
+ +
Fill to max:
+ +
+
+ ); +} + +// ─── 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} +
+
+ + + + +
+ +
+ +
+
+
+ ); +} + +// ─── Quick Actions Section ─────────────────────────────────────────────────── + +function QuickActionsSection({ elements, onUnlockBase, onSkipToFloor, onResetFloorHP }: { + elements: Record; + onUnlockBase: () => void; + onSkipToFloor: () => void; + onResetFloorHP: () => void; +}) { + return ( + + + + + Quick Actions + + + +
+ + + +
+
+
+ ); +} + +// ─── 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 - - - -
-
- -

- 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 - - - -
- - - -
-
-
+ + + + 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 ? ( + + ) : ( + + )} +
+
+ ); +} + +// ─── 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 ? ( - - ) : ( - - )} -
-
- ); - })} + {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 && ( - - )} - - - ); -}); - -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 */} -
- - {tiers.map((tier) => ( - - ))} -
- - {/* 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 ( +
+
{value}
+
{label}
+
+ ); +} + +// ─── 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 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 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 */}

🏔️ SPIRE

@@ -196,9 +187,7 @@ export function SpireCombatPage() {
- {/* 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 })}
))} -
-
-
- Cleared + +
+ ); +} + +function FloorLegend() { + return ( +
+
+
+ Cleared +
+
+
+ Uncleared +
+
+
+ Guardian +
+
+
+ Current +
+
+ ); +} + +// ─── 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 ( +
+
{value}
+
{label}
+
+ ); +} + +// ─── 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
-
-
- Current +
+
+ ); +} + +// ─── 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 ────────────────────────────────────── */} + ); +} + +// ─── 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 ( +
+ + {tiers.map((tier) => ( + + ))} +
+ ); +}