Add StatsTab mana breakdown per type (Bug14)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 3m34s

This commit is contained in:
Refactoring Agent
2026-04-27 13:46:18 +02:00
parent 33c5a49577
commit a69ea7575e
4 changed files with 254 additions and 9 deletions
+45 -8
View File
@@ -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
View File
@@ -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";
+4
View File
@@ -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} />