code:
/**
 * Returns a block that describes an overview of the slave's impact on your reputation and finances in the short term
 * @param {App.Entity.SlaveState} slave
 * @returns {DocumentFragment}
 */
globalThis.slaveExpenses = function(slave) {
	const frag = new DocumentFragment();
	let div;

	// Generate the lines of the report
	for (const line of getSlaveCostArray(slave)) {
		div = document.createElement("div");
		div.classList.add("indent");
		App.UI.DOM.appendNewElement("span", div, `${line.text}: `);
		App.UI.DOM.appendNewElement("span", div, cashFormat(line.value), "cash");
		frag.append(div);
	}

	// Total expenses
	const individualCosts = getSlaveCost(slave);
	div = document.createElement("div");
	div.classList.add("double-indent");

	App.UI.DOM.appendNewElement("span", div, "Predicted expense", "underline");
	div.append(": ");
	div.append(App.UI.DOM.cashFormat(-individualCosts));
	frag.append(div);

	// Income from last week vs expense gives net, assistant gated
	if (V.assistant.power > 0) {
		if (slave.lastWeeksCashIncome > 0) {
			div = document.createElement("div");
			div.classList.add("indent");
			App.UI.DOM.appendNewElement("span", div, `Income: `);
			div.append(App.UI.DOM.cashFormat(slave.lastWeeksCashIncome));
			frag.append(div);

			div = document.createElement("div");
			div.classList.add("double-indent");
			App.UI.DOM.appendNewElement("span", div, `Total`, "underline");
			App.UI.DOM.appendNewElement("span", div, `: `);
			div.append(App.UI.DOM.cashFormat(slave.lastWeeksCashIncome - individualCosts));
			frag.append(div);
		}
	}
	frag.append(slaveImpactLongTerm(slave));

	return frag;
};

/**
 * Returns a block that describes an overview of the slave's impact on your reputation and finances over the long term
 * @param {App.Entity.SlaveState} slave
 * @returns {DocumentFragment}
 */
globalThis.slaveImpactLongTerm = function(slave) {
	const frag = new DocumentFragment();
	const {he, his, him} = getPronouns(slave);
	let p;
	let div;
	let span;
	let cost;
	let expense;
	let income;
	let text;

	// Background and original cost
	if (slave.origin !== "") {
		App.UI.DOM.appendNewElement("div", frag, pronounsForSlaveProp(slave, slave.origin), "indent");
	}
	div = document.createElement("div");
	div.classList.add("indent");
	if (slave.slaveCost < 0) {
		const costSpan = App.UI.DOM.cashFormat(slave.slaveCost);
		costSpan.textContent += `.`;
		div.append(`You bought ${him} for `, costSpan);
		cost = slave.slaveCost;
	} else if (slave.slaveCost === 0) {
		div.append(`You spent nothing to acquire ${him}.`);
		cost = slave.slaveCost;
	} else {
		text = `You have no record of how much `;
		if (slave.origin !== "") {
			text += `this`;
		} else {
			text += `${he} originally`;
		}
		text += ` cost.`;
		div.append(text);
		cost = 0;
	}
	frag.append(div);

	// Lifetime totals, assistant gated
	if (V.assistant.power > 0) {
		const weeksOwned = V.week - slave.weekAcquired;
		// Lifetime expense
		div = document.createElement("div");
		div.classList.add("indent");
		if (slave.lifetimeCashExpenses < 0) {
			text = `In ${his} `;
			if (weeksOwned > 0) {
				text += `${weeksOwned} `;
				if (weeksOwned === 1) {
					text += `week`;
				} else {
					text += `weeks`;
				}
			} else {
				text += `time`;
			}
			text += ` with you, ${he} has cost `;
			const cash = (App.UI.DOM.cashFormat(slave.lifetimeCashExpenses));
			cash.textContent += `.`;
			div.append(text, cash);
			expense = slave.lifetimeCashExpenses;
		} else {
			div.append(`You have no record of ${him} costing you any ¤.`);
			expense = 0;
		}
		frag.append(div);

		// Lifetime income
		div = document.createElement("div");
		div.classList.add("indent");
		if (slave.lifetimeCashIncome > 0) {
			text = `In ${his} `;
			if (weeksOwned > 0) {
				text += `${weeksOwned} `;
				if (weeksOwned === 1) {
					text += `week`;
				} else {
					text += `weeks`;
				}
			} else {
				text += `time`;
			}
			text += ` with you, ${he} has earned `;
			const cash = App.UI.DOM.cashFormat(slave.lifetimeCashIncome);
			cash.textContent += `.`;
			div.append(text, cash);
			income = slave.lifetimeCashIncome;
		} else {
			div.append(`You have no record of ${him} making you any ¤.`);
			income = 0;
		}
		frag.append(div);

		// Lifetime total
		div = document.createElement("div");
		div.classList.add("indent");
		div.append(`Overall, `);
		if ((income + cost + expense) > 0) {
			div.append(`you have made `);
			div.append(App.UI.DOM.cashFormat(income + cost + expense));
			div.append(` net from ${his} labors.`);
		} else if ((income + cost + expense) === 0) {
			div.append(`${he} is even in cost and income.`);
		} else {
			div.append(`${he} has cost you `);
			const cash = App.UI.DOM.cashFormat(income + cost + expense);
			cash.textContent += `.`;
			div.append(cash);
		}
		frag.append(div);

		// Reputation:
		p = document.createElement("p");
		if (slave.lifetimeRepIncome !== 0 || slave.lifetimeRepExpenses !== 0) {
			let repTotal = (slave.lifetimeRepIncome + slave.lifetimeRepExpenses);
			if (V.cheatMode === 1 || V.debugMode === 1) {
				if (slave.lifetimeRepIncome !== 0) {
					div = document.createElement("div");
					div.classList.add("indent");
					div.append(`This week ${he} has `);
					if (slave.lastWeeksRepIncome === 0) {
						div.append(`had no positive effect on your reputation, but `);
					} else {
						div.append(`improved your reputation by `);
						App.UI.DOM.appendNewElement("span", div, num(slave.lastWeeksRepIncome), "green");
						div.append(` and `);
					}
					div.append(`over ${his} entire time with you, ${he} gained `);
					App.UI.DOM.appendNewElement("span", div, `${num(slave.lifetimeRepIncome)}.`, "green");
					p.append(div);
				}

				if (slave.lifetimeRepExpenses !== 0) {
					div = document.createElement("div");
					div.classList.add("indent");
					div.append(`This week ${he} has `);
					if (slave.lastWeeksRepExpenses === 0) {
						div.append(`had no negative effect on your reputation, but `);
					} else {
						div.append(`lowered your reputation by `);
						App.UI.DOM.appendNewElement("span", div, slave.lastWeeksRepExpenses.toString(), "red");
						div.append(` and `);
					}
					div.append(`over ${his} entire time with you, ${he} cost `);
					App.UI.DOM.appendNewElement("span", div, `${slave.lifetimeRepExpenses}.`, "red");
					p.append(div);
				}

				div = document.createElement("div");
				div.classList.add("indent");
				div.append(`Overall then, ${he} has changed your reputation by `);
				span = document.createElement("span");
				if (repTotal > 0) {
					span.classList.add("green");
				} else if (repTotal < 0) {
					span.classList.add("red");
				}
				span.append(num(repTotal));
				div.append(span);
				p.append(div);
			} else {
				/* lowercasedonkey: TODO: I don't like how vague my placeholder is. Probably need to set up some kind of sliding scale to describe how much rep (roughly) she has made or lost. Need to get a sense of common ranges. */
				div = document.createElement("div");
				div.classList.add("indent");
				div.append(`Overall, ${he} has `);
				if (repTotal === 0) {
					div.append(`had no impact on `);
				} else if (repTotal > 0) {
					App.UI.DOM.appendNewElement("span", div, `increased `, "green");
				} else if (repTotal < 0) {
					App.UI.DOM.appendNewElement("span", div, `decreased `, "red");
				}
				div.append(`your reputation.`);
				p.append(div);
			}
		}
		frag.append(p);
	}
	return frag;
};

repo_name: DontAskDontTell/fc

license: mit


code:
/**
 * @file Functions for rendering lists of slave summaries for various purposes. This includes
 * lists for the penthouse/facilities, selecting a slaves, facility leaders.
 *
 * For documentation see devNotes/slaveListing.md
 */

App.UI.SlaveList = {};

/**
 * @callback slaveTextGenerator
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */

/**
 * @callback slaveToElement
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLElement|DocumentFragment}
 */

App.UI.SlaveList.render = function() {
	const facilityPassages = new Set(
		["Main", "Head Girl Suite", "Spa", "Brothel", "Club", "Arcade", "Clinic", "Schoolroom", "Dairy", "Farmyard", "Servants' Quarters", "Master Suite", "Cellblock"]);

	/** @type {string} */
	let passageName;

	// potentially can be a problem if played long enough to reach Number.MAX_SAFE_INTEGER
	let listID = Number.MIN_SAFE_INTEGER;

	/** @type {App.Art.SlaveArtBatch} */
	let batchRenderer = null;

	return listDOM;

	/**
	 * @param {number[]} IDs
	 * @param {Array.<{id: number, rejects: string[]}>} rejectedSlaves
	 * @param {slaveToElement} interactionLink
	 * @param {slaveToElement} [postNote]
	 * @returns {DocumentFragment}
	 */
	function listDOM(IDs, rejectedSlaves, interactionLink, postNote) {
		passageName = passage();

		let res = document.createDocumentFragment();

		if ((V.seeImages === 1) && (V.seeSummaryImages === 1)) {
			batchRenderer = new App.Art.SlaveArtBatch(IDs, 1);
			res.appendChild(batchRenderer.writePreamble());
		} else {
			batchRenderer = null;
		}

		if (V.useSlaveListInPageJSNavigation === 1) {
			res.appendChild(createQuickList(IDs));
		}

		const fcs = App.Entity.facilities;
		const penthouse = fcs.penthouse;

		let anyFacilityExists = false;

		for (const f of Object.values(fcs)) {
			if (f !== penthouse && f.established) {
				anyFacilityExists = true;
				break;
			}
		}

		let showTransfers = false;
		if (anyFacilityExists) {
			if (facilityPassages.has(passageName)) {
				V.returnTo = passageName;
				showTransfers = true;
			}
		}

		for (const sid of IDs) {
			let ss = renderSlave(sid, interactionLink, showTransfers, postNote);
			let slaveDiv = document.createElement("div");
			slaveDiv.id = `slave-${sid}`;
			slaveDiv.classList.add("slaveSummary");
			if (V.slavePanelStyle === 2) {
				slaveDiv.classList.add("card");
			}
			slaveDiv.appendChild(ss);
			res.appendChild(slaveDiv);
		}

		for (const rs of rejectedSlaves) {
			const slave = slaveStateById(rs.id);
			const rejects = rs.rejects;
			const slaveName = SlaveFullName(slave);
			let slaveDiv = document.createElement("div");
			slaveDiv.id = `slave-${slave.ID}`;
			slaveDiv.style.setProperty("clear", "both");
			if (rejects.length === 1) {
				slaveDiv.innerHTML = rejects[0];
			} else {
				slaveDiv.appendChild(document.createTextNode(`${slaveName}: `));
				let ul = document.createElement("ul");
				for (const rr of rejects) {
					const li = document.createElement("li");
					li.innerHTML = rr;
					ul.appendChild(li);
				}
				slaveDiv.appendChild(ul);
			}
			res.appendChild(slaveDiv);
		}

		batchRenderer = null; // release reference

		return res;
	}

	/**
	 * @param {number} id
	 * @param {slaveToElement} interactionLink
	 * @param {boolean} showTransfers
	 * @param {slaveToElement} [postNote]
	 * @returns {DocumentFragment}
	 */
	function renderSlave(id, interactionLink, showTransfers, postNote) {
		let res = document.createDocumentFragment();
		if (V.slavePanelStyle === 0) {
			res.appendChild(document.createElement("br"));
		} else if (V.slavePanelStyle === 1) {
			const hr = document.createElement("hr");
			hr.style.margin = "0";
			res.appendChild(hr);
		}
		const slave = slaveStateById(id);

		if (batchRenderer && (!V.seeCustomImagesOnly || (V.seeCustomImagesOnly && slave.custom.image))) {
			let imgDiv = document.createElement("div");
			imgDiv.classList.add("imageRef", "smlImg", "margin-right");
			imgDiv.appendChild(batchRenderer.render(slave));
			res.appendChild(imgDiv);
		}
		// res.push(dividerAndImage(slave));
		res.appendChild(interactionLink(slave));

		SlaveStatClamp(slave);
		slave.trust = Math.trunc(slave.trust);
		slave.devotion = Math.trunc(slave.devotion);
		slave.health.condition = Math.trunc(slave.health.condition);
		slave.health.shortDamage = Math.trunc(slave.health.shortDamage);
		slave.health.longDamage = Math.trunc(slave.health.longDamage);
		slave.health.illness = Math.trunc(slave.health.illness);
		slave.health.tired = Math.trunc(slave.health.tired);
		slave.health.health = Math.trunc(slave.health.health);
		res.appendChild(document.createTextNode(' will '));
		const assignment = document.createElement("span");
		if ((slave.assignment === Job.REST) && (slave.health.condition >= -20)) {
			assignment.className = "freeAssignment";
			assignment.innerText = slave.assignment;
		} else if (slave.choosesOwnAssignment === 1) {
			assignment.innerText = `choose ${getPronouns(slave).possessive} own job`;
		} else {
			assignment.innerText = slave.assignment;
			if (slave.assignment === Job.CLASSES) {
				const role = tutorForSlave(slave);
				if (role) {
					assignment.innerText += ` on being a ${role}`;
				}
			}
			if (slave.sentence > 0) {
				assignment.innerText += ` for ${years(slave.sentence)}`;
			}
		}
		if (slave.assignment === Job.CLINIC) {
			let list = [];
			if (slave.health.condition <= 40) {
				list.push(`poor health`);
			}
			if (slave.health.shortDamage >= 10) {
				list.push(`injuries`);
			}
			if (slave.health.illness > 0) {
				list.push(`illness`);
			}
			if (S.Nurse) {
				if ((slave.chem > 15) && (V.clinicUpgradeFilters === 1)) {
					list.push(`unhealthy chemicals`);
				}
				if ((V.clinicInflateBelly > 0) && (slave.bellyImplant >= 0) && (slave.bellyImplant <= (V.arcologies[0].FSTransformationFetishistResearch ? 800000 : 130000))) {
					list.push(`implant filling`);
				}
				if ((slave.pregKnown === 1) && (V.clinicSpeedGestation > 0 || slave.pregControl === "speed up") && (slave.pregAdaptation * 1000 < slave.bellyPreg || slave.preg > slave.pregData.normalBirth / 1.33)) {
					list.push(`observation of accelerated pregnancy`);
				} else if ((slave.pregKnown === 1) && (V.clinicSpeedGestation > 0 || slave.pregControl === "speed up")) {
					list.push(`safely hurrying pregnancy along`);
				} else if (slave.pregAdaptation * 1000 < slave.bellyPreg || slave.preg > slave.pregData.normalBirth / 1.33) {
					list.push(`observation during pregnancy`);
				}
			}
			if (list.length > 0) {
				assignment.innerText += ` for ${toSentence(list)}`;
			} else {
				assignment.innerText += ", preparing to check out";
			}
		} else if (slave.assignment === Job.SPA) {
			let list = [];
			if (slave.fetish === Fetish.MINDBROKEN) {
				assignment.innerText += `, mindbroken`;
			} else {
				if ((slave.sexualFlaw !== "none") || (slave.behavioralFlaw !== "none")) {
					list.push(`overcoming flaws`);
				}
				if ((slave.trust < 60) || (slave.devotion < 60)) {
					list.push(`learning to accept life as a slave`);
				}
				if (slave.health.condition < 20) {
					list.push(`improving in health`);
				}
				if (list.length > 0) {
					assignment.innerText += `, ${toSentence(list)}`;
				} else {
					assignment.innerText += ", preparing to move out";
				}
			}
		} else if (slave.assignment === Job.SCHOOL) {
			let lessons = [];
			if (V.schoolroomRemodelBimbo === 1 && slave.intelligenceImplant > -15) {
				lessons.push("being a dumb bimbo");
			} else if (V.schoolroomRemodelBimbo === 0 && slave.intelligenceImplant < 30) {
				lessons.push("general education");
			}
			if (!((slave.voice === 0) || (slave.accent <= 1) || ((V.schoolroomUpgradeLanguage === 0) && (slave.accent <= 2)))) {
				lessons.push("speech");
			}
			if (!((slave.skill.oral > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.oral > 10)))) {
				lessons.push("oral");
			}
			if (!((slave.skill.whoring > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.whoring > 10)))) {
				lessons.push("whoring");
			}
			if (!((slave.skill.entertainment > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.entertainment > 10)))) {
				lessons.push("entertainment");
			}
			if (!((slave.skill.anal > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.anal > 10)))) {
				lessons.push("anal");
			}
			if (!((slave.skill.vaginal > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.vaginal > 10)) || (slave.vagina < 0))) {
				lessons.push("vaginal");
			}
			const role = tutorForSlave(slave);
			if (role && needsTutoring(slave)) {
				lessons.push(`being a good ${role}`);
			}
			if (lessons.length > 0) {
				assignment.innerText += `, practicing ${toSentence(lessons)}`;
			} else {
				assignment.innerText += ", studying for finals";
			}
		} else if (slave.assignment === Job.SUBORDINATE) {
			if (slave.subTarget === -1) {
				assignment.innerText += ", serving as your Stud";
			} else if (slave.subTarget !== 0) {
				const domSlave = getSlave(slave.subTarget);
				if (domSlave) {
					assignment.innerText += ", serving " + SlaveFullName(domSlave) + " exclusively";
				} else {
					slave.subTarget = 0;
				}
			}
		} else if (slave.assignment === Job.AGENT) {
			const arc = V.arcologies.find((a) => a.leaderID === slave.ID);
			if (arc) {
				assignment.innerText += `, leading `;
				if (passageName === "Neighbor Interact") {
					assignment.appendChild(App.UI.DOM.makeElement("span", arc.name, "name"));
				} else {
					assignment.appendChild(App.UI.DOM.passageLink(arc.name, "Neighbor Interact"));
				}
			}
		} else if (slave.assignment === Job.AGENTPARTNER) {
			const arc = V.arcologies.find((a) => a.leaderID === slave.relationshipTarget);
			if (arc) {
				assignment.innerText += ` in `;
				if (passageName === "Neighbor Interact") {
					assignment.appendChild(App.UI.DOM.makeElement("span", arc.name, "name"));
				} else {
					assignment.appendChild(App.UI.DOM.passageLink(arc.name, "Neighbor Interact"));
				}
			}
		}
		assignment.appendChild(document.createTextNode('.'));
		res.appendChild(assignment);
		if (V.assignmentRecords[slave.ID]) {
			res.appendChild(document.createTextNode(` Last assigned to ${V.assignmentRecords[slave.ID]}.`));
		}
		res.appendChild(document.createTextNode(' '));

		if ((V.displayAssignments === 1) && (passageName === "Main") && (slave.ID !== V.HeadGirlID) && (slave.ID !== V.RecruiterID) && (slave.ID !== V.BodyguardID)) {
			res.appendChild(App.UI.jobLinks.assignmentsFragment(slave.ID, "Main", (slave, assignment) => {
				App.UI.SlaveList.ScrollPosition.record();
				assignJob(slave, assignment);
			}));
		}
		if (showTransfers) {
			res.appendChild(document.createElement("br"));
			res.appendChild(document.createTextNode('Transfer to: '));
			res.appendChild(App.UI.jobLinks.transfersFragment(slave.ID, (slave, assignment) => {
				App.UI.SlaveList.ScrollPosition.record();
				assignJob(slave, assignment);
			}));
		}

		res.appendChild(App.UI.SlaveSummary.render(slave));

		if (postNote) {
			const pn = postNote(slave);
			if (pn) {
				let r = document.createElement("p");
				r.classList.add("si");
				r.appendChild(pn);
				res.appendChild(r);
			}
		}

		return res;
	}

	/**
	 * @param {number[]} IDs
	 * @returns {DocumentFragment}
	 */
	function createQuickList(IDs) {
		/**
		 *
		 * @param {Node} container
		 * @param {string} tagName
		 * @param {string} [content]
		 * @param {string|string[]} [classNames]
		 * @param {Object.<string, any>} [attributes]
		 * @returns {HTMLElement}
		 */
		function makeElement(container, tagName, content, classNames, attributes) {
			let res = document.createElement(tagName);
			container.appendChild(res);
			if (content) {
				res.textContent = content;
			}
			if (Array.isArray(classNames)) {
				for (const c of classNames) {
					res.classList.add(c);
				}
			} else if (classNames !== undefined) {
				res.classList.add(classNames);
			}

			if (attributes) {
				for (const [k, v] of Object.entries(attributes)) {
					res.setAttribute(k, v);
				}
			}
			return res;
		}

		const res = document.createDocumentFragment();

		/* Useful for finding weird combinations — usages of this passage that don't yet generate the quick index.
		*	<<print 'pass/count/indexed/flag::[' + passageName + '/' + Count + '/' + indexed + '/' + V.SlaveSummaryFiler + ']'>>
		*/

		if (IDs.length > 1 && passageName === "Main") {
			const buttons = [];
			let offset = -50;
			if (/Select/i.test(passageName)) {
				offset = -25;
			}
			/*
			 * we want <button data-quick-index="<<= listID>>">...
			 */
			const quickIndexBtn = document.createElement("button");
			res.appendChild(quickIndexBtn);
			quickIndexBtn.id = `quick-list-toggle${listID}`;
			quickIndexBtn.setAttribute('data-quick-index', listID.toString());
			quickIndexBtn.onclick = function(ev) {
				let which = /** @type {HTMLElement} */ (ev.target).attributes["data-quick-index"].value;
				let quick = $("div#list_index" + which);
				quick.toggleClass("ql-hidden");
			};
			/*
			 * we want <div id="list_index3" class=" hidden">...
			 */
			const listIndex = makeElement(res, "div", undefined, "ql-hidden");
			listIndex.id = `list_index${listID}`;

			for (const sID of IDs) {
				const IndexSlave = slaveStateById(sID);
				const indexSlaveName = SlaveFullName(IndexSlave);
				const devotionClass = getSlaveDevotionClass(IndexSlave);
				const trustClass = getSlaveTrustClass(IndexSlave);
				buttons.push({
					"data-name": indexSlaveName,
					"data-scroll-to": `#slave-${IndexSlave.ID}`,
					"data-scroll-offset": offset,
					"data-devotion": IndexSlave.devotion,
					"data-trust": IndexSlave.trust,
					"class": `${devotionClass} ${trustClass}`
				});
			}
			if (buttons.length > 0) {
				V.sortQuickList = V.sortQuickList || 'Devotion';
				makeElement(listIndex, "em", "Sorting: ");
				const qlSort = makeElement(listIndex, "span", V.sortQuickList, "strong");
				qlSort.id = "qlSort";
				listIndex.appendChild(document.createTextNode(". "));
				const linkSortByDevotion = makeElement(listIndex, "a", "Sort by Devotion");
				linkSortByDevotion.onclick = (ev) => {
					ev.preventDefault();
					V.sortQuickList = "Devotion";
					$("#qlSort").text(V.sortQuickList);
					$("#qlWrapper").removeClass("trust").addClass("devotion");
					sortButtonsByDevotion();
				};
				listIndex.append(" | ");
				const linkSortByTrust = makeElement(listIndex, "a", "Sort by Trust");
				linkSortByTrust.onclick = (ev) => {
					ev.preventDefault();
					V.sortQuickList = "Trust";
					$("#qlSort").text(V.sortQuickList);
					$("#qlWrapper").removeClass("devotion").addClass("trust");
					sortButtonsByTrust();
				};
				makeElement(listIndex, "br");
				const qlWrapper = makeElement(listIndex, "div", undefined, ["quick-list", "devotion"]);
				qlWrapper.id = "qlWrapper";
				for (const button of buttons) {
					const btn = makeElement(listIndex, 'button', button['data-name'], undefined, button);
					btn.onclick = App.UI.quickBtnScrollToHandler;
				}
			}
		}
		return res;
	}
}();

App.UI.SlaveList.Decoration = {};
/**
 * returns "HG", "BG", "PA", and "RC" prefixes
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLElement}
 */
App.UI.SlaveList.Decoration.penthousePositions = function(slave) {
	const fcs = App.Entity.facilities;
	if (fcs.headGirlSuite.manager.isEmployed(slave)) {
		return App.UI.DOM.makeElement("span", 'HG', ['lightcoral', 'strong']);
	}
	if (fcs.penthouse.manager.isEmployed(slave)) {
		return App.UI.DOM.makeElement("span", 'RC', ['lightcoral', 'strong']);
	}
	if (fcs.armory.manager.isEmployed(slave)) {
		return App.UI.DOM.makeElement("span", 'BG', ['lightcoral', 'strong']);
	}
	return App.UI.SlaveList.Decoration.personalAttention(slave);
};

/**
 * returns just the "PA" prefix
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLElement}
 */
App.UI.SlaveList.Decoration.personalAttention = function(slave) {
	if (V.personalAttention.task === PersonalAttention.TRAINING && V.personalAttention.slaves.some(s => s.ID === slave.ID)) {
		return App.UI.DOM.makeElement("span", 'PA', ['lightcoral', 'strong']);
	}
	return null;
};

App.UI.SlaveList.ScrollPosition = (function() {
	let lastPassage = null;
	let position = 0;

	return {
		reset: function() {
			lastPassage = null;
			position = 0;
		},

		record: function() {
			lastPassage = passage();
			position = window.pageYOffset;
		},

		restore: function() {
			$(document).one(':passageend', () => {
				if (lastPassage === passage()) {
					window.scrollTo(0, position);
				}
				this.reset();
			});
		}
	};
})();

App.UI.SlaveList.SlaveInteract = {};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} [text] print this text instead of slave name
 * @returns {DocumentFragment|HTMLElement}
 */
App.UI.SlaveList.SlaveInteract.stdInteract = function(slave, text) {
	const link = App.UI.DOM.passageLink(text ? text : SlaveFullName(slave), "Slave Interact", () => {
		App.UI.SlaveList.ScrollPosition.record();
		V.AS = slave.ID;
	});

	if (V.favorites.includes(slave.ID)) {
		return App.UI.DOM.combineNodes(
			App.UI.DOM.makeElement("span", String.fromCharCode(0xe800), ["icons", "favorite"]),
			" ", link);
	}

	return link;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {slaveToElement} decoratorFunction
 * @returns {DocumentFragment|HTMLElement}
 */
App.UI.SlaveList.SlaveInteract.decoratedInteract = function(slave, decoratorFunction) {
	const decoration = decoratorFunction ? decoratorFunction(slave) : null;
	const stdLink = App.UI.SlaveList.SlaveInteract.stdInteract(slave);
	if (decoration) {
		const fr = document.createDocumentFragment();
		fr.appendChild(decoration);
		fr.appendChild(document.createTextNode(' '));
		fr.appendChild(stdLink);
		return fr;
	}
	return stdLink;
};

/**
 * @param {string} passage
 * @returns {HTMLElement}
 */
App.UI.SlaveList.sortingLinks = function(passage) {
	const div = document.createElement("div");
	const textify = string => capFirstChar(string.replace(/([A-Z])/g, " $1"));

	let span = App.UI.DOM.makeElement("span", "Sort by: ");
	let order = ["devotion", "trust", "name", "assignment", "seniority", "actualAge", "visualAge", "physicalAge", "weeklyIncome", "beauty", "health", "weight", "muscles", "intelligence", "sexDrive", "pregnancy", "prestige"];
	const orderMap = order.map(so => {
		return {key: so, name: capFirstChar(so)};
	});

	div.append(
		span,
		App.UI.DOM.makeSelect(orderMap, V.sortSlavesBy, val => {
			V.sortSlavesBy = val;
			App.UI.reload();
		}),
	);

	span = App.UI.DOM.makeElement("span", " Sort direction: ");
	order = ["descending", "ascending"];
	span.append(App.UI.DOM.generateLinksStrip(order.map(so => V.sortSlavesOrder !== so
		? App.UI.DOM.passageLink(textify(so), passage, () => { V.sortSlavesOrder = so; }) : textify(so))));
	div.append(span);

	return div;
};

/**
 * Standard tabs for a facility with a single job (SJ)
 * @param {App.Entity.Facilities.Facility} facility
 * @param {string} [facilityPassage]
 * @param {boolean} [showTransfersTab=false]
 * @param {{assign: string, remove: string, transfer: (string| undefined)}} [tabCaptions]
 * @param {slaveToElement} [decoratorFunction]
 * @param {slaveToElement} [postNote]
 * @returns {DocumentFragment}
 */
App.UI.SlaveList.listSJFacilitySlaves = function(facility, facilityPassage, showTransfersTab = false, tabCaptions = undefined, decoratorFunction = undefined, postNote = undefined) {
	return App.UI.SlaveList.listJFacilitySlaves(facility.desc.defaultJob, facility, facilityPassage, showTransfersTab, tabCaptions, decoratorFunction, postNote);
};

/**
 * Standard tabs for a facility with a given job (J)
 * @param {string} jobName
 * @param {App.Entity.Facilities.Facility} facility
 * @param {string} [facilityPassage]
 * @param {boolean} [showTransfersTab=false]
 * @param {{assign: string, remove: string, transfer: (string| undefined)}} [tabCaptions]
 * @param {slaveToElement} [decoratorFunction]
 * @param {slaveToElement} [postNote]
 * @returns {DocumentFragment}
 */
App.UI.SlaveList.listJFacilitySlaves = function(jobName, facility, facilityPassage, showTransfersTab = false, tabCaptions = undefined, decoratorFunction = undefined, postNote = undefined) {
	const job = facility.job(jobName);

	facilityPassage = facilityPassage || passage();
	tabCaptions = tabCaptions || {
		assign: 'Assign a slave',
		remove: 'Remove a slave',
		transfer: 'Transfer from Facility',
	};
	const frag = document.createDocumentFragment();
	if (V.sortSlavesMain) {
		frag.append(this.sortingLinks(facilityPassage));
	}

	const tabBar = new App.UI.Tabs.TabBar("SlaveListing");

	if (facility.hasFreeSpace) {
		const assignableSlaveIDs = job.desc.partTime
			? V.slaves.map(slave => slave.ID) // all slaves can work here
			: [...App.Entity.facilities.penthouse.employeesIDs()]; // only slaves from the penthouse can be transferred here
		tabBar.addTab(tabCaptions.assign, "assign", assignableTabContent(assignableSlaveIDs));
	} else {
		tabBar.addTab(tabCaptions.assign, "assign", App.UI.DOM.makeElement("strong", `${capFirstChar(facility.name)} is full and cannot hold any more slaves`));
	}

	const facilitySlaves = [...job.employeesIDs()];
	if (facilitySlaves.length > 0) {
		SlaveSort.IDs(facilitySlaves);
		tabBar.addTab(tabCaptions.remove, "remove", App.UI.SlaveList.render(facilitySlaves, [],
			(slave) => App.UI.SlaveList.SlaveInteract.decoratedInteract(slave, decoratorFunction),
			(slave) => {
				const frag = new DocumentFragment();
				if (postNote) {
					frag.append(postNote(slave));
				}
				frag.append(App.UI.DOM.link(`Retrieve ${getPronouns(slave).object} from ${facility.name}`, () => {
					removeJob(slave, job.desc.assignment);
				}, [], facilityPassage));
				return frag;
			}
		));
	} else {
		tabBar.addTab(tabCaptions.remove, "remove", App.UI.DOM.makeElement("em", `${capFirstChar(facility.name)} is empty for the moment`));
	}

	if (showTransfersTab) {
		if (facility.hasFreeSpace) {
			// slaves from other facilities can be transferred here
			const transferableIDs = V.slaves.reduce((acc, slave) => {
				if (!assignmentVisible(slave) && !facility.isHosted(slave)) {
					acc.push(slave.ID);
				}
				return acc;
			}, []);
			tabBar.addTab(tabCaptions.transfer, "transfer", assignableTabContent(transferableIDs));
		} else {
			tabBar.addTab(tabCaptions.transfer, "transfer", App.UI.DOM.makeElement("strong", `${capFirstChar(facility.name)} is full and cannot hold any more slaves`));
		}
	}

	frag.append(tabBar.render());

	return frag;

	/**
	 * @param {number[]} slaveIDs
	 * @returns {DocumentFragment}
	 */
	function assignableTabContent(slaveIDs) {
		SlaveSort.IDs(slaveIDs);
		let rejectedSlaves = [];
		let passedSlaves = [];
		slaveIDs.forEach((id) => {
			const rejects = facility.canHostSlave(slaveStateById(id), jobName);
			if (rejects.length > 0) {
				rejectedSlaves.push({id: id, rejects: rejects});
			} else {
				passedSlaves.push(id);
			}
		}, []);
		return App.UI.SlaveList.render(passedSlaves, rejectedSlaves,
			(slave) => App.UI.SlaveList.SlaveInteract.decoratedInteract(slave, decoratorFunction),
			(slave) => {
				const frag = new DocumentFragment();
				if (postNote) {
					frag.append(postNote(slave));
				}
				frag.append(App.UI.DOM.link(`Send ${getPronouns(slave).object} to ${facility.name}`, () => {
					assignmentTransition(slave, job.desc.assignment, facilityPassage);
				}));
				return frag;
			});
	}
};

/**
 * @param {string[]} classNames
 */
App.UI.SlaveList.makeNameDecorator = function(classNames) {
	return (slave) => {
		const r = document.createElement("span");
		for (const c of classNames) {
			r.classList.add(c);
		}
		r.textContent = SlaveFullName(slave);
		return r;
	};
};

/**
 * @returns {DocumentFragment}
 */
App.UI.SlaveList.listNGPSlaves = function() {
	const thisPassage = 'New Game Plus';

	const frag = document.createDocumentFragment();
	frag.append(this.sortingLinks(thisPassage));
	const tabBar = new App.UI.Tabs.TabBar("NewGamePlus");

	let imported = [];
	let nonImported = [];
	for (const slave of V.slaves) {
		// @ts-ignore: handle the legacy assignment string
		if (slave.assignment === "be imported") {
			slave.assignment = Job.IMPORTED;
		}
		if (slave.assignment === Job.IMPORTED) {
			imported.push(slave.ID);
		} else {
			nonImported.push(slave.ID);
		}
	}

	if (imported.length < V.slavesToImportMax) {
		SlaveSort.IDs(nonImported);
		tabBar.addTab("Import a slave", "assign", App.UI.SlaveList.render(nonImported, [],
			App.UI.SlaveList.makeNameDecorator(["emphasizedSlave", "pink"]),
			(s) => App.UI.DOM.passageLink('Add to import list', thisPassage, () => assignJob(s, Job.IMPORTED))
		));
	} else {
		tabBar.addTab("Import a slave", "assign", App.UI.DOM.makeElement('strong', `Slave import limit reached`));
	}

	if (imported.length > 0) {
		SlaveSort.IDs(imported);
		tabBar.addTab("Remove from import", "remove", App.UI.SlaveList.render(imported, [],
			App.UI.SlaveList.makeNameDecorator(["emphasizedSlave", "pink"]),
			(s) => App.UI.DOM.passageLink('Remove from import list', thisPassage, () => removeJob(s, Job.IMPORTED))
		));
	} else {
		tabBar.addTab("Remove from import", "remove", App.UI.DOM.makeElement('em', "No slaves will go with you to the new game"));
	}

	frag.append(tabBar.render());

	return frag;
};

/**
 * Renders facility manager summary or a note with a link to select one
 * @param {App.Entity.Facilities.Facility} facility
 * @param {string} [selectionPassage] passage name for manager selection. "${Manager} Select" if omitted
 * @returns {DocumentFragment}
 */
App.UI.SlaveList.displayManager = function(facility, selectionPassage) {
	const managerCapName = capFirstChar(facility.desc.manager.position);
	selectionPassage = selectionPassage || `${managerCapName} Select`;
	const manager = facility.manager.currentEmployee;
	if (manager) {
		return this.render([manager.ID], [],
			App.UI.SlaveList.SlaveInteract.stdInteract,
			() => App.UI.DOM.passageLink(`Change or remove ${managerCapName}`, selectionPassage));
	} else {
		const frag = document.createDocumentFragment();
		frag.append(`You do not have a slave serving as a ${managerCapName}. `, App.UI.DOM.passageLink(`Appoint one`, selectionPassage));
		return frag;
	}
};

/**
 * Displays standard facility page with manager and list of workers
 * @param {App.Entity.Facilities.Facility} facility
 * @param {boolean} [showTransfersPage]
 * @returns {DocumentFragment}
 */
App.UI.SlaveList.stdFacilityPage = function(facility, showTransfersPage) {
	let frag = new DocumentFragment();
	if (facility.manager) {
		frag = this.displayManager(facility);
	}
	frag.append(this.listSJFacilitySlaves(facility, passage(), showTransfersPage));
	return frag;
};

App.UI.SlaveList.penthousePage = function() {
	const ph = App.Entity.facilities.penthouse;
	/** @type {slaveToElement} */
	const interactRenderer = (slave) =>
		App.UI.SlaveList.SlaveInteract.decoratedInteract(slave, App.UI.SlaveList.Decoration.penthousePositions);

	function overviewTabContent() {
		const fragment = document.createDocumentFragment();
		const A = V.arcologies[0];

		let slaveWrapper = App.UI.DOM.makeElement("div", null, ['margin-top']);
		if (V.HeadGirlID) {
			const HG = S.HeadGirl;
			slaveWrapper.append(App.UI.DOM.makeElement("span", SlaveFullName(HG), "slave-name"),
				" is serving as your Head Girl");
			if (A.FSEgyptianRevivalistLaw === 1) {
				slaveWrapper.append(" and Consort");
			}
			slaveWrapper.append(". ");
			const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Head Girl", "Head Girl Select"), "major-link");
			link.id = "manageHG";
			slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Head Girl Select"), "hotkey"));
			slaveWrapper.append(App.UI.SlaveList.render([HG.ID], [], interactRenderer));
		} else {
			if (V.slaves.length > 1) {
				slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Head Girl");
				if (A.FSEgyptianRevivalistLaw === 1) {
					slaveWrapper.append(" and Consort");
				}
				slaveWrapper.append(". ", App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select One", "Head Girl Select"), "major-link"),
					" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Head Girl Select"), "hotkey"));
				slaveWrapper.id = "manageHG";
				if (V.slavePanelStyle === 2) {
					slaveWrapper.classList.add("slaveSummary", "card");
				}
			} else {
				slaveWrapper.append("You do not have enough slaves to keep a Head Girl");
				slaveWrapper.classList.add("note");
			}
		}
		fragment.append(slaveWrapper);

		slaveWrapper = document.createElement("p");
		if (V.RecruiterID) {
			const RC = S.Recruiter;
			const {he} = getPronouns(RC);
			slaveWrapper.append(App.UI.DOM.makeElement("span", SlaveFullName(RC), "slave-name"),
				" is working");
			if (V.recruiterTarget !== "other arcologies") {
				slaveWrapper.append(" to recruit girls. ");
			} else {
				slaveWrapper.append(" as a Sexual Ambassador");
				if (A.influenceTarget === -1) {
					slaveWrapper.append(", but ", App.UI.DOM.makeElement("span", `${he} has no target to influence. `, "warning"));
				} else {
					const targetName = V.arcologies.find(a => a.direction === A.influenceTarget).name;
					slaveWrapper.append(` to ${targetName}. `);
				}
			}
			const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Recruiter", "Recruiter Select"), "major-link");
			link.id = "manageRecruiter";
			slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Recruiter Select"), "hotkey"));
			slaveWrapper.append(App.UI.SlaveList.render([RC.ID], [], interactRenderer));
		} else {
			slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Recruiter. ",
				App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "Recruiter Select"), "major-link"),
				" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Recruiter Select"), "hotkey"));
			slaveWrapper.id = "manageRecruiter";
			if (V.slavePanelStyle === 2) {
				slaveWrapper.classList.add("slaveSummary", "card");
			}
		}
		fragment.append(slaveWrapper);

		if (V.dojo) {
			slaveWrapper = document.createElement("p");
			const BG = S.Bodyguard;
			if (BG) {
				slaveWrapper.append(App.UI.DOM.makeElement("span", SlaveFullName(BG), "slave-name"),
					" is serving as your bodyguard. ");
				const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Bodyguard", "BG Select"), "major-link");
				link.id = "manageBG";
				slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("BG Select"), "hotkey"));
				slaveWrapper.append(App.UI.SlaveList.render([BG.ID], [], interactRenderer));
				slaveWrapper.append(App.MainView.useGuard());
			} else {
				slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Bodyguard. ",
					App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "BG Select"), "major-link"),
					" ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("BG Select"), "hotkey"));
				slaveWrapper.id = "manageBG";
				if (V.slavePanelStyle === 2) {
					slaveWrapper.classList.add("slaveSummary", "card");
				}
			}

			fragment.append(slaveWrapper);
		}
		return fragment;
	}

	/**
	 * @param {string} job
	 * @returns {{n: number, dom: DocumentFragment}}
	 */
	function _slavesForJob(job) {
		const employeesIDs = job === 'all' ? [...ph.employeesIDs()] : [...ph.job(job).employeesIDs()];
		if (employeesIDs.length === 0) {
			return {n: 0, dom: document.createDocumentFragment()};
		}

		SlaveSort.IDs(employeesIDs);
		return {
			n: employeesIDs.length,
			dom: App.UI.SlaveList.render(employeesIDs, [], interactRenderer,
				V.fucktoyInteractionsPosition === 1 && job === "fucktoy" ? App.MainView.useFucktoy : null),
		};
	}

	/**
	 * @typedef tabDesc
	 * @property {string} tabName
	 * @property {string} caption
	 * @property {Node} content
	 */

	/**
	 * @param {string} tabName
	 * @param {string} caption
	 * @param {Node} content
	 * @returns {tabDesc}
	 */
	function makeTabDesc(tabName, caption, content) {
		return {
			tabName: tabName,
			caption: caption,
			content: content,
		};
	}

	/**
	 * Displays encyclopedia entries for occupations at the top of the tab, if enabled
	 * @returns {HTMLSpanElement}
	 */
	function encycTips(jn) {
		const span = document.createElement("span");
		span.classList.add("note");
		if (V.showTipsFromEncy) {
			switch (jn) {
				case "rest":
					span.append(App.Encyclopedia.Assignments.rest());
					break;
				case "chooseOwn":
					break; /* no entry written for choose own */
				case "fucktoy":
					span.append(App.Encyclopedia.Assignments.fucktoy());
					break;
				case "classes":
					span.append(App.Encyclopedia.Assignments.attendingClasses());
					break;
				case "houseServant":
					span.append(App.Encyclopedia.Assignments.servitude());
					break;
				case Job.WHORE:
					span.append(App.Encyclopedia.Assignments.whoring());
					break;
				case "publicServant":
					span.append(App.Encyclopedia.Assignments.publicService());
					break;
				case "subordinateSlave":
					span.append(App.Encyclopedia.Assignments.sexualServitude());
					break;
				case "cow":
					span.append(App.Encyclopedia.Assignments.milking());
					break;
				case "gloryhole":
					span.append(App.Encyclopedia.Assignments.gloryHole());
					break;
				case "confinement":
					span.append(App.Encyclopedia.Assignments.confinement());
					break;
				default:
					span.append(App.UI.DOM.makeElement("span", "missing tip for this tab", "error"));
					break;
			}
		}
		return span;
	}

	function allTab() {
		const penthouseSlavesIDs = [...ph.employeesIDs()];
		if (S.HeadGirl) {
			penthouseSlavesIDs.push(S.HeadGirl.ID);
		}
		if (S.Recruiter) {
			penthouseSlavesIDs.push(S.Recruiter.ID);
		}
		if (S.Bodyguard) {
			penthouseSlavesIDs.push(S.Bodyguard.ID);
		}
		SlaveSort.IDs(penthouseSlavesIDs);
		return makeTabDesc("all", `All${V.useSlaveSummaryTabs > 0 ? ` (${penthouseSlavesIDs.length})` : ""}`,
			App.UI.SlaveList.render(penthouseSlavesIDs, [], interactRenderer));
	}

	function favorites() {
		SlaveSort.IDs(V.favorites);
		return makeTabDesc("favorites", `Favorites${V.useSlaveSummaryTabs > 0 ? ` (${V.favorites.length})` : ""}`,
			App.UI.SlaveList.render(V.favorites, [], interactRenderer));
	}

	const fragment = new DocumentFragment();

	if (V.positionMainLinks >= 0) {
		fragment.append(App.UI.DOM.makeElement("div", App.UI.View.mainLinks(), "center"));
	}

	if (V.sortSlavesMain) {
		fragment.append(App.UI.SlaveList.sortingLinks("Main"));
	}

	/** @type {tabDesc[]} */
	let tabs = [];

	// Overview tab
	if (V.useSlaveSummaryOverviewTab) {
		tabs.push(makeTabDesc("overview", "Special Roles", overviewTabContent()));
	}

	if (V.useSlaveSummaryTabs === 0) {
		tabs.push(allTab());
	}

	if (V.favorites.length > 0 || V.useSlaveSummaryTabs === 0) {
		tabs.push(favorites());
	}

	// tabs for each assignment
	for (const jn of ph.jobsNames) {
		const slaves = _slavesForJob(jn);
		if (slaves.n > 0) {
			tabs.push(makeTabDesc(jn, `${ph.desc.jobs[jn].position}${V.useSlaveSummaryTabs > 0 ? ` (${slaves.n})` : ""}`, App.UI.DOM.combineNodes(encycTips(jn), slaves.dom)));
		} else if (V.useSlaveSummaryTabs === 0) {
			tabs.push(makeTabDesc(jn, ph.desc.jobs[jn].position, encycTips(jn)));
		}
	}

	if (V.useSlaveSummaryTabs > 0) {
		tabs.push(allTab());
	}

	const tabBar = new App.UI.Tabs.TabBar("Main");
	let buttonClass = undefined;
	if (V.useSlaveSummaryTabs === 2) {
		buttonClass = "card";
	}
	let contentClass = undefined;
	if (V.useSlaveSummaryTabs === 0) {
		contentClass = "no-fade";
	} else if (V.useSlaveSummaryTabs === 2) {
		contentClass = "card";
	}
	for (const tab of tabs) {
		tabBar.addTab(tab.caption, tab.tabName, tab.content, buttonClass, contentClass);
	}
	fragment.append(tabBar.render(V.useSlaveSummaryTabs === 0));

	if (V.positionMainLinks <= 0) {
		fragment.append(App.UI.DOM.makeElement("div", App.UI.View.mainLinks(), "center"));
	}

	return fragment;
};

/**
 * @callback slaveFilterCallbackReasoned
 * @param {App.Entity.SlaveState} slave
 * @returns {string[]}
 */

/**
 * @callback slaveFilterCallbackSimple
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */

App.UI.SlaveList.slaveSelectionList = function() {
	/**
	 * @property {slaveFilterCallbackReasoned|slaveFilterCallbackSimple} filter
	 * @property {slaveToElement} interactionLink
	 * @property {slaveTestCallback} [expCheck]
	 * @property {slaveToElement} [postNote]
	 */
	let options = null;

	/**
	 * @type {HTMLDivElement}
	 */
	let slaveSelectionList = null;

	$(document).on(':passageinit', () => { slaveSelectionList = null; });

	return selection;

	/**
	 * @param {slaveFilterCallbackReasoned|slaveFilterCallbackSimple} filter
	 * @param {slaveToElement} interactionLink
	 * @param {slaveTestCallback} [experienceChecker]
	 * @param {slaveToElement} [postNote]
	 * @returns {HTMLElement}
	 */
	function selection(filter, interactionLink, experienceChecker, postNote) {
		if (experienceChecker === null) { experienceChecker = undefined; }
		options = {
			filter: filter,
			interactionLink: interactionLink,
			expCheck: experienceChecker,
			postNote: postNote
		};

		const div = document.createElement("div");
		div.append(_assignmentFilter(experienceChecker !== undefined));
		slaveSelectionList = App.UI.DOM.appendNewElement("div", div, _listSlaves('all'));
		return div;
	}

	function _updateList(assignment) {
		App.UI.DOM.replace(slaveSelectionList, _listSlaves(assignment));
	}

	/**
	 * Displays assignment filter links
	 * @param {boolean} includeExperienced
	 * @returns {HTMLElement}
	 */
	function _assignmentFilter(includeExperienced) {
		let filters = {
			all: "All"
		};
		let fNames = Object.keys(App.Entity.facilities);
		fNames.sort();
		for (const fn of fNames) {
			/** @type {App.Entity.Facilities.Facility} */
			const f = App.Entity.facilities[fn];
			if (f.established && f.hasEmployees) {
				filters[fn] = f.name;
			}
		}
		let links = [];
		for (const f in filters) {
			links.push(App.UI.DOM.link(filters[f], () => _updateList(f)));
		}
		if (includeExperienced) {
			links.push(App.UI.DOM.makeElement("span", App.UI.DOM.link('Experienced', () => _updateList('experienced')), "lime"));
		}

		return App.UI.DOM.generateLinksStrip(links);
	}

	/**
	 * @param {string} assignmentStr
	 * @returns {DocumentFragment}
	 */
	function _listSlaves(assignmentStr) {
		const slaves = V.slaves;
		/** @type {Array<number>} */
		let unfilteredIDs;
		switch (assignmentStr) {
			case 'all':
				unfilteredIDs = slaves.map(s => s.ID);
				break;
			case 'experienced':
				unfilteredIDs = slaves.reduce((acc, s) => {
					if (options.expCheck(s)) {
						acc.push(s.ID);
					}
					return acc;
				}, []);
				break;
			default:
				unfilteredIDs = [...App.Entity.facilities[assignmentStr].employeesIDs()]; // set to array
				break;
		}
		SlaveSort.IDs(unfilteredIDs);
		let passingIDs = [];
		let rejects = [];

		unfilteredIDs.forEach(id => {
			const fr = options.filter(slaveStateById(id));
			if (fr === true || (Array.isArray(fr) && fr.length === 0)) {
				passingIDs.push(id);
			} else {
				if (Array.isArray(fr)) { rejects.push({id: id, rejects: fr}); }
			}
		});

		// clamsi fragment to create a function which combines results of two optional tests
		// done this way to test for tests presence only once
		const listPostNote = options.expCheck
			? (options.postNote
				? s => options.expCheck(s) ? App.UI.DOM.combineNodes(
					App.UI.DOM.makeElement("span", "Has applicable career experience.", "lime"),
					document.createElement("br"),
					options.postNote(s)
				) : options.postNote(s)
				: s => options.expCheck(s) ? App.UI.DOM.makeElement("span", "Has applicable career experience.", "lime") : null)
			: options.postNote
				? s => options.postNote(s)
				: () => null;

		return App.UI.SlaveList.render(passingIDs, rejects, options.interactionLink, listPostNote);
	}
}();

/**
 * @param {App.Entity.Facilities.Facility} facility
 * @param {string} passage go here after the new facility manager is selected
 * @param {slaveToElement} [note]
 * @returns {HTMLElement}
 */
App.UI.SlaveList.facilityManagerSelection = function(facility, passage, note) {
	return App.UI.SlaveList.slaveSelectionList(slave => facility.manager.canEmploy(slave),
		(slave) => App.UI.DOM.passageLink(SlaveFullName(slave), passage,
			() => {
				assignJob(slave, facility.manager.desc.assignment);
			}),
		slave => facility.manager.slaveHasExperience(slave),
		note);
};


code:
// WARNING This file defines objects referenced in slaveSummaryWidgets.js.
// Either keep this file above the slaveSummaryWidgets.js in the name ordered list
// (tweego process them in this order), or rework the references.

/* eslint-disable camelcase */

App.UI.SlaveSummaryImpl = function() {
	const data = App.Data.SlaveSummary;

	const helpers = function() {
		/**
		 * @param {HTMLElement} element
		 * @param {string|string[]} [classNames]
		 */
		function _addClassNames(element, classNames) {
			if (classNames != undefined) { /* eslint-disable-line eqeqeq */
				if (Array.isArray(classNames)) {
					element.classList.add(...classNames);
				} else {
					element.classList.add(classNames);
				}
			}
		}

		/**
		 * @param {Node} container
		 * @param {string} text
		 * @param {string|string[]} [classNames]
		 * @param {boolean} [stdDecor=false]
		 * @param {number} [value]
		 */
		function makeSpan(container, text, classNames, stdDecor = false, value) {
			let r = document.createElement("span");
			_addClassNames(r, classNames);
			if (value != undefined && V.summaryStats) { /* eslint-disable-line eqeqeq */
				text += `[${value}]`;
			}
			r.textContent = stdDecor ? `${capFirstChar(text)}. ` : text + ' ';
			if (container) {
				container.appendChild(r);
			}
			return r;
		}

		/**
		 * @param {Node} container
		 * @param {string} text
		 * @returns {Text}
		 */
		function addText(container, text) {
			const r = document.createTextNode(text);
			if (container) {
				container.appendChild(r);
			}
			return r;
		}

		/**
		 * @param {Node} [container]
		 * @param {string|string[]} [classNames]
		 */
		function makeBlock(container, classNames) {
			let r = document.createElement("span");
			r.classList.add("ssb");
			_addClassNames(r, classNames);
			if (container) {
				container.appendChild(r);
			}
			return r;
		}

		/**
		 * @param {Node} container
		 * @param {string|string[]} [classNames]
		 * @returns {HTMLParagraphElement}
		 */
		function makeParagraph(container, classNames) {
			let r = document.createElement("p");
			r.classList.add("si");
			_addClassNames(r, classNames);
			if (container) {
				container.appendChild(r);
			}
			return r;
		}

		/**
		 * @typedef {object} StyledDesc
		 * @property {string} desc
		 * @property {string|string[]} [style]
		 */

		/** @typedef {Object.<number, StyledDesc>} StyledRatings */
		/**
		 * @param {Node} container
		 * @param {StyledRatings} ratings
		 * @param {number} [value]
		 * @param {number} [offset] value offset in the ratings dictionary (to eliminate negative values)
		 * @param {boolean} [stdDecor=false]
		 */
		function makeRatedStyledSpan(container, ratings, value, offset = 0, stdDecor = false) {
			const d = App.Ratings.numeric(ratings, value + offset);
			if (d) {
				makeSpan(container, d.desc, d.style, stdDecor, value);
			}
		}

		/**
		 * @param {Node} container
		 * @param {StyledDesc} styledDesc
		 * @param {number} [value]
		 * @param {boolean} [stdDecor]
		 */
		function makeStyledSpan(container, styledDesc, value, stdDecor = false) {
			if (styledDesc) {
				makeSpan(container, styledDesc.desc, styledDesc.style, stdDecor, value);
			}
		}

		/**
		 * @param {Node} container
		 * @param {StyledRatings} ratings
		 * @param {string|number} value
		 */
		function makeMappedStyledSpan(container, ratings, value) {
			const d = ratings[value];
			if (d) {
				makeSpan(container, d.desc, d.style);
			}
		}

		/**
		 * @param {Node} container
		 * @param {Object.<string, string>} ratings
		 * @param {string|number} value
		 * @param {string|string[]} [classNames]
		 */
		function makeMappedSpan(container, ratings, value, classNames) {
			const d = ratings[value];
			if (d) {
				makeSpan(container, d, classNames);
			}
		}

		/**
		 * Returns first three string characters with the first one uppercased (string -> Str)
		 * @param {string} s
		 * @returns {string}
		 */
		function firstThreeUc(s) {
			return s.charAt(0).toUpperCase() + s.charAt(1) + s.charAt(2);
		}

		/**
		 * @param {FC.ArcologyState} arcology
		 */
		function syncFSData(arcology) {
			arcology = arcology || V.arcologies[0];
			for (const fsp of App.Data.FutureSociety.fsNames) {
				/** @type {FC.FSPolicyValue} */
				const policy = arcology[fsp];
				const p = fsp.slice(2);
				FSData.policy[p] = {
					active: (policy === "unset" || _.isNil(policy)) ? 0 : 1,
					strength: Math.trunc(policy === "unset" ? 0 : policy / 10)
				};
			}

			const dislikeBigButts = (FSData.policy.TransformationFetishist.strength < 2) && (FSData.policy.HedonisticDecadence.strength < 2) && (FSData.policy.AssetExpansionist.strength < 2) && (arcology.FSIntellectualDependencyLawBeauty === 0);
			FSData.bigButts = dislikeBigButts ? -1 : 0;
		}

		/**
		 * @typedef {Object} FSDatum
		 * @property {number} active FS policy is active (0,1)
		 * @property {number} strength FS decoration level divided by 10
		 */
		const FSData = {
			/** @type {Object.<string, FSDatum>} */
			policy: {},
			bigButts: 0,
		};

		/**
		 *
		 * @param {ParentNode} container
		 * @param {App.Entity.SlaveState} slave
		 * @param {string} text
		 * @returns {HTMLSpanElement}
		 */
		function referenceSlaveWithPreview(container, slave, text) {
			const preview = App.UI.DOM.referenceSlaveWithPreview(slave, text);
			container.append(preview);
			return preview;
		}

		const longFamilyBits = {
			and: " and ",
			makeBit: s => s + '.',
			daughters10: "Has tons of daughters.",
			daughters5: "Has many daughters.",
			daughters1: "Has several daughters.",
			sisters10: "One of many sisters.",
			sisters5: "Has many sisters.",
			sisters1: "Has several sisters.",
			emotionBind: "Emotionally bonded to you.",
			emotionSlut: "Emotional slut."
		};

		const shortFamilyBits = {
			and: " & ",
			makeBit: s => s,
			daughters10: "tons of daughters",
			daughters5: "many daughters",
			daughters1: "has daughters",
			sisters10: "One of many sisters.",
			sisters5: "Has many sisters.",
			sisters1: "Has several sisters.",
			emotionBind: "E Bonded",
			emotionSlut: "E Slut"
		};

		/**
		 * @param {Node} container
		 * @param {App.Entity.SlaveState} slave
		 * @param {boolean} short
		 */
		function renderFamily(container, slave, short) {
			let handled = 0;
			const bits = short ? shortFamilyBits : longFamilyBits;
			const block = makeBlock();
			const cssClassName = "lightgreen";
			if (slave.mother > 0) {
				const ssj = V.slaves.find(s => s.ID === slave.mother);
				if (ssj) {
					helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
					addText(block, "'s ");
					let spanText = getPronouns(slave).daughter;
					if (slave.relationshipTarget === ssj.ID) {
						spanText += `${bits.and}${relationshipTerm(slave)}`;
						handled = 1;
					}
					makeSpan(block, bits.makeBit(spanText), cssClassName);
				}
			} else if (slave.mother === -1) {
				addText(block, `Your `);
				if (slave.relationship < -1) {
					makeSpan(block, bits.makeBit(`${getPronouns(slave).daughter}${bits.and}${PCrelationshipTerm(slave)}`), cssClassName);
					handled = 1;
				} else {
					makeSpan(block, bits.makeBit(getPronouns(slave).daughter), cssClassName);
				}
			} else if (slave.mother in V.missingTable && V.showMissingSlavesSD && V.showMissingSlaves) {
				addText(block, `${V.missingTable[slave.mother].fullName}'s `);
				makeSpan(block, bits.makeBit(getPronouns(slave).daughter), cssClassName);
			}
			if (slave.father > 0 && slave.father !== slave.mother) {
				const ssj = V.slaves.find(s => s.ID === slave.father);
				if (ssj) {
					helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
					addText(block, "'s ");
					let spanText = getPronouns(slave).daughter;
					if (slave.relationshipTarget === ssj.ID) {
						spanText += `${bits.and}${relationshipTerm(slave)}`;
						handled = 1;
					}
					makeSpan(block, bits.makeBit(spanText), cssClassName);
				}
			} else if (slave.father === -1 && slave.father !== slave.mother) {
				addText(block, `Your `);
				if (slave.relationship < -1) {
					makeSpan(block, bits.makeBit(`${getPronouns(slave).daughter}${bits.and}${PCrelationshipTerm(slave)}`), cssClassName);
					handled = 1;
				} else {
					makeSpan(block, bits.makeBit(getPronouns(slave).daughter), cssClassName);
				}
			} else if (slave.father in V.missingTable && slave.father !== slave.mother && V.showMissingSlavesSD && V.showMissingSlaves) {
				addText(block, `${V.missingTable[slave.father].fullName}'s `);
				makeSpan(block, bits.makeBit(getPronouns(slave).daughter), cssClassName);
			}
			if (areSisters(V.PC, slave) > 0) {
				addText(block, `Your `);
				if (slave.relationship < -1) {
					makeSpan(block, bits.makeBit(`${siblingTerm(V.PC, slave)}${bits.and}${PCrelationshipTerm(slave)}`), cssClassName);
					handled = 1;
				} else {
					makeSpan(block, bits.makeBit(siblingTerm(V.PC, slave)), cssClassName);
				}
			}
			if (slave.daughters === 1) {
				const ssj = V.slaves.find(s => s.mother === slave.ID || s.father === slave.ID);
				if (ssj) {
					helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
					addText(block, "'s ");
					let spanText = parentTerm(ssj, slave);
					if (slave.relationshipTarget === ssj.ID) {
						spanText += `${bits.and}${relationshipTerm(slave)}`;
						handled = 1;
					}
					makeSpan(block, bits.makeBit(spanText), cssClassName);
				}
			} else if (slave.daughters > 1) {
				if (slave.daughters > 10) {
					makeSpan(block, bits.daughters10, cssClassName);
				} else if (slave.daughters > 5) {
					makeSpan(block, bits.daughters5, cssClassName);
				} else {
					makeSpan(block, bits.daughters1, cssClassName);
				}
			}
			if (slave.sisters === 1) {
				const ssj = V.slaves.find(s => areSisters(s, slave) > 0);
				if (ssj) {
					helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
					addText(block, "'s ");
					let spanText = getPronouns(slave).sister;
					if (slave.relationshipTarget === ssj.ID) {
						spanText += `${bits.and}${relationshipTerm(slave)}`;
						handled = 1;
					}
					makeSpan(block, bits.makeBit(spanText), cssClassName);
				}
			} else if (slave.sisters > 1) {
				if (slave.sisters > 10) {
					makeSpan(block, bits.sisters10, cssClassName);
				} else if (slave.sisters > 5) {
					makeSpan(block, bits.sisters5, cssClassName);
				} else {
					makeSpan(block, bits.sisters1, cssClassName);
				}
			}
			if (slave.relationship > 0 && handled !== 1) {
				const ssj = V.slaves.find(s => s.ID === slave.relationshipTarget);
				if (ssj) {
					helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
					addText(block, "'s ");
					makeSpan(block, bits.makeBit(relationshipTerm(slave)), cssClassName);
				}
			} else if (slave.relationship === -3 && !areRelated(V.PC, slave)) {
				makeSpan(block, bits.makeBit(`Your ${getPronouns(slave).wife}`), cssClassName);
			} else if (slave.relationship === -2) {
				makeSpan(block, bits.emotionBind, cssClassName);
			} else if (slave.relationship === -1) {
				makeSpan(block, bits.emotionSlut, cssClassName);
			}

			if (block.textContent.length > 0) {
				container.appendChild(block);
			}
		}

		return {
			addText,
			makeSpan,
			makeBlock,
			makeParagraph,
			makeStyledSpan,
			makeRatedStyledSpan,
			makeMappedStyledSpan,
			makeMappedSpan,
			firstThreeUc,
			syncFSData,
			FSData,
			referenceSlaveWithPreview,
			renderFamily
		};
	}();

	const bits = function() {
		const addText = helpers.addText;
		const makeSpan = helpers.makeSpan;
		const makeBlock = helpers.makeBlock;
		const makeRatedStyledSpan = helpers.makeRatedStyledSpan;

		/**
		 * Returns index in the hips-ass rating table
		 * @param {App.Entity.SlaveState} slave
		 * @returns {number} 0 if butt is considered too small, 1 is too big, -1 otherwise.
		 */
		function hipsAssRating(slave) {
			const FSData = helpers.FSData;
			if (slave.hips < -1) {
				if (slave.butt > 2 && FSData.bigButts <= 0) {
					return 1;
				}
			} else if (slave.hips < 0) {
				if (slave.butt > 4 && FSData.bigButts <= 0) {
					return 1;
				}
			} else if (slave.hips > 2) {
				if (slave.butt <= 8) {
					return 0;
				}
			} else if (slave.hips > 1) {
				if (slave.butt <= 3 && (FSData.policy.SlimnessEnthusiast.active === 0 || (slave.boobs >= 500))) {
					return 0;
				}
			} else if (slave.hips > 0) {
				if (slave.butt > 8) {
					if (FSData.bigButts <= 0) {
						return 1;
					}
				} else if (slave.butt <= 2 && ((FSData.policy.SlimnessEnthusiast.active === 0) || (slave.boobs >= 500))) {
					return 0;
				}
			} else {
				if (slave.butt > 6) {
					if (FSData.bigButts <= 0) {
						return 1;
					}
				} else if (slave.butt <= 1 && (FSData.policy.SlimnessEnthusiast.active === 0 || (slave.boobs >= 500))) {
					return 0;
				}
			}
			return -1;
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {FC.Data.SlaveSummary.SmartVibrator} spData
		 * @returns {string}
		 */
		function smartFetishStr(slave, spData) {
			/** @type {string} */
			let value;
			if (slave.piercing.genitals.smart) {
				value = spData.system.piercing;
			} else if (slave.dickAccessory === "smart bullet vibrator") {
				value = spData.system.bullet;
			} else {
				value = spData.system.vibe;
			}
			value += ":";
			if (slave.fetishKnown === 1) {
				if (slave.clitSetting === "off") {
					return value + spData.setting.off;
				} else if ((slave.energy <= 95) && (slave.clitSetting === "all")) {
					return value + spData.setting.all;
				} else if ((slave.energy > 5) && (slave.clitSetting === "none")) {
					return value + spData.setting.none;
				} else if (((slave.fetish !== Fetish.NONE) && (slave.clitSetting === "vanilla"))) {
					return value + spData.setting.vanilla;
				} else if (slave.fetishStrength <= 95 || slave.fetish !== slave.clitSetting) {
					const s = value + spData.setting[slave.clitSetting];
					if (s) {
						return s;
					}
				}
				if (!["anti-men", "anti-women", "men", "women"].includes(slave.clitSetting)) {
					return value + spData.setting.monitoring;
				}
			} else {
				return value + spData.setting[slave.clitSetting];
			}
			return null;
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {FC.Data.SlaveSummary.SmartVibrator} spData
		 * @returns {string}
		 */
		function smartAttractionStr(slave, spData) {
			const sps = spData.setting;
			const cs = slave.clitSetting;
			if (slave.attrKnown === 1) {
				switch (cs) {
					case "women":
						if (slave.attrXX < 95) {
							return sps.women;
						} else {
							return sps.monitoring;
						}
					case "men":
						if (slave.attrXY < 95) {
							return sps.men;
						} else {
							return sps.monitoring;
						}
					case "anti-women":
						if (slave.attrXX > 0) {
							return sps["anti-women"];
						} else {
							return sps.monitoring;
						}
					case "anti-men":
						if (slave.attrXY > 0) {
							return sps["anti-men"];
						} else {
							return sps.monitoring;
						}
				}
			} else {
				switch (cs) {
					// fall-through
					case "women":
					case "men":
					case "anti-women":
					case "anti-men":
						return sps[cs];
				}
			}
			return null;
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_health(slave, c) {
			if (slave.health.health < -20) {
				makeSpan(c, "H", ["red", "strong"], true, slave.health.health);
			} else if (slave.health.health <= 20) {
				makeSpan(c, "H", ["yellow", "strong"], true, slave.health.health);
			} else if (slave.health.health > 20) {
				makeSpan(c, "H", ["green", "strong"], true, slave.health.health);
			}
			if (passage() === "Clinic" && V.clinicUpgradeScanner) {
				if (slave.chem > 15) {
					makeSpan(c, `C${Math.ceil(slave.chem / 10)}`, ["cyan", "strong"]);
				} else if (slave.chem <= 15 && slave.assignment === Job.CLINIC) {
					makeSpan(c, `CSafe`, ["green", "strong"]);
				}
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_illness(slave, c) {
			if (slave.health.illness > 2) {
				makeSpan(c, `Ill${slave.health.illness}`, ["red", "strong"], true, slave.health.illness);
			} else if (slave.health.illness > 0) {
				makeSpan(c, `Ill${slave.health.illness}`, ["yellow", "strong"], true, slave.health.illness);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_tired(slave, c) {
			helpers.makeRatedStyledSpan(c, data.short.health.tiredness, slave.health.tired, 0, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_health(slave, c) {
			helpers.makeRatedStyledSpan(c, data.long.health.health, slave.health.health, 100, true);
			if (passage() === "Clinic" && V.clinicUpgradeScanner) {
				if (slave.chem > 15) {
					makeSpan(c, `Carcinogen buildup: ${Math.ceil(slave.chem / 10)}.`, "cyan");
				} else if (slave.chem <= 15 && slave.assignment === Job.CLINIC) {
					makeSpan(c, `Safe chem levels.`, "green");
				}
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_illness(slave, c) {
			makeRatedStyledSpan(c, data.long.health.illness, slave.health.illness, 0, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_tired(slave, c) {
			makeRatedStyledSpan(c, data.long.health.tiredness, slave.health.tired, 0, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_age(slave, c) {
			const style = "pink";
			makeSpan(c, V.showAgeDetail ? `Age ${slave.actualAge}` : App.Ratings.numeric(data.long.body.age, slave.actualAge), style, true);
			/*
			 ** No NCS, then do the standard, However because of the wrinkles of Incubators, as long as visual age is greater
			 ** than or equal to physical age, we do the old physical body/Looks for fresh out of the can NCS slaves.
			 */
			if (((slave.geneMods.NCS === 0) || (slave.visualAge >= slave.physicalAge))) {
				if (slave.actualAge !== slave.physicalAge) {
					makeSpan(c, `${slave.physicalAge} year old body`, style, true);
				}
				if (slave.visualAge !== slave.physicalAge) {
					makeSpan(c, `Looks ${slave.visualAge}`, style, true);
				}
			} else {
				/*
				 ** Now the rub. The use of physical Age for the year old body above, basically conflicts with the changes
				 ** that NCS introduces, so here to *distinguish* the changes, we use visual age with the 'year old body'
				 ** and appears, for example: Slave release from incubator at age 10, Her summary would show, 'Age 0. 10
				 ** year old body.' But if she's given NCS a few weeks after release, while she's still before her first
				 ** birthday, it'll appear the same. But once her birthday fires, if we ran with the above code it would
				 ** say: 'Age 1. 11 year old body.' -- this conflicts with the way NCS works though, because she hasn't
				 ** visually aged, so our change here makes it say 'Age 1. Appears to have a 10 year old body.'
				 */
				makeSpan(c, `Appears to have a ${slave.visualAge} year old body`, style, true);
			}
			if (slave.geneMods.NCS === 1) {
				makeSpan(c, "NCS", "orange", true);
			}
			if (slave.geneMods.immortality === 1) {
				makeSpan(c, "Immortal", "orange", true);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_face(slave, c) {
			const r = App.Ratings.numeric(data.long.body.face, slave.face + 100);
			makeSpan(c, `${r.desc} ${slave.faceShape} face`, r.style, true, slave.face);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_eyes(slave, c) {
			if (!canSee(slave)) {
				makeSpan(c, "Blind.", "red");
			} else if (!canSeePerfectly(slave)) {
				makeSpan(c, "Nearsighted.", "yellow");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_ears(slave, c) {
			if (slave.hears <= -2) {
				makeSpan(c, "Deaf.", "red");
			} else if ((slave.hears === -1) && (slave.earwear !== "hearing aids")) {
				makeSpan(c, "Hard of hearing.", "yellow");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_lips(slave, c) {
			makeRatedStyledSpan(c, data.long.body.lips, slave.lips, 0, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_teeth(slave, c) {
			helpers.makeMappedStyledSpan(c, data.long.body.teeth, slave.teeth);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_muscles(slave, c) {
			const h = helpers;
			h.makeStyledSpan(c, App.Ratings.multiNumeric(data.long.body.muscles, [slave.muscles + 100, h.FSData.policy.PhysicalIdealist.active]), slave.muscles, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_voice(slave, c) {
			if (slave.voice === 0) {
				makeSpan(c, "Mute.", "red");
			} else {
				helpers.makeMappedStyledSpan(c, data.long.accent, slave.accent);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_tits_ass(slave, c) {
			const h = helpers;
			h.makeStyledSpan(c,
				App.Ratings.multiNumeric(data.long.body.titsAss,
					[slave.boobs, slave.butt, h.FSData.policy.AssetExpansionist.active, slave.weight + 100, slave.muscles + 100]));
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_hips(slave, c) {
			const di = hipsAssRating(slave);
			if (di >= 0) {
				helpers.makeMappedStyledSpan(c, data.long.body.hipsAss, di);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_waist(slave, c) {
			makeRatedStyledSpan(c, data.long.body.waist, slave.waist, 100, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_implants(slave, c) {
			const styles = "pink";
			if ((slave.boobsImplant !== 0) || (slave.buttImplant !== 0) || (slave.lipsImplant !== 0) || (slave.bellyImplant !== -1)) {
				makeSpan(c, "Implants.", styles);
			} else if ((slave.faceImplant > 5) || (slave.waist < -95)) {
				makeSpan(c, "Surgery enhanced.", styles);
			} else {
				makeSpan(c, "All natural.", styles);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_lactation(slave, c) {
			if (slave.lactation === 1) {
				makeSpan(c, "Lactating naturally.", "pink");
			} else if (slave.lactation === 2) {
				makeSpan(c, "Heavy lactation.", "pink");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function milk_flavor(slave, c) {
			if (slave.milkFlavor !== "none" && slave.lactation >= 1) {
				makeSpan(c, `${capFirstChar(slave.milkFlavor)} flavored milk.`, "pink");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_mods(slave, c) {
			const modScore = SlaveStatsChecker.modScore(slave);
			if (slave.piercing.corset.weight === 0 && modScore.piercing < 3 && modScore.tat < 2) {
				return;
			}
			if (modScore.total > 15 || (modScore.piercing > 8 && modScore.tat > 5)) {
				makeSpan(c, "Extensive body mods.");
			} else if (modScore.total > 7) {
				makeSpan(c, "Noticeable body mods.");
			} else {
				makeSpan(c, "Light body mods.");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_age(slave, c) {
			let r = makeSpan(c, "", "pink");
			if (V.showAgeDetail === 1) {
				r.textContent += slave.actualAge.toString();
			} else if (slave.actualAge >= 18) {
				r.textContent += App.Ratings.numeric(data.short.body.age, slave.actualAge);
			}

			if (slave.actualAge !== slave.physicalAge) {
				r.textContent += ` w ${slave.physicalAge}y-bdy`;
			}
			if (slave.visualAge !== slave.physicalAge) {
				r.textContent += ` Lks${slave.visualAge}`;
			}
			r.textContent += " ";
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_face(slave, c) {
			makeRatedStyledSpan(c, data.short.body.face, slave.face, 100, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_eyes(slave, c) {
			if (!canSee(slave)) {
				makeSpan(c, "Blind", "red");
			} else if (!canSeePerfectly(slave)) {
				makeSpan(c, "Sight-", "yellow");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_ears(slave, c) {
			if (slave.hears === -2) {
				makeSpan(c, "Deaf", "red");
			} else if ((slave.hears === -1) && (slave.earwear !== "hearing aids")) {
				makeSpan(c, "Hearing-", "yellow");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_lips(slave, c) {
			makeRatedStyledSpan(c, data.short.body.lips, slave.lips, 0, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_teeth(slave, c) {
			helpers.makeMappedStyledSpan(c, data.short.body.teeth, slave.teeth);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_muscles(slave, c) {
			const h = helpers;
			h.makeStyledSpan(c, App.Ratings.multiNumeric(data.short.body.muscles, [slave.muscles + 100, h.FSData.policy.PhysicalIdealist.active]), slave.muscles, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_voice(slave, c) {
			if (slave.voice === 0) {
				makeSpan(c, "Mute", "red");
			} else {
				helpers.makeMappedStyledSpan(c, data.short.accent, slave.accent);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_tits_ass(slave, c) {
			const h = helpers;
			h.makeStyledSpan(c,
				App.Ratings.multiNumeric(data.short.body.titsAss,
					[slave.boobs, slave.butt, h.FSData.policy.AssetExpansionist.active, slave.weight + 100, slave.muscles + 100]));
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_hips(slave, c) {
			const di = hipsAssRating(slave);
			if (di >= 0) {
				helpers.makeMappedStyledSpan(c, data.short.body.hipsAss, di);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_waist(slave, c) {
			makeRatedStyledSpan(c, data.short.body.waist, slave.waist, 100, false);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_implants(slave, c) {
			if ((slave.boobsImplant === 0) && (slave.buttImplant === 0) && (slave.waist >= -95) && (slave.lipsImplant === 0) && (slave.faceImplant <= 5) && (slave.bellyImplant === -1)) {
				makeSpan(c, "Natr", "pink");
			} else {
				makeSpan(c, "Impl", "pink");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_lactation(slave, c) {
			if (slave.lactation === 1) {
				makeSpan(c, "Lact", "pink");
			} else if (slave.lactation === 2) {
				makeSpan(c, "Lact", "pink");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_mods(slave, c) {
			const modScore = SlaveStatsChecker.modScore(slave);
			if (slave.piercing.corset.weight === 0 && modScore.piercing < 3 && modScore.tat < 2) {
				return;
			} else if (modScore.total > 15 || (modScore.piercing > 8 && modScore.tat > 5)) {
				makeSpan(c, "Mods++");
			} else if (modScore.total > 7) {
				makeSpan(c, "Mods+");
			} else {
				makeSpan(c, "Mods");
			}
			const brands = App.Medicine.Modification.brandRecord(slave);
			if (!jQuery.isEmptyObject(brands)) {
				makeSpan(c, "Br");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_intelligence(slave, c) {
			if (slave.fetish === Fetish.MINDBROKEN) {
				return;
			}
			const intelligence = slave.intelligence + slave.intelligenceImplant;
			const educationStr = App.Ratings.numeric(data.short.mental.education, slave.intelligenceImplant + 15);
			const intelligenceRating = App.Ratings.numeric(data.short.mental.intelligence, intelligence + 100);
			makeSpan(c, `${intelligenceRating.desc}${educationStr}`, intelligenceRating.style, true, intelligence);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_skills(slave, c) {
			const sd = data.short.skills;
			let SSkills = (slave.skill.anal + slave.skill.oral);
			if (((SSkills + slave.skill.whoring + slave.skill.entertainment) >= 400) && ((slave.vagina < 0) || (slave.skill.vaginal >= 100))) {
				helpers.makeStyledSpan(c, sd.mss);
			} else {
				SSkills += slave.skill.vaginal;
				helpers.makeStyledSpan(c, App.Ratings.multiNumeric(sd.sex, [SSkills, slave.vagina >= 0 ? 1 : 0]), Math.trunc(SSkills), true);
				helpers.makeRatedStyledSpan(c, sd.whoring, slave.skill.whoring, 0, true);
				helpers.makeRatedStyledSpan(c, sd.entertainment, slave.skill.entertainment, 0, true);
			}
			helpers.makeRatedStyledSpan(c, sd.combat, slave.skill.combat, 0, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_prestige(slave, c) {
			helpers.makeMappedStyledSpan(c, data.short.prestige, slave.prestige);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_porn_prestige(slave, c) {
			helpers.makeMappedStyledSpan(c, data.short.pornPrestige, slave.porn.prestige);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_intelligence(slave, c) {
			if (slave.fetish === Fetish.MINDBROKEN) {
				return;
			}
			const intelligence = slave.intelligence + slave.intelligenceImplant;
			const educationStr = App.Ratings.numeric(data.long.mental.education, slave.intelligenceImplant + 15);
			const intelligenceRating = App.Ratings.numeric(data.long.mental.intelligence, intelligence + 100);
			makeSpan(c, `${intelligenceRating.desc}${educationStr}`, intelligenceRating.style, true, intelligence);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_skills(slave, c) {
			const sd = data.long.skills;
			let SSkills = (slave.skill.anal + slave.skill.oral);
			if (((SSkills + slave.skill.whoring + slave.skill.entertainment) >= 400) && ((slave.vagina < 0) || (slave.skill.vaginal >= 100))) {
				helpers.makeStyledSpan(c, sd.mss);
			} else {
				SSkills += slave.skill.vaginal;
				helpers.makeStyledSpan(c, App.Ratings.multiNumeric(sd.sex, [SSkills, slave.vagina >= 0 ? 1 : 0]), Math.trunc(SSkills), true);
				helpers.makeRatedStyledSpan(c, sd.whoring, slave.skill.whoring, 0, true);
				helpers.makeRatedStyledSpan(c, sd.entertainment, slave.skill.entertainment, 0, true);
			}
			helpers.makeRatedStyledSpan(c, sd.combat, slave.skill.combat, 0, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_prestige(slave, c) {
			helpers.makeMappedStyledSpan(c, data.long.prestige, slave.prestige);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_porn_prestige(slave, c) {
			helpers.makeMappedStyledSpan(c, data.long.pornPrestige, slave.porn.prestige);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_clothes(slave, c) {
			makeSpan(c, App.Ratings.exact(data.long.clothes, slave.clothes, 'Naked.'));
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_collar(slave, c) {
			helpers.makeMappedSpan(c, data.long.accessory.collar, slave.collar);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_mask(slave, c) {
			helpers.makeMappedSpan(c, data.long.accessory.faceAccessory, slave.faceAccessory);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_mouth(slave, c) {
			helpers.makeMappedSpan(c, data.long.accessory.mouthAccessory, slave.mouthAccessory);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_belly(slave, c) {
			helpers.makeMappedSpan(c, data.long.accessory.belly, slave.bellyAccessory);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_arms(slave, c) {
			if (["hand gloves", "elbow gloves"].includes(slave.armAccessory)) {
				makeSpan(c, slave.armAccessory, undefined, true);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_legs(slave, c) {
			if (slave.legAccessory === "short stockings") {
				makeSpan(c, "Short stockings.");
			} else if (slave.legAccessory === "long stockings") {
				makeSpan(c, "Long stockings.");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_shoes(slave, c) {
			if (shoeHeelCategory(slave) > 0) {
				makeSpan(c, slave.shoes, undefined, true);
			} else if (slave.heels === 1) {
				makeSpan(c, "Crawling.", "yellow");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_chastity(slave, c) {
			if (slave.chastityAnus === 1 && slave.chastityPenis === 1 && slave.chastityVagina === 1) {
				makeSpan(c, "Full chastity.");
			} else if (slave.chastityPenis === 1 && slave.chastityVagina === 1) {
				makeSpan(c, "Genital chastity.");
			} else if ((slave.chastityAnus === 1 && slave.chastityVagina === 1) || (slave.chastityAnus === 1 && slave.chastityPenis === 1)) {
				makeSpan(c, "Combined chastity.");
			} else if (slave.chastityVagina === 1) {
				makeSpan(c, "Vaginal chastity.");
			} else if (slave.chastityPenis === 1) {
				makeSpan(c, "Chastity cage.");
			} else if (slave.chastityAnus === 1) {
				makeSpan(c, "Anal chastity.");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_vaginal_acc(slave, c) {
			if (slave.vaginalAttachment === "none") {
				helpers.makeMappedSpan(c, data.long.accessory.vaginal, slave.vaginalAccessory);
			} else {
				switch (slave.vaginalAttachment) {
					case "vibrator":
						makeSpan(c, "Vibrating dildo.");
						break;
					case "smart vibrator":
						makeSpan(c, "Smart vibrating dildo.");
						break;
				}
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_dick_acc(slave, c) {
			switch (slave.dickAccessory) {
				case "sock":
					makeSpan(c, "Cock sock.");
					break;
				case "bullet vibrator":
					makeSpan(c, "Frenulum bullet vibrator.");
					break;
				case "smart bullet vibrator":
					makeSpan(c, "Smart frenulum bullet vibrator.");
					break;
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_buttplug(slave, c) {
			helpers.makeMappedSpan(c, data.long.accessory.buttplug, slave.buttplug);
			helpers.makeMappedSpan(c, data.long.accessory.buttplugAttachment, slave.buttplugAttachment);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_fetish(slave, c) {
			function fetishStr(slave) {
				const tbl = data.short.fetish[slave.fetish];
				if (!tbl || typeof tbl === 'string') {
					return tbl;
				}
				return App.Ratings.numeric(tbl, slave.fetishStrength);
			}

			const fStr = fetishStr(slave);
			if (fStr) {
				makeSpan(c, fStr, "lightcoral", true, slave.fetishStrength);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_attraction(slave, c) {
			const sd = data.short.sexDrive;
			// check for special cases first
			if (slave.energy > 95 && slave.attrXX > 95 && slave.attrXY > 95) {
				helpers.makeStyledSpan(c, sd.synergy.nymphomni);
			} else if (slave.attrXX > 95 && slave.attrXY > 95) {
				helpers.makeStyledSpan(c, sd.synergy.omni);
				helpers.makeRatedStyledSpan(c, sd.energy, slave.energy, 0, true);
			} else {
				helpers.makeRatedStyledSpan(c, sd.XY, slave.attrXY, 0, true);
				helpers.makeRatedStyledSpan(c, sd.XX, slave.attrXX, 0, true);
				helpers.makeRatedStyledSpan(c, sd.energy, slave.energy, 0, true);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_smart_piercing(slave, c) {
			const s = smartFetishStr(slave, data.short.smartVibrator) || smartAttractionStr(slave, data.short.smartVibrator);
			if (s) {
				makeSpan(c, s);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_behavior_flaw(slave, c) {
			helpers.makeMappedSpan(c, data.short.mental.behavioralFlaw, slave.behavioralFlaw, "red");
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_sex_flaw(slave, c) {
			helpers.makeMappedStyledSpan(c, data.short.mental.sexualFlaw, slave.sexualFlaw);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_behavior_quirk(slave, c) {
			helpers.makeMappedSpan(c, data.short.mental.behavioralQuirk, slave.behavioralQuirk, "green");
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_sex_quirk(slave, c) {
			helpers.makeMappedSpan(c, data.short.mental.sexualQuirk, slave.sexualQuirk, "green");
		}

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_fetish(slave, c) {
			function fetishStr() {
				const tbl = data.long.fetish[slave.fetish];
				if (!tbl || typeof tbl === 'string') {
					return tbl;
				}
				return App.Ratings.numeric(tbl, slave.fetishStrength);
			}

			const fStr = fetishStr();
			if (fStr) {
				makeSpan(c, fStr, "lightcoral", true, slave.fetishStrength);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_attraction(slave, c) {
			const sd = data.long.sexDrive;

			// check for special cases first
			if (slave.energy > 95 && slave.attrXX > 95 && slave.attrXY > 95) {
				helpers.makeStyledSpan(c, sd.synergy.nymphomni);
			} else if (slave.attrXX > 95 && slave.attrXY > 95) {
				helpers.makeStyledSpan(c, sd.synergy.omni);
				helpers.makeRatedStyledSpan(c, sd.energy, slave.energy, 0, true);
			} else {
				helpers.makeRatedStyledSpan(c, sd.XY, slave.attrXY, 0, true);
				helpers.makeRatedStyledSpan(c, sd.XX, slave.attrXX, 0, true);
				helpers.makeRatedStyledSpan(c, sd.energy, slave.energy, 0, true);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_smart_piercing(slave, c) {
			const s = smartFetishStr(slave, data.long.smartVibrator) || smartAttractionStr(slave, data.long.smartVibrator);
			if (s) {
				makeSpan(c, s);
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_behavior_flaw(slave, c) {
			helpers.makeMappedSpan(c, data.long.mental.behavioralFlaw, slave.behavioralFlaw, "red");
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_sex_flaw(slave, c) {
			switch (slave.sexualFlaw) {
				case "hates oral":
				case "hates anal":
				case "hates penetration":
				case "shamefast":
					makeSpan(c, slave.sexualFlaw, "red", true);
					break;
				case "idealistic":
				case "repressed":
				case "apathetic":
				case "crude":
				case "judgemental":
					makeSpan(c, `Sexually ${slave.sexualFlaw}.`, "red");
					break;
				case "cum addict":
				case "anal addict":
				case "attention whore":
					makeSpan(c, slave.sexualFlaw, "yellow", true);
					break;
				case "breast growth":
					makeSpan(c, `Breast obsession.`, "yellow");
					break;
				case "abusive":
				case "malicious":
					makeSpan(c, `Sexually ${slave.sexualFlaw}.`, "yellow");
					break;
				case "self hating":
					makeSpan(c, `Self hatred.`, "yellow");
					break;
				case "neglectful":
					makeSpan(c, `Self neglectful.`, "yellow");
					break;
				case "breeder":
					makeSpan(c, `Breeding obsession.`, "yellow");
					break;
				default:
					slave.sexualFlaw = "none";
					break;
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_behavior_quirk(slave, c) {
			switch (slave.behavioralQuirk) {
				case "confident":
				case "cutting":
				case "funny":
				case "fitness":
				case "adores women":
				case "adores men":
				case "insecure":
				case "sinful":
				case "advocate":
					makeSpan(c, slave.behavioralQuirk, "green", true);
					break;
				default:
					slave.behavioralQuirk = "none";
					break;
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_sex_quirk(slave, c) {
			switch (slave.sexualQuirk) {
				case "gagfuck queen":
				case "painal queen":
				case "strugglefuck queen":
				case "tease":
				case "romantic":
				case "perverted":
				case "caring":
				case "unflinching":
				case "size queen":
					makeSpan(c, slave.sexualQuirk, "green", true);
					break;
				default:
					slave.sexualQuirk = "none";
					break;
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_family(slave, c) {
			helpers.renderFamily(c, slave, true);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_clone(slave, c) {
			if (typeof slave.clone === "string") {
				makeSpan(c, "Clone");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function short_rival(slave, c) {
			if (slave.rivalry !== 0) {
				const block = makeBlock(c, "lightsalmon");
				const ssj = V.slaves.find(s => s.ID === slave.rivalryTarget);
				if (ssj) {
					if (slave.rivalry <= 1) {
						block.textContent = 'Disl ';
						helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
					} else if (slave.rivalry <= 2) {
						helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
						addText(block, "'s rival");
					} else {
						block.textContent = 'Hates ';
						helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
					}
				}
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_family(slave, c) {
			helpers.renderFamily(c, slave, false);
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_clone(slave, c) {
			if (typeof slave.clone === "string") {
				makeSpan(c, slave.clone === PlayerName() ? `Your clone.` : `Clone of ${slave.clone}.`, "deepskyblue");
			}
		}

		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function long_rival(slave, c) {
			if (slave.rivalry !== 0) {
				const block = makeBlock(c);
				const ssj = V.slaves.find(s => s.ID === slave.rivalryTarget);
				if (ssj) {
					if (slave.rivalry <= 1) {
						makeSpan(block, "Dislikes", "lightsalmon");
						addText(block, ' ');
						helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
						addText(block, '.');
					} else if (slave.rivalry <= 2) {
						helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
						addText(block, "'s ");
						makeSpan(block, "rival.", "lightsalmon");
					} else {
						makeSpan(block, "Hates", "lightsalmon");
						addText(block, ' ');
						helpers.referenceSlaveWithPreview(block, ssj, SlaveFullName(ssj));
						addText(block, '.');
					}
				}
			}
		}

		return {
			long: {
				health: long_health,
				illness: long_illness,
				tired: long_tired,
				age: long_age,
				face: long_face,
				eyes: long_eyes,
				ears: long_ears,
				lips: long_lips,
				teeth: long_teeth,
				muscles: long_muscles,
				voice: long_voice,
				tits_ass: long_tits_ass,
				hips: long_hips,
				waist: long_waist,
				implants: long_implants,
				lactation: long_lactation,
				milkflavor: milk_flavor,
				mods: long_mods,
				intelligence: long_intelligence,
				skills: long_skills,
				prestige: long_prestige,
				porn_prestige: long_porn_prestige,
				clothes: long_clothes,
				collar: long_collar,
				mask: long_mask,
				mouth: long_mouth,
				belly: long_belly,
				arms: long_arms,
				legs: long_legs,
				shoes: long_shoes,
				chastity: long_chastity,
				vaginal_acc: long_vaginal_acc,
				dick_acc: long_dick_acc,
				buttplug: long_buttplug,
				fetish: long_fetish,
				attraction: long_attraction,
				smart_piercing: long_smart_piercing,
				behavior_flaw: long_behavior_flaw,
				sex_flaw: long_sex_flaw,
				behavior_quirk: long_behavior_quirk,
				sex_quirk: long_sex_quirk,
				family: long_family,
				clone: long_clone,
				rival: long_rival
			},
			short: {
				health: short_health,
				illness: short_illness,
				tired: short_tired,
				age: short_age,
				face: short_face,
				eyes: short_eyes,
				ears: short_ears,
				lips: short_lips,
				teeth: short_teeth,
				muscles: short_muscles,
				voice: short_voice,
				tits_ass: short_tits_ass,
				hips: short_hips,
				waist: short_waist,
				implants: short_implants,
				lactation: short_lactation,
				mods: short_mods,
				intelligence: short_intelligence,
				skills: short_skills,
				prestige: short_prestige,
				porn_prestige: short_porn_prestige,
				/*
				clothes: short_clothes,
				collar: short_collar,
				belly: short_belly,
				arms: short_arms,
				legs: short_legs,
				shoes: short_shoes,
				chastity: short_chastity,
				vaginal_acc: short_vaginal_acc,
				dick_acc: short_dick_acc,
				buttplug: short_buttplug,
				*/
				fetish: short_fetish,
				attraction: short_attraction,
				smart_piercing: short_smart_piercing,
				behavior_flaw: short_behavior_flaw,
				sex_flaw: short_sex_flaw,
				behavior_quirk: short_behavior_quirk,
				sex_quirk: short_sex_quirk,
				family: short_family,
				clone: short_clone,
				rival: short_rival
			}
		};
	}();

	return {
		helpers: helpers,
		bits: bits
	};
}();