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

This commit is contained in:
Refactoring Agent
2026-05-04 13:36:10 +02:00
parent 0eabd604b0
commit 837d963b63
41 changed files with 727 additions and 3935 deletions
@@ -10,11 +10,11 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
import type { EquipmentInstance, EnchantmentDesign, AppliedEnchantment, LootInventory, EquipmentCraftingProgress, EquipmentSlot } from '@/lib/game/types';
import { fmt, type GameStore } from '@/lib/game/store';
import { fmt } from '@/lib/game/stores';
import { CheckCircle, Sparkles } from 'lucide-react';
import { useGameStore } from '@/lib/game/stores';
export interface EnchantmentApplierProps {
store: GameStore;
selectedEquipmentInstance: string | null;
setSelectedEquipmentInstance: (id: string | null) => void;
selectedDesign: string | null;
@@ -24,7 +24,6 @@ export interface EnchantmentApplierProps {
}
export function EnchantmentApplier({
store,
selectedEquipmentInstance,
setSelectedEquipmentInstance,
selectedDesign,
@@ -32,15 +31,15 @@ export function EnchantmentApplier({
onEnchantmentApplied,
onCapacityExceeded,
}: EnchantmentApplierProps) {
const equippedInstances = store.equippedInstances;
const equipmentInstances = store.equipmentInstances;
const enchantmentDesigns = store.enchantmentDesigns;
const applicationProgress = store.applicationProgress;
const rawMana = store.rawMana;
const startApplying = store.startApplying;
const pauseApplication = store.pauseApplication;
const resumeApplication = store.resumeApplication;
const cancelApplication = store.cancelApplication;
const equippedInstances = useGameStore((s) => s.equippedInstances);
const equipmentInstances = useGameStore((s) => s.equipmentInstances);
const enchantmentDesigns = useGameStore((s) => s.enchantmentDesigns);
const applicationProgress = useGameStore((s) => s.applicationProgress);
const rawMana = useGameStore((s) => s.rawMana);
const startApplying = useGameStore((s) => s.startApplying);
const pauseApplication = useGameStore((s) => s.pauseApplication);
const resumeApplication = useGameStore((s) => s.resumeApplication);
const cancelApplication = useGameStore((s) => s.cancelApplication);
// Get equipped items as array - ONLY show items tagged 'Ready for Enchantment' (requirement cr5)
const equippedItems = Object.entries(equippedInstances)
@@ -119,7 +118,8 @@ export function EnchantmentApplier({
${selectedEquipmentInstance === instance.instanceId
? 'border-[var(--mana-light)] bg-[var(--mana-light)]/10'
: 'border-[var(--border-default)] bg-[var(--bg-sunken)]/50 hover:border-[var(--border-default)]'
}`}
}
`}
onClick={() => setSelectedEquipmentInstance(instance.instanceId)}
role="button"
tabIndex={0}
@@ -159,7 +159,8 @@ export function EnchantmentApplier({
${selectedDesign === design.id
? 'border-[var(--mana-stellar)] bg-[var(--mana-stellar)]/10'
: 'border-[var(--border-default)] bg-[var(--bg-sunken)]/50 hover:border-[var(--border-default)]'
}`}
}
`}
onClick={() => setSelectedDesign(design.id)}
role="button"
tabIndex={0}
@@ -252,7 +253,7 @@ export function EnchantmentApplier({
<ul className="list-disc list-inside mt-1">
{design.effects.map(eff => (
<li key={eff.effectId} className="text-[var(--text-secondary)]">
{ENCHANTMENT_EFFECTS[eff.effectId]?.name} x{eff.stacks}
{ENCHANTMENT_EFFECT_S[eff.effectId]?.name} x{eff.stacks}
</li>
))}
</ul>
@@ -6,7 +6,6 @@ import { Separator } from '@/components/ui/separator';
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress } from '@/lib/game/types';
import { type GameStore } from '@/lib/game/store';
import type { EnchantmentDesignerProps } from './EnchantmentDesigner/types';
import { EquipmentTypeSelector } from './EnchantmentDesigner/EquipmentTypeSelector';
import { EffectSelector } from './EnchantmentDesigner/EffectSelector';
@@ -23,9 +22,9 @@ import {
addEffectToDesign,
removeEffectFromDesign,
} from './EnchantmentDesigner/utils';
import { useGameStore } from '@/lib/game/stores';
export function EnchantmentDesigner({
store,
selectedEquipmentType,
setSelectedEquipmentType,
selectedEffects,
@@ -35,13 +34,13 @@ export function EnchantmentDesigner({
selectedDesign,
setSelectedDesign,
}: EnchantmentDesignerProps) {
const enchantmentDesigns = store.enchantmentDesigns;
const designProgress = store.designProgress;
const startDesigningEnchantment = store.startDesigningEnchantment;
const cancelDesign = store.cancelDesign;
const deleteDesign = store.deleteDesign;
const unlockedEffects = store.unlockedEffects;
const skills = store.skills;
const enchantmentDesigns = useGameStore((s) => s.enchantmentDesigns);
const designProgress = useGameStore((s) => s.designProgress);
const startDesigningEnchantment = useGameStore((s) => s.startDesigningEnchantment);
const cancelDesign = useGameStore((s) => s.cancelDesign);
const deleteDesign = useGameStore((s) => s.deleteDesign);
const unlockedEffects = useGameStore((s) => s.unlockedEffects);
const skills = useGameStore((s) => s.skills);
const enchantingLevel = skills.enchanting || 0;
const efficiencyBonus = (skills.efficientEnchant || 0) * 0.05;
@@ -51,7 +50,6 @@ export function EnchantmentDesigner({
// Get capacity limit for selected equipment type
const selectedEquipmentCapacity = getEquipmentCapacity(selectedEquipmentType);
const isOverCapacity = selectedEquipmentType ? designCapacityCost > selectedEquipmentCapacity : false;
// Calculate design time
const designTime = calculateDesignTime(selectedEffects);
@@ -86,7 +84,7 @@ export function EnchantmentDesigner({
const incompatibleEffects = getIncompatibleEffects(selectedEquipmentType, unlockedEffects);
// Get equipment types that the player actually owns (has instances of)
const ownedEquipmentTypes = getOwnedEquipmentTypes(store);
const ownedEquipmentTypes = getOwnedEquipmentTypes(useGameStore.getState());
// Get the reason why an effect is incompatible
const getIncompatibilityReasonWrapper = (effect: { id: string; name: string; description: string; allowedEquipmentCategories: any[] }) => {
@@ -131,7 +129,7 @@ export function EnchantmentDesigner({
selectedEffects={selectedEffects}
designCapacityCost={designCapacityCost}
selectedEquipmentCapacity={selectedEquipmentCapacity}
isOverCapacity={isOverCapacity}
isOverCapacity={designCapacityCost > selectedEquipmentCapacity}
designTime={designTime}
selectedEquipmentType={selectedEquipmentType}
handleCreateDesign={handleCreateDesign}
@@ -12,28 +12,27 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
import { Trash2, CheckCircle, AlertTriangle } from 'lucide-react';
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress, EquipmentSlot } from '@/lib/game/types';
import { fmt, type GameStore } from '@/lib/game/store';
import { fmt } from '@/lib/game/stores';
import { useGameStore } from '@/lib/game/stores';
import { useGameToast } from '@/components/game/GameToast';
export interface EnchantmentPreparerProps {
store: GameStore;
selectedEquipmentInstance: string | null;
setSelectedEquipmentInstance: (id: string | null) => void;
}
export function EnchantmentPreparer({
store,
selectedEquipmentInstance,
setSelectedEquipmentInstance,
}: EnchantmentPreparerProps) {
const showToast = useGameToast();
const equippedInstances = store.equippedInstances;
const equipmentInstances = store.equipmentInstances;
const preparationProgress = store.preparationProgress;
const rawMana = store.rawMana;
const skills = store.skills;
const startPreparing = store.startPreparing;
const cancelPreparation = store.cancelPreparation;
const equippedInstances = useGameStore((s) => s.equippedInstances);
const equipmentInstances = useGameStore((s) => s.equipmentInstances);
const preparationProgress = useGameStore((s) => s.preparationProgress);
const rawMana = useGameStore((s) => s.rawMana);
const skills = useGameStore((s) => s.skills);
const startPreparing = useGameStore((s) => s.startPreparing);
const cancelPreparation = useGameStore((s) => s.cancelPreparation);
// Get equipped items as array
const equippedItems = Object.entries(equippedInstances)
@@ -10,20 +10,17 @@ import { Package, Sparkles, Trash2, Anvil } from 'lucide-react';
import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes';
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
import { fmt, type GameStore } from '@/lib/game/store';
import { fmt } from '@/lib/game/stores';
import { useGameStore } from '@/lib/game/stores';
export interface EquipmentCrafterProps {
store: GameStore;
}
export function EquipmentCrafter({ store }: EquipmentCrafterProps) {
const lootInventory = store.lootInventory;
const equipmentCraftingProgress = store.equipmentCraftingProgress;
const rawMana = store.rawMana;
const currentAction = store.currentAction;
const startCraftingEquipment = store.startCraftingEquipment;
const cancelEquipmentCrafting = store.cancelEquipmentCrafting;
const deleteMaterial = store.deleteMaterial;
export function EquipmentCrafter() {
const lootInventory = useGameStore((s) => s.lootInventory);
const equipmentCraftingProgress = useGameStore((s) => s.equipmentCraftingProgress);
const rawMana = useGameStore((s) => s.rawMana);
const currentAction = useGameStore((s) => s.currentAction);
const startCraftingEquipment = useGameStore((s) => s.startCraftingEquipment);
const cancelEquipmentCrafting = useGameStore((s) => s.cancelEquipmentCrafting);
const deleteMaterial = useGameStore((s) => s.deleteMaterial);
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+15 -13
View File
@@ -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>
+78 -180
View File
@@ -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>
);
}
+14 -21
View File
@@ -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>
</>
+7 -7
View File
@@ -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)}
+59 -62
View File
@@ -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">
+16 -16
View File
@@ -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>
);
}
+67 -2
View File
@@ -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";
+45 -26
View File
@@ -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";
+110 -88
View File
@@ -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);
}