Fix mana conversion visibility and UI improvements
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 3m9s
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 3m9s
- Increase attunement mana conversion rates (0.2 -> 2 for Enchanter) - Hide mana types with current < 1 in ManaDisplay and LabTab - Only show owned equipment types when designing enchantments
This commit is contained in:
@@ -83,6 +83,11 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
instance: equipmentInstances[instanceId!],
|
||||
}));
|
||||
|
||||
// Get equipment types the player has available
|
||||
const ownedEquipmentTypeIds = new Set(
|
||||
Object.values(equipmentInstances).map(inst => inst.typeId)
|
||||
);
|
||||
|
||||
// Calculate total capacity cost for current design
|
||||
const designCapacityCost = selectedEffects.reduce(
|
||||
(total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus),
|
||||
@@ -201,7 +206,9 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
) : (
|
||||
<ScrollArea className="h-64">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.values(EQUIPMENT_TYPES).map(type => (
|
||||
{Object.values(EQUIPMENT_TYPES)
|
||||
.filter(type => ownedEquipmentTypeIds.has(type.id))
|
||||
.map(type => (
|
||||
<div
|
||||
key={type.id}
|
||||
className={`p-2 rounded border cursor-pointer transition-all ${
|
||||
|
||||
582
src/components/game/tabs/FamiliarTab.tsx
Executable file
582
src/components/game/tabs/FamiliarTab.tsx
Executable file
@@ -0,0 +1,582 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Sparkles, Heart, Zap, Star, Shield, Flame, Droplet, Wind, Mountain, Sun, Moon, Leaf, Skull,
|
||||
Brain, Link, Wind as Force, Droplets, TreeDeciduous, Hourglass, Gem, CircleDot, Circle,
|
||||
Sword, Wand2, ShieldCheck, TrendingUp, Clock, Crown
|
||||
} from 'lucide-react';
|
||||
import type { GameState, FamiliarInstance, FamiliarDef, FamiliarAbilityType } from '@/lib/game/types';
|
||||
import { FAMILIARS_DEF, getFamiliarXpRequired, getFamiliarAbilityValue } from '@/lib/game/data/familiars';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
|
||||
// Element icon mapping
|
||||
const ELEMENT_ICONS: Record<string, typeof Flame> = {
|
||||
fire: Flame,
|
||||
water: Droplet,
|
||||
air: Wind,
|
||||
earth: Mountain,
|
||||
light: Sun,
|
||||
dark: Moon,
|
||||
life: Leaf,
|
||||
death: Skull,
|
||||
mental: Brain,
|
||||
transference: Link,
|
||||
force: Force,
|
||||
blood: Droplets,
|
||||
metal: Shield,
|
||||
wood: TreeDeciduous,
|
||||
sand: Hourglass,
|
||||
crystal: Gem,
|
||||
stellar: Star,
|
||||
void: CircleDot,
|
||||
raw: Circle,
|
||||
};
|
||||
|
||||
// Rarity colors
|
||||
const RARITY_COLORS: Record<string, string> = {
|
||||
common: 'text-gray-400 border-gray-600',
|
||||
uncommon: 'text-green-400 border-green-600',
|
||||
rare: 'text-blue-400 border-blue-600',
|
||||
epic: 'text-purple-400 border-purple-600',
|
||||
legendary: 'text-amber-400 border-amber-600',
|
||||
};
|
||||
|
||||
const RARITY_BG: Record<string, string> = {
|
||||
common: 'bg-gray-900/50',
|
||||
uncommon: 'bg-green-900/20',
|
||||
rare: 'bg-blue-900/20',
|
||||
epic: 'bg-purple-900/20',
|
||||
legendary: 'bg-amber-900/20',
|
||||
};
|
||||
|
||||
// Role icons
|
||||
const ROLE_ICONS: Record<string, typeof Sword> = {
|
||||
combat: Sword,
|
||||
mana: Sparkles,
|
||||
support: Heart,
|
||||
guardian: ShieldCheck,
|
||||
};
|
||||
|
||||
// Ability type icons
|
||||
const ABILITY_ICONS: Record<FamiliarAbilityType, typeof Zap> = {
|
||||
damageBonus: Sword,
|
||||
manaRegen: Sparkles,
|
||||
autoGather: Zap,
|
||||
critChance: Star,
|
||||
castSpeed: Clock,
|
||||
manaShield: Shield,
|
||||
elementalBonus: Flame,
|
||||
lifeSteal: Heart,
|
||||
bonusGold: TrendingUp,
|
||||
autoConvert: Wand2,
|
||||
thorns: ShieldCheck,
|
||||
};
|
||||
|
||||
interface FamiliarTabProps {
|
||||
store: GameState & {
|
||||
setActiveFamiliar: (index: number, active: boolean) => void;
|
||||
setFamiliarNickname: (index: number, nickname: string) => void;
|
||||
summonFamiliar: (familiarId: string) => void;
|
||||
upgradeFamiliarAbility: (index: number, abilityType: FamiliarAbilityType) => void;
|
||||
getActiveFamiliarBonuses: () => ReturnType<typeof import('@/lib/game/familiar-slice').createFamiliarSlice>['getActiveFamiliarBonuses'] extends () => infer R ? R : never;
|
||||
getAvailableFamiliars: () => string[];
|
||||
};
|
||||
}
|
||||
|
||||
export function FamiliarTab({ store }: FamiliarTabProps) {
|
||||
const [selectedFamiliar, setSelectedFamiliar] = useState<number | null>(null);
|
||||
const [nicknameInput, setNicknameInput] = useState('');
|
||||
|
||||
const familiars = store.familiars;
|
||||
const activeFamiliarSlots = store.activeFamiliarSlots;
|
||||
const activeCount = familiars.filter(f => f.active).length;
|
||||
const availableFamiliars = store.getAvailableFamiliars();
|
||||
const familiarBonuses = store.getActiveFamiliarBonuses();
|
||||
|
||||
// Format XP display
|
||||
const formatXp = (current: number, level: number) => {
|
||||
const required = getFamiliarXpRequired(level);
|
||||
return `${current}/${required}`;
|
||||
};
|
||||
|
||||
// Get familiar definition
|
||||
const getFamiliarDef = (instance: FamiliarInstance): FamiliarDef | undefined => {
|
||||
return FAMILIARS_DEF[instance.familiarId];
|
||||
};
|
||||
|
||||
// Render a single familiar card
|
||||
const renderFamiliarCard = (instance: FamiliarInstance, index: number) => {
|
||||
const def = getFamiliarDef(instance);
|
||||
if (!def) return null;
|
||||
|
||||
const ElementIcon = ELEMENT_ICONS[def.element] || Circle;
|
||||
const RoleIcon = ROLE_ICONS[def.role] || Sparkles;
|
||||
const xpRequired = getFamiliarXpRequired(instance.level);
|
||||
const xpPercent = Math.min(100, (instance.experience / xpRequired) * 100);
|
||||
const bondPercent = instance.bond;
|
||||
const isSelected = selectedFamiliar === index;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={`${instance.familiarId}-${index}`}
|
||||
className={`cursor-pointer transition-all ${RARITY_BG[def.rarity]} ${
|
||||
isSelected ? 'ring-2 ring-amber-500' : ''
|
||||
} ${instance.active ? 'ring-1 ring-green-500/50' : ''} border ${RARITY_COLORS[def.rarity]}`}
|
||||
onClick={() => setSelectedFamiliar(isSelected ? null : index)}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-full bg-black/30 flex items-center justify-center">
|
||||
<ElementIcon className="w-6 h-6" style={{ color: ELEMENTS[def.element]?.color || '#fff' }} />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className={`text-sm ${RARITY_COLORS[def.rarity]}`}>
|
||||
{instance.nickname || def.name}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-400">
|
||||
<RoleIcon className="w-3 h-3" />
|
||||
<span>Lv.{instance.level}</span>
|
||||
{instance.active && (
|
||||
<Badge className="ml-1 bg-green-900/50 text-green-300 text-xs py-0">Active</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className={`${RARITY_COLORS[def.rarity]} text-xs`}>
|
||||
{def.rarity}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{/* XP Bar */}
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>XP</span>
|
||||
<span>{formatXp(instance.experience, instance.level)}</span>
|
||||
</div>
|
||||
<Progress value={xpPercent} className="h-1.5 bg-gray-800" />
|
||||
</div>
|
||||
|
||||
{/* Bond Bar */}
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<Heart className="w-3 h-3" /> Bond
|
||||
</span>
|
||||
<span>{bondPercent.toFixed(0)}%</span>
|
||||
</div>
|
||||
<Progress value={bondPercent} className="h-1.5 bg-gray-800" />
|
||||
</div>
|
||||
|
||||
{/* Abilities Preview */}
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{instance.abilities.slice(0, 3).map(ability => {
|
||||
const abilityDef = def.abilities.find(a => a.type === ability.type);
|
||||
if (!abilityDef) return null;
|
||||
const AbilityIcon = ABILITY_ICONS[ability.type] || Zap;
|
||||
const value = getFamiliarAbilityValue(abilityDef, instance.level, ability.level);
|
||||
|
||||
return (
|
||||
<Tooltip key={ability.type}>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="outline" className="text-xs py-0 flex items-center gap-1">
|
||||
<AbilityIcon className="w-3 h-3" />
|
||||
{ability.type === 'damageBonus' || ability.type === 'elementalBonus' ||
|
||||
ability.type === 'castSpeed' || ability.type === 'critChance' ||
|
||||
ability.type === 'lifeSteal' || ability.type === 'thorns' ||
|
||||
ability.type === 'bonusGold'
|
||||
? `+${value.toFixed(1)}%`
|
||||
: `+${value.toFixed(1)}`}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-sm">{abilityDef.desc}</p>
|
||||
<p className="text-xs text-gray-400">Level {ability.level}/10</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Render selected familiar details
|
||||
const renderFamiliarDetails = () => {
|
||||
if (selectedFamiliar === null || selectedFamiliar >= familiars.length) return null;
|
||||
|
||||
const instance = familiars[selectedFamiliar];
|
||||
const def = getFamiliarDef(instance);
|
||||
if (!def) return null;
|
||||
|
||||
const ElementIcon = ELEMENT_ICONS[def.element] || Circle;
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-amber-400 text-sm">Familiar Details</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => setSelectedFamiliar(null)}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Name and nickname */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-12 h-12 rounded-full bg-black/30 flex items-center justify-center">
|
||||
<ElementIcon className="w-8 h-8" style={{ color: ELEMENTS[def.element]?.color || '#fff' }} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`text-lg font-bold ${RARITY_COLORS[def.rarity]}`}>
|
||||
{def.name}
|
||||
</h3>
|
||||
{instance.nickname && (
|
||||
<p className="text-sm text-gray-400">"{instance.nickname}"</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nickname input */}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={nicknameInput}
|
||||
onChange={(e) => setNicknameInput(e.target.value)}
|
||||
placeholder="Set nickname..."
|
||||
className="h-8 text-sm bg-gray-800 border-gray-600"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
store.setFamiliarNickname(selectedFamiliar, nicknameInput);
|
||||
setNicknameInput('');
|
||||
}}
|
||||
disabled={!nicknameInput.trim()}
|
||||
>
|
||||
Set
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="text-sm text-gray-300 italic">
|
||||
{def.desc}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Level:</span>
|
||||
<span className="text-white">{instance.level}/100</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Bond:</span>
|
||||
<span className="text-white">{instance.bond.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Role:</span>
|
||||
<span className="text-white capitalize">{def.role}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Element:</span>
|
||||
<span style={{ color: ELEMENTS[def.element]?.color }}>{def.element}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-gray-700" />
|
||||
|
||||
{/* Abilities */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold text-gray-300">Abilities</h4>
|
||||
{instance.abilities.map(ability => {
|
||||
const abilityDef = def.abilities.find(a => a.type === ability.type);
|
||||
if (!abilityDef) return null;
|
||||
const AbilityIcon = ABILITY_ICONS[ability.type] || Zap;
|
||||
const value = getFamiliarAbilityValue(abilityDef, instance.level, ability.level);
|
||||
const upgradeCost = ability.level * 100;
|
||||
const canUpgrade = instance.experience >= upgradeCost && ability.level < 10;
|
||||
|
||||
return (
|
||||
<div key={ability.type} className="p-2 rounded bg-gray-800/50 border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<AbilityIcon className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-sm font-medium capitalize">
|
||||
{ability.type.replace(/([A-Z])/g, ' $1').trim()}
|
||||
</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">Lv.{ability.level}/10</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mb-2">{abilityDef.desc}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-green-400">
|
||||
Current: +{value.toFixed(2)}
|
||||
{ability.type === 'damageBonus' || ability.type === 'elementalBonus' ||
|
||||
ability.type === 'castSpeed' || ability.type === 'critChance' ||
|
||||
ability.type === 'lifeSteal' || ability.type === 'thorns' ||
|
||||
ability.type === 'bonusGold' ? '%' : ''}
|
||||
</span>
|
||||
{ability.level < 10 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 text-xs"
|
||||
disabled={!canUpgrade}
|
||||
onClick={() => store.upgradeFamiliarAbility(selectedFamiliar, ability.type)}
|
||||
>
|
||||
Upgrade ({upgradeCost} XP)
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Activate/Deactivate */}
|
||||
<Button
|
||||
className={`w-full ${instance.active ? 'bg-red-900/50 hover:bg-red-800/50' : 'bg-green-900/50 hover:bg-green-800/50'}`}
|
||||
onClick={() => store.setActiveFamiliar(selectedFamiliar, !instance.active)}
|
||||
disabled={!instance.active && activeCount >= activeFamiliarSlots}
|
||||
>
|
||||
{instance.active ? 'Deactivate' : 'Activate'}
|
||||
</Button>
|
||||
|
||||
{/* Flavor text */}
|
||||
{def.flavorText && (
|
||||
<p className="text-xs text-gray-500 italic text-center">
|
||||
"{def.flavorText}"
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Render summonable familiars
|
||||
const renderSummonableFamiliars = () => {
|
||||
if (availableFamiliars.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Available to Summon ({availableFamiliars.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-48">
|
||||
<div className="space-y-2">
|
||||
{availableFamiliars.map(familiarId => {
|
||||
const def = FAMILIARS_DEF[familiarId];
|
||||
if (!def) return null;
|
||||
|
||||
const ElementIcon = ELEMENT_ICONS[def.element] || Circle;
|
||||
const RoleIcon = ROLE_ICONS[def.role] || Sparkles;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={familiarId}
|
||||
className={`p-2 rounded border ${RARITY_COLORS[def.rarity]} ${RARITY_BG[def.rarity]} flex items-center justify-between`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ElementIcon className="w-5 h-5" style={{ color: ELEMENTS[def.element]?.color || '#fff' }} />
|
||||
<div>
|
||||
<div className={`text-sm font-medium ${RARITY_COLORS[def.rarity]}`}>
|
||||
{def.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 flex items-center gap-1">
|
||||
<RoleIcon className="w-3 h-3" />
|
||||
{def.role}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7"
|
||||
onClick={() => store.summonFamiliar(familiarId)}
|
||||
>
|
||||
Summon
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Render active bonuses
|
||||
const renderActiveBonuses = () => {
|
||||
const hasBonuses = Object.entries(familiarBonuses).some(([key, value]) => {
|
||||
if (key === 'damageMultiplier' || key === 'castSpeedMultiplier' ||
|
||||
key === 'elementalDamageMultiplier' || key === 'insightMultiplier') {
|
||||
return value > 1;
|
||||
}
|
||||
return value > 0;
|
||||
});
|
||||
|
||||
if (!hasBonuses) return null;
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-green-400 text-sm flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
Active Familiar Bonuses
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 text-sm">
|
||||
{familiarBonuses.damageMultiplier > 1 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Sword className="w-4 h-4 text-red-400" />
|
||||
<span>+{((familiarBonuses.damageMultiplier - 1) * 100).toFixed(0)}% DMG</span>
|
||||
</div>
|
||||
)}
|
||||
{familiarBonuses.manaRegenBonus > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-blue-400" />
|
||||
<span>+{familiarBonuses.manaRegenBonus.toFixed(1)} regen</span>
|
||||
</div>
|
||||
)}
|
||||
{familiarBonuses.autoGatherRate > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-yellow-400" />
|
||||
<span>+{familiarBonuses.autoGatherRate.toFixed(1)}/hr gather</span>
|
||||
</div>
|
||||
)}
|
||||
{familiarBonuses.critChanceBonus > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="w-4 h-4 text-amber-400" />
|
||||
<span>+{familiarBonuses.critChanceBonus.toFixed(1)}% crit</span>
|
||||
</div>
|
||||
)}
|
||||
{familiarBonuses.castSpeedMultiplier > 1 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-cyan-400" />
|
||||
<span>+{((familiarBonuses.castSpeedMultiplier - 1) * 100).toFixed(0)}% speed</span>
|
||||
</div>
|
||||
)}
|
||||
{familiarBonuses.elementalDamageMultiplier > 1 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Flame className="w-4 h-4 text-orange-400" />
|
||||
<span>+{((familiarBonuses.elementalDamageMultiplier - 1) * 100).toFixed(0)}% elem</span>
|
||||
</div>
|
||||
)}
|
||||
{familiarBonuses.lifeStealPercent > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Heart className="w-4 h-4 text-red-400" />
|
||||
<span>+{familiarBonuses.lifeStealPercent.toFixed(0)}% lifesteal</span>
|
||||
</div>
|
||||
)}
|
||||
{familiarBonuses.insightMultiplier > 1 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-purple-400" />
|
||||
<span>+{((familiarBonuses.insightMultiplier - 1) * 100).toFixed(0)}% insight</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Owned Familiars */}
|
||||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
||||
<Heart className="w-4 h-4" />
|
||||
Your Familiars ({familiars.length})
|
||||
</CardTitle>
|
||||
<div className="text-xs text-gray-400">
|
||||
Active Slots: {activeCount}/{activeFamiliarSlots}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{familiars.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{familiars.map((instance, index) => renderFamiliarCard(instance, index))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center p-8 text-gray-500">
|
||||
No familiars yet. Progress through the game to summon companions!
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Active Bonuses */}
|
||||
{renderActiveBonuses()}
|
||||
|
||||
{/* Selected Familiar Details */}
|
||||
{renderFamiliarDetails()}
|
||||
|
||||
{/* Summonable Familiars */}
|
||||
{renderSummonableFamiliars()}
|
||||
|
||||
{/* Familiar Guide */}
|
||||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-purple-400 text-sm flex items-center gap-2">
|
||||
<Crown className="w-4 h-4" />
|
||||
Familiar Guide
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm text-gray-300">
|
||||
<div>
|
||||
<h4 className="font-semibold text-amber-400 mb-1">Acquiring Familiars</h4>
|
||||
<p>Familiars become available to summon as you progress through floors, gather mana, and sign pacts with guardians. Higher rarity familiars are unlocked later.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-amber-400 mb-1">Leveling & Bond</h4>
|
||||
<p>Active familiars gain XP from combat, gathering, and time. Higher bond increases their power and XP gain. Upgrade abilities using XP to boost their effects.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-amber-400 mb-1">Roles</h4>
|
||||
<p>
|
||||
<span className="text-red-400">Combat</span> - Damage and crit bonuses<br/>
|
||||
<span className="text-blue-400">Mana</span> - Regeneration and auto-gathering<br/>
|
||||
<span className="text-green-400">Support</span> - Speed and utility<br/>
|
||||
<span className="text-amber-400">Guardian</span> - Defense and shields
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-amber-400 mb-1">Active Slots</h4>
|
||||
<p>You can have 1 familiar active by default. Upgrade through prestige to unlock more active slots for stacking bonuses.</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
567
src/components/game/tabs/GrimoireTab.tsx
Executable file
567
src/components/game/tabs/GrimoireTab.tsx
Executable file
@@ -0,0 +1,567 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { RotateCcw, Save, Trash2, Star, Flame, Clock, AlertCircle } from 'lucide-react';
|
||||
import { useGameContext } from '../GameContext';
|
||||
import { GUARDIANS, PRESTIGE_DEF, SKILLS_DEF, ELEMENTS } from '@/lib/game/constants';
|
||||
import { getTierMultiplier, getBaseSkillId } from '@/lib/game/skill-evolution';
|
||||
import { fmt, fmtDec, getBoonBonuses } from '@/lib/game/stores';
|
||||
import type { Memory } from '@/lib/game/types';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
export function GrimoireTab() {
|
||||
const { store } = useGameContext();
|
||||
const [showMemoryPicker, setShowMemoryPicker] = useState(false);
|
||||
|
||||
// Get all skills that can be saved to memory
|
||||
const saveableSkills = useMemo(() => {
|
||||
const skills: { skillId: string; level: number; tier: number; upgrades: string[]; name: string }[] = [];
|
||||
|
||||
for (const [skillId, level] of Object.entries(store.skills)) {
|
||||
if (level && level > 0) {
|
||||
const baseSkillId = getBaseSkillId(skillId);
|
||||
const tier = store.skillTiers?.[baseSkillId] || 1;
|
||||
const tieredSkillId = tier > 1 ? `${baseSkillId}_t${tier}` : baseSkillId;
|
||||
const upgrades = store.skillUpgrades?.[tieredSkillId] || [];
|
||||
const skillDef = SKILLS_DEF[baseSkillId];
|
||||
|
||||
if (skillId === baseSkillId || skillId.includes('_t')) {
|
||||
const actualLevel = store.skills[tieredSkillId] || store.skills[baseSkillId] || 0;
|
||||
if (actualLevel > 0) {
|
||||
skills.push({
|
||||
skillId: baseSkillId,
|
||||
level: actualLevel,
|
||||
tier,
|
||||
upgrades,
|
||||
name: skillDef?.name || baseSkillId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueSkills = new Map<string, typeof skills[0]>();
|
||||
for (const skill of skills) {
|
||||
const existing = uniqueSkills.get(skill.skillId);
|
||||
if (!existing || skill.tier > existing.tier || (skill.tier === existing.tier && skill.level > existing.level)) {
|
||||
uniqueSkills.set(skill.skillId, skill);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueSkills.values()).sort((a, b) => {
|
||||
if (a.tier !== b.tier) return b.tier - a.tier;
|
||||
if (a.level !== b.level) return b.level - a.level;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [store.skills, store.skillTiers, store.skillUpgrades]);
|
||||
|
||||
const isSkillInMemory = (skillId: string) => store.memories.some(m => m.skillId === skillId);
|
||||
const canAddMore = store.memories.length < store.memorySlots;
|
||||
|
||||
const addToMemory = (skill: typeof saveableSkills[0]) => {
|
||||
const memory: Memory = {
|
||||
skillId: skill.skillId,
|
||||
level: skill.level,
|
||||
tier: skill.tier,
|
||||
upgrades: skill.upgrades,
|
||||
};
|
||||
store.addMemory(memory);
|
||||
};
|
||||
|
||||
// Calculate total boons from active pacts
|
||||
const activeBoons = useMemo(() => {
|
||||
return getBoonBonuses(store.signedPacts);
|
||||
}, [store.signedPacts]);
|
||||
|
||||
// Check if player can sign a pact
|
||||
const canSignPact = (floor: number) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian) return false;
|
||||
if (!store.defeatedGuardians.includes(floor)) return false;
|
||||
if (store.signedPacts.includes(floor)) return false;
|
||||
if (store.signedPacts.length >= store.pactSlots) return false;
|
||||
if (store.rawMana < guardian.pactCost) return false;
|
||||
if (store.pactRitualFloor !== null) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Get pact affinity bonus for display
|
||||
const pactAffinityBonus = (store.prestigeUpgrades.pactAffinity || 0) * 10;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Current Status */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Loop Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-amber-400 game-mono">{store.loopCount}</div>
|
||||
<div className="text-xs text-gray-400">Loops Completed</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
|
||||
<div className="text-xs text-gray-400">Current Insight</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
|
||||
<div className="text-xs text-gray-400">Total Insight</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-green-400 game-mono">{store.memorySlots}</div>
|
||||
<div className="text-xs text-gray-400">Memory Slots</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pact Slots & Active Ritual */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<Flame className="w-4 h-4" />
|
||||
Pact Slots ({store.signedPacts.length}/{store.pactSlots})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Active Ritual Progress */}
|
||||
{store.pactRitualFloor !== null && (
|
||||
<div className="mb-4 p-3 rounded border-2 border-amber-500/50 bg-amber-900/20">
|
||||
{(() => {
|
||||
const guardian = GUARDIANS[store.pactRitualFloor];
|
||||
if (!guardian) return null;
|
||||
const requiredTime = guardian.pactTime * (1 - (store.prestigeUpgrades.pactAffinity || 0) * 0.1);
|
||||
const progress = Math.min(100, (store.pactRitualProgress / requiredTime) * 100);
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-amber-300 font-semibold text-sm">Signing Pact with {guardian.name}</span>
|
||||
<span className="text-xs text-gray-400">{fmtDec(progress, 1)}%</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-gray-700 rounded overflow-hidden mb-2">
|
||||
<div
|
||||
className="h-full bg-amber-500 transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-gray-400">
|
||||
<Clock className="w-3 h-3 inline mr-1" />
|
||||
{fmtDec(store.pactRitualProgress, 1)}h / {fmtDec(requiredTime, 1)}h
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 text-xs border-red-500/50 text-red-400 hover:bg-red-900/20"
|
||||
onClick={() => store.cancelPactRitual()}
|
||||
>
|
||||
Cancel Ritual
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Pacts */}
|
||||
{store.signedPacts.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{store.signedPacts.map((floor) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian) return null;
|
||||
return (
|
||||
<div
|
||||
key={floor}
|
||||
className="p-3 rounded border"
|
||||
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
||||
{guardian.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Floor {floor} Guardian</div>
|
||||
<div className="text-xs text-amber-300 mt-1 italic">"{guardian.uniquePerk}"</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0 text-red-400 hover:text-red-300"
|
||||
onClick={() => store.removePact(floor)}
|
||||
title="Break Pact"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{guardian.boons.map((boon, idx) => (
|
||||
<Badge key={idx} className="bg-gray-700/50 text-gray-200 text-xs">
|
||||
{boon.desc}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm text-center py-4">
|
||||
No active pacts. Defeat guardians and sign pacts to gain boons.
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Available Guardians for Pacts */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
Available Guardians ({store.defeatedGuardians.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{store.defeatedGuardians.length === 0 ? (
|
||||
<div className="text-gray-500 text-sm text-center py-4">
|
||||
Defeat guardians in the Spire to make them available for pacts.
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-64">
|
||||
<div className="space-y-2 pr-2">
|
||||
{store.defeatedGuardians
|
||||
.sort((a, b) => a - b)
|
||||
.map((floor) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian) return null;
|
||||
const canSign = canSignPact(floor);
|
||||
const notEnoughMana = store.rawMana < guardian.pactCost;
|
||||
const atCapacity = store.signedPacts.length >= store.pactSlots;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={floor}
|
||||
className="p-3 rounded border border-gray-700 bg-gray-800/50"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
||||
{guardian.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
Floor {floor} • {ELEMENTS[guardian.element]?.name || guardian.element}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-amber-300">{fmt(guardian.pactCost)} mana</div>
|
||||
<div className="text-xs text-gray-400">{guardian.pactTime}h ritual</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-purple-300 italic mb-2">"{guardian.uniquePerk}"</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{guardian.boons.map((boon, idx) => (
|
||||
<Badge key={idx} className="bg-gray-700/50 text-gray-200 text-xs">
|
||||
{boon.desc}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant={canSign ? 'default' : 'outline'}
|
||||
className="w-full"
|
||||
disabled={!canSign}
|
||||
onClick={() => store.startPactRitual(floor, store.rawMana)}
|
||||
>
|
||||
{atCapacity
|
||||
? 'Pact Slots Full'
|
||||
: notEnoughMana
|
||||
? 'Not Enough Mana'
|
||||
: store.pactRitualFloor !== null
|
||||
? 'Ritual in Progress'
|
||||
: 'Start Pact Ritual'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Memory Slots */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<Save className="w-4 h-4" />
|
||||
Memory Slots ({store.memories.length}/{store.memorySlots})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-gray-400 mb-3">
|
||||
Skills saved to memory will retain their level, tier, and upgrades when you start a new loop.
|
||||
</p>
|
||||
|
||||
{/* Saved Memories */}
|
||||
{store.memories.length > 0 ? (
|
||||
<div className="space-y-1 mb-3">
|
||||
{store.memories.map((memory) => {
|
||||
const skillDef = SKILLS_DEF[memory.skillId];
|
||||
const tierMult = getTierMultiplier(memory.tier > 1 ? `${memory.skillId}_t${memory.tier}` : memory.skillId);
|
||||
return (
|
||||
<div
|
||||
key={memory.skillId}
|
||||
className="flex items-center justify-between p-2 rounded border border-amber-600/30 bg-amber-900/10"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm text-amber-300">{skillDef?.name || memory.skillId}</span>
|
||||
{memory.tier > 1 && (
|
||||
<Badge className="bg-purple-600/50 text-purple-200 text-xs">
|
||||
T{memory.tier} ({tierMult}x)
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-purple-400 text-xs">Lv.{memory.level}</span>
|
||||
{memory.upgrades.length > 0 && (
|
||||
<Badge className="bg-amber-700/50 text-amber-200 text-xs flex items-center gap-1">
|
||||
<Star className="w-3 h-3" />
|
||||
{memory.upgrades.length}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0 text-red-400 hover:text-red-300"
|
||||
onClick={() => store.removeMemory(memory.skillId)}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm mb-3 text-center py-2">
|
||||
No memories saved. Add skills below.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Memory Button */}
|
||||
{canAddMore && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => setShowMemoryPicker(!showMemoryPicker)}
|
||||
>
|
||||
{showMemoryPicker ? 'Hide Skills' : 'Add Skill to Memory'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Skill Picker */}
|
||||
{showMemoryPicker && canAddMore && (
|
||||
<ScrollArea className="h-48 mt-2">
|
||||
<div className="space-y-1 pr-2">
|
||||
{saveableSkills.length === 0 ? (
|
||||
<div className="text-gray-500 text-xs text-center py-4">
|
||||
No skills with progress to save
|
||||
</div>
|
||||
) : (
|
||||
saveableSkills.map((skill) => {
|
||||
const isInMemory = isSkillInMemory(skill.skillId);
|
||||
const tierMult = getTierMultiplier(skill.tier > 1 ? `${skill.skillId}_t${skill.tier}` : skill.skillId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={skill.skillId}
|
||||
className={`p-2 rounded border cursor-pointer transition-all ${
|
||||
isInMemory
|
||||
? 'border-amber-500 bg-amber-900/30 opacity-50'
|
||||
: 'border-gray-700 bg-gray-800/50 hover:border-amber-500/50'
|
||||
}`}
|
||||
onClick={() => !isInMemory && addToMemory(skill)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm">{skill.name}</span>
|
||||
{skill.tier > 1 && (
|
||||
<Badge className="bg-purple-600/50 text-purple-200 text-xs">
|
||||
T{skill.tier} ({tierMult}x)
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-purple-400 text-sm">Lv.{skill.level}</span>
|
||||
{skill.upgrades.length > 0 && (
|
||||
<Badge className="bg-amber-700/50 text-amber-200 text-xs flex items-center gap-1">
|
||||
<Star className="w-3 h-3" />
|
||||
{skill.upgrades.length}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Active Boons Summary */}
|
||||
{store.signedPacts.length > 0 && (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Active Boons Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
{activeBoons.maxMana > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Max Mana:</span>
|
||||
<span className="text-blue-300">+{activeBoons.maxMana}</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.manaRegen > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Mana Regen:</span>
|
||||
<span className="text-blue-300">+{activeBoons.manaRegen}/h</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.castingSpeed > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Cast Speed:</span>
|
||||
<span className="text-amber-300">+{activeBoons.castingSpeed}%</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.elementalDamage > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Elem. Damage:</span>
|
||||
<span className="text-red-300">+{activeBoons.elementalDamage}%</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.rawDamage > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Raw Damage:</span>
|
||||
<span className="text-red-300">+{activeBoons.rawDamage}%</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.critChance > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Crit Chance:</span>
|
||||
<span className="text-yellow-300">+{activeBoons.critChance}%</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.critDamage > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Crit Damage:</span>
|
||||
<span className="text-yellow-300">+{activeBoons.critDamage}%</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.spellEfficiency > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Spell Cost:</span>
|
||||
<span className="text-green-300">-{activeBoons.spellEfficiency}%</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.manaGain > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Mana Gain:</span>
|
||||
<span className="text-blue-300">+{activeBoons.manaGain}%</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.insightGain > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Insight Gain:</span>
|
||||
<span className="text-purple-300">+{activeBoons.insightGain}%</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.studySpeed > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Study Speed:</span>
|
||||
<span className="text-cyan-300">+{activeBoons.studySpeed}%</span>
|
||||
</div>
|
||||
)}
|
||||
{activeBoons.prestigeInsight > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Prestige Insight:</span>
|
||||
<span className="text-purple-300">+{activeBoons.prestigeInsight}/loop</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Prestige Upgrades */}
|
||||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Insight Upgrades (Permanent)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{Object.entries(PRESTIGE_DEF).map(([id, def]) => {
|
||||
const level = store.prestigeUpgrades[id] || 0;
|
||||
const maxed = level >= def.max;
|
||||
const canBuy = !maxed && store.insight >= def.cost;
|
||||
|
||||
return (
|
||||
<div key={id} className="p-3 rounded border border-gray-700 bg-gray-800/50">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="font-semibold text-amber-400 text-sm">{def.name}</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{level}/{def.max}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 italic mb-2">{def.desc}</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={canBuy ? 'default' : 'outline'}
|
||||
className="w-full"
|
||||
disabled={!canBuy}
|
||||
onClick={() => store.doPrestige(id)}
|
||||
>
|
||||
{maxed ? 'Maxed' : `Upgrade (${fmt(def.cost)} insight)`}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Reset Game Button */}
|
||||
<div className="mt-4 pt-4 border-t border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400">Reset All Progress</div>
|
||||
<div className="text-xs text-gray-500">Clear all data and start fresh</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-red-600/50 text-red-400 hover:bg-red-900/20"
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to reset ALL progress? This cannot be undone!')) {
|
||||
store.resetGame();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user