Merge remote-tracking branch 'origin/master' into codex/tiaoyitiao

# Conflicts:
#	server-rs/crates/api-server/src/jump_hop.rs
#	server-rs/crates/api-server/src/modules/jump_hop.rs
This commit is contained in:
2026-06-06 21:04:46 +08:00
451 changed files with 25780 additions and 2687 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,10 @@ export type SelectionStage =
| 'jump-hop-result'
| 'jump-hop-runtime'
| 'jump-hop-gallery-detail'
| 'puzzle-clear-workspace'
| 'puzzle-clear-generating'
| 'puzzle-clear-result'
| 'puzzle-clear-runtime'
| 'bark-battle-workspace'
| 'bark-battle-generating'
| 'bark-battle-result'

View File

@@ -0,0 +1,73 @@
import { describe, expect, test } from 'vitest';
import { createMiniGameDraftGenerationState } from '../../services/miniGameDraftGenerationProgress';
import { shouldTickPlatformGenerationProgressClock } from './platformGenerationProgressClock';
describe('platformGenerationProgressClock', () => {
test('ticks while puzzle clear generation is still running', () => {
expect(
shouldTickPlatformGenerationProgressClock({
selectionStage: 'puzzle-clear-generating',
generationState: createMiniGameDraftGenerationState('puzzle-clear'),
}),
).toBe(true);
});
test('stops ticking after puzzle clear generation is ready or failed', () => {
const runningState = createMiniGameDraftGenerationState('puzzle-clear');
expect(
shouldTickPlatformGenerationProgressClock({
selectionStage: 'puzzle-clear-generating',
generationState: { ...runningState, phase: 'ready' },
}),
).toBe(false);
expect(
shouldTickPlatformGenerationProgressClock({
selectionStage: 'puzzle-clear-generating',
generationState: { ...runningState, phase: 'failed' },
}),
).toBe(false);
});
test('ticks for other shared mini game generation stages', () => {
expect(
shouldTickPlatformGenerationProgressClock({
selectionStage: 'jump-hop-generating',
generationState: createMiniGameDraftGenerationState('jump-hop'),
}),
).toBe(true);
expect(
shouldTickPlatformGenerationProgressClock({
selectionStage: 'wooden-fish-generating',
generationState: createMiniGameDraftGenerationState('wooden-fish'),
}),
).toBe(true);
});
test('ticks visual novel generation from its phase source', () => {
expect(
shouldTickPlatformGenerationProgressClock({
selectionStage: 'visual-novel-generating',
visualNovelGenerationStartedAtMs: 1000,
visualNovelGenerationPhase: 'generating',
}),
).toBe(true);
expect(
shouldTickPlatformGenerationProgressClock({
selectionStage: 'visual-novel-generating',
visualNovelGenerationStartedAtMs: 1000,
visualNovelGenerationPhase: 'ready',
}),
).toBe(false);
});
test('does not tick when no generating stage is active', () => {
expect(
shouldTickPlatformGenerationProgressClock({
selectionStage: 'platform',
generationState: createMiniGameDraftGenerationState('puzzle-clear'),
}),
).toBe(false);
});
});

View File

@@ -0,0 +1,36 @@
import type { MiniGameDraftGenerationState } from '../../services/miniGameDraftGenerationProgress';
import type { SelectionStage } from './platformEntryTypes';
type VisualNovelEntryGenerationPhase = 'generating' | 'ready' | 'failed';
type PlatformGenerationProgressClockInput = {
selectionStage: SelectionStage;
generationState?: MiniGameDraftGenerationState | null;
visualNovelGenerationStartedAtMs?: number | null;
visualNovelGenerationPhase?: VisualNovelEntryGenerationPhase;
};
export function shouldTickPlatformGenerationProgressClock({
selectionStage,
generationState,
visualNovelGenerationStartedAtMs,
visualNovelGenerationPhase,
}: PlatformGenerationProgressClockInput) {
if (selectionStage === 'visual-novel-generating') {
return (
visualNovelGenerationStartedAtMs != null &&
visualNovelGenerationPhase !== 'ready' &&
visualNovelGenerationPhase !== 'failed'
);
}
if (!selectionStage.endsWith('-generating')) {
return false;
}
return Boolean(
generationState &&
generationState.phase !== 'ready' &&
generationState.phase !== 'failed',
);
}

View File

@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';
import { isPuzzleCompileActionReady } from './puzzleDraftGenerationState';
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
describe('isPuzzleCompileActionReady', () => {
it('keeps compile action generating until the draft has a cover image', () => {
const session = {
sessionId: 'puzzle-session-1',
draft: {
coverImageSrc: null,
levels: [
{
generationStatus: 'generating',
coverImageSrc: null,
},
],
},
} as PuzzleAgentSessionSnapshot;
expect(isPuzzleCompileActionReady(session)).toBe(false);
});
it('treats compile action as ready after the selected cover exists', () => {
const session = {
sessionId: 'puzzle-session-1',
draft: {
coverImageSrc: '/generated-puzzle-assets/session/cover.png',
levels: [
{
generationStatus: 'ready',
coverImageSrc: '/generated-puzzle-assets/session/cover.png',
},
],
},
} as PuzzleAgentSessionSnapshot;
expect(isPuzzleCompileActionReady(session)).toBe(true);
});
});

View File

@@ -0,0 +1,20 @@
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
function hasText(value: string | null | undefined) {
return typeof value === 'string' && value.trim().length > 0;
}
export function isPuzzleCompileActionReady(
session: PuzzleAgentSessionSnapshot,
) {
const draft = session.draft;
if (!draft) {
return false;
}
if (hasText(draft.coverImageSrc)) {
return true;
}
return (
draft.levels?.some((level) => hasText(level.coverImageSrc)) === true
);
}