// ─── Discipline Effects ─────────────────────────────────────────────────────── // Computes bonuses from active disciplines and integrates with the unified effect system import type { DisciplineStoreState } from '../stores/discipline-slice'; import type { DisciplineState } from '../types/disciplines'; import { useDisciplineStore } from '../stores/discipline-slice'; import { ALL_DISCIPLINES } from '../data/disciplines'; import { calculateStatBonus, calculatePerkTier, getUnlockedPerks, } from '../utils/discipline-math'; /** * Known stat keys consumed by computeAllEffects() in effects.ts. * Perk bonuses are routed to these keys so they flow into the unified system. */ const KNOWN_BONUS_STATS = new Set([ 'maxManaBonus', 'regenBonus', 'regenMultiplier', 'clickManaBonus', 'baseDamageBonus', 'elementCapBonus', 'elementCap_lightning', 'meditationCapBonus', 'pactAffinityBonus', 'guardianBoonMultiplier', 'enchantPower', 'golemCapacity', 'craftingCostReduction', 'disciplineXpBonus', ]); export interface DisciplineEffectsResult { bonuses: Record; multipliers: Record; specials: Set; /** * Bonus to the meditation multiplier cap from disciplines. * Each point of meditationCapBonus adds +0.5 to the max meditation multiplier. */ meditationCapBonus: number; /** * Conversion entries: for each active discipline with a conversionRate, * maps target mana type → { rate, sourceManaTypes }. * The tick pipeline drains source mana types and adds to the target. */ conversions: Record; } export function computeDisciplineEffects(_state?: DisciplineStoreState): DisciplineEffectsResult { const { disciplines } = useDisciplineStore.getState(); const activeDiscs = Object.entries(disciplines) .filter(([, disc]) => disc && disc.xp > 0) .map(([id, disc]) => ({ id, disc, def: ALL_DISCIPLINES.find(d => d.id === id) })) .filter((entry): entry is { id: string; disc: DisciplineState; def: NonNullable } => !!entry.def); const bonuses: Record = {}; const multipliers: Record = {}; const specials = new Set(); let meditationCapBonus = 0; const conversions: Record = {}; function addBonus(stat: string, amount: number) { if (stat === 'meditationCapBonus') { meditationCapBonus += amount; return; } if (stat === 'regenMultiplier') { multipliers[stat] = (multipliers[stat] || 0) + amount; return; } bonuses[stat] = (bonuses[stat] || 0) + amount; } for (const { disc, def } of activeDiscs) { // Continuous stat bonus const statBonus = calculateStatBonus(def.statBonus.baseValue, disc.xp, def.scalingFactor); if (def.statBonus.stat) { addBonus(def.statBonus.stat, statBonus); } // Conversion entry — if this discipline defines conversionRate if (def.conversionRate && def.sourceManaTypes && def.sourceManaTypes.length > 0) { // Scale the conversion rate by the stat bonus multiplier const scaledRate = def.conversionRate + statBonus; conversions[def.manaType] = { rate: scaledRate, sourceManaTypes: def.sourceManaTypes, }; } // Perk unlocks const perks = getUnlockedPerks(def, disc.xp); for (const perk of perks) { if (perk.type === 'once') { if (perk.bonus) { addBonus(perk.bonus.stat, perk.bonus.amount); } else if (!perk.unlocksEffects) { specials.add(perk.id); } // Perks with unlocksEffects are handled by discipline-slice.ts processTick() } else if (perk.type === 'infinite') { if (perk.bonus) { const interval = perk.value; const tier = calculatePerkTier(disc.xp, perk.threshold, interval); if (tier > 0) { addBonus(perk.bonus.stat, tier * perk.bonus.amount); } } else if (!perk.unlocksEffects) { specials.add(perk.id); } } else if (perk.type === 'capped') { if (perk.bonus) { let tier = calculatePerkTier(disc.xp, perk.threshold, perk.value); if (tier > 0 && perk.maxTier !== undefined) { tier = Math.min(tier, perk.maxTier); } if (tier > 0) { addBonus(perk.bonus.stat, tier * perk.bonus.amount); } } else if (!perk.unlocksEffects) { specials.add(perk.id); } } } } return { bonuses, multipliers, specials, meditationCapBonus, conversions }; }