Added ranges to numerical stats randomization

This commit is contained in:
Jules 2023-10-14 23:34:09 +02:00
parent 56359e0b61
commit 4a38952310
2 changed files with 717 additions and 597 deletions
MMOItems-API/src/main/java/net/Indyuce/mmoitems

View File

@ -12,6 +12,7 @@ import org.jetbrains.annotations.NotNull;
import java.text.DecimalFormat;
import java.util.Random;
import java.util.regex.Pattern;
/**
* That Gaussian spread distribution thing that no one understands.
@ -19,245 +20,359 @@ import java.util.Random;
* @author indyuce
*/
public class NumericStatFormula implements RandomStatData<DoubleData>, UpdatableRandomStatData<DoubleData> {
private final double base, scale, spread, maxSpread;
private final double base, scale, spread, maxSpread, min, max;
private final boolean uniform, hasMin, hasMax;
private static final Random RANDOM = new Random();
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.####");
private static final Random RANDOM = new Random();
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.####");
public static final NumericStatFormula ZERO = new NumericStatFormula(0, 0, 0, 0);
public static final NumericStatFormula ZERO = new NumericStatFormula(0, 0, 0, 0);
/**
* When reading a numeric stat formula either from a config file
* (configuration section, or number) or when reading player input when a
* player edits a stat (string message). Although the string format would
* work in the config as well.
*
* Throws an IAE either if the format is not good or if the object does not
* have the right type
*
* @param object Object to read data from.
*/
public NumericStatFormula(Object object) {
Validate.notNull(object, "Config must not be null");
private final static double DEFAULT_MAX_SPREAD = .3;
if (object instanceof String) {
String[] split = object.toString().split(" ");
base = Double.parseDouble(split[0]);
scale = split.length > 1 ? Double.parseDouble(split[1]) : 0;
spread = split.length > 2 ? Double.parseDouble(split[2]) : 0;
maxSpread = split.length > 3 ? Double.parseDouble(split[3]) : 0;
return;
}
/**
* When reading a numeric stat formula either from a config file
* (configuration section, or number) or when reading player input when a
* player edits a stat (string message). Although the string format would
* work in the config as well.
* <p>
* Throws an IAE either if the format is not good or if the object does not
* have the right type.
*
* @param object Object to read data from.
*/
public NumericStatFormula(Object object) {
Validate.notNull(object, "Config must not be null");
if (object instanceof Number) {
base = Double.parseDouble(object.toString());
scale = 0;
spread = 0;
maxSpread = 0;
return;
}
if (object instanceof String) {
if (object instanceof ConfigurationSection) {
ConfigurationSection config = (ConfigurationSection) object;
base = config.getDouble("base");
scale = config.getDouble("scale");
spread = config.getDouble("spread");
maxSpread = config.getDouble("max-spread", .3);
// Uniform range from string
if (object.toString().contains("->")) {
String[] split = object.toString().replace(" ", "").split(Pattern.quote("->"));
base = 0;
scale = 0;
spread = 0;
maxSpread = 0;
uniform = true;
hasMin = true;
hasMax = true;
min = Double.parseDouble(split[0]);
max = Double.parseDouble(split[1]);
}
Validate.isTrue(spread >= 0, "Spread must be positive");
Validate.isTrue(maxSpread >= 0, "Max spread must be positive");
return;
}
// Gaussian distribution from string
else {
String[] split = object.toString().split(" ");
base = Double.parseDouble(split[0]);
scale = split.length > 1 ? Double.parseDouble(split[1]) : 0;
spread = split.length > 2 ? Double.parseDouble(split[2]) : 0;
maxSpread = split.length > 3 ? Double.parseDouble(split[3]) : 0;
hasMin = split.length > 4;
hasMax = split.length > 5;
min = hasMin ? Double.parseDouble(split[4]) : 0;
max = hasMax ? Double.parseDouble(split[5]) : 0;
uniform = false;
}
}
throw new IllegalArgumentException("Must specify a config section, a string or a number");
}
// Constant
else if (object instanceof Number) {
base = Double.parseDouble(object.toString());
scale = 0;
spread = 0;
maxSpread = 0;
uniform = false;
hasMin = false;
hasMax = false;
min = 0;
max = 0;
}
/**
* Formula for numeric statistics. These formulas allow stats to scale
* accordingly to the item level but also have a more or less important
* gaussian based random factor
*
* @param base Base value
* @param scale Value which scales with the item level
* @param spread The relative standard deviation of a normal law centered
* on (base + scale * level). If it's set to 0.1, the
* standard deviation will be 10% of the stat value without
* the random factor.
* @param maxSpread The max amount of deviation you can have. If it's set to
* 0.3, let A = base + scale * level, then the final stat
* value will be in [0.7 * A, 1.3 * A]
*/
public NumericStatFormula(double base, double scale, double spread, double maxSpread) {
this.base = base;
this.scale = scale;
this.spread = spread;
this.maxSpread = maxSpread;
}
// Load from config section
else if (object instanceof ConfigurationSection) {
ConfigurationSection config = (ConfigurationSection) object;
base = config.getDouble("base");
scale = config.getDouble("scale");
spread = config.getDouble("spread");
maxSpread = config.getDouble("max-spread", DEFAULT_MAX_SPREAD);
hasMin = config.contains("min");
hasMax = config.contains("max");
uniform = !config.contains("spread") && !config.contains("scale") && !config.contains("base") && hasMin && hasMax;
min = config.getDouble("min");
max = config.getDouble("max");
}
/**
* @return The 'Base' number of the item. This chooses the peak of the Gaussian Distribution.
* @see #getScale()
*/
public double getBase() { return base; }
// Error
else {
throw new IllegalArgumentException("Must specify a config section, a string or a number");
}
// Validates
Validate.isTrue(spread >= 0, "Spread must be positive");
Validate.isTrue(maxSpread >= 0, "Max spread must be positive");
}
/**
* @return When the item has a certain level or tier, this is how much each level shifts
* the peak, so that it is centered at {@code base + scale*level}
* @see #getBase()
*/
public double getScale() { return scale; }
/**
* A gaussian distribution with spread-based boundaries
*/
public NumericStatFormula(double base, double scale, double spread, double maxSpread) {
this(base, scale, spread, maxSpread, false, false, 0, false, 0);
}
/**
* @return Standard Deviation of the Gaussian Distribution
*/
public double getSpread() { return spread; }
/**
* A gaussian distribution with constant boundaries
*/
public NumericStatFormula(double base, double scale, double spread, double min, double max) {
this(base, scale, spread, 100, false, true, min, true, max);
}
/**
* @return For gaussian distributions, there always is that INSANELY SMALL chance of getting an INSANELY LARGE number.
* For example: At base atk dmg 10, and standard deviation 1:
* <p>68% of rolls will fall between 9 and 11;
* </p>95% of rolls will fall between 8 and 12;
* <p>99.7% of rolls will fall between 7 and 13;
* </p>10E-42 of a roll that will give you an epic 300 dmg sword
* <p></p>
* Whatever, this constrains to a minimum and maximum of output.
*/
public double getMaxSpread() { return maxSpread; }
/**
* Formula for numerical statistics. These formulas allow stats to scale
* accordingly to the item level but also have a more or less important
* gaussian/uniform based random factor.
*
* @param base Base value
* @param scale Value which scales with the item level
* @param spread The relative standard deviation of a normal law centered
* on (base + scale * level). If it's set to 0.1, the
* standard deviation will be 10% of the stat value without
* the random factor.
* @param maxSpread The max amount of deviation you can have. If it's set to
* 0.3, let A = base + scale * level, then the final stat
* value will be in [0.7 * A, 1.3 * A]
* @param hasMin Should the value have a lower threshold
* @param min Lower bound for numerical value
* @param hasMax Should the value have an upper threshold
* @param min Upper bound for numerical value
*/
public NumericStatFormula(double base, double scale, double spread, double maxSpread,
boolean uniform, boolean hasMin, double min, boolean hasMax, double max) {
this.base = base;
this.scale = scale;
this.spread = spread;
this.maxSpread = maxSpread;
this.uniform = uniform;
this.hasMin = hasMin;
this.hasMax = hasMax;
this.min = min;
this.max = max;
}
public static boolean useRelativeSpread;
/**
* @return The 'Base' number of the item. This chooses the peak of the Gaussian Distribution.
* @see #getScale()
*/
public double getBase() {
return base;
}
/**
* Applies the formula for a given input x.
*
* @param levelScalingFactor When choosing the mean of the distribution,
* the formula is <code>base + (scale*level)</code>.
* This is the <code>level</code>
*
* @return <b>Legacy formula: ???</b><br>
* Let A = {base} + {scale} * lvl, then the returned value is a
* random value taken in respect to a gaussian distribution
* centered on A, with average spread of {spread}%, and with a
* maximum offset of {maxSpread}% (relative to average value)
* <p></p>
* <b>Formula: Spread = Standard Deviation</b>
* The mean, the peak is located at <code>{base} + {scale}*lvl</code>. <br>
* The 'spread' is the standard deviation of the distribution. <br>
* 'Max Spread' constrains the result of this operation at <code>{mean}±{max spread}</code>
*/
public double calculate(double levelScalingFactor) {
/**
* @return When the item has a certain level or tier, this is how much each level shifts
* the peak, so that it is centered at {@code base + scale*level}
* @see #getBase()
*/
public double getScale() {
return scale;
}
// Calculate yes
return calculate(levelScalingFactor, RANDOM.nextGaussian());
}
/**
* @return Standard Deviation of the Gaussian Distribution
*/
public double getSpread() {
return spread;
}
/**
* @param levelScalingFactor Level to scale the scale with
* @param random Result of <code>RANDOM.nextGaussian()</code> or whatever other
* value that you actually want to pass.
* @return The calculated value
*/
public double calculate(double levelScalingFactor, double random) {
/**
* @return For gaussian distributions, there always is that INSANELY SMALL
* chance of getting an INSANELY LARGE number.
* <p>
* For example: At base atk dmg 10, and standard deviation 1:
* <p>68% of rolls will fall between 9 and 11;
* </p>95% of rolls will fall between 8 and 12;
* <p>99.7% of rolls will fall between 7 and 13;
* </p>10E-42 of a roll that will give you an epic 300 dmg sword
* <p></p>
* Whatever, this constrains to a minimum and maximum of output.
*/
public double getMaxSpread() {
return maxSpread;
}
// The mean, the center of the distribution
final double actualBase = base + (scale * levelScalingFactor);
public boolean isUniform() {
return uniform;
}
/*
* This is one pick from a gaussian distribution at mean 0, and
* standard deviation 1, multiplied by the spread chosen.
* Does it exceed the max spread (positive or negative)? Not anymore!
*/
final double spreadCoef = Math.min(Math.max(random * spread, -maxSpread), maxSpread);
public boolean hasMin() {
return hasMin;
}
return useRelativeSpread ? actualBase * (1 + spreadCoef) : actualBase + spreadCoef;
}
public boolean hasMax() {
return hasMax;
}
@Override
public DoubleData randomize(MMOItemBuilder builder) {
return new DoubleData(calculate(builder.getLevel()));
}
public double getMin() {
return min;
}
/**
* Save some formula in a config file. This method is used when editing stat
* data in the edition GUI (when a player inputs a numeric formula)
*
* @param config The formula will be saved in that config file
* @param path The config path used to save the formula
*/
public void fillConfigurationSection(ConfigurationSection config, String path, FormulaSaveOption option) {
if (path == null)
throw new NullPointerException("Path was empty");
public double getMax() {
return max;
}
if (scale == 0 && spread == 0 && maxSpread == 0)
config.set(path, base == 0 && option == FormulaSaveOption.DELETE_IF_ZERO ? null : base);
public static boolean RELATIVE_SPREAD;
else {
config.set(path + ".base", base);
config.set(path + ".scale", scale);
config.set(path + ".spread", spread);
config.set(path + ".max-spread", maxSpread);
}
}
/**
* Applies the formula for a given input x.
*
* @param levelScalingFactor When choosing the mean of the distribution,
* the formula is <code>base + (scale*level)</code>.
* This is the <code>level</code>
* @return <b>Legacy formula: ???</b><br>
* Let A = {base} + {scale} * lvl, then the returned value is a
* random value taken in respect to a gaussian distribution
* centered on A, with average spread of {spread}%, and with a
* maximum offset of {maxSpread}% (relative to average value)
* <p></p>
* <b>Formula: Spread = Standard Deviation</b>
* The mean, the peak is located at <code>{base} + {scale}*lvl</code>. <br>
* The 'spread' is the standard deviation of the distribution. <br>
* 'Max Spread' constrains the result of this operation at <code>{mean}±{max spread}</code>
*/
public double calculate(double levelScalingFactor) {
public void fillConfigurationSection(ConfigurationSection config, String path) {
fillConfigurationSection(config, path, FormulaSaveOption.DELETE_IF_ZERO);
}
// Calculate yes
return calculate(levelScalingFactor, uniform ? RANDOM.nextDouble() : RANDOM.nextGaussian());
}
@Override
public String toString() {
/**
* @param scaleFactor Level to scale the scale with
* @param random Result of <code>RANDOM.nextGaussian()</code> or whatever other
* value that you actually want to pass. It can be any valuation of
* a random variable with mean 0 and variance 1.
* @return The calculated final numerical value
*/
public double calculate(double scaleFactor, double random) {
if (scale == 0 && spread == 0)
return DECIMAL_FORMAT.format(base);
double value;
if (uniform) {
value = min + (max - min) * random;
} else {
if (scale == 0)
return "[" + DECIMAL_FORMAT.format(base * (1 - maxSpread)) + " -> " + DECIMAL_FORMAT.format(base * (1 + maxSpread)) + "] (" + DECIMAL_FORMAT.format(spread * 100)
+ "% Spread) (" + DECIMAL_FORMAT.format(base) + " Avg)";
// The mean, the center of the distribution
final double actualBase = base + (scale * scaleFactor);
return "{Base=" + DECIMAL_FORMAT.format(base) + (scale != 0 ? ",Scale=" + DECIMAL_FORMAT.format(scale) : "") + (spread != 0 ? ",Spread=" + spread : "")
+ (maxSpread != 0 ? ",Max=" + maxSpread : "") + "}";
}
/*
* This is one pick from a gaussian distribution at mean 0, and
* standard deviation 1, multiplied by the spread chosen.
* Does it exceed the max spread (positive or negative)? Not anymore!
*/
final double spreadCoef = Math.min(Math.max(random * spread, -maxSpread), maxSpread);
public static void reload() { useRelativeSpread = !MMOItems.plugin.getConfig().getBoolean("additive-spread-formula", false); }
value = RELATIVE_SPREAD ? actualBase * (1 + spreadCoef) : actualBase + spreadCoef;
if (hasMin) value = Math.max(min, value);
if (hasMax) value = Math.max(max, value);
}
@NotNull
@SuppressWarnings("unchecked")
@Override
public DoubleData reroll(@NotNull ItemStat stat, @NotNull DoubleData original, int determinedItemLevel) {
return value;
}
// Very well, chance checking is only available for NumericStatFormula class
final double expectedValue = getBase() + (getScale() * determinedItemLevel);
final double previousValue = original.getValue();
final double shift = previousValue - expectedValue;
final double shiftSD = useRelativeSpread ? Math.abs(shift / (getSpread() * expectedValue)) : Math.abs(shift / getSpread());
final double maxSD = getMaxSpread() / getSpread();
@Override
public DoubleData randomize(MMOItemBuilder builder) {
return new DoubleData(calculate(builder.getLevel()));
}
// Greater than max spread? Or heck, 0.1% Chance or less wth
if (shiftSD > maxSD || shiftSD > 3.5) {
/**
* Save some formula in a config file. This method is used when editing stat
* data in the edition GUI (when a player inputs a numeric formula).
*
* @param config The formula will be saved in that config file
* @param path The config path used to save the formula
* @param option If zero formulas should be ignored
*/
public void fillConfigurationSection(@NotNull ConfigurationSection config, @NotNull String path, @NotNull FormulaSaveOption option) {
if (path == null)
throw new NullPointerException("Path is empty");
// Just fully reroll value
return new DoubleData(calculate(determinedItemLevel));
if (scale == 0 && spread == 0 && maxSpread == 0 && !uniform) {
config.set(path, base == 0 && option == FormulaSaveOption.DELETE_IF_ZERO ? null : base);
return;
}
// Data arguably fine tbh, just use previous
} else {
//UPGRD//MMOItems.log("\u00a7a +\u00a77 Acceptable Range --- kept");
config.createSection(path);
config = config.getConfigurationSection(path);
// Just clone I guess
return original.cloneData(); }
}
if (!uniform) {
config.set("base", base);
config.set("scale", scale);
config.set("spread", spread);
config.set("max-spread", maxSpread);
}
public enum FormulaSaveOption {
if (hasMin) config.set("min", min);
if (hasMax) config.set("max", max);
}
/**
* When toggled on, if the formula is set to 0 then the configuration
* section will just be deleted. This option fixes a bug where ability
* modifiers with non null default values cannot take strictly null
* values because inputting 0 would just delete the config section.
*/
DELETE_IF_ZERO,
public void fillConfigurationSection(ConfigurationSection config, String path) {
fillConfigurationSection(config, path, FormulaSaveOption.DELETE_IF_ZERO);
}
/**
* No option used
*/
NONE;
}
@Override
public String toString() {
if (scale == 0 && spread == 0)
return DECIMAL_FORMAT.format(base);
if (scale == 0)
return "[" + DECIMAL_FORMAT.format(base * (1 - maxSpread)) + " -> " + DECIMAL_FORMAT.format(base * (1 + maxSpread)) + "] (" + DECIMAL_FORMAT.format(spread * 100)
+ "% Spread) (" + DECIMAL_FORMAT.format(base) + " Avg)";
return "{Base=" + DECIMAL_FORMAT.format(base) + (scale != 0 ? ",Scale=" + DECIMAL_FORMAT.format(scale) : "") + (spread != 0 ? ",Spread=" + spread : "")
+ (maxSpread != 0 ? ",Max=" + maxSpread : "") + "}";
}
public static void reload() {
RELATIVE_SPREAD = !MMOItems.plugin.getConfig().getBoolean("additive-spread-formula", false);
}
@NotNull
@SuppressWarnings("unchecked")
@Override
public DoubleData reroll(@NotNull ItemStat stat, @NotNull DoubleData original, int determinedItemLevel) {
// Very well, chance checking is only available for NumericStatFormula class
final double expectedValue = getBase() + (getScale() * determinedItemLevel);
final double previousValue = original.getValue();
final double shift = previousValue - expectedValue;
final double shiftSD = RELATIVE_SPREAD ? Math.abs(shift / (getSpread() * expectedValue)) : Math.abs(shift / getSpread());
final double maxSD = getMaxSpread() / getSpread();
// Greater than max spread? Or heck, 0.1% Chance or less wth
if (shiftSD > maxSD || shiftSD > 3.5) {
// Just fully reroll value
return new DoubleData(calculate(determinedItemLevel));
// Data arguably fine tbh, just use previous
} else {
//UPGRD//MMOItems.log("\u00a7a +\u00a77 Acceptable Range --- kept");
// Just clone I guess
return original.cloneData();
}
}
public enum FormulaSaveOption {
/**
* When toggled on, if the formula is set to 0 then the configuration
* section will just be deleted. This option fixes a bug where ability
* modifiers with non null default values cannot take strictly null
* values because inputting 0 would just delete the config section.
*/
DELETE_IF_ZERO,
/**
* No option used
*/
NONE;
}
}

View File

@ -39,7 +39,7 @@ import java.util.regex.Pattern;
public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> implements Upgradable, Previewable<NumericStatFormula, DoubleData> {
private final boolean moreIsBetter;
private final boolean moreIsBetter;
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.####");
@ -57,399 +57,404 @@ public class DoubleStat extends ItemStat<NumericStatFormula, DoubleData> impleme
this.moreIsBetter = moreIsBetter;
}
/**
* @return If this stat supports negatives stat values
*/
public boolean handleNegativeStats() {
return true;
}
/**
* @return For example knockback resistance, 0.01 = 1% so multiplies by 100 when displaying.
*/
public double multiplyWhenDisplaying() {
return 1;
}
/**
* Usually, a greater magnitude of stat benefits the player (more health, more attack damage).
* <p>However, its not impossible for a stat to be evil instead, who knows?
*/
public boolean moreIsBetter() { return moreIsBetter; }
@Override
public NumericStatFormula whenInitialized(Object object) {
if (object instanceof Number)
return new NumericStatFormula(Double.parseDouble(object.toString()), 0, 0, 0);
if (object instanceof ConfigurationSection)
return new NumericStatFormula(object);
throw new IllegalArgumentException("Must specify a number or a config section");
}
@Override
public void whenApplied(@NotNull ItemStackBuilder item, @NotNull DoubleData data) {
// Get Value
double value = data.getValue();
// Cancel if it its NEGATIVE and this doesn't support negative stats.
if (value < 0 && !handleNegativeStats()) { return; }
// Identify the upgrade amount
double upgradeShift = 0;
// Displaying upgrades?
if (UpgradeTemplate.isDisplayingUpgrades() && item.getMMOItem().getUpgradeLevel() != 0) {
// Get stat history
StatHistory hist = item.getMMOItem().getStatHistory(this);
if (hist != null) {
// Get as if it had never been upgraded
//HSY//MMOItems.log(" \u00a73-\u00a7a- \u00a77Stat Change Display Recalculation \u00a73-\u00a7a-\u00a73-");
DoubleData uData = (DoubleData) hist.recalculateUnupgraded();
// Calculate Difference
upgradeShift = value - uData.getValue(); } }
// Display in lore
if (value != 0 || upgradeShift != 0) {
String loreInsert = formatPath(getId(), MMOItems.plugin.getLanguage().getStatFormat(getPath()), moreIsBetter, value * multiplyWhenDisplaying());
if (upgradeShift != 0)
loreInsert += UpgradeTemplate.getUpgradeChangeSuffix(plus(upgradeShift * multiplyWhenDisplaying()) + (MythicLib.plugin.getMMOConfig().decimals.format(upgradeShift * multiplyWhenDisplaying())), !isGood(upgradeShift * multiplyWhenDisplaying()));
item.getLore().insert(getPath(), loreInsert);
}
/*
* Add NBT Data if it is not equal to ZERO, in which case it will just get removed.
*
* It is important that the tags are not excluded in getAppliedNBT() because the StatHistory does
* need that blanc tag information to remember when an Item did not initially have any of a stat.
*/
if (data.getValue() != 0) { item.addItemTag(getAppliedNBT(data)); }
}
@NotNull
@Deprecated
public static String formatPath(@NotNull String format, boolean moreIsBetter, double value) {
return formatPath("ATTACK_DAMAGE", format, moreIsBetter, value);
}
@NotNull
public static String formatPath(@NotNull String stat, @NotNull String format, boolean moreIsBetter, double value) {
final String valueFormatted = StatManager.format(stat, value);
final String colorPrefix = getColorPrefix(value < 0 && moreIsBetter);
return format
.replace("<plus>{value}", colorPrefix + (value > 0 ? "+" : "") + valueFormatted) // Replace conditional pluses with +value
.replace("{value}", colorPrefix + valueFormatted) // Replace loose pounds with the value
.replace("<plus>", (value > 0 ? "+" : "")); // Replace loose <plus>es
}
@NotNull
@Deprecated
public static String formatPath(@NotNull String format, boolean moreIsBetter, double min, double max) {
return formatPath("ATTACK_DAMAGE", format, moreIsBetter, min, max);
}
@NotNull
public static String formatPath(@NotNull String stat, @NotNull String format, boolean moreIsBetter, double min, double max) {
final String minFormatted = StatManager.format(stat, min), maxFormatted = StatManager.format(stat, max);
final String minPrefix = getColorPrefix(min < 0 && moreIsBetter), maxPrefix = getColorPrefix(max < 0 && moreIsBetter);
return format
.replace("<plus>", "")
.replace("{value}",
minPrefix + (min > 0 ? "+" : "") + minFormatted
+ MMOItems.plugin.getConfig().getString("stats-displaying.range-dash", "") +
maxPrefix + (min < 0 && max > 0 ? "+" : "") + maxFormatted);
}
@Override
public void whenPreviewed(@NotNull ItemStackBuilder item, @NotNull DoubleData currentData, @NotNull NumericStatFormula templateData) throws IllegalArgumentException {
Validate.isTrue(currentData instanceof DoubleData, "Current Data is not Double Data");
Validate.isTrue(templateData instanceof NumericStatFormula, "Template Data is not Numeric Stat Formula");
// Get Value
//SPRD//MMOItems.log("\u00a7c༺\u00a77 Calulating deviations of \u00a7b" + item.getMMOItem().getType().toString() + " " + item.getMMOItem().getId() + "\u00a77's \u00a7e" + getId());
double techMinimum = templateData.calculate(0, -2.5);
double techMaximum = templateData.calculate(0, 2.5);
// Cancel if it its NEGATIVE and this doesn't support negative stats.
if (techMaximum < 0 && !handleNegativeStats()) { return; }
if (techMinimum < 0 && !handleNegativeStats()) { techMinimum = 0; }
// Add NBT Path
item.addItemTag(getAppliedNBT(currentData));
// Display if not ZERO
if (techMinimum != 0 || techMaximum != 0) {
String builtRange;
if (SilentNumbers.round(techMinimum, 2) == SilentNumbers.round(techMaximum, 2)) {
builtRange = formatPath(getId(), MMOItems.plugin.getLanguage().getStatFormat(getPath()), moreIsBetter(), techMaximum * multiplyWhenDisplaying());
} else {
builtRange = formatPath(getId(), MMOItems.plugin.getLanguage().getStatFormat(getPath()), moreIsBetter(), techMinimum * multiplyWhenDisplaying(), techMaximum * multiplyWhenDisplaying()); }
// Just display normally
item.getLore().insert(getPath(), builtRange); }
}
@NotNull public static String getColorPrefix(boolean isNegative) {
// Get the base
return Objects.requireNonNull(MMOItems.plugin.getConfig().getString("stats-displaying.color-" + (isNegative ? "negative" : "positive"), ""));
}
@NotNull String plus(double amount) { if (amount >= 0) { return "+"; } else return ""; }
/**
* Usually, a greater magnitude of stat benefits the player (more health, more attack damage).
* <p>However, its not impossible for a stat to be evil instead, who knows?
* <p></p>
* This will return true if:
* <p> > The amount is positive, and more benefits the player
* </p> > The amount is negative, and more hurts the player
*/
public boolean isGood(double amount) {
return moreIsBetter() ? amount >= 0 : amount <= 0;
}
@Override
@NotNull
public ArrayList<ItemTag> getAppliedNBT(@NotNull DoubleData data) {
// Create Fresh
ArrayList<ItemTag> ret = new ArrayList<>();
// Add sole tag
ret.add(new ItemTag(getNBTPath(), data.getValue()));
// Return thay
return ret;
}
@Override
public void whenLoaded(@NotNull ReadMMOItem mmoitem) {
// Get tags
ArrayList<ItemTag> relevantTags = new ArrayList<>();
// Add sole tag
if (mmoitem.getNBT().hasTag(getNBTPath()))
relevantTags.add(ItemTag.getTagAtPath(getNBTPath(), mmoitem.getNBT(), SupportedNBTTagValues.DOUBLE));
// Use that
DoubleData bakedData = (DoubleData) getLoadedNBT(relevantTags);
// Valid?
if (bakedData != null) {
// Set
mmoitem.setData(this, bakedData);
}
}
@Override
@Nullable
public DoubleData getLoadedNBT(@NotNull ArrayList<ItemTag> storedTags) {
// You got a double righ
ItemTag tg = ItemTag.getTagAtPath(getNBTPath(), storedTags);
// Found righ
if (tg != null) {
// Thats it
return new DoubleData(SilentNumbers.round((Double) tg.getValue(), 4));
}
// Fail
return null;
}
@Override
public void whenClicked(@NotNull EditionInventory inv, @NotNull InventoryClickEvent event) {
if (event.getAction() == InventoryAction.PICKUP_HALF) {
inv.getEditedSection().set(getPath(), null);
inv.registerTemplateEdition();
inv.getPlayer().sendMessage(MMOItems.plugin.getPrefix() + "Successfully removed " + getName() + ChatColor.GRAY + ".");
return;
}
new StatEdition(inv, this).enable("Write in the chat the numeric value you want.",
"Second Format: {Base} {Scaling Value} {Spread} {Max Spread}", "Third Format: {Min Value} -> {Max Value}");
}
@Override
public void whenInput(@NotNull EditionInventory inv, @NotNull String message, Object... info) {
double base, scale, spread, maxSpread;
/*
* Supports the old RANGE formula with a minimum and a maximum value and
* automatically makes the conversion to the newest system. This way
* users can keep using the old system if they don't want to adapt to
* the complex gaussian stat calculation
*/
if (message.contains("->")) {
String[] split = message.replace(" ", "").split(Pattern.quote("->"));
Validate.isTrue(split.length > 1, "You must specify two (both min and max) values");
double min = Double.parseDouble(split[0]), max = Double.parseDouble(split[1]);
Validate.isTrue(max > min, "Max value must be greater than min value");
base = MMOUtils.truncation(min == -max ? (max - min) * .05 : (min + max) / 2, 3);
scale = 0; // No scale
maxSpread = MMOUtils.truncation((max - min) / (2 * base), 3);
spread = MMOUtils.truncation(.8 * maxSpread, 3);
}
// Newest system with gaussian values calculation
else {
String[] split = message.split(" ");
base = MMOUtils.parseDouble(split[0]);
scale = split.length > 1 ? MMOUtils.parseDouble(split[1]) : 0;
spread = split.length > 2 ? MMOUtils.parseDouble(split[2]) : 0;
maxSpread = split.length > 3 ? MMOUtils.parseDouble(split[3]) : 0;
}
// Save as a flat formula
if (scale == 0 && spread == 0 && maxSpread == 0)
inv.getEditedSection().set(getPath(), base);
else {
inv.getEditedSection().set(getPath() + ".base", base);
inv.getEditedSection().set(getPath() + ".scale", scale == 0 ? null : scale);
inv.getEditedSection().set(getPath() + ".spread", spread == 0 ? null : spread);
inv.getEditedSection().set(getPath() + ".max-spread", maxSpread == 0 ? null : maxSpread);
}
inv.registerTemplateEdition();
inv.getPlayer().sendMessage(MMOItems.plugin.getPrefix() + getName() + " successfully changed to {" + base + " - " + scale + " - " + spread
+ " - " + maxSpread + "}");
}
@Override
public void whenDisplayed(List<String> lore, Optional<NumericStatFormula> statData) {
if (statData.isPresent()) {
NumericStatFormula data = statData.get();
lore.add(ChatColor.GRAY + "Base Value: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getBase())
+ (data.getScale() != 0 ? ChatColor.GRAY + " (+" + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getScale()) + ChatColor.GRAY + ")" : ""));
if (data.getSpread() > 0)
lore.add(ChatColor.GRAY + "Spread: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getSpread() * 100) + "%" + ChatColor.GRAY + " (Max: "
+ ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMaxSpread() * 100) + "%" + ChatColor.GRAY + ")");
} else
lore.add(ChatColor.GRAY + "Current Value: " + ChatColor.GREEN + "---");
lore.add("");
lore.add(ChatColor.YELLOW + AltChar.listDash + " Left click to change this value.");
lore.add(ChatColor.YELLOW + AltChar.listDash + " Right click to remove this value.");
}
@Override
@NotNull public DoubleData getClearStatData() {
return new DoubleData(0D);
}
@NotNull
@Override
public UpgradeInfo loadUpgradeInfo(@Nullable Object obj) throws IllegalArgumentException {
// Return result of thay
return DoubleUpgradeInfo.GetFrom(obj);
}
@NotNull
@Override
public StatData apply(@NotNull StatData original, @NotNull UpgradeInfo info, int level) {
// Must be DoubleData
int i = level;
if (original instanceof DoubleData && info instanceof DoubleUpgradeInfo) {
// Get value
double value = ((DoubleData) original).getValue();
// If leveling up
if (i > 0) {
// While still positive
while (i > 0) {
// Apply PMP Operation Positively
value = ((DoubleUpgradeInfo) info).getPMP().apply(value);
// Decrease
i--;
}
// Degrading the item
} else if (i < 0) {
// While still negative
while (i < 0) {
// Apply PMP Operation Reversibly
value = ((DoubleUpgradeInfo) info).getPMP().reverse(value);
// Decrease
i++;
}
}
// Update
((DoubleData) original).setValue(value);
}
// Upgraded
return original;
}
public static class DoubleUpgradeInfo implements UpgradeInfo {
@NotNull PlusMinusPercent pmp;
/**
* Generate a <code>DoubleUpgradeInfo</code> from this <code><b>String</b></code>
* that represents a {@link PlusMinusPercent}.
* <p></p>
* To keep older MMOItems versions working the same way, instead of having no prefix
* to use the <i>set</i> function of the PMP, one must use an <b><code>s</code></b> prefix.
* @param obj A <code><u>String</u></code> that encodes for a PMP.
* @throws IllegalArgumentException If any part of the operation goes wrong (including reading the PMP).
*/
@NotNull public static DoubleUpgradeInfo GetFrom(@Nullable Object obj) throws IllegalArgumentException {
// Shall not be null
Validate.notNull(obj, FriendlyFeedbackProvider.quickForConsole(FFPMMOItems.get(), "Upgrade operation must not be null"));
// Does the string exist?
String str = obj.toString();
if (str.isEmpty()) {
throw new IllegalArgumentException(
FriendlyFeedbackProvider.quickForConsole(FFPMMOItems.get(), "Upgrade operation is empty"));
}
// Adapt to PMP format
char c = str.charAt(0); if (c == 's') { str = str.substring(1); } else if (c != '+' && c != '-' && c != 'n') { str = '+' + str; }
// Is it a valid plus minus percent?
FriendlyFeedbackProvider ffp = new FriendlyFeedbackProvider(FFPMMOItems.get());
PlusMinusPercent pmpRead = PlusMinusPercent.getFromString(str, ffp);
if (pmpRead == null) {
throw new IllegalArgumentException(
ffp.getFeedbackOf(FriendlyFeedbackCategory.ERROR).get(0).forConsole(ffp.getPalette()));
}
// Success
return new DoubleUpgradeInfo(pmpRead);
}
public DoubleUpgradeInfo(@NotNull PlusMinusPercent pmp) { this.pmp = pmp; }
/**
* The operation every level will perform.
* @see PlusMinusPercent
*/
@NotNull public PlusMinusPercent getPMP() { return pmp; }
}
/**
* @return If this stat supports negatives stat values
*/
public boolean handleNegativeStats() {
return true;
}
/**
* @return For example knockback resistance, 0.01 = 1% so multiplies by 100 when displaying.
*/
public double multiplyWhenDisplaying() {
return 1;
}
/**
* Usually, a greater magnitude of stat benefits the player (more health, more attack damage).
* <p>However, its not impossible for a stat to be evil instead, who knows?
*/
public boolean moreIsBetter() {
return moreIsBetter;
}
@Override
public NumericStatFormula whenInitialized(Object object) {
if (object instanceof Number)
return new NumericStatFormula(Double.parseDouble(object.toString()), 0, 0, 0);
if (object instanceof ConfigurationSection)
return new NumericStatFormula(object);
throw new IllegalArgumentException("Must specify a number or a config section");
}
@Override
public void whenApplied(@NotNull ItemStackBuilder item, @NotNull DoubleData data) {
// Get Value
double value = data.getValue();
// Cancel if it its NEGATIVE and this doesn't support negative stats.
if (value < 0 && !handleNegativeStats()) {
return;
}
// Identify the upgrade amount
double upgradeShift = 0;
// Displaying upgrades?
if (UpgradeTemplate.isDisplayingUpgrades() && item.getMMOItem().getUpgradeLevel() != 0) {
// Get stat history
StatHistory hist = item.getMMOItem().getStatHistory(this);
if (hist != null) {
// Get as if it had never been upgraded
//HSY//MMOItems.log(" \u00a73-\u00a7a- \u00a77Stat Change Display Recalculation \u00a73-\u00a7a-\u00a73-");
DoubleData uData = (DoubleData) hist.recalculateUnupgraded();
// Calculate Difference
upgradeShift = value - uData.getValue();
}
}
// Display in lore
if (value != 0 || upgradeShift != 0) {
String loreInsert = formatPath(getId(), getGeneralStatFormat(), moreIsBetter, value * multiplyWhenDisplaying());
if (upgradeShift != 0)
loreInsert += UpgradeTemplate.getUpgradeChangeSuffix(plus(upgradeShift * multiplyWhenDisplaying()) + (MythicLib.plugin.getMMOConfig().decimals.format(upgradeShift * multiplyWhenDisplaying())), !isGood(upgradeShift * multiplyWhenDisplaying()));
item.getLore().insert(getPath(), loreInsert);
}
/*
* Add NBT Data if it is not equal to ZERO, in which case it will just get removed.
*
* It is important that the tags are not excluded in getAppliedNBT() because the StatHistory does
* need that blanc tag information to remember when an Item did not initially have any of a stat.
*/
if (data.getValue() != 0) {
item.addItemTag(getAppliedNBT(data));
}
}
@NotNull
@Deprecated
public static String formatPath(@NotNull String format, boolean moreIsBetter, double value) {
return formatPath("ATTACK_DAMAGE", format, moreIsBetter, value);
}
@NotNull
public static String formatPath(@NotNull String stat, @NotNull String format, boolean moreIsBetter, double value) {
final String valueFormatted = StatManager.format(stat, value);
final String colorPrefix = getColorPrefix(value < 0 && moreIsBetter);
return format
.replace("<plus>{value}", colorPrefix + (value > 0 ? "+" : "") + valueFormatted) // Replace conditional pluses with +value
.replace("{value}", colorPrefix + valueFormatted) // Replace loose pounds with the value
.replace("<plus>", (value > 0 ? "+" : "")); // Replace loose <plus>es
}
@NotNull
@Deprecated
public static String formatPath(@NotNull String format, boolean moreIsBetter, double min, double max) {
return formatPath("ATTACK_DAMAGE", format, moreIsBetter, min, max);
}
@NotNull
public static String formatPath(@NotNull String stat, @NotNull String format, boolean moreIsBetter, double min, double max) {
final String minFormatted = StatManager.format(stat, min), maxFormatted = StatManager.format(stat, max);
final String minPrefix = getColorPrefix(min < 0 && moreIsBetter), maxPrefix = getColorPrefix(max < 0 && moreIsBetter);
return format
.replace("<plus>", "")
.replace("{value}",
minPrefix + (min > 0 ? "+" : "") + minFormatted
+ MMOItems.plugin.getConfig().getString("stats-displaying.range-dash", "") +
maxPrefix + (min < 0 && max > 0 ? "+" : "") + maxFormatted);
}
@Override
public void whenPreviewed(@NotNull ItemStackBuilder item, @NotNull DoubleData currentData, @NotNull NumericStatFormula templateData) throws IllegalArgumentException {
Validate.isTrue(currentData instanceof DoubleData, "Current Data is not Double Data");
Validate.isTrue(templateData instanceof NumericStatFormula, "Template Data is not Numeric Stat Formula");
// Get Value
//SPRD//MMOItems.log("\u00a7c༺\u00a77 Calulating deviations of \u00a7b" + item.getMMOItem().getType().toString() + " " + item.getMMOItem().getId() + "\u00a77's \u00a7e" + getId());
double techMinimum = templateData.calculate(0, -2.5);
double techMaximum = templateData.calculate(0, 2.5);
// Cancel if it its NEGATIVE and this doesn't support negative stats.
if (techMaximum < 0 && !handleNegativeStats()) {
return;
}
if (techMinimum < 0 && !handleNegativeStats()) {
techMinimum = 0;
}
// Add NBT Path
item.addItemTag(getAppliedNBT(currentData));
// Display if not ZERO
if (techMinimum != 0 || techMaximum != 0) {
String builtRange;
if (SilentNumbers.round(techMinimum, 2) == SilentNumbers.round(techMaximum, 2)) {
builtRange = formatPath(getId(), getGeneralStatFormat(), moreIsBetter(), techMaximum * multiplyWhenDisplaying());
} else {
builtRange = formatPath(getId(), getGeneralStatFormat(), moreIsBetter(), techMinimum * multiplyWhenDisplaying(), techMaximum * multiplyWhenDisplaying());
}
// Just display normally
item.getLore().insert(getPath(), builtRange);
}
}
@NotNull
public static String getColorPrefix(boolean isNegative) {
// Get the base
return Objects.requireNonNull(MMOItems.plugin.getConfig().getString("stats-displaying.color-" + (isNegative ? "negative" : "positive"), ""));
}
@NotNull
String plus(double amount) {
if (amount >= 0) {
return "+";
} else return "";
}
/**
* Usually, a greater magnitude of stat benefits the player (more health, more attack damage).
* <p>However, its not impossible for a stat to be evil instead, who knows?
* <p></p>
* This will return true if:
* <p> > The amount is positive, and more benefits the player
* </p> > The amount is negative, and more hurts the player
*/
public boolean isGood(double amount) {
return moreIsBetter() ? amount >= 0 : amount <= 0;
}
@Override
@NotNull
public ArrayList<ItemTag> getAppliedNBT(@NotNull DoubleData data) {
// Create Fresh
ArrayList<ItemTag> ret = new ArrayList<>();
// Add sole tag
ret.add(new ItemTag(getNBTPath(), data.getValue()));
// Return thay
return ret;
}
@Override
public void whenLoaded(@NotNull ReadMMOItem mmoitem) {
// Get tags
ArrayList<ItemTag> relevantTags = new ArrayList<>();
// Add sole tag
if (mmoitem.getNBT().hasTag(getNBTPath()))
relevantTags.add(ItemTag.getTagAtPath(getNBTPath(), mmoitem.getNBT(), SupportedNBTTagValues.DOUBLE));
// Use that
DoubleData bakedData = getLoadedNBT(relevantTags);
// Valid?
if (bakedData != null) {
// Set
mmoitem.setData(this, bakedData);
}
}
@Override
@Nullable
public DoubleData getLoadedNBT(@NotNull ArrayList<ItemTag> storedTags) {
// You got a double righ
ItemTag tg = ItemTag.getTagAtPath(getNBTPath(), storedTags);
// Found righ
if (tg != null) {
// Thats it
return new DoubleData(SilentNumbers.round((Double) tg.getValue(), 4));
}
// Fail
return null;
}
@Override
public void whenClicked(@NotNull EditionInventory inv, @NotNull InventoryClickEvent event) {
if (event.getAction() == InventoryAction.PICKUP_HALF) {
inv.getEditedSection().set(getPath(), null);
inv.registerTemplateEdition();
inv.getPlayer().sendMessage(MMOItems.plugin.getPrefix() + "Successfully removed " + getName() + ChatColor.GRAY + ".");
return;
}
new StatEdition(inv, this).enable("Write in the chat the numeric value you want.",
"Second Format: {Base} {Scaling Value} {Spread} {Max Spread}", "Third Format: {Min Value} -> {Max Value}");
}
@Override
public void whenInput(@NotNull EditionInventory inv, @NotNull String message, Object... info) {
final NumericStatFormula formula = new NumericStatFormula(message);
formula.fillConfigurationSection(inv.getEditedSection(), getPath(), NumericStatFormula.FormulaSaveOption.DELETE_IF_ZERO);
inv.registerTemplateEdition();
inv.getPlayer().sendMessage(MMOItems.plugin.getPrefix() + getName() + " successfully changed to {"
+ formula.getBase() + " - " + formula.getScale() + " - " + formula.getSpread() + " - " + formula.getMaxSpread() + " - ("
+ formula.getMin() + " -> " + formula.getMax() + ") }");
}
@Override
public void whenDisplayed(List<String> lore, Optional<NumericStatFormula> statData) {
if (statData.isPresent()) {
NumericStatFormula data = statData.get();
if (data.isUniform()) {
lore.add(ChatColor.GRAY + "Uniform: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMin()) + ChatColor.GRAY + " -> " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMax()));
} else {
lore.add(ChatColor.GRAY + "Base Value: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getBase())
+ (data.getScale() != 0 ? ChatColor.GRAY + " (+" + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getScale()) + ChatColor.GRAY + "/Lvl)" : ""));
if (data.getSpread() > 0)
lore.add(ChatColor.GRAY + "Spread: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getSpread() * 100) + "%" + ChatColor.GRAY + " (Max: "
+ ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMaxSpread() * 100) + "%" + ChatColor.GRAY + ")");
if (data.hasMin())
lore.add(ChatColor.GRAY + "Min: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMin()));
if (data.hasMax())
lore.add(ChatColor.GRAY + "Max: " + ChatColor.GREEN + DECIMAL_FORMAT.format(data.getMax()));
}
} else
lore.add(ChatColor.GRAY + "Current Value: " + ChatColor.GREEN + "---");
lore.add("");
lore.add(ChatColor.YELLOW + AltChar.listDash + " Left click to change this value.");
lore.add(ChatColor.YELLOW + AltChar.listDash + " Right click to remove this value.");
}
@Override
@NotNull
public DoubleData getClearStatData() {
return new DoubleData(0D);
}
@NotNull
@Override
public UpgradeInfo loadUpgradeInfo(@Nullable Object obj) throws IllegalArgumentException {
// Return result of thay
return DoubleUpgradeInfo.GetFrom(obj);
}
@NotNull
@Override
public StatData apply(@NotNull StatData original, @NotNull UpgradeInfo info, int level) {
// Must be DoubleData
int i = level;
if (original instanceof DoubleData && info instanceof DoubleUpgradeInfo) {
// Get value
double value = ((DoubleData) original).getValue();
// If leveling up
if (i > 0) {
// While still positive
while (i > 0) {
// Apply PMP Operation Positively
value = ((DoubleUpgradeInfo) info).getPMP().apply(value);
// Decrease
i--;
}
// Degrading the item
} else if (i < 0) {
// While still negative
while (i < 0) {
// Apply PMP Operation Reversibly
value = ((DoubleUpgradeInfo) info).getPMP().reverse(value);
// Decrease
i++;
}
}
// Update
((DoubleData) original).setValue(value);
}
// Upgraded
return original;
}
public static class DoubleUpgradeInfo implements UpgradeInfo {
@NotNull
PlusMinusPercent pmp;
/**
* Generate a <code>DoubleUpgradeInfo</code> from this <code><b>String</b></code>
* that represents a {@link PlusMinusPercent}.
* <p></p>
* To keep older MMOItems versions working the same way, instead of having no prefix
* to use the <i>set</i> function of the PMP, one must use an <b><code>s</code></b> prefix.
*
* @param obj A <code><u>String</u></code> that encodes for a PMP.
* @throws IllegalArgumentException If any part of the operation goes wrong (including reading the PMP).
*/
@NotNull
public static DoubleUpgradeInfo GetFrom(@Nullable Object obj) throws IllegalArgumentException {
// Shall not be null
Validate.notNull(obj, FriendlyFeedbackProvider.quickForConsole(FFPMMOItems.get(), "Upgrade operation must not be null"));
// Does the string exist?
String str = obj.toString();
if (str.isEmpty()) {
throw new IllegalArgumentException(
FriendlyFeedbackProvider.quickForConsole(FFPMMOItems.get(), "Upgrade operation is empty"));
}
// Adapt to PMP format
char c = str.charAt(0);
if (c == 's') {
str = str.substring(1);
} else if (c != '+' && c != '-' && c != 'n') {
str = '+' + str;
}
// Is it a valid plus minus percent?
FriendlyFeedbackProvider ffp = new FriendlyFeedbackProvider(FFPMMOItems.get());
PlusMinusPercent pmpRead = PlusMinusPercent.getFromString(str, ffp);
if (pmpRead == null) {
throw new IllegalArgumentException(
ffp.getFeedbackOf(FriendlyFeedbackCategory.ERROR).get(0).forConsole(ffp.getPalette()));
}
// Success
return new DoubleUpgradeInfo(pmpRead);
}
public DoubleUpgradeInfo(@NotNull PlusMinusPercent pmp) {
this.pmp = pmp;
}
/**
* The operation every level will perform.
*
* @see PlusMinusPercent
*/
@NotNull
public PlusMinusPercent getPMP() {
return pmp;
}
}
}