code:
window.mapMove = function (moveTo) {
	var currentPassage = V.passage;
	var destination_table = [];
	for (var i = 1; i < V.link_table.length; i++) {
		var temp = V.link_table[i].split("|")[1];
		if (temp) {
			destination_table[destination_table.length] = temp.split("]]")[0];
		}
	}
	var available = V.map.available;

	if (V.debug == 1 || available[currentPassage].includes(moveTo) && destination_table.includes(moveTo))
	//if(V.debug == 1 || available[currentPassage].includes(moveTo))
	{
		new Wikifier(null, '<<pass 5>>');
		SugarCube.State.display(moveTo);
	}
}

window.shopClothingFilterToggleTrait = function(trait) {
	let traits = V.shopClothingFilter.traits;
	if (traits) {
		let index = traits.indexOf(trait)
		if (index == -1) {
			traits.push(trait)
		} else {
			traits.splice(index, 1)
		}
	}
}

window.shopClothingFilterSortOnDescription = function(traitOne, traitTwo) {
	let descriptionOne = Wikifier.wikifyEval(`<<shopTraitDescription ${traitOne}>>`).textContent.trim();
	let descriptionTwo = Wikifier.wikifyEval(`<<shopTraitDescription ${traitTwo}>>`).textContent.trim();

	return descriptionOne > descriptionTwo
}

window.wikifier = function (widget, arg1, arg2, arg3) {
	if (arg3 !== undefined) {
		new Wikifier(null, '<<' + widget + ' ' + arg1 + ' ' + arg2 + ' ' + arg3 + '>>');
	}
	else if (arg2 !== undefined) {
		new Wikifier(null, '<<' + widget + ' ' + arg1 + ' ' + arg2 + '>>');
	}
	else if (arg1 !== undefined) {
		new Wikifier(null, '<<' + widget + ' ' + arg1 + '>>');
	}
	else if (arg1 === undefined) {
		new Wikifier(null, '<<' + widget + '>>');
	}
}

window.wikifier2 = function (str) {
	new Wikifier(null, str);
}

window.actionsreplace = function (bodypart) {
	var check = bodypart+"target";
	if (V[check] == "tentacles"){
		new Wikifier(null, '<<replace #'+ bodypart + 'action>><<'+ bodypart + 'ActionInitTentacle>><</replace>>');
	}else if (V[check] == "swarm"){
		new Wikifier(null, '<<replace #'+ bodypart + 'action>><<'+ bodypart + 'ActionInitSwarm>><</replace>>');
	}else if (V[check] == "vore"){
		new Wikifier(null, '<<replace #'+ bodypart + 'action>><<'+ bodypart + 'ActionInitVore>><</replace>>');
	}else if (V[check] == "struggle"){
		new Wikifier(null, '<<replace #'+ bodypart + 'action>><<'+ bodypart + 'ActionInitStruggle>><</replace>>');
	}else if (V[check] == "machine"){
		new Wikifier(null, '<<replace #'+ bodypart + 'action>><<'+ bodypart + 'ActionInitMachine>><</replace>>');
	}else if (V[check] == "self"){
		new Wikifier(null, '<<replace #'+ bodypart + 'action>><<'+ bodypart + 'ActionInitSelf>><</replace>>');
	}else{
		new Wikifier(null, '<<replace #'+ bodypart + 'action>><<'+ bodypart + 'ActionInit>><</replace>>');
	}
}

window.combatListColor = function (name, value, type) {
	var color = "";
	var check = "";
	if (value != undefined) {
		check = value;
	} else {
		check = V[name];
	}
	if (type === "") {
		switch (check) {
			/*leftaction or rightaction*/
			case "steal": case "penwhack": case "freeface": case "leftcovervagina": case "leftcoverpenis": case "leftcoveranus":
			case "rightcovervagina": case "rightcoverpenis": case "rightcoveranus":
			case "leftunderpull": case "leftskirtpull": case "leftlowerpull": case "leftupperpull":
			case "rightunderpull": case "rightskirtpull": case "rightlowerpull": case "rightupperpull": case "rightUndressOther": case "leftUndressOther":
			case "stopchoke": case "clench": case "shacklewhack": case "leftfold": case "rightfold":
			case "leftstruggleweak": case "rightstruggleweak":
			/*feetaction*/
			case "run": case "hide": case "confront":
			/*mouthaction*/
			case "pullaway": case "pullawayvagina": case "finish": case "novaginal": case "nopenile": case "noanal": case "scream":
			case "mock": case "breastclosed": case "breastpull": case "pullawaykiss": case "noupper":
			case "up": case "stifleorgasm": case "stifle":
			/*penisaction*/
			case "othermouthescape": case "escape": case "otheranusescape": case "fencingescape" :
			/*vaginaaction*/
			case "tribescape":
			/*anusaction*/
			case "doubleescape":
				color = "brat";
				break;

			/*leftaction or rightaction*/
			case "spray": case "lefthit": case "righthit": case "leftstruggle": case "rightstruggle": case "stopchokenoncon": case "pursuit_grab":
			/*feetaction*/
			case "kick":
			/*mouthaction*/
			case "bite": case "demand": case "breastbite": case "handbite": case "headbutt": case "bitepussy":
				color = "def";
				break;
			/*leftaction or rightaction*/
			case "behind": case "fold":
			case "leftcovervaginameek": case "leftcoverpenismeek": case "leftcoveranusmeek":
			case "rightcovervaginameek": case "rightcoverpenismeek": case "rightcoveranusmeek":
			case "leftprotect": case "rightprotect": case "leftgrip": case "rightgrip":
			case "leftcurl": case "rightcurl":
			/*mouthaction*/
			case "grasp": case "plead": case "forgive": case "down":
			case "letout": case "letoutorgasm": case "noises":
			/*penisaction*/
			case "thighbay": case "bay": case "otheranusbay":
			/*vaginaaction*/
			case "penisthighs":
			/*anusaction*/
			case "bottombay": case "penischeeks": case "penispussy": case "penispussydap": case "penisanus":
				color = "meek";
				break;

			/*leftaction or rightaction*/
			case "leftplay": case "leftgrab": case "leftstroke": case "leftchest": case "rightplay": case "rightgrab": case "rightstroke": case "rightchest":
			case "leftchest": case "rightchest": case "leftwork": case "rightwork": case "leftclit": case "rightclit":
			case "keepchoke":
			/*feetaction*/
			case "grab": case "vaginagrab": case "grabrub": case "vaginagrabrub": case "rub":
			/*mouthaction*/
			case "peniskiss": case "kiss": case "suck": case "lick": case "moan": case "breastsuck": case "breastlick": case "swallow": case "movetochest":
			case "othervagina": case "mouth": case "kissback": case "vaginalick": case "askchoke": case "anallick": case "analkiss":
			/*penisaction*/
			case "penistovagina": case "penistoanus": case "penisvaginafuck": case "penisanusfuck": case "othermouthtease": case "othermouthrub":
			case "othermouthcooperate": case "tease": case "cooperate": case "otheranustease": case "otheranusrub": case "otheranuscooperate": case "clitrub":
			case "vaginaEdging": case "otheranusEdging":
			/*fencing*/
			case "otherpenisrub": case "penistopenis": case "penistopenisfuck": case "fencingcooperate":
			/*vaginaaction*/
			case "vaginatopenis": case "vaginapenisfuck": case "othervaginarub": case "vaginatovagina": case "vaginatovaginafuck": case "tribcooperate": case "penisEdging":
			case "vaginatopenisdouble": case "vaginapenisdoublefuck": case "penispussydouble": case "penisanusdvp":
			/*anusaction*/
			case "anustopenis": case "anuspenisfuck": case "penistease": case "otherMouthAnusRub": case "otherAnusRub": case "penisEdging":
			/*doubleanusaction*/
			case "anustopenisdouble": case "anuspenisdoublefuck": case "penisdoubletease": case "penisDoubleEdging": case "doublecooperate": case "penisanusdouble":
				color = "sub";
				break;

			default:
				color = "white";
				break;
		}
	}
	else if (type === "Tentacle") {
		switch (check.replace(/\d+/g, '')) {
			/*leftaction or rightaction*/
			case "lefthittentacle": case "righthittentacle":
			case "lefthit": case "righthit":
			case "leftbanish": case "rightbanish":
			/*feetaction*/
			case "feethit":
			/*mouthaction*/
			case "mouthbitetentacle":
				color = "def";
				break;

			/*leftaction or rightaction*/
			case "leftfold": case "rightfold": case "leftstruggleweak": case "rightstruggleweak":
			/*mouthaction*/
			case "mouthpullawaytentacle": case "stifleorgasm": case "stifle":
			/*penisaction*/
			case "penispullawaytentacle":
			/*vaginaaction*/
			case "vaginapullawaytentacle":
			/*anusaction*/
			case "anuspullawaytentacle":
				color = "brat";
				break;

			/*leftaction or rightaction*/
			case "leftgrabtentacle": case "rightgrabtentacle": case "leftrubtentacle": case "rightrubtentacle":
			case "showbottomtentacle": case "showthighstentacle": case "showmouthtentacle": case "showpenistentacle": case "showvaginatentacle":
			case "leftgrab": case "rightgrab": case "leftrub": case "rightrub":
			case "showbottom": case "showthighs": case "showmouth":
			/*feetaction*/
			case "feetgrab": case "feetrubtentacle":
			/*mouthaction*/
			case "mouthlicktentacle": case "mouthkisstentacle": case "mouthcooperatetentacle":
			/*penisaction*/
			case "penisrubtentacle": case "peniscooperatetentacle":
			/*vaginaaction*/
			case "vaginarubtentacle": case "vaginacooperatetentacle":
			/*anusaction*/
			case "anusrubtentacle": case "anuscooperatetentacle":
			/*bottomuse*/
			case "bottomrubtentacle":
			/*breastuse*/
			case "chestrubtentacle":
				color = "sub";
				break;

			/*leftaction or rightaction*/
			case "leftprotect": case "rightprotect": case "leftgrip": case "rightgrip": case "leftcurl": case "rightcurl":
			/*mouthaction*/
			case "letout": case "letoutorgasm": case "noises":
				color = "meek";
				break;

			default:
				color = "white";
				break;
		}
	}
	else if (type === "Vore") {
		switch (check.replace(/\d+/g, '')) {
			case "leftescape": case "rightescape": case "lefthold": case "righthold": case "leftvorefree": case "rightvorefree":
			case "leftfold": case "rightfold": case "leftstruggleweak": case "rightstruggleweak":
				color = "brat";
				break;

			case "leftprotect": case "rightprotect": case "leftgrip": case "rightgrip": case "leftcurl": case "rightcurl":
				color = "meek";
				break;

			default:
				color = "white";
				break;
		}

	}
	else if (type === "Swarm") {
		switch (check.replace(/\d+/g, '')) {
			/*leftaction or rightaction*/
			case "leftfree": case "rightfree": case "frontpurgeleft": case "frontpurgeright":
			case "frontclearleft": case "frontclearright": case "backpurgeleft": case "backpurgeright":
			case "backclearleft": case "backclearright": case "chestclearleft": case "chestclearright":
			case "leftfold": case "rightfold": case "leftstruggleweak": case "rightstruggleweak":
				color = "brat";
				break;

			case "leftprotect": case "rightprotect": case "leftgrip": case "rightgrip": case "leftcurl": case "rightcurl":
				color = "meek";
				break;

			case "swim":
				color = "teal";
				break;

			default:
				color = "white";
				break;
		}

	}
	else if (type === "Struggle") {
		switch (check.replace(/\d+/g, '')) {
			/*leftaction or rightaction*/
			case "mouth_strengthen": case "mouth_grasp": case "vagina_strengthen": case "vagina_grasp":
			case "penis_strengthen": case "penis_grasp": case "anus_strengthen": case "anus_grasp":
			case "chest_strengthen": case "chest_grasp":
			case "leftfold": case "rightfold": case "leftstruggleweak": case "rightstruggleweak":
				color = "brat";
				break;

			/*leftaction or rightaction*/
			case "leftprotect": case "rightprotect": case "leftgrip": case "rightgrip": case "leftcurl": case "rightcurl":
			case "rest":
			/*feetaction*/
			case "evade": case "plant":
				color = "meek";
				break;

			/*leftaction or rightaction*/
			case "capture": case "mouth_pull": case "mouth_spray": case "vagina_pull": case "vagina_spray":
			case "penis_pull": case "penis_spray": case "anus_pull": case "anus_spray": case "chest_pull": case "chest_spray":
			/*mouthaction*/
			case "bite":
				color = "def";
				break;

			/*leftaction or rightaction*/
 			case "mouth_stroke": case "vagina_stroke": case "penis_stroke": case "anus_stroke": case "chest_stroke":
			/*mouthaction*/
			case "open": case "suck":
				color = "sub";
				break;

			default:
				color = "white";
				break;
		}

	}
	else if (type === "Machine") {
		switch (check.replace(/\d+/g, '')) {
			case "leftfold": case "rightfold": case "leftstruggleweak": case "rightstruggleweak": case "vaginal_push": case "anal_push":
				color = "brat";
				break;

			case "chain_struggle": case "whack": case "vaginal_whack": case "anal_whack":
				color = "def";
				break;

			case "leftprotect": case "rightprotect": case "leftgrip": case "rightgrip": case "leftcurl": case "rightcurl":
				color = "sub";
				break;

			default:
				color = "white";
				break;
		}

	}
	else if (type === "Self") {
		switch (check.replace(/\d+/g, '')) {
			/*leftaction or rightaction*/
			case "leftfree": case "rightfree":
			case "leftcovervagina": case "leftcoverpenis": case "leftcoveranus":
			case "rightcovervagina": case "rightcoverpenis": case "rightcoveranus":
			case "leftunderpull": case "leftskirtpull": case "leftlowerpull": case "leftupperpull":
			case "rightunderpull": case "rightskirtpull": case "rightlowerpull": case "rightupperpull":
			case "leftcovervagina": case "leftcoverpenis": case "leftcoveranus":
			case "rightcovervagina": case "rightcoverpenis": case "rightcoveranus":
			case "leftunderpull": case "leftskirtpull": case "leftlowerpull": case "leftupperpull":
			case "rightunderpull": case "rightskirtpull": case "rightlowerpull": case "rightupperpull":
			case "leftfold": case "rightfold": case "leftstruggleweak": case "rightstruggleweak":
				color = "brat";
				break;

			/*leftaction or rightaction*/
			case "leftprotect": case "rightprotect": case "leftgrip": case "rightgrip": case "leftcurl":
			case "rightcurl": case "behind":
			/*feetaction*/
			case "evade": case "plant":
				color = "meek";
				break;

			case "swim":
				color = "teal";
				break;

			default:
				color = "white";
				break;
		}

	}
	return color;
}

DefineMacroS("combatListColor", combatListColor);

function combatButtonAdjustments(name, extra) {
	jQuery(document).on('change', '#listbox-' + name, { "name": name, "extra": extra }, function (e) {
		/*console.log(e.data);*/
		new Wikifier(null, '<<replace #' + e.data.name + 'Difficulty>><<' + e.data.name + 'Difficulty' + e.data.extra + '>><</replace>>');
		$('#' + e.data.name + 'Select').removeClass('whiteList bratList meekList defList subList');
		$('#' + e.data.name + 'Select').addClass(combatListColor(e.data.name, undefined, e.data.extra) + "List");
	});
	return "";
}

DefineMacroS("combatButtonAdjustments", combatButtonAdjustments);

function combatDefaults() {
	jQuery(document).on('change', '#listbox--defaultoption', function (e) {
		new Wikifier(null, '<<replace #othersFeelings>><<othersFeelings ' + this.value + '>><</replace>>');
	});
	return "";
}

DefineMacroS("combatDefaults", combatDefaults);

function hairdressersReset() {
	jQuery(document).on('change', '.macro-listbox', function (e) {
		new Wikifier(null, '<<replace #hairDressers>><<hairDressersOptions>><</replace>>');
		new Wikifier(null, '<<replace #currentCost>>To pay: £<<print _currentCost / 100>><</replace>>');
	});
	return "";
}

DefineMacroS("hairdressersReset", hairdressersReset);

function hairdressersResetAlt() {
	jQuery(document).on('click', '.macro-cycle', function (e) {
		new Wikifier(null, '<<replace #hairDressersSydney>><<hairDressersOptionsSydney>><</replace>>');
		new Wikifier(null, '<<replace #currentCost>>To pay: £<<print _currentCost / 100>><</replace>>');
	});
	return "";
}

DefineMacroS("hairdressersResetAlt", hairdressersResetAlt);

function browsDyeReset() {
	jQuery(document).on('change', '.macro-listbox', function (e) {
		new Wikifier(null, '<<replace #browsColourPreview>><<browsColourPreview>><</replace>>');
	});
	return "";
}

DefineMacroS("browsDyeReset", browsDyeReset);

function NPCSettingsReset() {
	jQuery(document).on('change', '#listbox--npcid', function (e) {
		new Wikifier(null, '<<replace #npcSettingsMenu>><<npcSettingsMenu>><</replace>>');
	});
	return "";
}

DefineMacroS("NPCSettingsReset", NPCSettingsReset);

function loveInterestFunction() {
	jQuery(document).on('change', '#listbox-loveinterestprimary', function (e) {
		new Wikifier(null, '<<replace #loveInterest>><<loveInterest>><</replace>>');
	});
	jQuery(document).on('change', '#listbox-loveinterestsecondary', function (e) {
		new Wikifier(null, '<<replace #loveInterest>><<loveInterest>><</replace>>');
	});
	return "";
}

DefineMacroS("loveInterestFunction", loveInterestFunction);

window.between = function(x, min, max){
	return x >= min && x <= max;
}

function featsPointsMenuReset() {
	jQuery(document).on('change', '#listbox--upgradenameid', function (e) {
		new Wikifier(null, '<<updateFeatsPointsMenu>>');
	});
	return "";
}

DefineMacroS("featsPointsMenuReset", featsPointsMenuReset);

function startingPlayerImageReset() {
	jQuery(document).on('change', '#settingsDiv .macro-radiobutton,#settingsDiv .macro-numberslider,#settingsDiv .macro-checkbox', function (e) {
		new Wikifier(null, '<<startingPlayerImageUpdate>>');
	});
	return "";
}

DefineMacroS("startingPlayerImageReset", startingPlayerImageReset);

window.deck = function(){
	var names = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
	var suits = ['Hearts','Diamonds','Spades','Clubs'];
	var cards = [];

	for( var s = 0; s < suits.length; s++ ) {
		for( var n = 0; n < names.length; n++ ) {
			cards.push( {value:n+2, name:names[n], suits:suits[s]} );
		}
	}

	return cards;
}

window.shuffle = function(o) {
	for(var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
	return o;
};

function updateAskColour() {
	jQuery(document).on('change', '#listbox-askaction', function (e) {
		new Wikifier(null, '<<replaceAskColour>>');
	});
	return "";
}

DefineMacroS("updateAskColour", updateAskColour);

window.generateBabyName = function(name, gender, usedNames) {
	var result = "";
	if(name != undefined && name != null && name != ""){
		var result = name.replace(/[^a-zA-Z ]+/g,"");
		return result.substring(0,30);
	}else{
		var names = [];
		switch(gender){
			case "m":
				names = ['Addison','Algernon','Allan','Alpha','Anton','Axel','Bazza','Benton','Bernard','Brand','Brett','Cale','Calvin','Carol','Chuck','Chucky','Clay','Cornelius','Crofton','Darden','Dax','Den','Deven','Digby','Don','Douglas','Driscoll','Duane','Duke','Edmund','Elsdon','Freeman','Gabby','Garland','George','Godfrey','Graeme','Grier','Hammond','Harlan','Hendrix','Herman','Hewie','Hugh','Indiana','Ingram','Jackie','Jasper','Jaxon','Jaycob','Jere','Kamden','Kelcey','Kendall','Kevin','Kian','Kieran','Kirby','Lanny','Lawson','Laz','Leland','Levi','Lindon','Linton','Lionel','Lonny','Lucas','Manley','Maverick','Merlyn','Michael','Monty','Murphy','Nate','Ned','Nowell','Odell','Ollie','Osbert','Otto','Paget','Pip','Quintin','Raymund','Ricky','Robert','Ross','Rudolph','Sammy','Scotty','Stacey','Thad','Theodore','Tommy','Trey','Tyson','Val','Vernon','Willis','Wilmer','Winton','Wisdom'];
				break;
			case "f":
				names = ['Adelyn','Alene','Alexa','Aliah','Alyson','Angelica','Annalise','Annora','Azaria','Bessie','Betsy','Bettie','Biddy','Brianne','Camellia','Camille','Camryn','Caroline','Chastity','Chelsea','Chelsey','Cindy','Clematis','Darla','Deb','Debby','Dortha','Eleanora','Eliana','Elsabeth','Elyse','Emerson','Emmeline','Erica','Ettie','Eustacia','Evelyn','Gabrielle','Georgiana','Harper','Harrietta','Haylie','Haze','Hunter','Hyacinth','Indiana','Indie','Jacquetta','Janie','Jannine','Jonquil','Kaelyn','Kam','Khloe','Kolleen','Korrine','Kourtney','Krystine','Lavena','Leeann','Lela','Lesleigh','Lindsie','Lorena','Lucile','Luvinia','Lyn','Lyssa','Madeleine','Marian','Maudie','Maureen','Maxine','Melody','Milani','Misti','Nat','Noelle','Ottoline','Paige','Pauline','Payton','Pearl','Perlie','Petronel','Phebe','Posie','Praise','Rexana','Serena','Sharalyn','Sharla','Shauna','Sky','Sybella','Tracy','Tresha','Trudi','Wallis','Wilda','Wren','Yvette'];
				break;
		}
		names.pushUnique('Aaren','Addison','Alex','Alpha','Andie','Arden','Ariel','Artie','Ashton','Aston','Aubrey','Beau','Bernie','Bertie','Beverly','Bobbie','Brooklyn','Caelan','Cameron','Carol','Cary','Casey','Channing','Charley','Cherokee','Cheyenne','Coby','Codie','Collyn','Cyan','Dale','Dallas','Dana','Darby','Dee','Derby','Devan','Devin','Emmerson','Emory','Finley','Flannery','Florence','Gabby','Garnet','Garnett','Gray','Hadyn','Harlow','Hollis','Jackie','Jade','Jae','Jaiden','Johnnie','Joyce','Justice','Kam','Kelcey','Kelsey','Leslie','Lindsey','Lorin','Lyric','Maitland','Marley','McKinley','Merlyn','Murphy','Nicky','Oakley','Odell','Pacey','Paget','Peyton','Presley','Rain','Raleigh','Reagan','Regan','Reilly','Remington','Robbie','Rory','Royale','Sage','Sam','Schuyler','Selby','Shae','Shaye','Shelly','Skylar','Sloan','Stacey','Stacy','Tayler','Tommie','Tracey','Tristen','Tristin','Val');
		names.delete(usedNames);
		result = names[random(0,names.length - 1)];
		return result;
	}
}

DefineMacroS("generateBabyName", generateBabyName);

window.bulkProduceValue = function(plant, quantity = 250) {
	if(plant !== undefined){
		let baseCost = plant.plant_cost * quantity / 2;
		let seasonBoost = !plant.season.includes(V.season) ? 1.1 : 1;
		return Math.floor(baseCost * seasonBoost);
	}
}

window.pregnancyBellyVisible = function(){
	let size = State.variables.sexStats.vagina.pregnancy.bellySize;
	if(size <= 7) return false;
	if(size <= 11 && State.variables.worn.upper.name !== "naked" && State.variables.worn.upper.type.includes("bellyShow")) return false;
	if(size <= 17 && State.variables.worn.upper.type.includes("bellyHide")) return false;

	return true;
}

window.toTitleCase = function(str) {
    return str.replace(/\w\S*/g, function(txt){
        return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
    });
}

window.getRobinLocation = function(){
	if (V.NPCName[V.NPCNameList.indexOf("Robin")].init !== 1){
		return;

	} else if (V.robinlocationoverride && V.robinlocationoverride.during.contains(V.hour)){
		return T.robin_location = V.robinlocationoverride.location;

	} else if (V.robinmissing === 1){
		return T.robin_location = "missing";

	} else if (!between(V.hour, 7, 20)){ //if hour is 6 or lower, or 21 or higher
		return T.robin_location = "sleep";

	} else if (V.schoolday === 1 && between(V.hour, 8, 15)){
		return T.robin_location = "school";

	} else if ((V.weekday === 7 || V.weekday === 1) && between(V.hour, 9, 16) && V.NPCName[V.NPCNameList.indexOf("Robin")].trauma < 80){
		if (V.season === "winter"){
			return T.robin_location = "park";
		} else {
			return T.robin_location = "beach";
		}

	} else if (V.halloween === 1 && between(V.hour, 16, 18) && V.monthday === 31){
		return T.robin_location = "halloween";

	} else {
		return T.robin_location = "orphanage";
	}
}

window.setRobinLocationOverride = function(loc, hour){
	let override = {
		"location": loc,
		"during": [],
	};
	if (Array.isArray(hour)) override.during = hour;
	else override.during = [hour];

	// note: overrides get reset at midnight (in the <<day>> widget)
	V.robinlocationoverride = override;
	return;
}

window.DefaultActions = {
	create:  function (isMinimal = false, preload = false) {
		let storage = {};
		setup.actionsTypes.combatTypes.forEach(type => {
			storage[type] = {};
			// Decides whether you create all permutations of the structure.
			// Usually set isMinimal if saving to file for reduced memory.
			if (!isMinimal) {
				setup.actionsTypes.personTypes.forEach(person => {
					storage[type][person] = {};
					setup.actionsTypes.actionTypes.forEach(part => {
						if (person === 'Tentacles' && part === 'askActions') {
							// Do not add askActions to tentacle enemies.
							return;
						} else if (person !== 'Tentacles' && part === 'regrab') {
							// Do not add regrab to non-tentacle enemies.
							return;
						}
						storage[type][person][part] = [];
					});
				});
			}
		});
		if (preload) {
			// Load old actions into new structure.
			storage = this.loadOld(V.actionDefaults, storage);
		}
		return storage;
	},
	check: function (storage) {
		if (storage === undefined) {
			return;
		}
		if (storage['consensual'] === undefined || storage['rape'] === undefined) {
			storage = this.create(true, true);
		}
		return storage;
	},
	setup: function (recreate = false) {
		if (recreate || V.actionDefaults === undefined) {
			return this.create(true);
		}
		return V.actionDefaults;
	},
	load: function (from = {}) {
		Object.keys(from).forEach(type => {
			Object.keys(from[type]).forEach(person => {
				Object.keys(from[type][person]).forEach(part => {
					let actions = this.get(type, person, part);
					if (Array.isArray(actions)) {
						actions.forEach(action => {
							from[type][person][part].pushUnique(action);
						});
					} else {
						if (part === 'regrab') {
							let action = actions ? 1 : 0;
							from[type][person][part].pushUnique(action);
						}
					}
				})
			});
		});
		return from;
	},
	loadOld: function (from, to) {
		setup.actionsTypes.personTypes.forEach(person => {
			setup.actionsTypes.combatTypes.forEach(type => {
				setup.actionsTypes.actionTypes.forEach(part => {
					let actions = this.get(person, type, part, from);
					if (Array.isArray(actions)) {
						actions.forEach(action => {
							this.add(type, person, part, action, { value: to });
						});
					} else {
						if (part === 'regrab') {
							let action = actions ? 1 : 0;
							this.add(type, person, part, action, { value: to });
						}
					}
				})
			});
		});
		return to;
	},
	save: function (from, callback = this.add) {
		if (from === undefined) {
			return;
		}
		V.actionDefaults = this.setup(true);
		// Assume the structure is valid.
		let defaultTypes = Object.keys(from);
		defaultTypes.forEach(type => {
			let defaultPeople = Object.keys(from[type]);
			defaultPeople.forEach(person => {
				let defaultParts = Object.keys(from[type][person]);
				defaultParts.forEach(part => {
					let actionSets = from[type][person][part];
					if (actionSets !== undefined) {
						actionSets.forEach(action => {
							if (part === 'regrab') {
								action = action ? 1 : 0;
							}
							callback(type, person, part, action, { value: V.actionDefaults });
						});
					}
				});
			});
		});
	},
	add: function (type, person, part, action, to = { value: V.actionDefaults }) {
		if (action === 'rest') {
			return;
		}
		if (to.value[type][person] === undefined) {
			to.value[type][person] = {};
		}
		if (to.value[type][person][part] === undefined) {
			to.value[type][person][part] = [];
		}
		to.value[type][person][part].pushUnique(action);
	},
	addMany: function (type, person, part, actions, to = { value: V.actionDefaults }) {
		let filteredActions = actions.map(action => {
			if (part === 'regrab') {
				return action ? 1 : 0;
			}
			if (action !== 'rest') {
				return action;
			}
		});
		if (filteredActions.length <= 0) {
			return;
		}
		if (to.value[type][person] === undefined) {
			to.value[type][person] = {};
		}
		if (to.value[type][person][part] === undefined) {
			to.value[type][person][part] = [];
		}
		filteredActions.forEach(action => {
			to.value[type][person][part].pushUnique(action);
		});
	},
	get: function (type, person, part, from = V.actionDefaults, defaultValue = 'rest') {
		if (from[type] === undefined
			|| from[type][person] === undefined
			|| from[type][person][part] === undefined) {
			return [ defaultValue ];
		}
		return from[type][person][part];
	},
	setDefaults: function () {
		V.actionDefaults = this.create(true);
		let type = 'rape';
		this.addMany(type, 'Submissive', 'leftaction', ['leftchest']);
		this.addMany(type, 'Submissive', 'rightaction', ['rightchest']);
		this.addMany(type, 'Submissive', 'mouthaction', ['plead', 'suck', 'kiss', 'breastsuck']);
		this.addMany(type, 'Submissive', 'penisaction', ['tease', 'cooperate']);
		this.addMany(type, 'Submissive', 'vaginaaction', ['penistease', 'cooperate']);
		this.addMany(type, 'Submissive', 'anusaction', ['penistease', 'cooperate']);
		this.addMany(type, 'Submissive', 'feetaction', ['grabrub', 'grabrub', 'vaginagrabrub']);
		this.addMany(type, 'Defiant', 'leftaction', ['lefthit', 'leftstruggle']);
		this.addMany(type, 'Defiant', 'rightaction', ['penwhack', 'righthit', 'rightstruggle']);
		this.addMany(type, 'Defiant', 'mouthaction', ['pullaway', 'bite', 'breastbite', 'headbutt']);
		this.addMany(type, 'Defiant', 'penisaction', ['escape', 'otheranusescape', 'othermouthescape']);
		this.addMany(type, 'Defiant', 'vaginaaction', ['escape', 'othermouthescape']);
		this.addMany(type, 'Defiant', 'anusaction', ['escape', 'othermouthescape']);
		this.addMany(type, 'Defiant', 'feetaction', ['kick']);
		this.addMany(type, 'Tentacles', 'regrab', [0]);
		type = 'consensual';
		this.addMany(type, 'Submissive', 'leftaction', ['leftchest']);
		this.addMany(type, 'Submissive', 'rightaction', ['rightchest']);
		this.addMany(type, 'Submissive', 'mouthaction', ['kiss', 'suck', 'breastsuck', 'breastlick']);
		this.addMany(type, 'Submissive', 'penisaction', ['tease', 'cooperate']);
		this.addMany(type, 'Submissive', 'vaginaaction', ['penistease', 'cooperate']);
		this.addMany(type, 'Submissive', 'anusaction', ['penistease', 'cooperate']);
		this.addMany(type, 'Defiant', 'leftaction', [0]);
		this.addMany(type, 'Defiant', 'rightaction', ['penwhack']);
		this.addMany(type, 'Defiant', 'mouthaction', ['breastpull', 'breastclosed']);
		this.addMany(type, 'Defiant', 'penisaction', ['escape', 'otheranusescape', 'othermouthescape']);
		this.addMany(type, 'Defiant', 'vaginaaction', ['escape', 'othermouthescape']);
		this.addMany(type, 'Defiant', 'anusaction', ['escape', 'othermouthescape']);
		this.addMany(type, 'Tentacles', 'regrab', [0]);
		return V.actionDefaults;
	}
}

window.transferClothing = function(slot, index, newWardrobe){
	let oldWardrobeObject;
	if(V.wardrobe_location === "wardrobe"){
		oldWardrobeObject = V.wardrobe;
	} else {
		oldWardrobeObject = V.wardrobes[V.wardrobe_location];
	}
	let newWardrobeObject;
	if(newWardrobe === "wardrobe"){
		newWardrobeObject = V.wardrobe;
	} else {
		newWardrobeObject = V.wardrobes[newWardrobe];
	}
	if(oldWardrobeObject && newWardrobeObject){
		newWardrobeObject[slot].push(oldWardrobeObject[slot][index]);
		oldWardrobeObject[slot].deleteAt(index);
	}
	return;
}

window.clothingData = function(slot, item, data){
	if(item[data] !== undefined) return item[data];
	return setup.clothes[slot][clothesIndex(slot,item)][data];
}

window.clothesDataTrimmerLoop = function(){
	if(!V.passage || V.passage === "Start") return;
	const wardrobeKeys = Object.keys(V.wardrobes);
	setup.clothes_all_slots.forEach(slot => {
		clothesDataTrimmer(V.worn[slot]);
		clothesDataTrimmer(V.carried[slot]);
		if(Array.isArray(V.wardrobe[slot])){
			V.wardrobe[slot].forEach(item => {
				clothesDataTrimmer(item);
			})
		}
		if(Array.isArray(V.store[slot])){
			V.store[slot].forEach(item => {
				clothesDataTrimmer(item);
			})
		}

		for (let i = 0, l = wardrobeKeys.length; i < l; i++){
			if(Array.isArray(V.wardrobes[wardrobeKeys[i]][slot])){
				V.wardrobes[wardrobeKeys[i]][slot].forEach(item => {
					clothesDataTrimmer(item);
				})
			}
		}
		if(V.tryOn !== undefined){
			if(V.tryOn.ownedStored !== undefined){
				if(V.tryOn.ownedStored[slot] !== undefined && V.tryOn.ownedStored[slot] !== null){
					clothesDataTrimmer(V.tryOn.ownedStored[slot]);
				}
			}
			if(V.tryOn.tryingOn !== undefined){
				if(V.tryOn.tryingOn[slot] !== undefined && V.tryOn.tryingOn[slot] !== null){
					clothesDataTrimmer(V.tryOn.tryingOn[slot]);
				}
			}
		}
	});
}

window.clothesDataTrimmer = function(item){
	if(!item) return;
	const toDelete = ["name_cap","iconFile","accIcon","notuck","skirt","description","colour_options","accessory_colour_options","fabric_strength","integrity_max","bustresize","sleeve_img","breast_img","exposed_base","vagina_exposed_base","anus_exposed_base","state_top_base","state_base","word","femininity","strap","cost","shop","short"];
	//To prevent it from running on variables multiple times, when updating toDelete, the last of the new additions should be added here
	const trimmerVersion = ["shop","short"];
	let version = 0;
	let indexToUpdateVersion = toDelete.indexOf(trimmerVersion[version]);
	toDelete.forEach((v, index) => {
		if(indexToUpdateVersion === -1) {
			//Do Nothing
		} else if(item[v] !== undefined && item[trimmerVersion[version]] !== undefined) {
			delete item[v];
		}
		if(indexToUpdateVersion === index) {
			version++;
			indexToUpdateVersion = toDelete.indexOf(trimmerVersion[version])
		}
	});
}

window.clothesReturnLocation = function(item, type){
	if(!V.multipleWardrobes) return "wardrobe";
	let isolated = ["asylum","prison"];
	let lastTaken = item.lastTaken;
	if (!lastTaken || (V.multipleWardrobes !== "all" && !isolated.includes(lastTaken))
		|| !V.wardrobes[lastTaken]
		|| !V.wardrobes[lastTaken].unlocked) {
		lastTaken = 'wardrobe';
	}
	switch(type){
		case "rebuy":
			switch(V.location){
				case "asylum":
					if(item.type.includes("asylum")){
						return "asylum";
					}
					if(!isolated.includes(lastTaken)){
						return lastTaken;
					}
				case "prison":
					if(item.type.includes("prison")){
						return "prison";
					}
					if(!isolated.includes(lastTaken)){
						return lastTaken;
					}
				default:
					if(!isolated.includes(lastTaken)){
						return lastTaken;
					}
			}
		default:
			switch(V.location){
				case "asylum":
					return "asylum";
				case "prison":
					return "prison";
				default:
					if(!isolated.includes(lastTaken)){
						return lastTaken;
					}
			}
	}
	return "wardrobe";
}

//the 'modder' variable is specifically for modders name, should be kept as a short string
window.clothesIndex = function(slot, itemToIndex) {
	if(!slot || !itemToIndex || !itemToIndex.name) {
		console.log(`clothesIndex - slot or valid object not provided`);
		return 0;
	}
	let index = setup.clothes[slot].findIndex((item) => item.variable === itemToIndex.variable && item.modder === itemToIndex.modder)
	if(index === -1){
		console.log(`clothesIndex - ${slot} clothing item index not found for the '${itemToIndex.name}' with the modder set to '${itemToIndex.modder}'`);
		return 0;
	}
	return index;
}

// Runs before a passage load, returning a string redirects to the new passage name.
Config.navigation.override = function (dest) {
	switch (dest) {
		case 'Forest Shop Outfit':
		case 'Forest Shop Upper':
		case 'Forest Shop Lower':
		case 'Forest Shop Under Outfit':
		case 'Forest Shop Under Upper':
		case 'Forest Shop Under Lower':
		case 'Forest Shop Head':
		case 'Forest Shop Face':
		case 'Forest Shop Neck':
		case 'Forest Shop Legs':
		case 'Forest Shop Feet':
			return 'Forest Shop';

		case 'Over Outfit Shop':
		case 'Outfit Shop':
		case 'Top Shop':
		case 'Bottom Shop':
		case 'Under Outfit Shop':
		case 'Under Top Shop':
		case 'Under Bottom Shop':
		case 'Head Shop':
		case 'Face Shop':
		case 'Neck Shop':
		case 'Hands Shop':
		case 'Legs Shop':
		case 'Shoe Shop':
			return 'Clothing Shop';

		case 'Penis Inspection Flaunt Crossdress':
			return 'Penis Inspection Flaunt No Penis';

		case 'Pussy Inspection2':
			return 'Pussy Inspection 2';

		case 'Pussy Inspection Penis':
			return 'Pussy Inspection Flaunt No Pussy';

		case 'Forest Plant Sex No Tentacles':
			return 'Forest Plant Sex';

		case 'Forest Plant Sex No Tentacles Finish':
			return 'Forest Plant Sex Finish';

		case 'Forest Plant Passout No Tentacles':
			return 'Forest';

		case 'Moor Plant Sex No Tentacles':
			return 'Moor Plant Sex';

		case 'Moor Plant Sex No Tentacles Finish':
			return 'Moor Plant Sex Finish'

		case 'Underground Plant Molestation No Tentacles':
			return 'Underground Plant Molestation';

		case 'Underground Plant Molestation No Tentacles Finish':
			return 'Underground Plant Molestation Finish';

		default:
			return dest;
	}
}

window.currentSkillValue = function(skill){
    let result = V[skill];
    if(!result) {
        console.log(`currentSkillValue - skill '${skill}' unknown`);
        return 0;
    };
    if(['skulduggery','physique','danceskill','swimmingskill','athletics','willpower','tending','english'].includes(skill) && V.moorLuck > 0){
        result = Math.floor(result * (1 + (V.moorLuck / 100)));
    }
    switch(skill){
        case 'tending':
            if(V.backgroundTraits.includes("plantlover")){
                result = Math.floor(result * (1 + (V.trauma / (V.traumamax * 2))));
            }
        break;
    }
    return result;
}


code:
/*	allow forced redirection on link click
 *	when `V.nextPassage` is set, all links to other passages will lead to the one specified by it
 *	code within the links will not be executed
 *	the varible is unset before the next passage is rendered */
Macro.delete(['button', 'link'])
Macro.add(['button', 'link'], {
	isAsync : true,
	tags    : null,

	handler() {
		if (this.args.length === 0) {
			return this.error(`no ${this.name === 'button' ? 'button' : 'link'} text specified`);
		}

		const $link = jQuery(document.createElement(this.name === 'button' ? 'button' : 'a'));
		let passage;

		if (typeof this.args[0] === 'object') {
			if (this.args[0].isImage) {
				// Argument was in wiki image syntax.
				const $image = jQuery(document.createElement('img'))
					.attr('src', this.args[0].source)
					.appendTo($link);

				if (this.args[0].hasOwnProperty('passage')) {
					$image.attr('data-passage', this.args[0].passage);
				}

				if (this.args[0].hasOwnProperty('title')) {
					$image.attr('title', this.args[0].title);
				}

				if (this.args[0].hasOwnProperty('align')) {
					$image.attr('align', this.args[0].align);
				}

				passage = this.args[0].link;
			}
			else {
				// Argument was in wiki link syntax.
				$link.append(document.createTextNode(this.args[0].text));
				passage = this.args[0].link;
			}
		}
		else {
			// Argument was simply the link text.
			$link.wikiWithOptions({ profile : 'core' }, this.args[0]);
			passage = this.args.length > 1 ? this.args[1] : undefined;
		}

		if (passage != null) { // lazy equality for null
			$link.attr('data-passage', passage);

			if (Story.has(passage)) {
				$link.addClass('link-internal');

				if (Config.addVisitedLinkClass && State.hasPlayed(passage)) {
					$link.addClass('link-visited');
				}
			}
			else {
				$link.addClass('link-broken');
			}
		}
		else {
			$link.addClass('link-internal');
		}

		$link
			.addClass(`macro-${this.name}`)
			.ariaClick({
				namespace : '.macros',
				one       : passage != null // lazy equality for null
			}, this.createShadowWrapper(
				this.payload[0].contents !== ''
					/* don't execute linked code unless they don't lead to another passage (menus, etc) */
					? () => { if (!(passage && V.nextPassage)) {Wikifier.wikifyEval(this.payload[0].contents.trim())} }
					: null,
				passage != null // lazy equality for null
					? () => {
						//check V.nextPassage and redirect all links to it if present
						if (V.nextPassage){ V.nextPassageIntended = passage; passage = V.nextPassage; delete V.nextPassage };
						//save sidebar scrolling position
						window.scroll_uibar = document.querySelector("#storyCaptionDiv").scrollTop;
						//if passage hasn't changed (i.e. during combat), store scrolling position
						window.scroll_main = (V.passage === V.passagePrev ? document.scrollingElement.scrollTop : 0);
						//finally, play the passage
						Engine.play(passage) }
					: null
			))
			.appendTo(this.output);
	}
});

/* required change is within Wikifier class, but since redefining it entirely would not be feasible,
* we have to redirect `Wikifier.createInternalLink` to a local modified version
* ideally, this should be handled within sugarcube itself */
Wikifier.Parser.delete('link')
Wikifier.Parser.add({
	name     : 'link',
	profiles : ['core'],
	match    : '\\[\\[[^[]',

	handler(w) {
		const markup = Wikifier.helpers.parseSquareBracketedMarkup(w);

		if (markup.hasOwnProperty('error')) {
			w.outputText(w.output, w.matchStart, w.nextMatch);
			return;
		}

		w.nextMatch = markup.pos;

		// text=(text), forceInternal=(~), link=link, setter=(setter)
		const link  = Wikifier.helpers.evalPassageId(markup.link);
		const text  = markup.hasOwnProperty('text') ? Wikifier.helpers.evalText(markup.text) : link;
		const setFn = markup.hasOwnProperty('setter')
			? Wikifier.helpers.createShadowSetterCallback(Scripting.parse(markup.setter))
			: null

			// Debug view setup.
		const output = (Config.debug
			? new DebugView(w.output, 'link-markup', '[[link]]', w.source.slice(w.matchStart, w.nextMatch))
			: w
		).output;

		if (markup.forceInternal || !Wikifier.isExternalLink(link)) {
			/* Wikifier.createInternalLink(output, link, text, setFn); */
			/* replace with a local version */
			createInternalLink(output, link, text, setFn);
		}
		else {
			Wikifier.createExternalLink(output, link, text);
		}
	}
});

function createInternalLink(destination, passage, text, callback){
	const $link = jQuery(document.createElement('a'));

	if (passage != null) { // lazy equality for null
		$link.attr('data-passage', passage);

		if (Story.has(passage)) {
			$link.addClass('link-internal');

			if (Config.addVisitedLinkClass && State.hasPlayed(passage)) {
				$link.addClass('link-visited');
			}
		}
		else {
			$link.addClass('link-broken');
		}

		$link.ariaClick({ one : true }, () => {
			if (typeof callback === 'function') {
				callback();
			}
			//check V.nextPassage and redirect all links to it if present
			if (V.nextPassage){
				V.nextPassageIntended = passage;
				passage = V.nextPassage;
				delete V.nextPassage;
			}
			//save sidebar scrolling position
			window.scroll_uibar = document.querySelector("#storyCaptionDiv").scrollTop;
			//if passage hasn't changed (i.e. during combat), store scrolling position
			window.scroll_main = (V.passage === V.passagePrev ? document.scrollingElement.scrollTop : 0);
			Engine.play(passage);
		});
	}

	if (text) {
		$link.append(document.createTextNode(text));
	}

	if (destination) {
		$link.appendTo(destination);
	}

	// For legacy-compatibility we must return the DOM node.
	return $link[0];

}

repo_name: Area/degrees

license: mit


code:
window.checkNewVersion = function() {
	const API_key = "AIzaSyCu0F1pUOocM6R0wJ4tmug6SPTX19JRVQc"; // Google API key, restricted to Blogspot
	const dol_blog_id = "7051760000701686365"; // Blogspot BlogID

	if (State.variables.notifyUpdate == undefined)
		State.variables.notifyUpdate = true;

	if (State.variables.notifyUpdate) {
		// get the list of last 10 posts and check their titles to find latest version
		$.ajax({
			url: "https://www.googleapis.com/blogger/v3/blogs/" + dol_blog_id + "/posts?key=" + API_key,
			dataType: "jsonp",
			success: handleVersionResponse
		});
	}
}

function handleVersionResponse(response) {
	const titleRE = new RegExp("Degrees of Lewdity\\.*? version (\\d+\\.\\d+(?:\\.\\d+(?:\\.\\d+)?)?)");
	let version, postDate;

	// check if response is ok
	if ("items" in response) {
		// find the newest post with a title that matches new update title string
		for (let i = 0; i < response.items.length; i++) {
			if (titleRE.test(response.items[i].title)) {
				version = titleRE.exec(response.items[i].title)[1];
				postDate = new Date(Date.parse(response.items[i].updated || response.items[i].published));
				break;
			}
		}

		// make sure that such post was found within last 10 posts
		if (version && postDate) {
			let hours = Math.floor((Date.now() - postDate) / 1000 / 60 / 60); // hours since latest release
			let days = Math.floor(hours / 24); // whole days since latest release

			// check if latest version is newer than the current one
			if (version > StartConfig.version) {
				if (!State.variables.newVersionDismissed || (State.variables.newVersionDismissed && State.variables.remoteDoLVersion != version)) {
					// create data for notification
					State.variables.newVersionData = { version: version, days: days, hours: hours };
					delete State.variables.newVersionDismissed;

					// show notification if one isn't there already
					if ($('#new-version-notification').length == 0)
						$('.passage').prepend((new Wikifier(null, '<<newversionnotification>>')).output);
				}
				else {
					delete State.variables.newVersionData;
				}
				State.variables.remoteDoLVersion = version;
			}
		}
	}
}


code:
/**
 * ############# PURE EVIL ###################
 * Overwrites JSON.parse and JSON.stringify to
 * encode object class information.
 *
 * We make a best attempt to support all JSON features
 * and be as unobtrusive as possible, but still, monkey
 * patching is dangerous
 *
 * User beware. Here be dragons
 */

JSON.debug = {};

// Keep the old versions around
JSON.debug.parse = JSON.parse.bind(JSON);
JSON.debug.stringify = JSON.stringify.bind(JSON);

// We use this to prevent us from spamming the log if a
// specific class isn't able to be found in the serializer
// Also contains a count of the number of non-serialized object
// for a given classname/path
JSON.debug.stringifyMissingClasses = {}
JSON.debug.stringifyClasses = {}
JSON.debug.parseMissingClasses = {}
JSON.debug.parseClasses = {}

JSON.proto = Object.freeze({
  key: "__json_class__",
  path: "__json_class_path__",
  reviver: "__json_class_reviver__",
});
JSON.stringify = function (o, userReplacer, ...args) {
  return JSON.debug.stringify.call(this, o, (key, val) => {
	let nextVal = val;
	try {
	  if (val && typeof val === "object" && val.constructor !== Object && !Array.isArray(val)) {
		const classCtor = Object.getPrototypeOf(val).constructor;
		const classPath = classCtor.prototype[JSON.proto.path] || classCtor.name;
		if (JSON.debug.getFromPath(classPath)) {
		  nextVal = {
			[JSON.proto.key]: classPath,
			...val
		  }
		  if (!JSON.debug.stringifyClasses[classPath]) {
			console.log(`Serializing object with class ${classPath}`);
			JSON.debug.stringifyClasses[classPath] = 0;
		  }
		  JSON.debug.stringifyClasses[classPath]++;
		} else {
		  if (!JSON.debug.stringifyMissingClasses[classPath]) {
			console.warn(`Serializing object with class ${classPath} failed. Skipping objects like`, val);
			JSON.debug.stringifyMissingClasses[classPath] = 0;
		  }
		  JSON.debug.stringifyMissingClasses[classPath]++;
		}
	  }
	} catch (e) {
	  console.error(`Critical error occurred in JSON.parse. Attempting to continue`, e);
	} finally {
	  return userReplacer ? userReplacer(key, nextVal) : nextVal;
	}
  }, ...args)
}
JSON.debug.getFromPath = function getFromPath(path) {
  return (path || '').split('.').reduce((nextObj, nextPathPart) => {
	return (nextObj || {})[nextPathPart]
  }, window);
}

JSON.parse = function (string, userReviver, ...args) {
  return JSON.debug.parse.call(this, string, (key, val) => {
	let nextVal = val;
	const className = val[JSON.proto.key]
	if (className) {
	  const maybeClass = JSON.debug.getFromPath(className)
	  if (maybeClass && (typeof maybeClass === 'object' || typeof maybeClass === 'function')) {
		nextVal = JSON.revive(maybeClass, val);
		if (!JSON.debug.parseClasses[className]) {
		  console.log(`Revived object with class ${className}`, maybeClass);
		  JSON.debug.parseClasses[className] = 0;
		}
		JSON.debug.parseClasses[className]++;
	  } else {
		if (!JSON.debug.parseMissingClasses[className]) {
		  console.warn(`Parsing instance of ${className} failed. Skipping objects like`, val, maybeClass);
		  JSON.debug.parseMissingClasses[className] = 0;
		}
		JSON.debug.parseMissingClasses[className]++;
	  }
	}
	delete nextVal[JSON.proto.key];
	return userReviver ? userReviver(key, nextVal) : nextVal;
  }, ...args);
}
JSON.revive = function (reviverClass, data) {
  let nextVal = Object.create(reviverClass.prototype, Object.entries(data).reduce((acc, [key, val]) => {
	acc[key] = { value: val, configurable: true, writeable: true, enumerable: true }
	return acc
  }, {}));
  if (reviverClass[JSON.proto.reviver]) {
	try {
	  nextVal = reviverClass[JSON.proto.reviver].call(nextVal);
	} catch (e) {
	  console.error(`Failed to revive custom class ${reviverClass.name}`, e)
	}
  }
  return nextVal;
}


code:
window.prepareSaveDetails = function (forceRun){
	if("dolSaveDetails" in localStorage === false || forceRun === true){
		var saveDetails = {autosave: null, slots:[null,null,null,null,null,null,null,null]}
		var SugarCubeSaveDetails = Save.get();
		if(SugarCubeSaveDetails.autosave != null){
			saveDetails.autosave = {
				title:SugarCubeSaveDetails.autosave.title,
				date:SugarCubeSaveDetails.autosave.date,
				metadata:SugarCubeSaveDetails.autosave.metadata
			}
			if(saveDetails.autosave.metadata === undefined){
				saveDetails.autosave.metadata = {saveName:""};
			}
			if(saveDetails.autosave.metadata.saveName === undefined){
				saveDetails.autosave.metadata.saveName = "";
			}
		}
		for (var i=0; i<SugarCubeSaveDetails.slots.length;i++){
			if(SugarCubeSaveDetails.slots[i] !== null){
				saveDetails.slots[i] = {
					title:SugarCubeSaveDetails.slots[i].title,
					date:SugarCubeSaveDetails.slots[i].date,
					metadata:SugarCubeSaveDetails.slots[i].metadata
				};
				if(saveDetails.slots[i].metadata === undefined){
					saveDetails.slots[i].metadata = {saveName:"old save", saveId:0}
				}
				if(saveDetails.slots[i].metadata.saveName === undefined){
					saveDetails.slots[i].metadata.saveName = "old save";
				}
			}else{
				saveDetails.slots[i] = null;
			}
		}

		localStorage.setItem("dolSaveDetails" ,JSON.stringify(saveDetails));
	}
	return;
}

window.setSaveDetail = function (saveSlot, metadata, story){
	var saveDetails = JSON.parse(localStorage.getItem("dolSaveDetails"));
	if(saveSlot === "autosave"){
		saveDetails.autosave = {
			title:SugarCube.Story.get(V.passage).description(),
			date:Date.now(),
			metadata:metadata
		};
	}else{
		var slot = parseInt(saveSlot);
		saveDetails.slots[slot] = {
			title:SugarCube.Story.get(V.passage).description(),
			date:Date.now(),
			metadata:metadata
		};
	}
	localStorage.setItem("dolSaveDetails" ,JSON.stringify(saveDetails));
}

window.getSaveDetails = function (saveSlot){
	if("dolSaveDetails" in localStorage) return JSON.parse(localStorage.getItem("dolSaveDetails"));
}

window.deleteSaveDetails = function (saveSlot){
	var saveDetails = JSON.parse(localStorage.getItem("dolSaveDetails"));
	if(saveSlot === "autosave"){
		saveDetails.autosave = null;
	}else{
		var slot = parseInt(saveSlot);
		saveDetails.slots[slot] = null;
	}
	localStorage.setItem("dolSaveDetails" ,JSON.stringify(saveDetails));
}

window.deleteAllSaveDetails = function (saveSlot){
	var saveDetails = {autosave: null, slots:[null,null,null,null,null,null,null,null]};
	localStorage.setItem("dolSaveDetails" ,JSON.stringify(saveDetails));
}

window.returnSaveDetails = function () {
	return Save.get();
}

window.resetSaveMenu = function () {
	new Wikifier(null, '<<resetSaveMenu>>');
}

window.loadSave = function (saveSlot, confirm) {
	if (V.confirmLoad === true && confirm === undefined) {
		new Wikifier(null, '<<loadConfirm ' + saveSlot + '>>');
	} else {
		if (saveSlot === "auto") {
			Save.autosave.load();
		} else {
			Save.slots.load(saveSlot);
		}
	}
}

window.save = function (saveSlot, confirm, saveId, saveName) {
	if (saveId == null) {
		new Wikifier(null, '<<saveConfirm ' + saveSlot + '>>');
	} else if ((V.confirmSave === true && confirm != true) || (V.saveId != saveId && saveId != null)) {
		new Wikifier(null, '<<saveConfirm ' + saveSlot + '>>');
	} else {
		if (saveSlot != undefined) {
			updateSavesCount();
			Save.slots.save(saveSlot, null, { "saveId": saveId, "saveName": saveName });
			setSaveDetail(saveSlot, { "saveId": saveId, "saveName": saveName })
			V.currentOverlay = null;
			overlayShowHide("customOverlay");
		}
	}
}

window.deleteSave = function (saveSlot, confirm) {
	if (saveSlot === "all") {
		if (confirm === undefined) {
			new Wikifier(null, '<<clearSaveMenu>>');
			return;
		} else if (confirm === true) {
			Save.clear();
			deleteAllSaveDetails();
		}
	} else if (saveSlot === "auto") {
		if (V.confirmDelete === true && confirm === undefined) {
			new Wikifier(null, '<<deleteConfirm ' + saveSlot + '>>');
			return;
		} else {
			Save.autosave.delete();
			deleteSaveDetails("autosave");
		}
	} else {
		if (V.confirmDelete === true && confirm === undefined) {
			new Wikifier(null, '<<deleteConfirm ' + saveSlot + '>>');
			return;
		} else {
			Save.slots.delete(saveSlot);
			deleteSaveDetails(saveSlot)
		}
	}
	new Wikifier(null, '<<resetSaveMenu>>');
}

window.importSave = function (saveFile) {
	if (!window.FileReader) return; // Browser is not compatible

	var reader = new FileReader();

	reader.onloadend = function () {
		DeserializeGame(this.result);
	}

	reader.readAsText(saveFile[0]);
}

window.SerializeGame = function () { return Save.serialize(); }; window.DeserializeGame = function (myGameState) { return Save.deserialize(myGameState) };

window.getSaveData = function () {
	var input = document.getElementById("saveDataInput");
	updateExportDay();
	input.value = Save.serialize();
}

window.loadSaveData = function () {
	var input = document.getElementById("saveDataInput");
	var result = Save.deserialize(input.value);
	if (result === null) {
		input.value = "Invalid Save."
	}
}

window.clearTextBox = function (id) {
	document.getElementById(id).value = "";
}

window.topTextArea = function (id) {
	var textArea = document.getElementById(id);
	textArea.scroll(0, 0);
}

window.bottomTextArea = function (id) {
	var textArea = document.getElementById(id);
	textArea.scroll(0, textArea.scrollHeight);
}

window.copySavedata = function (id) {
	var saveData = document.getElementById(id);
	saveData.focus();
	saveData.select();

	try {
		var successful = document.execCommand('copy');
	} catch (err) {
		var copyTextArea = document.getElementById("CopyTextArea");
		copyTextArea.value = "Copying Error";
		console.log('Unable to copy: ', err);
	}
}

window.copySavedata = function (id) {
	var saveData = document.getElementById(id);
	saveData.focus();
	saveData.select();

	try {
		var successful = document.execCommand('copy');
	} catch (err) {
		var copyTextArea = document.getElementById("CopyTextArea");
		copyTextArea.value = "Copying Error";
		console.log('Unable to copy: ', err);
	}
}

window.updateExportDay = function(){
	if(V.saveDetails != undefined && SugarCube.State.history[0].variables.saveDetails != undefined){
		V.saveDetails.exported.days = clone(V.days);
		SugarCube.State.history[0].variables.saveDetails.exported.days = clone(SugarCube.State.history[0].variables.days);
		V.saveDetails.exported.count++;
		SugarCube.State.history[0].variables.saveDetails.exported.count++;
		V.saveDetails.exported.dayCount++;
		SugarCube.State.history[0].variables.saveDetails.exported.dayCount++;
		var sessionJson = sessionStorage.getItem(SugarCube.Story.domId + ".state");
		if(sessionJson != undefined){
			var session = JSON.parse(sessionJson);
			session.delta[0].variables.saveDetails.exported.days = clone(V.days);
			session.delta[0].variables.saveDetails.exported.dayCount++;
			session.delta[0].variables.saveDetails.exported.count++;
			sessionStorage.setItem(SugarCube.Story.domId + ".state", JSON.stringify(session));
		}
	}
}

window.updateSavesCount = function(){
	if(V.saveDetails != undefined && SugarCube.State.history[0].variables.saveDetails != undefined){
		V.saveDetails.slot.count++;
		SugarCube.State.history[0].variables.saveDetails.slot.count++;
		V.saveDetails.slot.dayCount++;
		SugarCube.State.history[0].variables.saveDetails.slot.dayCount++;
		var sessionJson = sessionStorage.getItem(SugarCube.Story.domId + ".state");
		if(sessionJson != undefined){
			var session = JSON.parse(sessionJson);
			session.delta[0].variables.saveDetails.slot.dayCount++;
			session.delta[0].variables.saveDetails.slot.count++;
			sessionStorage.setItem(SugarCube.Story.domId + ".state", JSON.stringify(session));
		}
	}
}

window.importSettings = function (data, type) {
	switch(type){
		case "text":
			V.importString = document.getElementById("settingsDataInput").value
			new Wikifier(null, '<<displaySettings "importConfirmDetails">>');
			break;
		case "file":
			var reader = new FileReader();
			reader.addEventListener('load', function (e) {
				V.importString = e.target.result;
				new Wikifier(null, '<<displaySettings "importConfirmDetails">>');
			});
			reader.readAsBinaryString(data[0]);
			break;
		case "function":
			importSettingsData(data);
			break;
	}
}

var importSettingsData = function (data) {
	var S = null;
	var result = data;
	if (result != null && result != undefined) {
		//console.log("json",JSON.parse(result));
		S = JSON.parse(result);
		if (V.passage === "Start" && S.starting != undefined) {
			S.starting = settingsConvert(false, "starting", S.starting)
		}
		if(S.general != undefined){
			S.general = settingsConvert(false, "general", S.general)
		}

		if (V.passage === "Start" && S.starting != undefined) {
			var listObject = settingsObjects("starting");
			var listKey = Object.keys(listObject);
			var namedObjects = ["player", "skinColor"];

			for (var i = 0; i < listKey.length; i++) {
				if (namedObjects.includes(listKey[i]) && S.starting[listKey[i]] != undefined) {
					var itemKey = Object.keys(listObject[listKey[i]]);
					for (var j = 0; j < itemKey.length; j++) {
						if (V[listKey[i]][itemKey[j]] != undefined && S.starting[listKey[i]][itemKey[j]] != undefined) {
							if (validateValue(listObject[listKey[i]][itemKey[j]], S.starting[listKey[i]][itemKey[j]])) {
								V[listKey[i]][itemKey[j]] = S.starting[listKey[i]][itemKey[j]];
							}
						}
					}
				} else if (!namedObjects.includes(listKey[i])) {
					if (V[listKey[i]] != undefined && S.starting[listKey[i]] != undefined) {
						if (validateValue(listObject[listKey[i]], S.starting[listKey[i]])) {
							V[listKey[i]] = S.starting[listKey[i]];
						}
					}
				}
			}
		}

		if (S.general != undefined) {
			var listObject = settingsObjects("general");
			var listKey = Object.keys(listObject);
			var namedObjects = ["map", "skinColor", "shopDefaults"];

			for (var i = 0; i < listKey.length; i++) {
				if (namedObjects.includes(listKey[i]) && S.general[listKey[i]] != undefined) {
					var itemKey = Object.keys(listObject[listKey[i]]);
					for (var j = 0; j < itemKey.length; j++) {
						if (V[listKey[i]][itemKey[j]] != undefined && S.general[listKey[i]][itemKey[j]] != undefined) {
							if (validateValue(listObject[listKey[i]][itemKey[j]], S.general[listKey[i]][itemKey[j]])) {
								V[listKey[i]][itemKey[j]] = S.general[listKey[i]][itemKey[j]];
							}
						}
					}
				} else if (!namedObjects.includes(listKey[i])) {
					if (V[listKey[i]] != undefined && S.general[listKey[i]] != undefined) {
						if (validateValue(listObject[listKey[i]], S.general[listKey[i]])) {
							V[listKey[i]] = S.general[listKey[i]];
						}
					}
				}
			}
		}

		if (S.npc != undefined) {
			var listObject = settingsObjects("npc");
			var listKey = Object.keys(listObject);
			for (var i = 0; i < V.NPCNameList.length; i++) {
				if (S.npc[V.NPCNameList[i]] != undefined) {
					for (var j = 0; j < listKey.length; j++) {
						//Overwrite to allow for "none" default value in the start passage to allow for rng to decide
						if (V.passage === "Start" && ["pronoun","gender"].includes(listKey[j]) && S.npc[V.NPCNameList[i]][listKey[j]] === "none"){
							V.NPCName[i][listKey[j]] = S.npc[V.NPCNameList[i]][listKey[j]];
						}
						else if (validateValue(listObject[listKey[j]], S.npc[V.NPCNameList[i]][listKey[j]])) {
							V.NPCName[i][listKey[j]] = S.npc[V.NPCNameList[i]][listKey[j]];
						}
					}
				}
			}
		}
	}
}

window.validateValue = function (keys, value) {
	//console.log("validateValue",keys,value);
	var keyArray = Object.keys(keys);
	var valid = false;
	if (keyArray.length === 0) {
		valid = true;
	}
	if (keyArray.includes("min")) {
		if (keys.min <= value && keys.max >= value) {
			valid = true;
		}
	}
	if (keyArray.includes("decimals") && value != undefined) {
		if (value.toFixed(keys.decimals) != value) {
			valid = false;
		}
	}
	if (keyArray.includes("bool")) {
		if (value === true || value === false) {
			valid = true;
		}
	}
	if (keyArray.includes("boolLetter")) {
		if (value === "t" || value === "f") {
			valid = true;
		}
	}
	if (keyArray.includes("strings") && value != undefined) {
		if (keys.strings.includes(value)) {
			valid = true;
		}
	}
	return valid;
}

window.exportSettings = function (data, type) {
	var S = {
		general: {
			map: {},
			skinColor: {},
			shopDefaults: {},
		},
		npc: {}
	};
	if (V.passage === "Start") {
		S.starting = {
			player: {},
			skinColor: {},
		};
		var listObject = settingsObjects("starting");
		var listKey = Object.keys(listObject);
		var namedObjects = ["player", "skinColor"];

		for (var i = 0; i < listKey.length; i++) {
			if (namedObjects.includes(listKey[i]) && V[listKey[i]] != undefined) {
				var itemKey = Object.keys(listObject[listKey[i]]);
				for (var j = 0; j < itemKey.length; j++) {
					if (V[listKey[i]][itemKey[j]] != undefined) {
						if (validateValue(listObject[listKey[i]][itemKey[j]], V[listKey[i]][itemKey[j]])) {
							S.starting[listKey[i]][itemKey[j]] = V[listKey[i]][itemKey[j]];
						}
					}
				}
			} else if (!namedObjects.includes(listKey[i])) {
				if (V[listKey[i]] != undefined) {
					if (validateValue(listObject[listKey[i]], V[listKey[i]])) {
						S.starting[listKey[i]] = V[listKey[i]];
					}
				}
			}
		}
	}

	var listObject = settingsObjects("general");
	var listKey = Object.keys(listObject);
	var namedObjects = ["map", "skinColor", "shopDefaults"];

	for (var i = 0; i < listKey.length; i++) {
		if (namedObjects.includes(listKey[i]) && V[listKey[i]] != undefined) {
			var itemKey = Object.keys(listObject[listKey[i]]);
			for (var j = 0; j < itemKey.length; j++) {
				if (V[listKey[i]][itemKey[j]] != undefined) {
					if (validateValue(listObject[listKey[i]][itemKey[j]], V[listKey[i]][itemKey[j]])) {
						S.general[listKey[i]][itemKey[j]] = V[listKey[i]][itemKey[j]];
					}
				}
			}
		} else if (!namedObjects.includes(listKey[i])) {
			if (V[listKey[i]] != undefined) {
				if (validateValue(listObject[listKey[i]], V[listKey[i]])) {
					S.general[listKey[i]] = V[listKey[i]];
				}
			}
		}
	}
	var listObject = settingsObjects("npc");
	var listKey = Object.keys(listObject);
	for (var i = 0; i < V.NPCNameList.length; i++) {
		S.npc[V.NPCNameList[i]] = {};
		for (var j = 0; j < listKey.length; j++) {
			//Overwrite to allow for "none" default value in the start passage to allow for rng to decide
			if (V.passage === "Start" && ["pronoun","gender"].includes(listKey[i]) && V.NPCName[i][listKey[j]] === "none"){
				S.npc[V.NPCNameList[i]][listKey[j]] = V.NPCName[i][listKey[j]];
			}
			else if (validateValue(listObject[listKey[j]], V.NPCName[i][listKey[j]])) {
				S.npc[V.NPCNameList[i]][listKey[j]] = V.NPCName[i][listKey[j]];
			}
		}
	}

	if (V.passage === "Start") {
		S.starting = settingsConvert(true, "starting", S.starting)
	}
	S.general = settingsConvert(true, "general", S.general)

	//console.log(S);
	var result = JSON.stringify(S);
	if (type === "text") {
		var textArea = document.getElementById("settingsDataInput");
		textArea.value = result;
	}
	else if (type === "file") {
		var blob = new Blob([result], { type: "text/plain;charset=utf-8" });
		saveAs(blob, "DolSettingsExport.txt");
	}
}

window.settingsObjects = function (type) {
	var result = undefined;
	/*boolLetter type also requires the bool type aswell*/
	switch (type) {
		case "starting":
			result = {
				bodysize: { min: 0, max: 3, decimals: 0 },
				penissize: { min: 0, max: 3, decimals: 0 },
				breastsize: { min: 0, max: 4, decimals: 0 },
				bottomsize: { min: 0, max: 3, decimals: 0 },
				breastsensitivity: { min: 0, max: 5, decimals: 0 },
				genitalsensitivity: { min: 0, max: 5, decimals: 0 },
				eyeselect: { strings: ["purple", "dark blue", "light blue", "amber", "hazel", "green", "red", "pink", "grey", "random"] },
				hairselect: { strings: ["red", "jetblack", "black", "brown", "softbrown", "lightbrown", "burntorange", "blond", "softblond", "platinumblond", "ashyblond", "strawberryblond", "ginger", "random"] },
				hairlength: { min: 0, max: 400, decimals: 0 },
				awareselect: { strings: ["innocent", "knowledgeable"] },
				background: { strings: ["waif", "nerd", "athlete", "delinquent", "promiscuous", "exhibitionist", "deviant", "beautiful", "crossdresser", "lustful", "greenthumb", "plantlover"] },
				gamemode: { strings: ["normal", "soft", "hard"] },
				player: {
					gender: { strings: ["m", "f", "h"] },
					gender_body: { strings: ["m", "f", "a"] },
					ballsExist: { bool: true },
					freckles: { bool: true, strings: ["random"] },
				},
				skinColor: {
					natural: { strings: ["light", "medium", "dark", "gyaru", "ylight", "ymedium", "ydark", "ygyaru"] },
					range: { min: 0, max: 100, decimals: 0 },
				}
			};
			break;
		case "general":
			result = {
				malechance: { min: 0, max: 100, decimals: 0 },
				dgchance: { min: 0, max: 100, decimals: 0 },
				cbchance: { min: 0, max: 100, decimals: 0 },
				malevictimchance: { min: 0, max: 100, decimals: 0 },
				homochance: { min: 0, max: 100, decimals: 0 },
				breast_mod: { min: -12, max: 12, decimals: 0 },
				penis_mod: { min: -8, max: 8, decimals: 0 },
				whitechance: { min: 0, max: 100, decimals: 0 },
				blackchance: { min: 0, max: 100, decimals: 0 },
				straponchance: { min: 0, max: 100, decimals: 0 },
				alluremod: { min: 0.2, max: 2, decimals: 1 },
				clothesPrice: { min: 1, max: 10, decimals: 1 },
				clothesPriceUnderwear: { min: 1, max: 2, decimals: 1 },
				clothesPriceSchool: { min: 1, max: 2, decimals: 1 },
				clothesPriceLewd: { min: 0.1, max: 2, decimals: 1 },
				rentmod: { min: 0.1, max: 3, decimals: 1 },
				beastmalechance: { min: 0, max: 100, decimals: 0 },
				monsterchance: { min: 0, max: 100, decimals: 0 },
				monsterhallucinations: { boolLetter: true, bool: true },
				blackwolfmonster: { min: 0, max: 2, decimals: 0 },
				bestialitydisable: { boolLetter: true, bool: true },
				swarmdisable: { boolLetter: true, bool: true },
				slimedisable: { boolLetter: true, bool: true },
				voredisable: { boolLetter: true, bool: true },
				tentacledisable: { boolLetter: true, bool: true },
				analdisable: { boolLetter: true, bool: true },
				analdoubledisable: { boolLetter: true, bool: true },
				analingusdisablegiving: { boolLetter: true, bool: true },
				analingusdisablereceiving: { boolLetter: true, bool: true },
				vaginaldoubledisable: { boolLetter: true, bool: true },
				transformdisable: { boolLetter: true, bool: true },
				transformdisabledivine: { boolLetter: true, bool: true },
				hirsutedisable: { boolLetter: true, bool: true },
				pbdisable: { boolLetter: true, bool: true },
				breastfeedingdisable: { boolLetter: true, bool: true },
				analpregdisable: { boolLetter: true, bool: true },
				watersportsdisable: { boolLetter: true, bool: true },
				spiderdisable: { boolLetter: true, bool: true },
				bodywritingdisable: { boolLetter: true, bool: true },
				parasitedisable: { boolLetter: true, bool: true},
				slugdisable: { boolLetter: true, bool: true},
				waspdisable: {boolLetter: true, bool: true},
				beedisable: { boolLetter: true, bool: true},
				lurkerdisable: {boolLetter: true, bool: true},
				horsedisable: {boolLetter: true, bool: true},
				plantdisable: {boolLetter: true, bool: true},
				footdisable: {boolLetter: true, bool: true},
				asphyxiaLvl: { min: 0, max: 4, decimals: 0 },
				NudeGenderDC: { min: 0, max: 2, decimals: 0 },
				breastsizemin: { min: 0, max: 4, decimals: 0 },
				breastsizemax: { min: 0, max: 13, decimals: 0 },
				bottomsizemax: { min: 0, max: 9, decimals: 0 },
				penissizemax: { min: -2, max: 4, decimals: 0 },
				penissizemin: { min: -2, max: 0, decimals: 0 },
				/*ToDo: Pregnancy, uncomment to properly enable, add defaults back to DolSettingsExport.json*/
				//baseVaginalPregnancyChance: { min: 0, max: 96, decimals: 0 },
				//baseNpcPregnancyChance: { min: 0, max: 16, decimals: 0 },
				//humanPregnancyMonths: { min: 1, max: 9, decimals: 0 },
				//wolfPregnancyWeeks: { min: 2, max: 12, decimals: 0 },
				//playerPregnancyHumanDisable: {boolLetter: true, bool: true},
				//playerPregnancyBeastDisable: {boolLetter: true, bool: true},
				//npcPregnancyDisable: {boolLetter: true, bool: true},
				images: { min: 0, max: 1, decimals: 0 },
				sidebarAnimations: { bool: true },
				combatAnimations: { bool: true },
				bodywritingImages: { bool: true },
				silhouettedisable: { boolLetter: true, bool: true },
				blinkingdisable: { boolLetter: true, bool: true },
				halfcloseddisable: { boolLetter: true, bool: true },
				numberify_enabled: { min: 0, max: 1, decimals: 0 },
				timestyle: { strings: ["military", "ampm"] },
				checkstyle: { strings: ["percentage", "words", "skillname"] },
				tipdisable: { boolLetter: true, bool: true },
				debugdisable: { boolLetter: true, bool: true },
				cheatdisabletoggle: { boolLetter: true, bool: true },
				showCaptionText: { bool: true },
				confirmSave: { bool: true },
				confirmLoad: { bool: true },
				confirmDelete: { bool: true },
				newWardrobeStyle: { bool: true },
				imgLighten: { strings: ["", "imgLighten", "imgLighten2"] },
				sidebarStats: { strings: ["Disabled", "Limited", "All"] },
				sidebarTime: { strings: ["Disabled", "top", "bottom"] },
				combatControls: { strings: ["radio", "lists", "limitedLists"] },
				reducedLineHeight: { bool: true },
				neverNudeMenus: { bool: true },
				skipStatisticsConfirmation: { bool: true},
				multipleWardrobes: { strings: [false, "isolated"] }, //, "all"
				outfitEditorPerPage: { min: 5, max: 20, decimals: 0 }, //, "all"
				map: {
					movement: { bool: true },
					top: { bool: true },
					markers: { bool: true },
				},
				skinColor: {
					tanImgEnabled: { boolLetter: true, bool: true },
					tanningEnabled: { bool: true },
				},
				shopDefaults: {
					alwaysBackToShopButton: { bool: true },
					color: { strings: ["black", "blue", "brown", "green", "pink", "purple", "red", "tangerine", "teal", "white", "yellow", "custom", "random"] },
					colourItems: { strings: ["disable","random","default"] },
					compactMode: { bool: true },
					disableReturn: { bool: true },
					highContrast: { bool: true },
					mannequinGender: { strings: ["same","opposite","male","female"] },
					mannequinGenderFromClothes:  { bool: true },
					noHelp: { bool: true },
					noTraits: { bool: true },
					secColor: { strings: ["black", "blue", "brown", "green", "pink", "purple", "red", "tangerine", "teal", "white", "yellow", "custom", "random"] },
				},
			};
			break;
		case "npc":
			result = {
				pronoun: { strings: ["m", "f"] },
				gender: { strings: ["m", "f"] },
				penissize: { min: 0, max: 4, decimals: 0 },
				breastsize: { min: 0, max: 12, decimals: 0 },
			}
			break;
	}
	return result;
}

/*Converts specific settings to so they dont look so chaotic to players*/
window.settingsConvert = function(exportType, type, settings){
	var listObject = settingsObjects(type);
	var result = settings;
	var keys = Object.keys(listObject);
	for (var i = 0; i < keys.length; i++){
		if (result[keys[i]] === undefined) continue;
		if(["map", "skinColor", "player", "shopDefaults"].includes(keys[i])){
			var itemKey = Object.keys(listObject[keys[i]]);
			for (var j = 0; j < itemKey.length; j++) {
				if (result[keys[i]][itemKey[j]] === undefined) continue;
				var keyArray = Object.keys(listObject[keys[i]][itemKey[j]]);
				if(exportType){
					if (keyArray.includes("boolLetter") && keyArray.includes("bool")) {
						if (result[keys[i]][itemKey[j]] === "t") {
							result[keys[i]][itemKey[j]] = true;
						}else if(result[keys[i]][itemKey[j]] === "f"){
							result[keys[i]][itemKey[j]] = false;
						}
					}
				}else{
					if (keyArray.includes("boolLetter") && keyArray.includes("bool")) {
						if (result[keys[i]][itemKey[j]] === true) {
							result[keys[i]][itemKey[j]] = "t";
						}else if(result[keys[i]][itemKey[j]] === false){
							result[keys[i]][itemKey[j]] = "f";
						}
					}
				}
			}
		}else{
			var keyArray = Object.keys(listObject[keys[i]]);
			if(exportType){
				if (keyArray.includes("boolLetter") && keyArray.includes("bool")) {
					if (result[keys[i]] === "t") {
						result[keys[i]] = true;
					}else if(result[keys[i]] === "f"){
						result[keys[i]] = false;
					}
				}
			}else{
				if (keyArray.includes("boolLetter") && keyArray.includes("bool")) {
					if (result[keys[i]] === true) {
						result[keys[i]] = "t";
					}else if(result[keys[i]] === false){
						result[keys[i]] = "f";
					}
				}
			}
		}
	}
	return result;
}

window.loadExternalExportFile = function () {
	importScripts("DolSettingsExport.json")
		.then(function () {
			var textArea = document.getElementById("settingsDataInput");
			textArea.value = JSON.stringify(DolSettingsExport);
		})
		.catch(function (err) {
			//console.log(err);
			var button = document.getElementById("LoadExternalExportFile");
			button.value = "Error Loading";
		});
}

// !!Hack warning!! Don't use it maybe?
window.updateMoment = function () {
	// change last (and only) moment in local history
	State.history[State.history.length - 1].variables = JSON.parse(JSON.stringify(V));
	// prepare the moment object with modified history
	let moment = SugarCube.State.marshalForSave();
	// replace moment.history with moment.delta, because that's what SugarCube expects to find
	// this is a bad thing to do probably btw, because while history and delta appear to look very similar,
	// they're not always the same thing, SugarCube actually decodes delta into history (see: https://github.com/tmedwards/sugarcube-2/blob/36a8e1600160817c44866205bc4d2b7730b2e70c/src/state.js#L527)
	// but for my purpose it works (i think?)
	//delete Object.assign(moment, {delta: moment.history}).history;
	// delta-encode the state
	delete Object.assign(moment, {delta: State.deltaEncode(moment.history)}).history;
	// replace saved moment in session with the new one
	let gameName = SugarCube.Story.domId;
	sessionStorage[gameName + ".state"] = JSON.stringify(moment);
	// it appears that this line is not necessary for it to work
	//SugarCube.session._engine[gameName + ".state"] = JSON.stringify(moment);

	// Voilà! F5 will reload the current state now without going to another passage!
}

window.isJsonString = function(s) {
	try {
		JSON.parse(s);
	} catch (e) {
		return false;
	}
	return true;
}


code:
//Sidebar scroll resetting fix
//When a passage is finished loading:
$(document).on(":passageend", function (event) {
	//simplified fix, iteration 2
	if (window.scroll_uibar) document.querySelector("#storyCaptionDiv").scroll(0, window.scroll_uibar);
	if (window.scroll_main) document.scrollingElement.scroll(0, window.scroll_main);
	/*previous version
	//get sidebar dom element, assign it to variable for convenience
	let sidebar = document.querySelector("#storyCaptionDiv");

	//if the sidebar was not found, quit before causing errors.
	if (sidebar == undefined) return;

	//if the bar was very close to the bottom, stay at the bottom. otherwise, scroll to last known scroll position
	if (setup.sidebarIsAtBottom){
		sidebar.scroll(0, sidebar.scrollHeight);
	} else {
		sidebar.scroll(0, setup.sidebarScrollY);
	}

	//update last known scroll position whenever the position is changed
	sidebar.addEventListener("scroll", function(){
		setup.sidebarScrollY = sidebar.scrollTop;
		setup.sidebarIsAtBottom = ((sidebar.scrollHeight - sidebar.scrollTop) < sidebar.clientHeight+10);
	});*/
});


code:
/*
 * Usage:
 * <<radiovar VARNAME VALUE [LABEL]>> ONCHANGE <</radiovar>>
 *
 * Will display a radiobutton, optionally labeled, that corresponds
 * to state variable VARNAME having value VALUE.
 *
 * When var value is set to this value, silently execute ONCHANGE
 */
Macro.add("radiovar", {
	tags: null,
	handler: function() {
		if (this.args.length < 2) return this.error("missing <<radiovar>> arguments");
		var varname = this.args[0];
		var value = this.args[1];
		var content = this.payload[0].contents;
		var e = $("<input>")
			.attr({
				name: "radiovar"+Util.slugify(varname),
				id: "radiovar"+Util.slugify(varname)+"-"+Util.slugify(value),
				tabindex: 0,
				type: "radio"
			})
			.prop("checked", State.getVar(varname) ==  value)
			.addClass("macro-radiovar")
			.on('change.macros', function () {
				if (this.checked) {
					State.setVar(varname, value);
					Wikifier.wikifyEval(content);
				}
			});
		if (this.args[2]) e = $("<label>").append(this.args[2], e);
		e.appendTo(this.output);
	}
});


code:
window.ZIndices = {
	bg: 0,

	over_head_back: 0,
	head_back: 1,
	basehead: 5,
	backhair: 10,
	back_lower: 15,
	base: 20,
	facebase: 21,
	tanBody: 22,
	bellyBase: 23,
	hirsute: 25,
	eyes: 30,
	sclera: 31,
	iris: 32,
	irisacc: 33,
	eyelids: 34,
	lashes: 35,
	mouth: 40,
	armsidle: 30,
	handsidle: 31.5,
	breasts: 35,
	tanbreasts: 30,
	blush: 50,
	freckles: 51,
	skin: 52,
	tears: 55,
	hair: 60,
	penis_chastity: 64,
	legs: 64,
	pbhair: 64,
	penisunderclothes: 64.3,
	pbhairballsunderclothes: 64.6,
	genitals: 65,
	penis: 160, // when exposed
	pbhairballs: 161, // when exposed
	underParasite: 66,
	under_upper_arms: 66,
	under_lower: 67,
	under_lower_top: 68,
	under_upper: 70,
	under_upper_top: 72,
	under_lower_high: 75,
	under_lower_top_high: 77,
	under: 75,
	feet: 85,
	lower: 90,
	lower_top: 92,
	upper: 95,
	upper_arms: 94,
	upper_tucked: 89,
	upper_arms_tucked: 88.5,
	upper_top: 97,

	collar: 103,
	neck: 103,

	arms_cover: 105,
	under_upper_arms_cover: 109,
	hands: 110,
	upper_arms_cover: 112,
	lower_high: 115,
	lower_top_high: 117,

	hairforwards: 132,
	fronthair: 133,
	brow: 138,
	horns: 140,
	face: 145,
	head: 150,
	over_head: 152,

	parasite: 160,

	tailPenisCover: 165,

	over_lower: 170,
	over_upper: 171,
	over_upper_arms: 170,
	over_upper_arms_cover: 174
};


code:
Renderer.Animations["idle"] = {
	frames: 2,
	duration: 1000
};
// Example animations for testing
let demo_rainbow_colors = [
	"#FF0000", "#FF8000", "#FFFF00", "#80FF00",
	"#00FF00", "#00FF80", "#00FFFF", "#0080FF",
	"#0000FF", "#8000FF", "#FF00FF", "#FF0080"
];
Renderer.Animations["idle_rainbow_gradient"] = {
	keyframes: demo_rainbow_colors.map((color, i) => ({
		frame: Math.floor(i * 2 / 12),
		duration: (i%6 === 0) ? 200 : 160, // 200 ms + 5 x 160 ms = 1000 ms
		blend: {
			gradient: "linear",
			values: [128, 0, 128, 256],
			colors: [
				...demo_rainbow_colors.slice(i),
				...demo_rainbow_colors.slice(0,i)
			]
		}
	}))
}
Renderer.Animations["idle_rainbow"] = {
	keyframes: [{
		frame: 0,
		duration: 200, // 1x200 + 5x160 = 1000, to align with idle animation
		blend: "#ff0000"
	}, {
		frame: 0,
		duration: 160,
		blend: "#ff8000"
	}, {
		frame: 0,
		duration: 160,
		blend: "#ffff00"
	}, {
		frame: 0,
		duration: 160,
		blend: "#80ff00"
	}, {
		frame: 0,
		duration: 160,
		blend: "#00ff00"
	}, {
		frame: 0,
		duration: 160,
		blend: "#00ff80"
	}, {
		frame: 1,
		duration: 200,
		blend: "#00ffff"
	}, {
		frame: 1,
		duration: 160,
		blend: "#0080ff"
	}, {
		frame: 1,
		duration: 160,
		blend: "#0000ff"
	}, {
		frame: 1,
		duration: 160,
		blend: "#8000ff"
	}, {
		frame: 1,
		duration: 160,
		blend: "#ff00ff"
	}, {
		frame: 1,
		duration: 160,
		blend: "#ff0080"
	}]
};
Renderer.Animations["blink"] = {
	keyframes: [{
		frame: 0,
		duration: 9000
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 14400
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 5400
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 1100
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 12500
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 23400
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 9000
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 1800
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 7200
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 10800
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 7200
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 7200
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 9000
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 12600
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 10800
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 5400
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 1800
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 900
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 9000
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 7200
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 10800
	}, {
		frame: 1,
		duration: 100
	}]
};

Renderer.Animations["blink-trauma"] = {
	frameCount: 2,
	keyframes: [{
		frame: 0,
		duration: 5400
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 23400
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 7200
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 7200
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 10800
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 900
	}, {
		frame: 1,
		duration: 100
	}, {
		frame: 0,
		duration: 10800
	}, {
		frame: 1,
		duration: 100
	}]
};

// cum drip animations generated using img/split2.bat ImageMagick script
// I believe their sizes could be reduced by keeping unique frames only
// (amount of keyframes does not matter; every different frame(subsprite) is re-rendered & cached

Renderer.Animations["AnalCumDripStart"] = {
	keyframes: [
		{frame: 0, duration: 3000},
		{frame: 1, duration: 100},
		{frame: 2, duration: 100},
		{frame: 3, duration: 100},
		{frame: 4, duration: 100}
	]
}
Renderer.Animations["AnalCumDripVerySlow"] = {
	keyframes: [
		{frame: 0, duration: 2000},
		{frame: 1, duration: 200},
		{frame: 2, duration: 200},
		{frame: 3, duration: 200},
		{frame: 4, duration: 300},
		{frame: 5, duration: 200},
		{frame: 6, duration: 100},
		{frame: 7, duration: 100},
		{frame: 8, duration: 100},
		{frame: 9, duration: 2000},
		{frame: 10, duration: 100},
		{frame: 11, duration: 100},
		{frame: 12, duration: 100},
		{frame: 13, duration: 100}
	]
};
Renderer.Animations["AnalCumDripSlow"] = {
	keyframes: [
		{frame: 0, duration: 1000},
		{frame: 1, duration: 200},
		{frame: 2, duration: 200},
		{frame: 3, duration: 200},
		{frame: 4, duration: 300},
		{frame: 5, duration: 200},
		{frame: 6, duration: 100},
		{frame: 7, duration: 100},
		{frame: 8, duration: 100},
		{frame: 9, duration: 1000},
		{frame: 10, duration: 100},
		{frame: 11, duration: 100},
		{frame: 12, duration: 100},
		{frame: 13, duration: 100}
	]
};
Renderer.Animations["AnalCumDripFast"] = {
	keyframes: [
		{frame: 0, duration: 1000},
		{frame: 1, duration: 200},
		{frame: 2, duration: 200},
		{frame: 3, duration: 200},
		{frame: 4, duration: 300},
		{frame: 5, duration: 200},
		{frame: 6, duration: 100},
		{frame: 7, duration: 100},
		{frame: 8, duration: 100},
		{frame: 9, duration: 100},
		{frame: 10, duration: 100},
		{frame: 11, duration: 100}
	]
};
Renderer.Animations["AnalCumDripVeryFast"] = {
	keyframes: [
		{frame: 0, duration: 80},
		{frame: 1, duration: 80},
		{frame: 2, duration: 80},
		{frame: 3, duration: 80},
		{frame: 4, duration: 80},
		{frame: 5, duration: 80},
		{frame: 6, duration: 80},
		{frame: 7, duration: 80},
		{frame: 8, duration: 80},
		{frame: 9, duration: 80},
		{frame: 10, duration: 80},
		{frame: 11, duration: 80},
		{frame: 12, duration: 80},
		{frame: 13, duration: 80},
		{frame: 14, duration: 80},
		{frame: 15, duration: 80},
		{frame: 16, duration: 80}
	]
};

Renderer.Animations["MouthCumDripStart"] = {
	keyframes: [
		{frame: 0, duration: 0}
	]
};
Renderer.Animations["MouthCumDripVerySlow"] = {
	keyframes: [
		{frame: 0, duration: 1700},
		{frame: 1, duration: 170},
		{frame: 2, duration: 170},
		{frame: 3, duration: 170},
		{frame: 4, duration: 170},
		{frame: 5, duration: 170},
		{frame: 6, duration: 170},
		{frame: 7, duration: 170}
	]
};
Renderer.Animations["MouthCumDripSlow"] = {
	keyframes: [
		{frame: 0, duration: 170},
		{frame: 1, duration: 170},
		{frame: 2, duration: 170},
		{frame: 3, duration: 170},
		{frame: 4, duration: 170},
		{frame: 5, duration: 170},
		{frame: 6, duration: 170},
		{frame: 7, duration: 170},
		{frame: 8, duration: 170},
		{frame: 9, duration: 170},
		{frame: 10, duration: 170},
		{frame: 11, duration: 170},
		{frame: 12, duration: 170},
		{frame: 13, duration: 170},
		{frame: 14, duration: 170},
		{frame: 15, duration: 170},
		{frame: 16, duration: 170},
		{frame: 17, duration: 170},
		{frame: 18, duration: 170},
		{frame: 19, duration: 170},
		{frame: 20, duration: 170},
		{frame: 21, duration: 170}
	]
};
Renderer.Animations["MouthCumDripFast"] = {
	keyframes: [
		{frame: 0, duration: 170},
		{frame: 1, duration: 170},
		{frame: 2, duration: 170},
		{frame: 3, duration: 170},
		{frame: 4, duration: 170},
		{frame: 5, duration: 170},
		{frame: 6, duration: 170},
		{frame: 7, duration: 170},
		{frame: 8, duration: 170},
		{frame: 9, duration: 170},
		{frame: 10, duration: 170},
		{frame: 11, duration: 170},
		{frame: 12, duration: 170},
	]
};
Renderer.Animations["MouthCumDripVeryFast"] = {
	keyframes: [
		{frame: 0, duration: 100},
		{frame: 1, duration: 100},
		{frame: 2, duration: 100},
		{frame: 3, duration: 100},
		{frame: 4, duration: 100},
		{frame: 5, duration: 100},
		{frame: 6, duration: 100},
		{frame: 7, duration: 100},
		{frame: 8, duration: 100},
		{frame: 9, duration: 100},
		{frame: 10, duration: 100},
		{frame: 11, duration: 100},
		{frame: 12, duration: 100},
		{frame: 13, duration: 100},
		{frame: 14, duration: 100},
		{frame: 15, duration: 100},
		{frame: 16, duration: 100},
		{frame: 17, duration: 100},
		{frame: 18, duration: 100},
		{frame: 19, duration: 100},
		{frame: 20, duration: 100},
		{frame: 21, duration: 100},
		{frame: 22, duration: 100},
		{frame: 23, duration: 100},
		{frame: 24, duration: 100},
		{frame: 25, duration: 100},
		{frame: 26, duration: 100},
		{frame: 27, duration: 100},
		{frame: 28, duration: 100},
		{frame: 29, duration: 100},
		{frame: 30, duration: 100},
		{frame: 31, duration: 100},
		{frame: 32, duration: 100},
		{frame: 33, duration: 100},
		{frame: 34, duration: 100},
		{frame: 35, duration: 100},
		{frame: 36, duration: 100},
		{frame: 37, duration: 100},
		{frame: 38, duration: 100},
		{frame: 39, duration: 100},
		{frame: 40, duration: 100},
		{frame: 41, duration: 100},
		{frame: 42, duration: 100},
		{frame: 43, duration: 100},
		{frame: 44, duration: 100},
		{frame: 45, duration: 100},
		{frame: 46, duration: 100},
		{frame: 47, duration: 100},
		{frame: 48, duration: 100},
		{frame: 49, duration: 100},
		{frame: 50, duration: 100},
		{frame: 51, duration: 100}
	]
};

Renderer.Animations["VaginalCumDripStart"] = {
	keyframes: [
		{frame: 0, duration: 3000},
		{frame: 1, duration: 100},
		{frame: 2, duration: 100},
		{frame: 3, duration: 100},
		{frame: 4, duration: 100},
		{frame: 5, duration: 100},
		{frame: 6, duration: 100}
	]
};
Renderer.Animations["VaginalCumDripVerySlow"] = {
	keyframes: [
		{frame: 0, duration: 2500},
		{frame: 1, duration: 200},
		{frame: 2, duration: 200},
		{frame: 3, duration: 200},
		{frame: 4, duration: 200},
		{frame: 5, duration: 200},
		{frame: 6, duration: 200},
		{frame: 7, duration: 200},
		{frame: 8, duration: 200},
		{frame: 9, duration: 200},
		{frame: 10, duration: 200},
		{frame: 11, duration: 200},
		{frame: 12, duration: 200},
		{frame: 13, duration: 200},
		{frame: 14, duration: 200}
	]
}
Renderer.Animations["VaginalCumDripSlow"] = {
	keyframes: [
		{frame: 0, duration: 2000},
		{frame: 1, duration: 200},
		{frame: 2, duration: 200},
		{frame: 3, duration: 200},
		{frame: 4, duration: 200},
		{frame: 5, duration: 200},
		{frame: 6, duration: 200},
		{frame: 7, duration: 200},
		{frame: 8, duration: 200},
		{frame: 9, duration: 200},
		{frame: 10, duration: 200},
		{frame: 11, duration: 600},
		{frame: 12, duration: 100},
		{frame: 13, duration: 100},
		{frame: 14, duration: 100},
		{frame: 15, duration: 100},
		{frame: 16, duration: 100},
		{frame: 17, duration: 100}
	]
};
Renderer.Animations["VaginalCumDripFast"] = {
	keyframes: [
		{frame: 0, duration: 800},
		{frame: 1, duration: 150},
		{frame: 2, duration: 150},
		{frame: 3, duration: 150},
		{frame: 4, duration: 150},
		{frame: 5, duration: 150},
		{frame: 6, duration: 150},
		{frame: 7, duration: 150},
		{frame: 8, duration: 150},
		{frame: 9, duration: 150},
		{frame: 10, duration: 150},
		{frame: 11, duration: 150}
	]
};
Renderer.Animations["VaginalCumDripVeryFast"] = {
	keyframes: [
		{frame: 0, duration: 100},
		{frame: 1, duration: 100},
		{frame: 2, duration: 100},
		{frame: 3, duration: 100},
		{frame: 4, duration: 100},
		{frame: 5, duration: 100},
		{frame: 6, duration: 100},
		{frame: 7, duration: 100},
		{frame: 8, duration: 100},
		{frame: 9, duration: 100},
		{frame: 10, duration: 100},
		{frame: 11, duration: 100}
	]
};


code:
/**
 * Example model, a 'xray-vaginal' and small part of 'close-vaginal' slots of sex animation.
 * See game/03-JavaScript/canvasmodel.js for API reference
 * and canvasmodel-example.twee for test passage (accessible in Debug/Main menu)
 */
Renderer.CanvasModels["sexdemo"] = {
	name: "sexdemo", // For debugging

	width: 768,
	height: 256,

	frames: 8, // For CSS animation only. JS animations can have as many frames as needed

	/**
	 * List of names of generated options, for debugging & tooling
	 */
	generatedOptions() {
		return [ ]
	},

	/**
	 * Default option values.
	 * All possible options should be initialized to some non-crashing values here.
	 * Should also have a `filter:{}` option for model filters
	 */
	defaultOptions() {
		return {
			/**
			 * Global animation speed, "" (no animation) | "slow" | "mid" | "fast" | "vfast"
			 */
			animation_speed: "",
			/**
			 * Sex position, "doggy" | "missionary"
			 */
			position: "doggy",
			/**
			 * PC penis type: "" (none) | "default" | "virgin"
			 */
			penis: "",
			/**
			 * PC has vagina
			 */
			vagina: false,
			/**
			 * PC has balls
			 */
			balls: false,
			/**
			 * Item index in setup.clothes.under_lower
			 */
			worn_under_lower: 0,
			/**
			 * Item state, "" (none) | "totheside"
			 */
			worn_under_lower_state: "",
			/**
			 * Item colour, key in setup.colours.clothes
			 */
			worn_under_lower_colour: "red",
			/**
			 * Show/hide xray-anal slot
			 */
			xray_vaginal_show: false,
			/**
			 * Animation speed override, "" to use animation_speed
			 */
			xray_vaginal_speed: "",
			/**
			 * Show/hide close-vaginal slot
			 */
			close_vagina_show: false,
			/**
			 * Animation speed override, "" to use animation_speed
			 */
			close_vagina_speed: "",
			/**
			 * Penetration state, "" (none) | "entrance" | "imminent" | "penetrated"
			 * NOT the equivalent of $vaginastate, tentacles share entrance/imminent/penetrated
			 * and $vaginastate="tentacledeep" should set options.vaginacumming=true
			 */
			vagina_state: "",
			vagina_aroused: false,
			/**
			 * Penetrator type, "" (empty) | "penis" (human) | "beast" | "tentacle" | "machine"
			 */
			vagina_penetrator: "",
			/**
			 * Penetrator subtype, options depend on vagina_penetrator:
			 * - for "human": NPC skin colour
			 * - for "beast": beast type
			 * - for others: "default"
			 */
			vagina_penetrator_subtype: "default",
			/**
			 * Penetrator size
			 */
			vagina_penetrator_size: 0,
			/**
			 * Cumming inside vagina
			 */
			vagina_cumming: false,
			/**
			 * User-supplied filters go here; typically this is empty in default options
			 */
			filters: {}
		}
	},

	/**
	 * This function is called before compiling layers for rendering or animation.
	 * Generated options and filters implementations go here.
	 */
	preprocess(options) {
		if (!options.xray_vaginal_speed) { // "" means use global
			options.xray_vaginal_speed = options.animation_speed;
		}
		if (!options.close_vagina_speed) { // "" means use global
			options.close_vagina_speed = options.animation_speed;
		}
		if (options.worn_under_lower) {
			let record = setup.colours.clothes_map[options.worn_under_lower_colour];
			if (record) {
				options.filters["under_lower"] = record.canvasfilter;
			} else {
				console.error("Unknown under_lower colour "+options.worn_under_lower_colour);
				options.filters["under_lower"] = {};
			}
		}
		// Despite the name, filters can have any layer properties, including those affectign composition
		options.filters["close_vagina"] = {
			dx: 386,
			dy: 0,
			width: 64
		};
	},

	/**
	 * The meat.
	 * Key is layer name, value is layer definition.
	 * Rendering order is guaranteed only via `z` property.
	 */
	layers: {
		"xray_vaginal": {
			/**
			 * X and Y offsets
			 */
			dx: 0,
			/**
			 * Shift down for futas
			 */
			dyfn(options) {
				return options.penis && options.vagina ? 64 : 0
			},
			/**
			 * This option is required because layer takes small part of the resulting image.
			 * Without it, the 2048 pixel wide image would be treated as 3-frame animation (=2048/768).
			 */
			width: 256,
			/**
			 * As this layer has `showfn`, it will be displayed automatically when needed,
			 * without <<showlayer "xray_vaginal">>.
			 * You'll need <<set _modeloptions.xray_vaginal_show = true>> instead.
			 * Not much difference but it makes more sense when you toggle multiple layers with one option.
			 */
			showfn(options) {
				return options.xray_vaginal_show && options.vagina_state === "penetrated";
			},
			srcfn(options) {
				let cum = options.vagina_cumming ? "cum" : "";
				switch (options.vagina_penetrator) {
					case "machine":
						return "img/sex/machine/vaginal/xray_vaginal.png";
					case "beast":
						switch (options.vagina_penetrator_subtype) {
							case "horse":
								return "img/sex/xrayvaginalhorse"+cum+".png"
							default:
								return "img/sex/xrayvaginalbeast"+cum+".png"
						}
					case "penis":
						switch (options.vagina_penetrator_subtype) {
							case "black":
								return "img/sex/black/xrayvaginal"+cum+".png"
							default:
								return "img/sex/xrayvaginal"+cum+".png"
						}
					case "tentacle":
						return "img/sex/xrayvaginaltentacle"+cum+".png"
					default:
						/**
						 * Fallback value, nothing will be renderer
						 */
						return "";
				}
			},
			/**
			 * Copied from z-index of #xrayvaginal
			 */
			z: 3,
			/**
			 * Returns name of animation, or empty string if none
			 */
			animationfn(options) {
				/**
				 * We use the fact that option values and animation names are same
				 */
				return options.xray_vaginal_speed ? ("sex-8f-"+options.xray_vaginal_speed) : "";
			}
		},
		/***
		 * CLOSEUPS
		 */
		"close_vagina": {
			z: 250,
			showfn(options) {
				return options.close_vagina_show
			},
			srcfn(options) {
				if (options.vagina_state === "penetrated") {
					return (options.vagina_penetrator === "beast" && options.vagina_penetrator_size >= 5) ?
						"img/sex/close/"+options.position+"/vaginapenetratebig.png" :
						"img/sex/close/"+options.position+"/vaginapenetrate.png"
				} else {
					return options.vagina_aroused ?
						"img/sex/close/"+options.position+"/vaginaaroused.png" :
						"img/sex/close/"+options.position+"/vagina.png"
				}
			},
			filters: [ "close_vagina" ],
			animationfn(options) {
				return "sex-6f-"+options.close_vagina_speed;
			}
		},
		"close_vagina_aroused": {
			z: 251,
			showfn(options) {
				return options.close_vagina_show && options.vagina_aroused
			},
			srcfn(options) {
				return "img/sex/close/"+options.position+"/vaginaaroused.png"
			},
			animationfn(options) {
				return "sex-6f-"+options.close_vagina_speed;
			}
		},
		/**
		 * Futa penis
		 */
		"close_vagina_penis": {
			z: 252,
			showfn(options) {
				/**
				 * Only exact 'false' would hide the layer (undefined etc won't),
				 * so cast non-boolean options to boolean via double negation.
				 */
				return options.close_vagina_show && !!options.penis
			},
			srcfn(options) {
				return "img/sex/close/"+options.position+"/" +
					(options.balls ? "futavagina.png" : "futavaginanoballs.png")
			},
			filters: [ "close_vagina" ],
			animationfn(options) {
				return "sex-6f-"+options.close_vagina_speed;
			}
		},
		"close_vagina_panties": {
			z: 253,
			showfn(options) {
				return options.close_vagina_show && !!options.worn_under_lower &&
					options.worn_under_lower_state === "totheside"
			},
			srcfn(options) {
				return "img/sex/close/"+options.position+"/vaginatotheside.png"
			},
			/**
			 * One for dx,dy,width, other for colours
			 */
			filters: [ "close_vagina", "under_lower" ],
			animationfn(options) {
				return "sex-6f-"+options.close_vagina_speed;
			}
		},
		"close_vagina_penetrator": {
			z: 254,
			showfn(options) {
				return options.close_vagina_show && !!options.vagina_state && !!options.vagina_penetrator
			},
			srcfn(options) {
				switch (options.vagina_state) {
					case "penetrated":
						switch (options.vagina_penetrator) {
							case "tentacle":
								return "img/sex/close/"+options.position+"/vaginatentaclepenetrated.png"
							case "machine":
								return "img/sex/close/machine/vaginal/"+options.position+"/penetrated_close.png"
							case "beast":
								if (options.vagina_penetrator_subtype === "horse" || options.vagina_penetrator_subtype === "centaur") {
									return "img/sex/close/beast/vaginapenetratedhorse.png"
								} else {
									return "img/sex/close/"+options.position+"/vaginapenetratedbeast.png"
								}
							default:
								return "img/sex/close/"+options.position+"/vaginapenetrated.png";
						}
					case "imminent":
					case "entrance":
						switch (options.vagina_penetrator) {
							case "tentacle":
								return "img/sex/close/"+options.position+"/vaginatentacleentrance.png"
							case "machine":
								return "img/sex/machine/vaginal/"+options.position+"/entrance_close.png"
							case "beast":
								if (options.vagina_penetrator_subtype === "horse" || options.vagina_penetrator_subtype === "centaur") {
									return "img/sex/close/beast/vaginaentrancehorse.png"
								} else {
									return "img/sex/close/"+options.position+"/vaginaentrancebeast.png"
								}
							default:
								return "img/sex/close/"+options.position+"/vaginaentrance.png";
						}
				}
			},
			filters: [ "close_vagina" ],
			animationfn(options) {
				return "sex-6f-"+options.close_vagina_speed;
			}
		}
	}
}

Renderer.Animations["sex-8f-slow"] = {
	frames: 8,
	duration: 330
};
Renderer.Animations["sex-8f-mid"] = {
	frames: 8,
	duration: 170
};
Renderer.Animations["sex-8f-fast"] = {
	frames: 8,
	duration: 110
};
Renderer.Animations["sex-8f-vfast"] = {
	frames: 8,
	duration: 80
};

Renderer.Animations["sex-6f-slow"] = {
	frames: 6,
	duration: 330
};
Renderer.Animations["sex-6f-mid"] = {
	frames: 6,
	duration: 170
};
Renderer.Animations["sex-6f-fast"] = {
	frames: 6,
	duration: 110
};
Renderer.Animations["sex-6f-vfast"] = {
	frames: 6,
	duration: 80
};


code:
:: CanvasModel Example
[[Bedroom]]
<hr>

<!-- "divsex" is cache slot id to preserve layer caches between passages -->
<<selectmodel "sexdemo" "divsex">>
	<<set _modeloptions.animation_speed to "fast">>
	<<set _modeloptions.position to "doggy">>
	<<set _modeloptions.penis to "default">>
	<<set _modeloptions.vagina to true>>
	<<set _modeloptions.worn_under_lower to 1>>
	<<set _modeloptions.worn_under_lower_state to "totheside">>
	<<set _modeloptions.worn_under_lower_colour to "blue">>
	<<set _modeloptions.xray_vaginal_show to true>>
	<<set _modeloptions.close_vagina_show to true>>
	<<set _modeloptions.vagina_state to "penetrated">>
	<<set _modeloptions.vagina_penetrator to "tentacle">>
	<<set _modeloptions.vagina_penetrator_size to 5>>
	<<set _modeloptions.vagina_cumming to true>>
<<animatemodel "">>

<!-- Editor hooks to "last model" -->
<<canvasModelEditor>>