@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import {
|
||||
acceptQuest,
|
||||
@@ -42,9 +39,7 @@ import {
|
||||
buildCompanionReactionBatch,
|
||||
} from '../../services/storyEngine/companionReactionDirector';
|
||||
import { resolveAllCompanionResolutions } from '../../services/storyEngine/companionResolutionDirector';
|
||||
import {
|
||||
appendConsequenceRecord,
|
||||
} from '../../services/storyEngine/consequenceLedger';
|
||||
import { appendConsequenceRecord } from '../../services/storyEngine/consequenceLedger';
|
||||
import { buildContentDiffReport } from '../../services/storyEngine/contentDiffReport';
|
||||
import { resolveEndingState } from '../../services/storyEngine/endingResolver';
|
||||
import { buildEpilogueSummary } from '../../services/storyEngine/epilogueComposer';
|
||||
@@ -97,8 +92,9 @@ const ENCOUNTER_ENTRY_DURATION_MS = 1800;
|
||||
const ENCOUNTER_ENTRY_TICK_MS = 180;
|
||||
|
||||
function dedupeStrings(values: Array<string | null | undefined>, limit = 10) {
|
||||
return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))]
|
||||
.slice(0, limit);
|
||||
return [
|
||||
...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean)),
|
||||
].slice(0, limit);
|
||||
}
|
||||
|
||||
function hydrateStoryEngineMemory(state: GameState): GameState {
|
||||
@@ -112,11 +108,15 @@ function hydrateStoryEngineMemory(state: GameState): GameState {
|
||||
}
|
||||
|
||||
const role =
|
||||
state.customWorldProfile.storyNpcs.find((npc) =>
|
||||
npc.id === state.currentEncounter?.id || npc.name === state.currentEncounter?.npcName,
|
||||
)
|
||||
?? state.customWorldProfile.playableNpcs.find((npc) =>
|
||||
npc.id === state.currentEncounter?.id || npc.name === state.currentEncounter?.npcName,
|
||||
state.customWorldProfile.storyNpcs.find(
|
||||
(npc) =>
|
||||
npc.id === state.currentEncounter?.id ||
|
||||
npc.name === state.currentEncounter?.npcName,
|
||||
) ??
|
||||
state.customWorldProfile.playableNpcs.find(
|
||||
(npc) =>
|
||||
npc.id === state.currentEncounter?.id ||
|
||||
npc.name === state.currentEncounter?.npcName,
|
||||
);
|
||||
if (!role) {
|
||||
return {
|
||||
@@ -126,17 +126,19 @@ function hydrateStoryEngineMemory(state: GameState): GameState {
|
||||
}
|
||||
|
||||
const themePack =
|
||||
state.customWorldProfile.themePack
|
||||
?? buildThemePackFromWorldProfile(state.customWorldProfile);
|
||||
state.customWorldProfile.themePack ??
|
||||
buildThemePackFromWorldProfile(state.customWorldProfile);
|
||||
const storyGraph =
|
||||
state.customWorldProfile.storyGraph
|
||||
?? buildFallbackWorldStoryGraph(state.customWorldProfile, themePack);
|
||||
state.customWorldProfile.storyGraph ??
|
||||
buildFallbackWorldStoryGraph(state.customWorldProfile, themePack);
|
||||
const narrativeProfile = normalizeActorNarrativeProfile(
|
||||
role.narrativeProfile,
|
||||
buildFallbackActorNarrativeProfile(role, storyGraph, themePack),
|
||||
);
|
||||
const npcState =
|
||||
state.npcStates[state.currentEncounter.id ?? state.currentEncounter.npcName];
|
||||
state.npcStates[
|
||||
state.currentEncounter.id ?? state.currentEncounter.npcName
|
||||
];
|
||||
const activeThreadIds =
|
||||
storyEngineMemory.activeThreadIds.length > 0
|
||||
? storyEngineMemory.activeThreadIds
|
||||
@@ -164,20 +166,25 @@ function hydrateStoryEngineMemory(state: GameState): GameState {
|
||||
...state,
|
||||
storyEngineMemory: {
|
||||
...storyEngineMemory,
|
||||
discoveredFactIds: dedupeStrings([
|
||||
...storyEngineMemory.discoveredFactIds,
|
||||
...visibilitySlice.sayableFactIds,
|
||||
], 16),
|
||||
activeThreadIds: dedupeStrings([
|
||||
...storyEngineMemory.activeThreadIds,
|
||||
...activeThreadIds,
|
||||
], 6),
|
||||
discoveredFactIds: dedupeStrings(
|
||||
[
|
||||
...storyEngineMemory.discoveredFactIds,
|
||||
...visibilitySlice.sayableFactIds,
|
||||
],
|
||||
16,
|
||||
),
|
||||
activeThreadIds: dedupeStrings(
|
||||
[...storyEngineMemory.activeThreadIds, ...activeThreadIds],
|
||||
6,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function findNewInventoryItems(previousState: GameState, nextState: GameState) {
|
||||
const previousIds = new Set(previousState.playerInventory.map((item) => item.id));
|
||||
const previousIds = new Set(
|
||||
previousState.playerInventory.map((item) => item.id),
|
||||
);
|
||||
return nextState.playerInventory.filter((item) => !previousIds.has(item.id));
|
||||
}
|
||||
|
||||
@@ -189,9 +196,9 @@ function ensureSceneChapterQuestState(params: {
|
||||
params.nextState.storyEngineMemory ?? createEmptyStoryEngineMemoryState();
|
||||
const scene = params.nextState.currentScenePreset;
|
||||
if (
|
||||
params.nextState.currentScene !== 'Story'
|
||||
|| !params.nextState.worldType
|
||||
|| !scene?.id
|
||||
params.nextState.currentScene !== 'Story' ||
|
||||
!params.nextState.worldType ||
|
||||
!scene?.id
|
||||
) {
|
||||
return {
|
||||
...params.nextState,
|
||||
@@ -199,9 +206,10 @@ function ensureSceneChapterQuestState(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const openedSceneChapterIds = dedupeStrings([
|
||||
...(storyEngineMemory.openedSceneChapterIds ?? []),
|
||||
], 64);
|
||||
const openedSceneChapterIds = dedupeStrings(
|
||||
[...(storyEngineMemory.openedSceneChapterIds ?? [])],
|
||||
64,
|
||||
);
|
||||
if (openedSceneChapterIds.includes(scene.id)) {
|
||||
return {
|
||||
...params.nextState,
|
||||
@@ -216,7 +224,10 @@ function ensureSceneChapterQuestState(params: {
|
||||
...storyEngineMemory,
|
||||
openedSceneChapterIds: [...openedSceneChapterIds, scene.id],
|
||||
};
|
||||
const existingChapterQuest = getChapterQuestForScene(params.nextState.quests, scene.id);
|
||||
const existingChapterQuest = getChapterQuestForScene(
|
||||
params.nextState.quests,
|
||||
scene.id,
|
||||
);
|
||||
if (existingChapterQuest) {
|
||||
return {
|
||||
...params.nextState,
|
||||
@@ -227,6 +238,13 @@ function ensureSceneChapterQuestState(params: {
|
||||
const chapterQuest = buildChapterQuestForScene({
|
||||
scene,
|
||||
worldType: params.nextState.worldType,
|
||||
context: {
|
||||
worldType: params.nextState.worldType,
|
||||
actState: params.nextState.storyEngineMemory?.actState ?? null,
|
||||
recentStoryMoments: params.nextState.storyHistory.slice(-6),
|
||||
playerCharacter: params.nextState.playerCharacter,
|
||||
playerProgression: params.nextState.playerProgression ?? null,
|
||||
},
|
||||
});
|
||||
if (!chapterQuest) {
|
||||
return {
|
||||
@@ -250,8 +268,8 @@ function applyStoryEngineEchoes(params: {
|
||||
}) {
|
||||
const hydratedState = hydrateStoryEngineMemory(params.nextState);
|
||||
const contracts = hydratedState.customWorldProfile
|
||||
? hydratedState.customWorldProfile.threadContracts
|
||||
?? buildThreadContractsFromProfile(hydratedState.customWorldProfile)
|
||||
? (hydratedState.customWorldProfile.threadContracts ??
|
||||
buildThreadContractsFromProfile(hydratedState.customWorldProfile))
|
||||
: [];
|
||||
const newItems = findNewInventoryItems(params.previousState, hydratedState);
|
||||
const signals = collectStorySignals({
|
||||
@@ -279,12 +297,13 @@ function applyStoryEngineEchoes(params: {
|
||||
state: stateWithSceneChapter,
|
||||
reactions,
|
||||
});
|
||||
const storyEngineMemory = stateWithReactions.storyEngineMemory ?? createEmptyStoryEngineMemoryState();
|
||||
const storyEngineMemory =
|
||||
stateWithReactions.storyEngineMemory ?? createEmptyStoryEngineMemoryState();
|
||||
const chapterState = advanceChapterState({
|
||||
previousChapter:
|
||||
stateWithReactions.chapterState
|
||||
?? storyEngineMemory.currentChapter
|
||||
?? null,
|
||||
stateWithReactions.chapterState ??
|
||||
storyEngineMemory.currentChapter ??
|
||||
null,
|
||||
nextChapter: resolveCurrentChapterState({
|
||||
state: stateWithReactions,
|
||||
}),
|
||||
@@ -358,7 +377,10 @@ function applyStoryEngineEchoes(params: {
|
||||
chapterState,
|
||||
});
|
||||
const campaignState = advanceCampaignState({
|
||||
previous: storyEngineMemory.campaignState ?? stateWithMutations.campaignState ?? null,
|
||||
previous:
|
||||
storyEngineMemory.campaignState ??
|
||||
stateWithMutations.campaignState ??
|
||||
null,
|
||||
next: resolveCampaignState({
|
||||
state: stateWithMutations,
|
||||
actState,
|
||||
@@ -380,9 +402,9 @@ function applyStoryEngineEchoes(params: {
|
||||
})
|
||||
: null;
|
||||
const activeScenarioPack =
|
||||
resolveScenarioPack(stateWithMutations.activeScenarioPackId)
|
||||
?? compiledPacks?.scenarioPack
|
||||
?? null;
|
||||
resolveScenarioPack(stateWithMutations.activeScenarioPackId) ??
|
||||
compiledPacks?.scenarioPack ??
|
||||
null;
|
||||
const activeCampaignPack = compiledPacks?.campaignPack ?? null;
|
||||
const playerStyleProfile = updatePlayerStyleProfileFromAction({
|
||||
current: storyEngineMemory.playerStyleProfile,
|
||||
@@ -401,15 +423,15 @@ function applyStoryEngineEchoes(params: {
|
||||
companionResolutions,
|
||||
factionTensionStates,
|
||||
})
|
||||
: storyEngineMemory.endingState ?? null;
|
||||
const epilogueSummary =
|
||||
endingState
|
||||
? buildEpilogueSummary({
|
||||
endingState,
|
||||
companionResolutions,
|
||||
})
|
||||
: null;
|
||||
const currentJourneyBeatId = journeyBeat?.id ?? storyEngineMemory.currentJourneyBeatId ?? null;
|
||||
: (storyEngineMemory.endingState ?? null);
|
||||
const epilogueSummary = endingState
|
||||
? buildEpilogueSummary({
|
||||
endingState,
|
||||
companionResolutions,
|
||||
})
|
||||
: null;
|
||||
const currentJourneyBeatId =
|
||||
journeyBeat?.id ?? storyEngineMemory.currentJourneyBeatId ?? null;
|
||||
const branchBudgetStatus = evaluateBranchBudget({
|
||||
consequenceLedger,
|
||||
authorialConstraintPack,
|
||||
@@ -455,20 +477,20 @@ function applyStoryEngineEchoes(params: {
|
||||
seeds: ['baseline', 'companion', 'explore'],
|
||||
})
|
||||
: [];
|
||||
const replaySummary =
|
||||
simulationRunResults[0]
|
||||
? replayNarrativeRun({
|
||||
recordedSeed: recordReplaySeed({
|
||||
seed: simulationRunResults[0].seed,
|
||||
label: `${activeCampaignPack?.title ?? 'campaign'} 基线回放`,
|
||||
}),
|
||||
result: simulationRunResults[0],
|
||||
}).summary
|
||||
: null;
|
||||
const replaySummary = simulationRunResults[0]
|
||||
? replayNarrativeRun({
|
||||
recordedSeed: recordReplaySeed({
|
||||
seed: simulationRunResults[0].seed,
|
||||
label: `${activeCampaignPack?.title ?? 'campaign'} 基线回放`,
|
||||
}),
|
||||
result: simulationRunResults[0],
|
||||
}).summary
|
||||
: null;
|
||||
const releaseGateReport = buildReleaseGateReport({
|
||||
qaReport: narrativeQaReport,
|
||||
simulationResults: simulationRunResults,
|
||||
unresolvedThreadCount: stateWithMutations.storyEngineMemory?.activeThreadIds.length ?? 0,
|
||||
unresolvedThreadCount:
|
||||
stateWithMutations.storyEngineMemory?.activeThreadIds.length ?? 0,
|
||||
});
|
||||
const saveMigrationManifest = buildSaveMigrationManifest({
|
||||
version: 'story-engine-v5',
|
||||
@@ -497,37 +519,41 @@ function applyStoryEngineEchoes(params: {
|
||||
simulationRunResults,
|
||||
},
|
||||
});
|
||||
const continueDigest = buildContinueGameDigest({
|
||||
state: {
|
||||
...stateWithMutations,
|
||||
chapterState,
|
||||
campaignState,
|
||||
storyEngineMemory: {
|
||||
...baseMemoryForQa,
|
||||
currentJourneyBeatId,
|
||||
narrativeQaReport,
|
||||
releaseGateReport,
|
||||
simulationRunResults,
|
||||
narrativeCodex,
|
||||
saveMigrationManifest,
|
||||
const continueDigest =
|
||||
buildContinueGameDigest({
|
||||
state: {
|
||||
...stateWithMutations,
|
||||
chapterState,
|
||||
campaignState,
|
||||
storyEngineMemory: {
|
||||
...baseMemoryForQa,
|
||||
currentJourneyBeatId,
|
||||
narrativeQaReport,
|
||||
releaseGateReport,
|
||||
simulationRunResults,
|
||||
narrativeCodex,
|
||||
saveMigrationManifest,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) + [
|
||||
epilogueSummary,
|
||||
replaySummary,
|
||||
telemetrySnapshot.summary,
|
||||
contentDiffReport.summary,
|
||||
`发布门禁:${releaseGateReport.status} / ${releaseGateReport.summary}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}) +
|
||||
[
|
||||
epilogueSummary,
|
||||
replaySummary,
|
||||
telemetrySnapshot.summary,
|
||||
contentDiffReport.summary,
|
||||
`发布门禁:${releaseGateReport.status} / ${releaseGateReport.summary}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
...stateWithMutations,
|
||||
chapterState,
|
||||
campaignState,
|
||||
activeScenarioPackId: activeScenarioPack?.id ?? stateWithMutations.activeScenarioPackId ?? null,
|
||||
activeCampaignPackId: activeCampaignPack?.id ?? stateWithMutations.activeCampaignPackId ?? null,
|
||||
activeScenarioPackId:
|
||||
activeScenarioPack?.id ?? stateWithMutations.activeScenarioPackId ?? null,
|
||||
activeCampaignPackId:
|
||||
activeCampaignPack?.id ?? stateWithMutations.activeCampaignPackId ?? null,
|
||||
storyEngineMemory: {
|
||||
...baseMemoryForQa,
|
||||
currentJourneyBeatId,
|
||||
@@ -604,14 +630,14 @@ export function createStoryProgressionActions({
|
||||
actionText,
|
||||
resultText,
|
||||
lastFunctionId,
|
||||
) => {
|
||||
const nextHistory = appendStoryHistory(gameState, actionText, resultText);
|
||||
) => {
|
||||
const nextHistory = appendStoryHistory(gameState, actionText, resultText);
|
||||
const stateWithHistory = applyStoryEngineEchoes({
|
||||
previousState: gameState,
|
||||
nextState: {
|
||||
...nextState,
|
||||
storyHistory: nextHistory,
|
||||
} as GameState,
|
||||
...nextState,
|
||||
storyHistory: nextHistory,
|
||||
} as GameState,
|
||||
actionText,
|
||||
lastFunctionId,
|
||||
});
|
||||
@@ -620,14 +646,14 @@ export function createStoryProgressionActions({
|
||||
setAiError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const nextStory = await generateStoryForState({
|
||||
state: stateWithHistory,
|
||||
try {
|
||||
const nextStory = await generateStoryForState({
|
||||
state: stateWithHistory,
|
||||
character,
|
||||
history: nextHistory,
|
||||
choice: actionText,
|
||||
lastFunctionId,
|
||||
});
|
||||
choice: actionText,
|
||||
lastFunctionId,
|
||||
});
|
||||
const recoveredState = applyStoryEngineEchoes({
|
||||
previousState: gameState,
|
||||
nextState: applyStoryReasoningRecovery(stateWithHistory),
|
||||
@@ -639,72 +665,91 @@ export function createStoryProgressionActions({
|
||||
} catch (error) {
|
||||
console.error('Failed to continue scripted story:', error);
|
||||
setAiError(error instanceof Error ? error.message : '未知智能生成错误');
|
||||
setCurrentStory(buildFallbackStoryForState(stateWithHistory, character, resultText));
|
||||
setCurrentStory(
|
||||
buildFallbackStoryForState(stateWithHistory, character, resultText),
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const commitGeneratedStateWithEncounterEntry: CommitGeneratedStateWithEncounterEntry = async (
|
||||
entryState,
|
||||
resolvedState,
|
||||
character,
|
||||
actionText,
|
||||
resultText,
|
||||
lastFunctionId,
|
||||
) => {
|
||||
setGameState(entryState);
|
||||
setAiError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
if (hasEncounterEntity(resolvedState)) {
|
||||
const runTicks = Math.max(1, Math.ceil(ENCOUNTER_ENTRY_DURATION_MS / ENCOUNTER_ENTRY_TICK_MS));
|
||||
const tickDurationMs = Math.max(1, Math.round(ENCOUNTER_ENTRY_DURATION_MS / runTicks));
|
||||
|
||||
for (let tick = 1; tick <= runTicks; tick += 1) {
|
||||
const progress = tick / runTicks;
|
||||
setGameState(interpolateEncounterTransitionState(entryState, resolvedState, progress));
|
||||
await new Promise(resolve => window.setTimeout(resolve, tickDurationMs));
|
||||
}
|
||||
}
|
||||
|
||||
const nextHistory = appendStoryHistory(gameState, actionText, resultText);
|
||||
const stateWithHistory = applyStoryEngineEchoes({
|
||||
previousState: gameState,
|
||||
nextState: {
|
||||
...resolvedState,
|
||||
storyHistory: nextHistory,
|
||||
} as GameState,
|
||||
const commitGeneratedStateWithEncounterEntry: CommitGeneratedStateWithEncounterEntry =
|
||||
async (
|
||||
entryState,
|
||||
resolvedState,
|
||||
character,
|
||||
actionText,
|
||||
resultText,
|
||||
lastFunctionId,
|
||||
});
|
||||
) => {
|
||||
setGameState(entryState);
|
||||
setAiError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
setGameState(stateWithHistory);
|
||||
if (hasEncounterEntity(resolvedState)) {
|
||||
const runTicks = Math.max(
|
||||
1,
|
||||
Math.ceil(ENCOUNTER_ENTRY_DURATION_MS / ENCOUNTER_ENTRY_TICK_MS),
|
||||
);
|
||||
const tickDurationMs = Math.max(
|
||||
1,
|
||||
Math.round(ENCOUNTER_ENTRY_DURATION_MS / runTicks),
|
||||
);
|
||||
|
||||
try {
|
||||
const nextStory = await generateStoryForState({
|
||||
state: stateWithHistory,
|
||||
character,
|
||||
history: nextHistory,
|
||||
choice: actionText,
|
||||
lastFunctionId,
|
||||
});
|
||||
const recoveredState = applyStoryEngineEchoes({
|
||||
for (let tick = 1; tick <= runTicks; tick += 1) {
|
||||
const progress = tick / runTicks;
|
||||
setGameState(
|
||||
interpolateEncounterTransitionState(
|
||||
entryState,
|
||||
resolvedState,
|
||||
progress,
|
||||
),
|
||||
);
|
||||
await new Promise((resolve) =>
|
||||
window.setTimeout(resolve, tickDurationMs),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const nextHistory = appendStoryHistory(gameState, actionText, resultText);
|
||||
const stateWithHistory = applyStoryEngineEchoes({
|
||||
previousState: gameState,
|
||||
nextState: applyStoryReasoningRecovery(stateWithHistory),
|
||||
nextState: {
|
||||
...resolvedState,
|
||||
storyHistory: nextHistory,
|
||||
} as GameState,
|
||||
actionText,
|
||||
lastFunctionId,
|
||||
});
|
||||
setGameState(recoveredState);
|
||||
setCurrentStory(nextStory);
|
||||
} catch (error) {
|
||||
console.error('Failed to continue encounter-entry story:', error);
|
||||
setAiError(error instanceof Error ? error.message : '未知智能生成错误');
|
||||
setCurrentStory(buildFallbackStoryForState(stateWithHistory, character, resultText));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
setGameState(stateWithHistory);
|
||||
|
||||
try {
|
||||
const nextStory = await generateStoryForState({
|
||||
state: stateWithHistory,
|
||||
character,
|
||||
history: nextHistory,
|
||||
choice: actionText,
|
||||
lastFunctionId,
|
||||
});
|
||||
const recoveredState = applyStoryEngineEchoes({
|
||||
previousState: gameState,
|
||||
nextState: applyStoryReasoningRecovery(stateWithHistory),
|
||||
actionText,
|
||||
lastFunctionId,
|
||||
});
|
||||
setGameState(recoveredState);
|
||||
setCurrentStory(nextStory);
|
||||
} catch (error) {
|
||||
console.error('Failed to continue encounter-entry story:', error);
|
||||
setAiError(error instanceof Error ? error.message : '未知智能生成错误');
|
||||
setCurrentStory(
|
||||
buildFallbackStoryForState(stateWithHistory, character, resultText),
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
commitGeneratedState,
|
||||
|
||||
@@ -9,23 +9,43 @@ import {
|
||||
} from '../data/characterPresets';
|
||||
import { setRuntimeCustomWorldProfile } from '../data/customWorldRuntime';
|
||||
import { getInitialPlayerCurrency } from '../data/economy';
|
||||
import { applyEquipmentLoadoutToState, buildInitialEquipmentLoadout, createEmptyEquipmentLoadout } from '../data/equipmentEffects';
|
||||
import { buildInitialNpcState, buildInitialPlayerInventory } from '../data/npcInteractions';
|
||||
import {
|
||||
applyEquipmentLoadoutToState,
|
||||
buildInitialEquipmentLoadout,
|
||||
createEmptyEquipmentLoadout,
|
||||
} from '../data/equipmentEffects';
|
||||
import {
|
||||
buildInitialNpcState,
|
||||
buildInitialPlayerInventory,
|
||||
} from '../data/npcInteractions';
|
||||
import { createInitialPlayerProgressionState } from '../data/playerProgression';
|
||||
import { createInitialGameRuntimeStats } from '../data/runtimeStats';
|
||||
import { ensureSceneEncounterPreview,RESOLVED_ENTITY_X_METERS } from '../data/sceneEncounterPreviews';
|
||||
import { getScenePreset,getWorldCampScenePreset } from '../data/scenePresets';
|
||||
import {
|
||||
ensureSceneEncounterPreview,
|
||||
RESOLVED_ENTITY_X_METERS,
|
||||
} from '../data/sceneEncounterPreviews';
|
||||
import { getScenePreset, getWorldCampScenePreset } from '../data/scenePresets';
|
||||
import { createEmptyStoryEngineMemoryState } from '../services/storyEngine/visibilityEngine';
|
||||
import { AnimationState, Character, CustomWorldProfile, Encounter, EquipmentLoadout, GameState, InventoryItem, SceneNpc, WorldType } from '../types';
|
||||
import {
|
||||
AnimationState,
|
||||
Character,
|
||||
CustomWorldProfile,
|
||||
Encounter,
|
||||
EquipmentLoadout,
|
||||
GameState,
|
||||
InventoryItem,
|
||||
SceneNpc,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import type { BottomTab } from '../types/navigation';
|
||||
|
||||
const PLAYER_BASE_MAX_HP = 180;
|
||||
|
||||
export type {BottomTab} from '../types/navigation';
|
||||
export type { BottomTab } from '../types/navigation';
|
||||
|
||||
function mergeStarterInventoryItems<T extends { category: string; name: string }>(
|
||||
explicitItems: T[],
|
||||
fallbackItems: T[],
|
||||
) {
|
||||
function mergeStarterInventoryItems<
|
||||
T extends { category: string; name: string },
|
||||
>(explicitItems: T[], fallbackItems: T[]) {
|
||||
const merged = new Map<string, T>();
|
||||
|
||||
[...explicitItems, ...fallbackItems].forEach((item) => {
|
||||
@@ -117,13 +137,15 @@ function createInitialCampEncounter(
|
||||
): Encounter | null {
|
||||
if (!worldType) return null;
|
||||
|
||||
const campScenePreset = getWorldCampScenePreset(worldType) ?? getScenePreset(worldType, 0);
|
||||
const campScenePreset =
|
||||
getWorldCampScenePreset(worldType) ?? getScenePreset(worldType, 0);
|
||||
const npcCandidates = (campScenePreset?.npcs ?? [])
|
||||
.filter((npc: SceneNpc) => Boolean(npc.characterId))
|
||||
.filter((npc: SceneNpc) => npc.characterId !== playerCharacter.id);
|
||||
if (npcCandidates.length === 0) return null;
|
||||
|
||||
const npc = npcCandidates[Math.floor(Math.random() * npcCandidates.length)] ?? null;
|
||||
const npc =
|
||||
npcCandidates[Math.floor(Math.random() * npcCandidates.length)] ?? null;
|
||||
if (!npc) return null;
|
||||
|
||||
return {
|
||||
@@ -145,6 +167,7 @@ function createInitialGameState(): GameState {
|
||||
customWorldProfile: null,
|
||||
playerCharacter: null,
|
||||
runtimeStats: createInitialGameRuntimeStats(),
|
||||
playerProgression: createInitialPlayerProgressionState(),
|
||||
currentScene: 'Selection',
|
||||
storyHistory: [],
|
||||
storyEngineMemory: createEmptyStoryEngineMemoryState(),
|
||||
@@ -191,14 +214,18 @@ function createInitialGameState(): GameState {
|
||||
}
|
||||
|
||||
export function useGameFlow() {
|
||||
const [gameState, setGameState] = useState<GameState>(() => createInitialGameState());
|
||||
const [gameState, setGameState] = useState<GameState>(() =>
|
||||
createInitialGameState(),
|
||||
);
|
||||
const [bottomTab, setBottomTab] = useState<BottomTab>('adventure');
|
||||
const [isMapOpen, setIsMapOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setRuntimeCustomWorldProfile(gameState.customWorldProfile);
|
||||
setRuntimeCharacterOverrides(
|
||||
gameState.customWorldProfile ? buildCustomWorldRuntimeCharacters(gameState.customWorldProfile) : null,
|
||||
gameState.customWorldProfile
|
||||
? buildCustomWorldRuntimeCharacters(gameState.customWorldProfile)
|
||||
: null,
|
||||
);
|
||||
}, [gameState.customWorldProfile]);
|
||||
|
||||
@@ -216,7 +243,7 @@ export function useGameFlow() {
|
||||
);
|
||||
const initialScenePreset = getScenePreset(resolvedWorldType, 0) ?? null;
|
||||
setIsMapOpen(false);
|
||||
setGameState(prev =>
|
||||
setGameState((prev) =>
|
||||
ensureSceneEncounterPreview({
|
||||
...prev,
|
||||
worldType: resolvedWorldType,
|
||||
@@ -225,6 +252,7 @@ export function useGameFlow() {
|
||||
sceneHostileNpcs: [],
|
||||
currentEncounter: null,
|
||||
npcInteractionActive: false,
|
||||
playerProgression: createInitialPlayerProgressionState(),
|
||||
storyEngineMemory: createEmptyStoryEngineMemoryState(),
|
||||
chapterState: null,
|
||||
campaignState: null,
|
||||
@@ -257,110 +285,114 @@ export function useGameFlow() {
|
||||
setBottomTab('adventure');
|
||||
setIsMapOpen(false);
|
||||
|
||||
setGameState(prev =>
|
||||
{
|
||||
const resolvedWorldType = prev.worldType;
|
||||
const resolvedCustomWorldProfile = prev.customWorldProfile;
|
||||
const initialScenePreset = resolvedWorldType
|
||||
? getWorldCampScenePreset(resolvedWorldType) ?? getScenePreset(resolvedWorldType, 0)
|
||||
: null;
|
||||
const initialEncounter = createInitialCampEncounter(
|
||||
resolvedWorldType,
|
||||
character,
|
||||
);
|
||||
const initialNpcState = initialEncounter
|
||||
? buildInitialNpcState(initialEncounter, resolvedWorldType, prev)
|
||||
: null;
|
||||
const initialEquipment = buildInitialEquipmentLoadout(
|
||||
character,
|
||||
resolvedCustomWorldProfile,
|
||||
);
|
||||
const explicitStarterItems =
|
||||
resolvedWorldType === WorldType.CUSTOM
|
||||
? buildExplicitCustomWorldRoleStarterState(
|
||||
resolvedCustomWorldProfile!,
|
||||
character,
|
||||
)
|
||||
: null;
|
||||
const mergedStarterEquipment = {
|
||||
weapon:
|
||||
explicitStarterItems?.equipment.weapon ?? initialEquipment.weapon,
|
||||
armor:
|
||||
explicitStarterItems?.equipment.armor ?? initialEquipment.armor,
|
||||
relic:
|
||||
explicitStarterItems?.equipment.relic ?? initialEquipment.relic,
|
||||
};
|
||||
const playerMaxHp = getCharacterMaxHp(
|
||||
character,
|
||||
resolvedWorldType,
|
||||
resolvedCustomWorldProfile,
|
||||
);
|
||||
|
||||
return ensureSceneEncounterPreview(
|
||||
applyEquipmentLoadoutToState({
|
||||
...prev,
|
||||
playerCharacter: character,
|
||||
runtimeStats: createInitialGameRuntimeStats({ isActiveRun: true }),
|
||||
currentScene: 'Story',
|
||||
storyHistory: [],
|
||||
storyEngineMemory: createEmptyStoryEngineMemoryState(),
|
||||
chapterState: null,
|
||||
campaignState: null,
|
||||
activeScenarioPackId: gameState.customWorldProfile?.scenarioPackId ?? null,
|
||||
activeCampaignPackId: gameState.customWorldProfile?.campaignPackId ?? null,
|
||||
characterChats: {},
|
||||
currentEncounter: initialEncounter,
|
||||
npcInteractionActive: false,
|
||||
currentScenePreset: initialScenePreset,
|
||||
lastObserveSignsSceneId: null,
|
||||
lastObserveSignsReport: null,
|
||||
animationState: AnimationState.IDLE,
|
||||
sceneHostileNpcs: [],
|
||||
playerX: 0,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'right',
|
||||
playerActionMode: 'idle',
|
||||
scrollWorld: false,
|
||||
inBattle: false,
|
||||
playerHp: playerMaxHp,
|
||||
playerMaxHp: playerMaxHp,
|
||||
playerMana: getCharacterMaxMana(character),
|
||||
playerMaxMana: getCharacterMaxMana(character),
|
||||
playerSkillCooldowns: createCharacterSkillCooldowns(character),
|
||||
activeBuildBuffs: [],
|
||||
activeCombatEffects: [],
|
||||
playerCurrency: getInitialPlayerCurrency(
|
||||
resolvedWorldType,
|
||||
resolvedCustomWorldProfile,
|
||||
),
|
||||
playerInventory: mergeStarterInventoryItems(
|
||||
explicitStarterItems?.inventory ?? [],
|
||||
buildInitialPlayerInventory(
|
||||
setGameState((prev) => {
|
||||
const resolvedWorldType = prev.worldType;
|
||||
const resolvedCustomWorldProfile = prev.customWorldProfile;
|
||||
const initialScenePreset = resolvedWorldType
|
||||
? (getWorldCampScenePreset(resolvedWorldType) ??
|
||||
getScenePreset(resolvedWorldType, 0))
|
||||
: null;
|
||||
const initialEncounter = createInitialCampEncounter(
|
||||
resolvedWorldType,
|
||||
character,
|
||||
);
|
||||
const initialNpcState = initialEncounter
|
||||
? buildInitialNpcState(initialEncounter, resolvedWorldType, prev)
|
||||
: null;
|
||||
const initialEquipment = buildInitialEquipmentLoadout(
|
||||
character,
|
||||
resolvedCustomWorldProfile,
|
||||
);
|
||||
const explicitStarterItems =
|
||||
resolvedWorldType === WorldType.CUSTOM
|
||||
? buildExplicitCustomWorldRoleStarterState(
|
||||
resolvedCustomWorldProfile!,
|
||||
character,
|
||||
)
|
||||
: null;
|
||||
const mergedStarterEquipment = {
|
||||
weapon:
|
||||
explicitStarterItems?.equipment.weapon ?? initialEquipment.weapon,
|
||||
armor: explicitStarterItems?.equipment.armor ?? initialEquipment.armor,
|
||||
relic: explicitStarterItems?.equipment.relic ?? initialEquipment.relic,
|
||||
};
|
||||
const playerMaxHp = getCharacterMaxHp(
|
||||
character,
|
||||
resolvedWorldType,
|
||||
resolvedCustomWorldProfile,
|
||||
);
|
||||
|
||||
return ensureSceneEncounterPreview(
|
||||
applyEquipmentLoadoutToState(
|
||||
{
|
||||
...prev,
|
||||
playerCharacter: character,
|
||||
runtimeStats: createInitialGameRuntimeStats({ isActiveRun: true }),
|
||||
playerProgression: createInitialPlayerProgressionState(),
|
||||
currentScene: 'Story',
|
||||
storyHistory: [],
|
||||
storyEngineMemory: createEmptyStoryEngineMemoryState(),
|
||||
chapterState: null,
|
||||
campaignState: null,
|
||||
activeScenarioPackId:
|
||||
gameState.customWorldProfile?.scenarioPackId ?? null,
|
||||
activeCampaignPackId:
|
||||
gameState.customWorldProfile?.campaignPackId ?? null,
|
||||
characterChats: {},
|
||||
currentEncounter: initialEncounter,
|
||||
npcInteractionActive: false,
|
||||
currentScenePreset: initialScenePreset,
|
||||
lastObserveSignsSceneId: null,
|
||||
lastObserveSignsReport: null,
|
||||
animationState: AnimationState.IDLE,
|
||||
sceneHostileNpcs: [],
|
||||
playerX: 0,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'right',
|
||||
playerActionMode: 'idle',
|
||||
scrollWorld: false,
|
||||
inBattle: false,
|
||||
playerHp: playerMaxHp,
|
||||
playerMaxHp: playerMaxHp,
|
||||
playerMana: getCharacterMaxMana(character),
|
||||
playerMaxMana: getCharacterMaxMana(character),
|
||||
playerSkillCooldowns: createCharacterSkillCooldowns(character),
|
||||
activeBuildBuffs: [],
|
||||
activeCombatEffects: [],
|
||||
playerCurrency: getInitialPlayerCurrency(
|
||||
resolvedWorldType,
|
||||
resolvedCustomWorldProfile,
|
||||
),
|
||||
),
|
||||
playerEquipment: createEmptyEquipmentLoadout(),
|
||||
npcStates: initialEncounter && initialNpcState
|
||||
? {
|
||||
[initialEncounter.id!]: initialNpcState,
|
||||
}
|
||||
: {},
|
||||
quests: [],
|
||||
roster: [],
|
||||
companions: [],
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
sparReturnEncounter: null,
|
||||
sparPlayerHpBefore: null,
|
||||
sparPlayerMaxHpBefore: null,
|
||||
sparStoryHistoryBefore: null,
|
||||
}, mergedStarterEquipment),
|
||||
);
|
||||
},
|
||||
);
|
||||
playerInventory: mergeStarterInventoryItems<InventoryItem>(
|
||||
explicitStarterItems?.inventory ?? [],
|
||||
buildInitialPlayerInventory(
|
||||
character,
|
||||
resolvedWorldType,
|
||||
resolvedCustomWorldProfile,
|
||||
),
|
||||
),
|
||||
playerEquipment: createEmptyEquipmentLoadout(),
|
||||
npcStates:
|
||||
initialEncounter && initialNpcState
|
||||
? {
|
||||
[initialEncounter.id!]: initialNpcState,
|
||||
}
|
||||
: {},
|
||||
quests: [],
|
||||
roster: [],
|
||||
companions: [],
|
||||
currentBattleNpcId: null,
|
||||
currentNpcBattleMode: null,
|
||||
currentNpcBattleOutcome: null,
|
||||
sparReturnEncounter: null,
|
||||
sparPlayerHpBefore: null,
|
||||
sparPlayerMaxHpBefore: null,
|
||||
sparStoryHistoryBefore: null,
|
||||
},
|
||||
mergedStarterEquipment,
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user