fix: discipline tab NaN display — correct statBonus.baseValue destructuring, rate-aware /sec labels, NaN guards
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s

- Fix root cause: baseValue was undefined (destructured from definition
  instead of statBonus.baseValue), causing calculateStatBonus to produce NaN
- Remove hardcoded /sec suffix from stat bonus display; now detects rate
  vs flat stats using isRateStat() helper
- Fix computePerkCurrentEffect: perks only show /sec for actual rate stats
- Add NaN guards in DisciplineCard display layer as safety net
- Clean up DisciplinesTab UX (proper summary label, remove unused rawMana)
This commit is contained in:
2026-05-28 18:38:28 +02:00
parent bc184cefb0
commit 8fef73d233
4 changed files with 50 additions and 19 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-28T13:28:24.658Z Generated: 2026-05-28T16:14:24.376Z
No circular dependencies found. ✅ No circular dependencies found. ✅
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-28T13:28:22.544Z", "generated": "2026-05-28T16:14:22.630Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
}, },
@@ -556,6 +556,7 @@
"constants.ts", "constants.ts",
"effects/discipline-effects.ts", "effects/discipline-effects.ts",
"stores/combatStore.ts", "stores/combatStore.ts",
"stores/discipline-slice.ts",
"stores/gameStore.types.ts", "stores/gameStore.types.ts",
"stores/manaStore.ts", "stores/manaStore.ts",
"stores/prestigeStore.ts", "stores/prestigeStore.ts",
+23 -13
View File
@@ -16,7 +16,7 @@ import { useManaStore } from '@/lib/game/stores/manaStore';
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore'; import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
import clsx from 'clsx'; import clsx from 'clsx';
import { DebugName } from '@/components/game/debug/debug-context'; import { DebugName } from '@/components/game/debug/debug-context';
import { TICKS_PER_SECOND, computePerkCurrentEffect, computeTotalPerkBonusForStat } from './disciplines-utils'; import { TICKS_PER_SECOND, computePerkCurrentEffect, computeTotalPerkBonusForStat, isRateStat } from './disciplines-utils';
import type { ComputedPerkEffect } from './disciplines-utils'; import type { ComputedPerkEffect } from './disciplines-utils';
// ─── Attunement Tabs ───────────────────────────────────────────────────────── // ─── Attunement Tabs ─────────────────────────────────────────────────────────
@@ -56,13 +56,14 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({
}) => { }) => {
const { const {
id, name, description, manaType, perks, id, name, description, manaType, perks,
statBonus, baseValue, drainBase, difficultyFactor, scalingFactor, statBonus, drainBase, difficultyFactor, scalingFactor,
conversionRate, sourceManaTypes, conversionRate, sourceManaTypes,
} = definition; } = definition;
const displayXp = xp; const displayXp = xp;
const progressPercent = Math.min(displayXp / Math.max(1, concurrentLimit * 100), 100); const progressPercent = Math.min(displayXp / Math.max(1, concurrentLimit * 100), 100);
const activeStatBonus = calculateStatBonus(baseValue, displayXp, scalingFactor); // statBonus.baseValue is the correct field — not a top-level baseValue
const activeStatBonus = calculateStatBonus(statBonus.baseValue, displayXp, scalingFactor);
const drainPerSecond = calculateManaDrain(drainBase, displayXp, difficultyFactor) * TICKS_PER_SECOND; const drainPerSecond = calculateManaDrain(drainBase, displayXp, difficultyFactor) * TICKS_PER_SECOND;
const elementDef = ELEMENTS[manaType]; const elementDef = ELEMENTS[manaType];
@@ -134,14 +135,23 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({
)} )}
{/* Stat Bonus with Perk Total */} {/* Stat Bonus with Perk Total */}
<div className="mt-2 text-sm"> {(() => {
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)}/sec on {statBonusLabel} // NaN guard — if the calculation produced NaN, show a safe fallback
{perkBonusTotal > 0 && ( const safeActive = Number.isFinite(activeStatBonus) ? activeStatBonus : 0;
<span className="text-green-400 ml-1"> const safePerk = Number.isFinite(perkBonusTotal) ? perkBonusTotal : 0;
({statBonusTotal.toFixed(2)}/sec with perks) const safeTotal = Number.isFinite(statBonusTotal) ? statBonusTotal : 0;
</span> const rateSuffix = isRateStat(statBonus.stat) ? '/sec' : '';
)} return (
</div> <div className="mt-2 text-sm">
<strong>Stat Bonus:</strong> {safeActive.toFixed(2)}{rateSuffix} on {statBonusLabel}
{safePerk > 0 && (
<span className="text-green-400 ml-1">
({safeTotal.toFixed(2)}{rateSuffix} with perks)
</span>
)}
</div>
);
})()}
{/* Perks */} {/* Perks */}
<div className="mt-2"> <div className="mt-2">
@@ -207,7 +217,7 @@ export const DisciplinesTab: React.FC = () => {
const handleToggle = useCallback((id: string, paused: boolean) => { const handleToggle = useCallback((id: string, paused: boolean) => {
if (paused) { if (paused) {
activate(id, { rawMana, elements, signedPacts }); activate(id, { elements, signedPacts });
} else { } else {
deactivate(id); deactivate(id);
} }
@@ -261,7 +271,7 @@ export const DisciplinesTab: React.FC = () => {
{/* Summary */} {/* Summary */}
<div className="mt-4 flex flex-col sm:flex-row gap-2 text-sm text-gray-500"> <div className="mt-4 flex flex-col sm:flex-row gap-2 text-sm text-gray-500">
<div>Active Disciple{activeIds.length}{activeIds.length === 1 ? '' : 's'} / {concurrentLimit}</div> <div>Active Disciplines: {activeIds.length} / {concurrentLimit}</div>
<div>Concurrent Limit: {concurrentLimit}</div> <div>Concurrent Limit: {concurrentLimit}</div>
</div> </div>
</div> </div>
+24 -4
View File
@@ -8,6 +8,23 @@ import { TICK_MS } from '@/lib/game/constants/core';
// TICK_MS = 200, so 5 ticks per second // TICK_MS = 200, so 5 ticks per second
export const TICKS_PER_SECOND = 1000 / TICK_MS; export const TICKS_PER_SECOND = 1000 / TICK_MS;
// Stat keys that represent per-second rates. All other stat keys are
// flat bonuses (capacities, powers, multipliers, percentages, etc.).
const RATE_STAT_KEYS = new Set([
'regenBonus',
'disciplineXpBonus',
]);
// Dynamic stat keys that are rates (checked by prefix)
const RATE_STAT_PREFIXES = [
'conversion_',
];
export function isRateStat(statKey: string): boolean {
if (RATE_STAT_KEYS.has(statKey)) return true;
return RATE_STAT_PREFIXES.some((prefix) => statKey.startsWith(prefix));
}
export interface ComputedPerkEffect { export interface ComputedPerkEffect {
description: string; description: string;
currentEffect: string; currentEffect: string;
@@ -29,15 +46,17 @@ export function computePerkCurrentEffect(
return 'unlocked'; return 'unlocked';
} }
const { amount } = perk.bonus; const { amount, stat } = perk.bonus;
const rateSuffix = isRateStat(stat) ? '/sec' : '';
switch (perk.type) { switch (perk.type) {
case 'once': case 'once':
return `+${amount} (unlocked)`; return `+${amount}${rateSuffix}`;
case 'infinite': { case 'infinite': {
const tier = calculatePerkTier(xp, perk.threshold, perk.value); const tier = calculatePerkTier(xp, perk.threshold, perk.value);
const total = tier * amount; const total = tier * amount;
return `+${total.toFixed(2)}/sec`; if (total === 0) return `+0${rateSuffix}`;
return `+${total.toFixed(2)}${rateSuffix}`;
} }
case 'capped': { case 'capped': {
let tier = calculatePerkTier(xp, perk.threshold, perk.value); let tier = calculatePerkTier(xp, perk.threshold, perk.value);
@@ -45,7 +64,8 @@ export function computePerkCurrentEffect(
tier = Math.min(tier, perk.maxTier); tier = Math.min(tier, perk.maxTier);
} }
const total = tier * amount; const total = tier * amount;
return `+${total.toFixed(2)}/sec`; if (total === 0) return `+0${rateSuffix}`;
return `+${total.toFixed(2)}${rateSuffix}`;
} }
default: default:
return 'active'; return 'active';