@@ -10,6 +10,7 @@ import type {
|
||||
PuzzleRuntimeLevelSnapshot,
|
||||
SwapPuzzlePiecesRequest,
|
||||
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
||||
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
|
||||
const LOCAL_PUZZLE_RUN_ID_PREFIX = 'local-puzzle-run-';
|
||||
@@ -53,10 +54,6 @@ function resolvePuzzleLevelConfig(levelIndex: number): PuzzleLevelConfig {
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePuzzleGridSize(clearedLevelCount: number): PuzzleGridSize {
|
||||
return resolvePuzzleLevelConfig(clearedLevelCount + 1).gridSize;
|
||||
}
|
||||
|
||||
const PUZZLE_INITIAL_SHUFFLE_ATTEMPTS = 64;
|
||||
|
||||
function buildLocalPuzzleRunId(profileId: string) {
|
||||
@@ -724,20 +721,88 @@ function buildLocalLevelName(previousLevelName: string, levelIndex: number) {
|
||||
}
|
||||
|
||||
// 本地兜底只保证单次游玩闭环:通关后立即重建下一关棋盘,不写回后端。
|
||||
function buildFallbackLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
|
||||
function resolveWorkLevelIndexById(
|
||||
levels: PuzzleDraftLevel[] | undefined,
|
||||
levelId: string | null | undefined,
|
||||
) {
|
||||
if (!levelId) {
|
||||
return -1;
|
||||
}
|
||||
return levels?.findIndex((level) => level.levelId === levelId) ?? -1;
|
||||
}
|
||||
|
||||
function resolveWorkLevelById(
|
||||
levels: PuzzleDraftLevel[] | undefined,
|
||||
levelId: string | null | undefined,
|
||||
) {
|
||||
const levelIndex = resolveWorkLevelIndexById(levels, levelId);
|
||||
return levelIndex >= 0 ? (levels?.[levelIndex] ?? null) : null;
|
||||
}
|
||||
|
||||
function resolveNextSameWorkLevel(
|
||||
work: PuzzleWorkSummary | null | undefined,
|
||||
currentLevel: PuzzleRuntimeLevelSnapshot,
|
||||
) {
|
||||
const levels = work?.levels;
|
||||
if (!levels?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentLevelIndexById = resolveWorkLevelIndexById(
|
||||
levels,
|
||||
currentLevel.levelId,
|
||||
);
|
||||
const nextLevelIndex =
|
||||
currentLevelIndexById >= 0
|
||||
? currentLevelIndexById + 1
|
||||
: currentLevel.levelIndex;
|
||||
return levels[nextLevelIndex] ?? null;
|
||||
}
|
||||
|
||||
function applyLocalNextLevelHandoff(
|
||||
run: PuzzleRunSnapshot,
|
||||
work: PuzzleWorkSummary | null | undefined,
|
||||
currentLevel: PuzzleRuntimeLevelSnapshot,
|
||||
) {
|
||||
const nextLevel = resolveNextSameWorkLevel(work, currentLevel);
|
||||
return {
|
||||
...run,
|
||||
nextLevelMode: nextLevel ? ('sameWork' as const) : ('none' as const),
|
||||
nextLevelProfileId: nextLevel ? currentLevel.profileId : null,
|
||||
nextLevelId: nextLevel?.levelId ?? null,
|
||||
recommendedNextProfileId: nextLevel ? currentLevel.profileId : null,
|
||||
recommendedNextWorks: [],
|
||||
};
|
||||
}
|
||||
|
||||
function buildFallbackLocalLevel(
|
||||
run: PuzzleRunSnapshot,
|
||||
work?: PuzzleWorkSummary | null,
|
||||
target?: { profileId?: string; levelId?: string | null },
|
||||
): PuzzleRunSnapshot {
|
||||
const currentLevel = run.currentLevel;
|
||||
if (!currentLevel || currentLevel.status !== 'cleared') {
|
||||
return run;
|
||||
}
|
||||
|
||||
const nextLevelIndex = run.currentLevelIndex + 1;
|
||||
const gridSize = resolvePuzzleGridSize(run.clearedLevelCount);
|
||||
const gridSize = resolvePuzzleLevelConfig(nextLevelIndex).gridSize;
|
||||
const nextProfileId =
|
||||
run.recommendedNextProfileId ??
|
||||
target?.profileId?.trim() ||
|
||||
run.nextLevelProfileId ||
|
||||
run.recommendedNextProfileId ||
|
||||
buildLocalNextProfileId(run.entryProfileId, nextLevelIndex);
|
||||
const nextLevel =
|
||||
resolveWorkLevelById(work?.levels, target?.levelId ?? run.nextLevelId) ??
|
||||
resolveNextSameWorkLevel(work, currentLevel);
|
||||
const startedAtMs = Date.now();
|
||||
const nextLevelName =
|
||||
nextLevel?.levelName ??
|
||||
buildLocalLevelName(currentLevel.levelName, nextLevelIndex);
|
||||
const nextCoverImageSrc =
|
||||
nextLevel?.coverImageSrc ?? currentLevel.coverImageSrc;
|
||||
|
||||
return {
|
||||
const nextRun: PuzzleRunSnapshot = {
|
||||
...run,
|
||||
currentLevelIndex: nextLevelIndex,
|
||||
currentGridSize: gridSize,
|
||||
@@ -749,10 +814,10 @@ function buildFallbackLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
|
||||
...currentLevel,
|
||||
runId: run.runId,
|
||||
levelIndex: nextLevelIndex,
|
||||
levelId: null,
|
||||
levelId: nextLevel?.levelId ?? null,
|
||||
gridSize,
|
||||
profileId: nextProfileId,
|
||||
levelName: buildLocalLevelName(currentLevel.levelName, nextLevelIndex),
|
||||
levelName: nextLevelName,
|
||||
board: buildInitialBoard(
|
||||
gridSize,
|
||||
run.runId,
|
||||
@@ -763,28 +828,32 @@ function buildFallbackLocalLevel(run: PuzzleRunSnapshot): PuzzleRunSnapshot {
|
||||
startedAtMs,
|
||||
clearedAtMs: null,
|
||||
elapsedMs: null,
|
||||
coverImageSrc: nextCoverImageSrc,
|
||||
...buildLevelTimerFields(nextLevelIndex),
|
||||
leaderboardEntries: [],
|
||||
},
|
||||
recommendedNextProfileId: null,
|
||||
nextLevelMode: 'none',
|
||||
nextLevelProfileId: null,
|
||||
nextLevelId: null,
|
||||
recommendedNextWorks: [],
|
||||
leaderboardEntries: [],
|
||||
};
|
||||
|
||||
if (!nextRun.currentLevel) {
|
||||
return nextRun;
|
||||
}
|
||||
return applyLocalNextLevelHandoff(nextRun, work, nextRun.currentLevel);
|
||||
}
|
||||
|
||||
export function startLocalPuzzleRun(
|
||||
item: PuzzleWorkSummary,
|
||||
levelId?: string | null,
|
||||
): PuzzleRunSnapshot {
|
||||
const gridSize = resolvePuzzleGridSize(0);
|
||||
const gridSize = resolvePuzzleLevelConfig(1).gridSize;
|
||||
const runId = buildLocalPuzzleRunId(item.profileId);
|
||||
const startedAtMs = Date.now();
|
||||
const firstLevel = item.levels?.[0] ?? null;
|
||||
const requestedLevelIndex = resolveWorkLevelIndexById(item.levels, levelId);
|
||||
const currentLevelIndex = requestedLevelIndex >= 0 ? requestedLevelIndex : 0;
|
||||
const firstLevel = item.levels?.[currentLevelIndex] ?? null;
|
||||
const firstLevelName = firstLevel?.levelName || item.levelName;
|
||||
const firstCoverImageSrc = firstLevel?.coverImageSrc ?? item.coverImageSrc;
|
||||
const secondLevel = item.levels?.[1] ?? null;
|
||||
const nextSameWorkLevel = item.levels?.[currentLevelIndex + 1] ?? null;
|
||||
return {
|
||||
runId,
|
||||
entryProfileId: item.profileId,
|
||||
@@ -811,10 +880,10 @@ export function startLocalPuzzleRun(
|
||||
...buildLevelTimerFields(1),
|
||||
leaderboardEntries: [],
|
||||
},
|
||||
recommendedNextProfileId: null,
|
||||
nextLevelMode: secondLevel ? 'sameWork' : 'none',
|
||||
nextLevelProfileId: secondLevel ? item.profileId : null,
|
||||
nextLevelId: secondLevel?.levelId ?? null,
|
||||
recommendedNextProfileId: nextSameWorkLevel ? item.profileId : null,
|
||||
nextLevelMode: nextSameWorkLevel ? 'sameWork' : 'none',
|
||||
nextLevelProfileId: nextSameWorkLevel ? item.profileId : null,
|
||||
nextLevelId: nextSameWorkLevel?.levelId ?? null,
|
||||
recommendedNextWorks: [],
|
||||
leaderboardEntries: [],
|
||||
};
|
||||
@@ -823,6 +892,7 @@ export function startLocalPuzzleRun(
|
||||
export function swapLocalPuzzlePieces(
|
||||
run: PuzzleRunSnapshot,
|
||||
payload: SwapPuzzlePiecesRequest,
|
||||
work?: PuzzleWorkSummary | null,
|
||||
): PuzzleRunSnapshot {
|
||||
const timedRun = withResolvedTimer(run);
|
||||
const currentLevel = timedRun.currentLevel;
|
||||
@@ -843,10 +913,13 @@ export function swapLocalPuzzlePieces(
|
||||
second.currentRow = firstPosition.row;
|
||||
second.currentCol = firstPosition.col;
|
||||
|
||||
return applyNextBoard(
|
||||
const nextRun = applyNextBoard(
|
||||
timedRun,
|
||||
rebuildBoardSnapshot(currentLevel.gridSize, pieces),
|
||||
);
|
||||
return nextRun.currentLevel?.status === 'cleared'
|
||||
? syncLocalPuzzleRunHandoff(nextRun, work)
|
||||
: nextRun;
|
||||
}
|
||||
|
||||
function dragSinglePiece(
|
||||
@@ -968,6 +1041,7 @@ function dragGroup(
|
||||
export function dragLocalPuzzlePiece(
|
||||
run: PuzzleRunSnapshot,
|
||||
payload: DragPuzzlePieceRequest,
|
||||
work?: PuzzleWorkSummary | null,
|
||||
): PuzzleRunSnapshot {
|
||||
const timedRun = withResolvedTimer(run);
|
||||
const currentLevel = timedRun.currentLevel;
|
||||
@@ -1003,16 +1077,32 @@ export function dragLocalPuzzlePiece(
|
||||
dragSinglePiece(pieces, moving, payload.targetRow, payload.targetCol);
|
||||
}
|
||||
|
||||
return applyNextBoard(
|
||||
const nextRun = applyNextBoard(
|
||||
timedRun,
|
||||
rebuildBoardSnapshot(currentLevel.gridSize, pieces),
|
||||
);
|
||||
return nextRun.currentLevel?.status === 'cleared'
|
||||
? syncLocalPuzzleRunHandoff(nextRun, work)
|
||||
: nextRun;
|
||||
}
|
||||
|
||||
export function advanceLocalPuzzleLevel(
|
||||
run: PuzzleRunSnapshot,
|
||||
work?: PuzzleWorkSummary | null,
|
||||
target?: { profileId?: string; levelId?: string | null },
|
||||
): PuzzleRunSnapshot {
|
||||
return buildFallbackLocalLevel(run);
|
||||
return buildFallbackLocalLevel(run, work, target);
|
||||
}
|
||||
|
||||
export function syncLocalPuzzleRunHandoff(
|
||||
run: PuzzleRunSnapshot,
|
||||
work: PuzzleWorkSummary | null | undefined,
|
||||
): PuzzleRunSnapshot {
|
||||
const currentLevel = run.currentLevel;
|
||||
if (!currentLevel) {
|
||||
return run;
|
||||
}
|
||||
return applyLocalNextLevelHandoff(run, work, currentLevel);
|
||||
}
|
||||
|
||||
export function restartLocalPuzzleLevel(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
DragPuzzlePieceRequest,
|
||||
AdvancePuzzleNextLevelRequest,
|
||||
PuzzleRunResponse,
|
||||
StartPuzzleRunRequest,
|
||||
SubmitPuzzleLeaderboardRequest,
|
||||
@@ -101,11 +102,21 @@ export async function dragPuzzlePieceOrGroup(
|
||||
/**
|
||||
* 进入推荐出的下一关。
|
||||
*/
|
||||
export async function advancePuzzleNextLevel(runId: string) {
|
||||
export async function advancePuzzleNextLevel(
|
||||
runId: string,
|
||||
payload: AdvancePuzzleNextLevelRequest = {},
|
||||
) {
|
||||
const targetProfileId = payload.targetProfileId?.trim() ?? '';
|
||||
return requestJson<PuzzleRunResponse>(
|
||||
`${PUZZLE_RUNTIME_API_BASE}/${encodeURIComponent(runId)}/next-level`,
|
||||
{
|
||||
method: 'POST',
|
||||
...(targetProfileId
|
||||
? {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ targetProfileId }),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
'进入下一关失败',
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user