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
+4 -371
View File
@@ -1,372 +1,5 @@
// ─── Game Utilities ─────────────────────────────────────────────────────────── // ─── Game Utilities - Re-export from focused modules ───────────────────────
// This file is kept for backward compatibility
// All utilities have been moved to focused modules in the utils/ directory
import type { GameState, SpellCost } from './types'; export * from './utils';
import type { ComputedEffects } from './upgrade-effects';
import {
GUARDIANS,
SPELLS_DEF,
FLOOR_ELEM_CYCLE,
HOURS_PER_TICK,
MAX_DAY,
INCURSION_START_DAY,
ELEMENT_OPPOSITES,
} from './constants';
// ─── Formatting Functions ─────────────────────────────────────────────────────
export function fmt(n: number): string {
if (!isFinite(n) || isNaN(n)) return '0';
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
return Math.floor(n).toString();
}
export function fmtDec(n: number, d: number = 1): string {
return isFinite(n) ? n.toFixed(d) : '0';
}
// ─── Floor Helpers ────────────────────────────────────────────────────────────
export function getFloorMaxHP(floor: number): number {
if (GUARDIANS[floor]) return GUARDIANS[floor].hp;
// Improved scaling: slower early game, faster late game
const baseHP = 100;
const floorScaling = floor * 50;
const exponentialScaling = Math.pow(floor, 1.7);
return Math.floor(baseHP + floorScaling + exponentialScaling);
}
export function getFloorElement(floor: number): string {
return FLOOR_ELEM_CYCLE[(floor - 1) % FLOOR_ELEM_CYCLE.length];
}
// ─── Computed Stats Functions ─────────────────────────────────────────────────
export function computeMaxMana(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
): number {
const pu = state.prestigeUpgrades;
const base =
100 +
(state.skills.manaWell || 0) * 100 +
(pu.manaWell || 0) * 500;
// Apply upgrade effects if provided
if (effects) {
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
}
return base;
}
export function computeElementMax(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
): number {
const pu = state.prestigeUpgrades;
const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25;
// Apply upgrade effects if provided
if (effects) {
return Math.floor((base + effects.elementCapBonus) * effects.elementCapMultiplier);
}
return base;
}
export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
): number {
const pu = state.prestigeUpgrades;
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
const base =
2 +
(state.skills.manaFlow || 0) * 1 +
(state.skills.manaSpring || 0) * 2 +
(pu.manaFlow || 0) * 0.5;
let regen = base * temporalBonus;
// Apply upgrade effects if provided
if (effects) {
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
}
return regen;
}
/**
* Compute regen with dynamic special effects (needs current mana, max mana, incursion)
*/
export function computeEffectiveRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
): number {
// Base regen from existing function
let regen = computeRegen(state, effects);
const incursionStrength = state.incursionStrength || 0;
// Apply incursion penalty
regen *= (1 - incursionStrength);
return regen;
}
export function computeClickMana(state: Pick<GameState, 'skills'>): number {
return (
1 +
(state.skills.manaTap || 0) * 1 +
(state.skills.manaSurge || 0) * 3
);
}
// ─── 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
// e.g., casting water (opposite of fire) at fire floor = super effective
if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective: +50% damage
// Check for weak: spell's opposite matches floor
// e.g., casting fire (whose opposite is water) at water floor = weak
if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak: -25% damage
return 1.0; // Neutral
}
// ─── 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'>,
spellId: string,
floorElem?: string
): number {
const sp = SPELLS_DEF[spellId];
if (!sp) return 5;
const skills = state.skills;
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5;
const pct = 1 + (skills.arcaneFury || 0) * 0.1;
// Elemental mastery bonus
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
// 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;
// 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 = (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) {
damage *= getElementalBonus(sp.elem, floorElem);
}
// Apply crit
if (Math.random() < critChance) {
damage *= critDamageMult;
}
return damage;
}
// ─── 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;
// 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
);
}
// ─── 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 ───────────────────────────────────────────────────────
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<string, { current: number; max: number; unlocked: boolean }>
): 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<string, { current: number; max: number; unlocked: boolean }>
): { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> } {
const newElements = { ...elements };
if (cost.type === 'raw') {
return { rawMana: rawMana - cost.amount, elements: newElements };
} else if (cost.element && newElements[cost.element]) {
newElements[cost.element] = {
...newElements[cost.element],
current: newElements[cost.element].current - cost.amount
};
return { rawMana, elements: newElements };
}
return { rawMana, elements: newElements };
}
+163 -199
View File
@@ -1,81 +1,10 @@
// ─── Combat Utility Functions ───────────────────────────────────────────────── // ─── Combat Utilities ────────────────────────────────────────────────────────
import type { GameState, SpellCost, EquipmentInstance } from '../types'; import type { GameState, SpellCost, EquipmentInstance } from '../types';
import { import { GUARDIANS, SPELLS_DEF, ELEMENT_OPPOSITES, INCURSION_START_DAY, MAX_DAY } from '../constants';
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 { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects'; import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects';
// ─── Equipment Spell Helper ───────────────────────────────────────────────────── // ─── Elemental Damage Bonus ──────────────────────────────────────────────────
// 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: +50% if spell element opposes floor element (super effective) // Elemental damage bonus: +50% if spell element opposes floor element (super effective)
// -25% if spell element matches its own opposite (weak) // -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 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( export function calcDamage(
state: Pick<GameState, 'skills' | 'signedPacts'>, state: Pick<GameState, 'skills' | 'signedPacts'>,
@@ -111,18 +121,24 @@ export function calcDamage(
// Elemental mastery bonus // Elemental mastery bonus
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15; const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
// Guardian bane bonus // Guardian bane bonus - check if current floor has a guardian with matching element
const guardianBonus = floorElem && GUARDIANS[Object.values(GUARDIANS).find(g => g.element === floorElem)?.hp ? 0 : 0] const isGuardianFloor = floorElem && Object.values(GUARDIANS).some(g => g.element === floorElem);
const guardianBonus = isGuardianFloor
? 1 + (skills.guardianBane || 0) * 0.2 ? 1 + (skills.guardianBane || 0) * 0.2
: 1; : 1;
const critChance = (skills.precision || 0) * 0.05; // Get boon bonuses from pacts
const pactMult = state.signedPacts.reduce( const boons = getBoonBonuses(state.signedPacts);
(m, f) => m * (GUARDIANS[f]?.pact || 1),
1
);
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 // Apply elemental bonus if floor element provided
if (floorElem) { if (floorElem) {
@@ -131,58 +147,33 @@ export function calcDamage(
// Apply crit // Apply crit
if (Math.random() < critChance) { if (Math.random() < critChance) {
damage *= 1.5; damage *= critDamageMult;
} }
return damage; return damage;
} }
// ─── Insight Calculation ──────────────────────────────────────────────────────── // ─── Insight Calculation ──────────────────────────────────────────────────────
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number { export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1; 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( 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 ─────────────────────────────────────────────────────────── // ─── Incursion Strength ───────────────────────────────────────────────────────
// 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 ─────────────────────────────────────────────────────────
export function getIncursionStrength(day: number, hour: number): number { export function getIncursionStrength(day: number, hour: number): number {
if (day < INCURSION_START_DAY) return 0; 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); return Math.min(0.95, (totalHours / maxHours) * 0.95);
} }
// ─── Spell Cost Helpers ───────────────────────────────────────────────────────── // ─── Spell Cost Helpers ───────────────────────────────────────────────────────
// Check if player can afford spell cost // Check if player can afford spell cost
export function canAffordSpellCost( export function canAffordSpellCost(
@@ -216,12 +207,11 @@ export function deductSpellCost(
const newElements = { ...elements }; const newElements = { ...elements };
if (cost.type === 'raw') { if (cost.type === 'raw') {
// Clamp to 0 to prevent negative mana return { rawMana: rawMana - cost.amount, elements: newElements };
return { rawMana: Math.max(0, rawMana - cost.amount), elements: newElements };
} else if (cost.element && newElements[cost.element]) { } else if (cost.element && newElements[cost.element]) {
newElements[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 }; return { rawMana, elements: newElements };
} }
@@ -229,93 +219,67 @@ export function deductSpellCost(
return { rawMana, elements: newElements }; return { rawMana, elements: newElements };
} }
// ─── Damage Breakdown Helper ────────────────────────────────────────────────── // ─── Equipment Spell Helpers ──────────────────────────────────────────────────
export interface DamageBreakdown { // Get active spells from equipped equipment
base: number; export function getActiveEquipmentSpells(
combatTrainBonus: number; equippedInstances: Record<string, string | null>,
arcaneFuryMult: number; equipmentInstances: Record<string, EquipmentInstance>
elemMasteryMult: number; ): string[] {
guardianBaneMult: number; const equippedIds = Object.values(equippedInstances).filter((id): id is string => id !== null);
pactMult: number; const spells: string[] = [];
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;
const baseDmg = spell.dmg; for (const id of equippedIds) {
const combatTrainBonus = (state.skills.combatTrain || 0) * 5; const instance = equipmentInstances[id];
const arcaneFuryMult = 1 + (state.skills.arcaneFury || 0) * 0.1; if (!instance) continue;
const elemMasteryMult = 1 + (state.skills.elementalMastery || 0) * 0.15;
const guardianBaneMult = isGuardianFloor ? (1 + (state.skills.guardianBane || 0) * 0.2) : 1; for (const ench of instance.enchantments) {
const pactMult = state.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1); const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
const precisionChance = (state.skills.precision || 0) * 0.05; if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
spells.push(effectDef.effect.spellId);
// 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';
} }
} }
return { return [...new Set(spells)];
base: baseDmg,
combatTrainBonus,
arcaneFuryMult,
elemMasteryMult,
guardianBaneMult,
pactMult,
precisionChance,
elemBonus,
elemBonusText,
total: calcDamage(state, activeSpellId, floorElem)
};
} }
// ─── Total DPS Calculation ───────────────────────────────────────────────────── // ─── DPS Calculation ──────────────────────────────────────────────────────────
// Compute total DPS from all sources (spells, equipment, etc.)
export function getTotalDPS( export function getTotalDPS(
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances'>, state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances' | 'spells' | 'prestigeUpgrades'>,
upgradeEffects: { attackSpeedMultiplier: number }, upgradeEffects: { spellDamageBonus?: number; attackSpeedMultiplier?: number; [key: string]: unknown },
floorElem: string floorElem?: string
): number { ): 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; let totalDPS = 0;
for (const { spellId } of activeEquipmentSpells) { // Get active equipment spells
const spell = SPELLS_DEF[spellId]; const activeSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances);
if (!spell) continue;
// Calculate DPS for each active spell
for (const spellId of activeSpells) {
const spellDef = SPELLS_DEF[spellId];
if (!spellDef) continue;
const spellCastSpeed = spell.castSpeed || 1; // Calculate damage per cast
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult; const damage = calcDamage(state, spellId, floorElem);
const damagePerCast = calcDamage(state, spellId, floorElem);
const castsPerSecond = totalCastSpeed * castsPerSecondMult;
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; return totalDPS;
+1 -9
View File
@@ -1,11 +1,7 @@
// ─── Floor Utility Functions ─────────────────────────────────────────────────── // ─── Floor Helpers ────────────────────────────────────────────────────────────
import { GUARDIANS, FLOOR_ELEM_CYCLE } from '../constants'; import { GUARDIANS, FLOOR_ELEM_CYCLE } from '../constants';
/**
* Get the max HP for a given floor
* Uses guardian data if available, otherwise uses scaling formula
*/
export function getFloorMaxHP(floor: number): number { export function getFloorMaxHP(floor: number): number {
if (GUARDIANS[floor]) return GUARDIANS[floor].hp; if (GUARDIANS[floor]) return GUARDIANS[floor].hp;
// Improved scaling: slower early game, faster late game // Improved scaling: slower early game, faster late game
@@ -15,10 +11,6 @@ export function getFloorMaxHP(floor: number): number {
return Math.floor(baseHP + floorScaling + exponentialScaling); return Math.floor(baseHP + floorScaling + exponentialScaling);
} }
/**
* Get the element for a given floor
* Cycles through FLOOR_ELEM_CYCLE array
*/
export function getFloorElement(floor: number): string { export function getFloorElement(floor: number): string {
return FLOOR_ELEM_CYCLE[(floor - 1) % FLOOR_ELEM_CYCLE.length]; return FLOOR_ELEM_CYCLE[(floor - 1) % FLOOR_ELEM_CYCLE.length];
} }
+1 -7
View File
@@ -1,8 +1,5 @@
// ─── Number Formatting Functions ──────────────────────────────────────────────── // ─── Formatting Functions ─────────────────────────────────────────────────────
/**
* Format a number with K, M, B suffixes for large numbers
*/
export function fmt(n: number): string { export function fmt(n: number): string {
if (!isFinite(n) || isNaN(n)) return '0'; if (!isFinite(n) || isNaN(n)) return '0';
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B'; if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
@@ -11,9 +8,6 @@ export function fmt(n: number): string {
return Math.floor(n).toString(); return Math.floor(n).toString();
} }
/**
* Format a number with a fixed number of decimal places
*/
export function fmtDec(n: number, d: number = 1): string { export function fmtDec(n: number, d: number = 1): string {
return isFinite(n) ? n.toFixed(d) : '0'; return isFinite(n) ? n.toFixed(d) : '0';
} }
+12 -23
View File
@@ -1,35 +1,24 @@
// ─── Game Utilities - Index ─────────────────────────────────────────────────── // ─── Game Utilities - Barrel Export ──────────────────────────────────────────
// Re-exports all utility functions for backward compatibility
// This allows imports from './utils' or './utils/index'
// Export from formatting.ts // Re-export everything from the focused modules
export { fmt, fmtDec } from './formatting'; export { fmt, fmtDec } from './formatting';
// Export from floor-utils.ts
export { getFloorMaxHP, getFloorElement } from './floor-utils'; export { getFloorMaxHP, getFloorElement } from './floor-utils';
// Export from mana-utils.ts
export { export {
DEFAULT_EFFECTS,
computeMaxMana, computeMaxMana,
computeElementMax, computeElementMax,
computeRegen, computeRegen,
computeEffectiveRegen, computeEffectiveRegen,
computeClickMana computeClickMana,
getMeditationBonus
} from './mana-utils'; } from './mana-utils';
// Export from combat-utils.ts
export { export {
getActiveEquipmentSpells, getElementalBonus,
getEffectiveSkillLevel, getBoonBonuses,
getElementalBonus, calcDamage,
calcDamage, calcInsight,
calcInsight, getIncursionStrength,
getMeditationBonus, canAffordSpellCost,
getIncursionStrength,
canAffordSpellCost,
deductSpellCost, deductSpellCost,
getDamageBreakdown, getActiveEquipmentSpells,
getTotalDPS, getTotalDPS
type DamageBreakdown
} from './combat-utils'; } from './combat-utils';
+46 -75
View File
@@ -1,48 +1,12 @@
// ─── Mana Calculation Functions ─────────────────────────────────────────────── // ─── Mana & Regen Utilities ──────────────────────────────────────────────────
import type { GameState } from '../types'; import type { GameState } from '../types';
import { HOURS_PER_TICK, TICK_MS } from '../constants';
import type { ComputedEffects } from '../upgrade-effects'; import type { ComputedEffects } from '../upgrade-effects';
import { getUnifiedEffects, type UnifiedEffects } from '../effects'; import { HOURS_PER_TICK } from '../constants';
// ─── Default Effects Constant ───────────────────────────────────────────────────
// Default empty effects for when effects aren't provided
export const DEFAULT_EFFECTS: ComputedEffects = {
maxManaMultiplier: 1,
maxManaBonus: 0,
regenMultiplier: 1,
regenBonus: 0,
clickManaMultiplier: 1,
clickManaBonus: 0,
meditationEfficiency: 1,
spellCostMultiplier: 1,
conversionEfficiency: 1,
baseDamageMultiplier: 1,
baseDamageBonus: 0,
attackSpeedMultiplier: 1,
critChanceBonus: 0,
critDamageMultiplier: 1.5,
elementalDamageMultiplier: 1,
studySpeedMultiplier: 1,
studyCostMultiplier: 1,
progressRetention: 0,
instantStudyChance: 0,
freeStudyChance: 0,
elementCapMultiplier: 1,
elementCapBonus: 0,
conversionCostMultiplier: 1,
doubleCraftChance: 0,
permanentRegenBonus: 0,
specials: new Set(),
activeUpgrades: [],
};
// ─── Mana Computation Functions ────────────────────────────────────────────────
export function computeMaxMana( export function computeMaxMana(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>, state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects | UnifiedEffects effects?: ComputedEffects
): number { ): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const base = const base =
@@ -50,12 +14,7 @@ export function computeMaxMana(
(state.skills.manaWell || 0) * 100 + (state.skills.manaWell || 0) * 100 +
(pu.manaWell || 0) * 500; (pu.manaWell || 0) * 500;
// If effects not provided, compute unified effects (includes equipment) // Apply upgrade effects if provided
if (!effects && state.equipmentInstances && state.equippedInstances) {
effects = getUnifiedEffects(state as any);
}
// Apply effects if available (now includes equipment bonuses)
if (effects) { if (effects) {
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier); return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
} }
@@ -77,8 +36,8 @@ export function computeElementMax(
} }
export function computeRegen( export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>, state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects | UnifiedEffects effects?: ComputedEffects
): number { ): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1; const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
@@ -90,12 +49,7 @@ export function computeRegen(
let regen = base * temporalBonus; let regen = base * temporalBonus;
// If effects not provided, compute unified effects (includes equipment) // Apply upgrade effects if provided
if (!effects && state.equipmentInstances && state.equippedInstances) {
effects = getUnifiedEffects(state as any);
}
// Apply effects if available (now includes equipment bonuses)
if (effects) { if (effects) {
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier; regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
} }
@@ -107,14 +61,12 @@ export function computeRegen(
* Compute regen with dynamic special effects (needs current mana, max mana, incursion) * Compute regen with dynamic special effects (needs current mana, max mana, incursion)
*/ */
export function computeEffectiveRegen( export function computeEffectiveRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>, state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects | UnifiedEffects effects?: ComputedEffects
): number { ): number {
// Base regen from existing function // Base regen from existing function
let regen = computeRegen(state, effects); let regen = computeRegen(state, effects);
const maxMana = computeMaxMana(state, effects);
const currentMana = state.rawMana;
const incursionStrength = state.incursionStrength || 0; const incursionStrength = state.incursionStrength || 0;
// Apply incursion penalty // Apply incursion penalty
@@ -123,23 +75,42 @@ export function computeEffectiveRegen(
return regen; return regen;
} }
export function computeClickMana( export function computeClickMana(state: Pick<GameState, 'skills'>): number {
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>, return (
effects?: ComputedEffects | UnifiedEffects
): number {
const base =
1 + 1 +
(state.skills.manaTap || 0) * 1 + (state.skills.manaTap || 0) * 1 +
(state.skills.manaSurge || 0) * 3; (state.skills.manaSurge || 0) * 3
);
// If effects not provided, compute unified effects (includes equipment) }
if (!effects && state.equipmentInstances && state.equippedInstances) {
effects = getUnifiedEffects(state as any); // 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;
// Apply effects if available (now includes equipment bonuses) const hasDeepTrance = skills.deepTrance === 1;
if (effects) { const hasVoidMeditation = skills.voidMeditation === 1;
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
} const hours = meditateTicks * HOURS_PER_TICK;
return base;
// 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;
} }