refactor: extract sub-components from monster functions (issue #99)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

- GuardianPactsTab: extracted GuardianCard, PactHeaderSummary, TierFilter + 5 helper components into guardian-pacts-components.tsx
- SpireSummaryTab: extracted TopStatsRow, NextGuardianCard, GuardianRoster, GuardianRosterItem, FloorLegend
- PrestigeTab: extracted InsightSummary, MemoriesCard, PactsCard, ResetLoopSection
- GameStateDebug: extracted WarningBanner, DisplayOptions, GameResetSection, ManaDebugSection, TimeControlSection, QuickActionsSection
- EquipmentCrafter: extracted CraftingProgress, BlueprintCard, BlueprintList, MaterialCard, MaterialsInventory
- PactDebug: extracted GuardianPactRow, GuardianPactList
- GameStateDebugSection: extracted DisplayOptions, GameResetSection, ManaDebugSection, TimeControlSection, QuickActionsSection
- PactDebugSection: extracted GuardianPactRow
- SpireCombatPage: extracted useSpireStats hook
- page.tsx: extracted GrimoireTab to separate file, useGameDerivedStats hook, TabTriggers, LazyTab wrapper

All files now under 400 lines. Build passes. All 639 tests pass.
This commit is contained in:
2026-05-20 18:38:24 +02:00
parent 53b3a94725
commit ce084a61a3
15 changed files with 1765 additions and 1539 deletions
+5 -3
View File
@@ -1,8 +1,10 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-20T13:20:47.227Z Generated: 2026-05-20T15:46:48.123Z
Found: 1 circular chain(s) — these MUST be fixed before modifying involved files. 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 ## How to fix
1. Identify which import in the chain can be extracted to a shared types/utils file. 1. Identify which import in the chain can be extracted to a shared types/utils file.
+8 -6
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_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.", "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." "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/attunements.ts",
"data/enchantment-effects.ts", "data/enchantment-effects.ts",
"effects/special-effects.ts", "effects/special-effects.ts",
"effects/upgrade-effects.ts", "effects/upgrade-effects.types.ts",
"types.ts" "types.ts"
], ],
"crafting-attunements.ts": [ "crafting-attunements.ts": [
@@ -139,7 +139,7 @@
"data/enchantment-effects.ts", "data/enchantment-effects.ts",
"data/equipment/index.ts", "data/equipment/index.ts",
"effects/special-effects.ts", "effects/special-effects.ts",
"effects/upgrade-effects.ts", "effects/upgrade-effects.types.ts",
"types.ts" "types.ts"
], ],
"crafting-equipment.ts": [ "crafting-equipment.ts": [
@@ -158,7 +158,6 @@
], ],
"crafting-utils.ts": [ "crafting-utils.ts": [
"data/crafting-recipes.ts", "data/crafting-recipes.ts",
"data/enchantment-effects.ts",
"data/equipment/index.ts", "data/equipment/index.ts",
"types.ts" "types.ts"
], ],
@@ -371,7 +370,7 @@
"effects/discipline-effects.ts": [ "effects/discipline-effects.ts": [
"data/disciplines/index.ts", "data/disciplines/index.ts",
"stores/discipline-slice.ts", "stores/discipline-slice.ts",
"types.ts", "types/disciplines.ts",
"utils/discipline-math.ts" "utils/discipline-math.ts"
], ],
"effects/dynamic-compute.ts": [ "effects/dynamic-compute.ts": [
@@ -431,7 +430,8 @@
"stores/craftingStore.types.ts", "stores/craftingStore.types.ts",
"stores/manaStore.ts", "stores/manaStore.ts",
"stores/uiStore.ts", "stores/uiStore.ts",
"types.ts" "types.ts",
"types/equipmentSlot.ts"
], ],
"stores/craftingStore.types.ts": [ "stores/craftingStore.types.ts": [
"types.ts" "types.ts"
@@ -448,6 +448,7 @@
"effects/discipline-effects.ts", "effects/discipline-effects.ts",
"stores/combatStore.ts", "stores/combatStore.ts",
"stores/discipline-slice.ts", "stores/discipline-slice.ts",
"stores/gameStore.ts",
"stores/manaStore.ts", "stores/manaStore.ts",
"stores/prestigeStore.ts", "stores/prestigeStore.ts",
"stores/uiStore.ts", "stores/uiStore.ts",
@@ -471,6 +472,7 @@
"effects/discipline-effects.ts", "effects/discipline-effects.ts",
"stores/combatStore.ts", "stores/combatStore.ts",
"stores/discipline-slice.ts", "stores/discipline-slice.ts",
"stores/gameStore.ts",
"stores/manaStore.ts", "stores/manaStore.ts",
"stores/prestigeStore.ts", "stores/prestigeStore.ts",
"stores/uiStore.ts", "stores/uiStore.ts",
+2
View File
@@ -52,6 +52,7 @@ Mana-Loop/
│ ├── app/ │ ├── app/
│ │ ├── components/ │ │ ├── components/
│ │ │ ├── GameOverScreen.tsx │ │ │ ├── GameOverScreen.tsx
│ │ │ ├── GrimoireTab.tsx
│ │ │ └── LeftPanel.tsx │ │ │ └── LeftPanel.tsx
│ │ ├── globals.css │ │ ├── globals.css
│ │ ├── layout.tsx │ │ ├── layout.tsx
@@ -140,6 +141,7 @@ Mana-Loop/
│ │ │ │ ├── SpireSummaryTab.test.ts │ │ │ │ ├── SpireSummaryTab.test.ts
│ │ │ │ ├── SpireSummaryTab.tsx │ │ │ │ ├── SpireSummaryTab.tsx
│ │ │ │ ├── StatsTab.tsx │ │ │ │ ├── StatsTab.tsx
│ │ │ │ ├── guardian-pacts-components.tsx
│ │ │ │ └── index.ts │ │ │ │ └── index.ts
│ │ │ ├── ActionButtons.tsx │ │ │ ├── ActionButtons.tsx
│ │ │ ├── ActivityLogPanel.tsx │ │ │ ├── ActivityLogPanel.tsx
+77
View File
@@ -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 <div className="p-4 text-center text-gray-400">Loading grimoire...</div>;
}
if (grimoireSpells.length === 0) {
return (
<div className="p-4 text-center text-gray-400">
No grimoire spells available yet. Defeat guardians to unlock spells.
</div>
);
}
const availablePages = Math.ceil(grimoireSpells.length / 12);
return (
<DebugName name="GrimoireTab">
<div className="space-y-4">
<div className="text-sm text-gray-400">
<p className="mb-2">A vast tome of arcane knowledge. Study carefully each spell costs insight to transcribe into your repertoire.</p>
<p>Available pages: {availablePages}. Spells in grimoire: {grimoireSpells.length}.</p>
</div>
<ScrollArea className="h-[600px] rounded border border-gray-700 p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{grimoireSpells.map(([id, spell]) => (
<div
key={id}
className="p-4 bg-gray-800/50 rounded border border-gray-600 hover:border-gray-500 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<span className="font-bold text-gray-100">{spell.name}</span>
<Badge variant="outline" className="border-gray-600">
{spell.elem}
</Badge>
</div>
{spell.desc && <p className="text-sm text-gray-400 mb-3">{spell.desc}</p>}
<div className="text-xs text-gray-500 space-y-1">
<div>Cost: {spell.cost.amount} {
spell.cost.type === 'element'
? spell.cost.element
: 'raw mana'
}</div>
<div>Power: {spell.dmg}</div>
{spell.effects && spell.effects.length > 0 && (
<div>Effects: {spell.effects.map(e => e.type).join(', ')}</div>
)}
</div>
</div>
))}
</div>
</ScrollArea>
</div>
</DebugName>
);
}
+123 -259
View File
@@ -3,7 +3,6 @@
import { useEffect, useState, lazy, Suspense } from 'react'; import { useEffect, useState, lazy, Suspense } from 'react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
// Import from new modular stores
import { import {
useGameStore, useGameStore,
useUIStore, useUIStore,
@@ -20,148 +19,68 @@ import {
} from '@/lib/game/stores'; } from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects'; import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
import { useGameLoop } from '@/lib/game/stores/gameHooks'; import { useGameLoop } from '@/lib/game/stores/gameHooks';
import { getUnifiedEffects, type UnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { SPELLS_DEF } from '@/lib/game/constants';
import { TimeDisplay } from '@/components/game';
import { hasSpecial, SPECIAL_EFFECTS } 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 { 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 { TooltipProvider } from '@/components/ui/tooltip';
import { ErrorBoundary } from '@/components/ErrorBoundary'; import { ErrorBoundary } from '@/components/ErrorBoundary';
import { DebugName } from '@/components/game/debug/debug-context'; import { DebugName } from '@/components/game/debug/debug-context';
// Import extracted components
import { GameOverScreen } from './components/GameOverScreen'; import { GameOverScreen } from './components/GameOverScreen';
import { LeftPanel } from './components/LeftPanel'; import { LeftPanel } from './components/LeftPanel';
import { GrimoireTab } from './components/GrimoireTab';
// Lazy load tab components // Lazy load tab components
const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DisciplinesTab }))); const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.DisciplinesTab })));
const SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab }))); const SpellsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.SpellsTab })));
const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab }))); const StatsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.StatsTab })));
const DebugTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DebugTab }))); const DebugTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.DebugTab })));
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab }))); const AchievementsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.AchievementsTab })));
const AttunementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AttunementsTab }))); const AttunementsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.AttunementsTab })));
const PrestigeTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.PrestigeTab }))); const PrestigeTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.PrestigeTab })));
const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.EquipmentTab }))); const EquipmentTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.EquipmentTab })));
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab }))); const GolemancyTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.GolemancyTab })));
const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GuardianPactsTab }))); const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.GuardianPactsTab })));
const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireSummaryTab }))); const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.SpireSummaryTab })));
const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab }))); const CraftingTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.CraftingTab })));
const SpireCombatPage = lazy(() => import('@/components/game/tabs/SpireCombatPage').then(module => ({ default: module.SpireCombatPage }))); const SpireCombatPage = lazy(() => import('@/components/game/tabs/SpireCombatPage').then(m => ({ default: m.SpireCombatPage })));
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>; const TabFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
// ============================================================================ function TabErrorFallback({ name }: { name: string }) {
// Grimoire Tab Component return <div className="p-4 text-red-400">{name} tab failed to load.</div>;
// ============================================================================
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 <div className="p-4 text-center text-gray-400">Loading grimoire...</div>;
} }
if (grimoireSpells.length === 0) { // ─── Derived Stats Hook ──────────────────────────────────────────────────────
return (
<div className="p-4 text-center text-gray-400">
No grimoire spells available yet. Defeat guardians to unlock spells.
</div>
);
}
const availablePages = Math.ceil(grimoireSpells.length / 12); function useGameDerivedStats() {
const { prestigeUpgrades } = usePrestigeStore(useShallow(s => ({
return ( prestigeUpgrades: s.prestigeUpgrades,
<DebugName name="GrimoireTab"> })));
<div className="space-y-4"> const { meditateTicks } = useManaStore(useShallow(s => ({
<div className="text-sm text-gray-400"> meditateTicks: s.meditateTicks,
<p className="mb-2">A vast tome of arcane knowledge. Study carefully each spell costs insight to transcribe into your repertoire.</p> })));
<p>Available pages: {availablePages}. Spells in grimoire: {grimoireSpells.length}.</p>
</div>
<ScrollArea className="h-[600px] rounded border border-gray-700 p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{grimoireSpells.map(([id, spell]) => (
<div
key={id}
className="p-4 bg-gray-800/50 rounded border border-gray-600 hover:border-gray-500 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<span className="font-bold text-gray-100">{spell.name}</span>
<Badge variant="outline" className="border-gray-600">
{spell.elem}
</Badge>
</div>
{spell.desc && <p className="text-sm text-gray-400 mb-3">{spell.desc}</p>}
<div className="text-xs text-gray-500 space-y-1">
<div>Cost: {spell.cost.amount} {
spell.cost.type === 'element'
? spell.cost.element
: 'raw mana'
}</div>
<div>Power: {spell.dmg}</div>
{spell.effects && spell.effects.length > 0 && <div>Effects: {spell.effects.map(e => e.type).join(', ')}</div>}
</div>
</div>
))}
</div>
</ScrollArea>
</div>
</DebugName>
);
}
// ============================================================================
// Main Game Component
// ============================================================================
export default function ManaLoopGame() {
const [selectedManaType, setSelectedManaType] = useState<string>('');
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
const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const day = useGameStore((s) => s.day);
const hour = useGameStore((s) => s.hour);
// Derived state
const upgradeEffects = getUnifiedEffects({ const upgradeEffects = getUnifiedEffects({
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {}, skillTiers: {},
equippedInstances, equippedInstances,
equipmentInstances equipmentInstances,
}); });
// Compute discipline bonuses from active disciplines
const disciplineEffects = computeDisciplineEffects(); const disciplineEffects = computeDisciplineEffects();
const maxMana = computeMaxMana({ const maxMana = computeMaxMana({
skills: {}, skills: {},
prestigeUpgrades, prestigeUpgrades,
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {} skillTiers: {},
}, upgradeEffects, disciplineEffects); }, upgradeEffects, disciplineEffects);
const baseRegen = computeRegen({ const baseRegen = computeRegen({
@@ -172,82 +91,29 @@ export default function ManaLoopGame() {
attunements: {}, attunements: {},
}, upgradeEffects, disciplineEffects); }, upgradeEffects, disciplineEffects);
const clickMana = computeClickMana({ const clickMana = computeClickMana({ skills: {} }, disciplineEffects);
skills: {},
}, disciplineEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency); const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const incursionStrength = getIncursionStrength(day, hour); const incursionStrength = getIncursionStrength(day, hour);
// Effective regen with incursion penalty
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
// Mana Cascade bonus
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE) const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
? Math.floor(maxMana / 100) * 0.1 ? Math.floor(maxMana / 100) * 0.1
: 0; : 0;
// Mana Waterfall bonus
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL) const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
? Math.floor(maxMana / 100) * 0.25 ? Math.floor(maxMana / 100) * 0.25
: 0; : 0;
// Effective regen
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier; const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
// Initialize game on mount return { maxMana, effectiveRegen, clickMana, meditationMultiplier };
useEffect(() => {
initGame();
}, [initGame]);
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 <GameOverScreen day={day} hour={hour} insightGained={loopInsight} totalInsight={insight} />;
} }
if (!mounted) return <div className="p-4 text-center text-gray-400">Loading...</div>; // ─── Tab Triggers ────────────────────────────────────────────────────────────
// Spire mode: full-page replacement view function TabTriggers() {
if (spireMode) {
return ( return (
<ErrorBoundary>
<Suspense fallback={<div className="p-4 text-center text-gray-400">Loading spire...</div>}>
<SpireCombatPage />
</Suspense>
</ErrorBoundary>
);
}
return (
<ErrorBoundary>
<TooltipProvider>
<div className="game-root min-h-screen flex flex-col">
{/* Header */}
<header className="sticky top-0 z-50 bg-gradient-to-b from-gray-900 to-gray-900/80 border-b border-gray-700 px-4 py-2">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
<div className="flex items-center gap-4">
<TimeDisplay day={day} hour={hour} insight={insight} />
</div>
</div>
</header>
{/* Main Content */}
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
<LeftPanel />
<div className="flex-1 min-w-0">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto"> <TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger> <TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger> <TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
@@ -263,106 +129,104 @@ export default function ManaLoopGame() {
<TabsTrigger value="spire" className="text-xs px-2 py-1">🏔 Spire</TabsTrigger> <TabsTrigger value="spire" className="text-xs px-2 py-1">🏔 Spire</TabsTrigger>
<TabsTrigger value="crafting" className="text-xs px-2 py-1"> Crafting</TabsTrigger> <TabsTrigger value="crafting" className="text-xs px-2 py-1"> Crafting</TabsTrigger>
</TabsList> </TabsList>
);
}
<TabsContent value="spells"> // ─── Lazy Tab Content ────────────────────────────────────────────────────────
<ErrorBoundary fallback={<div className="p-4 text-red-400">spells tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}> function LazyTab({ name, children }: { name: string; children: React.ReactNode }) {
<SpellsTab /> return (
<ErrorBoundary fallback={<TabErrorFallback name={name} />}>
<Suspense fallback={<TabFallback />}>
{children}
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
</TabsContent> );
}
<TabsContent value="stats"> // ─── Main Game Component ─────────────────────────────────────────────────────
<ErrorBoundary fallback={<div className="p-4 text-red-400">stats tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}> export default function ManaLoopGame() {
<StatsTab /> 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]);
const [mounted, setMounted] = useState(false);
useEffect(() => { setMounted(true); }, []); // eslint-disable-line react-hooks/set-state-in-effect
useEffect(() => {
if (spireMode) {
setActiveTab('spells'); // eslint-disable-line react-hooks/set-state-in-effect
}
}, [spireMode]);
if (gameOver) {
return <GameOverScreen day={day} hour={hour} insightGained={loopInsight} totalInsight={insight} />;
}
if (!mounted) return <div className="p-4 text-center text-gray-400">Loading...</div>;
if (spireMode) {
return (
<ErrorBoundary>
<Suspense fallback={<div className="p-4 text-center text-gray-400">Loading spire...</div>}>
<SpireCombatPage />
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
</TabsContent> );
}
<TabsContent value="disciplines"> return (
<ErrorBoundary fallback={<div className="p-4 text-red-400">disciplines tab failed to load.</div>}> <ErrorBoundary>
<Suspense fallback={<TabLoadingFallback />}> <TooltipProvider>
<DisciplinesTab /> <div className="game-root min-h-screen flex flex-col">
</Suspense> <header className="sticky top-0 z-50 bg-gradient-to-b from-gray-900 to-gray-900/80 border-b border-gray-700 px-4 py-2">
</ErrorBoundary> <div className="flex items-center justify-between">
</TabsContent> <h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
<div className="flex items-center gap-4">
<TimeDisplay day={day} hour={hour} insight={insight} />
</div>
</div>
</header>
<TabsContent value="grimoire"> <main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
<GrimoireTab /> <LeftPanel />
</TabsContent>
<TabsContent value="debug"> <div className="flex-1 min-w-0">
<ErrorBoundary fallback={<div className="p-4 text-red-400">debug tab failed to load.</div>}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<Suspense fallback={<TabLoadingFallback />}> <TabTriggers />
<DebugTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="attunements"> <TabsContent value="spells"><LazyTab name="spells"><SpellsTab /></LazyTab></TabsContent>
<ErrorBoundary fallback={<div className="p-4 text-red-400">attunements tab failed to load.</div>}> <TabsContent value="stats"><LazyTab name="stats"><StatsTab /></LazyTab></TabsContent>
<Suspense fallback={<TabLoadingFallback />}> <TabsContent value="disciplines"><LazyTab name="disciplines"><DisciplinesTab /></LazyTab></TabsContent>
<AttunementsTab /> <TabsContent value="grimoire"><GrimoireTab /></TabsContent>
</Suspense> <TabsContent value="debug"><LazyTab name="debug"><DebugTab /></LazyTab></TabsContent>
</ErrorBoundary> <TabsContent value="attunements"><LazyTab name="attunements"><AttunementsTab /></LazyTab></TabsContent>
</TabsContent> <TabsContent value="achievements"><LazyTab name="achievements"><AchievementsTab /></LazyTab></TabsContent>
<TabsContent value="prestige"><LazyTab name="prestige"><PrestigeTab /></LazyTab></TabsContent>
<TabsContent value="achievements"> <TabsContent value="equipment"><LazyTab name="equipment"><EquipmentTab /></LazyTab></TabsContent>
<ErrorBoundary fallback={<div className="p-4 text-red-400">achievements tab failed to load.</div>}> <TabsContent value="golemancy"><LazyTab name="golemancy"><GolemancyTab /></LazyTab></TabsContent>
<Suspense fallback={<TabLoadingFallback />}> <TabsContent value="pacts"><LazyTab name="pacts"><GuardianPactsTab /></LazyTab></TabsContent>
<AchievementsTab /> <TabsContent value="spire"><LazyTab name="spire"><SpireSummaryTab /></LazyTab></TabsContent>
</Suspense> <TabsContent value="crafting"><LazyTab name="crafting"><CraftingTab /></LazyTab></TabsContent>
</ErrorBoundary>
</TabsContent>
<TabsContent value="prestige">
<ErrorBoundary fallback={<div className="p-4 text-red-400">prestige tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<PrestigeTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="equipment">
<ErrorBoundary fallback={<div className="p-4 text-red-400">equipment tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<EquipmentTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="golemancy">
<ErrorBoundary fallback={<div className="p-4 text-red-400">golemancy tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<GolemancyTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="pacts">
<ErrorBoundary fallback={<div className="p-4 text-red-400">pacts tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<GuardianPactsTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="spire">
<ErrorBoundary fallback={<div className="p-4 text-red-400">spire tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<SpireSummaryTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="crafting">
<ErrorBoundary fallback={<div className="p-4 text-red-400">crafting tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<CraftingTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
</Tabs> </Tabs>
</div> </div>
</main> </main>
+117 -69
View File
@@ -9,67 +9,49 @@ import { Separator } from '@/components/ui/separator';
import { Package, Sparkles, Trash2, Anvil } from 'lucide-react'; import { Package, Sparkles, Trash2, Anvil } from 'lucide-react';
import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes'; import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes';
import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops'; 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 { fmt } from '@/lib/game/stores';
import { useCraftingStore, useCombatStore, useManaStore } from '@/lib/game/stores'; import { useCraftingStore, useCombatStore, useManaStore } from '@/lib/game/stores';
export function EquipmentCrafter() { // ─── Crafting Progress ───────────────────────────────────────────────────────
const lootInventory = useCraftingStore((s) => s.lootInventory);
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress); function CraftingProgress({ progress }: { progress: { blueprintId: string; progress: number; required: number; manaSpent: number } }) {
const rawMana = useManaStore((s) => s.rawMana); const recipe = CRAFTING_RECIPES[progress.blueprintId];
const currentAction = useCombatStore((s) => s.currentAction);
const startCraftingEquipment = useCraftingStore((s) => s.startCraftingEquipment);
const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting); const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Blueprint Selection */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
<Anvil className="w-4 h-4" />
Available Blueprints
</CardTitle>
</CardHeader>
<CardContent>
{equipmentCraftingProgress ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="text-sm text-gray-400"> <div className="text-sm text-gray-400">
Crafting: {CRAFTING_RECIPES[equipmentCraftingProgress.blueprintId]?.name} Crafting: {recipe?.name}
</div> </div>
<Progress value={(equipmentCraftingProgress.progress / equipmentCraftingProgress.required) * 100} className="h-3" /> <Progress value={(progress.progress / progress.required) * 100} className="h-3" />
<div className="flex justify-between text-xs text-gray-400"> <div className="flex justify-between text-xs text-gray-400">
<span>{equipmentCraftingProgress.progress.toFixed(1)}h / {equipmentCraftingProgress.required.toFixed(1)}h</span> <span>{progress.progress.toFixed(1)}h / {progress.required.toFixed(1)}h</span>
<span>Mana spent: {fmt(equipmentCraftingProgress.manaSpent)}</span> <span>Mana spent: {fmt(progress.manaSpent)}</span>
</div> </div>
<Button size="sm" variant="outline" onClick={cancelEquipmentCrafting}>Cancel</Button> <Button size="sm" variant="outline" onClick={cancelEquipmentCrafting}>Cancel</Button>
</div> </div>
) : ( );
<ScrollArea className="h-64"> }
<div className="space-y-2">
{lootInventory.blueprints.length === 0 ? ( // ─── Blueprint Card ───────────────────────────────────────────────────────────
<div className="text-center text-gray-400 py-4">
<Package className="w-12 h-12 mx-auto mb-2 opacity-50" /> function BlueprintCard({ bpId, lootInventory, rawMana, isCrafting }: {
<p>No blueprints discovered yet.</p> bpId: string;
<p className="text-xs mt-1">Defeat guardians to find blueprints!</p> lootInventory: LootInventory;
</div> rawMana: number;
) : ( isCrafting: boolean;
lootInventory.blueprints.map(bpId => { }) {
const recipe = CRAFTING_RECIPES[bpId]; const recipe = CRAFTING_RECIPES[bpId];
if (!recipe) return null; if (!recipe) return null;
const { canCraft, missingMaterials, missingMana } = canCraftRecipe( const { canCraft, missingMaterials } = canCraftRecipe(recipe, lootInventory.materials, rawMana);
recipe,
lootInventory.materials,
rawMana
);
const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity]; const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity];
const startCraftingEquipment = useCraftingStore((s) => s.startCraftingEquipment);
const currentAction = useCombatStore((s) => s.currentAction);
return ( return (
<div <div
key={bpId}
className="p-3 rounded border bg-gray-800/50" className="p-3 rounded border bg-gray-800/50"
style={{ borderColor: rarityStyle?.color }} style={{ borderColor: rarityStyle?.color }}
> >
@@ -122,49 +104,58 @@ export function EquipmentCrafter() {
<Button <Button
className="w-full mt-3" className="w-full mt-3"
size="sm" size="sm"
disabled={!canCraft || currentAction === 'craft'} disabled={!canCraft || isCrafting}
onClick={() => startCraftingEquipment(bpId)} onClick={() => startCraftingEquipment(bpId)}
> >
{canCraft ? 'Craft Equipment' : 'Missing Resources'} {canCraft ? 'Craft Equipment' : 'Missing Resources'}
</Button> </Button>
</div> </div>
); );
}) }
)}
// ─── Blueprint List ───────────────────────────────────────────────────────────
function BlueprintList({ lootInventory, rawMana }: { lootInventory: LootInventory; rawMana: number }) {
const currentAction = useCombatStore((s) => s.currentAction);
if (lootInventory.blueprints.length === 0) {
return (
<div className="text-center text-gray-400 py-4">
<Package className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No blueprints discovered yet.</p>
<p className="text-xs mt-1">Defeat guardians to find blueprints!</p>
</div>
);
}
return (
<ScrollArea className="h-64">
<div className="space-y-2">
{lootInventory.blueprints.map(bpId => (
<BlueprintCard
key={bpId}
bpId={bpId}
lootInventory={lootInventory}
rawMana={rawMana}
isCrafting={currentAction === 'craft'}
/>
))}
</div> </div>
</ScrollArea> </ScrollArea>
)} );
</CardContent> }
</Card>
{/* Materials Inventory */} // ─── Material Card ────────────────────────────────────────────────────────────
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> function MaterialCard({ matId, count }: { matId: string; count: number }) {
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
<Package className="w-4 h-4" />
Materials ({Object.values(lootInventory.materials).reduce((a, b) => a + b, 0)})
</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-64">
{Object.keys(lootInventory.materials).length === 0 ? (
<div className="text-center text-gray-400 py-4">
<Sparkles className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No materials collected yet.</p>
<p className="text-xs mt-1">Defeat floors to gather materials!</p>
</div>
) : (
<div className="grid grid-cols-2 gap-2">
{Object.entries(lootInventory.materials).map(([matId, count]) => {
if (count <= 0) return null;
const drop = LOOT_DROPS[matId]; const drop = LOOT_DROPS[matId];
if (!drop) return null; if (!drop) return null;
const rarityStyle = LOOT_RARITY_COLORS[drop.rarity]; const rarityStyle = LOOT_RARITY_COLORS[drop.rarity];
const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
return ( return (
<div <div
key={matId}
className="p-2 rounded border bg-gray-800/50 group relative" className="p-2 rounded border bg-gray-800/50 group relative"
style={{ borderColor: rarityStyle?.color }} style={{ borderColor: rarityStyle?.color }}
> >
@@ -186,14 +177,71 @@ export function EquipmentCrafter() {
</div> </div>
</div> </div>
); );
}
// ─── Materials Inventory ─────────────────────────────────────────────────────
function MaterialsInventory({ materials }: { materials: Record<string, number> }) {
const totalCount = Object.values(materials).reduce((a, b) => a + b, 0);
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
<Package className="w-4 h-4" />
Materials ({totalCount})
</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-64">
{Object.keys(materials).length === 0 ? (
<div className="text-center text-gray-400 py-4">
<Sparkles className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No materials collected yet.</p>
<p className="text-xs mt-1">Defeat floors to gather materials!</p>
</div>
) : (
<div className="grid grid-cols-2 gap-2">
{Object.entries(materials).map(([matId, count]) => {
if (count <= 0) return null;
return <MaterialCard key={matId} matId={matId} count={count} />;
})} })}
</div> </div>
)} )}
</ScrollArea> </ScrollArea>
</CardContent> </CardContent>
</Card> </Card>
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export function EquipmentCrafter() {
const lootInventory = useCraftingStore((s) => s.lootInventory);
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
const rawMana = useManaStore((s) => s.rawMana);
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
<Anvil className="w-4 h-4" />
Available Blueprints
</CardTitle>
</CardHeader>
<CardContent>
{equipmentCraftingProgress ? (
<CraftingProgress progress={equipmentCraftingProgress} />
) : (
<BlueprintList lootInventory={lootInventory} rawMana={rawMana} />
)}
</CardContent>
</Card>
<MaterialsInventory materials={lootInventory.materials} />
</div> </div>
); );
} }
EquipmentCrafter.displayName = "EquipmentCrafter"; EquipmentCrafter.displayName = 'EquipmentCrafter';
+151 -114
View File
@@ -13,50 +13,10 @@ import { useDebug } from '@/components/game/debug/debug-context';
import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores'; import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores';
import { computeMaxMana } from '@/lib/game/stores'; import { computeMaxMana } from '@/lib/game/stores';
export function GameStateDebug() { // ─── Warning Banner ──────────────────────────────────────────────────────────
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);
const gatherMana = useGameStore((s) => s.gatherMana);
const day = useGameStore((s) => s.day);
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);
const handleReset = () => {
if (confirmReset) {
resetGame();
setConfirmReset(false);
} else {
setConfirmReset(true);
setTimeout(() => setConfirmReset(false), 3000);
}
};
const handleAddMana = (amount: number) => {
for (let i = 0; i < amount; i++) {
gatherMana();
}
};
const getMaxMana = () => {
return computeMaxMana(
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
);
};
function WarningBanner() {
return ( return (
<div className="space-y-4">
{/* Warning Banner */}
<Card className="bg-amber-900/20 border-amber-600/50"> <Card className="bg-amber-900/20 border-amber-600/50">
<CardContent className="pt-4"> <CardContent className="pt-4">
<div className="flex items-center gap-2 text-amber-400"> <div className="flex items-center gap-2 text-amber-400">
@@ -68,8 +28,15 @@ export function GameStateDebug() {
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
);
}
{/* Display Options */} // ─── Display Options ─────────────────────────────────────────────────────────
function DisplayOptions() {
const { showComponentNames, toggleComponentNames } = useDebug();
return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2"> <CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
@@ -93,9 +60,13 @@ export function GameStateDebug() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
);
}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> // ─── Game Reset Section ──────────────────────────────────────────────────────
{/* Game Reset */}
function GameResetSection({ confirmReset, onReset }: { confirmReset: boolean; onReset: () => void }) {
return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-red-400 text-sm flex items-center gap-2"> <CardTitle className="text-red-400 text-sm flex items-center gap-2">
@@ -109,7 +80,7 @@ export function GameStateDebug() {
</p> </p>
<Button <Button
className={`w-full ${confirmReset ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'}`} className={`w-full ${confirmReset ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'}`}
onClick={handleReset} onClick={onReset}
> >
{confirmReset ? ( {confirmReset ? (
<> <>
@@ -125,8 +96,21 @@ export function GameStateDebug() {
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
);
}
{/* Mana Debug */} // ─── Mana Debug Section ──────────────────────────────────────────────────────
function ManaDebugSection({ rawMana, onAddMana, onFillMana }: {
rawMana: number;
onAddMana: (amount: number) => void;
onFillMana: () => void;
}) {
const maxMana = computeMaxMana(
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
);
return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-blue-400 text-sm flex items-center gap-2"> <CardTitle className="text-blue-400 text-sm flex items-center gap-2">
@@ -136,38 +120,42 @@ export function GameStateDebug() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="text-xs text-gray-400 mb-2"> <div className="text-xs text-gray-400 mb-2">
Current: {rawMana} / {getMaxMana() || '?'} Current: {rawMana} / {maxMana || '?'}
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Button size="sm" variant="outline" onClick={() => handleAddMana(10)}> <Button size="sm" variant="outline" onClick={() => onAddMana(10)}>
<Zap className="w-3 h-3 mr-1" /> +10 <Zap className="w-3 h-3 mr-1" /> +10
</Button> </Button>
<Button size="sm" variant="outline" onClick={() => handleAddMana(100)}> <Button size="sm" variant="outline" onClick={() => onAddMana(100)}>
<Zap className="w-3 h-3 mr-1" /> +100 <Zap className="w-3 h-3 mr-1" /> +100
</Button> </Button>
<Button size="sm" variant="outline" onClick={() => handleAddMana(1000)}> <Button size="sm" variant="outline" onClick={() => onAddMana(1000)}>
<Zap className="w-3 h-3 mr-1" /> +1K <Zap className="w-3 h-3 mr-1" /> +1K
</Button> </Button>
<Button size="sm" variant="outline" onClick={() => handleAddMana(10000)}> <Button size="sm" variant="outline" onClick={() => onAddMana(10000)}>
<Zap className="w-3 h-3 mr-1" /> +10K <Zap className="w-3 h-3 mr-1" /> +10K
</Button> </Button>
</div> </div>
<Separator className="bg-gray-700" /> <Separator className="bg-gray-700" />
<div className="text-xs text-gray-400 mb-2">Fill to max:</div> <div className="text-xs text-gray-400 mb-2">Fill to max:</div>
<Button <Button size="sm" className="w-full bg-blue-600 hover:bg-blue-700" onClick={onFillMana}>
size="sm"
className="w-full bg-blue-600 hover:bg-blue-700"
onClick={() => {
const max = getMaxMana() || 100;
useManaStore.setState((s) => ({ rawMana: Math.max(s.rawMana, max) }));
}}
>
Fill Mana Fill Mana
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
);
}
{/* Time Control */} // ─── Time Control Section ────────────────────────────────────────────────────
function TimeControlSection({ day, hour, paused, onSetDay, onTogglePause }: {
day: number;
hour: number;
paused: boolean;
onSetDay: (day: number) => void;
onTogglePause: () => void;
}) {
return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm flex items-center gap-2"> <CardTitle className="text-amber-400 text-sm flex items-center gap-2">
@@ -180,33 +168,32 @@ export function GameStateDebug() {
Current: Day {day}, Hour {hour} Current: Day {day}, Hour {hour}
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 1, hour: 0 })}> <Button size="sm" variant="outline" onClick={() => onSetDay(1)}>Day 1</Button>
Day 1 <Button size="sm" variant="outline" onClick={() => onSetDay(10)}>Day 10</Button>
</Button> <Button size="sm" variant="outline" onClick={() => onSetDay(20)}>Day 20</Button>
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 10, hour: 0 })}> <Button size="sm" variant="outline" onClick={() => onSetDay(30)}>Day 30</Button>
Day 10
</Button>
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 20, hour: 0 })}>
Day 20
</Button>
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 30, hour: 0 })}>
Day 30
</Button>
</div> </div>
<Separator className="bg-gray-700" /> <Separator className="bg-gray-700" />
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button size="sm" variant="outline" onClick={onTogglePause}>
size="sm"
variant="outline"
onClick={togglePause}
>
{paused ? '▶ Resume' : '⏸ Pause'} {paused ? '▶ Resume' : '⏸ Pause'}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
);
}
{/* Skills Debug - Quick Actions */} // ─── Quick Actions Section ───────────────────────────────────────────────────
function QuickActionsSection({ elements, onUnlockBase, onUnlockUtility, onSkipToFloor, onResetFloorHP }: {
elements: Record<string, { unlocked?: boolean }>;
onUnlockBase: () => void;
onUnlockUtility: () => void;
onSkipToFloor: () => void;
onResetFloorHP: () => void;
}) {
return (
<Card className="bg-gray-900/80 border-gray-700 md:col-span-2"> <Card className="bg-gray-900/80 border-gray-700 md:col-span-2">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2"> <CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
@@ -216,54 +203,104 @@ export function GameStateDebug() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Button <Button size="sm" variant="outline" onClick={onUnlockBase}>
size="sm"
variant="outline"
onClick={() => {
// 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 All Base Elements
</Button> </Button>
<Button <Button size="sm" variant="outline" onClick={onUnlockUtility}>
size="sm"
variant="outline"
onClick={() => {
// Unlock utility elements
['transference'].forEach(e => {
if (!elements[e]?.unlocked) {
unlockElement(e, 500);
}
});
}}
>
Unlock Utility Elements Unlock Utility Elements
</Button> </Button>
<Button <Button size="sm" variant="outline" onClick={onSkipToFloor}>
size="sm"
variant="outline"
onClick={() => debugSetFloor?.(100)}
>
Skip to Floor 100 Skip to Floor 100
</Button> </Button>
<Button <Button size="sm" variant="outline" onClick={onResetFloorHP}>
size="sm"
variant="outline"
onClick={() => resetFloorHP?.()}
>
Reset Floor HP Reset Floor HP
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export function GameStateDebug() {
const [confirmReset, setConfirmReset] = useState(false);
const { showComponentNames, toggleComponentNames } = useDebug();
const rawMana = useManaStore((s) => s.rawMana);
const elements = useManaStore((s) => s.elements);
const unlockElement = useManaStore((s) => s.unlockElement);
const gatherMana = useGameStore((s) => s.gatherMana);
const day = useGameStore((s) => s.day);
const hour = useGameStore((s) => s.hour);
const paused = useUIStore((s) => s.paused);
const togglePause = useUIStore((s) => s.togglePause);
const resetGame = useGameStore((s) => s.resetGame);
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
const resetFloorHP = useCombatStore((s) => s.resetFloorHP);
const handleReset = () => {
if (confirmReset) {
resetGame();
setConfirmReset(false);
} else {
setConfirmReset(true);
setTimeout(() => setConfirmReset(false), 3000);
}
};
const handleAddMana = (amount: number) => {
for (let i = 0; i < amount; i++) {
gatherMana();
}
};
const 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 (
<div className="space-y-4">
<WarningBanner />
<DisplayOptions />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<GameResetSection confirmReset={confirmReset} onReset={handleReset} />
<ManaDebugSection rawMana={rawMana} onAddMana={handleAddMana} onFillMana={handleFillMana} />
<TimeControlSection day={day} hour={hour} paused={paused} onSetDay={handleSetDay} onTogglePause={togglePause} />
<QuickActionsSection
elements={elements}
onUnlockBase={handleUnlockBase}
onUnlockUtility={handleUnlockUtility}
onSkipToFloor={() => debugSetFloor?.(100)}
onResetFloorHP={() => resetFloorHP?.()}
/>
</div> </div>
</div> </div>
); );
} }
GameStateDebug.displayName = "GameStateDebug"; GameStateDebug.displayName = 'GameStateDebug';
+76 -72
View File
@@ -6,48 +6,106 @@ import { Bug } from 'lucide-react';
import { usePrestigeStore, useManaStore, useUIStore, useGameStore } from '@/lib/game/stores'; import { usePrestigeStore, useManaStore, useUIStore, useGameStore } from '@/lib/game/stores';
import { GUARDIANS, ELEMENTS } from '@/lib/game/constants'; 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 (
<div
className={`p-2 rounded border flex items-center justify-between ${
isSigned ? 'border-green-600/50 bg-green-900/20' : 'border-gray-700'
}`}
style={{ borderColor: isSigned ? undefined : guardian.color, borderWidth: '1px' }}
>
<div>
<div className="text-sm font-semibold" style={{ color: guardian.color }}>
{guardian.name}
</div>
<div className="text-xs text-gray-400">
Floor {floor} | {guardian.pact}x multiplier
</div>
<div className="text-xs text-gray-500">
Element: {ELEMENTS[guardian.element]?.name || guardian.element}
</div>
</div>
<div className="flex gap-1">
{isSigned ? (
<Button size="sm" variant="destructive" onClick={onRemove} className="text-xs">
Remove
</Button>
) : (
<Button size="sm" variant="default" onClick={onForceSign} className="text-xs bg-amber-600 hover:bg-amber-700">
Force Sign
</Button>
)}
</div>
</div>
);
}
// ─── 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 (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{guardianFloors.map((floor) => (
<GuardianPactRow
key={floor}
floor={floor}
isSigned={signedPacts.includes(floor)}
onForceSign={() => onForceSign(floor)}
onRemove={() => onRemove(floor)}
/>
))}
</div>
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export function PactDebug() { export function PactDebug() {
// Get state from modular stores
const signedPacts = usePrestigeStore((s) => s.signedPacts); const signedPacts = usePrestigeStore((s) => s.signedPacts);
const signedPactDetails = usePrestigeStore((s) => s.signedPactDetails); const signedPactDetails = usePrestigeStore((s) => s.signedPactDetails);
const elements = useManaStore((s) => s.elements); const elements = useManaStore((s) => s.elements);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
// Get actions
const addSignedPact = usePrestigeStore((s) => s.addSignedPact); const addSignedPact = usePrestigeStore((s) => s.addSignedPact);
const removePact = usePrestigeStore((s) => s.removePact); const removePact = usePrestigeStore((s) => s.removePact);
const debugSetSignedPacts = usePrestigeStore((s) => s.debugSetSignedPacts); const debugSetSignedPacts = usePrestigeStore((s) => s.debugSetSignedPacts);
const debugSetPactDetails = usePrestigeStore((s) => s.debugSetPactDetails); const debugSetPactDetails = usePrestigeStore((s) => s.debugSetPactDetails);
const unlockElement = useManaStore((s) => s.unlockElement); const unlockElement = useManaStore((s) => s.unlockElement);
// Get log function from uiStore
const addLog = useUIStore((s) => s.addLog); 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 forcePact = (floor: number) => { const forcePact = (floor: number) => {
const guardian = GUARDIANS[floor]; const guardian = GUARDIANS[floor];
if (!guardian) return; if (!guardian) return;
// Check if already signed
if (signedPacts.includes(floor)) { if (signedPacts.includes(floor)) {
addLog(`⚠️ Already signed pact with ${guardian.name}!`); addLog(`⚠️ Already signed pact with ${guardian.name}!`);
return; return;
} }
// Check max pacts
const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0); const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0);
if (signedPacts.length >= maxPacts) { if (signedPacts.length >= maxPacts) {
addLog(`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`); addLog(`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`);
return; return;
} }
// Force sign the pact
addSignedPact(floor); addSignedPact(floor);
// Add pact details
const newSignedPactDetails = { const newSignedPactDetails = {
...signedPactDetails, ...signedPactDetails,
[floor]: { [floor]: {
@@ -62,13 +120,11 @@ export function PactDebug() {
addLog(`📜 DEBUG: Pact with ${guardian.name} force-signed!`); addLog(`📜 DEBUG: Pact with ${guardian.name} force-signed!`);
}; };
// Remove a pact
const removePactHandler = (floor: number) => { const removePactHandler = (floor: number) => {
const guardian = GUARDIANS[floor]; const guardian = GUARDIANS[floor];
removePact(floor); removePact(floor);
// Remove pact details
const newSignedPactDetails = { ...signedPactDetails }; const newSignedPactDetails = { ...signedPactDetails };
delete newSignedPactDetails[floor]; delete newSignedPactDetails[floor];
debugSetPactDetails(newSignedPactDetails); debugSetPactDetails(newSignedPactDetails);
@@ -76,7 +132,6 @@ export function PactDebug() {
addLog(`📜 DEBUG: Removed pact with ${guardian?.name || 'Unknown'}!`); addLog(`📜 DEBUG: Removed pact with ${guardian?.name || 'Unknown'}!`);
}; };
// Clear all pacts
const clearAllPacts = () => { const clearAllPacts = () => {
addLog(`📜 DEBUG: Cleared all pacts!`); addLog(`📜 DEBUG: Cleared all pacts!`);
debugSetSignedPacts([]); debugSetSignedPacts([]);
@@ -97,71 +152,20 @@ export function PactDebug() {
Force sign pacts with guardians (bypasses mana costs and signing time) Force sign pacts with guardians (bypasses mana costs and signing time)
</p> </p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2"> <GuardianPactList
{guardianFloors.map((floor) => { signedPacts={signedPacts}
const guardian = GUARDIANS[floor]; onForceSign={forcePact}
const isSigned = signedPacts.includes(floor); onRemove={removePactHandler}
/>
return (
<div
key={floor}
className={`p-2 rounded border flex items-center justify-between ${
isSigned ? 'border-green-600/50 bg-green-900/20' : 'border-gray-700'
}`}
style={{ borderColor: isSigned ? undefined : guardian.color, borderWidth: '1px' }}
>
<div>
<div className="text-sm font-semibold" style={{ color: guardian.color }}>
{guardian.name}
</div>
<div className="text-xs text-gray-400">
Floor {floor} | {guardian.pact}x multiplier
</div>
<div className="text-xs text-gray-500">
Element: {ELEMENTS[guardian.element]?.name || guardian.element}
</div>
</div>
<div className="flex gap-1">
{isSigned ? (
<Button
size="sm"
variant="destructive"
onClick={() => removePactHandler(floor)}
className="text-xs"
>
Remove
</Button>
) : (
<Button
size="sm"
variant="default"
onClick={() => forcePact(floor)}
className="text-xs bg-amber-600 hover:bg-amber-700"
>
Force Sign
</Button>
)}
</div>
</div>
);
})}
</div>
{/* Clear All Button */}
{signedPacts.length > 0 && ( {signedPacts.length > 0 && (
<div className="pt-2 border-t border-gray-700"> <div className="pt-2 border-t border-gray-700">
<Button <Button size="sm" variant="destructive" onClick={clearAllPacts} className="w-full text-xs">
size="sm"
variant="destructive"
onClick={clearAllPacts}
className="w-full text-xs"
>
Clear All Pacts ({signedPacts.length}) Clear All Pacts ({signedPacts.length})
</Button> </Button>
</div> </div>
)} )}
{/* Status */}
<div className="text-xs text-gray-400 pt-2 border-t border-gray-700"> <div className="text-xs text-gray-400 pt-2 border-t border-gray-700">
Signed Pacts: {signedPacts.length} | Signed Pacts: {signedPacts.length} |
Max Pacts: {1 + (prestigeUpgrades?.pactCapacity || 0)} Max Pacts: {1 + (prestigeUpgrades?.pactCapacity || 0)}
@@ -172,4 +176,4 @@ export function PactDebug() {
); );
} }
PactDebug.displayName = "PactDebug"; PactDebug.displayName = 'PactDebug';
@@ -13,48 +13,12 @@ import { useDebug } from '@/components/game/debug/debug-context';
import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores'; import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores';
import { computeMaxMana } from '@/lib/game/stores'; import { computeMaxMana } from '@/lib/game/stores';
export function GameStateDebugSection() { // ─── Display Options ─────────────────────────────────────────────────────────
const [confirmReset, setConfirmReset] = useState(false);
function DisplayOptions() {
const { showComponentNames, toggleComponentNames } = useDebug(); const { showComponentNames, toggleComponentNames } = useDebug();
const rawMana = useManaStore((s) => s.rawMana);
const day = useGameStore((s) => s.day);
const hour = useGameStore((s) => s.hour);
const paused = useUIStore((s) => s.paused);
const togglePause = useUIStore((s) => s.togglePause);
const resetGame = useGameStore((s) => s.resetGame);
const gatherMana = useGameStore((s) => s.gatherMana);
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
const resetFloorHP = useCombatStore((s) => s.resetFloorHP);
const elements = useManaStore((s) => s.elements);
const unlockElement = useManaStore((s) => s.unlockElement);
const handleReset = () => {
if (confirmReset) {
resetGame();
setConfirmReset(false);
} else {
setConfirmReset(true);
setTimeout(() => setConfirmReset(false), 3000);
}
};
const handleAddMana = (amount: number) => {
for (let i = 0; i < amount; i++) {
gatherMana();
}
};
const getMaxMana = () => {
return computeMaxMana(
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
);
};
return ( return (
<div className="space-y-4">
{/* Display Options */}
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2"> <CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
@@ -78,9 +42,13 @@ export function GameStateDebugSection() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
);
}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> // ─── Game Reset Section ──────────────────────────────────────────────────────
{/* Game Reset */}
function GameResetSection({ confirmReset, onReset }: { confirmReset: boolean; onReset: () => void }) {
return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-red-400 text-sm flex items-center gap-2"> <CardTitle className="text-red-400 text-sm flex items-center gap-2">
@@ -94,7 +62,7 @@ export function GameStateDebugSection() {
</p> </p>
<Button <Button
className={`w-full ${confirmReset ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'}`} className={`w-full ${confirmReset ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'}`}
onClick={handleReset} onClick={onReset}
> >
{confirmReset ? ( {confirmReset ? (
<> <>
@@ -110,8 +78,21 @@ export function GameStateDebugSection() {
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
);
}
{/* Mana Debug */} // ─── Mana Debug Section ──────────────────────────────────────────────────────
function ManaDebugSection({ rawMana, onAddMana, onFillMana }: {
rawMana: number;
onAddMana: (amount: number) => void;
onFillMana: () => void;
}) {
const maxMana = computeMaxMana(
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
);
return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-blue-400 text-sm flex items-center gap-2"> <CardTitle className="text-blue-400 text-sm flex items-center gap-2">
@@ -121,38 +102,42 @@ export function GameStateDebugSection() {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="text-xs text-gray-400 mb-2"> <div className="text-xs text-gray-400 mb-2">
Current: {rawMana} / {getMaxMana() || '?'} Current: {rawMana} / {maxMana || '?'}
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Button size="sm" variant="outline" onClick={() => handleAddMana(10)}> <Button size="sm" variant="outline" onClick={() => onAddMana(10)}>
<Zap className="w-3 h-3 mr-1" /> +10 <Zap className="w-3 h-3 mr-1" /> +10
</Button> </Button>
<Button size="sm" variant="outline" onClick={() => handleAddMana(100)}> <Button size="sm" variant="outline" onClick={() => onAddMana(100)}>
<Zap className="w-3 h-3 mr-1" /> +100 <Zap className="w-3 h-3 mr-1" /> +100
</Button> </Button>
<Button size="sm" variant="outline" onClick={() => handleAddMana(1000)}> <Button size="sm" variant="outline" onClick={() => onAddMana(1000)}>
<Zap className="w-3 h-3 mr-1" /> +1K <Zap className="w-3 h-3 mr-1" /> +1K
</Button> </Button>
<Button size="sm" variant="outline" onClick={() => handleAddMana(10000)}> <Button size="sm" variant="outline" onClick={() => onAddMana(10000)}>
<Zap className="w-3 h-3 mr-1" /> +10K <Zap className="w-3 h-3 mr-1" /> +10K
</Button> </Button>
</div> </div>
<Separator className="bg-gray-700" /> <Separator className="bg-gray-700" />
<div className="text-xs text-gray-400 mb-2">Fill to max:</div> <div className="text-xs text-gray-400 mb-2">Fill to max:</div>
<Button <Button size="sm" className="w-full bg-blue-600 hover:bg-blue-700" onClick={onFillMana}>
size="sm"
className="w-full bg-blue-600 hover:bg-blue-700"
onClick={() => {
const max = getMaxMana() || 100;
useManaStore.setState((s) => ({ rawMana: Math.max(s.rawMana, max) }));
}}
>
Fill Mana Fill Mana
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
);
}
{/* Time Control */} // ─── Time Control Section ────────────────────────────────────────────────────
function TimeControlSection({ day, hour, paused, onSetDay, onTogglePause }: {
day: number;
hour: number;
paused: boolean;
onSetDay: (day: number) => void;
onTogglePause: () => void;
}) {
return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm flex items-center gap-2"> <CardTitle className="text-amber-400 text-sm flex items-center gap-2">
@@ -165,29 +150,31 @@ export function GameStateDebugSection() {
Current: Day {day}, Hour {hour} Current: Day {day}, Hour {hour}
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 1, hour: 0 })}> <Button size="sm" variant="outline" onClick={() => onSetDay(1)}>Day 1</Button>
Day 1 <Button size="sm" variant="outline" onClick={() => onSetDay(10)}>Day 10</Button>
</Button> <Button size="sm" variant="outline" onClick={() => onSetDay(20)}>Day 20</Button>
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 10, hour: 0 })}> <Button size="sm" variant="outline" onClick={() => onSetDay(30)}>Day 30</Button>
Day 10
</Button>
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 20, hour: 0 })}>
Day 20
</Button>
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 30, hour: 0 })}>
Day 30
</Button>
</div> </div>
<Separator className="bg-gray-700" /> <Separator className="bg-gray-700" />
<div className="flex gap-2"> <div className="flex gap-2">
<Button size="sm" variant="outline" onClick={togglePause}> <Button size="sm" variant="outline" onClick={onTogglePause}>
{paused ? '▶ Resume' : '⏸ Pause'} {paused ? '▶ Resume' : '⏸ Pause'}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
);
}
{/* Quick Actions */} // ─── Quick Actions Section ───────────────────────────────────────────────────
function QuickActionsSection({ elements, onUnlockBase, onSkipToFloor, onResetFloorHP }: {
elements: Record<string, { unlocked?: boolean }>;
onUnlockBase: () => void;
onSkipToFloor: () => void;
onResetFloorHP: () => void;
}) {
return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2"> <CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
@@ -197,39 +184,91 @@ export function GameStateDebugSection() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Button <Button size="sm" variant="outline" onClick={onUnlockBase}>
size="sm"
variant="outline"
onClick={() => {
['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => {
if (!elements[e]?.unlocked) {
unlockElement(e, 0);
}
});
}}
>
Unlock All Base Elements Unlock All Base Elements
</Button> </Button>
<Button <Button size="sm" variant="outline" onClick={onSkipToFloor}>
size="sm"
variant="outline"
onClick={() => debugSetFloor?.(100)}
>
Skip to Floor 100 Skip to Floor 100
</Button> </Button>
<Button <Button size="sm" variant="outline" onClick={onResetFloorHP}>
size="sm"
variant="outline"
onClick={() => resetFloorHP?.()}
>
Reset Floor HP Reset Floor HP
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export function GameStateDebugSection() {
const [confirmReset, setConfirmReset] = useState(false);
const { showComponentNames, toggleComponentNames } = useDebug();
const rawMana = useManaStore((s) => s.rawMana);
const day = useGameStore((s) => s.day);
const hour = useGameStore((s) => s.hour);
const paused = useUIStore((s) => s.paused);
const togglePause = useUIStore((s) => s.togglePause);
const resetGame = useGameStore((s) => s.resetGame);
const gatherMana = useGameStore((s) => s.gatherMana);
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
const resetFloorHP = useCombatStore((s) => s.resetFloorHP);
const elements = useManaStore((s) => s.elements);
const unlockElement = useManaStore((s) => s.unlockElement);
const handleReset = () => {
if (confirmReset) {
resetGame();
setConfirmReset(false);
} else {
setConfirmReset(true);
setTimeout(() => setConfirmReset(false), 3000);
}
};
const handleAddMana = (amount: number) => {
for (let i = 0; i < amount; i++) {
gatherMana();
}
};
const 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 (
<div className="space-y-4">
<DisplayOptions />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<GameResetSection confirmReset={confirmReset} onReset={handleReset} />
<ManaDebugSection rawMana={rawMana} onAddMana={handleAddMana} onFillMana={handleFillMana} />
<TimeControlSection day={day} hour={hour} paused={paused} onSetDay={handleSetDay} onTogglePause={togglePause} />
<QuickActionsSection
elements={elements}
onUnlockBase={handleUnlockBase}
onSkipToFloor={() => debugSetFloor?.(100)}
onResetFloorHP={() => resetFloorHP?.()}
/>
</div> </div>
</div> </div>
); );
} }
GameStateDebugSection.displayName = "GameStateDebugSection"; GameStateDebugSection.displayName = 'GameStateDebugSection';
@@ -6,6 +6,51 @@ import { Bug } from 'lucide-react';
import { usePrestigeStore, useManaStore, useUIStore, useGameStore } from '@/lib/game/stores'; import { usePrestigeStore, useManaStore, useUIStore, useGameStore } from '@/lib/game/stores';
import { GUARDIANS, ELEMENTS } from '@/lib/game/constants'; 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 (
<div
className={`p-2 rounded border flex items-center justify-between ${
isSigned ? 'border-green-600/50 bg-green-900/20' : 'border-gray-700'
}`}
style={{ borderColor: isSigned ? undefined : guardian.color, borderWidth: '1px' }}
>
<div>
<div className="text-sm font-semibold" style={{ color: guardian.color }}>
{guardian.name}
</div>
<div className="text-xs text-gray-400">
Floor {floor} | {guardian.pact}x multiplier
</div>
<div className="text-xs text-gray-500">
Element: {ELEMENTS[guardian.element]?.name || guardian.element}
</div>
</div>
<div className="flex gap-1">
{isSigned ? (
<Button size="sm" variant="destructive" onClick={onRemove} className="text-xs">
Remove
</Button>
) : (
<Button size="sm" variant="default" onClick={onForceSign} className="text-xs bg-amber-600 hover:bg-amber-700">
Force Sign
</Button>
)}
</div>
</div>
);
}
// ─── Main Component ───────────────────────────────────────────────────────────
export function PactDebugSection() { export function PactDebugSection() {
const signedPacts = usePrestigeStore((s) => s.signedPacts); const signedPacts = usePrestigeStore((s) => s.signedPacts);
const signedPactDetails = usePrestigeStore((s) => s.signedPactDetails); const signedPactDetails = usePrestigeStore((s) => s.signedPactDetails);
@@ -99,53 +144,15 @@ export function PactDebugSection() {
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{guardianFloors.map((floor) => { {guardianFloors.map((floor) => (
const guardian = GUARDIANS[floor]; <GuardianPactRow
const isSigned = signedPacts.includes(floor);
return (
<div
key={floor} key={floor}
className={`p-2 rounded border flex items-center justify-between ${ floor={floor}
isSigned ? 'border-green-600/50 bg-green-900/20' : 'border-gray-700' isSigned={signedPacts.includes(floor)}
}`} onForceSign={() => forcePact(floor)}
style={{ borderColor: isSigned ? undefined : guardian.color, borderWidth: '1px' }} onRemove={() => removePactHandler(floor)}
> />
<div> ))}
<div className="text-sm font-semibold" style={{ color: guardian.color }}>
{guardian.name}
</div>
<div className="text-xs text-gray-400">
Floor {floor} | {guardian.pact}x multiplier
</div>
<div className="text-xs text-gray-500">
Element: {ELEMENTS[guardian.element]?.name || guardian.element}
</div>
</div>
<div className="flex gap-1">
{isSigned ? (
<Button
size="sm"
variant="destructive"
onClick={() => removePactHandler(floor)}
className="text-xs"
>
Remove
</Button>
) : (
<Button
size="sm"
variant="default"
onClick={() => forcePact(floor)}
className="text-xs bg-amber-600 hover:bg-amber-700"
>
Force Sign
</Button>
)}
</div>
</div>
);
})}
</div> </div>
<div className="text-xs text-gray-400 pt-2 border-t border-gray-700"> <div className="text-xs text-gray-400 pt-2 border-t border-gray-700">
@@ -158,4 +165,4 @@ export function PactDebugSection() {
); );
} }
PactDebugSection.displayName = "PactDebugSection"; PactDebugSection.displayName = 'PactDebugSection';
+22 -250
View File
@@ -5,14 +5,14 @@ import { useShallow } from 'zustand/react/shallow';
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore'; import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
import { useManaStore } from '@/lib/game/stores/manaStore'; import { useManaStore } from '@/lib/game/stores/manaStore';
import { useUIStore } from '@/lib/game/stores/uiStore'; import { useUIStore } from '@/lib/game/stores/uiStore';
import { GUARDIANS, ELEMENTS } from '@/lib/game/constants'; import { GUARDIANS } from '@/lib/game/constants';
import type { GuardianDef, GuardianBoon } from '@/lib/game/types';
import { DebugName } from '@/components/game/debug/debug-context'; 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 { ScrollArea } from '@/components/ui/scroll-area';
import { Shield, Swords, Clock, Sparkles, Check, Lock, ChevronRight } from 'lucide-react'; import {
import clsx from 'clsx'; GuardianCard,
PactHeaderSummary,
TierFilter,
} from './guardian-pacts-components';
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -24,172 +24,6 @@ function getGuardianStatus(floor: number, defeated: number[], signed: number[]):
return 'undefeated'; 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<GuardianCardProps> = 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<GuardianStatus, { label: string; color: string; bg: string }> = {
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 (
<Card
className={clsx(
'border transition-colors',
status === 'signed' && 'border-green-600/40',
status === 'defeated' && 'border-amber-600/40',
status === 'undefeated' && 'border-gray-700/60',
)}
>
<CardHeader className="pb-2">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<CardTitle className="text-sm flex items-center gap-2" style={{ color: elemColor }}>
<span>{elemSym}</span>
<span className="truncate">{guardian.name}</span>
</CardTitle>
<div className="text-xs text-gray-500 mt-0.5">Floor {floor} · {elemDef?.name ?? guardian.element}</div>
</div>
<Badge className={clsx('text-[10px] px-1.5 py-0 shrink-0', sc.bg, sc.color)}>
{sc.label}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
{/* Stats */}
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="flex items-center gap-1 text-gray-400">
<Shield className="w-3 h-3" />
<span>HP: {guardian.hp.toLocaleString()}</span>
</div>
<div className="flex items-center gap-1 text-gray-400">
<Swords className="w-3 h-3" />
<span>PWR: {guardian.power.toLocaleString()}</span>
</div>
<div className="flex items-center gap-1 text-gray-400">
<Shield className="w-3 h-3" />
<span>ARM: {Math.round((guardian.armor ?? 0) * 100)}%</span>
</div>
</div>
{/* Boons */}
<div className="space-y-1">
<div className="text-xs font-medium text-gray-300 flex items-center gap-1">
<Sparkles className="w-3 h-3" /> Boons
</div>
<div className="flex flex-wrap gap-1">
{guardian.boons.map((boon: GuardianBoon, i: number) => (
<span
key={i}
className="px-1.5 py-0.5 text-[10px] rounded border border-gray-600/50 text-gray-300"
>
{boon.desc}
</span>
))}
</div>
</div>
{/* Unique Perk */}
<div className="text-xs text-gray-400">
<span className="text-gray-500">Perk:</span> {guardian.uniquePerk}
</div>
{/* Pact Cost */}
<div className="flex items-center gap-3 text-xs text-gray-400">
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{formatHours(guardian.pactTime)}</span>
</div>
<div>
<span className="text-gray-500">Cost:</span> {guardian.pactCost.toLocaleString()} mana
</div>
</div>
{/* Ritual Progress */}
{isRitualActive && (
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-amber-400">Ritual in progress</span>
<span className="text-gray-400">{ritualProgress}/{ritualTime}h</span>
</div>
<div className="w-full bg-gray-700 rounded-full h-1.5">
<div
className="bg-amber-500 h-1.5 rounded-full transition-all"
style={{ width: `${Math.min(100, (ritualProgress / ritualTime) * 100)}%` }}
/>
</div>
{ritualComplete && (
<div className="text-xs text-green-400 flex items-center gap-1">
<Check className="w-3 h-3" /> Ritual complete pact will be signed on next tick
</div>
)}
</div>
)}
{/* Action Button */}
{status === 'defeated' && !isRitualActive && (
<button
onClick={() => 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 ? (
<><Lock className="w-3 h-3" /> Not enough mana</>
) : !hasSlot ? (
<><Lock className="w-3 h-3" /> No pact slots</>
) : (
<><ChevronRight className="w-3 h-3" /> Begin Pact Ritual</>
)}
</button>
)}
</CardContent>
</Card>
);
});
GuardianCard.displayName = 'GuardianCard';
// ─── Floor Tier Groups ──────────────────────────────────────────────────────
interface FloorTier { interface FloorTier {
label: string; label: string;
floors: number[]; floors: number[];
@@ -263,7 +97,6 @@ export const GuardianPactsTab: React.FC = () => {
} }
}, [startPactRitual, rawMana, addLog]); }, [startPactRitual, rawMana, addLog]);
// Cumulative boon summary from signed pacts
const cumulativeBoons = useMemo(() => { const cumulativeBoons = useMemo(() => {
const boonMap: Record<string, number> = {}; const boonMap: Record<string, number> = {};
for (const floor of signedPacts) { for (const floor of signedPacts) {
@@ -287,95 +120,34 @@ export const GuardianPactsTab: React.FC = () => {
return ( return (
<DebugName name="GuardianPactsTab"> <DebugName name="GuardianPactsTab">
<div className="space-y-4"> <div className="space-y-4">
{/* Header Summary */} <PactHeaderSummary
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]"> signedCount={signedPacts.length}
<CardContent className="py-3"> pactSlots={pactSlots}
<div className="flex flex-wrap items-center gap-4 text-xs"> defeatedCount={defeatedGuardians.length}
<div className="flex items-center gap-1.5"> cumulativeBoons={cumulativeBoons}
<Shield className="w-3.5 h-3.5 text-amber-400" /> />
<span className="text-gray-400">Pact Slots:</span>
<span className="text-gray-200">{signedPacts.length} / {pactSlots}</span>
</div>
<div className="flex items-center gap-1.5">
<Check className="w-3.5 h-3.5 text-green-400" />
<span className="text-gray-400">Signed:</span>
<span className="text-green-400">{signedPacts.length}</span>
</div>
<div className="flex items-center gap-1.5">
<Swords className="w-3.5 h-3.5 text-red-400" />
<span className="text-gray-400">Defeated:</span>
<span className="text-red-400">{defeatedGuardians.length}</span>
</div>
</div>
{/* Cumulative Boons */} <TierFilter
{signedPacts.length > 0 && ( tiers={tiers}
<div className="mt-2 pt-2 border-t border-gray-700/50"> activeTier={activeTier}
<div className="text-xs text-gray-400 mb-1">Active Boon Effects:</div> guardianFloors={guardianFloors}
<div className="flex flex-wrap gap-1"> onSelectTier={setActiveTier}
{Object.entries(cumulativeBoons).map(([type, value]) => ( />
<span
key={type}
className="px-1.5 py-0.5 text-[10px] rounded border border-green-600/30 text-green-300 bg-green-900/20"
>
{type}: +{value}
</span>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* Tier Filter */}
<div className="flex gap-2 flex-wrap">
<button
onClick={() => 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})
</button>
{tiers.map((tier) => (
<button
key={tier.label}
onClick={() => 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})
</button>
))}
</div>
{/* Guardian Cards */}
<ScrollArea className="h-[500px] rounded border border-gray-700 p-3"> <ScrollArea className="h-[500px] rounded border border-gray-700 p-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{filteredFloors.map((floor) => { {filteredFloors.map((floor) => {
const guardian = GUARDIANS[floor]; const guardian = GUARDIANS[floor];
if (!guardian) return null; 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 ( return (
<GuardianCard <GuardianCard
key={floor} key={floor}
floor={floor} floor={floor}
guardian={guardian} guardian={guardian}
status={status} status={getGuardianStatus(floor, defeatedGuardians, signedPacts)}
canAfford={canAfford} canAfford={rawMana >= guardian.pactCost}
hasSlot={hasSlot} hasSlot={signedPacts.length < pactSlots}
isRitualActive={isRitualActive} isRitualActive={pactRitualFloor === floor}
ritualProgress={pactRitualProgress} ritualProgress={pactRitualProgress}
onStartRitual={handleStartRitual} onStartRitual={handleStartRitual}
/> />
+147 -107
View File
@@ -23,6 +23,101 @@ import {
AlertDialogTrigger, AlertDialogTrigger,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
// ─── Stat Cell ────────────────────────────────────────────────────────────────
function PrestigeStatCell({ value, label, color }: { value: string | number; label: string; color: string }) {
return (
<div className="text-center">
<div className={`text-2xl font-bold ${color}`}>{value}</div>
<div className="text-xs text-gray-400 mt-0.5">{label}</div>
</div>
);
}
// ─── Insight Summary ──────────────────────────────────────────────────────────
function InsightSummary({ insight, totalInsight, loopCount, loopInsight }: {
insight: number;
totalInsight: number;
loopCount: number;
loopInsight: number;
}) {
return (
<Card className="bg-gray-900/60 border-gray-700">
<CardContent className="py-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<PrestigeStatCell value={fmt(insight)} label="Insight Available" color="text-amber-400" />
<PrestigeStatCell value={fmt(totalInsight)} label="Total Insight Earned" color="text-gray-200" />
<PrestigeStatCell value={loopCount} label="Loops Completed" color="text-gray-200" />
<PrestigeStatCell value={fmt(loopInsight)} label="This Loop's Insight" color="text-purple-400" />
</div>
</CardContent>
</Card>
);
}
// ─── Memories Card ────────────────────────────────────────────────────────────
function MemoriesCard({ memories, memorySlots }: { memories: { skillId: string; level: number; tier: number }[]; memorySlots: number }) {
return (
<Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="🧠 Memories" />
<CardContent className="pt-0">
<p className="text-xs text-gray-400 mb-2">
Skills carried between loops. Slots: {memories.length}/{memorySlots}
</p>
{memories.length === 0 ? (
<p className="text-xs text-gray-500 italic">No memories stored yet.</p>
) : (
<div className="space-y-1">
{memories.map((m) => (
<div key={m.skillId} className="text-xs text-gray-300 flex justify-between">
<span>{m.skillId}</span>
<span className="text-gray-500">Lv.{m.level} T{m.tier}</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
// ─── Pacts Card ───────────────────────────────────────────────────────────────
function PactsCard({ signedPacts, pactSlots, defeatedGuardians }: {
signedPacts: number[];
pactSlots: number;
defeatedGuardians: number[];
}) {
return (
<Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="📜 Pacts" />
<CardContent className="pt-0">
<p className="text-xs text-gray-400 mb-2">
Guardian pacts signed. Slots: {signedPacts.length}/{pactSlots}
</p>
{defeatedGuardians.length > 0 && (
<p className="text-xs text-gray-500 mb-1">
Defeated guardians: {defeatedGuardians.map((f) => `F${f}`).join(', ')}
</p>
)}
{signedPacts.length === 0 ? (
<p className="text-xs text-gray-500 italic">No pacts signed yet.</p>
) : (
<div className="space-y-1">
{signedPacts.map((f) => (
<div key={f} className="text-xs text-green-400">
Floor {f} Pact signed
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
// ─── Upgrade Card ───────────────────────────────────────────────────────────── // ─── Upgrade Card ─────────────────────────────────────────────────────────────
interface UpgradeCardProps { 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 (
<Card className="bg-gray-900/60 border-red-900/50">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-red-400">Reset Loop</h3>
<p className="text-xs text-gray-400 mt-1">
End the current loop and gain {fmt(loopInsight)} insight. Your prestige upgrades, memories, and pacts are preserved.
</p>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" className="flex-shrink-0 ml-4">
Reset Loop
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset the Loop?</AlertDialogTitle>
<AlertDialogDescription>
This will end your current loop and award you <strong className="text-amber-400">{fmt(loopInsight)} insight</strong>.
Your prestige upgrades, memories, and pacts will be preserved.
<br /><br />
Day, hour, mana, floor progress, and combat state will be reset.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onReset}>
Confirm Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
);
}
// ─── Main Component ─────────────────────────────────────────────────────────── // ─── Main Component ───────────────────────────────────────────────────────────
export function PrestigeTab() { export function PrestigeTab() {
@@ -130,80 +268,18 @@ export function PrestigeTab() {
return ( return (
<DebugName name="PrestigeTab"> <DebugName name="PrestigeTab">
<div className="space-y-4"> <div className="space-y-4">
{/* Insight & Loop Summary */} <InsightSummary
<Card className="bg-gray-900/60 border-gray-700"> insight={insight}
<CardContent className="py-4"> totalInsight={totalInsight}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> loopCount={loopCount}
<div className="text-center"> loopInsight={loopInsight}
<div className="text-2xl font-bold text-amber-400">{fmt(insight)}</div> />
<div className="text-xs text-gray-400 mt-0.5">Insight Available</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-200">{fmt(totalInsight)}</div>
<div className="text-xs text-gray-400 mt-0.5">Total Insight Earned</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-200">{loopCount}</div>
<div className="text-xs text-gray-400 mt-0.5">Loops Completed</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-400">{fmt(loopInsight)}</div>
<div className="text-xs text-gray-400 mt-0.5">This Loop&apos;s Insight</div>
</div>
</div>
</CardContent>
</Card>
{/* Memories & Pacts */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-gray-900/60 border-gray-700"> <MemoriesCard memories={memories} memorySlots={memorySlots} />
<SectionHeader title="🧠 Memories" /> <PactsCard signedPacts={signedPacts} pactSlots={pactSlots} defeatedGuardians={defeatedGuardians} />
<CardContent className="pt-0">
<p className="text-xs text-gray-400 mb-2">
Skills carried between loops. Slots: {memories.length}/{memorySlots}
</p>
{memories.length === 0 ? (
<p className="text-xs text-gray-500 italic">No memories stored yet.</p>
) : (
<div className="space-y-1">
{memories.map((m) => (
<div key={m.skillId} className="text-xs text-gray-300 flex justify-between">
<span>{m.skillId}</span>
<span className="text-gray-500">Lv.{m.level} T{m.tier}</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="📜 Pacts" />
<CardContent className="pt-0">
<p className="text-xs text-gray-400 mb-2">
Guardian pacts signed. Slots: {signedPacts.length}/{pactSlots}
</p>
{defeatedGuardians.length > 0 && (
<p className="text-xs text-gray-500 mb-1">
Defeated guardians: {defeatedGuardians.map((f) => `F${f}`).join(', ')}
</p>
)}
{signedPacts.length === 0 ? (
<p className="text-xs text-gray-500 italic">No pacts signed yet.</p>
) : (
<div className="space-y-1">
{signedPacts.map((f) => (
<div key={f} className="text-xs text-green-400">
Floor {f} Pact signed
</div>
))}
</div>
)}
</CardContent>
</Card>
</div> </div>
{/* Prestige Upgrades */}
<Card className="bg-gray-900/60 border-gray-700"> <Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="⬆️ Prestige Upgrades" /> <SectionHeader title="⬆️ Prestige Upgrades" />
<CardContent className="pt-0"> <CardContent className="pt-0">
@@ -227,43 +303,7 @@ export function PrestigeTab() {
</CardContent> </CardContent>
</Card> </Card>
{/* Reset Loop */} <ResetLoopSection loopInsight={loopInsight} onReset={handleResetLoop} />
<Card className="bg-gray-900/60 border-red-900/50">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-red-400">Reset Loop</h3>
<p className="text-xs text-gray-400 mt-1">
End the current loop and gain {fmt(loopInsight)} insight. Your prestige upgrades, memories, and pacts are preserved.
</p>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" className="flex-shrink-0 ml-4">
Reset Loop
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset the Loop?</AlertDialogTitle>
<AlertDialogDescription>
This will end your current loop and award you <strong className="text-amber-400">{fmt(loopInsight)} insight</strong>.
Your prestige upgrades, memories, and pacts will be preserved.
<br /><br />
Day, hour, mana, floor progress, and combat state will be reset.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetLoop}>
Confirm Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
</div> </div>
</DebugName> </DebugName>
); );
@@ -5,82 +5,21 @@ import { useShallow } from 'zustand/react/shallow';
import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores'; import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects'; import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
import { getUnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
import { useCraftingStore } from '@/lib/game/stores/craftingStore'; import { useCraftingStore } from '@/lib/game/stores/craftingStore';
import { GUARDIANS } from '@/lib/game/constants'; import { GUARDIANS } from '@/lib/game/constants';
import { getExtendedGuardian, isGuardianFloor } from '@/lib/game/data/guardian-encounters'; 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 { SpireHeader } from './SpireHeader';
import { RoomDisplay } from './RoomDisplay'; import { RoomDisplay } from './RoomDisplay';
import { SpireCombatControls } from './SpireCombatControls'; import { SpireCombatControls } from './SpireCombatControls';
import { SpireActivityLog } from './SpireActivityLog'; import { SpireActivityLog } from './SpireActivityLog';
import { SpireManaDisplay } from './SpireManaDisplay'; import { SpireManaDisplay } from './SpireManaDisplay';
export function SpireCombatPage() { // ─── Derived Stats Hook ──────────────────────────────────────────────────────
const [mounted, setMounted] = useState(false);
const [roomsCleared, setRoomsCleared] = useState(0);
// Combat store function useSpireStats(prestigeUpgrades: Record<string, number>, equippedInstances: unknown[], equipmentInstances: unknown[]) {
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
const disciplineEffects = computeDisciplineEffects(); const disciplineEffects = computeDisciplineEffects();
// Compute derived stats
const upgradeEffects = getUnifiedEffects({ const upgradeEffects = getUnifiedEffects({
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {}, skillTiers: {},
@@ -103,10 +42,70 @@ export function SpireCombatPage() {
attunements: {}, attunements: {},
}, upgradeEffects, disciplineEffects); }, 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]); const totalRooms = useMemo(() => getRoomsForFloor(currentFloor), [currentFloor]);
// Initialize room on floor change
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
setRoomsCleared(0); setRoomsCleared(0);
@@ -115,12 +114,10 @@ export function SpireCombatPage() {
setAction('climb'); setAction('climb');
}, [currentFloor, totalRooms, setCurrentRoom, setAction]); }, [currentFloor, totalRooms, setCurrentRoom, setAction]);
// Handle room/floor transitions
const handleRoomCleared = () => { const handleRoomCleared = () => {
const nextRoomIndex = roomsCleared + 1; const nextRoomIndex = roomsCleared + 1;
if (nextRoomIndex >= totalRooms) { if (nextRoomIndex >= totalRooms) {
// Floor cleared
const wasGuardian = isGuardianFloor(currentFloor); const wasGuardian = isGuardianFloor(currentFloor);
setClearedFloor(currentFloor, true); setClearedFloor(currentFloor, true);
@@ -138,30 +135,26 @@ export function SpireCombatPage() {
floor: currentFloor, floor: currentFloor,
}); });
// Auto-advance to next floor
const newFloor = currentFloor + 1; const newFloor = currentFloor + 1;
const newTotalRooms = getRoomsForFloor(newFloor); const newTotalRooms = getRoomsForFloor(newFloor);
const newRoom = generateSpireFloorState(newFloor, 0, newTotalRooms); const newRoom = generateSpireFloorState(newFloor, 0, newTotalRooms);
setCurrentRoom(newRoom); setCurrentRoom(newRoom);
setFloorHP(floorMaxHP); // Reset HP for new floor setFloorHP(floorMaxHP);
setClearedFloor(currentFloor, true); setClearedFloor(currentFloor, true);
setRoomsCleared(0); setRoomsCleared(0);
} else { } else {
// Next room on same floor
const newRoom = generateSpireFloorState(currentFloor, nextRoomIndex, totalRooms); const newRoom = generateSpireFloorState(currentFloor, nextRoomIndex, totalRooms);
setCurrentRoom(newRoom); setCurrentRoom(newRoom);
setRoomsCleared(nextRoomIndex); setRoomsCleared(nextRoomIndex);
} }
}; };
// Handle climb up
const handleClimbUp = () => { const handleClimbUp = () => {
startClimbUp(); startClimbUp();
addActivityLog('floor_transition', `⬆️ Climbing to floor ${currentFloor + 1}...`); addActivityLog('floor_transition', `⬆️ Climbing to floor ${currentFloor + 1}...`);
}; };
// Handle climb down
const handleClimbDown = () => { const handleClimbDown = () => {
if (currentFloor <= 1) return; if (currentFloor <= 1) return;
startClimbDown(); startClimbDown();
@@ -170,7 +163,6 @@ export function SpireCombatPage() {
addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`); addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`);
}; };
// Handle exit spire
const handleExitSpire = () => { const handleExitSpire = () => {
exitSpireMode(); exitSpireMode();
addActivityLog('floor_transition', '🚪 Exited the Spire.'); addActivityLog('floor_transition', '🚪 Exited the Spire.');
@@ -186,7 +178,6 @@ export function SpireCombatPage() {
return ( return (
<div className="min-h-screen bg-gray-950 flex flex-col"> <div className="min-h-screen bg-gray-950 flex flex-col">
{/* Compact header */}
<header className="sticky top-0 z-50 bg-gray-900/95 border-b border-gray-800 px-4 py-2"> <header className="sticky top-0 z-50 bg-gray-900/95 border-b border-gray-800 px-4 py-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-lg font-bold text-amber-400 tracking-wider">🏔 SPIRE</h1> <h1 className="text-lg font-bold text-amber-400 tracking-wider">🏔 SPIRE</h1>
@@ -196,9 +187,7 @@ export function SpireCombatPage() {
</div> </div>
</header> </header>
{/* Main content */}
<main className="flex-1 p-4 space-y-4 max-w-7xl mx-auto w-full"> <main className="flex-1 p-4 space-y-4 max-w-7xl mx-auto w-full">
{/* Top section: Header + Mana */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<SpireHeader <SpireHeader
@@ -218,7 +207,6 @@ export function SpireCombatPage() {
</div> </div>
</div> </div>
{/* Middle section: Room + Controls */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<RoomDisplay floorState={currentRoom} floor={currentFloor} /> <RoomDisplay floorState={currentRoom} floor={currentFloor} />
@@ -228,7 +216,6 @@ export function SpireCombatPage() {
</div> </div>
</div> </div>
{/* Bottom: Activity Log */}
<SpireActivityLog activityLog={activityLog} maxEntries={20} /> <SpireActivityLog activityLog={activityLog} maxEntries={20} />
</main> </main>
</div> </div>
+138 -99
View File
@@ -45,7 +45,6 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
const totalFloors = Math.min(maxFloor, 100); const totalFloors = Math.min(maxFloor, 100);
const clearedSet = new Set(Object.entries(clearedFloors).filter(([, v]) => v).map(([k]) => Number(k))); 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[][] = []; const rows: number[][] = [];
for (let i = 0; i < totalFloors; i += 10) { for (let i = 0; i < totalFloors; i += 10) {
rows.push(Array.from({ length: 10 }, (_, j) => i + j + 1).filter((f) => f <= totalFloors)); rows.push(Array.from({ length: 10 }, (_, j) => i + j + 1).filter((f) => f <= totalFloors));
@@ -88,6 +87,13 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
})} })}
</div> </div>
))} ))}
<FloorLegend />
</div>
);
}
function FloorLegend() {
return (
<div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500"> <div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-emerald-600/60 border border-gray-700" /> <div className="w-3 h-3 rounded bg-emerald-600/60 border border-gray-700" />
@@ -106,102 +112,47 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
<span>Current</span> <span>Current</span>
</div> </div>
</div> </div>
</div>
); );
} }
// ─── Main Component ─────────────────────────────────────────────────────────── // ─── Top Stats Row ───────────────────────────────────────────────────────────
export function SpireSummaryTab() { function TopStatsRow({ maxFloorReached, totalFloorsCleared, defeatedCount, insight }: {
const [mounted, setMounted] = useState(false); maxFloorReached: number;
totalFloorsCleared: number;
const { defeatedCount: number;
currentFloor, insight: number;
maxFloorReached, }) {
clearedFloors,
enterSpireMode,
} = useCombatStore(useShallow((s) => ({
currentFloor: s.currentFloor,
maxFloorReached: s.maxFloorReached,
clearedFloors: s.clearedFloors,
enterSpireMode: s.enterSpireMode,
})));
const { insight } = usePrestigeStore(useShallow((s) => ({
insight: s.insight,
})));
useEffect(() => {
setMounted(true);
}, []);
// Derived data
const defeatedGuardians = useMemo(() => {
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
}, [clearedFloors]);
const nextGuardian = useMemo(() => {
return GUARDIAN_FLOORS.find((floor) => !clearedFloors[floor]) || null;
}, [clearedFloors]);
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]);
if (!mounted) {
return ( return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading spire data
</div>
);
}
return (
<DebugName name="SpireSummaryTab">
<div className="space-y-4">
{/* ── Top Stats Row ─────────────────────────────────────────────── */}
<Card className="bg-gray-900/60 border-gray-700"> <Card className="bg-gray-900/60 border-gray-700">
<CardContent className="py-4"> <CardContent className="py-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center"> <StatCell value={maxFloorReached} label="Max Floor Reached" color="text-amber-400" />
<div className="text-2xl font-bold text-amber-400">{maxFloorReached}</div> <StatCell value={totalFloorsCleared} label="Floors Cleared" color="text-gray-200" />
<div className="text-xs text-gray-400 mt-0.5">Max Floor Reached</div> <StatCell value={defeatedCount} label="Guardians Defeated" color="text-emerald-400" />
</div> <StatCell value={fmt(insight)} label="Insight Earned" color="text-purple-400" />
<div className="text-center">
<div className="text-2xl font-bold text-gray-200">{totalFloorsCleared}</div>
<div className="text-xs text-gray-400 mt-0.5">Floors Cleared</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-emerald-400">{defeatedGuardians.length}</div>
<div className="text-xs text-gray-400 mt-0.5">Guardians Defeated</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-400">{fmt(insight)}</div>
<div className="text-xs text-gray-400 mt-0.5">Insight Earned</div>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
);
}
{/* ── Climb the Spire Button ────────────────────────────────────── */} function StatCell({ value, label, color }: { value: number | string; label: string; color: string }) {
<DebugName name="ClimbSpireButton"> return (
<Button <div className="text-center">
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-600 text-white" <div className={`text-2xl font-bold ${color}`}>{value}</div>
size="lg" <div className="text-xs text-gray-400 mt-0.5">{label}</div>
onClick={enterSpireMode} </div>
> );
<Mountain className="w-5 h-5 mr-2" /> }
Climb the Spire
</Button>
</DebugName>
{/* ── Next Guardian + Preparation ───────────────────────────────── */} // ── Next Guardian Card ──────────────────────────────────────────────────────
{nextGuardianData && nextGuardian && (
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 (
<Card className="bg-gray-900/60 border-amber-800/40"> <Card className="bg-gray-900/60 border-amber-800/40">
<SectionHeader <SectionHeader
title={`🛡️ Next Guardian — Floor ${nextGuardian}`} title={`🛡️ Next Guardian — Floor ${nextGuardian}`}
@@ -239,7 +190,18 @@ export function SpireSummaryTab() {
</div> </div>
</div> </div>
{/* Preparation recommendations */} <PreparationTips
counterElement={counterElement}
nextFloorElement={nextFloorElement}
hasHighArmor={!!(nextGuardianData.armor && nextGuardianData.armor > 0.15)}
/>
</CardContent>
</Card>
);
}
function PreparationTips({ counterElement, nextFloorElement, hasHighArmor }: { counterElement: string | null; nextFloorElement: string | null; hasHighArmor: boolean }) {
return (
<div className="bg-gray-800/50 rounded-lg p-3 space-y-2"> <div className="bg-gray-800/50 rounded-lg p-3 space-y-2">
<div className="text-xs font-medium text-gray-300">Recommended Preparation:</div> <div className="text-xs font-medium text-gray-300">Recommended Preparation:</div>
<div className="text-xs text-gray-400 space-y-1"> <div className="text-xs text-gray-400 space-y-1">
@@ -259,7 +221,7 @@ export function SpireSummaryTab() {
</span> </span>
</div> </div>
)} )}
{nextGuardianData.armor && nextGuardianData.armor > 0.15 && ( {hasHighArmor && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-red-400">🛡</span> <span className="text-red-400">🛡</span>
<span>High armor consider armor-piercing or raw damage spells</span> <span>High armor consider armor-piercing or raw damage spells</span>
@@ -271,22 +233,29 @@ export function SpireSummaryTab() {
</div> </div>
</div> </div>
</div> </div>
</CardContent> );
</Card> }
)}
{/* ── All Guardians List ────────────────────────────────────────── */} // ─── Guardian Roster ─────────────────────────────────────────────────────────
function GuardianRoster({ clearedFloors }: { clearedFloors: Record<number, boolean> }) {
return (
<Card className="bg-gray-900/60 border-gray-700"> <Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="🏛️ Guardian Roster" /> <SectionHeader title="🏛️ Guardian Roster" />
<CardContent className="pt-0"> <CardContent className="pt-0">
<div className="space-y-2"> <div className="space-y-2">
{GUARDIAN_FLOORS.map((floor) => { {GUARDIAN_FLOORS.map((floor) => (
const guardian = GUARDIANS[floor]; <GuardianRosterItem key={floor} floor={floor} guardian={GUARDIANS[floor]} isDefeated={!!clearedFloors[floor]} />
const isDefeated = clearedFloors[floor]; ))}
</div>
</CardContent>
</Card>
);
}
function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; guardian: (typeof GUARDIANS)[number]; isDefeated: boolean }) {
return ( return (
<div <div
key={floor}
className={`flex items-center justify-between p-2 rounded border ${ className={`flex items-center justify-between p-2 rounded border ${
isDefeated isDefeated
? 'bg-emerald-900/20 border-emerald-800/40' ? 'bg-emerald-900/20 border-emerald-800/40'
@@ -334,12 +303,82 @@ export function SpireSummaryTab() {
</div> </div>
</div> </div>
); );
})} }
</div>
</CardContent> // ─── Main Component ───────────────────────────────────────────────────────────
</Card>
export function SpireSummaryTab() {
const [mounted, setMounted] = useState(false);
const {
currentFloor,
maxFloorReached,
clearedFloors,
enterSpireMode,
} = useCombatStore(useShallow((s) => ({
currentFloor: s.currentFloor,
maxFloorReached: s.maxFloorReached,
clearedFloors: s.clearedFloors,
enterSpireMode: s.enterSpireMode,
})));
const { insight } = usePrestigeStore(useShallow((s) => ({
insight: s.insight,
})));
useEffect(() => {
setMounted(true);
}, []);
const defeatedGuardians = useMemo(() => {
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
}, [clearedFloors]);
const nextGuardian = useMemo(() => {
return GUARDIAN_FLOORS.find((floor) => !clearedFloors[floor]) || null;
}, [clearedFloors]);
const nextGuardianData = nextGuardian ? GUARDIANS[nextGuardian] : null;
const totalFloorsCleared = useMemo(() => {
return Object.values(clearedFloors).filter(Boolean).length;
}, [clearedFloors]);
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading spire data
</div>
);
}
return (
<DebugName name="SpireSummaryTab">
<div className="space-y-4">
<TopStatsRow
maxFloorReached={maxFloorReached}
totalFloorsCleared={totalFloorsCleared}
defeatedCount={defeatedGuardians.length}
insight={insight}
/>
<DebugName name="ClimbSpireButton">
<Button
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-600 text-white"
size="lg"
onClick={enterSpireMode}
>
<Mountain className="w-5 h-5 mr-2" />
Climb the Spire
</Button>
</DebugName>
{nextGuardianData && nextGuardian && (
<NextGuardianCard nextGuardian={nextGuardian} nextGuardianData={nextGuardianData} />
)}
<GuardianRoster clearedFloors={clearedFloors} />
{/* ── Floor Progress Map ────────────────────────────────────────── */}
<Card className="bg-gray-900/60 border-gray-700"> <Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="🗺️ Floor Progress" /> <SectionHeader title="🗺️ Floor Progress" />
<CardContent className="pt-0"> <CardContent className="pt-0">
@@ -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<GuardianCardProps> = 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<GuardianStatus, { label: string; color: string; bg: string }> = {
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 (
<Card
className={clsx(
'border transition-colors',
status === 'signed' && 'border-green-600/40',
status === 'defeated' && 'border-amber-600/40',
status === 'undefeated' && 'border-gray-700/60',
)}
>
<CardHeader className="pb-2">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<CardTitle className="text-sm flex items-center gap-2" style={{ color: elemColor }}>
<span>{elemSym}</span>
<span className="truncate">{guardian.name}</span>
</CardTitle>
<div className="text-xs text-gray-500 mt-0.5">Floor {floor} · {elemDef?.name ?? guardian.element}</div>
</div>
<Badge className={clsx('text-[10px] px-1.5 py-0 shrink-0', sc.bg, sc.color)}>
{sc.label}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
<GuardianStats guardian={guardian} />
<GuardianBoons guardian={guardian} />
<div className="text-xs text-gray-400">
<span className="text-gray-500">Perk:</span> {guardian.uniquePerk}
</div>
<div className="flex items-center gap-3 text-xs text-gray-400">
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{ritualTime}h</span>
</div>
<div>
<span className="text-gray-500">Cost:</span> {guardian.pactCost.toLocaleString()} mana
</div>
</div>
{isRitualActive && (
<RitualProgress ritualProgress={ritualProgress} ritualTime={ritualTime} ritualComplete={ritualComplete} />
)}
{status === 'defeated' && !isRitualActive && (
<PactActionButton
canAfford={canAfford}
hasSlot={hasSlot}
onStartRitual={() => onStartRitual(floor)}
/>
)}
</CardContent>
</Card>
);
});
GuardianCard.displayName = 'GuardianCard';
// ─── Guardian Stats ──────────────────────────────────────────────────────────
function GuardianStats({ guardian }: { guardian: GuardianDef }) {
return (
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="flex items-center gap-1 text-gray-400">
<Shield className="w-3 h-3" />
<span>HP: {guardian.hp.toLocaleString()}</span>
</div>
<div className="flex items-center gap-1 text-gray-400">
<Swords className="w-3 h-3" />
<span>PWR: {guardian.power.toLocaleString()}</span>
</div>
<div className="flex items-center gap-1 text-gray-400">
<Shield className="w-3 h-3" />
<span>ARM: {Math.round((guardian.armor ?? 0) * 100)}%</span>
</div>
</div>
);
}
// ─── Guardian Boons ──────────────────────────────────────────────────────────
function GuardianBoons({ guardian }: { guardian: GuardianDef }) {
return (
<div className="space-y-1">
<div className="text-xs font-medium text-gray-300 flex items-center gap-1">
<Sparkles className="w-3 h-3" /> Boons
</div>
<div className="flex flex-wrap gap-1">
{guardian.boons.map((boon: GuardianBoon, i: number) => (
<span
key={i}
className="px-1.5 py-0.5 text-[10px] rounded border border-gray-600/50 text-gray-300"
>
{boon.desc}
</span>
))}
</div>
</div>
);
}
// ─── Ritual Progress ─────────────────────────────────────────────────────────
function RitualProgress({ ritualProgress, ritualTime, ritualComplete }: { ritualProgress: number; ritualTime: number; ritualComplete: boolean }) {
return (
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-amber-400">Ritual in progress</span>
<span className="text-gray-400">{ritualProgress}/{ritualTime}h</span>
</div>
<div className="w-full bg-gray-700 rounded-full h-1.5">
<div
className="bg-amber-500 h-1.5 rounded-full transition-all"
style={{ width: `${Math.min(100, (ritualProgress / ritualTime) * 100)}%` }}
/>
</div>
{ritualComplete && (
<div className="text-xs text-green-400 flex items-center gap-1">
<Check className="w-3 h-3" /> Ritual complete pact will be signed on next tick
</div>
)}
</div>
);
}
// ─── Pact Action Button ──────────────────────────────────────────────────────
function PactActionButton({ canAfford, hasSlot, onStartRitual }: { canAfford: boolean; hasSlot: boolean; onStartRitual: () => void }) {
const disabled = !canAfford || !hasSlot;
return (
<button
onClick={onStartRitual}
disabled={disabled}
className={clsx(
'w-full rounded px-3 py-1.5 text-xs font-medium transition-colors flex items-center justify-center gap-1',
!disabled
? 'bg-amber-600/80 text-white hover:bg-amber-500'
: 'bg-gray-700 text-gray-500 cursor-not-allowed',
)}
>
{!canAfford ? (
<><Lock className="w-3 h-3" /> Not enough mana</>
) : !hasSlot ? (
<><Lock className="w-3 h-3" /> No pact slots</>
) : (
<><ChevronRight className="w-3 h-3" /> Begin Pact Ritual</>
)}
</button>
);
}
// ─── Pact Header Summary ────────────────────────────────────────────────────
export function PactHeaderSummary({
signedCount,
pactSlots,
defeatedCount,
cumulativeBoons,
}: {
signedCount: number;
pactSlots: number;
defeatedCount: number;
cumulativeBoons: Record<string, number>;
}) {
return (
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
<CardContent className="py-3">
<div className="flex flex-wrap items-center gap-4 text-xs">
<div className="flex items-center gap-1.5">
<Shield className="w-3.5 h-3.5 text-amber-400" />
<span className="text-gray-400">Pact Slots:</span>
<span className="text-gray-200">{signedCount} / {pactSlots}</span>
</div>
<div className="flex items-center gap-1.5">
<Check className="w-3.5 h-3.5 text-green-400" />
<span className="text-gray-400">Signed:</span>
<span className="text-green-400">{signedCount}</span>
</div>
<div className="flex items-center gap-1.5">
<Swords className="w-3.5 h-3.5 text-red-400" />
<span className="text-gray-400">Defeated:</span>
<span className="text-red-400">{defeatedCount}</span>
</div>
</div>
{signedCount > 0 && (
<div className="mt-2 pt-2 border-t border-gray-700/50">
<div className="text-xs text-gray-400 mb-1">Active Boon Effects:</div>
<div className="flex flex-wrap gap-1">
{Object.entries(cumulativeBoons).map(([type, value]) => (
<span
key={type}
className="px-1.5 py-0.5 text-[10px] rounded border border-green-600/30 text-green-300 bg-green-900/20"
>
{type}: +{value}
</span>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}
// ─── Tier Filter ─────────────────────────────────────────────────────────────
export function TierFilter({
tiers,
activeTier,
guardianFloors,
onSelectTier,
}: {
tiers: FloorTier[];
activeTier: string;
guardianFloors: number[];
onSelectTier: (tier: string) => void;
}) {
return (
<div className="flex gap-2 flex-wrap">
<button
onClick={() => 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})
</button>
{tiers.map((tier) => (
<button
key={tier.label}
onClick={() => 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})
</button>
))}
</div>
);
}