308 lines
8.4 KiB
TypeScript
308 lines
8.4 KiB
TypeScript
import * as _ from 'lodash';
|
|
import { Matcher } from '../matcher';
|
|
import { BodyType, Build, Gender, Kink, Species, TagId } from '../matcher-types';
|
|
import { SmartFilterSelection, SmartFilterSettings } from './types';
|
|
import { Character } from '../../interfaces';
|
|
import log from 'electron-log';
|
|
import core from '../../chat/core';
|
|
|
|
export interface SmartFilterOpts {
|
|
name: string;
|
|
kinks?: Kink[];
|
|
bodyTypes?: BodyType[];
|
|
builds?: Build[];
|
|
species?: Species[];
|
|
genders?: Gender[];
|
|
isAnthro?: boolean;
|
|
isHuman?: boolean;
|
|
}
|
|
|
|
export interface SmartFilterTestResult {
|
|
isFiltered: boolean;
|
|
builds: boolean;
|
|
bodyTypes: boolean;
|
|
species: boolean;
|
|
genders: boolean;
|
|
isAnthro: boolean;
|
|
isHuman: boolean;
|
|
kinks: boolean;
|
|
}
|
|
|
|
function getBaseLog(base: number, x: number): number {
|
|
return Math.log(x) / Math.log(base);
|
|
}
|
|
|
|
export class SmartFilter {
|
|
constructor(private opts: SmartFilterOpts) {}
|
|
|
|
test(c: Character): SmartFilterTestResult {
|
|
const builds = this.testBuilds(c);
|
|
const bodyTypes = this.testBodyTypes(c);
|
|
const species = this.testSpecies(c);
|
|
const isAnthro = this.testIsAnthro(c);
|
|
const isHuman = this.testIsHuman(c);
|
|
const kinks = this.testKinks(c);
|
|
const genders = this.testGenders(c);
|
|
|
|
const isFiltered = builds || bodyTypes || species || isAnthro || isHuman || kinks || genders;
|
|
const result = { isFiltered, builds, bodyTypes, species, isAnthro, isHuman, kinks, genders };
|
|
|
|
log.silly('smart-filter.test', { name: c.name, filterName: this.opts.name, result });
|
|
|
|
return result;
|
|
}
|
|
|
|
testKinks(c: Character): boolean {
|
|
if (!this.opts.kinks) {
|
|
return false;
|
|
}
|
|
|
|
const score = _.reduce(this.opts.kinks, (curScore, kinkId) => {
|
|
const pref = Matcher.getKinkPreference(c, kinkId);
|
|
|
|
if (pref) {
|
|
curScore.matches += 1;
|
|
curScore.score += pref;
|
|
}
|
|
|
|
return curScore;
|
|
}, { score: 0, matches: 0 });
|
|
|
|
const baseLog = getBaseLog(5, (this.opts.kinks?.length || 0) + 1);
|
|
const threshold = (baseLog * baseLog) + 1;
|
|
|
|
return score.matches >= 1 && score.score >= threshold;
|
|
}
|
|
|
|
testBuilds(c: Character): boolean {
|
|
if (!this.opts.builds) {
|
|
return false;
|
|
}
|
|
|
|
const build = Matcher.getTagValueList(TagId.Build, c);
|
|
|
|
return build !== null && _.findIndex(this.opts.builds || [], (b) => b === build) >= 0;
|
|
}
|
|
|
|
testGenders(c: Character): boolean {
|
|
if (!this.opts.genders) {
|
|
return false;
|
|
}
|
|
|
|
const gender = Matcher.getTagValueList(TagId.Gender, c);
|
|
|
|
return gender !== null && _.findIndex(this.opts.genders || [], (g) => g === gender) >= 0;
|
|
}
|
|
|
|
testBodyTypes(c: Character): boolean {
|
|
if (!this.opts.bodyTypes) {
|
|
return false;
|
|
}
|
|
|
|
const bodyType = Matcher.getTagValueList(TagId.BodyType, c);
|
|
|
|
return bodyType !== null && _.findIndex(this.opts.bodyTypes || [], (b) => b === bodyType) >= 0;
|
|
}
|
|
|
|
testSpecies(c: Character): boolean {
|
|
if (!this.opts.species) {
|
|
return false;
|
|
}
|
|
|
|
const species = Matcher.species(c);
|
|
|
|
return species !== null && _.findIndex(this.opts.species || [], (s) => s === species) >= 0;
|
|
}
|
|
|
|
testIsHuman(c: Character): boolean {
|
|
return !!this.opts.isHuman && (Matcher.isHuman(c) || false);
|
|
}
|
|
|
|
testIsAnthro(c: Character): boolean {
|
|
return !!this.opts.isAnthro && (Matcher.isAnthro(c) || false);
|
|
}
|
|
}
|
|
|
|
export type SmartFilterCollection = {
|
|
[key in keyof SmartFilterSelection]: SmartFilter;
|
|
};
|
|
|
|
|
|
export const smartFilters: SmartFilterCollection = {
|
|
ageplay: new SmartFilter({
|
|
name: 'ageplay',
|
|
kinks: [Kink.Ageplay, Kink.AgeProgression, Kink.AgeRegression, Kink.UnderageCharacters, Kink.Infantilism]
|
|
}),
|
|
|
|
anthro: new SmartFilter({
|
|
name: 'anthro',
|
|
isAnthro: true
|
|
}),
|
|
|
|
female: new SmartFilter({
|
|
name: 'female',
|
|
genders: [Gender.Female]
|
|
}),
|
|
|
|
feral: new SmartFilter({
|
|
name: 'feral',
|
|
bodyTypes: [BodyType.Feral]
|
|
}),
|
|
|
|
gore: new SmartFilter({
|
|
name: 'gore',
|
|
kinks: [
|
|
Kink.Abrasions, Kink.Castration, Kink.Death, Kink.Emasculation, Kink.ExecutionMurder, Kink.Gore, Kink.Impalement, Kink.Mutilation,
|
|
Kink.Necrophilia, Kink.NonsexualPain, Kink.NonsexualTorture, Kink.Nullification, Kink.ToothRemoval, Kink.WoundFucking,
|
|
Kink.Cannibalism, Kink.GenitalTorture
|
|
]
|
|
}),
|
|
|
|
human: new SmartFilter({
|
|
name: 'human',
|
|
isHuman: true
|
|
}),
|
|
|
|
hyper: new SmartFilter({
|
|
name: 'kinks',
|
|
kinks: [Kink.HyperAsses, Kink.HyperBalls, Kink.HyperBreasts, Kink.HyperCocks, Kink.HyperFat, Kink.HyperMuscle, Kink.HyperVaginas,
|
|
Kink.HyperVoluptous, Kink.HyperMuscleGrowth, Kink.MacroAsses, Kink.MacroBalls, Kink.MacroBreasts, Kink.MacroCocks]
|
|
}),
|
|
|
|
incest: new SmartFilter({
|
|
name: 'incest',
|
|
kinks: [Kink.Incest, Kink.IncestParental, Kink.IncestSiblings, Kink.ParentChildPlay, Kink.ForcedIncest]
|
|
}),
|
|
|
|
intersex: new SmartFilter({
|
|
name: 'intersex',
|
|
genders: [Gender.Transgender, Gender.Herm, Gender.MaleHerm, Gender.Cuntboy, Gender.Shemale]
|
|
}),
|
|
|
|
male: new SmartFilter({
|
|
name: 'male',
|
|
genders: [Gender.Male]
|
|
}),
|
|
|
|
microMacro: new SmartFilter({
|
|
name: 'microMacro',
|
|
kinks: [Kink.MacroAsses, Kink.MacroBalls, Kink.MacroBreasts, Kink.MacroCocks, Kink.Macrophilia, Kink.MegaMacro, Kink.Microphilia,
|
|
Kink.GrowthMacro, Kink.ShrinkingMicro, Kink.SizeDifferencesMicroMacro]
|
|
}),
|
|
|
|
obesity: new SmartFilter({
|
|
name: 'obesity',
|
|
builds: [Build.Tubby, Build.Obese, Build.Chubby]
|
|
}),
|
|
|
|
pregnancy: new SmartFilter({
|
|
name: 'pregnancy',
|
|
kinks: [Kink.AlternativePregnancy, Kink.AnalPregnancy, Kink.Birthing, Kink.ExtremePregnancy, Kink.MalePregnancy, Kink.Pregnancy]
|
|
}),
|
|
|
|
pokemon: new SmartFilter({
|
|
name: 'pokemon',
|
|
species: [Species.Pokemon]
|
|
}),
|
|
|
|
rape: new SmartFilter({
|
|
name: 'rape',
|
|
kinks: [Kink.PseudoRape, Kink.DubConsensual, Kink.Nonconsensual]
|
|
}),
|
|
|
|
scat: new SmartFilter({
|
|
name: 'scat',
|
|
kinks: [Kink.HyperScat, Kink.Scat, Kink.ScatTorture, Kink.Soiling, Kink.SwallowingFeces]
|
|
}),
|
|
|
|
std: new SmartFilter({
|
|
name: 'std',
|
|
kinks: [Kink.STDs]
|
|
}),
|
|
|
|
taur: new SmartFilter({
|
|
name: 'taur',
|
|
bodyTypes: [BodyType.Taur]
|
|
}),
|
|
|
|
unclean: new SmartFilter({
|
|
name: 'unclean',
|
|
kinks: [Kink.BelchingBurping, Kink.DirtyFeet, Kink.ExtremeMusk, Kink.Farting, Kink.Filth, Kink.Slob, Kink.Smegma, Kink.SwallowingVomit,
|
|
Kink.UnwashedMusk, Kink.Vomiting]
|
|
}),
|
|
|
|
vore: new SmartFilter({
|
|
name: 'vore',
|
|
kinks: [Kink.Absorption, Kink.AlternativeVore, Kink.AnalVore, Kink.Cannibalism, Kink.CockVore, Kink.CookingVore, Kink.Digestion,
|
|
Kink.Disposal, Kink.HardVore, Kink.RealisticVore, Kink.SoftVore, Kink.Unbirthing, Kink.UnrealisticVore, Kink.VoreBeingPredator,
|
|
Kink.VoreBeingPrey]
|
|
}),
|
|
|
|
watersports: new SmartFilter({
|
|
name: 'watersports',
|
|
kinks: [Kink.HyperWatersports, Kink.PissEnemas, Kink.SwallowingUrine, Kink.Watersports, Kink.Wetting]
|
|
}),
|
|
|
|
zoophilia: new SmartFilter({
|
|
name: 'zoophilia',
|
|
kinks: [Kink.Zoophilia, Kink.AnimalsFerals, Kink.Quadrupeds]
|
|
})
|
|
};
|
|
|
|
export function testSmartFilters(c: Character, opts: SmartFilterSettings): {
|
|
ageCheck: { ageMin: boolean; ageMax: boolean };
|
|
filters: { [key in keyof SmartFilterCollection]: SmartFilterTestResult }
|
|
} | null {
|
|
if (c.name === core.characters.ownCharacter.name) {
|
|
return null;
|
|
}
|
|
|
|
const coreCharacter = core.characters.get(c.name);
|
|
|
|
if (coreCharacter?.isChatOp || coreCharacter?.isBookmarked || coreCharacter?.isFriend) {
|
|
return null;
|
|
}
|
|
|
|
if (opts.exceptionNames.includes(c.name)) {
|
|
log.debug('smart-filter.exception', { name: c.name });
|
|
return null;
|
|
}
|
|
|
|
const ageCheck = { ageMin: false, ageMax: false };
|
|
|
|
if (opts.minAge !== null || opts.maxAge !== null) {
|
|
const age = Matcher.age(c) || Matcher.apparentAge(c)?.min || null;
|
|
|
|
if (age !== null) {
|
|
if (opts.minAge !== null && age < opts.minAge) {
|
|
log.debug('smart-filter.age.min', { name: c.name, age, minAge: opts.minAge });
|
|
ageCheck.ageMin = true;
|
|
}
|
|
|
|
if (opts.maxAge !== null && age > opts.maxAge) {
|
|
log.debug('smart-filter.age.max', { name: c.name, age, maxAge: opts.maxAge });
|
|
ageCheck.ageMax = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
ageCheck,
|
|
filters: _.mapValues(smartFilters, (f, k) => (opts.smartFilters as any)[k] && f.test(c))
|
|
};
|
|
}
|
|
|
|
export function matchesSmartFilters(c: Character, opts: SmartFilterSettings): boolean {
|
|
const match = testSmartFilters(c, opts);
|
|
|
|
if (!match) {
|
|
return false;
|
|
}
|
|
|
|
if (match.ageCheck.ageMax || match.ageCheck.ageMin) {
|
|
return true;
|
|
}
|
|
|
|
return !_.every(match.filters, (filterResult) => !filterResult.isFiltered);
|
|
}
|