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;
}
}
}