code:
:: FamilyTreeJS [script]
'use strict';

var lastActiveSlave, lastSlaves, lastPC;

/*
  To use, add something like:

		<<link "Pull up the file on her family tree.">>
		<<replace #familyTreeLink>>
			<<run renderFamilyTree($slaves, $activeSlave.ID)>>
		<</replace>>
	<</link>>

*/

window.renderFamilyTree = function(slaves, filterID) {

	var ftreeWidth,ftreeHeight;
	var chartWidth, chartHeight;
	var margin;
	d3.select('#ftree-canvas').remove();
	var svg = d3.select('#familyTree')
		.append('svg')
		.attr('id','ftree-canvas');
	var chartLayer = svg.append('g').classed('chartLayer', true);

	var range = 100;
	var data = buildFamilyTree(slaves, filterID);

	initFtreeSVG(data);
	runFtreeSim(data);

	function initFtreeSVG(data) {
		ftreeWidth = data.nodes.length * 45;
		ftreeHeight = data.nodes.length * 35;
		if(ftreeWidth < 600) {
			ftreeWidth = 600;
		} else if(ftreeWidth > 1920) {
			ftreeWidth = 1920;
		}
		if(ftreeHeight < 480) {
			ftreeHeight = 480;
		} else if(ftreeHeight > 1200) {
			ftreeHeight = 1200;
		}

		margin = {top:0, left:0, bottom:0, right:0 }

		chartWidth = ftreeWidth - (margin.left+margin.right)
		chartHeight = ftreeHeight - (margin.top+margin.bottom)

		svg.attr('width', ftreeWidth).attr('height', ftreeHeight)

		var defs = svg.append('defs');

		svg.append('defs').append('marker')
			.attr('id','arrowhead')
			.attr('viewBox','-0 -5 10 10')
			.attr('refX',13)
			.attr('refY',0)
			.attr('orient','auto')
			.attr('markerWidth',13)
			.attr('markerHeight',13)
			.attr('xoverflow','visible')
			.append('svg:path')
			.attr('d', 'M 0,-1 L 5,0 L 0,1')
			.attr('fill', '#a1a1a1')
			.style('stroke','none');

		chartLayer
			.attr('width', chartWidth)
			.attr('height', chartHeight)
			.attr('transform', 'translate('+[margin.left, margin.top]+')')
	}

	function runFtreeSim(data) {
		var simulation = d3.forceSimulation()
			.force('link', d3.forceLink().id(function(d) { return d.index }))
			.force('collide',d3.forceCollide( function(d){ return 60; }).iterations(4) )
			.force('charge', d3.forceManyBody().strength(-200).distanceMin(100).distanceMax(1000))
			.force('center', d3.forceCenter(chartWidth / 2, chartHeight / 2))
			.force('y', d3.forceY(100))
			.force('x', d3.forceX(200))

		var link = svg.append('g')
			.attr('class', 'link')
			.selectAll('link')
			.data(data.links)
			.enter()
			.append('line')
			.attr('marker-end','url(#arrowhead)')
			.attr('stroke', function(d) {
				if(d.type == 'homologous') {
					return '#862d59';
				} else if(d.type == 'paternal') {
					return '#24478f';
				} else {
					return '#aa909b';
				}
			})
			.attr('stroke-width', 2)
			.attr('fill', 'none');

		var node = svg.selectAll('.node')
			.data(data.nodes)
			.enter().append('g')
			.attr('class', 'node')
			.call(d3.drag()
			.on('start', dragstarted)
			.on('drag', dragged)
			.on('end', dragended));

		node.append('circle')
			.attr('r', function(d){  return d.r })
			.attr('stroke', function(d) {
				if(d.ID == filterID) {
					return '#ffff20';
				} else {
					return '#5a5a5a';
				}
			})
			.attr('class', 'node-circle')
			.attr('r', 20);

		node.append('text')
			.text(function(d) {
				var ssym;
				if(d.ID == -1) {
					if(d.dick == 1 && d.vagina == 1) {
						ssym = '☿'
					} else if (d.dick == 1) {
						ssym = '♂';
					} else if (d.vagina == 1) {
						ssym = '♀';
					}
				} else if (d.dick > 0 && d.vagina > -1) {
					ssym = '☿'
				} else if (d.dick > 0) {
					ssym = '♂';
				} else if (d.vagina > -1) {
					ssym = '♀';
				} else {
					ssym = '?';
				}
				return d.name + '('+ssym+')';
			})
			.attr('dy', 4)
			.attr('dx', function(d) { return -(8*d.name.length)/2; })
			.attr('class', 'node-text')
			.style('fill', function(d) {
				if(d.is_mother && d.is_father) {
					return '#b84dff';
				} else if(d.is_father) {
					return '#00ffff';
				} else if(d.is_mother) {
					return '#ff3399';
				} else if(d.unborn) {
					return '#a3a3c2';
				} else {
					return '#66cc66';
				}
			});

		var circles = svg.selectAll('.node-circle');
		var texts = svg.selectAll('.node-text');

		var ticked = function() {
			link
				.attr('x1', function(d) { return d.source.x; })
				.attr('y1', function(d) { return d.source.y; })
				.attr('x2', function(d) { return d.target.x; })
				.attr('y2', function(d) { return d.target.y; });

			node
				.attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";});
		}

		simulation.nodes(data.nodes)
			.on('tick', ticked);

		simulation.force('link')
			.links(data.links);

		function dragstarted(d) {
			if (!d3.event.active) simulation.alphaTarget(0.3).restart();
			d.fx = d.x;
			d.fy = d.y;
		}

		function dragged(d) {
			d.fx = d3.event.x;
			d.fy = d3.event.y;
		}

		function dragended(d) {
			if (!d3.event.active) simulation.alphaTarget(0);
			d.fx = null;
			d.fy = null;
		}

	}
};

window.buildFamilyTree = function(slaves = State.variables.slaves, filterID) {
	var family_graph = {
		nodes: [],
		links: []
	};
	var node_lookup = {};
	var preset_lookup = {
		'-2': 'Social elite',
		'-3': 'Client',
		'-4': 'Former master',
		'-5': 'An arcology owner',
		'-6': 'A citizen'
	};
	var outdads = {};
	var outmoms = {};
	var kids = {};

	var fake_pc = {
		slaveName: State.variables.PC.name + '(You)',
		mother: State.variables.PC.mother,
		father: State.variables.PC.father,
		dick: State.variables.PC.dick,
		vagina: State.variables.PC.vagina,
		ID: State.variables.PC.ID
	};
	var charList = [fake_pc];
	charList.push.apply(charList, slaves);
	charList.push.apply(charList, State.variables.tanks);

	var unborn = {};
	for(var i = 0; i < State.variables.tanks.length; i++) {
		unborn[State.variables.tanks[i].ID] = true;
	}

	for(var i  = 0; i < charList.length; i++) {
		var mom = charList[i].mother;
		var dad = charList[i].father;

		if(mom) {
			if(!kids[mom]) {
				kids[mom] = {};
			}
			kids[mom].mother = true;
		}
		if(dad) {
			if(!kids[dad]) {
				kids[dad] = {};
			}
			kids[dad].father = true;
		}
	}

	for(var i  = 0; i < charList.length; i++) {
		var character = charList[i];
		if(character.mother == 0 && character.father == 0 && !kids[character.ID]) {
			continue;
		}
		var mom = character.mother;
		if(mom < -6) {
			if(typeof outmoms[mom] == 'undefined') {
				outmoms[mom] = [];
			}
			outmoms[mom].push(character.slaveName);
		} else if(mom < 0 && typeof node_lookup[mom] == 'undefined' && typeof preset_lookup[mom] != 'undefined') {
			node_lookup[mom] = family_graph.nodes.length;
			charList.push({ID: mom, mother: 0, father: 0, is_father: true, dick: 0, vagina: 1, slaveName: preset_lookup[mom]});
		}

		var dad = character.father;
		if(dad < -6) {
			if(typeof outdads[dad] == 'undefined') {
				outdads[dad] = [];
			}
			outdads[dad].push(character.slaveName);
		} else if(dad < 0 && typeof node_lookup[dad] == 'undefined' && typeof preset_lookup[dad] != 'undefined') {
			node_lookup[dad] = family_graph.nodes.length;
			charList.push({ID: dad, mother: 0, father: 0, is_father: true, dick: 1, vagina: -1, slaveName: preset_lookup[dad]});
		}
	}
	var mkeys = Object.keys(outmoms);
	for(var i = 0; i < mkeys.length; i++) {
		var name;
		var key = mkeys[i];
		var names = outmoms[key];
		if(names.length == 1) {
			name = names[0];
		} else if(names.length == 2) {
			name = names.join(' and ');
		} else {
			names[-1] = 'and '+names[-1];
			name = names.join(', ');
		}
		node_lookup[key] = family_graph.nodes.length;
		//Outside extant slaves set
		charList.push({ID: key, mother: 0, father: 0, is_mother: true, dick: 0, vagina: 1, slaveName: name+"'s mother"});
	}

	var dkeys = Object.keys(outdads);
	for(var i = 0; i < dkeys.length; i++) {
		var name;
		var key = dkeys[i];
		var names = outdads[key];
		if(names.length == 1) {
			name = names[0];
		} else if(names.length == 2) {
			name = names.join(' and ');
		} else {
			names[-1] = 'and '+names[-1];
			name = names.join(', ');
		}
		node_lookup[key] = family_graph.nodes.length;
		//Outside extant slaves set
		charList.push({ID: key, mother: 0, father: 0, is_father: true, dick: 1, vagina: -1, slaveName: name+"'s father"});
	}

	var charHash = {};
	for(var i = 0; i < charList.length; i++) {
		charHash[charList[i].ID] = charList[i];
	}

	var related = {};
	var seen = {};
	var saveTree = {};
	function relatedTo(character, targetID, relIDs = {tree: {}, related: false}) {
		relIDs.tree[character.ID] = true;
		if(related[character.ID]) {
			relIDs.related = true;
			return relIDs;
		}
		if(character.ID == targetID) {
			relIDs.related = true;
		}
		if(seen[character.ID]) {
			return relIDs;
		}
		seen[character.ID] = true;
		if(character.mother != 0) {
			if(charHash[character.mother]) {
				relatedTo(charHash[character.mother], targetID, relIDs);
			}
		}
		if(character.father != 0) {
			if(charHash[character.father]) {
				relatedTo(charHash[character.father], targetID, relIDs);
			}
		}
		return relIDs;
	}
	if(filterID) {
		if(charHash[filterID]) {
			var relIDs = relatedTo(charHash[filterID], filterID);
			for(var k in relIDs.tree) {
				related[k] = true;
			}
			for(var i  = 0; i < charList.length; i++) {
				if(charHash[charList[i].ID]) {
					var pRelIDs = relatedTo(charHash[charList[i].ID], filterID);
					if(pRelIDs.related) {
						for(var k in pRelIDs.tree) {
							related[k] = true;
							if(saveTree[k]) {
								for(var k2 in saveTree[k].tree) {
									related[k2] = true;
								}
							}
						}
					}
					saveTree[charList[i].ID] = pRelIDs;
				}
			}
		}
	}

	for(var i  = 0; i < charList.length; i++) {
		var character = charList[i];
		var char_id = character.ID;
		if(character.mother == 0 && character.father == 0 && !kids[char_id]) {
			continue;
		}
		if(filterID && !related[char_id]) {
			continue;
		}
		node_lookup[char_id] = family_graph.nodes.length;
		var char_obj = {
			ID: char_id,
			name: character.slaveName,
			dick: character.dick,
			unborn: !!unborn[char_id],
			vagina: character.vagina
		};
		if(kids[char_id]) {
			char_obj.is_mother = !!kids[char_id].mother;
			char_obj.is_father = !!kids[char_id].father;
		}	else {
			char_obj.is_mother = false;
			char_obj.is_father = false;
		}
		family_graph.nodes.push(char_obj);
	}

	for(var i = 0; i < charList.length; i++) {
		var character = charList[i];
		var char_id = character.ID;
		if(character.mother == 0 && character.father == 0 && !kids[char_id]) {
			continue;
		}
		if(filterID && !related[char_id]) {
			if(related[character.mother]) {
				console.log('wtf, mom');
			}
			if(related[character.father]) {
				console.log('wtf, dad');
			}
			continue;
		}
		if(typeof node_lookup[character.mother] != 'undefined') {
			var ltype;
			if(character.mother == character.father) {
				ltype = 'homologous';
			} else {
				ltype = 'maternal';
			}
			family_graph.links.push({
				type: ltype,
				target: node_lookup[char_id]*1,
				source: node_lookup[character.mother]*1
			});
		}
		if(character.mother == character.father) {
			continue;
		}
		if(typeof node_lookup[character.father] != 'undefined') {
			family_graph.links.push({
				type: 'paternal',
				target: node_lookup[char_id]*1,
				source: node_lookup[character.father]*1
			});
		}
	}
	return family_graph;
};

/*Old version. To use, do something like:

<<run updateFamilyTree($activeSlave, $slaves, $PC)>>

If you want to update the tree, just re-call the run line.

If there's no active slave, you can do:

<<run updateFamilyTree(null, $slaves, $PC)>>
*/

window.updateFamilyTree = function(activeSlave = lastActiveSlave, slaves = lastSlaves, PC = lastPC) {
  lastActiveSlave = activeSlave;
  lastSlaves = slaves;
  lastPC = PC;
  var treeDepth = 0;
  var numTreeNodes = 0;

  var graphElement = document.getElementById("graph");
  if(!graphElement)
    return;
  graphElement.innerHTML = "";

  /* The way this code works is that we start with the activeSlave then we call
     slaveInfo() recursively to work our way up the tree finding their parents.

   */

  function getSlave(id, expectedGenes) {
    if(id == -1) {
      return {"slaveName":"YOU", "ID":id, "physicalAge":PC.physicalAge, "genes":PC.genes, father:PC.father, mother:PC.mother};
    }
    if(id == 0) {
      return {"slaveName":"-", "ID":id, "genes":expectedGenes};
    }
    if(id == activeSlave.ID) {
      return activeSlave;
    }
    for(var i = 0; i < slaves.length; ++i) {
      if(slaves[i].ID == id)
        return slaves[i];
    }
    return {"slaveName":"-", "ID":id, "genes":expectedGenes};
  }

  function slaveInfo(slave, activeSlaveId, recursionProtectSlaveId = {}) {
    numTreeNodes = 0;
    treeDepth = 0;
    if(recursionProtectSlaveId[slave.ID]) {
      console.log("Recursion protection");
      return slaveInfo_(slave, activeSlaveId);
    }
    recursionProtectSlaveId[slave.ID] = true;

    if(typeof slave.father == "undefined" || typeof slave.mother == "undefined")
       return slaveInfo_(slave, activeSlaveId);

    if(slave.father == -1 || slave.mother == -1) {
      return slaveInfo(getSlave(-1), activeSlaveId, recursionProtectSlaveId);
    }
    if(slave.father != 0) {
      return slaveInfo(getSlave(slave.father, "unknownXY"), activeSlaveId, recursionProtectSlaveId);
    }

    if(slave.mother != 0) {
      return slaveInfo(getSlave(slave.mother, "unknownXX"), activeSlaveId, recursionProtectSlaveId);
    }
    return slaveInfo_(slave, activeSlaveId);
  }
  function slaveInfo_(slave, activeSlaveId, slavesAdded={}, depth = 1) {
    numTreeNodes += 1;
    treeDepth = Math.max(treeDepth, depth);
    var shouldAddChildren = false;
    if(!slavesAdded[slave.ID]) {
      shouldAddChildren = true;
      slavesAdded[slave.ID] = true;
    }
    var data = {
      "name": slave.slaveName + (slave.physicalAge?(" (" + slave.physicalAge + ")"):""),
      "class" : slave.genes,
      "textClass": (activeSlaveId == slave.ID)?"emphasis":"",
      "marriages": [],
    };

    var spouseToChild = {};

    function maybeAddSpouseToChild(child) {
      if(child.ID == slave.ID)
        return;
      if (child.father == slave.ID) {
        if(!spouseToChild[child.mother]) {
          spouseToChild[child.mother] = []
        }
        spouseToChild[child.mother].push(child);
      } else if (child.mother == slave.ID) {
        if(!spouseToChild[child.father]) {
          spouseToChild[child.father] = []
        }
        spouseToChild[child.father].push(child);
      }
    }

    if(activeSlave.ID != PC.ID)
      maybeAddSpouseToChild(activeSlave);
    maybeAddSpouseToChild(getSlave(-1));

    for(var i = 0; i < slaves.length; ++i) {
      var child = slaves[i];
      if(child.ID != activeSlave.ID)
        maybeAddSpouseToChild(child);
    }

    for(var key in spouseToChild) {
      if(spouseToChild.hasOwnProperty(key)) {
        var children = shouldAddChildren?spouseToChild[key]:[];
        var spouse = getSlave(key, (slaves.genes=="XX")?"unknownXY":(slaves.genes=="XY")?"unknownXX":"unknown");
        var spouseName;
        if (spouse.ID != slave.ID){
          spouseName = spouse.slaveName + (spouse.physicalAge?(" (" + spouse.physicalAge + ")"):"")
        } else {
          spouseName = (spouse.ID==-1)?"(yourself)":"(themselves)";
        }
        var marriage = {
          "spouse": {"name": spouseName, "class": spouse.genes},
          "children": children.map(function(x) { return slaveInfo_(x, activeSlaveId, slavesAdded, depth+1)} ),
        };
        data.marriages.push(marriage);
      }
    }
    return data;
  }

  if(activeSlave == PC || activeSlave == null)
    activeSlave = getSlave(-1)
  const treeData = [slaveInfo(activeSlave, activeSlave.ID)];
  console.log("Family tree is", treeData, 'and has:', numTreeNodes);

  var parentWidth = document.getElementById('editFamily').offsetWidth;

  console.log(parentWidth, document.getElementById('passages').offsetWidth);
  if(!parentWidth)
    parentWidth = document.body.offsetWidth - 483;

  console.log(parentWidth, Math.min(200 + 40*numTreeNodes,parentWidth-200) + 200);

  dTree.init(treeData, {
	target: "#graph",
	debug: true,
	height: 50 + 50*treeDepth, /* very rough heuristics */
	width: Math.min(200 + 40*numTreeNodes,
                  parentWidth-200) + 200,
	callbacks: {
		nodeClick: function(name, extra) {
		}
	}
	});

};


code:
:: Foreach Macro JS [script]

Macro.add('foreach', {
	skipArgs : true,
	tags     : null,

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

		if (statement.length !== 0) {
			const parts = statement.match(/^(\S+?)\s+of\s+(\S.*?)\s*$/i);
			if(parts !== null) {
				variable = parts[1];
				array = parts[2];
			}
		}
		if(!variable || !array) {
			return this.error('invalid syntax, format: <<foreach variable of array-expression>>... <</foreach>>');
		}

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

		let resultLength = result['length'];

		// We don't check for "instanceof Array" to also be able to pass arguments or other strange objects
		if(typeof resultLength !== 'number' || (resultLength % 1) !== 0) {
			return this.error("bad evaluation: '" + result + "' is not an array or array-like object");
		}

		if(resultLength > Config.macros.maxLoopIterations) {
			return this.error('Array too large for maxLoopIterations (' + resultLength + ' > ' + Config.macros.maxLoopIterations + ')');
		}

		if(!new RegExp(`^(${Patterns.variable})$`).test(variable)) {
			return this.error('not a variable identifier: ' + variable);
		}

		if(resultLength <= 0) {
			// Don't bother with empty stuff
			return;
		}

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

		payload.replace(/^\n/, '');
		for(let i = 0; i < resultLength; ++ i) {
			Wikifier.setValue(variable, result[i]);
			new Wikifier(this.output, payload);
		}
	}
});

repo_name: PresidentConvert/fc


code:
:: FResult [script]

// this is a port of the FResult widget
// it has been broken up into several functions, because it grew too long
// it has been wrapped in a closure so as not to pollute the global namespace
// and so that nested functions are only evaluated once

window.FResult = (function() {
	"use strict";
	// we can't initialise our global variables on load, because SugarCube.State isn't initialised
	// instead, declare them and initialise on run time
	let V, incest_bonus;
	function FResult(slave) {
		V = State.variables;
		incest_bonus = V.arcologies[0].FSEgyptianRevivalist > 20 || V.arcologies[0].FSEgyptianRevivalistIncestPolicy === 1;

		calcUseWeights(slave);
		if (!slave.fuckdoll)
			calcNotFuckdoll(slave);
		else
			V.FResult += slave.fuckdoll/10;

		V.FResult += Math.max(0, slave.aphrodisiacs) * 2;

		if (slave.inflationType === "aphrodisiac")
			V.FResult += slave.inflation*4;

		if (slave.lactation > 0)
			V.FResult += 1;

		if (slave.nipples === "fuckable")
			calcFuckableTits(slave);

		if (V.seeAge === 1)
			calcAge(slave);
		if (slave.fetish === "mindbroken")
			V.FResult = Math.trunc(V.FResult*0.4);
		else
			V.FResult = Math.trunc(V.FResult*0.7);

		if (slave.pregWeek < 0)
			V.FResult -= Math.trunc(V.FResult*slave.pregWeek/10); // reduced the most just after birth

		calcAmputation(slave);

		if (V.arcologies[0].FSHedonisticDecadence > 20)
			calcHedonismWeight(slave);
		if (V.FResult < 2) {
			if (supremeRaceP(slave) && V.arcologies[0].FSSupremacist > 20)
				V.FResult = 0;
			else
				V.FResult = 2;
		}
	}

	function calcUseWeights(slave) {
		V.FResult = (3 - slave.anus)+(slave.muscles/30);
		if (slave.muscles < -95)
			V.FResult -= 5;
		else if (slave.muscles < -30)
			V.FResult -= 2;

		V.seed = V.oralUseWeight + V.vaginalUseWeight + V.analUseWeight;
		if (V.seed <= 0) return;

		V.FResult += (6+slave.tonguePiercing) * (V.oralUseWeight/V.seed) * (slave.oralSkill/30);
		if (slave.sexualFlaw === "cum addict")
			V.FResult += (V.oralUseWeight/V.seed) * (slave.oralSkill/30);
		if (canDoVaginal(slave)) {
			V.FResult += 6 * (V.vaginalUseWeight/V.seed) * (slave.vaginalSkill/30);
			V.FResult += (3 - slave.vagina);
			V.FResult += slave.vaginaLube;
		}
		if (canDoAnal(slave)) {
			V.FResult += 6 * (V.analUseWeight/V.seed) * (slave.analSkill/30);
			if (slave.sexualFlaw === "anal addict")
				V.FResult += (V.analUseWeight/V.seed) * (slave.analSkill/30);
			if (slave.inflationType === "aphrodisiac")
				V.FResult += (V.analUseWeight/V.seed) * (slave.inflation * 3);
		}
	}

	function calcFuckableTits(slave) {
		V.FResult += 2;
		if (slave.fetish === "boobs")
			V.FResult += Math.trunc(slave.fetishStrength/20);
	}

	function calcWorksWithRelatives(slave) {
		V.slaves.forEach(islave => {
			if (isParentP(slave, islave) && sameAssignmentP(slave, islave)) {
				V.FResult += 1;
				if (incest_bonus) V.FResult += 1;
			}
			if (areSisters(slave, islave) > 0 && sameAssignmentP(slave, islave)) {
				V.FResult += 1;
				if (incest_bonus) V.FResult += 1;
			}
		});
	}

	function calcWorksWithRelativesVanilla(slave) {
		const fre = getSlave(slave.relationTarget)
		if (fre !== undefined && sameAssignmentP(slave, fre)) {
			V.FResult += 2;
			if (incest_bonus) V.FResult += 2;
		}
	}

	function calcWorksWithRelationship(slave) {
		const fre = V.slaves.findIndex(s => {
			return haveRelationshipP(slave, s) && sameAssignmentP(slave, s);
		});
		if (fre !== -1) V.FResult += 1;
	}

	function calcWorksWithRival(slave) {
		const en = V.slaves.findIndex(s => {
			return isRivalP(slave, s) && sameAssignmentP(slave, s);
		});
		if (en !== -1) V.FResult -= 1;
	}

	function calcHInjectionsDiet(slave) {
		if (slave.drugs === "male hormone injections" || slave.drugs === "female hormone injections")
			V.FResult -= 10;
		if (slave.diet === "XXY")
			V.FResult += 2;
		else if (slave.diet === "XY" || slave.diet === "XX")
			V.FResult += 1;
		else if (slave.diet === "cum production")
			V.FResult += 1;
		else if (slave.diet === "fertility")
			V.FResult += 1;
	}
	function calcPreg(slave) {
		if (V.arcologies[0].FSRepopulationFocus > 20) {
			if (slave.preg > 10) V.FResult += 2;
			else V.FResult -= 2;
		} else if (V.arcologies[0].FSRestart > 20) {
			if (slave.bellyPreg >= 500 && slave.breedingMark === 1)
				V.FResult += 1;
			else if (slave.preg > 10)
				V.FResult -= 10;
			else
				V.FResult += 0;
		}
	}

	function calcRace(slave) {
		if (V.arcologies[0].FSSupremacist !== "unset" && supremeRaceP(slave))
			V.FResult -= (V.arcologies[0].FSSupremacist/5) + (V.arcologies[0].FSSupremacistLawME*10);
		if (V.arcologies[0].FSSubjugationist !== "unset" && inferiorRaceP(slave))
			V.FResult += (V.arcologies[0].FSSubjugationist/10) + (V.arcologies[0].FSSubjugationistLawME);
	}

	function calcSexAttributes(slave) {
		if (slave.clitPiercing > 2) V.FResult += 1;
		if (slave.fetishKnown === 1 && slave.fetishStrength > 60 && slave.fetish != "none")
			V.FResult += slave.fetishStrength/5;

		if (slave.attrKnown === 1) {
			V.FResult += Math.trunc(slave.attrXX/20);
			V.FResult += Math.trunc(slave.attrXY/20);
			if (slave.energy > 95) V.FResult += 3;
			else if (slave.energy > 80) V.FResult += 2;
			else if (slave.energy > 60) V.FResult += 1;
			else if (slave.energy <= 20) V.FResult -= 2;
			else if (slave.energy <= 40) V.FResult -= 1;
		}
		if (slave.sexualFlaw !== "none") V.FResult -= 2;
		if (slave.sexualQuirk !== "none") V.FResult += 2;
		if (slave.behavioralFlaw !== "none") V.FResult -= 2;
		if (slave.behavioralQuirk !== "none") V.FResult += 2;
	}

	function calcCareer(slave) {
		if (setup.whoreCareers.includes(slave.career))
			V.FResult += 1;
		else if (slave.oralCount + slave.analCount + slave.vaginalCount + slave.mammaryCount + slave.penetrativeCount > 1000)
			V.FResult += 1;
	}

	function calcSight(slave) {
		if (!canSee(slave)) V.FResult -= 3;
		else if (slave.eyes <= -1) {
			if (slave.eyewear !== "corrective glasses" && slave.eyewear !== "corrective contacts")
				V.FResult -= 1;
		} else if (slave.eyewear === "blurring glasses")
			V.FResult -= 1;
		else if (slave.eyewear === "blurring contacts")
			V.FResult -= 1;
	}

	function calcEgyptianBonus(slave) {
		if (V.racialVarieties === undefined) V.racialVarieties = [];
		V.seed = 0;
		V.racialVarieties.forEach(race => {
			if (slave.race === race) V.seed = 1;
		});
		if (V.seed === 0)
			V.racialVarieties.push(slave.race);
	}

	function calcYouthBonus(slave) {
		if (slave.visualAge < 30) {
			if (slave.actualAge > 30)
				V.FResult += 5; // experienced for her apparent age
			if (slave.physicalAge > 30)
				V.FResult -= slave.physicalAge/2; // too old :(
		}
	}

	function calcMatureBonus(slave) {
		if (slave.visualAge >= 30 && slave.actualAge >= 30 && slave.physicalAge < slave.visualAge)
			V.FResult += Math.min((slave.physicalAge - slave.visualAge) * 2, 20); // looks and acts mature, but has a body that just won't quit
	}

	function calcNotFuckdoll(slave) {
		if (V.familyTesting === 1 && totalRelatives(slave) > 0)
			calcWorksWithRelatives(slave);
		else if(!V.familyTesting && slave.relation !==0)
			calcWorksWithRelativesVanilla(slave);
		if (slave.relationship > 0) calcWorksWithRelationship(slave);
		if (slave.rivalry !== 0) calcWorksWithRival(slave);
		calcHInjectionsDiet(slave);
		calcPreg(slave);
		calcRace(slave);
		calcSexAttributes(slave);
		calcCareer(slave);
		calcSight(slave);
		if (V.arcologies[0].FSEgyptianRevivalist !== "unset")
			calcEgyptianBonus(slave);
		if (V.arcologies[0].FSYouthPreferentialist !== "unset")
			calcYouthBonus(slave);
		else if (V.arcologies[0].FSMaturityPreferentialist !== "unset")
			calcMatureBonus(slave);
	}

	function calcAge(slave) {
		if ((V.arcologies[0].FSRepopulationFocus !== "unset" || V.arcologies[0].FSGenderFundamentalist !== "unset") && slave.physicalAge === V.minimumSlaveAge && slave.physicalAge === V.fertilityAge && canGetPregnant(slave)) {
			V.FResult += 1;
			if (slave.birthWeek === 0) V.FResult += V.FResult;
			else if (slave.birthWeek < 4) V.FResult += 0.2*V.FResult;
		} else if (slave.physicalAge === V.minimumSlaveAge) {
			V.FResult += 1;
			if (slave.birthWeek === 0 ) V.FResult += 0.5*V.FResult;
			else if (slave.birthWeek < 4) V.FResult += 0.1*V.FResult;
		} else if (slave.physicalAge === V.fertilityAge && canGetPregnant(slave) && (V.arcologies[0].FSRepopulationFocus !== "unset" || V.arcologies[0].FSGenderFundamentalist !== "unset")) {
			V.FResult += 1;
			if (slave.birthWeek === 0)
				V.FResult += 0.5*V.FResult;
			else if (slave.birthWeek < 4)
				V.FResult += 0.1*V.FResult;
		}
	}

	function calcAmputation(slave) {
		switch(slave.amp) {
		case 0:
			break;
		case 1:
			V.FResult -= 2;
			break;
		case -2:
			break;
		case -5:
			break;
		default:
			V.FResult -= 1;
		}
	}

	function calcHedonismWeight(slave) {
		if (slave.weight < 10)
			V.FResult -= 2;
		else if (slave.weight > 190)
			V.FResult -= 5; // too fat
	}
	return FResult;
})();


code:
:: HTAG Macro JS [script]

/*
 * <<htag>> macro
 * A simple macro which allows to create wrapping html elements with dynamic IDs.
 *      idea blatantly robbed from the spanMacroJS.tw  but expanded to a more generic
 *      case, allowing <div>, <button> or whatever you want. elements, default is for
 *      the div though.  In addition, you can pass an object in as the first argument
 *      instead of an id, and each of the object's attributes will become attributes
 *      of the generate tag.
 *
 * Usage: <<htag id>>...<</htag>>
 * Usage: <<htag id tag>>...<</htag>>
 * Usage: <<htag atributes>>...<</htag>>
 * Usage: <<htag atributes tag>>...<</htag>>
 */
Macro.add('htag', {
	tags: null,
	handler() {
		const payload = this.payload[0].contents.replace(/(^\n+|\n+$)/, '');
		let htag = 'div';
		let attributes;
		function munge (val, key)  {
			return  key + '="' + val + '"';
		}

		if (1  >  this.args.length)
			return this.error('invalid syntax, format: <<htag [id [ tag ] | attributes [ tag ] >>');
		if (1  <  this.args.length)
			htag = String(this.args[1]).trim();
		if ("object" === typeof this.args[0])
			attributes = $.map(this.args[0], munge).join(" ");
		else
			attributes = 'id="' + String(this.args[0]).trim() + '"';
		if (Config.debug)
			this.debugView.modes({block: true});

		jQuery('<' + htag + ' ' + attributes + ' />')
			.wiki(payload)
			.appendTo(this.output);
	}
});


code:
:: heroCreator.tw [script]

window.getHeroSlave = function(heroSlave, baseHeroSlave) {
    var newSlave = clone(baseHeroSlave);
    for (var attrname in heroSlave) {
        newSlave[attrname] = heroSlave[attrname];
    };
    return newSlave;
}


code:
:: pregJS [script]

/*Major props to the anons who worked together to forge the Super Pregnancy Project. Let your legacy go unforgotten.*/
window.getPregBellySize = function(s) {
	var gestastionWeek = s.preg;
	var fetuses = s.pregType;
	var phi = 1.618;

	if(gestastionWeek <= 32) {
		var targetLen = ((0.00006396 * Math.pow(gestastionWeek, 4)) - (0.005501 * Math.pow(gestastionWeek, 3)) + (0.161 * Math.pow(gestastionWeek, 2)) - (0.76 * gestastionWeek) + 0.208);
	} else if(gestastionWeek <= 106) {
		var targetLen = ((-0.0000004675 * Math.pow(gestastionWeek, 4)) + (0.0001905 * Math.pow(gestastionWeek, 3)) - (0.029 * Math.pow(gestastionWeek, 2)) + (2.132 * gestastionWeek) - 16.575);
	} else {
		var targetLen = ((-0.00003266 * Math.pow(gestastionWeek,2)) + (0.076 * gestastionWeek) + 43.843);
	}

	var bellySize = ((4 / 3) * (Math.PI) * (phi / 2) * (Math.pow((targetLen / 2), 3)) * fetuses);
	return bellySize;
};

window.bellyAdjective = function(slave) {
	slave = slave || State.variables.activeSlave;
	if(slave.belly >= 1500) {
		if(slave.belly >= 1000000) {
			if(slave.preg > 10) {
				return 'unfathomably distended, brimming with life';
			} else {
				return 'unfathomable';
			}
		} else if(slave.belly >= 750000) {
			if(slave.preg > 10) {
				return 'monolithic bulging';
			} else {
				return 'monolithic';
			}
		} else if(slave.belly >= 600000) {
			if(slave.preg > 10) {
				return 'titanic bulging';
			} else {
				return 'titanic';
			}
		} else if(slave.belly >= 450000) {
			if(slave.preg > 10) {
				return 'gigantic bulgy';
			} else {
				return 'gigantic';
			}
		} else if(slave.belly >= 300000) {
			return 'massive';
		} else if(slave.belly >= 100000) {
			return 'giant';
		} else if(slave.belly >= 15000) {
			return 'huge';
		} else if(slave.belly >= 10000) {
			return 'big';
		} else {
			return 'swollen';
		}
	} else {
		return '';
	}
}

/* calculates and returns expected ovum count during conception*/
window.setPregType = function(actor) {
	/* IMHO rework is posssible. Can be more interesting to play, if this code will take in account more body conditions - age, fat, food, hormone levels, etc. */

	var ovum = 1;
	var fertilityStack = 0;	// adds an increasing bonus roll for stacked fertility drugs

	if(actor.broodmother < 1) { // Broodmothers should be not processed here. Necessary now.
		if(typeof actor.readyOva == "number" && actor.readyOva != 0) {
			ovum = actor.readyOva;  //just single override; for delayed impregnation cases
		} else if(actor.ID == -1) {
			if(actor.birthMaster > 0) { // Predisposed to twins
				if(actor.fertDrugs == 1) {
					ovum += jsEither([1, 1, 2, 2, 2, 2, 3, 3]);
				} else {
					ovum += jsEither([0, 0, 0, 1, 1, 1, 1, 1, 1, 2]);
				}
				if(actor.forcedFertDrugs > 0) {
					ovum += jsEither([1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4]);
				}
			} else {
				if(actor.fertDrugs == 1) {
					ovum += jsEither([0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3]);
				} else {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
				}
				if(actor.forcedFertDrugs > 0) {
					ovum += jsEither([0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4]);
				}
			}
		} else if(actor.pregType == 0) {
			ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);	//base chance for twins
			if(actor.hormones == 2) {
				ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2]);
				fertilityStack++;
			}
			if(actor.hormoneBalance >= 200) {
				ovum += jsEither([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2]);
				fertilityStack++;
			}
			if(actor.diet == "fertility") {
				ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
				fertilityStack++;
			}
			if(State.variables.masterSuitePregnancyFertilitySupplements == 1 && ((actor.assignment == "serve in the master suite" || actor.assignment == "be your Concubine"))) {
				ovum += jsEither([0, 0, 0, 1, 1, 2, 2, 2, 3, 3]);
				fertilityStack++;
				fertilityStack++;
			}
			if(State.variables.reproductionFormula == 1 && (State.variables.week-actor.weekAcquired > 0)) {
				fertilityStack++;
			}
			if(actor.drugs == "super fertility drugs") {
				ovum += jsEither([1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5]);
				fertilityStack++;
				fertilityStack++;
				fertilityStack++;
				fertilityStack++;
				fertilityStack++;
			} else if(actor.drugs == "fertility drugs") {
				ovum += jsEither([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3]);
				fertilityStack++;
			}
			if(State.variables.seeHyperPreg == 1) {
				if(actor.drugs == "super fertility drugs") {
					ovum += jsRandom(0, fertilityStack*2);
				} else {
					ovum += jsRandom(0, fertilityStack);
				}
			} else {
				ovum += jsRandom(0, fertilityStack);
				if(ovum > 12) {
					ovum = jsRandom(6, 12);
				}
			}
		}
	}
	return ovum;
}

/*
 Penetrative ability, ability to become pregnant, and canBreed() must be checked outside of this. Designed to assume .eggType == "human".
 target is the slave to get pregnant. Also accepts the PC.
 chance is the % chance to conceive.
 hole control's the hole involved (0 - vagina, 1 - ass, 2 - both). .mpreg did this.
 fatherID is the ID of her sire or 0 if undefined.
 displayOverride is an override if defined - fatherID must be defined in this case.
*/
window.knockMeUp = function(target, chance, hole, fatherID, displayOverride) {
	const V = State.variables;
	let r = "";
	if (V.seePreg !== 0) {
		if (jsRandom(0,99) < (chance + (V.reproductionFormula*((target.pregSource <= 0) ? ((target.ID == -1) ? 0 : 10) : 20)))) {
			if (target.mpreg === hole) {

				target.preg = 1;
				target.pregSource = (!fatherID ? 0 : fatherID);

				if (target.ID !== -1) {
					target.pregWeek = 1;
				}

				target.pregType = setPregType(target);
				WombImpregnate(target, target.pregType, target.pregSource, 1);

				if (V.menstruation === 1) {}
				else if (!displayOverride) {
					target.pregKnown = 1;
					if (target.ID === -1) {
						/* r += "@@.lime;You have gotten pregnant.@@"; */
					} else if (target.fuckdoll === 0) {
						r += "@@.lime;She has become pregnant.@@";
					} else {
						r += "@@.lime;It has become pregnant.@@";
					}
				} else {
					target.pregKnown = 1;
				}

			} else if (hole === 2) {

				target.preg = 1;
				target.pregSource = (!fatherID ? 0 : fatherID);

				if (target.ID !== -1) {
					target.pregWeek = 1;
				}

				target.pregType = setPregType(target);
				WombImpregnate(target, target.pregType, target.pregSource, 1);

				if (V.menstruation === 1) {}
				else if (!displayOverride) {
					target.pregKnown = 1;
					if (target.ID === -1) {
						/* r += "@@.lime;You have gotten pregnant.@@"; */
					} else if (target.fuckdoll === 0) {
						r += "@@.lime;She has become pregnant.@@";
					} else {
						r += "@@.lime;It has become pregnant.@@";
					}
				} else {
					target.pregKnown = 1;
				}
			}
		}
	}
	return r;
}

window.getIncubatorReserved = function(slaves) {
	var count = 0;
	slaves.forEach(function(s){
		if (s.reservedChildren > 0)
			count += s.reservedChildren;
		});
	return count;
}


code:
:: Quick List [script]

window.sortDomObjects = function (objects, attrName, reverse = 0) {
	reverse = (reverse) ? -1 : 1;
	function sortingByAttr (a, b) {
		var aVal = a.getAttribute(attrName);
		var bVal = b.getAttribute(attrName);
		var aInt = parseInt(aVal);
		if  (!isNaN(aInt))
			return ((parseInt(bVal) - aInt) * reverse);
		else if (bVal > aVal)
			return -1 * reverse;
		return ((aVal == bVal) ? 0 : 1) * reverse;
	}
	return objects.toArray().sort(sortingByAttr);
};

window.sortButtonsByDevotion = function () {
	var $sortedButtons = $('#qlWrapper button').remove();
	$sortedButtons = sortDomObjects($sortedButtons, 'data-devotion');
	$($sortedButtons).appendTo($('#qlWrapper'));
	quickListBuildLinks();
};

window.sortButtonsByTrust = function () {
	var $sortedButtons = $('#qlWrapper button').remove();
	$sortedButtons = sortDomObjects($sortedButtons, 'data-trust');
	$($sortedButtons).appendTo($('#qlWrapper'));
	quickListBuildLinks();
};

window.quickListBuildLinks = function () {
	$("[data-scroll-to]").click(function() {
		var $this = $(this), $toElement = $this.attr('data-scroll-to');
		// note the * 1 enforces $offset to be an integer, without
		// it we scroll to True, which goes nowhere fast.
                var $offset = $this.attr('data-scroll-offset') * 1 || 0;
                var $speed =   $this.attr('data-scroll-speed') * 1 || 500;
		// Use javascript scrollTop animation for in page navigation.
		$('html, body').animate({
			scrollTop: $($toElement).offset().top + $offset
		}, $speed);
	});
};

window.sortIncubatorPossiblesByName = function () {
	var $sortedIncubatorPossibles = $('#qlIncubator div.possible').detach();
	$sortedIncubatorPossibles = sortDomObjects($sortedIncubatorPossibles, 'data-name');
	$($sortedIncubatorPossibles).appendTo($('#qlIncubator'));
};

window.sortIncubatorPossiblesByPregnancyWeek = function () {
	var $sortedIncubatorPossibles = $('#qlIncubator div.possible').detach();
	$sortedIncubatorPossibles = sortDomObjects($sortedIncubatorPossibles, 'data-preg-week');
	$($sortedIncubatorPossibles).appendTo($('#qlIncubator'));
};

window.sortIncubatorPossiblesByPregnancyCount = function () {
	var $sortedIncubatorPossibles = $('#qlIncubator div.possible').detach();
	$sortedIncubatorPossibles = sortDomObjects($sortedIncubatorPossibles, 'data-preg-count');
	$($sortedIncubatorPossibles).appendTo($('#qlIncubator'));
};

window.sortIncubatorPossiblesByReservedSpots = function () {
	var $sortedIncubatorPossibles = $('#qlIncubator div.possible').detach();
	$sortedIncubatorPossibles = sortDomObjects($sortedIncubatorPossibles, 'data-reserved-spots');
	$($sortedIncubatorPossibles).appendTo($('#qlIncubator'));
};

window.sortIncubatorPossiblesByPreviousSort = function () {
	var sort = State.variables.sortIncubatorList;
	console.log(State.variables);
	console.log('sort', sort);
	if ('unsorted' !==  sort) {
		console.log("sort isn't unsorted", sort);
		if  ('Name' ===  sort) {
			console.log("sort is name", sort);
			sortIncubatorPossiblesByName();
		} else if ('Reserved Incubator Spots' === sort) {
			console.log("sort is spots", sort);
			sortIncubatorPossiblesByReservedSpots();
		} else if ('Pregnancy Week' === sort) {
			console.log("sort is week", sort);
			sortIncubatorPossiblesByPregnancyWeek();
		} else if ('Number of Children' === sort) {
			console.log("sort is count", sort);
			sortIncubatorPossiblesByPregnancyCount();
		}
	}
};


code:
:: rbuttonJS [script]
/* This is modifed radiobutton macro, for automatic checked state setup*/
/* Usage (be sure to use quotes around parameters):

	<<rbutton "$variable" "value">>
Or:
	<<rbutton "$variable" "value" "HTML_element_ID" "Text to replace with, inside html element with ID from previous parameter. <br> HTML tags allowed.">>

Group of radiobutton will be created based on variable name. Checked state will be setted up if variable contain value matched with second parameter. Full form of macro call can be used to display extended description of selected value.
*/

Macro.add('rbutton', {
	handler() {
	    if (this.args.length < 2) {
		const errors = [];
		if (this.args.length < 1) { errors.push('variable name'); }
		if (this.args.length < 2) { errors.push('checked value'); }
		return this.error(`no ${errors.join(' or ')} specified`);
	    }

	    // Ensure that the variable name argument is a string.
	    if (typeof this.args[0] !== 'string') {
		return this.error('variable name argument is not a string');
	    }

	    const varName = this.args[0].trim();

	    // Try to ensure that we receive the variable's name (incl. sigil), not its value.
	    if (varName[0] !== '$' && varName[0] !== '_') {
		return this.error(`variable name "${this.args[0]}" is missing its sigil ($ or _)`);
	    }

	    const initValue	 = Wikifier.getValue(this.args[0]);
	    const varId      = Util.slugify(varName);
	    const checkValue = this.args[1];
	    const el         = document.createElement('input');

	    var replaceID = "";
	    var replaceText = "";
	    if (typeof this.args[2] === 'string') {
			replaceID = this.args[2];
	    }
	    if (typeof this.args[3] === 'string') {
			replaceText = this.args[3];
	    }

	    /*
		Setup and initialize the group counter.
	    */
	    if (!TempState.hasOwnProperty(this.name)) {
		TempState[this.name] = {};
	    }

	    if (!TempState[this.name].hasOwnProperty(varId)) {
		TempState[this.name][varId] = 0;
	    }

	    /*
		Setup and append the input element to the output buffer.
	    */
	    jQuery(el)
		.attr({
		    id       : `${this.name}-${varId}-${TempState[this.name][varId]++}`,
		    name     : `${this.name}-${varId}`,
		    type     : 'radio',
		    tabindex : 0 // for accessiblity
		})
		.addClass(`macro-${this.name}`)
		.on('change', function () {
		    if (this.checked) {
			Wikifier.setValue(varName, checkValue);

			if (replaceID.length > 0 && replaceText.length > 0){

				var replaceEl = document.getElementById(replaceID);
				//alert (replaceEl);
				if (replaceEl != null) {
					replaceEl.innerHTML = replaceText;
					}

				}
		    }
		})
		.ready (function () {
			//alert ("DOM finished");
			if (el.checked && replaceID.length > 0 && replaceText.length > 0){

				var replaceEl = document.getElementById(replaceID);
				//alert (replaceEl);
				if (replaceEl != null) {
					replaceEl.innerHTML = replaceText;
				}

			}
		})
		.appendTo(this.output);

	    /*
		Set the story variable to the checked value and the input element to checked, if requested.
	    */
	    if (initValue == checkValue) {
		el.checked = true;
		Wikifier.setValue(varName, checkValue);
	    }
	}
    });


code:
:: rulesAssistant [script]

window.hasSurgeryRule = function(slave, rules) {
	return rules.some(
		rule => ruleApplied(slave, rule) && rule.set.autoSurgery > 0);
};

window.hasRuleFor = function(slave, rules, what) {
	return rules.some(
		rule => ruleApplied(slave, rule) && rule[what] !== "no default setting");
};

window.hasHColorRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "hColor");
};

window.hasHStyleRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "hStyle");
};

window.hasEyeColorRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "eyeColor");
};

window.lastPregRule = function(slave, rules) {
	return rules.some(rule =>
		ruleApplied(slave, rule) && rule.set.preg === -1);
};

window.mergeRules = function mergeRules(rules) {
	const combinedRule = {};
	rules.forEach(rule => {
		// A rule overrides any preceding ones if,
		//   * there are no preceding ones,
		//   * or it sets autoBrand,
		//   * or it does not set autoBrand and is not "no default setting"
		Object.keys(rule).forEach(key => {
			const applies = (combinedRule[key] === undefined ||
				(key === "autoBrand" && rule[key]) ||
				(key !== "autoBrand" && rule[key] !== "no default setting"));
			if (!applies) return;
			combinedRule[key] = rule[key];
		});
	});
	return combinedRule;
};

// return if a rule is applied on a slave
window.ruleApplied = function(slave, rule) {
	return slave.currentRules.includes(rule.ID);
};

// remove slave from the facility described by the rule
window.RAFacilityRemove = function RAFacilityRemove(slave, rule) {
	const V = State.variables;
	let r = "";
	if (!rule.facilityRemove) return r;
	switch(rule.setAssignment) {
	case "be confined in the arcade":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>${slave.slaveName} has been removed from ${V.arcadeName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "work in the brothel":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>${slave.slaveName} has been removed from ${V.brothelName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "serve in the club":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>${slave.slaveName} has been removed from ${clubName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "work in the dairy":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>${slave.slaveName} has been removed from ${V.dairyName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "rest in the spa":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>${slave.slaveName} has been removed from ${V.spaName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "get treatment in the clinic":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>{slave.slaveName} has been removed from ${V.clinicName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "serve in the master suite":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>{slave.slaveName} has been removed from ${V.masterSuiteName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "live with your Head Girl":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>${slave.slaveName} has been removed from ${HGSuiteName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "work as a servant":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>${slave.slaveName} has been removed from ${V.servantsQuartersName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "learn in the schoolroom":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>${slave.slaveName} has been removed from ${V.schoolroomName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;

	case "be confined in the cellblock":
		if (slave.assignment === rule.setAssignment) {
			r += `<br>${slave.slaveName} has been removed from ${V.cellblockName} and has been assigned to ${rule.removalAssignment}.`;
			assignJob(slave, rule.removalAssignment);
		}
		break;
	}
};

// return whether the rule applies to the slave
window.ruleAppliesP = function ruleAppliesP(cond, slave) {
	let flag;

	// attribute / function check
	switch (cond.function) {
	case true: // always applies
		flag = true;
		break;
	case false: // never applies
		flag = false;
		break;
	case "between": // between two values of a slave's attribute
		flag = between(
			slave[cond.data.attribute],
			cond.data.value[0],
			cond.data.value[1]);
		break;
	case "belongs": // the attribute belongs in the list of values
		flag = cond.data.value.includes(slave[cond.data.attribute])
		break;
	case "custom": // user provided JS function
		flag = eval(cond.data)(slave);
		break;
	}
	// assignment / facility / special slaves / specific slaves check
	let assignment = slave.assignment;
	if (assignment === "be your Concubine") assignment = "serve in the master suite";
	flag = flag &&
		!(cond.excludeSpecialSlaves && isLeaderP(slave)) &&
		(cond.assignment.length === 0 || cond.assignment.includes(assignment)) &&
		(cond.selectedSlaves.length === 0 || cond.selectedSlaves.includes(slave.ID)) &&
		!(cond.excludedSlaves.includes(slave.ID));
	return flag;
};

window.emptyDefaultRule = function emptyDefaultRule() {
	const id = generateNewID();
	const rule = {
		ID: id,
		name: `Rule ${id}`,
		condition: {
			function: false,
			data: {},
			excludeSpecialSlaves: false,
			assignment: [],
			selectedSlaves: [],
			excludedSlaves: [],
		},
		set: {
			releaseRules: "no default setting",
			clitSetting: "no default setting",
			clitSettingXY: "no default setting",
			clitSettingXX: "no default setting",
			clitSettingEnergy: "no default setting",
			speechRules: "no default setting",
			clothes: "no default setting",
			collar: "no default setting",
			shoes: "no default setting",
			legAccessory: "no default setting",
			virginAccessory: "no default setting",
			aVirginAccessory: "no default setting",
			vaginalAccessory: "no default setting",
			aVirginDickAccessory: "no default setting",
			dickAccessory: "no default setting",
			bellyAccessory: "no default setting",
			aVirginButtplug: "no default setting",
			buttplug: "no default setting",
			eyeColor: "no default setting",
			makeup: "no default setting",
			nails: "no default setting",
			hColor: "no default setting",
			hLength: "no default setting",
			hStyle: "no default setting",
			pubicHColor: "no default setting",
			pubicHStyle: "no default setting",
			nipplesPiercing: "no default setting",
			areolaePiercing: "no default setting",
			clitPiercing: "no default setting",
			vaginaLube: "no default setting",
			vaginaPiercing: "no default setting",
			dickPiercing: "no default setting",
			anusPiercing: "no default setting",
			lipsPiercing: "no default setting",
			tonguePiercing: "no default setting",
			earPiercing: "no default setting",
			nosePiercing: "no default setting",
			eyebrowPiercing: "no default setting",
			navelPiercing: "no default setting",
			corsetPiercing: "no default setting",
			boobsTat: "no default setting",
			buttTat: "no default setting",
			vaginaTat: "no default setting",
			dickTat: "no default setting",
			lipsTat: "no default setting",
			anusTat: "no default setting",
			shouldersTat: "no default setting",
			armsTat: "no default setting",
			legsTat: "no default setting",
			backTat: "no default setting",
			stampTat: "no default setting",
			curatives: "no default setting",
			livingRules: "no default setting",
			relationshipRules: "no default setting",
			standardPunishment: "no default setting",
			standardReward: "no default setting",
			diet: "no default setting",
			dietCum: "no default setting",
			dietMilk: "no default setting",
			muscles: "no default setting",
			XY: "no default setting",
			XX: "no default setting",
			gelding: "no default setting",
			preg: "no default setting",
			growth_boobs: "no default setting",
			growth_butt: "no default setting",
			growth_lips: "no default setting",
			growth_dick: "no default setting",
			growth_balls: "no default setting",
			aphrodisiacs: "no default setting",
			autoSurgery: 0,
			autoBrand: 0,
			pornFameSpending: "no default setting",
			dietGrowthSupport: 0,
			eyewear: "no default setting",
			setAssignment: "no default setting",
			facilityRemove: false,
			removalAssignment: "rest",
			surgery_eyes: "no default setting",
			surgery_lactation: "no default setting",
			surgery_prostate: "no default setting",
			surgery_cosmetic: "no default setting",
			surgery_accent: "no default setting",
			surgery_shoulders: "no default setting",
			surgery_shouldersImplant: "no default setting",
			surgery_boobs: "no default setting",
			surgery_hips: "no default setting",
			surgery_hipsImplant: "no default setting",
			surgery_butt: "no default setting",
			surgery_faceShape: "no default setting",
			surgery_lips: "no default setting",
			surgery_holes: "no default setting",
			surgery_hair: "no default setting",
			surgery_bodyhair: "no default setting",
			surgery_vasectomy: "no default setting",
			surgery_bellyImplant: "no default setting",
			underArmHColor: "no default setting",
			underArmHStyle: "no default setting",
			drug: "no default setting",
			eyes: "no default setting",
			pregSpeed: "no default setting",
			bellyImplantVol: -1,
			teeth: "no default setting",
		}
	};
	return rule;
};

// Saves the slave, silently fires the RA, saves the slave's after-RA state, and then reverts the slave.
// Call and then check potential change against $slaveAfterRA to see if the RA would revert it.
window.RulesDeconfliction = function RulesDeconfliction(slave) {
	const before = clone(slave);
	DefaultRules(slave);
	V.slaveAfterRA = clone(slave);
	slave = before;
}