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 {
  getBuildContributionAttributeRows,
  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 { normalizePlayerProgressionState } from '../data/playerProgression';
import { getSceneHostileNpcPresetIds } from '../data/scenePresets';
import type { CharacterChatTarget } from '../hooks/rpg-runtime-story';
import { getResourceLabelsForWorld } from '../services/customWorldPresentation';
import {
  AnimationState,
  type Character,
  type Encounter,
  type GameState,
  type InventoryItem,
  type NpcPersistentState,
  type SceneHostileNpc,
} 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 {
  buildCharacterSkillRenderId,
  getCharacterDetailSpriteStyle,
  getSkillDeliveryLabel,
  getSkillStyleLabel,
} from './CharacterInfoHelpers';
import {
  BuildContributionDetailPanel,
  CharacterAttributeGrid,
  CharacterIdentityBadges,
  CharacterSkillsList,
  MultiplierContributionList,
  PlayerLevelProgress,
  StatusRow,
} from './CharacterInfoShared';
import { PlatformEmptyState } from './common/PlatformEmptyState';
import { PlatformActionButton } from './common/PlatformActionButton';
import { PlatformPillBadge } from './common/PlatformPillBadge';
import { PlatformSubpanel } from './common/PlatformSubpanel';
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 { PixelCloseButton } from './PixelCloseButton';
import { ResolvedAssetImage } from './ResolvedAssetImage';
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 (
    <PlatformSubpanel surface="dark" radius="sm" padding="md">
      <div className="mb-3 text-[10px] uppercase tracking-[0.22em] text-zinc-500">
        {title}
      </div>
      {children}
    </PlatformSubpanel>
  );
}

function SkillMetricCard({ label, value }: { label: string; value: number }) {
  return (
    <PlatformSubpanel
      surface="dark"
      radius="xs"
      padding="sm"
      className="px-3 py-3"
    >
      <div className="text-[10px] uppercase tracking-[0.16em] text-zinc-500">
        {label}
      </div>
      <div className="mt-1 font-semibold text-white">{value}</div>
    </PlatformSubpanel>
  );
}

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 buildSelectionRenderKey(selection: GameCanvasEntitySelection | null) {
  if (!selection) {
    return 'none';
  }

  if (selection.kind === 'player') {
    return 'player';
  }

  if (selection.kind === 'companion') {
    return `companion-${selection.companion.npcId}`;
  }

  const encounter = selection.encounter;
  return `npc-${
    encounter.id ||
    selection.battleState?.id ||
    encounter.characterId ||
    encounter.monsterPresetId ||
    encounter.npcName
  }`;
}

function buildStableRenderKey(
  parts: Array<string | number | null | undefined>,
) {
  return parts
    .map((part, index) => {
      const normalized = String(part ?? '').trim();
      return normalized || `empty-${index}`;
    })
    .join(':');
}

function normalizeInventoryItemRenderIds(
  items: InventoryItem[],
  ownerKey: string,
) {
  const seenIds = new Map<string, number>();

  return items.map((item, index) => {
    // 运行时 NPC 背包可能带空 id；这里只修正展示层 key，不改写原始状态。
    const rawId = item.id.trim();
    const baseId =
      rawId ||
      buildStableRenderKey([
        'inventory',
        ownerKey,
        item.category,
        item.name,
        index,
      ]);
    const repeatedCount = seenIds.get(baseId) ?? 0;
    seenIds.set(baseId, repeatedCount + 1);

    if (rawId && repeatedCount === 0) {
      return item;
    }

    return {
      ...item,
      id:
        repeatedCount === 0
          ? baseId
          : buildStableRenderKey([baseId, repeatedCount]),
    };
  });
}

function NpcEncounterPortrait({
  encounter,
  character,
  hostileNpcPreset,
  battleState,
}: {
  encounter: Encounter;
  character: Character | null;
  hostileNpcPreset: ReturnType<typeof getHostileNpcPresetById> | null;
  battleState: SceneHostileNpc | null;
}) {
  // 详情立绘必须优先服从当前遭遇实例，否则会和画布上点击到的 NPC 形象错位。
  if (encounter.visual) {
    return (
      <MedievalNpcAnimator
        visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(
          encounter.visual,
        )}
        scale={2.08}
      />
    );
  }

  if (encounter.imageSrc?.trim()) {
    return (
      <ResolvedAssetImage
        src={encounter.imageSrc}
        alt={encounter.npcName}
        className="h-full w-full object-contain object-bottom"
        style={{ imageRendering: 'pixelated' }}
      />
    );
  }

  if (hostileNpcPreset) {
    return (
      <HostileNpcAnimator
        hostileNpc={hostileNpcPreset}
        animation={battleState?.animation ?? 'idle'}
        flip={(battleState?.facing ?? 'left') === 'right'}
      />
    );
  }

  if (character?.visual) {
    return (
      <MedievalNpcAnimator
        visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(
          character.visual,
        )}
        scale={2.08}
      />
    );
  }

  if (character) {
    return (
      <CharacterAnimator
        state={AnimationState.IDLE}
        character={character}
        className="h-full w-full"
        imageClassName="object-bottom"
        style={getCharacterDetailSpriteStyle(character)}
      />
    );
  }

  return (
    <MedievalNpcAnimator
      encounter={encounter}
      scale={GENERIC_NPC_SCENE_SCALE / 3}
    />
  );
}

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<BackstoryLockedChapter, 'affinityRequired'>
  > = [
    {
      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<string | null>(null);
  const [selectedContributionLabel, setSelectedContributionLabel] = useState<
    string | null
  >(null);
  const [selectedItemId, setSelectedItemId] = useState<string | null>(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 selectionRenderKey = buildSelectionRenderKey(selection);
  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 normalizedPlayerProgression = normalizePlayerProgressionState(
    gameState.playerProgression ?? null,
  );
  const selectedNpcLevel =
    npcEncounter?.levelProfile?.level ??
    npcBattleState?.levelProfile?.level ??
    null;
  const selectionRoleLabel =
    selection?.kind === 'player'
      ? '队长'
      : selection?.kind === 'companion'
        ? '同行'
        : selection?.kind === 'npc' && npcEncounter && npcState
          ? getNpcBadge(
              npcEncounter,
              npcState.affinity,
              Boolean(npcBattleState),
            )
          : null;
  const selectionLevelText =
    selection?.kind === 'player'
      ? `Lv.${normalizedPlayerProgression.level}`
      : selection?.kind === 'companion'
        ? `参考 Lv.${normalizedPlayerProgression.level}`
        : typeof selectedNpcLevel === 'number'
          ? `Lv.${selectedNpcLevel}`
          : null;
  const selectionRoleTone: 'amber' | 'sky' | 'rose' | 'emerald' | 'zinc' =
    selection?.kind === 'player'
      ? 'amber'
      : selection?.kind === 'companion'
        ? 'sky'
        : selection?.kind === 'npc' && npcEncounter && npcState
          ? npcEncounter.hostile ||
            Boolean(npcBattleState) ||
            npcState.affinity < 0
            ? 'rose'
            : 'emerald'
          : 'zinc';

  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(() => {
    const rawInventory =
      selection?.kind === 'player'
        ? gameState.playerInventory
        : selection?.kind === 'companion' && companionCharacter
          ? buildCharacterInventoryPreviewItems(
              companionCharacter,
              gameState.worldType,
            )
          : (npcState?.inventory ?? []);

    return normalizeInventoryItemRenderIds(rawInventory, selectionRenderKey);
  }, [
    companionCharacter,
    gameState.playerInventory,
    gameState.worldType,
    npcState?.inventory,
    selection?.kind,
    selectionRenderKey,
  ]);
  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, index) =>
        buildCharacterSkillRenderId(skill, index) === 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 (
    <AnimatePresence>
      {selection && (
        <motion.div
          key={`entity-modal-${selectionRenderKey}`}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="fixed inset-0 z-[72] flex items-center justify-center bg-black/72 p-4 backdrop-blur-sm"
          onClick={onClose}
        >
          <motion.div
            initial={{ opacity: 0, scale: 0.96, y: 8 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.96, y: 8 }}
            transition={{ duration: 0.18, ease: 'easeOut' }}
            className="pixel-nine-slice pixel-modal-shell flex max-h-[min(92vh,48rem)] w-full max-w-3xl flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
            style={getNineSliceStyle(UI_CHROME.modalPanel)}
            onClick={(event) => event.stopPropagation()}
          >
            <div className="relative flex items-center justify-between border-b border-white/10 px-5 py-4">
              <div className="min-w-0 pr-10">
                <div className="text-[10px] uppercase tracking-[0.24em] text-zinc-500">
                  详情
                </div>
                <div className="mt-1 text-lg font-semibold text-white">
                  {title}
                </div>
                <div className="mt-1 text-sm text-zinc-400">{subtitle}</div>
                {selectionRoleLabel ? (
                  <CharacterIdentityBadges
                    roleLabel={selectionRoleLabel}
                    levelText={selectionLevelText}
                    roleTone={selectionRoleTone}
                    className="mt-2"
                  />
                ) : null}
              </div>
              <PixelCloseButton onClick={onClose} label="关闭冒险详情" />
            </div>

            <div className="min-h-0 flex-1 overflow-y-auto p-4 sm:p-5">
              <div className="grid gap-4 lg:grid-cols-[minmax(0,0.82fr)_minmax(0,1.18fr)]">
                <div className="space-y-4">
                  <Section title="立绘">
                    <div className="flex flex-col items-center text-center">
                      <div className="flex h-44 w-full max-w-[16rem] items-end justify-center overflow-hidden rounded-2xl border border-white/10 bg-black/20">
                        {selection.kind === 'player' && playerCharacter ? (
                          playerCharacter.visual ? (
                            <MedievalNpcAnimator
                              visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(
                                playerCharacter.visual,
                              )}
                              scale={2.08}
                            />
                          ) : (
                            <CharacterAnimator
                              state={AnimationState.IDLE}
                              character={playerCharacter}
                              className="h-full w-full"
                              imageClassName="object-bottom"
                              style={getCharacterDetailSpriteStyle(
                                playerCharacter,
                              )}
                            />
                          )
                        ) : selection.kind === 'companion' &&
                          companionCharacter ? (
                          companionCharacter.visual ? (
                            <MedievalNpcAnimator
                              visualSpec={buildMedievalNpcVisualFromCustomWorldVisual(
                                companionCharacter.visual,
                              )}
                              scale={2.08}
                            />
                          ) : (
                            <CharacterAnimator
                              state={AnimationState.IDLE}
                              character={companionCharacter}
                              className="h-full w-full"
                              imageClassName="object-bottom"
                              style={getCharacterDetailSpriteStyle(
                                companionCharacter,
                              )}
                            />
                          )
                        ) : npcEncounter ? (
                          <NpcEncounterPortrait
                            encounter={npcEncounter}
                            character={npcCharacter}
                            hostileNpcPreset={hostileNpcPreset}
                            battleState={npcBattleState}
                          />
                        ) : null}
                      </div>
                      <p className="mt-3 text-sm leading-relaxed text-zinc-300">
                        {description}
                      </p>
                    </div>
                  </Section>
                </div>

                <div className="space-y-4">
                  {archiveCharacter && archiveNpcState ? (
                    <Section title="关系">
                      <div className="space-y-3">
                        <AffinityStatusCard
                          affinity={archiveNpcState.affinity}
                        />
                        <BackstoryArchive
                          publicSummary={archivePublicSummary}
                          unlockedChapters={unlockedBackstoryChapters}
                          lockedChapters={lockedBackstoryChapters}
                        />
                      </div>
                    </Section>
                  ) : selection.kind === 'npc' && npcState ? (
                    <Section title="关系">
                      <div className="space-y-3">
                        <AffinityStatusCard affinity={npcState.affinity} />
                        {genericNpcArchive ? (
                          <BackstoryArchive
                            publicSummary={genericNpcArchive.publicSummary}
                            unlockedChapters={
                              genericNpcArchive.unlockedChapters
                            }
                            lockedChapters={genericNpcArchive.lockedChapters}
                          />
                        ) : null}
                      </div>
                    </Section>
                  ) : null}

                  {selection.kind === 'companion' && companionChatTarget ? (
                    <Section title="私聊">
                      <PlatformSubpanel
                        surface="darkSky"
                        radius="lg"
                        padding="md"
                        as="div"
                        data-testid="private-chat-panel"
                      >
                        <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                          <div>
                            <div className="text-[10px] uppercase tracking-[0.18em] text-sky-200/80">
                              私聊
                            </div>
                            <div className="mt-1 text-sm text-zinc-300">
                              {privateChatUnlocked
                                ? '已解锁，可直接与该同伴单独交谈。'
                                : `好感达到 ${privateChatUnlockAffinity ?? DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY} 后解锁，当前 ${companionNpcState?.affinity ?? 0}。`}
                            </div>
                          </div>
                          <PlatformActionButton
                            surface="editorDark"
                            tone="ghost"
                            disabled={
                              !privateChatUnlocked || !onOpenCharacterChat
                            }
                            onClick={() => {
                              if (
                                !privateChatUnlocked ||
                                !onOpenCharacterChat
                              ) {
                                return;
                              }
                              onClose();
                              onOpenCharacterChat(companionChatTarget);
                            }}
                            className="rounded-xl border-sky-300/40 bg-sky-400/15 text-sky-50 hover:bg-sky-400/22 disabled:cursor-not-allowed disabled:border-white/8 disabled:bg-black/20 disabled:text-zinc-500 disabled:opacity-100"
                          >
                            {privateChatUnlocked
                              ? '聊天'
                              : `聊天（${privateChatUnlockAffinity ?? DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY} 解锁）`}
                          </PlatformActionButton>
                        </div>
                      </PlatformSubpanel>
                    </Section>
                  ) : null}

                  {(recentChronicleEntries.length > 0 ||
                    recentCarrierEchoes.length > 0 ||
                    sceneResidues.length > 0 ||
                    relatedConsequences.length > 0 ||
                    Boolean(selectedCompanionResolution)) && (
                    <Section title="最近回响">
                      <div className="space-y-3">
                        {selectedCompanionResolution && (
                          <PlatformSubpanel
                            surface="darkEmerald"
                            radius="xs"
                            padding="row"
                            as="div"
                            className="text-xs"
                            data-testid="companion-resolution-echo"
                          >
                            队友收束：
                            {selectedCompanionResolution.resolutionType} ·{' '}
                            {selectedCompanionResolution.summary}
                          </PlatformSubpanel>
                        )}
                        {relatedConsequences.length > 0 && (
                          <div className="space-y-1">
                            {relatedConsequences.map((record, index) => (
                              <PlatformSubpanel
                                key={
                                  record.id ||
                                  `consequence-${record.title}-${index}`
                                }
                                surface="dark"
                                radius="xs"
                                padding="row"
                                className="text-xs text-zinc-400"
                                data-testid="recent-consequence-echo"
                              >
                                <span className="text-white">
                                  {record.title}
                                </span>
                                {'：'}
                                {record.summary}
                              </PlatformSubpanel>
                            ))}
                          </div>
                        )}
                        {recentChronicleEntries.length > 0 && (
                          <div className="space-y-1">
                            {recentChronicleEntries.map((entry, index) => (
                              <PlatformSubpanel
                                key={
                                  entry.id ||
                                  `chronicle-${entry.title}-${index}`
                                }
                                surface="dark"
                                radius="xs"
                                padding="row"
                                data-testid="recent-chronicle-echo"
                              >
                                <div className="text-sm font-medium text-white">
                                  {entry.title}
                                </div>
                                <div className="mt-1 text-xs text-zinc-400">
                                  {entry.summary}
                                </div>
                              </PlatformSubpanel>
                            ))}
                          </div>
                        )}
                        {recentCarrierEchoes.length > 0 && (
                          <PlatformSubpanel
                            surface="dark"
                            radius="xs"
                            padding="row"
                            className="text-xs text-amber-100/85"
                            data-testid="recent-carrier-echo"
                          >
                            载体回响：{recentCarrierEchoes.join('；')}
                          </PlatformSubpanel>
                        )}
                        {sceneResidues.length > 0 && (
                          <div className="space-y-1">
                            {sceneResidues.map((residue, index) => (
                              <PlatformSubpanel
                                key={
                                  residue.id ||
                                  `residue-${residue.title}-${index}`
                                }
                                surface="dark"
                                radius="xs"
                                padding="row"
                                className="text-xs text-zinc-400"
                                data-testid="recent-scene-residue-echo"
                              >
                                <span className="text-white">
                                  {residue.title}
                                </span>
                                {'：'}
                                {residue.visibleClue}
                              </PlatformSubpanel>
                            ))}
                          </div>
                        )}
                      </div>
                    </Section>
                  )}

                  <Section title="属性">
                    <div className="space-y-4">
                      {selection.kind === 'player' ? (
                        <PlatformSubpanel
                          surface="darkAmber"
                          radius="xs"
                          padding="sm"
                          as="div"
                          data-testid="player-level-panel"
                        >
                          <div className="mb-2 text-[10px] uppercase tracking-[0.18em] text-amber-100/75">
                            等级
                          </div>
                          <PlayerLevelProgress
                            level={normalizedPlayerProgression.level}
                            currentLevelXp={
                              normalizedPlayerProgression.currentLevelXp
                            }
                            xpToNextLevel={
                              normalizedPlayerProgression.xpToNextLevel
                            }
                          />
                        </PlatformSubpanel>
                      ) : null}
                      <div className="space-y-3">
                        <StatusRow
                          label={resourceLabels.hp}
                          current={hp}
                          max={maxHp}
                          tone="hp"
                        />
                        {maxMana > 0 ? (
                          <StatusRow
                            label={resourceLabels.mp}
                            current={mana}
                            max={maxMana}
                            tone="mp"
                          />
                        ) : null}
                      </div>
                      {buildBreakdown ? (
                        <MultiplierContributionList
                          breakdown={buildBreakdown}
                          onSelectContribution={(row) =>
                            setSelectedContributionLabel(row.label)
                          }
                        />
                      ) : null}
                      <CharacterAttributeGrid
                        attributeProfile={selectedAttributeProfile}
                        attributeSchema={attributeSchema}
                        buildBreakdown={buildBreakdown}
                        resourceLabels={resourceLabels}
                        gridClassName="grid grid-cols-1 gap-2 text-sm text-zinc-300 sm:grid-cols-2"
                        cardClassName="rounded-xl border border-white/8 bg-black/25 px-3 py-2"
                      />
                    </div>
                  </Section>

                  {detailCharacter ? (
                    <Section title="技能">
                      <CharacterSkillsList
                        skills={displayedSkills}
                        onSelectSkill={setSelectedSkillId}
                      />
                    </Section>
                  ) : displayedSkills.length > 0 ? (
                    <Section title="技能">
                      <CharacterSkillsList
                        skills={displayedSkills}
                        onSelectSkill={setSelectedSkillId}
                      />
                    </Section>
                  ) : null}

                  <Section title="物品">
                    {inventory.length > 0 ? (
                      <InventoryItemGrid
                        items={inventory}
                        selectedItemId={selectedItemId}
                        minimumSlotCount={8}
                        onSelectItem={(item) => setSelectedItemId(item.id)}
                      />
                    ) : (
                      <PlatformEmptyState
                        surface="editorDark"
                        size="compact"
                        tone="soft"
                      >
                        暂无物品
                      </PlatformEmptyState>
                    )}
                  </Section>
                </div>
              </div>
            </div>
          </motion.div>
        </motion.div>
      )}

      {selectedContributionRow && detailCharacter && (
        <motion.div
          key={`contribution-modal-${selectionRenderKey}-${selectedContributionRow.label}`}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="fixed inset-0 z-[75] flex items-center justify-center bg-black/72 p-4 backdrop-blur-sm"
          onClick={() => setSelectedContributionLabel(null)}
        >
          <motion.div
            initial={{ opacity: 0, scale: 0.96, y: 8 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.96, y: 8 }}
            transition={{ duration: 0.18, ease: 'easeOut' }}
            className="pixel-nine-slice pixel-modal-shell flex max-h-[min(88vh,40rem)] w-full max-w-xl flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
            style={getNineSliceStyle(UI_CHROME.modalPanel)}
            onClick={(event) => event.stopPropagation()}
          >
            <div className="relative border-b border-white/10 px-4 py-3 sm:px-5 sm:py-4">
              <div className="min-w-0 pr-10">
                <div className="text-[10px] tracking-[0.22em] text-sky-300/80">
                  标签效果
                </div>
                <div className="mt-1 truncate text-sm font-semibold text-white">
                  {selectedContributionRow.label}
                </div>
                <div className="mt-1 text-[10px] tracking-[0.18em] text-zinc-500">
                  {detailCharacter.name}
                </div>
              </div>
              <PixelCloseButton
                onClick={() => setSelectedContributionLabel(null)}
                label="关闭标签效果"
              />
            </div>

            <div className="overflow-y-auto p-4 sm:p-5">
              <BuildContributionDetailPanel
                row={selectedContributionRow}
                attributes={selectedContributionAttributes}
              />
            </div>
          </motion.div>
        </motion.div>
      )}

      {selectedSkill ? (
        <motion.div
          key={`skill-modal-${selectionRenderKey}-${buildCharacterSkillRenderId(
            selectedSkill,
            displayedSkills.indexOf(selectedSkill),
          )}`}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="fixed inset-0 z-[76] flex items-center justify-center bg-black/72 p-4 backdrop-blur-sm"
          onClick={() => setSelectedSkillId(null)}
        >
          <motion.div
            initial={{ opacity: 0, scale: 0.96, y: 8 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.96, y: 8 }}
            transition={{ duration: 0.18, ease: 'easeOut' }}
            className="pixel-nine-slice pixel-modal-shell flex max-h-[min(90vh,54rem)] w-full max-w-4xl flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
            style={getNineSliceStyle(UI_CHROME.modalPanel)}
            onClick={(event) => event.stopPropagation()}
          >
            <div className="relative border-b border-white/10 px-4 py-3 sm:px-5 sm:py-4">
              <div className="min-w-0 pr-10">
                <div className="text-[10px] tracking-[0.22em] text-sky-300/80">
                  技能详情
                </div>
                <div className="mt-1 truncate text-sm font-semibold text-white">
                  {selectedSkill.name}
                </div>
                <div className="mt-1 text-[10px] tracking-[0.18em] text-zinc-500">
                  {selectedSkillOwnerName}
                </div>
              </div>
              <PixelCloseButton
                onClick={() => setSelectedSkillId(null)}
                label="关闭技能详情"
              />
            </div>

            <div className="space-y-4 overflow-y-auto p-4 sm:p-5">
              {detailCharacter && selectedSkillPreviewWorldType ? (
                <SkillEffectPreview
                  mode={selectedSkillPreviewMode}
                  worldType={selectedSkillPreviewWorldType}
                  character={detailCharacter}
                  skill={selectedSkill}
                  targetMonsterId={selectedSkillPreviewMonsterId}
                  npcEncounter={
                    selectedSkillPreviewMode === 'npc' ? npcEncounter : null
                  }
                  targetCharacter={
                    selectedSkillPreviewMode === 'npc' ? playerCharacter : null
                  }
                />
              ) : (
                <PlatformSubpanel
                  surface="dark"
                  radius="xs"
                  padding="sm"
                  className="px-4 py-3 text-sm text-zinc-400"
                >
                  {detailCharacter
                    ? '当前未进入具体世界，暂时无法恢复技能预览。'
                    : '该 NPC 当前没有独立技能演示模型，先展示真实技能数据。'}
                </PlatformSubpanel>
              )}

              <div className="flex flex-wrap gap-2">
                <PlatformPillBadge tone="darkSoft" size="xxs" className="px-2">
                  {getSkillDeliveryLabel(selectedSkill)}
                </PlatformPillBadge>
                <PlatformPillBadge tone="darkSky" size="xxs" className="px-2">
                  {getSkillStyleLabel(selectedSkill)}
                </PlatformPillBadge>
                {selectedSkill.buildBuffs?.length ? (
                  <PlatformPillBadge
                    tone="darkEmerald"
                    size="xxs"
                    className="px-2"
                  >
                    附带 {selectedSkill.buildBuffs.length} 个状态标签
                  </PlatformPillBadge>
                ) : null}
              </div>

              <div className="grid grid-cols-2 gap-2 text-sm text-zinc-300 sm:grid-cols-4">
                <SkillMetricCard label="伤害" value={selectedSkill.damage} />
                <SkillMetricCard label="法力" value={selectedSkill.manaCost} />
                <SkillMetricCard
                  label="冷却"
                  value={selectedSkill.cooldownTurns}
                />
                <SkillMetricCard label="距离" value={selectedSkill.range} />
              </div>

              <PlatformSubpanel
                surface="dark"
                radius="xs"
                padding="sm"
                className="px-4 py-3 text-sm leading-relaxed text-zinc-300"
              >
                {selectedSkill.name} 属于{getSkillStyleLabel(selectedSkill)}
                路线，通常以{getSkillDeliveryLabel(selectedSkill)}方式出手，
                造成 {selectedSkill.damage} 点伤害，消耗{' '}
                {selectedSkill.manaCost} 点灵力，冷却{' '}
                {selectedSkill.cooldownTurns} 回合。
                {selectedSkill.effects?.length
                  ? ` 该技能还会触发 ${selectedSkill.effects.length} 段战斗特效。`
                  : ''}
              </PlatformSubpanel>

              {selectedSkill.buildBuffs?.length ? (
                <PlatformSubpanel
                  surface="dark"
                  radius="xs"
                  padding="sm"
                  className="px-4 py-3"
                >
                  <div className="text-[10px] uppercase tracking-[0.16em] text-zinc-500">
                    附带状态标签
                  </div>
                  <div className="mt-3 flex flex-wrap gap-2">
                    {selectedSkill.buildBuffs.map((buff, index) => (
                      <PlatformPillBadge
                        key={buildStableRenderKey([
                          'skill-buff',
                          selectedSkill.id,
                          buff.id,
                          buff.name,
                          index,
                        ])}
                        tone="darkSky"
                        size="xxs"
                        className="px-2"
                      >
                        {buff.name} / {buff.tags.join('、')} /{' '}
                        {buff.durationTurns} 回合
                      </PlatformPillBadge>
                    ))}
                  </div>
                </PlatformSubpanel>
              ) : null}
            </div>
          </motion.div>
        </motion.div>
      ) : null}

      {(gameState.playerCharacter ?? detailCharacter) ? (
        <InventoryItemDetailModal
          item={selectedInventoryItem}
          playerCharacter={gameState.playerCharacter ?? detailCharacter!}
          worldType={gameState.worldType}
          ownerLabel={detailCharacter?.name ?? title}
          onClose={() => setSelectedItemId(null)}
        />
      ) : null}
    </AnimatePresence>
  );
}
