From 0ea37a7239abe1d1a0f9481caae91e1f89b260d4 Mon Sep 17 00:00:00 2001
From: jgl <jgl6@protonmail.com>
Date: Mon, 6 Nov 2023 09:14:52 +0100
Subject: [PATCH] Restore custom animal functionality

---
 js/003-data/gameVariableData.js            |  3 +-
 src/facilities/farmyard/animals/Animal.js  | 22 ++++-
 src/facilities/farmyard/animals/animals.js | 94 ++++++++++++++++------
 3 files changed, 94 insertions(+), 25 deletions(-)

diff --git a/js/003-data/gameVariableData.js b/js/003-data/gameVariableData.js
index fdbcf4cf2bb..ded5a1b0ef6 100644
--- a/js/003-data/gameVariableData.js
+++ b/js/003-data/gameVariableData.js
@@ -755,6 +755,7 @@ App.Data.resetOnNGPlus = {
 		/** @type {string[]} */
 		feline: [],
 	},
+	customAnimals: [],
 	farmyardName: "the Farmyard",
 
 	HGSuite: 0,
@@ -1300,7 +1301,7 @@ App.Data.resetOnNGPlus = {
 		raGrowthExpr: 0,
 		sexOverhaul: 0,
 		interactions: 0,
-		clitoralPenetration : 0,
+		clitoralPenetration: 0,
 		raSortOutput: 0,
 		favSeparateReport: 0,
 	},
diff --git a/src/facilities/farmyard/animals/Animal.js b/src/facilities/farmyard/animals/Animal.js
index 7f30b113735..ebcc0b9ec8d 100644
--- a/src/facilities/farmyard/animals/Animal.js
+++ b/src/facilities/farmyard/animals/Animal.js
@@ -30,6 +30,11 @@ App.Entity.Animal = class Animal {
 		return V.active[this.type] === this.name;
 	}
 
+	/** @returns {boolean} */
+	get isCustom() {
+		return V.customAnimals.find(animal => animal.name === this.name);
+	}
+
 	/** @returns {string} */
 	get ballType() {
 		return this.species;
@@ -48,6 +53,8 @@ App.Entity.Animal = class Animal {
 
 	/** @returns {this} */
 	sell() {
+		V.animals[this.type] = V.animals[this.type].filter(animal => animal !== this.name);
+
 		if (this.isActive) {
 			V.active[this.type] = V.animals[this.type].random() || null;
 		}
@@ -56,11 +63,24 @@ App.Entity.Animal = class Animal {
 			V.pit.animal = null;
 		}
 		*/
-		V.animals[this.type] = V.animals[this.type].filter(animal => animal !== this.name);
 
 		return this;
 	}
 
+	/** @returns {boolean} Removal succeeded */
+	remove() {
+		if (!this.isCustom) {
+			throw Error('Unable to remove default animal.');
+		}
+		const index = V.customAnimals.findIndex(animal => animal.name === this.name);
+		if (~index) {
+			V.customAnimals.splice(index, 1);
+			App.Data.animals.delete(this);
+			return true;
+		}
+		return false;
+	}
+
 	/** @param {string} name */
 	setName(name) {
 		this.name = name;
diff --git a/src/facilities/farmyard/animals/animals.js b/src/facilities/farmyard/animals/animals.js
index 9ffffda389e..b78710845a1 100644
--- a/src/facilities/farmyard/animals/animals.js
+++ b/src/facilities/farmyard/animals/animals.js
@@ -12,7 +12,7 @@ App.Facilities.Farmyard.animals = function() {
 
 	App.UI.DOM.appendNewElement("div", frag, domestic(), ['margin-bottom']);
 	App.UI.DOM.appendNewElement("div", frag, exotic(), ['margin-bottom']);
-	// frag.append(addAnimal()); // FIXME: causes save corruption.
+	frag.append(addAnimal());
 
 	V.nextButton = "Back";
 	V.nextLink = "Farmyard";
@@ -279,32 +279,34 @@ App.Facilities.Farmyard.animals = function() {
 	 * @param {function():void} param.setActiveHandler
 	 * @param {function():void} param.purchaseHandler
 	 * @param {function():void} param.sellHandler
+	 * @param {function():void} param.removeCustomHandler
 	 * @returns {string|HTMLElement}
 	 */
-	function animalLink({animal, active, type, price, setActiveHandler, purchaseHandler, sellHandler}) {
-		if (animal.purchased || V.animals[animal.type].some(a => a === animal.name)) {
-			const div = document.createElement("div");
-
-			const options = [];
+	function animalLink({
+		animal, active, type, price, setActiveHandler, purchaseHandler, sellHandler, removeCustomHandler
+	}) {
+		const div = document.createElement("div");
+		const options = [];
 
-			if (V.active[active] && V.active[active] === animal.name) {
-				options.push(
-					App.UI.DOM.disabledLink(`Set as active ${type}`, ['Already set as active']),
-					App.UI.DOM.link(`Sell`, sellHandler)
-				);
+		if (animal.purchased || V.animals[animal.type].some(a => a === animal.name)) {
+			if (V.active[active] && animal.isActive) {
+				options.push(App.UI.DOM.disabledLink(`Set as active ${type}`, ['Already set as active']));
 			} else {
-				options.push(
-					App.UI.DOM.link(`Set as active ${type}`, setActiveHandler),
-					App.UI.DOM.link(`Sell`, sellHandler)
-				);
+				options.push(App.UI.DOM.link(`Set as active ${type}`, setActiveHandler));
 			}
+			options.push(App.UI.DOM.link(`Sell`, sellHandler));
+		} else {
+			div.append(makePurchase(`Purchase ${animal.articleAn} ${animal.name}`, price, "farmyard", {notes: [`will incur upkeep costs`], handler: purchaseHandler}));
+		}
 
-			div.append(App.UI.DOM.generateLinksStrip(options));
+		if (animal.isCustom) {
+			options.push(App.UI.DOM.link(`Remove custom ${type}`, removeCustomHandler));
+		}
 
-			return div;
-		} else {
-			return makePurchase(`Purchase ${animal.articleAn} ${animal.name}`, price, "farmyard", {notes: [`will incur upkeep costs`], handler: purchaseHandler});
+		if (options.length) {
+			div.append(App.UI.DOM.generateLinksStrip(options));
 		}
+		return div;
 	}
 
 	/**
@@ -336,11 +338,10 @@ App.Facilities.Farmyard.animals = function() {
 					price: price,
 					setActiveHandler() {
 						animal.setActive();
-						App.UI.DOM.replace(div, animalList(type, rarity, price, active));
+						App.UI.DOM.replace(div, animalList(type, rarity, price, active, species, exclude));
 						App.UI.DOM.replace(activeDiv, activeAnimals());
 					},
 					purchaseHandler() {
-						cashX(forceNeg(price), "farmyard");
 						animal.purchase();
 						if (!V.active[animal.type]) {
 							animal.setActive();
@@ -354,6 +355,15 @@ App.Facilities.Farmyard.animals = function() {
 						App.UI.DOM.replace(activeDiv, activeAnimals());
 						App.UI.DOM.replace(div, animalList(type, rarity, price, active, species, exclude));
 					},
+					removeCustomHandler() {
+						if (animal.purchased || V.animals[animal.type].some(a => a === animal.name)) {
+							cashX(price * 0.75, "farmyard");
+							animal.sell();
+						}
+						animal.remove();
+						App.UI.DOM.replace(activeDiv, activeAnimals());
+						App.UI.DOM.replace(div, animalList(type, rarity, price, active, species, exclude));
+					}
 				};
 
 				optionDiv.append(animalLink(args));
@@ -365,7 +375,6 @@ App.Facilities.Farmyard.animals = function() {
 		return div;
 	}
 
-	// FIXME: added animals are just blindly shoved into session data and make the game will crash when a save is loaded in a different session
 	function addAnimal() {
 		const frag = new DocumentFragment();
 
@@ -531,11 +540,16 @@ App.Facilities.Farmyard.animals = function() {
 				disabledReasons.push(`Animal must have a species.`);
 			}
 
+			if (getAnimal(animal.name)) {
+				disabledReasons.push(`Animal with the same name already exists.`);
+			}
+
 			if (disabledReasons.length > 0) {
 				App.UI.DOM.appendNewElement("div", addDiv, App.UI.DOM.disabledLink(`Add`, disabledReasons), ['margin-top']);
 			} else {
 				App.UI.DOM.appendNewElement("div", addDiv, App.UI.DOM.link(`Add`, () => {
 					App.Data.animals.add(animal);
+					V.customAnimals.push(animal);
 
 					App.UI.reload();
 				}), ['margin-top']);
@@ -572,7 +586,7 @@ globalThis.getAnimal = function(name) {
 
 App.Facilities.Farmyard.animals.init = function() {
 	if (!App.Data.animals || App.Data.animals.size === 0) {
-		class Animal extends App.Entity.Animal {}
+		class Animal extends App.Entity.Animal { }
 
 		const dog = 'dog';
 		const cat = 'cat';
@@ -639,6 +653,40 @@ App.Facilities.Farmyard.animals.init = function() {
 			new Animal("puma", "puma", feline, exotic),
 			new Animal("tiger", "tiger", feline, exotic),
 		].forEach(animal => App.Data.animals.add(animal));
+
+		/** @param {Animal} data */
+		const restoreAnimal = function(data) {
+			const {name, species, type, rarity, dick, deadliness, articleAn} = data;
+			if (!name || !species || !type || !rarity) {
+				return;
+			}
+
+			const animal = new Animal(name, species, type, rarity);
+
+			if (dick && dick.size) {
+				animal.setDick(dick.size, dick.desc);
+			}
+			if (deadliness) {
+				animal.setDeadliness(deadliness);
+			}
+			if (articleAn && (articleAn === 'a' || articleAn === 'an')) {
+				animal.setArticle(articleAn);
+			}
+			return animal;
+		};
+
+		for (const animalData of V.customAnimals) {
+			const animal = restoreAnimal(animalData);
+			if (animal) {
+				App.Data.animals.add(animal);
+			} else {
+				const index = V.customAnimals.indexOf(animalData);
+				if (~index) {
+					V.customAnimals.splice(index, 1);
+					console.error('Invalid custom animal removed: ', animalData);
+				}
+			}
+		}
 	}
 };
 
-- 
GitLab