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>
375 lines
12 KiB
Java
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));
|
|
}
|
|
}
|
|
}
|