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 currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected;
if (currentSelections.length === 2 && upgradeDialogSkill) {
store.commitSkillUpgrades(upgradeDialogSkill, currentSelections);
store.commitSkillUpgrades(upgradeDialogSkill, currentSelections, upgradeDialogMilestone);
}
setPendingUpgradeSelections([]);
setUpgradeDialogSkill(null);

View File

@@ -7,9 +7,10 @@ import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
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 { 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 { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
@@ -204,6 +205,58 @@ export function SpireTab({ store }: SpireTabProps) {
</CardContent>
</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) */}
{store.currentStudyTarget && (
<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;
selectSkillUpgrade: (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;
// Attunement XP and leveling
@@ -1725,13 +1725,32 @@ export const useGameStore = create<GameStore>()(
});
},
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => {
set((state) => ({
skillUpgrades: {
...state.skillUpgrades,
[skillId]: upgradeIds,
},
}));
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone?: 5 | 10) => {
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: {
...state.skillUpgrades,
[skillId]: newUpgrades,
},
};
});
},
tierUpSkill: (skillId: string) => {
@@ -1744,6 +1763,7 @@ export const useGameStore = create<GameStore>()(
const nextTierSkillId = `${baseSkillId}_t${nextTier}`;
const currentLevel = state.skills[skillId] || 0;
const currentUpgrades = state.skillUpgrades?.[skillId] || [];
set({
skillTiers: {
@@ -1757,7 +1777,7 @@ export const useGameStore = create<GameStore>()(
},
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)],
});

View File

@@ -633,3 +633,33 @@ Stage Summary:
- UI for selecting and managing golems
- Documentation updated
- 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