Integrate Match3D Q1 flow
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-01 13:53:59 +08:00
parent 375f7493a3
commit df24467e1d
24 changed files with 2089 additions and 361 deletions

View File

@@ -70,6 +70,22 @@ function buildClientEventId(itemInstanceId: string) {
)}`;
}
function isRunState(
status: Match3DRunSnapshot['status'],
expected: 'running' | 'won' | 'failed' | 'stopped',
) {
return String(status).toLowerCase() === expected;
}
function isItemState(
state: Match3DItemSnapshot['state'],
expected: 'in_board' | 'in_tray' | 'cleared' | 'flying',
) {
return String(state)
.replace(/([a-z])([A-Z])/gu, '$1_$2')
.toLowerCase() === expected;
}
function isPointInsideCircle(
pointX: number,
pointY: number,
@@ -86,7 +102,7 @@ function findHitItem(
return run.items
.filter(
(item) =>
item.state === 'InBoard' &&
isItemState(item.state, 'in_board') &&
item.clickable &&
isPointInsideCircle(pointX, pointY, item),
)
@@ -137,13 +153,13 @@ function Match3DToken({
const visualSeed = resolveVisualSeed(item.visualKey);
const size = `${item.radius * 200}%`;
const itemStateClass =
item.state === 'Flying'
isItemState(item.state, 'flying')
? 'scale-75 opacity-0'
: item.clickable
? 'cursor-pointer opacity-100 hover:scale-105 active:scale-95'
: 'opacity-48';
if (item.state !== 'InBoard' && item.state !== 'Flying') {
if (!isItemState(item.state, 'in_board') && !isItemState(item.state, 'flying')) {
return null;
}
@@ -160,7 +176,7 @@ function Match3DToken({
}}
aria-label={`${visualSeed.label} ${item.clickable ? '可点击' : '被遮挡'}`}
data-testid={`match3d-item-${item.itemInstanceId}`}
disabled={disabled || !item.clickable || item.state !== 'InBoard'}
disabled={disabled || !item.clickable || !isItemState(item.state, 'in_board')}
onClick={() => onClick(item)}
>
<span className="relative z-10">{visualSeed.label}</span>
@@ -193,11 +209,11 @@ function Match3DSettlement({
onBack: () => void;
onRestart: () => void;
}) {
if (run.status === 'Running') {
if (isRunState(run.status, 'running')) {
return null;
}
const won = run.status === 'Won';
const stopped = run.status === 'Stopped';
const won = isRunState(run.status, 'won');
const stopped = isRunState(run.status, 'stopped');
const title = won ? '通关完成' : stopped ? '已停止' : '本轮失败';
const description = won
? `用时 ${formatElapsed(run.startedAtMs, run.remainingMs, run.durationLimitMs)}`
@@ -265,7 +281,7 @@ export function Match3DRuntimeShell({
}, [run?.remainingMs, run?.snapshotVersion]);
useEffect(() => {
if (!run || run.status !== 'Running') {
if (!run || !isRunState(run.status, 'running')) {
return undefined;
}
const timer = window.setInterval(() => {
@@ -296,7 +312,7 @@ export function Match3DRuntimeShell({
}, [run]);
const handleItemClick = async (item: Match3DItemSnapshot) => {
if (!run || run.status !== 'Running' || pendingClick) {
if (!run || !isRunState(run.status, 'running') || pendingClick) {
return;
}
const optimisticRun = buildOptimisticRun(run, item);
@@ -337,7 +353,7 @@ export function Match3DRuntimeShell({
};
const handleBoardPointerDown = (event: PointerEvent<HTMLDivElement>) => {
if (!run || run.status !== 'Running' || pendingClick) {
if (!run || !isRunState(run.status, 'running') || pendingClick) {
return;
}
const rect = stageRef.current?.getBoundingClientRect();