170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
import type {
|
|
GameState,
|
|
InventoryItem,
|
|
QuestLogEntry,
|
|
StorySignal,
|
|
ThreadContract,
|
|
} from '../../types';
|
|
|
|
function dedupeStrings(values: Array<string | null | undefined>, limit = 12) {
|
|
return [...new Set(values.map((value) => value?.trim() ?? '').filter(Boolean))]
|
|
.slice(0, limit);
|
|
}
|
|
|
|
function createSignalId(prefix: string, key: string) {
|
|
return `${prefix}:${key}`;
|
|
}
|
|
|
|
export function collectStorySignals(params: {
|
|
prevState: GameState;
|
|
nextState: GameState;
|
|
actionText: string;
|
|
lastFunctionId?: string | null;
|
|
rewardItems?: InventoryItem[];
|
|
}) {
|
|
const signals: StorySignal[] = [];
|
|
const activeThreadIds = params.nextState.storyEngineMemory?.activeThreadIds ?? [];
|
|
|
|
if (params.prevState.currentScenePreset?.id !== params.nextState.currentScenePreset?.id) {
|
|
if (params.prevState.currentScenePreset?.id) {
|
|
signals.push({
|
|
id: createSignalId('leave_scene', params.prevState.currentScenePreset.id),
|
|
signalType: 'leave_scene',
|
|
sceneId: params.prevState.currentScenePreset.id,
|
|
threadIds: activeThreadIds,
|
|
});
|
|
}
|
|
if (params.nextState.currentScenePreset?.id) {
|
|
signals.push({
|
|
id: createSignalId('enter_scene', params.nextState.currentScenePreset.id),
|
|
signalType: 'enter_scene',
|
|
sceneId: params.nextState.currentScenePreset.id,
|
|
threadIds: activeThreadIds,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (params.lastFunctionId === 'idle_observe_signs') {
|
|
signals.push({
|
|
id: createSignalId('inspect_scene', params.nextState.currentScenePreset?.id ?? 'scene'),
|
|
signalType: 'inspect_scene',
|
|
sceneId: params.nextState.currentScenePreset?.id ?? null,
|
|
threadIds: activeThreadIds,
|
|
});
|
|
}
|
|
if (params.nextState.currentEncounter?.kind === 'npc' || /聊|问|试探/u.test(params.actionText)) {
|
|
signals.push({
|
|
id: createSignalId(
|
|
'talk_to_actor',
|
|
params.nextState.currentEncounter?.id
|
|
?? params.prevState.currentEncounter?.id
|
|
?? params.actionText,
|
|
),
|
|
signalType: 'talk_to_actor',
|
|
actorId:
|
|
params.nextState.currentEncounter?.id
|
|
?? params.prevState.currentEncounter?.id
|
|
?? null,
|
|
threadIds: activeThreadIds,
|
|
});
|
|
}
|
|
if (params.lastFunctionId === 'npc_gift') {
|
|
signals.push({
|
|
id: createSignalId('give_item', params.actionText),
|
|
signalType: 'give_item',
|
|
actorId:
|
|
params.prevState.currentEncounter?.id
|
|
?? params.nextState.currentEncounter?.id
|
|
?? null,
|
|
threadIds: activeThreadIds,
|
|
});
|
|
}
|
|
if (params.lastFunctionId === 'npc_quest_accept') {
|
|
signals.push({
|
|
id: createSignalId('accept_contract', params.actionText),
|
|
signalType: 'accept_contract',
|
|
actorId:
|
|
params.prevState.currentEncounter?.id
|
|
?? params.nextState.currentEncounter?.id
|
|
?? null,
|
|
threadIds: activeThreadIds,
|
|
});
|
|
}
|
|
if ((params.rewardItems ?? []).length > 0) {
|
|
params.rewardItems!.forEach((item) => {
|
|
const threadIds = item.runtimeMetadata?.storyFingerprint?.relatedThreadIds ?? activeThreadIds;
|
|
signals.push({
|
|
id: createSignalId('obtain_carrier', item.id),
|
|
signalType: 'obtain_carrier',
|
|
carrierId: item.id,
|
|
threadIds,
|
|
});
|
|
});
|
|
}
|
|
|
|
return signals;
|
|
}
|
|
|
|
function updateQuestFromSignals(
|
|
quest: QuestLogEntry,
|
|
signals: StorySignal[],
|
|
contracts: ThreadContract[],
|
|
) {
|
|
const relevantSignals = signals.filter((signal) =>
|
|
(quest.threadId && signal.threadIds?.includes(quest.threadId))
|
|
|| (quest.contractId && contracts.some((contract) => contract.id === quest.contractId)),
|
|
);
|
|
if (relevantSignals.length <= 0) {
|
|
return quest;
|
|
}
|
|
|
|
const relatedCarrierIds = dedupeStrings([
|
|
...(quest.relatedCarrierIds ?? []),
|
|
...relevantSignals.map((signal) => signal.carrierId ?? ''),
|
|
], 8);
|
|
const discoveredFactIds = dedupeStrings([
|
|
...(quest.discoveredFactIds ?? []),
|
|
...relevantSignals.flatMap((signal) => signal.threadIds ?? []),
|
|
], 12);
|
|
|
|
return {
|
|
...quest,
|
|
visibleStage: Math.min(
|
|
(quest.visibleStage ?? 0) + relevantSignals.length,
|
|
Math.max(1, quest.steps?.length ?? 1),
|
|
),
|
|
relatedCarrierIds,
|
|
discoveredFactIds,
|
|
};
|
|
}
|
|
|
|
export function resolveSignalsToThreadUpdates(params: {
|
|
state: GameState;
|
|
signals: StorySignal[];
|
|
contracts?: ThreadContract[] | null;
|
|
}) {
|
|
const storyEngineMemory = params.state.storyEngineMemory;
|
|
if (!storyEngineMemory || params.signals.length <= 0) {
|
|
return params.state;
|
|
}
|
|
|
|
const contracts = params.contracts ?? [];
|
|
return {
|
|
...params.state,
|
|
storyEngineMemory: {
|
|
...storyEngineMemory,
|
|
activeThreadIds: dedupeStrings([
|
|
...storyEngineMemory.activeThreadIds,
|
|
...params.signals.flatMap((signal) => signal.threadIds ?? []),
|
|
], 8),
|
|
recentSignalIds: dedupeStrings([
|
|
...(storyEngineMemory.recentSignalIds ?? []),
|
|
...params.signals.map((signal) => signal.id),
|
|
], 12),
|
|
},
|
|
quests: params.state.quests.map((quest) =>
|
|
updateQuestFromSignals(quest, params.signals, contracts),
|
|
),
|
|
};
|
|
}
|