import { AnimatePresence, motion } from 'motion/react'; import { useState } from 'react'; import { getCharacterById } from '../data/characterPresets'; import { formatCurrency, getInventoryItemValue, } from '../data/economy'; import { getEquipmentSlotFromItem, getEquipmentSlotLabel, } from '../data/equipmentEffects'; import { isInventoryItemUsable, resolveInventoryItemUseEffect } from '../data/inventoryEffects'; import { buildInventoryItemDescription, getInventoryTagLabels, } from '../data/itemPresentation'; import { getRarityLabel, } from '../data/npcInteractions'; import { StoryGenerationNpcUi } from '../hooks/rpg-runtime-story'; import { GameState, InventoryItem, RuntimeNpcGiftItemView, RuntimeNpcTradeItemView, } from '../types'; import { getInventoryItemVisualSrc, getNineSliceStyle, UI_CHROME } from '../uiAssets'; import { PixelCloseButton } from './PixelCloseButton'; import { PixelIcon } from './PixelIcon'; interface NpcModalsProps { gameState: GameState; npcUi: StoryGenerationNpcUi; } type TradeDetailState = { itemId: string; source: 'buy' | 'sell'; } | null; function getItemVisualSrc(item: InventoryItem) { return getInventoryItemVisualSrc(item); } function buildTradeUseEffectText( effect: ReturnType | null, ) { if (!effect) return null; const parts = [ effect.hpRestore > 0 ? `生命 +${effect.hpRestore}` : null, effect.manaRestore > 0 ? `灵力 +${effect.manaRestore}` : null, effect.cooldownReduction > 0 ? `冷却 -${effect.cooldownReduction}` : null, ].filter((part): part is string => Boolean(part)); return parts.join(' / ') || '无直接效果'; } function TradeItemRow({ item, selected, unitPrice, currencyName, onClick, }: { item: InventoryItem; selected: boolean; unitPrice: number; currencyName: string; onClick: () => void; }) { return ( ); } function TradeQuantityStepper({ quantity, maxQuantity, onChange, }: { quantity: number; maxQuantity: number; onChange: (quantity: number) => void; }) { const safeMax = Math.max(1, maxQuantity); return (
数量
最多 {safeMax}
{quantity}
); } export function NpcModals({ gameState, npcUi }: NpcModalsProps) { const [tradeDetail, setTradeDetail] = useState(null); const npcInteraction = gameState.runtimeNpcInteraction ?? null; const currencyName = npcInteraction?.currencyName ?? '钱币'; const tradeModal = npcUi.tradeModal; const tradeMode = tradeModal?.mode ?? 'buy'; const tradeItemViews: RuntimeNpcTradeItemView[] = tradeMode === 'buy' ? npcInteraction?.trade.buyItems ?? [] : npcInteraction?.trade.sellItems ?? []; const activeTradeView = tradeModal ? tradeItemViews.find(view => view.itemId === (tradeMode === 'buy' ? tradeModal.selectedNpcItemId : tradeModal.selectedPlayerItemId), ) ?? null : null; const activeTradeItem = activeTradeView?.item ?? null; const activeTradeUnitPrice = activeTradeView?.unitPrice ?? 0; const activeTradeMaxQuantity = activeTradeView?.maxQuantity ?? 0; const activeTradeQuantity = tradeModal ? Math.max(1, Math.min(tradeModal.selectedQuantity, Math.max(1, activeTradeMaxQuantity))) : 1; const activeTradeTotalPrice = activeTradeUnitPrice * activeTradeQuantity; const canConfirmTrade = Boolean( activeTradeView && activeTradeView.canSubmit && activeTradeQuantity >= 1, ); const tradeItemList = tradeItemViews; const tradeDetailItem = tradeDetail ? (tradeDetail.source === 'buy' ? npcInteraction?.trade.buyItems ?? [] : npcInteraction?.trade.sellItems ?? []) .find(view => view.itemId === tradeDetail.itemId)?.item ?? null : null; const tradeDetailView = tradeDetail ? (tradeDetail.source === 'buy' ? npcInteraction?.trade.buyItems ?? [] : npcInteraction?.trade.sellItems ?? []) .find(view => view.itemId === tradeDetail.itemId) ?? null : null; const tradeDetailUseEffect = tradeDetailItem && gameState.playerCharacter ? resolveInventoryItemUseEffect(tradeDetailItem, gameState.playerCharacter) : null; const tradeDetailEquipSlot = tradeDetailItem ? getEquipmentSlotFromItem(tradeDetailItem) : null; const tradeDetailEffectText = buildTradeUseEffectText(tradeDetailUseEffect); const giftCandidates: RuntimeNpcGiftItemView[] = npcUi.giftModal ? npcInteraction?.gift.items ?? [] : []; const activeGiftView = giftCandidates.find(item => item.itemId === npcUi.giftModal?.selectedItemId) ?? null; const handleTradeItemClick = (view: RuntimeNpcTradeItemView) => { if (tradeMode === 'buy') { npcUi.selectTradeNpcItem(view.itemId); setTradeDetail({ itemId: view.itemId, source: 'buy' }); return; } npcUi.selectTradePlayerItem(view.itemId); setTradeDetail({ itemId: view.itemId, source: 'sell' }); }; return ( {tradeModal && ( event.stopPropagation()} >
交易
{npcInteraction?.npcName ?? tradeModal.encounter.npcName} / 你当前{currencyName}:{npcInteraction?.playerCurrency ?? gameState.playerCurrency}
{tradeModal.introText && (
{tradeModal.introText}
)}
{tradeMode === 'buy' ? '对方库存' : '你的背包'} {tradeItemList.length} 件
{tradeItemList.length > 0 ? tradeItemList.map(view => (
handleTradeItemClick(view)} /> {!view.canSubmit && view.reason && (
{view.reason}
)}
)) : (
{tradeMode === 'buy' ? '对方暂时没有可出售的物品。' : '你当前没有可出售的物品。'}
)}
{activeTradeItem ? (
{tradeMode === 'buy' ? '购买总价' : '出售总价'} {formatCurrency(activeTradeTotalPrice, gameState.worldType)}
{!activeTradeView?.canSubmit && activeTradeView?.reason && (
{activeTradeView.reason}
)}
) : (
请选择一件物品,右侧会显示数量、价格与详情。
)}
)} {tradeModal && tradeDetail && tradeDetailItem && ( setTradeDetail(null)} > event.stopPropagation()} >
物品详情
{tradeDetail.source === 'buy' ? '对方物品' : '背包物品'}
setTradeDetail(null)} label="关闭物品详情" placement="inline" />
{tradeDetailItem.name}
{tradeDetailItem.category} / {getRarityLabel(tradeDetailItem.rarity)}
库存: {tradeDetailItem.quantity}
估值: {getInventoryItemValue(tradeDetailItem)}
{tradeDetail.source === 'buy' ? `购买价格: ${formatCurrency(tradeDetailView?.unitPrice ?? 0, gameState.worldType)}` : `回收价格: ${formatCurrency(tradeDetailView?.unitPrice ?? 0, gameState.worldType)}`}

{buildInventoryItemDescription(tradeDetailItem, tradeDetailUseEffect)}

{tradeDetailEquipSlot ? `装备位:${getEquipmentSlotLabel(tradeDetailEquipSlot)}` : '不可装备'}
{isInventoryItemUsable(tradeDetailItem) ? '可立即使用' : '不可即时使用'}
标签:{getInventoryTagLabels(tradeDetailItem.tags).join(' / ') || '无'}
{tradeDetailEffectText && (
使用效果:{tradeDetailEffectText}
)}
)} {npcUi.giftModal && ( event.stopPropagation()} >
赠送礼物
{npcUi.giftModal.encounter.npcName}
{npcUi.giftModal.introText && (
{npcUi.giftModal.introText}
)} {giftCandidates.length > 0 ? giftCandidates.map(candidate => ( )) : (
当前没有适合送出的礼物。
)}
)} {npcUi.recruitModal && ( event.stopPropagation()} >
调整同行位置
队伍已满,请先选择一名当前同行角色离队,再邀请对方加入。
{npcUi.recruitModal.introText && (
{npcUi.recruitModal.introText}
)} {gameState.companions.length > 0 ? gameState.companions.map(companion => { const character = getCharacterById(companion.characterId); if (!character) return null; return ( ); }) : (
当前没有可替换的同行角色。
)}
)}
); }