Refine NPC interactions and runtime item generation
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
buildNpcLeaveResultText,
|
||||
buildNpcSparResultText,
|
||||
createNpcBattleMonster,
|
||||
generateNpcHelpReward,
|
||||
getChatAffinityOutcome,
|
||||
getNpcLootItems,
|
||||
getNpcSparMaxHp,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
} from '../../data/sceneEncounterPreviews';
|
||||
import { generateNextStep, streamNpcChatDialogue } from '../../services/ai';
|
||||
import type { StoryGenerationContext } from '../../services/aiTypes';
|
||||
import { generateQuestForNpcEncounter } from '../../services/questDirector';
|
||||
import type {
|
||||
Character,
|
||||
Encounter,
|
||||
@@ -459,7 +461,7 @@ export function createStoryNpcEncounterActions({
|
||||
await typewriterPromise;
|
||||
console.error('Failed to stream npc chat story:', error);
|
||||
setAiError(
|
||||
error instanceof Error ? error.message : 'NPC 对话 AI 不可用。',
|
||||
error instanceof Error ? error.message : '角色对话智能生成不可用。',
|
||||
);
|
||||
const fallbackOptions =
|
||||
getAvailableOptionsForState(provisionalState, character) ?? [];
|
||||
@@ -545,51 +547,136 @@ export function createStoryNpcEncounterActions({
|
||||
|
||||
switch (option.interaction.action) {
|
||||
case 'help': {
|
||||
const reward = buildNpcHelpReward(encounter);
|
||||
let cooldowns = gameState.playerSkillCooldowns;
|
||||
for (let index = 0; index < (reward.cooldownBonus ?? 0); index += 1) {
|
||||
cooldowns = Object.fromEntries(
|
||||
Object.entries(cooldowns).map(([skillId, turns]) => [
|
||||
skillId,
|
||||
Math.max(0, turns - 1),
|
||||
]),
|
||||
);
|
||||
}
|
||||
setAiError(null);
|
||||
setIsLoading(true);
|
||||
void (async () => {
|
||||
let committed = false;
|
||||
|
||||
let nextState = updateNpcState(
|
||||
gameState,
|
||||
encounter,
|
||||
(currentNpcState) => ({
|
||||
...markNpcFirstMeaningfulContactResolved(currentNpcState),
|
||||
helpUsed: true,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
const reward = await generateNpcHelpReward(encounter, gameState);
|
||||
let cooldowns = gameState.playerSkillCooldowns;
|
||||
for (
|
||||
let index = 0;
|
||||
index < (reward.cooldownBonus ?? 0);
|
||||
index += 1
|
||||
) {
|
||||
cooldowns = Object.fromEntries(
|
||||
Object.entries(cooldowns).map(([skillId, turns]) => [
|
||||
skillId,
|
||||
Math.max(0, turns - 1),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
nextState = {
|
||||
...nextState,
|
||||
playerHp: Math.min(
|
||||
nextState.playerMaxHp,
|
||||
nextState.playerHp + (reward.hp ?? 0),
|
||||
),
|
||||
playerMana: Math.min(
|
||||
nextState.playerMaxMana,
|
||||
nextState.playerMana + (reward.mana ?? 0),
|
||||
),
|
||||
playerSkillCooldowns: cooldowns,
|
||||
playerInventory: reward.item
|
||||
? addInventoryItems(nextState.playerInventory, [
|
||||
cloneInventoryItemForOwner(reward.item, 'player'),
|
||||
])
|
||||
: nextState.playerInventory,
|
||||
};
|
||||
let nextState = updateNpcState(
|
||||
gameState,
|
||||
encounter,
|
||||
(currentNpcState) => ({
|
||||
...markNpcFirstMeaningfulContactResolved(currentNpcState),
|
||||
helpUsed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
void commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
option.actionText,
|
||||
buildNpcHelpResultText(encounter, reward),
|
||||
option.functionId,
|
||||
);
|
||||
nextState = {
|
||||
...nextState,
|
||||
playerHp: Math.min(
|
||||
nextState.playerMaxHp,
|
||||
nextState.playerHp + (reward.hp ?? 0),
|
||||
),
|
||||
playerMana: Math.min(
|
||||
nextState.playerMaxMana,
|
||||
nextState.playerMana + (reward.mana ?? 0),
|
||||
),
|
||||
playerSkillCooldowns: cooldowns,
|
||||
playerInventory:
|
||||
reward.items.length > 0
|
||||
? addInventoryItems(
|
||||
nextState.playerInventory,
|
||||
reward.items.map((item) =>
|
||||
cloneInventoryItemForOwner(
|
||||
item,
|
||||
'player',
|
||||
item.quantity,
|
||||
),
|
||||
),
|
||||
)
|
||||
: nextState.playerInventory,
|
||||
};
|
||||
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
option.actionText,
|
||||
buildNpcHelpResultText(encounter, reward),
|
||||
option.functionId,
|
||||
);
|
||||
committed = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve npc help reward:', error);
|
||||
const reward = buildNpcHelpReward(encounter, gameState);
|
||||
let cooldowns = gameState.playerSkillCooldowns;
|
||||
for (
|
||||
let index = 0;
|
||||
index < (reward.cooldownBonus ?? 0);
|
||||
index += 1
|
||||
) {
|
||||
cooldowns = Object.fromEntries(
|
||||
Object.entries(cooldowns).map(([skillId, turns]) => [
|
||||
skillId,
|
||||
Math.max(0, turns - 1),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
let nextState = updateNpcState(
|
||||
gameState,
|
||||
encounter,
|
||||
(currentNpcState) => ({
|
||||
...markNpcFirstMeaningfulContactResolved(currentNpcState),
|
||||
helpUsed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
nextState = {
|
||||
...nextState,
|
||||
playerHp: Math.min(
|
||||
nextState.playerMaxHp,
|
||||
nextState.playerHp + (reward.hp ?? 0),
|
||||
),
|
||||
playerMana: Math.min(
|
||||
nextState.playerMaxMana,
|
||||
nextState.playerMana + (reward.mana ?? 0),
|
||||
),
|
||||
playerSkillCooldowns: cooldowns,
|
||||
playerInventory:
|
||||
reward.items.length > 0
|
||||
? addInventoryItems(
|
||||
nextState.playerInventory,
|
||||
reward.items.map((item) =>
|
||||
cloneInventoryItemForOwner(
|
||||
item,
|
||||
'player',
|
||||
item.quantity,
|
||||
),
|
||||
),
|
||||
)
|
||||
: nextState.playerInventory,
|
||||
};
|
||||
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
option.actionText,
|
||||
buildNpcHelpResultText(encounter, reward),
|
||||
option.functionId,
|
||||
);
|
||||
committed = true;
|
||||
} finally {
|
||||
if (!committed) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
case 'chat': {
|
||||
@@ -645,32 +732,83 @@ export function createStoryNpcEncounterActions({
|
||||
getNpcEncounterKey(encounter),
|
||||
);
|
||||
if (existingQuest) return true;
|
||||
setAiError(null);
|
||||
setIsLoading(true);
|
||||
void (async () => {
|
||||
let committed = false;
|
||||
|
||||
const quest = buildQuestForEncounter({
|
||||
issuerNpcId: getNpcEncounterKey(encounter),
|
||||
issuerNpcName: encounter.npcName,
|
||||
roleText: encounter.context,
|
||||
scene: gameState.currentScenePreset,
|
||||
worldType: gameState.worldType,
|
||||
});
|
||||
if (!quest) return true;
|
||||
try {
|
||||
const quest =
|
||||
(await generateQuestForNpcEncounter({
|
||||
state: gameState,
|
||||
encounter,
|
||||
})) ??
|
||||
buildQuestForEncounter({
|
||||
issuerNpcId: getNpcEncounterKey(encounter),
|
||||
issuerNpcName: encounter.npcName,
|
||||
roleText: encounter.context,
|
||||
scene: gameState.currentScenePreset,
|
||||
worldType: gameState.worldType,
|
||||
});
|
||||
if (!quest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextState = incrementRuntimeStats(
|
||||
updateNpcState(
|
||||
updateQuestLog(gameState, (quests) => acceptQuest(quests, quest)),
|
||||
encounter,
|
||||
(currentNpcState) =>
|
||||
markNpcFirstMeaningfulContactResolved(currentNpcState),
|
||||
),
|
||||
{ questsAccepted: 1 },
|
||||
);
|
||||
void commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
option.actionText,
|
||||
buildQuestAcceptResultText(quest),
|
||||
option.functionId,
|
||||
);
|
||||
const nextState = incrementRuntimeStats(
|
||||
updateNpcState(
|
||||
updateQuestLog(gameState, (quests) => acceptQuest(quests, quest)),
|
||||
encounter,
|
||||
(currentNpcState) =>
|
||||
markNpcFirstMeaningfulContactResolved(currentNpcState),
|
||||
),
|
||||
{questsAccepted: 1},
|
||||
);
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
option.actionText,
|
||||
buildQuestAcceptResultText(quest),
|
||||
option.functionId,
|
||||
);
|
||||
committed = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to accept npc quest:', error);
|
||||
const fallbackQuest = buildQuestForEncounter({
|
||||
issuerNpcId: getNpcEncounterKey(encounter),
|
||||
issuerNpcName: encounter.npcName,
|
||||
roleText: encounter.context,
|
||||
scene: gameState.currentScenePreset,
|
||||
worldType: gameState.worldType,
|
||||
});
|
||||
if (!fallbackQuest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextState = incrementRuntimeStats(
|
||||
updateNpcState(
|
||||
updateQuestLog(gameState, (quests) =>
|
||||
acceptQuest(quests, fallbackQuest),
|
||||
),
|
||||
encounter,
|
||||
(currentNpcState) =>
|
||||
markNpcFirstMeaningfulContactResolved(currentNpcState),
|
||||
),
|
||||
{questsAccepted: 1},
|
||||
);
|
||||
await commitGeneratedState(
|
||||
nextState,
|
||||
gameState.playerCharacter,
|
||||
option.actionText,
|
||||
buildQuestAcceptResultText(fallbackQuest),
|
||||
option.functionId,
|
||||
);
|
||||
committed = true;
|
||||
} finally {
|
||||
if (!committed) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
case 'quest_turn_in': {
|
||||
|
||||
Reference in New Issue
Block a user