code:
App.Markets["Slave Shelter"] = function() {
	const el = new DocumentFragment();
	let r = [];

	if (V.shelterSlaveBought === 1) {
		App.UI.DOM.appendNewElement("span", el, ` You already bought a shelter slave this week.`);
	} else {
		r.push(`You contact the Slave Shelter to review the profile of the slave the Shelter is offering to a good`);
		if (V.PC.title === 1) {
			r.push(`Master`);
		} else {
			r.push(`Mistress`);
		}
		r.push(`willing to pay the Shelter's nominal placement fee. The severe, tired-looking woman who answers your call hurries through the disclaimers. "All Shelter Slaves are provided as-is... the Shelter provides a single slave for placement each week... resale of Shelter slaves is contractually forbidden... we reserve the right to unannounced inspection of placed slaves... the Shelter follows up on reports of abuse or resale of its slaves..."`);

		App.UI.DOM.appendNewElement("span", el, r.join(" "));

		if (V.shelterSlave === 0) {
			const possibleOrigins = [];
			possibleOrigins.push("broken");
			if (V.seeExtreme === 1) {
				possibleOrigins.push("amputee", "blind", "deaf");
				if (jsRandom(1, 100) <= V.seeDicks) {
					possibleOrigins.push("cannibal victim male");
				} else {
					possibleOrigins.push("cannibal victim female");
				}
			}
			if (jsRandom(1, 100) <= V.seeDicks) {
				possibleOrigins.push("dickpain", "gelding");
			}
			if (jsRandom(0, 99) >= V.seeDicks) {
				possibleOrigins.push("plugs", "reaction", "used whore");
				if (V.seePreg !== 0) {
					possibleOrigins.push("breeder", "broken womb");
				}
			}
			if (V.week > 80) {
				possibleOrigins.push("degraded DoL");
			}

			const origin = possibleOrigins.random();
			let pedo;
			let minAge;
			let maxAge;
			switch (origin) {
				case "broken":
					V.shelterSlave = GenerateNewSlave(null, {disableDisability: 1});
					V.shelterSlave.origin = "$He has never communicated anything about $his background, since $he arrived at the shelter with a broken mind.";
					V.shelterSlave.career = "a slave";
					setHealth(V.shelterSlave, jsRandom(-30, -10));
					V.shelterSlave.anus = 3;
					applyMindbroken(V.shelterSlave, V.shelterSlave.intelligence);
					break;
				case "amputee":
					V.shelterSlave = GenerateNewSlave(null, {disableDisability: 1});
					V.shelterSlave.origin = "It's not clear why $his previous owner cut $his arms and legs off.";
					V.shelterSlave.career = "a slave";
					removeLimbs(V.shelterSlave, "all");
					V.shelterSlave.devotion = jsRandom(-100, -90);
					V.shelterSlave.trust = jsRandom(-100, -90);
					setHealth(V.shelterSlave, jsRandom(-60, -40), normalRandInt(10, 3), normalRandInt(20, 3));
					V.shelterSlave.behavioralFlaw = either("hates men", "hates women", "odd");
					V.shelterSlave.sexualFlaw = either("apathetic", "hates anal", "hates oral", "hates penetration");
					break;
				case "blind":
					V.shelterSlave = GenerateNewSlave(null, {minAge: V.minimumSlaveAge, maxAge: 22, disableDisability: 1});
					V.shelterSlave.origin = "$His owner purposely blinded $him by dumping boiling water into $his eyes.";
					V.shelterSlave.career = "a slave";
					eyeSurgery(V.shelterSlave, "both", "blind");
					setEyeColor(V.shelterSlave, "milky white");
					App.Medicine.Modification.addScar(V.shelterSlave, "face", "burn", 2);
					V.shelterSlave.devotion = jsRandom(-100, -90);
					V.shelterSlave.trust = jsRandom(-100, -90);
					setHealth(V.shelterSlave, jsRandom(-60, -40), normalRandInt(10, 3), normalRandInt(20, 3));
					V.shelterSlave.behavioralFlaw = "hates men";
					V.shelterSlave.sexualFlaw = "hates oral";
					V.shelterSlave.canRecruit = 0;
					V.shelterSlave.override_Eye_Color = 1; // TODO: Identifier 'override_Eye_Color' is not in camel case
					break;
				case "deaf":
					V.shelterSlave = GenerateNewSlave(null, {minAge: V.minimumSlaveAge, maxAge: 22, disableDisability: 1});
					V.shelterSlave.origin = "$His owner purposely deafened $him by piercing $his eardrums with knitting needles.";
					V.shelterSlave.career = "a slave";
					V.shelterSlave.hears = -2;
					V.shelterSlave.devotion = jsRandom(-100, -90);
					V.shelterSlave.trust = jsRandom(-100, -90);
					setHealth(V.shelterSlave, jsRandom(-60, -40), normalRandInt(10, 3), normalRandInt(20, 3));
					V.shelterSlave.behavioralFlaw = "hates men";
					V.shelterSlave.sexualFlaw = "hates oral";
					V.shelterSlave.canRecruit = 0;
					break;
				case "broken womb":
					V.shelterSlave = GenerateNewSlave("XX", {minAge: 6, maxAge: 22, disableDisability: 1});
					V.shelterSlave.origin = "$He was found unresponsive in the lower arcology with a gaping pussy and deflated belly. It is unclear what happened to $him.";
					V.shelterSlave.career = "homeless";
					setHealth(V.shelterSlave, jsRandom(-50, -30), normalRandInt(10, 3), normalRandInt(20, 3));
					V.shelterSlave.weight = -50;
					V.shelterSlave.vagina = 9;
					V.shelterSlave.bellySag = 20;
					applyMindbroken(V.shelterSlave, V.shelterSlave.intelligence);
					break;
				case "cannibal victim male":
					if (V.pedo_mode === 1) {
						pedo = 1;
					} else {
						maxAge = 42;
					}
					V.shelterSlave = GenerateNewSlave("XY", {
						minAge: 16, maxAge: maxAge, disableDisability: 1, ageOverridesPedoMode: pedo
					});
					V.shelterSlave.origin = "$His previous owner forced $him to cut off $his dick and balls and cook them.";
					V.shelterSlave.career = "a slave";
					V.shelterSlave.devotion = jsRandom(-100, -80);
					V.shelterSlave.trust = jsRandom(-100, -90);
					setHealth(V.shelterSlave, jsRandom(-60, -40), normalRandInt(10, 3), normalRandInt(20, 3));
					V.shelterSlave.dick = 0;
					V.shelterSlave.foreskin = 0;
					V.shelterSlave.balls = 0;
					V.shelterSlave.scrotum = 0;
					V.shelterSlave.behavioralFlaw = either("anorexic", "anorexic", "anorexic", "odd");
					V.shelterSlave.sexualFlaw = either("apathetic", "neglectful", "self hating");
					V.shelterSlave.fetish = either("masochist", "none", "none");
					App.Medicine.Modification.addScar(V.shelterSlave, "pubic mound", "scars from $his crudely performed self-castration");
					break;
				case "cannibal victim female":
					if (V.pedo_mode === 1) {
						pedo = 1;
					} else {
						maxAge = 42;
					}
					V.shelterSlave = GenerateNewSlave("XX", {
						minAge: 16, maxAge: maxAge, disableDisability: 1, ageOverridesPedoMode: pedo
					});
					V.shelterSlave.origin = "$His previous owner forced $him to cut off $his breasts and cook them.";
					V.shelterSlave.career = "a slave";
					V.shelterSlave.devotion = jsRandom(-100, -80);
					V.shelterSlave.trust = jsRandom(-100, -90);
					setHealth(V.shelterSlave, jsRandom(-60, -40), normalRandInt(10, 3), normalRandInt(20, 3));
					V.shelterSlave.boobs = 10;
					V.shelterSlave.boobsImplant = 0;
					V.shelterSlave.boobsImplantType = "none";
					V.shelterSlave.nipples = "tiny";
					V.shelterSlave.behavioralFlaw = either("anorexic", "anorexic", "anorexic", "odd");
					V.shelterSlave.sexualFlaw = either("apathetic", "neglectful", "self hating");
					V.shelterSlave.fetish = either("masochist", "none", "none");
					App.Medicine.Modification.addScar(V.shelterSlave, "left breast", "an ugly mess of scar tissue");
					App.Medicine.Modification.addScar(V.shelterSlave, "right breast", "an ugly mess of scar tissue");
					break;
				case "degraded DoL":
					V.shelterSlave = GenerateNewSlave(null, {minAge: 14, disableDisability: 1, ageOverridesPedoMode: 1});
					V.shelterSlave.origin = "$He is an enslaved Daughter of Liberty, caught some weeks after the failed coup. $His previous owner used $him as a punching bag and dart board, then when he was bored of $him tattooed obscenities all over $his body and threw $him away.";
					V.shelterSlave.career = "a slave";
					V.shelterSlave.devotion = jsRandom(-100, -90);
					V.shelterSlave.trust = jsRandom(-100, -90);
					setHealth(V.shelterSlave, jsRandom(-60, -40), normalRandInt(10, 3), normalRandInt(20, 3));
					V.shelterSlave.slaveName = either("Cumbitch", "Cumbucket", "Cumdoll", "Cumgulper", "Fuckhole", "Fuckmeat", "Fuckpuppet", "Fuckslut", "Fucktoy", "Rapemeat", "Sluttypig", "Spunkbucket", "Spunkswallow", "Whorelips");
					V.shelterSlave.slaveSurname = 0;
					V.shelterSlave.minorInjury = either("black eye", "bruise", "split lip", 0);
					V.shelterSlave.weight = jsRandom(-90, -30);
					V.shelterSlave.muscles = jsRandom(-75, 0);
					V.shelterSlave.energy = jsRandom(0, 30);
					V.shelterSlave.fetish = either("humiliation", "humiliation", "masochist", "none", "none", "submissive");
					V.shelterSlave.boobsTat = either("degradation", "rude words", "none");
					V.shelterSlave.buttTat = either("degradation", "rude words", 0);
					V.shelterSlave.lipsTat = either("degradation", "rude words", 0);
					V.shelterSlave.anusTat = either("degradation", "rude words", 0);
					V.shelterSlave.shouldersTat = either("degradation", "rude words", 0);
					V.shelterSlave.armsTat = either("degradation", "rude words", 0);
					V.shelterSlave.legsTat = either("degradation", "rude words", 0);
					V.shelterSlave.backTat = either("degradation", "rude words", 0);
					V.shelterSlave.stampTat = either("degradation", "rude words", 0);
					V.shelterSlave.skill.anal = jsRandom(10, 25);
					V.shelterSlave.anus = jsRandom(1, 4);
					if (isFertile(V.shelterSlave) && V.seePreg !== 0) {
						V.shelterSlave.preg = either(-3, -2, -2, -2, 0, 0, 2, 3, 4, 5);
						if (V.shelterSlave.preg > 0) {
							V.shelterSlave.pregSource = -2;
							V.shelterSlave.pregKnown = 1;
							V.shelterSlave.pregType = setPregType(V.shelterSlave);
							WombImpregnate(V.shelterSlave, V.shelterSlave.pregType, V.shelterSlave.pregSource, V.shelterSlave.preg);
						}
					}
					if (V.shelterSlave.vagina > -1) {
						V.shelterSlave.vagina = jsRandom(1, 4);
						V.shelterSlave.skill.vaginal = jsRandom(10, 25);
						V.shelterSlave.vaginaTat = either("degradation", "rude words", 0);
					}
					if (V.shelterSlave.dick > 1) {
						V.shelterSlave.dickTat = either("degradation", "rude words", 0);
					}
					break;
				case "gelding":
					V.shelterSlave = GenerateNewSlave("XY", {minAge: 20, maxAge: 42, disableDisability: 1});
					V.shelterSlave.origin = "$His previous owner gelded $him and used $him for anal abuse.";
					V.shelterSlave.career = "a slave";
					V.shelterSlave.devotion = jsRandom(-75, -60);
					V.shelterSlave.trust = jsRandom(-100, -75);
					setHealth(V.shelterSlave, jsRandom(-50, -30));
					V.shelterSlave.face = jsRandom(-60, -20);
					V.shelterSlave.dick = jsRandom(1, 2);
					V.shelterSlave.balls = 0;
					V.shelterSlave.anus = 4;
					V.shelterSlave.skill.oral = 0;
					V.shelterSlave.skill.anal = 15;
					V.shelterSlave.skill.whoring = 0;
					V.shelterSlave.skill.entertainment = 0;
					V.shelterSlave.skill.combat = 0;
					V.shelterSlave.attrXY = jsRandom(40, 60);
					V.shelterSlave.behavioralFlaw = either("anorexic", "gluttonous", "hates men", "odd");
					V.shelterSlave.sexualFlaw = either("apathetic", "hates anal");
					break;
				case "dickpain":
					V.shelterSlave = GenerateNewSlave("XY", {minAge: 20, maxAge: 42, disableDisability: 1});
					V.shelterSlave.origin = "$His background is obscure, but seems to have involved terrible abuse of $his huge cock and balls.";
					V.shelterSlave.career = "a slave";
					V.shelterSlave.devotion = jsRandom(-75, -60);
					V.shelterSlave.trust = jsRandom(-100, -75);
					setHealth(V.shelterSlave, jsRandom(-50, -30));
					V.shelterSlave.face = jsRandom(-60, -20);
					V.shelterSlave.dick = jsRandom(4, 5);
					V.shelterSlave.balls = jsRandom(4, 5);
					V.shelterSlave.anus = 0;
					V.shelterSlave.skill.oral = 0;
					V.shelterSlave.skill.anal = 0;
					V.shelterSlave.skill.whoring = 0;
					V.shelterSlave.skill.entertainment = 0;
					V.shelterSlave.skill.combat = 0;
					V.shelterSlave.energy = jsRandom(5, 10);
					V.shelterSlave.attrXX = 0;
					V.shelterSlave.behavioralFlaw = either("hates women", "odd");
					V.shelterSlave.sexualFlaw = either("apathetic");
					break;
				case "plugs":
					V.shelterSlave = GenerateNewSlave("XX", {minAge: 20, maxAge: 42, disableDisability: 1});
					V.shelterSlave.origin = "$His holes were cruelly stretched by constant plug use.";
					V.shelterSlave.career = "a slave";
					V.shelterSlave.devotion = jsRandom(-75, -60);
					V.shelterSlave.trust = jsRandom(-100, -75);
					setHealth(V.shelterSlave, jsRandom(-50, -30));
					V.shelterSlave.vagina = 4;
					V.shelterSlave.anus = 4;
					V.shelterSlave.skill.oral = 15;
					V.shelterSlave.skill.anal = 15;
					V.shelterSlave.skill.vaginal = 15;
					V.shelterSlave.skill.whoring = 0;
					V.shelterSlave.skill.entertainment = 0;
					V.shelterSlave.skill.combat = 0;
					V.shelterSlave.behavioralFlaw = either("anorexic", "gluttonous", "odd");
					V.shelterSlave.sexualFlaw = either("hates anal", "hates oral", "hates penetration");
					break;
				case "breeder":
					if (V.pedo_mode === 1) {
						minAge = (V.fertilityAge + 6);
						pedo = 1; // Old enough to have been pregnant many times.
					} else {
						minAge = 30;
						maxAge = 42;
					}
					V.shelterSlave = GenerateNewSlave("XX", {
						minAge: minAge, maxAge: maxAge, disableDisability: 1, ageOverridesPedoMode: pedo
					});
					V.shelterSlave.origin = "$His previous owner discarded $him after many pregnancies.";
					V.shelterSlave.career = "a breeder";
					V.shelterSlave.devotion = jsRandom(-75, -60);
					V.shelterSlave.trust = jsRandom(-100, -75);
					setHealth(V.shelterSlave, jsRandom(-50, -30));
					V.shelterSlave.preg = -2;
					V.shelterSlave.counter.birthsTotal = 13;
					V.shelterSlave.pregAdaptation = 60;
					V.shelterSlave.bellySag = 10;
					V.shelterSlave.bellySagPreg = 10;
					V.shelterSlave.vagina = 3;
					V.shelterSlave.skill.oral = 0;
					V.shelterSlave.skill.anal = 0;
					V.shelterSlave.skill.vaginal = 0;
					V.shelterSlave.skill.whoring = 0;
					V.shelterSlave.skill.entertainment = 0;
					V.shelterSlave.skill.combat = 0;
					V.shelterSlave.behavioralFlaw = either("gluttonous", "hates men", "odd");
					V.shelterSlave.sexualFlaw = either("apathetic", "hates penetration", "repressed");
					break;
				case "used whore":
					V.shelterSlave = GenerateNewSlave("XX", {
						minAge: 32, maxAge: 42, disableDisability: 1, ageOverridesPedoMode: 1
					});// 20 years of brothel service.
					V.shelterSlave.origin = "$He was worn out by twenty years of brothel service.";
					V.shelterSlave.career = "a prostitute";
					V.shelterSlave.devotion = jsRandom(-75, -60);
					V.shelterSlave.trust = jsRandom(-100, -75);
					setHealth(V.shelterSlave, jsRandom(-60, -40), normalRandInt(10, 3));
					V.shelterSlave.face = jsRandom(-60, -20);
					V.shelterSlave.preg = -2;
					V.shelterSlave.vagina = 4;
					V.shelterSlave.anus = 4;
					V.shelterSlave.skill.oral = 35;
					V.shelterSlave.skill.anal = 35;
					V.shelterSlave.skill.vaginal = 35;
					V.shelterSlave.skill.whoring = 35;
					V.shelterSlave.skill.entertainment = 15;
					V.shelterSlave.skill.combat = 0;
					V.shelterSlave.behavioralFlaw = either("anorexic", "gluttonous", "hates men", "odd");
					V.shelterSlave.sexualFlaw = either("apathetic", "hates anal", "hates oral", "hates penetration");
					break;
				case "reaction":
					V.shelterSlave = GenerateNewSlave(null, {disableDisability: 1});
					V.shelterSlave.origin = "$He was discarded after suffering a terrible reaction to growth hormone treatment.";
					V.shelterSlave.career = "a slave";
					V.shelterSlave.devotion = jsRandom(-75, -60);
					V.shelterSlave.trust = jsRandom(-100, -75);
					setHealth(V.shelterSlave, jsRandom(-60, -40), normalRandInt(10, 3), normalRandInt(20, 3));
					V.shelterSlave.chem = 1000;
					if ((V.shelterSlave.dick > 0) && (jsRandom(1, 2) === 1)) {
						V.shelterSlave.dick = jsRandom(5, 6);
					}
					if ((V.shelterSlave.balls > 0) && (jsRandom(1, 2) === 1)) {
						V.shelterSlave.balls = jsRandom(5, 10);
					}
					if (jsRandom(1, 3) === 1) {
						V.shelterSlave.boobs += 100 * jsRandom(10, 30);
					}
					if (jsRandom(1, 3) === 1) {
						V.shelterSlave.butt += jsRandom(3, 5);
					}
					V.shelterSlave.behavioralFlaw = either("anorexic", "gluttonous", "odd");
					V.shelterSlave.sexualFlaw = either("apathetic", "hates anal", "hates oral", "hates penetration");
					break;
				default:
					V.shelterSlave = GenerateNewSlave(null, {disableDisability: 1});
					throw Error(`"${origin}" not found`);
			}
		}
		const cost = 1000;
		const {his} = getPronouns(V.shelterSlave);

		App.UI.DOM.appendNewElement("p", el, `The placement fee is ${cashFormat(cost)}.`);

		if (V.cash >= cost) {
			App.UI.DOM.appendNewElement(
				"p",
				el,
				App.UI.DOM.link(
					`Buy ${his} slave contract`,
					() => {
						cashX(forceNeg(cost), "slaveTransfer", V.shelterSlave);
						V.shelterSlaveBought = 1;
						V.shelterSlave.origin = "You got $him at the Slave Shelter. " + V.shelterSlave.origin;
						jQuery("#slave-markets").empty().append(App.UI.newSlaveIntro(V.shelterSlave));
					}
				)
			);
		} else {
			App.UI.DOM.appendNewElement("p", el, `You lack the necessary funds to buy this slave.`, "note");
		}

		el.append(App.Desc.longSlave(V.shelterSlave, {market: "generic"}));
		App.UI.DOM.appendNewElement("p", el, pronounsForSlaveProp(V.shelterSlave, V.shelterSlave.origin));
	}
	return el;
};


code:
/**
 * Displays the UI for special/hero slave market, slaves that are from the /d/ databases.
 * @returns {Node}
 */
App.Markets["Special Slave"] = function() {
	const el = document.createElement("p");
	let p;
	let heroSlaves = App.Utils.buildHeroArray();
	let slave;
	let selectedID;

	// Element to show the entire catalog when selected, or a blurb for a random slave
	p = document.createElement("p");
	p.id = "complete-catalog";
	p.append(getRandomSlave());
	p.append(
		App.UI.DOM.link(
			`Pay to access complete catalog of slaves`,
			() => {
				cashX(-1000, "personalBusiness");
				jQuery("#complete-catalog").empty().append(catalog());
			}
		)
	);
	App.UI.DOM.appendNewElement("span", p, ` Costs ${cashFormat(1000)}`, "note");
	el.append(p);

	// The slave chosen, randomly or otherwise
	p = document.createElement("p");
	p.id = "show-slave";
	if (heroSlaves.length !== 0) {
		p.append(showSlave());
	}
	el.append(p);

	// Credits at the end
	App.UI.DOM.appendNewElement("span", el, `Best regards to /d/, whose fine denizens came up with most of the slaves in the "previously owned" database.`, "note");
	return el;

	function getRandomSlave() {
		const el = new DocumentFragment();
		if (heroSlaves.length === 0) {
			App.UI.DOM.appendNewElement("p", el, `Unfortunately, the catalog is empty.`);
		} else {
			slave = heroSlaves.random();
			selectedID = slave.ID;
			slave = App.Utils.getHeroSlave(slave);
			const {his} = getPronouns(slave);
			App.UI.DOM.appendNewElement("p", el, `You review a piece of merchandise via video call, making a few lewd demands to gauge ${his} obedience. The background of the video feed is luxurious and plush; somewhere offscreen someone is moaning rapturously.`, `scene-intro`);
		}
		return el;
	}

	function catalog() {
		const el = new DocumentFragment();
		const linkArray = [];
		App.UI.DOM.appendNewElement("p", el, `This is the complete catalog of slaves that you can acquire from other slaveowners. Most of these slaves are not for sale, so persuading their owners to part with them will be extremely expensive.`, "scene-intro");

		for (const hero of heroSlaves) {
			if (hero.ID === selectedID) {
				linkArray.push(
					App.UI.DOM.disabledLink(hero.slaveName, ["You are currently examining this slave"])
				);
			} else {
				linkArray.push(
					App.UI.DOM.link(
						hero.slaveName,
						() => {
							slave = App.Utils.getHeroSlave(hero);
							selectedID = hero.ID;
							refresh();
						})
				);
			}
		}
		App.UI.DOM.appendNewElement("p", el, App.UI.DOM.generateLinksStrip(linkArray));

		if (V.cheatMode) {
			let oneSlaveCost = (s) => { let cost = slaveCost(s); return cost + (10 * Math.trunc((cost / 10) * 2)); };
			let allHeroSlaves = App.Utils.buildHeroArray().map(
				(hs) => {
					const slave = App.Utils.getHeroSlave(hs);
					return {slave: slave, cost: oneSlaveCost(slave), ID: hs.ID};
				}
			);
			let totalCost = allHeroSlaves.reduce((acc, s) => acc += s.cost, 0);

			if (V.cash > totalCost) {
				App.UI.DOM.appendNewElement(
					"div",
					el,
					App.UI.DOM.link(
						`Buy all of them for ${cashFormat(totalCost)}`,
						() => {
							for (const hero of allHeroSlaves) {
								cashX(forceNeg(hero.cost), "slaveTransfer", hero.slave);
								newSlave(hero.slave);
								V.heroSlavesPurchased.push(hero.ID);
							}
							refresh();
						}
					)
				);
			} else {
				App.UI.DOM.appendNewElement(
					"div",
					el,
					App.UI.DOM.disabledLink(
						`Buy all of them for ${cashFormat(totalCost)}`,
						[`Cannot afford`]
					)
				);
			}
		}

		return el;

		function refresh() {
			heroSlaves = App.Utils.buildHeroArray();
			if (heroSlaves.length === 0) {
				jQuery("#complete-catalog").empty().append(`There are no longer any special slaves available.`);
				jQuery("#show-slave").empty();
			} else {
				jQuery("#complete-catalog").empty().append(catalog());
				jQuery("#show-slave").empty().append(showSlave());
			}
		}
	}

	function showSlave() {
		const el = new DocumentFragment();
		let p;
		const {his} = getPronouns(slave);
		const slaveCost = heroSlaveCost(slave, 20000);
		App.UI.DOM.appendNewElement("p", el, App.Desc.longSlave(slave, {market: "generic"}));
		App.UI.DOM.appendNewElement("p", el, `The offered price is ${cashFormat(slaveCost)}.`);
		App.UI.DOM.appendNewElement("p", el,
			App.UI.DOM.link(
				`Buy ${his} slave contract`,
				() => {
					slave.weekAcquired = V.week;
					V.nextButton = "Continue";
					V.nextLink = "Main";
					V.heroSlavesPurchased.push(selectedID);
					cashX(forceNeg(slaveCost), "slaveTransfer", slave);
					jQuery("#show-slave").empty().append(App.UI.newSlaveIntro(slave));
				},
			)
		);
		return el;
	}
};

license: mit


code:
/**
 * Displays the Market, where you can select individual slave markets to shop at.
 * @returns {Node}
 */

App.UI.buySlaves = function() {
	const el = new DocumentFragment();
	const minCost = minimumSlaveCost();
	let linkArray;

	App.UI.DOM.appendNewElement("h1", el, "The Market");
	App.UI.DOM.appendNewElement("p", el, `There are many different organizations to purchase slaves from, but many of them are selective about their customers and will only sell to you if you are reputable. `, `note`);

	if (V.slaveCostFactor > 1.1) {
		App.UI.DOM.appendNewElement("div", el, `There is a bull market for slaves; the price of slaves is very high. `, "yellow");
	} else if (V.slaveCostFactor > 1) {
		App.UI.DOM.appendNewElement("div", el, `The slave market is bullish; the price of slaves is high. `, "yellow");
	} else if (V.slaveCostFactor < 0.9) {
		App.UI.DOM.appendNewElement("div", el, `There is a bear market for slaves; the price of slaves is very low. `, "yellow");
	} else if (V.slaveCostFactor < 1) {
		App.UI.DOM.appendNewElement("div", el, `The slave market is bearish; the price of slaves is low. `, "yellow");
	} else {
		App.UI.DOM.appendNewElement("div", el, `The slave market is stable; the price of slaves is average.`);
	}

	App.UI.DOM.appendNewElement("h2", el, "Sex Slave Purchase Options");

	// Cheap
	el.append(storeBlock(App.Data.Markets.low));

	// Neighbors
	App.UI.DOM.appendNewElement("h3", el, "Neighboring Arcologies");
	App.UI.DOM.appendNewElement("div", el, "The arcology's prosperity and culture will affect slaves who have lived there.", "note");
	App.UI.DOM.appendNewElement("div", el, neighborsBlock());

	// Schools
	if (V.rep > 6000) {
		App.UI.DOM.appendNewElement("h3", el, "Slave Schools");
		App.UI.DOM.appendNewElement("div", el, "High prices; will be young and healthy.", "note");
		el.append(storeBlock(App.Data.Markets.schools));
	} else {
		App.UI.DOM.appendNewElement("div", el, "You are not reputable enough to acquire fresh school slaves.");
	}

	// High end
	App.UI.DOM.appendNewElement("h3", el, "Special");
	el.append(storeBlock(App.Data.Markets.high));
	return el;

	function storeBlock(storeCategory) {
		const el = new DocumentFragment();

		for (const store of storeCategory) {
			el.append(storeFront(store));
		}

		return el;
	}

	function neighborsBlock() {
		const el = new DocumentFragment();
		for (let i = 0; i < V.arcologies.length; i++) {
			if (V.arcologies[i].direction !== 0) {
				linkArray = [];
				const linkUnit = document.createElement("span");
				linkUnit.append(
					App.UI.DOM.link(
						"Slaves from",
						() => {
							V.market = new App.Markets.Global("neighbor");
							V.market.numArcology = i;
							updateNav();
						},
						[],
						"Market"
					)
				);
				App.UI.DOM.appendNewElement("span", linkUnit, ` ${V.arcologies[i].name}`, "bold");
				linkArray.push(linkUnit);
				if (V.cash > (minCost * 5)) {
					linkArray.push(
						App.UI.DOM.link(
							"x5",
							() => {
								V.market = new App.Markets.Global("neighbor");
								V.market.introType = "bulk";
								V.market.numSlaves = 5;
								V.market.numArcology = i;
								V.returnTo = "Buy Slaves";
							},
							[],
							"Bulk Slave Intro"
						)
					);
				}
				if (V.cash > (minCost * 10)) {
					linkArray.push(
						App.UI.DOM.link(
							"x10",
							() => {
								V.market = new App.Markets.Global("neighbor");
								V.market.introType = "bulk";
								V.market.numSlaves = 10;
								V.market.numArcology = i;
								V.returnTo = "Buy Slaves";
							},
							[],
							"Bulk Slave Intro"
						)
					);
				}
				App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(linkArray));
			}
		}
		return el;
	}

	/** *
	 * @param {market} store
	 * @returns {Node}
	 */
	function storeFront(store) {
		const el = document.createElement("div");

		// Check requirements
		const requirements = store.requirements;
		if (requirements === false) {
			return el;
		} else if (typeof requirements === "string") {
			el.append(requirements);
			return el;
		}
		if (!App.Markets[store.marketType] && V.debugMode) {
			App.UI.DOM.appendNewElement("span", el, "marketType not set up", "red");
		}

		const linkArray = [];
		linkArray.push(
			App.UI.DOM.link(
				store.title,
				() => {
					V.market = new App.Markets.Global(store.marketType);
					updateNav(store);
				},
				[],
				"Market"
			)
		);
		if (store.bulkAvailable !== false) {
			if (V.cash > (minCost * 5)) {
				linkArray.push(
					App.UI.DOM.link(
						`x5`,
						() => {
							V.market = new App.Markets.Global(store.marketType);
							V.market.introType = "bulk";
							V.market.numSlaves = 5;
							V.returnTo = "Buy Slaves";
						},
						[],
						`Bulk Slave Intro`
					)
				);
			}
			if (V.cash > (minCost * 10)) {
				linkArray.push(
					App.UI.DOM.link(
						`x10`,
						() => {
							V.market = new App.Markets.Global(store.marketType);
							V.market.introType = "bulk";
							V.market.numSlaves = 10;
							V.returnTo = "Buy Slaves";
						},
						[],
						`Bulk Slave Intro`
					)
				);
			}
		}

		el.append(App.UI.DOM.generateLinksStrip(linkArray));

		if (store.sale) {
			el.append(` ${store.sale}`);
		}

		if (store.note) {
			App.UI.DOM.appendNewElement("span", el, ` ${store.note}`, "note");
		}
		if (App.Data.misc.schools.has(store.marketType)) {
			if (V[store.marketType].schoolSale === 1) {
				App.UI.DOM.appendNewElement("span", el, `Offering your first purchase at half price this week. `, "yellow");
			}
		}
		return el;
	}

	/** @param {market} [store] */
	function updateNav(store) {
		// Sidebar
		V.nextButton = "Back";
		V.nextLink = "Buy Slaves";
		V.returnTo = "Buy Slaves";
		V.encyclopedia = store && store.encyclopedia ? store.encyclopedia : "Obtaining Slaves";
		// Multi-Purchase Support
		if (V.market.newSlaves.length > 0) {
			V.nextButton = "Continue";
			V.nextLink = "Bulk Slave Intro";
			V.returnTo = "Main";
		}
	}
};


code:
App.UI.market = function(menialWorkersOnly = null) {
	const span = document.createElement("span");
	span.id = "slave-markets";
	if (!menialWorkersOnly) {
		span.append(App.UI.buySlaves());
		span.append(App.UI.sellSlaves());
	}

	const menialSpan = document.createElement("span");
	menialSpan.id = "menial-span";
	menialSpan.append(App.UI.tradeMenials(menialWorkersOnly));
	span.append(menialSpan);

	if (!menialWorkersOnly) {
		const menialTransactionResult = document.createElement("div");
		menialTransactionResult.id = "menial-transaction-result";
		span.append(menialTransactionResult);
	}

	return span;
};


code:
/**
 * @typedef {Array<market>} marketCategory
 */

/**
 * @typedef {object} market
 * @property {string} title
 * @property {FC.SlaveMarketName | FC.SpecialMarketName} [marketType]
 * @property {string} [note]
 * @property {string} [encyclopedia]
 * @property {string} [sale]
 * @property {boolean} [bulkAvailable=true]
 * @property {boolean|string} [requirements] if requirements failed, may return string explaining why
 */

/** @type {Object.<string, marketCategory>} */
App.Data.Markets = {
	low: [
		{
			title: "Corporate Market",
			marketType: "corporate",
			get note() { return V.corp.Market === 1 ? `Slaves from your corporation purchased at a discounted rate.` : `Slaves from your corporation`; },
			encyclopedia: "The Corporation",
			sale: "",
			bulkAvailable: true,
			get requirements() { return (V.corp.Incorporated === 1); }
		},
		{
			title: "The Flesh Heap",
			marketType: "heap",
			bulkAvailable: false,
			note: `Broken and discarded slaves. Near useless, but cheap. No longer does bulk orders after complaints.`,
			encyclopedia: "The Flesh Heap"
		},
		{
			title: "Order a custom husk slave from the Flesh Heap",
			marketType: `Husk Slave`,
			note: `Will need to be used on arrival.`,
			encyclopedia: "The Flesh Heap",
			bulkAvailable: false,
			get requirements() { return (V.bodyswapAnnounced === 1); }
		},
		{
			title: "Kidnappers' Market",
			marketType: "kidnappers",
			note: `Slaves will tend to be low quality and resistant.`,
			encyclopedia: "Kidnapped Slaves",
			get requirements() { return (V.rep > 500) ? true : `You are not reputable enough to buy kidnapped slaves.`; }
		},
		{
			title: "Runaway Hunters' Market",
			marketType: "hunters",
			note: "Slaves will tend to be skilled but rebellious.",
			encyclopedia: "Kidnapped Slaves",
			get requirements() { return (V.rep > 1000) ? true : `You are not reputable enough to buy recaptured slaves.`; }
		},
		{
			title: "Indentures Market",
			marketType: "indentures",
			note: "Temporary enslavement and restrictions on treatment.",
			encyclopedia: "Indentured Servants",
			get requirements() { return (V.rep > 1500) ? true : `You are not reputable enough to buy indentured servants.`; }
		},
		{
			title: "Raiders' Market",
			marketType: "raiders",
			note: "Slaves will always be sold immediately upon reaching majority.",
			encyclopedia: "Kidnapped Slaves",
			get requirements() { return (V.rep > 2000) ? true : `You are not reputable enough to buy teenaged slaves.`; }
		},
		{
			title: "Raiders' Black Market",
			marketType: "underage raiders",
			note: "Very young slaves.",
			encyclopedia: "Kidnapped Slaves",
			get requirements() {
				if (V.rep <= 3000) {
					return `You are not reputable enough to buy underaged slaves.`;
				} else if (V.pedo_mode === 1 || (V.minimumSlaveAge <= 13 && V.minimumSlaveAge <= V.fertilityAge)) {
					return true;
				} else {
					return false;
				}
			}
		},
		{
			title: "Trainers' Market",
			marketType: "trainers",
			note: "Slaves will tend to be good quality and obedient.",
			encyclopedia: "Stables",
			sale: "",
			bulkAvailable: true,
			get requirements() { return (V.rep > 4000) ? true : `You are not reputable enough to buy trained slaves.`; }
		},
		{
			title: "Wetware CPUs",
			marketType: "wetware",
			note: "Ruined bodies but keen minds. Requires some TLC, but offers outstanding training at a discount.",
			encyclopedia: "Wetware CPUs",
			sale: "",
			bulkAvailable: true,
			get requirements() { return (V.arcologies[0].FSPaternalist === "unset") ? true : `The paternalistic nature of your society blocks the abusive Wetware CPU manufactures from operating within your arcology.`; }
		},
		{
			title: "Prisoner Sale",
			get marketType() { return V.prisonCircuit[V.prisonCircuitIndex]; },
			get note() {
				switch (V.prisonCircuit[V.prisonCircuitIndex]) {
					case "low tier criminals":
						return `Slaves will tend to be low to mid quality with few redeeming factors.`;
					case "gangs and smugglers":
						return `Slaves will tend to be low quality and difficult but may prove useful once broken.`;
					case "white collar":
						return `Slaves will tend to be medium to high quality with a variety of useful backgrounds.`;
					case "military prison":
						return `Slaves will tend to be high quality but defiant.`;
					case "juvenile detention":
						return `Slaves will tend to be young and healthy but defiant.`;
					default:
						return ``;
				}
			},
			get sale() {
				let tw = "This week ";
				switch (V.prisonCircuit[V.prisonCircuitIndex]) {
					case "low tier criminals":
						return tw + `a minor prison is selling inmates.`;
					case "gangs and smugglers":
						return tw + `a major prison is selling hardened criminals.`;
					case "white collar":
						return tw + `a white collar prison is selling inmates.`;
					case "military prison":
						return tw + `a military prison is selling inmates.`;
					case "juvenile detention":
						return tw + `a juvenile detention facility is selling inmates.`;
					default:
						return ``;
				}
			},
			get requirements() { return (V.rep > 5000) ? true : `You are not reputable enough to buy prison slaves.`; }
		},
	],
	schools: [
		{
			title: "The Slavegirl School",
			marketType: "TSS",
			note: "Straightforward slaves with good training.",
			encyclopedia: "Slave Schools",
			get requirements() { return (V.seeDicks !== 100); }
		},
		{
			title: "The Utopian Orphanage",
			marketType: "TUO",
			note: "Intelligent, unspoiled slaves just past their majority.",
			encyclopedia: "Slave Schools",
			get requirements() { return (V.seeDicks !== 100); }
		},
		{
			title: "Growth Research Institute",
			marketType: "GRI",
			note: "Poorly trained slaves with huge assets.",
			encyclopedia: "Slave Schools",
			get requirements() { return (V.seeDicks !== 100); }
		},
		{
			title: "St. Claver Preparatory",
			marketType: "SCP",
			note: "Slaves with basic training and solid implants.",
			encyclopedia: "Slave Schools",
			get requirements() { return (V.seeDicks !== 100); }
		},
		{
			title: "The Cattle Ranch",
			marketType: "TCR",
			note: "Mentally conditioned free-range cowgirls.",
			encyclopedia: "Slave Schools",
			get requirements() { return (V.seeDicks !== 100); }
		},
		{
			title: "The Hippolyta Academy",
			marketType: "HA",
			note: "High quality slaves of powerful physique and refined skills.",
			encyclopedia: "Slave Schools",
			get requirements() { return (V.seeDicks !== 100); }
		},
		{
			title: "L'École des Enculées",
			marketType: "LDE",
			note: "Slaves optimized and trained for anal.",
			encyclopedia: "Slave Schools",
			get requirements() { return (V.seeDicks !== 0); }
		},
		{
			title: "The Gymnasium-Academy",
			marketType: "TGA",
			note: "Well trained slaves with dicks.",
			encyclopedia: "Slave Schools",
			get requirements() { return (V.seeDicks !== 0); }
		},
		{
			title: "The Futanari Sisters",
			marketType: "TFS",
			note: "Highly skilled, highly feminine hermaphrodites.",
			encyclopedia: "Slave Schools",
			get requirements() { return (V.seeDicks !== 0); }
		},
		{
			title: "Nueva Universidad de Libertad",
			marketType: "NUL",
			note: "Androgynous slaves lacking genitalia.",
			encyclopedia: "Slave Schools",
		},
	],
	high: [
		{
			title: "Take in a shelter slave",
			marketType: "Slave Shelter",
			note: "Presents charity cases for a nominal fee.",
			encyclopedia: "Discarded Slaves",
			bulkAvailable: false,
			get requirements() {
				if (V.arcologies[0].FSDegradationist !== "unset") {
					return `The Slave Shelter does not place slaves into Degradationist arcologies.`;
				} else if (V.shelterAbuse > 10) {
					return `The Slave Shelter has banned you due to dark rumors about you.`;
				} else if (V.shelterAbuse > 5) {
					return `The Slave Shelter has banned you due to rumors that you resell its slaves.`;
				} else if (V.shelterSlaveBought === 1) {
					return `The Slave Shelter will not offer another slave for placement until next week.`;
				} else if (V.rep > 7000) {
					return true;
				} else {
					return `You are not reputable enough to take in Shelter slaves`;
				}
			}
		},
		{
			title: "Consult the household liquidator",
			marketType: "Household Liquidator",
			note: "Offers slaves close to one another at a very high price.",
			encyclopedia: "Household Liquidations",
			bulkAvailable: false,
			get requirements() { return (V.rep > 8000) ? true : `You are not reputable enough to consult the household liquidator.`; }
		},
		{
			title: "Place a special order",
			marketType: "Custom Slave",
			note: "Customizable but very expensive.",
			encyclopedia: "Kidnapped Slaves",
			bulkAvailable: false,
			get requirements() { return (V.rep > 10000) ? true : `You are not reputable enough to order custom slaves.`; }
		},
		{
			title: "Place a fulfillment order",
			marketType: "JobFulfillmentCenterOrder",
			note: "Fills leaderships roles for a price.",
			encyclopedia: "The Job Fulfillment Center",
			bulkAvailable: false,
			get requirements() { return (V.rep > 10000); }
		},
		{
			title: "Attend an auction of a prestigious slave",
			marketType: "Prestigious Slave",
			note: "Variable and expensive.",
			encyclopedia: "Slave Schools",
			bulkAvailable: false,
			get requirements() { return (V.rep > 12000) ? true : `You are not reputable enough to bid on prestigious slaves.`; }
		},
		{
			title: "Attend an auction of other Elite's stock",
			marketType: "Elite Slave",
			note: "Limited and very expensive.",
			encyclopedia: "Slave Schools",
			bulkAvailable: false,
			get requirements() { return (V.propOutcome === 1); }
		},
		{
			title: "Acquire other slaveowners' stock",
			marketType: "Special Slave",
			note: "Variable and expensive.",
			encyclopedia: "Pre-Owned Slaves",
			bulkAvailable: false,
			get requirements() { return (V.rep > 14000) ? true : `You are not reputable enough to acquire other slaveowners' stock.`; }
		},
	]
};


code:
App.UI.sellSlaves = function() {
	const el = new DocumentFragment();

	App.UI.DOM.appendNewElement("h2", el, "Selling slaves");

	const div = document.createElement("div");
	div.append(`While you are at the market, you may want to `);
	div.append(
		App.UI.DOM.passageLink(
			`look at your own underperforming slaves`,
			`Underperforming Slaves`
		)
	);
	el.append(div);

	return el;
};


code:
App.UI.tradeMenials = function(menialWorkersOnly) {
	const el = new DocumentFragment();
	const menialPrice = menialSlaveCost();
	const popCap = menialPopCap();
	let bulkMax;
	let linkArray;

	if (!menialWorkersOnly) {
		App.UI.DOM.appendNewElement("h2", el, "Menial Slaves");

		el.append(slaveDemand());
		el.append(slaveSupply());

		App.UI.DOM.appendNewElement("div", el, popCap.text);
	}
	bulkMax = popCap.value - V.menials - V.fuckdolls - V.menialBioreactors;
	if (!menialWorkersOnly) {
		App.UI.DOM.appendNewElement("div", el, `The parts of your arcology you own can house a total of ${num(popCap.value)} menial slaves.`);
	}

	el.append(menialTrading());

	if (!menialWorkersOnly) {
		if (V.arcologies[0].FSPaternalist === "unset") {
			el.append(fuckDollConversion());
			el.append(fuckDollTrading());
		}

		el.append(bioreactorTrading());
	}

	return el;

	function slaveDemand() {
		const el = new DocumentFragment();
		let div = document.createElement("div");
		let span;
		div.append(`Slave demand is `);
		if (V.menialDemandFactor <= -35000) {
			App.UI.DOM.appendNewElement("span", div, `extremely low`, ["red", "bold"]);
		} else if (V.menialDemandFactor <= -20000) {
			App.UI.DOM.appendNewElement("span", div, `very low`, ["red", "bold"]);
		} else if (V.menialDemandFactor <= 0) {
			App.UI.DOM.appendNewElement("span", div, `weak`, ["yellow", "bold"]);
		} else if (V.menialDemandFactor >= 35000) {
			App.UI.DOM.appendNewElement("span", div, `extremely high`, ["green", "bold"]);
		} else if (V.menialDemandFactor >= 20000) {
			App.UI.DOM.appendNewElement("span", div, `very high`, ["green", "bold"]);
		} else {
			App.UI.DOM.appendNewElement("span", div, `strong`, ["yellow", "bold"]);
		}
		if (V.deltaDemand > 0) {
			div.append(` and `);
			App.UI.DOM.appendNewElement("span", div, `improving`, ["green", "bold"]);
		} else if (V.deltaDemand < 0) {
			div.append(` and `);
			App.UI.DOM.appendNewElement("span", div, `decreasing`, ["red", "bold"]);
		}
		if (V.deltaDemand !== 0) {
			if (V.demandTimer - V.elapsedDemandTimer < 3) {
				App.UI.DOM.appendNewElement("span", div, ` but it might change soon.`);
			}
		}
		if (V.cheatMode && V.cheatModeM) {
			const menDemand = function() {
				const el = new DocumentFragment;
				App.UI.DOM.appendNewElement("span", el, `Slave Demand`, `yellowgreen`);
				el.append(`| ${V.menialDemandFactor}`);
				return el;
			};

			span = document.createElement("span");
			span.id = "menial-demand-factor";
			span.append(menDemand());

			App.UI.DOM.makeTextBox(
				V.menialDemandFactor,
				v => {
					V.menialDemandFactor = Math.clamp(Math.trunc(Number(v)), -50000, 50000) || V.menialDemandFactor;
					V.cheater = 1;
					jQuery("#menial-demand-factor").empty().append(menDemand());
				}
			);
			div.append(span);
		}
		el.append(div);
		return el;
	}

	function slaveSupply() {
		const el = new DocumentFragment();
		let span;
		let div = document.createElement("div");
		div.append(`Slave supply is `);

		if (V.menialSupplyFactor <= -35000) {
			App.UI.DOM.appendNewElement("span", div, "extremely low", ["green", "bold"]);
		} else if (V.menialSupplyFactor <= -20000) {
			App.UI.DOM.appendNewElement("span", div, "very low", ["green", "bold"]);
		} else if (V.menialSupplyFactor <= 0) {
			App.UI.DOM.appendNewElement("span", div, "weak", ["yellow", "bold"]);
		} else if (V.menialSupplyFactor >= 35000) {
			App.UI.DOM.appendNewElement("span", div, "extremely high", ["red", "bold"]);
		} else if (V.menialSupplyFactor >= 20000) {
			App.UI.DOM.appendNewElement("span", div, "very high", ["red", "bold"]);
		} else {
			App.UI.DOM.appendNewElement("span", div, "strong", ["yellow", "bold"]);
		}
		if (V.deltaSupply > 0) {
			div.append(` and `);
			App.UI.DOM.appendNewElement("span", div, "improving", ["red", "bold"]);
		} else if (V.deltaSupply < 0) {
			div.append(` and `);
			App.UI.DOM.appendNewElement("span", div, "decreasing", ["green", "bold"]);
		}
		if (V.deltaSupply !== 0) {
			if (V.supplyTimer - V.elapsedDemandTimer < 3) {
				App.UI.DOM.appendNewElement("span", div, ` but it might change soon.`);
			}
		}

		if (V.cheatMode && V.cheatModeM) {
			const menSupply = function() {
				const el = new DocumentFragment;
				App.UI.DOM.appendNewElement("span", el, `Slave Supply`, `yellowgreen`);
				el.append(`| ${V.menialSupplyFactor}`);
				return el;
			};

			span = document.createElement("span");
			span.id = "menial-supply-factor";
			span.append(menSupply());

			App.UI.DOM.makeTextBox(
				V.menialSupplyFactor,
				v => {
					V.menialSupplyFactor = Math.clamp(Math.trunc(Number(v)), -50000, 50000) || V.menialSupplyFactor;
					V.cheater = 1;
					jQuery("#menial-supply-factor").empty().append(menSupply());
				}
			);
			div.append(span);
		}
		el.append(div);

		return el;
	}

	function menialTrading() {
		const el = document.createElement("div");
		let div = document.createElement("div");
		if (!menialWorkersOnly) {
			if (V.menials > 1) {
				el.append(`You own ${num(Math.trunc(V.menials))} menial slaves.`);
			} else if (V.menials > 0) {
				el.append(`You own one menial slave.`);
			} else {
				el.append(`You do not own any menial slaves.`);
			}
			el.append(` The market price of menials is ${cashFormat(menialPrice)}.`);
		}

		if (bulkMax > 0 && V.cash > menialPrice) {
			div = document.createElement("div");
			linkArray = [];
			const buySomeMenials = function(number = 1) {
				const value = forceNeg(menialSlaveCost(number) * number);
				V.menials += number;
				V.menialSupplyFactor -= number;
				cashX(value, "menialTransfer");
				jQuery("#menial-transaction-result").empty().append(App.UI.DOM.cashFormat(value));
				if (menialWorkersOnly) {
					Engine.play(passage());
				}
			};

			linkArray.push(
				App.UI.DOM.link(
					"Buy",
					() => {
						buySomeMenials(1);
						jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
					}
				)
			);
			if (V.cash > (menialSlaveCost(5)) * 5) {
				makeLink(5, buySomeMenials);
			}
			if (V.cash > (menialSlaveCost(10)) * 10) {
				makeLink(10, buySomeMenials);
			}
			if (V.cash > (menialSlaveCost(100)) * 100) {
				makeLink(100, buySomeMenials);
			}
			if (V.cash > (menialPrice + 1) * 2) {
				let menialBulkPremium = Math.trunc(1 + Math.clamp(V.cash / menialPrice, 0, bulkMax) / 400);
				linkArray.push(
					App.UI.DOM.link(
						"max",
						() => {
							buySomeMenials(
								Math.trunc(Math.clamp(V.cash / (menialPrice + menialBulkPremium), 0, bulkMax))
							);
							jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
						},
					)
				);
			}
			div.append(App.UI.DOM.generateLinksStrip(linkArray));
			App.UI.DOM.appendNewElement("span", div, " Bulk transactions may require offering a premium.", "note");
			el.append(div);
		}

		if (!menialWorkersOnly && V.menials > 0) {
			linkArray = [];
			const sellSomeMenials = function(number = 1) {
				const value = menialSlaveCost(number) * number;
				V.menials -= number;
				V.menialDemandFactor -= number;
				cashX(value, "menialTransfer");
				jQuery("#menial-transaction-result").empty().append(App.UI.DOM.cashFormat(value));
			};
			linkArray.push(
				App.UI.DOM.link(
					"Sell",
					() => {
						sellSomeMenials(1);
						jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
					}
				)
			);

			if (V.menials >= 5) {
				makeLink(5, sellSomeMenials);
			}
			if (V.menials >= 10) {
				makeLink(10, sellSomeMenials);
			}
			if (V.menials >= 100) {
				makeLink(100, sellSomeMenials);
			}
			linkArray.push(
				App.UI.DOM.link(
					"all",
					() => {
						sellSomeMenials(V.menials);
						V.menials = 0; // Be quite sure.
						jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
					},
				)
			);
			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(linkArray));
		}
		return el;
	}

	function fuckDollConversion() {
		const el = document.createElement("div");
		let div = document.createElement("div");
		let convertCost;
		if (V.menials > 0) {
			if (V.arcadeUpgradeFuckdolls > 0) {
				convertCost = 100;
				const convertSomeMenialsToFuckdolls = function(number) {
					const value = forceNeg(number * convertCost);
					V.menials -= number;
					V.fuckdolls += number;
					cashX(value, "menialTransfer");
					jQuery("#menial-transaction-result").empty().append(App.UI.DOM.cashFormat(value));
				};
				linkArray = [];
				linkArray.push(
					App.UI.DOM.link(
						"Convert to Fuckdoll",
						() => {
							convertSomeMenialsToFuckdolls(1);
							jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
						}
					)
				);

				if (V.menials >= 10) {
					makeLink(10, convertSomeMenialsToFuckdolls);
					if (V.menials >= 100) {
						makeLink(100, convertSomeMenialsToFuckdolls);
					}
				}
				linkArray.push(
					App.UI.DOM.link(
						"all",
						() => {
							convertSomeMenialsToFuckdolls(V.menials);
							V.menials = 0; // Be really sure
							jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
						},
					)
				);

				div.append(App.UI.DOM.generateLinksStrip(linkArray));
				App.UI.DOM.appendNewElement("span", div, `Conversion costs ${cashFormat(convertCost)} each`, "note");
				el.append(div);
			}
			if (V.dairyFeedersUpgrade > 0) {
				convertCost = 500;
				const convertSomeMenialsToBioreactors = function(number) {
					const value = forceNeg(number * convertCost);
					V.menials -= number;
					V.menialBioreactors += number;
					cashX(value, "menialTransfer");
					jQuery("#menial-transaction-result").empty().append(App.UI.DOM.cashFormat(value));
				};
				linkArray = [];
				linkArray.push(
					App.UI.DOM.link(
						"Convert to Bioreactor",
						() => {
							convertSomeMenialsToBioreactors(1);
							jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
						}
					)
				);

				if (V.menials >= 10) {
					makeLink(10, convertSomeMenialsToBioreactors);
					if (V.menials >= 100) {
						makeLink(100, convertSomeMenialsToBioreactors);
					}
				}
				linkArray.push(
					App.UI.DOM.link(
						"all",
						() => {
							convertSomeMenialsToBioreactors(V.menials);
							V.menials = 0; // Be really sure
							jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
						},
					)
				);
				div = document.createElement("div");
				div.append(App.UI.DOM.generateLinksStrip(linkArray));
				App.UI.DOM.appendNewElement("span", div, `Conversion costs ${cashFormat(convertCost)} each`, "note");
				el.append(div);
			}
		}
		return el;
	}

	function fuckDollTrading() {
		const el = document.createElement("div");
		let div = document.createElement("div");
		if (V.fuckdolls > 1) {
			el.append(`You own ${num(Math.trunc(V.fuckdolls))} standard Fuckdolls. `);
		} else if (V.fuckdolls > 0) {
			el.append(`You own one standard Fuckdoll. `);
		} else if (V.arcologies[0].FSPaternalist === "unset") {
			el.append(`You do not own any standard Fuckdolls. `);
		}
		el.append(div);
		if ((V.fuckdolls > 0) || (V.arcologies[0].FSPaternalist === "unset")) {
			el.append(`The market price of standard Fuckdolls is ${cashFormat(menialPrice)}.`);
			if (bulkMax > 0) {
				if (V.arcologies[0].FSPaternalist === "unset" && V.cash > menialPrice) {
					div = document.createElement("div");
					linkArray = [];
					const buySomeFuckdolls = function(number = 1) {
						const value = forceNeg(menialSlaveCost(number) * number);
						V.fuckdolls += number;
						V.menialSupplyFactor -= number;
						cashX(value, "fuckdollsTransfer");
						jQuery("#menial-transaction-result").empty().append(App.UI.DOM.cashFormat(value));
					};

					linkArray.push(
						App.UI.DOM.link(
							"Buy",
							() => {
								buySomeFuckdolls(1);
								jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
							}
						)
					);
					if (V.cash > (menialSlaveCost(10)) * 10) {
						makeLink(10, buySomeFuckdolls);
					}
					if (V.cash > (menialSlaveCost(100)) * 100) {
						makeLink(100, buySomeFuckdolls);
					}
					if (V.cash > (menialPrice + 1) * 2) {
						let menialBulkPremium = Math.trunc(1 + Math.clamp(V.cash / menialPrice, 0, bulkMax) / 400);
						linkArray.push(
							App.UI.DOM.link(
								"max",
								() => {
									buySomeFuckdolls(
										Math.trunc(Math.clamp(V.cash / (menialPrice + menialBulkPremium), 0, bulkMax))
									);
									jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
								},
							)
						);
					}
					div.append(App.UI.DOM.generateLinksStrip(linkArray));
					App.UI.DOM.appendNewElement("span", div, " Bulk transactions may require offering a premium.", "note");
					el.append(div);
				}
			}
			if (V.fuckdolls >= 1) {
				linkArray = [];
				const sellSomeFuckdolls = function(number = 1) {
					const value = menialSlaveCost(number) * number;
					V.fuckdolls -= number;
					V.menialDemandFactor -= number;
					cashX(value, "fuckdollsTransfer");
					jQuery("#menial-transaction-result").empty().append(App.UI.DOM.cashFormat(value));
				};
				linkArray.push(
					App.UI.DOM.link(
						"Sell",
						() => {
							sellSomeFuckdolls(1);
							jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
						}
					)
				);

				if (V.fuckdolls >= 10) {
					makeLink(10, sellSomeFuckdolls);
					if (V.fuckdolls >= 100) {
						makeLink(100, sellSomeFuckdolls);
					}
				}
				linkArray.push(
					App.UI.DOM.link(
						"all",
						() => {
							sellSomeFuckdolls(V.fuckdolls);
							V.fuckdolls = 0; // Be quite sure.
							jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
						},
					)
				);
				App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(linkArray));
			}
		}

		return el;
	}

	function bioreactorTrading() {
		const el = document.createElement("div");
		let div = document.createElement("div");
		if (V.menialBioreactors > 1) {
			el.append(`You own ${num(Math.trunc(V.menialBioreactors))} standard bioreactors. `);
		} else if (V.menialBioreactors > 0) {
			el.append(`You own one standard bioreactor. `);
		} else if (V.arcologies[0].FSPaternalist === "unset") {
			el.append(`You do not own any standard bioreactors. `);
		}
		if ((V.menialBioreactors > 0) || (V.arcologies[0].FSPaternalist === "unset")) {
			el.append(`The market price of standard bioreactors is ${cashFormat(menialPrice - 100)}.`);
			if (bulkMax > 0) {
				if (V.arcologies[0].FSPaternalist === "unset" && V.cash > menialPrice) {
					div = document.createElement("div");
					linkArray = [];
					const buySomeBioreactors = function(number = 1) {
						const value = forceNeg(menialSlaveCost(number - 100) * number);
						V.menialBioreactors += number;
						V.menialSupplyFactor -= number;
						cashX(value, "bioreactorsTransfer");
						jQuery("#menial-transaction-result").empty().append(App.UI.DOM.cashFormat(value));
					};

					linkArray.push(
						App.UI.DOM.link(
							"Buy",
							() => {
								buySomeBioreactors(1);
								jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
							}
						)
					);
					if (V.cash > (menialSlaveCost(10)) * 10) {
						makeLink(10, buySomeBioreactors);
					}
					if (V.cash > (menialSlaveCost(100)) * 100) {
						makeLink(100, buySomeBioreactors);
					}
					if (V.cash > (menialPrice - 99) * 2) {
						let bioreactorBulkPremium = Math.trunc(1 + Math.clamp(V.cash / (menialPrice - 99), 0, bulkMax) / 400);
						linkArray.push(
							App.UI.DOM.link(
								"max",
								() => {
									buySomeBioreactors(
										Math.trunc(Math.clamp(V.cash / (menialPrice - 99 + bioreactorBulkPremium), 0, bulkMax))
									);
									jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
								},
							)
						);
					}
					div.append(App.UI.DOM.generateLinksStrip(linkArray));
					App.UI.DOM.appendNewElement("span", div, " Bulk transactions may require offering a premium.", "note");
					el.append(div);
				}
			}
			if (V.menialBioreactors >= 1) {
				linkArray = [];
				const sellSomeBioreactors = function(number = 1) {
					const value = menialSlaveCost(number - 100) * number;
					V.menialBioreactors -= number;
					V.menialDemandFactor -= number;
					jQuery("#menial-transaction-result").empty().append(App.UI.DOM.cashFormat(value));
					cashX(value, "bioreactorsTransfer");
				};
				linkArray.push(
					App.UI.DOM.link(
						"Sell",
						() => {
							sellSomeBioreactors(1);
							jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
						}
					)
				);

				if (V.menialBioreactors >= 10) {
					makeLink(10, sellSomeBioreactors);
					if (V.menialBioreactors >= 100) {
						makeLink(100, sellSomeBioreactors);
					}
				}
				linkArray.push(
					App.UI.DOM.link(
						"all",
						() => {
							sellSomeBioreactors(V.menialBioreactors);
							V.menialBioreactors = 0; // Be quite sure.
							jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
						},
					)
				);
				div = document.createElement("div");
				div.append(App.UI.DOM.generateLinksStrip(linkArray));
				el.append(div);
			}
		}
		return el;
	}

	/**
	 *
	 * @param {number} number
	 * @param {Function} runMe
	 */
	function makeLink(number, runMe) {
		linkArray.push(
			App.UI.DOM.link(
				`x${number}`,
				() => {
					runMe(number);
					jQuery("#menial-span").empty().append(App.UI.tradeMenials(menialWorkersOnly));
				}
			)
		);
	}
};


code:
/** get one arcology's opinion of another
 * @param {FC.ArcologyState} activeArcology index
 * @param {FC.ArcologyState} targetArcology index
 * @returns {number} opinion
 */
App.Neighbor.opinion = function(activeArcology, targetArcology) {
	const {shared, conflicting} = FutureSocieties.diplomaticFSes(activeArcology, targetArcology);

	let opinion = 0;

	if (activeArcology.direction === targetArcology.direction) {
		// ignore narcissism
		// this will be a problem in the future and as such, arcologies should realistically be given IDs
		return opinion;
	}

	for (const fs of shared) {
		opinion += activeArcology[fs];
		opinion += targetArcology[fs];
	}

	for (const [activeFS, targetFS] of conflicting) {
		opinion -= activeArcology[activeFS];
		opinion -= targetArcology[targetFS];
	}

	// unshared but uncontested multiculturalism gets a relationship bonus
	if (!shared.includes("FSNull") && !conflicting.some((pair) => pair.includes("FSNull"))) {
		if (activeArcology.FSNull !== "unset") {
			opinion += activeArcology.FSNull;
		} else if (targetArcology.FSNull !== "unset") {
			opinion += targetArcology.FSNull;
		}
	}

	return opinion;
};

/** set a new influence target for a given arcology
 * @param {number} arcID
 */
App.Neighbor.selectInfluenceTarget = function(arcID) {
	const notMulticulturalism = (f) => f !== "FSNull"; // multiculturalism can neither influence nor be influenced
	const influenceSources = FutureSocieties.influenceSources(arcID);
	const arcology = V.arcologies[arcID];
	if (influenceSources.length > 0) {
		let eligibleTargets = [];
		const obedient = (arcology.government === "your trustees" || arcology.government === "your agent");

		for (const target of V.arcologies) {
			if (arcology.direction !== target.direction) {
				if (!obedient || target.direction !== 0) {
					const {shared, conflicting} = FutureSocieties.diplomaticFSes(arcology, target);
					let count = 0;
					count += shared.filter(notMulticulturalism).length;
					count += conflicting.filter((pair) => pair.every(notMulticulturalism)).length;
					eligibleTargets.push(...Array(count).fill(target.direction));
				}
			}
		}

		if (eligibleTargets.length > 0) {
			arcology.influenceTarget = eligibleTargets.random();
		}
	}
};

App.Neighbor.PassiveFSInfluence = class {
	/** pick up social hints from an arcology's neighbors
	 * @param {number} arcID
	 */
	constructor(arcID) {
		this._arcID = arcID;
		/** @type {Map<number, {shared: FC.FutureSociety[], conflicting: FC.FutureSociety[][]}>} */
		this._relationships = new Map();

		const arcology = V.arcologies[this._arcID];
		for (let i = 0; i < V.arcologies.length; ++i) {
			if (i !== arcID) {
				this._relationships.set(i, FutureSocieties.diplomaticFSes(arcology, V.arcologies[i]));
			}
		}

		this._thresh = 5;
		if (arcology.direction === 0) {
			this._thresh -= V.policies.culturalOpenness * 5;
		}
		if (arcology.ownership >= 100) {
			this._thresh += 5;
		}
	}

	/** output the neighbors that have passively influenced a particular FS in this arcology
	 * @param {FC.FutureSociety} fs
	 */
	output(fs) {
		/** @type {number[]} */
		let shared = [];
		/** @type {Map<FC.FutureSociety, number[]>} */
		let conflicting = new Map();
		const arcology = V.arcologies[this._arcID];

		if (arcology.direction === 0 && fs === "FSNull") {
			return ``; // Multiculturalism in the player's arcology is not affected by passive influence
		}

		for (const [i, rel] of this._relationships) {
			if (rel.shared.some((s) => s === fs)) {
				if (V.arcologies[i][fs] > arcology[fs] + this._thresh) {
					shared.push(i);
				}
			} else {
				const conflict = rel.conflicting.find((p) => p[0] === fs);
				if (conflict) {
					const conflictFS = conflict[1];
					if (conflictFS === "FSNull") {
						continue; // no passive slowing from Multiculturalism
					}
					if (V.arcologies[i][conflictFS] > arcology[fs] + this._thresh) {
						const oldVal = conflicting.get(conflictFS);
						if (oldVal) {
							oldVal.push(i);
						} else {
							conflicting.set(conflictFS, [i]);
						}
					}
				}
			}
		}

		let t = [];
		// passive growth influence
		arcology[fs] += shared.length;
		if (V.showNeighborDetails && shared.length > 0) {
			t.push(FutureSocieties.displayName(fs));
			if (fs === "FSSubjugationist") {
				t.push(`of ${arcology.FSSubjugationistRace} people`);
			} else if (fs === "FSSupremacist") {
				t.push(`for ${arcology.FSSupremacistRace} people`);
			}
			t.push(`in ${arcology.name} is influenced by`);
			const sharedNames = shared.map((i) => V.arcologies[i].name + "'s");
			t.push(`${toSentence(sharedNames)} more advanced ${shared.length === 1 ? "society" : "societies"}.`);
		}

		// passive slowing influence
		arcology[fs] -= conflicting.size;
		if (V.showNeighborDetails && conflicting.size > 0) {
			/** @type {string} */
			let actualDisplayName = FutureSocieties.displayName(fs);
			if (fs === "FSSubjugationist") {
				actualDisplayName = `${arcology.FSSubjugationistRace} Subjugationism`;
			} else if (fs === "FSSupremacist") {
				actualDisplayName = `${arcology.FSSupremacistRace} Supremacy`;
			}
			t.push(`Development of ${actualDisplayName} in ${arcology.name} is slowed by contact with`);
			let conflictOutput = [];
			for (const [conflictFS, arcs] of conflicting) {
				const conflictNames = arcs.map((i) => V.arcologies[i].name + "'s");
				conflictOutput.push(`${toSentence(conflictNames)} more advanced ${FutureSocieties.displayAdj(conflictFS)} ${arcs.length === 1 ? "society" : "societies"}`);
			}
			t.push(toSentence(conflictOutput) + '.');
		}

		return t.join(" ");
	}
};

/** Get an arcology name from a given set, preferring unused ones.
 * @param {string[]} nameSet
 * @returns {string}
 */
App.Neighbor.getUnusedName = function(nameSet) {
	const filteredSet = nameSet.filter(n => !V.arcologies.some(a => a.name === n));
	return filteredSet.length > 0 ? filteredSet.random() : nameSet.random();
};


code:
App.UI.neighborDescription = function(i) {
	const el = new DocumentFragment();
	const averageProsperity = _.mean(V.arcologies.map((a) => a.prosperity));
	let r = [];
	r.push(App.UI.DOM.makeElement("span", V.arcologies[i].name, "bold"));
	if (V.arcologies[i].direction !== 0) {
		r.push(`is located to the ${V.arcologies[i].direction} of your arcology. It is governed by`);
		switch (V.arcologies[i].government) {
			case "elected officials":
				r.push(`elected officials, periodically paralyzing its development.`);
				break;
			case "a committee":
				r.push(`a committee, hindering its development.`);
				break;
			case "an oligarchy":
				r.push(`a small group of leading citizens, making its development very unpredictable.`);
				break;
			case "your trustees":
				r.push(`a small group of leading citizens who are serving as <span class="mediumseagreen">your trustees.</span>`);
				break;
			case "an individual":
				r.push(`<span class="cyan">an individual,</span> making its development vibrant but unpredictable.`);
				break;
			case "your agent":
				r.push(`<span class="deeppink">your agent,</span> who is directing the arcology in your stead.`);
				break;
			case "a corporation":
				r.push(`a corporation, making its development steady and unspectacular.`);
				break;
			default:
				r.push(`direct democracy, making its development dangerously unstable.`);
		}
	} else {
		r.push(`is your arcology.`);
	}
	let economicUncertainty = App.Utils.economicUncertainty(i);
	if (V.arcologies[i].direction === 0) {
		r.push(`You control <span class="lime">${V.arcologies[i].ownership}%</span> of the arcology${V.arcologies[0].ownership !== 100 ? `, and the largest minority holder controls ${V.arcologies[i].minority}%.` : `.`}`);
	} else if ((V.arcologies[i].government !== "your trustees") && (V.arcologies[i].government !== "your agent")) {
		r.push(`Its leadership has control of approximately ${Math.trunc(V.arcologies[i].ownership*economicUncertainty)}% of the arcology${(V.arcologies[i].minority > V.arcologies[i].ownership-10) ? `, a dangerously narrow margin over competition with a <span class="tan">${Math.trunc(V.arcologies[i].minority*economicUncertainty)}%</span> share` : ``}.`);
	}
	if (V.arcologies[i].PCminority > 0) {
		r.push(`You own ${V.arcologies[i].PCminority}% of this arcology${(((V.arcologies[i].government === "your trustees") || (V.arcologies[i].government === "your agent")) && V.arcologies[i].minority > V.arcologies[i].PCminority-10) ? `, a dangerously narrow margin over competition with a <span class="red">${Math.trunc(V.arcologies[i].minority*economicUncertainty)}%</span> share` : ``}.`);
	}
	r.push(`The arcology has an estimated GSP of <span class="yellowgreen">${cashFormat(Math.trunc(0.1*V.arcologies[i].prosperity*economicUncertainty))}m,</span>`);
	if (Math.abs(V.arcologies[i].prosperity - averageProsperity) < 5) {
		r.push(`average among`);
	} else if (V.arcologies[i].prosperity > averageProsperity) {
		r.push(`ahead of`);
	} else {
		r.push(`behind`);
	}
	r.push(`its neighbors.`);

	let desc = "";
	const neighborDescription = [];
	/** @type {Record<FC.FutureSociety, Record<95 | 40 | 0, string>>} */
	const FSThresholds = {
		FSSubjugationist: {
			95: `the home of an advanced project to create a subservient race of ${V.arcologies[i].FSSubjugationistRace} slaves`,
			40: `working to refine ${V.arcologies[i].FSSubjugationistRace} slavery`,
			0: `an excellent dumping ground for low quality ${V.arcologies[i].FSSubjugationistRace} slaves.`
		},
		FSSupremacist: {
			95: `a global magnet for ${V.arcologies[i].FSSupremacistRace} nationalists`,
			40: `becoming increasingly free of ${V.arcologies[i].FSSupremacistRace} slaves`,
			0: `the site of a furious debate over existing ${V.arcologies[i].FSSupremacistRace} slaves`
		},
		FSRepopulationFocus: {
			95: `notorious for the size and number of pregnancies among its population`,
			40: `known to be a good place to find slavegirls heavy with children`,
			0: `actively importing fertile slave girls`
		},
		FSRestart: {
			95: `notorious for the number of powerful civilians inhabiting it`,
			40: `known to be a good place to make connections`,
			0: `actively importing sterilization supplies`
		},
		FSGenderRadicalist: {
			95: `notorious for the openness with which its citizens fuck its slavegirls in the ass until they cum`,
			40: `known to be a good place to find slavegirls who cum when buttfucked`,
			0: `actively importing a wider variety of slave girls`
		},
		FSGenderFundamentalist: {
			95: `famous for its slave schools, crowded with a future generation of world class slaves`,
			40: `remarkable for its crowds of hugely pregnant slave women`,
			0: `importing increasing numbers of fertile slaves`
		},
		FSPaternalist: {
			95: `the home of an educated, enlightened caste of slaves more productive than some arcologies' citizens`,
			40: `becoming known for its unusually cheerful atmosphere`,
			0: `starting to demand abused slaves whose lives can be turned around`
		},
		FSDegradationist: {
			95: `renowned and feared by slaves worldwide, as a place of blood and steel from which few ever leave`,
			40: `becoming dreaded by slaves, since the few it exports are full of unbelievably awful stories`,
			0: `importing a rapidly increasing number of slaves, for some reason`
		},
		FSIntellectualDependency: {
			95: `both terrifying and alluring to slaves worldwide, as a place where none leave with any semblance of intelligence but always flaunt the enjoyment they've had`,
			40: `becoming known for its unusually simple-minded chattel`,
			0: `importing large quantities of aphrodisiacs and psychosuppressants`
		},
		FSSlaveProfessionalism: {
			95: `renowned as a source of some of the world's finest courtesans`,
			40: `becoming revered as an intellectual paradise`,
			0: `seeking out high-class slave trainers`
		},
		FSBodyPurist: {
			95: `a world leader in the drug industry due to its pharmaceutical research breakthroughs`,
			40: `pouring an ever increasing amount of money into drug research`,
			0: `setting up research programs to develop better slave drugs`,
		},
		FSTransformationFetishist: {
			95: `renowned as the source of some of the world's most unbelievable surgical transformations`,
			40: `rapidly moving from mere breast expansion to more esoteric surgical fetishism`,
			0: `receiving daily shipments of silicone and surgical necessities`,
		},
		FSYouthPreferentialist: {
			95: `famous for the intense celebratory attention slaves receive there once reaching their majorities`,
			40: `moving virginity and the taking of virginity ever higher in the public estimation`,
			0: `starting to get a reputation as an excellent place to get a good price for a virgin`,
		},
		FSMaturityPreferentialist: {
			95: `world famous among mature slaves, who see it as a paradise in which MILFs are the most valuable girls around`,
			40: `striking for the variety of well-preserved beauties that can be seen there`,
			0: `displaying an increasing demand for enslaved housewives and professional women`,
		},
		FSPetiteAdmiration: {
			95: `known as a place where even the shortest person can feel tall`,
			40: `investing large sums of money into petite clothing lines`,
			0: `starting to accumulate an unusually large number of short slaves`
		},
		FSStatuesqueGlorification: {
			95: `world famous among tall slaves, who see it as a paradise in which they are valued above all`,
			40: `going out of its way to import only the tallest of slaves`,
			0: `starting to drive out its shorter citizenry`,
		},
		FSSlimnessEnthusiast: {
			95: `very well known for the wonderful variety of nubile bodies that can be seen there`,
			40: `becoming known as an arcology that slims slaves down rather than turning them into piles of tits and ass`,
			0: `starting to display unusual fashions regarding breasts and butts`,
		},
		FSAssetExpansionist: {
			95: `widely considered an interior design masterpiece for its adaptations to slaves with fifty kilos of tits`,
			40: `a popular tourist destination just for the view, which features some truly spectacular bare boobs`,
			0: `demanding fatter slaves, since its citizens are learning they absorb growth hormones better`,
		},
		FSPastoralist: {
			95: `a world-renowned producer of cowgirl dairy products of all kinds`,
			40: `devoting more and more of its slaves to work as cowgirls, or to the service and upkeep of cowgirls`,
			0: `displaying an increasing public appetite for dairy, and yet imports almost no true cow's milk`,
		},
		FSCummunism: {
			95: `a world-renowned producer of cum-based products of all kinds`,
			40: `devoting more and more of its slaves to work as cumtanks, or to the service, upkeep and milking of ballgirls`,
			0: `displaying an increasing public appetite for cum, and has begun importing more and more specialized milkers`,
		},
		FSPhysicalIdealist: {
			95: `a constant standout at international athletic competitions, where both its citizens and slaves do very well`,
			40: `quite a sight, since its citizens and slaves all lift constantly`,
			0: `the site of a musclegirl fetish community`,
		},
		FSHedonisticDecadence:  {
			95: `very well known as a place where every imaginable desire and fantasy can be fulfilled`,
			40: `importing huge amounts of food and alcohol`,
			0: `the site of a large number of lazy individuals`,
		},
		FSIncestFetishist: {
			95: `highly recommended as a place to stop by if you like threesomes with twins or familial gangbangs`,
			40: `attracting a substantial number of families`,
			0: `devoting more and more of its resources into genealogy`,
		},
		FSChattelReligionist: {
			95: `a significant force in the global development of Chattel Religionist dogma`,
			40: `a popular destination for devout old world citizens engaging in sex tourism`,
			0: `in the throes of public dissension over its religious laws`,
		},
		FSNull: {
			95: `the source of several new and interesting cultural fusion trends in art, fashion and cuisine`,
			40: `a popular place to visit to broaden one's horizons`,
			0: `struggling with the conflict inherent in becoming a cultural melting pot`
		},
		FSRomanRevivalist: {
			95: `hugely famous for its broadcasts of gladiatorial combat, popular even in the old world`,
			40: `almost obnoxiously aspirational, with citizens competing to serve the state best`,
			0: `working its way through sword and sandals fashion towards proper historicity`,
		},
		FSNeoImperialist: {
			95: `utterly hierarchical, with techno-nobles holding near complete control over the very lives of the masses and powerful Knights in heavy powered plate keeping them firmly in line`,
			40: `increasingly syncretic, integrating the highest technology with total, unrepentant serfdom under tight-fisted techno-nobles and a rising class of elite Knights serving as the sword of the nobility`,
			0: `creating an extremely strict hierarchical system, with an elite caste of techno-nobility cementing itself through a series of complicated, emergent feudal dynamics`,
		},
		FSAztecRevivalist: {
			95: `world famous for its incredible architecture and highly qualified leading caste and military`,
			40: `constructing great pyramids and statues with equally weighty costs`,
			0: `struggling to embrace the amount of blood sacrifice and prayer involved`,
		},
		FSEgyptianRevivalist: {
			95: `a world famous tourist destination for the traditional festival in its plaza, which never stops`,
			40: `very much under renovation, as vast blocks of stone are imported around the clock`,
			0: `struggling with the fashion implications of so much white linen everywhere`,
		},
		FSEdoRevivalist: {
			95: `visibly trailing cherry blossoms, blown off its balconies by the wind`,
			40: `becoming a notable cultural center, even in the old world`,
			0: `the object of considerable debate among socially awkward teenagers worldwide`,
		},
		FSArabianRevivalist: {
			95: `a famous center of Arabian romanticism, since it has the wisdom not to be historically perfect`,
			40: `working through which parts of Arabian romanticism to adopt and which to discard`,
			0: `eagerly reveling in the most romantic parts of superficial Arabian romanticism`,
		},
		FSChineseRevivalist: {
			95: `now confidently mature about its possession of the Mandate of Heaven`,
			40: `often aggressive about its status as the middle kingdom to the point that it betrays uncertainty`,
			0: `moving towards a stage of refinement that will allow it to present itself properly`,
		},
	};

	for (const fs of App.Data.FutureSociety.fsNames) {
		const fsScore = V.arcologies[i][fs];
		if (fsScore !== "unset" && fsScore > 0) {
			if (fsScore > 80) {
				desc = "committed to ";
			} else if (fsScore > 20) {
				desc = "pursuing ";
			} else {
				desc = "just establishing ";
			}
			desc += `<span class="gold">${FutureSocieties.displayName(fs)},</span> and is `;
			for (const threshold of [95, 40, 0]) {
				if (fsScore >= threshold) {
					desc += FSThresholds[fs][threshold] + `.`;
					break;
				}
			}
			neighborDescription.push(desc);
		}
	}

	if (neighborDescription.length > 0) {
		if (neighborDescription.length > 2) {
			r.push(`Its culture is complex. First, it is ${neighborDescription[0]}`);
			for (let j = 1; j < neighborDescription.length; j++) {
				if (j < neighborDescription.length-1) {
					r.push(`It is`);
				} else {
					r.push(`Finally, the arcology is`);
				}
				r.push(neighborDescription[j]);
			}
		} else if (neighborDescription.length === 2) {
			r.push(`Its culture is developing along two lines. First, it is ${neighborDescription[0]} Second, it is ${neighborDescription[1]}`);
		} else {
			r.push(`Its culture is diverging from the old world: it is ${neighborDescription[0]}`);
		}
	}
	App.Events.addNode(el, r, "div");
	return el;
};


code:
App.Neighbor.Display = class {
	/** Create a neighbor display controller
	 * @param {function(number):void} onSelection function to be called when selection changes
	 */
	constructor(onSelection) {
		this.containerID = "neighbor-display";
		this._onSelection = onSelection;
	}

	/** Render the neighbor display to DOM
	 * @returns {Element}
	 */
	render() {
		const container = App.UI.DOM.makeElement("div", null, ["margin-top"]);
		container.id = this.containerID;

		function makeSortLink(/** @type {string} */ title, /** @type {string} */ mode) {
			if (V.neighborDisplay === mode) {
				return document.createTextNode(title);
			} else {
				return App.UI.DOM.link(title, () => { V.neighborDisplay = mode; this.rerender(); } );
			}
		}

		container.append("Neighbor display mode: ");
		container.appendChild(App.UI.DOM.generateLinksStrip([
			makeSortLink.call(this, "List by ID", "list"),
			makeSortLink.call(this, "List by Name", "list-name"),
			makeSortLink.call(this, "City Grid", "grid")
		]));

		// pick a mode
		if (V.neighborDisplay === "grid") {
			container.appendChild(this._renderGrid());
		} else if (V.neighborDisplay === "list-name") {
			container.appendChild(this._renderListName());
		} else { // default if V.neighborDisplay is unset for some reason; canonically "list"
			container.appendChild(this._renderListID());
		}
		return container;
	}

	/** Refresh an existing neighbor display list */
	rerender() {
		$(`#${this.containerID}`).replaceWith(this.render());
		this.select(V.activeArcologyIdx);
	}

	/** Render the display as a list, sorted by ID
	 * @returns {Element}
	 */
	_renderListID() {
		const container = App.UI.DOM.makeElement("div", null, ["neighbor-container", "neighbor-container-list"]);

		for (let i = 0; i < V.arcologies.length; ++i) {
			this._renderCell(container, i);
		}

		return container;
	}

	/** Render the display as a list, sorted by name
	 * @returns {Element}
	 */
	_renderListName() {
		const container = App.UI.DOM.makeElement("div", null, ["neighbor-container", "neighbor-container-list"]);

		const arcologyNames = V.arcologies.map((a) => a.name).sort((a, b) => a.localeCompare(b));
		for (const name of arcologyNames) {
			this._renderCell(container, V.arcologies.findIndex((a) => a.name === name));
		}

		return container;
	}

	/** Render the display as a grid
	 * @returns {Element}
	 */
	_renderGrid() {
		const container = App.UI.DOM.makeElement("div", null, ["neighbor-container", "neighbor-container-grid"]);

		const directionToIndex = (dir) => V.arcologies.findIndex((a) => a.direction === dir);
		for (const dir of [ "northwest", "north", "northeast", "west", 0, "east", "southwest", "south", "southeast" ]) {
			this._renderCell(container, directionToIndex(dir));
		}

		return container;
	}

	/** Render a single cell to the display
	 * @param {Element} container
	 * @param {number} arcID
	 */
	_renderCell(container, arcID) {
		const arcology = V.arcologies[arcID];

		/** render styled text with a tooltip
		 * @param {string} text
		 * @param {string} tooltipText
		 * @param {string} className
		 * @returns {HTMLSpanElement}
		 */
		function withTooltip(text, tooltipText, className) {
			let res = App.UI.DOM.makeElement("span", text, ["has-tooltip", className]);
			tippy(res, {content: tooltipText});
			return res;
		}

		/** @param {function(): void} selectLambda */
		function nameFrag(selectLambda) {
			return App.UI.DOM.makeElement("div", App.UI.DOM.link(arcology.name, selectLambda), "name");
		}

		function govGSPFrag() {
			let frag = document.createDocumentFragment();
			let gov;
			if (arcID === 0) {
				gov = App.UI.DOM.appendNewElement("div", frag, "Your arcology; ");
			} else {
				gov = App.UI.DOM.appendNewElement("div", frag, capFirstChar(arcology.government) + '; ');
			}
			const estimatedGSP = Math.trunc(0.1 * arcology.prosperity * App.Utils.economicUncertainty(arcID));
			App.UI.DOM.appendNewElement("span", gov, cashFormat(estimatedGSP) + "m GSP", "cash");
			return frag;
		}

		function fsFrag() {
			let frag = document.createDocumentFragment();
			frag.appendChild(document.createTextNode("FS: "));
			const fses = FutureSocieties.activeFSes(arcology);
			if (arcID === 0) {
				for (const fs of fses) {
					frag.appendChild(withTooltip("⯁", FutureSocieties.displayName(fs), "steelblue"));
				}
			} else {
				const diplo = FutureSocieties.diplomaticFSes(arcology, V.arcologies[0]);
				for (const fs of fses) {
					let style = "white";
					if (diplo.shared.includes(fs)) {
						style = "steelblue";
					} else {
						const conflict = diplo.conflicting.find((f) => f[0] === fs);
						if (conflict) {
							style = "red";
						}
					}
					frag.appendChild(withTooltip("⯁", FutureSocieties.displayName(fs), style));
				}
			}
			return App.UI.DOM.makeElement("div", frag);
		}

		function ownershipFrag() {
			let frag = document.createDocumentFragment();
			const owned = arcID === 0 || arcology.government === "your trustees" || arcology.government === "your agent";
			const yourPercent = arcID === 0 ? `${arcology.ownership}%` : `${arcology.PCminority}%`;
			const hostilePercent = owned ? `${arcology.minority}%` : `${arcology.ownership}%`;
			const challengerPercent = owned ? null : `${arcology.minority}%`;
			const publicPercent = `${100 - (arcology.ownership + arcology.minority + arcology.PCminority)}%`;
			App.UI.DOM.appendNewElement("li", frag, withTooltip(yourPercent, "Your ownership", "steelblue"));
			if (arcID !== 0) {
				App.UI.DOM.appendNewElement("li", frag, withTooltip(publicPercent, "Public ownership", "yellow"));
			}
			if (challengerPercent) {
				App.UI.DOM.appendNewElement("li", frag, withTooltip(challengerPercent, "Minority challenger ownership", "orange"));
			}
			App.UI.DOM.appendNewElement("li", frag, withTooltip(hostilePercent, "Hostile ownership", "red"));
			return App.UI.DOM.makeElement("ul", frag, "choices-strip");
		}

		let frag = document.createDocumentFragment();
		let classNames = ["neighbor-item"];
		if (!arcology) {
			// empty block
			classNames.push("neighbor-item-empty");
		} else {
			frag.appendChild(nameFrag(() => this.select(arcID)));
			frag.appendChild(govGSPFrag());
			frag.appendChild(fsFrag());
			frag.appendChild(ownershipFrag());
			if (arcID === 0) {
				classNames.push("neighbor-item-self");
			} else if (arcology.government === "your trustees" || arcology.government === "your agent") {
				classNames.push("neighbor-item-owned");
			} else if (arcology.rival) {
				classNames.push("neighbor-item-rival");
			} else {
				classNames.push("neighbor-item-unowned");
			}
		}

		const element = App.UI.DOM.appendNewElement("div", container, frag, classNames);
		element.id = `neighbor-cell-${arcID}`;
	}

	/** Set the selection to a particular arcology
	 * @param {number} arcID
	 */
	select(arcID) {
		const elementID = `#neighbor-cell-${arcID}`;

		$("div[id^='neighbor-cell']").each((i, e) => { $(e).removeClass("neighbor-item-selected"); });
		$(elementID).addClass("neighbor-item-selected");

		this._onSelection(arcID);
	}
};