diff --git a/src/js/utils.js b/src/js/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..eca740c543cbcfbf5729fa9fbd766671d7152e05
--- /dev/null
+++ b/src/js/utils.js
@@ -0,0 +1,299 @@
+/* This file contains only JS functions without dependencies on FC specific variables/conventions and do not rely on
+ * custom functions outside this file
+ */
+
+/**
+ * Returns whether x is undefined. Port of SugarCube's def.
+ * @param {any} x
+ * @returns {boolean}
+ */
+window.jsDef = function(x) {
+	return (typeof x !== "undefined" && x !== null && x !== undefined);
+};
+
+/**
+ * @param {number} n
+ * @returns {boolean}
+ */
+window.isFloat = function(n) {
+	return Number.isFinite(n) && Math.floor(n) !== n;
+};
+
+/**
+ * Determines if a is between low and high
+ * @param {number} a
+ * @param {number} low
+ * @param {number} high
+ * @returns {boolean}
+ */
+window.between = function(a, low, high) {
+	if (low === null) { low = -Infinity; }
+	if (high === null) { high = Infinity; }
+	return (a > low && a < high);
+};
+
+/**
+ * @param {number[]} obj
+ * @returns {number}
+ */
+window.hashChoice = function hashChoice(obj) {
+	let randint = Math.floor(Math.random() * hashSum(obj));
+	let ret;
+	Object.keys(obj).some((key) => {
+		if (randint < obj[key]) {
+			ret = key;
+			return true;
+		} else {
+			randint -= obj[key];
+			return false;
+		}
+	});
+	return ret;
+};
+
+/**
+ * @param {number[]} obj
+ * @returns {number}
+ */
+window.hashSum = function hashSum(obj) {
+	let sum = 0;
+	Object.keys(obj).forEach((key) => {
+		sum += obj[key];
+	});
+	return sum;
+};
+
+/**
+ * @param {Array} arr
+ * @returns {Object}
+ */
+window.arr2obj = function arr2obj(arr) {
+	const obj = {};
+	arr.forEach((item) => {
+		obj[item] = 1;
+	});
+	return obj;
+};
+
+/**
+ * @param {{}} object
+ * @param rest
+ */
+window.hashPush = function hashPush(object, ...rest) {
+	rest.forEach((item) => {
+		if (object[item] === undefined) {
+			object[item] = 1;
+		} else {
+			object[item] += 1;
+		}
+	});
+};
+
+/**
+ * @param {[]} array
+ * @return {{}}
+ */
+window.weightedArray2HashMap = function weightedArray2HashMap(array) {
+	const obj = {};
+	array.forEach((item) => {
+		if (obj[item] === undefined) {
+			obj[item] = 1;
+		} else {
+			obj[item] += 1;
+		}
+	});
+	return obj;
+};
+
+/**
+ * generate a random, almost unique ID that is compliant (possibly) with RFC 4122
+ *
+ * @return {string}
+ */
+window.generateNewID = function generateNewID() {
+	let date = Date.now(); // high-precision timer
+	let uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
+		let r = (date + Math.random() * 16) % 16 | 0;
+		date = Math.floor(date / 16);
+		return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
+	});
+	return uuid;
+};
+
+/**
+ * @param {Array} array
+ * @param {number} indexA
+ * @param {number} indexB
+ */
+window.arraySwap = function arraySwap(array, indexA, indexB) {
+	const tmp = array[indexA];
+	array[indexA] = array[indexB];
+	array[indexB] = tmp;
+};
+
+/**
+ * @param {string} string
+ * @returns {string}
+ */
+window.capFirstChar = function capFirstChar(string) {
+	return string.charAt(0).toUpperCase() + string.substr(1);
+};
+
+/**
+ * @param {string} word
+ * @returns {string}
+ */
+window.addA = function(word) {
+	let vocal = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
+	if (vocal.includes(word.charAt(0))) {
+		return `an ${word}`;
+	}
+	return `a ${word}`;
+};
+
+/**
+ * @param {number} i
+ * @returns {string}
+ */
+window.ordinalSuffix = function ordinalSuffix(i) {
+	let j = i % 10;
+	let k = i % 100;
+	if (j === 1 && k !== 11) {
+		return `${i}st`;
+	}
+	if (j === 2 && k !== 12) {
+		return `${i}nd`;
+	}
+	if (j === 3 && k !== 13) {
+		return `${i}rd`;
+	}
+	return `${i}th`;
+};
+
+/**
+ * @param {number} i
+ * @returns {string}
+ */
+window.ordinalSuffixWords = function(i) {
+	const text = ["zeroth", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth"];
+	if (i < text.length) {
+		return text[i];
+	}
+	return ordinalSuffix(i);
+};
+
+/**
+ * @param {Iterable<any>} array
+ * @returns {any[]}
+ */
+window.removeDuplicates = function removeDuplicates(array) {
+	return [...new Set(array)];
+};
+
+/**
+ * Maps an index from one list onto a matching index on the other.
+ * The first and last indexes will be matched to the first and last indexes of the other list,
+ * while indexes in between will go to the nearest index.
+ * @param {number} index The index in original list to map to new list.
+ * @param {*} originalList The original list the index refers into.
+ * @param {*} newList The new list which we want an index for
+ * @returns {number} The new index into newList
+ */
+App.Utils.mapIndexBetweenLists = function(index, originalList, newList) {
+	if (index === 0) { return 0; }
+	if (index === originalList.length - 1) { return newList.length - 1; }
+	index--;
+	const originalLimitedLength = originalList.length - 2;
+	const newLimitedLength = newList.length - 2;
+	return Math.round((index / originalLimitedLength) * newLimitedLength) + 1;
+};
+
+/**
+ * replaces special HTML characters with their '&xxx' forms
+ * @param {string} text
+ * @returns {string}
+ */
+App.Utils.escapeHtml = function(text) {
+	const map = {
+		'&': '&amp;',
+		'<': '&lt;',
+		'>': '&gt;',
+		'"': '&quot;',
+		"'": '&#039;'
+	};
+	return text.replace(/[&<>"']/g, m => map[m]);
+};
+
+/**
+ * Creates an object where the items are accessible via their ids.
+ *
+ * @param {Iterable} list
+ * @return {{}}
+ */
+window.mapIdList = function(list) {
+	let mappedList = {};
+	for (const item of list) {
+		mappedList[item.id] = item;
+	}
+	return mappedList;
+};
+
+/**
+ * Topological sorting algorithm
+ * https://gist.github.com/shinout/1232505
+ * Added keys parameter since it better suits our needs and updated to project code style.
+ *
+ * @param {[]} keys
+ * @param {[[]]} edges: list of edges. each edge forms Array<ID,ID> e.g. [12 , 3]
+ * @returns Array: topological sorted list of IDs
+ * @throws Error: in case there is a closed chain.
+ **/
+App.Utils.topologicalSort = function(keys, edges) {
+	let nodes = {}; // hash: stringified id of the node => { id: id, afters: list of ids }
+	let sorted = []; // sorted list of IDs ( returned value )
+	let visited = {}; // hash: id of already visited node => true
+
+	const Node = function(id) {
+		this.id = id;
+		this.afters = [];
+	};
+
+	// 1. build data structures
+	keys.forEach(key => {
+		nodes[key] = new Node(key);
+	});
+
+	edges.forEach(edge => {
+		const from = edge[0], to = edge[1];
+		if (!nodes[from]) { nodes[from] = new Node(from); }
+		if (!nodes[to]) { nodes[to] = new Node(to); }
+		nodes[from].afters.push(to);
+	});
+
+	// 2. topological sort
+	Object.keys(nodes).forEach(function visit(idstr, ancestors) {
+		let node = nodes[idstr];
+		let id = node.id;
+
+		// if already exists, do nothing
+		if (visited[idstr]) { return; }
+
+		if (!Array.isArray(ancestors)) { ancestors = []; }
+
+		ancestors.push(id);
+
+		visited[idstr] = true;
+
+		node.afters.forEach((afterID) => {
+			if (ancestors.indexOf(afterID) >= 0) { // if already in ancestors, a closed chain exists.
+				throw new Error('closed chain : ' + afterID + ' is in ' + id);
+			}
+
+			visit(afterID.toString(), ancestors.map(v => v)); // recursive call
+		});
+
+		sorted.unshift(id);
+	});
+
+	return sorted;
+};
diff --git a/src/js/utilJS.js b/src/js/utilsFC.js
similarity index 88%
rename from src/js/utilJS.js
rename to src/js/utilsFC.js
index 21bf38bdb6474c8c1edada40f8f0f19a5f90a604..779c29ea515ba0353508e31633d4e05d4b6d8665 100644
--- a/src/js/utilJS.js
+++ b/src/js/utilsFC.js
@@ -1,3 +1,4 @@
+/* contains functions that rely on FC specific variables/conventions */
 /* eslint-disable no-unused-vars */
 /*
  * Height.mean(nationality, race, genes, age) - returns the mean height for the given combination and age in years (>=2)
@@ -1187,9 +1188,6 @@ window.years = function(weeks) {
 	return r;
 };
 window.asDate = function(weeks, bonusDay = 0) {
-	if (weeks == null) {
-		weeks = State.variables.week;
-	}
 	let d = new Date(2037, 0, 12);
 	d.setDate(d.getDate() + weeks * 7 + bonusDay);
 	return d;
@@ -1197,14 +1195,6 @@ window.asDate = function(weeks, bonusDay = 0) {
 window.asDateString = function(weeks, bonusDay = 0) {
 	return asDate(weeks, bonusDay).toLocaleString(undefined, {year: 'numeric', month: 'long', day: 'numeric'});
 };
-/**
- * Returns whether x is undefined
- * @param {any} x
- * @returns {boolean}
- */
-window.jsDef = function(x) {
-	return (typeof x !== "undefined" && x !== null && x !== undefined);
-};
 
 /**
  * @param {number} s
@@ -1341,14 +1331,6 @@ window.budgetLine = function(category, title) {
 	}
 };
 
-/**
- * @param {number} n
- * @returns {boolean}
- */
-window.isFloat = function(n) {
-	return Number.isFinite(n) && Math.floor(n) !== n;
-};
-
 /*
 Make everything waiting for this execute. Usage:
 
@@ -1363,150 +1345,6 @@ if(typeof Categorizer === 'function') {
 */
 jQuery(document).trigger("categorizer.ready");
 
-/**
- * @param {number[]} obj
- * @returns {number}
- */
-window.hashChoice = function hashChoice(obj) {
-	let randint = Math.floor(Math.random() * hashSum(obj));
-	let ret;
-	Object.keys(obj).some((key) => {
-		if (randint < obj[key]) {
-			ret = key;
-			return true;
-		} else {
-			randint -= obj[key];
-			return false;
-		}
-	});
-	return ret;
-};
-
-/**
- * @param {number[]} obj
- * @returns {number}
- */
-window.hashSum = function hashSum(obj) {
-	let sum = 0;
-	Object.keys(obj).forEach((key) => {
-		sum += obj[key];
-	});
-	return sum;
-};
-
-/**
- * @param {Array} arr
- * @returns {Object}
- */
-window.arr2obj = function arr2obj(arr) {
-	const obj = {};
-	arr.forEach((item) => {
-		obj[item] = 1;
-	});
-	return obj;
-};
-
-window.hashPush = function hashPush(obj, ...rest) {
-	rest.forEach((item) => {
-		if (obj[item] === undefined) {
-			obj[item] = 1;
-		} else {
-			obj[item] += 1;
-		}
-	});
-};
-
-window.weightedArray2HashMap = function weightedArray2HashMap(arr) {
-	const obj = {};
-	arr.forEach((item) => {
-		if (obj[item] === undefined) {
-			obj[item] = 1;
-		} else {
-			obj[item] += 1;
-		}
-	});
-	return obj;
-};
-
-/**
- * Determines if a is between low and high
- * @param {number} a
- * @param {number} low
- * @param {number} high
- * @returns {boolean}
- */
-window.between = function between(a, low, high) {
-	if (low === null) { low = -Infinity; }
-	if (high === null) { high = Infinity; }
-	return (a > low && a < high);
-};
-
-// generate a random, almost unique ID that is compliant (possibly) with RFC 4122
-window.generateNewID = function generateNewID() {
-	let date = Date.now(); // high-precision timer
-	let uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
-		let r = (date + Math.random() * 16) % 16 | 0;
-		date = Math.floor(date / 16);
-		return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
-	});
-	return uuid;
-};
-
-/**
- * @param {Array} array
- * @param {number} a
- * @param {number} b
- */
-window.arraySwap = function arraySwap(array, a, b) {
-	const tmp = array[a];
-	array[a] = array[b];
-	array[b] = tmp;
-};
-
-// circumvents sugarcube, allowing a plain HTML5 UI within it
-window.html5passage = function html5passage(passageFunction) {
-	$(document).one(":passagedisplay", (ev) => {
-		const element = document.createElement("div");
-		element.classList.add("passage");
-		document.getElementById("passages").appendChild(element);
-		passageFunction(element);
-		$(document).off(":passagedisplay");
-	});
-};
-
-/**
- * If you want to include a SugarCube passage in a JS function use this. The result must be printed using the <<print>> macro.
- * @param {string} passageTitle
- * @returns {string}
- */
-window.jsInclude = function(passageTitle) {
-	if (Story.has(passageTitle)) {
-		return Story.get(passageTitle).processText();
-	} else {
-		return `<span class="red">Error: Passage ${passageTitle} does not exist.</span>`;
-	}
-};
-
-/**
- * @param {string} string
- * @returns {string}
- */
-window.capFirstChar = function capFirstChar(string) {
-	return string.charAt(0).toUpperCase() + string.substr(1);
-};
-
-/**
- * @param {string} word
- * @returns {string}
- */
-window.addA = function(word) {
-	let vocal = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
-	if (vocal.includes(word.charAt(0))) {
-		return `an ${word}`;
-	}
-	return `a ${word}`;
-};
-
 /**
  * @param {App.Entity.SlaveState} slave
  * @returns {string}
@@ -1688,45 +1526,6 @@ window.lengthToEitherUnit = function(s) {
 	return `${s}cm`;
 };
 
-/**
- * @param {number} i
- * @returns {string}
- */
-window.ordinalSuffix = function ordinalSuffix(i) {
-	let j = i % 10;
-	let k = i % 100;
-	if (j === 1 && k !== 11) {
-		return `${i}st`;
-	}
-	if (j === 2 && k !== 12) {
-		return `${i}nd`;
-	}
-	if (j === 3 && k !== 13) {
-		return `${i}rd`;
-	}
-	return `${i}th`;
-};
-
-/**
- * @param {number} i
- * @returns {string}
- */
-window.ordinalSuffixWords = function(i) {
-	const text = ["zeroth", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth"];
-	if (i < text.length) {
-		return text[i];
-	}
-	return ordinalSuffix(i);
-};
-
-/**
- * @param {Iterable<any>} array
- * @returns {any[]}
- */
-window.removeDuplicates = function removeDuplicates(array) {
-	return [...new Set(array)];
-};
-
 /**
  * @param {App.Entity.SlaveState} slave
  * @returns {string}
@@ -2331,220 +2130,6 @@ window.convertCareer = function(slave) {
 	return job;
 };
 
-App.UI.tabbar = function() {
-	return {
-		openTab: openTab,
-		tabButton: tabButton,
-		makeTab: makeTab,
-		handlePreSelectedTab: handlePreSelectedTab,
-		tabChoiceVarName: tabChoiceVarName
-	};
-
-	function openTab(evt, tabName) {
-		/* var passage = passage().trim().replace(/ /g,"+");*/
-		const tabcontent = document.getElementsByClassName("tabcontent");
-		for (let i = 0; i < tabcontent.length; i++) {
-			tabcontent[i].style.display = "none";
-		}
-		const tablinks = document.getElementsByClassName("tablinks");
-		for (let i = 0; i < tablinks.length; i++) {
-			tablinks[i].className = tablinks[i].className.replace(" active", "");
-		}
-		V.tabChoice[tabChoiceVarName()] = tabName; /* The regex strips spaces and " ' " from passage names, making "Servants' Quarters" into "ServantsQuarters" and allowing it to be used as a label in this object. */
-		document.getElementById(tabName).style.display = "block";
-		evt.currentTarget.className += " active";
-	}
-
-	/**
-	 * @param {string} name
-	 * @param {string} text
-	 * @returns {string}
-	 */
-	function tabButton(name, text) {
-		return `<button class="tablinks" onclick="App.UI.tabbar.openTab(event, '${name}')" id="tab ${name}">${text}</button>`;
-	}
-
-	/**
-	 * @param {string} name
-	 * @param {string} content
-	 * @returns {string}
-	 */
-	function makeTab(name, content) {
-		return `<div id="${name}" class="tabcontent"><div class="content">${content}</div></div>`;
-	}
-
-	function handlePreSelectedTab(defaultTab = "assign", immidiate = false) {
-		let selectedTab = State.variables.tabChoice[tabChoiceVarName()];
-		if (!selectedTab) { selectedTab = defaultTab; }
-		function selectTab() {
-			let tabBtn = document.getElementById(`tab ${selectedTab}`);
-			if (!tabBtn) {
-				tabBtn = document.getElementsByClassName('tablinks').item(0);
-			}
-			if (tabBtn) { tabBtn.click(); }
-		}
-		if (immidiate) {
-			selectTab();
-		} else {
-			$(document).one(':passagedisplay', selectTab);
-		}
-	}
-
-	function tabChoiceVarName() {
-		return passage().trim().replace(/ |'/g, '');
-	}
-}();
-
-
-/**
- * replaces special HTML characters with their '&xxx' forms
- * @param {string} text
- * @returns {string}
- */
-App.Utils.escapeHtml = function(text) {
-	const map = {
-		'&': '&amp;',
-		'<': '&lt;',
-		'>': '&gt;',
-		'"': '&quot;',
-		"'": '&#039;'
-	};
-	return text.replace(/[&<>"']/g, m => map[m]);
-};
-/**
- * Maps an index from one list onto a matching index on the other.
- * The first and last indexes will be matched to the first and last indexes of the other list,
- * while indexes in between will go to the nearest index.
- * @param {number} index The index in original list to map to new list.
- * @param {*} originalList The original list the index refers into.
- * @param {*} newList The new list which we want an index for
- * @returns {number} The new index into newList
- */
-App.Utils.mapIndexBetweenLists = function(index, originalList, newList) {
-	if (index === 0) { return 0; }
-	if (index === originalList.length - 1) { return newList.length - 1; }
-	index--;
-	const originalLimitedLength = originalList.length - 2;
-	const newLimitedLength = newList.length - 2;
-	return Math.round((index / originalLimitedLength) * newLimitedLength) + 1;
-};
-/**
- * Creates a HTML element with custom SugarCube attributes which works as a passage link
- *
- * The result works in the same way as the wiki markup in the SugarCube
- * @see https://www.motoslave.net/sugarcube/2/docs/#markup-html-attribute
- * @param {string} linkText link text
- * @param {string} passage the passage name to link to
- * @param {string} [setter=''] setter text (optional)
- * @param {string} [tooltip=''] tooltip text (optional)
- * @param {string} [elementType='a'] element type (optional) default is 'a'.
- * Could be any of 'a', 'audio', img', 'source', 'video'
- * @returns {string} element text
- *
- * @example
- * // equal to [[Go to town|Town]]
- * App.UI.passageLink("Go to town", "Town")
- */
-App.UI.passageLink = function(linkText, passage, setter, tooltip = '', elementType = 'a') {
-	let res = `<${elementType} data-passage="${passage}"`;
-	if (setter) {
-		res += ` data-setter="${App.Utils.escapeHtml(setter)}"`;
-	}
-	if (tooltip) {
-		res += ` title="${tooltip}"`;
-	}
-	res += `>${linkText}</${elementType}>`;
-	return res;
-};
-
-App.UI.link = function() {
-	let counter = 0;
-
-	// reset all handlers for each passage
-	$(document).on(':passageinit', function() {
-		State.temporary.linkHandlers = {};
-		counter = 0;
-	});
-
-	return makeLink;
-
-	/**
-	 * Creates a markup for a SugarCube link which executes given function with given arguments
-	 *
-	 * @param {string} linkText link text
-	 * @param {*} handler callable object
-	 * @param {*} args arguments
-	 * @param {string} [passage] the passage name to link to
-	 * @returns {string} link in SC markup
-	 */
-	function makeLink(linkText, handler, args = [], passage = '', tooltip = '') {
-		// pack handler and data
-		State.temporary.linkHandlers[counter] = {
-			f: handler,
-			args: Array.isArray(args) ? args : [args]
-		};
-
-		// can't say _linkHandlers here because SC does not recognize its own notation in "..._varName"
-		let SCHandlerText =
-			`State.temporary.linkHandlers[${counter}].f(...State.temporary.linkHandlers[${counter}].args);`;
-		++counter;
-
-		if (passage) {
-			return App.UI.passageLink(linkText, passage, SCHandlerText, tooltip);
-		} else {
-			if (tooltip) {
-				throw "Tooltips are not supported by the <<link>> markup.";
-			}
-			// data-passage scheme does not work with empty passage name
-			return `<<link "${linkText}">><<run ${SCHandlerText}>><</link>>`;
-		}
-	}
-}();
-
-/**
- * Replaces contents of the element, identified by the given selector, with wiki'ed new content
- *
- * The function is an analogue to the SugarCube <<replace>> macro (and is a simplified version of it)
- * @param {string} selector
- * @param {string} newContent
- */
-App.UI.replace = function(selector, newContent) {
-	let ins = jQuery(document.createDocumentFragment());
-	ins.wiki(newContent);
-	const target = $(selector);
-	target.empty();
-	target.append(ins);
-};
-
-/**
- * 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.
- *
- * @example
- * htag('test', "red") // <div id="red">test</div>
- * htag('test', {class: red}); // <div class="red">test</div>
- * htag('test', {class: red, id: green}); // <div class="red" id="green">test</div>
- * @param {string} text
- * @param {string|object} attributes
- * @param {string} [tag='div']
- * @returns {string}
- */
-App.UI.htag = function(text, attributes, tag = 'div') {
-	const payload = text.replace(/(^\n+|\n+$)/, "");
-
-	if (typeof attributes === "object") {
-		attributes = $.map(attributes, (val, key) => `${key}="${val}"`).join(" ");
-	} else {
-		attributes = `id="${attributes.trim()}"`;
-	}
-
-	return `<${tag} ${attributes}>${payload}</${tag}>`;
-};
-
 window.SkillIncrease = (function() {
 	return {
 		Oral: OralSkillIncrease,
@@ -3079,19 +2664,6 @@ window.changeSkinTone = function(skin, value) {
 	return prop;
 };
 
-/**
- * Creates a span for an link with tooltip containing the reasons why it is disabled
- * @param {string} link
- * @param {string[]} reasons
- * @returns {string}
- */
-App.UI.disabledLink = function(link, reasons) {
-	const tooltips = reasons.length === 1 ?
-		`<span class="tooltip">${reasons}</span>` :
-		`<div class="tooltip"><ul>${reasons.map(e => `<li>${e}</li>`).join('')}</ul></div>`;
-	return `<span class="textWithTooltip">${link}${tooltips}</span>`;
-};
-
 /**
  * Expression for SugarCube for referencing a slave by index
  * @param {number} i slave array index or -1 for activeSlave
@@ -3209,15 +2781,6 @@ window.moreNational = function(nation) {
 	return country;
 };
 
-//Creates an object where the items are accessible via their ids.
-window.mapIdList = function(list) {
-	let mappedList = {};
-	for (const item of list) {
-		mappedList[item.id] = item;
-	}
-	return mappedList;
-};
-
 /**
  * Returns a "disobedience factor" between 0 (perfectly obedient) and 100 (completely defiant)
  * @param {App.Entity.SlaveState} slave
@@ -3265,63 +2828,3 @@ window.randomRapeRivalryTarget = function(slave, predicate) {
 	const arr = V.slaves.filter((s) => { return canBeARapeRival(s) && canRape(slave, s); }).shuffle();
 	return arr.find(predicate);
 };
-
-/**
- * Topological sorting algorithm
- * https://gist.github.com/shinout/1232505
- * Added keys parameter since it better suits our needs and updated to project code style.
- *
- * @param {[]} keys
- * @param {[[]]} edges: list of edges. each edge forms Array<ID,ID> e.g. [12 , 3]
- * @returns Array: topological sorted list of IDs
- * @throws Error: in case there is a closed chain.
- **/
-App.Utils.topologicalSort = function(keys, edges) {
-	let nodes = {}; // hash: stringified id of the node => { id: id, afters: list of ids }
-	let sorted = []; // sorted list of IDs ( returned value )
-	let visited = {}; // hash: id of already visited node => true
-
-	const Node = function(id) {
-		this.id = id;
-		this.afters = [];
-	};
-
-	// 1. build data structures
-	keys.forEach(key => {
-		nodes[key] = new Node(key);
-	});
-
-	edges.forEach(edge => {
-		const from = edge[0], to = edge[1];
-		if (!nodes[from]) { nodes[from] = new Node(from); }
-		if (!nodes[to]) { nodes[to] = new Node(to); }
-		nodes[from].afters.push(to);
-	});
-
-	// 2. topological sort
-	Object.keys(nodes).forEach(function visit(idstr, ancestors) {
-		let node = nodes[idstr];
-		let id = node.id;
-
-		// if already exists, do nothing
-		if (visited[idstr]) { return; }
-
-		if (!Array.isArray(ancestors)) { ancestors = []; }
-
-		ancestors.push(id);
-
-		visited[idstr] = true;
-
-		node.afters.forEach((afterID) => {
-			if (ancestors.indexOf(afterID) >= 0) { // if already in ancestors, a closed chain exists.
-				throw new Error('closed chain : ' + afterID + ' is in ' + id);
-			}
-
-			visit(afterID.toString(), ancestors.map(v => v)); // recursive call
-		});
-
-		sorted.unshift(id);
-	});
-
-	return sorted;
-};
diff --git a/src/js/utilsSC.js b/src/js/utilsSC.js
new file mode 100644
index 0000000000000000000000000000000000000000..44adff3c742af50971702ebc3ca999706085c351
--- /dev/null
+++ b/src/js/utilsSC.js
@@ -0,0 +1,221 @@
+/**
+ * circumvents SugarCube, allowing a plain HTML5 UI within it
+ *
+ * @param passageFunction
+ */
+window.html5passage = function html5passage(passageFunction) {
+	$(document).one(":passagedisplay", (ev) => {
+		const element = document.createElement("div");
+		element.classList.add("passage");
+		document.getElementById("passages").appendChild(element);
+		passageFunction(element);
+		$(document).off(":passagedisplay");
+	});
+};
+
+/**
+ * If you want to include a SugarCube passage in a JS function use this. The result must be printed using the <<print>> macro.
+ * @param {string} passageTitle
+ * @returns {string}
+ */
+window.jsInclude = function(passageTitle) {
+	if (Story.has(passageTitle)) {
+		return Story.get(passageTitle).processText();
+	} else {
+		return `<span class="red">Error: Passage ${passageTitle} does not exist.</span>`;
+	}
+};
+
+/**
+ * Creates a HTML element with custom SugarCube attributes which works as a passage link
+ *
+ * The result works in the same way as the wiki markup in the SugarCube
+ * @see https://www.motoslave.net/sugarcube/2/docs/#markup-html-attribute
+ * @param {string} linkText link text
+ * @param {string} passage the passage name to link to
+ * @param {string} [setter=''] setter text (optional)
+ * @param {string} [tooltip=''] tooltip text (optional)
+ * @param {string} [elementType='a'] element type (optional) default is 'a'.
+ * Could be any of 'a', 'audio', img', 'source', 'video'
+ * @returns {string} element text
+ *
+ * @example
+ * // equal to [[Go to town|Town]]
+ * App.UI.passageLink("Go to town", "Town")
+ */
+App.UI.passageLink = function(linkText, passage, setter, tooltip = '', elementType = 'a') {
+	let res = `<${elementType} data-passage="${passage}"`;
+	if (setter) {
+		res += ` data-setter="${App.Utils.escapeHtml(setter)}"`;
+	}
+	if (tooltip) {
+		res += ` title="${tooltip}"`;
+	}
+	res += `>${linkText}</${elementType}>`;
+	return res;
+};
+
+App.UI.link = function() {
+	let counter = 0;
+
+	// reset all handlers for each passage
+	$(document).on(':passageinit', function() {
+		State.temporary.linkHandlers = {};
+		counter = 0;
+	});
+
+	return makeLink;
+
+	/**
+	 * Creates a markup for a SugarCube link which executes given function with given arguments
+	 *
+	 * @param {string} linkText link text
+	 * @param {*} handler callable object
+	 * @param {*} args arguments
+	 * @param {string} [passage] the passage name to link to
+	 * @returns {string} link in SC markup
+	 */
+	function makeLink(linkText, handler, args = [], passage = '', tooltip = '') {
+		// pack handler and data
+		State.temporary.linkHandlers[counter] = {
+			f: handler,
+			args: Array.isArray(args) ? args : [args]
+		};
+
+		// can't say _linkHandlers here because SC does not recognize its own notation in "..._varName"
+		let SCHandlerText =
+			`State.temporary.linkHandlers[${counter}].f(...State.temporary.linkHandlers[${counter}].args);`;
+		++counter;
+
+		if (passage) {
+			return App.UI.passageLink(linkText, passage, SCHandlerText, tooltip);
+		} else {
+			if (tooltip) {
+				throw "Tooltips are not supported by the <<link>> markup.";
+			}
+			// data-passage scheme does not work with empty passage name
+			return `<<link "${linkText}">><<run ${SCHandlerText}>><</link>>`;
+		}
+	}
+}();
+
+/**
+ * Replaces contents of the element, identified by the given selector, with wiki'ed new content
+ *
+ * The function is an analogue to the SugarCube <<replace>> macro (and is a simplified version of it)
+ * @param {string} selector
+ * @param {string} newContent
+ */
+App.UI.replace = function(selector, newContent) {
+	let ins = jQuery(document.createDocumentFragment());
+	ins.wiki(newContent);
+	const target = $(selector);
+	target.empty();
+	target.append(ins);
+};
+
+/**
+ * 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.
+ *
+ * @example
+ * htag('test', "red") // <div id="red">test</div>
+ * htag('test', {class: red}); // <div class="red">test</div>
+ * htag('test', {class: red, id: green}); // <div class="red" id="green">test</div>
+ * @param {string} text
+ * @param {string|object} attributes
+ * @param {string} [tag='div']
+ * @returns {string}
+ */
+App.UI.htag = function(text, attributes, tag = 'div') {
+	const payload = text.replace(/(^\n+|\n+$)/, "");
+
+	if (typeof attributes === "object") {
+		attributes = $.map(attributes, (val, key) => `${key}="${val}"`).join(" ");
+	} else {
+		attributes = `id="${attributes.trim()}"`;
+	}
+
+	return `<${tag} ${attributes}>${payload}</${tag}>`;
+};
+
+App.UI.tabbar = function() {
+	return {
+		openTab: openTab,
+		tabButton: tabButton,
+		makeTab: makeTab,
+		handlePreSelectedTab: handlePreSelectedTab,
+		tabChoiceVarName: tabChoiceVarName
+	};
+
+	function openTab(evt, tabName) {
+		/* var passage = passage().trim().replace(/ /g,"+");*/
+		const tabcontent = document.getElementsByClassName("tabcontent");
+		for (let i = 0; i < tabcontent.length; i++) {
+			tabcontent[i].style.display = "none";
+		}
+		const tablinks = document.getElementsByClassName("tablinks");
+		for (let i = 0; i < tablinks.length; i++) {
+			tablinks[i].className = tablinks[i].className.replace(" active", "");
+		}
+		V.tabChoice[tabChoiceVarName()] = tabName; /* The regex strips spaces and " ' " from passage names, making "Servants' Quarters" into "ServantsQuarters" and allowing it to be used as a label in this object. */
+		document.getElementById(tabName).style.display = "block";
+		evt.currentTarget.className += " active";
+	}
+
+	/**
+	 * @param {string} name
+	 * @param {string} text
+	 * @returns {string}
+	 */
+	function tabButton(name, text) {
+		return `<button class="tablinks" onclick="App.UI.tabbar.openTab(event, '${name}')" id="tab ${name}">${text}</button>`;
+	}
+
+	/**
+	 * @param {string} name
+	 * @param {string} content
+	 * @returns {string}
+	 */
+	function makeTab(name, content) {
+		return `<div id="${name}" class="tabcontent"><div class="content">${content}</div></div>`;
+	}
+
+	function handlePreSelectedTab(defaultTab = "assign", immidiate = false) {
+		let selectedTab = State.variables.tabChoice[tabChoiceVarName()];
+		if (!selectedTab) { selectedTab = defaultTab; }
+		function selectTab() {
+			let tabBtn = document.getElementById(`tab ${selectedTab}`);
+			if (!tabBtn) {
+				tabBtn = document.getElementsByClassName('tablinks').item(0);
+			}
+			if (tabBtn) { tabBtn.click(); }
+		}
+		if (immidiate) {
+			selectTab();
+		} else {
+			$(document).one(':passagedisplay', selectTab);
+		}
+	}
+
+	function tabChoiceVarName() {
+		return passage().trim().replace(/ |'/g, '');
+	}
+}();
+
+/**
+ * Creates a span for an link with tooltip containing the reasons why it is disabled
+ * @param {string} link
+ * @param {string[]} reasons
+ * @returns {string}
+ */
+App.UI.disabledLink = function(link, reasons) {
+	const tooltips = reasons.length === 1 ?
+		`<span class="tooltip">${reasons}</span>` :
+		`<div class="tooltip"><ul>${reasons.map(e => `<li>${e}</li>`).join('')}</ul></div>`;
+	return `<span class="textWithTooltip">${link}${tooltips}</span>`;
+};