mirror of
https://gitlab.com/phoenix-dvpmt/mmoitems.git
synced 2025-01-04 06:47:34 +01:00
Added ranges to numerical stats randomization
This commit is contained in:
parent
56359e0b61
commit
4a38952310
MMOItems-API/src/main/java/net/Indyuce/mmoitems
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user