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,387 @@
import {
type AttributeVector,
type Character,
type CustomWorldProfile,
type Encounter,
type InventoryItem,
type NpcPersistentState,
type RoleAttributeProfile,
type WorldAttributeSchema,
WorldType,
} from '../types';
import {
buildItemAttributeResonance,
buildRoleAttributeProfileFromLegacyData,
} from './attributeProfileGenerator';
import {
buildDefaultAxisVector,
getSortedAttributeEntries,
resolveAttributeSchema,
resolveCharacterAttributeProfile,
scoreAttributeFit,
} from './attributeResolver';
import {normalizeAttributeVector} from './attributeValidation';
import {getCharacterById} from './characterPresets';
import {getRuntimeCustomWorldProfile} from './customWorldRuntime';
import {getMonsterPresetById} from './hostileNpcPresets';
export type GiftAffinityInsight = {
resonanceScore: number;
resonanceBonus: number;
matchedAttributes: string[];
reasonText: string;
};
export type ChatAffinityOutcome = {
affinityGain: number;
matchedAttributes: string[];
summary: string;
};
export type RecruitReadinessInsight = {
readinessScore: number;
resonanceBonus: number;
tabooPenalty: number;
matchedAttributes: string[];
summary: string;
};
function resolveCustomWorldProfile(profile?: CustomWorldProfile | null) {
return profile ?? getRuntimeCustomWorldProfile();
}
function resolveInteractionWorldType(
worldType?: WorldType | null,
customWorldProfile?: CustomWorldProfile | null,
encounter?: Encounter | null,
) {
if (worldType) {
return worldType;
}
if (customWorldProfile || getRuntimeCustomWorldProfile()) {
return WorldType.CUSTOM;
}
const schemaId = encounter?.attributeProfile?.schemaId ?? '';
if (schemaId.includes('xianxia')) {
return WorldType.XIANXIA;
}
return WorldType.WUXIA;
}
function buildSchemaContext(
worldType?: WorldType | null,
customWorldProfile?: CustomWorldProfile | null,
encounter?: Encounter | null,
) {
const resolvedCustomWorldProfile = resolveCustomWorldProfile(customWorldProfile);
const resolvedWorldType = resolveInteractionWorldType(
worldType,
resolvedCustomWorldProfile,
encounter,
);
return {
worldType: resolvedWorldType,
customWorldProfile: resolvedCustomWorldProfile,
schema: resolveAttributeSchema(resolvedWorldType, resolvedCustomWorldProfile),
};
}
function resolveMonsterProfile(
encounter: Encounter,
worldType: WorldType,
) {
if (!encounter.monsterPresetId) {
return null;
}
return getMonsterPresetById(worldType, encounter.monsterPresetId)?.attributeProfile
?? getMonsterPresetById(WorldType.WUXIA, encounter.monsterPresetId)?.attributeProfile
?? getMonsterPresetById(WorldType.XIANXIA, encounter.monsterPresetId)?.attributeProfile
?? getMonsterPresetById(WorldType.CUSTOM, encounter.monsterPresetId)?.attributeProfile
?? null;
}
export function resolveEncounterAttributeProfile(
encounter: Encounter,
options: {
worldType?: WorldType | null;
customWorldProfile?: CustomWorldProfile | null;
} = {},
) {
const schemaContext = buildSchemaContext(
options.worldType,
options.customWorldProfile,
encounter,
);
if (encounter.characterId) {
const character = getCharacterById(encounter.characterId);
if (character) {
return resolveCharacterAttributeProfile(
character,
schemaContext.worldType,
schemaContext.customWorldProfile,
) ?? encounter.attributeProfile ?? null;
}
}
if (encounter.attributeProfile) {
return encounter.attributeProfile;
}
const monsterProfile = resolveMonsterProfile(encounter, schemaContext.worldType);
if (monsterProfile) {
return monsterProfile;
}
return buildRoleAttributeProfileFromLegacyData({
entityId: encounter.id ?? encounter.npcName,
schema: schemaContext.schema,
textBlocks: [
encounter.npcName,
encounter.context,
encounter.npcDescription,
],
}).profile;
}
function buildFocusVector(
profile: RoleAttributeProfile | null | undefined,
schema: WorldAttributeSchema,
limit: number,
direction: 'top' | 'bottom' = 'top',
) {
const slotIds = schema.slots.map(slot => slot.slotId);
const sourceEntries = getSortedAttributeEntries(profile, schema);
const orderedEntries = direction === 'bottom' ? [...sourceEntries].reverse() : sourceEntries;
const selectedEntries = orderedEntries.slice(0, limit);
const seed = Object.fromEntries(slotIds.map(slotId => [slotId, 0])) as AttributeVector;
selectedEntries.forEach(entry => {
seed[entry.slot.slotId] = entry.value;
});
return normalizeAttributeVector(seed, slotIds);
}
function getTopContributionAttributeNames(
profile: RoleAttributeProfile | null | undefined,
vector: AttributeVector,
schema: WorldAttributeSchema,
limit = 2,
) {
const slotIds = schema.slots.map(slot => slot.slotId);
const profileWeights = normalizeAttributeVector(profile?.values ?? {}, slotIds);
const vectorWeights = normalizeAttributeVector(vector, slotIds);
return schema.slots
.map(slot => ({
name: slot.name,
contribution: (profileWeights[slot.slotId] ?? 0) * (vectorWeights[slot.slotId] ?? 0),
}))
.sort((left, right) => right.contribution - left.contribution)
.filter(entry => entry.contribution > 0)
.slice(0, limit)
.map(entry => entry.name);
}
function uniqueNames(names: string[]) {
return [...new Set(names.filter(Boolean))];
}
function buildDialogueIntentVector(actionText: string, schema: WorldAttributeSchema) {
const slotIds = schema.slots.map(slot => slot.slotId);
const source = actionText.trim();
const seed = buildDefaultAxisVector({});
const addWeight = (slotId: string, weight: number, pattern: RegExp) => {
if (pattern.test(source)) {
seed[slotId] = (seed[slotId] ?? 0) + weight;
}
};
addWeight('axis_a', 0.24, /||||/u);
addWeight('axis_b', 0.2, /|||||/u);
addWeight('axis_c', 0.34, /||穿|||线||/u);
addWeight('axis_d', 0.3, /|||||||/u);
addWeight('axis_e', 0.34, /||||||||/u);
addWeight('axis_f', 0.22, /|||||/u);
const total = slotIds.reduce((sum, slotId) => sum + (seed[slotId] ?? 0), 0);
if (total <= 0) {
return normalizeAttributeVector(
buildDefaultAxisVector({
axis_c: 0.34,
axis_e: 0.38,
axis_f: 0.28,
}),
slotIds,
);
}
return normalizeAttributeVector(seed, slotIds);
}
export function buildEncounterAttributeRumors(
encounter: Encounter,
options: {
worldType?: WorldType | null;
customWorldProfile?: CustomWorldProfile | null;
limit?: number;
} = {},
) {
const schemaContext = buildSchemaContext(
options.worldType,
options.customWorldProfile,
encounter,
);
const profile = resolveEncounterAttributeProfile(encounter, options);
const evidence = profile?.evidence?.slice(0, options.limit ?? 2).map(entry => entry.reason) ?? [];
if (evidence.length > 0) {
return evidence;
}
return getSortedAttributeEntries(profile, schemaContext.schema)
.slice(0, options.limit ?? 2)
.map(entry => `${entry.slot.name}${entry.slot.definition}`);
}
export function buildGiftAffinityInsight(
item: InventoryItem,
encounter: Encounter,
options: {
worldType?: WorldType | null;
customWorldProfile?: CustomWorldProfile | null;
} = {},
): GiftAffinityInsight {
const schemaContext = buildSchemaContext(
options.worldType,
options.customWorldProfile,
encounter,
);
const npcProfile = resolveEncounterAttributeProfile(encounter, options);
const attributeResonance = item.attributeResonance ?? buildItemAttributeResonance(item);
const resonanceScore = scoreAttributeFit(
npcProfile,
attributeResonance.resonanceVector,
schemaContext.schema,
);
const resonanceBonus = Math.max(0, Math.min(6, Math.round(resonanceScore * 18) - 1));
const matchedAttributes = getTopContributionAttributeNames(
npcProfile,
attributeResonance.resonanceVector,
schemaContext.schema,
);
return {
resonanceScore,
resonanceBonus,
matchedAttributes,
reasonText: matchedAttributes.length > 0
? `${item.name}更对上对方看重的${matchedAttributes.join('、')}`
: attributeResonance.explanation ?? `${item.name}带来的属性共振暂时不够明显。`,
};
}
export function buildChatAffinityOutcome(params: {
playerCharacter: Character;
encounter: Encounter;
npcState: NpcPersistentState;
actionText: string;
worldType?: WorldType | null;
customWorldProfile?: CustomWorldProfile | null;
}): ChatAffinityOutcome {
const schemaContext = buildSchemaContext(
params.worldType,
params.customWorldProfile,
params.encounter,
);
const playerProfile = resolveCharacterAttributeProfile(
params.playerCharacter,
schemaContext.worldType,
schemaContext.customWorldProfile,
);
const npcProfile = resolveEncounterAttributeProfile(params.encounter, {
worldType: schemaContext.worldType,
customWorldProfile: schemaContext.customWorldProfile,
});
const intentVector = buildDialogueIntentVector(params.actionText, schemaContext.schema);
const actorFit = scoreAttributeFit(playerProfile, intentVector, schemaContext.schema);
const targetFit = scoreAttributeFit(npcProfile, intentVector, schemaContext.schema);
const sharedFocusVector = buildFocusVector(npcProfile, schemaContext.schema, 2);
const sharedResonance = scoreAttributeFit(playerProfile, sharedFocusVector, schemaContext.schema);
const baseGain = Math.max(4, 8 - params.npcState.chattedCount);
const bonusGain = Math.max(
0,
Math.min(4, Math.round(actorFit * 8 + targetFit * 6 + sharedResonance * 8) - 2),
);
const matchedAttributes = uniqueNames([
...getTopContributionAttributeNames(playerProfile, intentVector, schemaContext.schema),
...getTopContributionAttributeNames(npcProfile, intentVector, schemaContext.schema),
]).slice(0, 2);
return {
affinityGain: baseGain + bonusGain,
matchedAttributes,
summary: matchedAttributes.length > 0
? `你这番说法更对上了对方在${matchedAttributes.join('、')}上的判断方式。`
: '这轮交谈让对方更愿意继续观察你的来意。',
};
}
export function buildRecruitmentInsight(params: {
encounter: Encounter;
npcState: NpcPersistentState;
playerCharacter: Character;
worldType?: WorldType | null;
customWorldProfile?: CustomWorldProfile | null;
}): RecruitReadinessInsight {
const schemaContext = buildSchemaContext(
params.worldType,
params.customWorldProfile,
params.encounter,
);
const playerProfile = resolveCharacterAttributeProfile(
params.playerCharacter,
schemaContext.worldType,
schemaContext.customWorldProfile,
);
const npcProfile = resolveEncounterAttributeProfile(params.encounter, {
worldType: schemaContext.worldType,
customWorldProfile: schemaContext.customWorldProfile,
});
const preferenceVector = buildFocusVector(npcProfile, schemaContext.schema, 2);
const tabooVector = buildFocusVector(npcProfile, schemaContext.schema, 1, 'bottom');
const resonanceScore = scoreAttributeFit(playerProfile, preferenceVector, schemaContext.schema);
const tabooScore = scoreAttributeFit(playerProfile, tabooVector, schemaContext.schema);
const resonanceBonus = Math.max(0, Math.min(8, Math.round(resonanceScore * 24) - 2));
const tabooPenalty = Math.max(0, Math.min(4, Math.round(tabooScore * 12) - 5));
const matchedAttributes = getTopContributionAttributeNames(
playerProfile,
preferenceVector,
schemaContext.schema,
);
const tabooAttributeName = getTopContributionAttributeNames(
playerProfile,
tabooVector,
schemaContext.schema,
1,
)[0];
return {
readinessScore: params.npcState.affinity + resonanceBonus - tabooPenalty,
resonanceBonus,
tabooPenalty,
matchedAttributes,
summary: matchedAttributes.length > 0
? tabooPenalty > 0 && tabooAttributeName
? `你在${matchedAttributes.join('、')}上的做派更容易被对方视为可同行,但${tabooAttributeName}这一路数仍让他保留几分。`
: `你在${matchedAttributes.join('、')}上的做派,正是对方愿意并肩同行的那类信号。`
: '对方主要还是在看你们当前积累下来的信任。',
};
}