- 1차 — 코어 검증 (여기까지만 되어도 재미 판정 가능) 1. **기호 체계 + 책상 상태 상시 노출 UI** (§3) 2. **책상 적재 + 누진 패시브 페널티 A·B** (§4) 3. **표준 족보 판정 및 합본 실행** (§5) 4. **합본 기반 토큰 산출** (§8.2) 게임 플레이 후 피드백 해결 - 내용은 아래 문서 참고 - docs/decisions/2026-09-05_core-graybox-ux-feedback.md
70 lines
2.9 KiB
React
70 lines
2.9 KiB
React
import React, { useMemo, useState } from 'react';
|
|
import SymbolBadge from './SymbolBadge';
|
|
import { detectHands, resolveCombine, HAND_DEFS } from '../engine/hands';
|
|
import './CombinePanel.css';
|
|
|
|
// 합본 결재 (§5) — 책상에서 카드를 다중 선택하면 성립 가능한 족보를 자동 판정해 보여준다.
|
|
// 여러 족보가 동시 성립하면 최고 족보를 기본값으로 보여주되, 하위 족보도 선택할 수 있게 한다 (§5.3) —
|
|
// 명분 종류를 고르기 위해 일부러 하위 족보를 쓰는 선택이 존재해야 하기 때문이다.
|
|
export default function CombinePanel({ deskCards, onConfirm, onClose }) {
|
|
const [selectedIds, setSelectedIds] = useState([]);
|
|
|
|
const selection = useMemo(
|
|
() => deskCards.filter((c) => selectedIds.includes(c.id)),
|
|
[deskCards, selectedIds]
|
|
);
|
|
const matches = useMemo(() => detectHands(selection), [selection]);
|
|
|
|
const toggle = (id) => {
|
|
setSelectedIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
|
};
|
|
|
|
const confirm = (patternId) => {
|
|
const result = resolveCombine(selection, patternId);
|
|
if (result) onConfirm(result);
|
|
};
|
|
|
|
return (
|
|
<div className="combine-overlay" onClick={onClose}>
|
|
<div className="combine-panel" onClick={(e) => e.stopPropagation()}>
|
|
<div className="combine-header">
|
|
<h2>합본 결재</h2>
|
|
<button className="combine-close" onClick={onClose}>×</button>
|
|
</div>
|
|
|
|
<p className="combine-hint">책상에서 카드를 선택하면 성립하는 족보를 판정합니다.</p>
|
|
|
|
<div className="combine-desk-grid">
|
|
{deskCards.length === 0 && <p className="combine-empty">책상이 비어 있습니다.</p>}
|
|
{deskCards.map((c) => (
|
|
<button
|
|
key={c.id}
|
|
className={`combine-card ${selectedIds.includes(c.id) ? 'selected' : ''}`}
|
|
onClick={() => toggle(c.id)}
|
|
>
|
|
<SymbolBadge axis={c.axis} grade={c.grade} />
|
|
{c.name && <span className="combine-card-name">{c.name}</span>}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="combine-results">
|
|
{selection.length > 0 && matches.length === 0 && (
|
|
<p className="combine-no-match">선택한 {selection.length}장은 성립하는 족보가 없습니다.</p>
|
|
)}
|
|
{matches.map((patternId) => {
|
|
const def = HAND_DEFS[patternId];
|
|
return (
|
|
<button key={patternId} className="combine-match" onClick={() => confirm(patternId)}>
|
|
<span className="match-name">{def.name}</span>
|
|
<span className="match-score">실적 +{def.score}</span>
|
|
<span className="match-justification">명분 +{def.baseJustification}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|