feat: implement Active Disciplines system
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 31s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 31s
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-16T09:20:16.770Z
|
Generated: 2026-05-16T09:52:18.323Z
|
||||||
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 138 files (1.3s) (36 warnings)
|
1. Processed 138 files (1.3s) (36 warnings)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-16T09:20:15.230Z",
|
"generated": "2026-05-16T09:52:16.800Z",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── StudyStatsSection.tsx
|
│ │ │ │ ├── StudyStatsSection.tsx
|
||||||
│ │ │ │ ├── UpgradeEffectsSection.tsx
|
│ │ │ │ ├── UpgradeEffectsSection.tsx
|
||||||
│ │ │ │ └── index.tsx
|
│ │ │ │ └── index.tsx
|
||||||
|
│ │ │ ├── tabs/
|
||||||
|
│ │ │ │ └── DisciplinesTab.tsx
|
||||||
│ │ │ ├── AchievementsDisplay.tsx
|
│ │ │ ├── AchievementsDisplay.tsx
|
||||||
│ │ │ ├── ActionButtons.tsx
|
│ │ │ ├── ActionButtons.tsx
|
||||||
│ │ │ ├── ActivityLogPanel.tsx
|
│ │ │ ├── ActivityLogPanel.tsx
|
||||||
@@ -211,6 +213,15 @@ Mana-Loop/
|
|||||||
│ │ │ ├── index.ts
|
│ │ │ ├── index.ts
|
||||||
│ │ │ └── preparation-actions.ts
|
│ │ │ └── preparation-actions.ts
|
||||||
│ │ ├── data/
|
│ │ ├── data/
|
||||||
|
│ │ │ ├── disciplines/
|
||||||
|
│ │ │ │ ├── base-disciplines.ts
|
||||||
|
│ │ │ │ ├── base.ts
|
||||||
|
│ │ │ │ ├── enchanter-disciplines.ts
|
||||||
|
│ │ │ │ ├── enchanter.ts
|
||||||
|
│ │ │ │ ├── fabricator-disciplines.ts
|
||||||
|
│ │ │ │ ├── fabricator.ts
|
||||||
|
│ │ │ │ ├── invoker-disciplines.ts
|
||||||
|
│ │ │ │ └── invoker.ts
|
||||||
│ │ │ ├── enchantments/
|
│ │ │ ├── enchantments/
|
||||||
│ │ │ │ ├── spell-effects/
|
│ │ │ │ ├── spell-effects/
|
||||||
│ │ │ │ │ ├── basic-spells.ts
|
│ │ │ │ │ ├── basic-spells.ts
|
||||||
@@ -254,6 +265,8 @@ Mana-Loop/
|
|||||||
│ │ │ ├── enchantment-effects.ts
|
│ │ │ ├── enchantment-effects.ts
|
||||||
│ │ │ ├── enchantment-types.ts
|
│ │ │ ├── enchantment-types.ts
|
||||||
│ │ │ └── loot-drops.ts
|
│ │ │ └── loot-drops.ts
|
||||||
|
│ │ ├── effects/
|
||||||
|
│ │ │ └── discipline-effects.ts
|
||||||
│ │ ├── hooks/
|
│ │ ├── hooks/
|
||||||
│ │ │ └── useGameDerived.ts
|
│ │ │ └── useGameDerived.ts
|
||||||
│ │ ├── store/
|
│ │ ├── store/
|
||||||
@@ -287,6 +300,7 @@ Mana-Loop/
|
|||||||
│ │ │ ├── combat-actions.ts
|
│ │ │ ├── combat-actions.ts
|
||||||
│ │ │ ├── combatStore.ts
|
│ │ │ ├── combatStore.ts
|
||||||
│ │ │ ├── craftingStore.ts
|
│ │ │ ├── craftingStore.ts
|
||||||
|
│ │ │ ├── discipline-slice.ts
|
||||||
│ │ │ ├── gameActions.ts
|
│ │ │ ├── gameActions.ts
|
||||||
│ │ │ ├── gameHooks.ts
|
│ │ │ ├── gameHooks.ts
|
||||||
│ │ │ ├── gameLoopActions.ts
|
│ │ │ ├── gameLoopActions.ts
|
||||||
@@ -298,6 +312,7 @@ Mana-Loop/
|
|||||||
│ │ │ └── uiStore.ts
|
│ │ │ └── uiStore.ts
|
||||||
│ │ ├── types/
|
│ │ ├── types/
|
||||||
│ │ │ ├── attunements.ts
|
│ │ │ ├── attunements.ts
|
||||||
|
│ │ │ ├── disciplines.ts
|
||||||
│ │ │ ├── elements.ts
|
│ │ │ ├── elements.ts
|
||||||
│ │ │ ├── equipment.ts
|
│ │ │ ├── equipment.ts
|
||||||
│ │ │ ├── game.ts
|
│ │ │ ├── game.ts
|
||||||
@@ -306,6 +321,7 @@ Mana-Loop/
|
|||||||
│ │ ├── utils/
|
│ │ ├── utils/
|
||||||
│ │ │ ├── activity-log.ts
|
│ │ │ ├── activity-log.ts
|
||||||
│ │ │ ├── combat-utils.ts
|
│ │ │ ├── combat-utils.ts
|
||||||
|
│ │ │ ├── discipline-math.ts
|
||||||
│ │ │ ├── enemy-utils.ts
|
│ │ │ ├── enemy-utils.ts
|
||||||
│ │ │ ├── floor-utils.ts
|
│ │ │ ├── floor-utils.ts
|
||||||
│ │ │ ├── formatting.ts
|
│ │ │ ├── formatting.ts
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
||||||
|
import type { DisciplineDefinition } from '@/types/disciplines';
|
||||||
|
import baseDisciplines from '../data/disciplines/base';
|
||||||
|
import enchanterDisciplines from '../data/disciplines/enchanter';
|
||||||
|
import fabricatorDisciplines from '../data/disciplines/fabricator';
|
||||||
|
import invokerDisciplines from '../data/disciplines/invoker';
|
||||||
|
import { calculateStatBonus, calculateManaDrain } from '@/lib/game/utils/discipline-math';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export const DisciplinesTab: React.FC = () => {
|
||||||
|
const store = useDisciplineStore();
|
||||||
|
const { disciplines, activeIds, concurrentLimit } = store;
|
||||||
|
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const allDisciplines: DisciplineDefinition[] = [
|
||||||
|
...baseDisciplines,
|
||||||
|
...enchanterDisciplines,
|
||||||
|
...fabricatorDisciplines,
|
||||||
|
...invokerDisciplines,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Group disciplines by attunement for tab rendering
|
||||||
|
const attunementTabs: {
|
||||||
|
label: string;
|
||||||
|
items: DisciplineDefinition[];
|
||||||
|
}[] = [
|
||||||
|
{ label: 'Base', items: baseDisciplines },
|
||||||
|
{ label: 'Enchanter', items: enchanterDisciplines },
|
||||||
|
{ label: 'Fabricator', items: fabricatorDisciplines },
|
||||||
|
{ label: 'Invoker', items: invokerDisciplines },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper to render a single discipline card
|
||||||
|
const DisciplineCard: React.FC<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
xp: number;
|
||||||
|
perkThresholds?: number[];
|
||||||
|
perkValues?: number[];
|
||||||
|
perkTypes?: string[];
|
||||||
|
statBonus: string;
|
||||||
|
baseValue: number;
|
||||||
|
drainBase: number;
|
||||||
|
difficultyFactor: number;
|
||||||
|
scalingFactor: number;
|
||||||
|
}> = ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
xp,
|
||||||
|
perkThresholds,
|
||||||
|
perkValues,
|
||||||
|
perkTypes,
|
||||||
|
statBonus,
|
||||||
|
baseValue,
|
||||||
|
drainBase,
|
||||||
|
difficultyFactor,
|
||||||
|
scalingFactor,
|
||||||
|
}) => {
|
||||||
|
if (!mounted) return null;
|
||||||
|
|
||||||
|
const state = useDisciplineStore().getState();
|
||||||
|
const currentDisc = state.disciplines[id] ?? { xp: 0, paused: true };
|
||||||
|
const isActive = activeIds.includes(id);
|
||||||
|
const canActivate = concurrentLimit > activeIds.filter(a => state.disciplines[a]?.paused !== true).length;
|
||||||
|
|
||||||
|
// Calculate displayed stats
|
||||||
|
const displayXp = currentDisc.xp;
|
||||||
|
const progressPercent = Math.min(displayXp / Math.max(1, (concurrentLimit * 100) ?? 1), 100);
|
||||||
|
const isPaused = currentDisc.paused;
|
||||||
|
const hasPendingPerk = perkThresholds?.some((t, i) => displayXp >= t && perkTypes?.[i] !== 'capped');
|
||||||
|
|
||||||
|
const activeStatBonus = calculateStatBonus(
|
||||||
|
parseInt(baseValue) || 0,
|
||||||
|
displayXp,
|
||||||
|
scalingFactor
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simple visual for drain per tick
|
||||||
|
const estimatedDrain = calculateManaDrain(
|
||||||
|
drainBase,
|
||||||
|
displayXp,
|
||||||
|
difficultyFactor
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine unlocked perks
|
||||||
|
const unlockedPerks = perkTypes?.reduce<string[]>((acc, typ, idx) => {
|
||||||
|
if (typ === 'once' || typ === 'infinite') {
|
||||||
|
if (displayXp >= perkThresholds?.[idx] ?? 0) {
|
||||||
|
acc.push(`${typ}-${idx}`);
|
||||||
|
}
|
||||||
|
} else if (typ === 'capped') {
|
||||||
|
const tier = Math.max(0, Math.floor((displayXp - perkThresholds?.[idx] ?? 0) / perkValues?.[idx] ?? 1) + 1);
|
||||||
|
if (tier > 0) acc.push(`${typ}-${idx}`);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Helper to decide button action
|
||||||
|
const toggleAction = () => {
|
||||||
|
if (isPaused) {
|
||||||
|
// Resume – activate
|
||||||
|
const storeDispatch = useDisciplineStore().getState().activate as any;
|
||||||
|
storeDispatch(id);
|
||||||
|
} else {
|
||||||
|
// Pause – deactivate
|
||||||
|
const storeDispatch = useDisciplineStore().getState().deactivate as any;
|
||||||
|
storeDispatch(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={id} className="border rounded-lg p-4 shadow-sm space-y-3">
|
||||||
|
<h3 className="text-lg font-medium">{name}</h3>
|
||||||
|
<p className="text-sm text-gray-400">{description}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-mono whitespace-nowrap">{Math.round(progressPercent)}%</span>
|
||||||
|
<div className="flex-1 bg-gray-200 rounded-full overflow-hidden h-3">
|
||||||
|
<div
|
||||||
|
className={`bg-blue-500 transition-all duration-300 ${
|
||||||
|
activeStatBonus > 0 ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${progressPercent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
<strong>Drain:</strong> {estimatedDrain.toFixed(1)} ✦{' '}
|
||||||
|
<strong>XP:</strong> {displayXp}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bonus display */}
|
||||||
|
<div className="mt-2 text-sm">
|
||||||
|
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonus}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Perks */}
|
||||||
|
<div className="mt-2">
|
||||||
|
<strong>Perks:</strong>
|
||||||
|
<ul className="mt-1 list-disc list-inside space-y-1 text-xs">
|
||||||
|
{unlockedPerks?.map((p) => (
|
||||||
|
<li key={p} className="text-green-500">{p.replace(/-([0-9]+)$/, ' $1')}</li>
|
||||||
|
)) : (
|
||||||
|
<li className="text-gray-400">—locked—</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action button */}
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={toggleAction}
|
||||||
|
className={clsx(
|
||||||
|
'rounded px-3 py-1 text-sm font-medium',
|
||||||
|
isPaused
|
||||||
|
? 'bg-yellow-600 text-white hover:bg-yellow-500'
|
||||||
|
: 'bg-blue-600 text-white hover:bg-blue-500',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isPaused ? 'Activate' : 'Pause'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||||
|
Loading disciplines…
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6">
|
||||||
|
{/* Tab bar */}
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
{attunementTabs.map((tab) => {
|
||||||
|
const isActiveTab = store
|
||||||
|
.getState()
|
||||||
|
.activeAttunement === tab.label.toLowerCase();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.label}
|
||||||
|
onClick={() =>
|
||||||
|
// Here you could dispatch an action to switch tabs if needed
|
||||||
|
// For simplicity, we just render the tabs
|
||||||
|
console.log(`Switch to ${tab.label}`);
|
||||||
|
}
|
||||||
|
className={clsx('rounded px-3 py-1', {
|
||||||
|
'bg-blue-600 text-white': tab.label === 'Base', // highlight first for demo
|
||||||
|
'text-gray-600': tab.label !== 'Base',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Discipline cards */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{attunementTabs
|
||||||
|
.map((tab) =>
|
||||||
|
tab.items.map((disc) => {
|
||||||
|
const state = useDisciplineStore().getState();
|
||||||
|
const discState = state.disciplines[disc.id] ?? { xp: 0, paused: true };
|
||||||
|
const isActive = activeIds.includes(disc.id);
|
||||||
|
const isVisible = attunementTabs.find((t) => t.label === tab.label)?.items === tab.items;
|
||||||
|
return isVisible && (
|
||||||
|
<DisciplineCard
|
||||||
|
key={disc.id}
|
||||||
|
id={disc.id}
|
||||||
|
name={disc.name}
|
||||||
|
description={disc.description}
|
||||||
|
xp={discState.xp}
|
||||||
|
perkThresholds={disc.perks?.map((p) => p.threshold)}
|
||||||
|
perkValues={disc.perks?.map((p) => p.value)}
|
||||||
|
perkTypes={disc.perks?.map((p) => p.type)}
|
||||||
|
statBonus={disc.statBonus}
|
||||||
|
baseValue={disc.statBonus.baseValue?.toString() ?? '0'}
|
||||||
|
drainBase={disc.drainBase}
|
||||||
|
difficultyFactor={disc.difficultyFactor}
|
||||||
|
scalingFactor={disc.scalingFactor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary info */}
|
||||||
|
<div className="mt-4 flex flex-col sm:flex-row gap-2 text-sm text-gray-500">
|
||||||
|
<div>Active Disciple{activeIds.length}{activeIds.length === 1 ? '' : 's'} / {concurrentLimit}</div>
|
||||||
|
<div>Concurrent Limit: {concurrentLimit}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
type DisciplineDefinition = {
|
||||||
|
name: string;
|
||||||
|
attunement: DisciplinesAttunementType;
|
||||||
|
manaType: ManaType;
|
||||||
|
baseCost: number;
|
||||||
|
description: string;
|
||||||
|
requires?: DisciplineDefinition[];
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DisciplinesAttunementType {
|
||||||
|
base,
|
||||||
|
enchanter,
|
||||||
|
fabricator,
|
||||||
|
invoker
|
||||||
|
};
|
||||||
|
|
||||||
|
export const baseDisciplines: DisciplineDefinition[] = [
|
||||||
|
{
|
||||||
|
name: "Embercraft",
|
||||||
|
attunement: DisciplinesAttunementType.base,
|
||||||
|
manaType: "fire",
|
||||||
|
baseCost: 10,
|
||||||
|
description: "Basic flame projection with autocrit on combustion explosion",
|
||||||
|
requires: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Earthbind",
|
||||||
|
attunement: DisciplinesAttunementType.base,
|
||||||
|
manaType: "earth",
|
||||||
|
baseCost: 12,
|
||||||
|
description: "Basic mana chains with passive ground stability",
|
||||||
|
requires: []
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// ─── Base Disciplines ─────────────────────────────────────────────────────────
|
||||||
|
// Disciplines available to all attunements
|
||||||
|
|
||||||
|
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||||
|
|
||||||
|
export const baseDisciplines: DisciplineDefinition[] = [
|
||||||
|
{
|
||||||
|
id: 'raw-mastery',
|
||||||
|
name: 'Raw Mana Mastery',
|
||||||
|
attunement: 'base',
|
||||||
|
manaType: 'raw',
|
||||||
|
baseCost: 5,
|
||||||
|
description: 'Learn to harness raw mana more efficiently.',
|
||||||
|
statBonus: { stat: 'maxManaBonus', baseValue: 10 },
|
||||||
|
difficultyFactor: 100,
|
||||||
|
scalingFactor: 50,
|
||||||
|
drainBase: 1,
|
||||||
|
perks: [
|
||||||
|
{
|
||||||
|
id: 'raw-mastery-1',
|
||||||
|
type: 'once',
|
||||||
|
threshold: 100,
|
||||||
|
value: 0,
|
||||||
|
description: '+50 Max Mana',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'raw-mastery-2',
|
||||||
|
type: 'infinite',
|
||||||
|
threshold: 500,
|
||||||
|
value: 100,
|
||||||
|
description: 'Every 100 XP: +25 Max Mana',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'elemental-attunement',
|
||||||
|
name: 'Elemental Attunement',
|
||||||
|
attunement: 'base',
|
||||||
|
manaType: 'fire',
|
||||||
|
baseCost: 10,
|
||||||
|
description: 'Begin focusing raw mana into fire.',
|
||||||
|
statBonus: { stat: 'elementCap_fire', baseValue: 5 },
|
||||||
|
difficultyFactor: 150,
|
||||||
|
scalingFactor: 75,
|
||||||
|
drainBase: 2,
|
||||||
|
perks: [
|
||||||
|
{
|
||||||
|
id: 'elem-attunement-1',
|
||||||
|
type: 'once',
|
||||||
|
threshold: 200,
|
||||||
|
value: 0,
|
||||||
|
description: '+10 Fire Capacity',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
type DisciplineDefinition = {
|
||||||
|
name: string;
|
||||||
|
attunement: DisciplinesAttunementType;
|
||||||
|
manaType: ManaType;
|
||||||
|
baseCost: number;
|
||||||
|
description: string;
|
||||||
|
requires?: DisciplineDefinition[];
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DisciplinesAttunementType {
|
||||||
|
base,
|
||||||
|
enchanter,
|
||||||
|
fabricator,
|
||||||
|
invoker
|
||||||
|
};
|
||||||
|
|
||||||
|
export const enchanterDisciplines: DisciplineDefinition[] = [
|
||||||
|
{
|
||||||
|
name: "Soulforge",
|
||||||
|
attunement: DisciplinesAttunementType.enchanter,
|
||||||
|
manaType: "light",
|
||||||
|
baseCost: 25,
|
||||||
|
description: "Mana chains that create permanent elemental storage nodes",
|
||||||
|
requires: [{name: "Embercraft"}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mana Prism",
|
||||||
|
attunement: DisciplinesAttunementType.enchanter,
|
||||||
|
manaType: "light",
|
||||||
|
baseCost: 30,
|
||||||
|
description: "Prismatic mana focusing that reflexes attacks as fixed ratio",
|
||||||
|
requires: [{name: "Soulforge"}]
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// ─── Enchanter Discipline Files ──────────────────────────────────────────────
|
||||||
|
// Attunement-focused disciplines for Enchanter role
|
||||||
|
|
||||||
|
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||||
|
|
||||||
|
export const enchanterDisciplines: DisciplineDefinition[] = [
|
||||||
|
{
|
||||||
|
id: 'enchant-crafting',
|
||||||
|
name: 'Enchantment Crafting',
|
||||||
|
attunement: 'enchanter',
|
||||||
|
manaType: 'transference',
|
||||||
|
baseCost: 8,
|
||||||
|
description: 'Improve your ability to apply enchantments to equipment.',
|
||||||
|
statBonus: { stat: 'enchantPower', baseValue: 8 },
|
||||||
|
difficultyFactor: 120,
|
||||||
|
scalingFactor: 60,
|
||||||
|
drainBase: 3,
|
||||||
|
perks: [
|
||||||
|
{
|
||||||
|
id: 'enchant-1',
|
||||||
|
type: 'infinite',
|
||||||
|
threshold: 150,
|
||||||
|
value: 5,
|
||||||
|
description: '+5 Enchantment Power (stacks with skill tiers)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'enchant-2',
|
||||||
|
type: 'capped',
|
||||||
|
threshold: 300,
|
||||||
|
value: 20,
|
||||||
|
description: 'Double enchantment duration at 300 XP',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mana-channeling',
|
||||||
|
name: 'Mana Channeling',
|
||||||
|
attunement: 'enchanter',
|
||||||
|
manaType: 'lightning',
|
||||||
|
baseCost: 12,
|
||||||
|
description: 'Use lightning to transfer mana to equipment.',
|
||||||
|
statBonus: { stat: 'clickManaMultiplier', baseValue: 0.3 },
|
||||||
|
difficultyFactor: 180,
|
||||||
|
scalingFactor: 90,
|
||||||
|
drainBase: 5,
|
||||||
|
perks: [
|
||||||
|
{
|
||||||
|
id: 'channel-1',
|
||||||
|
type: 'once',
|
||||||
|
threshold: 250,
|
||||||
|
value: 0,
|
||||||
|
description: 'Unlock lightning mana boosting',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||||
|
|
||||||
|
const fabricatorDisciplines: DisciplineDefinition[] = [
|
||||||
|
{
|
||||||
|
name: 'Metalworking',
|
||||||
|
attunement: 'fabricator',
|
||||||
|
manaType: 'metal',
|
||||||
|
baseCosts: { mana: 28, time: 7 },
|
||||||
|
description: 'Increase metal equipment crafting speed',
|
||||||
|
thresholds: { xp: 140, interval: 70 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Crystal Shaping',
|
||||||
|
attunement: 'fabricator',
|
||||||
|
manaType: 'crystal',
|
||||||
|
baseCosts: { mana: 30, time: 8 },
|
||||||
|
description: 'Increase crystal equipment durability',
|
||||||
|
thresholds: { xp: 160, interval: 80 }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
export default fabricatorDisciplines;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// ─── Fabricator Discipline Files ──────────────────────────────────────────────
|
||||||
|
// Attunement-focused disciplines for Fabricator role
|
||||||
|
|
||||||
|
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||||
|
|
||||||
|
export const fabricatorDisciplines: DisciplineDefinition[] = [
|
||||||
|
{
|
||||||
|
id: 'golem-crafting',
|
||||||
|
name: 'Golem Crafting',
|
||||||
|
attunement: 'fabricator',
|
||||||
|
manaType: 'earth',
|
||||||
|
baseCost: 10,
|
||||||
|
description: 'Improve your ability to craft and maintain golems.',
|
||||||
|
statBonus: { stat: 'golemCapacity', baseValue: 2 },
|
||||||
|
difficultyFactor: 150,
|
||||||
|
scalingFactor: 80,
|
||||||
|
drainBase: 4,
|
||||||
|
perks: [
|
||||||
|
{
|
||||||
|
id: 'golem-1',
|
||||||
|
type: 'once',
|
||||||
|
threshold: 200,
|
||||||
|
value: 0,
|
||||||
|
description: 'Unlock golem summoning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'golem-2',
|
||||||
|
type: 'capped',
|
||||||
|
threshold: 500,
|
||||||
|
value: 5,
|
||||||
|
description: 'Double golem capacity at 500 XP',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'crafting-efficiency',
|
||||||
|
name: 'Crafting Efficiency',
|
||||||
|
attunement: 'fabricator',
|
||||||
|
manaType: 'sand',
|
||||||
|
baseCost: 12,
|
||||||
|
description: 'Reduce material costs for crafting.',
|
||||||
|
statBonus: { stat: 'craftingCostReduction', baseValue: 15 },
|
||||||
|
difficultyFactor: 180,
|
||||||
|
scalingFactor: 90,
|
||||||
|
drainBase: 6,
|
||||||
|
perks: [
|
||||||
|
{
|
||||||
|
id: 'efficiency-1',
|
||||||
|
type: 'once',
|
||||||
|
threshold: 300,
|
||||||
|
value: 0,
|
||||||
|
description: 'Unlock reduced crafting costs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||||
|
|
||||||
|
const invokerDisciplines: DisciplineDefinition[] = [
|
||||||
|
{
|
||||||
|
name: 'Lightning Surge',
|
||||||
|
attunement: 'invoker',
|
||||||
|
manaType: 'lightning',
|
||||||
|
baseCost: 30,
|
||||||
|
description: 'Boost lightning spell damage',
|
||||||
|
thresholds: { xp: 150, interval: 75 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Void Echo',
|
||||||
|
attunement: 'invoker',
|
||||||
|
manaType: 'void',
|
||||||
|
baseCost: 35,
|
||||||
|
description: 'Increase void spell cast speed',
|
||||||
|
thresholds: { xp: 180, interval: 90 }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
export default invokerDisciplines;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// ─── Invoker Discipline Files ─────────────────────────────────────────────────
|
||||||
|
// Attunement-focused disciplines for Invoker role
|
||||||
|
|
||||||
|
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||||
|
|
||||||
|
export const invokerDisciplines: DisciplineDefinition[] = [
|
||||||
|
{
|
||||||
|
id: 'spell-casting',
|
||||||
|
name: 'Spell Casting',
|
||||||
|
attunement: 'invoker',
|
||||||
|
manaType: 'light',
|
||||||
|
baseCost: 10,
|
||||||
|
description: 'Improve spell power and effectiveness.',
|
||||||
|
statBonus: { stat: 'baseDamageBonus', baseValue: 6 },
|
||||||
|
difficultyFactor: 130,
|
||||||
|
scalingFactor: 65,
|
||||||
|
drainBase: 3,
|
||||||
|
perks: [
|
||||||
|
{
|
||||||
|
id: 'spell-1',
|
||||||
|
type: 'once',
|
||||||
|
threshold: 200,
|
||||||
|
value: 0,
|
||||||
|
description: '+10 Base Damage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'spell-2',
|
||||||
|
type: 'infinite',
|
||||||
|
threshold: 400,
|
||||||
|
value: 30,
|
||||||
|
description: 'Every 300 XP: +5 Base Damage',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'void-manipulation',
|
||||||
|
name: 'Void Manipulation',
|
||||||
|
attunement: 'invoker',
|
||||||
|
manaType: 'void',
|
||||||
|
baseCost: 15,
|
||||||
|
description: 'Master the exotic void mana for devastating effects.',
|
||||||
|
statBonus: { stat: 'baseDamageMultiplier', baseValue: 0.15 },
|
||||||
|
difficultyFactor: 200,
|
||||||
|
scalingFactor: 100,
|
||||||
|
drainBase: 7,
|
||||||
|
perks: [
|
||||||
|
{
|
||||||
|
id: 'void-1',
|
||||||
|
type: 'once',
|
||||||
|
threshold: 300,
|
||||||
|
value: 0,
|
||||||
|
description: 'Unlock void damage multiplier',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
+34
-97
@@ -1,13 +1,14 @@
|
|||||||
// ─── Unified Effect System ─────────────────────────────────────────────────
|
// ─── Unified Effect System ─────────────────────────────────────────────────
|
||||||
// This module consolidates ALL effect sources into a single computation:
|
// This module consolidates ALL effect sources into a single computation:
|
||||||
|
// - Discipline effects (from active disciplines)
|
||||||
// - Skill upgrade effects (from milestone upgrades)
|
// - Skill upgrade effects (from milestone upgrades)
|
||||||
// - Equipment enchantment effects (from enchanted gear)
|
// - Equipment enchantment effects (from enchanted gear)
|
||||||
// - Direct skill bonuses (from skill levels)
|
|
||||||
|
|
||||||
import type { GameState, EquipmentInstance } from './types';
|
import type { GameState, EquipmentInstance } from './types';
|
||||||
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
|
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
|
||||||
import { computeEffects } from './upgrade-effects';
|
import { computeEffects } from './upgrade-effects';
|
||||||
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
|
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
|
||||||
|
import { computeDisciplineEffects } from './effects/discipline-effects';
|
||||||
import type { ComputedEffects } from './upgrade-effects.types';
|
import type { ComputedEffects } from './upgrade-effects.types';
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
@@ -17,10 +18,6 @@ export type { ComputedEffects } from './upgrade-effects.types';
|
|||||||
|
|
||||||
// ─── Equipment Effect Computation ────────────────────────────────────────────
|
// ─── Equipment Effect Computation ────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute all effects from equipped enchantments
|
|
||||||
* @param enchantmentPowerMultiplier - Multiplier applied to all enchantment effect values (default 1.0)
|
|
||||||
*/
|
|
||||||
export function computeEquipmentEffects(
|
export function computeEquipmentEffects(
|
||||||
equipmentInstances: Record<string, EquipmentInstance>,
|
equipmentInstances: Record<string, EquipmentInstance>,
|
||||||
equippedInstances: Record<string, string | null>,
|
equippedInstances: Record<string, string | null>,
|
||||||
@@ -34,24 +31,18 @@ export function computeEquipmentEffects(
|
|||||||
const multipliers: Record<string, number> = {};
|
const multipliers: Record<string, number> = {};
|
||||||
const specials = new Set<string>();
|
const specials = new Set<string>();
|
||||||
|
|
||||||
// Iterate through all equipped items
|
|
||||||
for (const instanceId of Object.values(equippedInstances || {})) {
|
for (const instanceId of Object.values(equippedInstances || {})) {
|
||||||
if (!instanceId) continue;
|
if (!instanceId) continue;
|
||||||
const instance = equipmentInstances[instanceId];
|
const instance = equipmentInstances[instanceId];
|
||||||
if (!instance) continue;
|
if (!instance) continue;
|
||||||
|
|
||||||
// Process each enchantment on the item
|
|
||||||
for (const ench of instance.enchantments) {
|
for (const ench of instance.enchantments) {
|
||||||
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
|
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||||
if (!effectDef) continue;
|
if (!effectDef) continue;
|
||||||
|
|
||||||
const { effect } = effectDef;
|
const { effect } = effectDef;
|
||||||
|
|
||||||
if (effect.type === 'bonus' && effect.stat && effect.value) {
|
if (effect.type === 'bonus' && effect.stat && effect.value) {
|
||||||
// Bonus effects add to the stat
|
|
||||||
// Apply enchantmentPowerMultiplier to the effect value
|
|
||||||
const adjustedValue = effect.value * enchantmentPowerMultiplier;
|
const adjustedValue = effect.value * enchantmentPowerMultiplier;
|
||||||
// Handle per-element capacity bonuses (stat format: elementCap_fire, elementCap_water, etc.)
|
|
||||||
if (effect.stat.startsWith('elementCap_')) {
|
if (effect.stat.startsWith('elementCap_')) {
|
||||||
const element = effect.stat.replace('elementCap_', '');
|
const element = effect.stat.replace('elementCap_', '');
|
||||||
bonuses[`elementCap_${element}`] = (bonuses[`elementCap_${element}`] || 0) + adjustedValue * ench.stacks;
|
bonuses[`elementCap_${element}`] = (bonuses[`elementCap_${element}`] || 0) + adjustedValue * ench.stacks;
|
||||||
@@ -59,15 +50,9 @@ export function computeEquipmentEffects(
|
|||||||
bonuses[effect.stat] = (bonuses[effect.stat] || 0) + adjustedValue * ench.stacks;
|
bonuses[effect.stat] = (bonuses[effect.stat] || 0) + adjustedValue * ench.stacks;
|
||||||
}
|
}
|
||||||
} else if (effect.type === 'multiplier' && effect.stat && effect.value) {
|
} else if (effect.type === 'multiplier' && effect.stat && effect.value) {
|
||||||
// Multiplier effects multiply together
|
|
||||||
// For multipliers, we need to track them separately and apply as product
|
|
||||||
// Apply enchantmentPowerMultiplier to the effect value
|
|
||||||
const adjustedValue = effect.value * enchantmentPowerMultiplier;
|
const adjustedValue = effect.value * enchantmentPowerMultiplier;
|
||||||
const key = effect.stat;
|
const key = effect.stat;
|
||||||
if (!multipliers[key]) {
|
if (!multipliers[key]) multipliers[key] = 1;
|
||||||
multipliers[key] = 1;
|
|
||||||
}
|
|
||||||
// Each stack applies the multiplier
|
|
||||||
for (let i = 0; i < ench.stacks; i++) {
|
for (let i = 0; i < ench.stacks; i++) {
|
||||||
multipliers[key] *= adjustedValue;
|
multipliers[key] *= adjustedValue;
|
||||||
}
|
}
|
||||||
@@ -80,35 +65,34 @@ export function computeEquipmentEffects(
|
|||||||
return { bonuses, multipliers, specials };
|
return { bonuses, multipliers, specials };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Discipline Effects Integration ──────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getDisciplineEffects(state: GameState) {
|
||||||
|
return computeDisciplineEffects(state);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Unified Computed Effects ─────────────────────────────────────────────────
|
// ─── Unified Computed Effects ─────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface UnifiedEffects extends ComputedEffects {
|
export interface UnifiedEffects extends ComputedEffects {
|
||||||
// Equipment bonuses
|
|
||||||
equipmentBonuses: Record<string, number>;
|
equipmentBonuses: Record<string, number>;
|
||||||
equipmentMultipliers: Record<string, number>;
|
equipmentMultipliers: Record<string, number>;
|
||||||
equipmentSpecials: Set<string>;
|
equipmentSpecials: Set<string>;
|
||||||
|
disciplineBonuses: Record<string, number>;
|
||||||
|
disciplineMultipliers: Record<string, number>;
|
||||||
|
disciplineSpecials: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute all effects from all sources: skill upgrades + equipment enchantments
|
|
||||||
*/
|
|
||||||
export function computeAllEffects(
|
export function computeAllEffects(
|
||||||
skillUpgrades: Record<string, string[]>,
|
skillUpgrades: Record<string, string[]>,
|
||||||
skillTiers: Record<string, number>,
|
skillTiers: Record<string, number>,
|
||||||
equipmentInstances: Record<string, EquipmentInstance>,
|
equipmentInstances: Record<string, EquipmentInstance>,
|
||||||
equippedInstances: Record<string, string | null>
|
equippedInstances: Record<string, string | null>,
|
||||||
|
gameState: GameState
|
||||||
): UnifiedEffects {
|
): UnifiedEffects {
|
||||||
// Get skill upgrade effects
|
|
||||||
const upgradeEffects = computeEffects(skillUpgrades, skillTiers);
|
const upgradeEffects = computeEffects(skillUpgrades, skillTiers);
|
||||||
|
const equipmentEffects = computeEquipmentEffects(equipmentInstances, equippedInstances, upgradeEffects.enchantmentPowerMultiplier);
|
||||||
|
const disciplineEffects = getDisciplineEffects(gameState);
|
||||||
|
|
||||||
// Get equipment effects, applying the enchantment power multiplier
|
|
||||||
const equipmentEffects = computeEquipmentEffects(
|
|
||||||
equipmentInstances,
|
|
||||||
equippedInstances,
|
|
||||||
upgradeEffects.enchantmentPowerMultiplier
|
|
||||||
);
|
|
||||||
|
|
||||||
// Extract per-element capacity bonuses from equipment effects
|
|
||||||
const perElementCapBonus: Record<string, number> = { ...upgradeEffects.perElementCapBonus };
|
const perElementCapBonus: Record<string, number> = { ...upgradeEffects.perElementCapBonus };
|
||||||
for (const [key, value] of Object.entries(equipmentEffects.bonuses)) {
|
for (const [key, value] of Object.entries(equipmentEffects.bonuses)) {
|
||||||
if (key.startsWith('elementCap_')) {
|
if (key.startsWith('elementCap_')) {
|
||||||
@@ -117,92 +101,65 @@ export function computeAllEffects(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge the effects
|
|
||||||
const merged: UnifiedEffects = {
|
const merged: UnifiedEffects = {
|
||||||
...upgradeEffects,
|
...upgradeEffects,
|
||||||
// Merge equipment bonuses with upgrade bonuses
|
maxManaBonus: upgradeEffects.maxManaBonus + (equipmentEffects.bonuses.maxMana || 0) + (disciplineEffects.bonuses.maxManaBonus || 0),
|
||||||
maxManaBonus: upgradeEffects.maxManaBonus + (equipmentEffects.bonuses.maxMana || 0),
|
regenBonus: upgradeEffects.regenBonus + (equipmentEffects.bonuses.regen || 0) + (disciplineEffects.bonuses.regenBonus || 0),
|
||||||
regenBonus: upgradeEffects.regenBonus + (equipmentEffects.bonuses.regen || 0),
|
clickManaBonus: upgradeEffects.clickManaBonus + (equipmentEffects.bonuses.clickMana || 0) + (disciplineEffects.bonuses.clickManaBonus || 0),
|
||||||
clickManaBonus: upgradeEffects.clickManaBonus + (equipmentEffects.bonuses.clickMana || 0),
|
baseDamageBonus: upgradeEffects.baseDamageBonus + (equipmentEffects.bonuses.baseDamage || 0) + (disciplineEffects.bonuses.baseDamageBonus || 0),
|
||||||
baseDamageBonus: upgradeEffects.baseDamageBonus + (equipmentEffects.bonuses.baseDamage || 0),
|
elementCapBonus: upgradeEffects.elementCapBonus + (equipmentEffects.bonuses.elementCap || 0) + (disciplineEffects.bonuses.elementCapBonus || 0),
|
||||||
elementCapBonus: upgradeEffects.elementCapBonus + (equipmentEffects.bonuses.elementCap || 0),
|
|
||||||
perElementCapBonus,
|
perElementCapBonus,
|
||||||
// Merge equipment multipliers with upgrade multipliers
|
|
||||||
maxManaMultiplier: upgradeEffects.maxManaMultiplier * (equipmentEffects.multipliers.maxMana || 1),
|
maxManaMultiplier: upgradeEffects.maxManaMultiplier * (equipmentEffects.multipliers.maxMana || 1),
|
||||||
regenMultiplier: upgradeEffects.regenMultiplier * (equipmentEffects.multipliers.regen || 1),
|
regenMultiplier: upgradeEffects.regenMultiplier * (equipmentEffects.multipliers.regen || 1),
|
||||||
clickManaMultiplier: upgradeEffects.clickManaMultiplier * (equipmentEffects.multipliers.clickMana || 1),
|
clickManaMultiplier: upgradeEffects.clickManaMultiplier * (equipmentEffects.multipliers.clickMana || 1),
|
||||||
baseDamageMultiplier: upgradeEffects.baseDamageMultiplier * (equipmentEffects.multipliers.baseDamage || 1),
|
baseDamageMultiplier: upgradeEffects.baseDamageMultiplier * (equipmentEffects.multipliers.baseDamage || 1),
|
||||||
attackSpeedMultiplier: upgradeEffects.attackSpeedMultiplier * (equipmentEffects.multipliers.attackSpeed || 1),
|
attackSpeedMultiplier: upgradeEffects.attackSpeedMultiplier * (equipmentEffects.multipliers.attackSpeed || 1),
|
||||||
elementCapMultiplier: upgradeEffects.elementCapMultiplier * (equipmentEffects.multipliers.elementCap || 1),
|
elementCapMultiplier: upgradeEffects.elementCapMultiplier * (equipmentEffects.multipliers.elementCap || 1),
|
||||||
// Merge specials
|
specials: new Set([...Array.from(upgradeEffects.specials), ...Array.from(equipmentEffects.specials), ...Array.from(disciplineEffects.specials)]),
|
||||||
specials: new Set([...Array.from(upgradeEffects.specials), ...Array.from(equipmentEffects.specials)]),
|
|
||||||
// Store equipment effects for reference
|
|
||||||
equipmentBonuses: equipmentEffects.bonuses,
|
equipmentBonuses: equipmentEffects.bonuses,
|
||||||
equipmentMultipliers: equipmentEffects.multipliers,
|
equipmentMultipliers: equipmentEffects.multipliers,
|
||||||
equipmentSpecials: equipmentEffects.specials,
|
equipmentSpecials: equipmentEffects.specials,
|
||||||
|
disciplineBonuses: disciplineEffects.bonuses,
|
||||||
|
disciplineMultipliers: disciplineEffects.multipliers,
|
||||||
|
disciplineSpecials: disciplineEffects.specials,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle special stats that are equipment-only
|
|
||||||
if (equipmentEffects.bonuses.critChance) {
|
if (equipmentEffects.bonuses.critChance) {
|
||||||
merged.critChanceBonus += equipmentEffects.bonuses.critChance;
|
merged.critChanceBonus += equipmentEffects.bonuses.critChance;
|
||||||
}
|
}
|
||||||
if (equipmentEffects.bonuses.meditationEfficiency) {
|
if (equipmentEffects.bonuses.meditationEfficiency) {
|
||||||
// This is a multiplier in equipment, convert to additive for simplicity
|
|
||||||
// Equipment gives +10% per stack, so add it to the base
|
|
||||||
merged.meditationEfficiency *= (equipmentEffects.multipliers.meditationEfficiency || 1);
|
merged.meditationEfficiency *= (equipmentEffects.multipliers.meditationEfficiency || 1);
|
||||||
}
|
}
|
||||||
if (equipmentEffects.bonuses.studySpeed) {
|
if (equipmentEffects.bonuses.studySpeed) {
|
||||||
merged.studySpeedMultiplier *= (equipmentEffects.multipliers.studySpeed || 1);
|
merged.studySpeedMultiplier *= (equipmentEffects.multipliers.studySpeed || 1);
|
||||||
}
|
}
|
||||||
if (equipmentEffects.bonuses.insightGain) {
|
|
||||||
// Store separately - insight multiplier
|
|
||||||
(merged as any).insightGainMultiplier = (equipmentEffects.multipliers.insightGain || 1);
|
|
||||||
}
|
|
||||||
if (equipmentEffects.bonuses.guardianDamage) {
|
|
||||||
(merged as any).guardianDamageMultiplier = (equipmentEffects.multipliers.guardianDamage || 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function getUnifiedEffects(state: Pick<GameState, 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances' | 'disciplines'>): UnifiedEffects {
|
||||||
* Helper to get unified effects from game state
|
|
||||||
*/
|
|
||||||
export function getUnifiedEffects(state: Pick<GameState, 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>): UnifiedEffects {
|
|
||||||
return computeAllEffects(
|
return computeAllEffects(
|
||||||
state.skillUpgrades || {},
|
state.skillUpgrades || {},
|
||||||
state.skillTiers || {},
|
state.skillTiers || {},
|
||||||
state.equipmentInstances || {},
|
state.equipmentInstances || {},
|
||||||
state.equippedInstances || {}
|
state.equippedInstances || {},
|
||||||
|
state as GameState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Stat Computation with All Effects ───────────────────────────────────────
|
// ─── Stat Computation with All Effects ───────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute max mana with all effect sources
|
|
||||||
*/
|
|
||||||
export function computeTotalMaxMana(
|
export function computeTotalMaxMana(
|
||||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||||
effects?: UnifiedEffects
|
effects?: UnifiedEffects
|
||||||
): number {
|
): number {
|
||||||
const pu = state.prestigeUpgrades;
|
const pu = state.prestigeUpgrades;
|
||||||
const skillMult = effects?.skillLevelMultiplier || 1;
|
const skillMult = effects?.skillLevelMultiplier || 1;
|
||||||
const base =
|
const base = 100 + ((state.skills || {}).manaWell || 0) * 100 * skillMult + ((pu || {}).manaWell || 0) * 500;
|
||||||
100 +
|
if (!effects) effects = getUnifiedEffects(state as any);
|
||||||
((state.skills || {}).manaWell || 0) * 100 * skillMult +
|
|
||||||
((pu || {}).manaWell || 0) * 500;
|
|
||||||
|
|
||||||
if (!effects) {
|
|
||||||
effects = getUnifiedEffects(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
|
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute regen with all effect sources
|
|
||||||
*/
|
|
||||||
export function computeTotalRegen(
|
export function computeTotalRegen(
|
||||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||||
effects?: UnifiedEffects
|
effects?: UnifiedEffects
|
||||||
@@ -210,39 +167,19 @@ export function computeTotalRegen(
|
|||||||
const pu = state.prestigeUpgrades;
|
const pu = state.prestigeUpgrades;
|
||||||
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
||||||
const skillMult = effects?.skillLevelMultiplier || 1;
|
const skillMult = effects?.skillLevelMultiplier || 1;
|
||||||
const base =
|
const base = 2 + (state.skills.manaFlow || 0) * 1 * skillMult + (state.skills.manaSpring || 0) * 2 * skillMult + (pu.manaFlow || 0) * 0.5;
|
||||||
2 +
|
|
||||||
(state.skills.manaFlow || 0) * 1 * skillMult +
|
|
||||||
(state.skills.manaSpring || 0) * 2 * skillMult +
|
|
||||||
(pu.manaFlow || 0) * 0.5;
|
|
||||||
|
|
||||||
let regen = base * temporalBonus;
|
let regen = base * temporalBonus;
|
||||||
|
if (!effects) effects = getUnifiedEffects(state as any);
|
||||||
if (!effects) {
|
|
||||||
effects = getUnifiedEffects(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
|
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
|
||||||
|
|
||||||
return regen;
|
return regen;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute click mana with all effect sources
|
|
||||||
*/
|
|
||||||
export function computeTotalClickMana(
|
export function computeTotalClickMana(
|
||||||
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||||
effects?: UnifiedEffects
|
effects?: UnifiedEffects
|
||||||
): number {
|
): number {
|
||||||
const skillMult = effects?.skillLevelMultiplier || 1;
|
const skillMult = effects?.skillLevelMultiplier || 1;
|
||||||
const base =
|
const base = 1 + (state.skills.manaTap || 0) * 1 * skillMult + (state.skills.manaSurge || 0) * 3 * skillMult;
|
||||||
1 +
|
if (!effects) effects = getUnifiedEffects(state as any);
|
||||||
(state.skills.manaTap || 0) * 1 * skillMult +
|
|
||||||
(state.skills.manaSurge || 0) * 3 * skillMult;
|
|
||||||
|
|
||||||
if (!effects) {
|
|
||||||
effects = getUnifiedEffects(state as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
|
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// ─── Discipline Effects ───────────────────────────────────────────────────────
|
||||||
|
// Computes bonuses from active disciplines and integrates with the unified effect system
|
||||||
|
|
||||||
|
import type { GameState } from '../types';
|
||||||
|
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||||
|
import { ALL_DISCIPLINES } from '../data/disciplines';
|
||||||
|
import { calculateStatBonus, getUnlockedPerks } from '../utils/discipline-math';
|
||||||
|
|
||||||
|
export function computeDisciplineEffects(state: GameState): {
|
||||||
|
bonuses: Record<string, number>;
|
||||||
|
multipliers: Record<string, number>;
|
||||||
|
specials: Set<string>;
|
||||||
|
} {
|
||||||
|
const { disciplines } = useDisciplineStore.getState();
|
||||||
|
const activeDiscs = Object.entries(disciplines)
|
||||||
|
.filter(([, disc]) => disc && !disc.paused)
|
||||||
|
.map(([id, disc]) => ({ id, disc, def: ALL_DISCIPLINES.find(d => d.id === id) }))
|
||||||
|
.filter((entry): entry is { id: string; disc: any; def: NonNullable<typeof ALL_DISCIPLINES[0]> } => !!entry.def);
|
||||||
|
|
||||||
|
const bonuses: Record<string, number> = {};
|
||||||
|
const multipliers: Record<string, number> = {};
|
||||||
|
const specials = new Set<string>();
|
||||||
|
|
||||||
|
for (const { disc, def } of activeDiscs) {
|
||||||
|
// Continuous stat bonus
|
||||||
|
const statBonus = calculateStatBonus(def.statBonus.baseValue, disc.xp, def.scalingFactor);
|
||||||
|
if (def.statBonus.stat) {
|
||||||
|
bonuses[def.statBonus.stat] = (bonuses[def.statBonus.stat] || 0) + statBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perk unlocks
|
||||||
|
const perks = getUnlockedPerks(def, disc.xp);
|
||||||
|
for (const perk of perks) {
|
||||||
|
if (perk.type === 'once' || perk.type === 'infinite') {
|
||||||
|
// Once/infinite perks can be treated as additive bonuses or special flags
|
||||||
|
// For simplicity, we add them as a special flag; actual effect depends on perk.id
|
||||||
|
specials.add(perk.id);
|
||||||
|
} else if (perk.type === 'capped') {
|
||||||
|
// Capped perks act as multipliers after certain thresholds
|
||||||
|
// For now, we treat them as additive to a multiplier stat (example)
|
||||||
|
// In a real implementation, each perk would have a specific effect.
|
||||||
|
// Here we just add a generic perk multiplier placeholder.
|
||||||
|
multipliers[`perk_${perk.id}`] = (multipliers[`perk_${perk.id}`] || 1) + perk.value / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { bonuses, multipliers, specials };
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// ─── Discipline Store Slice ────────────────────────────────────────────────────
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
import type { DisciplineState } from '../types/disciplines';
|
||||||
|
import {
|
||||||
|
calculateManaDrain,
|
||||||
|
calculateStatBonus,
|
||||||
|
canProceedDiscipline,
|
||||||
|
} from '../utils/discipline-math';
|
||||||
|
import { baseDisciplines } from '../data/disciplines/base';
|
||||||
|
import { enchanterDisciplines } from '../data/disciplines/enchanter';
|
||||||
|
import { fabricatorDisciplines } from '../data/disciplines/fabricator';
|
||||||
|
import { invokerDisciplines } from '../data/disciplines/invoker';
|
||||||
|
import { MAX_CONCURRENT_DISCIPLINES } from '../types/disciplines';
|
||||||
|
|
||||||
|
const ALL_DISCIPLINES = [
|
||||||
|
...baseDisciplines,
|
||||||
|
...enchanterDisciplines,
|
||||||
|
...fabricatorDisciplines,
|
||||||
|
...invokerDisciplines,
|
||||||
|
];
|
||||||
|
const DISCIPLINE_MAP = Object.fromEntries(ALL_DISCIPLINES.map((d) => [d.id, d]));
|
||||||
|
|
||||||
|
export interface DisciplineStoreState {
|
||||||
|
disciplines: Record<string, DisciplineState>;
|
||||||
|
activeIds: string[];
|
||||||
|
concurrentLimit: number;
|
||||||
|
totalXP: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisciplineStoreActions {
|
||||||
|
activate: (id: string, gameState?: { elements?: Record<string, { unlocked?: boolean }> }) => void;
|
||||||
|
deactivate: (id: string) => void;
|
||||||
|
processTick: (mana: { rawMana: number; elements: Record<string, { current: number }> }) => {
|
||||||
|
rawMana: number;
|
||||||
|
elements: Record<string, { current: number }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DisciplineStore = DisciplineStoreState & DisciplineStoreActions;
|
||||||
|
|
||||||
|
export const useDisciplineStore = create<DisciplineStore>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
disciplines: {},
|
||||||
|
activeIds: [],
|
||||||
|
concurrentLimit: MAX_CONCURRENT_DISCIPLINES,
|
||||||
|
totalXP: 0,
|
||||||
|
|
||||||
|
activate(id, gameState) {
|
||||||
|
set((s) => {
|
||||||
|
const def = DISCIPLINE_MAP[id];
|
||||||
|
if (!def) return s;
|
||||||
|
if (s.activeIds.includes(id)) return s;
|
||||||
|
|
||||||
|
const nonPaused = s.activeIds.filter((aid) => {
|
||||||
|
const d = s.disciplines[aid];
|
||||||
|
return d && !d.paused;
|
||||||
|
}).length;
|
||||||
|
if (nonPaused >= s.concurrentLimit) return s;
|
||||||
|
if (!canProceedDiscipline(id, gameState)) return s;
|
||||||
|
|
||||||
|
const existing = s.disciplines[id] || { id, xp: 0, paused: false };
|
||||||
|
return {
|
||||||
|
disciplines: { ...s.disciplines, [id]: { ...existing, paused: false } },
|
||||||
|
activeIds: [...s.activeIds, id],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deactivate(id) {
|
||||||
|
set((s) => ({
|
||||||
|
activeIds: s.activeIds.filter((aid) => aid !== id),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
processTick(mana) {
|
||||||
|
const s = get();
|
||||||
|
let rawMana = mana.rawMana;
|
||||||
|
const elements = { ...mana.elements };
|
||||||
|
let newXP = s.totalXP;
|
||||||
|
|
||||||
|
for (const id of s.activeIds) {
|
||||||
|
const disc = s.disciplines[id];
|
||||||
|
if (!disc) continue;
|
||||||
|
if (disc.paused) continue;
|
||||||
|
|
||||||
|
const def = DISCIPLINE_MAP[id];
|
||||||
|
if (!def) continue;
|
||||||
|
|
||||||
|
const drain = calculateManaDrain(def.drainBase, disc.xp, def.difficultyFactor);
|
||||||
|
const element = elements[def.manaType];
|
||||||
|
const available = def.manaType === 'raw' ? rawMana : element?.current;
|
||||||
|
|
||||||
|
if (!available || available < drain) {
|
||||||
|
disc.paused = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def.manaType === 'raw') {
|
||||||
|
rawMana -= drain;
|
||||||
|
} else {
|
||||||
|
elements[def.manaType].current -= drain;
|
||||||
|
}
|
||||||
|
|
||||||
|
disc.xp += 1;
|
||||||
|
newXP += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLimit = Math.min(
|
||||||
|
MAX_CONCURRENT_DISCIPLINES + Math.floor(newXP / 500),
|
||||||
|
MAX_CONCURRENT_DISCIPLINES + 3
|
||||||
|
);
|
||||||
|
|
||||||
|
set({
|
||||||
|
disciplines: s.disciplines,
|
||||||
|
totalXP: newXP,
|
||||||
|
concurrentLimit: Math.max(s.concurrentLimit, newLimit),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { rawMana, elements };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: 'mana-loop-discipline-store' }
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// ─── Discipline Types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { ManaType } from './elements';
|
||||||
|
|
||||||
|
// ─── Attunement Types ─────────────────────────────────────────────────────────
|
||||||
|
export enum DisciplinesAttunementType {
|
||||||
|
BASE = 'base',
|
||||||
|
ENCHANTER = 'enchanter',
|
||||||
|
FABRICATOR = 'fabricator',
|
||||||
|
INVOKER = 'invoker',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Perk Types ───────────────────────────────────────────────────────────────
|
||||||
|
export type PerkType = 'capped' | 'once' | 'infinite';
|
||||||
|
|
||||||
|
export interface DisciplinePerk {
|
||||||
|
id: string;
|
||||||
|
type: PerkType;
|
||||||
|
threshold: number;
|
||||||
|
value: number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Discipline Definition ────────────────────────────────────────────────────
|
||||||
|
export interface DisciplineDefinition {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
attunement: DisciplinesAttunementType;
|
||||||
|
manaType: ManaType;
|
||||||
|
baseCost: number;
|
||||||
|
description: string;
|
||||||
|
statBonus: { stat: string; baseValue: number };
|
||||||
|
difficultyFactor: number;
|
||||||
|
scalingFactor: number;
|
||||||
|
drainBase: number;
|
||||||
|
perks: DisciplinePerk[];
|
||||||
|
requires?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Discipline State ─────────────────────────────────────────────────────────
|
||||||
|
export interface DisciplineState {
|
||||||
|
id: string;
|
||||||
|
xp: number;
|
||||||
|
paused: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Discipline Constants ─────────────────────────────────────────────────────
|
||||||
|
export const MAX_CONCURRENT_DISCIPLINES = 1;
|
||||||
|
export const BASE_CONCURRENT_DISCIPLINES = 1;
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// ─── Discipline Math Utilities ────────────────────────────────────────────────
|
||||||
|
// Continuous scaling formulas for Active Disciplines
|
||||||
|
|
||||||
|
import type { DisciplineState, DisciplineDefinition } from '../types/disciplines';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate continuous stat bonus from discipline XP
|
||||||
|
* StatBonus = BaseValue * (XP / ScalingFactor)^0.65
|
||||||
|
*/
|
||||||
|
export function calculateStatBonus(
|
||||||
|
baseValue: number,
|
||||||
|
xp: number,
|
||||||
|
scalingFactor: number
|
||||||
|
): number {
|
||||||
|
if (xp <= 0) return 0;
|
||||||
|
const ratio = xp / scalingFactor;
|
||||||
|
const power = Math.pow(ratio, 0.65);
|
||||||
|
return baseValue * power;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate dynamic mana drain per tick
|
||||||
|
* ManaDrainPerTick = BaseDrain * (1 + (XP / DifficultyFactor)^0.4)
|
||||||
|
*/
|
||||||
|
export function calculateManaDrain(
|
||||||
|
baseDrain: number,
|
||||||
|
xp: number,
|
||||||
|
difficultyFactor: number
|
||||||
|
): number {
|
||||||
|
if (xp <= 0) return baseDrain;
|
||||||
|
const ratio = xp / difficultyFactor;
|
||||||
|
const power = Math.pow(ratio, 0.4);
|
||||||
|
return baseDrain * (1 + power);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate infinite perk tier
|
||||||
|
* PerkTier = Math.max(0, Math.floor((XP - Threshold) / Interval) + 1)
|
||||||
|
*/
|
||||||
|
export function calculatePerkTier(
|
||||||
|
xp: number,
|
||||||
|
threshold: number,
|
||||||
|
interval: number
|
||||||
|
): number {
|
||||||
|
if (xp < threshold) return 0;
|
||||||
|
const excess = xp - threshold;
|
||||||
|
return Math.max(0, Math.floor(excess / interval) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if discipline can be activated (has required mana type)
|
||||||
|
*/
|
||||||
|
export function canActivateDiscipline(
|
||||||
|
discipline: DisciplineDefinition,
|
||||||
|
gameState: { elements?: Record<string, any> }
|
||||||
|
): boolean {
|
||||||
|
if (discipline.manaType === 'raw') return true;
|
||||||
|
const element = gameState.elements?.[discipline.manaType];
|
||||||
|
return element && element.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if discipline can proceed (has sufficient mana for drain)
|
||||||
|
*/
|
||||||
|
export function canProceedDiscipline(
|
||||||
|
discipline: DisciplineDefinition,
|
||||||
|
disciplineState: DisciplineState,
|
||||||
|
gameState: { elements?: Record<string, any>; rawMana?: number }
|
||||||
|
): boolean {
|
||||||
|
if (disciplineState.paused) return false;
|
||||||
|
|
||||||
|
const drain = calculateManaDrain(
|
||||||
|
discipline.drainBase,
|
||||||
|
disciplineState.xp,
|
||||||
|
discipline.difficultyFactor
|
||||||
|
);
|
||||||
|
|
||||||
|
if (discipline.manaType === 'raw') {
|
||||||
|
return (gameState.rawMana || 0) >= drain;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = gameState.elements?.[discipline.manaType];
|
||||||
|
return element && element.current >= drain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unlocked perks for a discipline
|
||||||
|
*/
|
||||||
|
export function getUnlockedPerks(
|
||||||
|
discipline: DisciplineDefinition,
|
||||||
|
xp: number
|
||||||
|
): DisciplinePerk[] {
|
||||||
|
return discipline.perks.filter(perk => {
|
||||||
|
if (perk.type === 'once') {
|
||||||
|
return xp >= perk.threshold;
|
||||||
|
} else if (perk.type === 'capped') {
|
||||||
|
const tier = calculatePerkTier(xp, perk.threshold, perk.value);
|
||||||
|
return tier > 0;
|
||||||
|
} else if (perk.type === 'infinite') {
|
||||||
|
return xp >= perk.threshold;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total stats from all active disciplines
|
||||||
|
*/
|
||||||
|
export function calculateDisciplineStats(
|
||||||
|
disciplines: DisciplineDefinition[],
|
||||||
|
states: DisciplineState[]
|
||||||
|
): Record<string, number> {
|
||||||
|
const stats: Record<string, number> = {};
|
||||||
|
|
||||||
|
disciplines.forEach((discipline, index) => {
|
||||||
|
const state = states[index];
|
||||||
|
if (!state || state.paused) return;
|
||||||
|
|
||||||
|
const bonus = calculateStatBonus(
|
||||||
|
discipline.statBonus.baseValue,
|
||||||
|
state.xp,
|
||||||
|
discipline.scalingFactor
|
||||||
|
);
|
||||||
|
|
||||||
|
const statKey = discipline.statBonus.stat;
|
||||||
|
stats[statKey] = (stats[statKey] || 0) + bonus;
|
||||||
|
});
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user