code:
App.Data.NationalityPresets = {};

/**
 * @type {Map<string, Record<string,number>>}
 */
App.Data.NationalityPresets.Vanilla = new Map([
	["Vanilla Global", {
		Afghan: 2,
		Albanian: 1,
		Algerian: 3,
		American: 6,
		Andorran: 1,
		Angolan: 1,
		Antiguan: 1,
		Argentinian: 4,
		Armenian: 2,
		Aruban: 1,
		Australian: 3,
		Austrian: 2,
		Azerbaijani: 1,
		Bahamian: 1,
		Bahraini: 1,
		Bangladeshi: 4,
		Barbadian: 1,
		Belarusian: 1,
		Belgian: 2,
		Belizean: 1,
		Beninese: 1,
		Bermudian: 1,
		Bhutanese: 1,
		"Bissau-Guinean": 1,
		Bolivian: 2,
		Bosnian: 1,
		Brazilian: 4,
		British: 3,
		Bruneian: 1,
		Bulgarian: 2,
		Burkinabé: 1,
		Burmese: 2,
		Burundian: 1,
		Cambodian: 1,
		Cameroonian: 1,
		Canadian: 2,
		"Cape Verdean": 1,
		Catalan: 1,
		"Central African": 1,
		Chadian: 1,
		Chilean: 2,
		Chinese: 15,
		Colombian: 3,
		Comorian: 1,
		Congolese: 1,
		"a Cook Islander": 1,
		"Costa Rican": 1,
		Croatian: 1,
		Cuban: 2,
		Curaçaoan: 1,
		Cypriot: 1,
		Czech: 2,
		Danish: 2,
		Djiboutian: 1,
		Dominican: 2,
		Dominiquais: 1,
		Dutch: 2,
		"East Timorese": 1,
		Ecuadorian: 1,
		Egyptian: 3,
		Emirati: 2,
		Equatoguinean: 1,
		Eritrean: 1,
		Estonian: 2,
		Ethiopian: 3,
		Fijian: 1,
		Filipina: 3,
		Finnish: 2,
		French: 3,
		"French Guianan": 1,
		"French Polynesian": 1,
		Gabonese: 1,
		Gambian: 1,
		Georgian: 1,
		German: 3,
		Ghanan: 2,
		Greek: 2,
		Greenlandic: 1,
		Grenadian: 1,
		Guamanian: 1,
		Guatemalan: 1,
		Guinean: 1,
		Guyanese: 1,
		Haitian: 2,
		Honduran: 1,
		Hungarian: 2,
		"I-Kiribati": 1,
		Icelandic: 2,
		Indian: 11,
		Indonesian: 5,
		Iranian: 3,
		Iraqi: 2,
		Irish: 2,
		Israeli: 2,
		Italian: 3,
		Ivorian: 1,
		Jamaican: 2,
		Japanese: 3,
		Jordanian: 2,
		Kazakh: 2,
		Kenyan: 3,
		Kittitian: 1,
		Korean: 2,
		Kosovan: 1,
		Kurdish: 1,
		Kuwaiti: 1,
		Kyrgyz: 1,
		Laotian: 1,
		Latvian: 1,
		Lebanese: 2,
		Liberian: 1,
		Libyan: 2,
		"a Liechtensteiner": 1,
		Lithuanian: 2,
		Luxembourgian: 1,
		Macedonian: 1,
		Malagasy: 1,
		Malawian: 1,
		Malaysian: 3,
		Maldivian: 1,
		Malian: 2,
		Maltese: 1,
		Marshallese: 1,
		Mauritanian: 1,
		Mauritian: 1,
		Mexican: 4,
		Micronesian: 1,
		Moldovan: 1,
		Monégasque: 1,
		Mongolian: 1,
		Montenegrin: 1,
		Moroccan: 2,
		Mosotho: 1,
		Motswana: 1,
		Mozambican: 1,
		Namibian: 1,
		Nauruan: 1,
		Nepalese: 2,
		"New Caledonian": 1,
		"a New Zealander": 2,
		"Ni-Vanuatu": 1,
		Nicaraguan: 2,
		Nigerian: 4,
		Nigerien: 2,
		Niuean: 1,
		Norwegian: 2,
		Omani: 2,
		Pakistani: 3,
		Palauan: 1,
		Palestinian: 1,
		Panamanian: 1,
		"Papua New Guinean": 1,
		Paraguayan: 1,
		Peruvian: 2,
		Polish: 3,
		Portuguese: 2,
		"Puerto Rican": 1,
		Qatari: 1,
		Romanian: 2,
		Russian: 4,
		Rwandan: 1,
		Sahrawi: 1,
		"Saint Lucian": 1,
		Salvadoran: 2,
		Sammarinese: 1,
		Samoan: 1,
		"São Toméan": 1,
		Saudi: 2,
		Scottish: 1,
		Senegalese: 1,
		Serbian: 2,
		Seychellois: 1,
		"Sierra Leonean": 1,
		Singaporean: 1,
		Slovak: 2,
		Slovene: 1,
		"a Solomon Islander": 1,
		Somali: 1,
		"South African": 4,
		"South Sudanese": 1,
		Spanish: 3,
		"Sri Lankan": 1,
		Sudanese: 3,
		Surinamese: 1,
		Swazi: 1,
		Swedish: 2,
		Swiss: 2,
		Syrian: 1,
		Taiwanese: 1,
		Tajik: 1,
		Tanzanian: 2,
		Thai: 3,
		Tibetan: 1,
		Togolese: 1,
		Tongan: 1,
		Trinidadian: 1,
		Tunisian: 2,
		Turkish: 3,
		Turkmen: 1,
		Tuvaluan: 1,
		Ugandan: 2,
		Ukrainian: 3,
		Uruguayan: 2,
		Uzbek: 2,
		Vatican: 1,
		Venezuelan: 3,
		Vietnamese: 3,
		Vincentian: 1,
		Yemeni: 2,
		Zairian: 3,
		Zambian: 1,
		Zimbabwean: 2
	}],
	["Vanilla North America", {
		American: 6,
		Antiguan: 1,
		Aruban: 1,
		Bahamian: 1,
		Barbadian: 1,
		Belizean: 1,
		Bermudian: 1,
		Canadian: 2,
		"Costa Rican": 1,
		Cuban: 2,
		Curaçaoan: 1,
		Dominican: 2,
		Dominiquais: 1,
		Greenlandic: 1,
		Grenadian: 1,
		Guatemalan: 1,
		Haitian: 2,
		Honduran: 1,
		Jamaican: 2,
		Kittitian: 1,
		Mexican: 4,
		Nicaraguan: 2,
		Panamanian: 1,
		"Puerto Rican": 1,
		"Saint Lucian": 1,
		Salvadoran: 2,
		Trinidadian: 1,
		Vincentian: 1
	}],
	["Vanilla South America", {
		Argentinian: 4,
		Bolivian: 2,
		Brazilian: 4,
		Chilean: 2,
		Colombian: 3,
		Ecuadorian: 1,
		"French Guianan": 1,
		Guyanese: 1,
		Paraguayan: 1,
		Peruvian: 2,
		Surinamese: 1,
		Uruguayan: 2,
		Venezuelan: 3
	}],
	["Vanilla Middle East", {
		Afghan: 2,
		Armenian: 2,
		Azerbaijani: 1,
		Bahraini: 1,
		Cypriot: 1,
		Egyptian: 3,
		Emirati: 2,
		Georgian: 1,
		Iranian: 3,
		Iraqi: 2,
		Israeli: 2,
		Jordanian: 2,
		Kurdish: 1,
		Kuwaiti: 1,
		Lebanese: 2,
		Omani: 2,
		Palestinian: 1,
		Qatari: 1,
		Saudi: 2,
		Syrian: 1,
		Turkish: 3,
		Yemeni: 2
	}],
	["Vanilla Africa", {
		Algerian: 3,
		Angolan: 1,
		Beninese: 1,
		"Bissau-Guinean": 1,
		Burkinabé: 1,
		Burundian: 1,
		Cameroonian: 1,
		"Cape Verdean": 1,
		"Central African": 1,
		Chadian: 1,
		Comorian: 1,
		Congolese: 1,
		Djiboutian: 1,
		Equatoguinean: 1,
		Eritrean: 1,
		Ethiopian: 3,
		Gabonese: 1,
		Gambian: 1,
		Ghanan: 2,
		Guinean: 1,
		Ivorian: 1,
		Kenyan: 3,
		Liberian: 1,
		Libyan: 2,
		Malagasy: 1,
		Malawian: 1,
		Malian: 2,
		Mauritanian: 1,
		Mauritian: 1,
		Moroccan: 2,
		Mosotho: 1,
		Motswana: 1,
		Mozambican: 1,
		Namibian: 1,
		Nigerian: 4,
		Nigerien: 2,
		Rwandan: 1,
		Sahrawi: 1,
		"São Toméan": 1,
		Senegalese: 1,
		Seychellois: 1,
		"Sierra Leonean": 1,
		Somali: 1,
		"South African": 4,
		"South Sudanese": 1,
		Sudanese: 3,
		Swazi: 1,
		Tanzanian: 2,
		Togolese: 1,
		Tunisian: 2,
		Ugandan: 2,
		Zairian: 3,
		Zambian: 1,
		Zimbabwean: 2
	}],
	["Vanilla Asia", {
		Bangladeshi: 4,
		Bhutanese: 1,
		Bruneian: 1,
		Burmese: 2,
		Cambodian: 1,
		Chinese: 15,
		"East Timorese": 1,
		Filipina: 3,
		Indian: 11,
		Indonesian: 5,
		Japanese: 3,
		Kazakh: 2,
		Korean: 2,
		Kyrgyz: 1,
		Laotian: 1,
		Malaysian: 3,
		Maldivian: 1,
		Mongolian: 1,
		Nepalese: 2,
		Pakistani: 3,
		Singaporean: 1,
		"Sri Lankan": 1,
		Taiwanese: 1,
		Tajik: 1,
		Thai: 3,
		Tibetan: 1,
		Turkmen: 1,
		Uzbek: 2,
		Vietnamese: 3
	}],
	["Vanilla Europe", {
		Albanian: 1,
		Andorran: 1,
		Austrian: 2,
		Belarusian: 1,
		Belgian: 2,
		Bosnian: 1,
		British: 3,
		Bulgarian: 2,
		Catalan: 1,
		Croatian: 1,
		Czech: 2,
		Danish: 2,
		Dutch: 2,
		Estonian: 2,
		Finnish: 2,
		French: 3,
		German: 3,
		Greek: 2,
		Hungarian: 2,
		Icelandic: 2,
		Irish: 2,
		Italian: 3,
		Kosovan: 1,
		Latvian: 1,
		"a Liechtensteiner": 1,
		Lithuanian: 2,
		Luxembourgian: 1,
		Macedonian: 1,
		Maltese: 1,
		Moldovan: 1,
		Monégasque: 1,
		Montenegrin: 1,
		Norwegian: 2,
		Polish: 3,
		Portuguese: 2,
		Romanian: 2,
		Russian: 4,
		Sammarinese: 1,
		Scottish: 1,
		Serbian: 2,
		Slovak: 2,
		Slovene: 1,
		Spanish: 3,
		Swedish: 2,
		Swiss: 2,
		Ukrainian: 3,
		Vatican: 1
	}],
	["Vanilla Australia", {
		Australian: 3,
		"a Cook Islander": 1,
		Fijian: 1,
		"French Polynesian": 1,
		Guamanian: 1,
		"I-Kiribati": 1,
		Marshallese: 1,
		Micronesian: 1,
		Nauruan: 1,
		"New Caledonian": 1,
		"a New Zealander": 2,
		"Ni-Vanuatu": 1,
		Niuean: 1,
		Palauan: 1,
		"Papua New Guinean": 1,
		Samoan: 1,
		"a Solomon Islander": 1,
		Tongan: 1,
		Tuvaluan: 1
	}],
]);

/**
 * @type {Map<string, Record<string,number>>}
 */
App.Data.NationalityPresets.Mod = new Map([
	["Realism North America", {
		American: 36503,
		Antiguan: 11,
		Aruban: 10,
		Bahamian: 45,
		Barbadian: 28,
		Belizean: 50,
		Bermudian: 5,
		Canadian: 4188,
		"Costa Rican": 555,
		Cuban: 1141,
		Curaçaoan: 17,
		Dominican: 1249,
		Dominiquais: 7,
		Greenlandic: 5,
		Grenadian: 11,
		Guatemalan: 2277,
		Haitian: 1303,
		Honduran: 1177,
		Jamaican: 290,
		Kittitian: 6,
		Mexican: 15306,
		Nicaraguan: 731,
		Panamanian: 515,
		"Puerto Rican": 354,
		"Saint Lucian": 18,
		Salvadoran: 688,
		Trinidadian: 136,
		Vincentian: 11
	}],
	["Realism South America", {
		Argentinian: 5102,
		Bolivian: 1392,
		Brazilian: 22920,
		Chilean: 2005,
		Colombian: 5105,
		Ecuadorian: 2055,
		"French Guianan": 41,
		Guyanese: 83,
		Paraguayan: 817,
		Peruvian: 3829,
		Surinamese: 63,
		Uruguayan: 362,
		Venezuelan: 3826
	}],
	["Realism Middle East", {
		Afghan: 5090,
		Armenian: 286,
		Azerbaijani: 1086,
		Bahraini: 212,
		Cypriot: 131,
		Egyptian: 12826,
		Emirati: 1164,
		Georgian: 366,
		Iranian: 9047,
		Iraqi: 5982,
		Israeli: 1062,
		Jordanian: 1184,
		Kurdish: 7258,
		Kuwaiti: 511,
		Lebanese: 533,
		Omani: 612,
		Palestinian: 746,
		Qatari: 340,
		Saudi: 4131,
		Syrian: 2888,
		Turkish: 9091,
		Yemeni: 3996
	}],
	["Realism Africa", {
		Algerian: 5107,
		Angolan: 5166,
		Beninese: 1756,
		"Bissau-Guinean": 275,
		Burkinabé: 3105,
		Burundian: 1797,
		Cameroonian: 3688,
		"Cape Verdean": 66,
		"Central African": 679,
		Chadian: 2431,
		Comorian: 116,
		Congolese: 827,
		Djiboutian: 118,
		Equatoguinean: 210,
		Eritrean: 742,
		Ethiopian: 15303,
		Gabonese: 282,
		Gambian: 338,
		Ghanan: 4071,
		Guinean: 1978,
		Ivorian: 3741,
		Kenyan: 7408,
		Liberian: 727,
		Libyan: 760,
		Malagasy: 3989,
		Malawian: 3010,
		Malian: 3098,
		Mauritanian: 676,
		Mauritian: 128,
		Moroccan: 4240,
		Mosotho: 275,
		Motswana: 297,
		Mozambican: 4824,
		Namibian: 352,
		Nigerian: 29732,
		Nigerien: 4187,
		Rwandan: 1754,
		Sahrawi: 80,
		"São Toméan": 29,
		Senegalese: 2486,
		Seychellois: 9,
		"Sierra Leonean": 1056,
		Somali: 2469,
		"South African": 6688,
		"South Sudanese": 1918,
		Sudanese: 6099,
		Swazi: 177,
		Tanzanian: 9586,
		Togolese: 1166,
		Tunisian: 1316,
		Ugandan: 7338,
		Zairian: 13815,
		Zambian: 2844,
		Zimbabwean: 2355
	}],
	["Realism Asia", {
		Bangladeshi: 19160,
		Bhutanese: 94,
		Bruneian: 50,
		Burmese: 6043,
		Cambodian: 1972,
		Chinese: 143350,
		"East Timorese": 187,
		Filipina: 13266,
		Indian: 156457,
		Indonesian: 30475,
		Japanese: 11849,
		Kazakh: 2095,
		Korean: 7977,
		Kyrgyz: 730,
		Laotian: 841,
		Malaysian: 3838,
		Maldivian: 52,
		Mongolian: 369,
		Nepalese: 3456,
		Pakistani: 26109,
		Singaporean: 647,
		"Sri Lankan": 2149,
		Taiwanese: 2410,
		Tajik: 1202,
		Thai: 6919,
		Tibetan: 435,
		Turkmen: 707,
		Uzbek: 3805,
		Vietnamese: 3255
	}],
	/* I need reweighting and possibly country additions */
	["Realism Europe", {
		Albanian: 289,
		Andorran: 7,
		Austrian: 897,
		Belarusian: 899,
		Belgian: 1215,
		Bosnian: 333,
		British: 7189,
		Bulgarian: 615,
		Catalan: 767,
		Croatian: 379,
		Czech: 1039,
		Danish: 611,
		Dutch: 1769,
		Estonian: 122,
		Finnish: 578,
		French: 6886,
		German: 8173,
		Greek: 1062,
		Hungarian: 900,
		Icelandic: 37,
		Irish: 537,
		Italian: 5753,
		Kosovan: 193,
		Latvian: 168,
		"a Liechtensteiner": 4,
		Lithuanian: 263,
		Luxembourgian: 70,
		Macedonian: 205,
		Maltese: 43,
		Moldovan: 372,
		Monégasque: 4,
		Montenegrin: 61,
		Norwegian: 619,
		Polish: 3569,
		Portuguese: 969,
		Romanian: 1797,
		Russian: 13807,
		Sammarinese: 3,
		Scottish: 575,
		Serbian: 814,
		Slovak: 530,
		Slovene: 203,
		Spanish: 4586,
		Swedish: 1094,
		Swiss: 941,
		Ukrainian: 3989,
		Vatican: 1
	}],
	["Realism Australia", {
		Australian: 2952,
		"a Cook Islander": 1,
		Fijian: 98,
		"French Polynesian": 31,
		Guamanian: 18,
		"I-Kiribati": 15,
		Marshallese: 5,
		Micronesian: 12,
		Nauruan: 1,
		"New Caledonian": 33,
		"a New Zealander": 536,
		"Ni-Vanuatu": 38,
		Niuean: 1,
		Palauan: 2,
		"Papua New Guinean": 1136,
		Samoan: 22,
		"a Solomon Islander": 83,
		Tongan: 12,
		Tuvaluan: 1
	}],
	["East Asia", {Chinese: 21, Japanese: 2, Korean: 1}],
	["USA", {American: 8, Canadian: 1, Mexican: 3}],
	["Brazil", {
		Argentinian: 4,
		Bolivian: 2,
		Brazilian: 30,
		Chilean: 2,
		Colombian: 3,
		Ecuadorian: 1,
		"French Guianan": 1,
		Guyanese: 1,
		Paraguayan: 1,
		Peruvian: 2,
		Surinamese: 1,
		Uruguayan: 2,
		Venezuelan: 2
	}],
	["Mediterranean", {
		Albanian: 29,
		Algerian: 404,
		Andorran: 1,
		Bosnian: 35,
		British: 1,
		Bulgarian: 71,
		Catalan: 75,
		Croatian: 42,
		Cypriot: 12,
		Egyptian: 948,
		French: 671,
		Georgian: 49,
		Greek: 108,
		Israeli: 89,
		Italian: 605,
		Lebanese: 60,
		Libyan: 63,
		Maltese: 5,
		Monégasque: 1,
		Montenegrin: 7,
		Moroccan: 358,
		Palestinian: 18,
		Portuguese: 102,
		Romanian: 132,
		Russian: 40,
		Sammarinese: 1,
		Slovene: 21,
		Spanish: 466,
		Syrian: 171,
		Tunisian: 113,
		Turkish: 808,
		Ukrainian: 105,
		Vatican: 1,
	}],

	/* I need reweighting */
	["Realism Global", {
		Afghan: 5090,
		Albanian: 289,
		Algerian: 5107,
		American: 36503,
		Andorran: 7,
		Angolan: 5166,
		Antiguan: 11,
		Argentinian: 5102,
		Armenian: 286,
		Aruban: 10,
		Australian: 2952,
		Austrian: 897,
		Azerbaijani: 1086,
		Bahamian: 45,
		Bahraini: 212,
		Bangladeshi: 19160,
		Barbadian: 28,
		Belarusian: 899,
		Belgian: 1215,
		Belizean: 50,
		Beninese: 1756,
		Bermudian: 5,
		Bhutanese: 94,
		"Bissau-Guinean": 275,
		Bolivian: 1392,
		Bosnian: 333,
		Brazilian: 22920,
		British: 7189,
		Bruneian: 50,
		Bulgarian: 615,
		Burkinabé: 3105,
		Burmese: 6043,
		Burundian: 1797,
		Cambodian: 1972,
		Cameroonian: 3688,
		Canadian: 4188,
		"Cape Verdean": 66,
		Catalan: 767,
		"Central African": 679,
		Chadian: 2431,
		Chilean: 2005,
		Chinese: 143350,
		Colombian: 5405,
		Comorian: 116,
		Congolese: 827,
		"a Cook Islander": 1,
		"Costa Rican": 555,
		Croatian: 379,
		Cuban: 1141,
		Curaçaoan: 17,
		Cypriot: 131,
		Czech: 1039,
		Danish: 611,
		Djiboutian: 118,
		Dominican: 1249,
		Dominiquais: 7,
		Dutch: 1769,
		"East Timorese": 187,
		Ecuadorian: 2055,
		Egyptian: 12826,
		Emirati: 1164,
		Equatoguinean: 210,
		Eritrean: 742,
		Estonian: 122,
		Ethiopian: 15303,
		Fijian: 98,
		Filipina: 13266,
		Finnish: 578,
		French: 6886,
		"French Guianan": 41,
		"French Polynesian": 31,
		Gabonese: 282,
		Gambian: 338,
		Georgian: 366,
		German: 8173,
		Ghanan: 4071,
		Greek: 1062,
		Greenlandic: 5,
		Grenadian: 11,
		Guamanian: 18,
		Guatemalan: 2277,
		Guinean: 1978,
		Guyanese: 83,
		Haitian: 1303,
		Honduran: 1177,
		Hungarian: 900,
		"I-Kiribati": 15,
		Icelandic: 37,
		Indian: 156457,
		Indonesian: 30475,
		Iranian: 9047,
		Iraqi: 5982,
		Irish: 537,
		Israeli: 1062,
		Italian: 5753,
		Ivorian: 3741,
		Jamaican: 290,
		Japanese: 11849,
		Jordanian: 1184,
		Kazakh: 2095,
		Kenyan: 7408,
		Kittitian: 6,
		Korean: 7977,
		Kosovan: 193,
		Kurdish: 7258,
		Kuwaiti: 511,
		Kyrgyz: 730,
		Laotian: 841,
		Latvian: 168,
		Lebanese: 533,
		Liberian: 727,
		Libyan: 760,
		"a Liechtensteiner": 4,
		Lithuanian: 263,
		Luxembourgian: 70,
		Macedonian: 205,
		Malagasy: 3989,
		Malawian: 3010,
		Malaysian: 3838,
		Maldivian: 52,
		Malian: 3098,
		Maltese: 43,
		Marshallese: 5,
		Mauritanian: 676,
		Mauritian: 128,
		Mexican: 15306,
		Micronesian: 12,
		Moldovan: 372,
		Monégasque: 4,
		Mongolian: 369,
		Montenegrin: 61,
		Moroccan: 4240,
		Mosotho: 275,
		Motswana: 297,
		Mozambican: 4824,
		Namibian: 352,
		Nauruan: 1,
		Nepalese: 3456,
		"New Caledonian": 33,
		"a New Zealander": 536,
		"Ni-Vanuatu": 38,
		Nicaraguan: 731,
		Nigerian: 29732,
		Nigerien: 4187,
		Niuean: 1,
		Norwegian: 619,
		Omani: 612,
		Pakistani: 26109,
		Palauan: 2,
		Palestinian: 746,
		Panamanian: 515,
		"Papua New Guinean": 1136,
		Paraguayan: 817,
		Peruvian: 3829,
		Polish: 3569,
		Portuguese: 969,
		"Puerto Rican": 354,
		Qatari: 340,
		Romanian: 1797,
		Russian: 13807,
		Rwandan: 1754,
		Sahrawi: 80,
		"Saint Lucian": 18,
		Salvadoran: 688,
		Sammarinese: 3,
		Samoan: 22,
		"São Toméan": 29,
		Saudi: 4131,
		Scottish: 575,
		Senegalese: 2486,
		Serbian: 814,
		Seychellois: 9,
		"Sierra Leonean": 1056,
		Singaporean: 647,
		Slovak: 530,
		Slovene: 203,
		"a Solomon Islander": 83,
		Somali: 2469,
		"South African": 6688,
		"South Sudanese": 1918,
		Spanish: 4586,
		"Sri Lankan": 2149,
		Sudanese: 6099,
		Surinamese: 63,
		Swazi: 177,
		Swedish: 1094,
		Swiss: 941,
		Syrian: 2888,
		Taiwanese: 2410,
		Tajik: 1202,
		Tanzanian: 9586,
		Thai: 6919,
		Tibetan: 435,
		Togolese: 1166,
		Tongan: 12,
		Trinidadian: 136,
		Tunisian: 1316,
		Turkish: 9091,
		Turkmen: 707,
		Tuvaluan: 1,
		Ugandan: 7338,
		Ukrainian: 3989,
		Uruguayan: 362,
		Uzbek: 3805,
		Vatican: 1,
		Venezuelan: 3826,
		Vietnamese: 10898,
		Vincentian: 11,
		Yemeni: 3996,
		Zairian: 13815,
		Zambian: 2844,
		Zimbabwean: 2355
	}],

	["Japan/Korea weighted sea trade", {
		Chinese: 2000,
		Korean: 2000,
		Japanese: 2000,
		American: 500,
		Russian: 500,
		Filipina: 500,
		Taiwanese: 500,
		Vietnamese: 500,
		Cambodian: 500,
		Malaysian: 500,
		Indonesian: 500,
		Australian: 200,
		"Papua New Guinean": 200,
		"a New Zealander": 200,
		Canadian: 200,
		Mexican: 200,
		Ecuadorian: 200,
		Peruvian: 200,
		Chilean: 200,
		Thai: 200,
		Burmese: 200,
		Bangladeshi: 200,
		Indian: 200,
		"Sri Lankan": 200,
		Iranian: 200,
		Pakistani: 200,
		Omani: 200,
		Yemeni: 200,
		Malagasy: 200,
		Somali: 200,
		"a Solomon Islander": 200,
		"New Caledonian": 200,
		Fijian: 200,
		Mongolian: 200,
		Tibetan: 200,
		Laotian: 200,
		Argentinian: 80,
		Belgian: 80,
		Armenian: 80,
		Austrian: 80,
		Bulgarian: 80,
		British: 80,
		Brazilian: 80,
		Colombian: 80,
		"Costa Rican": 80,
		Croatian: 80,
		Danish: 80,
		Czech: 80,
		Cuban: 80,
		Dutch: 80,
		Egyptian: 80,
		Ethiopian: 80,
		Finnish: 80,
		French: 80,
		Greek: 80,
		German: 80,
		Georgian: 80,
		Hungarian: 80,
		Honduran: 80,
		Iraqi: 80,
		Irish: 80,
		Israeli: 80,
		Italian: 80,
		Kazakh: 80,
		Jordanian: 80,
		Moroccan: 80,
		Nepalese: 80,
		Nicaraguan: 80,
		Norwegian: 80,
		Portuguese: 80,
		Polish: 80,
		Paraguayan: 80,
		Romanian: 80,
		Salvadoran: 80,
		Scottish: 80,
		Serbian: 80,
		Singaporean: 80,
		"South African": 80,
		Swedish: 80,
		Spanish: 80,
		Turkish: 80,
		Tunisian: 80,
		Turkmen: 80,
		Ukrainian: 80,
		Uruguayan: 80,
		Venezuelan: 80,
		Afghan: 12,
		Azerbaijani: 12,
		Bolivian: 12,
		Dominican: 12,
		Emirati: 12,
		Gambian: 12,
		Grenadian: 12,
		Jamaican: 12,
		Latvian: 12,
		Luxembourgian: 12,
		Micronesian: 12,
		Panamanian: 12,
		"Puerto Rican": 12,
		Swiss: 12,
		Zambian: 12,
		Zimbabwean: 12,
		Togolese: 12,
		Syrian: 12,
		Slovene: 12,
		Senegalese: 12,
		Qatari: 12,
		Moldovan: 12,
		Macedonian: 12,
		Lebanese: 12,
		Kosovan: 12,
		Catalan: 12,
		Bosnian: 12,
		Belizean: 12,
		Bahamian: 12,
		Albanian: 12,
		Algerian: 12,
		Bahraini: 12,
		Beninese: 12,
		"Central African": 12,
		Congolese: 12,
		Cypriot: 12,
		Guatemalan: 12,
		Kurdish: 12,
		Liberian: 12,
		Marshallese: 12,
		Mozambican: 12,
		Sudanese: 12,
		Tongan: 12,
		Ugandan: 12,
		Mauritanian: 12,
		Libyan: 12,
		Kuwaiti: 12,
		Icelandic: 12,
		Ghanan: 12,
		Estonian: 12,
		Chadian: 12,
		Bermudian: 12,
		Bhutanese: 12,
		Bruneian: 12,
		Cameroonian: 12,
		Guyanese: 12,
		Kenyan: 12,
		Kyrgyz: 12,
		Montenegrin: 12,
		Nigerian: 12,
		Tanzanian: 12,
		Zairian: 12,
		Uzbek: 12,
		"South Sudanese": 12,
		Saudi: 12,
		Palestinian: 12,
		Nigerien: 12,
		Lithuanian: 12,
		Ivorian: 12,
		Haitian: 12,
		Greenlandic: 12,
		Gabonese: 12,
		Belarusian: 12,
		Angolan: 12,
		"French Polynesian": 12,
		Antiguan: 2,
		Burkinabé: 2,
		"Cape Verdean": 2,
		Malian: 2,
		Mosotho: 2,
		Niuean: 2,
		"Saint Lucian": 2,
		Slovak: 2,
		Vatican: 2,
		Tuvaluan: 2,
		Motswana: 2,
		Maltese: 2,
		Guamanian: 2,
		Equatoguinean: 2,
		Dominiquais: 2,
		Curaçaoan: 2,
		Comorian: 2,
		Burundian: 2,
		Eritrean: 2,
		"I-Kiribati": 2,
		Monégasque: 2,
		"Ni-Vanuatu": 2,
		Sammarinese: 2,
		Vincentian: 2,
		Trinidadian: 2,
		Tajik: 2,
		Surinamese: 2,
		Seychellois: 2,
		Samoan: 2,
		Namibian: 2,
		Malawian: 2,
		Guinean: 2,
		"French Guianan": 2,
		"East Timorese": 2,
		"a Cook Islander": 2,
		Aruban: 2,
		Andorran: 2,
		Barbadian: 2,
		"a Liechtensteiner": 2,
		Mauritian: 2,
		Nauruan: 2,
		Palauan: 2,
		Rwandan: 2,
		"Sierra Leonean": 2,
		"São Toméan": 2,
		Swazi: 2,
		Sahrawi: 2,
		Maldivian: 2,
		Kittitian: 2,
		Djiboutian: 2
	}],

	["Polish-Lithuanian Commonwealth with Silk Road", {
		Polish: 2000,
		Ukrainian: 2000,
		Belarusian: 2000,
		Chinese: 500,
		Japanese: 500,
		Korean: 500,
		Russian: 500,
		German: 500,
		French: 500,
		Italian: 500,
		Turkish: 500,
		Uzbek: 200,
		Kazakh: 200,
		Indian: 200,
		Pakistani: 200,
		Afghan: 200,
		Iranian: 200,
		Iraqi: 200,
		Syrian: 200,
		Egyptian: 200,
		Saudi: 200,
		Austrian: 200,
		British: 200,
		Bulgarian: 200,
		Croatian: 200,
		Czech: 200,
		Danish: 200,
		Dutch: 200,
		Estonian: 200,
		Finnish: 80,
		Greek: 200,
		Hungarian: 200,
		Latvian: 200,
		Lithuanian: 200,
		Macedonian: 80,
		Moldovan: 80,
		Romanian: 200,
		Spanish: 200,
		Swedish: 200,
		Nigerian: 80,
		Ethiopian: 80,
		Congolese: 80,
		Moroccan: 80,
		Algerian: 80,
		Tunisian: 80,
		Libyan: 80,
		Sudanese: 80,
		Bangladeshi: 80,
		Burmese: 80,
		Filipina: 80,
		Indonesian: 80,
		Kyrgyz: 80,
		Malaysian: 80,
		Nepalese: 80,
		Taiwanese: 80,
		Tajik: 80,
		Thai: 80,
		Tibetan: 80,
		Turkmen: 80,
		Vietnamese: 80,
		Australian: 80,
		Albanian: 80,
		Belgian: 80,
		Bosnian: 80,
		Irish: 80,
		Swiss: 80,
		Scottish: 80,
		Norwegian: 80,
		Serbian: 80,
		Slovak: 80,
		Portuguese: 80,
		Slovene: 80,
		Kosovan: 80,
		American: 80,
		Canadian: 80,
		Cuban: 80,
		Mexican: 80,
		Brazilian: 80,
		Colombian: 80,
		Argentinian: 80,
		Peruvian: 80,
		Venezuelan: 80,
		Catalan: 12,
		Georgian: 80,
		Armenian: 80,
		Yemeni: 80,
		Jordanian: 80,
		Emirati: 12,
		Mongolian: 12,
		Bolivian: 12,
		Paraguayan: 12,
		Chilean: 12,
		Uruguayan: 12,
		Ecuadorian: 12,
		Salvadoran: 12,
		Jamaican: 12,
		Dominiquais: 12,
		"Costa Rican": 12,
		Guatemalan: 12,
		Nicaraguan: 12,
		Haitian: 12,
		Dominican: 12,
		Honduran: 12,
		Lebanese: 12,
		Kuwaiti: 12,
		Kurdish: 12,
		Azerbaijani: 12,
		Bahraini: 12,
		Omani: 12,
		Palestinian: 12,
		Israeli: 12,
		Cypriot: 12,
		Qatari: 12,
		Icelandic: 12,
		Montenegrin: 12,
		Luxembourgian: 12,
		"Papua New Guinean": 12,
		Bhutanese: 12,
		Bruneian: 12,
		Laotian: 12,
		Singaporean: 12,
		Maldivian: 12,
		Cambodian: 12,
		"East Timorese": 12,
		Cameroonian: 12,
		Mosotho: 12,
		Ghanan: 12,
		Rwandan: 12,
		Malagasy: 12,
		Somali: 12,
		"South African": 12,
		Malawian: 12,
		Guinean: 12,
		Equatoguinean: 12,
		Angolan: 12,
		Beninese: 12,
		"Central African": 12,
		Eritrean: 12,
		Ivorian: 12,
		Mozambican: 12,
		"South Sudanese": 12,
		Ugandan: 12,
		Zairian: 12,
		Senegalese: 12,
		Namibian: 12,
		Mauritanian: 12,
		Kenyan: 12,
		Chadian: 12,
		Gabonese: 12,
		Liberian: 12,
		Mauritian: 12,
		Swazi: 12,
		Zambian: 12,
		Zimbabwean: 12,
		Tanzanian: 12,
		"Sierra Leonean": 12,
		Nigerien: 12,
		Gambian: 12,
		Burundian: 12,
		Guyanese: 2,
		"French Guianan": 2,
		Surinamese: 12,
		Greenlandic: 12,
		Bahamian: 2,
		Djiboutian: 2,
		Togolese: 2,
		Sahrawi: 2,
		Motswana: 2,
		"Cape Verdean": 2,
		Malian: 2,
		"São Toméan": 2,
		"Bissau-Guinean": 2,
		Burkinabé: 2,
		Comorian: 2,
		Seychellois: 2,
		Marshallese: 2,
		Niuean: 2,
		Tuvaluan: 2,
		Palauan: 2,
		Micronesian: 2,
		"a Cook Islander": 2,
		Fijian: 2,
		Nauruan: 2,
		Samoan: 2,
		"New Caledonian": 2,
		"French Polynesian": 2,
		Guamanian: 2,
		"a New Zealander": 2,
		"a Solomon Islander": 2,
		Tongan: 2,
		"Ni-Vanuatu": 2,
		"I-Kiribati": 2,
		"a Liechtensteiner": 2,
		Monégasque: 2,
		Sammarinese: 2,
		Andorran: 2,
		Maltese: 2,
		Vatican: 2,
		Bermudian: 2,
		"Saint Lucian": 2,
		Kittitian: 2,
		Antiguan: 2,
		Aruban: 2,
		Grenadian: 2,
		Trinidadian: 2,
		Vincentian: 2,
		Panamanian: 2,
		Curaçaoan: 2,
		Barbadian: 2,
		Belizean: 2,
		"Puerto Rican": 2,
		"Sri Lankan": 2
	}],

	["Caribbean and Gulf of Mexico", {
		Mexican: 1100,
		American: 1100,
		Guatemalan: 520,
		Haitian: 520,
		Cuban: 520,
		Dominican: 520,
		Honduran: 520,
		Salvadoran: 520,
		Nicaraguan: 520,
		"Costa Rican": 520,
		Panamanian: 520,
		"Puerto Rican": 520,
		Jamaican: 520,
		Trinidadian: 520,
		Colombian: 520,
		Venezuelan: 520,
		Brazilian: 520,
		Belizean: 200,
		Bahamian: 200,
		Barbadian: 200,
		Guyanese: 200,
		Surinamese: 200,
		"French Guianan": 200,
		Ecuadorian: 200,
		Peruvian: 200,
		Chilean: 200,
		Argentinian: 200,
		Uruguayan: 200,
		Paraguayan: 200,
		Bolivian: 200,
		Portuguese: 200,
		Spanish: 200,
		French: 200,
		British: 200,
		Irish: 200,
		Scottish: 200,
		Dutch: 200,
		Japanese: 200,
		Filipina: 200,
		Indonesian: 200,
		Algerian: 80,
		"Saint Lucian": 80,
		Grenadian: 80,
		Vincentian: 80,
		Antiguan: 80,
		Dominiquais: 80,
		Australian: 80,
		Bangladeshi: 80,
		Belgian: 80,
		Bermudian: 80,
		Curaçaoan: 80,
		Greenlandic: 80,
		German: 80,
		Indian: 80,
		Italian: 80,
		Korean: 80,
		Malaysian: 80,
		Nigerian: 80,
		Norwegian: 80,
		"Papua New Guinean": 80,
		Russian: 80,
		Swedish: 80,
		Turkish: 80,
		Taiwanese: 80,
		Chinese: 80,
		Canadian: 80,
		Iranian: 80,
		Kittitian: 80,
		Ivorian: 80,
		Moroccan: 80,
		Nigerien: 80,
		"French Polynesian": 80,
		Danish: 80,
		Bruneian: 80,
		Aruban: 80,
		Icelandic: 80,
		Vietnamese: 80,
		Samoan: 80,
		Congolese: 80,
		"a Solomon Islander": 80,
		"a New Zealander": 80,
		Catalan: 80,
		Tunisian: 80,
		"Sri Lankan": 80,
		"South African": 80,
		Singaporean: 80,
		Malagasy: 80,
		Thai: 80,
		Egyptian: 80,
		"Cape Verdean": 80,
		Angolan: 14,
		Afghan: 14,
		Azerbaijani: 14,
		Belarusian: 14,
		Libyan: 14,
		Austrian: 14,
		Bahraini: 14,
		Armenian: 14,
		Burmese: 14,
		Cambodian: 14,
		Cameroonian: 14,
		"Central African": 14,
		Cypriot: 14,
		Czech: 14,
		"a Cook Islander": 14,
		Croatian: 14,
		Fijian: 14,
		Ethiopian: 14,
		Estonian: 14,
		Georgian: 14,
		Ghanan: 14,
		Greek: 14,
		Hungarian: 14,
		Iraqi: 14,
		Israeli: 14,
		Kazakh: 14,
		Kuwaiti: 14,
		Maldivian: 14,
		Malawian: 14,
		Liberian: 14,
		Lebanese: 14,
		Marshallese: 14,
		Mauritian: 14,
		Namibian: 14,
		Mozambican: 14,
		Micronesian: 14,
		"New Caledonian": 14,
		Pakistani: 14,
		Omani: 14,
		Romanian: 14,
		Sammarinese: 14,
		Qatari: 14,
		Senegalese: 14,
		Somali: 14,
		Sudanese: 14,
		Syrian: 14,
		Swiss: 14,
		Turkmen: 14,
		Ukrainian: 14,
		Yemeni: 14,
		Zairian: 5,
		Albanian: 5,
		Bosnian: 5,
		Beninese: 5,
		Burundian: 5,
		Bulgarian: 5,
		Chadian: 5,
		Comorian: 5,
		Emirati: 5,
		Equatoguinean: 5,
		Finnish: 5,
		"East Timorese": 5,
		Gabonese: 5,
		Djiboutian: 5,
		Guinean: 5,
		Eritrean: 5,
		Gambian: 5,
		Guamanian: 5,
		Kurdish: 5,
		Jordanian: 5,
		Kenyan: 5,
		Kyrgyz: 5,
		Lithuanian: 5,
		Macedonian: 5,
		Latvian: 5,
		Malian: 5,
		Mosotho: 5,
		Motswana: 5,
		Moldovan: 5,
		Mongolian: 5,
		Mauritanian: 5,
		Nepalese: 5,
		Palestinian: 5,
		Polish: 5,
		Saudi: 5,
		"South Sudanese": 5,
		Uzbek: 5,
		"Sierra Leonean": 5,
		Seychellois: 5,
		Tajik: 5,
		Serbian: 5,
		Tongan: 5,
		Ugandan: 5,
		Zimbabwean: 5,
		Tuvaluan: 5,
		Togolese: 5,
		Luxembourgian: 5,
		Tibetan: 5,
		Zambian: 5,
		Tanzanian: 5,
		Kosovan: 3,
		Slovak: 3,
		Swazi: 3,
		Laotian: 3,
		Rwandan: 3,
		"Bissau-Guinean": 3,
		Burkinabé: 2,
		Niuean: 2,
		Vatican: 2,
		Slovene: 2,
		Maltese: 2,
		"I-Kiribati": 2,
		Monégasque: 2,
		"Ni-Vanuatu": 2,
		Andorran: 2,
		Bhutanese: 2,
		"a Liechtensteiner": 2,
		Montenegrin: 2,
		Nauruan: 2,
		Palauan: 2,
		"São Toméan": 2,
		Sahrawi: 2
	}],

]);


code:
/**
 * @typedef startingGirlsOptionsPreset
 * @property {string} name
 * @property {number} value
 * @property {number} max
 * @property {0|1} [extreme] undefined: ignore settings, 0: only when V.seeExtreme === 0, 1: only when V.seeExtreme === 1
 * @property {"on"|"off"} [style]
 */

/**
 * @typedef startingGirlsOptionsNonNumericPreset
 * @property {string} name
 * @property {FC.GenderGenes} value
 * @property {0|1} [extreme] undefined: ignore settings, 0: only when V.seeExtreme === 0, 1: only when V.seeExtreme === 1
 * @property {"on"|"off"} [style]
 */

/**
 * @type {{[key: string]: Array<startingGirlsOptionsPreset>}}
 */
App.Data.StartingGirls = {
	prestige: [
		{name: "None", value: 0, max: 0.5},
		{name: "Locally known", value: 1, max: 1.5},
		{name: "Regionally famous", value: 2, max: 2.5},
		{name: "World renowned", value: 3, max: 8}
	],
	weight: [
		{name: "Emaciated", value: -100, max: -95},
		{name: "Skinny", value: -50, max: -30},
		{name: "Thin", value: -20, max: -10},
		{name: "Average", value: 0, max: 10},
		{name: "Plush", value: 20, max: 30},
		{name: "Chubby", value: 50, max: 95},
		{name: "Fat", value: 100, max: 130},
		{name: "Obese", value: 140, max: 160},
		{name: "Super obese", value: 180, max: 190},
		{name: "Dangerously obese", value: 200, max: 999}
	],
	muscles: [
		{name: "Frail", value: -100, max: -95},
		{name: "Very weak", value: -66, max: -50},
		{name: "Weak", value: -41, max: -5},
		{name: "Normal", value: 0, max: 5},
		{name: "Toned", value: 20, max: 30},
		{name: "Well built", value: 40, max: 50},
		{name: "Quite muscular", value: 65, max: 95},
		{name: "Ripped", value: 100, max: 999}
	],
	waist: [
		{name: "Absurd", value: -100, max: -95},
		{name: "Hourglass", value: -55, max: -40},
		{name: "Feminine", value: -25, max: -15},
		{name: "Average", value: 0, max: 10},
		{name: "Unattractive", value: 15, max: 40},
		{name: "Ugly", value: 55, max: 95},
		{name: "Masculine", value: 100, max: 999}
	],
	face: [
		{name: "Very Ugly", value: -100, max: -95},
		{name: "Ugly", value: -55, max: -40},
		{name: "Unattractive", value: -15, max: -10},
		{name: "Average", value: 0, max: 10},
		{name: "Attractive", value: 15, max: 40},
		{name: "Beautiful", value: 55, max: 95},
		{name: "Very beautiful", value: 100, max: 999}
	],
	lips: [
		{name: "Thin", value: 5, max: 10},
		{name: "Normal", value: 15, max: 20},
		{name: "Pretty", value: 25, max: 40},
		{name: "Plush", value: 55, max: 70},
		{
			name: "Huge", value: 85, max: 95, extreme: 1
		},
		{
			name: "Facepussy", value: 100, max: 999, extreme: 1
		},
		{
			name: "Huge", value: 85, max: 999, extreme: 0
		}
	],
	intelligence: [
		{name: "Moronic", value: -100, max: -95},
		{name: "Very stupid", value: -60, max: -50},
		{name: "Stupid", value: -30, max: -15},
		{name: "Average", value: 0, max: 15},
		{name: "Smart", value: 30, max: 50},
		{name: "Very smart", value: 60, max: 95},
		{name: "Brilliant", value: 100, max: 999}
	],
	fetishStrength: [
		{name: "Very Low", value: 15, max: 30},
		{name: "Low", value: 45, max: 60},
		{name: "Normal", value: 75, max: 85},
		{name: "High", value: 90, max: 95},
		{name: "Extremely High", value: 100, max: 999}
	],
	attr: [
		{
			name: "Disgusted", value: 0, max: 5, style: "off"
		},
		{
			name: "Turned off", value: 10, max: 15, style: "off"
		},
		{
			name: "Not attracted", value: 25, max: 35, style: "off"
		},
		{name: "Indifferent", value: 50, max: 65},
		{
			name: "Attracted", value: 75, max: 85, style: "on"
		},
		{
			name: "Aroused", value: 90, max: 95, style: "on"
		},
		{
			name: "Passionate", value: 100, max: 999, style: "on"
		}
	],
	energy: [
		{
			name: "Frigid", value: 5, max: 10, style: "off"
		},
		{
			name: "Poor", value: 25, max: 40, style: "off"
		},
		{name: "Average", value: 45, max: 60},
		{
			name: "Powerful", value: 65, max: 80, style: "on"
		},
		{
			name: "Sex addict", value: 85, max: 99, style: "on"
		},
		{
			name: "Nympho", value: 100, max: 999, style: "on"
		}
	],
	skill: [
		{name: "Unskilled", value: 0, max: 10},
		{name: "Basic", value: 15, max: 30},
		{name: "Skilled", value: 35, max: 60},
		{name: "Expert", value: 65, max: 99},
		{name: "Masterful", value: 100, max: 999},
	],
	devotion: [
		{name: "Utterly hateful", value: -100, max: -95},
		{name: "Hateful", value: -70, max: -50},
		{name: "Resistant", value: -35, max: -20},
		{name: "Ambivalent", value: 0, max: 20},
		{name: "Accepting", value: 35, max: 50},
		{name: "Devoted", value: 70, max: 95},
		{name: "Worshipful", value: 100, max: 999}
	],
	trust: [
		{name: "Abjectly terrified", value: -100, max: -95},
		{name: "Terrified", value: -70, max: -50},
		{name: "Frightened", value: -35, max: -20},
		{name: "Fearful", value: 0, max: 20},
		{name: "Careful", value: 35, max: 50},
		{name: "Trusting", value: 70, max: 95},
		{name: "Absolute trust", value: 100, max: 999}
	]
};
/**
 * @type {{[key: string]: Array<startingGirlsOptionsNonNumericPreset>}}
 */
App.Data.StartingGirlsNonNumericPresets = {
	genes: [
		{name: "XX (Female)", value: "XX"},
		{name: "XY (Male)", value: "XY"}
	]
};

repo_name: Nithhogg/fc

license: mit


code:
App.Data.Art.legacySVGFilters = `<svg class="defs-only" style="width: 0; height: 0; position: absolute;">
	<filter id="skin-black" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.15 0 0 0 0 0 0.1 0 0 0 0 0 0.05 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-bronzed" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0.1 0 0 0 0.6 0 0 0 0 0 0.3 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-brown" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0 0 0.5 0 0 0 0 0 0.15 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-dark" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.5 0 0 0 0 0 0.2 0 0 0 0 0 0.1 0 0 0 0 0 1 0 "/>
	</filter>
	<filter id="skin-ebony" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.1 0 0 0 0 0 0.05 0 0 0 0 0 0.05 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-extremely-pale" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.05 0 0 0 0 0 1.05 0 0 0 0 0 1.1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-fair" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0.2 0 0.8 0 0 0.2 0 0 0.8 0 0.2 0 0 0 1 0"/>
	</filter>
	<filter id="skin-light" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.5 0 0 0 0.5 0 0.5 0 0 0.5 0 0 0.5 0 0.5 0 0 0 1 0"/>
	</filter>
	<filter id="skin-light-brown" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0 0 0.7 0 0 0 0 0 0.5 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-olive" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.9 0 0 0 0 0 0.8 0 0 0 0 0 0.6 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-pale" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.6 0 0 0 0.4 0 0.6 0 0 0.4 0 0 1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-tanned" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0 0 0.65 0 0 0 0 0 0.5 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-white" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-dyed-red" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.6 0 0 0 0.4 0 0.25 0 0 0.2 0 0 0 0 0.3 0 0 0 1 0 "/>
	</filter>
	<filter id="skin-dyed-green" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.7 0 0 0 0 0.2 0.65 0.15 0 0 0 0 0.5 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="skin-dyed-blue" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.3 0 0 0 0 0 0.5 0 0 0 0.4 0.2 0.25 0 0.1 0 0 0 1 0"/>
	</filter>

	<filter id="hair-ashen" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.7 0 0 0 0.1 0 0.7 0 0 0.1 0 0 0.7 0 0.1 0 0 0 1 0"/>
	</filter>
	<filter id="hair-auburn" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0.2 0 0.5 0 0 0 0 0 0.15 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-black" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.2 0 0 0 0 0 0.15 0 0 0 0 0 0.1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-blazing-red" color-interpolation-filters="sRGB">
		<feComponentTransfer>
			<feFuncR type="gamma" exponent="0.10" />
			<feFuncG type="gamma" exponent="3.00" />
			<feFuncB type="gamma" exponent="7.50" />
		</feComponentTransfer>
	</filter>
	<filter id="hair-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.4 0 0 0 0 0 1.3 0 0 0 0 0 0.7 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-blue" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.3 0 0 0 0 0 0.5 0 0 0 0.4 0.2 0.25 0 0.1 0 0 0 1 0"/>
	</filter>
	<filter id="hair-bright-red" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1 0 0 0 0.2 0 0.4 0 0 0 0 0 0.1 0 0.1 0 0 0 1 0"/>
	</filter>
	<filter id="hair-brown" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0 0 0.5 0 0 0 0 0 0.15 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-brunette" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0 0 0.5 0 0 0 0 0 0.15 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-burgundy" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.45 0 0 0 0.25 0 0.2 0 0 0.1 0 0 0.3 0 0.2 0 0 0 1 0"/>
	</filter>
	<filter id="hair-chestnut" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.9 0 0 0 0.2 0 0.7 0 0 0 0 0 0.1 0 0.1 0 0 0 1 0"/>
	</filter>
	<filter id="hair-chocolate-brown" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.6 0 0 0 0 0 0.2 0 0 0 0 0 0.1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-copper" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.5 0 0 0 -0.2 0 0.8 0 0 -0.2 0 0 0.1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-dark" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.5 0 0 0 0 0 0.2 0 0 0 0 0 0.1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-dark-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.1 0 0 0 0 0 0.9 0 0 0 0 0 0.4 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-dark-brown" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.6 0 0 0 0 0 0.35 0 0 0 0 0 0.1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-dark-red" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.7 0 0 0 0.2 0 0.15 0 0 0.1 0 0 0 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-deep-red" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.5 0 0 0 -0.3 0 0.3 0 0 -0.1 0 0 0 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-dirty-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0.3 0 0.7 0 0 0.3 0 0 0.2 0 0.5 0 0 0 1 0"/>
	</filter>
	<filter id="hair-fair-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1 0 0 0 0.5 0 0.8 0 0 0.4 0 0 0.6 0 0.2 0 0 0 1 0"/>
	</filter>
	<filter id="hair-ginger" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.2 0 0 0 0.1 0 0.9 0 0 0 0 0 0.1 0 0.3 0 0 0 1 0"/>
	</filter>
	<filter id="hair-golden" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.7 0 0 0 -0.1 0 1.5 0 0 -0.1 0 0 0.1 0 0.3 0 0 0 1 0"/>
	</filter>
	<filter id="hair-golden-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.8 0 0 0 -0.3 0 1.6 0 0 -0.4 0 0 0.3 0 0.1 0 0 0 1 0"/>
	</filter>
	<filter id="hair-green" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.7 0 0 0 0 0.2 0.65 0.15 0 0 0 0 0.5 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-gray" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-grey" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-hazel" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0.2 0 0.5 0 0 0.2 0 0 0.15 0 0.15 0 0 0 1 0"/>
	</filter>
	<filter id="hair-light-brown" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0 0 0.7 0 0 0 0 0 0.5 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-light-purple" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.7 0 0 0 0.4 0 0.1 0 0 0.2 0 0 0.7 0 0.3 0 0 0 1 0"/>
	</filter>
	<filter id="hair-neon-blue" color-interpolation-filters="sRGB">
		<feComponentTransfer>
			<feFuncR type="gamma" exponent="3.75" />
			<feFuncG type="gamma" exponent="0.75" />
			<feFuncB type="gamma" exponent="0.25" />
		</feComponentTransfer>
	</filter>
	<filter id="hair-neon-green" color-interpolation-filters="sRGB">
		<feComponentTransfer>
			<feFuncR type="gamma" exponent="3.00" />
			<feFuncG type="gamma" exponent="0.25" />
			<feFuncB type="gamma" exponent="0.98" />
		</feComponentTransfer>
	</filter>
	<filter id="hair-neon-pink" color-interpolation-filters="sRGB">
		<feComponentTransfer>
			<feFuncR type="gamma" exponent="0.10" />
			<feFuncG type="gamma" exponent="1.00" />
			<feFuncB type="gamma" exponent="0.50" />
		</feComponentTransfer>
	</filter>
	<filter id="hair-olive" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.7 0 0 0 0 0 0.6 0 0 0 0 0 0.15 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-onyx-black" color-interpolation-filters="sRGB">
		<feComponentTransfer>
			<feFuncR type="gamma" exponent="5.50" />
			<feFuncG type="gamma" exponent="6.00" />
			<feFuncB type="gamma" exponent="7.00" />
		</feComponentTransfer>
	</filter>
	<filter id="hair-pale-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.45 0 0 0 0.7 0 0.4 0 0 0.7 0 0 0.6 0 0.4 0 0 0 1 0"/>
	</filter>
	<filter id="hair-peachy" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.45 0 0 0 0 0 1.4 -0.1 0 -0.1 0 0 0.85 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-pink" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.5 0 0 0 0 0 1.5 -0.2 0 -0.2 0 0 1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-platinum-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.6 0 0 0 0.4 0 0.6 0 0 0.4 0 0 1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-purple" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.9 0 0 0 0 0 0.2 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-raven-black" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.2 0 0 0 0 0 0.2 0 0 0 0 0 0.3 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-red" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.6 0 0 0 0.4 0 0.25 0 0 0.2 0 0 0 0 0.3 0 0 0 1 0"/>
	</filter>
	<filter id="hair-red-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1 0 0 0 0.25 0 0.8 0 0 0.1 0 0 0.35 0 0.15 0 0 0 1 0"/>
	</filter>
	<filter id="hair-silver" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.8 0 0 0 0.3 0 0.8 0 0 0.3 0 0 0.8 0 0.3 0 0 0 1 0"/>
	</filter>
	<filter id="hair-strawberry-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="1.2 0 0 0 0.2 0 1.2 0 0 0 0 0 0.7 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-violet" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="2 -1 0 0 0 0 2 -1 0 0 -1 0 2 0 0 0 0 0 1 0"/>
	</filter>
	<filter id="hair-white" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.6 0 0 0 0.5 0 0.6 0 0 0.5 0 0 0.6 0 0.5 0 0 0 1 0"/>
	</filter>
	<filter id="hair-white-blonde" color-interpolation-filters="sRGB">
		<feColorMatrix type="matrix" values="0.7 0 0 0 0.5 0 0.7 0 0 0.5 0 0 0.5 0 0.4 0 0 0 1 0"/>
	</filter>
</svg>`;


code:
App.Data.Weather = {

	hotNice: [
		{name: "Sunny", severity: 1},
		{name: "Warm", severity: 1},
		{name: "Partly Cloudy", severity: 1},
		{name: "Light Rain", severity: 1},
		{name: "Clear and Calm", severity: 1},
	],

	windyNice: [
		{name: "Light Wind", severity: 1},
		{name: "Cloudy", severity: 1},
		{name: "Partly Cloudy", severity: 1},
		{name: "Overcast", severity: 1},
		{name: "Light Rain", severity: 1},
		{name: "Heavy Rain", severity: 1},
		{name: "Clear and Calm", severity: 1},
	],

	smokyNice: [
		{name: "Smoke Warning", severity: 1},
		{name: "Overcast", severity: 1},
		{name: "Clear and Calm", severity: 1},
	],

	toxicNice: [
		{name: "Cloudy", severity: 1},
		{name: "Light Rain", severity: 1},
		{name: "Heavy Rain", severity: 1},
		{name: "Sunny", severity: 1},
		{name: "Clear and Calm", severity: 1},
	],

	coldNice: [
		{name: "Overcast", severity: 1},
		{name: "Chilly", severity: 1},
		{name: "Heavy Rain", severity: 1},
		{name: "Gentle Snow", severity: 1},
		{name: "Clear and Calm", severity: 1},
	],

	tectonicNice: [
		{name: "Light Rain", severity: 1},
		{name: "Partly Cloudy", severity: 1},
		{name: "Heavy Rain", severity: 1},
		{name: "Sunny", severity: 1},
		{name: "Clear and Calm", severity: 1},
	],

	hotLight: [
		{name: "Light Sandstorms", severity: 2},
		{name: "High Heat", severity: 2},
		{name: "Minor Wildfires", severity: 2},
		{name: "Volcano Warning", severity: 2},
	],

	windyLight: [
		{name: "High Winds", severity: 2},
		{name: "T-Storm Warning", severity: 2},
		{name: "Light T-Storms", severity: 2},
		{name: "Flood Warning", severity: 2},
		{name: "Tornado Warning", severity: 2},
	],

	smokyLight: [
		{name: "Smoky", severity: 2},
		{name: "Ash Storm Warning", severity: 2},
		{name: "T-Storm Warning", severity: 2},
		{name: "Light T-Storms", severity: 2},
	],

	toxicLight: [
		{name: "Acid Rain", severity: 2},
		{name: "Radiation Warning", severity: 2},
		{name: "T-Storm Warning", severity: 2},
		{name: "Light T-Storms", severity: 2},
	],

	coldLight: [
		{name: "Subzero Temps", severity: 2},
		{name: "Light Snowstorms", severity: 2},
		{name: "Blizzard Warning", severity: 2},
	],

	tectonicLight: [
		{name: "T-Storm Warning", severity: 2},
		{name: "Light T-Storms", severity: 2},
		{name: "Light Mudslides", severity: 2},
		{name: "Earthquake Warning", severity: 2},
	],

	hotHeavy: [
		{name: "Severe Sandstorm", severity: 3},
		{name: "Extreme Heat", severity: 3},
		{name: "Serious Wildfires", severity: 3},
		{name: "Volcanic Rumbling", severity: 3},
	],

	windyHeavy: [
		{name: "Extreme Winds", severity: 3},
		{name: "Flooding", severity: 3},
		{name: "Tornadoes", severity: 3},
		{name: "Extreme T-storms", severity: 3},
	],

	smokyHeavy: [
		{name: "Dense Smoke", severity: 3},
		{name: "Ash Storms", severity: 3},
		{name: "Extreme T-storms", severity: 3},
	],

	toxicHeavy: [
		{name: "Heavy Acid Rain", severity: 3},
		{name: "Extreme T-storms", severity: 3},
		{name: "Radiological Drift", severity: 3},
	],

	coldHeavy: [
		{name: "Severe Snowstorm", severity: 3},
		{name: "Blizzard", severity: 3},
		{name: "Freezing Temps", severity: 3},
	],

	tectonicHeavy: [
		{name: "Violent Mudslides", severity: 3},
		{name: "Rumbling Earthquake", severity: 3},
		{name: "Extreme T-Storms", severity: 3},
	],

	hotExtreme: [
		{name: "Solar Flare", severity: 4},
		{name: "Devastating Sandstorm", severity: 4},
		{name: "Volcanic Eruption", severity: 4},
	],

	windyExtreme: [
		{name: "Ion Storm", severity: 4},
		{name: "Cataclysmic Rains", severity: 4},
		{name: "Cat 6 Hurricane", severity: 4},
	],

	smokyExtreme: [
		{name: "Cataclysmic Rains", severity: 4},
		{name: "Suffocating Ash Storms", severity: 4},
	],

	toxicExtreme: [
		{name: "Radiological Storm", severity: 4},
		{name: "Cataclysmic Acid Rain", severity: 4},
	],

	coldExtreme: [
		{name: "Polar Vortex", severity: 4},
		{name: "Apocalyptic Blizzard", severity: 4},
		{name: "Arctic Temps", severity: 4},
	],

	tectonicExtreme: [
		{name: "Hellish Earthquake", severity: 4},
		{name: "Ion Storm", severity: 4},
		{name: "Cataclysmic Rains", severity: 4},
	],

};


code:
App.Medicine.Surgery.SimpleReaction = class {
	/**
	 * If a surgery was invasive a slave can die, but high player skill also has a positive effect on the slaves health
	 * @returns {boolean}
	 */
	get invasive() { return true; }

	get removeJob() { return false; }

	get permanentChanges() { return true; }

	/**
	 * @param {FC.SlaveState} slave
	 * @returns {boolean}
	 */
	_hasEmotion(slave) { return slave.fetish !== Fetish.MINDBROKEN && slave.fuckdoll === 0; }

	/**
	 * @param {FC.SlaveState} slave
	 * @param {FC.Fetish} fetish
	 * @returns {boolean}
	 * @protected
	 */
	_strongKnownFetish(slave, fetish) {
		return (slave.fetish === fetish && slave.fetishStrength > 60 && slave.fetishKnown === 1);
	}

	/**
	 * @param {FC.SlaveState} slave
	 * @param {Partial<FC.SlaveState>} diff
	 */
	intro(slave, diff) {
		const {he, his, himself} = getPronouns(slave);
		const r = [];

		r.push(`As the remote surgery's long recovery cycle completes, ${slave.slaveName}`);
		if (!hasAnyLegs(slave)) {
			r.push(`is carried`);
		} else if (canWalk(slave)) {
			r.push(`walks`);
		} else {
			r.push(`is escorted`);
		}
		r.push(`out of the surgery room`);
		if (canSee(slave)) {
			r.push(`and catches sight of ${himself} in the floor-length mirror outside the door.`);
		} else {
			r.push(`and is detailed the modifications done to ${his} body, assuming ${he} hasn't already realized them.`);
		}

		return r;
	}

	/**
	 * @typedef {object} reactionResult
	 * @property {Array<Array<string|HTMLElement>>} longReaction can contain HTML, every array is a new paragraph. Shown when manually applied.
	 * @property {Array<string|HTMLElement>} shortReaction can contain HTML. May be empty. Intended for use with RA.
	 * @property {number} devotion 0 means no change
	 * @property {number} trust 0 means no change
	 */

	/**
	 * @returns {reactionResult}
	 * @protected
	 */
	_createReactionResult() {
		return {
			longReaction: [],
			shortReaction: [],
			devotion: 0,
			trust: 0,
		};
	}

	/**
	 * @param {FC.SlaveState} slave
	 * @param {Partial<FC.SlaveState>} diff
	 * @returns {reactionResult}
	 */
	reaction(slave, diff) {
		return this._createReactionResult();
	}

	/**
	 * @param {FC.SlaveState} slave
	 * @param {Partial<FC.SlaveState>} diff
	 * @param {reactionResult} previousReaction
	 * @returns {reactionResult}
	 */
	outro(slave, diff, previousReaction) {
		const reaction = this._createReactionResult();
		if (V.PC.skill.medicine < 100) {
			return reaction;
		}

		const {He, he, his, him, himself} = getPronouns(slave);
		const r = [];
		if (this.invasive) {
			r.push(`Since you performed the surgery yourself, and you do an artist's work, ${his} health is less affected by the surgery than it would have been if you'd paid some hack to do it remotely.`);
			improveCondition(slave, 5);
		}
		if (this._hasEmotion(slave)) {
			r.push(`${He} spent the surgery very aware that you were performing it personally.`);
			if (reaction.devotion < 0) {
				if (slave.devotion > 50) {
					r.push(`Though ${he} is unhappy with the results, ${he} consoles ${himself} with the knowledge that you cared enough to do it personally.`);
				} else if (slave.devotion >= -20) {
					r.push(`${He} is <span class="gold">even more afraid</span> of you afterward than ${he} would otherwise be. You must seem a cruel and near-omnipotent power to ${him}.`);
					reaction.trust -= 5;
				} else {
					r.push(`${He} is <span class="mediumorchid">even more hateful</span> of you afterward than ${he} would otherwise be. It must seem to ${him} that ${he}'s nothing more than a test subject to you.`);
					reaction.devotion -= 5;
				}
			} else if (this.permanentChanges) {
				if (slave.devotion > 50) {
					r.push(`Since ${he}'s happy with the results, ${he}'s almost beside ${himself} with <span class="hotpink">gratitude,</span> and filled with <span class="mediumaquamarine">admiration</span> of your skill.`);
					reaction.devotion += 4;
					reaction.trust += 4;
				} else if (slave.devotion >= -20) {
					r.push(`${He} is quite struck by how you performed the surgery personally. ${He} admires your refusal to be one of the idle rich, and <span class="hotpink">likes you more.</span>`);
					reaction.devotion += 5;
				} else {
					r.push(`${He} knows that ${he} should be grateful to you for performing the surgery personally, but the emotional turmoil of the occasion is too much for ${him}, and the realization makes little lasting impact.`);
				}
			}
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

/**
 * @type {Map<string, App.Medicine.Surgery.Reaction>}
 */
App.Medicine.Surgery.ReactionsMap = new Map();

App.Medicine.Surgery.Reaction = class extends App.Medicine.Surgery.SimpleReaction {
	constructor() {
		super();
		// Don't allow overwriting.
		if (App.Medicine.Surgery.ReactionsMap.has(this.key)) {
			throw new Error(`Key "${this.key}" already exists.`);
		}
		App.Medicine.Surgery.ReactionsMap.set(this.key, this);
	}

	// eslint-disable-next-line jsdoc/require-returns-check
	/**
	 * @returns {string}
	 */
	get key() { throw new Error("Method 'key()' must be implemented."); }
};


code:
App.Medicine.Surgery.Procedure = class {
	/**
	 * @param {FC.SlaveState} slave
	 */
	constructor(slave) {
		/**
		 * @type {FC.Util.DiffRecorder<FC.SlaveState>}
		 * @protected
		 */
		this._slave = App.Utils.Diff.getProxy(slave);
	}

	get originalSlave() {
		return this._slave.diffOriginal;
	}

	// eslint-disable-next-line jsdoc/require-returns-check
	/**
	 * @abstract
	 * @returns {string}
	 */
	get name() { throw new Error("Method 'name()' must be implemented."); }

	/**
	 * Lowercase and without punctuation at the end.
	 *
	 * @returns {string}
	 */
	get description() { return ""; }

	/**
	 * Additional note to put at the bottom of the tooltip
	 * @returns {string | DocumentFragment | HTMLElement}
	 */
	get note() { return null; }

	/**
	 * May die, but high player skill also reduces health impact.
	 *
	 * In some cases the damage may be applied outside the procedure system, the slave would still be able to die
	 * however. In these cases, invasive needs to be overwritten.
	 *
	 * @returns {boolean}
	 */
	get invasive() { return this.healthCost > 0; }

	/**
	 * The monetary cost of the surgery.
	 * @returns {number}
	 */
	get cost() {
		return Math.round(this._materialCost + this._workCost);
	}

	/**
	 * Part of the surgery cost related to the required materials
	 * @protected
	 * @returns {number}
	 */
	get _materialCost() {
		return 0;
	}

	/**
	 * Part of the surgery cost related to the work efforts
	 * This is the default cost. Subclasses should base their cost on this value if modifying it.
	 * @example
	 * get _workCost() {
	 *     // Make the surgery 4 times more expensive
	 *     return super._workCost * 4;
	 * }
	 * @protected
	 * @returns {number}
	 */
	get _workCost() {
		// TODO phase out V.surgeryCost and replace with static calculation
		return V.surgeryCost;
	}

	/**
	 * The health impact of a surgery.
	 * @returns {number}
	 */
	get healthCost() { return 0; }

	/**
	 * The value this surgery applies to the relevant slave property. Used by the RA to evaluate the effectiveness of
	 * the surgery. If numeric, it is the delta change, otherwise the result.
	 * @returns {any}
	 */
	get changeValue() { return null; }

	/**
	 * If there are any entries, the procedure cannot be applied. The reasons are the array entries.
	 * When overwriting always include disabled reasons of superclasses!
	 *
	 * @returns {Array<string>} May contain HTML
	 */
	get disabledReasons() {
		const reasons = [];

		/* disallow surgery for slaves on certain assignments */
		if (this.originalSlave.assignment === Job.AGENT ||
			this.originalSlave.assignment === Job.AGENTPARTNER) {
			const {his, him} = getPronouns(this.originalSlave);
			reasons.push(`You must retrieve ${this.originalSlave.slaveName} from ${his} assignment before performing surgery on ${him}.`);
		}

		return reasons;
	}

	// eslint-disable-next-line jsdoc/require-returns-check
	/**
	 * @param {boolean} cheat
	 * @returns {[Partial<FC.SlaveState>, App.Medicine.Surgery.SimpleReaction]}
	 */
	apply(cheat) { throw new Error("Method 'apply()' must be implemented."); }

	/**
	 * Convenience function to prepare the return value for apply()
	 *
	 * @param {App.Medicine.Surgery.SimpleReaction} reaction
	 * @returns {[Partial<FC.SlaveState>, App.Medicine.Surgery.SimpleReaction]}
	 * @protected
	 */
	_assemble(reaction) {
		return [this._slave.diffChange, reaction];
	}
};


code:
// cSpell:ignore URLID

/**
 * @type {string}
 - Status codes map
 - 0: Success
 - 1: Failed to start WebGL engine.
 - 2: Could not find art assets.
 - 3: Version mismatch. Update the assets using the link in Game Options.
 */
App.Art.webglErrorMessage = "";

App.Art.cacheArtData = function() {
	/**
	 * @param {NodeListOf<Element>} imagePassages
	 * @returns {Map<string, Element>}
	 */
	function makeCache(imagePassages) {
		const dict = new Map();

		for (const ip of imagePassages) {
			const name = ip.attributes.getNamedItem("name").value;
			let div = document.createElement("div");
			const svgData = atob(ip.innerHTML.replace(/data:image\/svg\+xml;base64,/, ''));
			div.innerHTML = svgData.trim();
			dict.set(name, div.children.item(0));
		}

		return dict;
	}

	App.Data.Art.Vector = makeCache(document.querySelectorAll('[tags="Twine.image"][name^="Art_Vector"]:not([name^="Art_Vector_Revamp"])'));
	App.Data.Art.VectorRevamp = makeCache(document.querySelectorAll('[tags="Twine.image"][name^="Art_Vector_Revamp"]'));
	App.Data.Art.OtherSVG = makeCache(document.querySelectorAll('[tags="Twine.image"][name="world"]'));
};

App.Art.URLIDMatcher = /url\(#(.*)\)/g;
App.Art.SvgQueue = class {
	/**
	 * @param {{trigger:string, action:string, value:string}[]} transformRules - when a 'data-transform' attribute with value "trigger" is seen on an element, perform 'action' with 'value'
	 * @param {Map<string, Element>} cache
	 * @param {string} displayClass
	 */
	constructor(transformRules, cache, displayClass) {
		this._transformRules = transformRules;
		/** @type {{attrs: NamedNodeMap, nodes: Element[]}[]} */
		this._container = [];
		this._cache = cache;
		this._displayClass = displayClass;
		this._rndID = Math.floor(Math.random() * 9007199254740991);
	}

	/** transform a node via the transform rules
	 * @param {Element} node
	 */
	_transform(node) {
		const trigger = node.getAttribute("data-transform");
		if (trigger) {
			const rule = this._transformRules.find((r) => r.trigger === trigger);
			if (rule && rule.value) {
				if (rule.action === "text-content") {
					node.textContent = rule.value;
				} else {
					// by default, set attribute (usually 'transform')
					node.setAttribute(rule.action, rule.value);
				}
			}
		}
	}

	/** select clip-path via the transform rules
	 * @param {Element} node
	 */
	_setclip(node) {
		const trigger = node.getAttribute("select_clip");
		if (trigger) {
			const rule = this._transformRules.find((r) => r.trigger === trigger);
			if (rule && rule.value) {
				// by default, set attribute (usually 'clip-path')
				node.setAttribute(rule.action, this._replaceURLRefs(rule.value));
			}
		}
	}

	/** Turn a single fixed ID into a unique one
	 * @param {string} id
	 */
	_makeUniqueID(id) {
		return `${id}_rndID_${this._rndID}`;
	}

	/** Turn any ID URL references in the target attribute string into unique ones
	 * @param {string} attr
	 * @returns {string}
	 */
	_replaceURLRefs(attr) {
		return attr.replace(App.Art.URLIDMatcher, (a, b) => `url(#${this._makeUniqueID(b)})`);
	}

	/** Append unique IDs to clip-path and filter references
	 * @param {Element} node
	 */
	_replaceIDs(node) {
		const cp = node.getAttribute('clip-path');
		if (cp) {
			node.setAttribute('clip-path', this._replaceURLRefs(cp));
		}
		const style = node.getAttribute('style');
		if (style && style.search("filter") > -1) {
			node.setAttribute('style', this._replaceURLRefs(style));
		}
		for (const nodeChild of node.children) {
			this._replaceIDs(nodeChild);
		}
	}

	/** add an SVG from the cache to the render queue
	 * @param {string} id
	 */
	add(id) {
		const res = this._cache.get(id);
		let clones = [];
		if (!res) {
			console.error(`Missing art resource: ${id}`);
			return;
		}
		for (const srcNode of res.children) {
			const node = /** @type {Element} */ (srcNode.cloneNode(true));
			if (node.nodeName === "defs") {
				for (const defNode of node.children) {
					defNode.setAttribute("id", this._makeUniqueID(defNode.id));
				}
			} else {
				this._replaceIDs(node);
			}
			this._transform(node);
			this._setclip(node);
			let transformNodes = node.querySelectorAll('g[data-transform]');
			for (const child of transformNodes) {
				this._transform(child);
			}
			let clipNodes = node.querySelectorAll('g[select_clip]');
			for (const child of clipNodes) {
				this._setclip(child);
			}
			clones.push(node);
		}
		this._container.push({attrs: res.attributes, nodes: clones});
	}

	/** concatenate the contents of a second queue into this one.
	 * displayClass must match. cache and transformFunc may differ (they are used only by add).
	 * @param {App.Art.SvgQueue} queue
	 */
	concat(queue) {
		if (this._displayClass !== queue._displayClass) {
			throw Error("Incompatible SVG queues. displayClass must match.");
		}
		this._container.push(...queue._container);
	}

	/** merge consecutive svg child nodes in the queue with the same svg attributes into bigger svg nodes, and write them out
	 * this prevents re-evaluating viewboxes and classes unnecessarily and improves layout performance with lots of art
	 * @returns {DocumentFragment}
	 */
	output() {
		/** evaluate whether an attribute list is equivalent or not
		 * @param {NamedNodeMap} left
		 * @param {NamedNodeMap} right
		 */
		function equalAttributes(left, right) {
			/** get all the attribute names from an attribute list
			 * @param {NamedNodeMap} attrs
			 * @returns {string[]}
			 */
			function attrNames(attrs) {
				let names = [];
				for (let index = 0; index < attrs.length; ++index) {
					names.push(attrs[index].nodeName);
				}
				return names;
			}

			if (!left && !right) {
				return true; // both are nullish, treat as equal
			} else if (!left || !right) {
				return false; // only one is nullish, not equal
			}

			const leftNames = attrNames(left);
			const rightNames = attrNames(right);
			const intersectionLength = _.intersection(leftNames, rightNames).length;
			if (leftNames.length !== intersectionLength || rightNames.length !== intersectionLength) {
				return false; // contain different attributes, not equal
			}
			// are all values equal?
			return leftNames.every((attr) => left.getNamedItem(attr).nodeValue === right.getNamedItem(attr).nodeValue);
		}

		let frag = document.createDocumentFragment();
		let currentAttributes;
		let outSvg;
		for (const svg of this._container) {
			if (!equalAttributes(currentAttributes, svg.attrs)) {
				outSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
				outSvg.setAttribute("class", this._displayClass);
				frag.appendChild(outSvg);
				for (let index = 0; index < svg.attrs.length; ++index) {
					outSvg.setAttribute(svg.attrs[index].nodeName, svg.attrs[index].nodeValue);
				}
				currentAttributes = svg.attrs;
			}
			outSvg.append(...svg.nodes);
		}
		return frag;
	}
};


code:
// The smaller, the more global
App.Data.World.TravelFrictionExponent = -0.6;
// The smaller, the more are large nationalities downscaled
App.Data.World.PopScaleFactor = 23;

/**
 * @typedef {Record<number, number>} gridPoint
 * Not intended for manual handling, use one of the functions below to get the data behind it.
 *
 * If gridPoint[0] >= 0: Point in the world data grids. (X, Y)
 * Otherwise           : Encodes a small territory
 */

/**
 * @returns {Generator<gridPoint, void, *>}
 */
App.Data.World.gridPoints = function*() {
	for (let x = 0; x < App.Data.World.GridDimensions.width; x++) {
		for (let y = 0; y < App.Data.World.GridDimensions.height; y++) {
			yield [x, y];
		}
	}
	for (let i = 0; i < App.Data.World.SmallTerritories.length; i++) {
		yield [-1, i];
	}
};

/**
 * Turn a grid cell into a global coordinate.
 * @param {gridPoint} p
 * @returns {[number, number]} Latitude, Longitude
 */
App.Data.World.gridPointToCoordinate = function(p) {
	if (p[0] === -1) {
		const st = App.Data.World.SmallTerritories[p[1]];
		return [st[2], st[3]];
	}

	const width = App.Data.World.GridDimensions.width;
	const height = App.Data.World.GridDimensions.height;

	const lon = p[0] / App.Data.World.GridDimensions.width * 360 - 180;
	const lat = p[1] / App.Data.World.GridDimensions.height * -180 + 90;

	const lonCell = 360 / width;
	const latCell = 180 / height;

	return [lat - latCell / 2, lon + lonCell / 2];
};

/**
 * Return the grid cell that encloses the given coordinates
 * @param {number} lat
 * @param {number} lon
 * @returns {gridPoint}
 */
App.Data.World.coordinateToGridPoint = function(lat, lon) {
	const width = App.Data.World.GridDimensions.width;
	const height = App.Data.World.GridDimensions.height;

	const lonCell = 360 / width;
	const latCell = 180 / height;

	const x = Math.floor((lon + 180) / lonCell);
	const y = Math.floor((-lat + 90) / latCell);

	return [x, y];
};

/**
 * @param {gridPoint} p
 * @returns {number}
 */
App.Data.World.populationAt = function(p) {
	if (p[0] === -1) {
		const st = App.Data.World.SmallTerritories[p[1]];
		return st[1];
	}
	return App.Data.World.PopGrid[p[1]][p[0]];
};

/**
 * @param {gridPoint} p
 * @returns {number}
 */
App.Data.World.nationIdAt = function(p) {
	if (p[0] === -1) {
		const st = App.Data.World.SmallTerritories[p[1]];
		return st[0];
	}
	return App.Data.World.NationGrid[p[1]][p[0]];
};

/**
 * @param {gridPoint} p
 * @returns {string}
 */
App.Data.World.nationAt = function(p) {
	const nationId = App.Data.World.nationIdAt(p);
	return App.Data.World.Nations[nationId] || "Stateless";
};


code:
/**
 * The diff proxy records all changes to the original object without changing the original object while emulating the
 * changes for access from outside. The resulting diff object can then be applied on the original. The resulting object
 * is the same as if the changes were applied directly to the original object.
 *
 * NOTE: When doing any changes to this, rerun the tests cases at devNotes/tests.
 */
App.Utils.Diff = (function() {
	const deletedSymbol = Symbol("deleted property");

	/**
	 * @template {object} T
	 * @param {T} base
	 * @returns {FC.Util.DiffRecorder<T>}
	 */
	function getProxy(base) {
		const diff = {};

		/**
		 * @typedef {Array<string|symbol>} path
		 * Can only point to objects or arrays, never to primitives
		 */

		/**
		 * @param {path} path
		 * @returns {any}
		 */
		function localDiff(path) {
			let value = diff;
			for (const key of path) {
				if (key in value) {
					value = value[key];
				} else {
					return {};
				}
			}
			return value;
		}

		/**
		 * @param {path} path
		 * @param {string|symbol} prop
		 * @param {any} value
		 */
		function setDiff(path, prop, value) {
			let localDiff = diff;
			/**
			 * @type {object}
			 */
			let original = base;
			let originalLost = false; // True, if the original object does not have this path
			for (const key of path) {
				if (key in original) {
					original = original[key];
				} else {
					originalLost = true;
				}
				if (!(key in localDiff)) {
					if (originalLost) {
						// Should not happen
						throw new TypeError("Neither original nor diff have this property: " + path);
					}

					if (_.isArray(original)) {
						// clone arrays to make sure push, pop, etc. operations don't mess anything up later
						// Deep copy in case array entries get modified later
						localDiff[key] = _.cloneDeep(original);
					} else if (_.isObject(original)) {
						// TODO: check if there is a way to keep the data structure valid without the need for deep clone
						localDiff[key] = _.cloneDeep(original);
					}
				}
				localDiff = localDiff[key];
			}
			localDiff[prop] = value;
		}

		/**
		 * @template {object} T
		 * @param {T} target
		 * @param {path} path
		 * @returns {FC.Util.DiffRecorder<T>|T}
		 */
		function makeProxy(target, path) {
			if (target == null) { // intentionally ==
				return target;
			}

			if (_.isArray(target)) {
				return new Proxy(target, {
					get: function(o, prop) {
						if (prop === "diffOriginal") {
							return o;
						} else if (prop === "diffChange") {
							return localDiff(path);
						}

						const value = prop in localDiff(path) ? localDiff(path)[prop] : o[prop];
						if (typeof value === "function") {
							if (prop in localDiff(path)) {
								return value.bind(localDiff(path));
							}
							if (["push", "pop", "shift", "unshift", "splice", "fill", "reverse"].includes(prop)) {
								// Deep copy in case array entries get modified later
								const diffArray = _.cloneDeep(o);
								setDiff(path.slice(0, -1), path.last(), diffArray);
								return value.bind(diffArray);
							}
							return value.bind(o);
						}
						if (value === deletedSymbol) {
							return undefined;
						}
						return makeProxy(value, [...path, prop]);
					},
					set: function(o, prop, value) {
						setDiff(path, prop, value);
						return true;
					},
					deleteProperty: function(o, prop) {
						setDiff(path, prop, deletedSymbol);
						return true;
					}
				});
			}

			if (_.isObject(target)) {
				return new Proxy(target, {
					get: function(o, prop) {
						if (prop === "diffOriginal") {
							return o;
						} else if (prop === "diffChange") {
							return localDiff(path);
						}

						if (prop in localDiff(path)) {
							if (localDiff(path)[prop] === deletedSymbol) {
								return undefined;
							}
							return makeProxy(localDiff(path)[prop], [...path, prop]);
						}
						return makeProxy(o[prop], [...path, prop]);
					},
					set: function(o, prop, value) {
						setDiff(path, prop, value);
						return true;
					},
					deleteProperty: function(o, prop) {
						setDiff(path, prop, deletedSymbol);
						return true;
					}
				});
			}

			return target;
		}

		// base is an object, therefore makeProxy() returns FC.Util.DiffRecorder
		// @ts-ignore
		return makeProxy(base, []);
	}

	/**
	 * @template {object} T
	 * @param {T} base
	 * @param {Partial<T>} diff
	 */
	function applyDiff(base, diff) {
		for (const key in diff) {
			const value = diff[key];
			// @ts-ignore
			if (value === deletedSymbol) {
				delete base[key];
			} else if (!_.isObject(value)) {
				base[key] = value;
			} else if (_.isArray(value)) {
				base[key] = value;
			} else if (base[key] === undefined || base[key] === null) {
				base[key] = value;
			} else {
				applyDiff(base[key], value);
			}
		}
	}

	return {
		getProxy: getProxy,
		applyDiff: applyDiff,
	};
})();


code:
App.Loader = (function() {
	/**
	 * Remember the last loaded script.
	 * @type {string}
	 */
	let lastScript = "";

	/**
	 * @param {string} name
	 * @param {string} path Relative to the HTML file
	 * @returns {HTMLScriptElement}
	 */
	function loadScript(name, path) {
		lastScript = name;

		const script = document.createElement("script");
		script.setAttribute("src", `${path}`);
		document.head.append(script);
		return script;
	}

	/**
	 * To make sure the scripts are loaded series, keep a queue of scripts to be loaded and only load the next once the
	 * previous one is finished.
	 *
	 * see {@link nextScript}
	 *
	 * @type {Array<()=>void>}
	 */
	const scriptQueue = [];

	class Group {
		/**
		 * @param {string} path
		 */
		constructor(path) {
			this._path = path;
			this._scripts = [];

			group.set(path, this);
			scriptQueue.push(() => {
				this._scripts.push(loadScript(path, path + "/index.js"));
			});
		}

		/**
		 * Loads a script as part of this group
		 * @param {string} subPath relative to group path
		 */
		queueSubscript(subPath) {
			scriptQueue.push(() => {
				this._scripts.push(loadScript(subPath, this._path + "/" + subPath + ".js"));
			});
		}

		/**
		 * Removes all script elements belonging to this Group. Does not undo any changes these scripts did.
		 */
		unload() {
			for (const script of this._scripts) {
				document.head.removeChild(script);
			}
		}
	}

	/**
	 * @type {Map<string, Group>}
	 */
	const group = new Map();

	/**
	 * Loads the group located at path
	 *
	 * @param {string} path
	 */
	function loadGroup(path) {
		if (group.has(path)) {
			group.get(path).unload();
		}
		new Group(path);
	}

	/**
	 * Gives the group located at path
	 *
	 * @param {string} path
	 * @returns {Group}
	 */
	function getGroup(path) {
		return group.get(path);
	}

	function nextScript() {
		if (scriptQueue.length > 0) {
			scriptQueue.shift()();
		}
	}

	return {
		loadGroup: loadGroup,
		getGroup: getGroup,
		nextScript: nextScript,
		executeTests: () => {
			loadGroup("../tests");
			nextScript();
		},
		get lastScript() {
			return lastScript;
		},
		get hasNextScript() {
			return scriptQueue.length > 0;
		}
	};
})();

/**
 * Dedicated Modding API to abstract the loading mechanisms
 */
App.Modding = (function() {
	let loadingDone = false;
	/**
	 * @type {Mod}
	 */
	let currentMod = null;

	/**
	 * @type {string[]}
	 */
	let modsToLoad = [];

	/**
	 * @type {Mod[]}
	 */
	const loadedMods = [];

	class Mod {
		/**
		 * @param {string} directory where this mod is located relative to 'mods/'
		 */
		constructor(directory) {
			this._path = "./mods/" + directory;
			this.name = directory;
			this.version = "0";
			this.description = "";

			currentMod = this;
			loadedMods.push(this);

			App.Loader.loadGroup(this._path);
			App.Loader.nextScript();
		}

		/**
		 * @returns {string}
		 */
		get path() {
			return this._path;
		}

		/**
		 * @param {string} path
		 */
		addSubscript(path) {
			App.Loader.getGroup(this._path).queueSubscript(path);
		}
	}

	/**
	 * Loads the next subscript or mod
	 */
	function nextScript() {
		if (App.Loader.hasNextScript) {
			App.Loader.nextScript();
		} else if (modsToLoad.length > 0) {
			new Mod(modsToLoad.shift());
		} else {
			loadingDone = true;
		}
	}

	/**
	 * Load all mods
	 */
	function loadMods() {
		modsToLoad = loadModList();
		nextScript();
	}

	/**
	 * @returns {string[]} modList
	 */
	function loadModList() {
		try {
			return SugarCube.storage.get("modList") || [];
		} catch (e) {
			console.error(e);
			console.error(`Failed to get modlist from SugarCube storage`);
			return [];
		}
	}

	return {
		internal: {
			load: loadMods,
			/** @returns {string[]} modList */
			get modList() {
				return loadModList();
			},
			/** @param {string[]} modList */
			set modList(modList) {
				SugarCube.storage.set("modList", modList);
			},
			get loadedMods() {
				return loadedMods;
			},
			get done() {
				return loadingDone;
			}
		},
		scriptDone: nextScript,
		get currentMod() {
			return currentMod;
		},
	};
})();

App.UI.playerMods = function() {
	let modList = loadModList();
	const container = document.createElement("div");
	container.append(makeModSettings());
	return container;

	function makeModSettings() {
		const f = new DocumentFragment();
		App.UI.DOM.appendNewElement("h2", f, "Player mods");
		App.UI.DOM.appendNewElement("div", f, "Player mods are mods loaded from external files located at 'mods/'. If a mod does not exist loading will fail and never finish. Mods are save independent.");
		if (!App.Modding.internal.done) {
			f.append("Mods have not finished loading. ", App.UI.DOM.link("Refresh", () => {
				modList = loadModList();
				refresh();
			}));
		}
		App.UI.DOM.appendNewElement("h3", f, "Currently loaded mods");
		f.append(loadedList());
		App.UI.DOM.appendNewElement("h3", f, "Edit mod list");
		f.append(makeEditor());
		return f;
	}

	/**
	 * @returns {string[]}
	 */
	function loadModList() {
		if (App.Status.storyReady) {
			return App.Modding.internal.modList;
		} else {
			return [];
		}
	}

	function loadedList() {
		const loadedMods = App.Modding.internal.loadedMods;
		const div = document.createElement("div");
		div.classList.add("grid-3columns-auto");
		for (const mod of loadedMods) {
			App.UI.DOM.appendNewElement("div", div, mod.name);
			App.UI.DOM.appendNewElement("div", div, mod.version);
			App.UI.DOM.appendNewElement("div", div, mod.description);
		}
		return div;
	}

	function refresh() {
		$(container).empty().append(makeModSettings());
	}

	function makeEditor() {
		const div = document.createElement("div");
		div.append("Add new mod. Input the exact name of the directory for the mod at 'mods/': ",
			App.UI.DOM.makeTextBox("", name => {
				modList.push(name);
				refresh();
			}));

		App.UI.DOM.appendNewElement("div", div, "Mods are loaded from top to bottom.");

		const listDiv = document.createElement("div");
		for (let i = 0; i < modList.length; i++) {
			const row = document.createElement("div");
			row.append(up(i), " ", down(i), " ", remove(i), " ", modList[i]);
			listDiv.append(row);
		}
		div.append(listDiv);

		div.append(App.UI.DOM.link("Finalize (Will reload the game!)", () => {
			App.Modding.internal.modList = modList;
			window.location.reload();
		}));
		return div;
	}

	/**
	 * @param {number} index
	 * @returns {HTMLElement}
	 */
	function up(index) {
		if (index === 0) {
			return App.UI.DOM.makeElement("span", "Up", ["gray"]);
		}
		const button = App.UI.DOM.makeElement("a", "Up");
		button.onclick = () => {
			arraySwap(modList, index, index - 1);
			refresh();
		};
		return button;
	}

	/**
	 * @param {number} index
	 * @returns {HTMLElement}
	 */
	function down(index) {
		if (index === modList.length - 1) {
			return App.UI.DOM.makeElement("span", "Down", ["gray"]);
		}
		const button = App.UI.DOM.makeElement("a", "Down");
		button.onclick = () => {
			arraySwap(modList, index, index + 1);
			refresh();
		};
		return button;
	}

	/**
	 * @param {number} index
	 * @returns {HTMLElement}
	 */
	function remove(index) {
		const button = App.UI.DOM.makeElement("a", "Remove");
		button.onclick = () => {
			modList.splice(index, 1);
			refresh();
		};
		return button;
	}
};


code:
// This provides some helpful functions from the array prototype to the map prototype

Object.defineProperty(Map.prototype, "filter", {
	/**
	 * Filters the map object the same way Array.prototype.filter filters Arrays
	 * Returns a new filtered map object
	 * @param {Function} callback the filtering function
	 * @returns {Map}
	 */
	value: function(callback) {
		const newMap = new Map();
		for (const [key, value] of this) {
			if (callback(value, key, this)) {
				newMap.set(key, value);
			}
		}
		return newMap;
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "randomKey", {
	/**
	 * Returns a random key from the Map.
	 * @returns {any}
	 */
	value: function() {
		if (this.size === 0) { return undefined; }
		return [...this.keys()].random();
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "randomValue", {
	/**
	 * Returns a random value from the Map.
	 * @returns {any}
	 */
	value: function() {
		if (this.size === 0) { return undefined; }
		return [...this.values()].random();
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "randomEntry", {
	/**
	 * Returns a random entry from the Map.
	 * @returns {any}
	 */
	value: function() {
		if (this.size === 0) { return undefined; }
		return [...this.entries()].random();
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "pluck", {
	/**
	 * Returns a random key/value pair from the Map. Removing it in the process.
	 * Returns an object with the properties: key and value
	 * @returns {{key: any, value: any}}
	 */
	value: function() {
		const key = this.randomKey();
		if (key === undefined) { return undefined; }
		const value = this.get(key);
		this.delete(key);
		return {key: key, value: value};
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "map", {
	/**
	 * Returns an array mapped using the provided callback.
	 * @param {Function} callback
	 * @returns {any[]}
	 */
	value: function(callback) {
		const newArray = [];
		for (const [key, value] of this) {
			newArray.push(callback(value, key, this));
		}
		return newArray;
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "find", {
	/**
	 * Returns a key/value object for the first entry in the map where predicate is true, and undefined otherwise.
	 *
	 * @param {Function} predicate
	 * find calls predicate once for each entry of the map, in ascending order, until it finds one where predicate returns true. If such an element is found, find immediately returns a key/value object. Otherwise, find returns undefined.
	 *
	 * @returns {{key: any, value: any}|undefined}
	 */
	value: function(predicate) {
		for (const [key, value] of this) {
			if (predicate(value, key, this)) {
				return {key: key, value: value};
			}
		}
		return undefined;
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "atIndex", {
	/**
	 * Returns a key/value object for the entry at the given index.
	 * Returns undefined if the index is out of range.
	 * Expensive; cache the result if you need it more than once.
	 * @param {number} index
	 *
	 * @returns {{key: any, value: any}|undefined}
	 */
	value: function(index) {
		const keys = [...this.keys()];
		const key = keys[index];
		if (!key) { return undefined; }
		return {key: key, value: this.get(key)};
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "every", {
	/**
	 * Determines whether all the members of a map satisfy the specified test.
	 *
	 * @param {Function} predicate
	 * A function that accepts up to three arguments. The every method calls the predicate function for each entry in the map until the predicate returns a value which is coercible to the Boolean value false, or until the end of the map.
	 */
	value: function(predicate) {
		for (const [key, value] of this) {
			if (!predicate(value, key, this)) {
				return false;
			}
		}
		return true;
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "some", {
	/**
	 * Returns true if at least one member of a map satisfy the specified test.
	 *
	 * @param {Function} predicate
	 * A function that accepts up to three arguments. The every method calls the predicate function for each entry in the map until the predicate returns a value which is coercible to the Boolean value true, or until the end of the map.
	 */
	value: function(predicate) {
		for (const [key, value] of this) {
			if (predicate(value, key, this)) {
				return true;
			}
		}
		return false;
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Map.prototype, "asArray", {
	/**
	 * Returns an array of the maps values.
	 * Expensive; Use `Map.prototype.values()` instead if possible.
	 */
	value: function() {
		return [...this.values()];
	},
	enumerable: false,
	configurable: true,
	writable: true
});


code:
Number.prototype.isBetween = function(min, max, inclusive = false) {
	return inclusive ? this >= min && this <= max : this > min && this < max;
};


code:
// This provides some helpful functions from the array prototype to the set prototype

// TODO:@franklygeorge implement as many of the extensions from map.extensions.js as makes sense for sets

Object.defineProperty(Set.prototype, "atIndex", {
	/**
	 * Returns a value for the entry at the given index.
	 * Returns undefined if the index is out of range.
	 * Expensive; cache the result if you need it more than once.
	 * @param {number} index
	 *
	 * @returns {any}
	 */
	value: function(index) {
		if (index + 1 > this.size) { return undefined; }
		return [...this][index];
	},
	enumerable: false,
	configurable: true,
	writable: true
});

Object.defineProperty(Set.prototype, "asArray", {
	/**
	 * Returns an array of the sets values.
	 * Expensive; Use `Set.prototype.values()` instead if possible.
	 */
	value: function() {
		return [...this];
	},
	enumerable: false,
	configurable: true,
	writable: true
});


code:
App.Medicine.Surgery.Reactions.AddDick = class extends App.Medicine.Surgery.SimpleReaction {
	/**
	 * @param {FC.SlaveState} slave
	 * @param {Partial<FC.SlaveState>} diff
	 * @returns {reactionResult}
	 */
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, His, his, him} = getPronouns(slave);
		const r = [];

		if (slave.fetish === Fetish.MINDBROKEN) {
			r.push(`The surgery is invasive, and ${he} spends some time in the autosurgery, slowly recovering. As feeling slowly returns to the lower half of ${his} body and ${his} thoughts coalesce against the dissipating fog of surgical pharmacology, ${he} strains to focus on ${his} groin${(canSee(slave)) ? `, visible in a ceiling mirror above ${him}` : ``}. ${His} eyes open wide as ${he} takes in the new member between ${his} legs. Suddenly, it twitches for the first time. The mixed pain of the terribly sore area and pleasure of new sensations floods ${him}, and ${he} jerks against the surgical restraints, gasping for breath. ${his} health has been severely affected by the intrusive surgery.`);
		} else if (slave.devotion > 50) {
			r.push(`The surgery is invasive, and ${he} spends some time in the autosurgery, slowly recovering. As feeling slowly returns to the lower half of ${his} body and ${his} thoughts coalesce against the dissipating fog of surgical pharmacology, ${he} strains to focus on ${his} groin${(canSee(slave)) ? `, visible in a ceiling mirror above ${him}` : ``}. As a devoted slave, ${he} knew the essentials of the surgery before it was performed, so ${he}'s excited to`);
			if (canSee(slave)) {
				r.push(`see`);
			} else {
				r.push(`feel`);
			}
			r.push(`the result. ${His} eyes open wide as ${he} takes in the new member between ${his} legs. Suddenly, it twitches for the first time. The mixed pain of the terribly sore area and pleasure of new sensations floods ${him}, and ${he} jerks against the surgical restraints, gasping for breath. ${He} is tremendously impressed that you would devote such immense resources to altering ${his} body, and is more willing than ever to submit to your plans for ${his} future. ${his} health has been severely affected by the intrusive surgery.`);
			reaction.trust += 5;
			reaction.devotion += 5;
		} else if (slave.devotion >= -20) {
			r.push(`The surgery is invasive, and ${he} spends some time in the autosurgery, slowly recovering. As feeling slowly returns to the lower half of ${his} body and ${his} thoughts coalesce against the dissipating fog of surgical pharmacology, ${he} focuses on ${his} groin${(canSee(slave)) ? `, visible in a ceiling mirror above ${him}` : ``}. ${He} slowly becomes aware of the soft new member resting between ${his} legs. ${He} has a penis. The thought process is visible on ${his} face as the fact sinks in. ${He} has a penis! Unsure what to make of this unbelievable development, ${he}`);
			if (canSee(slave)) {
				r.push(`turns ${his} gaze away from the mirror and`);
			}
			r.push(`shuts ${his} eyes tightly. ${He} is terrified of your apparently untrammeled power over ${his} body, so much so that ${he} is now more willing to submit to your plans for ${his} future. ${his} health has been severely affected by the intrusive surgery.`);
			reaction.trust -= 5;
			reaction.devotion += 5;
		} else {
			r.push(`The surgery is invasive, and ${he} spends some time in the autosurgery, slowly recovering. As feeling slowly returns to the lower half of ${his} body and ${his} thoughts coalesce against the dissipating fog of surgical pharmacology, ${he} focuses on ${his} groin${(canSee(slave)) ? `, visible in a ceiling mirror above ${him}` : ``}. ${He} slowly becomes aware of the soft new member resting between ${his} legs. ${He} has a penis. The thought process is visible on ${his} face as the fact sinks in. ${He} has a penis!`);
			if (slave.voice === 0) {
				r.push(`${He} tries to scream, but as a mute all ${he} can manage is pathetic, silent thrashing against the surgical restraints.`);
			} else {
				r.push(`A huge scream rises out of ${him}, going on and on, until ${he} pauses to gasp for breath so ${he} can go on screaming.`);
			}
			r.push(`${He} is terrified of your apparently untrammeled power over ${his} body, and furious at ${his} lack of control over ${his} own person. ${his} health has been severely affected by the intrusive surgery.`);
			reaction.trust -= 5;
			reaction.devotion -= 5;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAddDick = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Implant";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		if (this._slave.prostate === 0) {
			this._slave.prostate = 1;
		}
		this._slave.dick = 2;
		this._slave.clit = 0;
		this._slave.foreskin = this._slave.dick;
		return this._assemble(new App.Medicine.Surgery.Reactions.AddDick());
	}
};


code:
App.Medicine.Surgery.Reactions.AddForeskin = class extends App.Medicine.Surgery.SimpleReaction {
	/**
	 * @param {FC.SlaveState} slave
	 * @param {Partial<FC.SlaveState>} diff
	 * @returns {reactionResult}
	 */
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, His, his, him} = getPronouns(slave);
		const r = [];

		if (slave.dick > 0) {
			r.push(`${He} leaves the surgery gingerly, knowing ${he}'s had surgery on ${his} dick. Taking the first opportunity to check out ${his} member${(canSee(slave)) ? ` in a mirror` : ``}, ${he}'s`);
			if (slave.fetish === Fetish.MINDBROKEN) {
				r.push(`confused to find nothing has really changed.`);
			} else if (slave.devotion > 50) {
				r.push(`titillated to find that ${he} has a foreskin. ${He} examines it`);
				if (canSee(slave)) {
					r.push(`carefully and then touches it,`);
				}
				r.push(`very gently. ${He}'s very sore, obviously, but the sudden sensation is almost too much for ${him}. ${His} resolution to investigate ${his} remodeled dick, but later, is almost visible. It's obvious that ${he} can hardly wait, anticipating all the new sensations that this new and very sensitive patch of skin can offer ${him}. ${He}'s grateful to you for improving ${his} dick.`);
				reaction.trust += 5;
				reaction.devotion += 5;
			} else if (slave.devotion >= -20) {
				r.push(`quite surprised, and a little relieved, to find that ${he} has a foreskin. ${He} examines it`);
				if (canSee(slave)) {
					r.push(`carefully and then touches it,`);
				}
				r.push(`very gently. ${He}'s very sore, obviously, but the sudden sensation is almost too much for ${him}. ${He} seems to have been worried that something more dramatic than a reversal of circumcision had been done to ${him}, but ${his} chief reaction is mystified submission to you afterward. Your total power over ${his} body has been made clear to ${him} in a way that provokes confusion, not fear.`);
				reaction.devotion += 5;
			} else {
				r.push(`shocked to find that ${he} has a foreskin. ${He}'s not exactly resentful of you for doing this, since whatever ${his} feelings about circumcision might be, it's very far from what ${he} feared might be done to ${his} dick. ${His} reaction is dominated by fear of the unknown, since ${he}'s now wondering whether ${he} has any ability to predict your actions at all. Whatever ${his} mental model of you was before this, it probably didn't include you restoring ${his} foreskin.`);
				reaction.trust -= 5;
			}
		} else {
			r.push(`${He} leaves the surgery gingerly, knowing ${he}'s had surgery on ${his} genitals. Taking the first opportunity to check out ${his} crotch${(canSee(slave)) ? ` in a mirror` : ``}, ${he}'s`);
			if (slave.fetish === Fetish.MINDBROKEN) {
				r.push(`confused to find that ${his} clitoris is missing.`);
			} else if (slave.devotion > 50) {
				r.push(`titillated to find that ${his} clitoris has a hood. ${He} examines it`);
				if (canSee(slave)) {
					r.push(`carefully and then touches it,`);
				}
				r.push(`very gently. ${He}'s very sore, obviously, but the sudden sensation is almost too much for ${him}. ${His} resolution to investigate ${his} remodeled clit, but later, is almost visible. It's obvious that ${he} can hardly wait, anticipating all the new sensations that this new patch of skin can offer ${him}. ${He}'s grateful to you for improving ${his} pussy.`);
				reaction.trust += 5;
				reaction.devotion += 5;
			} else if (slave.devotion >= -20) {
				r.push(`quite surprised, and a little relieved, to find that ${his} clit has a hood. ${He} examines it`);
				if (canSee(slave)) {
					r.push(`carefully and then touches it,`);
				}
				r.push(`very gently. ${He}'s very sore, obviously, but the sudden sensation is almost too much for ${him}. ${He} seems to have been worried that something more dramatic than a reversal of circumcision had been done to ${him}, but ${his} chief reaction is mystified submission to you afterward. Your total power over ${his} body has been made clear to ${him} in a way that provokes confusion, not fear.`);
				reaction.devotion += 5;
			} else {
				r.push(`shocked to find that ${his} clitoris has a hood. ${He}'s not exactly resentful of you for doing this, since whatever ${his} feelings about circumcision might be, it's very far from what ${he} feared might be done to ${his} clit. ${His} reaction is dominated by fear of the unknown, since ${he}'s now wondering whether ${he} has any ability to predict your actions at all. Whatever ${his} mental model of you was before this, it probably didn't include you restoring ${his} clitoral hood.`);
				reaction.trust -= 5;
			}
		}
		r.push(`As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAddForeskin = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Graft on";
	}

	get healthCost() {
		return 10;
	}

	apply(cheat) {
		if (this._slave.dick > 0) {
			this._slave.foreskin = this._slave.dick;
		} else if (this._slave.clit > 0) {
			this._slave.foreskin = this._slave.clit;
		} else {
			this._slave.foreskin = 1;
		}
		return this._assemble(new App.Medicine.Surgery.Reactions.AddForeskin());
	}
};