feat: Add Attunements tab UI and filter skills by attunement access
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m14s
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m14s
- Create AttunementsTab component with visual display of all 3 attunements - Show primary mana type, regen stats, and capabilities for each attunement - Add visual effects for active attunements (color, glow) - Filter skill categories based on active attunements in SkillsTab - Add attunement tab navigation in main page - Display available skill categories summary in AttunementsTab
This commit is contained in:
@@ -12,7 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { RotateCcw } from 'lucide-react';
|
import { RotateCcw } from 'lucide-react';
|
||||||
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab } from '@/components/game/tabs';
|
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab, AttunementsTab } from '@/components/game/tabs';
|
||||||
import { ComboMeter, ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
|
import { ComboMeter, ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
|
||||||
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
||||||
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
|
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
|
||||||
@@ -230,8 +230,9 @@ export default function ManaLoopGame() {
|
|||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
||||||
<TabsTrigger value="spire" className="text-xs px-2 py-1">⚔️ Spire</TabsTrigger>
|
<TabsTrigger value="spire" className="text-xs px-2 py-1">⚔️ Spire</TabsTrigger>
|
||||||
|
<TabsTrigger value="attunements" className="text-xs px-2 py-1">✨ Attune</TabsTrigger>
|
||||||
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
|
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
|
||||||
<TabsTrigger value="spells" className="text-xs px-2 py-1">✨ Spells</TabsTrigger>
|
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
|
||||||
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡️ Gear</TabsTrigger>
|
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡️ Gear</TabsTrigger>
|
||||||
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
||||||
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
||||||
@@ -243,6 +244,10 @@ export default function ManaLoopGame() {
|
|||||||
<SpireTab store={store} />
|
<SpireTab store={store} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="attunements">
|
||||||
|
<AttunementsTab store={store} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="skills">
|
<TabsContent value="skills">
|
||||||
<SkillsTab store={store} />
|
<SkillsTab store={store} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
235
src/components/game/tabs/AttunementsTab.tsx
Normal file
235
src/components/game/tabs/AttunementsTab.tsx
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getTotalAttunementRegen, getAvailableSkillCategories } from '@/lib/game/data/attunements';
|
||||||
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
|
import type { GameStore, AttunementState } from '@/lib/game/types';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Lock, Sparkles } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface AttunementsTabProps {
|
||||||
|
store: GameStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||||
|
const attunements = store.attunements || {};
|
||||||
|
|
||||||
|
// Get active attunements
|
||||||
|
const activeAttunements = Object.entries(attunements)
|
||||||
|
.filter(([, state]) => state.active)
|
||||||
|
.map(([id]) => ATTUNEMENTS_DEF[id])
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Calculate total regen from attunements
|
||||||
|
const totalAttunementRegen = getTotalAttunementRegen(attunements);
|
||||||
|
|
||||||
|
// Get available skill categories
|
||||||
|
const availableCategories = getAvailableSkillCategories(attunements);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Overview Card */}
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 text-sm">Your Attunements</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-gray-400 mb-3">
|
||||||
|
Attunements are magical bonds tied to specific body locations. Each attunement grants unique capabilities,
|
||||||
|
mana regeneration, and access to specialized skills.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge className="bg-teal-900/50 text-teal-300">
|
||||||
|
+{totalAttunementRegen.toFixed(1)} raw mana/hr
|
||||||
|
</Badge>
|
||||||
|
<Badge className="bg-purple-900/50 text-purple-300">
|
||||||
|
{activeAttunements.length} active attunement{activeAttunements.length !== 1 ? 's' : ''}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Attunement Slots */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{Object.entries(ATTUNEMENTS_DEF).map(([id, def]) => {
|
||||||
|
const state = attunements[id];
|
||||||
|
const isActive = state?.active;
|
||||||
|
const isUnlocked = state?.active || def.unlocked;
|
||||||
|
|
||||||
|
// Get primary mana element info
|
||||||
|
const primaryElem = def.primaryManaType ? ELEMENTS[def.primaryManaType] : null;
|
||||||
|
|
||||||
|
// Get current mana for this attunement's type
|
||||||
|
const currentMana = def.primaryManaType ? store.elements[def.primaryManaType]?.current || 0 : 0;
|
||||||
|
const maxMana = def.primaryManaType ? store.elements[def.primaryManaType]?.max || 50 : 50;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={id}
|
||||||
|
className={`bg-gray-900/80 transition-all ${
|
||||||
|
isActive
|
||||||
|
? 'border-2 shadow-lg'
|
||||||
|
: isUnlocked
|
||||||
|
? 'border-gray-600'
|
||||||
|
: 'border-gray-800 opacity-70'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
borderColor: isActive ? def.color : undefined,
|
||||||
|
boxShadow: isActive ? `0 0 20px ${def.color}30` : undefined
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl">{def.icon}</span>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-sm" style={{ color: isActive ? def.color : '#9CA3AF' }}>
|
||||||
|
{def.name}
|
||||||
|
</CardTitle>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{ATTUNEMENT_SLOT_NAMES[def.slot]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isUnlocked && (
|
||||||
|
<Lock className="w-4 h-4 text-gray-600" />
|
||||||
|
)}
|
||||||
|
{isActive && (
|
||||||
|
<Badge className="text-xs" style={{ backgroundColor: `${def.color}30`, color: def.color }}>
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<p className="text-xs text-gray-400">{def.desc}</p>
|
||||||
|
|
||||||
|
{/* Mana Type */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-gray-500">Primary Mana</span>
|
||||||
|
{primaryElem ? (
|
||||||
|
<span style={{ color: primaryElem.color }}>
|
||||||
|
{primaryElem.sym} {primaryElem.name}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-purple-400">From Pacts</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mana bar (only for attunements with primary type) */}
|
||||||
|
{primaryElem && isActive && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Progress
|
||||||
|
value={(currentMana / maxMana) * 100}
|
||||||
|
className="h-2 bg-gray-800"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-gray-500">
|
||||||
|
<span>{currentMana.toFixed(1)}</span>
|
||||||
|
<span>/{maxMana}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div className="p-2 bg-gray-800/50 rounded">
|
||||||
|
<div className="text-gray-500">Raw Regen</div>
|
||||||
|
<div className="text-green-400 font-semibold">+{def.rawManaRegen}/hr</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-gray-800/50 rounded">
|
||||||
|
<div className="text-gray-500">Conversion</div>
|
||||||
|
<div className="text-cyan-400 font-semibold">
|
||||||
|
{def.conversionRate > 0 ? `${def.conversionRate}/hr` : '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Capabilities */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-xs text-gray-500">Capabilities</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{def.capabilities.map(cap => (
|
||||||
|
<Badge key={cap} variant="outline" className="text-xs">
|
||||||
|
{cap === 'enchanting' && '✨ Enchanting'}
|
||||||
|
{cap === 'disenchanting' && '🔄 Disenchant'}
|
||||||
|
{cap === 'scrollCrafting' && '📜 Scrolls'}
|
||||||
|
{cap === 'pacts' && '🤝 Pacts'}
|
||||||
|
{cap === 'guardianPowers' && '💜 Guardian Powers'}
|
||||||
|
{cap === 'elementalMastery' && '🌟 Elem. Mastery'}
|
||||||
|
{cap === 'golemCrafting' && '🗿 Golems'}
|
||||||
|
{cap === 'gearCrafting' && '⚒️ Gear'}
|
||||||
|
{cap === 'earthShaping' && '⛰️ Earth Shaping'}
|
||||||
|
{!['enchanting', 'disenchanting', 'scrollCrafting', 'pacts', 'guardianPowers',
|
||||||
|
'elementalMastery', 'golemCrafting', 'gearCrafting', 'earthShaping'].includes(cap) && cap}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Unlock condition for locked attunements */}
|
||||||
|
{!isUnlocked && def.unlockCondition && (
|
||||||
|
<div className="text-xs text-amber-400 italic">
|
||||||
|
🔒 {def.unlockCondition}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Level for unlocked attunements */}
|
||||||
|
{isUnlocked && state && (
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
Level {state.level} • {state.experience} XP
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Available Skills Summary */}
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 text-sm">Available Skill Categories</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-xs text-gray-400 mb-2">
|
||||||
|
Your attunements grant access to specialized skill categories:
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{availableCategories.map(cat => {
|
||||||
|
const attunement = Object.values(ATTUNEMENTS_DEF).find(a =>
|
||||||
|
a.skillCategories.includes(cat) && attunements[a.id]?.active
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={cat}
|
||||||
|
className={attunement ? '' : 'bg-gray-700/50 text-gray-400'}
|
||||||
|
style={attunement ? {
|
||||||
|
backgroundColor: `${attunement.color}30`,
|
||||||
|
color: attunement.color
|
||||||
|
} : undefined}
|
||||||
|
>
|
||||||
|
{cat === 'mana' && '💧 Mana'}
|
||||||
|
{cat === 'study' && '📚 Study'}
|
||||||
|
{cat === 'research' && '🔮 Research'}
|
||||||
|
{cat === 'ascension' && '⭐ Ascension'}
|
||||||
|
{cat === 'enchant' && '✨ Enchanting'}
|
||||||
|
{cat === 'effectResearch' && '🔬 Effect Research'}
|
||||||
|
{cat === 'invocation' && '💜 Invocation'}
|
||||||
|
{cat === 'pact' && '🤝 Pact Mastery'}
|
||||||
|
{cat === 'fabrication' && '⚒️ Fabrication'}
|
||||||
|
{cat === 'golemancy' && '🗿 Golemancy'}
|
||||||
|
{!['mana', 'study', 'research', 'ascension', 'enchant', 'effectResearch',
|
||||||
|
'invocation', 'pact', 'fabrication', 'golemancy'].includes(cat) && cat}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
|||||||
import { SKILLS_DEF, SKILL_CATEGORIES, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
import { SKILLS_DEF, SKILL_CATEGORIES, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||||
import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||||
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||||
|
import { getAvailableSkillCategories } from '@/lib/game/data/attunements';
|
||||||
import { fmt, fmtDec } from '@/lib/game/store';
|
import { fmt, fmtDec } from '@/lib/game/store';
|
||||||
import { formatStudyTime } from '@/lib/game/formatting';
|
import { formatStudyTime } from '@/lib/game/formatting';
|
||||||
import type { SkillUpgradeChoice, GameStore } from '@/lib/game/types';
|
import type { SkillUpgradeChoice, GameStore } from '@/lib/game/types';
|
||||||
@@ -127,7 +128,13 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{SKILL_CATEGORIES.map((cat) => {
|
{/* Get available skill categories based on attunements */}
|
||||||
|
{(() => {
|
||||||
|
const availableCategories = getAvailableSkillCategories(store.attunements || {});
|
||||||
|
|
||||||
|
return SKILL_CATEGORIES
|
||||||
|
.filter(cat => availableCategories.includes(cat.id))
|
||||||
|
.map((cat) => {
|
||||||
const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === cat.id);
|
const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === cat.id);
|
||||||
if (skillsInCat.length === 0) return null;
|
if (skillsInCat.length === 0) return null;
|
||||||
|
|
||||||
@@ -332,7 +339,8 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
});
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export { LabTab } from './LabTab';
|
|||||||
export { SkillsTab } from './SkillsTab';
|
export { SkillsTab } from './SkillsTab';
|
||||||
export { StatsTab } from './StatsTab';
|
export { StatsTab } from './StatsTab';
|
||||||
export { EquipmentTab } from './EquipmentTab';
|
export { EquipmentTab } from './EquipmentTab';
|
||||||
|
export { AttunementsTab } from './AttunementsTab';
|
||||||
|
|||||||
28
worklog.md
28
worklog.md
@@ -233,3 +233,31 @@ Stage Summary:
|
|||||||
- Attunements provide raw mana regen and convert to primary mana types
|
- Attunements provide raw mana regen and convert to primary mana types
|
||||||
- Skills are now organized by attunement (foundation for skill tab overhaul)
|
- Skills are now organized by attunement (foundation for skill tab overhaul)
|
||||||
- Lint checks pass, ready for UI implementation
|
- Lint checks pass, ready for UI implementation
|
||||||
|
|
||||||
|
---
|
||||||
|
Task ID: 10
|
||||||
|
Agent: Main
|
||||||
|
Task: Implement Attunement System - UI Overhaul
|
||||||
|
|
||||||
|
Work Log:
|
||||||
|
- **Created AttunementsTab component** (src/components/game/tabs/AttunementsTab.tsx):
|
||||||
|
- Displays all 3 attunements with their status (active/locked)
|
||||||
|
- Shows primary mana type and current mana for active attunements
|
||||||
|
- Displays raw mana regen and conversion rate stats
|
||||||
|
- Shows capabilities unlocked by each attunement
|
||||||
|
- Displays available skill categories based on active attunements
|
||||||
|
- Uses color coding and visual effects for active attunements
|
||||||
|
- **Updated page.tsx**:
|
||||||
|
- Added AttunementsTab import
|
||||||
|
- Added "✨ Attune" tab between Spire and Skills
|
||||||
|
- Added TabsContent for attunements
|
||||||
|
- **Updated SkillsTab.tsx**:
|
||||||
|
- Added import for getAvailableSkillCategories
|
||||||
|
- Modified skill rendering to filter categories by attunement access
|
||||||
|
- Skills now only show if the player has the appropriate attunement
|
||||||
|
|
||||||
|
Stage Summary:
|
||||||
|
- New Attunements tab shows all attunement details
|
||||||
|
- Skills are filtered based on active attunements
|
||||||
|
- Player can see exactly which skill categories they have access to
|
||||||
|
- Visual feedback shows active vs locked attunements
|
||||||
|
|||||||
Reference in New Issue
Block a user