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 (
+
+ );
+}
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';