refactor: 收口公开作品详情映射
This commit is contained in:
@@ -20,8 +20,9 @@
|
|||||||
|
|
||||||
- 背景:平台壳层直接判断公开作品详情入口的玩法类型、是否需要补读完整详情,以及自有作品按钮显示“编辑”还是“改造”,导致统一作品详情的纯决策散落在巨型 Implementation 内。
|
- 背景:平台壳层直接判断公开作品详情入口的玩法类型、是否需要补读完整详情,以及自有作品按钮显示“编辑”还是“改造”,导致统一作品详情的纯决策散落在巨型 Implementation 内。
|
||||||
- 决策:新增 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`,以 `getPlatformPublicWorkDetailKind`、`resolvePlatformPublicWorkDetailOpenStrategy`、`resolvePlatformPublicWorkActionMode`、`resolvePlatformPublicWorkDetailOpenDecision` 和 `resolveActivePlatformPublicWorkAuthorEntry` 收口公开作品详情 Strategy。`PlatformEntryFlowShellImpl.tsx` 只按 Strategy 调用现有详情读取 / 直接展示 Adapter,并保留作者请求竞态控制;启动、点赞、remix 和编辑副作用暂不抽走。
|
- 决策:新增 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`,以 `getPlatformPublicWorkDetailKind`、`resolvePlatformPublicWorkDetailOpenStrategy`、`resolvePlatformPublicWorkActionMode`、`resolvePlatformPublicWorkDetailOpenDecision` 和 `resolveActivePlatformPublicWorkAuthorEntry` 收口公开作品详情 Strategy。`PlatformEntryFlowShellImpl.tsx` 只按 Strategy 调用现有详情读取 / 直接展示 Adapter,并保留作者请求竞态控制;启动、点赞、remix 和编辑副作用暂不抽走。
|
||||||
|
- 追加决策:公开详情 entry 映射与公开详情反推玩法 work 摘要也归入 `platformPublicWorkDetailFlow.ts`,包括 RPG、拼图、大鱼吃小鱼、方洞挑战、视觉小说、跳一跳、敲木鱼和汪汪声浪的通用映射。抓大鹅 `mapMatch3DWorkToPublicWorkDetail` 归入 `platformMatch3DRuntimeProfile.ts`,继续委托 `normalizeMatch3DWorkForRuntimeUi` 做素材归一和背景资产提升,避免把 Match3D 运行态规则复制到公开详情 Flow Module。
|
||||||
- 影响范围:统一作品详情入口、公开详情打开策略、自有公开作品编辑 / 改造动作模式,以及后续新增玩法公开详情接入。
|
- 影响范围:统一作品详情入口、公开详情打开策略、自有公开作品编辑 / 改造动作模式,以及后续新增玩法公开详情接入。
|
||||||
- 验证方式:`npm run test -- src/components/platform-entry/platformPublicWorkDetailFlow.test.ts`、公开详情壳层交互回归、`npm run typecheck`、`npm run check:encoding`。
|
- 验证方式:`npm run test -- src/components/platform-entry/platformPublicWorkDetailFlow.test.ts`、`npm run test -- src/components/platform-entry/platformMatch3DRuntimeProfile.test.ts`、公开详情壳层交互回归、`npm run typecheck`、`npm run check:encoding`。
|
||||||
- 关联文档:`docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md`。
|
- 关联文档:`docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md`。
|
||||||
|
|
||||||
## 2026-06-03 平台入口弹窗状态规则收口
|
## 2026-06-03 平台入口弹窗状态规则收口
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
|||||||
|
|
||||||
平台入口公开作品身份、跨玩法去重、推荐运行态 kind 判定和最新排序收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%B9%B3%E5%8F%B0%E5%85%A5%E5%8F%A3PublicGalleryFlowModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
平台入口公开作品身份、跨玩法去重、推荐运行态 kind 判定和最新排序收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%B9%B3%E5%8F%B0%E5%85%A5%E5%8F%A3PublicGalleryFlowModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
统一作品详情页的玩法 kind、详情打开策略和自有作品动作模式收口到 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`,规则见 [【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md](./technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md)。
|
统一作品详情页的玩法 kind、详情打开策略、自有作品动作模式和公开详情映射收口到 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`;抓大鹅公开详情映射的素材归一仍归 `platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md](./technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md)。
|
||||||
|
|
||||||
创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,生产 Hub 只接收 `CreationWorkShelfItem[]` 与 UI 状态,不再接收各玩法 raw items 和回调列阵,规则见 [【前端架构】WorkShelfModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91WorkShelfModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,生产 Hub 只接收 `CreationWorkShelfItem[]` 与 UI 状态,不再接收各玩法 raw items 和回调列阵,规则见 [【前端架构】WorkShelfModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91WorkShelfModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,14 @@
|
|||||||
- `resolvePlatformPublicWorkActionMode(entry, viewerUserId)`
|
- `resolvePlatformPublicWorkActionMode(entry, viewerUserId)`
|
||||||
- `resolvePlatformPublicWorkDetailOpenDecision(entry, deps)`
|
- `resolvePlatformPublicWorkDetailOpenDecision(entry, deps)`
|
||||||
- `resolveActivePlatformPublicWorkAuthorEntry(args)`
|
- `resolveActivePlatformPublicWorkAuthorEntry(args)`
|
||||||
|
- `map*WorkToPublicWorkDetail(...)`
|
||||||
|
- `mapPublicWorkDetailToPuzzleWork(entry)`
|
||||||
|
- `mapPublicWorkDetailToBigFishWork(entry)`
|
||||||
|
- `mapPublicWorkDetailToSquareHoleWork(entry)`
|
||||||
|
- `mapBarkBattlePublicDetailToWorkSummary(entry)`
|
||||||
- `PlatformEntryFlowShellImpl.tsx` 继续作为 Adapter:根据 open strategy 调用 `openPublicWorkDetail`、`openPuzzlePublicWorkDetail`、`openJumpHopPublicWorkDetail`、`openWoodenFishPublicWorkDetail`、`openVisualNovelPublicWorkDetail` 或 `openRpgPublicWorkDetail`。
|
- `PlatformEntryFlowShellImpl.tsx` 继续作为 Adapter:根据 open strategy 调用 `openPublicWorkDetail`、`openPuzzlePublicWorkDetail`、`openJumpHopPublicWorkDetail`、`openWoodenFishPublicWorkDetail`、`openVisualNovelPublicWorkDetail` 或 `openRpgPublicWorkDetail`。
|
||||||
|
- 公开详情 entry 映射与公开详情反推玩法 work 摘要也收口到 Module。壳层只在运行态启动、编辑、改造、推荐缓存和详情展示时调用映射 Interface,不再在壳层顶部持有每个玩法的 DTO 拼装 Implementation。
|
||||||
|
- `mapMatch3DWorkToPublicWorkDetail` 归入 `platformMatch3DRuntimeProfile.ts`,继续委托 `normalizeMatch3DWorkForRuntimeUi` 处理素材归一和背景资产提升;`platformPublicWorkDetailFlow.ts` 不复制 Match3D 运行态素材规则。
|
||||||
- 本次不抽 `startSelectedPublicWork`、`likePublicWork`、`remixPublicWork`、`editOwnedPublicWork`。这些函数牵涉运行态启动、计数写入、草稿恢复、作品架缓存和多路错误 setter;若直接搬进一个 Hook,会形成浅 Interface。
|
- 本次不抽 `startSelectedPublicWork`、`likePublicWork`、`remixPublicWork`、`editOwnedPublicWork`。这些函数牵涉运行态启动、计数写入、草稿恢复、作品架缓存和多路错误 setter;若直接搬进一个 Hook,会形成浅 Interface。
|
||||||
|
|
||||||
## Interface 约束
|
## Interface 约束
|
||||||
@@ -26,16 +33,20 @@
|
|||||||
- `resolvePlatformPublicWorkActionMode` 只比较 `entry.ownerUserId` 与当前 viewer user id;当前用户拥有该公开作品时返回 `edit`,否则返回 `remix`。
|
- `resolvePlatformPublicWorkActionMode` 只比较 `entry.ownerUserId` 与当前 viewer user id;当前用户拥有该公开作品时返回 `edit`,否则返回 `remix`。
|
||||||
- `resolvePlatformPublicWorkDetailOpenDecision` 只表达直接展示公开详情的打开 / 阻断结果、错误文案、目标 stage 与可写入历史的路径;真正执行 setter、push history 的副作用仍由壳层 Adapter 执行。
|
- `resolvePlatformPublicWorkDetailOpenDecision` 只表达直接展示公开详情的打开 / 阻断结果、错误文案、目标 stage 与可写入历史的路径;真正执行 setter、push history 的副作用仍由壳层 Adapter 执行。
|
||||||
- `resolveActivePlatformPublicWorkAuthorEntry` 只在 `work-detail` 阶段选择统一公开详情 entry,在 RPG `detail` 阶段只选择非 draft 的 RPG 详情 entry;作者请求、竞态 request key 和缓存仍留壳层。
|
- `resolveActivePlatformPublicWorkAuthorEntry` 只在 `work-detail` 阶段选择统一公开详情 entry,在 RPG `detail` 阶段只选择非 draft 的 RPG 详情 entry;作者请求、竞态 request key 和缓存仍留壳层。
|
||||||
|
- `map*WorkToPublicWorkDetail` 只把各玩法已存在的 work / gallery summary 映射为统一详情 entry;公开码、封面、统计与标题字段继续复用 `rpgEntryWorldPresentation.ts` 的平台公开卡片映射。
|
||||||
|
- `mapPublicWorkDetailToPuzzleWork`、`mapPublicWorkDetailToBigFishWork`、`mapPublicWorkDetailToSquareHoleWork` 和 `mapBarkBattlePublicDetailToWorkSummary` 只用于公开详情 CTA、推荐缓存或运行态启动前的兼容 work 摘要拼装;缺省值必须留在 Module 测试中固定,壳层不得重复推导。
|
||||||
|
- Match3D 的公开详情与 work 摘要互转仍属于 Match3D Runtime Profile Module,因为它依赖 `generatedItemAssets` 归一化与背景资产提升。公开详情 Flow 只接统一详情策略,不复制该运行态规则。
|
||||||
|
|
||||||
## Depth / Leverage / Locality
|
## Depth / Leverage / Locality
|
||||||
|
|
||||||
- **Depth**:壳层传入公开作品 entry 和当前用户 id,即可得到详情打开策略和动作模式;玩法判定细则藏在 Module Implementation 内。
|
- **Depth**:壳层传入公开作品 entry、玩法 work summary 或当前用户 id,即可得到详情打开策略、动作模式和统一详情映射;玩法判定与 DTO 默认值藏在 Module Implementation 内。
|
||||||
- **Leverage**:新增玩法公开详情时先补 Strategy 单测,再接壳层 Adapter,不必在多个 JSX / callback 位置重复 sourceType 判断。
|
- **Leverage**:新增玩法公开详情时先补 Strategy / Mapping 单测,再接壳层 Adapter,不必在多个 JSX / callback 位置重复 sourceType 判断或 DTO 回填。
|
||||||
- **Locality**:公开作品详情入口的纯策略集中到一个小 Module;启动运行态、点赞、改造、编辑等副作用仍留在壳层,避免伪 Seam。
|
- **Locality**:公开作品详情入口的纯策略与通用映射集中到一个小 Module;Match3D 素材归一仍在 Match3D Module;启动运行态、点赞、改造、编辑等副作用仍留在壳层,避免伪 Seam。
|
||||||
|
|
||||||
## 验收
|
## 验收
|
||||||
|
|
||||||
- `npm run test -- src/components/platform-entry/platformPublicWorkDetailFlow.test.ts`
|
- `npm run test -- src/components/platform-entry/platformPublicWorkDetailFlow.test.ts`
|
||||||
|
- `npm run test -- src/components/platform-entry/platformMatch3DRuntimeProfile.test.ts`
|
||||||
- `npm run test -- src/components/platform-entry/platformPublicGalleryFlow.test.ts`
|
- `npm run test -- src/components/platform-entry/platformPublicGalleryFlow.test.ts`
|
||||||
- `npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx`
|
- `npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx`
|
||||||
- `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "public detail|owned public puzzle detail|direct missing public work detail"`
|
- `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "public detail|owned public puzzle detail|direct missing public work detail"`
|
||||||
|
|||||||
@@ -374,7 +374,6 @@ import {
|
|||||||
mapBarkBattleWorkToPlatformGalleryCard,
|
mapBarkBattleWorkToPlatformGalleryCard,
|
||||||
mapBigFishWorkToPlatformGalleryCard,
|
mapBigFishWorkToPlatformGalleryCard,
|
||||||
mapJumpHopWorkToPlatformGalleryCard,
|
mapJumpHopWorkToPlatformGalleryCard,
|
||||||
mapMatch3DWorkToPlatformGalleryCard,
|
|
||||||
mapPuzzleWorkToPlatformGalleryCard,
|
mapPuzzleWorkToPlatformGalleryCard,
|
||||||
mapSquareHoleWorkToPlatformGalleryCard,
|
mapSquareHoleWorkToPlatformGalleryCard,
|
||||||
mapVisualNovelWorkToPlatformGalleryCard,
|
mapVisualNovelWorkToPlatformGalleryCard,
|
||||||
@@ -500,6 +499,7 @@ import {
|
|||||||
hasMatch3DRuntimeAsset,
|
hasMatch3DRuntimeAsset,
|
||||||
hasMatch3DRuntimeBackgroundAsset,
|
hasMatch3DRuntimeBackgroundAsset,
|
||||||
mapMatch3DWorksForRuntimeUi,
|
mapMatch3DWorksForRuntimeUi,
|
||||||
|
mapMatch3DWorkToPublicWorkDetail,
|
||||||
mapPublicWorkDetailToMatch3DWork,
|
mapPublicWorkDetailToMatch3DWork,
|
||||||
normalizeMatch3DWorkForRuntimeUi,
|
normalizeMatch3DWorkForRuntimeUi,
|
||||||
promoteMatch3DGeneratedBackgroundAsset,
|
promoteMatch3DGeneratedBackgroundAsset,
|
||||||
@@ -516,6 +516,18 @@ import {
|
|||||||
type RecommendRuntimeKind,
|
type RecommendRuntimeKind,
|
||||||
} from './platformPublicGalleryFlow';
|
} from './platformPublicGalleryFlow';
|
||||||
import {
|
import {
|
||||||
|
mapBarkBattlePublicDetailToWorkSummary,
|
||||||
|
mapBarkBattleWorkToPublicWorkDetail,
|
||||||
|
mapBigFishWorkToPublicWorkDetail,
|
||||||
|
mapJumpHopWorkToPublicWorkDetail,
|
||||||
|
mapPublicWorkDetailToBigFishWork,
|
||||||
|
mapPublicWorkDetailToPuzzleWork,
|
||||||
|
mapPublicWorkDetailToSquareHoleWork,
|
||||||
|
mapPuzzleWorkToPublicWorkDetail,
|
||||||
|
mapRpgGalleryCardToPublicWorkDetail,
|
||||||
|
mapSquareHoleWorkToPublicWorkDetail,
|
||||||
|
mapVisualNovelWorkToPublicWorkDetail,
|
||||||
|
mapWoodenFishWorkToPublicWorkDetail,
|
||||||
resolveActivePlatformPublicWorkAuthorEntry,
|
resolveActivePlatformPublicWorkAuthorEntry,
|
||||||
resolvePlatformPublicWorkActionMode,
|
resolvePlatformPublicWorkActionMode,
|
||||||
resolvePlatformPublicWorkDetailOpenDecision,
|
resolvePlatformPublicWorkDetailOpenDecision,
|
||||||
@@ -719,18 +731,6 @@ function isRecommendRuntimeReadyForEntry(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRpgGalleryCardToPublicWorkDetail(
|
|
||||||
entry: CustomWorldGalleryCard,
|
|
||||||
): PlatformPublicGalleryCard {
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapPuzzleWorkToPublicWorkDetail(
|
|
||||||
item: PuzzleWorkSummary,
|
|
||||||
): PlatformPublicGalleryCard {
|
|
||||||
return mapPuzzleWorkToPlatformGalleryCard(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveVisiblePuzzleDetailCoverCount(
|
function resolveVisiblePuzzleDetailCoverCount(
|
||||||
entry: PlatformPublicGalleryCard | null,
|
entry: PlatformPublicGalleryCard | null,
|
||||||
run: PuzzleRunSnapshot | null,
|
run: PuzzleRunSnapshot | null,
|
||||||
@@ -747,44 +747,6 @@ function resolveVisiblePuzzleDetailCoverCount(
|
|||||||
return Math.max(1, run.clearedLevelCount + 1);
|
return Math.max(1, run.clearedLevelCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapMatch3DWorkToPublicWorkDetail(
|
|
||||||
item: Match3DWorkSummary,
|
|
||||||
): PlatformPublicGalleryCard {
|
|
||||||
return mapMatch3DWorkToPlatformGalleryCard(
|
|
||||||
normalizeMatch3DWorkForRuntimeUi(item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapSquareHoleWorkToPublicWorkDetail(
|
|
||||||
item: SquareHoleWorkSummary,
|
|
||||||
): PlatformPublicGalleryCard {
|
|
||||||
return mapSquareHoleWorkToPlatformGalleryCard(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapBigFishWorkToPublicWorkDetail(
|
|
||||||
item: BigFishWorkSummary,
|
|
||||||
): PlatformPublicGalleryCard {
|
|
||||||
return mapBigFishWorkToPlatformGalleryCard(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapVisualNovelWorkToPublicWorkDetail(
|
|
||||||
item: VisualNovelWorkSummary,
|
|
||||||
): PlatformPublicGalleryCard {
|
|
||||||
return mapVisualNovelWorkToPlatformGalleryCard(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapJumpHopWorkToPublicWorkDetail(
|
|
||||||
item: JumpHopGalleryCardResponse | JumpHopWorkProfileResponse,
|
|
||||||
): PlatformPublicGalleryCard {
|
|
||||||
return mapJumpHopWorkToPlatformGalleryCard(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapBarkBattleWorkToPublicWorkDetail(
|
|
||||||
item: BarkBattleWorkSummary,
|
|
||||||
): PlatformPublicGalleryCard {
|
|
||||||
return mapBarkBattleWorkToPlatformGalleryCard(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapBarkBattleWorkToPublishedConfig(
|
function mapBarkBattleWorkToPublishedConfig(
|
||||||
work: BarkBattleWorkSummary,
|
work: BarkBattleWorkSummary,
|
||||||
): BarkBattlePublishedConfig {
|
): BarkBattlePublishedConfig {
|
||||||
@@ -809,44 +771,6 @@ function mapBarkBattleWorkToPublishedConfig(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapBarkBattlePublicDetailToWorkSummary(
|
|
||||||
entry: PlatformPublicGalleryCard,
|
|
||||||
): BarkBattleWorkSummary | null {
|
|
||||||
if (!isBarkBattleGalleryEntry(entry)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
workId: entry.workId,
|
|
||||||
draftId: entry.sourceSessionId ?? null,
|
|
||||||
ownerUserId: entry.ownerUserId,
|
|
||||||
authorDisplayName: entry.authorDisplayName,
|
|
||||||
title: entry.worldName,
|
|
||||||
summary: entry.summaryText,
|
|
||||||
themeDescription: entry.themeTags[0] ?? entry.summaryText,
|
|
||||||
playerImageDescription: entry.themeTags[1] ?? entry.summaryText,
|
|
||||||
opponentImageDescription: entry.themeTags[2] ?? entry.summaryText,
|
|
||||||
onomatopoeia: undefined,
|
|
||||||
playerCharacterImageSrc: entry.coverCharacterImageSrcs[0] ?? null,
|
|
||||||
opponentCharacterImageSrc: entry.coverCharacterImageSrcs[1] ?? null,
|
|
||||||
uiBackgroundImageSrc: entry.coverImageSrc,
|
|
||||||
difficultyPreset: 'normal',
|
|
||||||
status: 'published',
|
|
||||||
generationStatus: 'ready',
|
|
||||||
publishReady: true,
|
|
||||||
playCount: entry.playCount ?? 0,
|
|
||||||
recentPlayCount7d: entry.recentPlayCount7d ?? 0,
|
|
||||||
updatedAt: entry.updatedAt,
|
|
||||||
publishedAt: entry.publishedAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapWoodenFishWorkToPublicWorkDetail(
|
|
||||||
item: WoodenFishGalleryCardResponse | WoodenFishWorkProfileResponse,
|
|
||||||
): PlatformPublicGalleryCard {
|
|
||||||
return mapWoodenFishWorkToPlatformGalleryCard(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapVisualNovelWorkDetailToSession(
|
function mapVisualNovelWorkDetailToSession(
|
||||||
work: VisualNovelWorkDetail,
|
work: VisualNovelWorkDetail,
|
||||||
): VisualNovelAgentSessionSnapshot {
|
): VisualNovelAgentSessionSnapshot {
|
||||||
@@ -892,122 +816,6 @@ function resolveMatch3DGenerationStateFromAssets(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapPublicWorkDetailToPuzzleWork(
|
|
||||||
entry: PlatformPublicGalleryCard,
|
|
||||||
): PuzzleWorkSummary | null {
|
|
||||||
if (!isPuzzleGalleryEntry(entry)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
workId: entry.workId,
|
|
||||||
profileId: entry.profileId,
|
|
||||||
ownerUserId: entry.ownerUserId,
|
|
||||||
sourceSessionId:
|
|
||||||
'sourceSessionId' in entry && typeof entry.sourceSessionId === 'string'
|
|
||||||
? entry.sourceSessionId
|
|
||||||
: null,
|
|
||||||
authorDisplayName: entry.authorDisplayName,
|
|
||||||
levelName: entry.worldName,
|
|
||||||
summary: entry.summaryText,
|
|
||||||
themeTags: entry.themeTags,
|
|
||||||
coverImageSrc: entry.coverImageSrc,
|
|
||||||
publicationStatus: 'published',
|
|
||||||
updatedAt: entry.updatedAt,
|
|
||||||
publishedAt: entry.publishedAt,
|
|
||||||
playCount: entry.playCount ?? 0,
|
|
||||||
remixCount: entry.remixCount ?? 0,
|
|
||||||
likeCount: entry.likeCount ?? 0,
|
|
||||||
pointIncentiveTotalHalfPoints: 0,
|
|
||||||
pointIncentiveClaimedPoints: 0,
|
|
||||||
pointIncentiveTotalPoints: 0,
|
|
||||||
pointIncentiveClaimablePoints: 0,
|
|
||||||
publishReady: true,
|
|
||||||
levels:
|
|
||||||
entry.coverSlides?.map((slide, index) => ({
|
|
||||||
levelId: slide.id || `puzzle-level-${index + 1}`,
|
|
||||||
levelName: slide.label,
|
|
||||||
pictureDescription: entry.summaryText,
|
|
||||||
candidates: [],
|
|
||||||
selectedCandidateId: null,
|
|
||||||
coverImageSrc: slide.imageSrc,
|
|
||||||
coverAssetId: null,
|
|
||||||
generationStatus: 'ready' as const,
|
|
||||||
})) ?? [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapPublicWorkDetailToBigFishWork(
|
|
||||||
entry: PlatformPublicGalleryCard,
|
|
||||||
): BigFishWorkSummary | null {
|
|
||||||
if (!isBigFishGalleryEntry(entry)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const levelCount = Number.parseInt(
|
|
||||||
entry.themeTags.find((tag) => /^\d+级$/u.test(tag))?.replace('级', '') ??
|
|
||||||
'0',
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
workId: entry.workId,
|
|
||||||
sourceSessionId: entry.profileId,
|
|
||||||
ownerUserId: entry.ownerUserId,
|
|
||||||
authorDisplayName: entry.authorDisplayName,
|
|
||||||
title: entry.worldName,
|
|
||||||
subtitle: entry.subtitle,
|
|
||||||
summary: entry.summaryText,
|
|
||||||
coverImageSrc: entry.coverImageSrc,
|
|
||||||
status: 'published',
|
|
||||||
updatedAt: entry.updatedAt,
|
|
||||||
publishedAt: entry.publishedAt,
|
|
||||||
publishReady: true,
|
|
||||||
levelCount: Number.isNaN(levelCount) ? 0 : levelCount,
|
|
||||||
levelMainImageReadyCount: 0,
|
|
||||||
levelMotionReadyCount: 0,
|
|
||||||
backgroundReady: Boolean(entry.coverImageSrc),
|
|
||||||
playCount: entry.playCount ?? 0,
|
|
||||||
remixCount: entry.remixCount ?? 0,
|
|
||||||
likeCount: entry.likeCount ?? 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapPublicWorkDetailToSquareHoleWork(
|
|
||||||
entry: PlatformPublicGalleryCard,
|
|
||||||
): SquareHoleWorkSummary | null {
|
|
||||||
if (!isSquareHoleGalleryEntry(entry)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
workId: entry.workId,
|
|
||||||
profileId: entry.profileId,
|
|
||||||
ownerUserId: entry.ownerUserId,
|
|
||||||
sourceSessionId:
|
|
||||||
'sourceSessionId' in entry && typeof entry.sourceSessionId === 'string'
|
|
||||||
? entry.sourceSessionId
|
|
||||||
: null,
|
|
||||||
gameName: entry.worldName,
|
|
||||||
themeText: entry.themeTags[0] ?? '方洞挑战',
|
|
||||||
twistRule: entry.subtitle,
|
|
||||||
summary: entry.summaryText,
|
|
||||||
tags: entry.themeTags,
|
|
||||||
coverImageSrc: entry.coverImageSrc,
|
|
||||||
backgroundPrompt: entry.backgroundPrompt ?? '方洞挑战运行背景',
|
|
||||||
backgroundImageSrc: entry.backgroundImageSrc ?? null,
|
|
||||||
shapeOptions: entry.shapeOptions ?? [],
|
|
||||||
holeOptions: entry.holeOptions ?? [],
|
|
||||||
shapeCount: entry.shapeCount ?? 8,
|
|
||||||
difficulty: entry.difficulty ?? 4,
|
|
||||||
publicationStatus: 'published',
|
|
||||||
playCount: entry.playCount ?? 0,
|
|
||||||
updatedAt: entry.updatedAt,
|
|
||||||
publishedAt: entry.publishedAt,
|
|
||||||
publishReady: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSquareHoleProfileFromSession(
|
function buildSquareHoleProfileFromSession(
|
||||||
session: SquareHoleSessionSnapshot | null,
|
session: SquareHoleSessionSnapshot | null,
|
||||||
): SquareHoleWorkProfile | null {
|
): SquareHoleWorkProfile | null {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
import type { PlatformMatch3DGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation';
|
import type { PlatformMatch3DGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
import {
|
import {
|
||||||
buildMatch3DProfileFromSession,
|
buildMatch3DProfileFromSession,
|
||||||
|
mapMatch3DWorkToPublicWorkDetail,
|
||||||
mapPublicWorkDetailToMatch3DWork,
|
mapPublicWorkDetailToMatch3DWork,
|
||||||
resolveActiveMatch3DRuntimeProfile,
|
resolveActiveMatch3DRuntimeProfile,
|
||||||
resolveMatch3DRuntimeBackgroundImageSrc,
|
resolveMatch3DRuntimeBackgroundImageSrc,
|
||||||
@@ -142,6 +143,31 @@ test('Match3D runtime profile maps public detail and promotes item background as
|
|||||||
expect(work?.backgroundImageObjectKey).toBe('oss/background-from-item.png');
|
expect(work?.backgroundImageObjectKey).toBe('oss/background-from-item.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Match3D runtime profile maps work summary to public detail with promoted background asset', () => {
|
||||||
|
const backgroundAsset = buildBackgroundAsset({
|
||||||
|
imageSrc: '/generated/match3d/detail-background.png',
|
||||||
|
});
|
||||||
|
const detail = mapMatch3DWorkToPublicWorkDetail(
|
||||||
|
buildProfile({
|
||||||
|
generatedBackgroundAsset: null,
|
||||||
|
backgroundImageSrc: null,
|
||||||
|
generatedItemAssets: [
|
||||||
|
buildItemAsset({
|
||||||
|
backgroundAsset,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(detail).toMatchObject({
|
||||||
|
sourceType: 'match3d',
|
||||||
|
workId: 'match3d-work-1',
|
||||||
|
profileId: 'match3d-profile-1',
|
||||||
|
backgroundImageSrc: '/generated/match3d/detail-background.png',
|
||||||
|
generatedBackgroundAsset: backgroundAsset,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Match3D runtime profile builds draft profile from session snapshot', () => {
|
test('Match3D runtime profile builds draft profile from session snapshot', () => {
|
||||||
const backgroundAsset = buildBackgroundAsset({
|
const backgroundAsset = buildBackgroundAsset({
|
||||||
imageSrc: '/generated/match3d/draft-background.png',
|
imageSrc: '/generated/match3d/draft-background.png',
|
||||||
|
|||||||
@@ -13,9 +13,18 @@ import {
|
|||||||
} from '../../services/match3dGeneratedModelCache';
|
} from '../../services/match3dGeneratedModelCache';
|
||||||
import {
|
import {
|
||||||
isMatch3DGalleryEntry,
|
isMatch3DGalleryEntry,
|
||||||
|
mapMatch3DWorkToPlatformGalleryCard,
|
||||||
type PlatformPublicGalleryCard,
|
type PlatformPublicGalleryCard,
|
||||||
} from '../rpg-entry/rpgEntryWorldPresentation';
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
|
|
||||||
|
export function mapMatch3DWorkToPublicWorkDetail(
|
||||||
|
item: Match3DWorkSummary,
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
return mapMatch3DWorkToPlatformGalleryCard(
|
||||||
|
normalizeMatch3DWorkForRuntimeUi(item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function mapPublicWorkDetailToMatch3DWork(
|
export function mapPublicWorkDetailToMatch3DWork(
|
||||||
entry: PlatformPublicGalleryCard,
|
entry: PlatformPublicGalleryCard,
|
||||||
): Match3DWorkSummary | null {
|
): Match3DWorkSummary | null {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { expect, test } from 'vitest';
|
import { expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||||
|
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||||
|
import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||||
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||||
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||||
|
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||||
|
import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||||
import {
|
import {
|
||||||
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
||||||
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||||
@@ -8,6 +15,18 @@ import {
|
|||||||
} from '../rpg-entry/rpgEntryWorldPresentation';
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
import {
|
import {
|
||||||
getPlatformPublicWorkDetailKind,
|
getPlatformPublicWorkDetailKind,
|
||||||
|
mapBarkBattlePublicDetailToWorkSummary,
|
||||||
|
mapBarkBattleWorkToPublicWorkDetail,
|
||||||
|
mapBigFishWorkToPublicWorkDetail,
|
||||||
|
mapJumpHopWorkToPublicWorkDetail,
|
||||||
|
mapPublicWorkDetailToBigFishWork,
|
||||||
|
mapPublicWorkDetailToPuzzleWork,
|
||||||
|
mapPublicWorkDetailToSquareHoleWork,
|
||||||
|
mapPuzzleWorkToPublicWorkDetail,
|
||||||
|
mapRpgGalleryCardToPublicWorkDetail,
|
||||||
|
mapSquareHoleWorkToPublicWorkDetail,
|
||||||
|
mapVisualNovelWorkToPublicWorkDetail,
|
||||||
|
mapWoodenFishWorkToPublicWorkDetail,
|
||||||
type PlatformPublicWorkDetailKind,
|
type PlatformPublicWorkDetailKind,
|
||||||
type PlatformPublicWorkDetailOpenStrategy,
|
type PlatformPublicWorkDetailOpenStrategy,
|
||||||
resolveActivePlatformPublicWorkAuthorEntry,
|
resolveActivePlatformPublicWorkAuthorEntry,
|
||||||
@@ -21,10 +40,21 @@ type TypedPlatformPublicGalleryCard = Extract<
|
|||||||
{ sourceType: string }
|
{ sourceType: string }
|
||||||
>;
|
>;
|
||||||
type PlatformGallerySourceType = TypedPlatformPublicGalleryCard['sourceType'];
|
type PlatformGallerySourceType = TypedPlatformPublicGalleryCard['sourceType'];
|
||||||
type TypedPlatformPublicGalleryCardOverrides = Partial<
|
type TypedPlatformPublicGalleryCardOverrides<
|
||||||
Omit<TypedPlatformPublicGalleryCard, 'sourceType'>
|
TSourceType extends PlatformGallerySourceType,
|
||||||
|
> = Partial<
|
||||||
|
Omit<
|
||||||
|
Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }>,
|
||||||
|
'sourceType'
|
||||||
|
>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
function narrowTypedEntry<TSourceType extends PlatformGallerySourceType>(
|
||||||
|
entry: TypedPlatformPublicGalleryCard,
|
||||||
|
): Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }> {
|
||||||
|
return entry as Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }>;
|
||||||
|
}
|
||||||
|
|
||||||
function buildRpgEntry(
|
function buildRpgEntry(
|
||||||
overrides: Partial<CustomWorldGalleryCard> = {},
|
overrides: Partial<CustomWorldGalleryCard> = {},
|
||||||
): CustomWorldGalleryCard {
|
): CustomWorldGalleryCard {
|
||||||
@@ -48,10 +78,10 @@ function buildRpgEntry(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTypedEntry(
|
function buildTypedEntry<TSourceType extends PlatformGallerySourceType>(
|
||||||
sourceType: PlatformGallerySourceType,
|
sourceType: TSourceType,
|
||||||
overrides: TypedPlatformPublicGalleryCardOverrides = {},
|
overrides: TypedPlatformPublicGalleryCardOverrides<TSourceType> = {},
|
||||||
): PlatformPublicGalleryCard {
|
): Extract<TypedPlatformPublicGalleryCard, { sourceType: TSourceType }> {
|
||||||
const common = {
|
const common = {
|
||||||
workId: `${sourceType}-work`,
|
workId: `${sourceType}-work`,
|
||||||
profileId: `${sourceType}-profile`,
|
profileId: `${sourceType}-profile`,
|
||||||
@@ -70,31 +100,30 @@ function buildTypedEntry(
|
|||||||
|
|
||||||
switch (sourceType) {
|
switch (sourceType) {
|
||||||
case 'puzzle':
|
case 'puzzle':
|
||||||
return { ...common, ...overrides, sourceType };
|
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||||
case 'big-fish':
|
case 'big-fish':
|
||||||
return { ...common, ...overrides, sourceType };
|
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||||
case 'match3d':
|
case 'match3d':
|
||||||
return { ...common, ...overrides, sourceType };
|
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||||
case 'square-hole':
|
case 'square-hole':
|
||||||
return { ...common, ...overrides, sourceType };
|
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||||
case 'visual-novel':
|
case 'visual-novel':
|
||||||
return { ...common, ...overrides, sourceType };
|
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||||
case 'jump-hop':
|
case 'jump-hop':
|
||||||
return { ...common, ...overrides, sourceType };
|
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||||
case 'wooden-fish':
|
case 'wooden-fish':
|
||||||
return { ...common, ...overrides, sourceType };
|
return narrowTypedEntry<TSourceType>({ ...common, ...overrides, sourceType });
|
||||||
case 'edutainment':
|
case 'edutainment':
|
||||||
return {
|
return narrowTypedEntry<TSourceType>({
|
||||||
...common,
|
...common,
|
||||||
...overrides,
|
|
||||||
sourceType,
|
sourceType,
|
||||||
templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
|
||||||
templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
|
||||||
};
|
|
||||||
case 'bark-battle':
|
|
||||||
return {
|
|
||||||
...common,
|
|
||||||
...overrides,
|
...overrides,
|
||||||
|
});
|
||||||
|
case 'bark-battle':
|
||||||
|
return narrowTypedEntry<TSourceType>({
|
||||||
|
...common,
|
||||||
sourceType,
|
sourceType,
|
||||||
authorPublicUserCode: null,
|
authorPublicUserCode: null,
|
||||||
coverRenderMode: 'image',
|
coverRenderMode: 'image',
|
||||||
@@ -102,14 +131,190 @@ function buildTypedEntry(
|
|||||||
themeMode: 'martial',
|
themeMode: 'martial',
|
||||||
playableNpcCount: 1,
|
playableNpcCount: 1,
|
||||||
landmarkCount: 1,
|
landmarkCount: 1,
|
||||||
};
|
...overrides,
|
||||||
|
});
|
||||||
default: {
|
default: {
|
||||||
const exhaustive: never = sourceType;
|
throw new Error(`Unsupported source type: ${sourceType}`);
|
||||||
return exhaustive;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPuzzleWork(
|
||||||
|
overrides: Partial<PuzzleWorkSummary> = {},
|
||||||
|
): PuzzleWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'puzzle-work',
|
||||||
|
profileId: 'puzzle-profile',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
sourceSessionId: 'puzzle-session',
|
||||||
|
authorDisplayName: '玩家',
|
||||||
|
workTitle: '拼图作品',
|
||||||
|
workDescription: '拼图描述',
|
||||||
|
levelName: '第一关',
|
||||||
|
summary: '拼图摘要',
|
||||||
|
themeTags: ['拼图'],
|
||||||
|
coverImageSrc: '/puzzle-cover.png',
|
||||||
|
publicationStatus: 'published',
|
||||||
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||||
|
playCount: 3,
|
||||||
|
remixCount: 2,
|
||||||
|
likeCount: 1,
|
||||||
|
publishReady: true,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBigFishWork(
|
||||||
|
overrides: Partial<BigFishWorkSummary> = {},
|
||||||
|
): BigFishWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'big-fish-work',
|
||||||
|
sourceSessionId: 'big-fish-session',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '玩家',
|
||||||
|
title: '大鱼作品',
|
||||||
|
subtitle: '大鱼吃小鱼',
|
||||||
|
summary: '大鱼摘要',
|
||||||
|
coverImageSrc: '/big-fish-cover.png',
|
||||||
|
status: 'published',
|
||||||
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||||
|
publishReady: true,
|
||||||
|
levelCount: 12,
|
||||||
|
levelMainImageReadyCount: 0,
|
||||||
|
levelMotionReadyCount: 0,
|
||||||
|
backgroundReady: true,
|
||||||
|
playCount: 4,
|
||||||
|
remixCount: 3,
|
||||||
|
likeCount: 2,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSquareHoleWork(
|
||||||
|
overrides: Partial<SquareHoleWorkSummary> = {},
|
||||||
|
): SquareHoleWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'square-hole-work',
|
||||||
|
profileId: 'square-hole-profile',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
sourceSessionId: 'square-hole-session',
|
||||||
|
gameName: '方洞作品',
|
||||||
|
themeText: '形状',
|
||||||
|
twistRule: '反直觉',
|
||||||
|
summary: '方洞摘要',
|
||||||
|
tags: ['方洞'],
|
||||||
|
coverImageSrc: '/square-hole-cover.png',
|
||||||
|
backgroundPrompt: '方洞背景',
|
||||||
|
backgroundImageSrc: '/square-hole-bg.png',
|
||||||
|
shapeOptions: [],
|
||||||
|
holeOptions: [],
|
||||||
|
shapeCount: 8,
|
||||||
|
difficulty: 4,
|
||||||
|
publicationStatus: 'published',
|
||||||
|
playCount: 5,
|
||||||
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||||
|
publishReady: true,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVisualNovelWork(
|
||||||
|
overrides: Partial<VisualNovelWorkSummary> = {},
|
||||||
|
): VisualNovelWorkSummary {
|
||||||
|
return {
|
||||||
|
runtimeKind: 'visual-novel',
|
||||||
|
profileId: 'visual-novel-profile',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
title: '视觉小说作品',
|
||||||
|
description: '视觉小说摘要',
|
||||||
|
coverImageSrc: '/visual-novel-cover.png',
|
||||||
|
tags: ['视觉小说'],
|
||||||
|
publishStatus: 'published',
|
||||||
|
publishReady: true,
|
||||||
|
playCount: 6,
|
||||||
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildJumpHopGalleryCard(
|
||||||
|
overrides: Partial<JumpHopGalleryCardResponse> = {},
|
||||||
|
): JumpHopGalleryCardResponse {
|
||||||
|
return {
|
||||||
|
publicWorkCode: 'JH-0001',
|
||||||
|
workId: 'jump-hop-work',
|
||||||
|
profileId: 'jump-hop-profile',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '玩家',
|
||||||
|
workTitle: '跳一跳作品',
|
||||||
|
workDescription: '跳一跳摘要',
|
||||||
|
coverImageSrc: '/jump-hop-cover.png',
|
||||||
|
themeTags: ['跳一跳'],
|
||||||
|
difficulty: 'standard',
|
||||||
|
stylePreset: 'paper-toy',
|
||||||
|
publicationStatus: 'published',
|
||||||
|
playCount: 7,
|
||||||
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||||
|
generationStatus: 'ready',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWoodenFishGalleryCard(
|
||||||
|
overrides: Partial<WoodenFishGalleryCardResponse> = {},
|
||||||
|
): WoodenFishGalleryCardResponse {
|
||||||
|
return {
|
||||||
|
publicWorkCode: 'WF-0001',
|
||||||
|
workId: 'wooden-fish-work',
|
||||||
|
profileId: 'wooden-fish-profile',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '玩家',
|
||||||
|
workTitle: '木鱼作品',
|
||||||
|
workDescription: '木鱼摘要',
|
||||||
|
coverImageSrc: '/wooden-fish-cover.png',
|
||||||
|
themeTags: ['敲木鱼'],
|
||||||
|
publicationStatus: 'published',
|
||||||
|
playCount: 8,
|
||||||
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||||
|
generationStatus: 'ready',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBarkBattleWork(
|
||||||
|
overrides: Partial<BarkBattleWorkSummary> = {},
|
||||||
|
): BarkBattleWorkSummary {
|
||||||
|
return {
|
||||||
|
workId: 'bark-battle-work',
|
||||||
|
draftId: 'bark-battle-draft',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '玩家',
|
||||||
|
title: '汪汪声浪作品',
|
||||||
|
summary: '汪汪摘要',
|
||||||
|
themeDescription: '森林擂台',
|
||||||
|
playerImageDescription: '小狗',
|
||||||
|
opponentImageDescription: '对手',
|
||||||
|
playerCharacterImageSrc: '/player.png',
|
||||||
|
opponentCharacterImageSrc: '/opponent.png',
|
||||||
|
uiBackgroundImageSrc: '/bark-bg.png',
|
||||||
|
difficultyPreset: 'normal',
|
||||||
|
status: 'published',
|
||||||
|
generationStatus: 'ready',
|
||||||
|
publishReady: true,
|
||||||
|
playCount: 9,
|
||||||
|
recentPlayCount7d: 2,
|
||||||
|
updatedAt: '2026-06-01T01:00:00.000Z',
|
||||||
|
publishedAt: '2026-06-01T00:00:00.000Z',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
test('platform public work detail flow resolves detail kind for every play kind', () => {
|
test('platform public work detail flow resolves detail kind for every play kind', () => {
|
||||||
const cases: Array<
|
const cases: Array<
|
||||||
[sourceType: PlatformGallerySourceType, kind: PlatformPublicWorkDetailKind]
|
[sourceType: PlatformGallerySourceType, kind: PlatformPublicWorkDetailKind]
|
||||||
@@ -221,6 +426,173 @@ test('platform public work detail flow resolves open strategy', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('platform public work detail flow maps work summaries to detail entries', () => {
|
||||||
|
const rpgEntry = buildRpgEntry();
|
||||||
|
|
||||||
|
expect(mapRpgGalleryCardToPublicWorkDetail(rpgEntry)).toBe(rpgEntry);
|
||||||
|
expect(mapPuzzleWorkToPublicWorkDetail(buildPuzzleWork())).toMatchObject({
|
||||||
|
sourceType: 'puzzle',
|
||||||
|
workId: 'puzzle-work',
|
||||||
|
profileId: 'puzzle-profile',
|
||||||
|
playCount: 3,
|
||||||
|
remixCount: 2,
|
||||||
|
likeCount: 1,
|
||||||
|
});
|
||||||
|
expect(mapBigFishWorkToPublicWorkDetail(buildBigFishWork())).toMatchObject({
|
||||||
|
sourceType: 'big-fish',
|
||||||
|
workId: 'big-fish-work',
|
||||||
|
profileId: 'big-fish-session',
|
||||||
|
playCount: 4,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
mapSquareHoleWorkToPublicWorkDetail(buildSquareHoleWork()),
|
||||||
|
).toMatchObject({
|
||||||
|
sourceType: 'square-hole',
|
||||||
|
workId: 'square-hole-work',
|
||||||
|
profileId: 'square-hole-profile',
|
||||||
|
backgroundPrompt: '方洞背景',
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
mapVisualNovelWorkToPublicWorkDetail(buildVisualNovelWork()),
|
||||||
|
).toMatchObject({
|
||||||
|
sourceType: 'visual-novel',
|
||||||
|
workId: 'visual-novel-profile',
|
||||||
|
profileId: 'visual-novel-profile',
|
||||||
|
playCount: 6,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
mapJumpHopWorkToPublicWorkDetail(buildJumpHopGalleryCard()),
|
||||||
|
).toMatchObject({
|
||||||
|
sourceType: 'jump-hop',
|
||||||
|
workId: 'jump-hop-work',
|
||||||
|
profileId: 'jump-hop-profile',
|
||||||
|
publicWorkCode: 'JH-0001',
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
mapWoodenFishWorkToPublicWorkDetail(buildWoodenFishGalleryCard()),
|
||||||
|
).toMatchObject({
|
||||||
|
sourceType: 'wooden-fish',
|
||||||
|
workId: 'wooden-fish-work',
|
||||||
|
profileId: 'wooden-fish-profile',
|
||||||
|
publicWorkCode: 'WF-0001',
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
mapBarkBattleWorkToPublicWorkDetail(buildBarkBattleWork()),
|
||||||
|
).toMatchObject({
|
||||||
|
sourceType: 'bark-battle',
|
||||||
|
workId: 'bark-battle-work',
|
||||||
|
sourceSessionId: 'bark-battle-draft',
|
||||||
|
coverRenderMode: 'scene_with_roles',
|
||||||
|
coverCharacterImageSrcs: ['/player.png', '/opponent.png'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('platform public work detail flow maps detail entries back to work summaries', () => {
|
||||||
|
expect(
|
||||||
|
mapPublicWorkDetailToPuzzleWork({
|
||||||
|
...buildTypedEntry('puzzle', {
|
||||||
|
coverSlides: [
|
||||||
|
{
|
||||||
|
id: 'level-1',
|
||||||
|
imageSrc: '/level-1.png',
|
||||||
|
label: '第一关',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
playCount: 10,
|
||||||
|
remixCount: 4,
|
||||||
|
likeCount: 3,
|
||||||
|
}),
|
||||||
|
sourceSessionId: 'puzzle-session',
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
workId: 'puzzle-work',
|
||||||
|
profileId: 'puzzle-profile',
|
||||||
|
sourceSessionId: 'puzzle-session',
|
||||||
|
playCount: 10,
|
||||||
|
remixCount: 4,
|
||||||
|
likeCount: 3,
|
||||||
|
pointIncentiveTotalPoints: 0,
|
||||||
|
levels: [
|
||||||
|
{
|
||||||
|
levelId: 'level-1',
|
||||||
|
levelName: '第一关',
|
||||||
|
coverImageSrc: '/level-1.png',
|
||||||
|
generationStatus: 'ready',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mapPublicWorkDetailToBigFishWork(
|
||||||
|
buildTypedEntry('big-fish', {
|
||||||
|
themeTags: ['大鱼', '12级'],
|
||||||
|
coverImageSrc: '/big-fish-cover.png',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
workId: 'big-fish-work',
|
||||||
|
sourceSessionId: 'big-fish-profile',
|
||||||
|
levelCount: 12,
|
||||||
|
backgroundReady: true,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
mapPublicWorkDetailToBigFishWork(
|
||||||
|
buildTypedEntry('big-fish', { themeTags: ['大鱼'] }),
|
||||||
|
)?.levelCount,
|
||||||
|
).toBe(0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mapPublicWorkDetailToSquareHoleWork(
|
||||||
|
buildTypedEntry('square-hole', { themeTags: [] }),
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
workId: 'square-hole-work',
|
||||||
|
profileId: 'square-hole-profile',
|
||||||
|
themeText: '方洞挑战',
|
||||||
|
backgroundPrompt: '方洞挑战运行背景',
|
||||||
|
shapeOptions: [],
|
||||||
|
holeOptions: [],
|
||||||
|
shapeCount: 8,
|
||||||
|
difficulty: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mapBarkBattlePublicDetailToWorkSummary(
|
||||||
|
{
|
||||||
|
...buildTypedEntry('bark-battle', {
|
||||||
|
themeTags: ['森林', '小狗', '对手'],
|
||||||
|
coverImageSrc: '/bark-bg.png',
|
||||||
|
coverCharacterImageSrcs: ['/player.png', '/opponent.png'],
|
||||||
|
playCount: 11,
|
||||||
|
recentPlayCount7d: 5,
|
||||||
|
}),
|
||||||
|
sourceSessionId: 'bark-draft',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
workId: 'bark-battle-work',
|
||||||
|
draftId: 'bark-draft',
|
||||||
|
themeDescription: '森林',
|
||||||
|
playerImageDescription: '小狗',
|
||||||
|
opponentImageDescription: '对手',
|
||||||
|
playerCharacterImageSrc: '/player.png',
|
||||||
|
opponentCharacterImageSrc: '/opponent.png',
|
||||||
|
uiBackgroundImageSrc: '/bark-bg.png',
|
||||||
|
difficultyPreset: 'normal',
|
||||||
|
playCount: 11,
|
||||||
|
recentPlayCount7d: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mapPublicWorkDetailToPuzzleWork(buildTypedEntry('big-fish'))).toBeNull();
|
||||||
|
expect(mapPublicWorkDetailToBigFishWork(buildTypedEntry('puzzle'))).toBeNull();
|
||||||
|
expect(
|
||||||
|
mapPublicWorkDetailToSquareHoleWork(buildTypedEntry('puzzle')),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
mapBarkBattlePublicDetailToWorkSummary(buildTypedEntry('puzzle')),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
test('platform public work detail flow resolves edit mode only for owned works', () => {
|
test('platform public work detail flow resolves edit mode only for owned works', () => {
|
||||||
const entry = buildTypedEntry('puzzle');
|
const entry = buildTypedEntry('puzzle');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
|
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||||
|
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||||
|
import type {
|
||||||
|
JumpHopGalleryCardResponse,
|
||||||
|
JumpHopWorkProfileResponse,
|
||||||
|
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||||
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||||
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||||
|
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||||
|
import type {
|
||||||
|
WoodenFishGalleryCardResponse,
|
||||||
|
WoodenFishWorkProfileResponse,
|
||||||
|
} from '../../../packages/shared/src/contracts/woodenFish';
|
||||||
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
|
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
|
||||||
import {
|
import {
|
||||||
isBarkBattleGalleryEntry,
|
isBarkBattleGalleryEntry,
|
||||||
@@ -10,6 +23,13 @@ import {
|
|||||||
isSquareHoleGalleryEntry,
|
isSquareHoleGalleryEntry,
|
||||||
isVisualNovelGalleryEntry,
|
isVisualNovelGalleryEntry,
|
||||||
isWoodenFishGalleryEntry,
|
isWoodenFishGalleryEntry,
|
||||||
|
mapBarkBattleWorkToPlatformGalleryCard,
|
||||||
|
mapBigFishWorkToPlatformGalleryCard,
|
||||||
|
mapJumpHopWorkToPlatformGalleryCard,
|
||||||
|
mapPuzzleWorkToPlatformGalleryCard,
|
||||||
|
mapSquareHoleWorkToPlatformGalleryCard,
|
||||||
|
mapVisualNovelWorkToPlatformGalleryCard,
|
||||||
|
mapWoodenFishWorkToPlatformGalleryCard,
|
||||||
type PlatformPublicGalleryCard,
|
type PlatformPublicGalleryCard,
|
||||||
} from '../rpg-entry/rpgEntryWorldPresentation';
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
import {
|
import {
|
||||||
@@ -94,6 +114,202 @@ export function isRpgPublicWorkDetailEntry(
|
|||||||
return !('sourceType' in entry);
|
return !('sourceType' in entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapRpgGalleryCardToPublicWorkDetail(
|
||||||
|
entry: CustomWorldGalleryCard,
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapPuzzleWorkToPublicWorkDetail(
|
||||||
|
item: PuzzleWorkSummary,
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
return mapPuzzleWorkToPlatformGalleryCard(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapSquareHoleWorkToPublicWorkDetail(
|
||||||
|
item: SquareHoleWorkSummary,
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
return mapSquareHoleWorkToPlatformGalleryCard(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapBigFishWorkToPublicWorkDetail(
|
||||||
|
item: BigFishWorkSummary,
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
return mapBigFishWorkToPlatformGalleryCard(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapVisualNovelWorkToPublicWorkDetail(
|
||||||
|
item: VisualNovelWorkSummary,
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
return mapVisualNovelWorkToPlatformGalleryCard(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapJumpHopWorkToPublicWorkDetail(
|
||||||
|
item: JumpHopGalleryCardResponse | JumpHopWorkProfileResponse,
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
return mapJumpHopWorkToPlatformGalleryCard(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapBarkBattleWorkToPublicWorkDetail(
|
||||||
|
item: BarkBattleWorkSummary,
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
return mapBarkBattleWorkToPlatformGalleryCard(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapWoodenFishWorkToPublicWorkDetail(
|
||||||
|
item: WoodenFishGalleryCardResponse | WoodenFishWorkProfileResponse,
|
||||||
|
): PlatformPublicGalleryCard {
|
||||||
|
return mapWoodenFishWorkToPlatformGalleryCard(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapBarkBattlePublicDetailToWorkSummary(
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
): BarkBattleWorkSummary | null {
|
||||||
|
if (!isBarkBattleGalleryEntry(entry)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workId: entry.workId,
|
||||||
|
draftId: entry.sourceSessionId ?? null,
|
||||||
|
ownerUserId: entry.ownerUserId,
|
||||||
|
authorDisplayName: entry.authorDisplayName,
|
||||||
|
title: entry.worldName,
|
||||||
|
summary: entry.summaryText,
|
||||||
|
themeDescription: entry.themeTags[0] ?? entry.summaryText,
|
||||||
|
playerImageDescription: entry.themeTags[1] ?? entry.summaryText,
|
||||||
|
opponentImageDescription: entry.themeTags[2] ?? entry.summaryText,
|
||||||
|
onomatopoeia: undefined,
|
||||||
|
playerCharacterImageSrc: entry.coverCharacterImageSrcs[0] ?? null,
|
||||||
|
opponentCharacterImageSrc: entry.coverCharacterImageSrcs[1] ?? null,
|
||||||
|
uiBackgroundImageSrc: entry.coverImageSrc,
|
||||||
|
difficultyPreset: 'normal',
|
||||||
|
status: 'published',
|
||||||
|
generationStatus: 'ready',
|
||||||
|
publishReady: true,
|
||||||
|
playCount: entry.playCount ?? 0,
|
||||||
|
recentPlayCount7d: entry.recentPlayCount7d ?? 0,
|
||||||
|
updatedAt: entry.updatedAt,
|
||||||
|
publishedAt: entry.publishedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapPublicWorkDetailToPuzzleWork(
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
): PuzzleWorkSummary | null {
|
||||||
|
if (!isPuzzleGalleryEntry(entry)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workId: entry.workId,
|
||||||
|
profileId: entry.profileId,
|
||||||
|
ownerUserId: entry.ownerUserId,
|
||||||
|
sourceSessionId:
|
||||||
|
'sourceSessionId' in entry && typeof entry.sourceSessionId === 'string'
|
||||||
|
? entry.sourceSessionId
|
||||||
|
: null,
|
||||||
|
authorDisplayName: entry.authorDisplayName,
|
||||||
|
levelName: entry.worldName,
|
||||||
|
summary: entry.summaryText,
|
||||||
|
themeTags: entry.themeTags,
|
||||||
|
coverImageSrc: entry.coverImageSrc,
|
||||||
|
publicationStatus: 'published',
|
||||||
|
updatedAt: entry.updatedAt,
|
||||||
|
publishedAt: entry.publishedAt,
|
||||||
|
playCount: entry.playCount ?? 0,
|
||||||
|
remixCount: entry.remixCount ?? 0,
|
||||||
|
likeCount: entry.likeCount ?? 0,
|
||||||
|
pointIncentiveTotalHalfPoints: 0,
|
||||||
|
pointIncentiveClaimedPoints: 0,
|
||||||
|
pointIncentiveTotalPoints: 0,
|
||||||
|
pointIncentiveClaimablePoints: 0,
|
||||||
|
publishReady: true,
|
||||||
|
levels:
|
||||||
|
entry.coverSlides?.map((slide, index) => ({
|
||||||
|
levelId: slide.id || `puzzle-level-${index + 1}`,
|
||||||
|
levelName: slide.label,
|
||||||
|
pictureDescription: entry.summaryText,
|
||||||
|
candidates: [],
|
||||||
|
selectedCandidateId: null,
|
||||||
|
coverImageSrc: slide.imageSrc,
|
||||||
|
coverAssetId: null,
|
||||||
|
generationStatus: 'ready' as const,
|
||||||
|
})) ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapPublicWorkDetailToBigFishWork(
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
): BigFishWorkSummary | null {
|
||||||
|
if (!isBigFishGalleryEntry(entry)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelCount = Number.parseInt(
|
||||||
|
entry.themeTags.find((tag) => /^\d+级$/u.test(tag))?.replace('级', '') ??
|
||||||
|
'0',
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
workId: entry.workId,
|
||||||
|
sourceSessionId: entry.profileId,
|
||||||
|
ownerUserId: entry.ownerUserId,
|
||||||
|
authorDisplayName: entry.authorDisplayName,
|
||||||
|
title: entry.worldName,
|
||||||
|
subtitle: entry.subtitle,
|
||||||
|
summary: entry.summaryText,
|
||||||
|
coverImageSrc: entry.coverImageSrc,
|
||||||
|
status: 'published',
|
||||||
|
updatedAt: entry.updatedAt,
|
||||||
|
publishedAt: entry.publishedAt,
|
||||||
|
publishReady: true,
|
||||||
|
levelCount: Number.isNaN(levelCount) ? 0 : levelCount,
|
||||||
|
levelMainImageReadyCount: 0,
|
||||||
|
levelMotionReadyCount: 0,
|
||||||
|
backgroundReady: Boolean(entry.coverImageSrc),
|
||||||
|
playCount: entry.playCount ?? 0,
|
||||||
|
remixCount: entry.remixCount ?? 0,
|
||||||
|
likeCount: entry.likeCount ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapPublicWorkDetailToSquareHoleWork(
|
||||||
|
entry: PlatformPublicGalleryCard,
|
||||||
|
): SquareHoleWorkSummary | null {
|
||||||
|
if (!isSquareHoleGalleryEntry(entry)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workId: entry.workId,
|
||||||
|
profileId: entry.profileId,
|
||||||
|
ownerUserId: entry.ownerUserId,
|
||||||
|
sourceSessionId:
|
||||||
|
'sourceSessionId' in entry && typeof entry.sourceSessionId === 'string'
|
||||||
|
? entry.sourceSessionId
|
||||||
|
: null,
|
||||||
|
gameName: entry.worldName,
|
||||||
|
themeText: entry.themeTags[0] ?? '方洞挑战',
|
||||||
|
twistRule: entry.subtitle,
|
||||||
|
summary: entry.summaryText,
|
||||||
|
tags: entry.themeTags,
|
||||||
|
coverImageSrc: entry.coverImageSrc,
|
||||||
|
backgroundPrompt: entry.backgroundPrompt ?? '方洞挑战运行背景',
|
||||||
|
backgroundImageSrc: entry.backgroundImageSrc ?? null,
|
||||||
|
shapeOptions: entry.shapeOptions ?? [],
|
||||||
|
holeOptions: entry.holeOptions ?? [],
|
||||||
|
shapeCount: entry.shapeCount ?? 8,
|
||||||
|
difficulty: entry.difficulty ?? 4,
|
||||||
|
publicationStatus: 'published',
|
||||||
|
playCount: entry.playCount ?? 0,
|
||||||
|
updatedAt: entry.updatedAt,
|
||||||
|
publishedAt: entry.publishedAt,
|
||||||
|
publishReady: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getPlatformPublicWorkDetailKind(
|
export function getPlatformPublicWorkDetailKind(
|
||||||
entry: PlatformPublicGalleryCard,
|
entry: PlatformPublicGalleryCard,
|
||||||
): PlatformPublicWorkDetailKind {
|
): PlatformPublicWorkDetailKind {
|
||||||
|
|||||||
Reference in New Issue
Block a user