using System; using System.Collections.Generic; using System.Linq; using ACE.Database; using ACE.Database.Models.World; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Entity.Models; using ACE.Server.Entity; using ACE.Server.Entity.Actions; using ACE.Server.Factories; using ACE.Server.Managers; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages.Messages; namespace ACE.Server.WorldObjects { partial class Creature { public TreasureDeath DeathTreasure { get => DeathTreasureType.HasValue ? DatabaseManager.World.GetCachedDeathTreasure(DeathTreasureType.Value) : null; } private bool onDeathEntered = false; /// /// Called when a monster or player dies, in conjunction with Die() /// /// The last damager that landed the death blow /// The damage type for the death message /// True if the death blow was a critical hit, generates a critical death message public virtual DeathMessage OnDeath(DamageHistoryInfo lastDamager, DamageType damageType, bool criticalHit = false) { if (onDeathEntered) return GetDeathMessage(lastDamager, damageType, criticalHit); onDeathEntered = true; IsTurning = false; IsMoving = false; //QuestManager.OnDeath(lastDamager?.TryGetAttacker()); if (KillQuest != null) OnDeath_HandleKillTask(KillQuest); if (KillQuest2 != null) OnDeath_HandleKillTask(KillQuest2); if (KillQuest3 != null) OnDeath_HandleKillTask(KillQuest3); if (!IsOnNoDeathXPLandblock) OnDeath_GrantXP(); return GetDeathMessage(lastDamager, damageType, criticalHit); } public DeathMessage GetDeathMessage(DamageHistoryInfo lastDamagerInfo, DamageType damageType, bool criticalHit = false) { var lastDamager = lastDamagerInfo?.TryGetAttacker(); if (lastDamagerInfo == null || lastDamagerInfo.Guid == Guid || lastDamager is Hotspot || lastDamager is Food) // !(lastDamager is Creature)? return Strings.General[1]; var deathMessage = Strings.GetDeathMessage(damageType, criticalHit); // if killed by a player, send them a message if (lastDamagerInfo.IsPlayer) { if (criticalHit && this is Player) deathMessage = Strings.PKCritical[0]; var killerMsg = string.Format(deathMessage.Killer, Name); if (lastDamager is Player playerKiller) playerKiller.Session.Network.EnqueueSend(new GameEventKillerNotification(playerKiller.Session, killerMsg)); } return deathMessage; } /// /// Kills a player/creature and performs the full death sequence /// public void Die() { Die(DamageHistory.LastDamager, DamageHistory.TopDamager); } private bool dieEntered = false; /// /// Performs the full death sequence for non-Player creatures /// protected virtual void Die(DamageHistoryInfo lastDamager, DamageHistoryInfo topDamager) { if (dieEntered) return; dieEntered = true; UpdateVital(Health, 0); if (topDamager != null) { KillerId = topDamager.Guid.Full; if (topDamager.IsPlayer) { var topDamagerPlayer = topDamager.TryGetAttacker(); if (topDamagerPlayer != null) topDamagerPlayer.CreatureKills = (topDamagerPlayer.CreatureKills ?? 0) + 1; } } CurrentMotionState = new Motion(MotionStance.NonCombat, MotionCommand.Ready); //IsMonster = false; PhysicsObj.StopCompletely(true); // broadcast death animation var motionDeath = new Motion(MotionStance.NonCombat, MotionCommand.Dead); var deathAnimLength = ExecuteMotion(motionDeath); EmoteManager.OnDeath(lastDamager); var dieChain = new ActionChain(); // wait for death animation to finish //var deathAnimLength = DatManager.PortalDat.ReadFromDat(MotionTableId).GetAnimationLength(MotionCommand.Dead); dieChain.AddDelaySeconds(deathAnimLength); dieChain.AddAction(this, () => { CreateCorpse(topDamager); Destroy(); }); dieChain.EnqueueChain(); } /// /// Called when an admin player uses the /smite command /// to instantly kill a creature /// public void Smite(WorldObject smiter, bool useTakeDamage = false) { if (useTakeDamage) { // deal remaining damage TakeDamage(smiter, DamageType.Bludgeon, Health.Current); } else { OnDeath(); var smiterInfo = new DamageHistoryInfo(smiter); Die(smiterInfo, smiterInfo); } } public void OnDeath() { OnDeath(null, DamageType.Undef); } /// /// Grants XP to players in damage history /// public void OnDeath_GrantXP() { if (this is Player && PlayerKillerStatus == PlayerKillerStatus.PKLite) return; var totalHealth = DamageHistory.TotalHealth; if (totalHealth == 0) return; foreach (var kvp in DamageHistory.TotalDamage) { var damager = kvp.Value.TryGetAttacker(); var playerDamager = damager as Player; if (playerDamager == null && kvp.Value.PetOwner != null) playerDamager = kvp.Value.TryGetPetOwner(); if (playerDamager == null) continue; var totalDamage = kvp.Value.TotalDamage; var damagePercent = totalDamage / totalHealth; var totalXP = (XpOverride ?? 0) * damagePercent; playerDamager.EarnXP((long)Math.Round(totalXP), XpType.Kill); // handle luminance if (LuminanceAward != null) { var totalLuminance = (long)Math.Round(LuminanceAward.Value * damagePercent); playerDamager.EarnLuminance(totalLuminance, XpType.Kill); } } } /// /// Handles the KillTask for a killed creature /// public void OnDeath_HandleKillTask(string killQuest) { /*var receivers = KillTask_GetEligibleReceivers(killQuest); foreach (var receiver in receivers) { var damager = receiver.Value.TryGetAttacker(); var player = damager as Player; if (player == null && receiver.Value.PetOwner != null) player = receiver.Value.TryGetPetOwner(); if (player != null) player.QuestManager.HandleKillTask(killQuest, this); }*/ // new method // with full fellowship support and new config option for capping, // building a pre-flattened structure is no longer really necessary, // and we can do this more iteratively. // one caveat to do this, we need to keep track of player and summoning caps separately // this is to prevent ordering bugs, such as a player being processed after a summon, // and already being at the 1 cap for players var summon_credit_cap = (int)PropertyManager.GetLong("summoning_killtask_multicredit_cap").Item - 1; var playerCredits = new Dictionary(); var summonCredits = new Dictionary(); // this option isn't really needed anymore, but keeping it around for compatibility // it is now synonymous with summoning_killtask_multicredit_cap <= 1 if (!PropertyManager.GetBool("allow_summoning_killtask_multicredit").Item) summon_credit_cap = 0; foreach (var kvp in DamageHistory.TotalDamage) { if (kvp.Value.TotalDamage <= 0) continue; var damager = kvp.Value.TryGetAttacker(); var combatPet = false; var playerDamager = damager as Player; if (playerDamager == null && kvp.Value.PetOwner != null) { playerDamager = kvp.Value.TryGetPetOwner(); combatPet = true; } if (playerDamager == null) continue; var killTaskCredits = combatPet ? summonCredits : playerCredits; var cap = combatPet ? summon_credit_cap : 1; if (cap <= 0) { // handle special case: use playerCredits killTaskCredits = playerCredits; cap = 1; } if (playerDamager.QuestManager.HasQuest(killQuest)) { TryHandleKillTask(playerDamager, killQuest, killTaskCredits, cap); } // check option that requires killer to have killtask to pass to fellows else if (!PropertyManager.GetBool("fellow_kt_killer").Item) { continue; } if (playerDamager.Fellowship == null) continue; // share with fellows in kill task range var fellows = playerDamager.Fellowship.WithinRange(playerDamager); foreach (var fellow in fellows) { if (fellow.QuestManager.HasQuest(killQuest)) TryHandleKillTask(fellow, killQuest, killTaskCredits, cap); } } } public bool TryHandleKillTask(Player player, string killTask, Dictionary killTaskCredits, int cap) { if (killTaskCredits.TryGetValue(player.Guid, out var currentCredits)) { if (currentCredits >= cap) return false; killTaskCredits[player.Guid]++; } else killTaskCredits[player.Guid] = 1; player.QuestManager.HandleKillTask(killTask, this); return true; } /// /// Returns a flattened structure of eligible Players, Fellows, and CombatPets /// public Dictionary KillTask_GetEligibleReceivers(string killQuest) { // http://acpedia.org/wiki/Announcements_-_2012/12_-_A_Growing_Twilight#Release_Notes var questName = QuestManager.GetQuestName(killQuest); // we are using DamageHistoryInfo here, instead of Creature or WorldObjectInfo // WeakReference may be null for expired CombatPets, but we still need the WeakReference references var receivers = new Dictionary(); foreach (var kvp in DamageHistory.TotalDamage) { if (kvp.Value.TotalDamage <= 0) continue; var damager = kvp.Value.TryGetAttacker(); var playerDamager = damager as Player; if (playerDamager == null && kvp.Value.PetOwner != null) { // handle combat pets playerDamager = kvp.Value.TryGetPetOwner(); if (playerDamager != null && playerDamager.QuestManager.HasQuest(questName)) { // only add combat pet to eligible receivers if player has quest, and allow_summoning_killtask_multicredit = true (default, retail) if (DamageHistory.HasDamager(playerDamager, true) && PropertyManager.GetBool("allow_summoning_killtask_multicredit").Item) receivers[kvp.Value.Guid] = kvp.Value; // add CombatPet else receivers[playerDamager.Guid] = new DamageHistoryInfo(playerDamager); // add dummy profile for PetOwner } // regardless if combat pet is eligible, we still want to continue traversing to the pet owner, and possibly fellows // in a scenario where combat pet does 100% damage: // - regardless if allow_summoning_killtask_multicredit is enabled/disabled, it should continue traversing into pet owner and possibly their fellows // - if pet owner doesn't have kill task, and fellow_kt_killer=false, any fellows with the task should still receive 1 credit } if (playerDamager == null) continue; // factors: // - has quest // - is killer (last damager, top damager, or any damager? in current context, considering it to be any damager) // - has fellowship // - server option: fellow_kt_killer // - server option: fellow_kt_landblock if (playerDamager.QuestManager.HasQuest(questName)) { // just add a fake DamageHistoryInfo for reference receivers[playerDamager.Guid] = new DamageHistoryInfo(playerDamager); } else if (PropertyManager.GetBool("fellow_kt_killer").Item) { // if this option is enabled (retail default), the killer is required to have kill task // for it to share with fellowship continue; } // we want to add fellowship members in a flattened structure // in this inner loop, instead of the outer loop // scenarios: // i am a summoner in a fellowship with 1 other player // we both have a killtask // - my combatpet does 100% damage to the monster // result: i get 1 killtask credit, and my fellow gets 1 killtask credit // - my combatpet does 50% damage to monster, and i do 50% damage // result: i get 2 killtask credits (1 if allow_summoning_killtask_multicredit server option is disabled), and my fellow gets 1 killtask credit // after update should be 2/2, instead of 2/1 // - my combatpet does 33% damage to monster, i do 33% damage, and fellow does 33% damage // result: same as previous scenario // after update should be 2/2, instead of 2/1 again // 2 players not in a fellowship both have a killtask // they each do 50% damage to monster // result: both players receive killtask credit if (playerDamager.Fellowship == null) continue; // share with fellows in kill task range var fellows = playerDamager.Fellowship.WithinRange(playerDamager); foreach (var fellow in fellows) { if (fellow.QuestManager.HasQuest(questName)) receivers[fellow.Guid] = new DamageHistoryInfo(fellow); } } return receivers; } /// /// Create a corpse for both creatures and players currently /// protected void CreateCorpse(DamageHistoryInfo killer, bool hadVitae = false) { if (NoCorpse) { if (killer != null && killer.IsOlthoiPlayer) return; var loot = GenerateTreasure(killer, null); foreach(var item in loot) { if (!string.IsNullOrEmpty(item.Quest)) // if the item has a Quest string, make the creature a "generator" of the item so that the pickup action applies the quest. item.GeneratorId = Guid.Full; item.Location = new Position(Location); LandblockManager.AddObject(item); } return; } var cachedWeenie = DatabaseManager.World.GetCachedWeenie("corpse"); var corpse = WorldObjectFactory.CreateNewWorldObject(cachedWeenie) as Corpse; var prefix = "Corpse"; if (TreasureCorpse) { // Hardcoded values from PCAPs of Treasure Pile Corpses, everything else lines up exactly with existing corpse weenie corpse.SetupTableId = 0x02000EC4; corpse.MotionTableId = 0x0900019B; corpse.SoundTableId = 0x200000C2; corpse.ObjScale = 0.4f; prefix = "Treasure"; } else { corpse.SetupTableId = SetupTableId; corpse.MotionTableId = MotionTableId; //corpse.SoundTableId = SoundTableId; // Do not change sound table for corpses corpse.PaletteBaseDID = PaletteBaseDID; corpse.ClothingBase = ClothingBase; corpse.PhysicsTableId = PhysicsTableId; if (ObjScale.HasValue) corpse.ObjScale = ObjScale; if (PaletteTemplate.HasValue) corpse.PaletteTemplate = PaletteTemplate; if (Shade.HasValue) corpse.Shade = Shade; //if (Translucency.HasValue) // Shadows have Translucency but their corpses do not, videographic evidence can be found on YouTube. //corpse.Translucency = Translucency; // Pull and save objdesc for correct corpse apperance at time of death var objDesc = CalculateObjDesc(); corpse.Biota.PropertiesAnimPart = objDesc.AnimPartChanges.Clone(corpse.BiotaDatabaseLock); corpse.Biota.PropertiesPalette = objDesc.SubPalettes.Clone(corpse.BiotaDatabaseLock); corpse.Biota.PropertiesTextureMap = objDesc.TextureChanges.Clone(corpse.BiotaDatabaseLock); } // use the physics location for accuracy, // especially while jumping corpse.Location = PhysicsObj.Position.ACEPosition(); corpse.VictimId = Guid.Full; corpse.Name = $"{prefix} of {Name}"; // set 'killed by' for looting rights var killerName = "misadventure"; if (killer != null) { if (!(Generator != null && Generator.Guid == killer.Guid) && Guid != killer.Guid) { if (!string.IsNullOrWhiteSpace(killer.Name)) killerName = killer.Name.TrimStart('+'); // vtank requires + to be stripped for regex matching. corpse.KillerId = killer.Guid.Full; if (killer.PetOwner != null) { var petOwner = killer.TryGetPetOwner(); if (petOwner != null) corpse.KillerId = petOwner.Guid.Full; } } } corpse.LongDesc = $"Killed by {killerName}."; bool saveCorpse = false; var player = this as Player; if (player != null) { corpse.SetPosition(PositionType.Location, corpse.Location); var killerIsOlthoiPlayer = killer != null && killer.IsOlthoiPlayer; var killerIsPkPlayer = killer != null && killer.IsPlayer && killer.Guid != Guid; //var dropped = killer != null && killer.IsOlthoiPlayer ? player.CalculateDeathItems_Olthoi(corpse, hadVitae) : player.CalculateDeathItems(corpse); if (killerIsOlthoiPlayer || player.IsOlthoiPlayer) { var dropped = player.CalculateDeathItems_Olthoi(corpse, hadVitae, killerIsOlthoiPlayer, killerIsPkPlayer); foreach (var wo in dropped) DoCantripLogging(killer, wo); corpse.RecalculateDecayTime(player); if (dropped.Count > 0) saveCorpse = true; corpse.PkLevel = PKLevel.PK; } else { var dropped = player.CalculateDeathItems(corpse); corpse.RecalculateDecayTime(player); if (dropped.Count > 0) saveCorpse = true; if ((player.Location.Cell & 0xFFFF) < 0x100) { player.SetPosition(PositionType.LastOutsideDeath, new Position(corpse.Location)); player.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePosition(player, PositionType.LastOutsideDeath, corpse.Location)); if (dropped.Count > 0) player.Session.Network.EnqueueSend(new GameMessageSystemChat($"Your corpse is located at ({corpse.Location.GetMapCoordStr()}).", ChatMessageType.Broadcast)); } var isPKdeath = player.IsPKDeath(killer); var isPKLdeath = player.IsPKLiteDeath(killer); if (isPKdeath) corpse.PkLevel = PKLevel.PK; if (!isPKdeath && !isPKLdeath) { var miserAug = player.AugmentationLessDeathItemLoss * 5; if (miserAug > 0) player.Session.Network.EnqueueSend(new GameMessageSystemChat($"Your augmentation has reduced the number of items you can lose by {miserAug}!", ChatMessageType.Broadcast)); } if (dropped.Count == 0 && !isPKLdeath) player.Session.Network.EnqueueSend(new GameMessageSystemChat($"You have retained all your items. You do not need to recover your corpse!", ChatMessageType.Broadcast)); } } else { corpse.IsMonster = true; if (killer == null || !killer.IsOlthoiPlayer) GenerateTreasure(killer, corpse); else GenerateTreasure_Olthoi(killer, corpse); if (killer != null && killer.IsPlayer && !killer.IsOlthoiPlayer) { if (Level >= 100) { CanGenerateRare = true; } else { var killerPlayer = killer.TryGetAttacker(); if (killerPlayer != null && Level > killerPlayer.Level) CanGenerateRare = true; } } else CanGenerateRare = false; } corpse.RemoveProperty(PropertyInt.Value); if (CanGenerateRare && killer != null) corpse.TryGenerateRare(killer); corpse.InitPhysicsObj(); // persist the original creature velocity (only used for falling) to corpse corpse.PhysicsObj.Velocity = PhysicsObj.Velocity; corpse.EnterWorld(); if (player != null) { if (corpse.PhysicsObj == null || corpse.PhysicsObj.Position == null) log.InfoFormat("[CORPSE] {0}'s corpse (0x{1}) failed to spawn! Tried at {2}", Name, corpse.Guid, player.Location.ToLOCString()); else log.InfoFormat("[CORPSE] {0}'s corpse (0x{1}) is located at {2}", Name, corpse.Guid, corpse.PhysicsObj.Position); } if (saveCorpse) { corpse.SaveBiotaToDatabase(); foreach (var item in corpse.Inventory.Values) item.SaveBiotaToDatabase(); } } public bool CanGenerateRare { get => GetProperty(PropertyBool.CanGenerateRare) ?? false; set { if (!value) RemoveProperty(PropertyBool.CanGenerateRare); else SetProperty(PropertyBool.CanGenerateRare, value); } } /// /// Transfers generated treasure from creature to corpse /// private List GenerateTreasure(DamageHistoryInfo killer, Corpse corpse) { var droppedItems = new List(); // create death treasure from loot generation factory if (DeathTreasure != null) { List items = LootGenerationFactory.CreateRandomLootObjects(DeathTreasure); foreach (WorldObject wo in items) { if (corpse != null) corpse.TryAddToInventory(wo); else droppedItems.Add(wo); DoCantripLogging(killer, wo); } } // move wielded treasure over, which also should include Wielded objects not marked for destroy on death. // allow server operators to configure this behavior due to errors in createlist post 16py data var dropFlags = PropertyManager.GetBool("creatures_drop_createlist_wield").Item ? DestinationType.WieldTreasure : DestinationType.Treasure; var wieldedTreasure = Inventory.Values.Concat(EquippedObjects.Values).Where(i => (i.DestinationType & dropFlags) != 0); foreach (var item in wieldedTreasure.ToList()) { if (item.Bonded == BondedStatus.Destroy) continue; if (TryDequipObjectWithBroadcasting(item.Guid, out var wo, out var wieldedLocation)) EnqueueBroadcast(new GameMessagePublicUpdateInstanceID(item, PropertyInstanceId.Wielder, ObjectGuid.Invalid)); if (corpse != null) { corpse.TryAddToInventory(item); EnqueueBroadcast(new GameMessagePublicUpdateInstanceID(item, PropertyInstanceId.Container, corpse.Guid), new GameMessagePickupEvent(item)); } else droppedItems.Add(item); } // contain and non-wielded treasure create if (Biota.PropertiesCreateList != null) { var createList = Biota.PropertiesCreateList.Where(i => (i.DestinationType & DestinationType.Contain) != 0 || (i.DestinationType & DestinationType.Treasure) != 0 && (i.DestinationType & DestinationType.Wield) == 0).ToList(); var selected = CreateListSelect(createList); foreach (var item in selected) { var wo = WorldObjectFactory.CreateNewWorldObject(item); if (wo != null) { if (corpse != null) corpse.TryAddToInventory(wo); else droppedItems.Add(wo); } } } return droppedItems; } /// /// Generates random amounts of slag on a corpse /// when an OlthoiPlayer is the killer /// private void GenerateTreasure_Olthoi(DamageHistoryInfo killer, Corpse corpse) { if (DeathTreasure == null) return; var slag = LootGenerationFactory.RollSlag(DeathTreasure); if (slag == null) return; corpse.TryAddToInventory(slag); } public void DoCantripLogging(DamageHistoryInfo killer, WorldObject wo) { var epicCantrips = wo.EpicCantrips; var legendaryCantrips = wo.LegendaryCantrips; if (epicCantrips.Count > 0 && log.IsDebugEnabled) log.Debug($"[LOOT][EPIC] {Name} ({Guid}) generated item with {epicCantrips.Count} epic{(epicCantrips.Count > 1 ? "s" : "")} - {wo.Name} ({wo.Guid}) - {GetSpellList(epicCantrips)} - killed by {killer?.Name} ({killer?.Guid})"); if (legendaryCantrips.Count > 0 && log.IsDebugEnabled) log.Debug($"[LOOT][LEGENDARY] {Name} ({Guid}) generated item with {legendaryCantrips.Count} legendar{(legendaryCantrips.Count > 1 ? "ies" : "y")} - {wo.Name} ({wo.Guid}) - {GetSpellList(legendaryCantrips)} - killed by {killer?.Name} ({killer?.Guid})"); } public static string GetSpellList(Dictionary spellTable) { var spells = new List(); foreach (var kvp in spellTable) spells.Add(new Server.Entity.Spell(kvp.Key, false)); return string.Join(", ", spells.Select(i => i.Name)); } public bool IsOnNoDeathXPLandblock => Location != null ? NoDeathXP_Landblocks.Contains(Location.LandblockId.Landblock) : false; /// /// A list of landblocks the player gains no xp from creature kills /// public static HashSet NoDeathXP_Landblocks = new HashSet() { 0x00B0, // Colosseum Arena One 0x00B1, // Colosseum Arena Two 0x00B2, // Colosseum Arena Three 0x00B3, // Colosseum Arena Four 0x00B4, // Colosseum Arena Five 0x5960, // Gauntlet Arena One (Celestial Hand) 0x5961, // Gauntlet Arena Two (Celestial Hand) 0x5962, // Gauntlet Arena One (Eldritch Web) 0x5963, // Gauntlet Arena Two (Eldritch Web) 0x5964, // Gauntlet Arena One (Radiant Blood) 0x5965, // Gauntlet Arena Two (Radiant Blood) 0x596B, // Gauntlet Staging Area (All Societies) }; } }