package me.eccentric_nz.TARDIS.achievement;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import org.apache.commons.lang.Validate;
import org.bukkit.entity.Player;
/**
* The Doctor Who Experience was one of the largest and most ambitious Doctor
* Who exhibitions ever staged. Featuring props and costumes from throughout the
* franchise's forty-seven year history, it also featured one new and different
* element; an interactive story in which people could take part and become part
* of the adventure.
*
* @author desht
*
* Adapted from ExperienceUtils code originally in ScrollingMenuSign.
*
* Credit to nisovin
* (http://forums.bukkit.org/threads/experienceutils-make-giving-taking-exp-a-bit-more-intuitive.54450/#post-1067480)
* for an implementation that avoids the problems of getTotalExperience(), which
* doesn't work properly after a player has enchanted something.
*
* Credit to comphenix for further contributions: See
* http://forums.bukkit.org/threads/experiencemanager-was-experienceutils-make-giving-taking-exp-a-bit-more-intuitive.54450/page-3#post-1273622
*
*/
public class TARDISXPRewarder {
private static int hardMaxLevel = 100000;
private static int xpTotalToReachLevel[];
private final WeakReference<Player> player;
private final String playerName;
static {
// 25 is an arbitrary value for the initial table size - the actual
// value isn't critically important since the table is resized as needed.
initLookupTables(25);
}
/**
* Create a new XPKCalculator for the given player.
*
* @param player the player for this XPKCalculator object
* @throws IllegalArgumentException if the player is null
*/
public TARDISXPRewarder(Player player) {
Validate.notNull(player, "Player cannot be null");
this.player = new WeakReference<Player>(player);
this.playerName = player.getName();
}
/**
* Get the current hard max level for which calculations will be done.
*
* @return the current hard max level
*/
public static int getHardMaxLevel() {
return hardMaxLevel;
}
/**
* Set the current hard max level for which calculations will be done.
*
* @param hardMaxLevel the new hard max level
*/
public static void setHardMaxLevel(int hardMaxLevel) {
TARDISXPRewarder.hardMaxLevel = hardMaxLevel;
}
/**
* Initialize the XP lookup table. See
* http://minecraft.gamepedia.com/Experience
*
* @param maxLevel The highest level handled by the lookup tables
*/
private static void initLookupTables(int maxLevel) {
xpTotalToReachLevel = new int[maxLevel];
for (int i = 0; i < xpTotalToReachLevel.length; i++) {
xpTotalToReachLevel[i]
= i >= 30 ? (int) (3.5 * i * i - 151.5 * i + 2220)
: i >= 16 ? (int) (1.5 * i * i - 29.5 * i + 360)
: 17 * i;
}
}
/**
* Calculate the level that the given XP quantity corresponds to, without
* using the lookup tables. This is needed if getLevelForExp() is called
* with an XP quantity beyond the range of the existing lookup tables.
*
* @param exp
* @return
*/
private static int calculateLevelForExp(int exp) {
int level = 0;
int curExp = 7; // level 1
int incr = 10;
while (curExp <= exp) {
curExp += incr;
level++;
incr += (level % 2 == 0) ? 3 : 4;
}
return level;
}
/**
* Get the Player associated with this XPKCalculator.
*
* @return the Player object
* @throws IllegalStateException if the player is no longer online
*/
public Player getPlayer() {
Player p = player.get();
if (p == null) {
throw new IllegalStateException("Player " + playerName + " is not online");
}
return p;
}
/**
* Adjust the player's XP by the given amount in an intelligent fashion.
* Works around some of the non-intuitive behaviour of the basic Bukkit
* player.giveExp() method.
*
* @param amt Amount of XP, may be negative
*/
public void changeExp(int amt) {
changeExp((double) amt);
}
/**
* Adjust the player's XP by the given amount in an intelligent fashion.
* Works around some of the non-intuitive behaviour of the basic Bukkit
* player.giveExp() method.
*
* @param amt Amount of XP, may be negative
*/
public void changeExp(double amt) {
setExp(getCurrentFractionalXP(), amt);
}
/**
* Set the player's experience
*
* @param amt Amount of XP, should not be negative
*/
public void setExp(int amt) {
setExp(0, amt);
}
/**
* Set the player's fractional experience.
*
* @param amt Amount of XP, should not be negative
*/
public void setExp(double amt) {
setExp(0, amt);
}
private void setExp(double base, double amt) {
int xp = (int) Math.max(base + amt, 0);
Player p = getPlayer();
int curLvl = p.getLevel();
int newLvl = getLevelForExp(xp);
// Increment level
if (curLvl != newLvl) {
p.setLevel(newLvl);
}
// Increment total experience - this should force the server to send an update packet
if (xp > base) {
p.setTotalExperience(p.getTotalExperience() + xp - (int) base);
}
double pct = (base - getXpForLevel(newLvl) + amt) / (double) (getXpNeededToLevelUp(newLvl));
p.setExp((float) pct);
}
/**
* Get the player's current XP total.
*
* @return the player's total XP
*/
public int getCurrentExp() {
Player p = getPlayer();
int lvl = p.getLevel();
int cur = getXpForLevel(lvl) + Math.round(getXpNeededToLevelUp(lvl) * p.getExp());
return cur;
}
/**
* Get the player's current fractional XP.
*
* @return The player's total XP with fractions.
*/
private double getCurrentFractionalXP() {
Player p = getPlayer();
int lvl = p.getLevel();
double cur = getXpForLevel(lvl) + (double) (getXpNeededToLevelUp(lvl) * p.getExp());
return cur;
}
/**
* Checks if the player has the given amount of XP.
*
* @param amt The amount to check for.
* @return true if the player has enough XP, false otherwise
*/
public boolean hasExp(int amt) {
return getCurrentExp() >= amt;
}
/**
* Checks if the player has the given amount of fractional XP.
*
* @param amt The amount to check for.
* @return true if the player has enough XP, false otherwise
*/
public boolean hasExp(double amt) {
return getCurrentFractionalXP() >= amt;
}
/**
* Get the level that the given amount of XP falls within.
*
* @param exp the amount to check for
* @return the level that a player with this amount total XP would be
* @throws IllegalArgumentException if the given XP is less than 0
*/
public int getLevelForExp(int exp) {
if (exp <= 0) {
return 0;
}
if (exp > xpTotalToReachLevel[xpTotalToReachLevel.length - 1]) {
// need to extend the lookup tables
int newMax = calculateLevelForExp(exp) * 2;
Validate.isTrue(newMax <= hardMaxLevel, "Level for exp " + exp + " > hard max level " + hardMaxLevel);
initLookupTables(newMax);
}
int pos = Arrays.binarySearch(xpTotalToReachLevel, exp);
return pos < 0 ? -pos - 2 : pos;
}
/**
* Retrieves the amount of experience the experience bar can hold at the
* given level.
*
* @param level the level to check
* @return the amount of experience at this level in the level bar
* @throws IllegalArgumentException if the level is less than 0
*/
public int getXpNeededToLevelUp(int level) {
Validate.isTrue(level >= 0, "Level may not be negative.");
return level > 30 ? 62 + (level - 30) * 7 : level >= 16 ? 17 + (level - 15) * 3 : 17;
}
/**
* Return the total XP needed to be the given level.
*
* @param level The level to check for.
* @return The amount of XP needed for the level.
* @throws IllegalArgumentException if the level is less than 0 or greater
* than the current hard maximum
*/
public int getXpForLevel(int level) {
Validate.isTrue(level >= 0 && level <= hardMaxLevel, "Invalid level " + level + "(must be in range 0.." + hardMaxLevel + ")");
if (level >= xpTotalToReachLevel.length) {
initLookupTables(level * 2);
}
return xpTotalToReachLevel[level];
}
}