mirror of
https://github.com/modernuo/ModernUO
synced 2026-08-11 22:23:06 -04:00
### Summary - `GenericEntityPersistence` is now a type of `GenericPersistence`. This allows developers to serialize both entities and non-entities in the same system. 🎉 - Each `SerializationThreadWorker` now allocates 1MB of heap for serialization _permanently_. If more memory is needed, that thread will double it's memory, not to exceed increments of 64MB. - Several bugs with serialization introduced with the pure MMF implementation have been fixed. - `BinaryFileReader` has been added back. 🎉 - Adds `world.useMultithreadedSaves` to allow disabling threaded saves. > [!IMPORTANT] > **Developer Note** > The split file serialization has been deprecated and is no longer used. We have effectively gone back to the same file writing we had before the pure MMF implementation.
9223 lines
245 KiB
C#
9223 lines
245 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2024 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: Mobile.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using Server.Accounting;
|
|
using Server.Collections;
|
|
using Server.ContextMenus;
|
|
using Server.Guilds;
|
|
using Server.HuePickers;
|
|
using Server.Items;
|
|
using Server.Logging;
|
|
using Server.Menus;
|
|
using Server.Mobiles;
|
|
using Server.Network;
|
|
using Server.Prompts;
|
|
using Server.Targeting;
|
|
using Server.Text;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using CalcMoves = Server.Movement.Movement;
|
|
|
|
namespace Server;
|
|
|
|
public delegate void TargetCallback(Mobile from, object targeted);
|
|
|
|
public delegate void TargetStateCallback<in T>(Mobile from, object targeted, T state);
|
|
|
|
public delegate void PromptCallback(Mobile from, string text);
|
|
|
|
public delegate void PromptStateCallback<in T>(Mobile from, string text, T state);
|
|
|
|
public class DamageEntry
|
|
{
|
|
public DamageEntry(Mobile damager) => Damager = damager;
|
|
|
|
public Mobile Damager { get; }
|
|
|
|
public int DamageGiven { get; set; }
|
|
|
|
public DateTime LastDamage { get; set; }
|
|
|
|
public bool HasExpired => Core.Now > LastDamage + ExpireDelay;
|
|
|
|
public List<DamageEntry> Responsible { get; set; }
|
|
|
|
public static TimeSpan ExpireDelay { get; set; } = TimeSpan.FromMinutes(2.0);
|
|
}
|
|
|
|
[Flags]
|
|
public enum StatType : byte
|
|
{
|
|
Str = 1,
|
|
Dex = 2,
|
|
Int = 4,
|
|
All = 7
|
|
}
|
|
|
|
public enum StatLockType : byte
|
|
{
|
|
Up,
|
|
Down,
|
|
Locked
|
|
}
|
|
|
|
[Flags]
|
|
[CustomEnum(new[] { "North", "Right", "East", "Down", "South", "Left", "West", "Up" })]
|
|
public enum Direction : byte
|
|
{
|
|
North = 0x0,
|
|
Right = 0x1,
|
|
East = 0x2,
|
|
Down = 0x3,
|
|
South = 0x4,
|
|
Left = 0x5,
|
|
West = 0x6,
|
|
Up = 0x7,
|
|
|
|
Mask = 0x7,
|
|
Running = 0x80,
|
|
ValueMask = 0x87
|
|
}
|
|
|
|
[Flags]
|
|
public enum MobileDelta
|
|
{
|
|
None = 0x00000000,
|
|
Name = 0x00000001,
|
|
Flags = 0x00000002,
|
|
Hits = 0x00000004,
|
|
Mana = 0x00000008,
|
|
Stam = 0x00000010,
|
|
Stat = 0x00000020,
|
|
Noto = 0x00000040,
|
|
Gold = 0x00000080,
|
|
Weight = 0x00000100,
|
|
Direction = 0x00000200,
|
|
Hue = 0x00000400,
|
|
Body = 0x00000800,
|
|
Armor = 0x00001000,
|
|
StatCap = 0x00002000,
|
|
GhostUpdate = 0x00004000,
|
|
Followers = 0x00008000,
|
|
Properties = 0x00010000,
|
|
TithingPoints = 0x00020000,
|
|
Resistances = 0x00040000,
|
|
WeaponDamage = 0x00080000,
|
|
Hair = 0x00100000,
|
|
FacialHair = 0x00200000,
|
|
Race = 0x00400000,
|
|
HealthbarYellow = 0x00800000,
|
|
HealthbarPoison = 0x01000000,
|
|
|
|
Attributes = 0x0000001C
|
|
}
|
|
|
|
public enum Healthbar
|
|
{
|
|
Normal,
|
|
Poison,
|
|
Yellow
|
|
}
|
|
|
|
public enum AccessLevel
|
|
{
|
|
Player,
|
|
Counselor,
|
|
GameMaster,
|
|
Seer,
|
|
Administrator,
|
|
Developer,
|
|
Owner
|
|
}
|
|
|
|
public enum VisibleDamageType
|
|
{
|
|
None,
|
|
Related,
|
|
Everyone,
|
|
Selective
|
|
}
|
|
|
|
public enum ResistanceType
|
|
{
|
|
Physical,
|
|
Fire,
|
|
Cold,
|
|
Poison,
|
|
Energy
|
|
}
|
|
|
|
public enum ApplyPoisonResult
|
|
{
|
|
Poisoned,
|
|
Immune,
|
|
HigherPoisonActive,
|
|
Cured
|
|
}
|
|
|
|
public delegate bool SkillCheckTargetHandler(
|
|
Mobile from, SkillName skill, object target, double minSkill,
|
|
double maxSkill
|
|
);
|
|
|
|
public delegate bool SkillCheckLocationHandler(Mobile from, SkillName skill, double minSkill, double maxSkill);
|
|
|
|
public delegate bool SkillCheckDirectTargetHandler(Mobile from, SkillName skill, object target, double chance);
|
|
|
|
public delegate bool SkillCheckDirectLocationHandler(Mobile from, SkillName skill, double chance);
|
|
|
|
public delegate TimeSpan RegenRateHandler(Mobile from);
|
|
|
|
public delegate bool AllowBeneficialHandler(Mobile from, Mobile target);
|
|
|
|
public delegate bool AllowHarmfulHandler(Mobile from, Mobile target);
|
|
|
|
public delegate Container CreateCorpseHandler(
|
|
Mobile from, List<Item> initialContent, List<Item> equippedItems
|
|
);
|
|
|
|
public delegate int AOSStatusHandler(Mobile from, int index);
|
|
|
|
/// <summary>
|
|
/// Base class representing players, npcs, and creatures.
|
|
/// </summary>
|
|
public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyListEntity, IValueLinkListNode<Mobile>
|
|
{
|
|
// Allow four warmode changes in 0.5 seconds, any more will be delay for two seconds
|
|
private const int WarmodeCatchCount = 4;
|
|
|
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(Mobile));
|
|
|
|
// TODO: Make these configurations
|
|
private static readonly TimeSpan WarmodeSpamCatch = TimeSpan.FromSeconds(Core.SE ? 1.0 : 0.5);
|
|
private static readonly TimeSpan WarmodeSpamDelay = TimeSpan.FromSeconds(Core.SE ? 4.0 : 2.0);
|
|
private static readonly TimeSpan ExpireCombatantDelay = TimeSpan.FromMinutes(1.0);
|
|
private static readonly TimeSpan ExpireAggressorsDelay = TimeSpan.FromSeconds(5.0);
|
|
|
|
private static readonly object m_GhostMutateContext = new();
|
|
|
|
private static readonly List<Mobile> m_Hears = new();
|
|
private static readonly List<IEntity> m_OnSpeech = new();
|
|
|
|
private static readonly string[] m_AccessLevelNames =
|
|
{
|
|
"a player",
|
|
"a counselor",
|
|
"a game master",
|
|
"a seer",
|
|
"an administrator",
|
|
"a developer",
|
|
"an owner"
|
|
};
|
|
|
|
private static readonly int[] m_InvalidBodies =
|
|
{
|
|
32,
|
|
95,
|
|
156,
|
|
197,
|
|
198
|
|
};
|
|
|
|
private static readonly Queue<Mobile> m_DeltaQueue = new();
|
|
|
|
private static readonly string[] m_GuildTypes =
|
|
{
|
|
"",
|
|
" (Chaos)",
|
|
" (Order)"
|
|
};
|
|
|
|
private static bool _disableCastParalyze = true;
|
|
|
|
public static void Configure()
|
|
{
|
|
_disableCastParalyze = ServerConfiguration.GetSetting("spellCasting.disableCastParalyze", true);
|
|
}
|
|
|
|
private List<object> _actions;
|
|
private AccessLevel m_AccessLevel;
|
|
|
|
private TimerExecutionToken _autoManifestTimerToken;
|
|
|
|
private Container m_Backpack;
|
|
|
|
private BankBox m_BankBox;
|
|
private Body m_Body;
|
|
private Body m_BodyMod;
|
|
private bool m_CanHearGhosts;
|
|
|
|
private int m_ChangingCombatant;
|
|
private Mobile m_Combatant;
|
|
private TimerExecutionToken _combatTimerToken;
|
|
private bool m_Criminal;
|
|
|
|
private MobileDelta m_DeltaFlags;
|
|
private Direction m_Direction;
|
|
private bool m_DisplayGuildTitle;
|
|
|
|
private TimerExecutionToken _expireAggrTimerToken;
|
|
private TimerExecutionToken _expireCombatantTimerToken;
|
|
private TimerExecutionToken _expireCriminalTimerToken;
|
|
private VirtualHairInfo _facialHair;
|
|
public VirtualHairInfo FacialHair => _facialHair ??= new VirtualHairInfo(FacialHairItemID, FacialHairHue);
|
|
|
|
private int m_Fame, m_Karma;
|
|
private bool m_Female, m_Warmode, m_Hidden, m_Blessed, m_Flying;
|
|
private int m_Followers, m_FollowersMax;
|
|
private bool m_Frozen;
|
|
private TimerExecutionToken _frozenTimerToken;
|
|
private BaseGuild m_Guild;
|
|
private string m_GuildTitle;
|
|
|
|
private VirtualHairInfo _hair;
|
|
public VirtualHairInfo Hair => _hair ??= new VirtualHairInfo(HairItemID, HairHue);
|
|
|
|
private int m_Hits, m_Stam, m_Mana;
|
|
|
|
private Item m_Holding;
|
|
private int m_Hue;
|
|
|
|
private int m_HueMod = -1;
|
|
private int m_Hunger;
|
|
|
|
private bool m_InDeltaQueue;
|
|
private int m_Kills;
|
|
private string m_Language;
|
|
private int m_LightLevel;
|
|
private Point3D m_Location;
|
|
private TimerExecutionToken _logoutTimerToken;
|
|
private Timer m_ManaTimer, m_HitsTimer, m_StamTimer;
|
|
|
|
private Map m_Map;
|
|
|
|
/* Logout:
|
|
*
|
|
* When a client logs into mobile x
|
|
* - if (x is Internalized ) move x to logout location and map
|
|
*
|
|
* When a client attached to a mobile disconnects
|
|
* - LogoutTimer is started
|
|
* - Delay is taken from Region.GetLogoutDelay to allow insta-logout regions.
|
|
* - OnTick : Location and map are stored, and mobile is internalized
|
|
*
|
|
* Some things to consider:
|
|
* - An internalized person getting killed (say, by poison). Where does the body go?
|
|
* - Regions now have a GetLogoutDelay( Mobile m ); virtual function (see above)
|
|
*/
|
|
|
|
private Item m_MountItem;
|
|
private string m_Name;
|
|
|
|
private string m_NameMod;
|
|
private NetState m_NetState;
|
|
private DateTime m_NextWarmodeChange;
|
|
private bool m_Paralyzed;
|
|
private TimerExecutionToken _paraTimerToken;
|
|
private bool m_Player;
|
|
private Poison m_Poison;
|
|
private Prompt m_Prompt;
|
|
private ObjectPropertyList m_PropertyList;
|
|
private Race m_Race;
|
|
private Region m_Region;
|
|
|
|
private int m_SolidHueOverride = -1;
|
|
private ISpell m_Spell;
|
|
private int m_StatCap;
|
|
private int m_Str, m_Dex, m_Int;
|
|
|
|
private StatLockType m_StrLock, m_DexLock, m_IntLock;
|
|
private Target m_Target;
|
|
private int m_TithingPoints;
|
|
private string m_Title;
|
|
private int m_TotalGold, m_TotalItems, m_TotalWeight;
|
|
private int m_VirtualArmor;
|
|
private int m_VirtualArmorMod;
|
|
private int m_WarmodeChanges;
|
|
private bool _warmodeSpamValue;
|
|
private IWeapon m_Weapon;
|
|
private bool _ignoreMobiles;
|
|
|
|
private bool m_YellowHealthbar;
|
|
private List<StatMod> _statMods;
|
|
private List<ResistanceMod> _resistanceMods;
|
|
private List<SkillMod> _skillMods;
|
|
|
|
public Mobile()
|
|
{
|
|
m_Region = Map.Internal.DefaultRegion;
|
|
Serial = World.NewMobile;
|
|
|
|
DefaultMobileInit();
|
|
|
|
World.AddEntity(this);
|
|
}
|
|
|
|
public Mobile(Serial serial)
|
|
{
|
|
m_Region = Map.Internal.DefaultRegion;
|
|
Serial = serial;
|
|
Aggressors = new List<AggressorInfo>();
|
|
Aggressed = new List<AggressorInfo>();
|
|
NextSkillTime = Core.TickCount;
|
|
DamageEntries = new List<DamageEntry>();
|
|
}
|
|
|
|
// Sectors
|
|
public Mobile Next { get; set; }
|
|
public Mobile Previous { get; set; }
|
|
public bool OnLinkList { get; set; }
|
|
|
|
public static bool DragEffects { get; set; } = true;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Race Race
|
|
{
|
|
get => m_Race ??= Race.DefaultRace;
|
|
set
|
|
{
|
|
var oldRace = Race;
|
|
|
|
m_Race = value ?? Race.DefaultRace;
|
|
|
|
Body = m_Race.Body(this);
|
|
UpdateResistances();
|
|
|
|
Delta(MobileDelta.Race);
|
|
|
|
OnRaceChange(oldRace);
|
|
}
|
|
}
|
|
|
|
public virtual double RacialSkillBonus => 0;
|
|
|
|
public int[] Resistances { get; private set; }
|
|
|
|
public virtual int BasePhysicalResistance => 0;
|
|
public virtual int BaseFireResistance => 0;
|
|
public virtual int BaseColdResistance => 0;
|
|
public virtual int BasePoisonResistance => 0;
|
|
public virtual int BaseEnergyResistance => 0;
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int PhysicalResistance => GetResistance(ResistanceType.Physical);
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int FireResistance => GetResistance(ResistanceType.Fire);
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int ColdResistance => GetResistance(ResistanceType.Cold);
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int PoisonResistance => GetResistance(ResistanceType.Poison);
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int EnergyResistance => GetResistance(ResistanceType.Energy);
|
|
|
|
public static int MaxPlayerResistance { get; set; } = 70;
|
|
|
|
public virtual bool NewGuildDisplay => false;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual bool IgnoreMobiles
|
|
{
|
|
get => _ignoreMobiles;
|
|
set
|
|
{
|
|
if (_ignoreMobiles != value)
|
|
{
|
|
_ignoreMobiles = value;
|
|
Delta(MobileDelta.Flags);
|
|
}
|
|
}
|
|
}
|
|
|
|
public object Party { get; set; }
|
|
|
|
public List<SkillMod> SkillMods => _skillMods;
|
|
public List<StatMod> StatMods => _statMods;
|
|
public List<ResistanceMod> ResistanceMods => _resistanceMods;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int VirtualArmorMod
|
|
{
|
|
get => m_VirtualArmorMod;
|
|
set
|
|
{
|
|
if (m_VirtualArmorMod != value)
|
|
{
|
|
m_VirtualArmorMod = value;
|
|
|
|
Delta(MobileDelta.Armor);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int MeleeDamageAbsorb { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int MagicDamageAbsorb { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int SkillsTotal => Skills?.Total ?? 0;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int SkillsCap
|
|
{
|
|
get => Skills?.Cap ?? 0;
|
|
set
|
|
{
|
|
if (Skills != null)
|
|
{
|
|
Skills.Cap = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int BaseSoundID { get; set; }
|
|
|
|
public long NextCombatTime { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int NameHue { get; set; } = -1;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Hunger
|
|
{
|
|
get => m_Hunger;
|
|
set
|
|
{
|
|
var oldValue = m_Hunger;
|
|
|
|
if (oldValue != value)
|
|
{
|
|
m_Hunger = value;
|
|
OnHungerChanged(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Thirst { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int BAC { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the number of steps this player may take when hidden before being revealed.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int AllowedStealthSteps { get; set; }
|
|
|
|
public Item Holding
|
|
{
|
|
get => m_Holding;
|
|
set
|
|
{
|
|
if (m_Holding != value)
|
|
{
|
|
if (m_Holding != null)
|
|
{
|
|
UpdateTotal(m_Holding, TotalType.Weight, -(m_Holding.TotalWeight + m_Holding.PileWeight));
|
|
|
|
if (m_Holding.HeldBy == this)
|
|
{
|
|
m_Holding.HeldBy = null;
|
|
}
|
|
}
|
|
|
|
if (value != null && m_Holding != null)
|
|
{
|
|
DropHolding();
|
|
}
|
|
|
|
m_Holding = value;
|
|
|
|
if (m_Holding != null)
|
|
{
|
|
UpdateTotal(m_Holding, TotalType.Weight, m_Holding.TotalWeight + m_Holding.PileWeight);
|
|
|
|
m_Holding.HeldBy ??= this;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public long LastMoveTime { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual bool Paralyzed
|
|
{
|
|
get => m_Paralyzed;
|
|
set
|
|
{
|
|
if (m_Paralyzed != value)
|
|
{
|
|
m_Paralyzed = value;
|
|
Delta(MobileDelta.Flags);
|
|
|
|
SendLocalizedMessage(m_Paralyzed ? 502381 : 502382);
|
|
_paraTimerToken.Cancel();
|
|
}
|
|
|
|
if (!value && m_NetState != null)
|
|
{
|
|
var now = Core.TickCount;
|
|
if (now - m_NetState._nextMovementTime > 0)
|
|
{
|
|
m_NetState._nextMovementTime = now;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool DisarmReady { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool StunReady { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Frozen
|
|
{
|
|
get => m_Frozen;
|
|
set
|
|
{
|
|
if (m_Frozen != value)
|
|
{
|
|
m_Frozen = value;
|
|
Delta(MobileDelta.Flags);
|
|
_frozenTimerToken.Cancel();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the <see cref="StatLockType">lock state</see> for the <see cref="RawStr" /> property.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public StatLockType StrLock
|
|
{
|
|
get => m_StrLock;
|
|
set
|
|
{
|
|
if (m_StrLock != value)
|
|
{
|
|
m_StrLock = value;
|
|
|
|
m_NetState.SendStatLockInfo(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the <see cref="StatLockType">lock state</see> for the <see cref="RawDex" /> property.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public StatLockType DexLock
|
|
{
|
|
get => m_DexLock;
|
|
set
|
|
{
|
|
if (m_DexLock != value)
|
|
{
|
|
m_DexLock = value;
|
|
|
|
m_NetState.SendStatLockInfo(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the <see cref="StatLockType">lock state</see> for the <see cref="RawInt" /> property.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public StatLockType IntLock
|
|
{
|
|
get => m_IntLock;
|
|
set
|
|
{
|
|
if (m_IntLock != value)
|
|
{
|
|
m_IntLock = value;
|
|
|
|
m_NetState.SendStatLockInfo(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
public long NextActionTime { get; set; }
|
|
|
|
public long NextActionMessage { get; set; }
|
|
|
|
public static int ActionMessageDelay { get; set; } = 125;
|
|
|
|
public static bool GlobalRegenThroughPoison { get; set; } = true;
|
|
|
|
public virtual bool RegenThroughPoison => GlobalRegenThroughPoison;
|
|
|
|
public virtual bool CanRegenHits => Alive && (RegenThroughPoison || !Poisoned);
|
|
public virtual bool CanRegenStam => Alive;
|
|
public virtual bool CanRegenMana => Alive;
|
|
|
|
public long NextSkillTime { get; set; }
|
|
|
|
public List<AggressorInfo> Aggressors { get; private set; }
|
|
|
|
public List<AggressorInfo> Aggressed { get; private set; }
|
|
|
|
public bool ChangingCombatant => m_ChangingCombatant > 0;
|
|
|
|
private void ExpireCombatant()
|
|
{
|
|
Combatant = null;
|
|
_expireCombatantTimerToken.Cancel();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Gets or sets which Mobile that this Mobile is currently engaged in combat with.
|
|
/// <seealso cref="OnCombatantChange" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual Mobile Combatant
|
|
{
|
|
get => m_Combatant;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_Combatant != value && value != this)
|
|
{
|
|
var old = m_Combatant;
|
|
|
|
++m_ChangingCombatant;
|
|
m_Combatant = value;
|
|
|
|
if (m_Combatant != null && !CanBeHarmful(m_Combatant, false) ||
|
|
!Region.OnCombatantChange(this, old, m_Combatant))
|
|
{
|
|
m_Combatant = old;
|
|
--m_ChangingCombatant;
|
|
return;
|
|
}
|
|
|
|
if (m_Combatant == null)
|
|
{
|
|
m_NetState.SendChangeCombatant(Serial.Zero);
|
|
_expireCombatantTimerToken.Cancel();
|
|
_combatTimerToken.Cancel();
|
|
}
|
|
else
|
|
{
|
|
m_NetState.SendChangeCombatant(m_Combatant.Serial);
|
|
if (!_expireCombatantTimerToken.Running)
|
|
{
|
|
Timer.StartTimer(ExpireCombatantDelay, ExpireCombatant, out _expireCombatantTimerToken);
|
|
}
|
|
|
|
if (!_combatTimerToken.Running)
|
|
{
|
|
Timer.StartTimer(TimeSpan.FromSeconds(0.01), 0, CheckCombatTime, out _combatTimerToken);
|
|
}
|
|
|
|
if (CanBeHarmful(m_Combatant, false))
|
|
{
|
|
DoHarmful(m_Combatant); // due to reflection, might make m_Combatant null
|
|
m_Combatant?.PlaySound(m_Combatant.GetAngerSound());
|
|
}
|
|
}
|
|
|
|
OnCombatantChange();
|
|
--m_ChangingCombatant;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CheckCombatTime()
|
|
{
|
|
if (Core.TickCount - NextCombatTime < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var combatant = Combatant;
|
|
|
|
// If no combatant, wrong map, one of us is a ghost, or cannot see, or deleted, then stop combat
|
|
if (combatant?.Deleted != false || Deleted || combatant.m_Map != m_Map ||
|
|
!combatant.Alive || !Alive || !CanSee(combatant) || combatant.IsDeadBondedPet ||
|
|
IsDeadBondedPet)
|
|
{
|
|
Combatant = null;
|
|
return;
|
|
}
|
|
|
|
var weapon = Weapon;
|
|
|
|
if (weapon == null || !InRange(combatant, weapon.MaxRange))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (InLOS(combatant))
|
|
{
|
|
weapon.OnBeforeSwing(this, combatant);
|
|
RevealingAction();
|
|
NextCombatTime =
|
|
Core.TickCount + (int)weapon.OnSwing(this, combatant).TotalMilliseconds;
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TotalGold => GetTotal(TotalType.Gold);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TotalItems => GetTotal(TotalType.Items);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TotalWeight => GetTotal(TotalType.Weight);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TithingPoints
|
|
{
|
|
get => m_TithingPoints;
|
|
set
|
|
{
|
|
if (m_TithingPoints != value)
|
|
{
|
|
m_TithingPoints = value;
|
|
|
|
Delta(MobileDelta.TithingPoints);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Followers
|
|
{
|
|
get => m_Followers;
|
|
set
|
|
{
|
|
if (m_Followers != value)
|
|
{
|
|
m_Followers = value;
|
|
|
|
Delta(MobileDelta.Followers);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int FollowersMax
|
|
{
|
|
get => m_FollowersMax;
|
|
set
|
|
{
|
|
if (m_FollowersMax != value)
|
|
{
|
|
m_FollowersMax = value;
|
|
|
|
Delta(MobileDelta.Followers);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool TargetLocked { get; set; }
|
|
|
|
public Target Target
|
|
{
|
|
get => m_Target;
|
|
set
|
|
{
|
|
var oldTarget = m_Target;
|
|
var newTarget = value;
|
|
|
|
if (oldTarget == newTarget)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_Target = null;
|
|
|
|
if (oldTarget != null && newTarget != null)
|
|
{
|
|
oldTarget.Cancel(this, TargetCancelType.Overridden);
|
|
}
|
|
|
|
m_Target = newTarget;
|
|
|
|
if (newTarget != null && !TargetLocked)
|
|
{
|
|
newTarget.SendTargetTo(m_NetState);
|
|
}
|
|
|
|
OnTargetChange();
|
|
}
|
|
}
|
|
|
|
[IgnoreDupe]
|
|
public bool Pushing { get; set; }
|
|
|
|
public virtual bool IsDeadBondedPet => false;
|
|
|
|
[IgnoreDupe]
|
|
public ISpell Spell
|
|
{
|
|
get => m_Spell;
|
|
set
|
|
{
|
|
if (m_Spell != null && value != null)
|
|
{
|
|
logger.Warning("Spell has been overwritten.");
|
|
}
|
|
|
|
m_Spell = value;
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Administrator)]
|
|
public bool AutoPageNotify { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Owner)]
|
|
public IAccount Account { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int VirtualArmor
|
|
{
|
|
get => m_VirtualArmor;
|
|
set
|
|
{
|
|
if (m_VirtualArmor != value)
|
|
{
|
|
m_VirtualArmor = value;
|
|
|
|
Delta(MobileDelta.Armor);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual double ArmorRating => 0.0;
|
|
|
|
/// <summary>
|
|
/// Overridable. Returns true if the player is alive, false if otherwise. By default, this is computed by:
|
|
/// <c>!Deleted && (!Player || !Body.IsGhost)</c>
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual bool Alive => !Deleted && (!m_Player || !m_Body.IsGhost);
|
|
|
|
public static CreateCorpseHandler CreateCorpseHandler { get; set; }
|
|
|
|
public virtual bool RetainPackLocsOnDeath => Core.AOS;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Container Corpse { get; set; }
|
|
|
|
public static char[] GhostChars { get; set; }
|
|
public static bool NoSpeechLOS { get; set; }
|
|
|
|
public static TimeSpan AutoManifestTimeout { get; set; } = TimeSpan.FromSeconds(5.0);
|
|
|
|
public static bool InsuranceEnabled { get; set; }
|
|
|
|
public static int ActionDelay { get; set; } = 500;
|
|
|
|
public static VisibleDamageType VisibleDamageType { get; set; }
|
|
|
|
public List<DamageEntry> DamageEntries { get; private set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Mobile LastKiller { get; set; }
|
|
|
|
public static bool DefaultShowVisibleDamage { get; set; }
|
|
|
|
public static bool DefaultCanSeeVisibleDamage { get; set; }
|
|
|
|
public virtual bool ShowVisibleDamage => DefaultShowVisibleDamage;
|
|
public virtual bool CanSeeVisibleDamage => DefaultCanSeeVisibleDamage;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Squelched { get; set; }
|
|
|
|
public virtual bool ShouldCheckStatTimers => true;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int LightLevel
|
|
{
|
|
get => m_LightLevel;
|
|
set
|
|
{
|
|
if (m_LightLevel != value)
|
|
{
|
|
m_LightLevel = value;
|
|
|
|
CheckLightLevels(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public string Profile { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public bool ProfileLocked { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
|
|
public bool Player
|
|
{
|
|
get => m_Player;
|
|
set
|
|
{
|
|
m_Player = value;
|
|
InvalidateProperties();
|
|
CheckStatTimers();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public string Title
|
|
{
|
|
get => m_Title;
|
|
set
|
|
{
|
|
m_Title = value;
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
|
|
public List<Item> Items { get; private set; }
|
|
|
|
public virtual int MaxWeight => int.MaxValue;
|
|
|
|
public static IWeapon DefaultWeapon { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.Counselor, canModify: true)]
|
|
public Skills Skills { get; private set; }
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.Administrator)]
|
|
public AccessLevel AccessLevel
|
|
{
|
|
get => m_AccessLevel;
|
|
set
|
|
{
|
|
var oldValue = m_AccessLevel;
|
|
|
|
if (oldValue != value)
|
|
{
|
|
m_AccessLevel = value;
|
|
Delta(MobileDelta.Noto);
|
|
InvalidateProperties();
|
|
|
|
SendMessage($"Your access level has been changed. You are now {GetAccessLevelName(value)}.");
|
|
|
|
ClearScreen();
|
|
SendEverything();
|
|
|
|
OnAccessLevelChanged(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Fame
|
|
{
|
|
get => m_Fame;
|
|
set
|
|
{
|
|
var oldValue = m_Fame;
|
|
|
|
if (oldValue != value)
|
|
{
|
|
m_Fame = value;
|
|
|
|
if (ShowFameTitle && (m_Player || m_Body.IsHuman) && oldValue >= 10000 != value >= 10000)
|
|
{
|
|
InvalidateProperties();
|
|
}
|
|
|
|
OnFameChange(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Karma
|
|
{
|
|
get => m_Karma;
|
|
set
|
|
{
|
|
var old = m_Karma;
|
|
|
|
if (old != value)
|
|
{
|
|
m_Karma = value;
|
|
OnKarmaChange(old);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Blessed
|
|
{
|
|
get => m_Blessed;
|
|
set
|
|
{
|
|
if (m_Blessed != value)
|
|
{
|
|
m_Blessed = value;
|
|
Delta(MobileDelta.HealthbarYellow);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual int Luck => 0;
|
|
|
|
[Hue]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int HueMod
|
|
{
|
|
get => m_HueMod;
|
|
set
|
|
{
|
|
if (m_HueMod != value)
|
|
{
|
|
m_HueMod = value;
|
|
|
|
Delta(MobileDelta.Hue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Hue]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int Hue
|
|
{
|
|
get => m_HueMod != -1 ? m_HueMod : m_Hue;
|
|
set
|
|
{
|
|
var oldHue = m_Hue;
|
|
|
|
if (oldHue != value)
|
|
{
|
|
m_Hue = value;
|
|
|
|
Delta(MobileDelta.Hue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Direction Direction
|
|
{
|
|
get => m_Direction;
|
|
set
|
|
{
|
|
if (m_Direction != value)
|
|
{
|
|
m_Direction = value;
|
|
Delta(MobileDelta.Direction);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Female
|
|
{
|
|
get => m_Female;
|
|
set
|
|
{
|
|
if (m_Female != value)
|
|
{
|
|
m_Female = value;
|
|
Delta(MobileDelta.Flags);
|
|
OnGenderChanged(!m_Female);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Flying
|
|
{
|
|
get => m_Flying;
|
|
set
|
|
{
|
|
if (m_Flying != value)
|
|
{
|
|
m_Flying = value;
|
|
Delta(MobileDelta.Flags);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Warmode
|
|
{
|
|
get => m_Warmode;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_Warmode != value)
|
|
{
|
|
_autoManifestTimerToken.Cancel();
|
|
|
|
m_Warmode = value;
|
|
Delta(MobileDelta.Flags);
|
|
|
|
m_NetState.SendSetWarMode(value);
|
|
|
|
if (!m_Warmode)
|
|
{
|
|
Combatant = null;
|
|
}
|
|
|
|
if (!Alive)
|
|
{
|
|
if (value)
|
|
{
|
|
Delta(MobileDelta.GhostUpdate);
|
|
}
|
|
else
|
|
{
|
|
SendRemovePacket(false);
|
|
}
|
|
}
|
|
|
|
OnWarmodeChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Hidden
|
|
{
|
|
get => m_Hidden;
|
|
set
|
|
{
|
|
if (m_Hidden != value)
|
|
{
|
|
m_Hidden = value;
|
|
// Delta( MobileDelta.Flags );
|
|
|
|
OnHiddenChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Owner)]
|
|
public NetState NetState
|
|
{
|
|
get
|
|
{
|
|
if (m_NetState?.Connection == null)
|
|
{
|
|
m_NetState = null;
|
|
}
|
|
|
|
return m_NetState;
|
|
}
|
|
set
|
|
{
|
|
if (m_NetState == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
#if THREADGUARD
|
|
if (Thread.CurrentThread != Core.Thread)
|
|
{
|
|
Utility.PushColor(ConsoleColor.Red);
|
|
Console.WriteLine("Attempting to set Mobile.NetState value from an invalid thread!");
|
|
Console.WriteLine(new StackTrace());
|
|
Utility.PopColor();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
m_Map?.OnClientChange(m_NetState, value, this);
|
|
m_Target?.Cancel(this, TargetCancelType.Disconnected);
|
|
m_Spell?.OnConnectionChanged();
|
|
m_NetState?.CancelAllTrades();
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
OnBeforeDisconnected();
|
|
EventSink.InvokeBeforeDisconnected(this);
|
|
|
|
m_NetState = value;
|
|
_logoutTimerToken.Cancel();
|
|
|
|
if (m_NetState == null)
|
|
{
|
|
OnDisconnected();
|
|
EventSink.InvokeDisconnected(this);
|
|
|
|
// Disconnected, start the logout timer
|
|
Timer.StartTimer(GetLogoutDelay(), Logout, out _logoutTimerToken);
|
|
}
|
|
else
|
|
{
|
|
OnConnected();
|
|
EventSink.InvokeConnected(this);
|
|
|
|
if (m_Map == Map.Internal && LogoutMap != null)
|
|
{
|
|
Map = LogoutMap;
|
|
Location = LogoutLocation;
|
|
}
|
|
}
|
|
|
|
for (var i = Items.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i >= Items.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var item = Items[i];
|
|
|
|
if (item is SecureTradeContainer)
|
|
{
|
|
for (var j = item.Items.Count - 1; j >= 0; --j)
|
|
{
|
|
if (j < item.Items.Count)
|
|
{
|
|
item.Items[j].OnSecureTrade(this, this, this, false);
|
|
AddToBackpack(item.Items[j]);
|
|
}
|
|
}
|
|
|
|
Timer.StartTimer(item.Delete);
|
|
}
|
|
}
|
|
|
|
DropHolding();
|
|
OnNetStateChanged();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public string Language
|
|
{
|
|
get => m_Language;
|
|
set => m_Language = value;
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int SpeechHue { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int EmoteHue { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int WhisperHue { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int YellHue { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public string GuildTitle
|
|
{
|
|
get => m_GuildTitle;
|
|
set
|
|
{
|
|
var old = m_GuildTitle;
|
|
|
|
if (old != value)
|
|
{
|
|
m_GuildTitle = value;
|
|
|
|
if (m_Guild?.Disbanded == false && m_GuildTitle != null)
|
|
{
|
|
SendLocalizedMessage(1018026, true, m_GuildTitle); // Your guild title has changed :
|
|
}
|
|
|
|
InvalidateProperties();
|
|
|
|
OnGuildTitleChange(old);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool DisplayGuildTitle
|
|
{
|
|
get => m_DisplayGuildTitle;
|
|
set
|
|
{
|
|
m_DisplayGuildTitle = value;
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Mobile GuildFealty { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public string NameMod
|
|
{
|
|
get => m_NameMod;
|
|
set
|
|
{
|
|
if (m_NameMod != value)
|
|
{
|
|
m_NameMod = value;
|
|
Delta(MobileDelta.Name);
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool YellowHealthbar
|
|
{
|
|
get => m_YellowHealthbar;
|
|
set
|
|
{
|
|
m_YellowHealthbar = value;
|
|
Delta(MobileDelta.HealthbarYellow);
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public string RawName
|
|
{
|
|
get => m_Name;
|
|
set => Name = value;
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual string Name
|
|
{
|
|
get => m_NameMod ?? m_Name;
|
|
set
|
|
{
|
|
if (m_Name != value) // I'm leaving out the && m_NameMod == null
|
|
{
|
|
var oldName = m_Name;
|
|
m_Name = value;
|
|
OnAfterNameChange(oldName, m_Name);
|
|
Delta(MobileDelta.Name);
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public DateTime LastStrGain { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public DateTime LastIntGain { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public DateTime LastDexGain { get; set; }
|
|
|
|
public DateTime LastStatGain
|
|
{
|
|
get
|
|
{
|
|
var d = LastStrGain;
|
|
|
|
if (LastIntGain > d)
|
|
{
|
|
d = LastIntGain;
|
|
}
|
|
|
|
if (LastDexGain > d)
|
|
{
|
|
d = LastDexGain;
|
|
}
|
|
|
|
return d;
|
|
}
|
|
set
|
|
{
|
|
LastStrGain = value;
|
|
LastIntGain = value;
|
|
LastDexGain = value;
|
|
}
|
|
}
|
|
|
|
public BaseGuild Guild
|
|
{
|
|
get => m_Guild;
|
|
set
|
|
{
|
|
var old = m_Guild;
|
|
|
|
if (old != value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
GuildTitle = null;
|
|
}
|
|
|
|
m_Guild = value;
|
|
|
|
Delta(MobileDelta.Noto);
|
|
InvalidateProperties();
|
|
|
|
OnGuildChange(old);
|
|
}
|
|
}
|
|
}
|
|
|
|
public Region WalkRegion { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Poisoned => m_Poison != null;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool IsBodyMod => m_BodyMod.BodyID != 0;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Body BodyMod
|
|
{
|
|
get => m_BodyMod;
|
|
set
|
|
{
|
|
if (m_BodyMod != value)
|
|
{
|
|
m_BodyMod = value;
|
|
|
|
Delta(MobileDelta.Body);
|
|
InvalidateProperties();
|
|
|
|
CheckStatTimers();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Body Body
|
|
{
|
|
get => IsBodyMod ? m_BodyMod : m_Body;
|
|
set
|
|
{
|
|
if (m_Body != value && !IsBodyMod)
|
|
{
|
|
m_Body = SafeBody(value);
|
|
|
|
Delta(MobileDelta.Body);
|
|
InvalidateProperties();
|
|
|
|
CheckStatTimers();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public Point3D LogoutLocation { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public Map LogoutMap { get; set; }
|
|
|
|
public Region Region => m_Region ?? (Map == null ? Map.Internal.DefaultRegion : Map.DefaultRegion);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int SolidHueOverride
|
|
{
|
|
get => m_SolidHueOverride;
|
|
set
|
|
{
|
|
if (m_SolidHueOverride == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_SolidHueOverride = value;
|
|
Delta(MobileDelta.Hue | MobileDelta.Body);
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual IWeapon Weapon
|
|
{
|
|
get
|
|
{
|
|
if (m_Weapon is Item item && !item.Deleted && item.Parent == this && CanSee(item))
|
|
{
|
|
return m_Weapon;
|
|
}
|
|
|
|
m_Weapon = null;
|
|
|
|
item = FindItemOnLayer(Layer.OneHanded) ?? FindItemOnLayer(Layer.TwoHanded);
|
|
|
|
if (item is IWeapon weapon)
|
|
{
|
|
return m_Weapon = weapon;
|
|
}
|
|
|
|
return GetDefaultWeapon();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public BankBox BankBox
|
|
{
|
|
get
|
|
{
|
|
if (m_BankBox?.Deleted == false && m_BankBox.Parent == this)
|
|
{
|
|
return m_BankBox;
|
|
}
|
|
|
|
m_BankBox = FindItemOnLayer<BankBox>(Layer.Bank);
|
|
|
|
if (m_BankBox == null)
|
|
{
|
|
AddItem(m_BankBox = new BankBox(this));
|
|
}
|
|
|
|
return m_BankBox;
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Container Backpack
|
|
{
|
|
get
|
|
{
|
|
if (m_Backpack?.Deleted != false || m_Backpack.Parent != this)
|
|
{
|
|
m_Backpack = FindItemOnLayer<Container>(Layer.Backpack);
|
|
}
|
|
|
|
return m_Backpack;
|
|
}
|
|
}
|
|
|
|
public virtual bool KeepsItemsOnDeath => m_AccessLevel > AccessLevel.Player;
|
|
|
|
public bool HasTrade => m_NetState?.Trades.Count > 0;
|
|
|
|
public bool NoMoveHS { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public int Kills
|
|
{
|
|
get => m_Kills;
|
|
set
|
|
{
|
|
var oldValue = m_Kills;
|
|
|
|
if (m_Kills != value)
|
|
{
|
|
m_Kills = Math.Max(value, 0);
|
|
|
|
if (oldValue >= 5 != m_Kills >= 5)
|
|
{
|
|
Delta(MobileDelta.Noto);
|
|
InvalidateProperties();
|
|
}
|
|
|
|
OnKillsChange(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public virtual bool Criminal
|
|
{
|
|
get => m_Criminal;
|
|
set
|
|
{
|
|
if (m_Criminal != value)
|
|
{
|
|
m_Criminal = value;
|
|
Delta(MobileDelta.Noto);
|
|
InvalidateProperties();
|
|
}
|
|
|
|
_expireCriminalTimerToken.Cancel();
|
|
|
|
if (m_Criminal)
|
|
{
|
|
Timer.StartTimer(ExpireCriminalDelay, ExpireCriminal, out _expireCriminalTimerToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static bool DisableDismountInWarmode { get; set; }
|
|
|
|
public static int BodyWeight { get; set; } = 11; // 11 + 3 for the backpack
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public IMount Mount
|
|
{
|
|
get
|
|
{
|
|
Item item = null;
|
|
|
|
if (m_MountItem?.Deleted == false && m_MountItem.Parent == this)
|
|
{
|
|
item = m_MountItem;
|
|
}
|
|
|
|
item ??= FindItemOnLayer(Layer.Mount);
|
|
|
|
if (item is not IMountItem mountItem)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
m_MountItem = item;
|
|
return mountItem.Mount;
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Mounted => Mount != null;
|
|
|
|
public virtual bool CanTarget => true;
|
|
public virtual bool ClickTitle => true;
|
|
|
|
public virtual bool PropertyTitle => OldPropertyTitles ? ClickTitle : true;
|
|
|
|
public static bool DisableHiddenSelfClick { get; set; } = true;
|
|
|
|
public static bool AsciiClickMessage { get; set; } = true;
|
|
|
|
public static bool GuildClickMessage { get; set; } = true;
|
|
|
|
public static bool OldPropertyTitles { get; set; }
|
|
|
|
public virtual bool ShowFameTitle // (m_Player || m_Body.IsHuman) && m_Fame >= 10000; }
|
|
=> true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the maximum attainable value for <see cref="RawStr" />, <see cref="RawDex" />, and <see cref="RawInt" />.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int StatCap
|
|
{
|
|
get => m_StatCap;
|
|
set
|
|
{
|
|
if (m_StatCap != value)
|
|
{
|
|
m_StatCap = value;
|
|
|
|
Delta(MobileDelta.StatCap);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Meditating { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool CanSwim { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool CantWalk { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool CanHearGhosts
|
|
{
|
|
get => m_CanHearGhosts || AccessLevel >= AccessLevel.Counselor;
|
|
set => m_CanHearGhosts = value;
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int RawStatTotal => RawStr + RawDex + RawInt;
|
|
|
|
public long NextSpellTime { get; set; }
|
|
|
|
public static AllowBeneficialHandler AllowBeneficialHandler { get; set; }
|
|
|
|
public static AllowHarmfulHandler AllowHarmfulHandler { get; set; }
|
|
|
|
public static SkillCheckTargetHandler SkillCheckTargetHandler { get; set; }
|
|
|
|
public static SkillCheckLocationHandler SkillCheckLocationHandler { get; set; }
|
|
|
|
public static SkillCheckDirectTargetHandler SkillCheckDirectTargetHandler { get; set; }
|
|
|
|
public static SkillCheckDirectLocationHandler SkillCheckDirectLocationHandler { get; set; }
|
|
|
|
public static AOSStatusHandler AOSStatusHandler { get; set; }
|
|
|
|
public static RegenRateHandler HitsRegenRateHandler { get; set; }
|
|
|
|
public static TimeSpan DefaultHitsRate { get; set; }
|
|
|
|
public static RegenRateHandler StamRegenRateHandler { get; set; }
|
|
|
|
public static TimeSpan DefaultStamRate { get; set; }
|
|
|
|
public static RegenRateHandler ManaRegenRateHandler { get; set; }
|
|
|
|
public static TimeSpan DefaultManaRate { get; set; }
|
|
|
|
public static TimeSpan ExpireCriminalDelay { get; set; } = TimeSpan.FromMinutes(2.0);
|
|
|
|
public Prompt Prompt
|
|
{
|
|
get => m_Prompt;
|
|
set
|
|
{
|
|
var oldPrompt = m_Prompt;
|
|
var newPrompt = value;
|
|
|
|
if (oldPrompt == newPrompt)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_Prompt = null;
|
|
|
|
// TODO: Cancel the prompt anyway?
|
|
if (newPrompt != null)
|
|
{
|
|
oldPrompt?.OnCancel(this);
|
|
}
|
|
|
|
m_Prompt = newPrompt;
|
|
NetState.SendPrompt(newPrompt);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the base, unmodified, strength of the Mobile. Ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="Str" />
|
|
/// <seealso cref="StatMod" />
|
|
/// <seealso cref="OnRawStrChange" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int RawStr
|
|
{
|
|
get => m_Str;
|
|
set
|
|
{
|
|
value = Math.Clamp(value, 1, 65000);
|
|
|
|
if (m_Str != value)
|
|
{
|
|
var oldValue = m_Str;
|
|
|
|
m_Str = value;
|
|
Delta(MobileDelta.Stat | MobileDelta.Hits);
|
|
|
|
if (Hits < HitsMax)
|
|
{
|
|
m_HitsTimer ??= new HitsTimer(this);
|
|
m_HitsTimer.Start();
|
|
}
|
|
else if (Hits > HitsMax)
|
|
{
|
|
Hits = HitsMax;
|
|
}
|
|
|
|
OnRawStrChange(oldValue);
|
|
OnRawStatChange(StatType.Str, oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the effective strength of the Mobile. This is the sum of the <see cref="RawStr" /> plus any additional
|
|
/// modifiers. Any attempts to set this value when under the influence of a <see cref="StatMod" /> will result in no change.
|
|
/// It ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="RawStr" />
|
|
/// <seealso cref="StatMod" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int Str
|
|
{
|
|
get => Math.Clamp(m_Str + GetStatOffset(StatType.Str), 1, 65000);
|
|
set
|
|
{
|
|
if (_statMods == null || _statMods.Count == 0)
|
|
{
|
|
RawStr = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the base, unmodified, dexterity of the Mobile. Ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="Dex" />
|
|
/// <seealso cref="StatMod" />
|
|
/// <seealso cref="OnRawDexChange" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int RawDex
|
|
{
|
|
get => m_Dex;
|
|
set
|
|
{
|
|
value = Math.Clamp(value, 1, 65000);
|
|
|
|
if (m_Dex != value)
|
|
{
|
|
var oldValue = m_Dex;
|
|
|
|
m_Dex = value;
|
|
Delta(MobileDelta.Stat | MobileDelta.Stam);
|
|
|
|
if (Stam < StamMax)
|
|
{
|
|
m_StamTimer ??= new StamTimer(this);
|
|
m_StamTimer.Start();
|
|
}
|
|
else if (Stam > StamMax)
|
|
{
|
|
Stam = StamMax;
|
|
}
|
|
|
|
OnRawDexChange(oldValue);
|
|
OnRawStatChange(StatType.Dex, oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the effective dexterity of the Mobile. This is the sum of the <see cref="RawDex" /> plus any additional
|
|
/// modifiers. Any attempts to set this value when under the influence of a <see cref="StatMod" /> will result in no change.
|
|
/// It ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="RawDex" />
|
|
/// <seealso cref="StatMod" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int Dex
|
|
{
|
|
get => Math.Clamp(m_Dex + GetStatOffset(StatType.Dex), 0, 65000);
|
|
set
|
|
{
|
|
if (_statMods == null || _statMods.Count == 0)
|
|
{
|
|
RawDex = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the base, unmodified, intelligence of the Mobile. Ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="Int" />
|
|
/// <seealso cref="StatMod" />
|
|
/// <seealso cref="OnRawIntChange" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int RawInt
|
|
{
|
|
get => m_Int;
|
|
set
|
|
{
|
|
value = Math.Clamp(value, 1, 65000);
|
|
|
|
if (m_Int != value)
|
|
{
|
|
var oldValue = m_Int;
|
|
|
|
m_Int = value;
|
|
Delta(MobileDelta.Stat | MobileDelta.Mana);
|
|
|
|
if (Mana < ManaMax)
|
|
{
|
|
m_ManaTimer ??= new ManaTimer(this);
|
|
m_ManaTimer.Start();
|
|
}
|
|
else if (Mana > ManaMax)
|
|
{
|
|
Mana = ManaMax;
|
|
}
|
|
|
|
OnRawIntChange(oldValue);
|
|
OnRawStatChange(StatType.Int, oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the effective intelligence of the Mobile. This is the sum of the <see cref="RawInt" /> plus any additional
|
|
/// modifiers. Any attempts to set this value when under the influence of a <see cref="StatMod" /> will result in no change.
|
|
/// It ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="RawInt" />
|
|
/// <seealso cref="StatMod" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int Int
|
|
{
|
|
get => Math.Clamp(m_Int + GetStatOffset(StatType.Int), 0, 65000);
|
|
set
|
|
{
|
|
if (_statMods == null || _statMods.Count == 0)
|
|
{
|
|
RawInt = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the current hit point of the Mobile. This value ranges from 0 to <see cref="HitsMax" />, inclusive. When
|
|
/// set
|
|
/// to the value of <see cref="HitsMax" />, the <see cref="AggressorInfo.CanReportMurder">CanReportMurder</see> flag of all
|
|
/// aggressors is reset to false, and the list of damage entries is cleared.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Hits
|
|
{
|
|
get => m_Hits;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
value = Math.Clamp(value, 0, HitsMax);
|
|
|
|
if (value == HitsMax)
|
|
{
|
|
m_HitsTimer?.Stop();
|
|
|
|
for (var i = 0; i < Aggressors.Count; i++) // reset reports on full HP
|
|
{
|
|
Aggressors[i].CanReportMurder = false;
|
|
}
|
|
|
|
if (DamageEntries.Count > 0)
|
|
{
|
|
DamageEntries.Clear(); // reset damage entries on full HP
|
|
}
|
|
}
|
|
else if (CanRegenHits)
|
|
{
|
|
m_HitsTimer ??= new HitsTimer(this);
|
|
m_HitsTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_HitsTimer?.Stop();
|
|
}
|
|
|
|
if (m_Hits != value)
|
|
{
|
|
var oldValue = m_Hits;
|
|
m_Hits = value;
|
|
Delta(MobileDelta.Hits);
|
|
OnHitsChange(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Gets the maximum hit point of the Mobile. By default, this returns: <c>50 + (<see cref="Str" /> / 2)</c>
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int HitsMax => 50 + Str / 2;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the current stamina of the Mobile. This value ranges from 0 to <see cref="StamMax" />, inclusive.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Stam
|
|
{
|
|
get => m_Stam;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
value = Math.Clamp(value, 0, StamMax);
|
|
|
|
if (CanRegenStam && value < StamMax)
|
|
{
|
|
m_StamTimer ??= new StamTimer(this);
|
|
m_StamTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_StamTimer?.Stop();
|
|
}
|
|
|
|
if (m_Stam != value)
|
|
{
|
|
var oldValue = m_Stam;
|
|
m_Stam = value;
|
|
Delta(MobileDelta.Stam);
|
|
OnStamChange(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Gets the maximum stamina of the Mobile. By default, this returns:
|
|
/// <c>
|
|
/// <see cref="Dex" />
|
|
/// </c>
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int StamMax => Dex;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the current stamina of the Mobile. This value ranges from 0 to <see cref="ManaMax" />, inclusive.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Mana
|
|
{
|
|
get => m_Mana;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
value = Math.Clamp(value, 0, ManaMax);
|
|
|
|
if (value == ManaMax)
|
|
{
|
|
m_ManaTimer?.Stop();
|
|
|
|
if (Meditating)
|
|
{
|
|
Meditating = false;
|
|
SendLocalizedMessage(501846); // You are at peace.
|
|
}
|
|
}
|
|
else if (CanRegenMana)
|
|
{
|
|
m_ManaTimer ??= new ManaTimer(this);
|
|
m_ManaTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_ManaTimer?.Stop();
|
|
}
|
|
|
|
if (m_Mana != value)
|
|
{
|
|
var oldValue = m_Mana;
|
|
m_Mana = value;
|
|
Delta(MobileDelta.Mana);
|
|
OnManaChange(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Gets the maximum mana of the Mobile. By default, this returns:
|
|
/// <c>
|
|
/// <see cref="Int" />
|
|
/// </c>
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int ManaMax => Int;
|
|
|
|
public Timer PoisonTimer { get; private set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Poison Poison
|
|
{
|
|
get => m_Poison;
|
|
set
|
|
{
|
|
m_Poison = value;
|
|
Delta(MobileDelta.HealthbarPoison);
|
|
|
|
if (PoisonTimer != null)
|
|
{
|
|
PoisonTimer.Stop();
|
|
PoisonTimer = null;
|
|
}
|
|
|
|
if (m_Poison != null)
|
|
{
|
|
PoisonTimer = m_Poison.ConstructTimer(this);
|
|
|
|
PoisonTimer?.Start();
|
|
}
|
|
|
|
CheckStatTimers();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int HairItemID
|
|
{
|
|
get => _hair?.ItemId ?? 0;
|
|
set
|
|
{
|
|
if (_hair == null && value > 0)
|
|
{
|
|
_hair = new VirtualHairInfo(value);
|
|
}
|
|
else if (value <= 0)
|
|
{
|
|
_hair = null;
|
|
}
|
|
else if (_hair != null)
|
|
{
|
|
_hair.ItemId = value;
|
|
}
|
|
|
|
Delta(MobileDelta.Hair);
|
|
this.MarkDirty();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int FacialHairItemID
|
|
{
|
|
get => _facialHair?.ItemId ?? 0;
|
|
set
|
|
{
|
|
if (_facialHair == null && value > 0)
|
|
{
|
|
_facialHair = new VirtualHairInfo(value);
|
|
}
|
|
else if (value <= 0)
|
|
{
|
|
_facialHair = null;
|
|
}
|
|
else if (_facialHair != null)
|
|
{
|
|
_facialHair.ItemId = value;
|
|
}
|
|
|
|
Delta(MobileDelta.FacialHair);
|
|
this.MarkDirty();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int HairHue
|
|
{
|
|
get => _hair?.Hue ?? 0;
|
|
set
|
|
{
|
|
if (_hair != null)
|
|
{
|
|
_hair.Hue = value;
|
|
Delta(MobileDelta.Hair);
|
|
this.MarkDirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int FacialHairHue
|
|
{
|
|
get => _facialHair?.Hue ?? 0;
|
|
set
|
|
{
|
|
if (_facialHair != null)
|
|
{
|
|
_facialHair.Hue = value;
|
|
Delta(MobileDelta.FacialHair);
|
|
this.MarkDirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
public Item ShieldArmor => FindItemOnLayer(Layer.TwoHanded);
|
|
|
|
public Item NeckArmor => FindItemOnLayer(Layer.Neck);
|
|
|
|
public Item HandArmor => FindItemOnLayer(Layer.Gloves);
|
|
|
|
public Item HeadArmor => FindItemOnLayer(Layer.Helm);
|
|
|
|
public Item ArmsArmor => FindItemOnLayer(Layer.Arms);
|
|
|
|
public Item LegsArmor => FindItemOnLayer(Layer.InnerLegs) ?? FindItemOnLayer(Layer.Pants);
|
|
|
|
public Item ChestArmor => FindItemOnLayer(Layer.InnerTorso) ?? FindItemOnLayer(Layer.Shirt);
|
|
|
|
public Item Talisman => FindItemOnLayer(Layer.Talisman);
|
|
|
|
public int CompareTo(Mobile other) => other == null ? -1 : Serial.CompareTo(other.Serial);
|
|
|
|
public virtual int HuedItemID => m_Female ? 0x2107 : 0x2106;
|
|
public ObjectPropertyList PropertyList => m_PropertyList ??= InitializePropertyList(new ObjectPropertyList(this));
|
|
|
|
public virtual void GetProperties(IPropertyList list)
|
|
{
|
|
AddNameProperties(list);
|
|
}
|
|
|
|
[IgnoreDupe]
|
|
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
|
|
public DateTime Created { get; set; } = Core.Now;
|
|
|
|
[IgnoreDupe]
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public Serial Serial { get; }
|
|
|
|
public byte SerializedThread { get; set; }
|
|
public int SerializedPosition { get; set; }
|
|
public int SerializedLength { get; set; }
|
|
|
|
public virtual void Serialize(IGenericWriter writer)
|
|
{
|
|
writer.Write(36); // version
|
|
|
|
writer.WriteDeltaTime(LastStrGain);
|
|
writer.WriteDeltaTime(LastIntGain);
|
|
writer.WriteDeltaTime(LastDexGain);
|
|
|
|
byte hairflag = 0x00;
|
|
|
|
if (_hair != null)
|
|
{
|
|
hairflag |= 0x01;
|
|
}
|
|
|
|
if (_facialHair != null)
|
|
{
|
|
hairflag |= 0x02;
|
|
}
|
|
|
|
writer.Write(hairflag);
|
|
|
|
if ((hairflag & 0x01) != 0)
|
|
{
|
|
_hair?.Serialize(writer);
|
|
}
|
|
|
|
if ((hairflag & 0x02) != 0)
|
|
{
|
|
_facialHair?.Serialize(writer);
|
|
}
|
|
|
|
writer.Write(Race);
|
|
|
|
writer.Write(m_TithingPoints);
|
|
|
|
writer.Write(Corpse);
|
|
|
|
writer.Write(CantWalk);
|
|
|
|
// VirtueInfo.Serialize(writer, Virtues);
|
|
|
|
writer.Write(Thirst);
|
|
writer.Write(BAC);
|
|
|
|
writer.Write(m_FollowersMax);
|
|
|
|
writer.Write(MagicDamageAbsorb);
|
|
|
|
writer.Write(GuildFealty);
|
|
|
|
writer.Write(m_Guild);
|
|
|
|
writer.Write(m_DisplayGuildTitle);
|
|
|
|
writer.Write(CanSwim);
|
|
|
|
writer.Write(Squelched);
|
|
|
|
writer.Write(m_Holding);
|
|
|
|
writer.Write(m_VirtualArmor);
|
|
|
|
writer.Write(BaseSoundID);
|
|
|
|
writer.Write(DisarmReady);
|
|
writer.Write(StunReady);
|
|
|
|
writer.Write(m_StatCap);
|
|
|
|
writer.Write(NameHue);
|
|
|
|
writer.Write(m_Hunger);
|
|
|
|
writer.Write(m_Location);
|
|
writer.Write(m_Body);
|
|
writer.Write(m_Name);
|
|
writer.Write(m_GuildTitle);
|
|
writer.Write(m_Criminal);
|
|
writer.Write(m_Kills);
|
|
writer.Write(SpeechHue);
|
|
writer.Write(EmoteHue);
|
|
writer.Write(WhisperHue);
|
|
writer.Write(YellHue);
|
|
writer.Write(m_Language);
|
|
writer.Write(m_Female);
|
|
writer.Write(m_Warmode);
|
|
writer.Write(m_Hidden);
|
|
writer.Write((byte)m_Direction);
|
|
writer.Write(m_Hue);
|
|
writer.Write(m_Str);
|
|
writer.Write(m_Dex);
|
|
writer.Write(m_Int);
|
|
writer.Write(m_Hits);
|
|
writer.Write(m_Stam);
|
|
writer.Write(m_Mana);
|
|
|
|
writer.Write(m_Map);
|
|
|
|
writer.Write(m_Blessed);
|
|
writer.Write(m_Fame);
|
|
writer.Write(m_Karma);
|
|
writer.Write((byte)m_AccessLevel);
|
|
Skills.Serialize(writer);
|
|
|
|
writer.Write(Items);
|
|
|
|
writer.Write(m_Player);
|
|
writer.Write(m_Title);
|
|
writer.Write(Profile);
|
|
writer.Write(ProfileLocked);
|
|
writer.Write(AutoPageNotify);
|
|
|
|
writer.Write(LogoutLocation);
|
|
writer.Write(LogoutMap);
|
|
|
|
writer.Write((byte)m_StrLock);
|
|
writer.Write((byte)m_DexLock);
|
|
writer.Write((byte)m_IntLock);
|
|
}
|
|
|
|
public bool Deleted { get; private set; }
|
|
|
|
public virtual void Delete()
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_NetState != null)
|
|
{
|
|
m_NetState.CancelAllTrades();
|
|
m_NetState.Disconnect($"Player {this} has been deleted.");
|
|
}
|
|
|
|
DropHolding();
|
|
|
|
Region.OnRegionChange(this, m_Region, null);
|
|
|
|
m_Region = null;
|
|
// Is the above line REALLY needed? The old Region system did NOT have said line
|
|
// and worked fine, because of this a LOT of extra checks have to be done everywhere...
|
|
// I guess this should be there for Garbage collection purposes, but, still, is it /really/ needed?
|
|
|
|
OnDelete();
|
|
|
|
for (var i = Items.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i < Items.Count)
|
|
{
|
|
Items[i].OnParentDeleted(this);
|
|
}
|
|
}
|
|
|
|
SendRemovePacket();
|
|
|
|
m_Guild?.OnDelete(this);
|
|
|
|
Deleted = true;
|
|
|
|
m_Map?.OnLeave(this);
|
|
m_Map = null;
|
|
|
|
_hair = null;
|
|
_facialHair = null;
|
|
m_MountItem = null;
|
|
|
|
World.RemoveEntity(this);
|
|
|
|
OnAfterDelete();
|
|
|
|
m_PropertyList = null;
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public Map Map
|
|
{
|
|
get => m_Map;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_Map != value)
|
|
{
|
|
m_NetState?.ValidateAllTrades();
|
|
|
|
var oldMap = m_Map;
|
|
|
|
if (m_Map != null)
|
|
{
|
|
m_Map.OnLeave(this);
|
|
|
|
ClearScreen();
|
|
SendRemovePacket();
|
|
}
|
|
|
|
for (var i = 0; i < Items.Count; ++i)
|
|
{
|
|
Items[i].Map = value;
|
|
}
|
|
|
|
m_Map = value;
|
|
InternalMapChange(oldMap);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public Point3D Location
|
|
{
|
|
get => m_Location;
|
|
set => SetLocation(value, true);
|
|
}
|
|
|
|
private void InternalMapChange(Map oldMap)
|
|
{
|
|
var ns = m_NetState;
|
|
m_Map?.OnEnter(this);
|
|
UpdateRegion();
|
|
|
|
if (ns != null)
|
|
{
|
|
ns.Sequence = 0;
|
|
|
|
if (m_Map != null)
|
|
{
|
|
ns.SendMapChange(Map);
|
|
|
|
ns.SendMapPatches();
|
|
|
|
ns.SendSeasonChange((byte)GetSeason(), true);
|
|
|
|
ns.SendMobileUpdate(this);
|
|
ns.SendServerChange(m_Location, m_Map);
|
|
}
|
|
|
|
ns.SendMobileIncoming(this, this);
|
|
|
|
ns.SendMobileUpdate(this);
|
|
CheckLightLevels(true);
|
|
ns.SendMobileUpdate(this);
|
|
}
|
|
|
|
SendEverything();
|
|
SendIncomingPacket();
|
|
|
|
ns.SendMobileIncoming(this, this);
|
|
ns.SendSupportedFeature();
|
|
ns.SendMobileUpdate(this);
|
|
ns.SendMobileAttributes(this);
|
|
|
|
OnMapChange(oldMap);
|
|
}
|
|
|
|
public virtual void MoveToWorld(Point3D newLocation, Map map)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_Map == map)
|
|
{
|
|
SetLocation(newLocation, true);
|
|
return;
|
|
}
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
var oldLocation = m_Location;
|
|
var oldMap = m_Map;
|
|
|
|
if (oldMap != null)
|
|
{
|
|
oldMap.OnLeave(this);
|
|
|
|
ClearScreen();
|
|
SendRemovePacket();
|
|
}
|
|
|
|
for (var i = 0; i < Items.Count; ++i)
|
|
{
|
|
Items[i].Map = map;
|
|
}
|
|
|
|
m_Map = map;
|
|
|
|
m_Location = newLocation;
|
|
InternalMapChange(oldMap);
|
|
OnLocationChange(oldLocation);
|
|
|
|
m_Region?.OnLocationChanged(this, oldLocation);
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public int X
|
|
{
|
|
get => m_Location.m_X;
|
|
set => Location = new Point3D(value, m_Location.m_Y, m_Location.m_Z);
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public int Y
|
|
{
|
|
get => m_Location.m_Y;
|
|
set => Location = new Point3D(m_Location.m_X, value, m_Location.m_Z);
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public int Z
|
|
{
|
|
get => m_Location.m_Z;
|
|
set => Location = new Point3D(m_Location.m_X, m_Location.m_Y, value);
|
|
}
|
|
|
|
public virtual void ProcessDelta()
|
|
{
|
|
var delta = m_DeltaFlags;
|
|
if (delta == MobileDelta.None)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var attrs = delta & MobileDelta.Attributes;
|
|
|
|
m_DeltaFlags = MobileDelta.None;
|
|
m_InDeltaQueue = false;
|
|
|
|
bool sendHits = false, sendStam = false, sendMana = false, sendAll = false, sendAny = false;
|
|
bool sendIncoming = false, sendNonlocalIncoming = false;
|
|
bool sendUpdate = false, sendRemove = false;
|
|
bool sendPublicStats = false, sendPrivateStats = false;
|
|
bool sendMoving = false, sendNonlocalMoving = false;
|
|
var sendOPLUpdate = ObjectPropertyList.Enabled && (delta & MobileDelta.Properties) != 0;
|
|
|
|
bool sendHair = false, sendFacialHair = false, removeHair = false, removeFacialHair = false;
|
|
|
|
bool sendHealthbarPoison = false, sendHealthbarYellow = false;
|
|
|
|
if (attrs != MobileDelta.None)
|
|
{
|
|
sendAny = true;
|
|
|
|
if (attrs == MobileDelta.Attributes)
|
|
{
|
|
sendAll = true;
|
|
}
|
|
else
|
|
{
|
|
sendHits = (attrs & MobileDelta.Hits) != 0;
|
|
sendStam = (attrs & MobileDelta.Stam) != 0;
|
|
sendMana = (attrs & MobileDelta.Mana) != 0;
|
|
}
|
|
}
|
|
|
|
if ((delta & MobileDelta.GhostUpdate) != 0)
|
|
{
|
|
sendNonlocalIncoming = true;
|
|
}
|
|
|
|
if ((delta & MobileDelta.Hue) != 0)
|
|
{
|
|
sendNonlocalIncoming = true;
|
|
sendUpdate = true;
|
|
sendRemove = true;
|
|
}
|
|
|
|
if ((delta & MobileDelta.Direction) != 0)
|
|
{
|
|
sendNonlocalMoving = true;
|
|
sendUpdate = true;
|
|
}
|
|
|
|
if ((delta & MobileDelta.Body) != 0)
|
|
{
|
|
sendUpdate = true;
|
|
sendIncoming = true;
|
|
}
|
|
|
|
if ((delta & (MobileDelta.Flags | MobileDelta.Noto)) != 0)
|
|
{
|
|
sendMoving = true;
|
|
}
|
|
|
|
if ((delta & MobileDelta.HealthbarPoison) != 0)
|
|
{
|
|
sendHealthbarPoison = true;
|
|
}
|
|
|
|
if ((delta & MobileDelta.HealthbarYellow) != 0)
|
|
{
|
|
sendHealthbarYellow = true;
|
|
}
|
|
|
|
if ((delta & MobileDelta.Name) != 0)
|
|
{
|
|
sendAll = false;
|
|
sendHits = false;
|
|
sendAny = sendStam || sendMana;
|
|
sendPublicStats = true;
|
|
}
|
|
|
|
if ((delta & (MobileDelta.WeaponDamage | MobileDelta.Resistances | MobileDelta.Stat |
|
|
MobileDelta.Weight | MobileDelta.Gold | MobileDelta.Armor | MobileDelta.StatCap |
|
|
MobileDelta.Followers | MobileDelta.TithingPoints | MobileDelta.Race)) != 0)
|
|
{
|
|
sendPrivateStats = true;
|
|
}
|
|
|
|
if ((delta & MobileDelta.Hair) != 0)
|
|
{
|
|
if (HairItemID <= 0)
|
|
{
|
|
removeHair = true;
|
|
}
|
|
|
|
sendHair = true;
|
|
}
|
|
|
|
if ((delta & MobileDelta.FacialHair) != 0)
|
|
{
|
|
if (FacialHairItemID <= 0)
|
|
{
|
|
removeFacialHair = true;
|
|
}
|
|
|
|
sendFacialHair = true;
|
|
}
|
|
|
|
var hairSerial = Hair.VirtualSerial;
|
|
var hairLength = removeHair
|
|
? OutgoingVirtualHairPackets.RemovePacketLength
|
|
: OutgoingVirtualHairPackets.EquipUpdatePacketLength;
|
|
|
|
Span<byte> hairPacket = stackalloc byte[hairLength].InitializePacket();
|
|
|
|
var facialHairSerial = FacialHair.VirtualSerial;
|
|
var facialHairLength = removeFacialHair
|
|
? OutgoingVirtualHairPackets.RemovePacketLength
|
|
: OutgoingVirtualHairPackets.EquipUpdatePacketLength;
|
|
|
|
Span<byte> facialHairPacket = stackalloc byte[facialHairLength].InitializePacket();
|
|
|
|
const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength;
|
|
|
|
Span<byte> mobileMovingCache = stackalloc byte[cacheLength];
|
|
mobileMovingCache.Clear();
|
|
|
|
var ourState = m_NetState;
|
|
|
|
if (ourState != null)
|
|
{
|
|
if (sendUpdate)
|
|
{
|
|
ourState.Sequence = 0;
|
|
ourState.SendMobileUpdate(this);
|
|
}
|
|
|
|
if (sendIncoming)
|
|
{
|
|
ourState.SendMobileIncoming(this, this);
|
|
}
|
|
|
|
if (sendMoving || !ourState.StygianAbyss && (sendHealthbarPoison || sendHealthbarYellow))
|
|
{
|
|
ourState.SendMobileMovingUsingCache(mobileMovingCache, this, this);
|
|
}
|
|
|
|
if (ourState.StygianAbyss)
|
|
{
|
|
if (sendHealthbarPoison)
|
|
{
|
|
ourState.SendMobileHealthbar(this, Healthbar.Poison);
|
|
}
|
|
|
|
if (sendHealthbarYellow)
|
|
{
|
|
ourState.SendMobileHealthbar(this, Healthbar.Yellow);
|
|
}
|
|
}
|
|
|
|
if (sendPublicStats || sendPrivateStats)
|
|
{
|
|
ourState.SendMobileStatus(this);
|
|
}
|
|
else if (sendAll)
|
|
{
|
|
ourState.SendMobileAttributes(this);
|
|
}
|
|
else if (sendAny)
|
|
{
|
|
if (sendHits)
|
|
{
|
|
ourState.SendMobileHits(this);
|
|
}
|
|
|
|
if (sendMana)
|
|
{
|
|
ourState.SendMobileMana(this);
|
|
}
|
|
|
|
if (sendStam)
|
|
{
|
|
ourState.SendMobileStam(this);
|
|
}
|
|
}
|
|
|
|
if (sendStam || sendMana)
|
|
{
|
|
if (Party is IParty ip)
|
|
{
|
|
if (sendMana)
|
|
{
|
|
ip.OnManaChanged(this);
|
|
}
|
|
|
|
if (sendStam)
|
|
{
|
|
ip.OnStamChanged(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sendHair)
|
|
{
|
|
if (removeHair)
|
|
{
|
|
OutgoingVirtualHairPackets.CreateRemoveHairPacket(hairPacket, (uint)hairSerial);
|
|
}
|
|
else
|
|
{
|
|
OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket(
|
|
hairPacket,
|
|
this,
|
|
(uint)hairSerial,
|
|
HairItemID,
|
|
HairHue,
|
|
Layer.Hair
|
|
);
|
|
}
|
|
|
|
ourState.Send(hairPacket);
|
|
}
|
|
|
|
if (sendFacialHair)
|
|
{
|
|
if (removeFacialHair)
|
|
{
|
|
OutgoingVirtualHairPackets.CreateRemoveHairPacket(facialHairPacket, (uint)facialHairSerial);
|
|
}
|
|
else
|
|
{
|
|
OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket(
|
|
facialHairPacket,
|
|
this,
|
|
(uint)facialHairSerial,
|
|
FacialHairItemID,
|
|
FacialHairHue,
|
|
Layer.FacialHair
|
|
);
|
|
}
|
|
|
|
ourState.Send(facialHairPacket);
|
|
}
|
|
|
|
if (sendOPLUpdate)
|
|
{
|
|
SendOPLPacketTo(ourState);
|
|
}
|
|
}
|
|
|
|
// TODO: Is it even valid to send packets to our state while we have a null map? Look into failing fast
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
sendMoving = sendMoving || sendNonlocalMoving;
|
|
sendIncoming = sendIncoming || sendNonlocalIncoming;
|
|
sendHits = sendHits || sendAll;
|
|
|
|
if (!(sendRemove || sendIncoming || sendPublicStats || sendHits || sendMoving ||
|
|
sendOPLUpdate || sendHair || sendFacialHair || sendHealthbarPoison ||
|
|
sendHealthbarYellow))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> statBufferTrue = stackalloc byte[OutgoingMobilePackets.MobileStatusCompactLength].InitializePacket();
|
|
Span<byte> statBufferFalse = stackalloc byte[OutgoingMobilePackets.MobileStatusCompactLength].InitializePacket();
|
|
Span<byte> hbpBuffer = stackalloc byte[OutgoingMobilePackets.MobileHealthbarPacketLength].InitializePacket();
|
|
Span<byte> hbyBuffer = stackalloc byte[OutgoingMobilePackets.MobileHealthbarPacketLength].InitializePacket();
|
|
Span<byte> deadBuffer = stackalloc byte[OutgoingMobilePackets.BondedStatusPacketLength].InitializePacket();
|
|
Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
|
Span<byte> hitsPacket = stackalloc byte[OutgoingMobilePackets.MobileAttributePacketLength].InitializePacket();
|
|
|
|
foreach (var state in Map.GetClientsInRange(m_Location))
|
|
{
|
|
var beholder = state.Mobile;
|
|
|
|
if (beholder == this || !beholder.CanSee(this))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (sendRemove)
|
|
{
|
|
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
|
state.Send(removeEntity);
|
|
}
|
|
|
|
if (sendIncoming)
|
|
{
|
|
state.SendMobileIncoming(beholder, this);
|
|
|
|
if (IsDeadBondedPet)
|
|
{
|
|
OutgoingMobilePackets.CreateBondedStatus(deadBuffer, Serial, true);
|
|
state.Send(deadBuffer);
|
|
}
|
|
}
|
|
|
|
if (sendMoving || !state.StygianAbyss && (sendHealthbarPoison || sendHealthbarYellow))
|
|
{
|
|
state.SendMobileMovingUsingCache(mobileMovingCache, beholder, this);
|
|
}
|
|
|
|
if (state.StygianAbyss)
|
|
{
|
|
if (sendHealthbarPoison)
|
|
{
|
|
OutgoingMobilePackets.CreateMobileHealthbar(hbpBuffer, this, Healthbar.Poison);
|
|
state.Send(hbpBuffer);
|
|
}
|
|
|
|
if (sendHealthbarYellow)
|
|
{
|
|
OutgoingMobilePackets.CreateMobileHealthbar(hbyBuffer, this, Healthbar.Yellow);
|
|
state.Send(hbyBuffer);
|
|
}
|
|
}
|
|
|
|
if (sendPublicStats)
|
|
{
|
|
if (CanBeRenamedBy(beholder))
|
|
{
|
|
OutgoingMobilePackets.CreateMobileStatusCompact(statBufferTrue, this, true);
|
|
state.Send(statBufferTrue);
|
|
}
|
|
else
|
|
{
|
|
OutgoingMobilePackets.CreateMobileStatusCompact(statBufferFalse, this, false);
|
|
state.Send(statBufferFalse);
|
|
}
|
|
}
|
|
else if (sendHits)
|
|
{
|
|
OutgoingMobilePackets.CreateMobileHits(hitsPacket, this, true);
|
|
state.Send(hitsPacket);
|
|
}
|
|
|
|
if (sendHair)
|
|
{
|
|
if (removeHair)
|
|
{
|
|
OutgoingVirtualHairPackets.CreateRemoveHairPacket(hairPacket, (uint)hairSerial);
|
|
}
|
|
else
|
|
{
|
|
OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket(
|
|
hairPacket,
|
|
this,
|
|
(uint)hairSerial,
|
|
HairItemID,
|
|
HairHue,
|
|
Layer.Hair
|
|
);
|
|
}
|
|
|
|
state.Send(hairPacket);
|
|
}
|
|
|
|
if (sendFacialHair)
|
|
{
|
|
if (removeFacialHair)
|
|
{
|
|
OutgoingVirtualHairPackets.CreateRemoveHairPacket(facialHairPacket, (uint)facialHairSerial);
|
|
}
|
|
else
|
|
{
|
|
OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket(
|
|
facialHairPacket,
|
|
this,
|
|
(uint)facialHairSerial,
|
|
FacialHairItemID,
|
|
FacialHairHue,
|
|
Layer.FacialHair
|
|
);
|
|
}
|
|
|
|
state.Send(facialHairPacket);
|
|
}
|
|
|
|
SendOPLPacketTo(state);
|
|
}
|
|
}
|
|
|
|
public ISpawner Spawner { get; set; }
|
|
|
|
public virtual void OnBeforeSpawn(Point3D location, Map m)
|
|
{
|
|
}
|
|
|
|
public virtual void OnAfterSpawn()
|
|
{
|
|
}
|
|
|
|
public virtual bool InRange(Point2D p, int range) =>
|
|
p.m_X >= Location.m_X - range
|
|
&& p.m_X <= Location.m_X + range
|
|
&& p.m_Y >= Location.m_Y - range
|
|
&& p.m_Y <= Location.m_Y + range;
|
|
|
|
public virtual bool InRange(Point3D p, int range) =>
|
|
p.m_X >= Location.m_X - range
|
|
&& p.m_X <= Location.m_X + range
|
|
&& p.m_Y >= Location.m_Y - range
|
|
&& p.m_Y <= Location.m_Y + range;
|
|
|
|
public virtual bool InRange(IPoint2D p, int range) =>
|
|
p.X >= Location.m_X - range
|
|
&& p.X <= Location.m_X + range
|
|
&& p.Y >= Location.m_Y - range
|
|
&& p.Y <= Location.m_Y + range;
|
|
|
|
protected virtual void OnRaceChange(Race oldRace)
|
|
{
|
|
}
|
|
|
|
public virtual void ComputeLightLevels(out int global, out int personal)
|
|
{
|
|
ComputeBaseLightLevels(out global, out personal);
|
|
|
|
m_Region?.AlterLightLevel(this, ref global, ref personal);
|
|
}
|
|
|
|
public virtual void ComputeBaseLightLevels(out int global, out int personal)
|
|
{
|
|
global = 0;
|
|
personal = m_LightLevel;
|
|
}
|
|
|
|
public virtual void CheckLightLevels(bool forceResend)
|
|
{
|
|
}
|
|
|
|
public virtual void UpdateResistances()
|
|
{
|
|
var delta = false;
|
|
|
|
if (Resistances == null)
|
|
{
|
|
Resistances = new[] { int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue };
|
|
}
|
|
else
|
|
{
|
|
for (var i = 0; i < Resistances.Length; ++i)
|
|
{
|
|
if (Resistances[i] != int.MinValue)
|
|
{
|
|
Resistances[i] = int.MinValue;
|
|
delta = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (delta)
|
|
{
|
|
Delta(MobileDelta.Resistances);
|
|
}
|
|
}
|
|
|
|
public virtual int GetResistance(ResistanceType type)
|
|
{
|
|
var v = (int)type;
|
|
|
|
if (Resistances == null || Resistances.Length == 0 || v < 0 || v >= Resistances.Length)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var res = Resistances[v];
|
|
|
|
if (res == int.MinValue)
|
|
{
|
|
ComputeResistances();
|
|
res = Resistances[v];
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
public virtual void AddResistanceMod(ResistanceMod mod)
|
|
{
|
|
if (mod == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_resistanceMods ??= new List<ResistanceMod>();
|
|
_resistanceMods.Add(mod);
|
|
|
|
UpdateResistances();
|
|
}
|
|
|
|
public virtual ResistanceMod GetResistanceMod(string name)
|
|
{
|
|
if (_resistanceMods == null || name == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
for (var i = 0; i < _resistanceMods.Count; i++)
|
|
{
|
|
var mod = _resistanceMods[i];
|
|
if (mod.Name == name)
|
|
{
|
|
return mod;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public virtual void RemoveResistanceMod(string name)
|
|
{
|
|
if (_resistanceMods == null || name == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = _resistanceMods.Count - 1; i >= 0; i--)
|
|
{
|
|
var mod = _resistanceMods[i];
|
|
if (mod.Name == name)
|
|
{
|
|
_resistanceMods.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
if (_resistanceMods.Count == 0)
|
|
{
|
|
_resistanceMods = null;
|
|
}
|
|
|
|
UpdateResistances();
|
|
}
|
|
|
|
public virtual void RemoveResistanceMod(ResistanceMod mod)
|
|
{
|
|
if (_resistanceMods == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_resistanceMods.Remove(mod);
|
|
|
|
if (_resistanceMods.Count == 0)
|
|
{
|
|
_resistanceMods = null;
|
|
}
|
|
|
|
UpdateResistances();
|
|
}
|
|
|
|
public virtual void ComputeResistances()
|
|
{
|
|
Resistances ??= new[] { int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue };
|
|
|
|
for (var i = 0; i < Resistances.Length; ++i)
|
|
{
|
|
Resistances[i] = 0;
|
|
}
|
|
|
|
Resistances[0] += BasePhysicalResistance;
|
|
Resistances[1] += BaseFireResistance;
|
|
Resistances[2] += BaseColdResistance;
|
|
Resistances[3] += BasePoisonResistance;
|
|
Resistances[4] += BaseEnergyResistance;
|
|
|
|
if (_resistanceMods != null)
|
|
{
|
|
for (var i = 0; i < _resistanceMods.Count; i++)
|
|
{
|
|
var mod = _resistanceMods[i];
|
|
var v = (int)mod.Type;
|
|
|
|
if (v >= 0 && v < Resistances.Length)
|
|
{
|
|
Resistances[v] += mod.Offset;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < Items.Count; ++i)
|
|
{
|
|
var item = Items[i];
|
|
|
|
if (item.CheckPropertyConflict(this))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Resistances[0] += item.PhysicalResistance;
|
|
Resistances[1] += item.FireResistance;
|
|
Resistances[2] += item.ColdResistance;
|
|
Resistances[3] += item.PoisonResistance;
|
|
Resistances[4] += item.EnergyResistance;
|
|
}
|
|
|
|
for (var i = 0; i < Resistances.Length; ++i)
|
|
{
|
|
var min = GetMinResistance((ResistanceType)i);
|
|
var max = GetMaxResistance((ResistanceType)i);
|
|
|
|
if (max < min)
|
|
{
|
|
max = min;
|
|
}
|
|
|
|
if (Resistances[i] > max)
|
|
{
|
|
Resistances[i] = max;
|
|
}
|
|
else if (Resistances[i] < min)
|
|
{
|
|
Resistances[i] = min;
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual int GetMinResistance(ResistanceType type) => int.MinValue;
|
|
|
|
public virtual int GetMaxResistance(ResistanceType type) => m_Player ? MaxPlayerResistance : int.MaxValue;
|
|
|
|
public int GetAOSStatus(int index) => AOSStatusHandler?.Invoke(this, index) ?? 0;
|
|
|
|
public virtual void SendPropertiesTo(NetState ns)
|
|
{
|
|
ns?.Send(PropertyList.Buffer);
|
|
}
|
|
|
|
public virtual void OnAosSingleClick(Mobile from)
|
|
{
|
|
var opl = PropertyList;
|
|
|
|
if (opl.Header > 0)
|
|
{
|
|
int hue;
|
|
|
|
if (NameHue != -1)
|
|
{
|
|
hue = NameHue;
|
|
}
|
|
else if (m_AccessLevel > AccessLevel.Player)
|
|
{
|
|
hue = 11;
|
|
}
|
|
else
|
|
{
|
|
hue = Notoriety.GetHue(Notoriety.Compute(from, this));
|
|
}
|
|
|
|
from.NetState.SendMessageLocalized(Serial, Body, MessageType.Label, hue, 3, opl.Header, Name, opl.HeaderArgs);
|
|
}
|
|
}
|
|
|
|
public virtual string ApplyNameSuffix(string suffix) => suffix;
|
|
|
|
public virtual void AddNameProperties(IPropertyList list)
|
|
{
|
|
string prefix;
|
|
|
|
if (ShowFameTitle && (m_Player || m_Body.IsHuman) && m_Fame >= 10000)
|
|
{
|
|
prefix = m_Female ? "Lady " : "Lord ";
|
|
}
|
|
else
|
|
{
|
|
prefix = " ";
|
|
}
|
|
|
|
var guild = m_Guild;
|
|
var hasTitle = PropertyTitle && !string.IsNullOrEmpty(Title);
|
|
var hasGuild = guild != null && (m_Player || m_DisplayGuildTitle);
|
|
|
|
string suffix = hasTitle switch
|
|
{
|
|
true when hasGuild => $" {Title} [{guild.Abbreviation.FixHtmlFormattable()}]",
|
|
true => $" {Title}",
|
|
false when hasGuild => $" [{guild.Abbreviation.FixHtmlFormattable()}]",
|
|
_ => " "
|
|
};
|
|
|
|
list.Add(1050045, $"{prefix}\t{Name ?? " "}\t{ApplyNameSuffix(suffix)}"); // ~1_PREFIX~~2_NAME~~3_SUFFIX~
|
|
|
|
if (guild != null && (m_DisplayGuildTitle || m_Player && guild.Type != GuildType.Regular))
|
|
{
|
|
var type = guild.Type >= 0 && (int)guild.Type < m_GuildTypes.Length ? m_GuildTypes[(int)guild.Type] : "";
|
|
|
|
var guildTitle = GuildTitle?.Trim() ?? "";
|
|
|
|
if (guildTitle.Length > 0)
|
|
{
|
|
if (NewGuildDisplay)
|
|
{
|
|
list.Add($"{guildTitle.FixHtmlFormattable()}, {guild.Name.FixHtmlFormattable()}");
|
|
}
|
|
else
|
|
{
|
|
list.Add($"{guildTitle.FixHtmlFormattable()}, {guild.Name.FixHtmlFormattable()} Guild{type}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
list.Add(guild.Name.FixHtml());
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void GetChildProperties(IPropertyList list, Item item)
|
|
{
|
|
}
|
|
|
|
public virtual void GetChildNameProperties(IPropertyList list, Item item)
|
|
{
|
|
}
|
|
|
|
private void UpdateAggrExpire()
|
|
{
|
|
if (Deleted || Aggressors.Count == 0 && Aggressed.Count == 0)
|
|
{
|
|
StopAggrExpire();
|
|
}
|
|
else if (!_expireAggrTimerToken.Running)
|
|
{
|
|
Timer.StartTimer(ExpireAggressorsDelay, ExpireAggressorsDelay, ExpireAggr, out _expireAggrTimerToken);
|
|
}
|
|
}
|
|
|
|
private void ExpireAggr()
|
|
{
|
|
if (Deleted || Aggressors.Count == 0 && Aggressed.Count == 0)
|
|
{
|
|
StopAggrExpire();
|
|
}
|
|
else
|
|
{
|
|
CheckAggrExpire();
|
|
}
|
|
}
|
|
|
|
private void StopAggrExpire()
|
|
{
|
|
_expireAggrTimerToken.Cancel();
|
|
}
|
|
|
|
private void CheckAggrExpire()
|
|
{
|
|
for (var i = Aggressors.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i >= Aggressors.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var info = Aggressors[i];
|
|
|
|
if (info.Expired)
|
|
{
|
|
var attacker = info.Attacker;
|
|
attacker.RemoveAggressed(this);
|
|
|
|
Aggressors.RemoveAt(i);
|
|
info.Free();
|
|
|
|
if (m_NetState != null && CanSee(attacker) && Utility.InUpdateRange(m_Location, attacker.m_Location))
|
|
{
|
|
m_NetState.SendMobileIncoming(this, attacker);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var i = Aggressed.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i >= Aggressed.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var info = Aggressed[i];
|
|
|
|
if (info.Expired)
|
|
{
|
|
var defender = info.Defender;
|
|
defender.RemoveAggressor(this);
|
|
|
|
Aggressed.RemoveAt(i);
|
|
info.Free();
|
|
|
|
if (m_NetState != null && CanSee(defender) && Utility.InUpdateRange(m_Location, defender.m_Location))
|
|
{
|
|
m_NetState.SendMobileIncoming(this, defender);
|
|
}
|
|
}
|
|
}
|
|
|
|
UpdateAggrExpire();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="skill" /> changes in some way.
|
|
/// </summary>
|
|
public virtual void OnSkillInvalidated(Skill skill)
|
|
{
|
|
}
|
|
|
|
public virtual void ValidateSkillMods()
|
|
{
|
|
if (_skillMods == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = _skillMods.Count - 1; i >= 0; i--)
|
|
{
|
|
var mod = _skillMods[i];
|
|
|
|
if (!mod.CheckCondition())
|
|
{
|
|
_skillMods.RemoveAt(i);
|
|
mod.Owner = null;
|
|
Skills[mod.Skill]?.Update();
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void AddSkillMod(SkillMod mod)
|
|
{
|
|
if (mod == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ValidateSkillMods();
|
|
_skillMods ??= new List<SkillMod>();
|
|
_skillMods.Add(mod);
|
|
mod.Owner = this;
|
|
Skills[mod.Skill]?.Update();
|
|
}
|
|
|
|
public virtual SkillMod GetSkillMod(string name)
|
|
{
|
|
if (_skillMods == null || name == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
ValidateSkillMods();
|
|
|
|
for (var i = 0; i < _skillMods.Count; i++)
|
|
{
|
|
var mod = _skillMods[i];
|
|
if (mod.Name == name)
|
|
{
|
|
return mod;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public virtual void RemoveSkillMod(string name)
|
|
{
|
|
if (_skillMods == null || name == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = _skillMods.Count - 1; i >= 0; i--)
|
|
{
|
|
var mod = _skillMods[i];
|
|
if (mod.Name == name)
|
|
{
|
|
mod.Owner = null;
|
|
Skills[mod.Skill]?.Update();
|
|
_skillMods.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
ValidateSkillMods();
|
|
|
|
if (_skillMods.Count == 0)
|
|
{
|
|
_skillMods = null;
|
|
}
|
|
}
|
|
|
|
public virtual void RemoveSkillMod(SkillMod mod)
|
|
{
|
|
if (_skillMods == null || mod == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_skillMods.Remove(mod))
|
|
{
|
|
mod.Owner = null;
|
|
Skills[mod.Skill]?.Update();
|
|
}
|
|
|
|
ValidateSkillMods();
|
|
|
|
if (_skillMods.Count == 0)
|
|
{
|
|
_skillMods = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a client, <paramref name="from" />, invokes a 'help request' for the Mobile.
|
|
/// Seemingly no longer functional in newer clients.
|
|
/// </summary>
|
|
public virtual void OnHelpRequest(Mobile from)
|
|
{
|
|
}
|
|
|
|
public void DelayChangeWarmode(bool value)
|
|
{
|
|
if (m_WarmodeChanges > WarmodeCatchCount)
|
|
{
|
|
_warmodeSpamValue = value;
|
|
m_WarmodeChanges++;
|
|
return;
|
|
}
|
|
|
|
if (m_Warmode == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var now = Core.Now;
|
|
var next = m_NextWarmodeChange;
|
|
|
|
if (now > next || m_WarmodeChanges == 0)
|
|
{
|
|
m_WarmodeChanges = 1;
|
|
m_NextWarmodeChange = now + WarmodeSpamCatch;
|
|
}
|
|
else if (m_WarmodeChanges++ == WarmodeCatchCount)
|
|
{
|
|
Timer.StartTimer(WarmodeSpamDelay, WarmodeSpamTimeout);
|
|
return;
|
|
}
|
|
|
|
Warmode = value;
|
|
}
|
|
|
|
private void WarmodeSpamTimeout()
|
|
{
|
|
Warmode = _warmodeSpamValue;
|
|
m_WarmodeChanges = 0;
|
|
}
|
|
|
|
public bool InLOS(Mobile target) =>
|
|
!Deleted && m_Map != null &&
|
|
(target == this || m_AccessLevel > AccessLevel.Player || m_Map.LineOfSight(this, target));
|
|
|
|
public bool InLOS(object target) =>
|
|
!Deleted && m_Map != null &&
|
|
(target == this || m_AccessLevel > AccessLevel.Player || target is Item item && item.RootParent == this
|
|
|| m_Map.LineOfSight(this, target));
|
|
|
|
public bool InLOS(Point3D target) =>
|
|
!Deleted && m_Map != null && (m_AccessLevel > AccessLevel.Player || m_Map.LineOfSight(this, target));
|
|
|
|
public bool AtPoint(int x, int y) => m_Location.m_X == x && m_Location.m_Y == y;
|
|
|
|
public bool BeginAction<T>() => BeginAction(typeof(T));
|
|
|
|
public bool BeginAction(object toLock)
|
|
{
|
|
if (_actions == null)
|
|
{
|
|
_actions = new List<object> { toLock };
|
|
return true;
|
|
}
|
|
|
|
if (!_actions.Contains(toLock))
|
|
{
|
|
_actions.Add(toLock);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool CanBeginAction<T>() => CanBeginAction(typeof(T));
|
|
|
|
public bool CanBeginAction(object toLock) => _actions?.Contains(toLock) != true;
|
|
|
|
public void EndAction<T>() => EndAction(typeof(T));
|
|
|
|
public void EndAction(object toLock)
|
|
{
|
|
if (_actions != null)
|
|
{
|
|
_actions.Remove(toLock);
|
|
|
|
if (_actions.Count == 0)
|
|
{
|
|
_actions = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual TimeSpan GetLogoutDelay() => Region.GetLogoutDelay(this);
|
|
|
|
private void ExpireParalyzed()
|
|
{
|
|
Paralyzed = false;
|
|
}
|
|
|
|
public virtual void Paralyze(TimeSpan duration)
|
|
{
|
|
if (!m_Paralyzed)
|
|
{
|
|
Paralyzed = true;
|
|
Timer.StartTimer(duration, ExpireParalyzed, out _paraTimerToken);
|
|
}
|
|
}
|
|
|
|
private void ExpireFrozen()
|
|
{
|
|
Frozen = false;
|
|
}
|
|
|
|
public virtual void Freeze(TimeSpan duration)
|
|
{
|
|
if (!m_Frozen)
|
|
{
|
|
Frozen = true;
|
|
Timer.StartTimer(duration, ExpireFrozen, out _frozenTimerToken);
|
|
}
|
|
}
|
|
|
|
public override string ToString() => $"{Serial} \"{Name}\"";
|
|
|
|
public virtual void SendSkillMessage()
|
|
{
|
|
if (NextActionMessage - Core.TickCount >= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NextActionMessage = Core.TickCount + ActionMessageDelay;
|
|
|
|
SendLocalizedMessage(500118); // You must wait a few moments to use another skill.
|
|
}
|
|
|
|
public virtual void SendActionMessage()
|
|
{
|
|
if (NextActionMessage - Core.TickCount >= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NextActionMessage = Core.TickCount + ActionMessageDelay;
|
|
|
|
SendLocalizedMessage(500119); // You must wait to perform another action.
|
|
}
|
|
|
|
public virtual void ClearHands()
|
|
{
|
|
ClearHand(FindItemOnLayer(Layer.OneHanded));
|
|
ClearHand(FindItemOnLayer(Layer.TwoHanded));
|
|
}
|
|
|
|
public virtual void ClearHand(Item item)
|
|
{
|
|
if (item?.Movable == true && !item.AllowEquippedCast(this))
|
|
{
|
|
var pack = Backpack;
|
|
|
|
if (pack == null)
|
|
{
|
|
AddToBackpack(item);
|
|
}
|
|
else
|
|
{
|
|
pack.DropItem(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void Attack(Mobile m)
|
|
{
|
|
if (CheckAttack(m))
|
|
{
|
|
Combatant = m;
|
|
}
|
|
}
|
|
|
|
public virtual bool CheckAttack(Mobile m) => Utility.InUpdateRange(Location, m.Location) && CanSee(m) && InLOS(m);
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after the <see cref="Combatant" /> property has changed.
|
|
/// <seealso cref="Combatant" />
|
|
/// </summary>
|
|
public virtual void OnCombatantChange()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after the <see cref="Hunger" /> property has changed.
|
|
/// <seealso cref="Hunger" />
|
|
/// </summary>
|
|
public virtual void OnHungerChanged(int oldValue)
|
|
{
|
|
}
|
|
|
|
public double GetDistanceToSqrt(Point3D p)
|
|
{
|
|
var xDelta = m_Location.m_X - p.m_X;
|
|
var yDelta = m_Location.m_Y - p.m_Y;
|
|
|
|
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
|
|
}
|
|
|
|
public double GetDistanceToSqrt(Mobile m)
|
|
{
|
|
var xDelta = m_Location.m_X - m.m_Location.m_X;
|
|
var yDelta = m_Location.m_Y - m.m_Location.m_Y;
|
|
|
|
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
|
|
}
|
|
|
|
public double GetDistanceToSqrt(Point2D p)
|
|
{
|
|
var xDelta = m_Location.m_X - p.X;
|
|
var yDelta = m_Location.m_Y - p.Y;
|
|
|
|
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
|
|
}
|
|
|
|
public double GetDistanceToSqrt(IPoint2D p)
|
|
{
|
|
var xDelta = m_Location.m_X - p.X;
|
|
var yDelta = m_Location.m_Y - p.Y;
|
|
|
|
return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
|
|
}
|
|
|
|
public virtual void AggressiveAction(Mobile aggressor) => AggressiveAction(aggressor, false);
|
|
|
|
public virtual void AggressiveAction(Mobile aggressor, bool criminal)
|
|
{
|
|
if (aggressor == this)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var args = AggressiveActionEventArgs.Create(this, aggressor, criminal);
|
|
|
|
EventSink.InvokeAggressiveAction(args);
|
|
|
|
args.Free();
|
|
|
|
if (Combatant == aggressor)
|
|
{
|
|
_expireCombatantTimerToken.Cancel();
|
|
Timer.StartTimer(ExpireCombatantDelay, ExpireCombatant, out _expireCombatantTimerToken);
|
|
}
|
|
|
|
var addAggressor = true;
|
|
|
|
var list = Aggressors;
|
|
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
var info = list[i];
|
|
|
|
if (info.Attacker == aggressor)
|
|
{
|
|
info.Refresh();
|
|
info.CriminalAggression = criminal;
|
|
info.CanReportMurder = criminal;
|
|
|
|
addAggressor = false;
|
|
}
|
|
}
|
|
|
|
list = aggressor.Aggressors;
|
|
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
var info = list[i];
|
|
|
|
if (info.Attacker == this)
|
|
{
|
|
info.Refresh();
|
|
|
|
addAggressor = false;
|
|
}
|
|
}
|
|
|
|
var addAggressed = true;
|
|
|
|
list = Aggressed;
|
|
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
var info = list[i];
|
|
|
|
if (info.Defender == aggressor)
|
|
{
|
|
info.Refresh();
|
|
|
|
addAggressed = false;
|
|
}
|
|
}
|
|
|
|
list = aggressor.Aggressed;
|
|
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
var info = list[i];
|
|
|
|
if (info.Defender == this)
|
|
{
|
|
info.Refresh();
|
|
info.CriminalAggression = criminal;
|
|
info.CanReportMurder = criminal;
|
|
|
|
addAggressed = false;
|
|
}
|
|
}
|
|
|
|
var setCombatant = false;
|
|
|
|
if (addAggressor)
|
|
{
|
|
Aggressors.Add(
|
|
AggressorInfo.Create(
|
|
aggressor,
|
|
this,
|
|
criminal
|
|
)
|
|
);
|
|
|
|
if (CanSee(aggressor))
|
|
{
|
|
m_NetState.SendMobileIncoming(this, aggressor);
|
|
}
|
|
|
|
if (Combatant == null)
|
|
{
|
|
setCombatant = true;
|
|
}
|
|
|
|
UpdateAggrExpire();
|
|
}
|
|
|
|
if (addAggressed)
|
|
{
|
|
aggressor.Aggressed.Add(
|
|
AggressorInfo.Create(
|
|
aggressor,
|
|
this,
|
|
criminal
|
|
)
|
|
);
|
|
|
|
if (CanSee(aggressor))
|
|
{
|
|
m_NetState.SendMobileIncoming(this, aggressor);
|
|
}
|
|
|
|
if (Combatant == null)
|
|
{
|
|
setCombatant = true;
|
|
}
|
|
|
|
UpdateAggrExpire();
|
|
}
|
|
|
|
if (setCombatant)
|
|
{
|
|
Combatant = aggressor;
|
|
}
|
|
|
|
Region.OnAggressed(aggressor, this, criminal);
|
|
}
|
|
|
|
public void RemoveAggressed(Mobile aggressed)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var list = Aggressed;
|
|
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
var info = list[i];
|
|
|
|
if (info.Defender == aggressed)
|
|
{
|
|
Aggressed.RemoveAt(i);
|
|
info.Free();
|
|
|
|
if (m_NetState != null && CanSee(aggressed))
|
|
{
|
|
m_NetState.SendMobileIncoming(this, aggressed);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
UpdateAggrExpire();
|
|
}
|
|
|
|
public void RemoveAggressor(Mobile aggressor)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var list = Aggressors;
|
|
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
var info = list[i];
|
|
|
|
if (info.Attacker == aggressor)
|
|
{
|
|
Aggressors.RemoveAt(i);
|
|
info.Free();
|
|
|
|
if (m_NetState != null && CanSee(aggressor))
|
|
{
|
|
m_NetState.SendMobileIncoming(this, aggressor);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
UpdateAggrExpire();
|
|
}
|
|
|
|
public virtual int GetTotal(TotalType type) =>
|
|
type switch
|
|
{
|
|
TotalType.Gold => m_TotalGold,
|
|
TotalType.Items => m_TotalItems,
|
|
TotalType.Weight => m_TotalWeight,
|
|
_ => 0
|
|
};
|
|
|
|
public virtual void UpdateTotal(Item sender, TotalType type, int delta)
|
|
{
|
|
if (delta == 0 || sender.IsVirtualItem)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (type)
|
|
{
|
|
default:
|
|
{
|
|
m_TotalGold += delta;
|
|
Delta(MobileDelta.Gold);
|
|
break;
|
|
}
|
|
|
|
case TotalType.Items:
|
|
{
|
|
m_TotalItems += delta;
|
|
break;
|
|
}
|
|
|
|
case TotalType.Weight:
|
|
{
|
|
m_TotalWeight += delta;
|
|
Delta(MobileDelta.Weight);
|
|
OnWeightChange(m_TotalWeight - delta);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void UpdateTotals()
|
|
{
|
|
if (Items == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var oldWeight = m_TotalWeight;
|
|
|
|
m_TotalGold = 0;
|
|
m_TotalItems = 0;
|
|
m_TotalWeight = 0;
|
|
|
|
for (var i = 0; i < Items.Count; ++i)
|
|
{
|
|
var item = Items[i];
|
|
|
|
item.UpdateTotals();
|
|
|
|
if (item.IsVirtualItem)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
m_TotalGold += item.TotalGold;
|
|
m_TotalItems += item.TotalItems + 1;
|
|
m_TotalWeight += item.TotalWeight + item.PileWeight;
|
|
}
|
|
|
|
if (m_Holding != null)
|
|
{
|
|
m_TotalWeight += m_Holding.TotalWeight + m_Holding.PileWeight;
|
|
}
|
|
|
|
if (m_TotalWeight != oldWeight)
|
|
{
|
|
OnWeightChange(oldWeight);
|
|
}
|
|
}
|
|
|
|
public void ClearTarget() => m_Target = null;
|
|
|
|
public Target BeginTarget(int range, bool allowGround, TargetFlags flags, TargetCallback callback) =>
|
|
Target = new SimpleTarget(range, flags, allowGround, callback);
|
|
|
|
public Target BeginTarget<T>(
|
|
int range, bool allowGround, TargetFlags flags, TargetStateCallback<T> callback,
|
|
T state
|
|
) =>
|
|
Target = new SimpleStateTarget<T>(range, flags, allowGround, callback, state);
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after the <see cref="Target">Target property</see> has changed.
|
|
/// </summary>
|
|
protected virtual void OnTargetChange()
|
|
{
|
|
}
|
|
|
|
public virtual bool CheckContextMenuDisplay(IEntity target) => true;
|
|
|
|
private bool InternalOnMove(Direction d)
|
|
{
|
|
if (!OnMove(d))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var e = MovementEventArgs.Create(this, d);
|
|
|
|
EventSink.InvokeMovement(e);
|
|
|
|
var ret = !e.Blocked;
|
|
|
|
e.Free();
|
|
|
|
return ret;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked before the Mobile <see cref="Move">moves</see>.
|
|
/// </summary>
|
|
/// <returns>True if the move is allowed, false if not.</returns>
|
|
protected virtual bool OnMove(Direction d)
|
|
{
|
|
if (m_Hidden && m_AccessLevel == AccessLevel.Player)
|
|
{
|
|
if (AllowedStealthSteps-- <= 0 || (d & Direction.Running) != 0 || Mounted)
|
|
{
|
|
RevealingAction();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool CheckMovement(Direction d, out int newZ) => CalcMoves.CheckMovement(this, d, out newZ);
|
|
|
|
private bool CanMove(Direction d, Point3D oldLocation, out Point3D newLocation)
|
|
{
|
|
newLocation = oldLocation;
|
|
|
|
if (m_Spell?.OnCasterMoving(d) == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (m_Paralyzed || m_Frozen)
|
|
{
|
|
SendLocalizedMessage(500111); // You are frozen and can not move.
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!CheckMovement(d, out var newZ))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var oldX = oldLocation.m_X;
|
|
var oldY = oldLocation.m_Y;
|
|
var oldZ = oldLocation.m_Z;
|
|
var x = oldX;
|
|
var y = oldY;
|
|
CalcMoves.Offset(d, ref x, ref y);
|
|
newLocation = new Point3D(x, y, newZ);
|
|
|
|
Pushing = false;
|
|
|
|
var map = m_Map;
|
|
|
|
if (map == null || !Region.CanMove(this, d, newLocation, oldLocation, map))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var oldSector = map.GetSector(oldX, oldY);
|
|
var newSector = map.GetSector(x, y);
|
|
|
|
if (oldSector != newSector)
|
|
{
|
|
using var queue = PooledRefQueue<IEntity>.Create(2048);
|
|
foreach (var m in oldSector.Mobiles)
|
|
{
|
|
if (m != this && m.X == oldX && m.Y == oldY && m.Z + 15 > oldZ && oldZ + 15 > m.Z)
|
|
{
|
|
queue.Enqueue(m);
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
if (!queue.Dequeue().OnMoveOff(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
foreach (var item in oldSector.Items)
|
|
{
|
|
if (item.AtWorldPoint(oldX, oldY) &&
|
|
(item.Z == oldZ || item.Z + item.ItemData.Height > oldZ && oldZ + 15 > item.Z))
|
|
{
|
|
queue.Enqueue(item);
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
if (!queue.Dequeue().OnMoveOff(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
foreach (var m in newSector.Mobiles)
|
|
{
|
|
if (m.X == x && m.Y == y && m.Z + 15 > newZ && newZ + 15 > m.Z)
|
|
{
|
|
queue.Enqueue(m);
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
if (!queue.Dequeue().OnMoveOver(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
foreach (var item in newSector.Items)
|
|
{
|
|
if (item.AtWorldPoint(x, y) &&
|
|
(item.Z == newZ || item.Z + item.ItemData.Height > newZ && newZ + 15 > item.Z))
|
|
{
|
|
queue.Enqueue(item);
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
if (!queue.Dequeue().OnMoveOver(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
using var queue = PooledRefQueue<(IEntity, byte)>.Create(2048);
|
|
foreach (var m in oldSector.Mobiles)
|
|
{
|
|
byte flag;
|
|
if (m != this && m.X == oldX && m.Y == oldY && m.Z + 15 > oldZ && oldZ + 15 > m.Z)
|
|
{
|
|
flag = 1;
|
|
}
|
|
else
|
|
{
|
|
flag = 0;
|
|
}
|
|
|
|
if (m.X == x && m.Y == y && m.Z + 15 > newZ && newZ + 15 > m.Z)
|
|
{
|
|
flag += 2;
|
|
}
|
|
|
|
if (flag > 0)
|
|
{
|
|
queue.Enqueue((m, flag));
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var (entity, flag) = queue.Dequeue();
|
|
|
|
if (flag > 0 && !entity.OnMoveOff(this) || flag > 1 && !entity.OnMoveOver(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
foreach (var item in oldSector.Items)
|
|
{
|
|
byte flag;
|
|
if (item.AtWorldPoint(oldX, oldY) &&
|
|
(item.Z == oldZ || item.Z + item.ItemData.Height > oldZ && oldZ + 15 > item.Z))
|
|
{
|
|
flag = 1;
|
|
}
|
|
else
|
|
{
|
|
flag = 0;
|
|
}
|
|
|
|
if (item.AtWorldPoint(x, y) &&
|
|
(item.Z == newZ || item.Z + item.ItemData.Height > newZ && newZ + 15 > item.Z))
|
|
{
|
|
flag += 2;
|
|
}
|
|
|
|
if (flag > 0)
|
|
{
|
|
queue.Enqueue((item, flag));
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var (entity, flag) = queue.Dequeue();
|
|
|
|
if (flag > 0 && !entity.OnMoveOff(this) || flag > 1 && !entity.OnMoveOver(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!InternalOnMove(d))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LastMoveTime = Core.TickCount;
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool Move(Direction d)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
var oldLocation = m_Location;
|
|
Point3D newLocation;
|
|
|
|
if ((m_Direction & Direction.Mask) == (d & Direction.Mask))
|
|
{
|
|
// We are actually moving (not just a direction change)
|
|
if (!CanMove(d, oldLocation, out newLocation))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
DisruptiveAction();
|
|
}
|
|
else
|
|
{
|
|
newLocation = oldLocation;
|
|
}
|
|
|
|
if (m_NetState != null)
|
|
{
|
|
m_NetState._nextMovementTime += ComputeMovementSpeed(d);
|
|
m_NetState.SendMovementAck(m_NetState.Sequence, this);
|
|
}
|
|
|
|
SetLocation(newLocation, false);
|
|
SetDirection(d);
|
|
|
|
if (m_Map != null)
|
|
{
|
|
using var moveQueue = PooledRefQueue<IEntity>.Create(2048);
|
|
using var moveClientQueue = PooledRefQueue<Mobile>.Create(2048);
|
|
|
|
foreach (var mob in m_Map.GetMobilesInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (mob == this)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (mob.NetState != null)
|
|
{
|
|
moveClientQueue.Enqueue(mob);
|
|
}
|
|
|
|
moveQueue.Enqueue(mob);
|
|
}
|
|
|
|
foreach (var item in m_Map.GetItemsInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (item.HandlesOnMovement)
|
|
{
|
|
moveQueue.Enqueue(item);
|
|
}
|
|
}
|
|
|
|
if (moveClientQueue.Count > 0)
|
|
{
|
|
const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength;
|
|
|
|
Span<byte> mobileMovingCache = stackalloc byte[cacheLength];
|
|
mobileMovingCache.Clear();
|
|
|
|
while (moveClientQueue.Count > 0)
|
|
{
|
|
var m = moveClientQueue.Dequeue();
|
|
var ns = m.NetState;
|
|
|
|
if (ns != null && Utility.InUpdateRange(m_Location, m.m_Location) && m.CanSee(this))
|
|
{
|
|
ns.SendMobileMovingUsingCache(mobileMovingCache, m, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
while (moveQueue.Count > 0)
|
|
{
|
|
moveQueue.Dequeue().OnMovement(this, oldLocation);
|
|
}
|
|
}
|
|
|
|
OnAfterMove(oldLocation);
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual void OnAfterMove(Point3D oldLocation)
|
|
{
|
|
}
|
|
|
|
public int ComputeMovementSpeed() => ComputeMovementSpeed(Direction, false);
|
|
|
|
public virtual int ComputeMovementSpeed(Direction dir, bool checkTurning = true)
|
|
{
|
|
if (Mounted)
|
|
{
|
|
return (dir & Direction.Running) != 0 ? CalcMoves.RunMountDelay : CalcMoves.WalkMountDelay;
|
|
}
|
|
|
|
return (dir & Direction.Running) != 0 ? CalcMoves.RunFootDelay : CalcMoves.WalkFootDelay;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a Mobile <paramref name="m" /> moves off this Mobile.
|
|
/// </summary>
|
|
/// <returns>True if the move is allowed, false if not.</returns>
|
|
public virtual bool OnMoveOff(Mobile m) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when a Mobile <paramref name="m" /> moves over this Mobile.
|
|
/// </summary>
|
|
/// <returns>True if the move is allowed, false if not.</returns>
|
|
public virtual bool OnMoveOver(Mobile m) => m_Map == null || Deleted || m.CheckShove(this);
|
|
|
|
public virtual bool CheckShove(Mobile shoved)
|
|
{
|
|
if ((m_Map.Rules & MapRules.FreeMovement) == 0)
|
|
{
|
|
if (!shoved.Alive || !Alive || shoved.IsDeadBondedPet || IsDeadBondedPet)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (shoved.m_Hidden && shoved.m_AccessLevel > AccessLevel.Player)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!Pushing)
|
|
{
|
|
Pushing = true;
|
|
|
|
int number;
|
|
|
|
if (AccessLevel > AccessLevel.Player)
|
|
{
|
|
number = shoved.m_Hidden ? 1019041 : 1019040;
|
|
}
|
|
else
|
|
{
|
|
if (Stam == StamMax)
|
|
{
|
|
number = shoved.m_Hidden ? 1019043 : 1019042;
|
|
Stam -= 10;
|
|
|
|
RevealingAction();
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SendLocalizedMessage(number);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile sees another Mobile, <paramref name="m" />, move.
|
|
/// </summary>
|
|
public virtual void OnMovement(Mobile m, Point3D oldLocation)
|
|
{
|
|
}
|
|
|
|
public virtual void CriminalAction(bool message)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Criminal = true;
|
|
|
|
Region.OnCriminalAction(this, message);
|
|
}
|
|
|
|
public virtual bool IsSnoop(Mobile from) => from != this;
|
|
|
|
/// <summary>
|
|
/// Overridable. Any call to <see cref="Resurrect" /> will silently fail if this method returns false.
|
|
/// <seealso cref="Resurrect" />
|
|
/// </summary>
|
|
public virtual bool CheckResurrect() => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked before the Mobile is <see cref="Resurrect">resurrected</see>.
|
|
/// <seealso cref="Resurrect" />
|
|
/// </summary>
|
|
public virtual void OnBeforeResurrect()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked after the Mobile is <see cref="Resurrect">resurrected</see>.
|
|
/// <seealso cref="Resurrect" />
|
|
/// </summary>
|
|
public virtual void OnAfterResurrect()
|
|
{
|
|
}
|
|
|
|
public virtual void Resurrect()
|
|
{
|
|
if (!Alive)
|
|
{
|
|
if (!Region.OnResurrect(this))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CheckResurrect())
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnBeforeResurrect();
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
Poison = null;
|
|
|
|
Warmode = false;
|
|
|
|
Hits = 10;
|
|
Stam = StamMax;
|
|
Mana = 0;
|
|
|
|
BodyMod = 0;
|
|
Body = Race.AliveBody(this);
|
|
|
|
ProcessDelta();
|
|
|
|
for (var i = Items.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i >= Items.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var item = Items[i];
|
|
|
|
if (item.ItemID == 0x204E)
|
|
{
|
|
item.Delete();
|
|
}
|
|
}
|
|
|
|
SendIncomingPacket();
|
|
|
|
OnAfterResurrect();
|
|
}
|
|
}
|
|
|
|
public void DropHolding()
|
|
{
|
|
var holding = m_Holding;
|
|
|
|
if (holding != null)
|
|
{
|
|
if (!holding.Deleted && holding.HeldBy == this && holding.Map == Map.Internal)
|
|
{
|
|
AddToBackpack(holding);
|
|
}
|
|
|
|
Holding = null;
|
|
holding.ClearBounce();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked before the Mobile is deleted.
|
|
/// </summary>
|
|
public virtual void OnDelete()
|
|
{
|
|
Spawner?.Remove(this);
|
|
Spawner = null;
|
|
}
|
|
|
|
public virtual bool CheckSpellCast(ISpell spell) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile casts a <paramref name="spell" />.
|
|
/// </summary>
|
|
/// <param name="spell"></param>
|
|
public virtual void OnSpellCast(ISpell spell)
|
|
{
|
|
var now = Core.TickCount;
|
|
if (m_NetState != null && now - m_NetState._nextMovementTime > 0)
|
|
{
|
|
m_NetState._nextMovementTime = now;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after <see cref="TotalWeight" /> changes.
|
|
/// </summary>
|
|
public virtual void OnWeightChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the <see cref="Skill.Base" /> or <see cref="Skill.BaseFixedPoint" /> property of
|
|
/// <paramref name="skill" /> changes.
|
|
/// </summary>
|
|
public virtual void OnSkillChange(SkillName skill, double oldBase)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Invoked after the mobile is deleted. When overridden, be sure to call the base method.
|
|
/// </summary>
|
|
public virtual void OnAfterDelete()
|
|
{
|
|
StopAggrExpire();
|
|
|
|
CheckAggrExpire();
|
|
|
|
PoisonTimer?.Stop();
|
|
m_HitsTimer?.Stop();
|
|
m_StamTimer?.Stop();
|
|
m_ManaTimer?.Stop();
|
|
_combatTimerToken.Cancel();
|
|
_expireCombatantTimerToken.Cancel();
|
|
_logoutTimerToken.Cancel();
|
|
_expireCriminalTimerToken.Cancel();
|
|
_paraTimerToken.Cancel();
|
|
_frozenTimerToken.Cancel();
|
|
_autoManifestTimerToken.Cancel();
|
|
}
|
|
|
|
public virtual bool AllowSkillUse(SkillName name) => true;
|
|
|
|
public virtual bool UseSkill(SkillName name) => Skills.UseSkill(this, name);
|
|
|
|
public virtual bool UseSkill(int skillID) => Skills.UseSkill(this, skillID);
|
|
|
|
public virtual DeathMoveResult GetParentMoveResultFor(Item item) => item.OnParentDeath(this);
|
|
|
|
public virtual DeathMoveResult GetInventoryMoveResultFor(Item item) => item.OnInventoryDeath(this);
|
|
|
|
public virtual void Kill()
|
|
{
|
|
if (!CanBeDamaged())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Alive || IsDeadBondedPet)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Region.OnBeforeDeath(this))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!OnBeforeDeath())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
m_NetState?.CancelAllTrades();
|
|
|
|
m_Spell?.OnCasterKilled();
|
|
// m_Spell.Disturb( DisturbType.Kill );
|
|
|
|
m_Target?.Cancel(this, TargetCancelType.Canceled);
|
|
|
|
DisruptiveAction();
|
|
|
|
Warmode = false;
|
|
|
|
DropHolding();
|
|
|
|
Hits = 0;
|
|
Stam = 0;
|
|
Mana = 0;
|
|
|
|
Poison = null;
|
|
Combatant = null;
|
|
|
|
if (Paralyzed)
|
|
{
|
|
Paralyzed = false;
|
|
}
|
|
|
|
if (Frozen)
|
|
{
|
|
Frozen = false;
|
|
}
|
|
|
|
var content = new List<Item>();
|
|
var equip = new List<Item>();
|
|
var moveToPack = new List<Item>();
|
|
|
|
var itemsCopy = new List<Item>(Items);
|
|
|
|
var pack = Backpack;
|
|
|
|
for (var i = 0; i < itemsCopy.Count; ++i)
|
|
{
|
|
var item = itemsCopy[i];
|
|
|
|
if (item == pack)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var res = GetParentMoveResultFor(item);
|
|
|
|
switch (res)
|
|
{
|
|
case DeathMoveResult.MoveToCorpse:
|
|
{
|
|
content.Add(item);
|
|
equip.Add(item);
|
|
break;
|
|
}
|
|
case DeathMoveResult.MoveToBackpack:
|
|
{
|
|
moveToPack.Add(item);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pack != null)
|
|
{
|
|
var packCopy = new List<Item>(pack.Items);
|
|
|
|
for (var i = 0; i < packCopy.Count; ++i)
|
|
{
|
|
var item = packCopy[i];
|
|
|
|
var res = GetInventoryMoveResultFor(item);
|
|
|
|
if (res == DeathMoveResult.MoveToCorpse)
|
|
{
|
|
content.Add(item);
|
|
}
|
|
else
|
|
{
|
|
moveToPack.Add(item);
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < moveToPack.Count; ++i)
|
|
{
|
|
var item = moveToPack[i];
|
|
|
|
if (RetainPackLocsOnDeath && item.Parent == pack)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
pack.DropItem(item);
|
|
}
|
|
}
|
|
|
|
var c = CreateCorpseHandler?.Invoke(this, content, equip);
|
|
|
|
if (m_Map != null)
|
|
{
|
|
var corpseSerial = c?.Serial ?? Serial.Zero;
|
|
|
|
Span<byte> deathAnimation = stackalloc byte[OutgoingMobilePackets.DeathAnimationPacketLength].InitializePacket();
|
|
Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
if (state != m_NetState)
|
|
{
|
|
OutgoingMobilePackets.CreateDeathAnimation(deathAnimation, Serial, corpseSerial);
|
|
state.Send(deathAnimation);
|
|
|
|
if (!state.Mobile.CanSee(this))
|
|
{
|
|
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
|
state.Send(removeEntity);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Region.OnDeath(this);
|
|
OnDeath(c);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked before the Mobile is <see cref="Kill">killed</see>.
|
|
/// <seealso cref="Kill" />
|
|
/// <seealso cref="OnDeath" />
|
|
/// </summary>
|
|
/// <returns>True to continue with death, false to override it.</returns>
|
|
public virtual bool OnBeforeDeath() => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked after the Mobile is <see cref="Kill">killed</see>. Primarily, this method is responsible for
|
|
/// deleting an NPC or turning a PC into a ghost.
|
|
/// <seealso cref="Kill" />
|
|
/// <seealso cref="OnBeforeDeath" />
|
|
/// </summary>
|
|
public virtual void OnDeath(Container c)
|
|
{
|
|
var sound = GetDeathSound();
|
|
|
|
if (sound >= 0)
|
|
{
|
|
Effects.PlaySound(this, sound);
|
|
}
|
|
|
|
if (!m_Player)
|
|
{
|
|
Delete();
|
|
return;
|
|
}
|
|
|
|
m_NetState.SendDeathStatus();
|
|
|
|
Warmode = false;
|
|
|
|
BodyMod = 0;
|
|
// Body = this.Female ? 0x193 : 0x192;
|
|
Body = Race.GhostBody(this);
|
|
|
|
var deathShroud = new Item(0x204E) { Movable = false, Layer = Layer.OuterTorso };
|
|
|
|
AddItem(deathShroud);
|
|
|
|
Items.Remove(deathShroud);
|
|
Items.Insert(0, deathShroud);
|
|
|
|
Poison = null;
|
|
Combatant = null;
|
|
|
|
Hits = 0;
|
|
Stam = 0;
|
|
Mana = 0;
|
|
|
|
ProcessDelta();
|
|
|
|
CheckStatTimers();
|
|
}
|
|
|
|
public virtual bool CheckTarget(Mobile from, Target targ, object targeted) => true;
|
|
|
|
public virtual void Use(Item item)
|
|
{
|
|
if (item?.Deleted != false || item.QuestItem || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DisruptiveAction();
|
|
|
|
if (m_Spell?.OnCasterUsingObject(item) == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var root = item.RootParent;
|
|
var okay = false;
|
|
|
|
if (!Utility.InUpdateRange(Location, item.GetWorldLocation()))
|
|
{
|
|
item.OnDoubleClickOutOfRange(this);
|
|
}
|
|
else if (!CanSee(item))
|
|
{
|
|
item.OnDoubleClickCantSee(this);
|
|
}
|
|
else if (!item.IsAccessibleTo(this))
|
|
{
|
|
var reg = Region.Find(item.GetWorldLocation(), item.Map);
|
|
|
|
if (reg?.SendInaccessibleMessage(item, this) != true)
|
|
{
|
|
item.OnDoubleClickNotAccessible(this);
|
|
}
|
|
}
|
|
else if (!CheckAlive(false))
|
|
{
|
|
item.OnDoubleClickDead(this);
|
|
}
|
|
else if (item.InSecureTrade)
|
|
{
|
|
item.OnDoubleClickSecureTrade(this);
|
|
}
|
|
else if (!AllowItemUse(item))
|
|
{
|
|
}
|
|
else if (!item.CheckItemUse(this, item))
|
|
{
|
|
}
|
|
else if (root is Mobile mobile && mobile.IsSnoop(this))
|
|
{
|
|
item.OnSnoop(this);
|
|
}
|
|
else if (Region.OnDoubleClick(this, item))
|
|
{
|
|
okay = true;
|
|
}
|
|
|
|
if (okay)
|
|
{
|
|
// TODO: Is this correct?
|
|
if (!item.Deleted)
|
|
{
|
|
item.OnItemUsed(this, item);
|
|
}
|
|
|
|
// TODO: Is this correct?
|
|
if (!item.Deleted)
|
|
{
|
|
item.OnDoubleClick(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void Use(Mobile m)
|
|
{
|
|
if (m?.Deleted != false || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DisruptiveAction();
|
|
|
|
if (m_Spell?.OnCasterUsingObject(m) == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Utility.InUpdateRange(Location, m.Location))
|
|
{
|
|
m.OnDoubleClickOutOfRange(this);
|
|
}
|
|
else if (!CanSee(m))
|
|
{
|
|
m.OnDoubleClickCantSee(this);
|
|
}
|
|
else if (!CheckAlive(false))
|
|
{
|
|
m.OnDoubleClickDead(this);
|
|
}
|
|
else if (Region.OnDoubleClick(this, m) && !m.Deleted)
|
|
{
|
|
m.OnDoubleClick(this);
|
|
}
|
|
}
|
|
|
|
public virtual void Lift(Item item, int amount, out bool rejected, out LRReason reject)
|
|
{
|
|
rejected = true;
|
|
reject = LRReason.Inspecific;
|
|
|
|
if (item == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var from = this;
|
|
var state = m_NetState;
|
|
|
|
if (from.AccessLevel >= AccessLevel.GameMaster || Core.TickCount - from.NextActionTime >= 0)
|
|
{
|
|
if (from.CheckAlive())
|
|
{
|
|
from.DisruptiveAction();
|
|
|
|
if (from.Holding != null)
|
|
{
|
|
reject = LRReason.AreHolding;
|
|
}
|
|
else if (from.AccessLevel < AccessLevel.GameMaster && !from.InRange(item.GetWorldLocation(), 2))
|
|
{
|
|
reject = LRReason.OutOfRange;
|
|
}
|
|
else if (!from.CanSee(item) || !from.InLOS(item))
|
|
{
|
|
reject = LRReason.OutOfSight;
|
|
}
|
|
else if (!item.VerifyMove(from))
|
|
{
|
|
reject = LRReason.CannotLift;
|
|
}
|
|
else if (!item.IsAccessibleTo(from))
|
|
{
|
|
reject = LRReason.CannotLift;
|
|
}
|
|
else if (item.Nontransferable && amount != item.Amount)
|
|
{
|
|
if (item.QuestItem)
|
|
{
|
|
from.SendLocalizedMessage(1074868); // Stacks of quest items cannot be unstacked.
|
|
}
|
|
|
|
reject = LRReason.CannotLift;
|
|
}
|
|
else if (!item.CheckLift(from, item, ref reject))
|
|
{
|
|
}
|
|
else
|
|
{
|
|
var root = item.RootParent;
|
|
|
|
if (root is Mobile mobile && !mobile.CheckNonlocalLift(from, item))
|
|
{
|
|
reject = LRReason.TryToSteal;
|
|
}
|
|
else if (!from.OnDragLift(item) || !item.OnDragLift(from))
|
|
{
|
|
reject = LRReason.Inspecific;
|
|
}
|
|
else if (!from.CheckAlive())
|
|
{
|
|
reject = LRReason.Inspecific;
|
|
}
|
|
else
|
|
{
|
|
item.SetLastMoved();
|
|
|
|
if (item.Spawner != null)
|
|
{
|
|
item.Spawner.Remove(item);
|
|
item.Spawner = null;
|
|
}
|
|
|
|
var oldAmount = item.Amount;
|
|
|
|
if (oldAmount <= 0)
|
|
{
|
|
logger.Error(
|
|
"Item {Type} ({Serial}) has amount of {OldAmount}, but must be at least 1",
|
|
item.GetType(),
|
|
item.Serial,
|
|
oldAmount
|
|
);
|
|
}
|
|
else
|
|
{
|
|
amount = Math.Clamp(amount, 1, oldAmount);
|
|
|
|
if (amount < oldAmount)
|
|
{
|
|
LiftItemDupe(item, amount);
|
|
}
|
|
}
|
|
|
|
var map = from.Map;
|
|
|
|
if (DragEffects && map != null && root is null or Item)
|
|
{
|
|
var rootItem = root as Item;
|
|
|
|
Span<byte> buffer = stackalloc byte[OutgoingPlayerPackets.DragEffectPacketLength]
|
|
.InitializePacket();
|
|
|
|
foreach (var ns in map.GetClientsInRange(from.Location))
|
|
{
|
|
if (ns.Mobile != from && ns.Mobile.CanSee(from) && ns.Mobile.InLOS(from) &&
|
|
ns.Mobile.CanSee(root))
|
|
{
|
|
OutgoingPlayerPackets.CreateDragEffect(
|
|
buffer,
|
|
rootItem?.Serial ?? Serial.Zero,
|
|
rootItem?.Location ?? item.Location,
|
|
from.Serial,
|
|
from.Location,
|
|
item.ItemID,
|
|
item.Hue,
|
|
amount
|
|
);
|
|
|
|
ns.Send(buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
var fixLoc = item.Location;
|
|
var fixMap = item.Map;
|
|
var shouldFix = item.Parent == null;
|
|
|
|
item.RecordBounce();
|
|
item.OnItemLifted(from, item);
|
|
item.Internalize();
|
|
|
|
from.Holding = item;
|
|
|
|
from.SendSound(item.GetLiftSound(from));
|
|
|
|
from.NextActionTime = Core.TickCount + ActionDelay;
|
|
|
|
if (fixMap != null && shouldFix)
|
|
{
|
|
fixMap.FixColumn(fixLoc.m_X, fixLoc.m_Y);
|
|
}
|
|
|
|
reject = LRReason.Inspecific;
|
|
rejected = false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
reject = LRReason.Inspecific;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SendActionMessage();
|
|
reject = LRReason.Inspecific;
|
|
}
|
|
|
|
if (rejected && state != null)
|
|
{
|
|
state.SendLiftReject(reject);
|
|
|
|
if (item.Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (item.Parent is Item)
|
|
{
|
|
state.SendContainerContentUpdate(item);
|
|
}
|
|
else if (item.Parent is Mobile)
|
|
{
|
|
state.SendEquipUpdate(item);
|
|
}
|
|
else
|
|
{
|
|
item.SendInfoTo(state);
|
|
}
|
|
|
|
if (item.Parent != null)
|
|
{
|
|
item.SendOPLPacketTo(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static T LiftItemDupe<T>(T oldItem, int amount) where T : Item
|
|
{
|
|
T item;
|
|
try
|
|
{
|
|
item = oldItem.GetType().CreateInstance<T>();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Warning(
|
|
e,
|
|
"[{Serial} {Name}]: Item must have a zero parameter constructor to be separated from a stack.",
|
|
oldItem.Serial,
|
|
oldItem.GetType().Name
|
|
);
|
|
|
|
return null;
|
|
}
|
|
|
|
var oldAmount = oldItem.Amount;
|
|
oldItem.Amount = amount;
|
|
|
|
item.Visible = oldItem.Visible;
|
|
item.Movable = oldItem.Movable;
|
|
item.LootType = oldItem.LootType;
|
|
item.Direction = oldItem.Direction;
|
|
item.Hue = oldItem.Hue;
|
|
item.ItemID = oldItem.ItemID;
|
|
item.Location = oldItem.Location;
|
|
item.Layer = oldItem.Layer;
|
|
item.Name = oldItem.Name;
|
|
item.Weight = oldItem.Weight;
|
|
|
|
item.Amount = oldAmount - amount;
|
|
item.Map = oldItem.Map;
|
|
|
|
oldItem.OnAfterDuped(item);
|
|
|
|
if (oldItem.Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.AddItem(item);
|
|
}
|
|
else if (oldItem.Parent is Item parentItem)
|
|
{
|
|
parentItem.AddItem(item);
|
|
}
|
|
|
|
item.Delta(ItemDelta.Update);
|
|
|
|
return item;
|
|
}
|
|
|
|
public virtual void SendDropEffect(Item item)
|
|
{
|
|
if (!DragEffects || item.Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var map = m_Map;
|
|
var root = item.RootParent;
|
|
var rootItem = root as Item;
|
|
|
|
if (map == null || root != null && rootItem == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> buffer = stackalloc byte[OutgoingPlayerPackets.DragEffectPacketLength].InitializePacket();
|
|
|
|
foreach (var ns in map.GetClientsInRange(m_Location))
|
|
{
|
|
if (ns.StygianAbyss || ns.Mobile == this ||
|
|
!ns.Mobile.CanSee(this) || !ns.Mobile.InLOS(this) || !ns.Mobile.CanSee(root))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
OutgoingPlayerPackets.CreateDragEffect(
|
|
buffer,
|
|
Serial,
|
|
Location,
|
|
rootItem?.Serial ?? Serial.Zero,
|
|
rootItem?.Location ?? item.Location,
|
|
item.ItemID,
|
|
item.Hue,
|
|
item.Amount
|
|
);
|
|
|
|
ns.Send(buffer);
|
|
}
|
|
}
|
|
|
|
public virtual bool Drop(Item to, Point3D loc)
|
|
{
|
|
var from = this;
|
|
var item = from.Holding;
|
|
|
|
var valid = item != null && item.HeldBy == from && item.Map == Map.Internal;
|
|
|
|
from.Holding = null;
|
|
|
|
if (!valid)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var bounced = true;
|
|
|
|
item.SetLastMoved();
|
|
|
|
if (to == null || !item.DropToItem(from, to, loc))
|
|
{
|
|
item.Bounce(from);
|
|
}
|
|
else
|
|
{
|
|
bounced = false;
|
|
}
|
|
|
|
item.ClearBounce();
|
|
|
|
if (!bounced)
|
|
{
|
|
SendDropEffect(item);
|
|
}
|
|
|
|
return !bounced;
|
|
}
|
|
|
|
public virtual bool Drop(Point3D loc)
|
|
{
|
|
var from = this;
|
|
var item = from.Holding;
|
|
|
|
var valid = item != null && item.HeldBy == from && item.Map == Map.Internal;
|
|
|
|
from.Holding = null;
|
|
|
|
if (!valid)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var bounced = true;
|
|
|
|
item.SetLastMoved();
|
|
|
|
if (!item.DropToWorld(from, loc))
|
|
{
|
|
item.Bounce(from);
|
|
}
|
|
else
|
|
{
|
|
bounced = false;
|
|
}
|
|
|
|
item.ClearBounce();
|
|
|
|
if (!bounced)
|
|
{
|
|
SendDropEffect(item);
|
|
}
|
|
|
|
return !bounced;
|
|
}
|
|
|
|
public virtual bool Drop(Mobile to, Point3D loc)
|
|
{
|
|
var from = this;
|
|
var item = from.Holding;
|
|
|
|
var valid = item != null && item.HeldBy == from && item.Map == Map.Internal;
|
|
|
|
from.Holding = null;
|
|
|
|
if (!valid)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var bounced = true;
|
|
|
|
item.SetLastMoved();
|
|
|
|
if (to == null || !item.DropToMobile(from, to, loc))
|
|
{
|
|
item.Bounce(from);
|
|
}
|
|
else
|
|
{
|
|
bounced = false;
|
|
}
|
|
|
|
item.ClearBounce();
|
|
|
|
if (!bounced)
|
|
{
|
|
SendDropEffect(item);
|
|
}
|
|
|
|
return !bounced;
|
|
}
|
|
|
|
public virtual bool MutateSpeech(List<Mobile> hears, ref string text, ref object context)
|
|
{
|
|
if (Alive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
using var sb = new ValueStringBuilder(stackalloc char[Math.Min(text.Length, 256)]);
|
|
for (var i = 0; i < text.Length; ++i)
|
|
{
|
|
sb.Append(text[i] != ' ' ? (GhostChars ?? DefaultGhostChars).RandomElement() : ' ');
|
|
}
|
|
|
|
text = sb.ToString();
|
|
context = m_GhostMutateContext;
|
|
return true;
|
|
}
|
|
|
|
private void AutoManifest()
|
|
{
|
|
if (!Alive)
|
|
{
|
|
Warmode = false;
|
|
}
|
|
}
|
|
|
|
public virtual void Manifest(TimeSpan delay)
|
|
{
|
|
Warmode = true;
|
|
_autoManifestTimerToken.Cancel();
|
|
Timer.StartTimer(delay, AutoManifest, out _autoManifestTimerToken);
|
|
}
|
|
|
|
public virtual bool CheckSpeechManifest()
|
|
{
|
|
if (Alive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var delay = AutoManifestTimeout;
|
|
|
|
if (delay > TimeSpan.Zero && (!Warmode || _autoManifestTimerToken.Running))
|
|
{
|
|
Manifest(delay);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public virtual bool CheckHearsMutatedSpeech(Mobile m, object context) =>
|
|
context != m_GhostMutateContext || m.Alive && !m.CanHearGhosts;
|
|
|
|
private void AddSpeechItemsFrom(List<IEntity> list, Container cont)
|
|
{
|
|
for (var i = 0; i < cont.Items.Count; ++i)
|
|
{
|
|
var item = cont.Items[i];
|
|
|
|
if (item.HandlesOnSpeech)
|
|
{
|
|
list.Add(item);
|
|
}
|
|
|
|
if (item is Container container)
|
|
{
|
|
AddSpeechItemsFrom(list, container);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void DoSpeech(string text, int[] keywords, MessageType type, int hue)
|
|
{
|
|
if (Deleted || CommandSystem.Handle(this, text, type))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var range = 15;
|
|
|
|
switch (type)
|
|
{
|
|
case MessageType.Regular:
|
|
{
|
|
SpeechHue = hue;
|
|
break;
|
|
}
|
|
case MessageType.Emote:
|
|
{
|
|
EmoteHue = hue;
|
|
break;
|
|
}
|
|
case MessageType.Whisper:
|
|
{
|
|
WhisperHue = hue;
|
|
range = 1;
|
|
break;
|
|
}
|
|
case MessageType.Yell:
|
|
{
|
|
YellHue = hue;
|
|
range = 18;
|
|
break;
|
|
}
|
|
case MessageType.System:
|
|
{
|
|
break;
|
|
}
|
|
case MessageType.Label:
|
|
{
|
|
break;
|
|
}
|
|
case MessageType.Focus:
|
|
{
|
|
break;
|
|
}
|
|
case MessageType.Spell:
|
|
{
|
|
break;
|
|
}
|
|
case MessageType.Guild:
|
|
{
|
|
break;
|
|
}
|
|
case MessageType.Alliance:
|
|
{
|
|
break;
|
|
}
|
|
case MessageType.Command:
|
|
{
|
|
break;
|
|
}
|
|
case MessageType.Encoded:
|
|
{
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
type = MessageType.Regular;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var regArgs = new SpeechEventArgs(this, text, type, hue, keywords);
|
|
|
|
EventSink.InvokeSpeech(regArgs);
|
|
Region.OnSpeech(regArgs);
|
|
OnSaid(regArgs);
|
|
|
|
if (regArgs.Blocked)
|
|
{
|
|
return;
|
|
}
|
|
|
|
text = regArgs.Speech;
|
|
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var hears = m_Hears;
|
|
var onSpeech = m_OnSpeech;
|
|
|
|
if (m_Map != null)
|
|
{
|
|
foreach (var heard in m_Map.GetMobilesInRange(m_Location, range))
|
|
{
|
|
if (!heard.CanSee(this) || !NoSpeechLOS && heard.Player && !heard.InLOS(this))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (heard.m_NetState != null)
|
|
{
|
|
hears.Add(heard);
|
|
}
|
|
|
|
if (heard.HandlesOnSpeech(this))
|
|
{
|
|
onSpeech.Add(heard);
|
|
}
|
|
|
|
for (var i = 0; i < heard.Items.Count; ++i)
|
|
{
|
|
var item = heard.Items[i];
|
|
|
|
if (item.HandlesOnSpeech)
|
|
{
|
|
onSpeech.Add(item);
|
|
}
|
|
|
|
if (item is Container container)
|
|
{
|
|
AddSpeechItemsFrom(onSpeech, container);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var item in m_Map.GetItemsInRange(m_Location, range))
|
|
{
|
|
if (item.HandlesOnSpeech)
|
|
{
|
|
onSpeech.Add(item);
|
|
}
|
|
|
|
if (item is Container container)
|
|
{
|
|
AddSpeechItemsFrom(onSpeech, container);
|
|
}
|
|
}
|
|
|
|
object mutateContext = null;
|
|
var mutatedText = text;
|
|
SpeechEventArgs mutatedArgs = null;
|
|
|
|
if (MutateSpeech(hears, ref mutatedText, ref mutateContext))
|
|
{
|
|
mutatedArgs = new SpeechEventArgs(this, mutatedText, type, hue, Array.Empty<int>());
|
|
}
|
|
|
|
CheckSpeechManifest();
|
|
|
|
ProcessDelta();
|
|
|
|
Span<byte> regBuffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text)].InitializePacket();
|
|
Span<byte> mutBuffer =
|
|
stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(mutatedText)].InitializePacket();
|
|
|
|
// TODO: Should this be sorted like onSpeech is below?
|
|
for (var i = 0; i < hears.Count; ++i)
|
|
{
|
|
var heard = hears[i];
|
|
|
|
if (mutatedArgs == null || !CheckHearsMutatedSpeech(heard, mutateContext))
|
|
{
|
|
var length = OutgoingMessagePackets.CreateMessage(
|
|
regBuffer,
|
|
Serial,
|
|
Body,
|
|
type,
|
|
hue,
|
|
3,
|
|
false,
|
|
m_Language,
|
|
Name,
|
|
text
|
|
);
|
|
|
|
if (length != regBuffer.Length)
|
|
{
|
|
regBuffer = regBuffer[..length]; // Adjust to the actual size
|
|
}
|
|
|
|
heard.OnSpeech(regArgs);
|
|
heard.NetState?.Send(regBuffer);
|
|
}
|
|
else
|
|
{
|
|
var length = OutgoingMessagePackets.CreateMessage(
|
|
mutBuffer,
|
|
Serial,
|
|
Body,
|
|
type,
|
|
hue,
|
|
3,
|
|
false,
|
|
m_Language,
|
|
Name,
|
|
mutatedText
|
|
);
|
|
|
|
if (length != mutBuffer.Length)
|
|
{
|
|
mutBuffer = mutBuffer[..length]; // Adjust to the actual size
|
|
}
|
|
|
|
heard.OnSpeech(mutatedArgs);
|
|
heard.NetState?.Send(mutBuffer);
|
|
}
|
|
}
|
|
|
|
if (onSpeech.Count > 1)
|
|
{
|
|
onSpeech.Sort(LocationComparer.GetInstance(this));
|
|
}
|
|
|
|
for (var i = 0; i < onSpeech.Count; ++i)
|
|
{
|
|
var obj = onSpeech[i];
|
|
|
|
if (obj is Mobile heard)
|
|
{
|
|
if (mutatedArgs == null || !CheckHearsMutatedSpeech(heard, mutateContext))
|
|
{
|
|
heard.OnSpeech(regArgs);
|
|
}
|
|
else
|
|
{
|
|
heard.OnSpeech(mutatedArgs);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
((Item)obj).OnSpeech(regArgs);
|
|
}
|
|
}
|
|
|
|
if (m_Hears.Count > 0)
|
|
{
|
|
m_Hears.Clear();
|
|
}
|
|
|
|
if (m_OnSpeech.Count > 0)
|
|
{
|
|
m_OnSpeech.Clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
public Mobile FindMostRecentDamager(bool allowSelf) => FindMostRecentDamageEntry(allowSelf)?.Damager;
|
|
|
|
public DamageEntry FindMostRecentDamageEntry(bool allowSelf)
|
|
{
|
|
for (var i = DamageEntries.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i >= DamageEntries.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var de = DamageEntries[i];
|
|
|
|
if (de.HasExpired)
|
|
{
|
|
DamageEntries.RemoveAt(i);
|
|
}
|
|
else if (allowSelf || de.Damager != this)
|
|
{
|
|
return de;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public Mobile FindLeastRecentDamager(bool allowSelf) => FindLeastRecentDamageEntry(allowSelf)?.Damager;
|
|
|
|
public DamageEntry FindLeastRecentDamageEntry(bool allowSelf)
|
|
{
|
|
for (var i = 0; i < DamageEntries.Count; ++i)
|
|
{
|
|
if (i < 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var de = DamageEntries[i];
|
|
|
|
if (de.HasExpired)
|
|
{
|
|
DamageEntries.RemoveAt(i);
|
|
--i;
|
|
}
|
|
else if (allowSelf || de.Damager != this)
|
|
{
|
|
return de;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public Mobile FindMostTotalDamager(bool allowSelf) => FindMostTotalDamageEntry(allowSelf)?.Damager;
|
|
|
|
public DamageEntry FindMostTotalDamageEntry(bool allowSelf)
|
|
{
|
|
DamageEntry mostTotal = null;
|
|
|
|
for (var i = DamageEntries.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i >= DamageEntries.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var de = DamageEntries[i];
|
|
|
|
if (de.HasExpired)
|
|
{
|
|
DamageEntries.RemoveAt(i);
|
|
}
|
|
else if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven > mostTotal.DamageGiven))
|
|
{
|
|
mostTotal = de;
|
|
}
|
|
}
|
|
|
|
return mostTotal;
|
|
}
|
|
|
|
public Mobile FindLeastTotalDamager(bool allowSelf) => FindLeastTotalDamageEntry(allowSelf)?.Damager;
|
|
|
|
public DamageEntry FindLeastTotalDamageEntry(bool allowSelf)
|
|
{
|
|
DamageEntry mostTotal = null;
|
|
|
|
for (var i = DamageEntries.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i >= DamageEntries.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var de = DamageEntries[i];
|
|
|
|
if (de.HasExpired)
|
|
{
|
|
DamageEntries.RemoveAt(i);
|
|
}
|
|
else if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven < mostTotal.DamageGiven))
|
|
{
|
|
mostTotal = de;
|
|
}
|
|
}
|
|
|
|
return mostTotal;
|
|
}
|
|
|
|
public DamageEntry FindDamageEntryFor(Mobile m)
|
|
{
|
|
for (var i = DamageEntries.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i >= DamageEntries.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var de = DamageEntries[i];
|
|
|
|
if (de.HasExpired)
|
|
{
|
|
DamageEntries.RemoveAt(i);
|
|
}
|
|
else if (de.Damager == m)
|
|
{
|
|
return de;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public virtual Mobile GetDamageMaster(Mobile damagee) => null;
|
|
|
|
public virtual DamageEntry RegisterDamage(int amount, Mobile from)
|
|
{
|
|
var de = FindDamageEntryFor(from) ?? new DamageEntry(from);
|
|
|
|
de.DamageGiven += amount;
|
|
de.LastDamage = Core.Now;
|
|
|
|
DamageEntries.Remove(de);
|
|
DamageEntries.Add(de);
|
|
|
|
var master = from.GetDamageMaster(this);
|
|
|
|
if (master != null)
|
|
{
|
|
var list = de.Responsible;
|
|
|
|
if (list == null)
|
|
{
|
|
de.Responsible = list = new List<DamageEntry>();
|
|
}
|
|
|
|
DamageEntry resp = null;
|
|
foreach (var check in list)
|
|
{
|
|
if (check.Damager == master)
|
|
{
|
|
resp = check;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (resp == null)
|
|
{
|
|
list.Add(resp = new DamageEntry(master));
|
|
}
|
|
|
|
resp.DamageGiven += amount;
|
|
resp.LastDamage = Core.Now;
|
|
}
|
|
|
|
return de;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile is <see cref="Damage">damaged</see>. It is called before
|
|
/// <see cref="Hits">hit points</see> are lowered or the Mobile is <see cref="Kill">killed</see>.
|
|
/// <seealso cref="Damage" />
|
|
/// <seealso cref="Hits" />
|
|
/// <seealso cref="Kill" />
|
|
/// </summary>
|
|
public virtual void OnDamage(int amount, Mobile from, bool willKill)
|
|
{
|
|
}
|
|
|
|
public virtual bool CanBeDamaged() => !m_Blessed;
|
|
|
|
public virtual void Damage(int amount, Mobile from = null, bool informMount = true, bool ignoreEvilOmen = false)
|
|
{
|
|
if (amount <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanBeDamaged() || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Region.OnDamage(this, ref amount))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var oldHits = Hits;
|
|
var newHits = oldHits - amount;
|
|
|
|
m_Spell?.OnCasterHurt();
|
|
|
|
// if (m_Spell != null && m_Spell.State == SpellState.Casting)
|
|
// m_Spell.Disturb( DisturbType.Hurt, false, true );
|
|
|
|
if (from != null)
|
|
{
|
|
RegisterDamage(amount, from);
|
|
}
|
|
|
|
DisruptiveAction();
|
|
|
|
Paralyzed = false;
|
|
|
|
switch (VisibleDamageType)
|
|
{
|
|
case VisibleDamageType.Related:
|
|
{
|
|
SendVisibleDamageRelated(from, amount);
|
|
break;
|
|
}
|
|
case VisibleDamageType.Everyone:
|
|
{
|
|
SendVisibleDamageEveryone(amount);
|
|
break;
|
|
}
|
|
case VisibleDamageType.Selective:
|
|
{
|
|
SendVisibleDamageSelective(from, amount);
|
|
break;
|
|
}
|
|
}
|
|
|
|
OnDamage(amount, from, newHits < 0);
|
|
|
|
if (informMount)
|
|
{
|
|
Mount?.OnRiderDamaged(amount, from, newHits < 0);
|
|
}
|
|
|
|
if (newHits < 0)
|
|
{
|
|
LastKiller = from;
|
|
|
|
Hits = 0;
|
|
|
|
if (oldHits >= 0)
|
|
{
|
|
Kill();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Hits = newHits;
|
|
}
|
|
}
|
|
|
|
public void SendVisibleDamageRelated(Mobile from, int amount)
|
|
{
|
|
var ourState = m_NetState ?? GetDamageMaster(from)?.m_NetState;
|
|
var theirState = from?.m_NetState ?? from?.GetDamageMaster(this)?.m_NetState;
|
|
|
|
if (amount > 0)
|
|
{
|
|
ourState.SendDamage(Serial, amount);
|
|
|
|
if (theirState != null && theirState != ourState)
|
|
{
|
|
theirState.SendDamage(Serial, amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SendVisibleDamageEveryone(int amount)
|
|
{
|
|
if (amount < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var map = m_Map;
|
|
|
|
if (map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var ns in map.GetClientsInRange(m_Location))
|
|
{
|
|
if (ns.Mobile.CanSee(this))
|
|
{
|
|
ns.SendDamage(Serial, amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SendVisibleDamageSelective(Mobile from, int amount)
|
|
{
|
|
var ourState = m_NetState;
|
|
var theirState = from?.m_NetState;
|
|
|
|
var damager = from;
|
|
var damaged = this;
|
|
|
|
if (ourState == null)
|
|
{
|
|
var master = GetDamageMaster(from);
|
|
|
|
if (master != null)
|
|
{
|
|
damaged = master;
|
|
ourState = master.m_NetState;
|
|
}
|
|
}
|
|
|
|
if (!damaged.ShowVisibleDamage)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (theirState == null && from != null)
|
|
{
|
|
var master = from.GetDamageMaster(this);
|
|
|
|
if (master != null)
|
|
{
|
|
damager = master;
|
|
theirState = master.m_NetState;
|
|
}
|
|
}
|
|
|
|
if (amount > 0)
|
|
{
|
|
if (damaged.CanSeeVisibleDamage)
|
|
{
|
|
ourState.SendDamage(Serial, amount);
|
|
}
|
|
|
|
if (theirState != null && theirState != ourState && damager.CanSeeVisibleDamage)
|
|
{
|
|
theirState.SendDamage(Serial, amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Heal(int amount) => Heal(amount, this);
|
|
|
|
public void Heal(int amount, Mobile from, bool message = true)
|
|
{
|
|
if (!Alive || IsDeadBondedPet)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Region.OnHeal(this, ref amount))
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnHeal(ref amount, from);
|
|
|
|
if (Hits + amount > HitsMax)
|
|
{
|
|
amount = HitsMax - Hits;
|
|
}
|
|
|
|
Hits += amount;
|
|
|
|
if (message && amount > 0)
|
|
{
|
|
m_NetState.SendMessageLocalizedAffix(
|
|
Serial.MinusOne,
|
|
-1,
|
|
MessageType.Label,
|
|
0x3B2,
|
|
3,
|
|
1008158,
|
|
"",
|
|
AffixType.Append | AffixType.System,
|
|
amount.ToString()
|
|
);
|
|
}
|
|
}
|
|
|
|
public virtual void OnHeal(ref int amount, Mobile from)
|
|
{
|
|
}
|
|
|
|
public virtual void Deserialize(IGenericReader reader)
|
|
{
|
|
var version = reader.ReadInt();
|
|
|
|
switch (version)
|
|
{
|
|
case 36: // Moved virtues to VirtueSystem
|
|
case 35: // Moved short term murders to PlayerMurderSystem
|
|
case 34: // Moved Stabled to PlayerMobile
|
|
case 33: // Removed created
|
|
case 32: // Removed StuckMenu
|
|
case 31:
|
|
{
|
|
LastStrGain = reader.ReadDeltaTime();
|
|
LastIntGain = reader.ReadDeltaTime();
|
|
LastDexGain = reader.ReadDeltaTime();
|
|
|
|
goto case 30;
|
|
}
|
|
case 30:
|
|
{
|
|
var hairflag = reader.ReadByte();
|
|
|
|
if ((hairflag & 0x01) != 0)
|
|
{
|
|
_hair = new VirtualHairInfo();
|
|
_hair.Deserialize(reader);
|
|
}
|
|
|
|
if ((hairflag & 0x02) != 0)
|
|
{
|
|
_facialHair = new VirtualHairInfo();
|
|
_facialHair.Deserialize(reader);
|
|
}
|
|
|
|
goto case 29;
|
|
}
|
|
case 29:
|
|
{
|
|
m_Race = reader.ReadRace();
|
|
goto case 28;
|
|
}
|
|
case 28:
|
|
{
|
|
if (version <= 30)
|
|
{
|
|
LastStatGain = reader.ReadDeltaTime();
|
|
}
|
|
|
|
goto case 27;
|
|
}
|
|
case 27:
|
|
{
|
|
m_TithingPoints = reader.ReadInt();
|
|
|
|
goto case 26;
|
|
}
|
|
case 26:
|
|
case 25:
|
|
case 24:
|
|
{
|
|
Corpse = reader.ReadEntity<Container>();
|
|
|
|
goto case 23;
|
|
}
|
|
case 23:
|
|
{
|
|
if (version < 33)
|
|
{
|
|
Created = reader.ReadDateTime();
|
|
}
|
|
|
|
goto case 22;
|
|
}
|
|
case 22: // Just removed followers
|
|
case 21:
|
|
{
|
|
if (version < 34)
|
|
{
|
|
// Migrated to PlayerMobile
|
|
AddToStabledMigration(this, reader.ReadEntitySet<Mobile>(true));
|
|
}
|
|
|
|
goto case 20;
|
|
}
|
|
case 20:
|
|
{
|
|
CantWalk = reader.ReadBool();
|
|
|
|
goto case 19;
|
|
}
|
|
case 19: // Just removed variables
|
|
case 18:
|
|
{
|
|
if (version < 36)
|
|
{
|
|
reader.ReadByte(); // VirtueInfo version
|
|
|
|
int mask = reader.ReadByte();
|
|
|
|
if (mask != 0)
|
|
{
|
|
var virtueValues = new int[8];
|
|
|
|
for (var i = 0; i < 8; ++i)
|
|
{
|
|
if ((mask & (1 << i)) != 0)
|
|
{
|
|
virtueValues[i] = reader.ReadInt();
|
|
}
|
|
}
|
|
|
|
AddToVirtueMigration(this, virtueValues);
|
|
}
|
|
}
|
|
|
|
goto case 17;
|
|
}
|
|
case 17:
|
|
{
|
|
Thirst = reader.ReadInt();
|
|
BAC = reader.ReadInt();
|
|
|
|
goto case 16;
|
|
}
|
|
case 16:
|
|
{
|
|
if (version < 35)
|
|
{
|
|
// Migrated to PlayerMurderSystem
|
|
AddToMurderMigration(this, reader.ReadInt());
|
|
}
|
|
|
|
if (version <= 24)
|
|
{
|
|
reader.ReadDateTime();
|
|
reader.ReadDateTime();
|
|
}
|
|
|
|
goto case 15;
|
|
}
|
|
case 15:
|
|
{
|
|
if (version < 22)
|
|
{
|
|
reader.ReadInt(); // followers
|
|
}
|
|
|
|
m_FollowersMax = reader.ReadInt();
|
|
|
|
goto case 14;
|
|
}
|
|
case 14:
|
|
{
|
|
MagicDamageAbsorb = reader.ReadInt();
|
|
|
|
goto case 13;
|
|
}
|
|
case 13:
|
|
{
|
|
GuildFealty = reader.ReadEntity<Mobile>();
|
|
|
|
goto case 12;
|
|
}
|
|
case 12:
|
|
{
|
|
m_Guild = reader.ReadEntity<BaseGuild>();
|
|
|
|
goto case 11;
|
|
}
|
|
case 11:
|
|
{
|
|
m_DisplayGuildTitle = reader.ReadBool();
|
|
|
|
goto case 10;
|
|
}
|
|
case 10:
|
|
{
|
|
CanSwim = reader.ReadBool();
|
|
|
|
goto case 9;
|
|
}
|
|
case 9:
|
|
{
|
|
Squelched = reader.ReadBool();
|
|
|
|
goto case 8;
|
|
}
|
|
case 8:
|
|
{
|
|
m_Holding = reader.ReadEntity<Item>();
|
|
|
|
goto case 7;
|
|
}
|
|
case 7:
|
|
{
|
|
m_VirtualArmor = reader.ReadInt();
|
|
|
|
goto case 6;
|
|
}
|
|
case 6:
|
|
{
|
|
BaseSoundID = reader.ReadInt();
|
|
|
|
goto case 5;
|
|
}
|
|
case 5:
|
|
{
|
|
DisarmReady = reader.ReadBool();
|
|
StunReady = reader.ReadBool();
|
|
|
|
goto case 4;
|
|
}
|
|
case 4:
|
|
{
|
|
if (version <= 25)
|
|
{
|
|
reader.ReadPoison();
|
|
}
|
|
|
|
goto case 3;
|
|
}
|
|
case 3:
|
|
{
|
|
m_StatCap = reader.ReadInt();
|
|
|
|
goto case 2;
|
|
}
|
|
case 2:
|
|
{
|
|
NameHue = reader.ReadInt();
|
|
|
|
goto case 1;
|
|
}
|
|
case 1:
|
|
{
|
|
m_Hunger = reader.ReadInt();
|
|
|
|
goto case 0;
|
|
}
|
|
case 0:
|
|
{
|
|
if (version < 11)
|
|
{
|
|
m_DisplayGuildTitle = true;
|
|
}
|
|
|
|
if (version < 3)
|
|
{
|
|
m_StatCap = 225;
|
|
}
|
|
|
|
if (version < 15)
|
|
{
|
|
m_Followers = 0;
|
|
m_FollowersMax = 5;
|
|
}
|
|
|
|
m_Location = reader.ReadPoint3D();
|
|
m_Body = new Body(reader.ReadInt());
|
|
m_Name = reader.ReadString();
|
|
m_GuildTitle = reader.ReadString();
|
|
m_Criminal = reader.ReadBool();
|
|
m_Kills = reader.ReadInt();
|
|
SpeechHue = reader.ReadInt();
|
|
EmoteHue = reader.ReadInt();
|
|
WhisperHue = reader.ReadInt();
|
|
YellHue = reader.ReadInt();
|
|
m_Language = reader.ReadString();
|
|
m_Female = reader.ReadBool();
|
|
m_Warmode = reader.ReadBool();
|
|
m_Hidden = reader.ReadBool();
|
|
m_Direction = (Direction)reader.ReadByte();
|
|
m_Hue = reader.ReadInt();
|
|
m_Str = reader.ReadInt();
|
|
m_Dex = reader.ReadInt();
|
|
m_Int = reader.ReadInt();
|
|
m_Hits = reader.ReadInt();
|
|
m_Stam = reader.ReadInt();
|
|
m_Mana = reader.ReadInt();
|
|
m_Map = reader.ReadMap();
|
|
m_Blessed = reader.ReadBool();
|
|
m_Fame = reader.ReadInt();
|
|
m_Karma = reader.ReadInt();
|
|
m_AccessLevel = (AccessLevel)reader.ReadByte();
|
|
|
|
Skills = new Skills(this, reader);
|
|
|
|
Items = reader.ReadEntityList<Item>();
|
|
|
|
m_Player = reader.ReadBool();
|
|
m_Title = reader.ReadString();
|
|
Profile = reader.ReadString();
|
|
ProfileLocked = reader.ReadBool();
|
|
|
|
if (version <= 18)
|
|
{
|
|
reader.ReadInt();
|
|
reader.ReadInt();
|
|
reader.ReadInt();
|
|
}
|
|
|
|
AutoPageNotify = reader.ReadBool();
|
|
|
|
LogoutLocation = reader.ReadPoint3D();
|
|
LogoutMap = reader.ReadMap();
|
|
|
|
m_StrLock = (StatLockType)reader.ReadByte();
|
|
m_DexLock = (StatLockType)reader.ReadByte();
|
|
m_IntLock = (StatLockType)reader.ReadByte();
|
|
|
|
_statMods = new List<StatMod>();
|
|
_skillMods = new List<SkillMod>();
|
|
|
|
if (version < 32)
|
|
{
|
|
if (reader.ReadBool())
|
|
{
|
|
var count = reader.ReadInt();
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
reader.ReadDateTime();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_Player && m_Map != Map.Internal)
|
|
{
|
|
LogoutLocation = m_Location;
|
|
LogoutMap = m_Map;
|
|
|
|
m_Map = Map.Internal;
|
|
}
|
|
|
|
m_Map?.OnEnter(this);
|
|
|
|
if (m_Criminal)
|
|
{
|
|
Timer.StartTimer(ExpireCriminalDelay, ExpireCriminal, out _expireCriminalTimerToken);
|
|
}
|
|
|
|
if (ShouldCheckStatTimers)
|
|
{
|
|
CheckStatTimers();
|
|
}
|
|
|
|
UpdateRegion();
|
|
|
|
UpdateResistances();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!m_Player)
|
|
{
|
|
Utility.Intern(ref m_Name);
|
|
}
|
|
|
|
Utility.Intern(ref m_Title);
|
|
Utility.Intern(ref m_Language);
|
|
}
|
|
|
|
public void ConvertHair()
|
|
{
|
|
Item hair;
|
|
|
|
if ((hair = FindItemOnLayer(Layer.Hair)) != null)
|
|
{
|
|
HairItemID = hair.ItemID;
|
|
HairHue = hair.Hue;
|
|
hair.Delete();
|
|
}
|
|
|
|
if ((hair = FindItemOnLayer(Layer.FacialHair)) != null)
|
|
{
|
|
FacialHairItemID = hair.ItemID;
|
|
FacialHairHue = hair.Hue;
|
|
hair.Delete();
|
|
}
|
|
}
|
|
|
|
public virtual void CheckStatTimers()
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Hits < HitsMax)
|
|
{
|
|
if (CanRegenHits)
|
|
{
|
|
m_HitsTimer ??= new HitsTimer(this);
|
|
m_HitsTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_HitsTimer?.Stop();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Hits = HitsMax;
|
|
}
|
|
|
|
if (Stam < StamMax)
|
|
{
|
|
if (CanRegenStam)
|
|
{
|
|
m_StamTimer ??= new StamTimer(this);
|
|
m_StamTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_StamTimer?.Stop();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Stam = StamMax;
|
|
}
|
|
|
|
if (Mana < ManaMax)
|
|
{
|
|
if (CanRegenMana)
|
|
{
|
|
m_ManaTimer ??= new ManaTimer(this);
|
|
m_ManaTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_ManaTimer?.Stop();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Mana = ManaMax;
|
|
}
|
|
}
|
|
|
|
public static string GetAccessLevelName(AccessLevel level) => m_AccessLevelNames[(int)level];
|
|
|
|
public virtual bool CanPaperdollBeOpenedBy(Mobile from) => Body.IsHuman || Body.IsGhost || IsBodyMod;
|
|
|
|
public virtual void GetChildContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list, Item item)
|
|
{
|
|
}
|
|
|
|
public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
|
|
{
|
|
}
|
|
|
|
public void Internalize()
|
|
{
|
|
Map = Map.Internal;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="item" /> is <see cref="AddItem">added</see> from the Mobile,
|
|
/// such
|
|
/// as when it is equipped.
|
|
/// <seealso cref="Items" />
|
|
/// <seealso cref="OnItemRemoved" />
|
|
/// </summary>
|
|
public virtual void OnItemAdded(Item item)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="item" /> is <see cref="RemoveItem">removed</see> from the
|
|
/// Mobile.
|
|
/// <seealso cref="Items" />
|
|
/// <seealso cref="OnItemAdded" />
|
|
/// </summary>
|
|
public virtual void OnItemRemoved(Item item)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="item" /> is becomes a child of the Mobile; it's worn or
|
|
/// contained
|
|
/// at some level of the Mobile's <see cref="Mobile.Backpack">backpack</see> or <see cref="Mobile.BankBox">bank box</see>
|
|
/// <seealso cref="OnSubItemRemoved" />
|
|
/// <seealso cref="OnItemAdded" />
|
|
/// </summary>
|
|
public virtual void OnSubItemAdded(Item item)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="item" /> is removed from the Mobile, its
|
|
/// <see cref="Mobile.Backpack">backpack</see>, or its <see cref="Mobile.BankBox">bank box</see>.
|
|
/// <seealso cref="OnSubItemAdded" />
|
|
/// <seealso cref="OnItemRemoved" />
|
|
/// </summary>
|
|
public virtual void OnSubItemRemoved(Item item)
|
|
{
|
|
}
|
|
|
|
public virtual void OnItemBounceCleared(Item item)
|
|
{
|
|
}
|
|
|
|
public virtual void OnSubItemBounceCleared(Item item)
|
|
{
|
|
}
|
|
|
|
public void AddItem(Item item)
|
|
{
|
|
if (item?.Deleted != false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (item.Parent == this)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (item.Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.RemoveItem(item);
|
|
}
|
|
else if (item.Parent is Item parentItem)
|
|
{
|
|
parentItem.RemoveItem(item);
|
|
}
|
|
else
|
|
{
|
|
item.SendRemovePacket();
|
|
}
|
|
|
|
item.Parent = this;
|
|
item.Map = m_Map;
|
|
|
|
Items.Add(item);
|
|
|
|
if (!item.IsVirtualItem)
|
|
{
|
|
UpdateTotal(item, TotalType.Gold, item.TotalGold);
|
|
UpdateTotal(item, TotalType.Items, item.TotalItems + 1);
|
|
UpdateTotal(item, TotalType.Weight, item.TotalWeight + item.PileWeight);
|
|
}
|
|
|
|
item.Delta(ItemDelta.Update);
|
|
|
|
item.OnAdded(this);
|
|
OnItemAdded(item);
|
|
|
|
if (item.PhysicalResistance != 0 || item.FireResistance != 0 || item.ColdResistance != 0 ||
|
|
item.PoisonResistance != 0 || item.EnergyResistance != 0)
|
|
{
|
|
UpdateResistances();
|
|
}
|
|
}
|
|
|
|
public void RemoveItem(Item item)
|
|
{
|
|
if (item == null || Items == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Items.Remove(item))
|
|
{
|
|
item.SendRemovePacket();
|
|
|
|
if (!item.IsVirtualItem)
|
|
{
|
|
UpdateTotal(item, TotalType.Gold, -item.TotalGold);
|
|
UpdateTotal(item, TotalType.Items, -(item.TotalItems + 1));
|
|
UpdateTotal(item, TotalType.Weight, -(item.TotalWeight + item.PileWeight));
|
|
}
|
|
|
|
item.Parent = null;
|
|
|
|
item.OnRemoved(this);
|
|
OnItemRemoved(item);
|
|
|
|
if (item.PhysicalResistance != 0 || item.FireResistance != 0 || item.ColdResistance != 0 ||
|
|
item.PoisonResistance != 0 || item.EnergyResistance != 0)
|
|
{
|
|
UpdateResistances();
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void Animate(int action, int frameCount, int repeatCount, bool forward, bool repeat, int delay)
|
|
{
|
|
var map = m_Map;
|
|
|
|
if (map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ProcessDelta();
|
|
|
|
Span<byte> buffer = stackalloc byte[OutgoingMobilePackets.MobileAnimationPacketLength].InitializePacket();
|
|
|
|
foreach (var state in map.GetClientsInRange(m_Location))
|
|
{
|
|
if (!state.Mobile.CanSee(this))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
state.Mobile.ProcessDelta();
|
|
|
|
if (Body.IsGargoyle)
|
|
{
|
|
frameCount = 10;
|
|
|
|
if (Flying)
|
|
{
|
|
action = action switch
|
|
{
|
|
>= 9 and <= 11 => 71,
|
|
>= 12 and <= 14 => 72,
|
|
20 => 77,
|
|
31 => 71,
|
|
34 => 78,
|
|
>= 200 and <= 259 => 75,
|
|
>= 260 and <= 270 => 75,
|
|
_ => action
|
|
};
|
|
}
|
|
else
|
|
{
|
|
action = action switch
|
|
{
|
|
>= 200 and <= 259 => 17,
|
|
>= 260 and <= 270 => 16,
|
|
_ => action
|
|
};
|
|
}
|
|
}
|
|
|
|
OutgoingMobilePackets.CreateMobileAnimation(
|
|
buffer,
|
|
Serial,
|
|
action,
|
|
frameCount,
|
|
repeatCount,
|
|
forward,
|
|
repeat,
|
|
delay
|
|
);
|
|
|
|
state.Send(buffer);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void SendSound(int soundID)
|
|
{
|
|
m_NetState.SendSoundEffect(soundID, this);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void SendSound(int soundID, IPoint3D p)
|
|
{
|
|
m_NetState.SendSoundEffect(soundID, p);
|
|
}
|
|
|
|
/**
|
|
* Plays a sound to netstates that can see this mobile
|
|
*/
|
|
public void PlaySound(int soundID)
|
|
{
|
|
if (soundID == -1 || m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> buffer = stackalloc byte[OutgoingEffectPackets.SoundPacketLength].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
if (state.Mobile.CanSee(this))
|
|
{
|
|
OutgoingEffectPackets.CreateSoundEffect(buffer, soundID, this);
|
|
state.Send(buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void SendOPLPacketTo(NetState ns)
|
|
{
|
|
if (ObjectPropertyList.Enabled)
|
|
{
|
|
ns.SendOPLInfo(this);
|
|
}
|
|
}
|
|
|
|
public virtual void OnAccessLevelChanged(AccessLevel oldLevel)
|
|
{
|
|
}
|
|
|
|
public virtual void OnFameChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
public virtual void OnKarmaChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
// Mobile did something which should unhide him
|
|
public virtual void RevealingAction()
|
|
{
|
|
if (m_Hidden && m_AccessLevel == AccessLevel.Player)
|
|
{
|
|
Hidden = false;
|
|
}
|
|
|
|
DisruptiveAction(); // Anything that unhides you will also distrupt meditation
|
|
}
|
|
|
|
public void SendRemovePacket(bool everyone = true)
|
|
{
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
if (state != m_NetState && (everyone || !state.Mobile.CanSee(this)))
|
|
{
|
|
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
|
state.Send(removeEntity);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ClearScreen()
|
|
{
|
|
if (m_Map == null || m_NetState == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var m in m_Map.GetMobilesInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (m != this && Utility.InUpdateRange(m_Location, m.m_Location))
|
|
{
|
|
m_NetState.SendRemoveEntity(m.Serial);
|
|
}
|
|
}
|
|
|
|
foreach (var item in m_Map.GetItemsInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (InRange(item.Location, item.GetUpdateRange(this)))
|
|
{
|
|
m_NetState.SendRemoveEntity(item.Serial);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked before the Mobile says something.
|
|
/// <seealso cref="DoSpeech" />
|
|
/// </summary>
|
|
public virtual void OnSaid(SpeechEventArgs e)
|
|
{
|
|
if (Squelched)
|
|
{
|
|
if (Core.ML)
|
|
{
|
|
SendLocalizedMessage(500168); // You can not say anything, you have been muted.
|
|
}
|
|
else
|
|
{
|
|
SendMessage("You can not say anything, you have been squelched."); // Cliloc ITSELF changed during ML.
|
|
}
|
|
|
|
e.Blocked = true;
|
|
}
|
|
|
|
if (!e.Blocked)
|
|
{
|
|
RevealingAction();
|
|
}
|
|
}
|
|
|
|
public virtual bool HandlesOnSpeech(Mobile from) => false;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile hears speech. This event will only be invoked if
|
|
/// <see cref="HandlesOnSpeech" /> returns true.
|
|
/// <seealso cref="DoSpeech" />
|
|
/// </summary>
|
|
public virtual void OnSpeech(SpeechEventArgs e)
|
|
{
|
|
}
|
|
|
|
public void SendEverything()
|
|
{
|
|
var ns = m_NetState;
|
|
|
|
if (m_Map == null || ns == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var m in m_Map.GetMobilesInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (CanSee(m) && Utility.InUpdateRange(m_Location, m.m_Location))
|
|
{
|
|
ns.SendMobileIncoming(this, m);
|
|
|
|
if (ns.StygianAbyss)
|
|
{
|
|
ns.SendMobileHealthbar(m, Healthbar.Yellow);
|
|
ns.SendMobileHealthbar(m, Healthbar.Poison);
|
|
}
|
|
|
|
if (m.IsDeadBondedPet)
|
|
{
|
|
ns.SendBondedStatus(m.Serial, true);
|
|
}
|
|
|
|
m.SendOPLPacketTo(ns);
|
|
}
|
|
}
|
|
|
|
foreach (var item in m_Map.GetItemsInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (CanSee(item) && InRange(item.Location, item.GetUpdateRange(this)))
|
|
{
|
|
item.SendInfoTo(ns);
|
|
}
|
|
}
|
|
|
|
var range = new Rectangle2D(
|
|
m_Location.X - Core.GlobalMaxUpdateRange,
|
|
m_Location.Y - Core.GlobalMaxUpdateRange,
|
|
Core.GlobalMaxUpdateRange * 2 + 1,
|
|
Core.GlobalMaxUpdateRange * 2 + 1
|
|
);
|
|
|
|
foreach (var multi in m_Map.GetMultisInBounds(range))
|
|
{
|
|
multi.SendInfoTo(ns);
|
|
}
|
|
}
|
|
|
|
public void UpdateRegion()
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var newRegion = Region.Find(m_Location, m_Map);
|
|
|
|
if (newRegion != m_Region)
|
|
{
|
|
Region.OnRegionChange(this, m_Region, newRegion);
|
|
|
|
var oldRegion = m_Region;
|
|
m_Region = newRegion;
|
|
OnRegionChange(oldRegion, newRegion);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <see cref="Map" /> changes.
|
|
/// </summary>
|
|
protected virtual void OnMapChange(Map oldMap)
|
|
{
|
|
}
|
|
|
|
public void SetDirection(Direction dir)
|
|
{
|
|
m_Direction = dir;
|
|
}
|
|
|
|
public virtual int GetSeason() => m_Map?.Season ?? 1;
|
|
|
|
public virtual int GetPacketFlags(bool stygianAbyss)
|
|
{
|
|
var flags = 0x0;
|
|
|
|
if (m_Paralyzed || m_Frozen || _disableCastParalyze && m_Spell?.BlocksMovement == true)
|
|
{
|
|
flags |= 0x01;
|
|
}
|
|
|
|
if (m_Female)
|
|
{
|
|
flags |= 0x02;
|
|
}
|
|
|
|
if (stygianAbyss)
|
|
{
|
|
if (m_Flying)
|
|
{
|
|
flags |= 0x04;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (m_Poison != null)
|
|
{
|
|
flags |= 0x04;
|
|
}
|
|
}
|
|
|
|
if (m_Blessed || m_YellowHealthbar)
|
|
{
|
|
flags |= 0x08;
|
|
}
|
|
|
|
if (IgnoreMobiles)
|
|
{
|
|
flags |= 0x10;
|
|
}
|
|
|
|
if (m_Warmode)
|
|
{
|
|
flags |= 0x40;
|
|
}
|
|
|
|
if (m_Hidden)
|
|
{
|
|
flags |= 0x80;
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
public virtual void OnGenderChanged(bool oldFemale)
|
|
{
|
|
}
|
|
|
|
public virtual void ToggleFlying()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after the Warmode property has changed.
|
|
/// </summary>
|
|
public virtual void OnWarmodeChanged()
|
|
{
|
|
}
|
|
|
|
public virtual void OnHiddenChanged()
|
|
{
|
|
AllowedStealthSteps = 0;
|
|
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
var m = state.Mobile;
|
|
if (!m.CanSee(this))
|
|
{
|
|
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
|
state.Send(removeEntity);
|
|
}
|
|
else
|
|
{
|
|
state.SendMobileIncoming(m, this);
|
|
|
|
if (IsDeadBondedPet)
|
|
{
|
|
state.SendBondedStatus(Serial, true);
|
|
}
|
|
|
|
SendOPLPacketTo(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void OnConnected()
|
|
{
|
|
}
|
|
|
|
public virtual void OnBeforeDisconnected()
|
|
{
|
|
}
|
|
|
|
public virtual void OnDisconnected()
|
|
{
|
|
}
|
|
|
|
public virtual void OnNetStateChanged()
|
|
{
|
|
}
|
|
|
|
public virtual bool CanSee(object o)
|
|
{
|
|
return o switch
|
|
{
|
|
Item item => CanSee(item),
|
|
Mobile mobile => CanSee(mobile),
|
|
_ => true
|
|
};
|
|
}
|
|
|
|
public virtual bool CanSee(Item item)
|
|
{
|
|
if (m_Map == Map.Internal)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (item.Map == Map.Internal)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (item.Parent != null)
|
|
{
|
|
if (item.Parent is Item parent)
|
|
{
|
|
if (!(CanSee(parent) && parent.IsChildVisibleTo(this, item)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (item.Parent is Mobile mobile)
|
|
{
|
|
if (!CanSee(mobile))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item is BankBox box && m_AccessLevel <= AccessLevel.Counselor && (box.Owner != this || !box.Opened))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (item is SecureTradeContainer container)
|
|
{
|
|
var trade = container.Trade;
|
|
|
|
if (trade != null && trade.From.Mobile != this && trade.To.Mobile != this)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return !item.Deleted && item.Map == m_Map && (item.Visible || item.CanSeeStaffOnly(this));
|
|
}
|
|
|
|
public virtual bool CanSee(Mobile m)
|
|
{
|
|
if (Deleted || m.Deleted || m_Map == Map.Internal || m.m_Map == Map.Internal)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return this == m || m.m_Map == m_Map &&
|
|
(!m.Hidden || m_AccessLevel != AccessLevel.Player &&
|
|
(m_AccessLevel >= m.AccessLevel || m_AccessLevel >= AccessLevel.Administrator)) &&
|
|
(m.Alive || Core.SE && Skills.SpiritSpeak.Value >= 100.0 || !Alive ||
|
|
m_AccessLevel > AccessLevel.Player || m.Warmode);
|
|
}
|
|
|
|
public virtual bool CanBeRenamedBy(Mobile from) =>
|
|
from.AccessLevel >= AccessLevel.GameMaster && from.m_AccessLevel > m_AccessLevel;
|
|
|
|
public virtual void OnGuildTitleChange(string oldTitle)
|
|
{
|
|
}
|
|
|
|
public virtual void OnAfterNameChange(string oldName, string newName)
|
|
{
|
|
}
|
|
|
|
public virtual void OnGuildChange(BaseGuild oldGuild)
|
|
{
|
|
}
|
|
|
|
public virtual int SafeBody(int body)
|
|
{
|
|
var delta = -1;
|
|
|
|
for (var i = 0; delta < 0 && i < m_InvalidBodies.Length; ++i)
|
|
{
|
|
delta = m_InvalidBodies[i] - body;
|
|
}
|
|
|
|
return delta != 0 ? body : 0;
|
|
}
|
|
|
|
private ObjectPropertyList InitializePropertyList(ObjectPropertyList list)
|
|
{
|
|
GetProperties(list);
|
|
list.Terminate();
|
|
return list;
|
|
}
|
|
|
|
public void ClearProperties()
|
|
{
|
|
m_PropertyList = null;
|
|
}
|
|
|
|
#nullable enable
|
|
public void InvalidateProperties()
|
|
{
|
|
if (!ObjectPropertyList.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_Map != null && m_Map != Map.Internal && !World.Loading)
|
|
{
|
|
int? oldHash;
|
|
int newHash;
|
|
if (m_PropertyList != null)
|
|
{
|
|
oldHash = m_PropertyList.Hash;
|
|
m_PropertyList.Reset();
|
|
InitializePropertyList(m_PropertyList);
|
|
newHash = m_PropertyList.Hash;
|
|
}
|
|
else
|
|
{
|
|
oldHash = null;
|
|
newHash = PropertyList.Hash;
|
|
}
|
|
|
|
if (oldHash != newHash)
|
|
{
|
|
Delta(MobileDelta.Properties);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClearProperties();
|
|
}
|
|
}
|
|
#nullable restore
|
|
|
|
public virtual void SetLocation(Point3D newLocation, bool isTeleport)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var oldLocation = m_Location;
|
|
|
|
if (oldLocation == newLocation)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_Location = newLocation;
|
|
m_Map?.OnMove(oldLocation, this);
|
|
UpdateRegion();
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
if (m_NetState != null)
|
|
{
|
|
m_NetState.ValidateAllTrades();
|
|
|
|
if (isTeleport && (!m_NetState.HighSeas || !NoMoveHS))
|
|
{
|
|
m_NetState.Sequence = 0;
|
|
m_NetState.SendMobileUpdate(this);
|
|
}
|
|
}
|
|
|
|
var map = m_Map;
|
|
|
|
if (map != null)
|
|
{
|
|
// First, send a remove message to everyone who can no longer see us. (inOldRange && !inNewRange)
|
|
Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
|
|
|
|
foreach (var ns in map.GetClientsInRange(oldLocation))
|
|
{
|
|
if (ns != m_NetState && !Utility.InUpdateRange(newLocation, ns.Mobile.Location))
|
|
{
|
|
OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
|
|
ns.Send(removeEntity);
|
|
}
|
|
}
|
|
|
|
var ourState = m_NetState;
|
|
|
|
// Check to see if we are attached to a client
|
|
if (ourState != null)
|
|
{
|
|
// We are attached to a client, so it's a bit more complex. We need to send new items and people to ourself, and ourself to other clients
|
|
|
|
foreach (var m in map.GetMobilesInRange(newLocation, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (m == this)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!Utility.InUpdateRange(newLocation, m.m_Location))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var inOldRange = Utility.InUpdateRange(oldLocation, m.m_Location);
|
|
var ns = m.m_NetState;
|
|
|
|
if (ns != null &&
|
|
(isTeleport && (!ns.HighSeas || !NoMoveHS) || !inOldRange) && m.CanSee(this))
|
|
{
|
|
ns.SendMobileIncoming(m, this);
|
|
|
|
if (ns.StygianAbyss)
|
|
{
|
|
ns.SendMobileHealthbar(this, Healthbar.Poison);
|
|
ns.SendMobileHealthbar(this, Healthbar.Yellow);
|
|
}
|
|
|
|
if (IsDeadBondedPet)
|
|
{
|
|
ns.SendBondedStatus(Serial, true);
|
|
}
|
|
|
|
SendOPLPacketTo(ns);
|
|
}
|
|
|
|
if (inOldRange || !CanSee(m))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ourState.SendMobileIncoming(this, m);
|
|
|
|
if (ourState.StygianAbyss)
|
|
{
|
|
ourState.SendMobileHealthbar(m, Healthbar.Poison);
|
|
ourState.SendMobileHealthbar(m, Healthbar.Yellow);
|
|
}
|
|
|
|
if (m.IsDeadBondedPet)
|
|
{
|
|
ourState.SendBondedStatus(m.Serial, true);
|
|
}
|
|
|
|
m.SendOPLPacketTo(ourState);
|
|
}
|
|
|
|
foreach (var item in map.GetItemsInRange(newLocation, Core.GlobalMaxUpdateRange))
|
|
{
|
|
var range = item.GetUpdateRange(this);
|
|
var loc = item.Location;
|
|
|
|
if (!Utility.InRange(oldLocation, loc, range) && Utility.InRange(newLocation, loc, range) &&
|
|
CanSee(item))
|
|
{
|
|
item.SendInfoTo(ourState);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We're not attached to a client, so simply send an Incoming
|
|
foreach (var ns in map.GetClientsInRange(newLocation))
|
|
{
|
|
var m = ns.Mobile;
|
|
if ((isTeleport && (!ns.HighSeas || !NoMoveHS) || !Utility.InUpdateRange(oldLocation, m.Location)) &&
|
|
m.CanSee(this))
|
|
{
|
|
ns.SendMobileIncoming(m, this);
|
|
|
|
if (ns.StygianAbyss)
|
|
{
|
|
ns.SendMobileHealthbar(this, Healthbar.Poison);
|
|
ns.SendMobileHealthbar(this, Healthbar.Yellow);
|
|
}
|
|
|
|
if (IsDeadBondedPet)
|
|
{
|
|
ns.SendBondedStatus(Serial, true);
|
|
}
|
|
|
|
SendOPLPacketTo(ns);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
OnLocationChange(oldLocation);
|
|
|
|
Region.OnLocationChanged(this, oldLocation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <see cref="Location" /> changes.
|
|
/// </summary>
|
|
protected virtual void OnLocationChange(Point3D oldLocation)
|
|
{
|
|
}
|
|
|
|
public bool HasFreeHand() => FindItemOnLayer(Layer.TwoHanded) == null;
|
|
|
|
public virtual IWeapon GetDefaultWeapon() => DefaultWeapon;
|
|
|
|
public BankBox FindBankNoCreate()
|
|
{
|
|
if (m_BankBox?.Deleted != false || m_BankBox.Parent != this)
|
|
{
|
|
m_BankBox = FindItemOnLayer<BankBox>(Layer.Bank);
|
|
}
|
|
|
|
return m_BankBox;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public T FindItemOnLayer<T>(Layer layer) where T : Item => FindItemOnLayer(layer) as T;
|
|
|
|
public Item FindItemOnLayer(Layer layer)
|
|
{
|
|
var eq = Items;
|
|
var count = eq.Count;
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
var item = eq[i];
|
|
|
|
// TODO: We only allow 1 item per layer. It's an implicit contract.
|
|
if (!item.Deleted && item.Layer == layer)
|
|
{
|
|
return item;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public void SendIncomingPacket()
|
|
{
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
var m = state.Mobile;
|
|
if (m.CanSee(this))
|
|
{
|
|
state.SendMobileIncoming(m, this);
|
|
|
|
if (state.StygianAbyss)
|
|
{
|
|
state.SendMobileHealthbar(this, Healthbar.Poison);
|
|
state.SendMobileHealthbar(this, Healthbar.Yellow);
|
|
}
|
|
|
|
if (IsDeadBondedPet)
|
|
{
|
|
state.SendBondedStatus(Serial, true);
|
|
}
|
|
|
|
SendOPLPacketTo(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool PlaceInBackpack(Item item) =>
|
|
!item.Deleted && Backpack?.TryDropItem(this, item, false) == true;
|
|
|
|
public bool AddToBackpack(Item item)
|
|
{
|
|
if (item.Deleted)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!PlaceInBackpack(item))
|
|
{
|
|
var loc = m_Location;
|
|
var map = m_Map;
|
|
|
|
if ((map == null || map == Map.Internal) && LogoutMap != null)
|
|
{
|
|
loc = LogoutLocation;
|
|
map = LogoutMap;
|
|
}
|
|
|
|
item.MoveToWorld(loc, map);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool CheckLift(Mobile from, Item item, ref LRReason reject) => true;
|
|
|
|
public virtual bool CheckNonlocalLift(Mobile from, Item item) =>
|
|
from == this || from.AccessLevel > AccessLevel && from.AccessLevel >= AccessLevel.GameMaster;
|
|
|
|
public virtual bool CheckTrade(
|
|
Mobile to, Item item, SecureTradeContainer cont, bool message, bool checkItems, int plusItems, int plusWeight
|
|
) => true;
|
|
|
|
public virtual bool OpenTrade(Mobile from, Item offer = null)
|
|
{
|
|
if (!from.Player || !Player || !from.Alive || !Alive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var ourState = m_NetState;
|
|
var theirState = from.m_NetState;
|
|
|
|
if (ourState == null || theirState == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var cont = theirState.FindTradeContainer(this);
|
|
|
|
if (!from.CheckTrade(this, offer, cont, true, true, 0, 0))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
cont ??= theirState.AddTrade(ourState);
|
|
|
|
if (offer != null)
|
|
{
|
|
cont.DropItem(offer);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when a Mobile (<paramref name="from" />) drops an
|
|
/// <see cref="Item">
|
|
/// <paramref name="dropped" />
|
|
/// </see>
|
|
/// onto the Mobile.
|
|
/// </summary>
|
|
public virtual bool OnDragDrop(Mobile from, Item dropped)
|
|
{
|
|
if (from == this)
|
|
{
|
|
var pack = Backpack;
|
|
return pack != null && dropped.DropToItem(from, pack, new Point3D(-1, -1, 0));
|
|
}
|
|
|
|
return from.InRange(Location, 2) && OpenTrade(from, dropped);
|
|
}
|
|
|
|
public virtual bool CheckEquip(Item item)
|
|
{
|
|
for (var i = 0; i < Items.Count; ++i)
|
|
{
|
|
if (Items[i].CheckConflictingLayer(this, item, item.Layer) ||
|
|
item.CheckConflictingLayer(this, Items[i], Items[i].Layer))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to wear <paramref name="item" />.
|
|
/// </summary>
|
|
/// <returns>True if the request is accepted, false if otherwise.</returns>
|
|
public virtual bool OnEquip(Item item)
|
|
{
|
|
// For some reason OSI allows equipping quest items, but they are unmarked in the process
|
|
if (item.QuestItem)
|
|
{
|
|
item.QuestItem = false;
|
|
|
|
// An item must be in your backpack (and not in a container within) to be toggled as a quest item.
|
|
SendLocalizedMessage(1074769);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to lift <paramref name="item" />.
|
|
/// </summary>
|
|
/// <returns>True if the lift is allowed, false if otherwise.</returns>
|
|
/// <example>
|
|
/// The following example demonstrates usage. It will disallow any attempts to pick up a pick axe if the Mobile does not
|
|
/// have
|
|
/// enough strength.
|
|
/// <code>
|
|
/// public override bool OnDragLift( Item item )
|
|
/// {
|
|
/// if (item is Pickaxe && this.Str < 60)
|
|
/// {
|
|
/// SendMessage( "That is too heavy for you to lift." );
|
|
/// return false;
|
|
/// }
|
|
///
|
|
/// return base.OnDragLift( item );
|
|
/// }</code>
|
|
/// </example>
|
|
public virtual bool OnDragLift(Item item) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> into a
|
|
/// <see cref="Container">
|
|
/// <paramref name="container" />
|
|
/// </see>
|
|
/// .
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemInto(Item item, Container container, Point3D loc) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> directly onto another
|
|
/// <see cref="Item" />, <paramref name="target" />. This is the case of stacking items.
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemOnto(Item item, Item target) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> into another
|
|
/// <see cref="Item" />, <paramref name="target" />. The target item is most likely a <see cref="Container" />.
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemToItem(Item item, Item target, Point3D loc) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to give <paramref name="item" /> to a Mobile (
|
|
/// <paramref name="target" />).
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemToMobile(Item item, Mobile target) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> to the world at a
|
|
/// <see cref="Point3D">
|
|
/// <paramref name="location" />
|
|
/// </see>
|
|
/// .
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemToWorld(Item item, Point3D location) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event when <paramref name="from" /> successfully uses <paramref name="item" /> while it's on this
|
|
/// Mobile.
|
|
/// <seealso cref="Item.OnItemUsed" />
|
|
/// </summary>
|
|
public virtual void OnItemUsed(Mobile from, Item item)
|
|
{
|
|
}
|
|
|
|
public virtual bool CheckNonlocalDrop(Mobile from, Item item, Item target) =>
|
|
from == this || from.AccessLevel > AccessLevel && from.AccessLevel >= AccessLevel.GameMaster;
|
|
|
|
public virtual bool CheckItemUse(Mobile from, Item item) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="from" /> successfully lifts <paramref name="item" /> from this
|
|
/// Mobile.
|
|
/// <seealso cref="Item.OnItemLifted" />
|
|
/// </summary>
|
|
public virtual void OnItemLifted(Mobile from, Item item)
|
|
{
|
|
}
|
|
|
|
public virtual bool AllowItemUse(Item item) => true;
|
|
|
|
public virtual bool AllowEquipFrom(Mobile mob) =>
|
|
mob == this || mob.AccessLevel >= AccessLevel.GameMaster && mob.AccessLevel > AccessLevel;
|
|
|
|
public virtual bool EquipItem(Item item)
|
|
{
|
|
if (item?.Deleted != false || !item.CanEquip(this))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (CheckEquip(item) && OnEquip(item) && item.OnEquip(this))
|
|
{
|
|
if (m_Spell?.OnCasterEquipping(item) == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// if (m_Spell != null && m_Spell.State == SpellState.Casting)
|
|
// m_Spell.Disturb( DisturbType.EquipRequest );
|
|
|
|
AddItem(item);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void DefaultMobileInit()
|
|
{
|
|
m_StatCap = 225;
|
|
m_FollowersMax = 5;
|
|
Skills = new Skills(this);
|
|
Items = new List<Item>();
|
|
_statMods = new List<StatMod>();
|
|
_skillMods = new List<SkillMod>();
|
|
Map = Map.Internal;
|
|
AutoPageNotify = true;
|
|
Aggressors = new List<AggressorInfo>();
|
|
Aggressed = new List<AggressorInfo>();
|
|
DamageEntries = new List<DamageEntry>();
|
|
|
|
NextSkillTime = Core.TickCount;
|
|
}
|
|
|
|
public virtual void Delta(MobileDelta flag)
|
|
{
|
|
#if THREADGUARD
|
|
if (Thread.CurrentThread != Core.Thread)
|
|
{
|
|
Utility.PushColor(ConsoleColor.Red);
|
|
Console.WriteLine("Attempting to queue a delta change from an invalid thread!");
|
|
Console.WriteLine(new StackTrace());
|
|
Utility.PopColor();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (m_Map == null || m_Map == Map.Internal || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_DeltaFlags |= flag;
|
|
|
|
if (!m_InDeltaQueue)
|
|
{
|
|
m_InDeltaQueue = true;
|
|
m_DeltaQueue.Enqueue(this);
|
|
}
|
|
}
|
|
|
|
public static void ProcessDeltaQueue()
|
|
{
|
|
var limit = m_DeltaQueue.Count;
|
|
|
|
while (m_DeltaQueue.Count > 0 && --limit >= 0)
|
|
{
|
|
var mob = m_DeltaQueue.Dequeue();
|
|
|
|
if (mob == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
mob.m_InDeltaQueue = false;
|
|
|
|
try
|
|
{
|
|
mob.ProcessDelta();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Debug(ex, "Process Delta Queue for {Mobile} failed", mob);
|
|
}
|
|
}
|
|
|
|
if (m_DeltaQueue.Count > 0)
|
|
{
|
|
logger.Warning("{Count} mobiles left in delta queue after processing.", m_DeltaQueue.Count);
|
|
}
|
|
}
|
|
|
|
public virtual void OnKillsChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
public bool CheckAlive(bool message = true)
|
|
{
|
|
if (Alive)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (message)
|
|
{
|
|
LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019048); // I am dead and cannot do that.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void LaunchBrowser(string url)
|
|
{
|
|
m_NetState?.LaunchBrowser(url);
|
|
}
|
|
|
|
public void InitStats(int str, int dex, int intel)
|
|
{
|
|
m_Str = str;
|
|
m_Dex = dex;
|
|
m_Int = intel;
|
|
|
|
Hits = HitsMax;
|
|
Stam = StamMax;
|
|
Mana = ManaMax;
|
|
|
|
Delta(MobileDelta.Stat | MobileDelta.Hits | MobileDelta.Stam | MobileDelta.Mana);
|
|
}
|
|
|
|
public virtual void DisplayPaperdollTo(Mobile to)
|
|
{
|
|
EventSink.InvokePaperdollRequest(to, this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile requests to open his own paperdoll via the 'Open Paperdoll' macro.
|
|
/// </summary>
|
|
public virtual void OnPaperdollRequest()
|
|
{
|
|
if (CanPaperdollBeOpenedBy(this))
|
|
{
|
|
DisplayPaperdollTo(this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when <paramref name="from" /> wants to see this Mobile's stats.
|
|
/// </summary>
|
|
/// <param name="from"></param>
|
|
public virtual void OnStatsQuery(Mobile from)
|
|
{
|
|
if (from.Map == Map && Utility.InUpdateRange(Location, from.Location) && from.CanSee(this))
|
|
{
|
|
from.m_NetState.SendMobileStatus(from, this);
|
|
}
|
|
|
|
if (from == this)
|
|
{
|
|
m_NetState.SendStatLockInfo(this);
|
|
}
|
|
|
|
if (Party is IParty ip)
|
|
{
|
|
ip.OnStatsQuery(from, this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when <paramref name="from" /> wants to see this Mobile's skills.
|
|
/// </summary>
|
|
public virtual void OnSkillsQuery(Mobile from)
|
|
{
|
|
if (from == this)
|
|
{
|
|
m_NetState.SendSkillsUpdate(Skills);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <see cref="Region" /> changes.
|
|
/// </summary>
|
|
public virtual void OnRegionChange(Region old, Region @new)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile is single clicked.
|
|
/// </summary>
|
|
public virtual void OnSingleClick(Mobile from)
|
|
{
|
|
if (Deleted || AccessLevel == AccessLevel.Player && DisableHiddenSelfClick && Hidden && from == this)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (GuildClickMessage)
|
|
{
|
|
var guild = m_Guild;
|
|
|
|
if (guild != null && (m_DisplayGuildTitle || m_Player && guild.Type != GuildType.Regular))
|
|
{
|
|
var title = GuildTitle?.Trim() ?? "";
|
|
string type;
|
|
|
|
if (guild.Type >= 0 && (int)guild.Type < m_GuildTypes.Length)
|
|
{
|
|
type = m_GuildTypes[(int)guild.Type];
|
|
}
|
|
else
|
|
{
|
|
type = "";
|
|
}
|
|
|
|
var text = string.Format(
|
|
title.Length <= 0 ? "[{1}]{2}" : "[{0}, {1}]{2}",
|
|
title,
|
|
guild.Abbreviation,
|
|
type
|
|
);
|
|
|
|
PrivateOverheadMessage(MessageType.Regular, SpeechHue, true, text, from.NetState);
|
|
}
|
|
}
|
|
|
|
int hue;
|
|
|
|
if (NameHue != -1)
|
|
{
|
|
hue = NameHue;
|
|
}
|
|
else if (AccessLevel > AccessLevel.Player)
|
|
{
|
|
hue = 11;
|
|
}
|
|
else
|
|
{
|
|
hue = Notoriety.GetHue(Notoriety.Compute(from, this));
|
|
}
|
|
|
|
var name = Name ?? "";
|
|
|
|
var prefix = "";
|
|
|
|
if (ShowFameTitle && (m_Player || m_Body.IsHuman) && m_Fame >= 10000)
|
|
{
|
|
prefix = m_Female ? "Lady" : "Lord";
|
|
}
|
|
|
|
var suffix = ApplyNameSuffix(ClickTitle && !string.IsNullOrEmpty(Title) ? Title : "");
|
|
|
|
string val;
|
|
|
|
if (prefix.Length > 0 && suffix.Length > 0)
|
|
{
|
|
val = $"{prefix} {name} {suffix}";
|
|
}
|
|
else if (prefix.Length > 0)
|
|
{
|
|
val = $"{prefix} {name}";
|
|
}
|
|
else if (suffix.Length > 0)
|
|
{
|
|
val = $"{name} {suffix}";
|
|
}
|
|
else
|
|
{
|
|
val = name;
|
|
}
|
|
|
|
PrivateOverheadMessage(MessageType.Label, hue, AsciiClickMessage, val, from.NetState);
|
|
}
|
|
|
|
public bool CheckSkill(SkillName skill, double minSkill, double maxSkill) =>
|
|
SkillCheckLocationHandler?.Invoke(this, skill, minSkill, maxSkill) == true;
|
|
|
|
public bool CheckSkill(SkillName skill, double chance) =>
|
|
SkillCheckDirectLocationHandler?.Invoke(this, skill, chance) == true;
|
|
|
|
public bool CheckTargetSkill(SkillName skill, object target, double minSkill, double maxSkill) =>
|
|
SkillCheckTargetHandler?.Invoke(this, skill, target, minSkill, maxSkill) == true;
|
|
|
|
public bool CheckTargetSkill(SkillName skill, object target, double chance) =>
|
|
SkillCheckDirectTargetHandler?.Invoke(this, skill, target, chance) == true;
|
|
|
|
public virtual void DisruptiveAction()
|
|
{
|
|
if (Meditating)
|
|
{
|
|
Meditating = false;
|
|
SendLocalizedMessage(500134); // You stop meditating.
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the sector this Mobile is in gets <see cref="Map.Sector.Activate">activated</see>.
|
|
/// </summary>
|
|
public virtual void OnSectorActivate()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the sector this Mobile is in gets
|
|
/// <see cref="Map.Sector.Deactivate">deactivated</see>.
|
|
/// </summary>
|
|
public virtual void OnSectorDeactivate()
|
|
{
|
|
}
|
|
|
|
public static TimeSpan GetHitsRegenRate(Mobile m) => HitsRegenRateHandler?.Invoke(m) ?? DefaultHitsRate;
|
|
|
|
public static TimeSpan GetStamRegenRate(Mobile m) => StamRegenRateHandler?.Invoke(m) ?? DefaultStamRate;
|
|
|
|
public static TimeSpan GetManaRegenRate(Mobile m) => ManaRegenRateHandler?.Invoke(m) ?? DefaultManaRate;
|
|
|
|
public static char[] DefaultGhostChars = { 'o', 'O' };
|
|
|
|
public Prompt BeginPrompt(PromptCallback callback, PromptCallback cancelCallback) =>
|
|
Prompt = new SimplePrompt(callback, cancelCallback);
|
|
|
|
public Prompt BeginPrompt(PromptCallback callback, bool callbackHandlesCancel = false) =>
|
|
Prompt = new SimplePrompt(callback, callbackHandlesCancel);
|
|
|
|
public Prompt BeginPrompt<T>(PromptStateCallback<T> callback, PromptStateCallback<T> cancelCallback, T state) =>
|
|
Prompt = new SimpleStatePrompt<T>(callback, cancelCallback, state);
|
|
|
|
public Prompt BeginPrompt<T>(PromptStateCallback<T> callback, bool callbackHandlesCancel, T state) =>
|
|
Prompt = new SimpleStatePrompt<T>(callback, callbackHandlesCancel, state);
|
|
|
|
public Prompt BeginPrompt<T>(PromptStateCallback<T> callback, T state) =>
|
|
BeginPrompt(callback, false, state);
|
|
|
|
public virtual int GetAngerSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public virtual int GetIdleSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID + 1;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public virtual int GetAttackSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID + 2;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public virtual int GetHurtSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID + 3;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public virtual int GetDeathSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID + 4;
|
|
}
|
|
|
|
if (m_Body.IsHuman)
|
|
{
|
|
return Utility.Random(m_Female ? 0x314 : 0x423, m_Female ? 4 : 5);
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.ItemAtEnumerable<Item> GetItemsAt() =>
|
|
m_Map == null ? Map.ItemAtEnumerable<Item>.Empty : m_Map.GetItemsAt(m_Location);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.ItemAtEnumerable<T> GetItemsAt<T>() where T : Item =>
|
|
m_Map == null ? Map.ItemAtEnumerable<T>.Empty : m_Map.GetItemsAt<T>(m_Location);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.ItemBoundsEnumerable<Item> GetItemsInRange(int range) => GetItemsInRange<Item>(range);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.ItemBoundsEnumerable<T> GetItemsInRange<T>(int range) where T : Item =>
|
|
m_Map == null ? Map.ItemBoundsEnumerable<T>.Empty : m_Map.GetItemsInRange<T>(m_Location, range);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.MobileAtEnumerable<Mobile> GetMobilesInRange() => GetMobilesInRange<Mobile>();
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.MobileAtEnumerable<T> GetMobilesInRange<T>() where T : Mobile =>
|
|
m_Map == null ? Map.MobileAtEnumerable<T>.Empty : m_Map.GetMobilesAt<T>(m_Location);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.MobileBoundsEnumerable<Mobile> GetMobilesInRange(int range) => GetMobilesInRange<Mobile>(range);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.MobileBoundsEnumerable<T> GetMobilesInRange<T>(int range) where T : Mobile =>
|
|
m_Map == null ? Map.MobileBoundsEnumerable<T>.Empty : m_Map.GetMobilesInRange<T>(m_Location, range);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.ClientAtEnumerable GetClientsAt() =>
|
|
m_Map == null ? Map.ClientAtEnumerable.Empty : Map.GetClientsAt(m_Location);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Map.ClientBoundsEnumerable GetClientsInRange(int range) =>
|
|
m_Map == null ? Map.ClientBoundsEnumerable.Empty : Map.GetClientsInRange(m_Location, range);
|
|
|
|
public void SayTo(Mobile to, bool ascii, string text) =>
|
|
PrivateOverheadMessage(MessageType.Regular, SpeechHue, ascii, text, to.NetState);
|
|
|
|
public void SayTo(Mobile to, string text) => SayTo(to, false, text);
|
|
|
|
public void SayTo(Mobile to, int number, string args = "") =>
|
|
to.NetState.SendMessageLocalized(Serial, Body, MessageType.Regular, SpeechHue, 3, number, Name, args);
|
|
|
|
public void Say(bool ascii, string text) => PublicOverheadMessage(MessageType.Regular, SpeechHue, ascii, text);
|
|
|
|
public void Say(string text) => PublicOverheadMessage(MessageType.Regular, SpeechHue, false, text);
|
|
|
|
public void Say(int number, AffixType type, string affix, string args) =>
|
|
PublicOverheadMessage(MessageType.Regular, SpeechHue, number, type, affix, args);
|
|
|
|
public void Say(int number, string args = "") => PublicOverheadMessage(MessageType.Regular, SpeechHue, number, args);
|
|
|
|
public void Emote(string text) => PublicOverheadMessage(MessageType.Emote, EmoteHue, false, text);
|
|
|
|
public void Emote(int number, string args = "") => PublicOverheadMessage(MessageType.Emote, EmoteHue, number, args);
|
|
|
|
public void Whisper(string text) => PublicOverheadMessage(MessageType.Whisper, WhisperHue, false, text);
|
|
|
|
public void Whisper(int number, string args = "") =>
|
|
PublicOverheadMessage(MessageType.Whisper, WhisperHue, number, args);
|
|
|
|
public void Yell(string text) => PublicOverheadMessage(MessageType.Yell, YellHue, false, text);
|
|
|
|
public void Yell(int number, string args = "") =>
|
|
PublicOverheadMessage(MessageType.Yell, YellHue, number, args);
|
|
|
|
public bool SendHuePicker(HuePicker p)
|
|
{
|
|
if (m_NetState != null)
|
|
{
|
|
p.SendTo(m_NetState);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool SendMenu(IMenu m)
|
|
{
|
|
if (m_NetState == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
m.SendTo(m_NetState);
|
|
return true;
|
|
}
|
|
|
|
public virtual bool CanBeBeneficial(Mobile target) => CanBeBeneficial(target, true, false);
|
|
|
|
public virtual bool CanBeBeneficial(Mobile target, bool message) => CanBeBeneficial(target, message, false);
|
|
|
|
public virtual bool CanBeBeneficial(Mobile target, bool message, bool allowDead)
|
|
{
|
|
if (target == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Deleted || target.Deleted || !Alive || IsDeadBondedPet ||
|
|
!allowDead && (!target.Alive || target.IsDeadBondedPet))
|
|
{
|
|
if (message)
|
|
{
|
|
SendLocalizedMessage(1001017); // You can not perform beneficial acts on your target.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (target == this)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if ( /*m_Player &&*/!Region.AllowBeneficial(this, target))
|
|
{
|
|
// TODO: Pets
|
|
// if (!(target.m_Player || target.Body.IsHuman || target.Body.IsAnimal))
|
|
// {
|
|
if (message)
|
|
{
|
|
SendLocalizedMessage(1001017); // You can not perform beneficial acts on your target.
|
|
}
|
|
|
|
return false;
|
|
// }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool IsBeneficialCriminal(Mobile target)
|
|
{
|
|
if (this == target)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var n = Notoriety.Compute(this, target);
|
|
|
|
return n is Notoriety.Criminal or Notoriety.Murderer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile <see cref="DoBeneficial">does a beneficial action</see>.
|
|
/// </summary>
|
|
public virtual void OnBeneficialAction(Mobile target, bool isCriminal)
|
|
{
|
|
if (isCriminal)
|
|
{
|
|
CriminalAction(false);
|
|
}
|
|
}
|
|
|
|
public virtual void DoBeneficial(Mobile target)
|
|
{
|
|
if (target == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnBeneficialAction(target, IsBeneficialCriminal(target));
|
|
|
|
Region.OnBeneficialAction(this, target);
|
|
target.Region.OnGotBeneficialAction(this, target);
|
|
}
|
|
|
|
public virtual bool BeneficialCheck(Mobile target)
|
|
{
|
|
if (CanBeBeneficial(target, true))
|
|
{
|
|
DoBeneficial(target);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public virtual bool CanBeHarmful(Mobile target) => CanBeHarmful(target, true);
|
|
|
|
public virtual bool CanBeHarmful(Mobile target, bool message) => CanBeHarmful(target, message, false);
|
|
|
|
public virtual bool CanBeHarmful(Mobile target, bool message, bool ignoreOurBlessedness)
|
|
{
|
|
if (target == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Deleted || !ignoreOurBlessedness && m_Blessed || target.Deleted || target.m_Blessed || !Alive ||
|
|
IsDeadBondedPet || !target.Alive || target.IsDeadBondedPet)
|
|
{
|
|
if (message)
|
|
{
|
|
SendLocalizedMessage(1001018); // You can not perform negative acts on your target.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (target == this)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// TODO: Pets
|
|
if (!Region.AllowHarmful(this, target))
|
|
{
|
|
if (message)
|
|
{
|
|
SendLocalizedMessage(1001018); // You can not perform negative acts on your target.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool IsHarmfulCriminal(Mobile target) =>
|
|
this != target && Notoriety.Compute(this, target) == Notoriety.Innocent;
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile <see cref="DoHarmful">does a harmful action</see>.
|
|
/// </summary>
|
|
public virtual void OnHarmfulAction(Mobile target, bool isCriminal)
|
|
{
|
|
if (isCriminal)
|
|
{
|
|
CriminalAction(false);
|
|
}
|
|
}
|
|
|
|
public virtual void DoHarmful(Mobile target, bool indirect = false)
|
|
{
|
|
if (target == null || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var isCriminal = IsHarmfulCriminal(target);
|
|
|
|
OnHarmfulAction(target, isCriminal);
|
|
target.AggressiveAction(this, isCriminal);
|
|
|
|
Region.OnDidHarmful(this, target);
|
|
target.Region.OnGotHarmful(this, target);
|
|
|
|
if (!indirect)
|
|
{
|
|
Combatant = target;
|
|
}
|
|
|
|
_expireCombatantTimerToken.Cancel();
|
|
Timer.StartTimer(ExpireCombatantDelay, ExpireCombatant, out _expireCombatantTimerToken);
|
|
}
|
|
|
|
public virtual bool HarmfulCheck(Mobile target)
|
|
{
|
|
if (CanBeHarmful(target))
|
|
{
|
|
DoHarmful(target);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public virtual void RemoveStatMod(StatMod mod)
|
|
{
|
|
if (mod == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Remove it just in case it was orphaned somehow.
|
|
mod.Remove();
|
|
|
|
if (_statMods == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_statMods.Remove(mod))
|
|
{
|
|
CheckStatTimers();
|
|
Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
|
|
}
|
|
|
|
if (_statMods.Count == 0)
|
|
{
|
|
_statMods = null;
|
|
}
|
|
}
|
|
|
|
public virtual void RemoveStatMod(string name)
|
|
{
|
|
if (_statMods == null || name == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = _statMods.Count - 1; i >= 0; i--)
|
|
{
|
|
var mod = _statMods[i];
|
|
if (mod.Name == name)
|
|
{
|
|
_statMods.RemoveAt(i);
|
|
mod.Remove();
|
|
CheckStatTimers();
|
|
Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
|
|
}
|
|
}
|
|
|
|
if (_statMods.Count == 0)
|
|
{
|
|
_statMods = null;
|
|
}
|
|
}
|
|
|
|
public virtual StatMod GetStatMod(string name, bool includeElapsed = false)
|
|
{
|
|
if (_statMods == null || name == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
for (var i = 0; i < _statMods.Count; i++)
|
|
{
|
|
var mod = _statMods[i];
|
|
if (mod.Name == name && !mod.HasElapsed() || includeElapsed)
|
|
{
|
|
return mod;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public virtual void AddStatMod(StatMod mod)
|
|
{
|
|
if (mod == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
mod.Owner = this;
|
|
|
|
_statMods ??= new List<StatMod>();
|
|
_statMods.Add(mod);
|
|
Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
|
|
CheckStatTimers();
|
|
}
|
|
|
|
private static MobileDelta GetStatDelta(StatType type)
|
|
{
|
|
MobileDelta delta = 0;
|
|
|
|
if ((type & StatType.Str) != 0)
|
|
{
|
|
delta |= MobileDelta.Hits;
|
|
}
|
|
|
|
if ((type & StatType.Dex) != 0)
|
|
{
|
|
delta |= MobileDelta.Stam;
|
|
}
|
|
|
|
if ((type & StatType.Int) != 0)
|
|
{
|
|
delta |= MobileDelta.Mana;
|
|
}
|
|
|
|
return delta;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the total modified offset for the specified stat type.
|
|
/// </summary>
|
|
public int GetStatOffset(StatType type)
|
|
{
|
|
if (_statMods == null || _statMods.Count == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var offset = 0;
|
|
|
|
for (var i = 0; i < _statMods.Count; i++)
|
|
{
|
|
var mod = _statMods[i];
|
|
if ((mod.Type & type) != 0 && !mod.HasElapsed())
|
|
{
|
|
offset += mod.Offset;
|
|
}
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the <see cref="RawStr" /> changes.
|
|
/// <seealso cref="RawStr" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
public virtual void OnRawStrChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <see cref="RawDex" /> changes.
|
|
/// <seealso cref="RawDex" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
public virtual void OnRawDexChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the <see cref="RawInt" /> changes.
|
|
/// <seealso cref="RawInt" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
public virtual void OnRawIntChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the <see cref="RawStr" />, <see cref="RawDex" />, or <see cref="RawInt" />
|
|
/// changes.
|
|
/// <seealso cref="OnRawStrChange" />
|
|
/// <seealso cref="OnRawDexChange" />
|
|
/// <seealso cref="OnRawIntChange" />
|
|
/// </summary>
|
|
public virtual void OnRawStatChange(StatType stat, int oldValue)
|
|
{
|
|
}
|
|
|
|
public virtual void OnHitsChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
public virtual void OnStamChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
public virtual void OnManaChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when a call to <see cref="ApplyPoison" /> failed because <see cref="CheckPoisonImmunity" />
|
|
/// returned false: the Mobile was resistant to the poison. By default, this broadcasts an overhead message: * The poison
|
|
/// seems to have no effect. *
|
|
/// <seealso cref="CheckPoisonImmunity" />
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnPoisonImmunity(Mobile from, Poison poison)
|
|
{
|
|
PublicOverheadMessage(MessageType.Emote, 0x3B2, 1005534); // * The poison seems to have no effect. *
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a call to <see cref="ApplyPoison" /> failed because
|
|
/// <see cref="CheckHigherPoison" /> returned false: the Mobile was already poisoned by an equal or greater strength poison.
|
|
/// <seealso cref="CheckHigherPoison" />
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnHigherPoison(Mobile from, Poison poison)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when a call to <see cref="ApplyPoison" /> succeeded. By default, this broadcasts an overhead
|
|
/// message varying by the level of the poison. Example: * Zippy begins to spasm uncontrollably. *
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnPoisoned(Mobile from, Poison poison, Poison oldPoison)
|
|
{
|
|
if (poison != null)
|
|
{
|
|
LocalOverheadMessage(MessageType.Regular, 0x21, 1042857 + poison.Level * 2);
|
|
NonlocalOverheadMessage(MessageType.Regular, 0x21, 1042858 + poison.Level * 2, Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Called from <see cref="ApplyPoison" />, this method checks if the Mobile is immune to some
|
|
/// <see cref="Poison" />. If true, <see cref="OnPoisonImmunity" /> will be invoked and
|
|
/// <see cref="ApplyPoisonResult.Immune" /> is returned.
|
|
/// <seealso cref="OnPoisonImmunity" />
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual bool CheckPoisonImmunity(Mobile from, Poison poison) => false;
|
|
|
|
/// <summary>
|
|
/// Overridable. Called from <see cref="ApplyPoison" />, this method checks if the Mobile is already poisoned by some
|
|
/// <see cref="Poison" /> of equal or greater strength. If true, <see cref="OnHigherPoison" /> will be invoked and
|
|
/// <see cref="ApplyPoisonResult.HigherPoisonActive" /> is returned.
|
|
/// <seealso cref="OnHigherPoison" />
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual bool CheckHigherPoison(Mobile from, Poison poison) =>
|
|
m_Poison != null && m_Poison.Level >= poison.Level;
|
|
|
|
/// <summary>
|
|
/// Overridable. Attempts to apply poison to the Mobile. Checks are made such that no
|
|
/// <see cref="CheckHigherPoison">higher poison is active</see> and that the Mobile is not
|
|
/// <see cref="CheckPoisonImmunity">immune to the poison</see>. Provided those assertions are true, the
|
|
/// <paramref name="poison" /> is applied and <see cref="OnPoisoned" /> is invoked.
|
|
/// <seealso cref="Poison" />
|
|
/// <seealso cref="CurePoison" />
|
|
/// </summary>
|
|
/// <returns>
|
|
/// One of four possible values:
|
|
/// <list type="table">
|
|
/// <item>
|
|
/// <term>
|
|
/// <see cref="ApplyPoisonResult.Cured">Cured</see>
|
|
/// </term>
|
|
/// <description>The <paramref name="poison" /> parameter was null and so <see cref="CurePoison" /> was invoked.</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>
|
|
/// <see cref="ApplyPoisonResult.HigherPoisonActive">HigherPoisonActive</see>
|
|
/// </term>
|
|
/// <description>The call to <see cref="CheckHigherPoison" /> returned false.</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>
|
|
/// <see cref="ApplyPoisonResult.Immune">Immune</see>
|
|
/// </term>
|
|
/// <description>The call to <see cref="CheckPoisonImmunity" /> returned false.</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>
|
|
/// <see cref="ApplyPoisonResult.Poisoned">Poisoned</see>
|
|
/// </term>
|
|
/// <description>The <paramref name="poison" /> was successfully applied.</description>
|
|
/// </item>
|
|
/// </list>
|
|
/// </returns>
|
|
public virtual ApplyPoisonResult ApplyPoison(Mobile from, Poison poison)
|
|
{
|
|
if (poison == null)
|
|
{
|
|
CurePoison(from);
|
|
return ApplyPoisonResult.Cured;
|
|
}
|
|
|
|
if (CheckHigherPoison(from, poison))
|
|
{
|
|
OnHigherPoison(from, poison);
|
|
return ApplyPoisonResult.HigherPoisonActive;
|
|
}
|
|
|
|
if (CheckPoisonImmunity(from, poison))
|
|
{
|
|
OnPoisonImmunity(from, poison);
|
|
return ApplyPoisonResult.Immune;
|
|
}
|
|
|
|
var oldPoison = m_Poison;
|
|
Poison = poison;
|
|
|
|
OnPoisoned(from, poison, oldPoison);
|
|
|
|
return ApplyPoisonResult.Poisoned;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Called from <see cref="CurePoison" />, this method checks to see that the Mobile can be cured of
|
|
/// <see cref="Poison" />
|
|
/// <seealso cref="CurePoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual bool CheckCure(Mobile from) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a call to <see cref="CurePoison" /> succeeded.
|
|
/// <seealso cref="CurePoison" />
|
|
/// <seealso cref="CheckCure" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnCured(Mobile from, Poison oldPoison)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a call to <see cref="CurePoison" /> failed.
|
|
/// <seealso cref="CurePoison" />
|
|
/// <seealso cref="CheckCure" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnFailedCure(Mobile from)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Attempts to cure any poison that is currently active.
|
|
/// </summary>
|
|
/// <returns>True if poison was cured, false if otherwise.</returns>
|
|
public virtual bool CurePoison(Mobile from)
|
|
{
|
|
if (CheckCure(from))
|
|
{
|
|
var oldPoison = m_Poison;
|
|
Poison = null;
|
|
|
|
OnCured(from, oldPoison);
|
|
|
|
return true;
|
|
}
|
|
|
|
OnFailedCure(from);
|
|
|
|
return false;
|
|
}
|
|
|
|
public void MovingEffect(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int hue, int renderMode
|
|
) =>
|
|
Effects.SendMovingEffect(this, to, itemID, speed, duration, fixedDirection, explodes, hue, renderMode);
|
|
|
|
public void MovingEffect(IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes) =>
|
|
Effects.SendMovingEffect(this, to, itemID, speed, duration, fixedDirection, explodes);
|
|
|
|
public void MovingParticles(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int hue, int renderMode, int effect, int explodeEffect, int explodeSound, EffectLayer layer, int unknown
|
|
) =>
|
|
Effects.SendMovingParticles(
|
|
this,
|
|
to,
|
|
itemID,
|
|
speed,
|
|
duration,
|
|
fixedDirection,
|
|
explodes,
|
|
hue,
|
|
renderMode,
|
|
effect,
|
|
explodeEffect,
|
|
explodeSound,
|
|
layer,
|
|
unknown
|
|
);
|
|
|
|
public void MovingParticles(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int hue, int renderMode, int effect, int explodeEffect, int explodeSound, int unknown
|
|
) =>
|
|
Effects.SendMovingParticles(
|
|
this,
|
|
to,
|
|
itemID,
|
|
speed,
|
|
duration,
|
|
fixedDirection,
|
|
explodes,
|
|
hue,
|
|
renderMode,
|
|
effect,
|
|
explodeEffect,
|
|
explodeSound,
|
|
(EffectLayer)255,
|
|
unknown
|
|
);
|
|
|
|
public void MovingParticles(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int effect, int explodeEffect, int explodeSound, int unknown
|
|
) =>
|
|
Effects.SendMovingParticles(
|
|
this,
|
|
to,
|
|
itemID,
|
|
speed,
|
|
duration,
|
|
fixedDirection,
|
|
explodes,
|
|
effect,
|
|
explodeEffect,
|
|
explodeSound,
|
|
unknown
|
|
);
|
|
|
|
public void MovingParticles(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int effect, int explodeEffect, int explodeSound
|
|
) =>
|
|
Effects.SendMovingParticles(
|
|
this,
|
|
to,
|
|
itemID,
|
|
speed,
|
|
duration,
|
|
fixedDirection,
|
|
explodes,
|
|
0,
|
|
0,
|
|
effect,
|
|
explodeEffect,
|
|
explodeSound,
|
|
0
|
|
);
|
|
|
|
public void FixedEffect(int itemID, int speed, int duration, int hue, int renderMode) =>
|
|
Effects.SendTargetEffect(this, itemID, speed, duration, hue, renderMode);
|
|
|
|
public void FixedEffect(int itemID, int speed, int duration) =>
|
|
Effects.SendTargetEffect(this, itemID, speed, duration);
|
|
|
|
public void FixedParticles(
|
|
int itemID, int speed, int duration, int effect, int hue, int renderMode, EffectLayer layer, int unknown
|
|
) =>
|
|
Effects.SendTargetParticles(this, itemID, speed, duration, hue, renderMode, effect, layer, unknown);
|
|
|
|
public void FixedParticles(
|
|
int itemID, int speed, int duration, int effect, int hue, int renderMode, EffectLayer layer
|
|
) =>
|
|
Effects.SendTargetParticles(this, itemID, speed, duration, hue, renderMode, effect, layer);
|
|
|
|
public void FixedParticles(int itemID, int speed, int duration, int effect, EffectLayer layer, int unknown) =>
|
|
Effects.SendTargetParticles(this, itemID, speed, duration, 0, 0, effect, layer, unknown);
|
|
|
|
public void FixedParticles(int itemID, int speed, int duration, int effect, EffectLayer layer) =>
|
|
Effects.SendTargetParticles(this, itemID, speed, duration, 0, 0, effect, layer);
|
|
|
|
public void BoltEffect(int hue) => Effects.SendBoltEffect(this, true, hue);
|
|
|
|
public Direction GetDirectionTo(int x, int y, bool run = false)
|
|
{
|
|
var dx = m_Location.m_X - x;
|
|
var dy = m_Location.m_Y - y;
|
|
|
|
var rx = (dx - dy) * 44;
|
|
var ry = (dx + dy) * 44;
|
|
|
|
var ax = rx.Abs();
|
|
var ay = ry.Abs();
|
|
|
|
Direction ret;
|
|
|
|
if ((ay >> 1) - ax >= 0)
|
|
{
|
|
ret = ry > 0 ? Direction.Up : Direction.Down;
|
|
}
|
|
else if ((ax >> 1) - ay >= 0)
|
|
{
|
|
ret = rx > 0 ? Direction.Left : Direction.Right;
|
|
}
|
|
else
|
|
{
|
|
ret = rx switch
|
|
{
|
|
>= 0 when ry >= 0 => Direction.West,
|
|
>= 0 => Direction.South,
|
|
< 0 when ry < 0 => Direction.East,
|
|
_ => Direction.North
|
|
};
|
|
}
|
|
|
|
if (run)
|
|
{
|
|
ret |= Direction.Running;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Direction GetDirectionTo(Point2D p, bool run = false) => GetDirectionTo(p.m_X, p.m_Y, run);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Direction GetDirectionTo(Point3D p, bool run = false) => GetDirectionTo(p.m_X, p.m_Y, run);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Direction GetDirectionTo(IPoint2D p, bool run = false) =>
|
|
p == null ? Direction.North | (run ? Direction.Running : 0) : GetDirectionTo(p.X, p.Y, run);
|
|
|
|
public void PublicOverheadMessage(
|
|
MessageType type, int hue, bool ascii, string text, bool noLineOfSight = true,
|
|
AccessLevel accessLevel = AccessLevel.Player
|
|
)
|
|
{
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text)].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
if (
|
|
state.Mobile.AccessLevel >= accessLevel &&
|
|
state.Mobile.CanSee(this) &&
|
|
(noLineOfSight || state.Mobile.InLOS(this))
|
|
)
|
|
{
|
|
var length = OutgoingMessagePackets.CreateMessage(
|
|
buffer,
|
|
Serial,
|
|
Body,
|
|
type,
|
|
hue,
|
|
3,
|
|
ascii,
|
|
Language,
|
|
Name,
|
|
text
|
|
);
|
|
|
|
if (length != buffer.Length)
|
|
{
|
|
buffer = buffer[..length]; // Adjust to the actual size
|
|
}
|
|
|
|
state.Send(buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void PublicOverheadMessage(MessageType type, int hue, int number, string args = "", bool noLineOfSight = true)
|
|
{
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLocalizedLength(args)].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
if (state.Mobile.CanSee(this) && (noLineOfSight || state.Mobile.InLOS(this)))
|
|
{
|
|
var length = OutgoingMessagePackets.CreateMessageLocalized(
|
|
buffer,
|
|
Serial,
|
|
Body,
|
|
type,
|
|
hue,
|
|
3,
|
|
number,
|
|
Name,
|
|
args
|
|
);
|
|
|
|
if (length != buffer.Length)
|
|
{
|
|
buffer = buffer[..length]; // Adjust to the actual size
|
|
}
|
|
|
|
state.Send(buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void PublicOverheadMessage(
|
|
MessageType type, int hue, int number, AffixType affixType, string affix,
|
|
string args = "", bool noLineOfSight = false,
|
|
AccessLevel accessLevel = AccessLevel.Player
|
|
)
|
|
{
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLocalizedAffixLength(affix, args)]
|
|
.InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
if (
|
|
state.Mobile.AccessLevel >= accessLevel &&
|
|
state.Mobile.CanSee(this) &&
|
|
(noLineOfSight || state.Mobile.InLOS(this))
|
|
)
|
|
{
|
|
var length = OutgoingMessagePackets.CreateMessageLocalizedAffix(
|
|
buffer,
|
|
Serial,
|
|
Body,
|
|
type,
|
|
hue,
|
|
3,
|
|
number,
|
|
Name,
|
|
affixType,
|
|
affix,
|
|
args
|
|
);
|
|
|
|
if (length != buffer.Length)
|
|
{
|
|
buffer = buffer[..length]; // Adjust to the actual size
|
|
}
|
|
|
|
state.Send(buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void PrivateOverheadMessage(MessageType type, int hue, bool ascii, string text, NetState state)
|
|
{
|
|
state.SendMessage(Serial, Body, type, hue, 3, ascii, m_Language, Name, text);
|
|
}
|
|
|
|
public void PrivateOverheadMessage(MessageType type, int hue, int number, NetState state) =>
|
|
PrivateOverheadMessage(type, hue, number, "", state);
|
|
|
|
public void PrivateOverheadMessage(MessageType type, int hue, int number, string args, NetState state) =>
|
|
state.SendMessageLocalized(Serial, Body, type, hue, 3, number, Name, args);
|
|
|
|
public void LocalOverheadMessage(MessageType type, int hue, bool ascii, string text) =>
|
|
m_NetState.SendMessage(Serial, Body, type, hue, 3, ascii, m_Language, Name, text);
|
|
|
|
public void LocalOverheadMessage(MessageType type, int hue, int number, string args = "") =>
|
|
m_NetState.SendMessageLocalized(Serial, Body, type, hue, 3, number, Name, args);
|
|
|
|
public void NonlocalOverheadMessage(MessageType type, int hue, int number, string args = "")
|
|
{
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLocalizedLength(args)].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
if (state != m_NetState && state.Mobile.CanSee(this))
|
|
{
|
|
var length = OutgoingMessagePackets.CreateMessageLocalized(
|
|
buffer,
|
|
Serial,
|
|
Body,
|
|
type,
|
|
hue,
|
|
3,
|
|
number,
|
|
Name,
|
|
args
|
|
);
|
|
|
|
if (length != buffer.Length)
|
|
{
|
|
buffer = buffer[..length]; // Adjust to the actual size
|
|
}
|
|
|
|
state.Send(buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void NonlocalOverheadMessage(MessageType type, int hue, bool ascii, string text)
|
|
{
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text)].InitializePacket();
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
if (state != m_NetState && state.Mobile.CanSee(this))
|
|
{
|
|
var length = OutgoingMessagePackets.CreateMessage(
|
|
buffer,
|
|
Serial,
|
|
Body,
|
|
type,
|
|
hue,
|
|
3,
|
|
ascii,
|
|
Language,
|
|
Name,
|
|
text
|
|
);
|
|
|
|
if (length != buffer.Length)
|
|
{
|
|
buffer = buffer[..length]; // Adjust to the actual size
|
|
}
|
|
|
|
state.Send(buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SendLocalizedMessage(int number, string args = "", int hue = 0x3B2) =>
|
|
m_NetState.SendMessageLocalized(Serial.MinusOne, -1, MessageType.Regular, hue, 3, number, "System", args);
|
|
|
|
public void SendLocalizedMessage(int number, bool append, string affix, string args = "", int hue = 0x3B2) =>
|
|
m_NetState.SendMessageLocalizedAffix(
|
|
Serial.MinusOne,
|
|
-1,
|
|
MessageType.Regular,
|
|
hue,
|
|
3,
|
|
number,
|
|
"System",
|
|
(append ? AffixType.Append : AffixType.Prepend) | AffixType.System,
|
|
affix,
|
|
args
|
|
);
|
|
|
|
public void SendMessage(string text) => SendMessage(0x3B2, text);
|
|
|
|
public void SendMessage(int hue, string text) =>
|
|
m_NetState.SendMessage(Serial.MinusOne, -1, MessageType.Regular, hue, 3, false, "ENU", "System", text);
|
|
|
|
public void SendAsciiMessage(string text) => SendAsciiMessage(0x3B2, text);
|
|
|
|
public void SendAsciiMessage(int hue, string text) =>
|
|
m_NetState.SendMessage(Serial.MinusOne, -1, MessageType.Regular, hue, 3, true, null, "System", text);
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile is double clicked. By default, this method can either dismount or open the
|
|
/// paperdoll.
|
|
/// <seealso cref="CanPaperdollBeOpenedBy" />
|
|
/// <seealso cref="DisplayPaperdollTo" />
|
|
/// </summary>
|
|
public virtual void OnDoubleClick(Mobile from)
|
|
{
|
|
if (this == from && (!DisableDismountInWarmode || !m_Warmode))
|
|
{
|
|
var mount = Mount;
|
|
|
|
if (mount != null)
|
|
{
|
|
mount.Rider = null;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (CanPaperdollBeOpenedBy(from))
|
|
{
|
|
DisplayPaperdollTo(from);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile is double clicked by someone who is over 18 tiles away.
|
|
/// <seealso cref="OnDoubleClick" />
|
|
/// </summary>
|
|
public virtual void OnDoubleClickOutOfRange(Mobile from)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile is double clicked by someone who can no longer see the Mobile. This
|
|
/// may
|
|
/// happen, for example, using 'Last Object' after the Mobile has hidden.
|
|
/// <seealso cref="OnDoubleClick" />
|
|
/// </summary>
|
|
public virtual void OnDoubleClickCantSee(Mobile from)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile is double clicked by someone who is not alive. Similar to
|
|
/// <see cref="OnDoubleClick" />, this method will show the paperdoll. It does not, however, provide any dismount
|
|
/// functionality.
|
|
/// <seealso cref="OnDoubleClick" />
|
|
/// </summary>
|
|
public virtual void OnDoubleClickDead(Mobile from)
|
|
{
|
|
if (CanPaperdollBeOpenedBy(from))
|
|
{
|
|
DisplayPaperdollTo(from);
|
|
}
|
|
}
|
|
|
|
private class ManaTimer : Timer
|
|
{
|
|
private readonly Mobile m_Owner;
|
|
|
|
public ManaTimer(Mobile m) : base(GetManaRegenRate(m), GetManaRegenRate(m)) => m_Owner = m;
|
|
|
|
protected override void OnTick()
|
|
{
|
|
if (m_Owner.CanRegenMana)
|
|
{
|
|
m_Owner.Mana++;
|
|
}
|
|
|
|
Delay = Interval = GetManaRegenRate(m_Owner);
|
|
}
|
|
}
|
|
|
|
private class HitsTimer : Timer
|
|
{
|
|
private readonly Mobile m_Owner;
|
|
|
|
public HitsTimer(Mobile m) : base(GetHitsRegenRate(m), GetHitsRegenRate(m)) => m_Owner = m;
|
|
|
|
protected override void OnTick()
|
|
{
|
|
if (m_Owner.CanRegenHits)
|
|
{
|
|
m_Owner.Hits++;
|
|
}
|
|
|
|
Delay = Interval = GetHitsRegenRate(m_Owner);
|
|
}
|
|
}
|
|
|
|
private class StamTimer : Timer
|
|
{
|
|
private readonly Mobile m_Owner;
|
|
|
|
public StamTimer(Mobile m) : base(GetStamRegenRate(m), GetStamRegenRate(m)) => m_Owner = m;
|
|
|
|
protected override void OnTick()
|
|
{
|
|
if (m_Owner.CanRegenStam)
|
|
{
|
|
m_Owner.Stam++;
|
|
}
|
|
|
|
Delay = Interval = GetStamRegenRate(m_Owner);
|
|
}
|
|
}
|
|
|
|
private void Logout()
|
|
{
|
|
if (m_Map != Map.Internal)
|
|
{
|
|
EventSink.InvokeLogout(this);
|
|
|
|
LogoutLocation = m_Location;
|
|
LogoutMap = m_Map;
|
|
|
|
Internalize();
|
|
}
|
|
}
|
|
|
|
private void ExpireCriminal()
|
|
{
|
|
Criminal = false;
|
|
}
|
|
}
|