Skip to content

Instantly share code, notes, and snippets.

@Jikoo
Last active December 11, 2024 17:09
Show Gist options
  • Save Jikoo/30ec040443a4701b8980 to your computer and use it in GitHub Desktop.
Save Jikoo/30ec040443a4701b8980 to your computer and use it in GitHub Desktop.
A utility for managing experience with Bukkit.
package com.github.jikoo.planarwrappers.util;
import org.bukkit.entity.Player;
/**
* A utility for managing player experience.
*/
public final class Experience {
/**
* Calculate a player's total experience based on level and progress to next.
*
* @param player the Player
* @return the amount of experience the Player has
*
* @see <a href=http://minecraft.wiki/Experience#Leveling_up>Experience#Leveling_up</a>
*/
public static int getExp(Player player) {
return getExpFromLevel(player.getLevel())
+ Math.round(getExpToNext(player.getLevel()) * player.getExp());
}
/**
* Calculate total experience based on level.
*
* @param level the level
* @return the total experience calculated
*
* @see <a href=http://minecraft.wiki/Experience#Leveling_up>Experience#Leveling_up</a>
*/
public static int getExpFromLevel(int level) {
if (level > 30) {
return (int) (4.5 * level * level - 162.5 * level + 2220);
}
if (level > 15) {
return (int) (2.5 * level * level - 40.5 * level + 360);
}
return level * level + 6 * level;
}
/**
* Calculate level (including progress to next level) based on total experience.
*
* @param exp the total experience
* @return the level calculated
*/
public static double getLevelFromExp(long exp) {
int level = getIntLevelFromExp(exp);
// Get remaining exp progressing towards next level. Cast to float for next bit of math.
float remainder = exp - (float) getExpFromLevel(level);
// Get level progress with float precision.
float progress = remainder / getExpToNext(level);
// Slap both numbers together and call it a day. While it shouldn't be possible for progress
// to be an invalid value (value < 0 || 1 <= value)
return ((double) level) + progress;
}
/**
* Calculate level based on total experience.
*
* @param exp the total experience
* @return the level calculated
*/
public static int getIntLevelFromExp(long exp) {
if (exp > 1395) {
return (int) ((Math.sqrt(72 * exp - 54215D) + 325) / 18);
}
if (exp > 315) {
return (int) (Math.sqrt(40 * exp - 7839D) / 10 + 8.1);
}
if (exp > 0) {
return (int) (Math.sqrt(exp + 9D) - 3);
}
return 0;
}
/**
* Get the total amount of experience required to progress to the next level.
*
* @param level the current level
*
* @see <a href=http://minecraft.wiki/Experience#Leveling_up>Experience#Leveling_up</a>
*/
private static int getExpToNext(int level) {
if (level >= 30) {
// Simplified formula. Internal: 112 + (level - 30) * 9
return level * 9 - 158;
}
if (level >= 15) {
// Simplified formula. Internal: 37 + (level - 15) * 5
return level * 5 - 38;
}
// Internal: 7 + level * 2
return level * 2 + 7;
}
/**
* Change a Player's experience.
*
* <p>This method is preferred over {@link Player#giveExp(int)}.
* <br>In older versions the method does not take differences in exp per level into account.
* This leads to overlevelling when granting players large amounts of experience.
* <br>In modern versions, while differing amounts of experience per level are accounted for, the
* approach used is loop-heavy and requires an excessive number of calculations, which makes it
* quite slow.
*
* @param player the Player affected
* @param exp the amount of experience to add or remove
*/
public static void changeExp(Player player, int exp) {
exp += getExp(player);
if (exp < 0) {
exp = 0;
}
double levelAndExp = getLevelFromExp(exp);
int level = (int) levelAndExp;
player.setLevel(level);
player.setExp((float) (levelAndExp - level));
}
private Experience() {}
}
@theepic007
Copy link

THANK YOU THANK YOU THANK YOU
you are a life saver.

@chuushi
Copy link

chuushi commented Jul 2, 2020

This util helped me a lot! Thank you!

@jordanwilliams1
Copy link

Great! Thanks!! This should be in the spigot api as stranded

@Programie
Copy link

Programie commented Apr 26, 2021

Thanks! Exactly what I needed.

But I found a small bug which seems to be caused by rounding and/or double to float conversion.

With a higher number of XP (like 2000), the changeExp() method gives an additional XP point to the player.

Example (assuming the player always has 0 XP points at level 0):

  • changeExp(player, 1000): The player gets 1000 XP points (level 26 + 3 XP points)
  • changeExp(player, 2000): The player gets 2001 XP points (level 34 + 104 XP points)

EDIT:

I managed to fix the issue by setting the player XP to 0 and give the player the remaining amount of XP:

public class Experience {
    ...
    public static void changeExp(Player player, int exp) {
        exp += getExp(player);

        if (exp < 0) {
            exp = 0;
        }

        int level = (int) getLevelFromExp(exp);
        int expAtLevel = getExpFromLevel(level);

        player.setLevel(level);
        player.setExp(0);
        player.giveExp(exp - expAtLevel);
    }
}

@Jikoo
Copy link
Author

Jikoo commented Apr 27, 2021

@Programie Interesting. I believe the point of returning a double was getting both values after a single calculation, but it's been a hot minute since I wrote this utility. I know the reason I used a double was to avoid loss of precision. Off the top of my head a float is a 32 bit datatype with only a 23? 24? bit significand where the double is 64 bits with a 50-ish significand, so by storing level as well in the single value it can potentially lose a decent amount of accuracy for very high levels. I'll look into it. May end up just requiring two calculations, which is a bit of a shame, but the logistics behind truncating mid-calculation are less than ideal. I believe your solution requires three calculations instead of two, but if it works it works.

@Jikoo
Copy link
Author

Jikoo commented Jun 2, 2021

@Programie Sorry it's taken me so long to dive into.

While your approach may work on modern versions, it's actually relatively severely suboptimal. Player#giveExp internally uses a loop. The original reason I wrote this utility was that the looping approach actually performs badly enough that a player using a macro to trigger experience changes could cause a second change to trigger before the first finished, resulting in charge-free operation for things that were supposed to consume experience on usage. The single-calculation approach fixed that. I did also include throttling to once per tick to my use case anyway, better safe than sorry.

In addition to the performance concerns of looping, back when this utility was written Player#giveExp actually did not take into account the difference in experience required per level. This meant that giving a player 700 experience at level 0 would result in them hitting level 100. If anyone is still using this on 1.8 (which for the record, I do not advocate) your solution will result in very incorrect levelling.

I've updated the method to truncate and recalculate progress with float precision (although I forgot I changed style preference in the years since I wrote it so the diff is absolutely unreadable). You can find the unit test here - values from -10 to 2000 being added to a base of 0 to 10 experience are tested.

/edit: And a note for anyone stumbling on this utility - it's now available via Maven using JitPack as I've thrown it into PlanarWrappers, a collection of utilities I keep rewriting or copying into new projects. License is WTFPL, credit is appreciated but by no means required.

@oddlama
Copy link

oddlama commented Oct 30, 2021

Hey @Jikoo, thanks for your work! It looks like you made a slight mistake while simplifying the formula here.

// Simplified formula. Internal: 112 + (level - 30) * 9
return level * 9 + 158;

I believe this should read level * 9 - 158, and as far as I can see this also affects your PlanarWrappers utility library.

@Jikoo
Copy link
Author

Jikoo commented Oct 30, 2021

@oddlama Nice catch. Looks like it used to be correct, I wonder how I pulled that off. I'll add unit tests with standard exp <-> levels to cover that for the future.

@anjoismysign
Copy link

License?

@Jikoo
Copy link
Author

Jikoo commented Apr 30, 2024

WTFPL/Unlicense, do what you like.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment