refactor: 收口生成进度 tick 判定

This commit is contained in:
2026-06-04 02:49:23 +08:00
parent 80dab35646
commit 5dd73186b0
7 changed files with 320 additions and 25 deletions

View File

@@ -498,6 +498,7 @@ import type {
import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
import { PlatformErrorDialog } from './PlatformErrorDialog';
import { PlatformFeedbackView } from './PlatformFeedbackView';
import { resolvePlatformGenerationProgressTickDecision } from './platformGenerationProgressTickModel';
import {
buildMatch3DProfileFromSession,
hasMatch3DRuntimeAsset,
@@ -3807,32 +3808,25 @@ export function PlatformEntryFlowShellImpl({
]);
useEffect(() => {
const activeGenerationState =
selectionStage === 'puzzle-generating'
? puzzleGenerationState
: selectionStage === 'match3d-generating'
? match3dGenerationState
: selectionStage === 'big-fish-generating'
? bigFishGenerationState
: selectionStage === 'square-hole-generating'
? squareHoleGenerationState
: selectionStage === 'jump-hop-generating'
? jumpHopGenerationState
: selectionStage === 'wooden-fish-generating'
? woodenFishGenerationState
: selectionStage === 'baby-object-match-generating'
? babyObjectMatchGenerationState
: null;
const shouldTickProgress =
selectionStage === 'visual-novel-generating'
? visualNovelGenerationStartedAtMs != null &&
visualNovelGenerationPhase !== 'ready' &&
visualNovelGenerationPhase !== 'failed'
: activeGenerationState != null &&
activeGenerationState.phase !== 'ready' &&
activeGenerationState.phase !== 'failed';
const progressTickDecision =
resolvePlatformGenerationProgressTickDecision({
selectionStage,
miniGameStates: {
puzzle: puzzleGenerationState,
match3d: match3dGenerationState,
'big-fish': bigFishGenerationState,
'square-hole': squareHoleGenerationState,
'jump-hop': jumpHopGenerationState,
'wooden-fish': woodenFishGenerationState,
'baby-object-match': babyObjectMatchGenerationState,
},
visualNovel: {
startedAtMs: visualNovelGenerationStartedAtMs,
phase: visualNovelGenerationPhase,
},
});
if (!shouldTickProgress) {
if (!progressTickDecision.shouldTick) {
return undefined;
}

View File

@@ -0,0 +1,173 @@
import { describe, expect, test } from 'vitest';
import type {
MiniGameDraftGenerationKind,
MiniGameDraftGenerationPhase,
MiniGameDraftGenerationState,
} from '../../services/miniGameDraftGenerationProgress';
import type { SelectionStage } from './platformEntryTypes';
import { resolvePlatformGenerationProgressTickDecision } from './platformGenerationProgressTickModel';
function buildGenerationState(
kind: MiniGameDraftGenerationKind,
phase: MiniGameDraftGenerationPhase = 'compile',
): MiniGameDraftGenerationState {
return {
kind,
phase,
startedAtMs: 1000,
completedAssetCount: 0,
totalAssetCount: 1,
error: null,
};
}
describe('platformGenerationProgressTickModel', () => {
test('ticks while a mini-game generation stage has a running state', () => {
const cases: Array<
[stage: SelectionStage, kind: MiniGameDraftGenerationKind]
> = [
['puzzle-generating', 'puzzle'],
['match3d-generating', 'match3d'],
['big-fish-generating', 'big-fish'],
['square-hole-generating', 'square-hole'],
['jump-hop-generating', 'jump-hop'],
['wooden-fish-generating', 'wooden-fish'],
['baby-object-match-generating', 'baby-object-match'],
];
for (const [selectionStage, kind] of cases) {
expect(
resolvePlatformGenerationProgressTickDecision({
selectionStage,
miniGameStates: {
[kind]: buildGenerationState(kind),
},
visualNovel: {
startedAtMs: null,
phase: 'generating',
},
}),
).toEqual({
activeKind: kind,
shouldTick: true,
});
}
});
test('does not tick mini-game generation when state is missing or terminal', () => {
expect(
resolvePlatformGenerationProgressTickDecision({
selectionStage: 'puzzle-generating',
miniGameStates: {},
visualNovel: {
startedAtMs: null,
phase: 'generating',
},
}),
).toEqual({
activeKind: 'puzzle',
shouldTick: false,
});
for (const phase of ['ready', 'failed'] as const) {
expect(
resolvePlatformGenerationProgressTickDecision({
selectionStage: 'puzzle-generating',
miniGameStates: {
puzzle: buildGenerationState('puzzle', phase),
},
visualNovel: {
startedAtMs: null,
phase: 'generating',
},
}),
).toEqual({
activeKind: 'puzzle',
shouldTick: false,
});
}
});
test('does not tick when stage and mini-game state do not match', () => {
expect(
resolvePlatformGenerationProgressTickDecision({
selectionStage: 'puzzle-generating',
miniGameStates: {
match3d: buildGenerationState('match3d'),
},
visualNovel: {
startedAtMs: null,
phase: 'generating',
},
}),
).toEqual({
activeKind: 'puzzle',
shouldTick: false,
});
});
test('ticks visual novel generation only after it has started and before terminal phases', () => {
expect(
resolvePlatformGenerationProgressTickDecision({
selectionStage: 'visual-novel-generating',
miniGameStates: {},
visualNovel: {
startedAtMs: 1000,
phase: 'generating',
},
}),
).toEqual({
activeKind: 'visual-novel',
shouldTick: true,
});
expect(
resolvePlatformGenerationProgressTickDecision({
selectionStage: 'visual-novel-generating',
miniGameStates: {},
visualNovel: {
startedAtMs: null,
phase: 'generating',
},
}),
).toEqual({
activeKind: 'visual-novel',
shouldTick: false,
});
for (const phase of ['ready', 'failed'] as const) {
expect(
resolvePlatformGenerationProgressTickDecision({
selectionStage: 'visual-novel-generating',
miniGameStates: {},
visualNovel: {
startedAtMs: 1000,
phase,
},
}),
).toEqual({
activeKind: 'visual-novel',
shouldTick: false,
});
}
});
test('does not tick non-generation stages even when states are present', () => {
expect(
resolvePlatformGenerationProgressTickDecision({
selectionStage: 'platform',
miniGameStates: {
puzzle: buildGenerationState('puzzle'),
},
visualNovel: {
startedAtMs: 1000,
phase: 'generating',
},
}),
).toEqual({
activeKind: null,
shouldTick: false,
});
});
});

View File

@@ -0,0 +1,79 @@
import type {
MiniGameDraftGenerationKind,
MiniGameDraftGenerationState,
} from '../../services/miniGameDraftGenerationProgress';
import type { SelectionStage } from './platformEntryTypes';
export type PlatformVisualNovelGenerationPhase =
| 'generating'
| 'ready'
| 'failed';
export type PlatformGenerationProgressTickKind =
| MiniGameDraftGenerationKind
| 'visual-novel';
export type PlatformGenerationProgressTickInput = {
selectionStage: SelectionStage;
miniGameStates: Partial<
Record<MiniGameDraftGenerationKind, MiniGameDraftGenerationState | null>
>;
visualNovel: {
startedAtMs: number | null;
phase: PlatformVisualNovelGenerationPhase;
};
};
export type PlatformGenerationProgressTickDecision = {
activeKind: PlatformGenerationProgressTickKind | null;
shouldTick: boolean;
};
const MINI_GAME_GENERATION_STAGE_TO_KIND: Partial<
Record<SelectionStage, MiniGameDraftGenerationKind>
> = {
'puzzle-generating': 'puzzle',
'match3d-generating': 'match3d',
'big-fish-generating': 'big-fish',
'square-hole-generating': 'square-hole',
'jump-hop-generating': 'jump-hop',
'wooden-fish-generating': 'wooden-fish',
'baby-object-match-generating': 'baby-object-match',
};
function shouldTickMiniGameGenerationState(
state: MiniGameDraftGenerationState | null | undefined,
) {
return state != null && state.phase !== 'ready' && state.phase !== 'failed';
}
/** 收口生成页进度 tick 判定,壳层只保留 interval 副作用。 */
export function resolvePlatformGenerationProgressTickDecision(
input: PlatformGenerationProgressTickInput,
): PlatformGenerationProgressTickDecision {
if (input.selectionStage === 'visual-novel-generating') {
return {
activeKind: 'visual-novel',
shouldTick:
input.visualNovel.startedAtMs != null &&
input.visualNovel.phase !== 'ready' &&
input.visualNovel.phase !== 'failed',
};
}
const activeKind =
MINI_GAME_GENERATION_STAGE_TO_KIND[input.selectionStage] ?? null;
if (!activeKind) {
return {
activeKind: null,
shouldTick: false,
};
}
return {
activeKind,
shouldTick: shouldTickMiniGameGenerationState(
input.miniGameStates[activeKind],
),
};
}