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 { Badge } from '@/components/ui/badge';
|
||||
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 { LootInventoryDisplay } from '@/components/game/LootInventory';
|
||||
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
|
||||
@@ -230,8 +230,9 @@ export default function ManaLoopGame() {
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<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="attunements" className="text-xs px-2 py-1">✨ Attune</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="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
||||
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
||||
@@ -243,6 +244,10 @@ export default function ManaLoopGame() {
|
||||
<SpireTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="attunements">
|
||||
<AttunementsTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="skills">
|
||||
<SkillsTab store={store} />
|
||||
</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 { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import { getAvailableSkillCategories } from '@/lib/game/data/attunements';
|
||||
import { fmt, fmtDec } from '@/lib/game/store';
|
||||
import { formatStudyTime } from '@/lib/game/formatting';
|
||||
import type { SkillUpgradeChoice, GameStore } from '@/lib/game/types';
|
||||
@@ -127,7 +128,13 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
</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);
|
||||
if (skillsInCat.length === 0) return null;
|
||||
|
||||
@@ -332,7 +339,8 @@ export function SkillsTab({ store }: SkillsTabProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,3 +8,4 @@ export { LabTab } from './LabTab';
|
||||
export { SkillsTab } from './SkillsTab';
|
||||
export { StatsTab } from './StatsTab';
|
||||
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
|
||||
- Skills are now organized by attunement (foundation for skill tab overhaul)
|
||||
- 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