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
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:
@@ -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. ✅
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
{(() => {
|
||||||
|
// NaN guard — if the calculation produced NaN, show a safe fallback
|
||||||
|
const safeActive = Number.isFinite(activeStatBonus) ? activeStatBonus : 0;
|
||||||
|
const safePerk = Number.isFinite(perkBonusTotal) ? perkBonusTotal : 0;
|
||||||
|
const safeTotal = Number.isFinite(statBonusTotal) ? statBonusTotal : 0;
|
||||||
|
const rateSuffix = isRateStat(statBonus.stat) ? '/sec' : '';
|
||||||
|
return (
|
||||||
<div className="mt-2 text-sm">
|
<div className="mt-2 text-sm">
|
||||||
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)}/sec on {statBonusLabel}
|
<strong>Stat Bonus:</strong> {safeActive.toFixed(2)}{rateSuffix} on {statBonusLabel}
|
||||||
{perkBonusTotal > 0 && (
|
{safePerk > 0 && (
|
||||||
<span className="text-green-400 ml-1">
|
<span className="text-green-400 ml-1">
|
||||||
({statBonusTotal.toFixed(2)}/sec with perks)
|
({safeTotal.toFixed(2)}{rateSuffix} with perks)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user