Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-08 11:58:47 +08:00
parent 9d2fc9e4b8
commit bd9fdcbe31
170 changed files with 18259 additions and 1049 deletions

View File

@@ -1,5 +1,5 @@
import { resolveCustomWorldCampScene } from '../services/customWorldCamp';
import { buildCustomCampSceneName } from '../services/customWorldPresentation';
import { resolveCustomWorldAnchorWorldType } from '../services/customWorldTheme';
import {
buildFallbackActorNarrativeProfile,
normalizeActorNarrativeProfile,
@@ -26,7 +26,11 @@ import {
} from './characterPresets';
import { resolveCustomWorldNpcMonsterPreset } from './customWorldNpcMonsters';
import { getRuntimeCustomWorldProfile, resolveRuleWorldType } from './customWorldRuntime';
import { getMonsterPresetById } from './hostileNpcPresets';
import {
resolveCustomWorldCampSceneImage,
resolveCustomWorldLandmarkImageMap,
} from './customWorldVisuals';
import { getMonsterPresetById, getMonsterPresetsByWorld } from './hostileNpcPresets';
import sceneNpcOverridesJson from './sceneNpcOverrides.json';
import sceneOverridesJson from './sceneOverrides.json';
@@ -124,18 +128,6 @@ function collectWorldImagePool(worldType: WorldType, requiredCount: number) {
return refs;
}
function collectAllImagePool() {
const refs: string[] = [];
for (const pack of PACKS) {
for (let imageNumber = 1; imageNumber <= pack.count; imageNumber += 1) {
refs.push(buildImagePath(pack.packName, imageNumber));
}
}
return refs;
}
function uniqueStrings(values: string[]) {
return [...new Set(values.filter(Boolean))];
}
@@ -305,7 +297,6 @@ export function buildEncounterFromSceneNpc(
function buildCustomSceneNpc(
npc: CustomWorldProfile['storyNpcs'][number],
profile: CustomWorldProfile,
anchorWorldType: WorldType,
): SceneNpc {
const themePack = profile.themePack ?? buildThemePackFromWorldProfile(profile);
const storyGraph =
@@ -316,7 +307,7 @@ function buildCustomSceneNpc(
);
const monsterPreset =
npc.initialAffinity < 0
? resolveCustomWorldNpcMonsterPreset(npc, anchorWorldType)
? resolveCustomWorldNpcMonsterPreset(npc)
: null;
const hostile = npc.initialAffinity < 0 || Boolean(monsterPreset);
const attributeProfile = monsterPreset?.attributeProfile
@@ -339,6 +330,7 @@ function buildCustomSceneNpc(
return {
id: npc.id,
characterId: npc.id,
name: npc.name,
title: npc.title,
role: npc.role,
@@ -384,11 +376,10 @@ function buildCustomSceneId(kind: 'camp' | 'landmark', index = 0) {
}
function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
const allImages = collectAllImagePool();
const imageOffset = hashText(profile.id || profile.name) % Math.max(1, allImages.length);
const anchorWorldType = resolveCustomWorldAnchorWorldType(profile);
const baseMonsterPool: string[] = getScenePresetsByWorld(anchorWorldType)
.flatMap((scene: ScenePreset) => getSceneHostileNpcPresetIds(scene))
const campSceneProfile = resolveCustomWorldCampScene(profile);
const landmarkImageMap = resolveCustomWorldLandmarkImageMap(profile);
const baseMonsterPool: string[] = getMonsterPresetsByWorld(WorldType.CUSTOM)
.map((monster) => monster.id)
.filter((monsterId: string, index: number, array: string[]) => array.indexOf(monsterId) === index);
const fallbackMonsterIds: string[] = baseMonsterPool.length > 0 ? baseMonsterPool : [];
const playableCharacters = buildCustomWorldPlayableCharacters(profile);
@@ -423,16 +414,16 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
sceneId: landmarkSceneIds[index] ?? '',
relativePosition:
index === 0 ? 'forward' : index === 1 ? 'left' : 'right',
summary: `营地可直接通往${landmark.name}`,
summary: `${campSceneProfile.name}可直接通往${landmark.name}`,
}))
.filter((connection) => connection.sceneId) as SceneConnectionInfo[];
const customScenes: ScenePreset[] = [
{
id: campSceneId,
name: buildCustomCampSceneName(profile),
description: `你在${profile.name}的临时营地整备行装。${profile.summary}`,
description: campSceneProfile.description,
worldType: WorldType.CUSTOM,
imageSrc: profile.landmarks[0]?.imageSrc ?? allImages[imageOffset] ?? '',
imageSrc: resolveCustomWorldCampSceneImage(profile),
connectedSceneIds: campConnections.map((connection) => connection.sceneId),
connections: campConnections,
forwardSceneId: pickForwardSceneIdFromConnections(campConnections),
@@ -452,7 +443,7 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
.map((npcId) => customStoryNpcById.get(npcId))
.filter(Boolean)
.map((npc) =>
buildCustomSceneNpc(npc!, profile, anchorWorldType),
buildCustomSceneNpc(npc!, profile),
);
if (sceneNpcs.length < 3) {
profile.storyNpcs
@@ -461,7 +452,7 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
)
.slice(0, 3 - sceneNpcs.length)
.forEach((npc) =>
sceneNpcs.push(buildCustomSceneNpc(npc, profile, anchorWorldType)),
sceneNpcs.push(buildCustomSceneNpc(npc, profile)),
);
}
const landmarkConnections = landmark.connections
@@ -489,7 +480,7 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
? ({
sceneId: campSceneId,
relativePosition: 'back',
summary: '可回到临时营地整备',
summary: `可回到${campSceneProfile.name}整备`,
} satisfies SceneConnectionInfo)
: null;
const connections = [
@@ -502,7 +493,7 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
const monsterSliceStart = (index * 2) % Math.max(1, fallbackMonsterIds.length || 1);
const seedMonsterIds: string[] = fallbackMonsterIds.slice(monsterSliceStart, monsterSliceStart + 2);
const hostileNpcs = seedMonsterIds
.map((monsterId: string) => buildHostileSceneNpc(buildCustomSceneId('landmark', index), anchorWorldType, monsterId))
.map((monsterId: string) => buildHostileSceneNpc(buildCustomSceneId('landmark', index), WorldType.CUSTOM, monsterId))
.filter(Boolean) as SceneNpc[];
const combinedNpcs = [...sceneNpcs, ...hostileNpcs];
@@ -511,7 +502,7 @@ function buildCustomScenePresets(profile: CustomWorldProfile): ScenePreset[] {
name: landmark.name,
description: landmark.description,
worldType: WorldType.CUSTOM,
imageSrc: landmark.imageSrc ?? allImages[(imageOffset + index + 1) % Math.max(1, allImages.length)] ?? '',
imageSrc: landmarkImageMap.get(landmark.id) ?? '',
connectedSceneIds,
connections,
forwardSceneId: pickForwardSceneIdFromConnections(connections),
@@ -1090,6 +1081,3 @@ export function buildSceneEntityCatalogText(worldType: WorldType, sceneId: strin
`当前场景残痕:${residueText}`,
].join('\n');
}