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' 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 && ( )}
{member.role[0]} {member.name} {showHeaderHealth && {Math.ceil(member.health)} / {maxHealth}}
{member.shield > 0 && } {showHealthText && {Math.floor(member.health)} / {maxHealth}}
{action && (
)} {children}
) })