import { memo, type ElementType, type ReactNode } from 'react'
import type { ControllerIconStyle } from '../input'
import type { PartyMember } from '../game'
import {
effectiveMaxHealth,
formatEffectTime,
memberHotEffects,
} from '../combat/rules'
import { ControllerBindingLabel } from './ControllerIcons'
import { barFillStyle } from './barStyles'
export type FloatingCombatText = {
id: number
memberId: string
value: number
}
type MemberEffectMode = 'full' | 'compact' | 'timed-basic'
const MemberEffects = memo(function MemberEffects({
member,
mode,
}: {
member: PartyMember
mode: MemberEffectMode
}) {
if (mode === 'compact') {
return (
{memberHotEffects(member).map((effect) => (
{effect.label}
))}
{member.shield > 0 && Shield {Math.ceil(member.shield)}}
{member.debuff && {member.debuff}}
)
}
if (mode === 'timed-basic') {
return (
{member.hotTicks > 0 && Renew {formatEffectTime(member.hotTicks)}}
{member.shield > 0 && Shield {Math.ceil(member.shield)}}
{member.debuff && member.debuffTicks && {member.debuff} {formatEffectTime(member.debuffTicks)}}
{(member.poisonStacks ?? 0) > 0 && Poison {member.poisonStacks}}
{(member.maxHealthPenaltyTicks ?? 0) > 0 && Max HP -25% {formatEffectTime(member.maxHealthPenaltyTicks ?? 0)}}
{(member.healingReductionTicks ?? 0) > 0 && Healing -25% {formatEffectTime(member.healingReductionTicks ?? 0)}}
)
}
return (
{memberHotEffects(member).map((effect) => (
{effect.label} {formatEffectTime(effect.ticks)}
))}
{member.shield > 0 && Shield {Math.ceil(member.shield)}}
{(member.damageReductionTicks ?? 0) > 0 && Barkskin {formatEffectTime(member.damageReductionTicks ?? 0)}}
{(member.bounceHeals ?? []).map((effect) => (
{effect.label} {effect.charges}
))}
{member.debuff && member.debuffTicks && {member.debuff} {formatEffectTime(member.debuffTicks)}}
{(member.poisonStacks ?? 0) > 0 && Poison {member.poisonStacks}}
{(member.maxHealthPenaltyTicks ?? 0) > 0 && Max HP -25% {formatEffectTime(member.maxHealthPenaltyTicks ?? 0)}}
{(member.healingReductionTicks ?? 0) > 0 && Healing -25% {formatEffectTime(member.healingReductionTicks ?? 0)}}
)
})
export const PartyMemberFrame = memo(function PartyMemberFrame({
as,
member,
selected = false,
baseClassName = 'party-member',
selectedClassName = 'selected',
deadClassName = 'dead',
effectMode = 'full',
floatingTexts = [],
showTargetMarker = false,
showHeaderHealth = true,
showHealthText = false,
targetBinding,
controllerIconStyle,
onSelect,
children,
}: {
as?: ElementType
member: PartyMember
selected?: boolean
baseClassName?: string
selectedClassName?: string
deadClassName?: string
effectMode?: MemberEffectMode
floatingTexts?: FloatingCombatText[]
showTargetMarker?: boolean
showHeaderHealth?: boolean
showHealthText?: boolean
targetBinding?: string | null
controllerIconStyle?: ControllerIconStyle
onSelect?: (id: string) => void
children?: ReactNode
}) {
const Component = as ?? (onSelect ? 'button' : 'div')
const maxHealth = effectiveMaxHealth(member)
const className = [
baseClassName,
selected ? selectedClassName : '',
member.health <= 0 ? deadClassName : '',
].filter(Boolean).join(' ')
const action = targetBinding ? {
binding: targetBinding,
iconStyle: controllerIconStyle ?? 'xbox',
} : null
return (
onSelect(member.id) : undefined}
aria-pressed={onSelect ? selected : undefined}
type={Component === 'button' ? 'button' : undefined}
>
{showTargetMarker && (
Target
)}
{member.role[0]}
{member.name}
{showHeaderHealth && {Math.ceil(member.health)} / {maxHealth}}
{member.shield > 0 && }
{showHealthText && {Math.floor(member.health)} / {maxHealth}}
{floatingTexts.map((entry) => +{entry.value})}
{action && (
)}
{children}
)
})