| 1 | |
| 2 | |
| 3 | |
| 4 | import type { PendingView } from '../../../src/engine/types'; |
| 5 | import type { PlayInteractionResult } from '../../../src/client/hooks/usePlayInteraction'; |
| 6 | import { useGameView } from '../../../src/client/components/GameViewCtx'; |
| 7 | import { displaySkillName } from '../../../src/client/utils/skillDisplay'; |
| 8 | |
| 9 | interface Props { |
| 10 | play: PlayInteractionResult; |
| 11 | pending: PendingView | null; |
| 12 | isRespondPending: boolean; |
| 13 | respondCandidateCount: number | null; |
| 14 | showCancelSelection: boolean; |
| 15 | showEndTurn: boolean; |
| 16 | isDiscardPhase: boolean; |
| 17 | isPerspectiveAwaiting: boolean; |
| 18 | discardMin: number; |
| 19 | discardMax: number; |
| 20 | } |
| 21 | |
| 22 | type Btn = { key: string; label: string; kind: 'ok' | 'cancel' | 'end' | 'plain'; onClick: () => void; disabled?: boolean; title?: string }; |
| 23 | |
| 24 | export function ActionBar({ play, pending, isRespondPending, respondCandidateCount, showCancelSelection, showEndTurn, isDiscardPhase, isPerspectiveAwaiting, discardMin, discardMax }: Props) { |
| 25 | const { canOperate } = useGameView(); |
| 26 | const { |
| 27 | selectedCardId, selectedTarget, selectedForDiscard, transformMode, transformWrapperName, distributeMode, activeDistribute, isDistributeActive, distSelected, distAllocations, |
| 28 | distTargetName, selectedActive, playButtonState, transformSubmit, selectedRespondCardId, respondTargetReady, altActions, handlePlayCard, handleSkillAction, handleTransformPlay, |
| 29 | isRespondTransformContext, handleRespond, handlePlayRespond, handleEndTurn, handleConfirmDiscard, handleDiscardSelectAll, handleTransformSelectAll, handleTransformInvert, |
| 30 | handleDistSubmit, handleDistClear, handleDistSelectAll, handleDistInvert, cancelSelection, setDistributeMode, |
| 31 | } = play; |
| 32 | const b: Btn[] = []; |
| 33 | const weapon = pending?.prompt?.type === 'useCardAndTarget'; |
| 34 | |
| 35 | if (isRespondPending) { |
| 36 | if (respondCandidateCount !== 0) b.push({ key: 'respond', label: '确定', kind: 'ok', onClick: handlePlayRespond, disabled: !selectedRespondCardId || !respondTargetReady }); |
| 37 | b.push({ key: 'skip', label: weapon ? '交出武器' : '取消', kind: 'cancel', onClick: () => handleRespond(), title: respondCandidateCount === 0 ? '没有可回应的牌' : undefined }); |
| 38 | } |
| 39 | if (canOperate && transformMode && transformMode.minCards > 1) { |
| 40 | const ids = transformMode.selectedCardIds; |
| 41 | b.push({ key: 't-all', label: '全选', kind: 'plain', onClick: handleTransformSelectAll, disabled: ids.length >= transformMode.maxCards }); |
| 42 | b.push({ key: 't-inv', label: '反选', kind: 'plain', onClick: handleTransformInvert, disabled: ids.length === 0 }); |
| 43 | } |
| 44 | if (canOperate && (selectedActive || isRespondTransformContext) && transformMode && transformMode.minCards > 1) { |
| 45 | const ok = transformSubmit?.canSubmit ?? false; |
| 46 | const needsTarget = transformSubmit?.needsTarget ?? false; |
| 47 | b.push({ key: 't-go', label: '确定', kind: 'ok', onClick: () => ok && handleTransformPlay(needsTarget ? selectedTarget! : ''), disabled: !ok, title: `使用${transformWrapperName ?? transformMode.wrapperName}` }); |
| 48 | } |
| 49 | if (canOperate && (selectedActive || isRespondTransformContext) && transformMode?.minCards === 1 && selectedCardId) { |
| 50 | if (isRespondTransformContext) b.push({ key: 't-one', label: '确定', kind: 'ok', onClick: () => handleTransformPlay(''), title: `使用${transformWrapperName ?? transformMode.wrapperName}` }); |
| 51 | else b.push({ key: 't-one', label: '确定', kind: 'ok', onClick: () => selectedTarget && handleTransformPlay(selectedTarget), disabled: !selectedTarget, title: `使用${transformWrapperName ?? transformMode.wrapperName}` }); |
| 52 | } |
| 53 | if (canOperate && selectedActive && !transformMode && selectedCardId && playButtonState) |
| 54 | b.push({ key: 'play', label: '确定', kind: 'ok', onClick: handlePlayCard, disabled: !playButtonState.canPlay, title: `出牌${playButtonState.targetLabel}` }); |
| 55 | if (canOperate && !transformMode && selectedCardId) for (const a of altActions) b.push({ key: `${a.skillId}:${a.actionType}`, label: displaySkillName(a.label), kind: 'plain', onClick: () => handleSkillAction(a) }); |
| 56 | if (!transformMode && showCancelSelection) b.push({ key: 'cancel', label: '取消', kind: 'cancel', onClick: cancelSelection }); |
| 57 | if (showEndTurn) b.push({ key: 'end', label: '结束回合', kind: 'end', onClick: handleEndTurn }); |
| 58 | if (canOperate && isDiscardPhase && isPerspectiveAwaiting) { |
| 59 | const n = selectedForDiscard.length; |
| 60 | b.push({ key: 'discard', label: '确定', kind: 'ok', onClick: handleConfirmDiscard, disabled: n < discardMin || n > discardMax, title: `弃置 ${n}/${discardMin}` }); |
| 61 | b.push({ key: 'd-all', label: '全选', kind: 'plain', onClick: handleDiscardSelectAll, disabled: n >= discardMax }); |
| 62 | } |
| 63 | if (canOperate && isDistributeActive && activeDistribute) { |
| 64 | const mode = activeDistribute.prompt.mode ?? 'allocate'; |
| 65 | const minTotal = activeDistribute.prompt.minTotal ?? 1; |
| 66 | const maxTotal = activeDistribute.prompt.maxTotal ?? 99; |
| 67 | const ok = |
| 68 | mode === 'select' |
| 69 | ? distSelected.size >= minTotal && distSelected.size <= maxTotal |
| 70 | : activeDistribute.externalTargetSelection |
| 71 | ? distSelected.size >= minTotal && distSelected.size <= maxTotal && !!distTargetName |
| 72 | : distAllocations.flatMap((a) => a.cardIds).length >= minTotal; |
| 73 | if (mode === 'select') { |
| 74 | b.push({ key: 'x-all', label: '全选', kind: 'plain', onClick: handleDistSelectAll, disabled: distSelected.size >= activeDistribute.cardIds.length }); |
| 75 | b.push({ key: 'x-inv', label: '反选', kind: 'plain', onClick: handleDistInvert, disabled: distSelected.size === 0 }); |
| 76 | } |
| 77 | b.push({ key: 'x-clear', label: '重选', kind: 'plain', onClick: handleDistClear, disabled: distSelected.size === 0 && distAllocations.length === 0 }); |
| 78 | b.push({ key: 'x-go', label: '确定', kind: 'ok', onClick: handleDistSubmit, disabled: !ok, title: activeDistribute.prompt.title }); |
| 79 | if (distributeMode) b.push({ key: 'x-cancel', label: '取消', kind: 'cancel', onClick: () => setDistributeMode(null) }); |
| 80 | } |
| 81 | if (!b.length) return null; |
| 82 | return ( |
| 83 | <div className="nm-control"> |
| 84 | {b.map(({ key, label, kind, onClick, disabled, title }) => ( |
| 85 | <button key={key} className={`nm-btn ${kind}`} onClick={onClick} disabled={disabled} title={title}> |
| 86 | {label} |
| 87 | </button> |
| 88 | ))} |
| 89 | </div> |
| 90 | ); |
| 91 | } |