1
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||
import {
|
||||
confirmLocalMatch3DClick,
|
||||
resolveLocalMatch3DItemTypeCount,
|
||||
startLocalMatch3DRun,
|
||||
} from '../../services/match3d-runtime';
|
||||
import {
|
||||
@@ -79,7 +80,10 @@ afterEach(() => {
|
||||
).__MATCH3D_KEEP_3D_TEST_RENDER__;
|
||||
});
|
||||
|
||||
function renderRuntime(run: Match3DRunSnapshot) {
|
||||
function renderRuntime(
|
||||
run: Match3DRunSnapshot,
|
||||
generatedItemAssets: Match3DGeneratedItemAsset[] = [],
|
||||
) {
|
||||
let currentRun = run;
|
||||
let authorityRun = run;
|
||||
const onClickItem = vi.fn(async (payload: Match3DClickItemRequest) => {
|
||||
@@ -92,6 +96,7 @@ function renderRuntime(run: Match3DRunSnapshot) {
|
||||
rerender(
|
||||
<Match3DRuntimeShell
|
||||
run={currentRun}
|
||||
generatedItemAssets={generatedItemAssets}
|
||||
onBack={vi.fn()}
|
||||
onRestart={vi.fn()}
|
||||
onOptimisticRunChange={onOptimisticRunChange}
|
||||
@@ -102,6 +107,7 @@ function renderRuntime(run: Match3DRunSnapshot) {
|
||||
const { rerender } = render(
|
||||
<Match3DRuntimeShell
|
||||
run={currentRun}
|
||||
generatedItemAssets={generatedItemAssets}
|
||||
onBack={vi.fn()}
|
||||
onRestart={vi.fn()}
|
||||
onOptimisticRunChange={onOptimisticRunChange}
|
||||
@@ -122,7 +128,7 @@ test('展示圆形空间和 7 格备选栏', () => {
|
||||
});
|
||||
|
||||
test('显示层把可消除物整体半径放大 2 倍且保留相对比例', () => {
|
||||
const run = startLocalMatch3DRun(25);
|
||||
const run = startLocalMatch3DRun(21);
|
||||
const firstItemByType = [...new Map(
|
||||
run.items.map((item) => [item.itemTypeId, item]),
|
||||
).values()];
|
||||
@@ -159,12 +165,7 @@ test('点击可见物品后先乐观入槽再等待确认', async () => {
|
||||
await waitFor(() => expect(onOptimisticRunChange).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
test('3D 模式下备选栏使用共享模型预览层,避免挤占中心棋盘上下文', () => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
__MATCH3D_KEEP_3D_TEST_RENDER__?: boolean;
|
||||
}
|
||||
).__MATCH3D_KEEP_3D_TEST_RENDER__ = true;
|
||||
test('运行态按物品实例稳定使用生成 2D 视角素材', () => {
|
||||
const run = startLocalMatch3DRun(1);
|
||||
const selectedItem = run.items[0]!;
|
||||
const nextRun: Match3DRunSnapshot = {
|
||||
@@ -187,13 +188,31 @@ test('3D 模式下备选栏使用共享模型预览层,避免挤占中心棋
|
||||
itemTypeId: selectedItem.itemTypeId,
|
||||
visualKey: selectedItem.visualKey,
|
||||
}
|
||||
: slot,
|
||||
: slot,
|
||||
),
|
||||
};
|
||||
const generatedItemAssets: Match3DGeneratedItemAsset[] = [
|
||||
{
|
||||
itemId: 'match3d-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc: null,
|
||||
imageObjectKey: null,
|
||||
imageViews: [1, 2, 3, 4, 5].map((viewIndex) => ({
|
||||
viewId: `view-${viewIndex}`,
|
||||
viewIndex,
|
||||
imageSrc: `/match3d/strawberry-view-${viewIndex}.png`,
|
||||
imageObjectKey: null,
|
||||
})),
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
status: 'image_ready',
|
||||
},
|
||||
];
|
||||
|
||||
renderRuntime(nextRun);
|
||||
renderRuntime(nextRun, generatedItemAssets);
|
||||
|
||||
expect(screen.getByTestId('match3d-tray-model-board')).toBeTruthy();
|
||||
const trayImage = screen.getByTestId('match3d-tray-image') as HTMLImageElement;
|
||||
expect(trayImage.src).toContain('/match3d/strawberry-view-');
|
||||
});
|
||||
|
||||
test('3D WebGL 画布锁定 CSS 尺寸,避免高 DPR 手机上溢出中心棋盘', () => {
|
||||
@@ -283,19 +302,81 @@ test('生成模型按运行态类型编号映射,并兼容仅 modelObjectKey
|
||||
);
|
||||
});
|
||||
|
||||
test('本地试玩按消除次数生成类型并在 25 类封顶', () => {
|
||||
test('运行态会先换签 generated 图片素材再渲染局内物品', async () => {
|
||||
const run = startLocalMatch3DRun(3);
|
||||
const generatedItemAssets: Match3DGeneratedItemAsset[] = [
|
||||
{
|
||||
itemId: 'match3d-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc: null,
|
||||
imageObjectKey: null,
|
||||
imageViews: [1, 2, 3, 4, 5].map((viewIndex) => ({
|
||||
viewId: `view-${viewIndex}`,
|
||||
viewIndex,
|
||||
imageSrc: null,
|
||||
imageObjectKey:
|
||||
`generated-match3d-assets/session/profile/items/match3d-item-1/views/view-${viewIndex}.png`,
|
||||
})),
|
||||
status: 'image_ready',
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
},
|
||||
];
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
read: {
|
||||
signedUrl: 'https://oss.example.com/match3d-view.png',
|
||||
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(
|
||||
<Match3DRuntimeShell
|
||||
run={run}
|
||||
generatedItemAssets={generatedItemAssets}
|
||||
onBack={vi.fn()}
|
||||
onRestart={vi.fn()}
|
||||
onOptimisticRunChange={vi.fn()}
|
||||
onClickItem={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('match3d-token-image').length).toBeGreaterThan(0);
|
||||
});
|
||||
expect(screen.getAllByTestId('match3d-token-image')[0]!.getAttribute('src')).toBe(
|
||||
'https://oss.example.com/match3d-view.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('本地试玩按难度档位生成类型并兼容历史硬核消除数', () => {
|
||||
const smallRun = startLocalMatch3DRun(12);
|
||||
const largeRun = startLocalMatch3DRun(100);
|
||||
const hardRun = startLocalMatch3DRun(20);
|
||||
const countTypes = (run: Match3DRunSnapshot) =>
|
||||
new Set(run.items.map((item) => item.itemTypeId)).size;
|
||||
|
||||
expect(countTypes(smallRun)).toBe(12);
|
||||
expect(countTypes(largeRun)).toBe(25);
|
||||
expect(largeRun.items).toHaveLength(300);
|
||||
expect(resolveLocalMatch3DItemTypeCount(8)).toBe(3);
|
||||
expect(resolveLocalMatch3DItemTypeCount(12)).toBe(9);
|
||||
expect(resolveLocalMatch3DItemTypeCount(16)).toBe(15);
|
||||
expect(resolveLocalMatch3DItemTypeCount(20)).toBe(21);
|
||||
expect(resolveLocalMatch3DItemTypeCount(21)).toBe(21);
|
||||
expect(countTypes(smallRun)).toBe(9);
|
||||
expect(countTypes(hardRun)).toBe(21);
|
||||
expect(hardRun.clearCount).toBe(21);
|
||||
expect(hardRun.items).toHaveLength(63);
|
||||
});
|
||||
|
||||
test('25 次以内生成不重复积木视觉签名', () => {
|
||||
const run = startLocalMatch3DRun(25);
|
||||
test('硬核档位生成不重复积木视觉签名', () => {
|
||||
const run = startLocalMatch3DRun(21);
|
||||
const firstItemByType = new Map(
|
||||
run.items.map((item) => [item.itemTypeId, item]),
|
||||
);
|
||||
@@ -311,14 +392,14 @@ test('25 次以内生成不重复积木视觉签名', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(firstItemByType.size).toBe(25);
|
||||
expect(visualKeys.size).toBe(25);
|
||||
expect(signatures.size).toBe(25);
|
||||
expect(firstItemByType.size).toBe(21);
|
||||
expect(visualKeys.size).toBe(21);
|
||||
expect(signatures.size).toBe(21);
|
||||
});
|
||||
|
||||
test('积木池覆盖参考图里的特殊件', () => {
|
||||
const shapes = new Set(
|
||||
startLocalMatch3DRun(25).items.map((item) =>
|
||||
startLocalMatch3DRun(21).items.map((item) =>
|
||||
resolveGeometryAsset(item.visualKey).shape,
|
||||
),
|
||||
);
|
||||
@@ -342,8 +423,8 @@ test('3D 特殊积木件使用可辨认挤出轮廓而不是基础代理体', as
|
||||
}
|
||||
});
|
||||
|
||||
test('15 次消除时每种视觉模型只对应一次消除目标', () => {
|
||||
const run = startLocalMatch3DRun(15);
|
||||
test('进阶档位保持 15 种视觉模型并按三消组复用', () => {
|
||||
const run = startLocalMatch3DRun(16);
|
||||
const countByVisualKey = new Map<string, number>();
|
||||
const typeByVisualKey = new Map<string, Set<string>>();
|
||||
|
||||
@@ -357,23 +438,26 @@ test('15 次消除时每种视觉模型只对应一次消除目标', () => {
|
||||
}
|
||||
|
||||
expect(countByVisualKey.size).toBe(15);
|
||||
expect([...countByVisualKey.values()]).toEqual(Array(15).fill(3));
|
||||
expect([...countByVisualKey.values()].sort((left, right) => left - right)).toEqual([
|
||||
...Array(14).fill(3),
|
||||
6,
|
||||
]);
|
||||
expect(
|
||||
[...typeByVisualKey.values()].every((itemTypeIds) => itemTypeIds.size === 1),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('25 次以内的随机抽取不会刷新重复物品', () => {
|
||||
for (const clearCount of [1, 12, 15, 24, 25]) {
|
||||
test('随机抽取不会刷新重复物品', () => {
|
||||
for (const clearCount of [1, 8, 12, 16, 21]) {
|
||||
const run = startLocalMatch3DRun(clearCount);
|
||||
const visualKeys = new Set(run.items.map((item) => item.visualKey));
|
||||
|
||||
expect(visualKeys.size).toBe(clearCount);
|
||||
expect(visualKeys.size).toBe(resolveLocalMatch3DItemTypeCount(clearCount));
|
||||
}
|
||||
});
|
||||
|
||||
test('25 类型局面按五档体积比例生成尺寸', () => {
|
||||
const run = startLocalMatch3DRun(25);
|
||||
test('硬核档位按五档体积比例生成尺寸', () => {
|
||||
const run = startLocalMatch3DRun(21);
|
||||
const radiusByVisualKey = new Map<string, number>();
|
||||
for (const item of run.items) {
|
||||
radiusByVisualKey.set(item.visualKey, item.radius);
|
||||
@@ -400,15 +484,15 @@ test('25 类型局面按五档体积比例生成尺寸', () => {
|
||||
tierCounts.set(tier, (tierCounts.get(tier) ?? 0) + 1);
|
||||
}
|
||||
|
||||
expect(tierCounts.get('XL')).toBe(5);
|
||||
expect(tierCounts.get('L')).toBe(8);
|
||||
expect(tierCounts.get('M')).toBe(7);
|
||||
expect(tierCounts.get('XS')).toBe(4);
|
||||
expect(tierCounts.get('XL')).toBe(4);
|
||||
expect(tierCounts.get('L')).toBe(7);
|
||||
expect(tierCounts.get('M')).toBe(6);
|
||||
expect(tierCounts.get('XS')).toBe(3);
|
||||
expect(tierCounts.get('S')).toBe(1);
|
||||
});
|
||||
|
||||
test('同一视觉模型在复用时保持唯一尺寸', () => {
|
||||
const run = startLocalMatch3DRun(30);
|
||||
const run = startLocalMatch3DRun(21);
|
||||
const radiiByVisualKey = new Map<string, Set<number>>();
|
||||
|
||||
for (const item of run.items) {
|
||||
@@ -417,13 +501,13 @@ test('同一视觉模型在复用时保持唯一尺寸', () => {
|
||||
radiiByVisualKey.set(item.visualKey, radii);
|
||||
}
|
||||
|
||||
expect(radiiByVisualKey.size).toBe(25);
|
||||
expect(radiiByVisualKey.size).toBe(21);
|
||||
expect([...radiiByVisualKey.values()].every((radii) => radii.size === 1)).toBe(true);
|
||||
});
|
||||
|
||||
test('托盘 3D 预览保留场内模型的相对尺寸比例', async () => {
|
||||
const three = await import('three');
|
||||
const run = startLocalMatch3DRun(25);
|
||||
const run = startLocalMatch3DRun(21);
|
||||
const firstItemByType = [...new Map(
|
||||
run.items.map((item) => [item.itemTypeId, item]),
|
||||
).values()];
|
||||
|
||||
Reference in New Issue
Block a user