code:
/**
 * Expand mousetrap with multiple binds per key, passage dependent bindings and a menu for custom rebinding.
 */
App.UI.Hotkeys = (function() {
	/**
	 * @typedef action
	 * @property {Function} callback
	 * @property {Array<string>} combinations 0 <= length <= 2
	 * @property {Array<string>} [passages] not existing means everywhere
	 * @property {string|function(): string} [uiName] allow different name in hotkey settings
	 */

	/**
	 * The key is used in the hotkey settings to describe the action
	 * @type {Object.<string, action>}
	 */
	const actions = {};

	/**
	 * Contains the default combinations for every action
	 * @type {Object.<string, Array<string>>}
	 */
	const defaultCombinations = {};

	/**
	 * References a key combination to a set of actions
	 * @type {Object.<string, Array<string>>}
	 */
	const bindings = {};

	/**
	 * To ensure we only record one at a time
	 * @type {boolean}
	 */
	let recording = false;

	/**
	 * @param {string} name used as key
	 * @param {action} action
	 */
	function addDefault(name, action) {
		actions[name] = action;
		for (const binding of action.combinations) {
			addBinding(name, binding);
		}
		defaultCombinations[name] = [...action.combinations];
	}

	/**
	 * @param {string} actionKey
	 * @param {string} combination
	 */
	function addBinding(actionKey, combination) {
		if (bindings[combination]) {
			bindings[combination].push(actionKey);
		} else {
			bindings[combination] = [actionKey];
			Mousetrap.bind(combination, e => {
				e.preventDefault();
				for (const binding of bindings[combination]) {
					const action = actions[binding];
					// only activate callback if we are on the right passage
					if (!action.passages || action.passages.includes(State.passage)) {
						action.callback();
					}
				}
			});
		}
	}

	/**
	 * @param {string} actionKey
	 * @param {string} combination
	 */
	function removeBinding(actionKey, combination) {
		if (bindings[combination]) {
			const index = bindings[combination].indexOf(actionKey);
			if (index > -1) {
				bindings[combination].splice(index, 1);
				if (bindings[combination].length === 0) {
					delete bindings[combination];
					Mousetrap.unbind(combination);
				}
			}
		}
	}

	/**
	 * @param {string} name
	 * @returns {string}
	 */
	function hotkeysForAction(name) {
		if (!actions[name]) {
			return "";
		}
		const c = actions[name].combinations;
		if (c.length === 0) {
			return "";
		}
		if (c.length === 1) {
			return `[${formatHotkey(c[0])}]`;
		}
		return `[${formatHotkey(c[0])},${formatHotkey(c[1])}]`;
	}

	/**
	 * @param {string} combination
	 * @returns {string}
	 */
	function formatHotkey(combination) {
		const parts = combination.split("+");

		for (let i = 0; i < parts.length; i++) {
			parts[i] = capFirstChar(parts[i]);
		}

		return parts.join("+");
	}

	/**
	 * @returns {HTMLDivElement}
	 */
	function settingsMenu() {
		const div = document.createElement("div");
		div.className = "hotkey-settings";

		for (const actionsKey in actions) {
			settingsRow(div, actionsKey);
		}

		return div;
	}

	/**
	 * @param {HTMLDivElement} container
	 * @param {string} actionKey
	 */
	function settingsRow(container, actionKey) {
		const action = actions[actionKey];
		// get correct name
		let name = actionKey;
		if (action.uiName) {
			if (typeof action.uiName === "string") {
				name = action.uiName;
			} else {
				name = action.uiName();
			}
		}
		App.UI.DOM.appendNewElement("div", container, name, "description");

		settingsCell(container, actionKey, 0);
		settingsCell(container, actionKey, 1);

		const button = App.UI.DOM.appendNewElement("button", container, "Reset");
		if (isDefault(actionKey)) {
			button.className = "inactive";
		} else {
			button.onclick = () => {
				action.combinations = [...defaultCombinations[actionKey]];
				saveToStorage();
				App.UI.reload();
			};
		}
	}

	/**
	 * Checks if the combinations assigned to an action are the default ones.
	 * @param {string} actionKey
	 * @returns {boolean}
	 */
	function isDefault(actionKey) {
		if (defaultCombinations[actionKey].length !== actions[actionKey].combinations.length) {
			return false;
		}
		if (defaultCombinations[actionKey].length === 0) {
			return true;
		}
		if (defaultCombinations[actionKey][0] !== actions[actionKey].combinations[0]) {
			return false;
		}
		if (defaultCombinations[actionKey].length === 1) {
			return true;
		}
		return defaultCombinations[actionKey][1] === actions[actionKey].combinations[1];
	}

	/**
	 * @param {HTMLDivElement} container
	 * @param {string} actionKey
	 * @param {number} index
	 */
	function settingsCell(container, actionKey, index) {
		const action = actions[actionKey];
		const button = App.UI.DOM.appendNewElement("button", container,
			action.combinations[index] ? formatHotkey(action.combinations[index]) : "", "combination");
		button.onclick = () => {
			if (recording) { return; }
			recording = true;

			$(button).empty();
			Mousetrap.record(function(sequence) {
				// sequence is an array like ['ctrl+k', 'c']
				const combination = sequence.join(" ");
				if (action.combinations[index]) {
					removeBinding(actionKey, action.combinations[index]);
				}
				action.combinations[index] = combination;
				addBinding(actionKey, combination);
				saveToStorage();
				App.UI.reload();
				recording = false;
			});
		};
	}

	/**
	 * Saves custom hotkeys to browser storage
	 */
	function saveToStorage() {
		const save = {};

		for (const actionsKey in actions) {
			if (!isDefault(actionsKey)) {
				save[actionsKey] = actions[actionsKey].combinations;
			}
		}

		SugarCube.storage.set("hotkeys", save);
	}

	/**
	 * Loads custom hotkeys from browser storage
	 */
	function loadFromStorage() {
		const save = SugarCube.storage.get("hotkeys");

		for (const saveKey in save) {
			// discard obsolete hotkeys
			if (actions[saveKey]) {
				actions[saveKey].combinations = save[saveKey];
				addBinding(saveKey, save[saveKey]);
			}
		}
	}

	/**
	 * Initialize custom hotkeys
	 */
	function init() {
		loadFromStorage();
		// :storyready is to late to influence the page, but it's the earliest where SugarCube.storage is available so
		// we refresh the passage if we happen to be on the settings passage.
		if (State.passage === "Hotkey Settings") {
			App.UI.reload();
		}
	}

	function settingsPage() {
		const f = document.createDocumentFragment();

		App.UI.DOM.appendNewElement("h1", f, "Hotkey Settings");

		const p = document.createElement("p");
		const ul = document.createElement("ul");

		let li = document.createElement("li");
		li.append("On keyboard layouts other than the ");
		const a = document.createElement("a");
		a.href = "https://en.wikipedia.org/wiki/File:KB_United_States.svg";
		a.target = "_blank";
		a.append("US-QWERTY layout");
		li.append(a,
			" there may be keys or combinations of keys where the recorded key is different from the key used to listen to key events. You will have to find these keys yourself through trial and error.");
		ul.append(li);

		App.UI.DOM.appendNewElement("li", ul, "Custom hotkeys are browser specific and are not part of your save.");

		li = document.createElement("li");
		li.append("While we try not to overwrite browser or OS level key combinations it is possible to do so with custom hotkeys. This also means that during recording of custom hotkeys no browser or OS level key combinations are available. There are however keys that cannot be overwritten, such as the ",
			App.UI.DOM.makeElement("code", "Win key"), " on Windows.");
		ul.append(li);

		p.append(ul);
		f.append(p);

		f.append(settingsMenu());
		return f;
	}

	return {
		add: addDefault,
		hotkeys: hotkeysForAction,
		init: init,
		settings: settingsPage,
	};
})();

// add hotkeys
App.UI.Hotkeys.add("endWeek", {
	callback: function() {
		$("#story-caption #endWeekButton").trigger("click");
	}, combinations: ["enter"], uiName: "Next Week"
});
App.UI.Hotkeys.add("nextLink", {
	callback: function() {
		$("#story-caption #nextButton").trigger("click");
	}, combinations: ["space"], uiName: "Continue/Back"
});
App.UI.Hotkeys.add("prev-slave", {
	callback: function() {
		$("#prev-slave a").trigger("click");
	}, combinations: ["left", "q"], uiName: "Previous Slave"
});
App.UI.Hotkeys.add("next-slave", {
	callback: function() {
		$("#next-slave a").trigger("click");
	}, combinations: ["right", "e"], uiName: "Next Slave"
});
App.UI.Hotkeys.add("prev-child", {
	callback: function() {
		$("#prev-child a.macro-link").trigger("click");
	}, combinations: ["left", "q"], uiName: "Previous Child"
});
App.UI.Hotkeys.add("next-child", {
	callback: function() {
		$("#next-child a.macro-link").trigger("click");
	}, combinations: ["right", "e"], uiName: "Next Child"
});
App.UI.Hotkeys.add("Previous Tab", {
	callback: function() {
		// Basically telling two different systems to go to the left tab, but since we never have multiple tab bars on
		// the same page (as that is bad UI design) the player won't notice.
		App.UI.tabBar.openLeftTab();
		App.UI.Tabs.left();
	}, combinations: []
});
App.UI.Hotkeys.add("Next Tab", {
	callback: function() {
		// Basically telling two different systems to go to the right tab, but since we never have multiple tab bars on
		// the same page (as that is bad UI design) the player won't notice.
		App.UI.tabBar.openRightTab();
		App.UI.Tabs.right();
	}, combinations: []
});
App.UI.Hotkeys.add("Close Dialog", {
	callback: function() {
		Dialog.close();
	}, combinations: ["esc"]
});
App.UI.Hotkeys.add("walkpast", {
	callback: function() {
		$("#walkpast a").trigger("click");
	}, combinations: [], uiName: "Walkpast"
});
App.UI.Hotkeys.add("Head Girl Select", {
	callback: function() {
		$("#manageHG a").trigger("click");
	}, combinations: ["h"]
});


code:
:: StoryData
{
	"ifid":"18DFC483-910E-4D3B-AC31-71AE10C5015A",
	"format": "SugarCube",
	"format-version": "2.36.0"
}

:: Start [nobr]

:: StoryTitle
Free Cities

repo_name: Banyanael/fc

license: mit


code:
/* eslint-disable no-undef */
"use strict";
/* Main SugarCube configuration file. */

/* Change the starting passage from the default 'start' to 'Alpha disclaimer'. */
Config.passages.start = "Alpha disclaimer";

/* Set description used by Save, for all passages, to give some decent information about game state. */
Config.passages.descriptions = function() {
	let sv = State.variables;
	if (sv.arcologies === undefined || sv.arcologies.length === 0) {
		// no arcology yet...
		return "New Game Setup, Week 0";
	} else {
		return `${V.endweekFlag ? "EW! " : ""}${sv.arcologies[0].name}, Week ${sv.week}, ${sv.slaves.length} Slaves, ${cashFormat(sv.cash)}`;
	}
};

/* Disable forward/back buttons in panel. */
Config.history.controls = false;

/* Set Autosaves. */
Config.saves.autosave = "autosave";

/* Save only one game state. */
Config.history.maxStates = 1;

/* Set to 'true' to enable SugarCube's debug mode.
Note: This is an 'engine level' debug mode, completely separate from the game's debug mode. */
Config.debug = false;

/* Set maximum loop iterations. Among other things, this controls the maximum number of slaves the player can own. */
Config.macros.maxLoopIterations = 5000;


code:
// Use release as save version
// We need to do this in a file after fc-version.js because the commit hash gets appended to it during compiling
Config.saves.version = `${App.Version.release}${App.Version.commitHash ? (`#${App.Version.commitHash}`) : ''}`;


code:
(function() {
	const readOnlySymbol = Symbol("readonly proxy");
	const dummy = () => true;

	/**
	 * Creates a readonly proxy for all the target properties except banned methods
	 * @param {any} target
	 * @param {string[]} bannedMethodNames The list of method names that are banned and can't be accesses via the proxy
	 * @param {string} errorMessage message for throwing exceptions
	 */
	const createProxyWithBannedMethods = (target, bannedMethodNames, errorMessage) => {
		return new Proxy(target, {
			get: function(o, prop) {
				if (prop === readOnlySymbol) { return true; }
				const val = o[prop];
				if (typeof val === 'function') {
					if (bannedMethodNames.includes(prop)) {
						return function() {
							throw Error(errorMessage);
						};
					}
					return val.bind(target);
				}
				return createReadonlyProxy(val);
			},
			set: dummy,
			deleteProperty: dummy
		});
	};

	globalThis.createReadonlyProxy = function(target) {
		if (target == null) { return target; } // intentionally ==
		if (target[readOnlySymbol]) { return target; }
		if (_.isArray(target)) {
			return createProxyWithBannedMethods(target, ['push', 'unshift', 'pop'], "Cannot modify a readonly array");
		} else if (_.isMap(target)) {
			return createProxyWithBannedMethods(target, ['clear', 'delete', 'set'], "Cannot modify a readonly Map");
		} else if (_.isSet(target)) {
			return createProxyWithBannedMethods(target, ['add', 'clear', 'delete'], "Cannot modify a readonly Set");
		} else if (_.isObject(target)) {
			return new Proxy(target, {
				get: function(o, prop) {
					if (prop === readOnlySymbol) { return true; }
					return createReadonlyProxy(o[prop]);
				},
				set: dummy,
				deleteProperty: dummy
			});
		}
		return target;
	};
	const cheaterSymbol = Symbol("cheating proxy");
	globalThis.createCheatProxy = function(target) {
		if (target == null) { return target; } // intentionally ==
		if (target[cheaterSymbol]) { return target; }
		if (_.isArray(target)) {
			return new Proxy(target, {
				get: function(o, prop) {
					if (prop === cheaterSymbol) { return true; }
					const val = o[prop];
					if (typeof val === 'function') {
						if (['push', 'unshift', 'pop'].includes(prop)) {
							return function(el) {
								const retval = Array.prototype[prop].apply(o, arguments);
								// Make sure we set cheater after calling the function
								State.variables.cheater = 1;// Can't use `V` because it probably points to a proxy.
								return retval;
							};
						}
						return val.bind(target);
					}
					return createCheatProxy(val);
				},
				set: function(o, prop, value) {
					o[prop] = value;
					State.variables.cheater = 1;// Can't use `V` because it probably points to a proxy.
					return true;
				},
				deleteProperty: function(o, prop) {
					delete o[prop];
					State.variables.cheater = 1;// Can't use `V` because it probably points to a proxy.
					return true;
				}
			});
		}
		if (_.isObject(target)) {
			return new Proxy(target, {
				get: function(o, prop) {
					if (prop === cheaterSymbol) { return true; }
					return createCheatProxy(o[prop]);
				},
				set: function(o, prop, value) {
					o[prop] = value;
					State.variables.cheater = 1;
					return true;
				},
				deleteProperty: function(o, prop) {
					delete o[prop];
					State.variables.cheater = 1;
					return true;
				}
			});
		}
		return target;
	};
})();
Object.defineProperty(window, "V", {
	get: function() {
		if (window.storyProxy != null) { return window.storyProxy; }
		return State.variables;
	}
});

// This should be used if the user might use V under normal, non-cheating circumstances but shouldn't be punished for accidentally setting the value. The attempt to make the change will simply be disregarded.
globalThis.runWithReadonlyProxy = function(callback) {
	globalThis.storyProxy = createReadonlyProxy(State.variables);
	try {
		return callback();
	} finally {
		globalThis.storyProxy = null;
	}
};
// This should be used if setting values would constitute cheating. For example, a debug view that shows all variables in an editable form; showing isn't cheating, but making a change would be.
globalThis.runWithCheatProxy = function(callback) {
	globalThis.storyProxy = createCheatProxy(State.variables);
	try {
		return callback();
	} finally {
		globalThis.storyProxy = null;
	}
};


code:
App.Arcology.Cell.BaseCell = class extends App.Entity.Serializable {
	/**
	 * @param {number} owner
	 */
	constructor(owner) {
		super();
		/**
		 * 0: private
		 * 1: player
		 * @type {number}
		 */
		this.owner = owner;
		/**
		 * List of classes this cell can be converted to.
		 * Format: "BaseCell" stands for App.Arcology.Cell.BaseCell
		 * Note: The current class needs to be included as well, otherwise converting back is not possible.
		 * @type {string[]}
		 */
		this.allowedConversions = [];
	}

	/**
	 * @returns {string}
	 */
	static get cellName() {
		return "baseCell";
	}

	/**
	 * @returns {string}
	 */
	get colorClass() {
		return "empty";
	}

	/**
	 * @returns {number}
	 */
	get width() {
		return 1;
	}

	/**
	 * @returns {string}
	 */
	get name() {
		return "Base Cell";
	}

	/**
	 * @returns {boolean}
	 */
	isBaseType() {
		return true;
	}

	renderCell(path, width) {
		const outerCell = document.createElement("div");
		outerCell.classList.add("outerCell");
		outerCell.style.width = `${width * this.width}%`;

		const innerCell = document.createElement("div");
		innerCell.classList.add("innerCell");
		innerCell.classList.add(this.owner === 1 ? this.colorClass : "private");
		innerCell.append(this.cellContent(path));

		outerCell.append(innerCell);

		return outerCell;
	}

	/**
	 * @param {Array<number>} path
	 * @returns {Node}
	 */
	cellContent(path) {
		return document.createDocumentFragment();
	}

	/**
	 * @param {App.Arcology.Building} containingBuilding
	 * @returns {DocumentFragment}
	 */
	cellPassage(containingBuilding) {
		/**
		 * Thanks JS!
		 * @type {App.Arcology.Cell.BaseCell}
		 */
		const that = this;

		const fragment = document.createDocumentFragment();

		const scene = document.createElement("p");
		scene.classList.add("scene-intro");
		scene.append(this._setting());
		if (this.canBeSold()) {
			scene.append(' ', ownership(this));
		}
		fragment.append(scene);

		if (this.owner === 1) {
			const upgrades = document.createElement("p");

			upgrades.append(this._body(containingBuilding));

			fragment.append(upgrades);

			if (this.allowedConversions.length > 0 && this.isBaseType()) {
				const p = document.createElement("p");
				if (V.rep < 5000) {
					fragment.append(App.UI.DOM.makeElement("p", "You don't have the reputation required to convert the sector base type."));
				} else {
					const cost = 50000;
					const rep = 5000;
					for (const ac of this.allowedConversions) {
						const cellClass = App.Arcology.Cell[ac];
						if (!(this instanceof cellClass)) {
							p.append(App.UI.DOM.link(`Convert sector to ${cellClass.cellName}.`, () => {
								if (Dialog.isOpen()) {
									Dialog.close();
								}

								/** @type {App.Arcology.Cell.BaseCell} */
								const newCell = new cellClass(1);
								newCell.allowedConversions = this.allowedConversions;
								containingBuilding.replaceCell(this, newCell);
								cashX(-cost, "capEx");
								repX(-rep, "capEx");

								containingBuilding.refresh();
								Dialog.setup(newCell.name);
								$(Dialog.body()).empty().append(newCell.cellPassage(containingBuilding));
								Dialog.open();
							}));

							App.UI.DOM.appendNewElement("span", p, ` Costs ${cashFormat(cost)} and ${rep} reputation as many citizens will lose most of what they own.`);
						}
					}
				}
				fragment.append(p);
			}
		}

		return fragment;

		/**
		 * @returns {DocumentFragment}
		 */
		function ownership(cell) {
			const DEMAND_FACTOR_DIFF = 20;
			const fragment = document.createDocumentFragment();
			const A = V.arcologies[0];
			const allCells = V.building.findCells(cell => !(cell instanceof App.Arcology.Cell.Filler));
			const ownedCells = allCells.filter(cell => cell.owner === 1).length;
			const oneCellPercentage = ((1 / allCells.length) * 100).toPrecision(3);

			/**
			 * @param {boolean} buy
			 * @returns {number}
			 */
			function price(buy) {
				let demand = A.demandFactor;
				if (buy) {
					demand += DEMAND_FACTOR_DIFF;
				}
				return 1000 * Math.trunc(A.prosperity * (1 + (demand / 100)));
			}

			if (cell.owner === 1) {
				fragment.append(`Selling this sector would relinquish a ${oneCellPercentage}% interest in ${A.name}. Such an interest is worth ${cashFormat(price(false))}.`);

				if (ownedCells > 1) {
					const span = document.createElement("span");
					span.classList.add("clear-formatting");
					span.append(App.Arcology.getCellLink("Sell", that.name, () => that.cellPassage(containingBuilding),
						() => {
							cashX(price(false), "capEx");
							App.Arcology.updateOwnership();
							A.demandFactor -= DEMAND_FACTOR_DIFF;
							cell.owner = 0;
						}));
					fragment.append(" ", span);
				}
			} else {
				fragment.append(`You will have to acquire an additional ${oneCellPercentage}% interest in ${A.name} to take control of this sector. Such an interest is worth ${cashFormat(price(true))} and will require a transaction cost of ${cashFormat(10000)} to acquire for a total cost of ${cashFormat(price(true) + 10000)}.`);
				const availableCells = allCells.length * ((100 - A.minority) / 100) - ownedCells;
				if (availableCells >= 1) {
					const buySpan = document.createElement("span");
					buySpan.classList.add("clear-formatting");
					buySpan.append(App.Arcology.getCellLink("Buy", that.name, () => that.cellPassage(containingBuilding),
						() => {
							cashX(-(price(true) + 10000), "capEx");
							A.demandFactor += DEMAND_FACTOR_DIFF;
							App.Arcology.updateOwnership();
							cell.owner = 1;
						}));
					fragment.append(" ", buySpan);

					if (V.rep >= 18000) {
						const repDiv = document.createElement("div");
						repDiv.classList.add("clear-formatting");

						const repPrice = Math.clamp(price(true) / 2, 0, 18000);
						repDiv.append("You have so much political capital that you can spend reputation to acquire ownership by spending reputation. ",
							App.Arcology.getCellLink("Use reputation", that.name, () => that.cellPassage(containingBuilding),
								() => {
									repX(-(repPrice), "capEx");
									A.demandFactor += DEMAND_FACTOR_DIFF;
									App.Arcology.updateOwnership();
									cell.owner = 1;
								}));
						fragment.append(repDiv);
					}
				} else {
					fragment.append("Too much of the arcology is owned by a single minority holder for you to force a purchase of this sector right now. Your control of the arcology should naturally resolve this situation in a few weeks.");
				}
			}
			return fragment;
		}
	}

	/**
	 * @returns {string|Node}
	 * @protected
	 */
	_setting() {
		return "baseCell";
	}

	/**
	 * @param {App.Arcology.Building} containingBuilding
	 * @returns {Node}
	 * @protected
	 */
	_body(containingBuilding) {
		return document.createDocumentFragment();
	}

	/**
	 * @param {string} name
	 * @param {function(): void} action
	 * @param {number} cost
	 * @param {App.Arcology.Building} containingBuilding
	 * @param {string} [note]
	 * @param {Node} [domNote]
	 * @returns {HTMLDivElement}
	 * @protected
	 */
	_makeInternalUpgrade(name, action, cost, containingBuilding, note, domNote) {
		const div = document.createElement("div");

		div.append(App.Arcology.getCellLink(name, this.name, () => this.cellPassage(containingBuilding),
			() => {
				cashX(-cost, "capEx");
				action();
			}));

		this._upgradeNote(div, cost, note, domNote);

		return div;
	}

	/**
	 * @param {string} name
	 * @param {function(): void} action
	 * @param {number} cost
	 * @param {string} passage
	 * @param {string} [note]
	 * @param {Node} [domNote]
	 * @returns {HTMLDivElement}
	 * @protected
	 */
	_makeExternalUpgrade(name, action, cost, passage, note, domNote) {
		const div = document.createElement("div");

		div.append(App.UI.DOM.passageLink(name, passage,
			() => {
				cashX(-cost, "capEx");
				action();
				Dialog.close();
			}));

		this._upgradeNote(div, cost, note, domNote);

		return div;
	}

	/**
	 * @param {HTMLDivElement} div
	 * @param {number} cost
	 * @param {string} [note]
	 * @param {Node} [domNote]
	 * @private
	 */
	_upgradeNote(div, cost, note, domNote) {
		if (cost > 0 || note === undefined) {
			note = ` Costs ${cashFormat(cost)}${note !== undefined ? ` ${note}` : ""}.`;
		}
		App.UI.DOM.appendNewElement("span", div, note, "detail");

		if (domNote !== undefined) {
			div.append(domNote); // this only exists for the farmyard, remove once that is out of alpha
		}
	}

	/**
	 * @returns {boolean}
	 */
	canBeSold() {
		return false;
	}

	static _cleanupConfigScheme(config) {
		super._cleanupConfigScheme(config);
		// BC code
	}

	/** @returns {App.Arcology.Cell.BaseCell} */
	clone() {
		return new App.Arcology.Cell.BaseCell(this.owner)._init(this);
	}

	get className() { return "App.Arcology.Cell.BaseCell"; }
};


code:
/** base class for class-based events */
App.Events.BaseEvent = class BaseEvent {
	/**
	 * build a new event
	 * parameters are necessary for serialization (so that saving with the event active will work correctly) and should not normally be used directly
	 * child classes will inherit this implementation automatically, and should not normally need their own constructor implementation
	 * @param {Array<number>} [actors]
	 * @param {object} [params]
	 */
	constructor(actors, params) {
		/** @member {Array<number>} actors - a list of IDs for the actors participating in this event. */
		this.actors = actors || [];
		/** @member {object} params - a set of parameters to pass to the event. */
		this.params = params || {};
	}

	/** Event predicates determine whether the event will occur or not.
	 * @callback eventPredicate
	 * @returns {boolean}
	 */
	/** generate an array of zero or more predicates which must all return true in order for the event to be valid.
	 * lambda predicates may add properties to {@link App.Events.BaseEvent#params the params member} in order to pass information on to the event.
	 * child classes should implement this.
	 * @returns {Array<eventPredicate>}
	 */
	eventPrerequisites() {
		return [];
	}

	/** Actor predicates determine whether an actor is qualified for an event or not.
	 * @callback actorPredicate
	 * @param {App.Entity.SlaveState} slave
	 * @returns {boolean}
	 */
	/** generate an array of zero or more arrays, each corresponding to an actor in the event, which contain zero or more predicates which must be satisfied by the actor.
	 * child classes should implement this, unless they don't need any actors, or are overriding castActors.
	 * @returns {Array<Array<actorPredicate>>}
	 */
	actorPrerequisites() {
		return [];
	}

	/** get the choice weight for the event. Weights should be integral; higher weight means higher probability of selection.
	 * @returns {number}
	 */
	get weight() {
		return 1;
	}

	/** run the event and attach DOM output to the event passage.
	 * child classes must implement this.
	 * @param {ParentNode} node - Document fragment which event output should be attached to
	 */
	execute(node) {
	}

	/** clone the event (needed for serialization).
	 * default implementation should suffice for child classes */
	clone() {
		return _.cloneDeep(this);
	}

	/** serialize the event instance so it persists through saves.
	 * default implementation should suffice for child classes assigned to App.Events */
	toJSON() {
		const reviveData = {actors: this.actors, params: this.params};
		return JSON.reviveWrapper(`new App.Events.${this.constructor.name}($ReviveData$.actors, $ReviveData$.params)`, reviveData);
	}

	/** get the event's (human-readable) name. must not include ":".
	 * default implementation should generally suffice (though we might want to de-camelcase here or something).
	 * @returns {string}
	 */
	get eventName() {
		return this.constructor.name;
	}

	/** build the actual list of actors that will be involved in this event.
	 * default implementation should suffice for child classes with a fixed number of actors; may be overridden for events with variable actor count.
	 * @param {App.Entity.SlaveState} [firstActor] - if supplied, the first actor should be this slave (fail if she is not qualified)
	 * @returns {boolean} - return false if sufficient qualified actors could not be found (cancel the event)
	 */
	castActors(firstActor) {
		const prereqs = this.actorPrerequisites();

		// if casting is already complete and still valid, don't do it again
		if (this.actors.length === prereqs.length) {
			let recast = false;
			for (let i = 0; i < this.actors.length; ++i) {
				const actor = getSlave(this.actors[i]);
				recast = actor && prereqs[i].some(p => !p(actor));
			}
			if (!recast) {
				return true; // all roles previously cast & prereqs still met
			}
		}

		this.actors = [];

		let i = 0;
		if (firstActor && prereqs.length > 0) {
			if (prereqs[0].every(p => p(firstActor))) {
				this.actors.push(firstActor.ID);
			} else {
				return false; // preselected first actor was not qualified
			}
			i = 1; // first actor is cast
		}

		for (; i < prereqs.length; ++i) {
			const qualified = V.slaves.filter(s => !this.actors.includes(s.ID) && prereqs[i].every(p => p(s)));
			if (qualified.length === 0) {
				return false; // a required actor was not found
			}
			this.actors.push(qualified.pluck().ID);
		}

		return true; // all actors cast
	}
};

/** This is a trivial event for use as an example. */
App.Events.TestEvent = class TestEvent extends App.Events.BaseEvent {
	constructor(actors, params) {
		super(actors, params);
	}

	eventPrerequisites() {
		return [];
	}

	actorPrerequisites() {
		return [
			[] // one actor, no requirements
		];
	}

	execute(node) {
		let [eventSlave] = this.actors.map(a => getSlave(a)); // mapped deconstruction of actors into local slave variables
		App.Events.drawEventArt(node, eventSlave);
		node.appendChild(document.createTextNode(`This test event for ${eventSlave.slaveName} was successful.`));
	}
};


code:
/**
 * A pure DOM Passage, the SugarCube Wikifier never gets invoked.
 */
// @ts-ignore
App.DomPassage = class extends Passage {
	/**
	 * @param {string} title
	 * @param {function():Node} callback
	 * @param {string[]} tags
	 */
	constructor(title, callback, tags = []) {
		super(title, {
			hasAttribute: a => a === "tags",
			getAttribute: () => tags.join(" ")
		});
		this.callback = callback;

		// @ts-ignore
		Story.add(this);
	}

	/**
	 * @returns {Node}
	 */
	render() {
		// In case the callback fails give out a nice error message instead of breaking completely.
		try {
			return this.callback();
		} catch (ex) {
			return App.UI.DOM.formatException(ex);
		}
	}
};


code:
// Articles map
/**
 * @typedef {object} article
 * @property {function():(HTMLElement|DocumentFragment)} render
 * @property {string} [category]
 */
/**
 * @type {Map<string, article>}
 */
App.Encyclopedia.articles = new Map();

/**
 * Adds an article if it does not exist yet.
 *
 * @param {string} key
 * @param {function():(HTMLElement|DocumentFragment)} article
 * @param {string} [category]
 */
App.Encyclopedia.addArticle = function(key, article, category) {
	if (!App.Encyclopedia.articles.has(key)) {
		App.Encyclopedia.articles.set(key, {render: article, category: category});
	} else {
		console.log(`Trying to overwrite encyclopedia article "${article}".`);
	}
};

/**
 * Renders the specified article.
 *
 * @param {string} article
 * @returns {string|HTMLElement|DocumentFragment}
 */
App.Encyclopedia.renderArticle = function(article) {
	if (App.Encyclopedia.articles.has(article)) {
		return App.Encyclopedia.articles.get(article).render();
	}

	// TODO: turn this back on when completed.
	// return `Encyclopedia article "${article}" not found.`;
	return null;
};

// Categories map
/**
 * @type {Map<string, function():(HTMLElement|DocumentFragment)>}
 */
App.Encyclopedia.categories = new Map();

/**
 * Adds an article if it does not exist yet.
 *
 * @param {string} key
 * @param {function():(HTMLElement|DocumentFragment)} category
 */
App.Encyclopedia.addCategory = function(key, category) {
	if (!App.Encyclopedia.categories.has(key)) {
		App.Encyclopedia.categories.set(key, category);
	} else {
		console.log(`Trying to overwrite encyclopedia category "${category}".`);
	}
};

/**
 * Renders the specified article.
 *
 * @param {string} article
 * @returns {string|HTMLElement|DocumentFragment}
 */
App.Encyclopedia.renderCategory = function(article) {
	if (!App.Encyclopedia.articles.has(article)) {
		// TODO: turn this back on when completed.
		// return `Encyclopedia article "${article}" not found.`;
		return null;
	}

	const category = App.Encyclopedia.articles.get(article).category;
	if (!category) {
		// TODO: turn this back on when completed.
		// return `Encyclopedia article "${article}" has no category.`;
		return null;
	}

	if (!App.Encyclopedia.categories.has(category)) {
		return `Encyclopedia category "${category}" not found.`;
	}

	return App.Encyclopedia.categories.get(category)();
};

// Utility functions
App.Encyclopedia.topic = function(topic) {
	return App.UI.DOM.makeElement("span", topic, ["encyclopedia", "topic"]);
};

/**
 * @param {Node} parent
 * @param {string} source
 * @param {string} [author]
 */
App.Encyclopedia.addArticleSource = function(parent, source, author) {
	const r = [];

	if (author) {
		r.push(`<span class='author'>${author},</span>`);
	}

	r.push(`<span class='article'>${source}</span>`);

	App.Events.addNode(parent, r, "p", "encyclopedia-source");
};


code:
App.Data.FacilityDesc = class {
	constructor() {
		/** Base name for state variables */
		this.baseName = "";
		/** Generic name for UI (Brothel, Club, etc.)
		 * If null, baseName is used instead
		 */
		this.genericName = "";
		/** @type {Object.<string, FC.Data.JobDesc>} */
		this.jobs = {};
		this.defaultJob = "";
		/** @type {FC.Data.ManagerJobDesc} */
		this.manager = null;
		this.decorated = false;
	}
};

App.Data.Facilities = {};
App.Entity.Facilities = {};

App.Entity.Facilities.Job = class {
	constructor() {
		/** @type {FC.Data.JobDesc} */
		this.desc = null;
		/** @type {App.Entity.Facilities.Facility} */
		this.facility = null;
	}

	/**
	 * Does slave meet the requirements for this job
	 * @param {App.Entity.SlaveState} slave
	 * @returns {string[]}
	 */
	checkRequirements(slave) {
		let r = [];
		if (this.desc.publicSexUse &&
			(slave.breedingMark === 1 && V.propOutcome === 1 && V.eugenicsFullControl !== 1 && V.arcologies[0].FSRestart !== "unset")) {
			r.push(`${slave.slaveName} is for private use only.`);
		}
		if (!this.desc.fuckdollAccepted && slave.fuckdoll > 0) {
			r.push(`Fuckdolls can't ${this.desc.assignment} at ${this.facility.name}.`);
		}
		if (!this.desc.broodmotherAccepted && slave.preg > 37 && slave.broodmother === 2) {
			r.push(`Birthing broodmothers can't ${this.desc.assignment}.`);
		}
		return r;
	}

	/**
	 * Can slave be employed at this position
	 * @param {App.Entity.SlaveState} slave
	 * @returns {string[]}
	 */
	canEmploy(slave) {
		if (this.isEmployed(slave)) {
			return [`${slave.slaveName} is already assigned to ${this.desc.assignment} at ${this.facility.name}.`];
		}
		if (!this._facilityHasFreeSpace) {
			return [`Capacity of ${this.facility.name} exceeded.`];
		}
		if (slave.assignment === this.desc.assignment) {
			return [`${slave.slaveName} is already assigned to ${this.desc.assignment}.`];
		}

		return this.checkRequirements(slave);
	}

	/**
	 * Is slave with the given ID already assigned to this job
	 * @param {number} id
	 * @returns {boolean}
	 */
	hasEmployeeWithId(id) {
		return this.employeesIDs().has(id);
	}

	/**
	 * Is slave already assigned to this job
	 * @param {App.Entity.SlaveState} slave
	 * @returns {boolean}
	 */
	isEmployed(slave) {
		return this.hasEmployeeWithId(slave.ID);
	}

	/**
	 * @callback assignmentCallback
	 * @param {App.Entity.SlaveState} slave the slave whose assignment changes
	 * @param {FC.Assignment} assignment new assignment
	 * @returns {void}
	 */

	/**
	 * Returns link text for the penthouse assignment
	 * @param {number} ID slave ID
	 * @param {string} [passage] passage to go to
	 * @param {assignmentCallback} [callback]
	 * @param {string} [linkText]
	 * @returns {HTMLAnchorElement}
	 */
	assignmentLinkElement(ID, passage, callback, linkText) {
		linkText = linkText || this.desc.position;
		return App.UI.DOM.assignmentLink(slaveStateById(ID), this.desc.assignment, passage, callback, linkText);
	}

	/**
	 * all slaves that are employed at this job
	 * @returns {App.Entity.SlaveState[]}
	 */
	employees() {
		return [...this.employeesIDs()].map(id => slaveStateById(id));
	}

	/**
	 * IDs for all slaves that are employed at this job
	 * @returns {Set<number>}
	 */
	employeesIDs() {
		return V.JobIDMap[this.desc.assignment];
	}

	/**
	 * assignment string but the generic facility name replaced with its in-game name
	 */
	get assignment() {
		return this.desc.assignment.replace("the " + this.facility.desc.baseName, this.facility.name);
	}

	/**
	 * Tests if slave is broken enough
	 * @protected
	 * @param {App.Entity.SlaveState} slave
	 * @param {number} [pureDevotion=50] Minimal devotion level to pass test with any trust
	 * @param {number} [devotion=-50] Minimal devotion for slaves with enough fear
	 * @param {number} [trust=-21] Maximal trust (i.e. minimal fear) for the less devotional (see above)
	 * @param {number} [pureFear=-51] Maximal low trust to pass test with any devotion (because of the fear)
	 * @param {number} [pureTrust=101] Minimal high trust level to pass test without devotion
	 * @returns {boolean}
	 */
	static _isBrokenEnough(slave, pureDevotion = 50, devotion = -51, trust = -21, pureFear = -51, pureTrust = 101) {
		return slave.devotion >= pureDevotion ||
			slave.trust >= pureTrust ||
			slave.trust <= pureFear ||
			(
				slave.devotion > devotion &&
				slave.trust < trust
			);
	}

	/**
	 * @protected
	 * Standard message that slave is not broken enough
	 * @param {App.Entity.SlaveState} slave
	 * @returns {string}
	 */
	static _stdBreakageMessage(slave) {
		return `${slave.slaveName} must be either more fearful of you or devoted to you.`;
	}

	/** @protected */
	get _facilityHasFreeSpace() {
		return this.facility.hasFreeSpace;
	}
};

App.Entity.Facilities.ManagingJob = class extends App.Entity.Facilities.Job {
	constructor() {
		super();
		/** @type {FC.Data.ManagerJobDesc} */
		this.desc = null;
	}

	/**
	 * Can slave be employed at this position
	 * @param {App.Entity.SlaveState} slave
	 * @returns {string[]}
	 */
	checkRequirements(slave) {
		let r = super.checkRequirements(slave);
		if (slave.devotion < this.desc.requiredDevotion) {
			r.push(`${slave.slaveName} must be more devoted to you.`);
		}
		if (this.desc.shouldWalk && !canWalk(slave)) {
			r.push(`${slave.slaveName} must be able to walk.`);
		}
		if (this.desc.shouldHold && !canHold(slave)) {
			r.push(`${slave.slaveName} must be able to hold on to objects.`);
		}
		if (this.desc.shouldSee && !canSee(slave)) {
			r.push(`${slave.slaveName} must have working eyes.`);
		}
		if (this.desc.shouldHear && !canHear(slave)) {
			r.push(`${slave.slaveName} must be able to hear.`);
		}
		if (this.desc.shouldTalk && !canTalk(slave)) {
			r.push(`${slave.slaveName} must be able to talk.`);
		}
		if (this.desc.shouldThink && slave.fetish === "mindbroken") {
			r.push(`${slave.slaveName} must possess cognition.`);
		}
		return r;
	}

	/**
	 * Returns true if slave has enough applicable skill or career
	 * @param {App.Entity.SlaveState} slave
	 * @returns {boolean}
	 */
	slaveHasExperience(slave) {
		return (this.desc.skill !== null && slave.skill[this.desc.skill] >= V.masteredXP) ||
			(typeof slave.career === 'string' && this.desc.careers.includes(slave.career));
	}

	/** @returns {App.Entity.SlaveState} */
	get currentEmployee() {
		const employees = this.employees();
		return employees.length > 0 ? employees[0] : null;
	}

	/** @protected */
	get _facilityHasFreeSpace() {
		return true;
	}

	/** @returns {string} */
	get positionAbbreviation() {
		return this.desc.positionAbbreviation || "L";
	}
};

App.Entity.Facilities.Facility = class {
	/**
	 * @param {FC.Data.FacilityDesc} desc defines state variable for this facility
	 * @param {Object.<string, App.Entity.Facilities.Job>} [jobs] job object that are not default
	 * @param {App.Entity.Facilities.ManagingJob} [manager]
	 */
	constructor(desc, jobs, manager) {
		this.desc = desc;
		jobs = jobs || {};
		/** @private @type {Object.<string, App.Entity.Facilities.Job>} */
		this._jobs = {};

		for (const jn in this.desc.jobs) {
			if (jobs[jn] !== undefined) {
				this._jobs[jn] = jobs[jn];
			} else {
				this._jobs[jn] = this._createJob(jn);
			}
			this._jobs[jn].facility = this;
			this._jobs[jn].desc = desc.jobs[jn];
		}

		if (manager === undefined) {
			// default manager job implementation
			manager = (this.desc.manager !== null) ? new App.Entity.Facilities.ManagingJob() : null;
		}
		/** @private */
		this._manager = manager;
		if (this._manager !== null) {
			this._manager.facility = this;
			this._manager.desc = this.desc.manager;
		}
	}

	/** Facility display name
	 * @returns {string} */
	get name() {
		const stateObj = V[this.desc.baseName];
		const res = (stateObj && typeof stateObj === "object") ? stateObj.name : V[this.desc.baseName + "Name"];
		return res !== undefined ? res : 'the ' + this.genericName;
	}

	set name(newName) {
		const stateObj = V[this.desc.baseName];
		if (stateObj && typeof stateObj === "object") {
			stateObj.name = newName;
		} else {
			V[this.desc.baseName + "Name"] = newName;
		}
	}

	/** @returns {string} */
	get nameCaps() {
		return capFirstChar(this.name);
	}

	/** @returns {string} */
	get UIName() {
		return capFirstChar(this.genericName);
	}

	/** Facility generic name ("Brothel", "Schoolroom", etc.)
	 * @returns {string} */
	get genericName() {
		return this.desc.genericName !== null ? this.desc.genericName : capFirstChar(this.desc.baseName);
	}

	/** All jobs at this facility
	 * @returns {string[]}
	 */
	get jobsNames() {
		return Object.keys(this.desc.jobs);
	}

	/**
	 * Returns job description
	 * @param {string} [name] job name; the default job will be used if omitted
	 * @returns {App.Entity.Facilities.Job}
	 */
	job(name) {
		return this._jobs[name || this.desc.defaultJob];
	}

	get jobs() {
		return Object.values(this._jobs);
	}

	get manager() {
		return this._manager;
	}

	/** Facility slave capacity
	 * @returns {number} */
	get capacity() {
		return V[this.desc.baseName] !== null && ((typeof V[this.desc.baseName] === "object") ? V[this.desc.baseName].capacity : V[this.desc.baseName]);
	}

	get established() {
		return this.capacity > 0;
	}

	/** Number of already hosted slaves
	 * @returns {number} */
	get hostedSlaves() {
		return this.jobs.reduce((acc, job) => { return acc + job.employeesIDs().size; }, 0);
	}

	/**
	 * Count of employees plus manager altogether
	 * @returns {number}
	 */
	get totalEmployeesCount() {
		return this.hostedSlaves + ((this.manager && this.manager.currentEmployee) ? 1 : 0);
	}

	/**
	 * @returns {number} count of non slave occupants (e.g. bioreactors in the dairy)
	 */
	get nonEmployeeOccupantsCount() {
		return 0;
	}

	/**
	 * @returns {number} total count of facility places taken by employees and other beings (e.g. bioreactors in the dairy)
	 */
	get totalOccupants() {
		return this.hostedSlaves + this.nonEmployeeOccupantsCount;
	}

	get hasEmployees() {
		return this.jobs.some(j => j.employeesIDs().size > 0);
	}

	get hasFreeSpace() {
		return this.capacity > this.totalOccupants;
	}

	get freeSpace() {
		return this.capacity > 0 ? this.capacity - this.totalOccupants : 0;
	}

	/**
	 * @param {string} name
	 * @returns {number}
	 */
	option(name) {
		return (typeof V[this.desc.baseName] === "object") ? V[this.desc.baseName][name] : V[this.desc.baseName + name];
	}

	/**
	 * @param {string} name
	 * @returns {number}
	 */
	upgrade(name) {
		return (typeof V[this.desc.baseName] === "object") ? V[this.desc.baseName].upgrade[name] : this.option("Upgrade" + name);
	}

	/** Can this facility be decorated? */
	get isDecorated() {
		return this.desc.decorated;
	}

	/** How is this facility currently decorated?
	 * @returns {FC.FutureSocietyDeco}
	 */
	get decoration() {
		if (!this.isDecorated) {
			return "standard";
		}
		return (typeof V[this.desc.baseName] === "object") ? V[this.desc.baseName].decoration : V[this.desc.baseName + "Decoration"];
	}

	/** Configure facility decoration
	 * @param {FC.FutureSocietyDeco} val
	 */
	set decoration(val) {
		if (this.isDecorated) {
			if (typeof V[this.desc.baseName] === "object") {
				V[this.desc.baseName].decoration = val;
			} else {
				V[this.desc.baseName + "Decoration"] = val;
			}
		}
	}

	/** Returns the facility's value, including all purchased upgrades. */
	get value() {
		return 10000;
	}

	/**
	 * Can this facility host the given slave
	 * @param {App.Entity.SlaveState} slave
	 * @param {string} [job]
	 * @returns {string[]} array with rejection reasons. Slave can be hosted if this is empty.
	 */
	canHostSlave(slave, job) {
		job = job || this.desc.defaultJob;
		const j = this.job(job);
		if (j === undefined) {
			if (this.jobs.length === 0) {
				const err = `${this.genericName} facility has no jobs for slaves`;
				console.warn(err);
				return [err];
			} else {
				console.warn(`Can't find job ${job} at ${this.name}.`); // eslint-disable-line no-console
			}
		}
		// if there are more than one job at this facility, test them too
		if (Object.keys(this.desc.jobs).length > 1 && this.isHosted(slave)) {
			return [`${slave.slaveName} is already assigned to ${slave.assignment} at ${this.name}.`];
		}
		return j.canEmploy(slave);
	}

	/**
	 * Does the given slave work at this facility
	 * @param {App.Entity.SlaveState} slave
	 * @returns {boolean}
	 */
	isHosted(slave) {
		return this.jobs.some(job => job.isEmployed(slave));
	}

	/**
	 * Returns link text for the job assignments
	 * @param {number} ID slave ID
	 * @param {string} [job] generate link only for this job
	 * @param {string} [passage]
	 * @param {assignmentCallback} [callback]
	 * @returns {HTMLElement[]}
	 */
	assignmentLinkElements(ID, job, passage, callback) {
		/** @type {App.Entity.SlaveState} */
		const slave = slaveStateById(ID);
		const jobs = job === undefined ? this._jobs : {job: this._jobs[job]};

		let res = [];
		for (const jn in jobs) {
			const j = jobs[jn];
			let rejects = j.canEmploy(slave);
			if (rejects.length === 0) {
				res.push(j.assignmentLinkElement(ID, passage, callback));
			} else {
				res.push(App.UI.DOM.disabledLink(j.desc.position, rejects));
			}
		}
		return res;
	}

	/**
	 * Returns link text for the facility transfer
	 * @param {number} ID slave ID
	 * @param {string} [job] transfer to this job (uses default job if this is undefined)
	 * @param {string} [passage]
	 * @param {assignmentCallback} [callback]
	 * @returns {HTMLElement}
	 */
	transferLinkElement(ID, job, passage, callback) {
		job = job || this.desc.defaultJob;
		return this._jobs[job].assignmentLinkElement(ID, passage, callback, this.genericName);
	}

	/**
	 * all slaves that are employed at this job
	 * @returns {App.Entity.SlaveState[]}
	 */
	employees() {
		const jobArray = this.jobs;
		if (jobArray.length === 1) {
			return jobArray[0].employees();
		}
		return [].concat(...jobArray.map(j => j.employees()));
	}

	/**
	 * IDs for all slaves that are employed at this facility (excluding the manager)
	 * @returns {Set<number>}
	 */
	employeesIDs() {
		const jobArray = this.jobs;
		if (jobArray.length === 1) {
			return jobArray[0].employeesIDs();
		}
		const res = new Set();
		for (const j of jobArray) {
			const ids = j.employeesIDs();
			for (const id of ids) {
				res.add(id);
			}
		}
		return res;
	}

	/**
	 * @param {boolean} long
	 * @returns {string}
	 */
	occupancyReport(long) {
		if (this.jobs.length === 0) {
			return this.manager && this.manager.currentEmployee ? this.manager.positionAbbreviation : "";
		}
		const nonEmployees = this.nonEmployeeOccupantsCount;
		const managerStr = this.manager && this.manager.currentEmployee ? (
			long ? `, ${this.manager.desc.position}` : `, ${this.manager.positionAbbreviation}`) : "";
		const slavesStr = long ? ` ${this.job().desc.position}${this.capacity !== 1 ? 's' : ''}` : "";
		return `${this.hostedSlaves}${nonEmployees ? `+${nonEmployees}` : ""}/${this.capacity}${slavesStr}${managerStr}`;
	}

	/**
	 * @protected
	 * @param {string} jobName
	 * @returns {App.Entity.Facilities.Job}
	 */
	_createJob(jobName) { /* eslint-disable-line no-unused-vars*/
		return new App.Entity.Facilities.Job();
	}
};

/**
 * Job for a facility with a single job option
 */
App.Entity.Facilities.FacilitySingleJob = class extends App.Entity.Facilities.Job {
	/**
	 * @param {number} ID slave ID
	 * @param {string} [targetPassage] passage to go to
	 * @param {assignmentCallback} [callback]
	 * @param {string} [linkText]
	 * @returns {HTMLAnchorElement}
	 */
	assignmentLinkElement(ID, targetPassage, callback, linkText) {
		linkText = linkText || this.desc.position;
		return App.UI.DOM.assignmentLink(slaveStateById(ID), this.desc.assignment, "",
			(slave, assignment) => {
				if (callback) {
					callback(slave, assignment);
				}
				assignmentTransition(slave, assignment, targetPassage || passage());
			}, linkText);
	}
};

App.Entity.Facilities.SingleJobFacility = class extends App.Entity.Facilities.Facility {
	/**
	 * @param {FC.Data.FacilityDesc} desc defines state variable for this facility
	 * @param {Object.<string, App.Entity.Facilities.Job>} [jobs] job object that are not default
	 * @param {App.Entity.Facilities.ManagingJob} [manager]
	 */
	constructor(desc, jobs, manager) {
		super(desc, jobs, manager);
		if (this.jobs.length !== 1) {
			throw Error(`SingleJobFacility accepts only a single job, but ${this.jobs.length} were provided`);
		}
		this._job = this.job(); // cache the only job
	}

	/**
	 * @override
	 * @protected
	 * @returns {App.Entity.Facilities.FacilitySingleJob}
	 */
	_createJob() {
		return new App.Entity.Facilities.FacilitySingleJob();
	}

	get facilityJob() {
		return this._job;
	}
};

/** Instances of all facility objects */
App.Entity.facilities = {};


code:
/**
 * Creates the facility passage for a facility.
 *
 * Not to be confused with `App.Entity.Facilities.Facility`, which handles the logic.
 */
App.Facilities.Facility = class Facility {
	/**
	 * @param {App.Entity.Facilities.Facility} facility The instance form of the facility. Typically found in `App.Entity.facilities`.
	 * @param {function():void} decommissionHandler
	 */
	constructor(facility, decommissionHandler) {
		this.facility = facility;

		/** @private */
		this._div = document.createElement("div");
		/** @private @type {Array<function():HTMLDivElement>} */
		this._sections = [];
		/** @private @type {FC.IUpgrade[]} */
		this._upgrades = [];
		/** @private @type {FC.Facilities.Rule[]} */
		this._rules = [];

		this._addUpgrades(...this.upgrades);
		this._addRules(...this.rules);

		this._addSections(
			() => this._makeIntro(decommissionHandler),
			() => this._makeExpand(),
			() => this._makeUpgrades(),
			() => this._makeRules(),
			() => this._makeCustomNodes(),
			() => this._makeMenials(),
			() => this._makeStats(),
			() => this._makeAds(),
			() => this._makeSlaves(),
			() => this._makeRename(),
		);
	}

	/**
	 * Puts the different sections together into one passage.
	 *
	 * @private
	 * @returns {DocumentFragment}
	 */
	_assemble() {
		const frag = new DocumentFragment();

		this._sections.forEach(section => frag.append(App.UI.DOM.makeElement("div", section(), ['margin-bottom'])));

		return frag;
	}

	/**
	 * Renders the facility onscreen.
	 *
	 * @returns {HTMLDivElement}
	 */
	render() {
		this._div.append(this._assemble());

		return this._div;
	}

	/**
	 * Refreshes the facility onscreen.
	 *
	 * @returns {void}
	 */
	refresh() {
		App.UI.DOM.replace(this._div, this._assemble());
	}

	/**
	 * Adds new sections to the facility passage.
	 *
	 * @param {...function():HTMLDivElement} args
	 *
	 * @private
	 * @returns {void}
	 */
	_addSections(...args) {
		this._sections.push(...args);
	}

	/**
	 * Adds new purchaseable upgrades.
	 * @param {...FC.IUpgrade} upgrades
	 *
	 * @private
	 * @returns {void}
	 */
	_addUpgrades(...upgrades) {
		this._upgrades.push(...upgrades);
	}

	/**
	 * Adds new rules able to be set by the player.
	 * @param {...FC.Facilities.Rule} rules
	 *
	 * @private
	 * @returns {void}
	 */
	_addRules(...rules) {
		this._rules.push(...rules);
	}

	/**
	 * Sets up the intro scene.
	 *
	 * @param {function():void} decommissionHandler
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeIntro(decommissionHandler) {
		const div = document.createElement("div");

		App.UI.DOM.appendNewElement("h1", div, this.facility.nameCaps);
		App.UI.DOM.appendNewElement("div", div, this.intro, ['scene-intro']);

		if (this.facility.totalEmployeesCount === 0) {
			div.append(App.UI.DOM.makeElement("div", App.UI.DOM.passageLink(`Decommission ${this.facility.name}`, "Main", decommissionHandler), ['indent']));
		}

		return div;
	}

	/**
	 * Allows the facility to be expanded.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeExpand() {
		const div = document.createElement("div");

		// TODO: rework once all facilities are objects
		const capacity = typeof V[this.facility.desc.baseName] === 'object'
			? V[this.facility.desc.baseName].capacity
			: V[this.facility.desc.baseName];
		const maximum = this.expand.maximum || Number.MAX_SAFE_INTEGER;
		const amount = this.expand.amount || 5;
		const cost = this.expand.cost || capacity * 1000 * V.upgradeMultiplierArcology;
		const desc = this.expand.desc ||
			`${this.facility.nameCaps} can support ${this.facility.capacity} slaves. There ${this.facility.hostedSlaves === 1 ? `is` : `are`} currently ${numberWithPluralOne(this.facility.hostedSlaves, "slave")} here.`;

		div.append(desc);

		if (!this.expand.unexpandable) {
			if (capacity < maximum) {
				App.UI.DOM.appendNewElement("div", div, App.UI.DOM.link(`Expand ${this.facility.name}`, () => {
					cashX(forceNeg(cost), "capEx");
					if (typeof V[this.facility.desc.baseName] === 'object') {		// TODO: change this once all facilities are objects
						V[this.facility.desc.baseName].capacity += amount;
					} else {
						V[this.facility.desc.baseName] += amount;
					}
					V.PC.skill.engineering += .1;

					this.refresh();
				}, [], '', `Costs ${cashFormat(cost)} and increases the capacity of ${this.facility.name} by ${amount}.`), ['indent']);
			} else {
				App.UI.DOM.appendNewElement("div", div, App.UI.DOM.disabledLink(`Expand ${this.facility.name}`, [
					`${this.facility.nameCaps} can support a maximum of ${maximum} slaves.`,
				]), ['indent']);
			}
		}

		if (this.facility.totalEmployeesCount > 0) {
			App.UI.DOM.appendNewElement("div", div, this.expand.removeAll || removeFacilityWorkers(this.facility, this.expand.removeManager, this.expand.removeSlave), ['indent']);
		}

		return div;
	}

	/**
	 * Allows the facility to be upgraded.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeUpgrades() {
		const div = document.createElement("div");

		if (this.upgrades.length > 0) {
			App.UI.DOM.appendNewElement("h2", div, `Upgrades`);

			this._upgrades.forEach(u => {
				const upgrade = new App.Upgrade(u.property, u.tiers, u.object);

				div.append(upgrade.render());
			});
		}

		return div;
	}

	/**
	 * Allows rules to be set up in the facility.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeRules() {
		const div = document.createElement("div");

		if (this.rules.length > 0 && this.rules.some(rule => rule.prereqs.every(prereq => prereq()))) {
			App.UI.DOM.appendNewElement("h2", div, `Rules`);

			this._rules.forEach(rule => {
				if (rule.prereqs.every(prereq => prereq())) {
					const options = new App.UI.OptionsGroup();
					const option = options.addOption(null, rule.property, rule.object || V);

					rule.options.forEach(o => {
						if (!o.prereqs || o.prereqs.every(prereq => prereq())) {
							option.addValue(o.link, o.value);

							const dialogHandler = o.dialog ? function() {
								if (Dialog.isOpen()) {
									Dialog.close();
								}
								if (o.dialog.name) {
									Dialog.setup(o.dialog.name);
								}
								$(Dialog.body()).empty().append(o.dialog.content);
								Dialog.open();
							} : null;

							if (dialogHandler && o.handler) {
								option.addCallback(() => {
									o.handler();
									dialogHandler();
								});
							} else if (o.handler) {
								option.addCallback(o.handler);
							} else if (dialogHandler) {
								option.addCallback(dialogHandler);
							}

							if (o.note) {
								option.addComment(o.note);
							}

							if ((rule.object && _.isEqual(rule.object[rule.property], o.value)) ||
							_.isEqual(V[rule.property], o.value)) {
								App.UI.DOM.appendNewElement("div", div, o.text);
							}
						}
					});

					App.UI.DOM.appendNewElement("div", div, options.render(), ['indent', 'margin-bottom']);
				}

				if (rule.nodes) {
					App.Events.addNode(div, rule.nodes);
				}
			});
		}

		return div;
	}

	/**
	 * Creates any custom nodes and adds them to the facility.
	 *
	 * Custom nodes are always placed between the rules section and the menial slaves section (or where they would be).
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeCustomNodes() {
		const div = document.createElement("div");

		if (this.customNodes) {
			this.customNodes.forEach(node => div.append(node));
		}

		return div;
	}

	/**
	 * Allows menial slaves to be bought, sold, and assigned to the facility.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeMenials() {
		const div = document.createElement("div");

		if (this.menials) {
			App.UI.DOM.appendNewElement("h2", div, `Menials`);
			App.UI.DOM.appendNewElement("div", div, this.menials, ['margin-bottom']);
		}

		return div;
	}

	/**
	 * Displays a table with statistics relating to the facility.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeStats() {
		const div = document.createElement("div");

		if (this.stats) {
			App.UI.DOM.appendNewElement("h2", div, `Statistics`);
			App.UI.DOM.appendNewElement("div", div, this.stats, ['margin-bottom']);
		}

		return div;
	}

	/**
	 * Adds a section for managing advertisements to the facility.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeAds() {
		const div = document.createElement("div");

		if (this.ads) {
			App.UI.DOM.appendNewElement("h2", div, `Advertising`);
			App.UI.DOM.appendNewElement("div", div, this.ads, ['margin-bottom']);
		}

		return div;
	}

	/**
	 * Displays a list of slaves that can be assigned and removed.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeSlaves() {
		const div = document.createElement("div");

		App.UI.DOM.appendNewElement("h2", div, `Slaves`);
		App.UI.DOM.appendNewElement("div", div, this.slaves || App.UI.SlaveList.stdFacilityPage(this.facility, true), ['margin-bottom']);

		App.UI.SlaveList.ScrollPosition.restore();

		return div;
	}

	/**
	 * Adds a textbox allowing the facility to be renamed.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeRename() {
		const div = document.createElement("div");

		App.UI.DOM.appendNewElement("h2", div, `Rename`);
		App.UI.DOM.appendNewElement("div", div, App.Facilities.rename(this.facility, () => this.refresh()));

		return div;
	}

	// Getters and Setters

	// Getters that returns a nullish type are used for type checking and autocompletion.

	/**
	 * The text displayed in the intro scene.
	 *
	 * @returns {string}
	 */
	 get intro() {
		return null;
	}

	/**
	 * The facility description in the intro scene.
	 *
	 * @returns {string}
	 */
	 get decorations() {
		return null;
	}

	/**
	 * Adds an option to expand the facility.
	 *
	 * Describes how many slaves are in the facility out of how many are supported by default.
	 *
	 * @returns {FC.Facilities.Expand}
	 */
	get expand() {
		return {};
	}

	/**
	 * Any upgrades available for purchase.
	 *
	 * @returns {FC.IUpgrade[]}
	 */
	get upgrades() {
		return [];
	}

	/**
	 * Any rules able to be set.
	 *
	 * @returns {FC.Facilities.Rule[]}
	 */
	get rules() {
		return [];
	}

	/**
	 * Any menials that are able to be assigned.
	 *
	 * @returns {HTMLDivElement}
	 */
	get menials() {
		return null;
	}

	/**
	 * Any statistics table to display.
	 *
	 * @returns {HTMLDivElement}
	 */
	get stats() {
		return null;
	}

	/**
	 * Any advertisements to display.
	 *
	 * @returns {HTMLDivElement}
	 */
	get ads() {
		return null;
	}

	/**
	 * Any slave selection menu to display.
	 *
	 * Uses the standard list if not defined.
	 *
	 * @returns {HTMLDivElement}
	 */
	get slaves() {
		return null;
	}

	/**
	 * Allows for the addition of any custom nodes to the facility.
	 *
	 * @returns {HTMLDivElement[]}
	 */
	 get customNodes() {
		return [];
	}
};


code:
App.SpecialSlavesProxy = class SpecialSlavesProxy {
	constructor() {
	}

	get Attendant() {
		return slaveStateById(V.AttendantID);
	}
	get Bodyguard() {
		return slaveStateById(V.BodyguardID);
	}
	get Concubine() {
		return slaveStateById(V.ConcubineID);
	}
	get DJ() {
		return slaveStateById(V.djID);
	}
	get Farmer() {
		return slaveStateById(V.FarmerID);
	}
	get HeadGirl() {
		return slaveStateById(V.HeadGirlID);
	}
	get Lurcher() {
		return slaveStateById(V.LurcherID);
	}
	get Madam() {
		return slaveStateById(V.MadamID);
	}
	get Matron() {
		return slaveStateById(V.MatronID);
	}
	get Milkmaid() {
		return slaveStateById(V.MilkmaidID);
	}
	get Nurse() {
		return slaveStateById(V.NurseID);
	}
	get Recruiter() {
		return slaveStateById(V.RecruiterID);
	}
	get Schoolteacher() {
		return slaveStateById(V.SchoolteacherID);
	}
	get Stewardess() {
		return slaveStateById(V.StewardessID);
	}
	get Stud() {
		return slaveStateById(V.StudID);
	}
	get Wardeness() {
		return slaveStateById(V.WardenessID);
	}
	get activeSlave() {
		return V.activeSlave;
	}
};

globalThis.S = new App.SpecialSlavesProxy();


code: new App.DomPassage("StoryCaption", () => { return App.UI.storyCaption(); });


code:
new App.DomPassage("Backwards Compatibility",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Main";
		V.returnTo = "Main";

		App.Update.setNonexistentProperties(V, App.Data.defaultGameStateVariables);

		// resetOnNGPlus contains half of the variables we need, but we use it politely here instead of forcing it so it
		// fills in holes instead of overwriting data.
		App.Update.setNonexistentProperties(V, App.Data.resetOnNGPlus);

		return App.Update.backwardsCompatibility();
	}
);


code:
new App.DomPassage("BirthStorm",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Slave Interact";

		return birth(getSlave(V.AS), {birthStorm: true});
	}
);

new App.DomPassage("csec",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Slave Interact";

		return birth(getSlave(V.AS), {cSection: true});
	}
);

new App.DomPassage("Surrogacy",
	() => {
		V.nextButton = "Continue";

		return App.UI.surrogacy();
	}
);


code:
new App.DomPassage("Costs Budget",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		return App.Budget.costs();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Rep Budget",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		return App.Budget.rep();
	}, ["jump-to-safe", "jump-from-safe"]
);


code:
new App.DomPassage("Slave Assignments Report",
	() => {
		V.nextLink = "Economics";
		V.nextButton = "Continue";

		const f = document.createDocumentFragment();

		try {
			App.UI.DOM.appendNewElement("h1", f, `${V.arcologies[0].name} Weekly Slave Report - Week ${V.week}`);
			f.append(App.EndWeek.slaveAssignmentReport());
		} finally {
			// whatever happens, stop the end week animation so we at least know there is an error.
			App.UI.EndWeekAnim.end();
		}

		return f;
	}
);

new App.DomPassage("Next Week",
	() => {
		App.EndWeek.nextWeek();

		// simulate <<goto "Main">> macro behaviour
		// @ts-ignore
		setTimeout(() => Engine.play("Main"), Engine.minDomActionDelay);

		return document.createDocumentFragment();
	}
);

new App.DomPassage("Economics",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Scheduled Event";

		return App.EndWeek.economics();
	}
);

new App.DomPassage("Security Force Naming-Colonel",
	() => {
		V.nextButton = " ";
		return App.UI.securityForceNamingColonel();
	}
);


code:
/* ### Non Random Events ### */

new App.DomPassage("conflictReport",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Scheduled Event";
		V.encyclopedia = "Battles";
		return App.Events.conflictReport();
	}
);
new App.DomPassage("conflictHandler",
	() => {
		return App.Events.conflictHandler();
	}
);

/* ### Random Events ### */

new App.DomPassage("Random Individual Event",
	() => {
		V.nextButton = "Continue";

		if (V.RIERemaining <= 0) {
			// first event for this week: reset counter
			V.RIERemaining = Math.max(1, Math.min(V.RIEPerWeek, Math.floor(getRieEligibleSlaves().length / 2)));
		}
		if (V.RIERemaining > 1) {
			// return to self if we have more events to play
			V.nextLink = "Random Individual Event";
		} else {
			// last event for this week: out to Next Week
			V.nextLink = "Next Week";
		}
		V.RIERemaining--;

		return App.Events.playRandomIndividualEvent();
	}
);

new App.DomPassage("Random Nonindividual Event",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Random Individual Event";

		return App.Events.playRandomNonindividualEvent();
	}
);

/* ### Scheduled Events ### */

new App.DomPassage("Scheduled Event",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Scheduled Event"; // return to self; playNonrandomEvent will forward automatically when necessary

		return App.Events.playNonrandomEvent();
	}
);

/* ### Player Events ### */

new App.DomPassage("Gameover",
	() => {
		return App.Events.Gameover();
	}
);


code:
/* ### Standard Facilities ### */
new App.DomPassage("Arcade", () => { return new App.Facilities.Arcade.arcade().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Brothel", () => { return new App.Facilities.Brothel.brothel().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Cellblock", () => { return new App.Facilities.Cellblock.cellblock().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Clinic", () => { return new App.Facilities.Clinic.clinic().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Club", () => { return new App.Facilities.Club.club().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Dairy", () => { return new App.Facilities.Dairy.dairy().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Farmyard", () => { return new App.Facilities.Farmyard.farmyard().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Head Girl Suite", () => { return new App.Facilities.HGSuite.headGirlSuite().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Incubator", () => { return App.UI.incubator(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Master Suite", () => { return new App.Facilities.MasterSuite.masterSuite().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Nursery", () => { return new App.Facilities.Nursery.nursery().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Pit", () => { return new App.Facilities.Pit.pit().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Schoolroom", () => { return new App.Facilities.Schoolroom.schoolroom().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Servants' Quarters", () => { return new App.Facilities.ServantsQuarters.servantsQuarters().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Spa", () => { return new App.Facilities.Spa.spa().render(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Transport Hub", () => { return App.SecExp.transportHub.GUI(); });
new App.DomPassage("Weapons Manufacturing", () => { return App.SecExp.weapManu.GUI(); }, ["jump-to-safe", "jump-from-safe"]);
new App.DomPassage("securityHQ", () => { return App.SecExp.secHub.GUI(); }, ["jump-to-safe", "jump-from-safe"]);
new App.DomPassage("secBarracks", () => { return App.SecExp.barracks.GUI(); }, ["jump-to-safe", "jump-from-safe"]);
new App.DomPassage("riotControlCenter", () => { return App.SecExp.riotCenter.GUI(); }, ["jump-to-safe", "jump-from-safe"]);
new App.DomPassage("propagandaHub", () => { return App.SecExp.propHub.GUI(); }, ["jump-to-safe", "jump-from-safe"]);

/* ### Slave Interact Facilities ### */
new App.DomPassage("Wardrobe",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Manage Penthouse";

		return App.UI.WardrobeShopping();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Salon",
	() => {
		V.nextButton = "Confirm changes";
		V.nextLink = "Slave Interact";
		V.encyclopedia = "The Auto Salon";

		return App.UI.salon(getSlave(V.AS));
	}, ["jump-from-safe"]
);

new App.DomPassage("Body Modification",
	() => {
		V.nextButton = "Confirm changes";
		V.nextLink = "Slave Interact";
		V.encyclopedia = "The Studio";

		return App.UI.bodyModification(getSlave(V.AS));
	}, ["jump-from-safe"]
);

/* ### Special Facilities ### */
new App.DomPassage("Agent Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Neighbor Interact";
		V.encyclopedia = "Agents";

		return App.Facilities.AgentSelect();
	}, ["jump-from-safe"]
);

new App.DomPassage("Attendant Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Spa";
		V.encyclopedia = "Attendant";

		return App.Facilities.AttendantSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("BG Select",
	() => {
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.encyclopedia = "Bodyguard";

		return App.Facilities.BGSelect();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Concubine Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Master Suite";
		V.encyclopedia = "Concubine";

		return App.Facilities.ConcubineSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("DJ Select",
	() => {
		return App.Facilities.DJSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Head Girl Select",
	() => {
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.encyclopedia = "Head Girl";

		return App.Facilities.HGSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Madam Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Brothel";
		V.encyclopedia = "Madam";

		return App.Facilities.MadamSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Milkmaid Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Dairy";
		V.encyclopedia = "Milkmaid";

		return App.Facilities.MilkmaidSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Nurse Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Clinic";
		V.encyclopedia = "Nurse";

		return App.Facilities.NurseSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Recruiter Select",
	() => {
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.encyclopedia = "Recruiter";

		return App.Facilities.RecruiterSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Schoolteacher Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Schoolroom";
		V.encyclopedia = "Schoolteacher";

		return App.Facilities.SchoolTeacherSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Stewardess Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Servants' Quarters";
		V.encyclopedia = "Stewardess";

		return App.Facilities.StewardessSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Wardeness Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Cellblock";
		V.encyclopedia = "Wardeness";

		return App.Facilities.WardenessSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Farmer Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Farmyard";
		V.encyclopedia = "Farmer";

		return App.Facilities.FarmerSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Matron Select",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Nursery";
		V.encyclopedia = "Matron";

		return App.Facilities.MatronSelect();
	}, ["jump-to-safe", "jump-hidden", "jump-from-safe"]
);

new App.DomPassage("Rules Assistant",
	() => {
		const div = document.createElement("div");
		App.RA.options(div);
		return div;
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Rules Assistant Summary",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Rules Assistant";
		V.returnTo = "Rules Assistant";
		return App.RA.summary();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Toy Shop",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";

		return App.UI.toyShop();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Dispensary",
	() => {
		V.nextButton = "Back"; V.nextLink = "Manage Penthouse"; V.encyclopedia = "The Pharmaceutical Fab.";

		return App.UI.dispensary();
	}, ["jump-to-safe", "jump-from-safe"]
);


code:
/* ### Central Slave Interact ### */
new App.DomPassage("Slave Interact",
	() => {
		V.nextButton = "Confirm changes";
		V.nextLink = "Main";

		return App.UI.SlaveInteract.mainPage(getSlave(V.AS));
	}, ["jump-from-safe"]
);

/* ### Single Interaction ### */
new App.DomPassage("SlaveOnSlaveFeeding",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Slave Interact";

		return App.UI.SlaveInteract.slaveOnSlaveFeedingSelection(getSlave(V.AS));
	}
);

new App.DomPassage("KillSlave", () => App.UI.SlaveInteract.killSlave(getSlave(V.AS)));

new App.DomPassage(
	"Slave Slave Swap Workaround",
	() => {
		V.nextButton = "Abort Operation";
		V.nextLink = "Main";
		return bodySwapSelection(getSlave(V.AS));
	}
);

new App.DomPassage(
	"Husk Slave Swap Workaround",
	() => {
		V.nextButton = "Abort Operation";
		if (V.activeSlave.tankBaby !== 3) {
			V.nextLink = "Scheduled Event";
			V.returnTo = "Scheduled Event";
		} else {
			V.nextLink = "Main";
			V.returnTo = "Incubator";
		}
		return huskSwapSelection(V.activeSlave);
	}
);

new App.DomPassage(
	"Incubator Retrieval Workaround",
	() => {
		V.returnTo = "Main";
		V.nextLink = "Incubator";
		V.nextButton = "Continue";
		return App.UI.facilityRetrievalWorkaround("Incubator");
	}
);

new App.DomPassage(
	"Nursery Retrieval Workaround",
	() => {
		V.returnTo = "Main";
		V.nextLink = "Nursery";
		V.nextButton = "Continue";
		return App.UI.facilityRetrievalWorkaround("Nursery");
	}
);

new App.DomPassage(
	"Agent Company",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Neighbor Interact";
		return App.UI.SlaveInteract.agentCompany(getSlave(V.AS));
	}, ["jump-from-safe"]
);

new App.DomPassage(
	"Surgery Degradation",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Remote Surgery";
		return App.UI.SlaveInteract.surgeryDegradation(getSlave(V.AS));
	}, ["jump-from-safe"]
);

new App.DomPassage(
	"Remote Surgery",
	() => {
		V.nextButton = "Confirm changes";
		V.nextLink = "Slave Interact";
		return App.UI.SlaveInteract.remoteSurgery(getSlave(V.AS));
	}, ["jump-from-safe"]
);

new App.DomPassage(
	"Import Slave",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Slave Interact";
		return App.UI.SlaveInteract.importSlave();
	}, ["jump-from-safe"]
);

new App.DomPassage(
	"Export Slave",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Slave Interact";
		return App.UI.SlaveInteract.exportSlave(getSlave(V.AS));
	}, ["jump-from-safe"]
);

new App.DomPassage(
	"Cheat Edit JS",
	() => {
		V.nextButton = " ";
		return App.UI.SlaveInteract.cheatEditSlave(getSlave(V.AS));
	}
);

new App.DomPassage(
	"Cheat Edit JS Apply",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Slave Interact";
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("p", el, `You perform the dark rituals, pray to the dark gods, and sell your soul for the power to change and mold slaves to your will.`);
		App.UI.DOM.appendNewElement("p", el, `This slave has been changed forever and you have lost a bit of your soul, YOU CHEATER!`);
		return el;
	}, ["jump-from-safe"]
);

new App.DomPassage("MpregSelf",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";

		return MpregSelf();
	}
);

new App.DomPassage("BreedingTest",
	() => {
		V.nextButton = "Confirm changes";
		V.nextLink = "Slave Interact";
		return App.UI.DOM.makeElement("div", App.Interact.eliteBreedingExam(getSlave(V.AS)));
	}
);

new App.DomPassage("Aztec Slave Sacrifice Penance",
	() => {
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.encyclopedia = "Aztec Revivalism";
		return App.UI.SlaveInteract.aztecSlaveSacrificePenance(getSlave(V.AS));
	}
);

new App.DomPassage("Aztec Slave Sacrifice Life",
	() => {
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.encyclopedia = "Aztec Revivalism";
		return App.UI.SlaveInteract.aztecSlaveSacrificeLife(getSlave(V.AS));
	}
);

new App.DomPassage("Abort",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Slave Interact";
		return App.Interact.abort(getSlave(V.AS));
	}
);

new App.DomPassage("FAnimalImpreg",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Slave Interact";
		return App.Interact.fAnimalImpreg(getSlave(V.AS));
	}
);

new App.DomPassage("FSlaveSlaveDick",
	() => {
		V.nextLink = "Slave Interact";
		V.nextButton = "Back";
		return App.Interact.fSlaveSlaveDick(getSlave(V.AS));
	}
);

new App.DomPassage("FSlaveSlaveVag",
	() => {
		V.nextLink = "Slave Interact";
		V.nextButton = "Back";
		return App.Interact.fSlaveSlaveVag(getSlave(V.AS));
	}
);

new App.DomPassage("FSlaveSlaveAss",
	() => {
		V.nextLink = "Slave Interact";
		V.nextButton = "Back";
		return App.Interact.fSlaveSlaveAss(getSlave(V.AS));
	}
);

new App.DomPassage("Matchmaking",
	() => {
		if (lastVisited("Child Interact") === 1) {
			V.nextLink = "Incubator";
		} else {
			V.nextLink = "Slave Interact";
		}
		V.nextButton = "Continue";
		return App.Interact.matchmaking(getSlave(V.AS));
	}
);

new App.DomPassage("FSelf",
	() => {
		V.nextLink = "Main";
		V.nextButton = "Back";
		return App.Interact.fSelf();
	}
);

new App.DomPassage("FMarry",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Slave Interact";
		return App.Interact.fMarry(getSlave(V.AS));
	}
);

new App.DomPassage("Discard Confirm",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Slave Interact";
		return App.Interact.discard(getSlave(V.AS));
	}
);

new App.DomPassage("Sell Slave",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Slave Interact";
		return App.Interact.sellSlave(getSlave(V.AS));
	}
);

new App.DomPassage("Brothel Assignment Scene",
	() => {
		return App.Facilities.Brothel.assignmentScene(getSlave(V.AS));
	}
);

new App.DomPassage("Industrial Dairy Assignment Scene",
	() => {
		return App.Facilities.Dairy.industrialAssignmentScene(getSlave(V.AS));
	}
);

new App.DomPassage("Free Range Dairy Assignment Scene",
	() => {
		return App.Facilities.Dairy.freeRangeAssignmentScene(getSlave(V.AS));
	}
);

new App.DomPassage("Personal Attention Select",
	() => {
		return App.UI.Player.personalAttention();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Agent Assignment Scene",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Neighbor Interact";
		return App.UI.SlaveInteract.agentAssignmentScene();
	}
);

new App.DomPassage("Artificial Insemination",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Remote Surgery";
		return App.UI.SlaveInteract.artificialInsemination();
	}
);

new App.DomPassage("Neighbor Interact",
	() => {
		V.nextLink = "Main";
		V.nextButton = "Back";
		return App.Neighbor.Interact();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Slave Slave Swap",
	() => {
		V.nextButton = "Continue";
		return App.UI.SlaveInteract.slaveSlaveSwap();
	}
);

new App.DomPassage("Husk Slave Swap",
	() => {
		V.nextButton = "Continue";
		return App.UI.SlaveInteract.huskSlaveSwap();
	}
);

new App.DomPassage("Prosthetics Configuration",
	() => {
		const slave = getSlave(V.AS);
		return App.UI.prostheticsConfigPassage(slave);
	}
);

new App.DomPassage("Multiple Organ Implant",
	() => {
		return App.UI.multipleOrganImplant();
	}
);

new App.DomPassage("Surgery Death",
	() => {
		V.nextLink = "Main";

		const slave = getSlave(V.AS);
		removeSlave(slave);

		const f = new DocumentFragment();
		App.Events.addParagraph(f, [`${slave.slaveName} <span class="health dec">has died from complications of surgery.</span>`]);
		return f;
	}, ["jump-from-safe"]
);

new App.DomPassage("Walk Past List",
	() => {
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		return App.UI.walkPastAll();
	}
);
new App.DomPassage("Find Slave",
	() => {
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		return App.UI.findSlave();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Analyze Pregnancy",
	() => {
		V.nextButton = "Continue";
		return App.UI.analyzePregnancy();
	}, ["jump-from-safe"]
);

new App.DomPassage("Analyze PC Pregnancy",
	() => {
		V.nextButton = "Continue";
		return App.UI.analyzePCPregnancy();
	}, ["jump-from-safe"]
);

new App.DomPassage("Surrogacy Workaround",
	() => {
		V.nextButton = "Cancel";
		return App.UI.surrogacyWorkaround();
	}
);

new App.DomPassage("Pit Workaround",
	() => {
		V.nextButton = V.pit.slavesFighting.length > 1 ? "Finish" : "Cancel";
		V.nextLink = "Pit";
		return App.Facilities.Pit.workaround();
	}
);

new App.DomPassage("Cloning Workaround",
	() => {
		V.nextButton = "Cancel";
		V.nextLink = "Gene Lab";
		return App.UI.cloningWorkaround();
	}
);

new App.DomPassage("Ova Transplant Workaround",
	() => {
		V.nextButton = "Cancel";
		return App.UI.ovaTransplantWorkaround();
	}
);

new App.DomPassage("Subordinate Targeting",
	() => {
		V.nextButton = "Back";
		V.nextLink = V.returnTo;
		return App.UI.subordinateTargeting();
	}
);


code:
new App.DomPassage("Alpha disclaimer", () => { return App.Intro.alphaDisclaimer(); });

new App.DomPassage("New Game Plus", () => { return App.Intro.newGamePlus(); });

new App.DomPassage("PC Body Intro", () => { return App.Intro.PCBodyIntro(); });

new App.DomPassage("Intro Summary", () => { return App.Intro.summary(); });

new App.DomPassage("Acquisition",
	() => {
		V.encyclopedia = "How to Play";
		return App.Intro.acquisition();
	}
);

new App.DomPassage("Customize Slave Trade", () => { return App.Intro.CustomSlaveTrade(); });

new App.DomPassage("Starting Girls", () => { return App.StartingGirls.passage(); });

new App.DomPassage("Gender Intro", () => { return App.Intro.genderIntro(); });

new App.DomPassage("PC Experience Intro", () => { return App.Intro.PCExperienceIntro(); });

new App.DomPassage("Economy Intro", () => { return App.Intro.economyIntro(); });

new App.DomPassage("Trade Intro", () => { return App.Intro.tradeIntro(); });

new App.DomPassage("Extreme Intro", () => { return App.Intro.extremeIntro(); });

new App.DomPassage("Slave Age Intro", () => { return App.Intro.slaveAgeIntro(); });

new App.DomPassage("PC Rumor Intro", () => { return App.Intro.PCRumorIntro(); });

new App.DomPassage("Terrain Intro", () => { return App.Intro.terrainIntro(); });

new App.DomPassage("Takeover Target", () => { return App.Intro.takeoverTarget(); });

new App.DomPassage("PC Preg Intro", () => { return App.Intro.PCPregIntro(); });

new App.DomPassage("PC Appearance Intro", () => { return App.Intro.PCAppearanceIntro(); });

new App.DomPassage("Location Intro", () => { return App.Intro.locationIntro(); });


code:
new App.DomPassage("Main",
	() => {
		V.nextButton = "END WEEK";
		V.nextLink = "End Week";
		V.encyclopedia = "How to Play";

		return App.MainView.full();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Future Society", () => { return App.UI.fsPassage(); }, ["jump-to-safe", "jump-from-safe"]);

new App.DomPassage("Manage Penthouse",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		V.encyclopedia = "What the Upgrades Do";
		return App.UI.managePenthouse();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Manage Personal Affairs",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		V.encyclopedia = "Being in Charge";
		return App.UI.managePersonalAffairs();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("retire",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		return retireScene(getSlave(V.AS));
	}
);

new App.DomPassage("Change Language",
	() => {
		V.nextButton = "Confirm changes";
		V.nextLink = "Main";
		return App.Arcology.changeLanguage();
	}, ["jump-from-safe"]
);

new App.DomPassage("Neighbor Arcology Cheat",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "MOD_Edit Neighbor Arcology Cheat Datatype Cleanup";
		return App.UI.Cheat.neighbors();
	}
);

new App.DomPassage("Manage Corporation",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		V.encyclopedia = "The Corporation";
		return App.Corporate.manage();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Manage Arcology",
	() => {
		return App.UI.manageArcology();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Policies",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		V.encyclopedia = "Future Societies";
		return App.UI.policies();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Universal Rules",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Manage Penthouse";
		return App.UI.universalRules();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Farmyard Animals",
	() => {
		return App.Facilities.Farmyard.animals();
	}
);

new App.DomPassage("Elective Surgery",
	() => {
		return App.UI.electiveSurgery();
	}
);

new App.DomPassage("Brothel Advertisement",
	() => {
		return App.Facilities.Brothel.ads();
	}, ["jump-from-safe"]
);

new App.DomPassage("Club Advertisement",
	() => {
		return App.Facilities.Club.ads();
	}, ["jump-from-safe"]
);

new App.DomPassage("Coursing Association",
	() => {
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		return App.UI.coursingAssociation();
	}, ["jump-from-safe"]
);

new App.DomPassage("Personal assistant options",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		return App.UI.personalAssistantOptions();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Assistant Appearance Pack Two",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Personal assistant options";
		return App.UI.assistantAppearancePackTwo();
	}
);

new App.DomPassage("The Black Market",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		V.returnTo = "Main";
		V.encyclopedia = "The Black Market";
		return App.UI.theBlackMarket();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("FirebaseTour",
	() => {
		V.nextButton = "Return to Operations";
		V.nextLink = "Firebase";
		return App.UI.firebaseTour();
	}
);

new App.DomPassage("Prosthetic Lab",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Manage Penthouse";
		return App.UI.prostheticLab();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Gene Lab",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Manage Penthouse";
		V.encyclopedia = "The Pharmaceutical Fab.";
		return App.UI.geneLab();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Breeder Proposal",
	() => {
		V.nextButton = " ";
		V.nextLink = "Main";
		return App.UI.breederProposal();
	}
);

new App.DomPassage("Implant Manufactory",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Manage Penthouse";
		V.encyclopedia = "The Pharmaceutical Fab.";
		return App.UI.implantManufactory();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Organ Farm",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Manage Penthouse";
		V.encyclopedia = "The Pharmaceutical Fab.";
		return App.UI.organFarm();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Barracks",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		return App.UI.barracks();
	}
);

new App.DomPassage("editSF",
	() => {
		V.nextButton = `Back to ${V.SF.Lower}'s Firebase`;
		V.nextLink = "Firebase";
		return App.UI.editSF();
	}
);

new App.DomPassage("Firebase",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		return App.UI.FireBase();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("edicts",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		return App.SecExp.edicts();
	}, ["jump-to-safe", "jump-from-safe"]
);


code:
new App.DomPassage("Options",
	() => {
		if (lastVisited("Slave Interact") === 1) {
			V.storedLink = "Slave Interact";
		} else {
			V.storedLink = "Main";
		}

		V.nextButton = "Back";
		V.nextLink = V.storedLink;
		V.encyclopedia = "How to Play";

		return App.UI.optionsPassage();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Description Options",
	() => {
		V.nextButton = "Back";
		if (V.storedLink !== "Slave Interact") {
			if (lastVisited("Slave Interact") === 1) {
				V.storedLink = "Slave Interact";
			} else {
				V.storedLink = "Options";
			}
		}
		V.nextLink = V.storedLink;

		return App.UI.descriptionOptions();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Summary Options",
	() => {
		V.nextButton = "Back";
		if (V.storedLink !== "Slave Interact" && V.storedLink !== "Main") {
			if (lastVisited("Main") === 1) {
				V.storedLink = "Main";
			} else {
				V.storedLink = "Options";
			}
		}
		V.nextLink = V.storedLink;
		V.passageSwitchHandler = App.EventHandlers.optionsChanged;

		return App.UI.summaryOptions();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Hotkey Settings",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";

		return App.UI.Hotkeys.settings();
	}, ["jump-to-safe", "jump-from-safe"]
);

new App.DomPassage("Edit Genetics",
	() => {
		return App.UI.editGenetics();
	}, ["jump-from-safe"]
);

new App.DomPassage("Variable Difference",
	() => {
		V.nextButton = " "; /* disable the nextButton. Forces the user to use the "Go Back" option contained in the function */
		return App.UI.variableDifference();
	}
);

new App.DomPassage("MOD_Edit Arcology Cheat",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "MOD_Edit Arcology Cheat Datatype Cleanup";
		return App.UI.Cheat.arcologyPassage();
	}
);

new App.DomPassage("MOD_Edit Arcology Cheat Datatype Cleanup",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Main";
		return App.UI.Cheat.arcologyCheatDatatypeCleanup();
	}
);

new App.DomPassage("MOD_Edit Neighbor Arcology Cheat Datatype Cleanup",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Main";
		return App.UI.Cheat.neighborArcologyCheatDatatypeCleanup();
	}
);

new App.DomPassage("PCCheatMenu",
	() => {
		V.nextButton = "Apply";
		V.nextLink = "PCCheatMenuCheatDatatypeCleanup";
		V.encyclopedia = "Design Your Master";
		return App.UI.Cheat.PCCheatMenu();
	}
);

new App.DomPassage("PCCheatMenuCheatDatatypeCleanup",
	() => {
		V.nextButton = "Continue";
		V.nextLink = "Manage Personal Affairs";
		return App.UI.Cheat.PCCheatMenuCheatDatatypeCleanup();
	}
);


code:
new App.DomPassage("Market",
	() => {
		const span = App.UI.DOM.makeElement("span", App.Markets[V.market.slaveMarket]());
		span.id = "slave-markets";
		return span;
	}
);

new App.DomPassage("Buy Slaves",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Main";
		V.encyclopedia = "Obtaining Slaves";

		return App.UI.market();
	}, ["jump-from-safe", "jump-to-safe"]
);

new App.DomPassage("Bulk Slave Intro", () => App.Markets.bulkSlaveIntro());

new App.DomPassage("Underperforming Slaves",
	() => {
		V.nextButton = "Back";
		V.nextLink = "Buy Slaves";
		return App.Underperformers.passage();
	}, ["jump-from-safe", "jump-to-safe"]
);