using System; using System.Linq; using ACE.Common.Extensions; using ACE.DatLoader; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Server.Entity.Actions; using ACE.Server.Managers; using ACE.Server.Network.GameMessages.Messages; namespace ACE.Server.WorldObjects { partial class Player { /// /// A player earns XP through natural progression, ie. kills and quests completed /// /// The amount of XP being added /// The source of XP being added /// True if this XP can be shared with Fellowship public void EarnXP(long amount, XpType xpType, ShareType shareType = ShareType.All) { //Console.WriteLine($"{Name}.EarnXP({amount}, {sharable}, {fixedAmount})"); // apply xp modifiers. Quest XP is multiplicative with general XP modification var questModifier = PropertyManager.GetDouble("quest_xp_modifier").Item; var modifier = PropertyManager.GetDouble("xp_modifier").Item; if (xpType == XpType.Quest) modifier *= questModifier; // should this be passed upstream to fellowship / allegiance? var enchantment = GetXPAndLuminanceModifier(xpType); var m_amount = (long)Math.Round(amount * enchantment * modifier); if (m_amount < 0) { log.Warn($"{Name}.EarnXP({amount}, {shareType})"); log.Warn($"modifier: {modifier}, enchantment: {enchantment}, m_amount: {m_amount}"); return; } GrantXP(m_amount, xpType, shareType); } /// /// Directly grants XP to the player, without the XP modifier /// /// The amount of XP to grant to the player /// The source of the XP being granted /// If TRUE, this XP can be shared with fellowship members public void GrantXP(long amount, XpType xpType, ShareType shareType = ShareType.All) { if (IsOlthoiPlayer) { if (HasVitae) UpdateXpVitae(amount); return; } if (Fellowship != null && Fellowship.ShareXP && shareType.HasFlag(ShareType.Fellowship)) { // this will divy up the XP, and re-call this function // with ShareType.Fellowship removed Fellowship.SplitXp((ulong)amount, xpType, shareType, this); return; } // Make sure UpdateXpAndLevel is done on this players thread EnqueueAction(new ActionEventDelegate(() => UpdateXpAndLevel(amount, xpType))); // for passing XP up the allegiance chain, // this function is only called at the very beginning, to start the process. if (shareType.HasFlag(ShareType.Allegiance)) UpdateXpAllegiance(amount); // only certain types of XP are granted to items if (xpType == XpType.Kill || xpType == XpType.Quest) GrantItemXP(amount); } /// /// Adds XP to a player's total XP, handles triggers (vitae, level up) /// private void UpdateXpAndLevel(long amount, XpType xpType) { // until we are max level we must make sure that we send var xpTable = DatManager.PortalDat.XpTable; var maxLevel = GetMaxLevel(); var maxLevelXp = xpTable.CharacterLevelXPList.Last(); if (Level != maxLevel) { var addAmount = amount; var amountLeftToEnd = (long)maxLevelXp - TotalExperience ?? 0; if (amount > amountLeftToEnd) addAmount = amountLeftToEnd; AvailableExperience += addAmount; TotalExperience += addAmount; var xpTotalUpdate = new GameMessagePrivateUpdatePropertyInt64(this, PropertyInt64.TotalExperience, TotalExperience ?? 0); var xpAvailUpdate = new GameMessagePrivateUpdatePropertyInt64(this, PropertyInt64.AvailableExperience, AvailableExperience ?? 0); Session.Network.EnqueueSend(xpTotalUpdate, xpAvailUpdate); CheckForLevelup(); } if (xpType == XpType.Quest) Session.Network.EnqueueSend(new GameMessageSystemChat($"You've earned {amount:N0} experience.", ChatMessageType.Broadcast)); if (HasVitae && xpType != XpType.Allegiance) UpdateXpVitae(amount); } /// /// Optionally passes XP up the Allegiance tree /// private void UpdateXpAllegiance(long amount) { if (!HasAllegiance) return; AllegianceManager.PassXP(AllegianceNode, (ulong)amount, true); } /// /// Handles updating the vitae penalty through earned XP /// /// The amount of XP to apply to the vitae penalty private void UpdateXpVitae(long amount) { var vitae = EnchantmentManager.GetVitae(); if (vitae == null) { log.Error($"{Name}.UpdateXpVitae({amount}) vitae null, likely due to cross-thread operation or corrupt EnchantmentManager cache. Please report this."); log.Error(Environment.StackTrace); return; } var vitaePenalty = vitae.StatModValue; var startPenalty = vitaePenalty; var maxPool = (int)VitaeCPPoolThreshold(vitaePenalty, DeathLevel.Value); var curPool = VitaeCpPool + amount; while (curPool >= maxPool) { curPool -= maxPool; vitaePenalty = EnchantmentManager.ReduceVitae(); if (vitaePenalty == 1.0f) break; maxPool = (int)VitaeCPPoolThreshold(vitaePenalty, DeathLevel.Value); } VitaeCpPool = (int)curPool; Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.VitaeCpPool, VitaeCpPool.Value)); if (vitaePenalty != startPenalty) { Session.Network.EnqueueSend(new GameMessageSystemChat("Your experience has reduced your Vitae penalty!", ChatMessageType.Magic)); EnchantmentManager.SendUpdateVitae(); } if (vitaePenalty.EpsilonEquals(1.0f) || vitaePenalty > 1.0f) { var actionChain = new ActionChain(); actionChain.AddDelaySeconds(2.0f); actionChain.AddAction(this, () => { var vitae = EnchantmentManager.GetVitae(); if (vitae != null) { var curPenalty = vitae.StatModValue; if (curPenalty.EpsilonEquals(1.0f) || curPenalty > 1.0f) EnchantmentManager.RemoveVitae(); } }); actionChain.EnqueueChain(); } } /// /// Returns the maximum possible character level /// public static uint GetMaxLevel() { return (uint)DatManager.PortalDat.XpTable.CharacterLevelXPList.Count - 1; } /// /// Returns TRUE if player >= MaxLevel /// public bool IsMaxLevel => Level >= GetMaxLevel(); /// /// Returns the remaining XP required to reach a level /// public long? GetRemainingXP(uint level) { var maxLevel = GetMaxLevel(); if (level < 1 || level > maxLevel) return null; var levelTotalXP = DatManager.PortalDat.XpTable.CharacterLevelXPList[(int)level]; return (long)levelTotalXP - TotalExperience.Value; } /// /// Returns the remaining XP required to the next level /// public ulong GetRemainingXP() { var maxLevel = GetMaxLevel(); if (Level >= maxLevel) return 0; var nextLevelTotalXP = DatManager.PortalDat.XpTable.CharacterLevelXPList[Level.Value + 1]; return nextLevelTotalXP - (ulong)TotalExperience.Value; } /// /// Returns the total XP required to reach a level /// public static ulong GetTotalXP(int level) { var maxLevel = GetMaxLevel(); if (level < 0 || level > maxLevel) return 0; return DatManager.PortalDat.XpTable.CharacterLevelXPList[level]; } /// /// Returns the total amount of XP required for a player reach max level /// public static long MaxLevelXP { get { var xpTable = DatManager.PortalDat.XpTable.CharacterLevelXPList; return (long)xpTable[xpTable.Count - 1]; } } /// /// Returns the XP required to go from level A to level B /// public ulong GetXPBetweenLevels(int levelA, int levelB) { // special case for max level var maxLevel = (int)GetMaxLevel(); levelA = Math.Clamp(levelA, 1, maxLevel - 1); levelB = Math.Clamp(levelB, 1, maxLevel); var levelA_totalXP = DatManager.PortalDat.XpTable.CharacterLevelXPList[levelA]; var levelB_totalXP = DatManager.PortalDat.XpTable.CharacterLevelXPList[levelB]; return levelB_totalXP - levelA_totalXP; } public ulong GetXPToNextLevel(int level) { return GetXPBetweenLevels(level, level + 1); } /// /// Determines if the player has advanced a level /// private void CheckForLevelup() { var xpTable = DatManager.PortalDat.XpTable; var maxLevel = GetMaxLevel(); if (Level >= maxLevel) return; var startingLevel = Level; bool creditEarned = false; // increases until the correct level is found while ((ulong)(TotalExperience ?? 0) >= xpTable.CharacterLevelXPList[(Level ?? 0) + 1]) { Level++; // increase the skill credits if the chart allows this level to grant a credit if (xpTable.CharacterLevelSkillCreditList[Level ?? 0] > 0) { AvailableSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[Level ?? 0]; TotalSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[Level ?? 0]; creditEarned = true; } // break if we reach max if (Level == maxLevel) { PlayParticleEffect(PlayScript.WeddingBliss, Guid); break; } } if (Level > startingLevel) { var message = (Level == maxLevel) ? $"You have reached the maximum level of {Level}!" : $"You are now level {Level}!"; message += (AvailableSkillCredits > 0) ? $"\nYou have {AvailableExperience:#,###0} experience points and {AvailableSkillCredits} skill credits available to raise skills and attributes." : $"\nYou have {AvailableExperience:#,###0} experience points available to raise skills and attributes."; var levelUp = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.Level, Level ?? 1); var currentCredits = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.AvailableSkillCredits, AvailableSkillCredits ?? 0); if (Level != maxLevel && !creditEarned) { var nextLevelWithCredits = 0; for (int i = (Level ?? 0) + 1; i <= maxLevel; i++) { if (xpTable.CharacterLevelSkillCreditList[i] > 0) { nextLevelWithCredits = i; break; } } message += $"\nYou will earn another skill credit at level {nextLevelWithCredits}."; } if (Fellowship != null) Fellowship.OnFellowLevelUp(this); if (AllegianceNode != null) AllegianceNode.OnLevelUp(); Session.Network.EnqueueSend(levelUp); SetMaxVitals(); // play level up effect PlayParticleEffect(PlayScript.LevelUp, Guid); Session.Network.EnqueueSend(new GameMessageSystemChat(message, ChatMessageType.Advancement), currentCredits); } } /// /// Spends the amount of XP specified, deducting it from available experience /// public bool SpendXP(long amount, bool sendNetworkUpdate = true) { if (amount > AvailableExperience) return false; AvailableExperience -= amount; if (sendNetworkUpdate) Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt64(this, PropertyInt64.AvailableExperience, AvailableExperience ?? 0)); return true; } /// /// Tries to spend all of the players Xp into Attributes, Vitals and Skills /// public void SpendAllXp(bool sendNetworkUpdate = true) { SpendAllAvailableAttributeXp(Strength, sendNetworkUpdate); SpendAllAvailableAttributeXp(Endurance, sendNetworkUpdate); SpendAllAvailableAttributeXp(Coordination, sendNetworkUpdate); SpendAllAvailableAttributeXp(Quickness, sendNetworkUpdate); SpendAllAvailableAttributeXp(Focus, sendNetworkUpdate); SpendAllAvailableAttributeXp(Self, sendNetworkUpdate); SpendAllAvailableVitalXp(Health, sendNetworkUpdate); SpendAllAvailableVitalXp(Stamina, sendNetworkUpdate); SpendAllAvailableVitalXp(Mana, sendNetworkUpdate); foreach (var skill in Skills) { if (skill.Value.AdvancementClass >= SkillAdvancementClass.Trained) SpendAllAvailableSkillXp(skill.Value, sendNetworkUpdate); } } /// /// Gives available XP of the amount specified, without increasing total XP /// public void RefundXP(long amount) { AvailableExperience += amount; var xpUpdate = new GameMessagePrivateUpdatePropertyInt64(this, PropertyInt64.AvailableExperience, AvailableExperience ?? 0); Session.Network.EnqueueSend(xpUpdate); } public void HandleMissingXp() { var verifyXp = GetProperty(PropertyInt64.VerifyXp) ?? 0; if (verifyXp == 0) return; var actionChain = new ActionChain(); actionChain.AddDelaySeconds(5.0f); actionChain.AddAction(this, () => { var xpType = verifyXp > 0 ? "unassigned experience" : "experience points"; var msg = $"This character was missing some {xpType} --\nYou have gained an additional {Math.Abs(verifyXp).ToString("N0")} {xpType}!"; Session.Network.EnqueueSend(new GameMessageSystemChat(msg, ChatMessageType.Broadcast)); if (verifyXp < 0) { // add to character's total XP TotalExperience -= verifyXp; CheckForLevelup(); } RemoveProperty(PropertyInt64.VerifyXp); }); actionChain.EnqueueChain(); } /// /// Returns the total amount of XP required to go from vitae to vitae + 0.01 /// /// The current player life force, ie. 0.95f vitae = 5% penalty /// The player DeathLevel, their level on last death private double VitaeCPPoolThreshold(float vitae, int level) { return (Math.Pow(level, 2.5) * 2.5 + 20.0) * Math.Pow(vitae, 5.0) + 0.5; } /// /// Raise the available XP by a percentage of the current level XP or a maximum /// public void GrantLevelProportionalXp(double percent, long min, long max) { var nextLevelXP = GetXPBetweenLevels(Level.Value, Level.Value + 1); var scaledXP = (long)Math.Round(nextLevelXP * percent); if (max > 0) scaledXP = Math.Min(scaledXP, max); if (min > 0) scaledXP = Math.Max(scaledXP, min); // apply xp modifiers? EarnXP(scaledXP, XpType.Quest, ShareType.Allegiance); } /// /// The player earns XP for items that can be leveled up /// by killing creatures and completing quests, /// while those items are equipped. /// public void GrantItemXP(long amount) { foreach (var item in EquippedObjects.Values.Where(i => i.HasItemLevel)) GrantItemXP(item, amount); } public void GrantItemXP(WorldObject item, long amount) { var prevItemLevel = item.ItemLevel.Value; var addItemXP = item.AddItemXP(amount); if (addItemXP > 0) Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt64(item, PropertyInt64.ItemTotalXp, item.ItemTotalXp.Value)); // handle item leveling up var newItemLevel = item.ItemLevel.Value; if (newItemLevel > prevItemLevel) { OnItemLevelUp(item, prevItemLevel); var actionChain = new ActionChain(); actionChain.AddAction(this, () => { var msg = $"Your {item.Name} has increased in power to level {newItemLevel}!"; Session.Network.EnqueueSend(new GameMessageSystemChat(msg, ChatMessageType.Broadcast)); EnqueueBroadcast(new GameMessageScript(Guid, PlayScript.AetheriaLevelUp)); }); actionChain.EnqueueChain(); } } /// /// Returns the multiplier to XP and Luminance from Trinkets and Augmentations /// public float GetXPAndLuminanceModifier(XpType xpType) { var enchantmentBonus = EnchantmentManager.GetXPBonus(); var augBonus = 0.0f; if (xpType == XpType.Kill && AugmentationBonusXp > 0) augBonus = AugmentationBonusXp * 0.05f; var modifier = 1.0f + enchantmentBonus + augBonus; //Console.WriteLine($"XPAndLuminanceModifier: {modifier}"); return modifier; } } }