@@ -1,181 +1,149 @@
import React , { useEffect , useState } from 'react' ;
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice' ;
import type { DisciplineDefinition } from '@/types/disciplines' ;
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 { useRef } from 'react' ;
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 ;
}
const DisciplineCard : React.FC < DisciplineCardProps > = ( {
id ,
name ,
description ,
perkThresholds ,
perkValues ,
perkTypes ,
statBonus ,
baseValue ,
drainBase ,
difficultyFactor ,
scalingFactor ,
} ) = > {
const activeIds = useDisciplineStore ( ( s ) = > s . activeIds ) ;
const concurrentLimit = useDisciplineStore ( ( s ) = > s . concurrentLimit ) ;
const currentDisc = useDisciplineStore ( ( s ) = > s . disciplines [ id ] ? ? { xp : 0 , paused : true } ) ;
const displayXp = currentDisc . xp ;
const progressPercent = Math . min ( displayXp / Math . max ( 1 , concurrentLimit * 100 ) , 100 ) ;
const isPaused = currentDisc . 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 = ( ) = > {
if ( isPaused ) {
useDisciplineStore . getState ( ) . activate ( id ) ;
} else {
useDisciplineStore . getState ( ) . deactivate ( id ) ;
}
} ;
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 store = useDisciplineStore ( ) ;
const { disciplines , activeIds , concurrentLimit } = store ;
const { activeIds , concurrentLimit } = useDisciplineStore ( ) ;
const [ mounted , setMounted ] = useState ( false ) ;
const [ activeAttunement , setActiveAttunement ] = useState < string > ( 'base' ) ;
useEffect ( ( ) = > {
setMounted ( true ) ;
} , [ ] ) ;
const allDisciplines : DisciplineDefinition [ ] = [
. . . baseDisciplines ,
. . . enchanterDisciplines ,
. . . fabricatorDisciplines ,
. . . invokerDisciplines ,
] ;
// Group disciplines by attunement for tab rendering
const attunementTabs : {
label : string ;
items : DisciplineDefinition [ ] ;
} [ ] = [
{ label : 'Base' , items : baseDisciplines } ,
{ label : 'Enchanter' , items : enchanterDisciplines } ,
{ label : 'Fabricator' , items : fabricatorDisciplines } ,
{ label : 'Invoker' , items : invokerDisciplines } ,
] ;
// Helper to render a single discipline card
const DisciplineCard : React.FC < {
id : string ;
name : string ;
description : string ;
xp : number ;
perkThresholds? : number [ ] ;
perkValues? : number [ ] ;
perkTypes? : string [ ] ;
statBonus : string ;
baseValue : number ;
drainBase : number ;
difficultyFactor : number ;
scalingFactor : number ;
} > = ( {
id ,
name ,
description ,
xp ,
perkThresholds ,
perkValues ,
perkTypes ,
statBonus ,
baseValue ,
drainBase ,
difficultyFactor ,
scalingFactor ,
} ) = > {
if ( ! mounted ) return null ;
const state = useDisciplineStore ( ) . getState ( ) ;
const currentDisc = state . disciplines [ id ] ? ? { xp : 0 , paused : true } ;
const isActive = activeIds . includes ( id ) ;
const canActivate = concurrentLimit > activeIds . filter ( a = > state . disciplines [ a ] ? . paused !== true ) . length ;
// Calculate displayed stats
const displayXp = currentDisc . xp ;
const progressPercent = Math . min ( displayXp / Math . max ( 1 , ( concurrentLimit * 100 ) ? ? 1 ) , 100 ) ;
const isPaused = currentDisc . paused ;
const hasPendingPerk = perkThresholds ? . some ( ( t , i ) = > displayXp >= t && perkTypes ? . [ i ] !== 'capped' ) ;
const activeStatBonus = calculateStatBonus (
parseInt ( baseValue ) || 0 ,
displayXp ,
scalingFactor
) ;
// Simple visual for drain per tick
const estimatedDrain = calculateManaDrain (
drainBase ,
displayXp ,
difficultyFactor
) ;
// Determine unlocked perks
const unlockedPerks = perkTypes ? . reduce < string [ ] > ( ( acc , typ , idx ) = > {
if ( typ === 'once' || typ === 'infinite' ) {
if ( displayXp >= perkThresholds ? . [ idx ] ? ? 0 ) {
acc . push ( ` ${ typ } - ${ idx } ` ) ;
}
} else if ( typ === 'capped' ) {
const tier = Math . max ( 0 , Math . floor ( ( displayXp - perkThresholds ? . [ idx ] ? ? 0 ) / perkValues ? . [ idx ] ? ? 1 ) + 1 ) ;
if ( tier > 0 ) acc . push ( ` ${ typ } - ${ idx } ` ) ;
}
return acc ;
} , [ ] ) ;
// Helper to decide button action
const toggleAction = ( ) = > {
if ( isPaused ) {
// Resume – activate
const storeDispatch = useDisciplineStore ( ) . getState ( ) . activate as any ;
storeDispatch ( id ) ;
} else {
// Pause – deactivate
const storeDispatch = useDisciplineStore ( ) . getState ( ) . deactivate as any ;
storeDispatch ( id ) ;
}
} ;
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 = { ` bg-blue-500 transition-all duration-300 ${
activeStatBonus > 0 ? 'bg-green-500' : 'bg-red-500'
} ` }
style = { { width : ` ${ progressPercent } % ` } }
/ >
< / div >
< / div >
< div className = "text-sm text-gray-400" >
< strong > Drain : < / strong > { estimatedDrain . toFixed ( 1 ) } ✦ { ' ' }
< strong > XP : < / strong > { displayXp }
< / div >
{ /* Bonus display */ }
< div className = "mt-2 text-sm" >
< strong > Stat Bonus : < / strong > { activeStatBonus . toFixed ( 2 ) } on { statBonus }
< / div >
{ /* Perks */ }
< 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 >
{ /* Action button */ }
< 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 >
) ;
} ;
if ( ! mounted ) {
return (
< div className = "flex items-center justify-center p-8 text-gray-500" >
@@ -184,25 +152,21 @@ export const DisciplinesTab: React.FC = () => {
) ;
}
const activeTab = ATTUNEMENT_TABS . find ( ( t ) = > t . key === activeAttunement ) ;
return (
< div className = "mt-6" >
{ /* Tab bar */ }
< div className = "flex gap-2 mb-4" >
{ attunementTabs . map ( ( tab ) = > {
const isActiveTab = store
. getState ( )
. activeAttunement === tab . label . toLowerCase ( ) ;
{ ATTUNEMENT_TABS . map ( ( tab ) = > {
const isActiveTab = activeAttunement === tab . key ;
return (
< button
key = { tab . label }
onClick = { ( ) = > {
// Here you could dispatch an action to switch tabs if needed
// For simplicity, we just render the tabs
console . log ( ` Switch to ${ tab . label } ` ) ;
} }
key = { tab . key }
onClick = { ( ) = > setActiveAttunement ( tab . key ) }
className = { clsx ( 'rounded px-3 py-1' , {
'bg-blue-600 text-white' : tab . label === 'Base' , // highlight first for demo
'text-gray-600' : tab . label !== 'Base' ,
'bg-blue-600 text-white' : isActiveTab ,
'text-gray-600' : ! isActiveTab ,
} ) }
>
{ tab . label }
@@ -211,36 +175,24 @@ export const DisciplinesTab: React.FC = () => {
} ) }
< / div >
{ /* Discipline cards */ }
{ /* Discipline cards — only render active tab */ }
< div className = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4" >
{ attunementTabs
. map ( ( tab ) = >
tab . items . map ( ( disc ) = > {
const state = useDisciplineStore ( ) . getState ( ) ;
const discState = state . disciplines [ disc . id ] ? ? { xp : 0 , paused : true } ;
const isActive = activeIds . includes ( disc . id ) ;
const isVisible = attunementTabs . find ( ( t ) = > t . label === tab . label ) ? . items === tab . items ;
return isVisible && (
< DisciplineCard
key = { disc . id }
id = { disc . id }
name = { disc . name }
description = { disc . description }
xp = { discState . xp }
perkThresholds = { disc . perks ? . map ( ( p ) = > p . threshold ) }
perkValues = { disc . perks ? . map ( ( p ) = > p . value ) }
perkTypes = { disc . perks ? . map ( ( p ) = > p . type ) }
statBonus = { disc . statBonus }
baseValue = { disc . statBonus . baseValue ? . toString ( ) ? ? '0' }
drainBase = { disc . drainBase }
difficultyFactor = { disc . difficultyFactor }
scalingFactor = { disc . scalingFactor }
/ >
) ;
} )
)
. flat ( )
}
{ activeTab ? . items . map ( ( disc ) = > (
< 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 }
/ >
) ) }
< / div >
{ /* Summary info */ }