code:
/* see documentation for details /devNotes/Extended Family Mode Explained.txt */

window.isMotherP = function isMotherP(daughter, mother) {
	return daughter.mother === mother.ID;
};

window.isFatherP = function isFatherP(daughter, father) {
	return daughter.father === father.ID;
};

window.isParentP = function isParentP(daughter, parent) {
	return isMotherP(daughter, parent) || isFatherP(daughter, parent);
};

window.sameDad = function(slave1, slave2) {
	if ((slave1.father === slave2.father) && (slave1.father !== 0 && slave1.father !== -2)) {
		return true;
	}
	return false;
};

window.sameMom = function(slave1, slave2) {
	if ((slave1.mother === slave2.mother) && (slave1.mother !== 0 && slave1.mother !== -2)) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} niece
 * @param {App.Entity.SlaveState} aunt
 * @returns {boolean}
 */
window.isAunt = function(niece, aunt) {
	if (!State.variables.showDistantRelatives) {
		return false;
	}

	if (!niece || !aunt || (niece.ID === aunt.ID)) {
		return false;
	}

	let mother;
	let father;

	if ((mother = getSlave(niece.mother)) && (mother.ID !== aunt.ID) && !sameTParent(mother, aunt) && sameMom(mother, aunt) && sameDad(mother, aunt)) {
		return true;
	} else if ((father = getSlave(niece.father)) && (father.ID !== aunt.ID) && !sameTParent(father, aunt) && sameMom(father, aunt) && sameDad(father, aunt)) {
		return true;
	}

	return false;
};

// testtest catches the case if a mother is a father or a father a mother - thank you familyAnon, for this code
window.sameTParent = function(slave1, slave2) {
	if (slave1.mother === -1 && slave1.father === -1 && slave2.mother === -1 && slave2.father === -1) {
		return 1;
	} else if (slave1.mother === slave2.father && slave1.father === slave2.mother && slave1.mother !== 0 && slave1.mother !== -2 && slave1.father !== 0 && slave1.father !== -2 && slave1.mother !== slave1.father) {
		return 2;
	} else if ((slave1.mother === slave2.father || slave1.father === slave2.mother) && slave1.mother !== 0 && slave1.mother !== -2 && slave2.mother !== 0 && slave2.mother !== -2 && slave1.mother !== slave1.father) {
		return 3;
	}
	return 0;
};

/*
window.sameTParent = function(slave1, slave2) {
	if ((slave1.mother === slave2.father || slave1.father === slave2.mother) && (slave1.mother !== 0 && slave1.mother !== -2 && slave1.father !== 0 && slave1.father !== -2)) {
		return true; //testtest catches the case if a mother is a father or a father a mother
	} else {
		return false;
	}
};
*/

window.areTwins = function(slave1, slave2) {
	if (!sameDad(slave1, slave2)) {
		return false;
	} else if (!sameMom(slave1, slave2)) {
		return false;
	} else if (slave1.actualAge === slave2.actualAge && slave1.birthWeek === slave2.birthWeek) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 * @returns {number}
 */
window.areSisters = function(slave1, slave2) {
	if (slave1.ID === slave2.ID) {
		return 0; // you are not your own sister
	} else if (((slave1.father === 0) || (slave1.father === -2)) && ((slave1.mother === 0) || (slave1.mother === -2))) {
		return 0; // not related
	} else {
		if (!sameDad(slave1, slave2) && sameMom(slave1, slave2)) {
			return 3; // half sisters
		} else if (sameDad(slave1, slave2) && !sameMom(slave1, slave2)) {
			return 3; // half sisters
		} else if (sameTParent(slave1, slave2) === 3) {
			return 3; // half sisters
		} else if (sameTParent(slave1, slave2) === 2) {
			return 2; // sisters
		} else if (sameDad(slave1, slave2) && sameMom(slave1, slave2)) {
			if (slave1.actualAge === slave2.actualAge && slave1.birthWeek === slave2.birthWeek) {
				return 1; // twins
			} else {
				return 2; // sisters
			}
		} else {
			return 0; // not related
		}
	}
};

/*
//3 = half-sisters, 2 = sisters, 1 = twins, 0 = not related
window.areSisters = function(c1, c2) {
	if(c1.ID === c2.ID) {
		return 0;
	}
	var sib = 4;
	if(sameMom(c1, c2)) {
		sib -= 1;
	}
	if(sameDad(c1, c2)) {
		sib -=1;
	}
	if (sib === 2 && c1.actualAge === c2.actualAge && c1.birthWeek === c2.birthWeek) {
		sib -= 1;
	}
	if(sib === 4) {
		return 0
	} else {
		return sib;
	}
}
*/
/**
 * @param {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 * @returns {boolean}
 */
window.areCousins = function(slave1, slave2) {
	if (!State.variables.showDistantRelatives) {
		return false;
	}

	if (!slave1 || !slave2 || (slave1.ID === slave2.ID) || areSisters(slave1, slave2)) {
		return false;
	}

	let slave1Mom;
	let slave1Dad;
	let slave2Mom;
	let slave2Dad;

	if ((slave1Mom = getSlave(slave1.mother)) && (slave2Mom = getSlave(slave2.mother)) && !sameTParent(slave1Mom, slave2Mom) && sameMom(slave1Mom, slave2Mom) && sameDad(slave1Mom, slave2Mom)) {
		return true;
	} else if ((slave1Mom = getSlave(slave1.mother)) && (slave2Dad = getSlave(slave2.father)) && !sameTParent(slave1Mom, slave2Dad) && sameMom(slave1Mom, slave2Dad) && sameDad(slave1Mom, slave2Dad)) {
		return true;
	} else if ((slave1Dad = getSlave(slave1.father)) && (slave2Mom = getSlave(slave2.mother)) && !sameTParent(slave1Dad, slave2Mom) && sameMom(slave1Dad, slave2Mom) && sameDad(slave1Dad, slave2Mom)) {
		return true;
	} else if ((slave1Dad = getSlave(slave1.father)) && (slave2Dad = getSlave(slave2.father)) && !sameTParent(slave1Dad, slave2Dad) && sameMom(slave1Dad, slave2Dad) && sameDad(slave1Dad, slave2Dad)) {
		return true;
	}

	return false;
};

/**
 * @param {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 * @returns {boolean}
 */
window.areRelated =
	function(slave1, slave2) {
		return (slave1.father === slave2.ID || slave1.mother === slave2.ID || slave2.father === slave1.ID || slave2.mother === slave1.ID || areSisters(slave1, slave2) > 0);
	};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.totalRelatives = function(slave) {
	let relatives = 0;
	if (slave.mother > 0) {
		relatives += 1;
	}
	if (slave.father > 0) {
		relatives += 1;
	}
	if (slave.daughters > 0) {
		relatives += slave.daughters;
	}
	if (slave.sisters > 0) {
		relatives += slave.sisters;
	}
	return relatives;
};

/**
 * @param {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 * @param {App.Entity.SlaveState[]} slaves
 * @returns {Array}	// I think
 */
window.mutualChildren = function(slave1, slave2, slaves) {
	return slaves.filter(function(s) {
		return s.ID !== slave1.ID && s.ID !== slave2.ID && s.mother > 0 && s.father > 0 && ((s.mother === slave1.ID && s.father === slave2.ID) || (s.mother === slave2.ID && s.father === slave1.ID));
	}).length;
};

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

/* OLD
window.randomRelatedSlave = function(slave, filterFunction) {
	if(!slave || !SugarCube) { return undefined; }
	if(typeof filterFunction !== 'function') { filterFunction = function(s, index, array) { return true; }; }
	return State.variables.slaves.filter(filterFunction).shuffle().find(function(s, index, array) {return areSisters(slave, s) || s.mother === slave.ID || s.father === slave.ID || slave.ID === s.mother || slave.ID === s.father; })
}
*/

/**
 * @param {App.Entity.SlaveState} slave
 * @param {function} filterFunction	// I think
 * @returns {App.Entity.SlaveState}
 */
window.randomRelatedSlave = function(slave, filterFunction) {
	if (!slave || !SugarCube) {
		return undefined;
	}
	if (typeof filterFunction !== 'function') {
		filterFunction = function( /* s, index, array*/ ) {
			return true;
		};
	}
	const arr = State.variables.slaves.filter(filterFunction);
	arr.shuffle();
	return arr.find(function(s) {
		return areSisters(slave, s) ||
			slave.ID === s.mother ||
			slave.ID === s.father ||
			s.ID === slave.mother ||
			s.ID === slave.father;
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {App.Entity.SlaveState}
 */
window.randomRelatedAvailableSlave = function(slave) {
	return randomRelatedSlave(slave, function(s) {
		return isSlaveAvailable(s);
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {App.Entity.SlaveState}
 */
window.randomSister = function(slave) {
	return randomRelatedSlave(slave, function(s) {
		return areSisters(slave, s);
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {App.Entity.SlaveState}
 */
window.randomTwinSister = function(slave) {
	return randomRelatedSlave(slave, function(s) {
		return areSisters(slave, s);
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {App.Entity.SlaveState}
 */
window.randomAvailableSister = function(slave) {
	return randomRelatedSlave(slave, function(s) {
		return isSlaveAvailable(s) && areSisters(slave, s);
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {App.Entity.SlaveState}
 */
window.randomAvailableTwinSister = function(slave) {
	return randomRelatedSlave(slave, function(s) {
		return isSlaveAvailable(s) && areSisters(slave, s);
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {App.Entity.SlaveState}
 */
window.randomDaughter = function(slave) {
	return randomRelatedSlave(slave, function(s) {
		return s.mother === slave.ID || s.father === slave.ID;
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {App.Entity.SlaveState}
 */
window.randomAvailableDaughter = function(slave) {
	return randomRelatedSlave(slave, function(s) {
		return isSlaveAvailable(s) && (s.mother === slave.ID || s.father === slave.ID);
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {App.Entity.SlaveState}
 */
window.randomParent = function(slave) {
	return randomRelatedSlave(slave, function(s) {
		return s.ID === slave.mother || s.ID === slave.father;
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {App.Entity.SlaveState}
 */
window.randomAvailableParent = function(slave) {
	return randomRelatedSlave(slave, function(s) {
		return isSlaveAvailable(s) && (s.ID === slave.mother || s.ID === slave.father);
	});
};

window.totalPlayerRelatives = function(pc) {
	let relatives = 0;
	if (pc.mother > 0) {
		relatives += 1;
	}
	if (pc.father > 0) {
		relatives += 1;
	}
	if (pc.daughters > 0) {
		relatives += pc.daughters;
	}
	if (pc.sisters > 0) {
		relatives += pc.sisters;
	}
	return relatives;
};

/**
 * @param {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 * @returns {string}
 */
window.relativeTerm =
	function(slave1, slave2) {
		if (slave2.mother === slave1.ID || slave2.father === slave1.ID) {
			if (slave2.genes === "XY" && State.variables.diversePronouns) {
				return "son";
			} else {
				return "daughter";
			}
		} else if (slave1.mother === slave2.ID) {
			return "mother";
		} else if (slave1.father === slave2.ID) {
			return "father";
		} else if (areSisters(slave2, slave1)) {
			if (slave2.genes === "XY" && State.variables.diversePronouns) {
				return "twin brother";
			} else {
				return "twin sister";
			}
		} else if (areSisters(slave2, slave1) === 2) {
			if (slave2.genes === "XY" && State.variables.diversePronouns) {
				return "brother";
			} else {
				return "sister";
			}
		} else if (areSisters(slave2, slave1) === 3) {
			if (slave2.genes === "XY" && State.variables.diversePronouns) {
				return "half-brother";
			} else {
				return "half-sister";
			}
		} else if (isAunt(slave1, slave2)) {
			if (slave2.genes === "XY" && State.variables.diversePronouns) {
				return "nephew";
			} else {
				return "niece";
			}
		} else if (isAunt(slave2, slave1)) {
			if (slave2.genes === "XY" && State.variables.diversePronouns) {
				return "uncle";
			} else {
				return "aunt";
			}
		} else if (areCousins(slave2, slave1)) {
			return "cousin";
		} else {
			return "some unknown blood connection";
		}
	};

repo_name: Raou/fc


code:
/* eslint-disable camelcase */
/* eslint-disable no-console */
let lastActiveSlave, lastSlaves, lastPC;

/*
	To use, add something like:

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

*/

window.renderFamilyTree = function(slaves, filterID) {
	'use strict';

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

	let 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);

		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) {
		let simulation = d3.forceSimulation()
			.force('link', d3.forceLink().id(function(d) {
				return d.index;
			}))
			// eslint-disable-next-line no-unused-vars
			.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));

		let 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');

		let 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) {
				let 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';
				}
			});

		svg.selectAll('.node-circle');
		svg.selectAll('.node-text');

		let 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) {
	let family_graph = {nodes: [], links: []};
	let node_lookup = {};
	let preset_lookup = {
		'-2': 'A citizen',
		'-3': 'Former master',
		'-4': 'An arcology owner',
		'-5': 'Client',
		'-6': 'Social Elite',
		'-7': 'Gene lab'
	};
	let outdads = {};
	let outmoms = {};
	let kids = {};

	let 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
	};
	let charList = [fake_pc];
	charList.push.apply(charList, slaves);
	charList.push.apply(charList, State.variables.tanks);

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

	for (let i = 0; i < charList.length; i++) {
		let mom = charList[i].mother;
		let 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 (let i = 0; i < charList.length; i++) {
		let character = charList[i];
		if (character.mother === 0 && character.father === 0 && !kids[character.ID]) {
			continue;
		}
		let mom = character.mother;
		if (mom < -6) {
			if (mom in State.variables.missingTable && State.variables.showMissingSlaves) {
				node_lookup[mom] = family_graph.nodes.length;
				let missing = State.variables.missingTable[mom];
				charList.push({
					ID: mom,
					mother: 0,
					father: 0,
					is_mother: true,
					dick: missing.dick,
					vagina: missing.vagina,
					slaveName: missing.slaveName
				});
			} else {
				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]
			});
		}

		let dad = character.father;
		if (dad < -6) {
			if (dad in State.variables.missingTable && State.variables.showMissingSlaves) {
				node_lookup[dad] = family_graph.nodes.length;
				let missing = State.variables.missingTable[dad];
				charList.push({
					ID: dad,
					mother: 0,
					father: 0,
					is_father: true,
					dick: missing.dick,
					vagina: missing.vagina,
					slaveName: missing.slaveName
				});
			} else {
				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]
			});
		}
	}
	let mkeys = Object.keys(outmoms);
	for (let i = 0; i < mkeys.length; i++) {
		let name;
		let key = mkeys[i];
		let 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`
		});
	}

	let dkeys = Object.keys(outdads);
	for (let i = 0; i < dkeys.length; i++) {
		let name;
		let key = dkeys[i];
		let 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`
		});
	}

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

	let related = {};
	let seen = {};
	let 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]) {
			let relIDs = relatedTo(charHash[filterID], filterID);
			for (let k in relIDs.tree) {
				related[k] = true;
			}
			for (let i = 0; i < charList.length; i++) {
				if (charHash[charList[i].ID]) {
					let pRelIDs = relatedTo(charHash[charList[i].ID], filterID);
					if (pRelIDs.related) {
						for (let k in pRelIDs.tree) {
							related[k] = true;
							if (saveTree[k]) {
								for (let k2 in saveTree[k].tree) {
									related[k2] = true;
								}
							}
						}
					}
					saveTree[charList[i].ID] = pRelIDs;
				}
			}
		}
	}

	for (let i = 0; i < charList.length; i++) {
		let character = charList[i];
		let 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;
		let 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 (let i = 0; i < charList.length; i++) {
		let character = charList[i];
		let 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') {
			let 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)>>
html_script_tag updateFamilyTree() end_html_script_tag
// tweego freaks out if the actual end of script tag is used even inside a comment
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;
	let treeDepth = 0;
	let numTreeNodes = 0;

	let 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 (let 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);
		let shouldAddChildren = false;
		if (!slavesAdded[slave.ID]) {
			shouldAddChildren = true;
			slavesAdded[slave.ID] = true;
		}
		let data = {
			"name": slave.slaveName + (slave.physicalAge ? (` (${slave.physicalAge})`) : ""),
			"class": slave.genes,
			"textClass": (activeSlaveId === slave.ID) ? "emphasis" : "",
			"marriages": [],
		};

		let 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 (let i = 0; i < slaves.length; ++i) {
			let child = slaves[i];
			if (child.ID !== activeSlave.ID) {
				maybeAddSpouseToChild(child);
			}
		}

		for (let key in spouseToChild) {
			if (spouseToChild.hasOwnProperty(key)) {
				let children = shouldAddChildren ? spouseToChild[key] : [];
				let spouse = getSlave(key, (slaves.genes === "XX") ? "unknownXY" : (slaves.genes === "XY") ? "unknownXX" : "unknown");
				let spouseName;
				if (spouse.ID !== slave.ID) {
					spouseName = spouse.slaveName + (spouse.physicalAge ? (` (${spouse.physicalAge})`) : "");
				} else {
					spouseName = (spouse.ID === -1) ? "(yourself)" : "(themselves)";
				}
				let 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);

	let 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:
/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.foodAmount = function(slave) {
	const V = State.variables;
	let food = 400;													// kg / food produced by base slave / week
	if (!slave) {
		return null;
	} else {
		if (V.Farmer !== 0) {										// if a farmer is assigned
			food *= 1.1;											// TODO: expand this to account for farmer XP and skill
			if (V.Farmer.skill.farmer >= V.masteredXP) {			// if farmer is master
				food *= 1.1;
			}
		}
		if (slave.devotion > 50) {
			food *= 1.1;
		} else if (slave.devotion < -50) {
			food *= 0.8;
		}
		if (slave.health > 50) {									// slave is extremely healthy or more
			food *= 1.1;
		} else if (slave.health < -50) {							// slave is unhealthy or less
			food *= 0.8;
		}
		if (slave.muscles > 30) {									// slave is muscular or more
			food *= 1.1;
		} else if (slave.muscles <= -6) {							// slave is weak or less
			food *= 0.8;
		}
		if (slave.weight > 95) {									// slave is overweight or more
			food *= 0.9;
		} else if (slave.weight > 130) {							// slave is fat or more
			food *= 0.8;
		} else if (slave.weight > 160) {							// slave is obese or more
			food *= 0.7;
		} else if (slave.weight > 190) {							// slave is very obese or more
			food *= 0.6;
		}
		if (slave.eyes === -1) {									// slave is nearsighted
			food *= 0.8;
		} else if (slave.eyes < -1) {								// slave is blind
			food *= 0.6;
		}
		if (slave.hears === -1) {									// slave is hard of hearing
			food *= 0.8;
		} else if (slave.hears < -1) {								// slave is deaf
			food *= 0.6;
		}
		if (slave.tired === 1) {									// slave is tired
			food *= 0.8;
		}
		food = Math.trunc(food);
		food = Math.clamp(food, 1, 1000000000000000000);
		return food;
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
window.farmShowsIncome = function(slave) {
																	// TODO: incorporate farmyardRestraints
	const V = State.variables;
	let arcology = V.arcologies[0];
	let cash = 100;													// TODO: this is just a placeholder
	if (!slave) {
		return null;
	} else {
		if (V.Farmer !== 0) {										// farmer is assigned
			cash *= 1.1;
			if (V.Farmer.skill.farmer >= V.masteredXP) {			// farmer is master
				cash *= 1.1;
			}
		}
		/* OPEN FS EFFECTS */
		if (arcology.FSSupremacist !== "unset") {
			if (slave.race === arcology.FSSupremacistRace) {		// slave is of supreme race
				cash *= 0.9;										// TODO: should supreme race slaves receive a penalty?
				repX(-10, "food");									// TODO: should this cause a rep loss
			}
		}
		if (arcology.FSSubjugationist !== "unset") {
			if (slave.race === arcology.FSSubjugationistRace) { 	// slave is of subjugated race
				cash *= 1.1;
				repX(10, "food");									// TODO: should this cause a rep gain?
			} else {
				cash *= 0.9;										// TODO: should unsubjugated race slaves receive a penalty?
			}
		}
		if (arcology.FSRepopulationFocus !== "unset") {
			if (slave.pregWeek > 16) {								// slave is visibly pregnant
				if (slave.eggType !== "human") {					// with a non-human
					cash *= 1.1;
				}
				cash *= 1.2;
			} else {												// slave is not visibly pregnant
				cash *= 0.8;										// TODO: should non-pregnant slaves receive a penalty?
			}
		}
		if (arcology.FSRestart !== "unset") {
			if (slave.pregWeek > 16) {								// slave is visibly pregnant
				if (slave.eggType !== "human") {					// with a non-human
					cash *= 0.9;
					repX(-15, "food");
				}
				cash *= 0.6;
				repX(-10, "food");
			}
		}
		if (arcology.FSGenderRadicalist !== "unset") {
			if (slave.dick > 0) {									// TODO: does this make sense?
				cash *= 1.1;
			} else {
				cash *= 0.8;
			}
		}
		if (arcology.FSGenderFundamentalist !== "unset") {
			if (slave.dick > 0) {
				cash *= 1.1;										// TODO: does this make sense?
			} else {
				cash *= 0.8;
			}
		}
		if (arcology.FSPaternalist !== "unset") {
			if (V.seeBestiality === 1) {
				if (V.farmyardBreeding === 1) {						// slaves are being bred
					cash *= 0.8;
				} else {											// slaves are putting on shows
					cash *= 0.9;
				}
			}
		}
		if (arcology.FSDegradationist !== "unset") {
			if (V.seeBestiality === 1) {
				if (V.farmyardBreeding === 1) {						// slaves are being bred
					cash *= 1.2;
				} else {											// slaves are putting on shows
					cash *= 1.1;
				}
			}
		}
		if (arcology.FSBodyPurist !== "unset") {
			if (slave.boobsImplant > 0) {							// slave has breast implants
				cash *= 0.9;
			}
			if (slave.buttImplant > 0) {							// slave has butt implants
				cash *= 0.9;
			}
			if (slave.faceImplant > 0) {							// slave has had facial surgery
				cash *= 0.9;
			}
			if (slave.lipsImplant > 0) {							// slave has lip implants
				cash *= 0.9;
			}
		}
		if (arcology.FSTransformationFetishist !== "unset") {
			if (slave.boobsImplant > 0) {							// slave has breast implants
				cash *= 1.1;
			}
			if (slave.buttImplant > 0) {							// slave has butt implants
				cash *= 1.1;
			}
			if (slave.faceImplant > 0) {							// slave has had facial surgery
				cash *= 1.1;
			}
			if (slave.lipsImplant > 0) {							// slave has lip implants
				cash *= 1.1;
			}
		}
		if (arcology.FSYouthPreferentialist !== "unset") {
			if (slave.visualAge > 25) {								// slave looks older than 25
				cash *= 0.8;										// TODO: not sure what age this should be
			} else {
				cash *= 1.2;
			}
		}
		if (arcology.FSMaturityPreferentialist !== "unset") {
			if (slave.visualAge > 25) {								// slave looks older than 25
				cash *= 1.2;										// TODO: not sure what age this should be
			} else {
				cash *= 0.8;
			}
		}
		if (arcology.FSSlimnessEnthusiast !== "unset") {
			if (slave.weight > 10) {								// slave is curvy or more
				cash *= 0.9;										// TODO: not sure what weight this should be
			}														// TODO: should implants affect income?
			if (slave.boobs > 799) {								// slave has larger than a D cup
				cash *= 0.9;										// TODO: should this be smaller?
			}
			if (slave.butt > 3) {									// slave has bigger than a bubble butt
				cash *= 0.9;
			}
		}
		if (arcology.FSAssetExpansionist !== "unset") {
			if (slave.weight > 10) {								// slave is curvy or more
				cash *= 1.1;										// TODO: not sure what weight this should be
			}														// TODO: should implants affect income?
			if (slave.boobs > 799) {								// slave has larger than a D cup
				cash *= 1.1;										// TODO: should this be smaller?
			}
			if (slave.butt > 3) {									// slave has bigger than a bubble butt
				cash *= 1.1;
			}
		}
		if (arcology.FSPastoralist !== "unset") {
			if (slave.boobs > 799) {								// slave has larger than a D cup
				cash *= 1.2;
			}
			if (slave.lactation > 0) {								// slave is lactating
				cash *= 1.1;
			}
		}
		if (arcology.FSPhysicalIdealist !== "unset") {
			if (slave.height > 169) {								// slave is tall or taller
				cash *= 1.1;
			} else if (slave.height < 160) {						// slave is short or shorter
				cash *= 0.9;
			}
		}
		if (arcology.FSHedonisticDecadence !== "unset") {
			if (slave.weight > 10) { 								// slave is curvy or fatter
				cash *= 1.1;
			} else if (slave.weight < -10) { 						// slave is thin or thinner
				cash *= 0.9;
			}
		}
		if (arcology.FSChattelReligionist !== "unset") { 			// TODO: I don't know what to put for this one
			if (slave.devotion < 21) { 								// if slave is careful or less
				cash *= 0.9;
			}
		}
																	// TODO: should I add the ancient cultures FS?
		/* CLOSE FS EFFECTS */

		if (setup.entertainmentCareers.includes(slave.career)) {
			cash *= 1.1;
		} else if (setup.farmerCareers.includes(slave.career)) {
			cash *= 1.1;
		}
		if (slave.prestige === 1) { 								// slave is prestigious
			cash *= 1.1;
		} else if (slave.prestige === 2) { 							// slave is very prestigious
			cash *= 1.2;
		} else if (slave.prestige === 3) { 							// slave is extremely prestigious
			cash *= 1.3;
		}
		if (slave.pornPrestige === 1) { 							// slave is prestigious from porn
			cash *= 1.1;
		} else if (slave.pornPrestige === 2) { 						// slave is very prestigious from porn
			cash *= 1.2;
		} else if (slave.pornPrestige === 3) { 						// slave is extremely prestigious from porn
			cash *= 1.3;
		} 															// TODO: add relationship checks
		if (slave.health > 20) { 									// slave is very healthy or more
			cash *= 1.1;
		} else if (slave.health < -20) { 							// slave is less than unhealthy
			cash *= 0.8;
		}
		if (slave.face > 40) {										// slave is beautiful or more
			cash *= 1.3;
		} else if (slave.face > 10) {								// slave is very pretty or more
			cash *= 1.1;
		} else if (slave.face < -10) {								// slave is less than unattractive
			cash *= 0.9;
		} else if (slave.face < -40) {								// slave is less than ugly
			cash *= 0.7;
		}
		if (slave.devotion > 50) { 									// slave is devoted or more
			cash *= 1.1;
		} else if (slave.devotion < -50) { 							// slave is reluctant or less
			cash *= 0.8;
		}
		if (slave.trust > 50) { 									// slave is trusting or more
			cash *= 1.1;
		} else if (slave.trust < -50) { 							// slave is less than frightened
			cash *= 0.8;											// TODO: should trust be a factor?
		}
		if (slave.weight > 30) { 									// slave is curvy or more
			cash *= 0.8;											// TODO: tie in Hedonistic FS
		} else if (slave.weight < -30) { 							// slave is very thin or less
			cash *= 0.8;											// TODO: put this on a scale
		}
		if (slave.muscles > 30) { 									// slave is muscular or more
			cash *= 0.9;											// TODO: tie in height eugenics policy
		} else if (slave.muscles < -30) {							// slave is very weak or less
			cash *= 0.9;											// TODO: should this be on a scale?
		}
		if (slave.eyes < 1) {										// slaves eyesight is nearsighted or worse
			cash *= 0.9;											// TODO: should nearsighted and blind slaves receive a penalty?
		}
		if (slave.hears < 0) {										// slave is hard of hearing or less
			cash *= 0.9;											// TODO: should hard of hearing slaves receive a penalty?
		}
		if (slave.boobs > 800) {									// slave has a DD cup or bigger
			cash *= 0.9;											// TODO: would this make sense?
		}
		if (slave.butt > 4) {										// slave has an enormous butt or bigger
			cash *= 0.9;											// TODO: would this make sense?
		}
		if (slave.preg > 10) {										// slave is pregnant and showing
			cash *= 0.8;											// TODO: not sure how to incorporate pregnancy
		}															// TODO: incorporate skills
		if (slave.tired === 1) {									// slave is tired
			cash *= 0.9;
		}
		if (slave.chem > 10) {										// slave has high genetic damage
			cash *= 0.9;
		}
		if (slave.intelligence > 50) {								// slave is very smart or better
			cash *= 1.1;
		} else if (slave.intelligence < -50) {						// slave is very slow or less
			cash *= 0.8;
		}
		if (slave.energy <= 20) {									// slave has no sex drive
			cash *= 0.7;
		} else if (slave.energy <= 40) {							// slave has poor sex drive
			cash *= 0.8;
		} else if (slave.energy <= 60) {							// slave has average sex drive
			cash *= 0.9;
		} else if (slave.energy <= 80) {							// slave has good sex drive
			cash *= 1.1;
		} else if (slave.energy <= 95) {							// slave has powerful sex drive
			cash *= 1.2;
		} else {													// slave is a nymphomaniac
			cash *= 1.3;
		}
		if (slave.fetish === "submissive") {
			cash *= 1.1;
		} else if (slave.fetish === "humiliation") {
			cash *= 1.1;
		} else if (slave.fetish === "masochist") {
			cash *= 1.1;
		}															// TODO: tie in quirks and flaws
		return cash;
	}
};


code:
window.FutureSocieties = (function() {
	"use strict";
	const FSString2Property = { // blame Hedonism and Eugenics for this
		Supremacist: "FSSupremacist",
		Subjugationist: "FSSubjugationist",
		GenderRadicalist: "FSGenderRadicalist",
		GenderFundamentalist: "FSGenderFundamentalist",
		Degradationist: "FSDegradationist",
		Paternalist: "FSPaternalist",
		BodyPurist: "FSBodyPurist",
		TransformationFetishist: "FSTransformationFetishist",
		YouthPreferentialist: "FSYouthPreferentialist",
		MaturityPreferentialist: "FSMaturityPreferentialist",
		SlimnessEnthusiast: "FSSlimnessEnthusiast",
		AssetExpansionist: "FSAssetExpansionist",
		Pastoralist: "FSPastoralist",
		PhysicalIdealist: "FSPhysicalIdealist",
		Hedonistic: "FSHedonisticDecadence",
		ChattelReligionist: "FSChattelReligionist",
		RomanRevivalist: "FSRomanRevivalist",
		EgyptianRevivalist: "FSEgyptianRevivalist",
		EdoRevivalist: "FSEdoRevivalist",
		ArabianRevivalist: "FSArabianRevivalist",
		ChineseRevivalist: "FSChineseRevivalist",
		AztecRevivalist: "FSAztecRevivalist",
		RepopulationFocus: "FSRepopulationFocus",
		Eugenics: "FSRestart"
	};
	const SocietyList = Object.keys(FSString2Property).map(key => FSString2Property[key]); // This returns an array containing the values of FSString2Property. E.g. "FSSupremacist" and "FSSubjugationist"

	return {
		remove: removeFS,
		DecorationCleanup: DecorationCleanup,
		Change: FSChange,
		ChangePorn: FSChangePorn,
		HighestDecoration: FSHighestDecoration
	};

	// call as FutureSocieties.remove(FS)
	// FS must be a string (e.g. "FSPaternalist" or "FSDegradationist").
	function removeFS(FS) {
		const V = State.variables;
		const arcology = V.arcologies[0];
		const FSDecoration = `${FS}Decoration`;
		const FSSMR = `${FS}SMR`;
		let FSLaw = `${FS}Law`;
		if (arcology[FS] === undefined) {
			// eslint-disable-next-line no-console
			console.log(`ERROR: bad FS reference, $arcologies[0].${FS} not defined`);
			return;
		}

		if (FS === "FSSupremacist" || FS === "FSSubjugationist") { FSLaw += "ME"; }
		if (FS !== "FSNull") { arcology[FSDecoration] = 20; }
		arcology[FS] = "unset";
		switch (FS) {
			case "FSPaternalist":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.slaveWatch = 0;
				break;
			case "FSDegradationist":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.liveTargets = 0;
				break;
			case "FSGenderRadicalist":
				arcology.FSGenderRadicalistLawBeauty = 0;
				arcology.FSGenderRadicalistLawFuta = 0;
				break;
			case "FSGenderFundamentalist":
				arcology.FSGenderFundamentalistLawBeauty = 0;
				arcology.FSGenderFundamentalistLawBimbo = 0;
				arcology.FSGenderFundamentalistSMR = 0;
				break;
			case "FSTransformationFetishist":
			case "FSAssetExpansionist":
				arcology[FSSMR] = 0;
				break;
			case "FSPhysicalIdealist":
				arcology.FSPhysicalIdealistLaw = 0;
				arcology.FSPhysicalIdealistSMR = 0;
				arcology.FSPhysicalIdealistStrongFat = 0;
				V.martialSchool = 0;
				break;
			case "FSHedonisticDecadence":
				arcology.FSHedonisticDecadenceLaw = 0;
				arcology.FSHedonisticDecadenceLaw2 = 0;
				arcology.FSHedonisticDecadenceSMR = 0;
				arcology.FSHedonisticDecadenceStrongFat = 0;
				break;
			case "FSChattelReligionist":
				arcology.FSChattelReligionistLaw = 0;
				arcology.FSChattelReligionistSMR = 0;
				arcology.FSChattelReligionistCreed = 0;
				V.subsidyChurch = 0;
				break;
			case "FSRepopulationFocus":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.universalRulesChildrenBecomeBreeders = 0;
				break;
			case "FSRestart":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.eliteOfficers = 0;
				V.propOutcome = 0;
				V.failedElite = 0;
				break;
			case "FSNull":
				break;
			default: // all others have one law and one SMR
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				break;
		}

		DecorationCleanup();
		resetFSCredits();
	}

	/* helper function, not callable */
	function resetFSCredits() {
		const V = State.variables;
		const arcology = V.arcologies[0];
		let activeFS = 0;
		for (let i = 0; i < SocietyList.length; i++) {
			if (arcology[SocietyList[i]] > 0) {
				activeFS++;
			}
		}
		if (arcology.FSNull > 0) { // multiculturalism is accounted for separately
			if (V.FSCreditCount === 4) {
				activeFS += arcology.FSNull / 25;
			} else if (V.FSCreditCount === 6) {
				activeFS += arcology.FSNull / 17;
			} else if (V.FSCreditCount === 7) {
				activeFS += arcology.FSNull / 15;
			} else {
				activeFS += arcology.FSNull / 20;
			}
		}
		V.FSCredits = Math.max(Math.trunc(V.FSGotRepCredits - activeFS), 0);
	}

	/* call as FutureSocieties.DecorationCleanup() */
	function DecorationCleanup() {
		ValidateFacilityDecoration("brothelDecoration");
		ValidateFacilityDecoration("dairyDecoration");
		ValidateFacilityDecoration("clubDecoration");
		ValidateFacilityDecoration("servantsQuartersDecoration");
		ValidateFacilityDecoration("schoolroomDecoration");
		ValidateFacilityDecoration("spaDecoration");
		ValidateFacilityDecoration("clinicDecoration");
		ValidateFacilityDecoration("arcadeDecoration");
		ValidateFacilityDecoration("cellblockDecoration");
		ValidateFacilityDecoration("masterSuiteDecoration");
		ValidateFacilityDecoration("nurseryDecoration");
		ValidateFacilityDecoration("farmyardDecoration");
	}

	/* helper function, not callable */
	/* decoration should be passed as "facilityDecoration" in quotes. For example, ValidateFacilityDecoration("brothelDecoration"). The quotes are important, do not pass it as a story variable. */
	function ValidateFacilityDecoration(decoration) {
		const V = State.variables;
		const FSString = V[decoration].replace(/ /g, ''); // removes spaces
		const activeFS = FSString2Property[FSString]; // gets the property name

		if (FSString === "standard") {
			// nothing to do
		} else if (activeFS === undefined) {
			// eslint-disable-next-line no-console
			console.log(`Error: $${decoration} is ${V[decoration]}`);
			V[decoration] = "standard";
		} else if (!Number.isFinite(V.arcologies[0][activeFS])) {
			if (V.arcologies[0][activeFS] !== "unset") {
				// eslint-disable-next-line no-console
				console.log(`Error: $arcologies[0].${activeFS} is ${V.arcologies[0][activeFS]}`);
			}
			V[decoration] = "standard";
		}
	}

	/* call as FutureSocieties.Change() */
	/* FSString should be in the FSString2Property object above */
	function FSChange(FSString, magnitude, bonusMultiplier = 1) {
		const V = State.variables;
		const arcology = V.arcologies[0];
		const activeFS = FSString2Property[FSString];

		if (activeFS === undefined) {
			return "ERROR: bad FS reference";
		} else if (Number.isFinite(arcology[activeFS])) {
			if (magnitude < 0) {
				repX(magnitude * V.FSSingleSlaveRep * (arcology[activeFS] / V.FSLockinLevel) / 3, 'futureSocieties'); // Reducing the reputation impact of slaves that are not adhering to societal ideals properly
			} else {
				repX(magnitude * V.FSSingleSlaveRep * (arcology[activeFS] / V.FSLockinLevel), 'futureSocieties');
			}
			arcology[activeFS] += 0.05 * magnitude * V.FSSingleSlaveRep * bonusMultiplier;
		}
	}

	/* call as FutureSocieties.ChangePorn() */
	/* FSString should be in the FSString2Property object above */
	function FSChangePorn(FSString, magnitude) {
		return FSChange(FSString, magnitude, State.variables.pornFameBonus);
	}

	/**
	 * Returns the highest decoration level of active future societies, call as FutureSocieties.HighestDecoration()
	 * @returns {number}
	 */
	function FSHighestDecoration() {
		const arcology = State.variables.arcologies[0];
		const decorationList = SocietyList.map(FS => `${FS}Decoration`);
		let level = 20; // All decorations start at 20

		for (let i = 0; i < decorationList.length; i++) {
			if (arcology[decorationList[i]] > level) {
				level = arcology[decorationList[i]];
			}
		}
		return level;
	}
})();