refactor: Redesign Invoker disciplines for pact bonuses and guardian boons
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

- Replace generic spell-casting/void-manipulation with pact-focused disciplines
- Add Pact Attunement (light): reduces pact signing time, boosts pact affinity
- Add Guardian's Boon (dark): amplifies all guardian unique perks
- Add pactAffinityBonus and guardianBoonMultiplier stat keys to effect system
- Apply pactAffinityBonus in pact signing time calculation (gameStore)
- Scale guardian boon values by guardianBoonMultiplier (combat-utils)
- Guard Invoker discipline activation behind signedPacts.length > 0
- Add 'Signed guardian pact' prerequisite display in discipline-math
This commit is contained in:
2026-05-26 21:43:46 +02:00
parent 02600754e7
commit 1aea72c013
10 changed files with 108 additions and 50 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-26T18:40:16.686Z Generated: 2026-05-26T18:59:01.066Z
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.6s) (2 warnings)
2. 1) effects/discipline-effects.ts > stores/discipline-slice.ts > stores/combatStore.ts > stores/combat-actions.ts 2. 1) effects/discipline-effects.ts > stores/discipline-slice.ts > stores/combatStore.ts > stores/combat-actions.ts
3. 2) utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.ts 3. 2) utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.ts
4. 3) utils/floor-utils.ts > utils/room-utils.ts 4. 3) utils/floor-utils.ts > utils/room-utils.ts
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-26T18:40:14.879Z", "generated": "2026-05-26T18:58:59.230Z",
"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."
}, },
@@ -419,6 +419,7 @@
"hooks/useGameDerived.ts": [ "hooks/useGameDerived.ts": [
"constants.ts", "constants.ts",
"data/guardian-encounters.ts", "data/guardian-encounters.ts",
"effects/discipline-effects.ts",
"effects/special-effects.ts", "effects/special-effects.ts",
"effects/upgrade-effects.ts", "effects/upgrade-effects.ts",
"stores/combatStore.ts", "stores/combatStore.ts",
+3 -1
View File
@@ -13,6 +13,7 @@ import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker';
import { calculateStatBonus, calculateManaDrain, checkDisciplinePrerequisites } from '@/lib/game/utils/discipline-math'; import { calculateStatBonus, calculateManaDrain, checkDisciplinePrerequisites } from '@/lib/game/utils/discipline-math';
import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines'; import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines';
import { useManaStore } from '@/lib/game/stores/manaStore'; import { useManaStore } from '@/lib/game/stores/manaStore';
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
import clsx from 'clsx'; import clsx from 'clsx';
// ─── Attunement Tabs ───────────────────────────────────────────────────────── // ─── Attunement Tabs ─────────────────────────────────────────────────────────
@@ -216,10 +217,11 @@ export const DisciplinesTab: React.FC = () => {
const [activeAttunement, setActiveAttunement] = useState<string>('base'); const [activeAttunement, setActiveAttunement] = useState<string>('base');
const elements = useManaStore((s) => s.elements); const elements = useManaStore((s) => s.elements);
const signedPacts = usePrestigeStore((s) => s.signedPacts);
const handleToggle = useCallback((id: string, paused: boolean) => { const handleToggle = useCallback((id: string, paused: boolean) => {
if (paused) { if (paused) {
activate(id, { elements }); activate(id, { elements, signedPacts });
} else { } else {
deactivate(id); deactivate(id);
} }
+51 -28
View File
@@ -1,59 +1,82 @@
// ─── Invoker Discipline Files ───────────────────────────────────────────────── // ─── Invoker Discipline Files ─────────────────────────────────────────────────
// Attunement-focused disciplines for Invoker role // Attunement-focused disciplines for Invoker role
// The Invoker forms pacts with guardians and channels their elemental boons.
import { DisciplinesAttunementType } from '../../types/disciplines'; import { DisciplinesAttunementType } from '../../types/disciplines';
import type { DisciplineDefinition } from '../../types/disciplines'; import type { DisciplineDefinition } from '../../types/disciplines';
export const invokerDisciplines: DisciplineDefinition[] = [ export const invokerDisciplines: DisciplineDefinition[] = [
{ {
id: 'spell-casting', id: 'pact-attunement',
name: 'Spell Casting', name: 'Pact Attunement',
attunement: DisciplinesAttunementType.INVOKER, attunement: DisciplinesAttunementType.INVOKER,
manaType: 'light', manaType: 'light',
baseCost: 10, baseCost: 12,
description: 'Improve spell power and effectiveness.', description:
statBonus: { stat: 'baseDamageBonus', baseValue: 6, label: 'Base Damage' }, 'Deepen your bond with guardian spirits. Reduces pact signing time and amplifies pact power.',
difficultyFactor: 130, statBonus: { stat: 'pactAffinityBonus', baseValue: 0.05, label: 'Pact Affinity' },
scalingFactor: 65, difficultyFactor: 150,
drainBase: 3, scalingFactor: 80,
drainBase: 4,
requires: ['signed_pact'],
perks: [ perks: [
{ {
id: 'spell-1', id: 'pact-affinity-scaling',
type: 'once', type: 'once',
threshold: 200, threshold: 100,
value: 0, value: 0,
description: '+10 Base Damage', description: 'Unlock pact affinity scaling — pact bonuses grow with XP.',
bonus: { stat: 'baseDamageBonus', amount: 10 },
}, },
{ {
id: 'spell-2', id: 'pact-affinity-infinite',
type: 'infinite', type: 'infinite',
threshold: 400, threshold: 200,
value: 30, value: 100,
description: 'Every 300 XP: +5 Base Damage', description: 'Every 100 XP: +5% pact affinity',
bonus: { stat: 'baseDamageBonus', amount: 5 }, bonus: { stat: 'pactAffinityBonus', amount: 0.05 },
},
{
id: 'pact-power-boost',
type: 'capped',
threshold: 500,
value: 200,
maxTier: 5,
description: 'Every 200 XP: +3% guardian pact boon strength (max +15%)',
bonus: { stat: 'guardianBoonMultiplier', amount: 0.03 },
}, },
], ],
}, },
{ {
id: 'void-manipulation', id: 'guardians-boon',
name: 'Void Manipulation', name: "Guardian's Boon",
attunement: DisciplinesAttunementType.INVOKER, attunement: DisciplinesAttunementType.INVOKER,
manaType: 'void', manaType: 'dark',
baseCost: 15, baseCost: 18,
description: 'Master the exotic void mana for devastating effects.', description:
statBonus: { stat: 'baseDamageMultiplier', baseValue: 0.15, label: 'Base Damage Multiplier' }, 'Channel the unique blessings of every guardian you have bonded with. Amplifies all guardian unique perks.',
statBonus: { stat: 'guardianBoonMultiplier', baseValue: 0.10, label: 'Guardian Boon Power' },
difficultyFactor: 200, difficultyFactor: 200,
scalingFactor: 100, scalingFactor: 100,
drainBase: 7, drainBase: 6,
requires: ['signed_pact'],
perks: [ perks: [
{ {
id: 'void-1', id: 'boon-1',
type: 'once', type: 'once',
threshold: 300, threshold: 100,
value: 0, value: 0,
description: 'Unlock void damage multiplier', description: 'Active guardian boons gain +10% effectiveness',
bonus: { stat: 'guardianBoonMultiplier', amount: 0.10 },
},
{
id: 'boon-2',
type: 'capped',
threshold: 200,
value: 350,
maxTier: 5,
description: 'Every 350 XP: +5% guardian boon effectiveness (max +25%)',
bonus: { stat: 'guardianBoonMultiplier', amount: 0.05 },
}, },
], ],
}, },
]; ];
+14
View File
@@ -73,6 +73,18 @@ export interface UnifiedEffects extends ComputedEffects {
disciplineBonuses: Record<string, number>; disciplineBonuses: Record<string, number>;
disciplineMultipliers: Record<string, number>; disciplineMultipliers: Record<string, number>;
disciplineSpecials: Set<string>; disciplineSpecials: Set<string>;
/**
* Total pact affinity bonus from disciplines and prestige upgrades.
* Reduces pact signing time: effectiveTime = baseTime * (1 - pactAffinityBonus).
* Capped at 0.9 to prevent zero/negative times.
*/
pactAffinityBonus: number;
/**
* Total guardian boon multiplier from disciplines.
* Scales the effectiveness of all guardian unique perks.
* 1.0 = base, 1.15 = +15%, etc.
*/
guardianBoonMultiplier: number;
} }
export function computeAllEffects( export function computeAllEffects(
@@ -128,6 +140,8 @@ export function computeAllEffects(
disciplineBonuses: disciplineEffects.bonuses, disciplineBonuses: disciplineEffects.bonuses,
disciplineMultipliers: disciplineEffects.multipliers, disciplineMultipliers: disciplineEffects.multipliers,
disciplineSpecials: disciplineEffects.specials, disciplineSpecials: disciplineEffects.specials,
pactAffinityBonus: Math.min(0.9, disciplineEffects.bonuses.pactAffinityBonus || 0),
guardianBoonMultiplier: 1 + (disciplineEffects.bonuses.guardianBoonMultiplier || 0),
}; };
if (equipmentEffects.bonuses.critChance) { if (equipmentEffects.bonuses.critChance) {
@@ -23,6 +23,8 @@ const KNOWN_BONUS_STATS = new Set([
'baseDamageBonus', 'baseDamageBonus',
'elementCapBonus', 'elementCapBonus',
'meditationCapBonus', 'meditationCapBonus',
'pactAffinityBonus',
'guardianBoonMultiplier',
]); ]);
export interface DisciplineEffectsResult { export interface DisciplineEffectsResult {
+7 -1
View File
@@ -46,7 +46,7 @@ export interface DisciplineStoreState {
} }
export interface DisciplineStoreActions { export interface DisciplineStoreActions {
activate: (id: string, gameState?: { elements?: Record<string, ElementState> }) => void; activate: (id: string, gameState?: { elements?: Record<string, ElementState>; signedPacts?: number[] }) => void;
deactivate: (id: string) => void; deactivate: (id: string) => void;
processTick: (mana: { rawMana: number; elements: Record<string, ElementState> }) => { processTick: (mana: { rawMana: number; elements: Record<string, ElementState> }) => {
rawMana: number; rawMana: number;
@@ -90,6 +90,12 @@ export const useDisciplineStore = create<DisciplineStore>()(
if (nonPaused >= s.concurrentLimit) return s; if (nonPaused >= s.concurrentLimit) return s;
if (!canProceedDiscipline(def, existing, gameState)) return s; if (!canProceedDiscipline(def, existing, gameState)) return s;
// Invoker disciplines require at least one signed guardian pact
if (def.attunement === 'invoker') {
const signedPacts = gameState?.signedPacts || [];
if (signedPacts.length === 0) return s;
}
// Check discipline prerequisites (requires field → discipline XP) // Check discipline prerequisites (requires field → discipline XP)
const prereqCheck = checkDisciplinePrerequisites(def, s.disciplines, ALL_DISCIPLINES); const prereqCheck = checkDisciplinePrerequisites(def, s.disciplines, ALL_DISCIPLINES);
if (!prereqCheck.canProceed) return s; if (!prereqCheck.canProceed) return s;
+4 -4
View File
@@ -223,11 +223,11 @@ export const useGameStore = create<GameCoordinatorStore>()(
if (ctx.prestige.pactRitualFloor !== null) { if (ctx.prestige.pactRitualFloor !== null) {
const guardian = getGuardianForFloor(ctx.prestige.pactRitualFloor); const guardian = getGuardianForFloor(ctx.prestige.pactRitualFloor);
if (guardian) { if (guardian) {
const pactAffinityBonus = 1 - (ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1; const pactAffinity = Math.min(0.9,
const requiredTime = guardian.pactTime * pactAffinityBonus; (ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1 + (disciplineEffects.bonuses.pactAffinityBonus || 0));
const newProgress = ctx.prestige.pactRitualProgress + HOURS_PER_TICK; const requiredTime = guardian.pactTime * (1 - pactAffinity);
if (newProgress >= requiredTime) { if (ctx.prestige.pactRitualProgress + HOURS_PER_TICK >= requiredTime) {
addLog(`📜 Pact signed with ${guardian.name}! You have gained their boons.`); addLog(`📜 Pact signed with ${guardian.name}! You have gained their boons.`);
// Unlock mana types granted by this guardian // Unlock mana types granted by this guardian
+17 -13
View File
@@ -71,7 +71,10 @@ export interface BoonBonuses {
} }
// Helper to calculate total boon bonuses from signed pacts // Helper to calculate total boon bonuses from signed pacts
export function getBoonBonuses(signedPacts: number[]): BoonBonuses { export function getBoonBonuses(
signedPacts: number[],
guardianBoonMultiplier: number = 1.0,
): BoonBonuses {
const bonuses: BoonBonuses = { const bonuses: BoonBonuses = {
maxMana: 0, maxMana: 0,
manaRegen: 0, manaRegen: 0,
@@ -92,42 +95,43 @@ export function getBoonBonuses(signedPacts: number[]): BoonBonuses {
if (!guardian) continue; if (!guardian) continue;
for (const boon of guardian.boons) { for (const boon of guardian.boons) {
let value = boon.value * guardianBoonMultiplier;
switch (boon.type) { switch (boon.type) {
case 'maxMana': case 'maxMana':
bonuses.maxMana += boon.value; bonuses.maxMana += value;
break; break;
case 'manaRegen': case 'manaRegen':
bonuses.manaRegen += boon.value; bonuses.manaRegen += value;
break; break;
case 'castingSpeed': case 'castingSpeed':
bonuses.castingSpeed += boon.value; bonuses.castingSpeed += value;
break; break;
case 'elementalDamage': case 'elementalDamage':
bonuses.elementalDamage += boon.value; bonuses.elementalDamage += value;
break; break;
case 'rawDamage': case 'rawDamage':
bonuses.rawDamage += boon.value; bonuses.rawDamage += value;
break; break;
case 'critChance': case 'critChance':
bonuses.critChance += boon.value; bonuses.critChance += value;
break; break;
case 'critDamage': case 'critDamage':
bonuses.critDamage += boon.value; bonuses.critDamage += value;
break; break;
case 'spellEfficiency': case 'spellEfficiency':
bonuses.spellEfficiency += boon.value; bonuses.spellEfficiency += value;
break; break;
case 'manaGain': case 'manaGain':
bonuses.manaGain += boon.value; bonuses.manaGain += value;
break; break;
case 'insightGain': case 'insightGain':
bonuses.insightGain += boon.value; bonuses.insightGain += value;
break; break;
case 'studySpeed': case 'studySpeed':
bonuses.studySpeed += boon.value; bonuses.studySpeed += value;
break; break;
case 'prestigeInsight': case 'prestigeInsight':
bonuses.prestigeInsight += boon.value; bonuses.prestigeInsight += value;
break; break;
} }
} }
+6
View File
@@ -125,6 +125,12 @@ export function checkDisciplinePrerequisites(
const missingPrereqs: string[] = []; const missingPrereqs: string[] = [];
for (const reqId of discipline.requires) { for (const reqId of discipline.requires) {
// Special case: 'signed_pact' requires at least one guardian pact
if (reqId === 'signed_pact') {
missingPrereqs.push('Signed guardian pact');
continue;
}
// Check if this is a discipline prerequisite (exists in definitions) // Check if this is a discipline prerequisite (exists in definitions)
const reqDef = allDefinitions.find((d) => d.id === reqId); const reqDef = allDefinitions.find((d) => d.id === reqId);
if (reqDef) { if (reqDef) {