import { X } from 'lucide-react'; import { AnimatePresence, motion } from 'motion/react'; import type { ReactNode } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS, DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY, } from '../data/affinityLevels'; import { buildRelationState, resolveAttributeSchema, resolveCharacterAttributeProfile, } from '../data/attributeResolver'; import { formatBuildContributionPercent, getBuildContributionAttributeRows, getBuildContributionQualityLabel, getCompanionBuildDamageBreakdown, getPlayerBuildDamageBreakdown, resolveMonsterOutgoingDamage, } from '../data/buildDamage'; import { getCharacterById, getCharacterMaxHp, getCharacterMaxMana, getCharacterPrivateChatUnlockAffinity, getCharacterPublicBackstorySummary, getInventoryItems, getLockedCharacterBackstoryChapters, getUnlockedCharacterBackstoryChapters, } from '../data/characterPresets'; import { getHostileNpcPresetById, getMonsterPresetsByWorld, } from '../data/hostileNpcPresets'; import { buildMedievalNpcVisualFromCustomWorldVisual } from '../data/medievalNpcVisuals'; import { buildEncounterAttributeRumors, resolveEncounterAttributeProfile, } from '../data/npcAttributeInsights'; import { buildInitialNpcState, createNpcBattleMonster, normalizeNpcPersistentState, } from '../data/npcInteractions'; import { getSceneHostileNpcPresetIds } from '../data/scenePresets'; import type { CharacterChatTarget } from '../hooks/useStoryGeneration'; import { getResourceLabelsForWorld } from '../services/customWorldPresentation'; import { AnimationState, type Character, type Encounter, type GameState, type InventoryItem, type NpcPersistentState, } from '../types'; import { getNineSliceStyle, UI_CHROME } from '../uiAssets'; import { AffinityStatusCard } from './AffinityStatusCard'; import { BackstoryArchive, type BackstoryLockedChapter, type BackstoryUnlockedChapter, } from './BackstoryArchive'; import { CharacterAnimator } from './CharacterAnimator'; import { getCharacterDetailSpriteStyle, getContributionVisualStyle, getSkillDeliveryLabel, getSkillStyleLabel, } from './CharacterInfoHelpers'; import { CharacterAttributeGrid, CharacterSkillsList, MultiplierContributionList, StatusRow, } from './CharacterInfoShared'; import { GENERIC_NPC_SCENE_SCALE } from './game-canvas/GameCanvasShared'; import type { GameCanvasEntitySelection } from './GameCanvas'; import { HostileNpcAnimator } from './HostileNpcAnimator'; import { InventoryItemDetailModal, InventoryItemGrid, } from './InventoryItemViews'; import { MedievalNpcAnimator } from './MedievalNpcAnimator'; import { SkillEffectPreview } from './SkillEffectPreview'; interface AdventureEntityModalProps { selection: GameCanvasEntitySelection | null; gameState: GameState; onClose: () => void; onOpenCharacterChat?: (target: CharacterChatTarget) => void; } const [ BACKSTORY_UNLOCK_AFFINITY_EASED = 15, BACKSTORY_UNLOCK_AFFINITY_FRIENDLY = 30, BACKSTORY_UNLOCK_AFFINITY_TRUSTED = 60, BACKSTORY_UNLOCK_AFFINITY_CLOSE = 90, ] = AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS; function estimateCharacterMaxHp( character: Character, worldType: GameState['worldType'], customWorldProfile: GameState['customWorldProfile'], ) { return getCharacterMaxHp(character, worldType, customWorldProfile); } function estimateNpcMaxHp( character: Character | null, worldType: GameState['worldType'], customWorldProfile: GameState['customWorldProfile'], ) { return character ? estimateCharacterMaxHp(character, worldType, customWorldProfile) : 120; } function estimateNpcMaxMana(character: Character | null) { return character ? getCharacterMaxMana(character) : 0; } function Section({ title, children }: { title: string; children: ReactNode }) { return (
{title}
{children}
); } function resolveSkillPreviewMonsterId(gameState: GameState) { if (!gameState.worldType) { return null; } const sceneMonsterId = getSceneHostileNpcPresetIds(gameState.currentScenePreset)[0] ?? null; if (sceneMonsterId) { return sceneMonsterId; } return getMonsterPresetsByWorld(gameState.worldType)[0]?.id ?? null; } function buildPreviewInventoryDescription( characterName: string, item: { category: string; name: string; quantity: number }, ) { const quantityText = item.quantity > 1 ? `,当前数量 x${item.quantity}` : ''; switch (item.category) { case '消耗品': return `${characterName} 随身准备的消耗品,适合在关键时刻快速补给${quantityText}。`; case '稀有品': return `${characterName} 妥善保管的稀有物件,通常和经历、身份或交易筹码有关${quantityText}。`; case '专属品': return `${characterName} 不轻易示人的专属信物,往往带着明显的个人痕迹${quantityText}。`; case '材料': return `${characterName} 随身收着的制作材料,可用于后续锻造或交换${quantityText}。`; default: return `${characterName} 携带的${item.category}${quantityText}。`; } } function getPreviewInventoryRarity(category: string): InventoryItem['rarity'] { switch (category) { case '专属品': return 'epic'; case '稀有品': return 'rare'; case '材料': return 'uncommon'; default: return 'common'; } } function buildCharacterInventoryPreviewItems( character: Character, worldType: GameState['worldType'], ) { return getInventoryItems(character, worldType).map( (item, index) => ({ id: `preview:${character.id}:${index}:${item.category}:${item.name}`, category: item.category, name: item.name, quantity: item.quantity, rarity: getPreviewInventoryRarity(item.category), tags: [], description: buildPreviewInventoryDescription(character.name, item), }) satisfies InventoryItem, ); } function getNpcBadge( encounter: Encounter, affinity: number, battleStatePresent: boolean, ) { if (encounter.hostile || battleStatePresent || affinity < 0) { return '敌对角色'; } return '相遇角色'; } function describeRelationStance(affinity: number) { switch (buildRelationState(affinity).stance) { case 'hostile': return '敌对'; case 'guarded': return '戒备'; case 'neutral': return '试探'; case 'cooperative': return '合作'; case 'bonded': return '深信'; default: return '未知'; } } function truncateBackstoryTeaser(text: string, maxLength = 42) { const normalized = text.replace(/\s+/g, ' ').trim(); if (normalized.length <= maxLength) { return normalized; } return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}…`; } function buildGenericNpcBackstoryArchive( encounter: Encounter, npcState: NpcPersistentState, rumors: string[], battleStatePresent: boolean, ): { publicSummary: string; unlockedChapters: BackstoryUnlockedChapter[]; lockedChapters: BackstoryLockedChapter[]; } { const publicSummary = npcState.affinity < 0 || encounter.hostile || battleStatePresent ? `${encounter.npcName}表面上仍以“${encounter.context}”的身份行动,但此刻已把你视作敌人。${encounter.npcDescription}` : `${encounter.npcName}以“${encounter.context}”的身份出现在你面前。${encounter.npcDescription}`; const relationText = describeRelationStance(npcState.affinity); const rumorText = rumors.length > 0 ? rumors.join(';') : `${encounter.npcName}当前显露出来的大多仍是“${encounter.context}”这一层身份。`; const contactText = npcState.firstMeaningfulContactResolved ? '你们已经越过最初的表面试探,对方开始显露更稳定的行事轮廓。' : '目前仍停留在初见观察阶段,对方真正的来历和立场还没有完全摊开。'; const chapterBlueprints: Array< BackstoryUnlockedChapter & Pick > = [ { id: 'surface-role', title: '表层身份', affinityRequired: BACKSTORY_UNLOCK_AFFINITY_EASED, content: `${contactText}你最先能确认的是,${encounter.npcName}眼下确实以“${encounter.context}”的身份处理局面。${encounter.npcDescription}`, }, { id: 'behavior-clues', title: '行事线索', affinityRequired: BACKSTORY_UNLOCK_AFFINITY_FRIENDLY, content: `随着接触增加,你已经能从细节里摸到一些稳定线索:${rumorText}`, }, { id: 'true-stance', title: '真实立场', affinityRequired: BACKSTORY_UNLOCK_AFFINITY_TRUSTED, content: npcState.affinity < 0 || encounter.hostile || battleStatePresent ? `${encounter.npcName}对你的态度已经明确落到“${relationText}”一侧,很多原本还能试探的空间都转成了直接敌意。` : `相处到这一步,你已能判断 ${encounter.npcName} 对你的态度偏“${relationText}”,不再只是维持表面身份,而是开始露出更真实的站位。`, }, { id: 'deep-concern', title: '深层牵挂', affinityRequired: BACKSTORY_UNLOCK_AFFINITY_CLOSE, content: npcState.affinity < 0 || encounter.hostile || battleStatePresent ? `即使局势已经转向对立,你仍能感觉到,${encounter.npcName}真正死守的东西,多半和“${encounter.context}”这一身份背后的职责、利益或旧牵连有关。` : `继续相处后,你大致能确认,${encounter.npcName}最在意的仍是“${encounter.context}”这一身份背后的职责、利益或牵挂。虽然未必会全盘交底,但已经肯让你看到更核心的顾虑。`, }, ]; return { publicSummary, unlockedChapters: chapterBlueprints .filter((chapter) => npcState.affinity >= chapter.affinityRequired) .map(({ affinityRequired: _affinityRequired, ...chapter }) => chapter), lockedChapters: chapterBlueprints .filter((chapter) => npcState.affinity < chapter.affinityRequired) .map((chapter) => ({ id: chapter.id, title: chapter.title, affinityRequired: chapter.affinityRequired, teaser: truncateBackstoryTeaser(chapter.content), })), }; } function buildGenericNpcSkillCatalog(params: { encounter: Encounter; npcState: NpcPersistentState; worldType: GameState['worldType']; customWorldProfile: GameState['customWorldProfile']; }): Character['skills'] { const { encounter, npcState, worldType, customWorldProfile } = params; const battleMonster = createNpcBattleMonster(encounter, npcState, 'fight', { worldType, customWorldProfile, }); const damage = resolveMonsterOutgoingDamage( battleMonster, 9, 1, worldType, customWorldProfile, ); const isRanged = Boolean( battleMonster.attackRange >= 3 || battleMonster.combatTags?.some((tag) => ['远射', '法修', '符阵', '雷法'].includes(tag), ), ); const basicAttack: Character['skills'][number] = { id: `npc-basic-attack:${encounter.id ?? encounter.npcName}`, name: '普通攻击', animation: AnimationState.ATTACK, casterAnimation: AnimationState.ATTACK, damage, manaCost: 0, cooldownTurns: 0, range: Number(battleMonster.attackRange.toFixed(1)), style: isRanged ? 'projectile' : 'steady', delivery: isRanged ? 'ranged' : 'melee', releaseDelayMs: isRanged ? 220 : 120, }; return [basicAttack]; } function buildFallbackCompanionNpcState(affinity: number): NpcPersistentState { return normalizeNpcPersistentState({ affinity, relationState: buildRelationState(affinity), helpUsed: false, chattedCount: 0, giftsGiven: 0, inventory: [], recruited: true, revealedFacts: [], knownAttributeRumors: [], firstMeaningfulContactResolved: true, seenBackstoryChapterIds: [], }); } export function AdventureEntityModal({ selection, gameState, onClose, onOpenCharacterChat, }: AdventureEntityModalProps) { const [selectedSkillId, setSelectedSkillId] = useState(null); const [selectedContributionLabel, setSelectedContributionLabel] = useState< string | null >(null); const [selectedItemId, setSelectedItemId] = useState(null); const playerCharacter = selection?.kind === 'player' ? gameState.playerCharacter : null; const companion = selection?.kind === 'companion' ? selection.companion : null; const companionCharacter = companion?.character ?? null; const companionRosterState = companion ? (gameState.companions.find((item) => item.npcId === companion.npcId) ?? gameState.roster.find((item) => item.npcId === companion.npcId) ?? null) : null; const companionNpcState = companion ? normalizeNpcPersistentState( gameState.npcStates[companion.npcId] ?? buildFallbackCompanionNpcState( companionRosterState?.joinedAtAffinity ?? 0, ), ) : null; const npcEncounter = selection?.kind === 'npc' ? selection.encounter : null; const npcCharacter = npcEncounter?.characterId ? getCharacterById(npcEncounter.characterId) : null; const npcId = npcEncounter?.id ?? npcEncounter?.npcName ?? null; const npcState = npcEncounter ? normalizeNpcPersistentState( gameState.npcStates[npcId ?? ''] ?? buildInitialNpcState(npcEncounter, gameState.worldType, gameState), ) : null; const monsterPresetId = npcEncounter?.monsterPresetId ?? null; const hostileNpcPreset = monsterPresetId && gameState.worldType ? getHostileNpcPresetById(gameState.worldType, monsterPresetId) : null; const npcBattleState = selection?.kind === 'npc' ? (selection.battleState ?? null) : null; const archiveCharacter = selection?.kind === 'companion' ? companionCharacter : selection?.kind === 'npc' ? npcCharacter : null; const archiveNpcState = selection?.kind === 'companion' ? companionNpcState : selection?.kind === 'npc' ? npcState : null; const detailCharacter = selection?.kind === 'player' ? playerCharacter : selection?.kind === 'companion' ? companionCharacter : npcCharacter; const archiveAffinity = archiveNpcState?.affinity ?? 0; const archivePublicSummary = archiveCharacter ? getCharacterPublicBackstorySummary(archiveCharacter, gameState.worldType) : null; const unlockedBackstoryChapters: BackstoryUnlockedChapter[] = archiveCharacter ? getUnlockedCharacterBackstoryChapters( archiveCharacter, archiveAffinity, gameState.worldType, ).map((chapter) => ({ id: chapter.id, title: chapter.title, content: chapter.content, })) : []; const lockedBackstoryChapters: BackstoryLockedChapter[] = archiveCharacter ? getLockedCharacterBackstoryChapters( archiveCharacter, archiveAffinity, gameState.worldType, ).map((chapter) => ({ id: chapter.id, title: chapter.title, teaser: chapter.teaser, affinityRequired: chapter.affinityRequired, })) : []; const privateChatUnlockAffinity = companionCharacter ? getCharacterPrivateChatUnlockAffinity( companionCharacter, gameState.worldType, ) : null; const privateChatUnlocked = Boolean( selection?.kind === 'companion' && companionCharacter && companionNpcState?.recruited && privateChatUnlockAffinity != null && companionNpcState.affinity >= privateChatUnlockAffinity, ); const title = selection?.kind === 'player' ? (playerCharacter?.name ?? '主角') : selection?.kind === 'companion' ? (companionCharacter?.name ?? '同行角色') : (npcEncounter?.npcName ?? '相遇角色'); const subtitle = selection?.kind === 'player' ? (playerCharacter?.title ?? '主角') : selection?.kind === 'companion' ? (companionCharacter?.title ?? '同行角色') : (npcEncounter?.context ?? '相遇角色'); const description = selection?.kind === 'player' ? (playerCharacter?.description ?? '') : selection?.kind === 'companion' ? (companionCharacter?.description ?? '') : (npcEncounter?.npcDescription ?? ''); const hp = selection?.kind === 'player' ? gameState.playerHp : selection?.kind === 'companion' ? (companion?.hp ?? (companionCharacter ? estimateCharacterMaxHp( companionCharacter, gameState.worldType, gameState.customWorldProfile, ) : 0)) : (npcBattleState?.hp ?? estimateNpcMaxHp( npcCharacter, gameState.worldType, gameState.customWorldProfile, )); const maxHp = selection?.kind === 'player' ? gameState.playerMaxHp : selection?.kind === 'companion' ? (companion?.maxHp ?? (companionCharacter ? estimateCharacterMaxHp( companionCharacter, gameState.worldType, gameState.customWorldProfile, ) : 0)) : (npcBattleState?.maxHp ?? estimateNpcMaxHp( npcCharacter, gameState.worldType, gameState.customWorldProfile, )); const mana = selection?.kind === 'player' ? gameState.playerMana : selection?.kind === 'companion' ? (companion?.mana ?? (companionCharacter ? getCharacterMaxMana(companionCharacter) : 0)) : estimateNpcMaxMana(npcCharacter); const maxMana = selection?.kind === 'player' ? gameState.playerMaxMana : selection?.kind === 'companion' ? (companion?.maxMana ?? (companionCharacter ? getCharacterMaxMana(companionCharacter) : 0)) : estimateNpcMaxMana(npcCharacter); const companionChatTarget = selection?.kind === 'companion' && companionCharacter ? ({ character: companionCharacter, npcId: companion?.npcId ?? null, roleLabel: '同行角色', hp, maxHp, mana, maxMana, affinity: companionNpcState?.affinity ?? null, } satisfies CharacterChatTarget) : null; const inventory = useMemo( () => selection?.kind === 'player' ? gameState.playerInventory : selection?.kind === 'companion' && companionCharacter ? buildCharacterInventoryPreviewItems( companionCharacter, gameState.worldType, ) : (npcState?.inventory ?? []), [ companionCharacter, gameState.playerInventory, gameState.worldType, npcState?.inventory, selection?.kind, ], ); const attributeSchema = resolveAttributeSchema( gameState.worldType, gameState.customWorldProfile, ); const selectedAttributeProfile = selection?.kind === 'player' ? playerCharacter ? resolveCharacterAttributeProfile( playerCharacter, gameState.worldType, gameState.customWorldProfile, ) : null : selection?.kind === 'companion' ? companionCharacter ? resolveCharacterAttributeProfile( companionCharacter, gameState.worldType, gameState.customWorldProfile, ) : null : npcCharacter ? resolveCharacterAttributeProfile( npcCharacter, gameState.worldType, gameState.customWorldProfile, ) : npcEncounter ? resolveEncounterAttributeProfile(npcEncounter, { worldType: gameState.worldType, customWorldProfile: gameState.customWorldProfile, }) : null; const resourceLabels = getResourceLabelsForWorld( gameState.worldType, gameState.customWorldProfile, ); const genericNpcRumors = npcEncounter && !npcCharacter ? buildEncounterAttributeRumors(npcEncounter, { worldType: gameState.worldType, customWorldProfile: gameState.customWorldProfile, limit: 3, }) : []; const genericNpcArchive = npcEncounter && npcState && !npcCharacter ? buildGenericNpcBackstoryArchive( npcEncounter, npcState, genericNpcRumors, Boolean(npcBattleState), ) : null; const genericNpcSkills = useMemo( () => npcEncounter && npcState && !npcCharacter ? buildGenericNpcSkillCatalog({ encounter: npcEncounter, npcState, worldType: gameState.worldType, customWorldProfile: gameState.customWorldProfile, }) : [], [ gameState.customWorldProfile, gameState.worldType, npcCharacter, npcEncounter, npcState, ], ); const displayedSkills = detailCharacter?.skills ?? genericNpcSkills; const buildBreakdown = useMemo( () => selection?.kind === 'player' && playerCharacter ? getPlayerBuildDamageBreakdown(gameState, playerCharacter) : detailCharacter ? getCompanionBuildDamageBreakdown( detailCharacter, gameState.worldType, gameState.customWorldProfile, ) : null, [detailCharacter, gameState, playerCharacter, selection?.kind], ); const selectedContributionRow = buildBreakdown?.rows.find( (row) => row.label === selectedContributionLabel, ) ?? null; const selectedContributionAttributes = selectedContributionRow ? getBuildContributionAttributeRows( selectedContributionRow, attributeSchema, { resourceLabels }, ) : []; const selectedSkill = displayedSkills.find((skill) => skill.id === selectedSkillId) ?? null; const selectedSkillPreviewWorldType = gameState.worldType ?? null; const selectedSkillPreviewMonsterId = selectedSkillPreviewWorldType ? resolveSkillPreviewMonsterId(gameState) : null; const selectedSkillPreviewMode = selection?.kind === 'npc' && npcEncounter && npcCharacter ? 'npc' : 'player'; const selectedInventoryItem = inventory.find((item) => item.id === selectedItemId) ?? null; const selectedSkillOwnerName = detailCharacter?.name ?? npcEncounter?.npcName ?? title; const recentChronicleEntries = gameState.storyEngineMemory?.chronicle?.slice(-3) ?? []; const recentCarrierEchoes = (gameState.storyEngineMemory?.recentCarrierIds ?? []) .map((carrierId) => gameState.playerInventory.find((item) => item.id === carrierId)?.runtimeMetadata?.storyFingerprint?.visibleClue ?? gameState.playerInventory.find((item) => item.id === carrierId)?.name ?? '', ) .filter(Boolean) .slice(0, 3); const sceneResidues = gameState.currentScenePreset?.narrativeResidues?.slice(0, 3) ?? []; const selectedCompanionResolution = detailCharacter ? gameState.storyEngineMemory?.companionResolutions?.find( (resolution) => resolution.characterId === detailCharacter.id, ) ?? null : null; const relatedConsequences = (gameState.storyEngineMemory?.consequenceLedger ?? []) .filter((record) => detailCharacter ? record.relatedIds.includes(detailCharacter.id) : npcEncounter ? record.relatedIds.includes(npcEncounter.id ?? npcEncounter.npcName) : false, ) .slice(-3); useEffect(() => { setSelectedSkillId(null); setSelectedContributionLabel(null); setSelectedItemId(null); }, [selection?.kind, title]); useEffect(() => { if (!selectedContributionLabel || selectedContributionRow) return; setSelectedContributionLabel(null); }, [selectedContributionLabel, selectedContributionRow]); useEffect(() => { if (!selectedSkillId || selectedSkill) return; setSelectedSkillId(null); }, [selectedSkill, selectedSkillId]); useEffect(() => { if (!selectedItemId || selectedInventoryItem) return; setSelectedItemId(null); }, [selectedInventoryItem, selectedItemId]); return ( {selection && ( event.stopPropagation()} >
详情
{title}
{subtitle}
{selection.kind === 'player' && playerCharacter ? ( playerCharacter.visual ? ( ) : ( ) ) : selection.kind === 'companion' && companionCharacter ? ( companionCharacter.visual ? ( ) : ( ) ) : npcCharacter ? ( npcCharacter.visual ? ( ) : ( ) ) : hostileNpcPreset ? ( ) : npcEncounter ? ( ) : null}
{selection.kind === 'npc' && npcEncounter && npcState && (
{getNpcBadge( npcEncounter, npcState.affinity, Boolean(npcBattleState), )}
)}

{description}

{archiveCharacter && archiveNpcState ? (
) : selection.kind === 'npc' && npcState ? (
{genericNpcArchive ? ( ) : null}
) : null} {selection.kind === 'companion' && companionChatTarget ? (
私聊
{privateChatUnlocked ? '已解锁,可直接与该同伴单独交谈。' : `好感达到 ${privateChatUnlockAffinity ?? DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY} 后解锁,当前 ${companionNpcState?.affinity ?? 0}。`}
) : null} {(recentChronicleEntries.length > 0 || recentCarrierEchoes.length > 0 || sceneResidues.length > 0 || relatedConsequences.length > 0 || Boolean(selectedCompanionResolution)) && (
{selectedCompanionResolution && (
队友收束:{selectedCompanionResolution.resolutionType} · {selectedCompanionResolution.summary}
)} {relatedConsequences.length > 0 && (
{relatedConsequences.map((record, index) => (
{record.title} {':'} {record.summary}
))}
)} {recentChronicleEntries.length > 0 && (
{recentChronicleEntries.map((entry, index) => (
{entry.title}
{entry.summary}
))}
)} {recentCarrierEchoes.length > 0 && (
载体回响:{recentCarrierEchoes.join(';')}
)} {sceneResidues.length > 0 && (
{sceneResidues.map((residue, index) => (
{residue.title} {':'} {residue.visibleClue}
))}
)}
)}
{maxMana > 0 ? ( ) : null}
{buildBreakdown ? ( setSelectedContributionLabel(row.label) } /> ) : null}
{detailCharacter ? (
) : displayedSkills.length > 0 ? (
) : null}
{inventory.length > 0 ? ( setSelectedItemId(item.id)} /> ) : (
暂无物品
)}
)} {selectedContributionRow && detailCharacter && ( setSelectedContributionLabel(null)} > event.stopPropagation()} >
标签效果
{selectedContributionRow.label}
{detailCharacter.name}
标签概览
{selectedContributionRow.label}
{getBuildContributionQualityLabel( selectedContributionRow.bonusDelta, )}
总加成{' '} {formatBuildContributionPercent( selectedContributionRow.bonusDelta, )}
属性加成
{selectedContributionAttributes.length > 0 ? (
{selectedContributionAttributes.map((attribute) => (
{attribute.label} {formatBuildContributionPercent( attribute.modifierDelta, )}
{attribute.definition}
))}
) : (
当前标签还没有可展示的属性适配明细。
)}
)} {selectedSkill ? ( setSelectedSkillId(null)} > event.stopPropagation()} >
技能详情
{selectedSkill.name}
{selectedSkillOwnerName}
{detailCharacter && selectedSkillPreviewWorldType ? ( ) : (
{detailCharacter ? '当前未进入具体世界,暂时无法恢复技能预览。' : '该 NPC 当前没有独立技能演示模型,先展示真实技能数据。'}
)}
{getSkillDeliveryLabel(selectedSkill)} {getSkillStyleLabel(selectedSkill)} {selectedSkill.buildBuffs?.length ? ( 附带 {selectedSkill.buildBuffs.length} 个状态标签 ) : null}
伤害
{selectedSkill.damage}
法力
{selectedSkill.manaCost}
冷却
{selectedSkill.cooldownTurns}
距离
{selectedSkill.range}
{selectedSkill.name} 属于{getSkillStyleLabel(selectedSkill)} 路线,通常以{getSkillDeliveryLabel(selectedSkill)}方式出手, 造成 {selectedSkill.damage} 点伤害,消耗{' '} {selectedSkill.manaCost} 点灵力,冷却{' '} {selectedSkill.cooldownTurns} 回合。 {selectedSkill.effects?.length ? ` 该技能还会触发 ${selectedSkill.effects.length} 段战斗特效。` : ''}
{selectedSkill.buildBuffs?.length ? (
附带状态标签
{selectedSkill.buildBuffs.map((buff) => ( {buff.name} / {buff.tags.join('、')} /{' '} {buff.durationTurns} 回合 ))}
) : null}
) : null} {(gameState.playerCharacter ?? detailCharacter) ? ( setSelectedItemId(null)} /> ) : null}
); }