package com.aionemu.gameserver.utils.stats; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.ArrayUtils; import com.aionemu.commons.utils.Rnd; import com.aionemu.gameserver.configs.main.FallDamageConfig; import com.aionemu.gameserver.configs.main.RatesConfig; import com.aionemu.gameserver.controllers.attack.AttackResult; import com.aionemu.gameserver.controllers.attack.AttackStatus; import com.aionemu.gameserver.controllers.observer.AttackerCriticalStatus; import com.aionemu.gameserver.model.SkillElement; import com.aionemu.gameserver.model.gameobjects.*; import com.aionemu.gameserver.model.gameobjects.player.Equipment; import com.aionemu.gameserver.model.gameobjects.player.Player; import com.aionemu.gameserver.model.gameobjects.player.Rates; import com.aionemu.gameserver.model.siege.Influence; import com.aionemu.gameserver.model.stats.calc.AdditionStat; import com.aionemu.gameserver.model.stats.calc.Stat2; import com.aionemu.gameserver.model.stats.calc.StatCapUtil; import com.aionemu.gameserver.model.stats.container.CreatureGameStats; import com.aionemu.gameserver.model.stats.container.PlayerGameStats; import com.aionemu.gameserver.model.stats.container.StatEnum; import com.aionemu.gameserver.model.templates.item.WeaponStats; import com.aionemu.gameserver.model.templates.item.enums.ItemSubType; import com.aionemu.gameserver.model.templates.npc.NpcRating; import com.aionemu.gameserver.skillengine.model.HitType; import com.aionemu.gameserver.world.WorldMapInstance; /** * Calculations are based on the following research:
* original: Link *
* backup: * Link * * * @author ATracer, alexa026, Neon */ public class StatFunctions { /** * @param maxLevelInRange * - level of the player who receives the reward (solo) or max player level in range (group) * @param target * - the npc * @return XP reward from target */ public static long calculateExperienceReward(int maxLevelInRange, Npc target) { WorldMapInstance instance = target.getPosition().getWorldMapInstance(); int baseXP = calculateBaseExp(target); float mapMulti = instance.getInstanceHandler().getExpMultiplier(); // map modifier to approach retail exp values if (instance.getParent().isInstanceType() && instance.getMaxPlayers() >= 2 && instance.getMaxPlayers() <= 6) { mapMulti *= instance.getMaxPlayers(); // on retail you get mob EP * max instance member count (only for group instances) mapMulti /= RatesConfig.XP_SOLO_RATES[0]; // custom: divide by regular xp rates, so they will not affect the rewarded XP } int xpPercentage = XPRewardEnum.xpRewardFrom(target.getLevel() - maxLevelInRange); long rewardXP = Math.round(baseXP * mapMulti * (xpPercentage / 100f)); return rewardXP; } /** * @return Experience value identical to the ones seen on aion databases (but seen retail exp rewards are always higher) */ private static int calculateBaseExp(Npc npc) { int maxHp = npc.getObjectTemplate().getStatsTemplate().getMaxHp(); if (maxHp <= 0) return 0; float multiplier; switch (npc.getRating()) { case JUNK: multiplier = 2f; break; case NORMAL: multiplier = 2.2f; break; case ELITE: multiplier = 4f; break; case HERO: multiplier = 5.4f; break; case LEGENDARY: multiplier = 6.4f; break; default: throw new IllegalArgumentException("Could not calculate experience reward for " + npc + " due to unknown rating."); } multiplier += npc.getRank().ordinal() * 0.2f; return Math.round(maxHp * multiplier); } /** * @return DP reward from target */ public static int calculateDPReward(Player player, Creature target) { int playerLevel = player.getCommonData().getLevel(); int targetLevel = target.getLevel(); NpcRating npcRating = ((Npc) target).getObjectTemplate().getRating(); // TODO: fix to see monster Rating level, NORMAL lvl 1, 2 | ELITE lvl 1, 2 etc.. // look at: // http://www.aionsource.com/forum/mechanic-analysis/42597-character-stats-xp-dp-origin-gerbator-team-july-2009-a.html int baseDP = targetLevel * calculateRatingMultiplier(npcRating); int xpPercentage = XPRewardEnum.xpRewardFrom(targetLevel - playerLevel); return (int) Rates.DP_PVE.calcResult(player, (int) Math.floor(baseDP * xpPercentage / 100f)); } /** * @return AP reward */ public static int calculatePvEApGained(Player player, Creature target) { if (player.getCommonData().getLevel() - target.getLevel() > 10) return 1; float apNpcRate = getApNpcRating(((Npc) target).getObjectTemplate().getRating()); // TODO: find out why they give 1/4 AP base(normal NpcRate) (5 AP retail) if (target.getName().equals("flame hoverstone")) apNpcRate = 0.5f; return (int) Rates.AP_PVE.calcResult(player, (int) Math.floor(15 * apNpcRate)); } /** * @return Points Lost in PvP Death */ public static int calculatePvPApLost(Player defeated, Player winner) { int pointsLost = defeated.getAbyssRank().getRank().getPointsLost(); // Level penalty calculation int difference = winner.getLevel() - defeated.getLevel(); if (difference >= 5) pointsLost = Math.round(pointsLost * 0.1f); else if (difference == 4) pointsLost = Math.round(pointsLost * 0.65f); else if (difference == 3) pointsLost = Math.round(pointsLost * 0.85f); return (int) Rates.AP_PVP_LOST.calcResult(defeated, pointsLost); } /** * @return Points Gained in PvP Kill */ public static int calculatePvpApGained(Player defeated, int winnerAbyssRank, int maxLevel) { int pointsGained = defeated.getAbyssRank().getRank().getPointsGained(); // Level penalty calculation int difference = maxLevel - defeated.getLevel(); if (difference > 4) { pointsGained = Math.round(pointsGained * 0.1f); } else if (difference < -3) { pointsGained = Math.round(pointsGained * 1.3f); } else { switch (difference) { case 3: pointsGained = Math.round(pointsGained * 0.85f); break; case 4: pointsGained = Math.round(pointsGained * 0.65f); break; case -2: pointsGained = Math.round(pointsGained * 1.1f); break; case -3: pointsGained = Math.round(pointsGained * 1.2f); break; } } // Abyss rank penalty calculation int defeatedAbyssRank = defeated.getAbyssRank().getRank().getId(); int abyssRankDifference = winnerAbyssRank - defeatedAbyssRank; if (winnerAbyssRank <= 7 && abyssRankDifference > 0) { float penaltyPercent = abyssRankDifference * 0.05f; pointsGained -= Math.round(pointsGained * penaltyPercent); } return pointsGained; } /** * @return XP Points Gained in PvP Kill TODO: Find the correct formula. */ public static int calculatePvpXpGained(Player defeated, int winnerAbyssRank, int maxLevel) { int pointsGained = 5000; // Level penalty calculation int difference = maxLevel - defeated.getLevel(); if (difference > 4) { pointsGained = Math.round(pointsGained * 0.1f); } else if (difference < -3) { pointsGained = Math.round(pointsGained * 1.3f); } else { switch (difference) { case 3: pointsGained = Math.round(pointsGained * 0.85f); break; case 4: pointsGained = Math.round(pointsGained * 0.65f); break; case -2: pointsGained = Math.round(pointsGained * 1.1f); break; case -3: pointsGained = Math.round(pointsGained * 1.2f); break; } } // Abyss rank penalty calculation int defeatedAbyssRank = defeated.getAbyssRank().getRank().getId(); int abyssRankDifference = winnerAbyssRank - defeatedAbyssRank; if (winnerAbyssRank <= 7 && abyssRankDifference > 0) { float penaltyPercent = abyssRankDifference * 0.05f; pointsGained -= Math.round(pointsGained * penaltyPercent); } return pointsGained; } public static int calculatePvpDpGained(Player defeated, int maxRank, int maxLevel) { int pointsGained; // base values int baseDp = 1064; int dpPerRank = 57; // adjust by rank pointsGained = (defeated.getAbyssRank().getRank().getId() - maxRank) * dpPerRank + baseDp; // adjust by level pointsGained = StatFunctions.adjustPvpDpGained(pointsGained, defeated.getLevel(), maxLevel); return pointsGained; } @SuppressWarnings("lossy-conversions") public static int adjustPvpDpGained(int points, int defeatedLvl, int killerLvl) { int pointsGained = points; int difference = killerLvl - defeatedLvl; // adjust by level if (difference >= 10) pointsGained = 0; else if (difference >= 0) pointsGained -= pointsGained * difference * 0.1; else if (difference <= -10) pointsGained *= 1.1; else pointsGained += pointsGained * Math.abs(difference) * 0.01; return pointsGained; } /** * Hate based on BOOST_HATE stat Now used only from skills, probably need to use for regular attack */ public static int calculateHate(Creature creature, int value) { Stat2 stat = new AdditionStat(StatEnum.BOOST_HATE, value, creature, 0.1f); return creature.getGameStats().getStat(StatEnum.BOOST_HATE, stat).getCurrent(); } public static List calculateAttackDamage(Creature attacker, SkillElement element, AttackStatus status, CalculationType... calculationTypes) { List attackResultList = new ArrayList<>(); if (AttackStatus.getBaseStatus(status) == AttackStatus.DODGE || AttackStatus.getBaseStatus(status) == AttackStatus.RESIST) { attackResultList.add(new AttackResult(0, AttackStatus.getBaseStatus(status))); return attackResultList; } Stat2 mainHandAttack; Stat2 offHandAttack = null; HitType hitType = HitType.PHHIT; if (element == SkillElement.NONE) { mainHandAttack = attacker.getGameStats().getMainHandPAttack(calculationTypes); if (attacker instanceof Player p) offHandAttack = p.getGameStats().getOffHandPAttack(calculationTypes); } else { hitType = HitType.MAHIT; mainHandAttack = attacker.getGameStats().getMainHandMAttack(calculationTypes); if (attacker instanceof Player p) offHandAttack = p.getGameStats().getOffHandMAttack(calculationTypes); } if (attacker instanceof Player p) { Equipment equipment = p.getEquipment(); Item mainHandWeapon = equipment.getMainHandWeapon(); if (mainHandWeapon != null) { Item offHandWeapon = equipment.getOffHandWeapon(); WeaponStats mainWeaponStats = mainHandWeapon.getItemTemplate().getWeaponStats(); WeaponStats offWeaponStats = (offHandWeapon == null || offHandWeapon.getItemTemplate().getItemSubType() == ItemSubType.SHIELD) ? null : offHandWeapon.getItemTemplate().getWeaponStats(); if (mainWeaponStats != null) { float mainHandDamage = mainHandAttack.getExactCurrent(); float offHandDamage = offHandAttack.getExactCurrent(); if (ArrayUtils.contains(calculationTypes, CalculationType.SKILL)) { // 80% of damage is added on retail if (offWeaponStats != null) { float totalBaseDamage = (offHandAttack.getExactBaseWithoutBaseRate() * p.getGameStats().getSkillEfficiency() + mainHandAttack.getExactBaseWithoutBaseRate()) * 0.8f; mainHandDamage = (mainHandAttack.getExactCurrentWithoutFixedBonus() + totalBaseDamage * offHandAttack.getFixedBonusRate()) * 0.8f; offHandDamage = (offHandAttack.getExactCurrentWithoutFixedBonus() + totalBaseDamage * mainHandAttack.getFixedBonusRate()) * 0.8f * p.getGameStats().getSkillEfficiency(); } } else { if (Rnd.nextInt(1000) >= p.getGameStats().getMaxDamageChance()) { offHandDamage *= p.getGameStats().getMinDamageRatio(); if (offHandDamage <= 0 && offWeaponStats != null) { offHandDamage = 1; } } } attackResultList.add(new AttackResult(mainHandDamage, status, hitType)); if (offWeaponStats != null) attackResultList.add(new AttackResult(offHandDamage, AttackStatus.getOffHandStats(status), hitType)); } } else { // Attack without weapon // "no weapon" damage has a power of 70, whereas weapons have their own power // TODO: parse values, but for now we can ignore it since most player weapons have a power of 100 float damage = Rnd.get(16, 20) * (1 + ((p.getGameStats().getPower().getCurrent() - 100) / 100f * 70f) / 100f) + mainHandAttack.getBonus(); attackResultList.add(new AttackResult(damage, status, hitType)); } } else { int val = attacker instanceof Homing ? 100 : Rnd.get(80, 120); attackResultList.add(new AttackResult(mainHandAttack.getCurrent() * val / 100f, status, hitType)); } return attackResultList; } /** * elemental resistance, 145 = 10% magical damage reduction (cap at +-1150) * @return damage reduced by elemental resistance */ private static float reduceDamageByElementalResistance(Creature attacked, SkillElement element, float damage) { return damage * (1 - getMovementModifier(attacked, SkillElement.getResistanceForElement(element), attacked.getGameStats().getMagicalDefenseFor(element))/ 1450f); } public static float calculateMagicalSkillDamage(Creature speller, Creature target, float baseDamage, int bonus, SkillElement element, boolean useMagicBoost, boolean useKnowledge) { CreatureGameStats sgs = speller.getGameStats(); CreatureGameStats tgs = target.getGameStats(); float magicBoost = useMagicBoost ? sgs.getMBoost().getCurrent() : 0; magicBoost -= speller instanceof Trap ? 0 : tgs.getMBResist().getCurrent(); magicBoost = (int) Math.max(0, limit(StatEnum.BOOST_MAGICAL_SKILL, magicBoost)); float knowledge = useKnowledge ? sgs.getKnowledge().getCurrent() : 100; // this line might be wrong now float damage = baseDamage * (1 + (magicBoost / (knowledge * 10))); damage = sgs.getStat(StatEnum.BOOST_SPELL_ATTACK, (int) damage).getCurrent(); // add bonus damage damage += bonus; if (element != SkillElement.NONE) { damage = reduceDamageByElementalResistance(target, element, damage); // damage is reduced by 100 per 1000 mdef damage -= target.getGameStats().getMDef().getCurrent()/10f; } if (damage < 0) { damage = 0; } else if (speller instanceof Npc && !(speller instanceof SummonedObject)) { int rnd = (int) (damage * 0.08f); damage += Rnd.get(-rnd, rnd); } return damage; } /** * Calculates MAGICAL CRITICAL chance */ public static boolean calculateMagicalCriticalRate(Creature attacker, Creature attacked, int criticalProb, boolean applyMcrit) { if (attacker instanceof Servant || attacker instanceof Homing || !applyMcrit) return false; float critical = attacker.getGameStats().getMCritical().getCurrent() - attacked.getGameStats().getMCR().getCurrent(); // add critical Prob if (criticalProb != 100) { if (critical <= 0) critical = 1; critical *= criticalProb / 100f; } return Rnd.nextInt(1000) < limit(StatEnum.MAGICAL_CRITICAL, critical); } public static int calculateRatingMultiplier(NpcRating npcRating) { // FIXME: to correct formula, have any reference? switch (npcRating) { case JUNK: case NORMAL: return 2; case ELITE: return 3; case HERO: return 4; case LEGENDARY: return 5; default: return 1; } } public static int getApNpcRating(NpcRating npcRating) { switch (npcRating) { case JUNK: return 1; case NORMAL: return 2; case ELITE: return 4; case HERO: return 35;// need check case LEGENDARY: return 2500;// need check default: return 1; } } /** * @return adjusted damage according to PVE or PVP modifiers */ public static float adjustDamageByPvpOrPveModifiers(Creature attacker, Creature target, float baseDamage, int pvpDamage, boolean useTemplateDmg, SkillElement element) { float attackBonus = 1; float defenseBonus = 1; float damage = baseDamage; if (attacker.isPvpTarget(target)) { if (pvpDamage > 0) damage *= pvpDamage * 0.01f; damage *= 0.42f; // PVP modifier 42%, last checked on NA (4.9) 19.03.2016 if (!useTemplateDmg) { if (attacker.getRace() != target.getRace() && !attacker.isInInstance()) damage *= Influence.getInstance().getPvpRaceBonus(attacker.getRace()); attackBonus = attacker.getGameStats().getStat(StatEnum.PVP_ATTACK_RATIO, 0).getCurrent() * 0.001f; defenseBonus = target.getGameStats().getStat(StatEnum.PVP_DEFEND_RATIO, 0).getCurrent() * 0.001f; switch (element) { case NONE: attackBonus += attacker.getGameStats().getStat(StatEnum.PVP_ATTACK_RATIO_PHYSICAL, 0).getCurrent() * 0.001f; defenseBonus += target.getGameStats().getStat(StatEnum.PVP_DEFEND_RATIO_PHYSICAL, 0).getCurrent() * 0.001f; break; default: attackBonus += attacker.getGameStats().getStat(StatEnum.PVP_ATTACK_RATIO_MAGICAL, 0).getCurrent() * 0.001f; defenseBonus += target.getGameStats().getStat(StatEnum.PVP_DEFEND_RATIO_MAGICAL, 0).getCurrent() * 0.001f; } } } else if (!useTemplateDmg) { if (attacker instanceof Player) { int levelDiff = target.getLevel() - attacker.getLevel(); // npcs dmg is not reduced because of the level difference GF (4.9) 23.04.2016 damage *= (1f - getNpcLevelDiffMod(levelDiff, 0)); } attackBonus = attacker.getGameStats().getStat(StatEnum.PVE_ATTACK_RATIO, 0).getCurrent() * 0.001f; defenseBonus = target.getGameStats().getStat(StatEnum.PVE_DEFEND_RATIO, 0).getCurrent() * 0.001f; switch (element) { case NONE: attackBonus += attacker.getGameStats().getStat(StatEnum.PVE_ATTACK_RATIO_PHYSICAL, 0).getCurrent() * 0.001f; defenseBonus += target.getGameStats().getStat(StatEnum.PVE_DEFEND_RATIO_PHYSICAL, 0).getCurrent() * 0.001f; break; default: attackBonus += attacker.getGameStats().getStat(StatEnum.PVE_ATTACK_RATIO_MAGICAL, 0).getCurrent() * 0.001f; defenseBonus += target.getGameStats().getStat(StatEnum.PVE_DEFEND_RATIO_MAGICAL, 0).getCurrent() * 0.001f; } } return damage + (damage * attackBonus) - (damage * defenseBonus); } /** * Must be called only once since this method triggers observe controller checks with side effects (e.g. consume one always dodge effect activation) */ public static boolean checkIsDodgedHit(Creature attacker, Creature attacked, int accMod) { // check if attacker is blinded if (attacker.getObserveController().checkAttackerStatus(AttackStatus.DODGE)) return true; // check always dodge if (attacked.getObserveController().checkAttackStatus(AttackStatus.DODGE)) return true; float accuracy = attacker.getGameStats().getMainHandPAccuracy().getCurrent() + accMod; float dodge = attacked.getGameStats().getEvasion().getBonus() + getMovementModifier(attacked, StatEnum.EVASION, attacked.getGameStats().getEvasion().getBase()); float dodgeRate = dodge - accuracy; if (attacked instanceof Npc npc) { // static npcs never dodge if (npc.hasStatic()) return false; int levelDiff = attacked.getLevel() - attacker.getLevel(); dodgeRate *= 1 + getNpcLevelDiffMod(levelDiff, 0); } return Rnd.nextInt(1000) < limit(StatEnum.EVASION, dodgeRate); } /** * Must be called only once since this method triggers observe controller checks with side effects (e.g. consume one always parry effect activation) */ public static boolean checkIsParriedHit(Creature attacker, Creature attacked, int accMod) { // check always parry if (attacked.getObserveController().checkAttackStatus(AttackStatus.PARRY)) return true; float accuracy = attacker.getGameStats().getMainHandPAccuracy().getCurrent() + accMod; float parry = attacked.getGameStats().getParry().getBonus() + getMovementModifier(attacked, StatEnum.PARRY, attacked.getGameStats().getParry().getBase()); return Rnd.nextInt(1000) < limit(StatEnum.PARRY, parry - accuracy); } /** * Must be called only once since this method triggers observe controller checks with side effects (e.g. consume one always block effect activation) */ public static boolean checkIsBlockedHit(Creature attacker, Creature attacked, int accMod) { // check always block if (attacked.getObserveController().checkAttackStatus(AttackStatus.BLOCK)) return true; float accuracy = attacker.getGameStats().getMainHandPAccuracy().getCurrent() + accMod; float block = attacked.getGameStats().getBlock().getBonus() + getMovementModifier(attacked, StatEnum.BLOCK, attacked.getGameStats().getBlock().getBase()); return Rnd.nextInt(1000) < limit(StatEnum.BLOCK, block - accuracy); } /** * http://www.wolframalpha.com/input/?i=-0.000126341+x%5E2%2B0.184411+x-13.7738 * https://docs.google.com/spreadsheet/ccc?key=0AqxBGNJV9RrzdGNjbEhQNHN3S3M5bUVfUVQxRkVIT3c&hl=en_US#gid=0 * https://docs.google.com/spreadsheets/d/1QEET5QAnxqxgT2T82g80C_9yH2D5iFos2TadR_UqPQs/edit#gid=1008537650 */ public static boolean checkIsPhysicalCriticalHit(Creature attacker, Creature attacked, boolean isMainHand, int criticalProb, boolean isSkill) { if (attacker instanceof Servant || attacker instanceof Homing) return false; float criticalRate; if (attacker instanceof Player && !isMainHand) criticalRate = ((PlayerGameStats) attacker.getGameStats()).getOffHandPCritical().getCurrent(); else criticalRate = attacker.getGameStats().getMainHandPCritical().getCurrent(); // check one time boost skill critical AttackerCriticalStatus acStatus = attacker.getObserveController().checkAttackerCriticalStatus(AttackStatus.CRITICAL, isSkill); if (acStatus.isResult()) { if (acStatus.isPercent()) criticalRate *= (1 + acStatus.getValue() / 100); else return Rnd.nextInt(1000) < acStatus.getValue(); } criticalRate -= attacked.getGameStats().getPCR().getCurrent(); // add critical Prob if (criticalProb != 100 && criticalRate > 0) { criticalRate *= criticalProb / 100f; } return Rnd.nextInt(1000) < limit(StatEnum.PHYSICAL_CRITICAL, criticalRate); } public static int calculateMagicalResistRate(Creature attacker, Creature attacked, int accMod, SkillElement element) { if (attacked.getObserveController().checkAttackStatus(AttackStatus.RESIST)) return 1000; if (element != SkillElement.NONE && attacked instanceof Summon summon && element == summon.getAlwaysResistElement()) return 1000; int levelDiff = attacked.getLevel() - attacker.getLevel(); int mResi = attacked.getGameStats().getMResist().getCurrent(); int resistRate = mResi - attacker.getGameStats().getMAccuracy().getCurrent() - accMod; if (mResi > 0 && levelDiff > 4) // only apply if creature has mres > 0 (to keep effect of AI#modifyOwnerStat) resistRate += (levelDiff - 4) * 100; if (attacker instanceof Player && attacked instanceof Player) // checked on retail: only applies to PvP return Math.min(500, resistRate); return (int) limit(StatEnum.MAGICAL_RESIST, resistRate); } public static int calculateFallDamage(Player player, float distance) { int fallDamage = 0; if (distance >= FallDamageConfig.MAXIMUM_DISTANCE_DAMAGE) { fallDamage = player.getLifeStats().getCurrentHp(); } else if (distance >= FallDamageConfig.MINIMUM_DISTANCE_DAMAGE) { float dmgPerMeter = player.getLifeStats().getMaxHp() * FallDamageConfig.FALL_DAMAGE_PERCENTAGE / 100f; fallDamage = (int) (distance * dmgPerMeter); } return fallDamage; } public static float getMovementModifier(Creature creature, StatEnum stat, float value) { if (!(creature instanceof Player) || stat == null) return value; Player player = (Player) creature; int h = player.getMoveController().getMovementHeading(); if (h < 0) return value; // 7 0 1 // \ | / // 6- -2 // / | \ // 5 4 3 switch (h) { case 7: case 0: case 1: switch (stat) { case WATER_RESISTANCE: case WIND_RESISTANCE: case FIRE_RESISTANCE: case EARTH_RESISTANCE: case ELEMENTAL_RESISTANCE_DARK: case ELEMENTAL_RESISTANCE_LIGHT: case PHYSICAL_DEFENSE: return value * 0.8f; } break; case 6: case 2: switch (stat) { case EVASION: return value + 300; case SPEED: return value * 0.8f; } break; case 5: case 4: case 3: switch (stat) { case PARRY: case BLOCK: return value + 500; case SPEED: return value * 0.6f; } break; } return value; } public static float adjustDamageByMovementModifier(Creature creature, float value) { if (!(creature instanceof Player)) return value; int h = ((Player) creature).getMoveController().getMovementHeading(); if (h < 0) return value; switch (h) { case 7: case 0: case 1: value *= 1.1f; break; case 6: case 2: value *= 0.8f; // correct? it's only 30% according to https://web.archive.org/web/20170429204823/gameguide.na.aiononline.com/aion/Combat break; case 5: case 4: case 3: value *= 0.8f; // correct? it's only 30% according to https://web.archive.org/web/20170429204823/gameguide.na.aiononline.com/aion/Combat break; } return value; } private static float getNpcLevelDiffMod(int levelDiff, int base) { switch (levelDiff) { case 3: return 0.1f; case 4: return 0.2f; case 5: return 0.3f; case 6: return 0.4f; case 7: return 0.5f; case 8: return 0.6f; case 9: return 0.7f; default: if (levelDiff > 9) return 0.8f; } return base; } /** * @return the smaller of {@code value} and {@code differenceLimit} for this StatEnum */ public static float limit(StatEnum statEnum, float value) { return Math.min(StatCapUtil.getDifferenceLimit(statEnum), value); } }