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
@@ -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";