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
|
# Circular Dependencies
|
||||||
Generated: 2026-05-26T16:28:29.780Z
|
Generated: 2026-05-26T16:39:59.755Z
|
||||||
Found: 6 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 7 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. Processed 135 files (1.5s) (2 warnings)
|
1. Processed 135 files (1.5s) (2 warnings)
|
||||||
2. 1) utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.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
|
3. 2) utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.ts
|
||||||
4. 3) stores/gameStore.ts > stores/gameActions.ts
|
4. 3) utils/floor-utils.ts > utils/room-utils.ts
|
||||||
5. 4) stores/gameStore.ts > stores/gameLoopActions.ts
|
5. 4) stores/gameStore.ts > stores/gameActions.ts
|
||||||
6. 5) stores/gameStore.ts > stores/tick-pipeline.ts
|
6. 5) stores/gameStore.ts > stores/gameLoopActions.ts
|
||||||
|
7. 6) stores/gameStore.ts > stores/tick-pipeline.ts
|
||||||
|
|
||||||
## How to fix
|
## How to fix
|
||||||
1. Identify which import in the chain can be extracted to a shared types/utils file.
|
1. Identify which import in the chain can be extracted to a shared types/utils file.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_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.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
@@ -488,6 +488,7 @@
|
|||||||
"data/disciplines/enchanter.ts",
|
"data/disciplines/enchanter.ts",
|
||||||
"data/disciplines/fabricator.ts",
|
"data/disciplines/fabricator.ts",
|
||||||
"data/disciplines/invoker.ts",
|
"data/disciplines/invoker.ts",
|
||||||
|
"stores/combatStore.ts",
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"types/disciplines.ts",
|
"types/disciplines.ts",
|
||||||
"utils/discipline-math.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 { BookOpen, Plus, Pause, Play } from 'lucide-react';
|
||||||
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
||||||
import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines';
|
import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines';
|
||||||
|
import { useManaStore } from '@/lib/game/stores/manaStore';
|
||||||
|
|
||||||
export function DisciplineDebugSection() {
|
export function DisciplineDebugSection() {
|
||||||
const disciplines = useDisciplineStore((s) => s.disciplines);
|
const disciplines = useDisciplineStore((s) => s.disciplines);
|
||||||
@@ -12,6 +13,7 @@ export function DisciplineDebugSection() {
|
|||||||
const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit);
|
const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit);
|
||||||
const activate = useDisciplineStore((s) => s.activate);
|
const activate = useDisciplineStore((s) => s.activate);
|
||||||
const deactivate = useDisciplineStore((s) => s.deactivate);
|
const deactivate = useDisciplineStore((s) => s.deactivate);
|
||||||
|
const elements = useManaStore((s) => s.elements);
|
||||||
|
|
||||||
const _handleTogglePause = (id: string) => {
|
const _handleTogglePause = (id: string) => {
|
||||||
const disc = disciplines[id];
|
const disc = disciplines[id];
|
||||||
@@ -41,7 +43,7 @@ export function DisciplineDebugSection() {
|
|||||||
const handleActivateAll = () => {
|
const handleActivateAll = () => {
|
||||||
ALL_DISCIPLINES.forEach((d) => {
|
ALL_DISCIPLINES.forEach((d) => {
|
||||||
if (!activeIds.includes(d.id)) {
|
if (!activeIds.includes(d.id)) {
|
||||||
activate(d.id);
|
activate(d.id, { elements });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -111,7 +113,7 @@ export function DisciplineDebugSection() {
|
|||||||
if (isActive) {
|
if (isActive) {
|
||||||
deactivate(def.id);
|
deactivate(def.id);
|
||||||
} else {
|
} 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 { invokerDisciplines } from '@/lib/game/data/disciplines/invoker';
|
||||||
import { calculateStatBonus, calculateManaDrain, checkDisciplinePrerequisites } 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 { ALL_DISCIPLINES } from '@/lib/game/data/disciplines';
|
||||||
|
import { useManaStore } from '@/lib/game/stores/manaStore';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
// ─── Attunement Tabs ─────────────────────────────────────────────────────────
|
// ─── Attunement Tabs ─────────────────────────────────────────────────────────
|
||||||
@@ -25,8 +26,8 @@ interface AttunementTab {
|
|||||||
const ATTUNEMENT_TABS: AttunementTab[] = [
|
const ATTUNEMENT_TABS: AttunementTab[] = [
|
||||||
{ key: 'base', label: 'Base', items: baseDisciplines },
|
{ key: 'base', label: 'Base', items: baseDisciplines },
|
||||||
{ key: 'elements', label: 'Mana Types', items: elementalAttunementDisciplines },
|
{ key: 'elements', label: 'Mana Types', items: elementalAttunementDisciplines },
|
||||||
{ key: 'elemental-regen', label: 'Elemental Regen', items: elementalRegenDisciplines },
|
{ key: 'elemental-regen', label: 'Elemental Flow', items: elementalRegenDisciplines },
|
||||||
{ key: 'elemental-regen-advanced', label: 'Advanced Regen', items: elementalRegenAdvancedDisciplines },
|
{ key: 'elemental-regen-advanced', label: 'Advanced Flow', items: elementalRegenAdvancedDisciplines },
|
||||||
{ key: 'enchanter', label: 'Enchanter', items: enchanterDisciplines },
|
{ key: 'enchanter', label: 'Enchanter', items: enchanterDisciplines },
|
||||||
{ key: 'fabricator', label: 'Fabricator', items: fabricatorDisciplines },
|
{ key: 'fabricator', label: 'Fabricator', items: fabricatorDisciplines },
|
||||||
{ key: 'invoker', label: 'Invoker', items: invokerDisciplines },
|
{ key: 'invoker', label: 'Invoker', items: invokerDisciplines },
|
||||||
@@ -50,6 +51,8 @@ export interface DisciplineCardDefinition {
|
|||||||
drainBase: number;
|
drainBase: number;
|
||||||
difficultyFactor: number;
|
difficultyFactor: number;
|
||||||
scalingFactor: number;
|
scalingFactor: number;
|
||||||
|
sourceManaTypes?: ManaType[];
|
||||||
|
conversionRate?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DisciplineCardRuntime {
|
export interface DisciplineCardRuntime {
|
||||||
@@ -58,6 +61,7 @@ export interface DisciplineCardRuntime {
|
|||||||
concurrentLimit: number;
|
concurrentLimit: number;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
missingPrereqs: string[];
|
missingPrereqs: string[];
|
||||||
|
missingSourceMana: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DisciplineCardCallbacks {
|
export interface DisciplineCardCallbacks {
|
||||||
@@ -77,7 +81,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
id, name, description, manaType, baseCost, perkThresholds, perkValues, perkTypes,
|
id, name, description, manaType, baseCost, perkThresholds, perkValues, perkTypes,
|
||||||
statBonusLabel, baseValue, drainBase, difficultyFactor, scalingFactor,
|
statBonusLabel, baseValue, drainBase, difficultyFactor, scalingFactor,
|
||||||
} = definition;
|
} = definition;
|
||||||
const { xp, paused: isPaused, concurrentLimit, isLocked, missingPrereqs } = runtime;
|
const { xp, paused: isPaused, concurrentLimit, isLocked, missingPrereqs, missingSourceMana } = runtime;
|
||||||
const { onToggle } = callbacks;
|
const { onToggle } = callbacks;
|
||||||
|
|
||||||
const displayXp = xp;
|
const displayXp = xp;
|
||||||
@@ -91,6 +95,8 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
const manaIcon = elementDef?.sym ?? '✦';
|
const manaIcon = elementDef?.sym ?? '✦';
|
||||||
const manaName = elementDef?.name ?? manaType;
|
const manaName = elementDef?.name ?? manaType;
|
||||||
|
|
||||||
|
const effectiveIsLocked = isLocked || missingSourceMana.length > 0;
|
||||||
|
|
||||||
const unlockedPerks = perkTypes?.reduce<string[]>((acc, typ, idx) => {
|
const unlockedPerks = perkTypes?.reduce<string[]>((acc, typ, idx) => {
|
||||||
const threshold = perkThresholds?.[idx];
|
const threshold = perkThresholds?.[idx];
|
||||||
if (threshold === undefined) return acc;
|
if (threshold === undefined) return acc;
|
||||||
@@ -109,7 +115,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h3 className="text-lg font-medium">{name}</h3>
|
<h3 className="text-lg font-medium">{name}</h3>
|
||||||
<span
|
<span
|
||||||
@@ -149,6 +155,12 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div className="mt-2 text-sm">
|
||||||
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonusLabel}
|
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonusLabel}
|
||||||
</div>
|
</div>
|
||||||
@@ -166,16 +178,16 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLocked && missingPrereqs.length > 0 && (
|
{effectiveIsLocked && (missingPrereqs.length > 0 || missingSourceMana.length > 0) && (
|
||||||
<div className="mt-2 text-xs text-red-400">
|
<div className="mt-2 text-xs text-red-400">
|
||||||
<strong>Requires:</strong> {missingPrereqs.join(', ')}
|
<strong>Requires:</strong> {[...missingPrereqs, ...missingSourceMana].join(', ')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4 flex justify-end">
|
<div className="mt-4 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={toggleAction}
|
onClick={toggleAction}
|
||||||
disabled={isLocked}
|
disabled={effectiveIsLocked}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded px-3 py-1 text-sm font-medium',
|
'rounded px-3 py-1 text-sm font-medium',
|
||||||
isLocked
|
isLocked
|
||||||
@@ -185,7 +197,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
|
|||||||
: 'bg-blue-600 text-white hover:bg-blue-500',
|
: 'bg-blue-600 text-white hover:bg-blue-500',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLocked ? 'Locked' : isPaused ? 'Start Practicing' : 'Stop Practicing'}
|
{effectiveIsLocked ? 'Locked' : isPaused ? 'Start Practicing' : 'Stop Practicing'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,13 +215,15 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
|
|
||||||
const [activeAttunement, setActiveAttunement] = useState<string>('base');
|
const [activeAttunement, setActiveAttunement] = useState<string>('base');
|
||||||
|
|
||||||
|
const elements = useManaStore((s) => s.elements);
|
||||||
|
|
||||||
const handleToggle = useCallback((id: string, paused: boolean) => {
|
const handleToggle = useCallback((id: string, paused: boolean) => {
|
||||||
if (paused) {
|
if (paused) {
|
||||||
activate(id);
|
activate(id, { elements });
|
||||||
} else {
|
} else {
|
||||||
deactivate(id);
|
deactivate(id);
|
||||||
}
|
}
|
||||||
}, [activate, deactivate]);
|
}, [activate, deactivate, elements]);
|
||||||
|
|
||||||
const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement);
|
const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement);
|
||||||
|
|
||||||
@@ -254,6 +268,8 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
statBonus: disc.statBonus.stat,
|
statBonus: disc.statBonus.stat,
|
||||||
statBonusLabel: disc.statBonus.label,
|
statBonusLabel: disc.statBonus.label,
|
||||||
requires: disc.requires,
|
requires: disc.requires,
|
||||||
|
sourceManaTypes: disc.sourceManaTypes,
|
||||||
|
conversionRate: disc.conversionRate,
|
||||||
baseValue: disc.statBonus.baseValue,
|
baseValue: disc.statBonus.baseValue,
|
||||||
drainBase: disc.drainBase,
|
drainBase: disc.drainBase,
|
||||||
difficultyFactor: disc.difficultyFactor,
|
difficultyFactor: disc.difficultyFactor,
|
||||||
@@ -265,6 +281,11 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
concurrentLimit,
|
concurrentLimit,
|
||||||
isLocked: !prereqCheck.canProceed,
|
isLocked: !prereqCheck.canProceed,
|
||||||
missingPrereqs: prereqCheck.missingPrereqs,
|
missingPrereqs: prereqCheck.missingPrereqs,
|
||||||
|
missingSourceMana: disc.sourceManaTypes
|
||||||
|
? disc.sourceManaTypes.filter(
|
||||||
|
(src) => src !== 'raw' && (!elements[src] || !elements[src].unlocked),
|
||||||
|
).map((src) => `${src} mana`)
|
||||||
|
: [],
|
||||||
}}
|
}}
|
||||||
callbacks={{
|
callbacks={{
|
||||||
onToggle: handleToggle,
|
onToggle: handleToggle,
|
||||||
|
|||||||
@@ -1,197 +1,233 @@
|
|||||||
// ─── Elemental Regen Disciplines (Composite + Exotic) ─────────────────────────
|
// ─── Elemental Conversion Disciplines (Composite + Exotic) ──────────────────────
|
||||||
// Regen disciplines for composite and exotic mana types.
|
// Conversion disciplines for composite and exotic mana types.
|
||||||
// All are BASE attunement so they are available to every role once the element is unlocked.
|
// All are BASE attunement so they are available to every role once the element is unlocked.
|
||||||
|
|
||||||
import { DisciplinesAttunementType } from '../../types/disciplines';
|
import { DisciplinesAttunementType } from '../../types/disciplines';
|
||||||
import type { DisciplineDefinition } 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[] = [
|
export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
|
||||||
// ── Composite Elements ─────────────────────────────────────────────────────
|
metalDiscipline,
|
||||||
{
|
sandDiscipline,
|
||||||
id: 'regen-metal',
|
lightningDiscipline,
|
||||||
name: 'Metal Mana Flow',
|
crystalDiscipline,
|
||||||
attunement: DisciplinesAttunementType.BASE,
|
stellarDiscipline,
|
||||||
manaType: 'metal',
|
voidDiscipline,
|
||||||
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 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,28 +1,38 @@
|
|||||||
// ─── Elemental Regen Disciplines (Base + Utility) ─────────────────────────────
|
// ─── Elemental Conversion Disciplines (Base + Utility) ─────────────────────────
|
||||||
// One discipline per mana type that provides passive regen for that element.
|
// 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.
|
// All are BASE attunement so they are available to every role once the element is unlocked.
|
||||||
|
|
||||||
import { DisciplinesAttunementType } from '../../types/disciplines';
|
import { DisciplinesAttunementType } from '../../types/disciplines';
|
||||||
import type { DisciplineDefinition } 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_DRAIN = 1.5;
|
||||||
const BASE_DIFF = 120;
|
const BASE_DIFF = 120;
|
||||||
const BASE_SCALE = 60;
|
const BASE_SCALE = 60;
|
||||||
|
|
||||||
function makeBaseRegen(id: string, name: string, manaType: string, cost: number): DisciplineDefinition {
|
function makeBaseConversion(
|
||||||
const shortId = id.replace('regen-', '');
|
id: string,
|
||||||
|
name: string,
|
||||||
|
manaType: string,
|
||||||
|
cost: number,
|
||||||
|
): DisciplineDefinition {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: `${name} Mana Flow`,
|
name: `${name} Mana Flow`,
|
||||||
attunement: DisciplinesAttunementType.BASE,
|
attunement: DisciplinesAttunementType.BASE,
|
||||||
manaType: manaType as DisciplineDefinition['manaType'],
|
manaType: manaType as DisciplineDefinition['manaType'],
|
||||||
baseCost: cost,
|
baseCost: cost,
|
||||||
description: `Attune your ${name.toLowerCase()} mana to regenerate passively over time.`,
|
description: `Convert raw mana into ${name.toLowerCase()} mana over time.`,
|
||||||
statBonus: { stat: `regen_${shortId}` as DisciplineDefinition['statBonus']['stat'], baseValue: BASE_REGEN, label: `${name} Regen/tick` },
|
statBonus: {
|
||||||
|
stat: `conversion_${manaType}` as DisciplineDefinition['statBonus']['stat'],
|
||||||
|
baseValue: BASE_CONVERSION,
|
||||||
|
label: `${name} Conversion/tick`,
|
||||||
|
},
|
||||||
difficultyFactor: BASE_DIFF,
|
difficultyFactor: BASE_DIFF,
|
||||||
scalingFactor: BASE_SCALE,
|
scalingFactor: BASE_SCALE,
|
||||||
drainBase: BASE_DRAIN,
|
drainBase: BASE_DRAIN,
|
||||||
|
conversionRate: BASE_CONVERSION,
|
||||||
|
sourceManaTypes: ['raw' as DisciplineDefinition['manaType']],
|
||||||
requires: [manaType],
|
requires: [manaType],
|
||||||
perks: [
|
perks: [
|
||||||
{
|
{
|
||||||
@@ -30,16 +40,16 @@ function makeBaseRegen(id: string, name: string, manaType: string, cost: number)
|
|||||||
type: 'once',
|
type: 'once',
|
||||||
threshold: 100,
|
threshold: 100,
|
||||||
value: 0,
|
value: 0,
|
||||||
description: `+${BASE_REGEN} ${name} Regen/tick`,
|
description: `+${BASE_CONVERSION} ${name} Conversion/tick`,
|
||||||
bonus: { stat: `regen_${shortId}`, amount: BASE_REGEN },
|
bonus: { stat: `conversion_${manaType}`, amount: BASE_CONVERSION },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `${id}-inf`,
|
id: `${id}-inf`,
|
||||||
type: 'infinite',
|
type: 'infinite',
|
||||||
threshold: 300,
|
threshold: 300,
|
||||||
value: 100,
|
value: 100,
|
||||||
description: `Every 100 XP: +0.25 ${name} Regen/tick`,
|
description: `Every 100 XP: +0.25 ${name} Conversion/tick`,
|
||||||
bonus: { stat: `regen_${shortId}`, amount: 0.25 },
|
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[] = [
|
export const elementalRegenDisciplines: DisciplineDefinition[] = [
|
||||||
// ── Base Elements ──────────────────────────────────────────────────────────
|
// ── Base Elements ──────────────────────────────────────────────────────────
|
||||||
makeBaseRegen('regen-fire', 'Fire', 'fire', 8),
|
makeBaseConversion('regen-fire', 'Fire', 'fire', 8),
|
||||||
makeBaseRegen('regen-water', 'Water', 'water', 8),
|
makeBaseConversion('regen-water', 'Water', 'water', 8),
|
||||||
makeBaseRegen('regen-air', 'Air', 'air', 8),
|
makeBaseConversion('regen-air', 'Air', 'air', 8),
|
||||||
makeBaseRegen('regen-earth', 'Earth', 'earth', 8),
|
makeBaseConversion('regen-earth', 'Earth', 'earth', 8),
|
||||||
makeBaseRegen('regen-light', 'Light', 'light', 8),
|
makeBaseConversion('regen-light', 'Light', 'light', 8),
|
||||||
makeBaseRegen('regen-dark', 'Dark', 'dark', 8),
|
makeBaseConversion('regen-dark', 'Dark', 'dark', 8),
|
||||||
makeBaseRegen('regen-death', 'Death', 'death', 8),
|
makeBaseConversion('regen-death', 'Death', 'death', 8),
|
||||||
|
|
||||||
// ── Utility Element ────────────────────────────────────────────────────────
|
// ── Utility Element ────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
@@ -62,11 +72,13 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [
|
|||||||
attunement: DisciplinesAttunementType.BASE,
|
attunement: DisciplinesAttunementType.BASE,
|
||||||
manaType: 'transference',
|
manaType: 'transference',
|
||||||
baseCost: 6,
|
baseCost: 6,
|
||||||
description: 'Attune your transference mana to regenerate passively over time.',
|
description: 'Convert raw mana into transference mana over time.',
|
||||||
statBonus: { stat: 'regen_transference', baseValue: 0.4, label: 'Transference Regen/tick' },
|
statBonus: { stat: 'conversion_transference', baseValue: 0.4, label: 'Transference Conversion/tick' },
|
||||||
difficultyFactor: 100,
|
difficultyFactor: 100,
|
||||||
scalingFactor: 50,
|
scalingFactor: 50,
|
||||||
drainBase: 1,
|
drainBase: 1,
|
||||||
|
conversionRate: 0.4,
|
||||||
|
sourceManaTypes: ['transference'],
|
||||||
requires: ['transference'],
|
requires: ['transference'],
|
||||||
perks: [
|
perks: [
|
||||||
{
|
{
|
||||||
@@ -74,16 +86,16 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [
|
|||||||
type: 'once',
|
type: 'once',
|
||||||
threshold: 100,
|
threshold: 100,
|
||||||
value: 0,
|
value: 0,
|
||||||
description: '+0.4 Transference Regen/tick',
|
description: '+0.4 Transference Conversion/tick',
|
||||||
bonus: { stat: 'regen_transference', amount: 0.4 },
|
bonus: { stat: 'conversion_transference', amount: 0.4 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'regen-transference-inf',
|
id: 'regen-transference-inf',
|
||||||
type: 'infinite',
|
type: 'infinite',
|
||||||
threshold: 300,
|
threshold: 300,
|
||||||
value: 100,
|
value: 100,
|
||||||
description: 'Every 100 XP: +0.2 Transference Regen/tick',
|
description: 'Every 100 XP: +0.2 Transference Conversion/tick',
|
||||||
bonus: { stat: 'regen_transference', amount: 0.2 },
|
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 };
|
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})
|
// Merge per-element cap bonuses from discipline effects (elementCap_{element})
|
||||||
for (const [key, value] of Object.entries(disciplineEffects.bonuses)) {
|
for (const [key, value] of Object.entries(disciplineEffects.bonuses)) {
|
||||||
|
|||||||
@@ -23,11 +23,19 @@ const KNOWN_BONUS_STATS = new Set([
|
|||||||
'elementCapBonus',
|
'elementCapBonus',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function computeDisciplineEffects(_state?: DisciplineStoreState): {
|
export interface DisciplineEffectsResult {
|
||||||
bonuses: Record<string, number>;
|
bonuses: Record<string, number>;
|
||||||
multipliers: Record<string, number>;
|
multipliers: Record<string, number>;
|
||||||
specials: Set<string>;
|
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 { disciplines } = useDisciplineStore.getState();
|
||||||
const activeDiscs = Object.entries(disciplines)
|
const activeDiscs = Object.entries(disciplines)
|
||||||
.filter(([, disc]) => disc && disc.xp > 0)
|
.filter(([, disc]) => disc && disc.xp > 0)
|
||||||
@@ -37,6 +45,7 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): {
|
|||||||
const bonuses: Record<string, number> = {};
|
const bonuses: Record<string, number> = {};
|
||||||
const multipliers: Record<string, number> = {};
|
const multipliers: Record<string, number> = {};
|
||||||
const specials = new Set<string>();
|
const specials = new Set<string>();
|
||||||
|
const conversions: Record<string, { rate: number; sourceManaTypes: string[] }> = {};
|
||||||
|
|
||||||
function addBonus(stat: string, amount: number) {
|
function addBonus(stat: string, amount: number) {
|
||||||
bonuses[stat] = (bonuses[stat] || 0) + amount;
|
bonuses[stat] = (bonuses[stat] || 0) + amount;
|
||||||
@@ -49,6 +58,16 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): {
|
|||||||
addBonus(def.statBonus.stat, statBonus);
|
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
|
// Perk unlocks
|
||||||
const perks = getUnlockedPerks(def, disc.xp);
|
const perks = getUnlockedPerks(def, disc.xp);
|
||||||
for (const perk of perks) {
|
for (const perk of perks) {
|
||||||
@@ -56,7 +75,6 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): {
|
|||||||
if (perk.bonus) {
|
if (perk.bonus) {
|
||||||
addBonus(perk.bonus.stat, perk.bonus.amount);
|
addBonus(perk.bonus.stat, perk.bonus.amount);
|
||||||
} else if (!perk.unlocksEffects) {
|
} else if (!perk.unlocksEffects) {
|
||||||
// Fallback: qualitative perk with no structured bonus — add as special flag
|
|
||||||
specials.add(perk.id);
|
specials.add(perk.id);
|
||||||
}
|
}
|
||||||
// Perks with unlocksEffects are handled by discipline-slice.ts processTick()
|
// 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;
|
}).length;
|
||||||
if (nonPaused >= s.concurrentLimit) return s;
|
if (nonPaused >= s.concurrentLimit) return s;
|
||||||
if (!canProceedDiscipline(def, existing, gameState)) return s;
|
if (!canProceedDiscipline(def, existing, gameState)) return s;
|
||||||
|
|
||||||
|
// Check discipline prerequisites (requires field → discipline XP)
|
||||||
const prereqCheck = checkDisciplinePrerequisites(def, s.disciplines, ALL_DISCIPLINES);
|
const prereqCheck = checkDisciplinePrerequisites(def, s.disciplines, ALL_DISCIPLINES);
|
||||||
if (!prereqCheck.canProceed) return s;
|
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 };
|
const discState = existing || { id, xp: 0, paused: false };
|
||||||
// Set currentAction to 'practicing' (only overrides 'meditate')
|
// Set currentAction to 'practicing' (only overrides 'meditate')
|
||||||
useCombatStore.getState().startPracticing();
|
useCombatStore.getState().startPracticing();
|
||||||
|
|||||||
@@ -263,22 +263,45 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
rawMana = disciplineResult.rawMana;
|
rawMana = disciplineResult.rawMana;
|
||||||
elements = disciplineResult.elements;
|
elements = disciplineResult.elements;
|
||||||
|
|
||||||
// Apply per-element regen from discipline effects (regen_{element})
|
// Apply discipline conversions: drain source mana, add to target element
|
||||||
for (const [key, value] of Object.entries(disciplineEffects.bonuses)) {
|
for (const [targetElem, conv] of Object.entries(disciplineEffects.conversions)) {
|
||||||
if (key.startsWith('regen_') && key !== 'regenBonus') {
|
const conversionAmount = conv.rate * HOURS_PER_TICK;
|
||||||
const element = key.replace('regen_', '');
|
// Check that all source mana types are available (unlocked and have enough)
|
||||||
if (elements[element]) {
|
let canConvert = true;
|
||||||
elements[element] = {
|
for (const srcType of conv.sourceManaTypes) {
|
||||||
...elements[element],
|
if (srcType === 'raw') {
|
||||||
current: Math.min(
|
if (rawMana < conversionAmount) {
|
||||||
elements[element].max,
|
canConvert = false;
|
||||||
elements[element].current + value * HOURS_PER_TICK,
|
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
|
// Unlock enchantment effects from newly unlocked discipline perks
|
||||||
if (disciplineResult.unlockedEffects.length > 0) {
|
if (disciplineResult.unlockedEffects.length > 0) {
|
||||||
useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
|
useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ export interface DisciplineDefinition {
|
|||||||
drainBase: number;
|
drainBase: number;
|
||||||
perks: DisciplinePerk[];
|
perks: DisciplinePerk[];
|
||||||
requires?: string[];
|
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 ─────────────────────────────────────────────────────────
|
// ─── Discipline State ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -87,10 +87,31 @@ export function canProceedDiscipline(
|
|||||||
return element && element.current >= drain;
|
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.
|
* 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[] }
|
* 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(
|
export function checkDisciplinePrerequisites(
|
||||||
discipline: DisciplineDefinition,
|
discipline: DisciplineDefinition,
|
||||||
@@ -104,11 +125,21 @@ export function checkDisciplinePrerequisites(
|
|||||||
const missingPrereqs: string[] = [];
|
const missingPrereqs: string[] = [];
|
||||||
|
|
||||||
for (const reqId of discipline.requires) {
|
for (const reqId of discipline.requires) {
|
||||||
const reqState = allDisciplines[reqId];
|
// Check if this is a discipline prerequisite (exists in definitions)
|
||||||
// A prerequisite is met if the discipline has XP > 0 (has been practiced)
|
const reqDef = allDefinitions.find((d) => d.id === reqId);
|
||||||
if (!reqState || reqState.xp <= 0) {
|
if (reqDef) {
|
||||||
const reqDef = allDefinitions.find((d) => d.id === reqId);
|
const reqState = allDisciplines[reqId];
|
||||||
missingPrereqs.push(reqDef?.name ?? 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