code:
/**
 * Returns true if two slaves are allowed to have sex according to the rules.
 * @param {FC.HumanState} slaveA
 * @param {FC.HumanState} slaveB
 * @returns {boolean}
 */
App.Utils.sexAllowed = function sexAllowed(slaveA, slaveB) {
	/* absolutely no sex of any kind with relatives, if incest is prohibited by game rules */
	const related = areRelated(slaveA, slaveB);
	if (V.seeIncest === 0 && related) {
		return false;
	}
	/* check most specific to least specific - master, partner, leader, family, slaves */
	if (slaveA === V.PC) {
		return slaveB.rules.release.master === 1;
	} else if (slaveB === V.PC) {
		return slaveA.rules.release.master === 1;
	} else if (haveRelationshipP(slaveA, slaveB)) {
		return (slaveA.rules.release.partner === 1) && (slaveB.rules.release.partner === 1);
	} else if (App.Utils.isDevelopmentFacilityLeader(slaveA, slaveB)) {
		return slaveA.rules.release.facilityLeader === 1;
	} else if (App.Utils.isDevelopmentFacilityLeader(slaveB, slaveA)) {
		return slaveB.rules.release.facilityLeader === 1;
	} else if (related) {
		return (slaveA.rules.release.family === 1) && (slaveB.rules.release.family === 1);
	} else {
		return (slaveA.rules.release.slaves === 1) && (slaveB.rules.release.slaves === 1);
	}
};

/**
 * Returns true if the second slave ID resolves to a real slave that the first slave can fuck.
 * @param {FC.HumanState} slave
 * @param {number} id - slave ID (player not allowed)
 * @returns {boolean}
 */
App.Utils.sexAllowedByID = function sexAllowedByID(slave, id) {
	if (id > 0) {
		const target = getSlave(id);
		return target && this.sexAllowed(slave, target);
	}
	return false;
};

/**
 * Returns true if a slave has a romantic partner other than the PC who is both willing and allowed to have sex with her.
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.hasPartnerSex = function hasPartnerSex(slave) {
	const hasWillingSlavePartner = (slave.rules.relationship === "permissive") && (slave.relationship >= 3) && (slave.relationshipTarget > 0);
	return hasWillingSlavePartner && this.sexAllowedByID(slave, slave.relationshipTarget);
};

/**
 * Returns true if a slave has a close family member other than the PC who is both willing and allowed to have sex with her.
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.hasFamilySex = function hasFamilySex(slave) {
	if (V.seeIncest === 0 || slave.rules.release.family === 0) {
		return false;
	}
	// check fast cases first
	if (this.sexAllowedByID(slave, slave.mother)) {
		return true;
	}
	if (this.sexAllowedByID(slave, slave.father)) {
		return true;
	}
	// search exhaustively for sisters/daughters only if necessary
	if (slave.sisters + slave.daughters === 0) {
		return false;
	}
	return V.slaves.some(s => areRelated(slave, s) && this.sexAllowed(slave, s));
};

/**
 * Returns true if the slave has any kind of nonassignment sex with someone other than the PC.
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.hasNonassignmentSex = function hasNonassignmentSex(slave) {
	return (slave.rules.release.slaves === 1) || this.hasFamilySex(slave) || this.hasPartnerSex(slave);
};

/**
 * Returns true if there is any restriction at all on how a slave may choose to get off.
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.releaseRestricted = function releaseRestricted(slave) {
	return (slave.rules.release.slaves === 0) || (V.seeIncest && slave.rules.release.family === 0) || (slave.rules.release.masturbation === 0) || (slave.rules.release.partner === 0);
};

/**
 * Returns true if employee is an employee of a development facility, and leader manages that same facility. Non-commutative!
 * @param {App.Entity.SlaveState} employee
 * @param {App.Entity.SlaveState} leader
 * @returns {boolean}
 */
App.Utils.isDevelopmentFacilityLeader = function(employee, leader) {
	if (!App.Data.misc.sexFromDevelopmentLeaders.includes(leader.assignment)) {
		return false;
	}
	try {
		return (leader.ID === App.Utils.jobForAssignment(employee.assignment).facility.manager.currentEmployee.ID);
	} catch {
		return false; // could mean no job for assignment, or facility does not have a manager assigned...doesn't matter, we're only interested in matches
	}
};

/**
 * Returns a short summary of the slave's release rules
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
App.Utils.releaseSummaryShort = function releaseSummaryShort(slave) {
	const rel = slave.rules.release;
	let ret = "";
	if (rel.masturbation === 1) {
		ret += "M";
	}
	if (rel.partner === 1) {
		ret += "P";
	}
	if (rel.family === 1 && V.seeIncest === 1) {
		ret += "F";
	}
	if (rel.slaves === 1) {
		ret += "O";
	}
	if (rel.master === 1) {
		ret += "Y";
	}
	if (ret === "") {
		ret = "None";
	}
	return ret;
};

/**
 * Returns a longer summary of the slave's release rules
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
App.Utils.releaseSummaryLong = function releaseSummaryLong(slave) {
	const rel = slave.rules.release;
	const includeFamily = (rel.family === 1) && (V.seeIncest === 1);

	if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "chastity";
	} else if (rel.masturbation === 1 && rel.partner === 0 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "masturbation only";
	} else if (rel.masturbation === 0 && rel.partner === 1 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "partner only";
	} else if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 1 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "facility leaders only";
	} else if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 0 && includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "family only";
	} else if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 1 && rel.master === 0) {
		return "slaves only";
	} else if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 0 && rel.master === 1) {
		return "you only";
	} else if (rel.slaves === 1) {
		let ret = "permissive";
		let exceptions = [];
		if (rel.partner === 0) {
			exceptions.push("partner");
		}
		if (rel.facilityLeader === 0) {
			exceptions.push("facility leaders");
		}
		if (!includeFamily) {
			exceptions.push("family");
		}
		if (rel.master === 0) {
			exceptions.push("you");
		}
		if (exceptions.length > 0) {
			ret += " except " + exceptions.reduce(function(res, ch, i, arr) { return res + (i === arr.length - 1 ? ' and ' : ', ') + ch; });
		}
		if (rel.masturbation === 0) {
			ret += ", no masturbation";
		}
		return ret;
	} else {
		let permissions = [];
		if (rel.masturbation === 1) {
			permissions.push("masturbation");
		}
		if (rel.partner === 1) {
			permissions.push("partner");
		}
		if (rel.facilityLeader === 1) {
			permissions.push("facility leaders");
		}
		if (includeFamily) {
			permissions.push("family");
		}
		if (rel.master === 1) {
			permissions.push("you");
		}
		if (permissions.length > 0) {
			return permissions.reduce(function(res, ch, i, arr) { return res + (i === arr.length - 1 ? ' and ' : ', ') + ch; });
		} else {
			return "restrictive";
		}
	}
};

/**
 * Returns a description of the slave's release rules
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
App.Desc.releaseDesc = function releaseDesc(slave) {
	const rel = slave.rules.release;
	const includeFamily = (rel.family === 1) && (V.seeIncest === 1);
	const {He, he, his, him} = getPronouns(slave);
	let r = "and ";
	let appendFrequency = false;
	if (rel.masturbation === 0 && rel.partner === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		r += `${he} is to remain completely chaste.`;
	} else if (rel.masturbation === 1 && rel.partner === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		r += `${he} is only allowed to masturbate`;
		if (slave.energy > 95) {
			r += `, which ${he} is constantly doing.`;
		} else {
			r += `.`;
		}
	} else if (rel.masturbation === 0 && rel.partner === 1 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		r += `${he} is not allowed to masturbate or proposition `;
		if (slave.rules.relationship === "permissive" && slave.relationship >= 3) {
			r += `slaves other than ${his} ${relationshipTerm(slave)}.`;
		} else {
			r += `other slaves; ${he} must find sexual release in ${his} duties.`;
		}
	} else if (rel.masturbation === 0 && rel.partner === 0 && includeFamily && rel.slaves === 0 && rel.master === 0) {
		r += `${he} is not allowed to masturbate. ${He} is only allowed to achieve sexual release with close family members,`;
		appendFrequency = true;
	} else if (rel.masturbation === 0 && rel.partner === 0 && !includeFamily && rel.slaves === 0 && rel.master === 1) {
		r += `${he} is not allowed to masturbate. ${He} must find you if ${he} wants sexual release,`;
		appendFrequency = true;
	} else if (rel.slaves === 1) {
		if (rel.masturbation === 0) {
			r += `${he} is not allowed to masturbate, but may `;
		} else {
			r += `${he} is allowed to masturbate. ${He} may also `;
		}
		if (V.universalRulesConsent === 1) {
			r += "proposition other slaves to find sexual release,";
		} else {
			r += "demand sex from other slaves,";
		}

		const exceptPartner = (rel.partner === 0) && (slave.rules.relationship === "permissive") && (slave.relationship >= 3);
		if (exceptPartner && !includeFamily) {
			r += ` except for ${his} ${relationshipTerm(slave)} and close family members,`;
		} else if (exceptPartner) {
			r += ` except for ${his} ${relationshipTerm(slave)},`;
		} else if (!includeFamily) {
			r += ` except for ${his} close family members,`;
		}

		appendFrequency = true;
	} else {
		if (rel.masturbation === 0) {
			r += `${he} is not allowed to masturbate, but may `;
		} else {
			r += `${he} is allowed to masturbate. ${He} may also `;
		}

		const showPartner = (rel.partner === 1) && (slave.rules.relationship === "permissive") && (slave.relationship >= 3);
		if (includeFamily && showPartner) {
			r += `have sex with ${his} ${relationshipTerm(slave)} and close family members,`;
		} else if (showPartner) {
			r += `fuck ${his} ${relationshipTerm(slave)} as much as ${he} wants,`;
		} else if (includeFamily && rel.master === 1) {
			r += `proposition sex from ${his} close family members and you,`;
		} else if (includeFamily) {
			r += `proposition sex from ${his} close family members,`;
		} else { // should mean rel.master === 1
			r += `find you for sexual relief,`;
		}

		appendFrequency = true;
	}
	if (appendFrequency) {
		if ((slave.devotion > 50) || (slave.energy > 95)) {
			r += ` which ${he} is constantly doing.`;
		} else if (slave.devotion > 20) {
			r += ` which ${he} is often willing to do.`;
		} else {
			r += ` which ${he} is rarely willing to do.`;
		}
	}
	if (rel.facilityLeader === 0) {
		r += ` Your slave leaders have been told not to touch ${him}, no matter how much ${he} begs.`;
	} // else case is "default state", no text
	return r;
};

App.Utils.testAllReleaseText = function testAllReleaseText() {
	let slave = new App.Entity.SlaveState();
	slave.rules.relationship = "permissive";
	slave.relationship = 4;
	slave.relationshipTarget = -1;
	let r = "";
	for (let i = 0; i < Math.pow(2, 5); ++i) {
		const bits = i.toString(2).padStart(5, "0");
		let rule = new App.Entity.ReleaseRulesState();
		rule.masturbation = Number(bits[0]);
		rule.partner = Number(bits[1]);
		rule.family = Number(bits[2]);
		rule.slaves = Number(bits[3]);
		rule.master = Number(bits[4]);
		slave.rules.release = rule;

		r += JSON.stringify(rule) + "\n";
		r += App.Utils.releaseSummaryShort(slave) + "\n";
		r += App.Utils.releaseSummaryLong(slave) + "\n";
		r += App.Desc.releaseDesc(slave) + "\n";
	}
	return r;
};


code:
App.Reminders = (function() {
	const displayDiv = document.createElement("div");
	const addDiv = document.createElement("div");
	return {
		add: add,
		list: list,
		fullDisplay: fullDisplay,
		slaveDisplay: slaveDisplay,
		slaveLink: slaveLink,
		dialog: dialog,
	};

	/**
	 * @param {string|Node} message
	 * @param {number} week
	 * @param {string} [category]
	 * @param {number} [slaveID]
	 */
	function add(message, week, category = "manual", slaveID = 0) {
		if (message === "" || message === null) {
			return;
		}
		const entry = {message: message, week: week, category: category};
		if (slaveID) {
			entry.slaveID = slaveID;
		}

		// V.reminders is sorted by week from low to high, we insert at the correct place so it remains sorted.
		const index = V.reminders.findIndex(e => e.week >= week);
		if (index === -1) {
			V.reminders.push(entry);
		} else {
			V.reminders.splice(index, 0, entry);
		}
	}

	/**
	 * @param {Object} [obj]
	 * @param {number} [obj.maxFuture] how far into the future should reminders be displayed.
	 * @param {function(FC.ReminderEntry):boolean} [obj.filter]
	 * @param {boolean} [obj.link] permit passage links
	 * @returns {HTMLSpanElement|DocumentFragment}
	 */
	function list({maxFuture = Number.POSITIVE_INFINITY, filter = null, link = false} = {}) {
		if (V.reminders.length === 0) {
			return document.createDocumentFragment();
		}

		if (!filter) {
			filter = () => true;
		}

		const outerSpan = document.createElement("span");

		function replace() {
			App.UI.DOM.replace(outerSpan, list({maxFuture, filter, link}));
			App.Utils.scheduleSidebarRefresh();
		}

		/**
		 * @param {FC.ReminderEntry} entry
		 */
		function clearEntry(entry) {
			V.reminders.splice(V.reminders.indexOf(entry), 1);
			replace();
		}

		// We only want to remove visible entries
		function clearOverdue() {
			V.reminders = V.reminders.filter(e => e.week >= V.week || e.week > V.week + maxFuture || !filter(e));
			replace();
		}

		function clearAll() {
			V.reminders = V.reminders.filter(e => e.week > V.week + maxFuture || !filter(e));
			replace();
		}

		let overdue = 0;
		let any = false;

		V.reminders.filter(e => e.week <= V.week + maxFuture && filter(e))
			.forEach(entry => {
				any = true;
				let week;
				let classes = []; // has to be an array, because makeElement takes no empty strings, but empty arrays.
				if (entry.week < V.week) {
					classes = ["red"];
					week = `${numberWithPluralOne(-(entry.week - V.week), 'week')} ago`;
					overdue++;
				} else if (entry.week === V.week) {
					classes = ["orange"];
					week = "today";
				} else {
					if (entry.week <= V.week + 5) {
						classes = ["green"];
					}
					week = `in ${numberWithPluralOne(entry.week - V.week, 'week')}`;
				}
				const div = document.createElement("div");
				div.append(entry.message, " ", App.UI.DOM.makeElement("span", week.toString(), classes));
				const slave = getSlave(entry.slaveID);
				if (slave) {
					const slaveName = link
						? App.UI.DOM.passageLink(SlaveFullName(slave), "Slave Interact", () => {
							V.AS = slave.ID;
							if (Dialog.isOpen()) {
								Dialog.close();
							}
						})
						: SlaveFullName(slave);
					div.append(" (for ", slaveName, ")");
				}
				div.append(" ", App.UI.DOM.link("Clear", clearEntry, [entry]));
				outerSpan.append(div);
			});

		if (overdue > 0) {
			outerSpan.append(App.UI.DOM.makeElement("div", App.UI.DOM.link("Clear Overdue", clearOverdue)));
		}
		if (any) {
			outerSpan.append(App.UI.DOM.makeElement("div", App.UI.DOM.link("Clear all", clearAll)));
		}

		return outerSpan;
	}

	/**
	 * @param {function():void} refresh
	 * @param {string} [category]
	 * @param {number} [slaveID]
	 * @returns {HTMLDivElement}
	 */
	function addField(refresh, category, slaveID) {
		jQuery(addDiv).empty();

		let entry = "";
		let week = 0;

		addDiv.append(
			App.UI.DOM.makeTextBox("", v => { entry = v; }),
			" in ", App.UI.DOM.makeTextBox(0, v => { week = v; }, true), " weeks.",
			" ", App.UI.DOM.link("Add", () => {
				add(entry, V.week + week, category, slaveID);
				refresh();
				App.Utils.scheduleSidebarRefresh();
				jQuery(addDiv).append(addField(refresh, category, slaveID));
			})
		);

		return addDiv;
	}

	/**
	 * @param {boolean} [link=false] show passage links
	 * @returns {HTMLElement}
	 */
	function fullDisplay(link) {
		jQuery(displayDiv).empty();
		displayDiv.append(App.UI.DOM.makeElement("h2", "Reminders"));

		const listEl = list({link});
		if (listEl !== null) {
			displayDiv.append(App.UI.DOM.makeElement("p", listEl, "indent"));
		}

		displayDiv.append(App.UI.DOM.makeElement("h3", "Add new"));
		displayDiv.append(App.UI.DOM.makeElement("p", addField(() => fullDisplay(link))));

		return displayDiv;
	}

	/**
	 * @param {number} slaveID
	 * @returns {HTMLElement}
	 */
	function slaveDisplay(slaveID) {
		jQuery(displayDiv).empty();
		displayDiv.append(App.UI.DOM.makeElement("h2", `Reminders for ${SlaveFullName(getSlave(slaveID))}`));

		const listEl = list({filter: e => e.slaveID === slaveID});
		if (listEl !== null) {
			displayDiv.append(App.UI.DOM.makeElement("p", listEl, "indent"));
		}

		displayDiv.append(App.UI.DOM.makeElement("h3", "Add new"));
		displayDiv.append(App.UI.DOM.makeElement("p", addField(() => slaveDisplay(slaveID), "slave", slaveID)));

		return displayDiv;
	}

	/**
	 * @param {number} slaveID
	 * @returns {HTMLElement}
	 */
	function slaveLink(slaveID) {
		return App.UI.DOM.link(String.fromCharCode(0x23f0), () => {
			dialog(slaveID);
		});
	}

	/**
	 * @param {number} [slaveID]
	 */
	function dialog(slaveID) {
		if (Dialog.isOpen()) {
			Dialog.close();
		}
		Dialog.setup("Reminders", "reminders");
		const showLinks = Story.lookup("tags", "jump-from-safe").some(x => x.title === passage());
		$(Dialog.body()).empty().append(slaveID ? slaveDisplay(slaveID) : fullDisplay(showLinks));
		Dialog.open();
	}
})();

license: mit


code:
/**
 * Removes slave from the game
 * @param {App.Entity.SlaveState} slave
 */

globalThis.removeSlave = function(slave) {
	const AS_ID = slave.ID;
	let LENGTH = V.slaves.length;
	const INDEX = V.slaveIndices[AS_ID];
	let missing = false;

	WombChangeID(V.PC, AS_ID, V.missingParentID);
	if (V.PC.pregSource === V.missingParentID) {
		missing = true;
	}

	if (V.PC.mother === AS_ID) {
		V.PC.mother = V.missingParentID;
		missing = true;
	}
	if (V.PC.father === AS_ID) {
		V.PC.father = V.missingParentID;
		missing = true;
	}
	if (V.PC.sisters > 0) {
		if (areSisters(V.PC, slave) > 0) {
			V.PC.sisters--;
		}
	}
	if (V.PC.daughters > 0) {
		if (slave.father === -1 || slave.mother === -1) {
			V.PC.daughters--;
		}
	}

	V.favorites.delete(AS_ID);
	V.reminders.deleteWith(r => r.slaveID === AS_ID);

	V.researchLab.tasks = V.researchLab.tasks.filter((t) => t.slaveID !== AS_ID);

	if (INDEX >= 0 && INDEX < LENGTH) {
		if (V.incubator.capacity > 0) {
			V.incubator.tanks.forEach(child => {
				if (AS_ID === child.mother) {
					child.mother = V.missingParentID;
					missing = true;
				}
				if (AS_ID === child.father) {
					child.father = V.missingParentID;
					missing = true;
				}
			});
		}
		if (V.nursery > 0) {
			V.cribs.forEach(child => {
				if (AS_ID === child.mother) {
					child.mother = V.missingParentID;
					missing = true;
				}
				if (AS_ID === child.father) {
					child.father = V.missingParentID;
					missing = true;
				}
			});
		}
		V.slaves.forEach(s => {
			WombChangeID(s, AS_ID, V.missingParentID);
			if (s.pregSource === V.missingParentID) {
				missing = true;
			}
			if (slave.daughters > 0) {
				if (s.mother === AS_ID) {
					s.mother = V.missingParentID;
				}
				if (s.father === AS_ID) {
					s.father = V.missingParentID;
				}
				missing = true;
			}
			if (slave.mother > 0 || slave.father > 0) {
				if (slave.mother === s.ID || slave.father === s.ID) {
					s.daughters--;
				}
			}
			if (slave.sisters > 0) {
				if (areSisters(slave, s) > 0) {
					s.sisters--;
				}
			}
			if (s.cumSource === AS_ID || s.milkSource === AS_ID) {
				deflate(s);
			}
			if (s.ID === slave.relationshipTarget) {
				s.relationship = 0;
				s.relationshipTarget = 0;
			}
			if (s.ID === slave.rivalryTarget) {
				s.rivalry = 0;
				s.rivalryTarget = 0;
			}
			/* moved to saDevotion as a discovery event
				if (s.origBodyOwnerID === AS_ID) {
				s.origBodyOwnerID = 0;
				}
			*/
			if (s.ID === slave.subTarget || slave.subTarget === s.ID) {
				slave.subTarget = 0;
				s.subTarget = 0;
			}

			if (s.partners.has(AS_ID)) {
				missing = true;

				s.partners.delete(AS_ID);
				s.partners.add(V.missingParentID);
			}
		});

		/* remove from Pit fighters list, if needed */
		if (V.pit && V.pit.fighterIDs) {
			V.pit.fighterIDs.delete(AS_ID);
		}

		/* remove from Coursing Association, if needed */
		if (V.LurcherID === AS_ID) {
			V.LurcherID = 0;
		}

		if (V.personalAttention.task === PersonalAttention.TRAINING) {
			V.personalAttention.slaves.deleteWith(s => s.ID === AS_ID);
			if (V.personalAttention.slaves.length === 0) {
				App.PersonalAttention.reset();
			}
		}

		/* Remove from facility array or leadership role, if needed */
		removeJob(slave, slave.assignment);

		if (V.traitor !== 0) {
			missing = true; /* no exceptions, fetus system relies on this */
			if (AS_ID === V.traitor.pregSource) {
				V.traitor.pregSource = 0;
			}
			if (V.traitor.mother === AS_ID) {
				V.traitor.mother = V.missingParentID;
			}
			if (V.traitor.father === AS_ID) {
				V.traitor.father = V.missingParentID;
			}
			if (V.traitor.origBodyOwnerID === AS_ID) {
				V.traitor.origBodyOwnerID = 0;
			}
			if (V.traitor.partners.has(AS_ID)) {
				missing = true;

				V.traitor.partners.delete(AS_ID);
				V.traitor.partners.add(V.missingParentID);
			}
		}
		if (V.boomerangSlave !== 0) {
			missing = true;
			if (AS_ID === V.boomerangSlave.pregSource) {
				V.boomerangSlave.pregSource = 0;
			}
			if (V.boomerangSlave.mother === AS_ID) {
				V.boomerangSlave.mother = V.missingParentID;
			}
			if (V.boomerangSlave.father === AS_ID) {
				V.boomerangSlave.father = V.missingParentID;
			}
			if (V.boomerangSlave.origBodyOwnerID === AS_ID) {
				V.boomerangSlave.origBodyOwnerID = 0;
			}
			if (V.boomerangSlave.partners.has(AS_ID)) {
				missing = true;

				V.boomerangSlave.partners.delete(AS_ID);
				V.boomerangSlave.partners.add(V.missingParentID);
			}
		}

		V.organs.deleteWith(s => s.ID === AS_ID);
		V.completedOrgans.deleteWith(s => s.ID === AS_ID);

		for (let o = 0; o < V.adjustProsthetics.length; o++) {
			if (V.adjustProsthetics[o].slaveID === AS_ID) {
				V.adjustProsthetics.deleteAt(o);
				V.adjustProstheticsCompleted--;
				o--;
			}
		}

		const geneIndex = V.genePool.findIndex(s => s.ID === AS_ID);
		if (geneIndex !== -1) {
			let keep = false;
			if (V.traitor !== 0) {
				if (isImpregnatedBy(V.traitor, slave) || V.traitor.ID === AS_ID) {
					/* did we impregnate the traitor, or are we the traitor? */
					keep = true;
				}
			}
			if (V.boomerangSlave !== 0) {
				if (isImpregnatedBy(V.boomerangSlave, slave) || V.boomerangSlave.ID === AS_ID) {
					/* did we impregnate the boomerang, or are we the boomerang? */
					keep = true;
				}
			}
			if (isImpregnatedBy(V.PC, slave)) {
				/* did we impregnate the PC */
				keep = true;
			}
			if (!keep) {
				/* avoid going through this loop if possible */
				keep = V.slaves.some(slave => {
					/* have we impregnated a slave that is not ourselves? */
					return (slave.ID !== AS_ID && isImpregnatedBy(slave, slave));
				});
			}
			if (!keep) {
				V.genePool.deleteAt(geneIndex);
			}
		}
		Object.values(V.missingTable).forEach(s => {
			if (s.mother === slave.ID || s.father === slave.ID) {
				missing = true;
			}
		});
		if (missing) {
			V.missingTable[V.missingParentID] = {
				slaveName: slave.slaveName,
				slaveSurname: slave.slaveSurname,
				fullName: SlaveFullName(slave),
				dick: slave.dick,
				vagina: slave.vagina,
				ID: V.missingParentID,
				mother: slave.mother,
				father: slave.father,
				inbreedingCoeff: slave.inbreedingCoeff
			};
			if (V.traitor && V.traitor.ID === slave.ID) {
				/* To link developing fetuses to their parent */
				V.traitor.missingParentTag = V.missingParentID;
			} else if (V.boomerangSlave && V.boomerangSlave.ID === slave.ID) {
				V.boomerangSlave.missingParentTag = V.missingParentID;
			}
			Object.values(V.missingTable).forEach(s => {
				if (s.mother === slave.ID) {
					s.mother = V.missingParentID;
				}
				if (s.father === slave.ID) {
					s.father = V.missingParentID;
				}
			});
			V.missingParentID--;
		}

		if (V.assignmentRecords[AS_ID]) {
			delete V.assignmentRecords[AS_ID];
		}

		// remove slaves from V.rulesToApplyOnce if needed
		removeFromRulesToApplyOnce(slave);

		V.slaves.deleteAt(INDEX);
		V.slaveIndices = slaves2indices();
		LENGTH--;
		V.JobIDMap = makeJobIdMap(); /* need to call this once more to update count of resting slaves*/
	}
};


code:
/**
 * @param {App.Entity.SlaveState} slave
 * @param {object[]} rules
 * @returns {boolean}
 */
globalThis.hasSurgeryRule = function(slave, rules) {
	return rules.some(
		rule => ruleApplied(slave, rule));
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {object[]} rules
 * @param {string} what
 * @returns {boolean}
 */
globalThis.hasRuleFor = function(slave, rules, what) {
	return rules.some(
		rule => ruleApplied(slave, rule) && rule[what] !== null);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {object[]} rules
 * @returns {boolean}
 */
globalThis.hasHColorRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "hColor");
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {FC.RA.Rule[]} rules
 * @returns {boolean}
 * */
globalThis.hasHStyleRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "hStyle");
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {FC.RA.Rule[]} rules
 * @returns {boolean}
 * */
globalThis.hasEyeColorRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "eyeColor");
};

/**
 * True, if the slave should be on contraceptives based on applied rules.
 *
 * @param {App.Entity.SlaveState} slave
 * @param {FC.RA.Rule[]} rules
 * @returns {boolean}
 */
globalThis.rulesDemandContraceptives = function(slave, rules) {
	// iterate over rules backwards, as the last rule has the highest priority
	for (let i = 0; i < rules.length; i++) {
		const rule = rules[rules.length - i - 1];
		if (ruleApplied(slave, rule) && rule.set.preg != null) {
			return rule.set.preg;
		}
	}
	return false;
};

/**
 * @param {Array<[FC.RA.RuleSetters, string]>} rules
 * @returns {[FC.RA.RuleSetters,object]}
 */
globalThis.mergeRules = function(rules) {
	if (rules.length === 0) {
		return [App.RA.newRule.setters(), {}];
	}

	const combinedRule = App.RA.newRule.setters();
	const sourceRecord = {};

	rules.forEach(rule => {
		App.RA.ruleDeepAssign(combinedRule, rule[0], sourceRecord, rule[1]);
	});
	return [combinedRule, sourceRecord];
};

/**
 * return if a rule is applied on a slave
 * @param {App.Entity.SlaveState} slave
 * @param {FC.RA.Rule} rule
 * @returns {boolean}
 */
globalThis.ruleApplied = function(slave, rule) {
	return slave.currentRules.includes(rule.ID);
};

/**
 * remove slave from the facility described by the rule
 * @param {App.Entity.SlaveState} slave
 * @param {object} rule
 * @returns {string}
 */
globalThis.RAFacilityRemove = function(slave, rule) {
	let r = "";
	if (!rule.facilityRemove) { return r; }
	if (slave.assignment === rule.setAssignment) {
		const facilityName = App.Utils.jobForAssignment(rule.setAssignment).facility.name;
		r += `<br>${slave.slaveName} has been removed from ${facilityName} and has been assigned to ${rule.removalAssignment}.`;
		assignJob(slave, rule.removalAssignment);
	}
	return r;
};

/**
 * return whether the rule applies to the slave
 * @param {FC.RA.Rule} rule
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean} flag */
globalThis.ruleAppliesP = function(rule, slave) {
	let V = State.variables;
	let cond = rule.condition;

	// Check if slave should be excluded from having rule applied to again
	if (cond.applyRuleOnce) {
		if (!V.rulesToApplyOnce[rule.ID]) {
			V.rulesToApplyOnce[rule.ID] = [];
		}
		if (V.rulesToApplyOnce[rule.ID].includes(slave.ID)) {
			return false;
		}
	} else {
		if (V.rulesToApplyOnce[rule.ID]) {
			delete V.rulesToApplyOnce[rule.ID];
		}
	}

	//  special slaves / specific slaves check
	if (cond.selectedSlaves.length > 0 && !cond.selectedSlaves.includes(slave.ID)) {
		return false;
	} else if (cond.excludedSlaves.includes(slave.ID)) {
		return false;
	}

	// attribute / function check
	if (!App.RA.Activation.evaluate(slave, cond.activation)) {
		return false;
	}

	if (cond.applyRuleOnce) {
		V.rulesToApplyOnce[rule.ID].push(slave.ID);
	}
	// If rule always applies.
	if (cond.applyRuleOnce && !V.rulesToApplyOnce[rule.ID].includes(slave.ID)) {
		V.rulesToApplyOnce[rule.ID].push(slave.ID);
	}

	return true;
};

App.RA.newRule = function() {
	return {
		rule: emptyRule,
		conditions: emptyConditions,
		setters: emptySetters,
		growth: emptyGrowth,
		surgery: emptySurgery
	};

	/** @returns {FC.RA.Rule} */
	function emptyRule() {
		const id = generateNewID();
		return {
			ID: id,
			name: `Rule ${id}`,
			condition: emptyConditions(),
			set: emptySetters()
		};
	}

	/** @returns {FC.RA.RuleConditions} */
	function emptyConditions() {
		return {
			activation: ["devotion", 20, "gt", 1, "and"],
			advancedMode: false,
			selectedSlaves: [],
			excludedSlaves: [],
			applyRuleOnce: false,
		};
	}
	/** @returns {FC.RA.RuleSetters} */
	function emptySetters() {
		return {
			releaseRules: emptyRelease(),
			toyHole: null,
			clitSetting: null,
			clitSettingXY: null,
			clitSettingXX: null,
			clitSettingEnergy: null,
			speechRules: null,
			choosesOwnClothes: null,
			clothes: null,
			collar: null,
			faceAccessory: null,
			mouthAccessory: null,
			shoes: null,
			armAccessory: null,
			legAccessory: null,
			chastityVagina: null,
			chastityAnus: null,
			chastityPenis: null,
			virginAccessory: null,
			vaginaLube: null,
			aVirginAccessory: null,
			vaginalAccessory: null,
			aVirginDickAccessory: null,
			dickAccessory: null,
			bellyAccessory: null,
			aVirginButtplug: null,
			buttplug: null,
			buttplugAttachment: null,
			vaginalAttachment: null,
			iris: null,
			sclera: null,
			pupil: null,
			makeup: null,
			nails: null,
			hColor: null,
			hornColor: null,
			hLength: null,
			haircuts: null,
			hStyle: null,
			eyebrowHColor: null,
			eyebrowHStyle: null,
			eyebrowFullness: null,
			markings: null,
			pubicHColor: null,
			pubicHStyle: null,
			piercing: new App.Entity.completePiercingStateRA(),
			boobsTat: null,
			buttTat: null,
			vaginaTat: null,
			dickTat: null,
			lipsTat: null,
			anusTat: null,
			shouldersTat: null,
			armsTat: null,
			legsTat: null,
			backTat: null,
			stampTat: null,
			birthsTat: null,
			abortionTat: null,
			brandDesign: null,
			brandTarget: null,
			scarTarget: null,
			scarDesign: null,
			curatives: null,
			livingRules: null,
			restRules: null,
			mobilityRules: null,
			relationshipRules: null,
			lactationRules: null,
			standardPunishment: null,
			standardReward: null,
			weight: null,
			diet: null,
			dietCum: null,
			dietMilk: null,
			onDiet: null,
			muscles: null,
			XY: null,
			XX: null,
			gelding: null,
			preg: null,
			abortion: null,
			growth: emptyGrowth(),
			// TODO: rename snake_case to camelCase?
			// eslint-disable-next-line camelcase
			hyper_drugs: 0,
			aphrodisiacs: null,
			autoBrand: 0,
			pornFeed: null,
			pornFameSpending: null,
			dietGrowthSupport: 0,
			eyewear: null,
			earwear: null,
			setAssignment: null,
			pitRules: null,
			facilityRemove: false,
			removalAssignment: Job.REST,
			surgery: emptySurgery(),
			underArmHColor: null,
			underArmHStyle: null,
			drug: null,
			eyes: null,
			pregSpeed: null,
			bellyImplantVol: -1,
			teeth: null,
			label: null,
			removeLabel: null,
			skinColor: null,
			inflationType: null,
			labelTagsClear: null,
			pronoun: null,
		};
	}

	/** @returns {FC.RA.RuleReleaseSetters} */
	function emptyRelease() {
		return {
			masturbation: null,
			partner: null,
			facilityLeader: null,
			family: null,
			slaves: null,
			master: null
		};
	}

	/** @returns {FC.RA.RuleGrowthSetters} */
	function emptyGrowth() {
		return {
			boobs: null,
			butt: null,
			lips: null,
			dick: null,
			balls: null,
			intensity: 0
		};
	}

	/** @returns {FC.RA.RuleSurgerySettings} */
	function emptySurgery() {
		return {
			voice: null,
			eyes: null,
			hears: null,
			smells: null,
			tastes: null,
			lactation: null,
			prostate: null,
			cosmetic: null,
			accent: null,
			shoulders: null,
			shouldersImplant: null,
			boobs: null,
			boobsImplantTypes: null,
			boobsImplantAllowReplacing: true,
			hips: null,
			hipsImplant: null,
			butt: null,
			buttImplantTypes: null,
			buttImplantAllowReplacing: true,
			faceShape: null,
			lips: null,
			holes: null,
			tummy: null,
			hair: null,
			bodyhair: null,
			vasectomy: null,
			earShape: null,
			horn: null,
			bellyImplant: null
		};
	}
}();

/**
 * @returns {FC.RA.Rule}
 */
globalThis.emptyDefaultRule = App.RA.newRule.rule;

/**
 * Creates RA target object used in rules for body properties
 * @param {"=="|">="|"<="|">"|"<"} condition comparison condition.
 * @param {number} val target value
 * @returns {FC.RA.NumericTarget}
 */
App.RA.makeTarget = function(condition, val) {
	return {
		cond: condition,
		val: val
	};
};

/**
 * Shall the current value be increased according to the target and condition
 * @param {number} current
 * @param {FC.RA.NumericTarget} target
 * @param {number} [step=1] change step
 * @returns {boolean}
 */
App.RA.shallGrow = function(current, target, step = 1) {
	return target && (((current + step <= target.val) && (target.cond === '==')) ||
		((current < target.val) && (target.cond === '>=' || target.cond === '>')) ||
		(current === target.val && target.cond === '>'));
};

/**
 * Shall the current value be decreased according to the target and condition
 * @param {number} current
 * @param {FC.RA.NumericTarget} target
 * @param {number} [step=1]
 * @returns {boolean}
 */
App.RA.shallShrink = function(current, target, step = 1) {
	return target && (((current - step >= target.val) && (target.cond === '==')) ||
		((current > target.val) && (target.cond === '<=' || target.cond === '<')) ||
		(current === target.val && target.cond === '<'));
};

/**
 * @template {object} T
 * @param {object} target Rule or rule part
 * @param {T} source Rule or rule part, describes the same part as target
 * @param {object} sourceRecord record what was overwritten. Overwrites are located the same as in target, marked with
 * sourceName. Describes the same rule(part) as target.
 * @param {string} sourceName Name for the source object.
 * @returns {T}
 */
App.RA.ruleDeepAssign = function deepAssign(target, source, sourceRecord, sourceName) {
	function isObject(o) {
		return (o !== undefined && o !== null && typeof o === 'object' && !Array.isArray(o));
	}

	for (const key in source) {
		if (!source.hasOwnProperty(key)) {
			continue;
		}
		if (isObject(source[key])) {
			if (!target.hasOwnProperty(key) || target[key] === null) {
				target[key] = {};
			}
			if (!sourceRecord.hasOwnProperty(key)) {
				sourceRecord[key] = {};
			}
			deepAssign(target[key], source[key], sourceRecord[key], sourceName);
		} else if (key === "label" || key === "removeLabel") {
			if (source[key] != null) {
				if (target[key] != null) {
					target[key] += "|" + source[key];
					sourceRecord[key] += "+" + sourceName;
				} else {
					target[key] = source[key];
					sourceRecord[key] = sourceName;
				}
			}
		} else {
			// A rule overrides any preceding ones if,
			// * there are no preceding ones,
			// * or it sets autoBrand,
			// * or it does not set autoBrand and is not null
			const overrides = (
				target[key] === undefined || target[key] === null ||
				(key === "autoBrand" && source[key]) ||
				(key !== "autoBrand" && source[key] !== null));
			if (overrides) {
				target[key] = source[key];
				if (source[key] !== null) {
					sourceRecord[key] = sourceName;
				}
			}
		}
	}
	return target;
};

globalThis.initRules = function() {
	const rule = emptyDefaultRule();
	rule.name = "Obedient Slaves";
	rule.condition.activation = ["devotion", 20, "gte", 1, "and"];

	V.defaultRules = [rule];
	V.rulesToApplyOnce = {};
};