创作数据流程收束
This commit is contained in:
@@ -1,249 +0,0 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import { applyStoryReasoningRecovery } from '../../data/storyRecovery';
|
||||
import type { Character, Encounter, GameState, StoryMoment, StoryOption } from '../../types';
|
||||
import {
|
||||
playOpeningAdventureSequence,
|
||||
type PreparedOpeningAdventure,
|
||||
} from './openingAdventure';
|
||||
|
||||
type BuildFallbackStoryForState = (
|
||||
state: GameState,
|
||||
character: Character,
|
||||
fallbackText?: string,
|
||||
) => StoryMoment;
|
||||
|
||||
type GenerateStoryForState = (params: {
|
||||
state: GameState;
|
||||
character: Character;
|
||||
history: StoryMoment[];
|
||||
choice?: string;
|
||||
lastFunctionId?: string | null;
|
||||
optionCatalog?: StoryOption[] | null;
|
||||
}) => Promise<StoryMoment>;
|
||||
|
||||
type BuildDialogueStoryMoment = (
|
||||
npcName: string,
|
||||
text: string,
|
||||
options: StoryOption[],
|
||||
streaming?: boolean,
|
||||
) => StoryMoment;
|
||||
|
||||
export function useStoryBootstrap(params: {
|
||||
gameState: GameState;
|
||||
currentStory: StoryMoment | null;
|
||||
isLoading: boolean;
|
||||
setGameState: Dispatch<SetStateAction<GameState>>;
|
||||
setCurrentStory: Dispatch<SetStateAction<StoryMoment | null>>;
|
||||
setAiError: Dispatch<SetStateAction<string | null>>;
|
||||
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||
prepareOpeningAdventure: (
|
||||
state: GameState,
|
||||
character: Character,
|
||||
) => PreparedOpeningAdventure | null;
|
||||
getNpcEncounterKey: (encounter: Encounter) => string;
|
||||
buildFallbackStoryForState: BuildFallbackStoryForState;
|
||||
generateStoryForState: GenerateStoryForState;
|
||||
buildDialogueStoryMoment: BuildDialogueStoryMoment;
|
||||
buildStoryContextFromState: (
|
||||
state: GameState,
|
||||
extras?: {
|
||||
lastFunctionId?: string | null;
|
||||
openingCampBackground?: string | null;
|
||||
openingCampDialogue?: string | null;
|
||||
encounterNpcStateOverride?: GameState['npcStates'][string] | null;
|
||||
},
|
||||
) => StoryGenerationContext;
|
||||
getStoryGenerationHostileNpcs: (
|
||||
state: GameState,
|
||||
) => GameState['sceneHostileNpcs'];
|
||||
hasRenderableDialogueTurns: (text: string, npcName: string) => boolean;
|
||||
inferOpeningCampFollowupOptions: (
|
||||
state: GameState,
|
||||
character: Character,
|
||||
baseOptions: StoryOption[],
|
||||
openingBackground: string,
|
||||
openingDialogue: string,
|
||||
) => Promise<StoryOption[]>;
|
||||
getTypewriterDelay: (char: string) => number;
|
||||
isNpcEncounter: (
|
||||
encounter: GameState['currentEncounter'],
|
||||
) => encounter is Encounter;
|
||||
isInitialCompanionEncounter: (
|
||||
encounter: GameState['currentEncounter'],
|
||||
) => encounter is Encounter;
|
||||
}) {
|
||||
const {
|
||||
gameState,
|
||||
currentStory,
|
||||
isLoading,
|
||||
setGameState,
|
||||
setCurrentStory,
|
||||
setAiError,
|
||||
setIsLoading,
|
||||
prepareOpeningAdventure,
|
||||
getNpcEncounterKey,
|
||||
buildFallbackStoryForState,
|
||||
generateStoryForState,
|
||||
buildDialogueStoryMoment,
|
||||
buildStoryContextFromState,
|
||||
getStoryGenerationHostileNpcs,
|
||||
hasRenderableDialogueTurns,
|
||||
inferOpeningCampFollowupOptions,
|
||||
getTypewriterDelay,
|
||||
isNpcEncounter,
|
||||
isInitialCompanionEncounter,
|
||||
} = params;
|
||||
|
||||
const [preparedOpeningAdventure, setPreparedOpeningAdventure] =
|
||||
useState<PreparedOpeningAdventure | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
gameState.currentScene !== 'Story' ||
|
||||
!gameState.playerCharacter ||
|
||||
gameState.storyHistory.length > 0 ||
|
||||
currentStory ||
|
||||
!isNpcEncounter(gameState.currentEncounter) ||
|
||||
gameState.currentEncounter.specialBehavior !== 'initial_companion'
|
||||
) {
|
||||
setPreparedOpeningAdventure(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setPreparedOpeningAdventure(
|
||||
prepareOpeningAdventure(gameState, gameState.playerCharacter),
|
||||
);
|
||||
}, [
|
||||
currentStory,
|
||||
gameState,
|
||||
isNpcEncounter,
|
||||
prepareOpeningAdventure,
|
||||
]);
|
||||
|
||||
const startOpeningAdventure = useCallback(async () => {
|
||||
if (
|
||||
!gameState.playerCharacter ||
|
||||
!isNpcEncounter(gameState.currentEncounter)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const encounter = gameState.currentEncounter;
|
||||
if (encounter.specialBehavior !== 'initial_companion') {
|
||||
return;
|
||||
}
|
||||
|
||||
const preparedStory =
|
||||
preparedOpeningAdventure?.encounterKey === getNpcEncounterKey(encounter)
|
||||
? preparedOpeningAdventure
|
||||
: prepareOpeningAdventure(gameState, gameState.playerCharacter);
|
||||
|
||||
if (!preparedStory) {
|
||||
return;
|
||||
}
|
||||
|
||||
await playOpeningAdventureSequence({
|
||||
gameState,
|
||||
character: gameState.playerCharacter,
|
||||
encounter,
|
||||
preparedStory,
|
||||
setGameState,
|
||||
setCurrentStory,
|
||||
setAiError,
|
||||
setIsLoading,
|
||||
buildDialogueStoryMoment,
|
||||
buildStoryContextFromState,
|
||||
getStoryGenerationHostileNpcs,
|
||||
hasRenderableDialogueTurns,
|
||||
inferOpeningCampFollowupOptions,
|
||||
getTypewriterDelay,
|
||||
});
|
||||
}, [
|
||||
buildDialogueStoryMoment,
|
||||
buildStoryContextFromState,
|
||||
gameState,
|
||||
getNpcEncounterKey,
|
||||
getStoryGenerationHostileNpcs,
|
||||
getTypewriterDelay,
|
||||
hasRenderableDialogueTurns,
|
||||
inferOpeningCampFollowupOptions,
|
||||
isNpcEncounter,
|
||||
prepareOpeningAdventure,
|
||||
preparedOpeningAdventure,
|
||||
setAiError,
|
||||
setCurrentStory,
|
||||
setGameState,
|
||||
setIsLoading,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const startStory = async () => {
|
||||
if (
|
||||
gameState.currentScene !== 'Story' ||
|
||||
!gameState.worldType ||
|
||||
!gameState.playerCharacter ||
|
||||
currentStory ||
|
||||
isLoading
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
gameState.storyHistory.length === 0 &&
|
||||
isInitialCompanionEncounter(gameState.currentEncounter) &&
|
||||
!gameState.npcInteractionActive
|
||||
) {
|
||||
setAiError(null);
|
||||
void startOpeningAdventure();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
setAiError(null);
|
||||
const nextStory = await generateStoryForState({
|
||||
state: gameState,
|
||||
character: gameState.playerCharacter,
|
||||
history: [],
|
||||
});
|
||||
setGameState(applyStoryReasoningRecovery(gameState));
|
||||
setCurrentStory(nextStory);
|
||||
} catch (error) {
|
||||
console.error('Failed to start story:', error);
|
||||
setAiError(error instanceof Error ? error.message : '未知智能生成错误');
|
||||
setCurrentStory(
|
||||
buildFallbackStoryForState(gameState, gameState.playerCharacter),
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void startStory();
|
||||
}, [
|
||||
buildFallbackStoryForState,
|
||||
currentStory,
|
||||
gameState,
|
||||
generateStoryForState,
|
||||
isInitialCompanionEncounter,
|
||||
isLoading,
|
||||
setAiError,
|
||||
setCurrentStory,
|
||||
setGameState,
|
||||
setIsLoading,
|
||||
startOpeningAdventure,
|
||||
]);
|
||||
|
||||
const resetPreparedOpeningAdventure = useCallback(() => {
|
||||
setPreparedOpeningAdventure(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
preparedOpeningAdventure,
|
||||
startOpeningAdventure,
|
||||
resetPreparedOpeningAdventure,
|
||||
};
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
applyEquipmentLoadoutToState,
|
||||
getEquipmentSlotFromItem,
|
||||
getEquipmentSlotLabel,
|
||||
} from '../data/equipmentEffects';
|
||||
import {
|
||||
EQUIPMENT_EQUIP_FUNCTION,
|
||||
EQUIPMENT_UNEQUIP_FUNCTION,
|
||||
} from '../data/functionCatalog';
|
||||
import {
|
||||
addInventoryItems,
|
||||
removeInventoryItem,
|
||||
} from '../data/npcInteractions';
|
||||
import { EquipmentSlotId, GameState, InventoryItem } from '../types';
|
||||
import type { CommitGeneratedState } from './generatedState';
|
||||
|
||||
function normalizeEquippedItem(item: InventoryItem) {
|
||||
return {
|
||||
...item,
|
||||
quantity: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function buildEquipResultText(
|
||||
item: InventoryItem,
|
||||
slot: EquipmentSlotId,
|
||||
replacedItem?: InventoryItem | null,
|
||||
) {
|
||||
return replacedItem
|
||||
? `你将${replacedItem.name}从${getEquipmentSlotLabel(slot)}位上换下,改为装备${item.name}。`
|
||||
: `你将${item.name}装备在${getEquipmentSlotLabel(slot)}位上。`;
|
||||
}
|
||||
|
||||
function buildUnequipResultText(item: InventoryItem) {
|
||||
return `你卸下了${item.name},暂时收回背包。`;
|
||||
}
|
||||
|
||||
export function useEquipmentFlow({
|
||||
gameState,
|
||||
commitGeneratedState,
|
||||
}: {
|
||||
gameState: GameState;
|
||||
commitGeneratedState: CommitGeneratedState;
|
||||
}) {
|
||||
const handleEquipInventoryItem = useCallback(
|
||||
async (itemId: string) => {
|
||||
if (!gameState.playerCharacter || gameState.inBattle) return false;
|
||||
|
||||
const item = gameState.playerInventory.find(
|
||||
(candidate) => candidate.id === itemId,
|
||||
);
|
||||
if (!item || item.quantity <= 0) return false;
|
||||
|
||||
const slot = getEquipmentSlotFromItem(item);
|
||||
if (!slot) return false;
|
||||
|
||||
const replacedItem = gameState.playerEquipment[slot];
|
||||
const nextEquipment = {
|
||||
...gameState.playerEquipment,
|
||||
[slot]: normalizeEquippedItem(item),
|
||||
};
|
||||
|
||||
let nextInventory = removeInventoryItem(
|
||||
gameState.playerInventory,
|
||||
item.id,
|
||||
1,
|
||||
);
|
||||
if (replacedItem) {
|
||||
nextInventory = addInventoryItems(nextInventory, [replacedItem]);
|
||||
}
|
||||
|
||||
const nextState = applyEquipmentLoadoutToState(
|
||||
{
|
||||
...gameState,
|
||||
playerInventory: nextInventory,
|
||||
},
|
||||
nextEquipment,
|
||||
);
|
||||
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
`装备${item.name}`,
|
||||
buildEquipResultText(item, slot, replacedItem),
|
||||
EQUIPMENT_EQUIP_FUNCTION.id,
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
[commitGeneratedState, gameState],
|
||||
);
|
||||
|
||||
const handleUnequipItem = useCallback(
|
||||
async (slot: EquipmentSlotId) => {
|
||||
if (!gameState.playerCharacter || gameState.inBattle) return false;
|
||||
|
||||
const equippedItem = gameState.playerEquipment[slot];
|
||||
if (!equippedItem) return false;
|
||||
|
||||
const nextEquipment = {
|
||||
...gameState.playerEquipment,
|
||||
[slot]: null,
|
||||
};
|
||||
const nextState = applyEquipmentLoadoutToState(
|
||||
{
|
||||
...gameState,
|
||||
playerInventory: addInventoryItems(gameState.playerInventory, [
|
||||
equippedItem,
|
||||
]),
|
||||
},
|
||||
nextEquipment,
|
||||
);
|
||||
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
`卸下${equippedItem.name}`,
|
||||
buildUnequipResultText(equippedItem),
|
||||
EQUIPMENT_UNEQUIP_FUNCTION.id,
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
[commitGeneratedState, gameState],
|
||||
);
|
||||
|
||||
return {
|
||||
handleEquipInventoryItem,
|
||||
handleUnequipItem,
|
||||
};
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
buildForgeSuccessText,
|
||||
executeDismantleItem,
|
||||
executeForgeRecipe,
|
||||
executeReforgeItem,
|
||||
getForgeRecipeViews,
|
||||
getReforgeCostView,
|
||||
} from '../data/forgeSystem';
|
||||
import {
|
||||
FORGE_CRAFT_FUNCTION,
|
||||
FORGE_DISMANTLE_FUNCTION,
|
||||
FORGE_REFORGE_FUNCTION,
|
||||
} from '../data/functionCatalog';
|
||||
import type { GameState } from '../types';
|
||||
import type { CommitGeneratedState } from './generatedState';
|
||||
|
||||
export function useForgeFlow({
|
||||
gameState,
|
||||
commitGeneratedState,
|
||||
}: {
|
||||
gameState: GameState;
|
||||
commitGeneratedState: CommitGeneratedState;
|
||||
}) {
|
||||
const forgeRecipes = useMemo(
|
||||
() =>
|
||||
getForgeRecipeViews(
|
||||
gameState.playerInventory,
|
||||
gameState.playerCurrency,
|
||||
gameState.worldType,
|
||||
),
|
||||
[gameState.playerCurrency, gameState.playerInventory, gameState.worldType],
|
||||
);
|
||||
|
||||
const handleCraftRecipe = useCallback(
|
||||
async (recipeId: string) => {
|
||||
if (!gameState.playerCharacter || gameState.inBattle) return false;
|
||||
|
||||
const result = executeForgeRecipe(
|
||||
gameState.playerInventory,
|
||||
recipeId,
|
||||
gameState.worldType,
|
||||
gameState.playerCurrency,
|
||||
);
|
||||
if (!result) return false;
|
||||
|
||||
const recipe = forgeRecipes.find(
|
||||
(candidate) => candidate.id === recipeId,
|
||||
);
|
||||
const nextState: GameState = {
|
||||
...gameState,
|
||||
playerCurrency: result.currency,
|
||||
playerInventory: result.inventory,
|
||||
};
|
||||
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
`制作${result.createdItem.name}`,
|
||||
buildForgeSuccessText('craft', {
|
||||
recipeName: recipe?.name ?? recipeId,
|
||||
createdItemName: result.createdItem.name,
|
||||
currencyText: recipe?.currencyText,
|
||||
}),
|
||||
FORGE_CRAFT_FUNCTION.id,
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
[commitGeneratedState, forgeRecipes, gameState],
|
||||
);
|
||||
|
||||
const handleDismantleItem = useCallback(
|
||||
async (itemId: string) => {
|
||||
if (!gameState.playerCharacter || gameState.inBattle) return false;
|
||||
|
||||
const sourceItem = gameState.playerInventory.find(
|
||||
(item) => item.id === itemId,
|
||||
);
|
||||
if (!sourceItem) return false;
|
||||
|
||||
const result = executeDismantleItem(gameState.playerInventory, itemId);
|
||||
if (!result) return false;
|
||||
|
||||
const nextState: GameState = {
|
||||
...gameState,
|
||||
playerInventory: result.inventory,
|
||||
};
|
||||
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
`拆解${sourceItem.name}`,
|
||||
buildForgeSuccessText('dismantle', {
|
||||
sourceItemName: sourceItem.name,
|
||||
outputNames: result.outputs.map((item) => item.name),
|
||||
}),
|
||||
FORGE_DISMANTLE_FUNCTION.id,
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
[commitGeneratedState, gameState],
|
||||
);
|
||||
|
||||
const handleReforgeItem = useCallback(
|
||||
async (itemId: string) => {
|
||||
if (!gameState.playerCharacter || gameState.inBattle) return false;
|
||||
|
||||
const sourceItem = gameState.playerInventory.find(
|
||||
(item) => item.id === itemId,
|
||||
);
|
||||
if (!sourceItem) return false;
|
||||
|
||||
const result = executeReforgeItem(
|
||||
gameState.playerInventory,
|
||||
itemId,
|
||||
gameState.playerCurrency,
|
||||
);
|
||||
if (!result) return false;
|
||||
|
||||
const nextState: GameState = {
|
||||
...gameState,
|
||||
playerCurrency: Math.max(
|
||||
0,
|
||||
gameState.playerCurrency - result.currencyCost,
|
||||
),
|
||||
playerInventory: result.inventory,
|
||||
};
|
||||
|
||||
const reforgeCost = getReforgeCostView(sourceItem, gameState.worldType);
|
||||
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
`重铸${sourceItem.name}`,
|
||||
buildForgeSuccessText('reforge', {
|
||||
sourceItemName: sourceItem.name,
|
||||
createdItemName: result.reforgedItem.name,
|
||||
currencyText: reforgeCost.currencyText,
|
||||
}),
|
||||
FORGE_REFORGE_FUNCTION.id,
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
[commitGeneratedState, gameState],
|
||||
);
|
||||
|
||||
return {
|
||||
forgeRecipes,
|
||||
handleCraftRecipe,
|
||||
handleDismantleItem,
|
||||
handleReforgeItem,
|
||||
getReforgeCostView,
|
||||
};
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { appendBuildBuffs } from '../data/buildDamage';
|
||||
import { INVENTORY_USE_FUNCTION } from '../data/functionCatalog';
|
||||
import {
|
||||
buildInventoryUseResultText,
|
||||
isInventoryItemUsable,
|
||||
resolveInventoryItemUseEffect,
|
||||
} from '../data/inventoryEffects';
|
||||
import { removeInventoryItem } from '../data/npcInteractions';
|
||||
import { incrementGameRuntimeStats } from '../data/runtimeStats';
|
||||
import { GameState } from '../types';
|
||||
import type { CommitGeneratedState } from './generatedState';
|
||||
|
||||
type TickCooldowns = (
|
||||
cooldowns: Record<string, number>,
|
||||
) => Record<string, number>;
|
||||
|
||||
export function useInventoryFlow({
|
||||
gameState,
|
||||
commitGeneratedState,
|
||||
tickCooldowns,
|
||||
}: {
|
||||
gameState: GameState;
|
||||
commitGeneratedState: CommitGeneratedState;
|
||||
tickCooldowns: TickCooldowns;
|
||||
}) {
|
||||
const handleUseInventoryItem = useCallback(
|
||||
async (itemId: string) => {
|
||||
if (!gameState.playerCharacter) return false;
|
||||
|
||||
const item = gameState.playerInventory.find(
|
||||
(candidate) => candidate.id === itemId,
|
||||
);
|
||||
if (!item || !isInventoryItemUsable(item) || item.quantity <= 0)
|
||||
return false;
|
||||
|
||||
const effect = resolveInventoryItemUseEffect(
|
||||
item,
|
||||
gameState.playerCharacter,
|
||||
);
|
||||
if (!effect) return false;
|
||||
|
||||
if (
|
||||
effect.hpRestore <= 0 &&
|
||||
effect.manaRestore <= 0 &&
|
||||
effect.cooldownReduction <= 0 &&
|
||||
effect.buildBuffs.length <= 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let cooldowns = gameState.playerSkillCooldowns;
|
||||
for (let index = 0; index < effect.cooldownReduction; index += 1) {
|
||||
cooldowns = tickCooldowns(cooldowns);
|
||||
}
|
||||
|
||||
const nextState: GameState = {
|
||||
...gameState,
|
||||
playerHp: Math.min(
|
||||
gameState.playerMaxHp,
|
||||
gameState.playerHp + effect.hpRestore,
|
||||
),
|
||||
playerMana: Math.min(
|
||||
gameState.playerMaxMana,
|
||||
gameState.playerMana + effect.manaRestore,
|
||||
),
|
||||
playerSkillCooldowns: cooldowns,
|
||||
activeBuildBuffs: appendBuildBuffs(
|
||||
gameState.activeBuildBuffs,
|
||||
effect.buildBuffs,
|
||||
),
|
||||
playerInventory: removeInventoryItem(
|
||||
gameState.playerInventory,
|
||||
item.id,
|
||||
1,
|
||||
),
|
||||
runtimeStats: incrementGameRuntimeStats(gameState.runtimeStats, {
|
||||
itemsUsed: 1,
|
||||
}),
|
||||
};
|
||||
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
`使用${item.name}`,
|
||||
buildInventoryUseResultText(item, effect),
|
||||
INVENTORY_USE_FUNCTION.id,
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
[commitGeneratedState, gameState, tickCooldowns],
|
||||
);
|
||||
|
||||
return {
|
||||
handleUseInventoryItem,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user