feat: discipline UI improvements - stat labels, prerequisites, mana type tab
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
- Add player-friendly label field to statBonus in DisciplineDefinition - Show prerequisite requirements on locked discipline cards - Disable activate button for locked disciplines - Restructure elemental attunement into dedicated 'Mana Types' tab - Add checkDisciplinePrerequisites utility function - Update store to enforce prerequisite checking on activation - Split discipline-prerequisites tests into separate file
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-25T10:43:13.495Z
|
Generated: 2026-05-25T10:50:06.358Z
|
||||||
Found: 6 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 6 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. Processed 134 files (1.4s) (2 warnings)
|
1. Processed 134 files (1.4s) (2 warnings)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-25T10:43:11.889Z",
|
"generated": "2026-05-25T10:50:04.757Z",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ Mana-Loop/
|
|||||||
│ │ │ ├── crafting-utils-recipe.test.ts
|
│ │ │ ├── crafting-utils-recipe.test.ts
|
||||||
│ │ │ ├── crafting-utils-time.test.ts
|
│ │ │ ├── crafting-utils-time.test.ts
|
||||||
│ │ │ ├── discipline-math.test.ts
|
│ │ │ ├── discipline-math.test.ts
|
||||||
|
│ │ │ ├── discipline-prerequisites.test.ts
|
||||||
│ │ │ ├── enemy-generator.test.ts
|
│ │ │ ├── enemy-generator.test.ts
|
||||||
│ │ │ ├── enemy-utils.test.ts
|
│ │ │ ├── enemy-utils.test.ts
|
||||||
│ │ │ ├── floor-utils.test.ts
|
│ │ │ ├── floor-utils.test.ts
|
||||||
@@ -248,6 +249,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── base.ts
|
│ │ │ │ ├── base.ts
|
||||||
│ │ │ │ ├── elemental-regen-advanced.ts
|
│ │ │ │ ├── elemental-regen-advanced.ts
|
||||||
│ │ │ │ ├── elemental-regen.ts
|
│ │ │ │ ├── elemental-regen.ts
|
||||||
|
│ │ │ │ ├── elemental.ts
|
||||||
│ │ │ │ ├── enchanter-special.ts
|
│ │ │ │ ├── enchanter-special.ts
|
||||||
│ │ │ │ ├── enchanter-spells.ts
|
│ │ │ │ ├── enchanter-spells.ts
|
||||||
│ │ │ │ ├── enchanter-utility.ts
|
│ │ │ │ ├── enchanter-utility.ts
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
|||||||
import type { ManaType } from '@/lib/game/types/elements';
|
import type { ManaType } from '@/lib/game/types/elements';
|
||||||
import { ELEMENTS } from '@/lib/game/constants/elements';
|
import { ELEMENTS } from '@/lib/game/constants/elements';
|
||||||
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
|
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
|
||||||
|
import { elementalAttunementDisciplines } from '@/lib/game/data/disciplines/elemental';
|
||||||
import { elementalRegenDisciplines } from '@/lib/game/data/disciplines/elemental-regen';
|
import { elementalRegenDisciplines } from '@/lib/game/data/disciplines/elemental-regen';
|
||||||
import { elementalRegenAdvancedDisciplines } from '@/lib/game/data/disciplines/elemental-regen-advanced';
|
import { elementalRegenAdvancedDisciplines } from '@/lib/game/data/disciplines/elemental-regen-advanced';
|
||||||
import { enchanterDisciplines } from '@/lib/game/data/disciplines/enchanter';
|
import { enchanterDisciplines } from '@/lib/game/data/disciplines/enchanter';
|
||||||
import { fabricatorDisciplines } from '@/lib/game/data/disciplines/fabricator';
|
import { fabricatorDisciplines } from '@/lib/game/data/disciplines/fabricator';
|
||||||
import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker';
|
import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker';
|
||||||
import { calculateStatBonus, calculateManaDrain } 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 clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
// ─── Attunement Tabs ─────────────────────────────────────────────────────────
|
// ─── Attunement Tabs ─────────────────────────────────────────────────────────
|
||||||
@@ -22,6 +24,7 @@ interface AttunementTab {
|
|||||||
|
|
||||||
const ATTUNEMENT_TABS: AttunementTab[] = [
|
const ATTUNEMENT_TABS: AttunementTab[] = [
|
||||||
{ key: 'base', label: 'Base', items: baseDisciplines },
|
{ key: 'base', label: 'Base', items: baseDisciplines },
|
||||||
|
{ key: 'elements', label: 'Mana Types', items: elementalAttunementDisciplines },
|
||||||
{ key: 'elemental-regen', label: 'Elemental Regen', items: elementalRegenDisciplines },
|
{ key: 'elemental-regen', label: 'Elemental Regen', items: elementalRegenDisciplines },
|
||||||
{ key: 'elemental-regen-advanced', label: 'Advanced Regen', items: elementalRegenAdvancedDisciplines },
|
{ key: 'elemental-regen-advanced', label: 'Advanced Regen', items: elementalRegenAdvancedDisciplines },
|
||||||
{ key: 'enchanter', label: 'Enchanter', items: enchanterDisciplines },
|
{ key: 'enchanter', label: 'Enchanter', items: enchanterDisciplines },
|
||||||
@@ -41,6 +44,8 @@ export interface DisciplineCardDefinition {
|
|||||||
perkValues?: number[];
|
perkValues?: number[];
|
||||||
perkTypes?: string[];
|
perkTypes?: string[];
|
||||||
statBonus: string;
|
statBonus: string;
|
||||||
|
statBonusLabel: string;
|
||||||
|
requires?: string[];
|
||||||
baseValue: number;
|
baseValue: number;
|
||||||
drainBase: number;
|
drainBase: number;
|
||||||
difficultyFactor: number;
|
difficultyFactor: number;
|
||||||
@@ -51,6 +56,8 @@ export interface DisciplineCardRuntime {
|
|||||||
xp: number;
|
xp: number;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
concurrentLimit: number;
|
concurrentLimit: number;
|
||||||
|
isLocked: boolean;
|
||||||
|
missingPrereqs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DisciplineCardCallbacks {
|
export interface DisciplineCardCallbacks {
|
||||||
@@ -68,9 +75,9 @@ interface DisciplineCardProps {
|
|||||||
const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, callbacks }) => {
|
const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, callbacks }) => {
|
||||||
const {
|
const {
|
||||||
id, name, description, manaType, baseCost, perkThresholds, perkValues, perkTypes,
|
id, name, description, manaType, baseCost, perkThresholds, perkValues, perkTypes,
|
||||||
statBonus, baseValue, drainBase, difficultyFactor, scalingFactor,
|
statBonus, statBonusLabel, baseValue, drainBase, difficultyFactor, scalingFactor,
|
||||||
} = definition;
|
} = definition;
|
||||||
const { xp, paused: isPaused, concurrentLimit } = runtime;
|
const { xp, paused: isPaused, concurrentLimit, isLocked, missingPrereqs } = runtime;
|
||||||
const { onToggle } = callbacks;
|
const { onToggle } = callbacks;
|
||||||
|
|
||||||
const displayXp = xp;
|
const displayXp = xp;
|
||||||
@@ -102,7 +109,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={id} className="border rounded-lg p-4 shadow-sm space-y-3">
|
<div key={id} className={clsx('border rounded-lg p-4 shadow-sm space-y-3', isLocked && 'opacity-60 border-gray-600')}>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h3 className="text-lg font-medium">{name}</h3>
|
<h3 className="text-lg font-medium">{name}</h3>
|
||||||
<span
|
<span
|
||||||
@@ -143,7 +150,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 text-sm">
|
<div className="mt-2 text-sm">
|
||||||
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonus}
|
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonusLabel}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@@ -159,17 +166,26 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isLocked && missingPrereqs.length > 0 && (
|
||||||
|
<div className="mt-2 text-xs text-red-400">
|
||||||
|
<strong>Requires:</strong> {missingPrereqs.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-4 flex justify-end">
|
<div className="mt-4 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={toggleAction}
|
onClick={toggleAction}
|
||||||
|
disabled={isLocked}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded px-3 py-1 text-sm font-medium',
|
'rounded px-3 py-1 text-sm font-medium',
|
||||||
isPaused
|
isLocked
|
||||||
|
? 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
||||||
|
: isPaused
|
||||||
? 'bg-yellow-600 text-white hover:bg-yellow-500'
|
? 'bg-yellow-600 text-white hover:bg-yellow-500'
|
||||||
: 'bg-blue-600 text-white hover:bg-blue-500',
|
: 'bg-blue-600 text-white hover:bg-blue-500',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isPaused ? 'Start Practicing' : 'Stop Practicing'}
|
{isLocked ? 'Locked' : isPaused ? 'Start Practicing' : 'Stop Practicing'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,6 +251,7 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{activeTab?.items.map((disc) => {
|
{activeTab?.items.map((disc) => {
|
||||||
const discState = disciplines[disc.id] ?? { xp: 0, paused: true };
|
const discState = disciplines[disc.id] ?? { xp: 0, paused: true };
|
||||||
|
const prereqCheck = checkDisciplinePrerequisites(disc, disciplines, ALL_DISCIPLINES);
|
||||||
return (
|
return (
|
||||||
<DisciplineCard
|
<DisciplineCard
|
||||||
key={disc.id}
|
key={disc.id}
|
||||||
@@ -248,6 +265,8 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
manaType: disc.manaType,
|
manaType: disc.manaType,
|
||||||
baseCost: disc.baseCost,
|
baseCost: disc.baseCost,
|
||||||
statBonus: disc.statBonus.stat,
|
statBonus: disc.statBonus.stat,
|
||||||
|
statBonusLabel: disc.statBonus.label,
|
||||||
|
requires: disc.requires,
|
||||||
baseValue: disc.statBonus.baseValue,
|
baseValue: disc.statBonus.baseValue,
|
||||||
drainBase: disc.drainBase,
|
drainBase: disc.drainBase,
|
||||||
difficultyFactor: disc.difficultyFactor,
|
difficultyFactor: disc.difficultyFactor,
|
||||||
@@ -257,6 +276,8 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
xp: discState.xp,
|
xp: discState.xp,
|
||||||
paused: discState.paused,
|
paused: discState.paused,
|
||||||
concurrentLimit,
|
concurrentLimit,
|
||||||
|
isLocked: !prereqCheck.canProceed,
|
||||||
|
missingPrereqs: prereqCheck.missingPrereqs,
|
||||||
}}
|
}}
|
||||||
callbacks={{
|
callbacks={{
|
||||||
onToggle: handleToggle,
|
onToggle: handleToggle,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const rawMastery: DisciplineDefinition = {
|
|||||||
manaType: 'raw',
|
manaType: 'raw',
|
||||||
baseCost: 5,
|
baseCost: 5,
|
||||||
description: 'Learn to harness raw mana more efficiently.',
|
description: 'Learn to harness raw mana more efficiently.',
|
||||||
statBonus: { stat: 'maxManaBonus', baseValue: 10 },
|
statBonus: { stat: 'maxManaBonus', baseValue: 10, label: 'Max Mana Bonus' },
|
||||||
difficultyFactor: 100,
|
difficultyFactor: 100,
|
||||||
scalingFactor: 50,
|
scalingFactor: 50,
|
||||||
drainBase: 1,
|
drainBase: 1,
|
||||||
@@ -42,14 +42,14 @@ const rawMastery: DisciplineDefinition = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const elementalAttunement: DisciplineDefinition = {
|
const attuneFire: DisciplineDefinition = {
|
||||||
id: 'elemental-attunement',
|
id: 'attune-fire',
|
||||||
name: 'Elemental Attunement',
|
name: 'Fire Attunement',
|
||||||
attunement: DisciplinesAttunementType.BASE,
|
attunement: DisciplinesAttunementType.BASE,
|
||||||
manaType: 'fire',
|
manaType: 'fire',
|
||||||
baseCost: 10,
|
baseCost: 10,
|
||||||
description: 'Begin focusing raw mana into fire.',
|
description: 'Begin focusing raw mana into fire.',
|
||||||
statBonus: { stat: 'elementCap_fire', baseValue: 5 },
|
statBonus: { stat: 'elementCap_fire', baseValue: 5, label: 'Fire Element Cap' },
|
||||||
difficultyFactor: 150,
|
difficultyFactor: 150,
|
||||||
scalingFactor: 75,
|
scalingFactor: 75,
|
||||||
drainBase: 2,
|
drainBase: 2,
|
||||||
@@ -71,7 +71,7 @@ const cappedPerkDiscipline: DisciplineDefinition = {
|
|||||||
manaType: 'raw',
|
manaType: 'raw',
|
||||||
baseCost: 1,
|
baseCost: 1,
|
||||||
description: 'Test discipline with capped perk.',
|
description: 'Test discipline with capped perk.',
|
||||||
statBonus: { stat: 'testStat', baseValue: 1 },
|
statBonus: { stat: 'testStat', baseValue: 1, label: 'Test Stat' },
|
||||||
difficultyFactor: 100,
|
difficultyFactor: 100,
|
||||||
scalingFactor: 100,
|
scalingFactor: 100,
|
||||||
drainBase: 1,
|
drainBase: 1,
|
||||||
@@ -196,28 +196,28 @@ describe('canActivateDiscipline', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when required element is unlocked', () => {
|
it('should return true when required element is unlocked', () => {
|
||||||
const result = canActivateDiscipline(elementalAttunement, {
|
const result = canActivateDiscipline(attuneFire, {
|
||||||
elements: { fire: { unlocked: true } },
|
elements: { fire: { unlocked: true } },
|
||||||
});
|
});
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when required element is locked', () => {
|
it('should return false when required element is locked', () => {
|
||||||
const result = canActivateDiscipline(elementalAttunement, {
|
const result = canActivateDiscipline(attuneFire, {
|
||||||
elements: { fire: { unlocked: false } },
|
elements: { fire: { unlocked: false } },
|
||||||
});
|
});
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return falsy when required element does not exist', () => {
|
it('should return falsy when required element does not exist', () => {
|
||||||
const result = canActivateDiscipline(elementalAttunement, {
|
const result = canActivateDiscipline(attuneFire, {
|
||||||
elements: {},
|
elements: {},
|
||||||
});
|
});
|
||||||
expect(result).toBeFalsy();
|
expect(result).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return falsy when elements is undefined', () => {
|
it('should return falsy when elements is undefined', () => {
|
||||||
const result = canActivateDiscipline(elementalAttunement, {});
|
const result = canActivateDiscipline(attuneFire, {});
|
||||||
expect(result).toBeFalsy();
|
expect(result).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -255,16 +255,16 @@ describe('canProceedDiscipline', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when element mana is sufficient', () => {
|
it('should return true when element mana is sufficient', () => {
|
||||||
const state: DisciplineState = { id: 'elemental-attunement', xp: 0, paused: false };
|
const state: DisciplineState = { id: 'attune-fire', xp: 0, paused: false };
|
||||||
const result = canProceedDiscipline(elementalAttunement, state, {
|
const result = canProceedDiscipline(attuneFire, state, {
|
||||||
elements: { fire: { current: 100, max: 100, unlocked: true } },
|
elements: { fire: { current: 100, max: 100, unlocked: true } },
|
||||||
});
|
});
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when element mana is insufficient', () => {
|
it('should return false when element mana is insufficient', () => {
|
||||||
const state: DisciplineState = { id: 'elemental-attunement', xp: 10000, paused: false };
|
const state: DisciplineState = { id: 'attune-fire', xp: 10000, paused: false };
|
||||||
const result = canProceedDiscipline(elementalAttunement, state, {
|
const result = canProceedDiscipline(attuneFire, state, {
|
||||||
elements: { fire: { current: 0, max: 100, unlocked: true } },
|
elements: { fire: { current: 0, max: 100, unlocked: true } },
|
||||||
});
|
});
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
@@ -340,10 +340,10 @@ describe('calculateDisciplineStats', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sum stats from multiple active disciplines', () => {
|
it('should sum stats from multiple active disciplines', () => {
|
||||||
const disciplines = [rawMastery, elementalAttunement];
|
const disciplines = [rawMastery, attuneFire];
|
||||||
const states: DisciplineState[] = [
|
const states: DisciplineState[] = [
|
||||||
{ id: 'raw-mastery', xp: 50, paused: false },
|
{ id: 'raw-mastery', xp: 50, paused: false },
|
||||||
{ id: 'elemental-attunement', xp: 75, paused: false },
|
{ id: 'attune-fire', xp: 75, paused: false },
|
||||||
];
|
];
|
||||||
const result = calculateDisciplineStats(disciplines, states);
|
const result = calculateDisciplineStats(disciplines, states);
|
||||||
expect(result.maxManaBonus).toBeGreaterThan(0);
|
expect(result.maxManaBonus).toBeGreaterThan(0);
|
||||||
@@ -351,10 +351,10 @@ describe('calculateDisciplineStats', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip paused disciplines', () => {
|
it('should skip paused disciplines', () => {
|
||||||
const disciplines = [rawMastery, elementalAttunement];
|
const disciplines = [rawMastery, attuneFire];
|
||||||
const states: DisciplineState[] = [
|
const states: DisciplineState[] = [
|
||||||
{ id: 'raw-mastery', xp: 50, paused: false },
|
{ id: 'raw-mastery', xp: 50, paused: false },
|
||||||
{ id: 'elemental-attunement', xp: 75, paused: true },
|
{ id: 'attune-fire', xp: 75, paused: true },
|
||||||
];
|
];
|
||||||
const result = calculateDisciplineStats(disciplines, states);
|
const result = calculateDisciplineStats(disciplines, states);
|
||||||
expect(result.maxManaBonus).toBeGreaterThan(0);
|
expect(result.maxManaBonus).toBeGreaterThan(0);
|
||||||
@@ -362,7 +362,7 @@ describe('calculateDisciplineStats', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing state for a discipline', () => {
|
it('should handle missing state for a discipline', () => {
|
||||||
const disciplines = [rawMastery, elementalAttunement];
|
const disciplines = [rawMastery, attuneFire];
|
||||||
const states: DisciplineState[] = [
|
const states: DisciplineState[] = [
|
||||||
{ id: 'raw-mastery', xp: 50, paused: false },
|
{ id: 'raw-mastery', xp: 50, paused: false },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { checkDisciplinePrerequisites } from '../utils/discipline-math';
|
||||||
|
import { DisciplinesAttunementType } from '../types/disciplines';
|
||||||
|
import type { DisciplineDefinition, DisciplineState } from '../types/disciplines';
|
||||||
|
|
||||||
|
// ─── Test Fixtures ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const rawMastery: DisciplineDefinition = {
|
||||||
|
id: 'raw-mastery',
|
||||||
|
name: 'Raw Mana Mastery',
|
||||||
|
attunement: DisciplinesAttunementType.BASE,
|
||||||
|
manaType: 'raw',
|
||||||
|
baseCost: 5,
|
||||||
|
description: 'Learn to harness raw mana more efficiently.',
|
||||||
|
statBonus: { stat: 'maxManaBonus', baseValue: 10, label: 'Max Mana Bonus' },
|
||||||
|
difficultyFactor: 100,
|
||||||
|
scalingFactor: 50,
|
||||||
|
drainBase: 1,
|
||||||
|
perks: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const attuneFire: DisciplineDefinition = {
|
||||||
|
id: 'attune-fire',
|
||||||
|
name: 'Fire Attunement',
|
||||||
|
attunement: DisciplinesAttunementType.BASE,
|
||||||
|
manaType: 'fire',
|
||||||
|
baseCost: 10,
|
||||||
|
description: 'Begin focusing raw mana into fire.',
|
||||||
|
statBonus: { stat: 'elementCap_fire', baseValue: 5, label: 'Fire Mana Capacity' },
|
||||||
|
difficultyFactor: 150,
|
||||||
|
scalingFactor: 75,
|
||||||
|
drainBase: 2,
|
||||||
|
perks: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── checkDisciplinePrerequisites ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('checkDisciplinePrerequisites', () => {
|
||||||
|
it('should return canProceed true when no requires field', () => {
|
||||||
|
const result = checkDisciplinePrerequisites(rawMastery, {}, [rawMastery]);
|
||||||
|
expect(result.canProceed).toBe(true);
|
||||||
|
expect(result.missingPrereqs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return canProceed true when prerequisite is met (xp > 0)', () => {
|
||||||
|
const disciplines: Record<string, DisciplineState> = {
|
||||||
|
'raw-mastery': { id: 'raw-mastery', xp: 100, paused: false },
|
||||||
|
};
|
||||||
|
const prereqDisc: DisciplineDefinition = {
|
||||||
|
...rawMastery,
|
||||||
|
id: 'prereq-test',
|
||||||
|
requires: ['raw-mastery'],
|
||||||
|
};
|
||||||
|
const result = checkDisciplinePrerequisites(prereqDisc, disciplines, [rawMastery]);
|
||||||
|
expect(result.canProceed).toBe(true);
|
||||||
|
expect(result.missingPrereqs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return canProceed false when prerequisite has xp = 0', () => {
|
||||||
|
const disciplines: Record<string, DisciplineState> = {
|
||||||
|
'raw-mastery': { id: 'raw-mastery', xp: 0, paused: true },
|
||||||
|
};
|
||||||
|
const prereqDisc: DisciplineDefinition = {
|
||||||
|
...rawMastery,
|
||||||
|
id: 'prereq-test',
|
||||||
|
requires: ['raw-mastery'],
|
||||||
|
};
|
||||||
|
const result = checkDisciplinePrerequisites(prereqDisc, disciplines, [rawMastery]);
|
||||||
|
expect(result.canProceed).toBe(false);
|
||||||
|
expect(result.missingPrereqs).toContain('Raw Mana Mastery');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return canProceed false when prerequisite state is missing', () => {
|
||||||
|
const disciplines: Record<string, DisciplineState> = {};
|
||||||
|
const prereqDisc: DisciplineDefinition = {
|
||||||
|
...rawMastery,
|
||||||
|
id: 'prereq-test',
|
||||||
|
requires: ['raw-mastery'],
|
||||||
|
};
|
||||||
|
const result = checkDisciplinePrerequisites(prereqDisc, disciplines, [rawMastery]);
|
||||||
|
expect(result.canProceed).toBe(false);
|
||||||
|
expect(result.missingPrereqs).toContain('Raw Mana Mastery');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return missing prereq ID when definition not found in allDefinitions', () => {
|
||||||
|
const disciplines: Record<string, DisciplineState> = {};
|
||||||
|
const prereqDisc: DisciplineDefinition = {
|
||||||
|
...rawMastery,
|
||||||
|
id: 'prereq-test',
|
||||||
|
requires: ['nonexistent-discipline'],
|
||||||
|
};
|
||||||
|
const result = checkDisciplinePrerequisites(prereqDisc, disciplines, [rawMastery]);
|
||||||
|
expect(result.canProceed).toBe(false);
|
||||||
|
expect(result.missingPrereqs).toContain('nonexistent-discipline');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple prerequisites - all met', () => {
|
||||||
|
const disciplines: Record<string, DisciplineState> = {
|
||||||
|
'raw-mastery': { id: 'raw-mastery', xp: 100, paused: false },
|
||||||
|
'attune-fire': { id: 'attune-fire', xp: 50, paused: false },
|
||||||
|
};
|
||||||
|
const prereqDisc: DisciplineDefinition = {
|
||||||
|
...rawMastery,
|
||||||
|
id: 'multi-prereq',
|
||||||
|
requires: ['raw-mastery', 'attune-fire'],
|
||||||
|
};
|
||||||
|
const allDefs = [rawMastery, attuneFire];
|
||||||
|
const result = checkDisciplinePrerequisites(prereqDisc, disciplines, allDefs);
|
||||||
|
expect(result.canProceed).toBe(true);
|
||||||
|
expect(result.missingPrereqs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple prerequisites - some missing', () => {
|
||||||
|
const disciplines: Record<string, DisciplineState> = {
|
||||||
|
'raw-mastery': { id: 'raw-mastery', xp: 100, paused: false },
|
||||||
|
};
|
||||||
|
const prereqDisc: DisciplineDefinition = {
|
||||||
|
...rawMastery,
|
||||||
|
id: 'multi-prereq',
|
||||||
|
requires: ['raw-mastery', 'attune-fire'],
|
||||||
|
};
|
||||||
|
const allDefs = [rawMastery, attuneFire];
|
||||||
|
const result = checkDisciplinePrerequisites(prereqDisc, disciplines, allDefs);
|
||||||
|
expect(result.canProceed).toBe(false);
|
||||||
|
expect(result.missingPrereqs.length).toBe(1);
|
||||||
|
expect(result.missingPrereqs).toContain('Fire Attunement');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty requires array', () => {
|
||||||
|
const prereqDisc: DisciplineDefinition = {
|
||||||
|
...rawMastery,
|
||||||
|
id: 'empty-prereqs',
|
||||||
|
requires: [],
|
||||||
|
};
|
||||||
|
const result = checkDisciplinePrerequisites(prereqDisc, {}, [rawMastery]);
|
||||||
|
expect(result.canProceed).toBe(true);
|
||||||
|
expect(result.missingPrereqs).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,16 +28,16 @@ describe('DisciplineStore', () => {
|
|||||||
|
|
||||||
it('should not activate when concurrent limit reached', () => {
|
it('should not activate when concurrent limit reached', () => {
|
||||||
useDisciplineStore.getState().activate('raw-mastery');
|
useDisciplineStore.getState().activate('raw-mastery');
|
||||||
useDisciplineStore.getState().activate('elemental-attunement');
|
useDisciplineStore.getState().activate('attune-fire');
|
||||||
expect(useDisciplineStore.getState().activeIds.length).toBe(1);
|
expect(useDisciplineStore.getState().activeIds.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should activate when no prior discipline state (optimistic)', () => {
|
it('should activate when no prior discipline state (optimistic)', () => {
|
||||||
// canProceedDiscipline returns true when disciplineState is undefined
|
// canProceedDiscipline returns true when disciplineState is undefined
|
||||||
useDisciplineStore.getState().activate('elemental-attunement', {
|
useDisciplineStore.getState().activate('attune-fire', {
|
||||||
elements: { fire: { unlocked: false } },
|
elements: { fire: { unlocked: false, current: 100, max: 100 } },
|
||||||
});
|
});
|
||||||
expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement');
|
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not activate when existing state has insufficient mana', () => {
|
it('should not activate when existing state has insufficient mana', () => {
|
||||||
@@ -49,10 +49,10 @@ describe('DisciplineStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should activate when required element is unlocked', () => {
|
it('should activate when required element is unlocked', () => {
|
||||||
useDisciplineStore.getState().activate('elemental-attunement', {
|
useDisciplineStore.getState().activate('attune-fire', {
|
||||||
elements: { fire: { unlocked: true } },
|
elements: { fire: { unlocked: true, current: 100, max: 100 } },
|
||||||
});
|
});
|
||||||
expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement');
|
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const baseDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'raw',
|
manaType: 'raw',
|
||||||
baseCost: 5,
|
baseCost: 5,
|
||||||
description: 'Learn to harness raw mana more efficiently.',
|
description: 'Learn to harness raw mana more efficiently.',
|
||||||
statBonus: { stat: 'maxManaBonus', baseValue: 10 },
|
statBonus: { stat: 'maxManaBonus', baseValue: 10, label: 'Max Mana' },
|
||||||
difficultyFactor: 100,
|
difficultyFactor: 100,
|
||||||
scalingFactor: 50,
|
scalingFactor: 50,
|
||||||
drainBase: 1,
|
drainBase: 1,
|
||||||
@@ -33,25 +33,4 @@ export const baseDisciplines: DisciplineDefinition[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'elemental-attunement',
|
|
||||||
name: 'Elemental Attunement',
|
|
||||||
attunement: DisciplinesAttunementType.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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
@@ -14,7 +14,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'metal',
|
manaType: 'metal',
|
||||||
baseCost: 12,
|
baseCost: 12,
|
||||||
description: 'Attune your metal mana to regenerate passively over time.',
|
description: 'Attune your metal mana to regenerate passively over time.',
|
||||||
statBonus: { stat: 'regen_metal', baseValue: 0.35 },
|
statBonus: { stat: 'regen_metal', baseValue: 0.35, label: 'Metal Regen/tick' },
|
||||||
difficultyFactor: 160,
|
difficultyFactor: 160,
|
||||||
scalingFactor: 80,
|
scalingFactor: 80,
|
||||||
drainBase: 2,
|
drainBase: 2,
|
||||||
@@ -43,7 +43,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'sand',
|
manaType: 'sand',
|
||||||
baseCost: 12,
|
baseCost: 12,
|
||||||
description: 'Attune your sand mana to regenerate passively over time.',
|
description: 'Attune your sand mana to regenerate passively over time.',
|
||||||
statBonus: { stat: 'regen_sand', baseValue: 0.35 },
|
statBonus: { stat: 'regen_sand', baseValue: 0.35, label: 'Sand Regen/tick' },
|
||||||
difficultyFactor: 160,
|
difficultyFactor: 160,
|
||||||
scalingFactor: 80,
|
scalingFactor: 80,
|
||||||
drainBase: 2,
|
drainBase: 2,
|
||||||
@@ -72,7 +72,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'lightning',
|
manaType: 'lightning',
|
||||||
baseCost: 12,
|
baseCost: 12,
|
||||||
description: 'Attune your lightning mana to regenerate passively over time.',
|
description: 'Attune your lightning mana to regenerate passively over time.',
|
||||||
statBonus: { stat: 'regen_lightning', baseValue: 0.35 },
|
statBonus: { stat: 'regen_lightning', baseValue: 0.35, label: 'Lightning Regen/tick' },
|
||||||
difficultyFactor: 160,
|
difficultyFactor: 160,
|
||||||
scalingFactor: 80,
|
scalingFactor: 80,
|
||||||
drainBase: 2,
|
drainBase: 2,
|
||||||
@@ -102,7 +102,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'crystal',
|
manaType: 'crystal',
|
||||||
baseCost: 18,
|
baseCost: 18,
|
||||||
description: 'Attune your crystal mana to regenerate passively over time.',
|
description: 'Attune your crystal mana to regenerate passively over time.',
|
||||||
statBonus: { stat: 'regen_crystal', baseValue: 0.25 },
|
statBonus: { stat: 'regen_crystal', baseValue: 0.25, label: 'Crystal Regen/tick' },
|
||||||
difficultyFactor: 220,
|
difficultyFactor: 220,
|
||||||
scalingFactor: 110,
|
scalingFactor: 110,
|
||||||
drainBase: 3,
|
drainBase: 3,
|
||||||
@@ -131,7 +131,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'stellar',
|
manaType: 'stellar',
|
||||||
baseCost: 18,
|
baseCost: 18,
|
||||||
description: 'Attune your stellar mana to regenerate passively over time.',
|
description: 'Attune your stellar mana to regenerate passively over time.',
|
||||||
statBonus: { stat: 'regen_stellar', baseValue: 0.25 },
|
statBonus: { stat: 'regen_stellar', baseValue: 0.25, label: 'Stellar Regen/tick' },
|
||||||
difficultyFactor: 220,
|
difficultyFactor: 220,
|
||||||
scalingFactor: 110,
|
scalingFactor: 110,
|
||||||
drainBase: 3,
|
drainBase: 3,
|
||||||
@@ -160,7 +160,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'void',
|
manaType: 'void',
|
||||||
baseCost: 18,
|
baseCost: 18,
|
||||||
description: 'Attune your void mana to regenerate passively over time.',
|
description: 'Attune your void mana to regenerate passively over time.',
|
||||||
statBonus: { stat: 'regen_void', baseValue: 0.25 },
|
statBonus: { stat: 'regen_void', baseValue: 0.25, label: 'Void Regen/tick' },
|
||||||
difficultyFactor: 220,
|
difficultyFactor: 220,
|
||||||
scalingFactor: 110,
|
scalingFactor: 110,
|
||||||
drainBase: 3,
|
drainBase: 3,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function makeBaseRegen(id: string, name: string, manaType: string, cost: number)
|
|||||||
manaType: manaType as DisciplineDefinition['manaType'],
|
manaType: manaType as DisciplineDefinition['manaType'],
|
||||||
baseCost: cost,
|
baseCost: cost,
|
||||||
description: `Attune your ${name.toLowerCase()} mana to regenerate passively over time.`,
|
description: `Attune your ${name.toLowerCase()} mana to regenerate passively over time.`,
|
||||||
statBonus: { stat: `regen_${shortId}` as DisciplineDefinition['statBonus']['stat'], baseValue: BASE_REGEN },
|
statBonus: { stat: `regen_${shortId}` as DisciplineDefinition['statBonus']['stat'], baseValue: BASE_REGEN, label: `${name} Regen/tick` },
|
||||||
difficultyFactor: BASE_DIFF,
|
difficultyFactor: BASE_DIFF,
|
||||||
scalingFactor: BASE_SCALE,
|
scalingFactor: BASE_SCALE,
|
||||||
drainBase: BASE_DRAIN,
|
drainBase: BASE_DRAIN,
|
||||||
@@ -61,7 +61,7 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'transference',
|
manaType: 'transference',
|
||||||
baseCost: 6,
|
baseCost: 6,
|
||||||
description: 'Attune your transference mana to regenerate passively over time.',
|
description: 'Attune your transference mana to regenerate passively over time.',
|
||||||
statBonus: { stat: 'regen_transference', baseValue: 0.4 },
|
statBonus: { stat: 'regen_transference', baseValue: 0.4, label: 'Transference Regen/tick' },
|
||||||
difficultyFactor: 100,
|
difficultyFactor: 100,
|
||||||
scalingFactor: 50,
|
scalingFactor: 50,
|
||||||
drainBase: 1,
|
drainBase: 1,
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// ─── Elemental Attunement Disciplines ──────────────────────────────────────────
|
||||||
|
// One discipline per base mana type that increases that element's capacity.
|
||||||
|
// All are BASE attunement so they are available to every role once the element is unlocked.
|
||||||
|
|
||||||
|
import { DisciplinesAttunementType } from '../../types/disciplines';
|
||||||
|
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||||
|
|
||||||
|
interface ElementalAttunementConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
manaType: DisciplineDefinition['manaType'];
|
||||||
|
cost: number;
|
||||||
|
baseValue: number;
|
||||||
|
difficultyFactor: number;
|
||||||
|
scalingFactor: number;
|
||||||
|
drainBase: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ELEMENTAL_ATTUNEMENTS: ElementalAttunementConfig[] = [
|
||||||
|
{ id: 'attune-fire', name: 'Fire', manaType: 'fire', cost: 10, baseValue: 5, difficultyFactor: 150, scalingFactor: 75, drainBase: 2 },
|
||||||
|
{ id: 'attune-water', name: 'Water', manaType: 'water', cost: 10, baseValue: 5, difficultyFactor: 150, scalingFactor: 75, drainBase: 2 },
|
||||||
|
{ id: 'attune-air', name: 'Air', manaType: 'air', cost: 10, baseValue: 5, difficultyFactor: 150, scalingFactor: 75, drainBase: 2 },
|
||||||
|
{ id: 'attune-earth', name: 'Earth', manaType: 'earth', cost: 10, baseValue: 5, difficultyFactor: 150, scalingFactor: 75, drainBase: 2 },
|
||||||
|
{ id: 'attune-light', name: 'Light', manaType: 'light', cost: 10, baseValue: 5, difficultyFactor: 150, scalingFactor: 75, drainBase: 2 },
|
||||||
|
{ id: 'attune-dark', name: 'Dark', manaType: 'dark', cost: 10, baseValue: 5, difficultyFactor: 150, scalingFactor: 75, drainBase: 2 },
|
||||||
|
{ id: 'attune-death', name: 'Death', manaType: 'death', cost: 12, baseValue: 4, difficultyFactor: 180, scalingFactor: 90, drainBase: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function makeElementalAttunement(cfg: ElementalAttunementConfig): DisciplineDefinition {
|
||||||
|
return {
|
||||||
|
id: cfg.id,
|
||||||
|
name: `${cfg.name} Attunement`,
|
||||||
|
attunement: DisciplinesAttunementType.BASE,
|
||||||
|
manaType: cfg.manaType,
|
||||||
|
baseCost: cfg.cost,
|
||||||
|
description: `Begin focusing raw mana into ${cfg.name.toLowerCase()}.`,
|
||||||
|
statBonus: { stat: `elementCap_${cfg.manaType}`, baseValue: cfg.baseValue, label: `${cfg.name} Mana Capacity` },
|
||||||
|
difficultyFactor: cfg.difficultyFactor,
|
||||||
|
scalingFactor: cfg.scalingFactor,
|
||||||
|
drainBase: cfg.drainBase,
|
||||||
|
perks: [
|
||||||
|
{
|
||||||
|
id: `${cfg.id}-1`,
|
||||||
|
type: 'once',
|
||||||
|
threshold: 200,
|
||||||
|
value: 0,
|
||||||
|
description: `+10 ${cfg.name} Capacity`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const elementalAttunementDisciplines: DisciplineDefinition[] =
|
||||||
|
ELEMENTAL_ATTUNEMENTS.map(makeElementalAttunement);
|
||||||
@@ -12,7 +12,7 @@ export const enchanterSpecialDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'death',
|
manaType: 'death',
|
||||||
baseCost: 22,
|
baseCost: 22,
|
||||||
description: 'Learn to enchant equipment with unique and powerful effects.',
|
description: 'Learn to enchant equipment with unique and powerful effects.',
|
||||||
statBonus: { stat: 'enchantPower', baseValue: 5 },
|
statBonus: { stat: 'enchantPower', baseValue: 5, label: 'Enchantment Power' },
|
||||||
difficultyFactor: 220,
|
difficultyFactor: 220,
|
||||||
scalingFactor: 130,
|
scalingFactor: 130,
|
||||||
drainBase: 4,
|
drainBase: 4,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const enchanterSpellDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'air',
|
manaType: 'air',
|
||||||
baseCost: 18,
|
baseCost: 18,
|
||||||
description: 'Learn to enchant casters with basic spell effects.',
|
description: 'Learn to enchant casters with basic spell effects.',
|
||||||
statBonus: { stat: 'enchantPower', baseValue: 4 },
|
statBonus: { stat: 'enchantPower', baseValue: 4, label: 'Enchantment Power' },
|
||||||
difficultyFactor: 160,
|
difficultyFactor: 160,
|
||||||
scalingFactor: 100,
|
scalingFactor: 100,
|
||||||
drainBase: 3,
|
drainBase: 3,
|
||||||
@@ -90,7 +90,7 @@ export const enchanterSpellDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'earth',
|
manaType: 'earth',
|
||||||
baseCost: 25,
|
baseCost: 25,
|
||||||
description: 'Learn to enchant casters with intermediate and compound spell effects.',
|
description: 'Learn to enchant casters with intermediate and compound spell effects.',
|
||||||
statBonus: { stat: 'enchantPower', baseValue: 6 },
|
statBonus: { stat: 'enchantPower', baseValue: 6, label: 'Enchantment Power' },
|
||||||
difficultyFactor: 250,
|
difficultyFactor: 250,
|
||||||
scalingFactor: 150,
|
scalingFactor: 150,
|
||||||
drainBase: 5,
|
drainBase: 5,
|
||||||
@@ -153,7 +153,7 @@ export const enchanterSpellDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'dark',
|
manaType: 'dark',
|
||||||
baseCost: 35,
|
baseCost: 35,
|
||||||
description: 'Learn to enchant casters with master and exotic spell effects.',
|
description: 'Learn to enchant casters with master and exotic spell effects.',
|
||||||
statBonus: { stat: 'enchantPower', baseValue: 10 },
|
statBonus: { stat: 'enchantPower', baseValue: 10, label: 'Enchantment Power' },
|
||||||
difficultyFactor: 350,
|
difficultyFactor: 350,
|
||||||
scalingFactor: 200,
|
scalingFactor: 200,
|
||||||
drainBase: 7,
|
drainBase: 7,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const enchanterUtilityDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'light',
|
manaType: 'light',
|
||||||
baseCost: 8,
|
baseCost: 8,
|
||||||
description: 'Learn to enchant equipment with utility effects.',
|
description: 'Learn to enchant equipment with utility effects.',
|
||||||
statBonus: { stat: 'studySpeed', baseValue: 0.05 },
|
statBonus: { stat: 'studySpeed', baseValue: 0.05, label: 'Study Speed' },
|
||||||
difficultyFactor: 80,
|
difficultyFactor: 80,
|
||||||
scalingFactor: 60,
|
scalingFactor: 60,
|
||||||
drainBase: 2,
|
drainBase: 2,
|
||||||
@@ -50,7 +50,7 @@ export const enchanterUtilityDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'water',
|
manaType: 'water',
|
||||||
baseCost: 15,
|
baseCost: 15,
|
||||||
description: 'Learn to enchant equipment with mana-boosting effects.',
|
description: 'Learn to enchant equipment with mana-boosting effects.',
|
||||||
statBonus: { stat: 'maxMana', baseValue: 10 },
|
statBonus: { stat: 'maxMana', baseValue: 10, label: 'Max Mana' },
|
||||||
difficultyFactor: 150,
|
difficultyFactor: 150,
|
||||||
scalingFactor: 100,
|
scalingFactor: 100,
|
||||||
drainBase: 3,
|
drainBase: 3,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const enchanterDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'transference',
|
manaType: 'transference',
|
||||||
baseCost: 8,
|
baseCost: 8,
|
||||||
description: 'Improve your ability to apply enchantments to equipment.',
|
description: 'Improve your ability to apply enchantments to equipment.',
|
||||||
statBonus: { stat: 'enchantPower', baseValue: 8 },
|
statBonus: { stat: 'enchantPower', baseValue: 8, label: 'Enchantment Power' },
|
||||||
difficultyFactor: 120,
|
difficultyFactor: 120,
|
||||||
scalingFactor: 60,
|
scalingFactor: 60,
|
||||||
drainBase: 3,
|
drainBase: 3,
|
||||||
@@ -40,7 +40,7 @@ export const enchanterDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'lightning',
|
manaType: 'lightning',
|
||||||
baseCost: 12,
|
baseCost: 12,
|
||||||
description: 'Use lightning to transfer mana to equipment.',
|
description: 'Use lightning to transfer mana to equipment.',
|
||||||
statBonus: { stat: 'clickManaMultiplier', baseValue: 0.3 },
|
statBonus: { stat: 'clickManaMultiplier', baseValue: 0.3, label: 'Click Mana Multiplier' },
|
||||||
difficultyFactor: 180,
|
difficultyFactor: 180,
|
||||||
scalingFactor: 90,
|
scalingFactor: 90,
|
||||||
drainBase: 5,
|
drainBase: 5,
|
||||||
@@ -61,7 +61,7 @@ export const enchanterDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'fire',
|
manaType: 'fire',
|
||||||
baseCost: 10,
|
baseCost: 10,
|
||||||
description: 'Learn to enchant weapons with basic elemental effects.',
|
description: 'Learn to enchant weapons with basic elemental effects.',
|
||||||
statBonus: { stat: 'enchantPower', baseValue: 3 },
|
statBonus: { stat: 'enchantPower', baseValue: 3, label: 'Enchantment Power' },
|
||||||
difficultyFactor: 100,
|
difficultyFactor: 100,
|
||||||
scalingFactor: 80,
|
scalingFactor: 80,
|
||||||
drainBase: 2,
|
drainBase: 2,
|
||||||
@@ -99,7 +99,7 @@ export const enchanterDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'dark',
|
manaType: 'dark',
|
||||||
baseCost: 20,
|
baseCost: 20,
|
||||||
description: 'Learn to enchant weapons with exotic and combat effects.',
|
description: 'Learn to enchant weapons with exotic and combat effects.',
|
||||||
statBonus: { stat: 'enchantPower', baseValue: 5 },
|
statBonus: { stat: 'enchantPower', baseValue: 5, label: 'Enchantment Power' },
|
||||||
difficultyFactor: 200,
|
difficultyFactor: 200,
|
||||||
scalingFactor: 120,
|
scalingFactor: 120,
|
||||||
drainBase: 4,
|
drainBase: 4,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const fabricatorDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'earth',
|
manaType: 'earth',
|
||||||
baseCost: 10,
|
baseCost: 10,
|
||||||
description: 'Improve your ability to craft and maintain golems.',
|
description: 'Improve your ability to craft and maintain golems.',
|
||||||
statBonus: { stat: 'golemCapacity', baseValue: 2 },
|
statBonus: { stat: 'golemCapacity', baseValue: 2, label: 'Golem Capacity' },
|
||||||
difficultyFactor: 150,
|
difficultyFactor: 150,
|
||||||
scalingFactor: 80,
|
scalingFactor: 80,
|
||||||
drainBase: 4,
|
drainBase: 4,
|
||||||
@@ -40,7 +40,7 @@ export const fabricatorDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'sand',
|
manaType: 'sand',
|
||||||
baseCost: 12,
|
baseCost: 12,
|
||||||
description: 'Reduce material costs for crafting.',
|
description: 'Reduce material costs for crafting.',
|
||||||
statBonus: { stat: 'craftingCostReduction', baseValue: 15 },
|
statBonus: { stat: 'craftingCostReduction', baseValue: 15, label: 'Crafting Cost Reduction' },
|
||||||
difficultyFactor: 180,
|
difficultyFactor: 180,
|
||||||
scalingFactor: 90,
|
scalingFactor: 90,
|
||||||
drainBase: 6,
|
drainBase: 6,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Aggregates all discipline definitions into a single ALL_DISCIPLINES array
|
// Aggregates all discipline definitions into a single ALL_DISCIPLINES array
|
||||||
|
|
||||||
import { baseDisciplines } from './base';
|
import { baseDisciplines } from './base';
|
||||||
|
import { elementalAttunementDisciplines } from './elemental';
|
||||||
import { elementalRegenDisciplines } from './elemental-regen';
|
import { elementalRegenDisciplines } from './elemental-regen';
|
||||||
import { elementalRegenAdvancedDisciplines } from './elemental-regen-advanced';
|
import { elementalRegenAdvancedDisciplines } from './elemental-regen-advanced';
|
||||||
import { enchanterDisciplines } from './enchanter';
|
import { enchanterDisciplines } from './enchanter';
|
||||||
@@ -14,6 +15,7 @@ import type { DisciplineDefinition } from '../../types/disciplines';
|
|||||||
|
|
||||||
export const ALL_DISCIPLINES: DisciplineDefinition[] = [
|
export const ALL_DISCIPLINES: DisciplineDefinition[] = [
|
||||||
...baseDisciplines,
|
...baseDisciplines,
|
||||||
|
...elementalAttunementDisciplines,
|
||||||
...elementalRegenDisciplines,
|
...elementalRegenDisciplines,
|
||||||
...elementalRegenAdvancedDisciplines,
|
...elementalRegenAdvancedDisciplines,
|
||||||
...enchanterDisciplines,
|
...enchanterDisciplines,
|
||||||
@@ -25,6 +27,7 @@ export const ALL_DISCIPLINES: DisciplineDefinition[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export { baseDisciplines } from './base';
|
export { baseDisciplines } from './base';
|
||||||
|
export { elementalAttunementDisciplines } from './elemental';
|
||||||
export { elementalRegenDisciplines } from './elemental-regen';
|
export { elementalRegenDisciplines } from './elemental-regen';
|
||||||
export { elementalRegenAdvancedDisciplines } from './elemental-regen-advanced';
|
export { elementalRegenAdvancedDisciplines } from './elemental-regen-advanced';
|
||||||
export { enchanterDisciplines } from './enchanter';
|
export { enchanterDisciplines } from './enchanter';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const invokerDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'light',
|
manaType: 'light',
|
||||||
baseCost: 10,
|
baseCost: 10,
|
||||||
description: 'Improve spell power and effectiveness.',
|
description: 'Improve spell power and effectiveness.',
|
||||||
statBonus: { stat: 'baseDamageBonus', baseValue: 6 },
|
statBonus: { stat: 'baseDamageBonus', baseValue: 6, label: 'Base Damage' },
|
||||||
difficultyFactor: 130,
|
difficultyFactor: 130,
|
||||||
scalingFactor: 65,
|
scalingFactor: 65,
|
||||||
drainBase: 3,
|
drainBase: 3,
|
||||||
@@ -40,7 +40,7 @@ export const invokerDisciplines: DisciplineDefinition[] = [
|
|||||||
manaType: 'void',
|
manaType: 'void',
|
||||||
baseCost: 15,
|
baseCost: 15,
|
||||||
description: 'Master the exotic void mana for devastating effects.',
|
description: 'Master the exotic void mana for devastating effects.',
|
||||||
statBonus: { stat: 'baseDamageMultiplier', baseValue: 0.15 },
|
statBonus: { stat: 'baseDamageMultiplier', baseValue: 0.15, label: 'Base Damage Multiplier' },
|
||||||
difficultyFactor: 200,
|
difficultyFactor: 200,
|
||||||
scalingFactor: 100,
|
scalingFactor: 100,
|
||||||
drainBase: 7,
|
drainBase: 7,
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import {
|
|||||||
calculateManaDrain,
|
calculateManaDrain,
|
||||||
calculateStatBonus,
|
calculateStatBonus,
|
||||||
canProceedDiscipline,
|
canProceedDiscipline,
|
||||||
|
checkDisciplinePrerequisites,
|
||||||
getUnlockedPerks,
|
getUnlockedPerks,
|
||||||
} from '../utils/discipline-math';
|
} from '../utils/discipline-math';
|
||||||
import { baseDisciplines } from '../data/disciplines/base';
|
import { baseDisciplines } from '../data/disciplines/base';
|
||||||
|
import { elementalAttunementDisciplines } from '../data/disciplines/elemental';
|
||||||
import { elementalRegenDisciplines } from '../data/disciplines/elemental-regen';
|
import { elementalRegenDisciplines } from '../data/disciplines/elemental-regen';
|
||||||
import { elementalRegenAdvancedDisciplines } from '../data/disciplines/elemental-regen-advanced';
|
import { elementalRegenAdvancedDisciplines } from '../data/disciplines/elemental-regen-advanced';
|
||||||
import { enchanterDisciplines } from '../data/disciplines/enchanter';
|
import { enchanterDisciplines } from '../data/disciplines/enchanter';
|
||||||
@@ -23,6 +25,7 @@ import { MAX_CONCURRENT_DISCIPLINES } from '../types/disciplines';
|
|||||||
|
|
||||||
const ALL_DISCIPLINES = [
|
const ALL_DISCIPLINES = [
|
||||||
...baseDisciplines,
|
...baseDisciplines,
|
||||||
|
...elementalAttunementDisciplines,
|
||||||
...elementalRegenDisciplines,
|
...elementalRegenDisciplines,
|
||||||
...elementalRegenAdvancedDisciplines,
|
...elementalRegenAdvancedDisciplines,
|
||||||
...enchanterDisciplines,
|
...enchanterDisciplines,
|
||||||
@@ -86,6 +89,8 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
}).length;
|
}).length;
|
||||||
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;
|
||||||
|
const prereqCheck = checkDisciplinePrerequisites(def, s.disciplines, ALL_DISCIPLINES);
|
||||||
|
if (!prereqCheck.canProceed) return s;
|
||||||
|
|
||||||
const discState = existing || { id, xp: 0, paused: false };
|
const discState = existing || { id, xp: 0, paused: false };
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export interface DisciplineDefinition {
|
|||||||
manaType: ManaType;
|
manaType: ManaType;
|
||||||
baseCost: number;
|
baseCost: number;
|
||||||
description: string;
|
description: string;
|
||||||
statBonus: { stat: string; baseValue: number };
|
statBonus: { stat: string; baseValue: number; label: string };
|
||||||
difficultyFactor: number;
|
difficultyFactor: number;
|
||||||
scalingFactor: number;
|
scalingFactor: number;
|
||||||
drainBase: number;
|
drainBase: number;
|
||||||
|
|||||||
@@ -87,6 +87,34 @@ export function canProceedDiscipline(
|
|||||||
return element && element.current >= drain;
|
return element && element.current >= drain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a discipline's prerequisites are met.
|
||||||
|
* Returns { canProceed: boolean, missingPrereqs: string[] }
|
||||||
|
* where missingPrereqs is a list of prerequisite discipline names that are not yet unlocked.
|
||||||
|
*/
|
||||||
|
export function checkDisciplinePrerequisites(
|
||||||
|
discipline: DisciplineDefinition,
|
||||||
|
allDisciplines: Record<string, DisciplineState>,
|
||||||
|
allDefinitions: DisciplineDefinition[],
|
||||||
|
): { canProceed: boolean; missingPrereqs: string[] } {
|
||||||
|
if (!discipline.requires || discipline.requires.length === 0) {
|
||||||
|
return { canProceed: true, missingPrereqs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingPrereqs: string[] = [];
|
||||||
|
|
||||||
|
for (const reqId of discipline.requires) {
|
||||||
|
const reqState = allDisciplines[reqId];
|
||||||
|
// A prerequisite is met if the discipline has XP > 0 (has been practiced)
|
||||||
|
if (!reqState || reqState.xp <= 0) {
|
||||||
|
const reqDef = allDefinitions.find((d) => d.id === reqId);
|
||||||
|
missingPrereqs.push(reqDef?.name ?? reqId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { canProceed: missingPrereqs.length === 0, missingPrereqs };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get unlocked perks for a discipline
|
* Get unlocked perks for a discipline
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user