Merge branch 'codex/backend-rewrite-spacetimedb' of http://82.157.175.59:3000/GenarrativeAI/Genarrative into codex/backend-rewrite-spacetimedb

# Conflicts:
#	docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md   resolved by origin/codex/backend-rewrite-spacetimedb(远端) version
This commit is contained in:
2026-04-25 17:06:57 +08:00
393 changed files with 2902 additions and 91859 deletions

View File

@@ -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 ?? '',

View File

@@ -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: [

View File

@@ -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: [],
},

View File

@@ -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

View File

@@ -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

View File

@@ -434,7 +434,6 @@ const compiledAgentDraftSession: CustomWorldAgentSessionSnapshot = {
id: 'landmark-1',
name: '回潮旧灯塔',
description: '旧灯塔是整片群岛最先看见异动的地方。',
dangerLevel: 'high',
sceneNpcIds: ['story-1'],
connections: [],
},

View File

@@ -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: [
{

View File

@@ -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,

View File

@@ -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(

View File

@@ -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,
},

View File

@@ -30,7 +30,7 @@ export const STORY_OPENING_CAMP_DIALOGUE_FUNCTION: FunctionDocumentationEntry =
storyMode: 'special_sequence',
uiMode: 'none',
executor:
'server-node/src/modules/rpg-runtime-story/RpgRuntimeStoryActionDomain.ts + src/hooks/rpg-runtime-story/storyContextBuilder.ts',
'server-rs/crates/api-server/src/runtime_story/compat.rs -> resolve_runtime_story_choice_action',
animationNote: '重点在对白本身,不额外驱动独立战斗/位移动画。',
storyNote: '会把 prompt 切到营地开场对白模式,并要求输出结构化对话行。',
uiNote: '不弹 modal直接进入对白流。',

View File

@@ -8,7 +8,7 @@ import type { FunctionDocumentationEntry } from '../types';
*/
const QUEST_OFFER_SOURCE = 'src/data/functionCatalog/npc/npcChatQuestOffer.ts';
const QUEST_OFFER_EXECUTOR =
'server-node/src/modules/quest/questStoryActionService.ts -> resolveQuestStoryAction';
'server-rs/crates/api-server/src/runtime_story/compat.rs -> resolve_runtime_story_choice_action';
export const NPC_CHAT_QUEST_OFFER_VIEW_FUNCTION: FunctionDocumentationEntry = {
id: 'npc_chat_quest_offer_view',

View File

@@ -16,7 +16,7 @@ export const BATTLE_ATTACK_BASIC_FUNCTION: FunctionDocumentationEntry = {
'这个 function 代表一次明确的普通攻击点击,后端直接结算伤害、敌方反击和下一轮战斗选项,不再请求 AI 续写整段战斗剧情。',
trigger: '仅在 battle 状态且场上仍有存活敌人时,由后端战斗 option 池下发。',
execution:
'前端透传 functionId后端 combatResolutionService 直接按普通攻击规则结算本回合。',
'前端透传 functionIdRust 后端经 story battle facade 调用 module-combat 按普通攻击规则结算本回合。',
result: '刷新 HP、战斗日志和下一轮战斗 options若敌人被击败再进入脱战剧情推理。',
state: 'battle',
category: 'battle',
@@ -25,7 +25,7 @@ export const BATTLE_ATTACK_BASIC_FUNCTION: FunctionDocumentationEntry = {
storyMode: 'local_only',
uiMode: 'none',
executor:
'server-node/src/modules/combat/combatResolutionService.ts -> resolveCombatAction',
'server-rs/crates/module-combat/src/lib.rs -> resolve_combat_action',
animationNote: '播放一次基础攻击和受击反馈,不扩展成连续多段连击。',
storyNote:
'战斗未结束时只展示本次结算文本;战斗结束后才请求脱战剧情。',

View File

@@ -16,7 +16,7 @@ export const BATTLE_USE_SKILL_FUNCTION: FunctionDocumentationEntry = {
'这个 functionId 可以对应多个技能 option 实例。前端只展示技能名和不可用原因,后端根据 runtimePayload.skillId 校验蓝量、冷却并结算本次技能效果。',
trigger: '仅在 battle 状态下由后端按角色技能列表生成,可能携带 disabled 状态。',
execution:
'前端透传 runtimePayload.skillId后端 combatResolutionService 校验技能并完成一次技能动作结算。',
'前端透传 runtimePayload.skillIdRust 后端经 story battle facade 调用 module-combat 校验技能并完成一次技能动作结算。',
result:
'更新 MP、技能冷却、敌我 HP 和下一轮战斗 options若战斗结束再触发脱战剧情推理。',
state: 'battle',
@@ -26,7 +26,7 @@ export const BATTLE_USE_SKILL_FUNCTION: FunctionDocumentationEntry = {
storyMode: 'local_only',
uiMode: 'none',
executor:
'server-node/src/modules/combat/combatResolutionService.ts -> resolveCombatAction',
'server-rs/crates/module-combat/src/lib.rs -> resolve_combat_action',
animationNote: '根据技能 option 播放一次技能演出,不在本 function 内追加多回合动作。',
storyNote:
'战斗未结束时使用本次技能结算文本;只有战斗结束才请求新剧情。',

View File

@@ -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: [],
},

View File

@@ -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: [
{

View File

@@ -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', () => {

View File

@@ -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,

View File

@@ -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: [
{

View File

@@ -16,7 +16,6 @@ const framework = {
camp: {
name: '旧灯塔营地',
description: '潮雾里的临时归处。',
dangerLevel: 'medium',
},
playableNpcs: [],
storyNpcs: [],

View File

@@ -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 条 connectionsrelativePosition 只能使用:${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)

View File

@@ -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 无效。');

View File

@@ -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() }

View File

@@ -69,7 +69,6 @@ export interface CustomWorldSceneImageRequest {
id: string;
name: string;
description: string;
dangerLevel: string;
};
userPrompt?: string;
prompt?: string;

View File

@@ -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: [],
},

View File

@@ -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)

View File

@@ -75,7 +75,6 @@ describe('buildExpandedCustomWorldProfile', () => {
id: 'landmark-1',
name: '断桥旧哨',
description: '旧哨火和断桥一起守着边城北口。',
dangerLevel: 'high',
sceneNpcIds: ['story-1'],
connections: [],
},

View File

@@ -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) => {

View File

@@ -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))]

View File

@@ -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: [],

View File

@@ -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),

View File

@@ -112,7 +112,6 @@ describe('buildUserPrompt', () => {
id: 'landmark-1',
name: '断桥旧哨',
description: '旧哨火和断桥一起守着边城北口。',
dangerLevel: 'high',
sceneNpcIds: ['story-1'],
connections: [],
},

View File

@@ -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[];