diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index d7e18c08..52038020 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -219,6 +219,7 @@ import { buildSquareHoleGenerationAnchorEntries, buildWoodenFishGenerationAnchorEntries, createMiniGameDraftGenerationState, + type MiniGameDraftGenerationKind, type MiniGameDraftGenerationState, resolveMiniGameDraftGenerationStartedAtMs, } from '../../services/miniGameDraftGenerationProgress'; @@ -234,6 +235,7 @@ import { buildSquareHolePublicWorkCode, buildVisualNovelPublicWorkCode, buildWoodenFishPublicWorkCode, + isSamePuzzleClearPublicWorkCode, isSamePuzzlePublicWorkCode, } from '../../services/publicWorkCode'; import { @@ -373,7 +375,12 @@ import { selectAdjacentPlatformRecommendEntry, } from '../rpg-entry/rpgEntryPublicGalleryViewModel'; import { + isBigFishGalleryEntry, isEdutainmentGalleryEntry, + isJumpHopGalleryEntry, + isPuzzleGalleryEntry, + isPuzzleClearGalleryEntry, + mapPuzzleClearWorkToPlatformGalleryCard, mapPuzzleWorkToPlatformGalleryCard, type PlatformPublicGalleryCard, resolvePlatformPublicWorkCode, @@ -415,6 +422,7 @@ import { buildBigFishCreationUrlState, buildJumpHopCreationUrlState, buildMatch3DCreationUrlState, + buildPuzzleClearCreationUrlState, buildPuzzleCreationUrlState, buildPuzzleDraftRuntimeUrlState, buildPuzzlePublishedRuntimeUrlState, @@ -456,6 +464,7 @@ import { buildPendingBigFishWorks, buildPendingJumpHopWorks, buildPendingMatch3DWorks, + buildPendingPuzzleClearWorks, buildPendingPuzzleWorks, buildPendingSquareHoleWorks, buildPendingVisualNovelWorks, @@ -557,6 +566,8 @@ import { } from './platformMiniGameDraftPayloadModel'; import { buildJumpHopPendingSession, + buildPuzzleClearPendingSession, + buildPuzzleClearSessionFromWorkDetail, buildPuzzleRuntimeWorkFromSession, buildSquareHoleProfileFromSession, buildVisualNovelSessionFromWorkDetail, @@ -752,7 +763,7 @@ async function resumePuzzleProfileSaveArchiveRaw(worldKey: string) { ); } -const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS = +const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS: JumpHopRuntimeRequestOptions = BACKGROUND_AUTH_REQUEST_OPTIONS; const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS: JumpHopRuntimeRequestOptions = RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS; @@ -778,6 +789,16 @@ function resolveCurrentRecommendRuntimeAuthPlan( hasStoredAccessToken: Boolean(getStoredAccessToken()), }); } + +function shouldUseRecommendRuntimeGuestAuth(authUi: RecommendRuntimeAuthUi) { + return ( + resolveCurrentRecommendRuntimeAuthPlan(authUi, { + embedded: true, + allowRuntimeGuestAuth: true, + }).requestKind === 'runtime-guest' + ); +} + async function buildRecommendRuntimeOptionsFromAuthPlan( plan: ReturnType, ) { @@ -797,6 +818,28 @@ async function buildRecommendRuntimeAuthOptions( resolveCurrentRecommendRuntimeAuthPlan(authUi, { embedded }), ); } + +function resolveRecommendEntryShareStage( + entry: PlatformPublicGalleryCard, +): PublishShareModalPayload['stage'] { + if (isBigFishGalleryEntry(entry)) { + return 'big-fish-runtime'; + } + + if (isPuzzleGalleryEntry(entry)) { + return 'puzzle-gallery-detail'; + } + + return 'work-detail'; +} + +function pushPuzzleResultHistoryEntry( + session: PuzzleAgentSessionSnapshot | null, +) { + pushAppHistoryPath('/creation/puzzle/result'); + writeCreationUrlState(buildPuzzleCreationUrlState(session)); +} + const PUZZLE_DRAFT_GENERATION_POINT_COST = 2; const MATCH3D_DRAFT_GENERATION_POINT_COST = 10; const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3; @@ -2795,6 +2838,7 @@ export function PlatformEntryFlowShellImpl({ bigFishEntries: bigFishGalleryEntries, match3dEntries: match3dGalleryEntries, puzzleEntries: puzzleGalleryEntries, + puzzleClearEntries: puzzleClearGalleryEntries, barkBattleGalleryEntries, barkBattleWorks, jumpHopEntries: jumpHopGalleryEntries, @@ -12358,6 +12402,12 @@ export function PlatformEntryFlowShellImpl({ { authMode: intent.authMode }, ); return; + case 'start-puzzle-clear': + setPublicWorkDetailError(null); + void startPuzzleClearRunFromProfile(intent.profileId, { + returnStage: intent.returnStage, + }); + return; case 'start-jump-hop': setPublicWorkDetailError(null); void startJumpHopRunFromProfile(intent.profileId, { @@ -12491,6 +12541,8 @@ export function PlatformEntryFlowShellImpl({ setBigFishError(intent.errorMessage); } else if (intent.errorTarget === 'puzzle') { setPuzzleError(intent.errorMessage); + } else if (intent.errorTarget === 'puzzle-clear') { + setPuzzleClearError(intent.errorMessage); } else if (intent.errorTarget === 'match3d') { setMatch3DError(intent.errorMessage); } else if (intent.errorTarget === 'square-hole') { @@ -12514,6 +12566,12 @@ export function PlatformEntryFlowShellImpl({ { embedded: intent.embedded }, ); break; + case 'start-puzzle-clear': + started = await startPuzzleClearRunFromProfile(intent.profileId, { + embedded: intent.embedded, + returnStage: intent.returnStage, + }); + break; case 'start-jump-hop': started = await startJumpHopRunFromProfile(intent.profileId, { embedded: intent.embedded, @@ -13092,6 +13150,7 @@ export function PlatformEntryFlowShellImpl({ hasBigFishRun: Boolean(bigFishRun), hasJumpHopRun: Boolean(jumpHopRun), hasMatch3DRun: Boolean(match3dRun), + hasPuzzleClearRun: Boolean(puzzleClearRun), hasSquareHoleRun: Boolean(squareHoleRun), hasVisualNovelRun: Boolean(visualNovelRun), hasWoodenFishRun: Boolean(woodenFishRun), @@ -13114,6 +13173,7 @@ export function PlatformEntryFlowShellImpl({ hasBigFishRun: Boolean(bigFishRun), hasJumpHopRun: Boolean(jumpHopRun), hasMatch3DRun: Boolean(match3dRun), + hasPuzzleClearRun: Boolean(puzzleClearRun), hasSquareHoleRun: Boolean(squareHoleRun), hasVisualNovelRun: Boolean(visualNovelRun), hasWoodenFishRun: Boolean(woodenFishRun), diff --git a/src/components/platform-entry/platformCreationUrlStateModel.ts b/src/components/platform-entry/platformCreationUrlStateModel.ts index 45a153dc..a33c8390 100644 --- a/src/components/platform-entry/platformCreationUrlStateModel.ts +++ b/src/components/platform-entry/platformCreationUrlStateModel.ts @@ -3,6 +3,10 @@ import type { BigFishSessionSnapshotResponse } from '../../../packages/shared/sr import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent'; import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession'; +import type { + PuzzleClearSessionSnapshotResponse, + PuzzleClearWorkProfileResponse, +} from '../../../packages/shared/src/contracts/puzzleClear'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { SquareHoleSessionSnapshot } from '../../../packages/shared/src/contracts/squareHoleAgent'; import type { VisualNovelAgentSessionSnapshot } from '../../../packages/shared/src/contracts/visualNovel'; @@ -393,6 +397,21 @@ export function buildJumpHopCreationUrlState(params: { }; } +export function buildPuzzleClearCreationUrlState(params: { + session?: PuzzleClearSessionSnapshotResponse | null; + work?: PuzzleClearWorkProfileResponse | null; +}): CreationUrlState { + const sessionId = normalizeCreationUrlValue(params.session?.sessionId); + const profileId = normalizeCreationUrlValue( + params.work?.summary.profileId ?? params.session?.draft?.profileId, + ); + return { + sessionId, + profileId, + workId: normalizeCreationUrlValue(params.work?.summary.workId ?? profileId), + }; +} + export function buildWoodenFishCreationUrlState(params: { session?: WoodenFishSessionSnapshotResponse | null; work?: WoodenFishWorkProfileResponse | null; diff --git a/src/components/platform-entry/platformDraftGenerationShelfModel.test.ts b/src/components/platform-entry/platformDraftGenerationShelfModel.test.ts index 0a2f4949..ac511878 100644 --- a/src/components/platform-entry/platformDraftGenerationShelfModel.test.ts +++ b/src/components/platform-entry/platformDraftGenerationShelfModel.test.ts @@ -751,6 +751,7 @@ function buildJumpHopWork( profileId: 'jump-hop-profile-base', ownerUserId: 'user-1', sourceSessionId: 'jump-hop-session-base', + themeText: '潮雾港口', workTitle: '潮雾跳一跳', workDescription: '潮雾港口跳一跳。', themeTags: [], diff --git a/src/components/platform-entry/platformDraftGenerationShelfModel.ts b/src/components/platform-entry/platformDraftGenerationShelfModel.ts index da69f6aa..4aff6fcd 100644 --- a/src/components/platform-entry/platformDraftGenerationShelfModel.ts +++ b/src/components/platform-entry/platformDraftGenerationShelfModel.ts @@ -4,6 +4,7 @@ import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contra import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop'; import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; +import type { PuzzleClearWorkSummaryResponse } from '../../../packages/shared/src/contracts/puzzleClear'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; @@ -1191,6 +1192,7 @@ export function buildPendingJumpHopWorks( profileId: `jump-hop-profile-${sessionId}`, ownerUserId: '', sourceSessionId: sessionId, + themeText: state.title ?? '跳一跳草稿', workTitle: '跳一跳草稿', workDescription: state.status === 'failed' @@ -1210,6 +1212,48 @@ export function buildPendingJumpHopWorks( }); } +export function buildPendingPuzzleClearWorks( + pending: Record | undefined, + existingItems: readonly PuzzleClearWorkSummaryResponse[], +): PuzzleClearWorkSummaryResponse[] { + if (!pending) { + return []; + } + + return Object.entries(pending) + .filter(([sessionId]) => + existingItems.every((item) => item.sourceSessionId !== sessionId), + ) + .map(([sessionId, state]) => { + const generationStatus = + state.status === 'failed' + ? 'failed' + : state.status === 'generating' + ? 'generating' + : 'ready'; + return { + runtimeKind: 'puzzle-clear', + workId: `puzzle-clear-work-${sessionId}`, + profileId: sessionId, + ownerUserId: '', + sourceSessionId: sessionId, + workTitle: '拼消消草稿', + workDescription: + state.status === 'failed' + ? '拼消消草稿生成失败,可重新打开处理。' + : '正在生成拼消消草稿。', + themePrompt: '', + coverImageSrc: null, + publicationStatus: 'draft', + playCount: 0, + updatedAt: state.updatedAt, + publishedAt: null, + publishReady: false, + generationStatus, + }; + }); +} + export function buildPendingWoodenFishWorks( pending: Record | undefined, existingItems: readonly WoodenFishWorkSummaryResponse[], diff --git a/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts b/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts index 3573db00..0b483ed1 100644 --- a/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts +++ b/src/components/platform-entry/platformMiniGameDraftPayloadModel.test.ts @@ -213,6 +213,7 @@ function buildJumpHopDraft( templateId: 'jump-hop', templateName: '跳一跳', profileId: 'jump-hop-profile-1', + themeText: '草稿主题', workTitle: '草稿跳一跳', workDescription: '从草稿恢复。', themeTags: ['草稿'], @@ -236,6 +237,7 @@ function buildJumpHopPayload( ): JumpHopWorkspaceCreateRequest { return { templateId: 'jump-hop', + themeText: '表单主题', workTitle: '表单跳一跳', workDescription: '从表单提交。', themeTags: ['表单'], diff --git a/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts b/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts index 19c9c413..311de55c 100644 --- a/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts +++ b/src/components/platform-entry/platformMiniGameDraftPayloadModel.ts @@ -76,7 +76,7 @@ export function buildPuzzleWorkUpdatePayloadFromDraft( } export function buildJumpHopDraftActionPayload( - actionType: 'compile-draft' | 'regenerate-character' | 'regenerate-tiles', + actionType: 'compile-draft' | 'regenerate-tiles', input: { payload?: JumpHopWorkspaceCreateRequest | null; draft?: JumpHopSessionSnapshotResponse['draft'] | null; diff --git a/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts b/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts index 7c1674d2..4bcb17f9 100644 --- a/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts +++ b/src/components/platform-entry/platformMiniGameSessionMappingModel.test.ts @@ -120,6 +120,7 @@ function buildJumpHopSummary( profileId: 'jump-hop-profile-1', ownerUserId: 'user-1', sourceSessionId: ' jump-hop-session-1 ', + themeText: '云阶机关', workTitle: '云阶跳跃', workDescription: '越过云阶。', themeTags: ['云阶'], @@ -522,6 +523,7 @@ describe('platformMiniGameSessionMappingModel', () => { templateId: 'jump-hop', templateName: '跳一跳', profileId: 'jump-hop-profile-1', + themeText: '云阶机关', workTitle: '云阶跳跃', workDescription: '越过云阶。', themeTags: ['云阶'], diff --git a/src/components/platform-entry/platformMiniGameSessionMappingModel.ts b/src/components/platform-entry/platformMiniGameSessionMappingModel.ts index f240bf09..b11e128d 100644 --- a/src/components/platform-entry/platformMiniGameSessionMappingModel.ts +++ b/src/components/platform-entry/platformMiniGameSessionMappingModel.ts @@ -1,4 +1,12 @@ -import type { JumpHopSessionSnapshotResponse, JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop'; +import type { + JumpHopSessionSnapshotResponse, + JumpHopWorkSummaryResponse, +} from '../../../packages/shared/src/contracts/jumpHop'; +import type { + PuzzleClearSessionSnapshotResponse, + PuzzleClearWorkProfileResponse, + PuzzleClearWorkSummaryResponse, +} from '../../../packages/shared/src/contracts/puzzleClear'; import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { SquareHoleSessionSnapshot } from '../../../packages/shared/src/contracts/squareHoleAgent'; @@ -59,6 +67,54 @@ export function buildPuzzleRuntimeWorkFromSession( }; } +export function buildPuzzleClearSessionFromWorkDetail( + work: PuzzleClearWorkProfileResponse, + fallbackItem?: PuzzleClearWorkSummaryResponse | null, +): PuzzleClearSessionSnapshotResponse { + const sessionId = + normalizeCreationUrlValue(work.summary.sourceSessionId) ?? + normalizeCreationUrlValue(fallbackItem?.sourceSessionId) ?? + work.summary.profileId; + return { + sessionId, + ownerUserId: work.summary.ownerUserId, + status: work.summary.generationStatus, + draft: work.draft, + createdAt: work.summary.updatedAt, + updatedAt: work.summary.updatedAt, + }; +} + +export function buildPuzzleClearPendingSession( + item: PuzzleClearWorkSummaryResponse, +): PuzzleClearSessionSnapshotResponse { + const sessionId = + normalizeCreationUrlValue(item.sourceSessionId) ?? item.profileId; + return { + sessionId, + ownerUserId: item.ownerUserId, + status: item.generationStatus, + draft: { + templateId: 'puzzle-clear', + templateName: '拼消消', + profileId: item.profileId, + workTitle: item.workTitle, + workDescription: item.workDescription, + themePrompt: item.themePrompt, + boardBackgroundPrompt: item.themePrompt, + generateBoardBackground: true, + boardBackgroundAsset: null, + cardBackImageSrc: null, + atlasAsset: null, + patternGroups: [], + cardAssets: [], + generationStatus: item.generationStatus, + }, + createdAt: item.updatedAt, + updatedAt: item.updatedAt, + }; +} + export function buildSquareHoleProfileFromSession( session: SquareHoleSessionSnapshot | null, ): SquareHoleWorkProfile | null { @@ -122,6 +178,7 @@ export function buildJumpHopPendingSession( templateId: 'jump-hop', templateName: '跳一跳', profileId: item.profileId, + themeText: item.themeText, workTitle: item.workTitle, workDescription: item.workDescription, themeTags: item.themeTags, diff --git a/src/components/platform-entry/platformPublicCodeSearchModel.test.ts b/src/components/platform-entry/platformPublicCodeSearchModel.test.ts index 5130d391..333801f6 100644 --- a/src/components/platform-entry/platformPublicCodeSearchModel.test.ts +++ b/src/components/platform-entry/platformPublicCodeSearchModel.test.ts @@ -303,6 +303,7 @@ function buildJumpHopCard( profileId, ownerUserId: 'user-1', authorDisplayName: '测试作者', + themeText: '潮雾港', workTitle: '潮雾跳一跳', workDescription: '潮雾跳一跳说明。', coverImageSrc: null, diff --git a/src/components/platform-entry/platformPublicGalleryFlow.test.ts b/src/components/platform-entry/platformPublicGalleryFlow.test.ts index c5df79fc..5c4020ba 100644 --- a/src/components/platform-entry/platformPublicGalleryFlow.test.ts +++ b/src/components/platform-entry/platformPublicGalleryFlow.test.ts @@ -128,6 +128,7 @@ function buildJumpHopEntry( profileId: 'jump-hop-profile', ownerUserId: 'user-1', authorDisplayName: '玩家', + themeText: '一路向前', workTitle: '跳一跳', workDescription: '一路向前。', coverImageSrc: '/jump-hop-cover.png', @@ -166,6 +167,13 @@ function buildTypedEntry( switch (sourceType) { case 'puzzle': return { ...common, ...overrides, sourceType }; + case 'puzzle-clear': + return { + ...common, + ...overrides, + sourceType, + themePrompt: '拼消消主题', + }; case 'big-fish': return { ...common, ...overrides, sourceType }; case 'match3d': @@ -769,6 +777,7 @@ test('platform public gallery flow builds feeds with visibility gates and bark b bigFishEntries: [hiddenBigFish], match3dEntries: [], puzzleEntries: [], + puzzleClearEntries: [], barkBattleGalleryEntries: [], barkBattleWorks: [draftBarkFallback, publishedBarkFallback], jumpHopEntries: [], @@ -793,6 +802,7 @@ test('platform public gallery flow builds feeds with visibility gates and bark b bigFishEntries: [hiddenBigFish], match3dEntries: [], puzzleEntries: [], + puzzleClearEntries: [], barkBattleGalleryEntries: [ buildBarkBattleWork({ workId: 'gallery-bark', @@ -828,6 +838,7 @@ test('platform public gallery flow preserves feed tie order and featured slice', bigFishEntries: [], match3dEntries: [], puzzleEntries: [], + puzzleClearEntries: [], barkBattleGalleryEntries: [], barkBattleWorks: [ buildBarkBattleWork({ @@ -868,6 +879,7 @@ test('platform public gallery flow preserves feed tie order and featured slice', bigFishEntries: [], match3dEntries: [], puzzleEntries: [], + puzzleClearEntries: [], barkBattleGalleryEntries: [], barkBattleWorks: [], jumpHopEntries: [], diff --git a/src/components/platform-entry/platformPublicGalleryFlow.ts b/src/components/platform-entry/platformPublicGalleryFlow.ts index b4cdc1df..5f5da533 100644 --- a/src/components/platform-entry/platformPublicGalleryFlow.ts +++ b/src/components/platform-entry/platformPublicGalleryFlow.ts @@ -8,12 +8,14 @@ import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contra import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish'; +import type { PuzzleClearGalleryCardResponse } from '../../services/puzzle-clear/puzzleClearClient'; import { isBarkBattleGalleryEntry, isBigFishGalleryEntry, isEdutainmentGalleryEntry, isJumpHopGalleryEntry, isMatch3DGalleryEntry, + isPuzzleClearGalleryEntry, isPuzzleGalleryEntry, isSquareHoleGalleryEntry, isVisualNovelGalleryEntry, @@ -22,6 +24,7 @@ import { mapBarkBattleWorkToPlatformGalleryCard, mapBigFishWorkToPlatformGalleryCard, mapJumpHopWorkToPlatformGalleryCard, + mapPuzzleClearWorkToPlatformGalleryCard, mapPuzzleWorkToPlatformGalleryCard, mapSquareHoleWorkToPlatformGalleryCard, mapVisualNovelWorkToPlatformGalleryCard, @@ -43,6 +46,7 @@ export type RecommendRuntimeKind = | 'jump-hop' | 'match3d' | 'puzzle' + | 'puzzle-clear' | 'square-hole' | 'wooden-fish' | 'visual-novel' @@ -53,6 +57,7 @@ export type PlatformRecommendRuntimeStartErrorTarget = | 'big-fish' | 'match3d' | 'puzzle' + | 'puzzle-clear' | 'square-hole'; export type PlatformRecommendRuntimeStartIntent = @@ -73,6 +78,12 @@ export type PlatformRecommendRuntimeStartIntent = returnStage: 'platform'; embedded: true; } + | { + type: 'start-puzzle-clear'; + profileId: string; + returnStage: 'platform'; + embedded: true; + } | { type: 'start-jump-hop'; profileId: string; @@ -133,6 +144,7 @@ export type PlatformRecommendRuntimeReadyState = { hasBigFishRun?: boolean; hasJumpHopRun?: boolean; hasMatch3DRun?: boolean; + hasPuzzleClearRun?: boolean; hasSquareHoleRun?: boolean; hasVisualNovelRun?: boolean; hasWoodenFishRun?: boolean; @@ -161,6 +173,7 @@ export type PlatformPublicGalleryFeedsInput = { bigFishEntries: readonly BigFishWorkSummary[]; match3dEntries: readonly Match3DWorkSummary[]; puzzleEntries: readonly PuzzleWorkSummary[]; + puzzleClearEntries: readonly PuzzleClearGalleryCardResponse[]; barkBattleGalleryEntries: readonly BarkBattleWorkSummary[]; barkBattleWorks: readonly BarkBattleWorkSummary[]; jumpHopEntries: readonly JumpHopGalleryCardResponse[]; @@ -194,21 +207,23 @@ export function getPlatformPublicGalleryEntryKey( ? 'big-fish' : isPuzzleGalleryEntry(entry) ? 'puzzle' - : isJumpHopGalleryEntry(entry) - ? 'jump-hop' - : isWoodenFishGalleryEntry(entry) - ? 'wooden-fish' - : isMatch3DGalleryEntry(entry) - ? 'match3d' - : isSquareHoleGalleryEntry(entry) - ? 'square-hole' - : isVisualNovelGalleryEntry(entry) - ? 'visual-novel' - : isBarkBattleGalleryEntry(entry) - ? 'bark-battle' - : isEdutainmentGalleryEntry(entry) - ? `edutainment:${entry.templateId}` - : 'rpg'; + : isPuzzleClearGalleryEntry(entry) + ? 'puzzle-clear' + : isJumpHopGalleryEntry(entry) + ? 'jump-hop' + : isWoodenFishGalleryEntry(entry) + ? 'wooden-fish' + : isMatch3DGalleryEntry(entry) + ? 'match3d' + : isSquareHoleGalleryEntry(entry) + ? 'square-hole' + : isVisualNovelGalleryEntry(entry) + ? 'visual-novel' + : isBarkBattleGalleryEntry(entry) + ? 'bark-battle' + : isEdutainmentGalleryEntry(entry) + ? `edutainment:${entry.templateId}` + : 'rpg'; return `${kind}:${entry.ownerUserId}:${entry.profileId}`; } @@ -223,6 +238,10 @@ export function getPlatformRecommendRuntimeKind( return 'puzzle'; } + if (isPuzzleClearGalleryEntry(entry)) { + return 'puzzle-clear'; + } + if (isJumpHopGalleryEntry(entry)) { return 'jump-hop'; } @@ -297,6 +316,15 @@ export function resolvePlatformRecommendRuntimeStartIntent( }; } + if (isPuzzleClearGalleryEntry(entry)) { + return { + type: 'start-puzzle-clear', + profileId: entry.profileId, + returnStage: 'platform', + embedded: true, + }; + } + if (isJumpHopGalleryEntry(entry)) { return { type: 'start-jump-hop', @@ -423,6 +451,9 @@ export function isPlatformRecommendRuntimeReadyForEntry( state.puzzleRunCurrentLevelProfileId === entry.profileId ); } + if (expectedKind === 'puzzle-clear') { + return Boolean(state.hasPuzzleClearRun); + } if (expectedKind === 'square-hole') { return Boolean(state.hasSquareHoleRun); } @@ -527,6 +558,7 @@ export function buildPlatformPublicGalleryFeeds( ...bigFishEntries, ...input.match3dEntries.map(mapMatch3DWorkToPublicWorkDetail), ...input.puzzleEntries.map(mapPuzzleWorkToPlatformGalleryCard), + ...input.puzzleClearEntries.map(mapPuzzleClearWorkToPlatformGalleryCard), ...barkBattleGalleryEntries, ...input.jumpHopEntries.map(mapJumpHopWorkToPlatformGalleryCard), ...barkBattleFallbackEntries, @@ -539,6 +571,7 @@ export function buildPlatformPublicGalleryFeeds( ...bigFishEntries, ...input.match3dEntries.map(mapMatch3DWorkToPublicWorkDetail), ...input.puzzleEntries.map(mapPuzzleWorkToPlatformGalleryCard), + ...input.puzzleClearEntries.map(mapPuzzleClearWorkToPlatformGalleryCard), ...(barkBattleGalleryEntries.length > 0 ? barkBattleGalleryEntries : barkBattleFallbackEntries), diff --git a/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts b/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts index d8d40e0c..3b1bf38a 100644 --- a/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts +++ b/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts @@ -6,6 +6,7 @@ import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/co import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import type { PuzzleClearGalleryCardResponse } from '../../../packages/shared/src/contracts/puzzleClear'; import type { CustomWorldGalleryCard, CustomWorldLibraryEntry, @@ -25,6 +26,7 @@ import { mapBarkBattleWorkToPublicWorkDetail, mapBigFishWorkToPublicWorkDetail, mapJumpHopWorkToPublicWorkDetail, + mapPuzzleClearWorkToPublicWorkDetail, mapPublicWorkDetailToBigFishWork, mapPublicWorkDetailToPuzzleWork, mapPublicWorkDetailToSquareHoleWork, @@ -133,6 +135,13 @@ function buildTypedEntry( ...overrides, sourceType, }); + case 'puzzle-clear': + return narrowTypedEntry({ + ...common, + ...overrides, + sourceType, + themePrompt: '拼消消主题', + }); case 'big-fish': return narrowTypedEntry({ ...common, @@ -324,6 +333,7 @@ function buildJumpHopGalleryCard( profileId: 'jump-hop-profile', ownerUserId: 'user-1', authorDisplayName: '玩家', + themeText: '跳一跳', workTitle: '跳一跳作品', workDescription: '跳一跳摘要', coverImageSrc: '/jump-hop-cover.png', @@ -339,6 +349,31 @@ function buildJumpHopGalleryCard( }; } +function buildPuzzleClearGalleryCard( + overrides: Partial = {}, +): PuzzleClearGalleryCardResponse { + return { + runtimeKind: 'puzzle-clear', + publicWorkCode: 'PCLR-0001', + workId: 'puzzle-clear-work', + profileId: 'puzzle-clear-profile', + ownerUserId: 'user-1', + sourceSessionId: 'puzzle-clear-session', + authorDisplayName: '玩家', + workTitle: '拼消消作品', + workDescription: '拼消消摘要', + themePrompt: '水果', + coverImageSrc: '/puzzle-clear-cover.png', + publicationStatus: 'published', + playCount: 6, + updatedAt: '2026-06-01T01:00:00.000Z', + publishedAt: '2026-06-01T00:00:00.000Z', + publishReady: true, + generationStatus: 'ready', + ...overrides, + }; +} + function buildWoodenFishGalleryCard( overrides: Partial = {}, ): WoodenFishGalleryCardResponse { @@ -448,6 +483,7 @@ test('platform public work detail flow resolves detail kind for every play kind' > = [ ['big-fish', 'big-fish'], ['puzzle', 'puzzle'], + ['puzzle-clear', 'puzzle-clear'], ['jump-hop', 'jump-hop'], ['wooden-fish', 'wooden-fish'], ['match3d', 'match3d'], @@ -509,6 +545,13 @@ test('platform public work detail flow resolves open strategy', () => { kind: 'edutainment', }, ], + [ + buildTypedEntry('puzzle-clear'), + { + type: 'use-entry', + kind: 'puzzle-clear', + }, + ], [ buildTypedEntry('puzzle'), { @@ -595,6 +638,14 @@ test('platform public work detail flow maps work summaries to detail entries', ( profileId: 'jump-hop-profile', publicWorkCode: 'JH-0001', }); + expect( + mapPuzzleClearWorkToPublicWorkDetail(buildPuzzleClearGalleryCard()), + ).toMatchObject({ + sourceType: 'puzzle-clear', + workId: 'puzzle-clear-work', + profileId: 'puzzle-clear-profile', + publicWorkCode: 'PCLR-0001', + }); expect( mapWoodenFishWorkToPublicWorkDetail(buildWoodenFishGalleryCard()), ).toMatchObject({ @@ -773,6 +824,12 @@ test('platform public work detail flow resolves like intent', () => { type: 'like-puzzle', profileId: 'puzzle-profile', }); + expect( + resolvePlatformPublicWorkLikeIntent(buildTypedEntry('puzzle-clear')), + ).toEqual({ + type: 'unsupported', + errorMessage: '拼消消点赞将在后续版本开放。', + }); expect(resolvePlatformPublicWorkLikeIntent(buildRpgEntry())).toEqual({ type: 'like-rpg-gallery', ownerUserId: 'user-1', @@ -826,6 +883,12 @@ test('platform public work detail flow resolves remix intent', () => { profileId: 'puzzle-profile', selectionStage: 'puzzle-result', }); + expect( + resolvePlatformPublicWorkRemixIntent(buildTypedEntry('puzzle-clear')), + ).toEqual({ + type: 'unsupported', + errorMessage: '拼消消作品改造将在后续版本开放。', + }); expect(resolvePlatformPublicWorkRemixIntent(buildRpgEntry())).toEqual({ type: 'remix-rpg-gallery', ownerUserId: 'user-1', @@ -1038,6 +1101,15 @@ test('platform public work detail flow resolves edit intent for unsupported and type: 'blocked', errorMessage: '这份跳一跳作品暂时请从作品架编辑。', }); + expect( + resolvePlatformPublicWorkEditIntent( + buildTypedEntry('puzzle-clear'), + buildEditIntentDeps(), + ), + ).toEqual({ + type: 'blocked', + errorMessage: '这份拼消消作品暂时请从作品架编辑。', + }); expect( resolvePlatformPublicWorkEditIntent( buildTypedEntry('wooden-fish'), @@ -1126,6 +1198,16 @@ test('platform public work detail flow resolves start intent for direct launches profileId: 'jump-hop-profile', returnStage: 'work-detail', }); + expect( + resolvePlatformPublicWorkStartIntent( + buildTypedEntry('puzzle-clear'), + buildStartIntentDeps(), + ), + ).toEqual({ + type: 'start-puzzle-clear', + profileId: 'puzzle-clear-profile', + returnStage: 'work-detail', + }); expect( resolvePlatformPublicWorkStartIntent( buildTypedEntry('wooden-fish'), diff --git a/src/components/platform-entry/platformPublicWorkDetailFlow.ts b/src/components/platform-entry/platformPublicWorkDetailFlow.ts index 9bcb4c0f..33598106 100644 --- a/src/components/platform-entry/platformPublicWorkDetailFlow.ts +++ b/src/components/platform-entry/platformPublicWorkDetailFlow.ts @@ -7,6 +7,10 @@ import type { import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import type { + PuzzleClearGalleryCardResponse, + PuzzleClearWorkProfileResponse, +} from '../../../packages/shared/src/contracts/puzzleClear'; import type { CustomWorldGalleryCard, CustomWorldLibraryEntry, @@ -25,6 +29,7 @@ import { isEdutainmentGalleryEntry, isJumpHopGalleryEntry, isMatch3DGalleryEntry, + isPuzzleClearGalleryEntry, isPuzzleGalleryEntry, isSquareHoleGalleryEntry, isVisualNovelGalleryEntry, @@ -32,6 +37,7 @@ import { mapBarkBattleWorkToPlatformGalleryCard, mapBigFishWorkToPlatformGalleryCard, mapJumpHopWorkToPlatformGalleryCard, + mapPuzzleClearWorkToPlatformGalleryCard, mapPuzzleWorkToPlatformGalleryCard, mapSquareHoleWorkToPlatformGalleryCard, mapVisualNovelWorkToPlatformGalleryCard, @@ -50,6 +56,7 @@ export type PlatformPublicWorkDetailKind = | 'jump-hop' | 'match3d' | 'puzzle' + | 'puzzle-clear' | 'rpg' | 'square-hole' | 'visual-novel' @@ -199,6 +206,11 @@ export type PlatformPublicWorkStartIntent = returnStage: 'work-detail'; authMode: 'isolated'; } + | { + type: 'start-puzzle-clear'; + profileId: string; + returnStage: 'work-detail'; + } | { type: 'start-jump-hop'; profileId: string; @@ -325,6 +337,12 @@ export function mapJumpHopWorkToPublicWorkDetail( return mapJumpHopWorkToPlatformGalleryCard(item); } +export function mapPuzzleClearWorkToPublicWorkDetail( + item: PuzzleClearGalleryCardResponse | PuzzleClearWorkProfileResponse, +): PlatformPublicGalleryCard { + return mapPuzzleClearWorkToPlatformGalleryCard(item); +} + export function mapBarkBattleWorkToPublicWorkDetail( item: BarkBattleWorkSummary, ): PlatformPublicGalleryCard { @@ -512,6 +530,10 @@ export function getPlatformPublicWorkDetailKind( return 'puzzle'; } + if (isPuzzleClearGalleryEntry(entry)) { + return 'puzzle-clear'; + } + if (isJumpHopGalleryEntry(entry)) { return 'jump-hop'; } @@ -560,6 +582,13 @@ export function resolvePlatformPublicWorkDetailOpenStrategy( }; } + if (isPuzzleClearGalleryEntry(entry)) { + return { + type: 'use-entry', + kind: 'puzzle-clear', + }; + } + if (isJumpHopGalleryEntry(entry)) { return { type: 'load-jump-hop-detail', @@ -653,6 +682,13 @@ export function resolvePlatformPublicWorkLikeIntent( }; } + if (isPuzzleClearGalleryEntry(entry)) { + return { + type: 'unsupported', + errorMessage: '拼消消点赞将在后续版本开放。', + }; + } + if (isBarkBattleGalleryEntry(entry)) { return { type: 'unsupported', @@ -700,6 +736,13 @@ export function resolvePlatformPublicWorkRemixIntent( }; } + if (isPuzzleClearGalleryEntry(entry)) { + return { + type: 'unsupported', + errorMessage: '拼消消作品改造将在后续版本开放。', + }; + } + if (isMatch3DGalleryEntry(entry)) { return { type: 'unsupported', @@ -833,6 +876,13 @@ export function resolvePlatformPublicWorkEditIntent( }; } + if (isPuzzleClearGalleryEntry(entry)) { + return { + type: 'blocked', + errorMessage: '这份拼消消作品暂时请从作品架编辑。', + }; + } + if (isWoodenFishGalleryEntry(entry)) { return { type: 'blocked', @@ -944,6 +994,14 @@ export function resolvePlatformPublicWorkStartIntent( }; } + if (isPuzzleClearGalleryEntry(entry)) { + return { + type: 'start-puzzle-clear', + profileId: entry.profileId, + returnStage: 'work-detail', + }; + } + if (isJumpHopGalleryEntry(entry)) { return { type: 'start-jump-hop', diff --git a/src/components/platform-entry/platformSelectionStageModel.ts b/src/components/platform-entry/platformSelectionStageModel.ts index 86bd0bea..e14ad152 100644 --- a/src/components/platform-entry/platformSelectionStageModel.ts +++ b/src/components/platform-entry/platformSelectionStageModel.ts @@ -48,6 +48,10 @@ const PROTECTED_DATA_LOSS_STABLE_STAGE_BY_STAGE = { 'puzzle-result': false, 'puzzle-gallery-detail': true, 'puzzle-runtime': false, + 'puzzle-clear-workspace': true, + 'puzzle-clear-generating': false, + 'puzzle-clear-result': false, + 'puzzle-clear-runtime': false, 'custom-world-generating': false, 'custom-world-result': false, } as const satisfies Record; diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index 2484c14c..e2fc0461 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -358,6 +358,7 @@ const { })); vi.mock('../../services/apiClient', () => ({ + BACKGROUND_AUTH_REQUEST_OPTIONS: {}, refreshStoredAccessToken: mockRefreshStoredAccessToken, })); @@ -2670,7 +2671,7 @@ test('profile total play time card always uses hours', async () => { }); const playTimeCard = screen.getByRole('button', { - name: /累计游玩/u, + name: /累计游戏时长/u, }); expect(within(playTimeCard).getByText('1.5小时')).toBeTruthy(); @@ -2684,11 +2685,11 @@ test('profile played works card shows count unit', async () => { }); const playedCard = screen.getByRole('button', { - name: /已玩游戏\s*1个/u, + name: /已玩游戏数量\s*1个/u, }); expect(within(playedCard).getByText('1个')).toBeTruthy(); - expect(within(playedCard).queryByText('已玩游戏数量')).toBeNull(); + expect(within(playedCard).getByText('已玩游戏数量')).toBeTruthy(); await screen.findByText('1 / 1'); }); @@ -2700,8 +2701,12 @@ test('profile stats cards are centered without update timestamp', async () => { const walletCard = screen.getByRole('button', { name: /泥点余额\s*0/u, }); - const playTimeCard = screen.getByRole('button', { name: /累计游玩\s*0小时/u }); - const playedCard = screen.getByRole('button', { name: /已玩游戏\s*0个/u }); + const playTimeCard = screen.getByRole('button', { + name: /累计游戏时长\s*0小时/u, + }); + const playedCard = screen.getByRole('button', { + name: /已玩游戏数量\s*0个/u, + }); for (const card of [walletCard, playTimeCard, playedCard]) { expect(card.className).toContain('platform-profile-stat-card'); @@ -2753,8 +2758,8 @@ test('mobile profile page matches the reference layout sections', async () => { expect(statPanel.className).toContain('platform-profile-stats-panel'); expect(statPanel.querySelector('.platform-profile-stats-grid')).toBeTruthy(); expect(within(statPanel).getByRole('button', { name: /泥点余额\s*70/u })).toBeTruthy(); - expect(within(statPanel).getByRole('button', { name: /累计游玩\s*0小时/u })).toBeTruthy(); - expect(within(statPanel).getByRole('button', { name: /已玩游戏\s*0个/u })).toBeTruthy(); + expect(within(statPanel).getByRole('button', { name: /累计游戏时长\s*0小时/u })).toBeTruthy(); + expect(within(statPanel).getByRole('button', { name: /已玩游戏数量\s*0个/u })).toBeTruthy(); expect( within(statPanel).getByRole('button', { name: /泥点余额\s*70/u }).className, ).toContain('platform-profile-stat-card'); diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index e45ae5d1..8e171794 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -326,6 +326,15 @@ const WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const; const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180; const PROFILE_QR_SCAN_INTERVAL_MS = 360; +function getDelayUntilNextProfileTaskReset(nowMs = Date.now()) { + const shiftedNow = nowMs + PROFILE_TASK_BEIJING_OFFSET_MS; + const nextDayStart = + Math.floor(shiftedNow / PROFILE_TASK_DAY_MS) * PROFILE_TASK_DAY_MS + + PROFILE_TASK_DAY_MS; + const nextResetAt = nextDayStart - PROFILE_TASK_BEIJING_OFFSET_MS; + return Math.max(PROFILE_TASK_MIN_RESET_DELAY_MS, nextResetAt - nowMs); +} + type ProfileReferralPanel = 'invite' | 'redeem' | 'community'; type ProfilePopupPanel = ProfileReferralPanel | 'saveArchives'; type BarcodeDetectorLike = { diff --git a/src/components/rpg-entry/rpgEntryWorldPresentation.ts b/src/components/rpg-entry/rpgEntryWorldPresentation.ts index c9a60b3f..e5e82f09 100644 --- a/src/components/rpg-entry/rpgEntryWorldPresentation.ts +++ b/src/components/rpg-entry/rpgEntryWorldPresentation.ts @@ -1018,6 +1018,9 @@ export function describePlatformPublicWorkKind( if (isPuzzleGalleryEntry(entry)) { return formatPlatformWorkDisplayTag('拼图'); } + if (isPuzzleClearGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('拼消消'); + } if (isMatch3DGalleryEntry(entry)) { return formatPlatformWorkDisplayTag('抓大鹅'); } diff --git a/src/services/jump-hop/jumpHopClient.runtime.test.ts b/src/services/jump-hop/jumpHopClient.runtime.test.ts index fa534194..a14db1e8 100644 --- a/src/services/jump-hop/jumpHopClient.runtime.test.ts +++ b/src/services/jump-hop/jumpHopClient.runtime.test.ts @@ -56,7 +56,7 @@ describe('jumpHopClient runtime requests', () => { it('submits jump input with a generated client event id', async () => { await submitJumpHopJump( 'run/1', - { chargeMs: 320 }, + { dragDistance: 320 }, { runtimeGuestToken: 'runtime-guest-token' }, ); @@ -69,7 +69,7 @@ describe('jumpHopClient runtime requests', () => { Authorization: 'Bearer runtime-guest-token', }, body: JSON.stringify({ - chargeMs: 320, + dragDistance: 320, clientEventId: 'jump-run/1-1780000000000', }), }), diff --git a/src/services/jump-hop/jumpHopClient.ts b/src/services/jump-hop/jumpHopClient.ts index 10d2a532..21184562 100644 --- a/src/services/jump-hop/jumpHopClient.ts +++ b/src/services/jump-hop/jumpHopClient.ts @@ -5,6 +5,7 @@ import type { JumpHopGalleryCardResponse, JumpHopGalleryDetailResponse, JumpHopGalleryResponse, + JumpHopJumpRequest, JumpHopLeaderboardResponse, JumpHopRunResponse, JumpHopRuntimeRunSnapshotResponse, @@ -22,7 +23,11 @@ import { requestJson, } from '../apiClient'; import { createCreationAgentClient } from '../creation-agent'; -import { type RuntimeGuestRequestOptions } from '../runtimeGuestAuth'; +import { + buildRuntimeGuestAuthOptions, + buildRuntimeGuestHeaders, + type RuntimeGuestRequestOptions, +} from '../runtimeGuestAuth'; import { buildRuntimeApiPath, requestRuntimeJson } from '../runtimeRequest'; const JUMP_HOP_API_BASE = '/api/creation/jump-hop/sessions'; @@ -40,11 +45,10 @@ type JumpHopRuntimeMode = 'draft' | 'published'; type JumpHopStartRunOptions = JumpHopRuntimeRequestOptions & { runtimeMode?: JumpHopRuntimeMode; }; -type JumpHopJumpPayload = { - dragDistance: number; - dragVectorX?: number; - dragVectorY?: number; -}; +type JumpHopJumpPayload = Pick< + JumpHopJumpRequest, + 'dragDistance' | 'dragVectorX' | 'dragVectorY' +>; export type { JumpHopActionRequest, diff --git a/src/services/publicWorkCode.test.ts b/src/services/publicWorkCode.test.ts index e51c73fb..28f4f3bc 100644 --- a/src/services/publicWorkCode.test.ts +++ b/src/services/publicWorkCode.test.ts @@ -4,10 +4,12 @@ import { buildCustomWorldPublicWorkCode, buildJumpHopPublicWorkCode, buildMatch3DPublicWorkCode, + buildPuzzleClearPublicWorkCode, buildWoodenFishPublicWorkCode, isSameCustomWorldPublicWorkCode, isSameJumpHopPublicWorkCode, isSameMatch3DPublicWorkCode, + isSamePuzzleClearPublicWorkCode, isSameWoodenFishPublicWorkCode, } from './publicWorkCode';