This commit is contained in:
2026-04-30 17:49:07 +08:00
parent 805d6f8cae
commit 9d684cb7b3
615 changed files with 15368 additions and 6172 deletions

View File

@@ -14,14 +14,23 @@ import { createPortal } from 'react-dom';
import { AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS } from '../../data/affinityLevels';
import {
buildCustomWorldPlayableCharacters,
buildCustomWorldRuntimeCharacters,
createCharacterSkillCooldowns,
getCharacterMaxHp,
getCharacterMaxMana,
ROLE_TEMPLATE_CHARACTERS,
setRuntimeCharacterOverrides,
} from '../../data/characterPresets';
import { setRuntimeCustomWorldProfile } from '../../data/customWorldRuntime';
import { normalizeCustomWorldLandmarks } from '../../data/customWorldSceneGraph';
import {
getAllCustomWorldSceneImages,
resolveCustomWorldCampSceneImage,
resolveCustomWorldLandmarkImage,
} from '../../data/customWorldVisuals';
import { buildInitialEquipmentLoadout } from '../../data/equipmentEffects';
import { createInitialPlayerProgressionState } from '../../data/playerProgression';
import { createInitialGameRuntimeStats } from '../../data/runtimeStats';
import { RESOLVED_ENTITY_X_METERS } from '../../data/sceneEncounterPreviews';
import { buildEncounterFromSceneNpc } from '../../data/scenePresets';
import { EDITOR_ITEM_CATALOG_API_PATH } from '../../editor/shared/editorApiClient';
@@ -75,6 +84,8 @@ import {
type SceneChapterBlueprint,
type SceneNpc,
type ScenePresetInfo,
type StoryMoment,
type StoryOption,
WorldType,
} from '../../types';
import {
@@ -1946,6 +1957,93 @@ function buildSceneActPreviewScenePreset(params: {
};
}
const SCENE_ACT_PREVIEW_SESSION_ID = 'runtime-scene-act-preview';
function buildSceneActPreviewNpcOption(params: {
functionId: string;
actionText: string;
npcId: string;
action: 'chat' | 'fight' | 'leave';
}): StoryOption {
return {
functionId: params.functionId,
actionText: params.actionText,
text: params.actionText,
detailText: '',
visuals: {
playerAnimation: AnimationState.IDLE,
playerMoveMeters: 0,
playerOffsetY: 0,
playerFacing: 'right',
scrollWorld: false,
monsterChanges: [],
},
interaction: {
kind: 'npc',
npcId: params.npcId,
action: params.action,
},
};
}
function buildSceneActPreviewOpeningStory(params: {
sceneName: string;
encounter: NonNullable<ReturnType<typeof buildEncounterFromSceneNpc>>;
}): StoryMoment {
const npcId = params.encounter.id ?? params.encounter.npcName;
const openingText = `${params.encounter.npcName}已经在${params.sceneName || '当前场景'}等你。`;
return {
text: openingText,
displayMode: 'dialogue',
dialogue: [
{
speaker: 'npc',
speakerName: params.encounter.npcName,
text: openingText,
},
],
streaming: false,
npcChatState: {
npcId,
npcName: params.encounter.npcName,
turnCount: 0,
customInputPlaceholder: '输入你想对 TA 说的话',
openingSource: 'npc_initiated',
sceneActId: null,
turnLimit: null,
remainingTurns: null,
limitReason: null,
forceExitAfterTurn: false,
terminationMode: null,
terminationReason: null,
isHostileChat: false,
pendingQuestOffer: null,
combatContext: null,
},
options: [
buildSceneActPreviewNpcOption({
functionId: 'npc_chat',
actionText: '先听他说完',
npcId,
action: 'chat',
}),
buildSceneActPreviewNpcOption({
functionId: 'npc_fight',
actionText: '与他对战',
npcId,
action: 'fight',
}),
buildSceneActPreviewNpcOption({
functionId: 'npc_leave',
actionText: '暂时离开',
npcId,
action: 'leave',
}),
],
};
}
function SceneActPreviewRuntime({
profile,
landmark,
@@ -2045,8 +2143,10 @@ function SceneActPreviewRuntime({
useNpcInteractionFlow(gameState);
const isPreviewReady =
gameState.currentScene === 'Story' &&
Boolean(gameState.playerCharacter) &&
gameState.currentScenePreset?.id === landmark.id;
gameState.runtimeSessionId === SCENE_ACT_PREVIEW_SESSION_ID &&
gameState.playerCharacter?.id === previewCharacter?.id &&
gameState.currentScenePreset?.id === previewScenePreset?.id &&
Boolean(storyFlow.currentStory);
useEffect(() => {
if (
@@ -2064,28 +2164,64 @@ function SceneActPreviewRuntime({
storyFlow.resetStoryState();
setBottomTab('adventure');
setIsMapOpen(false);
handleCustomWorldSelect(profile);
handleCharacterSelect(previewCharacter);
// 中文注释:幕预览只需要同步静态资料层,不能调用正式选世界入口;
// 后者会排队写入“已选世界但未选角”的中间态,把本地预览 GameState 覆盖回加载中。
setRuntimeCustomWorldProfile(profile);
setRuntimeCharacterOverrides(buildCustomWorldRuntimeCharacters(profile));
const previewCharacterMaxHp = getCharacterMaxHp(
previewCharacter,
WorldType.CUSTOM,
profile,
);
const previewCharacterMaxMana = getCharacterMaxMana(previewCharacter);
const previewEquipment = buildInitialEquipmentLoadout(
previewCharacter,
profile,
);
setGameState((current) => ({
...current,
worldType: WorldType.CUSTOM,
customWorldProfile: profile,
playerCharacter: previewCharacter,
runtimeSessionId: SCENE_ACT_PREVIEW_SESSION_ID,
runtimeActionVersion: 1,
// 中文注释:幕预览也统一复用正式 play 运行链,
// 只通过禁持久化控制“不写正式存档”。
runtimeMode: 'play',
runtimePersistenceDisabled: true,
runtimeStats: createInitialGameRuntimeStats({ isActiveRun: true }),
playerProgression: createInitialPlayerProgressionState(),
currentScene: 'Story',
currentScenePreset: previewScenePreset,
currentEncounter: previewEncounter,
npcInteractionActive: false,
sceneHostileNpcs: [],
inBattle: false,
playerHp: previewCharacterMaxHp,
playerMaxHp: previewCharacterMaxHp,
playerMana: previewCharacterMaxMana,
playerMaxMana: previewCharacterMaxMana,
playerSkillCooldowns: createCharacterSkillCooldowns(previewCharacter),
playerCurrency: 0,
playerInventory: [],
playerEquipment: previewEquipment,
npcStates: {},
quests: [],
roster: [],
companions: [],
storyHistory: [],
chapterState: null,
campaignState: null,
activeScenarioPackId: profile.scenarioPackId ?? null,
activeCampaignPackId: profile.campaignPackId ?? null,
currentBattleNpcId: null,
currentNpcBattleMode: null,
currentNpcBattleOutcome: null,
sparReturnEncounter: null,
sparPlayerHpBefore: null,
sparPlayerMaxHpBefore: null,
sparStoryHistoryBefore: null,
characterChats: {},
playerX: 0,
playerOffsetY: 0,
playerFacing: 'right',
@@ -2102,11 +2238,14 @@ function SceneActPreviewRuntime({
currentSceneActState: previewActRuntimeState,
},
}));
storyFlow.hydrateStoryState(
buildSceneActPreviewOpeningStory({
sceneName: previewScenePreset.name,
encounter: previewEncounter,
}),
);
}, [
act,
handleCharacterSelect,
handleCustomWorldSelect,
landmark.id,
previewActRuntimeState,
previewCharacter,
previewEncounter,