Phase 3: Split computed-stats.ts by responsibility
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
// ─── Combat Utility Functions ─────────────────────────────────────────────────
|
||||
|
||||
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 { 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: +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
|
||||
}
|
||||
|
||||
// ─── 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
|
||||
const guardianBonus = floorElem && GUARDIANS[Object.values(GUARDIANS).find(g => g.element === floorElem)?.hp ? 0 : 0]
|
||||
? 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
|
||||
);
|
||||
|
||||
let damage = baseDmg * pct * pactMult * elemMasteryBonus;
|
||||
|
||||
// Apply elemental bonus if floor element provided
|
||||
if (floorElem) {
|
||||
damage *= getElementalBonus(sp.elem, floorElem);
|
||||
}
|
||||
|
||||
// Apply crit
|
||||
if (Math.random() < critChance) {
|
||||
damage *= 1.5;
|
||||
}
|
||||
|
||||
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;
|
||||
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus;
|
||||
return Math.floor(
|
||||
(state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * 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') {
|
||||
// Clamp to 0 to prevent negative mana
|
||||
return { rawMana: Math.max(0, 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)
|
||||
};
|
||||
return { rawMana, elements: newElements };
|
||||
}
|
||||
|
||||
return { rawMana, elements: newElements };
|
||||
}
|
||||
|
||||
// ─── Damage Breakdown Helper ───────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
|
||||
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 {
|
||||
base: baseDmg,
|
||||
combatTrainBonus,
|
||||
arcaneFuryMult,
|
||||
elemMasteryMult,
|
||||
guardianBaneMult,
|
||||
pactMult,
|
||||
precisionChance,
|
||||
elemBonus,
|
||||
elemBonusText,
|
||||
total: calcDamage(state, activeSpellId, floorElem)
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Total DPS Calculation ─────────────────────────────────────────────────────
|
||||
|
||||
export function getTotalDPS(
|
||||
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances'>,
|
||||
upgradeEffects: { attackSpeedMultiplier: number },
|
||||
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;
|
||||
|
||||
const spellCastSpeed = spell.castSpeed || 1;
|
||||
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
|
||||
const damagePerCast = calcDamage(state, spellId, floorElem);
|
||||
const castsPerSecond = totalCastSpeed * castsPerSecondMult;
|
||||
|
||||
totalDPS += damagePerCast * castsPerSecond;
|
||||
}
|
||||
|
||||
return totalDPS;
|
||||
}
|
||||
Reference in New Issue
Block a user