using System;
using System.Collections.Generic;
using System.Linq;
using ACE.Common;
using ACE.DatLoader;
using ACE.DatLoader.FileTypes;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Factories;
using ACE.Server.Managers;
using ACE.Server.Network.Structure;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
namespace ACE.Server.WorldObjects
{
partial class Player
{
///
/// A list of players who have granted corpse looting permissions
/// with /permit
///
public Dictionary LootPermission;
///
/// Called when a player dies, in conjunction with Die()
///
/// The last damager that landed the death blow
/// The damage type for the death message
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.Info("[CORPSE] " + nearbyMsg);
var excludePlayers = new List();
var nearbyPlayers = EnqueueBroadcast(excludePlayers, true, 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);
// We enqueue the work onto the target landblock to ensure thread-safety. It's highly likely the lifestoneBlock is far away, and part of a different landblock group (and thus different thread).
lifestoneBlock.EnqueueAction(new ActionEventDelegate(() => lifestoneBlock.EnqueueBroadcast(excludePlayers, true, Sanctuary, LocalBroadcastRangeSq, broadcastMsg)));
}
return deathMessage;
}
public void HandlePKDeathBroadcast(DamageHistoryInfo lastDamager, DamageHistoryInfo topDamager)
{
if (topDamager == null || !topDamager.IsPlayer)
return;
var pkPlayer = topDamager.TryGetAttacker() as Player;
if (pkPlayer == null)
return;
if (IsPKDeath(topDamager))
{
pkPlayer.PkTimestamp = Time.GetUnixTime();
pkPlayer.PlayerKillsPk++;
var globalPKDe = $"{lastDamager.Name} has defeated {Name}!";
if ((Location.Cell & 0xFFFF) < 0x100)
globalPKDe += $" The kill occured at {Location.GetMapCoordStr()}";
globalPKDe += "\n[PKDe]";
PlayerManager.BroadcastToAll(new GameMessageSystemChat(globalPKDe, ChatMessageType.Broadcast));
}
else if (IsPKLiteDeath(topDamager))
pkPlayer.PlayerKillsPkl++;
}
///
/// Inflicts vitae
///
public void InflictVitaePenalty(int amount = 5)
{
DeathLevel = Level; // for calculating vitae XP
VitaeCpPool = 0; // reset vitae XP earned
var msgDeathLevel = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.DeathLevel, DeathLevel ?? 0);
var msgVitaeCpPool = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.VitaeCpPool, VitaeCpPool.Value);
Session.Network.EnqueueSend(msgDeathLevel, msgVitaeCpPool);
var vitae = EnchantmentManager.UpdateVitae();
var spellID = (uint)SpellId.Vitae;
var spell = new Spell(spellID);
var vitaeEnchantment = new Enchantment(this, Guid.Full, spellID, 0, (EnchantmentMask)spell.StatModType, vitae);
Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(Session, vitaeEnchantment));
}
public bool IsInDeathProcess;
///
/// 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
///
protected override void Die(DamageHistoryInfo lastDamager, DamageHistoryInfo topDamager)
{
IsInDeathProcess = true;
if (topDamager?.Guid == Guid && IsPKType)
{
var topDamagerOther = DamageHistory.GetTopDamager(false);
if (topDamagerOther != null && topDamagerOther.IsPlayer)
topDamager = topDamagerOther;
}
UpdateVital(Health, 0);
NumDeaths++;
suicideInProgress = false;
// todo: since we are going to be using 'time since Player last died to an OlthoiPlayer'
// as a factor in slag generation, this will eventually be moved to after the slag generation
//if (topDamager != null && topDamager.IsOlthoiPlayer)
//OlthoiLootTimestamp = (int)Time.GetUnixTime();
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);
}
var hadVitae = HasVitae;
// 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);
}
else
{
var msgPurgeBadEnchantments = new GameEventMagicPurgeBadEnchantments(Session);
EnchantmentManager.RemoveAllBadEnchantments();
Session.Network.EnqueueSend(msgPurgeBadEnchantments, new GameMessageSystemChat("Your augmentation prevents the tides of death from ripping away your current enchantments!", ChatMessageType.Broadcast));
}
// wait for the death animation to finish
var dieChain = new ActionChain();
var animLength = DatManager.PortalDat.ReadFromDat(MotionTableId).GetAnimationLength(MotionCommand.Dead);
dieChain.AddDelaySeconds(animLength + 1.0f);
dieChain.AddAction(this, () =>
{
CreateCorpse(topDamager, hadVitae);
ThreadSafeTeleportOnDeath(); // enter portal space
if (IsPKDeath(topDamager) || IsPKLiteDeath(topDamager))
SetMinimumTimeSincePK();
IsBusy = false;
});
dieChain.EnqueueChain();
}
///
/// Called when the player enters portal space after dying
///
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 bool suicideInProgress;
///
/// Called when player uses the /die command
///
public void HandleActionDie()
{
if (IsDead || Teleporting)
{
Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YoureTooBusy));
return;
}
if (suicideInProgress)
return;
suicideInProgress = true;
if (PropertyManager.GetBool("suicide_instant_death").Item)
Die(new DamageHistoryInfo(this), DamageHistory.TopDamager);
else
HandleSuicide(NumDeaths);
}
private static List SuicideMessages = new List()
{
"I feel faint...",
"My sight is growing dim...",
"My life is flashing before my eyes...",
"I see a light...",
"Oh cruel, cruel world!"
};
private void HandleSuicide(int numDeaths, int step = 0)
{
if (!suicideInProgress || numDeaths != NumDeaths)
return;
if (step < SuicideMessages.Count)
{
EnqueueBroadcast(new GameMessageHearSpeech(SuicideMessages[step], GetNameWithSuffix(), Guid.Full, ChatMessageType.Speech), LocalBroadcastRange);
OnTalk(SuicideMessages[step]);
var suicideChain = new ActionChain();
suicideChain.AddDelaySeconds(3.0f);
suicideChain.AddAction(this, () => HandleSuicide(numDeaths, step + 1));
suicideChain.EnqueueChain();
}
else
Die(new DamageHistoryInfo(this), DamageHistory.TopDamager);
}
public List CalculateDeathItems(Corpse corpse)
{
// https://web.archive.org/web/20140712134108/http://support.turbine.com/link/portal/24001/24001/Article/464/How-do-death-items-work-in-Asheron-s-Call-Could-you-explain-how-the-game-decides-what-you-drop-when-you-die-in-Asheron-s-Call
// Original formula:
// - When you are level 5 or under, you don't drop anything when you die.
// - From level 6 to level 10, you lose half your coins (not trade notes) and nothing else.
// - From level 11 to level 20, you lose half your coins and possibly one non-wielded item (that is, something that you were neither wearing nor holding in your hands).
// - From level 21 to level 35, you lose half your coins and some number of non-wielded items.
// - After level 35, you lose half your coins and some number of items. At this point, you can drop items that you were wearing or holding.
// Now, in those last two cases, I said 'some number'. Some number here is equal to your level divided by 10, rounded down, plus a random number between 0 and 2.
// So from level 21 to 29 you can lose between 2 and 4 items; from level 30 to 39 you can lose 3-5 items; from 40-49 you can lose 4-6 items, and so forth.
// By level 126 you can be losing up to 14 items.
// (one caveat here: if you were killed in a PK battle, you always lose items as if you were over level 35, although the exact number you lose
// is still determined by your real level / 10. In other words, PK deaths do not get the special protection from item loss that NPK deaths get under level 35.)
// So that's how many items you lose on death -- but how do we determine which items are lost? This is where the categories come into it.
// Each item in the game has a particular item type associated with it. The actual types (categories) are listed below.
// When we are deciding what items you drop on death, we make a list of all the items you are carrying, sorted by value (high value first).
// But we may adjust their values in two ways. If the item is not the most valuable item in that category, then we cut its value in half.
// And whether or not it is the most valuable item in its category, we randomize its value a little bit to mix things up.
// (To answer your first question explicitly, the cutting-the-value-in-half is not cumulative -- the second- and third- and fourth-most valuable items
// of one category all have their values halved, not halved and then quartered and then eighthed).
// Now note that we still keep these things in a list sorted by adjusted value -- so its possible to have your two expensive weapons
// listed first and second, if the value of the second weapon cut in half is still higher than the value of the third item. What I am trying to get at here is
// that we do not segregate the list based on item type; we only use item type to determine how we adjust the value of the item.
// Finally, we go down the list and mark the first # things as dropped, where # is the number of items we have calculated that you are going to drop this death.
// For instance, if you are level 48, you will drop the first 4-6 items on that list.
// The categories of items are:
// - Melee weapons
// - Missile weapons
// - Magic casters (like orbs & wands)
// - Armor
// - Clothing
// - Jewelry
// - Food
// - Gems
// - Components
// - Mana stones
// - Crafting ingredients
// - Parchments & books
// - Keys
// - Tradenotes
// - Miscellaneous
// A few categories are left out such as lifestones -- because you aren't likely to be carrying an item of that type.
// http://asheron.wikia.com/wiki/Death_Penalty
// The first item to drop will be the highest value item regardless of type.
// All items of the same type will now be counted at half their face value. This process repeats (next highest value item,
// if an item of the same type has already dropped, the face value is halved.)
// The number of items you drop can be reduced with the Clutch of the Miser augmentation. If you get the
// augmentation three times you will no longer drop any items (except half of your Pyreals and all Rares except if you're a PK).
// If you drop no items, you will not leave a corpse.
// Some players use gems, particularly the Archmage Portal Gems as death items, so it's important to note how
// stackable items work with the death system. When you die, a stack is considered to be one item for the purposes
// of death items. However if that stack is selected, only one item off the stack will drop. For example, let's say you
// can currently cover all of your items with 5 Portal Gems. After the event, if you do not stack these gems, there
// will be no change. However, if you combine the 5 Gems into a single stack, you would drop 1 gem and 4 other items on death.
// When you die there is a notification in your chat dialog stating what items you dropped, and your corpse location
// (if on the landscape). If you do not leave a corpse the notification will state so.
// Accounts with the Throne of Destiny expansion received a new formula for death items to prevent losing a
// pack per death at high levels. The formula was changed to divide by 20 levels instead of 10, thereby making
// the maximum dropped items the same.
// Tier 1 and 2 Rares will always drop, and do not count toward your death item count, so store them in a safe place.
// PKLite - no items or pyreals are lost on PKL death.
// Death items
// You can protect important items like your armor and weapons by carrying death items. These are items that are
// ideally light weight as well as high in value. You can tinker loot items with bags of Salvaged Gold to raise their
// face value. Although tinkering special items can remove some of the beneift, as you feel obligated to recover
// them. Items that can be purchased can just be left if the corpse would be too difficult to recover.
// Popular items to use as Death Items:
// - Massive Mana Charges (most common death item)
// - Pristine Mana Shards
// - Crowns (tinkered with gold)
// - Robes sold by the Mastermages
// - Nanner Island Portal Gems (somewhat difficult to obtain)
// Bonded items:
// Items that have the Special Property Bonded will never drop on death. Some notable bonded items include:
// - Academy Weapons (Starter weapons)
// - Augmentation Gems (Asheron's Benediction and Blackmoor's Favor)
// - Pathwarden Armor (Starter armor)
// - Trade Notes
// http://acpedia.org/wiki/Recovering_from_Death
// Recovering from Death
// Fortunately, you won't be in immediate danger of being killed again, because you're invulnerable to attack for a full minute if you don't attack another
// creature of cast any spells. Also, your secondary attributes, even if all had been reduced to 0 when you died, will be at 75 percent of their new
// maximum score, and any poisons or harmful enchantments afflicting you just before you died will be gone, giving you a fighting chance of making it
// back to town or meeting up with your allies.
// You can also find the location of your last corpse outdoors by typing @corpse.
// Corpse timer = Math.Max(1 hour, 5 mins * level)
// TODO: @permit and @consent commands
// You can have up to 20 people in your consent list at once. However, when you log off, all permissions to loot corpses will be removed.
// First, we sort the inventory by order of value.
// Second, we go back through the inventory starting at the most expensive item and look at each item's category.
// If we've seen the item category before, we divide the value of the item in half.
// At this point, we add a random 0-10% variance to each item's value.
// http://acpedia.org/wiki/Death_Item
// A Reign of Stone (April 2001) - Corpse permission commands added.
// The Changing of the Ways (May 2001) - Players are now given the coordinates of their characters' corpses when they die.
// Hidden Vein (May 2002) - Many changes made on the way item loss on death works. See http://acpedia.org/wiki/Hidden_Vein and http://acpedia.org/wiki/Announcements_-_2002/05_-_Hidden_Vein#Letter_to_the_Players for more details
// Throne of Destiny expansion (July 2005) - The formula used for working out how many items a character drops was updated. For details on the old formula and how it changed see http://acpedia.org/wiki/Announcements_-_2005/07_-_Throne_of_Destiny_(expansion)#FAQ_-_AC:TD_Level_Cap_Update
// if player dies in a PKLite battle,
// they don't drop any items, and revert back to NPK status
// if player dies on a No Drop landblock,
// they don't drop any items
if (corpse.IsOnNoDropLandblock || IsPKLiteDeath(corpse.KillerId))
return new List();
var numItemsDropped = GetNumItemsDropped(corpse);
var numCoinsDropped = GetNumCoinsDropped();
var level = Level ?? 1;
var canDropWielded = level >= 35;
// get all items in inventory
var inventory = GetAllPossessions();
// exclude pyreals from randomized death item calculation
inventory = inventory.Where(i => i.WeenieClassId != coinStackWcid).ToList();
// exclude wielded items if < level 35
if (!canDropWielded)
inventory = inventory.Where(i => i.CurrentWieldedLocation == null).ToList();
// exclude bonded items
inventory = inventory.Where(i => (i.GetProperty(PropertyInt.Bonded) ?? 0) == 0).ToList();
// handle items with BondedStatus.Destroy
var destroyedItems = HandleDestroyBonded();
// construct the list of death items
var sorted = new DeathItems(inventory);
var dropItems = new List();
if (numCoinsDropped > 0)
{
// add pyreals to dropped items
var pyreals = SpendCurrency(coinStackWcid, (uint)numCoinsDropped);
dropItems.AddRange(pyreals);
//Console.WriteLine($"Dropping {numCoinsDropped} pyreals");
}
// Remove the items from inventory
for (var i = 0; i < numItemsDropped && i < sorted.Inventory.Count; i++)
{
var deathItem = sorted.Inventory[i];
// split stack if needed
if ((deathItem.WorldObject.StackSize ?? 1) > 1)
{
var stack = FindObject(deathItem.WorldObject.Guid, SearchLocations.MyInventory | SearchLocations.MyEquippedItems, out var foundInContainer, out var rootContainer, out _);
if (stack != null)
{
AdjustStack(stack, -1, foundInContainer, rootContainer);
Session.Network.EnqueueSend(new GameMessageSetStackSize(stack));
var dropItem = WorldObjectFactory.CreateNewWorldObject(deathItem.WorldObject.WeenieClassId);
dropItem.SetStackSize(1);
//Console.WriteLine("Dropping " + deathItem.WorldObject.Name + " (stack)");
dropItems.Add(dropItem);
}
else
{
log.WarnFormat("Couldn't find death item stack 0x{0:X8}:{1} for player {2}", deathItem.WorldObject.Guid.Full, deathItem.WorldObject.Name, Name);
}
}
else
{
if (TryRemoveFromInventoryWithNetworking(deathItem.WorldObject.Guid, out _, RemoveFromInventoryAction.ToCorpseOnDeath) || TryDequipObjectWithNetworking(deathItem.WorldObject.Guid, out _, DequipObjectAction.ToCorpseOnDeath))
{
//Console.WriteLine("Dropping " + deathItem.WorldObject.Name);
dropItems.Add(deathItem.WorldObject);
}
else
{
log.WarnFormat("Couldn't find death item 0x{0:X8}:{1} for player {2}", deathItem.WorldObject.Guid.Full, deathItem.WorldObject.Name, Name);
}
}
}
// handle items with BondedStatus.Slippery: always drop on death
var slipperyItems = GetSlipperyItems();
foreach (var item in slipperyItems)
{
if (TryRemoveFromInventoryWithNetworking(item.Guid, out _, RemoveFromInventoryAction.ToCorpseOnDeath) || TryDequipObjectWithNetworking(item.Guid, out _, DequipObjectAction.ToCorpseOnDeath))
dropItems.Add(item);
}
var destroyCoins = PropertyManager.GetBool("corpse_destroy_pyreals").Item;
// add items to corpse
foreach (var dropItem in dropItems)
{
// coins already removed from SpendCurrency
if (destroyCoins && dropItem.WeenieType == WeenieType.Coin)
{
dropItem.Destroy();
continue;
}
if (!corpse.TryAddToInventory(dropItem))
{
log.Warn($"Player_Death: couldn't add item to {Name}'s corpse: {dropItem.Name}");
if (!TryAddToInventory(dropItem))
log.Warn($"Player_Death: couldn't re-add item to {Name}'s inventory: {dropItem.Name}");
}
}
// notify player of destroyed items?
dropItems.AddRange(destroyedItems);
// send network messages
var dropList = DropMessage(dropItems, numCoinsDropped);
if (!string.IsNullOrWhiteSpace(dropList))
{
Session.Network.EnqueueSend(new GameMessageSystemChat(dropList, ChatMessageType.Broadcast));
DeathItemLog(dropItems, corpse);
}
return dropItems;
}
public void DeathItemLog(List dropItems, Corpse corpse)
{
if (dropItems.Count == 0)
return;
var msg = $"[CORPSE] {Name} dropped items on corpse (0x{corpse.Guid}): ";
foreach (var dropItem in dropItems)
msg += $"{(dropItem.StackSize.HasValue && dropItem.StackSize > 1 ? dropItem.StackSize.Value.ToString("N0") + " " + dropItem.GetPluralName() : dropItem.Name)} (0x{dropItem.Guid}){(dropItem.WeenieClassId == 273 && PropertyManager.GetBool("corpse_destroy_pyreals").Item ? $" which {(dropItem.StackSize.HasValue && dropItem.StackSize > 1 ? "were" : "was")} destroyed" : "")}, ";
msg = msg.Substring(0, msg.Length - 2);
log.Info(msg);
}
///
/// The maximum # of items a player can drop
///
public const int MaxItemsDropped = 14;
///
/// Rolls for the # of items to drop for a player death
///
///
public int GetNumItemsDropped(Corpse corpse)
{
// Original formula:
// - When you are level 5 or under, you don't drop anything when you die.
// - From level 6 to level 10, you lose half your coins (not trade notes) and nothing else.
// - From level 11 to level 20, you lose half your coins and possibly one non-wielded item (that is, something that you were neither wearing nor holding in your hands).
// - From level 21 to level 35, you lose half your coins and some number of non-wielded items.
// - After level 35, you lose half your coins and some number of items. At this point, you can drop items that you were wearing or holding.
// Now, in those last two cases, I said 'some number'. Some number here is equal to your level divided by 10 (*20 after patch), rounded down, plus a random number between 0 and 2.
// So from level 21 to 29 you can lose between 2 and 4 items; from level 30 to 39 you can lose 3-5 items; from 40-49 you can lose 4-6 items, and so forth.
// By level 126 you can be losing up to 14 items.
// (one caveat here: if you were killed in a PK battle, you always lose items as if you were over level 35, although the exact number you lose
// is still determined by your real level / 10. In other words, PK deaths do not get the special protection from item loss that NPK deaths get under level 35.)
// So that's how many items you lose on death -- but how do we determine which items are lost? This is where the categories come into it.
// take augments into consideration?
var level = Level ?? 1;
if (level <= 10)
return 0;
if (level >= 11 && level <= 20)
return ThreadSafeRandom.Next(0, 1);
// level 21+
var numItemsDropped = (level / 20) + ThreadSafeRandom.Next(0, 2);
numItemsDropped = Math.Min(numItemsDropped, MaxItemsDropped); // is this really a max cap?
// The number of items you drop can be reduced with the Clutch of the Miser augmentation. If you get the
// augmentation three times you will no longer drop any items (except half of your Pyreals and all Rares except if you're a PK).
// If you drop no items, you will not leave a corpse.
if (!IsPKDeath(corpse.KillerId) && AugmentationLessDeathItemLoss > 0)
{
numItemsDropped = Math.Max(0, numItemsDropped - AugmentationLessDeathItemLoss * 5);
}
return numItemsDropped;
}
public int GetNumCoinsDropped()
{
// if level > 5, lose half coins
// (trade notes excluded)
var level = Level ?? 1;
var coins = CoinValue ?? 0;
var numCoinsDropped = level > 5 ? coins / 2 : 0;
return numCoinsDropped;
}
///
/// Builds the network text message for list of items dropped
///
public string DropMessage(List dropItems, int numCoinsDropped)
{
var msg = "";
var coinMsg = true;
for (var i = 0; i < dropItems.Count; i++)
{
var dropItem = dropItems[i];
var isCoin = dropItem.Name.Equals("Pyreal");
if (isCoin && !coinMsg)
continue;
if (i == 0)
msg += "You've lost ";
else
{
msg += ", ";
if (i == dropItems.Count - 1)
msg += "and ";
}
var stackSize = dropItem.StackSize ?? 1;
if (isCoin)
{
stackSize = numCoinsDropped;
coinMsg = false;
}
else
msg += "your ";
if (stackSize == 1)
msg += dropItem.Name;
else
msg += stackSize.ToString("N0") + " " + dropItem.GetPluralName();
}
if (msg.Length > 0)
msg += "!";
return msg;
}
public static TimeSpan PermitTime = TimeSpan.FromHours(1);
public void HandleActionAddPlayerPermission(string playerName)
{
// is this player online?
var player = PlayerManager.GetOnlinePlayer(playerName);
if (player == null)
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"{playerName} is not online.", ChatMessageType.Broadcast));
return;
}
// check for self-permit
if (Name.Equals(player.Name))
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"You already have permission to loot your corpse.", ChatMessageType.Broadcast));
return;
}
// verify other player has /consent on
if (!player.GetCharacterOption(CharacterOption.AcceptCorpseLootingPermissions))
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"{player.Name} is not accepting corpse looting permissions from other players.", ChatMessageType.Broadcast));
return;
}
// do they already have permission?
if (player.HasLootPermission(Guid))
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"{player.Name} already has permission to loot your corpse.", ChatMessageType.Broadcast));
return;
}
player.LootPermission.Add(Guid, DateTime.UtcNow + PermitTime);
// send messages to both players
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{Name} has given you permission to loot one of his or her corpses. This permission will last one hour.", ChatMessageType.Broadcast));
Session.Network.EnqueueSend(new GameMessageSystemChat($"You have given permission to {player.Name} to loot one of your corpses. This permission will last one hour.", ChatMessageType.Broadcast));
}
public void HandleActionRemovePlayerPermission(string playerName)
{
// is this player online?
var player = PlayerManager.GetOnlinePlayer(playerName);
// check for self-revoke
if (Name.Equals(player.Name))
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"You always have permission to loot your corpse.", ChatMessageType.Broadcast));
return;
}
// do they already have permission?
if (player == null || !player.HasLootPermission(Guid))
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"{player.Name} doesn't have permission to loot your corpse.", ChatMessageType.Broadcast));
return;
}
// remove looting permissions
player.LootPermission.Remove(Guid);
// send messages to both players
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{Name} has revoked permission to loot one of his or her corpses.", ChatMessageType.Broadcast));
Session.Network.EnqueueSend(new GameMessageSystemChat($"{player.Name}'s permission to loot your corpse has been revoked.", ChatMessageType.Broadcast));
}
///
/// Cleans out any expired permissions
///
public void PrunePermissions()
{
LootPermission = LootPermission.Where(p => p.Value >= DateTime.UtcNow).ToDictionary(p => p.Key, p => p.Value);
}
public bool HasLootPermission(ObjectGuid guid)
{
PrunePermissions();
return LootPermission.ContainsKey(guid);
}
public void HandleActionDisplayPlayerConsentList()
{
PrunePermissions();
if (LootPermission.Count == 0)
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"You do not have permission to loot anyone's corpse.", ChatMessageType.Broadcast));
return;
}
var playerNames = new List();
foreach (var playerGuid in LootPermission.Keys)
{
// is the granter required to stay online?
var player = PlayerManager.FindByGuid(playerGuid);
if (player == null)
{
Console.WriteLine($"{Name}.HandleActionDisplayPlayerConsentList(): couldn't find player guid {playerGuid}");
continue;
}
playerNames.Add(player.Name);
}
Session.Network.EnqueueSend(new GameMessageSystemChat("You have permissions to loot a corpse from these players:\n" + string.Join("\n", playerNames), ChatMessageType.Broadcast));
}
public void HandleActionClearPlayerConsentList()
{
PrunePermissions();
if (LootPermission.Count == 0)
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"You do not have permission to loot anyone's corpse.", ChatMessageType.Broadcast));
return;
}
LootPermission.Clear();
Session.Network.EnqueueSend(new GameMessageSystemChat($"You have cleared your consent list. Players will have to permit you again to allow you access to their corpse.", ChatMessageType.Broadcast));
}
///
/// A player can remove corpse looting permissions that were granted to them.
///
/// The granter name
public void HandleActionRemoveFromPlayerConsentList(string playerName)
{
var player = PlayerManager.FindByName(playerName);
if (player == null)
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"{playerName} is not online.", ChatMessageType.Broadcast));
return;
}
// check for self-revoke
if (Name.Equals(player.Name))
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"You always have permission to loot your corpse.", ChatMessageType.Broadcast));
return;
}
// do we have permissions?
if (!HasLootPermission(player.Guid))
{
Session.Network.EnqueueSend(new GameMessageSystemChat($"You don't have permission to loot {player.Name}'s corpse.", ChatMessageType.Broadcast));
return;
}
// remove looting permissions
LootPermission.Remove(player.Guid);
Session.Network.EnqueueSend(new GameMessageSystemChat($"You have removed your permissions to loot {player.Name}'s corpse.", ChatMessageType.Broadcast));
}
public bool UnderLifestoneProtection
{
get => GetProperty(PropertyBool.UnderLifestoneProtection) ?? false;
set { if (!value) RemoveProperty(PropertyBool.UnderLifestoneProtection); else SetProperty(PropertyBool.UnderLifestoneProtection, value); }
}
public double? LifestoneProtectionTimestamp
{
get => GetProperty(PropertyFloat.LifestoneProtectionTimestamp) ?? null;
set { if (!value.HasValue) RemoveProperty(PropertyFloat.LifestoneProtectionTimestamp); else SetProperty(PropertyFloat.LifestoneProtectionTimestamp, value.Value); }
}
public void SetLifestoneProtection()
{
UnderLifestoneProtection = true;
LifestoneProtectionTimestamp = 0;
}
public void HandleLifestoneProtection()
{
Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.LifestoneMagicProtectsYou));
EnqueueBroadcast(new GameMessageScript(Guid, PlayScript.ShieldUpBlue));
}
public static TimeSpan LifestoneProtectionTime = TimeSpan.FromMinutes(1);
public void LifestoneProtectionTick()
{
if (!UnderLifestoneProtection)
return;
LifestoneProtectionTimestamp += CachedHeartbeatInterval;
if (LifestoneProtectionTimestamp < LifestoneProtectionTime.TotalSeconds)
return;
UnderLifestoneProtection = false;
LifestoneProtectionTimestamp = null;
Session.Network.EnqueueSend(new GameMessageSystemChat("You're no longer protected by the Lifestone's magic!", ChatMessageType.Magic));
}
public void LifestoneProtectionDispel()
{
UnderLifestoneProtection = false;
LifestoneProtectionTimestamp = null;
Session.Network.EnqueueSend(new GameMessageSystemChat("Your actions have dispelled the Lifestone's magic!", ChatMessageType.Magic));
}
public double? MinimumTimeSincePk
{
get => GetProperty(PropertyFloat.MinimumTimeSincePk);
set { if (!value.HasValue) RemoveProperty(PropertyFloat.MinimumTimeSincePk); else SetProperty(PropertyFloat.MinimumTimeSincePk, value.Value); }
}
public void SetMinimumTimeSincePK()
{
if (IsOlthoiPlayer)
return;
if (PlayerKillerStatus == PlayerKillerStatus.NPK && MinimumTimeSincePk == null)
return;
var prevStatus = PlayerKillerStatus;
MinimumTimeSincePk = 0;
PlayerKillerStatus = PlayerKillerStatus.NPK;
if (prevStatus == PlayerKillerStatus.PK)
{
EnqueueBroadcast(new GameMessagePublicUpdatePropertyInt(this, PropertyInt.PlayerKillerStatus, (int)PlayerKillerStatus));
Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouAreTemporarilyNoLongerPK));
}
else if (prevStatus == PlayerKillerStatus.PKLite)
{
EnqueueBroadcast(new GameMessagePublicUpdatePropertyInt(this, PropertyInt.PlayerKillerStatus, (int)PlayerKillerStatus));
Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouAreNonPKAgain));
}
}
public void PK_DeathTick()
{
if (MinimumTimeSincePk == null || (PropertyManager.GetBool("pk_server_safe_training_academy").Item && RecallsDisabled))
return;
if (PkLevel == PKLevel.NPK && !PropertyManager.GetBool("pk_server").Item && !PropertyManager.GetBool("pkl_server").Item)
{
MinimumTimeSincePk = null;
return;
}
MinimumTimeSincePk += CachedHeartbeatInterval;
if (MinimumTimeSincePk < PropertyManager.GetDouble("pk_respite_timer").Item)
return;
MinimumTimeSincePk = null;
var werror = WeenieError.None;
var pkLevel = PkLevel;
if (PropertyManager.GetBool("pk_server").Item)
pkLevel = PKLevel.PK;
else if (PropertyManager.GetBool("pkl_server").Item)
pkLevel = PKLevel.PKLite;
switch (pkLevel)
{
case PKLevel.NPK:
return;
case PKLevel.PK:
PlayerKillerStatus = PlayerKillerStatus.PK;
werror = WeenieError.YouArePKAgain;
break;
case PKLevel.PKLite:
PlayerKillerStatus = PlayerKillerStatus.PKLite;
werror = WeenieError.YouAreNowPKLite;
break;
}
EnqueueBroadcast(new GameMessagePublicUpdatePropertyInt(this, PropertyInt.PlayerKillerStatus, (int)PlayerKillerStatus));
Session.Network.EnqueueSend(new GameEventWeenieError(Session, werror));
}
public List GetSlipperyItems()
{
var allPossessions = GetAllPossessions();
return allPossessions.Where(i => i.Bonded == BondedStatus.Slippery).ToList();
}
public List HandleDestroyBonded()
{
var destroyedItems = new List();
var allPossessions = GetAllPossessions();
foreach (var destroyItem in allPossessions.Where(i => i.Bonded == BondedStatus.Destroy).ToList())
{
TryConsumeFromInventoryWithNetworking(destroyItem, (destroyItem.StackSize ?? 1));
destroyedItems.Add(destroyItem);
}
return destroyedItems;
}
private static Database.Models.World.TreasureDeath OlthoiDeathTreasureType => Database.DatabaseManager.World.GetCachedDeathTreasure(2222) ?? new()
{
TreasureType = 2222,
Tier = 8,
LootQualityMod = 0,
UnknownChances = 19,
ItemChance = 100,
ItemMinAmount = 1,
ItemMaxAmount = 2,
ItemTreasureTypeSelectionChances = 8,
MagicItemChance = 100,
MagicItemMinAmount = 2,
MagicItemMaxAmount = 3,
MagicItemTreasureTypeSelectionChances = 8,
MundaneItemChance = 100,
MundaneItemMinAmount = 0,
MundaneItemMaxAmount = 1,
MundaneItemTypeSelectionChances = 7
};
///
/// Determines the amount of slag to drop on a Player corpse when killed by an OlthoiPlayer or the loot to drop when an OlthoiPlayer is killed by a Player Killer
///
public List CalculateDeathItems_Olthoi(Corpse corpse, bool hadVitae, bool killerIsOlthoiPlayer, bool killerIsPkPlayer)
{
if (killerIsOlthoiPlayer)
{
var slag = LootGenerationFactory.RollSlag(this, hadVitae);
if (slag == null)
return new();
if (!corpse.TryAddToInventory(slag))
log.Warn($"CalculateDeathItems_Olthoi: couldn't add item to {Name}'s corpse: {slag.Name}");
return new() { slag };
}
else if (killerIsPkPlayer)
{
if (hadVitae)
return new();
var items = LootGenerationFactory.CreateRandomLootObjects(OlthoiDeathTreasureType);
var gland = LootGenerationFactory.RollGland(this, hadVitae);
if (gland != null)
{
items.Add(gland);
}
foreach (WorldObject wo in items)
{
if (!corpse.TryAddToInventory(wo))
log.Warn($"CalculateDeathItems_Olthoi: couldn't add item to {Name}'s corpse: {wo.Name}");
}
return items;
}
else
{
return new();
}
}
}
}