### Summary * Fixes the accidental limitation of dead character speech (OoOo) to 256 characters. * Optimizes it by 1.5x ```cs | Method | Mean | Error | StdDev | Allocated | |------------------------ |----------:|---------:|---------:|----------:| | ManuaLoopMutation | 147.01 ns | 1.298 ns | 1.084 ns | - | | SpanLoopMutation | 92.03 ns | 0.583 ns | 0.487 ns | - | ```
9216 lines
245 KiB
C#
9216 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 Server.Buffers;
|
|
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;
|
|
}
|
|
|
|
ReadOnlySpan<char> ghostChars = (GhostChars ?? DefaultGhostChars).AsSpan();
|
|
|
|
var length = text.Length;
|
|
char[] rentedChars = null;
|
|
Span<char> chars = length <= 256
|
|
? stackalloc char[length]
|
|
: rentedChars = STArrayPool<char>.Shared.Rent(length);
|
|
|
|
try
|
|
{
|
|
var textSpan = text.AsSpan();
|
|
for (var i = 0; i < textSpan.Length; ++i)
|
|
{
|
|
chars[i] = textSpan[i] != ' ' ? ghostChars.RandomElement() : ' ';
|
|
}
|
|
|
|
text = new string(chars[..length]);
|
|
}
|
|
finally
|
|
{
|
|
if (rentedChars != null)
|
|
{
|
|
STArrayPool<char>.Shared.Return(rentedChars);
|
|
}
|
|
}
|
|
|
|
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 static 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:
|
|
case MessageType.Label:
|
|
case MessageType.Focus:
|
|
case MessageType.Spell:
|
|
case MessageType.Guild:
|
|
case MessageType.Alliance:
|
|
case MessageType.Command:
|
|
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 is Item parent)
|
|
{
|
|
if (!(CanSee(parent) && parent.IsChildVisibleTo(this, item)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (item.Parent is Mobile mobile && !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 readonly 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;
|
|
}
|
|
}
|