Match3D & Puzzle: runtime UI, assets, drag fix
Backend: stop treating background music as a required draft asset and remove auto-submit/plan for background music; load persisted generated UI/assets into Match3D agent session responses (added helpers to resolve profile id and fetch existing generated assets). Frontend: make Match3D result preview reuse runtime UI styles, unify runtime settings entry, update PuzzleRuntime to apply immediate pointermove transforms (disable drag transition), use SVG clipPath for merged piece rounding, ensure PuzzleRuntimeShell supplies platform theme classes, and adjust related tests. Docs & logs: update decision log, pitfalls and product docs to reflect these changes.
This commit is contained in:
@@ -46,6 +46,7 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||
import type {
|
||||
Match3DGeneratedBackgroundAsset,
|
||||
Match3DGeneratedItemAsset,
|
||||
Match3DWorkProfile,
|
||||
Match3DWorkSummary,
|
||||
@@ -433,6 +434,16 @@ type BabyObjectMatchRuntimeReturnStage =
|
||||
type VisualNovelEntryGenerationPhase = 'generating' | 'ready' | 'failed';
|
||||
type BabyObjectMatchGenerationPhase = 'generating' | 'ready' | 'failed';
|
||||
|
||||
type RecommendRuntimeState = {
|
||||
activeKind: RecommendRuntimeKind | null;
|
||||
babyObjectMatchDraft: BabyObjectMatchDraft | null;
|
||||
bigFishRun: BigFishRuntimeSnapshotResponse | null;
|
||||
match3dRun: Match3DRunSnapshot | null;
|
||||
puzzleRun: PuzzleRunSnapshot | null;
|
||||
squareHoleRun: SquareHoleRunSnapshot | null;
|
||||
visualNovelRun: VisualNovelRunSnapshot | null;
|
||||
};
|
||||
|
||||
type PuzzleSaveArchiveState = {
|
||||
runtimeKind?: unknown;
|
||||
entryProfileId?: unknown;
|
||||
@@ -529,6 +540,37 @@ function getPlatformRecommendRuntimeKind(
|
||||
return 'rpg';
|
||||
}
|
||||
|
||||
function isRecommendRuntimeReadyForEntry(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
state: RecommendRuntimeState,
|
||||
) {
|
||||
const expectedKind = getPlatformRecommendRuntimeKind(entry);
|
||||
if (state.activeKind !== expectedKind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedKind === 'big-fish') {
|
||||
return Boolean(state.bigFishRun);
|
||||
}
|
||||
if (expectedKind === 'match3d') {
|
||||
return Boolean(state.match3dRun);
|
||||
}
|
||||
if (expectedKind === 'puzzle') {
|
||||
return Boolean(state.puzzleRun);
|
||||
}
|
||||
if (expectedKind === 'square-hole') {
|
||||
return Boolean(state.squareHoleRun);
|
||||
}
|
||||
if (expectedKind === 'visual-novel') {
|
||||
return Boolean(state.visualNovelRun);
|
||||
}
|
||||
if (expectedKind === 'edutainment') {
|
||||
return Boolean(state.babyObjectMatchDraft);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isSamePlatformPublicGalleryEntry(
|
||||
left: PlatformPublicGalleryCard,
|
||||
right: PlatformPublicGalleryCard,
|
||||
@@ -631,7 +673,7 @@ function mapPublicWorkDetailToMatch3DWork(
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
return promoteMatch3DGeneratedBackgroundAsset({
|
||||
workId: entry.workId,
|
||||
profileId: entry.profileId,
|
||||
ownerUserId: entry.ownerUserId,
|
||||
@@ -663,6 +705,51 @@ function mapPublicWorkDetailToMatch3DWork(
|
||||
generatedItemAssets: normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
entry.generatedItemAssets ?? [],
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function findMatch3DGeneratedBackgroundAsset(
|
||||
generatedItemAssets: readonly Match3DGeneratedItemAsset[] | null | undefined,
|
||||
): Match3DGeneratedBackgroundAsset | null {
|
||||
return (
|
||||
generatedItemAssets
|
||||
?.map((asset) => asset.backgroundAsset ?? null)
|
||||
.find(Boolean) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function promoteMatch3DGeneratedBackgroundAsset<
|
||||
T extends Pick<
|
||||
Match3DWorkSummary,
|
||||
| 'backgroundPrompt'
|
||||
| 'backgroundImageSrc'
|
||||
| 'backgroundImageObjectKey'
|
||||
| 'generatedBackgroundAsset'
|
||||
| 'generatedItemAssets'
|
||||
>,
|
||||
>(profile: T): T {
|
||||
const backgroundAsset =
|
||||
profile.generatedBackgroundAsset ??
|
||||
findMatch3DGeneratedBackgroundAsset(profile.generatedItemAssets);
|
||||
if (!backgroundAsset) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
return {
|
||||
...profile,
|
||||
backgroundPrompt: profile.backgroundPrompt ?? backgroundAsset.prompt ?? null,
|
||||
backgroundImageSrc:
|
||||
profile.backgroundImageSrc ??
|
||||
backgroundAsset.imageSrc ??
|
||||
backgroundAsset.imageObjectKey ??
|
||||
null,
|
||||
backgroundImageObjectKey:
|
||||
profile.backgroundImageObjectKey ??
|
||||
backgroundAsset.imageObjectKey ??
|
||||
backgroundAsset.imageSrc ??
|
||||
null,
|
||||
generatedBackgroundAsset:
|
||||
profile.generatedBackgroundAsset ?? backgroundAsset,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -675,7 +762,10 @@ function buildMatch3DProfileFromSession(
|
||||
}
|
||||
|
||||
const now = session.updatedAt || new Date().toISOString();
|
||||
return {
|
||||
const generatedItemAssets = normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
draft.generatedItemAssets,
|
||||
);
|
||||
return promoteMatch3DGeneratedBackgroundAsset({
|
||||
workId: draft.profileId,
|
||||
profileId: draft.profileId,
|
||||
ownerUserId: 'current-user',
|
||||
@@ -697,10 +787,8 @@ function buildMatch3DProfileFromSession(
|
||||
backgroundImageSrc: draft.backgroundImageSrc ?? null,
|
||||
backgroundImageObjectKey: draft.backgroundImageObjectKey ?? null,
|
||||
generatedBackgroundAsset: draft.generatedBackgroundAsset ?? null,
|
||||
generatedItemAssets: normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
draft.generatedItemAssets,
|
||||
),
|
||||
};
|
||||
generatedItemAssets,
|
||||
});
|
||||
}
|
||||
|
||||
function hasMatch3DRuntimeAsset(
|
||||
@@ -791,10 +879,14 @@ function resolveMatch3DRuntimeGeneratedBackgroundAsset(
|
||||
publicWorkDetail: PlatformPublicGalleryCard | null,
|
||||
) {
|
||||
const runProfileId = run?.profileId?.trim() ?? '';
|
||||
const profileBackground = profile?.generatedBackgroundAsset ?? null;
|
||||
const profileBackground = profile
|
||||
? promoteMatch3DGeneratedBackgroundAsset(profile).generatedBackgroundAsset ??
|
||||
null
|
||||
: null;
|
||||
const publicBackground =
|
||||
publicWorkDetail && isMatch3DGalleryEntry(publicWorkDetail)
|
||||
? (publicWorkDetail.generatedBackgroundAsset ?? null)
|
||||
? (promoteMatch3DGeneratedBackgroundAsset(publicWorkDetail)
|
||||
.generatedBackgroundAsset ?? null)
|
||||
: null;
|
||||
|
||||
if (runProfileId && profile?.profileId === runProfileId) {
|
||||
@@ -832,18 +924,25 @@ function resolveMatch3DRuntimeBackgroundImageSrc(
|
||||
publicWorkDetail: PlatformPublicGalleryCard | null,
|
||||
) {
|
||||
const runProfileId = run?.profileId?.trim() ?? '';
|
||||
const resolvedProfile = profile
|
||||
? promoteMatch3DGeneratedBackgroundAsset(profile)
|
||||
: null;
|
||||
const resolvedPublicWork =
|
||||
publicWorkDetail && isMatch3DGalleryEntry(publicWorkDetail)
|
||||
? promoteMatch3DGeneratedBackgroundAsset(publicWorkDetail)
|
||||
: null;
|
||||
const profileBackground =
|
||||
profile?.backgroundImageSrc?.trim() ||
|
||||
profile?.generatedBackgroundAsset?.imageSrc?.trim() ||
|
||||
profile?.backgroundImageObjectKey?.trim() ||
|
||||
profile?.generatedBackgroundAsset?.imageObjectKey?.trim() ||
|
||||
resolvedProfile?.backgroundImageSrc?.trim() ||
|
||||
resolvedProfile?.generatedBackgroundAsset?.imageSrc?.trim() ||
|
||||
resolvedProfile?.backgroundImageObjectKey?.trim() ||
|
||||
resolvedProfile?.generatedBackgroundAsset?.imageObjectKey?.trim() ||
|
||||
'';
|
||||
const publicBackground =
|
||||
publicWorkDetail && isMatch3DGalleryEntry(publicWorkDetail)
|
||||
? publicWorkDetail.backgroundImageSrc?.trim() ||
|
||||
publicWorkDetail.backgroundImageObjectKey?.trim() ||
|
||||
''
|
||||
: '';
|
||||
resolvedPublicWork?.backgroundImageSrc?.trim() ||
|
||||
resolvedPublicWork?.generatedBackgroundAsset?.imageSrc?.trim() ||
|
||||
resolvedPublicWork?.backgroundImageObjectKey?.trim() ||
|
||||
resolvedPublicWork?.generatedBackgroundAsset?.imageObjectKey?.trim() ||
|
||||
'';
|
||||
|
||||
if (runProfileId && profile?.profileId === runProfileId) {
|
||||
return profileBackground || publicBackground || null;
|
||||
@@ -2438,6 +2537,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
selectionStageRef.current = selectionStage;
|
||||
}, [selectionStage]);
|
||||
|
||||
const resetRecommendRuntimeSelection = useCallback(() => {
|
||||
// 中文注释:推荐页嵌入运行态进入改造/创作后会清掉玩法 run;
|
||||
// 同步清空推荐选择,避免返回推荐页时复用已失效的运行态占位。
|
||||
recommendRuntimeStartRequestRef.current += 1;
|
||||
setActiveRecommendEntryKey(null);
|
||||
setActiveRecommendRuntimeKind(null);
|
||||
setActiveRecommendRuntimeError(null);
|
||||
setIsStartingRecommendEntry(false);
|
||||
}, []);
|
||||
|
||||
const updatePendingDraftShelfItem = useCallback(
|
||||
(
|
||||
kind: Exclude<CreationWorkShelfKind, 'rpg'>,
|
||||
@@ -3776,13 +3885,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
let runtimeProfile: Match3DWorkProfile | null = null;
|
||||
try {
|
||||
const { item } = await getMatch3DWorkDetail(profileId);
|
||||
runtimeProfile = {
|
||||
runtimeProfile = promoteMatch3DGeneratedBackgroundAsset({
|
||||
...item,
|
||||
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
response.session.draft?.generatedItemAssets,
|
||||
item.generatedItemAssets,
|
||||
),
|
||||
};
|
||||
});
|
||||
setMatch3DProfile(runtimeProfile);
|
||||
await refreshMatch3DShelf().catch(() => undefined);
|
||||
} catch {
|
||||
@@ -4875,13 +4984,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
let runtimeProfile: Match3DWorkProfile | null = null;
|
||||
try {
|
||||
const { item } = await getMatch3DWorkDetail(profileId);
|
||||
runtimeProfile = {
|
||||
runtimeProfile = promoteMatch3DGeneratedBackgroundAsset({
|
||||
...item,
|
||||
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
|
||||
response.session.draft?.generatedItemAssets,
|
||||
item.generatedItemAssets,
|
||||
),
|
||||
};
|
||||
});
|
||||
if (isViewingMatch3DGeneration(nextSession.sessionId)) {
|
||||
setMatch3DProfile(runtimeProfile);
|
||||
}
|
||||
@@ -5464,14 +5573,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const leavePuzzleFlow = useCallback(() => {
|
||||
setPuzzleOperation(null);
|
||||
puzzleRunRef.current = null;
|
||||
setPuzzleRun(null);
|
||||
setPuzzleRuntimeAuthMode('default');
|
||||
setPuzzleGenerationState(null);
|
||||
setIsPuzzleNextLevelGenerating(false);
|
||||
setActiveCreativeAgentSessionId(null);
|
||||
setCreativeDraftEditError(null);
|
||||
resetRecommendRuntimeSelection();
|
||||
puzzleFlow.leaveFlow();
|
||||
}, [puzzleFlow]);
|
||||
}, [puzzleFlow, resetRecommendRuntimeSelection]);
|
||||
|
||||
const leaveVisualNovelFlow = useCallback(() => {
|
||||
setVisualNovelWork(null);
|
||||
@@ -6602,12 +6713,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
// 中文注释:详情补读只为拿完整生成素材;失败时继续按摘要开局,避免推荐流卡死。
|
||||
}
|
||||
}
|
||||
runtimeProfile = {
|
||||
runtimeProfile = promoteMatch3DGeneratedBackgroundAsset({
|
||||
...runtimeProfile,
|
||||
generatedItemAssets: normalizeMatch3DGeneratedItemAssetsForRuntime(
|
||||
runtimeProfile.generatedItemAssets,
|
||||
),
|
||||
};
|
||||
});
|
||||
await preloadMatch3DGeneratedRuntimeAssets(
|
||||
runtimeProfile.generatedItemAssets,
|
||||
runtimeProfile.generatedBackgroundAsset,
|
||||
@@ -7314,6 +7425,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
void remixPuzzleGalleryWork(targetProfileId)
|
||||
.then((response) => {
|
||||
resetRecommendRuntimeSelection();
|
||||
puzzleFlow.setSession(response.session);
|
||||
setPuzzleOperation(null);
|
||||
setPuzzleRun(null);
|
||||
@@ -7338,6 +7450,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
isPuzzleBusy,
|
||||
puzzleFlow,
|
||||
resolvePuzzleErrorMessage,
|
||||
resetRecommendRuntimeSelection,
|
||||
runProtectedAction,
|
||||
setIsPuzzleBusy,
|
||||
setPuzzleError,
|
||||
@@ -9639,30 +9752,51 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const hasActiveEntry =
|
||||
activeRecommendEntryKey &&
|
||||
recommendRuntimeEntries.some(
|
||||
(entry) =>
|
||||
getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey,
|
||||
);
|
||||
if (hasActiveEntry || isStartingRecommendEntry) {
|
||||
const activeRecommendEntry = activeRecommendEntryKey
|
||||
? recommendRuntimeEntries.find(
|
||||
(entry) =>
|
||||
getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey,
|
||||
) ?? null
|
||||
: null;
|
||||
const isActiveRecommendRuntimeReady =
|
||||
activeRecommendEntry !== null &&
|
||||
isRecommendRuntimeReadyForEntry(activeRecommendEntry, {
|
||||
activeKind: activeRecommendRuntimeKind,
|
||||
babyObjectMatchDraft,
|
||||
bigFishRun,
|
||||
match3dRun,
|
||||
puzzleRun,
|
||||
squareHoleRun,
|
||||
visualNovelRun,
|
||||
});
|
||||
if (
|
||||
(activeRecommendEntry !== null && isActiveRecommendRuntimeReady) ||
|
||||
isStartingRecommendEntry
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstRecommendEntry = recommendRuntimeEntries[0];
|
||||
if (firstRecommendEntry) {
|
||||
void selectRecommendRuntimeEntry(firstRecommendEntry);
|
||||
const nextRecommendEntry = activeRecommendEntry ?? recommendRuntimeEntries[0];
|
||||
if (nextRecommendEntry) {
|
||||
void selectRecommendRuntimeEntry(nextRecommendEntry);
|
||||
}
|
||||
}, [
|
||||
activeRecommendEntryKey,
|
||||
activeRecommendRuntimeKind,
|
||||
babyObjectMatchDraft,
|
||||
bigFishRun,
|
||||
isStartingRecommendEntry,
|
||||
match3dRun,
|
||||
platformBootstrap.canReadProtectedData,
|
||||
platformBootstrap.isLoadingPlatform,
|
||||
platformBootstrap.isAuthenticated,
|
||||
platformBootstrap.platformTab,
|
||||
puzzleRun,
|
||||
recommendRuntimeEntries,
|
||||
selectRecommendRuntimeEntry,
|
||||
selectionStage,
|
||||
squareHoleRun,
|
||||
visualNovelRun,
|
||||
]);
|
||||
|
||||
const remixPublicWork = useCallback(
|
||||
@@ -9696,6 +9830,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (isPuzzleGalleryEntry(entry)) {
|
||||
void remixPuzzleGalleryWork(entry.profileId)
|
||||
.then((response) => {
|
||||
resetRecommendRuntimeSelection();
|
||||
puzzleFlow.setSession(response.session);
|
||||
setPuzzleOperation(null);
|
||||
enterCreateTab();
|
||||
@@ -9765,6 +9900,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
isPublicWorkDetailBusy,
|
||||
platformBootstrap,
|
||||
puzzleFlow,
|
||||
resetRecommendRuntimeSelection,
|
||||
resolveBigFishErrorMessage,
|
||||
resolvePuzzleErrorMessage,
|
||||
runProtectedAction,
|
||||
|
||||
Reference in New Issue
Block a user