import { AnimatePresence, motion } from 'motion/react'; import { type ReactNode, 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 { PlatformDarkOptionCard } from './common/PlatformDarkOptionCard'; import { PlatformEmptyState } from './common/PlatformEmptyState'; import { PlatformPillBadge } from './common/PlatformPillBadge'; import { PlatformStatusMessage } from './common/PlatformStatusMessage'; import { PlatformSubpanel } from './common/PlatformSubpanel'; 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 NpcModalEmptyState({ children }: { children: ReactNode }) { return ( {children} ); } 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 (
{item.name ?? item.id}
{item.category} / {getRarityLabel(item.rarity)} / 单价 {unitPrice}{' '} {currencyName}
x{item.quantity}
); } 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} )}
npcUi.setTradeMode('buy')} className="text-center text-sm" > 购买物品 npcUi.setTradeMode('sell')} className="text-center text-sm" > 出售物品
{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.selectGiftItem(candidate.itemId)} className="w-full" >
{candidate.item.name}
{candidate.item.category} /{' '} {getRarityLabel(candidate.item.rarity)}
{!candidate.canSubmit && candidate.reason && (
{candidate.reason}
)}
好感 +{candidate.affinityGain}
)) ) : ( 当前没有适合送出的礼物。 )}
)} {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 ( npcUi.selectRecruitRelease(companion.npcId) } className="w-full" >
{character.name}
{character.title}
); }) ) : ( 当前没有可替换的同行角色。 )}
)}
); }