code:
/**
 *
 * @param {"cash"|"rep"} budgetType
 * @returns {HTMLDivElement}
 */
App.Budget.table = function(budgetType) {
	// Set up object to track calculated displays
	const income = (budgetType === "cash") ? "lastWeeksCashIncome" : "lastWeeksRepIncome";
	const expenses = (budgetType === "cash") ? "lastWeeksCashExpenses" : "lastWeeksRepExpenses";
	if (V.temp.showEmptyBudgetEntries[budgetType] === undefined) {
		V.temp.showEmptyBudgetEntries[budgetType] = 0;
	}

	// Make the total 0 first, otherwise it gets counted as part of the new total
	V[income].Total = 0;
	V[income].Total = hashSum(V[income]);
	V[expenses].Total = 0;
	V[expenses].Total = hashSum(V[expenses]);

	const tableDiv = document.createElement("div");
	tableDiv.classList.add("budget");

	// HEADER
	generateHeader();

	// HEADER: FACILITIES
	createSectionHeader("Facilities");

	const F = App.Entity.facilities; // shortcut

	if (budgetType === "cash") {
		// PENTHOUSE
		addToggle(generateRowGroup("Penthouse", "PENTHOUSE"), [
			...generateRowCategory("Resting Slaves", "slaveAssignmentRest"),
			...generateRowCategory("Resting Vign", "slaveAssignmentRestVign"),
			...generateRowCategory("Fucktoys", "slaveAssignmentFucktoy"),
			...generateRowCategory("Taking Classes", "slaveAssignmentClasses"),
			...generateRowCategory("House Servants", "slaveAssignmentHouse"),
			...generateRowCategory("House Servant Vign", "slaveAssignmentHouseVign"),
			...generateRowCategory("Whores", "slaveAssignmentWhore"),
			...generateRowCategory("Whore Vign", "slaveAssignmentWhoreVign"),
			...generateRowCategory("Public Sluts", "slaveAssignmentPublic"),
			...generateRowCategory("Public Slut Vign", "slaveAssignmentPublicVign"),
			...generateRowCategory("Subordinate Slaves", "slaveAssignmentSubordinate"),
			...generateRowCategory("Milked", "slaveAssignmentMilked"),
			...generateRowCategory("MilkedVign", "slaveAssignmentMilkedVign"),
			...generateRowCategory("ExtraMilk", "slaveAssignmentExtraMilk"),
			...generateRowCategory("ExtraMilkVign", "slaveAssignmentExtraMilkVign"),
			...generateRowCategory("Gloryhole", "slaveAssignmentGloryhole"),
			...generateRowCategory("Confined Slaves", "slaveAssignmentConfinement")
		]);
		// Other
		generateRowCategory("Choosing Own Assignment", "slaveAssignmentChoice");

		// LEADERSHIP ROLES

		// HEAD GIRL
		// find passage name for HGSuite
		addToggle(generateRowGroup(F.headGirlSuite.nameCaps, "HEADGIRLSUITE"), [
			...generateRowCategory("Head Girl", "slaveAssignmentHeadgirl"),
			...generateRowCategory("Head Girl Fucktoys", "slaveAssignmentHeadgirlsuite")
		]);

		// RECRUITER
		addToggle(generateRowGroup("Recruiter", "RECRUITER"), [
			...generateRowCategory("Recruiter", "slaveAssignmentRecruiter")
		]);

		// BODYGUARD
		// find passage name for Armory
		addToggle(generateRowGroup(F.armory.nameCaps, "DOJO"), [
			...generateRowCategory("Bodyguard", "slaveAssignmentBodyguard")
		]);

		// CONCUBINE
		addToggle(generateRowGroup(F.masterSuite.nameCaps, "MASTERSUITE"), [
			...generateRowCategory("Master Suite Operation", "masterSuite"),
			...generateRowCategory("Master Suite Concubine", "slaveAssignmentConcubine"),
			...generateRowCategory("Master Suite Fucktoys", "slaveAssignmentMastersuite")
		]);

		// AGENT
		addToggle(generateRowGroup("Agent", "AGENT"), [
			...generateRowCategory("Agent", "slaveAssignmentAgent"),
			...generateRowCategory("Agent's Partner", "slaveAssignmentAgentPartner")
		]);

		// ARCADE
		addToggle(generateRowGroup(F.arcade.nameCaps, "ARCADE"), [
			...generateRowCategory("Arcade Operation", "arcade"),
			...generateRowCategory("Arcade Fuckdolls", "slaveAssignmentArcade")
		]);

		// BROTHEL
		addToggle(generateRowGroup(F.brothel.nameCaps, "BROTHEL"), [
			...generateRowCategory("Brothel Operation", "brothel"),
			...generateRowCategory("Brothel Madam", "slaveAssignmentMadam"),
			...generateRowCategory("Brothel MadamVign", "slaveAssignmentMadamVign"),
			...generateRowCategory("Brothel Whore", "slaveAssignmentBrothel"),
			...generateRowCategory("Brothel WhoreVign", "slaveAssignmentBrothelVign"),
			...generateRowCategory("Brothel Ads", "brothelAds")
		]);

		// CELLBLOCK
		addToggle(generateRowGroup(F.cellblock.nameCaps, "CELLBLOCK"), [
			...generateRowCategory("Cellblock Operation", "cellblock"),
			...generateRowCategory("Cellblock Warden", "slaveAssignmentWarden"),
			...generateRowCategory("Cellblock Slaves", "slaveAssignmentCellblock")
		]);

		// CLUB
		addToggle(generateRowGroup(F.club.nameCaps, "CLUB"), [
			...generateRowCategory("Club Operation", "club"),
			...generateRowCategory("Club DJ", "slaveAssignmentDj"),
			...generateRowCategory("Club DJVign", "slaveAssignmentDjVign"),
			...generateRowCategory("Club Sluts", "slaveAssignmentClub"),
			...generateRowCategory("Club Slut Vign", "slaveAssignmentClubVign"),
			...generateRowCategory("Club Ads", "clubAds")
		]);

		// CLINIC
		addToggle(generateRowGroup(F.clinic.nameCaps, "CLINIC"), [
			...generateRowCategory("Clinic Operation", "clinic"),
			...generateRowCategory("Clinic Nurse", "slaveAssignmentNurse"),
			...generateRowCategory("Clinic Slaves", "slaveAssignmentClinic")
		]);

		// DAIRY
		addToggle(generateRowGroup(F.dairy.nameCaps, "DAIRY"), [
			...generateRowCategory("Dairy Operation", "dairy"),
			...generateRowCategory("Dairy Milkmaid", "slaveAssignmentMilkmaid"),
			...generateRowCategory("Dairy Cows", "slaveAssignmentDairy"),
			...generateRowCategory("Dairy CowsVign", "slaveAssignmentDairyVign")
		]);

		// FARMYARD
		addToggle(generateRowGroup(F.farmyard.nameCaps, "FARMYARD"), [
			...generateRowCategory("Farmyard Operation", "farmyard"),
			...generateRowCategory("Farmyard Farmer", "slaveAssignmentFarmer"),
			...generateRowCategory("Farmyard Farmhands", "slaveAssignmentFarmyard"),
			...generateRowCategory("Farmyard FarmhandsVign", "slaveAssignmentFarmyardVign")
		]);

		// INCUBATOR
		addToggle(generateRowGroup(F.incubator.nameCaps, "INCUBATOR"), [
			...generateRowCategory("Incubator Operation", "incubator"),
			...generateRowCategory("Incubator Babies", "incubatorSlaves")
		]);

		// NURSERY
		addToggle(generateRowGroup(F.nursery.nameCaps, "NURSERY"), [
			...generateRowCategory("Nursery Operation", "nursery"),
			...generateRowCategory("Nursery Matron", "slaveAssignmentMatron"),
			...generateRowCategory("Nursery Nannies", "slaveAssignmentNursery"),
			...generateRowCategory("Nursery NanniesVign", "slaveAssignmentNurseryVign")
		]);

		// PIT
		addToggle(generateRowGroup(F.pit.nameCaps, "PIT"), [
			...generateRowCategory("Pit Operation", "pit")
		]);

		// PROSTHETIC LAB
		addToggle(generateRowGroup("Prosthetic Lab", "PROSTHETICLAB"), [
			...generateRowCategory("Prosthetic Lab Operation", "lab"),
			...generateRowCategory("Prosthetic Lab Research", "labResearch"),
			...generateRowCategory("Prosthetic Lab Scientists", "labScientists"),
			...generateRowCategory("Prosthetic Lab Menials", "labMenials")
		]);

		// SCHOOLROOM
		addToggle(generateRowGroup(F.schoolroom.nameCaps, "SCHOOLROOM"), [
			...generateRowCategory("Schoolroom Operation", "school"),
			...generateRowCategory("Schoolroom Teacher", "slaveAssignmentTeacher"),
			...generateRowCategory("Schoolroom Students", "slaveAssignmentSchool")
		]);

		// SERVANTS' QUARTERS
		addToggle(generateRowGroup(F.servantsQuarters.nameCaps, "SERVANTSQUARTERS"), [
			...generateRowCategory("Servants' Quarters Operation", "servantsQuarters"),
			...generateRowCategory("Servants' Quarters Steward", "slaveAssignmentSteward"),
			...generateRowCategory("Servants' Quarters Servants", "slaveAssignmentQuarter"),
			...generateRowCategory("Servants' Quarters ServantsVign", "slaveAssignmentQuarterVign")
		]);

		// SPA
		addToggle(generateRowGroup(F.spa.nameCaps, "SPA"), [
			...generateRowCategory("Spa Operation", "spa"),
			...generateRowCategory("Spa Attendant", "slaveAssignmentAttendant"),
			...generateRowCategory("Spa Slaves", "slaveAssignmentSpa")
		]);

		// HEADER: ARCOLOGY
		createSectionHeader("Arcology");

		// SLAVES
		addToggle(generateRowGroup("Slave Miscellaneous", "SLAVES"), [
			...generateRowCategory("Slave Porn", "porn"),
			...generateRowCategory("Slave Modifications", "slaveMod"),
			...generateRowCategory("Slave Surgery", "slaveSurgery"),
			...generateRowCategory("Slave Birthing", "birth")
		]);

		// MENIAL LABOR
		addToggle(generateRowGroup("Menial Labor", "LABOR"), [
			...generateRowCategory("Menials: Slaves", "menialTrades"),
			...generateRowCategory("Menials: Fuckdolls", "fuckdolls"),
			...generateRowCategory("Menials: Bioreactors", "menialBioreactors")
		]);

		// FLIPPING
		addToggle(generateRowGroup("Flipping", "FLIPPING"), [
			...generateRowCategory("Slave Transfer", "slaveTransfer"),
			...generateRowCategory("Indenture Renewal", "indentureRenewal"),
			...generateRowCategory("Menials", "menialTransfer"),
			...generateRowCategory("Fuckdolls", "fuckdollsTransfer"),
			...generateRowCategory("Bioreactors", "menialBioreactorsTransfer"),
			...generateRowCategory("Assistant: Menials", "menialTransferA"),
			...generateRowCategory("Assistant: Fuckdolls", "fuckdollsTransferA"),
			...generateRowCategory("Assistant: Bioreactors", "menialBioreactorsTransferA"),
			...generateRowCategory("Menial Retirement", "menialRetirement"),
			...generateRowCategory("Scientist Transfer", "labScientistsTransfer"),
			...generateRowCategory("Slave Babies", "babyTransfer")
		]);

		// FINANCIALS
		addToggle(generateRowGroup("Financials", "FINANCIALS"), [
			...generateRowCategory("Weather", "weather"),
			...generateRowCategory("Rents", "rents"),
			...generateRowCategory("Fines", "fines"),
			...generateRowCategory("Events", "event"),
			...generateRowCategory("Capital Expenses", "capEx"),
			...generateRowCategory("Future Society Shaping", "futureSocieties"),
			...generateRowCategory("School Subsidy", "schoolBacking"),
			...generateRowCategory("Arcology conflict", "war"),
			...generateRowCategory("Cheating", "cheating")
		]);

		// POLICIES
		addToggle(generateRowGroup("Policies", "POLICIES"), [
			...generateRowCategory("Policies", "policies"),
			...generateRowCategory("Subsidies and Barriers", "subsidiesAndBarriers")
		]);

		if (V.scenarios.contraceptivesBan) {
			addToggle(generateRowGroup("Abortions", "ABORTIONS"), [
				...generateRowCategory("Abortions", "abortions")
			]);
		}

		// EDICTS
		addToggle(generateRowGroup("Edicts", "EDICTS"), [
			...generateRowCategory("Edicts", "edicts")
		]);

		// PERSONAL FINANCE
		addToggle(generateRowGroup("Personal Finance", "PERSONALFINANCE"), [
			...generateRowCategory("Personal Business", "personalBusiness"),
			...generateRowCategory("Personal Living Expenses", "personalLivingExpenses"),
			...generateRowCategory("Your skills", "PCSkills"),
			...generateRowCategory("Your training expenses", "PCtraining"),
			...generateRowCategory("Your food expenses", "PCdiet"),
			...generateRowCategory("Your drug expenses", "PCdrugs"),
			...generateRowCategory("Your medical expenses", "PCmedical"),
			...generateRowCategory("Your cosmetic expenses", "PCcosmetics"),
			...generateRowCategory("Citizen Orphanage", "citizenOrphanage"),
			...generateRowCategory("Private Orphanage", "privateOrphanage"),
			...generateRowCategory("Stock dividends", "stocks"),
			...generateRowCategory("Stock trading", "stocksTraded")
		]);

		// SECURITY
		addToggle(generateRowGroup("Security", "SECURITY"), [
			...generateRowCategory("Mercenaries", "mercenaries"),
			...generateRowCategory("Security Expansion", "securityExpansion"),
			...generateRowCategory("Special Forces", "specialForces"),
			...generateRowCategory("Special Forces Capital Expenses", "specialForcesCap"),
			...generateRowCategory("Peacekeepers", "peacekeepers")
		]);
	} else if (budgetType === "rep") {
		// PENTHOUSE
		addToggle(generateRowGroup("Penthouse", "PENTHOUSE"), [
			...generateRowCategory("Fucktoys", "fucktoy"),
			...generateRowCategory("Public servants", "publicServant"),
			...generateRowCategory("Free glory holes", "gloryhole"),
			...generateRowCategory("Concubine", "concubine"),
			...generateRowCategory("Head girl", "headGirl"),
			...generateRowCategory("Bodyguard", "bodyguard"),
			...generateRowCategory("Recruiter", "recruiter"),
		]);

		// ARCADE
		addToggle(generateRowGroup(F.arcade.nameCaps, "ARCADE"), [
			...generateRowCategory("Arcade Operation", "arcade"),
			...generateRowCategory("Free arcade", "gloryholeArcade")
		]);

		// BROTHEL
		addToggle(generateRowGroup(F.brothel.nameCaps, "BROTHEL"), [
			...generateRowCategory("Brothel Operation", "brothel"),
		]);

		// CLUB
		addToggle(generateRowGroup(F.club.nameCaps, "CLUB"), [
			...generateRowCategory("Club Operation", "club"),
			...generateRowCategory("Club servants", "publicServantClub"),
			...generateRowCategory("Club ads", "clubAds"),
		]);

		// PIT
		addToggle(generateRowGroup(F.pit.nameCaps, "PIT"), [
			...generateRowCategory("Pit Operation", "pit")
		]);

		// SERVANTS' QUARTERS
		addToggle(generateRowGroup(F.servantsQuarters.nameCaps, "SERVANTSQUARTERS"), [
			...generateRowCategory("Servants' Quarters Operation", "servantsQuarters"),
		]);

		// SPA
		addToggle(generateRowGroup(F.spa.nameCaps, "SPA"), [
			...generateRowCategory("Spa Operation", "spa"),
		]);

		// FARMYARD
		addToggle(generateRowGroup(F.farmyard.nameCaps, "FARMYARD"), [
			...generateRowCategory("Shows", "shows"),
		]);

		// HEADER: ARCOLOGY
		createSectionHeader("Arcology");

		// SLAVES
		addToggle(generateRowGroup("Slave Miscellaneous", "SLAVES"), [
			...generateRowCategory("Slave trust and devotion", "slavesViewOfPC"),
			...generateRowCategory("Prestigious slaves", "prestigiousSlave"),
			...generateRowCategory("Porn", "porn"),
			...generateRowCategory("Selling/buying major slaves", "slaveTransfer"),
			...generateRowCategory("Public breeders", "publicBreeding"),
			...generateRowCategory("Selling babies", "babyTransfer"),
			...generateRowCategory("Birth", "birth"),
			...generateRowCategory("Slave retirement", "retirement"),
			...generateRowCategory("Vignettes", "vignette"),
			...generateRowCategory("Free Fuckdolls", "fuckdolls"),
		]);

		// POLICIES
		addToggle(generateRowGroup("Policies", "POLICIES"), [
			...generateRowCategory("Capital expenses", "capEx"),
			...generateRowCategory("Subsidies and Barriers", "subsidiesAndBarriers"),
			...generateRowCategory("Society shaping", "futureSocieties"),
			...generateRowCategory("Food", "food"),
		]);

		// EDICTS
		addToggle(generateRowGroup("Edicts", "EDICTS"), [
			...generateRowCategory("Edicts", "edicts")
		]);

		// PERSONAL FINANCE
		addToggle(generateRowGroup("Personal Finance", "PERSONALFINANCE"), [
			...generateRowCategory("Personal Business", "personalBusiness"),
			...generateRowCategory("Your appearance", "PCappearance"),
			...generateRowCategory("Your actions", "PCactions"),
			...generateRowCategory("Your skills", "PCRelationships"),
			...generateRowCategory("Slave relationships", "SlaveRelationships"),
			...generateRowCategory("Events", "event"),
		]);

		// SECURITY
		addToggle(generateRowGroup("Security", "SECURITY"), [
			...generateRowCategory("Security Expansion", "securityExpansion"),
			...generateRowCategory("Special Forces", "specialForces"),
			...generateRowCategory("Conflict", "war"),
			...generateRowCategory("Peacekeepers", "peacekeepers")
		]);

		addToggle(generateRowGroup("Waste", "WASTE"), [
			...generateRowCategory("Reputation decay", "multiplier"),
			...generateRowCategory("Overflow (your reputation cannot be higher than 20k)", "overflow"),
			...generateRowCategory("Income curve", "curve")
		]);
	}

	// BUDGET REPORT
	generateSummary();

	return tableDiv;

	function generateHeader() {
		App.UI.DOM.appendNewElement("div", tableDiv, (budgetType === "cash") ? "Budget Overview" : "Reputation Overview", ["header"]);
		App.UI.DOM.appendNewElement("div", tableDiv, "Income", ["header"]);
		App.UI.DOM.appendNewElement("div", tableDiv, "Expense", ["header"]);
		App.UI.DOM.appendNewElement("div", tableDiv, "Totals", ["header"]);
	}

	function generateSummary() {
		createSectionHeader("Summary Report");

		App.UI.DOM.appendNewElement("div", tableDiv, "Tracked totals");

		App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(Math.trunc(V[income].Total)), ["number",
			"final-result"]);
		App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(Math.trunc(V[expenses].Total)),
			["number", "final-result"]);
		App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(Math.trunc(V[income].Total + V[expenses].Total)),
			["number", "final-result"]);

		if (budgetType === "cash") {
			App.UI.DOM.appendNewElement("div", tableDiv, `Expenses budget for week ${V.week + 1}`);
			tableDiv.append(document.createElement("div"));
			App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(-V.costs), ["number"]);
			tableDiv.append(document.createElement("div"));
		}

		App.UI.DOM.appendNewElement("div", tableDiv, `Last week actuals`);
		tableDiv.append(document.createElement("div"));
		tableDiv.append(document.createElement("div"));
		if (budgetType === "cash") {
			App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(V.cash - V.cashLastWeek), ["number"]);
		} else {
			App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(V.rep - V.repLastWeek), ["number"]);
		}

		if (
			(budgetType === "cash" && (V.cash - V.cashLastWeek) === (V.lastWeeksCashIncome.Total + V.lastWeeksCashExpenses.Total)) ||
			(budgetType === "rep" && (V.rep - V.repLastWeek) === (V.lastWeeksRepIncome.Total + V.lastWeeksRepExpenses.Total))
		) {
			App.UI.DOM.appendNewElement("div", tableDiv, `The books are balanced, ${properTitle()}!`, ["green",
				"last-row"]);
			tableDiv.append(document.createElement("div"));
			tableDiv.append(document.createElement("div"));
			tableDiv.append(document.createElement("div"));
		} else {
			App.UI.DOM.appendNewElement("div", tableDiv, "Transaction tracking off by", ["red", "last-row"]);
			tableDiv.append(document.createElement("div"));
			tableDiv.append(document.createElement("div"));
			if (budgetType === "cash") {
				App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(
					(V.cash - V.cashLastWeek) - (V.lastWeeksCashIncome.Total + V.lastWeeksCashExpenses.Total)
				), ["number"]);
			} else {
				App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(
					(V.rep - V.repLastWeek) - (V.lastWeeksRepIncome.Total + V.lastWeeksRepExpenses.Total)
				), ["number"]);
			}
		}
	}

	function createSectionHeader(text) {
		App.UI.DOM.appendNewElement("div", tableDiv, text, ["section"]);
		App.UI.DOM.appendNewElement("div", tableDiv, null, ["section"]);
		App.UI.DOM.appendNewElement("div", tableDiv, null, ["section"]);
		App.UI.DOM.appendNewElement("div", tableDiv, null, ["section"]);
	}

	function generateRowCategory(node, category) {
		const r = [];
		if (V[income][category] || V[expenses][category] || V.temp.showEmptyBudgetEntries[budgetType]) {
			r.push(App.UI.DOM.appendNewElement("div", tableDiv, node, ["entry"]));
			r.push(App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(V[income][category]),
				["number", "entry"]));
			r.push(App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(-Math.abs(V[expenses][category])),
				["number", "entry"]));
			r.push(App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(V[income][category] + V[expenses][category]),
				["number", "entry"]));
		}
		return r;
	}

	function generateRowGroup(title, name) {
		/** @type {string[]} */
		const members = (budgetType === "cash" ? CategoryAssociatedGroup[name] : CategoryAssociatedGroupRep[name]);
		const groupIn = members.map((k) => V[income][k]).reduce((acc, cur) => acc + cur);
		const groupEx = members.map((k) => V[expenses][k]).reduce((acc, cur) => acc + cur);

		const r = [];
		if (groupIn || groupEx || V.temp.showEmptyBudgetEntries[budgetType]) {
			r.push(App.UI.DOM.appendNewElement("div", tableDiv, title, ["group"]));
			r.push(App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(groupIn), ["group", "number"]));
			r.push(App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(groupEx), ["group", "number"]));
			r.push(App.UI.DOM.appendNewElement("div", tableDiv, formatNumber(groupIn + groupEx),
				["group", "number"]));
		}
		return r;
	}

	/**
	 * @param {Array<HTMLDivElement>} heads
	 * @param {Array<HTMLDivElement>} content
	 */
	function addToggle(heads, content) {
		heads = heads.filter(e => !!e);
		if (heads.length === 0) {
			return;
		}
		content = content.filter(e => !!e);
		if (content.length === 0) {
			return;
		}
		// Based on App.UI.DOM.elementToggle(), which only allows one toggle element
		for (let htmlElement of heads) {
			htmlElement.classList.add("accordion", "closed");
		}
		for (let htmlElement of content) {
			htmlElement.classList.add("accordion-content", "hidden");
		}
		const toggle = () => {
			for (let htmlElement of heads) {
				htmlElement.classList.toggle("closed");
			}
			for (let htmlElement of content) {
				htmlElement.classList.toggle("hidden");
			}
		};
		for (let htmlElement of heads) {
			htmlElement.onclick = toggle;
		}
	}

	function formatNumber(num, invert = false) {
		if (invert) {
			num = -1 * num;
		}
		let span = document.createElement('span');
		span.append(budgetType === "cash" ? cashFormat(num) : repFormat(num, V.assistant.power < 3));
		if (num === 0) {
			// num overwrites gray, so we don't use it here.
			span.classList.add("gray");
		} else {
			span.classList.add((budgetType === "cash") ? "cash" : "reputation");
			// Display red if the value is negative, unless invert is true
			if (num < 0) {
				span.classList.add("dec");
				// Yellow for positive
			} else if (num > 0) {
				span.classList.add("inc");
				// Gray for exactly zero
			}
		}
		return span;
	}
};


code:
/**
 * Costs Budget Passage
 * @returns {DocumentFragment}
 */
App.Budget.costs = function() {
	const frag = new DocumentFragment();

	App.UI.DOM.appendNewElement("h1", frag, `Budget`);

	frag.append(
		intro(),
		loans(),
		netWorth(),
	);
	if (V.difficultySwitch === 1) {
		frag.append(economy());
	}
	frag.append(settings());

	// Table of Totals
	if (!V.lastWeeksCashIncome) {
		App.UI.DOM.appendNewElement("p", frag, "Financial data currently unavailable.");
	} else {
		App.UI.DOM.appendNewElement("p", frag, App.Budget.table("cash"));
	}

	errors(frag);
	return frag;

	/**
	 * @returns {DocumentFragment}
	 */
	function loans() {
		const frag = new DocumentFragment();

		frag.append(App.Budget.loans());

		return frag;
	}

	/**
	 * @returns {HTMLParagraphElement}
	 */
	function intro() {
		return App.UI.DOM.makeElement("p", `Here you can view many of the financial details of your arcology, ${properTitle()}. Proper cash flow is critical to the success of your long term goals. Find expensive waste here and you can change the right policies or sell off slackers. Find your next profit center and invest in new equipment, advertising, or flesh to maximize your assets.`, ["scene-intro"]);
	}

	/**
	 * @returns {DocumentFragment}
	 */
	function netWorth() {
		const frag = new DocumentFragment();

		frag.append(
			App.UI.DOM.makeElement("h2", `Net worth`),
			`You have a total net worth of ${cashFormat(Math.trunc(App.Utils.totalNetWorth()))}.`
		);

		if (V.debugMode) {
			App.UI.DOM.appendNewElement("div", frag, App.UI.DOM.link(`Recalculate net worth`, () => {
				App.UI.reload();
			}), ['indent']);
		}

		return frag;
	}

	/**
	 * @returns {DocumentFragment}
	 */
	function economy() {
		const frag = new DocumentFragment();

		App.UI.DOM.appendNewElement("h2", frag, `Economy`);

		const p = document.createElement("p");

		App.UI.DOM.appendNewElement("div", p, `The Local Economy score effects some prices in your ecology. The lower the score, the higher the prices. The base score is 100.`, ["detail"]);

		const grid = document.createElement("div");
		grid.className = "grid-2columns-auto";

		App.UI.DOM.appendNewElement("div", grid, "Global Economy", ["cash"]);
		if (V.cheatMode && V.cheatModeM) {
			const div = document.createElement("div");
			div.append(App.UI.DOM.makeTextBox(V.economy, v => {
				V.economy = v;
				V.cheater = 1;
			}, true));
			grid.append(div);
		} else {
			App.UI.DOM.appendNewElement("div", grid, String(V.economy));
		}

		App.UI.DOM.appendNewElement("div", grid, "Local Economy", ["cash"]);
		if (V.cheatMode && V.cheatModeM) {
			const div = document.createElement("div");
			div.append(App.UI.DOM.makeTextBox(V.localEcon, v => {
				V.localEcon = v;
				V.cheater = 1;
			}, true));
			grid.append(div);
		} else {
			App.UI.DOM.appendNewElement("div", grid, String(V.localEcon));
		}

		p.append(grid);

		const r = [];
		r.push("The current score is");
		if (V.localEcon > 100) {
			let econPercent = Math.trunc(1000 - 100000 / V.localEcon) / 10;
			r.push(`reducing prices by <span class="cash inc">${econPercent}%.</span>`);
		} else if (V.localEcon === 100) {
			r.push("equal to the base score. There are no price modifications.");
		} else {
			let econPercent = Math.trunc(100000 / V.localEcon - 1000) / 10;
			r.push(`increasing prices by <span class="cash dec">${econPercent}%.</span>`);
		}
		$(p).append(...App.Events.spaceSentences(r));

		frag.append(p);

		return frag;
	}

	/**
	 * @returns {HTMLParagraphElement}
	 */
	function settings() {
		const p = document.createElement("p");
		App.UI.DOM.appendNewElement("h2", p, `Weekly costs`);

		let options = new App.UI.OptionsGroup();
		V.temp.showEmptyBudgetEntries = V.temp.showEmptyBudgetEntries ?? {};
		if (V.temp.showEmptyBudgetEntries.cash === undefined) {
			V.temp.showEmptyBudgetEntries.cash = 0;
		}
		options.addOption("", "cash", V.temp.showEmptyBudgetEntries)
			.addValue("Normal", 0).on().addValue("Show Empty Entries", 1);
		p.append(options.render());

		return p;
	}

	/**
	 * @param {DocumentFragment} container
	 */
	function errors(container) {
		if (V.lastWeeksCashErrors.length > 0) {
			const p = document.createElement("p");
			p.append(App.UI.DOM.passageLink("Reset", "Costs Budget",
				() => { V.lastWeeksCashErrors = []; }));
			App.UI.DOM.appendNewElement("div", p, "Errors:", ["error"]);
			for (const error of V.lastWeeksCashErrors) {
				App.UI.DOM.appendNewElement("div", p, error, ["error"]);
			}
			container.append(p);
		}
	}
};

repo_name: Nithhogg/fc

license: mit


code:
App.Budget.loans = function() {
	/** @typedef {('bank'|'shark')} Lender */

	const frag = new DocumentFragment();

	const loan = (/** @type {Lender} */ name) => V.loans.find(loan => loan.name === name);

	App.UI.DOM.appendNewElement("h2", frag, `Loans`);

	if (V.loans.length) {
		frag.append(pay());
	}

	frag.append(
		bank(),
		loanshark(),
	);

	return frag;

	function pay() {
		const div = document.createElement("div");
		const links = [];
		const text = [];

		text.push(`You`);
		if (loan('bank')) {
			text.push(`still owe the bank ${cashFormat(Math.trunc(loan('bank').full))}`);
			if (loan('shark')) {
				text.push(`and`);
			}
		}
		if (loan('shark')) {
			text.push(`have ${years(loan('shark').deadline - V.week)} to pay the shark back ${cashFormat(Math.trunc(loan('shark').full))}`);
		}
		text.push(text.pop() + `.`);
		text.push(`You can pay off your loans here.`);

		if (loan('bank')) {
			const bank = loan('bank');
			if (V.cash > bank.full) {
				links.push(App.UI.DOM.link(`Pay off your bank loan`, () => {
					cashX(forceNeg(bank.full), "loan");
					V.loans.deleteAll(bank);

					App.UI.reload();
				}));
			} else {
				links.push(App.UI.DOM.disabledLink(`Pay off your bank loan`, [
					`You lack the necessary funds to pay off this loan`,
				]));
			}
		}
		if (loan('shark')) {
			const shark = loan('shark');
			if (V.cash > shark.full) {
				links.push(App.UI.DOM.link(`Pay off the loanshark`, () => {
					cashX(forceNeg(shark.full), "loan");
					V.loans.deleteAll(shark);

					App.UI.reload();
				}));
			} else {
				links.push(App.UI.DOM.disabledLink(`Pay off the loanshark`, [
					`You lack the necessary funds to pay off this loan`,
				]));
			}
		}

		div.append(text.join(' '));
		App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['indent']);

		return div;
	}

	function bank() {
		const div = document.createElement("div");
		const values = [10000, 100000, 1000000];
		const disabledReasons = [];
		const links = [];

		div.append(`You can take out a loan from one of the credit unions in the Free City, if you have the reputation for them to trust you.`);
		App.UI.DOM.appendNewElement("div", div,
			`If for any reason you lack the credits, sectors of ${V.arcologies[0].name} will be used in lieu of payment.`,
			["note", "indent"]);

		if (V.rep < 10000) {
			disabledReasons.push(`You need at least ${num(10000)} reputation to take a loan from this lender.`);
		}
		if (loan('bank')) {
			disabledReasons.push(`You have already taken out a loan from this lender.`);
		}

		if (disabledReasons.length === 0) {
			for (const amount of values) {
				const loan = createLoan("bank", amount);
				const f = new DocumentFragment();
				f.append(App.UI.DOM.link(cashFormat(amount), () => {
					V.loans.push(loan);
					cashX(amount, "loan");
					App.UI.reload();
				}));
				const info = new DocumentFragment();
				App.Events.addNode(info, [`${cashFormatColor(loan.full)} over ${num(loan.deadline - V.week)} weeks`]);
				f.append(" – ", App.UI.DOM.spanWithTooltip(info, terms("bank", loan)));
				links.push(f);
			}
		} else {
			values.map(val => links.push(App.UI.DOM.disabledLink(cashFormat(val), disabledReasons)));
		}

		App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['indent']);

		return div;
	}

	function loanshark() {
		const div = document.createElement("div");
		const values = [10000, 100000, 1000000];
		const disabledReasons = [];
		const links = [];

		div.append(`If you're not quite reputable enough, you can also borrow money from one of the local loan-sharks in the area.`);
		App.UI.DOM.appendNewElement("div", div,
			`If for any reason you miss a payment the lender will send his men to collect – forcibly, if necessary.`,
			["note", "indent"]);

		if (V.rep < 2000) {
			disabledReasons.push(`You need at least ${num(2000)} reputation to take a loan from this lender.`);
		}
		if (loan('shark')) {
			disabledReasons.push(`You have already taken out a loan from this lender.`);
		}

		if (disabledReasons.length === 0) {
			for (const amount of values) {
				const loan = createLoan("shark", amount);
				const f = new DocumentFragment();
				f.append(App.UI.DOM.link(cashFormat(amount), () => {
					V.loans.push(loan);
					cashX(amount, "loan");

					App.UI.reload();
				}));
				const info = new DocumentFragment();
				App.Events.addNode(info, [`${cashFormatColor(loan.full)} after ${num(loan.deadline - V.week)} weeks`]);
				f.append(" – ", App.UI.DOM.spanWithTooltip(info, terms("shark", loan)));
				links.push(f);
			}
		} else {
			values.map(val => links.push(App.UI.DOM.disabledLink(cashFormat(val), disabledReasons)));
		}

		App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['indent']);

		return div;
	}

	/**
	 * @param {"bank"|"shark"}lender
	 * @param {number} amount
	 * @returns {FC.Loan}
	 */
	function createLoan(lender, amount) {
		const term = Math.max(amount / 50000, 4);
		const apr = Math.max(Math.abs((V.rep - 20000) / 500), 5);
		let interest;
		let installments;
		if (lender === "shark") {
			interest = (amount * (term / 52) * (apr / 100)) * 3;
			installments = 1;
		} else {
			interest = amount * (term / 52) * (apr / 100);
			installments = term;
		}
		const full = amount + interest;

		return {
			name: lender,
			principal: amount,
			deadline: V.week + term,
			installments,
			apr,
			interest,
			full: Math.trunc(full),
		};
	}

	/**
	 * @param {Lender} lender
	 * @param {FC.Loan} loan
	 */
	function terms(lender, loan) {
		if (lender === 'bank') {
			return `You will pay about ${cashFormat(Math.trunc(loan.full / (loan.deadline - V.week)))} per week for ${num(loan.deadline - V.week)} weeks until you have paid off the entire balance. You will end up paying back about ${cashFormat(Math.trunc(loan.full))} after interest.`;
		} else {
			return `You will have ${num(loan.deadline - V.week)} weeks to pay off the full amount. You will end up paying back about ${cashFormat(Math.trunc(loan.full))} after interest.`;
		}
	}
};


code:
/**
 * Reputation budget passage
 * @returns {DocumentFragment}
 */
App.Budget.rep = function() {
	const f = new DocumentFragment();

	// App.UI.DOM.appendNewElement("h1", f, `Reputation Budget`);

	f.append(intro());
	f.append(settings());
	// Table of Totals
	if (!V.lastWeeksRepIncome) {
		App.UI.DOM.appendNewElement("p", f, "Reputation data currently unavailable.");
	} else {
		App.UI.DOM.appendNewElement("p", f, App.Budget.table("rep"));
	}

	errors(f);
	return f;

	/**
	 * @returns {HTMLParagraphElement}
	 */
	function intro() {
		const p = document.createElement("p");
		p.classList.add("scene-intro");
		p.append(`Reputation is a difficult thing to quantify, ${properTitle()}. Here you see an overview of topics that interest people in the arcology, and in turn, reflect on your own reputation. Much like a finance report, you can see here how your choices last week moved you closer to or further from your goals, and head any issues off before they get worse.`);
		if (V.assistant.power >= 3) {
			p.append(" Thanks to the arcology's powerful power computer core it is possible to track reputation changes to an unprecedented degree.");
		}
		return p;
	}

	/**
	 * @returns {HTMLParagraphElement}
	 */
	function settings() {
		const p = document.createElement("p");
		App.UI.DOM.appendNewElement("div", p, "Your weekly reputation changes are as follows:", ["detail"]);

		let options = new App.UI.OptionsGroup();
		V.temp.showEmptyBudgetEntries = V.temp.showEmptyBudgetEntries ?? {};
		if (V.temp.showEmptyBudgetEntries.rep === undefined) {
			V.temp.showEmptyBudgetEntries.rep = 0;
		}
		options.addOption("", "rep", V.temp.showEmptyBudgetEntries)
			.addValue("Normal", 0).on().addValue("Show Empty Entries", 1);
		p.append(options.render());

		return p;
	}

	/**
	 * @param {DocumentFragment} container
	 */
	function errors(container) {
		if (V.lastWeeksRepErrors.length > 0) {
			const p = document.createElement("p");
			p.append(App.UI.DOM.passageLink("Reset", "Rep Budget",
				() => { V.lastWeeksRepErrors = []; }));
			App.UI.DOM.appendNewElement("div", p, "Errors:", "error");
			for (const error of V.lastWeeksRepErrors) {
				App.UI.DOM.appendNewElement("div", p, error, "error");
			}
			container.append(p);
		}
	}
};


code:
App.UI.Cheat.PCCheatMenu = function() {
	const node = new DocumentFragment();

	App.UI.DOM.appendNewElement("h2", node, "Cheat Editing Player Character");

	App.UI.DOM.appendNewElement("div", node, App.UI.DOM.passageLink(
		"Cancel",
		"Manage Personal Affairs",
	));

	const tabBar = new App.UI.Tabs.TabBar("PCCheatMenu");
	tabBar.addTab("Player", "player", App.UI.Player.design());
	tabBar.addTab("Family", "family", App.Intro.editFamily(V.PC, true));
	node.append(tabBar.render());

	return node;
};


code:
App.UI.Cheat.PCCheatMenuCheatDatatypeCleanup = function() {
	App.Verify.playerState(V.PC);

	return App.Events.makeNode([`You perform the dark rituals, pray to the dark gods, and sell your soul for the power to reshape your body and life at will. What a cheater!`]);
};


code:
// cSpell:ignore CHEATY

App.UI.Cheat.arcologyCheatDatatypeCleanup = function() {
	const node = new DocumentFragment();

	App.Verify.playerState(V.PC);
	App.Verify.I.arcology();

	App.UI.DOM.appendNewElement("p", node, "The chanting grows louder and louder, CHEAT! CHEAT! CHEAT!");
	App.UI.DOM.appendNewElement("p", node, "The Arcology morphs and changes shape, CHEAT! CHEAT! CHEAT!");
	App.UI.DOM.appendNewElement("p", node, "The Arcology has been changed forever and you forfeit your CHEATING CHEATY SOUL!");

	return node;
};