325 lines
8.8 KiB
TypeScript
325 lines
8.8 KiB
TypeScript
import type { BigFishSessionSnapshotResponse } from '../../packages/shared/src/contracts/bigFish';
|
||
import type {
|
||
CreatePuzzleAgentSessionRequest,
|
||
PuzzleAgentSessionSnapshot,
|
||
} from '../../packages/shared/src/contracts/puzzleAgentSession';
|
||
import type {
|
||
CustomWorldGenerationProgress,
|
||
CustomWorldGenerationStep,
|
||
} from '../../packages/shared/src/contracts/runtime';
|
||
import type { CustomWorldStructuredAnchorEntry } from './customWorldAgentGenerationProgress';
|
||
|
||
export type MiniGameDraftGenerationKind = 'puzzle' | 'big-fish';
|
||
|
||
export type MiniGameDraftGenerationPhase =
|
||
| 'idle'
|
||
| 'compile'
|
||
| 'big-fish-draft'
|
||
| 'big-fish-levels'
|
||
| 'big-fish-runtime'
|
||
| 'puzzle-images'
|
||
| 'puzzle-select-image'
|
||
| 'ready'
|
||
| 'failed';
|
||
|
||
export type MiniGameDraftGenerationState = {
|
||
kind: MiniGameDraftGenerationKind;
|
||
phase: MiniGameDraftGenerationPhase;
|
||
startedAtMs: number;
|
||
completedAssetCount: number;
|
||
totalAssetCount: number;
|
||
error: string | null;
|
||
};
|
||
|
||
type MiniGameStepDefinition = {
|
||
id: MiniGameDraftGenerationPhase;
|
||
label: string;
|
||
detail: string;
|
||
weight: number;
|
||
};
|
||
|
||
type MiniGameAnchorSource = {
|
||
key: string;
|
||
label: string;
|
||
value: string;
|
||
};
|
||
|
||
const PUZZLE_STEPS = [
|
||
{
|
||
id: 'compile',
|
||
label: '编译拼图草稿',
|
||
detail: '整理主题、主体、构图与标签。',
|
||
weight: 34,
|
||
},
|
||
{
|
||
id: 'puzzle-images',
|
||
label: '生成拼图图片',
|
||
detail: '根据草稿生成候选图。',
|
||
weight: 33,
|
||
},
|
||
{
|
||
id: 'puzzle-select-image',
|
||
label: '确认正式图片',
|
||
detail: '选择候选图写入结果页。',
|
||
weight: 33,
|
||
},
|
||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||
|
||
const BIG_FISH_STEPS = [
|
||
{
|
||
id: 'big-fish-draft',
|
||
label: '整理玩法骨架',
|
||
detail: '收拢玩法承诺、成长阶梯与风险节奏。',
|
||
weight: 30,
|
||
},
|
||
{
|
||
id: 'big-fish-levels',
|
||
label: '编译等级蓝图',
|
||
detail: '生成每级角色描述、形象描述与动作描述。',
|
||
weight: 45,
|
||
},
|
||
{
|
||
id: 'big-fish-runtime',
|
||
label: '校准场地与参数',
|
||
detail: '整理背景蓝图与运行参数,准备结果页。',
|
||
weight: 25,
|
||
},
|
||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||
|
||
function clampProgress(value: number) {
|
||
return Math.max(0, Math.min(100, Math.round(value)));
|
||
}
|
||
|
||
function getStepDefinitions(kind: MiniGameDraftGenerationKind) {
|
||
return kind === 'puzzle' ? PUZZLE_STEPS : BIG_FISH_STEPS;
|
||
}
|
||
|
||
function getActiveStepIndex(
|
||
steps: ReadonlyArray<MiniGameStepDefinition>,
|
||
phase: MiniGameDraftGenerationPhase,
|
||
) {
|
||
if (phase === 'ready') {
|
||
return steps.length - 1;
|
||
}
|
||
const index = steps.findIndex((step) => step.id === phase);
|
||
return index >= 0 ? index : 0;
|
||
}
|
||
|
||
function buildMiniGameProgressSteps(
|
||
steps: ReadonlyArray<MiniGameStepDefinition>,
|
||
activeStepIndex: number,
|
||
state: MiniGameDraftGenerationState,
|
||
) {
|
||
return steps.map((step, index) => {
|
||
const isCompleted = state.phase === 'ready' || index < activeStepIndex;
|
||
const isActive =
|
||
state.phase !== 'failed' && !isCompleted && index === activeStepIndex;
|
||
const isAssetStep = step.id === state.phase && state.totalAssetCount > 0;
|
||
|
||
return {
|
||
id: step.id,
|
||
label: step.label,
|
||
detail: step.detail,
|
||
completed: isCompleted ? 1 : isAssetStep ? state.completedAssetCount : 0,
|
||
total: isAssetStep ? state.totalAssetCount : 1,
|
||
status: isCompleted ? 'completed' : isActive ? 'active' : 'pending',
|
||
} satisfies CustomWorldGenerationStep;
|
||
});
|
||
}
|
||
|
||
export function createMiniGameDraftGenerationState(
|
||
kind: MiniGameDraftGenerationKind,
|
||
): MiniGameDraftGenerationState {
|
||
return {
|
||
kind,
|
||
phase: kind === 'big-fish' ? 'big-fish-draft' : 'compile',
|
||
startedAtMs: Date.now(),
|
||
completedAssetCount: 0,
|
||
totalAssetCount: 0,
|
||
error: null,
|
||
};
|
||
}
|
||
|
||
function resolveBigFishPhaseByElapsedMs(
|
||
elapsedMs: number,
|
||
): MiniGameDraftGenerationPhase {
|
||
if (elapsedMs >= 4_500) {
|
||
return 'big-fish-runtime';
|
||
}
|
||
if (elapsedMs >= 1_800) {
|
||
return 'big-fish-levels';
|
||
}
|
||
return 'big-fish-draft';
|
||
}
|
||
|
||
export function buildMiniGameDraftGenerationProgress(
|
||
state: MiniGameDraftGenerationState | null,
|
||
nowMs = Date.now(),
|
||
): CustomWorldGenerationProgress | null {
|
||
if (!state) {
|
||
return null;
|
||
}
|
||
|
||
const elapsedMs = Math.max(0, nowMs - state.startedAtMs);
|
||
const normalizedState =
|
||
state.kind === 'big-fish' &&
|
||
state.phase !== 'failed' &&
|
||
state.phase !== 'ready'
|
||
? {
|
||
...state,
|
||
phase: resolveBigFishPhaseByElapsedMs(elapsedMs),
|
||
}
|
||
: state;
|
||
|
||
const steps = getStepDefinitions(normalizedState.kind);
|
||
const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase);
|
||
const completedWeight = steps
|
||
.slice(
|
||
0,
|
||
normalizedState.phase === 'ready' ? steps.length : activeStepIndex,
|
||
)
|
||
.reduce((sum, step) => sum + step.weight, 0);
|
||
const activeStep = steps[activeStepIndex] ?? steps[0];
|
||
const assetRatio =
|
||
normalizedState.totalAssetCount > 0
|
||
? Math.min(
|
||
1,
|
||
normalizedState.completedAssetCount / normalizedState.totalAssetCount,
|
||
)
|
||
: normalizedState.phase === 'ready'
|
||
? 1
|
||
: normalizedState.kind === 'big-fish'
|
||
? 0.55
|
||
: 0;
|
||
const overallProgress =
|
||
normalizedState.phase === 'failed'
|
||
? Math.max(1, completedWeight)
|
||
: normalizedState.phase === 'ready'
|
||
? 100
|
||
: completedWeight + activeStep.weight * assetRatio;
|
||
|
||
return {
|
||
phaseId: normalizedState.phase,
|
||
phaseLabel:
|
||
normalizedState.phase === 'failed'
|
||
? '生成失败'
|
||
: normalizedState.phase === 'ready'
|
||
? '生成完成'
|
||
: activeStep.label,
|
||
phaseDetail:
|
||
normalizedState.error ??
|
||
(normalizedState.phase === 'ready'
|
||
? normalizedState.kind === 'big-fish'
|
||
? '玩法草稿已准备完成,可进入结果页继续生成主图、动作和背景。'
|
||
: '完整草稿与资产已准备完成。'
|
||
: activeStep.detail),
|
||
batchLabel: activeStep.label,
|
||
overallProgress: clampProgress(overallProgress),
|
||
completedWeight: clampProgress(overallProgress),
|
||
totalWeight: 100,
|
||
elapsedMs,
|
||
estimatedRemainingMs:
|
||
normalizedState.phase === 'ready'
|
||
? 0
|
||
: normalizedState.kind === 'big-fish'
|
||
? Math.max(0, 7_000 - elapsedMs)
|
||
: null,
|
||
activeStepIndex,
|
||
steps: buildMiniGameProgressSteps(steps, activeStepIndex, normalizedState),
|
||
};
|
||
}
|
||
|
||
export function buildPuzzleGenerationAnchorEntries(
|
||
session: PuzzleAgentSessionSnapshot | null | undefined,
|
||
formPayload: CreatePuzzleAgentSessionRequest | null | undefined = null,
|
||
): CustomWorldStructuredAnchorEntry[] {
|
||
if (!session) {
|
||
return [];
|
||
}
|
||
|
||
const entries: Array<MiniGameAnchorSource | null> = [
|
||
{
|
||
key: 'puzzle-title',
|
||
label: '作品名称',
|
||
value:
|
||
formPayload?.workTitle?.trim() ||
|
||
formPayload?.seedText?.trim() ||
|
||
session.draft?.workTitle ||
|
||
session.anchorPack.themePromise.value,
|
||
},
|
||
{
|
||
key: 'work-description',
|
||
label: '作品描述',
|
||
value:
|
||
formPayload?.workDescription?.trim() ||
|
||
session.draft?.workDescription ||
|
||
'',
|
||
},
|
||
{
|
||
key: 'picture-description',
|
||
label: '画面描述',
|
||
value:
|
||
formPayload?.pictureDescription?.trim() ||
|
||
session.draft?.levels?.[0]?.pictureDescription ||
|
||
session.anchorPack.visualSubject.value,
|
||
},
|
||
];
|
||
|
||
return entries
|
||
.filter((entry): entry is MiniGameAnchorSource => Boolean(entry))
|
||
.map((entry) => ({
|
||
id: entry.key,
|
||
label: entry.label,
|
||
value: entry.value,
|
||
}))
|
||
.filter((entry) => entry.value.trim());
|
||
}
|
||
|
||
export function buildBigFishGenerationAnchorEntries(
|
||
session: BigFishSessionSnapshotResponse | null | undefined,
|
||
): CustomWorldStructuredAnchorEntry[] {
|
||
if (!session) {
|
||
return [];
|
||
}
|
||
|
||
const draft = session.draft;
|
||
const assetReadyCount = session.assetSlots.filter(
|
||
(slot) => slot.status === 'ready',
|
||
).length;
|
||
|
||
const entries: Array<MiniGameAnchorSource | null> = [
|
||
session.anchorPack.gameplayPromise,
|
||
session.anchorPack.ecologyVisualTheme,
|
||
session.anchorPack.growthLadder,
|
||
session.anchorPack.riskTempo,
|
||
draft
|
||
? {
|
||
key: 'level-characters',
|
||
label: '角色描述',
|
||
value: draft.levels
|
||
.map(
|
||
(level) =>
|
||
`Lv.${level.level} ${level.name}:${level.oneLineFantasy}`,
|
||
)
|
||
.join('\n'),
|
||
}
|
||
: null,
|
||
draft
|
||
? {
|
||
key: 'asset-coverage',
|
||
label: '图片与动作',
|
||
value: `已生成 ${assetReadyCount}/${session.assetSlots.length} 个资产`,
|
||
}
|
||
: null,
|
||
];
|
||
|
||
return entries
|
||
.filter((entry): entry is MiniGameAnchorSource => Boolean(entry))
|
||
.map((entry) => ({
|
||
id: entry.key,
|
||
label: entry.label,
|
||
value: entry.value,
|
||
}))
|
||
.filter((entry) => entry.value.trim());
|
||
}
|