feat: discipline UI improvements - stat labels, prerequisites, mana type tab
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s

- Add player-friendly label field to statBonus in DisciplineDefinition
- Show prerequisite requirements on locked discipline cards
- Disable activate button for locked disciplines
- Restructure elemental attunement into dedicated 'Mana Types' tab
- Add checkDisciplinePrerequisites utility function
- Update store to enforce prerequisite checking on activation
- Split discipline-prerequisites tests into separate file
This commit is contained in:
2026-05-25 15:20:02 +02:00
parent 2c58186a67
commit 635b3b3f70
21 changed files with 313 additions and 82 deletions
+30 -9
View File
@@ -4,12 +4,14 @@ import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
import type { ManaType } from '@/lib/game/types/elements';
import { ELEMENTS } from '@/lib/game/constants/elements';
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
import { elementalAttunementDisciplines } from '@/lib/game/data/disciplines/elemental';
import { elementalRegenDisciplines } from '@/lib/game/data/disciplines/elemental-regen';
import { elementalRegenAdvancedDisciplines } from '@/lib/game/data/disciplines/elemental-regen-advanced';
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 { calculateStatBonus, calculateManaDrain, checkDisciplinePrerequisites } from '@/lib/game/utils/discipline-math';
import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines';
import clsx from 'clsx';
// ─── Attunement Tabs ─────────────────────────────────────────────────────────
@@ -22,6 +24,7 @@ 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: 'enchanter', label: 'Enchanter', items: enchanterDisciplines },
@@ -41,6 +44,8 @@ export interface DisciplineCardDefinition {
perkValues?: number[];
perkTypes?: string[];
statBonus: string;
statBonusLabel: string;
requires?: string[];
baseValue: number;
drainBase: number;
difficultyFactor: number;
@@ -51,6 +56,8 @@ export interface DisciplineCardRuntime {
xp: number;
paused: boolean;
concurrentLimit: number;
isLocked: boolean;
missingPrereqs: string[];
}
export interface DisciplineCardCallbacks {
@@ -68,9 +75,9 @@ interface DisciplineCardProps {
const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, callbacks }) => {
const {
id, name, description, manaType, baseCost, perkThresholds, perkValues, perkTypes,
statBonus, baseValue, drainBase, difficultyFactor, scalingFactor,
statBonus, statBonusLabel, baseValue, drainBase, difficultyFactor, scalingFactor,
} = definition;
const { xp, paused: isPaused, concurrentLimit } = runtime;
const { xp, paused: isPaused, concurrentLimit, isLocked, missingPrereqs } = runtime;
const { onToggle } = callbacks;
const displayXp = xp;
@@ -102,7 +109,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
};
return (
<div key={id} className="border rounded-lg p-4 shadow-sm space-y-3">
<div key={id} className={clsx('border rounded-lg p-4 shadow-sm space-y-3', isLocked && 'opacity-60 border-gray-600')}>
<div className="flex items-center justify-between gap-2">
<h3 className="text-lg font-medium">{name}</h3>
<span
@@ -143,7 +150,7 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
</div>
<div className="mt-2 text-sm">
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonus}
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonusLabel}
</div>
<div className="mt-2">
@@ -159,17 +166,26 @@ const DisciplineCard: React.FC<DisciplineCardProps> = ({ definition, runtime, ca
</ul>
</div>
{isLocked && missingPrereqs.length > 0 && (
<div className="mt-2 text-xs text-red-400">
<strong>Requires:</strong> {missingPrereqs.join(', ')}
</div>
)}
<div className="mt-4 flex justify-end">
<button
onClick={toggleAction}
disabled={isLocked}
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',
isLocked
? 'bg-gray-600 text-gray-400 cursor-not-allowed'
: isPaused
? 'bg-yellow-600 text-white hover:bg-yellow-500'
: 'bg-blue-600 text-white hover:bg-blue-500',
)}
>
{isPaused ? 'Start Practicing' : 'Stop Practicing'}
{isLocked ? 'Locked' : isPaused ? 'Start Practicing' : 'Stop Practicing'}
</button>
</div>
</div>
@@ -235,6 +251,7 @@ export const DisciplinesTab: React.FC = () => {
<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 };
const prereqCheck = checkDisciplinePrerequisites(disc, disciplines, ALL_DISCIPLINES);
return (
<DisciplineCard
key={disc.id}
@@ -248,6 +265,8 @@ export const DisciplinesTab: React.FC = () => {
manaType: disc.manaType,
baseCost: disc.baseCost,
statBonus: disc.statBonus.stat,
statBonusLabel: disc.statBonus.label,
requires: disc.requires,
baseValue: disc.statBonus.baseValue,
drainBase: disc.drainBase,
difficultyFactor: disc.difficultyFactor,
@@ -257,6 +276,8 @@ export const DisciplinesTab: React.FC = () => {
xp: discState.xp,
paused: discState.paused,
concurrentLimit,
isLocked: !prereqCheck.canProceed,
missingPrereqs: prereqCheck.missingPrereqs,
}}
callbacks={{
onToggle: handleToggle,