Fix upgrade reset bugs: milestone merging and tier-up carryover
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Has been cancelled

- Fixed commitSkillUpgrades() to preserve upgrades from other milestones
  (selecting L10 upgrades no longer resets L5 selections)
- Fixed tierUpSkill() to carry over upgrades from previous tier
  (tier 2 now starts with tier 1's upgrades intact)
- Added summoned golems display to SpireTab with stats and progress bars
This commit is contained in:
Z User
2026-04-02 10:57:03 +00:00
parent d12076502a
commit e2671d7afd
4 changed files with 114 additions and 11 deletions

View File

@@ -96,7 +96,7 @@ export function SkillsTab({ store }: SkillsTabProps) {
const handleConfirm = () => { const handleConfirm = () => {
const currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected; const currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected;
if (currentSelections.length === 2 && upgradeDialogSkill) { if (currentSelections.length === 2 && upgradeDialogSkill) {
store.commitSkillUpgrades(upgradeDialogSkill, currentSelections); store.commitSkillUpgrades(upgradeDialogSkill, currentSelections, upgradeDialogMilestone);
} }
setPendingUpgradeSelections([]); setPendingUpgradeSelections([]);
setUpgradeDialogSkill(null); setUpgradeDialogSkill(null);

View File

@@ -7,9 +7,10 @@ import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { TooltipProvider } from '@/components/ui/tooltip'; import { TooltipProvider } from '@/components/ui/tooltip';
import { Swords, BookOpen, ChevronUp, ChevronDown, RotateCcw, X } from 'lucide-react'; import { Swords, BookOpen, ChevronUp, ChevronDown, RotateCcw, X, Mountain } from 'lucide-react';
import type { GameStore } from '@/lib/game/types'; import type { GameStore } from '@/lib/game/types';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants'; import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants';
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
import { fmt, fmtDec, getFloorElement, canAffordSpellCost } from '@/lib/game/store'; import { fmt, fmtDec, getFloorElement, canAffordSpellCost } from '@/lib/game/store';
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats'; import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting'; import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
@@ -204,6 +205,58 @@ export function SpireTab({ store }: SpireTabProps) {
</CardContent> </CardContent>
</Card> </Card>
{/* Summoned Golems Card */}
{store.golemancy.summonedGolems.length > 0 && (
<Card className="bg-gray-900/80 border-amber-600/50">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
<Mountain className="w-4 h-4" />
Active Golems ({store.golemancy.summonedGolems.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{store.golemancy.summonedGolems.map((summoned) => {
const golemDef = GOLEMS_DEF[summoned.golemId];
if (!golemDef) return null;
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
const damage = getGolemDamage(summoned.golemId, store.skills);
const attackSpeed = getGolemAttackSpeed(summoned.golemId, store.skills);
return (
<div key={summoned.golemId} className="p-2 rounded border border-gray-700 bg-gray-800/30">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<Mountain className="w-4 h-4" style={{ color: elemColor }} />
<span className="text-sm font-semibold game-panel-title" style={{ color: elemColor }}>
{golemDef.name}
</span>
</div>
{golemDef.isAoe && (
<Badge variant="outline" className="text-xs">AOE {golemDef.aoeTargets}</Badge>
)}
</div>
<div className="text-xs text-gray-400 game-mono">
{damage} DMG {attackSpeed.toFixed(1)}/hr
🛡 {Math.floor(golemDef.armorPierce * 100)}% Pierce
</div>
{/* Attack progress bar when climbing */}
{store.currentAction === 'climb' && summoned.attackProgress > 0 && (
<div className="space-y-0.5 mt-1">
<div className="flex justify-between text-xs text-gray-500">
<span>Attack</span>
<span>{Math.min(100, (summoned.attackProgress * 100)).toFixed(0)}%</span>
</div>
<Progress value={Math.min(100, summoned.attackProgress * 100)} className="h-1.5 bg-gray-700" />
</div>
)}
</div>
);
})}
</CardContent>
</Card>
)}
{/* Current Study (if any) */} {/* Current Study (if any) */}
{store.currentStudyTarget && ( {store.currentStudyTarget && (
<Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2"> <Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2">

View File

@@ -746,7 +746,7 @@ interface GameStore extends GameState, CraftingActions {
addLog: (message: string) => void; addLog: (message: string) => void;
selectSkillUpgrade: (skillId: string, upgradeId: string) => void; selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void; deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void; commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone?: 5 | 10) => void;
tierUpSkill: (skillId: string) => void; tierUpSkill: (skillId: string) => void;
// Attunement XP and leveling // Attunement XP and leveling
@@ -1725,13 +1725,32 @@ export const useGameStore = create<GameStore>()(
}); });
}, },
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => { commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone?: 5 | 10) => {
set((state) => ({ set((state) => {
const currentUpgrades = state.skillUpgrades?.[skillId] || [];
// If milestone is specified, merge by keeping other milestone's upgrades
let newUpgrades: string[];
if (milestone === 5) {
// Keep existing L10 upgrades, replace L5 upgrades
const existingL10 = currentUpgrades.filter(id => id.includes('_l10'));
newUpgrades = [...upgradeIds, ...existingL10];
} else if (milestone === 10) {
// Keep existing L5 upgrades, replace L10 upgrades
const existingL5 = currentUpgrades.filter(id => id.includes('_l5'));
newUpgrades = [...existingL5, ...upgradeIds];
} else {
// No milestone specified (legacy behavior), replace all
newUpgrades = upgradeIds;
}
return {
skillUpgrades: { skillUpgrades: {
...state.skillUpgrades, ...state.skillUpgrades,
[skillId]: upgradeIds, [skillId]: newUpgrades,
}, },
})); };
});
}, },
tierUpSkill: (skillId: string) => { tierUpSkill: (skillId: string) => {
@@ -1744,6 +1763,7 @@ export const useGameStore = create<GameStore>()(
const nextTierSkillId = `${baseSkillId}_t${nextTier}`; const nextTierSkillId = `${baseSkillId}_t${nextTier}`;
const currentLevel = state.skills[skillId] || 0; const currentLevel = state.skills[skillId] || 0;
const currentUpgrades = state.skillUpgrades?.[skillId] || [];
set({ set({
skillTiers: { skillTiers: {
@@ -1757,7 +1777,7 @@ export const useGameStore = create<GameStore>()(
}, },
skillUpgrades: { skillUpgrades: {
...state.skillUpgrades, ...state.skillUpgrades,
[nextTierSkillId]: [], // Start fresh with upgrades for new tier [nextTierSkillId]: currentUpgrades, // Carry over upgrades from previous tier
}, },
log: [`🌟 ${SKILLS_DEF[baseSkillId]?.name || baseSkillId} evolved to Tier ${nextTier}!`, ...state.log.slice(0, 49)], log: [`🌟 ${SKILLS_DEF[baseSkillId]?.name || baseSkillId} evolved to Tier ${nextTier}!`, ...state.log.slice(0, 49)],
}); });

View File

@@ -633,3 +633,33 @@ Stage Summary:
- UI for selecting and managing golems - UI for selecting and managing golems
- Documentation updated - Documentation updated
- Lint passes - Lint passes
---
Task ID: 28
Agent: Main
Task: Fix level upgrade reset loop bug and add golem display to SpireTab
Work Log:
- **Fixed upgrade reset bug in commitSkillUpgrades()**:
- Root cause: `commitSkillUpgrades()` was replacing ALL upgrades for a skill instead of merging by milestone
- When selecting level 10 upgrades, it would wipe out level 5 selections (and vice versa)
- Added optional `milestone` parameter (5 | 10) to the function
- When milestone is specified, the function now:
- Keeps existing upgrades from OTHER milestones
- Only replaces upgrades for the CURRENT milestone
- Updated type definition in GameStore interface
- Updated SkillsTab.tsx to pass `upgradeDialogMilestone` when committing
- **Added summoned golems display to SpireTab**:
- Imported GOLEMS_DEF and helper functions
- Added Mountain icon import
- Added "Active Golems" card that appears when golems are summoned
- Shows each golem's name, damage, attack speed, and armor pierce
- Displays attack progress bar when climbing
- AOE badge for golems with area attacks
Stage Summary:
- Level 5 and level 10 upgrades no longer reset each other
- Players can safely select upgrades at both milestones
- Summoned golems now visible in Spire tab during combat
- Lint passes, dev server running