feat(jump-hop): redesign sling platform gameplay

This commit is contained in:
2026-06-03 22:21:00 +08:00
parent 40ef89aeb5
commit 7d2d67a3f5
59 changed files with 6930 additions and 1973 deletions

View File

@@ -38,7 +38,10 @@ import type {
BabyObjectMatchDraft,
CreateBabyObjectMatchDraftRequest,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type {
JumpHopJumpRequest,
JumpHopWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import type {
CreateMatch3DSessionRequest,
ExecuteMatch3DActionRequest,
@@ -107,6 +110,7 @@ import type {
VisualNovelWorkDetail,
VisualNovelWorkSummary,
} from '../../../packages/shared/src/contracts/visualNovel';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import {
buildPublicWorkStagePath,
@@ -186,6 +190,7 @@ import {
jumpHopClient,
type JumpHopGalleryCardResponse,
type JumpHopRunResponse,
type JumpHopRuntimeRequestOptions,
type JumpHopSessionResponse,
type JumpHopSessionSnapshotResponse,
JumpHopWorkProfileResponse,
@@ -343,7 +348,6 @@ import {
type WoodenFishWorkProfileResponse,
type WoodenFishWorkspaceCreateRequest,
} from '../../services/wooden-fish/woodenFishClient';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { PublishShareModal } from '../common/PublishShareModal';
@@ -430,11 +434,11 @@ import {
PlatformErrorDialog,
type PlatformErrorDialogPayload,
} from './PlatformErrorDialog';
import { PlatformFeedbackView } from './PlatformFeedbackView';
import {
PlatformTaskCompletionDialog,
type PlatformTaskCompletionDialogPayload,
} from './PlatformTaskCompletionDialog';
import { PlatformFeedbackView } from './PlatformFeedbackView';
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
@@ -478,6 +482,30 @@ type PuzzleBackgroundCompileTask = {
error: string | null;
};
type MiniGameGenerationProgressTickStateMap = Partial<
Record<MiniGameDraftGenerationKind, MiniGameDraftGenerationState | null>
>;
export function resolveMiniGameGenerationProgressTickState(
selectionStage: SelectionStage,
states: MiniGameGenerationProgressTickStateMap,
) {
const stageKindMap: Partial<
Record<SelectionStage, MiniGameDraftGenerationKind>
> = {
'puzzle-generating': 'puzzle',
'big-fish-generating': 'big-fish',
'square-hole-generating': 'square-hole',
'match3d-generating': 'match3d',
'baby-object-match-generating': 'baby-object-match',
'jump-hop-generating': 'jump-hop',
'wooden-fish-generating': 'wooden-fish',
};
const kind = stageKindMap[selectionStage];
return kind ? (states[kind] ?? null) : null;
}
type PuzzleDetailReturnTarget = {
tab: PlatformHomeTab;
};
@@ -580,11 +608,11 @@ const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
'publish_missing_main_chapter',
'publish_missing_first_act',
]);
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS =
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS: JumpHopRuntimeRequestOptions =
BACKGROUND_AUTH_REQUEST_OPTIONS;
const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS =
const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS: JumpHopRuntimeRequestOptions =
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
async function buildRecommendRuntimeGuestOptions() {
async function buildRecommendRuntimeGuestOptions(): Promise<JumpHopRuntimeRequestOptions> {
const { token } = await ensureRuntimeGuestToken();
return {
...RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
@@ -599,9 +627,9 @@ function shouldUseRecommendRuntimeGuestAuth(
async function buildRecommendRuntimeAuthOptions(
authUi: { user?: { id?: string } | null } | null | undefined,
embedded?: boolean,
) {
): Promise<JumpHopRuntimeRequestOptions> {
if (!embedded) {
return {};
return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
}
if (shouldUseRecommendRuntimeGuestAuth(authUi)) {
@@ -2517,6 +2545,7 @@ function buildPendingJumpHopWorks(
profileId: `jump-hop-profile-${sessionId}`,
ownerUserId: '',
sourceSessionId: sessionId,
themeText: '跳一跳',
workTitle: '跳一跳草稿',
workDescription: '正在生成跳一跳玩法草稿。',
themeTags: [],
@@ -3233,6 +3262,8 @@ export function PlatformEntryFlowShellImpl({
const [jumpHopRun, setJumpHopRun] = useState<
JumpHopRunResponse['run'] | null
>(null);
const [jumpHopRuntimeRequestOptions, setJumpHopRuntimeRequestOptions] =
useState<JumpHopRuntimeRequestOptions | null>(null);
const [jumpHopWork, setJumpHopWork] =
useState<JumpHopWorkProfileResponse | null>(null);
const [jumpHopGalleryEntries, setJumpHopGalleryEntries] = useState<
@@ -4779,14 +4810,18 @@ export function PlatformEntryFlowShellImpl({
]);
useEffect(() => {
const activeGenerationState =
selectionStage === 'puzzle-generating'
? puzzleGenerationState
: selectionStage === 'match3d-generating'
? match3dGenerationState
: selectionStage === 'baby-object-match-generating'
? babyObjectMatchGenerationState
: null;
const activeGenerationState = resolveMiniGameGenerationProgressTickState(
selectionStage,
{
puzzle: puzzleGenerationState,
'big-fish': bigFishGenerationState,
'square-hole': squareHoleGenerationState,
match3d: match3dGenerationState,
'baby-object-match': babyObjectMatchGenerationState,
'jump-hop': jumpHopGenerationState,
'wooden-fish': woodenFishGenerationState,
},
);
const shouldTickProgress =
selectionStage === 'visual-novel-generating'
? visualNovelGenerationStartedAtMs != null &&
@@ -4808,11 +4843,15 @@ export function PlatformEntryFlowShellImpl({
return () => window.clearInterval(timerId);
}, [
babyObjectMatchGenerationState,
bigFishGenerationState,
jumpHopGenerationState,
match3dGenerationState,
puzzleGenerationState,
selectionStage,
squareHoleGenerationState,
visualNovelGenerationPhase,
visualNovelGenerationStartedAtMs,
woodenFishGenerationState,
]);
const runProtectedAction = useCallback(
@@ -6615,6 +6654,7 @@ export function PlatformEntryFlowShellImpl({
setJumpHopSession(null);
setJumpHopWork(null);
setJumpHopRun(null);
setJumpHopRuntimeRequestOptions(null);
setJumpHopGenerationState(null);
enterCreateTab();
setShowCreationTypeModal(false);
@@ -7629,6 +7669,7 @@ export function PlatformEntryFlowShellImpl({
setJumpHopRuntimeReturnStage('jump-hop-result');
setJumpHopGenerationState(null);
setJumpHopSession(null);
setJumpHopRuntimeRequestOptions(null);
setJumpHopError(null);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
@@ -8642,6 +8683,7 @@ export function PlatformEntryFlowShellImpl({
);
setJumpHopWork(null);
setJumpHopRun(null);
setJumpHopRuntimeRequestOptions(null);
setJumpHopGenerationState(generationState);
setIsJumpHopBusy(true);
setSelectionStage('jump-hop-generating');
@@ -8651,6 +8693,8 @@ export function PlatformEntryFlowShellImpl({
created.session.sessionId,
{
actionType: 'compile-draft',
themeText:
payload?.themeText ?? created.session.draft?.themeText,
workTitle: payload?.workTitle ?? created.session.draft?.workTitle,
workDescription:
payload?.workDescription ??
@@ -8742,7 +8786,7 @@ export function PlatformEntryFlowShellImpl({
}, [compileJumpHopSession, jumpHopSession, setSelectionStage]);
const regenerateJumpHopAsset = useCallback(
async (actionType: 'regenerate-character' | 'regenerate-tiles') => {
async (actionType: 'regenerate-tiles') => {
if (!jumpHopSession?.sessionId) {
setSelectionStage('jump-hop-workspace');
return;
@@ -8758,6 +8802,9 @@ export function PlatformEntryFlowShellImpl({
jumpHopSession.sessionId,
{
actionType,
profileId:
jumpHopWork?.summary.profileId ?? jumpHopSession.draft?.profileId,
themeText: jumpHopSession.draft?.themeText,
workTitle: jumpHopSession.draft?.workTitle,
workDescription: jumpHopSession.draft?.workDescription,
themeTags: jumpHopSession.draft?.themeTags,
@@ -8783,9 +8830,7 @@ export function PlatformEntryFlowShellImpl({
} catch (error) {
const errorMessage = resolveRpgCreationErrorMessage(
error,
actionType === 'regenerate-character'
? '重新生成跳一跳角色失败。'
: '重新生成跳一跳地块失败。',
'重新生成跳一跳地块失败。',
);
setJumpHopError(errorMessage);
setJumpHopGenerationState(
@@ -8858,7 +8903,9 @@ export function PlatformEntryFlowShellImpl({
setJumpHopError(null);
setJumpHopRuntimeReturnStage('jump-hop-result');
try {
const response = await jumpHopClient.startRun(profileId);
const response = await jumpHopClient.startRun(profileId, {
runtimeMode: 'draft',
});
setJumpHopRun(response.run);
setSelectionStage('jump-hop-runtime');
} catch (error) {
@@ -8889,13 +8936,30 @@ export function PlatformEntryFlowShellImpl({
setJumpHopError(null);
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
authUi,
options.embedded,
const runtimeGuestOptions =
options.embedded || shouldUseRecommendRuntimeGuestAuth(authUi)
? await buildRecommendRuntimeAuthOptions(authUi, true)
: RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
setJumpHopRuntimeRequestOptions(
runtimeGuestOptions.runtimeGuestToken?.trim()
? {
runtimeGuestToken: runtimeGuestOptions.runtimeGuestToken,
authImpact: runtimeGuestOptions.authImpact,
skipAuth: runtimeGuestOptions.skipAuth,
skipRefresh: runtimeGuestOptions.skipRefresh,
notifyAuthStateChange:
runtimeGuestOptions.notifyAuthStateChange,
clearAuthOnUnauthorized:
runtimeGuestOptions.clearAuthOnUnauthorized,
}
: null,
);
const [detail, runResponse] = await Promise.all([
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
jumpHopClient.startRun(normalizedProfileId, runtimeGuestOptions),
jumpHopClient.startRun(normalizedProfileId, {
...runtimeGuestOptions,
runtimeMode: 'published',
}),
]);
if (detail?.item) {
setJumpHopWork(detail.item);
@@ -8933,7 +8997,10 @@ export function PlatformEntryFlowShellImpl({
setIsJumpHopBusy(true);
setJumpHopError(null);
try {
const response = await jumpHopClient.restartRun(runId);
const response = await jumpHopClient.restartRun(
runId,
jumpHopRuntimeRequestOptions ?? undefined,
);
setJumpHopRun(response.run);
} catch (error) {
setJumpHopError(
@@ -8942,16 +9009,29 @@ export function PlatformEntryFlowShellImpl({
} finally {
setIsJumpHopBusy(false);
}
}, [jumpHopRun?.runId, startJumpHopTestRunFromProfile]);
}, [
jumpHopRun?.runId,
jumpHopRuntimeRequestOptions,
startJumpHopTestRunFromProfile,
]);
const submitJumpHopJumpAction = useCallback(
async (payload: { chargeMs: number }) => {
async (
payload: Pick<
JumpHopJumpRequest,
'dragDistance' | 'dragVectorX' | 'dragVectorY'
>,
) => {
const runId = jumpHopRun?.runId;
if (!runId) {
return;
}
try {
const response = await jumpHopClient.submitJump(runId, payload);
const response = await jumpHopClient.submitJump(
runId,
payload,
jumpHopRuntimeRequestOptions ?? undefined,
);
setJumpHopRun(response.run);
} catch (error) {
setJumpHopError(
@@ -8959,7 +9039,7 @@ export function PlatformEntryFlowShellImpl({
);
}
},
[jumpHopRun?.runId],
[jumpHopRun?.runId, jumpHopRuntimeRequestOptions],
);
const compileWoodenFishSession = useCallback(
@@ -13073,6 +13153,14 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isJumpHopGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError(null);
void startJumpHopRunFromProfile(selectedPublicWorkDetail.profileId, {
returnStage: 'work-detail',
});
return;
}
runProtectedAction(() => {
if (isBigFishGalleryEntry(selectedPublicWorkDetail)) {
const work = mapPublicWorkDetailToBigFishWork(selectedPublicWorkDetail);
@@ -13107,14 +13195,6 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isJumpHopGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError(null);
void startJumpHopRunFromProfile(selectedPublicWorkDetail.profileId, {
returnStage: 'work-detail',
});
return;
}
if (isWoodenFishGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError(null);
void startWoodenFishRunFromProfile(selectedPublicWorkDetail.profileId, {
@@ -13616,37 +13696,15 @@ export function PlatformEntryFlowShellImpl({
run={jumpHopRun}
isBusy={isJumpHopBusy}
error={jumpHopError}
runtimeRequestOptions={jumpHopRuntimeRequestOptions ?? undefined}
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
onRestart={() => {
if (!jumpHopRun?.runId || isJumpHopBusy) {
return;
}
setIsJumpHopBusy(true);
setJumpHopError(null);
void jumpHopClient
.restartRun(jumpHopRun.runId)
.then((response) => {
setJumpHopRun(response.run);
})
.catch((error) => {
setJumpHopError(
resolveRpgCreationErrorMessage(error, '重新开始跳一跳失败。'),
);
})
.finally(() => {
setIsJumpHopBusy(false);
});
void restartJumpHopRuntimeRun();
}}
onJump={async (payload) => {
const runId = jumpHopRun?.runId;
if (!runId) {
throw new Error('跳一跳运行态缺少 runId。');
}
const response = await jumpHopClient.submitJump(runId, payload);
setJumpHopRun(response.run);
await submitJumpHopJumpAction(payload);
}}
/>
);
@@ -15516,6 +15574,7 @@ export function PlatformEntryFlowShellImpl({
)}
progress={buildMiniGameDraftGenerationProgress(
bigFishGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isBigFishBusy}
error={bigFishError}
@@ -16110,6 +16169,7 @@ export function PlatformEntryFlowShellImpl({
)}
progress={buildMiniGameDraftGenerationProgress(
squareHoleGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isSquareHoleBusy}
error={squareHoleError}
@@ -16320,6 +16380,7 @@ export function PlatformEntryFlowShellImpl({
)}
progress={buildMiniGameDraftGenerationProgress(
jumpHopGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isJumpHopBusy}
error={jumpHopError}
@@ -16363,9 +16424,6 @@ export function PlatformEntryFlowShellImpl({
}}
onStartTestRun={startJumpHopTestRunFromProfile}
onPublish={publishJumpHopDraft}
onRegenerateCharacter={() => {
void regenerateJumpHopAsset('regenerate-character');
}}
onRegenerateTiles={() => {
void regenerateJumpHopAsset('regenerate-tiles');
}}
@@ -16390,6 +16448,7 @@ export function PlatformEntryFlowShellImpl({
profile={jumpHopWork}
isBusy={isJumpHopBusy}
error={jumpHopError}
runtimeRequestOptions={jumpHopRuntimeRequestOptions ?? undefined}
onBack={() => {
setSelectionStage(jumpHopRuntimeReturnStage);
}}
@@ -16448,6 +16507,7 @@ export function PlatformEntryFlowShellImpl({
)}
progress={buildMiniGameDraftGenerationProgress(
woodenFishGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isWoodenFishBusy}
error={woodenFishError}