diff --git a/js/rulesAssistant/conditionEditor.js b/js/rulesAssistant/conditionEditor.js
index 88a6ac2ba08643609311ea0a8be4656789d9a5ac..e6f4ec6be0004e6857e6223bd5ec849d95752fb4 100644
--- a/js/rulesAssistant/conditionEditor.js
+++ b/js/rulesAssistant/conditionEditor.js
@@ -533,9 +533,9 @@ App.RA.Activation.Editor = (function() {
 	}
 
 	/**
-	 * @typedef {"sub" | "div" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte"| "substr"} RulePairComparators
+	 * @typedef {"sub" | "div" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte"| "substr" | "match"} RulePairComparators
 	 * @typedef {object} RulePairComparatorDisplay
-	 * @property {string} key
+	 * @property {RulePairComparators} key
 	 * @property {string} name
 	 * @type {RulePairComparatorDisplay[]}
 	 */
@@ -549,6 +549,7 @@ App.RA.Activation.Editor = (function() {
 		{key: "sub", name: "-"},
 		{key: "div", name: "/"},
 		{key: "substr", name: "Contains"},
+		{key: "match", name: "Matches"},
 	];
 
 	class RulePair extends RuleContainer {
@@ -604,7 +605,7 @@ App.RA.Activation.Editor = (function() {
 					errorList.push("Both sides need to return the same type.");
 					return "error";
 				}
-			} else if (this.mode === "substr") {
+			} else if (this.mode === "substr" || this.mode === "match") {
 				if (this._child1.validate(errorList) === "string" && this._child2.validate(errorList) === "string") {
 					return "number";
 				} else {
@@ -1123,7 +1124,7 @@ App.RA.Activation.Editor = (function() {
 		}
 
 		/**
-		 * @param {"sub" | "div" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "substr"} mode
+		 * @param {"sub" | "div" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "substr" | "match"} mode
 		 */
 		function makePair(mode) {
 			const pair = new RulePair(mode);
@@ -1162,6 +1163,7 @@ App.RA.Activation.Editor = (function() {
 			["lt", () => makePair("lt")],
 			["lte", () => makePair("lte")],
 			["substr", () => makePair("substr")],
+			["match", () => makePair("match")],
 			["not", () => {
 				const negate = new RuleNegate();
 				negate.child = stack.popRulePart();
diff --git a/js/rulesAssistant/conditionEvaluation.js b/js/rulesAssistant/conditionEvaluation.js
index c2c2afca4d6ece627ba74ed9480857c2667875c4..e3c50573c7529e77a4f7cab7d70c471bd788ca74 100644
--- a/js/rulesAssistant/conditionEvaluation.js
+++ b/js/rulesAssistant/conditionEvaluation.js
@@ -656,15 +656,28 @@ App.RA.Activation.populateGetters = function() {
 	// Strings
 	gm.addString("label", {name: "Label", description: "Assigned Label", val: c => c.slave.custom.label});
 	gm.addString("genes", {
-		name: "Sex", description: "Genetic sex: Male: XX, Female: XY",
+		name: "Genetic sex", description: "Genetic sex: Female: XX, Male: XY",
 		val: c => c.slave.genes
 	});
+	gm.addString("phenotypesex", {
+		name: "Phenotype sex",
+		description: "A three-character string that encodes state of sexual organs" +
+			" as female('X'), male('Y'), both ('H'), or absent ('-'): vagina/dick, ovaries/balls, tits",
+		val: c => phenotypeSex(c.slave)
+	});
 	gm.addString("fetish", {
 		name: "Fetish",
 		description: "One of 'buttslut', 'cumslut', 'masochist', 'sadist', 'dom', 'submissive', 'boobs', " +
 			"'pregnancy', 'none' (AKA vanilla)",
 		val: c => c.slave.fetish
 	});
+	gm.addString("title", {
+		name: "Title",
+		description: "Slave title (class) without adjectives (slavegirl, MILF, bimbo, shemale, herm, etc.). ",
+		requirements: "The alternate titles game option is enabled",
+		enabled: () => V.newDescriptions === 1,
+		val: c => SlaveTitle(c.slave, false, false)
+	});
 };
 
 /**
@@ -750,6 +763,10 @@ App.RA.Activation.evaluate = function(slave, rule) {
 			const value = stack.popString();
 			stack.pushBoolean(stack.popString().includes(value));
 		}],
+		["match", () => {
+			const value = stack.popString();
+			stack.pushBoolean(stack.popString().match(value) !== null);
+		}],
 		["not", () => stack.pushBoolean(stack.popNumber() === 0)],
 		["ternarystr", () => {
 			const ifFalse = stack.popString();
diff --git a/src/futureSocieties/futureSociety.js b/src/futureSocieties/futureSociety.js
index ef28cb60a6c758131e22069205cd3437a83f2d57..a6245aa4d737ae9bc05f36cefe8b1f83065ea8fa 100644
--- a/src/futureSocieties/futureSociety.js
+++ b/src/futureSocieties/futureSociety.js
@@ -28,6 +28,7 @@ globalThis.FutureSocieties = (function() {
 		HighestDecoration: FSHighestDecoration,
 		arcSupport: arcSupport,
 		researchAvailable,
+		isActive,
 	};
 
 	/** get the list of FSes active for a particular arcology
@@ -810,4 +811,14 @@ globalThis.FutureSocieties = (function() {
 	function researchAvailable(fs, arcology) {
 		return (arcology || V.arcologies[0])[`FS${fs}Research`] === 1;
 	}
+
+	/**
+	 * Checks if the given FS active (i.e. not "unset")
+	 * @param {FC.FutureSociety} fs
+	 * @param {FC.ArcologyState} [arcology] Arcology to test, defaults to the PC's arcology
+	 * @returns {boolean}
+	 */
+	function isActive(fs, arcology) {
+		return (arcology || V.arcologies[0])[fs] !== "unset";
+	}
 })();
diff --git a/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js b/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js
index 5fa62bc7cac8f4e6b6381f63d5ccd45a69fa8c02..6347e8031d5b2a1d93336bb8f52cdd6c80913738 100644
--- a/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js
+++ b/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js
@@ -68,6 +68,8 @@ App.Encyclopedia.addArticle("RA Condition Editor", function() {
 		transformerRow(el, "/", "Divides the second value by the first value", "Number");
 		transformerRow(el, "Contains", "True, if the second value is somewhere in the first value",
 			"String");
+		transformerRow(el, "Matches", "True, if the first value matches the regular expression in the second value",
+			"String");
 		transformerRow(el, "Not …", "Negates the input value.", "Boolean");
 		transformerRow(el, "If … Then … Else …",
 			"If the first value is true, returns the second value, otherwise the third value. The second " +
diff --git a/src/js/statsChecker/statsChecker.js b/src/js/statsChecker/statsChecker.js
index 781a6287e8d5ec70199e231f7d5e571115768236..c1c4133cd75ee5f024dcb0424c8d5a1576470d7c 100644
--- a/src/js/statsChecker/statsChecker.js
+++ b/src/js/statsChecker/statsChecker.js
@@ -628,7 +628,7 @@ globalThis.isFertile = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState | App.Entity.PlayerState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canAchieveErection = function(slave) {
@@ -653,7 +653,7 @@ globalThis.canAchieveErection = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canPenetrate = function(slave) {
@@ -674,7 +674,7 @@ globalThis.canPenetrate = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canSee = function(slave) {
@@ -686,7 +686,7 @@ globalThis.canSee = function(slave) {
 
 /**
  *
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canSeePerfectly = function(slave) {
@@ -705,7 +705,7 @@ globalThis.canSeePerfectly = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canHear = function(slave) {
@@ -716,7 +716,7 @@ globalThis.canHear = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canSmell = function(slave) {
@@ -727,7 +727,7 @@ globalThis.canSmell = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canTaste = function(slave) {
@@ -738,7 +738,7 @@ globalThis.canTaste = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canHold = function(slave) {
@@ -751,7 +751,7 @@ globalThis.canHold = function(slave) {
 };
 
 /** If a slave can walk, she can move and stand.
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canWalk = function(slave) {
@@ -780,7 +780,7 @@ globalThis.canWalk = function(slave) {
 };
 
 /** If a slave can stand, she can move, but not necessarily walk.
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.canStand = function(slave) {
@@ -968,7 +968,7 @@ globalThis.canDoVaginal = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.tooFatSlave = function(slave) {
@@ -987,7 +987,7 @@ globalThis.tooFatSlave = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.tooBigBreasts = function(slave) {
@@ -1006,7 +1006,7 @@ globalThis.tooBigBreasts = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.tooBigBelly = function(slave) {
@@ -1025,7 +1025,7 @@ globalThis.tooBigBelly = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.tooBigBalls = function(slave) {
@@ -1042,7 +1042,7 @@ globalThis.tooBigBalls = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.tooBigDick = function(slave) {
@@ -1059,7 +1059,7 @@ globalThis.tooBigDick = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {boolean}
  */
 globalThis.tooBigButt = function(slave) {
@@ -1131,6 +1131,10 @@ globalThis.canBeReceptrix = function(slave) {
 	);
 };
 
+/**
+ * @param {FC.HumanState} slave
+ * @returns {string}
+ */
 globalThis.milkFlavor = function(slave) {
 	if (slave.milkFlavor === "none") {
 		return ``;
diff --git a/src/js/utilsAssessSlave.js b/src/js/utilsAssessSlave.js
index 39bf59bcf3e96d9e135c1cacd80d7d6ca0f88c9f..d3233de8c6d292b48cd90f55c44e926894e034e3 100644
--- a/src/js/utilsAssessSlave.js
+++ b/src/js/utilsAssessSlave.js
@@ -120,7 +120,7 @@ globalThis.maxHeight = function(slave) {
 	if (slave.geneticQuirks.neoteny === 2 && slave.physicalAge > 12) { /* Limit neoteny slaves to 12 year old max height */
 		max = Math.clamp(((Height.mean(slave.nationality, slave.race, slave.genes, 12) * 1.25) + slave.heightImplant * 10), 0, 274);
 	}
-	
+
 	if (slave.geneticQuirks.dwarfism === 2 && slave.geneticQuirks.gigantism !== 2) {
 		max = Math.min(max, 160);
 	}
@@ -249,7 +249,7 @@ globalThis.canMoveToRoom = function(slave) {
 };
 
 /**
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @returns {0|1|2|3} 0: No heel boost at all. 1: Pumps, slight boost. 2: High heels. 3: Painfully/extreme high heels
  */
 globalThis.shoeHeelCategory = function(slave) {
diff --git a/src/js/utilsSlave.js b/src/js/utilsSlave.js
index 1b13711a4f918af6d3f017b5fee947edacc6b559..e99c9c32cbdf351467137e6631fae0a86ce00f7b 100644
--- a/src/js/utilsSlave.js
+++ b/src/js/utilsSlave.js
@@ -2169,13 +2169,15 @@ globalThis.PoliteRudeTitle = function(slave) {
 
 /**
  * @param {App.Entity.SlaveState} slave
+ * @param {boolean} [adjective=true] Add adjectives
+ * @param {boolean} [variability=true] Use alternative titles
  * @returns {string}
  */
-globalThis.SlaveTitle = function(slave) {
+globalThis.SlaveTitle = function(slave, adjective = true, variability = true) {
 	let r;
 	if (V.newDescriptions === 1) {
 		if (slave.dick > 0 && slave.balls > 0 && slave.boobs > 300 && slave.vagina > -1 && slave.ovaries === 1) {
-			if (jsRandom(1, 100) > 50) {
+			if (variability && jsRandom(1, 100) > 50) {
 				r = "futanari";
 			} else {
 				r = "herm";
@@ -2236,6 +2238,10 @@ globalThis.SlaveTitle = function(slave) {
 			r = "slave";
 		}
 
+		if (!adjective) {
+			return r;
+		}
+
 		if (slave.visualAge < 13) {
 			if (slave.actualAge < 3) {
 				if (slave.actualAge < 1) {
@@ -2548,6 +2554,20 @@ globalThis.SlaveTitle = function(slave) {
 	return r;
 };
 
+/**
+ * Phenotype sex: a three-character string that encodes state of sexual organs
+ * as female('X'), male('Y'), both ('H'), or absent ('-'): vagina/dick, ovaries/balls, tits
+ * @param {FC.HumanState} human
+ * @returns {string}
+ */
+globalThis.phenotypeSex = function(human) {
+	const encode = (haveFemale, haveMale) =>
+		'-XYH'[haveFemale + haveMale * 2];
+	return encode(human.vagina > -1, human.dick > 0) +
+		encode(human.ovaries > 0, human.balls > 0) +
+		encode(human.boobs > 0, false);
+};
+
 /**
  * @param {App.Entity.SlaveState} slave
  */