import {
  Backpack,
  Clock3,
  Coins,
  Crosshair,
  Footprints,
  Heart,
  Loader2,
  MapPinned,
  PackageOpen,
  ScrollText,
  Skull,
  Swords,
  Users,
} from 'lucide-react';
import { motion } from 'motion/react';
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';

import { formatCurrency } from '../../data/economy';
import { getEquipmentSlotFromItem } from '../../data/equipmentEffects';
import { isContinueAdventureOption } from '../../data/functionCatalog';
import { getHostileNpcPresetById } from '../../data/hostileNpcPresets';
import { resolveInventoryItemUseEffect } from '../../data/inventoryEffects';
import { isQuestReadyToClaim } from '../../data/questFlow';
import { getScenePresetById } from '../../data/scenePresets';
import { getOptionImpactSummary } from '../../hooks/combatStoryUtils';
import type {
  BattleRewardUi,
  NpcChatQuestOfferUi,
  QuestFlowUi,
} from '../../hooks/rpg-runtime-story';
import type {
  ChapterState,
  Character,
  GoalHandoff,
  GoalPulseEvent,
  GoalStackState,
  InventoryItem,
  JourneyBeat,
  NpcBattleMode,
  QuestLogEntry,
  StoryMoment,
  StoryOption,
  WorldType,
} from '../../types';
import {
  CHROME_ICONS,
  getInventoryCategoryIcon,
  getNineSliceStyle,
  TAB_ICONS,
  UI_CHROME,
} from '../../uiAssets';
import { HostileNpcAnimator } from '../HostileNpcAnimator';
import { PixelIcon } from '../PixelIcon';

export interface RpgAdventurePanelProps {
  aiError: string | null;
  currentStory: StoryMoment;
  isLoading: boolean;
  displayedOptions: StoryOption[];
  hideOptions: boolean;
  canRefreshOptions: boolean;
  onRefreshOptions: () => void;
  onChoice: (option: StoryOption) => void;
  onSubmitNpcChatInput?: (input: string) => boolean;
  onExitNpcChat?: () => boolean;
  onOpenCharacter: () => void;
  onOpenInventory: () => void;
  playerCharacter: Character;
  worldType: WorldType | null;
  quests: QuestLogEntry[];
  questUi: QuestFlowUi;
  npcChatQuestOfferUi: NpcChatQuestOfferUi;
  goalStack: GoalStackState;
  goalPulse: GoalPulseEvent | null;
  onDismissGoalPulse: () => void;
  battleRewardUi: BattleRewardUi;
  playerHp: number;
  playerMaxHp: number;
  playerMana: number;
  playerMaxMana: number;
  playerSkillCooldowns: Record<string, number>;
  inBattle: boolean;
  currentNpcBattleMode: NpcBattleMode | null;
  statistics: {
    playTimeMs: number;
    hostileNpcsDefeated: number;
    questsAccepted: number;
    questsCompleted: number;
    questsTurnedIn: number;
    itemsUsed: number;
    scenesTraveled: number;
    currentSceneName: string;
    playerCurrency: number;
    playerLevel?: number;
    playerCurrentLevelXp?: number;
    playerXpToNextLevel?: number;
    playerTotalXp?: number;
    inventoryItemCount: number;
    inventoryStackCount: number;
    activeCompanionCount: number;
    rosterCompanionCount: number;
  };
  musicVolume: number;
  onMusicVolumeChange: (value: number) => void;
  onSaveAndExit: () => void;
  chapterState?: ChapterState | null;
  journeyBeat?: JourneyBeat | null;
  currentSceneActTitle?: string | null;
  currentSceneActIndex?: number | null;
  currentSceneActCount?: number | null;
}

const RpgAdventurePanelOverlays = lazy(async () => {
  const module = await import('./RpgAdventurePanelOverlays');

  return {
    default: module.RpgAdventurePanelOverlays,
  };
});

function AdventurePanelOverlayLoadingFallback() {
  return (
    <div className="fixed inset-0 z-[74] flex items-center justify-center bg-black/72 p-4 backdrop-blur-sm">
      <div
        className="pixel-nine-slice pixel-modal-shell flex min-h-32 w-full max-w-sm items-center justify-center px-5 py-6 text-center text-[11px] uppercase tracking-[0.24em] text-zinc-400 shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
        style={getNineSliceStyle(UI_CHROME.modalPanel)}
      >
        正在载入冒险面板
      </div>
    </div>
  );
}

function getOptionActionTextClass(option: StoryOption) {
  if ((option.priority ?? 1) >= 3)
    return 'text-fuchsia-200 group-hover:text-fuchsia-100';
  if ((option.priority ?? 1) >= 2)
    return 'text-sky-200 group-hover:text-sky-100';
  return 'text-zinc-300 group-hover:text-white';
}

function getOptionFunctionTagText(option: StoryOption) {
  const tagByFunctionId: Record<string, string> = {
    battle_all_in_crush: '战斗',
    battle_attack_basic: '战斗',
    battle_escape_breakout: '逃跑',
    battle_feint_step: '战斗',
    battle_finisher_window: '战斗',
    battle_guard_break: '战斗',
    battle_probe_pressure: '战斗',
    battle_recover_breath: '调息',
    battle_use_skill: '技能',
    camp_travel_home_scene: '场景',
    idle_call_out: '试探',
    idle_explore_forward: '探索',
    idle_follow_clue: '线索',
    idle_observe_signs: '观察',
    idle_rest_focus: '调息',
    idle_travel_next_scene: '场景',
    npc_chat: '聊天',
    npc_fight: '战斗',
    npc_gift: '送礼',
    npc_help: '求助',
    npc_leave: '离开',
    npc_preview_talk: '聊天',
    npc_quest_accept: '任务',
    npc_quest_turn_in: '任务',
    npc_recruit: '招募',
    npc_spar: '切磋',
    npc_trade: '交易',
    story_continue_adventure: '继续',
    treasure_inspect: '探查',
    treasure_leave: '离开',
    treasure_secure: '收取',
  };

  if (option.functionId.startsWith('npc_chat_quest_offer_')) {
    return '任务';
  }

  return tagByFunctionId[option.functionId] ?? null;
}

function RpgOptionActionLabel({ option }: { option: StoryOption }) {
  const tagText = getOptionFunctionTagText(option);

  return (
    <span className="flex min-w-0 flex-wrap items-center gap-1.5">
      {tagText ? (
        <span className="shrink-0 rounded border border-white/10 bg-white/10 px-1.5 py-0.5 text-[9px] leading-none text-zinc-300">
          {tagText}
        </span>
      ) : null}
      <span
        className={`min-w-0 break-words text-sm sm:text-[15px] ${getOptionActionTextClass(option)}`}
      >
        {option.actionText}
      </span>
    </span>
  );
}

function getDialogueTurnAlignmentClass(
  turn: NonNullable<StoryMoment['dialogue']>[number],
) {
  if (turn.speaker === 'system') {
    return 'justify-center';
  }

  return turn.speaker === 'player' ? 'justify-end' : 'justify-start';
}

function getDialogueTurnBubbleClass(
  turn: NonNullable<StoryMoment['dialogue']>[number],
) {
  if (turn.speaker === 'system') {
    return 'border-white/12 bg-white/[0.06] text-zinc-100';
  }

  if (turn.speaker === 'player') {
    return 'border-sky-400/20 bg-sky-500/10 text-sky-50';
  }

  if (turn.speaker === 'companion') {
    return 'border-emerald-400/20 bg-emerald-500/10 text-emerald-50';
  }

  return 'border-amber-400/20 bg-amber-500/10 text-amber-50';
}

function getDialogueTurnBubbleShapeClass(
  turn: NonNullable<StoryMoment['dialogue']>[number],
) {
  if (turn.speaker === 'system') {
    return 'rounded-full';
  }

  if (turn.speaker === 'player') {
    return 'rounded-2xl rounded-br-none';
  }

  if (turn.speaker === 'companion') {
    return 'rounded-2xl';
  }

  return 'rounded-2xl rounded-bl-none';
}

function getDialogueTurnLabel(
  turn: NonNullable<StoryMoment['dialogue']>[number],
) {
  if (turn.speaker === 'system') {
    return '系统';
  }

  if (turn.speaker === 'player') {
    return '你';
  }

  if (turn.speaker === 'companion') {
    return turn.speakerName?.trim() || '同伴';
  }

  return turn.speakerName?.trim() || '对方';
}

function getQuestRewardItemIcon(item: InventoryItem) {
  if (item.iconSrc) return item.iconSrc;
  if (item.tags.includes('weapon')) return '/UI/Icon_Eq_Weapon.png';
  if (item.tags.includes('armor')) return '/UI/Icon_Eq_Chest.png';
  if (item.tags.includes('relic')) return '/Icons/47_treasure.png';
  if (item.tags.includes('healing')) return '/Icons/12_potion.png';
  if (item.tags.includes('mana')) return '/UI/Hud_icon_magic.png';
  if (item.tags.includes('material')) return '/Icons/45_crystal.png';
  return getInventoryCategoryIcon(item.category);
}

function getRewardItemFrameClass(rarity: InventoryItem['rarity']) {
  switch (rarity) {
    case 'legendary':
      return 'border-amber-300/45 bg-gradient-to-br from-amber-500/18 via-orange-500/10 to-transparent shadow-[0_0_28px_rgba(245,158,11,0.12)]';
    case 'epic':
      return 'border-fuchsia-300/40 bg-gradient-to-br from-fuchsia-500/16 via-indigo-500/10 to-transparent shadow-[0_0_26px_rgba(217,70,239,0.1)]';
    case 'rare':
      return 'border-sky-300/40 bg-gradient-to-br from-sky-500/16 via-cyan-500/10 to-transparent shadow-[0_0_24px_rgba(56,189,248,0.08)]';
    case 'uncommon':
      return 'border-emerald-300/35 bg-gradient-to-br from-emerald-500/14 via-lime-500/8 to-transparent';
    default:
      return 'border-white/10 bg-white/[0.04]';
  }
}

function buildRewardItemDescription(item: InventoryItem) {
  if (item.description?.trim()) return item.description;

  const traits: string[] = [];
  if (item.tags.includes('healing')) traits.push('冒险中可恢复生命');
  if (item.tags.includes('mana')) traits.push('可恢复内力或保持法术节奏');
  if (item.tags.includes('weapon')) traits.push('适合进攻型构筑');
  if (item.tags.includes('armor')) traits.push('适合防御型构筑');
  if (item.tags.includes('relic')) traits.push('可作为稀有饰品级奖励');
  if (item.tags.includes('material')) traits.push('可作为制作材料');

  if (traits.length === 0) {
    return '任务奖励物品，可用于后续路线、交易或构筑规划';
  }

  return item.name + ' 奖励物品';
}

function formatPlayTime(playTimeMs: number) {
  const totalSeconds = Math.max(0, Math.floor(playTimeMs / 1000));
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;

  if (hours > 0) {
    return `${hours}小时 ${String(minutes).padStart(2, '0')}分${String(seconds).padStart(2, '0')}秒`;
  }

  return `${minutes}分${String(seconds).padStart(2, '0')}秒`;
}

function getOptionGoalAffordanceClass(option: StoryOption) {
  switch (option.goalAffordance?.relation) {
    case 'advance':
      return 'text-amber-200/85';
    case 'support':
      return 'text-sky-200/80';
    case 'detour':
      return 'text-zinc-400';
    default:
      return 'text-zinc-500';
  }
}

function RewardItemIconGrid({
  items,
  selectedItemId,
  onSelectItem,
  emptyText,
}: {
  items: InventoryItem[];
  selectedItemId: string | null;
  onSelectItem: (itemId: string) => void;
  emptyText: string;
}) {
  if (items.length === 0) {
    return (
      <div className="mt-3 rounded-xl border border-dashed border-white/10 bg-black/20 px-3 py-4 text-center text-xs text-zinc-500">
        {emptyText}
      </div>
    );
  }

  return (
    <div className="mt-3 flex flex-wrap gap-2.5">
      {items.map((item) => (
        <button
          key={item.id}
          type="button"
          onClick={() => onSelectItem(item.id)}
          aria-label={`查看奖励物品 ${item.name}`}
          title={item.name}
          className={`group relative flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl border transition hover:-translate-y-0.5 hover:border-white/20 sm:h-[4.5rem] sm:w-[4.5rem] ${getRewardItemFrameClass(
            item.rarity,
          )} ${
            selectedItemId === item.id
              ? 'ring-1 ring-amber-300/55 ring-offset-0'
              : ''
          }`}
        >
          <div className="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.12),transparent_55%)] opacity-0 transition group-hover:opacity-100" />
          <PixelIcon
            src={getQuestRewardItemIcon(item)}
            alt={item.name}
            className="relative z-[1] h-8 w-8 drop-shadow-[0_6px_10px_rgba(0,0,0,0.35)] sm:h-9 sm:w-9"
          />
          <span className="absolute bottom-1 right-1 rounded-full border border-black/35 bg-black/70 px-1.5 py-0.5 text-[10px] font-semibold text-white">
            {item.quantity}
          </span>
        </button>
      ))}
    </div>
  );
}

function getQuestObjectivePresentation(
  quest: QuestLogEntry,
  worldType: WorldType | null,
  sceneName: string,
) {
  const hostileNpcPreset =
    worldType && quest.objective.targetHostileNpcId
      ? getHostileNpcPresetById(worldType, quest.objective.targetHostileNpcId)
      : null;

  switch (quest.objective.kind) {
    case 'defeat_hostile_npc':
      return {
        eyebrow: '悬赏目标',
        accentClass: 'from-rose-500/20 via-orange-500/10 to-transparent',
        labelClass: 'border-rose-300/25 bg-rose-500/12 text-rose-50',
        icon: Crosshair,
        primaryLabel:
          hostileNpcPreset?.name ??
          quest.objective.targetHostileNpcId ??
          '未知敌对角色',
        secondaryLabel: sceneName,
        hostileNpcPreset,
        iconSrc: null as string | null,
      };
    case 'inspect_treasure':
      return {
        eyebrow: '宝藏踪迹',
        accentClass: 'from-amber-500/20 via-yellow-500/10 to-transparent',
        labelClass: 'border-amber-300/25 bg-amber-500/12 text-amber-50',
        icon: PackageOpen,
        primaryLabel: sceneName,
        secondaryLabel: '探查隐藏的奖励地点',
        hostileNpcPreset: null,
        iconSrc: '/Icons/47_treasure.png',
      };
    case 'spar_with_npc':
    default:
      return {
        eyebrow: '切磋会话',
        accentClass: 'from-sky-500/20 via-cyan-500/10 to-transparent',
        labelClass: 'border-sky-300/25 bg-sky-500/12 text-sky-50',
        icon: Swords,
        primaryLabel: quest.issuerNpcName,
        secondaryLabel: sceneName,
        hostileNpcPreset: null,
        iconSrc: '/UI/1_weapon.png',
      };
  }
}

function QuestProgressPips({
  progress,
  total,
  activeClassName,
}: {
  progress: number;
  total: number;
  activeClassName: string;
}) {
  const safeTotal = Math.max(1, total);

  return (
    <div
      className="grid gap-1.5"
      style={{ gridTemplateColumns: `repeat(${safeTotal}, minmax(0, 1fr))` }}
    >
      {Array.from({ length: safeTotal }, (_, index) => (
        <div
          key={`quest-progress-${safeTotal}-${index}`}
          className={`h-2 rounded-full border ${
            index < progress ? activeClassName : 'border-white/8 bg-black/20'
          }`}
        />
      ))}
    </div>
  );
}

function QuestRewardGrid({
  quest,
  worldType,
  selectedItemId,
  onSelectItem,
}: {
  quest: QuestLogEntry;
  worldType: WorldType | null;
  selectedItemId: string | null;
  onSelectItem: (itemId: string) => void;
}) {
  return (
    <div className="rounded-2xl border border-amber-300/15 bg-[radial-gradient(circle_at_top,rgba(251,191,36,0.14),transparent_65%),rgba(0,0,0,0.24)] p-3">
      <div className="flex items-start justify-between gap-3">
        <div>
          <div className="text-[10px] tracking-[0.24em] text-amber-200/80">
            奖励物品
          </div>
          <div className="mt-1 text-[11px] leading-relaxed text-zinc-500">
            点击物品图标查看详情。
          </div>
        </div>
        <ScrollText className="h-4 w-4 text-amber-200/70" />
      </div>

      <div className="mt-3 grid grid-cols-2 gap-2">
        <div className="rounded-xl border border-rose-300/18 bg-rose-500/10 px-3 py-2.5 text-rose-50">
          <div className="flex items-center gap-2">
            <Heart className="h-4 w-4" />
            <span className="text-sm font-semibold">
              +{quest.reward.affinityBonus}
            </span>
          </div>
          <div className="mt-1 text-[10px] uppercase tracking-[0.2em] text-rose-100/70">
            好感度
          </div>
        </div>
        <div className="rounded-xl border border-amber-300/18 bg-amber-500/10 px-3 py-2.5 text-amber-50">
          <div className="flex items-center gap-2">
            <Coins className="h-4 w-4" />
            <span className="text-sm font-semibold">
              {formatCurrency(quest.reward.currency, worldType)}
            </span>
          </div>
          <div className="mt-1 text-[10px] uppercase tracking-[0.2em] text-amber-100/70">
            货币
          </div>
        </div>
        <div className="rounded-xl border border-sky-300/18 bg-sky-500/10 px-3 py-2.5 text-sky-50">
          <div className="flex items-center gap-2">
            <ScrollText className="h-4 w-4" />
            <span className="text-sm font-semibold">
              +{quest.reward.experience ?? 0}
            </span>
          </div>
          <div className="mt-1 text-[10px] uppercase tracking-[0.2em] text-sky-100/70">
            经验
          </div>
        </div>
      </div>

      <RewardItemIconGrid
        items={quest.reward.items}
        selectedItemId={selectedItemId}
        onSelectItem={onSelectItem}
        emptyText="本任务无物品奖励。"
      />
    </div>
  );
}

function QuestObjectiveCard({
  quest,
  worldType,
  sceneName,
  statusLabel,
}: {
  quest: QuestLogEntry;
  worldType: WorldType | null;
  sceneName: string;
  statusLabel: string;
}) {
  const presentation = getQuestObjectivePresentation(
    quest,
    worldType,
    sceneName,
  );
  const AccentIcon = presentation.icon;

  return (
    <div
      className={`overflow-hidden rounded-2xl border border-white/10 bg-gradient-to-br ${presentation.accentClass}`}
    >
      <div className="flex items-center justify-between gap-3 border-b border-white/10 px-3 py-3">
        <div>
          <div className="text-[10px] tracking-[0.24em] text-white/70">
            {presentation.eyebrow}
          </div>
          <div className="mt-1 text-sm font-semibold text-white">
            {presentation.primaryLabel}
          </div>
        </div>
        <div
          className={`rounded-full border px-2.5 py-1 text-[10px] tracking-[0.16em] ${presentation.labelClass}`}
        >
          {statusLabel}
        </div>
      </div>

      <div className="grid gap-3 p-3 sm:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
        <div className="relative overflow-hidden rounded-2xl border border-white/10 bg-black/30 p-3">
          <div className="absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-black/40 to-transparent" />
          <div className="flex h-36 items-end justify-center">
            {presentation.hostileNpcPreset ? (
              <div className="rounded-full bg-[radial-gradient(circle,rgba(248,113,113,0.18),transparent_70%)] p-4">
                <HostileNpcAnimator
                  hostileNpc={presentation.hostileNpcPreset}
                  animation={
                    presentation.hostileNpcPreset.animations.move
                      ? 'move'
                      : 'idle'
                  }
                  className="origin-bottom scale-150 sm:scale-[1.85]"
                />
              </div>
            ) : (
              <motion.div
                animate={{ y: [0, -4, 0], scale: [1, 1.03, 1] }}
                transition={{
                  duration: 2.8,
                  ease: 'easeInOut',
                  repeat: Infinity,
                }}
                className="flex h-24 w-24 items-center justify-center rounded-3xl border border-white/10 bg-black/35"
              >
                {presentation.iconSrc ? (
                  <PixelIcon
                    src={presentation.iconSrc}
                    alt={presentation.primaryLabel}
                    className="h-14 w-14"
                  />
                ) : (
                  <AccentIcon className="h-11 w-11 text-white/85" />
                )}
              </motion.div>
            )}
          </div>
        </div>

        <div className="space-y-3">
          <div className="grid gap-2 sm:grid-cols-2">
            <div className="rounded-xl border border-white/10 bg-black/25 px-3 py-2.5 text-zinc-100">
              <div className="flex items-center gap-2 text-[10px] uppercase tracking-[0.18em] text-zinc-500">
                <AccentIcon className="h-3.5 w-3.5" />
                Objective
              </div>
              <div className="mt-2 text-sm font-medium text-white">
                {presentation.primaryLabel}
              </div>
            </div>
            <div className="rounded-xl border border-white/10 bg-black/25 px-3 py-2.5 text-zinc-100">
              <div className="flex items-center gap-2 text-[10px] uppercase tracking-[0.18em] text-zinc-500">
                <MapPinned className="h-3.5 w-3.5" />
                Area
              </div>
              <div className="mt-2 text-sm font-medium text-white">
                {presentation.secondaryLabel}
              </div>
            </div>
          </div>

          <div className="rounded-xl border border-white/10 bg-black/25 px-3 py-3">
            <div className="flex items-center justify-between gap-2">
              <div className="text-[10px] uppercase tracking-[0.2em] text-zinc-500">
                当前进度
              </div>
              <div className="text-xs text-zinc-400">
                {quest.progress}/{quest.objective.requiredCount}
              </div>
            </div>
            <div className="mt-3">
              <QuestProgressPips
                progress={quest.progress}
                total={quest.objective.requiredCount}
                activeClassName="border-amber-200/40 bg-amber-400/75"
              />
            </div>
          </div>

          <div className="rounded-xl border border-white/10 bg-black/25 px-3 py-3">
            <div className="text-[10px] uppercase tracking-[0.2em] text-zinc-500">
              任务描述
            </div>
            <div className="mt-2 text-sm leading-6 text-zinc-100">
              {quest.description}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function RpgAdventureStorySection(props: {
  currentSceneActTitle: string | null;
  currentSceneActIndex: number | null;
  currentSceneActCount: number | null;
  limitedNpcChatRemainingTurns: number | null;
  storyScrollContainerRef: React.RefObject<HTMLDivElement | null>;
  isDialogueStory: boolean;
  dialogueTurns: NonNullable<StoryMoment['dialogue']>;
  isNpcChatMode: boolean;
  isStoryStreaming: boolean;
  currentStory: StoryMoment;
}) {
  const {
    currentSceneActTitle,
    currentSceneActIndex,
    currentSceneActCount,
    limitedNpcChatRemainingTurns,
    storyScrollContainerRef,
    isDialogueStory,
    dialogueTurns,
    isNpcChatMode,
    isStoryStreaming,
    currentStory,
  } = props;

  return (
    <div
      ref={storyScrollContainerRef}
      className="pixel-nine-slice pixel-panel mb-2 min-h-0 flex-1 overflow-y-auto pr-1 scrollbar-hide"
      style={getNineSliceStyle(UI_CHROME.storyPanel)}
    >
      {(currentSceneActTitle || limitedNpcChatRemainingTurns !== null) && (
        <div className="mb-3 flex flex-wrap items-center gap-2 px-1">
          {currentSceneActTitle ? (
            <div className="inline-flex items-center gap-2 rounded-full border border-sky-300/18 bg-sky-500/10 px-3 py-1 text-[10px] tracking-[0.16em] text-sky-100">
              <span>当前幕</span>
              <span className="text-white/90">{currentSceneActTitle}</span>
              {currentSceneActIndex && currentSceneActCount ? (
                <span className="text-sky-100/65">
                  {currentSceneActIndex}/{currentSceneActCount}
                </span>
              ) : null}
            </div>
          ) : null}
          {limitedNpcChatRemainingTurns !== null ? (
            <div className="inline-flex items-center gap-2 rounded-full border border-rose-300/18 bg-rose-500/10 px-3 py-1 text-[10px] tracking-[0.16em] text-rose-100">
              <span>剩余交谈</span>
              <span className="text-white/90">
                {limitedNpcChatRemainingTurns} 轮
              </span>
            </div>
          ) : null}
        </div>
      )}
      {isDialogueStory ? (
        <div className="space-y-3">
          {dialogueTurns.length > 0 ? (
            dialogueTurns.map((turn, index) => (
              <div
                key={`${turn.speaker}-${turn.speakerName ?? 'default'}-${index}-${turn.text}`}
                className={`flex ${getDialogueTurnAlignmentClass(turn)}`}
              >
                <div
                  className={`max-w-[88%] border px-3 py-2 text-sm leading-relaxed ${getDialogueTurnBubbleClass(turn)} ${getDialogueTurnBubbleShapeClass(turn)}`}
                >
                  <div className="sr-only">
                    {turn.speaker === 'player' ? '你' : '对方'}
                  </div>
                  <div className="mb-1 text-[10px] tracking-[0.18em] text-zinc-400">
                    {getDialogueTurnLabel(turn)}
                  </div>
                  {turn.text}
                </div>
              </div>
            ))
          ) : isNpcChatMode && !isStoryStreaming ? (
            <div className="h-1" aria-hidden="true" />
          ) : (
            <div className="flex justify-start">
              <div className="rounded-2xl border border-white/10 bg-black/20 px-3 py-2 text-sm text-zinc-400">
                对话正在展开...
              </div>
            </div>
          )}
        </div>
      ) : (
        <p className="font-serif text-[15px] leading-7 text-zinc-200 sm:text-base">
          {currentStory.text}
        </p>
      )}
    </div>
  );
}

function RpgAdventureChoiceSection(props: {
  isNpcChatMode: boolean;
  canRefreshOptions: boolean;
  shouldHideChoiceUi: boolean;
  onRefreshOptions: () => void;
  onOpenCharacter: () => void;
  onOpenInventory: () => void;
  onExitNpcChat?: () => boolean;
  isLoading: boolean;
  isStoryStreaming: boolean;
  displayedOptions: StoryOption[];
  playerCharacter: Character;
  playerHp: number;
  playerMaxHp: number;
  playerMana: number;
  playerMaxMana: number;
  playerSkillCooldowns: Record<string, number>;
  currentNpcBattleMode: NpcBattleMode | null;
  hasDeferredAdventureOptions: boolean;
  handleOptionChoice: (option: StoryOption) => void;
  isNpcQuestOfferMode: boolean;
  npcChatDraft: string;
  setNpcChatDraft: (value: string) => void;
  npcChatPlaceholder: string;
  submitNpcChatDraft: () => void;
}) {
  const {
    isNpcChatMode,
    canRefreshOptions,
    shouldHideChoiceUi,
    onRefreshOptions,
    onOpenCharacter,
    onOpenInventory,
    onExitNpcChat,
    isLoading,
    isStoryStreaming,
    displayedOptions,
    playerCharacter,
    playerHp,
    playerMaxHp,
    playerMana,
    playerMaxMana,
    playerSkillCooldowns,
    currentNpcBattleMode,
    hasDeferredAdventureOptions,
    handleOptionChoice,
    isNpcQuestOfferMode,
    npcChatDraft,
    setNpcChatDraft,
    npcChatPlaceholder,
    submitNpcChatDraft,
  } = props;

  return (
    <div className="mt-auto shrink-0 pb-[calc(env(safe-area-inset-bottom,0px)+0.35rem)]">
      <div className="mb-1.5 flex flex-wrap items-center justify-between gap-2">
        <div className="flex min-w-0 flex-wrap items-center gap-2">
          <button
            type="button"
            onClick={onOpenCharacter}
            aria-label="打开队伍"
            className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-white/10 bg-black/20 px-2 text-zinc-300 transition-colors hover:text-white"
          >
            <PixelIcon src={TAB_ICONS.character.active} className="h-4 w-4" />
            <span className="text-xs leading-none">队伍</span>
          </button>
          <button
            type="button"
            onClick={onOpenInventory}
            aria-label="打开背包"
            className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-white/10 bg-black/20 px-2 text-zinc-300 transition-colors hover:text-white"
          >
            <PixelIcon src={TAB_ICONS.inventory.active} className="h-4 w-4" />
            <span className="text-xs leading-none">背包</span>
          </button>
        </div>

        <div className="flex shrink-0 items-center gap-2">
          {isNpcChatMode && canRefreshOptions && !shouldHideChoiceUi ? (
            <button
              type="button"
              onClick={onRefreshOptions}
              aria-label="换一换选项"
              className="inline-flex h-8 shrink-0 items-center gap-1.5 self-start rounded-md border border-white/10 bg-black/20 px-2 text-zinc-300 transition-colors hover:text-white"
            >
              <PixelIcon
                src={CHROME_ICONS.refreshOptions}
                className="h-4 w-4"
              />
              <span className="text-xs leading-none">换一换</span>
            </button>
          ) : !isNpcChatMode && canRefreshOptions && !shouldHideChoiceUi ? (
            <button
              type="button"
              onClick={onRefreshOptions}
              aria-label="换一换选项"
              className="inline-flex h-8 shrink-0 items-center gap-1.5 self-start rounded-md border border-white/10 bg-black/20 px-2 text-zinc-300 transition-colors hover:text-white"
            >
              <PixelIcon
                src={CHROME_ICONS.refreshOptions}
                className="h-4 w-4"
              />
              <span className="text-xs leading-none">换一换</span>
            </button>
          ) : null}
          {isNpcChatMode ? (
            <button
              type="button"
              onClick={() => onExitNpcChat?.()}
              aria-label="退出聊天"
              className="inline-flex h-8 shrink-0 items-center gap-1.5 self-start rounded-md border border-rose-300/20 bg-rose-500/10 px-2 text-rose-100 transition-colors hover:bg-rose-500/15"
            >
              <span className="text-xs leading-none">退出聊天</span>
            </button>
          ) : null}
        </div>
      </div>

      <div className="space-y-1.5">
        {isLoading && !isStoryStreaming ? (
          <div className="flex items-center justify-center space-x-2 p-4 text-zinc-600">
            <Loader2 className="h-4 w-4 animate-spin" />
            <span className="text-xs uppercase tracking-widest">
              剧情推演中...
            </span>
          </div>
        ) : isStoryStreaming ? (
          <div className="flex items-center justify-center p-4 text-[11px] tracking-[0.18em] text-zinc-500">
            对话进行中
          </div>
        ) : shouldHideChoiceUi ? (
          <div className="p-4" aria-hidden="true" />
        ) : (
          <>
            {displayedOptions.map((option, index) => {
              const optionImpactSummary = getOptionImpactSummary(
                option,
                playerCharacter,
                playerHp,
                playerMaxHp,
                playerMana,
                playerMaxMana,
                playerSkillCooldowns,
                currentNpcBattleMode,
              );
              const isDeferredContinueOption =
                hasDeferredAdventureOptions &&
                isContinueAdventureOption(option);
              const optionDisabled = option.disabled === true;

              if (isDeferredContinueOption) {
                return (
                  <motion.button
                    key={`${option.functionId}-${option.actionText}-${index}`}
                    type="button"
                    initial={{ opacity: 0, y: 22 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ duration: 0.24, ease: 'easeOut' }}
                    onClick={() => handleOptionChoice(option)}
                    className="pixel-nine-slice pixel-pressable pixel-choice-button group w-full text-left"
                    style={getNineSliceStyle(UI_CHROME.choiceButton)}
                  >
                    <div className="flex items-center justify-between gap-3">
                      <RpgOptionActionLabel option={option} />
                      <PixelIcon
                        src={CHROME_ICONS.optionArrow}
                        className="h-3 w-3 opacity-70 transition-opacity group-hover:opacity-100"
                      />
                    </div>
                    <div className="mt-1 text-[10px] leading-relaxed text-zinc-500">
                      剧情推理完成，继续后显示新的冒险选项
                    </div>
                  </motion.button>
                );
              }

              return (
                <button
                  key={`${option.functionId}-${option.actionText}-${index}`}
                  type="button"
                  onClick={() => handleOptionChoice(option)}
                  disabled={optionDisabled}
                  className={`pixel-nine-slice pixel-choice-button group w-full text-left ${optionDisabled ? 'cursor-not-allowed opacity-55' : 'pixel-pressable'}`}
                  style={getNineSliceStyle(UI_CHROME.choiceButton)}
                >
                  <div className="flex items-center justify-between gap-3">
                    <RpgOptionActionLabel option={option} />
                    <PixelIcon
                      src={CHROME_ICONS.optionArrow}
                      className="h-3 w-3 opacity-70 transition-opacity group-hover:opacity-100"
                    />
                  </div>
                  {!isNpcChatMode && option.goalAffordance?.label && (
                    <div
                      className={`mt-1 text-[10px] ${getOptionGoalAffordanceClass(option)}`}
                    >
                      {option.goalAffordance.label}
                    </div>
                  )}
                  {!isNpcChatMode && optionImpactSummary && !optionDisabled && (
                    <div className="mt-1 text-[10px] text-zinc-500">
                      {optionImpactSummary}
                    </div>
                  )}
                </button>
              );
            })}
            {isNpcChatMode && !isNpcQuestOfferMode ? (
              <div className="pixel-nine-slice pixel-panel border border-white/10 bg-black/25 p-1.5">
                <div className="flex min-w-0 items-center gap-2">
                  <input
                    value={npcChatDraft}
                    onChange={(event) => setNpcChatDraft(event.target.value)}
                    onKeyDown={(event) => {
                      if (
                        event.key === 'Enter' &&
                        !event.nativeEvent.isComposing
                      ) {
                        event.preventDefault();
                        submitNpcChatDraft();
                      }
                    }}
                    placeholder={npcChatPlaceholder}
                    className="h-9 min-w-0 flex-1 rounded-md border border-white/10 bg-black/35 px-3 text-sm text-zinc-100 outline-none placeholder:text-zinc-500 focus:border-amber-200/40"
                    maxLength={80}
                    disabled={isLoading}
                  />
                  <button
                    type="button"
                    onClick={submitNpcChatDraft}
                    disabled={isLoading || !npcChatDraft.trim()}
                    className="inline-flex h-9 shrink-0 items-center rounded-md border border-amber-300/20 bg-amber-500/10 px-2.5 text-[11px] text-amber-100 transition-colors disabled:cursor-not-allowed disabled:opacity-40 sm:px-3 sm:text-xs"
                  >
                    发送
                  </button>
                </div>
              </div>
            ) : null}
          </>
        )}
      </div>
    </div>
  );
}

function RpgAdventureOverlaySection(props: {
  shouldMountAdventureOverlays: boolean;
  worldType: WorldType | null;
  quests: QuestLogEntry[];
  questUi: QuestFlowUi;
  battleRewardUi: BattleRewardUi;
  statistics: RpgAdventurePanelProps['statistics'];
  statisticsCards: Array<{
    key: string;
    label: string;
    value: string;
    detail: string;
    icon: typeof Clock3;
  }>;
  musicVolume: number;
  onMusicVolumeChange: (value: number) => void;
  onSaveAndExit: () => void;
  saveAndExitDisabled: boolean;
  isGoalPanelOpen: boolean;
  setIsGoalPanelOpen: (value: boolean) => void;
  isQuestPanelOpen: boolean;
  setIsQuestPanelOpen: (value: boolean) => void;
  isSettingsPanelOpen: boolean;
  setIsSettingsPanelOpen: (value: boolean) => void;
  isStatsPanelOpen: boolean;
  setIsStatsPanelOpen: (value: boolean) => void;
  chapterState: ChapterState | null;
  journeyBeat: JourneyBeat | null;
  goalStack: GoalStackState;
  goalPulse: GoalPulseEvent | null;
  onDismissGoalPulse: () => void;
  selectedQuest: QuestLogEntry | null;
  setSelectedQuestId: (questId: string | null) => void;
  completionNoticeQuest: QuestLogEntry | null;
  setCompletionNoticeQuestId: (questId: string | null) => void;
  rewardQuest: QuestLogEntry | null;
  setRewardQuestId: (questId: string | null) => void;
  rewardQuestHandoff: GoalHandoff | null;
  setRewardQuestHandoff: (handoff: GoalHandoff | null) => void;
  selectedRewardItemQuestId: string | null;
  setSelectedRewardItemQuestId: (questId: string | null) => void;
  selectedRewardItemId: string | null;
  setSelectedRewardItemId: (itemId: string | null) => void;
  selectedBattleRewardItemId: string | null;
  setSelectedBattleRewardItemId: (itemId: string | null) => void;
  selectedRewardItem: InventoryItem | null;
  selectedRewardUseEffect: ReturnType<typeof resolveInventoryItemUseEffect> | null;
  selectedRewardEquipSlot: ReturnType<typeof getEquipmentSlotFromItem> | null;
  selectedQuestSceneName: string;
  getQuestStatusLabel: (status: QuestLogEntry['status']) => string;
  pendingNpcQuestOffer: QuestLogEntry | null;
  onAcceptPendingNpcQuestOffer: () => string | null;
}) {
  if (!props.shouldMountAdventureOverlays) {
    return null;
  }

  return (
      <Suspense fallback={<AdventurePanelOverlayLoadingFallback />}>
        <RpgAdventurePanelOverlays
        worldType={props.worldType}
        quests={props.quests}
        questUi={props.questUi}
        battleRewardUi={props.battleRewardUi}
        statistics={props.statistics}
        statisticsCards={props.statisticsCards}
        musicVolume={props.musicVolume}
        onMusicVolumeChange={props.onMusicVolumeChange}
        onSaveAndExit={props.onSaveAndExit}
        saveAndExitDisabled={props.saveAndExitDisabled}
        isGoalPanelOpen={props.isGoalPanelOpen}
        setIsGoalPanelOpen={props.setIsGoalPanelOpen}
        isQuestPanelOpen={props.isQuestPanelOpen}
        setIsQuestPanelOpen={props.setIsQuestPanelOpen}
        isSettingsPanelOpen={props.isSettingsPanelOpen}
        setIsSettingsPanelOpen={props.setIsSettingsPanelOpen}
        isStatsPanelOpen={props.isStatsPanelOpen}
        setIsStatsPanelOpen={props.setIsStatsPanelOpen}
        chapterState={props.chapterState}
        journeyBeat={props.journeyBeat}
        goalStack={props.goalStack}
        goalPulse={props.goalPulse}
        onDismissGoalPulse={props.onDismissGoalPulse}
        selectedQuest={props.selectedQuest}
        setSelectedQuestId={props.setSelectedQuestId}
        completionNoticeQuest={props.completionNoticeQuest}
        setCompletionNoticeQuestId={props.setCompletionNoticeQuestId}
        rewardQuest={props.rewardQuest}
        setRewardQuestId={props.setRewardQuestId}
        rewardQuestHandoff={props.rewardQuestHandoff}
        setRewardQuestHandoff={props.setRewardQuestHandoff}
        selectedRewardItemQuestId={props.selectedRewardItemQuestId}
        setSelectedRewardItemQuestId={props.setSelectedRewardItemQuestId}
        selectedRewardItemId={props.selectedRewardItemId}
        setSelectedRewardItemId={props.setSelectedRewardItemId}
        selectedBattleRewardItemId={props.selectedBattleRewardItemId}
        setSelectedBattleRewardItemId={props.setSelectedBattleRewardItemId}
        selectedRewardItem={props.selectedRewardItem}
        selectedRewardUseEffect={props.selectedRewardUseEffect}
        selectedRewardEquipSlot={props.selectedRewardEquipSlot}
        selectedQuestSceneName={props.selectedQuestSceneName}
        getQuestStatusLabel={props.getQuestStatusLabel}
        pendingNpcQuestOffer={props.pendingNpcQuestOffer}
        onAcceptPendingNpcQuestOffer={props.onAcceptPendingNpcQuestOffer}
      />
    </Suspense>
  );
}

const LEGACY_ADVENTURE_PANEL_HELPERS = {
  getQuestRewardItemIcon,
  getRewardItemFrameClass,
  buildRewardItemDescription,
  RewardItemIconGrid,
  getQuestObjectivePresentation,
  QuestProgressPips,
  QuestRewardGrid,
  QuestObjectiveCard,
};

void LEGACY_ADVENTURE_PANEL_HELPERS;

/**
 * RPG 冒险主面板。
 * 维持原有视觉与交互，只把真实实现迁到 `rpg-runtime-panels`，
 * 并把 story / choice / overlay 三个主 section 显式拆开。
 */
export function RpgAdventurePanel({
  aiError,
  currentStory,
  isLoading,
  displayedOptions,
  hideOptions,
  canRefreshOptions,
  onRefreshOptions,
  onChoice,
  onSubmitNpcChatInput,
  onExitNpcChat,
  onOpenCharacter,
  onOpenInventory,
  playerCharacter,
  worldType,
  quests,
  questUi,
  npcChatQuestOfferUi,
  goalStack,
  goalPulse,
  onDismissGoalPulse,
  battleRewardUi,
  playerHp,
  playerMaxHp,
  playerMana,
  playerMaxMana,
  playerSkillCooldowns,
  currentNpcBattleMode,
  statistics,
  musicVolume,
  onMusicVolumeChange,
  onSaveAndExit,
  chapterState = null,
  journeyBeat = null,
  currentSceneActTitle = null,
  currentSceneActIndex = null,
  currentSceneActCount = null,
}: RpgAdventurePanelProps) {
  const isDialogueStory = currentStory.displayMode === 'dialogue';
  const dialogueTurns = currentStory.dialogue ?? [];
  const npcChatState = currentStory.npcChatState ?? null;
  const isNpcChatMode = Boolean(npcChatState);
  const pendingNpcQuestOffer = npcChatState?.pendingQuestOffer?.quest ?? null;
  const isNpcQuestOfferMode = Boolean(pendingNpcQuestOffer);
  const isStoryStreaming = Boolean(currentStory.streaming);
  const shouldHideChoiceUi = hideOptions;
  const storyScrollContainerRef = useRef<HTMLDivElement | null>(null);
  const hasDeferredAdventureOptions = Boolean(
    currentStory.deferredOptions?.length,
  );
  const saveAndExitDisabled = isLoading || isStoryStreaming;
  const primaryQuestGoal =
    goalStack.activeGoal?.sourceKind === 'quest'
      ? goalStack.activeGoal
      : goalStack.immediateStepGoal?.sourceKind === 'quest'
        ? goalStack.immediateStepGoal
        : null;
  const [isGoalPanelOpen, setIsGoalPanelOpen] = useState(false);
  const [isQuestPanelOpen, setIsQuestPanelOpen] = useState(false);
  const [isSettingsPanelOpen, setIsSettingsPanelOpen] = useState(false);
  const [isStatsPanelOpen, setIsStatsPanelOpen] = useState(false);
  const [selectedQuestId, setSelectedQuestId] = useState<string | null>(null);
  const [pendingAcceptedQuestId, setPendingAcceptedQuestId] = useState<
    string | null
  >(null);
  const [completionNoticeQuestId, setCompletionNoticeQuestId] = useState<
    string | null
  >(null);
  const [rewardQuestId, setRewardQuestId] = useState<string | null>(null);
  const [rewardQuestHandoff, setRewardQuestHandoff] =
    useState<GoalHandoff | null>(null);
  const [selectedRewardItemQuestId, setSelectedRewardItemQuestId] = useState<
    string | null
  >(null);
  const [selectedRewardItemId, setSelectedRewardItemId] = useState<
    string | null
  >(null);
  const [selectedBattleRewardItemId, setSelectedBattleRewardItemId] = useState<
    string | null
  >(null);
  const [npcChatDraft, setNpcChatDraft] = useState('');
  const lastAutoOpenedGoalRef = useRef<string | null>(null);
  const lastAutoOpenedPulseRef = useRef<string | null>(null);
  const battleReward = battleRewardUi.reward;
  const hasCompletedQuest = useMemo(
    () => quests.some((quest) => isQuestReadyToClaim(quest)),
    [quests],
  );
  const selectedQuest = useMemo(
    () =>
      quests.find((quest) => quest.id === selectedQuestId) ??
      (pendingNpcQuestOffer?.id === selectedQuestId
        ? pendingNpcQuestOffer
        : null),
    [pendingNpcQuestOffer, quests, selectedQuestId],
  );
  const rewardQuest = useMemo(
    () => quests.find((quest) => quest.id === rewardQuestId) ?? null,
    [quests, rewardQuestId],
  );
  const selectedRewardItemQuest = useMemo(
    () =>
      quests.find((quest) => quest.id === selectedRewardItemQuestId) ?? null,
    [quests, selectedRewardItemQuestId],
  );
  const selectedBattleRewardItem = useMemo(
    () =>
      battleReward?.items.find(
        (item) => item.id === selectedBattleRewardItemId,
      ) ?? null,
    [battleReward, selectedBattleRewardItemId],
  );
  const selectedRewardItem = useMemo(
    () =>
      selectedRewardItemQuest?.reward.items.find(
        (item) => item.id === selectedRewardItemId,
      ) ?? selectedBattleRewardItem,
    [selectedBattleRewardItem, selectedRewardItemId, selectedRewardItemQuest],
  );

  const getQuestStatusLabel = (status: QuestLogEntry['status']) => {
    if (status === 'ready_to_turn_in' || status === 'completed')
      return '待交付';
    if (status === 'turned_in') return '已交付';
    if (status === 'discovered') return '已发现';
    if (status === 'failed') return '已失败';
    if (status === 'expired') return '已过期';
    return '进行中';
  };

  useEffect(() => {
    if (completionNoticeQuestId) return;

    const pendingNoticeQuest = quests.find(
      (quest) => isQuestReadyToClaim(quest) && !quest.completionNotified,
    );

    if (pendingNoticeQuest) {
      setCompletionNoticeQuestId(pendingNoticeQuest.id);
    }
  }, [completionNoticeQuestId, quests]);

  useEffect(() => {
    if (!pendingAcceptedQuestId) return;

    const acceptedQuest = quests.find(
      (quest) => quest.id === pendingAcceptedQuestId,
    );
    if (!acceptedQuest) return;

    setIsQuestPanelOpen(false);
    setSelectedQuestId(acceptedQuest.id);
    setSelectedRewardItemQuestId(null);
    setSelectedRewardItemId(null);
    setSelectedBattleRewardItemId(null);
    setPendingAcceptedQuestId(null);
  }, [pendingAcceptedQuestId, quests]);

  useEffect(() => {
    if (battleReward) return;
    setSelectedBattleRewardItemId(null);
  }, [battleReward]);

  useEffect(() => {
    setNpcChatDraft('');
  }, [npcChatState?.npcId, npcChatState?.turnCount]);

  useEffect(() => {
    if (!primaryQuestGoal) {
      return;
    }

    if (lastAutoOpenedGoalRef.current === primaryQuestGoal.id) {
      return;
    }

    lastAutoOpenedGoalRef.current = primaryQuestGoal.id;
    setIsGoalPanelOpen(true);
  }, [primaryQuestGoal]);

  useEffect(() => {
    if (!goalPulse) {
      return;
    }

    if (lastAutoOpenedPulseRef.current === goalPulse.id) {
      return;
    }

    lastAutoOpenedPulseRef.current = goalPulse.id;
    setIsGoalPanelOpen(true);
  }, [goalPulse]);

  useEffect(() => {
    const container = storyScrollContainerRef.current;
    if (!container) return;

    const animationFrameId = window.requestAnimationFrame(() => {
      container.scrollTo({
        top: container.scrollHeight,
        behavior: 'auto',
      });
    });

    return () => window.cancelAnimationFrame(animationFrameId);
  }, [
    currentStory.displayMode,
    currentStory.text,
    dialogueTurns.length,
    displayedOptions.length,
    isLoading,
    isStoryStreaming,
  ]);

  const completionNoticeQuest = completionNoticeQuestId
    ? (quests.find((quest) => quest.id === completionNoticeQuestId) ?? null)
    : null;
  const selectedRewardUseEffect = selectedRewardItem
    ? resolveInventoryItemUseEffect(selectedRewardItem, playerCharacter)
    : null;
  const selectedRewardEquipSlot = selectedRewardItem
    ? getEquipmentSlotFromItem(selectedRewardItem)
    : null;
  const selectedQuestSceneName = useMemo(() => {
    if (!selectedQuest) return '当前区域';
    if (!worldType) return selectedQuest.sceneId ?? '当前区域';
    return (
      getScenePresetById(worldType, selectedQuest.sceneId)?.name ??
      selectedQuest.sceneId ??
      '当前区域'
    );
  }, [selectedQuest, worldType]);
  const statisticsCards = useMemo(
    () => [
      {
        key: 'play-time',
        label: '游戏时长',
        value: formatPlayTime(statistics.playTimeMs),
        detail: '累计冒险时间',
        icon: Clock3,
      },
      {
        key: 'hostile-npc-defeats',
        label: '击杀怪物',
        value: `${statistics.hostileNpcsDefeated}`,
        detail: '累计战斗击杀',
        icon: Skull,
      },
      {
        key: 'quests-completed',
        label: '完成任务',
        value: `${statistics.questsCompleted}`,
        detail: `已接 ${statistics.questsAccepted} / 已交 ${statistics.questsTurnedIn}`,
        icon: ScrollText,
      },
      {
        key: 'travel-count',
        label: '场景移动',
        value: `${statistics.scenesTraveled}`,
        detail: statistics.currentSceneName,
        icon: Footprints,
      },
      {
        key: 'currency',
        label: '当前货币',
        value: `${statistics.playerCurrency}`,
        detail: '可用于交易与奖励结算',
        icon: Coins,
      },
      {
        key: 'inventory',
        label: '背包物品',
        value: `${statistics.inventoryItemCount}`,
        detail: `${statistics.inventoryStackCount} 组物品 / 使用 ${statistics.itemsUsed} 次`,
        icon: Backpack,
      },
      {
        key: 'companions',
        label: '同伴人数',
        value: `${statistics.activeCompanionCount + statistics.rosterCompanionCount}`,
        detail: `出战 ${statistics.activeCompanionCount} / 营地 ${statistics.rosterCompanionCount}`,
        icon: Users,
      },
      {
        key: 'scene',
        label: '当前区域',
        value: statistics.currentSceneName,
        detail: '本次冒险所在地',
        icon: MapPinned,
      },
    ],
    [statistics],
  );
  const limitedNpcChatRemainingTurns =
    npcChatState?.turnLimit && npcChatState.limitReason === 'negative_affinity'
      ? Math.max(0, npcChatState.remainingTurns ?? 0)
      : null;
  const shouldMountAdventureOverlays =
    isGoalPanelOpen ||
    isSettingsPanelOpen ||
    isStatsPanelOpen ||
    isQuestPanelOpen ||
    Boolean(selectedQuest) ||
    Boolean(completionNoticeQuest) ||
    Boolean(rewardQuest) ||
    Boolean(battleReward) ||
    Boolean(selectedRewardItem);

  const handleOptionChoice = (option: StoryOption) => {
    const pendingQuestAction =
      typeof option.runtimePayload?.npcChatQuestOfferAction === 'string'
        ? option.runtimePayload.npcChatQuestOfferAction
        : null;
    if (pendingQuestAction && pendingNpcQuestOffer) {
      if (pendingQuestAction === 'view') {
        setSelectedQuestId(pendingNpcQuestOffer.id);
        return;
      }

      if (pendingQuestAction === 'replace') {
        void npcChatQuestOfferUi.replacePendingOffer();
        return;
      }

      if (pendingQuestAction === 'abandon') {
        npcChatQuestOfferUi.abandonPendingOffer();
        return;
      }
    }

    if (
      option.interaction?.kind === 'npc' &&
      option.interaction.action === 'quest_accept'
    ) {
      setPendingAcceptedQuestId(option.interaction.questId ?? null);
    }

    onChoice(option);
  };

  const handleDismissGoalPanel = () => {
    setIsGoalPanelOpen(false);
    onDismissGoalPulse();
  };

  const submitNpcChatDraft = () => {
    const nextInput = npcChatDraft.trim();
    if (!nextInput || !onSubmitNpcChatInput) {
      return;
    }

    const submitted = onSubmitNpcChatInput(nextInput);
    if (submitted) {
      setNpcChatDraft('');
    }
  };

  return (
    <div className="relative flex min-h-0 flex-1 flex-col">
      <button
        type="button"
        onClick={() => setIsSettingsPanelOpen(true)}
        aria-label="打开设置"
        title="打开设置"
        className="fixed z-[26] flex min-h-[2.75rem] min-w-[2.75rem] items-center justify-center border-0 bg-transparent p-0 shadow-none transition duration-150 hover:-translate-y-px hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-200/60"
        style={{
          top: 'calc(env(safe-area-inset-top, 0px) + 0.75rem)',
          right: 'calc(clamp(0.75rem, 2vw, 1rem) - 5px)',
        }}
      >
        <PixelIcon
          src={CHROME_ICONS.settings}
          className="h-[1.8rem] w-[1.8rem] drop-shadow-[0_4px_10px_rgba(0,0,0,0.45)]"
        />
      </button>
      <button
        type="button"
        onClick={() => {
          setIsQuestPanelOpen(true);
          onDismissGoalPulse();
        }}
        className="fixed right-0 z-[26] flex min-w-[3.1rem] flex-col items-center gap-1 rounded-l-xl border border-r-0 border-white/10 bg-black/78 pl-2 pr-1.5 py-2 text-[10px] text-zinc-200 shadow-[0_8px_18px_rgba(0,0,0,0.35)] backdrop-blur-md transition hover:text-white"
        style={{ top: 'calc(env(safe-area-inset-top, 0px) + 7vh)' }}
      >
        {(hasCompletedQuest || goalPulse) && (
          <span
            aria-hidden="true"
            className={`absolute -left-1 top-1 h-3.5 w-3.5 rounded-full border shadow-[0_0_14px_rgba(245,158,11,0.5)] ${
              hasCompletedQuest
                ? 'border-red-200/45 bg-red-500 shadow-[0_0_14px_rgba(239,68,68,0.55)]'
                : 'border-amber-200/45 bg-amber-500'
            }`}
          />
        )}
        <PixelIcon src={CHROME_ICONS.map} className="h-4 w-4" />
        <span className="leading-none">任务</span>
        {primaryQuestGoal?.title ? (
          <span className="max-w-[3.6rem] truncate text-[9px] text-zinc-400">
            {primaryQuestGoal.title}
          </span>
        ) : (
          <span className="rounded-full border border-white/10 bg-white/10 px-1.5 py-0.5 text-[9px] text-white">
            {quests.length}
          </span>
        )}
      </button>

      {aiError && (
        <div
          className="pixel-nine-slice pixel-panel mb-3 text-xs text-amber-100"
          style={getNineSliceStyle(UI_CHROME.panel)}
        >
          {aiError}
        </div>
      )}

      <RpgAdventureStorySection
        currentSceneActTitle={currentSceneActTitle}
        currentSceneActIndex={currentSceneActIndex}
        currentSceneActCount={currentSceneActCount}
        limitedNpcChatRemainingTurns={limitedNpcChatRemainingTurns}
        storyScrollContainerRef={storyScrollContainerRef}
        isDialogueStory={isDialogueStory}
        dialogueTurns={dialogueTurns}
        isNpcChatMode={isNpcChatMode}
        isStoryStreaming={isStoryStreaming}
        currentStory={currentStory}
      />

      <RpgAdventureChoiceSection
        isNpcChatMode={isNpcChatMode}
        canRefreshOptions={canRefreshOptions}
        shouldHideChoiceUi={shouldHideChoiceUi}
        onRefreshOptions={onRefreshOptions}
        onOpenCharacter={onOpenCharacter}
        onOpenInventory={onOpenInventory}
        onExitNpcChat={onExitNpcChat}
        isLoading={isLoading}
        isStoryStreaming={isStoryStreaming}
        displayedOptions={displayedOptions}
        playerCharacter={playerCharacter}
        playerHp={playerHp}
        playerMaxHp={playerMaxHp}
        playerMana={playerMana}
        playerMaxMana={playerMaxMana}
        playerSkillCooldowns={playerSkillCooldowns}
        currentNpcBattleMode={currentNpcBattleMode}
        hasDeferredAdventureOptions={hasDeferredAdventureOptions}
        handleOptionChoice={handleOptionChoice}
        isNpcQuestOfferMode={isNpcQuestOfferMode}
        npcChatDraft={npcChatDraft}
        setNpcChatDraft={setNpcChatDraft}
        npcChatPlaceholder={
          npcChatState?.customInputPlaceholder ?? '输入你想说的话'
        }
        submitNpcChatDraft={submitNpcChatDraft}
      />

      <RpgAdventureOverlaySection
        shouldMountAdventureOverlays={shouldMountAdventureOverlays}
        worldType={worldType}
        quests={quests}
        questUi={questUi}
        battleRewardUi={battleRewardUi}
        statistics={statistics}
        statisticsCards={statisticsCards}
        musicVolume={musicVolume}
        onMusicVolumeChange={onMusicVolumeChange}
        onSaveAndExit={onSaveAndExit}
        saveAndExitDisabled={saveAndExitDisabled}
        isGoalPanelOpen={isGoalPanelOpen}
        setIsGoalPanelOpen={setIsGoalPanelOpen}
        isQuestPanelOpen={isQuestPanelOpen}
        setIsQuestPanelOpen={setIsQuestPanelOpen}
        isSettingsPanelOpen={isSettingsPanelOpen}
        setIsSettingsPanelOpen={setIsSettingsPanelOpen}
        isStatsPanelOpen={isStatsPanelOpen}
        setIsStatsPanelOpen={setIsStatsPanelOpen}
        chapterState={chapterState}
        journeyBeat={journeyBeat}
        goalStack={goalStack}
        goalPulse={goalPulse}
        onDismissGoalPulse={handleDismissGoalPanel}
        selectedQuest={selectedQuest}
        setSelectedQuestId={setSelectedQuestId}
        completionNoticeQuest={completionNoticeQuest}
        setCompletionNoticeQuestId={setCompletionNoticeQuestId}
        rewardQuest={rewardQuest}
        setRewardQuestId={setRewardQuestId}
        rewardQuestHandoff={rewardQuestHandoff}
        setRewardQuestHandoff={setRewardQuestHandoff}
        selectedRewardItemQuestId={selectedRewardItemQuestId}
        setSelectedRewardItemQuestId={setSelectedRewardItemQuestId}
        selectedRewardItemId={selectedRewardItemId}
        setSelectedRewardItemId={setSelectedRewardItemId}
        selectedBattleRewardItemId={selectedBattleRewardItemId}
        setSelectedBattleRewardItemId={setSelectedBattleRewardItemId}
        selectedRewardItem={selectedRewardItem}
        selectedRewardUseEffect={selectedRewardUseEffect}
        selectedRewardEquipSlot={selectedRewardEquipSlot}
        selectedQuestSceneName={selectedQuestSceneName}
        getQuestStatusLabel={getQuestStatusLabel}
        pendingNpcQuestOffer={pendingNpcQuestOffer}
        onAcceptPendingNpcQuestOffer={() => {
          const acceptedQuestId = npcChatQuestOfferUi.acceptPendingOffer();
          if (!acceptedQuestId) return null;
          setSelectedQuestId(null);
          return acceptedQuestId;
        }}
      />
    </div>
  );
}

export default RpgAdventurePanel;
