code:
package com.lilithsthrone.game.dialogue.utils;

/**
 * @since 0.1.85
 * @version 0.1.86
 * @author Innoxia
 */
public enum InventoryInteraction {
	SEX,
	COMBAT,
	TRADING,
	FULL_MANAGEMENT,
	CHARACTER_CREATION;
}

repo_name: klorpa/Pedo

license: unknown


code:
package com.lilithsthrone.game.dialogue.utils;

import com.lilithsthrone.game.character.GameCharacter;
import com.lilithsthrone.game.combat.Spell;
import com.lilithsthrone.main.Main;
import com.lilithsthrone.utils.Colour;
import com.lilithsthrone.world.Cell;

/**
 * Used in Pathing.
 *
 * @since 0.3.1
 * @version 0.3.5
 * @author Innoxia
 */
public enum MapTravelType {

	WALK_SAFE("Walk (safest)",
			"Use the safest path to your destination.",
			"Click once on the map to mark your desired end point, then click that end point again to travel there. You can queue up waypoints by holding shift while clicking.",
			Colour.GENERIC_MINOR_GOOD) {
				@Override
				public boolean isAvailable(Cell c, GameCharacter character) {
					return true;
				}

				@Override
				public String getUnavailablilityDescription(Cell c, GameCharacter character) {
					return "";
				}
			},

	WALK_DANGEROUS("Walk (fastest)",
			"Use the fastest path to your destination.",
			"Click once on the map to mark your desired end point, then click that end point again to travel there. You can queue up waypoints by holding shift while clicking.",
			Colour.GENERIC_MINOR_BAD) {
				@Override
				public boolean isAvailable(Cell c, GameCharacter character) {
					return true;
				}

				@Override
				public String getUnavailablilityDescription(Cell c, GameCharacter character) {
					return "";
				}
			},

	FLYING("Fly",
			"Fly to your destination.",
			"Click once on the map to mark your desired end point, then click that end point again to travel there.",
			Colour.SPELL_SCHOOL_AIR) {
				@Override
				public boolean isAvailable(Cell c, GameCharacter character) {
					return character.isPartyAbleToFly();
				}

				@Override
				public String getUnavailablilityDescription(Cell c, GameCharacter character) {
					if(!character.isAbleToFly()) {
						return "You are not able to fly!";
					}
					return "Not all of your companions are able to fly!";
				}
			},

	TELEPORT("Teleport",
			"Teleport to your destination.",
			"Click once on the map to mark your desired end point, then click that end point again to travel there.",
			Colour.SPELL_SCHOOL_ARCANE) {
				@Override
				public boolean isAvailable(Cell c, GameCharacter character) {
					if(Main.game.isDebugMode()) {
						return true;
					}
					if(!character.getWorldLocation().getTeleportPermissions().isOutgoing()
							|| (c!=null && !c.getType().getTeleportPermissions().isIncoming())
							|| (c!=null && !c.getPlace().getPlaceType().getTeleportPermissions().isIncoming())) {
						return false;
					}
					return (character.isAbleToTeleport() && character.getMana()>=Spell.TELEPORT.getModifiedCost(character));
				}

				@Override
				public String getUnavailablilityDescription(Cell c, GameCharacter character) {
					if(!character.getWorldLocation().getTeleportPermissions().isOutgoing()) {
						return "You cannot teleport out of the area '"+character.getWorldLocation().getName()+"'!";
					}
					if(c!=null && !c.getType().getTeleportPermissions().isIncoming()) {
						return "You cannot teleport into the area '"+c.getType().getName()+"'!";
					}
					if(c!=null && !c.getPlace().getPlaceType().getTeleportPermissions().isIncoming()) {
						return "You cannot teleport into the tile '"+c.getPlace().getName()+"'!";
					}
					if(!character.isAbleToTeleport()) {
						return character.getUnableToTeleportDescription();
					}
					return "You require at least "+Spell.TELEPORT.getModifiedCost(character)+" aura to cast this spell!";
				}
			};

	private String name;
	private String description;
	private String useInstructions;
	private Colour colour;

	private MapTravelType(String name, String description, String useInstructions, Colour colour) {
		this.name = name;
		this.description = description;
		this.useInstructions = useInstructions;
		this.colour = colour;
	}

	public String getName() {
		return name;
	}

	public String getDescription() {
		return description;
	}

	public String getUseInstructions() {
		return useInstructions;
	}

	public Colour getColour() {
		return colour;
	}

	public abstract boolean isAvailable(Cell c, GameCharacter character);

	public abstract String getUnavailablilityDescription(Cell c, GameCharacter character);
}


code:
package com.lilithsthrone.game.dialogue.utils;

import java.util.Map.Entry;

import com.lilithsthrone.game.character.effects.StatusEffect;
import com.lilithsthrone.game.dialogue.DialogueNode;
import com.lilithsthrone.game.dialogue.DialogueNodeType;
import com.lilithsthrone.game.dialogue.responses.Response;
import com.lilithsthrone.game.dialogue.responses.ResponseEffectsOnly;
import com.lilithsthrone.main.Main;
import com.lilithsthrone.utils.Util;

/**
 * @since 0.1.62
 * @version 0.3.1
 * @author Innoxia
 */
public class MiscDialogue {

	private static StringBuilder descriptionSB = new StringBuilder();

	public static final DialogueNode STATUS_EFFECTS = new DialogueNode("Important status effect updates", "", true) {

		@Override
		public String getContent() {
			descriptionSB.setLength(0);

			for(Entry<StatusEffect, String> e : Main.game.getPlayer().getStatusEffectDescriptions().entrySet()){
				descriptionSB.append("
"
										+"
"+Util.capitaliseSentence(e.getKey().getName(Main.game.getPlayer()))+"
"
										+ e.getValue()
									+"
");
			}

			return descriptionSB.toString();
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 0) {
				return new ResponseEffectsOnly("Continue", "Carry on with whatever you were doing."){
					@Override
					public void effects() {
						Main.game.restoreSavedContent(false);
					}
				};
			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.STATUS_EFFECT_MESSAGE;
		}
	};
}


code:
package com.lilithsthrone.game.dialogue.utils;

import java.util.List;

import com.lilithsthrone.game.character.npc.NPC;
import com.lilithsthrone.game.character.race.Race;
import com.lilithsthrone.game.character.race.Subspecies;
import com.lilithsthrone.game.dialogue.DialogueNode;
import com.lilithsthrone.game.dialogue.encounters.Encounter;
import com.lilithsthrone.game.dialogue.responses.Response;
import com.lilithsthrone.main.Main;
import com.lilithsthrone.utils.Util;
import com.lilithsthrone.world.WorldType;

/**
 * @since 0.3.2
 * @version 0.3.5.5
 * @author Innoxia
 */
public class OffspringMapDialogue {

	private static List<NPC> getOffspringList() {
		WorldType worldType = Main.game.getPlayer().getWorldLocation();

		return Main.game.getOffspringNotSpawned(npc->
			npc.getSubspecies().getWorldLocations().keySet().contains(worldType)
				&& (worldType==WorldType.HARPY_NEST
						?(npc.getHalfDemonSubspecies()==null || npc.getHalfDemonSubspecies().getRace()==Race.HARPY)
						:(npc.getHalfDemonSubspecies()==null || npc.getHalfDemonSubspecies().getRace()!=Race.HARPY))
				|| (Main.game.getPlayer().getLocationPlace().getPlaceType().getEncounterType()==Encounter.DOMINION_ALLEY
						&& (npc.getSubspecies()==Subspecies.ANGEL
							|| npc.getSubspecies()==Subspecies.FOX_ASCENDANT
							|| npc.getSubspecies()==Subspecies.FOX_ASCENDANT_FENNEC))
				|| (Main.game.getPlayer().getLocationPlace().getPlaceType().getEncounterType()==Encounter.DOMINION_CANAL
						&& (npc.getSubspecies()==Subspecies.ALLIGATOR_MORPH
							|| npc.getSubspecies()==Subspecies.SLIME
							|| npc.getSubspecies()==Subspecies.RAT_MORPH)));
	}

	public static final DialogueNode OFFSPRING_CHOICE = new DialogueNode("Offspring list", "-", true) {
		@Override
		public String getContent() {
			UtilText.nodeContentSB.setLength(0);

			boolean noOffspring = getOffspringList().isEmpty();

			UtilText.nodeContentSB.append(
					"
"
						+ "The arcane enchantment in the map causes a list of names of all your unmet offspring to be projected into the air in front of you, with corresponding dots showing their exact location on the physical map itself."
						+ (noOffspring
								? " It doesn't look like you have any unmet offspring to be found right now..."
								: " Luckily, it looks like one of your offspring is in this very area."
									+ " As you take a look at the key, you see just who it is...")
					+ "
"
					+ "
");

				if(Main.game.getOffspringNotSpawned(npc->true).isEmpty()) {
					UtilText.nodeContentSB.append("[style.colourDisabled(No offspring available)]");

				} else {
					for(NPC npc : Main.game.getOffspringNotSpawned(npc->true)) {
						if(!getOffspringList().contains(npc)) {
							UtilText.nodeContentSB.append("[style.colourDisabled("+(npc.isPlayer()?"You":Util.capitaliseSentence(npc.getName(true)))+")]"
									+ " ("+Util.capitaliseSentence(npc.getSubspecies().getName(npc))+")"
									+ " Mother: "+npc.getMother().getName(true)
									+ " Father: "+npc.getFather().getName(true)
									+ "
");

						} else {
							UtilText.nodeContentSB.append(""+(npc.isPlayer()?"You":Util.capitaliseSentence(npc.getName(true)))+""
									+ " ("+Util.capitaliseSentence(npc.getSubspecies().getName(npc))+")"
									+ " Mother: "+npc.getMother().getName(true)
									+ " Father: "+npc.getFather().getName(true)
									+ "
");
						}
					}
				}

			UtilText.nodeContentSB.append("
");

			return UtilText.nodeContentSB.toString();
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 0) {
				return new Response("Back", "Decide not to look for any of your offspring in this location after all.", Main.game.getDefaultDialogueNoEncounter());

			} else if(index-1 true).size()) {
				NPC offspring = Main.game.getOffspringNotSpawned(npc->true).get(index-1);

				if(!getOffspringList().contains(offspring)) {
					return new Response(offspring.getName(true),
							UtilText.parse(offspring,"[npc.Name] cannot be found in this area, due to [npc.her] subspecies..."),
							null);
				}

				return new Response(offspring.getName(true),
						"Search for this offspring in this area.",
						offspring.getEncounterDialogue()) {
					@Override
					public void effects() {
						Main.game.getOffspringSpawned().add(offspring);

						offspring.setLocation(Main.game.getPlayer(), true);

						Main.game.setActiveNPC(offspring);

						Main.game.getTextStartStringBuilder().append(
								"
"
									+ UtilText.parse(offspring,
										"Consulting the map, you soon find that your [npc.daughter], [npc.name], is living in this particular area of the alleyways."
										+ " Putting away your map, you start to search for [npc.herHim]...")
								+ "
");
					}
				};

			} else {
				return null;
			}
		}
	};
}


code:
package com.lilithsthrone.game.dialogue.utils;

import com.lilithsthrone.game.Game;
import com.lilithsthrone.game.PropertyValue;
import com.lilithsthrone.game.character.CharacterUtils;
import com.lilithsthrone.game.character.attributes.Attribute;
import com.lilithsthrone.game.character.body.valueEnums.AgeCategory;
import com.lilithsthrone.game.character.body.valueEnums.CupSize;
import com.lilithsthrone.game.character.body.valueEnums.Lactation;
import com.lilithsthrone.game.character.fetishes.Fetish;
import com.lilithsthrone.game.character.gender.*;
import com.lilithsthrone.game.character.npc.NPC;
import com.lilithsthrone.game.character.persona.SexualOrientation;
import com.lilithsthrone.game.character.persona.SexualOrientationPreference;
import com.lilithsthrone.game.character.race.FurryPreference;
import com.lilithsthrone.game.character.race.Subspecies;
import com.lilithsthrone.game.character.race.SubspeciesPreference;
import com.lilithsthrone.game.combat.Combat;
import com.lilithsthrone.game.dialogue.DialogueNode;
import com.lilithsthrone.game.dialogue.DialogueNodeType;
import com.lilithsthrone.game.dialogue.responses.Response;
import com.lilithsthrone.game.dialogue.responses.ResponseEffectsOnly;
import com.lilithsthrone.game.dialogue.story.CharacterCreation;
import com.lilithsthrone.game.settings.*;
import com.lilithsthrone.main.Main;
import com.lilithsthrone.rendering.Artist;
import com.lilithsthrone.rendering.ArtistWebsite;
import com.lilithsthrone.rendering.Artwork;
import com.lilithsthrone.rendering.SVGImages;
import com.lilithsthrone.utils.Colour;
import com.lilithsthrone.utils.CreditsSlot;
import com.lilithsthrone.utils.Units;
import com.lilithsthrone.utils.Util;
import com.lilithsthrone.utils.Units.UnitType;
import com.lilithsthrone.utils.Units.ValueType;

import java.awt.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.*;

/**
 * @since 0.1.0
 * @version 0.3.1
 * @author Innoxia
 */
public class OptionsDialogue {

	private static boolean confirmNewGame = false;

	public static final DialogueNode MENU = new DialogueNode("Menu", "Menu", true) {

		@Override
		public String getLabel() {
			return "";
		}

		@Override
		public String getContent(){
			return "
"+Main.GAME_NAME+"
"
					+ "
Created by "+Main.AUTHOR+"
"
					+ "
"
					+ "
"
						+ "This game is a text-based erotic RPG, and contains a lot of graphic sexual content. You must agree to the game's disclaimer before playing this game!"
					+ "
"
					+"
"
						+ "You can visit my blog (https://lilithsthrone.blogspot.co.uk) to check on development progress (use the 'Blog' button below to open the blog in your default browser)."
					+ "
"
					+ "
"
						+ "Please use either my blog or github to get the latest official version of Lilith's Throne!"
					+ "
"
					+ getJavaVersionInformation()
					+ (Toolkit.getDefaultToolkit().getScreenSize().getHeight()<800
							?"
"
								+ "If the game's resolution isn't fitting to your screen, press the keys: 'Windows' + 'Up Arrow' to maximise!"
							+ "
"
							:"")
					+ "
"
					+ (Main.game.isStarted() || Main.getProperties().name.isEmpty()
							?""
							:"
Last save:
"
								+ "
" + Main.getProperties().name + "
"
								+ "
Level " + Main.getProperties().level + " " + Util.capitaliseSentence(Main.getProperties().race) + "
"
								+ "
" + UtilText.formatAsMoney(Main.getProperties().money, "b") + "
"
								+ "
" + UtilText.formatAsEssences(Main.getProperties().arcaneEssences, "b", false) + "
"
								+ "
Quest: " + Util.capitaliseSentence(Main.getProperties().quest) + "
");
		}

		@Override
		public Response getResponse(int responseTab, int index) {

			 if (index == 1) {
				 if(confirmNewGame || !Main.game.isStarted()) {
					return new ResponseEffectsOnly(
							(!Main.game.isStarted()
									?"New Game"
									:"Confirm"),
							"Start a new game."
								+(Main.game.isStarted()
									?"

[style.italicsMinorBad(Remember to save your game first!)]"
									:"")){
						@Override
						public void effects() {
							//Fixes a bug where inventory would stay on screen
							if (Main.game.isStarted()) {
								Main.game.setInCombat(false);
								Main.game.setInSex(false);
							}

							Main.mainController.setAttributePanelContent("");
							Main.mainController.setRightPanelContent("");
							Main.mainController.setButtonsContent("");
							Main.game.setRenderMap(false);
							Main.startNewGame(CharacterCreation.CHARACTER_CREATION_START);
							confirmNewGame = false;
						}
					};

				 } else {
					 return new Response(
							 "New Game",
							 "Start a new game."
								+(Main.game.isStarted()
									?"

[style.italicsMinorBad(Remember to save your game first!)]"
									:""),
								MENU){
							@Override
							public void effects() {
								confirmNewGame = true;
							}
						};
				 }

			} else if (index == 2) {
				return new Response("Save/Load", "Open the save/load game window.", SAVE_LOAD){
					@Override
					public void effects() {
						loadConfirmationName = ""; overwriteConfirmationName = ""; deleteConfirmationName = "";
						confirmNewGame=false;
					}
				};

			} else if (index == 3) {
				return new Response("Export character", "Open the character export game window.", IMPORT_EXPORT){
					@Override
					public void effects() {
						loadConfirmationName = ""; overwriteConfirmationName = ""; deleteConfirmationName = "";
						confirmNewGame=false;
					}
				};

			} else if (index == 4) {
				return new Response("Disclaimer", "View the game's disclaimer.", DISCLAIMER){
					@Override
					public void effects() {
						confirmNewGame=false;
					}
				};

			} else if (index == 5) {
				return new ResponseEffectsOnly("Quit", "Quits your current game and closes the program.

Remember to save your game first!"){
					@Override
					public void effects() {
						Main.primaryStage.close();
						confirmNewGame=false;

					}
				};

			} else if (index == 6) {
				return new Response("Options", "Open the options page.", OPTIONS){
					@Override
					public void effects() {
						confirmNewGame=false;

					}
				};

			} else if (index == 7) {
				return new Response("Content Options", "Set your preferred content settings.", CONTENT_PREFERENCE){
					@Override
					public void effects() {
						confirmNewGame=false;

					}
				};

			} else if (index == 8) {
				return new Response("Patch notes", "View the patch notes for this version.", PATCH_NOTES);

			} else if (index == 9) {
				return new Response("Credits", "View the game's credits screen.", CREDITS);

			} else if (index == 10) {
				return new ResponseEffectsOnly("Blog", "Opens the page:

https://lilithsthrone.blogspot.co.uk/

Externally in your default browser."){
					@Override
					public void effects() {
						Util.openLinkInDefaultBrowser("https://lilithsthrone.blogspot.co.uk/");
						confirmNewGame=false;
					}
				};

			} else if (index == 11) {
				return new ResponseEffectsOnly("Github", "Opens the page:

https://github.com/Innoxia/liliths-throne-public

Externally in your default browser."){
					@Override
					public void effects() {
						Util.openLinkInDefaultBrowser("https://github.com/Innoxia/liliths-throne-public");
						confirmNewGame=false;
					}
				};

			} else if (index == 0) {
				if(Main.game.isStarted()) {
					return new ResponseEffectsOnly("Resume", "Return to whatever you were doing before opening this menu."){
						@Override
						public void effects() {
							Main.mainController.openOptions();
							confirmNewGame=false;

						}
					};

				} else {
					if(Main.isLoadGameAvailable(Main.getProperties().lastSaveLocation)) {
						return new ResponseEffectsOnly("Resume", "Continue playing from your last save."){
							@Override
							public void effects() {
								Main.loadGame(Main.getProperties().lastSaveLocation);
								confirmNewGame=false;

							}
						};
					} else if ( "".equals(Main.getProperties().lastSaveLocation) ) {
						return new Response("Resume", "There is no previously saved game to resume.", null);
					} else {
						return new Response("Resume", "Previously saved game (by the title '"+Main.getProperties().lastSaveLocation+"') not found in 'data/saves' folder.", null);
					}
				}

			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	private static String getJavaVersionInformation() {
		StringBuilder sb = new StringBuilder();

		sb.append("
"
					+ "Your java version: "+System.getProperty("java.version"));
//				+" | ");

//		String[] version = System.getProperty("java.version").split("\\.");
//		if(version[0]!=null) {
//			if(Integer.valueOf(version[0])<9) {
//				sb.append("You have an old version of java! This game needs at least 9.0.1 to work correctly!");
//			} else {
//				sb.append("Your java is up to date!");
//			}
//		}
//		if(version.length>=2) {
//			if(Integer.valueOf(version[1])<8) {
//				sb.append("You have an old version of java! This game needs at least v1.8.0_131 to work correctly!");
//
//			} else {
//				if(version.length==3){
//					String[] versionMinor = version[2].split("_");
//					if(versionMinor.length>=2)
//						if(Integer.valueOf(versionMinor[1])<131) {
//							sb.append("You have an old version of java! This game needs at least v1.8.0_131 to work correctly!");
//
//						} else {
//							sb.append("Your java is up to date!");
//						}
//				} else {
//					sb.append("This game needs at least v1.8.0_131 to work correctly!");
//				}
//			}
//		}

		sb.append("
");

		return sb.toString();
	}

	public static String loadConfirmationName = "";
	public static String overwriteConfirmationName = "";
	public static String deleteConfirmationName = "";

	public static final DialogueNode SAVE_LOAD = new DialogueNode("Save game files", "", true) {

		@Override
		public String getContent() {
			return "";
		}

		@Override
		public String getHeaderContent(){
			StringBuilder saveLoadSB = new StringBuilder();

			saveLoadSB.append(
					"
"
						+ "Please Note:"
					+ "
"
					+ "
"
						+ "1. Only standard characters (letters and numbers) will work for save file names.
"
						+ "2. The 'AutoSave' file is automatically overwritten every time you move between maps.
"
						+ "3. The 'QuickSave' file is automatically overwritten every time you quick save (binding is "+Main.getProperties().hotkeyMapPrimary.get(KeyboardAction.QUICKSAVE).getFullName()+").
"
						+ "4. You cannot save during scenes which restrict your movement, including combat and sex."
					+ "
"
					+ "
"
						+ "
"
							+ "Time"
						+ "
"
						+ "
"
							+ "Name"
						+ "
"
						+ "
"
							+ "Save | Load | Delete"
						+ "
"
					+ "
");

			int i=0;

			if(Main.game.isStarted()) {
				saveLoadSB.append(getSaveLoadRow(null, null, i%2==0));
				i++;
			}

			Main.getSavedGames().sort(Comparator.comparingLong(File::lastModified).reversed());

			for(File f : Main.getSavedGames()){
				saveLoadSB.append(getSaveLoadRow(""+Util.getFileTime(f)+"", f.getName(), i%2==0));
				i++;
			}

			saveLoadSB.append("

");

			return saveLoadSB.toString();
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 1) {
				return new Response("Confirmations: ",
						"Toggle confirmations being shown when you click to load, overwrite, or delete a saved game."
							+ " When turned on, it will take two clicks to apply any button press."
							+ " When turned off, it will only take one click.",
						SAVE_LOAD) {
					@Override
					public String getTitle() {
						return "Confirmations: "+(Main.getProperties().hasValue(PropertyValue.overwriteWarning)
								?"ON"
								:"OFF");
					}

					@Override
					public void effects() {
						loadConfirmationName = "";
						overwriteConfirmationName = "";
						deleteConfirmationName = "";
						Main.getProperties().setValue(PropertyValue.overwriteWarning, !Main.getProperties().hasValue(PropertyValue.overwriteWarning));
						Main.getProperties().savePropertiesAsXML();
					}
				};

			} else if (index == 0) {
				return new Response("Back", "Back to the main menu.", MENU);

			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	public static final DialogueNode IMPORT_EXPORT = new DialogueNode("Export character", "", true) {

		@Override
		public String getContent() {
			return "";
		}

		@Override
		public String getHeaderContent(){
			StringBuilder saveLoadSB = new StringBuilder();

			saveLoadSB.append("
"
						+ "Here you can export your current character, or delete any characters that you've exported in the past."
						+ " Any NPC can be exported in-game by viewing their information screen (either from the 'characters present' or your phone's 'contacts' screen), and then pressing the small 'export character' button in the top-right."
					+ "
"
					+ "
"
						+ "Exported characters can be used as a playable character when starting a new game (choose 'Start (Import)'), or as an importable slave at the Auction Block in Slaver Alley."
					+ "
"
					+ "
"
						+ "
"
							+ "Time"
						+ "
"
						+ "
"
							+ "Name"
						+ "
"
						+ "
"
							+ "Functions"
						+ "
"
					+ "
");

			Main.getCharactersForImport().sort(Comparator.comparingLong(File::lastModified).reversed());
			int i = 0;
			for(File f : Main.getCharactersForImport()){
				saveLoadSB.append(OptionsDialogue.getImportRow(""+Util.getFileTime(f)+"", f.getName(), i%2==0));
			}

			saveLoadSB.append("

");

			return saveLoadSB.toString();
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 1) {
				return new Response("Confirmations: ",
						"Toggle confirmations being shown when you click to load, overwrite, or delete a saved game."
							+ " When turned on, it will take two clicks to apply any button press."
							+ " When turned off, it will only take one click.",
							IMPORT_EXPORT) {
					@Override
					public String getTitle() {
						return "Confirmations: "+(Main.getProperties().hasValue(PropertyValue.overwriteWarning)
								?"ON"
								:"OFF");
					}

					@Override
					public void effects() {
						OptionsDialogue.loadConfirmationName = "";
						OptionsDialogue.overwriteConfirmationName = "";
						OptionsDialogue.deleteConfirmationName = "";
						Main.getProperties().setValue(PropertyValue.overwriteWarning, !Main.getProperties().hasValue(PropertyValue.overwriteWarning));
						Main.getProperties().savePropertiesAsXML();
					}
				};

			} else if (index == 2) {
				if(Main.game.isStarted()) {
					return new Response("Export character", "Exports your character file to the 'data/characters/' folder.", IMPORT_EXPORT){
						@Override
						public void effects() {
							CharacterUtils.saveCharacterAsXML(Main.game.getPlayer());
							Main.game.flashMessage(Colour.GENERIC_GOOD, "Character exported!");
						}
					};
				} else {
					return new Response("Export character", "You'll need to start a game first!", null);
				}

			} else if (index == 0) {
				return new Response("Back", "Back to the main menu.", OptionsDialogue.MENU);

			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	private static String getSaveLoadRow(String date, String name, boolean altColour) {
		if(name!=null){
			String baseName = Util.getFileName(name);
			String identifierName = Util.getFileIdentifier(name);

			return "
"
						+ "
"
							+ date
						+ "
"
						+ "
"
							+ baseName
						+ "
"
						+ "
"
							+ (Main.isSaveGameAvailable()
									?(name.equals(overwriteConfirmationName)
										?"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskSaveConfirm()+"

"
										:"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskOverwrite()+"

")
									:"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskSaveDisabled()+"

")

							+ (name.equals(loadConfirmationName)
									?"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskLoadConfirm()+"

"
									:"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskLoad()+"

")

							+ (name.equals(deleteConfirmationName)
								?"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskDeleteConfirm()+"

"
								:"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskDelete()+"

")
						+ "
"
					+ "
";

		} else {
			return "
"
						+ "
"
							+ "-"
						+ "
"
						+ "
"
							+"<form style='padding:0;margin:0;text-align:center;'><input type='text' id='new_save_name' value='New Save' style='padding:0;margin:0;width:100%;'></form>"
						+ "
"
						+ "
"
							+ (Main.isSaveGameAvailable()
								?"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskSave()+"

"
								:"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskSaveDisabled()+"

")
						+ "
"
					+ "
";

		}
	}

	private static String getImportRow(String date, String name, boolean altColour) {
		String baseName = Util.getFileName(name);
		String identifierName = Util.getFileIdentifier(name);

		return "
"
					+ "
"
						+ date
					+ "
"
					+ "
"
						+ baseName
					+ "
"
					+ "
"
					+ (name.equals(deleteConfirmationName)
							?"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskDeleteConfirm()+"

"
							:"

"+SVGImages.SVG_IMAGE_PROVIDER.getDiskDelete()+"

")
					+ "
"
				+ "
";
	}

	public static final DialogueNode OPTIONS = new DialogueNode("Options", "Options", true) {

		@Override
		public String getContent(){
			UtilText.nodeContentSB.setLength(0);

			UtilText.nodeContentSB.append(
					"
"
					+ "Light/Dark theme:"
					+ "
This switches the main display between a light and dark theme. (Work in progress!)"
					+ "
"

					+"
"
					+ "Font-size:
"
					+ "This cycles the game's base font size. This currently only affects the size of the text in the main dialogue, but in the future I'll expand it to include every display element.
"
					+ "Minimum font size is "+Game.FONT_SIZE_MINIMUM+". Default font size is "+Game.FONT_SIZE_NORMAL+". Maximum font size is "+Game.FONT_SIZE_HUGE+".
"
					+ "Current font size: "+Main.getProperties().fontSize+"."
					+ "
"

					+"
"
					+ "Fade-in:"
					+ "
This option is responsible for fading in the main part of the text each time a new scene is displayed."
					+ " Although it makes scene transitions a little prettier, it is off by default, as it can cause some annoying lag in inventory screens."
					+ "
"

					+"
"
					+ "Difficulty (Currently set to "+Main.getProperties().difficultyLevel.getName()+"):");

			for(DifficultyLevel dl : DifficultyLevel.values()) {
				UtilText.nodeContentSB.append("
"+(
						Main.getProperties().difficultyLevel==dl
							?""+Util.capitaliseSentence(dl.getName())+" "+dl.getDescription()
							:""+Util.capitaliseSentence(dl.getName())+" [style.colourDisabled("+dl.getDescription()+")]")
						 );
			}

			UtilText.nodeContentSB.append("
");

			return UtilText.nodeContentSB.toString();
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 1) {
				return new Response("Keybinds", "Open the keybinds page, where you can customise all the game's key bindings.", KEYBINDS);

			} else if (index == 2) {

				if (Main.getProperties().hasValue(PropertyValue.lightTheme)) {
					return new Response("Dark theme", "Switch the theme to the dark variant.", OPTIONS){
						@Override
						public void effects() {
							Main.mainController.switchTheme();

						}
						};
				} else {
					return new Response("Light theme (WIP)", "Switch the theme to the light variant.

This is still a work in progress....", OPTIONS){
						@Override
						public void effects() {
							Main.mainController.switchTheme();

						}
					};
				}

			} else if (index == 3) {
				return new Response("Font-size -",
						"Decrease the size of the game's font. Default value is 18. Current value is "+Main.getProperties().fontSize+".",
								OPTIONS){
					@Override
					public void effects() {
						if (Main.getProperties().fontSize > Game.FONT_SIZE_MINIMUM) {
							Main.getProperties().fontSize--;
						}
						Main.saveProperties();

					}
				};

			} else if (index == 4) {
				return new Response("Font-size +",
						"Increase the size of the game's font. Default value is 18. Current value is "+Main.getProperties().fontSize+".",
								OPTIONS){
					@Override
					public void effects() {
						if (Main.getProperties().fontSize < Game.FONT_SIZE_HUGE) {
							Main.getProperties().fontSize++;
						}
						Main.saveProperties();

					}
				};

			} else if (index == 5) {
				return new Response("Fade-in: " + (Main.getProperties().hasValue(PropertyValue.fadeInText)
						? "[style.boldGood(ON)]"
						: "[style.boldBad(OFF)]"), "Toggle the fading in of the game's text. If turned on, it may cause some minor lag in inventory screens.", OPTIONS) {
					@Override
					public void effects() {
						Main.getProperties().setValue(PropertyValue.fadeInText, !Main.getProperties().hasValue(PropertyValue.fadeInText));
						Main.saveProperties();
					}
				};

			} else if (index == 6) {
				return new Response("Gender pronouns", "Customise all gender pronouns and names.", OPTIONS_PRONOUNS);

			} else if (index == 7) {
				return new Response("Gender preferences", "Set your preferred gender encounter rates.", GENDER_PREFERENCE);

			} else if (index == 8) {
				return new Response("Orientation preferences", "Set your preferred sexual orientation encounter rates.", ORIENTATION_PREFERENCE);

			} else if (index == 9) {
				return new Response("Age preferences", "Set your preferred age encounter rates.", AGE_PREFERENCE);

			} else if (index == 10) {
				return new Response("Furry preferences", "Set your preferred transformation encounter rates.", FURRY_PREFERENCE);

			} else if (index == 11) {
				return new Response("Unit preferences", "Set your preferred measurement units.", UNIT_PREFERENCE);

			} else if (index == 12) {
				return new Response("Difficulty: "+Main.getProperties().difficultyLevel.getName(), "Cycle the game's difficulty.", OPTIONS){
					@Override
					public void effects() {
						switch(Main.getProperties().difficultyLevel) {
							case NORMAL:
								Main.getProperties().difficultyLevel = DifficultyLevel.LEVEL_SCALING;
								break;
							case LEVEL_SCALING:
								Main.getProperties().difficultyLevel = DifficultyLevel.HARD;
								break;
							case HARD:
								Main.getProperties().difficultyLevel = DifficultyLevel.NIGHTMARE;
								break;
							case NIGHTMARE:
								Main.getProperties().difficultyLevel = DifficultyLevel.HELL;
								break;
							case HELL:
								Main.getProperties().difficultyLevel = DifficultyLevel.NORMAL;
								break;
						}
						Main.saveProperties();

						for(NPC npc : Main.game.getAllNPCs()) {
							if(!Main.game.isInCombat() || !Combat.getAllCombatants(false).contains(npc)) {
								npc.setMana(npc.getAttributeValue(Attribute.MANA_MAXIMUM));
								npc.setHealth(npc.getAttributeValue(Attribute.HEALTH_MAXIMUM));
							}
						}
					}
				};

			} else if (index == 0) {
				return new Response("Back", "Back to the main menu.", MENU);

			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	public static final DialogueNode KEYBINDS = new DialogueNode("Options", "Options", true) {

		@Override
		public String getHeaderContent() {
			return "
"
					+ "
"
					+ "
Action	Primary binding	Secondary binding
"
					+ getKeybindTableRow(KeyboardAction.MENU)
					+ "

"

					+ getKeybindTableRow(KeyboardAction.QUICKSAVE)
					+ getKeybindTableRow(KeyboardAction.QUICKLOAD)
					+ "

"

					+ getKeybindTableRow(KeyboardAction.MENU_SELECT)
					+ getKeybindTableRow(KeyboardAction.INVENTORY)
					+ getKeybindTableRow(KeyboardAction.JOURNAL)
					+ getKeybindTableRow(KeyboardAction.MAP)
					+ getKeybindTableRow(KeyboardAction.CHARACTERS)
					+ getKeybindTableRow(KeyboardAction.ZOOM)
					+ "

"

					+ getKeybindTableRow(KeyboardAction.MOVE_NORTH)
					+ getKeybindTableRow(KeyboardAction.MOVE_WEST)
					+ getKeybindTableRow(KeyboardAction.MOVE_SOUTH)
					+ getKeybindTableRow(KeyboardAction.MOVE_EAST)
					+ "

"

					+ getKeybindTableRow(KeyboardAction.RESPOND_1)
					+ getKeybindTableRow(KeyboardAction.RESPOND_2)
					+ getKeybindTableRow(KeyboardAction.RESPOND_3)
					+ getKeybindTableRow(KeyboardAction.RESPOND_4)
					+ getKeybindTableRow(KeyboardAction.RESPOND_5)
					+ getKeybindTableRow(KeyboardAction.RESPOND_6)
					+ getKeybindTableRow(KeyboardAction.RESPOND_7)
					+ getKeybindTableRow(KeyboardAction.RESPOND_8)
					+ getKeybindTableRow(KeyboardAction.RESPOND_9)
					+ getKeybindTableRow(KeyboardAction.RESPOND_10)
					+ getKeybindTableRow(KeyboardAction.RESPOND_11)
					+ getKeybindTableRow(KeyboardAction.RESPOND_12)
					+ getKeybindTableRow(KeyboardAction.RESPOND_13)
					+ getKeybindTableRow(KeyboardAction.RESPOND_14)
					+ getKeybindTableRow(KeyboardAction.RESPOND_0)
					+ "

"

					+ getKeybindTableRow(KeyboardAction.RESPOND_NEXT_PAGE)
					+ getKeybindTableRow(KeyboardAction.RESPOND_PREVIOUS_PAGE)

					+ getKeybindTableRow(KeyboardAction.RESPOND_NEXT_TAB)
					+ getKeybindTableRow(KeyboardAction.RESPOND_PREVIOUS_TAB)
					+ "

"

					+ getKeybindTableRow(KeyboardAction.MOVE_RESPONSE_CURSOR_NORTH)
					+ getKeybindTableRow(KeyboardAction.MOVE_RESPONSE_CURSOR_WEST)
					+ getKeybindTableRow(KeyboardAction.MOVE_RESPONSE_CURSOR_SOUTH)
					+ getKeybindTableRow(KeyboardAction.MOVE_RESPONSE_CURSOR_EAST)
					+ "
"
					+ "
";
		}

		@Override
		public String getContent(){
			return "";
		}

		ArrayList<Properties> presets;

		private void loadPresets() {
			presets = new ArrayList<>();

			// Load all text files in the folder as properties
			File presetFolder = new File("res/keybinds");
			if (presetFolder.exists() && presetFolder.isDirectory()) {
				for (File f : presetFolder.listFiles((dir, name) -> name.endsWith(".txt"))) {
					try (FileInputStream input = new FileInputStream(f)) {
						Properties preset = new Properties();
						preset.load(input);
						presets.add(preset);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			} else {
				presetFolder.mkdirs();
			}
		}

		private void loadPreset(Properties preset) {
			// Clear existing mappings
			Main.getProperties().hotkeyMapPrimary.clear();
			Main.getProperties().hotkeyMapSecondary.clear();

			// Create a key mapping for every action contained in the given property
			for (KeyboardAction ka : KeyboardAction.values()) {
				if (preset.containsKey(ka.name())) {
					String[] keys = preset.getProperty(ka.name()).split(" or ");
					Main.getProperties().hotkeyMapPrimary.put(ka, KeyCodeWithModifiers.fromString(keys[0]));
					if (keys.length == 2) Main.getProperties().hotkeyMapSecondary.put(ka, KeyCodeWithModifiers.fromString(keys[1]));
				}
			}
		}

		private void savePreset(int index) {
			// Create new properties containing current key mappings
			Properties preset = new Properties();
			preset.setProperty("NAME", "Custom " + index);
			preset.setProperty("DESCRIPTION", "Reapply your previously saved key bindings.");

			for (Map.Entry<KeyboardAction, KeyCodeWithModifiers> e : Main.getProperties().hotkeyMapPrimary.entrySet())
				if (e.getValue() != null)
					preset.setProperty(e.getKey().name(), e.getValue().toString());

			for (Map.Entry<KeyboardAction, KeyCodeWithModifiers> e : Main.getProperties().hotkeyMapSecondary.entrySet()) {
				if (e.getValue() != null) {
					// Write or append to existing entry
					String primary = preset.getProperty(e.getKey().name());
					primary = primary == null ? e.getValue().toString() : primary + " or " + e.getValue().toString();
					preset.setProperty(e.getKey().name(), primary);
				}
			}

			// Write properties to file
			try (FileOutputStream output = new FileOutputStream("res/keybinds/custom" + index + ".txt")) {
				preset.store(output, "");
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			// Load the presets if uninitialized
			if (presets == null) loadPresets();

			if (index == 0) {
				return new Response("Back", "Go back to the options menu.", OPTIONS);

			} else if (index <= presets.size()) {
				Properties preset = presets.get(index - 1);
				return new Response(preset.getProperty("NAME", "Custom " + index), preset.getProperty("DESCRIPTION", "Reapply your previously saved key bindings."), KEYBINDS) {
					@Override
					public void effects() {
						loadPreset(preset);
						Main.saveProperties();
					}
				};

			} else if (index == 14) {
				return new Response("Save preset",
						"Store the current key bindings in a file. If you want to delete any saved presets, navigate to the 'res/keybinds' folder and delete the .txt files that you no longer want."
								+ " (They will stop showing up in this list after a game restart.)",
						KEYBINDS) {
					@Override
					public void effects() {
						savePreset(presets.size() - 2);
						loadPresets();
					}
				};
			}
			return null;
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	private static String getKeybindTableRow(KeyboardAction action) {
		return "
"
				+ ""
				+ action.getName()
				+ "	"
				+ ""
				+ "
"
				+ (Main.getProperties().hotkeyMapPrimary.get(action) == null ? "-" : Main.getProperties().hotkeyMapPrimary.get(action).getFullName())
				+ "
"
				+ "
x
"
				+ "	"
				+ ""
				+ "
"
				+ (Main.getProperties().hotkeyMapSecondary.get(action) == null ? "-" : Main.getProperties().hotkeyMapSecondary.get(action).getFullName())
				+ "
"
				+ "
x
"
				+ "	"
				+ "
";
	}

	public static final DialogueNode OPTIONS_PRONOUNS = new DialogueNode("Options", "Options", true) {

		@Override
		public String getContent() {
			StringBuilder sb = new StringBuilder();

			sb.append("
"
						+ "
Global gender names:
"
						+ "
"
							+ "
"
							+ "Body Parts	"
								+ "Masculine	"
								+ "Androgynous	"
								+ "Feminine	"
							+ "
");

			for(GenderNames gn : GenderNames.values()) {
				sb.append(getGenderNameTableRow(gn));
			}

			sb.append("
"
					+ "
"

					+ "
"
						+ "
Player-specific pronouns:
"
						+ "
"
							+ "
"
								+ "Pronoun	"
								+ "Masculine	"
								+ "Feminine	"
							+ "
"
							+ getPronounTableRow(GenderPronoun.NOUN)
							+ getPronounTableRow(GenderPronoun.YOUNG_NOUN)
							+ getPronounTableRow(GenderPronoun.SECOND_PERSON)
							+ getPronounTableRow(GenderPronoun.THIRD_PERSON)
							+ getPronounTableRow(GenderPronoun.POSSESSIVE_BEFORE_NOUN)
							+ getPronounTableRow(GenderPronoun.POSSESSIVE_ALONE)
						+ "
"
					+ "
"
					+ "
Androgynous bodies (option 3)
"
					+ "
"
					+ "Feminine: Treated as feminine.
"
					+ "Clothing feminine: Treated according to clothing femininity."
							+ " If clothing is neutral, treated as feminine.
"
					+ "Clothing masculine: Treated according to clothing femininity."
							+ " If clothing is neutral, treated as masculine.
"
					+ "Masculine: Treated as masculine.
"
					+ "
");

			return sb.toString();
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 1) {
				return new ResponseEffectsOnly("Save", "Save all the pronouns that are currently displayed.") {
					@Override
					public void effects() {
						for(GenderNames gn : GenderNames.values()) {
							Main.getProperties().genderNameMale.put(gn, ((String) Main.mainController.getWebEngine().executeScript("document.getElementById('GENDER_NAME_MASCULINE_" + gn +"').value")).toLowerCase());
							Main.getProperties().genderNameNeutral.put(gn, ((String) Main.mainController.getWebEngine().executeScript("document.getElementById('GENDER_NAME_ANDROGYNOUS_" + gn +"').value")).toLowerCase());
							Main.getProperties().genderNameFemale.put(gn, ((String) Main.mainController.getWebEngine().executeScript("document.getElementById('GENDER_NAME_FEMININE_" + gn +"').value")).toLowerCase());
						}
						for (GenderPronoun gp : GenderPronoun.values()) {
							Main.getProperties().genderPronounFemale.put(gp, ((String) Main.mainController.getWebEngine().executeScript("document.getElementById('feminine_" + gp +"').value")).toLowerCase());
							Main.getProperties().genderPronounMale.put(gp, ((String) Main.mainController.getWebEngine().executeScript("document.getElementById('masculine_" + gp +"').value")).toLowerCase());
						}
						Main.saveProperties();
						Main.game.flashMessage(Colour.GENERIC_GOOD, "Pronouns saved!");
					}
				};

			} else if (index == 2) {
				return new Response("Defaults", "Resets all pronouns to their default values.", OPTIONS_PRONOUNS){
					@Override
					public void effects() {
						for(GenderNames gn : GenderNames.values()) {
							Main.getProperties().genderNameMale.put(gn, gn.getMasculine());
							Main.getProperties().genderNameNeutral.put(gn, gn.getNeutral());
							Main.getProperties().genderNameFemale.put(gn, gn.getFeminine());
						}
						for (GenderPronoun gp : GenderPronoun.values()) {
							Main.getProperties().genderPronounFemale.put(gp, gp.getFeminine());
							Main.getProperties().genderPronounMale.put(gp, gp.getMasculine());
						}
						Main.saveProperties();

					}
				};

			} else if (index == 3) {
				return new Response(""+Util.capitaliseSentence(Main.getProperties().androgynousIdentification.getName())+"",
						"Cycle the way the game treats androgynous bodies as described above.", OPTIONS_PRONOUNS){
					@Override
					public void effects() {
						switch(Main.getProperties().androgynousIdentification){
							case FEMININE:
								Main.getProperties().androgynousIdentification=AndrogynousIdentification.CLOTHING_FEMININE;
								break;
							case CLOTHING_FEMININE:
								Main.getProperties().androgynousIdentification=AndrogynousIdentification.CLOTHING_MASCULINE;
								break;
							case CLOTHING_MASCULINE:
								Main.getProperties().androgynousIdentification=AndrogynousIdentification.MASCULINE;
								break;
							case MASCULINE:
								Main.getProperties().androgynousIdentification=AndrogynousIdentification.FEMININE;
								break;
							default:
								break;
						}

						Main.saveProperties();
					}
				};

			} else if (index == 0) {
				return new Response("Back", "Go back to the options menu.", OPTIONS);

			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	private static String getGenderNameTableRow(GenderNames name) {
		return "
"
					+ ""
						+ (name.isHasPenis()?"[style.colourGood(Penis)]":"[style.colourDisabled(Penis)]")
						+ " " + (name.isHasVagina()?"[style.colourGood(Vagina)]":"[style.colourDisabled(Vagina)]")
						+ " " + (name.isHasBreasts()?"[style.colourGood(Breasts)]":"[style.colourDisabled(Breasts)]")
					+ "	"
					+ ""
						+"<form style='padding:0;margin:0;text-align:center;'><input type='text' id='GENDER_NAME_MASCULINE_" + name + "' value='"
						+ UtilText.parseForHTMLDisplay(Main.getProperties().genderNameMale.get(name))
						+ "'>"
						+ "</form>"
					+ "	"
					+ ""
						+"<form style='padding:0;margin:0;text-align:center;'><input type='text' id='GENDER_NAME_ANDROGYNOUS_" + name + "' value='"
						+ UtilText.parseForHTMLDisplay(Main.getProperties().genderNameNeutral.get(name))
						+ "'>"
						+ "</form>"
					+ "	"
					+ ""
						+"<form style='padding:0;margin:0;text-align:center;'><input type='text' id='GENDER_NAME_FEMININE_" + name + "' value='"
						+ UtilText.parseForHTMLDisplay(Main.getProperties().genderNameFemale.get(name))
						+ "'>"
						+ "</form>"
					+ "	"
				+ "
";
	}

	private static String getPronounTableRow(GenderPronoun pronoun) {
		return "
"
				+ ""
					+ pronoun.getName()
				+ "	"
					+ ""
					+"<form style='padding:0;margin:0;text-align:center;'><input type='text' id='masculine_" + pronoun + "' value='"+ UtilText.parseForHTMLDisplay(Main.getProperties().genderPronounMale.get(pronoun))+ "'>"
					+ "</form>"
				+ "	"
				+ ""
					+"<form style='padding:0;margin:0;text-align:center;'><input type='text' id='feminine_" + pronoun + "' value='"+ UtilText.parseForHTMLDisplay(Main.getProperties().genderPronounFemale.get(pronoun))+ "'></form>"
				+ "	"
				+ "
";
	}

	public static final DialogueNode PATCH_NOTES = new DialogueNode("Patch Notes", "Patch notes", true) {

		@Override
		public String getContent(){
			return Main.patchNotes;
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 0) {
				return new Response("Back", "Go back to the options menu.", MENU);

			}else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	public static final DialogueNode DISCLAIMER = new DialogueNode("", "", true) {

		@Override
		public String getContent(){
			return Main.disclaimer;
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 0) {
				return new Response("Back", "Go back to the options menu.", MENU);

			}else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	public static final DialogueNode GENDER_PREFERENCE = new DialogueNode("Gender preferences", "", true) {

		@Override
		public String getHeaderContent(){
			UtilText.nodeContentSB.setLength(0);

			UtilText.nodeContentSB.append(
					"
"
					+ "These options will determine the gender encounter rates of random NPCs."
					+ " Some NPCs, such as random succubi attackers, have restrictions on their gender, but your preferences will be taken into account wherever possible.
"
					+ "A visual representation of the encounter chances can be seen in the bars at the bottom of each section."
					+ " (The different shades of each gender are solely for recognition in the bars, and don't mean anything other than that.)"
					+ "
"
					+ "A character is considered to have breasts if they are at least an AA-cup."
					+ "
");

			UtilText.nodeContentSB.append(getGenderPreferencesPanel(PronounType.MASCULINE));
			UtilText.nodeContentSB.append(getGenderPreferencesPanel(PronounType.NEUTRAL));
			UtilText.nodeContentSB.append(getGenderPreferencesPanel(PronounType.FEMININE));

			return UtilText.nodeContentSB.toString();
		}

		@Override
		public String getContent(){
			return "";
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			 if (index == 0) {
				return new Response("Back", "Go back to the options menu.", OPTIONS);

			} else if (index == 1) {
				return new Response("Defaults", "Restore all gender preferences to their default values.", GENDER_PREFERENCE) {
					@Override
					public void effects() {
						Main.getProperties().resetGenderPreferences();
						Main.getProperties().savePropertiesAsXML();
					}
				};

			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	private static String getGenderPreferencesPanel(PronounType type) {
		int count = 0;
		Colour colour = Colour.MASCULINE;
		switch(type) {
			case FEMININE:
				colour = Colour.FEMININE;
				break;
			case MASCULINE:
				colour = Colour.MASCULINE;
				break;
			case NEUTRAL:
				colour = Colour.ANDROGYNOUS;
				break;
		}

		StringBuilder sb = new StringBuilder();

		sb.append("
"
				+ "
"+Util.capitaliseSentence(type.getName())+"
");

		for(Gender g : Gender.values()) {
			if(g.getType()==type) {
				sb.append(
						"
"
							+ "
"
								+ "
" +Util.capitaliseSentence(g.getName())+"
");

				for(ContentPreferenceValue preference : ContentPreferenceValue.values()) {
					sb.append("
"+Util.capitaliseSentence(preference.getName())+"
");
				}

				sb.append("

"
								+ "" +Util.capitaliseSentence(g.getName())+"s have "
										+(g.getGenderName().isHasVagina()?"a [style.colourGood(vagina)]":"no [style.colourBad(vagina)]")+", "
										+(g.getGenderName().isHasPenis()?"a [style.colourGood(penis)]":"no [style.colourBad(penis)]")+", and "
										+ (g.getGenderName().isHasBreasts()?"[style.colourGood(breasts)]":"no [style.colourBad(breasts)]")+"."
								+ "
"
							+ "
"
						+ "
"
						+ "
");
				count++;
			}
		}

		sb.append(
				getGenderRepresentation()
				+"
");

		return sb.toString();
	}

	private static String getOrientationRepresentation() {
		float total=0;
		for(SexualOrientation o : SexualOrientation.values()) {
			total+=Main.getProperties().orientationPreferencesMap.get(o);
		}

		StringBuilder sb = new StringBuilder();

		if(total==0) {
			sb.append("
");

		} else {
			sb.append("
");

			for(SexualOrientation o : SexualOrientation.values()) {
				sb.append("

");
			}
		}

		sb.append("
");

		return sb.toString();
	}

	public static final DialogueNode ORIENTATION_PREFERENCE = new DialogueNode("Orientation preferences", "", true) {

		@Override
		public String getHeaderContent(){
			UtilText.nodeContentSB.setLength(0);

			UtilText.nodeContentSB.append(
					"
"
					+ "These options will determine the sexual orientation encounter rates of random NPCs."
					+ " Note that the race and femininity of NPCs can have an influence on their orientation, and that some NPCs have pre-determined orientations, but your preferences will be taken into account wherever possible.
"
					+ "A visual representation of the encounter chances can be seen in the bars at the bottom."
					+ " (The different shades of each orientation are solely for recognition in the bars, and don't mean anything other than that.)"
					+ "
"

					+ "
");

			UtilText.nodeContentSB.append(getOrientationPreferencesPanel(SexualOrientation.ANDROPHILIC));
			UtilText.nodeContentSB.append(getOrientationPreferencesPanel(SexualOrientation.AMBIPHILIC));
			UtilText.nodeContentSB.append(getOrientationPreferencesPanel(SexualOrientation.GYNEPHILIC));

			UtilText.nodeContentSB.append(getOrientationRepresentation() + "
");

			return UtilText.nodeContentSB.toString();
		}

		@Override
		public String getContent(){
			return "";
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			 if (index == 0) {
				return new Response("Back", "Go back to the options menu.", OPTIONS);

			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	private static String getOrientationPreferencesPanel(SexualOrientation orient) {
		Colour colour = orient.getColour();

		StringBuilder sb = new StringBuilder();

		sb.append("
"
				+ "
"
					+ "
" +Util.capitaliseSentence(orient.getName())+"
");

		for(SexualOrientationPreference preference : SexualOrientationPreference.values()) {
			sb.append("
"
							+Util.capitaliseSentence(preference.getName())
						+"
");
		}

		sb.append("
"
				+ "
"
				+ "

");

		return sb.toString();
	}

	private static String getGenderRepresentation() {

		float total=0;
		for(Gender g : Gender.values()) {
			total+=Main.getProperties().genderPreferencesMap.get(g);
		}

		StringBuilder sb = new StringBuilder();

		if(total==0) {
			sb.append("
");

		} else {
			sb.append("
");

			int f=0, m=0, n=0;
			for(Gender g : Gender.values()) {
				switch(g.getType()) {
					case MASCULINE:
						if(Main.getProperties().genderPreferencesMap.get(g)>0) {
							sb.append("

");
							sb.append("

");
						}
						m++;
						break;
					case NEUTRAL:
						if(Main.getProperties().genderPreferencesMap.get(g)>0) {
							sb.append("

");
							sb.append("

");
						}
						n++;
						break;
					case FEMININE:
						if(Main.getProperties().genderPreferencesMap.get(g)>0) {
							sb.append("

");
							sb.append("

");
						}
						f++;
						break;
					default:
						break;
				}
			}
		}

		sb.append("
");

		return sb.toString();
	}

	public static final DialogueNode AGE_PREFERENCE = new DialogueNode("Age preferences", "", true) {

		@Override
		public String getHeaderContent(){
			UtilText.nodeContentSB.setLength(0);

			UtilText.nodeContentSB.append(
					"
"
					+ "These options will determine the age encounter rates of random NPCs, based on their femininity."
					+ " Some NPCs, such as demons and harpies, may appear to be younger than they actually are, but your preferences will be taken into account wherever possible.
"
					+ "A visual representation of the age chances can be seen in the bars at the bottom of each section."
					+ "
");

			UtilText.nodeContentSB.append(getAgePreferencesPanel(PronounType.MASCULINE));
			UtilText.nodeContentSB.append(getAgePreferencesPanel(PronounType.NEUTRAL));
			UtilText.nodeContentSB.append(getAgePreferencesPanel(PronounType.FEMININE));

			return UtilText.nodeContentSB.toString();
		}

		@Override
		public String getContent(){
			return "";
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 0) {
				return new Response("Back", "Go back to the options menu.", OPTIONS);

			} else if (index == 1) {
				return new Response("Defaults", "Restore all age preferences to their default values.", AGE_PREFERENCE) {
					@Override
					public void effects() {
						Main.getProperties().resetAgePreferences();
						Main.getProperties().savePropertiesAsXML();
					}
				};

			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	private static String getAgePreferencesPanel(PronounType type) {

		StringBuilder sb = new StringBuilder();

		sb.append("
"
				+ "
"+Util.capitaliseSentence(type.getName())+"
");

		int i=AgeCategory.values().length-1;
		for(AgeCategory ageCat : AgeCategory.values()) {
			sb.append(
					"
"
						+ "
"
							+ "
" +Util.capitaliseSentence(ageCat.getName())+"
");

			for(ContentPreferenceValue preference : ContentPreferenceValue.values()) {
				sb.append(
						"
"
								+Util.capitaliseSentence(preference.getName())
						+"
");
			}

			sb.append("
"
					+ "
"
					+ "
");
			i--;
		}

		sb.append(
				getAgeRepresentation(type)
				+"
");

		return sb.toString();
	}

	private static String getAgeRepresentation(PronounType type) {

		float total=0;
		for(AgeCategory ageCat : AgeCategory.values()) {
			total+=Main.getProperties().agePreferencesMap.get(type).get(ageCat);
		}

		StringBuilder sb = new StringBuilder();

		if(total==0) {
			sb.append("
");

		} else {
			sb.append("
");

			int i=(AgeCategory.values().length*2)-1;
			for(AgeCategory ageCat : AgeCategory.values()) {
				if(Main.getProperties().agePreferencesMap.get(type).get(ageCat)>0) {
					sb.append("

");
					sb.append("

");
				}
				i--;
				i--;
			}
		}

		sb.append("
");

		return sb.toString();
	}

	public static final DialogueNode FURRY_PREFERENCE = new DialogueNode("Furry preferences", "", true) {

		@Override
		public String getHeaderContent(){
			UtilText.nodeContentSB.setLength(0);

			UtilText.nodeContentSB.append(
					"
"
						+ "These options determine the amount of furry content that you'll encounter in the game."
						+ " The 'Human encounters' option determines what the chance is for random NPCs to be fully human."
						+ " These options only affect random NPCs at the moment, but I'll do my best to add reduced-furry versions of each major NPC as well!"

						+ "

[style.boldGood(Hover over the buttons to see what each option means!)]"

						+ "

Please note that mythological and demonic races, such as harpies and demons, are not affected by furry preferences."
					+ "
"

					+ "");

			UtilText.nodeContentSB.append(
					"
"

						+"
"
							+ "Human encounters"
							+ "
"
								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleZero()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleZeroDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleOne()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleOneDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleTwo()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleTwoDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleThree()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleThreeDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleFour()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleFourDisabled()+"

")
							+"
"
						+ "
"

						+"
"
							+ "Taurs"
							+ "
"
								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleZero()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleZeroDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleOne()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleOneDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleTwo()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleTwoDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleThree()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleThreeDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleFour()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleFourDisabled()+"

")
							+"
"
						+ "
"

						+"
"
							+ "Forced TF Racial Limits"
							+ "
"
								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleZero()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleZeroDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleOne()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleOneDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleTwo()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleTwoDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleThree()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleThreeDisabled()+"

")

								+ "

"+SVGImages.SVG_IMAGE_PROVIDER.getScaleFour()+"

"
									:"'>
"+SVGImages.SVG_IMAGE_PROVIDER.getScaleFourDisabled()+"

")
							+"
"
						+ "
"

					+ "
");

			UtilText.nodeContentSB.append("
"
												+ "
"
													+"
"
														+ "Set all furry preferences:"
													+ "
");
			for(FurryPreference fp : FurryPreference.values()) {
				UtilText.nodeContentSB.append("
"+fp.getName()+"
");
			}
			UtilText.nodeContentSB.append("
"
												+ "
"
													+"
"
														+ "Set all spawn frequencies:"
													+ "
");
			for(SubspeciesPreference sp : SubspeciesPreference.values()) {
				UtilText.nodeContentSB.append("
"+Util.capitaliseSentence(sp.getName())+"
");
			}
			UtilText.nodeContentSB.append("
"
											+"
");

			UtilText.nodeContentSB.append("
"
											+"
"
												+"
"
													+ "Furry Preference"
													+ "Spawn frequency"
												+ "
"
											+ "
");

			int i=0;
			for(Subspecies subspecies : Subspecies.values()) {
				if(subspecies.isDisplayedInFurryPreferences()) {
					UtilText.nodeContentSB.append(getSubspeciesPreferencesPanel(subspecies, i%2==0));
					i++;
				}
			}
			UtilText.nodeContentSB.append("
");

			return UtilText.nodeContentSB.toString();
		}

		@Override
		public String getContent(){
			return "";
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			 if (index == 0) {
				return new Response("Back", "Go back to the options menu.", OPTIONS);

			} else if(index==1) {
				return new Response("Defaults", "Reset all furry and spawn preferences to their default settings.", FURRY_PREFERENCE) {
					@Override
					public void effects() {
						for(Subspecies subspecies : Subspecies.values()) {
							Main.getProperties().setFeminineFurryPreference(subspecies, FurryPreference.NORMAL);
							Main.getProperties().setMasculineFurryPreference(subspecies, FurryPreference.NORMAL);

							Main.getProperties().setFeminineSubspeciesPreference(subspecies, subspecies.getSubspeciesPreferenceDefault());
							Main.getProperties().setMasculineSubspeciesPreference(subspecies, subspecies.getSubspeciesPreferenceDefault());
						}
						Main.saveProperties();
					}
				};
			}

			return null;
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	private static String getEntryBackgroundColour(boolean alternative) {
		if(Main.getProperties().hasValue(PropertyValue.lightTheme)) {
			if(alternative) {
				return "#d9d9d9";
			}
			return "#dddddd";
		} else {
			if(alternative) {
				return "#222222";
			}
			return "#1f1f1f";
		}
	}

	private static String getSubspeciesPreferencesPanel(Subspecies s, boolean altColour) {
		StringBuilder sb = new StringBuilder();

		sb.append("
");

			sb.append("
"
							+ "
"+Util.capitaliseSentence(s.getNamePlural(null))+"
"
							+ "
"+SVGImages.SVG_IMAGE_PROVIDER.getInformationIcon()+"
"
						+ "
");

			// Feminine:
			sb.append("
"
						+"" +Util.capitaliseSentence(s.getSingularFemaleName(null))+""
					+"
");

			sb.append("
");

				for(FurryPreference preference : FurryPreference.values()) {
					sb.append("

"+preference.getSVGImage(false)+"

"
									:"' style='max-width:30px; width:7%;'>
"+preference.getSVGImage(true)+"

"));
				}
				sb.append("

");
				for(SubspeciesPreference preference : SubspeciesPreference.values()) {
					sb.append("

"+preference.getSVGImage(false)+"

"
									:"' style='max-width:30px; width:7%;'>
"+preference.getSVGImage(true)+"

"));
				}

			sb.append("
");

			// Masculine:
			sb.append("
"
					+"" +Util.capitaliseSentence(s.getSingularMaleName(null))+""
				+"
");

			sb.append("
");

				for(FurryPreference preference : FurryPreference.values()) {
					sb.append("

"+preference.getSVGImage(false)+"

"
									:"' style='max-width:30px; width:7%;'>
"+preference.getSVGImage(true)+"

"));
				}
				sb.append("

");
				for(SubspeciesPreference preference : SubspeciesPreference.values()) {
					sb.append("

"+preference.getSVGImage(false)+"

"
									:"' style='max-width:30px; width:7%;'>
"+preference.getSVGImage(true)+"

"));
				}

			sb.append("
");

		sb.append("
");

		return sb.toString();
	}

	public static final DialogueNode UNIT_PREFERENCE = new DialogueNode("Unit preferences", "", true) {

		@Override
		public String getHeaderContent() {
			UtilText.nodeContentSB.setLength(0);

			UtilText.nodeContentSB.append(
					"
"
							+ "These options determine the units used throughout the game."
							+ " Using auto detect will attempt to identify the correct settings for your system language."
							+ "
Overriding any option disables auto detect."
							+ "
"

							+ "");

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
					"AUTO_LOCALE",
					Colour.BASE_BLUE_LIGHT,
					"Automatic",
					"When enabled, the system locale is used. Otherwise, the following options are applied.",
					Main.getProperties().hasValue(PropertyValue.autoLocale)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
					"METRIC_SIZES",
					Colour.BASE_BLUE_STEEL,
					"Metric sizes",
					"The game will use metres and centimetres instead of feet and inches.",
					Main.getProperties().hasValue(PropertyValue.metricSizes)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
					"METRIC_FLUIDS",
					Colour.BASE_BLUE_STEEL,
					"Metric fluids",
					"The game will use litres and millilitres instead of gallons and ounces.",
					Main.getProperties().hasValue(PropertyValue.metricFluids)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
					"METRIC_WEIGHTS",
					Colour.BASE_BLUE_STEEL,
					"Metric weights",
					"The game will use kilograms and grams instead of pounds and ounces.",
					Main.getProperties().hasValue(PropertyValue.metricWeights)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
					"TWENTYFOUR_HOUR_TIME",
					Colour.BASE_LILAC_LIGHT,
					"24 hour time",
					"The time will be displayed as 24 hours instead of AM/PM.",
					Main.getProperties().hasValue(PropertyValue.twentyFourHourTime)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
					"INTERNATIONAL_DATE",
					Colour.BASE_LILAC_LIGHT,
					"International date",
					"The abbreviated date will be displayed as day.month.year instead of month/day/year.",
					Main.getProperties().hasValue(PropertyValue.internationalDate)));

			return UtilText.nodeContentSB.toString();
		}

		@Override
		public String getContent() {
			return "";
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 0) {
				return new Response("Back", "Go back to the options menu.", OPTIONS);

			} else {
				return null;
			}
		}
	};

	public static final DialogueNode CONTENT_PREFERENCE = new DialogueNode("Content Options", "", true) {

		@Override
		public String getHeaderContent(){
			UtilText.nodeContentSB.setLength(0);

			UtilText.nodeContentSB.append(getCustomContentPreferenceDivStart(Colour.GENERIC_GOOD, "Autosave Frequency", "Choose how often want the game to autosave when you transition from one map to another."));
			for(int i=2; i>=0; i--) {
				UtilText.nodeContentSB.append("
"
						+(Main.getProperties().autoSaveFrequency==i
							?"[style.boldGood("
							:"[style.colourDisabled(")
						+com.lilithsthrone.game.Properties.autoSaveLabels[i]+")]
");
			}
			UtilText.nodeContentSB.append("

");

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
								"ENCHANTMENT_LIMITS",
								Colour.GENERIC_ARCANE,
								"Enchantment Capacity",
								"Toggle the 'enchantment capacity' mechanic, which restricts how many enchanted items you can wear. This is on by default, and you will potentially break the balance of the game's combat by turning it off.",
								Main.getProperties().hasValue(PropertyValue.enchantmentLimits)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
								"ARTWORK",
								Colour.BASE_BLUE_LIGHT,
								"Artwork",
								"Enables artwork to be displayed in characters' information screens.",
								Main.getProperties().hasValue(PropertyValue.artwork)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv("THUMBNAIL",
								Colour.BASE_BLUE_STEEL,
								"Thumbnails",
								"Enables tooltips containing thumbnail images of the character.",
								Main.getProperties().hasValue(PropertyValue.thumbnail)));

			UtilText.nodeContentSB.append(getCustomContentPreferenceDivStart(Colour.BASE_AQUA, "Preferred Artist", "Which artist's work is used by default."));
			List<Artist> artists = new ArrayList<>(Artwork.allArtists);
			Collections.reverse(artists);// So that they're in alphabetical order
			for(Artist artist : artists) {
				if (!artist.getName().equals("Custom")) {
					UtilText.nodeContentSB.append(
							(Main.getProperties().preferredArtist.equals(artist.getFolderName())
									?"
"
									+ ""+artist.getName()+""
									+ "
"
									:"
"
									+ "[style.boldDisabled("+artist.getName()+")]"
									+ "
"));
				}
			}
			UtilText.nodeContentSB.append("

");

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"AUTO_SEX_CLOTHING_MANAGEMENT",
							Colour.BASE_BLUE_STEEL,
							"Post-sex clothing replacement",
							"Enables equipped clothing to be automatically pulled back into their pre-sex states after sex scenes.",
							Main.getProperties().hasValue(PropertyValue.autoSexClothingManagement)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"AGE",
							Colour.AGE_TWENTIES,
							"Age",
							"This enables descriptions of the age that characters appear to be.",
							Main.getProperties().hasValue(PropertyValue.ageContent)));

			UtilText.nodeContentSB.append(OptionsDialogue.getContentPreferenceVariableDiv(
                    		"BIRTH_AGE",
                    		Colour.BASE_BLUE_LIGHT,
                    		"Age offspring spawn at",
                    		"This changes the age group of spawned offspring, current age group is "+AgeCategory.valueOf(Main.getProperties().birthAge).getName()+" because of the current target age.",
                    		Main.getProperties().birthAge+"",
                    		Main.getProperties().birthAge,
                    		1,
                    		30));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"NON_CON",
							Colour.BASE_CRIMSON,
							"Non-consent",
							"This enables the 'resist' pace in sex scenes, which contains some more extreme non-consensual descriptions.",
							Main.getProperties().hasValue(PropertyValue.nonConContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"SADISTIC_SEX",
							Colour.BASE_RED,
							"Sadistic sex",
							"This unlocks 'sadistic' sex actions, such as choking, slapping, and spitting on partners in sex.",
							Main.getProperties().hasValue(PropertyValue.sadisticSexContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"SILLY",
							Colour.GENERIC_GOOD,
							"Silly mode",
							"This enables funny flavour text throughout the game.",
							Main.getProperties().hasValue(PropertyValue.sillyMode)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"OPPORTUNISTIC_ATTACKERS",
							Colour.BASE_CRIMSON,
							"Opportunistic attackers",
							"This makes random attacks more likely when you're high on lust, low on health, covered in fluids, exposed, or drunk.",
							Main.game.isOpportunisticAttackersEnabled()));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"BYPASS_SEX_ACTIONS",
							Colour.BASE_PINK,
							"Sex action bypass",
							"If disabled, action requirements during sex may no longer be bypassed. (i.e. All 'Corruptive' actions will be unavailable.)",
							Main.getProperties().hasValue(PropertyValue.bypassSexActions)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"VOLUNTARY_NTR",
							Colour.GENERIC_MINOR_BAD,
							"Voluntary NTR",
							"When enabled, you will get the option to offer certain enemies sex with your companions as a way to avoid combat.",
							Main.getProperties().hasValue(PropertyValue.voluntaryNTR)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"INVOLUNTARY_NTR",
							Colour.GENERIC_BAD,
							"Involuntary NTR",
							"When enabled, enemies might choose to only have sex with your companion after beating your party in combat."
									+ " When disabled, all post-combat-loss sex scenes will involve you.",
							Main.getProperties().hasValue(PropertyValue.involuntaryNTR)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"INCEST",
							Colour.BASE_ROSE,
							"Incest",
							"This will enable sexual actions with all of your blood-relatives.",
							Main.getProperties().hasValue(PropertyValue.incestContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"LACTATION",
							Colour.BASE_YELLOW_LIGHT,
							"Lactation",
							"This enables lactation content.",
							Main.getProperties().hasValue(PropertyValue.lactationContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"CUM_REGENERATION",
							Colour.CUM,
							"Cum Regeneration",
							"This enables cum regeneration related content, such as decreasing quantity for multiple orgasms in one session and the full balls status effect."
							+ "
When disabled, balls will always be treated as full, but without any negative effects.",
							Main.getProperties().hasValue(PropertyValue.cumRegenerationContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"URETHRAL",
							Colour.BASE_PINK_DEEP,
							"Urethral content",
							"This enables urethral transformations and penetrations.",
							Main.getProperties().hasValue(PropertyValue.urethralContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"NIPPLE_PEN",
							Colour.BASE_PINK_DEEP,
							"Nipple Penetrations",
							"This enables nipple-penetration transformations and sex actions.",
							Main.getProperties().hasValue(PropertyValue.nipplePenContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"ANAL",
							Colour.BASE_ORANGE,
							"Anal Content",
							"When disabled, removes all anal-related actions from being available during sex.",
							Main.getProperties().hasValue(PropertyValue.analContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"FOOT",
							Colour.BASE_TAN,
							"Foot Content",
							"When disabled, removes all foot-related actions from being available during sex.",
							Main.getProperties().hasValue(PropertyValue.footContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"FUTA_BALLS",
							Colour.BASE_PINK,
							"Futanari Testicles",
							"When enabled, futanari NPCs will be able to have external testicles. When disabled, they are locked to always being internal.",
							Main.getProperties().hasValue(PropertyValue.futanariTesticles)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"CLOACA",
							Colour.BASE_PINK_LIGHT,
							"Bipedal Cloacas",
							"When enabled, certain bipedal races (such as harpies and alligator-morphs) will have cloacas."
									+ " When disabled, all bipeds with cloacas will be treated as having a regular genitalia configuration."
									+ " Some special races, such as lamia, always have cloacas, and are not affected by this.",
							Main.getProperties().hasValue(PropertyValue.bipedalCloaca)));

			UtilText.nodeContentSB.append(getCustomContentPreferenceDivStart(Colour.NIPPLES, "Multi-breasts", "Choose how you want the game to display multi-breasts."));
			for(int i=2; i>=0; i--) {
				UtilText.nodeContentSB.append("
"
							+(Main.getProperties().multiBreasts==i
								?(i==0?"[style.boldBad(":"[style.boldGood(")
								:"[style.colourDisabled(")
							+com.lilithsthrone.game.Properties.multiBreastsLabels[i]+")]
");
			}
			UtilText.nodeContentSB.append("

");

			UtilText.nodeContentSB.append(getCustomContentPreferenceDivStart(Colour.NIPPLES_CROTCH, "Crotch-boobs & Udders", "Choose how you want the game to handle udders and crotch-boobs."));
			for(int i=2; i>=0; i--) {
				UtilText.nodeContentSB.append("
"
						+(Main.getProperties().udders==i
							?(i==0?"[style.boldBad(":"[style.boldGood(")
							:"[style.colourDisabled(")
						+com.lilithsthrone.game.Properties.uddersLabels[i]+")]
");
			}
			UtilText.nodeContentSB.append("

");

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"HAIR_FACIAL",
							Colour.BASE_LILAC_LIGHT,
							"Facial hair",
							"This enables facial hair descriptions and content.",
							Main.getProperties().hasValue(PropertyValue.facialHairContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"HAIR_PUBIC",
							Colour.BASE_LILAC,
							"Pubic hair",
							"This enables pubic hair descriptions and content.",
							Main.getProperties().hasValue(PropertyValue.pubicHairContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
						"HAIR_BODY",
						Colour.BASE_PURPLE,
						"Underarm hair",
						"This enables underarm hair descriptions and content.",
						Main.getProperties().hasValue(PropertyValue.bodyHairContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
						"HAIR_ASS",
						Colour.BASE_PURPLE_DARK,
						"Ass hair",
						"This enables ass hair descriptions and content.",
						Main.getProperties().hasValue(PropertyValue.assHairContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"FEMININE_BEARD",
							Colour.BASE_BLUE_STEEL,
							"Feminine Beards",
							"This enables feminine characters to grow beards.",
							Main.getProperties().hasValue(PropertyValue.feminineBeardsContent)));

			UtilText.nodeContentSB.append(getContentPreferenceVariableDiv(
							"FORCED_TF",
							Colour.TRANSFORMATION_GENERIC,
							"Forced TF",
							"This sets the amount of NPCs spawning with the '"+Fetish.FETISH_TRANSFORMATION_GIVING.getName(null)+"' fetish, which causes them to forcibly transform you after beating you in combat.",
							Main.getProperties().forcedTFPercentage+"%",
							Main.getProperties().forcedTFPercentage,
							0,
							100));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
					"SPITTING_ENABLED",
					Colour.BASE_BLUE,
					"Rejecting TF potions",
					"Forced TF potions may be spat out if this is enabled.",
					!Main.game.isSpittingDisabled()));

			UtilText.nodeContentSB.append(getCustomContentPreferenceDivStart(Colour.BASE_GREEN, "Forced TF Gender Tendency", "This allows you to override NPC tastes when a forced transformation will alter your gender presentation."));
			UtilText.nodeContentSB.append((Main.getProperties().getForcedTFTendency()==ForcedTFTendency.NEUTRAL
												?"
"
													+ ForcedTFTendency.NEUTRAL.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedTFTendency.NEUTRAL.getName()+")]"
													+ "
")
											+ (Main.getProperties().getForcedTFTendency()==ForcedTFTendency.FEMININE
												?"
"
													+ ForcedTFTendency.FEMININE.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedTFTendency.FEMININE.getName()+")]"
													+ "
")
											+(Main.getProperties().getForcedTFTendency()==ForcedTFTendency.FEMININE_HEAVY
												?"
"
													+ ForcedTFTendency.FEMININE_HEAVY.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedTFTendency.FEMININE_HEAVY.getName()+")]"
													+ "
")
											+(Main.getProperties().getForcedTFTendency()==ForcedTFTendency.MASCULINE_HEAVY
												?"
"
													+ ForcedTFTendency.MASCULINE_HEAVY.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedTFTendency.MASCULINE_HEAVY.getName()+")]"
													+ "
")
											+(Main.getProperties().getForcedTFTendency()==ForcedTFTendency.MASCULINE
												?"
"
													+ ForcedTFTendency.MASCULINE.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedTFTendency.MASCULINE.getName()+")]"
													+ "
"));
			UtilText.nodeContentSB.append("

");

			UtilText.nodeContentSB.append(getContentPreferenceVariableDiv(
							"FORCED_FETISH",
							Colour.FETISH,
							"Forced Fetishes",
							"This sets the amount of NPCs spawning with the '"+Fetish.FETISH_KINK_GIVING.getName(null)+"' fetish, which causes them to try and forcibly give you fetishes after beating you in combat.",
							Main.getProperties().forcedFetishPercentage+"%",
							Main.getProperties().forcedFetishPercentage,
							0,
							100));

			UtilText.nodeContentSB.append(getCustomContentPreferenceDivStart(Colour.FETISH, "Forced Fetish Tendency",
					"This allows you to override NPC tastes and control the tendency for forced fetishes to be for topping or bottoming."));
			UtilText.nodeContentSB.append((Main.getProperties().getForcedFetishTendency()==ForcedFetishTendency.NEUTRAL
												?"
"
													+ ForcedFetishTendency.NEUTRAL.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedFetishTendency.NEUTRAL.getName()+")]"
													+ "
")
										+(Main.getProperties().getForcedFetishTendency()==ForcedFetishTendency.BOTTOM
												?"
"
													+ ForcedFetishTendency.BOTTOM.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedFetishTendency.BOTTOM.getName()+")]"
													+ "
")
										+(Main.getProperties().getForcedFetishTendency()==ForcedFetishTendency.BOTTOM_HEAVY
												?"
"
													+ ForcedFetishTendency.BOTTOM_HEAVY.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedFetishTendency.BOTTOM_HEAVY.getName()+")]"
													+ "
")
										+(Main.getProperties().getForcedFetishTendency()==ForcedFetishTendency.TOP_HEAVY
												?"
"
													+ ForcedFetishTendency.TOP_HEAVY.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedFetishTendency.TOP_HEAVY.getName()+")]"
													+ "
")
										+(Main.getProperties().getForcedFetishTendency()==ForcedFetishTendency.TOP
												?"
"
													+ ForcedFetishTendency.TOP.getName()
													+ "
"
												:"
"
													+ "[style.colourDisabled("+ForcedFetishTendency.TOP.getName()+")]"
													+ "
"));
			UtilText.nodeContentSB.append("

");

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
						"FURRY_TAIL_PENETRATION",
						Colour.BASE_MAGENTA,
						"Furry tail penetrations",
						"This enables furry tails to engage in penetrative actions in sex.",
						Main.getProperties().hasValue(PropertyValue.furryTailPenetrationContent)));

			UtilText.nodeContentSB.append(getContentPreferenceDiv(
							"INFLATION_CONTENT",
							Colour.CUM,
							"Cum Inflation",
							"This enables cum inflation mechanics.",
							Main.getProperties().hasValue(PropertyValue.inflationContent)));

			UtilText.nodeContentSB.append(getBreastsContentPreferenceVariableDiv( //TODO
							"PREGNANCY_BREAST_GROWTH",
							Colour.BASE_PINK,
							"Average Pregnancy Breast Growth",
							"Set the average cup size growth that characters will gain from each pregnancy. Actual breast growth will be within "+Util.intToString(Main.getProperties().pregnancyBreastGrowthVariance)+" sizes of this value.",
							Main.getProperties().pregnancyBreastGrowth==0
								?"[style.boldDisabled(Disabled)]"
								:Main.getProperties().pregnancyBreastGrowth+" cup"+(Main.getProperties().pregnancyBreastGrowth!=1?"s":""),
							Main.getProperties().pregnancyBreastGrowth, 0, 10,
							Main.getProperties().pregnancyUdderGrowth==0
								?"[style.boldDisabled(Disabled)]"
								:Main.getProperties().pregnancyUdderGrowth+" cup"+(Main.getProperties().pregnancyUdderGrowth!=1?"s":""),
							Main.getProperties().pregnancyUdderGrowth, 0, 10));

			UtilText.nodeContentSB.append(getBreastsContentPreferenceVariableDiv(
							"PREGNANCY_BREAST_GROWTH_LIMIT",
							Colour.BASE_PINK_LIGHT,
							"Pregnancy Breast Growth Limit",
							"Set the maximum limit of cup size that characters' breasts will grow to from pregnancies.",
							CupSize.getCupSizeFromInt(Main.getProperties().pregnancyBreastGrowthLimit).getCupSizeName()+"-cup",
							Main.getProperties().pregnancyBreastGrowthLimit, 0, 100,
							CupSize.getCupSizeFromInt(Main.getProperties().pregnancyUdderGrowthLimit).getCupSizeName()+"-cup",
							Main.getProperties().pregnancyUdderGrowthLimit, 0, 100));

			UtilText.nodeContentSB.append(getBreastsContentPreferenceVariableDiv(
							"PREGNANCY_LACTATION",
							Colour.BASE_YELLOW,
							"Average Pregnancy Lactation",
							"Set the average increase in lactation that characters will gain as a result of each pregnancy. Actual lactation increase will be within "
									+Units.fluid(Main.getProperties().pregnancyLactationIncreaseVariance)+" of this value.",
							Main.getProperties().pregnancyLactationIncrease==0
								?"[style.boldDisabled(Disabled)]"
								:Units.fluid(Main.getProperties().pregnancyLactationIncrease),
							Main.getProperties().pregnancyLactationIncrease, 0, 1000,
							Main.getProperties().pregnancyUdderLactationIncrease==0
								?"[style.boldDisabled(Disabled)]"
								:Units.fluid(Main.getProperties().pregnancyUdderLactationIncrease),
							Main.getProperties().pregnancyUdderLactationIncrease, 0, 1000));

			UtilText.nodeContentSB.append(getBreastsContentPreferenceVariableDiv(
							"PREGNANCY_LACTATION_LIMIT",
							Colour.BASE_YELLOW_LIGHT,
							"Pregnancy Lactation Limit",
							"Set the maximum limit of lactation that characters will gain from pregnancies.",
							Units.fluid(Main.getProperties().pregnancyLactationLimit, Units.ValueType.PRECISE, Units.UnitType.SHORT),
							Main.getProperties().pregnancyLactationLimit, 0, Lactation.SEVEN_MONSTROUS_AMOUNT_POURING.getMaximumValue(),
							Units.fluid(Main.getProperties().pregnancyUdderLactationLimit, Units.ValueType.PRECISE, Units.UnitType.SHORT),
							Main.getProperties().pregnancyUdderLactationLimit, 0, Lactation.SEVEN_MONSTROUS_AMOUNT_POURING.getMaximumValue()));

			UtilText.nodeContentSB.append(getBreastsContentPreferenceVariableDiv(
							"BREAST_SIZE_PREFERENCE",
							Colour.NIPPLES,
							"Cup Size Preference",
							"Affects randomly-generated NPCs' cup sizes (will not be reduced to below AA-cup).",
							(Main.getProperties().breastSizePreference>=0?"+":"") + Main.getProperties().breastSizePreference,
							Main.getProperties().breastSizePreference, -20, 20,
							(Main.getProperties().udderSizePreference>=0?"+":"") + Main.getProperties().udderSizePreference,
							Main.getProperties().udderSizePreference, -20, 20));

			UtilText.nodeContentSB.append(getContentPreferenceVariableDiv(
							"PENIS_SIZE_PREFERENCE",
							Colour.PENIS,
							"Penis Size Preference",
							"Affects randomly-generated NPCs' penis sizes (will not be reduced to below "+Units.size(8)+").",
							(Main.getProperties().penisSizePreference>=0?"+":"") + Units.size(Main.getProperties().penisSizePreference, ValueType.PRECISE, UnitType.SHORT),
							Main.getProperties().penisSizePreference,
							-20,
							20));

			return UtilText.nodeContentSB.toString();
		}

		@Override
		public String getContent(){
			return "";
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 0) {
				return new Response("Back", "Go back to the options menu.", MENU);

			} else {
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};

	/**
	 * TO be followed by:

	 */
	private static String getCustomContentPreferenceDivStart(Colour colour, String title, String description) {
		StringBuilder contentSB = new StringBuilder();

		contentSB.append(
				"
"
					+ "
"
						+ ""+ title+": "
						+ description
					+ "
"
					+ "
");

		return contentSB.toString();
	}

	private static String getContentPreferenceDiv(String id, Colour colour, String title, String description, boolean enabled) {
		StringBuilder contentSB = new StringBuilder();

		contentSB.append(
				"
"
					+ "
"
						+ ""+ title+": "
						+ description
					+ "
"
					+ "
");

		if(enabled) {
			contentSB.append(
					"
"
							+ "[style.boldGood(ON)]"
						+ "
"
					+ "
"
						+ "[style.colourDisabled(OFF)]"
					+ "
");
		} else {
			contentSB.append(
					"
"
						+ "[style.colourDisabled(ON)]"
					+ "
"
					+"
"
						+ "[style.boldBad(OFF)]"
					+ "
");
		}

		contentSB.append("
"
				+ "
");

		return contentSB.toString();
	}

	private static String getBreastsContentPreferenceVariableDiv(
			String id,
			Colour colour,
			String title,
			String description,
			String valueDisplay,
			int value, int minimum, int maximum,
			String valueDisplayUdders,
			int valueUdders, int minimumUdders, int maximumUdders) {

		StringBuilder contentSB = new StringBuilder();

		contentSB.append(
				"
"
					+ "
"
						+ ""+ title+": "
						+ description
					+ "
"
					+ "
");

		contentSB.append(
				"
"
					+ "Breasts: "
					+ "
"
							+ (value==maximum?"[style.boldDisabled(+)]":"[style.boldGood(+)]")
					+ "
"
					+ "
"
						+ ""+valueDisplay+""
					+ "
"
					+ "
"
						+ (value==minimum?"[style.boldDisabled(-)]":"[style.boldBad(-)]")
					+ "
"
				+ "
");

		contentSB.append(
				"
"
					+ "Udders: "
					+ "
"
							+ (valueUdders==maximumUdders?"[style.boldDisabled(+)]":"[style.boldGood(+)]")
					+ "
"
					+ "
"
						+ ""+valueDisplayUdders+""
					+ "
"
					+ "
"
						+ (valueUdders==minimumUdders?"[style.boldDisabled(-)]":"[style.boldBad(-)]")
					+ "
"
				+ "
");

		contentSB.append("
"
				+"
");

		return contentSB.toString();
	}

	private static String getContentPreferenceVariableDiv(String id, Colour colour, String title, String description, String valueDisplay, int value, int minimum, int maximum) {
		StringBuilder contentSB = new StringBuilder();

		contentSB.append(
				"
"
					+ "
"
						+ ""+ title+": "
						+ description
					+ "
"
					+ "
");

		contentSB.append(
				"
"
						+ (value==maximum?"[style.boldDisabled(+)]":"[style.boldGood(+)]")
				+ "
"
				+ "
"
					+ ""+valueDisplay+""
				+ "
"
				+ "
"
					+ (value==minimum?"[style.boldDisabled(-)]":"[style.boldBad(-)]")
				+ "
");

		contentSB.append("
"
				+"
");

		return contentSB.toString();
	}

	public static final DialogueNode CREDITS = new DialogueNode("Credits", "", true) {

		@Override
		public String getContent(){
			UtilText.nodeContentSB.setLength(0);

			UtilText.nodeContentSB.append(
					"
"
						+ "Thank you for playing "+Main.GAME_NAME+", I hope you enjoy it just as much as I do making it!"
						+ " Thank you so much to all of you who offer financial support! Thanks to you, I'm able to spend more time working on "+Main.GAME_NAME+", and I promise that I'll make this game the very best that I can!"
					+ "
"
					+"
"
						+ Main.GAME_NAME+" has been created by:
"
						+ "Innoxia"
						+ "

"
						+ "Artists whose character art can be found in the game:
");

			for(Artist artist : Artwork.allArtists) {
				if (!artist.getName().equals("Custom")) {
					UtilText.nodeContentSB.append(""+artist.getName()+"
");
				}
			}

			UtilText.nodeContentSB.append("
"
					+ "Contributors:
" // In alphabetical order:
					+ "DJ Addi
"
					+ "DSG
"
					+ "Irbynx
"
					+ "Nnxx
"
					+ "Norin
"
					+ "Phlarx
"
					+ "Pimgd
"
					+ "Rfpnj
"
					+ "Tukaima
");

			UtilText.nodeContentSB.append("
"
						+ "Special thanks to:
"
						+ "Sensei,
"
						+ "loveless, Blue999, and DesuDemona
"
						+ "Github & wiki contributors
"
						+ "Everyone who's financially supported me,
"
						+ "Bug reporters,
"
						+ "and
"
						+ "Everyone for playing Lilith's Throne!"
					+ "
"
					+ "
"
					+ "
Legendary Patrons
"
					+ "
");

			for(CreditsSlot cs : Main.credits) {
				if(cs.getLegendaryCount()>0) {
					UtilText.nodeContentSB.append("
");
					UtilText.nodeContentSB.append("
");
					if(cs.getName().equals("Anonymous")) {
						UtilText.nodeContentSB.append("? ");
						UtilText.nodeContentSB.append("? ");
						UtilText.nodeContentSB.append("? ");
						UtilText.nodeContentSB.append("? ");
					} else {
						for(int i=0; i<cs.getUncommonCount()%5; i++) {
							UtilText.nodeContentSB.append("&#9679 ");
						}

						for(int i=0; i<cs.getRareCount()%5; i++) {
							UtilText.nodeContentSB.append("&#9679 ");
						}

						for(int i=0; i<cs.getEpicCount()/5; i++) {// 5-marks:
							UtilText.nodeContentSB.append("&#127775 ");
						}
						for(int i=0; i<cs.getEpicCount()%5; i++) {
							UtilText.nodeContentSB.append("&#9679 ");
						}

						for(int i=0; i<cs.getLegendaryCount()/5; i++) {// 5-marks:
							UtilText.nodeContentSB.append("&#127775 ");
						}
						for(int i=0; i<cs.getLegendaryCount()%5; i++) {
							UtilText.nodeContentSB.append("&#9679 ");
						}
					}
					UtilText.nodeContentSB.append("
");
					UtilText.nodeContentSB.append("
");
					UtilText.nodeContentSB.append(" "+(cs.getSubspeciesTier()!=null?""+cs.getName()+"":cs.getName()));
					UtilText.nodeContentSB.append("
");
				}
			}

			UtilText.nodeContentSB.append(
					"
"
					+ "
"
					+ "
Epic Patrons
"
					+ "
");

			for(CreditsSlot cs : Main.credits) {
				if(cs.getLegendaryCount()==0 && cs.getEpicCount()>0) {
					UtilText.nodeContentSB.append("
");
					UtilText.nodeContentSB.append("
");
					for(int i=0; i<cs.getUncommonCount()%5; i++) {
						UtilText.nodeContentSB.append("&#9679 ");
					}

					for(int i=0; i<cs.getRareCount()%5; i++) {
						UtilText.nodeContentSB.append("&#9679 ");
					}

					for(int i=0; i<cs.getEpicCount()/5; i++) {// 5-marks:
						UtilText.nodeContentSB.append("&#127775 ");
					}
					for(int i=0; i<cs.getEpicCount()%5; i++) {
						UtilText.nodeContentSB.append("&#9679 ");
					}
					UtilText.nodeContentSB.append("
");
					UtilText.nodeContentSB.append("
");
					UtilText.nodeContentSB.append(" "+(cs.getSubspeciesTier()!=null?""+cs.getName()+"":cs.getName()));
					UtilText.nodeContentSB.append("
");
				}
			}

			UtilText.nodeContentSB.append(
					"
"
					+ "
"
					+ "
Demonic backers
"
					+ "
");

			for(CreditsSlot cs : Main.credits) {
				if(cs.getLegendaryCount()==0 && cs.getEpicCount()==0) {
					UtilText.nodeContentSB.append("
");
					UtilText.nodeContentSB.append(" "+(cs.getSubspeciesTier()!=null?""+cs.getName()+"":cs.getName()));
				}
			}

			UtilText.nodeContentSB.append("
");

			return UtilText.nodeContentSB.toString();
		}

		@Override
		public Response getResponse(int responseTab, int index) {
			if (index == 0) {
				return new Response("Back", "Go back to the options menu.", MENU);

			} else {
				int i=1;
				for(Artist artist : Artwork.allArtists) {
					for(ArtistWebsite website : artist.getWebsites()) {
						if(index==i) {
							return new ResponseEffectsOnly(website.getName(), "Opens the page:

"+website.getURL()+"

Externally in your default browser."){
								@Override
								public void effects() {
									Util.openLinkInDefaultBrowser(website.getURL());
								}
							};
						}
						i++;
					}
				}
				return null;
			}
		}

		@Override
		public DialogueNodeType getDialogueNodeType() {
			return DialogueNodeType.OPTIONS;
		}
	};
}


code:
package com.lilithsthrone.game.dialogue.utils;

/**
 * @since 0.2.11
 * @version 0.2.11
 * @author Innoxia
 */
public enum ParseMode {
	UNKNOWN,
	CONDITIONAL,
	REGULAR,
	REGULAR_SCRIPT;
}


code:
package com.lilithsthrone.game.dialogue.utils;

import java.util.List;

import com.lilithsthrone.game.character.GameCharacter;
import com.lilithsthrone.game.character.body.types.BodyPartType;
import com.lilithsthrone.utils.Colour;

/**
 * @since 0.1.?
 * @version 0.3.3
 * @author Innoxia
 */
public abstract class ParserCommand {

	private boolean allowsCapitalisation;
	private boolean allowPronoun;
	private String description, arguments;
	private List<String> tags;
	private BodyPartType relatedBodyPart;

	public ParserCommand(List<String> tags, boolean allowsCapitalisation, boolean allowPronoun, String arguments, String description) {
		this(tags, allowsCapitalisation, allowPronoun, arguments, description, BodyPartType.GENERIC);
	}

	public ParserCommand(List<String> tags, boolean allowsCapitalisation, boolean allowPronoun, String arguments, String description, BodyPartType relatedBodyPart) {
		this.tags=tags;
		this.allowsCapitalisation=allowsCapitalisation;
		this.allowPronoun=allowPronoun;
		this.description=description;
		this.arguments=arguments;
		this.relatedBodyPart=relatedBodyPart;
	}

	public boolean isAllowsCapitalisation() {
		return allowsCapitalisation;
	}

	public boolean isAllowsPronoun() {
		return allowPronoun;
	}

	public String getDescription() {
		return description;
	}

	public String getArguments() {
		return arguments;
	}

	public List<String> getTags() {
		return tags;
	}

	public BodyPartType getRelatedBodyPart() {
		return relatedBodyPart;
	}

	public String getArgumentExample() {
		return "Example argument";
	}

	public String getExampleBeforeParse(String target, String arguments) {
		return "["+target+"."+tags.get(0)+""
				+(arguments==""?"":"("+arguments+")")+"]";
	}

	public abstract String parse(List<GameCharacter> specialNPCs, String command, String arguments, String target, GameCharacter character);

	/**
	 * Some methods might return a null or empty string for a descriptor. This method accounts for that, applying a descriptor if one is available and then returning the descriptor + name combination.
	 */
	protected static String applyDescriptor(String descriptor, String name) {
		if(descriptor==null) {
			return name;
		}

		return (descriptor.length() > 0 ? descriptor + (descriptor.charAt(descriptor.length()-1)=='-'?"":" ") : "") + name;
	}

	/**
	 * Some methods might return a null or empty string for a determiner. This method accounts for that, applying a special determiner if one is available and then returning the descriptor + name combination.
	 */
	protected String applyDeterminer(String descriptor, String input) {
		if(descriptor==null)
			return input;

		return (descriptor.length() > 0 ? descriptor + " " : (UtilText.isVowel(input.charAt(0))?"an ":"a ")) + input;
	}
}


code:
package com.lilithsthrone.game.dialogue.utils;

/**
 * @since 0.2.11
 * @version 0.3.4
 * @author Innoxia
 */
public enum ParserTag {

	SEX_ALLOW_MUFFLED_SPEECH,

	SEX_DESCRIPTION;
}