feat: add baby object match edutainment flow
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
BABY_OBJECT_MATCH_EDUTAINMENT_TAG,
|
||||
hasBabyObjectMatchRequiredTag,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import {
|
||||
createBabyObjectMatchDraft,
|
||||
deleteLocalBabyObjectMatchDraft,
|
||||
listLocalBabyObjectMatchDrafts,
|
||||
publishBabyObjectMatchWork,
|
||||
} from './babyObjectMatchClient';
|
||||
|
||||
describe('babyObjectMatchClient', () => {
|
||||
beforeEach(() => {
|
||||
const store = new Map<string, string>();
|
||||
vi.stubGlobal('window', {
|
||||
localStorage: {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store.set(key, value);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test('creates local demo draft with exact edutainment tag', async () => {
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: () => '11111111-2222-3333-4444-555555555555',
|
||||
});
|
||||
|
||||
const response = await createBabyObjectMatchDraft({
|
||||
itemAName: ' 苹果 ',
|
||||
itemBName: '香蕉',
|
||||
});
|
||||
|
||||
expect(response.draft.templateName).toBe('宝贝识物');
|
||||
expect(response.draft.itemNames).toEqual(['苹果', '香蕉']);
|
||||
expect(response.draft.itemAssets).toHaveLength(2);
|
||||
expect(response.draft.itemAssets[0]?.generationProvider).toBe(
|
||||
'placeholder',
|
||||
);
|
||||
expect(response.draft.themeTags).toContain(
|
||||
BABY_OBJECT_MATCH_EDUTAINMENT_TAG,
|
||||
);
|
||||
expect(hasBabyObjectMatchRequiredTag(response.draft.themeTags)).toBe(true);
|
||||
});
|
||||
|
||||
test('rejects draft creation when any item name is empty', async () => {
|
||||
await expect(
|
||||
createBabyObjectMatchDraft({
|
||||
itemAName: '苹果',
|
||||
itemBName: ' ',
|
||||
}),
|
||||
).rejects.toThrow('请填写两个物品名称。');
|
||||
});
|
||||
|
||||
test('publish normalizes exact edutainment tag into payload', async () => {
|
||||
const response = await createBabyObjectMatchDraft({
|
||||
itemAName: '杯子',
|
||||
itemBName: '勺子',
|
||||
});
|
||||
const published = await publishBabyObjectMatchWork({
|
||||
draft: {
|
||||
...response.draft,
|
||||
themeTags: ['儿童教育', '寓教于乐 '],
|
||||
},
|
||||
});
|
||||
|
||||
expect(published.publicWorkCode).toMatch(/^BO-/u);
|
||||
expect(published.draft.publicationStatus).toBe('published');
|
||||
expect(published.draft.themeTags[0]).toBe(
|
||||
BABY_OBJECT_MATCH_EDUTAINMENT_TAG,
|
||||
);
|
||||
expect(hasBabyObjectMatchRequiredTag(published.draft.themeTags)).toBe(true);
|
||||
});
|
||||
|
||||
test('deletes local baby object match draft by profile id', async () => {
|
||||
const response = await createBabyObjectMatchDraft({
|
||||
itemAName: '苹果',
|
||||
itemBName: '香蕉',
|
||||
});
|
||||
|
||||
expect(listLocalBabyObjectMatchDrafts()).toHaveLength(1);
|
||||
|
||||
const nextItems = deleteLocalBabyObjectMatchDraft(response.draft.profileId);
|
||||
|
||||
expect(nextItems).toHaveLength(0);
|
||||
expect(listLocalBabyObjectMatchDrafts()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
231
src/services/edutainment-baby-object/babyObjectMatchClient.ts
Normal file
231
src/services/edutainment-baby-object/babyObjectMatchClient.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import type {
|
||||
BabyObjectMatchDraft,
|
||||
BabyObjectMatchItemAsset,
|
||||
BabyObjectMatchPublishRequest,
|
||||
BabyObjectMatchPublishResponse,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
SaveBabyObjectMatchDraftRequest,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import {
|
||||
BABY_OBJECT_MATCH_EDUTAINMENT_TAG,
|
||||
BABY_OBJECT_MATCH_TEMPLATE_ID,
|
||||
BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||
normalizeBabyObjectMatchTags,
|
||||
validateBabyObjectMatchItemNames,
|
||||
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import { buildBabyObjectMatchPublicWorkCode } from '../publicWorkCode';
|
||||
|
||||
const STORAGE_KEY = 'genarrative.edutainmentBabyObject.localDrafts.v1';
|
||||
|
||||
type LocalDraftStore = Record<string, BabyObjectMatchDraft>;
|
||||
|
||||
function canUseLocalStorage() {
|
||||
return (
|
||||
typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'
|
||||
);
|
||||
}
|
||||
|
||||
function readLocalDraftStore(): LocalDraftStore {
|
||||
if (!canUseLocalStorage()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const rawValue = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!rawValue) {
|
||||
return {};
|
||||
}
|
||||
const parsed = JSON.parse(rawValue) as LocalDraftStore;
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeLocalDraftStore(store: LocalDraftStore) {
|
||||
if (!canUseLocalStorage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
|
||||
}
|
||||
|
||||
function createLocalId(prefix: string) {
|
||||
const randomPart =
|
||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
? crypto.randomUUID().replace(/-/gu, '')
|
||||
: Math.random().toString(36).slice(2);
|
||||
|
||||
return `${prefix}-${Date.now().toString(36)}-${randomPart.slice(0, 12)}`;
|
||||
}
|
||||
|
||||
function encodeSvgDataUri(svg: string) {
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
function buildPlaceholderItemImage(itemName: string, index: number) {
|
||||
const palettes = [
|
||||
{
|
||||
bg: '#fef3c7',
|
||||
accent: '#fb7185',
|
||||
shadow: '#f59e0b',
|
||||
text: '#7c2d12',
|
||||
},
|
||||
{
|
||||
bg: '#dbeafe',
|
||||
accent: '#34d399',
|
||||
shadow: '#60a5fa',
|
||||
text: '#064e3b',
|
||||
},
|
||||
] as const;
|
||||
const palette = palettes[index % palettes.length]!;
|
||||
const displayText = itemName.slice(0, 6);
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="96" fill="${palette.bg}"/><circle cx="256" cy="238" r="132" fill="${palette.accent}" opacity=".92"/><ellipse cx="256" cy="356" rx="132" ry="34" fill="${palette.shadow}" opacity=".22"/><circle cx="210" cy="202" r="24" fill="#fff" opacity=".82"/><circle cx="302" cy="202" r="24" fill="#fff" opacity=".82"/><path d="M204 276c30 30 74 30 104 0" fill="none" stroke="#fff" stroke-width="18" stroke-linecap="round"/><text x="256" y="438" text-anchor="middle" font-family="Arial,'Microsoft YaHei',sans-serif" font-size="42" font-weight="700" fill="${palette.text}">${displayText}</text></svg>`;
|
||||
|
||||
return encodeSvgDataUri(svg);
|
||||
}
|
||||
|
||||
function buildItemAsset(
|
||||
itemName: string,
|
||||
index: number,
|
||||
): BabyObjectMatchItemAsset {
|
||||
return {
|
||||
itemId: `baby-object-item-${index + 1}`,
|
||||
itemName,
|
||||
imageSrc: buildPlaceholderItemImage(itemName, index),
|
||||
assetObjectId: null,
|
||||
generationProvider: 'placeholder',
|
||||
prompt: `生成适合 4-8 岁儿童识物分类游戏的${itemName}物品图,绘本草地舞台风格,单个物体,透明或干净背景,无文字、无水印、无按钮。`,
|
||||
};
|
||||
}
|
||||
|
||||
function saveDraftToLocalStore(draft: BabyObjectMatchDraft) {
|
||||
const store = readLocalDraftStore();
|
||||
store[draft.profileId] = draft;
|
||||
writeLocalDraftStore(store);
|
||||
}
|
||||
|
||||
export function normalizeBabyObjectMatchDraft(
|
||||
draft: BabyObjectMatchDraft,
|
||||
): BabyObjectMatchDraft {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
...draft,
|
||||
templateId: BABY_OBJECT_MATCH_TEMPLATE_ID,
|
||||
templateName: BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||
workTitle: draft.workTitle.trim() || '宝贝识物',
|
||||
workDescription: draft.workDescription.trim(),
|
||||
itemNames: [
|
||||
draft.itemNames[0].trim(),
|
||||
draft.itemNames[1].trim(),
|
||||
],
|
||||
itemAssets: [
|
||||
{
|
||||
...draft.itemAssets[0],
|
||||
itemName: draft.itemNames[0].trim(),
|
||||
},
|
||||
{
|
||||
...draft.itemAssets[1],
|
||||
itemName: draft.itemNames[1].trim(),
|
||||
},
|
||||
],
|
||||
themeTags: normalizeBabyObjectMatchTags(draft.themeTags),
|
||||
updatedAt: draft.updatedAt || now,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前为本地 Demo 创作链路。真实 image-2 接入后替换为后端接口,
|
||||
* 但返回契约保持 BabyObjectMatchDraftResponse。
|
||||
*/
|
||||
export async function createBabyObjectMatchDraft(
|
||||
payload: CreateBabyObjectMatchDraftRequest,
|
||||
) {
|
||||
const validated = validateBabyObjectMatchItemNames(payload);
|
||||
if (!validated.valid) {
|
||||
throw new Error('请填写两个物品名称。');
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const draftId = createLocalId('baby-object-draft');
|
||||
const profileId = createLocalId('baby-object-profile');
|
||||
const itemNames: [string, string] = [
|
||||
validated.itemAName,
|
||||
validated.itemBName,
|
||||
];
|
||||
const draft = normalizeBabyObjectMatchDraft({
|
||||
draftId,
|
||||
profileId,
|
||||
templateId: BABY_OBJECT_MATCH_TEMPLATE_ID,
|
||||
templateName: BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||
workTitle: '宝贝识物',
|
||||
workDescription: `${itemNames[0]}和${itemNames[1]}识物分类`,
|
||||
itemNames,
|
||||
itemAssets: [buildItemAsset(itemNames[0], 0), buildItemAsset(itemNames[1], 1)],
|
||||
themeTags: [BABY_OBJECT_MATCH_EDUTAINMENT_TAG, '宝贝识物'],
|
||||
publicationStatus: 'draft',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
publishedAt: null,
|
||||
});
|
||||
|
||||
saveDraftToLocalStore(draft);
|
||||
return { draft };
|
||||
}
|
||||
|
||||
export async function saveBabyObjectMatchDraft(
|
||||
payload: SaveBabyObjectMatchDraftRequest,
|
||||
) {
|
||||
const draft = normalizeBabyObjectMatchDraft({
|
||||
...payload.draft,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
saveDraftToLocalStore(draft);
|
||||
|
||||
return { draft };
|
||||
}
|
||||
|
||||
export async function publishBabyObjectMatchWork(
|
||||
payload: BabyObjectMatchPublishRequest,
|
||||
): Promise<BabyObjectMatchPublishResponse> {
|
||||
const draft = normalizeBabyObjectMatchDraft({
|
||||
...payload.draft,
|
||||
publicationStatus: 'published',
|
||||
publishedAt: payload.draft.publishedAt ?? new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
saveDraftToLocalStore(draft);
|
||||
|
||||
return {
|
||||
draft,
|
||||
publicWorkCode: buildBabyObjectMatchPublicWorkCode(draft.profileId),
|
||||
};
|
||||
}
|
||||
|
||||
export function listLocalBabyObjectMatchDrafts() {
|
||||
return Object.values(readLocalDraftStore()).sort(
|
||||
(left, right) =>
|
||||
new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(),
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteLocalBabyObjectMatchDraft(profileId: string) {
|
||||
const normalizedProfileId = profileId.trim();
|
||||
if (!normalizedProfileId) {
|
||||
return listLocalBabyObjectMatchDrafts();
|
||||
}
|
||||
|
||||
const store = readLocalDraftStore();
|
||||
delete store[normalizedProfileId];
|
||||
writeLocalDraftStore(store);
|
||||
|
||||
return listLocalBabyObjectMatchDrafts();
|
||||
}
|
||||
|
||||
export const babyObjectMatchClient = {
|
||||
createDraft: createBabyObjectMatchDraft,
|
||||
deleteDraft: deleteLocalBabyObjectMatchDraft,
|
||||
saveDraft: saveBabyObjectMatchDraft,
|
||||
publish: publishBabyObjectMatchWork,
|
||||
listLocalDrafts: listLocalBabyObjectMatchDrafts,
|
||||
};
|
||||
1
src/services/edutainment-baby-object/index.ts
Normal file
1
src/services/edutainment-baby-object/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './babyObjectMatchClient';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
buildBabyObjectMatchGenerationAnchorEntries,
|
||||
buildMatch3DGenerationAnchorEntries,
|
||||
buildMiniGameDraftGenerationProgress,
|
||||
buildPuzzleGenerationAnchorEntries,
|
||||
@@ -226,6 +227,37 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('baby object match generation exposes two item names', () => {
|
||||
const state = createMiniGameDraftGenerationState('baby-object-match');
|
||||
const progress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
state.startedAtMs + 9_000,
|
||||
);
|
||||
const entries = buildBabyObjectMatchGenerationAnchorEntries({
|
||||
itemAName: '苹果',
|
||||
itemBName: '香蕉',
|
||||
});
|
||||
|
||||
expect(progress?.steps.map((step) => step.id)).toEqual([
|
||||
'baby-object-draft',
|
||||
'baby-object-images',
|
||||
'baby-object-ready',
|
||||
]);
|
||||
expect(progress?.phaseId).toBe('baby-object-images');
|
||||
expect(entries).toEqual([
|
||||
{
|
||||
id: 'baby-object-item-1',
|
||||
label: '物品 1',
|
||||
value: '苹果',
|
||||
},
|
||||
{
|
||||
id: 'baby-object-item-2',
|
||||
label: '物品 2',
|
||||
value: '香蕉',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('puzzle generation anchors expose form payload as the display source', () => {
|
||||
const entries = buildPuzzleGenerationAnchorEntries({
|
||||
sessionId: 'puzzle-session-1',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { BigFishSessionSnapshotResponse } from '../../packages/shared/src/contracts/bigFish';
|
||||
import type {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
} from '../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type {
|
||||
CreateMatch3DSessionRequest,
|
||||
Match3DAgentSessionSnapshot,
|
||||
@@ -18,7 +22,8 @@ export type MiniGameDraftGenerationKind =
|
||||
| 'puzzle'
|
||||
| 'big-fish'
|
||||
| 'square-hole'
|
||||
| 'match3d';
|
||||
| 'match3d'
|
||||
| 'baby-object-match';
|
||||
|
||||
export type MiniGameDraftGenerationPhase =
|
||||
| 'idle'
|
||||
@@ -37,6 +42,9 @@ export type MiniGameDraftGenerationPhase =
|
||||
| 'match3d-upload-images'
|
||||
| 'match3d-generate-models'
|
||||
| 'match3d-ready'
|
||||
| 'baby-object-draft'
|
||||
| 'baby-object-images'
|
||||
| 'baby-object-ready'
|
||||
| 'puzzle-images'
|
||||
| 'puzzle-select-image'
|
||||
| 'ready'
|
||||
@@ -191,6 +199,27 @@ const MATCH3D_PHASE_ORDER: Partial<
|
||||
'match3d-generate-models': 5,
|
||||
};
|
||||
|
||||
const BABY_OBJECT_MATCH_STEPS = [
|
||||
{
|
||||
id: 'baby-object-draft',
|
||||
label: '整理识物草稿',
|
||||
detail: '写入两个物品名称与寓教于乐标签。',
|
||||
weight: 22,
|
||||
},
|
||||
{
|
||||
id: 'baby-object-images',
|
||||
label: '生成物品图',
|
||||
detail: '为两个物品准备绘本风格图片资产。',
|
||||
weight: 68,
|
||||
},
|
||||
{
|
||||
id: 'baby-object-ready',
|
||||
label: '准备结果页',
|
||||
detail: '校验草稿字段并进入结果页。',
|
||||
weight: 10,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
|
||||
function clampProgress(value: number) {
|
||||
return Math.max(0, Math.min(100, Math.round(value)));
|
||||
}
|
||||
@@ -205,6 +234,9 @@ function getStepDefinitions(kind: MiniGameDraftGenerationKind) {
|
||||
if (kind === 'match3d') {
|
||||
return MATCH3D_STEPS;
|
||||
}
|
||||
if (kind === 'baby-object-match') {
|
||||
return BABY_OBJECT_MATCH_STEPS;
|
||||
}
|
||||
return BIG_FISH_STEPS;
|
||||
}
|
||||
|
||||
@@ -260,7 +292,9 @@ export function createMiniGameDraftGenerationState(
|
||||
? 'square-hole-draft'
|
||||
: kind === 'match3d'
|
||||
? 'match3d-work-title'
|
||||
: 'compile',
|
||||
: kind === 'baby-object-match'
|
||||
? 'baby-object-draft'
|
||||
: 'compile',
|
||||
startedAtMs: Date.now(),
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
@@ -313,6 +347,18 @@ function resolveMatch3DPhaseByElapsedMs(
|
||||
return currentOrder > elapsedOrder ? currentPhase : elapsedPhase;
|
||||
}
|
||||
|
||||
function resolveBabyObjectMatchPhaseByElapsedMs(
|
||||
elapsedMs: number,
|
||||
): MiniGameDraftGenerationPhase {
|
||||
if (elapsedMs >= 52_000) {
|
||||
return 'baby-object-ready';
|
||||
}
|
||||
if (elapsedMs >= 8_000) {
|
||||
return 'baby-object-images';
|
||||
}
|
||||
return 'baby-object-draft';
|
||||
}
|
||||
|
||||
function resolvePuzzleTimelineByElapsedMs(elapsedMs: number) {
|
||||
let elapsedBeforePhase = 0;
|
||||
|
||||
@@ -360,27 +406,34 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
phase: puzzleTimeline.phase,
|
||||
}
|
||||
: state.kind === 'big-fish' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveBigFishPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state.kind === 'square-hole' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveSquareHolePhaseByElapsedMs(elapsedMs),
|
||||
phase: resolveBigFishPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state.kind === 'match3d' &&
|
||||
: state.kind === 'square-hole' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveMatch3DPhaseByElapsedMs(elapsedMs, state.phase),
|
||||
phase: resolveSquareHolePhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state;
|
||||
: state.kind === 'match3d' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveMatch3DPhaseByElapsedMs(elapsedMs, state.phase),
|
||||
}
|
||||
: state.kind === 'baby-object-match' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveBabyObjectMatchPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state;
|
||||
|
||||
const steps = getStepDefinitions(normalizedState.kind);
|
||||
const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase);
|
||||
@@ -401,13 +454,15 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? 1
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? (puzzleTimeline?.activeStepProgressRatio ?? 0)
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? 0.55
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? 0.42
|
||||
: normalizedState.kind === 'match3d'
|
||||
? 0.5
|
||||
: 0;
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? 0.55
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? 0.42
|
||||
: normalizedState.kind === 'match3d'
|
||||
? 0.5
|
||||
: normalizedState.kind === 'baby-object-match'
|
||||
? 0.52
|
||||
: 0;
|
||||
const overallProgress =
|
||||
normalizedState.phase === 'failed'
|
||||
? Math.max(1, completedWeight)
|
||||
@@ -436,7 +491,9 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? '玩法草稿已准备完成,可进入结果页继续生成主图、动作和背景。'
|
||||
: normalizedState.kind === 'match3d'
|
||||
? '抓大鹅素材与草稿已准备完成,可进入结果页继续编辑。'
|
||||
: '首关草稿与正式图已准备完成,可进入结果页补作品信息。'
|
||||
: normalizedState.kind === 'baby-object-match'
|
||||
? '宝贝识物草稿已准备完成,可进入结果页继续发布。'
|
||||
: '首关草稿与正式图已准备完成,可进入结果页补作品信息。'
|
||||
: activeStep.detail),
|
||||
batchLabel: activeStep.label,
|
||||
overallProgress: clampProgress(cappedOverallProgress),
|
||||
@@ -448,13 +505,15 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? 0
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? Math.max(0, PUZZLE_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? Math.max(0, 7_000 - elapsedMs)
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? Math.max(0, 12_000 - elapsedMs)
|
||||
: normalizedState.kind === 'match3d'
|
||||
? Math.max(0, 10 * 60_000 - elapsedMs)
|
||||
: null,
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? Math.max(0, 7_000 - elapsedMs)
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? Math.max(0, 12_000 - elapsedMs)
|
||||
: normalizedState.kind === 'match3d'
|
||||
? Math.max(0, 10 * 60_000 - elapsedMs)
|
||||
: normalizedState.kind === 'baby-object-match'
|
||||
? Math.max(0, 60_000 - elapsedMs)
|
||||
: null,
|
||||
activeStepIndex,
|
||||
steps: buildMiniGameProgressSteps(
|
||||
steps,
|
||||
@@ -580,6 +639,22 @@ export function buildMatch3DGenerationAnchorEntries(
|
||||
.filter((entry) => entry.value.trim());
|
||||
}
|
||||
|
||||
export function buildBabyObjectMatchGenerationAnchorEntries(
|
||||
formPayload: CreateBabyObjectMatchDraftRequest | null | undefined,
|
||||
draft: BabyObjectMatchDraft | null | undefined = null,
|
||||
): CustomWorldStructuredAnchorEntry[] {
|
||||
const itemNames =
|
||||
formPayload?.itemAName?.trim() || formPayload?.itemBName?.trim()
|
||||
? [formPayload.itemAName.trim(), formPayload.itemBName.trim()]
|
||||
: (draft?.itemNames ?? []);
|
||||
|
||||
return itemNames.filter(Boolean).map((value, index) => ({
|
||||
id: `baby-object-item-${index + 1}`,
|
||||
label: `物品 ${index + 1}`,
|
||||
value,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSquareHoleGenerationAnchorEntries(
|
||||
session: SquareHoleSessionSnapshot | null | undefined,
|
||||
): CustomWorldStructuredAnchorEntry[] {
|
||||
|
||||
@@ -45,6 +45,14 @@ export function buildVisualNovelPublicWorkCode(profileId: string) {
|
||||
return `VN-${suffix}`;
|
||||
}
|
||||
|
||||
export function buildBabyObjectMatchPublicWorkCode(profileId: string) {
|
||||
const normalized = normalizePublicCodeText(profileId);
|
||||
const fallback = normalized || '00000000';
|
||||
const suffix = fallback.slice(-8).padStart(8, '0');
|
||||
|
||||
return `BO-${suffix}`;
|
||||
}
|
||||
|
||||
export function isSamePuzzlePublicWorkCode(keyword: string, profileId: string) {
|
||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||
|
||||
@@ -103,3 +111,16 @@ export function isSameVisualNovelPublicWorkCode(
|
||||
normalizedKeyword === normalizePublicCodeText(profileId)
|
||||
);
|
||||
}
|
||||
|
||||
export function isSameBabyObjectMatchPublicWorkCode(
|
||||
keyword: string,
|
||||
profileId: string,
|
||||
) {
|
||||
const normalizedKeyword = normalizePublicCodeText(keyword);
|
||||
|
||||
return (
|
||||
normalizedKeyword ===
|
||||
normalizePublicCodeText(buildBabyObjectMatchPublicWorkCode(profileId)) ||
|
||||
normalizedKeyword === normalizePublicCodeText(profileId)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user