This commit is contained in:
2026-04-27 14:23:19 +08:00
parent 09d3fe59b3
commit fa2dbb310b
75 changed files with 7363 additions and 1487 deletions

View File

@@ -1,3 +1,8 @@
import {
canUseLimitedPrimaryNpcChat,
resolveActiveSceneActEncounterFocusNpcId,
resolveActiveSceneActEncounterNpcIds,
} from '../services/customWorldSceneActRuntime';
import { AnimationState, Encounter, GameState, SceneNpc, WorldType } from '../types';
import { getRecruitedNpcIds } from './companionRoster';
import {
@@ -15,10 +20,6 @@ import {
getSceneHostileNpcs,
getWorldCampScenePreset,
} from './scenePresets';
import {
canUseLimitedPrimaryNpcChat,
resolveActiveSceneActEncounterNpcIds,
} from '../services/customWorldSceneActRuntime';
export const EXPLORE_APPROACH_DURATION_MS = 4000;
export const PREVIEW_ENTITY_X_METERS = 12;
@@ -115,7 +116,11 @@ function getAvailableFriendlySceneNpcs(state: GameState) {
const activeActNpcIdSet = new Set(activeActNpcIds);
return getSceneFriendlyNpcs(state.currentScenePreset)
.filter(candidate => !isCampScene || Boolean(candidate.characterId))
.filter(candidate =>
!isCampScene ||
Boolean(candidate.characterId) ||
activeActNpcIdSet.has(candidate.id),
)
.filter(candidate => candidate.characterId !== state.playerCharacter?.id)
.filter(candidate => !recruitedNpcIds.has(candidate.id))
.filter(candidate =>
@@ -126,6 +131,29 @@ function getAvailableFriendlySceneNpcs(state: GameState) {
);
}
function getAvailableActiveSceneActNpcs(state: GameState) {
const recruitedNpcIds = getRecruitedNpcIds(state);
const activeActNpcIds = resolveActiveSceneActEncounterNpcIds({
profile: state.customWorldProfile,
sceneId: state.currentScenePreset?.id ?? null,
storyEngineMemory: state.storyEngineMemory,
});
const activeActNpcIdSet = new Set(activeActNpcIds);
if (activeActNpcIdSet.size === 0) {
return [];
}
return (state.currentScenePreset?.npcs ?? [])
.filter(candidate => {
const candidateIds = [candidate.id, candidate.characterId].filter(
(value): value is string => Boolean(value),
);
return candidateIds.some(id => activeActNpcIdSet.has(id));
})
.filter(candidate => candidate.characterId !== state.playerCharacter?.id)
.filter(candidate => !recruitedNpcIds.has(candidate.id));
}
function getAvailableHostileSceneNpcs(state: GameState) {
const recruitedNpcIds = getRecruitedNpcIds(state);
@@ -142,6 +170,54 @@ function pickEncounterHostileNpcs(hostileNpcs: Array<SceneNpc & { monsterPresetI
return hostileNpcs.filter(npc => selectedMonsterIds.has(npc.monsterPresetId));
}
function pickFriendlySceneNpcForActiveAct(state: GameState, npcs: SceneNpc[]) {
const focusNpcId = resolveActiveSceneActEncounterFocusNpcId({
profile: state.customWorldProfile,
sceneId: state.currentScenePreset?.id ?? null,
storyEngineMemory: state.storyEngineMemory,
});
return (
npcs.find(
(npc) =>
npc.id === focusNpcId ||
(npc.characterId ? npc.characterId === focusNpcId : false),
) ?? pickRandomItem(npcs)
);
}
function hasActiveSceneActEncounterTarget(state: GameState) {
return resolveActiveSceneActEncounterNpcIds({
profile: state.customWorldProfile,
sceneId: state.currentScenePreset?.id ?? null,
storyEngineMemory: state.storyEngineMemory,
}).length > 0;
}
function buildEmptyEncounterPreview() {
return {
sceneHostileNpcs: [],
currentEncounter: null,
npcInteractionActive: false,
inBattle: false,
};
}
function buildActiveSceneActNpcEncounter(
state: GameState,
availableNpcs: SceneNpc[],
xMeters: number,
) {
const npc = pickFriendlySceneNpcForActiveAct(state, availableNpcs);
return {
sceneHostileNpcs: [],
currentEncounter: npc ? buildFriendlyEncounter(npc, xMeters) : null,
npcInteractionActive: false,
inBattle: false,
};
}
function buildHostileEncounterGroup(
state: GameState,
entryX: number,
@@ -218,12 +294,15 @@ function buildResolvedHostileBattleState(state: GameState, hostileEncounters: En
export function createSceneEncounterPreview(state: GameState) {
if (!state.worldType || !state.currentScenePreset) {
return {
sceneHostileNpcs: [],
currentEncounter: null,
npcInteractionActive: false,
inBattle: false,
};
return buildEmptyEncounterPreview();
}
if (hasActiveSceneActEncounterTarget(state)) {
return buildActiveSceneActNpcEncounter(
state,
getAvailableActiveSceneActNpcs(state),
PREVIEW_ENTITY_X_METERS,
);
}
const availableNpcs = getAvailableFriendlySceneNpcs(state);
@@ -237,12 +316,7 @@ export function createSceneEncounterPreview(state: GameState) {
const kind = pickRandomItem(availableKinds);
if (!kind) {
return {
sceneHostileNpcs: [],
currentEncounter: null,
npcInteractionActive: false,
inBattle: false,
};
return buildEmptyEncounterPreview();
}
if (kind === 'hostile') {
@@ -255,7 +329,7 @@ export function createSceneEncounterPreview(state: GameState) {
}
if (kind === 'npc') {
const npc = pickRandomItem(availableNpcs);
const npc = pickFriendlySceneNpcForActiveAct(state, availableNpcs);
return {
sceneHostileNpcs: [],
@@ -276,19 +350,22 @@ export function createSceneEncounterPreview(state: GameState) {
export function createSceneCallOutEncounter(state: GameState) {
if (!state.worldType || !state.currentScenePreset) {
return {
sceneHostileNpcs: [],
currentEncounter: null,
npcInteractionActive: false,
inBattle: false,
};
return buildEmptyEncounterPreview();
}
if (hasActiveSceneActEncounterTarget(state)) {
return buildActiveSceneActNpcEncounter(
state,
getAvailableActiveSceneActNpcs(state),
CALL_OUT_ENTRY_X_METERS,
);
}
const availableNpcs = getAvailableFriendlySceneNpcs(state);
const availableKinds: Array<'hostile' | 'npc' | 'treasure'> = [];
const availableHostiles = getAvailableHostileSceneNpcs(state);
if (availableHostiles.length > 0) availableKinds.push('hostile');
const availableNpcs = getAvailableFriendlySceneNpcs(state);
if (availableNpcs.length > 0) availableKinds.push('npc');
if (TREASURE_ENCOUNTERS_ENABLED && (state.currentScenePreset.treasureHints?.length ?? 0) > 0) {
availableKinds.push('treasure');
@@ -305,7 +382,7 @@ export function createSceneCallOutEncounter(state: GameState) {
}
if (kind === 'npc') {
const npc = pickRandomItem(availableNpcs);
const npc = pickFriendlySceneNpcForActiveAct(state, availableNpcs);
return {
sceneHostileNpcs: [],
currentEncounter: npc ? buildFriendlyEncounter(npc, CALL_OUT_ENTRY_X_METERS) : null,