code:
App.Underperformers = {};

/** Select only slaves which are not reasonably expected to produce any income (brand new slaves, servants, fucktoys, etc)
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
App.Underperformers.expectIncome = function(slave) {
	const productionJobs = [Job.ARCADE, Job.MILKED, Job.WHORE, Job.GLORYHOLE, Job.BROTHEL, Job.DAIRY];
	return productionJobs.includes(slave.assignment) && ((slave.weekAcquired + 1) < V.week) && _.isFinite(slave.lastWeeksCashIncome);
};

App.Underperformers.highSale = function() {
	const description = App.UI.DOM.makeElement("div", "Take the rough value of a slave and divide it by how much they made overall last week. This will tell you how many weeks it might take them to earn the same amount you'd get for selling them right now.", "note");

	const frag = App.UI.SlaveList.render(
		getBestSlavesIDs(
			{
				part:(slave) => {
					const ratio = slaveCost(slave) / (slave.lastWeeksCashIncome - getSlaveCost(slave));
					return ratio > 0 ? ratio : 100000000 + ratio;
				},
				count: 7,
				filter: App.Underperformers.expectIncome
			}
		),
		[],
		App.UI.SlaveList.SlaveInteract.stdInteract,
		(slave) => $(document.createDocumentFragment()).append(
			`Worth ${cashFormatColor(slaveCost(slave))} / Nets ${cashFormatColor(slave.lastWeeksCashIncome - getSlaveCost(slave))} a week = ${(Math.trunc(slaveCost(slave) / (slave.lastWeeksCashIncome - getSlaveCost(slave)))) > 0 ? (Math.trunc(slaveCost(slave) / (slave.lastWeeksCashIncome - getSlaveCost(slave)))) : "infinite"} weeks`
		).get(0)
	);

	frag.prepend(description);
	return frag;
};

App.Underperformers.expensive = function() {
	const description = App.UI.DOM.makeElement("div", "This list looks for moochers by weighing their weekly income against the weekly cost of providing for them.", "note");

	const frag = App.UI.SlaveList.render(
		getBestSlavesIDs(
			{
				part:(slave) => (slave.lastWeeksCashIncome - getSlaveCost(slave)),
				largest: false,
				count: 7,
				filter: App.Underperformers.expectIncome
			}
		),
		[],
		App.UI.SlaveList.SlaveInteract.stdInteract,
		(slave) => $(document.createDocumentFragment()).append(
			`${cashFormatColor(Math.trunc(slave.lastWeeksCashIncome - getSlaveCost(slave)))} net last week`
		).get(0)
	);

	frag.prepend(description);
	return frag;
};


code:
/**
 * @callback passageLinkHandler
 * @returns {void}
 */
/**
 * 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 {passageLinkHandler} [handler] 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 {HTMLElement} element text
 *
 * @example
 * // equal to [[Go to town|Town]]
 * App.UI.passageLink("Go to town", "Town")
 */
App.UI.DOM.passageLink = function(linkText, passage, handler, tooltip = '', elementType = 'a') {
	let res = document.createElement(elementType);
	res.setAttribute("data-passage", passage);
	res.onclick = (ev) => {
		ev.preventDefault();
		if (handler) {
			handler();
		}
		Engine.play(passage);
	};

	if (tooltip) {
		res.title = tooltip;
	}
	res.textContent = linkText;
	return res;
};

/**
 * Returns link element for an assignment
 * @param {App.Entity.SlaveState} slave
 * @param {string} assignment
 * @param {string} [passage] passage to go to
 * @param {assignmentCallback} [action] action that changes slave state. The default one is a call to assignJob()
 * @param {string} [linkText]
 * @returns {HTMLAnchorElement}
 */
App.UI.DOM.assignmentLink = function(slave, assignment, passage, action, linkText) {
	let res = document.createElement("a");
	res.textContent = linkText;
	res.onclick = (e) => {
		e.preventDefault();
		if (action) {
			action(slave, assignment);
		} else {
			assignJob(slave, assignment);
		}
		if (passage !== '') {
			SugarCube.Engine.play(passage);
		}
	};
	return res;
};

/**
 * Creates a markup for a SugarCube link which executes given function with given arguments
 *
 * @param {string} linkText link text
 * @param {function(...any):void} handler callable object
 * @param {any|Array<any>} args arguments
 * @param {string} [passage] the passage name to link to
 * @param {string} [tooltip]
 * @returns {HTMLAnchorElement} link in SC markup
 */
App.UI.DOM.link = function(linkText, handler, args = [], passage = "", tooltip = "") {
	const hArgs = Array.isArray(args) ? args : [args];
	const link = document.createElement("a");
	link.textContent = linkText;
	link.title = tooltip;
	link.onclick = () => {
		handler(...hArgs);
		if (passage !== '') {
			SugarCube.Engine.play(passage);
		}
	};
	return link;
};

/**
 * Creates a span for an link with tooltip containing the reasons why it is disabled
 * @param {string} link
 * @param {string[]} reasons
 * @returns {HTMLSpanElement}
 */
App.UI.DOM.disabledLink = function(link, reasons) {
	/** @type {HTMLElement} */
	let tooltip;
	if (reasons.length === 1) {
		tooltip = document.createElement("span");
		tooltip.textContent = reasons[0];
	} else {
		tooltip = document.createElement("div");
		let ul = document.createElement("ul");
		tooltip.appendChild(ul);
		for (const li of reasons.map(r => {
			const li = document.createElement("li");
			li.textContent = r;
			return li;
		})) {
			ul.appendChild(li);
		}
	}
	tooltip.className = "tooltip";
	let res = document.createElement("span");
	res.textContent = link;
	res.className = "textWithTooltip";
	res.appendChild(tooltip);
	return res;
};

/**
 * @param {string} tag - valid HTML tag
 * @param {string|Node} [content]
 * @param {string|Array<string>} [classNames]
 * @returns {HTMLElement}
 */
// @ts-ignore
App.UI.DOM.makeElement = function(tag, content, classNames) {
	const element = document.createElement(tag);
	if (classNames !== undefined) {
		if (Array.isArray(classNames)) {
			element.classList.add(...classNames);
		} else {
			element.classList.add(classNames);
		}
	}
	if (content) {
		element.append(content);
	}
	return element;
};

/**
 * @param {string} tag - valid HTML tag
 * @param {ParentNode} parent
 * @param {string|Node} [content]
 * @param {string|Array<string>} [classNames]
 * @returns {HTMLElement}
 */
// @ts-ignore
App.UI.DOM.appendNewElement = function(tag, parent, content, classNames) {
	// @ts-ignore
	const element = App.UI.DOM.makeElement(tag, content, classNames);
	parent.append(element);
	return element;
};

/**
 * @param {string} linkText
 * @param {string|Node} newContent
 * @returns {HTMLSpanElement}
 */
App.UI.DOM.linkReplace = function(linkText, newContent) {
	const span = document.createElement("span");
	span.append(App.UI.DOM.link(linkText, () => {
		span.innerHTML = "";
		span.append(newContent);
	}));
	return span;
};

/**
 * @param {string} selector for jQuery
 * @param {JQuery.Node} newContent
 */
App.UI.DOM.replace = function(selector, newContent) {
	$(selector).empty().append(newContent);
};

/**
 * @param {string} passage
 * @returns {Element}
 */
App.Utils.passageElement = function(passage) {
	return document.querySelector(`tw-passagedata[name="${passage}"]`);
};

/**
 * @param {(Node|string)[]} content
 * @returns {DocumentFragment}
 */
App.UI.DOM.combineNodes = function(...content) {
	let fragment = document.createDocumentFragment();
	fragment.append(...content);
	return fragment;
};

/**
 * @param {string|number} defaultValue
 * @param {function(string):void|function(number):void} onEnter - accepts number if numberOnly = true, otherwise string
 * @param {boolean} [numberOnly]
 * @returns {HTMLInputElement}
 */
App.UI.DOM.makeTextBox = function(defaultValue, onEnter, numberOnly = false) {
	const input = document.createElement("input");
	input.type = "text";
	input.value = defaultValue;

	let updateValue;
	if (numberOnly) {
		/*
		We could use input.type = "number", but at least in firefox submitting an invalid value will set input.value
		to 0 and trigger a change event we can't distinguish from setting the value to 0 explicitly.
		The workaround is resetting the value to the last known valid value and not triggering onEnter.
		*/
		let oldValue = defaultValue;
		updateValue = event => {
			const newValue = Number(event.target.value);
			if (!Number.isNaN(newValue)) {
				onEnter(newValue);
				oldValue = newValue;
			} else {
				// reset the value to the last known valid value
				event.target.value = oldValue;
			}
		};
	} else {
		updateValue = e => {
			onEnter(e.target.value);
		};
	}
	input.addEventListener('change', updateValue);

	return input;
};

/**
 * @param {string|number} defaultValue
 * @param {function(string):void} onEnter
 * @returns {HTMLInputElement}
 */
App.UI.DOM.colorInput = function(defaultValue, onEnter) {
	const input = document.createElement("input");
	input.type = "color";
	input.value = defaultValue;

	input.addEventListener('change', e => { onEnter(e.target.value); });

	return input;
};

/**
 * Concats an array of DOM nodes or strings into a human readable list.
 *
 * @param {Array<Node|string>} content
 * @param {string} [delimiter]
 * @param {string} [lastDelimiter]
 * @returns {DocumentFragment|string}
 */
App.UI.DOM.arrayToList = function(content, delimiter = ", ", lastDelimiter = " and ") {
	if (content.length === 0) {
		return "none";
	}
	if (content.length === 1) {
		return content[0];
	}
	const fragment = document.createDocumentFragment();
	const last = content.pop();
	for (let i = 0; i < content.length; i++) {
		fragment.append(content[i]);
		if (i < content.length - 1) {
			fragment.append(delimiter);
		}
	}
	content.push(last); // don't leave the array modified
	fragment.append(lastDelimiter, last);
	return fragment;
};

/**
 * @param {function (*):void} callback
 * @param {Object.<string, *>} options
 * @param {*|function (): *} currentValue
 * @param {string} [description]
 * @returns {HTMLSpanElement}
 */
App.UI.DOM.Widgets.optionSelector = function(callback, options, currentValue, description) {
	const res = document.createElement("span");
	res.classList.add("optionMacro");
	if (description) {
		App.UI.DOM.appendNewElement("span", res, description, "optionDescription");
	}
	const curVal = (currentValue instanceof Function) ? currentValue() : currentValue;
	const valueNode = App.UI.DOM.appendNewElement("span", res, null, "optionValue");
	for (const s in options) {
		const v = options[s];
		const choice = App.UI.DOM.appendNewElement("span", valueNode, null, "optionMacroOption");
		if (v === curVal) {
			choice.classList.add("optionMacroSelected");
			choice.innerText = s;
		} else {
			choice.append(App.UI.DOM.link(s, () => { callback(v); }));
		}
	}
	return res;
};

/**
 * @param {string} text
 * @returns {HTMLElement}
 */
App.Utils.htmlToElement = function(text) {
	const template = document.createElement("template");
	text = text.trim(); // Never return a text node of whitespace as the result
	template.innerHTML = text;
	// @ts-ignore
	return template.content.firstChild;
};

/**
 * Show a list of links (or disabled links) as a delimited strip
 * @param {Node[]} links
 * @returns {HTMLUListElement}
 */
App.UI.DOM.generateLinksStrip = function(links) {
	const strip = document.createElement('ul');
	strip.className = "choicesStrip";

	links.reduce((list, lnk) => {
		const li = document.createElement("li");
		li.appendChild(lnk);
		list.appendChild(li);
		return list;
	}, strip);
	return strip;
};

/**
 * @param {Node|string} head
 * @param {HTMLDivElement} [content]
 * @param {boolean} [hidden]
 * @returns {DocumentFragment}
 */
App.UI.DOM.accordion = function(head, content, hidden = true) {
	const fragment = document.createDocumentFragment();
	const button = App.UI.DOM.appendNewElement("button", fragment, head, "accordion");

	if (content) {
		App.UI.DOM.elementToggle(button, [content], hidden);
		fragment.append(content);
	} else {
		button.classList.add("empty");
	}

	return fragment;
};

/**
 * @param {HTMLElement} toggleElement
 * @param {Array<HTMLElement>} content
 * @param {boolean} [startHidden=true]
 */
App.UI.DOM.elementToggle = function(toggleElement, content, startHidden = true) {
	toggleElement.classList.add("accordion");
	for (let htmlElement of content) {
		htmlElement.classList.add("accordion-content");
	}
	const toggle = () => {
		toggleElement.classList.toggle("closed");
		for (let htmlElement of content) {
			htmlElement.classList.toggle("hidden");
		}
	};
	toggleElement.onclick = toggle;
	if (startHidden) {
		toggle();
	}
};

/*
	<<includeDOM element>>
	Simply inserts a given DOM element.
 */
Macro.add("includeDOM", {
	handler() {
		// Basically the same as <<set>>, we just use whatever Scripting.evalJavaScript(this.args.full) returns instead
		// of discarding it.
		try {
			this.output.append(Scripting.evalJavaScript(this.args.full));
		} catch (ex) {
			return this.error(`bad evaluation: ${typeof ex === 'object' ? `${ex.name}: ${ex.message}` : ex}`, null,
				ex.stack);
		}
	}
});

App.UI.DOM.cashFormat = function(s) {
	const span = document.createElement("span");
	if (s < 0) { // Display red if the value is negative, unless invert is true
		span.classList.add("red");
	} else if (s !== 0) { // Yellow for positive
		span.classList.add("cash");
	}
	span.textContent = cashFormat(Math.trunc(s));
	return span;
};

repo_name: NovX/fc

license: mit