172 lines
5.1 KiB
TypeScript
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;
|
|
}, {});
|
|
}
|