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
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s
This commit is contained in:
@@ -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,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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user