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:
@@ -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";
|
||||
Reference in New Issue
Block a user