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:
@@ -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),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
@@ -183,6 +184,9 @@ function QuickActionsSection({ onUnlockBase }: {
|
||||
<Button size="sm" variant="outline" onClick={onUnlockBase}>
|
||||
Unlock All Base Elements
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={onAddStarterMaterials}>
|
||||
Add Starter Materials
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</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 (
|
||||
<DebugName name="GameStateDebugSection">
|
||||
<div className="space-y-4">
|
||||
@@ -259,6 +278,7 @@ export function GameStateDebugSection() {
|
||||
<TimeControlSection day={day} hour={hour} paused={paused} onSetDay={handleSetDay} onTogglePause={togglePause} />
|
||||
<QuickActionsSection
|
||||
onUnlockBase={handleUnlockBase}
|
||||
onAddStarterMaterials={handleAddStarterMaterials}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<string | null>(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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user