/// <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++; // 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); // 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 msgNumDeaths = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.NumDeaths, NumDeaths); // send network messages for player death Session.Network.EnqueueSend(msgHealthUpdate, msgNumDeaths); // update vitae // players who died in a PKLite fight do not accrue vitae var pkLiteKiller = GetKiller_PKLite(); if (pkLiteKiller == null) { InflictVitaePenalty(); } if (AugmentationSpellsRemainPastDeath == 0 || topDamager is Player && topDamager.PlayerKillerStatus == PlayerKillerStatus.PK) { 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); // enter portal space dieChain.AddAction(this, () => CreateCorpse(topDamager)); dieChain.AddAction(this, TeleportOnDeath); dieChain.AddAction(this, SetLifestoneProtection); dieChain.AddAction(this, SetMinimumTimeSincePK); dieChain.EnqueueChain(); }
public static void Handle(ClientMessage message, Session session) { if (session.Player.Positions.ContainsKey(PositionType.Sanctuary)) { // session.Player.Teleport(session.Player.Positions[PositionType.Sanctuary]); string msg = $"{session.Player.Name} is recalling to the lifestone."; var sysChatMessage = new GameMessageSystemChat(msg, ChatMessageType.Recall); session.Player.Mana.Current = session.Player.Mana.Current / 2; var updatePlayersMana = new GameMessagePrivateUpdateAttribute2ndLevel(session, Vital.Mana, session.Player.Mana.Current); var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(session, PropertyInt.CombatMode, 1); var motionLifestoneRecall = new UniversalMotion(MotionStance.Standing, new MotionItem(MotionCommand.LifestoneRecall)); var animationEvent = new GameMessageUpdateMotion(session.Player.Guid, session.Player.Sequences.GetCurrentSequence(Sequence.SequenceType.ObjectInstance), session.Player.Sequences, motionLifestoneRecall); // TODO: This needs to be changed to broadcast sysChatMessage to only those in local chat hearing range // FIX: Recall text isn't being broadcast yet, need to address session.Network.EnqueueSend(updatePlayersMana, updateCombatMode, sysChatMessage); session.Player.EnqueueMovementEvent(motionLifestoneRecall, session.Player.Guid); session.Player.SetDelayedTeleport(TimeSpan.FromSeconds(14), session.Player.Positions[PositionType.Sanctuary]); } else { ChatPacket.SendServerMessage(session, "Your spirit has not been attuned to a sanctuary location.", ChatMessageType.Broadcast); } }
/// <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); 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); // Stand back up SetStance(MotionStance.NonCombat); }); teleportChain.EnqueueChain(); }
public void TakeDamage(WorldObject source, DamageType damageType, float _amount, BodyPart bodyPart, bool crit = false) { var amount = (uint)Math.Round(_amount); var percent = (float)amount / Health.MaxValue; // update health Health.Current = (uint)Math.Max(0, (int)Health.Current - amount); if (Health.Current == 0) { HandleActionDie(); return; } // send network messages var msgHealth = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, Health.Current); var hitSound = new GameMessageSound(Guid, GetHitSound(source, bodyPart), 1.0f); var creature = source as Creature; var splatter = new GameMessageScript(Guid, (PlayScript)Enum.Parse(typeof(PlayScript), "Splatter" + GetSplatterHeight() + creature.GetSplatterDir(this))); var damageLocation = (DamageLocation)BodyParts.Indices[bodyPart]; var text = new GameEventDefenderNotification(Session, creature.Name, damageType, percent, amount, damageLocation, crit, AttackConditions.None); Session.Network.EnqueueSend(text, msgHealth, hitSound, splatter); if (percent >= 0.1f) { var wound = new GameMessageSound(Guid, Sound.Wound1, 1.0f); Session.Network.EnqueueSend(wound); } }
public override void SetMaxVitals() { base.SetMaxVitals(); var health = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, Health.Current); var stamina = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Stamina, Stamina.Current); var mana = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Mana, Mana.Current); Session.Network.EnqueueSend(health, stamina, mana); }
/// <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 Motion(MotionStance.NonCombat, 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 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 GameEventMagicPurgeEnchantments(Session); // update vitae var vitae = EnchantmentManager.UpdateVitae(); var spellID = (uint)SpellId.Vitae; var spell = new Spell(spellID); var vitaeEnchantment = new Enchantment(this, Guid, spellID, spell.Duration, 0, (EnchantmentMask)spell.StatModType, vitae); var msgVitaeEnchantment = new GameEventMagicUpdateEnchantment(Session, vitaeEnchantment); // send network messages for player death Session.Network.EnqueueSend(msgHealthUpdate, 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(); }
protected override void DoOnKill(Session killerSession) { // First do on-kill OnKill(killerSession); // Then get onKill from our parent ActionChain killChain = base.OnKillInternal(killerSession); // Send the teleport out after we animate death killChain.AddAction(this, () => { // teleport to sanctuary or best location var newPosition = Sanctuary ?? LastPortal ?? Location; // Enqueue a teleport action, followed by Stand-up // Queue the teleport to lifestone ActionChain teleportChain = GetTeleportChain(newPosition); teleportChain.AddAction(this, () => { 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); killerSession.Network.EnqueueSend(msgHealthUpdate, msgStaminaUpdate, msgManaUpdate); // Stand back up DoMotion(new UniversalMotion(MotionStance.Standing)); // add a Corpse at the current location via the ActionQueue to honor the motion and teleport delays // QueuedGameAction addCorpse = new QueuedGameAction(this.Guid.Full, corpse, true, GameActionType.ObjectCreate); // AddToActionQueue(addCorpse); // If the player is outside of the landblock we just died in, then reboadcast the death for // the players at the lifestone. if (Positions.ContainsKey(PositionType.LastOutsideDeath) && Positions[PositionType.LastOutsideDeath].Cell != newPosition.Cell) { string currentDeathMessage = $"died to {killerSession.Player.Name}."; ActionBroadcastKill($"{Name} has {currentDeathMessage}", Guid, killerSession.Player.Guid); } }); teleportChain.EnqueueChain(); }); killChain.EnqueueChain(); }
/// <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(); })); }
public static void HandleSetHealth(Session session, params string[] parameters) { if (parameters?.Length > 0) { if (ushort.TryParse(parameters[0], out var health)) { session.Player.Health.Current = health; var updatePlayersHealth = new GameMessagePrivateUpdateAttribute2ndLevel(session, Vital.Health, session.Player.Health.Current); var message = new GameMessageSystemChat($"Attempting to set health to {health}...", ChatMessageType.Broadcast); session.Network.EnqueueSend(updatePlayersHealth, message); return; } } ChatPacket.SendServerMessage(session, "Usage: /sethealth 200 (max Max Health)", ChatMessageType.Broadcast); }
/// <summary> /// Player Death/Kill, use this to kill a session's player /// </summary> /// <remarks> /// TODO: /// 1. Find the best vitae formula and add vitae /// 2. Generate the correct death message, or have it passed in as a parameter. /// 3. Find the correct player death noise based on the player model and play on death. /// 4. Determine if we need to Send Queued Action for Lifestone Materialize, after Action Location. /// 5. Find the health after death formula and Set the correct health /// </remarks> private void OnKill(Session killerSession) { ObjectGuid killerId = killerSession.Player.Guid; IsAlive = false; Health.Current = 0; // Set the health to zero NumDeaths++; // Increase the NumDeaths counter DeathLevel = Level; // For calculating vitae XP VitaeCpPool = 0; // Set vitae XP // TODO: Generate a death message based on the damage type to pass in to each death message: string currentDeathMessage = $"died to {killerSession.Player.Name}."; // Send Vicitim Notification, or "Your Death" event to the client: // create and send the client death event, GameEventYourDeath 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); // var msgDeathSound = new GameMessageSound(Guid, Sound.Death1, 1.0f); // handle 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, spellID, (double)spell.Duration, 0, spell.StatModType, vitae); var msgVitaeEnchantment = new GameEventMagicUpdateEnchantment(Session, vitaeEnchantment); var msgHealthUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, 0); // Send first death message group Session.Network.EnqueueSend(msgHealthUpdate, msgYourDeath, msgNumDeaths, msgDeathLevel, msgVitaeCpPool, msgPurgeEnchantments, msgVitaeEnchantment); // Broadcast the 019E: Player Killed GameMessage ActionBroadcastKill($"{Name} has {currentDeathMessage}", Guid, killerId); }
private static void HandleHealingRecipe(Player player, WorldObject source, WorldObject target, Recipe recipe) { ActionChain chain = new ActionChain(); // skill will be null since the difficulty is calculated manually if (recipe.SkillId == null) { log.Warn($"healing recipe has null skill id (should almost certainly be healing, but who knows). recipe id {recipe.RecipeId}."); player.SendUseDoneEvent(); return; } if (!(target is Player)) { var message = new GameMessageSystemChat($"The {source.Name} cannot be used on {target.Name}.", ChatMessageType.Craft); player.Session.Network.EnqueueSend(message); player.SendUseDoneEvent(); return; } Player targetPlayer = target as Player; Ability vital = (Ability?)recipe.HealingAttribute ?? Ability.Health; // there's a skill associated with this Skill skillId = (Skill)recipe.SkillId.Value; // this shouldn't happen, but sanity check for unexpected nulls if (!player.Skills.ContainsKey(skillId)) { log.Warn("Unexpectedly missing skill in Recipe usage"); player.SendUseDoneEvent(); return; } CreatureSkill skill = player.Skills[skillId]; // at this point, we've validated that the target is a player, and the target is below max health if (target.Guid != player.Guid) { // TODO: validate range } MotionCommand cmd = MotionCommand.SkillHealSelf; if (target.Guid != player.Guid) { cmd = MotionCommand.Woah; // guess? nothing else stood out } // everything pre-validatable is validated. action will be attempted unless cancelled, so // queue up the animation and action UniversalMotion motion = new UniversalMotion(MotionStance.Standing, new MotionItem(cmd)); chain.AddAction(player, () => player.HandleActionMotion(motion)); chain.AddDelaySeconds(0.5); chain.AddAction(player, () => { // TODO: revalidate range if other player (they could have moved) double difficulty = 2 * (targetPlayer.Vitals[vital].MaxValue - targetPlayer.Vitals[vital].Current); if (difficulty <= 0) { // target is at max (or higher?) health, do nothing var text = "You are already at full health."; if (target.Guid != player.Guid) { text = $"{target.Name} is already at full health"; } player.Session.Network.EnqueueSend(new GameMessageSystemChat(text, ChatMessageType.Craft)); player.SendUseDoneEvent(); return; } if (player.CombatMode != CombatMode.NonCombat && player.CombatMode != CombatMode.Undef) { difficulty *= 1.1; } uint boost = source.Boost ?? 0; double multiplier = source.HealkitMod ?? 1; double playerSkill = skill.ActiveValue + boost; if (skill.Status == SkillStatus.Trained) { playerSkill *= 1.1; } else if (skill.Status == SkillStatus.Specialized) { playerSkill *= 1.5; } // usage is inevitable at this point, consume the use if ((recipe.ResultFlags & (uint)RecipeResult.SourceItemUsesDecrement) > 0) { if (source.Structure <= 1) { player.DestroyInventoryItem(source); } else { source.Structure--; source.SendPartialUpdates(player.Session, _updateStructure); } } double percentSuccess = CreatureSkill.GetPercentSuccess((uint)playerSkill, (uint)difficulty); if (_random.NextDouble() <= percentSuccess) { string expertly = ""; if (_random.NextDouble() < 0.1d) { expertly = "expertly "; multiplier *= 1.2; } // calculate amount restored uint maxRestore = targetPlayer.Vitals[vital].MaxValue - targetPlayer.Vitals[vital].Current; // TODO: get actual forumula for healing. this is COMPLETELY wrong. this is 60 + random(1-60). double amountRestored = 60d + _random.Next(1, 61); amountRestored *= multiplier; uint actualRestored = (uint)Math.Min(maxRestore, amountRestored); targetPlayer.Vitals[vital].Current += actualRestored; var updateVital = new GameMessagePrivateUpdateAttribute2ndLevel(player.Session, vital.GetVital(), targetPlayer.Vitals[vital].Current); player.Session.Network.EnqueueSend(updateVital); if (targetPlayer.Guid != player.Guid) { // tell the other player they got healed var updateVitalToTarget = new GameMessagePrivateUpdateAttribute2ndLevel(targetPlayer.Session, vital.GetVital(), targetPlayer.Vitals[vital].Current); targetPlayer.Session.Network.EnqueueSend(updateVitalToTarget); } string name = "yourself"; if (targetPlayer.Guid != player.Guid) { name = targetPlayer.Name; } string vitalName = "Health"; if (vital == Ability.Stamina) { vitalName = "Stamina"; } else if (vital == Ability.Mana) { vitalName = "Mana"; } string uses = source.Structure == 1 ? "use" : "uses"; var text = string.Format(recipe.SuccessMessage, expertly, name, actualRestored, vitalName, source.Name, source.Structure, uses); var message = new GameMessageSystemChat(text, ChatMessageType.Craft); player.Session.Network.EnqueueSend(message); if (targetPlayer.Guid != player.Guid) { // send text to the other player too text = string.Format(recipe.AlternateMessage, player.Name, expertly, actualRestored, vitalName); message = new GameMessageSystemChat(text, ChatMessageType.Craft); targetPlayer.Session.Network.EnqueueSend(message); } } player.SendUseDoneEvent(); }); chain.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(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(); }
/// <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> /// 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++; suicideInProgress = false; // 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); TeleportOnDeath(); // enter portal space SetLifestoneProtection(); if (IsPKDeath(topDamager) || IsPKLiteDeath(topDamager)) { SetMinimumTimeSincePK(); } }); dieChain.EnqueueChain(); }