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 (
);
}
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()}
>
{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.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}
);
}