diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 0e38cbd..ab33ca4 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # 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. 1. Processed 134 files (1.4s) (2 warnings) diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index c75ceff..f66de88 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_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.", "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." }, diff --git a/docs/project-structure.txt b/docs/project-structure.txt index e9396d9..73aa449 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -200,6 +200,7 @@ Mana-Loop/ │ │ │ ├── crafting-utils-recipe.test.ts │ │ │ ├── crafting-utils-time.test.ts │ │ │ ├── discipline-math.test.ts +│ │ │ ├── discipline-prerequisites.test.ts │ │ │ ├── enemy-generator.test.ts │ │ │ ├── enemy-utils.test.ts │ │ │ ├── floor-utils.test.ts @@ -248,6 +249,7 @@ Mana-Loop/ │ │ │ │ ├── base.ts │ │ │ │ ├── elemental-regen-advanced.ts │ │ │ │ ├── elemental-regen.ts +│ │ │ │ ├── elemental.ts │ │ │ │ ├── enchanter-special.ts │ │ │ │ ├── enchanter-spells.ts │ │ │ │ ├── enchanter-utility.ts diff --git a/src/components/game/tabs/DisciplinesTab.tsx b/src/components/game/tabs/DisciplinesTab.tsx index 785c0e1..f09de85 100644 --- a/src/components/game/tabs/DisciplinesTab.tsx +++ b/src/components/game/tabs/DisciplinesTab.tsx @@ -4,12 +4,14 @@ import type { DisciplineDefinition } from '@/lib/game/types/disciplines'; import type { ManaType } from '@/lib/game/types/elements'; import { ELEMENTS } from '@/lib/game/constants/elements'; 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 { elementalRegenAdvancedDisciplines } from '@/lib/game/data/disciplines/elemental-regen-advanced'; import { enchanterDisciplines } from '@/lib/game/data/disciplines/enchanter'; import { fabricatorDisciplines } from '@/lib/game/data/disciplines/fabricator'; 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'; // ─── Attunement Tabs ───────────────────────────────────────────────────────── @@ -22,6 +24,7 @@ interface AttunementTab { const ATTUNEMENT_TABS: AttunementTab[] = [ { key: 'base', label: 'Base', items: baseDisciplines }, + { key: 'elements', label: 'Mana Types', items: elementalAttunementDisciplines }, { key: 'elemental-regen', label: 'Elemental Regen', items: elementalRegenDisciplines }, { key: 'elemental-regen-advanced', label: 'Advanced Regen', items: elementalRegenAdvancedDisciplines }, { key: 'enchanter', label: 'Enchanter', items: enchanterDisciplines }, @@ -41,6 +44,8 @@ export interface DisciplineCardDefinition { perkValues?: number[]; perkTypes?: string[]; statBonus: string; + statBonusLabel: string; + requires?: string[]; baseValue: number; drainBase: number; difficultyFactor: number; @@ -51,6 +56,8 @@ export interface DisciplineCardRuntime { xp: number; paused: boolean; concurrentLimit: number; + isLocked: boolean; + missingPrereqs: string[]; } export interface DisciplineCardCallbacks { @@ -68,9 +75,9 @@ interface DisciplineCardProps { const DisciplineCard: React.FC = ({ definition, runtime, callbacks }) => { const { id, name, description, manaType, baseCost, perkThresholds, perkValues, perkTypes, - statBonus, baseValue, drainBase, difficultyFactor, scalingFactor, + statBonus, statBonusLabel, baseValue, drainBase, difficultyFactor, scalingFactor, } = definition; - const { xp, paused: isPaused, concurrentLimit } = runtime; + const { xp, paused: isPaused, concurrentLimit, isLocked, missingPrereqs } = runtime; const { onToggle } = callbacks; const displayXp = xp; @@ -102,7 +109,7 @@ const DisciplineCard: React.FC = ({ definition, runtime, ca }; return ( -
+

{name}

= ({ definition, runtime, ca
- Stat Bonus: {activeStatBonus.toFixed(2)} on {statBonus} + Stat Bonus: {activeStatBonus.toFixed(2)} on {statBonusLabel}
@@ -159,17 +166,26 @@ const DisciplineCard: React.FC = ({ definition, runtime, ca
+ {isLocked && missingPrereqs.length > 0 && ( +
+ Requires: {missingPrereqs.join(', ')} +
+ )} +
@@ -235,6 +251,7 @@ export const DisciplinesTab: React.FC = () => {
{activeTab?.items.map((disc) => { const discState = disciplines[disc.id] ?? { xp: 0, paused: true }; + const prereqCheck = checkDisciplinePrerequisites(disc, disciplines, ALL_DISCIPLINES); return ( { manaType: disc.manaType, baseCost: disc.baseCost, statBonus: disc.statBonus.stat, + statBonusLabel: disc.statBonus.label, + requires: disc.requires, baseValue: disc.statBonus.baseValue, drainBase: disc.drainBase, difficultyFactor: disc.difficultyFactor, @@ -257,6 +276,8 @@ export const DisciplinesTab: React.FC = () => { xp: discState.xp, paused: discState.paused, concurrentLimit, + isLocked: !prereqCheck.canProceed, + missingPrereqs: prereqCheck.missingPrereqs, }} callbacks={{ onToggle: handleToggle, diff --git a/src/lib/game/__tests__/discipline-math.test.ts b/src/lib/game/__tests__/discipline-math.test.ts index efadaf7..a57aa6b 100644 --- a/src/lib/game/__tests__/discipline-math.test.ts +++ b/src/lib/game/__tests__/discipline-math.test.ts @@ -20,7 +20,7 @@ const rawMastery: DisciplineDefinition = { manaType: 'raw', baseCost: 5, description: 'Learn to harness raw mana more efficiently.', - statBonus: { stat: 'maxManaBonus', baseValue: 10 }, + statBonus: { stat: 'maxManaBonus', baseValue: 10, label: 'Max Mana Bonus' }, difficultyFactor: 100, scalingFactor: 50, drainBase: 1, @@ -42,14 +42,14 @@ const rawMastery: DisciplineDefinition = { ], }; -const elementalAttunement: DisciplineDefinition = { - id: 'elemental-attunement', - name: 'Elemental Attunement', +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 }, + statBonus: { stat: 'elementCap_fire', baseValue: 5, label: 'Fire Element Cap' }, difficultyFactor: 150, scalingFactor: 75, drainBase: 2, @@ -71,7 +71,7 @@ const cappedPerkDiscipline: DisciplineDefinition = { manaType: 'raw', baseCost: 1, description: 'Test discipline with capped perk.', - statBonus: { stat: 'testStat', baseValue: 1 }, + statBonus: { stat: 'testStat', baseValue: 1, label: 'Test Stat' }, difficultyFactor: 100, scalingFactor: 100, drainBase: 1, @@ -196,28 +196,28 @@ describe('canActivateDiscipline', () => { }); it('should return true when required element is unlocked', () => { - const result = canActivateDiscipline(elementalAttunement, { + const result = canActivateDiscipline(attuneFire, { elements: { fire: { unlocked: true } }, }); expect(result).toBe(true); }); it('should return false when required element is locked', () => { - const result = canActivateDiscipline(elementalAttunement, { + const result = canActivateDiscipline(attuneFire, { elements: { fire: { unlocked: false } }, }); expect(result).toBe(false); }); it('should return falsy when required element does not exist', () => { - const result = canActivateDiscipline(elementalAttunement, { + const result = canActivateDiscipline(attuneFire, { elements: {}, }); expect(result).toBeFalsy(); }); it('should return falsy when elements is undefined', () => { - const result = canActivateDiscipline(elementalAttunement, {}); + const result = canActivateDiscipline(attuneFire, {}); expect(result).toBeFalsy(); }); }); @@ -255,16 +255,16 @@ describe('canProceedDiscipline', () => { }); it('should return true when element mana is sufficient', () => { - const state: DisciplineState = { id: 'elemental-attunement', xp: 0, paused: false }; - const result = canProceedDiscipline(elementalAttunement, state, { + const state: DisciplineState = { id: 'attune-fire', xp: 0, paused: false }; + const result = canProceedDiscipline(attuneFire, state, { elements: { fire: { current: 100, max: 100, unlocked: true } }, }); expect(result).toBe(true); }); it('should return false when element mana is insufficient', () => { - const state: DisciplineState = { id: 'elemental-attunement', xp: 10000, paused: false }; - const result = canProceedDiscipline(elementalAttunement, state, { + const state: DisciplineState = { id: 'attune-fire', xp: 10000, paused: false }; + const result = canProceedDiscipline(attuneFire, state, { elements: { fire: { current: 0, max: 100, unlocked: true } }, }); expect(result).toBe(false); @@ -340,10 +340,10 @@ describe('calculateDisciplineStats', () => { }); it('should sum stats from multiple active disciplines', () => { - const disciplines = [rawMastery, elementalAttunement]; + const disciplines = [rawMastery, attuneFire]; const states: DisciplineState[] = [ { 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); expect(result.maxManaBonus).toBeGreaterThan(0); @@ -351,10 +351,10 @@ describe('calculateDisciplineStats', () => { }); it('should skip paused disciplines', () => { - const disciplines = [rawMastery, elementalAttunement]; + const disciplines = [rawMastery, attuneFire]; const states: DisciplineState[] = [ { 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); expect(result.maxManaBonus).toBeGreaterThan(0); @@ -362,7 +362,7 @@ describe('calculateDisciplineStats', () => { }); it('should handle missing state for a discipline', () => { - const disciplines = [rawMastery, elementalAttunement]; + const disciplines = [rawMastery, attuneFire]; const states: DisciplineState[] = [ { id: 'raw-mastery', xp: 50, paused: false }, ]; diff --git a/src/lib/game/__tests__/discipline-prerequisites.test.ts b/src/lib/game/__tests__/discipline-prerequisites.test.ts new file mode 100644 index 0000000..5f1fbcc --- /dev/null +++ b/src/lib/game/__tests__/discipline-prerequisites.test.ts @@ -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 = { + '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 = { + '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 = {}; + 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 = {}; + 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 = { + '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 = { + '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([]); + }); +}); diff --git a/src/lib/game/__tests__/store-actions-discipline.test.ts b/src/lib/game/__tests__/store-actions-discipline.test.ts index d395d01..594517f 100644 --- a/src/lib/game/__tests__/store-actions-discipline.test.ts +++ b/src/lib/game/__tests__/store-actions-discipline.test.ts @@ -28,16 +28,16 @@ describe('DisciplineStore', () => { it('should not activate when concurrent limit reached', () => { useDisciplineStore.getState().activate('raw-mastery'); - useDisciplineStore.getState().activate('elemental-attunement'); + useDisciplineStore.getState().activate('attune-fire'); expect(useDisciplineStore.getState().activeIds.length).toBe(1); }); it('should activate when no prior discipline state (optimistic)', () => { // canProceedDiscipline returns true when disciplineState is undefined - useDisciplineStore.getState().activate('elemental-attunement', { - elements: { fire: { unlocked: false } }, + useDisciplineStore.getState().activate('attune-fire', { + 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', () => { @@ -49,10 +49,10 @@ describe('DisciplineStore', () => { }); it('should activate when required element is unlocked', () => { - useDisciplineStore.getState().activate('elemental-attunement', { - elements: { fire: { unlocked: true } }, + useDisciplineStore.getState().activate('attune-fire', { + elements: { fire: { unlocked: true, current: 100, max: 100 } }, }); - expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement'); + expect(useDisciplineStore.getState().activeIds).toContain('attune-fire'); }); }); diff --git a/src/lib/game/data/disciplines/base.ts b/src/lib/game/data/disciplines/base.ts index 2378e21..58330e6 100644 --- a/src/lib/game/data/disciplines/base.ts +++ b/src/lib/game/data/disciplines/base.ts @@ -12,7 +12,7 @@ export const baseDisciplines: DisciplineDefinition[] = [ manaType: 'raw', baseCost: 5, description: 'Learn to harness raw mana more efficiently.', - statBonus: { stat: 'maxManaBonus', baseValue: 10 }, + statBonus: { stat: 'maxManaBonus', baseValue: 10, label: 'Max Mana' }, difficultyFactor: 100, scalingFactor: 50, 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', - }, - ], - }, ]; \ No newline at end of file diff --git a/src/lib/game/data/disciplines/elemental-regen-advanced.ts b/src/lib/game/data/disciplines/elemental-regen-advanced.ts index b3d20cc..ea5a000 100644 --- a/src/lib/game/data/disciplines/elemental-regen-advanced.ts +++ b/src/lib/game/data/disciplines/elemental-regen-advanced.ts @@ -14,7 +14,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [ manaType: 'metal', baseCost: 12, 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, scalingFactor: 80, drainBase: 2, @@ -43,7 +43,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [ manaType: 'sand', baseCost: 12, 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, scalingFactor: 80, drainBase: 2, @@ -72,7 +72,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [ manaType: 'lightning', baseCost: 12, 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, scalingFactor: 80, drainBase: 2, @@ -102,7 +102,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [ manaType: 'crystal', baseCost: 18, 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, scalingFactor: 110, drainBase: 3, @@ -131,7 +131,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [ manaType: 'stellar', baseCost: 18, 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, scalingFactor: 110, drainBase: 3, @@ -160,7 +160,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [ manaType: 'void', baseCost: 18, 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, scalingFactor: 110, drainBase: 3, diff --git a/src/lib/game/data/disciplines/elemental-regen.ts b/src/lib/game/data/disciplines/elemental-regen.ts index 1845278..b3baf94 100644 --- a/src/lib/game/data/disciplines/elemental-regen.ts +++ b/src/lib/game/data/disciplines/elemental-regen.ts @@ -19,7 +19,7 @@ function makeBaseRegen(id: string, name: string, manaType: string, cost: number) manaType: manaType as DisciplineDefinition['manaType'], baseCost: cost, 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, scalingFactor: BASE_SCALE, drainBase: BASE_DRAIN, @@ -61,7 +61,7 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [ manaType: 'transference', baseCost: 6, 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, scalingFactor: 50, drainBase: 1, diff --git a/src/lib/game/data/disciplines/elemental.ts b/src/lib/game/data/disciplines/elemental.ts new file mode 100644 index 0000000..34124b5 --- /dev/null +++ b/src/lib/game/data/disciplines/elemental.ts @@ -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); diff --git a/src/lib/game/data/disciplines/enchanter-special.ts b/src/lib/game/data/disciplines/enchanter-special.ts index 400f0c9..9d57e35 100644 --- a/src/lib/game/data/disciplines/enchanter-special.ts +++ b/src/lib/game/data/disciplines/enchanter-special.ts @@ -12,7 +12,7 @@ export const enchanterSpecialDisciplines: DisciplineDefinition[] = [ manaType: 'death', baseCost: 22, 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, scalingFactor: 130, drainBase: 4, diff --git a/src/lib/game/data/disciplines/enchanter-spells.ts b/src/lib/game/data/disciplines/enchanter-spells.ts index 92a7a7a..5dee7c2 100644 --- a/src/lib/game/data/disciplines/enchanter-spells.ts +++ b/src/lib/game/data/disciplines/enchanter-spells.ts @@ -12,7 +12,7 @@ export const enchanterSpellDisciplines: DisciplineDefinition[] = [ manaType: 'air', baseCost: 18, description: 'Learn to enchant casters with basic spell effects.', - statBonus: { stat: 'enchantPower', baseValue: 4 }, + statBonus: { stat: 'enchantPower', baseValue: 4, label: 'Enchantment Power' }, difficultyFactor: 160, scalingFactor: 100, drainBase: 3, @@ -90,7 +90,7 @@ export const enchanterSpellDisciplines: DisciplineDefinition[] = [ manaType: 'earth', baseCost: 25, 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, scalingFactor: 150, drainBase: 5, @@ -153,7 +153,7 @@ export const enchanterSpellDisciplines: DisciplineDefinition[] = [ manaType: 'dark', baseCost: 35, 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, scalingFactor: 200, drainBase: 7, diff --git a/src/lib/game/data/disciplines/enchanter-utility.ts b/src/lib/game/data/disciplines/enchanter-utility.ts index b3c3116..ecf7f7c 100644 --- a/src/lib/game/data/disciplines/enchanter-utility.ts +++ b/src/lib/game/data/disciplines/enchanter-utility.ts @@ -12,7 +12,7 @@ export const enchanterUtilityDisciplines: DisciplineDefinition[] = [ manaType: 'light', baseCost: 8, 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, scalingFactor: 60, drainBase: 2, @@ -50,7 +50,7 @@ export const enchanterUtilityDisciplines: DisciplineDefinition[] = [ manaType: 'water', baseCost: 15, description: 'Learn to enchant equipment with mana-boosting effects.', - statBonus: { stat: 'maxMana', baseValue: 10 }, + statBonus: { stat: 'maxMana', baseValue: 10, label: 'Max Mana' }, difficultyFactor: 150, scalingFactor: 100, drainBase: 3, diff --git a/src/lib/game/data/disciplines/enchanter.ts b/src/lib/game/data/disciplines/enchanter.ts index e4eddb9..426201a 100644 --- a/src/lib/game/data/disciplines/enchanter.ts +++ b/src/lib/game/data/disciplines/enchanter.ts @@ -12,7 +12,7 @@ export const enchanterDisciplines: DisciplineDefinition[] = [ manaType: 'transference', baseCost: 8, description: 'Improve your ability to apply enchantments to equipment.', - statBonus: { stat: 'enchantPower', baseValue: 8 }, + statBonus: { stat: 'enchantPower', baseValue: 8, label: 'Enchantment Power' }, difficultyFactor: 120, scalingFactor: 60, drainBase: 3, @@ -40,7 +40,7 @@ export const enchanterDisciplines: DisciplineDefinition[] = [ manaType: 'lightning', baseCost: 12, 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, scalingFactor: 90, drainBase: 5, @@ -61,7 +61,7 @@ export const enchanterDisciplines: DisciplineDefinition[] = [ manaType: 'fire', baseCost: 10, description: 'Learn to enchant weapons with basic elemental effects.', - statBonus: { stat: 'enchantPower', baseValue: 3 }, + statBonus: { stat: 'enchantPower', baseValue: 3, label: 'Enchantment Power' }, difficultyFactor: 100, scalingFactor: 80, drainBase: 2, @@ -99,7 +99,7 @@ export const enchanterDisciplines: DisciplineDefinition[] = [ manaType: 'dark', baseCost: 20, 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, scalingFactor: 120, drainBase: 4, diff --git a/src/lib/game/data/disciplines/fabricator.ts b/src/lib/game/data/disciplines/fabricator.ts index 1c4e460..72eddab 100644 --- a/src/lib/game/data/disciplines/fabricator.ts +++ b/src/lib/game/data/disciplines/fabricator.ts @@ -12,7 +12,7 @@ export const fabricatorDisciplines: DisciplineDefinition[] = [ manaType: 'earth', baseCost: 10, description: 'Improve your ability to craft and maintain golems.', - statBonus: { stat: 'golemCapacity', baseValue: 2 }, + statBonus: { stat: 'golemCapacity', baseValue: 2, label: 'Golem Capacity' }, difficultyFactor: 150, scalingFactor: 80, drainBase: 4, @@ -40,7 +40,7 @@ export const fabricatorDisciplines: DisciplineDefinition[] = [ manaType: 'sand', baseCost: 12, description: 'Reduce material costs for crafting.', - statBonus: { stat: 'craftingCostReduction', baseValue: 15 }, + statBonus: { stat: 'craftingCostReduction', baseValue: 15, label: 'Crafting Cost Reduction' }, difficultyFactor: 180, scalingFactor: 90, drainBase: 6, diff --git a/src/lib/game/data/disciplines/index.ts b/src/lib/game/data/disciplines/index.ts index 3e18137..85df3f5 100644 --- a/src/lib/game/data/disciplines/index.ts +++ b/src/lib/game/data/disciplines/index.ts @@ -2,6 +2,7 @@ // Aggregates all discipline definitions into a single ALL_DISCIPLINES array import { baseDisciplines } from './base'; +import { elementalAttunementDisciplines } from './elemental'; import { elementalRegenDisciplines } from './elemental-regen'; import { elementalRegenAdvancedDisciplines } from './elemental-regen-advanced'; import { enchanterDisciplines } from './enchanter'; @@ -14,6 +15,7 @@ import type { DisciplineDefinition } from '../../types/disciplines'; export const ALL_DISCIPLINES: DisciplineDefinition[] = [ ...baseDisciplines, + ...elementalAttunementDisciplines, ...elementalRegenDisciplines, ...elementalRegenAdvancedDisciplines, ...enchanterDisciplines, @@ -25,6 +27,7 @@ export const ALL_DISCIPLINES: DisciplineDefinition[] = [ ]; export { baseDisciplines } from './base'; +export { elementalAttunementDisciplines } from './elemental'; export { elementalRegenDisciplines } from './elemental-regen'; export { elementalRegenAdvancedDisciplines } from './elemental-regen-advanced'; export { enchanterDisciplines } from './enchanter'; diff --git a/src/lib/game/data/disciplines/invoker.ts b/src/lib/game/data/disciplines/invoker.ts index 006c872..d52bf90 100644 --- a/src/lib/game/data/disciplines/invoker.ts +++ b/src/lib/game/data/disciplines/invoker.ts @@ -12,7 +12,7 @@ export const invokerDisciplines: DisciplineDefinition[] = [ manaType: 'light', baseCost: 10, description: 'Improve spell power and effectiveness.', - statBonus: { stat: 'baseDamageBonus', baseValue: 6 }, + statBonus: { stat: 'baseDamageBonus', baseValue: 6, label: 'Base Damage' }, difficultyFactor: 130, scalingFactor: 65, drainBase: 3, @@ -40,7 +40,7 @@ export const invokerDisciplines: DisciplineDefinition[] = [ manaType: 'void', baseCost: 15, 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, scalingFactor: 100, drainBase: 7, diff --git a/src/lib/game/stores/discipline-slice.ts b/src/lib/game/stores/discipline-slice.ts index b45f5db..f41d568 100644 --- a/src/lib/game/stores/discipline-slice.ts +++ b/src/lib/game/stores/discipline-slice.ts @@ -8,9 +8,11 @@ import { calculateManaDrain, calculateStatBonus, canProceedDiscipline, + checkDisciplinePrerequisites, getUnlockedPerks, } from '../utils/discipline-math'; import { baseDisciplines } from '../data/disciplines/base'; +import { elementalAttunementDisciplines } from '../data/disciplines/elemental'; import { elementalRegenDisciplines } from '../data/disciplines/elemental-regen'; import { elementalRegenAdvancedDisciplines } from '../data/disciplines/elemental-regen-advanced'; import { enchanterDisciplines } from '../data/disciplines/enchanter'; @@ -23,6 +25,7 @@ import { MAX_CONCURRENT_DISCIPLINES } from '../types/disciplines'; const ALL_DISCIPLINES = [ ...baseDisciplines, + ...elementalAttunementDisciplines, ...elementalRegenDisciplines, ...elementalRegenAdvancedDisciplines, ...enchanterDisciplines, @@ -86,6 +89,8 @@ export const useDisciplineStore = create()( }).length; if (nonPaused >= s.concurrentLimit) 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 }; return { diff --git a/src/lib/game/types/disciplines.ts b/src/lib/game/types/disciplines.ts index dff90e7..fc5f95f 100644 --- a/src/lib/game/types/disciplines.ts +++ b/src/lib/game/types/disciplines.ts @@ -30,7 +30,7 @@ export interface DisciplineDefinition { manaType: ManaType; baseCost: number; description: string; - statBonus: { stat: string; baseValue: number }; + statBonus: { stat: string; baseValue: number; label: string }; difficultyFactor: number; scalingFactor: number; drainBase: number; diff --git a/src/lib/game/utils/discipline-math.ts b/src/lib/game/utils/discipline-math.ts index ad1e2c0..a5b9150 100644 --- a/src/lib/game/utils/discipline-math.ts +++ b/src/lib/game/utils/discipline-math.ts @@ -87,6 +87,34 @@ export function canProceedDiscipline( 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, + 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 */