Refactor server-rs runtime and update related docs
This commit is contained in:
@@ -784,7 +784,6 @@ function buildOpeningSceneSearchText(
|
||||
return [
|
||||
campScene.name,
|
||||
campScene.description,
|
||||
campScene.dangerLevel,
|
||||
profile.playerGoal,
|
||||
profile.summary,
|
||||
'开局场景',
|
||||
@@ -920,7 +919,6 @@ function buildLandmarkSearchText(
|
||||
return [
|
||||
landmark.name,
|
||||
landmark.description,
|
||||
landmark.dangerLevel,
|
||||
...landmark.sceneNpcIds.map((npcId) => storyNpcById.get(npcId)?.name ?? ''),
|
||||
...landmark.connections.flatMap((connection) => [
|
||||
landmarkById.get(connection.targetLandmarkId)?.name ?? '',
|
||||
|
||||
@@ -190,7 +190,6 @@ function createProfile(): CustomWorldProfile {
|
||||
camp: {
|
||||
name: '潮灯居',
|
||||
description: '玩家最初落脚的旧灯塔内院。',
|
||||
dangerLevel: 'medium',
|
||||
},
|
||||
landmarks: [],
|
||||
creatorIntent: null,
|
||||
@@ -215,7 +214,6 @@ function createProfileWithLandmark(): CustomWorldProfile {
|
||||
id: 'landmark-1',
|
||||
name: '沉钟栈桥',
|
||||
description: '旧钟与潮声常年相撞的码头栈桥。',
|
||||
dangerLevel: 'medium',
|
||||
imageSrc: '/generated-custom-world-scenes/original-scene.png',
|
||||
sceneNpcIds: ['story-1', 'story-2', 'story-3'],
|
||||
connections: [],
|
||||
@@ -277,7 +275,6 @@ function CampEditorFlowHarness() {
|
||||
id: 'custom-scene-camp',
|
||||
name: '潮灯居',
|
||||
description: '玩家最初落脚的旧灯塔内院。',
|
||||
dangerLevel: 'medium',
|
||||
imageSrc: '/generated-custom-world-scenes/original-camp.png',
|
||||
sceneNpcIds: ['story-1', 'story-2', 'story-3'],
|
||||
connections: [
|
||||
|
||||
@@ -182,7 +182,6 @@ const baseProfile = {
|
||||
camp: {
|
||||
name: '潮灯居',
|
||||
description: '玩家最初落脚的旧灯塔内院。',
|
||||
dangerLevel: 'medium',
|
||||
},
|
||||
anchorContent: {
|
||||
worldPromise: {
|
||||
@@ -233,7 +232,6 @@ const baseProfile = {
|
||||
id: 'landmark-1',
|
||||
name: '沉钟栈桥',
|
||||
description: '旧钟与潮声常年相撞的码头栈桥。',
|
||||
dangerLevel: 'medium',
|
||||
sceneNpcIds: ['story-1'],
|
||||
connections: [],
|
||||
},
|
||||
|
||||
@@ -352,9 +352,7 @@ export function GameCanvasEntityLayer({
|
||||
const isCampCompanionEncounter =
|
||||
encounter.specialBehavior === 'initial_companion'
|
||||
|| encounter.specialBehavior === 'camp_companion';
|
||||
const peacefulAnchorX = isCampCompanionEncounter
|
||||
? RESOLVED_ENTITY_X_METERS
|
||||
: encounter.xMeters ?? monsterAnchorMeters;
|
||||
const peacefulAnchorX = RESOLVED_ENTITY_X_METERS;
|
||||
const isPeacefulEncounterMoving =
|
||||
(!isCampCompanionEncounter && sceneTransitionPhase !== 'idle')
|
||||
|| Math.abs(peacefulAnchorX - RESOLVED_ENTITY_X_METERS) > 0.01;
|
||||
@@ -373,10 +371,7 @@ export function GameCanvasEntityLayer({
|
||||
const peacefulBottomOffsetPx = peacefulResolvedCharacter
|
||||
? getCharacterBottomOffsetPx(stageLiftPx, peacefulResolvedCharacter)
|
||||
: stageLiftPx + peacefulHostileBottomOffsetPx;
|
||||
const peacefulNpcSpriteFacing =
|
||||
encounter.kind === 'treasure' || peacefulResolvedCharacter
|
||||
? towardPeacefulPlayer
|
||||
: getRenderableNpcFacing(encounter, towardPeacefulPlayer, {medievalVisual: true});
|
||||
const peacefulNpcSpriteFacing = towardPeacefulPlayer;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -241,7 +241,6 @@ export function createLandmarkDraft(
|
||||
),
|
||||
name: `自定义场景${profile.landmarks.length + 1}`,
|
||||
description: '',
|
||||
dangerLevel: '中',
|
||||
imageSrc: undefined,
|
||||
sceneNpcIds: profile.storyNpcs.slice(0, 3).map((npc) => npc.id),
|
||||
connections: previousLandmark
|
||||
|
||||
@@ -434,7 +434,6 @@ const compiledAgentDraftSession: CustomWorldAgentSessionSnapshot = {
|
||||
id: 'landmark-1',
|
||||
name: '回潮旧灯塔',
|
||||
description: '旧灯塔是整片群岛最先看见异动的地方。',
|
||||
dangerLevel: 'high',
|
||||
sceneNpcIds: ['story-1'],
|
||||
connections: [],
|
||||
},
|
||||
|
||||
@@ -185,7 +185,6 @@ describe('characterPresets custom world runtime characters', () => {
|
||||
{
|
||||
name: '夜港旧栈',
|
||||
description: '潮雾和旧木桥把视线切成断续几段。',
|
||||
dangerLevel: 'medium',
|
||||
sceneNpcNames: ['沈雾', '陆沉', '顾潮'],
|
||||
connections: [
|
||||
{
|
||||
@@ -198,7 +197,6 @@ describe('characterPresets custom world runtime characters', () => {
|
||||
{
|
||||
name: '断桥外沿',
|
||||
description: '旧桥断口还挂着潮湿残旗。',
|
||||
dangerLevel: 'high',
|
||||
sceneNpcNames: ['沈雾', '陆沉', '顾潮'],
|
||||
connections: [
|
||||
{
|
||||
|
||||
@@ -858,7 +858,6 @@ function normalizeLandmark(
|
||||
id: toText(value.id, `saved-landmark-${index + 1}`),
|
||||
name,
|
||||
description: toText(value.description),
|
||||
dangerLevel: toText(value.dangerLevel),
|
||||
imageSrc: toText(value.imageSrc) || undefined,
|
||||
narrativeResidues:
|
||||
preserveStructuredRecordArray<SceneNarrativeResidue>(
|
||||
@@ -891,7 +890,6 @@ function normalizeCampScene(
|
||||
name: toText(value.name, fallback.name),
|
||||
description: toText(value.description, fallback.description),
|
||||
visualDescription: toText(value.visualDescription) || undefined,
|
||||
dangerLevel: toText(value.dangerLevel, fallback.dangerLevel),
|
||||
imageSrc: toText(value.imageSrc) || undefined,
|
||||
sceneNpcIds: toStringArray(value.sceneNpcIds),
|
||||
connections: toRecordArray(value.connections)
|
||||
@@ -978,6 +976,8 @@ function normalizeSceneActBlueprint(
|
||||
const advanceRule = toText(value.advanceRule);
|
||||
const title = toText(value.title);
|
||||
const summary = toText(value.summary);
|
||||
const primaryNpcId = toText(value.primaryNpcId, encounterNpcIds[0] ?? '');
|
||||
const oppositeNpcId = toText(value.oppositeNpcId, primaryNpcId);
|
||||
|
||||
if (!title && !summary && encounterNpcIds.length === 0) {
|
||||
return null;
|
||||
@@ -997,7 +997,14 @@ function normalizeSceneActBlueprint(
|
||||
backgroundImageSrc: toText(value.backgroundImageSrc) || undefined,
|
||||
backgroundAssetId: toText(value.backgroundAssetId) || undefined,
|
||||
encounterNpcIds,
|
||||
primaryNpcId: toText(value.primaryNpcId, encounterNpcIds[0] ?? ''),
|
||||
primaryNpcId,
|
||||
oppositeNpcId,
|
||||
eventDescription: toText(
|
||||
value.eventDescription,
|
||||
oppositeNpcId
|
||||
? `第 ${index + 1} 幕中,玩家与${oppositeNpcId}正面接触,推动当前场景事件升级。`
|
||||
: `第 ${index + 1} 幕中,玩家处理当前场景的关键事件。`,
|
||||
),
|
||||
linkedThreadIds: toStringArray(value.linkedThreadIds),
|
||||
advanceRule: SCENE_ACT_ADVANCE_RULES.has(advanceRule as never)
|
||||
? (advanceRule as SceneActBlueprint['advanceRule'])
|
||||
@@ -1033,6 +1040,10 @@ function normalizeSceneChapterBlueprints(value: unknown) {
|
||||
sceneId,
|
||||
title: toText(entry.title, toText(entry.sceneName, sceneId)),
|
||||
summary: toText(entry.summary),
|
||||
sceneTaskDescription: toText(
|
||||
entry.sceneTaskDescription,
|
||||
`首次进入${toText(entry.title, toText(entry.sceneName, sceneId))}时,确认当前场景核心任务与关键角色。`,
|
||||
),
|
||||
linkedThreadIds: toStringArray(entry.linkedThreadIds),
|
||||
linkedLandmarkIds: toStringArray(entry.linkedLandmarkIds),
|
||||
acts,
|
||||
|
||||
@@ -384,7 +384,6 @@ export function normalizeCustomWorldLandmarks(params: {
|
||||
name: landmark.name,
|
||||
description: landmark.description,
|
||||
visualDescription: landmark.visualDescription,
|
||||
dangerLevel: landmark.dangerLevel,
|
||||
imageSrc: landmark.imageSrc,
|
||||
narrativeResidues: landmark.narrativeResidues,
|
||||
sceneNpcIds: resolveSceneNpcIdsForLandmark(
|
||||
|
||||
@@ -204,7 +204,7 @@ type CustomWorldSceneImageMatchOptions = {
|
||||
| 'camp'
|
||||
| 'ownedSettingLayers'
|
||||
> | null;
|
||||
landmark?: Pick<CustomWorldLandmark, 'id' | 'name' | 'description' | 'dangerLevel'> | null;
|
||||
landmark?: Pick<CustomWorldLandmark, 'id' | 'name' | 'description'> | null;
|
||||
usedImageSrcs?: Iterable<string>;
|
||||
};
|
||||
|
||||
@@ -328,7 +328,6 @@ function buildSourceText(
|
||||
themeHints,
|
||||
landmark?.name,
|
||||
landmark?.description,
|
||||
landmark?.dangerLevel,
|
||||
`scene-${index + 1}`,
|
||||
seedKey,
|
||||
]).join(' ');
|
||||
@@ -492,7 +491,7 @@ export function resolveCustomWorldLandmarkImage(
|
||||
| 'compatibilityTemplateWorldType'
|
||||
| 'ownedSettingLayers'
|
||||
>,
|
||||
landmark: Pick<CustomWorldLandmark, 'id' | 'name' | 'description' | 'dangerLevel' | 'imageSrc'>,
|
||||
landmark: Pick<CustomWorldLandmark, 'id' | 'name' | 'description' | 'imageSrc'>,
|
||||
index: number,
|
||||
usedImageSrcs?: Iterable<string>,
|
||||
) {
|
||||
@@ -586,7 +585,6 @@ export function resolveCustomWorldCampSceneImage(
|
||||
id: 'custom-scene-camp',
|
||||
name: campScene.name,
|
||||
description: campScene.description,
|
||||
dangerLevel: campScene.dangerLevel,
|
||||
},
|
||||
usedImageSrcs,
|
||||
},
|
||||
|
||||
@@ -70,7 +70,6 @@ describe('scene background assets', () => {
|
||||
id: 'landmark-1',
|
||||
name: '残城旧营',
|
||||
description: '断墙、军帐与营火灰烬混在一起,像一处被遗弃的边关驻地。',
|
||||
dangerLevel: 'high',
|
||||
imageSrc: generatedImage,
|
||||
sceneNpcIds: [],
|
||||
connections: [],
|
||||
@@ -79,7 +78,6 @@ describe('scene background assets', () => {
|
||||
id: 'landmark-2',
|
||||
name: '雾锁渡桥',
|
||||
description: '古桥横跨冷河,雾气压在水面上,只有残灯还在摇晃。',
|
||||
dangerLevel: 'medium',
|
||||
sceneNpcIds: [],
|
||||
connections: [],
|
||||
},
|
||||
@@ -87,7 +85,6 @@ describe('scene background assets', () => {
|
||||
id: 'landmark-3',
|
||||
name: '地宫裂隙',
|
||||
description: '墓道向下坍塌,石阶与机关残痕一路通往地底深处。',
|
||||
dangerLevel: 'extreme',
|
||||
sceneNpcIds: [],
|
||||
connections: [],
|
||||
},
|
||||
|
||||
@@ -140,7 +140,6 @@ describe('scenePresets custom world npc mapping', () => {
|
||||
{
|
||||
name: '雾潮码头',
|
||||
description: '旧船桩和潮雾把视线切成断续的几段。',
|
||||
dangerLevel: 'high',
|
||||
sceneNpcNames: ['沈雾', '陆沉', '顾潮'],
|
||||
connections: [
|
||||
{
|
||||
@@ -153,7 +152,6 @@ describe('scenePresets custom world npc mapping', () => {
|
||||
{
|
||||
name: '断桥旧道',
|
||||
description: '半塌的桥面上还挂着旧索和残旗。',
|
||||
dangerLevel: 'high',
|
||||
sceneNpcNames: ['沈雾', '陆沉', '顾潮'],
|
||||
connections: [
|
||||
{
|
||||
|
||||
@@ -1047,8 +1047,14 @@ describe('npcEncounterActions', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('opens hostile npc encounters as a declaration dialogue with only escape and fight options', () => {
|
||||
it('lets hostile npc encounters speak first on first contact', async () => {
|
||||
const encounter = createEncounter();
|
||||
streamNpcChatTurnMock.mockResolvedValueOnce({
|
||||
affinityDelta: 0,
|
||||
affinityText: '关系暂未变化',
|
||||
npcReply: '先别急着拔剑,我有话要问你。',
|
||||
suggestions: ['你想问什么'],
|
||||
});
|
||||
const actions = createNpcEncounterActions({
|
||||
gameState: createState({
|
||||
currentEncounter: encounter,
|
||||
@@ -1071,6 +1077,7 @@ describe('npcEncounterActions', () => {
|
||||
});
|
||||
|
||||
expect(actions.enterNpcInteraction(encounter, '与断桥客搭话')).toBe(true);
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(actions.setGameState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -1080,18 +1087,26 @@ describe('npcEncounterActions', () => {
|
||||
expect(actions.setCurrentStory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
displayMode: 'dialogue',
|
||||
options: [
|
||||
dialogue: [
|
||||
expect.objectContaining({
|
||||
functionId: 'battle_escape_breakout',
|
||||
actionText: '逃跑',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
functionId: 'npc_fight',
|
||||
actionText: '与他对战',
|
||||
speaker: 'npc',
|
||||
text: '先别急着拔剑,我有话要问你。',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(streamNpcChatTurnMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({id: 'npc-rival'}),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
'【NPC 主动开场】',
|
||||
expect.anything(),
|
||||
expect.objectContaining({npcInitiatesConversation: true}),
|
||||
);
|
||||
});
|
||||
|
||||
it('lets the current act primary npc enter limited chat even with negative affinity', () => {
|
||||
|
||||
@@ -1466,17 +1466,6 @@ export function createStoryNpcEncounterActions({
|
||||
nextTurnCount: 0,
|
||||
});
|
||||
|
||||
if ((npcState.affinity < 0 || encounter.hostile) && !limitedChatDirective) {
|
||||
setCurrentStory(
|
||||
buildHostileNpcStoryMoment(
|
||||
encounter,
|
||||
playerCharacter,
|
||||
npcState.affinity,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const npcInteractionOptions =
|
||||
getAvailableOptionsForState(nextState, playerCharacter) ?? [];
|
||||
const chatOptions = npcInteractionOptions.filter((option) =>
|
||||
@@ -1514,6 +1503,17 @@ export function createStoryNpcEncounterActions({
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((npcState.affinity < 0 || encounter.hostile) && !limitedChatDirective) {
|
||||
setCurrentStory(
|
||||
buildHostileNpcStoryMoment(
|
||||
encounter,
|
||||
playerCharacter,
|
||||
npcState.affinity,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return enterNpcChat(
|
||||
encounter,
|
||||
seedChatOption,
|
||||
|
||||
@@ -213,14 +213,12 @@ function buildSavedProfile() {
|
||||
camp: {
|
||||
name: '回潮暂栖所',
|
||||
description: '能暂时收拢队伍、整理灯册与潮路线索的落脚点。',
|
||||
dangerLevel: 'low',
|
||||
},
|
||||
landmarks: [
|
||||
{
|
||||
id: 'landmark-1',
|
||||
name: '回潮旧灯塔',
|
||||
description: '被海雾啃旧的石塔仍在夜里维持着微弱灯火。',
|
||||
dangerLevel: 'high',
|
||||
sceneNpcIds: ['story-1'],
|
||||
connections: [
|
||||
{
|
||||
@@ -243,7 +241,6 @@ function buildSavedProfile() {
|
||||
id: 'landmark-2',
|
||||
name: '雾栈尽头',
|
||||
description: '旧栈桥尽头只剩断索、潮板和被抹掉的船名。',
|
||||
dangerLevel: 'high',
|
||||
sceneNpcIds: [],
|
||||
connections: [
|
||||
{
|
||||
|
||||
@@ -16,7 +16,6 @@ const framework = {
|
||||
camp: {
|
||||
name: '旧灯塔营地',
|
||||
description: '潮雾里的临时归处。',
|
||||
dangerLevel: 'medium',
|
||||
},
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
|
||||
@@ -35,7 +35,6 @@ type CustomWorldGenerationLandmarkOutline = {
|
||||
name: string;
|
||||
description: string;
|
||||
visualDescription?: string;
|
||||
dangerLevel: string;
|
||||
sceneNpcNames: string[];
|
||||
connections: Array<{
|
||||
targetLandmarkName: string;
|
||||
@@ -58,7 +57,6 @@ type CustomWorldGenerationFramework = {
|
||||
camp: {
|
||||
name: string;
|
||||
description: string;
|
||||
dangerLevel: string;
|
||||
};
|
||||
playableNpcs: CustomWorldGenerationRoleOutline[];
|
||||
storyNpcs: CustomWorldGenerationRoleOutline[];
|
||||
@@ -92,7 +90,7 @@ function buildFrameworkSummaryText(
|
||||
.slice(0, maxLandmarks)
|
||||
.map(
|
||||
(landmark) =>
|
||||
`${landmark.name}(${landmark.dangerLevel},${landmark.description})`,
|
||||
`${landmark.name}(${landmark.description})`,
|
||||
)
|
||||
.join('、');
|
||||
|
||||
@@ -193,7 +191,6 @@ export function buildCustomWorldFrameworkPrompt(settingText: string) {
|
||||
' "camp": {',
|
||||
' "name": "开局归处名称",',
|
||||
' "description": "这是玩家进入世界后的第一处落脚点描述",',
|
||||
' "dangerLevel": "low|medium|high|extreme"',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
@@ -460,7 +457,7 @@ export function buildCustomWorldFrameworkJsonRepairPrompt(
|
||||
'顶层必须只包含:name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts、camp。',
|
||||
'不要输出 playableNpcs、storyNpcs、landmarks、items 或任何其他字段。',
|
||||
'majorFactions 与 coreConflicts 必须是字符串数组。',
|
||||
'camp 必须是对象,且包含:name、description、dangerLevel。',
|
||||
'camp 必须是对象,且包含:name、description。',
|
||||
'原始文本:',
|
||||
responseText.trim(),
|
||||
].join('\n');
|
||||
@@ -576,7 +573,6 @@ export function buildCustomWorldLandmarkSeedBatchPrompt(params: {
|
||||
' {',
|
||||
' "name": "场景名称",',
|
||||
' "description": "极简场景描述",',
|
||||
' "dangerLevel": "low|medium|high|extreme"',
|
||||
' }',
|
||||
' ]',
|
||||
'}',
|
||||
@@ -584,7 +580,7 @@ export function buildCustomWorldLandmarkSeedBatchPrompt(params: {
|
||||
'要求:',
|
||||
`- 必须生成恰好 ${batchCount} 个 landmarks。`,
|
||||
'- 这是一个完全独立的自定义世界;不要在场景描述里直接写“武侠世界”“仙侠世界”等现成世界名。',
|
||||
'- 这一步只保留:name、description、dangerLevel。',
|
||||
'- 这一步只保留:name、description。',
|
||||
'- 不要输出 sceneNpcNames、connections、imageSrc 或其他字段。',
|
||||
'- 名称必须具体且互不重复,不要使用 场景1、地点1 之类的占位名。',
|
||||
'- description 控制在 8 到 18 个汉字内。',
|
||||
@@ -610,7 +606,7 @@ export function buildCustomWorldLandmarkSeedBatchJsonRepairPrompt(params: {
|
||||
forbiddenNames.length > 0
|
||||
? `禁止使用这些重复场景名:${forbiddenNames.join('、')}。`
|
||||
: '',
|
||||
'每个地标只包含:name、description、dangerLevel。',
|
||||
'每个地标只包含:name、description。',
|
||||
'不要输出 sceneNpcNames、connections 或其他字段。',
|
||||
'原始文本:',
|
||||
responseText.trim(),
|
||||
@@ -643,7 +639,7 @@ export function buildCustomWorldLandmarkNetworkBatchPrompt(params: {
|
||||
landmarkBatch
|
||||
.map(
|
||||
(landmark) =>
|
||||
`- ${landmark.name} / 危险度:${landmark.dangerLevel} / 描述:${landmark.description}`,
|
||||
`- ${landmark.name} / 描述:${landmark.description}`,
|
||||
)
|
||||
.join('\n'),
|
||||
'',
|
||||
@@ -672,7 +668,7 @@ export function buildCustomWorldLandmarkNetworkBatchPrompt(params: {
|
||||
`- 每个场景必须提供恰好 2 条 connections;relativePosition 只能使用:${relativePositionValues}。`,
|
||||
'- targetLandmarkName 必须来自全部场景名,且不能连接自己,两个目标场景不能重复。',
|
||||
'- summary 控制在 4 到 10 个汉字内。',
|
||||
'- 不要输出 description、dangerLevel、backstory 或其他字段。',
|
||||
'- 不要输出 description、backstory 或其他字段。',
|
||||
'- 所有生成文本都必须使用中文。',
|
||||
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
|
||||
].join('\n');
|
||||
@@ -691,7 +687,7 @@ export function buildCustomWorldLandmarkNetworkBatchJsonRepairPrompt(params: {
|
||||
`landmarks 里只能保留这些场景名:${expectedNames.join('、')}。`,
|
||||
'每个场景对象只包含:name、sceneNpcNames、connections。',
|
||||
'connections 里的每个对象必须包含:targetLandmarkName、relativePosition、summary。',
|
||||
'不要输出 description、dangerLevel 或其他字段。',
|
||||
'不要输出 description 或其他字段。',
|
||||
'原始文本:',
|
||||
responseText.trim(),
|
||||
].join('\n');
|
||||
@@ -883,7 +879,6 @@ export function buildCustomWorldGenerationPrompt(settingText: string) {
|
||||
' "camp": {',
|
||||
' "name": "开局归处名称",',
|
||||
' "description": "玩家进入世界后的第一处落脚点描述",',
|
||||
' "dangerLevel": "low|medium|high|extreme"',
|
||||
' },',
|
||||
' "playableNpcs": [',
|
||||
' {',
|
||||
@@ -957,7 +952,6 @@ export function buildCustomWorldGenerationPrompt(settingText: string) {
|
||||
' {',
|
||||
' "name": "场景名称",',
|
||||
' "description": "场景描述",',
|
||||
' "dangerLevel": "low|medium|high|extreme",',
|
||||
' "sceneNpcNames": ["会在这个场景出现的角色1", "会在这个场景出现的角色2", "会在这个场景出现的角色3"],',
|
||||
' "connections": [',
|
||||
' {',
|
||||
@@ -1005,21 +999,6 @@ function clampSceneImageText(value: string, maxLength: number) {
|
||||
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}…`;
|
||||
}
|
||||
|
||||
function describeDangerLevel(dangerLevel: string) {
|
||||
const normalized = dangerLevel.trim().toLowerCase();
|
||||
if (normalized === 'low' || normalized === '低')
|
||||
return '气氛相对平静,但暗藏细节张力';
|
||||
if (normalized === 'medium' || normalized === '中')
|
||||
return '带有明确的探索压力与潜在威胁';
|
||||
if (normalized === 'high' || normalized === '高')
|
||||
return '危险感强烈,空间中有明显压迫感';
|
||||
if (normalized === 'extreme' || normalized === '极高')
|
||||
return '极端危险,环境本身就像会吞没闯入者';
|
||||
return dangerLevel.trim()
|
||||
? `危险氛围:${dangerLevel.trim()}`
|
||||
: '危险气质保持克制但不可忽视';
|
||||
}
|
||||
|
||||
export const DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT = [
|
||||
'文字',
|
||||
'水印',
|
||||
@@ -1041,7 +1020,7 @@ export function buildCustomWorldSceneImagePrompt(
|
||||
CustomWorldProfile,
|
||||
'name' | 'subtitle' | 'summary' | 'tone' | 'playerGoal' | 'settingText'
|
||||
>,
|
||||
landmark: Pick<CustomWorldLandmark, 'name' | 'description' | 'dangerLevel'>,
|
||||
landmark: Pick<CustomWorldLandmark, 'name' | 'description'>,
|
||||
userPrompt = '',
|
||||
options: {
|
||||
hasReferenceImage?: boolean;
|
||||
@@ -1056,7 +1035,6 @@ export function buildCustomWorldSceneImagePrompt(
|
||||
const landmarkName = clampSceneImageText(landmark.name, 18) || '未命名场景';
|
||||
const landmarkDescription = clampSceneImageText(landmark.description, 96);
|
||||
const requestedVisual = clampSceneImageText(userPrompt, 120);
|
||||
const dangerMood = describeDangerLevel(landmark.dangerLevel);
|
||||
|
||||
return [
|
||||
'为横版 16:9 2D RPG 生成高完成度像素风场景背景,适合作为剧情探索与战斗底图。',
|
||||
@@ -1075,7 +1053,6 @@ export function buildCustomWorldSceneImagePrompt(
|
||||
`场景名称:${landmarkName}。`,
|
||||
landmarkDescription ? `场景描述:${landmarkDescription}。` : '',
|
||||
requestedVisual ? `本次想要生成的画面内容:${requestedVisual}。` : '',
|
||||
`${dangerMood}。`,
|
||||
'不要出现 UI、字幕、文字、水印、logo 或装饰边框,人物仅可作为很小的远景剪影,画面重点放在场景本身,不要遮挡下半部分的角色展示区域。',
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -339,7 +339,6 @@ function createLandmark(
|
||||
return {
|
||||
name: `场景${index + 1}`,
|
||||
description: `场景描述${index + 1}`,
|
||||
dangerLevel: 'high',
|
||||
sceneNpcNames: options?.storyNpcNames ?? [
|
||||
`世界NPC${index + 1}`,
|
||||
`世界NPC${index + 2}`,
|
||||
@@ -931,7 +930,6 @@ describe('ai orchestration fallbacks', () => {
|
||||
id: 'landmark-1',
|
||||
name: '雾潮码头',
|
||||
description: '被潮雾与旧升降机包围的码头。',
|
||||
dangerLevel: 'high',
|
||||
},
|
||||
userPrompt: '雨夜的栈桥横跨黑色海沟,塔楼灯火被潮雾吞没。',
|
||||
size: '1280*720',
|
||||
@@ -1002,7 +1000,6 @@ describe('ai orchestration fallbacks', () => {
|
||||
id: 'landmark-1',
|
||||
name: '雾潮码头',
|
||||
description: '被潮雾与旧升降机包围的码头。',
|
||||
dangerLevel: 'high',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow('DashScope API key 无效。');
|
||||
|
||||
@@ -1987,7 +1987,6 @@ export async function generateCustomWorldSceneImage({
|
||||
id: landmark.id,
|
||||
name: landmark.name,
|
||||
description: landmark.description,
|
||||
dangerLevel: landmark.dangerLevel,
|
||||
},
|
||||
...(referenceImageSrc?.trim()
|
||||
? { referenceImageSrc: referenceImageSrc.trim() }
|
||||
|
||||
@@ -69,7 +69,6 @@ export interface CustomWorldSceneImageRequest {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
dangerLevel: string;
|
||||
};
|
||||
userPrompt?: string;
|
||||
prompt?: string;
|
||||
|
||||
@@ -86,7 +86,6 @@ describe('normalizeCustomWorldProfile', () => {
|
||||
{
|
||||
name: '北侧塌桥',
|
||||
description: '横跨裂谷的旧桥只剩半截石拱。',
|
||||
dangerLevel: 'high',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -191,7 +190,6 @@ describe('normalizeCustomWorldProfile', () => {
|
||||
{
|
||||
name: '北侧塌桥',
|
||||
description: '断桥上方还残留着旧索道。',
|
||||
dangerLevel: 'high',
|
||||
sceneNpcNames: ['梁砺'],
|
||||
connections: [
|
||||
{
|
||||
@@ -204,7 +202,6 @@ describe('normalizeCustomWorldProfile', () => {
|
||||
{
|
||||
name: '雾潮码头',
|
||||
description: '潮雾会把来路和去路都遮住一半。',
|
||||
dangerLevel: 'medium',
|
||||
sceneNpcNames: ['苏雾', '顾岚'],
|
||||
connections: [],
|
||||
},
|
||||
|
||||
@@ -115,7 +115,6 @@ export interface CustomWorldGenerationLandmarkOutline {
|
||||
name: string;
|
||||
description: string;
|
||||
visualDescription?: string;
|
||||
dangerLevel: string;
|
||||
sceneNpcNames: string[];
|
||||
connections: CustomWorldGenerationLandmarkConnectionOutline[];
|
||||
}
|
||||
@@ -125,7 +124,6 @@ export interface CustomWorldGenerationCampOutline {
|
||||
name: string;
|
||||
description: string;
|
||||
visualDescription?: string;
|
||||
dangerLevel: string;
|
||||
imageSrc?: string;
|
||||
sceneNpcIds?: string[];
|
||||
sceneNpcNames?: string[];
|
||||
@@ -714,7 +712,6 @@ export function normalizeCustomWorldGenerationFramework(
|
||||
camp: {
|
||||
name: fallback.camp?.name ?? '归舍',
|
||||
description: fallback.camp?.description ?? '',
|
||||
dangerLevel: fallback.camp?.dangerLevel ?? 'low',
|
||||
},
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
@@ -786,7 +783,6 @@ export function buildCustomWorldRawProfileFromFramework(
|
||||
camp: {
|
||||
name: framework.camp.name,
|
||||
description: framework.camp.description,
|
||||
dangerLevel: framework.camp.dangerLevel,
|
||||
},
|
||||
playableNpcs: framework.playableNpcs.map((npc) => ({
|
||||
name: npc.name,
|
||||
@@ -816,7 +812,6 @@ export function buildCustomWorldRawProfileFromFramework(
|
||||
name: landmark.name,
|
||||
description: landmark.description,
|
||||
visualDescription: landmark.visualDescription,
|
||||
dangerLevel: landmark.dangerLevel,
|
||||
sceneNpcNames: [...landmark.sceneNpcNames],
|
||||
connections: landmark.connections.map((connection) => ({
|
||||
targetLandmarkName: connection.targetLandmarkName,
|
||||
@@ -1071,7 +1066,6 @@ function normalizeCampOutline(
|
||||
name: toText(item.name) || fallback.name,
|
||||
description: toText(item.description) || fallback.description,
|
||||
visualDescription: toText(item.visualDescription) || undefined,
|
||||
dangerLevel: toText(item.dangerLevel) || fallback.dangerLevel,
|
||||
imageSrc: toText(item.imageSrc) || undefined,
|
||||
sceneNpcIds: toStringArray(item.sceneNpcIds),
|
||||
sceneNpcNames: [
|
||||
@@ -1107,7 +1101,6 @@ function normalizeLandmarkOutlineList(value: unknown) {
|
||||
toText(item.description) ||
|
||||
truncateText(`${name}暗藏新的局势变化。`, 40),
|
||||
visualDescription: toText(item.visualDescription) || undefined,
|
||||
dangerLevel: toText(item.dangerLevel) || 'medium',
|
||||
sceneNpcNames: [
|
||||
...toStringArray(item.sceneNpcNames),
|
||||
...toStringArray(item.npcs, 'name'),
|
||||
@@ -1168,7 +1161,6 @@ function normalizeLandmarkDraftList(value: unknown) {
|
||||
name,
|
||||
description: toText(item.description),
|
||||
visualDescription: toText(item.visualDescription) || undefined,
|
||||
dangerLevel: toText(item.dangerLevel),
|
||||
imageSrc: toText(item.imageSrc) || undefined,
|
||||
sceneNpcIds: toStringArray(item.sceneNpcIds),
|
||||
sceneNpcNames: [
|
||||
@@ -1215,7 +1207,6 @@ function normalizeCampScene(
|
||||
name: toText(item.name) || fallback.name,
|
||||
description: toText(item.description) || fallback.description,
|
||||
visualDescription: toText(item.visualDescription) || undefined,
|
||||
dangerLevel: toText(item.dangerLevel) || fallback.dangerLevel,
|
||||
imageSrc: toText(item.imageSrc) || undefined,
|
||||
sceneNpcIds: toStringArray(item.sceneNpcIds),
|
||||
connections: toRecordArray(item.connections)
|
||||
@@ -1439,7 +1430,7 @@ export function buildCustomWorldReferenceText(
|
||||
.slice(0, 10)
|
||||
.map(
|
||||
(landmark) =>
|
||||
`- ${landmark.name}:${landmark.description};危险度:${landmark.dangerLevel};场景角色:${
|
||||
`- ${landmark.name}:${landmark.description};场景角色:${
|
||||
landmark.sceneNpcIds
|
||||
.map((npcId) => storyNpcById.get(npcId)?.name)
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -75,7 +75,6 @@ describe('buildExpandedCustomWorldProfile', () => {
|
||||
id: 'landmark-1',
|
||||
name: '断桥旧哨',
|
||||
description: '旧哨火和断桥一起守着边城北口。',
|
||||
dangerLevel: 'high',
|
||||
sceneNpcIds: ['story-1'],
|
||||
connections: [],
|
||||
},
|
||||
|
||||
@@ -158,11 +158,6 @@ export function buildExpandedCustomWorldProfile(
|
||||
...landmark,
|
||||
id: landmark.id || createEntryId('landmark', landmark.name, index),
|
||||
description: clampText(landmark.description, 96),
|
||||
dangerLevel:
|
||||
landmark.dangerLevel ||
|
||||
(resolveCustomWorldCompatibilityTemplateWorldType(profile) === WorldType.XIANXIA
|
||||
? 'high'
|
||||
: 'medium'),
|
||||
}));
|
||||
const landmarkIdByReference = new Map<string, string>();
|
||||
landmarkDrafts.forEach((landmark) => {
|
||||
|
||||
@@ -14,7 +14,6 @@ type CampProfileSeed = Pick<
|
||||
| 'name'
|
||||
| 'description'
|
||||
| 'visualDescription'
|
||||
| 'dangerLevel'
|
||||
| 'imageSrc'
|
||||
| 'sceneNpcIds'
|
||||
| 'connections'
|
||||
@@ -92,7 +91,6 @@ export function buildFallbackCustomWorldCampScene(
|
||||
id: 'custom-scene-camp',
|
||||
name: fallbackName,
|
||||
description: buildFallbackCampDescription(profile, fallbackName),
|
||||
dangerLevel: 'low',
|
||||
sceneNpcIds: [],
|
||||
connections: [],
|
||||
narrativeResidues: null,
|
||||
@@ -110,7 +108,6 @@ export function resolveCustomWorldCampScene(
|
||||
name: camp?.name?.trim() || fallback.name,
|
||||
description: camp?.description?.trim() || fallback.description,
|
||||
visualDescription: camp?.visualDescription?.trim() || undefined,
|
||||
dangerLevel: camp?.dangerLevel?.trim() || fallback.dangerLevel,
|
||||
imageSrc: camp?.imageSrc?.trim() || undefined,
|
||||
sceneNpcIds: Array.isArray(camp?.sceneNpcIds)
|
||||
? [...new Set(camp.sceneNpcIds.map((entry) => entry.trim()).filter(Boolean))]
|
||||
|
||||
@@ -59,7 +59,6 @@ function createBaseProfile(): CustomWorldProfile {
|
||||
id: 'camp-1',
|
||||
name: '守夜营地',
|
||||
description: '潮线后的临时据点。',
|
||||
dangerLevel: 'medium',
|
||||
imageSrc: '/images/camp/camp.webp',
|
||||
sceneNpcIds: [],
|
||||
connections: [],
|
||||
@@ -69,7 +68,6 @@ function createBaseProfile(): CustomWorldProfile {
|
||||
id: 'landmark-1',
|
||||
name: '潮汐码头',
|
||||
description: '涨潮时会吞掉半截栈桥。',
|
||||
dangerLevel: 'high',
|
||||
imageSrc: '/images/landmark/docks.webp',
|
||||
sceneNpcIds: [],
|
||||
connections: [],
|
||||
|
||||
@@ -353,7 +353,7 @@ function inferRoleArchetypeLabel(
|
||||
}
|
||||
|
||||
function inferSceneBucketLabel(
|
||||
landmark: Pick<CustomWorldProfile['landmarks'][number], 'name' | 'description' | 'dangerLevel'>,
|
||||
landmark: Pick<CustomWorldProfile['landmarks'][number], 'name' | 'description'>,
|
||||
) {
|
||||
const source = `${landmark.name} ${landmark.description}`;
|
||||
|
||||
@@ -365,9 +365,7 @@ function inferSceneBucketLabel(
|
||||
if (/[洞|穴|地下|遗迹|墓]/u.test(source)) return '地底遗迹区';
|
||||
if (/[街|巷|城|镇|居]/u.test(source)) return '群落聚居区';
|
||||
|
||||
return landmark.dangerLevel === 'high' || landmark.dangerLevel === 'extreme'
|
||||
? '高压交汇区'
|
||||
: '叙事缓冲区';
|
||||
return '叙事缓冲区';
|
||||
}
|
||||
|
||||
function buildRoleArchetypes(profile: CustomWorldProfile) {
|
||||
@@ -388,7 +386,7 @@ function buildSceneBuckets(profile: CustomWorldProfile) {
|
||||
id: `scene-bucket-${index + 1}`,
|
||||
label: inferSceneBucketLabel(landmark),
|
||||
moodTags: dedupeStrings(
|
||||
[landmark.dangerLevel, ...splitToneTags(profile.tone)],
|
||||
splitToneTags(profile.tone),
|
||||
4,
|
||||
),
|
||||
keywords: dedupeStrings([landmark.name, landmark.description], 4),
|
||||
|
||||
@@ -112,7 +112,6 @@ describe('buildUserPrompt', () => {
|
||||
id: 'landmark-1',
|
||||
name: '断桥旧哨',
|
||||
description: '旧哨火和断桥一起守着边城北口。',
|
||||
dangerLevel: 'high',
|
||||
sceneNpcIds: ['story-1'],
|
||||
connections: [],
|
||||
},
|
||||
|
||||
@@ -375,7 +375,6 @@ export interface CustomWorldCampScene {
|
||||
name: string;
|
||||
description: string;
|
||||
visualDescription?: string;
|
||||
dangerLevel: string;
|
||||
imageSrc?: string;
|
||||
sceneNpcIds: string[];
|
||||
connections: CustomWorldSceneConnection[];
|
||||
@@ -387,7 +386,6 @@ export interface CustomWorldLandmark {
|
||||
name: string;
|
||||
description: string;
|
||||
visualDescription?: string;
|
||||
dangerLevel: string;
|
||||
imageSrc?: string;
|
||||
sceneNpcIds: string[];
|
||||
connections: CustomWorldSceneConnection[];
|
||||
|
||||
Reference in New Issue
Block a user