fix: discipline stat bonuses not applied to max mana and other stats in reactive UI
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s
Bug A: useMemo with empty deps in useGameDerived.ts — disciplineEffects and clickMana were computed once on mount and never recomputed, so the display always showed bonuses as 0. Fixed by subscribing to discipline store state and passing disciplineEffects to computeClickMana(). Bug B: autoPaused disciplines excluded from computeDisciplineEffects — when a discipline auto-paused due to insufficient mana drain, it lost all its stat bonuses even though it remained active. Fixed by changing the filter from !disc.autoPaused to !disc.paused, so auto-paused disciplines keep their earned bonuses (they just stop gaining XP). Added regression tests in discipline-effects-reactivity.test.ts.
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-06-14T19:56:38.228Z
|
||||
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
Generated: 2026-06-14T21:49:16.203Z
|
||||
Found: 7 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
|
||||
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
||||
2. 2) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||
3. 3) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts
|
||||
4. 4) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts > stores/non-combat-room-actions.ts
|
||||
2. 2) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts
|
||||
3. 3) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts > stores/crafting-equipment-tick.ts
|
||||
4. 4) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts > stores/pipelines/equipment-crafting.ts
|
||||
5. 5) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||
6. 6) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts
|
||||
7. 7) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts > stores/non-combat-room-actions.ts
|
||||
|
||||
## How to fix
|
||||
1. Identify which import in the chain can be extracted to a shared types/utils file.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-06-14T19:56:35.928Z",
|
||||
"generated": "2026-06-14T21:49:13.932Z",
|
||||
"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."
|
||||
},
|
||||
@@ -245,6 +245,9 @@
|
||||
"data/disciplines/elemental.ts": [
|
||||
"types/disciplines.ts"
|
||||
],
|
||||
"data/disciplines/enchanter-combat.ts": [
|
||||
"types/disciplines.ts"
|
||||
],
|
||||
"data/disciplines/enchanter-special.ts": [
|
||||
"types/disciplines.ts"
|
||||
],
|
||||
@@ -265,6 +268,7 @@
|
||||
"data/disciplines/elemental-regen-advanced.ts",
|
||||
"data/disciplines/elemental-regen.ts",
|
||||
"data/disciplines/elemental.ts",
|
||||
"data/disciplines/enchanter-combat.ts",
|
||||
"data/disciplines/enchanter-special.ts",
|
||||
"data/disciplines/enchanter-spells.ts",
|
||||
"data/disciplines/enchanter-utility.ts",
|
||||
@@ -558,6 +562,7 @@
|
||||
"stores/combat-damage.ts",
|
||||
"stores/combat-invocation.ts",
|
||||
"stores/combat-melee.ts",
|
||||
"stores/combat-room-enchantments.ts",
|
||||
"stores/combat-state.types.ts",
|
||||
"stores/dot-runtime.ts",
|
||||
"stores/golem-combat-actions.ts",
|
||||
@@ -618,6 +623,14 @@
|
||||
"utils/index.ts",
|
||||
"utils/spire-utils.ts"
|
||||
],
|
||||
"stores/combat-room-enchantments.ts": [
|
||||
"data/guardian-encounters.ts",
|
||||
"effects/discipline-effects.ts",
|
||||
"stores/combat-state.types.ts",
|
||||
"stores/craftingStore.ts",
|
||||
"types.ts",
|
||||
"utils/room-enchantments-utils.ts"
|
||||
],
|
||||
"stores/combat-state.types.ts": [
|
||||
"types.ts",
|
||||
"utils/invocation-utils.ts"
|
||||
@@ -983,6 +996,10 @@
|
||||
"data/guardian-encounters.ts"
|
||||
],
|
||||
"utils/result.ts": [],
|
||||
"utils/room-enchantments-utils.ts": [
|
||||
"data/enchantments/special-effects.ts",
|
||||
"types.ts"
|
||||
],
|
||||
"utils/room-utils.ts": [
|
||||
"constants.ts",
|
||||
"data/guardian-encounters.ts",
|
||||
|
||||
@@ -224,6 +224,7 @@ Mana-Loop/
|
||||
│ │ │ │ ├── day30-blank-page.test.ts
|
||||
│ │ │ │ ├── design-validation-perk-gating.test.ts
|
||||
│ │ │ │ ├── discipline-deactivate-on-spire-entry.test.ts
|
||||
│ │ │ │ ├── discipline-effects-reactivity.test.ts
|
||||
│ │ │ │ ├── discipline-math.test.ts
|
||||
│ │ │ │ ├── discipline-prerequisites.test.ts
|
||||
│ │ │ │ ├── discipline-reactivate-bug.test.ts
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||
import { baseDisciplines } from '../data/disciplines/base';
|
||||
|
||||
// ─── Test Fixtures ────────────────────────────────────────────────────────────
|
||||
|
||||
const rawMastery = baseDisciplines.find((d) => d.id === 'raw-mastery')!;
|
||||
|
||||
/**
|
||||
* Helper: set the discipline store to a known state.
|
||||
* We use the store's setState directly so we can control exactly what
|
||||
* computeDisciplineEffects sees when it calls getState().
|
||||
*/
|
||||
function setDisciplineState(overrides: Partial<ReturnType<typeof useDisciplineStore.getState>>) {
|
||||
useDisciplineStore.setState(overrides as any);
|
||||
}
|
||||
|
||||
// ─── Bug B: autoPaused disciplines should still contribute stat bonuses ───────
|
||||
|
||||
describe('computeDisciplineEffects — autoPaused discipline bonuses (Bug B)', () => {
|
||||
beforeEach(() => {
|
||||
// Reset to clean state
|
||||
setDisciplineState({
|
||||
disciplines: {},
|
||||
activeIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should include stat bonuses from an auto-paused discipline with XP', () => {
|
||||
setDisciplineState({
|
||||
disciplines: {
|
||||
'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: false, autoPaused: true },
|
||||
},
|
||||
activeIds: ['raw-mastery'],
|
||||
});
|
||||
|
||||
const effects = computeDisciplineEffects();
|
||||
expect(effects.bonuses.maxManaBonus).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should NOT include stat bonuses from a manually paused discipline', () => {
|
||||
setDisciplineState({
|
||||
disciplines: {
|
||||
'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: true, autoPaused: false },
|
||||
},
|
||||
activeIds: [],
|
||||
});
|
||||
|
||||
const effects = computeDisciplineEffects();
|
||||
expect(effects.bonuses.maxManaBonus || 0).toBe(0);
|
||||
});
|
||||
|
||||
it('should include stat bonuses from an active (non-paused) discipline', () => {
|
||||
setDisciplineState({
|
||||
disciplines: {
|
||||
'raw-mastery': { id: 'raw-mastery', xp: 500, paused: false, autoPaused: false },
|
||||
},
|
||||
activeIds: ['raw-mastery'],
|
||||
});
|
||||
|
||||
const effects = computeDisciplineEffects();
|
||||
expect(effects.bonuses.maxManaBonus).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('auto-paused discipline with 0 XP should not contribute bonuses', () => {
|
||||
setDisciplineState({
|
||||
disciplines: {
|
||||
'raw-mastery': { id: 'raw-mastery', xp: 0, paused: false, autoPaused: true },
|
||||
},
|
||||
activeIds: ['raw-mastery'],
|
||||
});
|
||||
|
||||
const effects = computeDisciplineEffects();
|
||||
expect(effects.bonuses.maxManaBonus || 0).toBe(0);
|
||||
});
|
||||
|
||||
it('should distinguish autoPaused from paused: mixed scenario', () => {
|
||||
setDisciplineState({
|
||||
disciplines: {
|
||||
'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: false, autoPaused: true },
|
||||
},
|
||||
activeIds: ['raw-mastery'],
|
||||
});
|
||||
|
||||
const effects = computeDisciplineEffects();
|
||||
// autoPaused=true, paused=false → should be included
|
||||
expect(effects.bonuses.maxManaBonus).toBeGreaterThan(0);
|
||||
|
||||
// Now manually pause it
|
||||
setDisciplineState({
|
||||
disciplines: {
|
||||
'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: true, autoPaused: false },
|
||||
},
|
||||
activeIds: [],
|
||||
});
|
||||
|
||||
const effects2 = computeDisciplineEffects();
|
||||
expect(effects2.bonuses.maxManaBonus || 0).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Bug A concept: computeDisciplineEffects reads current state ──────────────
|
||||
|
||||
describe('computeDisciplineEffects — reactive state reads (Bug A)', () => {
|
||||
beforeEach(() => {
|
||||
setDisciplineState({
|
||||
disciplines: {},
|
||||
activeIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return updated bonuses when discipline XP changes in the store', () => {
|
||||
// Start with some XP
|
||||
setDisciplineState({
|
||||
disciplines: {
|
||||
'raw-mastery': { id: 'raw-mastery', xp: 100, paused: false, autoPaused: false },
|
||||
},
|
||||
activeIds: ['raw-mastery'],
|
||||
});
|
||||
|
||||
const effects1 = computeDisciplineEffects();
|
||||
const bonus1 = effects1.bonuses.maxManaBonus;
|
||||
expect(bonus1).toBeGreaterThan(0);
|
||||
|
||||
// Simulate tick: XP increases
|
||||
setDisciplineState({
|
||||
disciplines: {
|
||||
'raw-mastery': { id: 'raw-mastery', xp: 5000, paused: false, autoPaused: false },
|
||||
},
|
||||
activeIds: ['raw-mastery'],
|
||||
});
|
||||
|
||||
const effects2 = computeDisciplineEffects();
|
||||
const bonus2 = effects2.bonuses.maxManaBonus;
|
||||
|
||||
// Higher XP → higher bonus (stat bonus is monotonically increasing)
|
||||
expect(bonus2).toBeGreaterThan(bonus1);
|
||||
});
|
||||
|
||||
it('should return zero bonuses when store is empty (fresh game)', () => {
|
||||
setDisciplineState({
|
||||
disciplines: {},
|
||||
activeIds: [],
|
||||
});
|
||||
|
||||
const effects = computeDisciplineEffects();
|
||||
expect(effects.bonuses.maxManaBonus || 0).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -84,7 +84,7 @@ export interface DisciplineEffectsResult {
|
||||
export function computeDisciplineEffects(_state?: DisciplineStoreState): DisciplineEffectsResult {
|
||||
const { disciplines } = useDisciplineStore.getState();
|
||||
const activeDiscs = Object.entries(disciplines)
|
||||
.filter(([, disc]) => disc && disc.xp > 0 && !disc.autoPaused)
|
||||
.filter(([, disc]) => disc && disc.xp > 0 && !disc.paused)
|
||||
.map(([id, disc]) => ({ id, disc, def: ALL_DISCIPLINES.find(d => d.id === id) }))
|
||||
.filter((entry): entry is { id: string; disc: DisciplineState; def: NonNullable<typeof ALL_DISCIPLINES[0]> } => !!entry.def);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useManaStore } from '../stores/manaStore';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||
import { useAttunementStore } from '../stores/attunementStore';
|
||||
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||
import { computeEffects } from '../effects/upgrade-effects';
|
||||
import {
|
||||
computeMaxMana,
|
||||
@@ -39,9 +40,11 @@ export function useManaStats() {
|
||||
const hour = useGameStore((s) => s.hour);
|
||||
const attunements = useAttunementStore((s) => s.attunements);
|
||||
|
||||
const disciplines = useDisciplineStore((s) => s.disciplines);
|
||||
|
||||
const disciplineEffects = useMemo(
|
||||
() => computeDisciplineEffects(),
|
||||
[]
|
||||
[disciplines]
|
||||
);
|
||||
|
||||
const upgradeEffects = useMemo(
|
||||
@@ -60,8 +63,8 @@ export function useManaStats() {
|
||||
);
|
||||
|
||||
const clickMana = useMemo(
|
||||
() => computeClickMana(),
|
||||
[]
|
||||
() => computeClickMana(disciplineEffects),
|
||||
[disciplineEffects]
|
||||
);
|
||||
|
||||
const meditationCap = 5.0 + disciplineEffects.meditationCapBonus;
|
||||
|
||||
Reference in New Issue
Block a user