Add StatsTab mana breakdown per type (Bug14)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 3m34s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 3m34s
This commit is contained in:
@@ -1,14 +1,51 @@
|
|||||||
# Sub-Task 9 Progress: StatsTab Mana Breakdown
|
# Sub-Task 9 Progress: StatsTab Mana Breakdown
|
||||||
|
|
||||||
## Status: Pending
|
## Status: Completed
|
||||||
|
|
||||||
## Completed Steps
|
## Completed Steps
|
||||||
- [ ] Review Sub-Task 8 completion (get conversion drain data)
|
- [x] Review Sub-Task 8 completion (get conversion drain data)
|
||||||
- [ ] Design mana breakdown UI for StatsTab
|
- [x] Design mana breakdown UI for StatsTab
|
||||||
- [ ] Implement data fetching for each mana type's stats
|
- [x] Implement data fetching for each mana type's stats
|
||||||
- [ ] Add modifiers display (attunements, conversion drains)
|
- [x] Add modifiers display (attunements, conversion drains)
|
||||||
- [ ] Test reactive updates
|
- [x] Test reactive updates
|
||||||
- [ ] Commit and push changes
|
- [x] Commit and push changes
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
1. **src/components/game/stats/ManaTypeBreakdown.tsx** (NEW)
|
||||||
|
- Created new component to display detailed breakdown for each mana type
|
||||||
|
- Shows current value, capacity, and effective regen rate
|
||||||
|
- Lists modifiers: attunement bonuses, conversion drains from Sub-Task 8
|
||||||
|
- Groups elements by category (base, composite, exotic, utility)
|
||||||
|
- Shows recipe for composite/exotic elements
|
||||||
|
- Includes progress bars for visual clarity
|
||||||
|
|
||||||
|
2. **src/components/game/tabs/StatsTab.tsx** (MODIFIED)
|
||||||
|
- Added import for ManaTypeBreakdown component
|
||||||
|
- Added ManaTypeBreakdown section after ManaStatsSection
|
||||||
|
|
||||||
|
### Features Implemented
|
||||||
|
- Raw Mana section with:
|
||||||
|
- Current/max display
|
||||||
|
- Progress bar with color coding
|
||||||
|
- Base regen, conversion drain, effective regen
|
||||||
|
- Per-attunement conversion drain breakdown
|
||||||
|
|
||||||
|
- Elemental Mana sections (for each unlocked element):
|
||||||
|
- Current/max display with element-specific colors
|
||||||
|
- Progress bar with element color
|
||||||
|
- Conversion rate from attunements
|
||||||
|
- Source attunement names and levels
|
||||||
|
- Recipe display for composite/exotic elements
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- `npm run build` completed successfully with no errors
|
||||||
|
- Component uses reactive data from store
|
||||||
|
- Properly handles conversion drains from Sub-Task 8
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
(Add UI design notes here)
|
- Uses `computeEffectiveRegenForDisplay` from mana-utils.ts for regen calculations
|
||||||
|
- Uses `getAttunementConversionRate` from attunements.ts for per-attunement drain
|
||||||
|
- Conversion drains are read from `store.conversionDrains` (populated by Sub-Task 8)
|
||||||
|
- Element categories (base, composite, exotic, utility) are preserved from ELEMENTS constant
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
| 6 | CraftingTab Prepare/Apply Disenchant Consolidation (Bug8) | Completed | Sub-Task 5 | |
|
| 6 | CraftingTab Prepare/Apply Disenchant Consolidation (Bug8) | Completed | Sub-Task 5 | |
|
||||||
| 7 | SkillsTab Modifications (Bugs 9,11,12,13) | Completed | None | |
|
| 7 | SkillsTab Modifications (Bugs 9,11,12,13) | Completed | None | |
|
||||||
| 8 | Mana System Conversion Regen Deduction (Bug10) | Completed | None | |
|
| 8 | Mana System Conversion Regen Deduction (Bug10) | Completed | None | |
|
||||||
| 9 | StatsTab Mana Breakdown (Bug14) | Pending | Sub-Task 8 | |
|
| 9 | StatsTab Mana Breakdown (Bug14) | Completed | Sub-Task 8 | |
|
||||||
| 10 | Essence Refining Investigation (Bug15) | Completed | None | |
|
| 10 | Essence Refining Investigation (Bug15) | Completed | None | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
|
import { fmt, fmtDec } from '@/lib/game/store';
|
||||||
|
import type { GameStore } from '@/lib/game/types';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { ATTUNEMENTS_DEF, getAttunementConversionRate } from '@/lib/game/data/attunements';
|
||||||
|
import { computeEffectiveRegenForDisplay, computeMaxMana, computeElementMax } from '@/lib/game/store';
|
||||||
|
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||||
|
import { Droplet } from 'lucide-react';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
|
||||||
|
export interface ManaTypeBreakdownProps {
|
||||||
|
store: GameStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ManaTypeBreakdown({ store }: ManaTypeBreakdownProps) {
|
||||||
|
// Compute unified effects for regen calculations
|
||||||
|
const effects = getUnifiedEffects(store);
|
||||||
|
|
||||||
|
// Get effective regen info for raw mana
|
||||||
|
const regenInfo = computeEffectiveRegenForDisplay(store, effects);
|
||||||
|
|
||||||
|
// Compute max mana
|
||||||
|
const maxMana = computeMaxMana(store, effects);
|
||||||
|
|
||||||
|
// Get unlocked elements sorted by category then name
|
||||||
|
const unlockedElements = Object.entries(store.elements)
|
||||||
|
.filter(([, state]) => state.unlocked)
|
||||||
|
.map(([id, state]) => {
|
||||||
|
const def = ELEMENTS[id];
|
||||||
|
if (!def) return null;
|
||||||
|
const elemMax = computeElementMax(store, effects);
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: def.name,
|
||||||
|
sym: def.sym,
|
||||||
|
color: def.color,
|
||||||
|
current: state.current,
|
||||||
|
max: elemMax,
|
||||||
|
cat: def.cat,
|
||||||
|
recipe: def.recipe,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (!a || !b) return 0;
|
||||||
|
if (a.cat !== b.cat) return a.cat.localeCompare(b.cat);
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||||||
|
<Droplet className="w-4 h-4" />
|
||||||
|
Mana Type Breakdown
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Raw Mana Section */}
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded border border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">🌀</span>
|
||||||
|
<span className="font-semibold text-purple-300">Raw Mana</span>
|
||||||
|
<span className="text-xs text-gray-500">(base)</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-gray-300">{fmt(store.rawMana)}</span>
|
||||||
|
<span className="text-gray-500"> / </span>
|
||||||
|
<span className="text-gray-400">{fmt(maxMana)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="w-full bg-gray-700 rounded-full h-2 mb-3">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded-full transition-all duration-300 bg-purple-500"
|
||||||
|
style={{ width: `${Math.min(100, (store.rawMana / maxMana) * 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Regen info */}
|
||||||
|
<div className="text-xs space-y-1">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Base Regen:</span>
|
||||||
|
<span className="text-green-400">{fmtDec(regenInfo.rawRegen, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
{regenInfo.conversionDrain > 0 && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Conversion Drain:</span>
|
||||||
|
<span className="text-red-400">-{fmtDec(regenInfo.conversionDrain, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Separator className="bg-gray-700 my-1" />
|
||||||
|
<div className="flex justify-between font-semibold">
|
||||||
|
<span className="text-gray-300">Effective Regen:</span>
|
||||||
|
<span className="text-green-400">{fmtDec(regenInfo.effectiveRegen, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show conversion drains by attunement */}
|
||||||
|
{store.conversionDrains && Object.keys(store.conversionDrains).length > 0 && (
|
||||||
|
<>
|
||||||
|
<Separator className="bg-gray-700 my-1" />
|
||||||
|
<div className="text-gray-400 mb-1">Conversion Drains:</div>
|
||||||
|
{Object.entries(store.conversionDrains).map(([attId, rate]) => {
|
||||||
|
const attDef = ATTUNEMENTS_DEF[attId];
|
||||||
|
if (!attDef || rate <= 0) return null;
|
||||||
|
return (
|
||||||
|
<div key={attId} className="flex justify-between pl-2">
|
||||||
|
<span className="text-gray-500">{attDef.name}:</span>
|
||||||
|
<span className="text-red-400">-{fmtDec(rate, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="bg-gray-700" />
|
||||||
|
|
||||||
|
{/* Elemental Mana Sections */}
|
||||||
|
{unlockedElements.map((elem) => {
|
||||||
|
if (!elem) return null;
|
||||||
|
|
||||||
|
// Find attunements that convert TO this element
|
||||||
|
const convertingAttunements = Object.entries(store.attunements || {})
|
||||||
|
.filter(([attId, attState]) => {
|
||||||
|
if (!attState.active) return false;
|
||||||
|
const attDef = ATTUNEMENTS_DEF[attId];
|
||||||
|
return attDef?.primaryManaType === elem.id && attDef.conversionRate > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total conversion rate TO this element
|
||||||
|
const totalConversionRate = convertingAttunements.reduce((total, [attId, attState]) => {
|
||||||
|
return total + getAttunementConversionRate(attId, attState.level || 1);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={elem.id} className="p-3 bg-gray-800/50 rounded border border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">{elem.sym}</span>
|
||||||
|
<span className="font-semibold" style={{ color: elem.color }}>{elem.name}</span>
|
||||||
|
<span className="text-xs text-gray-500">({elem.cat})</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-gray-300">{fmt(elem.current)}</span>
|
||||||
|
<span className="text-gray-500"> / </span>
|
||||||
|
<span className="text-gray-400">{fmt(elem.max)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="w-full bg-gray-700 rounded-full h-2 mb-3">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, (elem.current / elem.max) * 100)}%`,
|
||||||
|
backgroundColor: elem.color
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Conversion info */}
|
||||||
|
{totalConversionRate > 0 ? (
|
||||||
|
<div className="text-xs space-y-1">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400">Conversion Rate:</span>
|
||||||
|
<span className="text-green-400">+{fmtDec(totalConversionRate, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
{convertingAttunements.length > 0 && (
|
||||||
|
<div className="text-gray-500 pl-2">
|
||||||
|
Source: {convertingAttunements.map(([attId]) => {
|
||||||
|
const attDef = ATTUNEMENTS_DEF[attId];
|
||||||
|
const level = store.attunements[attId]?.level || 1;
|
||||||
|
return `${attDef?.name} (Lv.${level})`;
|
||||||
|
}).join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-gray-500">No active conversion to this element</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show recipe for composite/exotic elements */}
|
||||||
|
{elem.recipe && (
|
||||||
|
<div className="text-xs text-gray-500 mt-2 pt-2 border-t border-gray-700">
|
||||||
|
Recipe: {elem.recipe.map(r => `${ELEMENTS[r]?.sym} ${ELEMENTS[r]?.name || r}`).join(' + ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ManaTypeBreakdown.displayName = "ManaTypeBreakdown";
|
||||||
@@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { FlaskConical, Trophy, RotateCcw } from 'lucide-react';
|
import { FlaskConical, Trophy, RotateCcw } from 'lucide-react';
|
||||||
import { ManaStatsSection } from '../stats/ManaStatsSection';
|
import { ManaStatsSection } from '../stats/ManaStatsSection';
|
||||||
|
import { ManaTypeBreakdown } from '../stats/ManaTypeBreakdown';
|
||||||
import { CombatStatsSection } from '../stats/CombatStatsSection';
|
import { CombatStatsSection } from '../stats/CombatStatsSection';
|
||||||
import { StudyStatsSection } from '../stats/StudyStatsSection';
|
import { StudyStatsSection } from '../stats/StudyStatsSection';
|
||||||
import { UpgradeEffectsSection } from '../stats/UpgradeEffectsSection';
|
import { UpgradeEffectsSection } from '../stats/UpgradeEffectsSection';
|
||||||
@@ -79,6 +80,9 @@ export function StatsTab({
|
|||||||
hasEternalFlow={hasEternalFlow}
|
hasEternalFlow={hasEternalFlow}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Mana Type Breakdown */}
|
||||||
|
<ManaTypeBreakdown store={store} />
|
||||||
|
|
||||||
{/* Combat Stats */}
|
{/* Combat Stats */}
|
||||||
<CombatStatsSection store={store} />
|
<CombatStatsSection store={store} />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user