Split custom world generation into staged lightweight batches
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { buildCustomCampSceneName } from '../services/customWorldPresentation';
|
||||
import { resolveCustomWorldAnchorWorldType } from '../services/customWorldTheme';
|
||||
import { CustomWorldProfile, Encounter, SceneNpc, WorldType } from '../types';
|
||||
import {
|
||||
CustomWorldProfile,
|
||||
Encounter,
|
||||
SceneConnectionInfo,
|
||||
SceneNpc,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import { buildRoleAttributeProfileFromLegacyData } from './attributeProfileGenerator';
|
||||
import { resolveAttributeSchema } from './attributeResolver';
|
||||
import {
|
||||
@@ -24,6 +30,7 @@ export interface ScenePreset {
|
||||
worldType: WorldType;
|
||||
forwardSceneId?: string;
|
||||
connectedSceneIds: string[];
|
||||
connections: SceneConnectionInfo[];
|
||||
monsterIds: string[];
|
||||
npcs: SceneNpc[];
|
||||
treasureHints: string[];
|
||||
@@ -121,6 +128,83 @@ function collectAllImagePool() {
|
||||
return refs;
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]) {
|
||||
return [...new Set(values.filter(Boolean))];
|
||||
}
|
||||
|
||||
function buildDefaultSceneConnections(
|
||||
connectedSceneIds: string[],
|
||||
forwardSceneId?: string,
|
||||
): SceneConnectionInfo[] {
|
||||
const uniqueSceneIds = uniqueStrings(connectedSceneIds);
|
||||
const branchPositions: Array<SceneConnectionInfo['relativePosition']> = [
|
||||
'left',
|
||||
'right',
|
||||
'back',
|
||||
'portal',
|
||||
];
|
||||
const resolvedForwardSceneId =
|
||||
forwardSceneId && uniqueSceneIds.includes(forwardSceneId)
|
||||
? forwardSceneId
|
||||
: uniqueSceneIds[0];
|
||||
const branchSceneIds = uniqueSceneIds.filter(
|
||||
(sceneId) => sceneId !== resolvedForwardSceneId,
|
||||
);
|
||||
const connections: SceneConnectionInfo[] = [];
|
||||
|
||||
if (resolvedForwardSceneId) {
|
||||
connections.push({
|
||||
sceneId: resolvedForwardSceneId,
|
||||
relativePosition: 'forward',
|
||||
summary: '沿主路继续深入前方区域',
|
||||
});
|
||||
}
|
||||
|
||||
branchSceneIds.forEach((sceneId, index) => {
|
||||
connections.push({
|
||||
sceneId,
|
||||
relativePosition: branchPositions[index] ?? 'portal',
|
||||
summary:
|
||||
index === 0
|
||||
? '这里分出一条支路'
|
||||
: index === 1
|
||||
? '这里还能转向另一条路'
|
||||
: '这里还有额外通路',
|
||||
});
|
||||
});
|
||||
|
||||
return connections;
|
||||
}
|
||||
|
||||
function pickForwardSceneIdFromConnections(connections: SceneConnectionInfo[]) {
|
||||
const preferredOrder: Array<SceneConnectionInfo['relativePosition']> = [
|
||||
'forward',
|
||||
'north',
|
||||
'east',
|
||||
'right',
|
||||
'up',
|
||||
'outside',
|
||||
'portal',
|
||||
'left',
|
||||
'west',
|
||||
'south',
|
||||
'down',
|
||||
'inside',
|
||||
'back',
|
||||
];
|
||||
|
||||
for (const relativePosition of preferredOrder) {
|
||||
const matchedConnection = connections.find(
|
||||
(connection) => connection.relativePosition === relativePosition,
|
||||
);
|
||||
if (matchedConnection?.sceneId) {
|
||||
return matchedConnection.sceneId;
|
||||
}
|
||||
}
|
||||
|
||||
return connections[0]?.sceneId;
|
||||
}
|
||||
|
||||
function hashText(value: string) {
|
||||
let hash = 0;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
@@ -225,7 +309,23 @@ function buildCustomSceneNpc(
|
||||
name: npc.name,
|
||||
role: npc.role,
|
||||
avatar: npc.name.slice(0, 1) || '?',
|
||||
description: `${npc.description} 动机:${npc.motivation}`,
|
||||
description: [
|
||||
npc.description,
|
||||
npc.backstoryReveal.publicSummary
|
||||
? `公开背景:${npc.backstoryReveal.publicSummary}`
|
||||
: '',
|
||||
npc.motivation ? `动机:${npc.motivation}` : '',
|
||||
npc.skills.length > 0
|
||||
? `技能:${npc.skills.map((skill) => skill.name).join('、')}`
|
||||
: '',
|
||||
npc.initialItems.length > 0
|
||||
? `随身物:${npc.initialItems
|
||||
.map((item) => `${item.name}x${item.quantity}`)
|
||||
.join('、')}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
gender: inferCustomNpcGender(npc.id, npc.name),
|
||||
monsterPresetId: monsterPreset?.id,
|
||||
hostileNpcPresetId: monsterPreset?.id,
|
||||
@@ -254,6 +354,18 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
|
||||
const playableCharacters = buildCustomWorldPlayableCharacters(profile);
|
||||
const campSceneId = buildCustomSceneId('camp');
|
||||
const landmarkSceneIds = profile.landmarks.map((_, index) => buildCustomSceneId('landmark', index));
|
||||
const landmarkSceneIdByLandmarkId = new Map(
|
||||
profile.landmarks.map((landmark, index) => [
|
||||
landmark.id,
|
||||
buildCustomSceneId('landmark', index),
|
||||
]),
|
||||
);
|
||||
const landmarkById = new Map(
|
||||
profile.landmarks.map((landmark) => [landmark.id, landmark]),
|
||||
);
|
||||
const customStoryNpcById = new Map(
|
||||
profile.storyNpcs.map((npc) => [npc.id, npc]),
|
||||
);
|
||||
const campNpcs = playableCharacters.slice(1).map(character => {
|
||||
const npc = buildCharacterNpc(character.id, WorldType.CUSTOM, profile);
|
||||
return npc
|
||||
@@ -265,10 +377,15 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
|
||||
: null;
|
||||
}).filter(Boolean) as SceneNpc[];
|
||||
|
||||
const customStoryNpcs = profile.storyNpcs.map(npc =>
|
||||
buildCustomSceneNpc(npc, profile, anchorWorldType),
|
||||
);
|
||||
const chunkSize = Math.max(4, Math.ceil(customStoryNpcs.length / Math.max(1, profile.landmarks.length)));
|
||||
const campConnections = profile.landmarks
|
||||
.slice(0, 3)
|
||||
.map((landmark, index) => ({
|
||||
sceneId: landmarkSceneIds[index] ?? '',
|
||||
relativePosition:
|
||||
index === 0 ? 'forward' : index === 1 ? 'left' : 'right',
|
||||
summary: `从营地可直接通往${landmark.name}`,
|
||||
}))
|
||||
.filter((connection) => connection.sceneId) as SceneConnectionInfo[];
|
||||
const customScenes: ScenePreset[] = [
|
||||
{
|
||||
id: campSceneId,
|
||||
@@ -276,8 +393,9 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
|
||||
description: `你在${profile.name}的临时营地整备行装。${profile.summary}`,
|
||||
worldType: WorldType.CUSTOM,
|
||||
imageSrc: profile.landmarks[0]?.imageSrc ?? allImages[imageOffset] ?? '',
|
||||
connectedSceneIds: landmarkSceneIds.slice(0, 3),
|
||||
forwardSceneId: landmarkSceneIds[0],
|
||||
connectedSceneIds: campConnections.map((connection) => connection.sceneId),
|
||||
connections: campConnections,
|
||||
forwardSceneId: pickForwardSceneIdFromConnections(campConnections),
|
||||
monsterIds: [],
|
||||
treasureHints: [
|
||||
`${profile.name}地图残页`,
|
||||
@@ -286,14 +404,57 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
|
||||
npcs: campNpcs,
|
||||
},
|
||||
...profile.landmarks.map((landmark, index): ScenePreset => {
|
||||
const sceneNpcs = customStoryNpcs.slice(index * chunkSize, (index + 1) * chunkSize);
|
||||
const connectedSceneIds: string[] = [
|
||||
campSceneId,
|
||||
landmarkSceneIds[(index - 1 + landmarkSceneIds.length) % landmarkSceneIds.length],
|
||||
landmarkSceneIds[(index + 1) % landmarkSceneIds.length],
|
||||
]
|
||||
.filter((sceneId): sceneId is string => Boolean(sceneId))
|
||||
.filter((sceneId, sceneIndex, array) => array.indexOf(sceneId) === sceneIndex);
|
||||
const sceneNpcs = landmark.sceneNpcIds
|
||||
.map((npcId) => customStoryNpcById.get(npcId))
|
||||
.filter(Boolean)
|
||||
.map((npc) =>
|
||||
buildCustomSceneNpc(npc!, profile, anchorWorldType),
|
||||
);
|
||||
if (sceneNpcs.length < 3) {
|
||||
profile.storyNpcs
|
||||
.filter(
|
||||
(npc) => !sceneNpcs.some((sceneNpc) => sceneNpc.id === npc.id),
|
||||
)
|
||||
.slice(0, 3 - sceneNpcs.length)
|
||||
.forEach((npc) =>
|
||||
sceneNpcs.push(buildCustomSceneNpc(npc, profile, anchorWorldType)),
|
||||
);
|
||||
}
|
||||
const landmarkConnections = landmark.connections
|
||||
.map((connection) => {
|
||||
const targetSceneId = landmarkSceneIdByLandmarkId.get(
|
||||
connection.targetLandmarkId,
|
||||
);
|
||||
const targetLandmark = landmarkById.get(connection.targetLandmarkId);
|
||||
if (!targetSceneId || !targetLandmark) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
sceneId: targetSceneId,
|
||||
relativePosition: connection.relativePosition,
|
||||
summary:
|
||||
connection.summary || `可通往${targetLandmark.name}`,
|
||||
} satisfies SceneConnectionInfo;
|
||||
})
|
||||
.filter((connection): connection is SceneConnectionInfo =>
|
||||
Boolean(connection),
|
||||
);
|
||||
const shouldLinkCamp = index < 3;
|
||||
const extraCampConnection = shouldLinkCamp
|
||||
? ({
|
||||
sceneId: campSceneId,
|
||||
relativePosition: 'back',
|
||||
summary: '可回到临时营地整备',
|
||||
} satisfies SceneConnectionInfo)
|
||||
: null;
|
||||
const connections = [
|
||||
...landmarkConnections,
|
||||
...(extraCampConnection ? [extraCampConnection] : []),
|
||||
];
|
||||
const connectedSceneIds = uniqueStrings(
|
||||
connections.map((connection) => connection.sceneId),
|
||||
);
|
||||
const monsterSliceStart = (index * 2) % Math.max(1, fallbackMonsterIds.length || 1);
|
||||
const monsterIds: string[] = fallbackMonsterIds.slice(monsterSliceStart, monsterSliceStart + 2);
|
||||
const hostileNpcs = monsterIds
|
||||
@@ -307,7 +468,8 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
|
||||
worldType: WorldType.CUSTOM,
|
||||
imageSrc: landmark.imageSrc ?? allImages[(imageOffset + index + 1) % Math.max(1, allImages.length)] ?? '',
|
||||
connectedSceneIds,
|
||||
forwardSceneId: connectedSceneIds.find(sceneId => sceneId !== campSceneId) ?? campSceneId,
|
||||
connections,
|
||||
forwardSceneId: pickForwardSceneIdFromConnections(connections),
|
||||
monsterIds,
|
||||
treasureHints: [
|
||||
`${landmark.name}的旧线索`,
|
||||
@@ -745,6 +907,10 @@ function buildScenePoolFromTemplates(templates: SceneTemplate[]): ScenePreset[]
|
||||
...template,
|
||||
...sceneOverride,
|
||||
imageSrc: sceneOverride.imageSrc ?? imagePool[index] ?? imagePool[0] ?? '',
|
||||
connections: buildDefaultSceneConnections(
|
||||
sceneOverride.connectedSceneIds ?? template.connectedSceneIds,
|
||||
sceneOverride.forwardSceneId ?? template.forwardSceneId,
|
||||
),
|
||||
npcs: mergeNpcs(characterNpcs, [...hostileNpcs, ...template.extraNpcs], template.worldType),
|
||||
} satisfies ScenePreset;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user