1
This commit is contained in:
@@ -4,10 +4,7 @@ import { useState } from 'react';
|
||||
import { getCharacterById } from '../data/characterPresets';
|
||||
import {
|
||||
formatCurrency,
|
||||
getCurrencyName,
|
||||
getInventoryItemValue,
|
||||
getNpcBuybackPrice,
|
||||
getNpcPurchasePrice,
|
||||
} from '../data/economy';
|
||||
import {
|
||||
getEquipmentSlotFromItem,
|
||||
@@ -19,12 +16,15 @@ import {
|
||||
getInventoryTagLabels,
|
||||
} from '../data/itemPresentation';
|
||||
import {
|
||||
buildInitialNpcState,
|
||||
getGiftCandidates,
|
||||
getRarityLabel,
|
||||
} from '../data/npcInteractions';
|
||||
import { StoryGenerationNpcUi } from '../hooks/rpg-runtime-story';
|
||||
import { GameState, InventoryItem } from '../types';
|
||||
import {
|
||||
GameState,
|
||||
InventoryItem,
|
||||
RuntimeNpcGiftItemView,
|
||||
RuntimeNpcTradeItemView,
|
||||
} from '../types';
|
||||
import { CHROME_ICONS, getInventoryItemVisualSrc, getNineSliceStyle, UI_CHROME } from '../uiAssets';
|
||||
import { PixelIcon } from './PixelIcon';
|
||||
|
||||
@@ -38,10 +38,6 @@ type TradeDetailState = {
|
||||
source: 'buy' | 'sell';
|
||||
} | null;
|
||||
|
||||
function getNpcEncounterKey(encounter: NonNullable<GameState['currentEncounter']>) {
|
||||
return encounter.id ?? encounter.npcName;
|
||||
}
|
||||
|
||||
function getItemVisualSrc(item: InventoryItem) {
|
||||
return getInventoryItemVisualSrc(item);
|
||||
}
|
||||
@@ -88,7 +84,7 @@ function TradeItemRow({
|
||||
<PixelIcon src={getItemVisualSrc(item)} className="h-7 w-7" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-white">{item.name}</div>
|
||||
<div className="truncate text-sm font-medium text-white">{item.name ?? item.id}</div>
|
||||
<div className="mt-1 text-[10px] text-zinc-500">
|
||||
{item.category} / {getRarityLabel(item.rarity)} / 单价 {unitPrice} {currencyName}
|
||||
</div>
|
||||
@@ -150,71 +146,70 @@ function TradeQuantityStepper({
|
||||
|
||||
export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
||||
const [tradeDetail, setTradeDetail] = useState<TradeDetailState>(null);
|
||||
const currencyName = getCurrencyName(
|
||||
gameState.worldType,
|
||||
gameState.customWorldProfile,
|
||||
);
|
||||
const npcInteraction = gameState.runtimeNpcInteraction ?? null;
|
||||
const currencyName = npcInteraction?.currencyName ?? '钱币';
|
||||
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 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(
|
||||
activeTradeItem &&
|
||||
activeTradeMaxQuantity > 0 &&
|
||||
activeTradeQuantity >= 1 &&
|
||||
activeTradeQuantity <= activeTradeMaxQuantity &&
|
||||
(tradeMode === 'sell' || gameState.playerCurrency >= activeTradeTotalPrice)
|
||||
activeTradeView &&
|
||||
activeTradeView.canSubmit &&
|
||||
activeTradeQuantity >= 1,
|
||||
);
|
||||
const tradeItemList = tradeMode === 'buy'
|
||||
? (tradeNpcState?.inventory ?? [])
|
||||
: gameState.playerInventory;
|
||||
const tradeItemList = tradeItemViews;
|
||||
const tradeDetailItem = tradeDetail
|
||||
? (tradeDetail.source === 'buy' ? tradeNpcState?.inventory ?? [] : gameState.playerInventory)
|
||||
.find(item => item.id === tradeDetail.itemId) ?? null
|
||||
? (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 = npcUi.giftModal
|
||||
? getGiftCandidates(gameState.playerInventory, npcUi.giftModal.encounter, {
|
||||
worldType: gameState.worldType,
|
||||
customWorldProfile: gameState.customWorldProfile,
|
||||
})
|
||||
const giftCandidates: RuntimeNpcGiftItemView[] = npcUi.giftModal
|
||||
? npcInteraction?.gift.items ?? []
|
||||
: [];
|
||||
const activeGiftView =
|
||||
giftCandidates.find(item => item.itemId === npcUi.giftModal?.selectedItemId) ?? null;
|
||||
|
||||
const handleTradeItemClick = (item: InventoryItem) => {
|
||||
const handleTradeItemClick = (view: RuntimeNpcTradeItemView) => {
|
||||
if (tradeMode === 'buy') {
|
||||
npcUi.selectTradeNpcItem(item.id);
|
||||
setTradeDetail({ itemId: item.id, source: 'buy' });
|
||||
npcUi.selectTradeNpcItem(view.itemId);
|
||||
setTradeDetail({ itemId: view.itemId, source: 'buy' });
|
||||
return;
|
||||
}
|
||||
|
||||
npcUi.selectTradePlayerItem(item.id);
|
||||
setTradeDetail({ itemId: item.id, source: 'sell' });
|
||||
npcUi.selectTradePlayerItem(view.itemId);
|
||||
setTradeDetail({ itemId: view.itemId, source: 'sell' });
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{tradeModal && tradeNpcState && (
|
||||
{tradeModal && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -234,7 +229,7 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-white">交易</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
{tradeModal.encounter.npcName} / 你当前{currencyName}:{gameState.playerCurrency}
|
||||
{npcInteraction?.npcName ?? tradeModal.encounter.npcName} / 你当前{currencyName}:{npcInteraction?.playerCurrency ?? gameState.playerCurrency}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -285,19 +280,22 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
||||
</div>
|
||||
|
||||
<div className="max-h-[42vh] space-y-2 overflow-y-auto pr-1 scrollbar-hide">
|
||||
{tradeItemList.length > 0 ? tradeItemList.map(item => (
|
||||
<div key={item.id}>
|
||||
{tradeItemList.length > 0 ? tradeItemList.map(view => (
|
||||
<div key={view.itemId}>
|
||||
<TradeItemRow
|
||||
item={item}
|
||||
item={view.item}
|
||||
selected={tradeMode === 'buy'
|
||||
? tradeModal.selectedNpcItemId === item.id
|
||||
: tradeModal.selectedPlayerItemId === item.id}
|
||||
unitPrice={tradeMode === 'buy'
|
||||
? getNpcPurchasePrice(item, tradeNpcState.affinity)
|
||||
: getNpcBuybackPrice(item, tradeNpcState.affinity)}
|
||||
? tradeModal.selectedNpcItemId === view.itemId
|
||||
: tradeModal.selectedPlayerItemId === view.itemId}
|
||||
unitPrice={view.unitPrice}
|
||||
currencyName={currencyName}
|
||||
onClick={() => handleTradeItemClick(item)}
|
||||
onClick={() => handleTradeItemClick(view)}
|
||||
/>
|
||||
{!view.canSubmit && view.reason && (
|
||||
<div className="mt-1 px-1 text-[10px] text-rose-300">
|
||||
{view.reason}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)) : (
|
||||
<div className="rounded-xl border border-dashed border-white/10 bg-black/20 px-4 py-6 text-sm text-zinc-500">
|
||||
@@ -324,9 +322,9 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
||||
{formatCurrency(activeTradeTotalPrice, gameState.worldType)}
|
||||
</span>
|
||||
</div>
|
||||
{tradeMode === 'buy' && gameState.playerCurrency < activeTradeTotalPrice && (
|
||||
{!activeTradeView?.canSubmit && activeTradeView?.reason && (
|
||||
<div className="mt-2 text-xs text-rose-300">
|
||||
当前货币不足,还差 {formatCurrency(activeTradeTotalPrice - gameState.playerCurrency, gameState.worldType)}。
|
||||
{activeTradeView.reason}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -411,8 +409,8 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
||||
<div>估值: {getInventoryItemValue(tradeDetailItem)}</div>
|
||||
<div>
|
||||
{tradeDetail.source === 'buy'
|
||||
? `购买价格: ${formatCurrency(getNpcPurchasePrice(tradeDetailItem, tradeNpcState?.affinity ?? 0), gameState.worldType)}`
|
||||
: `回收价格: ${formatCurrency(getNpcBuybackPrice(tradeDetailItem, tradeNpcState?.affinity ?? 0), gameState.worldType)}`}
|
||||
? `购买价格: ${formatCurrency(tradeDetailView?.unitPrice ?? 0, gameState.worldType)}`
|
||||
: `回收价格: ${formatCurrency(tradeDetailView?.unitPrice ?? 0, gameState.worldType)}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -489,10 +487,10 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
||||
)}
|
||||
{giftCandidates.length > 0 ? giftCandidates.map(candidate => (
|
||||
<button
|
||||
key={candidate.item.id}
|
||||
key={candidate.itemId}
|
||||
type="button"
|
||||
onClick={() => npcUi.selectGiftItem(candidate.item.id)}
|
||||
className={`w-full rounded-lg border px-3 py-2 text-left transition-colors ${npcUi.giftModal?.selectedItemId === candidate.item.id ? 'border-rose-400/60 bg-rose-500/10' : 'border-white/5 bg-black/20 hover:border-white/15'}`}
|
||||
onClick={() => npcUi.selectGiftItem(candidate.itemId)}
|
||||
className={`w-full rounded-lg border px-3 py-2 text-left transition-colors ${npcUi.giftModal?.selectedItemId === candidate.itemId ? 'border-rose-400/60 bg-rose-500/10' : 'border-white/5 bg-black/20 hover:border-white/15'}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -500,9 +498,9 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
||||
<div>
|
||||
<div className="text-sm text-white">{candidate.item.name}</div>
|
||||
<div className="mt-1 text-[10px] text-zinc-500">{candidate.item.category} / {getRarityLabel(candidate.item.rarity)}</div>
|
||||
{candidate.attributeInsight?.reasonText && (
|
||||
{!candidate.canSubmit && candidate.reason && (
|
||||
<div className="mt-1 text-[10px] text-rose-200/80">
|
||||
属性共振:{candidate.attributeInsight.reasonText}
|
||||
{candidate.reason}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -523,7 +521,7 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
||||
<button type="button" onClick={npcUi.closeGiftModal} className="pixel-nine-slice pixel-pressable px-4 py-2 text-xs text-zinc-200" style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 14, paddingY: 8 })}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" disabled={!npcUi.giftModal.selectedItemId} onClick={npcUi.confirmGift} className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${npcUi.giftModal.selectedItemId ? 'text-white' : 'text-zinc-600'}`} style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 14, paddingY: 8 })}>
|
||||
<button type="button" disabled={!activeGiftView?.canSubmit} onClick={npcUi.confirmGift} className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${activeGiftView?.canSubmit ? 'text-white' : 'text-zinc-600'}`} style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 14, paddingY: 8 })}>
|
||||
确认赠礼
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user