Files
Server/src/engine/objects/Experience.java
T
MagicBotandClaude Opus 5.5 0c7b6f6c46 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>
2026-10-23 11:42:52 -04:00

375 lines
12 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
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 static engine.gameManager.LootManager.LOOTMANAGER;
public class Experience {
// 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
// 1
// R0
15, // Level 1
105, // Level 2
285, // Level 3
555, // Level 4
610, // Level 5
682.5f, // Level 6
730, // Level 7
975, // Level 8
1251.92f, // Level 9
// R1
1563.46f, // Level 10
1909.62f, // Level 11
2290.38f, // Level 12
2705.77f, // Level 13
3155.77f, // Level 14
3640.38f, // Level 15
4159.62f, // Level 16
4713.46f, // Level 17
5301.92f, // Level 18
5925, // Level 19
// R2
6582.69f, // Level 20
7275, // Level 21
8001.92f, // Level 22
8763.46f, // Level 23
9559.62f, // Level 24
10390.38f, // Level 25
11255.77f, // Level 26
12155.77f, // Level 27
13090.38f, // Level 28
14059.62f, // Level 29
// R3
15063.46f, // Level 30
16101.92f, // Level 31
17175, // Level 32
18282.69f, // Level 33
19425, // Level 34
20601.92f, // Level 35
21813.46f, // Level 36
23059.62f, // Level 37
24340.38f, // Level 38
25655.77f, // Level 39
// R4
45910.38f, // Level 40
34348.87f, // Level 41
37458.16f, // Level 42
40745.21f, // Level 43
44214.76f, // Level 44
47871.68f, // Level 45
51720.87f, // Level 46
55767.16f, // Level 47
60015.37f, // Level 48
64470.37f, // Level 49
// R5
69137.03f, // Level 50
74020.16f, // Level 51
79124.63f, // Level 52
84455.34f, // Level 53
90017.03f, // Level 54
95814.66f, // Level 55
101853.03f, // Level 56
108137, // Level 57
114671.37f, // Level 58
121461.11f, // Level 59
// R6
128510.92f, // Level 60
135825.79f, // Level 61
143410.47f, // Level 62
151269.87f, // Level 63
159408.82f, // Level 64
167832.16f, // Level 65
176544.74f, // Level 66
185551.45f, // Level 67
194857.08f, // Level 68
204466.55f, // Level 69
// R7
214384.63f, // Level 70
224616.24f, // Level 71
235166.21f, // Level 72
246039.34f, // Level 73
257240.58f, // Level 74
1 // 268774.71 //Level 75
};
// Used to calcuate the amount of experience a monster grants in the
// following formula
// expGranted = a(moblevel)^2 + b(moblevel) + c
private static final float EXPQUADA = 10.0f;
private static final float EXPQUADB = 6.0f;
private static final float EXPQUADC = -10.0f;
// Adds addtional exp per addtional member of a group using the following
// (expGranted / group.size()) * (groupBonus * (group.size()-1) +1)
private static final float GROUP_BONUS = 0.5f; // 0.2 grants (20%) addtional
// exp per group member
// called to determine current level based on xp
public static int getLevel(PlayerCharacter player, int experience) {
int level = 1;
while (level < MBServerStatics.LEVELCAP && experience >= getBaseExperience(player, level + 1))
level++;
return 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) {
int clientLevel = level - 1;
double levelScale;
levelScale = Math.max(1.0, (clientLevel - 39) * (double) EXP_LEVEL_SLOPE + 1.0);
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
public static float maxXPPerKill(int level) {
if (level < 1)
level = 1;
if (level > 75)
level = 75;
return MaxExpPerLevel[level];
}
// Returns a penalty modifier depending on mob color
public static double getConMod(AbstractCharacter pc, AbstractCharacter mob) {
switch (TargetColor.getCon(pc, mob)) {
case Red:
return 1.25;
case Orange:
return 1.15;
case Yellow:
return 1.05;
case Blue:
return 1;
case Cyan:
return 0.8;
case Green:
return 0.5;
default:
return 0;
}
}
public static double getGroupMemberPenalty(double leadership,
PlayerCharacter currPlayer, ArrayList<PlayerCharacter> players,
int highestLevel) {
double penalty = 0.0;
int level = currPlayer.getLevel();
// Group Size Penalty
if (players.size() > 2)
penalty = (players.size() - 2) * 1.5;
// Calculate Penalty For Highest level -> Current Player difference, !=
// check to prevent divide by zero error
if (highestLevel != level)
penalty += ((highestLevel - level) * .5);
// double avgLevels = totalLevels / adjustedGroupSize;
// if (adjustedGroupSize >= 1)
// if (level < avgLevels)
// penalty += ((avgLevels - level) * .5);
// Extra noob penalty
if ((highestLevel - level) > 25)
penalty += (highestLevel - level - 25);
return penalty;
}
public static void doExperience(PlayerCharacter killer, AbstractCharacter mob, Group g) {
// Check for some failure conditions
if (killer == null || mob == null)
return;
double grantedExperience = 0.0;
if (g != null) { // Do group EXP stuff
int leadership = 0;
int highestLevel = 0;
double penalty = 0.0;
ArrayList<PlayerCharacter> giveEXPTo = new ArrayList<>();
// Check if leader is within range of kill and then get leadership
// skill
Vector3fImmutable killLoc = mob.getLoc();
if (killLoc.distanceSquared2D(g.getGroupLead().getLoc()) < (MBServerStatics.EXP_RANGE * MBServerStatics.EXP_RANGE)) {
CharacterSkill leaderskill = g.getGroupLead().skills
.get("Leadership");
if (leaderskill != null)
leadership = leaderskill.getNumTrains();
if (leadership > 90)
leadership = 90; // leadership caps at 90%
}
// Check every group member for distance to see if they get xp
for (PlayerCharacter pc : g.getMembers()) {
if (pc.isAlive()) { // Skip if the player is dead.
// Check within range
if (killLoc.distanceSquared2D(pc.getLoc()) < (MBServerStatics.EXP_RANGE * MBServerStatics.EXP_RANGE)) {
giveEXPTo.add(pc);
// Track highest level character
if (pc.getLevel() > highestLevel)
highestLevel = pc.getLevel();
}
}
}
// Process every player in the group getting XP
for (PlayerCharacter playerCharacter : giveEXPTo) {
if (playerCharacter.getLevel() >= MBServerStatics.LEVELCAP)
continue;
// Sets Max XP with server exp mod taken into account.
grantedExperience = (double) LOOTMANAGER.NORMAL_EXP_RATE * maxXPPerKill(playerCharacter.getLevel());
// Adjust XP for Mob Level
grantedExperience *= getConMod(playerCharacter, mob);
// Process XP for this member
penalty = getGroupMemberPenalty(leadership, playerCharacter, giveEXPTo,
highestLevel);
// Leadership Penalty Reduction
if (leadership > 0)
penalty -= ((leadership) * 0.01) * penalty;
// Modify for hotzone
if (grantedExperience != 0)
if (ZoneManager.inHotZone(mob.getLoc()))
grantedExperience *= LOOTMANAGER.HOTZONE_EXP_RATE;
// Check for 0 XP due to white mob, otherwise subtract penalty
// xp
if (grantedExperience == 0)
grantedExperience = 1;
else {
grantedExperience -= (penalty * 0.01) * grantedExperience;
// Errant Penalty Calculation
if (playerCharacter.getGuild().isEmptyGuild())
grantedExperience *= 0.6;
}
if (grantedExperience == 0)
grantedExperience = 1;
// Grant the player the EXP
playerCharacter.grantXP((int) Math.floor(grantedExperience));
}
} else { // Give EXP to a single character
if (!killer.isAlive()) // Skip if the player is dead.
return;
if (killer.getLevel() >= MBServerStatics.LEVELCAP)
return;
// Get XP and adjust for Mob Level with world xp modifier taken into account
grantedExperience = (double) LOOTMANAGER.NORMAL_EXP_RATE * maxXPPerKill(killer.getLevel());
grantedExperience *= getConMod(killer, mob);
// Modify for hotzone
if (ZoneManager.inHotZone(mob.getLoc()))
grantedExperience *= LOOTMANAGER.HOTZONE_EXP_RATE;
// Errant penalty
if (grantedExperience != 1)
if (killer.getGuild().isEmptyGuild())
grantedExperience *= .6;
// Grant XP
killer.grantXP((int) Math.floor(grantedExperience));
}
}
}