feat: Add Mana Circulation discipline with regen multiplier and meditation cap perks
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s

This commit is contained in:
2026-05-26 20:58:55 +02:00
parent 46013a15c8
commit 02600754e7
11 changed files with 88 additions and 17 deletions
+3 -1
View File
@@ -13,6 +13,7 @@ import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePresti
import { getUnifiedEffects } from '@/lib/game/effects';
import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores';
import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@/lib/game/effects';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
export function LeftPanel() {
const [isGathering, setIsGathering] = useState(false);
@@ -53,10 +54,11 @@ export function LeftPanel() {
}, [isGathering, gatherMana]);
const upgradeEffects = getUnifiedEffects({ skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances });
const disciplineEffects = computeDisciplineEffects();
const maxMana = computeTotalMaxMana({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const baseRegen = computeTotalRegen({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const clickMana = computeTotalClickMana({ skills: {}, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus);
const incursionStrength = getIncursionStrength(useGameStore((s) => s.day), useGameStore((s) => s.hour));
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
+1 -1
View File
@@ -90,7 +90,7 @@ function useGameDerivedStats() {
}, upgradeEffects, disciplineEffects);
const clickMana = computeClickMana({}, disciplineEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus);
const incursionStrength = getIncursionStrength(day, hour);
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
+32
View File
@@ -35,4 +35,36 @@ export const baseDisciplines: DisciplineDefinition[] = [
},
],
},
{
id: 'mana-circulation',
name: 'Mana Circulation',
attunement: DisciplinesAttunementType.BASE,
manaType: 'raw',
baseCost: 5,
description:
'Cultivate and circulate raw mana through the body — the foundation upon which all elemental work is built.',
statBonus: { stat: 'regenBonus', baseValue: 0.2, label: 'Raw Mana Regen' },
difficultyFactor: 100,
scalingFactor: 50,
drainBase: 1,
perks: [
{
id: 'mana-circulation-regen',
type: 'infinite',
threshold: 100,
value: 100,
description: 'Every 100 XP: +10% raw mana regen',
bonus: { stat: 'regenMultiplier', amount: 0.10 },
},
{
id: 'mana-circulation-meditation',
type: 'capped',
threshold: 100,
value: 100,
description: 'Every 100 XP: +0.5 max meditation multiplier (7 tiers, up to +3.5)',
bonus: { stat: 'meditationCapBonus', amount: 0.5 },
maxTier: 7,
},
],
},
];
+21 -2
View File
@@ -18,15 +18,22 @@ import {
const KNOWN_BONUS_STATS = new Set([
'maxManaBonus',
'regenBonus',
'regenMultiplier',
'clickManaBonus',
'baseDamageBonus',
'elementCapBonus',
'meditationCapBonus',
]);
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 }.
@@ -45,9 +52,18 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): Discipl
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;
}
@@ -90,7 +106,10 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): Discipl
}
} else if (perk.type === 'capped') {
if (perk.bonus) {
const tier = calculatePerkTier(disc.xp, perk.threshold, perk.value);
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);
}
@@ -101,5 +120,5 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): Discipl
}
}
return { bonuses, multipliers, specials, conversions };
return { bonuses, multipliers, specials, meditationCapBonus, conversions };
}
+4 -2
View File
@@ -21,6 +21,7 @@ import { computePactMultiplier, computePactInsightMultiplier } from '../utils/pa
import { ELEMENTS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier, HOURS_PER_TICK, TICK_MS } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
import { computeDisciplineEffects } from '../effects/discipline-effects';
/**
* Hook for all mana-related derived stats
@@ -52,9 +53,10 @@ export function useManaStats() {
[]
);
const disciplineEffects = computeDisciplineEffects();
const meditationMultiplier = useMemo(
() => getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency),
[meditateTicks, upgradeEffects.meditationEfficiency]
() => getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus),
[meditateTicks, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus]
);
const incursionStrength = useMemo(
+1 -1
View File
@@ -75,7 +75,7 @@ export function useManaStats() {
const clickMana = computeClickMana({}, disciplineEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus);
const incursionStrength = getIncursionStrength(day, hour);
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
+2 -2
View File
@@ -121,7 +121,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
{ skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
undefined,
disciplineEffects,
);
) * (1 + (disciplineEffects.multipliers.regenMultiplier || 0));
// Time progression
let hour = ctx.game.hour + HOURS_PER_TICK;
@@ -172,7 +172,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
if (ctx.combat.currentAction === 'meditate') {
meditateTicks++;
meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1);
meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1, disciplineEffects.meditationCapBonus);
} else {
meditateTicks = 0;
}
+5
View File
@@ -26,6 +26,11 @@ export interface DisciplinePerk {
description: string;
unlocksEffects?: string[];
bonus?: PerkBonus;
/**
* For capped perks: maximum number of tiers. If unset, capped perks
* behave like infinite (no cap on tier count).
*/
maxTier?: number;
}
// ─── Discipline Definition ────────────────────────────────────────────────────
+17 -6
View File
@@ -124,29 +124,40 @@ export function computeClickMana(
// ─── Meditation Bonus ─────────────────────────────────────────────────────────
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number {
export function getMeditationBonus(
meditateTicks: number,
skills: Record<string, number>,
meditationEfficiency: number = 1,
disciplineMeditationCap: number = 0,
): 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
// Determine the hard cap for this meditation session.
// disciplineMeditationCap adds +0.5 per point (e.g. from Mana Circulation discipline).
// Base max is 5.0 (Void Meditation), each discipline bonus adds +0.5.
const maxMultiplier = 5.0 + disciplineMeditationCap;
// Base meditation: ramps up over 4 hours, capped at 1.5x or discipline cap
let bonus = 1 + Math.min(hours / 4, 0.5);
bonus = Math.min(bonus, maxMultiplier);
// With Meditation Focus: up to 2.5x after 4 hours
if (hasMeditation && hours >= 4) {
bonus = 2.5;
bonus = Math.min(2.5, maxMultiplier);
}
// With Deep Trance: up to 3.0x after 6 hours
if (hasDeepTrance && hours >= 6) {
bonus = 3.0;
bonus = Math.min(3.0, maxMultiplier);
}
// With Void Meditation: up to 5.0x after 8 hours
// With Void Meditation: up to maxMultiplier after 8 hours
if (hasVoidMeditation && hours >= 8) {
bonus = 5.0;
bonus = maxMultiplier;
}
// Apply meditation efficiency from upgrades