ace/Source/ACE.Server/WorldObjects/Creature_Death.cs
Coldeve-ACE a31112ea51
Common Logging Changes (#4345)
* Update ShardDatabaseOfflineTools.cs

* Update Corpse.cs

* Update Creature_Death.cs

* Update Player_Death.cs

* Update Player_Inventory.cs

* Update WorldObject_Decay.cs

* Update HouseManager.cs

* Update Player_House.cs

* Update RecipeManager.cs

* Update Player_Allegiance.cs

* Update AuthenticationDatabase.cs

* Update ShardDatabase.cs

* Update WorldDatabase.cs

* Update Player_Magic.cs

* Update Player_Skills.cs

* Update Player_Attributes.cs

* Update Player_Combat.cs

* Update PhysicsObj.cs

* Update Player_Vitals.cs

* Update Player_Missile.cs

* Update Player_Melee.cs

* Update Monster_Melee.cs

* Update Session.cs
2025-08-11 22:38:12 -04:00

770 lines
30 KiB
C#

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;
/// <summary>
/// Called when a monster or 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>
/// <param name="criticalHit">True if the death blow was a critical hit, generates a critical death message</param>
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;
}
/// <summary>
/// Kills a player/creature and performs the full death sequence
/// </summary>
public void Die()
{
Die(DamageHistory.LastDamager, DamageHistory.TopDamager);
}
private bool dieEntered = false;
/// <summary>
/// Performs the full death sequence for non-Player creatures
/// </summary>
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<MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.Dead);
dieChain.AddDelaySeconds(deathAnimLength);
dieChain.AddAction(this, () =>
{
CreateCorpse(topDamager);
Destroy();
});
dieChain.EnqueueChain();
}
/// <summary>
/// Called when an admin player uses the /smite command
/// to instantly kill a creature
/// </summary>
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);
}
/// <summary>
/// Grants XP to players in damage history
/// </summary>
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);
}
}
}
/// <summary>
/// Handles the KillTask for a killed creature
/// </summary>
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<ObjectGuid, int>();
var summonCredits = new Dictionary<ObjectGuid, int>();
// 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<ObjectGuid, int> 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;
}
/// <summary>
/// Returns a flattened structure of eligible Players, Fellows, and CombatPets
/// </summary>
public Dictionary<ObjectGuid, DamageHistoryInfo> 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<CombatPet> may be null for expired CombatPets, but we still need the WeakReference<PetOwner> references
var receivers = new Dictionary<ObjectGuid, DamageHistoryInfo>();
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;
}
/// <summary>
/// Create a corpse for both creatures and players currently
/// </summary>
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); }
}
/// <summary>
/// Transfers generated treasure from creature to corpse
/// </summary>
private List<WorldObject> GenerateTreasure(DamageHistoryInfo killer, Corpse corpse)
{
var droppedItems = new List<WorldObject>();
// create death treasure from loot generation factory
if (DeathTreasure != null)
{
List<WorldObject> 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;
}
/// <summary>
/// Generates random amounts of slag on a corpse
/// when an OlthoiPlayer is the killer
/// </summary>
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<int, float> spellTable)
{
var spells = new List<Server.Entity.Spell>();
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;
/// <summary>
/// A list of landblocks the player gains no xp from creature kills
/// </summary>
public static HashSet<ushort> NoDeathXP_Landblocks = new HashSet<ushort>()
{
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)
};
}
}