feat: add puzzle onboarding and match3d entry updates
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-07 23:30:54 +08:00
parent df80876f60
commit e8fee0172a
27 changed files with 1802 additions and 68 deletions

View File

@@ -25,6 +25,13 @@ import {
createMatch3DThreeGeometry,
measureMatch3DItemPreviewDimension,
resolveMatch3DColliderBounds,
resolveMatch3DBoardDepthPlan,
resolveMatch3DBoundaryRadius,
resolveMatch3DPhysicsStabilityPlan,
resolveMatch3DSpawnTimingPlan,
resolveMatch3DStackTargetY,
resolveMatch3DSpawnDelay,
resolveMatch3DSpawnY,
resolveMatch3DTrayPreviewRotation,
resolveMatch3DTrayPreviewReferenceDimension,
resolveMatch3DTrayPreviewScale,
@@ -447,6 +454,143 @@ test('3D 物体碰撞体按同款视觉尺寸生成', async () => {
).toBeCloseTo(cylinderBounds.height);
});
test('中心场地 3D 纵深随物体总量增加并随消除进度回补', () => {
const smallDepthPlan = resolveMatch3DBoardDepthPlan(30, 30);
const largeDepthPlan = resolveMatch3DBoardDepthPlan(300, 300);
const earlyBottomY = resolveMatch3DStackTargetY(300, 300, 0);
const lateBottomY = resolveMatch3DStackTargetY(300, 60, 0);
expect(largeDepthPlan.initialDepth).toBeGreaterThan(
smallDepthPlan.initialDepth,
);
expect(largeDepthPlan.layerCapacity).toBeLessThan(
smallDepthPlan.layerCapacity,
);
expect(largeDepthPlan.layerCount).toBeGreaterThan(
smallDepthPlan.layerCount,
);
expect(largeDepthPlan.surfaceY).toBeGreaterThan(largeDepthPlan.baseY);
expect(lateBottomY).toBeGreaterThan(earlyBottomY);
expect(lateBottomY).toBeLessThanOrEqual(largeDepthPlan.surfaceY);
});
test('高数量 3D 局面使用更稳定的物理参数', () => {
const smallPlan = resolveMatch3DPhysicsStabilityPlan(30);
const largePlan = resolveMatch3DPhysicsStabilityPlan(300);
expect(largePlan.contactFriction).toBeGreaterThan(
smallPlan.contactFriction,
);
expect(largePlan.contactRestitution).toBeLessThan(
smallPlan.contactRestitution,
);
expect(largePlan.linearDamping).toBeGreaterThan(smallPlan.linearDamping);
expect(largePlan.angularDamping).toBeGreaterThan(smallPlan.angularDamping);
expect(largePlan.solverIterations).toBeGreaterThan(
smallPlan.solverIterations,
);
expect(largePlan.maxHorizontalSpeed).toBeLessThan(
smallPlan.maxHorizontalSpeed,
);
});
test('3D 真实边界半径比视觉半径更保守,避免长条贴边穿出锅壁', () => {
const longBrick = resolveGeometryAsset('block-black-1x8');
const radius = 1;
const boundaryRadius = resolveMatch3DBoundaryRadius(longBrick, radius);
const visualRadius = Math.hypot(
resolveMatch3DColliderBounds(longBrick, radius).width / 2,
resolveMatch3DColliderBounds(longBrick, radius).depth / 2,
);
expect(boundaryRadius).toBeCloseTo(visualRadius);
expect(boundaryRadius).toBeGreaterThan(2.4);
});
test('100 次局面的新物体会按层级延迟生成并逐层回落', () => {
const fastTimingPlan = resolveMatch3DSpawnTimingPlan(29);
const smallDepthPlan = resolveMatch3DBoardDepthPlan(30, 30);
const largeDepthPlan = resolveMatch3DBoardDepthPlan(300, 300);
const smallTimingPlan = resolveMatch3DSpawnTimingPlan(30);
const largeTimingPlan = resolveMatch3DSpawnTimingPlan(300);
const bottomDelay = resolveMatch3DSpawnDelay(0, largeDepthPlan.layerCapacity);
const middleDelay = resolveMatch3DSpawnDelay(30, largeDepthPlan.layerCapacity);
const topDelay = resolveMatch3DSpawnDelay(120, largeDepthPlan.layerCapacity);
const dynamicCapacityDelay = resolveMatch3DSpawnDelay(
120,
largeDepthPlan.layerCapacity,
);
const defaultCapacityDelay = resolveMatch3DSpawnDelay(
120,
smallDepthPlan.layerCapacity,
);
expect(bottomDelay).toBe(0);
expect(middleDelay).toBeGreaterThan(bottomDelay);
expect(topDelay).toBeGreaterThan(middleDelay);
expect(dynamicCapacityDelay).toBeGreaterThan(defaultCapacityDelay);
expect(smallTimingPlan.frameSpawnLimit).toBeLessThan(
fastTimingPlan.frameSpawnLimit,
);
expect(smallTimingPlan.burstSize).toBeLessThan(fastTimingPlan.burstSize);
expect(smallTimingPlan.layerDelayMs).toBeGreaterThan(
fastTimingPlan.layerDelayMs,
);
expect(
resolveMatch3DSpawnDelay(29, smallDepthPlan.layerCapacity, smallTimingPlan),
).toBeGreaterThan(450);
expect(largeTimingPlan.initialDelayMs).toBeGreaterThan(
smallTimingPlan.initialDelayMs,
);
expect(largeTimingPlan.frameSpawnLimit).toBeLessThan(
smallTimingPlan.frameSpawnLimit,
);
expect(largeTimingPlan.burstSize).toBeLessThanOrEqual(6);
expect(largeTimingPlan.layerDelayMs).toBeGreaterThanOrEqual(
smallTimingPlan.layerDelayMs,
);
expect(
resolveMatch3DSpawnDelay(299, largeDepthPlan.layerCapacity, largeTimingPlan),
).toBeGreaterThan(5000);
});
test('3D 新物体生成高度会避让同位置已有堆叠', () => {
const plannedSpawnY = 2;
const raisedSpawnY = resolveMatch3DSpawnY(
plannedSpawnY,
0.8,
0.7,
{ x: 0.1, z: 0.1 },
[
{
boundaryRadius: 0.7,
colliderHeight: 0.9,
x: 0.18,
y: 2.4,
z: 0.15,
},
],
);
const unchangedSpawnY = resolveMatch3DSpawnY(
plannedSpawnY,
0.8,
0.7,
{ x: 0.1, z: 0.1 },
[
{
boundaryRadius: 0.7,
colliderHeight: 0.9,
x: 3,
y: 4,
z: 3,
},
],
);
expect(raisedSpawnY).toBeGreaterThan(plannedSpawnY);
expect(unchangedSpawnY).toBe(plannedSpawnY);
});
test('积木视觉键不会被统一兜底成红色苹字', () => {
const run = startLocalMatch3DRun(2);
run.items = run.items.slice(0, 2).map((item, index) => ({