// ─── Combat Utilities ──────────────────────────────────────────────────────── import type { SpellCost, EquipmentInstance } from '../types'; import type { DisciplineBonuses } from './mana-utils'; import { SPELLS_DEF, ELEMENT_OPPOSITES, INCURSION_START_DAY, MAX_DAY } from '../constants'; import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects'; import { getGuardianForFloor } from '../data/guardian-encounters'; import { STATIC_GUARDIANS as BASE_GUARDIANS } from '../data/guardian-data'; // ─── Damage Calculation Params ────────────────────────────────────────────── export interface DamageCalcParams { signedPacts: number[]; } // ─── Insight Calculation Params ───────────────────────────────────────────── export interface InsightCalcParams { maxFloorReached: number; totalManaGathered: number; signedPacts: number[]; prestigeUpgrades: Record; } // ─── DPS Calculation Params ───────────────────────────────────────────────── export interface DPSCalcParams { signedPacts: number[]; equippedInstances: Record; equipmentInstances: Record; spells: Record; prestigeUpgrades: Record; } // ─── Elemental Damage Bonus ────────────────────────────────────────────────── // Elemental damage bonus: +50% if spell element opposes floor element (super effective) // -25% if spell element matches its own opposite (weak) export function getElementalBonus(spellElem: string, floorElem: string): number { if (spellElem === 'raw') return 1.0; // Raw mana has no elemental bonus if (spellElem === floorElem) return 1.25; // Same element: +25% damage // Check for super effective first: spell is the opposite of floor if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective: +50% damage // Check for weak: spell's opposite matches floor if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak: -25% damage return 1.0; // Neutral } /** * Get the elemental bonus against a multi-element floor/guardian. * Uses the minimum bonus across all elements so multi-element guardians * are not trivially countered (each element can resist different spells). */ export function getMultiElementBonus(spellElem: string, floorElems: string[]): number { if (floorElems.length === 0) return 1.0; if (floorElems.length === 1) return getElementalBonus(spellElem, floorElems[0]); return Math.min(...floorElems.map(e => getElementalBonus(spellElem, e))); } // ─── Boon Bonuses ───────────────────────────────────────────────────────────── export interface BoonBonuses { maxMana: number; manaRegen: number; castingSpeed: number; elementalDamage: number; rawDamage: number; critChance: number; critDamage: number; spellEfficiency: number; manaGain: number; insightGain: number; studySpeed: number; prestigeInsight: number; } // Helper to calculate total boon bonuses from signed pacts export function getBoonBonuses( signedPacts: number[], guardianBoonMultiplier: number = 1.0, ): BoonBonuses { const bonuses: BoonBonuses = { maxMana: 0, manaRegen: 0, castingSpeed: 0, elementalDamage: 0, rawDamage: 0, critChance: 0, critDamage: 0, spellEfficiency: 0, manaGain: 0, insightGain: 0, studySpeed: 0, prestigeInsight: 0, }; for (const floor of signedPacts) { const guardian = getGuardianForFloor(floor); if (!guardian) continue; for (const boon of guardian.boons) { let value = boon.value * guardianBoonMultiplier; switch (boon.type) { case 'maxMana': bonuses.maxMana += value; break; case 'manaRegen': bonuses.manaRegen += value; break; case 'castingSpeed': bonuses.castingSpeed += value; break; case 'elementalDamage': bonuses.elementalDamage += value; break; case 'rawDamage': bonuses.rawDamage += value; break; case 'critChance': bonuses.critChance += value; break; case 'critDamage': bonuses.critDamage += value; break; case 'spellEfficiency': bonuses.spellEfficiency += value; break; case 'manaGain': bonuses.manaGain += value; break; case 'insightGain': bonuses.insightGain += value; break; case 'studySpeed': bonuses.studySpeed += value; break; case 'prestigeInsight': bonuses.prestigeInsight += value; break; } } } return bonuses; } // ─── Damage Calculation ─────────────────────────────────────────────────────── export function calcDamage( state: DamageCalcParams, spellId: string, floorElem?: string, discipline?: DisciplineBonuses, ): number { const sp = SPELLS_DEF[spellId]; if (!sp) return 5; // Base damage: spell base + discipline bonus const discBaseDmg = discipline?.bonuses?.baseDamageBonus || 0; const baseDmg = sp.dmg + discBaseDmg; // Percentage multiplier const discDmgMult = discipline?.bonuses?.baseDamageMultiplier || 0; const pct = 1 + discDmgMult; // Elemental mastery bonus const elemMasteryBonus = 1; // Guardian bane bonus const isGuardianFloor = floorElem && Object.values(BASE_GUARDIANS).some(g => g.element.includes(floorElem)); const guardianBonus = 1; // Get boon bonuses from pacts const boons = getBoonBonuses(state.signedPacts); // Apply raw damage and elemental damage bonuses const rawDamageMult = 1 + boons.rawDamage / 100; const elemDamageMult = 1 + boons.elementalDamage / 100; // Apply crit chance and damage from boons const critChance = boons.critChance / 100; const critDamageMult = 1.5 + boons.critDamage / 100; let damage = baseDmg * pct * elemMasteryBonus * guardianBonus * rawDamageMult * elemDamageMult; // Apply elemental bonus if floor element provided if (floorElem) { damage *= getElementalBonus(sp.elem, floorElem); } // Apply crit if (Math.random() < critChance) { damage *= critDamageMult; } // Final NaN guard — if anything produced NaN, return a safe fallback if (!Number.isFinite(damage)) return 5; return damage; } // ─── Insight Calculation ────────────────────────────────────────────────────── export function calcInsight(state: InsightCalcParams, discipline?: DisciplineBonuses): number { const pu = state.prestigeUpgrades; const discInsightBonus = discipline?.bonuses?.insightGainBonus || 0; const skillBonus = 1 + discInsightBonus; // Get boon bonuses for insight gain const boons = getBoonBonuses(state.signedPacts); const boonInsightMult = 1 + boons.insightGain / 100; const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus * boonInsightMult; // Add prestigeInsight bonus per loop const prestigeInsightBonus = boons.prestigeInsight; return Math.floor( (state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150 + prestigeInsightBonus) * mult ); } // ─── Incursion Strength ─────────────────────────────────────────────────────── export function getIncursionStrength(day: number, hour: number): number { if (day < INCURSION_START_DAY) return 0; const totalHours = (day - INCURSION_START_DAY) * 24 + hour; const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24; return Math.min(0.95, (totalHours / maxHours) * 0.95); } // ─── Spell Cost Helpers ─────────────────────────────────────────────────────── // Check if player can afford spell cost export function canAffordSpellCost( cost: SpellCost, rawMana: number, elements: Record ): boolean { if (cost.type === 'raw') { return rawMana >= cost.amount; } else { const elem = elements[cost.element || '']; return elem && elem.unlocked && elem.current >= cost.amount; } } // Deduct spell cost from appropriate mana pool export function deductSpellCost( cost: SpellCost, rawMana: number, elements: Record ): { rawMana: number; elements: Record } { const newElements = { ...elements }; if (cost.type === 'raw') { const deductedAmount = Math.min(rawMana, cost.amount); return { rawMana: rawMana - deductedAmount, elements: newElements }; } else if (cost.element && newElements[cost.element]) { const elem = newElements[cost.element]; const deductedAmount = Math.min(elem.current, cost.amount); newElements[cost.element] = { ...elem, current: elem.current - deductedAmount }; return { rawMana, elements: newElements }; } return { rawMana, elements: newElements }; } // ─── Melee Damage Calculation (spec §4.3) ──────────────────────────────────── /** * Map from sword enchantment specialId to element type for elemental matchup. */ const SWORD_ENCHANT_ELEMENT: Record = { fireBlade: 'fire', frostBlade: 'frost', lightningBlade: 'lightning', voidBlade: 'void', }; /** * Calculate melee damage for a sword attack (spec §4.3). * * Formula: baseDmg = sword.baseDamage + sword.elementalEnchantDamage * damage = baseDmg × getElementalBonus(sword.enchantElement, enemy.element) * * No crit, no discipline damage bonus for melee in v1. * attackSpeedMult from equipment does apply to meleeProgress accumulation. */ export function calcMeleeDamage( swordInstance: EquipmentInstance, swordType: { stats?: { baseDamage?: number } }, enemyElement: string, ): number { const baseDmg = swordType.stats?.baseDamage || 5; // Determine enchant element from sword's enchantments let enchantElement: string | null = null; for (const ench of swordInstance.enchantments) { const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; if (effectDef?.effect.type === 'special' && effectDef.effect.specialId) { const elem = SWORD_ENCHANT_ELEMENT[effectDef.effect.specialId]; if (elem) { enchantElement = elem; break; } } } // Apply elemental bonus if sword has an elemental enchant if (enchantElement) { return baseDmg * getElementalBonus(enchantElement, enemyElement); } return baseDmg; } // ─── Equipment Spell Helpers ────────────────────────────────────────────────── // Return type for active equipment spells with source equipment export interface ActiveEquipmentSpell { spellId: string; equipmentId: string; } // Get active spells from equipped equipment export function getActiveEquipmentSpells( equippedInstances: Record, equipmentInstances: Record ): ActiveEquipmentSpell[] { const equippedIds = Object.values(equippedInstances || {}).filter((id): id is string => id !== null); const spells: ActiveEquipmentSpell[] = []; for (const id of equippedIds) { const instance = equipmentInstances[id]; if (!instance) continue; for (const ench of instance.enchantments) { const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) { const exists = spells.some(s => s.spellId === effectDef.effect.spellId && s.equipmentId === id); if (!exists) { spells.push({ spellId: effectDef.effect.spellId, equipmentId: id }); } } } } return spells; } // ─── DPS Calculation ────────────────────────────────────────────────────────── // Compute total DPS from all sources (spells, equipment, etc.) export function getTotalDPS( state: DPSCalcParams, upgradeEffects: { spellDamageBonus?: number; attackSpeedMultiplier?: number; [key: string]: unknown }, floorElem?: string, discipline?: DisciplineBonuses, ): number { let totalDPS = 0; // Get active equipment spells const activeSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances); // Calculate DPS for each active spell for (const { spellId } of activeSpells) { const spellDef = SPELLS_DEF[spellId]; if (!spellDef) continue; // Calculate damage per cast const damage = calcDamage(state, spellId, floorElem, discipline); // Get cast speed (spells per second) const baseCastTime = spellDef.baseCastTime || 1.0; const castingSpeedBonus = 1; const equipmentAttackSpeed = upgradeEffects.attackSpeedMultiplier || 1; const castTime = baseCastTime / (castingSpeedBonus * equipmentAttackSpeed); // DPS for this spell const spellDPS = damage / castTime; totalDPS += spellDPS; } // Add equipment DPS bonuses from upgrade effects if (upgradeEffects.spellDamageBonus) { totalDPS *= (1 + upgradeEffects.spellDamageBonus / 100); } return totalDPS; }