[High] [Bug] Cannot restart practicing a discipline after stopping it #319

Closed
opened 2026-06-08 15:31:08 +02:00 by Anexim · 3 comments
Owner

Bug: Cannot restart practicing a discipline after stopping it

Description

When a player stops practicing a discipline (clicks "Stop Practicing"), they are unable to restart it by clicking "Start Practicing" again. The button appears clickable but the discipline does not reactivate — it silently fails.

Reproduction Steps

  1. Activate any discipline (e.g., Raw Mana Mastery) — it starts practicing
  2. Click "Stop Practicing" to deactivate it
  3. Click "Start Practicing" again to reactivate it
  4. Expected: Discipline resumes practicing
  5. Actual: Nothing happens — discipline remains inactive

Root Cause

The bug is in the interaction between DisciplinesTab.tsx and discipline-slice.ts.

In DisciplinesTab.tsx (line 91), the handleToggle callback calls activate without passing rawMana:

const handleToggle = useCallback((id: string, paused: boolean) => {
    if (paused) {
      activate(id, { elements, signedPacts });  // ← no rawMana!
    } else {
      deactivate(id);
    }
}, [activate, deactivate, elements, signedPacts]);

In discipline-slice.ts (line 72-86), the activate method calls canProceedDiscipline(def, existing, gameState). After deactivation, existing is defined (it has { id, xp, paused: true }), so the early-return if (!disciplineState) return true on first activation no longer applies.

In discipline-math.ts (line 70-86), canProceedDiscipline checks:

if (discipline.manaType === 'raw') {
    return (gameState.rawMana || 0) >= drain;
}

Since rawMana is never passed, gameState.rawMana is undefined, so undefined || 0 = 0, and 0 >= 1 (drain is 1) returns false. The activation is silently rejected.

Affected Files

  • src/components/game/tabs/DisciplinesTab.tsx — UI does not pass rawMana in gameState
  • src/lib/game/stores/discipline-slice.tsactivate method relies on gameState.rawMana for re-activation guard
  • src/lib/game/utils/discipline-math.tscanProceedDiscipline returns false when rawMana is missing

Impact

  • Severity: High — core game loop mechanic is broken
  • Scope: ALL disciplines are affected (raw and elemental), since canProceedDiscipline requires mana availability for re-activation but the UI never supplies it
  • Workaround: None for the player; the discipline cannot be restarted without cheating via debug tools

Evidence

  • Existing test src/lib/game/__tests__/discipline-reactivate-bug.test.ts documents this exact bug — the first test case explicitly asserts the broken behavior (activeIds does NOT contain the discipline after reactivation without rawMana)
  • The test was written as a bug documentation test (for "bug #163"), confirming this is a known but unfixed issue

Suggested Fix Direction

Pass rawMana from the mana store into the activate call in DisciplinesTab.tsx, or change canProceedDiscipline to be optimistic (return true) when gameState doesn't include mana info, matching the first-activation behavior.

## Bug: Cannot restart practicing a discipline after stopping it ### Description When a player stops practicing a discipline (clicks "Stop Practicing"), they are unable to restart it by clicking "Start Practicing" again. The button appears clickable but the discipline does not reactivate — it silently fails. ### Reproduction Steps 1. Activate any discipline (e.g., Raw Mana Mastery) — it starts practicing 2. Click "Stop Practicing" to deactivate it 3. Click "Start Practicing" again to reactivate it 4. **Expected:** Discipline resumes practicing 5. **Actual:** Nothing happens — discipline remains inactive ### Root Cause The bug is in the interaction between `DisciplinesTab.tsx` and `discipline-slice.ts`. **In `DisciplinesTab.tsx` (line 91)**, the `handleToggle` callback calls `activate` without passing `rawMana`: ```ts const handleToggle = useCallback((id: string, paused: boolean) => { if (paused) { activate(id, { elements, signedPacts }); // ← no rawMana! } else { deactivate(id); } }, [activate, deactivate, elements, signedPacts]); ``` **In `discipline-slice.ts` (line 72-86)**, the `activate` method calls `canProceedDiscipline(def, existing, gameState)`. After deactivation, `existing` is defined (it has `{ id, xp, paused: true }`), so the early-return `if (!disciplineState) return true` on first activation no longer applies. **In `discipline-math.ts` (line 70-86)**, `canProceedDiscipline` checks: ```ts if (discipline.manaType === 'raw') { return (gameState.rawMana || 0) >= drain; } ``` Since `rawMana` is never passed, `gameState.rawMana` is `undefined`, so `undefined || 0` = `0`, and `0 >= 1` (drain is 1) returns `false`. The activation is silently rejected. ### Affected Files - `src/components/game/tabs/DisciplinesTab.tsx` — UI does not pass `rawMana` in `gameState` - `src/lib/game/stores/discipline-slice.ts` — `activate` method relies on `gameState.rawMana` for re-activation guard - `src/lib/game/utils/discipline-math.ts` — `canProceedDiscipline` returns `false` when `rawMana` is missing ### Impact - **Severity:** High — core game loop mechanic is broken - **Scope:** ALL disciplines are affected (raw and elemental), since `canProceedDiscipline` requires mana availability for re-activation but the UI never supplies it - **Workaround:** None for the player; the discipline cannot be restarted without cheating via debug tools ### Evidence - Existing test `src/lib/game/__tests__/discipline-reactivate-bug.test.ts` documents this exact bug — the first test case explicitly asserts the broken behavior (`activeIds` does NOT contain the discipline after reactivation without `rawMana`) - The test was written as a bug documentation test (for "bug #163"), confirming this is a known but unfixed issue ### Suggested Fix Direction Pass `rawMana` from the mana store into the `activate` call in `DisciplinesTab.tsx`, or change `canProceedDiscipline` to be optimistic (return `true`) when `gameState` doesn't include mana info, matching the first-activation behavior.
Anexim added the ai:todo label 2026-06-08 15:31:08 +02:00
n8n-gitea was assigned by Anexim 2026-06-08 15:31:08 +02:00
Anexim added ai:in-progress and removed ai:todo labels 2026-06-09 09:57:13 +02:00
Author
Owner

Starting investigation. Root cause is clear: DisciplinesTab.tsx doesn't pass rawMana in the gameState object when calling activate(id, { elements, signedPacts }), so on re-activation after deactivation, canProceedDiscipline fails silently. Reading all three affected files now.

Starting investigation. Root cause is clear: `DisciplinesTab.tsx` doesn't pass `rawMana` in the `gameState` object when calling `activate(id, { elements, signedPacts })`, so on re-activation after deactivation, `canProceedDiscipline` fails silently. Reading all three affected files now.
Anexim added ai:done and removed ai:in-progress labels 2026-06-09 10:00:34 +02:00
Author
Owner

Fixed. The root cause was that DisciplinesTab.tsx called activate(id, { elements, signedPacts }) without passing rawMana. After deactivation, the discipline state exists (with paused: true), so canProceedDiscipline evaluates mana drain check — but gameState.rawMana was undefined, causing undefined || 0 = 0 >= drain to return false, silently rejecting reactivation.

Changes:

  1. DisciplinesTab.tsx — Added rawMana from useManaStore to the gameState passed to activate()
  2. discipline-slice.ts — Updated activate type signature to include rawMana?: number
  3. discipline-reactivate-bug.test.ts — Updated test to assert correct behavior: reactivation with sufficient mana succeeds; reactivation with 0 mana correctly fails

All 1087 tests pass.

Fixed. The root cause was that `DisciplinesTab.tsx` called `activate(id, { elements, signedPacts })` without passing `rawMana`. After deactivation, the discipline state exists (with `paused: true`), so `canProceedDiscipline` evaluates mana drain check — but `gameState.rawMana` was `undefined`, causing `undefined || 0 = 0 >= drain` to return `false`, silently rejecting reactivation. Changes: 1. **DisciplinesTab.tsx** — Added `rawMana` from `useManaStore` to the `gameState` passed to `activate()` 2. **discipline-slice.ts** — Updated `activate` type signature to include `rawMana?: number` 3. **discipline-reactivate-bug.test.ts** — Updated test to assert correct behavior: reactivation with sufficient mana succeeds; reactivation with 0 mana correctly fails All 1087 tests pass.
Author
Owner

Root cause analysis & robust fix applied.

The previous fix (passing rawMana from the UI) was fragile — it only addressed raw mana disciplines, and any future discipline using a different mana type (earth, fire, metal, etc.) would have the same silent failure.

The real problem: The activate() method required the UI to manually construct and pass a gameState bag ({ elements, rawMana, signedPacts }). If any field was missing, canProceedDiscipline silently returned false. This is a class of bug that would keep resurfacing.

The robust fix: activate() now reads rawMana, elements, and signedPacts directly from useManaStore and usePrestigeStore via getState(). The UI only needs to call activate(id) — no gameState bag needed. Tests can still pass explicit overrides when they need to control the check.

Changes:

  • discipline-slice.tsactivate() resolves game state from stores by default; gameStateOverrides param kept for test control
  • DisciplinesTab.tsx — simplified to just activate(id), removed all gameState bag construction
  • discipline-reactivate-bug.test.ts — updated to set mana store state directly instead of passing gameState

All 1088 tests pass. This fix handles ALL 22 mana types (raw + 7 base + 1 utility + 8 composite + 6 exotic) and will not break when new mana types or new guard conditions are added.

**Root cause analysis & robust fix applied.** The previous fix (passing `rawMana` from the UI) was fragile — it only addressed raw mana disciplines, and any future discipline using a different mana type (earth, fire, metal, etc.) would have the same silent failure. **The real problem:** The `activate()` method required the UI to manually construct and pass a `gameState` bag (`{ elements, rawMana, signedPacts }`). If any field was missing, `canProceedDiscipline` silently returned false. This is a class of bug that would keep resurfacing. **The robust fix:** `activate()` now reads `rawMana`, `elements`, and `signedPacts` directly from `useManaStore` and `usePrestigeStore` via `getState()`. The UI only needs to call `activate(id)` — no gameState bag needed. Tests can still pass explicit overrides when they need to control the check. Changes: - `discipline-slice.ts` — `activate()` resolves game state from stores by default; `gameStateOverrides` param kept for test control - `DisciplinesTab.tsx` — simplified to just `activate(id)`, removed all gameState bag construction - `discipline-reactivate-bug.test.ts` — updated to set mana store state directly instead of passing gameState All 1088 tests pass. This fix handles ALL 22 mana types (raw + 7 base + 1 utility + 8 composite + 6 exotic) and will not break when new mana types or new guard conditions are added.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Anexim/Mana-Loop#319