Add generationStatus and match3d/runtime fixes
Introduce persistent generationStatus to work summaries (puzzle & match3d) and propagate generation recovery rules across docs and frontend/backends so "generating" is restored from server-side work summary rather than ephemeral front-end notices. Update API server image/asset handling (improve match3d material sheet green/alpha decontamination and promote generatedItemAssets background fields) and add runtime improvements: alpha-based hotspot hit-testing, tray insertion/three-match animation behavior, and session re-read on client-side VectorEngine timeouts/lock-screen interruptions. Many docs, tests and related frontend modules updated/added to reflect these contract and behavior changes.
This commit is contained in:
@@ -603,6 +603,53 @@ describe('puzzleLocalRuntime', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('本地试玩直达后续关卡时继承作品 UI 背景', () => {
|
||||
const workWithLevels: PuzzleWorkSummary = {
|
||||
...baseWork,
|
||||
levels: [
|
||||
{
|
||||
levelId: 'puzzle-level-1',
|
||||
levelName: '第一关',
|
||||
pictureDescription: '第一关画面',
|
||||
candidates: [],
|
||||
selectedCandidateId: null,
|
||||
coverImageSrc: '/level-1.png',
|
||||
coverAssetId: null,
|
||||
uiBackgroundImageSrc:
|
||||
'/generated-puzzle-assets/session/ui/background.png',
|
||||
uiBackgroundImageObjectKey:
|
||||
'generated-puzzle-assets/session/ui/background.png',
|
||||
backgroundMusic: null,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
{
|
||||
levelId: 'puzzle-level-2',
|
||||
levelName: '第二关',
|
||||
pictureDescription: '第二关画面',
|
||||
candidates: [],
|
||||
selectedCandidateId: null,
|
||||
coverImageSrc: '/level-2.png',
|
||||
coverAssetId: null,
|
||||
uiBackgroundImageSrc: null,
|
||||
uiBackgroundImageObjectKey: null,
|
||||
backgroundMusic: null,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const run = startLocalPuzzleRun(workWithLevels, 'puzzle-level-2');
|
||||
|
||||
expect(run.currentLevel?.levelId).toBe('puzzle-level-2');
|
||||
expect(run.currentLevel?.coverImageSrc).toBe('/level-2.png');
|
||||
expect(run.currentLevel?.uiBackgroundImageSrc).toBe(
|
||||
'/generated-puzzle-assets/session/ui/background.png',
|
||||
);
|
||||
expect(run.currentLevel?.uiBackgroundImageObjectKey).toBe(
|
||||
'generated-puzzle-assets/session/ui/background.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('暂停和冻结时间不会消耗本地倒计时', () => {
|
||||
const run = startLocalPuzzleRun(baseWork);
|
||||
const pausedRun = setLocalPuzzlePaused(
|
||||
|
||||
@@ -12,7 +12,10 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
||||
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import { resolvePuzzleUiBackgroundSource } from './puzzleUiBackgroundSource';
|
||||
import {
|
||||
resolvePuzzleUiBackgroundFields,
|
||||
resolvePuzzleUiBackgroundSource,
|
||||
} from './puzzleUiBackgroundSource';
|
||||
|
||||
const LOCAL_PUZZLE_RUN_ID_PREFIX = 'local-puzzle-run-';
|
||||
const PUZZLE_FREEZE_TIME_DURATION_MS = 10_000;
|
||||
@@ -761,6 +764,15 @@ function resolveNextSameWorkLevel(
|
||||
return levels[nextLevelIndex] ?? null;
|
||||
}
|
||||
|
||||
function resolvePuzzleWorkUiBackgroundCarrier(
|
||||
work: PuzzleWorkSummary | null | undefined,
|
||||
) {
|
||||
return (
|
||||
work?.levels?.find((level) => resolvePuzzleUiBackgroundSource(level)) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function applyLocalNextLevelHandoff(
|
||||
run: PuzzleRunSnapshot,
|
||||
work: PuzzleWorkSummary | null | undefined,
|
||||
@@ -803,11 +815,11 @@ function buildFallbackLocalLevel(
|
||||
buildLocalLevelName(currentLevel.levelName, nextLevelIndex);
|
||||
const nextCoverImageSrc =
|
||||
nextLevel?.coverImageSrc ?? currentLevel.coverImageSrc;
|
||||
const nextUiBackgroundImageSrc =
|
||||
resolvePuzzleUiBackgroundSource(nextLevel) ?? currentLevel.uiBackgroundImageSrc;
|
||||
const nextUiBackgroundImageObjectKey = resolvePuzzleUiBackgroundSource(nextLevel)
|
||||
? nextLevel?.uiBackgroundImageObjectKey?.trim() || null
|
||||
: currentLevel.uiBackgroundImageObjectKey ?? null;
|
||||
const nextUiBackground = resolvePuzzleUiBackgroundFields(
|
||||
nextLevel,
|
||||
resolvePuzzleWorkUiBackgroundCarrier(work),
|
||||
currentLevel,
|
||||
);
|
||||
const nextBackgroundMusic =
|
||||
nextLevel?.backgroundMusic ?? currentLevel.backgroundMusic;
|
||||
|
||||
@@ -838,8 +850,8 @@ function buildFallbackLocalLevel(
|
||||
clearedAtMs: null,
|
||||
elapsedMs: null,
|
||||
coverImageSrc: nextCoverImageSrc,
|
||||
uiBackgroundImageSrc: nextUiBackgroundImageSrc,
|
||||
uiBackgroundImageObjectKey: nextUiBackgroundImageObjectKey,
|
||||
uiBackgroundImageSrc: nextUiBackground.uiBackgroundImageSrc,
|
||||
uiBackgroundImageObjectKey: nextUiBackground.uiBackgroundImageObjectKey,
|
||||
backgroundMusic: nextBackgroundMusic,
|
||||
...buildLevelTimerFields(nextLevelIndex),
|
||||
leaderboardEntries: [],
|
||||
@@ -865,9 +877,10 @@ export function startLocalPuzzleRun(
|
||||
const firstLevel = item.levels?.[currentLevelIndex] ?? null;
|
||||
const firstLevelName = firstLevel?.levelName || item.levelName;
|
||||
const firstCoverImageSrc = firstLevel?.coverImageSrc ?? item.coverImageSrc;
|
||||
const firstUiBackgroundImageSrc = resolvePuzzleUiBackgroundSource(firstLevel);
|
||||
const firstUiBackgroundImageObjectKey =
|
||||
firstLevel?.uiBackgroundImageObjectKey?.trim() || null;
|
||||
const firstUiBackground = resolvePuzzleUiBackgroundFields(
|
||||
firstLevel,
|
||||
resolvePuzzleWorkUiBackgroundCarrier(item),
|
||||
);
|
||||
const firstBackgroundMusic = firstLevel?.backgroundMusic ?? null;
|
||||
const nextSameWorkLevel = item.levels?.[currentLevelIndex + 1] ?? null;
|
||||
return {
|
||||
@@ -888,8 +901,8 @@ export function startLocalPuzzleRun(
|
||||
authorDisplayName: item.authorDisplayName,
|
||||
themeTags: item.themeTags,
|
||||
coverImageSrc: firstCoverImageSrc,
|
||||
uiBackgroundImageSrc: firstUiBackgroundImageSrc,
|
||||
uiBackgroundImageObjectKey: firstUiBackgroundImageObjectKey,
|
||||
uiBackgroundImageSrc: firstUiBackground.uiBackgroundImageSrc,
|
||||
uiBackgroundImageObjectKey: firstUiBackground.uiBackgroundImageObjectKey,
|
||||
backgroundMusic: firstBackgroundMusic,
|
||||
board: buildInitialBoard(gridSize, runId, item.profileId, 1),
|
||||
status: 'playing',
|
||||
|
||||
@@ -6,15 +6,27 @@ type PuzzleUiBackgroundFields = {
|
||||
export function resolvePuzzleUiBackgroundSource(
|
||||
level: PuzzleUiBackgroundFields | null | undefined,
|
||||
) {
|
||||
const imageSrc = level?.uiBackgroundImageSrc?.trim();
|
||||
if (imageSrc) {
|
||||
return imageSrc;
|
||||
}
|
||||
|
||||
const objectKey = level?.uiBackgroundImageObjectKey?.trim().replace(/^\/+/u, '');
|
||||
if (!objectKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `/${objectKey}`;
|
||||
return resolvePuzzleUiBackgroundFields(level).uiBackgroundImageSrc;
|
||||
}
|
||||
|
||||
export function resolvePuzzleUiBackgroundFields(
|
||||
...sources: Array<PuzzleUiBackgroundFields | null | undefined>
|
||||
) {
|
||||
for (const source of sources) {
|
||||
const imageSrc = source?.uiBackgroundImageSrc?.trim();
|
||||
const objectKey = source?.uiBackgroundImageObjectKey
|
||||
?.trim()
|
||||
.replace(/^\/+/u, '');
|
||||
if (imageSrc || objectKey) {
|
||||
return {
|
||||
uiBackgroundImageSrc: imageSrc || (objectKey ? `/${objectKey}` : null),
|
||||
uiBackgroundImageObjectKey: objectKey || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
uiBackgroundImageSrc: null,
|
||||
uiBackgroundImageObjectKey: null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user