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
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:
@@ -50,3 +50,4 @@ server.log
|
|||||||
# Skills directory
|
# Skills directory
|
||||||
.desloppify/
|
.desloppify/
|
||||||
test-results/
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|||||||
@@ -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. ✅
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user