1
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user