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
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m1s
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { SPELLS_DEF, GUARDIANS, HOURS_PER_TICK } from '../constants';
|
||||
import type { GameAction, SpellState } from '../types';
|
||||
import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
|
||||
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType } from '../types';
|
||||
import { getFloorMaxHP } from '../utils';
|
||||
import { usePrestigeStore } from './prestigeStore';
|
||||
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 {
|
||||
// Floor state
|
||||
@@ -24,9 +26,38 @@ export interface CombatState {
|
||||
// Spire mode
|
||||
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: Record<string, SpellState>;
|
||||
|
||||
// Activity Log (for Spire Mode UI)
|
||||
activityLog: ActivityLogEntry[];
|
||||
|
||||
// Achievements
|
||||
achievements: AchievementState;
|
||||
|
||||
// Stats tracking
|
||||
totalSpellsCast: number;
|
||||
totalDamageDealt: number;
|
||||
totalCraftsCompleted: number;
|
||||
|
||||
// Actions
|
||||
setCurrentFloor: (floor: number) => void;
|
||||
advanceFloor: () => void;
|
||||
@@ -37,10 +68,32 @@ export interface CombatState {
|
||||
setSpell: (spellId: string) => 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
|
||||
learnSpell: (spellId: string) => 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
|
||||
enterSpireMode: () => void;
|
||||
|
||||
@@ -75,10 +128,48 @@ export const useCombatStore = create<CombatState>()(
|
||||
currentAction: 'meditate',
|
||||
castProgress: 0,
|
||||
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: {
|
||||
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) => {
|
||||
set({
|
||||
currentFloor: floor,
|
||||
@@ -125,6 +216,66 @@ export const useCombatStore = create<CombatState>()(
|
||||
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: () => {
|
||||
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: (
|
||||
skills: Record<string, number>,
|
||||
rawMana: number,
|
||||
@@ -156,84 +327,17 @@ export const useCombatStore = create<CombatState>()(
|
||||
onFloorCleared: (floor: number, wasGuardian: boolean) => void,
|
||||
onDamageDealt: (damage: number) => { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> },
|
||||
) => {
|
||||
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 };
|
||||
return processCombatTick(
|
||||
get,
|
||||
set,
|
||||
skills,
|
||||
rawMana,
|
||||
elements,
|
||||
maxMana,
|
||||
attackSpeedMult,
|
||||
onFloorCleared,
|
||||
onDamageDealt,
|
||||
);
|
||||
},
|
||||
|
||||
resetCombat: (startFloor: number, spellsToKeep: string[] = []) => {
|
||||
@@ -282,16 +386,6 @@ export const useCombatStore = create<CombatState>()(
|
||||
)
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
// makeInitialSpells is now in combat-actions.ts
|
||||
// Re-export for backward compatibility
|
||||
export { makeInitialSpells } from './combat-actions';
|
||||
|
||||
Reference in New Issue
Block a user