diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index e23949e..4626e97 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,13 +1,14 @@ # Circular Dependencies -Generated: 2026-05-26T16:28:29.780Z -Found: 6 circular chain(s) — these MUST be fixed before modifying involved files. +Generated: 2026-05-26T16:39:59.755Z +Found: 7 circular chain(s) — these MUST be fixed before modifying involved files. 1. Processed 135 files (1.5s) (2 warnings) -2. 1) utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.ts -3. 2) utils/floor-utils.ts > utils/room-utils.ts -4. 3) stores/gameStore.ts > stores/gameActions.ts -5. 4) stores/gameStore.ts > stores/gameLoopActions.ts -6. 5) stores/gameStore.ts > stores/tick-pipeline.ts +2. 1) effects/discipline-effects.ts > stores/discipline-slice.ts > stores/combatStore.ts > stores/combat-actions.ts +3. 2) utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.ts +4. 3) utils/floor-utils.ts > utils/room-utils.ts +5. 4) stores/gameStore.ts > stores/gameActions.ts +6. 5) stores/gameStore.ts > stores/gameLoopActions.ts +7. 6) stores/gameStore.ts > stores/tick-pipeline.ts ## How to fix 1. Identify which import in the chain can be extracted to a shared types/utils file. diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 937e8fa..9c7e120 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-26T16:28:28.147Z", + "generated": "2026-05-26T16:39:58.077Z", "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." }, @@ -488,6 +488,7 @@ "data/disciplines/enchanter.ts", "data/disciplines/fabricator.ts", "data/disciplines/invoker.ts", + "stores/combatStore.ts", "types.ts", "types/disciplines.ts", "utils/discipline-math.ts", diff --git a/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx b/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx index 602bf1d..5285151 100644 --- a/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx +++ b/src/components/game/tabs/DebugTab/DisciplineDebugSection.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { BookOpen, Plus, Pause, Play } from 'lucide-react'; import { useDisciplineStore } from '@/lib/game/stores/discipline-slice'; import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines'; +import { useManaStore } from '@/lib/game/stores/manaStore'; export function DisciplineDebugSection() { const disciplines = useDisciplineStore((s) => s.disciplines); @@ -12,6 +13,7 @@ export function DisciplineDebugSection() { const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit); const activate = useDisciplineStore((s) => s.activate); const deactivate = useDisciplineStore((s) => s.deactivate); + const elements = useManaStore((s) => s.elements); const _handleTogglePause = (id: string) => { const disc = disciplines[id]; @@ -41,7 +43,7 @@ export function DisciplineDebugSection() { const handleActivateAll = () => { ALL_DISCIPLINES.forEach((d) => { if (!activeIds.includes(d.id)) { - activate(d.id); + activate(d.id, { elements }); } }); }; @@ -111,7 +113,7 @@ export function DisciplineDebugSection() { if (isActive) { deactivate(def.id); } else { - activate(def.id); + activate(def.id, { elements }); } }} > diff --git a/src/components/game/tabs/DisciplinesTab.tsx b/src/components/game/tabs/DisciplinesTab.tsx index c924fa1..446cb91 100644 --- a/src/components/game/tabs/DisciplinesTab.tsx +++ b/src/components/game/tabs/DisciplinesTab.tsx @@ -12,6 +12,7 @@ import { fabricatorDisciplines } from '@/lib/game/data/disciplines/fabricator'; import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker'; import { calculateStatBonus, calculateManaDrain, checkDisciplinePrerequisites } from '@/lib/game/utils/discipline-math'; import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines'; +import { useManaStore } from '@/lib/game/stores/manaStore'; import clsx from 'clsx'; // ─── Attunement Tabs ───────────────────────────────────────────────────────── @@ -25,8 +26,8 @@ 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: 'elemental-regen', label: 'Elemental Flow', items: elementalRegenDisciplines }, + { key: 'elemental-regen-advanced', label: 'Advanced Flow', items: elementalRegenAdvancedDisciplines }, { key: 'enchanter', label: 'Enchanter', items: enchanterDisciplines }, { key: 'fabricator', label: 'Fabricator', items: fabricatorDisciplines }, { key: 'invoker', label: 'Invoker', items: invokerDisciplines }, @@ -50,6 +51,8 @@ export interface DisciplineCardDefinition { drainBase: number; difficultyFactor: number; scalingFactor: number; + sourceManaTypes?: ManaType[]; + conversionRate?: number; } export interface DisciplineCardRuntime { @@ -58,6 +61,7 @@ export interface DisciplineCardRuntime { concurrentLimit: number; isLocked: boolean; missingPrereqs: string[]; + missingSourceMana: string[]; } export interface DisciplineCardCallbacks { @@ -77,7 +81,7 @@ const DisciplineCard: React.FC = ({ definition, runtime, ca id, name, description, manaType, baseCost, perkThresholds, perkValues, perkTypes, statBonusLabel, baseValue, drainBase, difficultyFactor, scalingFactor, } = definition; - const { xp, paused: isPaused, concurrentLimit, isLocked, missingPrereqs } = runtime; + const { xp, paused: isPaused, concurrentLimit, isLocked, missingPrereqs, missingSourceMana } = runtime; const { onToggle } = callbacks; const displayXp = xp; @@ -91,6 +95,8 @@ const DisciplineCard: React.FC = ({ definition, runtime, ca const manaIcon = elementDef?.sym ?? '✦'; const manaName = elementDef?.name ?? manaType; + const effectiveIsLocked = isLocked || missingSourceMana.length > 0; + const unlockedPerks = perkTypes?.reduce((acc, typ, idx) => { const threshold = perkThresholds?.[idx]; if (threshold === undefined) return acc; @@ -109,7 +115,7 @@ const DisciplineCard: React.FC = ({ definition, runtime, ca }; return ( -
+

{name}

= ({ definition, runtime, ca
+ {definition.conversionRate != null && definition.sourceManaTypes && ( +
+ Converts: {definition.sourceManaTypes.map(s => s === 'raw' ? 'raw' : ELEMENTS[s]?.name ?? s).join(' + ')} → {manaName} +
+ )} +
Stat Bonus: {activeStatBonus.toFixed(2)} on {statBonusLabel}
@@ -166,16 +178,16 @@ const DisciplineCard: React.FC = ({ definition, runtime, ca
- {isLocked && missingPrereqs.length > 0 && ( + {effectiveIsLocked && (missingPrereqs.length > 0 || missingSourceMana.length > 0) && (
- Requires: {missingPrereqs.join(', ')} + Requires: {[...missingPrereqs, ...missingSourceMana].join(', ')}
)}
@@ -203,13 +215,15 @@ export const DisciplinesTab: React.FC = () => { const [activeAttunement, setActiveAttunement] = useState('base'); + const elements = useManaStore((s) => s.elements); + const handleToggle = useCallback((id: string, paused: boolean) => { if (paused) { - activate(id); + activate(id, { elements }); } else { deactivate(id); } - }, [activate, deactivate]); + }, [activate, deactivate, elements]); const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement); @@ -254,6 +268,8 @@ export const DisciplinesTab: React.FC = () => { statBonus: disc.statBonus.stat, statBonusLabel: disc.statBonus.label, requires: disc.requires, + sourceManaTypes: disc.sourceManaTypes, + conversionRate: disc.conversionRate, baseValue: disc.statBonus.baseValue, drainBase: disc.drainBase, difficultyFactor: disc.difficultyFactor, @@ -265,6 +281,11 @@ export const DisciplinesTab: React.FC = () => { concurrentLimit, isLocked: !prereqCheck.canProceed, missingPrereqs: prereqCheck.missingPrereqs, + missingSourceMana: disc.sourceManaTypes + ? disc.sourceManaTypes.filter( + (src) => src !== 'raw' && (!elements[src] || !elements[src].unlocked), + ).map((src) => `${src} mana`) + : [], }} callbacks={{ onToggle: handleToggle, diff --git a/src/lib/game/data/disciplines/elemental-regen-advanced.ts b/src/lib/game/data/disciplines/elemental-regen-advanced.ts index 24518e4..b1fc822 100644 --- a/src/lib/game/data/disciplines/elemental-regen-advanced.ts +++ b/src/lib/game/data/disciplines/elemental-regen-advanced.ts @@ -1,197 +1,233 @@ -// ─── Elemental Regen Disciplines (Composite + Exotic) ───────────────────────── -// Regen disciplines for composite and exotic mana types. +// ─── Elemental Conversion Disciplines (Composite + Exotic) ────────────────────── +// Conversion disciplines for composite and exotic mana types. // 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'; +// ── Composite Elements ───────────────────────────────────────────────────── + +const COMP_CONVERSION = 0.35; +const COMP_DRAIN = 2; +const COMP_DIFF = 160; +const COMP_SCALE = 80; + +const metalDiscipline: DisciplineDefinition = { + id: 'regen-metal', + name: 'Metal Mana Flow', + attunement: DisciplinesAttunementType.BASE, + manaType: 'metal', + baseCost: 12, + description: 'Convert raw mana + fire mana + earth mana into metal mana over time.', + statBonus: { stat: 'conversion_metal', baseValue: COMP_CONVERSION, label: 'Metal Conversion/tick' }, + difficultyFactor: COMP_DIFF, + scalingFactor: COMP_SCALE, + drainBase: COMP_DRAIN, + conversionRate: COMP_CONVERSION, + sourceManaTypes: ['raw', 'fire', 'earth'], + requires: ['metal'], + perks: [ + { + id: 'regen-metal-1', + type: 'once', + threshold: 150, + value: 0, + description: '+0.35 Metal Conversion/tick', + bonus: { stat: 'conversion_metal', amount: COMP_CONVERSION }, + }, + { + id: 'regen-metal-inf', + type: 'infinite', + threshold: 400, + value: 100, + description: 'Every 100 XP: +0.15 Metal Conversion/tick', + bonus: { stat: 'conversion_metal', amount: 0.15 }, + }, + ], +}; + +const sandDiscipline: DisciplineDefinition = { + id: 'regen-sand', + name: 'Sand Mana Flow', + attunement: DisciplinesAttunementType.BASE, + manaType: 'sand', + baseCost: 12, + description: 'Convert raw mana + earth mana + water mana into sand mana over time.', + statBonus: { stat: 'conversion_sand', baseValue: COMP_CONVERSION, label: 'Sand Conversion/tick' }, + difficultyFactor: COMP_DIFF, + scalingFactor: COMP_SCALE, + drainBase: COMP_DRAIN, + conversionRate: COMP_CONVERSION, + sourceManaTypes: ['raw', 'earth', 'water'], + requires: ['sand'], + perks: [ + { + id: 'regen-sand-1', + type: 'once', + threshold: 150, + value: 0, + description: '+0.35 Sand Conversion/tick', + bonus: { stat: 'conversion_sand', amount: COMP_CONVERSION }, + }, + { + id: 'regen-sand-inf', + type: 'infinite', + threshold: 400, + value: 100, + description: 'Every 100 XP: +0.15 Sand Conversion/tick', + bonus: { stat: 'conversion_sand', amount: 0.15 }, + }, + ], +}; + +const lightningDiscipline: DisciplineDefinition = { + id: 'regen-lightning', + name: 'Lightning Mana Flow', + attunement: DisciplinesAttunementType.BASE, + manaType: 'lightning', + baseCost: 12, + description: 'Convert raw mana + fire mana + air mana into lightning mana over time.', + statBonus: { stat: 'conversion_lightning', baseValue: COMP_CONVERSION, label: 'Lightning Conversion/tick' }, + difficultyFactor: COMP_DIFF, + scalingFactor: COMP_SCALE, + drainBase: COMP_DRAIN, + conversionRate: COMP_CONVERSION, + sourceManaTypes: ['raw', 'fire', 'air'], + requires: ['lightning'], + perks: [ + { + id: 'regen-lightning-1', + type: 'once', + threshold: 150, + value: 0, + description: '+0.35 Lightning Conversion/tick', + bonus: { stat: 'conversion_lightning', amount: COMP_CONVERSION }, + }, + { + id: 'regen-lightning-inf', + type: 'infinite', + threshold: 400, + value: 100, + description: 'Every 100 XP: +0.15 Lightning Conversion/tick', + bonus: { stat: 'conversion_lightning', amount: 0.15 }, + }, + ], +}; + +// ── Exotic Elements ──────────────────────────────────────────────────────── + +const EXO_CONVERSION = 0.25; +const EXO_DRAIN = 3; +const EXO_DIFF = 220; +const EXO_SCALE = 110; + +const crystalDiscipline: DisciplineDefinition = { + id: 'regen-crystal', + name: 'Crystal Mana Flow', + attunement: DisciplinesAttunementType.BASE, + manaType: 'crystal', + baseCost: 18, + description: 'Convert raw mana + sand mana + light mana into crystal mana over time.', + statBonus: { stat: 'conversion_crystal', baseValue: EXO_CONVERSION, label: 'Crystal Conversion/tick' }, + difficultyFactor: EXO_DIFF, + scalingFactor: EXO_SCALE, + drainBase: EXO_DRAIN, + conversionRate: EXO_CONVERSION, + sourceManaTypes: ['raw', 'sand', 'light'], + requires: ['crystal'], + perks: [ + { + id: 'regen-crystal-1', + type: 'once', + threshold: 200, + value: 0, + description: '+0.25 Crystal Conversion/tick', + bonus: { stat: 'conversion_crystal', amount: EXO_CONVERSION }, + }, + { + id: 'regen-crystal-inf', + type: 'infinite', + threshold: 500, + value: 100, + description: 'Every 100 XP: +0.1 Crystal Conversion/tick', + bonus: { stat: 'conversion_crystal', amount: 0.1 }, + }, + ], +}; + +const stellarDiscipline: DisciplineDefinition = { + id: 'regen-stellar', + name: 'Stellar Mana Flow', + attunement: DisciplinesAttunementType.BASE, + manaType: 'stellar', + baseCost: 18, + description: 'Convert raw mana + fire mana + light mana into stellar mana over time.', + statBonus: { stat: 'conversion_stellar', baseValue: EXO_CONVERSION, label: 'Stellar Conversion/tick' }, + difficultyFactor: EXO_DIFF, + scalingFactor: EXO_SCALE, + drainBase: EXO_DRAIN, + conversionRate: EXO_CONVERSION, + sourceManaTypes: ['raw', 'fire', 'light'], + requires: ['stellar'], + perks: [ + { + id: 'regen-stellar-1', + type: 'once', + threshold: 200, + value: 0, + description: '+0.25 Stellar Conversion/tick', + bonus: { stat: 'conversion_stellar', amount: EXO_CONVERSION }, + }, + { + id: 'regen-stellar-inf', + type: 'infinite', + threshold: 500, + value: 100, + description: 'Every 100 XP: +0.1 Stellar Conversion/tick', + bonus: { stat: 'conversion_stellar', amount: 0.1 }, + }, + ], +}; + +const voidDiscipline: DisciplineDefinition = { + id: 'regen-void', + name: 'Void Mana Flow', + attunement: DisciplinesAttunementType.BASE, + manaType: 'void', + baseCost: 18, + description: 'Convert raw mana + dark mana + death mana into void mana over time.', + statBonus: { stat: 'conversion_void', baseValue: EXO_CONVERSION, label: 'Void Conversion/tick' }, + difficultyFactor: EXO_DIFF, + scalingFactor: EXO_SCALE, + drainBase: EXO_DRAIN, + conversionRate: EXO_CONVERSION, + sourceManaTypes: ['raw', 'dark', 'death'], + requires: ['void'], + perks: [ + { + id: 'regen-void-1', + type: 'once', + threshold: 200, + value: 0, + description: '+0.25 Void Conversion/tick', + bonus: { stat: 'conversion_void', amount: EXO_CONVERSION }, + }, + { + id: 'regen-void-inf', + type: 'infinite', + threshold: 500, + value: 100, + description: 'Every 100 XP: +0.1 Void Conversion/tick', + bonus: { stat: 'conversion_void', amount: 0.1 }, + }, + ], +}; + export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [ - // ── Composite Elements ───────────────────────────────────────────────────── - { - id: 'regen-metal', - name: 'Metal Mana Flow', - attunement: DisciplinesAttunementType.BASE, - manaType: 'metal', - baseCost: 12, - description: 'Attune your metal mana to regenerate passively over time.', - statBonus: { stat: 'regen_metal', baseValue: 0.35, label: 'Metal Regen/tick' }, - difficultyFactor: 160, - scalingFactor: 80, - drainBase: 2, - requires: ['metal'], - perks: [ - { - id: 'regen-metal-1', - type: 'once', - threshold: 150, - value: 0, - description: '+0.35 Metal Regen/tick', - bonus: { stat: 'regen_metal', amount: 0.35 }, - }, - { - id: 'regen-metal-inf', - type: 'infinite', - threshold: 400, - value: 100, - description: 'Every 100 XP: +0.15 Metal Regen/tick', - bonus: { stat: 'regen_metal', amount: 0.15 }, - }, - ], - }, - { - id: 'regen-sand', - name: 'Sand Mana Flow', - attunement: DisciplinesAttunementType.BASE, - manaType: 'sand', - baseCost: 12, - description: 'Attune your sand mana to regenerate passively over time.', - statBonus: { stat: 'regen_sand', baseValue: 0.35, label: 'Sand Regen/tick' }, - difficultyFactor: 160, - scalingFactor: 80, - drainBase: 2, - requires: ['sand'], - perks: [ - { - id: 'regen-sand-1', - type: 'once', - threshold: 150, - value: 0, - description: '+0.35 Sand Regen/tick', - bonus: { stat: 'regen_sand', amount: 0.35 }, - }, - { - id: 'regen-sand-inf', - type: 'infinite', - threshold: 400, - value: 100, - description: 'Every 100 XP: +0.15 Sand Regen/tick', - bonus: { stat: 'regen_sand', amount: 0.15 }, - }, - ], - }, - { - id: 'regen-lightning', - name: 'Lightning Mana Flow', - attunement: DisciplinesAttunementType.BASE, - manaType: 'lightning', - baseCost: 12, - description: 'Attune your lightning mana to regenerate passively over time.', - statBonus: { stat: 'regen_lightning', baseValue: 0.35, label: 'Lightning Regen/tick' }, - difficultyFactor: 160, - scalingFactor: 80, - drainBase: 2, - requires: ['lightning'], - perks: [ - { - id: 'regen-lightning-1', - type: 'once', - threshold: 150, - value: 0, - description: '+0.35 Lightning Regen/tick', - bonus: { stat: 'regen_lightning', amount: 0.35 }, - }, - { - id: 'regen-lightning-inf', - type: 'infinite', - threshold: 400, - value: 100, - description: 'Every 100 XP: +0.15 Lightning Regen/tick', - bonus: { stat: 'regen_lightning', amount: 0.15 }, - }, - ], - }, - // ── Exotic Elements ──────────────────────────────────────────────────────── - { - id: 'regen-crystal', - name: 'Crystal Mana Flow', - attunement: DisciplinesAttunementType.BASE, - manaType: 'crystal', - baseCost: 18, - description: 'Attune your crystal mana to regenerate passively over time.', - statBonus: { stat: 'regen_crystal', baseValue: 0.25, label: 'Crystal Regen/tick' }, - difficultyFactor: 220, - scalingFactor: 110, - drainBase: 3, - requires: ['crystal'], - perks: [ - { - id: 'regen-crystal-1', - type: 'once', - threshold: 200, - value: 0, - description: '+0.25 Crystal Regen/tick', - bonus: { stat: 'regen_crystal', amount: 0.25 }, - }, - { - id: 'regen-crystal-inf', - type: 'infinite', - threshold: 500, - value: 100, - description: 'Every 100 XP: +0.1 Crystal Regen/tick', - bonus: { stat: 'regen_crystal', amount: 0.1 }, - }, - ], - }, - { - id: 'regen-stellar', - name: 'Stellar Mana Flow', - attunement: DisciplinesAttunementType.BASE, - manaType: 'stellar', - baseCost: 18, - description: 'Attune your stellar mana to regenerate passively over time.', - statBonus: { stat: 'regen_stellar', baseValue: 0.25, label: 'Stellar Regen/tick' }, - difficultyFactor: 220, - scalingFactor: 110, - drainBase: 3, - requires: ['stellar'], - perks: [ - { - id: 'regen-stellar-1', - type: 'once', - threshold: 200, - value: 0, - description: '+0.25 Stellar Regen/tick', - bonus: { stat: 'regen_stellar', amount: 0.25 }, - }, - { - id: 'regen-stellar-inf', - type: 'infinite', - threshold: 500, - value: 100, - description: 'Every 100 XP: +0.1 Stellar Regen/tick', - bonus: { stat: 'regen_stellar', amount: 0.1 }, - }, - ], - }, - { - id: 'regen-void', - name: 'Void Mana Flow', - attunement: DisciplinesAttunementType.BASE, - manaType: 'void', - baseCost: 18, - description: 'Attune your void mana to regenerate passively over time.', - statBonus: { stat: 'regen_void', baseValue: 0.25, label: 'Void Regen/tick' }, - difficultyFactor: 220, - scalingFactor: 110, - drainBase: 3, - requires: ['void'], - perks: [ - { - id: 'regen-void-1', - type: 'once', - threshold: 200, - value: 0, - description: '+0.25 Void Regen/tick', - bonus: { stat: 'regen_void', amount: 0.25 }, - }, - { - id: 'regen-void-inf', - type: 'infinite', - threshold: 500, - value: 100, - description: 'Every 100 XP: +0.1 Void Regen/tick', - bonus: { stat: 'regen_void', amount: 0.1 }, - }, - ], - }, + metalDiscipline, + sandDiscipline, + lightningDiscipline, + crystalDiscipline, + stellarDiscipline, + voidDiscipline, ]; diff --git a/src/lib/game/data/disciplines/elemental-regen.ts b/src/lib/game/data/disciplines/elemental-regen.ts index a5ed408..9e1c341 100644 --- a/src/lib/game/data/disciplines/elemental-regen.ts +++ b/src/lib/game/data/disciplines/elemental-regen.ts @@ -1,28 +1,38 @@ -// ─── Elemental Regen Disciplines (Base + Utility) ───────────────────────────── -// One discipline per mana type that provides passive regen for that element. +// ─── Elemental Conversion Disciplines (Base + Utility) ───────────────────────── +// One discipline per mana type that converts raw mana into that element. // 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'; -const BASE_REGEN = 0.5; +const BASE_CONVERSION = 0.5; const BASE_DRAIN = 1.5; const BASE_DIFF = 120; const BASE_SCALE = 60; -function makeBaseRegen(id: string, name: string, manaType: string, cost: number): DisciplineDefinition { - const shortId = id.replace('regen-', ''); +function makeBaseConversion( + id: string, + name: string, + manaType: string, + cost: number, +): DisciplineDefinition { return { id, name: `${name} Mana Flow`, attunement: DisciplinesAttunementType.BASE, 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, label: `${name} Regen/tick` }, + description: `Convert raw mana into ${name.toLowerCase()} mana over time.`, + statBonus: { + stat: `conversion_${manaType}` as DisciplineDefinition['statBonus']['stat'], + baseValue: BASE_CONVERSION, + label: `${name} Conversion/tick`, + }, difficultyFactor: BASE_DIFF, scalingFactor: BASE_SCALE, drainBase: BASE_DRAIN, + conversionRate: BASE_CONVERSION, + sourceManaTypes: ['raw' as DisciplineDefinition['manaType']], requires: [manaType], perks: [ { @@ -30,16 +40,16 @@ function makeBaseRegen(id: string, name: string, manaType: string, cost: number) type: 'once', threshold: 100, value: 0, - description: `+${BASE_REGEN} ${name} Regen/tick`, - bonus: { stat: `regen_${shortId}`, amount: BASE_REGEN }, + description: `+${BASE_CONVERSION} ${name} Conversion/tick`, + bonus: { stat: `conversion_${manaType}`, amount: BASE_CONVERSION }, }, { id: `${id}-inf`, type: 'infinite', threshold: 300, value: 100, - description: `Every 100 XP: +0.25 ${name} Regen/tick`, - bonus: { stat: `regen_${shortId}`, amount: 0.25 }, + description: `Every 100 XP: +0.25 ${name} Conversion/tick`, + bonus: { stat: `conversion_${manaType}`, amount: 0.25 }, }, ], }; @@ -47,13 +57,13 @@ function makeBaseRegen(id: string, name: string, manaType: string, cost: number) export const elementalRegenDisciplines: DisciplineDefinition[] = [ // ── Base Elements ────────────────────────────────────────────────────────── - makeBaseRegen('regen-fire', 'Fire', 'fire', 8), - makeBaseRegen('regen-water', 'Water', 'water', 8), - makeBaseRegen('regen-air', 'Air', 'air', 8), - makeBaseRegen('regen-earth', 'Earth', 'earth', 8), - makeBaseRegen('regen-light', 'Light', 'light', 8), - makeBaseRegen('regen-dark', 'Dark', 'dark', 8), - makeBaseRegen('regen-death', 'Death', 'death', 8), + makeBaseConversion('regen-fire', 'Fire', 'fire', 8), + makeBaseConversion('regen-water', 'Water', 'water', 8), + makeBaseConversion('regen-air', 'Air', 'air', 8), + makeBaseConversion('regen-earth', 'Earth', 'earth', 8), + makeBaseConversion('regen-light', 'Light', 'light', 8), + makeBaseConversion('regen-dark', 'Dark', 'dark', 8), + makeBaseConversion('regen-death', 'Death', 'death', 8), // ── Utility Element ──────────────────────────────────────────────────────── { @@ -62,11 +72,13 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [ attunement: DisciplinesAttunementType.BASE, manaType: 'transference', baseCost: 6, - description: 'Attune your transference mana to regenerate passively over time.', - statBonus: { stat: 'regen_transference', baseValue: 0.4, label: 'Transference Regen/tick' }, + description: 'Convert raw mana into transference mana over time.', + statBonus: { stat: 'conversion_transference', baseValue: 0.4, label: 'Transference Conversion/tick' }, difficultyFactor: 100, scalingFactor: 50, drainBase: 1, + conversionRate: 0.4, + sourceManaTypes: ['transference'], requires: ['transference'], perks: [ { @@ -74,16 +86,16 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [ type: 'once', threshold: 100, value: 0, - description: '+0.4 Transference Regen/tick', - bonus: { stat: 'regen_transference', amount: 0.4 }, + description: '+0.4 Transference Conversion/tick', + bonus: { stat: 'conversion_transference', amount: 0.4 }, }, { id: 'regen-transference-inf', type: 'infinite', threshold: 300, value: 100, - description: 'Every 100 XP: +0.2 Transference Regen/tick', - bonus: { stat: 'regen_transference', amount: 0.2 }, + description: 'Every 100 XP: +0.2 Transference Conversion/tick', + bonus: { stat: 'conversion_transference', amount: 0.2 }, }, ], }, diff --git a/src/lib/game/effects.ts b/src/lib/game/effects.ts index fa0ff00..64d1378 100644 --- a/src/lib/game/effects.ts +++ b/src/lib/game/effects.ts @@ -93,14 +93,10 @@ export function computeAllEffects( } } - // Merge per-element regen from discipline effects (regen_{element}) + // Per-element regen from discipline effects (regen_{element}) — no longer produced + // by elemental disciplines (they now use conversion instead). + // Kept for backward compatibility with any upgrade effects that may provide per-element regen. const perElementRegenBonus: Record = { ...upgradeEffects.perElementRegenBonus }; - for (const [key, value] of Object.entries(disciplineEffects.bonuses)) { - if (key.startsWith('regen_') && key !== 'regenBonus') { - const element = key.replace('regen_', ''); - perElementRegenBonus[element] = (perElementRegenBonus[element] || 0) + value; - } - } // Merge per-element cap bonuses from discipline effects (elementCap_{element}) for (const [key, value] of Object.entries(disciplineEffects.bonuses)) { diff --git a/src/lib/game/effects/discipline-effects.ts b/src/lib/game/effects/discipline-effects.ts index 1f215cd..1307a2e 100644 --- a/src/lib/game/effects/discipline-effects.ts +++ b/src/lib/game/effects/discipline-effects.ts @@ -23,11 +23,19 @@ const KNOWN_BONUS_STATS = new Set([ 'elementCapBonus', ]); -export function computeDisciplineEffects(_state?: DisciplineStoreState): { +export interface DisciplineEffectsResult { bonuses: Record; multipliers: Record; specials: Set; -} { + /** + * Conversion entries: for each active discipline with a conversionRate, + * maps target mana type → { rate, sourceManaTypes }. + * The tick pipeline drains source mana types and adds to the target. + */ + conversions: Record; +} + +export function computeDisciplineEffects(_state?: DisciplineStoreState): DisciplineEffectsResult { const { disciplines } = useDisciplineStore.getState(); const activeDiscs = Object.entries(disciplines) .filter(([, disc]) => disc && disc.xp > 0) @@ -37,6 +45,7 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): { const bonuses: Record = {}; const multipliers: Record = {}; const specials = new Set(); + const conversions: Record = {}; function addBonus(stat: string, amount: number) { bonuses[stat] = (bonuses[stat] || 0) + amount; @@ -49,6 +58,16 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): { addBonus(def.statBonus.stat, statBonus); } + // Conversion entry — if this discipline defines conversionRate + if (def.conversionRate && def.sourceManaTypes && def.sourceManaTypes.length > 0) { + // Scale the conversion rate by the stat bonus multiplier + const scaledRate = def.conversionRate + statBonus; + conversions[def.manaType] = { + rate: scaledRate, + sourceManaTypes: def.sourceManaTypes, + }; + } + // Perk unlocks const perks = getUnlockedPerks(def, disc.xp); for (const perk of perks) { @@ -56,7 +75,6 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): { if (perk.bonus) { addBonus(perk.bonus.stat, perk.bonus.amount); } else if (!perk.unlocksEffects) { - // Fallback: qualitative perk with no structured bonus — add as special flag specials.add(perk.id); } // Perks with unlocksEffects are handled by discipline-slice.ts processTick() @@ -83,5 +101,5 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): { } } - return { bonuses, multipliers, specials }; + return { bonuses, multipliers, specials, conversions }; } diff --git a/src/lib/game/stores/discipline-slice.ts b/src/lib/game/stores/discipline-slice.ts index 67848d0..74bc3e9 100644 --- a/src/lib/game/stores/discipline-slice.ts +++ b/src/lib/game/stores/discipline-slice.ts @@ -89,9 +89,23 @@ export const useDisciplineStore = create()( }).length; if (nonPaused >= s.concurrentLimit) return s; if (!canProceedDiscipline(def, existing, gameState)) return s; + + // Check discipline prerequisites (requires field → discipline XP) const prereqCheck = checkDisciplinePrerequisites(def, s.disciplines, ALL_DISCIPLINES); if (!prereqCheck.canProceed) return s; + // For conversion disciplines: gate on having all source mana types unlocked + if (def.sourceManaTypes && def.sourceManaTypes.length > 0) { + const elements = gameState?.elements; + if (elements) { + for (const srcType of def.sourceManaTypes) { + if (srcType === 'raw') continue; // raw is always available + const srcElem = elements[srcType]; + if (!srcElem || !srcElem.unlocked) return s; + } + } + } + const discState = existing || { id, xp: 0, paused: false }; // Set currentAction to 'practicing' (only overrides 'meditate') useCombatStore.getState().startPracticing(); diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index 2a7f775..273a99c 100644 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -263,22 +263,45 @@ export const useGameStore = create()( rawMana = disciplineResult.rawMana; elements = disciplineResult.elements; - // Apply per-element regen from discipline effects (regen_{element}) - for (const [key, value] of Object.entries(disciplineEffects.bonuses)) { - if (key.startsWith('regen_') && key !== 'regenBonus') { - const element = key.replace('regen_', ''); - if (elements[element]) { - elements[element] = { - ...elements[element], - current: Math.min( - elements[element].max, - elements[element].current + value * HOURS_PER_TICK, - ), + // Apply discipline conversions: drain source mana, add to target element + for (const [targetElem, conv] of Object.entries(disciplineEffects.conversions)) { + const conversionAmount = conv.rate * HOURS_PER_TICK; + // Check that all source mana types are available (unlocked and have enough) + let canConvert = true; + for (const srcType of conv.sourceManaTypes) { + if (srcType === 'raw') { + if (rawMana < conversionAmount) { + canConvert = false; + break; + } + } else if (!elements[srcType] || !elements[srcType].unlocked || elements[srcType].current < conversionAmount) { + canConvert = false; + break; + } + } + if (!canConvert) continue; + // Drain source mana types + for (const srcType of conv.sourceManaTypes) { + if (srcType === 'raw') { + rawMana -= conversionAmount; + } else if (elements[srcType]) { + elements[srcType] = { + ...elements[srcType], + current: elements[srcType].current - conversionAmount, }; } } + // Add to target element + if (elements[targetElem]) { + elements[targetElem] = { + ...elements[targetElem], + current: Math.min( + elements[targetElem].max, + elements[targetElem].current + conversionAmount, + ), + }; + } } - // Unlock enchantment effects from newly unlocked discipline perks if (disciplineResult.unlockedEffects.length > 0) { useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects); diff --git a/src/lib/game/types/disciplines.ts b/src/lib/game/types/disciplines.ts index 34cfd0e..5bdc8f5 100644 --- a/src/lib/game/types/disciplines.ts +++ b/src/lib/game/types/disciplines.ts @@ -42,6 +42,18 @@ export interface DisciplineDefinition { drainBase: number; perks: DisciplinePerk[]; requires?: string[]; + /** + * If set, this discipline converts source mana types into the target manaType + * instead of providing passive regen. The conversion rate is the amount of + * target mana produced per tick (before XP scaling). + */ + conversionRate?: number; + /** + * Mana types consumed as input for conversion. Always includes 'raw'. + * For composites/exotics, also includes constituent element(s). + * Example: ['raw'] for base, ['raw', 'fire', 'earth'] for metal. + */ + sourceManaTypes?: ManaType[]; } // ─── Discipline State ───────────────────────────────────────────────────────── diff --git a/src/lib/game/utils/discipline-math.ts b/src/lib/game/utils/discipline-math.ts index a5b9150..964c1d4 100644 --- a/src/lib/game/utils/discipline-math.ts +++ b/src/lib/game/utils/discipline-math.ts @@ -87,10 +87,31 @@ export function canProceedDiscipline( return element && element.current >= drain; } +// ─── Known mana type names for display ──────────────────────────────────────── +const MANA_TYPE_NAMES: Record = { + raw: 'raw', + fire: 'fire', + water: 'water', + air: 'air', + earth: 'earth', + light: 'light', + dark: 'dark', + death: 'death', + transference: 'transference', + metal: 'metal', + sand: 'sand', + lightning: 'lightning', + crystal: 'crystal', + stellar: 'stellar', + void: 'void', +}; + /** * Check if a discipline's prerequisites are met. + * For disciplines with sourceManaTypes (conversion disciplines), requires are + * mana type IDs that must be unlocked (e.g. fire, earth). * Returns { canProceed: boolean, missingPrereqs: string[] } - * where missingPrereqs is a list of prerequisite discipline names that are not yet unlocked. + * where missingPrereqs is a list of human-readable prerequisite descriptions. */ export function checkDisciplinePrerequisites( discipline: DisciplineDefinition, @@ -104,11 +125,21 @@ export function checkDisciplinePrerequisites( 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); + // Check if this is a discipline prerequisite (exists in definitions) + const reqDef = allDefinitions.find((d) => d.id === reqId); + if (reqDef) { + const reqState = allDisciplines[reqId]; + if (!reqState || reqState.xp <= 0) { + missingPrereqs.push(reqDef?.name ?? reqId); + } + } else { + // Treat as mana type requirement — display as " mana" + const typeName = MANA_TYPE_NAMES[reqId]; + if (typeName) { + missingPrereqs.push(`${typeName} mana`); + } else { + missingPrereqs.push(reqId); + } } }