code:
// cSpell:ignore xxmaskedxx, nopussy, flaccidfutanarimix, micropp
App.Art.GenAI.Ill.ClothesPromptPart = class ClothesPromptPart extends App.Art.GenAI.PromptPart {
	/** @type {Record<FC.Clothes, {positive: string, negative: string}>} */
	clothesPrompts = {
		"no clothing": {
			"positive": "nude",
			"negative": "",
		},
		"a Fuckdoll suit": {  // NG good gen requires LoRA, but below will work without LoRA as well
			"positive": "kneeling, latex bodysuit, bdsm, bondage, full body, bound, bound arms, bound legs, covered eyes, mask, anus cutout, pussy cutout, open mouth",
			"negative": "eyes, nipples, breasts",
		},
		"conservative clothing": {
			"positive": "black pants, white shirt",
			"negative": "nipples, pussy",
		},
		"chains": {
			"positive": "chained, chained wrists, ankle cuffs, wrist cuffs, handcuffs, own hands together, nude,",
			"negative": "handcuff dangle, clothes, jeans, underwear, pants, shorts, skirt, panties",
		},
		"Western clothing": {
			"positive": "cowboy hat, white shirt, tied shirt, jeans",
			"negative": "",
		},
		"body oil": {
			"positive": "nude, oiled, shiny skin, nipples, pussy",
			"negative": "",
		},
		"a toga": {
			"positive": "toga, linen clothing, gold belt, leather sandals",
			"negative": "nipples",
		},
		"a huipil": {  // Doesn't work well
			"positive": "woven wrap top,  colorfull short poncho, decorative sash, fringed mini skirt, layered bracelets, patterned textiles",
			"negative": "nipples",
		},
		"a slutty qipao": {
			"positive": "china dress, breast cutout, short dress",
			"negative": "nipples",
		},
		"a kimono": {
			"positive": "white kimono, long sleeves, long dress, sandals, white socks",
			"negative": "nipples, breasts, shoulder",
		},
		"spats and a tank top": {
			"positive": "bike shorts, tank top, spats \\(footwear\\)",
			"negative": "nipples, breasts",
		},
		"uncomfortable straps": {
			"positive": "nude, leather, harness, bondage",
			"negative": "",
		},
		"shibari ropes": {
			"positive": "nude, shibari, bondage, harness, thigh belt",
			"negative": "",
		},
		"restrictive latex": {
			"positive": "latex bodysuit, long sleeves, gloves, breasts out, breast cutout, nipples, shiny clothes",
			"negative": "",
		},
		"a latex catsuit": {  // Doesn't work well
			"positive": "catsuit, latex",
			"negative": "nipples, breast cutout, breasts",
		},
		"attractive lingerie": {
			"positive": "$color pants, g-string, lace trim, $color bra",
			"negative": "clothes, jeans, pants",
		},
		"attractive lingerie for a pregnant woman": {  // Cupless part doesn't work well
			"positive": "cotton thong, cotton bra, cotton stockings, cupless bra",
			"negative": "pussy",
		},
		"kitty lingerie": {  // Broken for photorealistic models, probably works for anime models
			"positive": "kitty lingerie, cat lingerie, kawaii lingerie",
			"negative": "nude, pussy, nipples",
		},
		"a maternity dress": {
			"positive": "maternity dress, (short dress)",
			"negative": "nipples, breasts, breast cutout, stomach cutout, pussy, long dress",
		},
		"stretch pants and a crop-top": {
			"positive": "crop top, leggings",
			"negative": "nipples",
		},
		"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 nipple bikini": {
			"positive": "string bikini, micro bikini, $color bikini",
			"negative": "nipples",
		},
		"a string bikini": {  // Cupless part doesn't work well
			"positive": "cupless bikini, string bikini, crotchless bikini, $color bikini, nipples, pussy",
			"negative": "",
		},
		"a scalemail bikini": {  // Doesn't work well
			"positive": "bikini armor, scale armor",
			"negative": "nipples",
		},
		"striped panties": {
			"positive": "blue striped panties, bare breasts, topless",
			"negative": "",
		},
		"a cheerleader outfit": {
			"positive": "$color clothes, cheerleader, skirt, crop top, school emblem",
			"negative": "(pom pom)",
		},
		"clubslut netting": {
			"positive": "see-through clothes, see-through cleavage, $color bodysuit, fishnet bodysuit, crotchless, fishnet gloves, fishnets",
			"negative": "",
		},
		"cutoffs and a t-shirt": {
			"positive": "t-shirt, white shirt, short shorts, short jeans",
			"negative": "",
		},
		"slutty business attire": {
			"positive": "white blazer, white skirt, pencil skirt, microskirt, panties, black panties",
			"negative": ""
		},
		"nice business attire": {
			"positive": "black blazer, black skirt, medium skirt, pencil skirt",
			"negative": "nipples, (cleavage)",
		},
		"a ball gown": {
			"positive": "$color dress, $material dress, gown, long gloves, long skirt, strapless",
			"negative": "",
		},
		"a slave gown": {
			"positive": "transparent lace dress, transparent clothing, anklet, long skirt, strapless",
			"negative": "",
		},
		"a halter top dress": {
			"positive": "$color $material dress, halter dress, luxurious dress, backless dress, long dress",
			"negative": "jeans, nude, pussy, nipples",
		},
		"an evening dress": {
			"positive": "$color $material evening gown, (medium dress)",
			"negative": "",
		},
		"a mini dress": {
			"positive": "$color $material short dress, strapless",
			"negative": "",
		},
		"a comfortable bodysuit": {
			"positive": "$colorLatex latex bodysuit, bare shoulders, long sleeves",
			"negative": "",
		},
		"a leotard": {
			"positive": "strapless leotard, $color leotard",
			"negative": "",
		},
		"a monokini": {
			"positive": `$color monokini, breasts out, ${V.aiAgeFilter ? "" : ", nipples"}`,
			"negative": "",
		},
		"an apron": {
			"positive": "naked apron, white apron, sideboob",
			"negative": "",
		},
		"overalls": {
			"positive": "naked overalls",
			"negative": "",
		},
		"a cybersuit": {
			"positive": "cyberpunk, catsuit, $colorLatex bodysuit, full body",
			"negative": "",
		},
		"a tight Imperial bodysuit": {
			"positive": "cyberpunk, catsuit, utility belt, bracer, armored bodysuit, pauldrons",
			"negative": "bare legs, bare arms, bare shoulders, nude, pussy, nipples",
		},
		"battlearmor": {
			"positive": "armor, chest armor, leg armor, arm armor, hip armor, pauldrons, armored gloves, armored boots, high collar, utility belt",
			"negative": "",
		},
		"Imperial Plate": {  // Doesn't work well
			"positive": "roman armor, armor, chest armor, leg armor, arm armor, hip armor, pauldrons, armored gloves, armored boots, high collar, utility belt",
			"negative": "",
		},
		"a bunny outfit": {
			"positive": "playboy bunny, rabbit ears, hairband, fishnets",
			"negative": "",
		},
		"a slutty maid outfit": {
			"positive": "maid apron, maid headdress, maid, microdress, white panties, cleavage cutout",
			"negative": "",
		},
		"a nice maid outfit": {
			"positive": "maid apron, maid headdress, maid, square neckline, long dress, dress lift",
			"negative": "",
		},
		"a slutty nurse outfit": {
			"positive": "nurse, nurse cap, microskirt, white latex dress, panties, cleavage cutout",
			"negative": "",
		},
		"a nice nurse outfit": {
			"positive": "nurse, nurse cap, medical scrubs, pants",
			"negative": "nipples",
		},
		"a dirndl": {
			"positive": "dirndl, frilled dress, floral print, corset",
			"negative": "nipples",
		},
		"a long qipao": {
			"positive": "china dress, long dress",
			"negative": "nipples",
		},
		"lederhosen": {
			"positive": "brown suede lederhosen, embroidery, suspenders, puffy sleeves, white sleeves, square neckline",
			"negative": "nipples",
		},
		"a biyelgee costume": {  // Doesn't work well
			"positive": "biyelgee costume, turtleneck, medium dress, richly embroidered silk, wide decorative belt, long sleeves with ornate cuffs, intricate patterns, traditional headdress with beadwork and fabric accents",
			"negative": "nipples",
		},
		"a hanbok": {
			"positive": "hanbok, jeogori \\(clothes\\), white cuffs, floral embroidery, wide ribbon, tied neatly at the front, white beoseon",
			"negative": "nipples",
		},
		"a burkini": { // Doesn't work well
			"positive": "black burqa, burkini",
			"negative": "hair, nipples",
		},
		"a hijab and blouse": {
			"positive": "hijab, blouse, short sleeves, long skirt",
			"negative": "nipples",
		},
		"a hijab and abaya": {
			"positive": "hijab, abaya",
			"negative": "nipples",
		},
		"a niqab and abaya": {  // Doesn't work well
			"positive": "niqab, covered face, abaya, long dress, black dress",
			"negative": "nipples",
		},
		"a burqa": {  // Doesn't work well
			"positive": "burqa, muslim clothes, mouth mask, long dress, black dress",
			"negative": "",
		},
		"a police uniform": {
			"positive": "police uniform, policewoman, police hat, jacket, pants, utility belt, holster",
			"negative": "",
		},
		"a gothic lolita dress": {
			"positive": "lolita fashion, medium dress, black dress, frilled dress, frilled sleeves, square neckline",
			"negative": "",
		},
		"a one-piece swimsuit": {
			"positive": "one-piece swimsuit, high collar",
			"negative": "",
		},
		"a nice pony outfit": {
			"positive": "bdsm, bodysuit, horse mask, face mask",
			"negative": "nude"
		},
		"a slutty pony outfit": {
			"positive": "bdsm, bodysuit, horse mask, breasts out",
			"negative": "pussy",
		},
		"a button-up shirt and panties": {
			"positive": "collared shirt, loose clothes, oversized clothes, panties, no pants",
			"negative": "",
		},
		"a button-up shirt": {
			"positive": "collared shirt, loose clothes, oversized clothes, no panties, no pants, bottomless, pussy",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a sweater": {
			"positive": "sweater, oversized clothes, bottomless, no panties, no pants, pussy",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a t-shirt": {
			"positive": "t-shirt, oversized clothes, bottomless, no panties, no pants, pussy",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a tank-top": {
			"positive": "tank top, bottomless, no panties, no pants, pussy",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a tube top": {
			"positive": "tube top, bottomless, no panties, no pants, pussy",
			"negative": "jeans, pants, skirt, shorts, nude, nipples",
		},
		"an oversized t-shirt": {
			"positive": "t-shirt, (oversized clothes), bottomless, no panties, no pants, pussy",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a bra": {
			"positive": "bra, bottomless,(no panties), no pants, pussy",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a sports bra": {
			"positive": "sports bra, bottomless,(no panties), no pants, pussy",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a striped bra": {
			"positive": "striped bra, bottomless,(no panties), no pants, pussy",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"pasties": {  // Doesn't work well
			"positive": "pasties, bottomless,(no panties), no pants, pussy",
			"negative": "jeans, pants, skirt, shorts, nipples",
		},
		"a tube top and thong": {
			"positive": "tube top, thong",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"a sweater and panties": {
			"positive": "sweater, oversized clothes, panties",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"a tank-top and panties": {
			"positive": "tank top, thong",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"a t-shirt and thong": {
			"positive": "t-shirt, thong",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"an oversized t-shirt and boyshorts": {
			"positive": "t-shirt, oversized clothes, boyshort panties",
			"negative": "jeans, pants, skirt, nipples, pussy",
		},
		"sport shorts and a t-shirt": {
			"positive": "t-shirt, sports panties",
			"negative": "jeans, pants, skirt, nipples, pussy",
		},
		"sport shorts and a sports bra": {
			"positive": "sports bra, sports panties",
			"negative": "jeans, pants, skirt, nipples, pussy",
		},
		"a t-shirt and panties": {
			"positive": "t-shirt, panties",
			"negative": "jeans, pants, skirt, shorts, nipples, pussy",
		},
		"striped underwear": {
			"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": {
			"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",
			"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, miniskirt",
			"negative": "",
		},
		"a red army uniform": { // not red army, but soviet is at least close
			"positive": "soviet uniform, military uniform, shirt, necktie, miniskirt",
			"negative": "",
		},
		"battledress": {
			"positive": "military jacket, camouflage jacket, camouflage pants, tank top",
			"negative": "",
		},
		"a mounty outfit": {  // Doesn't work well
			"positive": "mounty, red military jacket, black pants",
			"negative": "jeans, shorts, pussy, nipples",
		},
		"harem gauze": {
			"positive": "veil, belly chain, harem outfit, loose clothes, see-through clothes",
			"negative": "",
		},
		"slutty jewelry": {
			"positive": "nude, ornate ring, gem, belly chain, chain between breasts, armlet, gold headband",
			"negative": ""
		},
		"a Santa dress": {
			"positive": "santa costume, santa dress, medium dress, fur trim",
			"negative": ""
		},
		"a bimbo outfit": {
			"positive": "pink tube top, pink skirt, latex, (miniskirt)",
			"negative": "jeans, nude, pussy, nipples",
		},
		"a slutty outfit": {
			"positive": "(pink:1.1) crop top, pink microskirt, panties, tight clothes, midriff, navel",
			"negative": "jeans, nude, nipples",
		},
		"a courtesan dress": {
			"positive": "long dress, layered fabric, corset, bare shoulders, long sleeves, detached sleeves",
			"negative": "",
		},
		"a schoolgirl outfit": {
			"positive": "school uniform, microskirt, white panties, pleated skirt, white blouse, tied shirt",
			"negative": "",
		},
		"petite admi outfit": {
			"positive": "pencil skirt, miniskirt, shirt, shirt tucked in",
			"negative": "nipples, nude",
		}
	};

	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, cleavage cutout",
			"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, medium dress",
			"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": "gown, long dress, luxurious dress, cleavage, slave straps",
			"negative": "jeans",
		},
		"a halter top dress": {
			"positive": "halterneck, long dress, luxurious dress, backless dress",
			"negative": "jeans",
		},
		"a leotard": {
			"positive": "leotard",
			"negative": "jeans",
		},
		"a monokini": {
			"positive": "swimsuit, monokini, breasts out",
			"negative": "jeans",
		},
		"an apron": {
			"positive": "naked apron, white apron, sideboob",
			"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";
		}
		if (this.slave.race === "catgirl") {
			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;
		}
	}
};


code:
App.Art.GenAI.Ill.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 "collar, neck bell";
		} else if (this.slave.collar === "bowtie") {
			return "bowtie";
		} else if (this.slave.collar === "leather with cowbell") {  // Doesn't work well, better than "leather with cowbell collar"
			return "collar, leather collar, cowbell";
		} else if (this.slave.collar === "neck corset") { // Doesn't work well, but doesn't add real corsets
			return "neck corset, posture collar";
		} else if (this.slave.collar === "neck tie") {
			return "necktie";
		} 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;
	}
};

repo_name: Nithhogg/fc

license: mit


code:
// cSpell: ignore nopussy_v1

App.Art.GenAI.Ill.CrotchPromptPart = class CrotchPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		const slave = asSlave(this.slave);
		// censored or covered by clothes
		const exposesCrotch = this.helper.exposesCrotch(this.slave.clothes);
		const activelyExposesCrotch = this.helper.activelyExposesCrotch(slave?.assignment);
		const exposesCameltoe = this.helper.exposesUnderwear(this.slave.clothes);

		if (!(exposesCrotch || activelyExposesCrotch || exposesCameltoe) || this.censored) {
			return undefined;
		}
		let promptParts = [];

		// null slaves
		if (this.slave.dick === 0 && this.slave.vagina === -1) {
			return "(no pussy)";
		}

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

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

		if (exposesCrotch || activelyExposesCrotch) {

			// futanari
			if (dick && vagina >= 0) {
				promptParts.push("futanari");
				promptParts.push(dickDescription());
				promptParts.push(vaginaDescription());
				// male
			} else if (dick) {
				promptParts.push(dickDescription());
				// female
			} else if (vagina >= 0) {
				promptParts.push(vaginaDescription());
			}

			if (anus >= 3) {
				promptParts.push("gaping anus");
			} else {
				promptParts.push("anus");
			}

			if (activelyExposesCrotch && !exposesCrotch) {
				promptParts.push("panties aside");
			}

			return promptParts.join(", ");
		} else if (exposesCameltoe) {
			if (this.slave.dick) {
				return "bulge";
			} else {
				return "cameltoe";
			}
		}

		return undefined;

		/** @returns {string} */
		function dickDescription() {
			let dickDescriptors = [];
			switch (dick) {
				case 0:
					break;
				case 1:
					dickDescriptors.push("(small penis:1.5)");
					break;
				case 2:
					dickDescriptors.push("(small penis:1.2)");
					break;
				case 3:
					dickDescriptors.push("small penis");
					break;
				case 4:
				case 5:
					dickDescriptors.push("penis");
					break;
				case 6:
				case 7:
					dickDescriptors.push("large penis");
					break;
				case 8:
					dickDescriptors.push("huge penis");
					break;
				case 9:
					dickDescriptors.push("gigantic penis");
					break;
				case 10:
				case 11:
					dickDescriptors.push("(gigantic penis:1.3)");
					break;
			}

			/**
			 * @type {number}
			 * * 0: circumcised
			 * * 1+:uncut, also affects clitoral hood size
			 */
			if (dickDescriptors.length) {
				if (foreskin === 0) {
					dickDescriptors.push("circumcised");
				} else {
					dickDescriptors.push("foreskin");
				}
			}

			if (scrotum && balls > 1) {
				switch (balls) {
					case 2:
						dickDescriptors.push("small testicles");
						break;
					case 3:
						break;
					case 4:
						dickDescriptors.push("large testicles");
						break;
					case 5:
					case 6:
						dickDescriptors.push("huge testicles");
						break;
					case 7:
					case 8:
						dickDescriptors.push("gigantic testicles");
						break;
					case 9:
						dickDescriptors.push("(gigantic testicles:1.2)");
						break;
					case 10:
					case 11:
						dickDescriptors.push("(gigantic testicles:1.5)");
						break;
				}
			} else {
				dickDescriptors.push("no testicles");
			}

			return dickDescriptors.join(", ");
		}

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

			if (clit < 1) {
				// nothing
			} else if (clit < 3) {
				vaginaDescriptors.push("large clitoris");
			} else if (clit < 4 || dick !== 0) {
				vaginaDescriptors.push("huge clitoris");
			} else if (clit === 4) {
				vaginaDescriptors.push("futanari, (no testicles), (small penis:1.5), erection");
			} else {
				vaginaDescriptors.push("futanari, (no testicles), (small penis:1.2), erection");
			}

			if (vagina >= 4) {
				vaginaDescriptors.push("gaping vagina");
			}

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

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

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

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

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

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

			if (balls < 2 || !scrotum) {
				promptParts.push("testicles, scrotum");
			}
		} else if (vagina === -1) {
			promptParts.push("pussy");
		}

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


code:
App.Art.GenAI.Ill.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.Ill.EarsPromptPart = class EarsPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.slave.faceAccessory === "cat ears") {
			return `cat ears hairband`;
		}
		const parts = [];
		if (this.slave.earT !== "none" && this.slave.earT !== "normal") {
			parts.push(`${this.slave.earT} ears`);
		}

		switch (this.slave.earShape) {
			case "bird":
				parts.push("bird ears");
				break;
			case "cow": case "deer": case "robot": case "sheep":
				// TODO: "deer" and "sheep" need LoRAs since the model adds deer/sheep horns and negative prompting doesn't remove them
				parts.push(`${this.slave.earShape} ears`);
				break;
			case "damaged":
				if (this.slave.earT !== "none") {
					parts.push(`cut human ear`);
				} else {
					parts.push(`cut ear`);
				}
				break;
			case "dragon":
				parts.push("dragon ears");
				break; // TODO: needs a lora
			case "elven": case "pointy":
				parts.push(`pointy ears`);
				break;
			case "gazelle":
				parts.push(`gazelle ears`);
				break; // TODO: needs a lora for reliable results
			case "holes":
				break; // TODO: will need a lora
			case "none":
				break; // TODO: currently handled in negative prompt, but that doesn't work very reliably. We need a lora
			case "normal":
				break;
			case "orcish":
				parts.push("orc ears");
				break; // TODO: nees a lora; model attempts to make an orc; adding orc to negatives makes the model remove the ears
		}

		return parts.join(`, `);
	}

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

		switch (this.slave.earShape) {
			case "bird":
				parts.push("(wings)");	// prompt for bird ear cause wings to be drawn
				break;
			case "cow": case "deer": case "robot": case "sheep":
				if (this.slave.horn === "none") {
					parts.push("cow horns");
				}
				break;
			case "damaged":
				break;
			case "dragon":
				if (this.slave.horn === "none") {
					parts.push("dragon horns");
				}
				break;
			case "elven": case "pointy":
				break;
			case "gazelle":
				if (this.slave.horn === "none") {
					parts.push("gazelle horns");
				}
				break;
			case "holes":
			case "none":
			case "normal":
			case "orcish":
				break; // TODO: nees a lora; model attempts to make an orc; adding orc to negatives makes the model remove the ears
		}

		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.Ill.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;
		}

		let prompts = [];

		if (slave?.fuckdoll !== 0) {
			if (slave?.fuckdoll < 50) {
				return `open mouth, clenched fists`; // NG proxy for terrified for early adaptation
			} else {
				return "expressionless";
			}
		} else if (this.slave.fetish === Fetish.MINDBROKEN) {
			return "mind break, saliva, empty eyes";
		} else {
			if (this.slave.devotion < -50) {
				prompts.push(`angry, frown`);
			} else if (this.slave.devotion < -20) {
				prompts.push(`angry`);
			} else if (this.slave.devotion < 51) {
				prompts.push("expressionless");
			} else if (!(slave?.assignment === Job.PUBLIC || slave?.assignment === Job.ARCADE || slave?.assignment === Job.CLUB)) {
				if (this.slave.devotion < 95 && this.slave.mouthAccessory === "none") {
					prompts.push(`seductive smile`);
				} else if (this.slave.mouthAccessory === "none") {
					prompts.push(`seductive smile, parted lips, happy`);
				}
			}

			if (slave) {
				// When slave not very devoted, allow crying expression
				// When slave is devoted, crying paired with smile creates a weird expression
				if (this.slave.devotion < 51) {
					if (slave.trust < -90) {
						prompts.push(`scared, looking down, looking away, crying, tears`);
					} else if (slave.trust < -50) {
						prompts.push(`scared, looking down, looking away, crying`);
					} else if (slave.trust < -20) {
						prompts.push(`scared, looking down, looking away`);
					} else if (slave.trust < 51) {
						prompts.push("looking down, looking away");
					} else {
						prompts.push("looking at viewer");
					}
				} else {
					if (slave.trust < -20) {
						prompts.push("looking down, looking away");
					} else {
						prompts.push("looking at viewer");
					}
				}
			}

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

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

		return undefined;
	}

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


code:
App.Art.GenAI.Ill.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)) {
			if (!canSee(this.slave)) {
				positive.push("one eye closed");
			} 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("heart-shaped pupils");
						break;
					case "hypnotic":
						positive.push("@_@");
						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(`${this.slave.eye.left.iris} eyes, eyepatch`);
		} else if (hasRightEye(this.slave)) {
			positive.push(`${this.slave.eye.right.iris} eyes, eyepatch`);
		} 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.Ill.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;
		} else if (this.slave.eyebrowHStyle === "natural") {
			return;
		} else {
			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.Ill.GenderPromptPart = class GenderPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		let prompt = [];
		if (this.isFeminine) {
			prompt.push("1girl");
			if (this.slave.race === "catgirl") {
				prompt.push("cat girl");
			}
		} else if (this.isMasculine) {
			prompt.push("1boy");
			if (this.slave.race === "catgirl") {
				prompt.push("cat boy");
			}
		} else {
			if (this.slave.race === "catgirl") {
				prompt.push("cat girl");
			}
		}

		return prompt.join(", ");
	}

	/**
	 * @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;
			}
		} else if (this.isMasculine) {
			if (perceivedGender(this.slave) > 1) { // Masculine hormone but Feminine appearing
				return undefined;
			} else { // Masculine hormone, Masculine appearing
				return "1girl, " + facialHair;
			}
		} else {
			return undefined;
		}
	}
};


code:
App.Art.GenAI.Ill.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.75) {
			hairLength = `absurdly long hair`;
		} else if (heightVhLength >= 0.5) {
			hairLength = `very long hair`;
		} else if (heightVhLength >= 0.2) {
			hairLength = `long hair`;
		} else if (this.slave.hLength >= 15) {
			hairLength = `medium hair`;
		} else {
			hairLength = `short hair`;
		}

		const parts = [hairLength];
		parts.push(`${this.slave.hColor} hair`);

		if (stylePostfix) {
			parts.push(`${styleStr} hair`);
		}

		if (this.slave.hEffect.includes(`undercoloring`) || this.slave.hEffect.includes(`highlights`)) {
			parts.push(`${this.slave.hEffect} hair`); // color is already included in hEffect
		} else if (this.slave.hEffect.includes(`stripes`)) {
			parts.push(`${this.slave.hEffect} in 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.Ill.HealthPromptPart = class HealthPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (asSlave(this.slave)?.fuckdoll > 0) {
			return undefined;
		}

		if (this.slave.health.condition < -50) {
			return `sick`;
		} else if (this.slave.health.condition < -10) {
			return `tired`;
		} else {
			return null;
		}
	}

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


code:
App.Art.GenAI.Ill.HeightPromptPart = class HeightPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.slave.height < 150) {
			if (this.slave.weight > 30) {
				if (V.aiAgeFilter) {
					return "shortstack";
				} else {
					return "shortstack, loli";
				}
			} else {
				if (V.aiAgeFilter) {
					return "petite";
				} else {
					return "petite, loli";
				}
			}
		} else if (this.slave.height > 180) {
			if (this.isFeminine) {
				return "tall female";
			} else {
				return "tall male";
			}
		}

		return undefined;
	}

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


code:
App.Art.GenAI.Ill.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 || this.slave.hips === 1) {
			return null;
		} 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:1.1)";
		} else if (this.slave.hips === -1) {
			return "wide hips";
		} else if (this.slave.hips === 0 || this.slave.hips === 1) {
			return null;
		} else if (this.slave.hips === 2) {
			return `narrow hips`;
		} else {
			return `(narrow hips:1.1)`;
		}
	}
};


code:
App.Art.GenAI.Ill.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 = [];
		const societyStyles = {
			FSRomanRevivalist: "column, marble \\(stone\\), greco-roman architecture, frescoed walls",
			FSNeoImperialist: "militaristic decoration, tiles, black floor, black walls",
			FSEgyptianRevivalist: "column, sandstone, ancient egyptian",
			FSEdoRevivalist: "ancient japan, wooden beam, tatami",
			FSArabianRevivalist: "arabic decoration, arched doorways, lantern",
			FSChineseRevivalist: "ancient chinese decoration, wooden beam, red wooden pillar",
			FSAztecRevivalist: "mesoamerican architecture, aztec decoration",
			FSAntebellumRevivalist: "antebellum architecture, crystal chandeliers, patterned wallpaper",
		};

		let useEnv = false;
		let style = "";
		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:
					useEnv = true;
					parts.push("hallway");
					break;
				case Job.ARCADE:
					useEnv = true;
					parts.push("restrained, chain, handcuffs, ankle cuffs, neck ring, (bondage), (from side), fellatio, vaginal, sex, 2boys, penis, glory hole, cum on body, cum on breasts, neon lights, dark hallway");
					break;
				case Job.GLORYHOLE:
					useEnv = true;
					parts.push("glory hole, fellatio, handjob, double handjob, cum, cum on body");
					break;
				case Job.ARENA: // training
				case Job.PIT: // fighting
					useEnv = true;
					parts.push("arena, holding weapon");
					break;
				case Job.BROTHEL:
					useEnv = true;
					parts.push("pink light, indoors, love hotel, bedroom, bed, night");
					break;
				case Job.MADAM:
					useEnv = true;
					parts.push("pink light, indoors, love hotel, main hall");
					break;
				case Job.CELLBLOCK:
				case Job.CONFINEMENT:
					parts.push("prisoner, prison cell, prison, jail, jail cell, indoors, underground, dark room");
					break;
				case Job.WARDEN:
					parts.push("dark room, high-tech command room, computer screen, prison, prison cell");
					break;
				case Job.CLASSES:
				case Job.SCHOOL:
					useEnv = true;
					parts.push("classroom, school desk, desk, school chair, from side");
					break;
				case Job.TEACHER:
					useEnv = true;
					parts.push("classroom, teacher, teaching, chalkboard, holding book");
					break;
				case Job.CLINIC:
					useEnv = true;
					parts.push("clinic, hospital, hospital bed, monitor");
					break;
				case Job.NURSE:
					useEnv = true;
					parts.push("clinic, hospital, hospital bed, holding clipboard");
					break;
				case Job.CLUB:
					useEnv = true;
					parts.push("party, dance floor, neon lights, disco ball, crowd, orgy, sex, fellatio, cum on body, 3boys, penis");
					break;
				case Job.DJ:
					useEnv = true;
					parts.push("dj, dj desk, headphones, desk, disco ball, party, neon lights, crowd");
					break;
				case Job.WHORE:
					parts.push("alley, outdoors, night, 1boy, faceless, penis");
					break;
				case Job.PUBLIC:
					useEnv = true;
					parts.push("outdoors, crowd, street, orgy, sex, fellatio, cum on body, 3boys, penis");
					break;
				case Job.CONCUBINE:
				case Job.MASTERSUITE:
				case Job.FUCKTOY:
					useEnv = true;
					parts.push("upscale interior, luxurious apartment, on bed");
					break;
				case Job.BODYGUARD:
					useEnv = true;
					parts.push("indoors, luxurious apartment, bedroom, holding gun, handgun");
					break;
				case Job.MILKED:
				case Job.DAIRY:
				case Job.MILKMAID:
					parts.push("barn, stall");
					if (this.slave.dick > 0) {
						parts.push("penis milking");
					}
					break;
				case Job.HEADGIRL:
				case Job.HEADGIRLSUITE:
					useEnv = true;
					parts.push("office, desk, computer, monitor, whip");
					break;
				case Job.HOUSE:
				case Job.QUARTER:
				case Job.STEWARD:
					useEnv = true;
					parts.push("kitchen, cooking");
					break;
				case Job.SPA:
				case Job.ATTENDANT:
					useEnv = true;
					parts.push("sonsen, wooden wall, indoors, waterfall, fog, in water");
					break;
				case Job.REST:
					parts.push("bedroom");
					switch (slave.rules.living) {
						case "normal":
							// nothing to add
							break;
						case "spare":
							parts.push("empty room, single bed");
							break;
						case "luxurious":
							parts.push("luxurious room, decorations");
							break;
					}
					break;
				case Job.RECRUITER:
					useEnv = true;
					parts.push("cafe, table, clipboard");
					break;
				case Job.AGENT:
				case Job.AGENTPARTNER:
					useEnv = true;
					parts.push("office, clipboard, large window");
					break;
				case Job.FARMER:
				case Job.FARMYARD:
					parts.push("outdoors, 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("outdoors, forest, path");
					break;
				case Job.TANK:
					parts.push("tank interior, driving");
					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
					parts.push("hallway");
					useEnv = true;
					break;
			}

			if (useEnv) {
				parts.push(style);
			}
		} 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;
		}

		let parts = [];

		if (this.positive().includes("outdoor")) {
			parts.push("bedroom, bed");
		}

		if (this.slave.rules.living === "spare") {
			parts.push("decorations, furniture");
		}

		return parts.join(", ");
	}

	/**
	 * @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.Ill.MusclesPromptPart = class MusclesPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (this.slave.muscles > 95) {
			return 'muscular, bodybuilder';
		} else if (this.slave.muscles > 50) {
			return 'muscular';
		} else if (this.slave.muscles > 30) {
			return 'toned';
		} else {
			return undefined;
		}
	}

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


code:
App.Art.GenAI.Ill.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.Ill.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 = [];

		let expressiveSex = false;

		if (isAmputee(this.slave)) {
			parts.push(`on chair`); // posture change prevents genning arms/legs, looks more natural
		} else if (isQuadrupedal(this.slave)) {
			parts.push(`all fours`);
		} else if (!canStand(this.slave) || slave?.fuckdoll !== 0) {
			parts.push("kneeling");
		} else {
			switch (slave?.assignment) {
				case Job.ARENA: // training
				case Job.PIT: // fighting
				case Job.MADAM:
				case Job.WARDEN:
				case Job.TEACHER:
				case Job.DJ:
				case Job.BODYGUARD:
				case Job.MILKMAID:
				case Job.HEADGIRL:
				case Job.HEADGIRLSUITE:
				case Job.HOUSE:
				case Job.QUARTER:
				case Job.STEWARD:
				case Job.ATTENDANT:
				case Job.NURSE:
				case Job.AGENT:
				case Job.AGENTPARTNER:
				case Job.FARMER:
				case Job.FARMYARD:
				case Job.MATRON: // head nursery nanny
				case Job.NURSERY: // nanny
				case Job.LURCHER:
					parts.push("standing");
					break;
				case Job.ARCADE:
					parts.push("(all fours), bent over, elbows on ground");
					break;
				case Job.CLUB:
				case Job.GLORYHOLE:
				case Job.PUBLIC:
					if (this.slave.height < 150 || this.slave.visualAge < 14) {
						parts.push("standing");
					} else {
						parts.push("kneeling");
					}
					break;
				case Job.FUCKTOY:
				case Job.BROTHEL:
				case Job.CONCUBINE:
				case Job.MASTERSUITE:
					expressiveSex = true;
					parts.push("on back, spread legs");
					break;
				case Job.CELLBLOCK:
				case Job.CONFINEMENT:
					parts.push("sitting, on ground");
					break;
				case Job.CLASSES:
				case Job.SCHOOL:
				case Job.RECRUITER:
				case Job.TANK:
					parts.push("sitting, on chair");
					break;
				case Job.CLINIC:
					parts.push("on back, resting, sleeping, closed eyes");
					break;
				case Job.SPA:
				case Job.REST:
					parts.push("resting");
					break;
				case Job.WHORE:
					expressiveSex = true;
					parts.push("standing, bent over");
					break;
				case Job.MILKED:
				case Job.DAIRY:
					parts.push("standing, bent over, milking machine");
					break;
				case Job.SUBORDINATE:
				case undefined:
				default:
					parts.push("hallway");
					break;
			}
		}

		if (!isAmputee(this.slave) && !isQuadrupedal(this.slave)) { // no arms pose for amputees and quadrupeds
			if (slave?.fuckdoll !== 0) {
				parts.push("arms at side");
			} else if (this.slave.devotion < -20) {
				if (expressiveSex) {
					parts.push("covering crotch, covering breasts");
				} else {
					if (this.slave.devotion < -20) {
						parts.push(`from side, crossed arms`);
					} else {
						parts.push(`crossed arms`);
					}
				}
			} else if (expressiveSex) {	// these are only valid for not hating and expressive sex work slaves
				if (this.slave.devotion < 21) {
					// doesn't like master
					parts.push("arms at side");
				} else if (this.slave.devotion < 96) {
					// willing
					if (slave?.vagina !== -1) {
						parts.push(`spread pussy`);
					}
				} else {
					// excited
					if (slave?.vagina !== -1) {
						parts.push(`spread pussy, masturbation`);
					}
				}
			}
		}

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

		return parts.join(`, `);
	}

	/**
	 * @override
	 */
	negative() {
		const slave = asSlave(this.slave);

		let fromAboveWork =
			slave?.assignment === Job.GLORYHOLE ||
			slave?.assignment === Job.PUBLIC ||
			slave?.assignment === Job.FUCKTOY ||
			slave?.assignment === Job.BROTHEL ||
			slave?.assignment === Job.CONCUBINE ||
			slave?.assignment === Job.MASTERSUITE ||
			slave?.assignment === Job.WHORE ||
			slave?.assignment === Job.MILKED ||
			slave?.assignment === Job.DAIRY;

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


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

		if (this.slave.bellyPreg >= this.slave.bellyFluid) {
			/**
			 * how big their belly is in CCs
			 *
			 * ||thresholds:|
			 * |-|-|
			 * 100	| bloated
			 * 1500   | early pregnancy
			 * 5000   | obviously pregnant
			 * 10000  | very pregnant
			 * 15000  | full term
			 * 30000  | full term twins
			 * 45000  | full term triplets
			 * 60000  | full term quads
			 * 75000  | full term quints
			 * 90000  | full term sextuplets
			 * 105000 | full term septuplets
			 * 120000 | full term octuplets
			 * 150000 | oversized pregnancy
			 * 300000 | hyperpreg state 1
			 * 450000 | hyperpreg state 2
			 * 600000 | hyperpreg state 3
			 * 750000 | hyperpreg state 4
			 */
			if (this.slave.belly < 100) {
				return undefined;
			} else if (this.slave.belly < 5000) {
				return "(belly:1.2)";
			} else if (this.slave.belly < 8000) {
				return "early pregnancy";
			} else if (this.slave.belly < 30000) {
				return "pregnant";
			} else if (this.slave.belly < 100000) {
				return "hyper pregnancy";
			} else if (this.slave.belly < 150000) {
				return "(hyper pregnancy:1.2)";
			} else if (this.slave.belly < 300000) {
				return "(hyper pregnancy:1.5)";
			} else if (this.slave.belly < 600000) {
				return "(hyper pregnancy:1.7)";
			} else {
				return "(hyper pregnancy:2.0)";
			}
		} else {
			/**
			 * how big their belly is in CCs (fluid distension only)
			 *
			 * ||thresholds|
			 * |-|-|
			 * 100   | bloated
			 * 2000  | clearly bloated (2 L)
			 * 5000  | very full (~1 gal)
			 * 10000 | full to bursting (~2 gal)
			 */
			if (this.slave.bellyFluid < 5000) {
				return "early pregnancy";
			} else if (this.slave.bellyFluid < 10000) {
				return "pregnant";
			}  else {
				return "hyper pregnancy";
			}
		}
	}

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


code:
App.Art.GenAI.Ill.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.Ill.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() {
		return undefined;
	}

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


code:
App.Art.GenAI.Ill.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 = `${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 = "light skin";
					break;
				case "fair":
				case "light":
				case "beige":
					skinDesc = "light skin";
					break;
				case "sun tanned":
				case "tan":
					skinDesc = "light skin, tan, tanlines";
					break;
				case "light olive":
				case "spray tanned":
					skinDesc = "tan";
					break;
				case "olive":
				case "bronze":
				case "dark beige":
				case "dark olive":
				case "light brown":
				case "brown":
					skinDesc = "light brown skin";
					break;
				case "dark":
				case "dark brown":
				case "black":
				case "ebony":
					skinDesc = "dark skin";
					break;
				case "pure black":
					skinDesc = "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 += ", mole under eye";
			} else if (marking === "birthmark") {
				skinDesc += ", birthmark";
			} else if (marking === "freckles") {
				skinDesc += ", freckles";
			} else if (marking === "heavily freckled") {
				skinDesc += ", heavily freckles";
			} else {
				skinDesc += `, 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 fair":
			case "extremely pale":
			case "very pale":
			case "pale":
			case "light beige":
			case "very fair":
				return "dark skin";
			case "fair":
			case "light":
			case "beige":
				return "";
			case "sun tanned":
			case "tan":
				return "";
			case "light olive":
			case "spray tanned":
			case "olive":
			case "bronze":
			case "dark beige":
			case "dark olive":
			case "light brown":
			case "brown":
			case "dark":
			case "dark brown":
			case "black":
			case "ebony":
			case "pure black":
			default:
				return "tanlines";
		}
	}

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


code:
// cSpell: ignore derpibooru

App.Art.GenAI.Ill.StylePromptPart = class StylePromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		let prompts = ["masterpiece, best quality, amazing quality"];
		// Check if filter is on and needed, then pick model-appropriate framing tags.
		if (this.censored) {
			prompts.push("general");
		}

		prompts.push("full body, full-length portrait");

		switch (V.aiStyle) {
			case 0: //	Custom Style
				return V.aiCustomStylePos;
			case 1: // Photorealistic
				return prompts.join(", ") + ", realistic";
			case 2: //	Anime/Hentai
				return prompts.join(", ") + ", anime";
			case 3: // No extra styling
			default:
				return prompts.join(", ");
		}
	}

	/**
	 * @override
	 */
	negative() {
		let prompts = [];
		if (this.censored) {
			prompts.push("nsfw, nude");
		} else {
			prompts.push("censored");
		}

		if (V.aiStyle === 0) {
			return prompts.join(", ") + ", " + V.aiCustomStyleNeg;
		}

		let defaultPrompt = "bad quality, worst quality, worst detail, sketch, watermark, breath, from below";
		if (prompts.length !== 0) {
			return defaultPrompt + ", " + prompts.join(", ");
		} else {
			return defaultPrompt;
		}
	}

	/**
	 * @override
	 */
	face() {
		switch (V.aiStyle) {
			case 0: //	Custom Style
				return V.aiCustomStylePos;
			case 1: // Photorealistic
				return "photorealistic";
			case 2: //	Anime/Hentai
				return "masterpiece, best quality, anime";
			case 3: // None
				return "";
		}
	}
};


code:
App.Art.GenAI.Ill.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();

		// 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) {
			// have no effect
			// parts.push('belly tattoo');
		}
		if (loc.breast) {
			parts.push('breast tattoo');
		}
		if (loc.dick) {
			// no effect
			// parts.push('penis tattoo');
		}
		if (loc.vagina) {
			parts.push('pubic tattoo');
		}
		if (loc.lips) {
			// no effect
			// parts.push('lip tattoo');
		}

		return parts.join(', ');
	}

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


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

		if (this.slave.waist > 30) {
			return "muffin top";
		} 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 > 95) {
			return "(narrow waist:1.1)";
		} else if (this.slave.waist > 30) {
			return `narrow waist`;
		} else {
			return null;
		}
	}
};


code:
App.Art.GenAI.Ill.WeightPromptPart = class WeightPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		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 undefined;
		} 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 'absurdly obese';
		}
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.weight < -10 || this.slave.weight >= 10) {
			return undefined;
		} else {
			return "skinny, emaciated, curvy, fat, obese";
		}
	}

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


code:
App.Art.GenAI.getOpenPoseData = (function() {
	/**
	 * @param {FC.SlaveState} slave
	 * @returns {Promise<string>} or null if posing should be prompt-only
	 */
	async function getData(slave) {
		if (V.aiOpenPose) {
			if (isAmputee(slave)) {
				return null; // TODO: openpose really thinks you should have limbs...might be able to do something with special poses, though? worth experimenting with probably
			}
			if (slave.custom.aiPose) {
				switch (slave.custom.aiPose.type) {
					case "Library":
						return poseFromLibrary(slave.custom.aiPose.name);
					case "JSON":
						return fetch(`resources/poses/${slave.custom.aiPose.filename}.json`)
							.then(value => {
								return value.text();
							}).then(obj => {
								return App.Art.GenAI.sdClient.renderOpenPoseJSON(obj);
							});
					case "PNG":
						return fetch(`resources/poses/${slave.custom.aiPose.filename}.png`)
							.then(value => {
								return value.blob();
							}).then(blob => {
								return blobToBase64(blob);
							});
					default:
						throw new Error("Unexpected custom pose type");
				}
			} else {
				// TODO: pick a pose programmatically. should align with the prompts in PosturePromptPart, otherwise weirdness will ensue.
				return null; // for now, bail out here - lines below dummied out to prevent "unreachable code after return" debugger complaints
				// let pose = "Standing, Neutral";
				// return poseFromLibrary(pose);
			}
		}
		return null;
	}

	/**
	 * @param {string} name
	 * @returns {Promise<string>}
	 */
	async function poseFromLibrary(name) {
		const entry = App.Data.Art.Poses[name];
		if (!entry) {
			throw new Error(`Could not find pose in library: ${name}`);
		}
		if (!entry.cache) {
			entry.cache = await App.Art.GenAI.sdClient.renderOpenPoseJSON(entry.poseData);
		}
		return entry.cache;
	}

	return getData;
})();


code:
App.Art.GenAI.DemographicsPromptPart = class DemographicsPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		const slave = asSlave(this.slave);
		const parts = [];

		// height, age, gender
		let height = '';

		if (slave?.height < 150) {
			height = 'short ';
		} else if (slave?.height > 180) {
			height = 'tall ';
		}

		if (this.isFeminine) {
			if ((slave?.visualAge < 16) && !V.aiAgeFilter) {
				parts.push(height + 'young girl', 'loli');
			} else if (slave?.visualAge < 18 && !V.aiAgeFilter) {
				parts.push(height + 'young woman', 'teenager');
			} else if (slave?.visualAge <= 20) {
				parts.push(height + 'young woman');
			} else if (slave?.visualAge <= 40) {
				parts.push(height + 'woman');
			} else if (slave?.visualAge >= 40) {
				parts.push(height + 'mature');
			}

			if (perceivedGender(this.slave) < -1) {
				parts.push('butch');
			}

			if (this.slave.race === "catgirl") {
				parts.push("catgirl");
			}
		} else if (this.isMasculine) {
			if ((slave?.visualAge < 16) && !V.aiAgeFilter) {
				parts.push(height + 'young boy', 'shota');
			} else if (slave?.visualAge < 18 && !V.aiAgeFilter) {
				parts.push(height + 'young man', 'teenager');
			} else if (slave?.visualAge <= 20) {
				parts.push(height + 'young man');
			} else if (slave?.visualAge <= 40) {
				parts.push(height + 'man');
			} else if (slave?.visualAge >= 40) {
				parts.push(height + 'older man');
			}

			if (perceivedGender(this.slave) > 1) {
				parts.push('feminine');
			}

			if (this.slave.race === "catgirl") {
				parts.push("catboy");
			}
		} else {
			if (this.slave.race === "catgirl") {
				parts.push("catperson");
			}
		}

		if ((slave?.visualAge < 10) && !V.aiAgeFilter) {
			parts.push(height + 'young child');
		}

		if (slave?.visualAge >= 60) {
			parts.push('elderly');
		}

		if ((slave?.visualAge < 10) && !V.aiAgeFilter) {
			parts.push('kid');
		} else if ((slave?.visualAge < 6) && !V.aiAgeFilter) {
			parts.push('child');
		}

		// breasts
		let breastShape = '';
		if (slave?.boobs > 300) {
			switch (slave?.boobShape) {
				case "perky":
				case "torpedo-shaped":
					breastShape = 'perky ';
					break;
				case "downward-facing":
				case "saggy":
				case "wide-set":
					breastShape = 'saggy ';
					break;
			}
		}

		if (slave?.boobs < 300) {
			breastShape += this.helper.isPony() ? 'flat chested' : 'flat chest'; // Pony uses a slightly different tag
		} else if (slave?.boobs < 400) {
			breastShape += 'tiny ';
		} else if (slave?.boobs < 500 || this.censored) {
			breastShape += 'small ';
		} else if (slave?.boobs < 650 || this.slave.visualAge < 6) {
			breastShape += 'medium ';
		} else if (slave?.boobs < 800 || this.slave.visualAge < 10) {
			breastShape += 'large ';
		} else if (slave?.boobs < 1000 || this.slave.visualAge < 18) {
			breastShape += 'huge ';
		} else {//if (slave?.boobs < 1400) {
			breastShape += 'giant breasts, huge ';
		}

		// breast/nipple exposure for clothing
		const clothingBreasts = [
			"no clothing", "chains", "body oil", "uncomfortable straps", "shibari robes", "striped panties", "clubslut netting", "striped underwear", "a thong", "a skimpy loincloth", "boyshorts", "panties", "cutoffs", "sport shorts", "harem gauze", "slutty jewelry", "a slutty pony outfit"
		];
		const clothingCleavage = [
			"conservative clothing", "Western clothing", "a slutty qipao", "spats and a tank top", "a latex catsuit", "attractive lingerie", "attractive lingerie for a pregnant woman", "kitty lingerie", "a maternity dress", "stretch pants and a crop-top", "a succubus outfit", "a fallen nuns habit", "a cheerleader outfit", "cutoffs and a t-shirt", "slutty business attire", "a ball gown", "a slave gown", "a halter top dress", "an evening dress", "a mini dress", "a bunny outfit", "a slutty maid outfit", "a slutty nurse outfit", "a dirndl", "a gothic lolita dress", "a button-up shirt and panties", "a button-up shirt", "a t-shirt", "a tank-top", "a tube top", "a bra", "a sports bra", "a striped bra", "a tube top and thong", "a tank-top and panties", "a t-shirt and thong", "leather pants and a tube top", "a bimbo outfit", "a slutty outfit", "a courtesan dress"
		];
		const clothingSideboob = [
			"a toga", "a chattel habit", "a scalemail bikini", "a slave gown", "a leotard", "an apron", "overalls", "a courtesan dress"
		];
		const clothingUnderboob = [
			"stretch pants and a crop-top", "a succubus outfit", "a penitent nuns habit", "a t-shirt", "a tank-top", "a tube top", "a tube top and thong", "a tank-top and panties", "a t-shirt and thong", "a t-shirt and panties", "leather pants and a tube top", "harem gauze", "a bimbo outfit", "a slutty outfit"
		];

		const clothingBreastParts = [];

		if (clothingBreasts.includes(slave?.clothes)) {
			if (slave?.boobsImplant === 0 && slave?.boobs > 300) {
				clothingBreastParts.push('breasts');
			} else if (slave?.boobsImplant !== 0) {
				clothingBreastParts.push('fake tits');
			}
		}
		if (clothingCleavage.includes(slave?.clothes)) {
			clothingBreastParts.push('cleavage');
		}
		if (clothingSideboob.includes(slave?.clothes)) {
			clothingBreastParts.push('sideboob');
		}
		if (clothingUnderboob.includes(slave?.clothes)) {
			clothingBreastParts.push('underboob');
		}
		if (clothingBreastParts.length === 0) {
			if (slave?.boobsImplant === 0 && slave?.boobs > 300) {
				clothingBreastParts.push('breasts');
			} else if (slave?.boobsImplant !== 0) {
				clothingBreastParts.push('fake tits');
			}
		}

		parts.push(breastShape + clothingBreastParts.join(', '));

		// display nipple if uncovered, or covered nipples if large
		// nipple piercings also handled here
		if (this.helper.exposesNipples(this.slave.clothes)) {
			switch (slave?.nipples) {
				case "huge":
					parts.push('big nipples');
					break;
				case "puffy":
					parts.push('puffy nipples');
					break;
				case "fuckable":
				case "inverted":
				case "partially inverted":
					parts.push('inverted nipples');
					break;
				default:
					parts.push('nipples');
					break;
			}

			if ((slave?.piercing.areola.weight > 0 || slave?.piercing.nipple.weight > 0) && (slave?.visualAge < 18 && V.aiAgeFilter)) {
				parts.push('nipple piercings'); // the models can only handle this in a very basic way
			}
		} else {
			switch (slave?.nipples) {
				case "huge":
				case "puffy":
					parts.push(this.helper.isPony() ? 'nipple outline' : 'covered nipples'); // Using covered nipples on Pony is detrimental when clothed
					break;
				default:
					if (slave?.visualAge < 18 && V.aiAgeFilter) {
						// TODO: this prompting makes the clothes transparent on Illustrious (and maybe other models). If the goal is to censor the nipples why are we even prompting for them at all?
						parts.push('covered nipples');
					}
			}
		}

		// waist and hips
		if (slave?.waist < -40) {
			parts.push('narrow waist');
		}

		if (slave?.hips > 1) {
			parts.push('wide hips');
		}
		if (slave?.hips > 2) {
			parts.push('wide hips, thick thighs');
		}

		return parts.join(', ');
	}

	/**
	 * @override
	 */
	negative() {
		const slave = asSlave(this.slave);
		const parts = [];

		// handles masculine female phenotype
		if (this.isFeminine && perceivedGender(this.slave) < -1) {
			parts.push('woman');
		}

		switch (slave?.boobShape) {
			case "perky":
			case "torpedo-shaped":
			case "spherical":
				parts.push('saggy');
				break;
			default:
				if (slave?.boobsImplant > 1) {
					parts.push('saggy');
				}
		}

		if (slave?.waist > 10) {
			parts.push('narrow waist');
		}

		if (slave?.hips < -1) {
			parts.push('wide hips');
		}
		if (slave?.hips < -2) {
			parts.push('huge hips');
		}

		return parts.join(', ');
	}

	face() {
		const slave = asSlave(this.slave);
		const parts = [];

		// age, gender
		if (this.isFeminine) {
			if ((slave?.visualAge < 18) && !V.aiAgeFilter) {
				switch (V.aiBaseModel) {
					case 1: case 2: // SDXL and Pony
						parts.push('young, loli');
						break;
					default:
						parts.push('young girl');
				}
			} else if (slave?.visualAge <= 20) {
				parts.push('young woman');
			} else if (slave?.visualAge <= 40) {
				parts.push('woman');
			} else if (slave?.visualAge >= 40) {
				parts.push('mature');
			}

			if (perceivedGender(this.slave) < -1) {
				parts.push('butch');
			} else if (perceivedGender(this.slave) > -1 && perceivedGender(this.slave) < 1) {
				parts.push('androgynous');
			}

			if (this.slave.race === "catgirl") {
				parts.push("catgirl");
			}
		} else if (this.isMasculine) {
			if ((slave?.visualAge < 18) && !V.aiAgeFilter) {
				switch (V.aiBaseModel) {
					case 1: case 2: // SDXL and Pony
						parts.push('young, shota');
						break;
					default:
						parts.push('young boy');
				}
			} else if (slave?.visualAge <= 20) {
				parts.push('young man');
			} else if (slave?.visualAge <= 40) {
				parts.push('man');
			} else if (slave?.visualAge >= 40) {
				parts.push('older man');
			}

			if (perceivedGender(this.slave) > 1) {
				switch (V.aiBaseModel) {
					case 1: case 2: case 6:
						parts.push('girly, femboy');
						break;
					default:
						parts.push('feminine');
				}
			} else if (perceivedGender(this.slave) > -1 && perceivedGender(this.slave) < 1) {
				parts.push('androgynous');
			}
		}

		if ((slave?.visualAge < 13) && !V.aiAgeFilter) {
			parts.push('child');
		}

		if (slave?.visualAge >= 60) {
			parts.push('elderly');
		}

		if (slave?.visualAge >= 13 && slave?.visualAge < 18 && !V.aiAgeFilter) {
			parts.push('teenager');
		}

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


code:
App.Art.GenAI.StructurePromptPart = class StructurePromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		// define stance and image composition

		const slave = asSlave(this.slave);
		const customPrompt = slave?.custom?.aiPrompts?.pose;

		const parts = [];

		parts.push('portrait, solo'); // portrait structure prompt may become unnecessary once rest of body part prompts are in?

		if (customPrompt) {
			parts.push(customPrompt);
			return parts.join(', ');
		}

		if (isQuadrupedal(this.slave)) {
			return 'all fours'; // too hard to do anything else with this stance
		} else if (canStand(this.slave)) {
			parts.push('standing');

			if (slave?.trust < -20) {
				parts.push('look away, look down');
			} else if (slave?.devotion < -20) {
				parts.push('defiant, head up');
			}
		} else {
			parts.push('kneeling');

			if ((slave?.trust < -20) && (slave?.devotion < 51)) {
				parts.push('look down, bowing');
			} else if (slave?.devotion > 95) {
				parts.push('from side, look up');
			}
		}

		if (V.arcologies[0] && (V.arcologies[0].FSChattelReligionist > 0) && (slave?.devotion > 95)) {
			parts.push('praying');
		} else if ((slave?.need > 100) && (slave?.fetishKnown === 1) && (slave?.fetishStrength > 60)) {
			switch (slave?.fetish) {
				case "boobs":
					parts.push('holding breasts');
					break;
				case "pregnancy":
					parts.push('holding waist');
					break;
				case "cumslut":
					parts.push('holding mouth');
					break;
				case "buttslut":
					parts.push('holding ass');
					break;
				case "dom":
					parts.push('hands on hips');
					break;
			}
		}

		return parts.join(', ');
	}

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

		return undefined;
	}
};


code:
App.Art.GenAI.AccessoryPromptPart = class AccessoryPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @returns {string}
	 */
	positive() {
	/* 	let accessories = [];

		if (this.slave.armAccessory !== "none") {
			accessories.push(this.slave.armAccessory);
		}

		if (this.slave.legAccessory !== "none") {
			accessories.push(this.slave.legAccessory);
		}

		if (this.slave.bellyAccessory !== "none") {
			accessories.push(this.slave.bellyAccessory);
		}

		if (this.slave.buttplugAttachment !== "none") {
			accessories.push(this.slave.buttplugAttachment);
		}

		switch (this.slave.shoes) {
			case "none":
				accessories.push("bare feet");
				break;
			case "heels":
			case "extreme heels":
				accessories.push("high heels");
				break;
			case "pumps":
			case "flats":
				accessories.push("(female pumps shoes)");
				break;
			case "boots":
				accessories.push("high heeled knee boots");
				break;
			case "platform heels":
			case "extreme platform heels":
				accessories.push("platform heels");
				break;
			case "platform shoes":
				accessories.push("high heeled platform knee boots");
				break;

		return accessories.join(', '); */

		if (this.slave.eyewear.includes("glasses")) {
			return 'clear glasses';
		}
		return undefined;
	}

	/**
	 * @returns {string}
	 * */
	negative() {
		return undefined;
	}
	face() {
		const parts = [];
		if (this.slave.eyewear.includes("glasses")) {
			parts.push('clear glasses');
		}
		return parts.join(', ');
	}
};


code:
App.Art.GenAI.AgePromptPart = class AgePromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		let ageTags = ``;
		if (this.slave.visualAge < 10) {
			ageTags = `child`;
		} else if (this.slave.visualAge < 13) {
			ageTags = `pre-teen`;
		} else if (this.slave.visualAge < 18) {
			ageTags = `teen, teenager`;
		} else if (this.slave.visualAge < 25) {
			ageTags = `youthful adult, college age`;
		} else if (this.slave.visualAge < 40) {
			ageTags = `adult`;
		} else if (this.slave.visualAge < 60) {
			ageTags = `middle age`;
		} else {
			ageTags = `elderly`;
		}
		let grade = "";
		if (this.slave.visualAge < 18 && V.aiAgeFilter) {
			if (this.slave.visualAge < 5) {
				grade = "preschooler";
			} else if (this.slave.visualAge < 7) {
				grade = "kindergartner";
			} else {
				grade = ordinalSuffixWords(this.slave.visualAge - 6) + " grader";
			}
		}
		return `${ageTags}${this.censored ? `, ${grade}` : `, ${this.slave.visualAge} year old`}`;
	}

	/**
	 * @override
	 */
	negative() {
		if (this.slave.visualAge < 20) {
			return `${this.censored ? `school, class, 20 year old, ` : ""}elderly, adult, 30 year old, 40 year old`;
		} else if (this.slave.visualAge < 30) {
			/* empty */
		} else if (this.slave.visualAge < 40) {
			return `child, teen`;
		} else {
			return `child, young, teen, college age`;
		}
		return undefined;
	}
};


code:
App.Art.GenAI.AmputationPromptPart = class AmputationPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (isAmputee(this.slave)) {
			return this.helper.lora("amputee-000003", 1);
		}
	}

	/**
	 * @override
	 */
	negative() {
		if (isAmputee(this.slave) && this.helper.hasLora("amputee-000003")) {
			return undefined; // Space for negative prompt if needed NG
		}
		return undefined;
	}
};


code:
App.Art.GenAI.AndroidPromptPart = class AndroidPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		const parts = [];

		if (asSlave(this.slave)?.fuckdoll > 0) {
			return; // limbs covered by fuckdoll suit
		} else if (this.helper.hasLora("hololive_roboco-san") || this.helper.isPony()) {
			if (this.helper.hasLora("hololive_roboco-san") && (hasBothProstheticArms(this.slave) || hasBothProstheticLegs(this.slave))) {
				parts.push(this.helper.lora("hololive_roboco-san", 1, ", android"));
			}
			if (hasBothProstheticArms(this.slave)) {
				parts.push(`mechanical arms`);
			}
			if (hasBothProstheticLegs(this.slave)) {
				parts.push(`mechanical legs`);
			}
		}
		if (this.helper.hasLora('RobotDog0903') && isQuadrupedal(this.slave)) {
			parts.push(this.helper.lora("RobotDog0903", .8, "", "quadruped, "));
		}

		return parts.join(`, `);
	}

	/**
	 * @override
	 */
	negative() {
		if (asSlave(this.slave)?.fuckdoll > 0) {
			return; // limbs covered by fuckdoll suit
		}
		const parts = [];
		if (this.helper.hasLora("hololive_roboco-san") || this.helper.isPony()) {
			if (!hasBothProstheticArms(this.slave) && !hasBothProstheticLegs(this.slave)) {
				return; // they have no prostetics so we don't need to worry about prompt bleeding
			} else if (!hasBothProstheticArms(this.slave)) {
				parts.push(`mechanical arms`);
			} else if (!hasBothProstheticLegs(this.slave)) {
				parts.push(`mechanical legs`);
			}
		}
		return parts.join(`, `);
	}
};


code:
App.Art.GenAI.ArousalPromptPart = class ArousalPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		let prompt = {terms: [], weight: 1};
		if (this.censored) {
			if (asSlave(this.slave)?.fuckdoll > 0) {
				return undefined;
			}
			if (this.slave.energy > 60) {
				prompt.terms.push("blush");
			}
			if (this.slave.energy > 80) {
				prompt.terms.push("sweat", "heavy breathing");
			}
			if (this.slave.energy > 95) {
				prompt.weight = 1.1;
			}
		} else if (asSlave(this.slave)?.fuckdoll > 0) {
			// fuckdolls are kept in a state of permanent arousal, with genitals exposed
			if (this.slave.vagina >= 0) {
				prompt.terms.push("pussy juice");
			}
			if (this.slave.dick > 0 && canAchieveErection(this.slave)) {
				prompt.terms.push("precum, erection");
			}
		} else {
			const genitalsCovered = App.Data.clothes.get(this.slave.clothes).exposure < 3;
			if (!genitalsCovered && this.slave.vagina >= 0 && this.slave.vaginaLube === 2) {
				prompt.terms.push("pussy juice"); // no equivalent for penises
			}
			if (this.slave.energy > 60) {
				prompt.terms.push("blush");
				if (this.slave.dick > 0 && canAchieveErection(this.slave)) {
					prompt.terms.push("erection");
				} else if (this.slave.dick > 0) {
					prompt.terms.push("flaccid");
				}
			}
			if (this.slave.energy > 80) {
				prompt.terms.push("sweat", "heavy breathing");
				if (!genitalsCovered && this.slave.dick > 0) { // // NG add male precum (check gen), remove pussy mention if slave has penis
					prompt.terms.push("precum");
				} else if (!genitalsCovered && this.slave.vagina >= 0 && this.slave.vaginaLube === 1) {
					prompt.terms.push("pussy juice");
				}
			}
			if (this.slave.energy > 95) {
				prompt.weight = 1.1;
			}
		}

		if (prompt.terms) {
			if (prompt.weight !== 1) {
				return `(${prompt.terms.join(", ")}:${prompt.weight})`;
			}
			return `${prompt.terms.join(", ")}`;
		}
		return undefined;
	}

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

	/**
	 * @override
	 */
	face() {
		let prompt = {terms: [], weight: 1};

		if (asSlave(this.slave)?.fuckdoll > 0) {
			return undefined;
		}
		if (this.slave.energy > 60) {
			prompt.terms.push("blush");
		}
		if (this.slave.energy > 80) {
			prompt.terms.push("sweat", "heavy breathing");
		}
		if (this.slave.energy > 95) {
			prompt.weight = 1.1;
		}

		if (prompt.terms) {
			if (prompt.weight !== 1) {
				return `(${prompt.terms.join(", ")}:${prompt.weight})`;
			}
			return `${prompt.terms.join(", ")}`;
		}
		return undefined;
	}
};


code:
App.Art.GenAI.BeautyPromptPart = class BeautyPromptPart extends App.Art.GenAI.PromptPart {
	/**
	 * @override
	 */
	positive() {
		if (asSlave(this.slave)?.fuckdoll > 0) {
			return undefined; // face not visible
		}

		if (this.slave.face < -95) {
			return "ugly";
		} else if (this.slave.face < -50) {
			return "unattractive";
		} else if (this.slave.face < 10) { /* empty */ } else if (this.slave.face < 50) {
			return "attractive";
		} else if (this.slave.face < 95) {
			if (geneToGender(this.slave.genes) === "XY") {
				return "handsome";
			} else {
				return "beautiful";
			}
		} else {
			return "gorgeous";
		}
	}

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