Files
Genarrative/src/hooks/useStoryOptions.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

113 lines
2.8 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { sortStoryOptionsByPriority } from '../data/stateFunctions';
import { annotateStoryOptionsWithGoalAffordance } from '../services/storyEngine/goalDirector';
import type { GoalStackState, StoryMoment } from '../types';
const OPTION_PAGE_SIZE = 3;
export function useStoryOptions(
currentStory: StoryMoment | null,
goalStack?: GoalStackState | null,
) {
const [optionWindowStart, setOptionWindowStart] = useState(0);
const activeOptionPool = useMemo(() => {
if (!currentStory) {
return [];
}
return sortStoryOptionsByPriority(
annotateStoryOptionsWithGoalAffordance(
currentStory.options,
goalStack,
),
);
}, [currentStory, goalStack]);
const optionPoolSignature = useMemo(
() =>
activeOptionPool
.map((option) =>
[
option.functionId,
option.actionText,
option.text ?? '',
option.goalAffordance?.goalId ?? '',
option.goalAffordance?.relation ?? '',
].join('::'),
)
.join('||'),
[activeOptionPool],
);
useEffect(() => {
setOptionWindowStart(0);
}, [currentStory, optionPoolSignature]);
const displayedOptions = useMemo(
() => {
const windowOptions = activeOptionPool.slice(
optionWindowStart,
optionWindowStart + OPTION_PAGE_SIZE,
);
if (
windowOptions.some(
(option) => option.goalAffordance?.relation === 'advance',
)
) {
return windowOptions;
}
const pinnedAdvanceOption =
activeOptionPool.find(
(option) => option.goalAffordance?.relation === 'advance',
) ?? null;
if (!pinnedAdvanceOption) {
return windowOptions;
}
return [
pinnedAdvanceOption,
...windowOptions
.filter(
(option) =>
!(
option.functionId === pinnedAdvanceOption.functionId
&& option.actionText === pinnedAdvanceOption.actionText
),
)
.slice(0, OPTION_PAGE_SIZE - 1),
];
},
[activeOptionPool, optionWindowStart],
);
const canRefreshOptions = activeOptionPool.length > OPTION_PAGE_SIZE;
const handleRefreshOptions = useCallback(() => {
if (activeOptionPool.length <= OPTION_PAGE_SIZE) return;
const nextStart = optionWindowStart + OPTION_PAGE_SIZE;
if (nextStart < activeOptionPool.length) {
setOptionWindowStart(nextStart);
return;
}
setOptionWindowStart(0);
}, [activeOptionPool.length, optionWindowStart]);
const resetStoryOptions = useCallback(() => {
setOptionWindowStart(0);
}, []);
return {
activeOptionPool,
displayedOptions,
optionWindowStart,
canRefreshOptions,
handleRefreshOptions,
resetStoryOptions,
};
}