import { AnimatePresence, motion } from 'motion/react'; import { useEffect, useMemo, useState } from 'react'; import { resolveAttributeSchema, resolveCharacterAttributeProfile, } from '../data/attributeResolver'; import { type BuildDamageBreakdown, formatBuildContributionPercent, getBuildContributionAttributeRows, getBuildContributionQualityLabel, getCompanionBuildDamageBreakdown, getPlayerBuildDamageBreakdown, } from '../data/buildDamage'; import { getCharacterEquipment, getCharacterPublicBackstorySummary, getLockedCharacterBackstoryChapters, getUnlockedCharacterBackstoryChapters, } from '../data/characterPresets'; import { buildInitialEquipmentLoadout, EQUIPMENT_SLOTS, getEquipmentRarityLabel, getEquipmentSlotLabel, } from '../data/equipmentEffects'; import { buildMedievalNpcVisualFromCustomWorldVisual } from '../data/medievalNpcVisuals'; import { normalizePlayerProgressionState } from '../data/playerProgression'; import type { CharacterChatTarget } from '../hooks/rpg-runtime-story'; import { getResourceLabelsForWorld } from '../services/customWorldPresentation'; import { AnimationState, Character, CompanionArcState, CompanionRenderState, CompanionResolution, CustomWorldProfile, EquipmentLoadout, GameState, TimedBuildBuff, WorldType, } from '../types'; import { getEquipmentSlotIcon, getNineSliceStyle, UI_CHROME, } from '../uiAssets'; import { AffinityStatusCard } from './AffinityStatusCard'; import { BackstoryArchive } from './BackstoryArchive'; import { CharacterAnimator } from './CharacterAnimator'; import { getCharacterDetailSpriteStyle, getContributionVisualStyle, getGenderLabel, } from './CharacterInfoHelpers'; import { CharacterAttributeGrid, CharacterIdentityBadges, CharacterSkillsList, MultiplierContributionList, PlayerLevelProgress, StatusRow, } from './CharacterInfoShared'; import type { GameCanvasEntitySelection } from './GameCanvas'; import { MedievalNpcAnimator } from './MedievalNpcAnimator'; import { PixelCloseButton } from './PixelCloseButton'; import { PixelIcon } from './PixelIcon'; import { ResolvedAssetImage } from './ResolvedAssetImage'; interface CharacterPanelProps { worldType: WorldType | null; customWorldProfile?: CustomWorldProfile | null; playerCharacter: Character; playerProgression?: GameState['playerProgression'] | null; playerHp: number; playerMaxHp: number; playerMana: number; playerMaxMana: number; playerEquipment: EquipmentLoadout; activeBuildBuffs?: TimedBuildBuff[]; companionRenderStates: CompanionRenderState[]; npcStates?: GameState['npcStates']; onOpenCamp?: () => void; onOpenCharacterChat?: (target: CharacterChatTarget) => void; chatSummaries?: Record; onInspectMember?: (selection: GameCanvasEntitySelection) => void; companionArcStates?: CompanionArcState[]; companionResolutions?: CompanionResolution[]; } type PartyMember = { id: string; npcId: string | null; renderState: CompanionRenderState | null; character: Character; roleLabel: string; hp: number; maxHp: number; mana: number; maxMana: number; isLeader: boolean; levelText: string | null; }; type EquipmentRow = { key: string; slotLabel: string; itemLabel: string; rarityLabel: string; }; function buildLeaderEquipmentRows( playerCharacter: Character, playerEquipment: EquipmentLoadout, ): EquipmentRow[] { const starterLoadout = buildInitialEquipmentLoadout(playerCharacter); return EQUIPMENT_SLOTS.map((slot) => { const equippedItem = playerEquipment[slot] ?? starterLoadout[slot]; return { key: `leader-${slot}`, slotLabel: getEquipmentSlotLabel(slot), itemLabel: equippedItem?.name ?? '绌轰綅', rarityLabel: equippedItem ? getEquipmentRarityLabel(equippedItem.rarity) : '绌轰綅', }; }); } function buildCompanionEquipmentRows( character: Character, keyPrefix: string, ): EquipmentRow[] { return getCharacterEquipment(character).map((item) => ({ key: `${keyPrefix}-${item.slot}-${item.item}`, slotLabel: item.slot, itemLabel: item.item, rarityLabel: item.rarity, })); } export function CharacterPanel({ worldType, customWorldProfile = null, playerCharacter, playerProgression = null, playerHp, playerMaxHp, playerMana, playerMaxMana, playerEquipment, activeBuildBuffs = [], companionRenderStates, npcStates = {}, onInspectMember, companionArcStates = [], companionResolutions = [], }: CharacterPanelProps) { const [selectedMemberId, setSelectedMemberId] = useState(null); const [selectedContributionLabel, setSelectedContributionLabel] = useState< string | null >(null); const normalizedPlayerProgression = normalizePlayerProgressionState(playerProgression); const leaderLevelText = `Lv.${normalizedPlayerProgression.level}`; const companionReferenceLevelText = `参考 Lv.${normalizedPlayerProgression.level}`; const partyMembers = useMemo( () => [ { id: `leader-${playerCharacter.id}`, npcId: null, renderState: null, character: playerCharacter, roleLabel: '\u961f\u957f', hp: playerHp, maxHp: playerMaxHp, mana: playerMana, maxMana: playerMaxMana, isLeader: true, levelText: leaderLevelText, }, ...companionRenderStates.map((companion) => ({ id: companion.npcId, npcId: companion.npcId, renderState: companion, character: companion.character, roleLabel: '\u540c\u884c', hp: companion.hp, maxHp: companion.maxHp, mana: companion.mana, maxMana: companion.maxMana, isLeader: false, levelText: companionReferenceLevelText, })), ], [ companionReferenceLevelText, companionRenderStates, leaderLevelText, playerCharacter, playerHp, playerMaxHp, playerMana, playerMaxMana, ], ); const selectedMember = useMemo( () => partyMembers.find((member) => member.id === selectedMemberId) ?? null, [partyMembers, selectedMemberId], ); const buildBreakdownByMemberId = useMemo( () => Object.fromEntries( partyMembers.map((member) => [ member.id, member.isLeader ? getPlayerBuildDamageBreakdown( { worldType, customWorldProfile, playerEquipment, activeBuildBuffs, } as GameState, playerCharacter, ) : getCompanionBuildDamageBreakdown( member.character, worldType, customWorldProfile, ), ]), ) as Record, [ activeBuildBuffs, customWorldProfile, partyMembers, playerCharacter, playerEquipment, worldType, ], ); const selectedBuildBreakdown = selectedMember ? (buildBreakdownByMemberId[selectedMember.id] ?? null) : null; const selectedContributionRow = selectedBuildBreakdown?.rows.find( (row) => row.label === selectedContributionLabel, ) ?? null; const selectedAttributeSchema = resolveAttributeSchema( worldType, customWorldProfile, ); const resourceLabels = getResourceLabelsForWorld( worldType, customWorldProfile, ); const selectedMemberAffinity = selectedMember?.npcId ? (npcStates[selectedMember.npcId]?.affinity ?? 0) : null; const selectedMemberArcState = selectedMember && !selectedMember.isLeader ? (companionArcStates.find( (arcState) => arcState.characterId === selectedMember.character.id, ) ?? null) : null; const selectedMemberResolution = selectedMember && !selectedMember.isLeader ? (companionResolutions.find( (resolution) => resolution.characterId === selectedMember.character.id, ) ?? null) : null; const selectedMemberPublicBackstory = selectedMember && !selectedMember.isLeader && selectedMemberAffinity != null ? getCharacterPublicBackstorySummary(selectedMember.character, worldType) : null; const selectedMemberUnlockedBackstoryChapters = selectedMember && !selectedMember.isLeader && selectedMemberAffinity != null ? getUnlockedCharacterBackstoryChapters( selectedMember.character, selectedMemberAffinity, worldType, ).map((chapter) => ({ id: chapter.id, title: chapter.title, content: chapter.content, })) : []; const selectedMemberLockedBackstoryChapters = selectedMember && !selectedMember.isLeader && selectedMemberAffinity != null ? getLockedCharacterBackstoryChapters( selectedMember.character, selectedMemberAffinity, worldType, ).map((chapter) => ({ id: chapter.id, title: chapter.title, teaser: chapter.teaser, affinityRequired: chapter.affinityRequired, })) : []; const selectedEquipmentRows = selectedMember ? selectedMember.isLeader ? buildLeaderEquipmentRows(playerCharacter, playerEquipment) : buildCompanionEquipmentRows(selectedMember.character, selectedMember.id) : []; const selectedMemberAttributeProfile = useMemo( () => selectedMember ? resolveCharacterAttributeProfile( selectedMember.character, worldType, customWorldProfile, ) : null, [customWorldProfile, selectedMember, worldType], ); const selectedContributionAttributes = selectedContributionRow ? getBuildContributionAttributeRows( selectedContributionRow, selectedAttributeSchema, { resourceLabels }, ) : []; useEffect(() => { if (!selectedContributionLabel) return; if (!selectedContributionRow) { setSelectedContributionLabel(null); } }, [selectedContributionLabel, selectedContributionRow]); useEffect(() => { if (!onInspectMember || !selectedMemberId) return; setSelectedMemberId(null); }, [onInspectMember, selectedMemberId]); const handleMemberInspect = (member: PartyMember) => { if (onInspectMember) { if (member.isLeader) { onInspectMember({ kind: 'player' }); return; } if (member.renderState) { onInspectMember({ kind: 'companion', companion: member.renderState }); return; } } setSelectedMemberId(member.id); }; return ( <>
队伍成员
{partyMembers.map((member) => ( ))}
{selectedContributionRow && selectedMember && ( setSelectedContributionLabel(null)} > event.stopPropagation()} >
{'\u6807\u7b7e\u6548\u679c'}
{selectedContributionRow.label}
{selectedMember.character.name}
setSelectedContributionLabel(null)} label="关闭标签效果" />
标签概览
{selectedContributionRow.label}
{getBuildContributionQualityLabel( selectedContributionRow.bonusDelta, )}
{'\u603b\u52a0\u6210'}{' '} {formatBuildContributionPercent( selectedContributionRow.bonusDelta, )}
{'\u5c5e\u6027\u52a0\u6210'}
{selectedContributionAttributes.length > 0 ? (
{selectedContributionAttributes.map((attribute) => (
{attribute.label} {formatBuildContributionPercent( attribute.modifierDelta, )}
))}
) : (
{ '\u5f53\u524d\u6807\u7b7e\u8fd8\u6ca1\u6709\u53ef\u5c55\u793a\u7684\u5c5e\u6027\u9002\u914d\u660e\u7ec6\u3002' }
)}
)}
{selectedMember && ( setSelectedMemberId(null)} > event.stopPropagation()} >
角色详情
{selectedMember.character.name}
{selectedMember.character.title}
{getGenderLabel(selectedMember.character.gender)}
setSelectedMemberId(null)} label="关闭角色详情" />
{selectedMember.character.visual ? ( ) : ( )}
{selectedMember.character.name}
{selectedMember.character.title}

{selectedMember.character.description}

状态
{selectedMember.isLeader && (
等级
)} {selectedMemberAffinity != null && ( )} {selectedMemberArcState && (
个人线阶段
{selectedMemberArcState.currentStage}
{selectedMemberArcState.arcTheme}
)} {selectedMemberResolution && (
收束状态
{selectedMemberResolution.resolutionType}
{selectedMemberResolution.summary}
)} {selectedMemberAffinity != null && ( )} {selectedBuildBreakdown && ( setSelectedContributionLabel(row.label) } /> )}
{selectedMemberAffinity == null && (
背景故事
{selectedMember.character.backstory}
)}
性格
{selectedMember.character.personality}
{'\u6280\u80fd'}
装备
{selectedEquipmentRows.map((item) => (
{item.slotLabel}
{item.itemLabel}
{item.rarityLabel}
))}
)}
); }