/// <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}"); }
/// <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)); }
/// <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)); }
/// <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)); } }
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(); }
/// <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)); } }
public virtual void SetMaxVitals() { var missingHealth = Health.Missing; Health.Current = Health.MaxValue; Stamina.Current = Stamina.MaxValue; Mana.Current = Mana.MaxValue; DamageHistory.OnHeal(missingHealth); }
/// <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(); })); }
/// <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}"); }
/// <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); }
/// <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)); } } }
/// <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; }
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()); } }
/// <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); } } }
/// <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); }
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)); } }
/// <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); }
/// <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(); }
/// <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(); }
/// <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); }
/// <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(); }
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; } }
/// <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(); }