Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-04 02:33:15 +08:00
11 changed files with 1580 additions and 570 deletions

View File

@@ -8,156 +8,237 @@ import type {
const MATCH3D_TRAY_SLOT_COUNT = 7;
const MATCH3D_LOCAL_DURATION_MS = 600_000;
const MATCH3D_MAX_ITEM_TYPE_COUNT = 25;
const MATCH3D_LOCAL_BASE_RADIUS = 0.072;
type Match3DSizeTier = 'XL' | 'L' | 'M' | 'XS' | 'S';
type Match3DVisualSeed = {
itemTypeId: string;
visualKey: string;
colorClassName: string;
label: string;
sizeScale?: number;
};
type Match3DSelectedVisualSeed = Match3DVisualSeed & {
radiusScale: number;
relativeVolume: number;
sizeTier: Match3DSizeTier;
};
const MATCH3D_SIZE_TIER_RULES: Array<{
radiusScale: number;
ratio: number;
relativeVolume: number;
sizeTier: Match3DSizeTier;
}> = [
{ sizeTier: 'XL', ratio: 0.2, relativeVolume: 1.86, radiusScale: 1.23 },
{ sizeTier: 'L', ratio: 0.3, relativeVolume: 1.4, radiusScale: 1.12 },
{ sizeTier: 'M', ratio: 0.3, relativeVolume: 1, radiusScale: 1 },
{ sizeTier: 'XS', ratio: 0.15, relativeVolume: 0.73, radiusScale: 0.9 },
{ sizeTier: 'S', ratio: 0.05, relativeVolume: 0.44, radiusScale: 0.76 },
];
export const MATCH3D_VISUAL_SEEDS: Match3DVisualSeed[] = [
// 中文注释:水果题材内置视觉键要和后端 module-match3d 保持一致,避免不同物品被兜底成同一图案
// 中文注释:默认 25 类使用参考图中的积木件,形状、尺寸和颜色都要能区分
{
itemTypeId: 'watermelon',
visualKey: 'watermelon-green',
colorClassName: 'from-emerald-500 to-green-800',
label: '西瓜',
sizeScale: 1.24,
itemTypeId: 'block-red-2x4',
visualKey: 'block-red-2x4',
colorClassName: 'from-rose-400 to-red-700',
label: '红色二乘四',
},
{
itemTypeId: 'apple',
visualKey: 'apple-red',
colorClassName: 'from-rose-400 to-red-600',
label: '苹果',
sizeScale: 1,
itemTypeId: 'block-blue-1x2',
visualKey: 'block-blue-1x2',
colorClassName: 'from-blue-300 to-blue-700',
label: '蓝色一乘二',
},
{
itemTypeId: 'banana',
visualKey: 'banana-yellow',
colorClassName: 'from-yellow-300 to-amber-500',
label: '香蕉',
sizeScale: 1.04,
itemTypeId: 'block-yellow-2x2',
visualKey: 'block-yellow-2x2',
colorClassName: 'from-yellow-300 to-yellow-600',
label: '黄色二乘二',
},
{
itemTypeId: 'grape',
visualKey: 'grape-purple',
colorClassName: 'from-violet-400 to-purple-700',
label: '葡萄',
sizeScale: 0.78,
itemTypeId: 'block-green-1x4',
visualKey: 'block-green-1x4',
colorClassName: 'from-emerald-300 to-green-700',
label: '绿色一乘四',
},
{
itemTypeId: 'melon',
visualKey: 'melon-green',
colorClassName: 'from-emerald-300 to-green-600',
label: '甜瓜',
sizeScale: 1.12,
itemTypeId: 'block-orange-1x6',
visualKey: 'block-orange-1x6',
colorClassName: 'from-orange-300 to-orange-700',
label: '橙色一乘六',
},
{
itemTypeId: 'berry',
visualKey: 'berry-blue',
colorClassName: 'from-sky-300 to-blue-600',
label: '蓝莓',
sizeScale: 0.78,
itemTypeId: 'block-white-1x1',
visualKey: 'block-white-1x1',
colorClassName: 'from-slate-50 to-slate-300',
label: '白色一乘一',
},
{
itemTypeId: 'peach',
visualKey: 'peach-pink',
colorClassName: 'from-pink-300 to-orange-400',
label: '桃子',
sizeScale: 1,
itemTypeId: 'block-black-1x8',
visualKey: 'block-black-1x8',
colorClassName: 'from-zinc-700 to-black',
label: '黑色一乘八',
},
{
itemTypeId: 'plum',
visualKey: 'plum-indigo',
colorClassName: 'from-indigo-300 to-indigo-700',
label: '李子',
sizeScale: 0.86,
itemTypeId: 'block-tan-2x3',
visualKey: 'block-tan-2x3',
colorClassName: 'from-amber-100 to-yellow-600',
label: '米色二乘三',
},
{
itemTypeId: 'lime',
visualKey: 'lime-lime',
colorClassName: 'from-lime-300 to-lime-600',
label: '青柠',
sizeScale: 0.86,
itemTypeId: 'block-lime-1x2',
visualKey: 'block-lime-1x2',
colorClassName: 'from-lime-300 to-lime-700',
label: '青柠一乘二',
},
{
itemTypeId: 'orange',
visualKey: 'orange-orange',
colorClassName: 'from-orange-300 to-orange-600',
label: '橙子',
sizeScale: 1,
itemTypeId: 'block-darkred-2x2',
visualKey: 'block-darkred-2x2',
colorClassName: 'from-red-700 to-red-950',
label: '深红二乘二',
},
{
itemTypeId: 'pear',
visualKey: 'pear-cyan',
colorClassName: 'from-cyan-300 to-teal-600',
label: '',
sizeScale: 1,
itemTypeId: 'block-blue-1x4',
visualKey: 'block-blue-1x4',
colorClassName: 'from-sky-300 to-blue-700',
label: '蓝色一乘四',
},
{
itemTypeId: 'red-circle',
visualKey: 'red_circle',
colorClassName: 'from-rose-400 to-red-600',
label: '',
itemTypeId: 'block-pink-2x4',
visualKey: 'block-pink-2x4',
colorClassName: 'from-pink-300 to-pink-600',
label: '粉色二乘四',
},
{
itemTypeId: 'yellow-triangle',
visualKey: 'yellow_triangle',
colorClassName: 'from-yellow-300 to-amber-500',
label: '',
itemTypeId: 'block-gray-1x6',
visualKey: 'block-gray-1x6',
colorClassName: 'from-zinc-400 to-zinc-700',
label: '灰色一乘六',
},
{
itemTypeId: 'purple-diamond',
visualKey: 'purple_diamond',
colorClassName: 'from-violet-400 to-purple-700',
label: '',
itemTypeId: 'block-lavender-tile-2x2',
visualKey: 'block-lavender-tile-2x2',
colorClassName: 'from-purple-200 to-purple-500',
label: '薰衣草光板',
},
{
itemTypeId: 'green-square',
visualKey: 'green_square',
colorClassName: 'from-emerald-300 to-green-600',
label: '',
itemTypeId: 'block-teal-tile-1x3',
visualKey: 'block-teal-tile-1x3',
colorClassName: 'from-teal-300 to-teal-700',
label: '青色长光板',
},
{
itemTypeId: 'blue-star',
visualKey: 'blue_star',
colorClassName: 'from-sky-300 to-blue-600',
label: '',
itemTypeId: 'block-mint-tile-1x4',
visualKey: 'block-mint-tile-1x4',
colorClassName: 'from-emerald-100 to-emerald-400',
label: '薄荷长光板',
},
{
itemTypeId: 'orange-hexagon',
visualKey: 'orange_hexagon',
colorClassName: 'from-orange-300 to-orange-600',
label: '',
itemTypeId: 'block-magenta-tile-2x2',
visualKey: 'block-magenta-tile-2x2',
colorClassName: 'from-fuchsia-500 to-pink-800',
label: '洋红光板',
},
{
itemTypeId: 'cyan-capsule',
visualKey: 'cyan_capsule',
colorClassName: 'from-cyan-300 to-teal-600',
label: '',
itemTypeId: 'block-orange-tile-2x2-stud',
visualKey: 'block-orange-tile-2x2-stud',
colorClassName: 'from-orange-300 to-amber-700',
label: '橙色单钉板',
},
{
itemTypeId: 'pink-heart',
visualKey: 'pink_heart',
colorClassName: 'from-pink-300 to-rose-500',
label: '',
itemTypeId: 'block-purple-slope-1x2',
visualKey: 'block-purple-slope-1x2',
colorClassName: 'from-violet-400 to-violet-900',
label: '紫色斜坡',
},
{
itemTypeId: 'lime-leaf',
visualKey: 'lime_leaf',
colorClassName: 'from-lime-300 to-lime-600',
label: '',
itemTypeId: 'block-brown-slope-1x2',
visualKey: 'block-brown-slope-1x2',
colorClassName: 'from-orange-900 to-stone-700',
label: '棕色斜坡',
},
{
itemTypeId: 'white-moon',
visualKey: 'white_moon',
colorClassName: 'from-slate-100 to-slate-400',
label: '',
itemTypeId: 'block-sky-slope-2x2',
visualKey: 'block-sky-slope-2x2',
colorClassName: 'from-sky-300 to-sky-600',
label: '天蓝斜坡',
},
{
itemTypeId: 'block-green-cylinder',
visualKey: 'block-green-cylinder',
colorClassName: 'from-green-400 to-green-800',
label: '绿色圆柱',
},
{
itemTypeId: 'block-clear-ring',
visualKey: 'block-clear-ring',
colorClassName: 'from-slate-50 to-slate-300',
label: '透明圆环',
},
{
itemTypeId: 'block-mint-arch',
visualKey: 'block-mint-arch',
colorClassName: 'from-emerald-100 to-emerald-300',
label: '薄荷拱门',
},
{
itemTypeId: 'block-gold-cone',
visualKey: 'block-gold-cone',
colorClassName: 'from-yellow-300 to-amber-700',
label: '金色锥形件',
},
];
function hashNumber(value: number) {
let state = Math.max(1, value >>> 0);
state ^= state << 13;
state ^= state >>> 7;
state ^= state << 17;
return state >>> 0;
}
function resolveSizeTierPlan(typeCount: number) {
const baseCounts = MATCH3D_SIZE_TIER_RULES.map((rule) => ({
...rule,
count: Math.floor(typeCount * rule.ratio),
remainder: typeCount * rule.ratio - Math.floor(typeCount * rule.ratio),
}));
let assignedCount = baseCounts.reduce((sum, rule) => sum + rule.count, 0);
const remainderOrder = [...baseCounts].sort(
(left, right) => right.remainder - left.remainder,
);
let cursor = 0;
while (assignedCount < typeCount) {
remainderOrder[cursor % remainderOrder.length]!.count += 1;
assignedCount += 1;
cursor += 1;
}
return baseCounts.flatMap((rule) => Array(rule.count).fill(rule));
}
function selectVisualSeeds(clearCount: number): Match3DSelectedVisualSeed[] {
const typeCount = Math.min(MATCH3D_MAX_ITEM_TYPE_COUNT, clearCount);
const seeds = [...MATCH3D_VISUAL_SEEDS];
let state = hashNumber(clearCount * 2_654_435_761);
for (let index = seeds.length - 1; index > 0; index -= 1) {
state = hashNumber(state + index);
const swapIndex = state % (index + 1);
[seeds[index], seeds[swapIndex]] = [seeds[swapIndex]!, seeds[index]!];
}
const sizeTierPlan = resolveSizeTierPlan(typeCount);
return seeds.slice(0, typeCount).map((seed, index) => ({
...seed,
radiusScale: sizeTierPlan[index]!.radiusScale,
relativeVolume: sizeTierPlan[index]!.relativeVolume,
sizeTier: sizeTierPlan[index]!.sizeTier,
}));
}
function createEmptyTray(): Match3DTraySlot[] {
return Array.from({ length: MATCH3D_TRAY_SLOT_COUNT }, (_, slotIndex) => ({
slotIndex,
@@ -188,7 +269,7 @@ function normalizeRemainingMs(run: Match3DRunSnapshot, nowMs = Date.now()) {
}
function buildItem(
seed: Match3DVisualSeed,
seed: Match3DSelectedVisualSeed,
index: number,
copyIndex: number,
): Match3DItemSnapshot {
@@ -198,9 +279,7 @@ function buildItem(
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 baseRadius =
0.072 - Math.min(0.018, ring * 0.004) + (copyIndex % 2) * 0.004;
const radius = baseRadius * (seed.sizeScale ?? 1);
const radius = MATCH3D_LOCAL_BASE_RADIUS * seed.radiusScale;
return {
itemInstanceId: `${seed.itemTypeId}-${copyIndex + 1}`,
itemTypeId: seed.itemTypeId,
@@ -332,12 +411,12 @@ function settleMatchedTrayItems(run: Match3DRunSnapshot) {
export function startLocalMatch3DRun(clearCount = 12): Match3DRunSnapshot {
const normalizedClearCount = Math.max(1, Math.round(clearCount));
const typeCount = Math.min(10, normalizedClearCount);
const selectedSeeds = selectVisualSeeds(normalizedClearCount);
const items = Array.from({ length: normalizedClearCount }, (_, clearIndex) =>
Array.from({ length: 3 }, (_, copyOffset) => {
const seed =
MATCH3D_VISUAL_SEEDS[clearIndex % typeCount] ??
MATCH3D_VISUAL_SEEDS[0]!;
selectedSeeds[clearIndex % selectedSeeds.length] ??
selectedSeeds[0]!;
return buildItem(
seed,
clearIndex * 3 + copyOffset,