Phase 3: Split utils.ts by responsibility
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m49s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m49s
This commit is contained in:
+4
-371
@@ -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
@@ -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;
|
for (const id of equippedIds) {
|
||||||
elemBonusText: string;
|
const instance = equipmentInstances[id];
|
||||||
total: number;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const spellCastSpeed = spell.castSpeed || 1;
|
// Calculate DPS for each active spell
|
||||||
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
|
for (const spellId of activeSpells) {
|
||||||
const damagePerCast = calcDamage(state, spellId, floorElem);
|
const spellDef = SPELLS_DEF[spellId];
|
||||||
const castsPerSecond = totalCastSpeed * castsPerSecondMult;
|
if (!spellDef) continue;
|
||||||
|
|
||||||
totalDPS += damagePerCast * castsPerSecond;
|
// Calculate damage per cast
|
||||||
|
const damage = calcDamage(state, spellId, floorElem);
|
||||||
|
|
||||||
|
// 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,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,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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
getEffectiveSkillLevel,
|
|
||||||
getElementalBonus,
|
getElementalBonus,
|
||||||
|
getBoonBonuses,
|
||||||
calcDamage,
|
calcDamage,
|
||||||
calcInsight,
|
calcInsight,
|
||||||
getMeditationBonus,
|
|
||||||
getIncursionStrength,
|
getIncursionStrength,
|
||||||
canAffordSpellCost,
|
canAffordSpellCost,
|
||||||
deductSpellCost,
|
deductSpellCost,
|
||||||
getDamageBreakdown,
|
getActiveEquipmentSpells,
|
||||||
getTotalDPS,
|
getTotalDPS
|
||||||
type DamageBreakdown
|
|
||||||
} from './combat-utils';
|
} from './combat-utils';
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply effects if available (now includes equipment bonuses)
|
// Meditation bonus now affects regen rate directly
|
||||||
if (effects) {
|
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number {
|
||||||
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
|
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;
|
||||||
}
|
}
|
||||||
return base;
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user