cleanup: delete computed-stats.ts shim and store/index.ts
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 57s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 57s
- Delete src/lib/game/computed-stats.ts (root-level re-export shim) - Delete src/lib/game/store/index.ts (nothing imports from it) - Update __tests__/computed-stats.test.ts to import from ../utils instead - Clean up craftingStore.ts imports (remove unused useGameStore, CraftingApply) Typecheck and lint pass (pre-existing DisciplinesTab.tsx errors unchanged)
This commit is contained in:
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Target, FlaskConical, Sparkles, Play, Pause, X } from 'lucide-react';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import { formatStudyTime } from '@/lib/game/formatting';
|
||||
import { formatStudyTime } from '@/lib/game/utils/formatting';
|
||||
import type { EquipmentInstance, EnchantmentDesign } from '@/lib/game/types';
|
||||
|
||||
interface CraftingProgressProps {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, type ReactNode } from 'react';
|
||||
import { useSkillStore } from '@/lib/game/stores/skillStore';
|
||||
import { useManaStore } from '@/lib/game/stores/manaStore';
|
||||
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
||||
import { useUIStore } from '@/lib/game/stores/uiStore';
|
||||
@@ -9,7 +8,6 @@ import { useCombatStore } from '@/lib/game/stores/combatStore';
|
||||
import { useGameStore } from '@/lib/game/stores/gameStore';
|
||||
import { computeEffects } from '@/lib/game/upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/special-effects';
|
||||
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||
import {
|
||||
computeMaxMana,
|
||||
computeRegen,
|
||||
@@ -32,7 +30,6 @@ import { GameContext } from './context-create';
|
||||
|
||||
function createUnifiedStore(
|
||||
gameStore: ReturnType<typeof useGameStore.getState>,
|
||||
skillState: ReturnType<typeof useSkillStore.getState>,
|
||||
manaState: ReturnType<typeof useManaStore.getState>,
|
||||
prestigeState: ReturnType<typeof usePrestigeStore.getState>,
|
||||
uiState: ReturnType<typeof useUIStore.getState>,
|
||||
@@ -49,7 +46,7 @@ function createUnifiedStore(
|
||||
resetGame: gameStore.resetGame,
|
||||
gatherMana: gameStore.gatherMana,
|
||||
startNewLoop: gameStore.startNewLoop,
|
||||
|
||||
|
||||
// From manaStore
|
||||
rawMana: manaState.rawMana,
|
||||
meditateTicks: manaState.meditateTicks,
|
||||
@@ -61,25 +58,7 @@ function createUnifiedStore(
|
||||
convertMana: manaState.convertMana,
|
||||
unlockElement: manaState.unlockElement,
|
||||
craftComposite: manaState.craftComposite,
|
||||
|
||||
// From skillStore
|
||||
skills: skillState.skills,
|
||||
skillProgress: skillState.skillProgress,
|
||||
skillUpgrades: skillState.skillUpgrades,
|
||||
skillTiers: skillState.skillTiers,
|
||||
paidStudySkills: skillState.paidStudySkills,
|
||||
currentStudyTarget: skillState.currentStudyTarget,
|
||||
parallelStudyTarget: skillState.parallelStudyTarget,
|
||||
setSkillLevel: skillState.setSkillLevel,
|
||||
startStudyingSkill: skillState.startStudyingSkill,
|
||||
startStudyingSpell: skillState.startStudyingSpell,
|
||||
cancelStudy: skillState.cancelStudy,
|
||||
selectSkillUpgrade: skillState.selectSkillUpgrade,
|
||||
deselectSkillUpgrade: skillState.deselectSkillUpgrade,
|
||||
commitSkillUpgrades: skillState.commitSkillUpgrades,
|
||||
tierUpSkill: skillState.tierUpSkill,
|
||||
getSkillUpgradeChoices: skillState.getSkillUpgradeChoices,
|
||||
|
||||
|
||||
// From prestigeStore
|
||||
loopCount: prestigeState.loopCount,
|
||||
insight: prestigeState.insight,
|
||||
@@ -101,7 +80,7 @@ function createUnifiedStore(
|
||||
cancelPactRitual: prestigeState.cancelPactRitual,
|
||||
removePact: prestigeState.removePact,
|
||||
defeatGuardian: prestigeState.defeatGuardian,
|
||||
|
||||
|
||||
// From combatStore
|
||||
currentFloor: combatState.currentFloor,
|
||||
floorHP: combatState.floorHP,
|
||||
@@ -115,7 +94,7 @@ function createUnifiedStore(
|
||||
setSpell: combatState.setSpell,
|
||||
learnSpell: combatState.learnSpell,
|
||||
advanceFloor: combatState.advanceFloor,
|
||||
|
||||
|
||||
// From uiStore
|
||||
log: uiState.logs,
|
||||
paused: uiState.paused,
|
||||
@@ -131,125 +110,114 @@ function createUnifiedStore(
|
||||
export function GameProvider({ children }: { children: ReactNode }) {
|
||||
// Get all individual stores
|
||||
const gameStore = useGameStore();
|
||||
const skillState = useSkillStore();
|
||||
const manaState = useManaStore();
|
||||
const prestigeState = usePrestigeStore();
|
||||
const uiState = useUIStore();
|
||||
const combatState = useCombatStore();
|
||||
|
||||
|
||||
// Create unified store object for backward compatibility
|
||||
const unifiedStore = useMemo(
|
||||
() => createUnifiedStore(gameStore, skillState, manaState, prestigeState, uiState, combatState),
|
||||
[gameStore, skillState, manaState, prestigeState, uiState, combatState]
|
||||
() => createUnifiedStore(gameStore, manaState, prestigeState, uiState, combatState),
|
||||
[gameStore, manaState, prestigeState, uiState, combatState]
|
||||
);
|
||||
|
||||
|
||||
// Computed effects from upgrades
|
||||
const upgradeEffects = useMemo(
|
||||
() => computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {}),
|
||||
[skillState.skillUpgrades, skillState.skillTiers]
|
||||
() => computeEffects({}, {}),
|
||||
[]
|
||||
);
|
||||
|
||||
|
||||
// Create a minimal state object for compute functions
|
||||
const stateForCompute = useMemo(() => ({
|
||||
skills: skillState.skills,
|
||||
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
||||
skillUpgrades: skillState.skillUpgrades,
|
||||
skillTiers: skillState.skillTiers,
|
||||
signedPacts: prestigeState.signedPacts,
|
||||
rawMana: manaState.rawMana,
|
||||
meditateTicks: manaState.meditateTicks,
|
||||
incursionStrength: gameStore.incursionStrength,
|
||||
}), [skillState, prestigeState, manaState, gameStore.incursionStrength]);
|
||||
|
||||
}), [prestigeState, manaState, gameStore.incursionStrength]);
|
||||
|
||||
// Derived stats
|
||||
const maxMana = useMemo(
|
||||
() => computeMaxMana(stateForCompute, upgradeEffects),
|
||||
[stateForCompute, upgradeEffects]
|
||||
);
|
||||
|
||||
|
||||
const baseRegen = useMemo(
|
||||
() => computeRegen(stateForCompute, upgradeEffects),
|
||||
[stateForCompute, upgradeEffects]
|
||||
);
|
||||
|
||||
|
||||
const clickMana = useMemo(() => computeClickMana(stateForCompute), [stateForCompute]);
|
||||
|
||||
|
||||
// Floor element from combat store
|
||||
const floorElem = useMemo(() => getFloorElement(combatState.currentFloor), [combatState.currentFloor]);
|
||||
const floorElemDef = ELEMENTS[floorElem];
|
||||
const isGuardianFloor = !!GUARDIANS[combatState.currentFloor];
|
||||
const currentGuardian = GUARDIANS[combatState.currentFloor];
|
||||
const activeSpellDef = SPELLS_DEF[combatState.activeSpell];
|
||||
|
||||
|
||||
const meditationMultiplier = useMemo(
|
||||
() => getMeditationBonus(manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency),
|
||||
[manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency]
|
||||
() => getMeditationBonus(manaState.meditateTicks, {}, upgradeEffects.meditationEfficiency),
|
||||
[manaState.meditateTicks, upgradeEffects.meditationEfficiency]
|
||||
);
|
||||
|
||||
|
||||
const incursionStrength = useMemo(
|
||||
() => getIncursionStrength(gameStore.day, gameStore.hour),
|
||||
[gameStore.day, gameStore.hour]
|
||||
);
|
||||
|
||||
const studySpeedMult = useMemo(
|
||||
() => getStudySpeedMultiplier(skillState.skills),
|
||||
[skillState.skills]
|
||||
);
|
||||
|
||||
const studyCostMult = useMemo(
|
||||
() => getStudyCostMultiplier(skillState.skills),
|
||||
[skillState.skills]
|
||||
);
|
||||
|
||||
|
||||
const studySpeedMult = 1;
|
||||
|
||||
const studyCostMult = 1;
|
||||
|
||||
// Effective regen calculations
|
||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||||
|
||||
|
||||
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
||||
? Math.floor(maxMana / 100) * 0.1
|
||||
: 0;
|
||||
|
||||
|
||||
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
||||
? Math.floor(maxMana / 100) * 0.25
|
||||
: 0;
|
||||
|
||||
|
||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
||||
|
||||
|
||||
// Has special flags for UI
|
||||
const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL);
|
||||
const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE);
|
||||
const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW);
|
||||
const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW);
|
||||
|
||||
|
||||
// Active boons
|
||||
const activeBoons = useMemo(
|
||||
() => getBoonBonuses(prestigeState.signedPacts),
|
||||
[prestigeState.signedPacts]
|
||||
);
|
||||
|
||||
|
||||
// DPS calculation - based on active spell, attack speed, and damage
|
||||
const dps = useMemo(() => {
|
||||
if (!activeSpellDef) return 0;
|
||||
const baseDmg = calcDamage(
|
||||
{ skills: skillState.skills, signedPacts: prestigeState.signedPacts },
|
||||
{ signedPacts: prestigeState.signedPacts },
|
||||
combatState.activeSpell,
|
||||
floorElem
|
||||
);
|
||||
const dmgWithEffects = baseDmg * upgradeEffects.baseDamageMultiplier + upgradeEffects.baseDamageBonus;
|
||||
const attackSpeed = (1 + (skillState.skills.quickCast || 0) * 0.05) * upgradeEffects.attackSpeedMultiplier;
|
||||
const attackSpeed = (1 + 0 * 0.05) * upgradeEffects.attackSpeedMultiplier;
|
||||
const castSpeed = activeSpellDef.castSpeed || 1;
|
||||
return dmgWithEffects * attackSpeed * castSpeed;
|
||||
}, [activeSpellDef, skillState.skills, prestigeState.signedPacts, floorElem, upgradeEffects, combatState.activeSpell]);
|
||||
|
||||
}, [activeSpellDef, prestigeState.signedPacts, floorElem, upgradeEffects, combatState.activeSpell]);
|
||||
|
||||
// Helper functions
|
||||
const canCastSpell = (spellId: string): boolean => {
|
||||
const spell = SPELLS_DEF[spellId];
|
||||
if (!spell) return false;
|
||||
return canAffordSpellCost(spell.cost, manaState.rawMana, manaState.elements);
|
||||
};
|
||||
|
||||
|
||||
const value: GameContextValue = {
|
||||
store: unifiedStore,
|
||||
skillStore: skillState,
|
||||
manaStore: manaState,
|
||||
prestigeStore: prestigeState,
|
||||
uiStore: uiState,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ElementDef, GuardianDef, SpellDef, GameAction } from '@/lib/game/types';
|
||||
import { useSkillStore } from '@/lib/game/stores/skillStore';
|
||||
import { useManaStore } from '@/lib/game/stores/manaStore';
|
||||
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
||||
import { useUIStore } from '@/lib/game/stores/uiStore';
|
||||
@@ -20,7 +19,7 @@ export interface UnifiedStore {
|
||||
resetGame: () => void;
|
||||
gatherMana: () => void;
|
||||
startNewLoop: () => void;
|
||||
|
||||
|
||||
// From manaStore
|
||||
rawMana: number;
|
||||
meditateTicks: number;
|
||||
@@ -32,34 +31,7 @@ export interface UnifiedStore {
|
||||
convertMana: (element: string, amount: number) => boolean;
|
||||
unlockElement: (element: string, cost: number) => boolean;
|
||||
craftComposite: (target: string, recipe: string[]) => boolean;
|
||||
|
||||
// From skillStore
|
||||
skills: Record<string, number>;
|
||||
skillProgress: Record<string, number>;
|
||||
skillUpgrades: Record<string, string[]>;
|
||||
skillTiers: Record<string, number>;
|
||||
paidStudySkills: Record<string, number>;
|
||||
currentStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
|
||||
parallelStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
|
||||
setSkillLevel: (skillId: string, level: number) => void;
|
||||
startStudyingSkill: (skillId: string, rawMana: number) => { started: boolean; cost: number };
|
||||
startStudyingSpell: (spellId: string, rawMana: number, studyTime: number) => { started: boolean; cost: number };
|
||||
cancelStudy: (retentionBonus: number) => void;
|
||||
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
||||
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
||||
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void;
|
||||
tierUpSkill: (skillId: string) => void;
|
||||
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => {
|
||||
available: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
milestone: 5 | 10;
|
||||
effect: { type: string; stat?: string; value?: number; specialId?: string }
|
||||
}>;
|
||||
selected: string[]
|
||||
};
|
||||
|
||||
|
||||
// From prestigeStore
|
||||
loopCount: number;
|
||||
insight: number;
|
||||
@@ -81,7 +53,7 @@ export interface UnifiedStore {
|
||||
cancelPactRitual: () => void;
|
||||
removePact: (floor: number) => void;
|
||||
defeatGuardian: (floor: number) => void;
|
||||
|
||||
|
||||
// From combatStore
|
||||
currentFloor: number;
|
||||
floorHP: number;
|
||||
@@ -95,7 +67,7 @@ export interface UnifiedStore {
|
||||
setSpell: (spellId: string) => void;
|
||||
learnSpell: (spellId: string) => void;
|
||||
advanceFloor: () => void;
|
||||
|
||||
|
||||
// From uiStore
|
||||
log: string[];
|
||||
paused: boolean;
|
||||
@@ -110,17 +82,16 @@ export interface UnifiedStore {
|
||||
export interface GameContextValue {
|
||||
// Unified store for backward compatibility
|
||||
store: UnifiedStore;
|
||||
|
||||
|
||||
// Individual stores for direct access if needed
|
||||
skillStore: ReturnType<typeof useSkillStore.getState>;
|
||||
manaStore: ReturnType<typeof useManaStore.getState>;
|
||||
prestigeStore: ReturnType<typeof usePrestigeStore.getState>;
|
||||
uiStore: ReturnType<typeof useUIStore.getState>;
|
||||
combatStore: ReturnType<typeof useCombatStore.getState>;
|
||||
|
||||
|
||||
// Computed effects from upgrades
|
||||
upgradeEffects: ReturnType<typeof computeEffects>;
|
||||
|
||||
|
||||
// Derived stats
|
||||
maxMana: number;
|
||||
baseRegen: number;
|
||||
@@ -134,25 +105,25 @@ export interface GameContextValue {
|
||||
incursionStrength: number;
|
||||
studySpeedMult: number;
|
||||
studyCostMult: number;
|
||||
|
||||
|
||||
// Effective regen calculations
|
||||
effectiveRegenWithSpecials: number;
|
||||
manaCascadeBonus: number;
|
||||
manaWaterfallBonus: number;
|
||||
effectiveRegen: number;
|
||||
|
||||
|
||||
// Has special flags
|
||||
hasManaWaterfall: boolean;
|
||||
hasFlowSurge: boolean;
|
||||
hasManaOverflow: boolean;
|
||||
hasEternalFlow: boolean;
|
||||
|
||||
|
||||
// DPS calculation
|
||||
dps: number;
|
||||
|
||||
|
||||
// Boons
|
||||
activeBoons: ReturnType<typeof getBoonBonuses>;
|
||||
|
||||
|
||||
// Helpers
|
||||
canCastSpell: (spellId: string) => boolean;
|
||||
hasSpecial: (effects: ReturnType<typeof computeEffects>, specialId: string) => boolean;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { canAffordSpellCost, fmt } from '@/lib/game/stores';
|
||||
import { useCombatStore, useSkillStore, useManaStore } from '@/lib/game/stores';
|
||||
import { useCombatStore, useManaStore } from '@/lib/game/stores';
|
||||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
|
||||
// Format spell cost for display
|
||||
function formatSpellCost(cost: { type: 'raw' | 'element'; element?: string; amount: number }): string {
|
||||
@@ -26,33 +24,24 @@ function getSpellCostColor(cost: { type: 'raw' | 'element'; element?: string; am
|
||||
return ELEMENTS[cost.element || '']?.color || '#9CA3AF';
|
||||
}
|
||||
|
||||
// Format study time
|
||||
function formatStudyTime(hours: number): string {
|
||||
if (hours < 1) return `${Math.round(hours * 60)}m`;
|
||||
return `${hours.toFixed(1)}h`;
|
||||
}
|
||||
|
||||
export function SpellsTab() {
|
||||
const spells = useCombatStore((s) => s.spells);
|
||||
const activeSpell = useCombatStore((s) => s.activeSpell);
|
||||
const setSpell = useCombatStore((s) => s.setSpell);
|
||||
const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
|
||||
const setCurrentStudyTarget = useSkillStore((s) => s.setCurrentStudyTarget);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const { studySpeedMult, studyCostMult } = useStudyStats();
|
||||
|
||||
|
||||
const spellTiers = [0, 1, 2, 3, 4];
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{spellTiers.map(tier => {
|
||||
const spellsInTier = Object.entries(SPELLS_DEF).filter(([, def]) => def.tier === tier);
|
||||
if (spellsInTier.length === 0) return null;
|
||||
|
||||
|
||||
const tierNames = ['Basic Spells (Raw Mana)', 'Tier 1 - Elemental', 'Tier 2 - Advanced', 'Tier 3 - Master', 'Tier 4 - Legendary'];
|
||||
const tierColors = ['text-gray-400', 'text-green-400', 'text-blue-400', 'text-purple-400', 'text-amber-400'];
|
||||
|
||||
|
||||
return (
|
||||
<div key={tier}>
|
||||
<h3 className={`text-lg font-semibold mb-3 ${tierColors[tier]}`}>{tierNames[tier]}</h3>
|
||||
@@ -60,23 +49,14 @@ export function SpellsTab() {
|
||||
{spellsInTier.map(([id, def]) => {
|
||||
const state = spells?.[id];
|
||||
const learned = state?.learned;
|
||||
const isStudying = currentStudyTarget?.id === id;
|
||||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
||||
const baseStudyTime = def.studyTime || (def.tier * 4);
|
||||
const isActive = activeSpell === id;
|
||||
const canCast = learned && canAffordSpellCost(def.cost, rawMana, elements);
|
||||
|
||||
// Apply skill modifiers
|
||||
const studyTime = baseStudyTime / studySpeedMult;
|
||||
const unlockCost = Math.floor(def.unlock * studyCostMult);
|
||||
|
||||
// Can start studying?
|
||||
const canStudy = !learned && !isStudying && rawMana >= unlockCost;
|
||||
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={id}
|
||||
className={`bg-gray-900/80 border-gray-700 ${learned ? '' : 'opacity-75'} ${isStudying ? 'border-purple-500' : ''} ${canCast ? 'ring-1 ring-green-500/30' : ''}`}
|
||||
className={`bg-gray-900/80 border-gray-700 ${learned ? '' : 'opacity-75'} ${canCast ? 'ring-1 ring-green-500/30' : ''}`}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -95,16 +75,16 @@ export function SpellsTab() {
|
||||
{def.elem !== 'raw' && <span className="mr-2">{elemDef?.sym} {elemDef?.name}</span>}
|
||||
<span className="mr-2">⚔️ {def.dmg} dmg</span>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Cost display */}
|
||||
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
|
||||
Cost: {formatSpellCost(def.cost)}
|
||||
</div>
|
||||
|
||||
|
||||
{def.desc && (
|
||||
<div className="text-xs text-gray-500 italic">{def.desc}</div>
|
||||
)}
|
||||
|
||||
|
||||
{def.effects && Array.isArray(def.effects) && def.effects.length > 0 && (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{def.effects.map((eff, i) => (
|
||||
@@ -117,7 +97,7 @@ export function SpellsTab() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{learned ? (
|
||||
<div className="flex gap-2">
|
||||
<Badge className="bg-green-900/50 text-green-300">Learned</Badge>
|
||||
@@ -128,36 +108,9 @@ export function SpellsTab() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : isStudying ? (
|
||||
<div className="space-y-1">
|
||||
<Progress
|
||||
value={Math.min(100, ((state?.studyProgress || 0) / studyTime) * 100)}
|
||||
className="h-2 bg-gray-800"
|
||||
/>
|
||||
<div className="text-xs text-purple-400">
|
||||
Studying... {formatStudyTime(state?.studyProgress || 0)}/{formatStudyTime(studyTime)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className={studySpeedMult > 1 ? 'text-green-400' : ''}>
|
||||
Study: {formatStudyTime(studyTime)}{studySpeedMult > 1 && <span className="text-xs ml-1">({Math.round(studySpeedMult * 100)}% speed)</span>}
|
||||
</span>
|
||||
{' • '}
|
||||
<span className={studyCostMult < 1 ? 'text-green-400' : ''}>
|
||||
Cost: {fmt(unlockCost)} mana{studyCostMult < 1 && <span className="text-xs ml-1">({Math.round(studyCostMult * 100)}% cost)</span>}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={canStudy ? 'default' : 'outline'}
|
||||
disabled={!canStudy}
|
||||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||||
onClick={() => setCurrentStudyTarget({ type: 'spell', id, progress: 0, required: studyTime })}
|
||||
>
|
||||
Start Study ({fmt(unlockCost)} mana)
|
||||
</Button>
|
||||
<div className="text-xs text-gray-500">
|
||||
Not yet learned
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,59 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useSkillStore, usePrestigeStore, fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { usePrestigeStore, fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||
import { ManaStatsSection } from './StatsTab/ManaStatsSection';
|
||||
import { CombatStatsSection } from './StatsTab/CombatStatsSection';
|
||||
import { PactStatusSection } from './StatsTab/PactStatusSection';
|
||||
import { StudyStatsSection } from './StatsTab/StudyStatsSection';
|
||||
import { ElementStatsSection } from './StatsTab/ElementStatsSection';
|
||||
import { ActiveUpgradesSection } from './StatsTab/ActiveUpgradesSection';
|
||||
import { LoopStatsSection } from './StatsTab/LoopStatsSection';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
|
||||
export function StatsTab() {
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
|
||||
|
||||
const manaStats = useManaStats();
|
||||
const combatStats = useCombatStats();
|
||||
const studyStats = useStudyStats();
|
||||
|
||||
// Compute element max
|
||||
const elemMax = (() => {
|
||||
const ea = skillTiers?.elemAttune || 1;
|
||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||||
const level = skills[tieredSkillId] || skills.elemAttune || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return 10 + level * 50 * tierMult + (prestigeUpgrades.elementalAttune || 0) * 25;
|
||||
})();
|
||||
|
||||
// Get all selected skill upgrades
|
||||
const getAllSelectedUpgrades = (): { skillId: string; upgrade: SkillUpgradeChoice }[] => {
|
||||
const upgrades: { skillId: string; upgrade: SkillUpgradeChoice }[] = [];
|
||||
for (const [skillId, selectedIds] of Object.entries(skillUpgrades)) {
|
||||
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
||||
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
|
||||
if (!path) continue;
|
||||
for (const tier of path.tiers) {
|
||||
if (tier.skillId === skillId) {
|
||||
for (const upgradeId of selectedIds) {
|
||||
const upgrade = (tier as any).upgrades?.find((u: any) => u.id === upgradeId);
|
||||
if (upgrade) {
|
||||
upgrades.push({ skillId, upgrade });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return upgrades;
|
||||
};
|
||||
|
||||
const selectedUpgrades = getAllSelectedUpgrades();
|
||||
// Compute element max (base + prestige only)
|
||||
const elemMax = 10 + (prestigeUpgrades.elementalAttune || 0) * 25;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -65,7 +30,6 @@ export function StatsTab() {
|
||||
meditationMultiplier={manaStats.meditationMultiplier}
|
||||
upgradeEffects={manaStats.upgradeEffects}
|
||||
elemMax={elemMax}
|
||||
selectedUpgrades={selectedUpgrades}
|
||||
/>
|
||||
<CombatStatsSection
|
||||
activeSpellDef={combatStats.activeSpellDef}
|
||||
@@ -82,7 +46,6 @@ export function StatsTab() {
|
||||
<ElementStatsSection
|
||||
elemMax={elemMax}
|
||||
/>
|
||||
<ActiveUpgradesSection selectedUpgrades={selectedUpgrades} />
|
||||
<LoopStatsSection />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Star } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
|
||||
interface ActiveUpgradesSectionProps {
|
||||
selectedUpgrades: { skillId: string; upgrade: SkillUpgradeChoice }[];
|
||||
}
|
||||
|
||||
export function ActiveUpgradesSection({ selectedUpgrades }: ActiveUpgradesSectionProps) {
|
||||
if (selectedUpgrades.length === 0) {
|
||||
return (
|
||||
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-[var(--mana-light)] game-panel-title text-xs flex items-center gap-2">
|
||||
<Star className="w-4 h-4" />
|
||||
Active Skill Upgrades (0)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div style={{ color: 'var(--text-muted)' }} className="text-sm">No skill upgrades selected yet. Level skills to 5 or 10 to choose upgrades.</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-[var(--mana-light)] game-panel-title text-xs flex items-center gap-2">
|
||||
<Star className="w-4 h-4" />
|
||||
Active Skill Upgrades ({selectedUpgrades.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{selectedUpgrades.map(({ skillId, upgrade }) => (
|
||||
<div key={upgrade.id} className="p-2 rounded transition-colors" style={{ border: '1px solid var(--mana-light)/30', background: 'var(--mana-light)/10' }}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span style={{ color: 'var(--mana-light)' }} className="text-sm font-semibold">{upgrade.name}</span>
|
||||
<Badge variant="outline" className="text-xs" style={{ color: 'var(--text-muted)', borderColor: 'var(--border-subtle)' }}>
|
||||
{SKILLS_DEF[skillId]?.name || skillId}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{upgrade.desc}</div>
|
||||
{upgrade.effect.type === 'multiplier' && (
|
||||
<div className="text-xs mt-1" style={{ color: 'var(--color-success)' }}>
|
||||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'bonus' && (
|
||||
<div className="text-xs mt-1" style={{ color: 'var(--mana-water)' }}>
|
||||
+{upgrade.effect.value} {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'special' && (
|
||||
<div className="text-xs mt-1" style={{ color: 'var(--mana-crystal)' }}>
|
||||
⚡ {upgrade.effect.specialDesc || 'Special effect active'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Swords } from 'lucide-react';
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
|
||||
interface CombatStatsSectionProps {
|
||||
activeSpellDef: any;
|
||||
@@ -12,17 +10,6 @@ interface CombatStatsSectionProps {
|
||||
}
|
||||
|
||||
export function CombatStatsSection({ activeSpellDef, pactMultiplier }: CombatStatsSectionProps) {
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
|
||||
const upgradeEffects = getUnifiedEffects({
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
equippedInstances: {},
|
||||
equipmentInstances: {},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -38,36 +25,6 @@ export function CombatStatsSection({ activeSpellDef, pactMultiplier }: CombatSta
|
||||
<span style={{ color: 'var(--text-muted)' }}>Active Spell Base Damage:</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>{activeSpellDef?.dmg || 5}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Combat Training Bonus:</span>
|
||||
<span style={{ color: 'var(--mana-fire)' }}>+{(skills.combatTrain || 0) * 5}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Arcane Fury Multiplier:</span>
|
||||
<span style={{ color: 'var(--mana-fire)' }}>×{fmtDec(1 + (skills.arcaneFury || 0) * 0.1, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Elemental Mastery:</span>
|
||||
<span style={{ color: 'var(--mana-fire)' }}>×{fmtDec(1 + (skills.elementalMastery || 0) * 0.15, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Guardian Bane:</span>
|
||||
<span style={{ color: 'var(--mana-fire)' }}>×{fmtDec(1 + (skills.guardianBane || 0) * 0.2, 2)} (vs guardians)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Critical Hit Chance:</span>
|
||||
<span style={{ color: 'var(--mana-light)' }}>{((skills.precision || 0) * 5)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Critical Multiplier:</span>
|
||||
<span style={{ color: 'var(--mana-light)' }}>1.5x</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Spell Echo Chance:</span>
|
||||
<span style={{ color: 'var(--mana-light)' }}>{((skills.spellEcho || 0) * 10)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Pact Multiplier:</span>
|
||||
<span style={{ color: 'var(--mana-light)' }}>×{fmtDec(pactMultiplier, 2)}</span>
|
||||
@@ -77,8 +34,14 @@ export function CombatStatsSection({ activeSpellDef, pactMultiplier }: CombatSta
|
||||
<span style={{ color: 'var(--mana-fire)' }}>{fmt(activeSpellDef ? activeSpellDef.dmg * pactMultiplier : 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Critical Multiplier:</span>
|
||||
<span style={{ color: 'var(--mana-light)' }}>1.5x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,17 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { FlaskConical } from 'lucide-react';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { useSkillStore, usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
||||
import { usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
||||
|
||||
interface ElementStatsSectionProps {
|
||||
elemMax: number;
|
||||
}
|
||||
|
||||
export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
|
||||
const getElemAttunementBonus = () => {
|
||||
const ea = skillTiers?.elemAttune || 1;
|
||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||||
const level = skills[tieredSkillId] || skills.elemAttune || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return level * 50 * tierMult;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -41,10 +30,6 @@ export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
|
||||
<span style={{ color: 'var(--text-muted)' }}>Element Capacity:</span>
|
||||
<span style={{ color: 'var(--color-success)' }}>{elemMax}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Elem. Attunement Bonus:</span>
|
||||
<span style={{ color: 'var(--color-success)' }}>+{getElemAttunementBonus()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Prestige Attunement:</span>
|
||||
<span style={{ color: 'var(--color-success)' }}>+{(prestigeUpgrades.elementalAttune || 0) * 25}</span>
|
||||
@@ -55,10 +40,6 @@ export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
|
||||
<span style={{ color: 'var(--text-muted)' }}>Unlocked Elements:</span>
|
||||
<span style={{ color: 'var(--color-success)' }}>{Object.values(elements || {}).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Elem. Crafting Bonus:</span>
|
||||
<span style={{ color: 'var(--color-success)' }}>×{fmtDec(1 + (skills.elemCrafting || 0) * 0.25, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-[var(--border-subtle)] my-3" />
|
||||
@@ -79,4 +60,4 @@ export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import { useCombatStore, usePrestigeStore, useManaStore, useSkillStore } from '@/lib/game/stores';
|
||||
import { useCombatStore, usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
||||
|
||||
export function LoopStatsSection() {
|
||||
const spells = useCombatStore((s) => s.spells);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const insight = usePrestigeStore((s) => s.insight);
|
||||
const totalInsight = usePrestigeStore((s) => s.totalInsight);
|
||||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
||||
const totalManaGathered = useManaStore((s) => s.totalManaGathered);
|
||||
const loopCount = usePrestigeStore((s) => s.loopCount);
|
||||
const memorySlots = usePrestigeStore((s) => s.memorySlots);
|
||||
|
||||
|
||||
const spellsLearned = Object.values(spells || {}).filter((s: any) => s.learned).length;
|
||||
const totalSkillLevels = Object.values(skills || {}).reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -52,10 +50,6 @@ export function LoopStatsSection() {
|
||||
<div className="text-xl font-bold text-[var(--text-secondary)] game-mono">{spellsLearned}</div>
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>Spells Learned</div>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-[var(--text-secondary)] game-mono">{totalSkillLevels}</div>
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>Total Skill Levels</div>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-[var(--text-secondary)] game-mono">{fmt(totalManaGathered)}</div>
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>Total Mana Gathered</div>
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Droplet } from 'lucide-react';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { useSkillStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
|
||||
interface ManaStatsSectionProps {
|
||||
maxMana: number;
|
||||
@@ -14,7 +12,6 @@ interface ManaStatsSectionProps {
|
||||
meditationMultiplier: number;
|
||||
upgradeEffects: any;
|
||||
elemMax: number;
|
||||
selectedUpgrades: { skillId: string; upgrade: SkillUpgradeChoice }[];
|
||||
}
|
||||
|
||||
export function ManaStatsSection({
|
||||
@@ -25,16 +22,7 @@ export function ManaStatsSection({
|
||||
meditationMultiplier,
|
||||
upgradeEffects,
|
||||
elemMax,
|
||||
selectedUpgrades,
|
||||
}: ManaStatsSectionProps) {
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
|
||||
const getTierMultiplier = (skillId: string) => {
|
||||
return 1;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -50,22 +38,6 @@ export function ManaStatsSection({
|
||||
<span style={{ color: 'var(--text-muted)' }}>Base Max Mana:</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>100</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Mana Well Bonus:</span>
|
||||
<span style={{ color: 'var(--mana-water)' }}>
|
||||
{(() => {
|
||||
const mw = skillTiers?.manaWell || 1;
|
||||
const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell';
|
||||
const level = (skills || {})[tieredSkillId] || (skills || {}).manaWell || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${fmt(level * 100 * tierMult)} (${level} lvl × 100 × ${tierMult}x tier)`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Prestige Mana Well:</span>
|
||||
<span style={{ color: 'var(--mana-water)' }}>+{fmt((prestigeUpgrades.manaWell || 0) * 500)}</span>
|
||||
</div>
|
||||
{upgradeEffects.maxManaBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--mana-light)' }}>Upgrade Mana Bonus:</span>
|
||||
@@ -88,30 +60,6 @@ export function ManaStatsSection({
|
||||
<span style={{ color: 'var(--text-muted)' }}>Base Regen:</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>2/hr</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Mana Flow Bonus:</span>
|
||||
<span style={{ color: 'var(--mana-water)' }}>
|
||||
{(() => {
|
||||
const mf = skillTiers?.manaFlow || 1;
|
||||
const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow';
|
||||
const level = skills[tieredSkillId] || skills.manaFlow || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${fmtDec(level * 1 * tierMult, 2)}/hr (${level} lvl × 1 × ${tierMult}x tier)`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Mana Spring Bonus:</span>
|
||||
<span style={{ color: 'var(--mana-water)' }}>+{(skills.manaSpring || 0) * 2}/hr</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Prestige Mana Flow:</span>
|
||||
<span style={{ color: 'var(--mana-water)' }}>+{fmtDec((prestigeUpgrades.manaFlow || 0) * 0.5)}/hr</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Temporal Echo:</span>
|
||||
<span style={{ color: 'var(--mana-water)' }}>×{fmtDec(1 + (prestigeUpgrades.temporalEcho || 0) * 0.1, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm font-semibold border-t border-[var(--border-subtle)] pt-2">
|
||||
<span style={{ color: 'var(--text-secondary)' }}>Base Regen:</span>
|
||||
<span style={{ color: 'var(--mana-water)' }}>{fmtDec(baseRegen, 2)}/hr</span>
|
||||
@@ -142,18 +90,6 @@ export function ManaStatsSection({
|
||||
<span style={{ color: 'var(--text-muted)' }}>Click Mana Value:</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>+{clickMana}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Mana Tap Bonus:</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>+{skills.manaTap || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Mana Surge Bonus:</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>+{(skills.manaSurge || 0) * 3}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Mana Overflow:</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>×{fmtDec(1 + (skills.manaOverflow || 0) * 0.25, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
@@ -173,7 +109,7 @@ export function ManaStatsSection({
|
||||
</div>
|
||||
</div>
|
||||
{/* Special Effects */}
|
||||
{(upgradeEffects.hasSteadyStream || upgradeEffects.hasManaTorrent ||
|
||||
{(upgradeEffects.hasSteadyStream || upgradeEffects.hasManaTorrent ||
|
||||
upgradeEffects.hasDesperateWells || upgradeEffects.manaCascadeBonus > 0 ||
|
||||
upgradeEffects.manaWaterfallBonus > 0) && (
|
||||
<>
|
||||
@@ -240,4 +176,4 @@ export function ManaStatsSection({
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import { fmtDec } from '@/lib/game/stores';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
|
||||
interface StudyStatsSectionProps {
|
||||
studySpeedMult: number;
|
||||
@@ -11,8 +10,6 @@ interface StudyStatsSectionProps {
|
||||
}
|
||||
|
||||
export function StudyStatsSection({ studySpeedMult, studyCostMult }: StudyStatsSectionProps) {
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -28,29 +25,21 @@ export function StudyStatsSection({ studySpeedMult, studyCostMult }: StudyStatsS
|
||||
<span style={{ color: 'var(--text-muted)' }}>Study Speed:</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>×{fmtDec(studySpeedMult, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Quick Learner Bonus:</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>+{((skills.quickLearner || 0) * 10)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Study Cost:</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>{Math.round(studyCostMult * 100)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Focused Mind Bonus:</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>-{((skills.focusedMind || 0) * 5)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Progress Retention:</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>{Math.round((1 + (skills.knowledgeRetention || 0) * 0.2) * 100)}%</span>
|
||||
<span style={{ color: 'var(--mana-crystal)' }}>100%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { BookOpen, X } from 'lucide-react';
|
||||
import { SKILLS_DEF, SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { formatStudyTime } from '@/lib/game/formatting';
|
||||
import { formatStudyTime } from '@/lib/game/utils/formatting';
|
||||
import type { StudyTarget } from '@/lib/game/types';
|
||||
|
||||
interface StudyProgressProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import { formatHour } from '@/lib/game/formatting';
|
||||
import { formatHour } from '@/lib/game/utils/formatting';
|
||||
|
||||
interface TimeDisplayProps {
|
||||
day: number;
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
removeEffectFromDesign,
|
||||
} from './EnchantmentDesigner/utils';
|
||||
import { useCraftingStore } from '@/lib/game/stores';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
|
||||
export function EnchantmentDesigner({
|
||||
selectedEquipmentType,
|
||||
@@ -44,15 +43,8 @@ export function EnchantmentDesigner({
|
||||
const unlockedEffects = useCraftingStore((s) => s.unlockedEffects);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
|
||||
// Skill store selectors
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
|
||||
const enchantingLevel = skills?.enchanting || 0;
|
||||
const efficiencyBonus = (skillUpgrades?.['efficientEnchant'] || []).length * 0.05 || 0;
|
||||
|
||||
// Calculate total capacity cost for current design
|
||||
const designCapacityCost = calculateDesignCapacityCost(selectedEffects, efficiencyBonus);
|
||||
const designCapacityCost = calculateDesignCapacityCost(selectedEffects, 0);
|
||||
|
||||
// Get capacity limit for selected equipment type
|
||||
const selectedEquipmentCapacity = getEquipmentCapacity(selectedEquipmentType);
|
||||
@@ -62,7 +54,7 @@ export function EnchantmentDesigner({
|
||||
|
||||
// Add effect to design
|
||||
const addEffect = (effectId: string) => {
|
||||
addEffectToDesign(effectId, selectedEffects, efficiencyBonus, setSelectedEffects);
|
||||
addEffectToDesign(effectId, selectedEffects, 0, setSelectedEffects);
|
||||
};
|
||||
|
||||
// Remove effect from design
|
||||
@@ -117,8 +109,8 @@ export function EnchantmentDesigner({
|
||||
setSelectedEffects={setSelectedEffects}
|
||||
availableEffects={availableEffects}
|
||||
incompatibleEffects={incompatibleEffects}
|
||||
enchantingLevel={enchantingLevel}
|
||||
efficiencyBonus={efficiencyBonus}
|
||||
enchantingLevel={0}
|
||||
efficiencyBonus={0}
|
||||
designProgress={designProgress}
|
||||
addEffect={addEffect}
|
||||
removeEffect={removeEffect}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||
import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
|
||||
import type { EquipmentSlot } from '@/lib/game/types';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import { useGameStore, useCraftingStore, useManaStore, useSkillStore } from '@/lib/game/stores';
|
||||
import { useGameStore, useCraftingStore, useManaStore } from '@/lib/game/stores';
|
||||
import { useGameToast } from '@/components/game/GameToast';
|
||||
|
||||
export interface EnchantmentPreparerProps {
|
||||
@@ -31,7 +31,6 @@ export function EnchantmentPreparer({
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
const preparationProgress = useCraftingStore((s) => s.preparationProgress);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const startPreparing = useCraftingStore((s) => s.startPreparing);
|
||||
const cancelPreparation = useCraftingStore((s) => s.cancelPreparation);
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
// Re-exports all game tab components for cleaner imports
|
||||
|
||||
// Tab components
|
||||
export { CraftingTab } from './tabs/CraftingTab';
|
||||
export { SpireTab } from './tabs/SpireTab';
|
||||
export { SpellsTab } from './tabs/SpellsTab';
|
||||
export { SkillsTab } from './SkillsTab';
|
||||
export { StatsTab } from './tabs/StatsTab';
|
||||
export { CraftingTab } from './crafting';
|
||||
export { SpellsTab } from './SpellsTab';
|
||||
export { StatsTab } from './StatsTab';
|
||||
|
||||
// UI components
|
||||
export { ActionButtons } from './ActionButtons';
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import { formatHour } from '@/lib/game/formatting';
|
||||
import { TimeDisplay } from '@/components/game/TimeDisplay';
|
||||
|
||||
interface HeaderProps {
|
||||
day: number;
|
||||
hour: number;
|
||||
insight: number;
|
||||
}
|
||||
|
||||
export function Header({ day, hour, insight }: HeaderProps) {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-[var(--bg-surface)]/95 backdrop-blur-sm border-b border-[var(--border-subtle)] px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Game Title - always visible */}
|
||||
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
|
||||
|
||||
{/* Desktop header content */}
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
<TimeDisplay
|
||||
day={day}
|
||||
hour={hour}
|
||||
insight={insight}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile header content - compact */}
|
||||
<div className="flex md:hidden items-center gap-2">
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-bold game-mono text-[var(--mana-light)]">
|
||||
D{day} {formatHour(hour)}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-secondary)]">
|
||||
{fmt(insight)} 💎
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Header.displayName = "Header";
|
||||
@@ -1,142 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import {
|
||||
Mountain,
|
||||
Sparkles,
|
||||
Brain,
|
||||
Wand2,
|
||||
Bone,
|
||||
Shield,
|
||||
Hammer,
|
||||
Gem,
|
||||
Trophy,
|
||||
FlaskConical,
|
||||
BarChart3,
|
||||
BookOpen,
|
||||
Wrench
|
||||
} from 'lucide-react';
|
||||
|
||||
interface TabBarProps {
|
||||
activeTab: string;
|
||||
onTabChange: (value: string) => void;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
// Tab configuration with groups
|
||||
const TAB_GROUPS = [
|
||||
{
|
||||
name: 'World',
|
||||
tabs: [
|
||||
{ value: 'spire', label: 'Spire', icon: Mountain, mobileLabel: 'Spire' },
|
||||
{ value: 'attunements', label: 'Attune', icon: Sparkles, mobileLabel: 'Attune' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Power',
|
||||
tabs: [
|
||||
{ value: 'skills', label: 'Skills', icon: Brain, mobileLabel: 'Skills' },
|
||||
{ value: 'spells', label: 'Spells', icon: Wand2, mobileLabel: 'Spells' },
|
||||
{ value: 'golemancy', label: 'Golems', icon: Bone, mobileLabel: 'Golems' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Gear',
|
||||
tabs: [
|
||||
{ value: 'equipment', label: 'Gear', icon: Shield, mobileLabel: 'Gear' },
|
||||
{ value: 'crafting', label: 'Craft', icon: Hammer, mobileLabel: 'Craft' },
|
||||
{ value: 'loot', label: 'Loot', icon: Gem, mobileLabel: 'Loot' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Meta',
|
||||
tabs: [
|
||||
{ value: 'achievements', label: 'Achieve', icon: Trophy, mobileLabel: 'Achieve' },
|
||||
{ value: 'stats', label: 'Stats', icon: BarChart3, mobileLabel: 'Stats' },
|
||||
{ value: 'debug', label: 'Debug', icon: Wrench, mobileLabel: 'Debug' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export function TabBar({ activeTab, onTabChange, isMobile = false }: TabBarProps) {
|
||||
if (isMobile) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex overflow-x-auto scrollbar-thin gap-1 pb-2" style={{ flexWrap: 'nowrap' }}>
|
||||
{TAB_GROUPS.map((group, groupIndex) => (
|
||||
<div key={group.name} className="flex items-center flex-shrink-0">
|
||||
{groupIndex > 0 && (
|
||||
<Separator orientation="vertical" className="h-6 mx-1 bg-[var(--border-subtle)]" />
|
||||
)}
|
||||
{group.tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.value;
|
||||
return (
|
||||
<Tooltip key={tab.value}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => onTabChange(tab.value)}
|
||||
className={`
|
||||
flex items-center justify-center p-2 flex-shrink-0 transition-all border text-[var(--font-display)]
|
||||
${isActive
|
||||
? 'border-[var(--border-accent)] bg-[var(--bg-raised)] text-[var(--interactive-primary)]'
|
||||
: 'border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-panel)] hover:border-[var(--border-subtle)]'
|
||||
}
|
||||
`}
|
||||
aria-label={tab.label}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{tab.label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop view - grouped tabs with separators
|
||||
return (
|
||||
<div className="flex items-center gap-1 w-full" style={{ flexWrap: 'nowrap' }}>
|
||||
{TAB_GROUPS.map((group, groupIndex) => (
|
||||
<div key={group.name} className="flex items-center flex-shrink-0">
|
||||
{groupIndex > 0 && (
|
||||
<Separator orientation="vertical" className="h-6 mx-2 bg-[var(--border-subtle)]" />
|
||||
)}
|
||||
{group.tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.value;
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className={`
|
||||
text-xs px-3 py-1.5 relative transition-all whitespace-nowrap text-[var(--font-display)] tracking-wider
|
||||
${isActive
|
||||
? 'text-[var(--interactive-primary)]'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
style={isActive ? {
|
||||
borderBottom: '2px solid var(--border-accent)',
|
||||
} : {}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TabBar.displayName = "TabBar";
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { BookOpen, X } from 'lucide-react';
|
||||
import { useGameContext } from '../GameContext';
|
||||
import { formatStudyTime } from '../types';
|
||||
import { SKILLS_DEF, SPELLS_DEF } from '@/lib/game/constants';
|
||||
|
||||
interface StudyProgressProps {
|
||||
target: NonNullable<ReturnType<typeof useGameContext>['store']['currentStudyTarget']>;
|
||||
showCancel?: boolean;
|
||||
speedLabel?: string;
|
||||
}
|
||||
|
||||
export function StudyProgress({ target, showCancel = true, speedLabel }: StudyProgressProps) {
|
||||
const { store, studySpeedMult } = useGameContext();
|
||||
|
||||
const progressPct = Math.min(100, (target.progress / target.required) * 100);
|
||||
const isSkill = target.type === 'skill';
|
||||
const def = isSkill ? SKILLS_DEF[target.id] : SPELLS_DEF[target.id];
|
||||
const currentLevel = isSkill ? store.skills[target.id] || 0 : 0;
|
||||
|
||||
const handleCancel = () => {
|
||||
// Calculate retention bonus from knowledge retention skill
|
||||
const retentionBonus = 0.2 * (store.skills.knowledgeRetention || 0);
|
||||
store.cancelStudy(retentionBonus);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-3 rounded border border-purple-600/50 bg-purple-900/20">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4 text-purple-400" />
|
||||
<span className="text-sm font-semibold text-purple-300">
|
||||
{def?.name}
|
||||
{isSkill && ` Lv.${currentLevel + 1}`}
|
||||
</span>
|
||||
</div>
|
||||
{showCancel && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||||
<span>
|
||||
{formatStudyTime(target.progress)} / {formatStudyTime(target.required)}
|
||||
</span>
|
||||
<span>{speedLabel ?? `${studySpeedMult.toFixed(1)}x speed`}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StudyProgress.displayName = "StudyProgress";
|
||||
@@ -1,128 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useGameContext } from '../GameContext';
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
|
||||
interface UpgradeDialogProps {
|
||||
skillId: string | null;
|
||||
milestone: 5 | 10;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function UpgradeDialog({ skillId, milestone, onClose }: UpgradeDialogProps) {
|
||||
const { store } = useGameContext();
|
||||
|
||||
const skillDef = skillId ? SKILLS_DEF[skillId] : null;
|
||||
const { available, selected: alreadySelected } = skillId
|
||||
? store.getSkillUpgradeChoices(skillId, milestone)
|
||||
: { available: [], selected: [] };
|
||||
|
||||
// Use local state for selections within this dialog session
|
||||
const [pendingSelections, setPendingSelections] = useState<string[]>(() => [...alreadySelected]);
|
||||
|
||||
const toggleUpgrade = (upgradeId: string) => {
|
||||
setPendingSelections((prev) => {
|
||||
if (prev.includes(upgradeId)) {
|
||||
return prev.filter((id) => id !== upgradeId);
|
||||
} else if (prev.length < 2) {
|
||||
return [...prev, upgradeId];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDone = () => {
|
||||
if (pendingSelections.length === 2 && skillId) {
|
||||
store.commitSkillUpgrades(skillId, pendingSelections);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setPendingSelections([...alreadySelected]);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Don't render if no skill selected
|
||||
if (!skillId) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={!!skillId} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-amber-400">Choose Upgrade - {skillDef?.name || skillId}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Level {milestone} Milestone - Select 2 upgrades ({pendingSelections.length}/2 chosen)
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2 mt-4">
|
||||
{available.map((upgrade) => {
|
||||
const isSelected = pendingSelections.includes(upgrade.id);
|
||||
const canToggle = pendingSelections.length < 2 || isSelected;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={upgrade.id}
|
||||
className={`p-3 rounded border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? 'border-amber-500 bg-amber-900/30'
|
||||
: canToggle
|
||||
? 'border-gray-600 bg-gray-800/50 hover:border-amber-500/50 hover:bg-gray-800'
|
||||
: 'border-gray-700 bg-gray-800/30 opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (canToggle) {
|
||||
toggleUpgrade(upgrade.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-semibold text-sm text-amber-300">{upgrade.name}</div>
|
||||
{isSelected && <Badge className="bg-amber-600 text-amber-100">Selected</Badge>}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
||||
{upgrade.effect.type === 'multiplier' && (
|
||||
<div className="text-xs text-green-400 mt-1">
|
||||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'bonus' && (
|
||||
<div className="text-xs text-blue-400 mt-1">
|
||||
+{upgrade.effect.value} {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'special' && (
|
||||
<div className="text-xs text-cyan-400 mt-1">⚡ {upgrade.desc || 'Special effect'}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setPendingSelections([...alreadySelected]);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="default" onClick={handleDone} disabled={pendingSelections.length !== 2}>
|
||||
{pendingSelections.length < 2 ? `Select ${2 - pendingSelections.length} more` : 'Confirm'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
UpgradeDialog.displayName = "UpgradeDialog";
|
||||
@@ -1,67 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { fmtDec } from '@/lib/game/stores';
|
||||
import { GUARDIANS } from '@/lib/game/constants';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Swords } from 'lucide-react';
|
||||
|
||||
// Modular stores
|
||||
import { useSkillStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
|
||||
export function CombatStatsSection() {
|
||||
// Get state from modular stores
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
|
||||
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">
|
||||
<Swords className="w-4 h-4" />
|
||||
Combat Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Combat Training Bonus:</span>
|
||||
<span className="text-red-300">+{(skills.combatTrain || 0) * 5}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Arcane Fury Multiplier:</span>
|
||||
<span className="text-red-300">×{fmtDec(1 + (skills.arcaneFury || 0) * 0.1, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Elemental Mastery:</span>
|
||||
<span className="text-red-300">×{fmtDec(1 + (skills.elementalMastery || 0) * 0.15, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Guardian Bane:</span>
|
||||
<span className="text-red-300">×{fmtDec(1 + (skills.guardianBane || 0) * 0.2, 2)} (vs guardians)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Critical Hit Chance:</span>
|
||||
<span className="text-amber-300">{(skills.precision || 0) * 5}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Critical Multiplier:</span>
|
||||
<span className="text-amber-300">1.5x</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Spell Echo Chance:</span>
|
||||
<span className="text-amber-300">{(skills.spellEcho || 0) * 10}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-300">×{fmtDec(signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
CombatStatsSection.displayName = "CombatStatsSection";
|
||||
@@ -1,268 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import type { UnifiedEffects } from '@/lib/game/effects';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Droplet } from 'lucide-react';
|
||||
|
||||
// Modular stores
|
||||
import { useSkillStore, usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
||||
|
||||
export interface ManaStatsSectionProps {
|
||||
upgradeEffects: UnifiedEffects;
|
||||
maxMana: number;
|
||||
baseRegen: number;
|
||||
clickMana: number;
|
||||
meditationMultiplier: number;
|
||||
effectiveRegen: number;
|
||||
incursionStrength: number;
|
||||
manaCascadeBonus: number;
|
||||
manaWaterfallBonus: number;
|
||||
hasManaWaterfall: boolean;
|
||||
hasFlowSurge: boolean;
|
||||
hasManaOverflow: boolean;
|
||||
hasEternalFlow: boolean;
|
||||
}
|
||||
|
||||
export function ManaStatsSection({
|
||||
upgradeEffects,
|
||||
maxMana,
|
||||
baseRegen,
|
||||
clickMana,
|
||||
meditationMultiplier,
|
||||
effectiveRegen,
|
||||
incursionStrength,
|
||||
manaCascadeBonus,
|
||||
manaWaterfallBonus,
|
||||
hasManaWaterfall,
|
||||
hasFlowSurge,
|
||||
hasManaOverflow,
|
||||
hasEternalFlow,
|
||||
}: ManaStatsSectionProps) {
|
||||
// Get state from modular stores
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-blue-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<Droplet className="w-4 h-4" />
|
||||
Mana Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Base Max Mana:</span>
|
||||
<span className="text-gray-200">100</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Well Bonus:</span>
|
||||
<span className="text-blue-300">
|
||||
{(() => {
|
||||
const mw = skillTiers?.manaWell || 1;
|
||||
const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell';
|
||||
const level = skills[tieredSkillId] || skills.manaWell || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${fmt(level * 100 * tierMult)} (${level} lvl × 100 × ${tierMult}x tier)`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Prestige Mana Well:</span>
|
||||
<span className="text-blue-300">+{fmt((prestigeUpgrades.manaWell || 0) * 500)}</span>
|
||||
</div>
|
||||
{upgradeEffects.maxManaBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Upgrade Mana Bonus:</span>
|
||||
<span className="text-amber-300">+{fmt(upgradeEffects.maxManaBonus)}</span>
|
||||
</div>
|
||||
)}
|
||||
{upgradeEffects.maxManaMultiplier > 1 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Upgrade Mana Multiplier:</span>
|
||||
<span className="text-amber-300">×{fmtDec(upgradeEffects.maxManaMultiplier, 2)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||||
<span className="text-gray-300">Total Max Mana:</span>
|
||||
<span className="text-blue-400">{fmt(maxMana)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Base Regen:</span>
|
||||
<span className="text-gray-200">2/hr</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Flow Bonus:</span>
|
||||
<span className="text-blue-300">
|
||||
{(() => {
|
||||
const mf = skillTiers?.manaFlow || 1;
|
||||
const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow';
|
||||
const level = skills[tieredSkillId] || skills.manaFlow || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${fmtDec(level * 1 * tierMult)}/hr (${level} lvl × 1 × ${tierMult}x tier)`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Spring Bonus:</span>
|
||||
<span className="text-blue-300">+{(skills.manaSpring || 0) * 2}/hr</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Prestige Mana Flow:</span>
|
||||
<span className="text-blue-300">+{fmtDec((prestigeUpgrades.manaFlow || 0) * 0.5)}/hr</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Temporal Echo:</span>
|
||||
<span className="text-blue-300">×{fmtDec(1 + (prestigeUpgrades.temporalEcho || 0) * 0.1, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||||
<span className="text-gray-300">Base Regen:</span>
|
||||
<span className="text-blue-400">{fmtDec(baseRegen, 2)}/hr</span>
|
||||
</div>
|
||||
{upgradeEffects.regenBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Upgrade Regen Bonus:</span>
|
||||
<span className="text-amber-300">+{fmtDec(upgradeEffects.regenBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{upgradeEffects.permanentRegenBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Permanent Regen Bonus:</span>
|
||||
<span className="text-amber-300">+{fmtDec(upgradeEffects.permanentRegenBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{upgradeEffects.regenMultiplier > 1 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Upgrade Regen Multiplier:</span>
|
||||
<span className="text-amber-300">×{fmtDec(upgradeEffects.regenMultiplier, 2)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-gray-700 my-3" />
|
||||
{upgradeEffects.activeUpgrades.length > 0 && (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<span className="text-xs text-amber-400 game-panel-title">Active Skill Upgrades</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mb-3">
|
||||
{upgradeEffects.activeUpgrades.map((upgrade, idx) => (
|
||||
<div key={idx} className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||
<span className="text-gray-300">{upgrade.name}</span>
|
||||
<span className="text-gray-400">{upgrade.desc}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Separator className="bg-gray-700 my-3" />
|
||||
</>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Click Mana Value:</span>
|
||||
<span className="text-purple-300">+{clickMana}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Tap Bonus:</span>
|
||||
<span className="text-purple-300">+{skills.manaTap || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Surge Bonus:</span>
|
||||
<span className="text-purple-300">+{(skills.manaSurge || 0) * 3}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Overflow:</span>
|
||||
<span className="text-purple-300">×{fmtDec(1 + (skills.manaOverflow || 0) * 0.25, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className={`font-semibold ${meditationMultiplier > 1.5 ? 'text-purple-400' : 'text-gray-300'}`}>
|
||||
Meditation Multiplier:
|
||||
</span>
|
||||
<span className={`font-semibold ${meditationMultiplier > 1.5 ? 'text-purple-400' : 'text-gray-300'}`}>
|
||||
{fmtDec(meditationMultiplier, 2)}x
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-300">Effective Regen:</span>
|
||||
<span className="text-green-400 font-semibold">{fmtDec(effectiveRegen, 2)}/hr</span>
|
||||
</div>
|
||||
{incursionStrength > 0 && !hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM) && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-red-400">Incursion Penalty:</span>
|
||||
<span className="text-red-400">-{Math.round(incursionStrength * 100)}%</span>
|
||||
</div>
|
||||
)}
|
||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM) && incursionStrength > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-400">Steady Stream:</span>
|
||||
<span className="text-green-400">Immune to incursion</span>
|
||||
</div>
|
||||
)}
|
||||
{manaCascadeBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Cascade Bonus:</span>
|
||||
<span className="text-cyan-400">+{fmtDec(manaCascadeBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{manaWaterfallBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Waterfall Bonus:</span>
|
||||
<span className="text-cyan-400">+{fmtDec(manaWaterfallBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{hasManaWaterfall && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Waterfall:</span>
|
||||
<span className="text-cyan-400">+0.25 regen per 100 max mana</span>
|
||||
</div>
|
||||
)}
|
||||
{hasFlowSurge && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Flow Surge:</span>
|
||||
<span className="text-cyan-400">Clicks activate +100% regen for 1hr</span>
|
||||
</div>
|
||||
)}
|
||||
{hasManaOverflow && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Overflow:</span>
|
||||
<span className="text-cyan-400">Raw mana can exceed max by 20%</span>
|
||||
</div>
|
||||
)}
|
||||
{hasEternalFlow && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-400">Eternal Flow:</span>
|
||||
<span className="text-green-400">Regen immune to ALL penalties</span>
|
||||
</div>
|
||||
)}
|
||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_TORRENT) && rawMana > maxMana * 0.75 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Torrent:</span>
|
||||
<span className="text-cyan-400">+50% regen (high mana)</span>
|
||||
</div>
|
||||
)}
|
||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.DESPERATE_WELLS) && rawMana < maxMana * 0.25 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Desperate Wells:</span>
|
||||
<span className="text-cyan-400">+50% regen (low mana)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
ManaStatsSection.displayName = "ManaStatsSection";
|
||||
@@ -1,239 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { ATTUNEMENTS_DEF, getAttunementConversionRate } from '@/lib/game/data/attunements';
|
||||
import { computeMaxMana, computeElementMax } from '@/lib/game/stores';
|
||||
import { computeEffectiveRegenForDisplay } from '@/lib/game/store-modules/computed-stats';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Droplet } from 'lucide-react';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
// Modular stores
|
||||
import { useManaStore, useSkillStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
|
||||
export function ManaTypeBreakdown() {
|
||||
// Get state from modular stores
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
// attunements is not in modular stores - using empty object as fallback
|
||||
const attunements: Record<string, { active: boolean; level: number; experience: number }> = {};
|
||||
|
||||
// Compute unified effects for regen calculations
|
||||
const effects = getUnifiedEffects({
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
equippedInstances: {},
|
||||
equipmentInstances: {}
|
||||
});
|
||||
|
||||
// Get effective regen info for raw mana
|
||||
const regenInfo = computeEffectiveRegenForDisplay({
|
||||
skills,
|
||||
prestigeUpgrades,
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
elements,
|
||||
rawMana,
|
||||
attunements
|
||||
} as any, effects);
|
||||
|
||||
// Compute max mana
|
||||
const maxMana = computeMaxMana({
|
||||
skills,
|
||||
prestigeUpgrades,
|
||||
skillUpgrades,
|
||||
skillTiers
|
||||
}, effects);
|
||||
|
||||
// Get unlocked elements sorted by category then name
|
||||
const unlockedElements = Object.entries(elements)
|
||||
.filter(([, state]) => state.unlocked)
|
||||
.map(([id, state]) => {
|
||||
const def = ELEMENTS[id];
|
||||
if (!def) return null;
|
||||
const elemMax = computeElementMax({
|
||||
skills,
|
||||
prestigeUpgrades,
|
||||
skillUpgrades,
|
||||
skillTiers
|
||||
} as any, effects, id);
|
||||
return {
|
||||
id,
|
||||
name: def.name,
|
||||
sym: def.sym,
|
||||
color: def.color,
|
||||
current: state.current,
|
||||
max: elemMax,
|
||||
cat: def.cat,
|
||||
recipe: def.recipe,
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => {
|
||||
if (!a || !b) return 0;
|
||||
if (a.cat !== b.cat) return a.cat.localeCompare(b.cat);
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<Droplet className="w-4 h-4" />
|
||||
Mana Type Breakdown
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Raw Mana Section */}
|
||||
<div className="p-3 bg-gray-800/50 rounded border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">🌀</span>
|
||||
<span className="font-semibold text-purple-300">Raw Mana</span>
|
||||
<span className="text-xs text-gray-500">(base)</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-300">{fmt(rawMana)}</span>
|
||||
<span className="text-gray-500"> / </span>
|
||||
<span className="text-gray-400">{fmt(maxMana)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="w-full bg-gray-700 rounded-full h-2 mb-3">
|
||||
<div
|
||||
className="h-2 rounded-full transition-all duration-300 bg-purple-500"
|
||||
style={{ width: `${Math.min(100, (rawMana / maxMana) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Regen info */}
|
||||
<div className="text-xs space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Base Regen:</span>
|
||||
<span className="text-green-400">{fmtDec(regenInfo.rawRegen, 2)}/hr</span>
|
||||
</div>
|
||||
{regenInfo.conversionDrain > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Conversion Drain:</span>
|
||||
<span className="text-red-400">-{fmtDec(regenInfo.conversionDrain, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator className="bg-gray-700 my-1" />
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span className="text-gray-300">Effective Regen:</span>
|
||||
<span className="text-green-400">{fmtDec(regenInfo.effectiveRegen, 2)}/hr</span>
|
||||
</div>
|
||||
|
||||
{/* Show conversion drains by attunement */}
|
||||
{attunements && Object.keys(attunements).length > 0 && (
|
||||
<>
|
||||
<Separator className="bg-gray-700 my-1" />
|
||||
<div className="text-gray-400 mb-1">Conversion Drains:</div>
|
||||
{Object.entries(attunements).map(([attId, attState]) => {
|
||||
const attDef = ATTUNEMENTS_DEF[attId];
|
||||
if (!attDef || attState.level === 0) return null;
|
||||
const rate = getAttunementConversionRate(attId, attState.level);
|
||||
if (rate <= 0) return null;
|
||||
return (
|
||||
<div key={attId} className="flex justify-between pl-2">
|
||||
<span className="text-gray-500">{attDef.name}:</span>
|
||||
<span className="text-red-400">-{fmtDec(rate, 2)}/hr</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-gray-700" />
|
||||
|
||||
{/* Elemental Mana Sections */}
|
||||
{unlockedElements.map((elem) => {
|
||||
if (!elem) return null;
|
||||
|
||||
// Find attunements that convert TO this element
|
||||
const convertingAttunements = Object.entries(attunements || {})
|
||||
.filter(([attId, attState]) => {
|
||||
if (!attState.active) return false;
|
||||
const attDef = ATTUNEMENTS_DEF[attId];
|
||||
return attDef?.primaryManaType === elem.id && attDef.conversionRate > 0;
|
||||
});
|
||||
|
||||
// Calculate total conversion rate TO this element
|
||||
const totalConversionRate = convertingAttunements.reduce((total, [attId, attState]) => {
|
||||
return total + getAttunementConversionRate(attId, attState.level || 1);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div key={elem.id} className="p-3 bg-gray-800/50 rounded border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{elem.sym}</span>
|
||||
<span className="font-semibold" style={{ color: elem.color }}>{elem.name}</span>
|
||||
<span className="text-xs text-gray-500">({elem.cat})</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-300">{fmt(elem.current)}</span>
|
||||
<span className="text-gray-500"> / </span>
|
||||
<span className="text-gray-400">{fmt(elem.max)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="w-full bg-gray-700 rounded-full h-2 mb-3">
|
||||
<div
|
||||
className="h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(100, (elem.current / elem.max) * 100)}%`,
|
||||
backgroundColor: elem.color
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Conversion info */}
|
||||
{totalConversionRate > 0 ? (
|
||||
<div className="text-xs space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Conversion Rate:</span>
|
||||
<span className="text-green-400">+{fmtDec(totalConversionRate, 2)}/hr</span>
|
||||
</div>
|
||||
{convertingAttunements.length > 0 && (
|
||||
<div className="text-gray-500 pl-2">
|
||||
Source: {convertingAttunements.map(([attId]) => {
|
||||
const attDef = ATTUNEMENTS_DEF[attId];
|
||||
const level = attunements[attId]?.level || 1;
|
||||
return `${attDef?.name} (Lv.${level})`;
|
||||
}).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-500">No active conversion to this element</div>
|
||||
)}
|
||||
|
||||
{/* Show recipe for composite/exotic elements */}
|
||||
{elem.recipe && (
|
||||
<div className="text-xs text-gray-500 mt-2 pt-2 border-t border-gray-700">
|
||||
Recipe: {elem.recipe.map(r => `${ELEMENTS[r]?.sym} ${ELEMENTS[r]?.name || r}`).join(' + ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
ManaTypeBreakdown.displayName = "ManaTypeBreakdown";
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { fmtDec } from '@/lib/game/stores';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
|
||||
// Modular stores
|
||||
import { useSkillStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
|
||||
export interface StudyStatsSectionProps {
|
||||
studySpeedMult: number;
|
||||
studyCostMult: number;
|
||||
}
|
||||
|
||||
export function StudyStatsSection({ studySpeedMult, studyCostMult }: StudyStatsSectionProps) {
|
||||
// Get state from modular stores
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Study Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Study Speed:</span>
|
||||
<span className="text-purple-300">×{fmtDec(studySpeedMult, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Quick Learner Bonus:</span>
|
||||
<span className="text-purple-300">+{((skills.quickLearner || 0) * 10)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Study Cost:</span>
|
||||
<span className="text-purple-300">{Math.round(studyCostMult * 100)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Focused Mind Bonus:</span>
|
||||
<span className="text-purple-300">-{((skills.focusedMind || 0) * 5)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Progress Retention:</span>
|
||||
<span className="text-purple-300">{Math.round((1 + (skills.knowledgeRetention || 0) * 0.2) * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
StudyStatsSection.displayName = "StudyStatsSection";
|
||||
@@ -1,86 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Star } from 'lucide-react';
|
||||
|
||||
// Modular stores
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
|
||||
export function UpgradeEffectsSection() {
|
||||
// Get state from modular stores
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
|
||||
// Helper function to get all selected skill upgrades
|
||||
function getAllSelectedUpgrades() {
|
||||
const upgrades = [];
|
||||
for (const [skillId, selectedIds] of Object.entries(skillUpgrades)) {
|
||||
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
||||
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
|
||||
if (!path) continue;
|
||||
for (const tier of path.tiers) {
|
||||
if (tier.skillId === skillId) {
|
||||
for (const upgradeId of selectedIds) {
|
||||
const upgrade = tier.upgrades.find(u => u.id === upgradeId);
|
||||
if (upgrade) {
|
||||
upgrades.push({ skillId, upgrade });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return upgrades;
|
||||
}
|
||||
|
||||
const selectedUpgrades = getAllSelectedUpgrades();
|
||||
|
||||
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">
|
||||
<Star className="w-4 h-4" />
|
||||
Active Skill Upgrades ({selectedUpgrades.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{selectedUpgrades.length === 0 ? (
|
||||
<div className="text-gray-500 text-sm">No skill upgrades selected yet. Level skills to 5 or 10 to choose upgrades.</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{selectedUpgrades.map(({ skillId, upgrade }) => (
|
||||
<div key={upgrade.id} className="p-2 rounded border border-amber-600/30 bg-amber-900/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-amber-300 text-sm font-semibold">{upgrade.name}</span>
|
||||
<Badge variant="outline" className="text-xs text-gray-400">
|
||||
{SKILLS_DEF[skillId]?.name || skillId}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
||||
{upgrade.effect && upgrade.effect.type === 'multiplier' && (
|
||||
<div className="text-xs text-green-400 mt-1">
|
||||
+{Math.round((upgrade.effect.value - 1) * 100)}% {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect && upgrade.effect.type === 'bonus' && (
|
||||
<div className="text-xs text-blue-400 mt-1">
|
||||
+{upgrade.effect.value} {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect && upgrade.effect.type === 'special' && (
|
||||
<div className="text-xs text-cyan-400 mt-1">
|
||||
⚡ {upgrade.effect.specialDesc || 'Special effect active'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
UpgradeEffectsSection.displayName = "UpgradeEffectsSection";
|
||||
@@ -1,11 +0,0 @@
|
||||
export { ManaStatsSection } from './ManaStatsSection';
|
||||
export type { ManaStatsSectionProps } from './ManaStatsSection';
|
||||
|
||||
export { CombatStatsSection } from './CombatStatsSection';
|
||||
export type { CombatStatsSectionProps } from './CombatStatsSection';
|
||||
|
||||
export { StudyStatsSection } from './StudyStatsSection';
|
||||
export type { StudyStatsSectionProps } from './StudyStatsSection';
|
||||
|
||||
export { UpgradeEffectsSection } from './UpgradeEffectsSection';
|
||||
export type { UpgradeEffectsSectionProps } from './UpgradeEffectsSection';
|
||||
Reference in New Issue
Block a user