- Current: Day {day}, Hour {hour}
+ Current: Day {day}, Hour {Number.isFinite(hour) ? hour.toFixed(2) : '0.00'}
diff --git a/src/components/game/tabs/DisciplineCard.tsx b/src/components/game/tabs/DisciplineCard.tsx
index 19160ab..e5caa6a 100644
--- a/src/components/game/tabs/DisciplineCard.tsx
+++ b/src/components/game/tabs/DisciplineCard.tsx
@@ -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 = ({
- 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 = ({
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 = ({
{/* Lock Reasons */}
- {effectiveIsLocked && (missingPrereqs.length > 0 || missingSourceMana.length > 0) && (
+ {(effectiveIsLocked || atConcurrentLimit) && (missingPrereqs.length > 0 || missingSourceMana.length > 0 || atConcurrentLimit) && (
-
Requires: {[...missingPrereqs, ...missingSourceMana].join(', ')}
+ {atConcurrentLimit &&
At limit: {activeIds.length}/{concurrentLimit} disciplines active
}
+ {missingPrereqs.length > 0 &&
Requires: {missingPrereqs.join(', ')}
}
+ {missingSourceMana.length > 0 &&
Missing mana: {missingSourceMana.join(', ')}
}
+
+ )}
+ {atConcurrentLimit && missingPrereqs.length === 0 && missingSourceMana.length === 0 && (
+
+ At limit: {activeIds.length}/{concurrentLimit} disciplines active. Gain XP to unlock more slots.
)}
@@ -150,17 +164,23 @@ export const DisciplineCard: React.FC = ({
diff --git a/src/components/game/tabs/DisciplinesTab.tsx b/src/components/game/tabs/DisciplinesTab.tsx
index 00eee79..4927a7d 100644
--- a/src/components/game/tabs/DisciplinesTab.tsx
+++ b/src/components/game/tabs/DisciplinesTab.tsx
@@ -41,6 +41,7 @@ const ATTUNEMENT_TABS: AttunementTab[] = [
interface CardWrapperProps {
disc: DisciplineDefinition;
disciplines: Record;
+ activeIds: string[];
concurrentLimit: number;
elements: ReturnType['elements'];
signedPacts: ReturnType['signedPacts'];
@@ -48,15 +49,16 @@ interface CardWrapperProps {
}
const CardWrapper: React.FC = ({
- 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 (
{
key={disc.id}
disc={disc}
disciplines={disciplines}
+ activeIds={activeIds}
concurrentLimit={concurrentLimit}
elements={elements}
signedPacts={signedPacts}
diff --git a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx
index 6efb128..00a54d4 100644
--- a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx
+++ b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx
@@ -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;
diff --git a/src/components/game/tabs/StatsTab/ElementStatsSection.tsx b/src/components/game/tabs/StatsTab/ElementStatsSection.tsx
index 9fa0a86..1c867fa 100644
--- a/src/components/game/tabs/StatsTab/ElementStatsSection.tsx
+++ b/src/components/game/tabs/StatsTab/ElementStatsSection.tsx
@@ -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 (
{def?.sym}
-
{state.current}/{state.max}
+
{fmtDec(state.current, 2)}/{fmtDec(state.max, 0)}
);
})}
diff --git a/src/components/game/tabs/StatsTab/ManaStatsSection.tsx b/src/components/game/tabs/StatsTab/ManaStatsSection.tsx
index 7d130ea..f8737f9 100644
--- a/src/components/game/tabs/StatsTab/ManaStatsSection.tsx
+++ b/src/components/game/tabs/StatsTab/ManaStatsSection.tsx
@@ -90,7 +90,7 @@ export function ManaStatsSection({ stats, elemMax }: ManaStatsSectionProps) {
2/hr
- Base Regen:
+ Computed Base Regen:
{fmtDec(baseRegen, 2)}/hr
{upgradeEffects.regenBonus > 0 && (
diff --git a/src/lib/game/stores/crafting-initial-state.ts b/src/lib/game/stores/crafting-initial-state.ts
index 0d946d8..1942e4a 100644
--- a/src/lib/game/stores/crafting-initial-state.ts
+++ b/src/lib/game/stores/crafting-initial-state.ts
@@ -3,6 +3,38 @@
import * as CraftingUtils from '../crafting-utils';
import type { EquipmentInstance } from '../types';
+import type { CraftingState } from './craftingStore.types';
+
+/**
+ * Create the full default state for the crafting store.
+ * Used by both initial store creation and resetCrafting().
+ */
+export function createDefaultCraftingState(): CraftingState {
+ const initial = createInitialEquipmentInstances();
+ return {
+ designProgress: null,
+ designProgress2: null,
+ preparationProgress: null,
+ applicationProgress: null,
+ equipmentCraftingProgress: null,
+ enchantmentDesigns: [],
+ unlockedEffects: [],
+ equippedInstances: initial.equippedInstances,
+ equipmentInstances: initial.instances,
+ lootInventory: {
+ materials: {},
+ blueprints: [],
+ },
+ enchantmentSelection: {
+ selectedEquipmentType: null,
+ selectedEffects: [],
+ designName: '',
+ selectedDesign: null,
+ selectedEquipmentInstance: null,
+ },
+ lastError: null,
+ };
+}
export function createInitialEquipmentInstances() {
const staffId = CraftingUtils.generateInstanceId();
diff --git a/src/lib/game/stores/craftingStore.ts b/src/lib/game/stores/craftingStore.ts
index 4fa1e05..2fef9fc 100644
--- a/src/lib/game/stores/craftingStore.ts
+++ b/src/lib/game/stores/craftingStore.ts
@@ -15,7 +15,7 @@ import * as CraftingEquipment from '../crafting-equipment';
import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '../crafting-actions/equipment-actions';
import { ErrorCode } from '../utils/result';
import { createSafeStorage } from '../utils/safe-persist';
-import { createInitialEquipmentInstances } from './crafting-initial-state';
+import { createDefaultCraftingState } from './crafting-initial-state';
import {
getFabricatorRecipe,
deductFabricatorMana,
@@ -28,30 +28,9 @@ import { processEquipmentCraftingTick } from './crafting-equipment-tick';
export const useCraftingStore = create()(
persist(
(set, get) => {
- const initial = createInitialEquipmentInstances();
+ const defaultState = createDefaultCraftingState();
return {
- // Initial state
- designProgress: null,
- designProgress2: null,
- preparationProgress: null,
- applicationProgress: null,
- equipmentCraftingProgress: null,
- enchantmentDesigns: [],
- unlockedEffects: [],
- equippedInstances: initial.equippedInstances,
- equipmentInstances: initial.instances,
- lootInventory: {
- materials: {},
- blueprints: [],
- },
- enchantmentSelection: {
- selectedEquipmentType: null,
- selectedEffects: [],
- designName: '',
- selectedDesign: null,
- selectedEquipmentInstance: null,
- },
- lastError: null,
+ ...defaultState,
// Actions
setDesignProgress: (progress) => set({ designProgress: progress }),
@@ -369,6 +348,10 @@ export const useCraftingStore = create()(
const state = get();
return processEquipmentCraftingTick(state, set as unknown as (partial: Partial) => void);
},
+
+ resetCrafting: () => {
+ set(createDefaultCraftingState());
+ },
};
},
{
diff --git a/src/lib/game/stores/craftingStore.types.ts b/src/lib/game/stores/craftingStore.types.ts
index c55bfc0..8e9569e 100644
--- a/src/lib/game/stores/craftingStore.types.ts
+++ b/src/lib/game/stores/craftingStore.types.ts
@@ -73,6 +73,7 @@ export interface CraftingActions {
clearLastError: () => void;
unlockEffects: (effectIds: string[]) => void;
processEquipmentCraftingTick: () => { completed: boolean; logMessage?: string };
+ resetCrafting: () => void;
}
export type CraftingStore = CraftingState & CraftingActions;
diff --git a/src/lib/game/stores/discipline-slice.ts b/src/lib/game/stores/discipline-slice.ts
index 3cf4e7f..685acd7 100644
--- a/src/lib/game/stores/discipline-slice.ts
+++ b/src/lib/game/stores/discipline-slice.ts
@@ -95,14 +95,8 @@ export const useDisciplineStore = create()(
if (nonPaused >= s.concurrentLimit) return s;
if (!canProceedDiscipline(def, existing, gameState)) return s;
- // Invoker disciplines require at least one signed guardian pact
- if (def.attunement === 'invoker') {
- const signedPacts = gameState?.signedPacts || [];
- if (signedPacts.length === 0) return s;
- }
-
// Check discipline prerequisites (requires field → discipline XP or mana type unlock)
- const prereqCheck = checkDisciplinePrerequisites(def, s.disciplines, ALL_DISCIPLINES, gameState?.elements);
+ const prereqCheck = checkDisciplinePrerequisites(def, s.disciplines, ALL_DISCIPLINES, gameState?.elements, gameState?.signedPacts);
if (!prereqCheck.canProceed) return s;
// For conversion disciplines: gate on having all source mana types unlocked
diff --git a/src/lib/game/stores/gameActions.ts b/src/lib/game/stores/gameActions.ts
index 13e09bb..fb4d9e4 100644
--- a/src/lib/game/stores/gameActions.ts
+++ b/src/lib/game/stores/gameActions.ts
@@ -4,6 +4,9 @@ import { useUIStore } from './uiStore';
import { usePrestigeStore } from './prestigeStore';
import { useManaStore } from './manaStore';
import { useCombatStore } from './combatStore';
+import { useDisciplineStore } from './discipline-slice';
+import { useAttunementStore } from './attunementStore';
+import { useCraftingStore } from './craftingStore';
import { computeDisciplineEffects } from '../effects/discipline-effects';
// Exact localStorage keys matching each store's persist config `name`
@@ -32,6 +35,9 @@ export const createResetGame = (set: (state: Partial) => v
usePrestigeStore.getState().resetPrestige();
useManaStore.getState().resetMana({});
useCombatStore.getState().resetCombat(startFloor);
+ useDisciplineStore.getState().resetDisciplines();
+ useAttunementStore.getState().resetAttunements();
+ useCraftingStore.getState().resetCrafting();
set({
...initialState,
diff --git a/src/lib/game/utils/discipline-math.ts b/src/lib/game/utils/discipline-math.ts
index 92bead7..ef9de35 100644
--- a/src/lib/game/utils/discipline-math.ts
+++ b/src/lib/game/utils/discipline-math.ts
@@ -117,6 +117,7 @@ export function checkDisciplinePrerequisites(
allDisciplines: Record,
allDefinitions: DisciplineDefinition[],
elements?: Record,
+ signedPacts?: number[],
): { canProceed: boolean; missingPrereqs: string[] } {
if (!discipline.requires || discipline.requires.length === 0) {
return { canProceed: true, missingPrereqs: [] };
@@ -127,7 +128,9 @@ export function checkDisciplinePrerequisites(
for (const reqId of discipline.requires) {
// Special case: 'signed_pact' requires at least one guardian pact
if (reqId === 'signed_pact') {
- missingPrereqs.push('Signed guardian pact');
+ if (!signedPacts || signedPacts.length === 0) {
+ missingPrereqs.push('Signed guardian pact');
+ }
continue;
}