Files
Mana-Loop/src/components/game/tabs/DisciplinesTab.tsx
T
n8n-gitea c3a5f333da
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
fix: resolve 22 remaining issues - type exports, dead code, state mutations, orphaned components
2026-05-18 21:03:43 +02:00

225 lines
7.2 KiB
TypeScript

import React, { useEffect, useState, useCallback } from 'react';
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
import { enchanterDisciplines } from '@/lib/game/data/disciplines/enchanter';
import { fabricatorDisciplines } from '@/lib/game/data/disciplines/fabricator';
import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker';
import { calculateStatBonus, calculateManaDrain } from '@/lib/game/utils/discipline-math';
import clsx from 'clsx';
interface AttunementTab {
key: string;
label: string;
items: DisciplineDefinition[];
}
const ATTUNEMENT_TABS: AttunementTab[] = [
{ key: 'base', label: 'Base', items: baseDisciplines },
{ key: 'enchanter', label: 'Enchanter', items: enchanterDisciplines },
{ key: 'fabricator', label: 'Fabricator', items: fabricatorDisciplines },
{ key: 'invoker', label: 'Invoker', items: invokerDisciplines },
];
interface DisciplineCardProps {
id: string;
name: string;
description: string;
perkThresholds?: number[];
perkValues?: number[];
perkTypes?: string[];
statBonus: string;
baseValue: number;
drainBase: number;
difficultyFactor: number;
scalingFactor: number;
xp: number;
paused: boolean;
concurrentLimit: number;
onToggle: (id: string, paused: boolean) => void;
}
const DisciplineCard: React.FC<DisciplineCardProps> = ({
id,
name,
description,
perkThresholds,
perkValues,
perkTypes,
statBonus,
baseValue,
drainBase,
difficultyFactor,
scalingFactor,
xp,
paused,
concurrentLimit,
onToggle,
}) => {
const displayXp = xp;
const progressPercent = Math.min(displayXp / Math.max(1, concurrentLimit * 100), 100);
const isPaused = paused;
const activeStatBonus = calculateStatBonus(baseValue, displayXp, scalingFactor);
const estimatedDrain = calculateManaDrain(drainBase, displayXp, difficultyFactor);
const unlockedPerks = perkTypes?.reduce<string[]>((acc, typ, idx) => {
const threshold = perkThresholds?.[idx];
if (threshold === undefined) return acc;
if (typ === 'once' || typ === 'infinite') {
if (displayXp >= threshold) acc.push(`${typ}-${idx}`);
} else if (typ === 'capped') {
const interval = perkValues?.[idx] ?? 1;
const tier = Math.max(0, Math.floor((displayXp - threshold) / interval) + 1);
if (tier > 0) acc.push(`${typ}-${idx}`);
}
return acc;
}, []);
const toggleAction = () => {
onToggle(id, isPaused);
};
return (
<div key={id} className="border rounded-lg p-4 shadow-sm space-y-3">
<h3 className="text-lg font-medium">{name}</h3>
<p className="text-sm text-gray-400">{description}</p>
<div className="flex items-center gap-2">
<span className="text-xs font-mono whitespace-nowrap">{Math.round(progressPercent)}%</span>
<div className="flex-1 bg-gray-200 rounded-full overflow-hidden h-3">
<div
className={`transition-all duration-300 ${activeStatBonus > 0 ? 'bg-green-500' : 'bg-red-500'}`}
style={{ width: `${Math.round(progressPercent)}%` }}
/>
</div>
</div>
<div className="text-sm text-gray-400">
<strong>Drain:</strong> {estimatedDrain.toFixed(1)} {' '}
<strong>XP:</strong> {displayXp}
</div>
<div className="mt-2 text-sm">
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonus}
</div>
<div className="mt-2">
<strong>Perks:</strong>
<ul className="mt-1 list-disc list-inside space-y-1 text-xs">
{unlockedPerks && unlockedPerks.length > 0 ? (
unlockedPerks.map((p) => (
<li key={p} className="text-green-500">{p.replace(/-([0-9]+)$/, ' $1')}</li>
))
) : (
<li className="text-gray-400">locked</li>
)}
</ul>
</div>
<div className="mt-4 flex justify-end">
<button
onClick={toggleAction}
className={clsx(
'rounded px-3 py-1 text-sm font-medium',
isPaused
? 'bg-yellow-600 text-white hover:bg-yellow-500'
: 'bg-blue-600 text-white hover:bg-blue-500',
)}
>
{isPaused ? 'Activate' : 'Pause'}
</button>
</div>
</div>
);
};
export const DisciplinesTab: React.FC = () => {
const activeIds = useDisciplineStore((s) => s.activeIds);
const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit);
const disciplines = useDisciplineStore((s) => s.disciplines);
const activate = useDisciplineStore((s) => s.activate);
const deactivate = useDisciplineStore((s) => s.deactivate);
const [mounted, setMounted] = useState(false);
const [activeAttunement, setActiveAttunement] = useState<string>('base');
useEffect(() => {
setMounted(true);
}, []);
const handleToggle = useCallback((id: string, paused: boolean) => {
if (paused) {
activate(id);
} else {
deactivate(id);
}
}, [activate, deactivate]);
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading disciplines
</div>
);
}
const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement);
return (
<div className="mt-6">
{/* Tab bar */}
<div className="flex gap-2 mb-4">
{ATTUNEMENT_TABS.map((tab) => {
const isActiveTab = activeAttunement === tab.key;
return (
<button
key={tab.key}
onClick={() => setActiveAttunement(tab.key)}
className={clsx('rounded px-3 py-1', {
'bg-blue-600 text-white': isActiveTab,
'text-gray-600': !isActiveTab,
})}
>
{tab.label}
</button>
);
})}
</div>
{/* Discipline cards — only render active tab */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{activeTab?.items.map((disc) => {
const discState = disciplines[disc.id] ?? { xp: 0, paused: true };
return (
<DisciplineCard
key={disc.id}
id={disc.id}
name={disc.name}
description={disc.description}
perkThresholds={disc.perks?.map((p) => p.threshold)}
perkValues={disc.perks?.map((p) => p.value)}
perkTypes={disc.perks?.map((p) => p.type)}
statBonus={disc.statBonus.stat}
baseValue={disc.statBonus.baseValue}
drainBase={disc.drainBase}
difficultyFactor={disc.difficultyFactor}
scalingFactor={disc.scalingFactor}
xp={discState.xp}
paused={discState.paused}
concurrentLimit={concurrentLimit}
onToggle={handleToggle}
/>
);
})}
</div>
{/* Summary info */}
<div className="mt-4 flex flex-col sm:flex-row gap-2 text-sm text-gray-500">
<div>Active Disciple{activeIds.length}{activeIds.length === 1 ? '' : 's'} / {concurrentLimit}</div>
<div>Concurrent Limit: {concurrentLimit}</div>
</div>
</div>
);
};