1
This commit is contained in:
@@ -210,6 +210,45 @@ describe('npcInteractions', () => {
|
||||
expect(questOption?.detailText).not.toContain('完成后可获得');
|
||||
});
|
||||
|
||||
it('builds hostile npc encounters as a direct declaration dialogue with only escape and fight', () => {
|
||||
const encounter = createEncounter();
|
||||
const hostileState = {
|
||||
...buildInitialNpcState(encounter, WorldType.WUXIA),
|
||||
affinity: -12,
|
||||
};
|
||||
const story = buildNpcEncounterStoryMoment({
|
||||
encounter,
|
||||
npcState: hostileState,
|
||||
playerCharacter: createCharacter(),
|
||||
playerInventory: [],
|
||||
activeQuests: [],
|
||||
scene: {
|
||||
id: 'scene-pass',
|
||||
name: '断桥口',
|
||||
npcs: [],
|
||||
treasureHints: [],
|
||||
},
|
||||
worldType: WorldType.WUXIA,
|
||||
partySize: 0,
|
||||
});
|
||||
|
||||
expect(story.displayMode).toBe('dialogue');
|
||||
expect(story.dialogue).toEqual([
|
||||
expect.objectContaining({
|
||||
speaker: 'npc',
|
||||
speakerName: 'Trader Lin',
|
||||
}),
|
||||
]);
|
||||
expect(story.options.map((option) => option.functionId)).toEqual([
|
||||
'battle_escape_breakout',
|
||||
'npc_fight',
|
||||
]);
|
||||
expect(story.options.map((option) => option.actionText)).toEqual([
|
||||
'逃跑',
|
||||
'与他对战',
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds concrete trade action text for story continuation', () => {
|
||||
const encounter = createEncounter();
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
import { flattenDirectedRuntimeRewardItems } from './runtimeItemNarrative';
|
||||
import {
|
||||
getStoryOptionPriority,
|
||||
resolveFunctionOption,
|
||||
sortStoryOptionsByPriority,
|
||||
} from './stateFunctions';
|
||||
|
||||
@@ -1392,6 +1393,77 @@ function buildNpcOption(
|
||||
} as StoryOption;
|
||||
}
|
||||
|
||||
function buildHostileNpcDialogueText(
|
||||
encounter: Encounter,
|
||||
affinity: number,
|
||||
) {
|
||||
const hostilityText =
|
||||
affinity <= -20
|
||||
? '旧账就留到今天一起清。'
|
||||
: affinity <= -10
|
||||
? '我们之间已经没什么可谈的了。'
|
||||
: '你再往前一步,我就当你是在挑衅。';
|
||||
const contextText = encounter.context?.trim()
|
||||
? `你居然还敢带着${encounter.context}的事来见我,`
|
||||
: '';
|
||||
|
||||
return `${contextText}${hostilityText} 要么现在转身逃开,要么就拔刀。`;
|
||||
}
|
||||
|
||||
function buildHostileNpcEscapeOption(params: {
|
||||
state?: GameState | null;
|
||||
worldType: WorldType | null;
|
||||
playerCharacter: Character;
|
||||
}) {
|
||||
const functionContext =
|
||||
params.worldType
|
||||
? {
|
||||
worldType: params.worldType,
|
||||
playerCharacter: params.playerCharacter,
|
||||
inBattle: false,
|
||||
currentSceneId: params.state?.currentScenePreset?.id ?? null,
|
||||
currentSceneName: params.state?.currentScenePreset?.name ?? null,
|
||||
monsters: [],
|
||||
playerHp: params.state?.playerHp ?? 1,
|
||||
playerMaxHp: params.state?.playerMaxHp ?? 1,
|
||||
playerMana: params.state?.playerMana ?? 0,
|
||||
playerMaxMana: params.state?.playerMaxMana ?? 0,
|
||||
}
|
||||
: null;
|
||||
const resolvedOption = functionContext
|
||||
? resolveFunctionOption(
|
||||
'battle_escape_breakout',
|
||||
functionContext,
|
||||
'逃跑',
|
||||
)
|
||||
: null;
|
||||
|
||||
if (resolvedOption) {
|
||||
return {
|
||||
...resolvedOption,
|
||||
actionText: '逃跑',
|
||||
text: '逃跑',
|
||||
detailText: '',
|
||||
} satisfies StoryOption;
|
||||
}
|
||||
|
||||
return {
|
||||
functionId: 'battle_escape_breakout',
|
||||
actionText: '逃跑',
|
||||
text: '逃跑',
|
||||
detailText: '',
|
||||
priority: getStoryOptionPriority('battle_escape_breakout'),
|
||||
visuals: {
|
||||
playerAnimation: AnimationState.RUN,
|
||||
playerMoveMeters: -0.6,
|
||||
playerOffsetY: 0,
|
||||
playerFacing: 'left',
|
||||
scrollWorld: true,
|
||||
monsterChanges: [],
|
||||
},
|
||||
} satisfies StoryOption;
|
||||
}
|
||||
|
||||
function buildQuestAcceptOpportunityDetail(params: {
|
||||
issuerNpcId: string;
|
||||
issuerNpcName: string;
|
||||
@@ -2024,20 +2096,35 @@ export function buildNpcEncounterStoryMoment({
|
||||
Boolean(encounter.monsterPresetId);
|
||||
|
||||
if (isHostileEncounter) {
|
||||
const hostileDialogueText =
|
||||
overrideText ?? buildHostileNpcDialogueText(encounter, npcState.affinity);
|
||||
options.push(
|
||||
buildHostileNpcEscapeOption({
|
||||
state,
|
||||
worldType,
|
||||
playerCharacter,
|
||||
}),
|
||||
);
|
||||
options.push(
|
||||
buildNpcOption(
|
||||
NPC_FIGHT_FUNCTION.id,
|
||||
`迎战${encounter.npcName}`,
|
||||
'对方敌意已明确,靠近后就会直接进入战斗。',
|
||||
'与他对战',
|
||||
'',
|
||||
npcId,
|
||||
'fight',
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
text:
|
||||
overrideText ??
|
||||
`${scene?.name ?? '当前地界'}里,${encounter.npcName}已将你视为敌人。它一照面就摆出了进攻姿态,当前好感为 ${npcState.affinity}。`,
|
||||
text: hostileDialogueText,
|
||||
displayMode: 'dialogue',
|
||||
dialogue: [
|
||||
{
|
||||
speaker: 'npc',
|
||||
speakerName: encounter.npcName,
|
||||
text: hostileDialogueText,
|
||||
},
|
||||
],
|
||||
options: sortStoryOptionsByPriority(options),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user