Update Match3D/image-generation docs & code
Adds/updates documentation, assets and implementation for Match3D and puzzle image generation workflows. Key changes: decision logs and pitfalls updated to prefer VectorEngine Gemini for Match3D material sheets and to require edits (multipart) for 1:1 container reference images; guidance added for when to use APIMart vs VectorEngine. .env.example clarified APIMart/Responses config. Many new public assets and PPT visuals added. Code changes across frontend and backend: updated shared contracts, server-rs match3d/puzzle/image-generation handlers, VectorEngine/OpenAI image generation clients, and multiple React components/tests to handle UI/background/container image signing, edits workflow, and puzzle UI background resolution. Added src/services/puzzle-runtime/puzzleUiBackgroundSource.ts and related test updates. Includes notes about multipart HTTP/1.1 requirement and test/verification commands in docs.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
||||
import type { PuzzlePieceState } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import {
|
||||
@@ -62,7 +63,7 @@ function boardPositionSignature(run: ReturnType<typeof startLocalPuzzleRun>) {
|
||||
|
||||
function solveCurrentLevel(run: ReturnType<typeof startLocalPuzzleRun>) {
|
||||
let nextRun = run;
|
||||
for (let index = 0; index < 12; index += 1) {
|
||||
for (let index = 0; index < 24; index += 1) {
|
||||
const currentLevel = nextRun.currentLevel;
|
||||
if (!currentLevel || currentLevel.status === 'cleared') {
|
||||
return nextRun;
|
||||
@@ -574,6 +575,34 @@ describe('puzzleLocalRuntime', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('本地试玩在只有 UI 背景 objectKey 时也能继承生成图', () => {
|
||||
const workWithRuntimeAssets: PuzzleWorkSummary = {
|
||||
...baseWork,
|
||||
levels: [
|
||||
{
|
||||
levelId: 'puzzle-level-1',
|
||||
levelName: '第一关',
|
||||
pictureDescription: '第一关画面',
|
||||
candidates: [],
|
||||
selectedCandidateId: null,
|
||||
coverImageSrc: '/level-1.png',
|
||||
coverAssetId: null,
|
||||
uiBackgroundImageSrc: null,
|
||||
uiBackgroundImageObjectKey:
|
||||
'generated-puzzle-assets/session/ui/background-object-key.png',
|
||||
backgroundMusic: null,
|
||||
generationStatus: 'ready',
|
||||
} as PuzzleDraftLevel,
|
||||
],
|
||||
};
|
||||
|
||||
const run = startLocalPuzzleRun(workWithRuntimeAssets);
|
||||
|
||||
expect(run.currentLevel?.uiBackgroundImageSrc).toBe(
|
||||
'/generated-puzzle-assets/session/ui/background-object-key.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('暂停和冻结时间不会消耗本地倒计时', () => {
|
||||
const run = startLocalPuzzleRun(baseWork);
|
||||
const pausedRun = setLocalPuzzlePaused(
|
||||
|
||||
@@ -12,6 +12,7 @@ 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';
|
||||
|
||||
const LOCAL_PUZZLE_RUN_ID_PREFIX = 'local-puzzle-run-';
|
||||
const PUZZLE_FREEZE_TIME_DURATION_MS = 10_000;
|
||||
@@ -803,7 +804,10 @@ function buildFallbackLocalLevel(
|
||||
const nextCoverImageSrc =
|
||||
nextLevel?.coverImageSrc ?? currentLevel.coverImageSrc;
|
||||
const nextUiBackgroundImageSrc =
|
||||
nextLevel?.uiBackgroundImageSrc ?? currentLevel.uiBackgroundImageSrc;
|
||||
resolvePuzzleUiBackgroundSource(nextLevel) ?? currentLevel.uiBackgroundImageSrc;
|
||||
const nextUiBackgroundImageObjectKey = resolvePuzzleUiBackgroundSource(nextLevel)
|
||||
? nextLevel?.uiBackgroundImageObjectKey?.trim() || null
|
||||
: currentLevel.uiBackgroundImageObjectKey ?? null;
|
||||
const nextBackgroundMusic =
|
||||
nextLevel?.backgroundMusic ?? currentLevel.backgroundMusic;
|
||||
|
||||
@@ -835,6 +839,7 @@ function buildFallbackLocalLevel(
|
||||
elapsedMs: null,
|
||||
coverImageSrc: nextCoverImageSrc,
|
||||
uiBackgroundImageSrc: nextUiBackgroundImageSrc,
|
||||
uiBackgroundImageObjectKey: nextUiBackgroundImageObjectKey,
|
||||
backgroundMusic: nextBackgroundMusic,
|
||||
...buildLevelTimerFields(nextLevelIndex),
|
||||
leaderboardEntries: [],
|
||||
@@ -860,7 +865,9 @@ export function startLocalPuzzleRun(
|
||||
const firstLevel = item.levels?.[currentLevelIndex] ?? null;
|
||||
const firstLevelName = firstLevel?.levelName || item.levelName;
|
||||
const firstCoverImageSrc = firstLevel?.coverImageSrc ?? item.coverImageSrc;
|
||||
const firstUiBackgroundImageSrc = firstLevel?.uiBackgroundImageSrc ?? null;
|
||||
const firstUiBackgroundImageSrc = resolvePuzzleUiBackgroundSource(firstLevel);
|
||||
const firstUiBackgroundImageObjectKey =
|
||||
firstLevel?.uiBackgroundImageObjectKey?.trim() || null;
|
||||
const firstBackgroundMusic = firstLevel?.backgroundMusic ?? null;
|
||||
const nextSameWorkLevel = item.levels?.[currentLevelIndex + 1] ?? null;
|
||||
return {
|
||||
@@ -882,6 +889,7 @@ export function startLocalPuzzleRun(
|
||||
themeTags: item.themeTags,
|
||||
coverImageSrc: firstCoverImageSrc,
|
||||
uiBackgroundImageSrc: firstUiBackgroundImageSrc,
|
||||
uiBackgroundImageObjectKey: firstUiBackgroundImageObjectKey,
|
||||
backgroundMusic: firstBackgroundMusic,
|
||||
board: buildInitialBoard(gridSize, runId, item.profileId, 1),
|
||||
status: 'playing',
|
||||
|
||||
20
src/services/puzzle-runtime/puzzleUiBackgroundSource.ts
Normal file
20
src/services/puzzle-runtime/puzzleUiBackgroundSource.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
type PuzzleUiBackgroundFields = {
|
||||
uiBackgroundImageSrc?: string | null;
|
||||
uiBackgroundImageObjectKey?: string | null;
|
||||
};
|
||||
|
||||
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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user