Files
Mana-Loop/src/lib/game/effects/discipline-effects.ts
T
n8n-gitea 8cebea9586 fix(#165,#166,#167,#168,#169,#171,#172): resolve 7 open bug issues
#172 - Grimoire tab: removed dead 'loaded' state guard that permanently showed loading
#169 - Transference Mana Flow: added elements param to checkDisciplinePrerequisites so mana type unlocks are verified
#168 - Perk descriptions: wired 4 broken perks (enchant-2, channel-1, golem-2, efficiency-1) with actual bonus effects; fixed enchant-1 interval (5→50); fixed study-mana-enchantments stat (maxMana→maxManaBonus)
#171 - Shields: removed all shield equipment (4 types), recipes, category, slot mappings; added 'shields' to AGENTS.md banned list
#166 - regenMultiplier: merged disciplineEffects.multipliers.regenMultiplier into computeAllEffects()
#165 - Meditation cap: added meditationCap display to ManaStatsSection UI; updated perk description
#167 - XP accumulation: added Meditative Mastery base discipline with disciplineXpBonus stat; wired into tick pipeline
2026-05-28 09:32:43 +02:00

132 lines
4.7 KiB
TypeScript

// ─── 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<string, number>;
multipliers: Record<string, number>;
specials: Set<string>;
/**
* 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<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
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 };
}