// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // 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 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 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)); } } }