code:
package com.lilithsthrone.utils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import com.lilithsthrone.game.PropertyValue;
import com.lilithsthrone.game.character.effects.StatusEffect;
import com.lilithsthrone.main.Main;

import javafx.application.Platform;

/**
 * Collection of utility functions for date, time and number format conversion.
 *
 * @since 0.2.11
 * @version 0.3.1
 * @author Addi
 */
public enum Units {
    FORMATTER;

    public final int MIN_PRECISION = 0;
    public final int MAX_PRECISION = 2;

    public final List<String> imperialCountries = Arrays.asList("US", "LR", "MM");
    public final List<String> twelveHourCountries = Arrays.asList("US", "UK", "PH", "CA", "AU", "NZ", "IN", "EG", "SA", "CO", "PK", "MY", "SG", "ZA");

    DateTimeFormatter shortDate;
    DateTimeFormatter longDate;
    DateTimeFormatter time;
    NumberFormat number;

    Locale defaultLocale;

    Units() {
        defaultLocale = Locale.getDefault();

        if (Main.getProperties() == null) {
            new NullPointerException("Unit formatters initialized before properties. Assuming auto locale.").printStackTrace();
            updateFormats(true);
        } else {
            updateSettings();
            updateFormats(Main.getProperties().hasValue(PropertyValue.autoLocale));
        }
    }

    /**
     * Resets the 24-hour and imperial system settings to reflect automatically detected values.
     */
    public void updateSettings() {
        if (Main.getProperties().hasValue(PropertyValue.autoLocale)) {
            String countryCode = defaultLocale.getCountry().toUpperCase();
            boolean isMetric = !imperialCountries.contains(countryCode);
            Main.getProperties().setValue(PropertyValue.metricSizes, isMetric);
            Main.getProperties().setValue(PropertyValue.metricFluids, isMetric);
            Main.getProperties().setValue(PropertyValue.metricWeights, isMetric);
            Main.getProperties().setValue(PropertyValue.metricWeights, isMetric);
            Main.getProperties().setValue(PropertyValue.twentyFourHourTime, !twelveHourCountries.contains(countryCode));
            Main.getProperties().setValue(PropertyValue.internationalDate, isMetric);
        }
    }

    /**
     * Resets the date formatter depending on the system locale (if automatic) or the imperial number flag (if manual).
     * @param autoLocale Determines if automatic or manual detection is used
     */
    public void updateDateFormat(boolean autoLocale) {
        Locale.setDefault(autoLocale ? defaultLocale : Locale.ENGLISH);
        shortDate = (autoLocale ? DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
                : DateTimeFormatter.ofPattern(Main.getProperties().hasValue(PropertyValue.internationalDate) ? "MM/dd/yy" : "dd.MM.yy"))
                .withZone(ZoneId.systemDefault());
        longDate = DateTimeFormatter.ofPattern("d'%o %m' yyyy")
                .withZone(ZoneId.systemDefault());
    }

    /**
     * Resets the time formatter depending on the system locale (if automatic) or the 24 hour time flag (if manual).
     * @param autoLocale Determines if automatic or manual detection is used
     */
    public void updateTimeFormat(boolean autoLocale) {
        time = (autoLocale ? DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
                : DateTimeFormatter.ofPattern(Main.getProperties().hasValue(PropertyValue.twentyFourHourTime) ? "HH:mm" : "hh:mm a"))
                .withZone(ZoneId.systemDefault());
    }

    /**
     * Resets the number formatter depending on the system locale (if automatic), defaulting to English otherwise.
     * In all cases, output numbers are rounded correctly to the 2nd fraction digit.
     * @param autoLocale Determines if automatic or manual detection is used
     */
    public void updateNumberFormat(boolean autoLocale) {
        number = NumberFormat.getNumberInstance(autoLocale ? Locale.getDefault() : Locale.ENGLISH);
        number.setRoundingMode(RoundingMode.HALF_UP);
        number.setMinimumFractionDigits(MIN_PRECISION);
        number.setMaximumFractionDigits(MAX_PRECISION);
        Platform.runLater(StatusEffect::updateAttributeModifiers);
    }

    /**
     * Resets every formatter. See {@link Units#updateDateFormat(boolean)}, {@link Units#updateTimeFormat(boolean)} and
     * {@link Units#updateNumberFormat(boolean)} for details.
     * @param autoLocale Determines if automatic or manual detection is used
     */
    public void updateFormats(boolean autoLocale) {
        updateDateFormat(autoLocale);
        updateTimeFormat(autoLocale);
        updateNumberFormat(autoLocale);
    }

    /**
     * Formats a number with the current number formatter.
     * @param amount The floating point number to format
     * @return A string containing the localized number
     */
    public static String number(double amount) {
        return FORMATTER.number.format(amount);
    }

    /**
     * Convenience overload of {@link Units#number(double)} for integers.
     * @param amount The integer number to format
     */
    public static String number(long amount) {
        return FORMATTER.number.format(amount);
    }

    /**
     * Formats a number using the specified amount of minimum and maximum fractional places. Otherwise equal to
     * {@link Units#number(double)}.
     * @param minPrecision Minimum fractional digits
     * @param maxPrecision Maximum fractional digits
     */
    public static String number(double amount, int minPrecision, int maxPrecision) {
        // Apply new settings and generate output
        NumberFormat formatter = FORMATTER.number;
        formatter.setMinimumFractionDigits(minPrecision);
        formatter.setMaximumFractionDigits(maxPrecision);
        String output = formatter.format(amount);

        // Restore default settings
        formatter.setMinimumFractionDigits(FORMATTER.MIN_PRECISION);
        formatter.setMaximumFractionDigits(FORMATTER.MAX_PRECISION);

        return output;
    }

    /**
     * Specifies the length of dates, where SHORT will use the default numeric style and LONG will use the text style.
     */
    public enum DateType {
        SHORT,
        LONG
    }

    /**
     * Formats a short date and time. See {@link Units#dateTime(TemporalAccessor, DateType)} for details.
     */
    public static String dateTime(TemporalAccessor timePoint) {
        return dateTime(timePoint, DateType.SHORT);
    }

    /**
     * Formats a point in time with the current date and time formatter, separated with ", ".
     * @param timePoint The point in time to convert
     * @param type The style of the date, see {@link DateType} for details
     * @return A string containing the localized date and time
     */
    public static String dateTime(TemporalAccessor timePoint, DateType type) {
        return date(timePoint, type) + ", " + time(timePoint);
    }

    /**
     * Formats a short date. See {@link Units#date(TemporalAccessor, DateType)} for details.
     */
    public static String date(TemporalAccessor timePoint) {
        return date(timePoint, DateType.SHORT);
    }

    /**
     * Similar to {@link Units#dateTime(TemporalAccessor)}, except that this function only outputs the date.
     */
    public static String date(TemporalAccessor timePoint, DateType type) {
        if (type == DateType.SHORT)
            return FORMATTER.shortDate.format(timePoint);
        return FORMATTER.longDate.format(timePoint)
                .replaceFirst("%o", getOrdinal(timePoint.get(ChronoField.DAY_OF_MONTH)))
                .replaceFirst("%m", getMonthName(timePoint.get(ChronoField.MONTH_OF_YEAR)));
    }

    /**
     * Similar to {@link Units#dateTime(TemporalAccessor)}, except that this function only outputs the time.
     */
    public static String time(TemporalAccessor timePoint) {
        return FORMATTER.time.format(timePoint);
    }

    /**
     * Specifies the display of values, where NUMERIC is a rounded number, PRECISE is a number rounded to at most 2
     * places in the metric and eights in the imperial system and TEXT is a more aggressively rounded full text form.
     */
    public enum ValueType {
        NUMERIC,
        PRECISE,
        TEXT
    }

    /**
     * Specifies the length of units, where NONE means that they will be omitted, SHORT means the abbreviation, LONG
     * means the full text unit and LONG_SINGULAR means full text singular units concatenated with "-" instead of " ".
     */
    public enum UnitType {
        NONE,
        SHORT,
        LONG,
        LONG_SINGULAR
    }

    /**
     * Shortcut for {@link Units#size(double, ValueType, UnitType)} with numeric value and short unit.
     */
    public static String size(double cm) {
        return size(cm, ValueType.NUMERIC, UnitType.SHORT);
    }

    /**
     * Shortcut for {@link Units#size(double, ValueType, UnitType)} with numeric value.
     */
    public static String size(double cm, UnitType uType) {
        return size(cm, ValueType.NUMERIC, uType);
    }

    /**
     * Formats a size, given in centimetres, with the current number formatter and units depending on the imperial unit
     * setting as well as the given type.
     * @param cm Amount of centimetres to convert
     * @param vType The format of the value, see {@link ValueType}
     * @param uType The format of the units, see {@link UnitType}
     * @return A string containing the localized, wrapped, converted size and its associated unit
     */
    public static String size(double cm, ValueType vType, UnitType uType) {
        if (Main.getProperties().hasValue(PropertyValue.metricSizes))
            return sizeAsMetric(cm, vType, uType);
        else
            return sizeAsImperial(cm, vType, uType);
    }

    public final static String INCH_SYMBOL = """;
    public final static String FOOT_SYMBOL = "'";
    /**
     * Converts a size, given in centimetres, to the imperial form.
     * @param cm Amount of centimetres to format
     * @param vType The format of the value, see {@link ValueType}
     * @param uType The format of the units, see {@link UnitType}
     * @return A string containing the imperial, formatted size, including unit
     */
    @SuppressWarnings("incomplete-switch")
    public static String sizeAsImperial(double cm, ValueType vType, UnitType uType) {
        // Convert centimetres to inches
        double inches = cm / 2.54;

        // Wrap inches to feet
        double roundingFactor = (vType == ValueType.PRECISE || Math.abs(inches) < 1) ? 0.25 : 1;
        long feet = (long) (roundTo(inches, roundingFactor) / 12);
        double remainingInches = roundTo(inches, roundingFactor) % 12;

        StringBuilder output = new StringBuilder();

        boolean both = uType != UnitType.NONE && vType != ValueType.TEXT
                && feet != 0 && remainingInches != 0;
        boolean wrap = (vType == ValueType.TEXT && Math.abs(inches) >= 11.5)
                || both || (feet != 0 && remainingInches == 0);
        double usedValue = wrap ? feet : inches;

        // Append first unit, which may be either feet or inches
        output.append(value(usedValue, vType, true));
        switch (uType) {
            case NONE:
                break;
            case SHORT:
                output.append(wrap ? FOOT_SYMBOL : INCH_SYMBOL);
                break;
            case LONG:
                if (Math.floor(inches) == 0 && vType != ValueType.PRECISE) {
                    output.setLength(0);
                    return output.append("less than ")
                            .append(vType == ValueType.TEXT ? "one" : "1")
                            .append(" inch").toString();
                }

                output.append(" ");
                if (Math.abs(usedValue) >= 1 + roundingFactor / 2 || usedValue == 0.0) output.append(wrap ? "feet" : "inches");
                else output.append(wrap ? "foot" : "inch");
                break;
            case LONG_SINGULAR:
                output.append("-").append(wrap ? "foot" : "inch");
        }

        // Append second unit for long or short notation and if neither value is 0
        if (both) {
            if (uType == UnitType.LONG) output.append(" and ");
            if (uType == UnitType.LONG_SINGULAR) output.append("-");
            remainingInches = Math.abs(roundTo(remainingInches, roundingFactor));
            output.append(value(remainingInches, vType, true));
            switch (uType) {
                case SHORT:
                    output.append(INCH_SYMBOL);
                    break;
                case LONG:
                    output.append(" ").append(remainingInches >= 1 + roundingFactor / 2 ? "inches" : "inch");
                    break;
                case LONG_SINGULAR:
                    break;
            }
        }

        return output.toString();
    }

    /**
     * Formats a size, given in centimetres, to the metric form.
     * @param cm Amount of centimetres to convert
     * @param vType The format of the value, see {@link ValueType}
     * @param uType The format of the units, see {@link UnitType}
     * @return A string containing the metric, formatted size, including unit
     */
    public static String sizeAsMetric(double cm, ValueType vType, UnitType uType) {
        double m = cm / 100;
        return valueWithUnit(cm, "cm", "centimetre", m, "m", "metre", vType, uType, false);
    }

    /**
     * Shortcut for {@link Units#fluid(double, ValueType, UnitType)} with numeric value and short unit.
     */
    public static String fluid(double ml) {
        return fluid(ml, ValueType.NUMERIC, UnitType.SHORT);
    }

    /**
     * Shortcut for {@link Units#fluid(double, ValueType, UnitType)} with numeric value.
     */
    public static String fluid(double ml, UnitType uType) {
        return fluid(ml, ValueType.NUMERIC, uType);
    }

    /**
     * Shortcut for {@link Units#fluid(double, ValueType, UnitType)} with short units.
     */
    public static String fluid(double ml, ValueType vType) {
        return fluid(ml, vType, UnitType.SHORT);
    }

    /**
     * Formats a fluid volume, given in millilitres, with the current number formatter and units depending on the
     * imperial unit setting as well as the given type.
     * @param ml Amount of millilitres to convert
     * @param vType The format of the value, see {@link ValueType}
     * @param uType The format of the units, see {@link UnitType}
     * @return A string containing the localized, wrapped, converted volume and its associated unit
     */
    public static String fluid(double ml, ValueType vType, UnitType uType) {
        if (Main.getProperties().hasValue(PropertyValue.metricFluids))
            return fluidAsMetric(ml, vType, uType);
        else
            return fluidAsImperial(ml, vType, uType);
    }

    public static float mlToOz(float ml) {
    	return (float) (ml / 28.4131);
    }

    /**
     * Converts a fluid volume, given in millilitres, to the imperial form.
     * @param ml Amount of millilitres to convert
     * @param vType The format of the value, see {@link ValueType}
     * @param uType The format of the units, see {@link UnitType}
     * @return A string containing the imperial, formatted volume, including unit
     */
    public static String fluidAsImperial(double ml, ValueType vType, UnitType uType) {
        // Convert millilitres to ounces
        double oz = ml / 28.4131;

        double gal = oz / 160;
        return valueWithUnit(oz, "oz", "ounce", gal, "gal", "gallon", vType, uType, true);
    }

    /**
     * Formats a fluid volume, given in millilitres, to the metric form.
     * @param ml Amount of millilitres to format
     * @param vType The format of the value, see {@link ValueType}
     * @param uType The format of the units, see {@link UnitType}
     * @return A string containing the metric, formatted volume, including unit
     */
    public static String fluidAsMetric(double ml, ValueType vType, UnitType uType) {
        double l = ml / 1000;
        return valueWithUnit(ml, "mL", "millilitre", l, "L", "litre", vType, uType, false); // Innoxia's note: I usually prefer the lowercase l for ml and l, but LT's font makes it look bad.
    }

    /**
     * Shortcut for {@link Units#weight(double, ValueType, UnitType)} with numeric value and short unit.
     */
    public static String weight(double grams) {
        return weight(grams, ValueType.NUMERIC, UnitType.SHORT);
    }

    /**
     * Shortcut for {@link Units#weight(double, ValueType, UnitType)} with numeric value.
     */
    public static String weight(double grams, UnitType uType) {
        return weight(grams, ValueType.NUMERIC, uType);
    }

    /**
     * Formats a weight, given in grams, with the current number formatter and units depending on the imperial unit
     * setting as well as the given type.
     * @param grams Amount of grams to convert
     * @param vType The format of the value, see {@link ValueType}
     * @param uType The format of the units, see {@link UnitType}
     * @return A string containing the localized, wrapped, converted weight and its associated unit
     */
    public static String weight(double grams, ValueType vType, UnitType uType) {
        if (Main.getProperties().hasValue(PropertyValue.metricWeights))
            return weightAsMetric(grams, vType, uType);
        else
            return weightAsImperial(grams, vType, uType);
    }

    /**
     * Converts a weight, given in grams, to the imperial form.
     * @param grams Amount of grams to convert
     * @param vType The format of the value, see {@link ValueType}
     * @param uType The format of the units, see {@link UnitType}
     * @return A string containing the imperial, formatted weight, including unit
     */
    public static String weightAsImperial(double grams, ValueType vType, UnitType uType) {
        // Convert grams to ounces
        double oz = grams / 28.34952;

        double lb = oz / 16;
        return valueWithUnit(oz, "oz", "ounce", lb, "lb", "pound", vType, uType, true);
    }

    /**
     * Formats a weight, given in grams, to the metric form.
     * @param grams Amount of grams to format
     * @param vType The format of the value, see {@link ValueType}
     * @param uType The format of the units, see {@link UnitType}
     * @return A string containing the metric, formatted weight, including unit
     */
    public static String weightAsMetric(double grams, ValueType vType, UnitType uType) {
        double kg = grams / 1000;
        return valueWithUnit(grams, "g", "gram", kg, "kg", "kilogram", vType, uType, false);
    }

    private static String value(double value, ValueType vType, boolean useQuarters) {
        switch (vType) {
            case PRECISE:
                if (useQuarters) return withQuarters(value);
                return number(value);
            case TEXT:
                return Util.intToString((int) aggressiveRound(value));
            default:
                if (useQuarters) return Math.abs(value) < 0.875 ? withQuarters(value) : number(Math.round(value));
                return number(adaptiveRound(value));
        }
    }

    private static String valueWithUnit(double value, String shortUnit, String unit,
                                        double wrappedValue, String shortWrappedUnit, String wrappedUnit,
                                        ValueType vType, UnitType uType, boolean useQuarters) {
        StringBuilder output = new StringBuilder();
        boolean wrap = Math.abs(wrappedValue) >= 1 && vType != ValueType.PRECISE;
        double usedValue = wrap ? wrappedValue : value;
        if (useQuarters) usedValue = roundTo(usedValue, 0.25);

        if(value>0 && usedValue==0) {
        	output.append("<");
        	usedValue = 0.25;
        }

        // Append value with increased precision if it is wrapped and numeric
        output.append(value(usedValue, wrap && vType == ValueType.NUMERIC ? ValueType.PRECISE : vType, useQuarters));

        // Append unit
        switch (uType) {
            case NONE:
                break;
            case SHORT:
                output.append(" ").append(wrap ? shortWrappedUnit : shortUnit);
                break;
            case LONG:
                if (Math.floor(value) == 0 && vType != ValueType.PRECISE) {
                    output.setLength(0);
                    return output.append("less than ")
                            .append(vType == ValueType.TEXT ? "one " : "1 ")
                            .append(unit).toString();
                }

                output.append(" ").append(wrap ? wrappedUnit : unit);
                if (Math.abs(usedValue) > 1 || usedValue == 0.0) output.append("s");
                break;
            case LONG_SINGULAR:
                output.append("-").append((wrap ? wrappedUnit : unit));
        }

        return output.toString();
    }

    /**
     * Formats the given value with eighth fraction symbols instead of fractional digits.
     * @param value The number to format
     * @return A string containing the formatted number with an optional eighth symbol
     */
    public static String withQuarters(double value) {
        boolean negative = value < 0;
        value = roundTo(Math.abs(value), 0.25);
        double floor = Math.floor(value);

        if (value == floor) return (negative ? '-' : "") + number(floor);

        int quarters = (int) Math.round((value - floor) / 0.25);
        return (negative ? '-' : "") + (floor == 0 ? "" : number(floor)) + getQuarterSymbol(quarters);
    }

    private static String getQuarterSymbol(int quarters) {
        switch (quarters) {
            case 1: return "¼";
            case 2: return "½";
            case 3: return "¾";
            default: return "";
        }
    }

    private final static String[] suffixes = new String[] { "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th" };
    /**
     * Selects the correct ordinal for a given value. Only the ordinal itself is returned.
     * @param value The number to select an ordinal for
     * @return A string containing the ordinal
     */
    public static String getOrdinal(int value) {
        switch (value % 100) {
            case 11:
            case 12:
            case 13:
                return "th";
            default:
                return suffixes[value % 10];
        }
    }

    public static String getMonthName(int month) {
        switch (month) {
            case 1: return "January";
            case 2: return "February";
            case 3: return "March";
            case 4: return "April";
            case 5: return "May";
            case 6: return "June";
            case 7: return "July";
            case 8: return "August";
            case 9: return "September";
            case 10: return "October";
            case 11: return "November";
            case 12: return "December";
        }
        return "";
    }

    /**
     * Rounds a given number to a given amount of fractional places.
     * @param value Number to round
     * @param places Amount of fractional places
     * @return A correctly rounded float
     */
    public static float round(double value, int places) {
        if (places < 0) throw new IllegalArgumentException("Amount of fractional places cannot be less than 0.");
        return BigDecimal.valueOf(value).setScale(places, RoundingMode.HALF_UP).floatValue();
    }

    /**
     * Rounds a given number to the nearest multiple of the second parameter. Note that {@link Units#round(double, int)} is
     * more precise and should be preferred.
     * @param value Number to round
     * @param nearest Number to round to
     * @return A rounded float
     */
    public static float roundTo(double value, double nearest) {
        return (float) (Math.round(value / nearest) * nearest);
    }

    /**
     * Convenience overload of {@link Units#roundTo(double, double)} for integers.
     * @param nearest Integer number to round to
     * @return A rounded integer
     */
    public static long roundTo(double value, long nearest) {
        return Math.round(value / nearest) * nearest;
    }

    /**
     * Rounds a given number based on its value. Values below 2 are rounded to 1 fractional digit, values below 10 are
     * rounded to the nearest 0.5, otherwise the number is rounded to the nearest integer.
     * @param value Number to round
     * @return A variably rounded double
     */
    public static double adaptiveRound(double value) {
        double absoluteValue = Math.abs(value);
        if (absoluteValue <= 2) return round(value, 1);
        if (absoluteValue <= 10) return roundTo(value, 0.5);
        return Math.round(value);
    }

    /**
     * Rounds a given number based on its value, possibly exceeding the original value by more than 1. Values below 10
     * are rounded to the nearest integer, values below 50 are rounded to the nearest 5, otherwise the number is rounded
     * to the nearest 10.
     * @param value Number to round
     * @return A variably rounded long
     */
    public static long aggressiveRound(double value) {
        double absoluteValue = Math.abs(value);
        if (absoluteValue <= 10) return Math.round(value);
        if (absoluteValue <= 50) return roundTo(value, 5);
        return roundTo(value, 10);
    }
}


code:
package com.lilithsthrone.utils;

import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.regex.Pattern;

import com.lilithsthrone.game.character.GameCharacter;
import com.lilithsthrone.game.character.body.CoverableArea;
import com.lilithsthrone.game.character.race.Subspecies;
import com.lilithsthrone.game.inventory.InventorySlot;
import com.lilithsthrone.game.inventory.clothing.AbstractClothing;
import com.lilithsthrone.game.inventory.clothing.DisplacementType;
import com.lilithsthrone.main.Main;

import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;

/**
 * This is just a big mess of utility classes that I wanted to throw somewhere.
 *
 * @since 0.1.0
 * @version 0.3.1
 * @author Innoxia
 */
public class Util {

	public static Random random = new Random();

	private static StringBuilder utilitiesStringBuilder = new StringBuilder();

	private static Map<KeyCode, String> KEY_NAMES = new LinkedHashMap<KeyCode, String>() {
		private static final long serialVersionUID = 1L;
	{
		put(KeyCode.ADD, "+");
		put(KeyCode.ALT, "Alt");
		put(KeyCode.AMPERSAND, "&");
		put(KeyCode.ASTERISK, "*");
		put(KeyCode.BACK_QUOTE, "\"");
		put(KeyCode.BACK_SLASH, "\\");
		put(KeyCode.BACK_SPACE, "Back space");
		put(KeyCode.BRACELEFT, "{");
		put(KeyCode.BRACERIGHT, "}");
		put(KeyCode.CAPS, "Caps");
		put(KeyCode.CLOSE_BRACKET, "]");
		put(KeyCode.COLON, ":");
		put(KeyCode.COMMA, ",");
		put(KeyCode.CONTROL, "Ctrl");
		put(KeyCode.DELETE, "Delete");
		put(KeyCode.DIVIDE, "/");
		put(KeyCode.DOLLAR, "$");
		put(KeyCode.DOWN, "Down");
		put(KeyCode.END, "End");
		put(KeyCode.ENTER, "Enter");
		put(KeyCode.EQUALS, "=");
		put(KeyCode.ESCAPE, "Esc");
		put(KeyCode.EURO_SIGN, "&euro"); // €
		put(KeyCode.EXCLAMATION_MARK, "!");
		put(KeyCode.GREATER, ">");
		put(KeyCode.KP_DOWN, "Down");
		put(KeyCode.KP_LEFT, "Left");
		put(KeyCode.KP_RIGHT, "Right");
		put(KeyCode.KP_UP, "Up");
		put(KeyCode.LEFT, "Left");
		put(KeyCode.LEFT_PARENTHESIS, "(");
		put(KeyCode.LESS, "<");
		put(KeyCode.MINUS, "-");
		put(KeyCode.NUMPAD0, "0");
		put(KeyCode.NUMPAD1, "1");
		put(KeyCode.NUMPAD2, "2");
		put(KeyCode.NUMPAD3, "3");
		put(KeyCode.NUMPAD4, "4");
		put(KeyCode.NUMPAD5, "5");
		put(KeyCode.NUMPAD6, "6");
		put(KeyCode.NUMPAD7, "7");
		put(KeyCode.NUMPAD8, "9");
		put(KeyCode.NUMPAD9, "9");
		put(KeyCode.OPEN_BRACKET, "[");
		put(KeyCode.PAGE_DOWN, "Pg Dn");
		put(KeyCode.PAGE_UP, "Pg Up");
		put(KeyCode.PERIOD, ".");
		put(KeyCode.PLUS, "+");
		put(KeyCode.POUND, "&pound"); // £
		put(KeyCode.POWER, "^");
		put(KeyCode.QUOTE, "\"");
		put(KeyCode.RIGHT, "Right");
		put(KeyCode.RIGHT_PARENTHESIS, ")");
		put(KeyCode.SEMICOLON, ";");
		put(KeyCode.SHIFT, "Sft");
		put(KeyCode.SLASH, "/");
		put(KeyCode.SPACE, "Space");
		put(KeyCode.SUBTRACT, "-");
		put(KeyCode.TAB, "Tab");
	}};

	// What madness is this
	public static String inputStreamToString(InputStream is) {
		if (is == null)
			return "";
		try (java.util.Scanner s = new java.util.Scanner(is)) {
			return s.useDelimiter("\\A").hasNext() ? s.next() : "";
		}
	}

	public static Color midpointColor(Color first, Color second) {

		double r = (first.getRed() + second.getRed())/2;
		double g = (first.getGreen() + second.getGreen())/2;
		double b = (first.getBlue() + second.getBlue())/2;
//		System.out.println(r+","+g+","+b);
		return Color.color(r, g, b);
	}

	public static String toWebHexString(Color colour) {
		String c = colour.toString().substring(2, 8);
//		System.out.println(c);
		return "#"+c;
	}

	public static Color newColour(double r, double g, double b) {
		return Color.color(r / 255, g / 255, b / 255);
	}

	public static Color newColour(int hex) {
		return newColour((hex & 0xFF0000) >> 16, (hex & 0xFF00) >> 8, (hex & 0xFF));
	}

	/**
	 * Takes an input, and a maximum value, and returns LT's universal "dropoff" formula to it.
	 * @param input
	 * @param maxValue
	 * @return
	 */
	public static float getModifiedDropoffValue(float input, float maxValue) {
		if(Math.abs(input)>Math.abs(maxValue)) {
			input = Math.signum(input) * maxValue;
		}
//		float value = Math.abs(input)/Math.abs(maxValue);
//		//y = 0.75 * cos((x*(pi/2))-(pi/2))
//		return ((int)((Math.signum(input) * maxValue * 0.75f * Math.cos((value * (Math.PI/2)) - (Math.PI/2)))*100))/100f;

//		sin(x*pi/2)/2
		if(input < maxValue/2) {
			return input;

		} else {
			float excess = Math.abs(input) - Math.abs(maxValue/2);
			float value = (excess/Math.abs(maxValue))*2;
			float multiplier = (float) (Math.sin(value * (Math.PI/2))/2f);
//			System.out.println(input+", "+value +", "+ multiplier);
			return Math.round((maxValue/2 + (multiplier * (maxValue/2)))*100)/100f;
		}
	}

	public static class Value<T, S> {
		private T key;
		private S value;

		public Value(T key, S value) {
			this.key = key;
			this.value = value;
		}

		public T getKey() {
			return key;
		}

		public S getValue() {
			return value;
		}
	}

	@SafeVarargs
	public static <T, S> LinkedHashMap<T, S> newHashMapOfValues(Value<T, S>... values) {
		LinkedHashMap<T, S> map = new LinkedHashMap<>();

		for (Value<T, S> v : values) {
			if(v!=null) {
				map.put(v.getKey(), v.getValue());
			}
		}

		return map;
	}

	public String keyCodeToShortString(KeyCode keyCode) {
		switch (keyCode) {
		case OPEN_BRACKET:
			return "[";
		case CLOSE_BRACKET:
			return "]";
		case UP:
			return "Up";
		case DOWN:
			return "Down";
		case LEFT:
			return "Left";
		case RIGHT:
			return "Right";
		default:
			return keyCode.toString();
		}
	}

	public static void openLinkInDefaultBrowser(String url) {
		Runtime runtime = Runtime.getRuntime();
		try {
			runtime.exec("xdg-open " + url);
		} catch (IOException e0) {
			Desktop desktop = Desktop.getDesktop();
			try {
				desktop.browse(new URI(url));
			} catch (IOException | URISyntaxException e) {
				e.printStackTrace();
				e0.printStackTrace();
			}
		}
	}

	public static String getFileTime(File file) {
		try {
			Instant fileTime = Files.getLastModifiedTime(file.toPath()).toInstant();
			return Units.dateTime(fileTime);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return "Unknown";
	}

	/**
	 * @param values The values to add to the new list.
	 * @return A list of provided values, with nulls stripped.
	 */
	@SafeVarargs
	public static  ArrayList newArrayListOfValues(U... values) {
		ArrayList list = new ArrayList<>(Arrays.asList(values));
		list.removeIf(e -> e==null);
		return list;
	}

	@SafeVarargs
	/**
	 * @param lists The lists to merge.
	 * @return A new ArrayList which contains all the elements from both lists.
	 */
	public static  ArrayList mergeLists(List... lists) {
		ArrayList mergedList = new ArrayList<>();

		for(List list : lists) {
			if(list!=null) {
				for(U value : list) {
					mergedList.add(value);
				}
			}
		}

		return mergedList;
	}

	@SafeVarargs
	public static  HashSet newHashSetOfValues(U... values) {
		return new HashSet<>(Arrays.asList(values));
	}

	@SafeVarargs
	/**
	 * @param maps The maps to draw entries from.
	 * @return A new map containing all of the entries from the provided 'maps'. Nulls are stripped, and 'maps' are unaltered.
	 */
	public static  Map<U, List<T>> mergeMaps(Map<U, List<T>>... maps) {
		Map<U, List<T>> mergedMap = new HashMap<>();

		for(Map<U, List<T>> map : maps) {
			if(map!=null) {
				for(Entry<U, List<T>> entry : map.entrySet()) {
					mergedMap.putIfAbsent(entry.getKey(), new ArrayList<>());
					mergedMap.get(entry.getKey()).addAll(entry.getValue());
				}
			}
		}

		return mergedMap;
	}

	public static <T> T getRandomObjectFromWeightedMap(Map<T, Integer> map) {
		return getRandomObjectFromWeightedMap(map, Util.random);
	}

	public static <T> T getRandomObjectFromWeightedMap(Map<T, Integer> map, Random rnd) {
		int total = 0;
		for(int i : map.values()) {
			total+=i;
		}

		if(total==0) {
			return null;
		}

		int choice = rnd.nextInt(total) + 1;

		total = 0;
		for(Entry<T, Integer> entry : map.entrySet()) {
			total+=entry.getValue();
			if(choice<=total) {
				return entry.getKey();
			}
		}

		return null;
	}

	/*public static <T> T getFirstObjectFromWeightedMap(Map<T, Integer> map) { //returning last entry added?
		int total = 0;
		for(int i : map.values()) {
			total+=i;
		}

		if(total==0) {
			return null;
		}

		int choice = 1;

		total = 0;
		for(Entry<T, Integer> entry : map.entrySet()) {
			total+=entry.getValue();
			if(choice<=total) {
				return entry.getKey();
			}
		}

		return null;
	}*/

	public static <T> T getRandomObjectFromWeightedFloatMap(Map<T, Float> map) {
		int total = 0;
		for(float f : map.values()) {
			total+=f;
		}

		float choice = (float) (Math.random()*total);

		total = 0;
		for(Entry<T, Float> entry : map.entrySet()) {
			total+=entry.getValue();
			if(choice<=total) {
				return entry.getKey();
			}
		}

		return null;
	}

	private static String[] numbersLessThanTwenty = {
			"zero",
			"one",
			"two",
			"three",
			"four",
			"five",
			"six",
			"seven",
			"eight",
			"nine",
			"ten",
			"eleven",
			"twelve",
			"thirteen",
			"fourteen",
			"fifteen",
			"sixteen",
			"seventeen",
			"eighteen",
			"nineteen"
	};
	private static String[] positionsLessThanTwenty = {
			"zero",
			"first",
			"second",
			"third",
			"fourth",
			"fifth",
			"sixth",
			"seventh",
			"eighth",
			"ninth",
			"tenth",
			"eleventh",
			"twelfth",
			"thirteenth",
			"fourteenth",
			"fifteenth",
			"sixteenth",
			"seventeenth",
			"eighteenth",
			"nineteenth"
	};
	private static String[] tensGreaterThanNineteen = {
			"",
			"",
			"twenty",
			"thirty",
			"forty",
			"fifty",
			"sixty",
			"seventy",
			"eighty",
			"ninety"
	};

	/**
	 * Only works for values -99,999 to 99,999.
	 * @param integer
	 * @return
	 */
	public static String intToString(int integer) {
		String intToString = "";

		if(integer<0) {
			intToString = "minus ";
		}
		integer = Math.abs(integer);
		if (integer >= 100_000) {
			return String.valueOf(integer); // Too big
		}

		if(integer>=1000) {
			if((integer/1000)<20) {
				intToString+=numbersLessThanTwenty[(integer/1000)]+" thousand";
			} else {
				intToString+=tensGreaterThanNineteen[integer/10000] + (((integer/1000)%10!=0)?"-"+numbersLessThanTwenty[(integer/1000)%10]:"")+" thousand";
			}
		}

		if(integer>=100) {
			if(integer>=1000 && integer%1000 != 0) {
				intToString+=", ";
			}
			integer = integer % 1000;
			if (intToString.isEmpty() || integer>=100) {
				intToString += numbersLessThanTwenty[integer/100]+" hundred";
			}
			if(integer%100!=0) {
				intToString+=" and ";
				integer = integer % 100;
			}
		}

		if(integer%100<20) {
			if (integer%100 == 0) {
				if (intToString.isEmpty()) {
					return "zero";
				}
			} else {
				intToString+=numbersLessThanTwenty[integer%100];
			}
		} else {
			intToString+=tensGreaterThanNineteen[(integer%100)/10] + ((integer%10!=0)?"-"+numbersLessThanTwenty[integer%10]:"");
		}

		return intToString;
	}

	public static String intToDate(int integer) {
		int remainderHundred = integer%100;
		if(remainderHundred<=10 || remainderHundred>20) {
			if(integer%10==1) {
				return integer+"st";
			} else if(integer%10==2) {
				return integer+"nd";
			} else if(integer%10==3) {
				return integer+"rd";
			}
		}
		return integer+"th";
	}

	/**
	 * @param integer Input number to convert.
	 * @return 'once', 'twice', or 'integer times'
	 */
	public static String intToCount(int integer) {
		if(integer==1) {
			return "once";
		} else if(integer==2) {
			return "twice";
		}

		return intToString(integer)+" times";
	}

	public static String intToPosition(int integer) {
		String intToString = "";

		if(integer<0) {
			intToString = "minus ";
		}
		integer = Math.abs(integer);
		if (integer >= 100_000) {
			return String.valueOf(integer); // Too big
		}

		if(integer>=1000) {
			if((integer/1000)<20) {
				intToString+=numbersLessThanTwenty[(integer/1000)]+" thousand";
			} else {
				intToString+=tensGreaterThanNineteen[integer/10000] + (((integer/1000)%10!=0)?"-"+numbersLessThanTwenty[(integer/1000)%10]:"")+" thousand";
			}
		}

		if(integer>=100) {
			if(integer>=1000 && integer%1000 != 0) {
				intToString+=", ";
			}
			integer = integer % 1000;
			if (intToString.isEmpty() || integer>=100) {
				intToString += numbersLessThanTwenty[integer/100]+" hundred";
			}
			if(integer%100!=0) {
				intToString+=" and ";
				integer = integer % 100;
			}
		}

		if(integer%100<20) {
			if (integer%100 == 0) {
				if (intToString.isEmpty()) {
					return "zero";
				}
			} else {
				intToString+=positionsLessThanTwenty[integer%100];
			}
		} else {
			intToString+=tensGreaterThanNineteen[(integer%100)/10] + ((integer%10!=0)?"-"+positionsLessThanTwenty[integer%10]:"");
		}

		return intToString;
	}

	private final static TreeMap<Integer, String> numeralMap = new TreeMap<Integer, String>();
	static {
        numeralMap.put(1000, "M");
        numeralMap.put(900, "CM");
        numeralMap.put(500, "D");
        numeralMap.put(400, "CD");
        numeralMap.put(100, "C");
        numeralMap.put(90, "XC");
        numeralMap.put(50, "L");
        numeralMap.put(40, "XL");
        numeralMap.put(10, "X");
        numeralMap.put(9, "IX");
        numeralMap.put(5, "V");
        numeralMap.put(4, "IV");
        numeralMap.put(1, "I");
    }

	public static String intToNumerals(int integer) {
		if(integer<=0) {
			return "0";
		}
		int l =  numeralMap.floorKey(integer);
        if (integer == l) {
            return numeralMap.get(integer);
        }
        return numeralMap.get(l) + intToNumerals(integer-l);
	}

	public static String intToTally(int integer, int max) {
		StringBuilder numeralSB = new StringBuilder();
		int limit = Math.min(integer, max);
		for(int i=0; i<limit/5; i++) {
			numeralSB.append("IIII ");
		}
		for(int i=0; i<limit%5; i++) {
			numeralSB.append("I");
		}
		if(limit<integer) {
			numeralSB.append("... (Total: "+integer+")");
		}

		return numeralSB.toString();
	}

	public static String getKeyCodeCharacter(KeyCode code) {
		String name = KEY_NAMES.get(code);
		return name != null? name : code.getName();
	}

	public static String capitaliseSentence(String sentence) {
		if(sentence==null || sentence.isEmpty()) {
			return sentence;
		}
		return Character.toUpperCase(sentence.charAt(0)) + sentence.substring(1);
	}

	public static boolean isVowel(char c) {
		return "AEIOUaeiou".indexOf(c) != -1;
	}

	/**
	 * Turns a normal sentence into a stuttering sentence. Example:
	 * "How far is it to the town hall?" "H-How far is it to the town h-hall?"
	 * "How far i-is it to the t-town hall?"
	 *
	 * @param sentence
	 *            sentence to apply stutters
	 * @param frequency
	 *            of stutter words (i.e. 4 would be 1 in 4 words are stutters)
	 * @return
	 *            modified sentence
	 */
	public static String addStutter(String sentence, int frequency) {
		StringBuilder modifiedSentence = new StringBuilder();
		int openingCurly = 0;
		int closingCurly = 0;
		int openingSquare = 0;
		int closingSquare = 0;
		float chance = 1f/frequency;
		for(int i = sentence.length()-1; i>=0; i--) {
			if(sentence.charAt(i)=='(') {
				openingCurly++;
			} else if(sentence.charAt(i)==')') {
				closingCurly++;
			} else if(sentence.charAt(i)=='[') {
				openingSquare++;
			} else if(sentence.charAt(i)==']') {
				closingSquare++;
			}

			if(sentence.charAt(i)==' '
					&& Character.isLetter(sentence.charAt(i+1))
//					&& sentence.charAt(i+1)!='#'
//					&& sentence.charAt(i+1)!='('
//					&& sentence.charAt(i+1)!='['
					&& openingCurly==closingCurly
					&& openingSquare==closingSquare) {
				if(Math.random()<chance) {
					modifiedSentence.append("-");
					modifiedSentence.append(sentence.charAt(i+1));
				}
			}
			modifiedSentence.append(sentence.charAt(i));
		}

		modifiedSentence.reverse();
		return modifiedSentence.toString();
	}

	private static Pattern endOfSentence = Pattern.compile("[,.!?]");

	private static boolean isEndOfSentence(char c) {
		return endOfSentence.matcher(String.valueOf(c)).matches();
	}

	/**
	 * Inserts words randomly into a sentence.

	 *
	 * @param sentence
	 *            sentence to insert words into
	 * @param frequency
	 *            how often words are inserted. 1/frequency is the probability of inserting a word
	 * @param inserts
	 *            list of strings to insert into. These are appended to the end of words, so ensure
	 *            any whitespace wanted is put before the insert. A space separates the next word
	 * @param middle
	 *            boolean, whether to avoid inserting at the start/end of a sentence
	 * @return
	 *            modified sentence
	 */
	private static String insertIntoSentences(String sentence, int frequency, String[] inserts, boolean middle) {

		StringBuilder modifiedSentence = new StringBuilder();
		int openingCurly = 0;
		int closingCurly = 0;
		int openingSquare = 0;
		int closingSquare = 0;
		float chance = 1f/frequency;
		for(int i = sentence.length()-1; i>=0; i--) {
			if(sentence.charAt(i)=='(') {
				openingCurly++;
			} else if(sentence.charAt(i)==')') {
				closingCurly++;
			} else if(sentence.charAt(i)=='[') {
				openingSquare++;
			} else if(sentence.charAt(i)==']') {
				closingSquare++;
			}
			if(i!=sentence.length()-1
					&& sentence.charAt(i+1)==' '
					&& !isEndOfSentence(sentence.charAt(i))
					&& (i==0 || !middle || !isEndOfSentence(sentence.charAt(i-1)))
					&& openingCurly==closingCurly
					&& openingSquare==closingSquare) {
				if(Math.random()<chance) {
					String word = Util.randomItemFrom(inserts);
					char[] charArray = word.toCharArray();
					for(int cIndex=charArray.length-1; cIndex>=0; cIndex--) {
						modifiedSentence.append(charArray[cIndex]);
					}
				}
			}
			modifiedSentence.append(sentence.charAt(i));
		}

		modifiedSentence.reverse();
		return modifiedSentence.toString();
	}

	private static String insertIntoSentences(String sentence, int frequency, String[] inserts) {
		return insertIntoSentences(sentence, frequency, inserts, true);
	}

	private static String[] bimboWords = new String[] { ", like,", ", like,", ", like,", ", um,", ", uh,", ", ah," };
	/**
	 * Turns a normal sentence into the kind of thing a Bimbo would come out with.
	 * Can be safely used in conjunction with addStutter.
	 * If using addStutter after using addBimbo, bimbo words can also become stuttered.

	 * Example: "How far is it to the town hall?"

	 * "How, like, far is it to the town, uh, hall and stuff?"

	 * "How far is, like, it to the, um, town hall and stuff?"

	 * "Like, How far is it to the, like, town hall?"

	 * Used in conjunction with addStutter(): "L-Like, How far is it t-to the, like, town hall?"
	 *
	 * @param sentence
	 *            sentence to apply bimbo modifications
	 * @param frequency
	 *            of bimbo interjections (i.e. 4 would be 1 in 4 words have a
	 *            bimbo interjection)
	 * @return
	 *            modified sentence
	 */
	public static String addBimbo(String sentence, int frequency) {
		sentence = insertIntoSentences(sentence, frequency, bimboWords);
		utilitiesStringBuilder.setLength(0);
		utilitiesStringBuilder.append(sentence);

		// 1/3 chance of having a bimbo sentence ending: TODO improve so it can be added anywhere
		if(!sentence.endsWith("~") && !sentence.endsWith("-") && !sentence.endsWith("#ENDIF")) {
			switch (random.nextInt(6)) {
				case 0:
					char end = utilitiesStringBuilder.charAt(utilitiesStringBuilder.length() - 1);
					utilitiesStringBuilder.deleteCharAt(utilitiesStringBuilder.length() - 1);
					utilitiesStringBuilder.append(" and stuff");
					utilitiesStringBuilder.append(end);
					break;
				case 1:
					utilitiesStringBuilder.deleteCharAt(utilitiesStringBuilder.length() - 1);
					utilitiesStringBuilder.append(", y'know?");
					break;
				default:
					break;
			}
		}

		return utilitiesStringBuilder.toString();
	}

	private static String[] broWords = new String[] { ", like,", ", like, dude,", ", like,", ", um,", ", uh,", ", ah," };
	public static String addBro(String sentence, int frequency) {
		sentence = insertIntoSentences(sentence, frequency, broWords);
		utilitiesStringBuilder.setLength(0);
		utilitiesStringBuilder.append(sentence);

		// 1/3 chance of having a bimbo sentence ending: TODO improve so it can be added anywhere
		if(!sentence.endsWith("~") && !sentence.endsWith("-") && !sentence.endsWith("#ENDIF")) {
			switch (random.nextInt(6)) {
				case 0:
					char end = utilitiesStringBuilder.charAt(utilitiesStringBuilder.length() - 1);
					utilitiesStringBuilder.deleteCharAt(utilitiesStringBuilder.length() - 1);
					utilitiesStringBuilder.append(" and stuff");
					utilitiesStringBuilder.append(end);
					break;
				case 1:
					utilitiesStringBuilder.deleteCharAt(utilitiesStringBuilder.length() - 1);
					utilitiesStringBuilder.append(", y'know, dude?");
					break;
				default:
					break;
			}
		}

		return utilitiesStringBuilder.toString();
	}

	private static String[] muffledSounds = new String[] { " ~Mrph~", " ~Mmm~", " ~Mrmm~" };
	/**
	 * Turns a normal sentence into a muffled sentence.

	 * Example:

	 * "How far is it to the town hall?"

	 * "How ~Mrph~ far is it ~Mmm~ to the town ~Mrph~ hall?"

	 *
	 * @param sentence
	 *            sentence to apply muffles
	 * @param frequency
	 *            of muffled words (i.e. 4 would be 1 in 4 words are muffled)
	 * @return
	 *            modified sentence
	 */
	public static String addMuffle(String sentence, int frequency) {
		return insertIntoSentences(sentence, frequency, muffledSounds);
	}

	public static String replaceWithMuffle(String sentence, int wordToMuffleRatio) {
		int muffles = sentence.split(" ").length/wordToMuffleRatio;
		StringBuilder muffleSB = new StringBuilder();
		for(int i=0; i<muffles; i++) {
			muffleSB.append(muffledSounds[random.nextInt(muffledSounds.length)]);
		}
		muffleSB.delete(0, 1); // Remove space at start
		return muffleSB.toString();
	}

	private static String[] sexSounds = new String[] { " ~Aah!~", " ~Mmm!~", "~Ooh!~" };
	/**
	 * Turns a normal sentence into a sexy sentence.

	 * Example:

	 * "How far is it to the town hall?"

	 * "How ~Aah!~ far is it ~Mmm!~ to the town ~Aah!~ hall?"

	 *
	 * @param sentence
	 *            sentence to apply sexy modifications
	 * @param frequency
	 *            of sex sounds (i.e. 4 would be 1 in 4 words are sexy)
	 * @return
	 *            modified sentence
	 */
	public static String addSexSounds(String sentence, int frequency) {
		return insertIntoSentences(sentence, frequency, sexSounds);
	}

	private static String[] drunkSounds = new String[] { " ~Hic!~" };
	/**
	 * Turns a normal sentence into a drunk one.

	 * Example:

	 * "How far is it to the town hall?"

	 * "How ~Hic!~ far ish it ~Hic!~ to the town ~Hic!~ hall?"

	 *
	 * @param sentence to apply drunk modifications to.
	 * @param frequency of drunk sounds (i.e. 4 would be 1 in 4 words are drunk)
	 * @return modified sentence
	 */
	public static String addDrunkSlur(String sentence, int frequency) {
		sentence = insertIntoSentences(sentence, frequency, drunkSounds, false);

		String [] split = sentence.split("\\[(.*?)\\]");
		for(String s : split) {
			String [] splitConditional = s.split("#IF\\((.*?)\\)|#ELSEIF\\((.*?)\\)"); // Do not replace text inside conditional parsing statements
			for(String s2 : splitConditional) {
				String sReplace = s2
						.replaceAll("Hi ", "Heeey ")
						.replaceAll("yes", "yesh")
						.replaceAll("Is", "Ish")
						.replaceAll("is", "ish")
						.replaceAll("It's", "It'sh")
						.replaceAll("it's", "it'sh")
						.replaceAll("So", "Sho")
						.replaceAll("so", "sho");

					sentence = sentence.replace(s2, sReplace);
			}
		}

		return sentence;

//		return insertIntoSentences(sentence, frequency, drunkSounds, false)
//			.replaceAll("Hi ", "Heeey ")
//			.replaceAll("yes", "yesh")
//			.replaceAll("is", "ish")
//			.replaceAll("So", "Sho")
//			.replaceAll("so", "sho");
	}

	/**
	 * Applies a lisp to speech (a speech defect in which s is pronounced like th in thick and z is pronounced like th in this). Modified sibilants are italicised in order to assist with reading.

	 * Example:

	 * "Is there a zoo that's nearby?"

	 * "Ith there a thoo that'th nearby?"

	 *
	 * @param sentence The speech to which the lisp should be applied.
	 * @return The modified sentence.
	 */
	public static String applyLisp(String sentence) {
		String [] split = sentence.split("\\[(.*?)\\]");
		for(String s : split) {
			String [] splitConditional = s.split("#IF\\((.*?)\\)|#ELSEIF\\((.*?)\\)"); // Do not replace text inside conditional parsing statements
			for(String s2 : splitConditional) {
				String sReplace = s2
						.replaceAll("s", "th")
						.replaceAll("z", "th")
						.replaceAll("S", "Th")
						.replaceAll("Z", "Th");

					sentence = sentence.replace(s2, sReplace);
			}
		}

		return sentence;
	}

	/**
	 * Builds a string representing the list of items in a collection.
	 *
	 * If there is one item, that string will be returned:
	 * "something".
	 * If there are two items, they are combined with the combining word:
	 * "something and nothing".
	 * If there are three or more items, all will be combined with commas, except the last two will use the combining word:
	 * "something, nothing and everything".
	 *
	 * @param items a {@link Collection} of items to turn into a pretty list
	 * @param stringExtractor the function used to get the strings out of the objects in the collection
	 * @param combiningWord the word used to combine the last two items
	 * @param <T> the type of the objects in the collection
	 * @return a pretty string list representing the collection
	 */
	private static <T> String toStringList(Collection<T> items, Function<T, String> stringExtractor, String combiningWord) {
		Iterator<T> itemIterator = items.iterator();
		utilitiesStringBuilder.setLength(0);
		try {
			T currentItem = itemIterator.next();

			utilitiesStringBuilder.append(stringExtractor.apply(currentItem));
			if (itemIterator.hasNext()) { // If more than one item, enter the loop
				currentItem = itemIterator.next();
				while (itemIterator.hasNext()) { // Use commas until we're on the last item
					utilitiesStringBuilder.append(", " + stringExtractor.apply(currentItem));
					currentItem = itemIterator.next();
				}
				utilitiesStringBuilder.append((items.size()>2?", ":" ") + combiningWord + " " + stringExtractor.apply(currentItem));
			}
		} catch(NoSuchElementException ex) {
			System.err.println("Util.toStringList() error - NoSuchElementException! (It's probably nothing to worry about...)");
			ex.printStackTrace();
		}
		return utilitiesStringBuilder.toString();
	}

	public static String SplitR(String choices, String splitter) {
	    return choices.split(splitter)[random.nextInt(choices.length() - choices.replace(splitter, "").length() + 1)];
	};

	public static String subspeciesToStringList(Collection<Subspecies> subspecies, boolean capitalise) {
		return Util.toStringList(subspecies,
				(Subspecies o) ->
				""
					+(capitalise
							?Util.capitaliseSentence(o.getNamePlural(null))
							:o.getNamePlural(null))
					+"",
				"and");
	}

	public static String clothesToStringList(Collection<AbstractClothing> clothingSet, boolean capitalise) {
		return Util.toStringList(clothingSet, (AbstractClothing o) -> (capitalise?Util.capitaliseSentence(o.getClothingType().getName()):o.getClothingType().getName()), "and");
	}

	public static String setToStringListCoverableArea(Set<CoverableArea> coverableAreaSet) {
		return Util.toStringList(coverableAreaSet, (CoverableArea o) -> Util.capitaliseSentence(o.getName()), "and");
	}

	public static String stringsToStringList(List<String> list, boolean capitalise) {
		return Util.toStringList(list, (String o) -> capitalise?Util.capitaliseSentence(o):o, "and");
	}

	public static String stringsToStringChoice(List<String> list, boolean capitalise) {
		return Util.toStringList(list, (String o) -> capitalise?Util.capitaliseSentence(o):o, "or");
	}

	public static String colourSetToStringList(Set<Colour> colourSet) {
		return Util.toStringList(colourSet, Colour::getName, "and");
	}

	public static String coverableAreaListToStringList(List<CoverableArea> coverableAreaCollection) {
		return Util.toStringList(coverableAreaCollection, CoverableArea::getName, "and");
	}

	public static String inventorySlotsToStringList(List<InventorySlot> inventorySlots) {
		return Util.toStringList(inventorySlots, InventorySlot::getName, "and");
	}

	public static String inventorySlotsToParsedStringList(List<InventorySlot> inventorySlots, GameCharacter owner) {
		return Util.toStringList(inventorySlots, ((slot) -> slot.getNameOfAssociatedPart(owner)), "and");
	}

	public static String tattooInventorySlotsToStringList(List<InventorySlot> inventorySlots) {
		return Util.toStringList(inventorySlots, InventorySlot::getTattooSlotName, "and");
	}

	public static String displacementTypesToStringList(List<DisplacementType> displacedList) {
		return Util.toStringList(displacedList, DisplacementType::getDescriptionPast, "and");
	}

	public static <Any> Any randomItemFrom(List<Any> list) {
		if(list.isEmpty()) {
			return null;
		}
		return list.get(Util.random.nextInt(list.size()));
	}

	public static <Any> Any randomItemFrom(Set<Any> set) {
		List<Any> list = new ArrayList<>(set);
		return randomItemFrom(list);
	}

	public static <Any> Any randomItemFrom(Any[] array) {
		return array[Util.random.nextInt(array.length)];
	}

	public static int randomItemFrom(int[] array) {
		return array[Util.random.nextInt(array.length)];
	}

	public static String getClosestStringMatch(String input, Collection<String> choices) {
		if (choices.contains(input)) {
			return input;
		}
		int distance = Integer.MAX_VALUE;
		String closestString = input;
		for(String choice : choices) {
			int newDistance = getLevenshteinDistance(input, choice);
			if(newDistance < distance) {
				closestString = choice;
				distance = newDistance;
			}
		}
		return closestString;
	}

	public static int getLevenshteinDistance(String inputOne, String inputTwo) {
		// Don't care about case:
		inputOne = inputOne.toLowerCase();
		inputTwo = inputTwo.toLowerCase();

		// i == 0
		int[] costs = new int[inputTwo.length() + 1];

		for (int j = 0; j < costs.length; j++) {
			costs[j] = j;
		}
		for (int i = 1; i <= inputOne.length(); i++) {
			// j == 0; nw = lev(i - 1, j)
			costs[0] = i;
			int nw = i - 1;
			for (int j = 1; j <= inputTwo.length(); j++) {
				int cj = Math.min(
						1 + Math.min(costs[j], costs[j - 1]),
						inputOne.charAt(i - 1) == inputTwo.charAt(j - 1)
							? nw
							: nw + 1);
				nw = costs[j];
				costs[j] = cj;
			}
		}
		return costs[inputTwo.length()];
	}

	private static Map<String, List<String>> errorLogMap = new HashMap<>();
	public static void logGetNpcByIdError(String method, String id) {
		if(Main.DEBUG) { // So this doesn't flood error.log
			errorLogMap.putIfAbsent(method, new ArrayList<>());
			if(!errorLogMap.get(method).contains(id)) {
				System.err.println("Main.game.getNPCById("+id+") returning null in method: "+method);
				errorLogMap.get(method).add(id);
			}
		}
	}

	public static String getFileName(File f) {
		return f.getName().substring(0, f.getName().lastIndexOf('.'));
	}

	public static String getFileIdentifier(File f) {
		return f.getName().substring(0, f.getName().lastIndexOf('.')).replaceAll("'", "Q");
	}

	public static String getFileName(String filePath) {
		return filePath.substring(0, filePath.lastIndexOf('.'));
	}

	public static String getFileIdentifier(String filePath) {
		return filePath.substring(0, filePath.lastIndexOf('.')).replaceAll("'", "Q");
	}
}

repo_name: klorpa/Pedo

license: unknown


code:
package com.lilithsthrone.utils;

/**
 * @since 0.1.0
 * @version 0.3.1
 * @author Innoxia
 */
public class Vector2i {
	int x, y;

	public Vector2i(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public Vector2i(Vector2i vectorToCopy) {
		this.x = vectorToCopy.getX();
		this.y = vectorToCopy.getY();
	}

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return y;
	}

	public void setY(int y) {
		this.y = y;
	}

	public static float getDistance(Vector2i point1, Vector2i point2) {
		return (float) Math.sqrt(Math.pow(point1.getX()-point2.getX(), 2) + Math.pow(point1.getY()-point2.getY(), 2));
	}

	@Override
	public String toString() {
		return "["+x+", "+y+"]";
	}

	@Override
	public boolean equals(Object o) {
		if(o instanceof Vector2i){
			if(((Vector2i)o).getX() == x
				&& ((Vector2i)o).getY() == y){
					return true;
			}
		}
		return false;
	}

	@Override
	public int hashCode() {
		int result = 17;
		result = 31 * result + x;
		result = 31 * result + y;
		return result;
	}

	public boolean equals (int x, int y) {
		return this.x==x && this.y==y;
	}
}


code:
package com.lilithsthrone.utils;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * @since 0.1.87
 * @version 0.1.87
 * @author Innoxia
 */
public interface XMLSaving {

	/**
	 * @param parentElement The parent element to which to save this object to.
	 * @param doc The document to which to save this object to.
	 * @return The base element for this object's XML export.
	 */
	public Element saveAsXML(Element parentElement, Document doc);

	/**
	 * @param parentElement The parent element from which to load this object from.
	 * @param doc The document to which to load this object from.
	 * @return The base element for this object's XML export.
	 */
	public static <T extends Object> T loadFromXML(Element parentElement, Document doc) {
		return loadFromXML(null, parentElement, doc);
	}

	/**
	 * @param log A StringBuilder to write a log to.
	 * @param parentElement The parent element from which to load this object from.
	 * @param doc The document to which to load this object from.
	 * @return The base element for this object's XML export.
	 */
	public static <T extends Object> T loadFromXML(StringBuilder log, Element parentElement, Document doc) {
		return null;
	}
}


code:
package com.lilithsthrone.utils.comparators;

import java.util.Comparator;

import com.lilithsthrone.game.inventory.clothing.AbstractClothing;

/**
 * Compares by rarity, using the InventorySlot in index 0 of available equip slots.
 *
 * @since 0.1.0
 * @version 0.3.4
 * @author Innoxia
 */
public class ClothingRarityComparator implements Comparator<AbstractClothing> {
	@Override
	public int compare(AbstractClothing first, AbstractClothing second) {
		if (!first.isEnchantmentKnown() && !second.isEnchantmentKnown()) {
			return first.getClothingType().getEquipSlots().get(0).compareTo(second.getClothingType().getEquipSlots().get(0));

		} else if (!first.isEnchantmentKnown()) {
			return -1;

		} else if (!second.isEnchantmentKnown()) {
			return 1;
		}

		int result = first.getRarity().compareTo(second.getRarity());

		if (result != 0) {
			return result;
		} else {
			return first.getClothingType().getEquipSlots().get(0).compareTo(second.getClothingType().getEquipSlots().get(0));
		}
	}
}


code:
package com.lilithsthrone.utils.comparators;

import java.util.Comparator;

import com.lilithsthrone.game.inventory.clothing.AbstractClothingType;

/**
 * Compares by rarity, using the InventorySlot in index 0 of available equip slots.
 *
 * @since 0.1.2
 * @version 0.3.4
 * @author Innoxia
 */
public class ClothingTypeRarityComparator implements Comparator<AbstractClothingType> {
	@Override
	public int compare(AbstractClothingType first, AbstractClothingType second) {
		int result = first.getRarity().compareTo(second.getRarity());
		if (result != 0) {
			return result;
		} else {
			return first.getEquipSlots().get(0).getZLayer() - second.getEquipSlots().get(0).getZLayer();
		}
	}
}


code:
package com.lilithsthrone.utils.comparators;

import java.util.Comparator;

import com.lilithsthrone.game.inventory.clothing.AbstractClothing;

/**
 * Compares by zLayer, using the InventorySlot in index 0 of available equip slots.
 *
 * @since 0.1.0
 * @version 0.3.4
 * @author Innoxia
 */
public class ClothingZLayerComparator implements Comparator<AbstractClothing> {
	@Override
	public int compare(AbstractClothing o1, AbstractClothing o2) {
		if (o2.getClothingType().getEquipSlots().get(0).getZLayer() > o1.getClothingType().getEquipSlots().get(0).getZLayer()) {
			return 1;

		} else if (o2.getClothingType().getEquipSlots().get(0).getZLayer() == o1.getClothingType().getEquipSlots().get(0).getZLayer()) {
			return 0;

		} else {
			return -1;
		}
	}
}


code:
package com.lilithsthrone.utils.comparators;

import java.util.Comparator;

import com.lilithsthrone.game.inventory.clothing.AbstractClothing;

/**
 * @since 0.1.66
 * @version 0.2.5
 * @author Innoxia
 */
public class InventoryClothingComparator implements Comparator<AbstractClothing> {

	@Override
	public int compare(AbstractClothing first, AbstractClothing second) {
		if(!first.isEnchantmentKnown() && !second.isEnchantmentKnown()) {
			return 0;
		} else if(first.isEnchantmentKnown() && !second.isEnchantmentKnown()) {
			return -1;
		} else if(!first.isEnchantmentKnown() && second.isEnchantmentKnown()) {
			return 1;
		}

		int result = first.getRarity().compareTo(second.getRarity());

		if (result != 0) {
			return result;

		} else {
			result = first.getClothingType().toString().compareTo(second.getClothingType().toString());

			if(result!=0) {
				return result;
			} else {
				return first.getColour().getName().compareTo(second.getColour().getName());
			}
		}
	}
}


code:
package com.lilithsthrone.utils.comparators;

import java.util.Comparator;

import com.lilithsthrone.game.inventory.item.AbstractItem;

/**
 * @since 0.1.66
 * @version 0.2.5
 * @author Innoxia
 */
public class InventoryItemComparator implements Comparator<AbstractItem> {

	@Override
	public int compare(AbstractItem first, AbstractItem second) {
		int result = first.getRarity().compareTo(second.getRarity());

		if (result != 0) {
			return result;

		} else {
			result = first.getItemType().toString().compareTo(second.getItemType().toString());

			if(result!=0) {
				return result;
			} else {
				return first.getColour().getName().compareTo(second.getColour().getName());
			}
		}
	}
}


code:
package com.lilithsthrone.utils.comparators;

import java.util.Comparator;

import com.lilithsthrone.game.inventory.weapon.AbstractWeapon;

/**
 * @since 0.1.66
 * @version 0.2.5
 * @author Innoxia
 */
public class InventoryWeaponComparator implements Comparator<AbstractWeapon> {

	@Override
	public int compare(AbstractWeapon first, AbstractWeapon second) {
		int result = first.getRarity().compareTo(second.getRarity());

		if (result != 0) {
			return result;

		} else {
			result = first.getWeaponType().toString().compareTo(second.getWeaponType().toString());

			if(result!=0) {
				return result;
			} else {
				return first.getColour().getName().compareTo(second.getColour().getName());
			}
		}
	}
}


code:
package com.lilithsthrone.utils.comparators;

import java.util.Comparator;

import com.lilithsthrone.game.inventory.item.AbstractItemType;

/**
 * Compares by rarity.
 *
 * @since 0.1.2
 * @version 0.3.2
 * @author Innoxia
 */
public class ItemTypeRarityComparator implements Comparator<AbstractItemType> {
	@Override
	public int compare(AbstractItemType first, AbstractItemType second) {
		int result = first.getRarity().compareTo(second.getRarity());
		if (result != 0) {
			return result;
		} else {
			return first.getValue(null) - second.getValue(null);
		}
	}
}


code:
package com.lilithsthrone.utils.comparators;

import java.util.Comparator;

import com.lilithsthrone.game.inventory.clothing.AbstractClothing;

/**
 * Compares by zLayer of the InventorySlot in index 0 of available equip slots.
 *
 * @since 0.1.0
 * @version 0.3.4
 * @author Innoxia
 */
public class ReverseClothingZLayerComparator implements Comparator<AbstractClothing> {
	@Override
	public int compare(AbstractClothing o1, AbstractClothing o2) {
		if (o2.getClothingType().getEquipSlots().get(0).getZLayer() < o1.getClothingType().getEquipSlots().get(0).getZLayer()) {
			return 1;
		} else if (o2.getClothingType().getEquipSlots().get(0).getZLayer() == o1.getClothingType().getEquipSlots().get(0).getZLayer()) {
			return 0;
		} else {
			return -1;
		}
	}
}


code:
package com.lilithsthrone.utils.comparators;

import java.util.Comparator;

import com.lilithsthrone.game.inventory.weapon.AbstractWeaponType;

/**
 * Compares by rarity.
 *
 * @since 0.1.2
 * @version 0.3.2
 * @author Innoxia
 */
public class WeaponTypeRarityComparator implements Comparator<AbstractWeaponType> {
	@Override
	public int compare(AbstractWeaponType first, AbstractWeaponType second) {
		int result = first.getRarity().compareTo(second.getRarity());
		if (result != 0) {
			return result;
		} else {
			return (int) (first.getDamage() - second.getDamage());
		}
	}
}


code:
package com.lilithsthrone.utils.time;

import java.time.LocalDateTime;
import java.time.temporal.ChronoField;

/**
 * Implemented using equations found here: https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth
 *
 * @since 0.3.5.5
 * @version 0.3.5.5
 * @author Innoxia
 */
public class DateAndTime {

	private static final boolean DEBUG = false;

	private static double sin(double deg) {
		return Math.sin(Math.toRadians(deg));
	}

	private static double cos(double deg) {
		return Math.cos(Math.toRadians(deg));
	}

	private static double asin(double x) {
		return Math.toDegrees(Math.asin(x));
	}

	private static double acos(double x) {
		return Math.toDegrees(Math.acos(x));
	}

	/**
	 * Only works for dates of 4712 BCE or greater.
	 *
	 * @param date The date to convert to julianDate
	 * @return A double value representing days since 4713 BCE.
	 */
	private static double julianDate(LocalDateTime date) {
		int years = Math.abs(-4713 - date.getYear()) - 1; // -1 to get rid of the year which we are currently in
		int leapYearSwitch = 4713 + 1582;
		int leapYears = 0;
		if(years>leapYearSwitch) {
			int extraYears = (years%leapYearSwitch);
			leapYears = 1 + ((leapYearSwitch-1)/4) // 4712 BCE was a leap year, so add 1 for that, then start from 4712
					+ (extraYears/4) - extraYears/100 + extraYears/400; // After 1582, leap years are not counted for years that are divisible by 100 but not divisible by 400.
		} else {
			leapYears = years/4;
		}

		double day = date.get(ChronoField.DAY_OF_YEAR);
		double julianDate = (years * 365d) + day + leapYears - 10 - 0.5; // Subtract 10 to account for changeover from Julian to Gregorian in 1582

		if(DEBUG) {
			System.out.println("leapYears: "+leapYears);
			System.out.println("julianDate: "+julianDate);
		}
//		System.out.println("julianDate: "+julianDate);

		return julianDate;
	}

	/**
	 * https://en.wikipedia.org/wiki/Julian_day
	 *
	 * @param date The date you want to convert to julian days.
	 * @return How many julian days have passed since Jan 1st, 2000 12:00.
	 */
	private static double julianDay(LocalDateTime date) {
		return julianDate(date) - 2451545.0 + 0.0008;
	}

	/**
	 * https://en.wikipedia.org/wiki/Solar_time#Mean_solar_time
	 *
	 * @param julianDay The date, converted to julianDays.
	 * @param longitudeWest The longitude, in degrees, of the observer (west is negative, east is positive).
	 * @return An approximation of mean solar time at julianDay.
	 */
	private static double meanSolarNoon(double julianDay, double longitudeWest) {
		return julianDay - (longitudeWest/360);
	}

	/**
	 * https://en.wikipedia.org/wiki/Mean_anomaly
	 *
	 * @param meanSolarNoon Pass in a value from meanSolarNoon().
	 * @return The solar mean anomaly.
	 */
	private static double solarMeanAnomaly(double meanSolarNoon) {
		return (357.5291 + 0.98560028 * meanSolarNoon) % 360;
	}

	/**
	 * https://en.wikipedia.org/wiki/Equation_of_the_center
	 *
	 * @param solarMeanAnomaly Pass in value calculated from solarMeanAnomaly().
	 * @return The Equation of the centre value needed to calculate ecliptic longitude.
	 */
	private static double equationOfTheCenter(double solarMeanAnomaly) {
		return (1.9148*sin(solarMeanAnomaly)) + (0.0200*sin(2*solarMeanAnomaly)) + (0.0003*sin(3*solarMeanAnomaly));
	}

	/**
	 * https://en.wikipedia.org/wiki/Ecliptic_coordinate_system#Spherical_coordinates
	 *
	 * @param solarMeanAnomaly Pass in value calculated from solarMeanAnomaly().
	 * @param equationOfTheCenter Pass in value calculated from equationOfTheCenter().
	 * @return The ecliptic longitude.
	 */
	private static double eclipticLongitude(double solarMeanAnomaly, double equationOfTheCenter) {
		return (solarMeanAnomaly + equationOfTheCenter + 180 + 102.9372) % 360;
	}

	/**
	 * https://en.wikipedia.org/wiki/Julian_day
	 *
	 * @param meanSolarNoon Pass in value calculated from meanSolarNoon().
	 * @param solarMeanAnomaly Pass in value calculated from solarMeanAnomaly().
	 * @param eclipticLongitude Pass in value calculated from eclipticLongitude().
	 * @return The julain date for the local true solar transit.
	 */
	private static double solarTransit(double meanSolarNoon, double solarMeanAnomaly, double eclipticLongitude) {
		return 2451545.0 + meanSolarNoon + (0.0053*sin(solarMeanAnomaly)) - (0.0069*sin(2*eclipticLongitude));
	}

	/**
	 * https://en.wikipedia.org/wiki/Declination
	 *
	 * @param eclipticLongitude Pass in value calculated from eclipticLongitude().
	 * @return The declination of the sun.
	 */
	private static double declinationOfTheSun(double eclipticLongitude) {
		return asin(sin(eclipticLongitude) * sin(23.44));
	}

	/**
	 * https://en.wikipedia.org/wiki/Hour_angle
	 *
	 * @param declinationOfTheSun Pass in value calculated from declinationOfTheSun().
	 * @param angle The desired SolarElevationAngle from which times of 'rise' and 'set' can be found.
	 * @param latitude The latitude of the observer.
	 * @return The hour angle from the observer's zenith.
	 */
	private static double hourAngle(double declinationOfTheSun, SolarElevationAngle angle, double latitude) {
		return acos((sin(angle.getAngle()) - sin(latitude) * sin(declinationOfTheSun)) / (cos(latitude) * cos(declinationOfTheSun)));
	}

	/**
	 * @param solarTransit Pass in value calculated from solarTransit().
	 * @param hourAngle Pass in value calculated from hourAngle().
	 * @return The julian date at which the desired SolarElevationAngle transitions into the next phase (i.e. sunrise).
	 */
	private static double sunrise(double solarTransit, double hourAngle) {
		return solarTransit - (hourAngle/360d);
	}

	/**
	 * @param solarTransit Pass in value calculated from solarTransit().
	 * @param hourAngle Pass in value calculated from hourAngle().
	 * @return The julian date at which the desired SolarElevationAngle transitions back into the previous phase (i.e. sunset).
	 */
	private static double sunset(double solarTransit, double hourAngle) {
		return solarTransit + (hourAngle/360d);
	}

	/**
	 * @param julianDay The Julian date which you want converted into LocalDateTime.
	 * @return The LocalDateTime of the passed in Julian date.
	 */
	public static LocalDateTime julianDayToDate(double julianDate) {
//		  Q = JD+0.5
//		  Z = Integer part of Q
//		  W = (Z - 1867216.25)/36524.25
//		  X = W/4
//		  A = Z+1+W-X
//		  B = A+1524
//		  C = (B-122.1)/365.25
//		  D = 365.25xC
//		  E = (B-D)/30.6001
//		  F = 30.6001xE
//		  Day of month = B-D-F+(Q-Z)
//		  Month = E-1 or E-13 (must get number less than or equal to 12)
//		  Year = C-4715 (if Month is January or February) or C-4716 (otherwise)

		double Q = (julianDate);
		int Z = (int) Q;
		int W = (int) ((Z - 1867216.25)/36524.25);
		int X = W/4;
		int A = Z+1+W-X;
		int B = A+1524;
		int C = (int) ((B-122.1)/365.25);
		int D = (int) (365.25*C);
		int E = (int) ((B-D)/30.6001);
		int F = (int) (30.6001*E);

		int day = (int) (B-D-F+(Q-Z)) + 1;
		int month = (E-1)<=12?(E-1):E-13;
		int year = month==1||month==2?C-4715:C-4716;

		final double dayFraction = julianDate%1;
		final int hours = Math.min(23, (int) (dayFraction * 24));
		final int minutes = Math.min(59, (int) ((dayFraction * 24 - hours) * 60d));
		final int seconds = Math.min(59, (int) ((dayFraction * 24 * 3600 - (hours * 3600 + minutes * 60)) + .5));

		try {
			return LocalDateTime.of(year, month, day, hours, minutes, seconds);
		} catch(Exception ex) {
			if(DEBUG) {
				System.err.println("Error in julianDayToDate(): "+julianDate+": "+hours+":"+minutes+":"+seconds+" | "+day+", "+month+", "+year);
			}
			return null;
		}
	}

	/**
	 * @param date The date at which you want to find times of change of SolarElevationAngle.
	 * @param angle The SolarElevationAngle that you are interested in.
	 * @return The times when this SolarElevationAngle transitions into a different state, with [0] being the AM change (sunrise) and [1] being the PM change (sunset).

	 * This method will return null if the supplied SolarElevationAngle does not have a transition! This typically occurs at higher or lower latitudes.
	 *  For example, at the latitude of London during summer months, Astronomical Twilight never fully transitions into night.
	 */
	public static LocalDateTime[] getTimeOfSolarElevationChange(LocalDateTime date, SolarElevationAngle angle, double latitude, double longitudeWest) {
		double julianDay = julianDay(date);
		double meanSolarNoon = meanSolarNoon(julianDay, longitudeWest);
		double solarMeanAnomaly = solarMeanAnomaly(meanSolarNoon);
		double equationOfTheCenter = equationOfTheCenter(solarMeanAnomaly);
		double eclipticLongitude = eclipticLongitude(solarMeanAnomaly, equationOfTheCenter);
		double solarTransit = solarTransit(meanSolarNoon, solarMeanAnomaly, eclipticLongitude);
		double declinationOfTheSun = declinationOfTheSun(eclipticLongitude);
		double hourAngle = hourAngle(declinationOfTheSun, angle, latitude);

		double sunrise = sunrise(solarTransit, hourAngle);
		LocalDateTime sunriseLDT = Double.isNaN(sunrise)?null:julianDayToDate(sunrise);

		double sunset = sunset(solarTransit, hourAngle);
		LocalDateTime sunsetLDT = Double.isNaN(sunset)?null:julianDayToDate(sunset);

		if(DEBUG) {
			System.out.println("julianDay: "+julianDay);
			System.out.println("meanSolarNoon: "+meanSolarNoon);
			System.out.println("solarMeanAnomaly: "+solarMeanAnomaly);
			System.out.println("equationOfTheCenter: "+equationOfTheCenter);
			System.out.println("eclipticLongitude: "+eclipticLongitude);
			System.out.println("solarTransit: "+solarTransit);
			System.out.println("declinationOfTheSun: "+declinationOfTheSun);
			System.out.println("hourAngle: "+hourAngle);

			System.out.println(sunrise);
			System.out.println("Sunrise: "+sunriseLDT);

			System.out.println(sunset);
			System.out.println("Sunset: "+sunsetLDT);
		}
//		if(angle==SolarElevationAngle.SUN_ALTITUDE_SUNRISE_SUNSET) {
//			System.out.println(angle);
//			System.out.println("Sunrise: "+sunrise+" | "+sunriseLDT);
//			System.out.println("Sunset: "+sunset+" | "+sunsetLDT);
//		}

		return new LocalDateTime[] {sunriseLDT, sunsetLDT};
	}

	public static DayPeriod getDayPeriod(LocalDateTime date, double latitude, double longitudeWest) {

		LocalDateTime[] times = getTimeOfSolarElevationChange(date, SolarElevationAngle.SUN_ALTITUDE_SUNRISE_SUNSET, latitude, longitudeWest);
		if(times[0]==null || (date.isAfter(times[0]) && date.isBefore(times[1]))) {
			return DayPeriod.DAY;
		}

		times = getTimeOfSolarElevationChange(date, SolarElevationAngle.SUN_ALTITUDE_CIVIL_TWILIGHT, latitude, longitudeWest);
		if(times[0]==null || (date.isAfter(times[0]) && date.isBefore(times[1]))) {
			return DayPeriod.CIVIL_TWILIGHT;
		}

		times = getTimeOfSolarElevationChange(date, SolarElevationAngle.SUN_ALTITUDE_NAUTICAL_TWILIGHT, latitude, longitudeWest);
		if(times[0]==null || (date.isAfter(times[0]) && date.isBefore(times[1]))) {
			return DayPeriod.NAUTICAL_TWILIGHT;
		}

		times = getTimeOfSolarElevationChange(date, SolarElevationAngle.SUN_ALTITUDE_ASTRONOMICAL_TWILIGHT, latitude, longitudeWest);
		if(times[0]==null || (date.isAfter(times[0]) && date.isBefore(times[1]))) {
			return DayPeriod.ASTRONOMICAL_TWILIGHT;
		}

		return DayPeriod.NIGHT;
	}

}


code:
package com.lilithsthrone.utils.time;

import com.lilithsthrone.utils.Colour;

/**
 * https://en.wikipedia.org/wiki/Twilight
 *
 * @since 0.3.5.5
 * @version 0.3.5.5
 * @author Innoxia
 */
public enum DayPeriod {

	DAY("day-time", Colour.BASE_BLUE_LIGHT),

	CIVIL_TWILIGHT("civil twilight", Colour.BASE_PURPLE_LIGHT),

	NAUTICAL_TWILIGHT("nautical twilight", Colour.BASE_PURPLE),

	ASTRONOMICAL_TWILIGHT("astronomical twilight", Colour.BASE_BLUE_STEEL),

	NIGHT("night-time", Colour.BASE_BLUE_DARK);

	private String name;
	private Colour colour;

	private DayPeriod(String name, Colour colour) {
		this.name = name;
		this.colour = colour;
	}

	public String getName() {
		return name;
	}

	public Colour getColour() {
		return colour;
	}
}


code:
package com.lilithsthrone.utils.time;

/**
 *  The altitude of the sun (solar elevation angle) at different moments
 *
 * @since 0.3.5.5
 * @version 0.3.5.5
 * @author Innoxia
 */
public enum SolarElevationAngle {

	SUN_ALTITUDE_SUNRISE_SUNSET(-0.833),

	SUN_ALTITUDE_CIVIL_TWILIGHT(-6.0),

	SUN_ALTITUDE_NAUTICAL_TWILIGHT(-12.0),

	SUN_ALTITUDE_ASTRONOMICAL_TWILIGHT(-18.0)
	;

	private double angle;

	private SolarElevationAngle(double angle) {
		this.angle = angle;
	}

	public double getAngle() {
		return angle;
	}
}


code:
package com.lilithsthrone.world;

import java.awt.Color;
import java.util.List;
import java.util.Map;

import com.lilithsthrone.game.character.GameCharacter;
import com.lilithsthrone.utils.Colour;
import com.lilithsthrone.world.places.AbstractPlaceType;

/**
 * @since 0.2.12
 * @version 0.2.12
 * @author Innoxia
 */
public abstract class AbstractWorldType {

	private String name;
	private String fileLocation;
	private Colour colour;
	private int worldSize;
	private int timeToTransition;

	private int tileSetRowNumber;
	private int moveCost;
	private AbstractPlaceType standardPlace;
	private AbstractPlaceType cutOffZone;
	private List<AbstractPlaceType> places;
	private List<AbstractPlaceType> dangerousPlaces;

	private boolean usesFile;
	private Map<Color, AbstractPlaceType> placesMap;

	@Deprecated
	AbstractWorldType(int worldSize,
			String name,
			Colour colour,
			int timeToTransition,
			AbstractPlaceType standardPlace,
			AbstractPlaceType cutOffZone,
			List<AbstractPlaceType> places,
			List<AbstractPlaceType> dangerousPlaces) {
		this.worldSize=worldSize;

		this.name = name;
		this.colour = colour;
		this.timeToTransition=timeToTransition;
		this.moveCost = 5;

		this.standardPlace = standardPlace;
		this.cutOffZone = cutOffZone;

		this.places = places;
		this.dangerousPlaces = dangerousPlaces;

		fileLocation = null;
		usesFile = false;
	}

	protected AbstractWorldType(String name,
			Colour colour,
			int timeToTransition,
			String fileLocation,
			Map<Color, AbstractPlaceType> placesMap) {
		this.name = name;
		this.colour = colour;
		this.timeToTransition=timeToTransition;
		moveCost = 5;

		standardPlace = null;
		cutOffZone = null;

		places = null;
		dangerousPlaces = null;

		this.fileLocation = fileLocation;
		usesFile = true;
		this.placesMap=placesMap;
	}

	@Override
	public boolean equals(Object o) { // I know it doesn't include everything, but this should be enough to check for equality.
		if(super.equals(o)){
			if(o instanceof AbstractWorldType){
				if(((AbstractWorldType)o).getName().equals(getName())
						&& ((AbstractWorldType)o).getName().equals(getName())
						&& ((AbstractWorldType)o).getColour() == getColour()
						&& ((AbstractWorldType)o).getColour() == getColour()
						&& ((AbstractWorldType)o).getPlacesMap().equals(getPlacesMap())
						){
					return true;
				}
			}
		}
		return false;
	}

	@Override
	public int hashCode() { // I know it doesn't include everything, but this should be enough to check for equality.
		int result = 17;
		result = 31 * result + getName().hashCode();
		result = 31 * result + getColour().hashCode();
		if(getFileLocation()!=null) {
			result = 31 * result + getFileLocation().hashCode();
		}
		result = 31 * result + getPlacesMap().hashCode();
		return result;
	}

	public int getTileSetRowNumber() {
		return tileSetRowNumber;
	}

	public String getName() {
		return name;
	}

	public Colour getColour() {
		return colour;
	}

	public int getTimeToTransition() {
		return timeToTransition;
	}

	public int getMoveCost() {
		return moveCost;
	}

	public boolean isRevealedOnStart() {
		return false;
	}

	public AbstractPlaceType getStandardPlace() {
		return standardPlace;
	}

	public AbstractPlaceType getCutOffZone() {
		return cutOffZone;
	}

	public List<AbstractPlaceType> getPlaces() {
		return places;
	}

	public List<AbstractPlaceType> getDangerousPlaces() {
		return dangerousPlaces;
	}

	public String getFileLocation() {
		return fileLocation;
	}

	public boolean isUsesFile() {
		return usesFile;
	}

	public Map<Color, AbstractPlaceType> getPlacesMap() {
		return placesMap;
	}

	public int getWorldSize() {
		return worldSize;
	}

	public boolean isCompanionSexBlocked(GameCharacter companion) {
		return getCompanionSexBlockedReason(companion)!=null || getCompanionSexBlockedReason(companion).isEmpty();
	}

	public abstract String getCompanionSexBlockedReason(GameCharacter companion);

}


code:
package com.lilithsthrone.world;

/**
 * @since 0.1.0
 * @version 0.1.75
 * @author Innoxia
 */
public enum Bearing {
	NORTH,
	NORTH_EAST,
	EAST,
	SOUTH_EAST,
	SOUTH,
	SOUTH_WEST,
	WEST,
	NORTH_WEST,
	RANDOM;
}


code:
package com.lilithsthrone.world;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.lilithsthrone.game.Game;
import com.lilithsthrone.game.character.CharacterUtils;
import com.lilithsthrone.game.inventory.CharacterInventory;
import com.lilithsthrone.game.inventory.Rarity;
import com.lilithsthrone.game.inventory.clothing.AbstractClothing;
import com.lilithsthrone.game.inventory.item.AbstractItem;
import com.lilithsthrone.game.inventory.weapon.AbstractWeapon;
import com.lilithsthrone.main.Main;
import com.lilithsthrone.utils.Vector2i;
import com.lilithsthrone.utils.XMLSaving;
import com.lilithsthrone.world.places.GenericPlace;
import com.lilithsthrone.world.places.PlaceUpgrade;

/**
 * @since 0.1.0
 * @version 0.3.2
 * @author Innoxia
 */
public class Cell implements XMLSaving {

	public static final int CELL_MAXIMUM_INVENTORY_SPACE = 48;

	private WorldType type;

	private Vector2i location;

	private boolean discovered;
	private boolean travelledTo;
	private GenericPlace place;
	private CharacterInventory inventory;
	private Set<String> charactersPresentIds;
	private Set<String> charactersHomeIds;
	private Set<String> charactersGlobalIds;

	public Cell(WorldType type, Vector2i location) {
		this.type = type;
		this.location = location;

		discovered = false;
		travelledTo = false;
		place = new GenericPlace(type.getStandardPlace());

		inventory = new CharacterInventory(0, CELL_MAXIMUM_INVENTORY_SPACE);
	}

	@Override
	public Element saveAsXML(Element parentElement, Document doc) {
		Element element = doc.createElement("cell");
		parentElement.appendChild(element);

		Element location = doc.createElement("location");
		element.appendChild(location);
		CharacterUtils.addAttribute(doc, location, "x", String.valueOf(this.getLocation().getX()));
		CharacterUtils.addAttribute(doc, location, "y", String.valueOf(this.getLocation().getY()));

		CharacterUtils.addAttribute(doc, element, "discovered", String.valueOf(this.discovered));
		CharacterUtils.addAttribute(doc, element, "travelledTo", String.valueOf(this.travelledTo));

		place.saveAsXML(element, doc);

		if(!inventory.isEmpty()) {
			inventory.saveAsXML(element, doc);
		}
		return element;
	}

	public static Cell loadFromXML(Element parentElement, Document doc, WorldType type) {

		Element locationElement = ((Element)parentElement.getElementsByTagName("location").item(0));

		Cell cell = new Cell(
				type,
				new Vector2i(
					Integer.valueOf(locationElement.getAttribute("x")),
					Integer.valueOf(locationElement.getAttribute("y"))));

		cell.setDiscovered(Boolean.valueOf(parentElement.getAttribute("discovered")));
		if(Main.isVersionOlderThan(Game.loadingVersion, "0.2.11.5")) {
			cell.setTravelledTo(Boolean.valueOf(parentElement.getAttribute("discovered")));

		} else {
			try {
				if(type.isRevealedOnStart()) {
					cell.setTravelledTo(true);
				} else {
					cell.setTravelledTo(Boolean.valueOf(parentElement.getAttribute("travelledTo")));
				}
			} catch(Exception ex) {
			}
		}

		cell.setPlace(GenericPlace.loadFromXML(((Element)parentElement.getElementsByTagName("place").item(0)), doc, cell), false);

		try {
			Node invNode = parentElement.getElementsByTagName("characterInventory").item(0);
			if(invNode!=null) {
				cell.setInventory(CharacterInventory.loadFromXML(((Element)invNode), doc));
			}
		} catch(Exception ex) {
			System.err.println("Cell import error 1");
		}

		cell.getInventory().setMaximumInventorySpace(CELL_MAXIMUM_INVENTORY_SPACE);

		return cell;
	}

	@Override
	public String toString() {
		return "Cell ("+this.getLocation().toString()+"): Place: "+this.getPlace().getPlaceType().getName();
	}

	public String getId() {
		return type.toString()+"-X:"+location.getX()+"-Y:"+location.getY();
	}

	public WorldType getType() {
		return type;
	}

	public void setType(WorldType type) {
		this.type = type;
	}

	public String getPlaceName() {
		return place.getName();
	}

	public boolean isDiscovered() {
		return discovered;
	}

	public void setDiscovered(boolean discovered) {
		this.discovered = discovered;
	}

	public boolean isTravelledTo() {
		return travelledTo;
	}

	public void setTravelledTo(boolean travelledTo) {
		this.travelledTo = travelledTo;
	}

	public GenericPlace getPlace() {
		return place;
	}

	public void setPlace(GenericPlace place, boolean applyInventoryInit) {
		this.place = place;
		if(applyInventoryInit) {
			place.getPlaceType().applyInventoryInit(this.getInventory());
		}
	}

	public boolean addPlaceUpgrade(PlaceUpgrade upgrade) {
		return getPlace().addPlaceUpgrade(this, upgrade);
	}

	public boolean removePlaceUpgrade(PlaceUpgrade upgrade) {
		return getPlace().removePlaceUpgrade(this, upgrade);
	}

	public Vector2i getLocation() {
		return location;
	}

	public void setLocation(Vector2i location) {
		this.location = location;
	}

	public void resetInventory(){
		resetInventory(null);
	}

	public void resetInventory(List<Rarity> rarityOfItemsToSave){
		if(rarityOfItemsToSave!=null && !rarityOfItemsToSave.isEmpty()) {
			List<AbstractItem> itemsToSave = new ArrayList<>();
			for(AbstractItem item : this.inventory.getAllItemsInInventory().keySet()) {
				if(rarityOfItemsToSave.contains(item.getRarity())) {
					itemsToSave.add(item);
				}
			}

			List<AbstractWeapon> weaponsToSave = new ArrayList<>();
			for(AbstractWeapon weapon : this.inventory.getAllWeaponsInInventory().keySet()) {
				if(rarityOfItemsToSave.contains(weapon.getRarity())) {
					weaponsToSave.add(weapon);
				}
			}

			List<AbstractClothing> clothingToSave = new ArrayList<>();
			for(AbstractClothing clothing : this.inventory.getAllClothingInInventory().keySet()) {
				if(rarityOfItemsToSave.contains(clothing.getRarity())) {
					clothingToSave.add(clothing);
				}
			}

			this.inventory = new CharacterInventory(0, 48);

			for(AbstractItem item : itemsToSave) {
				this.inventory.addItem(item);
			}
			for(AbstractWeapon weapon : weaponsToSave) {
				this.inventory.addWeapon(weapon);
			}
			for(AbstractClothing clothing : clothingToSave) {
				this.inventory.addClothing(clothing);
			}

		} else {
			this.inventory = new CharacterInventory(0, 48);
		}
	}

	public CharacterInventory getInventory() {
		return inventory;
	}

	public void setInventory(CharacterInventory inventory) {
		this.inventory = inventory;
	}

	public Set<String> getCharactersPresentIds() {
		return charactersPresentIds;
	}

	public void addCharacterPresentId(String id) {
		if(id.equals("NOT_SET")) {
			return;
		}
		if(charactersPresentIds==null) {
			charactersPresentIds = Collections.synchronizedSet(new HashSet<>());
		}
		synchronized (charactersPresentIds) {
			charactersPresentIds.add(id);
		}
	}

	public void removeCharacterPresentId(String id) {
		if(charactersPresentIds==null) {
			return;
		}
		synchronized (charactersPresentIds) {
//			System.out.println("removed "+id);
			charactersPresentIds.remove(id);
		}
	}

	public Set<String> getCharactersHomeIds() {
		return charactersHomeIds;
	}

	public void addCharacterHomeId(String id) {
		if(id.equals("NOT_SET")) {
			return;
		}
		if(charactersHomeIds==null) {
			charactersHomeIds = Collections.synchronizedSet(new HashSet<>());
		}
		synchronized (charactersHomeIds) {
			charactersHomeIds.add(id);
		}
	}

	public void removeCharacterHomeId(String id) {
		if(charactersHomeIds==null) {
			return;
		}
		synchronized (charactersHomeIds) {
			charactersHomeIds.remove(id);
		}
	}

	public Set<String> getCharactersGlobalIds() {
		return charactersGlobalIds;
	}

	public void addCharacterGlobalId(String id) {
		if(id.equals("NOT_SET")) {
			return;
		}
		if(charactersGlobalIds==null) {
			charactersGlobalIds = Collections.synchronizedSet(new HashSet<>());
		}
		synchronized (charactersGlobalIds) {
			charactersGlobalIds.add(id);
		}
	}

	public void removeCharacterGlobalId(String id) {
		if(charactersGlobalIds==null) {
			return;
		}
		synchronized (charactersGlobalIds) {
			charactersGlobalIds.remove(id);
		}
	}

}


code:
package com.lilithsthrone.world;

/**
 * @since 0.1.0
 * @version 0.1.75
 * @author Innoxia
 */
public enum EntranceType {
	RANDOM,
	SPECIAL,
	ALIGNED,
	ALIGNED_FLIP_HORIZONTAL,
	ALIGNED_FLIP_VERTICAL;
}