Files
Genarrative/src/data/attributeResolver.ts
2026-04-28 20:25:37 +08:00

172 lines
5.1 KiB
TypeScript

import type {
AttributeVector,
Character,
CombatActionAttributeProfile,
CustomWorldProfile,
RoleActionDefinition,
RoleAttributeProfile,
RoleRelationState,
WorldAttributeSchema,
WorldAttributeSlot,
WorldType,
} from '../types';
import {WORLD_ATTRIBUTE_SLOT_IDS} from '../types';
import { resolveRelationStanceFromAffinity } from './affinityLevels';
import {normalizeAttributeVector, roundNumber} from './attributeValidation';
import {getWorldAttributeSchema} from './worldAttributeSchemas';
export function resolveRelationStance(affinity: number): RoleRelationState['stance'] {
return resolveRelationStanceFromAffinity(affinity);
}
export function buildRelationState(affinity: number): RoleRelationState {
return {
affinity,
stance: resolveRelationStance(affinity),
};
}
export function resolveAttributeSchema(
worldType: WorldType | null | undefined,
customWorldProfile?: CustomWorldProfile | null,
) {
return getWorldAttributeSchema(worldType, customWorldProfile);
}
export function resolveCharacterAttributeProfile(
character: Character,
worldType: WorldType | null | undefined,
customWorldProfile?: CustomWorldProfile | null,
) {
void customWorldProfile;
if (worldType && character.attributeProfiles?.[worldType]) {
return character.attributeProfiles[worldType] ?? character.attributeProfile;
}
if (worldType === 'CUSTOM' && character.attributeProfiles?.CUSTOM) {
return character.attributeProfiles.CUSTOM;
}
return character.attributeProfile;
}
export function getAttributeSlotValue(profile: RoleAttributeProfile | null | undefined, slotId: string) {
return profile?.values?.[slotId] ?? 0;
}
export function getNormalizedAttributeWeights(
profile: RoleAttributeProfile | null | undefined,
schema: WorldAttributeSchema,
) {
return normalizeAttributeVector(profile?.values ?? {}, schema.slots.map(slot => slot.slotId));
}
export function scoreAttributeFit(
profile: RoleAttributeProfile | null | undefined,
vector: AttributeVector | null | undefined,
schema: WorldAttributeSchema,
) {
const weights = getNormalizedAttributeWeights(profile, schema);
const normalizedVector = normalizeAttributeVector(vector ?? {}, schema.slots.map(slot => slot.slotId));
return roundNumber(
schema.slots.reduce(
(sum, slot) => sum + (weights[slot.slotId] ?? 0) * (normalizedVector[slot.slotId] ?? 0),
0,
),
4,
);
}
export function scoreActionMatch(
actorProfile: RoleAttributeProfile | null | undefined,
action:
| Pick<RoleActionDefinition, 'intentVector' | 'resistVector' | 'baseScore'>
| Pick<CombatActionAttributeProfile, 'intentVector' | 'resistVector'>,
schema: WorldAttributeSchema,
options: {
targetProfile?: RoleAttributeProfile | null;
actorCoefficient?: number;
targetCoefficient?: number;
relationModifier?: number;
contextModifier?: number;
} = {},
) {
const actorFit = scoreAttributeFit(actorProfile, action.intentVector, schema);
const targetResistance = action.resistVector
? scoreAttributeFit(options.targetProfile, action.resistVector, schema)
: 0;
const actorCoefficient = options.actorCoefficient ?? 1;
const targetCoefficient = options.targetCoefficient ?? 1;
const baseScore = 'baseScore' in action ? action.baseScore : 0;
return roundNumber(
baseScore
+ actorFit * actorCoefficient
- targetResistance * targetCoefficient
+ (options.relationModifier ?? 0)
+ (options.contextModifier ?? 0),
4,
);
}
export function getSortedAttributeEntries(
profile: RoleAttributeProfile | null | undefined,
schema: WorldAttributeSchema,
) {
return [...schema.slots]
.map(slot => ({
slot,
value: getAttributeSlotValue(profile, slot.slotId),
}))
.sort((left, right) => right.value - left.value);
}
export function describeTopAttributes(
profile: RoleAttributeProfile | null | undefined,
schema: WorldAttributeSchema,
limit = 3,
) {
return getSortedAttributeEntries(profile, schema)
.slice(0, limit)
.map(entry => `${entry.slot.name}${entry.value}`);
}
export function formatAttributeList(
profile: RoleAttributeProfile | null | undefined,
schema: WorldAttributeSchema,
limit = schema.slots.length,
) {
return getSortedAttributeEntries(profile, schema)
.slice(0, limit)
.map(entry => ({
slot: entry.slot,
value: entry.value,
}));
}
export function getLeadingAttributeSlot(
profile: RoleAttributeProfile | null | undefined,
schema: WorldAttributeSchema,
) {
return getSortedAttributeEntries(profile, schema)[0]?.slot ?? schema.slots[0] ?? null;
}
export function buildSchemaSummary(schema: WorldAttributeSchema, limit = 6) {
return schema.slots.slice(0, limit).map(slot => ({
name: slot.name,
}));
}
export function getSlotById(schema: WorldAttributeSchema, slotId: string): WorldAttributeSlot | null {
return schema.slots.find(slot => slot.slotId === slotId) ?? null;
}
export function buildDefaultAxisVector(overrides: Partial<Record<(typeof WORLD_ATTRIBUTE_SLOT_IDS)[number], number>>) {
return WORLD_ATTRIBUTE_SLOT_IDS.reduce<AttributeVector>((result, slotId) => {
result[slotId] = overrides[slotId] ?? 0;
return result;
}, {});
}