Better character matcher

This commit is contained in:
Mr. Stallion 2019-06-29 15:59:29 -05:00
parent 5856057c81
commit 61a9a64ec8
5 changed files with 535 additions and 376 deletions

View File

@ -195,6 +195,9 @@
tray.setToolTip(l('title'));
tray.on('click', (_) => this.trayClicked(tab));
const view = new electron.remote.BrowserView();
// view.webContents.openDevTools();
view.setAutoResize({width: true, height: true});
electron.ipcRenderer.send('tab-added', view.webContents.id);
const tab = {active: false, view, user: undefined, hasNew: false, tray};

View File

@ -4,10 +4,10 @@
<div class="alert alert-info" v-show="loading">Loading character information.</div>
<div class="alert alert-danger" v-show="error">{{error}}</div>
</div>
<div class="col-md-4 col-lg-3 col-xl-2" v-if="!loading && character">
<div class="col-md-4 col-lg-3 col-xl-2" v-if="!loading && character && character.character && characterMatch && selfCharacter">
<sidebar :character="character" :characterMatch="characterMatch" @memo="memo" @bookmarked="bookmarked" :oldApi="oldApi"></sidebar>
</div>
<div class="col-md-8 col-lg-9 col-xl-10 profile-body" v-if="!loading && character">
<div class="col-md-8 col-lg-9 col-xl-10 profile-body" v-if="!loading && character && character.character && characterMatch && selfCharacter">
<div id="characterView">
<div>
<div v-if="character.ban_reason" id="headerBanReason" class="alert alert-warning">
@ -36,6 +36,7 @@
<div class="card-body">
<div class="tab-content">
<div role="tabpanel" class="tab-pane" :class="{active: tab === '0'}" id="overview">
<match-report :characterMatch="characterMatch"></match-report>
<div v-bbcode="character.character.description" style="margin-bottom: 10px"></div>
<character-kinks :character="character" :oldApi="oldApi" ref="tab0"></character-kinks>
</div>
@ -84,6 +85,7 @@
import Sidebar from './sidebar.vue';
import core from '../../chat/core';
import { Matcher, MatchReport } from './matcher';
import MatchReportView from './match-report.vue';
interface ShowableVueTab extends Vue {
@ -99,7 +101,8 @@
'character-groups': GroupsView,
'character-infotags': InfotagsView,
'character-images': ImagesView,
'character-kinks': CharacterKinksView
'character-kinks': CharacterKinksView,
'match-report': MatchReportView
}
})
export default class CharacterPage extends Vue {
@ -147,9 +150,9 @@
return this.load();
}
async load(mustLoad = true) {
this.loading = true;
this.error = '';
try {
const due: Promise<any>[] = [];
@ -173,7 +176,6 @@
this.loading = false;
}
memo(memo: {id: number, memo: string}): void {
Vue.set(this.character!, 'memo', memo);
}
@ -182,7 +184,6 @@
Vue.set(this.character!, 'bookmarked', state);
}
protected async loadSelfCharacter(): Promise<Character> {
// console.log('SELF');
@ -197,10 +198,9 @@
return this.selfCharacter;
}
private async _getCharacter(): Promise<void> {
this.error = '';
this.character = undefined;
if(this.name === undefined || this.name.length === 0)
return;
@ -218,8 +218,9 @@
return;
}
const matcher = new Matcher(this.selfCharacter.character, this.character.character);
this.characterMatch = matcher.match();
this.characterMatch = Matcher.generateReport(this.selfCharacter.character, this.character.character);
console.log('Match', this.selfCharacter.character.name, this.character.character.name, this.characterMatch);
}
}
</script>
@ -445,23 +446,123 @@
.infotag {
&.match-score {
padding-top: 2px;
padding-left: 4px;
padding-right: 4px;
margin-left: -4px;
margin-right: -4px;
border-radius: 2px;
padding-bottom: 2px;
margin-bottom: 1rem;
.infotag-value {
margin-bottom: 0;
}
}
&.match {
background-color: green;
background-color: rgba(0, 255, 0, 0.5);
border: solid 1px rgba(0, 255, 0, 0.15);
}
&.mismatch {
background-color: red;
background-color: rgba(255, 0, 0, 0.6);
border: 1px solid rgba(255, 0, 0, 0.3);
}
&.weakMatch {
background-color: rgba(0, 162, 0, 0.6);
&.weak-match {
background-color: rgba(0, 162, 0, 0.35);
border: 1px solid rgba(0, 162, 0, 0.15);
}
&.weakMismatch {
background-color: rgba(255, 96, 0, 0.6);
&.weak-mismatch {
background-color: rgba(255, 225, 0, 0.6);
border: 1px solid rgba(255, 225, 0, 0.3);
}
}
.match-report {
display: flex;
flex-direction: row;
background-color: rgba(0,0,0,0.2);
/* width: 100%; */
margin-top: -1.2rem;
margin-left: -1.2rem;
margin-right: -1.2rem;
padding: 1rem;
margin-bottom: 1rem;
padding-bottom: 0;
padding-top: 0.5rem;
max-width: 25rem;
h3 {
font-size: 1.25rem;
}
.scores {
float: left;
flex: 1;
margin-right: 1rem;
max-width: 25rem;
ul {
padding: 0;
padding-left: 0.5rem;
list-style: none;
}
.match-score {
font-size: 0.85rem;
border-radius: 2px;
margin-bottom: 4px;
padding: 2px;
padding-left: 4px;
padding-right: 4px;
span {
color: white;
font-weight: bold;
}
&.match {
background-color: rgba(0, 255, 0, 0.5);
border: solid 1px rgba(0, 255, 0, 0.15);
}
&.mismatch {
background-color: rgba(255, 0, 0, 0.6);
border: 1px solid rgba(255, 0, 0, 0.3);
}
&.weak-match {
background-color: rgba(0, 162, 0, 0.35);
border: 1px solid rgba(0, 162, 0, 0.15);
}
&.weak-mismatch {
background-color: rgba(255, 225, 0, 0.6);
border: 1px solid rgba(255, 225, 0, 0.3);
}
}
}
.vs {
margin-left: 1rem;
margin-right: 1rem;
text-align: center;
font-size: 5rem;
line-height: 0;
color: rgba(255, 255, 255, 0.3);
margin-top: auto;
margin-bottom: auto;
font-style: italic;
font-family: 'Times New Roman', Georgia, serif;
}
}
</style>

View File

@ -13,7 +13,7 @@
import { DisplayInfotag } from './interfaces';
// import { Character as CharacterInfo } from '../../interfaces';
import {Store} from './data_store';
import { MatchReport, TagId } from './matcher';
import { MatchReport, Score, TagId } from './matcher';
@Component
@ -31,29 +31,36 @@
infotag: true,
};
console.log(`Infotag ${this.infotag.id}: ${this.label}`);
// console.log(`Infotag ${this.infotag.id}: ${this.label}`);
const id = this.infotag.id;
if ((this.characterMatch) && (this.infotag.id in this.characterMatch)) {
const n = this.characterMatch[this.infotag.id];
if (this.characterMatch) {
const scores = this.theirInterestIsRelevant(id)
? this.characterMatch.them.scores
: (this.yourInterestIsRelevant(id) ? this.characterMatch.you.scores : null);
console.log(`Found match [${this.infotag.id} === ${TagId[this.infotag.id]}]: ${n}`);
if (scores) {
const score = scores[id] as Score;
if (n >= 1)
styles.match = true;
else if(n >= 0.5)
styles.weakMatch = true;
else if(n === 0)
styles.neutral = true;
else if(n <= -1)
styles.mismatch = true;
else if(n <= -0.5)
styles.weakMismatch = true;
styles[score.getRecommendedClass()] = true;
styles['match-score'] = true;
}
}
return styles;
}
theirInterestIsRelevant(id: number): boolean {
return ((id === TagId.FurryPreference) || (id === TagId.Orientation));
}
yourInterestIsRelevant(id: number): boolean {
return ((id === TagId.Gender) || (id === TagId.Age) || (id === TagId.Species))
}
get label(): string {
const infotag = Store.kinks.infotags[this.infotag.id];
if(typeof infotag === 'undefined')

View File

@ -0,0 +1,73 @@
<template>
<div id="match-report" :class="{'match-report': true, minimized: minimized}" v-if="(haveScores(characterMatch.you) || haveScores(characterMatch.them))">
<a @click="toggleMinimize()"><i :class="{fa: true, 'fa-plus': minimized, 'fa-minus': !minimized}"></i></a>
<div v-if="haveScores(characterMatch.you)" class="scores you">
<h3>{{characterMatch.you.you.name}}</h3>
<ul>
<li v-for="score in getScores(characterMatch.you)" v-if="shouldShowScore(score)" :class="getScoreClass(score)" v-html="score.description"></li>
</ul>
</div>
<div class="vs">
vs.
</div>
<div v-if="haveScores(characterMatch.them)" class="scores them">
<h3>{{characterMatch.them.you.name}}</h3>
<ul>
<li v-for="score in getScores(characterMatch.them)" v-if="shouldShowScore(score)" :class="getScoreClass(score)" v-html="score.description"></li>
</ul>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop } from '@f-list/vue-ts';
import Vue from 'vue';
import { MatchReport, MatchResult, Score, Scoring } from './matcher';
import * as _ from 'lodash';
@Component
export default class MatchReportView extends Vue {
@Prop({required: true})
readonly characterMatch!: MatchReport;
minimized = false;
getScoreClass(score: Score) {
const classes: any = {};
classes[score.getRecommendedClass()] = true;
classes['match-score'] = true;
return classes;
}
haveScores(result: MatchResult): boolean {
return !_.every(
result.scores,
(s: Score) => (s.score === Scoring.NEUTRAL)
);
}
shouldShowScore(score: Score) {
return (score.score !== Scoring.NEUTRAL);
}
getScores(result: MatchResult): Score[] {
return _.map(result.scores, (s: Score) => (s));
}
toggleMinimize() {
this.minimized = !this.minimized;
}
}
</script>

View File

@ -1,5 +1,7 @@
import * as _ from 'lodash';
import { Character } from '../../interfaces';
import { Character, CharacterInfotag } from '../../interfaces';
/* eslint-disable no-null-keyword */
export enum TagId {
Age = 1,
@ -39,7 +41,6 @@ export enum Orientation {
BiCurious = 128
}
export enum BodyType {
Anthro = 122,
Feral = 121,
@ -54,28 +55,10 @@ export enum BodyType {
export enum KinkPreference {
Favorite = 1,
Yes = 0.5,
Maybe = 0,
Maybe = -0.5,
No = -1
}
type ScoringCallback = (you: Character, them: Character) => number;
interface CompatibilityCollection {
[key: number]: ScoringCallback;
}
const orientationCompatibility: CompatibilityCollection = {
[Orientation.Straight]: (you: Character, them: Character) => Matcher.isCis(you, them) ? (Matcher.isSameSexCis(you, them) ? -1 : 1) : 0,
[Orientation.Gay]: (you: Character, them: Character) => Matcher.isCis(you, them) ? (Matcher.isSameSexCis(you, them) ? 1 : -1) : 0,
[Orientation.Bisexual]: (you: Character) => Matcher.isGenderedCis(you) ? 1 : 0,
[Orientation.Asexual]: () => 0,
[Orientation.Unsure]: () => 0,
[Orientation.BiMalePreference]: (you: Character, them: Character) => Matcher.isCis(you, them) ? (Matcher.isMaleCis(you) ? 1 : 0.5) : 0,
[Orientation.BiFemalePreference]: (you: Character, them: Character) => Matcher.isCis(you, them) ? (Matcher.isFemaleCis(you) ? 1 : 0.5) : 0,
[Orientation.Pansexual]: () => 1,
[Orientation.BiCurious]: (you: Character, them: Character) => Matcher.isCis(you, them) ? (Matcher.isSameSexCis(you, them) ? 0.5 : Matcher.isGenderedCis(you) ? 1 : 0) : 0
};
enum Kink {
Females = 554,
MaleHerms = 552,
@ -91,9 +74,10 @@ enum Kink {
UnderageCharacters = 207,
AnthroCharacters = 587,
Humans = 609
}
Humans = 609,
Mammals = 224
}
enum FurryPreference {
FurriesOnly = 39,
@ -118,11 +102,10 @@ const genderKinkMapping: GenderKinkIdMap = {
[Gender.Transgender]: Kink.Transgenders
};
// if no species and 'no furry chareacters', === human
// if no species and dislike 'antho characters' === human
enum Species {
enum Species {
Human = 609,
Equine = 236,
Feline = 212,
@ -138,24 +121,34 @@ const genderKinkMapping: GenderKinkIdMap = {
Procyon = 325,
Rodent = 283,
Ursine = 326,
MarineMammal,
MarineMammal = 309,
Primate = 613,
Elf = 611,
Orc = 615,
Fish = 608,
Reptile = 225,
Anthro = 587,
Minotaur = 121212
Minotaur = 12121212
}
const nonAnthroSpecies = [Species.Human, Species.Elf, Species.Orc];
// const mammalSpecies = [Species.Human, Species.Equine, Species.Feline, Species.Canine, Species.Vulpine, Species.Cervine, Species.Lapine, Species.Musteline, Species.Rodent, Species.Ursine, Species.MarineMammal, Species.Primate, Species.Elf, Species.Orc, Species.Anthro, Species.Minotaur];
const mammalSpecies = [Species.Equine, Species.Feline, Species.Canine, Species.Vulpine, Species.Cervine, Species.Lapine, Species.Musteline, Species.Rodent, Species.Ursine, Species.MarineMammal, Species.Primate, Species.Elf, Species.Orc, Species.Anthro, Species.Minotaur];
interface SpeciesMap {
[key: number]: string[]
}
interface SpeciesStrMap {
[key: number]: string;
}
const speciesNames: SpeciesStrMap = {
[Species.MarineMammal]: 'marine mammals',
[Species.Elf]: 'elves',
[Species.Fish]: 'fishes'
};
const speciesMapping: SpeciesMap = {
[Species.Human]: ['human', 'humanoid', 'angel', 'android'],
[Species.Equine]: ['horse', 'stallion', 'mare', 'filly', 'equine', 'shire', 'donkey', 'mule', 'zebra', 'centaur', 'pony' ],
@ -180,7 +173,7 @@ const speciesMapping: SpeciesMap = {
[Species.Reptile]: ['chameleon', 'anole', 'alligator', 'snake', 'crocodile', 'lizard'],
[Species.Anthro]: ['anthro', 'anthropomorphic'],
[Species.Minotaur]: ['minotaur']
}
};
interface KinkPreferenceMap {
[key: string]: KinkPreference;
@ -194,9 +187,76 @@ const kinkMapping: KinkPreferenceMap = {
};
export interface MatchReport {
[key: number]: number;
you: MatchResult;
them: MatchResult;
}
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;
}
export interface MatchResult {
you: Character,
them: Character,
scores: MatchResultScores;
info: MatchResultCharacterInfo;
}
export enum Scoring {
MATCH = 1,
WEAK_MATCH = 0.5,
NEUTRAL = 0,
WEAK_MISMATCH = 0.5,
MISMATCH = -1
}
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'
};
export class Score {
readonly score: Scoring;
readonly description: string;
constructor(score: Scoring, description: string = '') {
if ((score !== Scoring.NEUTRAL) && (description === ''))
throw new Error('Description must be provided if score is not neutral');
this.score = score;
this.description = description;
}
getRecommendedClass(): string {
return scoreClasses[this.score];
}
}
/**
* 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 {
you: Character;
them: Character;
@ -206,350 +266,300 @@ export class Matcher {
this.them = them;
}
match(): MatchReport {
static generateReport(you: Character, them: Character): MatchReport {
const youThem = new Matcher(you, them);
const themYou = new Matcher(them, you);
return {
[TagId.Orientation]: this.resolveScore(TagId.Orientation, orientationCompatibility),
[TagId.Gender]: this.resolveGenderScore(),
[TagId.Age]: this.resolveAgeScore(),
[TagId.FurryPreference]: this.resolveFurryScore(),
[TagId.Species]: this.resolveSpeciesScore()
you: youThem.match(),
them: themYou.match()
};
}
match(): MatchResult {
return {
you: this.you,
them: this.them,
scores: {
[TagId.Orientation]: this.resolveOrientationScore(),
[TagId.Gender]: this.resolveGenderScore(),
[TagId.Age]: this.resolveAgeScore(),
[TagId.FurryPreference]: this.resolveFurryPairingsScore(),
[TagId.Species]: this.resolveSpeciesScore()
},
info: {
species: Matcher.species(this.you),
gender: Matcher.getTagValueList(TagId.Gender, this.you),
orientation: Matcher.getTagValueList(TagId.Orientation, this.you),
}
};
}
private resolveScore(tagId: number, compatibilityMap: any, you: Character = this.you, them: Character = this.them): number {
const v = Matcher.getTagValueList(tagId, this.them);
if ((!v) || (!(v in compatibilityMap)))
return 0;
return compatibilityMap[v](you, them);
}
private resolveSpeciesScore() {
private resolveOrientationScore(): Score {
const you = this.you;
const them = this.them;
const yourSpecies = Matcher.species(you);
const yourGender = Matcher.getTagValueList(TagId.Gender, you);
const theirGender = Matcher.getTagValueList(TagId.Gender, them);
const yourOrientation = Matcher.getTagValueList(TagId.Orientation, you);
if ((yourGender === null) || (theirGender === null) || (yourOrientation === null))
return new Score(Scoring.NEUTRAL);
// Question: If someone identifies themselves as 'straight cuntboy', how should they be matched? like a straight female?
// CIS
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>');
}
}
// Can't do anything with Gender.None
return new Score(Scoring.NEUTRAL);
}
private 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, `Undecided on <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 resolveSpeciesScore(): Score {
const you = this.you;
const them = this.them;
const theirSpecies = Matcher.species(them);
if (
((yourSpecies !== null) && (Matcher.hatesSpecies(them, yourSpecies))) ||
((theirSpecies !== null) && (Matcher.hatesSpecies(you, theirSpecies)))
) {
return -1;
if (theirSpecies === null)
return new Score(Scoring.NEUTRAL);
const speciesScore = Matcher.getKinkSpeciesPreference(you, theirSpecies);
if (speciesScore !== null) {
const speciesName = speciesNames[theirSpecies] || `${Species[theirSpecies].toLowerCase()}s`;
return this.formatKinkScore(speciesScore, speciesName);
}
if (
((yourSpecies !== null) && (Matcher.maybeSpecies(them, yourSpecies))) ||
((theirSpecies !== null) && (Matcher.maybeSpecies(you, theirSpecies)))
) {
return -0.5;
if (Matcher.isAnthro(them)) {
const anthroScore = Matcher.getKinkPreference(them, Kink.AnthroCharacters);
if (anthroScore !== null)
return this.formatKinkScore(anthroScore, 'anthros');
}
if (
((yourSpecies !== null) && (Matcher.likesSpecies(them, yourSpecies))) ||
((theirSpecies !== null) && (Matcher.likesSpecies(you, theirSpecies)))
) {
return 1;
if (Matcher.isMammal(them)) {
const mammalScore = Matcher.getKinkPreference(them, Kink.Mammals);
if (mammalScore !== null)
return this.formatKinkScore(mammalScore, 'mammals');
}
return 0;
return new Score(Scoring.NEUTRAL);
}
formatScoring(score: Scoring, description: string): Score {
let type = '';
private resolveFurryScore() {
switch (score) {
case Scoring.MISMATCH:
type = 'No';
break;
case Scoring.WEAK_MISMATCH:
type = 'Undecided on';
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 them = this.them;
const youAreAnthro = Matcher.isAnthro(you);
const theyAreAnthro = Matcher.isAnthro(them);
const youAreHuman = Matcher.isHuman(you);
const theyAreHuman = Matcher.isHuman(them);
const yourScore = theyAreAnthro ? Matcher.furryLikeabilityScore(you) : theyAreHuman ? Matcher.humanLikeabilityScore(you) : 0;
const theirScore = youAreAnthro ? Matcher.furryLikeabilityScore(them) : youAreHuman ? Matcher.humanLikeabilityScore(them) : 0;
const score = theyAreAnthro
? Matcher.furryLikeabilityScore(you)
: (theyAreHuman ? Matcher.humanLikeabilityScore(you) : Scoring.NEUTRAL);
return Math.min(yourScore || 0, theirScore || 0);
return this.formatScoring(score, theyAreAnthro ? 'furry pairings' : theyAreHuman ? 'human pairings' : '');
}
static furryLikeabilityScore(c: Character): number | null {
const anthroKink = Matcher.getKinkPreference(c, Kink.AnthroCharacters);
if ((anthroKink === KinkPreference.Yes) || (anthroKink === KinkPreference.Favorite)) {
return 1;
}
if (anthroKink === KinkPreference.Maybe) {
return -0.5;
}
if (anthroKink === KinkPreference.No) {
return -1;
}
static furryLikeabilityScore(c: Character): Scoring {
const furryPreference = Matcher.getTagValueList(TagId.FurryPreference, c);
if (
(furryPreference === FurryPreference.FursAndHumans) ||
(furryPreference === FurryPreference.FurriesPreferredHumansOk) ||
(furryPreference === FurryPreference.FurriesOnly)
) {
return 1;
}
)
return Scoring.MATCH;
if (furryPreference === FurryPreference.HumansPreferredFurriesOk) {
return 0.5;
}
if (furryPreference === FurryPreference.HumansPreferredFurriesOk)
return Scoring.WEAK_MATCH;
if (furryPreference === FurryPreference.HumansOnly) {
return -1;
}
if (furryPreference === FurryPreference.HumansOnly)
return Scoring.MISMATCH;
return 0;
return Scoring.NEUTRAL;
}
static humanLikeabilityScore(c: Character): number | null {
const humanKink = Matcher.getKinkPreference(c, Kink.Humans);
if ((humanKink === KinkPreference.Yes) || (humanKink === KinkPreference.Favorite)) {
return 1;
}
if (humanKink === KinkPreference.Maybe) {
return -0.5;
}
if (humanKink === KinkPreference.No) {
return -1;
}
static humanLikeabilityScore(c: Character): Scoring {
const humanPreference = Matcher.getTagValueList(TagId.FurryPreference, c);
if (
(humanPreference === FurryPreference.FursAndHumans) ||
(humanPreference === FurryPreference.HumansPreferredFurriesOk) ||
(humanPreference === FurryPreference.HumansOnly)
) {
return 1;
}
(humanPreference === FurryPreference.FursAndHumans)
|| (humanPreference === FurryPreference.HumansPreferredFurriesOk)
|| (humanPreference === FurryPreference.HumansOnly)
)
return Scoring.MATCH;
if (humanPreference === FurryPreference.FurriesPreferredHumansOk) {
return 0.5;
}
if (humanPreference === FurryPreference.FurriesPreferredHumansOk)
return Scoring.WEAK_MATCH;
if (humanPreference === FurryPreference.FurriesOnly) {
return -1;
}
if (humanPreference === FurryPreference.FurriesOnly)
return Scoring.MISMATCH;
return 0;
return Scoring.NEUTRAL;
}
static likesFurs(c: Character) {
const score = this.furryLikeabilityScore(c);
return (score !== null) ? (score > 0) : false;
}
static hatesFurs(c: Character) {
const score = this.furryLikeabilityScore(c);
return (score !== null) ? (score < 0) : false;
}
static likesHumans(c: Character) {
const score = this.humanLikeabilityScore(c);
return (score !== null) ? (score > 0) : false;
}
static hatesHumans(c: Character) {
const score = this.humanLikeabilityScore(c);
return (score !== null) ? (score < 0) : false;
}
private resolveAgeScore(): number {
private resolveAgeScore(): Score {
const you = this.you;
const them = this.them;
const yourAgeTag = Matcher.getTagValue(TagId.Age, you);
const theirAgeTag = Matcher.getTagValue(TagId.Age, them);
if ((!yourAgeTag) || (!theirAgeTag)) {
return 0;
}
if (!theirAgeTag)
return new Score(Scoring.NEUTRAL);
if ((!yourAgeTag.string) || (!theirAgeTag.string)) {
return 0;
}
if (!theirAgeTag.string)
return new Score(Scoring.NEUTRAL);
const yourAge = parseInt(yourAgeTag.string, 10);
const theirAge = parseInt(theirAgeTag.string, 10);
if (
((theirAge < 16) && (Matcher.hates(you, Kink.Ageplay))) ||
((yourAge < 16) && (Matcher.hates(them, Kink.Ageplay))) ||
((theirAge < 16) && (Matcher.has(you, Kink.Ageplay) === false)) ||
((yourAge < 16) && (Matcher.has(them, Kink.Ageplay) === false)) ||
((yourAge < theirAge) && (Matcher.hates(you, Kink.OlderCharacters))) ||
((yourAge > theirAge) && (Matcher.hates(them, Kink.OlderCharacters))) ||
((yourAge > theirAge) && (Matcher.hates(you, Kink.YoungerCharacters))) ||
((yourAge < theirAge) && (Matcher.hates(them, Kink.YoungerCharacters))) ||
((theirAge < 18) && (Matcher.hates(you, Kink.UnderageCharacters))) ||
((yourAge < 18) && (Matcher.hates(them, Kink.UnderageCharacters)))
)
return -1;
const ageplayScore = Matcher.getKinkPreference(you, Kink.Ageplay);
const underageScore = Matcher.getKinkPreference(you, Kink.UnderageCharacters);
if (
((theirAge < 18) && (Matcher.likes(you, Kink.UnderageCharacters))) ||
((yourAge < 18) && (Matcher.likes(them, Kink.UnderageCharacters))) ||
((yourAge > theirAge) && (Matcher.likes(you, Kink.YoungerCharacters))) ||
((yourAge < theirAge) && (Matcher.likes(them, Kink.YoungerCharacters))) ||
((yourAge < theirAge) && (Matcher.likes(you, Kink.OlderCharacters))) ||
((yourAge > theirAge) && (Matcher.likes(them, Kink.OlderCharacters))) ||
((theirAge < 16) && (Matcher.likes(you, Kink.Ageplay))) ||
((yourAge < 16) && (Matcher.likes(them, Kink.Ageplay)))
)
return 1;
if ((theirAge < 16) && (ageplayScore !== null))
return this.formatKinkScore(ageplayScore, `ages of ${theirAge}`);
return 0;
if ((theirAge < 16) && (ageplayScore === null))
return this.formatKinkScore(KinkPreference.No, `ages of ${theirAge}`);
if ((theirAge < 18) && (underageScore !== null))
return this.formatKinkScore(underageScore, `ages of ${theirAge}`);
if ((yourAgeTag) && (yourAgeTag.string)) {
const olderCharactersScore = Matcher.getKinkPreference(you, Kink.OlderCharacters);
const youngerCharactersScore = Matcher.getKinkPreference(you, Kink.YoungerCharacters);
const yourAge = parseInt(yourAgeTag.string, 10);
if ((yourAge < theirAge) && (olderCharactersScore !== null))
return this.formatKinkScore(olderCharactersScore, 'older characters');
if ((yourAge > theirAge) && (youngerCharactersScore !== null))
return this.formatKinkScore(youngerCharactersScore, 'younger characters');
}
return new Score(Scoring.NEUTRAL);
}
private resolveGenderScore() {
private resolveGenderScore(): Score {
const you = this.you;
const them = this.them;
const yourGender = Matcher.getTagValueList(TagId.Gender, you);
const theirGender = Matcher.getTagValueList(TagId.Gender, them);
const yourGenderScore = Matcher.genderLikeabilityScore(them, yourGender);
const theirGenderScore = Matcher.genderLikeabilityScore(you, theirGender);
if (theirGender === null)
return new Score(Scoring.NEUTRAL);
const yourFinalScore = (yourGenderScore !== null) ? yourGenderScore : this.resolveScore(TagId.Orientation, orientationCompatibility, you, them);
const theirFinalScore = (theirGenderScore !== null) ? theirGenderScore : this.resolveScore(TagId.Orientation, orientationCompatibility, them, you);
const genderName = `${Gender[theirGender].toLowerCase()}s`;
const genderKinkScore = Matcher.getKinkGenderPreference(you, theirGender);
return Math.min(yourFinalScore, theirFinalScore);
if (genderKinkScore !== null)
return this.formatKinkScore(genderKinkScore, genderName);
return new Score(Scoring.NEUTRAL);
}
static getTagValue(tagId: number, c: Character) {
static getTagValue(tagId: number, c: Character): CharacterInfotag | undefined {
return c.infotags[tagId];
}
static getTagValueList(tagId: number, c: Character): number | undefined {
static getTagValueList(tagId: number, c: Character): number | null {
const t = this.getTagValue(tagId, c);
if ((!t) || (!t.list)) {
return;
}
if ((!t) || (!t.list))
return null;
return t.list;
}
// Considers males and females only
static isSameSexCis(a: Character, b: Character): boolean {
const aGender = this.getTagValueList(TagId.Gender, a);
const bGender = this.getTagValueList(TagId.Gender, b);
if ((aGender !== Gender.Male) && (aGender !== Gender.Female)) {
return false;
}
return ((aGender !== undefined) && (aGender === bGender));
}
// Considers
static isGenderedCis(c: Character): boolean {
const gender = this.getTagValueList(TagId.Gender, c);
return ((!!gender) && (gender !== Gender.None));
}
static isMaleCis(c: Character): boolean {
const gender = this.getTagValueList(TagId.Gender, c);
return (gender === Gender.Male);
}
static isFemaleCis(c: Character): boolean {
const gender = this.getTagValueList(TagId.Gender, c);
return (gender === Gender.Female);
}
static isCis(...characters: Character[]): boolean {
return _.every(characters, (c: Character) => ((Matcher.isMaleCis(c)) || (Matcher.isFemaleCis(c))));
}
static genderLikeabilityScore(c: Character, gender?: Gender): number | null {
if (gender === undefined) {
return null;
}
const byKink = Matcher.getKinkGenderPreference(c, gender);
if (byKink !== null) {
if ((byKink === KinkPreference.Yes) || (byKink === KinkPreference.Favorite)) {
return 1;
}
if (byKink === KinkPreference.Maybe) {
return -0.5;
}
if (byKink === KinkPreference.No) {
return -1;
}
}
if (this.isCis(c)) {
if ((gender !== Gender.Female) && (gender !== Gender.Male)) {
return -1;
}
}
return null;
}
static likesGender(c: Character, gender: Gender): boolean | null {
const byKink = Matcher.getKinkGenderPreference(c, gender);
if (byKink !== null)
return ((byKink === KinkPreference.Yes) || (byKink === KinkPreference.Favorite));
if ((Matcher.isCis(c)) && ((gender === Gender.Male) || (gender === Gender.Female)))
return gender !== this.getTagValueList(TagId.Gender, c);
return null;
}
static dislikesGender(c: Character, gender: Gender): boolean | null {
const byKink = Matcher.getKinkGenderPreference(c, gender);
if (byKink !== null)
return (byKink === KinkPreference.No);
if ((Matcher.isCis(c)) && ((gender === Gender.Male) || (gender === Gender.Female)))
return gender === this.getTagValueList(TagId.Gender, c);
return null;
}
static maybeGender(c: Character, gender: Gender): boolean | null {
const byKink = Matcher.getKinkGenderPreference(c, gender);
if (byKink !== null)
return (byKink === KinkPreference.Maybe);
return null;
static isCisGender(...genders: Gender[]): boolean {
return _.every(genders, (g: Gender) => ((g === Gender.Female) || (g === Gender.Male)));
}
static getKinkPreference(c: Character, kinkId: number): KinkPreference | null {
@ -570,74 +580,40 @@ export class Matcher {
return this.getKinkPreference(c, species);
}
static likesSpecies(c: Character, species: Species): boolean | null {
const byKink = Matcher.getKinkSpeciesPreference(c, species);
if (byKink !== null)
return ((byKink === KinkPreference.Yes) || (byKink === KinkPreference.Favorite));
return null;
}
static maybeSpecies(c: Character, species: Species): boolean | null {
const byKink = Matcher.getKinkSpeciesPreference(c, species);
if (byKink !== null)
return (byKink === KinkPreference.Maybe);
return null;
}
static hatesSpecies(c: Character, species: Species): boolean | null {
const byKink = Matcher.getKinkSpeciesPreference(c, species);
if (byKink !== null)
return (byKink === KinkPreference.No);
return null;
}
static likes(c: Character, kinkId: Kink): boolean {
const r = Matcher.getKinkPreference(c, kinkId);
return ((r === KinkPreference.Favorite) || (r === KinkPreference.Yes));
}
static hates(c: Character, kinkId: Kink): boolean {
const r = Matcher.getKinkPreference(c, kinkId);
return (r === KinkPreference.No);
}
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 = this.getTagValueList(TagId.BodyType, c);
if (bodyTypeId === BodyType.Anthro) {
if (bodyTypeId === BodyType.Anthro)
return true;
}
const speciesId = this.species(c);
if (!speciesId)
return null;
return (nonAnthroSpecies.indexOf(parseInt(`${speciesId}`, 10)) < 0);
return (nonAnthroSpecies.indexOf(speciesId) < 0);
}
static isHuman(c: Character): boolean | null {
const bodyTypeId = this.getTagValueList(TagId.BodyType, c);
if (bodyTypeId === BodyType.Human) {
if (bodyTypeId === BodyType.Human)
return true;
}
const speciesId = this.species(c);
@ -650,9 +626,8 @@ export class Matcher {
const mySpecies = this.getTagValue(TagId.Species, c);
if ((!mySpecies) || (!mySpecies.string)) {
if ((!mySpecies) || (!mySpecies.string))
return Species.Human; // best guess
}
const finalSpecies = mySpecies.string.toLowerCase();
@ -671,6 +646,6 @@ export class Matcher {
}
);
return foundSpeciesId;
return (foundSpeciesId === null) ? null : parseInt(foundSpeciesId, 10);
}
}