完善抓大鹅创作入口与运行态表现

This commit is contained in:
2026-05-01 22:07:55 +08:00
parent 8c03ec95c6
commit 9a3db67e13
25 changed files with 1320 additions and 183 deletions

View File

@@ -14,68 +14,147 @@ type Match3DVisualSeed = {
visualKey: string;
colorClassName: string;
label: string;
sizeScale?: number;
};
export const MATCH3D_VISUAL_SEEDS: Match3DVisualSeed[] = [
// 中文注释:水果题材内置视觉键要和后端 module-match3d 保持一致,避免不同物品被兜底成同一图案。
{
itemTypeId: 'watermelon',
visualKey: 'watermelon-green',
colorClassName: 'from-emerald-500 to-green-800',
label: '西瓜',
sizeScale: 1.24,
},
{
itemTypeId: 'apple',
visualKey: 'apple-red',
colorClassName: 'from-rose-400 to-red-600',
label: '苹',
label: '苹',
sizeScale: 1,
},
{
itemTypeId: 'banana',
visualKey: 'banana-yellow',
colorClassName: 'from-yellow-300 to-amber-500',
label: '蕉',
label: '蕉',
sizeScale: 1.04,
},
{
itemTypeId: 'grape',
visualKey: 'grape-purple',
colorClassName: 'from-violet-400 to-purple-700',
label: '萄',
label: '萄',
sizeScale: 0.78,
},
{
itemTypeId: 'melon',
visualKey: 'melon-green',
colorClassName: 'from-emerald-300 to-green-600',
label: '瓜',
label: '瓜',
sizeScale: 1.12,
},
{
itemTypeId: 'berry',
visualKey: 'berry-blue',
colorClassName: 'from-sky-300 to-blue-600',
label: '莓',
label: '莓',
sizeScale: 0.78,
},
{
itemTypeId: 'peach',
visualKey: 'peach-pink',
colorClassName: 'from-pink-300 to-orange-400',
label: '桃',
label: '桃',
sizeScale: 1,
},
{
itemTypeId: 'plum',
visualKey: 'plum-indigo',
colorClassName: 'from-indigo-300 to-indigo-700',
label: '李',
label: '李',
sizeScale: 0.86,
},
{
itemTypeId: 'lime',
visualKey: 'lime-lime',
colorClassName: 'from-lime-300 to-lime-600',
label: '柠',
label: '柠',
sizeScale: 0.86,
},
{
itemTypeId: 'orange',
visualKey: 'orange-orange',
colorClassName: 'from-orange-300 to-orange-600',
label: '橙',
label: '橙',
sizeScale: 1,
},
{
itemTypeId: 'candy',
visualKey: 'candy-cyan',
itemTypeId: 'pear',
visualKey: 'pear-cyan',
colorClassName: 'from-cyan-300 to-teal-600',
label: '',
label: '',
sizeScale: 1,
},
{
itemTypeId: 'red-circle',
visualKey: 'red_circle',
colorClassName: 'from-rose-400 to-red-600',
label: '圆',
},
{
itemTypeId: 'yellow-triangle',
visualKey: 'yellow_triangle',
colorClassName: 'from-yellow-300 to-amber-500',
label: '三',
},
{
itemTypeId: 'purple-diamond',
visualKey: 'purple_diamond',
colorClassName: 'from-violet-400 to-purple-700',
label: '菱',
},
{
itemTypeId: 'green-square',
visualKey: 'green_square',
colorClassName: 'from-emerald-300 to-green-600',
label: '方',
},
{
itemTypeId: 'blue-star',
visualKey: 'blue_star',
colorClassName: 'from-sky-300 to-blue-600',
label: '星',
},
{
itemTypeId: 'orange-hexagon',
visualKey: 'orange_hexagon',
colorClassName: 'from-orange-300 to-orange-600',
label: '六',
},
{
itemTypeId: 'cyan-capsule',
visualKey: 'cyan_capsule',
colorClassName: 'from-cyan-300 to-teal-600',
label: '胶',
},
{
itemTypeId: 'pink-heart',
visualKey: 'pink_heart',
colorClassName: 'from-pink-300 to-rose-500',
label: '心',
},
{
itemTypeId: 'lime-leaf',
visualKey: 'lime_leaf',
colorClassName: 'from-lime-300 to-lime-600',
label: '叶',
},
{
itemTypeId: 'white-moon',
visualKey: 'white_moon',
colorClassName: 'from-slate-100 to-slate-400',
label: '月',
},
];
@@ -117,8 +196,11 @@ function buildItem(
const angle = index * 0.86 + copyIndex * 0.22;
const spread = 0.16 + (ring % 4) * 0.085;
const x = 0.5 + Math.cos(angle) * spread + ((index % 3) - 1) * 0.026;
const y = 0.5 + Math.sin(angle * 1.13) * spread + ((copyIndex % 3) - 1) * 0.02;
const radius = 0.072 - Math.min(0.018, ring * 0.004) + (copyIndex % 2) * 0.004;
const y =
0.5 + Math.sin(angle * 1.13) * spread + ((copyIndex % 3) - 1) * 0.02;
const baseRadius =
0.072 - Math.min(0.018, ring * 0.004) + (copyIndex % 2) * 0.004;
const radius = baseRadius * (seed.sizeScale ?? 1);
return {
itemInstanceId: `${seed.itemTypeId}-${copyIndex + 1}`,
itemTypeId: seed.itemTypeId,
@@ -142,7 +224,10 @@ function recomputeClickable(items: Match3DItemSnapshot[]) {
};
}
const coveredByHigherLayer = boardItems.some((other) => {
if (other.itemInstanceId === item.itemInstanceId || other.layer <= item.layer) {
if (
other.itemInstanceId === item.itemInstanceId ||
other.layer <= item.layer
) {
return false;
}
const distance = Math.hypot(other.x - item.x, other.y - item.y);
@@ -173,7 +258,9 @@ function resolveRunStatus(run: Match3DRunSnapshot): Match3DRunSnapshot {
remainingMs: Math.max(0, run.remainingMs),
};
}
const trayIsFull = run.traySlots.every((slot) => Boolean(slot.itemInstanceId));
const trayIsFull = run.traySlots.every((slot) =>
Boolean(slot.itemInstanceId),
);
if (trayIsFull) {
return {
...run,
@@ -202,7 +289,9 @@ function settleMatchedTrayItems(run: Match3DRunSnapshot) {
]);
}
const matchedSlots = [...slotsByType.values()].find((slots) => slots.length >= 3);
const matchedSlots = [...slotsByType.values()].find(
(slots) => slots.length >= 3,
);
if (!matchedSlots) {
return {
run,
@@ -213,7 +302,9 @@ function settleMatchedTrayItems(run: Match3DRunSnapshot) {
const clearedItemInstanceIds = matchedSlots
.slice(0, 3)
.map((slot) => slot.itemInstanceId)
.filter((itemInstanceId): itemInstanceId is string => Boolean(itemInstanceId));
.filter((itemInstanceId): itemInstanceId is string =>
Boolean(itemInstanceId),
);
const clearedSet = new Set(clearedItemInstanceIds);
const nextRun = {
...run,
@@ -241,11 +332,17 @@ function settleMatchedTrayItems(run: Match3DRunSnapshot) {
export function startLocalMatch3DRun(clearCount = 12): Match3DRunSnapshot {
const normalizedClearCount = Math.max(1, Math.round(clearCount));
const typeCount = Math.min(MATCH3D_VISUAL_SEEDS.length, normalizedClearCount);
const typeCount = Math.min(10, normalizedClearCount);
const items = Array.from({ length: normalizedClearCount }, (_, clearIndex) =>
Array.from({ length: 3 }, (_, copyOffset) => {
const seed = MATCH3D_VISUAL_SEEDS[clearIndex % typeCount] ?? MATCH3D_VISUAL_SEEDS[0]!;
return buildItem(seed, clearIndex * 3 + copyOffset, clearIndex * 3 + copyOffset);
const seed =
MATCH3D_VISUAL_SEEDS[clearIndex % typeCount] ??
MATCH3D_VISUAL_SEEDS[0]!;
return buildItem(
seed,
clearIndex * 3 + copyOffset,
clearIndex * 3 + copyOffset,
);
}),
).flat();
const nowMs = Date.now();
@@ -274,7 +371,9 @@ export function buildLocalMatch3DOptimisticRun(
run: Match3DRunSnapshot,
itemInstanceId: string,
): Match3DRunSnapshot {
const targetItem = run.items.find((item) => item.itemInstanceId === itemInstanceId);
const targetItem = run.items.find(
(item) => item.itemInstanceId === itemInstanceId,
);
const nextTrayIndex = findNextTrayIndex(run.traySlots);
if (!targetItem || targetItem.state !== 'InBoard' || nextTrayIndex < 0) {
return run;
@@ -397,7 +496,9 @@ export async function confirmLocalMatch3DClick(
};
}
export function stopLocalMatch3DRun(run: Match3DRunSnapshot): Match3DRunSnapshot {
export function stopLocalMatch3DRun(
run: Match3DRunSnapshot,
): Match3DRunSnapshot {
if (run.status !== 'Running') {
return run;
}