init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,342 @@
import type {QuestGenerationContext} from '../services/aiTypes';
import {
buildFallbackActorNarrativeProfile,
normalizeActorNarrativeProfile,
} from '../services/storyEngine/actorNarrativeProfile';
import { buildThemePackFromWorldProfile } from '../services/storyEngine/themePack';
import { buildFallbackWorldStoryGraph } from '../services/storyEngine/worldStoryGraph';
import type {
EquipmentLoadout,
GameState,
RuntimeItemGenerationChannel,
RuntimeItemGenerationContext,
ScenePresetInfo,
} from '../types';
import {getCharacterCombatTags, getTimedBuildBuffTags, normalizeBuildTags} from './buildTags';
type GapDefinition = {
id: string;
tags: string[];
};
const BUILD_GAP_DEFINITIONS: GapDefinition[] = [
{id: 'survival_gap', tags: ['守御', '护体', '回复', '续战']},
{id: 'mana_gap', tags: ['法力', '冷却', '护持', '过载']},
{id: 'finisher_gap', tags: ['爆发', '重击', '追击', '压制']},
{id: 'mobility_gap', tags: ['突进', '快袭', '风行', '游击']},
{id: 'control_gap', tags: ['控场', '符阵', '镇邪', '反击']},
];
function dedupeStrings(values: Array<string | null | undefined | false>) {
return [...new Set(
values
.map(value => typeof value === 'string' ? value.trim() : '')
.filter(Boolean),
)];
}
function collectLoadoutBuildTags(loadout: EquipmentLoadout | null | undefined) {
if (!loadout) return [] as string[];
return normalizeBuildTags([
...(loadout.weapon?.buildProfile?.tags ?? []),
loadout.weapon?.buildProfile?.role ?? '',
...(loadout.armor?.buildProfile?.tags ?? []),
loadout.armor?.buildProfile?.role ?? '',
...(loadout.relic?.buildProfile?.tags ?? []),
loadout.relic?.buildProfile?.role ?? '',
], 6);
}
function buildSceneTags(scene: Pick<ScenePresetInfo, 'name' | 'description' | 'treasureHints'> | null) {
if (!scene) return [] as string[];
const seedParts = dedupeStrings([
scene.name,
...(scene.treasureHints ?? []),
]);
return seedParts
.flatMap(part => part.split(/[\s/]+/u))
.map(part => part.trim())
.filter(part => part.length >= 2)
.slice(0, 8);
}
function buildRecentStorySummary(lines: string[]) {
if (lines.length <= 0) return '最近没有形成稳定的事件线索。';
return lines.join(' / ');
}
function buildRecentStoryLines(storyHistory: GameState['storyHistory']) {
return storyHistory
.slice(-4)
.map(moment => moment.text.trim())
.filter(Boolean)
.slice(-3);
}
function derivePlayerBuildGaps(playerBuildTags: string[]) {
const tagSet = new Set(playerBuildTags);
return BUILD_GAP_DEFINITIONS
.filter(definition => definition.tags.filter(tag => tagSet.has(tag)).length <= 0)
.map(definition => definition.id)
.slice(0, 3);
}
function resolveRelatedNpcNarrativeProfile(params: {
customWorldProfile: GameState['customWorldProfile'];
encounter: GameState['currentEncounter'];
}) {
const { customWorldProfile, encounter } = params;
if (!customWorldProfile || !encounter || encounter.kind !== 'npc') {
return null;
}
const role =
customWorldProfile.storyNpcs.find((npc) =>
npc.id === encounter.id || npc.name === encounter.npcName,
)
?? customWorldProfile.playableNpcs.find((npc) =>
npc.id === encounter.id || npc.name === encounter.npcName,
);
if (!role) {
return encounter.narrativeProfile ?? null;
}
const themePack =
customWorldProfile.themePack ?? buildThemePackFromWorldProfile(customWorldProfile);
const storyGraph =
customWorldProfile.storyGraph
?? buildFallbackWorldStoryGraph(customWorldProfile, themePack);
return normalizeActorNarrativeProfile(
role.narrativeProfile,
buildFallbackActorNarrativeProfile(role, storyGraph, themePack),
);
}
function resolveActiveThreadIds(params: {
customWorldProfile: GameState['customWorldProfile'];
relatedNpcNarrativeProfile: RuntimeItemGenerationContext['relatedNpcNarrativeProfile'];
storyEngineMemory?: GameState['storyEngineMemory'] | QuestGenerationContext['activeThreadIds'];
}) {
const threadSource = params.storyEngineMemory;
if (Array.isArray(threadSource) && threadSource.length > 0) {
return threadSource.slice(0, 4);
}
if (
threadSource &&
!Array.isArray(threadSource) &&
threadSource.activeThreadIds?.length
) {
return threadSource.activeThreadIds.slice(0, 4);
}
if (params.relatedNpcNarrativeProfile?.relatedThreadIds.length) {
return params.relatedNpcNarrativeProfile.relatedThreadIds.slice(0, 4);
}
if (!params.customWorldProfile) {
return [];
}
const themePack =
params.customWorldProfile.themePack
?? buildThemePackFromWorldProfile(params.customWorldProfile);
const storyGraph =
params.customWorldProfile.storyGraph
?? buildFallbackWorldStoryGraph(params.customWorldProfile, themePack);
return storyGraph.visibleThreads.slice(0, 3).map((thread) => thread.id);
}
function buildBaseRuntimeContext(params: {
worldType: GameState['worldType'];
customWorldProfile: GameState['customWorldProfile'];
scene: Pick<ScenePresetInfo, 'id' | 'name' | 'description' | 'treasureHints'> | null;
encounter: GameState['currentEncounter'];
relatedNpcState: GameState['npcStates'][string] | null;
storyEngineMemory?: GameState['storyEngineMemory'] | QuestGenerationContext['activeThreadIds'];
storyHistory: GameState['storyHistory'];
playerCharacterId: string;
playerBuildTags: string[];
playerEquipmentTags: string[];
generationChannel: RuntimeItemGenerationChannel;
}) {
const {
worldType,
customWorldProfile,
scene,
encounter,
relatedNpcState,
storyEngineMemory,
storyHistory,
playerCharacterId,
playerBuildTags,
playerEquipmentTags,
generationChannel,
} = params;
const recentStoryLines = buildRecentStoryLines(storyHistory);
const relatedNpcNarrativeProfile = resolveRelatedNpcNarrativeProfile({
customWorldProfile,
encounter,
});
const activeThreadIds = resolveActiveThreadIds({
customWorldProfile,
relatedNpcNarrativeProfile,
storyEngineMemory,
});
return {
worldType,
customWorldProfile,
sceneId: scene?.id ?? null,
sceneName: scene?.name ?? null,
sceneDescription: scene?.description ?? null,
sceneTags: buildSceneTags(scene),
treasureHints: [...(scene?.treasureHints ?? [])],
encounter: encounter ?? null,
encounterNpcId: encounter?.id ?? encounter?.characterId ?? encounter?.monsterPresetId ?? encounter?.npcName ?? null,
encounterNpcName: encounter?.npcName ?? null,
encounterContextText: encounter?.context ?? null,
relatedNpcState,
relatedNpcNarrativeProfile,
relatedScene: scene,
recentStorySummary: buildRecentStorySummary(recentStoryLines),
recentActions: recentStoryLines,
activeThreadIds,
playerCharacterId,
playerBuildTags,
playerBuildGaps: derivePlayerBuildGaps(playerBuildTags),
playerEquipmentTags,
generationChannel,
} satisfies RuntimeItemGenerationContext;
}
export function buildLooseRuntimeItemGenerationContext(params: {
worldType: GameState['worldType'];
customWorldProfile?: GameState['customWorldProfile'];
scene?: Pick<ScenePresetInfo, 'id' | 'name' | 'description' | 'treasureHints'> | null;
encounter?: GameState['currentEncounter'];
relatedNpcState?: GameState['npcStates'][string] | null;
storyHistory?: GameState['storyHistory'];
playerCharacterId?: string;
playerBuildTags?: string[];
playerEquipmentTags?: string[];
generationChannel: RuntimeItemGenerationChannel;
}) {
return buildBaseRuntimeContext({
worldType: params.worldType,
customWorldProfile: params.customWorldProfile ?? null,
scene: params.scene ?? null,
encounter: params.encounter ?? null,
relatedNpcState: params.relatedNpcState ?? null,
storyEngineMemory: params.customWorldProfile?.storyGraph?.visibleThreads.map((thread) => thread.id) ?? [],
storyHistory: params.storyHistory ?? [],
playerCharacterId: params.playerCharacterId ?? 'runtime-loose-player',
playerBuildTags: params.playerBuildTags ?? [],
playerEquipmentTags: params.playerEquipmentTags ?? [],
generationChannel: params.generationChannel,
});
}
export function buildRuntimeItemGenerationContext(params: {
state: GameState;
generationChannel: RuntimeItemGenerationChannel;
encounter?: GameState['currentEncounter'];
scene?: GameState['currentScenePreset'];
}) {
const {state, generationChannel} = params;
const encounter = params.encounter ?? state.currentEncounter;
const scene = params.scene ?? state.currentScenePreset;
const relatedNpcState = encounter
? state.npcStates[encounter.id ?? encounter.npcName] ?? null
: null;
const playerBuildTags = state.playerCharacter
? normalizeBuildTags([
...getCharacterCombatTags(state.playerCharacter),
...collectLoadoutBuildTags(state.playerEquipment),
...getTimedBuildBuffTags(state.activeBuildBuffs),
], 6)
: [];
return buildBaseRuntimeContext({
worldType: state.worldType,
customWorldProfile: state.customWorldProfile,
scene,
encounter,
relatedNpcState,
storyEngineMemory: state.storyEngineMemory,
storyHistory: state.storyHistory,
playerCharacterId: state.playerCharacter?.id ?? 'unknown-player',
playerBuildTags,
playerEquipmentTags: collectLoadoutBuildTags(state.playerEquipment),
generationChannel,
});
}
export function buildQuestRuntimeItemGenerationContext(params: {
context: QuestGenerationContext;
generationChannel?: RuntimeItemGenerationChannel;
issuerNpcId: string;
issuerNpcName: string;
roleText: string;
scene?: Pick<ScenePresetInfo, 'id' | 'name' | 'description' | 'treasureHints'> | null;
}) {
const {
context,
issuerNpcId,
issuerNpcName,
roleText,
scene,
generationChannel = 'quest_reward',
} = params;
const playerBuildTags = context.playerCharacter
? normalizeBuildTags([
...getCharacterCombatTags(context.playerCharacter),
...collectLoadoutBuildTags(context.playerEquipment),
], 6)
: [];
return buildBaseRuntimeContext({
worldType: context.worldType,
customWorldProfile: context.customWorldProfile ?? null,
scene: scene ?? (
context.currentSceneName
? {
id: context.currentSceneId ?? '',
name: context.currentSceneName,
description: context.currentSceneDescription ?? '',
treasureHints: [],
}
: null
),
encounter: {
id: issuerNpcId,
kind: 'npc',
npcName: issuerNpcName,
npcDescription: roleText,
npcAvatar: '',
context: roleText,
},
relatedNpcState: context.issuerAffinity == null
? null
: {
affinity: context.issuerAffinity,
helpUsed: false,
chattedCount: 0,
giftsGiven: 0,
inventory: [],
recruited: false,
revealedFacts: [],
},
storyEngineMemory: context.activeThreadIds,
storyHistory: context.recentStoryMoments ?? [],
playerCharacterId: context.playerCharacter?.id ?? 'quest-player',
playerBuildTags,
playerEquipmentTags: collectLoadoutBuildTags(context.playerEquipment),
generationChannel,
});
}