fix: migrate golemancy/activity/achievements, fix CraftingTab/SkillsTab/SpireTab/GolemancyTab/AchievementsTab store reads
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m1s

This commit is contained in:
Refactoring Agent
2026-05-05 16:11:28 +02:00
parent ed616738fd
commit ed69a8f2b4
9 changed files with 352 additions and 140 deletions
+1
View File
@@ -386,6 +386,7 @@ Mana-Loop/
│ │ │ │ ├── store-methods.test.ts │ │ │ │ ├── store-methods.test.ts
│ │ │ │ └── stores.test.ts │ │ │ │ └── stores.test.ts
│ │ │ ├── attunementStore.ts │ │ │ ├── attunementStore.ts
│ │ │ ├── combat-actions.ts
│ │ │ ├── combatStore.ts │ │ │ ├── combatStore.ts
│ │ │ ├── craftingStore.ts │ │ │ ├── craftingStore.ts
│ │ │ ├── gameActions.ts │ │ │ ├── gameActions.ts
@@ -11,15 +11,15 @@ import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recip
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops'; import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types'; import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
import { fmt } from '@/lib/game/stores'; import { fmt } from '@/lib/game/stores';
import { useGameStore, useCraftingStore } from '@/lib/game/stores'; import { useCraftingStore, useCombatStore, useManaStore } from '@/lib/game/stores';
export function EquipmentCrafter() { export function EquipmentCrafter() {
const lootInventory = useCraftingStore((s) => s.lootInventory); const lootInventory = useCraftingStore((s) => s.lootInventory);
const equipmentCraftingProgress = useGameStore((s) => s.equipmentCraftingProgress); const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
const rawMana = useGameStore((s) => s.rawMana); const rawMana = useManaStore((s) => s.rawMana);
const currentAction = useGameStore((s) => s.currentAction); const currentAction = useCombatStore((s) => s.currentAction);
const startCraftingEquipment = useGameStore((s) => s.startCraftingEquipment); const startCraftingEquipment = useCraftingStore((s) => s.startCraftingEquipment);
const cancelEquipmentCrafting = useGameStore((s) => s.cancelEquipmentCrafting); const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
const deleteMaterial = useCraftingStore((s) => s.deleteMaterial); const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
return ( return (
+8 -8
View File
@@ -1,16 +1,16 @@
'use client'; 'use client';
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay'; import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
import { useGameStore } from '@/lib/game/stores'; import { useCombatStore, useManaStore, usePrestigeStore } from '@/lib/game/stores';
export function AchievementsTab() { export function AchievementsTab() {
const achievements = useGameStore((s) => s.achievements); const achievements = useCombatStore((s) => s.achievements);
const maxFloorReached = useGameStore((s) => s.maxFloorReached); const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
const totalManaGathered = useGameStore((s) => s.totalManaGathered); const totalManaGathered = useManaStore((s) => s.totalManaGathered);
const signedPacts = useGameStore((s) => s.signedPacts); const signedPacts = usePrestigeStore((s) => s.signedPacts);
const totalSpellsCast = useGameStore((s) => s.totalSpellsCast); const totalSpellsCast = useCombatStore((s) => s.totalSpellsCast);
const totalDamageDealt = useGameStore((s) => s.totalDamageDealt); const totalDamageDealt = useCombatStore((s) => s.totalDamageDealt);
const totalCraftsCompleted = useGameStore((s) => s.totalCraftsCompleted); const totalCraftsCompleted = useCombatStore((s) => s.totalCraftsCompleted);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
+11 -11
View File
@@ -13,18 +13,18 @@ import {
EnchantmentApplier, EnchantmentApplier,
EquipmentCrafter, EquipmentCrafter,
} from '@/components/game/crafting'; } from '@/components/game/crafting';
import { useGameStore } from '@/lib/game/stores'; import { useCombatStore, useCraftingStore } from '@/lib/game/stores';
import { useGameToast } from '@/components/game/GameToast'; import { useGameToast } from '@/components/game/GameToast';
export function CraftingTab() { export function CraftingTab() {
const showToast = useGameToast(); const showToast = useGameToast();
const currentAction = useGameStore((s) => s.currentAction); const currentAction = useCombatStore((s) => s.currentAction);
const designProgress = useGameStore((s) => s.designProgress); const designProgress = useCraftingStore((s) => s.designProgress);
const preparationProgress = useGameStore((s) => s.preparationProgress); const preparationProgress = useCraftingStore((s) => s.preparationProgress);
const applicationProgress = useGameStore((s) => s.applicationProgress); const applicationProgress = useCraftingStore((s) => s.applicationProgress);
const equipmentCraftingProgress = useGameStore((s) => s.equipmentCraftingProgress); const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
const pauseApplication = useGameStore((s) => s.pauseApplication); const pauseApplication = useCraftingStore((s) => s.pauseApplication);
const resumeApplication = useGameStore((s) => s.resumeApplication); const resumeApplication = useCraftingStore((s) => s.resumeApplication);
const [activeTab, setActiveTab] = useState<'fabricate' | 'enchant'>('fabricate'); const [activeTab, setActiveTab] = useState<'fabricate' | 'enchant'>('fabricate');
const [enchantStage, setEnchantStage] = useState<'design' | 'prepare' | 'apply'>('design'); const [enchantStage, setEnchantStage] = useState<'design' | 'prepare' | 'apply'>('design');
@@ -176,7 +176,7 @@ export function CraftingTab() {
<SectionHeader <SectionHeader
title="Designing Enchantment" title="Designing Enchantment"
action={ action={
<ActionButton variant="ghost" size="sm" onClick={() => useGameStore.getState().cancelDesign()}> <ActionButton variant="ghost" size="sm" onClick={() => useCraftingStore.getState().cancelDesign()}>
Cancel Cancel
</ActionButton> </ActionButton>
} }
@@ -198,7 +198,7 @@ export function CraftingTab() {
<SectionHeader <SectionHeader
title="Preparing Equipment" title="Preparing Equipment"
action={ action={
<ActionButton variant="ghost" size="sm" onClick={() => useGameStore.getState().cancelPreparation()}> <ActionButton variant="ghost" size="sm" onClick={() => useCraftingStore.getState().cancelPreparation()}>
Cancel Cancel
</ActionButton> </ActionButton>
} }
@@ -230,7 +230,7 @@ export function CraftingTab() {
<> <>
<ActionButton variant="secondary" size="sm" onClick={pauseApplication}>Pause</ActionButton> <ActionButton variant="secondary" size="sm" onClick={pauseApplication}>Pause</ActionButton>
<ActionButton variant="ghost" size="sm" onClick={() => { <ActionButton variant="ghost" size="sm" onClick={() => {
useGameStore.getState().cancelApplication(); useCraftingStore.getState().cancelApplication();
showToast('warning', 'Enchantment Cancelled', 'The enchantment application was cancelled.'); showToast('warning', 'Enchantment Cancelled', 'The enchantment application was cancelled.');
}}>Cancel</ActionButton> }}>Cancel</ActionButton>
</> </>
+4 -4
View File
@@ -9,16 +9,16 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration } from '@/lib/game/data/golems'; import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration } from '@/lib/game/data/golems';
import { ELEMENTS } from '@/lib/game/constants'; import { ELEMENTS } from '@/lib/game/constants';
import { useGameStore, useManaStore, useSkillStore, useCombatStore, useAttunementStore } from '@/lib/game/stores'; import { useManaStore, useSkillStore, useCombatStore, useAttunementStore } from '@/lib/game/stores';
export function GolemancyTab() { export function GolemancyTab() {
const attunements = useAttunementStore((s) => s.attunements); const attunements = useAttunementStore((s) => s.attunements);
const elements = useManaStore((s) => s.elements); const elements = useManaStore((s) => s.elements);
const skills = useSkillStore((s) => s.skills); const skills = useSkillStore((s) => s.skills);
const golemancy = useGameStore((s) => s.golemancy); const golemancy = useCombatStore((s) => s.golemancy);
const currentFloor = useCombatStore((s) => s.currentFloor); const currentFloor = useCombatStore((s) => s.currentFloor);
const currentRoom = useGameStore((s) => s.currentRoom); const currentRoom = useCombatStore((s) => s.currentRoom);
const toggleGolem = useGameStore((s) => s.toggleGolem); const toggleGolem = useCombatStore((s) => s.toggleGolem);
const rawMana = useManaStore((s) => s.rawMana); const rawMana = useManaStore((s) => s.rawMana);
// Get Fabricator level and golem slots // Get Fabricator level and golem slots
+4 -4
View File
@@ -39,7 +39,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { SkillRow } from './SkillRow'; import { SkillRow } from './SkillRow';
import { useSkillUpgradeSelection } from '@/lib/game/hooks/useSkillUpgradeSelection'; import { useSkillUpgradeSelection } from '@/lib/game/hooks/useSkillUpgradeSelection';
import { CategorySkillsList } from './CategorySkillsList'; import { CategorySkillsList } from './CategorySkillsList';
import { useGameStore, useSkillStore, usePrestigeStore } from '@/lib/game/stores'; import { useSkillStore, usePrestigeStore } from '@/lib/game/stores';
export function SkillsTab() { export function SkillsTab() {
const showToast = useGameToast(); const showToast = useGameToast();
@@ -55,11 +55,11 @@ export function SkillsTab() {
const skillUpgrades = useSkillStore((s) => s.skillUpgrades); const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const skillTiers = useSkillStore((s) => s.skillTiers); const skillTiers = useSkillStore((s) => s.skillTiers);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const currentStudyTarget = useGameStore((s) => s.currentStudyTarget); const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
const parallelStudyTarget = useGameStore((s) => s.parallelStudyTarget); const parallelStudyTarget = useSkillStore((s) => s.parallelStudyTarget);
const startStudyingSkill = useSkillStore((s) => s.startStudyingSkill); const startStudyingSkill = useSkillStore((s) => s.startStudyingSkill);
const startParallelStudySkill = useSkillStore((s) => s.startParallelStudySkill); const startParallelStudySkill = useSkillStore((s) => s.startParallelStudySkill);
const cancelStudy = useGameStore((s) => s.cancelStudy); const cancelStudy = useSkillStore((s) => s.cancelStudy);
const commitSkillUpgrades = useSkillStore((s) => s.commitSkillUpgrades); const commitSkillUpgrades = useSkillStore((s) => s.commitSkillUpgrades);
const tierUpSkill = useSkillStore((s) => s.tierUpSkill); const tierUpSkill = useSkillStore((s) => s.tierUpSkill);
+13 -13
View File
@@ -12,7 +12,7 @@ import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats
import { getUnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting'; import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
import { canAffordSpellCost, getFloorElement } from '@/lib/game/stores'; import { canAffordSpellCost, getFloorElement } from '@/lib/game/stores';
import { useGameStore, useManaStore, useSkillStore, useCombatStore, usePrestigeStore, useCraftingStore } from '@/lib/game/stores'; import { useManaStore, useSkillStore, useCombatStore, usePrestigeStore, useCraftingStore } from '@/lib/game/stores';
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems'; import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
// Extracted components // Extracted components
@@ -52,16 +52,16 @@ export function SpireTab({ simpleMode = false }: SpireTabProps) {
const activeSpell = useCombatStore((s) => s.activeSpell); const activeSpell = useCombatStore((s) => s.activeSpell);
const startClimbUp = useCombatStore((s) => s.startClimbUp); const startClimbUp = useCombatStore((s) => s.startClimbUp);
const startClimbDown = useCombatStore((s) => s.startClimbDown); const startClimbDown = useCombatStore((s) => s.startClimbDown);
const enterSpireMode = useGameStore((s) => s.enterSpireMode); const enterSpireMode = useCombatStore((s) => s.enterSpireMode);
const spireMode = useGameStore((s) => s.spireMode); const spireMode = useCombatStore((s) => s.spireMode);
const climbDirection = useGameStore((s) => s.climbDirection) || 'up'; const climbDirection = useCombatStore((s) => s.climbDirection) || 'up';
const clearedFloors = useGameStore((s) => s.clearedFloors || {}); const clearedFloors = useCombatStore((s) => s.clearedFloors || {});
const currentRoom = useGameStore((s) => s.currentRoom); const currentRoom = useCombatStore((s) => s.currentRoom);
const equipmentSpellStates = useGameStore((s) => s.equipmentSpellStates); const equipmentSpellStates = useCombatStore((s) => s.equipmentSpellStates);
const golemancy = useGameStore((s) => s.golemancy); const golemancy = useCombatStore((s) => s.golemancy);
const activityLog = useGameStore((s) => s.activityLog); const activityLog = useCombatStore((s) => s.activityLog);
const currentStudyTarget = useGameStore((s) => s.currentStudyTarget); const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
const parallelStudyTarget = useGameStore((s) => s.parallelStudyTarget); const parallelStudyTarget = useSkillStore((s) => s.parallelStudyTarget);
const signedPacts = usePrestigeStore((s) => s.signedPacts); const signedPacts = usePrestigeStore((s) => s.signedPacts);
const skills = useSkillStore((s) => s.skills); const skills = useSkillStore((s) => s.skills);
const skillUpgrades = useSkillStore((s) => s.skillUpgrades); const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
@@ -73,8 +73,8 @@ export function SpireTab({ simpleMode = false }: SpireTabProps) {
const designProgress = useCraftingStore((s) => s.designProgress); const designProgress = useCraftingStore((s) => s.designProgress);
const designProgress2 = useCraftingStore((s) => s.designProgress2); const designProgress2 = useCraftingStore((s) => s.designProgress2);
const preparationProgress = useCraftingStore((s) => s.preparationProgress); const preparationProgress = useCraftingStore((s) => s.preparationProgress);
const applicationProgress = useGameStore((s) => s.applicationProgress); const applicationProgress = useCraftingStore((s) => s.applicationProgress);
const equipmentCraftingProgress = useGameStore((s) => s.equipmentCraftingProgress); const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
// Derived data // Derived data
const floorElem = getFloorElement(currentFloor); const floorElem = getFloorElement(currentFloor);
+117
View File
@@ -0,0 +1,117 @@
// ─── Combat Actions ─────────────────────────────────────────────────────────────
// Extracted combat logic from combatStore.ts
import { SPELLS_DEF, GUARDIANS, HOURS_PER_TICK } from '../constants';
import type { CombatState } from './combatStore';
import type { SpellState } from '../types';
import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
import { usePrestigeStore } from './prestigeStore';
export function processCombatTick(
get: () => CombatState,
set: (state: Partial<CombatState>) => void,
skills: Record<string, number>,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
maxMana: number,
attackSpeedMult: number,
onFloorCleared: (floor: number, wasGuardian: boolean) => void,
onDamageDealt: (damage: number) => {
rawMana: number;
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
modifiedDamage?: number;
},
) {
const state = get();
const logMessages: string[] = [];
let totalManaGathered = 0;
if (state.currentAction !== 'climb') {
return { rawMana, elements, logMessages, totalManaGathered };
}
const spellId = state.activeSpell;
const spellDef = SPELLS_DEF[spellId];
if (!spellDef) {
return { rawMana, elements, logMessages, totalManaGathered };
}
// Calculate cast speed
const baseAttackSpeed = 1 + (skills.quickCast || 0) * 0.05;
const totalAttackSpeed = baseAttackSpeed * attackSpeedMult;
const spellCastSpeed = spellDef.castSpeed || 1;
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
let castProgress = (state.castProgress || 0) + progressPerTick;
let floorHP = state.floorHP;
let currentFloor = state.currentFloor;
let floorMaxHP = state.floorMaxHP;
// Process complete casts
while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) {
// Deduct spell cost
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
rawMana = afterCost.rawMana;
elements = afterCost.elements;
totalManaGathered += spellDef.cost.amount;
// Calculate base damage
const floorElement = getFloorElement(currentFloor);
const damage = calcDamage(
{ skills, signedPacts: usePrestigeStore.getState().signedPacts },
spellId,
floorElement,
);
// Let gameStore apply damage modifiers (executioner, berserker, spell echo)
const result = onDamageDealt(damage);
rawMana = result.rawMana;
elements = result.elements;
const finalDamage = result.modifiedDamage || damage;
// Apply damage
floorHP = Math.max(0, floorHP - finalDamage);
castProgress -= 1;
// Check if floor is cleared
if (floorHP <= 0) {
const wasGuardian = GUARDIANS[currentFloor];
onFloorCleared(currentFloor, !!wasGuardian);
currentFloor = Math.min(currentFloor + 1, 100);
floorMaxHP = getFloorMaxHP(currentFloor);
floorHP = floorMaxHP;
castProgress = 0;
if (wasGuardian) {
logMessages.push(`⚔️ ${wasGuardian.name} defeated!`);
} else if (currentFloor % 5 === 0) {
logMessages.push(`🏰 Floor ${currentFloor - 1} cleared!`);
}
}
}
set({
currentFloor,
floorHP,
floorMaxHP: getFloorMaxHP(currentFloor),
maxFloorReached: Math.max(state.maxFloorReached, currentFloor),
castProgress,
});
return { rawMana, elements, logMessages, totalManaGathered };
}
// Helper function to create initial spells
export function makeInitialSpells(spellsToKeep: string[] = []): Record<string, SpellState> {
const startSpells: Record<string, SpellState> = {
manaBolt: { learned: true, level: 1, studyProgress: 0 },
};
// Add kept spells
for (const spellId of spellsToKeep) {
startSpells[spellId] = { learned: true, level: 1, studyProgress: 0 };
}
return startSpells;
}
+187 -93
View File
@@ -3,11 +3,13 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { SPELLS_DEF, GUARDIANS, HOURS_PER_TICK } from '../constants'; import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType } from '../types';
import type { GameAction, SpellState } from '../types'; import { getFloorMaxHP } from '../utils';
import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
import { usePrestigeStore } from './prestigeStore'; import { usePrestigeStore } from './prestigeStore';
import { useGameStore } from './gameStore'; import { useGameStore } from './gameStore';
import { generateFloorState } from '../store-modules/room-utils';
import { addActivityLogEntry } from '../store-modules/activity-log';
import { processCombatTick, makeInitialSpells } from './combat-actions';
export interface CombatState { export interface CombatState {
// Floor state // Floor state
@@ -24,9 +26,38 @@ export interface CombatState {
// Spire mode // Spire mode
spireMode: boolean; spireMode: boolean;
// Room system for special floors
currentRoom: FloorState;
// Spire climbing state
clearedFloors: Record<number, boolean>;
climbDirection: 'up' | 'down' | null;
isDescending: boolean;
// Golemancy (summoned golems)
golemancy: GolemancyState;
// Equipment spell states for multi-casting
equipmentSpellStates: EquipmentSpellState[];
// Combat special effect tracking
comboHitCount: number;
floorHitCount: number;
// Spells // Spells
spells: Record<string, SpellState>; spells: Record<string, SpellState>;
// Activity Log (for Spire Mode UI)
activityLog: ActivityLogEntry[];
// Achievements
achievements: AchievementState;
// Stats tracking
totalSpellsCast: number;
totalDamageDealt: number;
totalCraftsCompleted: number;
// Actions // Actions
setCurrentFloor: (floor: number) => void; setCurrentFloor: (floor: number) => void;
advanceFloor: () => void; advanceFloor: () => void;
@@ -37,10 +68,32 @@ export interface CombatState {
setSpell: (spellId: string) => void; setSpell: (spellId: string) => void;
setCastProgress: (progress: number) => void; setCastProgress: (progress: number) => void;
// Room state
setCurrentRoom: (room: FloorState) => void;
// Spire climbing
setClimbDirection: (direction: 'up' | 'down' | null) => void;
setClearedFloor: (floor: number, cleared: boolean) => void;
setIsDescending: (descending: boolean) => void;
climbDownFloor: () => void;
exitSpireMode: () => void;
// Golemancy
toggleGolem: (golemId: string) => void;
setEnabledGolems: (golemIds: string[]) => void;
// Spells // Spells
learnSpell: (spellId: string) => void; learnSpell: (spellId: string) => void;
setSpellState: (spellId: string, state: Partial<SpellState>) => void; setSpellState: (spellId: string, state: Partial<SpellState>) => void;
// Activity Log
addActivityLog: (eventType: ActivityEventType, message: string, details?: ActivityLogEntry['details']) => void;
// Stats
incrementSpellsCast: () => void;
addDamageDealt: (damage: number) => void;
incrementCraftsCompleted: () => void;
// Spire mode // Spire mode
enterSpireMode: () => void; enterSpireMode: () => void;
@@ -75,10 +128,48 @@ export const useCombatStore = create<CombatState>()(
currentAction: 'meditate', currentAction: 'meditate',
castProgress: 0, castProgress: 0,
spireMode: false, spireMode: false,
// Room system
currentRoom: generateFloorState(1),
// Spire climbing state
clearedFloors: {},
climbDirection: null,
isDescending: false,
// Golemancy
golemancy: {
enabledGolems: [],
summonedGolems: [],
lastSummonFloor: 0,
},
// Equipment spell states
equipmentSpellStates: [],
// Combat tracking
comboHitCount: 0,
floorHitCount: 0,
// Spells
spells: { spells: {
manaBolt: { learned: true, level: 1, studyProgress: 0 }, manaBolt: { learned: true, level: 1, studyProgress: 0 },
}, },
// Activity Log
activityLog: [],
// Achievements
achievements: {
unlocked: [],
progress: {},
},
// Stats tracking
totalSpellsCast: 0,
totalDamageDealt: 0,
totalCraftsCompleted: 0,
setCurrentFloor: (floor: number) => { setCurrentFloor: (floor: number) => {
set({ set({
currentFloor: floor, currentFloor: floor,
@@ -125,6 +216,66 @@ export const useCombatStore = create<CombatState>()(
set({ castProgress: progress }); set({ castProgress: progress });
}, },
// Room state
setCurrentRoom: (room: FloorState) => {
set({ currentRoom: room });
},
// Spire climbing
setClimbDirection: (direction: 'up' | 'down' | null) => {
set({ climbDirection: direction });
},
setClearedFloor: (floor: number, cleared: boolean) => {
set((state) => ({
clearedFloors: { ...state.clearedFloors, [floor]: cleared },
}));
},
setIsDescending: (descending: boolean) => {
set({ isDescending: descending });
},
climbDownFloor: () => {
set((s) => {
if (s.currentFloor <= 1) return s;
const newFloor = s.currentFloor - 1;
return {
currentFloor: newFloor,
currentRoom: generateFloorState(newFloor),
floorMaxHP: getFloorMaxHP(newFloor),
floorHP: getFloorMaxHP(newFloor),
castProgress: 0,
};
});
},
exitSpireMode: () => {
set({ spireMode: false, climbDirection: null, isDescending: false });
},
// Golemancy
toggleGolem: (golemId: string) => {
set((s) => {
const enabledGolems = s.golemancy?.enabledGolems || [];
const isEnabled = enabledGolems.includes(golemId);
return {
golemancy: {
...s.golemancy,
enabledGolems: isEnabled
? enabledGolems.filter(id => id !== golemId)
: [...enabledGolems, golemId]
},
};
});
},
setEnabledGolems: (golemIds: string[]) => {
set((s) => ({
golemancy: { ...s.golemancy, enabledGolems: golemIds },
}));
},
enterSpireMode: () => { enterSpireMode: () => {
set({ spireMode: true }); set({ spireMode: true });
}, },
@@ -147,6 +298,26 @@ export const useCombatStore = create<CombatState>()(
})); }));
}, },
// Activity Log
addActivityLog: (eventType: ActivityEventType, message: string, details?: ActivityLogEntry['details']) => {
set((state) => ({
activityLog: addActivityLogEntry(state, eventType, message, details),
}));
},
// Stats
incrementSpellsCast: () => {
set((state) => ({ totalSpellsCast: state.totalSpellsCast + 1 }));
},
addDamageDealt: (damage: number) => {
set((state) => ({ totalDamageDealt: state.totalDamageDealt + damage }));
},
incrementCraftsCompleted: () => {
set((state) => ({ totalCraftsCompleted: state.totalCraftsCompleted + 1 }));
},
processCombatTick: ( processCombatTick: (
skills: Record<string, number>, skills: Record<string, number>,
rawMana: number, rawMana: number,
@@ -156,84 +327,17 @@ export const useCombatStore = create<CombatState>()(
onFloorCleared: (floor: number, wasGuardian: boolean) => void, onFloorCleared: (floor: number, wasGuardian: boolean) => void,
onDamageDealt: (damage: number) => { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> }, onDamageDealt: (damage: number) => { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> },
) => { ) => {
const state = get(); return processCombatTick(
const logMessages: string[] = []; get,
let totalManaGathered = 0; set,
skills,
if (state.currentAction !== 'climb') { rawMana,
return { rawMana, elements, logMessages, totalManaGathered }; elements,
} maxMana,
attackSpeedMult,
const spellId = state.activeSpell; onFloorCleared,
const spellDef = SPELLS_DEF[spellId]; onDamageDealt,
if (!spellDef) {
return { rawMana, elements, logMessages, totalManaGathered };
}
// Calculate cast speed
const baseAttackSpeed = 1 + (skills.quickCast || 0) * 0.05;
const totalAttackSpeed = baseAttackSpeed * attackSpeedMult;
const spellCastSpeed = spellDef.castSpeed || 1;
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
let castProgress = (state.castProgress || 0) + progressPerTick;
let floorHP = state.floorHP;
let currentFloor = state.currentFloor;
let floorMaxHP = state.floorMaxHP;
// Process complete casts
while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) {
// Deduct spell cost
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
rawMana = afterCost.rawMana;
elements = afterCost.elements;
totalManaGathered += spellDef.cost.amount;
// Calculate base damage
const floorElement = getFloorElement(currentFloor);
const damage = calcDamage(
{ skills, signedPacts: usePrestigeStore.getState().signedPacts },
spellId,
floorElement
); );
// Let gameStore apply damage modifiers (executioner, berserker, spell echo)
const result = onDamageDealt(damage);
rawMana = result.rawMana;
elements = result.elements;
const finalDamage = result.modifiedDamage || damage;
// Apply damage
floorHP = Math.max(0, floorHP - finalDamage);
castProgress -= 1;
// Check if floor is cleared
if (floorHP <= 0) {
const wasGuardian = GUARDIANS[currentFloor];
onFloorCleared(currentFloor, !!wasGuardian);
currentFloor = Math.min(currentFloor + 1, 100);
floorMaxHP = getFloorMaxHP(currentFloor);
floorHP = floorMaxHP;
castProgress = 0;
if (wasGuardian) {
logMessages.push(`⚔️ ${wasGuardian.name} defeated!`);
} else if (currentFloor % 5 === 0) {
logMessages.push(`🏰 Floor ${currentFloor - 1} cleared!`);
}
}
}
set({
currentFloor,
floorHP,
floorMaxHP: getFloorMaxHP(currentFloor),
maxFloorReached: Math.max(state.maxFloorReached, currentFloor),
castProgress,
});
return { rawMana, elements, logMessages, totalManaGathered };
}, },
resetCombat: (startFloor: number, spellsToKeep: string[] = []) => { resetCombat: (startFloor: number, spellsToKeep: string[] = []) => {
@@ -282,16 +386,6 @@ export const useCombatStore = create<CombatState>()(
) )
); );
// Helper function to create initial spells // makeInitialSpells is now in combat-actions.ts
export function makeInitialSpells(spellsToKeep: string[] = []): Record<string, SpellState> { // Re-export for backward compatibility
const startSpells: Record<string, SpellState> = { export { makeInitialSpells } from './combat-actions';
manaBolt: { learned: true, level: 1, studyProgress: 0 },
};
// Add kept spells
for (const spellId of spellsToKeep) {
startSpells[spellId] = { learned: true, level: 1, studyProgress: 0 };
}
return startSpells;
}