code:
/**
 * Returns a block that describes an overview of the slave's impact on your reputation and finances in the short term
 * @param {FC.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 {FC.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 - Math.max(0, slave.weekAcquired);
		// Lifetime expense
		div = document.createElement("div");
		div.classList.add("indent");
		if (slave.lifetimeCashExpenses < 0) {
			text = `In ${his} `;
			if (weeksOwned > 0) {
				if (weeksOwned === 1) {
					text += `week`;
				} else {
					text += `${weeksOwned} weeks`;
				}
			} else {
				text += `time`;
			}
			// Time spent as your agent in another arcology will not be accounted for here.
			text += ` with you in the arcology, ${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) {
				if (weeksOwned === 1) {
					text += `week`;
				} else {
					text += `${weeksOwned} weeks`;
				}
			} else {
				text += `time`;
			}
			// Time spent as your agent in another arcology will not be accounted for here.
			text += ` with you in the arcology, ${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: Nithhogg/fc

license: mit


code:
/**
 * Plots the history of a slave's attribute over time
 * any changes here should be reflected in historyChange.js
 * @param {FC.SlaveState} slave
 */
globalThis.slaveHistoryGraph = function(slave) {
	const frag = new DocumentFragment();

	const history = slave.history;

	if (history.length <= 1) {
		const p = document.createElement('p');
		p.textContent = "No history available, pass some weeks.";
		frag.appendChild(p);
		return frag;
	}
	const plotKeys = Object.keys(history[0]).filter(
		key => key !== 'week' && key !== 'lastAssignment' && key !== 'expenses' && key !== 'reputation_loss'
	);

	const tabBar = new App.UI.Tabs.TabBar("SlaveHistoryGraph");
	const f = new DocumentFragment();
	tabBar.customNode = f;

	for (const plotKey of plotKeys) {
		if (plotKey === "income") {
			tabBar.addTab(plotKey, plotKey, plotData(plotKey, "expenses"));
			continue;
		} else if (plotKey === "reputation") {
			tabBar.addTab(plotKey, plotKey, plotData(plotKey, "reputation_loss"));
			continue;
		} else {
			tabBar.addTab(plotKey, plotKey, plotData(plotKey));
		}
	}

	frag.appendChild(tabBar.render());

	/**
	 *
	 * @param {string} key
	 * @param {string} [keyNeg]
	 * @returns {HTMLDivElement}
	 */
	function plotData(key, keyNeg = null) {
		const container = document.createElement('div');

		const plotWidth = 800;
		const plotHeight = 450;
		const canvas = App.UI.DOM.createCanvas(800, 450);

		const leftPlotMargin = 50;
		const rightPlotMargin = 75;
		const innerPlotMargin = 50;
		container.appendChild(canvas);

		const weeksAssignment = [];
		for (let i = 0; i < history.length; i++) {
			const assignmentKey = Object.keys(globalThis.Job).find(key => globalThis.Job[key] === history[i].lastAssignment);
			weeksAssignment.push({week: history[i].week, assignment: toTitleCase(assignmentKey)});
		}

		const textKey = toTitleCase(key.replace("_", " "));
		const values = history.map(entry => entry[key]);
		const textKeyNeg = keyNeg ? toTitleCase(keyNeg.replace("_", " ")) : "";
		const negValues = keyNeg ? history.map(entry => -Math.abs(entry[keyNeg])) : null;

		const ctx = canvas.getContext('2d');
		ctx.clearRect(0, 0, plotWidth, plotHeight);

		// set the font height,cannot access parent
		ctx.font = "16px/1 Helmet,Freesans,sans-serif";
		ctx.fillStyle = 'white';

		// Calculate min and max values for y-axis
		let minVal = Math.min(...values);
		if (negValues) {
			minVal = Math.min(minVal, ...negValues);
		}
		const axisMinInc = 20;
		const minAxis = (Math.floor((minVal -1) / axisMinInc)) * axisMinInc;

		let maxVal = Math.max(...values);
		let axisMaxInc;
		if (maxVal <= 100) {
			axisMaxInc = 20;
		} else if (maxVal <= 2000) {
			axisMaxInc = 100;
		} else {
			axisMaxInc = 1000;
		}
		const maxAxis = (Math.ceil((maxVal+1) / axisMaxInc)) * axisMaxInc;

		const xStep = (plotWidth - leftPlotMargin - rightPlotMargin - 2*innerPlotMargin) / (weeksAssignment.length - 1);
		const yScale = 300 / (maxAxis - minAxis);

		// Draw top y-axis line
		ctx.strokeStyle = 'gray';
		ctx.setLineDash([5, 15]);
		const topAxisMark = Math.round((maxVal+1) / axisMaxInc) * axisMaxInc;
		const yTopPos = 350 - (topAxisMark - minAxis) * yScale;
		ctx.beginPath();
		ctx.moveTo(leftPlotMargin, yTopPos);
		ctx.lineTo(plotWidth - rightPlotMargin, yTopPos);
		ctx.stroke();
		ctx.fillText(topAxisMark.toString(), 10, yTopPos);

		// Draw bottom y-axis line
		ctx.strokeStyle = 'gray';
		ctx.setLineDash([5, 15]);
		const botAxisMark = Math.round((minVal-1) / axisMinInc) * axisMaxInc;
		const yBotPos = 350 - (botAxisMark - minAxis) * yScale;
		ctx.beginPath();
		ctx.moveTo(leftPlotMargin, yBotPos);
		ctx.lineTo(plotWidth - rightPlotMargin, yBotPos);
		ctx.stroke();
		ctx.fillText(botAxisMark.toString(), 10, yBotPos);

		// Draw 0 y-axis line if present
		if (minVal < 0 && maxVal > 0) {
			ctx.strokeStyle = 'gray';
			ctx.setLineDash([5, 15]);
			const yZeroPos = 350 - (0 - minAxis) * yScale;
			ctx.beginPath();
			ctx.moveTo(50, yZeroPos);
			ctx.lineTo(plotWidth  - rightPlotMargin, yZeroPos);
			ctx.stroke();
			ctx.fillText('0', 10, yZeroPos);
		}

		// Set white color for lines and text
		ctx.strokeStyle = 'white';
		ctx.fillStyle = 'white';
		ctx.setLineDash([]);

		// Draw Box Border Over any tracing lines
		ctx.beginPath();
		ctx.moveTo(leftPlotMargin, 350);
		ctx.lineTo(plotWidth - rightPlotMargin, 350);
		ctx.lineTo(plotWidth - rightPlotMargin, 50);
		ctx.lineTo(leftPlotMargin, 50);
		ctx.lineTo(leftPlotMargin, 350);
		ctx.stroke();

		// Plot data
		ctx.beginPath();
		ctx.moveTo(leftPlotMargin + innerPlotMargin, 350 - (values[0] - minAxis) * yScale);
		for (let i = 0; i < weeksAssignment.length; i++) {
			ctx.lineTo(innerPlotMargin + leftPlotMargin + i * xStep, 350 - (values[i] - minAxis) * yScale);
		}
		ctx.stroke();
		// label x axis (weeks + assignment label)
		ctx.fillStyle = 'white';
		for (let i = 0; i < weeksAssignment.length; i++) {
			ctx.fillText(`Week ${weeksAssignment[i].week.toString()}`, 30 + innerPlotMargin + i * xStep, 370);
			ctx.fillText(weeksAssignment[i].assignment.toString(), 30 +innerPlotMargin + i * xStep, 390);
		}

		// Label y-values for each data point
		ctx.fillStyle = 'yellow';
		ctx.fillText(textKey, 100, 30);
		for (let i = 0; i < values.length; i++) {
			const xPos = innerPlotMargin + leftPlotMargin - 10 + i * xStep;
			const yPos = 350 - (values[i] - minAxis) * yScale;
			ctx.fillText(values[i].toString(), xPos, yPos - 10);
		}

		// Plot negative data if keyNeg is provided
		if (keyNeg) {
			ctx.strokeStyle = 'lightcoral';
			ctx.fillStyle = 'lightcoral';
			ctx.beginPath();
			ctx.moveTo(leftPlotMargin + innerPlotMargin, 350 - (negValues[0] - minAxis) * yScale);
			for (let i = 1; i < weeksAssignment.length; i++) {
				ctx.lineTo(leftPlotMargin + innerPlotMargin + i * xStep, 350 - (negValues[i] - minAxis) * yScale);
			}
			ctx.stroke();
			ctx.fillText(textKeyNeg.toString(), 250, 30);

			// label y-values for each negative data point
			for (let i = 0; i < negValues.length; i++) {
				const xPos = leftPlotMargin + innerPlotMargin - 10 + i * xStep;
				const yPos = 350 - (negValues[i] - minAxis) * yScale;
				ctx.fillText(negValues[i].toString(), xPos, yPos - 10);
			}
		}
		return container;
	}
	return frag;
};


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 {FC.SlaveState} slave
 * @returns {string}
 */

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

/**
 * @param {number[]} IDs
 * @param {Array.<{id: number, rejects: string[]}>} rejectedSlaves
 * @param {slaveToElement} interactionLink
 * @param {slaveToElement} [postNote]
 * @returns {DocumentFragment}
 */
App.UI.SlaveList.render = function(IDs, rejectedSlaves, interactionLink, postNote) {
	const facilityPassages = new Set(
		["Main", "Head Girl Suite", "Spa", "Brothel", "Club", "Arcade", "Clinic", "Schoolroom", "Dairy", "Farmyard",
			"Servants' Quarters", "Master Suite", "Cellblock"]);

	// required, if multiple list are displayed on the same passage
	const uuid = generateNewID();
	const passageName = passage();

	const res = document.createDocumentFragment();

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

	res.append(createQuickList(IDs, uuid));

	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 slaveSummary = renderSlave(sid, interactionLink, showTransfers, postNote);
		let slaveDiv = document.createElement("div");
		slaveDiv.id = `slave-${sid}-${uuid}`;
		slaveDiv.classList.add("slaveSummary");
		if (V.slavePanelStyle === 2) {
			slaveDiv.classList.add("card");
		}
		slaveDiv.appendChild(slaveSummary);
		res.appendChild(slaveDiv);
	}

	for (const rs of rejectedSlaves) {
		const slave = getSlave(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 reject of rejects) {
				const li = document.createElement("li");
				li.innerHTML = reject;
				ul.appendChild(li);
			}
			slaveDiv.appendChild(ul);
		}
		res.appendChild(slaveDiv);
	}

	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) {
			res.appendChild(App.UI.sectionBreak());
		}
		const slave = getSlave(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 {
			if (slave.assignment === Job.IMPORTED) {
				assignment.innerText = "be imported";
			} 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 (V.spaFix !== 2 && ((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");
			}
			if (!((slave.skill.penetrative > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.penetrative > 10)) || (penetrativeSocialUse() < 40))) {
				lessons.push("penetrative");
			}
			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
	 * @param {string} uuid
	 * @returns {DocumentFragment}
	 */
	function createQuickList(IDs, uuid) {
		if (V.useSlaveListInPageJSNavigation < 1 || IDs.length < 2 || passageName !== "Main") {
			return new DocumentFragment();
		}

		const f = new DocumentFragment();
		let currentSort = null;

		let toggleButton;
		if (V.useSlaveListInPageJSNavigation === 1) {
			toggleButton = App.UI.DOM.appendNewElement("button", f, "Quick-List", ["quick-button"]);
		}

		const indexSorting = document.createElement("div");
		f.append(indexSorting);

		const sortSpan = App.UI.DOM.makeElement("span", "None", ["bold"]);

		indexSorting.append("Sorting: ", sortSpan, " ",
			App.UI.DOM.link("Sort by Devotion",
				() => sortButtons("Devotion", (a, b) => b.devotion - a.devotion, "devotion")),
			" | ",
			App.UI.DOM.link("Sort by Trust", () => sortButtons("Trust", (a, b) => b.trust - a.trust, "trust"))
		);

		const indexContainer = document.createElement("div");
		indexContainer.classList.add("quick-list");
		f.append(indexContainer);
		/**
		 * @typedef {object} JumpButton
		 * @property {FC.SlaveState} slave
		 * @property {HTMLButtonElement} button
		 */
		/**
		 * @type {JumpButton[]}
		 */
		const jumpButtons = [];
		for (const id of IDs) {
			const btn = document.createElement("button");
			const slave = getSlave(id);
			btn.append(SlaveFullName(slave));
			btn.classList.add(getSlaveDevotionClass(slave), getSlaveTrustClass(slave));
			btn.onclick = () => $('html, body').animate({scrollTop: $(`#slave-${id}-${uuid}`).offset().top - 50}, 300);
			jumpButtons.push({slave: slave, button: btn});
			indexContainer.append(btn);
		}

		if (V.useSlaveListInPageJSNavigation === 1) {
			App.UI.DOM.elementToggle(toggleButton, [indexSorting, indexContainer]);
		}

		return f;

		/**
		 * @param {string} name
		 * @param {function(FC.SlaveState, FC.SlaveState):number} compareFn
		 * @param {string} colorClass
		 */
		function sortButtons(name, compareFn, colorClass) {
			$(sortSpan).empty().append(name);
			jumpButtons.sort((a, b) => compareFn(a.slave, b.slave));
			$(indexContainer).empty().append(jumpButtons.map(v => v.button));
			if (currentSort) {
				indexContainer.classList.remove(currentSort);
			}
			indexContainer.classList.add(colorClass);
			currentSort = colorClass;
		}
	}
};

App.UI.SlaveList.Decoration = {};
/**
 * returns "HG", "BG", "PA", and "RC" prefixes
 * @param {FC.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 {FC.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 {FC.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.temp.AS = slave.ID;
	});

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

	return link;
};

/**
 * @param {FC.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");

	let span = App.UI.DOM.makeElement("span", "Sort by: ");
	const orderMap = Array.from(App.Data.SlaveSorting.entries())
		.map(([name, key]) => {
			return {key: key, name: name};
		});
	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: ");
	const order = ["descending", "ascending"];
	span.append(App.UI.DOM.generateLinksStrip(order.map(so =>
		V.sortSlavesOrder === so ? capFirstChar(so)
			: App.UI.DOM.link(capFirstChar(so), () => {
				V.sortSlavesOrder = so;
				App.UI.reload();
			})
	)));
	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
			? getSlaves().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 = getSlaves().asArray().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(getSlave(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 getSlaves().values()) {
		// @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.temp.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 (slaveCount() > 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
	 * @param {string} jobName
	 * @returns {HTMLSpanElement}
	 */
	function encyclopediaTips(jobName) {
		const span = document.createElement("span");
		span.classList.add("note");
		if (V.showTipsFromEncy) {
			switch (jobName) {
				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 penthouseTab() {
		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("penthouse", `Penthouse${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));
	}

	function arcologyTab() {
		const allSlaveIDs = getSlaves().map((slave) => slave.ID);
		SlaveSort.IDs(allSlaveIDs);
		return makeTabDesc("arcology", `Arcology${V.useSlaveSummaryTabs > 0 ? ` (${allSlaveIDs.length})` : ""}`,
			App.UI.SlaveList.render(allSlaveIDs, [], 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.favorites.length > 0 || V.useSlaveSummaryTabs === 0) {
		tabs.push(favorites());
	}

	tabs.push(penthouseTab());

	// 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(encyclopediaTips(jn), slaves.dom)));
		} else if (V.useSlaveSummaryTabs === 0) {
			tabs.push(makeTabDesc(jn, ph.desc.jobs[jn].position, encyclopediaTips(jn)));
		}
	}

	if (V.useSlaveArcologyTab) {
		tabs.push(arcologyTab());
	}

	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 {FC.SlaveState} slave
 * @returns {string[]}
 */

/**
 * @callback slaveFilterCallbackSimple
 * @param {FC.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) {
		/** @type {Array<number>} */
		let unfilteredIDs;
		switch (assignmentStr) {
			case 'all':
				unfilteredIDs = getSlaves().map(s => s.ID);
				break;
			case 'experienced':
				unfilteredIDs = getSlaves().asArray().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(getSlave(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.

// cSpell:ignore Natr

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 {{[key: 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 {{[key: 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) {
				const policy = arcology[fsp];
				const p = fsp.slice(2);
				FSData.policy[p] = {
					active: _.isNil(policy) ? 0 : 1,
					strength: Math.trunc((policy ?? 0) / 10)
				};
			}

			if (FSData.policy.SlimnessEnthusiast.active) {
				FSData.bigButts = -1;
			} else if ((FSData.policy.TransformationFetishist.strength >= 2) || (FSData.policy.HedonisticDecadence.strength >= 2) || (FSData.policy.AssetExpansionist.strength >= 2) || (arcology.FSIntellectualDependencyLawBeauty >= 1)) {
				FSData.bigButts = 1;
			} else {
				FSData.bigButts = 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 {Partial<Record<FC.FSName<FC.FutureSociety>, FSDatum>>}} */
			policy: {},
			/**
			 * -1: disapprove, 0: don't really care, 1: fashionable
			 * @type {-1|0|1} */
			bigButts: 0,
		};

		/**
		 *
		 * @param {ParentNode} container
		 * @param {FC.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 {FC.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 (areRelated(V.PC, slave) || areDistantlyRelated(V.PC, slave)) {
				addText(block, `Your `);
				if (slave.relationship < -1) {
					makeSpan(block, bits.makeBit(toSentence([...relativeTerms(V.PC, slave), PCrelationshipTerm(slave)])), cssClassName);
					handled = 1;
				} else {
					makeSpan(block, bits.makeBit(relativeTerm(V.PC, slave)), cssClassName);
				}
			}
			if (slave.mother > 0) {
				const ssj = getSlaves().find(s => s.ID === slave.mother)?.value;
				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 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 = getSlaves().find(s => s.ID === slave.father)?.value;
				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 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 (slave.daughters === 1) {
				const ssj = getSlaves().find(s => s.mother === slave.ID || s.father === slave.ID)?.value;
				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 = getSlaves().find(s => areSisters(s, slave) > 0)?.value;
				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 = getSlaves().find(s => s.ID === slave.relationshipTarget)?.value;
				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 {FC.SlaveState} slave
		 * @returns {number} 0 if butt is considered too small, 1 is too big, 2 is too big but that's fashionable, -1 otherwise.
		 */
		function hipsAssRating(slave) {
			const bigButts = helpers.FSData.bigButts;
			const bigButtReaction = bigButts > 0 ? 2 : 1;

			if (slave.hips < -1) {
				if (slave.butt > 2 && bigButts <= 0) {
					return bigButtReaction;
				}
			} else if (slave.hips < 0) {
				if (slave.butt > 4 && bigButts <= 0) {
					return bigButtReaction;
				}
			} else if (slave.hips > 2) {
				if (slave.butt < 9) {
					return 0;
				}
			} else if (slave.hips > 1) {
				if (slave.butt < 4 && (bigButts === 0 || (slave.boobs >= 500))) {
					return 0;
				}
			} else if (slave.hips > 0) {
				if (slave.butt > 8) {
					return bigButtReaction;
				} else if (slave.butt < 3 && ((bigButts === 0) || (slave.boobs >= 500))) {
					return 0;
				}
			} else {
				if (slave.butt > 6) {
					if (bigButts <= 0) {
						return bigButtReaction;
					}
				} else if (slave.butt < 2 && (bigButts === 0 || (slave.boobs >= 500))) {
					return 0;
				}
			}
			return -1;
		}

		/**
		 * @param {FC.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.fetishStrength <= 95 || !smartPiercingReinforcesFetish(slave)) {
					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 {FC.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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortHealth(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortIllness(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortTired(slave, c) {
			helpers.makeRatedStyledSpan(c, data.short.health.tiredness, slave.health.tired, 0, true);
		}

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longHealth(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longIllness(slave, c) {
			makeRatedStyledSpan(c, data.long.health.illness, slave.health.illness, 0, true);
		}

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longAge(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longFace(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longEyes(slave, c) {
			if (!canSee(slave)) {
				makeSpan(c, "Blind.", "red");
			} else if (!canSeePerfectly(slave)) {
				makeSpan(c, "Nearsighted.", "yellow");
			}
		}

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longEars(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longLips(slave, c) {
			makeRatedStyledSpan(c, data.long.body.lips, slave.lips, 0, true);
		}

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longMuscles(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longVoice(slave, c) {
			if (slave.voice === 0) {
				makeSpan(c, "Mute.", "red");
			} else {
				helpers.makeMappedStyledSpan(c, data.long.accent, slave.accent);
			}
		}

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longTitsAndAss(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longHips(slave, c) {
			const di = hipsAssRating(slave);
			if (di >= 0) {
				helpers.makeMappedStyledSpan(c, data.long.body.hipsAss, di);
			}
		}

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longImplants(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longLactation(slave, c) {
			if (slave.lactation === 1) {
				makeSpan(c, "Lactating naturally.", "pink");
			} else if (slave.lactation === 2) {
				makeSpan(c, "Heavy lactation.", "pink");
			}
		}

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longMods(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortAge(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortFace(slave, c) {
			makeRatedStyledSpan(c, data.short.body.face, slave.face, 100, true);
		}

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

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

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

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortMuscles(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortVoice(slave, c) {
			if (slave.voice === 0) {
				makeSpan(c, "Mute", "red");
			} else {
				helpers.makeMappedStyledSpan(c, data.short.accent, slave.accent);
			}
		}

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortTitsAndAss(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortHips(slave, c) {
			const di = hipsAssRating(slave);
			if (di >= 0) {
				helpers.makeMappedStyledSpan(c, data.short.body.hipsAss, di);
			}
		}

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortImplants(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortLactation(slave, c) {
			if (slave.lactation === 1) {
				makeSpan(c, "Lact", "pink");
			} else if (slave.lactation === 2) {
				makeSpan(c, "Lact", "pink");
			}
		}

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortMods(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortIntelligence(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortSkills(slave, c) {
			const sd = data.short.skills;
			let SSkills = (slave.skill.anal + slave.skill.oral);
			let PSU = penetrativeSocialUse();
			if (
				((SSkills + slave.skill.whoring + slave.skill.entertainment) >= 400) &&
				((slave.vagina < 0) || (slave.skill.vaginal >= 100)) &&
				((PSU < 60 && (PSU < 40 || (!canAchieveErection(slave) && slave.clit < 3))) || (slave.skill.penetrative >= 100))
			) {
				helpers.makeStyledSpan(c, sd.mss);
			} else {
				SSkills += slave.skill.vaginal + adjustedPenSkill(slave);
				helpers.makeStyledSpan(c, App.Ratings.multiNumeric(sd.sex, [SSkills, ((slave.vagina >= 0 ? 1 : 0) + (slave.dick > 0 ? 2 : 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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortPrestige(slave, c) {
			helpers.makeMappedStyledSpan(c, data.short.prestige, slave.prestige);
		}

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longIntelligence(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longSkills(slave, c) {
			const sd = data.long.skills;
			let SSkills = (slave.skill.anal + slave.skill.oral);
			let PSU = penetrativeSocialUse();
			if (
				((SSkills + slave.skill.whoring + slave.skill.entertainment) >= 400) &&
				((slave.vagina < 0) || (slave.skill.vaginal >= 100)) &&
				((PSU < 60 && (PSU < 40 || (!canAchieveErection(slave) && slave.clit < 3))) || (slave.skill.penetrative >= 100))
			) {
				helpers.makeStyledSpan(c, sd.mss);
			} else {
				SSkills += slave.skill.vaginal + adjustedPenSkill(slave);
				helpers.makeStyledSpan(c, App.Ratings.multiNumeric(sd.sex, [SSkills, ((slave.vagina >= 0 ? 1 : 0) + (slave.dick > 0 ? 2 : 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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longPrestige(slave, c) {
			helpers.makeMappedStyledSpan(c, data.long.prestige, slave.prestige);
		}

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

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

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

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

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

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

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

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

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longChastity(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longVaginalAcc(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longDickAcc(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longButtplug(slave, c) {
			helpers.makeMappedSpan(c, data.long.accessory.buttplug, slave.buttplug);
			helpers.makeMappedSpan(c, data.long.accessory.buttplugAttachment, slave.buttplugAttachment);
		}

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortFetish(slave, c) {
			/**
			 * @param {FC.SlaveState} slave
			 */
			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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortAttraction(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortSmartPiercing(slave, c) {
			const s = smartFetishStr(slave, data.short.smartVibrator) || smartAttractionStr(slave, data.short.smartVibrator);
			if (s) {
				makeSpan(c, s);
			}
		}

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

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

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

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longFetish(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longAttraction(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longSmartPiercing(slave, c) {
			const s = smartFetishStr(slave, data.long.smartVibrator) || smartAttractionStr(slave, data.long.smartVibrator);
			if (s) {
				makeSpan(c, s);
			}
		}

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longSexFlaw(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longBehaviorQuirk(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longSexQuirk(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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortFamily(slave, c) {
			helpers.renderFamily(c, slave, true);
		}

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function shortRival(slave, c) {
			if (slave.rivalry !== 0) {
				const block = makeBlock(c, "lightsalmon");
				const ssj = getSlaves().find(s => s.ID === slave.rivalryTarget)?.value;
				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 {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longFamily(slave, c) {
			helpers.renderFamily(c, slave, false);
		}

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

		/**
		 * @param {FC.SlaveState} slave
		 * @param {Node} c
		 * @returns {void}
		 */
		function longRival(slave, c) {
			if (slave.rivalry !== 0) {
				const block = makeBlock(c);
				const ssj = getSlaves().find(s => s.ID === slave.rivalryTarget)?.value;
				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: longHealth,
				illness: longIllness,
				tired: longTired,
				age: longAge,
				face: longFace,
				eyes: longEyes,
				ears: longEars,
				lips: longLips,
				teeth: longTeeth,
				muscles: longMuscles,
				voice: longVoice,
				titsAndAss: longTitsAndAss,
				hips: longHips,
				waist: longWaist,
				implants: longImplants,
				lactation: longLactation,
				milkflavor: milkFlavor,
				mods: longMods,
				intelligence: longIntelligence,
				skills: longSkills,
				prestige: longPrestige,
				pornPrestige: longPornPrestige,
				clothes: longClothes,
				collar: longCollar,
				mask: longMask,
				mouth: longMouth,
				belly: longBelly,
				arms: longArms,
				legs: longLegs,
				shoes: longShoes,
				chastity: longChastity,
				vaginalAcc: longVaginalAcc,
				dickAcc: longDickAcc,
				buttplug: longButtplug,
				fetish: longFetish,
				attraction: longAttraction,
				smartPiercing: longSmartPiercing,
				behaviorFlaw: longBehaviorFlaw,
				sexFlaw: longSexFlaw,
				behaviorQuirk: longBehaviorQuirk,
				sexQuirk: longSexQuirk,
				family: longFamily,
				clone: longClone,
				rival: longRival
			},
			short: {
				health: shortHealth,
				illness: shortIllness,
				tired: shortTired,
				age: shortAge,
				face: shortFace,
				eyes: shortEyes,
				ears: shortEars,
				lips: shortLips,
				teeth: shortTeeth,
				muscles: shortMuscles,
				voice: shortVoice,
				titsAndAss: shortTitsAndAss,
				hips: shortHips,
				waist: shortWaist,
				implants: shortImplants,
				lactation: shortLactation,
				mods: shortMods,
				intelligence: shortIntelligence,
				skills: shortSkills,
				prestige: shortPrestige,
				pornPrestige: shortPornPrestige,
				/*
				clothes: shortClothes,
				collar: shortCollar,
				belly: shortBelly,
				arms: shortArms,
				legs: shortLegs,
				shoes: shortShoes,
				chastity: shortChastity,
				vaginalAcc: shortVaginalAcc,
				dickAcc: shortDickAcc,
				buttplug: shortButtplug,
				*/
				fetish: shortFetish,
				attraction: shortAttraction,
				smartPiercing: shortSmartPiercing,
				behaviorFlaw: shortBehaviorFlaw,
				sexFlaw: shortSexFlaw,
				behaviorQuirk: shortBehaviorQuirk,
				sexQuirk: shortSexQuirk,
				family: shortFamily,
				clone: shortClone,
				rival: shortRival
			}
		};
	}();

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