Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user