Refine NPC interactions and runtime item generation
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { formatCurrency, getInventoryItemValue } from '../data/economy';
|
||||
import { getEquipmentSlotFromItem, getEquipmentSlotLabel, isInventoryItemEquippable } from '../data/equipmentEffects';
|
||||
import { type ForgeRecipeView,getReforgeCostView } from '../data/forgeSystem';
|
||||
import { isInventoryItemUsable, resolveInventoryItemUseEffect } from '../data/inventoryEffects';
|
||||
import { formatCurrency } from '../data/economy';
|
||||
import {
|
||||
getEquipmentSlotFromItem,
|
||||
getEquipmentSlotLabel,
|
||||
isInventoryItemEquippable,
|
||||
} from '../data/equipmentEffects';
|
||||
import { type ForgeRecipeView, getReforgeCostView } from '../data/forgeSystem';
|
||||
import { resolveInventoryItemUseEffect } from '../data/inventoryEffects';
|
||||
import { buildInitialPlayerInventory } from '../data/npcInteractions';
|
||||
import { Character, InventoryItem, WorldType } from '../types';
|
||||
import { CHROME_ICONS, getInventoryCategoryIcon, getNineSliceStyle, UI_CHROME } from '../uiAssets';
|
||||
import { PixelIcon } from './PixelIcon';
|
||||
import { getNineSliceStyle, UI_CHROME } from '../uiAssets';
|
||||
import {
|
||||
InventoryItemDetailModal,
|
||||
InventoryItemGrid,
|
||||
} from './InventoryItemViews';
|
||||
|
||||
interface InventoryPanelProps {
|
||||
playerCharacter: Character;
|
||||
@@ -28,56 +34,6 @@ interface InventoryPanelProps {
|
||||
onReforgeItem: (itemId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
function getInventoryRarityClass(rarity: InventoryItem['rarity']) {
|
||||
switch (rarity) {
|
||||
case 'legendary':
|
||||
return 'border-amber-400/45 bg-gradient-to-br from-amber-500/16 to-orange-500/8';
|
||||
case 'epic':
|
||||
return 'border-fuchsia-400/40 bg-gradient-to-br from-fuchsia-500/14 to-purple-500/8';
|
||||
case 'rare':
|
||||
return 'border-sky-400/40 bg-gradient-to-br from-sky-500/14 to-cyan-500/8';
|
||||
case 'uncommon':
|
||||
return 'border-emerald-400/35 bg-gradient-to-br from-emerald-500/12 to-lime-500/8';
|
||||
default:
|
||||
return 'border-white/10 bg-white/[0.04]';
|
||||
}
|
||||
}
|
||||
|
||||
function getInventoryRarityLabel(rarity: InventoryItem['rarity']) {
|
||||
switch (rarity) {
|
||||
case 'legendary':
|
||||
return '传说';
|
||||
case 'epic':
|
||||
return '史诗';
|
||||
case 'rare':
|
||||
return '稀有';
|
||||
case 'uncommon':
|
||||
return '优秀';
|
||||
default:
|
||||
return '普通';
|
||||
}
|
||||
}
|
||||
|
||||
function getInventoryItemIcon(item: InventoryItem) {
|
||||
return item.iconSrc ?? getInventoryCategoryIcon(item.category);
|
||||
}
|
||||
|
||||
function buildItemSummary(item: InventoryItem, useEffect: ReturnType<typeof resolveInventoryItemUseEffect>) {
|
||||
if (item.description?.trim()) return item.description;
|
||||
if (!useEffect) return `${item.name} 当前更适合作为交易、拆解或后续锻造素材使用。`;
|
||||
|
||||
const parts = [
|
||||
useEffect.hpRestore > 0 ? `恢复 ${useEffect.hpRestore} 点气血` : null,
|
||||
useEffect.manaRestore > 0 ? `恢复 ${useEffect.manaRestore} 点灵力` : null,
|
||||
useEffect.cooldownReduction > 0 ? `额外推进 ${useEffect.cooldownReduction} 回合冷却` : null,
|
||||
useEffect.buildBuffs.length > 0 ? `获得 ${useEffect.buildBuffs.map(buff => buff.name).join('、')}` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.length > 0
|
||||
? `${item.name} 可以立即使用,${parts.join(',')}。`
|
||||
: `${item.name} 当前更适合作为交易、拆解或后续锻造素材使用。`;
|
||||
}
|
||||
|
||||
export function InventoryPanel({
|
||||
playerCharacter,
|
||||
worldType,
|
||||
@@ -97,119 +53,104 @@ export function InventoryPanel({
|
||||
}: InventoryPanelProps) {
|
||||
const [selectedItem, setSelectedItem] = useState<InventoryItem | null>(null);
|
||||
const [isUsingItem, setIsUsingItem] = useState(false);
|
||||
const [equipmentActionKey, setEquipmentActionKey] = useState<string | null>(null);
|
||||
const [equipmentActionKey, setEquipmentActionKey] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forgeActionKey, setForgeActionKey] = useState<string | null>(null);
|
||||
|
||||
const inventoryItems = useMemo(
|
||||
() => (playerInventory.length > 0 ? playerInventory : buildInitialPlayerInventory(playerCharacter, worldType)),
|
||||
() =>
|
||||
playerInventory.length > 0
|
||||
? playerInventory
|
||||
: buildInitialPlayerInventory(playerCharacter, worldType),
|
||||
[playerCharacter, playerInventory, worldType],
|
||||
);
|
||||
|
||||
const inventorySlotCount = Math.max(16, Math.ceil(inventoryItems.length / 4) * 4);
|
||||
const inventorySlots = [
|
||||
...inventoryItems,
|
||||
...Array.from({ length: Math.max(0, inventorySlotCount - inventoryItems.length) }, () => null),
|
||||
];
|
||||
|
||||
const selectedItemUseEffect = selectedItem
|
||||
? resolveInventoryItemUseEffect(selectedItem, playerCharacter)
|
||||
: null;
|
||||
const selectedItemEquipSlot = selectedItem ? getEquipmentSlotFromItem(selectedItem) : null;
|
||||
const selectedItemReforgeCost = selectedItem ? getReforgeCostView(selectedItem, worldType) : null;
|
||||
const selectedItemEquipSlot = selectedItem
|
||||
? getEquipmentSlotFromItem(selectedItem)
|
||||
: null;
|
||||
const selectedItemReforgeCost = selectedItem
|
||||
? getReforgeCostView(selectedItem, worldType)
|
||||
: null;
|
||||
|
||||
const canUseSelectedItem = Boolean(
|
||||
selectedItem &&
|
||||
selectedItemUseEffect &&
|
||||
(
|
||||
(selectedItemUseEffect.hpRestore > 0 && playerHp < playerMaxHp) ||
|
||||
(selectedItemUseEffect.manaRestore > 0 && playerMana < playerMaxMana) ||
|
||||
selectedItemUseEffect.cooldownReduction > 0 ||
|
||||
selectedItemUseEffect.buildBuffs.length > 0
|
||||
),
|
||||
selectedItemUseEffect &&
|
||||
((selectedItemUseEffect.hpRestore > 0 && playerHp < playerMaxHp) ||
|
||||
(selectedItemUseEffect.manaRestore > 0 && playerMana < playerMaxMana) ||
|
||||
selectedItemUseEffect.cooldownReduction > 0 ||
|
||||
selectedItemUseEffect.buildBuffs.length > 0),
|
||||
);
|
||||
|
||||
const canEquipSelectedItem = Boolean(
|
||||
selectedItem &&
|
||||
selectedItemEquipSlot &&
|
||||
isInventoryItemEquippable(selectedItem) &&
|
||||
!inBattle,
|
||||
selectedItemEquipSlot &&
|
||||
isInventoryItemEquippable(selectedItem) &&
|
||||
!inBattle,
|
||||
);
|
||||
|
||||
const canDismantleSelectedItem = Boolean(
|
||||
selectedItem &&
|
||||
!inBattle &&
|
||||
(
|
||||
isInventoryItemEquippable(selectedItem) ||
|
||||
selectedItem.buildProfile
|
||||
),
|
||||
!inBattle &&
|
||||
(isInventoryItemEquippable(selectedItem) || selectedItem.buildProfile),
|
||||
);
|
||||
|
||||
const canReforgeSelectedItem = Boolean(
|
||||
selectedItem &&
|
||||
!inBattle &&
|
||||
isInventoryItemEquippable(selectedItem) &&
|
||||
selectedItem.buildProfile &&
|
||||
selectedItemReforgeCost &&
|
||||
selectedItemReforgeCost.currencyCost <= playerCurrency,
|
||||
!inBattle &&
|
||||
isInventoryItemEquippable(selectedItem) &&
|
||||
selectedItem.buildProfile &&
|
||||
selectedItemReforgeCost &&
|
||||
selectedItemReforgeCost.currencyCost <= playerCurrency,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="flex-1 overflow-y-auto scrollbar-hide">
|
||||
<div className="grid grid-cols-4 gap-2 sm:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7">
|
||||
{inventorySlots.map((item, index) => {
|
||||
if (!item) {
|
||||
return (
|
||||
<div
|
||||
key={`empty-slot-${index}`}
|
||||
className="aspect-square rounded-xl border border-dashed border-white/8 bg-black/12"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedItem(item)}
|
||||
className={`relative aspect-square rounded-xl border p-1.5 transition-colors hover:border-white/25 ${getInventoryRarityClass(item.rarity)}`}
|
||||
title={`${item.name} x${item.quantity}`}
|
||||
>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<PixelIcon
|
||||
src={getInventoryItemIcon(item)}
|
||||
className="h-9 w-9 drop-shadow-[0_4px_8px_rgba(0,0,0,0.35)] sm:h-11 sm:w-11"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-1 right-1 rounded-full border border-black/30 bg-black/65 px-1.5 py-0.5 text-[10px] font-semibold text-white">
|
||||
{item.quantity}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<InventoryItemGrid
|
||||
items={inventoryItems}
|
||||
selectedItemId={selectedItem?.id ?? null}
|
||||
onSelectItem={setSelectedItem}
|
||||
/>
|
||||
|
||||
<div className="mt-4 rounded-2xl border border-white/10 bg-black/20 p-4">
|
||||
<div className="mb-2 flex items-center justify-between gap-3 text-xs uppercase tracking-[0.2em] text-zinc-500">
|
||||
<span>工坊</span>
|
||||
<span className="text-emerald-200/80">{formatCurrency(playerCurrency, worldType)}</span>
|
||||
<span className="text-emerald-200/80">
|
||||
{formatCurrency(playerCurrency, worldType)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{forgeRecipes.map(recipe => (
|
||||
{forgeRecipes.map((recipe) => (
|
||||
<div
|
||||
key={recipe.id}
|
||||
className="rounded-xl border border-white/8 bg-black/20 p-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-white">{recipe.name}</div>
|
||||
<div className="mt-1 text-xs text-zinc-400">{recipe.description}</div>
|
||||
<div className="mt-2 text-xs text-emerald-200/80">产物:{recipe.resultLabel}</div>
|
||||
<div className="mt-1 text-[11px] text-zinc-500">花费:{recipe.currencyText}</div>
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{recipe.name}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-400">
|
||||
{recipe.description}
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-emerald-200/80">
|
||||
产物:{recipe.resultLabel}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-zinc-500">
|
||||
花费:{recipe.currencyText}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!recipe.canCraft || inBattle || forgeActionKey === recipe.id}
|
||||
disabled={
|
||||
!recipe.canCraft ||
|
||||
inBattle ||
|
||||
forgeActionKey === recipe.id
|
||||
}
|
||||
onClick={async () => {
|
||||
setForgeActionKey(recipe.id);
|
||||
const crafted = await onCraftRecipe(recipe.id);
|
||||
@@ -224,11 +165,15 @@ export function InventoryPanel({
|
||||
: 'border-white/8 bg-black/20 text-zinc-500'
|
||||
}`}
|
||||
>
|
||||
{forgeActionKey === recipe.id ? '制作中...' : recipe.kind === 'forge' ? '锻造' : '合成'}
|
||||
{forgeActionKey === recipe.id
|
||||
? '制作中...'
|
||||
: recipe.kind === 'forge'
|
||||
? '锻造'
|
||||
: '合成'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{recipe.requirements.map(requirement => (
|
||||
{recipe.requirements.map((requirement) => (
|
||||
<span
|
||||
key={`${recipe.id}-${requirement.id}`}
|
||||
className={`rounded-full border px-2 py-1 text-[10px] ${
|
||||
@@ -237,7 +182,8 @@ export function InventoryPanel({
|
||||
: 'border-white/10 bg-black/20 text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
{requirement.label} {requirement.owned}/{requirement.quantity}
|
||||
{requirement.label} {requirement.owned}/
|
||||
{requirement.quantity}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -247,200 +193,133 @@ export function InventoryPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{selectedItem && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/72 p-4 backdrop-blur-sm"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.96, y: 8 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.96, y: 8 }}
|
||||
transition={{ duration: 0.18, ease: 'easeOut' }}
|
||||
className="pixel-nine-slice pixel-modal-shell flex max-h-[min(92vh,42rem)] w-full max-w-md flex-col overflow-hidden shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:max-w-lg"
|
||||
style={getNineSliceStyle(UI_CHROME.modalPanel)}
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
<div className="relative border-b border-white/10 px-4 py-3 sm:px-5 sm:py-4">
|
||||
<div className="min-w-0 pr-10">
|
||||
<div className="text-[10px] uppercase tracking-[0.2em] text-zinc-500">{selectedItem.category}</div>
|
||||
<div className="mt-1 truncate text-sm font-semibold text-white">{selectedItem.name}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
className="absolute right-4 top-3 p-1 text-zinc-400 transition-colors hover:text-white sm:right-5 sm:top-4"
|
||||
>
|
||||
<PixelIcon src={CHROME_ICONS.close} className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`flex h-24 w-24 shrink-0 items-center justify-center rounded-2xl border ${getInventoryRarityClass(selectedItem.rarity)}`}>
|
||||
<PixelIcon
|
||||
src={getInventoryItemIcon(selectedItem)}
|
||||
className="h-14 w-14 drop-shadow-[0_6px_10px_rgba(0,0,0,0.35)]"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div className="rounded-full border border-white/10 bg-white/6 px-2 py-1 text-[10px] uppercase tracking-[0.16em] text-zinc-200">
|
||||
{getInventoryRarityLabel(selectedItem.rarity)}
|
||||
</div>
|
||||
<div className="text-sm text-zinc-300">数量:{selectedItem.quantity}</div>
|
||||
<div className="text-sm text-zinc-300">持有者:{playerCharacter.name}</div>
|
||||
<div className="text-sm text-zinc-300">
|
||||
可使用:{isInventoryItemUsable(selectedItem) ? '是' : '否'}
|
||||
</div>
|
||||
<div className="text-sm text-zinc-300">
|
||||
可装备:{selectedItemEquipSlot ? getEquipmentSlotLabel(selectedItemEquipSlot) : '否'}
|
||||
</div>
|
||||
<div className="text-sm text-zinc-300">
|
||||
价值:{formatCurrency(getInventoryItemValue(selectedItem), worldType)}
|
||||
</div>
|
||||
{selectedItemReforgeCost && (
|
||||
<div className="text-sm text-zinc-300">
|
||||
重铸成本:{selectedItemReforgeCost.currencyText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pixel-nine-slice pixel-panel" style={getNineSliceStyle(UI_CHROME.infoPanel)}>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-zinc-300">
|
||||
<div className="rounded-lg border border-white/6 bg-black/20 px-3 py-2">类型:{selectedItem.category}</div>
|
||||
<div className="rounded-lg border border-white/6 bg-black/20 px-3 py-2">标签:{selectedItem.tags.length}</div>
|
||||
</div>
|
||||
<div className="mt-3 text-sm leading-relaxed text-zinc-300">
|
||||
{buildItemSummary(selectedItem, selectedItemUseEffect)}
|
||||
</div>
|
||||
{selectedItemUseEffect?.buildBuffs.length ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{selectedItemUseEffect.buildBuffs.map(buff => (
|
||||
<span
|
||||
key={buff.id}
|
||||
className="rounded-full border border-sky-400/20 bg-sky-500/10 px-2 py-1 text-[10px] text-sky-100"
|
||||
>
|
||||
{buff.name} / {buff.tags.join('、')} / {buff.durationTurns} 回合
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{selectedItem.tags.length > 0 ? (
|
||||
selectedItem.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full border border-white/10 bg-black/20 px-2 py-1 text-[10px] text-zinc-300"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="rounded-full border border-white/10 bg-black/20 px-2 py-1 text-[10px] text-zinc-300">
|
||||
无标签
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!selectedItem || !canDismantleSelectedItem || forgeActionKey === selectedItem.id}
|
||||
onClick={async () => {
|
||||
if (!selectedItem) return;
|
||||
setForgeActionKey(selectedItem.id);
|
||||
const dismantled = await onDismantleItem(selectedItem.id);
|
||||
setForgeActionKey(null);
|
||||
if (dismantled) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}}
|
||||
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${
|
||||
canDismantleSelectedItem && forgeActionKey !== selectedItem.id ? 'text-white' : 'text-zinc-600'
|
||||
}`}
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 14, paddingY: 8 })}
|
||||
>
|
||||
{forgeActionKey === selectedItem.id ? '拆解中...' : '拆解'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!selectedItem || !canReforgeSelectedItem || forgeActionKey === `${selectedItem.id}:reforge`}
|
||||
onClick={async () => {
|
||||
if (!selectedItem) return;
|
||||
setForgeActionKey(`${selectedItem.id}:reforge`);
|
||||
const reforged = await onReforgeItem(selectedItem.id);
|
||||
setForgeActionKey(null);
|
||||
if (reforged) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}}
|
||||
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${
|
||||
canReforgeSelectedItem && forgeActionKey !== `${selectedItem.id}:reforge` ? 'text-white' : 'text-zinc-600'
|
||||
}`}
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 14, paddingY: 8 })}
|
||||
>
|
||||
{forgeActionKey === `${selectedItem.id}:reforge` ? '重铸中...' : '重铸'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!selectedItem || !canEquipSelectedItem || equipmentActionKey === selectedItem.id}
|
||||
onClick={async () => {
|
||||
if (!selectedItem) return;
|
||||
setEquipmentActionKey(selectedItem.id);
|
||||
const equipped = await onEquipItem(selectedItem.id);
|
||||
setEquipmentActionKey(null);
|
||||
if (equipped) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}}
|
||||
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${
|
||||
canEquipSelectedItem && equipmentActionKey !== selectedItem.id ? 'text-white' : 'text-zinc-600'
|
||||
}`}
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 14, paddingY: 8 })}
|
||||
>
|
||||
{equipmentActionKey === selectedItem.id
|
||||
? '装备中...'
|
||||
: selectedItemEquipSlot
|
||||
? `装备到 ${getEquipmentSlotLabel(selectedItemEquipSlot)}`
|
||||
: '不可装备'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
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={!selectedItem || !canUseSelectedItem || isUsingItem}
|
||||
onClick={async () => {
|
||||
if (!selectedItem) return;
|
||||
setIsUsingItem(true);
|
||||
const used = await onUseItem(selectedItem.id);
|
||||
setIsUsingItem(false);
|
||||
if (used) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}}
|
||||
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${canUseSelectedItem && !isUsingItem ? 'text-white' : 'text-zinc-600'}`}
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, { paddingX: 14, paddingY: 8 })}
|
||||
>
|
||||
{isUsingItem ? '使用中...' : '使用'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<InventoryItemDetailModal
|
||||
item={selectedItem}
|
||||
playerCharacter={playerCharacter}
|
||||
worldType={worldType}
|
||||
onClose={() => setSelectedItem(null)}
|
||||
footer={
|
||||
selectedItem ? (
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
!canDismantleSelectedItem ||
|
||||
forgeActionKey === selectedItem.id
|
||||
}
|
||||
onClick={async () => {
|
||||
setForgeActionKey(selectedItem.id);
|
||||
const dismantled = await onDismantleItem(selectedItem.id);
|
||||
setForgeActionKey(null);
|
||||
if (dismantled) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}}
|
||||
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${
|
||||
canDismantleSelectedItem && forgeActionKey !== selectedItem.id
|
||||
? 'text-white'
|
||||
: 'text-zinc-600'
|
||||
}`}
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, {
|
||||
paddingX: 14,
|
||||
paddingY: 8,
|
||||
})}
|
||||
>
|
||||
{forgeActionKey === selectedItem.id ? '拆解中...' : '拆解'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
!canReforgeSelectedItem ||
|
||||
forgeActionKey === `${selectedItem.id}:reforge`
|
||||
}
|
||||
onClick={async () => {
|
||||
setForgeActionKey(`${selectedItem.id}:reforge`);
|
||||
const reforged = await onReforgeItem(selectedItem.id);
|
||||
setForgeActionKey(null);
|
||||
if (reforged) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}}
|
||||
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${
|
||||
canReforgeSelectedItem &&
|
||||
forgeActionKey !== `${selectedItem.id}:reforge`
|
||||
? 'text-white'
|
||||
: 'text-zinc-600'
|
||||
}`}
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, {
|
||||
paddingX: 14,
|
||||
paddingY: 8,
|
||||
})}
|
||||
>
|
||||
{forgeActionKey === `${selectedItem.id}:reforge`
|
||||
? '重铸中...'
|
||||
: '重铸'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
!canEquipSelectedItem ||
|
||||
equipmentActionKey === selectedItem.id
|
||||
}
|
||||
onClick={async () => {
|
||||
setEquipmentActionKey(selectedItem.id);
|
||||
const equipped = await onEquipItem(selectedItem.id);
|
||||
setEquipmentActionKey(null);
|
||||
if (equipped) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}}
|
||||
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${
|
||||
canEquipSelectedItem && equipmentActionKey !== selectedItem.id
|
||||
? 'text-white'
|
||||
: 'text-zinc-600'
|
||||
}`}
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, {
|
||||
paddingX: 14,
|
||||
paddingY: 8,
|
||||
})}
|
||||
>
|
||||
{equipmentActionKey === selectedItem.id
|
||||
? '装备中...'
|
||||
: selectedItemEquipSlot
|
||||
? `装备到 ${getEquipmentSlotLabel(selectedItemEquipSlot)}`
|
||||
: '不可装备'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
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={!canUseSelectedItem || isUsingItem}
|
||||
onClick={async () => {
|
||||
setIsUsingItem(true);
|
||||
const used = await onUseItem(selectedItem.id);
|
||||
setIsUsingItem(false);
|
||||
if (used) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}}
|
||||
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${canUseSelectedItem && !isUsingItem ? 'text-white' : 'text-zinc-600'}`}
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, {
|
||||
paddingX: 14,
|
||||
paddingY: 8,
|
||||
})}
|
||||
>
|
||||
{isUsingItem ? '使用中...' : '使用'}
|
||||
</button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user