From 378e434d443647ff9835185e8bcdba463d0fd36b Mon Sep 17 00:00:00 2001 From: zhipu Date: Thu, 26 Mar 2026 16:02:56 +0000 Subject: [PATCH] Fix build errors and add Equipment tab - Create missing StudyProgress component for SkillsTab - Create missing UpgradeDialog component for SkillsTab - Add EquipmentTab with gear selection interface - Shows all 8 equipment slots with equipped items - Displays inventory of unequipped items - Allows equipping/unequipping gear - Shows equipment stats summary and active effects --- src/app/page.tsx | 9 +- src/components/game/tabs/EquipmentTab.tsx | 393 +++++++++++++++++++++ src/components/game/tabs/StudyProgress.tsx | 73 ++++ src/components/game/tabs/UpgradeDialog.tsx | 116 ++++++ src/components/game/tabs/index.ts | 1 + 5 files changed, 590 insertions(+), 2 deletions(-) create mode 100644 src/components/game/tabs/EquipmentTab.tsx create mode 100644 src/components/game/tabs/StudyProgress.tsx create mode 100644 src/components/game/tabs/UpgradeDialog.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 29230ab..6d85da1 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,7 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { RotateCcw } from 'lucide-react'; -import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab } from '@/components/game/tabs'; +import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab } from '@/components/game/tabs'; import { ComboMeter, ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game'; import { LootInventoryDisplay } from '@/components/game/LootInventory'; import { AchievementsDisplay } from '@/components/game/AchievementsDisplay'; @@ -226,10 +226,11 @@ export default function ManaLoopGame() { {/* Right Panel - Tabs */}
- + โš”๏ธ Spire ๐Ÿ“š Skills โœจ Spells + ๐Ÿ›ก๏ธ Gear ๐Ÿ”ง Craft ๐Ÿ”ฌ Lab ๐Ÿ“Š Stats @@ -256,6 +257,10 @@ export default function ManaLoopGame() { + + + + diff --git a/src/components/game/tabs/EquipmentTab.tsx b/src/components/game/tabs/EquipmentTab.tsx new file mode 100644 index 0000000..d089f5c --- /dev/null +++ b/src/components/game/tabs/EquipmentTab.tsx @@ -0,0 +1,393 @@ +'use client'; + +import { useState } from 'react'; +import { + EQUIPMENT_TYPES, + EQUIPMENT_SLOTS, + getEquipmentBySlot, + type EquipmentSlot, + type EquipmentType, +} from '@/lib/game/data/equipment'; +import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; +import { fmt } from '@/lib/game/store'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@/components/ui/tooltip'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; +import type { GameStore, EquipmentInstance } from '@/lib/game/types'; + +export interface EquipmentTabProps { + store: GameStore; +} + +// Slot display names +const SLOT_NAMES: Record = { + mainHand: 'Main Hand', + offHand: 'Off Hand', + head: 'Head', + body: 'Body', + hands: 'Hands', + feet: 'Feet', + accessory1: 'Accessory 1', + accessory2: 'Accessory 2', +}; + +// Slot icons +const SLOT_ICONS: Record = { + mainHand: 'โš”๏ธ', + offHand: '๐Ÿ›ก๏ธ', + head: '๐ŸŽฉ', + body: '๐Ÿ‘•', + hands: '๐Ÿงค', + feet: '๐Ÿ‘ข', + accessory1: '๐Ÿ’', + accessory2: '๐Ÿ“ฟ', +}; + +// Rarity colors +const RARITY_COLORS: Record = { + common: 'border-gray-500 bg-gray-800/30', + uncommon: 'border-green-500 bg-green-900/20', + rare: 'border-blue-500 bg-blue-900/20', + epic: 'border-purple-500 bg-purple-900/20', + legendary: 'border-amber-500 bg-amber-900/20', + mythic: 'border-red-500 bg-red-900/20', +}; + +const RARITY_TEXT_COLORS: Record = { + common: 'text-gray-300', + uncommon: 'text-green-400', + rare: 'text-blue-400', + epic: 'text-purple-400', + legendary: 'text-amber-400', + mythic: 'text-red-400', +}; + +export function EquipmentTab({ store }: EquipmentTabProps) { + const [selectedSlot, setSelectedSlot] = useState(null); + + // Get unequipped items + const equippedIds = new Set(Object.values(store.equippedInstances).filter(Boolean)); + const unequippedItems = Object.values(store.equipmentInstances).filter( + (inst) => !equippedIds.has(inst.instanceId) + ); + + // Equip an item to a slot + const handleEquip = (instanceId: string, slot: EquipmentSlot) => { + store.equipItem(instanceId, slot); + setSelectedSlot(null); + }; + + // Unequip from a slot + const handleUnequip = (slot: EquipmentSlot) => { + store.unequipItem(slot); + }; + + // Get items that can be equipped in a slot + const getEquippableItems = (slot: EquipmentSlot): EquipmentInstance[] => { + const equipmentTypes = getEquipmentBySlot(slot); + const typeIds = new Set(equipmentTypes.map((t) => t.id)); + + return unequippedItems.filter((inst) => typeIds.has(inst.typeId)); + }; + + // Get all items that can go in a slot (including accessories that can go in either accessory slot) + const getItemsForSlot = (slot: EquipmentSlot): EquipmentInstance[] => { + if (slot === 'accessory1' || slot === 'accessory2') { + // Accessories can go in either slot + const accessoryTypes = EQUIPMENT_TYPES; + const accessoryTypeIds = Object.values(accessoryTypes) + .filter((t) => t.category === 'accessory') + .map((t) => t.id); + + return unequippedItems.filter((inst) => accessoryTypeIds.includes(inst.typeId)); + } + return getEquippableItems(slot); + }; + + return ( +
+ {/* Equipment Slots */} + + + + Equipped Gear + + + +
+ {EQUIPMENT_SLOTS.map((slot) => { + const instanceId = store.equippedInstances[slot]; + const instance = instanceId ? store.equipmentInstances[instanceId] : null; + const equipmentType = instance ? EQUIPMENT_TYPES[instance.typeId] : null; + + return ( +
+
+
+ {SLOT_ICONS[slot]} + + {SLOT_NAMES[slot]} + +
+ {instance && ( + + )} +
+ + {instance ? ( +
+
+ {instance.name} +
+
+ Capacity: {instance.usedCapacity}/{instance.totalCapacity} +
+ {instance.enchantments.length > 0 && ( +
+ {instance.enchantments.map((ench, i) => { + const effect = ENCHANTMENT_EFFECTS[ench.effectId]; + return ( + + + + + {effect?.name || ench.effectId} + {ench.stacks > 1 && ` x${ench.stacks}`} + + + +

{effect?.description || 'Unknown effect'}

+

+ Category: {effect?.category || 'unknown'} +

+
+
+
+ ); + })} +
+ )} +
+ ) : ( +
+ Empty +
+ )} +
+ ); + })} +
+
+
+ + {/* Inventory */} + + + + Equipment Inventory ({unequippedItems.length} items) + + + + {unequippedItems.length === 0 ? ( +
+ No unequipped items. Craft new gear in the Crafting tab. +
+ ) : ( +
+ {unequippedItems.map((instance) => { + const equipmentType = EQUIPMENT_TYPES[instance.typeId]; + const validSlots = equipmentType + ? (equipmentType.category === 'accessory' + ? ['accessory1', 'accessory2'] as EquipmentSlot[] + : [equipmentType.slot]) + : []; + + return ( +
+
+
+
+ {instance.name} +
+
+ {equipmentType?.description} +
+
+ + {equipmentType?.category || 'unknown'} + +
+ +
+
+ Capacity: {instance.usedCapacity}/{instance.totalCapacity} + {instance.quality < 100 && ( + + (Quality: {instance.quality}%) + + )} +
+ {instance.enchantments.length > 0 && ( +
+ {instance.enchantments.map((ench, i) => { + const effect = ENCHANTMENT_EFFECTS[ench.effectId]; + return ( + + {effect?.name || ench.effectId} + + ); + })} +
+ )} +
+ + {validSlots.length > 0 && ( +
+ + + + + + + + +

Delete this item

+
+
+
+
+ )} +
+ ); + })} +
+ )} +
+
+ + {/* Equipment Stats Summary */} + + + + Equipment Stats Summary + + + +
+
+
+ {Object.values(store.equipmentInstances).length} +
+
Total Items
+
+
+
+ {equippedIds.size} +
+
Equipped
+
+
+
+ {unequippedItems.length} +
+
In Inventory
+
+
+
+ {Object.values(store.equipmentInstances).reduce( + (sum, inst) => sum + inst.enchantments.length, + 0 + )} +
+
Total Enchantments
+
+
+ + {/* Active Effects from Equipment */} +
+
Active Effects from Equipment:
+
+ {(() => { + const effects = store.getEquipmentEffects(); + const effectEntries = Object.entries(effects).filter(([, v]) => v > 0); + + if (effectEntries.length === 0) { + return No active effects; + } + + return effectEntries.map(([stat, value]) => ( + + {stat}: +{fmt(value)} + + )); + })()} +
+
+
+
+
+ ); +} diff --git a/src/components/game/tabs/StudyProgress.tsx b/src/components/game/tabs/StudyProgress.tsx new file mode 100644 index 0000000..77d5a0b --- /dev/null +++ b/src/components/game/tabs/StudyProgress.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { SKILLS_DEF } from '@/lib/game/constants'; +import { formatStudyTime } from '@/lib/game/formatting'; +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import type { StudyTarget } from '@/lib/game/types'; + +export interface StudyProgressProps { + currentStudyTarget: StudyTarget; + skills: Record; + studySpeedMult: number; + cancelStudy: () => void; +} + +export function StudyProgress({ + currentStudyTarget, + skills, + studySpeedMult, + cancelStudy +}: StudyProgressProps) { + const { id, progress, required } = currentStudyTarget; + + // Get skill name + const baseId = id.includes('_t') ? id.split('_t')[0] : id; + const skillDef = SKILLS_DEF[baseId]; + const skillName = skillDef?.name || id; + + // Get current level + const currentLevel = skills[id] || skills[baseId] || 0; + + // Calculate progress percentage + const progressPercent = Math.min((progress / required) * 100, 100); + + // Estimated completion + const remainingHours = required - progress; + const effectiveSpeed = studySpeedMult; + const realTimeRemaining = remainingHours / effectiveSpeed; + + return ( +
+
+
+ {skillName} + + Level {currentLevel} โ†’ {currentLevel + 1} + +
+ +
+ +
+
+ {formatStudyTime(progress)} / {formatStudyTime(required)} + {progressPercent.toFixed(1)}% +
+ + {studySpeedMult > 1 && ( +
+ Speed: {(studySpeedMult * 100).toFixed(0)}% โ€ข ETA: {formatStudyTime(realTimeRemaining)} +
+ )} +
+
+ ); +} diff --git a/src/components/game/tabs/UpgradeDialog.tsx b/src/components/game/tabs/UpgradeDialog.tsx new file mode 100644 index 0000000..77e58cf --- /dev/null +++ b/src/components/game/tabs/UpgradeDialog.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { SKILLS_DEF } from '@/lib/game/constants'; +import { getUpgradesForSkillAtMilestone, getTierMultiplier } from '@/lib/game/skill-evolution'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import type { SkillUpgradeChoice } from '@/lib/game/types'; + +export interface UpgradeDialogProps { + open: boolean; + skillId: string | null; + milestone: 5 | 10; + pendingSelections: string[]; + available: SkillUpgradeChoice[]; + alreadySelected: string[]; + onToggle: (upgradeId: string) => void; + onConfirm: () => void; + onCancel: () => void; + onOpenChange: (open: boolean) => void; +} + +export function UpgradeDialog({ + open, + skillId, + milestone, + pendingSelections, + available, + alreadySelected, + onToggle, + onConfirm, + onCancel, + onOpenChange, +}: UpgradeDialogProps) { + if (!skillId) return null; + + // Get skill name + const baseId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId; + const skillDef = SKILLS_DEF[baseId]; + const skillName = skillDef?.name || skillId; + + const currentSelections = pendingSelections.length > 0 ? pendingSelections : alreadySelected; + const canConfirm = currentSelections.length === 2; + + return ( + + + + + Level {milestone} Milestone: {skillName} + + + Choose 2 upgrades for this skill. These choices are permanent. + + + +
+ {available.map((upgrade) => { + const isSelected = currentSelections.includes(upgrade.id); + const canSelect = isSelected || currentSelections.length < 2; + + return ( +
canSelect && onToggle(upgrade.id)} + className={`p-3 rounded border cursor-pointer transition-all ${ + isSelected + ? 'border-amber-500 bg-amber-900/30' + : canSelect + ? 'border-gray-700 hover:border-gray-500 bg-gray-800/30' + : 'border-gray-800 bg-gray-900/30 opacity-50 cursor-not-allowed' + }`} + > +
+ + {upgrade.name} + + {isSelected && ( + Selected + )} +
+

{upgrade.desc}

+
+ ); + })} + + {available.length === 0 && ( +
+ No upgrades available at this milestone. +
+ )} +
+ + + + + +
+
+ ); +} diff --git a/src/components/game/tabs/index.ts b/src/components/game/tabs/index.ts index ae3900a..bca3380 100644 --- a/src/components/game/tabs/index.ts +++ b/src/components/game/tabs/index.ts @@ -7,3 +7,4 @@ export { SpellsTab } from './SpellsTab'; export { LabTab } from './LabTab'; export { SkillsTab } from './SkillsTab'; export { StatsTab } from './StatsTab'; +export { EquipmentTab } from './EquipmentTab';