fix: resolve 5 bugs — missing import, infinite render loop, stale closures, discipline XP
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

- #249: Add missing getAllGuardianFloors import to SpireSummaryTab.tsx
- #250/#252: Add useRef guard in SpireCombatPage useEffect to prevent infinite re-render loop
- #251: Fix stale closure in PactDebugSection signAllPacts/forcePact — read signedPacts from store.getState()
- #253: Fix DisciplineDebugSection handleAddXP to update totalXP and concurrentLimit
- #252: Marked duplicate of #250
This commit is contained in:
2026-06-02 12:07:07 +02:00
parent fe78ae047f
commit f6f6ef4379
10 changed files with 65 additions and 103 deletions
+1
View File
@@ -50,3 +50,4 @@ server.log
# Skills directory # Skills directory
.desloppify/ .desloppify/
test-results/ test-results/
playwright-report/
+1 -1
View File
@@ -1,4 +1,4 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-06-01T10:58:05.599Z Generated: 2026-06-02T08:49:51.414Z
No circular dependencies found. ✅ No circular dependencies found. ✅
+11 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_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.", "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." "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.ts",
"types/equipmentSlot.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": [ "stores/discipline-slice.ts": [
"data/disciplines/base.ts", "data/disciplines/base.ts",
"data/disciplines/elemental-regen-advanced.ts", "data/disciplines/elemental-regen-advanced.ts",
-4
View File
@@ -19,10 +19,6 @@ Mana-Loop/
│ ├── enchanter-happy-path.spec.ts │ ├── enchanter-happy-path.spec.ts
│ ├── fabricator-happy-path.spec.ts │ ├── fabricator-happy-path.spec.ts
│ └── playtest.spec.ts │ └── playtest.spec.ts
├── playwright-report/
│ ├── data/
│ │ └── 199a0ed84e7318aab410b0ec2f96ea8f6478a4da.png
│ └── index.html
├── public/ ├── public/
│ ├── fonts/ │ ├── fonts/
│ │ ├── GeistMonoVF.woff │ │ ├── GeistMonoVF.woff
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BookOpen, Plus, Pause, Play } from 'lucide-react'; import { BookOpen, Plus, Pause, Play } from 'lucide-react';
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice'; 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 { ALL_DISCIPLINES } from '@/lib/game/data/disciplines';
import { useManaStore } from '@/lib/game/stores/manaStore'; import { useManaStore } from '@/lib/game/stores/manaStore';
import { DebugName } from '@/components/game/debug/debug-context'; import { DebugName } from '@/components/game/debug/debug-context';
@@ -32,11 +33,18 @@ export function DisciplineDebugSection() {
useDisciplineStore.setState((s) => { useDisciplineStore.setState((s) => {
const disc = s.disciplines[id]; const disc = s.disciplines[id];
if (!disc) return s; 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 { return {
disciplines: { disciplines: {
...s.disciplines, ...s.disciplines,
[id]: { ...disc, xp: disc.xp + amount }, [id]: { ...disc, xp: disc.xp + amount },
}, },
totalXP: newTotalXP,
concurrentLimit: Math.max(s.concurrentLimit, newLimit),
}; };
}); });
}; };
@@ -167,8 +167,9 @@ function TimeControlSection({ day, hour, paused, onSetDay, onTogglePause }: {
// ─── Quick Actions Section ─────────────────────────────────────────────────── // ─── Quick Actions Section ───────────────────────────────────────────────────
function QuickActionsSection({ onUnlockBase }: { function QuickActionsSection({ onUnlockBase, onAddStarterMaterials }: {
onUnlockBase: () => void; onUnlockBase: () => void;
onAddStarterMaterials: () => void;
}) { }) {
return ( return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
@@ -183,6 +184,9 @@ function QuickActionsSection({ onUnlockBase }: {
<Button size="sm" variant="outline" onClick={onUnlockBase}> <Button size="sm" variant="outline" onClick={onUnlockBase}>
Unlock All Base Elements Unlock All Base Elements
</Button> </Button>
<Button size="sm" variant="outline" onClick={onAddStarterMaterials}>
Add Starter Materials
</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -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 ( return (
<DebugName name="GameStateDebugSection"> <DebugName name="GameStateDebugSection">
<div className="space-y-4"> <div className="space-y-4">
@@ -259,6 +278,7 @@ export function GameStateDebugSection() {
<TimeControlSection day={day} hour={hour} paused={paused} onSetDay={handleSetDay} onTogglePause={togglePause} /> <TimeControlSection day={day} hour={hour} paused={paused} onSetDay={handleSetDay} onTogglePause={togglePause} />
<QuickActionsSection <QuickActionsSection
onUnlockBase={handleUnlockBase} onUnlockBase={handleUnlockBase}
onAddStarterMaterials={handleAddStarterMaterials}
/> />
</div> </div>
</div> </div>
@@ -71,13 +71,16 @@ export function PactDebugSection() {
const guardian = getGuardianForFloor(floor); const guardian = getGuardianForFloor(floor);
if (!guardian) return; 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}!`); addLog(`⚠️ Already signed pact with ${guardian.name}!`);
return; return;
} }
const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0); if (currentSignedPacts.length >= maxPacts) {
if (signedPacts.length >= maxPacts) {
addLog(`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`); addLog(`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`);
return; return;
} }
@@ -111,8 +114,14 @@ export function PactDebugSection() {
}; };
const signAllPacts = () => { const signAllPacts = () => {
const maxPacts = 1 + (prestigeUpgrades?.pactCapacity || 0);
guardianFloors.forEach((floor) => { 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); forcePact(floor);
} }
}); });
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores'; import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects'; import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
@@ -127,7 +127,15 @@ export function SpireCombatPage() {
return base + floorBonus + randomVariation; return base + floorBonus + randomVariation;
}, [currentFloor, seededRandom]); }, [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<string | null>(null);
useEffect(() => { useEffect(() => {
const key = `${currentFloor}:${totalRooms}`;
if (lastGeneratedRef.current === key) return; // already generated
lastGeneratedRef.current = key;
setRoomsCleared(0); setRoomsCleared(0);
const newRoom = generateSpireFloorState(currentFloor, 0, totalRooms); const newRoom = generateSpireFloorState(currentFloor, 0, totalRooms);
setCurrentRoom(newRoom); setCurrentRoom(newRoom);
+1 -1
View File
@@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { useCombatStore, usePrestigeStore } from '@/lib/game/stores'; import { useCombatStore, usePrestigeStore } from '@/lib/game/stores';
import { FLOOR_ELEM_CYCLE } from '@/lib/game/constants'; 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 { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';