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[]} [notes]
	 * @returns {HTMLDivElement}
	 * @protected
	 */
	_makeInternalUpgrade(name, action, cost, containingBuilding, notes) {
		return makePurchase(name, cost, "capEx", {
			notes,
			handler: () => {
				if (Dialog.isOpen()) {
					Dialog.close();
				}
				if (action) {
					action();
					V.building.refresh();
				}
				Dialog.setup(this.name);
				$(Dialog.body()).empty().append(this.cellPassage(containingBuilding));
				Dialog.open();
			},
		});
	}

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

		div.append(makePurchase(name, cost, "capEx", {
			notes,
			handler: () => {
				action();
				Dialog.close();
				Engine.play(passage);
			}
		}));

		return div;
	}

	/**
	 * @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"; }
};

repo_name: Fanatey/fc

license: mit


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 && slave.skill[this.desc.skill] >= V.masteredXP) || (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 occupancy = this.facility.hostedSlaves;
		const maximum = this.expand.maximum || Number.MAX_SAFE_INTEGER;
		const amount = this.expand.amount;
		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.`;
		let cost = this.expand.cost || capacity * 1000 * V.upgradeMultiplierArcology;

		div.append(desc);
		if (!this.expand.unexpandable) {
			if (this.expand.maximum) {
				App.UI.DOM.appendNewElement("div", div, `${this.facility.nameCaps} can support a maximum of ${num(maximum)} slaves.`);
			}
			if (capacity < maximum) {
				if (amount) {
					App.UI.DOM.appendNewElement("div", div,
						makePurchase(`Expand slave capacity.`, cost, "capEx", {
							notes: [`increases the capacity of ${this.facility.name} by ${num(amount)}.`],
							handler: () => { expandFacility(amount, cost); }
						})
					);
				} else {
					const link = document.createElement("span");
					const baseCost = cost;
					const array = [];
					for (const expand of [10, 25, 50, 100].filter(s => capacity - occupancy >= s)) {
						cost = baseCost * (expand / 5);
						array.push(
							App.UI.DOM.link(`x${expand}`, () => { expandFacility(expand, cost); }, [], '', `An additional ${num(expand)} slots will cost ${cashFormat(Math.trunc(cost))}.`)
						);
					}
					link.append(App.UI.DOM.generateLinksStrip(array));

					const linkArray = [];
					App.UI.DOM.appendNewElement("div", div);
					div.append(`Expanding ${this.facility.name} by ${num(5)} slots will cost ${cashFormat(Math.trunc(baseCost))}: `);
					linkArray.push(App.UI.DOM.link(`x5`, () => { expandFacility(5, baseCost); }));
					if (capacity - occupancy >= 10) {
						linkArray.push(App.UI.DOM.linkReplace(`Additional options`, link));
					}
					div.append(App.UI.DOM.generateLinksStrip(linkArray));
				}
			}
		}

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

		const expandFacility = (num, price) => {
			if (typeof V[this.facility.desc.baseName] === 'object') {		// TODO: change this once all facilities are objects
				V[this.facility.desc.baseName].capacity += num;
			} else {
				V[this.facility.desc.baseName] += num;
			}
			V.PC.skill.engineering += 0.1;
			cashX(-price, "capEx");

			App.UI.reload();
		};

		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 === true))) {
			App.UI.DOM.appendNewElement("h2", div, `Rules`);

			this._rules.forEach(rule => {
				if (rule.prereqs.every(prereq => prereq === true)) {
					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 === true)) {
							option.addValue(o.link, o.value);
							if (o.handler) {
								option.addCallback(o.handler);
							}
							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(), ['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 {Array<HTMLDivElement|DocumentFragment>}
	 */
	 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";

		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();
	}
);


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.Mods.SecExp.transportHub.GUI(); });
new App.DomPassage("Weapons Manufacturing", () => { return App.Mods.SecExp.weapManu.GUI(); }, ["jump-to-safe", "jump-from-safe"]);
new App.DomPassage("securityHQ", () => { return App.Mods.SecExp.secHub.GUI(); }, ["jump-to-safe", "jump-from-safe"]);
new App.DomPassage("secBarracks", () => { return App.Mods.SecExp.barracks.GUI(); }, ["jump-to-safe", "jump-from-safe"]);
new App.DomPassage("riotControlCenter", () => { return App.Mods.SecExp.riotCenter.GUI(); }, ["jump-to-safe", "jump-from-safe"]);
new App.DomPassage("propagandaHub", () => { return App.Mods.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("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();
	}
);

new App.DomPassage("Dinner Party Preparations",
() => {
		V.nextButton = "Cancel The Event";
  V.nextLink = "Main";
		return App.Mods.DinnerParty.Prep();
}
);

new App.DomPassage("Dinner Party Execution",
() => {
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.encyclopedia = "Personal Assistant";
		return App.Mods.DinnerParty.Execution();
}
);


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("Doctor Consultation",
	() => {
		return App.UI.doctorConsultation();
	}
);

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.blackMarket();
	}, ["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.Mods.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") {
			V.storedLink = lastVisited("Slave Interact") === 1 ? "Slave Interact" : "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") {
			V.storedLink = lastVisited("Main") === 1 ? "Main" : "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"]
);


code:
App.Corporate.Shared.averageRange = class {
	constructor({center, range}) {
		this._const = {
			center,
			range
		};
	}

	get center() { return this._const.center; }

	get range() { return this._const.range; }

	roll() {
		let roll = Math.clamp(gaussianPair(0, 0.2)[0], -0.5, 0.5);
		return {roll, value: (roll * this.range) + this.center};
	}

	rollInt() {
		let retval = this.roll();
		retval.value = Math.trunc(retval.value);
		return retval;
	}
};

App.Corporate.Shared.RelatedDivisionType = class {
	constructor() {
		this._var = {
			to: [],
			from: [],
			all: []
		};
	}

	get to() { return this._var.to; }

	get from() { return this._var.from; }

	get all() { return this._var.all; }

	addTo(value) {
		this._var.to.push(value);
		this._var.all.push(value);
	}

	addFrom(value) {
		this._var.from.push(value);
		this._var.all.push(value);
	}

	get anyFounded() { return this.all.some(div => div.founded); }
};
App.Corporate.Shared.FoundingType = class {
	constructor(division, {corporateCash, startingSize = 10}) {
		this._const = {
			division,
			cash: corporateCash,
			size: startingSize
		};
	}

	get cash() { return this._const.cash; }

	get size() { return this._const.size; }

	get startingPrice() {
		let div = this._const.division;
		return this._const.cash + div.foundingCost;
	}
};
App.Corporate.Shared.SellOverflowSlaves = function(division) {
	const slavesToSell = division.activeSlaves - division.developmentCount;
	if (slavesToSell > 0) {
		const slaveProcCost = Math.trunc(App.Corporate.slaveMarketPurchaseValue(division, -slavesToSell));
		App.Corporate.chargeAsset(slaveProcCost, "slaves");
		division.activeSlaves -= slavesToSell;
		V.menialDemandFactor -= slavesToSell;
	}
};
App.Corporate.Shared.SellUnhousedSlaves = function(division, divLedger, rate) {
	if (divLedger.market.sell != 0) {
		return;
	}

	let housing = Math.trunc(2 * rate * division.developmentCount);
	let unhoused = division.heldSlaves - housing - divLedger.transfer.total;
	if (unhoused <= 0) {
		return;
	}

	divLedger.market.sell = unhoused;
};
App.Corporate.Shared.MessageProcessedSlaves = function(division, verbPhrase, color) {
	let procCount = Math.trunc(division.developmentCount * division.processRate);
	let slaveCountedNoun = numberWithPluralNonZero(procCount, "slave");

	return `It ${verbPhrase} approximately <span class="${color}">${slaveCountedNoun}</span> each week when operating at capacity (${division.developmentCount})`;
};
App.Corporate.Shared.MessageSlaveToMarket = function(division) {
	return `The slaves from this division can be sold for <span class='yellowgreen'>${cashFormat(Math.trunc(division.soldSlaveValue * menialSlaveCost()))}</span> each.`;
};
App.Corporate.Shared.EndWeekProcessing_Slaves = function(processingCount, rate) {
	const perDevPair = rate.roll();
	let slaveIncrease = perDevPair.value * processingCount;
	if (slaveIncrease < 1) {
		slaveIncrease = (slaveIncrease > Math.random() ? 1 : 0);
	}
	return {efficiency: perDevPair.roll, value: Math.trunc(slaveIncrease)};
};
App.Corporate.Shared.FoundingSetupAutoBuy = function(division) {
	let foundedFrom = division.relatedDivisions.from.filter(div => div.founded);
	if (foundedFrom.length === 0) {
		division.setAutoBuyFromMarket(true);
	} else {
		for (let otherDiv of foundedFrom) {
			if (otherDiv.getAutoSendToMarket()) {
				otherDiv.setAutoSendToDivision(division, true);
			}
		}
	}
};
App.Corporate.Shared.FoundingSetupAutoSell = function(division) {
	let foundedTo = division.relatedDivisions.to.filter(div => div.founded);
	if (foundedTo.length === 0) {
		division.setAutoSendToMarket(true);
	} else {
		for (let otherDiv of foundedTo) {
			if (otherDiv.getAutoBuyFromMarket()) {
				division.setAutoSendToDivision(otherDiv, true);
			}
		}
	}
};

App.Corporate.Division.Base = class {
	constructor({id, name, focusDescription, sizeCost, maintenance, founding, merger}) {
		this._const = {};
		this._const.id = id;
		this._const.corpId = `Div${capFirstChar(id)}`;
		this._const.cost = sizeCost;
		this._const.name = name;
		this._const.focusDescription = focusDescription;
		this._const.maintenance = {
			quadratic: maintenance.quadratic,
			linear: maintenance.linear,
			category: maintenance.category
		};
		if (founding != null) {
			this._const.founding = new App.Corporate.Shared.FoundingType(this, founding);
		}
		if (merger != null && _.isObject(merger) || (Array.isArray(merger) && merger.length > 0)) {
			if (!Array.isArray(merger)) {
				merger = [merger];
			}
			this._const.merger = merger;
		}
		this.relatedDivisions = new App.Corporate.Shared.RelatedDivisionType();
	}

	// initialized data
	get id() { return this._const.id; }

	get sizeCost() { return this._const.cost; }

	get name() { return this._const.name; }

	get focusDescription() { return this._const.focusDescription; }

	get founding() { return this._const.founding; }

	get nextDivisions() { return null; }

	get hasMergers() { return this._const.merger != null; }

	get mergerChoices() { return this._const.merger; }

	get maintenanceCategory() { return this._const.maintenance.category; }

	// stored variables
	get founded() { return this.getStored('') == 1; }

	get foundedDate() { return this.getStored('Founded'); }

	get developmentCount() { return this.getStored("Dev"); }

	set developmentCount(value) {
		if (value < 0) {
			throw Error("Cannot set development count to less than 0");
		}
		// dissolve is the only function that sets founded to false.
		if (value === 0 && this.founded) {
			throw Error("Cannot set development count to 0; use dissolve instead.");
		}
		this.setStored("Dev", value);
	}

	// calculated
	get availableRoom() { return Math.max(0, this.developmentCount - this.activeSlaves); }

	get maintenanceCost() {
		return Math.trunc(this._const.maintenance.linear * 1000 * this.developmentCount + this._const.maintenance.quadratic * Math.pow(this.activeSlaves, 2));
	}

	get foundingCostDivision() { return this._const.founding.size * this.sizeCost; }

	get foundingCost() { return this.foundingCostDivision; }

	get canFoundCorporation() { return this._const.founding != null; }

	get foundingCash() {
		if (!this.canFoundCorporation) {
			throw Error(`${this.name} is not set up found a corporation`);
		}
		return 1000 * this._const.founding.startingPrice;
	}

	get value() {
		const developmentValue = this.developmentCount * this.sizeCost * 800;
		let slaveProcValue = 0;
		let slaveHeldValue = 0;

		if (this.activeSlaves > 0) {
			slaveProcValue = this.activeSlaves * this.purchasedSlaveValue * 1000;
		}
		if (this.heldSlaves > 0) {
			slaveHeldValue = this.heldSlaves * this.soldSlaveValue * 1000;
		}

		return developmentValue + slaveProcValue + slaveHeldValue;
	}

	/**
	 * @abstract
	 * @returns {boolean}
	 */
	get fromMarket() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {boolean}
	 */
	get toMarket() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {number}
	 */
	get heldSlaves() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @param {number} value
	 */
	set heldSlaves(value) { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {number}
	 */
	get activeSlaves() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @param {number} value
	 */
	set activeSlaves(value) { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {number}
	 */
	get processRate() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {number}
	 */
	get initialSlaveValue() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {number}
	 */
	get soldSlaveValue() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {string}
	 */
	get slaveAction() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {string}
	 */
	get nounFinishedSlave() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {string}
	 */
	get nounSlaveFromMarket() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {string}
	 */
	messageSlaveCount() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @returns {string}
	 */
	messageSlaveOutput() { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @param {object} divLedger
	 * @returns {string}
	 */
	message_endWeek_Slaves(divLedger) { throw Error("Must be defined"); }

	/**
	 * @abstract
	 * @param {object} ledger
	 * @param {object} divLedger
	 * @returns {string}
	 */
	endWeek_Slaves(ledger, divLedger) { throw Error("Must be defined"); }

	/**
	 * The value of a slave ignoring enhancements from founded divisions
	 * The actual value of a slave after all improvements
	 * @returns {number}
	 */
	get purchasedSlaveValue() {
		let cheapest = {'value': Number.MAX_VALUE, 'div': null};
		let expensive = {'value': 0, 'div': null};
		for (let fromDiv of this.relatedDivisions.from) {
			let initialValue = fromDiv.initialSlaveValue;
			if (initialValue < cheapest.value) {
				cheapest.value = initialValue;
				cheapest.div = fromDiv;
			}
			if (fromDiv.founded) {
				let soldValue = fromDiv.soldSlaveValue;
				if (soldValue > expensive.value) {
					expensive.value = soldValue;
					expensive.div = fromDiv;
				}
			}
		}
		if (expensive.div != null && expensive.value != cheapest.value) {
			// The added value of an owned intermediary takes time to work its way through this division to the next
			let expensiveDiv = expensive.div;
			let valueDiff = expensive.value - cheapest.value;
			let weeksSinceFounding = V.week - (expensiveDiv.foundedDate || 0);
			let weeksToProcess = 10 * expensiveDiv.processRate;
			let multiplier = Math.min(weeksSinceFounding / weeksToProcess, 1);
			let finalAddedValue = valueDiff * multiplier;
			return cheapest.value + finalAddedValue;
		} else if (cheapest.div != null) {
			return cheapest.value;
		}
		throw Error("No route to acquisition found.");
	}

	/**
	 * @returns {number}
	 */
	get maintenanceSlaves() { return this.activeSlaves * this.processRate; }

	/**
	 * @returns {{cost: number, perUnit: number}}
	 */
	getDisplayMaintenanceCost() {
		const cost = this.maintenanceCost;
		const processedCount = this.maintenanceSlaves;

		return {cost, perUnit: cost / processedCount};
	}

	/**
	 * @param {App.Corporate.Division.Base} division
	 * @returns {boolean}
	 */
	getAutoSendToDivision(division) {
		return (!App.Corporate.ownsIntermediaryDivision(this, division) && this.getStored(`To${division.id}`) === 1);
	}

	/**
	 * @param {App.Corporate.Division.Base} division
	 * @param {boolean} value
	 */
	setAutoSendToDivision(division, value) {
		this.setStored(`To${division.id}`, value ? 1 : 0);
	}

	/**
	 * @returns {boolean}
	 */
	getAutoSendToMarket() {
		return !!this.getStored("ToMarket");
	}

	/**
	 * @param {boolean} value
	 */
	setAutoSendToMarket(value) {
		this.setStored("ToMarket", value ? 1 : 0);
	}

	/**
	 * @returns {boolean}
	 */
	getAutoBuyFromMarket() {
		return !!this.getStored("FromMarket");
	}

	/**
	 * @param {boolean} value
	 */
	setAutoBuyFromMarket(value) {
		this.setStored("FromMarket", value ? 1 : 0);
	}

	/**
	 * @param {object} divLedger
	 */
	endweek_Revenue(divLedger) {
		// Unless otherwise specified, divisions don't produce revenue directly.
	}

	/**
	 * @param {object} divLedger
	 */
	endWeek_Transfer(divLedger) {
		let divisions = [];
		for (let otherDiv of this.relatedDivisions.to.filter(div => div.founded && this.getAutoSendToDivision(div))) {
			const otherLedger = divLedger.weekLedger.getDivision(otherDiv);

			const room = otherDiv.availableRoom - otherLedger.transfer.in;
			if (room === 0) {
				continue;
			}
			divisions.push({division: otherDiv, room});
		}
		const fillDivisions = evenFillArray(divisions, this.heldSlaves, pair => pair.room);
		for (const filled of fillDivisions) {
			const division = filled.item.division;
			const value = filled.value;
			divLedger.transfer.addDivision(division, value);
		}
	}

	/**
	 * @param {object} divLedger
	 */
	endWeek_Market(divLedger) {
		if (this.getAutoSendToMarket()) {
			divLedger.market.sell = this.heldSlaves - divLedger.transfer.total;
		}
		if (this.getAutoBuyFromMarket()) {
			divLedger.market.buy = this.availableRoom - divLedger.transfer.in;
		}
		App.Corporate.Shared.SellUnhousedSlaves(this, divLedger, this.processRate);
	}

	create() {
		if (this.founded) {
			throw Error(`${this.name} has already been founded.`);
		}

		App.Corporate.expandedDivision();
		App.Corporate.chargeAsset(this.foundingCostDivision * 1000, "development");
		this.setStored('', 1);
		this.developmentCount = this._const.founding.size;
		this.setStored('Founded', V.week);
	}

	dissolve() {
		this.setStored('', 0);
		App.Corporate.sellDevelopment(this);
		App.Corporate.dissolvedDivision();
		this.relatedDivisions.to.forEach(nextDep => delete V.corp[`${this._const.corpId}To${nextDep.id}`]);
	}

	// private helpers
	/**
	 * @param {string} key
	 * @returns {*}
	 */
	getStored(key) { return V.corp[this._const.corpId + key]; }

	/**
	 * @param {string} key
	 * @param {*} value
	 */
	setStored(key, value) { V.corp[this._const.corpId + key] = value; }
};


code:
App.Corporate.Division.Acquiring = class extends App.Corporate.Division.Base {
	constructor({slaveValue, acquiring, mercenaryHelp, nextDivision}) {
		super(arguments[0]);
		this._const.slaveValue = slaveValue;
		this._const.acquiring = new App.Corporate.Shared.averageRange(acquiring);
		this._const.nextDivisions = nextDivision;
		if (mercenaryHelp != null) {
			this._const.mercenaryHelp = {
				level: mercenaryHelp.level,
				cost: mercenaryHelp.cost
			};
		}
	}

	// abstract virtual definitions
	get fromMarket() { return false; }

	get toMarket() { return true; }

	get heldSlaves() { return this.getStored("Slaves"); }

	set heldSlaves(value) { this.setStored("Slaves", Math.trunc(value)); }

	get activeSlaves() { return 0; }

	set activeSlaves(value) { throw Error("Cannot set active slaves for acquiring divisions"); }

	get processRate() { return this._const.acquiring.center; }

	get purchasedSlaveValue() { return null; }

	get initialSlaveValue() { return this.soldSlaveValue; }

	get soldSlaveValue() { return this._const.slaveValue; }

	get nounFinishedSlave() { return "slave"; }

	get nounSlaveFromMarket() { return "slave"; }

	messageSlaveCount() {
		return `It averages <span class="positive">${numberWithPluralNonZero(this.developmentCount, "new slave")}</span> each week.`;
	}

	messageSlaveOutput() {
		return App.Corporate.Shared.MessageSlaveToMarket(this);
	}

	message_endWeek_Slaves(divLedger) {
		let newSlaves = divLedger.slaves.value;
		if (newSlaves > 0) {
			return `<span class="positive">acquired ${numberWithPlural(newSlaves, "slave")}` +
				(this.hadMercenaryHelp ? " with the help of your mercenaries." : ".");
		} else {
			return `<span class="red">failed to acquire any slaves` +
				(this.hadMercenaryHelp ? " even with the help of your mercenaries." : ".");
		}
	}

	endWeek_Slaves(divLedger) {
		let slaves = App.Corporate.Shared.EndWeekProcessing_Slaves(this.developmentCount, this._const.acquiring);
		this.heldSlaves += slaves.value;
		return divLedger.slaves.apply(slaves);
	}

	// virtual overrides
	get nextDivisions() { return this._const.nextDivisions; }

	dissolve() {
		App.Corporate.sellSlaves(this, this.heldSlaves);
		super.dissolve();
		delete V.corp[`${this._const.corpId}Slaves`];
	}

	getAutoBuyFromMarket() {
		return false;
	}

	setAutoBuyFromMarket(value) {
		throw Error("Acquiring divisions cannot acquire from the market");
	}

	get availableRoom() { return 0; }

	get maintenanceSlaves() { return this.developmentCount; }

	get maintenanceCost() {
		// If it makes sense to have mercenaries help with other types of divisions, this code and the mercenaryHelp property will need to be moved into the super class.
		let baseCost = super.maintenanceCost;

		if (this.hadMercenaryHelp) {
			const mercHelpCost = Math.trunc((V.mercenaries - this._const.mercenaryHelp.level) * this._const.mercenaryHelp.cost * 1000);
			baseCost += mercHelpCost;
		}
		return baseCost;
	}

	create() {
		super.create();
		this.heldSlaves = 0;
		App.Corporate.Shared.FoundingSetupAutoSell(this);
	}

	get hadMercenaryHelp() { return this._const.mercenaryHelp != null && V.mercenariesHelpCorp === 1 && V.mercenaries > this._const.mercenaryHelp.level; }
};


code:
App.Corporate.Division.Processing = class extends App.Corporate.Division.Base {
	constructor({founding, addedValue, processing, nextDivision, slaveProcessType, slaveProcessDescription}) {
		super(arguments[0]);
		this._const.addedValue = addedValue;
		this._const.processing = new App.Corporate.Shared.averageRange(processing);
		this._const.nextDivisions = nextDivision;
		this._const.slaveProcessType = slaveProcessType;
		this._const.slaveProcessDescription = slaveProcessDescription;
	}

	// abstract virtual definitions
	get fromMarket() { return true; }

	get toMarket() { return true; }

	get heldSlaves() { return this.getStored("Slaves2"); }

	set heldSlaves(value) { this.setStored("Slaves2", Math.trunc(value)); }

	get activeSlaves() { return this.getStored("Slaves"); }

	set activeSlaves(value) { this.setStored("Slaves", Math.trunc(value)); }

	get processRate() { return this._const.processing.center; }

	get soldSlaveValue() {
		// TODO: find a way to cache this.
		return this.purchasedSlaveValue + this._const.addedValue;
	}

	get slaveAction() {
		return this._const.slaveProcessDescription;
	}

	get nounFinishedSlave() { return `${this._const.slaveProcessDescription.past} slave`; }

	get nounSlaveFromMarket() { return this._const.slaveProcessDescription.market; }

	messageSlaveCount() {
		return App.Corporate.Shared.MessageProcessedSlaves(this, `can ${this._const.slaveProcessType.present}`, 'green');
	}

	messageSlaveOutput() {
		return App.Corporate.Shared.MessageSlaveToMarket(this);
	}

	message_endWeek_Slaves(divLedger) {
		let newSlaves = divLedger.slaves.value;
		// The division
		let retval = this._const.slaveProcessType.past; // exploited
		if (newSlaves <= 0) {
			retval += " none of its slaves.";
		} else {
			retval += ` <span class="positive">${numberWithPlural(newSlaves, 'slave')}.</span>`;
		}
		retval += " The division ";
		if (this.activeSlaves) {
			retval += `is still ${this._const.slaveProcessDescription.present} ${numberWithPlural(this.activeSlaves, "slave")}.`;
		} else {
			retval += `doesn't have any slaves to ${this._const.slaveProcessDescription.future}.`;
		}
		return retval;
	}

	endWeek_Slaves(divLedger) {
		let slaves = App.Corporate.Shared.EndWeekProcessing_Slaves(this.activeSlaves, this._const.processing);
		this.activeSlaves -= slaves.value;
		this.heldSlaves += slaves.value;
		return divLedger.slaves.apply(slaves);
	}

	get initialSlaveValue() {
		const values = this.relatedDivisions.from.map(fromDiv => fromDiv.initialSlaveValue);
		if (values.length === 0) {
			throw Error("No route to acquisition found.");
		}
		return Math.min(...values) + this._const.addedValue;
	}

	// virtual override
	get nextDivisions() { return this._const.nextDivisions; }

	get developmentCount() { return super.developmentCount; }

	set developmentCount(value) {
		super.developmentCount = value;
		App.Corporate.Shared.SellOverflowSlaves(this);
	}

	dissolve() {
		App.Corporate.sellSlaves(this, this.heldSlaves);
		super.dissolve();
		delete V.corp[this._const.corpId + "Slaves"];
		delete V.corp[this._const.corpId + "Slaves2"];
	}

	get foundingCostSlaves() { return this._const.founding.size * this.purchasedSlaveValue; }

	get foundingCost() { return this.foundingCostDivision + this.foundingCostSlaves; }

	create() {
		super.create();
		App.Corporate.chargeAsset(this.foundingCostSlaves * 1000, "slaves");
		this.activeSlaves = this._const.founding.size;
		this.heldSlaves = 0;
		App.Corporate.Shared.FoundingSetupAutoBuy(this);
		App.Corporate.Shared.FoundingSetupAutoSell(this);
	}
};