refactor: Replace natural-regen disciplines with mana conversion speed disciplines
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:
2026-05-26 20:40:11 +02:00
parent 1c1bbf8017
commit 46013a15c8
12 changed files with 430 additions and 263 deletions
@@ -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 });
}
}}
>
+31 -10
View File
@@ -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,