Phase 3: Split utils.ts by responsibility
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m49s

This commit is contained in:
Refactoring Agent
2026-04-24 13:45:12 +02:00
parent b3291c3b5e
commit 23d0a129c1
6 changed files with 227 additions and 684 deletions
+163 -199
View File
@@ -1,81 +1,10 @@
// ─── Combat Utility Functions ─────────────────────────────────────────────────
// ─── Combat Utilities ────────────────────────────────────────────────────────
import type { GameState, SpellCost, EquipmentInstance } from '../types';
import {
GUARDIANS,
SPELLS_DEF,
FLOOR_ELEM_CYCLE,
HOURS_PER_TICK,
MAX_DAY,
INCURSION_START_DAY,
ELEMENT_OPPOSITES,
ELEMENTS,
TICK_MS,
} from '../constants';
import { EQUIPMENT_TYPES } from '../data/equipment';
import { GUARDIANS, SPELLS_DEF, ELEMENT_OPPOSITES, INCURSION_START_DAY, MAX_DAY } from '../constants';
import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects';
// ─── Equipment Spell Helper ─────────────────────────────────────────────────────
// Get all spells from equipped caster weapons (staves, wands, etc.)
// Returns array of { spellId, equipmentInstanceId }
export function getActiveEquipmentSpells(
equippedInstances: Record<string, string | null>,
equipmentInstances: Record<string, EquipmentInstance>
): Array<{ spellId: string; equipmentId: string }> {
const spells: Array<{ spellId: string; equipmentId: string }> = [];
// Check main hand and off hand for caster equipment
const weaponSlots = ['mainHand', 'offHand'] as const;
for (const slot of weaponSlots) {
const instanceId = equippedInstances[slot];
if (!instanceId) continue;
const instance = equipmentInstances[instanceId];
if (!instance) continue;
// Check if this is a caster-type equipment
const equipType = EQUIPMENT_TYPES[instance.typeId];
if (!equipType || equipType.category !== 'caster') continue;
// Get spells from enchantments
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
spells.push({
spellId: effectDef.effect.spellId,
equipmentId: instanceId,
});
}
}
}
return spells;
}
// ─── Skill Level Helper ─────────────────────────────────────────────────────────
// Helper to get effective skill level accounting for tiers
export function getEffectiveSkillLevel(
skills: Record<string, number>,
baseSkillId: string,
skillTiers: Record<string, number> = {}
): { level: number; tier: number; tierMultiplier: number } {
// Find the highest tier the player has for this base skill
const currentTier = skillTiers[baseSkillId] || 1;
// Look for the tiered skill ID (e.g., manaFlow_t2)
const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
// Tier multiplier: each tier is 10x more powerful
const tierMultiplier = Math.pow(10, currentTier - 1);
return { level, tier: currentTier, tierMultiplier };
}
// ─── Elemental Damage Bonus ─────────────────────────────────────────────────────
// ─── Elemental Damage Bonus ──────────────────────────────────────────────────
// Elemental damage bonus: +50% if spell element opposes floor element (super effective)
// -25% if spell element matches its own opposite (weak)
@@ -95,7 +24,88 @@ export function getElementalBonus(spellElem: string, floorElem: string): number
return 1.0; // Neutral
}
// ─── Damage Calculation ─────────────────────────────────────────────────────────
// ─── Boon Bonuses ─────────────────────────────────────────────────────────────
// Helper to calculate total boon bonuses from signed pacts
export function getBoonBonuses(signedPacts: number[]): {
maxMana: number;
manaRegen: number;
castingSpeed: number;
elementalDamage: number;
rawDamage: number;
critChance: number;
critDamage: number;
spellEfficiency: number;
manaGain: number;
insightGain: number;
studySpeed: number;
prestigeInsight: number;
} {
const bonuses = {
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 = GUARDIANS[floor];
if (!guardian) continue;
for (const boon of guardian.boons) {
switch (boon.type) {
case 'maxMana':
bonuses.maxMana += boon.value;
break;
case 'manaRegen':
bonuses.manaRegen += boon.value;
break;
case 'castingSpeed':
bonuses.castingSpeed += boon.value;
break;
case 'elementalDamage':
bonuses.elementalDamage += boon.value;
break;
case 'rawDamage':
bonuses.rawDamage += boon.value;
break;
case 'critChance':
bonuses.critChance += boon.value;
break;
case 'critDamage':
bonuses.critDamage += boon.value;
break;
case 'spellEfficiency':
bonuses.spellEfficiency += boon.value;
break;
case 'manaGain':
bonuses.manaGain += boon.value;
break;
case 'insightGain':
bonuses.insightGain += boon.value;
break;
case 'studySpeed':
bonuses.studySpeed += boon.value;
break;
case 'prestigeInsight':
bonuses.prestigeInsight += boon.value;
break;
}
}
}
return bonuses;
}
// ─── Damage Calculation ───────────────────────────────────────────────────────
export function calcDamage(
state: Pick<GameState, 'skills' | 'signedPacts'>,
@@ -111,18 +121,24 @@ export function calcDamage(
// Elemental mastery bonus
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
// Guardian bane bonus
const guardianBonus = floorElem && GUARDIANS[Object.values(GUARDIANS).find(g => g.element === floorElem)?.hp ? 0 : 0]
// Guardian bane bonus - check if current floor has a guardian with matching element
const isGuardianFloor = floorElem && Object.values(GUARDIANS).some(g => g.element === floorElem);
const guardianBonus = isGuardianFloor
? 1 + (skills.guardianBane || 0) * 0.2
: 1;
const critChance = (skills.precision || 0) * 0.05;
const pactMult = state.signedPacts.reduce(
(m, f) => m * (GUARDIANS[f]?.pact || 1),
1
);
// Get boon bonuses from pacts
const boons = getBoonBonuses(state.signedPacts);
let damage = baseDmg * pct * pactMult * elemMasteryBonus;
// 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 = (skills.precision || 0) * 0.05 + 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) {
@@ -131,58 +147,33 @@ export function calcDamage(
// Apply crit
if (Math.random() < critChance) {
damage *= 1.5;
damage *= critDamageMult;
}
return damage;
}
// ─── Insight Calculation ────────────────────────────────────────────────────────
// ─── Insight Calculation ──────────────────────────────────────────────────────
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
const pu = state.prestigeUpgrades;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1;
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus;
// 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) * mult
(state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150 + prestigeInsightBonus) * mult
);
}
// ─── Meditation Bonus ───────────────────────────────────────────────────────────
// Meditation bonus now affects regen rate directly
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number {
const hasMeditation = skills.meditation === 1;
const hasDeepTrance = skills.deepTrance === 1;
const hasVoidMeditation = skills.voidMeditation === 1;
const hours = meditateTicks * HOURS_PER_TICK;
// Base meditation: ramps up over 4 hours to 1.5x
let bonus = 1 + Math.min(hours / 4, 0.5);
// With Meditation Focus: up to 2.5x after 4 hours
if (hasMeditation && hours >= 4) {
bonus = 2.5;
}
// With Deep Trance: up to 3.0x after 6 hours
if (hasDeepTrance && hours >= 6) {
bonus = 3.0;
}
// With Void Meditation: up to 5.0x after 8 hours
if (hasVoidMeditation && hours >= 8) {
bonus = 5.0;
}
// Apply meditation efficiency from upgrades (Deep Wellspring, etc.)
bonus *= meditationEfficiency;
return bonus;
}
// ─── Incursion Strength ─────────────────────────────────────────────────────────
// ─── Incursion Strength ───────────────────────────────────────────────────────
export function getIncursionStrength(day: number, hour: number): number {
if (day < INCURSION_START_DAY) return 0;
@@ -191,7 +182,7 @@ export function getIncursionStrength(day: number, hour: number): number {
return Math.min(0.95, (totalHours / maxHours) * 0.95);
}
// ─── Spell Cost Helpers ─────────────────────────────────────────────────────────
// ─── Spell Cost Helpers ───────────────────────────────────────────────────────
// Check if player can afford spell cost
export function canAffordSpellCost(
@@ -216,12 +207,11 @@ export function deductSpellCost(
const newElements = { ...elements };
if (cost.type === 'raw') {
// Clamp to 0 to prevent negative mana
return { rawMana: Math.max(0, rawMana - cost.amount), elements: newElements };
return { rawMana: rawMana - cost.amount, elements: newElements };
} else if (cost.element && newElements[cost.element]) {
newElements[cost.element] = {
...newElements[cost.element],
current: Math.max(0, newElements[cost.element].current - cost.amount)
current: newElements[cost.element].current - cost.amount
};
return { rawMana, elements: newElements };
}
@@ -229,93 +219,67 @@ export function deductSpellCost(
return { rawMana, elements: newElements };
}
// ─── Damage Breakdown Helper ──────────────────────────────────────────────────
// ─── Equipment Spell Helpers ──────────────────────────────────────────────────
export interface DamageBreakdown {
base: number;
combatTrainBonus: number;
arcaneFuryMult: number;
elemMasteryMult: number;
guardianBaneMult: number;
pactMult: number;
precisionChance: number;
elemBonus: number;
elemBonusText: string;
total: number;
}
export function getDamageBreakdown(
state: Pick<GameState, 'skills' | 'signedPacts'>,
activeSpellId: string,
floorElem: string,
isGuardianFloor: boolean
): DamageBreakdown | null {
const spell = SPELLS_DEF[activeSpellId];
if (!spell) return null;
// Get active spells from equipped equipment
export function getActiveEquipmentSpells(
equippedInstances: Record<string, string | null>,
equipmentInstances: Record<string, EquipmentInstance>
): string[] {
const equippedIds = Object.values(equippedInstances).filter((id): id is string => id !== null);
const spells: string[] = [];
const baseDmg = spell.dmg;
const combatTrainBonus = (state.skills.combatTrain || 0) * 5;
const arcaneFuryMult = 1 + (state.skills.arcaneFury || 0) * 0.1;
const elemMasteryMult = 1 + (state.skills.elementalMastery || 0) * 0.15;
const guardianBaneMult = isGuardianFloor ? (1 + (state.skills.guardianBane || 0) * 0.2) : 1;
const pactMult = state.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1);
const precisionChance = (state.skills.precision || 0) * 0.05;
// Elemental bonus
let elemBonus = 1.0;
let elemBonusText = '';
if (spell.elem !== 'raw' && floorElem) {
if (spell.elem === floorElem) {
elemBonus = 1.25;
elemBonusText = '+25% same element';
} else if (ELEMENTS[spell.elem] && ELEMENT_OPPOSITES[floorElem] === spell.elem) {
elemBonus = 1.5;
elemBonusText = '+50% super effective';
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) {
spells.push(effectDef.effect.spellId);
}
}
}
return {
base: baseDmg,
combatTrainBonus,
arcaneFuryMult,
elemMasteryMult,
guardianBaneMult,
pactMult,
precisionChance,
elemBonus,
elemBonusText,
total: calcDamage(state, activeSpellId, floorElem)
};
return [...new Set(spells)];
}
// ─── Total DPS Calculation ─────────────────────────────────────────────────────
// ─── DPS Calculation ──────────────────────────────────────────────────────────
// Compute total DPS from all sources (spells, equipment, etc.)
export function getTotalDPS(
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances'>,
upgradeEffects: { attackSpeedMultiplier: number },
floorElem: string
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances' | 'spells' | 'prestigeUpgrades'>,
upgradeEffects: { spellDamageBonus?: number; attackSpeedMultiplier?: number; [key: string]: unknown },
floorElem?: string
): number {
const quickCastBonus = 1 + (state.skills.quickCast || 0) * 0.05;
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
const castsPerSecondMult = HOURS_PER_TICK / (TICK_MS / 1000);
const activeEquipmentSpells = getActiveEquipmentSpells(
state.equippedInstances,
state.equipmentInstances
);
let totalDPS = 0;
for (const { spellId } of activeEquipmentSpells) {
const spell = SPELLS_DEF[spellId];
if (!spell) continue;
// 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;
const spellCastSpeed = spell.castSpeed || 1;
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
const damagePerCast = calcDamage(state, spellId, floorElem);
const castsPerSecond = totalCastSpeed * castsPerSecondMult;
// Calculate damage per cast
const damage = calcDamage(state, spellId, floorElem);
totalDPS += damagePerCast * castsPerSecond;
// Get cast speed (spells per second)
// Base cast time is 1 second, modified by casting speed bonuses
const baseCastTime = spellDef.baseCastTime || 1.0;
const castingSpeedBonus = 1 + (state.skills.castingSpeed || 0) * 0.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;