1
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import type { CustomWorldRoleAssetSummary } from '../../../packages/shared/src/contracts/customWorldAgent.js';
|
||||
import type {
|
||||
CustomWorldRoleAssetSummary,
|
||||
CustomWorldSceneAssetSummary,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
|
||||
import {
|
||||
getRoleAssetSummaryById,
|
||||
rebuildRoleAssetCoverage,
|
||||
mergeRoleAssetIntoDraftProfile,
|
||||
} from './customWorldAgentRoleAssetStateService.js';
|
||||
|
||||
@@ -31,6 +35,17 @@ type SyncRoleAssetsPayload = {
|
||||
animationMap?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
type SceneKind = 'camp' | 'landmark';
|
||||
|
||||
type SyncSceneAssetsPayload = {
|
||||
sceneId: string;
|
||||
sceneKind: SceneKind;
|
||||
imageSrc: string;
|
||||
generatedSceneAssetId: string;
|
||||
generatedScenePrompt?: string | null;
|
||||
generatedSceneModel?: string | null;
|
||||
};
|
||||
|
||||
export type SyncRoleAssetsResult = {
|
||||
roleId: string;
|
||||
updatedRole: Record<string, unknown>;
|
||||
@@ -38,6 +53,97 @@ export type SyncRoleAssetsResult = {
|
||||
draftProfile: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SceneAssetStudioContext = {
|
||||
sceneId: string;
|
||||
sceneKind: SceneKind;
|
||||
sceneName: string;
|
||||
sceneDescription: string;
|
||||
imageSrc: string | null;
|
||||
readyActCount: number;
|
||||
missingActCount: number;
|
||||
};
|
||||
|
||||
export type SyncSceneAssetsResult = {
|
||||
sceneId: string;
|
||||
sceneKind: SceneKind;
|
||||
updatedScene: Record<string, unknown>;
|
||||
updatedAssetSummaries: CustomWorldSceneAssetSummary[];
|
||||
draftProfile: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function cloneRecord<T extends Record<string, unknown>>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function toSceneDescription(scene: Record<string, unknown>, sceneKind: SceneKind) {
|
||||
if (sceneKind === 'camp') {
|
||||
return (
|
||||
toText(scene.description) ||
|
||||
toText(scene.summary) ||
|
||||
toText(scene.mood)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
toText(scene.description) ||
|
||||
toText(scene.summary) ||
|
||||
toText(scene.purpose) ||
|
||||
toText(scene.mood)
|
||||
);
|
||||
}
|
||||
|
||||
function findSceneActsBySceneId(
|
||||
draftProfile: Record<string, unknown>,
|
||||
sceneId: string,
|
||||
) {
|
||||
return toRecordArray(draftProfile.sceneChapters)
|
||||
.filter((chapter) => toText(chapter.sceneId) === sceneId)
|
||||
.flatMap((chapter) => toRecordArray(chapter.acts));
|
||||
}
|
||||
|
||||
function updateSceneChapterActsForScene(params: {
|
||||
draftProfile: Record<string, unknown>;
|
||||
sceneId: string;
|
||||
imageSrc: string;
|
||||
generatedSceneAssetId: string;
|
||||
}) {
|
||||
return toRecordArray(params.draftProfile.sceneChapters).map((chapter) => {
|
||||
if (toText(chapter.sceneId) !== params.sceneId) {
|
||||
return chapter;
|
||||
}
|
||||
|
||||
return {
|
||||
...chapter,
|
||||
acts: toRecordArray(chapter.acts).map((act) => ({
|
||||
...act,
|
||||
backgroundImageSrc: params.imageSrc,
|
||||
backgroundAssetId: params.generatedSceneAssetId,
|
||||
})),
|
||||
} satisfies Record<string, unknown>;
|
||||
});
|
||||
}
|
||||
|
||||
function buildSceneAssetFallbackSummary(params: {
|
||||
sceneId: string;
|
||||
sceneKind: SceneKind;
|
||||
updatedScene: Record<string, unknown>;
|
||||
imageSrc: string;
|
||||
generatedSceneAssetId: string;
|
||||
}) {
|
||||
return {
|
||||
sceneId: params.sceneId,
|
||||
sceneName:
|
||||
toText(params.updatedScene.name) ||
|
||||
(params.sceneKind === 'camp' ? '开局营地' : '未命名场景'),
|
||||
actId: null,
|
||||
actTitle: params.sceneKind === 'camp' ? '营地正式背景图' : '场景正式背景图',
|
||||
imageSrc: params.imageSrc,
|
||||
assetId: params.generatedSceneAssetId,
|
||||
status: 'ready',
|
||||
nextPointCost: 0,
|
||||
} satisfies CustomWorldSceneAssetSummary;
|
||||
}
|
||||
|
||||
export class CustomWorldAgentAssetBridgeService {
|
||||
buildRoleAssetStudioContext(snapshot: unknown, roleId: string) {
|
||||
const profile = toRecord(snapshot);
|
||||
@@ -96,4 +202,123 @@ export class CustomWorldAgentAssetBridgeService {
|
||||
draftProfile,
|
||||
};
|
||||
}
|
||||
|
||||
buildSceneAssetStudioContext(
|
||||
snapshot: unknown,
|
||||
sceneId: string,
|
||||
sceneKind: SceneKind,
|
||||
): SceneAssetStudioContext {
|
||||
const profile = toRecord(snapshot);
|
||||
if (!profile) {
|
||||
throw new Error('当前世界草稿为空,无法打开场景资产工坊。');
|
||||
}
|
||||
|
||||
const scene =
|
||||
sceneKind === 'camp'
|
||||
? toRecord(profile.camp)
|
||||
: toRecordArray(profile.landmarks).find(
|
||||
(item) => toText(item.id) === sceneId,
|
||||
) ?? null;
|
||||
if (!scene) {
|
||||
throw new Error('未找到目标场景,无法进入场景资产工坊。');
|
||||
}
|
||||
|
||||
const sceneActs = findSceneActsBySceneId(profile, sceneId);
|
||||
const readyActCount = sceneActs.filter((act) =>
|
||||
Boolean(toText(act.backgroundImageSrc) || toText(act.backgroundAssetId)),
|
||||
).length;
|
||||
|
||||
return {
|
||||
sceneId,
|
||||
sceneKind,
|
||||
sceneName:
|
||||
toText(scene.name) || (sceneKind === 'camp' ? '开局营地' : '未命名场景'),
|
||||
sceneDescription: toSceneDescription(scene, sceneKind),
|
||||
imageSrc: toText(scene.imageSrc) || null,
|
||||
readyActCount,
|
||||
missingActCount: Math.max(0, sceneActs.length - readyActCount),
|
||||
};
|
||||
}
|
||||
|
||||
applySceneAssetPublishResult(
|
||||
snapshot: unknown,
|
||||
payload: SyncSceneAssetsPayload,
|
||||
): SyncSceneAssetsResult {
|
||||
const profile = toRecord(snapshot);
|
||||
if (!profile) {
|
||||
throw new Error('当前世界草稿为空,无法同步场景资产。');
|
||||
}
|
||||
|
||||
const nextDraftProfile = cloneRecord(profile);
|
||||
let updatedScene: Record<string, unknown> | null = null;
|
||||
|
||||
if (payload.sceneKind === 'camp') {
|
||||
const currentCamp = toRecord(nextDraftProfile.camp);
|
||||
if (!currentCamp || toText(currentCamp.id) !== payload.sceneId) {
|
||||
throw new Error('目标营地不存在,无法同步场景资产。');
|
||||
}
|
||||
|
||||
updatedScene = {
|
||||
...currentCamp,
|
||||
imageSrc: payload.imageSrc,
|
||||
generatedSceneAssetId: payload.generatedSceneAssetId,
|
||||
generatedScenePrompt: payload.generatedScenePrompt ?? null,
|
||||
generatedSceneModel: payload.generatedSceneModel ?? null,
|
||||
};
|
||||
nextDraftProfile.camp = updatedScene;
|
||||
} else {
|
||||
let touched = false;
|
||||
nextDraftProfile.landmarks = toRecordArray(nextDraftProfile.landmarks).map(
|
||||
(item) => {
|
||||
if (toText(item.id) !== payload.sceneId) {
|
||||
return item;
|
||||
}
|
||||
|
||||
touched = true;
|
||||
updatedScene = {
|
||||
...item,
|
||||
imageSrc: payload.imageSrc,
|
||||
generatedSceneAssetId: payload.generatedSceneAssetId,
|
||||
generatedScenePrompt: payload.generatedScenePrompt ?? null,
|
||||
generatedSceneModel: payload.generatedSceneModel ?? null,
|
||||
};
|
||||
return updatedScene;
|
||||
},
|
||||
);
|
||||
|
||||
if (!touched || !updatedScene) {
|
||||
throw new Error('目标地点不存在,无法同步场景资产。');
|
||||
}
|
||||
}
|
||||
|
||||
nextDraftProfile.sceneChapters = updateSceneChapterActsForScene({
|
||||
draftProfile: nextDraftProfile,
|
||||
sceneId: payload.sceneId,
|
||||
imageSrc: payload.imageSrc,
|
||||
generatedSceneAssetId: payload.generatedSceneAssetId,
|
||||
});
|
||||
|
||||
const updatedAssetSummaries = rebuildRoleAssetCoverage(
|
||||
nextDraftProfile,
|
||||
).sceneAssets.filter((entry) => entry.sceneId === payload.sceneId);
|
||||
|
||||
return {
|
||||
sceneId: payload.sceneId,
|
||||
sceneKind: payload.sceneKind,
|
||||
updatedScene: updatedScene ?? {},
|
||||
updatedAssetSummaries:
|
||||
updatedAssetSummaries.length > 0
|
||||
? updatedAssetSummaries
|
||||
: [
|
||||
buildSceneAssetFallbackSummary({
|
||||
sceneId: payload.sceneId,
|
||||
sceneKind: payload.sceneKind,
|
||||
updatedScene: updatedScene ?? {},
|
||||
imageSrc: payload.imageSrc,
|
||||
generatedSceneAssetId: payload.generatedSceneAssetId,
|
||||
}),
|
||||
],
|
||||
draftProfile: nextDraftProfile,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user