using System.Runtime.CompilerServices; using ACE.DatLoader; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Server.Managers; using ACE.Server.Network; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages.Messages; using ACE.Server.WorldObjects.Entity; namespace ACE.Server.WorldObjects { partial class Player { /// /// Handles the GameAction 0x44 - RaiseVital network message from client /// public bool HandleActionRaiseVital(PropertyAttribute2nd vital, uint amount) { if (!Vitals.TryGetValue(vital, out var creatureVital)) { log.Warn($"{Name}.HandleActionRaiseVital({vital}, {amount}) - invalid vital"); return false; } if (amount > AvailableExperience) { // there is a client bug for vitals only, // where the client will enable the button to raise a vital by 10 // if the player only has enough AvailableExperience to raise it by 1 ChatPacket.SendServerMessage(Session, $"Your attempt to raise {vital.ToSentence()} has failed.", ChatMessageType.Broadcast); log.Warn($"{Name}.HandleActionRaiseVital({vital}, {amount}) - amount > AvailableExperience ({AvailableExperience})"); return false; } var prevRank = creatureVital.Ranks; if (!SpendVitalXp(creatureVital, amount)) return false; Session.Network.EnqueueSend(new GameMessagePrivateUpdateVital(this, creatureVital)); if (prevRank != creatureVital.Ranks) { // checks if max rank is achieved and plays fireworks w/ special text var suffix = ""; if (creatureVital.IsMaxRank) { // fireworks PlayParticleEffect(PlayScript.WeddingBliss, Guid); suffix = $" and has reached its upper limit"; } var sound = new GameMessageSound(Guid, Sound.RaiseTrait); var msg = new GameMessageSystemChat($"Your base {vital.ToSentence()} is now {creatureVital.Base}{suffix}!", ChatMessageType.Advancement); Session.Network.EnqueueSend(sound, msg); } return true; } private bool SpendVitalXp(CreatureVital creatureVital, uint amount, bool sendNetworkUpdate = true) { // ensure vital is not already max rank if (creatureVital.IsMaxRank) { log.Warn($"{Name}.SpendVitalXp({creatureVital.Vital}, {amount}) - player tried to raise vital beyond max rank"); return false; } // the client should already handle this naturally, // but ensure player can't spent xp beyond the max rank var amountToEnd = creatureVital.ExperienceLeft; if (amount > amountToEnd) { log.Warn($"{Name}.SpendVitalXp({creatureVital.Vital}, {amount}) - player tried to raise vital beyond {amountToEnd} experience"); return false; // returning error here, instead of setting amount to amountToEnd } // everything looks good at this point, // spend xp on vital if (!SpendXP(amount, sendNetworkUpdate)) { log.Warn($"{Name}.SpendVitalXp({creatureVital.Vital}, {amount}) - SpendXP failed"); return false; } creatureVital.ExperienceSpent += amount; // calculate new rank creatureVital.Ranks = (ushort)CalcVitalRank(creatureVital.ExperienceSpent); return true; } public void SpendAllAvailableVitalXp(CreatureVital creatureVital, bool sendNetworkUpdate = true) { var amountRemaining = creatureVital.ExperienceLeft; if (amountRemaining > AvailableExperience) amountRemaining = (uint)AvailableExperience; SpendVitalXp(creatureVital, amountRemaining, sendNetworkUpdate); } public override void SetMaxVitals() { base.SetMaxVitals(); var health = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, Health.Current); var stamina = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Stamina, Stamina.Current); var mana = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Mana, Mana.Current); Session.Network.EnqueueSend(health, stamina, mana); if (Fellowship != null) FellowVitalUpdate = true; } /// /// Sets the current vital to a new value /// /// The actual change in the vital, after clamping between 0 and MaxVital public override int UpdateVital(CreatureVital vital, int newVal) { var prevVal = vital.Current; var change = base.UpdateVital(vital, newVal); if (change == 0) return 0; Session.Network.EnqueueSend(new GameMessagePrivateUpdateAttribute2ndLevel(this, vital.ToEnum(), vital.Current)); if (Fellowship != null) FellowVitalUpdate = true; // check for exhaustion if (vital.Vital == PropertyAttribute2nd.Stamina || vital.Vital == PropertyAttribute2nd.MaxStamina) { if (newVal == 0) { OnExhausted(); } // retail was missing the 'exhausted done' automatic hook here else if (prevVal == 0 && PropertyManager.GetBool("runrate_add_hooks").Item) { HandleRunRateUpdate(); } } return change; } /// /// Called on Player heartbeat /// Sends a packet containing the latest health info for the selected target /// public void HandleTargetVitals() { var target = selectedTarget?.TryGetWorldObject() as Creature; if (target == null || target.Health.Current <= 0) return; var healthPercent = (float)target.Health.Current / target.Health.MaxValue; Session.Network.EnqueueSend(new GameEventUpdateHealth(Session, target.Guid.Full, healthPercent)); } /// /// Returns the maximum rank that can be purchased with an xp amount /// /// The amount of xp used to make the purchase public static int CalcVitalRank(uint xpAmount) { var rankXpTable = DatManager.PortalDat.XpTable.VitalXpList; for (var i = rankXpTable.Count - 1; i >= 0; i--) { var rankAmount = rankXpTable[i]; if (xpAmount >= rankAmount) return i; } return -1; } /// /// Called every ~5 secs to regenerate player vitals /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool VitalHeartBeat() { var vitalUpdate = base.VitalHeartBeat(); if (vitalUpdate && Fellowship != null) FellowVitalUpdate = true; return vitalUpdate; } /// /// Called when a player equips/dequips an item w/ GearMaxHealth /// public void HandleMaxHealthUpdate() { var gearMaxHealth = GetGearMaxHealth(); if (gearMaxHealth == 0) GearMaxHealth = null; else GearMaxHealth = gearMaxHealth; Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.GearMaxHealth, gearMaxHealth)); if (Health.Current > Health.MaxValue) Health.Current = Health.MaxValue; Session.Network.EnqueueSend(new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, Health.Current)); } } }