Skip to content

Instantly share code, notes, and snippets.

@Jikoo
Last active April 30, 2024 17:14
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • 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.gamepedia.com/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.gamepedia.com/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.gamepedia.com/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() {}
}
@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