refactor: extract sub-components from monster functions (issue #99)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
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:
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+104
-240
@@ -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) {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ─── Derived Stats Hook ──────────────────────────────────────────────────────
|
||||||
// Main Game Component
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export default function ManaLoopGame() {
|
function useGameDerivedStats() {
|
||||||
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
const { prestigeUpgrades } = usePrestigeStore(useShallow(s => ({
|
||||||
const [activeTab, setActiveTab] = useState('spells');
|
prestigeUpgrades: s.prestigeUpgrades,
|
||||||
|
})));
|
||||||
// ALL hooks must be called before any conditional returns
|
const { meditateTicks } = useManaStore(useShallow(s => ({
|
||||||
useGameLoop();
|
meditateTicks: s.meditateTicks,
|
||||||
|
})));
|
||||||
// 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,30 +91,80 @@ 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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tab Triggers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function TabTriggers() {
|
||||||
|
return (
|
||||||
|
<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="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
||||||
|
<TabsTrigger value="disciplines" className="text-xs px-2 py-1">📚 Disciplines</TabsTrigger>
|
||||||
|
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
||||||
|
<TabsTrigger value="debug" className="text-xs px-2 py-1">🐛 Debug</TabsTrigger>
|
||||||
|
<TabsTrigger value="attunements" className="text-xs px-2 py-1">⚗️ Attunements</TabsTrigger>
|
||||||
|
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achievements</TabsTrigger>
|
||||||
|
<TabsTrigger value="prestige" className="text-xs px-2 py-1">✨ Prestige</TabsTrigger>
|
||||||
|
<TabsTrigger value="equipment" className="text-xs px-2 py-1">⚔️ Equipment</TabsTrigger>
|
||||||
|
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golemancy</TabsTrigger>
|
||||||
|
<TabsTrigger value="pacts" className="text-xs px-2 py-1">📜 Pacts</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>
|
||||||
|
</TabsList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Lazy Tab Content ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function LazyTab({ name, children }: { name: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary fallback={<TabErrorFallback name={name} />}>
|
||||||
|
<Suspense fallback={<TabFallback />}>
|
||||||
|
{children}
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main Game Component ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function ManaLoopGame() {
|
||||||
|
const [activeTab, setActiveTab] = useState('spells');
|
||||||
|
|
||||||
|
useGameLoop();
|
||||||
|
|
||||||
|
const { day, hour, initGame } = useGameStore(useShallow(s => ({
|
||||||
|
day: s.day,
|
||||||
|
hour: s.hour,
|
||||||
|
initGame: s.initGame,
|
||||||
|
})));
|
||||||
|
const { insight, loopInsight } = usePrestigeStore(useShallow(s => ({
|
||||||
|
insight: s.insight,
|
||||||
|
loopInsight: s.loopInsight,
|
||||||
|
})));
|
||||||
|
const spireMode = useCombatStore((s) => s.spireMode);
|
||||||
|
const gameOver = useUIStore((s) => s.gameOver);
|
||||||
|
|
||||||
|
useGameDerivedStats();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initGame();
|
initGame();
|
||||||
}, [initGame]);
|
}, [initGame]);
|
||||||
@@ -203,21 +172,18 @@ export default function ManaLoopGame() {
|
|||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
useEffect(() => { setMounted(true); }, []); // eslint-disable-line react-hooks/set-state-in-effect
|
useEffect(() => { setMounted(true); }, []); // eslint-disable-line react-hooks/set-state-in-effect
|
||||||
|
|
||||||
// React to spireMode changes from combat store
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (spireMode) {
|
if (spireMode) {
|
||||||
setActiveTab('spells'); // eslint-disable-line react-hooks/set-state-in-effect
|
setActiveTab('spells'); // eslint-disable-line react-hooks/set-state-in-effect
|
||||||
}
|
}
|
||||||
}, [spireMode]);
|
}, [spireMode]);
|
||||||
|
|
||||||
// Conditional returns AFTER all hooks
|
|
||||||
if (gameOver) {
|
if (gameOver) {
|
||||||
return <GameOverScreen day={day} hour={hour} insightGained={loopInsight} totalInsight={insight} />;
|
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 (!mounted) return <div className="p-4 text-center text-gray-400">Loading...</div>;
|
||||||
|
|
||||||
// Spire mode: full-page replacement view
|
|
||||||
if (spireMode) {
|
if (spireMode) {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
@@ -232,7 +198,6 @@ export default function ManaLoopGame() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="game-root min-h-screen flex flex-col">
|
<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">
|
<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">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
|
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
|
||||||
@@ -242,127 +207,26 @@ export default function ManaLoopGame() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
|
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
|
||||||
<LeftPanel />
|
<LeftPanel />
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
<TabTriggers />
|
||||||
<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="disciplines" className="text-xs px-2 py-1">📚 Disciplines</TabsTrigger>
|
|
||||||
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
|
||||||
<TabsTrigger value="debug" className="text-xs px-2 py-1">🐛 Debug</TabsTrigger>
|
|
||||||
<TabsTrigger value="attunements" className="text-xs px-2 py-1">⚗️ Attunements</TabsTrigger>
|
|
||||||
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achievements</TabsTrigger>
|
|
||||||
<TabsTrigger value="prestige" className="text-xs px-2 py-1">✨ Prestige</TabsTrigger>
|
|
||||||
<TabsTrigger value="equipment" className="text-xs px-2 py-1">⚔️ Equipment</TabsTrigger>
|
|
||||||
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golemancy</TabsTrigger>
|
|
||||||
<TabsTrigger value="pacts" className="text-xs px-2 py-1">📜 Pacts</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>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="spells">
|
<TabsContent value="spells"><LazyTab name="spells"><SpellsTab /></LazyTab></TabsContent>
|
||||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">spells 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>
|
||||||
<SpellsTab />
|
<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="stats">
|
<TabsContent value="equipment"><LazyTab name="equipment"><EquipmentTab /></LazyTab></TabsContent>
|
||||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">stats 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>
|
||||||
<StatsTab />
|
<TabsContent value="spire"><LazyTab name="spire"><SpireSummaryTab /></LazyTab></TabsContent>
|
||||||
</Suspense>
|
<TabsContent value="crafting"><LazyTab name="crafting"><CraftingTab /></LazyTab></TabsContent>
|
||||||
</ErrorBoundary>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="disciplines">
|
|
||||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">disciplines tab failed to load.</div>}>
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<DisciplinesTab />
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="grimoire">
|
|
||||||
<GrimoireTab />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="debug">
|
|
||||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">debug tab failed to load.</div>}>
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<DebugTab />
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="attunements">
|
|
||||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">attunements tab failed to load.</div>}>
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<AttunementsTab />
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="achievements">
|
|
||||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">achievements tab failed to load.</div>}>
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<AchievementsTab />
|
|
||||||
</Suspense>
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -9,22 +9,220 @@ 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';
|
||||||
|
|
||||||
|
// ─── Crafting Progress ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function CraftingProgress({ progress }: { progress: { blueprintId: string; progress: number; required: number; manaSpent: number } }) {
|
||||||
|
const recipe = CRAFTING_RECIPES[progress.blueprintId];
|
||||||
|
const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
Crafting: {recipe?.name}
|
||||||
|
</div>
|
||||||
|
<Progress value={(progress.progress / progress.required) * 100} className="h-3" />
|
||||||
|
<div className="flex justify-between text-xs text-gray-400">
|
||||||
|
<span>{progress.progress.toFixed(1)}h / {progress.required.toFixed(1)}h</span>
|
||||||
|
<span>Mana spent: {fmt(progress.manaSpent)}</span>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="outline" onClick={cancelEquipmentCrafting}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Blueprint Card ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function BlueprintCard({ bpId, lootInventory, rawMana, isCrafting }: {
|
||||||
|
bpId: string;
|
||||||
|
lootInventory: LootInventory;
|
||||||
|
rawMana: number;
|
||||||
|
isCrafting: boolean;
|
||||||
|
}) {
|
||||||
|
const recipe = CRAFTING_RECIPES[bpId];
|
||||||
|
if (!recipe) return null;
|
||||||
|
|
||||||
|
const { canCraft, missingMaterials } = canCraftRecipe(recipe, lootInventory.materials, rawMana);
|
||||||
|
const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity];
|
||||||
|
const startCraftingEquipment = useCraftingStore((s) => s.startCraftingEquipment);
|
||||||
|
const currentAction = useCombatStore((s) => s.currentAction);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="p-3 rounded border bg-gray-800/50"
|
||||||
|
style={{ borderColor: rarityStyle?.color }}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold" style={{ color: rarityStyle?.color }}>
|
||||||
|
{recipe.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 capitalize">{recipe.rarity}</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{recipe.equipmentTypeId ? 'Equipment' : 'Other'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-gray-400 mb-2">{recipe.description}</div>
|
||||||
|
|
||||||
|
<Separator className="bg-gray-700 my-2" />
|
||||||
|
|
||||||
|
<div className="text-xs space-y-1">
|
||||||
|
<div className="text-gray-500">Materials:</div>
|
||||||
|
{Object.entries(recipe.materials).map(([matId, amount]) => {
|
||||||
|
const available = lootInventory.materials[matId] || 0;
|
||||||
|
const matDrop = LOOT_DROPS[matId];
|
||||||
|
const hasEnough = available >= amount;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={matId} className="flex justify-between">
|
||||||
|
<span>{matDrop?.name || matId}</span>
|
||||||
|
<span className={hasEnough ? 'text-green-400' : 'text-red-400'}>
|
||||||
|
{available} / {amount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="flex justify-between mt-2">
|
||||||
|
<span>Mana Cost:</span>
|
||||||
|
<span className={rawMana >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
|
||||||
|
{fmt(recipe.manaCost)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Craft Time:</span>
|
||||||
|
<span>{recipe.craftTime}h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full mt-3"
|
||||||
|
size="sm"
|
||||||
|
disabled={!canCraft || isCrafting}
|
||||||
|
onClick={() => startCraftingEquipment(bpId)}
|
||||||
|
>
|
||||||
|
{canCraft ? 'Craft Equipment' : 'Missing Resources'}
|
||||||
|
</Button>
|
||||||
|
</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>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Material Card ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function MaterialCard({ matId, count }: { matId: string; count: number }) {
|
||||||
|
const drop = LOOT_DROPS[matId];
|
||||||
|
if (!drop) return null;
|
||||||
|
|
||||||
|
const rarityStyle = LOOT_RARITY_COLORS[drop.rarity];
|
||||||
|
const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="p-2 rounded border bg-gray-800/50 group relative"
|
||||||
|
style={{ borderColor: rarityStyle?.color }}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold" style={{ color: rarityStyle?.color }}>
|
||||||
|
{drop.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">x{count}</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||||
|
onClick={() => deleteMaterial(matId, count)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function EquipmentCrafter() {
|
export function EquipmentCrafter() {
|
||||||
const lootInventory = useCraftingStore((s) => s.lootInventory);
|
const lootInventory = useCraftingStore((s) => s.lootInventory);
|
||||||
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
const rawMana = useManaStore((s) => s.rawMana);
|
||||||
const currentAction = useCombatStore((s) => s.currentAction);
|
|
||||||
const startCraftingEquipment = useCraftingStore((s) => s.startCraftingEquipment);
|
|
||||||
const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
|
|
||||||
const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
{/* Blueprint Selection */}
|
|
||||||
<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">
|
||||||
@@ -34,166 +232,16 @@ export function EquipmentCrafter() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{equipmentCraftingProgress ? (
|
{equipmentCraftingProgress ? (
|
||||||
<div className="space-y-3">
|
<CraftingProgress progress={equipmentCraftingProgress} />
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
Crafting: {CRAFTING_RECIPES[equipmentCraftingProgress.blueprintId]?.name}
|
|
||||||
</div>
|
|
||||||
<Progress value={(equipmentCraftingProgress.progress / equipmentCraftingProgress.required) * 100} className="h-3" />
|
|
||||||
<div className="flex justify-between text-xs text-gray-400">
|
|
||||||
<span>{equipmentCraftingProgress.progress.toFixed(1)}h / {equipmentCraftingProgress.required.toFixed(1)}h</span>
|
|
||||||
<span>Mana spent: {fmt(equipmentCraftingProgress.manaSpent)}</span>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" variant="outline" onClick={cancelEquipmentCrafting}>Cancel</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<ScrollArea className="h-64">
|
<BlueprintList lootInventory={lootInventory} rawMana={rawMana} />
|
||||||
<div className="space-y-2">
|
|
||||||
{lootInventory.blueprints.length === 0 ? (
|
|
||||||
<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>
|
|
||||||
) : (
|
|
||||||
lootInventory.blueprints.map(bpId => {
|
|
||||||
const recipe = CRAFTING_RECIPES[bpId];
|
|
||||||
if (!recipe) return null;
|
|
||||||
|
|
||||||
const { canCraft, missingMaterials, missingMana } = canCraftRecipe(
|
|
||||||
recipe,
|
|
||||||
lootInventory.materials,
|
|
||||||
rawMana
|
|
||||||
);
|
|
||||||
|
|
||||||
const rarityStyle = LOOT_RARITY_COLORS[recipe.rarity];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={bpId}
|
|
||||||
className="p-3 rounded border bg-gray-800/50"
|
|
||||||
style={{ borderColor: rarityStyle?.color }}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start mb-2">
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold" style={{ color: rarityStyle?.color }}>
|
|
||||||
{recipe.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 capitalize">{recipe.rarity}</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{recipe.equipmentTypeId ? 'Equipment' : 'Other'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-xs text-gray-400 mb-2">{recipe.description}</div>
|
|
||||||
|
|
||||||
<Separator className="bg-gray-700 my-2" />
|
|
||||||
|
|
||||||
<div className="text-xs space-y-1">
|
|
||||||
<div className="text-gray-500">Materials:</div>
|
|
||||||
{Object.entries(recipe.materials).map(([matId, amount]) => {
|
|
||||||
const available = lootInventory.materials[matId] || 0;
|
|
||||||
const matDrop = LOOT_DROPS[matId];
|
|
||||||
const hasEnough = available >= amount;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={matId} className="flex justify-between">
|
|
||||||
<span>{matDrop?.name || matId}</span>
|
|
||||||
<span className={hasEnough ? 'text-green-400' : 'text-red-400'}>
|
|
||||||
{available} / {amount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div className="flex justify-between mt-2">
|
|
||||||
<span>Mana Cost:</span>
|
|
||||||
<span className={rawMana >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
|
|
||||||
{fmt(recipe.manaCost)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>Craft Time:</span>
|
|
||||||
<span>{recipe.craftTime}h</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="w-full mt-3"
|
|
||||||
size="sm"
|
|
||||||
disabled={!canCraft || currentAction === 'craft'}
|
|
||||||
onClick={() => startCraftingEquipment(bpId)}
|
|
||||||
>
|
|
||||||
{canCraft ? 'Craft Equipment' : 'Missing Resources'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Materials Inventory */}
|
<MaterialsInventory materials={lootInventory.materials} />
|
||||||
<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 ({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];
|
|
||||||
if (!drop) return null;
|
|
||||||
|
|
||||||
const rarityStyle = LOOT_RARITY_COLORS[drop.rarity];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={matId}
|
|
||||||
className="p-2 rounded border bg-gray-800/50 group relative"
|
|
||||||
style={{ borderColor: rarityStyle?.color }}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-semibold" style={{ color: rarityStyle?.color }}>
|
|
||||||
{drop.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400">x{count}</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
|
||||||
onClick={() => deleteMaterial(matId, count)}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EquipmentCrafter.displayName = "EquipmentCrafter";
|
EquipmentCrafter.displayName = 'EquipmentCrafter';
|
||||||
|
|||||||
@@ -6,18 +6,227 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import {
|
import {
|
||||||
RotateCcw, AlertTriangle, Zap, Clock, Settings, Eye,
|
RotateCcw, AlertTriangle, Zap, Clock, Settings, Eye,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useDebug } from '@/components/game/debug/debug-context';
|
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';
|
||||||
|
|
||||||
|
// ─── Warning Banner ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function WarningBanner() {
|
||||||
|
return (
|
||||||
|
<Card className="bg-amber-900/20 border-amber-600/50">
|
||||||
|
<CardContent className="pt-4">
|
||||||
|
<div className="flex items-center gap-2 text-amber-400">
|
||||||
|
<AlertTriangle className="w-5 h-5" />
|
||||||
|
<span className="font-semibold">Debug Mode</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-amber-300/70 mt-1">
|
||||||
|
These tools are for development and testing. Using them may break game balance or save data.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Display Options ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function DisplayOptions() {
|
||||||
|
const { showComponentNames, toggleComponentNames } = useDebug();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
Display Options
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="show-component-names" className="text-sm">Show Component Names</Label>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Display component names at the top of each component for debugging
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="show-component-names"
|
||||||
|
checked={showComponentNames}
|
||||||
|
onCheckedChange={toggleComponentNames}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Game Reset Section ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function GameResetSection({ confirmReset, onReset }: { confirmReset: boolean; onReset: () => void }) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-red-400 text-sm flex items-center gap-2">
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
Game Reset
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Reset all game progress and start fresh. This cannot be undone.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
className={`w-full ${confirmReset ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'}`}
|
||||||
|
onClick={onReset}
|
||||||
|
>
|
||||||
|
{confirmReset ? (
|
||||||
|
<>
|
||||||
|
<AlertTriangle className="w-4 h-4 mr-2" />
|
||||||
|
Click Again to Confirm Reset
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
Reset Game
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-blue-400 text-sm flex items-center gap-2">
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
Mana Debug
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-2">
|
||||||
|
Current: {rawMana} / {maxMana || '?'}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onAddMana(10)}>
|
||||||
|
<Zap className="w-3 h-3 mr-1" /> +10
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onAddMana(100)}>
|
||||||
|
<Zap className="w-3 h-3 mr-1" /> +100
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onAddMana(1000)}>
|
||||||
|
<Zap className="w-3 h-3 mr-1" /> +1K
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onAddMana(10000)}>
|
||||||
|
<Zap className="w-3 h-3 mr-1" /> +10K
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-gray-700" />
|
||||||
|
<div className="text-xs text-gray-400 mb-2">Fill to max:</div>
|
||||||
|
<Button size="sm" className="w-full bg-blue-600 hover:bg-blue-700" onClick={onFillMana}>
|
||||||
|
Fill Mana
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
Time Control
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
Current: Day {day}, Hour {hour}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onSetDay(1)}>Day 1</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onSetDay(10)}>Day 10</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onSetDay(20)}>Day 20</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onSetDay(30)}>Day 30</Button>
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-gray-700" />
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button size="sm" variant="outline" onClick={onTogglePause}>
|
||||||
|
{paused ? '▶ Resume' : '⏸ Pause'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
Quick Actions
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button size="sm" variant="outline" onClick={onUnlockBase}>
|
||||||
|
Unlock All Base Elements
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={onUnlockUtility}>
|
||||||
|
Unlock Utility Elements
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={onSkipToFloor}>
|
||||||
|
Skip to Floor 100
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={onResetFloorHP}>
|
||||||
|
Reset Floor HP
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function GameStateDebug() {
|
export function GameStateDebug() {
|
||||||
const [confirmReset, setConfirmReset] = useState(false);
|
const [confirmReset, setConfirmReset] = useState(false);
|
||||||
const { showComponentNames, toggleComponentNames } = useDebug();
|
const { showComponentNames, toggleComponentNames } = useDebug();
|
||||||
|
|
||||||
// Get state from modular stores
|
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
const rawMana = useManaStore((s) => s.rawMana);
|
||||||
const elements = useManaStore((s) => s.elements);
|
const elements = useManaStore((s) => s.elements);
|
||||||
const unlockElement = useManaStore((s) => s.unlockElement);
|
const unlockElement = useManaStore((s) => s.unlockElement);
|
||||||
@@ -26,8 +235,6 @@ export function GameStateDebug() {
|
|||||||
const hour = useGameStore((s) => s.hour);
|
const hour = useGameStore((s) => s.hour);
|
||||||
const paused = useUIStore((s) => s.paused);
|
const paused = useUIStore((s) => s.paused);
|
||||||
const togglePause = useUIStore((s) => s.togglePause);
|
const togglePause = useUIStore((s) => s.togglePause);
|
||||||
|
|
||||||
// Get actions from stores
|
|
||||||
const resetGame = useGameStore((s) => s.resetGame);
|
const resetGame = useGameStore((s) => s.resetGame);
|
||||||
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
|
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
|
||||||
const resetFloorHP = useCombatStore((s) => s.resetFloorHP);
|
const resetFloorHP = useCombatStore((s) => s.resetFloorHP);
|
||||||
@@ -48,222 +255,52 @@ export function GameStateDebug() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMaxMana = () => {
|
const handleFillMana = () => {
|
||||||
return computeMaxMana(
|
const maxMana = computeMaxMana(
|
||||||
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
|
{ 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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Warning Banner */}
|
<WarningBanner />
|
||||||
<Card className="bg-amber-900/20 border-amber-600/50">
|
<DisplayOptions />
|
||||||
<CardContent className="pt-4">
|
|
||||||
<div className="flex items-center gap-2 text-amber-400">
|
|
||||||
<AlertTriangle className="w-5 h-5" />
|
|
||||||
<span className="font-semibold">Debug Mode</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-amber-300/70 mt-1">
|
|
||||||
These tools are for development and testing. Using them may break game balance or save data.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Display Options */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
Display Options
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Label htmlFor="show-component-names" className="text-sm">Show Component Names</Label>
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Display component names at the top of each component for debugging
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="show-component-names"
|
|
||||||
checked={showComponentNames}
|
|
||||||
onCheckedChange={toggleComponentNames}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Game Reset */}
|
<GameResetSection confirmReset={confirmReset} onReset={handleReset} />
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
<ManaDebugSection rawMana={rawMana} onAddMana={handleAddMana} onFillMana={handleFillMana} />
|
||||||
<CardHeader className="pb-2">
|
<TimeControlSection day={day} hour={hour} paused={paused} onSetDay={handleSetDay} onTogglePause={togglePause} />
|
||||||
<CardTitle className="text-red-400 text-sm flex items-center gap-2">
|
<QuickActionsSection
|
||||||
<RotateCcw className="w-4 h-4" />
|
elements={elements}
|
||||||
Game Reset
|
onUnlockBase={handleUnlockBase}
|
||||||
</CardTitle>
|
onUnlockUtility={handleUnlockUtility}
|
||||||
</CardHeader>
|
onSkipToFloor={() => debugSetFloor?.(100)}
|
||||||
<CardContent className="space-y-3">
|
onResetFloorHP={() => resetFloorHP?.()}
|
||||||
<p className="text-xs text-gray-400">
|
/>
|
||||||
Reset all game progress and start fresh. This cannot be undone.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
className={`w-full ${confirmReset ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'}`}
|
|
||||||
onClick={handleReset}
|
|
||||||
>
|
|
||||||
{confirmReset ? (
|
|
||||||
<>
|
|
||||||
<AlertTriangle className="w-4 h-4 mr-2" />
|
|
||||||
Click Again to Confirm Reset
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
|
||||||
Reset Game
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Mana Debug */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-blue-400 text-sm flex items-center gap-2">
|
|
||||||
<Zap className="w-4 h-4" />
|
|
||||||
Mana Debug
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<div className="text-xs text-gray-400 mb-2">
|
|
||||||
Current: {rawMana} / {getMaxMana() || '?'}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(10)}>
|
|
||||||
<Zap className="w-3 h-3 mr-1" /> +10
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(100)}>
|
|
||||||
<Zap className="w-3 h-3 mr-1" /> +100
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(1000)}>
|
|
||||||
<Zap className="w-3 h-3 mr-1" /> +1K
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(10000)}>
|
|
||||||
<Zap className="w-3 h-3 mr-1" /> +10K
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Separator className="bg-gray-700" />
|
|
||||||
<div className="text-xs text-gray-400 mb-2">Fill to max:</div>
|
|
||||||
<Button
|
|
||||||
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
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Time Control */}
|
|
||||||
<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">
|
|
||||||
<Clock className="w-4 h-4" />
|
|
||||||
Time Control
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
Current: Day {day}, Hour {hour}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 1, hour: 0 })}>
|
|
||||||
Day 1
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 10, hour: 0 })}>
|
|
||||||
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>
|
|
||||||
<Separator className="bg-gray-700" />
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={togglePause}
|
|
||||||
>
|
|
||||||
{paused ? '▶ Resume' : '⏸ Pause'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Skills Debug - Quick Actions */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700 md:col-span-2">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
|
||||||
<Settings className="w-4 h-4" />
|
|
||||||
Quick Actions
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button
|
|
||||||
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
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
// Unlock utility elements
|
|
||||||
['transference'].forEach(e => {
|
|
||||||
if (!elements[e]?.unlocked) {
|
|
||||||
unlockElement(e, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Unlock Utility Elements
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => debugSetFloor?.(100)}
|
|
||||||
>
|
|
||||||
Skip to Floor 100
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => resetFloorHP?.()}
|
|
||||||
>
|
|
||||||
Reset Floor HP
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
GameStateDebug.displayName = "GameStateDebug";
|
GameStateDebug.displayName = 'GameStateDebug';
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|
||||||
// Get all guardian floors
|
|
||||||
const guardianFloors = Object.keys(GUARDIANS || {}).map(Number).sort((a, b) => a - b);
|
|
||||||
|
|
||||||
// Force sign a pact with a guardian (bypass costs and time)
|
const addLog = useUIStore((s) => s.addLog);
|
||||||
|
|
||||||
const forcePact = (floor: number) => {
|
const 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([]);
|
||||||
@@ -96,74 +151,23 @@ export function PactDebug() {
|
|||||||
<p className="text-xs text-gray-400 mb-2">
|
<p className="text-xs text-gray-400 mb-2">
|
||||||
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">
|
|
||||||
{guardianFloors.map((floor) => {
|
|
||||||
const guardian = GUARDIANS[floor];
|
|
||||||
const isSigned = signedPacts.includes(floor);
|
|
||||||
|
|
||||||
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 */}
|
<GuardianPactList
|
||||||
|
signedPacts={signedPacts}
|
||||||
|
onForceSign={forcePact}
|
||||||
|
onRemove={removePactHandler}
|
||||||
|
/>
|
||||||
|
|
||||||
{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)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,4 +176,4 @@ export function PactDebug() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PactDebug.displayName = "PactDebug";
|
PactDebug.displayName = 'PactDebug';
|
||||||
|
|||||||
@@ -13,6 +13,194 @@ 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';
|
||||||
|
|
||||||
|
// ─── Display Options ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function DisplayOptions() {
|
||||||
|
const { showComponentNames, toggleComponentNames } = useDebug();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
Display Options
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="show-component-names" className="text-sm">Show Component Names</Label>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Display component names at the top of each component for debugging
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="show-component-names"
|
||||||
|
checked={showComponentNames}
|
||||||
|
onCheckedChange={toggleComponentNames}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Game Reset Section ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function GameResetSection({ confirmReset, onReset }: { confirmReset: boolean; onReset: () => void }) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-red-400 text-sm flex items-center gap-2">
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
Game Reset
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
Reset all game progress and start fresh. This cannot be undone.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
className={`w-full ${confirmReset ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'}`}
|
||||||
|
onClick={onReset}
|
||||||
|
>
|
||||||
|
{confirmReset ? (
|
||||||
|
<>
|
||||||
|
<AlertTriangle className="w-4 h-4 mr-2" />
|
||||||
|
Click Again to Confirm Reset
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
Reset Game
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-blue-400 text-sm flex items-center gap-2">
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
Mana Debug
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-2">
|
||||||
|
Current: {rawMana} / {maxMana || '?'}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onAddMana(10)}>
|
||||||
|
<Zap className="w-3 h-3 mr-1" /> +10
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onAddMana(100)}>
|
||||||
|
<Zap className="w-3 h-3 mr-1" /> +100
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onAddMana(1000)}>
|
||||||
|
<Zap className="w-3 h-3 mr-1" /> +1K
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onAddMana(10000)}>
|
||||||
|
<Zap className="w-3 h-3 mr-1" /> +10K
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-gray-700" />
|
||||||
|
<div className="text-xs text-gray-400 mb-2">Fill to max:</div>
|
||||||
|
<Button size="sm" className="w-full bg-blue-600 hover:bg-blue-700" onClick={onFillMana}>
|
||||||
|
Fill Mana
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
Time Control
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
Current: Day {day}, Hour {hour}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onSetDay(1)}>Day 1</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onSetDay(10)}>Day 10</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onSetDay(20)}>Day 20</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => onSetDay(30)}>Day 30</Button>
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-gray-700" />
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button size="sm" variant="outline" onClick={onTogglePause}>
|
||||||
|
{paused ? '▶ Resume' : '⏸ Pause'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
Quick Actions
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<Button size="sm" variant="outline" onClick={onUnlockBase}>
|
||||||
|
Unlock All Base Elements
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={onSkipToFloor}>
|
||||||
|
Skip to Floor 100
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={onResetFloorHP}>
|
||||||
|
Reset Floor HP
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function GameStateDebugSection() {
|
export function GameStateDebugSection() {
|
||||||
const [confirmReset, setConfirmReset] = useState(false);
|
const [confirmReset, setConfirmReset] = useState(false);
|
||||||
const { showComponentNames, toggleComponentNames } = useDebug();
|
const { showComponentNames, toggleComponentNames } = useDebug();
|
||||||
@@ -22,7 +210,6 @@ export function GameStateDebugSection() {
|
|||||||
const hour = useGameStore((s) => s.hour);
|
const hour = useGameStore((s) => s.hour);
|
||||||
const paused = useUIStore((s) => s.paused);
|
const paused = useUIStore((s) => s.paused);
|
||||||
const togglePause = useUIStore((s) => s.togglePause);
|
const togglePause = useUIStore((s) => s.togglePause);
|
||||||
|
|
||||||
const resetGame = useGameStore((s) => s.resetGame);
|
const resetGame = useGameStore((s) => s.resetGame);
|
||||||
const gatherMana = useGameStore((s) => s.gatherMana);
|
const gatherMana = useGameStore((s) => s.gatherMana);
|
||||||
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
|
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
|
||||||
@@ -46,190 +233,42 @@ export function GameStateDebugSection() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMaxMana = () => {
|
const handleFillMana = () => {
|
||||||
return computeMaxMana(
|
const maxMana = computeMaxMana(
|
||||||
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
|
{ 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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Display Options */}
|
<DisplayOptions />
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
Display Options
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Label htmlFor="show-component-names" className="text-sm">Show Component Names</Label>
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Display component names at the top of each component for debugging
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="show-component-names"
|
|
||||||
checked={showComponentNames}
|
|
||||||
onCheckedChange={toggleComponentNames}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Game Reset */}
|
<GameResetSection confirmReset={confirmReset} onReset={handleReset} />
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
<ManaDebugSection rawMana={rawMana} onAddMana={handleAddMana} onFillMana={handleFillMana} />
|
||||||
<CardHeader className="pb-2">
|
<TimeControlSection day={day} hour={hour} paused={paused} onSetDay={handleSetDay} onTogglePause={togglePause} />
|
||||||
<CardTitle className="text-red-400 text-sm flex items-center gap-2">
|
<QuickActionsSection
|
||||||
<RotateCcw className="w-4 h-4" />
|
elements={elements}
|
||||||
Game Reset
|
onUnlockBase={handleUnlockBase}
|
||||||
</CardTitle>
|
onSkipToFloor={() => debugSetFloor?.(100)}
|
||||||
</CardHeader>
|
onResetFloorHP={() => resetFloorHP?.()}
|
||||||
<CardContent className="space-y-3">
|
/>
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Reset all game progress and start fresh. This cannot be undone.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
className={`w-full ${confirmReset ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'}`}
|
|
||||||
onClick={handleReset}
|
|
||||||
>
|
|
||||||
{confirmReset ? (
|
|
||||||
<>
|
|
||||||
<AlertTriangle className="w-4 h-4 mr-2" />
|
|
||||||
Click Again to Confirm Reset
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
|
||||||
Reset Game
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Mana Debug */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-blue-400 text-sm flex items-center gap-2">
|
|
||||||
<Zap className="w-4 h-4" />
|
|
||||||
Mana Debug
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<div className="text-xs text-gray-400 mb-2">
|
|
||||||
Current: {rawMana} / {getMaxMana() || '?'}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(10)}>
|
|
||||||
<Zap className="w-3 h-3 mr-1" /> +10
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(100)}>
|
|
||||||
<Zap className="w-3 h-3 mr-1" /> +100
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(1000)}>
|
|
||||||
<Zap className="w-3 h-3 mr-1" /> +1K
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(10000)}>
|
|
||||||
<Zap className="w-3 h-3 mr-1" /> +10K
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Separator className="bg-gray-700" />
|
|
||||||
<div className="text-xs text-gray-400 mb-2">Fill to max:</div>
|
|
||||||
<Button
|
|
||||||
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
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Time Control */}
|
|
||||||
<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">
|
|
||||||
<Clock className="w-4 h-4" />
|
|
||||||
Time Control
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
Current: Day {day}, Hour {hour}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 1, hour: 0 })}>
|
|
||||||
Day 1
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => useGameStore.setState({ day: 10, hour: 0 })}>
|
|
||||||
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>
|
|
||||||
<Separator className="bg-gray-700" />
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button size="sm" variant="outline" onClick={togglePause}>
|
|
||||||
{paused ? '▶ Resume' : '⏸ Pause'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
|
||||||
<Zap className="w-4 h-4" />
|
|
||||||
Quick Actions
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button
|
|
||||||
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
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => debugSetFloor?.(100)}
|
|
||||||
>
|
|
||||||
Skip to Floor 100
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => resetFloorHP?.()}
|
|
||||||
>
|
|
||||||
Reset Floor HP
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</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);
|
key={floor}
|
||||||
|
floor={floor}
|
||||||
return (
|
isSigned={signedPacts.includes(floor)}
|
||||||
<div
|
onForceSign={() => forcePact(floor)}
|
||||||
key={floor}
|
onRemove={() => removePactHandler(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>
|
</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';
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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'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>
|
||||||
|
|||||||
@@ -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,23 +87,219 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500">
|
<FloorLegend />
|
||||||
<div className="flex items-center gap-1">
|
</div>
|
||||||
<div className="w-3 h-3 rounded bg-emerald-600/60 border border-gray-700" />
|
);
|
||||||
<span>Cleared</span>
|
}
|
||||||
|
|
||||||
|
function FloorLegend() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-emerald-600/60 border border-gray-700" />
|
||||||
|
<span>Cleared</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-gray-800 border border-gray-700" />
|
||||||
|
<span>Uncleared</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-gray-800 border border-amber-500" />
|
||||||
|
<span>Guardian</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-amber-600/60 border border-amber-400" />
|
||||||
|
<span>Current</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Top Stats Row ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function TopStatsRow({ maxFloorReached, totalFloorsCleared, defeatedCount, insight }: {
|
||||||
|
maxFloorReached: number;
|
||||||
|
totalFloorsCleared: number;
|
||||||
|
defeatedCount: number;
|
||||||
|
insight: 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">
|
||||||
|
<StatCell value={maxFloorReached} label="Max Floor Reached" color="text-amber-400" />
|
||||||
|
<StatCell value={totalFloorsCleared} label="Floors Cleared" color="text-gray-200" />
|
||||||
|
<StatCell value={defeatedCount} label="Guardians Defeated" color="text-emerald-400" />
|
||||||
|
<StatCell value={fmt(insight)} label="Insight Earned" color="text-purple-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
</CardContent>
|
||||||
<div className="w-3 h-3 rounded bg-gray-800 border border-gray-700" />
|
</Card>
|
||||||
<span>Uncleared</span>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCell({ value, label, color }: { value: number | string; 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Next Guardian Card ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function NextGuardianCard({ nextGuardian, nextGuardianData }: { nextGuardian: number; nextGuardianData: (typeof GUARDIANS)[number] }) {
|
||||||
|
const counterElement = getCounterElement(nextGuardianData.element);
|
||||||
|
const nextFloorElement = FLOOR_ELEM_CYCLE[(nextGuardian - 1) % FLOOR_ELEM_CYCLE.length];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/60 border-amber-800/40">
|
||||||
|
<SectionHeader
|
||||||
|
title={`🛡️ Next Guardian — Floor ${nextGuardian}`}
|
||||||
|
className="text-amber-400"
|
||||||
|
/>
|
||||||
|
<CardContent className="pt-0 space-y-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="w-10 h-10 rounded-full flex items-center justify-center text-lg font-bold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${nextGuardianData.color}20`,
|
||||||
|
border: `2px solid ${nextGuardianData.color}`,
|
||||||
|
color: nextGuardianData.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nextGuardian}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-100">{nextGuardianData.name}</div>
|
||||||
|
<div className="flex items-center gap-2 mt-0.5">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs"
|
||||||
|
style={{ borderColor: getElementColor(nextGuardianData.element), color: getElementColor(nextGuardianData.element) }}
|
||||||
|
>
|
||||||
|
{nextGuardianData.element}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-gray-500">HP: {fmt(nextGuardianData.hp)}</span>
|
||||||
|
{nextGuardianData.armor && (
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
Armor: {Math.round(nextGuardianData.armor * 100)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-3 h-3 rounded bg-gray-800 border border-amber-500" />
|
<PreparationTips
|
||||||
<span>Guardian</span>
|
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="text-xs font-medium text-gray-300">Recommended Preparation:</div>
|
||||||
|
<div className="text-xs text-gray-400 space-y-1">
|
||||||
|
{counterElement && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-emerald-400">⚡</span>
|
||||||
|
<span>
|
||||||
|
Use <span style={{ color: getElementColor(counterElement) }} className="font-medium">{counterElement}</span> spells for super effective damage (+50%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{nextFloorElement && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-blue-400">🔄</span>
|
||||||
|
<span>
|
||||||
|
Floor element: <span style={{ color: getElementColor(nextFloorElement) }} className="font-medium">{nextFloorElement}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasHighArmor && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-red-400">🛡️</span>
|
||||||
|
<span>High armor — consider armor-piercing or raw damage spells</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-amber-400">💡</span>
|
||||||
|
<span>Ensure mana pools are full before attempting</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
</div>
|
||||||
<div className="w-3 h-3 rounded bg-amber-600/60 border border-amber-400" />
|
</div>
|
||||||
<span>Current</span>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Guardian Roster ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function GuardianRoster({ clearedFloors }: { clearedFloors: Record<number, boolean> }) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/60 border-gray-700">
|
||||||
|
<SectionHeader title="🏛️ Guardian Roster" />
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{GUARDIAN_FLOORS.map((floor) => (
|
||||||
|
<GuardianRosterItem key={floor} floor={floor} guardian={GUARDIANS[floor]} isDefeated={!!clearedFloors[floor]} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; guardian: (typeof GUARDIANS)[number]; isDefeated: boolean }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between p-2 rounded border ${
|
||||||
|
isDefeated
|
||||||
|
? 'bg-emerald-900/20 border-emerald-800/40'
|
||||||
|
: 'bg-gray-800/40 border-gray-700/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="w-7 h-7 rounded flex items-center justify-center text-xs font-bold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: isDefeated ? `${guardian.color}30` : '#374151',
|
||||||
|
color: isDefeated ? guardian.color : '#6B7280',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{floor}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={`text-sm font-medium ${isDefeated ? 'text-gray-100' : 'text-gray-400'}`}>
|
||||||
|
{guardian.name}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="text-[10px] px-1.5 py-0.5 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${guardian.color}15`,
|
||||||
|
color: guardian.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{guardian.element}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-gray-500">HP: {fmt(guardian.hp)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isDefeated ? (
|
||||||
|
<Badge variant="outline" className="border-emerald-600 text-emerald-400 text-xs">
|
||||||
|
✓ Defeated
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="border-gray-600 text-gray-500 text-xs">
|
||||||
|
Undefeated
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -135,7 +330,6 @@ export function SpireSummaryTab() {
|
|||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Derived data
|
|
||||||
const defeatedGuardians = useMemo(() => {
|
const defeatedGuardians = useMemo(() => {
|
||||||
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
|
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
|
||||||
}, [clearedFloors]);
|
}, [clearedFloors]);
|
||||||
@@ -146,9 +340,6 @@ export function SpireSummaryTab() {
|
|||||||
|
|
||||||
const nextGuardianData = nextGuardian ? GUARDIANS[nextGuardian] : null;
|
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(() => {
|
const totalFloorsCleared = useMemo(() => {
|
||||||
return Object.values(clearedFloors).filter(Boolean).length;
|
return Object.values(clearedFloors).filter(Boolean).length;
|
||||||
}, [clearedFloors]);
|
}, [clearedFloors]);
|
||||||
@@ -164,31 +355,13 @@ export function SpireSummaryTab() {
|
|||||||
return (
|
return (
|
||||||
<DebugName name="SpireSummaryTab">
|
<DebugName name="SpireSummaryTab">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* ── Top Stats Row ─────────────────────────────────────────────── */}
|
<TopStatsRow
|
||||||
<Card className="bg-gray-900/60 border-gray-700">
|
maxFloorReached={maxFloorReached}
|
||||||
<CardContent className="py-4">
|
totalFloorsCleared={totalFloorsCleared}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
defeatedCount={defeatedGuardians.length}
|
||||||
<div className="text-center">
|
insight={insight}
|
||||||
<div className="text-2xl font-bold text-amber-400">{maxFloorReached}</div>
|
/>
|
||||||
<div className="text-xs text-gray-400 mt-0.5">Max Floor Reached</div>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* ── Climb the Spire Button ────────────────────────────────────── */}
|
|
||||||
<DebugName name="ClimbSpireButton">
|
<DebugName name="ClimbSpireButton">
|
||||||
<Button
|
<Button
|
||||||
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-600 text-white"
|
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-600 text-white"
|
||||||
@@ -200,146 +373,12 @@ export function SpireSummaryTab() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DebugName>
|
</DebugName>
|
||||||
|
|
||||||
{/* ── Next Guardian + Preparation ───────────────────────────────── */}
|
|
||||||
{nextGuardianData && nextGuardian && (
|
{nextGuardianData && nextGuardian && (
|
||||||
<Card className="bg-gray-900/60 border-amber-800/40">
|
<NextGuardianCard nextGuardian={nextGuardian} nextGuardianData={nextGuardianData} />
|
||||||
<SectionHeader
|
|
||||||
title={`🛡️ Next Guardian — Floor ${nextGuardian}`}
|
|
||||||
className="text-amber-400"
|
|
||||||
/>
|
|
||||||
<CardContent className="pt-0 space-y-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center text-lg font-bold"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${nextGuardianData.color}20`,
|
|
||||||
border: `2px solid ${nextGuardianData.color}`,
|
|
||||||
color: nextGuardianData.color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{nextGuardian}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold text-gray-100">{nextGuardianData.name}</div>
|
|
||||||
<div className="flex items-center gap-2 mt-0.5">
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs"
|
|
||||||
style={{ borderColor: getElementColor(nextGuardianData.element), color: getElementColor(nextGuardianData.element) }}
|
|
||||||
>
|
|
||||||
{nextGuardianData.element}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-xs text-gray-500">HP: {fmt(nextGuardianData.hp)}</span>
|
|
||||||
{nextGuardianData.armor && (
|
|
||||||
<span className="text-xs text-gray-500">
|
|
||||||
Armor: {Math.round(nextGuardianData.armor * 100)}%
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Preparation recommendations */}
|
|
||||||
<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 text-gray-400 space-y-1">
|
|
||||||
{counterElement && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-emerald-400">⚡</span>
|
|
||||||
<span>
|
|
||||||
Use <span style={{ color: getElementColor(counterElement) }} className="font-medium">{counterElement}</span> spells for super effective damage (+50%)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{nextFloorElement && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-blue-400">🔄</span>
|
|
||||||
<span>
|
|
||||||
Floor element: <span style={{ color: getElementColor(nextFloorElement) }} className="font-medium">{nextFloorElement}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{nextGuardianData.armor && nextGuardianData.armor > 0.15 && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-red-400">🛡️</span>
|
|
||||||
<span>High armor — consider armor-piercing or raw damage spells</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-amber-400">💡</span>
|
|
||||||
<span>Ensure mana pools are full before attempting</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── All Guardians List ────────────────────────────────────────── */}
|
<GuardianRoster clearedFloors={clearedFloors} />
|
||||||
<Card className="bg-gray-900/60 border-gray-700">
|
|
||||||
<SectionHeader title="🏛️ Guardian Roster" />
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{GUARDIAN_FLOORS.map((floor) => {
|
|
||||||
const guardian = GUARDIANS[floor];
|
|
||||||
const isDefeated = clearedFloors[floor];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={floor}
|
|
||||||
className={`flex items-center justify-between p-2 rounded border ${
|
|
||||||
isDefeated
|
|
||||||
? 'bg-emerald-900/20 border-emerald-800/40'
|
|
||||||
: 'bg-gray-800/40 border-gray-700/50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="w-7 h-7 rounded flex items-center justify-center text-xs font-bold"
|
|
||||||
style={{
|
|
||||||
backgroundColor: isDefeated ? `${guardian.color}30` : '#374151',
|
|
||||||
color: isDefeated ? guardian.color : '#6B7280',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{floor}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className={`text-sm font-medium ${isDefeated ? 'text-gray-100' : 'text-gray-400'}`}>
|
|
||||||
{guardian.name}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className="text-[10px] px-1.5 py-0.5 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${guardian.color}15`,
|
|
||||||
color: guardian.color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{guardian.element}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-gray-500">HP: {fmt(guardian.hp)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{isDefeated ? (
|
|
||||||
<Badge variant="outline" className="border-emerald-600 text-emerald-400 text-xs">
|
|
||||||
✓ Defeated
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="outline" className="border-gray-600 text-gray-500 text-xs">
|
|
||||||
Undefeated
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* ── 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user