init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,283 @@
import type { BigFishSessionSnapshotResponse } from '../../packages/shared/src/contracts/bigFish';
import type { 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'
| 'puzzle-images'
| 'puzzle-select-image'
| 'big-fish-main-images'
| 'big-fish-motions'
| 'big-fish-background'
| '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: 'compile',
label: '编译玩法草稿',
detail: '生成关卡角色描述、生态背景与运行参数。',
weight: 25,
},
{
id: 'big-fish-main-images',
label: '生成角色图片',
detail: '为每个成长阶段生成主形象。',
weight: 30,
},
{
id: 'big-fish-motions',
label: '生成动作素材',
detail: '补齐漂浮与游动动作素材。',
weight: 30,
},
{
id: 'big-fish-background',
label: '生成场地背景',
detail: '生成玩法场地背景图。',
weight: 15,
},
] 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: 'compile',
startedAtMs: Date.now(),
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
};
}
export function buildMiniGameDraftGenerationProgress(
state: MiniGameDraftGenerationState | null,
nowMs = Date.now(),
): CustomWorldGenerationProgress | null {
if (!state) {
return null;
}
const steps = getStepDefinitions(state.kind);
const activeStepIndex = getActiveStepIndex(steps, state.phase);
const completedWeight = steps
.slice(0, state.phase === 'ready' ? steps.length : activeStepIndex)
.reduce((sum, step) => sum + step.weight, 0);
const activeStep = steps[activeStepIndex] ?? steps[0];
const assetRatio =
state.totalAssetCount > 0
? Math.min(1, state.completedAssetCount / state.totalAssetCount)
: state.phase === 'ready'
? 1
: 0;
const overallProgress =
state.phase === 'failed'
? Math.max(1, completedWeight)
: state.phase === 'ready'
? 100
: completedWeight + activeStep.weight * assetRatio;
return {
phaseId: state.phase,
phaseLabel:
state.phase === 'failed'
? '生成失败'
: state.phase === 'ready'
? '生成完成'
: activeStep.label,
phaseDetail:
state.error ??
(state.phase === 'ready'
? '完整草稿与资产已准备完成。'
: activeStep.detail),
batchLabel: activeStep.label,
overallProgress: clampProgress(overallProgress),
completedWeight: clampProgress(overallProgress),
totalWeight: 100,
elapsedMs: Math.max(0, nowMs - state.startedAtMs),
estimatedRemainingMs: state.phase === 'ready' ? 0 : null,
activeStepIndex,
steps: buildMiniGameProgressSteps(steps, activeStepIndex, state),
};
}
export function buildPuzzleGenerationAnchorEntries(
session: PuzzleAgentSessionSnapshot | null | undefined,
): CustomWorldStructuredAnchorEntry[] {
if (!session) {
return [];
}
const draft = session.draft;
const entries: Array<MiniGameAnchorSource | null> = [
session.anchorPack.themePromise,
session.anchorPack.visualSubject,
session.anchorPack.visualMood,
session.anchorPack.compositionHooks,
session.anchorPack.tagsAndForbidden,
draft
? {
key: 'draft-summary',
label: '草稿摘要',
value: draft.summary,
}
: null,
draft?.coverImageSrc
? {
key: 'cover-image',
label: '正式图片',
value: '已生成并应用',
}
: 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());
}
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());
}