code:
/*
* <<span>> macro
* A minimal macro which allows to create <span> elements with dynamic IDs.
*
* Usage: <<span $variable>>...<</span>>
*/
Macro.add('span', {
	skipArgs: true,
	tags: null,

	handler() {
		const payload = this.payload[0].contents.replace(/(^\n+|\n+$)/, '');
		let statement = this.args.raw.trim();
		let result;

		if (statement.length === 0) {
			return this.error('invalid syntax, format: <<span id>>');
		}

		try {
			result = Scripting.evalTwineScript(statement);
		} catch (ex) {
			return this.error(`bad evaluation: ${typeof ex === 'object' ? ex.message : ex}`);
		}

		Config.debug && this.debugView.modes({block: true});

		jQuery(`<span id='${String(result)}' />`)
			.wiki(payload)
			.appendTo(this.output);
	}
});

repo_name: IchthysdeKilt/fc

license: mit


code:
/**
 * True if slave has at least one eye
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyEyes = function(slave) {
	return !!slave.eye.right || !!slave.eye.left;
};

/**
 * True if slave has at least one eye that is natural
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyNaturalEyes = function(slave) {
	return getLeftEyeType(slave) === 1 || getRightEyeType(slave) === 1;
};

/**
 * True if slave has at least one eye that is prosthetic (cyber or glass)
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyProstheticEyes = function(slave) {
	return getLeftEyeType(slave) > 1 || getRightEyeType(slave) > 1;
};

/**
 * True if slave has at least one eye that is cybernetic
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyCyberneticEyes = function(slave) {
	return getLeftEyeType(slave) === 3 || getRightEyeType(slave) === 3;
};

/**
 * True if slave has both eyes
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothEyes = function(slave) {
	return !!slave.eye.right && !!slave.eye.left;
};

/**
 * True if slave has both eyes and they are natural
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothNaturalEyes = function(slave) {
	return getLeftEyeType(slave) === 1 && getRightEyeType(slave) === 1;
};

/**
 * True if slave has both eyes and they are prosthetic (cyber or glass)
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothProstheticEyes = function(slave) {
	return getLeftEyeType(slave) > 1 && getRightEyeType(slave) > 1;
};

/**
 * True if slave has both eyes and they are cybernetic
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothCyberneticEyes = function(slave) {
	return getLeftEyeType(slave) === 3 && getRightEyeType(slave) === 3;
};

/**
 * True if slave has left eye
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasLeftEye = function(slave) {
	return !!slave.eye.left;
};
/**
 * True if slave has right eye
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasRightEye = function(slave) {
	return !!slave.eye.right;
};

/**
 * Returns type of the left eye.
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getLeftEyeType = function(slave) {
	if (hasLeftEye(slave)) {
		return slave.eye.left.type;
	} else {
		return 0;
	}
};

/**
 * Returns type of the right eye.
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getRightEyeType = function(slave) {
	if (hasRightEye(slave)) {
		return slave.eye.right.type;
	} else {
		return 0;
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getLeftEyeVision = function(slave) {
	if (hasLeftEye(slave)) {
		return slave.eye.left.vision;
	} else {
		return 0;
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getRightEyeVision = function(slave) {
	if (hasRightEye(slave)) {
		return slave.eye.right.vision;
	} else {
		return 0;
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getBestVision = function(slave) {
	return Math.max(getRightEyeVision(slave), getLeftEyeVision(slave));
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getWorstVision = function(slave) {
	return Math.min(getRightEyeVision(slave), getLeftEyeVision(slave));
};

/**
 *
 * @param {App.Entity.SlaveState} slave
 * @param {number} vision
 * @returns {boolean}
 */
window.anyVisionEquals = function(slave, vision) {
	return getRightEyeVision(slave) === vision || getLeftEyeVision(slave) === vision;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
window.getLeftEyeColor = function(slave) {
	if (hasLeftEye(slave)) {
		return slave.eye.left.iris;
	} else {
		return "empty";
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
window.getRightEyeColor = function(slave) {
	if (hasLeftEye(slave)) {
		return slave.eye.right.iris;
	} else {
		return "empty";
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
window.getLeftEyePupil = function(slave) {
	if (hasLeftEye(slave)) {
		return slave.eye.left.pupil;
	} else {
		return "circular";
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
window.getRightEyePupil = function(slave) {
	if (hasLeftEye(slave)) {
		return slave.eye.right.pupil;
	} else {
		return "circular";
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasVisibleHeterochromia = function(slave) {
	return hasAnyEyes(slave) && getLeftEyeColor(slave) !== getRightEyeColor(slave);
};

/**
 * Gives the genetic color of the specified eye.
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} side
 * @returns {string}
 */
window.getGeneticEyeColor = function(slave, side) {
	if (side !== "left" && side !== "right") { return "ERROR:" + side; }

	if (slave.geneticQuirks.albinism === 2) {
		return slave.albinismOverride.eyeColor;
	} else {
		if (side === "left" && typeof slave.geneticQuirks.heterochromia === "string") {
			return slave.geneticQuirks.heterochromia;
		} else {
			return slave.eye.origColor;
		}
	}
};

/**
 * Counts the number of eyes that are not the genetic color
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getLenseCount = function(slave) {
	let count = 0;

	if (hasRightEye(slave) && getRightEyeColor(slave) !== getGeneticEyeColor(slave, "right")) {
		count++;
	}

	if (hasLeftEye(slave) && getLeftEyeColor(slave) !== getGeneticEyeColor(slave, "left")) {
		count++;
	}

	return count;
};


code:
/* limb checker */

/**
 * True if slave has no limbs, neither natural nor prosthetic
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isAmputee = function(slave) {
	return !(slave.leg.right || slave.leg.left || slave.arm.right || slave.arm.left);
};

/**
 * True if slave has at least one natural limb
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyNaturalLimbs = function(slave) {
	return getLeftArmID(slave) === 1 || getRightArmID(slave) === 1 || getLeftLegID(slave) === 1 || getRightLegID(slave) === 1;
};

/**
 * True if slave has at least one prosthetic limb
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyProstheticLimbs = function(slave) {
	return getLeftArmID(slave) > 1 || getRightArmID(slave) > 1 || getLeftLegID(slave) > 1 || getRightLegID(slave) > 1;
};

/**
 * True if slave has at least one leg
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyLegs = function(slave) {
	return !!slave.leg.right || !!slave.leg.left;
};

/**
 * True if slave has at least one arm
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyArms = function(slave) {
	return !!slave.arm.right || !!slave.arm.left;
};

/**
 * True if slave has at least one leg that is natural
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyNaturalLegs = function(slave) {
	return getLeftLegID(slave) === 1 || getRightLegID(slave) === 1;
};

/**
 * True if slave has at least one arm that is natural
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyNaturalArms = function(slave) {
	return getLeftArmID(slave) === 1 || getRightArmID(slave) === 1;
};

/**
 * True if slave has at least one leg that is prosthetic
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyProstheticLegs = function(slave) {
	return getLeftLegID(slave) > 1 || getRightLegID(slave) > 1;
};

/**
 * True if slave has at least one arm that is prosthetic
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAnyProstheticArms = function(slave) {
	return getLeftArmID(slave) > 1 || getRightArmID(slave) > 1;
};

/**
 * True if slave has both legs
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothLegs = function(slave) {
	return !!slave.leg.right && !!slave.leg.left;
};

/**
 * True if slave has both arms
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothArms = function(slave) {
	return !!slave.arm.right && !!slave.arm.left;
};

/**
 * True if slave has both legs and they are natural
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothNaturalLegs = function(slave) {
	return getLeftLegID(slave) === 1 && getRightLegID(slave) === 1;
};

/**
 * True if slave has both arms and they are natural
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothNaturalArms = function(slave) {
	return getLeftArmID(slave) === 1 && getRightArmID(slave) === 1;
};

/**
 * True if slave has both arms and they are artificial
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothProstheticArms = function(slave) {
	return getLeftArmID(slave) > 1 && getRightArmID(slave) > 1;
};

/**
 * True if slave has both legs and they are artificial
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasBothProstheticLegs = function(slave) {
	return getLeftLegID(slave) > 1 && getRightLegID(slave) > 1;
};

/**
 * True if slave has all limbs
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAllLimbs = function(slave) {
	return hasBothLegs(slave) && hasBothArms(slave);
};

/**
 * True if slave has all limbs and all are natural
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasAllNaturalLimbs = function(slave) {
	return hasBothNaturalLegs(slave) && hasBothNaturalArms(slave);
};

/**
 * True if slave has left arm
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasLeftArm = function(slave) {
	return !!slave.arm.left;
};

/**
 * True if slave has right arm
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasRightArm = function(slave) {
	return !!slave.arm.right;
};

/**
 * True if slave has left leg
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasLeftLeg = function(slave) {
	return !!slave.leg.left;
};

/**
 * True if slave has right leg
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.hasRightLeg = function(slave) {
	return !!slave.leg.right;
};

/**
 * Returns limb ID of the left arm. Uses new IDs.
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getLeftArmID = function(slave) {
	if (hasLeftArm(slave)) {
		return slave.arm.left.type;
	} else {
		return 0;
	}
};

/**
 * Returns limb ID of the right arm. Uses new IDs.
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getRightArmID = function(slave) {
	if (hasRightArm(slave)) {
		return slave.arm.right.type;
	} else {
		return 0;
	}
};

/**
 * Returns limb ID of the left leg. Uses new IDs.
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getLeftLegID = function(slave) {
	if (hasLeftLeg(slave)) {
		return slave.leg.left.type;
	} else {
		return 0;
	}
};

/**
 * Returns limb ID of the right leg. Uses new IDs.
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.getRightLegID = function(slave) {
	if (hasRightLeg(slave)) {
		return slave.leg.right.type;
	} else {
		return 0;
	}
};

/**
 * Returns a very short description of the specified limb ID. Uses new limb IDs.
 *
 * @param {number} id
 * @returns {string}
 */
window.idToDescription = function(id) {
	switch (id) {
		case 0:
			return "amputated";
		case 1:
			return "healthy";
		case 2:
			return "modern prosthetic";
		case 3:
			return "advanced, sex-focused prosthetic";
		case 4:
			return "advanced, beauty-focused prosthetic";
		case 5:
			return "advanced, combat-focused prosthetic";
		case 6:
			return "highly advanced cybernetic";
		default:
			return "unknown id: " + id;
	}
};

/**
 * Returns count of specified limb type. Uses new limb IDs:
 * 0: no limb
 * 1: natural
 * 2: basic
 * 3: sex
 * 4: beauty
 * 5: combat
 * 6: cybernetic
 *
 * Can also be used to check for groups:
 * 101: any limbs, that are not amputated
 * 102: prosthetic limbs off all kind
 * 103: sex-prosthetic
 * 104: beauty-prosthetic
 * 105: combat-prosthetic
 *
 * 103-105 mean the sum of 3-5 and 6 respectfully.
 *
 * No ID means all limbs = 101
 *
 * @param {App.Entity.SlaveState} slave
 * @param {number} [id]
 * @returns {number}
 */
window.getLimbCount = function(slave, id = 101) {
	if (id < 100) {
		let n = 0;
		if (getLeftArmID(slave) === id) {
			n++;
		}
		if (getRightArmID(slave) === id) {
			n++;
		}
		if (getLeftLegID(slave) === id) {
			n++;
		}
		if (getRightLegID(slave) === id) {
			n++;
		}
		return n;
	}
	switch (id) {
		case 101:
			return getLimbCount(slave, 1) + getLimbCount(slave, 102);
		case 102:
			return getLimbCount(slave, 2) + getLimbCount(slave, 3) + getLimbCount(slave, 4) + getLimbCount(slave, 5) + getLimbCount(slave, 6);
		case 103:
			return getLimbCount(slave, 3) + getLimbCount(slave, 6);
		case 104:
			return getLimbCount(slave, 4) + getLimbCount(slave, 6);
		case 105:
			return getLimbCount(slave, 5) + getLimbCount(slave, 6);
	}
	// unknown id defaults to 0
	return 0;
};

/**
 * Returns count of specified leg type. Uses new limb IDs.
 *
 * @param {App.Entity.SlaveState} slave
 * @param {number} id
 * @returns {number}
 */
window.getLegCount = function(slave, id) {
	let n = 0;

	if (getLeftLegID(slave) === id) {
		n++;
	}
	if (getRightLegID(slave) === id) {
		n++;
	}

	return n;
};

/**
 * Returns count of specified arm type. Uses new limb IDs.
 *
 * @param {App.Entity.SlaveState} slave
 * @param {number} id
 * @returns {number}
 */
window.getArmCount = function(slave, id) {
	let n = 0;

	if (getLeftArmID(slave) === id) {
		n++;
	}
	if (getRightArmID(slave) === id) {
		n++;
	}

	return n;
};

/**
 * Returns a string depending on the limbs a slave has.
 * By default a variation of "arms and legs", but this can be changed via parameters.
 * Expects the slave to at least have one limb.
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} [arms]
 * @param {string} [arm]
 * @param {string} [legs]
 * @param {string} [leg]
 */
window.armsAndLegs = function(slave, arms = "arms", arm = "arm", legs = "legs", leg = "leg") {
	let r = "";
	if (hasAnyArms(slave)) {
		if (hasBothArms(slave)) {
			r += arms;
		} else {
			r += arm;
		}
		if (hasAnyLegs(slave)) {
			r += " and ";
		}
	}

	if (hasBothLegs(slave)) {
		r += legs;
	} else if (hasAnyLegs(slave)) {
		r += leg;
	}

	return r;
};


code:
/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isSexuallyPure = function(slave) {
	if (!slave) {
		return null;
	}
	return (slave.vagina < 1 && slave.anus < 1 && !slave.counter.anal && !slave.counter.vaginal && !slave.counter.oral && !slave.counter.mammary && !slave.counter.penetrative && !slave.counter.publicUse);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isSlaveAvailable = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.assignment === "be your agent") {
		return false;
	} else if (slave.assignment === "live with your agent") {
		return false;
	} else if (slave.assignment === "be confined in the arcade") {
		return false;
	} else if (slave.assignment === "work in the dairy" && State.variables.dairyRestraintsSetting >= 2) {
		return false;
	}
	return true;
};

window.SlaveStatsChecker = (function() {
	return {
		checkForLisp: hasLisp,
		isModded: isModded,
		isUnmodded: isUnmodded,
		modScore: modScore
	};

	/* call as SlaveStatsChecker.checkForLisp() */
	function hasLisp(slave) {
		if (State.variables.disableLisping === 1 || !canTalk(slave)) {
			return false;
		}
		return (slave.lips > 70 || (slave.lipsPiercing + slave.tonguePiercing > 2) || slave.teeth === "gapped");
	}

	/** call as SlaveStatsChecker.modScore()
	 * @param {App.Entity.SlaveState} slave
	 * @returns {number} // I think
	 */
	function modScore(slave) {
		V.piercingScore = piercingScore(slave);
		V.tatScore = tatScore(slave);
		V.brandScore = brandScore(slave);
		V.scarScore = scarScore(slave);
		return V.tatScore + V.piercingScore + V.brandScore + V.scarScore;
	}

	/**
	 * helper function, not callable
	 * @param {App.Entity.SlaveState} slave
	 * @returns {number}
	 */
	function piercingScore(slave) {
		let score = 0;

		if (slave.earPiercing > 0) {
			score += slave.earPiercing * 0.75 - 0.5;
		}
		if (slave.nosePiercing > 0) {
			score += slave.nosePiercing * 0.75 - 0.5;
		}
		if (slave.eyebrowPiercing > 0) {
			score += slave.eyebrowPiercing * 0.75 - 0.5;
		}
		if (slave.navelPiercing > 0) {
			score += slave.navelPiercing * 0.75 - 0.5;
		}
		if (slave.corsetPiercing > 0) {
			score += slave.corsetPiercing * 0.75 + 0.5;
		}
		if (slave.nipplesPiercing > 0) {
			score += slave.nipplesPiercing * 0.75 - 0.25;
		}
		if (slave.areolaePiercing > 0) {
			score += slave.areolaePiercing * 0.75 + 0.5;
		}
		if (slave.lipsPiercing > 0) {
			score += slave.lipsPiercing * 0.75 - 0.25;
		}
		if (slave.tonguePiercing > 0) {
			score += slave.tonguePiercing * 0.75 - 0.25;
		}
		if (slave.clitPiercing === 3) /* smart piercing */ {
			score += 1.25;
		} else if (slave.clitPiercing > 0) {
			score += slave.clitPiercing * 0.75 - 0.25;
		}

		if (slave.vaginaPiercing > 0) {
			score += slave.vaginaPiercing * 0.75 - 0.25;
		}
		if (slave.dickPiercing > 0) {
			score += slave.dickPiercing * 0.75 - 0.25;
		}
		if (slave.anusPiercing > 0) {
			score += slave.anusPiercing * 0.75 - 0.25;
		}

		return score;
	}

	/**
	 * helper function, not callable
	 * @param {App.Entity.SlaveState} slave
	 * @returns {number}
	 */
	function tatScore(slave) {
		let score = 0;

		if (slave.boobsTat !== 0) {
			score += 1.25;
		}
		if (slave.buttTat !== 0) {
			score += 1.25;
		}
		if (slave.lipsTat !== 0) {
			score += 1.25;
		}
		if (slave.shouldersTat !== 0) {
			score += 1;
		}
		if (slave.backTat !== 0) {
			score += 1.25;
		}
		if (slave.armsTat !== 0) {
			score += 1;
		}
		if (slave.legsTat !== 0) {
			score += 1;
		}
		if (slave.stampTat !== 0) {
			score += 1;
		}
		if (slave.vaginaTat !== 0) {
			score += 1;
		}
		if (slave.dickTat !== 0) {
			score += 1;
		}
		if (slave.bellyTat !== 0) {
			if ((slave.preg > slave.pregData.normalBirth / 1.33 && slave.pregType >= 20) || slave.belly >= 300000) {
				score += 0.75;
			} else if ((slave.preg > slave.pregData.normalBirth / 2 && slave.pregType >= 20) || (slave.preg > slave.pregData.normalBirth / 1.33 && slave.pregType >= 10) || slave.belly >= 150000) {
				score += 1;
			} else if (slave.belly >= 10000 || slave.bellyImplant >= 8000) {
				score += 1;
			} else if ((slave.preg >= slave.pregData.normalBirth / 4 && slave.pregType >= 20) || (slave.preg > slave.pregData.normalBirth / 4 && slave.pregType >= 10) || slave.belly >= 5000) {
				score += 0.5;
			} else if (slave.belly >= 1500) {
				score += 0.25;
			} else {
				score += 0.1;
			}
		}
		if (slave.anusTat === "bleached") {
			score += 0.5;
		} else if (slave.anusTat !== 0) {
			score += 1.25;
		}
		if (slave.abortionTat > 0 || (slave.abortionTat === 0 && slave.pregKnown === 1)) {
			score += 1;
		}
		if (slave.birthsTat > 0 || (slave.birthsTat === 0 && slave.pregKnown === 1)) {
			score += 1;
		}
		return score;
	}

	/**
	 * helper function, not callable
	 * @param {App.Entity.SlaveState} slave
	 * @returns {number}
	 */
	function brandScore(slave) {
		let score = 0;
		score += Object.getOwnPropertyNames(slave.brand).length;
		return score;
	}

	/**
	 * helper function, not callable
	 * @param {App.Entity.SlaveState} slave
	 * @returns {number}
	 */
	function scarScore(slave) {
		let score = 0;
		let scars;
		if (slave.hasOwnProperty("scar")) { /* For very old saves this may not be defined yet and blocks the save from loading. */
			scars = Object.keys(slave.scar);
			for (const bodypart of scars) {
				const bodyparts = Object.keys(slave.scar[bodypart]);
				for (const kind of bodyparts) {
					score += slave.scar[bodypart][kind];
				}
			}
		}
		return score;
	}

	/**
	 * call as SlaveStatsChecker.isModded()
	 * @param {App.Entity.SlaveState} slave
	 * @returns {boolean}
	 */
	function isModded(slave) {
		const tattoos = tatScore(slave);
		const piercings = piercingScore(slave);
		const brands = brandScore(slave);
		const scars = scarScore(slave);
		const mods = piercings + tattoos + scars;

		return (mods > 15 || (piercings > 8 && tattoos > 5) || brands > 1);
	}

	/**
	 * call as SlaveStatsChecker.isUnmodded()
	 * @param {App.Entity.SlaveState} slave
	 * @returns {boolean}
	 */
	function isUnmodded(slave) {
		const tattoos = tatScore(slave);
		const piercings = piercingScore(slave);
		const brands = brandScore(slave);
		const scars = scarScore(slave);

		return (!isModded(slave) && slave.corsetPiercing === 0 && piercings < 3 && tattoos < 2 && brands === 0 && scars <= 1);
	}
}());

/**
 * Returns if slave is considered slim or not by arcology standards.
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isSlim = function(slave) {
	let slim = false;
	const ArcologyZero = State.variables.arcologies[0];

	if (ArcologyZero.FSSlimnessEnthusiastLaw === 1) {
		return (slimLawPass(slave) === 1);
	}
	if ((slave.boobs < 500) && (slave.butt < 3)) {
		if ((slave.muscles <= 30) && (ArcologyZero.FSPhysicalIdealist === "unset") && (slave.weight <= 10) && (ArcologyZero.FSHedonisticDecadence === "unset")) {
			slim = true;
		} else if (ArcologyZero.FSPhysicalIdealist !== "unset") {
			if ((ArcologyZero.FSPhysicalIdealistStrongFat === 1) && (slave.weight <= 30)) {
				slim = true;
			} else if (slave.weight <= 10) {
				slim = true;
			}
		} else if ((ArcologyZero.FSHedonisticDecadence !== "unset") && (slave.weight <= 30)) {
			if (ArcologyZero.FSHedonisticDecadenceStrongFat === 1) {
				slim = true;
			} else if (slave.muscles <= 30) {
				slim = true;
			}
		}
	}
	return slim;
};

/**
 * Returns if slave is considered slim or not by Slimness Enthusiast Law.
 * @param {App.Entity.SlaveState} slave
 * @returns {number} 1: yes, 0: no
 */
window.slimLawPass = function(slave) {
	let slimLawPass = 0;
	const ArcologyZero = V.arcologies[0];

	if (ArcologyZero.FSSlimnessEnthusiastLaw === 1) {
		if ((slave.boobs < 300) && (slave.butt <= 1) && (slave.waist <= 10)) {
			if ((ArcologyZero.FSPhysicalIdealist === "unset") && (ArcologyZero.FSHedonisticDecadenceStrongFat === 0) && (slave.muscles > 30)) {
				/* muscle check*/
				slimLawPass = 0;
			} else if ((ArcologyZero.FSHedonisticDecadence !== "unset") || (ArcologyZero.FSPhysicalIdealistStrongFat === 1)) {
				/* weight check*/
				if (slave.weight > 30) {
					slimLawPass = 0;
				}
			} else if (slave.weight > 10) {
				slimLawPass = 0;
			} else {
				slimLawPass = 1;
			}
		}
	}

	return slimLawPass;
};

/**
 * Returns if slave is considered an acceptable height by arcology standards.
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.heightPass = function(slave) {
	let arcology = V.arcologies[0];

	if (arcology.FSPetiteAdmiration !== "unset") {
		if (arcology.FSPetiteAdmirationLaw2 === 1) {
			if (slave.height < Height.mean(slave) - 5) {
				return true;
			}
		} else {
			if (slave.height < 160) {
				return true;
			}
		}
	} else if (arcology.FSStatuesqueGlorification !== "unset") {
		if (arcology.FSStatuesqueGlorificationLaw2 === 1) {
			if (slave.height + heelLength(slave) > Height.mean(slave) + 5) {
				return true;
			}
		} else {
			if (slave.height + heelLength(slave) >= 170) {
				return true;
			}
		}
	}
	return false;
};

/**
 * Returns the height, in cm, of a slave's heels
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.heelLength = function(slave) {
	switch (slave.shoes) {
		case "pumps":
			// 2 inch heels
			return 5;
		case "platform shoes":
			// 3 inch platform, no heels
			return 8;
		case "heels":
			// 5 inch heels
			return 13;
		case "extreme heels":
			// 8 inch heels
			return 21;
		case "platform heels":
			// 8 inches, but not painful like extremes (3 inch platforms)
			return 21;
		case "extreme platform heels":
			// 12 inches! 8 inch heel, 4 inch platform
			return 30;
	}
	return 0;
};

/**
 * Returns slave bimbo body degree (FSIntellectualDependencyLawBeauty).
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.bimboScore = function(slave) {
	let degree = 0;
	let mods = SlaveStatsChecker.modScore(slave);

	if (slave.lips > 70) {
		degree++;
	}
	if ((slave.lipsImplant / slave.lips) >= 0.60) {
		degree++;
	}
	if (slave.boobs >= 2000) {
		degree++;
		if (slave.boobs >= 10000) {
			degree++;
		}
	}
	if ((slave.boobsImplant / slave.boobs) >= 0.60) {
		degree++;
	}
	if (slave.butt > 4) {
		degree++;
		if (slave.butt > 10) {
			degree++;
		}
	}
	if ((slave.buttImplant / slave.butt) >= 0.60) {
		degree++;
	}
	if (slave.belly >= 1500) {
		degree++;
		if (slave.belly >= 20000) {
			degree++;
		}
	}
	if (slave.waist <= -60) {
		degree++;
	}
	if (slave.hips > 1) {
		degree++;
		if (slave.hips > 2) {
			degree++;
		}
	}
	if (slave.dick > 3 && canPenetrate(slave)) {
		degree++;
		if (slave.balls > 5) {
			degree++;
		}
	}
	if (slave.hLength >= 100) {
		degree++;
	}
	if (slave.makeup > 1 && slave.nails > 1) {
		degree++;
	}
	if (mods >= 10) {
		degree++;
	}
	if (V.piercingScore > 5) {
		degree++;
	}
	if (V.tatScore > 3) {
		degree++;
	}
	if (setup.highHeels.includes(slave.shoes)) {
		degree++;
	}
	if (slave.skin === "sun tanned" || slave.skin === "spray tanned") {
		degree++;
	}
	if (setup.sluttyClothes.includes(slave.clothes)) {
		degree++;
	}

	// degree can go far higher than the max to allow various FS combinations to take advantage.
	return Math.clamp(degree, 0, 6);
};

/**
 * Returns if slave is considered stacked (big T&A) or not.
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isStacked = function(slave) {
	return (slave.butt > 4) && (slave.boobs > 800);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isXY = function(slave) {
	return (slave.dick > 0);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isYoung = function(slave) {
	return (slave.visualAge < 30);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isPreg = function(slave) {
	return ((slave.bellyPreg >= 5000) || (slave.bellyImplant >= 5000));
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isNotPreg = function(slave) {
	return (!isPreg(slave) && (slave.belly < 100) && (slave.weight < 30) && !setup.fakeBellies.includes(slave.bellyAccessory));
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isPure = function(slave) {
	return ((slave.boobsImplant === 0) && (slave.buttImplant === 0) && (slave.waist >= -95) && (slave.lipsImplant === 0) && (slave.faceImplant < 30) && (slave.bellyImplant === -1) && (Math.abs(slave.shouldersImplant) < 2) && (Math.abs(slave.hipsImplant) < 2));
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isSurgicallyImproved = function(slave) {
	return ((slave.boobsImplant > 0) && (slave.buttImplant > 0) && (slave.waist < -10) && (slave.lipsImplant > 0));
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isFullyPotent = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.dick > 0 && slave.balls > 0 && slave.ballType !== "sterile" && slave.hormoneBalance < 100 && slave.drugs !== "hormone blockers") {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canGetPregnant = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.preg === -1) { /* contraceptives check */
		return false;
	} else if (!isFertile(slave)) { /* check other fertility factors */
		return false;
	} else if ((slave.ovaries === 1) && (canDoVaginal(slave))) {
		return true;
	} else if ((slave.mpreg === 1) && (canDoAnal(slave))) {
		/* pregmod */
		return true;
	}
	return false;
};

/** contraceptives (.preg === -1) do not negate this function
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isFertile = function(slave) {
	if (!slave) {
		return null;
	}

	if (slave.womb.length > 0 && slave.geneticQuirks.superfetation < 2) {
		/* currently pregnant without superfetation */
		return false;
	} else if (slave.broodmother > 0) {
		/* currently broodmother */
		return false;
	} else if (slave.preg < -1) {
		/* sterile */
		return false;
	} else if (slave.pregWeek < 0) {
		/* postpartum */
		return false;
	} else if (slave.pubertyXX === 0) {
		/* pregmod start */
		return false;
	} else if (slave.ovaryAge >= 47) {
		return false;
	} else if (slave.inflation > 2) {
		return false;
	} else if (slave.bellyImplant !== -1) {
		return false;
	} else if (slave.mpreg === 1 || slave.ovaries === 1) {
		if (slave.womb.length > 0) { // superfetation route
			if (slave.fertPeak !== 0) {
				return false;
			}
		}
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canAchieveErection = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.dick <= 0) {
		return false;
	} else if (slave.dick < 11 && slave.drugs === "priapism agents") {
		return true;
	} else if (slave.dick > 6) {
		return false;
	} else if (slave.aphrodisiacs > 1 || (slave.inflationType === "aphrodisiac" && slave.inflation >= 2)) {
		return true;
	} else if (slave.ballType === "sterile") {
		return false;
	} else if ((slave.balls > 0 ? slave.hormoneBalance < 100 : slave.hormoneBalance <= -100) && slave.drugs !== "hormone blockers") {
		return true;
	} else if (slave.aphrodisiacs > 0 || slave.inflationType === "aphrodisiac") {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canPenetrate = function(slave) {
	if (!slave) {
		return null;
	} else if (!canAchieveErection(slave)) {
		return false;
	} else if (slave.chastityPenis === 1) {
		return false;
	} else if (slave.dick > 7) {
		return false;
	}
	return true;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canSee = function(slave) {
	if (!slave) {
		return null;
	}
	return (getBestVision(slave)) > 0;
};

/**
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canSeePerfectly = function(slave) {
	if (!canSee(slave)) {
		return false;
	}

	if (getBestVision(slave) === 2 && (slave.eyewear === "blurring glasses") || (slave.eyewear === "blurring contacts")) {
		// could see perfectly, but being blurred
		return false;
	} else if (getBestVision(slave) < 2 && !(slave.eyewear === "corrective glasses" || slave.eyewear === "corrective contacts")) {
		// can't see perfectly and not corrected
		return false;
	}
	return true;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canHear = function(slave) {
	if (!slave) {
		return null;
	}
	return ((slave.hears > -2) && (slave.earwear !== "deafening ear plugs"));
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canSmell = function(slave) {
	if (!slave) {
		return null;
	}
	return (slave.smells > -1);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canTaste = function(slave) {
	if (!slave) {
		return null;
	}
	return (slave.tastes > -1);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canHold = function(slave) {
	if (!slave) {
		return null;
	}
	return hasAnyArms(slave);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canWalk = function(slave) {
	if (!slave) {
		return null;
	} else if (!hasBothLegs(slave)) {
		return false;
	} else if (tooFatSlave(slave)) {
		return false;
	} else if (tooBigBreasts(slave)) {
		return false;
	} else if (tooBigDick(slave)) {
		return false;
	} else if (tooBigBalls(slave)) {
		return false;
	} else if (tooBigButt(slave)) {
		return false;
	} else if (tooBigBelly(slave)) {
		return false;
	} else if (slave.heels === 1 && !setup.highHeels.includes(slave.shoes)) {
		return false;
	}
	return true;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {boolean} checkLanguage Does a bad accent count as being unable to speak?
 * @returns {boolean}
 */
window.canTalk = function(slave, checkLanguage = true) {
	if (!slave) {
		return null;
	} else if (checkLanguage && slave.accent > 2) {
		return false;
	} else if (slave.voice === 0) {
		return false;
	} else if (slave.lips > 95) {
		return false;
	} else if (slave.collar === "dildo gag") {
		return false;
	} else if (slave.collar === "massive dildo gag") {
		return false;
	} else if (slave.collar === "ball gag") {
		return false;
	} else if (slave.collar === "bit gag") {
		return false;
	}
	return true;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canDoAnal = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.chastityAnus === 1) {
		return false;
	}
	return true;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.canDoVaginal = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.vagina < 0) {
		return false;
	} else if (slave.chastityVagina === 1) {
		return false;
	}
	return true;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.tooFatSlave = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.weight > 190 + (slave.muscles / 5) && slave.physicalAge >= 18) {
		return true;
	} else if (slave.weight > 130 + (slave.muscles / 20) && slave.physicalAge <= 3) {
		return true;
	} else if (slave.weight > 160 + (slave.muscles / 15) && slave.physicalAge <= 12) {
		return true;
	} else if (slave.weight > 185 + (slave.muscles / 10) && slave.physicalAge < 18) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.tooBigBreasts = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.boobs > 30000 + (slave.muscles * 100) && slave.physicalAge >= 18) {
		return true;
	} else if (slave.boobs > 5000 + (slave.muscles * 10) && slave.physicalAge <= 3) {
		return true;
	} else if (slave.boobs > 10000 + (slave.muscles * 20) && slave.physicalAge <= 12) {
		return true;
	} else if (slave.boobs > 20000 + (slave.muscles * 50) && slave.physicalAge < 18) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.tooBigBelly = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.belly >= 450000 + (slave.muscles * 2000) && slave.physicalAge >= 18) {
		return true;
	} else if (slave.belly >= 350000 + (slave.muscles * 1000) && slave.physicalAge >= 13) {
		return true;
	} else if (slave.belly >= 30000 + (slave.muscles * 500) && slave.physicalAge <= 3) {
		return true;
	} else if (slave.belly >= 150000 + (slave.muscles * 800) && slave.physicalAge <= 12) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.tooBigBalls = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.balls >= 30 + (slave.muscles * 0.3) && slave.physicalAge <= 3) {
		return true;
	} else if (slave.balls >= 60 + (slave.muscles * 0.5) && slave.physicalAge <= 12) {
		return true;
	} else if (slave.balls >= 90 + (slave.muscles * 0.7)) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.tooBigDick = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.dick >= 20 + (slave.muscles * 0.1) && slave.physicalAge <= 3 && slave.dick !== 0) {
		return true;
	} else if (slave.dick >= 45 + (slave.muscles * 0.3) && slave.physicalAge <= 12) {
		return true;
	} else if (slave.dick >= 68 + (slave.muscles * 0.4)) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.tooBigButt = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.butt > 10 && slave.physicalAge <= 3) {
		return true;
	} else if (slave.butt > 14 && slave.physicalAge <= 12) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.isVegetable = function(slave) {
	if (!slave) {
		return false;
	}
	return (slave.fetish === "mindbroken");
};

/**
 * Returns the hair color the slave was (or would be) born with.
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
window.getGeneticHairColor = function(slave) {
	if (slave.geneticQuirks.albinism === 2) {
		return slave.albinismOverride.hColor;
	}
	return slave.origHColor;
};

/**
 * Returns the skin color the slave was (or would be) born with.
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
window.getGeneticSkinColor = function(slave) {
	if (slave.geneticQuirks.albinism === 2) {
		return slave.albinismOverride.skin;
	}
	return slave.origSkin;
};


code:
/* config.history.tracking = false;*/
// State.expired.disable;

/**
 * @param {number} x
 * @param {number} minValue
 * @param {number} maxValue
 * @param {number} [defaultValue=0]
 * @returns {number}
 */
window.variableAsNumber = function(x, minValue, maxValue, defaultValue = 0) {
	x = Number(x);
	if (isNaN(x)) {
		return defaultValue;
	}
	if (x < minValue) { // Works even if minValue is undefined.
		return minValue;
	}
	if (x > maxValue) { // Works even if maxValue is undefined.
		return maxValue;
	}
	return x;
};

if (typeof interpolate === "undefined") {
	const interpolate = function(x0, y0, x1, y1, x) {
		if (x <= x0) {
			return y0;
		} else if (x >= x1) {
			return y1;
		} else {
			return (x - x0) * ((y1 - y0) / (x1 - x0)) + y0;
		}
	};
	window.interpolate = interpolate;
}

/**
 * @param {any[]} arr
 * @param {any} val
 * @returns {any[]}
 */
window.removeFromArray = function(arr, val) {
	for (let i = 0; i < arr.length; i++) {
		if (val === arr[i]) {
			return arr.splice(i, 1);
		}
	}
	return null;
};

/**
 * @param {any[]} arr
 * @param {any} callback
 * @param {any} thisArg
 * @returns {Array}
 */
window.filterInPlace = function(arr, callback, thisArg) {
	let j = 0;

	arr.forEach(function(e, i) {
		if (callback.call(thisArg, e, i, arr)) {
			arr[j++] = e;
		}
	});

	arr.length = j;
	return arr;
};

/** pregmod: are slave2's sperm compatible with slave1's eggs?
 * @param {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 * @returns {boolean}
 */
window.canBreed = function(slave1, slave2) {
	if (!slave1 || !slave2) {
		return null;
	}
	return (slave1.eggType === slave2.ballType);
};

/** assuming slave1 is fertile, could slave2 impregnate slave1?
 * slave2 must have dick and balls with compatible sperm;
 * both slaves must not be in chastity; slave2 need not achieve erection
 * @param {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 * @returns {boolean}
 */
window.canImpreg = function(slave1, slave2) {
	if (!slave1 || !slave2) {
		return null;
	} else if (slave2.dick < 1) {
		return false;
	} else if (slave2.balls < 1) {
		return false;
	} else if (slave2.chastityPenis === 1) {
		return false;
	} else if (slave2.pubertyXY === 0) {
		/* pregmod start */
		return false;
	} else if (slave2.vasectomy === 1) {
		return false;
	} else if (!canBreed(slave1, slave2)) {
		return false; /* pregmod end */
	} else if (!canGetPregnant(slave1)) {
		/* includes chastity checks */
		return false;
	} else {
		return true;
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string|number}
 */
window.relationTargetWord = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.relation === "daughter") {
		return "mother";
	} else if (slave.relation === "mother") {
		return "daughter";
	}
	return slave.relation;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.milkAmount = function(slave) {
	let milk;
	let calcs;
	if (!slave) {
		return null;
	}
	calcs = slave.boobs - slave.boobsImplant - slave.boobsMilk;
	if (calcs > 40000) {
		milk = (158 + ((calcs - 40000) / 600));
	} else if (calcs > 25000) {
		milk = (128 + ((calcs - 25000) / 500));
	} else if (calcs > 10000) {
		milk = (78 + ((calcs - 10000) / 300));
	} else if (calcs > 5000) {
		milk = (53 + ((calcs - 5000) / 200));
	} else if (calcs > 2000) {
		milk = (29 + ((calcs - 2000) / 125));
	} else if (calcs > 800) {
		milk = (16 + ((calcs - 800) / 80));
	} else {
		milk = (8 + ((calcs - 400) / 50));
	}
	if (slave.boobsImplant > 0) {
		milk *= Math.max(0.25, (1 - (slave.boobsImplant / slave.boobs)));
	}
	if (slave.lactation === 2) {
		milk *= 1.2;
	}
	milk += (milk * ((slave.devotion - 50) / 200));
	calcs = (slave.hormoneBalance / 50);
	if (slave.balls !== 0 && calcs > -2) {
		calcs -= 1;
	} else if (slave.ovaries !== 1 && calcs < 2) {
		calcs += 1;
	}
	milk *= (1 + (calcs * 0.1));
	milk *= (1 + (slave.preg / 100));
	milk *= (1 + (slave.health.condition / 50));
	milk *= (1 + (slave.weight / 500));
	milk *= (1 + (slave.lactationAdaptation / 500));
	milk += (slave.boobsMilk / 100);
	milk *= healthPenalty(slave);
	milk = Math.trunc(milk);
	milk = Math.clamp(milk, 1, 1000000000000000000);
	return milk;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.cumAmount = function(slave) {
	let cum = 0;
	let calcs = 0;
	if (!slave) {
		return null;
	}
	if (slave.drugs === "testicle enhancement") {
		cum = ((slave.balls * 3.5) + 1);
	} else if (slave.drugs === "hyper testicle enhancement") {
		cum = ((slave.balls * 5) + 1);
	} else {
		cum = ((slave.balls * 2.5) + 1);
	}
	if (slave.ballType === "sterile") {
		cum *= 0.8;
	}
	if (slave.diet === "cum production") {
		cum *= 1.2;
	}
	calcs = (slave.hormoneBalance / 50);
	cum *= (1 - (calcs * 0.1));
	if (slave.scrotum === 0) {
		cum *= 0.8;
	}
	if (slave.prostate === 0) {
		cum *= 0.2; // being generous here
	} else if (slave.prostate === 2) {
		cum *= 1.2;
	} else if (slave.prostate === 3) {
		cum *= 1.5;
	}
	if (slave.devotion > 50) {
		cum += (cum * (slave.devotion / 100));
	} else if (slave.devotion < -50) {
		cum += (cum * (slave.devotion / 100));
	}
	if (slave.health.condition > 50) {
		cum += (cum * (slave.health.condition / 50));
	} else if (slave.health.condition < -50) {
		cum += (cum * (slave.health.condition / 50));
	}
	cum = Math.trunc(cum);
	cum = Math.clamp(cum, 1, 1000000000000000000);
	return cum;
};

/**
 * @param {string} text
 * @returns {string}
 */
window.lispReplace = function(text) {
	text = text.replace(/Sh/g, "Th");
	text = text.replace(/SS/g, "Th");
	text = text.replace(/Ss/g, "Th");
	text = text.replace(/sS/g, "Th");
	text = text.replace(/S/g, "Th");
	text = text.replace(/aX/g, "aKTH");
	text = text.replace(/eX/g, "eKTH");
	text = text.replace(/iX/g, "iKTH");
	text = text.replace(/oX/g, "oKTH");
	text = text.replace(/uX/g, "uKTH");
	text = text.replace(/yX/g, "yKTH");
	text = text.replace(/AX/g, "AKTH");
	text = text.replace(/EX/g, "EKTH");
	text = text.replace(/IX/g, "IKTH");
	text = text.replace(/OX/g, "OKTH");
	text = text.replace(/UX/g, "UKTH");
	text = text.replace(/YX/g, "YKTH");
	text = text.replace(/Xa/g, "Tha");
	text = text.replace(/Xe/g, "The");
	text = text.replace(/Xi/g, "Thi");
	text = text.replace(/Xo/g, "Tho");
	text = text.replace(/Xu/g, "Thu");
	text = text.replace(/Xy/g, "Thy");
	text = text.replace(/XA/g, "THA");
	text = text.replace(/XE/g, "THE");
	text = text.replace(/XI/g, "THI");
	text = text.replace(/XO/g, "THO");
	text = text.replace(/XU/g, "THU");
	text = text.replace(/XY/g, "THY");
	text = text.replace(/X/g, "EKTH");
	text = text.replace(/zZ/g, "Th");
	text = text.replace(/Zz/g, "Th");
	text = text.replace(/ZZ/g, "TH");
	text = text.replace(/Z/g, "Th");
	text = text.replace(/Cia/g, "Tha");
	text = text.replace(/Ci/g, "Thi");
	text = text.replace(/Ce/g, "The");
	text = text.replace(/Cy/g, "Thy");
	text = text.replace(/CIA/g, "THA");
	text = text.replace(/CI/g, "THI");
	text = text.replace(/CE/g, "THE");
	text = text.replace(/CY/g, "THY");
	text = text.replace(/ss/g, "th");
	text = text.replace(/sh/g, "th");
	text = text.replace(/s/g, "th");
	text = text.replace(/zz/g, "th");
	text = text.replace(/z/g, "th");
	text = text.replace(/ax/g, "akth");
	text = text.replace(/ex/g, "ekth");
	text = text.replace(/ix/g, "ikth");
	text = text.replace(/ox/g, "okth");
	text = text.replace(/ux/g, "ukth");
	text = text.replace(/yx/g, "ykth");
	text = text.replace(/Ax/g, "Akth");
	text = text.replace(/Ex/g, "Ekth");
	text = text.replace(/Ix/g, "Ikth");
	text = text.replace(/Ox/g, "Okth");
	text = text.replace(/Ux/g, "Ukth");
	text = text.replace(/Yx/g, "Ykth");
	text = text.replace(/xa/g, "tha");
	text = text.replace(/xe/g, "the");
	text = text.replace(/xi/g, "thi");
	text = text.replace(/xo/g, "tho");
	text = text.replace(/xu/g, "thu");
	text = text.replace(/xy/g, "thy");
	text = text.replace(/xA/g, "thA");
	text = text.replace(/xE/g, "thE");
	text = text.replace(/xI/g, "thI");
	text = text.replace(/xO/g, "thO");
	text = text.replace(/xU/g, "thU");
	text = text.replace(/xY/g, "thY");
	text = text.replace(/x/g, "ekth");
	text = text.replace(/cia/g, "tha");
	text = text.replace(/ci/g, "thi");
	text = text.replace(/ce/g, "the");
	text = text.replace(/cy/g, "thy");
	text = text.replace(/cI/g, "thI");
	text = text.replace(/cE/g, "thE");
	text = text.replace(/cY/g, "thY");
	return text;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {Object} arcology
 * @returns {number}
 */
window.repGainSacrifice = function(slave, arcology) {
	if (!slave || !arcology || arcology.FSAztecRevivalist === "unset" || arcology.FSAztecRevivalist <= 0) {
		return 0;
	}
	return Math.ceil(
		(Math.min(100, Math.pow(1.0926, State.variables.week - slave.weekAcquired)) + slave.prestige * 30) * arcology.FSAztecRevivalist / 100 / ((State.variables.slavesSacrificedThisWeek || 0) + 1));
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
window.bodyguardSuccessorEligible = function(slave) {
	if (!slave) {
		return false;
	}
	return (slave.devotion > 50 && slave.muscles >= 0 && slave.weight < 100 && slave.boobs < 8000 && slave.butt < 10 && slave.belly < 5000 && slave.balls < 10 && slave.dick < 10 && slave.preg < 20 && slave.fuckdoll === 0 && slave.fetish !== "mindbroken" && canWalk(slave) && canHold(slave) && canSee(slave) && canHear(slave));
};

window.ngUpdateGenePool = function(genePool = []) {
	const transferredSlaveIds = (State.variables.slaves || [])
		.filter(s => s.ID >= 1200000)
		.map(s => s.ID - 1200000);
	return genePool
		.filter(s => (transferredSlaveIds.includes(s.ID)))
		.map(function(s) {
			const result = jQuery.extend(true, {}, s);
			result.ID += 1200000;
			return result;
		});
};

window.ngUpdateMissingTable = function(missingTable) {
	const newTable = {};

	(State.variables.slaves || [])
		.forEach(s => ([s.pregSource + 1200000, s.mother + 1200000, s.father + 1200000]
			.filter(i => (i in missingTable))
			.forEach(i => {
				newTable[i - 1200000] = missingTable[i];
				newTable[i - 1200000].ID -= 1200000;
			})));

	return newTable;
};

/**
 * @param {any} obj
 * @returns {string}
 */
window.toJson = function(obj) {
	let jsontext = JSON.stringify(obj);
	jsontext = jsontext.replace(/^{/, "");
	jsontext = jsontext.replace(/}$/, "");
	return jsontext;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
window.nippleColor = function(slave) {
	if (skinToneLevel(slave.skin) < 8) {
		if (slave.preg > slave.pregData.normalBirth / 4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "brown";
		} else {
			return "pink";
		}
	} else if (skinToneLevel(slave.skin) < 14) {
		if (slave.preg > slave.pregData.normalBirth / 4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "dark brown";
		} else {
			return "pink";
		}
	} else if (skinToneLevel(slave.skin) > 20) {
		if (slave.preg > slave.pregData.normalBirth / 4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "black";
		} else {
			return "dark brown";
		}
	} else {
		if (slave.preg > slave.pregData.normalBirth / 4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "dark brown";
		} else {
			return "brown";
		}
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {Object} PC
 * @returns {number}
 */
window.overpowerCheck = function(slave, PC) {
	let strength;

	if (State.variables.arcologies[0].FSPhysicalIdealist !== "unset") {
		if (PC.title === 1) {
			strength = 130;
		} else {
			strength = 100;
		}
	} else if (PC.title === 1) {
		strength = 50;
	} else {
		strength = 30;
	}
	strength += (185-slave.height);
	strength -= (PC.belly/1000);
	strength += (PC.skill.warfare/3);
	strength -= (State.variables.PC.health.shortDamage);

	return strength;
};

/**
 * returns array of IDs of all characters who impregnated slave
 * @param {App.Entity.SlaveState} slave
 * @returns {number[]}
 */
window.impregnatedBy = function(slave) {
	const IDArray = [];
	if (!Array.isArray(slave.womb)) {
		WombInit(slave);
	}
	for (let i = 0; i < slave.womb.length; i++) {
		IDArray.push(slave.womb[i].fatherID);
	}
	return IDArray;
};

/**
 * returns true if mother was impregnated by father
 * @param {App.Entity.SlaveState} mother
 * @param {App.Entity.SlaveState} father
 * @returns {boolean}
 */
window.isImpregnatedBy = function(mother, father) {
	return impregnatedBy(mother).includes(father.ID);
};

/**
 * @param {App.Entity.SlaveState} slave
 */
window.SoftenBehavioralFlaw = function SoftenBehavioralFlaw(slave) {
	switch (slave.behavioralFlaw) {
		case "arrogant":
			slave.behavioralQuirk = "confident";
			break;
		case "bitchy":
			slave.behavioralQuirk = "cutting";
			break;
		case "odd":
			slave.behavioralQuirk = "funny";
			break;
		case "hates men":
			slave.behavioralQuirk = "adores women";
			break;
		case "hates women":
			slave.behavioralQuirk = "adores men";
			break;
		case "gluttonous":
			slave.behavioralQuirk = "fitness";
			break;
		case "anorexic":
			slave.behavioralQuirk = "insecure";
			break;
		case "devout":
			slave.behavioralQuirk = "sinful";
			break;
		case "liberated":
			slave.behavioralQuirk = "advocate";
			break;
	}
	slave.behavioralFlaw = "none";
};

/**
 * @param {App.Entity.SlaveState} slave
 */
window.SoftenSexualFlaw = function SoftenSexualFlaw(slave) {
	switch (slave.sexualFlaw) {
		case "hates oral":
			slave.sexualQuirk = "gagfuck queen";
			break;
		case "hates anal":
			slave.sexualQuirk = "painal queen";
			break;
		case "hates penetration":
			slave.sexualQuirk = "strugglefuck queen";
			break;
		case "shamefast":
			slave.sexualQuirk = "tease";
			break;
		case "idealistic":
			slave.sexualQuirk = "romantic";
			break;
		case "repressed":
			slave.sexualQuirk = "perverted";
			break;
		case "apathetic":
			slave.sexualQuirk = "caring";
			break;
		case "crude":
			slave.sexualQuirk = "unflinching";
			break;
		case "judgemental":
			slave.sexualQuirk = "size queen";
			break;
	}
	slave.sexualFlaw = "none";
};

/**
 * @param {object} PC
 */
window.generatePlayerPronouns = function(PC) {
	if (PC.title === 0) {
		PC.pronoun = App.Data.Pronouns.Kind.female;
	} else {
		PC.pronoun = App.Data.Pronouns.Kind.male;
	}
};

window.generateAssistantPronouns = function() {
	if (V.assistant === 0) {
		V.assistantPronouns.pronoun = App.Data.Pronouns.Kind.ai;
	} else if ((V.assistantAppearance === "incubus") && (V.diversePronouns === 1)) {
		V.assistantPronouns.pronoun = App.Data.Pronouns.Kind.male;
	} else {
		V.assistantPronouns.pronoun = App.Data.Pronouns.Kind.female;
	}

	if (V.assistant === 0) {
		V.marketAssistantPronouns.pronoun = App.Data.Pronouns.Kind.ai;
	} else if ((V.assistantAppearance === "succubus") && (V.diversePronouns === 1)) {
		V.marketAssistantPronouns.pronoun = App.Data.Pronouns.Kind.male;
	} else {
		V.marketAssistantPronouns.pronoun = App.Data.Pronouns.Kind.female;
	}
};

window.printTrinkets = function printTrinkets() {
	function trinketPluralReplacer(desc) {
		let r;
		switch (desc) {
			/* not supported
				best in show ribbons
				napkins
				saPorn trinkets
				wedding photos
			*/
			// should never have plurals
			case "a collection of diplomas from expensive schools":
			case "a framed low denomination piece of paper money from your native country":
			case "a battered old assault rifle":
			case "a framed picture of a slave with her sale price scrawled across the bottom":
			case "an artist's impression of an early arcology design":
			case "a framed copy of the first news story featuring yourself":
			case "a miniature model of your first arcology":
			case "a copy of the first porno you starred in":
			case "a framed picture of your late Master":
			case "your favorite handgun, whose sight has instilled fear in many":
			case "a news clipping of your first successful live hack":
			case "a damaged plate carrier bearing Daughters of Liberty insignia":
			case "a Daughters of Liberty flag that once hung in their forward command post within your arcology":
			case "a Daughters of Liberty brassard":
			case "a shot-torn flag of the failed nation whose militants attacked the Free City":
				return desc;
				// manual replacement
			case "a thank-you note from a MILF tourist whom you made feel welcome in the arcology":
				return "several thank-you notes from MILF tourists whom you made feel welcome in the arcology";
				// replacement by groups
			default:
				r = desc;
				if (desc.endsWith("citizen")) { // will not reduce spam from different future societies
					r = r.replace("message", "messages");
					r = r.replace("from a", "from");
					r = r.replace("a ", "several ");
					r = r.replace("citizen", "citizens");
					r = r.replace("number", "numbers");
					r = r.replace("test", "tests");
				} else if (desc.endsWith("acquaintance")) {
					r = r.replace("note", "notes");
					r = r.replace("from a", "from");
					r = r.replace("a ", "several ");
					r = r.replace("owner", "owners");
				}
				return r;
		}
	}

	if (State.variables.trinkets.length === 0) {
		return "";
	}

	const trinkets = weightedArray2HashMap(State.variables.trinkets);
	let trinketString = [];
	let plurals = false;

	for (const trinketDesc in trinkets) {
		if (trinkets[trinketDesc] === 1) {
			trinketString.push(trinketDesc);
		} else if (trinkets[trinketDesc] > 1) {
			trinketString.push(trinketPluralReplacer(trinketDesc));
			plurals = true;
		}
	}

	// depending on length of trinketString, add necessary conjunctions
	if (trinketString.length === 1) {
		if (plurals === false) {
			trinketString = `a single item: ${trinketString[0]}`;
		} else {
			trinketString = trinketString[0];
		}
	} else if (trinketString.length === 2 && plurals === false) {
		trinketString = `a couple of items: ${trinketString[0]}, and ${trinketString[1]}`;
	} else {
		trinketString[trinketString.length - 1] = `and ${trinketString[trinketString.length - 1]}`;
		trinketString = trinketString.join(", ");
	}
	return `There's a display case behind your desk, with ${trinketString}.`;
};

/**
 * @param {number} nmbr1
 * @param {number} nmbr2
 */
window.pregNumberName = function(nmbr1, nmbr2) {
	let pt = "";
	let p1 = nmbr1 % 10;
	let p2 = ((nmbr1 % 100) - (nmbr1 % 10)) / 10;
	let p3 = ((nmbr1 % 1000) - (nmbr1 % 100)) / 100;
	switch (nmbr1) {
		case 1:
			pt += "bab";
			break;
		case 2:
			pt += "twin";
			break;
		default:
			switch (nmbr1) {
				case 3:
					pt += "tri";
					break;
				case 4:
					pt += "quadru";
					break;
				case 5:
					pt += "quintu";
					break;
				case 6:
					pt += "sextu";
					break;
				case 7:
					pt += "septu";
					break;
				case 8:
					pt += "octu";
					break;
				case 9:
					pt += "nonu";
					break;
				default:
					switch (p1)	{
						case 1:
							pt += "un";
							break;
						case 2:
							pt += "duo";
							break;
						case 3:
							pt += "tre";
							break;
						case 4:
							pt += "quattuor";
							break;
						case 5:
							pt += "quin";
							break;
						case 6:
							pt += "sex";
							break;
						case 7:
							pt += "septen";
							break;
						case 8:
							pt += "octo";
							break;
						case 9:
							pt += "novem";
							break;
					}
					switch (p2)	{
						case 1:
							pt += "dec";
							break;
						case 2:
							pt += "vigint";
							break;
						case 3:
							pt += "trigint";
							break;
						case 4:
							pt += "quadragint";
							break;
						case 5:
							pt += "quinquagint";
							break;
						case 6:
							pt += "sexagint";
							break;
						case 7:
							pt += "septuagint";
							break;
						case 8:
							pt += "octogint";
							break;
						case 9:
							pt += "nonagint";
							break;
					}
					if (nmbr1 >= 100) {
						if (p2 !== 0) {
							pt += "i";
						}
						switch (p3)	{
							case 1:
								pt += "centu";
								break;
							case 2:
								pt += "bicentu";
								break;
							case 3:
								pt += "tricentu";
								break;
							case 4:
								pt += "quadricentu";
								break;
							case 5:
								pt += "quincentu";
								break;
							case 6:
								pt += "sexcentu";
								break;
							case 7:
								pt += "septemcentu";
								break;
							case 8:
								pt += "octocentu";
								break;
							case 9:
								pt += "novemcentu";
								break;
						}
					} else {
						pt += "u";
					}
			}
			pt += "plet";
	}
	if (nmbr2 === 2) {
		if (nmbr1 === 1) {
			pt += "ie";
		}
		pt += "s";
	} else {
		if (nmbr1 === 1) {
			pt += "y";
		}
	}
	return pt;
};


code:
/**
 * @param {App.Entity.SlaveState} slave
 */
window.SlaveStatClamp = function SlaveStatClamp(slave) {
	slave.energy = Math.clamp(slave.energy, 0, 100);

	if (slave.devotion > 100) {
		if (slave.trust < -95) {
			slave.trust = -100;
		} else if (slave.trust < 100 && slave.trust > 20) {
			slave.trust += Math.trunc((slave.devotion - 100) * 5) / 10;
		} else {
			repX(4 * (slave.devotion - 100), "slavesViewOfPC", slave);
		}
		slave.devotion = 100;
	} else if (slave.devotion < -95) {
		slave.devotion = -100;
	}
	if (slave.trust > 100) {
		if (slave.devotion < -95) {
			slave.devotion = -100;
		} else if (slave.devotion < 100 && slave.devotion > 20) {
			slave.devotion += Math.trunc(slave.trust - 100);
		} else {
			repX(4 * (slave.trust - 100), "slavesViewOfPC", slave);
		}
		slave.trust = 100;
	} else if (slave.trust < -95) {
		slave.trust = -100;
	}
	if (slave.trust < -100) {
		slave.trust = -100;
	}
	if (slave.devotion < -100) {
		slave.devotion = -100;
	}
	if (isNaN(slave.counter.oral)) {
		slave.counter.oral = 0;
		alert("Oral count has broken, report what you just did!");
	}
	if (isNaN(slave.counter.anal)) {
		slave.counter.anal = 0;
		alert("Anal count has broken, report what you just did!");
	}
	if (isNaN(slave.counter.vaginal)) {
		slave.counter.vaginal = 0;
		alert("Vaginal count has broken, report what you just did!");
	}
	if (isNaN(slave.counter.mammary)) {
		slave.counter.mammary = 0;
		alert("Titfuck count has broken, report what you just did!");
	}
	if (isNaN(slave.counter.penetrative)) {
		slave.counter.penetrative = 0;
		alert("Penetration count has broken, report what you just did!");
	}
	if (slave.foreskin === undefined) {
		slave.foreskin = 0;
	}
	if (!hasAnyNaturalLegs(slave) && slave.heels === 1) {
		slave.heels = 0;
	}
	if (slave.vagina < 0 && slave.mpreg === 0 && slave.preg === -1) {
		slave.preg = 0;
		WombFlush(slave);
	}
};


code:
App.Medicine.Surgery = {};
/**
 * Composes the Procedure object from its parts
 *
 * This function has the only purpose to ensure the result object has all required properties
 * @param {string} typeId
 * @param {string} label
 * @param {number} effect
 * @param {string} desc
 * @param {slaveOperation} [action]
 * @param {number} [costs] money costs
 * @param {number} [hCosts] health costs
 * @param {string} [surgeryType]
 * @returns {App.Medicine.Surgery.Procedure}
 */
App.Medicine.Surgery.makeOption = function(typeId, label, effect, desc, action, costs, hCosts, surgeryType) {
	return {
		typeId: typeId,
		label: label,
		targetEffect: effect,
		description: desc,
		costs: costs,
		healthCosts: hCosts,
		action: action,
		surgeryType: surgeryType
	};
};

/**
 * Composes the procedure option with empty .action, i.e. a procedure that can't be applied
 * @param {string} typeId
 * @param {string} label
 * @param {string} desc
 * @returns {App.Medicine.Surgery.Procedure}
 */
App.Medicine.Surgery.makeImpossibleOption = function(typeId, label, desc) {
	return this.makeOption(typeId, label, 0, desc);
};

/**
 * Various constants for procedures
 */
App.Medicine.Keys = {
	Surgery: {
		Target: {
			/**
			 * Type id's for breast-related procedures
			 */
			breast: {
				installImplant: "breast.implant.install",
				removeImplant: "breast.implant.remove",
				fillUp: "breast.implant.fill",
				drain: "breast.implant.drain",
				changeImplant: "breast.implant.replace",
				reduction: "breast.tissue.reduce",
			},
			/**
			 * Type id's for butt-related procedures
			 */
			butt: {
				installImplant: "butt.implant.install",
				removeImplant: "butt.implant.remove",
				fillUp: "butt.implant.fill",
				drain: "butt.implant.drain",
				changeImplant: "but.implant.replace",
				reduction: "butt.tissue.reduce",
			}
		}
	}
};

/**
 * Commit procedure, executing its action and subtracting its costs
 * @param {App.Medicine.Surgery.Procedure} surgery
 * @param {App.Entity.SlaveState} slave
 */
App.Medicine.Surgery.commit = function(surgery, slave) {
	V.surgeryType = surgery.surgeryType;
	surgery.action(slave);
	cashX(forceNeg(surgery.costs), "slaveSurgery", slave);
	surgeryDamage(slave, surgery.healthCosts);
};

/**
 * Returns markup for a link to execute the given procedure
 * @param {string} passage Passage to go after the surgery
 * @param {App.Medicine.Surgery.Procedure} surgery
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
App.Medicine.Surgery.makeLink = function(passage, surgery, slave) {
	if (surgery.action === undefined) {
		return App.UI.disabledLink(surgery.label, [surgery.description]);
	}

	function healthCosts() {
		const hc = (State.variables.PC.skill.medicine >= 100) ? Math.round(surgery.healthCosts / 2) : surgery.healthCosts;
		if (hc > 30) {
			return 'substantial';
		} else if (hc > 20) {
			return 'significant';
		} else if (hc > 10) {
			return 'moderate';
		} else if (hc > 5) {
			return 'light';
		}
		return 'insignificant';
	}

	return App.UI.link(surgery.label, App.Medicine.Surgery.commit, [surgery, slave], passage,
		`${capFirstChar(surgery.description)}.\nSurgery costs: ${cashFormat(surgery.costs)}.\nProjected health damage: ${healthCosts()}.`);
};

/**
 * Helpers for composing procedure descriptions
 */
App.Medicine.Surgery.ListHelpers = class {
	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {string} bodyPart
	 * @param {Object.<string, string>} keys
	 * @param {App.Utils.Pronouns} pronouns
	 * @param {boolean} showCCs
	 */
	constructor(slave, bodyPart, keys, pronouns, showCCs) {
		/** @private */
		this._slave = slave;
		/** @private */
		this._bodyPart = bodyPart;
		/** @private */
		this._keys = keys;
		/** @private */
		this._pronouns = pronouns;
		/** @private */
		this._V = State.variables;
		this._showCCs = showCCs;
	}

	/**
	 * @param {string} name
	 * @param {number} implantType
	 * @param {number} size
	 * @returns {App.Medicine.Surgery.Procedure}
	 */
	installImplants(name, implantType, size) {
		return App.Medicine.Surgery.makeOption(this._keys.installImplant, `${capFirstChar(name)} implants`, size,
			`place ${name}${this._showCCs ? ` ${size}cc` : ''} implants into ${this._pronouns.his} ${this._bodyPart}`,
			slave => {
				slave[`${this._bodyPart}Implant`] = size;
				slave[`${this._bodyPart}ImplantType`] = implantType;
				slave[this._bodyPart] += size;
			}, this._V.surgeryCost, 10, this._bodyPart
		);
	}

	removeImplants() {
		return App.Medicine.Surgery.makeOption(this._keys.removeImplant, "Remove implants",
			-this._slave[`${this._bodyPart}Implant`],
			`remove ${this._pronouns.his} ${this._bodyPart} implants`,
			slave => {
				slave[`${this._bodyPart}`] -= slave[`${this._bodyPart}Implant`];
				slave[`${this._bodyPart}Implant`] = 0;
				slave[`${this._bodyPart}ImplantType`] = 0;
			}, this._V.surgeryCost, 5, `${this._bodyPart}Loss`
		);
	}

	/**
	 * @param {string} name
	 * @param {number} implantType
	 * @param {number} size
	 * @returns {App.Medicine.Surgery.Procedure}
	 */
	replaceImplants(name, implantType, size) {
		return App.Medicine.Surgery.makeOption(this._keys.changeImplant, `${capFirstChar(name)} implants`,
			size - this._slave.boobsImplant,
			`replace ${this._pronouns.his} ${this._bodyPart} implants with ${name}${this._showCCs ? ` (${size}cc)` : ''} ones`,
			slave => {
				slave[this._bodyPart] += size - slave[`${this._bodyPart}Implant`];
				slave[`${this._bodyPart}Implant`] = size;
				slave[`${this._bodyPart}ImplantType`] = implantType;
			}, this._V.surgeryCost, 10, this._bodyPart
		);
	}

	/**
	 * @param {number} volume
	 * @returns {App.Medicine.Surgery.Procedure}
	 */
	fillUp(volume) {
		return App.Medicine.Surgery.makeOption(this._keys.fillUp, "Add inert filler", volume,
			`add ${this._showCCs ? `${volume}cc of` : 'some'} inert filler to each of ${this._pronouns.his} ${this._bodyPart} implants`,
			slave => {
				slave[`${this._bodyPart}Implant`] += volume;
				slave[this._bodyPart] += volume;
			},
			this._V.surgeryCost, 10, this._bodyPart
		);
	}

	/**
	 * @param {number} volume
	 * @returns {App.Medicine.Surgery.Procedure}
	 */
	drain(volume) {
		return App.Medicine.Surgery.makeOption(this._keys.drain, `Drain ${volume}cc`, -volume,
			`drain ${this._showCCs ? `${volume}cc of` : 'some'} inert filler from ${this._pronouns.his} ${this._bodyPart} implants`,
			slave => {
				slave[`${this._bodyPart}Implant`] -= volume;
				slave[this._bodyPart] -= volume;
			}, this._V.surgeryCost, 5, `${this._bodyPart}Loss`
		);
	}

	/**
	 * @param {string} procedureName
	 * @param {number} sizeChange
	 * @returns {App.Medicine.Surgery.Procedure}
	 */
	reduce(procedureName, sizeChange) {
		return App.Medicine.Surgery.makeOption(this._keys.reduction,
			`${capFirstChar(procedureName)} ${this._bodyPart}`, -200,
			`${procedureName} ${this._pronouns.his} ${this._bodyPart}`,
			slave => {
				slave[this._bodyPart] -= sizeChange;
			}, this._V.surgeryCost, 5, `${this._bodyPart}Loss`
		);
	}
};

/**
 * Returns options to accept all possible surgeries
 * @returns {App.Medicine.Surgery.SizingOptions}
 */
App.Medicine.Surgery.allSizingOptions = function() {
	return {
		augmentation: true,
		reduction: true,
		strings: true,
		replace: true
	};
};

App.Medicine.Surgery.sizingProcedures = function() {
	return {
		bodyPart: bodyPart,
		boobs: boobSizingProcedures,
		butt: buttSizingProcedures
	};

	/**
	 * Returns list of available surgeries targeted at changing size of the given body part
	 * @param {string} bodyPart
	 * @param {App.Entity.SlaveState} slave
	 * @param {App.Medicine.Surgery.SizingOptions} [options]
	 * @returns {App.Medicine.Surgery.Procedure[]}
	 */
	function bodyPart(bodyPart, slave, options) {
		switch (bodyPart) {
			case "boob":
			case "boobs":
			case "breast":
			case "breasts":
				return boobSizingProcedures(slave, options);
			case "ass":
			case "booty":
			case "butt":
				return buttSizingProcedures(slave, options);
			default:
				throw `No sizing procedures for ${bodyPart}`;
		}
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {App.Medicine.Surgery.SizingOptions} [options]
	 * @returns {App.Medicine.Surgery.Procedure[]}
	 */
	function boobSizingProcedures(slave, options = {}) {
		const thisArcology = V.arcologies[0];
		const largeImplantsAvailable = thisArcology.FSTransformationFetishistResearch === 1;
		const advancedFillableImplantsAvailable = V.ImplantProductionUpgrade === 1;
		const pronouns = getPronouns(slave);
		const {he, His} = pronouns;

		const types = App.Medicine.Keys.Surgery.Target.breast; // shortcuts
		const helper = new App.Medicine.Surgery.ListHelpers(slave, "boobs", types, pronouns, V.showBoobCCs);

		let r = [];
		if (options.augmentation) {
			if (slave.boobs > 50000) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.fillUp, "Increase boobs", `<em>${His} breasts are as large as ${he} can physically support</em>`));
			} else if (slave.boobsImplant > 10000 && !largeImplantsAvailable && slave.boobsImplantType !== 1) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.fillUp, "Increase boobs", `<em>${His} implants are filled to capacity</em>`));
			} else if (slave.indentureRestrictions >= 2) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.installImplant, "Change boob size", `<em>${His} indenture forbids elective surgery</em>`));
			} else if (slave.breastMesh === 1) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.installImplant, "Put implants", `<em>${His} supportive mesh implant blocks implantation</em>`));
			} else if (slave.boobsImplant === 0) {
				if (options.strings) {
					r.push(helper.installImplants("string", 1, 400));
				}
				if (V.surgeryUpgrade === 1) {
					r.push(helper.installImplants("large", 0, 600));
				}
				r.push(helper.installImplants("standard", 0, 400));
				r.push(helper.installImplants("small", 0, 200));
			} else if (slave.boobsImplant > 10000 && slave.boobsImplantType !== 1 && advancedFillableImplantsAvailable) {
				r.push(helper.fillUp(1000));
			} else if (slave.boobsImplant > 9800 && slave.boobsImplantType !== 1 && advancedFillableImplantsAvailable) {
				r.push(helper.replaceImplants("hyper fillable", 0, 11000));
			} else if (slave.boobsImplant > 2000 && slave.boobsImplantType !== 1) {
				r.push(helper.fillUp(400));
			} else if (slave.boobsImplant > 1800 && slave.boobsImplantType !== 1) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.fillUp, "Add inert filler", `<em>${His} implants are filled to capacity</em>`));

				const advancedFillable = helper.replaceImplants("advanced fillable", 0, 2200);
				if (!advancedFillableImplantsAvailable) {
					advancedFillable.costs += 10000;
					advancedFillable.label += " (special order)";
					advancedFillable.description += " (special order)";
				}
				r.push(advancedFillable);
			} else if (slave.boobsImplant > 600 && slave.boobsImplantType !== 1) {
				r.push(helper.fillUp(200));
			} else if (slave.boobsImplant > 400 && slave.boobsImplantType !== 1) {
				r.push(helper.replaceImplants("fillable", 0, 800));
			} else if (slave.boobsImplant > 200 && slave.boobsImplantType !== 1) {
				r.push(helper.replaceImplants("large", 0, 600));
			} else if (slave.boobsImplant > 0 && slave.boobsImplantType !== 1) {
				r.push(helper.replaceImplants("standard", 0, 400));
				r.push(helper.replaceImplants("large", 0, 600));
			}
		}

		if (options.reduction && (slave.boobs > 300 || slave.boobsImplant > 0)) {
			if (slave.boobsImplant > 0) {
				if (slave.boobsImplantType === 1 && slave.boobsImplant > 400) {
					if (slave.boobsImplant > 8000) {
						r.push(helper.drain(1000));
					} else if (slave.boobsImplant > 5000) {
						r.push(helper.drain(750));
					} else if (slave.boobsImplant > 2000) {
						r.push(helper.drain(500));
					} else if (slave.boobsImplant > 1000) {
						r.push(helper.drain(250));
					} else if (slave.boobsImplant > 500) {
						r.push(helper.drain(100));
					}
				}
				r.push(helper.removeImplants());
			}
			if ((slave.boobs > 300) && (slave.boobsImplant === 0) && slave.indentureRestrictions < 2) {
				r.push(helper.reduce("reduce", 200));
				if (slave.boobs < 675) {
					r.push(helper.reduce("slightly reduce", 25));
				}
			}
			if ((slave.boobsImplant === 0) && slave.indentureRestrictions < 2 && (slave.breedingMark !== 1 || V.propOutcome !== 1 || V.eugenicsFullControl === 1 || V.arcologies[0].FSRestart === "unset")) {
				if (slave.boobs >= 7000) {
					r.push(App.Medicine.Surgery.makeOption(types.reduction, "Mastectomy", 300 - slave.boobs,
						"perform mastectomy",
						slave => {
							slave.boobs = 300;
						},
						V.surgeryCost, 30, "mastectomy+"
					));
				} else if (slave.boobs >= 2000) {
					r.push(App.Medicine.Surgery.makeOption(types.reduction, "Mastectomy", 300 - slave.boobs,
						"perform mastectomy",
						slave => {
							slave.boobs = 300;
						},
						V.surgeryCost, 30, "mastectomy"
					));
				}
			}
		}
		return r;
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {App.Medicine.Surgery.SizingOptions} [options]
	 * @returns {App.Medicine.Surgery.Procedure[]}
	 */
	function buttSizingProcedures(slave, options = {}) {
		const thisArcology = V.arcologies[0];
		const largeImplantsAvailable = thisArcology.FSTransformationFetishistResearch === 1;
		const advancedFillableImplantsAvailable = V.ImplantProductionUpgrade === 1;
		const pronouns = getPronouns(slave);
		const {he, His} = pronouns;

		const types = App.Medicine.Keys.Surgery.Target.butt; // shortcuts
		const helper = new App.Medicine.Surgery.ListHelpers(slave, "butt", types, pronouns, false);

		let r = [];

		if (options.augmentation) {
			if (slave.indentureRestrictions >= 2) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.installImplant, "Change butt size", `<em>${His} indenture forbids elective surgery</em>`));
			} else if (slave.butt > 19) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.fillUp, "Increase butt", `<em>${His} butt is as large as it can possibly get</em>`));
			} else if (slave.butt > 7 && !largeImplantsAvailable && thisArcology.FSAssetExpansionist === "unset" && slave.buttImplantType !== 1) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.installImplant, "Increase butt", `<em>${His} butt is as large as ${he} can physically support</em>`));
			} else if (slave.buttImplant > 7 && !largeImplantsAvailable && slave.buttImplantType !== 1) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.installImplant, "Increase butt", `<em>${His} butt implants are filled to capacity</em>`));
			} else if (slave.buttImplant === 0) {
				r.push(helper.installImplants("standard", 0, 1));
				if (options.strings) {
					r.push(helper.installImplants("string", 1, 1));
				}
			} else if (slave.buttImplant === 1 && slave.buttImplantType !== 1) {
				r.push(helper.replaceImplants("bigger", 0, 2));
			} else if (slave.buttImplant === 2 && slave.buttImplantType !== 1) {
				r.push(helper.replaceImplants("fillable", 0, 3));
			} else if (slave.buttImplant === 4 && slave.buttImplantType !== 1) {
				r.push(App.Medicine.Surgery.makeImpossibleOption(types.fillUp, "Increase size", `<em>${His} implants are filled to capacity</em>`));
				const advancedFillable = helper.replaceImplants("advanced fillable", 0, 5);
				if (!advancedFillableImplantsAvailable) {
					advancedFillable.costs += 10000;
					advancedFillable.label += " (special order)";
					advancedFillable.description += " (special order)";
				}
				r.push(advancedFillable);
			} else if (slave.buttImplant === 8 && slave.buttImplantType !== 1 && advancedFillableImplantsAvailable) {
				r.push(helper.replaceImplants("hyper fillable", 0, 9));
			} else if (slave.buttImplant > 2 && slave.buttImplantType !== 1) {
				r.push(helper.fillUp(1));
			}
		}

		if (options.reduction) {
			if (slave.buttImplant > 0) {
				if (slave.indentureRestrictions < 2) {
					if (slave.buttImplantType === 1 && slave.buttImplant > 1) {
						r.push(helper.drain(1));
					}
					r.push(helper.removeImplants());
				}
			}
			if ((slave.butt > 1) && (slave.buttImplant === 0)) {
				if (slave.indentureRestrictions < 2) {
					r.push(helper.reduce("reduce", 1));
				}
			}
		}
		return r;
	}
}();

/**
 * Clean up extremities on removal or piercings, tats, and brands
 * For limbs use removeLimbs()
 * @param {App.Entity.SlaveState} slave
 * @param {string} part
 */
window.surgeryAmp = function(slave, part) {
	switch (part) {
		case "left ear":
			delete slave.brand["left ear"];
			slave.earShape = "none";
			slave.earT = "none";
			slave.earPiercing = 0;
			surgeryDamage(slave, 10);
			break;
		case "right ear":
			delete slave.brand["right ear"];
			slave.earShape = "none";
			slave.earT = "none";
			slave.earPiercing = 0;
			surgeryDamage(slave, 10);
			break;
		case "dick":
			slave.dick = 0;
			slave.foreskin = 0;
			slave.skill.vaginal = 0;
			slave.dickPiercing = 0;
			slave.dickTat = 0;
			slave.dickAccessory = "none";
			slave.chastityPenis = 0;
			surgeryDamage(slave, 20);
			break;
		case "vagina":
			slave.vagina = -1;
			slave.ovaries = 0;
			slave.preg = -2;
			slave.pregSource = 0;
			slave.skill.vaginal = 0;
			slave.vaginaTat = 0;
			slave.vaginalAccessory = "none";
			slave.vaginalAttachment = "none";
			slave.chastityVagina = 0;
			surgeryDamage(slave, 20);
			break;
		case "horn":
			slave.horn = "none";
			slave.hornColor = "none";
			surgeryDamage(slave, 10);
			break;
		case "voicebox":
			slave.voice = 0;
			slave.voiceImplant = 0;
			surgeryDamage(slave, 10);
			break;
		case "lips":
			slave.lips -= slave.lipsImplant;
			slave.lipsImplant = 0;
			if (slave.skill.oral > 10) {
				slave.skill.oral -= 10;
			}
			break;
		default:
			// eslint-disable-next-line no-console
			console.log(`ERROR: Unknown amputation type: ` + part);
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} side
 * @param {string} action
 */
window.eyeSurgery = function(slave, side, action) {
	if (side === "both") {
		eyeSurgery(slave, "left", action);
		eyeSurgery(slave, "right", action);
		return;
	}

	if (side !== "left" && side !== "right") { return; } // make sure side can be used to access the object
	let eyeExists = slave.eye[side] !== null;

	/* actions that don't need an eye */
	switch (action) {
		case "normal":
			addEye(slave); // color overwritten by genetics
			return;
		case "glass":
			if (!eyeExists) { // color stays the same if possible
				addEye(slave);
			}
			slave.eye[side].type = 2;
			slave.eye[side].vision = 0;
			return;
		case "cybernetic":
			if (!eyeExists) { // color stays the same if possible
				addEye(slave);
			}
			slave.eye[side].type = 3;
			slave.eye[side].vision = 2;
			return;
	}

	function addEye(slave) {
		slave.eye[side] = new App.Entity.SingleEyeState();
		resetEyeColor(slave, side);
	}

	/* actions that need an eye */
	if (!eyeExists) { return; } // make sure the eye exists

	switch (action) {
		case "remove":
			slave.eye[side] = null;
			break;
		case "blind":
			slave.eye[side].vision = 0;
			break;
		case "blur":
			slave.eye[side].vision = 1;
			break;
		case "fix":
			slave.eye[side].vision = 2;
			break;
		default:
			// eslint-disable-next-line no-console
			console.log(`ERROR: Unknown surgery action: ` + action);
	}
};

/**
 * To be used during slave generation or slave styling (auto salon)
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} color to set eye to
 * @param {string} [side] "left", "right", "both"
 */
window.setEyeColor = function setEyeColor(slave, color, side = "both") {
	if (side === "both") {
		setEyeColor(slave, color, "left");
		setEyeColor(slave, color, "right");
		return;
	}

	if (side !== "left" && side !== "right") { return; }

	slave.eye[side].iris = color;
};

/**
 * Set all colors of the eye.
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} iris
 * @param {string} pupil
 * @param {string} sclera
 * @param {string} side
 */
window.setEyeColorFull = function setEyeColorFull(slave, iris, pupil, sclera, side) {
	if (side === "both") {
		setEyeColorFull(slave, iris, pupil, sclera, "left");
		setEyeColorFull(slave, iris, pupil, sclera, "right");
		return;
	}

	if (side !== "left" && side !== "right") { return; }

	if (iris !== "") {
		slave.eye[side].iris = iris;
	}
	if (pupil !== "") {
		slave.eye[side].pupil = pupil;
	}
	if (sclera !== "") {
		slave.eye[side].sclera = sclera;
	}
};

/**
 * Set genetic eye color
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} color
 * @param {boolean} heterochromia
 */
window.setGeneticEyeColor = function(slave, color, heterochromia = false) {
	if (heterochromia) {
		slave.geneticQuirks.heterochromia = color;
	} else {
		slave.eye.origColor = color;
	}
};

/**
 * Sets the eye color to the genetic color.
 * Takes heterochromia and albinism into account.
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} side
 */
window.resetEyeColor = function(slave, side) {
	if (side === "both") {
		resetEyeColor(slave, "left");
		resetEyeColor(slave, "right");
		return;
	}

	if (side !== "left" && side !== "right") { return; }

	slave.eye[side].iris = getGeneticEyeColor(slave, side);
};

/**
 * Allowed values for limb:
 * left arm, right arm, left leg, right leg, all
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} limb
 */
window.removeLimbs = function(slave, limb) {
	function remove(limb, side) {
		const prosthetic = findProsthetic(slave, limbToProsthetic(slave[limb][side].type));

		if (prosthetic) {
			prosthetic[limb][side] = slave[limb][side];
		}

		slave[limb][side] = null;
	}

	switch (limb) {
		case "left arm":
			if (!hasLeftArm(slave)) {
				return;
			}
			remove("arm", "left");
			delete slave.brand["left upper arm"];
			delete slave.brand["left lower arm"];
			delete slave.brand["left wrist"];
			delete slave.brand["left hand"];
			// slave.armsTat = 0;
			if (!hasAnyArms(slave)) {
				slave.armAccessory = "none";
				// slave.nails = 0;
			}
			break;
		case "right arm":
			if (!hasRightArm(slave)) {
				return;
			}
			remove("arm", "right");
			delete slave.brand["right upper arm"];
			delete slave.brand["right lower arm"];
			delete slave.brand["right wrist"];
			delete slave.brand["right hand"];
			// slave.armsTat = 0;
			if (!hasAnyArms(slave)) {
				slave.armAccessory = "none";
				// slave.nails = 0;
			}
			break;
		case "left leg":
			if (!hasLeftLeg(slave)) {
				return;
			}
			remove("leg", "left");
			delete slave.brand["left thigh"];
			delete slave.brand["left calf"];
			delete slave.brand["left ankle"];
			delete slave.brand["left foot"];
			if (!hasAnyLegs(slave)) {
				slave.legsTat = 0;
				slave.shoes = "none";
				slave.legAccessory = "none";
				slave.heightImplant = 0;
				slave.heels = 0;
			}
			break;
		case "right leg":
			if (!hasRightLeg(slave)) {
				return;
			}
			remove("leg", "right");
			delete slave.brand["right thigh"];
			delete slave.brand["right calf"];
			delete slave.brand["right ankle"];
			delete slave.brand["right foot"];
			if (!hasAnyLegs(slave)) {
				slave.legsTat = 0;
				slave.shoes = "none";
				slave.legAccessory = "none";
				slave.heightImplant = 0;
				slave.heels = 0;
			}
			break;
		case "all":
			removeLimbs(slave, "left arm");
			removeLimbs(slave, "right arm");
			removeLimbs(slave, "left leg");
			removeLimbs(slave, "right leg");
			break;
	}
};

/**
 * Expects amputated limbs. Will overwrite existing limbs.
 *
 * Allowed values for limb:
 * left arm, right arm, left leg, right leg, all
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} limb
 * @param {number} id
 */
window.attachLimbs = function(slave, limb, id) {
	function attach(limb, side) {
		let prosthetic = findProsthetic(slave, limbToProsthetic(id));

		if (prosthetic) {
			slave[limb][side] = prosthetic[limb][side];
		} else {
			slave[limb][side] = new App.Entity.LimbState();
			slave[limb][side].type = id;
		}
	}

	switch (limb) {
		case "left arm":
			attach("arm", "left");
			break;
		case "right arm":
			attach("arm", "right");
			break;
		case "left leg":
			attach("leg", "left");
			break;
		case "right leg":
			attach("leg", "right");
			break;
		case "all":
			attachLimbs(slave, "left arm", id);
			attachLimbs(slave, "right arm", id);
			attachLimbs(slave, "left leg", id);
			attachLimbs(slave, "right leg", id);
			break;
		default:
			// eslint-disable-next-line no-console
			console.log(`ERROR: Unknown attach point: ` + limb);
	}
};

window.upgradeLimbs = function(slave, newId) {
	let changed = false;

	/**
	 * @param {number} oldId
	 * @returns {number}
	 */
	function computeUpgrade(oldId) {
		if (newId < 2) {
			return oldId;
		} else if (newId === 2 && oldId === 0) {
			return 2;
		} else if (newId < 6 && oldId <= 2) {
			return newId;
		} else if (newId > oldId) {
			return newId;
		} else {
			return oldId;
		}
	}

	/**
	 * @param {string} limb
	 * @param {function} idFunction
	 */
	function upgradeLimb(limb, idFunction) {
		let oldId = idFunction(slave);
		if (oldId === 1) {
			return;
		}
		if (oldId > 1) {
			removeLimbs(slave, limb);
		}
		if (newId > 1) {
			attachLimbs(slave, limb, computeUpgrade(oldId));
		}
		if (oldId !== idFunction(slave)) {
			changed = true;
		}
	}

	upgradeLimb("left arm", getLeftArmID);
	upgradeLimb("right arm", getRightArmID);
	upgradeLimb("left leg", getLeftLegID);
	upgradeLimb("right leg", getRightLegID);

	return changed;
};

/**
 * Changes a slaves limbs to the specified value AND sets all related variables.
 * Intended for giving prosthetics during slave generation and events.
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} limb
 * @param {number} id
 * @param {boolean} clean if the slave should be cleaned of all existing
 */
window.configureLimbs = function(slave, limb, id, clean = false) {
	if (limb === "all") {
		configureLimbs(slave, "left arm", id);
		configureLimbs(slave, "right arm", id);
		configureLimbs(slave, "left leg", id);
		configureLimbs(slave, "right leg", id);
		return;
	}

	if (clean) {
		slave.PLimb = 0;
		slave.readyProsthetics = [];
	}

	let p = limbToProsthetic(id);
	if (p) {
		addProsthetic(slave, p);
	}

	if (id >= 6 && slave.PLimb < 2) {
		slave.PLimb = 2;
		addProsthetic(slave, "interfaceP2");
	} else if (id >= 2 && slave.PLimb < 1) {
		slave.PLimb = 1;
		addProsthetic(slave, "interfaceP1");
	}

	switch (limb) {
		case "left arm":
			if (id !== getLeftArmID(slave)) {
				removeLimbs(slave, "left arm");
				if (id > 0) {
					attachLimbs(slave, "left arm", id);
				}
			}
			break;
		case "right arm":
			if (id !== getRightArmID(slave)) {
				removeLimbs(slave, "right arm");
				if (id > 0) {
					attachLimbs(slave, "right arm", id);
				}
			}
			break;
		case "left leg":
			if (id !== getLeftLegID(slave)) {
				removeLimbs(slave, "left leg");
				if (id > 0) {
					attachLimbs(slave, "left leg", id);
				}
			}
			break;
		case "right leg":
			if (id !== getRightLegID(slave)) {
				removeLimbs(slave, "right leg");
				if (id > 0) {
					attachLimbs(slave, "right leg", id);
				}
			}
			break;
	}
};

/**
 * Prepare and set up for new Fuckdoll
 * @param {App.Entity.SlaveState} slave
 */
window.beginFuckdoll = function(slave) {
	slave.fuckdoll = 1;
	slave.toyHole = "all her holes";
	if ((slave.pubicHStyle !== "bald") && (slave.pubicHStyle !== "hairless")) {
		slave.pubicHStyle = "waxed";
	}
	slave.rules.living = "spare";
	slave.rules.speech = "restrictive";
	slave.rules.release.masturbation = 0;
	slave.rules.release.partner = 0;
	slave.rules.release.family = 0;
	slave.rules.release.slaves = 0;
	slave.rules.relationship = "restrictive";
	slave.choosesOwnClothes = 0;
	slave.clothes = "a Fuckdoll suit";
	slave.collar = "none";
	if ((!hasAnyLegs(slave)) || (slave.shoes !== "none")) {
		slave.shoes = "heels";
	}
	slave.armAccessory = "none";
	slave.legAccessory = "none";
	slave.vaginalAccessory = "none";
	slave.vaginalAttachment = "none";
	slave.dickAccessory = "none";
	slave.buttplug = "none";
	slave.chastityAnus = 0;
	slave.chastityPenis = 0;
	slave.chastityVagina = 0;
	slave.attrKnown = 1;
	slave.fetishKnown = 1;
	slave.subTarget = 0;
	slave.sentence = 0;
	slave.training = 0;
	slave.inflation = 0;
	slave.inflationType = "none";
	slave.inflationMethod = 0;
	slave.milkSource = 0;
	slave.cumSource = 0;
};


code:
Macro.add("textbox2", {
	handler: function() {
		if (this.args.length < 2) {
			const e = [];
			return this.args.length < 1 && e.push("variable name"), this.args.length < 2 && e.push("default value"), this.error(`no ${e.join(" or ")} specified`);
		}
		if (typeof this.args[0] !== "string") { return this.error("variable name argument is not a string"); }
		const t = this.args[0].trim();
		if (t[0] !== "$" && t[0] !== "_") { return this.error(`variable name "${this.args[0]}" is missing its sigil ($ or _)`); }
		Config.debug && this.debugView.modes({
			block: true
		});
		const r = Util.slugify(t);
		const a = this.args[1];
		const isNumber = typeof(a) === "number";
		const inputElement = document.createElement("input");
		let autofocus = false;
		let passage = void 0;
		let setargs = null;
		if (this.args.length > 3) {
			passage = this.args[2];
			autofocus = this.args[3] === "autofocus";
			if (!autofocus) {
				setargs = this.args[3];
			}
		} else if (this.args.length > 2) {
			if (this.args[2] === "autofocus") {
				autofocus = true;
			} else {
				passage = this.args[2];
			}
		}
		if (passage !== (void 0) && typeof(passage) === "object") {
			passage = passage.link;
		}
		if (!passage) {
			passage = State.passage;
		}

		function gotoPassage() {
			if (passage) {
				const currentScrollPosition = window.pageYOffset;
				const currentPassage = State.passage;
				if (setargs) {
					Scripting.evalTwineScript(setargs);
				}
				Engine.play(passage);
				if (currentPassage === passage) {
					Scripting.evalJavaScript(`window.scrollTo(0, ${currentScrollPosition});`);
				}
			}
		}

		function valueToNumberIfSame(v) {
			if (!isNumber) {
				return v;
			} // Do nothing
			try {
				return parseInt(v, 10);
			} catch (error) {
				return v;
			}
		}

		jQuery(inputElement).attr({
			id: `${this.name}-${r}`,
			name: `${this.name}-${r}`,
			// type: isNumber ? "number" : "text", /* TODO - hide spinner if we do this */
			tabindex: 0
		}).addClass(`macro-${this.name}`)
			.on("change", function() {
				State.setVar(t, valueToNumberIfSame(this.value));
			}).on("blur", function() {
				State.setVar(t, valueToNumberIfSame(this.value));
				// eslint-disable-next-line eqeqeq
				if (this.value != a) { // If the value has actually changed, reload the page. Note != and not !== because types might be different
					gotoPassage();
				}
			})
			.on("keypress", function(e) {
				e.which === 13 && (e.preventDefault(), State.setVar(t, valueToNumberIfSame(this.value)), gotoPassage());
			}).appendTo(this.output), State.setVar(t, a), inputElement.value = a, autofocus && (inputElement.setAttribute("autofocus", "autofocus"), postdisplay[`#autofocus:${inputElement.id}`] = function(e) {
			delete postdisplay[e], setTimeout(function() {
				return inputElement.focus();
			}, Engine.minDomActionDelay);
		});
	}
});


code:
/* Nicked off greyelf, works for replace textboxes */
window.setReplaceTextboxMaxLength = function(storyVarName, maxLength) {
	const textboxId = `#textbox-${Util.slugify(storyVarName)}`;
	$(textboxId)
		.attr("maxlength", maxLength)
		.css({
			"min-width": "initial",
			"width": `${maxLength}em`,
			"padding": "3px 2px"
		});
};

/* Nicked off TheMadExile, works for non-replace textboxes */
window.setTextboxMaxLength = function(storyVarName, maxLength) {
	const textboxId = `#textbox-${Util.slugify(storyVarName)}`;
	postdisplay[`${textboxId}-maxlength`] = function(taskName) {
		delete postdisplay[taskName];
		$(textboxId)
			.attr("maxlength", maxLength)
			.css({
				"min-width": "initial",
				"width": `${maxLength}em`,
				"padding": "3px 2px"
			});
	};
};


code:
/* This file contains only JS functions without dependencies on FC specific variables/conventions and do not rely on
 * custom functions outside this file
 */

/**
 * Returns whether x is undefined. Port of SugarCube's def.
 * @param {any} x
 * @returns {boolean}
 */
window.jsDef = function(x) {
	return (typeof x !== "undefined" && x !== null && x !== undefined);
};

/**
 * @param {number} n
 * @returns {boolean}
 */
window.isFloat = function(n) {
	return Number.isFinite(n) && Math.floor(n) !== n;
};

/**
 * Determines if a is between low and high
 * @param {number} a
 * @param {number} low
 * @param {number} high
 * @returns {boolean}
 */
window.between = function(a, low, high) {
	if (low === null) { low = -Infinity; }
	if (high === null) { high = Infinity; }
	return (a > low && a < high);
};

/**
 * @param {number[]} obj
 * @returns {number}
 */
window.hashChoice = function hashChoice(obj) {
	let randint = Math.floor(Math.random() * hashSum(obj));
	let ret;
	Object.keys(obj).some((key) => {
		if (randint < obj[key]) {
			ret = key;
			return true;
		} else {
			randint -= obj[key];
			return false;
		}
	});
	return ret;
};

/**
 * @param {number[]} obj
 * @returns {number}
 */
window.hashSum = function hashSum(obj) {
	let sum = 0;
	Object.keys(obj).forEach((key) => {
		sum += obj[key];
	});
	return sum;
};

/**
 * @param {Array} arr
 * @returns {Object}
 */
window.arr2obj = function arr2obj(arr) {
	const obj = {};
	arr.forEach((item) => {
		obj[item] = 1;
	});
	return obj;
};

/**
 * @param {{}} object
 * @param rest
 */
window.hashPush = function hashPush(object, ...rest) {
	rest.forEach((item) => {
		if (object[item] === undefined) {
			object[item] = 1;
		} else {
			object[item] += 1;
		}
	});
};

/**
 * @param {[]} array
 * @return {{}}
 */
window.weightedArray2HashMap = function weightedArray2HashMap(array) {
	const obj = {};
	array.forEach((item) => {
		if (obj[item] === undefined) {
			obj[item] = 1;
		} else {
			obj[item] += 1;
		}
	});
	return obj;
};

/**
 * generate a random, almost unique ID that is compliant (possibly) with RFC 4122
 *
 * @return {string}
 */
window.generateNewID = function generateNewID() {
	let date = Date.now(); // high-precision timer
	let uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
		let r = (date + Math.random() * 16) % 16 | 0;
		date = Math.floor(date / 16);
		return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
	});
	return uuid;
};

/**
 * @param {Array} array
 * @param {number} indexA
 * @param {number} indexB
 */
window.arraySwap = function arraySwap(array, indexA, indexB) {
	const tmp = array[indexA];
	array[indexA] = array[indexB];
	array[indexB] = tmp;
};

/**
 * @param {string} string
 * @returns {string}
 */
window.capFirstChar = function capFirstChar(string) {
	return string.charAt(0).toUpperCase() + string.substr(1);
};

/**
 * @param {string} word
 * @returns {string}
 */
window.addA = function(word) {
	let vocal = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
	if (vocal.includes(word.charAt(0))) {
		return `an ${word}`;
	}
	return `a ${word}`;
};

/**
 * @param {number} i
 * @returns {string}
 */
window.ordinalSuffix = function ordinalSuffix(i) {
	let j = i % 10;
	let k = i % 100;
	if (j === 1 && k !== 11) {
		return `${i}st`;
	}
	if (j === 2 && k !== 12) {
		return `${i}nd`;
	}
	if (j === 3 && k !== 13) {
		return `${i}rd`;
	}
	return `${i}th`;
};

/**
 * @param {number} i
 * @returns {string}
 */
window.ordinalSuffixWords = function(i) {
	const text = ["zeroth", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth"];
	if (i < text.length) {
		return text[i];
	}
	return ordinalSuffix(i);
};

/**
 * @param {Iterable<any>} array
 * @returns {any[]}
 */
window.removeDuplicates = function removeDuplicates(array) {
	return [...new Set(array)];
};

/**
 * Maps an index from one list onto a matching index on the other.
 * The first and last indexes will be matched to the first and last indexes of the other list,
 * while indexes in between will go to the nearest index.
 * @param {number} index The index in original list to map to new list.
 * @param {*} originalList The original list the index refers into.
 * @param {*} newList The new list which we want an index for
 * @returns {number} The new index into newList
 */
App.Utils.mapIndexBetweenLists = function(index, originalList, newList) {
	if (index === 0) { return 0; }
	if (index === originalList.length - 1) { return newList.length - 1; }
	index--;
	const originalLimitedLength = originalList.length - 2;
	const newLimitedLength = newList.length - 2;
	return Math.round((index / originalLimitedLength) * newLimitedLength) + 1;
};

/**
 * replaces special HTML characters with their '&xxx' forms
 * @param {string} text
 * @returns {string}
 */
App.Utils.escapeHtml = function(text) {
	const map = {
		'&': '&',
		'<': '<',
		'>': '>',
		'"': '"',
		"'": '''
	};
	return text.replace(/[&<>"']/g, m => map[m]);
};

/**
 * Creates an object where the items are accessible via their ids.
 *
 * @param {Iterable} list
 * @return {{}}
 */
window.mapIdList = function(list) {
	let mappedList = {};
	for (const item of list) {
		mappedList[item.id] = item;
	}
	return mappedList;
};

/**
 * Topological sorting algorithm
 * https://gist.github.com/shinout/1232505
 * Added keys parameter since it better suits our needs and updated to project code style.
 *
 * @param {[]} keys
 * @param {[[]]} edges: list of edges. each edge forms Array<ID,ID> e.g. [12 , 3]
 * @returns Array: topological sorted list of IDs
 * @throws Error: in case there is a closed chain.
 **/
App.Utils.topologicalSort = function(keys, edges) {
	let nodes = {}; // hash: stringified id of the node => { id: id, afters: list of ids }
	let sorted = []; // sorted list of IDs ( returned value )
	let visited = {}; // hash: id of already visited node => true

	const Node = function(id) {
		this.id = id;
		this.afters = [];
	};

	// 1. build data structures
	keys.forEach(key => {
		nodes[key] = new Node(key);
	});

	edges.forEach(edge => {
		const from = edge[0], to = edge[1];
		if (!nodes[from]) { nodes[from] = new Node(from); }
		if (!nodes[to]) { nodes[to] = new Node(to); }
		nodes[from].afters.push(to);
	});

	// 2. topological sort
	Object.keys(nodes).forEach(function visit(idstr, ancestors) {
		let node = nodes[idstr];
		let id = node.id;

		// if already exists, do nothing
		if (visited[idstr]) { return; }

		if (!Array.isArray(ancestors)) { ancestors = []; }

		ancestors.push(id);

		visited[idstr] = true;

		node.afters.forEach((afterID) => {
			if (ancestors.indexOf(afterID) >= 0) { // if already in ancestors, a closed chain exists.
				throw new Error('closed chain : ' + afterID + ' is in ' + id);
			}

			visit(afterID.toString(), ancestors.map(v => v)); // recursive call
		});

		sorted.unshift(id);
	});

	return sorted;
};