Merge branch 'codex/profile-mobile-ui-reference'

This commit is contained in:
2026-05-25 01:41:05 +08:00
74 changed files with 5512 additions and 1090 deletions

View File

@@ -9,6 +9,9 @@ export type CreationEntryTypeConfig = {
visible: boolean;
open: boolean;
sortOrder: number;
categoryId: string;
categoryLabel: string;
categorySortOrder: number;
updatedAtMicros: number;
};
@@ -23,6 +26,14 @@ export type CreationEntryConfig = {
title: string;
description: string;
};
eventBanner: {
title: string;
description: string;
coverImageSrc: string;
prizePoolMudPoints: number;
startsAtText: string;
endsAtText: string;
};
creationTypes: CreationEntryTypeConfig[];
};

View File

@@ -36,12 +36,15 @@ describe('miniGameDraftGenerationProgress', () => {
expect(progress?.steps[0]?.detail).toBe(
'建立可恢复草稿,整理首关描述与关卡结构,约 8 秒。',
);
expect(progress?.estimatedRemainingMs).toBe(296_500);
expect(progress?.overallProgress).toBeGreaterThan(0);
expect(progress?.steps[2]?.detail).toBe(
'调用 gpt-image-2 生成 1:1 拼图首图,预计 4 分钟。',
);
expect(progress?.estimatedRemainingMs).toBe(446_500);
expect(progress?.overallProgress).toBe(0);
expect(progress?.steps[0]?.completed).toBeGreaterThan(0);
});
test('puzzle draft generation advances steps across the current asset pipeline', () => {
test('puzzle draft generation starts total progress from zero', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
@@ -51,42 +54,81 @@ describe('miniGameDraftGenerationProgress', () => {
error: null,
};
const imageProgress = buildMiniGameDraftGenerationProgress(state, 26_000);
const uiProgress = buildMiniGameDraftGenerationProgress(state, 206_000);
const writeBackProgress = buildMiniGameDraftGenerationProgress(
state,
296_000,
);
const progress = buildMiniGameDraftGenerationProgress(state, 1000);
expect(imageProgress?.phaseId).toBe('puzzle-cover-image');
expect(imageProgress?.estimatedRemainingMs).toBe(273_000);
expect(imageProgress?.steps[1]?.status).toBe('completed');
expect(imageProgress?.steps[2]?.status).toBe('active');
expect(imageProgress?.steps[2]?.completed).toBeGreaterThan(0);
expect(uiProgress?.phaseId).toBe('puzzle-ui-assets');
expect(writeBackProgress?.phaseId).toBe('puzzle-select-image');
expect(writeBackProgress?.estimatedRemainingMs).toBe(3_000);
expect(writeBackProgress?.steps[4]?.status).toBe('completed');
expect(writeBackProgress?.steps[5]?.status).toBe('active');
expect(progress?.overallProgress).toBe(0);
expect(progress?.completedWeight).toBe(0);
expect(progress?.estimatedRemainingMs).toBe(448_000);
expect(progress?.steps[0]?.completed).toBe(0);
});
test('puzzle write-back step turns completed once rounded progress reaches 100%', () => {
test('puzzle draft generation total progress advances after startup', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
startedAtMs: 1000,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
};
const progress = buildMiniGameDraftGenerationProgress(state, 7000);
expect(progress?.overallProgress).toBeGreaterThan(0);
expect(progress?.overallProgress).toBeLessThan(88);
expect(progress?.phaseId).toBe('compile');
});
test('puzzle draft generation keeps current step until real progress advances it', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
startedAtMs: 1000,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
};
const longRunningProgress = buildMiniGameDraftGenerationProgress(
state,
296_000,
);
const progressedState: MiniGameDraftGenerationState = {
...state,
phase: 'puzzle-cover-image',
};
const realProgress = buildMiniGameDraftGenerationProgress(
progressedState,
296_000,
);
expect(longRunningProgress?.phaseId).toBe('compile');
expect(longRunningProgress?.steps[0]?.status).toBe('active');
expect(longRunningProgress?.steps[1]?.status).toBe('pending');
expect(longRunningProgress?.overallProgress).toBeLessThanOrEqual(98);
expect(longRunningProgress?.overallProgress).toBeGreaterThan(40);
expect(realProgress?.phaseId).toBe('puzzle-cover-image');
expect(realProgress?.steps[1]?.status).toBe('completed');
expect(realProgress?.steps[2]?.status).toBe('active');
});
test('puzzle write-back step stays active until the generation action finishes', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'puzzle-select-image',
startedAtMs: 1_000,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
};
const progress = buildMiniGameDraftGenerationProgress(state, 298_950);
const progress = buildMiniGameDraftGenerationProgress(state, 448_950);
expect(progress?.phaseId).toBe('puzzle-select-image');
expect(progress?.overallProgress).toBe(98);
expect(progress?.estimatedRemainingMs).toBe(50);
expect(progress?.steps[5]?.completed).toBe(1);
expect(progress?.steps[5]?.status).toBe('completed');
expect(progress?.steps[5]?.completed).toBe(0.98);
expect(progress?.steps[5]?.status).toBe('active');
});
test('puzzle direct upload generation skips the first image generation step', () => {
@@ -112,14 +154,14 @@ describe('miniGameDraftGenerationProgress', () => {
'生成UI与背景',
'写入正式草稿',
]);
expect(progress?.phaseId).toBe('puzzle-level-scene');
expect(progress?.phaseId).toBe('compile');
expect(progress?.steps[2]?.detail).toContain('直接使用上传图作为参考');
expect(progress?.estimatedRemainingMs).toBe(189_000);
expect(writeBackProgress?.phaseId).toBe('puzzle-select-image');
expect(writeBackProgress?.phaseId).toBe('compile');
expect(writeBackProgress?.estimatedRemainingMs).toBe(3_000);
});
test('puzzle draft generation keeps moving without claiming completion before response', () => {
test('puzzle draft generation does not advance or claim completion before response', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
@@ -129,18 +171,88 @@ describe('miniGameDraftGenerationProgress', () => {
error: null,
};
const progress = buildMiniGameDraftGenerationProgress(state, 480_000);
const progress = buildMiniGameDraftGenerationProgress(state, 630_000);
expect(progress?.phaseId).toBe('puzzle-select-image');
expect(progress?.overallProgress).toBe(98);
expect(progress?.phaseId).toBe('compile');
expect(progress?.overallProgress).toBeLessThan(88);
expect(progress?.overallProgress).toBeGreaterThan(80);
expect(progress?.estimatedRemainingMs).toBe(0);
expect(progress?.steps[5]?.completed).toBe(1);
expect(progress?.steps[5]?.status).toBe('completed');
expect(progress?.steps.every((step) => step.status === 'completed')).toBe(
expect(progress?.steps[0]?.status).toBe('active');
expect(progress?.steps[0]?.completed).toBe(0.98);
expect(progress?.steps.slice(1).every((step) => step.status === 'pending')).toBe(
true,
);
});
test('puzzle draft generation advances steps from backend progress percent only', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
startedAtMs: 1_000,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
metadata: {
puzzleAiRedraw: true,
puzzleProgressPercent: 88,
} as MiniGameDraftGenerationState['metadata'],
};
const imageProgress = buildMiniGameDraftGenerationProgress(state, 120_000);
const uiProgress = buildMiniGameDraftGenerationProgress(
{
...state,
metadata: {
puzzleAiRedraw: true,
puzzleProgressPercent: 94,
} as MiniGameDraftGenerationState['metadata'],
},
230_000,
);
const writeProgress = buildMiniGameDraftGenerationProgress(
{
...state,
metadata: {
puzzleAiRedraw: true,
puzzleProgressPercent: 96,
} as MiniGameDraftGenerationState['metadata'],
},
260_000,
);
expect(imageProgress?.phaseId).toBe('puzzle-cover-image');
expect(imageProgress?.steps[2]?.status).toBe('active');
expect(imageProgress?.steps[3]?.status).toBe('pending');
expect(uiProgress?.phaseId).toBe('puzzle-ui-assets');
expect(uiProgress?.steps[4]?.status).toBe('active');
expect(uiProgress?.steps[5]?.status).toBe('pending');
expect(writeProgress?.phaseId).toBe('puzzle-select-image');
expect(writeProgress?.steps[5]?.status).toBe('active');
});
test('puzzle backend milestone starts fake progress from the current step entry time', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
phase: 'compile',
startedAtMs: 1_000,
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
metadata: {
puzzleAiRedraw: true,
puzzleProgressPercent: 88,
puzzleActivePhaseId: 'puzzle-cover-image',
puzzleActiveStepStartedAtMs: 120_000,
} as MiniGameDraftGenerationState['metadata'],
};
const progress = buildMiniGameDraftGenerationProgress(state, 121_000);
expect(progress?.phaseId).toBe('puzzle-cover-image');
expect(progress?.steps[2]?.status).toBe('active');
expect(progress?.steps[2]?.completed).toBeLessThan(0.02);
});
test('puzzle ready copy points to result page work info completion', () => {
const state: MiniGameDraftGenerationState = {
kind: 'puzzle',
@@ -268,6 +380,19 @@ describe('miniGameDraftGenerationProgress', () => {
);
});
test('match3d draft generation starts total progress from zero', () => {
const state = createMiniGameDraftGenerationState('match3d');
const progress = buildMiniGameDraftGenerationProgress(
state,
state.startedAtMs,
);
expect(progress?.overallProgress).toBe(0);
expect(progress?.completedWeight).toBe(0);
expect(progress?.steps[0]?.completed).toBe(0);
});
test('match3d draft generation keeps backend observed asset phase', () => {
const state = {
...createMiniGameDraftGenerationState('match3d'),

View File

@@ -91,6 +91,9 @@ export type MiniGameDraftGenerationState = {
error: string | null;
metadata?: {
puzzleAiRedraw?: boolean;
puzzleActivePhaseId?: MiniGameDraftGenerationPhase;
puzzleActiveStepStartedAtMs?: number;
puzzleProgressPercent?: number;
};
};
@@ -111,10 +114,14 @@ type MiniGameAnchorSource = {
value: string;
};
const PUZZLE_COVER_IMAGE_GENERATION_EXPECTED_MS = 240_000;
const PUZZLE_IMAGE_GENERATION_EXPECTED_MS = 90_000;
const PUZZLE_COMPILE_EXPECTED_MS = 8_000;
const PUZZLE_LEVEL_NAME_EXPECTED_MS = 10_000;
const PUZZLE_WRITE_DRAFT_EXPECTED_MS = 10_000;
const PUZZLE_COMPILE_MILESTONE_PROGRESS = 88;
const PUZZLE_IMAGE_MILESTONE_PROGRESS = 94;
const PUZZLE_UI_MILESTONE_PROGRESS = 96;
function shouldSkipPuzzleCoverGeneration(state: MiniGameDraftGenerationState) {
return state.metadata?.puzzleAiRedraw === false;
@@ -160,8 +167,8 @@ function buildPuzzleTimedSteps(state: MiniGameDraftGenerationState) {
steps.push({
id: 'puzzle-cover-image',
label: '生成拼图首图',
detail: '调用 gpt-image-2 生成 1:1 拼图首图,预计 90 秒。',
durationMs: PUZZLE_IMAGE_GENERATION_EXPECTED_MS,
detail: '调用 gpt-image-2 生成 1:1 拼图首图,预计 4 分钟。',
durationMs: PUZZLE_COVER_IMAGE_GENERATION_EXPECTED_MS,
});
}
@@ -204,30 +211,40 @@ function resolvePuzzleEstimatedWaitMs(state: MiniGameDraftGenerationState) {
const PUZZLE_NON_READY_MAX_PROGRESS = 98;
const BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS = 6 * 60_000;
function buildPuzzlePhaseTimeline(state: MiniGameDraftGenerationState): Array<{
phase: Extract<
MiniGameDraftGenerationPhase,
| 'compile'
| 'puzzle-level-name'
| 'puzzle-cover-image'
| 'puzzle-level-scene'
| 'puzzle-ui-assets'
| 'puzzle-select-image'
>;
durationMs: number;
}> {
return buildPuzzleTimedSteps(state).map((step) => ({
phase: step.id as Extract<
MiniGameDraftGenerationPhase,
| 'compile'
| 'puzzle-level-name'
| 'puzzle-cover-image'
| 'puzzle-level-scene'
| 'puzzle-ui-assets'
| 'puzzle-select-image'
>,
durationMs: step.durationMs,
}));
function resolvePuzzleBackendProgressPercent(
state: MiniGameDraftGenerationState,
) {
const progressPercent = state.metadata?.puzzleProgressPercent;
if (typeof progressPercent !== 'number' || !Number.isFinite(progressPercent)) {
return null;
}
return Math.max(0, Math.min(100, Math.round(progressPercent)));
}
function resolvePuzzlePhaseByBackendProgress(
state: MiniGameDraftGenerationState,
): MiniGameDraftGenerationPhase | null {
const progressPercent = resolvePuzzleBackendProgressPercent(state);
if (progressPercent == null) {
return null;
}
// 中文注释:拼图生成页的跨步骤只跟随后端会话真实里程碑;
// 每步内部的等待反馈仍由本地假进度补足。
if (progressPercent >= 96) {
return 'puzzle-select-image';
}
if (progressPercent >= 94) {
return 'puzzle-ui-assets';
}
if (progressPercent >= 88) {
return shouldSkipPuzzleCoverGeneration(state)
? 'puzzle-level-scene'
: 'puzzle-cover-image';
}
return null;
}
const BIG_FISH_STEPS = [
@@ -685,32 +702,141 @@ function resolveWoodenFishTimelineByElapsedMs(elapsedMs: number) {
};
}
function resolvePuzzleTimelineByElapsedMs(
function resolvePuzzleActiveStepProgressRatio(
steps: ReadonlyArray<TimedMiniGameStepDefinition>,
activeStepIndex: number,
elapsedMs: number,
state: MiniGameDraftGenerationState,
) {
let elapsedBeforePhase = 0;
for (const item of buildPuzzlePhaseTimeline(state)) {
const elapsedInPhase = elapsedMs - elapsedBeforePhase;
if (elapsedInPhase < item.durationMs) {
return {
phase: item.phase,
activeStepProgressRatio: Math.max(
0,
Math.min(1, elapsedInPhase / item.durationMs),
),
};
}
elapsedBeforePhase += item.durationMs;
const activeStep = steps[activeStepIndex];
if (!activeStep) {
return 0;
}
return {
phase: 'puzzle-select-image' as const,
activeStepProgressRatio: 1,
};
const elapsedBeforeActiveStep = steps
.slice(0, activeStepIndex)
.reduce((sum, step) => sum + step.durationMs, 0);
const elapsedInActiveStep = Math.max(0, elapsedMs - elapsedBeforeActiveStep);
return Math.max(
0,
Math.min(0.98, elapsedInActiveStep / Math.max(1, activeStep.durationMs)),
);
}
function resolvePuzzleActiveStepElapsedProgressRatio(
state: MiniGameDraftGenerationState,
steps: ReadonlyArray<TimedMiniGameStepDefinition>,
activeStepIndex: number,
elapsedMs: number,
effectiveNowMs: number,
) {
if (resolvePuzzleBackendProgressPercent(state) != null) {
const stepStartedAtMs = state.metadata?.puzzleActiveStepStartedAtMs;
if (
state.metadata?.puzzleActivePhaseId === state.phase &&
typeof stepStartedAtMs === 'number' &&
Number.isFinite(stepStartedAtMs)
) {
const activeStep = steps[activeStepIndex];
if (!activeStep) {
return 0;
}
return Math.max(
0,
Math.min(
0.98,
(effectiveNowMs - stepStartedAtMs) /
Math.max(1, activeStep.durationMs),
),
);
}
return resolvePuzzleActiveStepProgressRatio(
steps,
activeStepIndex,
elapsedMs,
);
}
const activeStep = steps[activeStepIndex];
if (!activeStep) {
return 0;
}
// 中文注释:未收到后端真实里程碑时,跨步骤必须卡住;
// 但当前步骤内的假进度要按整段等待时间继续向前走,避免短步骤几秒后停死。
const fallbackDurationMs = Math.max(1, resolvePuzzleEstimatedWaitMs(state));
return Math.max(
0,
Math.min(0.98, elapsedMs / fallbackDurationMs),
);
}
function resolveElapsedActiveStepProgressRatio(
kind: MiniGameDraftGenerationKind,
elapsedMs: number,
) {
const estimatedWaitMs =
kind === 'big-fish'
? 7_000
: kind === 'square-hole'
? 12_000
: kind === 'match3d'
? MATCH3D_ESTIMATED_WAIT_MS
: kind === 'baby-object-match'
? BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS
: kind === 'jump-hop'
? JUMP_HOP_ESTIMATED_WAIT_MS
: kind === 'wooden-fish'
? WOODEN_FISH_ESTIMATED_WAIT_MS
: 1;
return Math.max(
0,
Math.min(0.98, elapsedMs / Math.max(1, estimatedWaitMs)),
);
}
function resolvePuzzleOverallProgress(
state: MiniGameDraftGenerationState,
activeStepProgressRatio: number,
) {
const backendProgressPercent = resolvePuzzleBackendProgressPercent(state);
// 中文注释88 以下的后端进度只保留为会话事实,不参与首帧总进度抬升。
// 生成页恢复时必须先从 0% 起步,再由当前步骤内的假进度平滑推进。
const backendProgressFloor =
backendProgressPercent != null &&
backendProgressPercent >= PUZZLE_COMPILE_MILESTONE_PROGRESS
? backendProgressPercent
: 0;
const range =
state.phase === 'puzzle-select-image'
? {
start: PUZZLE_UI_MILESTONE_PROGRESS,
end: PUZZLE_NON_READY_MAX_PROGRESS,
}
: state.phase === 'puzzle-ui-assets'
? {
start: PUZZLE_IMAGE_MILESTONE_PROGRESS,
end: PUZZLE_UI_MILESTONE_PROGRESS,
}
: state.phase === 'puzzle-cover-image' ||
state.phase === 'puzzle-level-scene'
? {
start: PUZZLE_COMPILE_MILESTONE_PROGRESS,
end: PUZZLE_IMAGE_MILESTONE_PROGRESS,
}
: {
start: 0,
end: PUZZLE_COMPILE_MILESTONE_PROGRESS,
};
const fakeProgress =
range.start + (range.end - range.start) * activeStepProgressRatio;
const nextProgress = Math.min(
PUZZLE_NON_READY_MAX_PROGRESS,
Math.max(range.start, backendProgressFloor, fakeProgress),
);
return nextProgress;
}
export function buildMiniGameDraftGenerationProgress(
@@ -726,11 +852,11 @@ export function buildMiniGameDraftGenerationProgress(
? state.finishedAtMs
: nowMs;
const elapsedMs = Math.max(0, effectiveNowMs - state.startedAtMs);
const puzzleTimeline =
const puzzleBackendPhase =
state.kind === 'puzzle' &&
state.phase !== 'failed' &&
state.phase !== 'ready'
? resolvePuzzleTimelineByElapsedMs(elapsedMs, state)
? resolvePuzzlePhaseByBackendProgress(state)
: null;
const woodenFishTimeline =
state.kind === 'wooden-fish' &&
@@ -739,10 +865,10 @@ export function buildMiniGameDraftGenerationProgress(
? resolveWoodenFishTimelineByElapsedMs(elapsedMs)
: null;
const normalizedState =
puzzleTimeline != null
puzzleBackendPhase != null
? {
...state,
phase: puzzleTimeline.phase,
phase: puzzleBackendPhase,
}
: woodenFishTimeline != null
? {
@@ -786,47 +912,83 @@ export function buildMiniGameDraftGenerationProgress(
}
: state;
const puzzleTimedSteps =
normalizedState.kind === 'puzzle'
? buildPuzzleTimedSteps(normalizedState)
: null;
const steps =
normalizedState.kind === 'puzzle'
? buildPuzzleSteps(normalizedState)
? buildWeightedPuzzleSteps(
puzzleTimedSteps ?? buildPuzzleTimedSteps(normalizedState),
)
: getStepDefinitions(normalizedState.kind);
const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase);
const activeStep = steps[activeStepIndex] ?? steps[0];
const activeStepProgressRatio =
normalizedState.kind === 'puzzle'
? normalizedState.phase === 'ready'
? 1
: normalizedState.phase === 'failed'
? 0
: resolvePuzzleActiveStepElapsedProgressRatio(
normalizedState,
puzzleTimedSteps ?? buildPuzzleTimedSteps(normalizedState),
activeStepIndex,
elapsedMs,
effectiveNowMs,
)
: normalizedState.totalAssetCount > 0
? Math.min(
1,
normalizedState.completedAssetCount / normalizedState.totalAssetCount,
)
: normalizedState.phase === 'ready'
? 1
: normalizedState.kind === 'big-fish'
? resolveElapsedActiveStepProgressRatio(
normalizedState.kind,
elapsedMs,
)
: normalizedState.kind === 'square-hole'
? resolveElapsedActiveStepProgressRatio(
normalizedState.kind,
elapsedMs,
)
: normalizedState.kind === 'match3d'
? resolveElapsedActiveStepProgressRatio(
normalizedState.kind,
elapsedMs,
)
: normalizedState.kind === 'baby-object-match'
? resolveElapsedActiveStepProgressRatio(
normalizedState.kind,
elapsedMs,
)
: normalizedState.kind === 'jump-hop'
? resolveElapsedActiveStepProgressRatio(
normalizedState.kind,
elapsedMs,
)
: normalizedState.kind === 'wooden-fish'
? (woodenFishTimeline?.activeStepProgressRatio ?? 0)
: 0;
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 === 'puzzle'
? (puzzleTimeline?.activeStepProgressRatio ?? 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
: normalizedState.kind === 'jump-hop'
? 0.5
: normalizedState.kind === 'wooden-fish'
? (woodenFishTimeline?.activeStepProgressRatio ?? 0)
: 0;
const overallProgress =
normalizedState.phase === 'failed'
? Math.max(1, completedWeight)
: normalizedState.phase === 'ready'
? 100
: completedWeight + activeStep.weight * assetRatio;
: normalizedState.kind === 'puzzle'
? resolvePuzzleOverallProgress(
normalizedState,
activeStepProgressRatio,
)
: completedWeight + activeStep.weight * activeStepProgressRatio;
const cappedOverallProgress =
normalizedState.phase === 'ready' || normalizedState.phase === 'failed'
? overallProgress
@@ -890,7 +1052,7 @@ export function buildMiniGameDraftGenerationProgress(
steps,
activeStepIndex,
normalizedState,
assetRatio,
activeStepProgressRatio,
),
};
}

View File

@@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest';
import {
buildCustomWorldPublicWorkCode,
buildJumpHopPublicWorkCode,
buildWoodenFishPublicWorkCode,
isSameCustomWorldPublicWorkCode,
isSameJumpHopPublicWorkCode,
isSameWoodenFishPublicWorkCode,
} from './publicWorkCode';
@@ -32,6 +34,16 @@ describe('publicWorkCode', () => {
);
});
it('builds and matches custom world public work codes from profile ids', () => {
expect(buildCustomWorldPublicWorkCode('world-public-1')).toBe('CW-00000001');
expect(isSameCustomWorldPublicWorkCode('cw-00000001', 'world-public-1')).toBe(
true,
);
expect(
isSameCustomWorldPublicWorkCode('world-public-1', 'world-public-1'),
).toBe(true);
});
it('matches wooden fish public work codes and raw profile ids', () => {
expect(
isSameWoodenFishPublicWorkCode(

View File

@@ -53,6 +53,28 @@ export function buildBabyObjectMatchPublicWorkCode(profileId: string) {
return `BO-${suffix}`;
}
function normalizeCustomWorldPublicWorkCodeSuffix(profileId: string) {
const digits = profileId
.split('')
.filter((character) => character >= '0' && character <= '9')
.join('');
if (digits.length === 0) {
const bytes = new TextEncoder().encode(profileId);
const checksum = bytes.reduce((accumulator, value) => {
return (accumulator * 131 + value) >>> 0;
}, 0);
return String(checksum % 100_000_000).padStart(8, '0');
}
return digits.slice(-8).padStart(8, '0');
}
export function buildCustomWorldPublicWorkCode(profileId: string) {
return `CW-${normalizeCustomWorldPublicWorkCodeSuffix(profileId)}`;
}
function normalizeBarkBattlePublicWorkCodeSuffix(workId: string) {
const normalized = normalizePublicCodeText(workId);
const withoutPrefix = normalized.startsWith('BB')
@@ -155,6 +177,19 @@ export function isSameBabyObjectMatchPublicWorkCode(
);
}
export function isSameCustomWorldPublicWorkCode(
keyword: string,
profileId: string,
) {
const normalizedKeyword = normalizePublicCodeText(keyword);
return (
normalizedKeyword ===
normalizePublicCodeText(buildCustomWorldPublicWorkCode(profileId)) ||
normalizedKeyword === normalizePublicCodeText(profileId)
);
}
export function isSameBarkBattlePublicWorkCode(keyword: string, workId: string) {
const normalizedKeyword = normalizePublicCodeText(keyword);