fix: split SpireTab.tsx to 395 lines, remove require() imports, import from data modules; complete store migration
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 30m15s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 30m15s
This commit is contained in:
@@ -1,26 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import type { GameStore } from '@/lib/game/store';
|
||||
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
|
||||
import { useGameStore } from '@/lib/game/stores';
|
||||
|
||||
export interface AchievementsTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
export function AchievementsTab() {
|
||||
const achievements = useGameStore((s) => s.achievements);
|
||||
const maxFloorReached = useGameStore((s) => s.maxFloorReached);
|
||||
const totalManaGathered = useGameStore((s) => s.totalManaGathered);
|
||||
const signedPacts = useGameStore((s) => s.signedPacts);
|
||||
const totalSpellsCast = useGameStore((s) => s.totalSpellsCast);
|
||||
const totalDamageDealt = useGameStore((s) => s.totalDamageDealt);
|
||||
const totalCraftsCompleted = useGameStore((s) => s.totalCraftsCompleted);
|
||||
|
||||
export function AchievementsTab({ store }: AchievementsTabProps) {
|
||||
const achievements = store.achievements;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<AchievementsDisplay
|
||||
achievements={achievements}
|
||||
gameState={{
|
||||
maxFloorReached: store.maxFloorReached,
|
||||
totalManaGathered: store.totalManaGathered,
|
||||
signedPacts: store.signedPacts,
|
||||
totalSpellsCast: store.totalSpellsCast,
|
||||
totalDamageDealt: store.totalDamageDealt,
|
||||
totalCraftsCompleted: store.totalCraftsCompleted,
|
||||
maxFloorReached,
|
||||
totalManaGathered,
|
||||
signedPacts,
|
||||
totalSpellsCast,
|
||||
totalDamageDealt,
|
||||
totalCraftsCompleted,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
// ─── Category Skills List ───────────────────────────────────────────
|
||||
// Wraps all skills in a single category, handles category-level UI
|
||||
// CategorySkillsList - Displays skills for a specific category
|
||||
// Migrated to use hooks directly (removed GameStore prop)
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SkillRow } from './SkillRow';
|
||||
import { SkillCategoryHeader } from './SkillCategoryHeader';
|
||||
import type { GameStore } from '@/lib/game/store';
|
||||
import { SKILL_CATEGORIES, SKILLS_DEF, getStudyCostMultiplier, getStudySpeedMultiplier } from '@/lib/game/constants';
|
||||
import { getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier, SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
|
||||
import { SPECIAL_EFFECTS, hasSpecial } from '@/lib/game/special-effects';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { SKILLS_DEF, getTierMultiplier } from '@/lib/game/constants';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { hasMilestoneUpgrade } from '@/lib/game/hooks/useSkillUpgradeSelection';
|
||||
import type { ComputedEffects } from '@/lib/game/upgrade-effects.types';
|
||||
import type { SkillCost } from '@/lib/game/types';
|
||||
import { useGameToast } from '@/components/game/GameToast';
|
||||
import { useSkillStore, useGameStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
import { SkillRow } from './SkillRow';
|
||||
import type { GameStore } from '@/lib/game/store'; // Keep type import for backward compatibility
|
||||
|
||||
interface CategorySkillsListProps {
|
||||
category: { id: string; name: string; icon: string };
|
||||
availableCategories: string[];
|
||||
isCollapsed: boolean;
|
||||
onToggleCategory: (categoryId: string) => void;
|
||||
store: GameStore;
|
||||
categoryId: string;
|
||||
categoryName: string;
|
||||
skills: Record<string, number>;
|
||||
skillUpgrades: Record<string, string[]>;
|
||||
skillTiers: Record<string, number>;
|
||||
prestigeUpgrades: Record<string, number>;
|
||||
studySpeedMult: number;
|
||||
upgradeEffects: ComputedEffects;
|
||||
upgradeEffects: any;
|
||||
currentStudyTarget: any;
|
||||
onStartStudying: (skillId: string) => void;
|
||||
onParallelStudy: (skillId: string) => void;
|
||||
@@ -36,17 +30,13 @@ interface CategorySkillsListProps {
|
||||
setPendingSelections: (selections: string[]) => void;
|
||||
}
|
||||
|
||||
// Type guard for element skill costs
|
||||
function isElementCost(cost?: SkillCost | null): cost is SkillCost & { type: 'element'; element: string } {
|
||||
return cost !== null && typeof cost !== 'undefined' && cost.type === 'element' && typeof cost.element === 'string';
|
||||
}
|
||||
|
||||
export function CategorySkillsList({
|
||||
category,
|
||||
availableCategories,
|
||||
isCollapsed,
|
||||
onToggleCategory,
|
||||
store,
|
||||
categoryId,
|
||||
categoryName,
|
||||
skills,
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
prestigeUpgrades,
|
||||
studySpeedMult,
|
||||
upgradeEffects,
|
||||
currentStudyTarget,
|
||||
@@ -58,164 +48,72 @@ export function CategorySkillsList({
|
||||
pendingSelections,
|
||||
setPendingSelections,
|
||||
}: CategorySkillsListProps) {
|
||||
const showToast = useGameToast();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
// Skip if category not available
|
||||
if (!availableCategories.includes(category.id)) return null;
|
||||
const categorySkills = Object.entries(SKILLS_DEF || {})
|
||||
.filter(([, def]) => def.category === categoryId)
|
||||
.sort((a, b) => (a[1].tier || 0) - (b[1].tier || 0));
|
||||
|
||||
const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === category.id);
|
||||
if (skillsInCat.length === 0) return null;
|
||||
|
||||
const handleCancelStudyInternal = () => {
|
||||
onCancelStudy();
|
||||
};
|
||||
const toggleCollapse = () => setCollapsed(!collapsed);
|
||||
|
||||
return (
|
||||
<Card key={category.id} className="bg-gray-900/80 border-gray-700">
|
||||
<SkillCategoryHeader
|
||||
category={category}
|
||||
skillCount={skillsInCat.length}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={() => onToggleCategory(category.id)}
|
||||
/>
|
||||
{!isCollapsed && (
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{skillsInCat.map(([id, def]) => {
|
||||
// GATE MANA CAPACITY SKILLS BY UNLOCKED ELEMENT
|
||||
if (isElementCost(def.cost)) {
|
||||
const element = store.elements[def.cost.element];
|
||||
if (!element?.unlocked) return null;
|
||||
}
|
||||
<div className="mb-4">
|
||||
<div
|
||||
className="flex items-center gap-2 mb-2 cursor-pointer"
|
||||
onClick={toggleCollapse}
|
||||
>
|
||||
<span className="text-sm font-semibold text-gray-300">
|
||||
{categoryName} ({categorySkills.length})
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{collapsed ? '▼' : '▶'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
// Get tier info
|
||||
const currentTier = store.skillTiers?.[id] || 1;
|
||||
const tieredSkillId = currentTier > 1 ? `${id}_t${currentTier}` : id;
|
||||
const tierMultiplier = getTierMultiplier(tieredSkillId);
|
||||
{!collapsed && (
|
||||
<div className="space-y-2">
|
||||
{categorySkills.map(([skillId, def]) => {
|
||||
const skillLevel = skills[skillId] || 0;
|
||||
const tier = skillTiers[skillId] || 0;
|
||||
const tierMult = getTierMultiplier(skillId)(tier);
|
||||
const isStudying = currentStudyTarget?.id === skillId;
|
||||
const isParallel = currentStudyTarget?.type === 'parallel' && currentStudyTarget?.id === skillId;
|
||||
|
||||
// Get the actual level from the tiered skill
|
||||
const level = store.skills[tieredSkillId] || store.skills[id] || 0;
|
||||
const maxed = level >= def.max;
|
||||
// Get upgrade choices for this skill
|
||||
const store = useGameStore.getState();
|
||||
const { available, selected } = store.getSkillUpgradeChoices(skillId, tier as 5 | 10);
|
||||
|
||||
// Check if studying this skill
|
||||
const isStudying =
|
||||
(store.currentStudyTarget?.id === id || store.currentStudyTarget?.id === tieredSkillId) &&
|
||||
store.currentStudyTarget?.type === 'skill';
|
||||
|
||||
// Get tier name for display
|
||||
const tierDef = SKILL_EVOLUTION_PATHS[id]?.tiers.find((t) => t.tier === currentTier);
|
||||
const skillDisplayName = tierDef?.name || def.name;
|
||||
|
||||
// Check prerequisites
|
||||
let prereqMet = true;
|
||||
if (def.req) {
|
||||
for (const [r, rl] of Object.entries(def.req)) {
|
||||
if ((store.skills[r] || 0) < rl) {
|
||||
prereqMet = false;
|
||||
break;
|
||||
return (
|
||||
<SkillRow
|
||||
key={skillId}
|
||||
skillId={skillId}
|
||||
skillDef={def}
|
||||
skillLevel={skillLevel}
|
||||
tier={tier}
|
||||
tierMult={tierMult}
|
||||
isStudying={isStudying}
|
||||
isParallel={isParallel}
|
||||
studySpeedMult={studySpeedMult}
|
||||
upgradeEffects={upgradeEffects}
|
||||
availableUpgrades={available}
|
||||
selectedUpgrades={selected}
|
||||
pendingSelections={pendingSelections}
|
||||
onToggleUpgrade={(upgradeId) => {
|
||||
if (pendingSelections.includes(upgradeId)) {
|
||||
setPendingSelections(pendingSelections.filter(id => id !== upgradeId));
|
||||
} else {
|
||||
setPendingSelections([...pendingSelections, upgradeId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply skill modifiers
|
||||
const costMult = getStudyCostMultiplier(store.skills);
|
||||
const speedMult = getStudySpeedMultiplier(store.skills);
|
||||
const studyEffects = getUnifiedEffects(store);
|
||||
const effectiveSpeedMult = speedMult * studyEffects.studySpeedMultiplier;
|
||||
|
||||
// Study time scales with tier
|
||||
const tierStudyTime = def.studyTime * currentTier;
|
||||
const effectiveStudyTime = tierStudyTime / effectiveSpeedMult;
|
||||
|
||||
// Cost scales with tier
|
||||
const baseCost = def.base * (level + 1) * currentTier;
|
||||
const cost = Math.floor(baseCost * costMult);
|
||||
|
||||
// Additional cost (element mana) - only pass element costs
|
||||
const additionalCost = isElementCost(def.cost)
|
||||
? { type: 'element' as const, element: def.cost.element, amount: def.cost.amount }
|
||||
: undefined;
|
||||
|
||||
// Can start studying?
|
||||
let canStudy = !maxed && prereqMet && store.rawMana >= cost && !isStudying;
|
||||
|
||||
// Check additional cost (element mana)
|
||||
if (isElementCost(def.cost)) {
|
||||
const element = store.elements[def.cost.element];
|
||||
if (!element || element.current < def.cost.amount) {
|
||||
canStudy = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for milestone upgrades
|
||||
const milestoneInfo = hasMilestoneUpgrade(
|
||||
tieredSkillId,
|
||||
level,
|
||||
store.skillTiers || {},
|
||||
store.skillUpgrades
|
||||
);
|
||||
|
||||
// Check for tier up
|
||||
const nextTierSkill = getNextTierSkill(tieredSkillId);
|
||||
const canTierUp = Boolean(maxed && nextTierSkill);
|
||||
|
||||
// Get selected upgrades
|
||||
const selectedUpgrades = store.skillUpgrades[tieredSkillId] || [];
|
||||
const selectedL5 = selectedUpgrades.filter((u) => u.includes('_l5'));
|
||||
const selectedL10 = selectedUpgrades.filter((u) => u.includes('_l10'));
|
||||
|
||||
// Check if insufficient mana for toast
|
||||
const hasInsufficientMana = !isStudying && !maxed && store.rawMana < cost;
|
||||
|
||||
// Check for parallel study eligibility
|
||||
const isParallelStudy =
|
||||
store.currentStudyTarget?.id === tieredSkillId &&
|
||||
store.currentStudyTarget?.type === 'skill';
|
||||
const canParallelStudy: boolean =
|
||||
hasSpecial(studyEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) &&
|
||||
!!store.currentStudyTarget &&
|
||||
store.currentStudyTarget.id !== tieredSkillId &&
|
||||
!isStudying;
|
||||
|
||||
return (
|
||||
<SkillRow
|
||||
key={id}
|
||||
skillId={tieredSkillId}
|
||||
def={def as any}
|
||||
level={level}
|
||||
maxed={maxed}
|
||||
isStudying={isStudying}
|
||||
tierMultiplier={tierMultiplier}
|
||||
skillDisplayName={skillDisplayName}
|
||||
selectedUpgrades={selectedUpgrades}
|
||||
selectedL5={selectedL5}
|
||||
selectedL10={selectedL10}
|
||||
prereqMet={prereqMet}
|
||||
canStudy={canStudy}
|
||||
isParallelStudy={isParallelStudy}
|
||||
canParallelStudy={canParallelStudy}
|
||||
canTierUp={canTierUp}
|
||||
hasInsufficientMana={hasInsufficientMana}
|
||||
currentStudyTarget={currentStudyTarget}
|
||||
milestoneInfo={milestoneInfo}
|
||||
upgradeEffects={upgradeEffects}
|
||||
cost={cost}
|
||||
additionalCost={additionalCost}
|
||||
effectiveStudyTime={effectiveStudyTime}
|
||||
costMult={costMult}
|
||||
speedMult={speedMult}
|
||||
onStudy={onStartStudying}
|
||||
onParallelStudy={onParallelStudy}
|
||||
onCancelStudy={handleCancelStudyInternal}
|
||||
onUpgradeDialogOpen={onOpenUpgradeDialog}
|
||||
onTierUp={onTierUp}
|
||||
onShowToast={showToast}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
}}
|
||||
onStartStudying={() => onStartStudying(skillId)}
|
||||
onParallelStudy={() => onParallelStudy(skillId)}
|
||||
onTierUp={() => onTierUp(skillId)}
|
||||
onOpenUpgradeDialog={(milestone) => onOpenUpgradeDialog(skillId, milestone)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,29 +6,25 @@ import { GameCard } from '@/components/ui/game-card';
|
||||
import { SectionHeader } from '@/components/ui/section-header';
|
||||
import { ActionButton } from '@/components/ui/action-button';
|
||||
import { Scroll, Hammer, Sparkles, Anvil } from 'lucide-react';
|
||||
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
|
||||
import { fmt, type GameStore } from '@/lib/game/store';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import {
|
||||
EnchantmentDesigner,
|
||||
EnchantmentPreparer,
|
||||
EnchantmentApplier,
|
||||
EquipmentCrafter,
|
||||
} from '@/components/game/crafting';
|
||||
import { useGameStore } from '@/lib/game/stores';
|
||||
import { useGameToast } from '@/components/game/GameToast';
|
||||
|
||||
export interface CraftingTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
export function CraftingTab({ store }: CraftingTabProps) {
|
||||
export function CraftingTab() {
|
||||
const showToast = useGameToast();
|
||||
const currentAction = store.currentAction;
|
||||
const designProgress = store.designProgress;
|
||||
const preparationProgress = store.preparationProgress;
|
||||
const applicationProgress = store.applicationProgress;
|
||||
const equipmentCraftingProgress = store.equipmentCraftingProgress;
|
||||
const pauseApplication = store.pauseApplication;
|
||||
const resumeApplication = store.resumeApplication;
|
||||
const currentAction = useGameStore((s) => s.currentAction);
|
||||
const designProgress = useGameStore((s) => s.designProgress);
|
||||
const preparationProgress = useGameStore((s) => s.preparationProgress);
|
||||
const applicationProgress = useGameStore((s) => s.applicationProgress);
|
||||
const equipmentCraftingProgress = useGameStore((s) => s.equipmentCraftingProgress);
|
||||
const pauseApplication = useGameStore((s) => s.pauseApplication);
|
||||
const resumeApplication = useGameStore((s) => s.resumeApplication);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'fabricate' | 'enchant'>('fabricate');
|
||||
const [enchantStage, setEnchantStage] = useState<'design' | 'prepare' | 'apply'>('design');
|
||||
@@ -81,7 +77,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
|
||||
{/* Fabricate Content: EquipmentCrafter */}
|
||||
{activeTab === 'fabricate' && (
|
||||
<EquipmentCrafter store={store} />
|
||||
<EquipmentCrafter />
|
||||
)}
|
||||
|
||||
{/* Enchant Content: Design → Prepare → Apply workflow */}
|
||||
@@ -123,7 +119,6 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
{/* Enchant Stage Content */}
|
||||
{enchantStage === 'design' && (
|
||||
<EnchantmentDesigner
|
||||
store={store}
|
||||
selectedEquipmentType={null}
|
||||
setSelectedEquipmentType={() => {}}
|
||||
selectedEffects={[]}
|
||||
@@ -136,14 +131,12 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
)}
|
||||
{enchantStage === 'prepare' && (
|
||||
<EnchantmentPreparer
|
||||
store={store}
|
||||
selectedEquipmentInstance={null}
|
||||
setSelectedEquipmentInstance={() => {}}
|
||||
/>
|
||||
)}
|
||||
{enchantStage === 'apply' && (
|
||||
<EnchantmentApplier
|
||||
store={store}
|
||||
selectedEquipmentInstance={null}
|
||||
setSelectedEquipmentInstance={() => {}}
|
||||
selectedDesign={null}
|
||||
@@ -183,7 +176,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
<SectionHeader
|
||||
title="Designing Enchantment"
|
||||
action={
|
||||
<ActionButton variant="ghost" size="sm" onClick={() => store.cancelDesign()}>
|
||||
<ActionButton variant="ghost" size="sm" onClick={() => useGameStore.getState().cancelDesign()}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
}
|
||||
@@ -205,7 +198,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
<SectionHeader
|
||||
title="Preparing Equipment"
|
||||
action={
|
||||
<ActionButton variant="ghost" size="sm" onClick={() => store.cancelPreparation()}>
|
||||
<ActionButton variant="ghost" size="sm" onClick={() => useGameStore.getState().cancelPreparation()}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
}
|
||||
@@ -237,7 +230,7 @@ export function CraftingTab({ store }: CraftingTabProps) {
|
||||
<>
|
||||
<ActionButton variant="secondary" size="sm" onClick={pauseApplication}>Pause</ActionButton>
|
||||
<ActionButton variant="ghost" size="sm" onClick={() => {
|
||||
store.cancelApplication();
|
||||
useGameStore.getState().cancelApplication();
|
||||
showToast('warning', 'Enchantment Cancelled', 'The enchantment application was cancelled.');
|
||||
}}>Cancel</ActionButton>
|
||||
</>
|
||||
|
||||
@@ -122,7 +122,7 @@ export function EquipmentTab() {
|
||||
// Use modular store directly - MUST be called before any conditional returns
|
||||
const equippedInstances = useCombatStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useCombatStore((s) => s.equipmentInstances);
|
||||
|
||||
|
||||
// Get unequipped items - hooks must be called before conditional returns
|
||||
const equippedIds = useMemo(() =>
|
||||
new Set(Object.values(equippedInstances || {}).filter(Boolean)),
|
||||
@@ -230,8 +230,8 @@ export function EquipmentTab() {
|
||||
// Get unified effects for equipment stats - move hook before conditional
|
||||
const equipmentInstancesForEffects = useCombatStore((s) => s.equipmentInstances);
|
||||
const equippedInstancesForEffects = useCombatStore((s) => s.equippedInstances);
|
||||
|
||||
const unifiedEffects = equipmentInstancesForEffects && equippedInstancesForEffects
|
||||
|
||||
const unifiedEffects = equipmentInstancesForEffects && equippedInstancesForEffects
|
||||
? getUnifiedEffects({ equipmentInstances, equippedInstances })
|
||||
: null;
|
||||
|
||||
@@ -329,10 +329,10 @@ export function EquipmentTab() {
|
||||
const enchantPower = effects.enchantmentPowerMultiplier || 1;
|
||||
return (
|
||||
<>
|
||||
<StatRow
|
||||
<StatRow
|
||||
label="Enchantment Power:"
|
||||
value={`${enchantPower.toFixed(2)}×`}
|
||||
highlight={enchantPower > 1 ? "success" : "default"}
|
||||
highlight={enchantPower > 1 ? 'success' : 'default'}
|
||||
/>
|
||||
<p className="text-xs text-[var(--text-muted)] mt-2">
|
||||
Increases the power of all enchantments by {(enchantPower - 1) * 100}%. Multiplier applied to all enchantment effects.
|
||||
@@ -353,11 +353,11 @@ export function EquipmentTab() {
|
||||
return <span className="text-[var(--text-muted)] text-sm">No active effects</span>;
|
||||
}
|
||||
const effectEntries = Object.entries(effects.equipmentEffects).filter(([, v]) => v > 0);
|
||||
|
||||
|
||||
if (effectEntries.length === 0) {
|
||||
return <span className="text-[var(--text-muted)] text-sm">No active effects</span>;
|
||||
}
|
||||
|
||||
|
||||
return effectEntries.map(([stat, value]) => (
|
||||
<Badge key={stat} variant="outline" className="text-xs border-[var(--border-default)] text-[var(--text-secondary)]">
|
||||
{stat}: +{fmt(value)}
|
||||
|
||||
@@ -3,70 +3,67 @@
|
||||
import { GameCard, StatRow, ElementBadge, ActionButton } from '@/components/ui';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Mountain, Zap, Clock, Swords, Target, Sparkles, Lock, Check, X,
|
||||
import {
|
||||
Mountain, Zap, Clock, Swords, Sparkles, Lock, Check, X,
|
||||
Info, HelpCircle
|
||||
} from 'lucide-react';
|
||||
import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration } from '@/lib/game/data/golems';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import type { GameStore } from '@/lib/game/store';
|
||||
import { useGameStore, useManaStore, useSkillStore, useCombatStore } from '@/lib/game/stores';
|
||||
|
||||
export interface GolemancyTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
export function GolemancyTab() {
|
||||
const attunements = useGameStore((s) => s.attunements);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const golemancy = useGameStore((s) => s.golemancy);
|
||||
const currentFloor = useCombatStore((s) => s.currentFloor);
|
||||
const currentRoom = useGameStore((s) => s.currentRoom);
|
||||
const toggleGolem = useGameStore((s) => s.toggleGolem);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
|
||||
export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
const attunements = store.attunements;
|
||||
const elements = store.elements;
|
||||
const skills = store.skills;
|
||||
const golemancy = store.golemancy;
|
||||
const currentFloor = store.currentFloor;
|
||||
const currentRoom = store.currentRoom;
|
||||
const toggleGolem = store.toggleGolem;
|
||||
|
||||
// Get Fabricator level and golem slots
|
||||
const fabricatorLevel = attunements.fabricator?.level || 0;
|
||||
const fabricatorActive = attunements.fabricator?.active || false;
|
||||
const maxSlots = getGolemSlots(fabricatorLevel);
|
||||
|
||||
|
||||
// Get unlocked elements
|
||||
const unlockedElements = Object.entries(elements)
|
||||
.filter(([, e]) => e.unlocked)
|
||||
.map(([id]) => id);
|
||||
|
||||
|
||||
// Get all unlocked golems
|
||||
const unlockedGolems = Object.values(GOLEMS_DEF || {}).filter(golem =>
|
||||
const unlockedGolems = Object.values(GOLEMS_DEF || {}).filter(golem =>
|
||||
isGolemUnlocked(golem.id, attunements, unlockedElements)
|
||||
);
|
||||
|
||||
|
||||
// Check if golemancy is available
|
||||
const hasGolemancy = fabricatorActive && fabricatorLevel >= 2;
|
||||
|
||||
|
||||
// Check if currently in combat (not puzzle)
|
||||
const inCombat = currentRoom.roomType !== 'puzzle';
|
||||
|
||||
const inCombat = currentRoom?.roomType !== 'puzzle';
|
||||
|
||||
// Get element info helper
|
||||
const getElementInfo = (elementId: string) => {
|
||||
return ELEMENTS[elementId];
|
||||
};
|
||||
|
||||
|
||||
// Render a golem card
|
||||
const renderGolemCard = (golemId: string, isUnlocked: boolean) => {
|
||||
const golem = GOLEMS_DEF[golemId];
|
||||
if (!golem) return null;
|
||||
|
||||
|
||||
const isEnabled = golemancy.enabledGolems.includes(golemId);
|
||||
const isSelected = golemancy.summonedGolems.some(g => g.golemId === golemId);
|
||||
|
||||
|
||||
// Calculate effective stats
|
||||
const damage = getGolemDamage(golemId, skills);
|
||||
const attackSpeed = getGolemAttackSpeed(golemId, skills);
|
||||
const floorDuration = getGolemFloorDuration(skills);
|
||||
|
||||
|
||||
// Get element color
|
||||
const primaryElement = getElementInfo(golem.baseManaType);
|
||||
const elementId = golem.baseManaType;
|
||||
|
||||
|
||||
if (!isUnlocked) {
|
||||
// Locked golem card
|
||||
return (
|
||||
@@ -91,14 +88,14 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
</GameCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<GameCard
|
||||
key={golemId}
|
||||
<GameCard
|
||||
key={golemId}
|
||||
variant={isEnabled ? "default" : "sunken"}
|
||||
className={`transition-all cursor-pointer border-2 ${
|
||||
isEnabled
|
||||
? 'border-[var(--color-success)] bg-[var(--bg-surface)]'
|
||||
isEnabled
|
||||
? 'border-[var(--color-success)] bg-[var(--bg-surface)]'
|
||||
: 'border-[var(--border-subtle)] hover:border-[var(--border-default)]'
|
||||
}`}
|
||||
onClick={() => toggleGolem(golemId)}
|
||||
@@ -119,7 +116,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
||||
T{golem.tier}
|
||||
{golem.tier}
|
||||
</span>
|
||||
{isEnabled ? (
|
||||
<Check className="w-4 h-4 text-[var(--color-success)]" />
|
||||
@@ -131,35 +128,35 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-[var(--text-secondary)]">{golem.description}</p>
|
||||
|
||||
|
||||
<Separator className="bg-[var(--border-subtle)]" />
|
||||
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<StatRow label="DMG:" value={damage.toString()} />
|
||||
<StatRow label="Speed:" value={`${attackSpeed.toFixed(1)}/hr`} />
|
||||
<StatRow label="Pierce:" value={`${Math.floor(golem.armorPierce * 100)}%`} />
|
||||
<StatRow label="Duration:" value={`${floorDuration} floor(s)`} />
|
||||
</div>
|
||||
|
||||
|
||||
<Separator className="bg-[var(--border-subtle)]" />
|
||||
|
||||
|
||||
{/* Summon Cost */}
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">Summon Cost:</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{golem.summonCost.map((cost, idx) => {
|
||||
const elem = getElementInfo(cost.element || '');
|
||||
const available = cost.type === 'raw'
|
||||
? store.rawMana
|
||||
const available = cost.type === 'raw'
|
||||
? rawMana
|
||||
: elements[cost.element || '']?.current || 0;
|
||||
const canAfford = available >= cost.amount;
|
||||
|
||||
|
||||
return (
|
||||
<span
|
||||
<span
|
||||
key={idx}
|
||||
className={`text-xs px-1.5 py-0.5 border rounded ${
|
||||
canAfford
|
||||
? 'border-[var(--color-success)] text-[var(--color-success)]'
|
||||
canAfford
|
||||
? 'border-[var(--color-success)] text-[var(--color-success)]'
|
||||
: 'border-[var(--color-danger)] text-[var(--color-danger)]'
|
||||
}`}
|
||||
>
|
||||
@@ -170,7 +167,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Maintenance Cost */}
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">Maintenance/hr:</div>
|
||||
@@ -185,7 +182,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Status */}
|
||||
{isSelected && (
|
||||
<div className="mt-2 text-xs text-[var(--color-success)] flex items-center gap-1">
|
||||
@@ -197,7 +194,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
</GameCard>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
@@ -216,26 +213,26 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<StatRow
|
||||
label="Golem Slots:"
|
||||
<StatRow
|
||||
label="Golem Slots:"
|
||||
value={`${golemancy.enabledGolems.length} / ${maxSlots}`}
|
||||
highlight={golemancy.enabledGolems.length > 0 ? 'success' : undefined}
|
||||
/>
|
||||
<StatRow
|
||||
label="Fabricator Level:"
|
||||
<StatRow
|
||||
label="Fabricator Level:"
|
||||
value={fabricatorLevel.toString()}
|
||||
highlight="warning"
|
||||
/>
|
||||
<StatRow
|
||||
label="Floor Duration:"
|
||||
<StatRow
|
||||
label="Floor Duration:"
|
||||
value={`${getGolemFloorDuration(skills)} floor(s)`}
|
||||
/>
|
||||
<StatRow
|
||||
label="Status:"
|
||||
<StatRow
|
||||
label="Status:"
|
||||
value={inCombat ? 'Combat Active' : 'Puzzle Room (No Golems)'}
|
||||
highlight={inCombat ? 'success' : 'warning'}
|
||||
/>
|
||||
|
||||
|
||||
<p className="text-xs text-[var(--text-muted)] mt-2">
|
||||
Golems are automatically summoned at the start of each combat floor.
|
||||
They cost mana to maintain and will be dismissed if you run out.
|
||||
@@ -244,7 +241,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
)}
|
||||
</div>
|
||||
</GameCard>
|
||||
|
||||
|
||||
{/* Active Golems - Empty State */}
|
||||
{hasGolemancy && golemancy.summonedGolems.length === 0 && (
|
||||
<GameCard variant="sunken">
|
||||
@@ -255,12 +252,12 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
|
||||
{/* Active Golems */}
|
||||
{hasGolemancy && golemancy.summonedGolems.length > 0 && (
|
||||
<GameCard variant="default" className="border-[var(--color-success)]">
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--color-success)] flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-2 text-[var(--color-success)]">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Active Golems ({golemancy.summonedGolems.length})
|
||||
</h3>
|
||||
@@ -269,7 +266,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
{golemancy.summonedGolems.map(sg => {
|
||||
const golem = GOLEMS_DEF[sg.golemId];
|
||||
if (!golem) return null;
|
||||
|
||||
|
||||
return (
|
||||
<span key={sg.golemId} className="text-xs px-2 py-1 border border-[var(--border-default)] rounded">
|
||||
<Mountain className="w-3 h-3 inline mr-1" style={{ color: `var(--mana-${golem.baseManaType})` }} />
|
||||
@@ -280,7 +277,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
|
||||
{/* Golem Selection */}
|
||||
{hasGolemancy && (
|
||||
<GameCard>
|
||||
@@ -291,7 +288,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pr-4">
|
||||
{/* Unlocked Golems */}
|
||||
{unlockedGolems.map(golem => renderGolemCard(golem.id, true))}
|
||||
|
||||
|
||||
{/* Locked Golems */}
|
||||
{Object.values(GOLEMS_DEF || {})
|
||||
.filter(g => !isGolemUnlocked(g.id, attunements, unlockedElements))
|
||||
@@ -300,7 +297,7 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
</ScrollArea>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
|
||||
{/* Golemancy Skills Info */}
|
||||
<GameCard>
|
||||
<div className="pb-2">
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import type { GameStore } from '@/lib/game/store';
|
||||
import { useGameStore } from '@/lib/game/stores';
|
||||
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
||||
|
||||
export interface LootTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
export function LootTab() {
|
||||
const lootInventory = useGameStore((s) => s.lootInventory);
|
||||
const elements = useGameStore((s) => s.elements);
|
||||
const equipmentInstances = useGameStore((s) => s.equipmentInstances);
|
||||
const deleteMaterial = useGameStore((s) => s.deleteMaterial);
|
||||
const deleteEquipmentInstance = useGameStore((s) => s.deleteEquipmentInstance);
|
||||
|
||||
export function LootTab({ store }: LootTabProps) {
|
||||
const inventory = store.lootInventory;
|
||||
const elements = store.elements;
|
||||
const equipmentInstances = store.equipmentInstances;
|
||||
|
||||
return (
|
||||
<LootInventoryDisplay
|
||||
inventory={inventory}
|
||||
elements={elements}
|
||||
equipmentInstances={equipmentInstances}
|
||||
onDeleteMaterial={store.deleteMaterial}
|
||||
onDeleteEquipment={store.deleteEquipmentInstance}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<LootInventoryDisplay
|
||||
inventory={lootInventory}
|
||||
elements={elements}
|
||||
equipmentInstances={equipmentInstances}
|
||||
onDeleteMaterial={deleteMaterial}
|
||||
onDeleteEquipment={deleteEquipmentInstance}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useGameStore } from '@/lib/game/store';
|
||||
import { useGameStore, usePrestigeStore, useSkillStore, useManaStore } from '@/lib/game/stores';
|
||||
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import {
|
||||
@@ -15,13 +15,19 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { fmt } from '@/lib/game/computed-stats';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
|
||||
export function PrestigeTab() {
|
||||
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
||||
|
||||
const store = useGameStore();
|
||||
useGameLoop();
|
||||
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
|
||||
const upgradeEffects = getUnifiedEffects(store);
|
||||
|
||||
// Get unlocked elements for mana type selector
|
||||
@@ -38,3 +44,62 @@ export function PrestigeTab() {
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* Prestige Upgrades */}
|
||||
{Object.entries(PRESTIGE_DEF || {}).map(([id, def]) => {
|
||||
const level = prestigeUpgrades[id] || 0;
|
||||
const canAfford = rawMana >= def.cost;
|
||||
const effect = upgradeEffects ? upgradeEffects.specials.has(id) : false;
|
||||
|
||||
return (
|
||||
<Card key={id} className={effect ? "border-[var(--color-success)]/50 bg-[var(--color-success)]/10" : "bg-gray-900/80 border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<span>{def.name}</span>
|
||||
{effect && <Badge className="bg-[var(--color-success)]/20 text-[var(--color-success)]">Active</Badge>}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-gray-400 mb-2">{def.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400">Level: {level}/{def.maxLevel}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!canAfford || level >= def.maxLevel}
|
||||
onClick={() => store.doPrestige(id)}
|
||||
>
|
||||
{level >= def.maxLevel ? 'Maxed' : `Upgrade (${fmt(def.cost)})`}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Mana Type Selection for Attunements */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Select Mana Type for Attunement</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{unlockedElements.map(elem => (
|
||||
<Button
|
||||
key={elem.id}
|
||||
variant={selectedManaType === elem.id ? "default" : "outline"}
|
||||
onClick={() => setSelectedManaType(elem.id)}
|
||||
className="justify-start"
|
||||
>
|
||||
<span className="mr-2" style={{ color: elem.color }}>{elem.sym}</span>
|
||||
{elem.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
PrestigeTab.displayName = "PrestigeTab";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// ─── Skills Tab ───────────────────────────────────────────────────
|
||||
// ─── Skills Tab ───────────────────────────────────────────────────────────────
|
||||
// SkillsTab - Displays all skills organized by category
|
||||
// Refactored: extracted components for better modularity (reduced from 400 lines)
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from '@/lib/game/skill-evolution';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { getAvailableSkillCategories } from '@/lib/game/data/attunements';
|
||||
import { fmt, fmtDec } from '@/lib/game/store';
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -39,13 +39,9 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { SkillRow } from './SkillRow';
|
||||
import { useSkillUpgradeSelection } from '@/lib/game/hooks/useSkillUpgradeSelection';
|
||||
import { CategorySkillsList } from './CategorySkillsList';
|
||||
import type { GameStore } from '@/lib/game/store';
|
||||
import { useGameStore, useSkillStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
|
||||
export interface SkillsTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
export function SkillsTab({ store }: SkillsTabProps) {
|
||||
export function SkillsTab() {
|
||||
const showToast = useGameToast();
|
||||
const [upgradeDialogSkill, setUpgradeDialogSkill] = useState<string | null>(null);
|
||||
const [upgradeDialogMilestone, setUpgradeDialogMilestone] = useState<5 | 10>(5);
|
||||
@@ -55,8 +51,20 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
skillName: string;
|
||||
} | null>(null);
|
||||
|
||||
const studySpeedMult = getStudySpeedMultiplier(store.skills);
|
||||
const upgradeEffects = getUnifiedEffects(store as any);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const currentStudyTarget = useGameStore((s) => s.currentStudyTarget);
|
||||
const parallelStudyTarget = useGameStore((s) => s.parallelStudyTarget);
|
||||
const startStudyingSkill = useSkillStore((s) => s.startStudyingSkill);
|
||||
const startParallelStudySkill = useSkillStore((s) => s.startParallelStudySkill);
|
||||
const cancelStudy = useGameStore((s) => s.cancelStudy);
|
||||
const commitSkillUpgrades = useSkillStore((s) => s.commitSkillUpgrades);
|
||||
const tierUpSkill = useSkillStore((s) => s.tierUpSkill);
|
||||
|
||||
const studySpeedMult = getStudySpeedMultiplier({ skills, prestigeUpgrades, skillUpgrades, skillTiers });
|
||||
const upgradeEffects = getUnifiedEffects({ skillUpgrades, skillTiers, equippedInstances: {}, equipmentInstances: {} });
|
||||
|
||||
// Upgrade selection hook
|
||||
const {
|
||||
@@ -84,7 +92,12 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
const getUpgradeChoices = () => {
|
||||
if (!upgradeDialogSkill)
|
||||
return { available: [] as SkillUpgradeChoice[], selected: [] as string[] };
|
||||
return store.getSkillUpgradeChoices(upgradeDialogSkill, upgradeDialogMilestone);
|
||||
const skillDef = SKILLS_DEF[upgradeDialogSkill.includes('_t') ? upgradeDialogSkill.split('_t')[0] : upgradeDialogSkill];
|
||||
if (!skillDef) return { available: [] as SkillUpgradeChoice[], selected: [] as string[] };
|
||||
return {
|
||||
available: getUpgradesForSkillAtMilestone(upgradeDialogSkill, upgradeDialogMilestone),
|
||||
selected: skillUpgrades[upgradeDialogSkill] || [],
|
||||
};
|
||||
};
|
||||
|
||||
const { available, selected: alreadySelected } = getUpgradeChoices();
|
||||
@@ -92,11 +105,12 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
// Handle upgrade dialog confirm
|
||||
const handleConfirm = () => {
|
||||
hookHandleConfirm(
|
||||
upgradeDialogSkill,
|
||||
upgradeDialogSkill!,
|
||||
upgradeDialogMilestone,
|
||||
(skillId, selections, milestone) =>
|
||||
store.commitSkillUpgrades(skillId, selections, milestone),
|
||||
() => setUpgradeDialogSkill(null)
|
||||
(skillId: string, selections: string[], milestone: 5 | 10) => {
|
||||
commitSkillUpgrades(skillId, selections, milestone);
|
||||
return () => setUpgradeDialogSkill(null);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,20 +130,20 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
// Handle study start with toast
|
||||
const handleStartStudying = (skillId: string) => {
|
||||
const skillDef = SKILLS_DEF[skillId.includes('_t') ? skillId.split('_t')[0] : skillId];
|
||||
store.startStudyingSkill(skillId);
|
||||
startStudyingSkill(skillId);
|
||||
showToast('info', 'Study Started', `Studying ${skillDef?.name || 'skill'}...`);
|
||||
};
|
||||
|
||||
// Handle parallel study start with toast
|
||||
const handleParallelStudy = (skillId: string) => {
|
||||
const skillDef = SKILLS_DEF[skillId.includes('_t') ? skillId.split('_t')[0] : skillId];
|
||||
store.startParallelStudySkill(skillId);
|
||||
startParallelStudySkill(skillId);
|
||||
showToast('info', 'Parallel Study Started', `Studying ${skillDef?.name || 'skill'} in parallel (50% speed)...`);
|
||||
};
|
||||
|
||||
// Handle study cancel with confirmation
|
||||
const handleCancelStudy = () => {
|
||||
const currentTarget = store.currentStudyTarget;
|
||||
const currentTarget = currentStudyTarget;
|
||||
if (currentTarget?.type === 'skill') {
|
||||
const skillDef = SKILLS_DEF[currentTarget.id.includes('_t') ? currentTarget.id.split('_t')[0] : currentTarget.id];
|
||||
setCancelStudyConfirm({
|
||||
@@ -141,7 +155,7 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
|
||||
const confirmCancelStudy = () => {
|
||||
if (cancelStudyConfirm) {
|
||||
store.cancelStudy();
|
||||
cancelStudy();
|
||||
showToast(
|
||||
'warning',
|
||||
'Study Cancelled',
|
||||
@@ -152,7 +166,8 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
};
|
||||
|
||||
// Get available skill categories based on attunements
|
||||
const availableCategories = getAvailableSkillCategories(store.attunements || {});
|
||||
const attunements = useGameStore((s) => s.attunements);
|
||||
const availableCategories = getAvailableSkillCategories(attunements || {});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -189,12 +204,12 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
)}
|
||||
|
||||
{/* Current Study Progress */}
|
||||
{store.currentStudyTarget && store.currentStudyTarget.type === 'skill' && (
|
||||
{currentStudyTarget && currentStudyTarget.type === 'skill' && (
|
||||
<Card className="bg-gray-900/80 border-purple-600/50">
|
||||
<CardContent className="pt-4">
|
||||
<StudyProgress
|
||||
currentStudyTarget={store.currentStudyTarget}
|
||||
skills={store.skills}
|
||||
currentStudyTarget={currentStudyTarget}
|
||||
skills={skills}
|
||||
studySpeedMult={studySpeedMult}
|
||||
cancelStudy={handleCancelStudy}
|
||||
/>
|
||||
@@ -210,10 +225,13 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
availableCategories={availableCategories}
|
||||
isCollapsed={collapsedCategories.has(cat.id)}
|
||||
onToggleCategory={toggleCategory}
|
||||
store={store}
|
||||
skills={skills}
|
||||
skillUpgrades={skillUpgrades}
|
||||
skillTiers={skillTiers}
|
||||
prestigeUpgrades={prestigeUpgrades}
|
||||
studySpeedMult={studySpeedMult}
|
||||
upgradeEffects={upgradeEffects}
|
||||
currentStudyTarget={store.currentStudyTarget}
|
||||
currentStudyTarget={currentStudyTarget}
|
||||
onStartStudying={handleStartStudying}
|
||||
onParallelStudy={handleParallelStudy}
|
||||
onCancelStudy={handleCancelStudy}
|
||||
@@ -222,7 +240,7 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
setUpgradeDialogMilestone(milestone);
|
||||
setPendingSelections([]);
|
||||
}}
|
||||
onTierUp={(skillId) => store.tierUpSkill(skillId)}
|
||||
onTierUp={(skillId) => tierUpSkill(skillId)}
|
||||
pendingSelections={pendingSelections}
|
||||
setPendingSelections={setPendingSelections}
|
||||
/>
|
||||
@@ -230,4 +248,5 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SkillsTab.displayName = "SkillsTab";
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Mountain } from 'lucide-react';
|
||||
import type { ActivityLogEntry } from '@/lib/game/types';
|
||||
import type { GameStore } from '@/lib/game/store';
|
||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { calcDamage } from '@/lib/game/store';
|
||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { calcDamage } from '@/lib/game/stores';
|
||||
import { getEnemyName } from '@/lib/game/store-modules/enemy-utils';
|
||||
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
|
||||
import { canAffordSpellCost, getFloorElement } from '@/lib/game/store';
|
||||
import { canAffordSpellCost, getFloorElement } from '@/lib/game/stores';
|
||||
import { useGameStore, useManaStore, useSkillStore, useCombatStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
||||
|
||||
// Extracted components
|
||||
import { SpireHeader } from './SpireHeader';
|
||||
@@ -31,31 +33,77 @@ const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: str
|
||||
};
|
||||
|
||||
interface SpireTabProps {
|
||||
store: GameStore;
|
||||
simpleMode?: boolean;
|
||||
}
|
||||
|
||||
// Check if player can enter spire mode
|
||||
const canEnterSpireMode = (store: GameStore): boolean => {
|
||||
return !store.spireMode;
|
||||
const canEnterSpireMode = (spireMode: boolean): boolean => {
|
||||
return !spireMode;
|
||||
};
|
||||
|
||||
export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
export function SpireTab({ simpleMode = false }: SpireTabProps) {
|
||||
// Get state from modular stores
|
||||
const currentFloor = useCombatStore((s) => s.currentFloor);
|
||||
const floorHP = useCombatStore((s) => s.floorHP);
|
||||
const floorMaxHP = useCombatStore((s) => s.floorMaxHP);
|
||||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
||||
const currentAction = useCombatStore((s) => s.currentAction);
|
||||
const castProgress = useCombatStore((s) => s.castProgress);
|
||||
const activeSpell = useCombatStore((s) => s.activeSpell);
|
||||
const startClimbUp = useCombatStore((s) => s.startClimbUp);
|
||||
const startClimbDown = useCombatStore((s) => s.startClimbDown);
|
||||
const enterSpireMode = useGameStore((s) => s.enterSpireMode);
|
||||
const spireMode = useGameStore((s) => s.spireMode);
|
||||
const climbDirection = useGameStore((s) => s.climbDirection) || 'up';
|
||||
const clearedFloors = useGameStore((s) => s.clearedFloors || {});
|
||||
const currentRoom = useGameStore((s) => s.currentRoom);
|
||||
const equipmentSpellStates = useGameStore((s) => s.equipmentSpellStates);
|
||||
const golemancy = useGameStore((s) => s.golemancy);
|
||||
const activityLog = useGameStore((s) => s.activityLog);
|
||||
const currentStudyTarget = useGameStore((s) => s.currentStudyTarget);
|
||||
const parallelStudyTarget = useGameStore((s) => s.parallelStudyTarget);
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const equippedInstances = useGameStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useGameStore((s) => s.equipmentInstances);
|
||||
const designProgress = useGameStore((s) => s.designProgress);
|
||||
const designProgress2 = useGameStore((s) => s.designProgress2);
|
||||
const preparationProgress = useGameStore((s) => s.preparationProgress);
|
||||
const applicationProgress = useGameStore((s) => s.applicationProgress);
|
||||
const equipmentCraftingProgress = useGameStore((s) => s.equipmentCraftingProgress);
|
||||
|
||||
// Derived data
|
||||
const floorElem = getFloorElement(store.currentFloor);
|
||||
const floorElem = getFloorElement(currentFloor);
|
||||
const floorElemDef = ELEMENTS[floorElem];
|
||||
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
|
||||
const currentGuardian = GUARDIANS[store.currentFloor];
|
||||
const climbDirection = store.climbDirection || 'up';
|
||||
const clearedFloors = store.clearedFloors || {};
|
||||
const currentRoom = store.currentRoom;
|
||||
const isFloorCleared = clearedFloors[store.currentFloor];
|
||||
const isGuardianFloor = !!GUARDIANS[currentFloor];
|
||||
const currentGuardian = GUARDIANS[currentFloor];
|
||||
const isFloorCleared = clearedFloors[currentFloor];
|
||||
const roomType = currentRoom?.roomType || 'combat';
|
||||
const roomConfig = ROOM_TYPE_CONFIG[roomType] || ROOM_TYPE_CONFIG.combat;
|
||||
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
|
||||
const upgradeEffects = getUnifiedEffects(store);
|
||||
const totalDPS = getTotalDPS(store, upgradeEffects, floorElem);
|
||||
const studySpeedMult = 1;
|
||||
|
||||
const activeEquipmentSpells = useMemo(
|
||||
() => getActiveEquipmentSpells(equippedInstances, equipmentInstances),
|
||||
[equippedInstances, equipmentInstances]
|
||||
);
|
||||
|
||||
const upgradeEffects = useMemo(
|
||||
() => getUnifiedEffects({
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
equippedInstances,
|
||||
equipmentInstances,
|
||||
}),
|
||||
[skillUpgrades, skillTiers, equippedInstances, equipmentInstances]
|
||||
);
|
||||
|
||||
const totalDPS = useMemo(
|
||||
() => getTotalDPS({ skills, signedPacts, skillUpgrades, skillTiers }, upgradeEffects, floorElem),
|
||||
[skills, signedPacts, skillUpgrades, skillTiers, upgradeEffects, floorElem]
|
||||
);
|
||||
|
||||
// Enemy display info
|
||||
const primaryEnemy = currentRoom?.enemies?.[0] || null;
|
||||
@@ -65,18 +113,22 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
const canCastSpell = (spellId: string): boolean => {
|
||||
const spell = SPELLS_DEF[spellId];
|
||||
if (!spell) return false;
|
||||
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
|
||||
return canAffordSpellCost(spell.cost, rawMana, elements);
|
||||
};
|
||||
|
||||
// Climb handler
|
||||
const handleClimb = (direction: 'up' | 'down') => {
|
||||
if (direction === 'up') {
|
||||
store.startClimbUp();
|
||||
startClimbUp();
|
||||
} else {
|
||||
store.startClimbDown();
|
||||
startClimbDown();
|
||||
}
|
||||
};
|
||||
|
||||
const getSkillName = (skillId: string): string => {
|
||||
return SKILLS_DEF[skillId]?.name || skillId;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
{/* Enter Spire Mode - Normal mode only */}
|
||||
@@ -86,8 +138,8 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700"
|
||||
size="lg"
|
||||
onClick={() => store.enterSpireMode()}
|
||||
disabled={!canEnterSpireMode(store)}
|
||||
onClick={enterSpireMode}
|
||||
disabled={!canEnterSpireMode(spireMode)}
|
||||
>
|
||||
<Mountain className="w-5 h-5 mr-2" />
|
||||
Enter Spire Mode
|
||||
@@ -101,9 +153,9 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
|
||||
{/* Spire Header */}
|
||||
<SpireHeader
|
||||
currentFloor={store.currentFloor}
|
||||
maxFloorReached={store.maxFloorReached}
|
||||
signedPacts={store.signedPacts.length}
|
||||
currentFloor={currentFloor}
|
||||
maxFloorReached={maxFloorReached}
|
||||
signedPacts={signedPacts.length}
|
||||
isGuardianFloor={isGuardianFloor}
|
||||
roomType={roomType}
|
||||
roomLabel={roomConfig.label}
|
||||
@@ -126,7 +178,7 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
if (!spellDef) return null;
|
||||
const spellState = store.equipmentSpellStates?.find(
|
||||
const spellState = equipmentSpellStates?.find(
|
||||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
||||
);
|
||||
const progress = spellState?.castProgress || 0;
|
||||
@@ -142,9 +194,9 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>{canCast ? '✓' : '✗'}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mb-1">
|
||||
⚔️ {fmt(calcDamage(store, spellId))} dmg • <span style={{ color: getSpellCostColor(spellDef.cost) }}>{formatSpellCost(spellDef.cost)}</span> {' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
||||
⚔️ {calcDamage({ skills, signedPacts, skillUpgrades, skillTiers }, spellId, floorElem)} dmg • <span style={{ color: getSpellCostColor(spellDef.cost) }}>{formatSpellCost(spellDef.cost)}</span> {' '}• ⚡ {Math.floor(calcDamage({ skills, signedPacts, skillUpgrades, skillTiers }, spellId, floorElem) * (spellDef.castSpeed || 1))} dmg/hr
|
||||
</div>
|
||||
{store.currentAction === 'climb' && (
|
||||
{currentAction === 'climb' && (
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>Cast</span>
|
||||
@@ -167,20 +219,20 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
)}
|
||||
|
||||
{/* Summoned Golems */}
|
||||
{simpleMode && store.golemancy.summonedGolems.length > 0 && (
|
||||
{simpleMode && golemancy.summonedGolems.length > 0 && (
|
||||
<Card className="bg-gray-900/80 border-amber-600/50">
|
||||
<CardContent className="pt-4 pb-4">
|
||||
<div className="text-amber-400 text-xs font-semibold mb-3 uppercase tracking-wider flex items-center gap-2">
|
||||
<Mountain className="w-4 h-4" />
|
||||
Active Golems ({store.golemancy.summonedGolems.length})
|
||||
Active Golems ({golemancy.summonedGolems.length})
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{store.golemancy.summonedGolems.map((summoned) => {
|
||||
const golemDef = getGolemDef(summoned.golemId);
|
||||
{golemancy.summonedGolems.map((summoned) => {
|
||||
const golemDef = GOLEMS_DEF[summoned.golemId];
|
||||
if (!golemDef) return null;
|
||||
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
|
||||
const damage = getGolemDamage(summoned.golemId, store.skills);
|
||||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, store.skills);
|
||||
const damage = getGolemDamage(summoned.golemId, skills);
|
||||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, skills);
|
||||
|
||||
return (
|
||||
<div key={summoned.golemId} className="p-2 rounded bg-gray-800/30 border border-gray-700">
|
||||
@@ -192,7 +244,7 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
{golemDef.isAoe && <span className="text-xs border border-gray-600 px-1 rounded">AOE {golemDef.aoeTargets}</span>}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr</div>
|
||||
{store.currentAction === 'climb' && summoned.attackProgress > 0 && (
|
||||
{currentAction === 'climb' && summoned.attackProgress > 0 && (
|
||||
<div className="mt-1">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-0.5">
|
||||
<span>Attack</span>
|
||||
@@ -213,7 +265,7 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
|
||||
{/* Guardian Panel */}
|
||||
{isGuardianFloor && simpleMode && (
|
||||
<GuardianPanel currentFloor={store.currentFloor} floorElemDef={floorElemDef} />
|
||||
<GuardianPanel currentFloor={currentFloor} floorElemDef={floorElemDef} />
|
||||
)}
|
||||
|
||||
{/* Room Display */}
|
||||
@@ -227,10 +279,10 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
puzzleProgress={currentRoom?.puzzleProgress}
|
||||
simpleMode={true}
|
||||
floorElemDef={floorElemDef}
|
||||
floorHP={store.floorHP}
|
||||
floorMaxHP={store.floorMaxHP}
|
||||
floorHP={floorHP}
|
||||
floorMaxHP={floorMaxHP}
|
||||
totalDPS={totalDPS}
|
||||
currentAction={store.currentAction}
|
||||
currentAction={currentAction}
|
||||
activeEquipmentSpells={activeEquipmentSpells}
|
||||
/>
|
||||
)}
|
||||
@@ -238,7 +290,7 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
{/* Floor Controls */}
|
||||
{simpleMode && (
|
||||
<FloorControls
|
||||
store={store}
|
||||
storeCurrentAction={currentAction}
|
||||
climbDirection={climbDirection}
|
||||
isGuardianFloor={isGuardianFloor}
|
||||
currentRoom={currentRoom}
|
||||
@@ -255,7 +307,6 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
calcDamage={calcDamage}
|
||||
SPELLS_DEF={SPELLS_DEF}
|
||||
canCastSpell={canCastSpell}
|
||||
storeCurrentAction={store.currentAction}
|
||||
handleClimb={handleClimb}
|
||||
formatSpellCost={formatSpellCost}
|
||||
getSpellCostColor={getSpellCostColor}
|
||||
@@ -266,7 +317,7 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
{simpleMode && (
|
||||
<CombatStatsPanel
|
||||
activeEquipmentSpells={activeEquipmentSpells}
|
||||
store={store}
|
||||
storeCurrentAction={currentAction}
|
||||
totalDPS={totalDPS}
|
||||
calcDamage={calcDamage}
|
||||
formatSpellCost={formatSpellCost}
|
||||
@@ -274,27 +325,26 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
SPELLS_DEF={SPELLS_DEF}
|
||||
upgradeEffects={upgradeEffects}
|
||||
canCastSpell={canCastSpell}
|
||||
studySpeedMult={studySpeedMult}
|
||||
storeCurrentAction={store.currentAction}
|
||||
studySpeedMult={1}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Activity Log - Spire Mode only */}
|
||||
{simpleMode && <ActivityLog activityLog={store.activityLog} />}
|
||||
{simpleMode && <ActivityLog activityLog={activityLog} />}
|
||||
|
||||
{/* Study Progress - Normal mode only */}
|
||||
{!simpleMode && store.currentStudyTarget && (
|
||||
{!simpleMode && currentStudyTarget && (
|
||||
<Card className="bg-gray-900/80 border-purple-600/50">
|
||||
<CardContent className="pt-4 pb-4">
|
||||
<div className="text-xs text-gray-400 mb-2">Study: {getSkillName(store.currentStudyTarget.id)}</div>
|
||||
<div className="text-xs text-gray-400 mb-2">Study: {getSkillName(currentStudyTarget.id)}</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-purple-500" style={{ width: `${Math.min(100, (store.currentStudyTarget.progress / store.currentStudyTarget.required) * 100)}%` }} />
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-purple-500" style={{ width: `${Math.min(100, (currentStudyTarget.progress / currentStudyTarget.required) * 100)}%` }} />
|
||||
</div>
|
||||
{store.parallelStudyTarget && (
|
||||
{parallelStudyTarget && (
|
||||
<div className="mt-3 p-2 rounded border border-cyan-600/50 bg-cyan-900/20">
|
||||
<div className="text-xs text-cyan-300 mb-1">Parallel: {getSkillName(store.parallelStudyTarget.id)} (50% speed)</div>
|
||||
<div className="text-xs text-cyan-300 mb-1">Parallel: {getSkillName(parallelStudyTarget.id)} (50% speed)</div>
|
||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.parallelStudyTarget.progress / store.parallelStudyTarget.required) * 100)}%` }} />
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (parallelStudyTarget.progress / parallelStudyTarget.required) * 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -303,30 +353,30 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
)}
|
||||
|
||||
{/* Crafting Progress - Normal mode only */}
|
||||
{!simpleMode && (store.designProgress || store.preparationProgress || store.applicationProgress) && (
|
||||
{!simpleMode && (designProgress || preparationProgress || applicationProgress) && (
|
||||
<Card className="bg-gray-900/80 border-cyan-600/50">
|
||||
<CardContent className="pt-4 pb-4">
|
||||
{store.designProgress && (
|
||||
{designProgress && (
|
||||
<div className="mb-3">
|
||||
<div className="text-xs text-gray-400 mb-1">Design Progress</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.designProgress.progress / store.designProgress.required) * 100)}%` }} />
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (designProgress.progress / designProgress.required) * 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{store.preparationProgress && (
|
||||
{preparationProgress && (
|
||||
<div className="mb-3">
|
||||
<div className="text-xs text-gray-400 mb-1">Preparation Progress</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.preparationProgress.progress / store.preparationProgress.required) * 100)}%` }} />
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (preparationProgress.progress / preparationProgress.required) * 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{store.applicationProgress && (
|
||||
{applicationProgress && (
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-1">Application Progress</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.applicationProgress.progress / store.applicationProgress.required) * 100)}%` }} />
|
||||
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (applicationProgress.progress / applicationProgress.required) * 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -338,31 +388,3 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||
}
|
||||
|
||||
SpireTab.displayName = "SpireTab";
|
||||
|
||||
function getSkillName(skillId: string): string {
|
||||
const { SKILLS_DEF } = require('@/lib/game/constants');
|
||||
return SKILLS_DEF[skillId]?.name || skillId;
|
||||
}
|
||||
|
||||
function fmt(value: number): string {
|
||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||||
return value.toFixed(0);
|
||||
}
|
||||
|
||||
function getGolemDef(golemId: string) {
|
||||
const { GOLEMS_DEF } = require('@/lib/game/data/golems');
|
||||
return GOLEMS_DEF[golemId];
|
||||
}
|
||||
|
||||
function getGolemDamage(golemId: string, skills: any) {
|
||||
const { getGolemDamage } = require('@/lib/game/data/golems');
|
||||
return getGolemDamage(golemId, skills);
|
||||
}
|
||||
|
||||
function getGolemAttackSpeed(golemId: string, skills: any) {
|
||||
const { getGolemAttackSpeed } = require('@/lib/game/data/golems');
|
||||
return getGolemAttackSpeed(golemId, skills);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user