This commit is contained in:
2026-05-01 01:30:02 +08:00
parent aabad6407f
commit 2e9d0f4640
92 changed files with 4548 additions and 248 deletions

View File

@@ -48,6 +48,7 @@ import type {
CustomWorldLibraryEntry,
ProfilePlayedWorkSummary,
ProfilePlayStatsResponse,
ProfileSaveArchiveResumeResponse,
ProfileSaveArchiveSummary,
} from '../../../packages/shared/src/contracts/runtime';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
@@ -125,13 +126,18 @@ import {
extendLocalPuzzleTime,
isLocalPuzzleRun,
refreshLocalPuzzleTimer,
resolvePuzzleRestartLevelId,
restartLocalPuzzleLevel,
setLocalPuzzlePaused,
startLocalPuzzleRun,
submitLocalPuzzleLeaderboard,
swapLocalPuzzlePieces,
} from '../../services/puzzle-runtime/puzzleLocalRuntime';
import { deletePuzzleWork, listPuzzleWorks } from '../../services/puzzle-works';
import {
claimPuzzleWorkPointIncentive,
deletePuzzleWork,
listPuzzleWorks,
} from '../../services/puzzle-works';
import { deleteRpgCreationAgentSession } from '../../services/rpg-creation';
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
import {
@@ -141,10 +147,8 @@ import {
recordRpgEntryWorldGalleryPlay,
remixRpgEntryWorldGallery,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import {
getRpgProfilePlayStats,
resumeRpgProfileSaveArchive,
} from '../../services/rpg-entry/rpgProfileClient';
import { getRpgProfilePlayStats } from '../../services/rpg-entry/rpgProfileClient';
import { requestRpgRuntimeJson } from '../../services/rpg-runtime/rpgRuntimeRequest';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import {
@@ -201,6 +205,16 @@ type PuzzleSaveArchiveState = {
currentLevelId?: unknown;
};
async function resumePuzzleProfileSaveArchiveRaw(worldKey: string) {
return requestRpgRuntimeJson<
ProfileSaveArchiveResumeResponse<PuzzleSaveArchiveState>
>(
`/profile/save-archives/${encodeURIComponent(worldKey)}`,
{ method: 'POST' },
'恢复拼图存档失败',
);
}
type AgentResultBlockerView = {
code?: string;
message: string;
@@ -297,6 +311,10 @@ function mapPublicWorkDetailToPuzzleWork(
playCount: entry.playCount ?? 0,
remixCount: entry.remixCount ?? 0,
likeCount: entry.likeCount ?? 0,
pointIncentiveTotalHalfPoints: 0,
pointIncentiveClaimedPoints: 0,
pointIncentiveTotalPoints: 0,
pointIncentiveClaimablePoints: 0,
publishReady: true,
levels:
entry.coverSlides?.map((slide, index) => ({
@@ -729,11 +747,10 @@ function mergePuzzleServiceRuntimeState(
}
const serviceLevel = serviceRun.currentLevel;
const leaderboardEntries =
serviceLevel.leaderboardEntries.length > 0
? serviceLevel.leaderboardEntries
: serviceRun.leaderboardEntries;
if (
currentRun.currentLevel.status === 'cleared' &&
serviceLevel.status !== 'cleared'
) {
return {
...currentRun,
recommendedNextProfileId: serviceRun.recommendedNextProfileId,
@@ -741,8 +758,27 @@ function mergePuzzleServiceRuntimeState(
nextLevelProfileId: serviceRun.nextLevelProfileId,
nextLevelId: serviceRun.nextLevelId,
recommendedNextWorks: serviceRun.recommendedNextWorks,
leaderboardEntries,
currentLevel: {
leaderboardEntries:
currentRun.currentLevel.leaderboardEntries.length > 0
? currentRun.currentLevel.leaderboardEntries
: currentRun.leaderboardEntries,
};
}
const leaderboardEntries =
serviceLevel.leaderboardEntries.length > 0
? serviceLevel.leaderboardEntries
: serviceRun.leaderboardEntries;
return {
...currentRun,
recommendedNextProfileId: serviceRun.recommendedNextProfileId,
nextLevelMode: serviceRun.nextLevelMode,
nextLevelProfileId: serviceRun.nextLevelProfileId,
nextLevelId: serviceRun.nextLevelId,
recommendedNextWorks: serviceRun.recommendedNextWorks,
leaderboardEntries,
currentLevel: {
...currentRun.currentLevel,
status: serviceLevel.status,
startedAtMs: serviceLevel.startedAtMs,
@@ -836,6 +872,8 @@ export function PlatformEntryFlowShellImpl({
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
string | null
>(null);
const [claimingPuzzlePointIncentiveProfileId, setClaimingPuzzlePointIncentiveProfileId] =
useState<string | null>(null);
const isBigFishCreationVisible = isPlatformCreationTypeVisible('big-fish');
const [profilePlayStats, setProfilePlayStats] =
useState<ProfilePlayStatsResponse | null>(null);
@@ -1569,6 +1607,7 @@ export function PlatformEntryFlowShellImpl({
setIsPuzzleNextLevelGenerating(false);
setPuzzleError(null);
setDeletingCreationWorkId(null);
setClaimingPuzzlePointIncentiveProfileId(null);
setProfilePlayStats(null);
setProfilePlayStatsError(null);
setIsProfilePlayStatsOpen(false);
@@ -1812,6 +1851,7 @@ export function PlatformEntryFlowShellImpl({
setPuzzleRun(run);
setPuzzleRuntimeReturnStage(returnStage);
setSelectionStage('puzzle-runtime');
void platformBootstrap.refreshSaveArchives();
pushAppHistoryPath(
buildPublicWorkStagePath(
'puzzle-runtime',
@@ -1830,6 +1870,7 @@ export function PlatformEntryFlowShellImpl({
},
[
isPuzzleBusy,
platformBootstrap,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
@@ -1863,6 +1904,10 @@ export function PlatformEntryFlowShellImpl({
playCount: 0,
remixCount: 0,
likeCount: 0,
pointIncentiveTotalHalfPoints: 0,
pointIncentiveClaimedPoints: 0,
pointIncentiveTotalPoints: 0,
pointIncentiveClaimablePoints: 0,
publishReady: Boolean(puzzleSession?.resultPreview?.publishReady),
levels: draft.levels,
} satisfies PuzzleWorkSummary;
@@ -1963,9 +2008,7 @@ export function PlatformEntryFlowShellImpl({
}
const timerId = window.setInterval(() => {
if (!isLocalPuzzleRun(puzzleRun)) {
return;
}
// 中文注释:正式 run 的棋盘交互也在前端即时裁决,倒计时展示同样走本地时钟;超时落库仍由 onTimeExpired 拉取后端快照完成。
setPuzzleRun((currentRun) =>
currentRun ? refreshLocalPuzzleTimer(currentRun) : currentRun,
);
@@ -2009,7 +2052,7 @@ export function PlatformEntryFlowShellImpl({
const syncPuzzleRuntimeTimeout = useCallback(async () => {
if (
!puzzleRun?.currentLevel ||
puzzleRun.currentLevel.status !== 'playing'
puzzleRun.currentLevel.status === 'cleared'
) {
return;
}
@@ -2040,9 +2083,11 @@ export function PlatformEntryFlowShellImpl({
if (!puzzleRun?.currentLevel) {
return null;
}
const expectedStatus =
propKind === 'extendTime' ? 'failed' : 'playing';
if (puzzleRun.currentLevel.status !== expectedStatus) {
const canUseProp =
propKind === 'extendTime'
? puzzleRun.currentLevel.status !== 'cleared'
: puzzleRun.currentLevel.status === 'playing';
if (!canUseProp) {
return null;
}
@@ -2072,6 +2117,7 @@ export function PlatformEntryFlowShellImpl({
puzzleRunRef.current = nextRun;
setPuzzleRun(nextRun);
void platformBootstrap.refreshProfileDashboard();
void platformBootstrap.refreshSaveArchives();
return nextRun;
},
[platformBootstrap, puzzleRun],
@@ -2084,6 +2130,10 @@ export function PlatformEntryFlowShellImpl({
}
setPuzzleError(null);
const restartLevelId = resolvePuzzleRestartLevelId(
puzzleRun,
selectedPuzzleDetail,
);
if (isLocalPuzzleRun(puzzleRun)) {
const nextRun = restartLocalPuzzleLevel(puzzleRunRef.current ?? puzzleRun);
puzzleRunRef.current = nextRun;
@@ -2098,7 +2148,7 @@ export function PlatformEntryFlowShellImpl({
? selectedPuzzleDetail
: undefined,
false,
currentLevel.levelId ?? null,
restartLevelId,
);
}, [
isPuzzleBusy,
@@ -2120,7 +2170,7 @@ export function PlatformEntryFlowShellImpl({
platformBootstrap.setSaveError(null);
try {
const resumedArchive = await resumeRpgProfileSaveArchive(
const resumedArchive = await resumePuzzleProfileSaveArchiveRaw(
entry.worldKey,
);
platformBootstrap.setSaveEntries((currentEntries) =>
@@ -2130,8 +2180,7 @@ export function PlatformEntryFlowShellImpl({
: currentEntry,
),
);
const gameState = resumedArchive.snapshot
.gameState as PuzzleSaveArchiveState;
const gameState = resumedArchive.snapshot.gameState;
const profileId =
typeof gameState.currentProfileId === 'string' &&
gameState.currentProfileId.trim()
@@ -2145,7 +2194,13 @@ export function PlatformEntryFlowShellImpl({
gameState.currentLevelId.trim()
? gameState.currentLevelId
: null;
await startPuzzleRunFromProfile(profileId, 'platform', undefined, false, levelId);
await startPuzzleRunFromProfile(
profileId,
'platform',
undefined,
false,
levelId,
);
} catch (error) {
platformBootstrap.setSaveError(
resolvePuzzleErrorMessage(error, '恢复拼图存档失败。'),
@@ -2191,7 +2246,27 @@ export function PlatformEntryFlowShellImpl({
if (isLocalPuzzleRun(puzzleRun)) {
setPuzzleRun(submitLocalPuzzleLeaderboard(puzzleRun, payload.nickname));
setIsPuzzleLeaderboardBusy(false);
void advanceLocalPuzzleNextLevel({
run: puzzleRun,
sourceSessionId:
selectedPuzzleDetail?.sourceSessionId ??
puzzleSession?.sessionId ??
null,
})
.then(({ run }) => {
setPuzzleRun((currentRun) => {
if (!currentRun) {
return currentRun;
}
return mergePuzzleServiceRuntimeState(currentRun, run);
});
})
.catch(() => {
// 中文注释:本地试玩缺少后端候选时保留本地排行榜和既有下一关入口,避免结算被探测请求打断。
})
.finally(() => {
setIsPuzzleLeaderboardBusy(false);
});
return;
}
@@ -2203,6 +2278,7 @@ export function PlatformEntryFlowShellImpl({
}
return mergePuzzleServiceRuntimeState(currentRun, run);
});
void platformBootstrap.refreshSaveArchives();
})
.catch((error) => {
submittedPuzzleLeaderboardKeysRef.current.delete(submitKey);
@@ -2215,8 +2291,11 @@ export function PlatformEntryFlowShellImpl({
});
}, [
authUi?.user?.displayName,
platformBootstrap,
puzzleRun,
puzzleSession,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setPuzzleError,
]);
@@ -2263,6 +2342,9 @@ export function PlatformEntryFlowShellImpl({
})
: await advancePuzzleNextLevel(puzzleRun.runId);
setPuzzleRun(run);
if (!isLocalPuzzleRun(puzzleRun)) {
void platformBootstrap.refreshSaveArchives();
}
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
} finally {
@@ -2272,6 +2354,7 @@ export function PlatformEntryFlowShellImpl({
}, [
isPuzzleBusy,
isPuzzleLeaderboardBusy,
platformBootstrap,
puzzleRun,
puzzleSession,
resolvePuzzleErrorMessage,
@@ -2565,6 +2648,55 @@ export function PlatformEntryFlowShellImpl({
[],
);
const handleClaimPuzzlePointIncentive = useCallback(
(work: PuzzleWorkSummary) => {
if (claimingPuzzlePointIncentiveProfileId) {
return;
}
runProtectedAction(() => {
setClaimingPuzzlePointIncentiveProfileId(work.profileId);
setPuzzleError(null);
void claimPuzzleWorkPointIncentive(work.profileId)
.then((response) => {
const updatedWork = response.item;
setPuzzleWorks((current) =>
current.map((item) =>
mergePuzzleWorkSummary(item, updatedWork),
),
);
setPuzzleGalleryEntries((current) =>
current.map((item) =>
mergePuzzleWorkSummary(item, updatedWork),
),
);
setSelectedPuzzleDetail((current) =>
current ? mergePuzzleWorkSummary(current, updatedWork) : current,
);
syncUpdatedPublicWorkDetail(
mapPuzzleWorkToPublicWorkDetail(updatedWork),
);
})
.catch((error) => {
setPuzzleError(
resolvePuzzleErrorMessage(error, '领取拼图积分激励失败。'),
);
})
.finally(() => {
setClaimingPuzzlePointIncentiveProfileId(null);
});
});
},
[
claimingPuzzlePointIncentiveProfileId,
resolvePuzzleErrorMessage,
runProtectedAction,
setPuzzleError,
syncUpdatedPublicWorkDetail,
],
);
const likePublicWork = useCallback(
(entry: PlatformPublicGalleryCard) => {
if (isPublicWorkDetailBusy) {
@@ -3480,6 +3612,10 @@ export function PlatformEntryFlowShellImpl({
onDeletePuzzle={(item) => {
handleDeletePuzzleWork(item);
}}
onClaimPuzzlePointIncentive={(item) => {
handleClaimPuzzlePointIncentive(item);
}}
claimingPuzzleProfileId={claimingPuzzlePointIncentiveProfileId}
/>
</Suspense>
);