fchat-rising/learn/matcher.ts

1501 lines
53 KiB
TypeScript
Raw Permalink Normal View History

2020-10-04 21:30:54 +00:00
/* eslint-disable no-null-keyword, max-file-line-count */
2019-06-29 01:37:41 +00:00
import * as _ from 'lodash';
2021-03-06 20:59:28 +00:00
import { Character, CharacterInfotag, KinkChoice } from '../interfaces';
2020-10-04 21:30:54 +00:00
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
2020-10-24 20:47:50 +00:00
// tslint:disable-next-line ban-ts-ignore
// @ts-ignore
2020-10-24 19:43:25 +00:00
import anyAscii from 'any-ascii';
2021-03-25 20:53:37 +00:00
import { Store } from '../site/character_page/data_store';
2020-10-24 19:43:25 +00:00
2020-10-04 21:30:54 +00:00
import {
2022-12-24 20:50:07 +00:00
BodyType,
bodyTypeKinkMapping,
2021-03-25 20:53:37 +00:00
fchatGenderMap,
2020-10-04 21:30:54 +00:00
FurryPreference,
2021-03-25 20:53:37 +00:00
Gender,
genderKinkMapping,
Kink,
KinkBucketScore,
kinkComparisonExclusionGroups,
kinkComparisonExclusions,
kinkComparisonSwaps,
kinkMapping,
kinkMatchScoreMap,
kinkMatchWeights,
KinkPreference,
likelyHuman,
mammalSpecies,
nonAnthroSpecies,
2020-10-04 21:30:54 +00:00
Orientation,
2021-04-22 23:49:21 +00:00
Position,
2022-12-24 20:50:07 +00:00
PostLengthPreference,
postLengthPreferenceMapping,
postLengthPreferenceScoreMapping,
Scoring,
2021-03-25 20:53:37 +00:00
Species,
SpeciesMap,
speciesMapping,
SpeciesMappingCache,
2020-10-04 21:30:54 +00:00
speciesNames,
SubDomRole,
TagId
} from './matcher-types';
2019-06-29 01:37:41 +00:00
2019-07-07 01:37:15 +00:00
2019-06-29 01:37:41 +00:00
export interface MatchReport {
2021-03-06 20:59:28 +00:00
_isVue: true;
2019-06-29 20:59:29 +00:00
you: MatchResult;
them: MatchResult;
2020-10-04 21:30:54 +00:00
youMultiSpecies: boolean;
themMultiSpecies: boolean;
2020-10-24 23:17:31 +00:00
merged: MatchResultScores;
2020-10-04 21:30:54 +00:00
score: Scoring | null;
2020-11-21 20:41:08 +00:00
details: MatchScoreDetails;
2019-06-29 20:59:29 +00:00
}
export interface MatchResultCharacterInfo {
species: Species | null;
gender: Gender | null;
orientation: Orientation | null;
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
export interface MatchResultScores {
[key: number]: Score;
[TagId.Orientation]: Score;
[TagId.Gender]: Score;
[TagId.Age]: Score;
[TagId.FurryPreference]: Score;
[TagId.Species]: Score;
2021-03-06 20:59:28 +00:00
[TagId.Kinks]: Score;
2019-06-29 20:59:29 +00:00
}
2020-11-21 20:41:08 +00:00
export interface MatchScoreDetails {
totalScoreDimensions: number;
dimensionsAtScoreLevel: number;
}
2019-06-29 20:59:29 +00:00
export interface MatchResult {
you: Character,
them: Character,
scores: MatchResultScores;
info: MatchResultCharacterInfo;
2019-07-07 21:44:32 +00:00
total: number;
yourAnalysis: CharacterAnalysis;
theirAnalysis: CharacterAnalysis;
2019-06-29 20:59:29 +00:00
}
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'
};
2020-10-24 23:17:31 +00:00
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'
};
2019-06-29 20:59:29 +00:00
export class Score {
readonly score: Scoring;
readonly description: string;
2020-10-24 23:17:31 +00:00
readonly shortDesc: string;
2019-06-29 20:59:29 +00:00
2020-10-24 23:17:31 +00:00
constructor(score: Scoring, description: string = '', shortDesc: string = '') {
2019-06-29 20:59:29 +00:00
if ((score !== Scoring.NEUTRAL) && (description === ''))
throw new Error('Description must be provided if score is not neutral');
this.score = score;
this.description = description;
2020-10-24 23:17:31 +00:00
this.shortDesc = shortDesc;
2019-06-29 20:59:29 +00:00
}
getRecommendedClass(): string {
2019-07-07 01:37:15 +00:00
return Score.getClasses(this.score);
}
2020-10-24 23:17:31 +00:00
getRecommendedIcon(): string {
return Score.getIcon(this.score);
}
2019-07-07 01:37:15 +00:00
static getClasses(score: Scoring): string {
return scoreClasses[score];
2019-06-29 20:59:29 +00:00
}
2020-10-24 23:17:31 +00:00
static getIcon(score: Scoring): string {
return scoreIcons[score];
}
2019-06-29 20:59:29 +00:00
}
2020-10-04 21:30:54 +00:00
export interface CharacterAnalysisVariation {
readonly character: Character;
readonly analysis: CharacterAnalysis;
}
2019-07-07 21:44:32 +00:00
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;
2019-10-27 18:58:41 +00:00
readonly subDomRole: SubDomRole | null;
2021-04-22 23:49:21 +00:00
readonly position: Position | null;
2021-03-25 20:53:37 +00:00
readonly postLengthPreference: PostLengthPreference | null;
readonly bodyType: BodyType | null;
2019-07-07 21:44:32 +00:00
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);
2019-10-27 18:58:41 +00:00
this.subDomRole = Matcher.getTagValueList(TagId.SubDomRole, c);
2021-04-22 23:49:21 +00:00
this.position = Matcher.getTagValueList(TagId.Position, c);
2021-03-25 20:53:37 +00:00
this.postLengthPreference = Matcher.getTagValueList(TagId.PostLength, c);
this.bodyType = Matcher.getTagValueList(TagId.BodyType, c);
2019-07-07 21:44:32 +00:00
this.age = Matcher.age(c);
2019-07-07 21:44:32 +00:00
this.isAnthro = Matcher.isAnthro(c);
this.isHuman = Matcher.isHuman(c);
this.isMammal = Matcher.isMammal(c);
}
}
2019-06-29 20:59:29 +00:00
/**
* 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
*/
2019-06-29 01:37:41 +00:00
export class Matcher {
2019-07-07 21:44:32 +00:00
readonly you: Character;
readonly them: Character;
readonly yourAnalysis: CharacterAnalysis;
readonly theirAnalysis: CharacterAnalysis;
2019-06-29 01:37:41 +00:00
2019-07-07 21:44:32 +00:00
constructor(you: Character, them: Character, yourAnalysis?: CharacterAnalysis, theirAnalysis?: CharacterAnalysis) {
2019-06-29 01:37:41 +00:00
this.you = you;
this.them = them;
2019-07-07 21:44:32 +00:00
this.yourAnalysis = yourAnalysis || new CharacterAnalysis(you);
this.theirAnalysis = theirAnalysis || new CharacterAnalysis(them);
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
static generateReport(you: Character, them: Character): MatchReport {
2019-07-07 21:44:32 +00:00
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);
2019-06-29 20:59:29 +00:00
2021-03-21 21:28:13 +00:00
const youThemMatch = youThem.match('their');
const themYouMatch = themYou.match('your');
2020-10-24 23:17:31 +00:00
2020-10-04 21:30:54 +00:00
const report: MatchReport = {
2021-03-06 20:59:28 +00:00
_isVue: true,
2020-10-24 23:17:31 +00:00
you: youThemMatch,
them: themYouMatch,
2020-10-04 21:30:54 +00:00
youMultiSpecies: false,
themMultiSpecies: false,
2020-10-24 23:17:31 +00:00
merged: Matcher.mergeResults(youThemMatch, themYouMatch),
2020-11-21 20:41:08 +00:00
score: null,
details: {
totalScoreDimensions: 0,
dimensionsAtScoreLevel: 0
}
2019-06-29 20:59:29 +00:00
};
2020-10-04 21:30:54 +00:00
report.score = Matcher.calculateReportScore(report);
2020-11-21 20:41:08 +00:00
report.details.totalScoreDimensions = Matcher.countScoresTotal(report);
report.details.dimensionsAtScoreLevel = Matcher.countScoresAtLevel(report, report.score) || 0;
2021-03-06 20:59:28 +00:00
// log.debug('report.generate', report);
2020-10-04 21:30:54 +00:00
return report;
}
2024-07-06 03:38:57 +00:00
static getYiffBot4000MatchReport(you: Character, them: Character): MatchReport {
const scores: MatchResultScores = {
[TagId.Orientation]: new Score(Scoring.MATCH, 'Perfect match!'),
[TagId.Gender]: new Score(Scoring.MATCH, 'Perfect match!'),
[TagId.Age]: new Score(Scoring.MATCH, 'Perfect match!'),
[TagId.FurryPreference]: new Score(Scoring.MATCH, 'Perfect match!'),
[TagId.Species]: new Score(Scoring.MATCH, 'Perfect match!'),
[TagId.SubDomRole]: new Score(Scoring.MATCH, 'Perfect match!'),
[TagId.Kinks]: new Score(Scoring.MATCH, 'Perfect match!'),
[TagId.PostLength]: new Score(Scoring.MATCH, 'Perfect match!'),
[TagId.Position]: new Score(Scoring.MATCH, 'Perfect match!'),
[TagId.BodyType]: new Score(Scoring.MATCH, 'Perfect match!')
};
const yourAnalysis = new CharacterAnalysis(you);
const theirAnalysis = new CharacterAnalysis(them);
return {
_isVue: true,
you: {
you,
them,
scores,
info: {
species: Matcher.species(you),
gender: Matcher.getTagValueList(TagId.Gender, you),
orientation: Matcher.getTagValueList(TagId.Orientation, you)
},
total: _.sum(_.values(scores).map((s: Score) => s.score)),
yourAnalysis,
theirAnalysis
},
them: {
you: them,
them: you,
scores,
info: {
species: Matcher.species(them),
gender: Matcher.getTagValueList(TagId.Gender, them),
orientation: Matcher.getTagValueList(TagId.Orientation, them)
} ,
total: _.sum(_.values(scores).map((s: Score) => s.score)),
yourAnalysis: theirAnalysis,
theirAnalysis: yourAnalysis
},
youMultiSpecies: false,
themMultiSpecies: false,
merged: scores,
score: Scoring.MATCH,
details: {
totalScoreDimensions: _.values(scores).length * 2,
dimensionsAtScoreLevel: _.values(scores).length * 2
}
};
}
2020-10-04 21:30:54 +00:00
static identifyBestMatchReport(you: Character, them: Character): MatchReport {
const reportStartTime = Date.now();
const yourCharacterAnalyses = Matcher.generateAnalysisVariations(you);
const theirCharacterAnalyses = Matcher.generateAnalysisVariations(them);
let bestScore = null;
2020-10-04 21:48:48 +00:00
let bestScoreLevelCount = -10000;
2020-10-04 21:30:54 +00:00
let bestReport: MatchReport;
2024-07-06 03:38:57 +00:00
if (you.name === 'YiffBot 4000' || them.name === 'YiffBot 4000') {
return Matcher.getYiffBot4000MatchReport(you, them);
}
2020-10-04 21:30:54 +00:00
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);
2021-03-21 21:28:13 +00:00
const youThemMatch = youThem.match('their');
const themYouMatch = themYou.match('your');
2020-10-24 23:17:31 +00:00
2020-10-04 21:30:54 +00:00
const report: MatchReport = {
2021-03-06 20:59:28 +00:00
_isVue: true,
2020-10-24 23:17:31 +00:00
you: youThemMatch,
them: themYouMatch,
2020-10-04 21:30:54 +00:00
youMultiSpecies: (yourCharacterAnalyses.length > 1),
themMultiSpecies: (theirCharacterAnalyses.length > 1),
2020-10-24 23:17:31 +00:00
merged: Matcher.mergeResults(youThemMatch, themYouMatch),
2020-11-21 20:41:08 +00:00
score: null,
details: {
totalScoreDimensions: 0,
dimensionsAtScoreLevel: 0
}
2020-10-04 21:30:54 +00:00
};
report.score = Matcher.calculateReportScore(report);
const scoreLevelCount = Matcher.countScoresAtLevel(report, report.score);
2020-11-21 20:41:08 +00:00
report.details.totalScoreDimensions = Matcher.countScoresTotal(report);
report.details.dimensionsAtScoreLevel = scoreLevelCount || 0;
2020-10-04 21:30:54 +00:00
if (
(bestScore === null)
|| (
(report.score !== null)
&& (report.score >= bestScore)
&& (scoreLevelCount !== null)
2020-10-04 21:48:48 +00:00
&& (report.score * scoreLevelCount > bestScoreLevelCount)
2020-10-04 21:30:54 +00:00
)
) {
bestScore = report.score;
2020-10-04 21:48:48 +00:00
bestScoreLevelCount = ((scoreLevelCount !== null) && (report.score !== null)) ? report.score * scoreLevelCount : -1000;
2020-10-04 21:30:54 +00:00
bestReport = report;
}
}
}
2020-11-21 20:41:08 +00:00
log.debug(
'report.identify.best',
2021-03-06 20:59:28 +00:00
{
buildTime: Date.now() - reportStartTime,
variations: yourCharacterAnalyses.length * theirCharacterAnalyses.length,
report: bestReport!
}
2020-11-21 20:41:08 +00:00
);
2020-10-04 21:30:54 +00:00
return bestReport!;
}
2020-10-24 23:17:31 +00:00
// 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;
}
2020-10-04 21:30:54 +00:00
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}}};
2020-10-04 21:30:54 +00:00
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;
2019-06-29 01:37:41 +00:00
}
2021-03-21 21:28:13 +00:00
match(pronoun: string): MatchResult {
2019-07-07 21:44:32 +00:00
const data: MatchResult = {
2019-06-29 20:59:29 +00:00
you: this.you,
them: this.them,
2019-07-07 21:44:32 +00:00
yourAnalysis: this.yourAnalysis,
theirAnalysis: this.theirAnalysis,
2019-07-07 01:37:15 +00:00
total: 0,
2019-06-29 20:59:29 +00:00
scores: {
[TagId.Orientation]: this.resolveOrientationScore(),
[TagId.Gender]: this.resolveGenderScore(),
[TagId.Age]: this.resolveAgeScore(),
[TagId.FurryPreference]: this.resolveFurryPairingsScore(),
2019-10-27 18:58:41 +00:00
[TagId.Species]: this.resolveSpeciesScore(),
2021-03-06 20:59:28 +00:00
[TagId.SubDomRole]: this.resolveSubDomScore(),
2021-03-25 20:53:37 +00:00
[TagId.Kinks]: this.resolveKinkScore(pronoun),
2021-04-22 23:49:21 +00:00
[TagId.PostLength]: this.resolvePostLengthScore(),
[TagId.Position]: this.resolvePositionScore(),
[TagId.BodyType]: this.resolveBodyTypeScore()
2019-06-29 20:59:29 +00:00
},
info: {
species: Matcher.species(this.you),
gender: Matcher.getTagValueList(TagId.Gender, this.you),
2019-07-06 16:49:19 +00:00
orientation: Matcher.getTagValueList(TagId.Orientation, this.you)
2019-06-29 20:59:29 +00:00
}
2019-07-07 01:37:15 +00:00
};
data.total = _.reduce(
data.scores,
(accum: number, s: Score) => (accum + s.score),
0
);
return data;
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
private resolveOrientationScore(): Score {
// Question: If someone identifies themselves as 'straight cuntboy', how should they be matched? like a straight female?
2019-07-07 21:44:32 +00:00
return Matcher.scoreOrientationByGender(this.yourAnalysis.gender, this.yourAnalysis.orientation, this.theirAnalysis.gender);
2019-07-07 01:37:15 +00:00
}
2019-07-07 21:44:32 +00:00
static scoreOrientationByGender(yourGender: Gender | null, yourOrientation: Orientation | null, theirGender: Gender | null): Score {
2024-07-06 03:38:57 +00:00
if ((yourGender === null) || (theirGender === null) || (yourOrientation === null) || yourGender === Gender.None || theirGender === Gender.None)
2019-07-07 21:44:32 +00:00
return new Score(Scoring.NEUTRAL);
2019-07-07 01:37:15 +00:00
2019-06-29 20:59:29 +00:00
// CIS
2019-07-06 16:49:19 +00:00
// tslint:disable-next-line curly
2019-07-07 21:44:32 +00:00
if (Matcher.isCisGender(yourGender)) {
2019-06-29 20:59:29 +00:00
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>');
}
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
return new Score(Scoring.NEUTRAL);
2019-06-29 01:37:41 +00:00
}
2019-07-06 16:49:19 +00:00
static formatKinkScore(score: KinkPreference, description: string): Score {
2019-06-29 20:59:29 +00:00
if (score === KinkPreference.No)
return new Score(Scoring.MISMATCH, `No <span>${description}</span>`);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (score === KinkPreference.Maybe)
2020-06-30 16:51:56 +00:00
return new Score(Scoring.WEAK_MISMATCH, `Hesitant about <span>${description}</span>`);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (score === KinkPreference.Yes)
return new Score(Scoring.WEAK_MATCH, `Likes <span>${description}</span>`);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (score === KinkPreference.Favorite)
return new Score(Scoring.MATCH, `Loves <span>${description}</span>`);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
return new Score(Scoring.NEUTRAL);
2019-06-29 01:37:41 +00:00
}
2021-03-25 20:53:37 +00:00
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]);
}
2023-03-12 05:43:58 +00:00
static getSpeciesName(species: Species): string {
return speciesNames[species] || `${Species[species].toLowerCase()}s`;
}
2019-06-29 20:59:29 +00:00
private resolveSpeciesScore(): Score {
const you = this.you;
2019-07-07 21:44:32 +00:00
const theirAnalysis = this.theirAnalysis;
const theirSpecies = theirAnalysis.species;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (theirSpecies === null)
return new Score(Scoring.NEUTRAL);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
const speciesScore = Matcher.getKinkSpeciesPreference(you, theirSpecies);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (speciesScore !== null) {
2020-10-18 22:52:56 +00:00
// console.log(this.them.name, speciesScore, theirSpecies);
2019-06-29 20:59:29 +00:00
const speciesName = speciesNames[theirSpecies] || `${Species[theirSpecies].toLowerCase()}s`;
2019-06-29 01:37:41 +00:00
2019-07-06 16:49:19 +00:00
return Matcher.formatKinkScore(speciesScore, speciesName);
2019-06-29 01:37:41 +00:00
}
2019-07-07 21:44:32 +00:00
if (theirAnalysis.isAnthro) {
2019-07-02 01:03:28 +00:00
const anthroScore = Matcher.getKinkPreference(you, Kink.AnthroCharacters);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (anthroScore !== null)
2019-07-06 16:49:19 +00:00
return Matcher.formatKinkScore(anthroScore, 'anthros');
2019-06-29 01:37:41 +00:00
}
2019-07-07 21:44:32 +00:00
if (theirAnalysis.isMammal) {
2019-07-02 01:03:28 +00:00
const mammalScore = Matcher.getKinkPreference(you, Kink.Mammals);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (mammalScore !== null)
2019-07-06 16:49:19 +00:00
return Matcher.formatKinkScore(mammalScore, 'mammals');
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
return new Score(Scoring.NEUTRAL);
2019-06-29 01:37:41 +00:00
}
2019-07-07 21:44:32 +00:00
2019-06-29 20:59:29 +00:00
formatScoring(score: Scoring, description: string): Score {
let type = '';
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
switch (score) {
case Scoring.MISMATCH:
type = 'No';
break;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
case Scoring.WEAK_MISMATCH:
2020-06-30 16:51:56 +00:00
type = 'Hesitant about';
2019-06-29 20:59:29 +00:00
break;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
case Scoring.WEAK_MATCH:
type = 'Likes';
break;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
case Scoring.MATCH:
type = 'Loves';
break;
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
return new Score(score, `${type} <span>${description}</span>`);
}
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
private resolveFurryPairingsScore(): Score {
const you = this.you;
2019-07-07 21:44:32 +00:00
const theyAreAnthro = this.theirAnalysis.isAnthro;
const theyAreHuman = this.theirAnalysis.isHuman;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
const score = theyAreAnthro
? Matcher.furryLikeabilityScore(you)
: (theyAreHuman ? Matcher.humanLikeabilityScore(you) : Scoring.NEUTRAL);
2019-06-29 01:37:41 +00:00
2019-06-29 22:24:44 +00:00
if (score === Scoring.WEAK_MATCH)
2019-07-06 16:49:19 +00:00
return new Score(
score,
theyAreAnthro
2020-03-15 02:31:28 +00:00
? 'Prefers <span>humans</span>, ok with anthros'
: 'Prefers <span>anthros</span>, ok with humans'
2019-07-06 16:49:19 +00:00
);
2019-06-29 22:24:44 +00:00
2019-06-29 20:59:29 +00:00
return this.formatScoring(score, theyAreAnthro ? 'furry pairings' : theyAreHuman ? 'human pairings' : '');
2019-06-29 01:37:41 +00:00
}
2021-03-06 20:59:28 +00:00
2021-03-21 21:28:13 +00:00
private resolveKinkScore(pronoun: string): Score {
2021-03-25 20:53:37 +00:00
// const kinkScore = this.resolveKinkBucketScore('all');
2021-03-06 20:59:28 +00:00
2021-03-25 20:53:37 +00:00
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);
2021-03-06 20:59:28 +00:00
2022-12-24 20:50:07 +00:00
if (scores.favorite.count + scores.yes.count + scores.maybe.count + scores.no.count < 10) {
return new Score(Scoring.NEUTRAL);
}
2021-03-25 20:53:37 +00:00
if (weighted === 0) {
2021-03-06 20:59:28 +00:00
return new Score(Scoring.NEUTRAL);
}
2021-03-25 20:53:37 +00:00
if (weighted < 0) {
if (Math.abs(weighted) < kinkMatchWeights.weakMismatchThreshold) {
2021-03-21 21:28:13 +00:00
return new Score(Scoring.WEAK_MISMATCH, `Hesitant about ${pronoun} <span>kinks</span>`);
2021-03-06 20:59:28 +00:00
}
2021-03-21 21:28:13 +00:00
return new Score(Scoring.MISMATCH, `Dislikes ${pronoun} <span>kinks</span>`);
2021-03-06 20:59:28 +00:00
}
2021-03-25 20:53:37 +00:00
if (Math.abs(weighted) < kinkMatchWeights.weakMatchThreshold) {
2021-03-21 21:28:13 +00:00
return new Score(Scoring.WEAK_MATCH, `Likes ${pronoun} <span>kinks</span>`);
2021-03-06 20:59:28 +00:00
}
2021-03-21 21:28:13 +00:00
return new Score(Scoring.MATCH, `Loves ${pronoun} <span>kinks</span>`);
2021-03-06 20:59:28 +00:00
}
2019-06-29 20:59:29 +00:00
static furryLikeabilityScore(c: Character): Scoring {
const furryPreference = Matcher.getTagValueList(TagId.FurryPreference, c);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (
(furryPreference === FurryPreference.FursAndHumans) ||
(furryPreference === FurryPreference.FurriesPreferredHumansOk) ||
(furryPreference === FurryPreference.FurriesOnly)
)
return Scoring.MATCH;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (furryPreference === FurryPreference.HumansPreferredFurriesOk)
return Scoring.WEAK_MATCH;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (furryPreference === FurryPreference.HumansOnly)
return Scoring.MISMATCH;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
return Scoring.NEUTRAL;
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
static humanLikeabilityScore(c: Character): Scoring {
const humanPreference = Matcher.getTagValueList(TagId.FurryPreference, c);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (
(humanPreference === FurryPreference.FursAndHumans)
|| (humanPreference === FurryPreference.HumansPreferredFurriesOk)
|| (humanPreference === FurryPreference.HumansOnly)
)
return Scoring.MATCH;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (humanPreference === FurryPreference.FurriesPreferredHumansOk)
return Scoring.WEAK_MATCH;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (humanPreference === FurryPreference.FurriesOnly)
return Scoring.MISMATCH;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
return Scoring.NEUTRAL;
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
private resolveAgeScore(): Score {
2019-06-29 01:37:41 +00:00
const you = this.you;
2019-07-07 21:44:32 +00:00
const theirAge = this.theirAnalysis.age;
2019-06-29 01:37:41 +00:00
2019-07-07 21:44:32 +00:00
if (theirAge === null)
2019-06-29 20:59:29 +00:00
return new Score(Scoring.NEUTRAL);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
const ageplayScore = Matcher.getKinkPreference(you, Kink.Ageplay);
const underageScore = Matcher.getKinkPreference(you, Kink.UnderageCharacters);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if ((theirAge < 16) && (ageplayScore !== null))
2019-07-06 16:49:19 +00:00
return Matcher.formatKinkScore(ageplayScore, `ages of ${theirAge}`);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if ((theirAge < 16) && (ageplayScore === null))
2019-07-06 16:49:19 +00:00
return Matcher.formatKinkScore(KinkPreference.No, `ages of ${theirAge}`);
2019-06-29 01:37:41 +00:00
2019-09-24 19:53:43 +00:00
if ((theirAge < 18) && (theirAge >= 16) && (underageScore !== null))
2019-07-06 16:49:19 +00:00
return Matcher.formatKinkScore(underageScore, `ages of ${theirAge}`);
2019-06-29 01:37:41 +00:00
2019-07-07 21:44:32 +00:00
const yourAge = this.yourAnalysis.age;
2019-07-11 00:36:11 +00:00
if ((yourAge !== null) && (yourAge > 0) && (theirAge > 0) && (yourAge <= 80) && (theirAge <= 80)) {
2019-06-29 20:59:29 +00:00
const olderCharactersScore = Matcher.getKinkPreference(you, Kink.OlderCharacters);
const youngerCharactersScore = Matcher.getKinkPreference(you, Kink.YoungerCharacters);
2019-07-11 00:38:09 +00:00
const ageDifference = Math.abs(yourAge - theirAge);
2019-06-29 01:37:41 +00:00
2020-07-01 14:33:29 +00:00
if ((yourAge < theirAge) && (olderCharactersScore !== null) && (ageDifference >= 8))
2019-07-06 16:49:19 +00:00
return Matcher.formatKinkScore(olderCharactersScore, 'older characters');
2019-06-29 01:37:41 +00:00
2020-07-01 14:33:29 +00:00
if ((yourAge > theirAge) && (youngerCharactersScore !== null) && (ageDifference >= 8))
2019-07-06 16:49:19 +00:00
return Matcher.formatKinkScore(youngerCharactersScore, 'younger characters');
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
return new Score(Scoring.NEUTRAL);
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
private resolveGenderScore(): Score {
const you = this.you;
2023-03-15 01:28:50 +00:00
const yourGender = this.yourAnalysis.gender;
const yourOrientation = this.yourAnalysis.orientation;
2019-07-07 21:44:32 +00:00
const theirGender = this.theirAnalysis.gender;
2019-06-29 01:37:41 +00:00
2024-07-06 03:38:57 +00:00
if (theirGender === null) {
2019-06-29 20:59:29 +00:00
return new Score(Scoring.NEUTRAL);
2024-07-06 03:38:57 +00:00
}
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
const genderName = `${Gender[theirGender].toLowerCase()}s`;
const genderKinkScore = Matcher.getKinkGenderPreference(you, theirGender);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (genderKinkScore !== null)
2019-07-06 16:49:19 +00:00
return Matcher.formatKinkScore(genderKinkScore, genderName);
2019-06-29 01:37:41 +00:00
2023-03-15 01:28:50 +00:00
if (yourGender && yourOrientation) {
if (Matcher.isCisGender(yourGender) && !Matcher.isCisGender(theirGender)) {
if ([
Orientation.Straight,
Orientation.Gay,
Orientation.Bisexual,
Orientation.BiCurious,
Orientation.BiFemalePreference,
Orientation.BiMalePreference
].includes(yourOrientation)) {
2024-07-06 01:03:36 +00:00
const nonBinaryPref = Matcher.getKinkPreference(you, Kink.Nonbinary);
if(nonBinaryPref) {
return Matcher.formatKinkScore(nonBinaryPref, 'non-binary genders');
}
2023-03-15 01:28:50 +00:00
return new Score(Scoring.MISMATCH, 'No <span>non-binary</span> genders');
}
}
}
2019-06-29 20:59:29 +00:00
return new Score(Scoring.NEUTRAL);
2019-06-29 01:37:41 +00:00
}
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) {
2022-08-09 00:15:43 +00:00
return Matcher.formatKinkScore(bodyTypePreference, `${BodyType[theirBodyType].toLowerCase()}s`);
}
}
return new Score(Scoring.NEUTRAL);
}
2019-10-27 18:58:41 +00:00
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);
2020-11-21 20:41:08 +00:00
if (yourSubDomRole === SubDomRole.UsuallyDominant) {
if (theirSubDomRole === SubDomRole.Switch)
2021-04-22 23:49:21 +00:00
return new Score(Scoring.MATCH, `Loves <span>switches</span> (role)`);
2020-11-21 20:41:08 +00:00
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) {
2019-10-27 18:58:41 +00:00
if (theirSubDomRole === SubDomRole.Switch)
2021-04-22 23:49:21 +00:00
return new Score(Scoring.WEAK_MATCH, `Likes <span>switches</span> (role)`);
2019-10-27 18:58:41 +00:00
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>');
2020-06-30 16:51:56 +00:00
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>dominants</span>');
2020-03-15 14:02:31 +00:00
}
2020-11-21 20:41:08 +00:00
if (yourSubDomRole === SubDomRole.UsuallySubmissive) {
if (theirSubDomRole === SubDomRole.Switch)
2021-04-22 23:49:21 +00:00
return new Score(Scoring.MATCH, `Loves <span>switches</span> (role)`);
2020-11-21 20:41:08 +00:00
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) {
2019-10-27 18:58:41 +00:00
if (theirSubDomRole === SubDomRole.Switch)
2021-04-22 23:49:21 +00:00
return new Score(Scoring.WEAK_MATCH, `Likes <span>switches</span> (role)`);
2019-10-27 18:58:41 +00:00
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>');
2020-06-30 16:51:56 +00:00
return new Score(Scoring.WEAK_MISMATCH, 'Hesitant about <span>submissives</span>');
2019-10-27 18:58:41 +00:00
}
// You must be a switch
if (theirSubDomRole === SubDomRole.Switch)
2021-04-22 23:49:21 +00:00
return new Score(Scoring.MATCH, `Loves <span>switches</span> (role)`);
2019-10-27 18:58:41 +00:00
// 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);
}
2021-04-22 23:49:21 +00:00
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);
}
2019-10-27 18:58:41 +00:00
2021-03-06 20:59:28 +00:00
private resolveKinkBucketScore(bucket: 'all' | 'favorite' | 'yes' | 'maybe' | 'no' | 'positive' | 'negative'): KinkBucketScore {
const yourKinks = Matcher.getAllStandardKinks(this.you);
const theirKinks = Matcher.getAllStandardKinks(this.them);
2021-03-06 20:59:28 +00:00
2021-03-21 21:28:13 +00:00
// let missed = 0;
2021-03-06 20:59:28 +00:00
const result: any = _.reduce(
yourKinks,
(accum, yourKinkValue: any, yourKinkId: any) => {
const theirKinkId = (yourKinkId in kinkComparisonSwaps) ? kinkComparisonSwaps[yourKinkId] : yourKinkId;
2021-03-25 20:53:37 +00:00
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;
}
2021-03-06 20:59:28 +00:00
if (
(!(theirKinkId in theirKinks))
2021-03-25 20:53:37 +00:00
|| (isExcluded)
2021-03-06 20:59:28 +00:00
) {
return accum;
}
const theirKinkValue = theirKinks[theirKinkId] as any;
2021-03-25 20:53:37 +00:00
if (isBucketMatch) {
2021-03-06 20:59:28 +00:00
return {
score: accum.score + this.getKinkMatchScore(yourKinkValue, theirKinkValue),
2021-03-25 20:53:37 +00:00
count: accum.count + 1,
total: accum.total
2021-03-06 20:59:28 +00:00
};
}
return accum;
},
2021-03-25 20:53:37 +00:00
{ score: 0, count: 0, total: 0 }
2021-03-06 20:59:28 +00:00
);
2021-03-21 21:28:13 +00:00
// const yourBucketCounts = this.countKinksByBucket(yourKinks);
// const theirBucketCounts = this.countKinksByBucket(theirKinks);
2021-03-25 20:53:37 +00:00
result.weighted = ((result.count === 0) || (Math.abs(result.score) < 1))
2021-03-06 20:59:28 +00:00
? 0
: (
2021-03-25 20:53:37 +00:00
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)
2021-03-06 20:59:28 +00:00
);
return result;
}
2021-03-21 21:28:13 +00:00
// 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
// }
// );
// }
static getAllStandardKinks(c: Character): { [key: number]: KinkChoice } {
2021-03-06 20:59:28 +00:00
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 ?? {};
2021-03-06 20:59:28 +00:00
_.each(children, (child) => kinks[child] = custom.choice);
}
}
2021-03-06 20:59:28 +00:00
return kinks as any;
}
static findKinkById(c: Character, kinkId: number): KinkChoice | number | undefined {
if (kinkId in c.kinks) {
return c.kinks[kinkId];
}
for (const custom of Object.values(c.customs)) {
if (custom) {
const children = (custom as any).children ?? [];
if (children.includes(kinkId)) {
return custom.choice;
}
}
}
return undefined;
}
2021-03-06 20:59:28 +00:00
private getKinkMatchScore(aValue: string, bValue: string): number {
2021-03-25 20:53:37 +00:00
return _.get(kinkMatchScoreMap, `${aValue}.${bValue}`, 0) * 7; // forces range above 1.0
2021-03-06 20:59:28 +00:00
}
2019-06-29 20:59:29 +00:00
static getTagValue(tagId: number, c: Character): CharacterInfotag | undefined {
return c.infotags[tagId];
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
static getTagValueList(tagId: number, c: Character): number | null {
2020-03-15 14:02:31 +00:00
const t = Matcher.getTagValue(tagId, c);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if ((!t) || (!t.list))
2019-06-29 01:37:41 +00:00
return null;
2019-06-29 20:59:29 +00:00
return t.list;
2019-06-29 01:37:41 +00:00
}
2019-07-07 21:44:32 +00:00
static isCisGender(...genders: Gender[] | null[]): boolean {
2019-06-29 20:59:29 +00:00
return _.every(genders, (g: Gender) => ((g === Gender.Female) || (g === Gender.Male)));
2019-06-29 01:37:41 +00:00
}
static getKinkPreference(c: Character, kinkId: number): KinkPreference | null {
const kinkVal = Matcher.findKinkById(c, kinkId);
if (kinkVal === undefined) {
return null;
}
if (typeof kinkVal === 'string') {
return kinkMapping[kinkVal];
}
const custom = c.customs[kinkVal];
if (!custom) {
return null;
}
return kinkMapping[custom.choice];
2019-06-29 01:37:41 +00:00
}
static getKinkGenderPreference(c: Character, gender: Gender): KinkPreference | null {
2024-07-06 01:03:36 +00:00
if (!(gender in genderKinkMapping)) {
2019-06-29 01:37:41 +00:00
return null;
2024-07-06 01:03:36 +00:00
}
2019-06-29 01:37:41 +00:00
2020-03-15 14:02:31 +00:00
return Matcher.getKinkPreference(c, genderKinkMapping[gender]);
2019-06-29 01:37:41 +00:00
}
static getKinkSpeciesPreference(c: Character, species: Species): KinkPreference | null {
2020-03-15 14:02:31 +00:00
return Matcher.getKinkPreference(c, species);
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
static has(c: Character, kinkId: Kink): boolean {
2019-06-29 01:37:41 +00:00
const r = Matcher.getKinkPreference(c, kinkId);
2019-06-29 20:59:29 +00:00
return (r !== null);
2019-06-29 01:37:41 +00:00
}
2019-06-29 20:59:29 +00:00
static isMammal(c: Character): boolean | null {
const species = Matcher.species(c);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (species === null)
return null;
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
return (mammalSpecies.indexOf(species) >= 0);
2019-06-29 01:37:41 +00:00
}
static isAnthro(c: Character): boolean | null {
2020-03-15 14:02:31 +00:00
const bodyTypeId = Matcher.getTagValueList(TagId.BodyType, c);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (bodyTypeId === BodyType.Anthro)
2019-06-29 01:37:41 +00:00
return true;
2020-03-15 14:02:31 +00:00
const speciesId = Matcher.species(c);
2019-06-29 01:37:41 +00:00
if (!speciesId)
return null;
2019-06-29 20:59:29 +00:00
return (nonAnthroSpecies.indexOf(speciesId) < 0);
2019-06-29 01:37:41 +00:00
}
static isHuman(c: Character): boolean | null {
2020-03-15 14:02:31 +00:00
const bodyTypeId = Matcher.getTagValueList(TagId.BodyType, c);
2019-06-29 01:37:41 +00:00
2019-06-29 20:59:29 +00:00
if (bodyTypeId === BodyType.Human)
2019-06-29 01:37:41 +00:00
return true;
2020-03-15 14:02:31 +00:00
const speciesId = Matcher.species(c);
2019-06-29 01:37:41 +00:00
return (speciesId === Species.Human);
}
static species(c: Character): Species | null {
2020-03-15 14:02:31 +00:00
const mySpecies = Matcher.getTagValue(TagId.Species, c);
2019-06-29 01:37:41 +00:00
2020-04-11 17:46:57 +00:00
if ((!mySpecies) || (!mySpecies.string)) {
2019-06-29 01:37:41 +00:00
return Species.Human; // best guess
2020-04-11 17:46:57 +00:00
}
2019-06-29 01:37:41 +00:00
2020-10-18 22:52:56 +00:00
const s = Matcher.getMappedSpecies(mySpecies.string);
if (!s) {
2020-10-30 22:47:33 +00:00
log.debug('matcher.species.unknown', { character: c.name, species: mySpecies.string });
2020-10-18 22:52:56 +00:00
}
return s;
2020-10-04 21:30:54 +00:00
}
2020-10-18 22:52:56 +00:00
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;
2020-10-24 19:43:25 +00:00
private static matchMappedSpecies(species: string, mapping: SpeciesMappingCache, skipAscii: boolean = false): Species | null {
2020-10-04 21:30:54 +00:00
let foundSpeciesId: Species | null = null;
let match = '';
2020-10-24 19:43:25 +00:00
const finalSpecies = (skipAscii ? species : anyAscii(species)).toLowerCase().trim();
2019-06-29 01:37:41 +00:00
_.each(
2020-10-18 22:52:56 +00:00
mapping,
(matchers, speciesId: string) => {
2019-06-29 01:37:41 +00:00
_.each(
2020-10-18 22:52:56 +00:00
matchers,
(matcher) => {
// finalSpecies.indexOf(k) >= 0)
if ((matcher.keyword.length > match.length) && (matcher.regexp.test(finalSpecies))) {
match = matcher.keyword;
2019-07-06 16:49:19 +00:00
foundSpeciesId = parseInt(speciesId, 10);
2019-06-29 01:37:41 +00:00
}
}
);
}
);
2019-09-24 19:53:43 +00:00
return foundSpeciesId;
2019-06-29 01:37:41 +00:00
}
2019-07-07 01:37:15 +00:00
2020-10-18 22:52:56 +00:00
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)
2020-10-24 19:43:25 +00:00
|| Matcher.matchMappedSpecies(species, Matcher.speciesMappingCache, true)
|| Matcher.matchMappedSpecies(species, Matcher.likelyHumanCache)
|| Matcher.matchMappedSpecies(species, Matcher.likelyHumanCache, true);
2020-10-18 22:52:56 +00:00
}
2020-10-04 21:30:54 +00:00
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 [];
}
2020-10-18 22:52:56 +00:00
const speciesStr = mySpecies.string.toLowerCase().replace(/optionally|alternatively/g, ',')
.replace(/[)(]/g, ' ').trim();
2020-10-04 21:30:54 +00:00
const matches = speciesStr.split(/[,]? or |,/);
return _.filter(_.map(matches, (m) => m.toLowerCase().trim()), (m) => (m !== ''));
}
2019-07-07 01:37:15 +00:00
static strToGender(fchatGenderStr: string | undefined): Gender | null {
if (fchatGenderStr === undefined) {
return null;
}
if (fchatGenderStr in fchatGenderMap) {
return fchatGenderMap[fchatGenderStr];
}
return null;
}
2020-11-21 20:41:08 +00:00
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;
}
2022-01-01 00:06:08 +00:00
static age(c: Character): number | null {
const rawAge = Matcher.getTagValue(TagId.Age, c);
2022-01-02 22:37:57 +00:00
if (!rawAge || !rawAge.string) {
return null;
}
2022-03-19 22:44:53 +00:00
const ageStr = rawAge.string.toLowerCase().replace(/[,.]/g, '').trim();
2022-01-02 22:37:57 +00:00
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0)
|| (ageStr.indexOf('lolli') >= 0) || (ageStr.indexOf('pup') >= 0)) {
2022-01-02 22:37:57 +00:00
return 10;
}
2022-03-19 22:44:53 +00:00
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);
}
2022-01-01 00:06:08 +00:00
return age && !Number.isNaN(age) && Number.isFinite(age) ? age : null;
}
2022-01-02 22:37:57 +00:00
static apparentAge(c: Character): { min: number, max: number } | null {
const rawAge = Matcher.getTagValue(TagId.ApparentAge, c);
if ((!rawAge) || (!rawAge.string)) {
return null;
}
2022-03-19 22:44:53 +00:00
const ageStr = rawAge.string.toLowerCase().replace(/[,.]/g, '').trim();
2022-01-02 22:37:57 +00:00
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)) {
2022-01-02 22:37:57 +00:00
return { min: 10, max: 10 };
}
return null;
}
2020-11-21 20:41:08 +00:00
static calculateSearchScoreForMatch(
score: Scoring,
2022-01-01 00:06:08 +00:00
match: MatchReport,
penalty: number
2020-11-21 20:41:08 +00:00
): number {
2024-07-06 03:38:57 +00:00
if (match.you.you.name === 'YiffBot 4000' || match.you.them.name === 'YiffBot 4000') {
return kinkMatchWeights.unicornThreshold;
}
2020-11-21 20:41:08 +00:00
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);
}
2021-03-06 20:59:28 +00:00
// const kinkScore = match.you.kinkScore.weighted;
2020-11-21 20:41:08 +00:00
log.debug(
'report.score.search',
match.you.you.name,
match.them.you.name,
2020-11-21 20:41:08 +00:00
{
you: match.you.you.name,
them: match.them.you.name,
searchScore: (atLevelScore + aboveLevelScore),
atLevelScore,
aboveLevelScore,
atLevelMul,
aboveLevelMul,
dimensionsAboveScoreLevel,
dimensionsAtScoreLevel,
theirAtLevelDimensions,
2022-01-01 00:06:08 +00:00
theirAboveLevelDimensions,
penalty
2020-11-21 20:41:08 +00:00
}
);
2022-01-01 00:06:08 +00:00
return (atLevelScore + aboveLevelScore + penalty);
2020-11-21 20:41:08 +00:00
}
2019-06-29 01:37:41 +00:00
}