fix: polish bark battle creation flow

This commit is contained in:
kdletters
2026-05-22 05:00:07 +08:00
parent 01da85a577
commit bf82f04b64
73 changed files with 9362 additions and 2663 deletions

View File

@@ -1,3 +1,4 @@
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
@@ -9,6 +10,7 @@ import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contra
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
import {
buildBabyObjectMatchPublicWorkCode,
buildBarkBattlePublicWorkCode,
buildBigFishPublicWorkCode,
buildMatch3DPublicWorkCode,
buildPuzzlePublicWorkCode,
@@ -19,6 +21,9 @@ import type { CustomWorldProfile } from '../../types';
const MATCH3D_CONTAINER_REFERENCE_COVER_SRC =
'/match3d-background-references/pot-fused-reference.png';
const BARK_BATTLE_REFERENCE_COVER_SRC =
'/creation-type-references/bark-battle.webp';
const DEFAULT_CREATION_WORK_AUTHOR = '玩家';
export type CreationWorkShelfKind =
| 'rpg'
@@ -27,6 +32,7 @@ export type CreationWorkShelfKind =
| 'square-hole'
| 'puzzle'
| 'baby-object-match'
| 'bark-battle'
| 'visual-novel';
export type CreationWorkShelfStatus = 'draft' | 'published';
@@ -84,6 +90,10 @@ export type CreationWorkShelfSource =
kind: 'visual-novel';
item: VisualNovelWorkSummary;
}
| {
kind: 'bark-battle';
item: BarkBattleWorkSummary;
}
| {
kind: 'baby-object-match';
item: BabyObjectMatchDraft;
@@ -103,6 +113,7 @@ export type CreationWorkShelfItem = {
hasUnreadUpdate?: boolean;
title: string;
summary: string;
authorDisplayName: string;
updatedAt: string;
coverImageSrc: string | null;
coverRenderMode: 'image' | 'scene_with_roles';
@@ -127,6 +138,7 @@ export function buildCreationWorkShelfItems(params: {
squareHoleItems?: SquareHoleWorkSummary[];
puzzleItems: PuzzleWorkSummary[];
babyObjectMatchItems?: BabyObjectMatchDraft[];
barkBattleItems?: BarkBattleWorkSummary[];
visualNovelItems?: VisualNovelWorkSummary[];
canDeleteRpg?: boolean;
canDeleteBigFish?: boolean;
@@ -134,6 +146,7 @@ export function buildCreationWorkShelfItems(params: {
canDeleteSquareHole?: boolean;
canDeletePuzzle?: boolean;
canDeleteBabyObjectMatch?: boolean;
canDeleteBarkBattle?: boolean;
canDeleteVisualNovel?: boolean;
onOpenRpgDraft?: (item: CustomWorldWorkSummary) => void;
onEnterRpgPublished?: (profileId: string) => void;
@@ -149,6 +162,8 @@ export function buildCreationWorkShelfItems(params: {
onClaimPuzzlePointIncentive?: (item: PuzzleWorkSummary) => void;
onOpenBabyObjectMatchDetail?: (item: BabyObjectMatchDraft) => void;
onDeleteBabyObjectMatch?: (item: BabyObjectMatchDraft) => void;
onOpenBarkBattleDetail?: (item: BarkBattleWorkSummary) => void;
onDeleteBarkBattle?: (item: BarkBattleWorkSummary) => void;
onOpenVisualNovelDetail?: (item: VisualNovelWorkSummary) => void;
onDeleteVisualNovel?: (item: VisualNovelWorkSummary) => void;
getItemState?: (
@@ -163,6 +178,7 @@ export function buildCreationWorkShelfItems(params: {
squareHoleItems = [],
puzzleItems,
babyObjectMatchItems = [],
barkBattleItems = [],
visualNovelItems = [],
canDeleteRpg = false,
canDeleteBigFish = false,
@@ -170,6 +186,7 @@ export function buildCreationWorkShelfItems(params: {
canDeleteSquareHole = false,
canDeletePuzzle = false,
canDeleteBabyObjectMatch = false,
canDeleteBarkBattle = false,
canDeleteVisualNovel = false,
onOpenRpgDraft,
onEnterRpgPublished,
@@ -185,6 +202,8 @@ export function buildCreationWorkShelfItems(params: {
onClaimPuzzlePointIncentive,
onOpenBabyObjectMatchDetail,
onDeleteBabyObjectMatch,
onOpenBarkBattleDetail,
onDeleteBarkBattle,
onOpenVisualNovelDetail,
onDeleteVisualNovel,
getItemState,
@@ -229,6 +248,12 @@ export function buildCreationWorkShelfItems(params: {
onDelete: onDeleteBabyObjectMatch,
}),
),
...mergeBarkBattleShelfSourceItems(barkBattleItems).map((item) =>
mapBarkBattleWorkToShelfItem(item, canDeleteBarkBattle, {
onOpen: onOpenBarkBattleDetail,
onDelete: onDeleteBarkBattle,
}),
),
...visualNovelItems.map((item) =>
mapVisualNovelWorkToShelfItem(item, canDeleteVisualNovel, {
onOpen: onOpenVisualNovelDetail,
@@ -259,6 +284,28 @@ export function buildCreationWorkShelfItems(params: {
);
}
function mergeBarkBattleShelfSourceItems(
items: readonly BarkBattleWorkSummary[],
): BarkBattleWorkSummary[] {
const byWorkId = new Map<string, BarkBattleWorkSummary>();
for (const item of items) {
const current = byWorkId.get(item.workId);
if (!current) {
byWorkId.set(item.workId, item);
continue;
}
if (current.status !== 'published' && item.status === 'published') {
byWorkId.set(item.workId, { ...current, ...item });
continue;
}
if (current.status === item.status) {
byWorkId.set(item.workId, { ...current, ...item });
}
}
return Array.from(byWorkId.values());
}
type RpgWorkShelfAdapter = {
onOpenDraft?: (item: CustomWorldWorkSummary) => void;
onEnterPublished?: (profileId: string) => void;
@@ -303,6 +350,7 @@ function mapRpgWorkToShelfItem(
status: item.status,
title: item.title,
summary: item.summary,
authorDisplayName: resolveAuthorDisplayName(item, libraryEntry),
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: item.coverRenderMode ?? 'image',
@@ -342,6 +390,7 @@ function mapBigFishWorkToShelfItem(
status: item.status,
title: item.title,
summary: item.summary,
authorDisplayName: resolveAuthorDisplayName(item),
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: 'image',
@@ -386,6 +435,7 @@ function mapMatch3DWorkToShelfItem(
status,
title: item.gameName,
summary: item.summary,
authorDisplayName: resolveAuthorDisplayName(item),
updatedAt: item.updatedAt,
coverImageSrc,
coverRenderMode: 'image',
@@ -434,6 +484,7 @@ function mapPuzzleWorkToShelfItem(
item.workDescription?.trim() ||
item.summary.trim() ||
(status === 'draft' ? '未填写作品描述' : ''),
authorDisplayName: resolveAuthorDisplayName(item),
updatedAt: item.updatedAt,
coverImageSrc,
coverRenderMode: 'image',
@@ -500,6 +551,7 @@ function mapBabyObjectMatchDraftToShelfItem(
summary:
item.workDescription.trim() ||
`${item.itemNames[0]}${item.itemNames[1]}识物分类`,
authorDisplayName: resolveAuthorDisplayName(item),
updatedAt: item.updatedAt,
coverImageSrc,
coverRenderMode: 'image',
@@ -549,6 +601,7 @@ function mapVisualNovelWorkToShelfItem(
status,
title,
summary,
authorDisplayName: resolveAuthorDisplayName(item),
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: 'image',
@@ -578,6 +631,72 @@ function mapVisualNovelWorkToShelfItem(
};
}
function mapBarkBattleWorkToShelfItem(
item: BarkBattleWorkSummary,
canDelete: boolean,
adapter: WorkShelfAdapter<BarkBattleWorkSummary>,
): CreationWorkShelfItem {
const status = item.status;
const publicWorkCode =
status === 'published' ? buildBarkBattlePublicWorkCode(item.workId) : null;
const playerCharacterImageSrc = normalizeCoverImageSrc(
item.playerCharacterImageSrc,
);
const opponentCharacterImageSrc = normalizeCoverImageSrc(
item.opponentCharacterImageSrc,
);
const coverImageSrc =
normalizeCoverImageSrc(item.uiBackgroundImageSrc) ??
playerCharacterImageSrc ??
opponentCharacterImageSrc ??
BARK_BATTLE_REFERENCE_COVER_SRC;
const coverCharacterImageSrcs = [
playerCharacterImageSrc,
opponentCharacterImageSrc,
].filter((imageSrc): imageSrc is string => Boolean(imageSrc));
const canRenderSceneWithRoles =
Boolean(normalizeCoverImageSrc(item.uiBackgroundImageSrc)) &&
coverCharacterImageSrcs.length >= 2;
return {
id: item.workId,
kind: 'bark-battle',
status,
title: item.title.trim() || '汪汪声浪大作战',
summary:
item.summary.trim() ||
item.themeDescription.trim() ||
(status === 'draft' ? '未填写作品描述' : ''),
authorDisplayName: resolveAuthorDisplayName(item),
updatedAt: item.updatedAt,
coverImageSrc,
coverRenderMode: canRenderSceneWithRoles ? 'scene_with_roles' : 'image',
coverCharacterImageSrcs,
publicWorkCode,
sharePath:
publicWorkCode && status === 'published'
? buildPublicWorkStagePath('work-detail', publicWorkCode)
: null,
openActionLabel: status === 'published' ? '查看详情' : '继续创作',
canDelete,
canShare: status === 'published' && Boolean(publicWorkCode),
badges: [
buildStatusBadge(status),
{ id: 'type', label: '汪汪', tone: 'neutral' },
],
metrics:
status === 'published'
? buildPublishedMetrics({
playCount: item.playCount,
remixCount: 0,
likeCount: 0,
})
: [],
actions: buildWorkShelfActions(item, adapter),
source: { kind: 'bark-battle', item },
};
}
function mapSquareHoleWorkToShelfItem(
item: SquareHoleWorkSummary,
canDelete: boolean,
@@ -596,6 +715,7 @@ function mapSquareHoleWorkToShelfItem(
status,
title: item.gameName,
summary: item.summary,
authorDisplayName: resolveAuthorDisplayName(item),
updatedAt: item.updatedAt,
coverImageSrc,
coverRenderMode: 'image',
@@ -625,6 +745,26 @@ function mapSquareHoleWorkToShelfItem(
};
}
function resolveAuthorDisplayName(
...sources: Array<unknown>
) {
for (const source of sources) {
const authorDisplayName =
source &&
typeof source === 'object' &&
'authorDisplayName' in source &&
typeof source.authorDisplayName === 'string'
? source.authorDisplayName.trim()
: '';
if (authorDisplayName) {
return authorDisplayName;
}
}
return DEFAULT_CREATION_WORK_AUTHOR;
}
function normalizeCoverImageSrc(value?: string | null) {
return value?.trim() || null;
}
@@ -816,11 +956,34 @@ function isPersistedCreationWorkGenerating(item: CreationWorkShelfItem) {
return item.source.item.generationStatus === 'generating';
case 'puzzle':
return isPersistedPuzzleDraftGenerating(item.source.item);
case 'bark-battle':
return isPersistedBarkBattleDraftGenerating(item.source.item);
default:
return false;
}
}
export function isPersistedBarkBattleDraftGenerating(
item: BarkBattleWorkSummary,
) {
if (item.status === 'published') {
return false;
}
return (
item.generationStatus === 'pending_assets' ||
!hasBarkBattleRequiredImages(item)
);
}
export function hasBarkBattleRequiredImages(item: BarkBattleWorkSummary) {
return Boolean(
normalizeCoverImageSrc(item.playerCharacterImageSrc) &&
normalizeCoverImageSrc(item.opponentCharacterImageSrc) &&
normalizeCoverImageSrc(item.uiBackgroundImageSrc),
);
}
export function isPersistedPuzzleDraftGenerating(item: PuzzleWorkSummary) {
if (item.generationStatus !== 'generating') {
return false;