import { AnimatePresence, motion } from 'motion/react'; import { useState } from 'react'; import { getCharacterById } from '../data/characterPresets'; import { formatCurrency, getCurrencyName, getInventoryItemValue, getNpcBuybackPrice, getNpcPurchasePrice, } from '../data/economy'; import { getEquipmentSlotFromItem, getEquipmentSlotLabel, } from '../data/equipmentEffects'; import { isInventoryItemUsable, resolveInventoryItemUseEffect } from '../data/inventoryEffects'; import { buildInventoryItemDescription, getInventoryTagLabels, } from '../data/itemPresentation'; import { buildInitialNpcState, getGiftCandidates, getRarityLabel, } from '../data/npcInteractions'; import { StoryGenerationNpcUi } from '../hooks/rpg-runtime-story'; import { GameState, InventoryItem } from '../types'; import { CHROME_ICONS, getInventoryItemVisualSrc, getNineSliceStyle, UI_CHROME } from '../uiAssets'; import { PixelIcon } from './PixelIcon'; interface NpcModalsProps { gameState: GameState; npcUi: StoryGenerationNpcUi; } type TradeDetailState = { itemId: string; source: 'buy' | 'sell'; } | null; function getNpcEncounterKey(encounter: NonNullable) { return encounter.id ?? encounter.npcName; } 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 currencyName = getCurrencyName( gameState.worldType, gameState.customWorldProfile, ); const tradeModal = npcUi.tradeModal; const tradeNpcState = tradeModal ? gameState.npcStates[getNpcEncounterKey(tradeModal.encounter)] ?? buildInitialNpcState(tradeModal.encounter, gameState.worldType, gameState) : null; const selectedTradeNpcItem = tradeNpcState?.inventory.find(item => item.id === tradeModal?.selectedNpcItemId) ?? null; const selectedTradePlayerItem = tradeModal?.selectedPlayerItemId ? gameState.playerInventory.find(item => item.id === tradeModal?.selectedPlayerItemId) ?? null : null; const tradeMode = tradeModal?.mode ?? 'buy'; const activeTradeItem = tradeMode === 'buy' ? selectedTradeNpcItem : selectedTradePlayerItem; const activeTradeUnitPrice = tradeModal && activeTradeItem && tradeNpcState ? tradeMode === 'buy' ? getNpcPurchasePrice(activeTradeItem, tradeNpcState.affinity) : getNpcBuybackPrice(activeTradeItem, tradeNpcState.affinity) : 0; const activeTradeMaxQuantity = activeTradeItem?.quantity ?? 0; const activeTradeQuantity = tradeModal ? Math.max(1, Math.min(tradeModal.selectedQuantity, Math.max(1, activeTradeMaxQuantity))) : 1; const activeTradeTotalPrice = activeTradeUnitPrice * activeTradeQuantity; const canConfirmTrade = Boolean( activeTradeItem && activeTradeMaxQuantity > 0 && activeTradeQuantity >= 1 && activeTradeQuantity <= activeTradeMaxQuantity && (tradeMode === 'sell' || gameState.playerCurrency >= activeTradeTotalPrice) ); const tradeItemList = tradeMode === 'buy' ? (tradeNpcState?.inventory ?? []) : gameState.playerInventory; const tradeDetailItem = tradeDetail ? (tradeDetail.source === 'buy' ? tradeNpcState?.inventory ?? [] : gameState.playerInventory) .find(item => item.id === 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 = npcUi.giftModal ? getGiftCandidates(gameState.playerInventory, npcUi.giftModal.encounter, { worldType: gameState.worldType, customWorldProfile: gameState.customWorldProfile, }) : []; const handleTradeItemClick = (item: InventoryItem) => { if (tradeMode === 'buy') { npcUi.selectTradeNpcItem(item.id); setTradeDetail({ itemId: item.id, source: 'buy' }); return; } npcUi.selectTradePlayerItem(item.id); setTradeDetail({ itemId: item.id, source: 'sell' }); }; return ( {tradeModal && tradeNpcState && ( event.stopPropagation()} >
交易
{tradeModal.encounter.npcName} / 你当前{currencyName}:{gameState.playerCurrency}
{tradeModal.introText && (
{tradeModal.introText}
)}
{tradeMode === 'buy' ? '对方库存' : '你的背包'} {tradeItemList.length} 件
{tradeItemList.length > 0 ? tradeItemList.map(item => (
handleTradeItemClick(item)} />
)) : (
{tradeMode === 'buy' ? '对方暂时没有可出售的物品。' : '你当前没有可出售的物品。'}
)}
{activeTradeItem ? (
{tradeMode === 'buy' ? '购买总价' : '出售总价'} {formatCurrency(activeTradeTotalPrice, gameState.worldType)}
{tradeMode === 'buy' && gameState.playerCurrency < activeTradeTotalPrice && (
当前货币不足,还差 {formatCurrency(activeTradeTotalPrice - gameState.playerCurrency, gameState.worldType)}。
)}
) : (
请选择一件物品,右侧会显示数量、价格与详情。
)}
)} {tradeModal && tradeDetail && tradeDetailItem && ( setTradeDetail(null)} > event.stopPropagation()} >
物品详情
{tradeDetail.source === 'buy' ? '对方物品' : '背包物品'}
{tradeDetailItem.name}
{tradeDetailItem.category} / {getRarityLabel(tradeDetailItem.rarity)}
库存: {tradeDetailItem.quantity}
估值: {getInventoryItemValue(tradeDetailItem)}
{tradeDetail.source === 'buy' ? `购买价格: ${formatCurrency(getNpcPurchasePrice(tradeDetailItem, tradeNpcState?.affinity ?? 0), gameState.worldType)}` : `回收价格: ${formatCurrency(getNpcBuybackPrice(tradeDetailItem, tradeNpcState?.affinity ?? 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 ( ); }) : (
当前没有可替换的同行角色。
)}
)}
); }