diff --git a/.gitignore b/.gitignore
index 128c8f8..6d70c40 100755
--- a/.gitignore
+++ b/.gitignore
@@ -50,3 +50,4 @@ server.log
# Skills directory
.desloppify/
test-results/
+playwright-report/
diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt
index e12c6b3..66b6c0b 100644
--- a/docs/circular-deps.txt
+++ b/docs/circular-deps.txt
@@ -1,4 +1,4 @@
# Circular Dependencies
-Generated: 2026-06-01T10:58:05.599Z
+Generated: 2026-06-02T08:49:51.414Z
No circular dependencies found. ✅
diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json
index c5cd177..20634b6 100644
--- a/docs/dependency-graph.json
+++ b/docs/dependency-graph.json
@@ -1,6 +1,6 @@
{
"_meta": {
- "generated": "2026-06-01T10:58:03.834Z",
+ "generated": "2026-06-02T08:49:49.529Z",
"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."
},
@@ -586,6 +586,16 @@
"types.ts",
"types/equipmentSlot.ts"
],
+ "stores/debugBridge.ts": [
+ "stores/attunementStore.ts",
+ "stores/combatStore.ts",
+ "stores/craftingStore.ts",
+ "stores/discipline-slice.ts",
+ "stores/gameStore.ts",
+ "stores/manaStore.ts",
+ "stores/prestigeStore.ts",
+ "stores/uiStore.ts"
+ ],
"stores/discipline-slice.ts": [
"data/disciplines/base.ts",
"data/disciplines/elemental-regen-advanced.ts",
diff --git a/docs/project-structure.txt b/docs/project-structure.txt
index 194f816..43fc2b0 100644
--- a/docs/project-structure.txt
+++ b/docs/project-structure.txt
@@ -19,10 +19,6 @@ Mana-Loop/
│ ├── enchanter-happy-path.spec.ts
│ ├── fabricator-happy-path.spec.ts
│ └── playtest.spec.ts
-├── playwright-report/
-│ ├── data/
-│ │ └── 199a0ed84e7318aab410b0ec2f96ea8f6478a4da.png
-│ └── index.html
├── public/
│ ├── fonts/
│ │ ├── GeistMonoVF.woff
diff --git a/playwright-report/index.html b/playwright-report/index.html
deleted file mode 100644
index 3acc56f..0000000
--- a/playwright-report/index.html
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
-
- Playwright Test Report
-
-
-
-
-
-
-
-data:application/zip;base64,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
\ No newline at end of file
diff --git a/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx b/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx
index d0ba818..38f15d1 100644
--- a/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx
+++ b/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BookOpen, Plus, Pause, Play } from 'lucide-react';
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
+import { MAX_CONCURRENT_DISCIPLINES } from '@/lib/game/types/disciplines';
import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines';
import { useManaStore } from '@/lib/game/stores/manaStore';
import { DebugName } from '@/components/game/debug/debug-context';
@@ -32,11 +33,18 @@ export function DisciplineDebugSection() {
useDisciplineStore.setState((s) => {
const disc = s.disciplines[id];
if (!disc) return s;
+ const newTotalXP = s.totalXP + amount;
+ const newLimit = Math.min(
+ MAX_CONCURRENT_DISCIPLINES + Math.floor(newTotalXP / 500),
+ MAX_CONCURRENT_DISCIPLINES + 3,
+ );
return {
disciplines: {
...s.disciplines,
[id]: { ...disc, xp: disc.xp + amount },
},
+ totalXP: newTotalXP,
+ concurrentLimit: Math.max(s.concurrentLimit, newLimit),
};
});
};
diff --git a/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx b/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx
index f466388..2a0fc94 100644
--- a/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx
+++ b/src/components/game/tabs/DebugTab/GameStateDebugSection.tsx
@@ -167,8 +167,9 @@ function TimeControlSection({ day, hour, paused, onSetDay, onTogglePause }: {
// ─── Quick Actions Section ───────────────────────────────────────────────────
-function QuickActionsSection({ onUnlockBase }: {
+function QuickActionsSection({ onUnlockBase, onAddStarterMaterials }: {
onUnlockBase: () => void;
+ onAddStarterMaterials: () => void;
}) {
return (
@@ -183,6 +184,9 @@ function QuickActionsSection({ onUnlockBase }: {
+
@@ -248,6 +252,21 @@ export function GameStateDebugSection() {
});
};
+ const handleAddStarterMaterials = () => {
+ useCraftingStore.setState((s) => ({
+ lootInventory: {
+ ...s.lootInventory,
+ materials: {
+ ...s.lootInventory.materials,
+ manaCrystalDust: (s.lootInventory.materials.manaCrystalDust || 0) + 20,
+ earthShard: (s.lootInventory.materials.earthShard || 0) + 10,
+ metalShard: (s.lootInventory.materials.metalShard || 0) + 5,
+ elementalCore: (s.lootInventory.materials.elementalCore || 0) + 3,
+ },
+ },
+ }));
+ };
+
return (
@@ -259,6 +278,7 @@ export function GameStateDebugSection() {
diff --git a/src/components/game/tabs/DebugTab/PactDebugSection.tsx b/src/components/game/tabs/DebugTab/PactDebugSection.tsx
index 341e4d3..2ae5d90 100644
--- a/src/components/game/tabs/DebugTab/PactDebugSection.tsx
+++ b/src/components/game/tabs/DebugTab/PactDebugSection.tsx
@@ -71,13 +71,16 @@ export function PactDebugSection() {
const guardian = getGuardianForFloor(floor);
if (!guardian) return;
- if (signedPacts.includes(floor)) {
+ // Always read fresh state from store to avoid stale closures
+ const currentSignedPacts = usePrestigeStore.getState().signedPacts;
+ const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0);
+
+ if (currentSignedPacts.includes(floor)) {
addLog(`⚠️ Already signed pact with ${guardian.name}!`);
return;
}
- const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0);
- if (signedPacts.length >= maxPacts) {
+ if (currentSignedPacts.length >= maxPacts) {
addLog(`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`);
return;
}
@@ -111,8 +114,14 @@ export function PactDebugSection() {
};
const signAllPacts = () => {
+ const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0);
guardianFloors.forEach((floor) => {
- if (!signedPacts.includes(floor)) {
+ // Read fresh state from store to avoid stale closure bug:
+ // signedPacts from render-time closure is always the initial value
+ // during the loop, so the maxPacts check never triggers.
+ const currentSigned = usePrestigeStore.getState().signedPacts;
+ if (currentSigned.length >= maxPacts) return;
+ if (!currentSigned.includes(floor)) {
forcePact(floor);
}
});
diff --git a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx
index 651e4cf..7bbd333 100644
--- a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx
+++ b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState, useEffect, useMemo } from 'react';
+import { useState, useEffect, useMemo, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
@@ -127,7 +127,15 @@ export function SpireCombatPage() {
return base + floorBonus + randomVariation;
}, [currentFloor, seededRandom]);
+ // Track the last floor+totalRooms combo we generated a room for.
+ // Prevents infinite re-render loop: without this guard, the effect
+ // fires → setCurrentRoom → store update → re-render → tick advances
+ // currentFloor → effect fires → ... (loop).
+ const lastGeneratedRef = useRef(null);
useEffect(() => {
+ const key = `${currentFloor}:${totalRooms}`;
+ if (lastGeneratedRef.current === key) return; // already generated
+ lastGeneratedRef.current = key;
setRoomsCleared(0);
const newRoom = generateSpireFloorState(currentFloor, 0, totalRooms);
setCurrentRoom(newRoom);
diff --git a/src/components/game/tabs/SpireSummaryTab.tsx b/src/components/game/tabs/SpireSummaryTab.tsx
index f97d554..792f0e3 100644
--- a/src/components/game/tabs/SpireSummaryTab.tsx
+++ b/src/components/game/tabs/SpireSummaryTab.tsx
@@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useCombatStore, usePrestigeStore } from '@/lib/game/stores';
import { FLOOR_ELEM_CYCLE } from '@/lib/game/constants';
-import { getGuardianForFloor } from '@/lib/game/data/guardian-encounters';
+import { getGuardianForFloor, getAllGuardianFloors } from '@/lib/game/data/guardian-encounters';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';