code:
App.Art.GenAI.BreastsPromptPart = class BreastsPromptPart extends App.Art.GenAI.PromptPart {
	_breastPrompt() {
		if (this.censored) {
			return null;
		}
		let breastWord = "breasts";

		let breastDescriptors = [];

		let {boobs, boobShape} = this.slave;
		// @ts-ignore
		boobShape = boobShape.toLowerCase();

		if (boobShape && boobShape !== "normal") {
			breastDescriptors.push(boobShape);
		}

		if (boobs < 300) {
			breastDescriptors.push("flat");
			breastWord = this.helper.isPony() ? "chested" : "chest";
		} else if (boobs < 400) {
			breastDescriptors.push("very small");
		} else if (boobs < 500) {
			breastDescriptors.push("small");
		} else if (boobs < 650) {
			breastDescriptors.push("medium");
		} else if (boobs < 800) {
			breastDescriptors.push("large");
		} else if (boobs < 1000) {
			breastDescriptors.push("very large");
		} else if (boobs < 1400) {
			breastDescriptors.push("huge");
		} else if (this.helper.isPony()) {
			if (boobs < 1600) {
				breastDescriptors.push("gigantic");
			} else {
				breastDescriptors.push("hyper");
			}
		} else {
			if (this.helper.hasLora("BEReaction")) {
				return this.helper.lora("BEReaction", 1, ", bereaction, breast expansion, (gigantic breasts:1.2)");
			} else {
				if (boobs < 1600) {
					breastDescriptors.push("very huge");
				} else if (boobs < 1800) {
					breastDescriptors.push("enormous");
				} else if (boobs < 2050) {
					breastDescriptors.push("extremely enormous");
				} else if (boobs < 2600) {
					breastDescriptors.push("massive");
				} else if (boobs < 2900) {
					breastDescriptors.push("extremely massive");
				} else {
					breastDescriptors.push("unbelievably massive");
				}
			}
		}

		breastDescriptors.push(breastWord);

		return breastDescriptors.join(" ");
	}

	_colorMatcher() {
		const {race, skin} = this.slave;

		/*
         * --RACES--
         * white
         * black
         * latina
         * asian
         * middle eastern
         * amerindian
         * southern european
         * semitic
         * malay
         * indo-aryan
         * pacific islander
         * mixed race
         */

		// TODO: Mixed race should maybe consider nationality but that's a PITA

		if ([
			"pure white",
			"ivory",
			"white",
		].includes(skin)) {
			return "pale pink";
		}

		if ([
			"extremely pale",
			"very pale",
			"pale",
		].includes(skin)) {
			switch (race) {
				case "white":
				case "amerindian":
				case "semitic":
				case "latina":
					return "light pink";
				case "asian":
					return "dusty rose";
				case "middle eastern":
				case "malay":
				case "southern european":
				case "indo-aryan":
				case "pacific islander":
				case "mixed race":
					return "light beige";
			}
		}

		if ([
			"extremely fair",
			"very fair",
			"fair",
			"light",
			"light olive",
		].includes(skin)) {
			switch (race) {
				case "white":
				case "amerindian":
				case "semitic":
				case "latina":
					return "pink";
				case "asian":
					return "rose colored";
				case "middle eastern":
				case "malay":
				case "southern european":
				case "indo-aryan":
				case "pacific islander":
				case "mixed race":
					return "light brown";
			}
		}

		if ([
			"sun tanned",
			"spray tanned",
			"tan",
			"olive",
			"bronze",
			"dark olive",
		].includes(skin)) {
			switch (race) {
				case "white":
				case "amerindian":
				case "semitic":
				case "latina":
					return "dark pink";
				case "asian":
					return "light brown";
				case "middle eastern":
				case "malay":
				case "southern european":
				case "indo-aryan":
				case "pacific islander":
				case "mixed race":
					return "brown";
			}
		}

		if ([
			"dark",
			"light beige",
			"beige",
			"dark beige",
			"light brown",
		].includes(skin)) {
			switch (race) {
				case "white":
				case "amerindian":
				case "semitic":
				case "latina":
					return "light brown";
				case "asian":
					return "brown";
				case "middle eastern":
				case "malay":
				case "southern european":
				case "indo-aryan":
				case "pacific islander":
				case "mixed race":
					return "dark brown";
			}
		}

		if ([
			"brown",
			"dark brown",
			"black",
		].includes(skin)) {
			switch (race) {
				case "white":
				case "amerindian":
				case "semitic":
				case "latina":
					return "brown";
				case "asian":
					return "dark brown";
				case "middle eastern":
				case "malay":
				case "southern european":
				case "indo-aryan":
				case "pacific islander":
				case "mixed race":
					return "very dark brown";
			}
		}

		if ([
			"ebony",
			"pure black",
		].includes(skin)) {
			return "black";
		}
	}

	_nippleAreolaPrompt() {
		if (this.censored) {
			return null;
		}

		const exposesNipples = this.helper.exposesNipples(this.slave.clothes);
		if (!exposesNipples) {
			return null;
		}

		let {areolae, areolaeShape, nipples} = this.slave;
		// @ts-ignore
		areolaeShape = areolaeShape.toLowerCase();

		let areolaDescriptors = [];

		if (areolaeShape && areolaeShape !== "circle") {
			areolaDescriptors.push(areolaeShape);
		}

		switch (areolae) {
			case 0: break;
			case 1: areolaDescriptors.push("big"); break;
			case 2: areolaDescriptors.push("large"); break;
			case 3: areolaDescriptors.push("huge"); break;
			case 4: areolaDescriptors.push("massive"); break;
		}

		const nippleAreolaColor = this._colorMatcher();

		areolaDescriptors.push("areola");

		let nippleDescriptors = [nippleAreolaColor, "nipples"];

		if (nipples) {
			nippleDescriptors.unshift(nipples);
		}

		return [
			nippleDescriptors.join(" "),
			areolaDescriptors.join(" ")
		].join(", ");
	}

	/**
	 * @override
	 */
	positive() {
		let promptParts = [];

		const breastDescriptor = this._breastPrompt();
		if (breastDescriptor) {
			promptParts.push(breastDescriptor);
		}

		const nippleDescriptor = this._nippleAreolaPrompt();
		if (nippleDescriptor) {
			promptParts.push(nippleDescriptor);
		}

		return promptParts.join(",");
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.boobs < 300) {
			return `medium breasts, large breasts, huge breasts${this.censored ? ", bare breasts, (nipples:1.1), areola, exposed chest" : ""}`;
		} else if (this.slave.boobs < 650) {
			return this.censored ? "bare breasts, (nipples:1.1), areola, exposed chest" : undefined;
		} else {
			return `small breasts, flat chest${this.censored ? ", bare breasts, (nipples:1.3), areola, exposed chest" : ""}`;
		}
	}
};


code:
// cSpell:ignore xxmaskedxx, nopussy, flaccidfutanarimix, micropp
App.Art.GenAI.ClothesPromptPart = class ClothesPromptPart extends App.Art.GenAI.PromptPart {
	/** @type {Record<FC.Clothes, {positive: string, negative: string}>} */
	clothesPrompts = {
		"no clothing": {
			"positive": "completely nude, frontal shot, lower body",
			"negative": "",
		},
		"a Fuckdoll suit": {  // NG good gen requires LoRA, but below will work without LoRA as well
			"positive": "black latex bodysuit, long sleeves" + App.Art.GenAI.PromptHelpers.lora("xxmaskedxx_lora_v01", .8, " xxmaskedxx", ", "),
			"negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples",
		},
		"conservative clothing": {
			"positive": "black pants, white silk blouse",
			"negative": "",
		},
		"chains": {
			"positive": "metal chain harness, wrist cuffs with short chains, waist chain, thigh chains, standing",
			"negative": "clothes, jeans, underwear, pants, shorts, skirt, panties",
		},
		"Western clothing": {
			"positive": "cowboy hat, white tied blouse, jeans hotpants, standing, hands on hips",
			"negative": "",
		},
		"body oil": {  // Doesn't work well
			"positive": "body oil, completely nude, wet skin, shiny skin, lower body",
			"negative": "clothes, jeans, underwear, pants, shorts, skirt, panties",
		},
		"a toga": {  // Doesn't work well
			"positive": "short white toga, sturdy white linen, gold belt, wrapped leather sandals",
			"negative": "",
		},
		"a huipil": {  // Doesn't work well
			"positive": "woven wrap top,  colorful short poncho, decorative sash, fringed mini skirt, layered bracelets, patterned textiles",
			"negative": "",
		},
		"a slutty qipao": {
			"positive": "short red silk qipao with gold embroidery, high mandarin collar, side slit up to the thigh, sleeveless, breast cutout, traditional frog buttons, subtle floral pattern woven into the fabric",
			"negative": "",
		},
		"a kimono": {
			"positive": "traditional kimono, crane and cherry blossom patterns, wide obi sash, long flowing sleeves",
			"negative": "",
		},
		"spats and a tank top": {  // Spats don't work well
			"positive": "bike shorts, tank top",
			"negative": "bike, jeans, nude, pussy, nipples",
		},
		"uncomfortable straps": {
			"positive": "leather harness, ring harness, thigh bands, armlet,  leather belt",
			"negative": "",
		},
		"shibari ropes": {
			"positive": "shibari rope, bondage, rope harness thigh ropes",
			"negative": "clothes, jeans, underwear, pants, shorts, skirt, panties",
		},
		"restrictive latex": {  // Doesn't work well
			"positive": "latex catsuit, long sleeves, long gloves, breast cutout, glossy finish,  glossy latex texture, reflective material, straight posture, arms on side",
			"negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, midriff",
		},
		"a latex catsuit": {  // Doesn't work well
			"positive": "latex catsuit, long sleeves, long gloves,  glossy finish,  glossy latex texture, reflective material",
			"negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy",
		},
		"attractive lingerie": {
			"positive": "$color lace g-string, lace bra, lace stockings",
			"negative": "clothes, jeans, pants",
		},
		"attractive lingerie for a pregnant woman": {  // Cupless part doesn't work well
			"positive": "cotton thong, cotton bra, cotton stockings",
			"negative": "",
		},
		"kitty lingerie": {  // Broken for photorealistic models, probably works for anime models
			"positive": "kitty lingerie, cat lingerie, kawaii lingerie",
			"negative": "cat ears, jeans, nude, pussy, nipples",
		},
		"a maternity dress": {
			"positive": "maternity dress, loose dress",
			"negative": "",
		},
		"stretch pants and a crop-top": {
			"positive": "crop top, leggings",
			"negative": "",
		},
		"a succubus outfit": {
			"positive": "succubus costume, red leather corset, red leather miniskirt, black demon horns, black lace stockings",
			"negative": "",
		},
		"a fallen nuns habit": {
			"positive": "latex mini dress, latex nun veil, long latex gloves, cross amulet, latex stockings, glossy finish,  glossy latex texture, reflective material",
			"negative": "",
		},
		"a penitent nuns habit": {
			"positive": "long burlap dress, long burlap sleeves, long  burlap skirt, beige nun veil, rope belt",
			"negative": "jeans, nude, pussy, nipples",
		},
		"a chattel habit": {
			"positive": "white latex mini dress, white latex nun veil, long latex gloves, cross amulet, gold belt, latex stockings, transparent clothing, glossy finish,  glossy latex texture, reflective material",
			"negative": "",
		},
		"a string bikini": {  // Cupless part doesn't work well
			"positive": "floral pattern, $color bikini top, bikini bottom",
			"negative": "",
		},
		"a scalemail bikini": {  // Doesn't work well
			"positive": "studded leather bikini, chainmail covering, bikini bottom, leather bracer",
			"negative": "",
		},
		"striped panties": {
			"positive": "blue striped panties, bare breasts, topless",
			"negative": "",
		},
		"a cheerleader outfit": {
			"positive": "$color cheerleader outfit, skirt, crop top, school emblem",
			"negative": "pom pom",
		},
		"clubslut netting": {
			"positive": "transparent clothing, $color crotchless fishnet bodysuit, fishnet gloves, fishnet stockings",
			"negative": "",
		},
		"cutoffs and a t-shirt": {
			"positive": "white t-shirt, jean shorts",
			"negative": "",
		},
		"slutty business attire": {
			"positive": "closed white blazer, white pencil skirt, black underwear, black stockings",
			"negative": ""
		},
		"nice business attire": {
			"positive": "white blazer, collared shirt, black skirt, black pantyhose",
			"negative": "",
		},
		"a ball gown": {
			"positive": "$color luxurious $material dress, long gloves, long skirt, strapless",
			"negative": "",
		},
		"a slave gown": {
			"positive": "transparent lace dress, transparend clothing, anklet, long skirt, strapless",
			"negative": "",
		},
		"a halter top dress": {
			"positive": "$color $material halterneck  dress, luxurious dress, bare back, long skirt",
			"negative": "jeans, nude, pussy, nipples",
		},
		"an evening dress": {
			"positive": "$color $material long wrapped dress, stockings",
			"negative": "",
		},
		"a mini dress": {
			"positive": "$color $material mini dress, strapless, stockings",
			"negative": "",
		},
		"a comfortable bodysuit": {
			"positive": "$colorLatex latex catsuit, off shoulder, long sleeves",
			"negative": "",
		},
		"a leotard": {
			"positive": "strapless $color leotard, pantyhose",
			"negative": "",
		},
		"a monokini": {  // This one likely will never do quite right since the monokini was a topless bikini, but if you search for it you get a lot of other bikini designs - franklygeorge
			"positive": `sporty $color one-piece swimsuit, topless one-piece swimsuit, monokini${V.aiAgeFilter ? "" : ", nipples"}`,
			"negative": "",
		},
		"an apron": {
			"positive": "short white kitchen apron, no clothes underneath, open sides, thighs, bare hips, side boob, bare legs, barefoot",
			"negative": "",
		},
		"overalls": {
			"positive": "overalls, naked overalls",
			"negative": "",
		},
		"a cybersuit": {  // Doesn't work well
			"positive": "futuristic $colorLatex catsuit, neon accents, glowing lines, shiny material",
			"negative": "",
		},
		"a tight Imperial bodysuit": {  // Doesn't work well
			"positive": "futuristic catsuit, long pants, neon accents, glowing lines, shiny material, cyberpunk, utility belt, bracers, armored corset, armored pauldron",
			"negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples",
		},
		"battlearmor": {  // Doesn't work well
			"positive": "knight armor, high collar, long armored pants, big pauldron, long knight gloves, armored corset, spikes, reflective material",
			"negative": "",
		},
		"Imperial Plate": {  // Doesn't work well
			"positive": "black roman armor, high collar, long skirt over pants, long pants, armored skirt, oversized pauldron, knee boots, layered armor, long knight gloves, armored corset, reflective material, long flowing cape",
			"negative": "",
		},
		"a bunny outfit": {
			"positive": "black satin leotard, strapless, pantyhose, stockings, headband, bunny ears",
			"negative": "",
		},
		"a slutty maid outfit": {
			"positive": "slutty french maid outfit, maid skirt, stockings, corset, frilly headband, apron",
			"negative": "",
		},
		"a nice maid outfit": {
			"positive": "french maid outfit, long petticoat skirt, high collar, long sleeves,  corset, frilly headband, apron",
			"negative": "",
		},
		"a slutty nurse outfit": {
			"positive": "nurse, short white latex mini dress, latex stockings, nurse cap",
			"negative": "",
		},
		"a nice nurse outfit": {
			"positive": "nurse, white medical scrubs, pants",
			"negative": "",
		},
		"a dirndl": {
			"positive": "dirndl, frilly white blouse with floral motif, corset, frilly cotton skirt",
			"negative": "",
		},
		"a long qipao": {
			"positive": "long red silk qipao with gold embroidery, high mandarin collar,  side slit up to the thigh, long sleeves, traditional frog buttons, subtle floral pattern woven into the fabric",
			"negative": "",
		},
		"lederhosen": {
			"positive": "traditional brown suede lederhosen, detailed embroidery, suspenders, white puff-sleeve blouse with a square neckline, knee-high wool socks",
			"negative": "",
		},
		"a biyelgee costume": {  // Doesn't work well
			"positive": "traditional biyelgee costume, long skirt, richly embroidered silk, wide decorative belt, long sleeves with ornate cuffs, intricate patterns, traditional headdress with beadwork and fabric accents",
			"negative": "",
		},
		"a hanbok": {
			"positive": "traditional hanbok with a pastel pink jeogori, white cuffs, delicate floral embroidery, over a flowing sky blue chima, with soft pleats, wide ribbon, tied neatly at the front, paired with white beoseon",
			"negative": "",
		},
		"a burkini": { // Doesn't work well
			"positive": "black burqa, burkini",
			"negative": "hair",
		},
		"a hijab and blouse": {
			"positive": "hijab, blouse, short sleeves, long skirt",
			"negative": "",
		},
		"a hijab and abaya": {
			"positive": "hijab, abaya",
			"negative": "",
		},
		"a niqab and abaya": {  // Doesn't work well
			"positive": "niqab, covered face, abaya",
			"negative": "",
		},
		"a burqa": {  // Doesn't work well
			"positive": "burqa, muslim clothes",
			"negative": "",
		},
		"a police uniform": {
			"positive": "police uniform, policewoman, police hat, jacket, pants, utility belt, holster",
			"negative": "",
		},
		"a gothic lolita dress": {
			"positive": "long black lace dress, long petticoat skirt, lace trim, long sleeves, long gloves",
			"negative": "",
		},
		"a one-piece swimsuit": {
			"positive": "sporty swimsuit,  high collar, shiny material",
			"negative": "",
		},
		"a nice pony outfit": {
			"positive":  App.Art.GenAI.PromptHelpers.lora("ponygirl", .7, " ponygirl, ",) + "bdsm, bodysuit, horse mask",
			"negative": "nude"
		},
		"a slutty pony outfit": {
			"positive": App.Art.GenAI.PromptHelpers.lora("ponygirl", .7, " ponygirl, ",) + "bdsm, horse mask",
			"negative": "pussy",
		},
		"a button-up shirt and panties": {  // Often not bottomless
			"positive": "collared shirt, oversized clothes, panties, (bottomless:1.1), thighs",
			"negative": "",
		},
		"a button-up shirt": {  // Often not bottomless
			"positive": "collared shirt, oversized clothes, nude, (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a sweater": {  // Often not bottomless
			"positive": "sweater, oversized clothes, nude, (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a t-shirt": {  // Often not bottomless
			"positive": "t-shirt, (nude:1.1), (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a tank-top": {  // Often not bottomless
			"positive": "tank top, bare shoulders, (nude:1.1), (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a tube top": {  // Often not bottomless
			"positive": "tube top, bare shoulders, (nude:1.1), (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nude, nipples",
		},
		"an oversized t-shirt": {  // Often not bottomless
			"positive": "t-shirt, oversized clothes, (nude:1.1), (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a bra": {  // Often not bottomless
			"positive": "bra, (nude:1.1), (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a sports bra": {  // Often not bottomless
			"positive": "sports bra, (nude:1.1), (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a striped bra": {  // Often not bottomless
			"positive": "striped bra, (nude:1.1), (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"pasties": {  // Doesn't work well
			"positive": "pasties, nude, (bottomless:1.1), thighs",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a tube top and thong": {
			"positive": "tube top, bare shoulders, (nude:1.1), (bottomless:1.1), g-string, thighs",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"a sweater and panties": {  // Often not bottomless
			"positive": "sweater, oversized clothes, panties, thighs",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"a tank-top and panties": {  // Often not bottomless
			"positive": "tank top, bare shoulders, panties, thighs",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"a t-shirt and thong": {  // Often not bottomless
			"positive": "t-shirt, g-string, thighs",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"an oversized t-shirt and boyshorts": {  // Doesn't work well
			"positive": "t-shirt, oversized clothes, boyshort panties, thighs",
			"negative": "jeans, pants, skirt, nipples, pussy",
		},
		"sport shorts and a t-shirt": {
			"positive": "t-shirt, sport shorts",
			"negative": "jeans, pants, skirt, nipples, pussy",
		},
		"sport shorts and a sports bra": {
			"positive": "sports bra, sport shorts",
			"negative": "jeans, pants, skirt, nipples, pussy",
		},
		"a t-shirt and panties": {  // Often not bottomless
			"positive": "t-shirt, panties, thighs",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"striped underwear": {  // Often not bottomless
			"positive": "striped panties, striped bra",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"a thong": {
			"positive": "thong, topless",
			"negative": "jeans, pants, skirt, shorts, pussy",
		},
		"a skimpy loincloth": {  // Doesn't work well
			"positive": "loincloth, topless",
			"negative": "jeans, pants, skirt, shorts, pussy",
		},
		"boyshorts": {
			"positive": "boyshort panties, topless",
			"negative": "jeans, pants, skirt, pussy",
		},
		"panties": {
			"positive": "panties, topless",
			"negative": "jeans, pants, skirt, pussy",
		},
		"panties and pasties": {  // Doesn't work well
			"positive": "panties, pasties, topless",
			"negative": "jeans, pants, skirt, pussy, nipples",
		},
		"cutoffs": {
			"positive": "jean shorts, topless",
			"negative": "pussy",
		},
		"sport shorts": {
			"positive": "sport shorts, topless",
			"negative": "jeans, pants, skirt, pussy",
		},
		"a sweater and cutoffs": {
			"positive": "sweater, jean shorts",
			"negative": "pussy, nipples",
		},
		"leather pants and a tube top": {
			"positive": "leather pants, tube top, bare shoulders",
			"negative": "jeans, pants, skirt, shorts, pussy, nipples",
		},
		"a t-shirt and jeans": {
			"positive": "t-shirt, jeans",
			"negative": "pussy, nipples",
		},
		"leather pants and pasties": {  // Doesn't work well
			"positive": "leather pants, pasties, topless",
			"negative": "jeans, pants, skirt, shorts, pussy, nipples",
		},
		"leather pants": {
			"positive": "leather pants, topless",
			"negative": "jeans, pants, skirt, shorts, pussy",
		},
		"jeans": {
			"positive": "jeans, topless",
			"negative": "pussy",
		},
		"a military uniform": {
			"positive": "military uniform, shirt, necktie, skirt",
			"negative": "",
		},
		"a red army uniform": { // not red army, but soviet is at least close
			"positive": "soviet uniform, military uniform, shirt, necktie, skirt",
			"negative": "",
		},
		"battledress": {
			"positive": "military fatigues, camouflage pants, tank top",
			"negative": "",
		},
		"a mounty outfit": {  // Doesn't work well
			"positive": "mounty, red military jacket",
			"negative": "jeans, shorts, pussy, nipples",
		},
		"harem gauze": {
			"positive": "face veil, belly chain, belly dancer outfit, loose dress, see-through, transparent clothes",
			"negative": "",
		},
		"slutty jewelry": {
			"positive": "completely nude, gem amulet, belly chains, gold chain bra, gold chains, armlet, gold headdress",
			"negative": ""
		},
		"a Santa dress": {
			"positive": "santa costume, santa dress, stockings, fur trim",
			"negative": ""
		},
		"a bimbo outfit": {
			"positive": "pink tube top, latex pink miniskirt",
			"negative": "jeans, nude, pussy, nipples",
		},
		"a slutty outfit": {
			"positive": "(pink:1.1) crop top, pink lowleg microskirt, hip bones, groin, tight clothes, midriff, navel, (thighs:1.1)",
			"negative": "jeans, nude, nipples",
		},
		"a courtesan dress": {  // Corset was messing stuff up, so I removed it
			"positive": "long dress, layered fabric, corset, bare shoulders, long sleeves, detached sleeves, stockings",
			"negative": "",
		},
		"a schoolgirl outfit": {
			"positive": "white tight blouse tied above navel, plaid micro skirt, stockings",
			"negative": "",
		},
		"a superhero outfit": {
			"positive": "red and blue superman leotard, superman symbol, red cape, red boots, standing, hands on hips",
			"negative": "",
		},
		"a cardigan": {
			"positive": "crop top, loose cardigan, leather pants, knee boots",
			"negative": "",
		},
		"a streetwear outfit": {
			"positive": "ripped fishnet stockings, short denim skirt, crop top, leather jacket",
			"negative": "",
		},
		"a country outfit": {
			"positive": "tight embroidered corset top, puffed sleeves, flower embroidery, flared folk skirt, thigh-high boots",
			"negative": "",
		},
		"a leather outfit": {
			"positive": "black leather corset, sleeveless, tight leather pants, leather bracer, knee boots",
			"negative": "",
		},
		"a turtleneck and miniskirt": {
			"positive": "form-fitting turtleneck sweater, leather mini skirt, thigh-high boots",
			"negative": "",
		},
		"a red priestess outfit": {
			"positive": "crimson red robe, detached sleeves, long skirt, bone headdress, corset, ritual ruby necklace, lace stockings, high heels, standing, praying",
			"negative": "",
		},
		"an eastern european outfit": {
			"positive": "embroidered blouse, corset, long floral skirt, floral crown, high heels",
			"negative": "",
		},
		"a samba outfit": {
			"positive": "carnival outfit, elaborate feathered headdress, jeweled bikini, sparkling bra top, beaded thong, high heels, feathered backpiece, sequins, decorative arm bands, shimmering textures",
			"negative": "",
		},
		"a tribal outfit": {
			"positive": "tribal-inspired outfit, beaded halter top, decorative feathers, feather headdress, leather armband, fringed mini skirt, leather straps, ornamental patterns, braided accessories, high boots, sensual tribal fashion",
			"negative": "",
		},
		"a tropical outfit": {
			"positive": "woven grass skirt, woven grass bra top, flower lei, decorative arm bands",
			"negative": "",
		},
		"a valkyrie outfit": {
			"positive": "fur-trimmed leather corset, metal accents, metal bracer, mini skirt, fur coat, braided hair accessories, warrior elegance, knee boots, valkyrie",
			"negative": "",
		},
		"a baroque dress": {
			"positive": "$color petticoat baroque dress, floor length skirt, high heels, standing",
			"negative": "",
		},
		"a cocktail dress": {
			"positive": "$color frill cocktail dress, off-the-shoulder design, fitted waist, high heels, stockings, knee length, standing",
			"negative": "",
		},
		"an ice queen outfit": {
			"positive": "long white lace dress, fur coat, high heels, white stockings, white crown",
			"negative": "",
		},
		"a magical girl costume": {
			"positive": "magical girl costume, frilly skirt, pastel colors, high heels, stockings",
			"negative": "",
		},
		"a sundress": {
			"positive": "short floral sundress, wide-brim sunhat, sandals",
			"negative": "",
		},
		"a wedding dress": {
			"positive": "white wedding dress, veil, high heels, holding a rose bouquet",
			"negative": "",
		},
		"an armored dress": {
			"positive": "Fantasy armor, long skirt, metallic corset, gold embroidery, royal cloak, metal accessories, shoulder pads, bracer, high boots",
			"negative": "",
		},
		"a babydoll": {
			"positive": "$color babydoll nightgown, transparent fabric, bows, thighhighs",
			"negative": "",
		},
		"a coat and corset": {
			"positive": "black trench coat, corset, lace stockings, lace skirt",
			"negative": "",
		},
		"a frilly bikini": {
			"positive": "$color ruffled bikini set, frill bikini, frill bikini bottom, frilly details",
			"negative": "",
		},
	};

	clothesPromptsAgeControl = {
		"no clothing": {
			"positive": "strapless tube top, visible shoulders",
			"negative": "",
		},
		"chains": {
			"positive": "metal chains collar, chainmail tube top, visible shoulders, chain belt, chainmail skirt",
			"negative": "jeans, pants, skirt",
		},
		"body oil": {
			"positive": "(shiny skin, glistening skin, body oil:1.1), strapless swimsuit, visible shoulders",
			"negative": "jeans",
		},
		"a slutty qipao": {
			"positive": "qipao, chinese clothing",
			"negative": "jeans, nude, pussy, nipples",
		},
		"spats and a tank top": {
			"positive": "bike shorts, tank top",
			"negative": "bike, jeans, nude, pussy, nipples",
		},
		"uncomfortable straps": {
			"positive": "leather straps top, visible shoulders, leather belt, leather straps skirt",
			"negative": "jeans, pants, shorts",
		},
		"shibari ropes": {
			"positive": "macrame tube top, ropes, rope belt, macrame skirt",
			"negative": "jeans, pants, shorts",
		},
		"attractive lingerie": {
			"positive": "strapless swimsuit, visible shoulders",
			"negative": "jeans, pants",
		},
		"attractive lingerie for a pregnant woman": {
			"positive": "strapless swimsuit, visible shoulders",
			"negative": "jeans, pants",
		},
		"kitty lingerie": { // Broken for photorealistic models, probably works for anime models
			"positive": "strapless hello kitty swimsuit, visible shoulders",
			"negative": "cat ears, jeans",
		},
		"a maternity dress": {
			"positive": "wide dress, loose dress",
			"negative": "jeans, nude, pussy, nipples",
		},
		"a succubus outfit": {
			"positive": "demon costume, red leather top, red leather miniskirt, black demon horns",
			"negative": "jeans, nude, pussy, nipples",
		},
		"a penitent nuns habit": {
			"positive": "(latex nun habit:1.1), ropes",
			"negative": "jeans",
		},
		"a chattel habit": {
			"positive": "(white latex nun habit:1.1), gold belt, sleveless, cleavage, visible shoulders",
			"negative": "",
		},
		"a string bikini": {
			"positive": "strapless swimsuit",
			"negative": "jeans,",
		},
		"a scalemail bikini": {
			"positive": "chainmail swimsuit",
			"negative": "jeans",
		},
		"striped panties": {
			"positive": "strapless blue striped swimsuit",
			"negative": "jeans",
		},
		"clubslut netting": {
			"colors": ["light blue", "pink", "lime green"],
			"positive": "rave clothing, fishnet clothing, $color bodysuit, choker",
			"negative": "jeans, pants, corset",
		},
		"a slave gown": {
			"positive": "ballgown, long dress, luxurious dress, cleavage, slave straps",
			"negative": "jeans",
		},
		"a halter top dress": {
			"positive": "(halterneck:1.1), long dress, luxurious dress, backless dress",
			"negative": "jeans",
		},
		"a leotard": {
			"positive": "leotard",
			"negative": "jeans",
		},
		"a monokini": {
			"positive": "swimsuit",
			"negative": "jeans",
		},
		"an apron": {
			"positive": "apron swimsuit",
			"negative": "t-shirt, shirt, pants, shorts",
		},
		"overalls": {
			"positive": "overalls, visible shoulders, sleeveless",
			"negative": "t-shirt, shirt, pants, shorts, topless",
		},
		"a bunny outfit": {
			"positive": "magazine bunny costume, backless leotard",
			"negative": "jeans, nude, rabbit ears",
		},
		"a gothic lolita dress": {
			"positive": "gothic dress, short dress, thighhighs",
			"negative": "jeans",
		},
		"a button-up shirt and panties": {
			"positive": "collared shirt, oversized clothes, swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"a button-up shirt": {
			"positive": "collared shirt, oversized clothes, swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"a sweater": {
			"positive": "only sweater, oversized clothes, swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"a t-shirt": {
			"positive": "only t-shirt, swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"a tank-top": {
			"positive": "only tank top, visible shoulders",
			"negative": "jeans",
		},
		"a tube top": {
			"positive": "only tube top, visible shoulders",
			"negative": "jeans",
		},
		"an oversized t-shirt": {
			"positive": "only t-shirt, oversized clothes, swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"a bra": {
			"positive": "white swimsuit top",
			"negative": "jeans, pants, skirt, shorts",
		},
		"a sports bra": {
			"positive": "sports swimsuit top",
			"negative": "jeans, pants, skirt, shorts",
		},
		"a striped bra": {
			"positive": "striped swimsuit top",
			"negative": "jeans, pants, skirt, shorts",
		},
		"pasties": {
			"positive": "strapless tube top, visible shoulders",
			"negative": "",
		},
		"a tube top and thong": {
			"positive": "tube top, visible shoulders",
			"negative": "jeans",
		},
		"a sweater and panties": {
			"positive": "sweater, oversized clothes, swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"a tank-top and panties": {
			"positive": "tank top, visible shoulders",
			"negative": "jeans",
		},
		"a t-shirt and thong": {
			"positive": "t-shirt, swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"an oversized t-shirt and boyshorts": {
			"positive": "t-shirt, oversized clothes, swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"sport shorts and a sports bra": {
			"positive": "sports swimsuit top",
			"negative": "jeans, pants, skirt",
		},
		"a t-shirt and panties": {
			"positive": "t-shirt, swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"striped underwear": {
			"positive": "striped swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"a thong": {
			"positive": "tube top, visible shoulders",
			"negative": "jeans",
		},
		"a skimpy loincloth": {
			"positive": "leather straples swimsuit",
			"negative": "jeans, pants, skirt, shorts",
		},
		"boyshorts": {
			"positive": "swimsuit top",
			"negative": "jeans",
		},
		"panties": {
			"positive": "swimsuit top",
			"negative": "jeans",
		},
		"panties and pasties": {
			"positive": "swimsuit top",
			"negative": "jeans",
		},
		"cutoffs": {
			"positive": "jean shorts, strapless tube top, visible shoulders",
			"negative": "",
		},
		"sport shorts": {
			"positive": "sports swimsuit top, sport shorts",
			"negative": "jeans, pants, skirt",
		},
		"leather pants and a tube top": {
			"positive": "leather pants, tube top, visible shoulders",
			"negative": "jeans, skirt, shorts",
		},
		"leather pants and pasties": {
			"positive": "leather pants, swimsuit top",
			"negative": "jeans, skirt, shorts",
		},
		"leather pants": {
			"positive": "leather pants, swimsuit top",
			"negative": "jeans, skirt, shorts",
		},
		"jeans": {
			"positive": "jeans, swimsuit top",
			"negative": "",
		},
		"harem gauze": {
			"positive": "harem outfit, loose dress",
			"negative": "jeans, shorts",
		},
		"slutty jewelry": {
			"positive": "jewelry, gem, gold chains, armlet, visible shoulders",
			"negative": "jeans, pants, shorts"
		},
		"a bimbo outfit": {
			"positive": "(pink tube top:1.1), cleavage",
			"negative": "",
		},
		"a slutty outfit": {
			"positive": "(pink crop top:1.1), cleavage",
			"negative": "",
		},
		"a courtesan dress": {  // Corset was messing stuff up, so I removed it
			"positive": "(luxurious flowing dress:1.1), exposed shoulders, long sleeves, detached sleeves",
			"negative": "jeans, nude, pussy, nipples",
		},
	};
	/**
	 * @returns {string}
	 */
	getClothes() {
		let clothes = this.slave.clothes;
		if (!this.clothesPrompts.hasOwnProperty(clothes) && !V.customClothesPrompts.hasOwnProperty(clothes)) {
			console.error("AI Art: Missing clothing:", clothes);
			clothes = "no clothing";
		}
		return clothes;
	}

	/**
	 * Replace the literal "$color" in a prompt with the name of a color
	 * @param {string} prompt
	 * @returns {string}
	 */
	replacer(prompt) {
		const colors =
		[
			"light blue", "cream colored", "pink", "white", "black", "silver", "gold", "deep red", "amethyst", "emerald", "deep blue", "turquoise",
			"bright red", "black and red", "silver and gold", "black and gold", "white and gold", "emerald and gold", "deep blue and silver",
			"deep blue and gold"
		];
		const colorsLatex = ["pink", "white", "black", "deep red"];
		const materials = ["latex", "satin", "lace", "sequin", "silk", "tulle", "fishnet", "frill"];

		let availableColorsLatex = [...colorsLatex];
		let availableColors = [...colors];
		let availableMaterials = [...materials];

		let randomIndex = Math.floor(seededRandom(this.slave.natural.artSeed - prompt.length) * availableColorsLatex.length);
		prompt = prompt.replaceAll('$colorLatex', availableColorsLatex[randomIndex] );

		randomIndex = Math.floor(seededRandom(this.slave.natural.artSeed - prompt.length) * availableColors.length);
		prompt = prompt.replaceAll('$color', availableColors[randomIndex] );

		randomIndex = Math.floor(seededRandom(this.slave.natural.artSeed - prompt.length) * availableMaterials.length);
		prompt = prompt.replaceAll('$material', availableMaterials[randomIndex] );

		return prompt;
	}

	/**
	 * Adds missing words to the negative prompt is aiAgeControl is active
	 * @param {string} negPrompt
	 * @returns {string}
	 */
	addNegativeControl(negPrompt) {
		const toAdd = ["penis", "pussy", "nude", "scrotum", "clitoris", "topless"];
		if (this.censored) {
			toAdd.forEach(w => {
				if (!negPrompt.includes(w)) {
					negPrompt += `${negPrompt.length > 0 ? ", " : ""}${w}`;
				}
			});
		}
		return negPrompt;
	}

	/**
	 * @override
	 */
	positive() {
		let basePrompt;
		if (V.customClothesPrompts.hasOwnProperty(this.getClothes()) && V.customClothesPrompts[this.getClothes()].positive !== '') {
			basePrompt = V.customClothesPrompts[this.getClothes()];
		} else {
			if (this.censored){
				basePrompt = this.clothesPromptsAgeControl[this.getClothes()] ?? this.clothesPrompts[this.getClothes()];
			} else {
				basePrompt = this.clothesPrompts[this.getClothes()];
			}
		}

		const coloredPrompt = this.replacer(basePrompt.positive);
		return coloredPrompt;
	}

	/**
	 * @override
	 */
	negative() {
		if (V.customClothesPrompts.hasOwnProperty(this.getClothes()) && V.customClothesPrompts[this.getClothes()].negative !== '') {
			return this.addNegativeControl(V.customClothesPrompts[this.getClothes()].negative + (this.censored) ? ", (nude:1.3), (nipples:1.1), areola" : "");
		} else {
			return this.censored ? this.addNegativeControl(this.clothesPromptsAgeControl[this.getClothes()]?.negative ?? this.clothesPrompts[this.getClothes()].negative) : this.clothesPrompts[this.getClothes()].negative;
		}
	}
};

repo_name: Nithhogg/fc

license: mit


code:
App.Art.GenAI.CollarPromptPart = class CollarPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (asSlave(this.slave)?.fuckdoll > 0) {
			return undefined; // fuckdolls can't wear collars
		}

		if (this.slave.collar === "bell collar") {  // Doesn't work well, better than "bell collar collar"
			return "bell collar";
		} else if (this.slave.collar === "bowtie") {
			return "bowtie, collar";
		} else if (this.slave.collar === "leather with cowbell") {  // Doesn't work well, better than "leather with cowbell collar"
			return "leather collar, cowbell around neck";
		} else if (this.slave.collar === "neck corset") { // Doesn't work well, but doesn't add real corsets
			return "tall leather collar, tight collar";
		} else if (this.slave.collar === "neck tie") {
			return "(necktie:1.2), collar";
		} else if (this.slave.collar === "satin choker") {
			return "satin choker";
		} else if (this.slave.collar !== "none") {
			if (this.slave.collar.includes("biometrics")) { // Avoid a pregnancy prompt
				return "collar";
			}
			return `${this.slave.collar} collar`;
		}
		return undefined;
	}

	/**
	 * @override
	 */
	negative() {
		return undefined;
	}
};


code:
// cSpell: ignore nopussy_v1

App.Art.GenAI.CrotchPromptPart = class CrotchPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		// censored or covered by clothes
		const exposesCrotch = this.helper.exposesCrotch(this.slave.clothes);
		const exposesUnderwear = this.helper.exposesUnderwear(this.slave.clothes);
		if (!exposesCrotch || this.censored) {
			if (!this.censored && this.slave.dick) {
				return "bulge";
			}
			if (!this.censored && !this.slave.dick && this.slave.vagina && exposesUnderwear) {
				return "cameltoe";
			}
			return undefined;
		}
		let promptParts = [];

		// null slaves
		if (this.slave.dick === 0 && this.slave.vagina === -1) {
			if (this.helper.hasLora("nopussy_v1")) {
				return this.helper.lora("nopussy_v1", 1);
			} else {
				return undefined; // probably renders as female anyway; use the LoRA if you want good results
			}
		}

		// chasity device
		if (this.slave.chastityPenis  || this.slave.chastityVagina) {
			let type = this.slave.chastityPenis ? "cage" : "belt";
			return `chastity ${type}` + this.helper.lora("chastitybelt", .7, "", ", ");
		}

		const {vagina, clit, labia, dick, foreskin, scrotum, balls} = this.slave;

		// futanari
		if (dick && vagina >= 0) {
			// cSpell: ignore flaccidfutanarimix, micropp
			if (this.slave.dick > 4 && this.helper.hasLora("flaccidfutanarimix-locon-dim64-alpha64-highLR")) {
				promptParts.push(this.helper.lora("flaccidfutanarimix-locon-dim64-alpha64-highLR", .8)); // Massive, unrealistic penis for futa - Converts to female appearance
			} else if (this.slave.dick >= 2 && this.helper.hasLora("futanari")) {
				promptParts.push(this.helper.lora("futanari")); // Normal penis for futa - Converts to female appearance
			} else if (this.slave.dick < 2 && this.slave.dick > 0 && this.helper.hasLora("micropp_128dim_nai_v2")) {
				promptParts.push(this.helper.lora("micropp_128dim_nai_v2", .8)); // Micro penis for futa - Converts to female appearance
			} else {
				// Applying this hard to override dick and pussy prompts.
				promptParts.push("futanari, herm");
			}
			promptParts.push(`[${dickDescription()}]`);
			promptParts.push(`[${vaginaDescription()}]`);
		// male
		} else if (dick) {
			if (this.slave.dick < 2 && this.helper.hasLora("micropp_128dim_nai_v2")) {
				promptParts.push(this.helper.lora("micropp_128dim_nai_v2", .8)); // Micropenis
			} else if (this.slave.dick < 4 && this.helper.hasLora("OnlyCocksV1LORA")) {
				promptParts.push(this.helper.lora("OnlyCocksV1LORA", .8)); // Average Male Penis. Note this LoRA is always erect...
			} else if (this.helper.hasLora("flaccidfutanarimix-locon-dim64-alpha64-highLR")) {
				promptParts.push(this.helper.lora("flaccidfutanarimix-locon-dim64-alpha64-highLR", .8)); // Massive schlong. Always flaccid...
			}
			promptParts.push(dickDescription());
		// female
		} else if (vagina >= 0) {
			promptParts.push(vaginaDescription());
		}

		return promptParts.join(", ");

		/** @returns {string} */
		function dickDescription() {
			let dickDescriptors = [];
			switch (dick) {
				case 0: break;
				case 1: dickDescriptors.push("tiny"); break;
				case 2: dickDescriptors.push("small"); break;
				case 3: dickDescriptors.push("normal-sized"); break;
				case 4: dickDescriptors.push("big"); break;
				case 5: dickDescriptors.push("huge"); break;
				case 6: dickDescriptors.push("gigantic"); break;
				case 7: dickDescriptors.push("massive"); break;
				case 8: dickDescriptors.push("extremely massive"); break;
				case 9: dickDescriptors.push("absurdly massive"); break;
				case 10:
				case 11: dickDescriptors.push("unbelievably massive"); break;
			}

			if (dickDescriptors.length) {
				if (foreskin === 0) {
					dickDescriptors.push("uncircumcised");
				} else {
					dickDescriptors.push("circumcised");
				}
			}

			dickDescriptors.push("penis");

			if (scrotum && balls > 1) {
				dickDescriptors.push("with");

				switch (balls) {
					case 2: dickDescriptors.push("small"); break;
					case 3: break;
					case 4: dickDescriptors.push("large"); break;
					case 5: dickDescriptors.push("massive"); break;
					case 6: dickDescriptors.push("huge"); break;
					case 7: dickDescriptors.push("gigantic"); break;
					case 8: dickDescriptors.push("enormous"); break;
					case 9: dickDescriptors.push("extremely enormous"); break;
					case 10:
					case 11: dickDescriptors.push("inhumanly enormous"); break;
				}

				dickDescriptors.push("balls");
			}

			return dickDescriptors.join(" ");
		}

		/** @returns {string} */
		function vaginaDescription() {
			let vaginaDescriptors = ["pussy"];

			let clitDescriptors = [];
			switch (clit) {
				case 0: break;
				case 1: clitDescriptors.push("large"); break;
				case 2: clitDescriptors.push("huge"); break;
				case 3: clitDescriptors.push("enormous"); break;
				case 4: clitDescriptors.push("penis-like"); break;
				case 5: clitDescriptors.push("large and penis-like"); break;
			}

			if (clitDescriptors.length) {
				clitDescriptors.push("clit");
				vaginaDescriptors.push(clitDescriptors.join(" "));
			}

			let labiaDescriptors = [];
			switch (labia) {
				case 0: break;
				case 1: labiaDescriptors.push("large"); break;
				case 2: labiaDescriptors.push("huge"); break;
				case 3: labiaDescriptors.push("enormous"); break;
			}

			if (labiaDescriptors.length) {
				labiaDescriptors.push("labia");
				vaginaDescriptors.push(labiaDescriptors.join(" "));
			}

			return vaginaDescriptors.join(",");
		}
	}

	/**
	 * @override
	 */
	negative() {
		const {vagina, dick, balls, scrotum} = this.slave;
		let promptParts = [];

		if (!dick && vagina >= 0) {
			promptParts.push("penis");
		}

		if (dick) {
			if (vagina < 0) {
				promptParts.push("pussy");
			}

			if (balls < 2) {
				promptParts.push("balls");
			}

			if (!scrotum) {
				promptParts.push("scrotum");
			}
		}

		return promptParts.join(", ");
	}
};


code:
App.Art.GenAI.CustomPromptPart = class CustomPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		let positive = "";
		const slave = asSlave(this.slave);
		if (slave && slave.useRulesAssistant === 1 && slave.custom.aiPrompts?.positiveRA) {
			positive += slave.custom.aiPrompts?.positiveRA;
		}

		if (slave && slave.custom.aiPrompts?.positive) {
			if (positive !== "" && positive.charAt(positive.length-1) !== ",") {
				positive += ",";
			}
			positive += slave.custom.aiPrompts?.positive;
		}

		if (positive !== "") {
			return positive;
		} else {
			return undefined;
		}
	}

	/**
	 * @override
	 */
	negative() {
		let negative = "";
		const slave = asSlave(this.slave);
		if (slave && slave.useRulesAssistant === 1 && slave.custom.aiPrompts?.negativeRA) {
			negative += slave.custom.aiPrompts?.negativeRA;
		}

		if (slave && slave.custom.aiPrompts?.negative) {
			if (negative !== "" && negative.charAt(negative.length-1) !== ",") {
				negative += ",";
			}
			negative += slave.custom.aiPrompts?.negative;
		}

		if (negative !== "") {
			return negative;
		} else {
			return undefined;
		}
	}
};


code:
App.Art.GenAI.EarsPromptPart = class EarsPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.slave.faceAccessory === "cat ears") {
			return `cat ears`; // TODO: this probably needs a lora as it is a cat face mask with cat ears, not actual cat ears
		}
		const parts = [];
		if (this.slave.earT !== "none" && this.slave.earT !== "normal") {
			parts.push(`${this.slave.earT} ears`);
		}
		return parts.join(`, `);
	}

	/**
	 * @override
	 */
	negative() {
		const parts = [];
		if (this.slave.tail === "none") {
			parts.push(`tail`); // animal ears bleed into tail
		}
		if (this.slave.earT === "none" && this.slave.earShape === "none") {
			parts.push(`ear`, `ears`);
		}
		return parts.join(`, `);
	}

	/**
	 * @override
	 */
	face() {
		return this.positive();
	}
};


code:
App.Art.GenAI.ExpressionPromptPart = class ExpressionPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		const slave = asSlave(this.slave);
		const customPrompt = slave?.custom?.aiPrompts?.expressionPositive;
		if (customPrompt) {
			return customPrompt;
		}

		if (slave?.fuckdoll !== 0) {
			if (slave?.fuckdoll < 50) {
				return `open mouth, clenched fists`; // NG proxy for terrified for early adaptation
			} else {
				return undefined;
			}
		} else if (this.helper.hasLora("Empty Eyes - Drooling v5 - 32dim") && this.slave.fetish === Fetish.MINDBROKEN) {
			return this.helper.lora("Empty Eyes - Drooling v5 - 32dim", 1, " empty eyes, drooling");
		} else {
			let devotionPart;
			if (this.slave.devotion < -50) {
				devotionPart = `angry expression, hateful`;
			} else if (this.slave.devotion < -20) {
				devotionPart = `angry`;
			} else if (this.slave.devotion < 51) {
				devotionPart = null;
			} else if (this.slave.devotion < 95) {
				devotionPart = `smile`;
			} else {
				devotionPart = `smile, grin, loving expression`;
			}

			let trustPart;
			if (slave) {
				if (slave.trust < -90) {
					trustPart = `(scared expression:1.2), looking down, crying, tears`;
				}
				if (slave.trust < -50) {
					trustPart = `(scared expression:1.1), looking down, crying`;
				} else if (slave.trust < -20) {
					trustPart = `scared expression, looking down`;
				} else if (slave.trust < 51) {
					trustPart = `looking at viewer`;
					if (!devotionPart) {
						trustPart += `, neutral expression`;
					}
				} else if (slave.trust < 95) {
					trustPart = `looking at viewer, confident`;
				} else {
					trustPart = `looking at viewer, confident, smirk`;
				}
			}

			let expression;

			if (devotionPart && trustPart) {
				expression = `${devotionPart}, ${trustPart}`;
			} else if (devotionPart) {
				expression = `${devotionPart}`;
			} else if (trustPart) {
				expression = `${trustPart}`;
			}

			if (this.helper.isXLBased()) {
				return `${expression}`;
			}
			return `(${expression})`;
		}
	}

	/**
	 * @override
	 */
	negative() {
		const slave = asSlave(this.slave);
		const customPrompt = slave?.custom?.aiPrompts?.expressionNegative;
		if (customPrompt) {
			return customPrompt;
		}

		if (slave?.fuckdoll !== 0) {
			return `smile, angry, confident`;
	   } else if (this.slave.fetish === Fetish.MINDBROKEN) {
			 return `smile, angry, looking at viewer, confident`;
		} else {
			let devotionPart;
			if (this.slave.devotion < -50) {
				devotionPart = `smile, loving expression`;
			} else if (this.slave.devotion < -20) {
				devotionPart = `smile`;
			} else if (this.slave.devotion < 51) {
				devotionPart = null;
			} else {
				devotionPart = `angry`;
			}

			let trustPart;
			if (slave) {
				if (slave.trust < -50) {
					trustPart = `looking at viewer, confident`;
				} else if (slave.trust < -20) {
					trustPart = null;
				} else {
					trustPart = `looking away`;
				}
			}

			if (devotionPart && trustPart) {
				return `${devotionPart}, ${trustPart}`;
			} else if (devotionPart) {
				return devotionPart;
			} else if (trustPart) {
				return trustPart;
			}
		}
	}

	/**
	 * @override
	 */
	face() {
		return this.positive();
	}
};


code:
App.Art.GenAI.EyePromptPart = class EyePromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		const positive = [];
		const slave = asSlave(this.slave);
		if (slave?.fuckdoll > 0) {
			return undefined; // eyes are not visible behind fuckdoll mask
		} else if (hasBothEyes(this.slave)) {
			positive.push(this.helper.lora("Loraeyes_V1"));
			if (!canSee(this.slave) && this.helper.hasLora("eye-allsclera")) {
				positive.push(this.helper.lora("eye-allsclera", 1));
			} else if (this.slave.eye.left.iris === this.slave.eye.right.iris) {
				if (this.slave.eye.left.sclera !== "white") {
					positive.push(`${this.slave.eye.left.iris} eyes, ${this.slave.eye.left.sclera} sclera`);
				} else {
					positive.push(`${this.slave.eye.left.iris} eyes` );
				}
				switch (this.slave.eye.left.sclera) {
					case "catlike": case "demonic": case "devilish": case "serpent-like":
						positive.push("slit pupils");
						break;
					case "goat-like":
						positive.push("horizontal pupils"); // Probably won't do anything, even on Pony;
						break;
					case "heart-shaped":
						positive.push("<3 eyes");
						break;
					case "hypnotic":
						positive.push("spiral eyes");
						break;
					case "vacant":
						positive.push("blank eyes");
						break;
				}
			} else {
				positive.push(`heterochromia, ${this.slave.eye.left.iris} left eye, ${this.slave.eye.right.iris} right eye`);
			}
		} else if (hasLeftEye(this.slave)) { // one-eyed prompts don't seem to work well regardless of wording (no/empty/missing/etc)
			positive.push(`no right eye, ${this.slave.eye.left.iris} left eye`);
		} else if (hasRightEye(this.slave)) {
			positive.push(`no left eye, ${this.slave.eye.right.iris} right eye`);
		} else {
			positive.push(`closed eyes`);
		}
		if (this.slave.eyewear === "corrective glasses" || this.slave.eyewear === "blurring glasses" || this.slave.eyewear === "glasses"){
			positive.push(`glasses`);
		}
		return positive.filter(p => !!p).join(`, `);
	}

	/**
	 * @override
	 */
	negative() {
		return undefined;
	}

	/**
	 * @override
	 */
	face() {
		return this.positive();
	}
};


code:
App.Art.GenAI.EyebrowPromptPart = class EyebrowPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.helper.isXLBased()) {
			return;
		}
		const slave = asSlave(this.slave);
		if (slave?.fuckdoll > 0) {
			return; // covered by fuckdoll mask
		}
		if (this.slave.eyebrowHStyle === "shaved" || this.slave.eyebrowHStyle === "bald" || this.slave.eyebrowHStyle === "hairless") {
			return;
		}
		return `${this.slave.eyebrowFullness} eyebrows`;
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.eyebrowHStyle === "shaved" || this.slave.eyebrowHStyle === "bald" || this.slave.eyebrowHStyle === "hairless") {
			return "eyebrows";
		}
		return;
	}

	/**
	 * @override
	 */
	face() {
		return this.positive();
	}
};


code:
App.Art.GenAI.FakeTitsPromptPart = class FakeTitsPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.censored) {
			return undefined;
		} else {
			if (this.slave.boobsImplant > 1) {
				return `fake tits`;
			}
		}
		return undefined;
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.boobsImplant === 0) {
			return `fake tits`; // Space for negative prompt if needed NG
		}
		return undefined;
	}
};


code:
App.Art.GenAI.GenderPromptPart = class GenderPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		let prompt = undefined;
		if (this.isFeminine) {
			prompt = "1girl, ";
			if (this.slave.race === "catgirl") {
				prompt += "catgirl, catperson" + this.helper.lora("CatgirlLoraV7", .8, "", " ");
			} else if (this.slave.visualAge >= 20) {
				prompt += "woman";
			} else {
				prompt += "girl";
			}
		} else if (this.isMasculine) {
			prompt = "1boy, ";
			if (this.slave.race === "catgirl") {
				prompt += "catboy, catperson" + this.helper.lora("CatgirlLoraV7", .8, "", " ");
			} else if (this.slave.visualAge >= 20) {
				prompt += "man";
			} else {
				prompt += "boy";
			}
		} else {
			if (this.slave.race === "catgirl") {
				prompt = "catperson" + this.helper.lora("CatgirlLoraV7", .8, "", " ");
			} else {
				prompt = undefined;
			}
		}
		if (this.censored && typeof prompt !== "undefined") {
			prompt = `${this.slave.visualAge} year old ${prompt}`;
		}
		return prompt;
	}

	/**
	 * @override
	 */
	negative() {
		let facialHair = this.slave.hormoneBalance > -20 ? "beard, mustache, " : ""; // NG make permanent part of negative prompt?
		if (this.isFeminine) {
			if (perceivedGender(this.slave) < -1) {  // Feminine hormone but Masculine appearing
				return undefined;
			} else { // Feminine hormone, Feminine appearing
				return `${facialHair}man, boy`;
			}
		} else if (this.isMasculine) {
			if (perceivedGender(this.slave) > 1) { // Masculine hormone but Feminine appearing
				return undefined;
			} else { // Masculine hormone, Masculine appearing
				return `${facialHair}woman, girl`;
			}
		} else {
			return undefined;
		}
	}
};


code:
App.Art.GenAI.HairPromptPart = class HairPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.slave.hStyle === "bald" || this.slave.hStyle === "shaved" || this.slave.hLength === 0) {
			return `bald`;
		}

		const styleObj = App.Medicine.Modification.hairStyles.Normal.find(hs => hs.value === this.slave.hStyle);
		let styleStr = (styleObj?.title || this.slave.hStyle).toLowerCase();
		const stylePostfix = styleStr.startsWith("in") || styleStr === "up";

		const heightVhLength = this.slave.hLength / this.slave.height;
		let hairLength = '';
		if (heightVhLength > 0.9) {
			if (this.helper.isPony()) {
				hairLength = `absurdly long`;
			} else {
				hairLength = `(very long:1.2)`;
			}
		} else if (heightVhLength > 0.7) {
			if (this.helper.isPony()) {
				hairLength = `extremely long`;
			} else {
				hairLength = `(very long:1.1)`;
			}
		} else if (heightVhLength >= 0.4) {
			hairLength = `very long`;
		} else if (heightVhLength >= 0.2) {
			hairLength = `long`;
		} else if (this.slave.hLength >= 15) {
			hairLength = `medium`;
		} else {
			hairLength = `short`;
		}
		const parts = [];
		if (stylePostfix) {
			parts.push(`${hairLength} ${this.slave.hColor} hair ${styleStr}`);
		} else if (this.helper.isXLBased()) {
			parts.push(`${hairLength} ${this.slave.hStyle} hair, ${this.slave.hColor} hair`);
		} else {
			parts.push(`${this.slave.hStyle} hair, ${hairLength} ${this.slave.hColor} hair`);
		}

		return parts.join(`, `);
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.hStyle === "bald" || this.slave.hStyle === "shaved" || this.slave.hLength === 0) {
			return `hair, long hair, short hair`;
		}
		return;
	}

	/**
	 * @override
	 */
	face() {
		return this.positive();
	}
};


code:
App.Art.GenAI.HealthPromptPart = class HealthPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (asSlave(this.slave)?.fuckdoll > 0) {
			return undefined;
		}
		if (this.slave.health.condition < -90) {
			return `(very sick, ill:1.1)`;
		} else if (this.slave.health.condition < -50) {
			return `sick, ill`;
		} else if (this.slave.health.condition < -10) {
			return `tired`;
		} else if (this.slave.health.condition < 90) {
			return null;
		} else {
			return `healthy`;
		}
	}

	/**
	 * @override
	 */
	negative() {
		if (asSlave(this.slave)?.fuckdoll > 0) {
			return undefined;
		}
		if (this.slave.health.condition > 50) {
			return `sick, ill`;
		}
		return;
	}
};


code:
App.Art.GenAI.HeightPromptPart = class HeightPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.slave.height < 150) {
			return `short`;
		} else if (this.slave.height > 180) {
			return `tall`;
		}
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.height < 150) {
			return `tall`;
		} else if (this.slave.height > 180) {
			return `short`;
		}
	}
};


code:
App.Art.GenAI.HipsPromptPart = class HipsPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.censored) {
			return undefined;
		}
		if (this.slave.hips <= -2) {
			return `(narrow hips:1.1)`;
		} else if (this.slave.hips === -1) {
			return `narrow hips`;
		} else if (this.slave.hips === 0) {
			return null;
		} else if (this.slave.hips === 1) {
			return `hips`;
		} else if (this.slave.hips === 2) {
			return `wide hips`;
		} else {
			return `(wide hips:1.1)`;
		}
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.hips <= -2) {
			return `wide hips`;
		} else if (this.slave.hips <= 1) {
			return null;
		} else {
			return `narrow hips`;
		}
	}
};


code:
App.Art.GenAI.HugeFakeTitsPromptPart = class HugeFakeTitsPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.censored){
			return undefined;
		} else if (
			this.helper.hasLora("hugefaketits1") &&
			this.slave.boobsImplant !== 0
		) {
			const ImplantPercentage = this.slave.boobsImplant / (this.slave.boobs - this.slave.boobsImplant);
			if (ImplantPercentage > 0.2) {
				return this.helper.lora("hugefaketits1", Math.clamp(ImplantPercentage / 2, 0.1, 0.5));
			}
		}
		return undefined;
	}

	/**
	 * @override
	 */
	negative() {
		return undefined;
	}
};


code:
App.Art.GenAI.LocationPromptPart = class LocationPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @returns {string|undefined}
	 */
	positive() {
		if (V.aiLocationBackgrounds === 0) { return undefined; }
		const slave = asSlave(this.slave);
		const parts = [];
		let style;
		const societyStyles = {
			FSRomanRevivalist: "marble columns, mosaic tile flooring, frescoed walls",
			FSNeoImperialist: "black marble floor, black marble walls",
			FSEgyptianRevivalist: "sandstone columns, hieroglyphic wall carvings, striped stone flooring, gold-trimmed decor",
			FSEdoRevivalist: "shoji screen walls, tatami mat flooring, wooden beams, minimalist decor",
			FSArabianRevivalist: "mashrabiya screens, arched doorways, mosaic tile walls, ornate lantern sconces",
			FSChineseRevivalist: "wooden lattice screens, carved red columns, curved roof beams",
			FSAztecRevivalist: "aztec architecture",
			FSAntebellumRevivalist: "antebellum architecture, crystal chandeliers, patterned wallpaper",
		  };

		for (const [id, styleName] of Object.entries(societyStyles)) {
			if (FutureSocieties.isActive(id)) {
			  style = styleName;
			  break; // stop at the first active one
			}
		  }
		if (V.aiLocationBackgrounds === 1) {
			switch (slave?.assignment) {
				case undefined:
					parts.push(style);
					parts.push("hallway");
					break;
				case Job.ARCADE:
				case Job.GLORYHOLE:
					parts.push(style);
					parts.push("dark sterile hallway, neon lights");
					break;
				case Job.ARENA: // training
				case Job.PIT: // fighting
					parts.push("boxing ring, wwe arena");
					break;
				case Job.BROTHEL:
					parts.push(style);
					parts.push("red upscale interior, red room, red light, bed, dark atmosphere");
					break;
				case Job.MADAM:
					parts.push(style);
					parts.push("red upscale interior, red room, red light, dark atmosphere, gold decor, night, grand staircase, chandelier");
					break;
				case Job.CELLBLOCK:
				case Job.CONFINEMENT:
					parts.push("prison cell, prison bars, dark walls");
					break;
				case Job.WARDEN:
					parts.push("dark room, high tech command room, computer screens, dark surrounding, prison bars");
					break;
				case Job.CLASSES:
				case Job.SCHOOL:
				case Job.TEACHER:
					parts.push(style);
					parts.push("school room, chalkboard background, school desk");
					break;
				case Job.CLINIC:
				case Job.NURSE:
					parts.push("hospital, examination room, white tiled wall, clinic");
					break;
				case Job.CLUB:
					parts.push(style);
					parts.push("dance club, dance floor, flashing neon lights, fog, dancing crowd");
					break;
				case Job.DJ:
					parts.push(style);
					parts.push("dance club, dance floor, flashing neon lights, fog, raised stage, dj desk, headphones, mixing music");
					break;
				case Job.WHORE:
				case Job.PUBLIC:
					parts.push(style);
					parts.push("bustling street, walking crowd, outdoor");
					break;
				case Job.CONCUBINE:
				case Job.MASTERSUITE:
				case Job.FUCKTOY:
				case Job.BODYGUARD:
					parts.push(style);
					parts.push("upscale interior, luxurious apartment, bed");
					break;
				case Job.MILKED:
				case Job.DAIRY:
				case Job.MILKMAID:
					parts.push("dairy, milking stalls, milk tanks");
					break;
				case Job.HEADGIRL:
				case Job.HEADGIRLSUITE:
					parts.push(style);
					parts.push("office, desk, computer monitor, rustic interior, office chair, dark wood");
					break;
				case Job.HOUSE:
				case Job.QUARTER:
				case Job.STEWARD:
					parts.push(style);
					parts.push("modern kitchen, kitchen, dark wood");
					break;
				case Job.SPA:
				case Job.ATTENDANT:
					parts.push("spa, stone floor, wood walls, indoor pool, indoor waterfall, fog");
					break;
				case Job.REST:
					parts.push("bedroom");
					switch (slave.rules.living) {
						case "normal":
							// nothing to add
							break;
						case "spare":
							parts.push("empty room, sparse room");
							break;
						case "luxurious":
							parts.push("luxurious room");
							break;
					}
					break;
				case Job.RECRUITER:
					parts.push("coffee shop, clipboard");
					break;
				case Job.AGENT:
				case Job.AGENTPARTNER:
					parts.push("office, clipboard, big windows");
					break;
				case Job.FARMER:
				case Job.FARMYARD:
					parts.push("outdoor, farm");
					break;
				case Job.MATRON: // head nursery nanny
				case Job.NURSERY: // nanny
					break; // I wouldn't worry about this location at the moment since the nursery is overdue for a rework - franklygeorge
				case Job.LURCHER:
					parts.push("outdoor, trail, forest");
					break;
				case Job.TANK:
					if (!this.helper.isXLBased()) { // while SD1.X seems to have a reasonable understanding of this concept. XL+ doesn't or I am using the wrong prompting - franklygeorge
						parts.push("cloning tank"); // TODO: this prompt could use more work. Could also use a LoRA to solidify its understanding. - franklygeorge
					}
					break;
				case Job.SUBORDINATE: // serving another slave. doesn't really have a location, though you could probably grab the location of their master using `getHuman(slave.subTarget)` though that would require restructuring this - franklygeorge
					break;
			}
		} else {
			switch (slave.clothes){
				case "a ball gown":
				case "a courtesan dress":
				case "a halter top dress":
				case "a mini dress":
				case "a slave gown":
				case "a gothic lolita dress":
				case "an evening dress":
				case "a maternity dress":
					return "ballroom, marble floor, dancing crowd";
				case "a bimbo outfit":
				case "a bunny outfit":
				case "clubslut netting":
				case "a slutty outfit":
				case "a tube top":
				case "a tube top and thong":
				case "leather pants":
				case "leather pants and a tube top":
				case "leather pants and pasties":
				case "no clothing":
					return this.randomizer("club");
				case "a biyelgee costume":
				case "a dirndl":
				case "a klan robe":
				case "lederhosen":
				case "Western clothing":
				case "a confederate army uniform":
					return "outdoor, folk festival, dancing crowd";
				case "a bra":
				case "a button-up shirt":
				case "a button-up shirt and panties":
				case "a skimpy loincloth":
				case "a sports bra":
				case "a striped bra":
				case "a sweater":
				case "a sweater and cutoffs":
				case "a sweater and panties":
				case "a t-shirt":
				case "a t-shirt and jeans":
				case "a t-shirt and panties":
				case "a t-shirt and thong":
				case "a tank-top":
				case "a tank-top and panties":
				case "a thong":
				case "an oversized t-shirt":
				case "an oversized t-shirt and boyshorts":
				case "attractive lingerie":
				case "attractive lingerie for a pregnant woman":
				case "boyshorts":
				case "choosing her own clothes":
				case "cutoffs":
				case "jeans":
				case "kitty lingerie":
				case "cutoffs and a t-shirt":
				case "panties":
				case "panties and pasties":
				case "pasties":
				case "striped panties":
				case "striped underwear":
					return this.randomizer("penthouse");
				case "a burkini":
				case "a monokini":
				case "a one-piece swimsuit":
				case "a scalemail bikini":
				case "a string bikini":
					return this.randomizer("bathing");
				case "a chattel habit":
				case "a fallen nuns habit":
				case "a penitent nuns habit":
				case "a succubus outfit":
					return "church, altar, big windows";
				case "a burqa":
				case "a hijab and abaya":
				case "a hijab and blouse":
				case "a niqab and abaya":
					return "mosque";
				case "a cheerleader outfit":
				case "spats and a tank top":
				case "sport shorts":
				case "sport shorts and a sports bra":
				case "sport shorts and a t-shirt":
				case "stretch pants and a crop-top":
					return "stadium, football field, cheering crowd";
				case "a comfortable bodysuit":
				case "a cybersuit":
				case "a Fuckdoll suit":
				case "a latex catsuit":
					return "cyberpunk street, neon light, night";
				case "a hanbok":
				case "a huipil":
				case "a kimono":
				case "a long qipao":
				case "a slutty qipao":
				case "a toga":
					return "hotel lobby, traditional architecture";
				case "a leotard":
					return "dance studio, big mirror wall";
				case "a military uniform":
				case "conservative clothing":
				case "nice business attire":
				case "slutty business attire":
					return "office, rustic interior, office chair, office desk, bookshelf";
				case "a nice maid outfit":
				case "a slutty maid outfit":
				case "an apron":
					return "modern kitchen, kitchen";
				case "a nice nurse outfit":
				case "a slutty nurse outfit":
					return "hospital, clinice, examination room";
				case "a police uniform":
					return "street, night, police cruiser, flashing lights";
				case "a schoolgirl outfit":
					return "school room, chalkboard background, school desk";
				case "battlearmor":
				case "Imperial Plate":
				case "a tight Imperial bodysuit":
				case "battledress":
					return "outdoor, airfield, fighter jet";
				case "harem gauze":
				case "slutty jewelry":
					return "arabian room, silk curtains, sandstone";
				case "restrictive latex":
				case "shibari ropes":
				case "uncomfortable straps":
					return "prison cell, prison bars, dark walls";
				/* case "a mounty outfit":
				case "a nice pony outfit":
				case "a red army uniform":
				case "a Santa dress":
				case "a schutzstaffel uniform":
				case "a slutty klan robe":
				case "a slutty pony outfit":
				case "a slutty schutzstaffel uniform":
				case "body oil":
				case "chains":
				case "overalls":
				 */
				default:
					return "hallway";
			}
		}
		return parts.join(", ");
	}

	/**
	 * @returns {string|undefined}
	 * */
	negative() {
		if (!V.aiLocationBackgrounds) { return undefined; }
		if (this.positive().includes("outdoor")) {
			return "bedroom, bed";
		}
	}

	/**
	 * @param {"bathing" | "club" | "penthouse"} type
	 * @returns {string}
	 */
	randomizer(type) {
		let livingString = "apartment";
		switch (this.slave.rules.living) {
			case "spare":
				livingString = "empty apartment, sparse apartment";
				break;
			case "luxurious":
				livingString = "luxurious apartment";
				break;
		}
		const data = {
			bathing: [
				"wet skin, beach, palms, sunset",
				"wet skin, tropical setting, corals",
				"wet skin, skyscraper, city skyline, jacuzzi, partially under water",
				"wet skin, indoor pool",
				"wet skin, pool mediterranean mansion",
				"tropical waterfall, resort, wood floor, sunset",
				"beach, sand, sun lounger",
			],
			club: [
				"strip club, dance floor",
				"grungy bar, counter, beverages",
				"vintage bar, counter, beverages",
				"rooftop bar, counter, beverages",
				"beach bar, sand, sunset, counter, beverages",
			],
			penthouse: [
				`${livingString}, vintage interior`,
				`${livingString}, vintage interior, bed`,
				`${livingString}, modern interior`,
				`${livingString}, modern interior, bed`,
				`${livingString}, futuristic interior, neon glow`,
				`${livingString}, futuristic interior, neon glow, bed`,
			],
		};
		const items = data[type];

		let randomIndex = Math.floor(seededRandom(this.slave.natural.artSeed - this.slave.clothes.length) * items.length);
		return items[randomIndex];
	}
};


code:
App.Art.GenAI.MusclesPromptPart = class MusclesPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.helper.isXLOrPony()) {
			if (this.slave.muscles > 95) {
				return 'huge muscles';
			} else if (this.slave.muscles > 50) {
				return 'muscles';
			} else if (this.slave.muscles > 30) {
				return 'toned';
			}
		}
		if (this.slave.muscles > 95) {
			return `(very muscular:1.3), bodybuilder`;
		} else if (this.slave.muscles > 50) {
			return `(muscular:1.3)`;
		} else if (this.slave.muscles > 30) {
			return `(muscular:1.2)`;
		} else if (this.slave.muscles > 10) {
			return `muscular`;
		} else if (this.slave.muscles > -10) {
			return null;
		} else if (this.slave.muscles > -95) {
			return `soft`;
		} else {
			return `frail, weak`;
		}
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.muscles < -5) {
			return `muscular`;
		}
	}
};


code:
/* map some tiny nationalities that AI models haven't been trained on to a larger neighbor or region with similar-looking people */
const microstateFix = {
	// European Microstates
	"a Liechtensteiner": "Swiss",
	"Andorran": "Catalan",
	"Luxembourgian": "Belgian",
	// "Monégasque": "French", // Monégasque works fine in testing, strangely
	"Sammarinese": "Italian",
	"Vatican": "Italian",

	// Caribbean islands
	"Antiguan": "Caribbean",
	"Aruban": "Caribbean",
	"Curaçaoan": "Caribbean",
	"Dominiquais": "Caribbean",
	"Grenadian": "Caribbean",
	"Kittitian": "Caribbean",
	"Saint Lucian": "Caribbean",
	"Vincentian": "Caribbean",

	// Oceania - micronesian nations and major tourist destinations all seem to do ok as-is, so just a couple to map
	"French Polynesian": "Polynesian",
	"Niuean": "Polynesian",
	"a Cook Islander": "Polynesian",
};

App.Art.GenAI.NationalityPromptPart = class NationalityPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		const slave = asSlave(this.slave);
		if (["Stateless", "none", "slave", ""].includes(this.slave.nationality) || slave?.fuckdoll > 0) {
			return;
		}
		if (this.slave.nationality.endsWith("Revivalist")) {
			return;
		}
		const nationalityPrompt = microstateFix[this.slave.nationality] || this.slave.nationality;
		switch (V.aiNationality) {
			case 0: return; // disabled
			case 1: return `[${nationalityPrompt}]`; // weak
			case 2: return nationalityPrompt; // strong;
			default: throw new Error(`Unexpected value for aiNationality: ${V.aiNationality}` );
		}
	}

	/**
	 * @override
	 */
	negative() {
		return undefined;
	}

	/**
	 * @override
	 */
	face() {
		return this.positive();
	}
};


code:
App.Art.GenAI.PiercingsPromptPart = class PiercingsPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		const isFuckdoll = asSlave(this.slave)?.fuckdoll !== 0;

		let piercingParts = [];
		if (this.slave.piercing.areola.weight > 0 && !this.censored && this.helper.exposesNipples(this.slave.clothes)) {
			if (!isFuckdoll || this.slave.race === "catgirl") { // TODO: needs exposure check
				let desc = this.slave.piercing.areola.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.areola.desc) + ` `) : ``;
				piercingParts.push(`${desc}areola piercing`);
			}
		}
		if (this.slave.piercing.ear.weight > 0) {
			if (!isFuckdoll || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur
				let desc = this.slave.piercing.ear.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.ear.desc) + ` `) : ``;
				piercingParts.push(`${desc}earrings`);
			}
		}
		if (this.slave.piercing.eyebrow.weight > 0) {
			if (!isFuckdoll || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur
				let desc = this.slave.piercing.eyebrow.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.eyebrow.desc) + ` `) : ``;
				piercingParts.push(`${desc}eyebrow piercing`);
			}
		}
		if (this.slave.piercing.lips.weight > 0) {
			let desc = this.slave.piercing.lips.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.lips.desc) + ` `) : ``;
			piercingParts.push(`${desc}lip piercing`);
		}
		if (this.slave.piercing.navel.weight > 0 && !this.censored && this.helper.exposesMidriff(this.slave.clothes)) {
			if (!isFuckdoll || this.slave.race === "catgirl") { // covered by fuckdoll suit or fur
				let desc = this.slave.piercing.navel.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.navel.desc) + ` `) : ``;
				piercingParts.push(`${desc}navel piercing`);
			}
		}
		if (this.slave.piercing.nipple.weight > 0 && !this.censored && this.helper.exposesNipples(this.slave.clothes)) {
			if (!isFuckdoll) { // TODO: needs exposure check
				let desc = this.slave.piercing.nipple.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.nipple.desc) + ` `) : ``;
				piercingParts.push(`${desc}nipple piercing`);
			}
		}
		if (this.slave.piercing.nose.weight > 0) {
			if (!isFuckdoll || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur
				let desc = this.slave.piercing.nose.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.nose.desc) + ` `) : ``;
				piercingParts.push(`${desc}nose piercing`);
			}
		}
		if (this.slave.piercing.tongue.weight > 0) {
			let desc = this.slave.piercing.tongue.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.tongue.desc) + ` `) : ``;
			piercingParts.push(`${desc}tongue piercing`);
		}
		if (this.slave.piercing.vagina.weight > 0 && this.slave.dick <= 0 && !this.censored && this.helper.exposesCrotch(this.slave.clothes)) {
			let desc = this.slave.piercing.vagina.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.vagina.desc) + ` `) : ``;
			piercingParts.push(`${desc}labia piercing`);
		}

		if (piercingParts.length > 0) {
			return piercingParts.join(`, `);
		}
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.piercing.areola.weight === 0 && this.slave.piercing.ear.weight === 0 && this.slave.piercing.eyebrow.weight === 0 && this.slave.piercing.genitals.weight === 0 && this.slave.piercing.lips.weight === 0 && this.slave.piercing.navel.weight === 0 && this.slave.piercing.nipple.weight === 0 && this.slave.piercing.nose.weight === 0 && this.slave.piercing.tongue.weight === 0 && this.slave.piercing.vagina.weight === 0) {
			return `piercings`;
		}
	}
};


code:
App.Art.GenAI.PosturePromptPart = class PosturePromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		const slave = asSlave(this.slave);
		const customPrompt = slave?.custom?.aiPrompts?.pose;
		if (customPrompt) {
			return customPrompt;
		}

		const parts = [];

		if (isAmputee(this.slave)) {
			parts.push(`sitting in chair`); // posture change prevents genning arms/legs, looks more natural
		} else if (slave?.fuckdoll !== 0) {
			if (this.helper.hasLora("Standing Straight v1 - locon 32dim") && !V.aiOpenPose) { // always prefer OpenPose over lora; less side effects
				parts.push(this.helper.lora("Standing Straight v1 - locon 32dim", 1));
			}
			parts.push(`standing straight`);
		} else if (isQuadrupedal(this.slave)) {
			parts.push(`on all fours`);
		} else if (canStand(this.slave)) {
			parts.push(`standing`);
		} else {
			parts.push(`kneeling`);
		}

		if (!isAmputee(this.slave) && !isQuadrupedal(this.slave)) { // no arms pose for amputees and quadrupeds
			if (this.slave.devotion < -50) {
				parts.push(`from side, arms crossed`);
			} else if (this.slave.devotion < -20) {
				parts.push(`arms crossed`);
			} else if (this.slave.devotion < 21) {
				// parts.push(`standing`);
			} else {
				parts.push(`arms behind back, from front`);
			}
		}

		if (slave?.fuckdoll !== 0) {
			// trustPart = ``;
		} else if (slave?.trust < -50) {
			parts.push(`trembling, head down`);
		} else if (slave?.trust < -20) {
			parts.push(`trembling`);
		}

		return parts.join(`, `);
	}

	/**
	 * @override
	 */
	negative() {
		if (asSlave(this.slave)?.custom?.aiPrompts?.pose) {
			return undefined;
		}

		if (isAmputee(this.slave) || !canWalk(this.slave)) {
			return undefined;
		} else {
			return "from above";
		}
	}
};


code:
App.Art.GenAI.PregPromptPart = class PregPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.censored) {
			return undefined;
		}

		if (this.slave.belly >= 10000) {
			return "pregnant, full term";
		} else if (this.slave.belly >= 5000) {
			return "[pregnant:0.3]";
		} else if (this.slave.belly >= 1500) {
			return "[pregnant:0.5]";
		} else if (this.slave.belly >= 100) {
			return "bloated, [pregnant:0.8]";
		}
	}

	/**
	 * @override
	 */
	negative() {
		return undefined;
	}
};


code:
App.Art.GenAI.PubicHairPromptPart = class PubicHairPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		const exposesCrotch = this.helper.exposesCrotch(this.slave.clothes);
		if (!exposesCrotch || this.censored) {
			return undefined;
		}

		if (this.slave.pubicHStyle === "waxed" || this.slave.pubicHStyle === "bald" || this.slave.pubicHStyle === "hairless" || this.slave.physicalAge < Math.min(this.slave.pubertyAgeXX, this.slave.pubertyAgeXY)) {
			return;
		}
		if ((asSlave(this.slave)?.fuckdoll > 0)) {
			return; // pubic region should be covered by clothes
		}
		const style = (this.slave.pubicHStyle === "bushy in the front and neat in the rear" ? "bushy" : this.slave.pubicHStyle); // less complicated prompt works better for the long style
		return `${this.slave.pubicHColor} ${style} pubic hair`;
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.pubicHStyle === "waxed" || this.slave.pubicHStyle === "bald" || this.slave.pubicHStyle === "hairless" || this.slave.physicalAge < Math.min(this.slave.pubertyAgeXX, this.slave.pubertyAgeXY)) {
			return "pubic hair";
		}
		return;
	}
};


code:
App.Art.GenAI.RacePromptPart = class RacePromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.slave.race === "white") {
			return "caucasian";
		} else if (this.slave.race === "black") {
			return "african";
		} else if (this.slave.race === "catgirl") {
			return undefined; // catgirl/catboy race is covered by gender prompt (SD 1.X)/demographics prompt
		}
		return this.slave.race;
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.race !== "asian" && this.slave.race !== "catgirl") {
			return "asian";
		}
		return;
	}

	/**
	 * @override
	 */
	face() {
		return undefined;
	}
};


code:
App.Art.GenAI.SkinPromptPart = class SkinPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		let skinDesc = "";

		// Albinism takes full priority
		if (this.slave.geneticQuirks.albinism === 2) {
			skinDesc = "albino";
		// Furry races override standard skin
		} else if (this.slave.race === "catgirl") {
			skinDesc = `covered in ${this.slave.skin} fur`;
		// Otherwise, determine based on skin tone
		} else {
			switch (this.slave.skin) {
				case "pure white":
				case "ivory":
				case "white":
				case "extremely fair":
					skinDesc = "white skin";
					break;
				case "extremely pale":
				case "very pale":
				case "pale":
				case "light beige":
				case "very fair":
					skinDesc = "pale skin";
					break;
				case "fair":
				case "light":
				case "beige":
					skinDesc = "fair skin";
					break;
				case "light olive":
				case "sun tanned":
				case "spray tanned":
				case "tan":
					skinDesc = "tan skin";
					break;
				case "olive":
				case "bronze":
				case "dark beige":
					skinDesc = this.helper.isXLOrPony() ? "olive skin" : "tan skin";
					break;
				case "dark olive":
				case "light brown":
				case "brown":
					skinDesc = this.helper.isXLBased() ? "brown skin" : "tan skin";
					break;
				case "dark":
				case "dark brown":
				case "black":
				case "ebony":
					skinDesc = this.helper.lora("melanin", 0.8, ", melanin, ") + "dark skin";
					break;
				case "pure black":
					skinDesc = this.helper.lora("melanin", 1, ", melanin, ") + "black skin";
					break;
				default:
					skinDesc = `${this.slave.skin} skin`;
			}
		}

		// ─── MARKINGS ───
		const marking = this.slave.markings?.toLowerCase?.();
		if (marking && marking !== "none") {
			if (marking === "beauty mark") {
				skinDesc += ", a small beauty mark";
			} else if (marking === "birthmark") {
				skinDesc += ", a subtle birthmark";
			} else if (marking === "freckles") {
				skinDesc += ", light freckles across her skin";
			} else if (marking === "heavily freckled") {
				skinDesc += ", heavily freckled skin";
			} else {
				skinDesc += `, with body markings: ${marking}`;
			}
		}

		return skinDesc;
	}

	/**
	 * @override
	 */
	negative() {
		if (this.helper.isXLBased()) {
			if (this.positive()?.includes("tan skin")) {
				return "tan lines";
			}
			return;
		}
		switch (this.slave.skin) {
			case "pure white":
			case "ivory":
			case "white":
			case "extremely pale":
			case "very pale":
			case "pale":
			case "extremely fair":
			case "very fair":
			case "fair":
			case "light":
			case "light olive":
				return "dark skin";
			case "sun tanned":
			case "spray tanned":
			case "tan":
			case "olive":
				return "black skin";
			case "bronze":
			case "dark olive":
			case "dark":
			case "light beige":
			case "beige":
			case "dark beige":
			case "light brown":
			case "brown":
			case "dark brown":
			case "black":
			case "ebony":
			case "pure black":
				return "light skin";
		}
	}

	/**
	 * @override
	 */
	face() {
		return this.positive();
	}
};


code:
// cSpell: ignore derpibooru

App.Art.GenAI.StylePromptPart = class StylePromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		let positive = "";
		if (this.helper.isPony()) {
			// this is a negative LoRA but it has to go in the positive prompt to be loaded
			positive += this.helper.lora("badanatomy_SDXL_negative_LORA_AutismMix_v1", -0.2, " ");
		}
		// Check if filter is on and needed, then pick model-appropriate framing tags.
		if (this.censored) {
			if (this.helper.isPony()) {
				positive += "front view, half-length portrait, ";
			} else {
				positive += "straight-on, cowboy shot, ";
			}
			positive += "face focus, ";
		} else {
			if (this.helper.isPony()) {
				positive += "full-length portrait, ";
			} else {
				positive += "full body, portrait, ";
			}
		}
		switch (V.aiStyle) {
			case 0: //	Custom Style
				return V.aiCustomStylePos;
			case 1: // Photorealistic
				if (this.helper.isPony()) {
					positive += "score_9, score_8_up, score_7_up, ";
				}
				return positive + "photorealistic, dark theme, black background";
			case 2: //	Anime/Hentai
				if (this.helper.isXL()) {
					return positive + "masterpiece, best quality"; // SDXL gets flat when asked for 2d or anime, and gets old looking when asked for hentai
				} else if (this.helper.isPony()) {
					return positive + "score_9, score_8_up, score_7_up, source_anime, source_cartoon"; // Pony does better with scoring; `source_anime, source_cartoon` is much better than `2d, anime, hentai`
				}
				return positive + "2d, anime, hentai";
			case 3: // No extra styling
				return positive;
		}
	}

	/**
	 * @override
	 */
	negative() {
		let negative = "";
		if (V.aiStyle !== 0) {
			negative += "text, watermark, low quality, censored, mosaic_censoring, bar_censor, lowres, bad anatomy, bad hands, ";
			if (this.helper.isPony()) {
				negative += "score_4, score_5, score_6, source_pony, derpibooru_p_low, ";
			}
		}
		if (this.censored) {
			negative += "NSFW, nude, ";
			if (this.helper.isPony()) {
				negative += "full-length portrait, ";
			} else {
				negative += "full body, ";
			}
		} else {
			negative += "close-up, ";
			if (this.helper.isPony()) {
				negative += "half-length portrait, ";
			} else {
				negative += "cowboy shot, ";
			}
		}
		switch (V.aiStyle) {
			case 0: // Custom Style
				return negative + V.aiCustomStyleNeg;
			case 1: // Photorealistic
				if (this.helper.isPony()) {
					negative += "source_anime, source_cartoon, ";
				}
				return negative + "greyscale, monochrome, cg, render";
			case 2: // Anime/Hentai
				if (this.helper.isXL()) {
					negative += "worst quality, low quality, ";
				}
				return negative + "greyscale, monochrome, photography, 3d render, speech bubble";
			case 3: // None
				return negative;
		}
	}

	/**
	 * @override
	 */
	face() {
		switch (V.aiStyle) {
			case 0: //	Custom Style
				return V.aiCustomStylePos;
			case 1: // Photorealistic
				return "photorealistic, dark theme, black background";
			case 2: //	Anime/Hentai
				if (this.helper.isXL()) {
					return "masterpiece, best quality";
				} else if (this.helper.isPony()) {
					return "score_9, score_8_up, score_7_up, score_6_up, source_anime, source_cartoon";
				}
				return "2d, anime, hentai";
			case 3: // None
				return "";
		}
	}
};


code:
App.Art.GenAI.TattooPromptPart = class TattooPromptPart extends App.Art.GenAI.PromptPart {
	locations() {
		return {
			arm: !!this.slave.armsTat && this.helper.exposesArms(this.slave.clothes),
			leg: !!this.slave.legsTat && this.helper.exposesLegs(this.slave.clothes),
			shoulder: !!this.slave.shouldersTat && this.helper.exposesArms(this.slave.clothes),
			belly: !!this.slave.bellyTat && this.helper.exposesMidriff(this.slave.clothes),
			breast: !!this.slave.boobsTat && this.helper.exposesBreasts(this.slave.clothes),
			dick: !!this.slave.dickTat && this.slave.dick > 0 && this.helper.exposesCrotch(this.slave.clothes),
			vagina: !!this.slave.vaginaTat && this.slave.vagina > -1 && (this.helper.exposesMidriff(this.slave.clothes) || this.helper.exposesCrotch(this.slave.clothes)),
			lips: !!this.slave.lipsTat,
		};
	}

	/**
	 * @override
	 */
	positive() {
		const parts = [];
		const loc = this.locations();
		if (this.helper.isPony()) { // Most SDXL models don't work the best with tattoos, so they have also been excluded - franklygeorge
			// TODO: add tattoo descriptions back in where/when they work
			if (loc.arm) {
				parts.push('arm tattoo');
			}
			if (loc.leg) {
				parts.push('leg tattoo');
			}
			if (loc.shoulder) {
				parts.push('shoulder tattoo');
			}
			if (loc.belly) {
				parts.push('belly tattoo');
			}
			if (loc.breast) {
				parts.push('breast tattoo');
				switch (this.slave.boobsTat) {
					case "tribal patterns":
					case "flowers":
						parts.push('floral chest tattoo');
						break;
					case "scenes":
					case "Asian art":
					case "degradation":
					case "counting":
					case "advertisements":
					case "rude words":
					case "bovine patterns":
					case "sacrament":
					case "sacrilege":
					case "possessive":
					case "paternalist":
				}
			}
			// if (loc.dick) {
			// 	parts.push('dick tattoo');
			// }
			if (loc.vagina) {
				parts.push('womb tattoo');
			}
			// if (loc.lips) {
			// 	parts.push('lip tattoo');
			// }
		} else { // SD1.X and SDXL
			// SD1.X and SDXL models do really bad with tattoos - franklygeorge
			// Life Like is on or off with no sense of location and most of the others (SD1.X) just add weird artifacts - franklygeorge
			// So we don't generate tattoos for them at all - franklygeorge
			// If someone can figure out how to make them work reliably (LoRA maybe?) then be my guest - franklygeorge
			// https://civitai.com/models/139749/tattoo-sleeve (SD1.X) shows potential promise for arm tattoos - franklygeorge
			// https://civitai.com/models/355902/tattoos (SD1.X) shows promise in general if we can figure out how to isolate it to a single location - franklygeorge

			// if (loc.arm) {
			// 	parts.push('arm tattoo');
			// }
			// if (loc.leg) {
			// 	parts.push('leg tattoo');
			// }
			// if (loc.shoulder) {
			// 	parts.push('shoulder tattoo');
			// }
			// if (loc.belly) {
			// 	parts.push('belly tattoo');
			// }
			// if (loc.breast) {
			// 	parts.push('breast tattoo');
			// }
			// if (loc.dick) {
			// 	parts.push('dick tattoo');
			// }
			if (loc.vagina) {
				switch (this.slave.vaginaTat) {
					case "tribal patterns":
					case "flowers":
					case "scenes":
					case "Asian art":
					case "degradation":
					case "counting":
					case "advertisements":
					case "rude words":
					case "bovine patterns":
					case "sacrament":
					case "sacrilege":
					case "possessive":
					case "paternalist":
						break;
					case "lewd crest":
						if (this.helper.isXL()) {
							if (this.helper.hasLora("lewd_crest_crotch_tattoo_sdxl")) {
								// This doesn't work with most realistic models - franklygeorge
								parts.push(this.helper.lora("lewd_crest_crotch_tattoo_sdxl", 0.8, '(pubic tattoo)'));
							}
						} else if (this.helper.hasLora("womb_tattoo")) {
							// Note that this lora seems to work with all the SD1.X models I tested to some degree or another
							// except for Life Like where it doesn't work until you turn it up so high that it disorts everything else.
							// This is interesting since it works pretty well with Life Like if you remove all the rest of our prompting. - franklygeorge
							parts.push(this.helper.lora("womb_tattoo", 0.8, '(womb tattoo, pubic tattoo)'));
						}
						break;
				}
			}
			// if (loc.lips) {
			// 	parts.push('lip tattoo');
			// }
		}

		return parts.join(', ');
	}

	/**
	 * @override
	 */
	negative() {
		const parts = [];
		const loc = this.locations();
		if (this.positive().includes('tattoo')) { // only add negative prompting if it is actually needed
			if (!loc.arm) {
				parts.push('arm tattoo');
			}
			if (!loc.leg) {
				parts.push('leg tattoo');
			}
			if (!loc.shoulder) {
				parts.push('shoulder tattoo');
			}
			if (!loc.belly) {
				parts.push('belly tattoo');
			}
			if (!loc.breast) {
				parts.push('breast tattoo');
			}
			// if (!loc.dick) {

			// }
			if (!loc.vagina) {
				parts.push('womb tattoo');
			}
			// if (!loc.lips) {

			// }
		}
		return parts.join(', ');
	}
};


code:
App.Art.GenAI.WaistPromptPart = class WaistPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.censored) {
			return undefined;
		}
		if (this.slave.waist > 95) {
			return `very wide waist`;
		} else if (this.slave.waist > 10) {
			return `wide waist`;
		} else if (this.slave.waist > -40) {
			return null;
		} else if (this.slave.waist > -95) {
			return `narrow waist`;
		} else {
			return `(narrow waist:1.1)`;
		}
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.waist > 30) {
			return `narrow waist`;
		} else if (this.slave.waist > -30) {
			return null;
		} else if (this.slave.waist > -95) {
			return `wide waist`;
		}
	}
};


code:
App.Art.GenAI.WeightPromptPart = class WeightPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.helper.isXLOrPony()) {
			if (this.slave.weight < -95) {
				return 'skinny, emaciated';
			} else if (this.slave.weight < -30) {
				return 'skinny';
			} else if (this.slave.weight < -10) {
				return `slim`;
			} else if (this.slave.weight < 10) {
				return null;
			} else if (this.slave.weight < 30) {
				return `curvy`;
			} else if (this.slave.weight < 60) {
				return 'fat';
			} else if (this.slave.weight < 95) {
				return 'obese';
			} else {
				return 'fat, obese';
			}
		}
		if (this.slave.weight < -95) {
			return `emaciated, very thin, skinny`;
		} else if (this.slave.weight < -30) {
			return `very thin, skinny`;
		} else if (this.slave.weight < -10) {
			return `slim`;
		} else if (this.slave.weight < 10) {
			return null;
		} else if (this.slave.weight < 30) {
			return `curvy`;
		} else if (this.slave.weight < 95) {
			return `plump, chubby`;
		} else {
			return `fat, obese, plump`;
		}
	}

	/**
	 * @override
	 */
	negative() {
		const parts = [];
		if (this.helper.isXLOrPony()) {
			if (this.slave.weight < -30) {
				parts.push('fat');
			}
		}
		if (this.slave.weight < -30) {
			parts.push(`plump, chubby`);
		} else if (this.slave.weight < 50) {
			// do nothing
		} else {
			parts.push(`thin, skinny`);
		}
		return parts.join(`, `);
	}

	/**
	 * @override
	 */
	face() {
		return this.positive();
	}
};


code:
App.Art.GenAI.UI.QueueOverlay = function() {
	// Internal State
	let toggleVisible = true;
	let generating = false;

	// Setup containers
	const container = document.createElement("div");
	container.classList.add("ai-queue-overlay");
	document.body.append(container);

	const mainQueueSpan = document.createElement("span");
	const backlogSpan = document.createElement("span");

	// Export functionality
	return {init: init, toggle: toggle};

	function init() {
		// Setup content
		const spinnerSpan = document.createElement("span");
		spinnerSpan.classList.add("spinner");
		container.append(spinnerSpan);

		container.append("(Queue: ", mainQueueSpan, " / Backlog: ", backlogSpan, ") ");

		const button = document.createElement("button");
		button.append("\uf410");
		button.onclick = () => App.Art.GenAI.sdQueue.interrupt();
		container.append(button);

		// Callback
		App.Art.GenAI.sdQueue.registerStatusChangeCallback(queueChangeCallback);
	}

	/**
	 * @param {ArtQueueState} status
	 */
	function queueChangeCallback(status) {
		mainQueueSpan.textContent = String(status.mainQueueCount);
		backlogSpan.textContent = String(status.backlogCount);
		generating = status.active;
		updateVisible();
	}

	/**
	 * @param {boolean} visible
	 */
	function toggle(visible) {
		toggleVisible = visible;
		updateVisible();
	}

	function updateVisible() {
		const visible = V.aiQueueOverlay === 1 && toggleVisible && generating;
		if (visible) {
			container.classList.remove("hidden");
		} else {
			container.classList.add("hidden");
		}
	}
}();