Compute XP thresholds from the client formula instead of the LevelToExp table

Client ArcCharacter_ExpForLevelUp (0x4715D0): XP to reach level L is
(L-1)^3 * max(1, 1 + (L-40) * 0.0512f) * class * race rune_exp_req_to_level,
rounded half up. getLevel steps levels with it up to LEVELCAP. Both methods
now take the PlayerCharacter so the factors come from its runes; the CSR
promotions 2901+ have no rune and use the base class factor.

Same values: every player race is 10 and every class rune 15, and the formula
equals the removed table for all 76 levels. Levels above the table no longer
use the old approximate fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-23 11:42:52 -04:00
1 parent db67e56302
commit 0c7b6f6c46
2 files changed
+55 -135

No files matched your search

+41 -121
View File
@@ -12,114 +12,17 @@ package engine.objects;
import engine.gameManager.ZoneManager;
import engine.math.Vector3fImmutable;
import engine.mbEnums.TargetColor;
import engine.parsers.objects.RuneTemplate;
import engine.server.MBServerStatics;
import java.util.ArrayList;
import java.util.TreeMap;
import static engine.gameManager.LootManager.LOOTMANAGER;
public class Experience {
private static final TreeMap<Integer, Integer> ExpToLevel;
private static final int[] LevelToExp = {Integer.MIN_VALUE, // Pad
// everything
// over 1
// R0
0, // Level 1
150, // Level 2
1200, // Level 3
4050, // Level 4
9600, // Level 5
18750, // Level 6
32400, // Level 7
51450, // Level 8
76800, // Level 9
// R1
109350, // Level 10
150000, // Level 11
199650, // Level 12
259200, // Level 13
329550, // Level 14
411600, // Level 15
506250, // Level 16
614400, // Level 17
736950, // Level 18
874800, // Level 19
// R2
1028850, // Level 20
1200000, // Level 21
1389150, // Level 22
1597200, // Level 23
1825050, // Level 24
2073600, // Level 25
2343750, // Level 26
2636400, // Level 27
2952450, // Level 28
3292800, // Level 29
// R3
3658350, // Level 30
4050000, // Level 31
4468650, // Level 32
4915200, // Level 33
5390550, // Level 34
5895600, // Level 35
6431250, // Level 36
6998400, // Level 37
7597950, // Level 38
8230800, // Level 39
// R4
8897850, // Level 40
10091520, // Level 41
11396777, // Level 42
12820187, // Level 43
14368505, // Level 44
16048666, // Level 45
17867790, // Level 46
19833183, // Level 47
21952335, // Level 48
24232919, // Level 49
// R5
26682793, // Level 50
29310000, // Level 51
32122766, // Level 52
35129502, // Level 53
38338805, // Level 54
41759452, // Level 55
45400409, // Level 56
49270824, // Level 57
53380030, // Level 58
57737542, // Level 59
// R6
62353064, // Level 60
67236479, // Level 61
72397859, // Level 62
77847457, // Level 63
83595712, // Level 64
89653247, // Level 65
96030869, // Level 66
102739569, // Level 67
109790524, // Level 68
117195093, // Level 69
// R7
124964822, // Level 70
133111438, // Level 71
141646855, // Level 72
150583171, // Level 73
159932666, // Level 74
169707808, // Level 75
179921247, // Level 76
};
// Client ArcCharacter_ExpForLevelUp (0x4715D0) float constant at 0x154195C
private static final float EXP_LEVEL_SLOPE = 0.0512f;
private static final float[] MaxExpPerLevel = {Float.MIN_VALUE, // Pad
// everything
// over
@@ -227,39 +130,56 @@ public class Experience {
// (expGranted / group.size()) * (groupBonus * (group.size()-1) +1)
private static final float GROUP_BONUS = 0.5f; // 0.2 grants (20%) addtional
static {
ExpToLevel = new TreeMap<>();
// flip keys and values for other Map
for (int i = 1; i < LevelToExp.length; i++) {
ExpToLevel.put(LevelToExp[i], i);
}
} // end Static block
// exp per group member
// called to determine current level based on xp
public static int getLevel(int experience) {
public static int getLevel(PlayerCharacter player, int experience) {
int expKey = ExpToLevel.floorKey(experience);
int level = ExpToLevel.get(expKey);
int level = 1;
if (level > MBServerStatics.LEVELCAP)
level = MBServerStatics.LEVELCAP;
while (level < MBServerStatics.LEVELCAP && experience >= getBaseExperience(player, level + 1))
level++;
return level;
}
// Get the base xp for a level
public static int getBaseExperience(int level) {
// The client function returns the XP to reach level + 1, so it is called with level - 1.
// Arithmetic order and the half-up rounding follow the client so every threshold matches.
public static int getBaseExperience(PlayerCharacter player, int level) {
if (level < LevelToExp.length)
return LevelToExp[level];
int clientLevel = level - 1;
double levelScale;
int fLevel = level - 1;
int baseXP = fLevel * fLevel * fLevel;
levelScale = Math.max(1.0, (clientLevel - 39) * (double) EXP_LEVEL_SLOPE + 1.0);
return (int) ((fLevel < 40) ? (baseXP * 150)
: (baseXP * (150 + (7.6799998 * (level - 40)))));
return (int) (long) ((double) clientLevel * clientLevel * clientLevel * levelScale
* getClassExpFactor(player) * getRaceExpFactor(player) + 0.5);
}
// rune_exp_req_to_level; the client uses 1.0 when the rune is missing
private static double getRaceExpFactor(PlayerCharacter player) {
RuneTemplate rune = null;
if (player.race != null)
rune = RuneTemplate._runes.get(player.race.getRaceRuneID());
return (rune == null) ? 1.0 : rune.rune_exp_req_to_level;
}
// GUESS: the client's class rune (0x45EAD0) is the promotion once taken; every CLASS rune is 15 either way.
// The CSR promotions 2901+ have no rune, so they fall back to the base class.
private static double getClassExpFactor(PlayerCharacter player) {
RuneTemplate rune = null;
if (player.promotionClass != null)
rune = RuneTemplate._runes.get(player.promotionClass.getObjectUUID());
if (rune == null && player.baseClass != null)
rune = RuneTemplate._runes.get(player.baseClass.getObjectUUID());
return (rune == null) ? 1.0 : rune.rune_exp_req_to_level;
}
// Max XP granted for killing a blue, yellow or orange mob
+14 -14
View File
@@ -1267,7 +1267,7 @@ public class PlayerCharacter extends AbstractCharacter {
player.endLoc = Vector3fImmutable.ZERO;
//get level based on experience
player.level = (short) Experience.getLevel(player.exp);
player.level = (short) Experience.getLevel(player, player.exp);
player.setHealth(999999f);
player.mana.set(999999f);
@@ -2828,7 +2828,7 @@ public class PlayerCharacter extends AbstractCharacter {
}
public short getPCLevel() {
short level = (short) Experience.getLevel(this.exp);
short level = (short) Experience.getLevel(this, this.exp);
if (this.promotionClass == null && level >= 10)
return (short) 10;
@@ -2873,8 +2873,8 @@ public class PlayerCharacter extends AbstractCharacter {
public synchronized void grantXP(int xp) {
// Stop players from getting experience past the cap
if (this.exp + xp >= Experience.getBaseExperience(MBServerStatics.LEVELCAP))
xp = Experience.getBaseExperience(MBServerStatics.LEVELCAP) - this.exp + 1;
if (this.exp + xp >= Experience.getBaseExperience(this, MBServerStatics.LEVELCAP))
xp = Experience.getBaseExperience(this, MBServerStatics.LEVELCAP) - this.exp + 1;
if (xp == 0)
xp = 1;
@@ -2890,7 +2890,7 @@ public class PlayerCharacter extends AbstractCharacter {
//not promoted at level 10, start checking for negative EXP
if (this.promotionClass == null && this.getLevel() == 10) {
if (this.getExp() == Experience.getBaseExperience(11)) {
if (this.getExp() == Experience.getBaseExperience(this, 11)) {
if (this.overFlowEXP == 110000)
return;
@@ -2913,10 +2913,10 @@ public class PlayerCharacter extends AbstractCharacter {
} else {
//Player exp begins negative exp, add remaing exp after level 11 to overflow
if (this.getExp() + remainingXP >= Experience.getBaseExperience(11)) {
if (this.getExp() + remainingXP >= Experience.getBaseExperience(this, 11)) {
this.overFlowEXP = remainingXP - (Experience.getBaseExperience(11) - this.getExp());
this.exp = Experience.getBaseExperience(11);
this.overFlowEXP = remainingXP - (Experience.getBaseExperience(this, 11) - this.getExp());
this.exp = Experience.getBaseExperience(this, 11);
GrantExperienceMsg grantExperienceMsg = new GrantExperienceMsg(this, remainingXP);
Dispatch dispatch = Dispatch.borrow(this, grantExperienceMsg);
@@ -2948,7 +2948,7 @@ public class PlayerCharacter extends AbstractCharacter {
else
nextLevel = level + 2;
int nextLevelEXP = Experience.getBaseExperience(nextLevel);
int nextLevelEXP = Experience.getBaseExperience(this, nextLevel);
// if overflow > 0, u have level 11 experience + overflow, but level is still 10 due to just promoting.
//Use level + 2 experience for next level.
@@ -2971,8 +2971,8 @@ public class PlayerCharacter extends AbstractCharacter {
checkGuildStatus();
this.addDatabaseJob("EXP", MBServerStatics.FIVE_MINUTES);
// double overflow exp used up, remaining overflow will just add to level + 1.
} else if (this.getExp() + this.overFlowEXP >= Experience.getBaseExperience(level + 1)) {
int nextExperience = Experience.getBaseExperience(level + 1) + this.overFlowEXP;
} else if (this.getExp() + this.overFlowEXP >= Experience.getBaseExperience(this, level + 1)) {
int nextExperience = Experience.getBaseExperience(this, level + 1) + this.overFlowEXP;
this.exp = nextExperience;
this.level++;
charReloadRequired = true;
@@ -2988,10 +2988,10 @@ public class PlayerCharacter extends AbstractCharacter {
} else {
// Hand out each Level one at a time.
isNewLevel = Experience.getLevel(exp + remainingXP) > this.getLevel();
isNewLevel = Experience.getLevel(this, exp + remainingXP) > this.getLevel();
if (isNewLevel) {
neededXP = Experience.getBaseExperience(this.getLevel() + 1) - this.exp;
neededXP = Experience.getBaseExperience(this, this.getLevel() + 1) - this.exp;
charReloadRequired = true;
this.exp += neededXP;
@@ -4088,7 +4088,7 @@ public class PlayerCharacter extends AbstractCharacter {
tmpLevel = (short) Math.min(tmpLevel, 75);
while (this.level < tmpLevel) {
grantXP(Experience.getBaseExperience(tmpLevel) - this.exp);
grantXP(Experience.getBaseExperience(this, tmpLevel) - this.exp);
}
}