import * as _ from 'lodash'; import { Character } from '../../interfaces'; export enum TagId { Age = 1, Orientation = 2, Gender = 3, Build = 13, FurryPreference = 29, BdsmRole = 15, Position = 41, BodyType = 51, ApparentAge = 64, RelationshipStatus = 42, Species = 9, LanguagePreference = 49 } export enum Gender { Male = 1, Female = 2, Transgender = 3, Herm = 32, MaleHerm = 51, Cuntboy = 69, None = 105, Shemale = 141 } export enum Orientation { Straight = 4, Gay = 5, Bisexual = 6, Asexual = 7, Unsure = 8, BiMalePreference = 89, BiFemalePreference = 90, Pansexual = 127, BiCurious = 128 } export enum BodyType { Anthro = 122, Feral = 121, Morphable = 123, Varies = 124, Other = 125, Androgynous = 126, Human = 143, Taur = 145 } export enum KinkPreference { Favorite = 1, Yes = 0.5, Maybe = 0, 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, Males = 553, Transgenders = 551, Herms = 132, Shemales = 356, Cuntboys = 231, OlderCharacters = 109, YoungerCharacters = 197, Ageplay = 196, UnderageCharacters = 207, AnthroCharacters = 587, Humans = 609 } enum FurryPreference { FurriesOnly = 39, FursAndHumans = 40, HumansOnly = 41, HumansPreferredFurriesOk = 150, FurriesPreferredHumansOk = 149 } interface GenderKinkIdMap { [key: number]: Kink } const genderKinkMapping: GenderKinkIdMap = { [Gender.Female]: Kink.Females, [Gender.Male]: Kink.Males, [Gender.Cuntboy]: Kink.Cuntboys, [Gender.Herm]: Kink.Herms, [Gender.MaleHerm]: Kink.MaleHerms, [Gender.Shemale]: Kink.Shemales, [Gender.Transgender]: Kink.Transgenders }; // if no species and 'no furry chareacters', === human // if no species and dislike 'antho characters' === human enum Species { Human = 609, Equine = 236, Feline = 212, Canine = 226, Vulpine = 213, Avian = 215, Amphibian = 223, Cervine = 227, Insect = 237, Lapine = 214, Musteline = 328, Dragon = 228, Procyon = 325, Rodent = 283, Ursine = 326, MarineMammal, Primate = 613, Elf = 611, Orc = 615, Fish = 608, Reptile = 225, Anthro = 587, Minotaur = 121212 } 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]; interface SpeciesMap { [key: number]: string[] } const speciesMapping: SpeciesMap = { [Species.Human]: ['human', 'humanoid', 'angel', 'android'], [Species.Equine]: ['horse', 'stallion', 'mare', 'filly', 'equine', 'shire', 'donkey', 'mule', 'zebra', 'centaur', 'pony' ], [Species.Feline]: ['cat', 'kitten', 'catgirl', 'neko', 'tiger', 'puma', 'lion', 'lioness', 'tigress', 'feline', 'jaguar', 'cheetah', 'lynx', 'leopard'], [Species.Canine]: ['dog', 'wolf', 'dingo', 'coyote', 'jackal', 'canine', 'doberman', 'husky'], [Species.Vulpine]: ['fox', 'fennec', 'kitsune', 'vulpine', 'vixen'], [Species.Avian]: ['bird', 'gryphon', 'phoenix', 'roc', 'chimera', 'avian'], [Species.Amphibian]: ['salamander', 'frog', 'toad', 'newt'], [Species.Cervine]: ['deer', 'elk', 'moose'], [Species.Insect]: ['bee', 'wasp', 'spider', 'scorpion', 'ant', 'insect'], [Species.Lapine]: ['bunny', 'rabbit', 'hare', 'lapine'], [Species.Dragon]: ['dragon', 'drake', 'wyvern'], [Species.Musteline]: ['mink', 'ferret', 'weasel', 'stoat', 'otter', 'wolverine', 'marten'], [Species.Procyon]: ['raccoon', 'coatimund', 'longtail'], [Species.Rodent]: ['rat', 'mouse', 'chipmunk', 'squirrel', 'rodent'], [Species.Ursine]: ['bear', 'panda', 'black bear', 'brown bear', 'polar bear'], [Species.MarineMammal]: ['whale', 'killer whale', 'dolphin'], [Species.Primate]: ['monkey', 'ape', 'chimp', 'chimpanzee', 'gorilla'], [Species.Elf]: ['elf'], [Species.Fish]: ['fish', 'shark', 'great white'], [Species.Orc]: ['orc'], [Species.Reptile]: ['chameleon', 'anole', 'alligator', 'snake', 'crocodile', 'lizard'], [Species.Anthro]: ['anthro', 'anthropomorphic'], [Species.Minotaur]: ['minotaur'] } interface KinkPreferenceMap { [key: string]: KinkPreference; } const kinkMapping: KinkPreferenceMap = { favorite: KinkPreference.Favorite, yes: KinkPreference.Yes, maybe: KinkPreference.Maybe, no: KinkPreference.No }; export interface MatchReport { [key: number]: number; } export class Matcher { you: Character; them: Character; constructor(you: Character, them: Character) { this.you = you; this.them = them; } match(): MatchReport { return { [TagId.Orientation]: this.resolveScore(TagId.Orientation, orientationCompatibility), [TagId.Gender]: this.resolveGenderScore(), [TagId.Age]: this.resolveAgeScore(), [TagId.FurryPreference]: this.resolveFurryScore(), [TagId.Species]: this.resolveSpeciesScore() }; } 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() { const you = this.you; const them = this.them; const yourSpecies = Matcher.species(you); const theirSpecies = Matcher.species(them); if ( ((yourSpecies !== null) && (Matcher.hatesSpecies(them, yourSpecies))) || ((theirSpecies !== null) && (Matcher.hatesSpecies(you, theirSpecies))) ) { return -1; } if ( ((yourSpecies !== null) && (Matcher.maybeSpecies(them, yourSpecies))) || ((theirSpecies !== null) && (Matcher.maybeSpecies(you, theirSpecies))) ) { return -0.5; } if ( ((yourSpecies !== null) && (Matcher.likesSpecies(them, yourSpecies))) || ((theirSpecies !== null) && (Matcher.likesSpecies(you, theirSpecies))) ) { return 1; } return 0; } private resolveFurryScore() { 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; return Math.min(yourScore || 0, theirScore || 0); } 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; } const furryPreference = Matcher.getTagValueList(TagId.FurryPreference, c); if ( (furryPreference === FurryPreference.FursAndHumans) || (furryPreference === FurryPreference.FurriesPreferredHumansOk) || (furryPreference === FurryPreference.FurriesOnly) ) { return 1; } if (furryPreference === FurryPreference.HumansPreferredFurriesOk) { return 0.5; } if (furryPreference === FurryPreference.HumansOnly) { return -1; } return 0; } 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; } const humanPreference = Matcher.getTagValueList(TagId.FurryPreference, c); if ( (humanPreference === FurryPreference.FursAndHumans) || (humanPreference === FurryPreference.HumansPreferredFurriesOk) || (humanPreference === FurryPreference.HumansOnly) ) { return 1; } if (humanPreference === FurryPreference.FurriesPreferredHumansOk) { return 0.5; } if (humanPreference === FurryPreference.FurriesOnly) { return -1; } return 0; } 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 { 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 ((!yourAgeTag.string) || (!theirAgeTag.string)) { return 0; } 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; 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; return 0; } private resolveGenderScore() { 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); const yourFinalScore = (yourGenderScore !== null) ? yourGenderScore : this.resolveScore(TagId.Orientation, orientationCompatibility, you, them); const theirFinalScore = (theirGenderScore !== null) ? theirGenderScore : this.resolveScore(TagId.Orientation, orientationCompatibility, them, you); return Math.min(yourFinalScore, theirFinalScore); } static getTagValue(tagId: number, c: Character) { return c.infotags[tagId]; } static getTagValueList(tagId: number, c: Character): number | undefined { const t = this.getTagValue(tagId, c); if ((!t) || (!t.list)) { return; } 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 getKinkPreference(c: Character, kinkId: number): KinkPreference | null { if (!(kinkId in c.kinks)) return null; return kinkMapping[c.kinks[kinkId] as string]; } static getKinkGenderPreference(c: Character, gender: Gender): KinkPreference | null { if (!(gender in genderKinkMapping)) return null; return this.getKinkPreference(c, genderKinkMapping[gender]); } static getKinkSpeciesPreference(c: Character, species: Species): KinkPreference | null { 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 isAnthro(c: Character): boolean | null { const bodyTypeId = this.getTagValueList(TagId.BodyType, c); if (bodyTypeId === BodyType.Anthro) { return true; } const speciesId = this.species(c); if (!speciesId) return null; return (nonAnthroSpecies.indexOf(parseInt(`${speciesId}`, 10)) < 0); } static isHuman(c: Character): boolean | null { const bodyTypeId = this.getTagValueList(TagId.BodyType, c); if (bodyTypeId === BodyType.Human) { return true; } const speciesId = this.species(c); return (speciesId === Species.Human); } static species(c: Character): Species | null { let foundSpeciesId: Species | null = null; let match = ''; const mySpecies = this.getTagValue(TagId.Species, c); if ((!mySpecies) || (!mySpecies.string)) { return Species.Human; // best guess } const finalSpecies = mySpecies.string.toLowerCase(); _.each( speciesMapping as any, (keywords: string[], speciesId: Species) => { _.each( keywords, (k: string) => { if ((k.length > match.length) && (finalSpecies.indexOf(k) >= 0)) { match = k; foundSpeciesId = speciesId; } } ); } ); return foundSpeciesId; } }