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
|
||||
Generated: 2026-05-20T13:20:47.227Z
|
||||
Found: 1 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
Generated: 2026-05-20T15:46:48.123Z
|
||||
Found: 3 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
|
||||
1. Processed 125 files (1.4s) (4 warnings)
|
||||
1. Processed 125 files (1.5s) (3 warnings)
|
||||
2. 1) stores/gameStore.ts > stores/gameActions.ts
|
||||
3. 2) stores/gameStore.ts > stores/gameLoopActions.ts
|
||||
|
||||
## How to fix
|
||||
1. Identify which import in the chain can be extracted to a shared types/utils file.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-05-20T13:20:45.668Z",
|
||||
"generated": "2026-05-20T15:46:46.373Z",
|
||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
"data/attunements.ts",
|
||||
"data/enchantment-effects.ts",
|
||||
"effects/special-effects.ts",
|
||||
"effects/upgrade-effects.ts",
|
||||
"effects/upgrade-effects.types.ts",
|
||||
"types.ts"
|
||||
],
|
||||
"crafting-attunements.ts": [
|
||||
@@ -139,7 +139,7 @@
|
||||
"data/enchantment-effects.ts",
|
||||
"data/equipment/index.ts",
|
||||
"effects/special-effects.ts",
|
||||
"effects/upgrade-effects.ts",
|
||||
"effects/upgrade-effects.types.ts",
|
||||
"types.ts"
|
||||
],
|
||||
"crafting-equipment.ts": [
|
||||
@@ -158,7 +158,6 @@
|
||||
],
|
||||
"crafting-utils.ts": [
|
||||
"data/crafting-recipes.ts",
|
||||
"data/enchantment-effects.ts",
|
||||
"data/equipment/index.ts",
|
||||
"types.ts"
|
||||
],
|
||||
@@ -371,7 +370,7 @@
|
||||
"effects/discipline-effects.ts": [
|
||||
"data/disciplines/index.ts",
|
||||
"stores/discipline-slice.ts",
|
||||
"types.ts",
|
||||
"types/disciplines.ts",
|
||||
"utils/discipline-math.ts"
|
||||
],
|
||||
"effects/dynamic-compute.ts": [
|
||||
@@ -431,7 +430,8 @@
|
||||
"stores/craftingStore.types.ts",
|
||||
"stores/manaStore.ts",
|
||||
"stores/uiStore.ts",
|
||||
"types.ts"
|
||||
"types.ts",
|
||||
"types/equipmentSlot.ts"
|
||||
],
|
||||
"stores/craftingStore.types.ts": [
|
||||
"types.ts"
|
||||
@@ -448,6 +448,7 @@
|
||||
"effects/discipline-effects.ts",
|
||||
"stores/combatStore.ts",
|
||||
"stores/discipline-slice.ts",
|
||||
"stores/gameStore.ts",
|
||||
"stores/manaStore.ts",
|
||||
"stores/prestigeStore.ts",
|
||||
"stores/uiStore.ts",
|
||||
@@ -471,6 +472,7 @@
|
||||
"effects/discipline-effects.ts",
|
||||
"stores/combatStore.ts",
|
||||
"stores/discipline-slice.ts",
|
||||
"stores/gameStore.ts",
|
||||
"stores/manaStore.ts",
|
||||
"stores/prestigeStore.ts",
|
||||
"stores/uiStore.ts",
|
||||
|
||||
@@ -52,6 +52,7 @@ Mana-Loop/
|
||||
│ ├── app/
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── GameOverScreen.tsx
|
||||
│ │ │ ├── GrimoireTab.tsx
|
||||
│ │ │ └── LeftPanel.tsx
|
||||
│ │ ├── globals.css
|
||||
│ │ ├── layout.tsx
|
||||
@@ -140,6 +141,7 @@ Mana-Loop/
|
||||
│ │ │ │ ├── SpireSummaryTab.test.ts
|
||||
│ │ │ │ ├── SpireSummaryTab.tsx
|
||||
│ │ │ │ ├── StatsTab.tsx
|
||||
│ │ │ │ ├── guardian-pacts-components.tsx
|
||||
│ │ │ │ └── index.ts
|
||||
│ │ │ ├── ActionButtons.tsx
|
||||
│ │ │ ├── ActivityLogPanel.tsx
|
||||
|
||||
@@ -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 { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
// Import from new modular stores
|
||||
import {
|
||||
useGameStore,
|
||||
useUIStore,
|
||||
@@ -20,148 +19,68 @@ import {
|
||||
} from '@/lib/game/stores';
|
||||
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
|
||||
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
||||
import { getUnifiedEffects, type UnifiedEffects } from '@/lib/game/effects';
|
||||
import { SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { TimeDisplay } from '@/components/game';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import type { SpellDef } from '@/lib/game/types';
|
||||
|
||||
import { TimeDisplay } from '@/components/game';
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
import { DebugName } from '@/components/game/debug/debug-context';
|
||||
|
||||
// Import extracted components
|
||||
import { GameOverScreen } from './components/GameOverScreen';
|
||||
import { LeftPanel } from './components/LeftPanel';
|
||||
import { GrimoireTab } from './components/GrimoireTab';
|
||||
|
||||
// Lazy load tab components
|
||||
const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DisciplinesTab })));
|
||||
const SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab })));
|
||||
const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab })));
|
||||
const DebugTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DebugTab })));
|
||||
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab })));
|
||||
const AttunementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AttunementsTab })));
|
||||
const PrestigeTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.PrestigeTab })));
|
||||
const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.EquipmentTab })));
|
||||
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
|
||||
const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GuardianPactsTab })));
|
||||
const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireSummaryTab })));
|
||||
const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab })));
|
||||
const SpireCombatPage = lazy(() => import('@/components/game/tabs/SpireCombatPage').then(module => ({ default: module.SpireCombatPage })));
|
||||
const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.DisciplinesTab })));
|
||||
const SpellsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.SpellsTab })));
|
||||
const StatsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.StatsTab })));
|
||||
const DebugTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.DebugTab })));
|
||||
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.AchievementsTab })));
|
||||
const AttunementsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.AttunementsTab })));
|
||||
const PrestigeTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.PrestigeTab })));
|
||||
const EquipmentTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.EquipmentTab })));
|
||||
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.GolemancyTab })));
|
||||
const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.GuardianPactsTab })));
|
||||
const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.SpireSummaryTab })));
|
||||
const CraftingTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.CraftingTab })));
|
||||
const SpireCombatPage = lazy(() => import('@/components/game/tabs/SpireCombatPage').then(m => ({ default: m.SpireCombatPage })));
|
||||
|
||||
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
|
||||
const TabFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
|
||||
|
||||
// ============================================================================
|
||||
// Grimoire Tab Component
|
||||
// ============================================================================
|
||||
|
||||
function GrimoireTab() {
|
||||
const [grimoireSpells, setGrimoireSpells] = useState<[string, SpellDef][]>(() => {
|
||||
if (typeof window !== 'undefined' && SPELLS_DEF) {
|
||||
return Object.entries(SPELLS_DEF).filter((entry): entry is [string, SpellDef] => !!entry[1].grimoire);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const loaded = typeof window !== 'undefined';
|
||||
|
||||
if (!loaded) {
|
||||
return <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>
|
||||
);
|
||||
function TabErrorFallback({ name }: { name: string }) {
|
||||
return <div className="p-4 text-red-400">{name} tab failed to load.</div>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Game Component
|
||||
// ============================================================================
|
||||
// ─── Derived Stats Hook ──────────────────────────────────────────────────────
|
||||
|
||||
export default function ManaLoopGame() {
|
||||
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState('spells');
|
||||
|
||||
// ALL hooks must be called before any conditional returns
|
||||
useGameLoop();
|
||||
|
||||
// Use useShallow to combine multi-field subscriptions and reduce re-renders
|
||||
const { day, hour, initGame } = useGameStore(useShallow(s => ({ day: s.day, hour: s.hour, initGame: s.initGame })));
|
||||
const { prestigeUpgrades, insight, loopInsight } = usePrestigeStore(useShallow(s => ({ prestigeUpgrades: s.prestigeUpgrades, insight: s.insight, loopInsight: s.loopInsight })));
|
||||
const { rawMana, meditateTicks } = useManaStore(useShallow(s => ({ rawMana: s.rawMana, meditateTicks: s.meditateTicks })));
|
||||
const spireMode = useCombatStore((s) => s.spireMode);
|
||||
const gameOver = useUIStore((s) => s.gameOver);
|
||||
|
||||
// Get equipment state from crafting store
|
||||
function useGameDerivedStats() {
|
||||
const { prestigeUpgrades } = usePrestigeStore(useShallow(s => ({
|
||||
prestigeUpgrades: s.prestigeUpgrades,
|
||||
})));
|
||||
const { meditateTicks } = useManaStore(useShallow(s => ({
|
||||
meditateTicks: s.meditateTicks,
|
||||
})));
|
||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
const day = useGameStore((s) => s.day);
|
||||
const hour = useGameStore((s) => s.hour);
|
||||
|
||||
// Derived state
|
||||
const upgradeEffects = getUnifiedEffects({
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
equippedInstances,
|
||||
equipmentInstances
|
||||
equipmentInstances,
|
||||
});
|
||||
|
||||
// Compute discipline bonuses from active disciplines
|
||||
const disciplineEffects = computeDisciplineEffects();
|
||||
|
||||
const maxMana = computeMaxMana({
|
||||
skills: {},
|
||||
prestigeUpgrades,
|
||||
skillUpgrades: {},
|
||||
skillTiers: {}
|
||||
skillTiers: {},
|
||||
}, upgradeEffects, disciplineEffects);
|
||||
|
||||
const baseRegen = computeRegen({
|
||||
@@ -172,30 +91,80 @@ export default function ManaLoopGame() {
|
||||
attunements: {},
|
||||
}, upgradeEffects, disciplineEffects);
|
||||
|
||||
const clickMana = computeClickMana({
|
||||
skills: {},
|
||||
}, disciplineEffects);
|
||||
|
||||
const clickMana = computeClickMana({ skills: {} }, disciplineEffects);
|
||||
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
|
||||
// Effective regen with incursion penalty
|
||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||||
|
||||
// Mana Cascade bonus
|
||||
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
||||
? Math.floor(maxMana / 100) * 0.1
|
||||
: 0;
|
||||
|
||||
// Mana Waterfall bonus
|
||||
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
||||
? Math.floor(maxMana / 100) * 0.25
|
||||
: 0;
|
||||
|
||||
// Effective regen
|
||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
||||
|
||||
// Initialize game on mount
|
||||
return { maxMana, effectiveRegen, clickMana, meditationMultiplier };
|
||||
}
|
||||
|
||||
// ─── Tab Triggers ────────────────────────────────────────────────────────────
|
||||
|
||||
function TabTriggers() {
|
||||
return (
|
||||
<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(() => {
|
||||
initGame();
|
||||
}, [initGame]);
|
||||
@@ -203,21 +172,18 @@ export default function ManaLoopGame() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => { setMounted(true); }, []); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
|
||||
// React to spireMode changes from combat store
|
||||
useEffect(() => {
|
||||
if (spireMode) {
|
||||
setActiveTab('spells'); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
}
|
||||
}, [spireMode]);
|
||||
|
||||
// Conditional returns AFTER all hooks
|
||||
if (gameOver) {
|
||||
return <GameOverScreen day={day} hour={hour} insightGained={loopInsight} totalInsight={insight} />;
|
||||
}
|
||||
|
||||
if (!mounted) return <div className="p-4 text-center text-gray-400">Loading...</div>;
|
||||
|
||||
// Spire mode: full-page replacement view
|
||||
if (spireMode) {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
@@ -232,7 +198,6 @@ export default function ManaLoopGame() {
|
||||
<ErrorBoundary>
|
||||
<TooltipProvider>
|
||||
<div className="game-root min-h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-gradient-to-b from-gray-900 to-gray-900/80 border-b border-gray-700 px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
|
||||
@@ -242,127 +207,26 @@ export default function ManaLoopGame() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
|
||||
<LeftPanel />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
||||
<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>
|
||||
<TabTriggers />
|
||||
|
||||
<TabsContent value="spells">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">spells tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<SpellsTab />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stats">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">stats tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<StatsTab />
|
||||
</Suspense>
|
||||
</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>
|
||||
<TabsContent value="spells"><LazyTab name="spells"><SpellsTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="stats"><LazyTab name="stats"><StatsTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="disciplines"><LazyTab name="disciplines"><DisciplinesTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="grimoire"><GrimoireTab /></TabsContent>
|
||||
<TabsContent value="debug"><LazyTab name="debug"><DebugTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="attunements"><LazyTab name="attunements"><AttunementsTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="achievements"><LazyTab name="achievements"><AchievementsTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="prestige"><LazyTab name="prestige"><PrestigeTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="equipment"><LazyTab name="equipment"><EquipmentTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="golemancy"><LazyTab name="golemancy"><GolemancyTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="pacts"><LazyTab name="pacts"><GuardianPactsTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="spire"><LazyTab name="spire"><SpireSummaryTab /></LazyTab></TabsContent>
|
||||
<TabsContent value="crafting"><LazyTab name="crafting"><CraftingTab /></LazyTab></TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -9,22 +9,220 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { Package, Sparkles, Trash2, Anvil } from 'lucide-react';
|
||||
import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes';
|
||||
import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
||||
import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
|
||||
import type { LootInventory } from '@/lib/game/types';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import { useCraftingStore, useCombatStore, useManaStore } from '@/lib/game/stores';
|
||||
|
||||
// ─── Crafting Progress ───────────────────────────────────────────────────────
|
||||
|
||||
function CraftingProgress({ progress }: { progress: { blueprintId: string; progress: number; required: number; manaSpent: number } }) {
|
||||
const recipe = CRAFTING_RECIPES[progress.blueprintId];
|
||||
const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
|
||||
|
||||
return (
|
||||
<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() {
|
||||
const lootInventory = useCraftingStore((s) => s.lootInventory);
|
||||
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const currentAction = useCombatStore((s) => s.currentAction);
|
||||
const startCraftingEquipment = useCraftingStore((s) => s.startCraftingEquipment);
|
||||
const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
|
||||
const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Blueprint Selection */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
||||
@@ -34,166 +232,16 @@ export function EquipmentCrafter() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{equipmentCraftingProgress ? (
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
<CraftingProgress progress={equipmentCraftingProgress} />
|
||||
) : (
|
||||
<ScrollArea className="h-64">
|
||||
<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>
|
||||
<BlueprintList lootInventory={lootInventory} rawMana={rawMana} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Materials Inventory */}
|
||||
<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>
|
||||
<MaterialsInventory materials={lootInventory.materials} />
|
||||
</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 { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
import {
|
||||
RotateCcw, AlertTriangle, Zap, Clock, Settings, Eye,
|
||||
} from 'lucide-react';
|
||||
import { useDebug } from '@/components/game/debug/debug-context';
|
||||
import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores';
|
||||
import { computeMaxMana } from '@/lib/game/stores';
|
||||
|
||||
// ─── Warning Banner ──────────────────────────────────────────────────────────
|
||||
|
||||
function WarningBanner() {
|
||||
return (
|
||||
<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() {
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
const { showComponentNames, toggleComponentNames } = useDebug();
|
||||
|
||||
// Get state from modular stores
|
||||
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const unlockElement = useManaStore((s) => s.unlockElement);
|
||||
@@ -26,8 +235,6 @@ export function GameStateDebug() {
|
||||
const hour = useGameStore((s) => s.hour);
|
||||
const paused = useUIStore((s) => s.paused);
|
||||
const togglePause = useUIStore((s) => s.togglePause);
|
||||
|
||||
// Get actions from stores
|
||||
const resetGame = useGameStore((s) => s.resetGame);
|
||||
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
|
||||
const resetFloorHP = useCombatStore((s) => s.resetFloorHP);
|
||||
@@ -48,222 +255,52 @@ export function GameStateDebug() {
|
||||
}
|
||||
};
|
||||
|
||||
const getMaxMana = () => {
|
||||
return computeMaxMana(
|
||||
const handleFillMana = () => {
|
||||
const maxMana = computeMaxMana(
|
||||
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
|
||||
);
|
||||
) || 100;
|
||||
useManaStore.setState((s) => ({ rawMana: Math.max(s.rawMana, maxMana) }));
|
||||
};
|
||||
|
||||
const handleSetDay = (d: number) => {
|
||||
useGameStore.setState({ day: d, hour: 0 });
|
||||
};
|
||||
|
||||
const handleUnlockBase = () => {
|
||||
['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => {
|
||||
if (!elements[e]?.unlocked) {
|
||||
unlockElement(e, 500);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleUnlockUtility = () => {
|
||||
['transference'].forEach(e => {
|
||||
if (!elements[e]?.unlocked) {
|
||||
unlockElement(e, 500);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Warning Banner */}
|
||||
<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 */}
|
||||
<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>
|
||||
<WarningBanner />
|
||||
<DisplayOptions />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Game Reset */}
|
||||
<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={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>
|
||||
<GameResetSection confirmReset={confirmReset} onReset={handleReset} />
|
||||
<ManaDebugSection rawMana={rawMana} onAddMana={handleAddMana} onFillMana={handleFillMana} />
|
||||
<TimeControlSection day={day} hour={hour} paused={paused} onSetDay={handleSetDay} onTogglePause={togglePause} />
|
||||
<QuickActionsSection
|
||||
elements={elements}
|
||||
onUnlockBase={handleUnlockBase}
|
||||
onUnlockUtility={handleUnlockUtility}
|
||||
onSkipToFloor={() => debugSetFloor?.(100)}
|
||||
onResetFloorHP={() => resetFloorHP?.()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 { 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() {
|
||||
// Get state from modular stores
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const signedPactDetails = usePrestigeStore((s) => s.signedPactDetails);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
|
||||
// Get actions
|
||||
|
||||
const addSignedPact = usePrestigeStore((s) => s.addSignedPact);
|
||||
const removePact = usePrestigeStore((s) => s.removePact);
|
||||
const debugSetSignedPacts = usePrestigeStore((s) => s.debugSetSignedPacts);
|
||||
const debugSetPactDetails = usePrestigeStore((s) => s.debugSetPactDetails);
|
||||
const unlockElement = useManaStore((s) => s.unlockElement);
|
||||
|
||||
// Get log function from uiStore
|
||||
const addLog = useUIStore((s) => s.addLog);
|
||||
|
||||
// Get all guardian floors
|
||||
const guardianFloors = Object.keys(GUARDIANS || {}).map(Number).sort((a, b) => a - b);
|
||||
|
||||
// Force sign a pact with a guardian (bypass costs and time)
|
||||
const addLog = useUIStore((s) => s.addLog);
|
||||
|
||||
const forcePact = (floor: number) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian) return;
|
||||
|
||||
// Check if already signed
|
||||
if (signedPacts.includes(floor)) {
|
||||
addLog(`⚠️ Already signed pact with ${guardian.name}!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check max pacts
|
||||
const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0);
|
||||
if (signedPacts.length >= maxPacts) {
|
||||
addLog(`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Force sign the pact
|
||||
addSignedPact(floor);
|
||||
|
||||
// Add pact details
|
||||
|
||||
const newSignedPactDetails = {
|
||||
...signedPactDetails,
|
||||
[floor]: {
|
||||
@@ -62,13 +120,11 @@ export function PactDebug() {
|
||||
addLog(`📜 DEBUG: Pact with ${guardian.name} force-signed!`);
|
||||
};
|
||||
|
||||
// Remove a pact
|
||||
const removePactHandler = (floor: number) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
|
||||
|
||||
removePact(floor);
|
||||
|
||||
// Remove pact details
|
||||
|
||||
const newSignedPactDetails = { ...signedPactDetails };
|
||||
delete newSignedPactDetails[floor];
|
||||
debugSetPactDetails(newSignedPactDetails);
|
||||
@@ -76,7 +132,6 @@ export function PactDebug() {
|
||||
addLog(`📜 DEBUG: Removed pact with ${guardian?.name || 'Unknown'}!`);
|
||||
};
|
||||
|
||||
// Clear all pacts
|
||||
const clearAllPacts = () => {
|
||||
addLog(`📜 DEBUG: Cleared all pacts!`);
|
||||
debugSetSignedPacts([]);
|
||||
@@ -96,74 +151,23 @@ export function PactDebug() {
|
||||
<p className="text-xs text-gray-400 mb-2">
|
||||
Force sign pacts with guardians (bypasses mana costs and signing time)
|
||||
</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 && (
|
||||
<div className="pt-2 border-t border-gray-700">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={clearAllPacts}
|
||||
className="w-full text-xs"
|
||||
>
|
||||
<Button size="sm" variant="destructive" onClick={clearAllPacts} className="w-full text-xs">
|
||||
Clear All Pacts ({signedPacts.length})
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status */}
|
||||
<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)}
|
||||
</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 { 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() {
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
const { showComponentNames, toggleComponentNames } = useDebug();
|
||||
@@ -22,7 +210,6 @@ export function GameStateDebugSection() {
|
||||
const hour = useGameStore((s) => s.hour);
|
||||
const paused = useUIStore((s) => s.paused);
|
||||
const togglePause = useUIStore((s) => s.togglePause);
|
||||
|
||||
const resetGame = useGameStore((s) => s.resetGame);
|
||||
const gatherMana = useGameStore((s) => s.gatherMana);
|
||||
const debugSetFloor = useCombatStore((s) => s.debugSetFloor);
|
||||
@@ -46,190 +233,42 @@ export function GameStateDebugSection() {
|
||||
}
|
||||
};
|
||||
|
||||
const getMaxMana = () => {
|
||||
return computeMaxMana(
|
||||
const handleFillMana = () => {
|
||||
const maxMana = computeMaxMana(
|
||||
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} }
|
||||
);
|
||||
) || 100;
|
||||
useManaStore.setState((s) => ({ rawMana: Math.max(s.rawMana, maxMana) }));
|
||||
};
|
||||
|
||||
const handleSetDay = (d: number) => {
|
||||
useGameStore.setState({ day: d, hour: 0 });
|
||||
};
|
||||
|
||||
const handleUnlockBase = () => {
|
||||
['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => {
|
||||
if (!elements[e]?.unlocked) {
|
||||
unlockElement(e, 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 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>
|
||||
<DisplayOptions />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Game Reset */}
|
||||
<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={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>
|
||||
<GameResetSection confirmReset={confirmReset} onReset={handleReset} />
|
||||
<ManaDebugSection rawMana={rawMana} onAddMana={handleAddMana} onFillMana={handleFillMana} />
|
||||
<TimeControlSection day={day} hour={hour} paused={paused} onSetDay={handleSetDay} onTogglePause={togglePause} />
|
||||
<QuickActionsSection
|
||||
elements={elements}
|
||||
onUnlockBase={handleUnlockBase}
|
||||
onSkipToFloor={() => debugSetFloor?.(100)}
|
||||
onResetFloorHP={() => resetFloorHP?.()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 { 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() {
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const signedPactDetails = usePrestigeStore((s) => s.signedPactDetails);
|
||||
@@ -99,53 +144,15 @@ export function PactDebugSection() {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
{guardianFloors.map((floor) => (
|
||||
<GuardianPactRow
|
||||
key={floor}
|
||||
floor={floor}
|
||||
isSigned={signedPacts.includes(floor)}
|
||||
onForceSign={() => forcePact(floor)}
|
||||
onRemove={() => removePactHandler(floor)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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 { useManaStore } from '@/lib/game/stores/manaStore';
|
||||
import { useUIStore } from '@/lib/game/stores/uiStore';
|
||||
import { GUARDIANS, ELEMENTS } from '@/lib/game/constants';
|
||||
import type { GuardianDef, GuardianBoon } from '@/lib/game/types';
|
||||
import { GUARDIANS } from '@/lib/game/constants';
|
||||
import { DebugName } from '@/components/game/debug/debug-context';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Shield, Swords, Clock, Sparkles, Check, Lock, ChevronRight } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
GuardianCard,
|
||||
PactHeaderSummary,
|
||||
TierFilter,
|
||||
} from './guardian-pacts-components';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -24,172 +24,6 @@ function getGuardianStatus(floor: number, defeated: number[], signed: number[]):
|
||||
return 'undefeated';
|
||||
}
|
||||
|
||||
function formatHours(hours: number): string {
|
||||
return `${hours}h`;
|
||||
}
|
||||
|
||||
// ─── Guardian Card ───────────────────────────────────────────────────────────
|
||||
|
||||
interface GuardianCardProps {
|
||||
floor: number;
|
||||
guardian: GuardianDef;
|
||||
status: GuardianStatus;
|
||||
canAfford: boolean;
|
||||
hasSlot: boolean;
|
||||
isRitualActive: boolean;
|
||||
ritualProgress: number;
|
||||
onStartRitual: (floor: number) => void;
|
||||
}
|
||||
|
||||
const GuardianCard: React.FC<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 {
|
||||
label: string;
|
||||
floors: number[];
|
||||
@@ -263,7 +97,6 @@ export const GuardianPactsTab: React.FC = () => {
|
||||
}
|
||||
}, [startPactRitual, rawMana, addLog]);
|
||||
|
||||
// Cumulative boon summary from signed pacts
|
||||
const cumulativeBoons = useMemo(() => {
|
||||
const boonMap: Record<string, number> = {};
|
||||
for (const floor of signedPacts) {
|
||||
@@ -287,95 +120,34 @@ export const GuardianPactsTab: React.FC = () => {
|
||||
return (
|
||||
<DebugName name="GuardianPactsTab">
|
||||
<div className="space-y-4">
|
||||
{/* Header Summary */}
|
||||
<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">{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>
|
||||
<PactHeaderSummary
|
||||
signedCount={signedPacts.length}
|
||||
pactSlots={pactSlots}
|
||||
defeatedCount={defeatedGuardians.length}
|
||||
cumulativeBoons={cumulativeBoons}
|
||||
/>
|
||||
|
||||
{/* Cumulative Boons */}
|
||||
{signedPacts.length > 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>
|
||||
<TierFilter
|
||||
tiers={tiers}
|
||||
activeTier={activeTier}
|
||||
guardianFloors={guardianFloors}
|
||||
onSelectTier={setActiveTier}
|
||||
/>
|
||||
|
||||
{/* 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">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{filteredFloors.map((floor) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian) return null;
|
||||
const status = getGuardianStatus(floor, defeatedGuardians, signedPacts);
|
||||
const isRitualActive = pactRitualFloor === floor;
|
||||
const hasSlot = signedPacts.length < pactSlots;
|
||||
const canAfford = rawMana >= guardian.pactCost;
|
||||
|
||||
return (
|
||||
<GuardianCard
|
||||
key={floor}
|
||||
floor={floor}
|
||||
guardian={guardian}
|
||||
status={status}
|
||||
canAfford={canAfford}
|
||||
hasSlot={hasSlot}
|
||||
isRitualActive={isRitualActive}
|
||||
status={getGuardianStatus(floor, defeatedGuardians, signedPacts)}
|
||||
canAfford={rawMana >= guardian.pactCost}
|
||||
hasSlot={signedPacts.length < pactSlots}
|
||||
isRitualActive={pactRitualFloor === floor}
|
||||
ritualProgress={pactRitualProgress}
|
||||
onStartRitual={handleStartRitual}
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,101 @@ import {
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
// ─── Stat Cell ────────────────────────────────────────────────────────────────
|
||||
|
||||
function PrestigeStatCell({ value, label, color }: { value: string | number; label: string; color: string }) {
|
||||
return (
|
||||
<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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
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 ───────────────────────────────────────────────────────────
|
||||
|
||||
export function PrestigeTab() {
|
||||
@@ -130,80 +268,18 @@ export function PrestigeTab() {
|
||||
return (
|
||||
<DebugName name="PrestigeTab">
|
||||
<div className="space-y-4">
|
||||
{/* Insight & Loop Summary */}
|
||||
<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">
|
||||
<div className="text-center">
|
||||
<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>
|
||||
<InsightSummary
|
||||
insight={insight}
|
||||
totalInsight={totalInsight}
|
||||
loopCount={loopCount}
|
||||
loopInsight={loopInsight}
|
||||
/>
|
||||
|
||||
{/* Memories & Pacts */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<MemoriesCard memories={memories} memorySlots={memorySlots} />
|
||||
<PactsCard signedPacts={signedPacts} pactSlots={pactSlots} defeatedGuardians={defeatedGuardians} />
|
||||
</div>
|
||||
|
||||
{/* Prestige Upgrades */}
|
||||
<Card className="bg-gray-900/60 border-gray-700">
|
||||
<SectionHeader title="⬆️ Prestige Upgrades" />
|
||||
<CardContent className="pt-0">
|
||||
@@ -227,43 +303,7 @@ export function PrestigeTab() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Reset Loop */}
|
||||
<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>
|
||||
<ResetLoopSection loopInsight={loopInsight} onReset={handleResetLoop} />
|
||||
</div>
|
||||
</DebugName>
|
||||
);
|
||||
|
||||
@@ -5,82 +5,21 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores';
|
||||
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
||||
import { useCraftingStore } from '@/lib/game/stores/craftingStore';
|
||||
import { GUARDIANS } from '@/lib/game/constants';
|
||||
import { getExtendedGuardian, isGuardianFloor } from '@/lib/game/data/guardian-encounters';
|
||||
import { getRoomsForFloor, generateSpireFloorState, calcInsight } from '@/lib/game/utils/spire-utils';
|
||||
import { getRoomsForFloor, generateSpireFloorState } from '@/lib/game/utils/spire-utils';
|
||||
import { SpireHeader } from './SpireHeader';
|
||||
import { RoomDisplay } from './RoomDisplay';
|
||||
import { SpireCombatControls } from './SpireCombatControls';
|
||||
import { SpireActivityLog } from './SpireActivityLog';
|
||||
import { SpireManaDisplay } from './SpireManaDisplay';
|
||||
|
||||
export function SpireCombatPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [roomsCleared, setRoomsCleared] = useState(0);
|
||||
// ─── Derived Stats Hook ──────────────────────────────────────────────────────
|
||||
|
||||
// Combat store
|
||||
const {
|
||||
currentFloor,
|
||||
floorHP,
|
||||
floorMaxHP,
|
||||
castProgress,
|
||||
clearedFloors,
|
||||
isDescending,
|
||||
currentRoom,
|
||||
activityLog,
|
||||
setCurrentRoom,
|
||||
setFloorHP,
|
||||
setClearedFloor,
|
||||
climbDownFloor,
|
||||
exitSpireMode,
|
||||
startClimbUp,
|
||||
startClimbDown,
|
||||
addActivityLog,
|
||||
processCombatTick,
|
||||
setAction,
|
||||
} = useCombatStore(useShallow((s) => ({
|
||||
currentFloor: s.currentFloor,
|
||||
floorHP: s.floorHP,
|
||||
floorMaxHP: s.floorMaxHP,
|
||||
castProgress: s.castProgress,
|
||||
clearedFloors: s.clearedFloors,
|
||||
isDescending: s.isDescending,
|
||||
currentRoom: s.currentRoom,
|
||||
activityLog: s.activityLog,
|
||||
setCurrentRoom: s.setCurrentRoom,
|
||||
setFloorHP: s.setFloorHP,
|
||||
setClearedFloor: s.setClearedFloor,
|
||||
climbDownFloor: s.climbDownFloor,
|
||||
exitSpireMode: s.exitSpireMode,
|
||||
startClimbUp: s.startClimbUp,
|
||||
startClimbDown: s.startClimbDown,
|
||||
addActivityLog: s.addActivityLog,
|
||||
processCombatTick: s.processCombatTick,
|
||||
setAction: s.setAction,
|
||||
})));
|
||||
|
||||
// Mana store
|
||||
const { rawMana, elements } = useManaStore(useShallow((s) => ({
|
||||
rawMana: s.rawMana,
|
||||
elements: s.elements,
|
||||
})));
|
||||
|
||||
// Prestige store
|
||||
const { prestigeUpgrades, insight } = usePrestigeStore(useShallow((s) => ({
|
||||
prestigeUpgrades: s.prestigeUpgrades,
|
||||
insight: s.insight,
|
||||
})));
|
||||
|
||||
// Crafting store for equipment effects
|
||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
|
||||
// Discipline effects
|
||||
function useSpireStats(prestigeUpgrades: Record<string, number>, equippedInstances: unknown[], equipmentInstances: unknown[]) {
|
||||
const disciplineEffects = computeDisciplineEffects();
|
||||
|
||||
// Compute derived stats
|
||||
const upgradeEffects = getUnifiedEffects({
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
@@ -103,10 +42,70 @@ export function SpireCombatPage() {
|
||||
attunements: {},
|
||||
}, upgradeEffects, disciplineEffects);
|
||||
|
||||
// Total rooms for current floor
|
||||
return { maxMana, baseRegen };
|
||||
}
|
||||
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
export function SpireCombatPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [roomsCleared, setRoomsCleared] = useState(0);
|
||||
|
||||
const {
|
||||
currentFloor,
|
||||
floorHP,
|
||||
floorMaxHP,
|
||||
castProgress,
|
||||
clearedFloors,
|
||||
isDescending,
|
||||
currentRoom,
|
||||
activityLog,
|
||||
setCurrentRoom,
|
||||
setFloorHP,
|
||||
setClearedFloor,
|
||||
climbDownFloor,
|
||||
exitSpireMode,
|
||||
startClimbUp,
|
||||
startClimbDown,
|
||||
addActivityLog,
|
||||
setAction,
|
||||
} = useCombatStore(useShallow((s) => ({
|
||||
currentFloor: s.currentFloor,
|
||||
floorHP: s.floorHP,
|
||||
floorMaxHP: s.floorMaxHP,
|
||||
castProgress: s.castProgress,
|
||||
clearedFloors: s.clearedFloors,
|
||||
isDescending: s.isDescending,
|
||||
currentRoom: s.currentRoom,
|
||||
activityLog: s.activityLog,
|
||||
setCurrentRoom: s.setCurrentRoom,
|
||||
setFloorHP: s.setFloorHP,
|
||||
setClearedFloor: s.setClearedFloor,
|
||||
climbDownFloor: s.climbDownFloor,
|
||||
exitSpireMode: s.exitSpireMode,
|
||||
startClimbUp: s.startClimbUp,
|
||||
startClimbDown: s.startClimbDown,
|
||||
addActivityLog: s.addActivityLog,
|
||||
setAction: s.setAction,
|
||||
})));
|
||||
|
||||
const { rawMana, elements } = useManaStore(useShallow((s) => ({
|
||||
rawMana: s.rawMana,
|
||||
elements: s.elements,
|
||||
})));
|
||||
|
||||
const { prestigeUpgrades, insight } = usePrestigeStore(useShallow((s) => ({
|
||||
prestigeUpgrades: s.prestigeUpgrades,
|
||||
insight: s.insight,
|
||||
})));
|
||||
|
||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
|
||||
const { maxMana, baseRegen } = useSpireStats(prestigeUpgrades, equippedInstances, equipmentInstances);
|
||||
|
||||
const totalRooms = useMemo(() => getRoomsForFloor(currentFloor), [currentFloor]);
|
||||
|
||||
// Initialize room on floor change
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
setRoomsCleared(0);
|
||||
@@ -115,12 +114,10 @@ export function SpireCombatPage() {
|
||||
setAction('climb');
|
||||
}, [currentFloor, totalRooms, setCurrentRoom, setAction]);
|
||||
|
||||
// Handle room/floor transitions
|
||||
const handleRoomCleared = () => {
|
||||
const nextRoomIndex = roomsCleared + 1;
|
||||
|
||||
if (nextRoomIndex >= totalRooms) {
|
||||
// Floor cleared
|
||||
const wasGuardian = isGuardianFloor(currentFloor);
|
||||
setClearedFloor(currentFloor, true);
|
||||
|
||||
@@ -138,30 +135,26 @@ export function SpireCombatPage() {
|
||||
floor: currentFloor,
|
||||
});
|
||||
|
||||
// Auto-advance to next floor
|
||||
const newFloor = currentFloor + 1;
|
||||
const newTotalRooms = getRoomsForFloor(newFloor);
|
||||
const newRoom = generateSpireFloorState(newFloor, 0, newTotalRooms);
|
||||
|
||||
setCurrentRoom(newRoom);
|
||||
setFloorHP(floorMaxHP); // Reset HP for new floor
|
||||
setFloorHP(floorMaxHP);
|
||||
setClearedFloor(currentFloor, true);
|
||||
setRoomsCleared(0);
|
||||
} else {
|
||||
// Next room on same floor
|
||||
const newRoom = generateSpireFloorState(currentFloor, nextRoomIndex, totalRooms);
|
||||
setCurrentRoom(newRoom);
|
||||
setRoomsCleared(nextRoomIndex);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle climb up
|
||||
const handleClimbUp = () => {
|
||||
startClimbUp();
|
||||
addActivityLog('floor_transition', `⬆️ Climbing to floor ${currentFloor + 1}...`);
|
||||
};
|
||||
|
||||
// Handle climb down
|
||||
const handleClimbDown = () => {
|
||||
if (currentFloor <= 1) return;
|
||||
startClimbDown();
|
||||
@@ -170,7 +163,6 @@ export function SpireCombatPage() {
|
||||
addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`);
|
||||
};
|
||||
|
||||
// Handle exit spire
|
||||
const handleExitSpire = () => {
|
||||
exitSpireMode();
|
||||
addActivityLog('floor_transition', '🚪 Exited the Spire.');
|
||||
@@ -186,7 +178,6 @@ export function SpireCombatPage() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-lg font-bold text-amber-400 tracking-wider">🏔️ SPIRE</h1>
|
||||
@@ -196,9 +187,7 @@ export function SpireCombatPage() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
<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="lg:col-span-2">
|
||||
<SpireHeader
|
||||
@@ -218,7 +207,6 @@ export function SpireCombatPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Middle section: Room + Controls */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div className="lg:col-span-2">
|
||||
<RoomDisplay floorState={currentRoom} floor={currentFloor} />
|
||||
@@ -228,7 +216,6 @@ export function SpireCombatPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom: Activity Log */}
|
||||
<SpireActivityLog activityLog={activityLog} maxEntries={20} />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,6 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
|
||||
const totalFloors = Math.min(maxFloor, 100);
|
||||
const clearedSet = new Set(Object.entries(clearedFloors).filter(([, v]) => v).map(([k]) => Number(k)));
|
||||
|
||||
// Group floors into rows of 10 for display
|
||||
const rows: number[][] = [];
|
||||
for (let i = 0; i < totalFloors; i += 10) {
|
||||
rows.push(Array.from({ length: 10 }, (_, j) => i + j + 1).filter((f) => f <= totalFloors));
|
||||
@@ -88,23 +87,219 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
<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>
|
||||
<FloorLegend />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FloorLegend() {
|
||||
return (
|
||||
<div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500">
|
||||
<div className="flex items-center gap-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 className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-gray-800 border border-gray-700" />
|
||||
<span>Uncleared</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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 className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-gray-800 border border-amber-500" />
|
||||
<span>Guardian</span>
|
||||
|
||||
<PreparationTips
|
||||
counterElement={counterElement}
|
||||
nextFloorElement={nextFloorElement}
|
||||
hasHighArmor={!!(nextGuardianData.armor && nextGuardianData.armor > 0.15)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function PreparationTips({ counterElement, nextFloorElement, hasHighArmor }: { counterElement: string | null; nextFloorElement: string | null; hasHighArmor: boolean }) {
|
||||
return (
|
||||
<div className="bg-gray-800/50 rounded-lg p-3 space-y-2">
|
||||
<div className="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 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>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 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>
|
||||
</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>
|
||||
);
|
||||
@@ -135,7 +330,6 @@ export function SpireSummaryTab() {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Derived data
|
||||
const defeatedGuardians = useMemo(() => {
|
||||
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
|
||||
}, [clearedFloors]);
|
||||
@@ -146,9 +340,6 @@ export function SpireSummaryTab() {
|
||||
|
||||
const nextGuardianData = nextGuardian ? GUARDIANS[nextGuardian] : null;
|
||||
|
||||
const counterElement = nextGuardianData ? getCounterElement(nextGuardianData.element) : null;
|
||||
const nextFloorElement = nextGuardian ? FLOOR_ELEM_CYCLE[(nextGuardian - 1) % FLOOR_ELEM_CYCLE.length] : null;
|
||||
|
||||
const totalFloorsCleared = useMemo(() => {
|
||||
return Object.values(clearedFloors).filter(Boolean).length;
|
||||
}, [clearedFloors]);
|
||||
@@ -164,31 +355,13 @@ export function SpireSummaryTab() {
|
||||
return (
|
||||
<DebugName name="SpireSummaryTab">
|
||||
<div className="space-y-4">
|
||||
{/* ── Top Stats Row ─────────────────────────────────────────────── */}
|
||||
<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">
|
||||
<div className="text-center">
|
||||
<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>
|
||||
<TopStatsRow
|
||||
maxFloorReached={maxFloorReached}
|
||||
totalFloorsCleared={totalFloorsCleared}
|
||||
defeatedCount={defeatedGuardians.length}
|
||||
insight={insight}
|
||||
/>
|
||||
|
||||
{/* ── Climb the Spire Button ────────────────────────────────────── */}
|
||||
<DebugName name="ClimbSpireButton">
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-600 text-white"
|
||||
@@ -200,146 +373,12 @@ export function SpireSummaryTab() {
|
||||
</Button>
|
||||
</DebugName>
|
||||
|
||||
{/* ── Next Guardian + Preparation ───────────────────────────────── */}
|
||||
{nextGuardianData && nextGuardian && (
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
<NextGuardianCard nextGuardian={nextGuardian} nextGuardianData={nextGuardianData} />
|
||||
)}
|
||||
|
||||
{/* ── All Guardians List ────────────────────────────────────────── */}
|
||||
<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];
|
||||
<GuardianRoster clearedFloors={clearedFloors} />
|
||||
|
||||
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">
|
||||
<SectionHeader title="🗺️ Floor Progress" />
|
||||
<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