fix: resolve critical bugs - disciplines, debug reset, floating point, spire loop
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

Fixes:
- Issue 193: Remove unnecessary useEffect that set activeTab when spireMode is true, and redundant setAction('climb') in SpireCombatPage
- Issue 194: Fix signed_pact prerequisite check in checkDisciplinePrerequisites by accepting signedPacts param; add 'At Limit' feedback on discipline button when concurrent limit reached
- Issue 195: Add resetDisciplines(), resetAttunements(), resetCrafting() calls to createResetGame; add resetCrafting action to crafting store
- Issue 196: Fix floating point display in ElementStatsSection (mana pools) and GameStateDebug (time); fix duplicate 'Base Regen' label in ManaStatsSection

All 917 tests pass. Files stay under 400-line limit.
This commit is contained in:
2026-05-29 14:10:04 +02:00
parent e20216bda5
commit a33e9429fe
15 changed files with 89 additions and 54 deletions
+26 -6
View File
@@ -13,6 +13,7 @@ export interface DisciplineCardProps {
definition: DisciplineDefinition;
xp: number;
paused: boolean;
activeIds: string[];
concurrentLimit: number;
isLocked: boolean;
missingPrereqs: string[];
@@ -23,7 +24,7 @@ export interface DisciplineCardProps {
// ─── Component ────────────────────────────────────────────────────────────────
export const DisciplineCard: React.FC<DisciplineCardProps> = ({
definition, xp, paused: isPaused, concurrentLimit,
definition, xp, paused: isPaused, activeIds, concurrentLimit,
isLocked, missingPrereqs, missingSourceMana, onToggle,
}) => {
const {
@@ -41,6 +42,12 @@ export const DisciplineCard: React.FC<DisciplineCardProps> = ({
const manaColor = elementDef?.color ?? '#888888';
const manaIcon = elementDef?.sym ?? '✦';
const manaName = elementDef?.name ?? manaType;
const isActive = activeIds.includes(id);
const activeNotPaused = activeIds.filter((aid) => {
// Count how many active disciplines are not paused
return aid === id ? !isPaused : true;
}).length;
const atConcurrentLimit = !isActive && activeIds.length >= concurrentLimit;
const effectiveIsLocked = isLocked || missingSourceMana.length > 0;
const statBonusLabel = statBonus.label;
@@ -140,9 +147,16 @@ export const DisciplineCard: React.FC<DisciplineCardProps> = ({
</div>
{/* Lock Reasons */}
{effectiveIsLocked && (missingPrereqs.length > 0 || missingSourceMana.length > 0) && (
{(effectiveIsLocked || atConcurrentLimit) && (missingPrereqs.length > 0 || missingSourceMana.length > 0 || atConcurrentLimit) && (
<div className="mt-2 text-xs text-red-400">
<strong>Requires:</strong> {[...missingPrereqs, ...missingSourceMana].join(', ')}
{atConcurrentLimit && <div><strong>At limit:</strong> {activeIds.length}/{concurrentLimit} disciplines active</div>}
{missingPrereqs.length > 0 && <div><strong>Requires:</strong> {missingPrereqs.join(', ')}</div>}
{missingSourceMana.length > 0 && <div><strong>Missing mana:</strong> {missingSourceMana.join(', ')}</div>}
</div>
)}
{atConcurrentLimit && missingPrereqs.length === 0 && missingSourceMana.length === 0 && (
<div className="mt-2 text-xs text-amber-400">
<strong>At limit:</strong> {activeIds.length}/{concurrentLimit} disciplines active. Gain XP to unlock more slots.
</div>
)}
@@ -150,17 +164,23 @@ export const DisciplineCard: React.FC<DisciplineCardProps> = ({
<div className="mt-4 flex justify-end">
<button
onClick={() => onToggle(id, isPaused)}
disabled={effectiveIsLocked}
disabled={effectiveIsLocked || atConcurrentLimit}
className={clsx(
'rounded px-3 py-1 text-sm font-medium',
effectiveIsLocked
effectiveIsLocked || atConcurrentLimit
? 'bg-gray-600 text-gray-400 cursor-not-allowed'
: isPaused
? 'bg-yellow-600 text-white hover:bg-yellow-500'
: 'bg-blue-600 text-white hover:bg-blue-500',
)}
>
{effectiveIsLocked ? 'Locked' : isPaused ? 'Start Practicing' : 'Stop Practicing'}
{effectiveIsLocked
? 'Locked'
: atConcurrentLimit
? `At Limit (${activeIds.length}/${concurrentLimit})`
: isPaused
? 'Start Practicing'
: 'Stop Practicing'}
</button>
</div>
</div>
+5 -2
View File
@@ -41,6 +41,7 @@ const ATTUNEMENT_TABS: AttunementTab[] = [
interface CardWrapperProps {
disc: DisciplineDefinition;
disciplines: Record<string, { xp: number; paused: boolean }>;
activeIds: string[];
concurrentLimit: number;
elements: ReturnType<typeof useManaStore.getState>['elements'];
signedPacts: ReturnType<typeof usePrestigeStore.getState>['signedPacts'];
@@ -48,15 +49,16 @@ interface CardWrapperProps {
}
const CardWrapper: React.FC<CardWrapperProps> = ({
disc, disciplines, concurrentLimit, elements, onToggle,
disc, disciplines, activeIds, concurrentLimit, elements, signedPacts, onToggle,
}) => {
const discState = disciplines[disc.id] ?? { xp: 0, paused: true };
const prereqCheck = checkDisciplinePrerequisites(disc, disciplines, ALL_DISCIPLINES, elements);
const prereqCheck = checkDisciplinePrerequisites(disc, disciplines, ALL_DISCIPLINES, elements, signedPacts);
return (
<DisciplineCard
definition={disc}
xp={discState.xp}
paused={discState.paused}
activeIds={activeIds}
concurrentLimit={concurrentLimit}
isLocked={!prereqCheck.canProceed}
missingPrereqs={prereqCheck.missingPrereqs}
@@ -129,6 +131,7 @@ export const DisciplinesTab: React.FC = () => {
key={disc.id}
disc={disc}
disciplines={disciplines}
activeIds={activeIds}
concurrentLimit={concurrentLimit}
elements={elements}
signedPacts={signedPacts}
@@ -113,8 +113,7 @@ export function SpireCombatPage() {
setRoomsCleared(0);
const newRoom = generateSpireFloorState(currentFloor, 0, totalRooms);
setCurrentRoom(newRoom);
setAction('climb');
}, [currentFloor, totalRooms, setCurrentRoom, setAction]);
}, [currentFloor, totalRooms, setCurrentRoom]);
const _handleRoomCleared = () => {
const nextRoomIndex = roomsCleared + 1;
@@ -5,7 +5,7 @@ import { DebugName } from '@/components/game/debug/debug-context';
import { Separator } from '@/components/ui/separator';
import { FlaskConical } from 'lucide-react';
import { ELEMENTS } from '@/lib/game/constants';
import { usePrestigeStore, useManaStore } from '@/lib/game/stores';
import { usePrestigeStore, useManaStore, fmtDec } from '@/lib/game/stores';
import type { ElementState } from '@/lib/game/types';
interface ElementStatsSectionProps {
@@ -54,7 +54,7 @@ export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
return (
<div key={id} className="p-2 rounded transition-colors" style={{ border: `1px solid ${def?.color}30`, background: 'var(--bg-sunken)/50', textAlign: 'center' }}>
<div className="text-lg" style={{ color: def?.color }}>{def?.sym}</div>
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>{state.current}/{state.max}</div>
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>{fmtDec(state.current, 2)}/{fmtDec(state.max, 0)}</div>
</div>
);
})}
@@ -90,7 +90,7 @@ export function ManaStatsSection({ stats, elemMax }: ManaStatsSectionProps) {
<span style={{ color: 'var(--text-secondary)' }}>2/hr</span>
</div>
<div className="flex justify-between text-sm font-semibold border-t border-[var(--border-subtle)] pt-2">
<span style={{ color: 'var(--text-secondary)' }}>Base Regen:</span>
<span style={{ color: 'var(--text-secondary)' }}>Computed Base Regen:</span>
<span style={{ color: 'var(--mana-water)' }}>{fmtDec(baseRegen, 2)}/hr</span>
</div>
{upgradeEffects.regenBonus > 0 && (