fix: stabilize match3d demo discovery
This commit is contained in:
@@ -447,7 +447,10 @@ function settleMatchedTrayItems(
|
||||
};
|
||||
}
|
||||
|
||||
export function startLocalMatch3DRun(clearCount = 12): Match3DRunSnapshot {
|
||||
export function startLocalMatch3DRun(
|
||||
clearCount = 12,
|
||||
profileId = 'local-match3d-profile',
|
||||
): Match3DRunSnapshot {
|
||||
const normalizedClearCount =
|
||||
normalizeLocalMatch3DRuntimeClearCount(clearCount);
|
||||
const selectedSeeds = selectVisualSeeds(normalizedClearCount);
|
||||
@@ -467,7 +470,7 @@ export function startLocalMatch3DRun(clearCount = 12): Match3DRunSnapshot {
|
||||
const nowMs = Date.now();
|
||||
return {
|
||||
runId: `local-match3d-run-${nowMs}`,
|
||||
profileId: 'local-match3d-profile',
|
||||
profileId,
|
||||
status: 'Running',
|
||||
snapshotVersion: 1,
|
||||
startedAtMs: nowMs,
|
||||
|
||||
@@ -95,7 +95,7 @@ test('local Match3D runtime adapter exposes the same runtime seam as the server
|
||||
const started = await adapter.startRun('ignored-local-profile');
|
||||
const clickableItem = started.run.items.find((item) => item.clickable);
|
||||
|
||||
expect(started.run.profileId).toBe('local-match3d-profile');
|
||||
expect(started.run.profileId).toBe('ignored-local-profile');
|
||||
expect(clickableItem).toBeTruthy();
|
||||
|
||||
const clickResult = await adapter.clickItem(started.run.runId, {
|
||||
@@ -117,6 +117,15 @@ test('local Match3D runtime adapter exposes the same runtime seam as the server
|
||||
expect(stopped.run.status).toBe('Stopped');
|
||||
});
|
||||
|
||||
test('local Match3D runtime adapter keeps the requested profile id on restart', async () => {
|
||||
const adapter = createLocalMatch3DRuntimeAdapter({ clearCount: 1 });
|
||||
const started = await adapter.startRun('match3d-demo-20260525');
|
||||
const restarted = await adapter.restartRun(started.run.runId);
|
||||
|
||||
expect(started.run.profileId).toBe('match3d-demo-20260525');
|
||||
expect(restarted.run.profileId).toBe('match3d-demo-20260525');
|
||||
});
|
||||
|
||||
test('local Match3D runtime adapter keeps authority run local to the adapter', async () => {
|
||||
const adapter = createLocalMatch3DRuntimeAdapter({ initialRun: startLocalMatch3DRun(1) });
|
||||
const first = await adapter.getRun('unused-run-id');
|
||||
|
||||
@@ -36,6 +36,7 @@ export type Match3DRuntimeAdapter = {
|
||||
|
||||
export type LocalMatch3DRuntimeAdapterOptions = {
|
||||
clearCount?: number;
|
||||
profileId?: string;
|
||||
initialRun?: Match3DRunResponse['run'];
|
||||
};
|
||||
|
||||
@@ -74,11 +75,16 @@ export function createServerMatch3DRuntimeAdapter(
|
||||
export function createLocalMatch3DRuntimeAdapter(
|
||||
options: LocalMatch3DRuntimeAdapterOptions = {},
|
||||
): Match3DRuntimeAdapter {
|
||||
let authorityRun = options.initialRun ?? startLocalMatch3DRun(options.clearCount);
|
||||
let authorityRun =
|
||||
options.initialRun ??
|
||||
startLocalMatch3DRun(options.clearCount, options.profileId);
|
||||
|
||||
return {
|
||||
async startRun() {
|
||||
authorityRun = startLocalMatch3DRun(options.clearCount);
|
||||
async startRun(profileId) {
|
||||
authorityRun = startLocalMatch3DRun(
|
||||
options.clearCount,
|
||||
profileId || options.profileId,
|
||||
);
|
||||
return { run: authorityRun };
|
||||
},
|
||||
async getRun() {
|
||||
@@ -91,7 +97,10 @@ export function createLocalMatch3DRuntimeAdapter(
|
||||
return result;
|
||||
},
|
||||
async restartRun() {
|
||||
authorityRun = startLocalMatch3DRun(options.clearCount);
|
||||
authorityRun = startLocalMatch3DRun(
|
||||
options.clearCount,
|
||||
authorityRun.profileId || options.profileId,
|
||||
);
|
||||
return { run: authorityRun };
|
||||
},
|
||||
async stopRun() {
|
||||
|
||||
@@ -36,6 +36,42 @@ describe('match3dSpritesheetParser', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('同一行图标高度错位时仍按行内横向顺序映射标签', () => {
|
||||
const width = 36;
|
||||
const height = 24;
|
||||
const alpha = new Uint8ClampedArray(width * height);
|
||||
const paint = (x0: number, y0: number, x1: number, y1: number) => {
|
||||
for (let y = y0; y <= y1; y += 1) {
|
||||
for (let x = x0; x <= x1; x += 1) {
|
||||
alpha[y * width + x] = 255;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
paint(2, 1, 5, 4);
|
||||
paint(12, 1, 15, 4);
|
||||
paint(22, 1, 25, 4);
|
||||
paint(2, 15, 5, 20);
|
||||
paint(12, 13, 15, 18);
|
||||
paint(22, 10, 25, 15);
|
||||
|
||||
const regions = detectMatch3DSpritesheetRegions({
|
||||
alpha,
|
||||
width,
|
||||
height,
|
||||
labels: ['返回', '设置', '方格', '移出', '凑齐', '打乱'],
|
||||
});
|
||||
|
||||
expect(regions.map((region) => `${region.label}:${region.x}`)).toEqual([
|
||||
'返回:2',
|
||||
'设置:12',
|
||||
'方格:22',
|
||||
'移出:2',
|
||||
'凑齐:12',
|
||||
'打乱:22',
|
||||
]);
|
||||
});
|
||||
|
||||
test('忽略小噪点,只返回可用矩形素材', () => {
|
||||
const width = 8;
|
||||
const height = 8;
|
||||
|
||||
@@ -79,8 +79,7 @@ export function detectMatch3DSpritesheetRegions({
|
||||
}
|
||||
}
|
||||
|
||||
return components
|
||||
.sort((left, right) => left.y - right.y || left.x - right.x)
|
||||
return sortMatch3DSpritesheetComponentsByRows(components)
|
||||
.map((component, index) => ({
|
||||
label: labels[index] ?? `素材${index + 1}`,
|
||||
x: component.x,
|
||||
@@ -90,6 +89,67 @@ export function detectMatch3DSpritesheetRegions({
|
||||
}));
|
||||
}
|
||||
|
||||
function sortMatch3DSpritesheetComponentsByRows(
|
||||
components: Match3DDetectedComponent[],
|
||||
) {
|
||||
const rows: Array<{
|
||||
top: number;
|
||||
bottom: number;
|
||||
components: Match3DDetectedComponent[];
|
||||
}> = [];
|
||||
|
||||
[...components]
|
||||
.sort(
|
||||
(left, right) =>
|
||||
resolveMatch3DSpritesheetComponentCenterY(left) -
|
||||
resolveMatch3DSpritesheetComponentCenterY(right) ||
|
||||
left.x - right.x,
|
||||
)
|
||||
.forEach((component) => {
|
||||
const row = rows.find((entry) =>
|
||||
isMatch3DSpritesheetComponentInRow(component, entry),
|
||||
);
|
||||
if (!row) {
|
||||
rows.push({
|
||||
top: component.y,
|
||||
bottom: component.y + component.height - 1,
|
||||
components: [component],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
row.top = Math.min(row.top, component.y);
|
||||
row.bottom = Math.max(row.bottom, component.y + component.height - 1);
|
||||
row.components.push(component);
|
||||
});
|
||||
|
||||
return rows
|
||||
.sort((left, right) => left.top - right.top)
|
||||
.flatMap((row) =>
|
||||
row.components.sort((left, right) => left.x - right.x || left.y - right.y),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMatch3DSpritesheetComponentCenterY(
|
||||
component: Match3DDetectedComponent,
|
||||
) {
|
||||
return component.y + component.height / 2;
|
||||
}
|
||||
|
||||
function isMatch3DSpritesheetComponentInRow(
|
||||
component: Match3DDetectedComponent,
|
||||
row: { top: number; bottom: number },
|
||||
) {
|
||||
const bottom = component.y + component.height - 1;
|
||||
if (component.y <= row.bottom && bottom >= row.top) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const gap =
|
||||
component.y > row.bottom ? component.y - row.bottom : row.top - bottom;
|
||||
return gap <= Math.max(2, component.height * 0.25);
|
||||
}
|
||||
|
||||
export function buildMatch3DItemSpritesheetViewRegions<
|
||||
Region extends Match3DSpritesheetRegion,
|
||||
>(
|
||||
|
||||
Reference in New Issue
Block a user