ab3afae2a6
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m2s
- New files: element-distance.ts, conversion-costs.ts, conversion-rates.ts - All conversion types (discipline, attunement, pact) use unified formula - Conversion costs scale exponentially by element tier (10^(d+1) raw, 10*(d+1) per component) - Costs deducted from regen, not from mana pool - Auto-pause on insufficient regen with UI warning - Meditation boosts conversion rates (reduced by distance) - Attunement levels provide +50% multiplicative bonus per level - Guardian pacts provide +0.15/hr base rate + invoker level bonus - Removed convertMana, processConvertAction, craftComposite from manaStore - Stats tab shows per-element conversion breakdown with formulas - ManaDisplay shows per-element net regen rates - All 916 tests pass, all files under 400 lines
156 lines
5.3 KiB
TypeScript
156 lines
5.3 KiB
TypeScript
// ─── Discipline Effects ───────────────────────────────────────────────────────
|
|
// Computes bonuses from active disciplines and integrates with the unified effect system
|
|
//
|
|
// NEW MODEL: Conversion disciplines contribute to conversion_{element} stat bonuses.
|
|
// The unified conversion-rates.ts calculator handles rate computation and regen deduction.
|
|
// This file no longer builds a direct conversions map for the tick pipeline.
|
|
|
|
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',
|
|
'clickManaMultiplier',
|
|
'studySpeed',
|
|
// Conversion stat bonuses (one per element)
|
|
'conversion_fire',
|
|
'conversion_water',
|
|
'conversion_air',
|
|
'conversion_earth',
|
|
'conversion_light',
|
|
'conversion_dark',
|
|
'conversion_death',
|
|
'conversion_transference',
|
|
'conversion_metal',
|
|
'conversion_sand',
|
|
'conversion_lightning',
|
|
'conversion_frost',
|
|
'conversion_blackflame',
|
|
'conversion_radiantflames',
|
|
'conversion_miasma',
|
|
'conversion_shadowglass',
|
|
'conversion_crystal',
|
|
'conversion_stellar',
|
|
'conversion_void',
|
|
'conversion_soul',
|
|
'conversion_plasma',
|
|
'conversion_time',
|
|
]);
|
|
|
|
export interface DisciplineEffectsResult {
|
|
bonuses: Record<string, number>;
|
|
multipliers: Record<string, number>;
|
|
specials: Set<string>;
|
|
meditationCapBonus: number;
|
|
/**
|
|
* Conversion entries: for each active discipline with a conversionRate,
|
|
* maps target mana type → { rate, sourceManaTypes }.
|
|
* Used by the unified conversion calculator for rate computation.
|
|
*/
|
|
conversions: Record<string, { rate: number; sourceManaTypes: string[] }>;
|
|
}
|
|
|
|
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<typeof ALL_DISCIPLINES[0]> } => !!entry.def);
|
|
|
|
const bonuses: Record<string, number> = {};
|
|
const multipliers: Record<string, number> = {};
|
|
const specials = new Set<string>();
|
|
let meditationCapBonus = 0;
|
|
const conversions: Record<string, { rate: number; sourceManaTypes: string[] }> = {};
|
|
|
|
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 (includes conversion_{element} for regen disciplines)
|
|
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
|
|
// This is used by the unified conversion calculator
|
|
if (def.conversionRate && def.sourceManaTypes && def.sourceManaTypes.length > 0) {
|
|
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);
|
|
}
|
|
} 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 };
|
|
}
|