refactor: Redesign Invoker disciplines for pact bonuses and guardian boons
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
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:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,81 @@
|
|||||||
// ─── 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 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user