From 6f11021ea7514a43aa2818d4ea286db398a74f29 Mon Sep 17 00:00:00 2001
From: Svornost <11434-svornost@users.noreply.gitgud.io>
Date: Mon, 26 Sep 2022 22:58:16 -0400
Subject: [PATCH] Porn UX rework

---
 css/facilities/studio.css                     |  29 +++
 js/002-config/fc-js-init.js                   |   1 +
 src/005-passages/managePassages.js            |   9 +
 src/endWeek/saPorn.js                         |  11 +
 src/facilities/penthouse/managePenthouse.js   |   7 +-
 src/facilities/studio/studio.js               | 130 +++++++++++
 src/facilities/studio/studioCharts.js         | 158 +++++++++++++
 .../encyclopediaX-SeriesArcology.js           | 121 ++++++----
 src/gui/quicklinks.js                         |   1 +
 src/interaction/siRecords.js                  | 219 +++++++-----------
 src/js/porn.js                                |  16 +-
 11 files changed, 514 insertions(+), 188 deletions(-)
 create mode 100644 css/facilities/studio.css
 create mode 100644 src/facilities/studio/studio.js
 create mode 100644 src/facilities/studio/studioCharts.js

diff --git a/css/facilities/studio.css b/css/facilities/studio.css
new file mode 100644
index 00000000000..57c8e3aed05
--- /dev/null
+++ b/css/facilities/studio.css
@@ -0,0 +1,29 @@
+/* these colors have to be accessed directly for graph colors */
+:root {
+	--genre-color-paraphilia: yellow;
+	--genre-color-fetish: lightcoral;
+	--genre-color-generic: gray;
+	--genre-color-quirk: lawngreen;
+	--genre-color-general: white;
+}
+
+/* and they have to be accessed through classes for text */
+.genre.paraphilia {
+	color: var(--genre-color-paraphilia);
+}
+
+.genre.fetish {
+	color: var(--genre-color-fetish);
+}
+
+.genre.generic {
+	color: var(--genre-color-generic);
+}
+
+.genre.quirk {
+	color: var(--genre-color-quirk);
+}
+
+.genre.general {
+	color: var(--genre-color-general);
+}
diff --git a/js/002-config/fc-js-init.js b/js/002-config/fc-js-init.js
index 79e7657b200..bd683646b91 100644
--- a/js/002-config/fc-js-init.js
+++ b/js/002-config/fc-js-init.js
@@ -76,6 +76,7 @@ App.Mods.SecExp = {};
 App.Mods.SF = {};
 App.Neighbor = {};
 App.PersonalAttention = {};
+App.Porn = {};
 App.RA = {};
 App.RA.Activation = {};
 App.Ratings = {};
diff --git a/src/005-passages/managePassages.js b/src/005-passages/managePassages.js
index 1a7f9fe228a..e7f53b6ad1e 100644
--- a/src/005-passages/managePassages.js
+++ b/src/005-passages/managePassages.js
@@ -239,3 +239,12 @@ new App.DomPassage("edicts",
 		return App.Mods.SecExp.edicts();
 	}, ["jump-to-safe", "jump-from-safe"]
 );
+
+new App.DomPassage("Media Studio",
+	() => {
+		V.nextButton = "Back";
+		V.nextLink = "Manage Penthouse";
+		V.encyclopedia = "Media Hub";
+		return App.UI.mediaStudio();
+	}, ["jump-to-safe", "jump-from-safe"]
+);
diff --git a/src/endWeek/saPorn.js b/src/endWeek/saPorn.js
index 246dd03b182..56f99456b4f 100644
--- a/src/endWeek/saPorn.js
+++ b/src/endWeek/saPorn.js
@@ -28,6 +28,7 @@ App.SlaveAssignment.porn = function saPorn(slave) {
 		prestigeCommentary(slave);
 		faceCommentary(slave);
 		hack();
+		checkFocus();
 
 		allGenreViews(slave);
 		updateViewerCount(slave);
@@ -212,6 +213,16 @@ App.SlaveAssignment.porn = function saPorn(slave) {
 		}
 	}
 
+	function checkFocus() {
+		if (slave.porn.focus !== "none") {
+			const focusGenre = App.Porn.getGenreByFocusName(slave.porn.focus);
+			if (!focusGenre || !focusGenre.valid(slave)) {
+				r += `${He} has been instructed to focus on ${slave.porn.focus} aspects of ${his} sex life to improve the porn ${he} produces, but ${he} <span class="red">can no longer do so.</span>`;
+				slave.porn.focus = "none";
+			}
+		}
+	}
+
 	function hack() {
 		if (V.PC.skill.hacking > 10) {
 			r += `With your hacking skills, you manage to tweak search algorithms to display ${his} content more often. `;
diff --git a/src/facilities/penthouse/managePenthouse.js b/src/facilities/penthouse/managePenthouse.js
index 15ff5968be7..90561620fd7 100644
--- a/src/facilities/penthouse/managePenthouse.js
+++ b/src/facilities/penthouse/managePenthouse.js
@@ -471,12 +471,7 @@ App.UI.managePenthouse = function() {
 		if (V.studio === 0) {
 			r.push(makeLink("Install a media hub to convert slave video feeds into pornography", () => { V.studio = 1; }, 10000));
 		} else {
-			r.push(`The arcology's video systems are connected to a media hub that can convert slave video feeds into pornography.`);
-			if (V.studioFeed === 0) {
-				r.push(makeLink("Upgrade the media hub to allow better control of pornographic content", () => { V.studioFeed = 1; }, 15000));
-			} else {
-				r.push(`It has been upgraded to allow superior control of a slave's pornographic content.`);
-			}
+			r.push(`The arcology's video systems are connected to a`, App.UI.DOM.passageLink("media hub", "Media Studio"), `that can convert slave video feeds into pornography.`);
 		}
 		App.Events.addNode(el, r, "div");
 
diff --git a/src/facilities/studio/studio.js b/src/facilities/studio/studio.js
new file mode 100644
index 00000000000..b767057814f
--- /dev/null
+++ b/src/facilities/studio/studio.js
@@ -0,0 +1,130 @@
+App.UI.mediaStudio = function() {
+	const t = new DocumentFragment();
+	const r = new SpacedTextAccumulator(t);
+
+	r.push(`The media hub is a small room wired in to almost every camera in ${V.arcologies[0].name}. From here, you and your personal assistant can edit, produce, and distribute pornography featuring your slaves going about their daily lives.`);
+	r.toNode("p", ["note"]);
+
+	if (V.studioFeed === 0) {
+		r.push(makePurchase("Upgrade the media hub to allow better control of pornographic content", 15000, "capEx", {
+			handler: () => { V.studioFeed = 1; },
+			refresh: () => App.UI.reload()
+		}));
+	} else {
+		r.push(`It has been upgraded to allow superior control of a slave's pornographic content.`);
+	}
+	r.toParagraph();
+
+	if (V.PC.career === "escort" || V.PC.career === "prostitute" || V.PC.career === "child prostitute") {
+		if (V.PC.career === "escort") {
+			r.push(`You retain some contacts from your past life in the industry that may be willing to cut you some discounts on promotion costs, should you return to it.`);
+		} else {
+			r.push(`You were approached in the past to star in some adult films and they may be willing to cut you some discounts on promotion costs, should you accept their offer.`);
+		}
+		if (V.PCSlutContacts !== 2) {
+			r.push(`You are not baring your body for all to see.`);
+			r.push(
+				App.UI.DOM.link(
+					`Star in porn for a discount`,
+					() => {
+						V.PCSlutContacts = 2;
+						App.UI.reload();
+					}
+				)
+			);
+		} else {
+			if (V.PC.career === "escort") {
+				r.push(`You are starring in hardcore porn once more.`);
+			} else if (V.PC.actualAge < V.minimumSlaveAge) {
+				r.push(`You are taking part in porn that may disturb people.`);
+			} else {
+				r.push(`You are starring in some hardcore porn.`);
+			}
+			r.push(
+				App.UI.DOM.link(
+					`Stop doing porn for a discount`,
+					() => {
+						V.PCSlutContacts = 1;
+						App.UI.reload();
+					}
+				)
+			);
+		}
+		r.toParagraph();
+	}
+
+	/** @param {App.Entity.SlaveState} slave */
+	function slavePornSummary(slave) {
+		const res = new DocumentFragment();
+
+		if (V.slavePanelStyle === 0) {
+			res.appendChild(document.createElement("br"));
+		} else if (V.slavePanelStyle === 1) {
+			const hr = document.createElement("hr");
+			hr.style.margin = "0";
+			res.appendChild(hr);
+		}
+
+		if (batchRenderer && (!V.seeCustomImagesOnly || (V.seeCustomImagesOnly && slave.custom.image))) {
+			let imgDiv = document.createElement("div");
+			imgDiv.classList.add("imageRef", "smlImg", "margin-right");
+			imgDiv.appendChild(batchRenderer.render(slave));
+			res.appendChild(imgDiv);
+		}
+
+		const r = new SpacedTextAccumulator(res);
+		r.push(App.UI.DOM.link(SlaveFullName(slave), () => { V.AS = slave.ID; }, [], "Slave Interact"));
+		if (slave.porn.feed) {
+			r.push("is making porn.");
+		} else {
+			r.push("is");
+			r.push(App.UI.DOM.makeElement("span", "not making porn.", ["red"]));
+		}
+		const f2 = new DocumentFragment();
+		App.UI.SlaveSummaryImpl.bits.long.porn_prestige(slave, f2); // why do these bits not just return the element?
+		App.UI.SlaveSummaryImpl.bits.long.face(slave, f2);
+		r.push(f2);
+		if (V.studioFeed && slave.porn.feed) {
+			if (slave.porn.focus === "none") {
+				r.push("Guided by viewers.");
+			} else {
+				const genre = App.Porn.getGenreByFocusName(slave.porn.focus);
+				r.push("Focused on");
+				r.push(App.UI.DOM.makeElement("span", `${genre.focusName}`, ["genre", genre.type.name]));
+				r.push("porn.");
+			}
+		}
+		if (slave.porn.spending > 0) {
+			r.push("Spending", App.UI.DOM.cashFormat(slave.porn.spending), "on promotion.");
+		}
+		r.toNode("div");
+
+		res.append(App.Porn.makeFameProgressChart(slave));
+		res.append(App.Porn.makeViewershipChart(slave));
+
+		return res;
+	}
+
+	// just dump them all into one giant list for now.  TODO: sorting and filtering might come later?
+	const slaves = V.slaves;
+	let batchRenderer = null;
+	if ((V.seeImages === 1) && (V.seeSummaryImages === 1)) {
+		batchRenderer = new App.Art.SlaveArtBatch(slaves.map(s => s.ID), 1);
+		t.appendChild(batchRenderer.writePreamble());
+	} else {
+		batchRenderer = null;
+	}
+
+	for (const slave of slaves) {
+		let slaveDiv = document.createElement("div");
+		slaveDiv.id = `slave-${slave.ID}`;
+		slaveDiv.classList.add("slaveSummary");
+		if (V.slavePanelStyle === 2) {
+			slaveDiv.classList.add("card");
+		}
+		slaveDiv.appendChild(slavePornSummary(slave));
+		t.append(slaveDiv);
+	}
+
+	return t;
+};
diff --git a/src/facilities/studio/studioCharts.js b/src/facilities/studio/studioCharts.js
new file mode 100644
index 00000000000..84511ceb98e
--- /dev/null
+++ b/src/facilities/studio/studioCharts.js
@@ -0,0 +1,158 @@
+/** Make a bar chart showing the slave's progress towards the next/previous level of porn fame in her chosen genre.
+ * @param {App.Entity.SlaveState} slave
+ */
+App.Porn.makeFameProgressChart = function(slave) {
+	const container = document.createElement("div");
+	const genre = App.Porn.getGenreByFameName(slave.porn.fameType);
+	const fameVal = slave.porn.prestige > 0 ? slave.porn.fame[genre.fameVar] : slave.porn.viewerCount;
+
+	if (slave.porn.prestige === 3) {
+		container.append("Worldwide fame reached.");
+	} else {
+		if (slave.porn.prestige === 0) {
+			container.append(App.UI.DOM.makeElement("div", `Fame progress (total viewership):`));
+		} else {
+			container.append(App.UI.DOM.makeElement("div", `Fame progress (in ${genre.fameName} porn): `));
+		}
+		// ranges
+		let bottomThreshold = 0;
+		let nextThreshold = 100000;
+		if (slave.porn.prestige === 2) {
+			bottomThreshold = 40000;
+			nextThreshold = V.pornStars[genre.fameVar].p3ID === 0 ? 150000 : 0;
+		} else if (slave.porn.prestige === 1) {
+			bottomThreshold = 5000;
+			nextThreshold = 50000;
+		}
+		const maxVal = (nextThreshold ? nextThreshold : 150000) * 1.1;
+		const curVal = Math.min(fameVal, nextThreshold * 1.1);
+
+		// container
+		const margin = {top: 10, right: 30, bottom: 30, left: 90},
+			width = 600 - margin.left - margin.right,
+			height = 60 - margin.top - margin.bottom;
+		const svg = d3.select(container)
+			.append("svg")
+			.attr("width", width + margin.left + margin.right)
+			.attr("height", height + margin.top + margin.bottom);
+
+		// X axis
+		const x = d3.scaleLinear()
+			.domain([0, maxVal])
+			.range([0, width]);
+		svg.append("g")
+			.attr("transform", `translate(0, ${height})`)
+			.call(d3.axisBottom(x))
+			.selectAll("text")
+				.attr("transform", "translate(-10,0)rotate(-45)")
+				.style("text-anchor", "end");
+
+		// Y axis (invisible)
+		const y = d3.scaleBand()
+			.range([ 0, height ])
+			.domain([""])
+
+		// Bar
+		svg.selectAll("myRect")
+			.data([curVal])
+			.join("rect")
+			.attr("x", x(0))
+			.attr("y", y(""))
+			.attr("width", d => x(d))
+			.attr("height", y.bandwidth())
+			.attr("fill", "var(--link-color)")
+
+		// downgrade threshold
+		if (bottomThreshold > 0) {
+			svg.append("line")
+				.attr("x1", x(bottomThreshold))
+				.attr("x2", x(bottomThreshold))
+				.attr("y1", 0)
+				.attr("y2", height)
+				.attr("stroke-width", 2)
+				.attr("stroke", "red")
+		}
+
+		// upgrade threshold
+		if (nextThreshold > 0) {
+			svg.append("line")
+				.attr("x1", x(nextThreshold))
+				.attr("x2", x(nextThreshold))
+				.attr("y1", 0)
+				.attr("y2", height)
+				.attr("stroke-width", 2)
+				.attr("stroke", "green")
+		}
+	}
+
+	if (slave.porn.prestige === 2 && V.pornStars[genre.fameVar].p3ID !== 0) {
+		container.append(App.UI.DOM.makeElement("div", "You already have another slave with worldwide fame in this genre.", ["note"]));
+	}	
+	return container;
+};
+
+/** Make a treemap chart showing the slave's current viewership distribution among the porn genres.
+ * @param {App.Entity.SlaveState} slave
+ */
+App.Porn.makeViewershipChart = function(slave) {
+	const container = document.createElement("div");
+
+	const data = {name: "Total viewership", children: []};
+	for (const type of Object.values(App.Porn.GenreType)) {
+		const child = {name: type, children: []};
+		for (const genre of App.Porn.getGenresByType(type)) {
+			if (slave.porn.fame[genre.fameVar] > 0) {
+				child.children.push({name: genre.fameName, value: Math.trunc(slave.porn.fame[genre.fameVar]), type: type.name});
+			}
+		}
+		if (child.children.length > 0) {
+			data.children.push(child);
+		}
+	}
+	const root = d3.hierarchy(data)
+		.sum(d => +d.value)
+		.sort((a,b) => d3.descending(a.value, b.value));
+	
+	// container size
+	const margin = {top: 10, right: 10, bottom: 10, left: 10},
+		width = 600 - margin.left - margin.right,
+		height = 200 - margin.top - margin.bottom;
+
+	// treemap construction
+	const layout = d3.treemap()
+		.size([width - margin.left - margin.right, height - margin.top - margin.bottom])
+		.padding(1)
+		(root);
+
+	const svg = d3.select(container)
+		.append("svg")
+		.attr("width", width + margin.left + margin.right)
+		.attr("height", height + margin.top + margin.bottom)
+		.append("g")
+		.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+	// rectangles
+	svg.selectAll("rect")
+		.data(layout.leaves())
+		.join("rect")
+			.attr('x', d => d.x0)
+			.attr('y', d => d.y0)
+			.attr('width', d => d.x1 - d.x0)
+			.attr('height', d => d.y1 - d.y0)
+			.style("fill", d => `var(--genre-color-${d.data.type})`)
+			.attr("data-tippy-content", d => `${d.data.name} (${d.data.value})`)
+			// @ts-ignore - can't specify generic arguments from JS, but the right one will be picked
+			.call(x => tippy(x.nodes()));
+
+	// text labels
+	svg.selectAll("text")
+		.data(layout.leaves())
+    	.join("text")
+			.attr("x", d => d.x0 + 5)
+			.attr("y", d => d.y0 + 15)
+			.text(d => d.data.name)
+			.attr("font-size", "15px")
+			.attr("fill", "black")
+
+	return container;	
+};
diff --git a/src/gui/Encyclopedia/encyclopediaX-SeriesArcology.js b/src/gui/Encyclopedia/encyclopediaX-SeriesArcology.js
index 83cda2acbe9..80953d4d39e 100644
--- a/src/gui/Encyclopedia/encyclopediaX-SeriesArcology.js
+++ b/src/gui/Encyclopedia/encyclopediaX-SeriesArcology.js
@@ -2,20 +2,17 @@ App.Encyclopedia.addArticle("The X-Series Arcology", function() {
 	const t = new DocumentFragment();
 	const r = new SpacedTextAccumulator(t);
 
-	r.push("The X-Series Arcology is the latest model of 'arcology', designed in 2037 based off the model of keeping Free Cities as independent from the old world as fiscally viable. The first arcology was designed in early 2016 by the Berlin-based architecture firm Franz-Crekcht as a");
-	r.push('"Skyscraper for the next generation"');
-	r.push("- the A-Series. The original A-Series was made to exist as an arcology integrated within old world nations, but capable of producing its own energy, food, and housing for the population contained within, marketed as buildings you could");
-	r.push('"Live, work, eat and shop at without ever having to leave"');
-	r.push(". These first arcology units were rolled out by the end of the year based on a standard design prototype, and cheap housing prices combined with the promise of stable white-collar work immediately attracted a sizeable population to the A-series arcologies. Based off of these designs, F-C continued to produce arcology schematics, including the K-series and B-series, and formulated plans for arcologies that could operate in rural, maritime, and even oceanic environments. When the popularity of the Arcology model became evident, other design firms replicated their own takes, producing designs like the V-series, D-series, F-series, and finally the ultra-modern X-series that you presently inhabit, designed more as a quasi-independent city-state than the original");
-	r.push('"next-generation skyscrapers"');
-	r.push(". It wasn't long before arcologies began to form 'clusters' together, relying more on each other for trade and cultural exchange than the old world that surrounded them; increasingly complex infrastructure and surrounding networks began to connect the clusters, tying them together and making them stand apart from the rest of society, largely free from the purview of bureaucrats and old cultural mores. As the world outside the increasingly self-sufficient arcologies began to deteriorate, it wasn't long after that before the first arcologies declared independence from their parent nations, creating the original Free Cities we know today.");
+	r.push(`The X-Series Arcology is the latest model of 'arcology', designed in 2037 based off the model of keeping Free Cities as independent from the old world as fiscally viable. The first arcology was designed in early 2016 by the Berlin-based architecture firm Franz-Crekcht as a "Skyscraper for the next generation" - the A-Series. The original A-Series was made to exist as an arcology integrated within old world nations, but capable of producing its own energy, food, and housing for the population contained within, marketed as buildings you could "Live, work, eat and shop at without ever having to leave".`);
+	r.push(`These first arcology units were rolled out by the end of the year based on a standard design prototype, and cheap housing prices combined with the promise of stable white-collar work immediately attracted a sizeable population to the A-series arcologies. Based off of these designs, F-C continued to produce arcology schematics, including the K-series and B-series, and formulated plans for arcologies that could operate in rural, maritime, and even oceanic environments. When the popularity of the Arcology model became evident, other design firms replicated their own takes, producing designs like the V-series, D-series, F-series, and finally the ultra-modern X-series that you presently inhabit, designed more as a quasi-independent city-state than the original "next-generation skyscrapers".`);
+	r.toParagraph();
+	r.push("It wasn't long before arcologies began to form 'clusters' together, relying more on each other for trade and cultural exchange than the old world that surrounded them; increasingly complex infrastructure and surrounding networks began to connect the clusters, tying them together and making them stand apart from the rest of society, largely free from the purview of bureaucrats and old cultural mores. As the world outside the increasingly self-sufficient arcologies began to deteriorate, it wasn't long after that before the first arcologies declared independence from their parent nations, creating the original Free Cities we know today.");
 	r.toParagraph();
 
 	r.push("Choose a more particular entry below:");
 	r.toNode("div");
 
 	return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("What the Upgrades Do", function() {
  const t = new DocumentFragment();
@@ -32,37 +29,38 @@ App.Encyclopedia.addArticle("What the Upgrades Do", function() {
 	 */
   const highlight = (text, tag=["bold"]) => App.UI.DOM.makeElement("span", text, tag);
 
- App.Events.addNode(t, [`There are a lot of upgrades available for your arcology, ${properTitle()}. Please relax; some panic upon reviewing the options is normal. This list should familiarize you with your choices.`], "div", "note");
-
- r.push(highlight("Construction", ["div"]));
- r.push("The first upgrade section on the arcology management menu offers an escalating series of generic upgrades for the arcology. A few of these have minor beneficial side effects, but all share the same main effect: they raise the arcology's maximum prosperity level. You will be informed on the end of week report if your arcology is nearing, at, or over this level. Upgrading early is", highlight("not", ["note"]), "useless, since prosperity will increase more rapidly if the cap is much higher than the current prosperity level.");
- r.toParagraph();
-
- r.push(highlight("Facilities", ["div"]));
- r.push("These upgrades unlock the various facilities, which are detailed", link("here.", "Facilities"));
- r.toParagraph();
-
- r.push(highlight("Penthouse Improvements", ["div"]));
- r.push("The master suite and Head Girl suite options function like facilities. The master suite is the facility for the fucktoy assignment, and the Head Girl suite can house a single slave for her use.");
- r.toNode("div");
- App.Events.addNode(t, [highlight(highlight("Kitchen upgrade:"), ["bold", "note"]), "this increases the chances of success for dieting and opens up additional dietary options."], "div", ["indent"]);
- App.Events.addNode(t, [highlight(highlight("Feeding phalli:"), ["bold", "note"]), "unbroken slaves will find this disgusting, but it can cause beneficial oral fetishes to appear."], "div", ["indent"]);
- App.Events.addNode(t, [highlight(highlight("Drug fuckmachines:"), ["bold", "note"]), "unbroken slaves will resent this, but it may cause beneficial anal fetishes to appear."], "div", ["indent"]);
- App.Events.addNode(t, [highlight(highlight("Personal armory:"), ["bold", "note"]), "unlocks bodyguard options on the main menu."], "div", ["indent"]);
- App.Events.addNode(t, [highlight(highlight("Pharmaceutical Fabricator:"), ["bold", "note"]), "requires a lot of", link("reputation", "Arcologies and Reputation"), "green", "to buy and use; unlocks powerful drug upgrades."], "div", ["indent"]);
- App.Events.addNode(t, [highlight(highlight("Surgery upgrade:"), ["bold", "note"]), "enables several extreme surgical options like virginity restoration and hermaphrodite creation."], "div", ["indent"]);
- r.toNode("p");
-
- r.push(highlight("Special Upgrades", ["div"]));
- r.push("Upgrades obtained during special events are listed here for reference. They cannot be purchased normally. Week:");
- r.toNode("div");
- App.Events.addNode(t, [highlight("24:"), "Arming yourself and or your", link("drones", "Security Drones"), "if installed."], "div", ["indent"]);
- App.Events.addNode(t, [highlight("62:"), "Establishing mercs."], "div", ["indent"]);
- App.Events.addNode(t, [highlight("65:"), "Giving your established mercs a unique title."], "div", ["indent"]);
- App.Events.addNode(t, [highlight("72 or later:"), "Establish the", link("Special Force"), "(if the mod is enabled)."], "div", ["indent"]);
+	App.Events.addNode(t, [`There are a lot of upgrades available for your arcology, ${properTitle()}. Please relax; some panic upon reviewing the options is normal. This list should familiarize you with your choices.`], "div", "note");
+
+	t.append(App.UI.DOM.makeElement("h3", "Construction"));
+	r.push("The first upgrade section on the arcology management menu offers an escalating series of generic upgrades for the arcology. A few of these have minor beneficial side effects, but all share the same main effect: they raise the arcology's maximum prosperity level. You will be informed on the end of week report if your arcology is nearing, at, or over this level. Upgrading early is", highlight("not", ["note"]), "useless, since prosperity will increase more rapidly if the cap is much higher than the current prosperity level.");
+	r.toParagraph();
+
+	t.append(App.UI.DOM.makeElement("h3", "Facilities"));
+	r.push("These upgrades unlock the various facilities, which are detailed", link("here.", "Facilities"));
+	r.toParagraph();
+
+	t.append(App.UI.DOM.makeElement("h3", "Penthouse Improvements"));
+	r.push("The master suite and Head Girl suite options function like facilities. The master suite is the facility for the fucktoy assignment, and the Head Girl suite can house a single slave for her use.");
+	r.toNode("div");
+
+	App.Events.addNode(t, [highlight("Kitchen upgrade:", ["bold", "note"]), "this increases the chances of success for dieting and opens up additional dietary options."], "div", ["indent"]);
+	App.Events.addNode(t, [highlight("Feeding phalli:", ["bold", "note"]), "unbroken slaves will find this disgusting, but it can cause beneficial oral fetishes to appear."], "div", ["indent"]);
+	App.Events.addNode(t, [highlight("Drug fuckmachines:", ["bold", "note"]), "unbroken slaves will resent this, but it may cause beneficial anal fetishes to appear."], "div", ["indent"]);
+	App.Events.addNode(t, [highlight("Personal armory:", ["bold", "note"]), "unlocks bodyguard options on the main menu."], "div", ["indent"]);
+	App.Events.addNode(t, [highlight("Pharmaceutical Fabricator:", ["bold", "note"]), "requires a lot of", link("reputation", "Arcologies and Reputation"), "green", "to buy and use; unlocks powerful drug upgrades."], "div", ["indent"]);
+	App.Events.addNode(t, [highlight("Surgery upgrade:", ["bold", "note"]), "enables several extreme surgical options like virginity restoration and hermaphrodite creation."], "div", ["indent"]);
+	App.Events.addNode(t, [highlight("Media Hub:", ["bold", "note"]), "allows you to produce, encode, and stream videos of your slaves' daily lives to popular online pornography sites."], "div", ["indent"]);
+
+	t.append(App.UI.DOM.makeElement("h3", "Special Upgrades"));
+	r.push("Upgrades obtained during special events are listed here for reference. They cannot be purchased normally. Week:");
+	r.toNode("div");
+	App.Events.addNode(t, [highlight("24:"), "Arming yourself and or your", link("drones", "Security Drones"), "if installed."], "div", ["indent"]);
+	App.Events.addNode(t, [highlight("62:"), "Establishing mercs."], "div", ["indent"]);
+	App.Events.addNode(t, [highlight("65:"), "Giving your established mercs a unique title."], "div", ["indent"]);
+	App.Events.addNode(t, [highlight("72 or later:"), "Establish the", link("Special Force"), "(if the mod is enabled)."], "div", ["indent"]);
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("Personal Assistant", function() {
  const t = new DocumentFragment();
@@ -82,7 +80,7 @@ App.Encyclopedia.addArticle("Personal Assistant", function() {
 	r.toNode("p", ["note"]);
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("The Wardrobe", function() {
  const t = new DocumentFragment();
@@ -95,7 +93,7 @@ App.Encyclopedia.addArticle("The Wardrobe", function() {
 	r.toParagraph();
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("The Auto Salon", function() {
  const t = new DocumentFragment();
@@ -112,7 +110,7 @@ App.Encyclopedia.addArticle("The Auto Salon", function() {
 	r.toParagraph();
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("The Body Mod Studio", function() {
  const t = new DocumentFragment();
@@ -128,7 +126,7 @@ App.Encyclopedia.addArticle("The Body Mod Studio", function() {
 	r.toNode("p", ["note"]);
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("The Remote Surgery", function() {
  const t = new DocumentFragment();
@@ -147,7 +145,7 @@ App.Encyclopedia.addArticle("The Remote Surgery", function() {
 	r.toNode("p", ["note"]);
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("The Pharmaceutical Fab", function() {
  const t = new DocumentFragment();
@@ -160,7 +158,7 @@ App.Encyclopedia.addArticle("The Pharmaceutical Fab", function() {
 	r.toNode("div");
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("Security Drones", function() {
  const t = new DocumentFragment();
@@ -185,7 +183,7 @@ App.Encyclopedia.addArticle("Security Drones", function() {
 	r.toParagraph();
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("Water Filtration", function() {
  const t = new DocumentFragment();
@@ -207,7 +205,7 @@ App.Encyclopedia.addArticle("Water Filtration", function() {
 	r.toParagraph();
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
 
 App.Encyclopedia.addArticle("Slave Nutrition", function() {
  const t = new DocumentFragment();
@@ -229,9 +227,37 @@ App.Encyclopedia.addArticle("Slave Nutrition", function() {
 	r.toParagraph();
 
  return t;
-}, "X-SeriesArchology");
+}, "X-SeriesArcology");
+
+App.Encyclopedia.addArticle("Media Hub", function() {
+	const r = new SpacedTextAccumulator();
+
+	r.push("Not everyone can be a Free Cities titan. But thanks to the power of the internet, everyone can watch!");
+	r.toNode("div", ["note"]);
+
+	r.push("—", App.UI.DOM.makeElement("span", `Porn Insider magazine, October 2035: "Free Cities Pornography: a new era of sex slave voyeurism"`, ["note"]));
+	r.toNode("p", ["note"]);
+
+	r.push("Constructing and upgrading the Media Hub allows you to produce and stream pornographic videos featuring your slaves for the world to see (and pay for).");
+	r.toParagraph();
+
+	r.push("Slaves featured in pornography will gain viewers and fame over time. Some may even become world-famous porn stars in their own right, with a little help.");
+	r.toParagraph();
+
+	r.push("Slaves with pretty faces will gain viewership significantly faster. Particularly ugly slaves may benefit from having their faces covered with a mask, or covered by a", App.Encyclopedia.Dialog.linkDOM("fuckdoll", "Fuckdolls"), "suit.");
+	r.push("Viewership can also be driven by investing cash in promotion and advertising, or by search engine optimization and feed hacking.");
+	r.toParagraph();
+
+	r.push("Slaves will produce porn in a variety of genres based on their characteristics. By default, her viewers will decide what she focuses on with their views and credits, but you can upgrade the media hub to allow youto choose a particular genre for a slave to focus on. Having too many slaves focusing on a particular genre will decrease the ability of that genre to draw new viewers. Genres associated with", App.Encyclopedia.Dialog.linkDOM("paraphilias", "Paraphilias"), "will have their viewership increase fastest, followed by", App.Encyclopedia.Dialog.linkDOM("fetish", "Fetishes"), "genres. More general genres (such as those based on age, weight, and build) grow slowly, and genres associated with sexual", App.Encyclopedia.Dialog.linkDOM("quirks", "Quirks"), "grow slowest of all, and are likely to require investment to promote.");
+	r.toParagraph();
+
+	r.push("If you stop producing porn of a particular slave, her viewership will decline over time, until she is once again unknown.");
+	r.toParagraph();
+
+	return r.container();
+}, "X-SeriesArcology");
 
-App.Encyclopedia.addCategory("X-SeriesArchology", function() {
+App.Encyclopedia.addCategory("X-SeriesArcology", function() {
  const r = [];
 	if (V.encyclopedia !== "The X-Series Arcology") {
 		r.push(App.Encyclopedia.Dialog.linkDOM("The X-Series Arcology"));
@@ -246,5 +272,6 @@ App.Encyclopedia.addCategory("X-SeriesArchology", function() {
  r.push(App.Encyclopedia.Dialog.linkDOM("Security Drones"));
  r.push(App.Encyclopedia.Dialog.linkDOM("Water Filtration"));
  r.push(App.Encyclopedia.Dialog.linkDOM("Slave Nutrition"));
+ r.push(App.Encyclopedia.Dialog.linkDOM("Media Hub"));
  return App.UI.DOM.generateLinksStrip(r);
 }); 
diff --git a/src/gui/quicklinks.js b/src/gui/quicklinks.js
index 8b1a5b37b69..3eec9f15e89 100644
--- a/src/gui/quicklinks.js
+++ b/src/gui/quicklinks.js
@@ -66,6 +66,7 @@ App.UI.quickMenu = (function() {
 			"Prosthetic Lab": true,
 			"Wardrobe": true,
 			"Toy Shop": true,
+			"Media Studio": true,
 			"The Black Market": true,
 		},
 		Tools: {
diff --git a/src/interaction/siRecords.js b/src/interaction/siRecords.js
index 8581fc8610a..18abe00a1db 100644
--- a/src/interaction/siRecords.js
+++ b/src/interaction/siRecords.js
@@ -18,167 +18,128 @@ App.UI.SlaveInteract.records = function(slave, refresh) {
 
 		if (slave.porn.prestige === 3) {
 			App.UI.DOM.appendNewElement("div", el, `${He} is so prestigious in the realm of ${slave.porn.fameType} porn that ${his} fame is self-sustaining.`, ["note"]);
-		} else if (slave.porn.feed === 0) {
-			r = [];
-			r.push(`The media hub is not releasing highlights of ${his} sex life.`);
-			r.push(
-				App.UI.DOM.link(
-					"Release",
-					() => {
-						slave.porn.feed = 1;
-						refresh();
-					}
-				)
-			);
-			App.Events.addNode(el, r, "div");
 		} else {
-			r = [];
-			r.push(`The media hub is releasing highlights of ${his} sex life`);
-			if (slave.porn.spending < 500) {
-				r.push(`to those who can find it.`);
-			} else if (slave.porn.spending < 2500) {
-				r.push(`on several websites.`);
-			} else if (slave.porn.spending > 5000) {
-				r.push(`through your old distributor.`);
-			} else {
-				r.push(`on many websites.`);
-			}
-			if (slave.porn.spending === 0) {
-				linkArray = [];
-				linkArray.push(
+			if (slave.porn.feed === 0) {
+				r = [];
+				r.push(`The media hub is not releasing highlights of ${his} sex life.`);
+				r.push(
 					App.UI.DOM.link(
-						"Halt",
+						"Release",
 						() => {
-							slave.porn.feed = 0;
-							slave.porn.focus = "none";
+							slave.porn.feed = 1;
 							refresh();
 						}
 					)
 				);
-				linkArray.push(
-					App.UI.DOM.link(
-						"Publicize",
-						() => {
-							slave.porn.spending += 1000;
-							refresh();
-						},
-						[],
-						"",
-						`Will cost ${cashFormat(1000)} weekly.`
-					)
-				);
-				r.push(App.UI.DOM.generateLinksStrip(linkArray));
 				App.Events.addNode(el, r, "div");
 			} else {
-				r.push(
-					App.UI.DOM.makeTextBox(
-						slave.porn.spending,
-						v => {
-							slave.porn.spending = v;
-						},
-						true
-					)
-				);
-				r.push(`weekly is spent to publicize them.`);
-
-				linkArray = [];
-				linkArray.push(
-					App.UI.DOM.link(
-						"Halt",
-						() => {
-							slave.porn.spending = 0;
-							slave.porn.feed = 0;
-							slave.porn.focus = "none";
-							V.PCSlutContacts = 1;
-							refresh();
-						}
-					)
-				);
-				if (slave.porn.spending <= 4000) {
+				r = [];
+				r.push(`The media hub is releasing highlights of ${his} sex life`);
+				if (slave.porn.spending < 500) {
+					r.push(`to those who can find it.`);
+				} else if (slave.porn.spending < 2500) {
+					r.push(`on several websites.`);
+				} else if (slave.porn.spending > 5000) {
+					r.push(`through your old distributor.`);
+				} else {
+					r.push(`on many websites.`);
+				}
+				if (slave.porn.spending === 0) {
+					linkArray = [];
 					linkArray.push(
 						App.UI.DOM.link(
-							"Increase",
+							"Halt",
+							() => {
+								slave.porn.feed = 0;
+								slave.porn.focus = "none";
+								refresh();
+							}
+						)
+					);
+					linkArray.push(
+						App.UI.DOM.link(
+							"Publicize",
 							() => {
 								slave.porn.spending += 1000;
 								refresh();
 							},
 							[],
 							"",
-							`Spending more than ${cashFormat(5000)} weekly will have no effect.`
+							`Will cost ${cashFormat(1000)} weekly.`
 						)
 					);
-				}
-				linkArray.push(
-					App.UI.DOM.link(
-						"Decrease",
-						() => {
-							slave.porn.spending -= 1000;
-							refresh();
-						},
-						[],
-						"",
-						`Will cost ${cashFormat(1000)} weekly.`
-					)
-				);
-				r.push(App.UI.DOM.generateLinksStrip(linkArray));
-				App.Events.addNode(el, r, "div");
+					r.push(App.UI.DOM.generateLinksStrip(linkArray));
+					App.Events.addNode(el, r, "div");
+				} else {
+					r.push(
+						App.UI.DOM.makeTextBox(
+							slave.porn.spending,
+							v => {
+								slave.porn.spending = v;
+							},
+							true
+						)
+					);
+					r.push(`weekly is spent to publicize them.`);
 
-				if (V.PC.career === "escort" || V.PC.career === "prostitute" || V.PC.career === "child prostitute") {
-					r = [];
-					if (V.PC.career === "escort") {
-						r.push(`You retain some contacts from your past life in the industry that may be willing to cut you some discounts should you return to it.`);
-					} else {
-						r.push(`You were approached in the past to star in some adult films and they may be willing to cut you some discounts should you accept their offer.`);
-					}
-					if (V.PCSlutContacts !== 2) {
-						r.push(`You are not baring your body for all to see.`);
-						r.push(
-							App.UI.DOM.link(
-								`Star in porn for a discount`,
-								() => {
-									V.PCSlutContacts = 2;
-									refresh();
-								}
-							)
-						);
-					} else {
-						if (V.PC.career === "escort") {
-							r.push(`You are starring in hardcore porn once more.`);
-						} else if (V.PC.actualAge < V.minimumSlaveAge) {
-							r.push(`You are taking part in porn that may disturb people.`);
-						} else {
-							r.push(`You are starring in some hardcore porn.`);
-						}
-						r.push(
+					linkArray = [];
+					linkArray.push(
+						App.UI.DOM.link(
+							"Halt",
+							() => {
+								slave.porn.spending = 0;
+								slave.porn.feed = 0;
+								slave.porn.focus = "none";
+								refresh();
+							}
+						)
+					);
+					if (slave.porn.spending <= 4000) {
+						linkArray.push(
 							App.UI.DOM.link(
-								`Stop doing porn for a discount`,
+								"Increase",
 								() => {
-									V.PCSlutContacts = 1;
+									slave.porn.spending += 1000;
 									refresh();
-								}
+								},
+								[],
+								"",
+								`Spending more than ${cashFormat(5000)} weekly will have no effect.`
 							)
 						);
 					}
+					linkArray.push(
+						App.UI.DOM.link(
+							"Decrease",
+							() => {
+								slave.porn.spending -= 1000;
+								refresh();
+							}
+						)
+					);
+					r.push(App.UI.DOM.generateLinksStrip(linkArray));
 					App.Events.addNode(el, r, "div");
 				}
-			}
-			if (V.studioFeed === 1) {
-				r = [];
-				if (slave.porn.viewerCount < 100) {
-					r.push(`${He} lacks the fame in porn needed to discern what ${his} feed is getting tagged as.`);
-				} else {
-					if (slave.porn.prestige > 0) {
-						r.push(`${He} is known for ${slave.porn.fameType === "generic" ? `standard, vanilla` : slave.porn.fameType} porn${(slave.porn.prestige > 1) ? ` and viewers have grown to expect it from ${him}` : ``}.`);
-					}
-					if (slave.porn.focus === "none") {
-						r.push(`You are allowing ${his} viewers to guide the direction of ${his} content.`);
+				if (V.studioFeed === 1) {
+					if (slave.porn.viewerCount < 100) {
+						r.push(`${He} lacks the fame in porn needed to discern what ${his} feed is getting tagged as.`);
 					} else {
-						r.push(`You are focusing attention on the ${slave.porn.focus} aspect of ${his} content.`);
+						r = [];
+						if (slave.porn.prestige > 0) {
+							r.push(`${He} is known for ${slave.porn.fameType === "generic" ? `standard, vanilla` : slave.porn.fameType} porn${(slave.porn.prestige > 1) ? ` and viewers have grown to expect it from ${him}` : ``}.`);
+						}
+						if (slave.porn.focus === "none") {
+							r.push(`You are allowing ${his} viewers to guide the direction of ${his} content.`);
+						} else {
+							r.push(`You are focusing attention on the ${slave.porn.focus} aspect of ${his} content.`);
+						}
+						r.push(App.Porn.genreChoiceLinks("Slave Interact", slave));
+						App.Events.addNode(el, r, "div");
 					}
-					r.push(App.Porn.genreChoiceLinks("Slave Interact", slave));
 				}
-				App.Events.addNode(el, r, "div");
 			}
+			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.combineNodes(App.Porn.makeFameProgressChart(slave)));
+			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.combineNodes("Current viewership breakdown:", App.Porn.makeViewershipChart(slave)));
 		}
 	}
 	App.UI.DOM.appendNewElement("h3", el, "Financial");
diff --git a/src/js/porn.js b/src/js/porn.js
index 5c17c90ecf8..00e8db62357 100644
--- a/src/js/porn.js
+++ b/src/js/porn.js
@@ -1,34 +1,38 @@
-App.Porn = {};
 App.Porn.GenreType = {
 	paraphilia: {
 		focusedViewershipFactor: 1.5,
 		unfocusedViewershipFactor: 0.5,
 		viewershipSoakingFactor: 0.0,
-		bonusViewership: function(slave) { return slave.fetishStrength * 2.0; }
+		bonusViewership: function(slave) { return slave.fetishStrength * 2.0; },
+		name: "paraphilia"
 	},
 	fetish: {
 		focusedViewershipFactor: 2.0,
 		unfocusedViewershipFactor: 0.5,
 		viewershipSoakingFactor: 1.0,
-		bonusViewership: function(slave) { return slave.fetishStrength; }
+		bonusViewership: function(slave) { return slave.fetishStrength; },
+		name: "fetish"
 	},
 	general: {
 		focusedViewershipFactor: 4.0,
 		unfocusedViewershipFactor: 0.5,
 		viewershipSoakingFactor: 1.0,
-		bonusViewership: function(slave) { return 0.0; }
+		bonusViewership: function(slave) { return 0.0; },
+		name: "general"
 	},
 	quirk: {
 		focusedViewershipFactor: 6.0,
 		unfocusedViewershipFactor: 0.5,
 		viewershipSoakingFactor: 1.0,
-		bonusViewership: function(slave) { return 0.0; }
+		bonusViewership: function(slave) { return 0.0; },
+		name: "quirk"
 	},
 	generic: {
 		focusedViewershipFactor: 5.0,
 		unfocusedViewershipFactor: 1.0,
 		viewershipSoakingFactor: 0.0,
-		bonusViewership: function(slave) { return 0.0; }
+		bonusViewership: function(slave) { return 0.0; },
+		name: "generic"
 	}
 };
 
-- 
GitLab