diff --git a/src/main/java/cat/nyaa/ukit/utils/ExperienceUtils.java b/src/main/java/cat/nyaa/ukit/utils/ExperienceUtils.java index cccc635..91e18c1 100644 --- a/src/main/java/cat/nyaa/ukit/utils/ExperienceUtils.java +++ b/src/main/java/cat/nyaa/ukit/utils/ExperienceUtils.java @@ -1,6 +1,6 @@ package cat.nyaa.ukit.utils; -import com.google.common.primitives.Ints; +import com.google.common.base.Strings; import org.bukkit.entity.Player; public final class ExperienceUtils { @@ -10,12 +10,32 @@ public final class ExperienceUtils { /** * How much exp points at least needed to reach this level. * i.e. getLevel() = level && getExp() == 0 + * The formula is from the way how exp level grows in vanilla, and the formula is: + * \[ + * f(x) = + * \left \{ + * \begin{array}{l} + * x ^ 2 + 6 x, 0 \leq x \leq 16 \\ + * \frac{5}{2} x ^ 2 - \frac{81}{2} x + 360, 16 < x \leq 31 \\ + * \frac{9}{2} x ^ 2 - \frac{325}{2} x + 2220, 31 < x \leq 21863 + * \end{array} + * \right . + * \] */ - public static int getExpForLevel(int level) { - if (level < 0) throw new IllegalArgumentException(); - else if (level <= 16) return (level + 6) * level; - else if (level < 32) return Ints.checkedCast(Math.round(2.5 * level * level - 40.5 * level + 360)); - else return Ints.checkedCast(Math.round(4.5 * level * level - 162.5 * level + 2220)); + public static int getLeastExpForLevel(int level) { + if (level < 0) { + throw new IllegalArgumentException(); + } + if (level <= 16) { + return (level + 6) * level; + } + if (level <= 31) { + return (((5 * level - 81) * level) / 2) + 360; + } + if(level <= 21863) { + return ((9 * level - 325) * level + 4440) >>> 1; + } + throw new IllegalArgumentException(Strings.lenientFormat("Out of range: The exp points of the level %s is out of the range of Integer.", level)); } /** @@ -23,31 +43,38 @@ public static int getExpForLevel(int level) { */ public static int getExpPoints(Player p) { int pointForCurrentLevel = Math.round(p.getExpToLevel() * p.getExp()); - return getExpForLevel(p.getLevel()) + pointForCurrentLevel; + return getLeastExpForLevel(p.getLevel()) + pointForCurrentLevel; } - public static void subtractExpPoints(Player p, int points) { - if (points < 0) throw new IllegalArgumentException(); - if (points == 0) return; - int total = getExpPoints(p); - if (total < points) throw new IllegalArgumentException("Negative Exp Left"); - int newLevel = getLevelForExp(total - points); - int remPoint = total - points - getExpForLevel(newLevel); - p.setLevel(newLevel); - p.setExp(0); - p.giveExp(remPoint); + public static void subtractPlayerExpPoints(Player p, int points) { + addPlayerExpPoints(p, -points); } /** * Which level the player at if he/she has this mount of exp points - * TODO optimization + * The formula of this method is the exact inverse function of the function getLeastExpForLevel. + * \[ + * f(y) = + * \left \{ + * \begin{array}{l} + * \sqrt{9 + y} - 3 , 0 \leq y \leq 352 \\ + * \frac{\sqrt{40 y - 7839} }{10} + \frac{81}{10} , 352 < y \leq 1507 \\ + * \frac{\sqrt{72 y - 54215} }{18} + \frac{325}{18} , 1507 < y \leq 2 ^ {31} + * \end{array} + * \right . + * \] */ public static int getLevelForExp(int exp) { if (exp < 0) throw new IllegalArgumentException(); - for (int lv = 1; lv < 21000; lv++) { - if (getExpForLevel(lv) > exp) return lv - 1; + if(exp <= 352) { + return (int) Math.sqrt(9 + exp) - 3; + } + + if(exp <= 1507) { + return (int) (Math.sqrt(40 * exp - 7839) / 10 + 81d / 10d); + } else { + return (int) (Math.sqrt(72L * exp - 54215) / 18 + 325d / 18d); } - throw new IllegalArgumentException("exp too large"); } /** @@ -55,15 +82,17 @@ public static int getLevelForExp(int exp) { * Related events may be triggered. * * @param p the target player - * @param exp amount of xp to be added to the player, + * @param points amount of xp to be added to the player, * if negative, then subtract from the player. * @throws IllegalArgumentException if the player ended with negative xp */ - public static void addPlayerExperience(Player p, int exp) { - if (exp > 0) { - p.giveExp(exp); - } else if (exp < 0) { - subtractExpPoints(p, -exp); - } + public static void addPlayerExpPoints(Player p, int points) { + int playerPreviousExoPoints = getExpPoints(p); + if (playerPreviousExoPoints < -points) throw new IllegalArgumentException("Negative Exp Left"); + int newLevel = getLevelForExp(playerPreviousExoPoints + points); + int remPoint = playerPreviousExoPoints + points - getLeastExpForLevel(newLevel); + p.setLevel(newLevel); + p.setExp(0); + p.giveExp(remPoint); } } \ No newline at end of file diff --git a/src/main/java/cat/nyaa/ukit/xpstore/XpStoreFunction.java b/src/main/java/cat/nyaa/ukit/xpstore/XpStoreFunction.java index 51293e1..452aca4 100644 --- a/src/main/java/cat/nyaa/ukit/xpstore/XpStoreFunction.java +++ b/src/main/java/cat/nyaa/ukit/xpstore/XpStoreFunction.java @@ -21,6 +21,7 @@ import org.bukkit.event.entity.ExpBottleEvent; import org.bukkit.event.entity.ProjectileLaunchEvent; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataType; @@ -33,7 +34,7 @@ public class XpStoreFunction implements SubCommandExecutor, SubTabCompleter, Lis private final NamespacedKey LoreLineIndexKey; private final String EXPBOTTLE_PERMISSION_NODE = "ukit.xpstore"; private final Map playerExpBottleMap = new HashMap<>(); - private final List subCommands = List.of("store", "take"); + private final List subCommands = List.of("store", "take", "supply"); public XpStoreFunction(SpigotLoader pluginInstance) { this.pluginInstance = pluginInstance; @@ -77,47 +78,102 @@ public boolean invokeCommand(CommandSender commandSender, Command command, Strin senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.notValidAmount.produce(Pair.of("input", args[1]))); return true; } - if (args[0].equalsIgnoreCase("store")) { - var expTotal = amountItem * amountInput; - if (ExperienceUtils.getExpPoints(senderPlayer) < expTotal) { - senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.notEnoughExp.produce( - Pair.of("expTotal", expTotal), - Pair.of("expPerBottle", amountInput), - Pair.of("amount", amountItem) - )); - return true; + switch (args[0].toLowerCase()) { + case "store" -> { + int amountPerBottle; + + if(args.length > 3) { + if ("level".equals(args[2])) { + amountPerBottle = ExperienceUtils.getLeastExpForLevel(amountInput); + } else { + amountPerBottle = amountInput; + } + } else { + amountPerBottle = amountInput; + } + + int expTotal = amountPerBottle * amountInput; + + if(ExperienceUtils.getExpPoints(senderPlayer) < expTotal) { + senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.notEnoughExp.produce(Pair.of("amount", expTotal))); + return true; + } + + ItemStack itemSaved = addExpToItemStack(itemInHand, amountPerBottle); + Utils.setItemInHand(senderPlayer, Pair.of(itemSlot, itemSaved)); + ExperienceUtils.subtractPlayerExpPoints(senderPlayer, expTotal); + senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.expSaved.produce(Pair.of("amount", expTotal))); + } + case "supply" -> { + int expMovedTotal; + if(args.length < 3 || !(args[2].equals("level"))) { + expMovedTotal = getMinimumDivisible(amountInput - ExperienceUtils.getExpPoints(senderPlayer), amountItem); + } else { + expMovedTotal = getMinimumDivisible(ExperienceUtils.getLeastExpForLevel(amountInput) - ExperienceUtils.getExpPoints(senderPlayer), amountItem); + } + int amountMovedAverage = expMovedTotal / amountItem; + int amountRemaining = getExpContained(itemInHand) - amountMovedAverage; + if(amountRemaining < 0) { + senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.notEnoughExpInBottle.produce(Pair.of("amount", amountMovedAverage))); + return true; + } + ItemStack itemSaved = addExpToItemStack(itemInHand, -amountMovedAverage); + Utils.setItemInHand(senderPlayer, Pair.of(itemSlot, itemSaved)); + ExperienceUtils.addPlayerExpPoints(senderPlayer, expMovedTotal); + if(expMovedTotal <= 0) { + senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.expSaved.produce(Pair.of("amount", -expMovedTotal))); + } else { + senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.expTook.produce(Pair.of("amount", expMovedTotal))); + } } - var itemSaved = addExpToItemStack(itemInHand, amountInput); - Utils.setItemInHand(senderPlayer, Pair.of(itemSlot, itemSaved)); - ExperienceUtils.subtractExpPoints(senderPlayer, expTotal); - senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.expSaved.produce( - Pair.of("amount", expTotal) - )); - } else if (args[0].equalsIgnoreCase("take")) { - var expTotal = getMinimumDivisible(amountInput, amountItem); - var amountAverage = expTotal / amountItem; - var amountContained = getExpContained(itemInHand); - if (amountContained < amountAverage) { - senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.notEnoughExpInBottle.produce( - Pair.of("amount", amountAverage) - )); - return true; + case "take" -> { + int expTotal; + + if(args.length > 3) { + if ("level".equals(args[2])) { + expTotal = getMaximumDivisible(ExperienceUtils.getLeastExpForLevel(amountInput), amountItem); + } else { + expTotal = getMaximumDivisible(amountInput, amountItem); + } + } else { + expTotal = getMaximumDivisible(amountInput, amountItem); + } + + if(ExperienceUtils.getExpPoints(senderPlayer) < expTotal) { + senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.notEnoughExp.produce(Pair.of("amount", expTotal))); + return true; + } + + int amountPerBottle = expTotal / amountItem; + int amountContained = getExpContained(itemInHand); + if (amountContained < amountPerBottle) { + senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.notEnoughExpInBottle.produce(Pair.of("amount", amountPerBottle))); + return true; + } + ItemStack itemSaved = addExpToItemStack(itemInHand, -amountPerBottle); + Utils.setItemInHand(senderPlayer, Pair.of(itemSlot, itemSaved)); + ExperienceUtils.addPlayerExpPoints(senderPlayer, expTotal); + senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.expTook.produce(Pair.of("amount", expTotal))); } - var itemSaved = addExpToItemStack(itemInHand, -amountAverage); - Utils.setItemInHand(senderPlayer, Pair.of(itemSlot, itemSaved)); - ExperienceUtils.addPlayerExperience(senderPlayer, expTotal); - senderPlayer.sendMessage(pluginInstance.language.xpStoreLang.expTook.produce( - Pair.of("amount", expTotal) - )); } return true; } private int getMinimumDivisible(int amountExpect, int factor) { - if (amountExpect % factor == 0) + assert factor > 0; + if (amountExpect % factor == 0) { return amountExpect; - else - return amountExpect - (amountExpect % factor) + factor; + } else { + if(amountExpect > 0) { + return amountExpect - (amountExpect % factor) + factor; + } else { + return amountExpect - (amountExpect % factor); + } + } + } + + private int getMaximumDivisible(int amountExpect, int factor) { + return - getMinimumDivisible(-amountExpect, factor); } @Override @@ -133,22 +189,40 @@ public List tabComplete(CommandSender sender, Command command, String al if (args.length < 2) { return subCommands.stream().filter(t -> t.startsWith(args[0].toLowerCase())).toList(); } else if (args.length == 2) { - var item = Utils.getItemInHand(senderPlayer, Material.EXPERIENCE_BOTTLE); - if (args[0].equalsIgnoreCase("store")) { - if (item == null) - return List.of(pluginInstance.language.xpStoreLang.noExpBottleInHandTabNotice.produce()); - else - return List.of(String.valueOf(ExperienceUtils.getExpPoints(senderPlayer) / item.value().getAmount())); - } else if (args[0].equalsIgnoreCase("take")) { - if (item == null) - return List.of(pluginInstance.language.xpStoreLang.noExpBottleInHandTabNotice.produce()); - else - return List.of(String.valueOf(getExpContained(item.value()) * item.value().getAmount())); + Pair item = Utils.getItemInHand(senderPlayer, Material.EXPERIENCE_BOTTLE); + switch (args[0].toLowerCase()) { + case "store"-> { + if (item == null) { + return List.of(pluginInstance.language.xpStoreLang.noExpBottleInHandTabNotice.produce()); + } else { + return List.of(String.valueOf(ExperienceUtils.getExpPoints(senderPlayer) / item.value().getAmount())); + } + } + case "take" -> { + if (item == null) { + return List.of(pluginInstance.language.xpStoreLang.noExpBottleInHandTabNotice.produce()); + } else { + return List.of(String.valueOf(getExpContained(item.value()) * item.value().getAmount())); + } + } + case "supply" -> { + if (item == null) { + return List.of(pluginInstance.language.xpStoreLang.noExpBottleInHandTabNotice.produce()); + } else { + return List.of("0", "1", "30"); + } + } + default -> { + return List.of(); + } + } + + } else { + if(args.length == 3) { + return List.of("point", "level"); } else { return List.of(); } - } else { - return List.of(); } }