1396 lines
49 KiB
TypeScript
1396 lines
49 KiB
TypeScript
/* eslint-disable no-null-keyword, max-file-line-count */
|
|
|
|
import * as _ from 'lodash';
|
|
import { Character, CharacterInfotag, KinkChoice } from '../interfaces';
|
|
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
|
|
// tslint:disable-next-line ban-ts-ignore
|
|
// @ts-ignore
|
|
import anyAscii from 'any-ascii';
|
|
|
|
import { Store } from '../site/character_page/data_store';
|
|
|
|
import {
|
|
BodyType,
|
|
bodyTypeKinkMapping,
|
|
fchatGenderMap,
|
|
FurryPreference,
|
|
Gender,
|
|
genderKinkMapping,
|
|
Kink,
|
|
KinkBucketScore,
|
|
kinkComparisonExclusionGroups,
|
|
kinkComparisonExclusions,
|
|
kinkComparisonSwaps,
|
|
kinkMapping,
|
|
kinkMatchScoreMap,
|
|
kinkMatchWeights,
|
|
KinkPreference,
|
|
likelyHuman,
|
|
mammalSpecies,
|
|
nonAnthroSpecies,
|
|
Orientation,
|
|
Position,
|
|
PostLengthPreference,
|
|
postLengthPreferenceMapping,
|
|
postLengthPreferenceScoreMapping,
|
|
Scoring,
|
|
Species,
|
|
SpeciesMap,
|
|
speciesMapping,
|
|
SpeciesMappingCache,
|
|
speciesNames,
|
|
SubDomRole,
|
|
TagId
|
|
} from './matcher-types';
|
|
|
|
|
|
export interface MatchReport {
|
|
_isVue: true;
|
|
you: MatchResult;
|
|
them: MatchResult;
|
|
youMultiSpecies: boolean;
|
|
themMultiSpecies: boolean;
|
|
merged: MatchResultScores;
|
|
score: Scoring | null;
|
|
details: MatchScoreDetails;
|
|
}
|
|
|
|
export interface MatchResultCharacterInfo {
|
|
species: Species | null;
|
|
gender: Gender | null;
|
|
orientation: Orientation | null;
|
|
}
|
|
|
|
export interface MatchResultScores {
|
|
[key: number]: Score;
|
|
[TagId.Orientation]: Score;
|
|
[TagId.Gender]: Score;
|
|
[TagId.Age]: Score;
|
|
[TagId.FurryPreference]: Score;
|
|
[TagId.Species]: Score;
|
|
[TagId.Kinks]: Score;
|
|
}
|
|
|
|
export interface MatchScoreDetails {
|
|
totalScoreDimensions: number;
|
|
dimensionsAtScoreLevel: number;
|
|
}
|
|
|
|
export interface MatchResult {
|
|
you: Character,
|
|
them: Character,
|
|
scores: MatchResultScores;
|
|
info: MatchResultCharacterInfo;
|
|
total: number;
|
|
|
|
yourAnalysis: CharacterAnalysis;
|
|
theirAnalysis: CharacterAnalysis;
|
|
}
|
|
|
|
|
|
export interface ScoreClassMap {
|
|
[key: number]: string;
|
|
}
|
|
|
|
const scoreClasses: ScoreClassMap = {
|
|
[Scoring.MATCH]: 'match',
|
|
[Scoring.WEAK_MATCH]: 'weak-match',
|
|
[Scoring.NEUTRAL]: 'neutral',
|
|
[Scoring.WEAK_MISMATCH]: 'weak-mismatch',
|
|
[Scoring.MISMATCH]: 'mismatch'
|
|
};
|
|
|
|
const scoreIcons: ScoreClassMap = {
|
|
[Scoring.MATCH]: 'fas fa-heart',
|
|
[Scoring.WEAK_MATCH]: 'fas fa-thumbs-up',
|
|
[Scoring.NEUTRAL]: 'fas fa-meh',
|
|
[Scoring.WEAK_MISMATCH]: 'fas fa-question-circle',
|
|
[Scoring.MISMATCH]: 'fas fa-heart-broken'
|
|
};
|
|
|
|
export class Score {
|
|
readonly score: Scoring;
|
|
readonly description: string;
|
|
readonly shortDesc: string;
|
|
|
|
constructor(score: Scoring, description: string = '', shortDesc: string = '') {
|
|
if ((score !== Scoring.NEUTRAL) && (description === ''))
|
|
throw new Error('Description must be provided if score is not neutral');
|
|
|
|
this.score = score;
|
|
this.description = description;
|
|
this.shortDesc = shortDesc;
|
|
}
|
|
|
|
getRecommendedClass(): string {
|
|
return Score.getClasses(this.score);
|
|
}
|
|
|
|
getRecommendedIcon(): string {
|
|
return Score.getIcon(this.score);
|
|
}
|
|
|
|
static getClasses(score: Scoring): string {
|
|
return scoreClasses[score];
|
|
}
|
|
|
|
static getIcon(score: Scoring): string {
|
|
return scoreIcons[score];
|
|
}
|
|
}
|
|
|
|
export interface CharacterAnalysisVariation {
|
|
readonly character: Character;
|
|
readonly analysis: CharacterAnalysis;
|
|
}
|
|
|
|
export class CharacterAnalysis {
|
|
readonly character: Character;
|
|
|
|
readonly gender: Gender | null;
|
|
readonly orientation: Orientation | null;
|
|
readonly species: Species | null;
|
|
readonly furryPreference: FurryPreference | null;
|
|
readonly age: number | null;
|
|
readonly subDomRole: SubDomRole | null;
|
|
readonly position: Position | null;
|
|
readonly postLengthPreference: PostLengthPreference | null;
|
|
readonly bodyType: BodyType | null;
|
|
|
|
readonly isAnthro: boolean | null;
|
|
readonly isHuman: boolean | null;
|
|
readonly isMammal: boolean | null;
|
|
|
|
constructor(c: Character) {
|
|
this.character = c;
|
|
|
|
this.gender = Matcher.getTagValueList(TagId.Gender, c);
|
|
this.orientation = Matcher.getTagValueList(TagId.Orientation, c);
|
|
this.species = Matcher.species(c);
|
|
this.furryPreference = Matcher.getTagValueList(TagId.FurryPreference, c);
|
|
this.subDomRole = Matcher.getTagValueList(TagId.SubDomRole, c);
|
|
this.position = Matcher.getTagValueList(TagId.Position, c);
|
|
this.postLengthPreference = Matcher.getTagValueList(TagId.PostLength, c);
|
|
this.bodyType = Matcher.getTagValueList(TagId.BodyType, c);
|
|
|
|
this.age = Matcher.age(c);
|
|
|
|
this.isAnthro = Matcher.isAnthro(c);
|
|
this.isHuman = Matcher.isHuman(c);
|
|
this.isMammal = Matcher.isMammal(c);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Answers the question: What YOU think about THEM
|
|
* Never what THEY think about YOU
|
|
*
|
|
* So, when comparing two characters, you have to run it twice (you, them / them, you)
|
|
* to get the full picture
|
|
*/
|
|
export class Matcher {
|
|
readonly you: Character;
|
|
readonly them: Character;
|
|
|
|
readonly yourAnalysis: CharacterAnalysis;
|
|
readonly theirAnalysis: CharacterAnalysis;
|
|
|
|
constructor(you: Character, them: Character, yourAnalysis?: CharacterAnalysis, theirAnalysis?: CharacterAnalysis) {
|
|
this.you = you;
|
|
this.them = them;
|
|
|
|
this.yourAnalysis = yourAnalysis || new CharacterAnalysis(you);
|
|
this.theirAnalysis = theirAnalysis || new CharacterAnalysis(them);
|
|
}
|
|
|
|
static generateReport(you: Character, them: Character): MatchReport {
|
|
const yourAnalysis = new CharacterAnalysis(you);
|
|
const theirAnalysis = new CharacterAnalysis(them);
|
|
|
|
const youThem = new Matcher(you, them, yourAnalysis, theirAnalysis);
|
|
const themYou = new Matcher(them, you, theirAnalysis, yourAnalysis);
|
|
|
|
const youThemMatch = youThem.match('their');
|
|
const themYouMatch = themYou.match('your');
|
|
|
|
const report: MatchReport = {
|
|
_isVue: true,
|
|
you: youThemMatch,
|
|
them: themYouMatch,
|
|
youMultiSpecies: false,
|
|
themMultiSpecies: false,
|
|
merged: Matcher.mergeResults(youThemMatch, themYouMatch),
|
|
score: null,
|
|
details: {
|
|
totalScoreDimensions: 0,
|
|
dimensionsAtScoreLevel: 0
|
|
}
|
|
};
|
|
|
|
report.score = Matcher.calculateReportScore(report);
|
|
|
|
report.details.totalScoreDimensions = Matcher.countScoresTotal(report);
|
|
report.details.dimensionsAtScoreLevel = Matcher.countScoresAtLevel(report, report.score) || 0;
|
|
|
|
// log.debug('report.generate', report);
|
|
|
|
return report;
|
|
}
|
|
|
|
static identifyBestMatchReport(you: Character, them: Character): MatchReport {
|
|
const reportStartTime = Date.now();
|
|
|
|
const yourCharacterAnalyses = Matcher.generateAnalysisVariations(you);
|
|
const theirCharacterAnalyses = Matcher.generateAnalysisVariations(them);
|
|
|
|
let bestScore = null;
|
|
let bestScoreLevelCount = -10000;
|
|
let bestReport: MatchReport;
|
|
|
|
for(const yourAnalysis of yourCharacterAnalyses) {
|
|
for (const theirAnalysis of theirCharacterAnalyses) {
|
|
const youThem = new Matcher(yourAnalysis.character, theirAnalysis.character, yourAnalysis.analysis, theirAnalysis.analysis);
|
|
const themYou = new Matcher(theirAnalysis.character, yourAnalysis.character, theirAnalysis.analysis, yourAnalysis.analysis);
|
|
|
|
const youThemMatch = youThem.match('their');
|
|
const themYouMatch = themYou.match('your');
|
|
|
|
const report: MatchReport = {
|
|
_isVue: true,
|
|
you: youThemMatch,
|
|
them: themYouMatch,
|
|
youMultiSpecies: (yourCharacterAnalyses.length > 1),
|
|
themMultiSpecies: (theirCharacterAnalyses.length > 1),
|
|
merged: Matcher.mergeResults(youThemMatch, themYouMatch),
|
|
score: null,
|
|
details: {
|
|
totalScoreDimensions: 0,
|
|
dimensionsAtScoreLevel: 0
|
|
}
|
|
};
|
|
|
|
report.score = Matcher.calculateReportScore(report);
|
|
|
|
const scoreLevelCount = Matcher.countScoresAtLevel(report, report.score);
|
|
|
|
report.details.totalScoreDimensions = Matcher.countScoresTotal(report);
|
|
report.details.dimensionsAtScoreLevel = scoreLevelCount || 0;
|
|
|
|
if (
|
|
(bestScore === null)
|
|
|| (
|
|
(report.score !== null)
|
|
&& (report.score >= bestScore)
|
|
&& (scoreLevelCount !== null)
|
|
&& (report.score * scoreLevelCount > bestScoreLevelCount)
|
|
)
|
|
) {
|
|
bestScore = report.score;
|
|
bestScoreLevelCount = ((scoreLevelCount !== null) && (report.score !== null)) ? report.score * scoreLevelCount : -1000;
|
|
bestReport = report;
|
|
}
|
|
}
|
|
}
|
|
|
|
log.debug(
|
|
'report.identify.best',
|
|
{
|
|
buildTime: Date.now() - reportStartTime,
|
|
variations: yourCharacterAnalyses.length * theirCharacterAnalyses.length,
|
|
report: bestReport!
|
|
}
|
|
);
|
|
|
|
return bestReport!;
|
|
}
|
|
|
|
|
|
// tslint:disable-next-line
|
|
private static mergeResultScores(scores: MatchResultScores, results: MatchResultScores): void {
|
|
_.each(scores, (v: Score, k: any) => {
|
|
if (
|
|
// tslint:disable-next-line no-unsafe-any
|
|
((!(k in results)) || (v.score < results[k].score))
|
|
&& (v.score !== Scoring.NEUTRAL)
|
|
) {
|
|
results[k] = v;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
static mergeResults(you: MatchResult, them: MatchResult): MatchResultScores {
|
|
const results: MatchResultScores = {} as any;
|
|
|
|
Matcher.mergeResultScores(you.scores, results);
|
|
Matcher.mergeResultScores(them.scores, results);
|
|
|
|
return results;
|
|
}
|
|
|
|
|
|
static generateAnalysisVariations(c: Character): CharacterAnalysisVariation[] {
|
|
const speciesOptions = Matcher.getAllSpeciesAsStr(c);
|
|
|
|
if (speciesOptions.length === 0) {
|
|
speciesOptions.push('');
|
|
}
|
|
|
|
return _.map(
|
|
speciesOptions,
|
|
(species) => {
|
|
// Avoid _.cloneDeep because it chokes on array-like objects with very large keys
|
|
// _.cloneDeep will happily make arrays with 41 million elements
|
|
const nc = {...c, infotags: {...c.infotags, [TagId.Species]: {string: species}}};
|
|
|
|
return { character: nc, analysis: new CharacterAnalysis(nc) };
|
|
}
|
|
);
|
|
}
|
|
|
|
static calculateReportScore(m: MatchReport): Scoring | null {
|
|
const yourScores = _.values(m.you.scores);
|
|
const theirScores = _.values(m.them.scores);
|
|
|
|
const finalScore = _.reduce(
|
|
_.concat(yourScores, theirScores),
|
|
(accum: Scoring | null, score: Score) => {
|
|
if (accum === null) {
|
|
return (score.score !== Scoring.NEUTRAL) ? score.score : null;
|
|
}
|
|
|
|
return (score.score === Scoring.NEUTRAL) ? accum : Math.min(accum, score.score);
|
|
},
|
|
null
|
|
);
|
|
|
|
if ((finalScore !== null) && (finalScore > 0)) {
|
|
// Manage edge cases where high score may not be ideal
|
|
|
|
// Nothing to score
|
|
if ((yourScores.length === 0) || (theirScores.length === 0)) {
|
|
// can't know
|
|
return Scoring.NEUTRAL;
|
|
}
|
|
|
|
// Only neutral scores given
|
|
if (
|
|
(_.every(yourScores, (n: Scoring) => n === Scoring.NEUTRAL)) ||
|
|
(_.every(theirScores, (n: Scoring) => n === Scoring.NEUTRAL))
|
|
) {
|
|
return Scoring.NEUTRAL;
|
|
}
|
|
}
|
|
|
|
// console.log('Profile score', c.character.name, score, m.you.total, m.them.total,
|
|
// m.you.total + m.them.total, m.you.total * m.them.total);
|
|
|
|
return (finalScore === null) ? Scoring.NEUTRAL : finalScore;
|
|
}
|
|
|
|
match(pronoun: string): MatchResult {
|
|
const data: MatchResult = {
|
|
you: this.you,
|
|
them: this.them,
|
|
|
|
yourAnalysis: this.yourAnalysis,
|
|
theirAnalysis: this.theirAnalysis,
|
|
|
|
total: 0,
|
|
|
|
scores: {
|
|
[TagId.Orientation]: this.resolveOrientationScore(),
|
|
[TagId.Gender]: this.resolveGenderScore(),
|
|
[TagId.Age]: this.resolveAgeScore(),
|
|
[TagId.FurryPreference]: this.resolveFurryPairingsScore(),
|
|
[TagId.Species]: this.resolveSpeciesScore(),
|
|
[TagId.SubDomRole]: this.resolveSubDomScore(),
|
|
[TagId.Kinks]: this.resolveKinkScore(pronoun),
|
|
[TagId.PostLength]: this.resolvePostLengthScore(),
|
|
[TagId.Position]: this.resolvePositionScore(),
|
|
[TagId.BodyType]: this.resolveBodyTypeScore()
|
|
},
|
|
|
|
info: {
|
|
species: Matcher.species(this.you),
|
|
gender: Matcher.getTagValueList(TagId.Gender, this.you),
|
|
orientation: Matcher.getTagValueList(TagId.Orientation, this.you)
|
|
}
|
|
};
|
|
|
|
data.total = _.reduce(
|
|
data.scores,
|
|
(accum: number, s: Score) => (accum + s.score),
|
|
0
|
|
);
|
|
|
|
return data;
|
|
}
|
|
|
|
private resolveOrientationScore(): Score {
|
|
// Question: If someone identifies themselves as 'straight cuntboy', how should they be matched? like a straight female?
|
|
|
|
return Matcher.scoreOrientationByGender(this.yourAnalysis.gender, this.yourAnalysis.orientation, this.theirAnalysis.gender);
|
|
}
|
|
|
|
|
|
static scoreOrientationByGender(yourGender: Gender | null, yourOrientation: Orientation | null, theirGender: Gender | null): Score {
|
|
if ((yourGender === null) || (theirGender === null) || (yourOrientation === null))
|
|
return new Score(Scoring.NEUTRAL);
|
|
|
|
// CIS
|
|
// tslint:disable-next-line curly
|
|
if (Matcher.isCisGender(yourGender)) {
|
|
if (yourGender === theirGender) {
|
|
// same sex CIS
|
|
if (yourOrientation === Orientation.Straight)
|
|
return new Score(Scoring.MISMATCH, 'No <span>same sex</span>');
|
|
|
|
if (
|
|
(yourOrientation === Orientation.Gay)
|
|
|| (yourOrientation === Orientation.Bisexual)
|
|
|| (yourOrientation === Orientation.Pansexual)
|
|
|| ((yourOrientation === Orientation.BiFemalePreference) && (theirGender === Gender.Female))
|
|
|| ((yourOrientation === Orientation.BiMalePreference) && (theirGender === Gender.Male))
|
|
)
|
|
return new Score(Scoring.MATCH, 'Loves <span>same sex</span>');
|
|
|
|
if (
|
|
(yourOrientation === Orientation.BiCurious)
|
|
|| ((yourOrientation === Orientation.BiFemalePreference) && (theirGender === Gender.Male))
|
|
|| ((yourOrientation === Orientation.BiMalePreference) && (theirGender === Gender.Female))
|
|
)
|
|
return new Score(Scoring.WEAK_MATCH, 'Likes <span>same sex</span>');
|
|
} else if (Matcher.isCisGender(theirGender)) {
|
|
// straight CIS
|
|
if (yourOrientation === Orientation.Gay)
|
|
return new Score(Scoring.MISMATCH, 'No <span>opposite sex</span>');
|
|
|
|
if (
|
|
(yourOrientation === Orientation.Straight)
|
|
|| (yourOrientation === Orientation.Bisexual)
|
|
|| (yourOrientation === Orientation.BiCurious)
|
|
|| (yourOrientation === Orientation.Pansexual)
|
|
|| ((yourOrientation === Orientation.BiFemalePreference) && (theirGender === Gender.Female))
|
|
|| ((yourOrientation === Orientation.BiMalePreference) && (theirGender === Gender.Male))
|
|
)
|
|
return new Score(Scoring.MATCH, 'Loves <span>opposite sex</span>');
|
|
|
|
if (
|
|
((yourOrientation === Orientation.BiFemalePreference) && (theirGender === Gender.Male))
|
|
|| ((yourOrientation === Orientation.BiMalePreference) && (theirGender === Gender.Female))
|
|
)
|
|
return new Score(Scoring.WEAK_MATCH, 'Likes <span>opposite sex</span>');
|
|
}
|
|
}
|
|
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
|
|
static formatKinkScore(score: KinkPreference, description: string): Score {
|
|
if (score === KinkPreference.No)
|
|
return new Score(Scoring.MISMATCH, `No <span>${description}</span>`);
|
|
|
|
if (score === KinkPreference.Maybe)
|
|
return new Score(Scoring.WEAK_MISMATCH, `Hesitant about <span>${description}</span>`);
|
|
|
|
if (score === KinkPreference.Yes)
|
|
return new Score(Scoring.WEAK_MATCH, `Likes <span>${description}</span>`);
|
|
|
|
if (score === KinkPreference.Favorite)
|
|
return new Score(Scoring.MATCH, `Loves <span>${description}</span>`);
|
|
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
private resolvePostLengthScore(): Score {
|
|
const yourLength = this.yourAnalysis.postLengthPreference;
|
|
const theirLength = this.theirAnalysis.postLengthPreference;
|
|
|
|
if (
|
|
(!yourLength)
|
|
|| (!theirLength)
|
|
|| (yourLength === PostLengthPreference.NoPreference)
|
|
|| (theirLength === PostLengthPreference.NoPreference)
|
|
) {
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
const score = postLengthPreferenceScoreMapping[yourLength][theirLength];
|
|
|
|
return this.formatScoring(score, postLengthPreferenceMapping[theirLength]);
|
|
}
|
|
|
|
static getSpeciesName(species: Species): string {
|
|
return speciesNames[species] || `${Species[species].toLowerCase()}s`;
|
|
}
|
|
|
|
private resolveSpeciesScore(): Score {
|
|
const you = this.you;
|
|
const theirAnalysis = this.theirAnalysis;
|
|
const theirSpecies = theirAnalysis.species;
|
|
|
|
if (theirSpecies === null)
|
|
return new Score(Scoring.NEUTRAL);
|
|
|
|
const speciesScore = Matcher.getKinkSpeciesPreference(you, theirSpecies);
|
|
|
|
if (speciesScore !== null) {
|
|
// console.log(this.them.name, speciesScore, theirSpecies);
|
|
const speciesName = speciesNames[theirSpecies] || `${Species[theirSpecies].toLowerCase()}s`;
|
|
|
|
return Matcher.formatKinkScore(speciesScore, speciesName);
|
|
}
|
|
|
|
if (theirAnalysis.isAnthro) {
|
|
const anthroScore = Matcher.getKinkPreference(you, Kink.AnthroCharacters);
|
|
|
|
if (anthroScore !== null)
|
|
return Matcher.formatKinkScore(anthroScore, 'anthros');
|
|
}
|
|
|
|
if (theirAnalysis.isMammal) {
|
|
const mammalScore = Matcher.getKinkPreference(you, Kink.Mammals);
|
|
|
|
if (mammalScore !== null)
|
|
return Matcher.formatKinkScore(mammalScore, 'mammals');
|
|
}
|
|
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
|
|
formatScoring(score: Scoring, description: string): Score {
|
|
let type = '';
|
|
|
|
switch (score) {
|
|
case Scoring.MISMATCH:
|
|
type = 'No';
|
|
break;
|
|
|
|
case Scoring.WEAK_MISMATCH:
|
|
type = 'Hesitant about';
|
|
break;
|
|
|
|
case Scoring.WEAK_MATCH:
|
|
type = 'Likes';
|
|
break;
|
|
|
|
case Scoring.MATCH:
|
|
type = 'Loves';
|
|
break;
|
|
}
|
|
|
|
return new Score(score, `${type} <span>${description}</span>`);
|
|
}
|
|
|
|
private resolveFurryPairingsScore(): Score {
|
|
const you = this.you;
|
|
const theyAreAnthro = this.theirAnalysis.isAnthro;
|
|
const theyAreHuman = this.theirAnalysis.isHuman;
|
|
|
|
const score = theyAreAnthro
|
|
? Matcher.furryLikeabilityScore(you)
|
|
: (theyAreHuman ? Matcher.humanLikeabilityScore(you) : Scoring.NEUTRAL);
|
|
|
|
if (score === Scoring.WEAK_MATCH)
|
|
return new Score(
|
|
score,
|
|
theyAreAnthro
|
|
? 'Prefers <span>humans</span>, ok with anthros'
|
|
: 'Prefers <span>anthros</span>, ok with humans'
|
|
);
|
|
|
|
return this.formatScoring(score, theyAreAnthro ? 'furry pairings' : theyAreHuman ? 'human pairings' : '');
|
|
}
|
|
|
|
|
|
private resolveKinkScore(pronoun: string): Score {
|
|
// const kinkScore = this.resolveKinkBucketScore('all');
|
|
|
|
const scores = {
|
|
favorite: this.resolveKinkBucketScore('favorite'),
|
|
yes: this.resolveKinkBucketScore('yes'),
|
|
maybe: this.resolveKinkBucketScore('maybe'),
|
|
no: this.resolveKinkBucketScore('no')
|
|
};
|
|
|
|
const weighted = scores.favorite.weighted + scores.yes.weighted + scores.maybe.weighted + scores.no.weighted;
|
|
|
|
log.debug('report.score.kink', this.them.name, this.you.name, scores, weighted);
|
|
|
|
if (scores.favorite.count + scores.yes.count + scores.maybe.count + scores.no.count < 10) {
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
if (weighted === 0) {
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
if (weighted < 0) {
|
|
if (Math.abs(weighted) < kinkMatchWeights.weakMismatchThreshold) {
|
|
return new Score(Scoring.WEAK_MISMATCH, `Hesitant about ${pronoun} <span>kinks</span>`);
|
|
}
|
|
|
|
return new Score(Scoring.MISMATCH, `Dislikes ${pronoun} <span>kinks</span>`);
|
|
}
|
|
|
|
if (Math.abs(weighted) < kinkMatchWeights.weakMatchThreshold) {
|
|
return new Score(Scoring.WEAK_MATCH, `Likes ${pronoun} <span>kinks</span>`);
|
|
}
|
|
|
|
return new Score(Scoring.MATCH, `Loves ${pronoun} <span>kinks</span>`);
|
|
}
|
|
|
|
|
|
static furryLikeabilityScore(c: Character): Scoring {
|
|
const furryPreference = Matcher.getTagValueList(TagId.FurryPreference, c);
|
|
|
|
if (
|
|
(furryPreference === FurryPreference.FursAndHumans) ||
|
|
(furryPreference === FurryPreference.FurriesPreferredHumansOk) ||
|
|
(furryPreference === FurryPreference.FurriesOnly)
|
|
)
|
|
return Scoring.MATCH;
|
|
|
|
if (furryPreference === FurryPreference.HumansPreferredFurriesOk)
|
|
return Scoring.WEAK_MATCH;
|
|
|
|
if (furryPreference === FurryPreference.HumansOnly)
|
|
return Scoring.MISMATCH;
|
|
|
|
return Scoring.NEUTRAL;
|
|
}
|
|
|
|
static humanLikeabilityScore(c: Character): Scoring {
|
|
const humanPreference = Matcher.getTagValueList(TagId.FurryPreference, c);
|
|
|
|
if (
|
|
(humanPreference === FurryPreference.FursAndHumans)
|
|
|| (humanPreference === FurryPreference.HumansPreferredFurriesOk)
|
|
|| (humanPreference === FurryPreference.HumansOnly)
|
|
)
|
|
return Scoring.MATCH;
|
|
|
|
if (humanPreference === FurryPreference.FurriesPreferredHumansOk)
|
|
return Scoring.WEAK_MATCH;
|
|
|
|
if (humanPreference === FurryPreference.FurriesOnly)
|
|
return Scoring.MISMATCH;
|
|
|
|
return Scoring.NEUTRAL;
|
|
}
|
|
|
|
private resolveAgeScore(): Score {
|
|
const you = this.you;
|
|
const theirAge = this.theirAnalysis.age;
|
|
|
|
if (theirAge === null)
|
|
return new Score(Scoring.NEUTRAL);
|
|
|
|
const ageplayScore = Matcher.getKinkPreference(you, Kink.Ageplay);
|
|
const underageScore = Matcher.getKinkPreference(you, Kink.UnderageCharacters);
|
|
|
|
if ((theirAge < 16) && (ageplayScore !== null))
|
|
return Matcher.formatKinkScore(ageplayScore, `ages of ${theirAge}`);
|
|
|
|
if ((theirAge < 16) && (ageplayScore === null))
|
|
return Matcher.formatKinkScore(KinkPreference.No, `ages of ${theirAge}`);
|
|
|
|
if ((theirAge < 18) && (theirAge >= 16) && (underageScore !== null))
|
|
return Matcher.formatKinkScore(underageScore, `ages of ${theirAge}`);
|
|
|
|
const yourAge = this.yourAnalysis.age;
|
|
|
|
if ((yourAge !== null) && (yourAge > 0) && (theirAge > 0) && (yourAge <= 80) && (theirAge <= 80)) {
|
|
const olderCharactersScore = Matcher.getKinkPreference(you, Kink.OlderCharacters);
|
|
const youngerCharactersScore = Matcher.getKinkPreference(you, Kink.YoungerCharacters);
|
|
const ageDifference = Math.abs(yourAge - theirAge);
|
|
|
|
if ((yourAge < theirAge) && (olderCharactersScore !== null) && (ageDifference >= 8))
|
|
return Matcher.formatKinkScore(olderCharactersScore, 'older characters');
|
|
|
|
if ((yourAge > theirAge) && (youngerCharactersScore !== null) && (ageDifference >= 8))
|
|
return Matcher.formatKinkScore(youngerCharactersScore, 'younger characters');
|
|
}
|
|
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
private resolveGenderScore(): Score {
|
|
const you = this.you;
|
|
const theirGender = this.theirAnalysis.gender;
|
|
|
|
if (theirGender === null)
|
|
return new Score(Scoring.NEUTRAL);
|
|
|
|
const genderName = `${Gender[theirGender].toLowerCase()}s`;
|
|
const genderKinkScore = Matcher.getKinkGenderPreference(you, theirGender);
|
|
|
|
if (genderKinkScore !== null)
|
|
return Matcher.formatKinkScore(genderKinkScore, genderName);
|
|
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
private resolveBodyTypeScore(): Score {
|
|
const theirBodyType = Matcher.getTagValueList(TagId.BodyType, this.them);
|
|
|
|
if (theirBodyType && theirBodyType in bodyTypeKinkMapping) {
|
|
const bodyTypePreference = Matcher.getKinkPreference(this.you, bodyTypeKinkMapping[theirBodyType]);
|
|
|
|
if (bodyTypePreference !== null) {
|
|
return Matcher.formatKinkScore(bodyTypePreference, `${BodyType[theirBodyType].toLowerCase()}s`);
|
|
}
|
|
}
|
|
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
private resolveSubDomScore(): Score {
|
|
const you = this.you;
|
|
const yourSubDomRole = this.yourAnalysis.subDomRole;
|
|
const theirSubDomRole = this.theirAnalysis.subDomRole;
|
|
const yourRoleReversalPreference = Matcher.getKinkPreference(you, Kink.RoleReversal);
|
|
|
|
if ((!yourSubDomRole) || (!theirSubDomRole))
|
|
return new Score(Scoring.NEUTRAL);
|
|
|
|
if (yourSubDomRole === SubDomRole.UsuallyDominant) {
|
|
if (theirSubDomRole === SubDomRole.Switch)
|
|
return new Score(Scoring.MATCH, `Loves <span>switches</span> (role)`);
|
|
|
|
if ((theirSubDomRole === SubDomRole.AlwaysSubmissive) || (theirSubDomRole === SubDomRole.UsuallySubmissive))
|
|
return new Score(Scoring.MATCH, `Loves <span>submissives</span>`);
|
|
|
|
if (yourRoleReversalPreference === KinkPreference.Favorite)
|
|
return new Score(Scoring.MATCH, `Loves <span>role reversal</span>`);
|
|
|
|
if (yourRoleReversalPreference === KinkPreference.Yes)
|
|
return new Score(Scoring.MATCH, `Likes <span>role reversal</span>`);
|
|
|
|
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>dominants</span>');
|
|
}
|
|
|
|
if (yourSubDomRole === SubDomRole.AlwaysDominant) {
|
|
if (theirSubDomRole === SubDomRole.Switch)
|
|
return new Score(Scoring.WEAK_MATCH, `Likes <span>switches</span> (role)`);
|
|
|
|
if ((theirSubDomRole === SubDomRole.AlwaysSubmissive) || (theirSubDomRole === SubDomRole.UsuallySubmissive))
|
|
return new Score(Scoring.MATCH, `Loves <span>submissives</span>`);
|
|
|
|
if (yourRoleReversalPreference === KinkPreference.Favorite)
|
|
return new Score(Scoring.MATCH, `Loves <span>role reversal</span>`);
|
|
|
|
if (yourRoleReversalPreference === KinkPreference.Yes)
|
|
return new Score(Scoring.MATCH, `Likes <span>role reversal</span>`);
|
|
|
|
if ((yourSubDomRole === SubDomRole.AlwaysDominant) && (theirSubDomRole === SubDomRole.AlwaysDominant))
|
|
return new Score(Scoring.MISMATCH, 'No <span>dominants</span>');
|
|
|
|
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>dominants</span>');
|
|
}
|
|
|
|
if (yourSubDomRole === SubDomRole.UsuallySubmissive) {
|
|
if (theirSubDomRole === SubDomRole.Switch)
|
|
return new Score(Scoring.MATCH, `Loves <span>switches</span> (role)`);
|
|
|
|
if ((theirSubDomRole === SubDomRole.AlwaysDominant) || (theirSubDomRole === SubDomRole.UsuallyDominant))
|
|
return new Score(Scoring.MATCH, `Loves <span>dominants</span>`);
|
|
|
|
if (yourRoleReversalPreference === KinkPreference.Favorite)
|
|
return new Score(Scoring.MATCH, `Loves <span>role reversal</span>`);
|
|
|
|
if (yourRoleReversalPreference === KinkPreference.Yes)
|
|
return new Score(Scoring.MATCH, `Likes <span>role reversal</span>`);
|
|
|
|
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>submissives</span>');
|
|
}
|
|
|
|
if (yourSubDomRole === SubDomRole.AlwaysSubmissive) {
|
|
if (theirSubDomRole === SubDomRole.Switch)
|
|
return new Score(Scoring.WEAK_MATCH, `Likes <span>switches</span> (role)`);
|
|
|
|
if ((theirSubDomRole === SubDomRole.AlwaysDominant) || (theirSubDomRole === SubDomRole.UsuallyDominant))
|
|
return new Score(Scoring.MATCH, `Loves <span>dominants</span>`);
|
|
|
|
if (yourRoleReversalPreference === KinkPreference.Favorite)
|
|
return new Score(Scoring.MATCH, `Loves <span>role reversal</span>`);
|
|
|
|
if (yourRoleReversalPreference === KinkPreference.Yes)
|
|
return new Score(Scoring.MATCH, `Likes <span>role reversal</span>`);
|
|
|
|
if ((yourSubDomRole === SubDomRole.AlwaysSubmissive) && (theirSubDomRole === SubDomRole.AlwaysSubmissive))
|
|
return new Score(Scoring.MISMATCH, 'No <span>submissives</span>');
|
|
|
|
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>submissives</span>');
|
|
}
|
|
|
|
// You must be a switch
|
|
if (theirSubDomRole === SubDomRole.Switch)
|
|
return new Score(Scoring.MATCH, `Loves <span>switches</span> (role)`);
|
|
|
|
// if (yourRoleReversalPreference === KinkPreference.Favorite)
|
|
// return new Score(Scoring.MATCH, `Loves <span>role reversal</span>`);
|
|
//
|
|
// if (yourRoleReversalPreference === KinkPreference.Yes)
|
|
// return new Score(Scoring.MATCH, `Likes <span>role reversal</span>`);
|
|
|
|
if ((theirSubDomRole === SubDomRole.AlwaysDominant) || (theirSubDomRole === SubDomRole.UsuallyDominant))
|
|
return new Score(Scoring.MATCH, `Loves <span>dominants</span>`);
|
|
|
|
if ((theirSubDomRole === SubDomRole.AlwaysSubmissive) || (theirSubDomRole === SubDomRole.UsuallySubmissive))
|
|
return new Score(Scoring.MATCH, `Loves <span>submissives</span>`);
|
|
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
private resolvePositionScore(): Score {
|
|
const yourPosition = this.yourAnalysis.position;
|
|
const theirPosition = this.theirAnalysis.position;
|
|
|
|
if ((!yourPosition) || (!theirPosition))
|
|
return new Score(Scoring.NEUTRAL);
|
|
|
|
if (yourPosition === Position.UsuallyTop) {
|
|
if (theirPosition === Position.Switch)
|
|
return new Score(Scoring.MATCH, `Loves <span>switches</span> (position)`);
|
|
|
|
if ((theirPosition === Position.AlwaysBottom) || (theirPosition === Position.UsuallyBottom))
|
|
return new Score(Scoring.MATCH, `Loves <span>bottoms</span>`);
|
|
|
|
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>tops</span>');
|
|
}
|
|
|
|
if (yourPosition === Position.AlwaysTop) {
|
|
if (theirPosition === Position.Switch)
|
|
return new Score(Scoring.WEAK_MATCH, `Likes <span>switches</span> (position)`);
|
|
|
|
if ((theirPosition === Position.AlwaysBottom) || (theirPosition === Position.UsuallyBottom))
|
|
return new Score(Scoring.MATCH, `Loves <span>bottoms</span>`);
|
|
|
|
if ((yourPosition === Position.AlwaysTop) && (theirPosition === Position.AlwaysTop))
|
|
return new Score(Scoring.MISMATCH, 'No <span>tops</span>');
|
|
|
|
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>tops</span>');
|
|
}
|
|
|
|
if (yourPosition === Position.UsuallyBottom) {
|
|
if (theirPosition === Position.Switch)
|
|
return new Score(Scoring.MATCH, `Loves <span>switches</span> (position)`);
|
|
|
|
if ((theirPosition === Position.AlwaysTop) || (theirPosition === Position.UsuallyTop))
|
|
return new Score(Scoring.MATCH, `Loves <span>tops</span>`);
|
|
|
|
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>bottoms</span>');
|
|
}
|
|
|
|
if (yourPosition === Position.AlwaysBottom) {
|
|
if (theirPosition === Position.Switch)
|
|
return new Score(Scoring.WEAK_MATCH, `Likes <span>switches</span> (position)`);
|
|
|
|
if ((theirPosition === Position.AlwaysTop) || (theirPosition === Position.UsuallyTop))
|
|
return new Score(Scoring.MATCH, `Loves <span>tops</span>`);
|
|
|
|
if ((yourPosition === Position.AlwaysBottom) && (theirPosition === Position.AlwaysBottom))
|
|
return new Score(Scoring.MISMATCH, 'No <span>bottoms</span>');
|
|
|
|
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>bottoms</span>');
|
|
}
|
|
|
|
// You must be a switch
|
|
if (theirPosition === Position.Switch)
|
|
return new Score(Scoring.MATCH, `Loves <span>switches</span> (position)`);
|
|
|
|
// if (yourRoleReversalPreference === KinkPreference.Favorite)
|
|
// return new Score(Scoring.MATCH, `Loves <span>role reversal</span>`);
|
|
//
|
|
// if (yourRoleReversalPreference === KinkPreference.Yes)
|
|
// return new Score(Scoring.MATCH, `Likes <span>role reversal</span>`);
|
|
|
|
if ((theirPosition === Position.AlwaysTop) || (theirPosition === Position.UsuallyTop))
|
|
return new Score(Scoring.MATCH, `Loves <span>tops</span>`);
|
|
|
|
if ((theirPosition === Position.AlwaysBottom) || (theirPosition === Position.UsuallyBottom))
|
|
return new Score(Scoring.MATCH, `Loves <span>bottoms</span>`);
|
|
|
|
return new Score(Scoring.NEUTRAL);
|
|
}
|
|
|
|
|
|
private resolveKinkBucketScore(bucket: 'all' | 'favorite' | 'yes' | 'maybe' | 'no' | 'positive' | 'negative'): KinkBucketScore {
|
|
const yourKinks = this.getAllStandardKinks(this.you);
|
|
const theirKinks = this.getAllStandardKinks(this.them);
|
|
|
|
// let missed = 0;
|
|
|
|
const result: any = _.reduce(
|
|
yourKinks,
|
|
(accum, yourKinkValue: any, yourKinkId: any) => {
|
|
const theirKinkId = (yourKinkId in kinkComparisonSwaps) ? kinkComparisonSwaps[yourKinkId] : yourKinkId;
|
|
|
|
const isExcluded = (yourKinkId in kinkComparisonExclusions)
|
|
|| ((Store.shared.kinks[yourKinkId]) && (Store.shared.kinks[yourKinkId].kink_group in kinkComparisonExclusionGroups));
|
|
|
|
const isBucketMatch = (yourKinkValue === bucket)
|
|
|| (bucket === 'all')
|
|
|| ((bucket === 'negative') && ((yourKinkValue === 'no') || (yourKinkValue === 'maybe')))
|
|
|| ((bucket === 'positive') && ((yourKinkValue === 'favorite') || (yourKinkValue === 'yes')));
|
|
|
|
if ((isBucketMatch) && (!isExcluded)) {
|
|
accum.total += 1;
|
|
}
|
|
|
|
if (
|
|
(!(theirKinkId in theirKinks))
|
|
|| (isExcluded)
|
|
) {
|
|
return accum;
|
|
}
|
|
|
|
const theirKinkValue = theirKinks[theirKinkId] as any;
|
|
|
|
if (isBucketMatch) {
|
|
return {
|
|
score: accum.score + this.getKinkMatchScore(yourKinkValue, theirKinkValue),
|
|
count: accum.count + 1,
|
|
total: accum.total
|
|
};
|
|
}
|
|
|
|
return accum;
|
|
},
|
|
{ score: 0, count: 0, total: 0 }
|
|
);
|
|
|
|
// const yourBucketCounts = this.countKinksByBucket(yourKinks);
|
|
// const theirBucketCounts = this.countKinksByBucket(theirKinks);
|
|
|
|
result.weighted = ((result.count === 0) || (Math.abs(result.score) < 1))
|
|
? 0
|
|
: (
|
|
Math.log(result.total) * Math.log(Math.abs(result.score)) * Math.sign(result.score)
|
|
// (Math.log(result.count) / Math.log(kinkMatchWeights.logBase)) // log 8 base
|
|
// * (result.score / result.count)
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
// private countKinksByBucket(kinks: { [key: number]: KinkChoice }): { favorite: number, yes: number, maybe: number, no: number } {
|
|
// return _.reduce(
|
|
// kinks,
|
|
// (accum, kinkValue) => {
|
|
// accum[kinkValue] += 1;
|
|
// return accum;
|
|
// },
|
|
// {
|
|
// favorite: 0,
|
|
// yes: 0,
|
|
// maybe: 0,
|
|
// no: 0
|
|
// }
|
|
// );
|
|
// }
|
|
|
|
private getAllStandardKinks(c: Character): { [key: number]: KinkChoice } {
|
|
const kinks = _.pickBy(c.kinks, _.isString);
|
|
|
|
// Avoid using _.forEach on c.customs because lodash thinks it is an array
|
|
for (const custom of Object.values(c.customs)) {
|
|
if (custom) {
|
|
const children = (custom as any).children ?? {};
|
|
|
|
_.each(children, (child) => kinks[child] = custom.choice);
|
|
}
|
|
}
|
|
|
|
return kinks as any;
|
|
}
|
|
|
|
|
|
private getKinkMatchScore(aValue: string, bValue: string): number {
|
|
return _.get(kinkMatchScoreMap, `${aValue}.${bValue}`, 0) * 7; // forces range above 1.0
|
|
}
|
|
|
|
|
|
static getTagValue(tagId: number, c: Character): CharacterInfotag | undefined {
|
|
return c.infotags[tagId];
|
|
}
|
|
|
|
static getTagValueList(tagId: number, c: Character): number | null {
|
|
const t = Matcher.getTagValue(tagId, c);
|
|
|
|
if ((!t) || (!t.list))
|
|
return null;
|
|
|
|
return t.list;
|
|
}
|
|
|
|
static isCisGender(...genders: Gender[] | null[]): boolean {
|
|
return _.every(genders, (g: Gender) => ((g === Gender.Female) || (g === Gender.Male)));
|
|
}
|
|
|
|
static getKinkPreference(c: Character, kinkId: number): KinkPreference | null {
|
|
if (!(kinkId in c.kinks))
|
|
return null;
|
|
|
|
const kinkVal = c.kinks[kinkId];
|
|
|
|
if (kinkVal === undefined) {
|
|
return null;
|
|
}
|
|
|
|
if (typeof kinkVal === 'string') {
|
|
return kinkMapping[c.kinks[kinkId] as string];
|
|
}
|
|
|
|
const custom = c.customs[kinkVal];
|
|
|
|
if (!custom) {
|
|
return null;
|
|
}
|
|
|
|
return kinkMapping[custom.choice];
|
|
}
|
|
|
|
static getKinkGenderPreference(c: Character, gender: Gender): KinkPreference | null {
|
|
if (!(gender in genderKinkMapping))
|
|
return null;
|
|
|
|
return Matcher.getKinkPreference(c, genderKinkMapping[gender]);
|
|
}
|
|
|
|
static getKinkSpeciesPreference(c: Character, species: Species): KinkPreference | null {
|
|
return Matcher.getKinkPreference(c, species);
|
|
}
|
|
|
|
static has(c: Character, kinkId: Kink): boolean {
|
|
const r = Matcher.getKinkPreference(c, kinkId);
|
|
|
|
return (r !== null);
|
|
}
|
|
|
|
static isMammal(c: Character): boolean | null {
|
|
const species = Matcher.species(c);
|
|
|
|
if (species === null)
|
|
return null;
|
|
|
|
return (mammalSpecies.indexOf(species) >= 0);
|
|
}
|
|
|
|
static isAnthro(c: Character): boolean | null {
|
|
const bodyTypeId = Matcher.getTagValueList(TagId.BodyType, c);
|
|
|
|
if (bodyTypeId === BodyType.Anthro)
|
|
return true;
|
|
|
|
const speciesId = Matcher.species(c);
|
|
|
|
if (!speciesId)
|
|
return null;
|
|
|
|
return (nonAnthroSpecies.indexOf(speciesId) < 0);
|
|
}
|
|
|
|
static isHuman(c: Character): boolean | null {
|
|
const bodyTypeId = Matcher.getTagValueList(TagId.BodyType, c);
|
|
|
|
if (bodyTypeId === BodyType.Human)
|
|
return true;
|
|
|
|
const speciesId = Matcher.species(c);
|
|
|
|
return (speciesId === Species.Human);
|
|
}
|
|
|
|
static species(c: Character): Species | null {
|
|
const mySpecies = Matcher.getTagValue(TagId.Species, c);
|
|
|
|
if ((!mySpecies) || (!mySpecies.string)) {
|
|
return Species.Human; // best guess
|
|
}
|
|
|
|
const s = Matcher.getMappedSpecies(mySpecies.string);
|
|
|
|
if (!s) {
|
|
log.debug('matcher.species.unknown', { character: c.name, species: mySpecies.string });
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
static generateSpeciesMappingCache(mapping: SpeciesMap): SpeciesMappingCache {
|
|
return _.mapValues(
|
|
mapping,
|
|
(keywords: string[]) => _.map(
|
|
keywords,
|
|
(keyword: string) => {
|
|
const keywordPlural = `${keyword}s`; // this is weak: elf -> elves doesn't occur
|
|
return {
|
|
keyword,
|
|
regexp: RegExp(`(^|\\b)(${keyword}|${keywordPlural})($|\\b)`)
|
|
};
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
private static speciesMappingCache?: SpeciesMappingCache;
|
|
private static likelyHumanCache?: SpeciesMappingCache;
|
|
|
|
private static matchMappedSpecies(species: string, mapping: SpeciesMappingCache, skipAscii: boolean = false): Species | null {
|
|
let foundSpeciesId: Species | null = null;
|
|
let match = '';
|
|
|
|
const finalSpecies = (skipAscii ? species : anyAscii(species)).toLowerCase().trim();
|
|
|
|
_.each(
|
|
mapping,
|
|
(matchers, speciesId: string) => {
|
|
_.each(
|
|
matchers,
|
|
(matcher) => {
|
|
|
|
// finalSpecies.indexOf(k) >= 0)
|
|
if ((matcher.keyword.length > match.length) && (matcher.regexp.test(finalSpecies))) {
|
|
match = matcher.keyword;
|
|
foundSpeciesId = parseInt(speciesId, 10);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
);
|
|
|
|
return foundSpeciesId;
|
|
}
|
|
|
|
static getMappedSpecies(species: string): Species | null {
|
|
if (!Matcher.speciesMappingCache) {
|
|
Matcher.speciesMappingCache = Matcher.generateSpeciesMappingCache(speciesMapping);
|
|
}
|
|
|
|
if (!Matcher.likelyHumanCache) {
|
|
Matcher.likelyHumanCache = Matcher.generateSpeciesMappingCache(likelyHuman);
|
|
}
|
|
|
|
return Matcher.matchMappedSpecies(species, Matcher.speciesMappingCache)
|
|
|| Matcher.matchMappedSpecies(species, Matcher.speciesMappingCache, true)
|
|
|| Matcher.matchMappedSpecies(species, Matcher.likelyHumanCache)
|
|
|| Matcher.matchMappedSpecies(species, Matcher.likelyHumanCache, true);
|
|
}
|
|
|
|
static getAllSpecies(c: Character): Species[] {
|
|
const species = Matcher.getAllSpeciesAsStr(c);
|
|
return _.filter(_.map(species, (s) => Matcher.getMappedSpecies(s)), (s) => (s !== null)) as Species[];
|
|
}
|
|
|
|
static getAllSpeciesAsStr(c: Character): string[] {
|
|
const mySpecies = Matcher.getTagValue(TagId.Species, c);
|
|
|
|
if ((!mySpecies) || (!mySpecies.string)) {
|
|
return [];
|
|
}
|
|
|
|
const speciesStr = mySpecies.string.toLowerCase().replace(/optionally|alternatively/g, ',')
|
|
.replace(/[)(]/g, ' ').trim();
|
|
const matches = speciesStr.split(/[,]? or |,/);
|
|
|
|
return _.filter(_.map(matches, (m) => m.toLowerCase().trim()), (m) => (m !== ''));
|
|
}
|
|
|
|
static strToGender(fchatGenderStr: string | undefined): Gender | null {
|
|
if (fchatGenderStr === undefined) {
|
|
return null;
|
|
}
|
|
|
|
if (fchatGenderStr in fchatGenderMap) {
|
|
return fchatGenderMap[fchatGenderStr];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
static countScoresAtLevel(
|
|
m: MatchReport, scoreLevel: Scoring | null,
|
|
skipYours: boolean = false,
|
|
skipTheirs: boolean = false
|
|
): number | null {
|
|
if (scoreLevel === null) {
|
|
return null;
|
|
}
|
|
|
|
const yourScores = skipYours ? [] : _.values(m.you.scores);
|
|
const theirScores = skipTheirs ? [] : _.values(m.them.scores);
|
|
|
|
return _.reduce(
|
|
_.concat(yourScores, theirScores),
|
|
(accum: number, score: Score) => accum + (score.score === scoreLevel ? 1 : 0),
|
|
0
|
|
);
|
|
}
|
|
|
|
static countScoresAboveLevel(
|
|
m: MatchReport, scoreLevel: Scoring | null,
|
|
skipYours: boolean = false,
|
|
skipTheirs: boolean = false
|
|
): number {
|
|
if (scoreLevel === null) {
|
|
return 0;
|
|
}
|
|
|
|
const yourScores = skipYours ? [] : _.values(m.you.scores);
|
|
const theirScores = skipTheirs ? [] : _.values(m.them.scores);
|
|
|
|
return _.reduce(
|
|
_.concat(yourScores, theirScores),
|
|
(accum: number, score: Score) => accum + ((score.score > scoreLevel) && (score.score !== Scoring.NEUTRAL) ? 1 : 0),
|
|
0
|
|
);
|
|
}
|
|
|
|
static countScoresTotal(m: MatchReport): number {
|
|
return _.values(m.you.scores).length
|
|
+ _.values(m.them.scores).length;
|
|
}
|
|
|
|
static age(c: Character): number | null {
|
|
const rawAge = Matcher.getTagValue(TagId.Age, c);
|
|
|
|
if (!rawAge || !rawAge.string) {
|
|
return null;
|
|
}
|
|
|
|
const ageStr = rawAge.string.toLowerCase().replace(/[,.]/g, '').trim();
|
|
|
|
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0)
|
|
|| (ageStr.indexOf('lolli') >= 0) || (ageStr.indexOf('pup') >= 0)) {
|
|
return 10;
|
|
}
|
|
|
|
let age: number | null = null;
|
|
|
|
const exactMatch = /^[0-9]+$/.exec(ageStr);
|
|
const rangeMatch = exactMatch ? null : ageStr.match(/^([0-9]+)-([0-9]+)$/);
|
|
|
|
if (exactMatch) {
|
|
// '18'
|
|
age = parseInt(rawAge.string, 10);
|
|
} else if (rangeMatch) {
|
|
// '18-22'
|
|
const v1 = parseInt(rangeMatch[1], 10);
|
|
const v2 = parseInt(rangeMatch[2], 10);
|
|
|
|
age = Math.min(v1, v2);
|
|
}
|
|
|
|
return age && !Number.isNaN(age) && Number.isFinite(age) ? age : null;
|
|
}
|
|
|
|
static apparentAge(c: Character): { min: number, max: number } | null {
|
|
const rawAge = Matcher.getTagValue(TagId.ApparentAge, c);
|
|
|
|
if ((!rawAge) || (!rawAge.string)) {
|
|
return null;
|
|
}
|
|
|
|
const ageStr = rawAge.string.toLowerCase().replace(/[,.]/g, '').trim();
|
|
|
|
if (ageStr === '') {
|
|
return null;
|
|
}
|
|
|
|
// '18'
|
|
if (/^[0-9]+$/.exec(ageStr)) {
|
|
const val = parseInt(rawAge.string, 10);
|
|
|
|
return { min: val, max: val };
|
|
}
|
|
|
|
// '18-22'
|
|
const rangeMatch = ageStr.match(/^([0-9]+)-([0-9]+)$/);
|
|
|
|
if (rangeMatch) {
|
|
const v1 = parseInt(rangeMatch[1], 10);
|
|
const v2 = parseInt(rangeMatch[2], 10);
|
|
|
|
return { min: Math.min(v1, v2), max: Math.max(v1, v2) };
|
|
}
|
|
|
|
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0)
|
|
|| (ageStr.indexOf('lolli') >= 0) || (ageStr.indexOf('pup') >= 0)) {
|
|
return { min: 10, max: 10 };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static calculateSearchScoreForMatch(
|
|
score: Scoring,
|
|
match: MatchReport,
|
|
penalty: number
|
|
): number {
|
|
const totalScoreDimensions = match ? Matcher.countScoresTotal(match) : 0;
|
|
const dimensionsAtScoreLevel = match ? (Matcher.countScoresAtLevel(match, score) || 0) : 0;
|
|
const dimensionsAboveScoreLevel = match ? (Matcher.countScoresAboveLevel(match, Math.max(score, Scoring.WEAK_MATCH))) : 0;
|
|
|
|
let atLevelScore = 0;
|
|
let aboveLevelScore = 0;
|
|
|
|
let theirAtLevelDimensions = 0;
|
|
let atLevelMul = 0;
|
|
let theirAboveLevelDimensions = 0;
|
|
let aboveLevelMul = 0;
|
|
|
|
if ((dimensionsAtScoreLevel > 0) && (totalScoreDimensions > 0)) {
|
|
const matchRatio = dimensionsAtScoreLevel / totalScoreDimensions;
|
|
theirAtLevelDimensions = Matcher.countScoresAtLevel(match, score, true, false) || 0;
|
|
|
|
// 1.0 == bad balance; 0.0 == ideal balance
|
|
atLevelMul = Math.abs((theirAtLevelDimensions / (dimensionsAtScoreLevel)) - 0.5) * 2;
|
|
|
|
atLevelScore = (1 - (atLevelMul * 0.5)) * Math.pow(dimensionsAtScoreLevel, matchRatio);
|
|
}
|
|
|
|
if ((dimensionsAboveScoreLevel > 0) && (totalScoreDimensions > 0)) {
|
|
const matchRatio = dimensionsAboveScoreLevel / totalScoreDimensions;
|
|
|
|
theirAboveLevelDimensions = Matcher.countScoresAboveLevel(match, score, true, false) || 0;
|
|
|
|
// 1.0 == bad balance; 0.0 == ideal balance
|
|
aboveLevelMul = Math.abs((theirAboveLevelDimensions / (dimensionsAboveScoreLevel)) - 0.5) * 2;
|
|
|
|
aboveLevelScore = (1 - (aboveLevelMul * 0.5)) * Math.pow(dimensionsAboveScoreLevel, matchRatio);
|
|
}
|
|
|
|
// const kinkScore = match.you.kinkScore.weighted;
|
|
|
|
log.debug(
|
|
'report.score.search',
|
|
match.you.you.name,
|
|
match.them.you.name,
|
|
{
|
|
you: match.you.you.name,
|
|
them: match.them.you.name,
|
|
searchScore: (atLevelScore + aboveLevelScore),
|
|
atLevelScore,
|
|
aboveLevelScore,
|
|
atLevelMul,
|
|
aboveLevelMul,
|
|
dimensionsAboveScoreLevel,
|
|
dimensionsAtScoreLevel,
|
|
theirAtLevelDimensions,
|
|
theirAboveLevelDimensions,
|
|
penalty
|
|
}
|
|
);
|
|
|
|
return (atLevelScore + aboveLevelScore + penalty);
|
|
}
|
|
}
|