162 lines
5.6 KiB
TypeScript
162 lines
5.6 KiB
TypeScript
// ─── 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 for active spell
|
|
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!`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process equipment spell states (for progress bars in UI)
|
|
const updatedEquipmentSpellStates = [...state.equipmentSpellStates];
|
|
for (let i = 0; i < updatedEquipmentSpellStates.length; i++) {
|
|
const eSpell = updatedEquipmentSpellStates[i];
|
|
const eSpellDef = SPELLS_DEF[eSpell.spellId];
|
|
if (!eSpellDef) continue;
|
|
|
|
// Calculate progress for this equipment spell
|
|
const eSpellCastSpeed = eSpellDef.castSpeed || 1;
|
|
const eProgressPerTick = HOURS_PER_TICK * eSpellCastSpeed * totalAttackSpeed;
|
|
let eCastProgress = (eSpell.castProgress || 0) + eProgressPerTick;
|
|
|
|
// Process complete casts for equipment spells
|
|
while (eCastProgress >= 1 && canAffordSpellCost(eSpellDef.cost, rawMana, elements)) {
|
|
// Deduct cost
|
|
const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements);
|
|
rawMana = eAfterCost.rawMana;
|
|
elements = eAfterCost.elements;
|
|
totalManaGathered += eSpellDef.cost.amount;
|
|
|
|
// Calculate damage
|
|
const eFloorElement = getFloorElement(currentFloor);
|
|
const eDamage = calcDamage(
|
|
{ skills, signedPacts: usePrestigeStore.getState().signedPacts },
|
|
eSpell.spellId,
|
|
eFloorElement,
|
|
);
|
|
|
|
const eResult = onDamageDealt(eDamage);
|
|
rawMana = eResult.rawMana;
|
|
elements = eResult.elements;
|
|
const eFinalDamage = eResult.modifiedDamage || eDamage;
|
|
|
|
floorHP = Math.max(0, floorHP - eFinalDamage);
|
|
eCastProgress -= 1;
|
|
|
|
if (floorHP <= 0) break; // Floor cleared, stop processing
|
|
}
|
|
|
|
// Update equipment spell state
|
|
updatedEquipmentSpellStates[i] = { ...eSpell, castProgress: eCastProgress % 1 };
|
|
}
|
|
|
|
set({
|
|
currentFloor,
|
|
floorHP,
|
|
floorMaxHP: getFloorMaxHP(currentFloor),
|
|
maxFloorReached: Math.max(state.maxFloorReached, currentFloor),
|
|
castProgress,
|
|
equipmentSpellStates: updatedEquipmentSpellStates,
|
|
});
|
|
|
|
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;
|
|
}
|