コード例 #1
0
        /// <summary>
        /// Updates a particular vital according to regeneration rate
        /// </summary>
        /// <param name="vital">The vital stat to update (health/stamina/mana)</param>
        public void VitalHeartBeat(CreatureVital vital)
        {
            // Current and MaxValue are properties and include overhead in getting their values. We cache them so we only hit the overhead once.
            var vitalCurrent = vital.Current;
            var vitalMax     = vital.MaxValue;

            if (vitalCurrent == vitalMax)
            {
                return;
            }

            if (vitalCurrent > vitalMax)
            {
                UpdateVital(vital, vitalMax);
                return;
            }

            if (vital.RegenRate == 0.0)
            {
                return;
            }

            // take attributes into consideration (strength, endurance)
            var attributeMod = GetAttributeMod(vital);

            // take stance into consideration (combat, crouch, sitting, sleeping)
            var stanceMod = GetStanceMod(vital);

            // take enchantments into consideration:
            // (regeneration / rejuvenation / mana renewal / etc.)
            var enchantmentMod = EnchantmentManager.GetRegenerationMod(vital);

            var augMod = 1.0f;

            if (this is Player player && player.AugmentationFasterRegen > 0)
            {
                augMod += player.AugmentationFasterRegen;
            }

            // cap rate?
            var currentTick = vital.RegenRate * attributeMod * stanceMod * enchantmentMod * augMod;

            // add in partially accumulated / rounded vitals from previous tick(s)
            var totalTick = currentTick + vital.PartialRegen;

            // accumulate partial vital rates between ticks
            var intTick = (int)totalTick;

            vital.PartialRegen = totalTick - intTick;

            if (intTick > 0)
            {
                UpdateVitalDelta(vital, intTick);
                if (vital.Vital == PropertyAttribute2nd.MaxHealth)
                {
                    DamageHistory.OnHeal((uint)intTick);
                }
            }
            //Console.WriteLine($"VitalTick({vital.Vital.ToSentence()}): attributeMod={attributeMod}, stanceMod={stanceMod}, enchantmentMod={enchantmentMod}, regenRate={vital.RegenRate}, currentTick={currentTick}, totalTick={totalTick}, accumulated={vital.PartialRegen}");
        }
コード例 #2
0
        /// <summary>
        /// gets the damage occuring in the last maxage seconds.  removes damage
        /// entries from queue older than maxage
        /// </summary>
        /// <param name="maxage">seconds to calculate damage received</param>
        /// <returns>damage received</returns>
        public static long GetRecentDamage(float maxage)
        {
            DateTime since = DateTime.UtcNow - TimeSpan.FromSeconds(maxage);

            while (DamageHistory.Any())
            {
                Damage next = DamageHistory.Peek();
                if (next.Time >= since)
                {
                    break;
                }

                DamageHistory.Dequeue();
            }

            long sum = 0;

            foreach (var q in DamageHistory)
            {
                if (SingularSettings.Debug)
                {
                    if (q.Time < since)
                    {
                        Logger.WriteDebug("GetRecentDamage: Program Error: entry {0} {1:HH:mm:ss.FFFF} older than {2:HH:mm:ss.FFFF}", q.Amount, q.Time, since);
                    }
                }
                sum += q.Amount;
            }
            return(DamageHistory.Sum(v => v.Amount));
        }
コード例 #3
0
        /// <summary>
        /// Applies some amount of damage to this monster from source
        /// </summary>
        /// <param name="source">The attacker / source of damage</param>
        /// <param name="amount">The amount of damage rounded</param>
        public virtual uint TakeDamage(WorldObject source, DamageType damageType, float amount, bool crit = false)
        {
            var tryDamage = (int)Math.Round(amount);
            var damage    = -UpdateVitalDelta(Health, -tryDamage);

            // TODO: update monster stamina?

            // source should only be null for combined DoT ticks from multiple sources
            if (source != null)
            {
                if (damage >= 0)
                {
                    DamageHistory.Add(source, damageType, (uint)damage);
                }
                else
                {
                    DamageHistory.OnHeal((uint)-damage);
                }
            }

            if (Health.Current <= 0)
            {
                OnDeath(DamageHistory.LastDamager, damageType, crit);

                Die();
            }
            return((uint)Math.Max(0, damage));
        }
コード例 #4
0
        /// <summary>
        /// Applies damages to a player from a physical damage source
        /// </summary>
        public void TakeDamage(WorldObject source, DamageType damageType, float _amount, BodyPart bodyPart, bool crit = false)
        {
            if (Invincible ?? false)
            {
                return;
            }

            // check lifestone protection
            if (UnderLifestoneProtection)
            {
                HandleLifestoneProtection();
                return;
            }

            var amount  = (uint)Math.Round(_amount);
            var percent = (float)amount / Health.MaxValue;

            // update health
            var damageTaken = (uint)-UpdateVitalDelta(Health, (int)-amount);

            DamageHistory.Add(source, damageType, damageTaken);

            // update stamina
            UpdateVitalDelta(Stamina, -1);

            if (Fellowship != null)
            {
                Fellowship.OnVitalUpdate(this);
            }

            if (Health.Current == 0)
            {
                OnDeath(source, damageType, crit);
                Die();
                return;
            }

            var damageLocation = (DamageLocation)BodyParts.Indices[bodyPart];

            // send network messages
            var creature = source as Creature;
            var hotspot  = source as Hotspot;

            if (creature != null)
            {
                var text = new GameEventDefenderNotification(Session, creature.Name, damageType, percent, amount, damageLocation, crit, AttackConditions.None);
                Session.Network.EnqueueSend(text);

                var hitSound = new GameMessageSound(Guid, GetHitSound(source, bodyPart), 1.0f);
                var splatter = new GameMessageScript(Guid, (PlayScript)Enum.Parse(typeof(PlayScript), "Splatter" + creature.GetSplatterHeight() + creature.GetSplatterDir(this)));
                EnqueueBroadcast(hitSound, splatter);
            }

            if (percent >= 0.1f)
            {
                EnqueueBroadcast(new GameMessageSound(Guid, Sound.Wound1, 1.0f));
            }
        }
コード例 #5
0
ファイル: Creature.cs プロジェクト: Zegeger/ACE
        private void SetEphemeralValues()
        {
            CombatMode    = CombatMode.NonCombat;
            DamageHistory = new DamageHistory(this);

            if (CreatureType == ACE.Entity.Enum.CreatureType.Human && !(WeenieClassId == 1 || WeenieClassId == 4))
            {
                GenerateNewFace();
            }

            if (CreatureType == ACE.Entity.Enum.CreatureType.Shadow || CreatureType == ACE.Entity.Enum.CreatureType.Simulacrum)
            {
                GenerateNewFace();
            }

            // If any of the vitals don't exist for this biota, one will be created automatically in the CreatureVital ctor
            Vitals[PropertyAttribute2nd.MaxHealth]  = new CreatureVital(this, PropertyAttribute2nd.MaxHealth);
            Vitals[PropertyAttribute2nd.MaxStamina] = new CreatureVital(this, PropertyAttribute2nd.MaxStamina);
            Vitals[PropertyAttribute2nd.MaxMana]    = new CreatureVital(this, PropertyAttribute2nd.MaxMana);

            // If any of the attributes don't exist for this biota, one will be created automatically in the CreatureAttribute ctor
            Attributes[PropertyAttribute.Strength]     = new CreatureAttribute(this, PropertyAttribute.Strength);
            Attributes[PropertyAttribute.Endurance]    = new CreatureAttribute(this, PropertyAttribute.Endurance);
            Attributes[PropertyAttribute.Coordination] = new CreatureAttribute(this, PropertyAttribute.Coordination);
            Attributes[PropertyAttribute.Quickness]    = new CreatureAttribute(this, PropertyAttribute.Quickness);
            Attributes[PropertyAttribute.Focus]        = new CreatureAttribute(this, PropertyAttribute.Focus);
            Attributes[PropertyAttribute.Self]         = new CreatureAttribute(this, PropertyAttribute.Self);

            foreach (var skillProperty in Biota.BiotaPropertiesSkill)
            {
                Skills[(Skill)skillProperty.Type] = new CreatureSkill(this, (Skill)skillProperty.Type);
            }

            if (Health.Current == 0)
            {
                Health.Current = Health.MaxValue;
            }
            if (Stamina.Current == 0)
            {
                Stamina.Current = Stamina.MaxValue;
            }
            if (Mana.Current == 0)
            {
                Mana.Current = Mana.MaxValue;
            }

            if (!(this is Player))
            {
                GenerateWieldList();
                GenerateWieldedTreasure();
            }

            Value = null; // Creatures don't have value. By setting this to null, it effectively disables the Value property. (Adding/Subtracting from null results in null)

            CurrentMotionState = new UniversalMotion(MotionStance.Invalid, new MotionItem(MotionCommand.Invalid));

            QueueNextTick();
        }
コード例 #6
0
ファイル: Player_Combat.cs プロジェクト: Plover99/ACE
        /// <summary>
        /// Applies damages to a player from a physical damage source
        /// </summary>
        public void TakeDamage(WorldObject source, DamageType damageType, float _amount, BodyPart bodyPart, bool crit = false)
        {
            if (Invincible ?? false)
            {
                return;
            }

            var amount  = (uint)Math.Round(_amount);
            var percent = (float)amount / Health.MaxValue;

            // update health
            var damageTaken = (uint)-UpdateVitalDelta(Health, (int)-amount);

            DamageHistory.Add(source, damageType, damageTaken);

            if (Health.Current == 0)
            {
                OnDeath(source, damageType, crit);
                Die();
                return;
            }

            // update stamina
            UpdateVitalDelta(Stamina, -1);

            var damageLocation = (DamageLocation)BodyParts.Indices[bodyPart];

            // send network messages
            var creature = source as Creature;
            var hotspot  = source as Hotspot;

            if (creature != null)
            {
                var text = new GameEventDefenderNotification(Session, creature.Name, damageType, percent, amount, damageLocation, crit, AttackConditions.None);
                Session.Network.EnqueueSend(text);

                var hitSound = new GameMessageSound(Guid, GetHitSound(source, bodyPart), 1.0f);
                var splatter = new GameMessageScript(Guid, (PlayScript)Enum.Parse(typeof(PlayScript), "Splatter" + creature.GetSplatterHeight() + creature.GetSplatterDir(this)));
                EnqueueBroadcast(hitSound, splatter);
            }
            else if (hotspot != null)
            {
                if (!string.IsNullOrWhiteSpace(hotspot.ActivationTalkString))
                {
                    Session.Network.EnqueueSend(new GameMessageSystemChat(hotspot.ActivationTalkString.Replace("%i", amount.ToString()), ChatMessageType.Craft));
                }
                if (!hotspot.Visibility)
                {
                    hotspot.EnqueueBroadcast(new GameMessageSound(hotspot.Guid, Sound.TriggerActivated, 1.0f));
                }
            }

            if (percent >= 0.1f)
            {
                EnqueueBroadcast(new GameMessageSound(Guid, Sound.Wound1, 1.0f));
            }
        }
コード例 #7
0
ファイル: Creature_Vitals.cs プロジェクト: shagar-zharla/ACE
        public virtual void SetMaxVitals()
        {
            var missingHealth = Health.Missing;

            Health.Current  = Health.MaxValue;
            Stamina.Current = Stamina.MaxValue;
            Mana.Current    = Mana.MaxValue;

            DamageHistory.OnHeal(missingHealth);
        }
コード例 #8
0
        /// <summary>
        /// Called when the player enters portal space after dying
        /// </summary>
        public void ThreadSafeTeleportOnDeath()
        {
            // teleport to sanctuary or best location
            var newPosition = Sanctuary ?? Instantiation ?? Location;

            WorldManager.ThreadSafeTeleport(this, newPosition, new ActionEventDelegate(() =>
            {
                // Stand back up
                SetCombatMode(CombatMode.NonCombat);

                SetLifestoneProtection();

                var teleportChain = new ActionChain();
                if (!IsLoggingOut) // If we're in the process of logging out, we skip the delay
                {
                    teleportChain.AddDelaySeconds(3.0f);
                }
                teleportChain.AddAction(this, () =>
                {
                    // currently happens while in portal space
                    var newHealth  = (uint)Math.Round(Health.MaxValue * 0.75f);
                    var newStamina = (uint)Math.Round(Stamina.MaxValue * 0.75f);
                    var newMana    = (uint)Math.Round(Mana.MaxValue * 0.75f);

                    var msgHealthUpdate  = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, newHealth);
                    var msgStaminaUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Stamina, newStamina);
                    var msgManaUpdate    = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Mana, newMana);

                    UpdateVital(Health, newHealth);
                    UpdateVital(Stamina, newStamina);
                    UpdateVital(Mana, newMana);

                    Session.Network.EnqueueSend(msgHealthUpdate, msgStaminaUpdate, msgManaUpdate);

                    // reset damage history for this player
                    DamageHistory.Reset();

                    OnHealthUpdate();

                    IsInDeathProcess = false;

                    if (IsLoggingOut)
                    {
                        LogOut_Final(true);
                    }
                });

                teleportChain.EnqueueChain();
            }));
        }
コード例 #9
0
        /// <summary>
        /// Updates a particular vital according to regeneration rate
        /// </summary>
        /// <param name="vital">The vital stat to update (health/stamina/mana)</param>
        public void VitalTick(CreatureVital vital)
        {
            if (vital.Current == vital.MaxValue)
            {
                return;
            }

            if (vital.Current > vital.MaxValue)
            {
                UpdateVital(vital, vital.MaxValue);
                return;
            }

            if (vital.RegenRate == 0.0)
            {
                return;
            }

            // take attributes into consideration (strength, endurance)
            var attributeMod = GetAttributeMod(vital);

            // take stance into consideration (combat, crouch, sitting, sleeping)
            var stanceMod = GetStanceMod(vital);

            // take enchantments into consideration:
            // (regeneration / rejuvenation / mana renewal / etc.)
            var enchantmentMod = EnchantmentManager.GetRegenerationMod(vital);

            // cap rate?
            var currentTick = vital.RegenRate * attributeMod * stanceMod * enchantmentMod;

            // add in partially accumulated / rounded vitals from previous tick(s)
            var totalTick = currentTick + vital.PartialRegen;

            // accumulate partial vital rates between ticks
            var intTick = (int)totalTick;

            vital.PartialRegen = totalTick - intTick;

            if (intTick > 0)
            {
                UpdateVitalDelta(vital, intTick);
                if (vital.Vital == PropertyAttribute2nd.MaxHealth)
                {
                    DamageHistory.OnHeal((uint)intTick);
                }
            }
            //Console.WriteLine($"VitalTick({vital.Vital.ToSentence()}): attributeMod={attributeMod}, stanceMod={stanceMod}, enchantmentMod={enchantmentMod}, regenRate={vital.RegenRate}, currentTick={currentTick}, totalTick={totalTick}, accumulated={vital.PartialRegen}");
        }
コード例 #10
0
ファイル: Creature_Tick.cs プロジェクト: justonia/ACE
        /// <summary>
        /// Called every ~5 seconds for Creatures
        /// </summary>
        public override void Heartbeat(double currentUnixTime)
        {
            var expireItems = new List <WorldObject>();

            // added where clause
            foreach (var wo in EquippedObjects.Values.Where(i => i.EnchantmentManager.HasEnchantments || i.RemainingLifespan.HasValue))
            {
                // FIXME: wo.NextHeartbeatTime is double.MaxValue here
                //if (wo.NextHeartbeatTime <= currentUnixTime)
                //wo.Heartbeat(currentUnixTime);

                // just go by parent heartbeats, only for enchantments?
                // TODO: handle players dropping / picking up items
                wo.EnchantmentManager.HeartBeat(CachedHeartbeatInterval);

                if (wo.RemainingLifespan != null)
                {
                    wo.RemainingLifespan -= (int)CachedHeartbeatInterval;

                    if (wo.RemainingLifespan <= 0)
                    {
                        expireItems.Add(wo);
                    }
                }
            }

            VitalHeartBeat();

            EmoteManager.HeartBeat();

            DamageHistory.TryPrune();

            // delete items when RemainingLifespan <= 0
            foreach (var expireItem in expireItems)
            {
                expireItem.DeleteObject(this);

                if (this is Player player)
                {
                    player.Session.Network.EnqueueSend(new GameMessageSystemChat($"Its lifespan finished, your {expireItem.Name} crumbles to dust.", ChatMessageType.Broadcast));
                }
            }

            base.Heartbeat(currentUnixTime);
        }
コード例 #11
0
        /// <summary>
        /// Applies some amount of damage to this monster from source
        /// </summary>
        /// <param name="source">The attacker / source of damage</param>
        /// <param name="amount">The amount of damage rounded</param>
        public virtual void TakeDamage(WorldObject source, DamageType damageType, float amount, bool crit = false)
        {
            var tryDamage = (uint)Math.Round(amount);
            var damage    = (uint)-UpdateVitalDelta(Health, (int)-tryDamage);

            // TODO: update monster stamina?

            // source should only be null for combined DoT ticks from multiple sources
            if (source != null)
            {
                DamageHistory.Add(source, damageType, damage);
            }

            if (Health.Current <= 0)
            {
                OnDeath();
                Die();

                // this should only probably go to the last damager
                var lastDamager = DamageHistory.LastDamager as Player;
                if (lastDamager != null)
                {
                    var deathMessage = Strings.GetDeathMessage(damageType, crit);
                    lastDamager.Session.Network.EnqueueSend(new GameMessageSystemChat(string.Format(deathMessage.Killer, Name), ChatMessageType.Broadcast));
                }

                // split xp between players in damage history?
                foreach (var kvp in DamageHistory.TotalDamage)
                {
                    var damager     = kvp.Key;
                    var totalDamage = kvp.Value;

                    var playerDamager = damager as Player;
                    if (playerDamager == null)
                    {
                        continue;
                    }

                    var damagePercent = totalDamage / Health.MaxValue;
                    var totalXP       = (XpOverride ?? 0) * damagePercent;

                    playerDamager.EarnXP((long)Math.Round(totalXP));
                }
            }
        }
コード例 #12
0
        /// <summary>
        /// gets the damage occuring in the last maxage seconds.  removes damage
        /// entries from queue older than maxage.  additionally calculates damage
        /// at another time boundary less than maxage (referred to as recent)
        /// </summary>
        /// <param name="maxage">seconds to calculate damage received</param>
        /// <param name="alldmg">damage received since maxage</param>
        /// <param name="recentage">more recent timeframe</param>
        /// <param name="recentdmg">damage since more recent timeframe</param>
        public static void GetRecentDamage(float maxage, out long alldmg, float recentage, out long recentdmg)
        {
            DateTime now         = DateTime.UtcNow;
            DateTime sinceoldest = now - TimeSpan.FromSeconds(maxage);
            DateTime sincerecent = now - TimeSpan.FromSeconds(recentage);

            recentdmg = 0;
            alldmg    = 0;

            if (DamageHistory == null)
            {
                return;
            }

            while (DamageHistory.Any())
            {
                Damage next = DamageHistory.Peek();
                if (next.Time >= sinceoldest)
                {
                    break;
                }

                DamageHistory.Dequeue();
            }

            foreach (var q in DamageHistory)
            {
                alldmg += q.Amount;
                if (q.Time < sincerecent)
                {
                    recentage += q.Amount;
                }

                if (SingularSettings.Debug)
                {
                    if (q.Time < sinceoldest)
                    {
                        Logger.WriteDebug("GetRecentDamage: Program Error: entry {0} {1:HH:mm:ss.FFFF} older than {2:HH:mm:ss.FFFF}", q.Amount, q.Time, sinceoldest);
                    }
                }
            }

            return;
        }
コード例 #13
0
        public async Task <ActionResult> InsertDamageHistory([FromBody] DamageHistory dto)
        {
            if (dto == null)
            {
                return(BadRequest());
            }

            try
            {
                //Add and save the db context
                _context.DamageHistory.Add(dto);
                await _context.SaveChangesAsync();

                return(Ok());
            }
            catch
            {
                return(BadRequest());
            }
        }
コード例 #14
0
        /// <summary>
        /// Applies some amount of damage to this monster from source
        /// </summary>
        /// <param name="source">The attacker / source of damage</param>
        /// <param name="amount">The amount of damage rounded</param>
        public virtual void TakeDamage(WorldObject source, float amount, bool crit = false)
        {
            var tryDamage = (uint)Math.Round(amount);
            var damage    = (uint)-UpdateVitalDelta(Health, (int)-tryDamage);

            DamageHistory.Add(source, damage);

            if (Health.Current <= 0)
            {
                OnDeath();
                Die();

                var player = source as Player;
                if (player != null)
                {
                    var deathMessage = GetDeathMessage(source, crit);
                    player.Session.Network.EnqueueSend(new GameMessageSystemChat(string.Format(deathMessage, Name), ChatMessageType.Broadcast));
                    player.EarnXP((long)XpOverride);
                }
            }
        }
コード例 #15
0
        /// <summary>
        /// Called every ~5 seconds for Creatures
        /// </summary>
        public override void Heartbeat(double currentUnixTime)
        {
            // added where clause
            foreach (var wo in EquippedObjects.Values.Where(i => i.EnchantmentManager.HasEnchantments))
            {
                // FIXME: wo.NextHeartbeatTime is double.MaxValue here
                //if (wo.NextHeartbeatTime <= currentUnixTime)
                //wo.Heartbeat(currentUnixTime);

                // just go by parent heartbeats, only for enchantments?
                // TODO: handle players dropping / picking up items
                wo.EnchantmentManager.HeartBeat(HeartbeatInterval);
            }

            VitalHeartBeat();

            EmoteManager.HeartBeat();

            DamageHistory.TryPrune();

            base.Heartbeat(currentUnixTime);
        }
コード例 #16
0
ファイル: Player_Move.cs プロジェクト: nbucciarelli/ACE
        public void TakeDamage_Falling(float amount)
        {
            if (IsDead || Invincible)
            {
                return;
            }

            // handle lifestone protection?
            if (UnderLifestoneProtection)
            {
                HandleLifestoneProtection();
                return;
            }

            // scale by bludgeon protection
            var resistance = EnchantmentManager.GetResistanceMod(DamageType.Bludgeon);
            var damage     = (uint)Math.Round(amount * resistance);

            // update health
            var damageTaken = (uint)-UpdateVitalDelta(Health, (int)-damage);

            DamageHistory.Add(this, DamageType.Bludgeon, damageTaken);

            var msg = Strings.GetFallMessage(damageTaken, Health.MaxValue);

            SendMessage(msg, ChatMessageType.Combat);

            if (Health.Current <= 0)
            {
                OnDeath(new DamageHistoryInfo(this), DamageType.Bludgeon, false);
                Die();
            }
            else
            {
                EnqueueBroadcast(new GameMessageSound(Guid, Sound.Wound3, 1.0f));
            }
        }
コード例 #17
0
ファイル: Player_Death.cs プロジェクト: Illbatting/ACE
        /// <summary>
        /// Called when a player dies, in conjunction with Die()
        /// </summary>
        /// <param name="lastDamager">The last damager that landed the death blow</param>
        /// <param name="damageType">The damage type for the death message</param>
        public override DeathMessage OnDeath(WorldObject lastDamager, DamageType damageType, bool criticalHit = false)
        {
            var deathMessage = base.OnDeath(lastDamager, damageType, criticalHit);

            var playerMsg    = string.Format(deathMessage.Victim, Name, lastDamager.Name);
            var msgYourDeath = new GameEventYourDeath(Session, playerMsg);

            Session.Network.EnqueueSend(msgYourDeath);

            // broadcast to nearby players
            var nearbyMsg     = string.Format(deathMessage.Broadcast, Name, lastDamager.Name);
            var broadcastMsg  = new GameMessageSystemChat(nearbyMsg, ChatMessageType.Broadcast);
            var nearbyPlayers = EnqueueBroadcast(false, broadcastMsg);

            var excludePlayers = nearbyPlayers.ToList();

            excludePlayers.Add(this);   // exclude self

            // if the player's lifestone is in a different landblock, also broadcast their demise to that landblock
            if (Sanctuary != null && Location.Landblock != Sanctuary.Landblock)
            {
                // ActionBroadcastKill might not work if other players around lifestone aren't aware of this player yet...
                // this existing broadcast method is also based on the current visible objects to the player,
                // and the player hasn't entered portal space or teleported back to the lifestone yet, so this doesn't work
                //ActionBroadcastKill(nearbyMsg, Guid, lastDamager.Guid);

                // instead, we get all of the players in the lifestone landblock + adjacent landblocks,
                // and possibly limit that to some radius around the landblock?
                var lifestoneBlock = LandblockManager.GetLandblock(new LandblockId(Sanctuary.Landblock << 16 | 0xFFFF), true);
                lifestoneBlock.EnqueueBroadcast(excludePlayers, true, broadcastMsg);
            }

            // reset damage history for this player
            DamageHistory.Reset();

            return(deathMessage);
        }
コード例 #18
0
ファイル: Player_Death.cs プロジェクト: ServerSelection/ACE
        /// <summary>
        /// Called when the player enters portal space after dying
        /// </summary>
        public void TeleportOnDeath()
        {
            // teleport to sanctuary or best location
            var newPosition = Sanctuary ?? LastPortal ?? Location;

            Teleport(newPosition);

            // Stand back up
            SetCombatMode(CombatMode.NonCombat);

            var teleportChain = new ActionChain();

            teleportChain.AddDelaySeconds(3.0f);
            teleportChain.AddAction(this, () =>
            {
                // currently happens while in portal space
                var newHealth  = (uint)Math.Round(Health.MaxValue * 0.75f);
                var newStamina = (uint)Math.Round(Stamina.MaxValue * 0.75f);
                var newMana    = (uint)Math.Round(Mana.MaxValue * 0.75f);

                var msgHealthUpdate  = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, newHealth);
                var msgStaminaUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Stamina, newStamina);
                var msgManaUpdate    = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Mana, newMana);

                UpdateVital(Health, newHealth);
                UpdateVital(Stamina, newStamina);
                UpdateVital(Mana, newMana);

                Session.Network.EnqueueSend(msgHealthUpdate, msgStaminaUpdate, msgManaUpdate);

                // reset damage history for this player
                DamageHistory.Reset();
            });

            teleportChain.EnqueueChain();
        }
コード例 #19
0
        /// <summary>
        /// Broadcasts the player death animation, updates vitae, and sends network messages for player death
        /// Queues the action to call TeleportOnDeath and enter portal space soon
        /// </summary>
        protected override void Die(WorldObject lastDamager, WorldObject topDamager)
        {
            UpdateVital(Health, 0);
            NumDeaths++;
            DeathLevel  = Level; // for calculating vitae XP
            VitaeCpPool = 0;     // reset vitae XP earned

            // killer = top damager for looting rights
            if (topDamager != null)
            {
                Killer = topDamager.Guid.Full;
            }

            // broadcast death animation
            var deathAnim = new UniversalMotion(MotionStance.NonCombat, new MotionItem(MotionCommand.Dead));

            EnqueueBroadcastMotion(deathAnim);

            // killer death message = last damager
            var killerMsg           = lastDamager != null ? " to " + lastDamager.Name : "";
            var currentDeathMessage = $"died{killerMsg}.";

            // create network messages for player death
            var msgHealthUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, 0);

            // TODO: death sounds? seems to play automatically in client
            // var msgDeathSound = new GameMessageSound(Guid, Sound.Death1, 1.0f);
            var msgYourDeath         = new GameEventYourDeath(Session, $"You have {currentDeathMessage}");
            var msgNumDeaths         = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.NumDeaths, NumDeaths ?? 0);
            var msgDeathLevel        = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.DeathLevel, DeathLevel ?? 0);
            var msgVitaeCpPool       = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.VitaeCpPool, VitaeCpPool.Value);
            var msgPurgeEnchantments = new GameEventPurgeAllEnchantments(Session);

            // update vitae
            var vitae = EnchantmentManager.UpdateVitae();

            var spellID             = (uint)Network.Enum.Spell.Vitae;
            var spellBase           = DatManager.PortalDat.SpellTable.Spells[spellID];
            var spell               = DatabaseManager.World.GetCachedSpell(spellID);
            var vitaeEnchantment    = new Enchantment(this, Guid, spellID, (double)spell.Duration, 0, spell.StatModType, vitae);
            var msgVitaeEnchantment = new GameEventMagicUpdateEnchantment(Session, vitaeEnchantment);

            // send network messages for player death
            Session.Network.EnqueueSend(msgHealthUpdate, msgYourDeath, msgNumDeaths, msgDeathLevel, msgVitaeCpPool, msgPurgeEnchantments, msgVitaeEnchantment);

            // wait for the death animation to finish
            var dieChain   = new ActionChain();
            var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.Dead);

            dieChain.AddDelaySeconds(animLength + 1.0f);

            // enter portal space
            dieChain.AddAction(this, CreateCorpse);
            dieChain.AddAction(this, TeleportOnDeath);
            dieChain.EnqueueChain();

            // if the player's lifestone is in a different landblock, also broadcast their demise to that landblock
            if (Sanctuary != null && Location.Landblock != Sanctuary.Landblock)
            {
                var killerGuid = lastDamager != null ? lastDamager.Guid : Guid;
                ActionBroadcastKill($"{Name} has {currentDeathMessage}", Guid, killerGuid);
            }
            DamageHistory.Reset();
        }
コード例 #20
0
ファイル: Player_Death.cs プロジェクト: Mad-Hatz/ACE
        /// <summary>
        /// Called when a player dies, in conjunction with Die()
        /// </summary>
        /// <param name="lastDamager">The last damager that landed the death blow</param>
        /// <param name="damageType">The damage type for the death message</param>
        public override DeathMessage OnDeath(DamageHistoryInfo lastDamager, DamageType damageType, bool criticalHit = false)
        {
            var topDamager = DamageHistory.GetTopDamager(false);

            HandlePKDeathBroadcast(lastDamager, topDamager);

            var deathMessage = base.OnDeath(lastDamager, damageType, criticalHit);

            var lastDamagerObj = lastDamager?.TryGetAttacker();

            if (lastDamagerObj != null)
            {
                lastDamagerObj.EmoteManager.OnKill(this);
            }

            var playerMsg = "";

            if (lastDamager != null)
            {
                playerMsg = string.Format(deathMessage.Victim, Name, lastDamager.Name);
            }
            else
            {
                playerMsg = deathMessage.Victim;
            }

            var msgYourDeath = new GameEventVictimNotification(Session, playerMsg);

            Session.Network.EnqueueSend(msgYourDeath);

            // broadcast to nearby players
            var nearbyMsg = "";

            if (lastDamager != null)
            {
                nearbyMsg = string.Format(deathMessage.Broadcast, Name, lastDamager.Name);
            }
            else
            {
                nearbyMsg = deathMessage.Broadcast;
            }

            var broadcastMsg = new GameMessagePlayerKilled(nearbyMsg, Guid, lastDamager?.Guid ?? ObjectGuid.Invalid);

            log.Debug("[CORPSE] " + nearbyMsg);

            var excludePlayers = new List <Player>();

            var nearbyPlayers = EnqueueBroadcast(excludePlayers, false, broadcastMsg);

            excludePlayers.AddRange(nearbyPlayers);

            if (Fellowship != null)
            {
                Fellowship.OnDeath(this);
            }

            // if the player's lifestone is in a different landblock, also broadcast their demise to that landblock
            if (PropertyManager.GetBool("lifestone_broadcast_death").Item&& Sanctuary != null && Location.Landblock != Sanctuary.Landblock)
            {
                // ActionBroadcastKill might not work if other players around lifestone aren't aware of this player yet...
                // this existing broadcast method is also based on the current visible objects to the player,
                // and the player hasn't entered portal space or teleported back to the lifestone yet, so this doesn't work
                //ActionBroadcastKill(nearbyMsg, Guid, lastDamager.Guid);

                // instead, we get all of the players in the lifestone landblock + adjacent landblocks,
                // and possibly limit that to some radius around the landblock?
                var lifestoneBlock = LandblockManager.GetLandblock(new LandblockId(Sanctuary.Landblock << 16 | 0xFFFF), true);
                lifestoneBlock.EnqueueBroadcast(excludePlayers, true, Sanctuary, LocalBroadcastRangeSq, broadcastMsg);
            }

            return(deathMessage);
        }
コード例 #21
0
ファイル: Player_Death.cs プロジェクト: Mad-Hatz/ACE
        /// <summary>
        /// Broadcasts the player death animation, updates vitae, and sends network messages for player death
        /// Queues the action to call TeleportOnDeath and enter portal space soon
        /// </summary>
        protected override void Die(DamageHistoryInfo lastDamager, DamageHistoryInfo topDamager)
        {
            if (topDamager?.Guid == Guid && IsPKType)
            {
                var topDamagerOther = DamageHistory.GetTopDamager(false);

                if (topDamagerOther != null && topDamagerOther.IsPlayer)
                {
                    topDamager = topDamagerOther;
                }
            }

            UpdateVital(Health, 0);
            NumDeaths++;
            suicideInProgress = false;

            if (CombatMode == CombatMode.Magic && MagicState.IsCasting)
            {
                FailCast(false);
            }

            // TODO: instead of setting IsBusy here,
            // eventually all of the places that check for states such as IsBusy || Teleporting
            // might want to use a common function, and IsDead should return a separate error
            IsBusy = true;

            // killer = top damager for looting rights
            if (topDamager != null)
            {
                KillerId = topDamager.Guid.Full;
            }

            // broadcast death animation
            var deathAnim = new Motion(MotionStance.NonCombat, MotionCommand.Dead);

            EnqueueBroadcastMotion(deathAnim);

            // create network messages for player death
            var msgHealthUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, 0);

            // TODO: death sounds? seems to play automatically in client
            // var msgDeathSound = new GameMessageSound(Guid, Sound.Death1, 1.0f);
            var msgNumDeaths = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.NumDeaths, NumDeaths);

            // send network messages for player death
            Session.Network.EnqueueSend(msgHealthUpdate, msgNumDeaths);

            if (lastDamager?.Guid == Guid) // suicide
            {
                var msgSelfInflictedDeath = new GameEventWeenieError(Session, WeenieError.YouKilledYourself);
                Session.Network.EnqueueSend(msgSelfInflictedDeath);
            }

            // update vitae
            // players who died in a PKLite fight do not accrue vitae
            if (!IsPKLiteDeath(topDamager))
            {
                InflictVitaePenalty();
            }

            if (IsPKDeath(topDamager) || AugmentationSpellsRemainPastDeath == 0)
            {
                var msgPurgeEnchantments = new GameEventMagicPurgeEnchantments(Session);
                EnchantmentManager.RemoveAllEnchantments();
                Session.Network.EnqueueSend(msgPurgeEnchantments);
            }

            // wait for the death animation to finish
            var dieChain   = new ActionChain();
            var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.Dead);

            dieChain.AddDelaySeconds(animLength + 1.0f);

            dieChain.AddAction(this, () =>
            {
                CreateCorpse(topDamager);

                ThreadSafeTeleportOnDeath(); // enter portal space

                if (IsPKDeath(topDamager) || IsPKLiteDeath(topDamager))
                {
                    SetMinimumTimeSincePK();
                }

                IsBusy = false;
            });

            dieChain.EnqueueChain();
        }
コード例 #22
0
        private static void HandleCombatLog(object sender, LuaEventArgs args)
        {
            // Since we hooked this in ctor, make sure we are the selected CC
            if (RoutineManager.Current.Name != SingularRoutine.Instance.Name)
            {
                return;
            }

            // convert args to usable form
            var  e           = new CombatLogEventArgs(args.EventName, args.FireTimeStamp, args.Args);
            bool itWasDamage = false;

            if (TrackDamage || SingularRoutine.CurrentWoWContext == WoWContext.Normal)
            {
                if (e.DestGuid == StyxWoW.Me.Guid && e.SourceGuid != StyxWoW.Me.Guid)
                {
                    long damageAmount = 0;
                    switch (e.EventName)
                    {
                    case "SWING_DAMAGE":
                        itWasDamage  = true;
                        damageAmount = (long)e.Args[11];
                        Logger.WriteDebug("HandleCombatLog(Damage): {0} = {1}", e.EventName, damageAmount);
                        break;

                    case "SPELL_DAMAGE":
                    case "SPELL_PERIODIC_DAMAGE":
                    case "RANGE_DAMAGE":
                        itWasDamage  = true;
                        damageAmount = (long)e.Args[14];
                        break;
                    }

                    if (TrackDamage)
                    {
                        if (itWasDamage)
                        {
                            Logger.WriteDebug("HandleCombatLog(Damage): {0} = {1}", e.EventName, damageAmount);
                        }
                        else
                        {
                            LogUndesirableEvent("On Character", e);
                        }

                        if (damageAmount > 0)
                        {
                            DamageHistory.Enqueue(new Damage(DateTime.UtcNow, damageAmount));
                        }
                    }

                    if (itWasDamage && SingularRoutine.CurrentWoWContext == WoWContext.Normal)
                    {
                        WoWUnit enemy = e.SourceUnit;
                        if (Unit.ValidUnit(enemy) && enemy.IsPlayer)
                        {
                            Logger.WriteDiagnostic("GankDetect: received {0} src={1} dst={2}", args.EventName, e.SourceGuid, e.DestGuid);

                            // if (guidLastEnemy != enemy.Guid || (TimeLastAttackedByEnemyPlayer - DateTime.UtcNow).TotalSeconds > 30)
                            {
                                guidLastEnemy = enemy.Guid;
                                string extra = "";
                                if (e.Args.GetUpperBound(0) >= 12)
                                {
                                    extra = string.Format(" using {0}", e.SpellName);
                                }

                                AttackedWithSpellSchool = WoWSpellSchool.None;
                                if (e.Args.GetUpperBound(0) >= 12)
                                {
                                    AttackedWithSpellSchool = e.SpellSchool;
                                }

                                Logger.WriteDiagnostic("GankDetect: attacked by Level {0} {1}{2}", enemy.Level, enemy.SafeName(), extra);
                                if (SingularSettings.Instance.TargetWorldPvpRegardless && (BotPoi.Current == null || BotPoi.Current.Guid != enemy.Guid))
                                {
                                    Logger.Write(LogColor.Hilite, "GankDetect: setting {0} as BotPoi Kill Target", enemy.SafeName());
                                    BotPoi.Current = new BotPoi(enemy, PoiType.Kill);
                                }
                            }

                            AttackingEnemyPlayer          = enemy;
                            TimeLastAttackedByEnemyPlayer = DateTime.UtcNow;
                        }
                    }
                }
            }

            // Logger.WriteDebug("[CombatLog] " + e.Event + " - " + e.SourceName + " - " + e.SpellName);

            switch (e.Event)
            {
            default:
                LogUndesirableEvent("From Character", e);
                break;

            // spell_cast_failed only passes filter in Singular debug mode
            case "SPELL_CAST_FAILED":
                Logger.WriteDiagnostic("[CombatLog] {0} {1}#{2} failure: '{3}'", e.Event, e.Spell.Name, e.SpellId, e.Args[14]);
                if (e.Args[14].ToString() == LocalizedLineOfSightFailure)
                {
                    WoWGuid guid = WoWGuid.Empty;
                    try
                    {
                        LastLineOfSightTarget = e.DestUnit;
                        guid = LastLineOfSightTarget == null ? WoWGuid.Empty : LastLineOfSightTarget.Guid;
                    }
                    catch
                    {
                    }

                    if (!guid.IsValid)
                    {
                        Logger.WriteFile("[CombatLog] no valid destunit so using CurrentTarget");
                        LastLineOfSightTarget = StyxWoW.Me.CurrentTarget;
                        guid = StyxWoW.Me.CurrentTargetGuid;
                    }

                    LastLineOfSightFailure = DateTime.UtcNow;
                    Logger.WriteFile("[CombatLog] cast failed due to los reported at {0} on target {1:X}", LastLineOfSightFailure.ToString("HH:mm:ss.fff"), e.DestGuid);
                }
                else if (e.Args[14].ToString() == LocalizedUnitNotInfrontFailure)
                {
                    WoWGuid guid = e.DestGuid;
                    LastUnitNotInfrontFailure = DateTime.UtcNow;
                    if (guid.IsValid && guid != WoWGuid.Empty)
                    {
                        LastUnitNotInfrontGuid = guid;
                        Logger.WriteFile("[CombatLog] not facing SpellTarget [{0}] at {1}", LastUnitNotInfrontGuid, LastUnitNotInfrontFailure.ToString("HH:mm:ss.fff"));
                    }
                    else
                    {
                        LastUnitNotInfrontGuid = Spell.LastSpellTarget;
                        Logger.WriteFile("[CombatLog] not facing LastTarget [{0}] at {1}", LastUnitNotInfrontGuid, LastUnitNotInfrontFailure.ToString("HH:mm:ss.fff"), guid);
                    }
                }
                else if (!MovementManager.IsMovementDisabled && StyxWoW.Me.Class == WoWClass.Warrior && e.Args[14].ToString() == LocalizedNoPathAvailableFailure)
                {
                    LastNoPathFailure = DateTime.UtcNow;
                    LastNoPathGuid    = StyxWoW.Me.CurrentTargetGuid;
                    if (!StyxWoW.Me.GotTarget())
                    {
                        Logger.WriteFile("[CombatLog] cast failed - no path available to current target");
                    }
                    else
                    {
                        Logger.WriteFile("[CombatLog] cast failed - no path available to {0}, heightOffGround={1}, pos={2}",
                                         StyxWoW.Me.CurrentTarget.SafeName(),
                                         StyxWoW.Me.CurrentTarget.HeightOffTheGround(),
                                         StyxWoW.Me.CurrentTarget.Location
                                         );
                    }
                }
                else if (!SingularRoutine.IsManualMovementBotActive && (StyxWoW.Me.Class == WoWClass.Druid || StyxWoW.Me.Class == WoWClass.Shaman))
                {
                    if (LocalizedShapeshiftMessages.ContainsKey(e.Args[14].ToString()))
                    {
                        string symbolicName = LocalizedShapeshiftMessages[e.Args[14].ToString()];
                        SuppressShapeshiftUntil = DateTime.UtcNow.Add(TimeSpan.FromSeconds(30));
                        Logger.Write(LogColor.Cancel, "/cancel{0} - due to Shapeshift Error '{1}' on cast, suppress form for {2:F1} seconds", StyxWoW.Me.Shapeshift.ToString().CamelToSpaced(), symbolicName, (SuppressShapeshiftUntil - DateTime.UtcNow).TotalSeconds);
                        Lua.DoString("CancelShapeshiftForm()");
                    }
                }
                else if (StyxWoW.Me.Class == WoWClass.Rogue && SingularSettings.Instance.Rogue().UsePickPocket)
                {
                    if (e.Args[14].ToString() == LocalizedNoPocketsToPickFailure)
                    {
                        HandleRogueNoPocketsError();
                    }
                }
                break;

            case "SPELL_AURA_APPLIED":
            case "SPELL_CAST_SUCCESS":
                if (e.SourceGuid != StyxWoW.Me.Guid)
                {
                    return;
                }

                // Update the last spell we cast. So certain classes can 'switch' their logic around.
                Spell.LastSpellCast = e.SpellName;
                Logger.WriteDebug("Storing {0} as last spell cast.", Spell.LastSpellCast);

                // following commented block should not be needed since rewrite of Pet summon
                //
                //// Force a wait for all summoned minions. This prevents double-casting it.
                //if (StyxWoW.Me.Class == WoWClass.Warlock && e.SpellName.StartsWith("Summon "))
                //{
                //    StyxWoW.SleepForLagDuration();
                //}
                break;

            case "SWING_MISSED":
                if (e.Args[11].ToString() == "EVADE")
                {
                    HandleEvadeBuggedMob(args, e);
                }
                else if (e.Args[11].ToString() == "IMMUNE")
                {
                    WoWUnit unit = e.DestUnit;
                    if (unit != null && !unit.IsPlayer)
                    {
                        Logger.WriteDebug("{0} is immune to Physical spell school", unit.Name);
                        SpellImmunityManager.Add(unit.Entry, WoWSpellSchool.Physical, e.Spell);
                    }
                }
                break;

            case "SPELL_MISSED":
            case "RANGE_MISSED":
                // Why log misses?  Because users of classes with DoTs testing on training dummies
                // .. that they don't have enough +Hit for will get DoT spam.  This allows easy
                // .. diagnosis of false reports of rotation issues where a user simply isn't geared
                // .. this happens more at the beginning of an expansion especially
                if (SingularSettings.Debug)
                {
                    Logger.WriteDebug(
                        "[CombatLog] {0} {1}#{2} {3}",
                        e.Event,
                        e.Spell.Name,
                        e.SpellId,
                        e.Args[14]
                        );
                }

                if (e.Args[14].ToString() == "EVADE")
                {
                    HandleEvadeBuggedMob(args, e);
                }
                else if (e.Args[14].ToString() == "IMMUNE")
                {
                    WoWUnit unit = e.DestUnit;
                    if (unit != null && !unit.IsPlayer)
                    {
                        Logger.WriteDebug("{0} is immune to {1} spell school", unit.Name, e.SpellSchool);
                        SpellImmunityManager.Add(unit.Entry, e.SpellSchool, e.Spell);
                    }

                    if (StyxWoW.Me.Class == WoWClass.Rogue && e.SpellId == 6770)
                    {
                        WoWUnit unitImmune = unit;
                        if (unitImmune == null)
                        {
                            unitImmune = ObjectManager.GetObjectByGuid <WoWUnit>(Singular.ClassSpecific.Rogue.Common.lastSapTarget);
                        }

                        Singular.ClassSpecific.Rogue.Common.AddEntryToSapImmuneList(unitImmune);
                    }
                }
                break;

            case "UNIT_DIED":
                if (StyxWoW.Me.CurrentTarget != null && e.DestGuid == StyxWoW.Me.CurrentTarget.Guid)
                {
                    Spell.LastSpellCast = "";
                }

                try
                {
                    WoWUnit        corpse = e.SourceUnit;
                    WoWPartyMember pm     = Unit.GroupMemberInfos.First(m => m.Guid == corpse.Guid);
                    Logger.WriteDiagnostic("Combat Log: UNIT_DIED - Role={0} {1}", pm.Role & (~WoWPartyMember.GroupRole.Leader), corpse.SafeName());
                }
                catch
                {
                }
                break;
            }
        }
コード例 #23
0
        /// <summary>
        /// Method used to perform the animation, sound, and vital update on consumption of food or potions
        /// </summary>
        /// <param name="consumableName">Name of the consumable</param>
        /// <param name="sound">Either Sound.Eat1 or Sound.Drink1</param>
        /// <param name="buffType">ConsumableBuffType.Spell,ConsumableBuffType.Health,ConsumableBuffType.Stamina,ConsumableBuffType.Mana</param>
        /// <param name="boostAmount">Amount the Vital is boosted by; can be null, if buffType = ConsumableBuffType.Spell</param>
        /// <param name="spellDID">Id of the spell cast by the consumable; can be null, if buffType != ConsumableBuffType.Spell</param>
        public void ApplyConsumable(string consumableName, Sound sound, ConsumableBuffType buffType, uint?boostAmount, uint?spellDID)
        {
            MotionCommand motionCommand;

            if (sound == Sound.Eat1)
            {
                motionCommand = MotionCommand.Eat;
            }
            else
            {
                motionCommand = MotionCommand.Drink;
            }

            // start the eat/drink motion
            var motion = new Motion(MotionStance.NonCombat, motionCommand);

            EnqueueBroadcastMotion(motion);

            var motionTable = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId);
            var animTime    = motionTable.GetAnimationLength(CurrentMotionState.Stance, motionCommand, MotionCommand.Ready);

            var actionChain = new ActionChain();

            actionChain.AddDelaySeconds(animTime);

            actionChain.AddAction(this, () =>
            {
                GameMessageSystemChat buffMessage;

                if (buffType == ConsumableBuffType.Spell)
                {
                    bool result = false;

                    uint spellId = spellDID ?? 0;

                    if (spellId != 0)
                    {
                        result = CreateSingleSpell(spellId);
                    }

                    if (result)
                    {
                        var spell   = new Server.Entity.Spell(spellId);
                        buffMessage = new GameMessageSystemChat($"{consumableName} casts {spell.Name} on you.", ChatMessageType.Magic);
                    }
                    else
                    {
                        buffMessage = new GameMessageSystemChat($"Consuming {consumableName} attempted to apply a spell not yet fully implemented.", ChatMessageType.System);
                    }
                }
                else
                {
                    CreatureVital creatureVital;
                    string vitalName;

                    // Null check for safety
                    if (boostAmount == null)
                    {
                        boostAmount = 0;
                    }

                    switch (buffType)
                    {
                    case ConsumableBuffType.Health:
                        creatureVital = Health;
                        vitalName     = "Health";
                        break;

                    case ConsumableBuffType.Mana:
                        creatureVital = Mana;
                        vitalName     = "Mana";
                        break;

                    default:
                        creatureVital = Stamina;
                        vitalName     = "Stamina";
                        break;
                    }

                    var vitalChange = UpdateVitalDelta(creatureVital, (uint)boostAmount);
                    if (vitalName == "Health")
                    {
                        DamageHistory.OnHeal((uint)vitalChange);
                        if (Fellowship != null)
                        {
                            Fellowship.OnVitalUpdate(this);
                        }
                    }

                    buffMessage = new GameMessageSystemChat($"You regain {vitalChange} {vitalName}.", ChatMessageType.Craft);
                }

                var soundEvent = new GameMessageSound(Guid, sound, 1.0f);
                EnqueueBroadcast(soundEvent);

                Session.Network.EnqueueSend(buffMessage);

                // return to original stance
                var returnStance = new Motion(CurrentMotionState.Stance);
                EnqueueBroadcastMotion(returnStance);
            });

            actionChain.EnqueueChain();
        }