refactor: Replace natural-regen disciplines with mana conversion speed disciplines
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m25s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m25s
- Add conversionRate + sourceManaTypes fields to DisciplineDefinition
- Rewrite elemental-regen.ts: 8 base disciplines now convert raw→element
- Rewrite elemental-regen-advanced.ts: 6 composite/exotic disciplines with proper source recipes
- Update discipline-effects.ts: produce conversion entries instead of regen bonuses
- Update gameStore.ts tick: drain source mana types, add to target element
- Update discipline-slice.ts: gate activation on source mana type access
- Update discipline-math.ts: resolve mana type IDs to 'X mana' display names
- Update DisciplinesTab.tsx: show conversion info, source requirements, and lock state
- Update DisciplineDebugSection.tsx: pass elements to activate()
- Update effects.ts: remove regen_{element} merge (no longer produced)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<DisciplineCardProps> = ({ 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<DisciplineCardProps> = ({ definition, runtime, ca
|
||||
const manaIcon = elementDef?.sym ?? '✦';
|
||||
const manaName = elementDef?.name ?? manaType;
|
||||
|
||||
const effectiveIsLocked = isLocked || missingSourceMana.length > 0;
|
||||
|
||||
const unlockedPerks = perkTypes?.reduce<string[]>((acc, typ, idx) => {
|
||||
const threshold = perkThresholds?.[idx];
|
||||
if (threshold === undefined) return acc;
|
||||
@@ -109,7 +115,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={id} className={clsx('border rounded-lg p-4 shadow-sm space-y-3', isLocked && 'opacity-60 border-gray-600')}>
|
||||
<div key={id} className={clsx('border rounded-lg p-4 shadow-sm space-y-3', effectiveIsLocked && 'opacity-60 border-gray-600')}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-lg font-medium">{name}</h3>
|
||||
<span
|
||||
@@ -149,6 +155,12 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{definition.conversionRate != null && definition.sourceManaTypes && (
|
||||
<div className="mt-2 text-xs text-gray-400">
|
||||
<strong>Converts:</strong> {definition.sourceManaTypes.map(s => s === 'raw' ? 'raw' : ELEMENTS[s]?.name ?? s).join(' + ')} → {manaName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 text-sm">
|
||||
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonusLabel}
|
||||
</div>
|
||||
@@ -166,16 +178,16 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{isLocked && missingPrereqs.length > 0 && (
|
||||
{effectiveIsLocked && (missingPrereqs.length > 0 || missingSourceMana.length > 0) && (
|
||||
<div className="mt-2 text-xs text-red-400">
|
||||
<strong>Requires:</strong> {missingPrereqs.join(', ')}
|
||||
<strong>Requires:</strong> {[...missingPrereqs, ...missingSourceMana].join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex justify-end">
|
||||
<button
|
||||
onClick={toggleAction}
|
||||
disabled={isLocked}
|
||||
disabled={effectiveIsLocked}
|
||||
className={clsx(
|
||||
'rounded px-3 py-1 text-sm font-medium',
|
||||
isLocked
|
||||
@@ -185,7 +197,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
||||
: 'bg-blue-600 text-white hover:bg-blue-500',
|
||||
)}
|
||||
>
|
||||
{isLocked ? 'Locked' : isPaused ? 'Start Practicing' : 'Stop Practicing'}
|
||||
{effectiveIsLocked ? 'Locked' : isPaused ? 'Start Practicing' : 'Stop Practicing'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,13 +215,15 @@ export const DisciplinesTab: React.FC = () => {
|
||||
|
||||
const [activeAttunement, setActiveAttunement] = useState<string>('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,
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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<string, number> = { ...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)) {
|
||||
|
||||
@@ -23,11 +23,19 @@ const KNOWN_BONUS_STATS = new Set([
|
||||
'elementCapBonus',
|
||||
]);
|
||||
|
||||
export function computeDisciplineEffects(_state?: DisciplineStoreState): {
|
||||
export interface DisciplineEffectsResult {
|
||||
bonuses: Record<string, number>;
|
||||
multipliers: Record<string, number>;
|
||||
specials: Set<string>;
|
||||
} {
|
||||
/**
|
||||
* 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<string, { rate: number; sourceManaTypes: string[] }>;
|
||||
}
|
||||
|
||||
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<string, number> = {};
|
||||
const multipliers: Record<string, number> = {};
|
||||
const specials = new Set<string>();
|
||||
const conversions: Record<string, { rate: number; sourceManaTypes: string[] }> = {};
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -89,9 +89,23 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
||||
}).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();
|
||||
|
||||
@@ -263,22 +263,45 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
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);
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -87,10 +87,31 @@ export function canProceedDiscipline(
|
||||
return element && element.current >= drain;
|
||||
}
|
||||
|
||||
// ─── Known mana type names for display ────────────────────────────────────────
|
||||
const MANA_TYPE_NAMES: Record<string, string> = {
|
||||
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 "<name> mana"
|
||||
const typeName = MANA_TYPE_NAMES[reqId];
|
||||
if (typeName) {
|
||||
missingPrereqs.push(`${typeName} mana`);
|
||||
} else {
|
||||
missingPrereqs.push(reqId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user