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
+1 -1
View File
@@ -1,5 +1,5 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-26T16:39:59.755Z Generated: 2026-05-26T18:40:16.686Z
Found: 7 circular chain(s) — these MUST be fixed before modifying involved files. Found: 7 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 135 files (1.5s) (2 warnings) 1. Processed 135 files (1.5s) (2 warnings)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-26T16:39:58.077Z", "generated": "2026-05-26T18:40:14.879Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
}, },
+3 -1
View File
@@ -13,6 +13,7 @@ import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePresti
import { getUnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores'; import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores';
import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@/lib/game/effects'; import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@/lib/game/effects';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
export function LeftPanel() { export function LeftPanel() {
const [isGathering, setIsGathering] = useState(false); const [isGathering, setIsGathering] = useState(false);
@@ -53,10 +54,11 @@ export function LeftPanel() {
}, [isGathering, gatherMana]); }, [isGathering, gatherMana]);
const upgradeEffects = getUnifiedEffects({ skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }); const upgradeEffects = getUnifiedEffects({ skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances });
const disciplineEffects = computeDisciplineEffects();
const maxMana = computeTotalMaxMana({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects); const maxMana = computeTotalMaxMana({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const baseRegen = computeTotalRegen({ 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 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 incursionStrength = getIncursionStrength(useGameStore((s) => s.day), useGameStore((s) => s.hour));
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier; const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
+1 -1
View File
@@ -90,7 +90,7 @@ function useGameDerivedStats() {
}, upgradeEffects, disciplineEffects); }, upgradeEffects, disciplineEffects);
const clickMana = computeClickMana({}, 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 incursionStrength = getIncursionStrength(day, hour);
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); 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([ const KNOWN_BONUS_STATS = new Set([
'maxManaBonus', 'maxManaBonus',
'regenBonus', 'regenBonus',
'regenMultiplier',
'clickManaBonus', 'clickManaBonus',
'baseDamageBonus', 'baseDamageBonus',
'elementCapBonus', 'elementCapBonus',
'meditationCapBonus',
]); ]);
export interface DisciplineEffectsResult { export interface DisciplineEffectsResult {
bonuses: Record<string, number>; bonuses: Record<string, number>;
multipliers: Record<string, number>; multipliers: Record<string, number>;
specials: Set<string>; 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, * Conversion entries: for each active discipline with a conversionRate,
* maps target mana type → { rate, sourceManaTypes }. * maps target mana type → { rate, sourceManaTypes }.
@@ -45,9 +52,18 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): Discipl
const bonuses: Record<string, number> = {}; const bonuses: Record<string, number> = {};
const multipliers: Record<string, number> = {}; const multipliers: Record<string, number> = {};
const specials = new Set<string>(); const specials = new Set<string>();
let meditationCapBonus = 0;
const conversions: Record<string, { rate: number; sourceManaTypes: string[] }> = {}; const conversions: Record<string, { rate: number; sourceManaTypes: string[] }> = {};
function addBonus(stat: string, amount: number) { 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; bonuses[stat] = (bonuses[stat] || 0) + amount;
} }
@@ -90,7 +106,10 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): Discipl
} }
} else if (perk.type === 'capped') { } else if (perk.type === 'capped') {
if (perk.bonus) { 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) { if (tier > 0) {
addBonus(perk.bonus.stat, tier * perk.bonus.amount); 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 { ELEMENTS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier, HOURS_PER_TICK, TICK_MS } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters'; import { getGuardianForFloor } from '../data/guardian-encounters';
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects'; import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
import { computeDisciplineEffects } from '../effects/discipline-effects';
/** /**
* Hook for all mana-related derived stats * Hook for all mana-related derived stats
@@ -52,9 +53,10 @@ export function useManaStats() {
[] []
); );
const disciplineEffects = computeDisciplineEffects();
const meditationMultiplier = useMemo( const meditationMultiplier = useMemo(
() => getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency), () => getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus),
[meditateTicks, upgradeEffects.meditationEfficiency] [meditateTicks, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus]
); );
const incursionStrength = useMemo( const incursionStrength = useMemo(
+1 -1
View File
@@ -75,7 +75,7 @@ export function useManaStats() {
const clickMana = computeClickMana({}, 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 incursionStrength = getIncursionStrength(day, hour);
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); 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: {} }, { skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
undefined, undefined,
disciplineEffects, disciplineEffects,
); ) * (1 + (disciplineEffects.multipliers.regenMultiplier || 0));
// Time progression // Time progression
let hour = ctx.game.hour + HOURS_PER_TICK; let hour = ctx.game.hour + HOURS_PER_TICK;
@@ -172,7 +172,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
if (ctx.combat.currentAction === 'meditate') { if (ctx.combat.currentAction === 'meditate') {
meditateTicks++; meditateTicks++;
meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1); meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1, disciplineEffects.meditationCapBonus);
} else { } else {
meditateTicks = 0; meditateTicks = 0;
} }
+5
View File
@@ -26,6 +26,11 @@ export interface DisciplinePerk {
description: string; description: string;
unlocksEffects?: string[]; unlocksEffects?: string[];
bonus?: PerkBonus; 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 ──────────────────────────────────────────────────── // ─── Discipline Definition ────────────────────────────────────────────────────
+17 -6
View File
@@ -124,29 +124,40 @@ export function computeClickMana(
// ─── Meditation Bonus ───────────────────────────────────────────────────────── // ─── 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 hasMeditation = skills.meditation === 1;
const hasDeepTrance = skills.deepTrance === 1; const hasDeepTrance = skills.deepTrance === 1;
const hasVoidMeditation = skills.voidMeditation === 1; const hasVoidMeditation = skills.voidMeditation === 1;
const hours = meditateTicks * HOURS_PER_TICK; 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); let bonus = 1 + Math.min(hours / 4, 0.5);
bonus = Math.min(bonus, maxMultiplier);
// With Meditation Focus: up to 2.5x after 4 hours // With Meditation Focus: up to 2.5x after 4 hours
if (hasMeditation && hours >= 4) { if (hasMeditation && hours >= 4) {
bonus = 2.5; bonus = Math.min(2.5, maxMultiplier);
} }
// With Deep Trance: up to 3.0x after 6 hours // With Deep Trance: up to 3.0x after 6 hours
if (hasDeepTrance && hours >= 6) { 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) { if (hasVoidMeditation && hours >= 8) {
bonus = 5.0; bonus = maxMultiplier;
} }
// Apply meditation efficiency from upgrades // Apply meditation efficiency from upgrades