feat: Adds Stamina System to overhaul overweight. (#1465)
## Overhaul to Stamina (Overweight) System
### Added configurations
```json
{
"settings": {
"stamina.enableMountStamina": "True",
"stamina.cannotMoveWhenFatigued": "True",
"stamina.stonesPerOverweightLoss": "25",
"stamina.stonesOverweightAllowance": "4",
"stamina.baseOverweightLoss": "5",
"stamina.additionalLossWhenBelow": "0.1",
"stamina.mountLastMoveStepsReset": "01:00:00:00",
"stamina.enableMountStamina": "True",
"stamina.useMountStaminaOnlyWhenOverloaded": "False",
},
}
```
- `stamina.cannotMoveWhenFatigued` - By default, Pre-AOS expansions will outright block a player if they are fatigued. A player is fatigued when they run out of stamina by any mechanism.
- `stamina.stonesPerOverweightLoss` - The amount of stamina lost for every X stones above overweight. Example, if a person is overweight 28 stones overweight, then there there is 1 additional stamina. 28 / 25 = 1 (no decimals, no rounding)
- `stamina.stonesOverweightAllowance` - The number of stones allowed before overweight penalties take affect. This _does not_ substract from the overweight stones calculation.
- `stamina.baseOverweightLoss` - The base amount of stamina loss for being overweight. While running, the final amount is multiplied by 2. If mount stamina is turned off, then the final amount is divided by 3 while mounted.
### Mount stamina
Mounts have a new property `StepsMax` to determine the maximum steps they can take before being fatigued. To regain steps, the player _must stay on the mount and not move_ (per OSI). The gain rates are configurable. If a mount is dismounted, or the player logs off, the mount is considered inactive and mounts regain all of their steps after _24 hours_.
### Changes to player stamina
Players now have a proper inactivity time reset for their steps. If a player is idle for 16 seconds (including logging off, or the server being offline), then the steps counter is rest.
### Developer Notes
- `StepsTaken` - This field has been removed. It was not serialized and could not be used reliably. If a developer was using it, then the recommendation is to build a mechanism to track total steps another way.
- `IHasStamina` - This new interface was added and currently `IMount` and `PlayerMobile` are valid types.
Important: Entities that are sent to the StaminaSystem for tracking (mounts, players, or something else), must be an `ISerializable` to be serialized properly. If they are not, then the serialization system will record a null, and nothing will be deserialized upon world load. No errors will be given.
This commit is contained in:
parent
25eaff0b68
commit
0f2870e94a
38 changed files with 983 additions and 493 deletions
12
Projects/Server/Mobiles/IHasSteps.cs
Normal file
12
Projects/Server/Mobiles/IHasSteps.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
|
||||
namespace Server.Mobiles;
|
||||
|
||||
public interface IHasSteps
|
||||
{
|
||||
int StepsMax { get; }
|
||||
|
||||
int StepsGainedPerIdleTime { get; }
|
||||
|
||||
TimeSpan IdleTimePerStepsGain { get; }
|
||||
}
|
||||
|
|
@ -13,9 +13,11 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Server.Mobiles;
|
||||
|
||||
public interface IMount
|
||||
public interface IMount : IHasSteps
|
||||
{
|
||||
Mobile Rider { get; set; }
|
||||
void OnRiderDamaged(int amount, Mobile from, bool willKill);
|
||||
|
|
|
|||
|
|
@ -4081,33 +4081,49 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
switch (d & Direction.Mask)
|
||||
{
|
||||
case Direction.North:
|
||||
--y;
|
||||
break;
|
||||
{
|
||||
--y;
|
||||
break;
|
||||
}
|
||||
case Direction.Right:
|
||||
++x;
|
||||
--y;
|
||||
break;
|
||||
{
|
||||
++x;
|
||||
--y;
|
||||
break;
|
||||
}
|
||||
case Direction.East:
|
||||
++x;
|
||||
break;
|
||||
{
|
||||
++x;
|
||||
break;
|
||||
}
|
||||
case Direction.Down:
|
||||
++x;
|
||||
++y;
|
||||
break;
|
||||
{
|
||||
++x;
|
||||
++y;
|
||||
break;
|
||||
}
|
||||
case Direction.South:
|
||||
++y;
|
||||
break;
|
||||
{
|
||||
++y;
|
||||
break;
|
||||
}
|
||||
case Direction.Left:
|
||||
--x;
|
||||
++y;
|
||||
break;
|
||||
{
|
||||
--x;
|
||||
++y;
|
||||
break;
|
||||
}
|
||||
case Direction.West:
|
||||
--x;
|
||||
break;
|
||||
{
|
||||
--x;
|
||||
break;
|
||||
}
|
||||
case Direction.Up:
|
||||
--x;
|
||||
--y;
|
||||
break;
|
||||
{
|
||||
--x;
|
||||
--y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
newLocation.m_X = x;
|
||||
|
|
@ -5428,38 +5444,64 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
switch (type)
|
||||
{
|
||||
case MessageType.Regular:
|
||||
SpeechHue = hue;
|
||||
break;
|
||||
{
|
||||
SpeechHue = hue;
|
||||
break;
|
||||
}
|
||||
case MessageType.Emote:
|
||||
EmoteHue = hue;
|
||||
break;
|
||||
{
|
||||
EmoteHue = hue;
|
||||
break;
|
||||
}
|
||||
case MessageType.Whisper:
|
||||
WhisperHue = hue;
|
||||
range = 1;
|
||||
break;
|
||||
{
|
||||
WhisperHue = hue;
|
||||
range = 1;
|
||||
break;
|
||||
}
|
||||
case MessageType.Yell:
|
||||
YellHue = hue;
|
||||
range = 18;
|
||||
break;
|
||||
{
|
||||
YellHue = hue;
|
||||
range = 18;
|
||||
break;
|
||||
}
|
||||
case MessageType.System:
|
||||
break;
|
||||
{
|
||||
break;
|
||||
}
|
||||
case MessageType.Label:
|
||||
break;
|
||||
{
|
||||
break;
|
||||
}
|
||||
case MessageType.Focus:
|
||||
break;
|
||||
{
|
||||
break;
|
||||
}
|
||||
case MessageType.Spell:
|
||||
break;
|
||||
{
|
||||
break;
|
||||
}
|
||||
case MessageType.Guild:
|
||||
break;
|
||||
{
|
||||
break;
|
||||
}
|
||||
case MessageType.Alliance:
|
||||
break;
|
||||
{
|
||||
break;
|
||||
}
|
||||
case MessageType.Command:
|
||||
break;
|
||||
{
|
||||
break;
|
||||
}
|
||||
case MessageType.Encoded:
|
||||
break;
|
||||
{
|
||||
break;
|
||||
}
|
||||
default:
|
||||
type = MessageType.Regular;
|
||||
break;
|
||||
{
|
||||
type = MessageType.Regular;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var regArgs = new SpeechEventArgs(this, text, type, hue, keywords);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ namespace Server.Mobiles
|
|||
{
|
||||
}
|
||||
|
||||
public override int StepsMax => 6400;
|
||||
public override string CorpseName => "an unholy corpse";
|
||||
public override bool IsDispellable => false;
|
||||
public override bool IsBondable => false;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ namespace Server.Mobiles
|
|||
{
|
||||
}
|
||||
|
||||
public override int StepsMax => 6400;
|
||||
public override string CorpseName => "a holy corpse";
|
||||
public override bool IsDispellable => false;
|
||||
public override bool IsBondable => false;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ namespace Server.Factions
|
|||
{
|
||||
}
|
||||
|
||||
public override int StepsMax => 6400;
|
||||
public override string CorpseName => "a war horse corpse";
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
|
||||
|
|
|
|||
|
|
@ -468,61 +468,4 @@ namespace Server.Factions
|
|||
Timer.StartTimer(Register);
|
||||
}
|
||||
}
|
||||
|
||||
public class VirtualMount : IMount
|
||||
{
|
||||
private readonly VirtualMountItem m_Item;
|
||||
|
||||
public VirtualMount(VirtualMountItem item) => m_Item = item;
|
||||
|
||||
Mobile IMount.Rider
|
||||
{
|
||||
get => m_Item.Rider;
|
||||
set { }
|
||||
}
|
||||
|
||||
public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class VirtualMountItem : Item, IMountItem
|
||||
{
|
||||
private VirtualMount _mount;
|
||||
|
||||
[SerializableField(0)]
|
||||
private Mobile _rider;
|
||||
|
||||
public VirtualMountItem(Mobile mob) : base(0x3EA0)
|
||||
{
|
||||
Layer = Layer.Mount;
|
||||
|
||||
Rider = mob;
|
||||
_mount = new VirtualMount(this);
|
||||
}
|
||||
|
||||
public IMount Mount => _mount;
|
||||
|
||||
[AfterDeserialization]
|
||||
private void AfterDeserialization()
|
||||
{
|
||||
if (_rider?.Deleted != false)
|
||||
{
|
||||
Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
_mount = new VirtualMount(this);
|
||||
}
|
||||
}
|
||||
|
||||
public override DeathMoveResult OnParentDeath(Mobile parent)
|
||||
{
|
||||
_mount = null;
|
||||
Delete();
|
||||
|
||||
return DeathMoveResult.RemainEquipped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
|
||||
namespace Server.Factions
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
|
||||
namespace Server.Factions
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ public static class PlayerMurderSystem
|
|||
// Only the players that are online
|
||||
private static readonly HashSet<MurderContext> _contextTerms = new(MurderContext.EqualityComparer.Default);
|
||||
|
||||
private static readonly Timer _murdererTimer = new MurdererTimer();
|
||||
|
||||
private static TimeSpan _shortTermMurderDuration;
|
||||
|
||||
private static TimeSpan _longTermMurderDuration;
|
||||
|
|
@ -41,8 +39,6 @@ public static class PlayerMurderSystem
|
|||
EventSink.Disconnected += OnDisconnected;
|
||||
EventSink.Login += OnLogin;
|
||||
EventSink.PlayerDeleted += OnPlayerDeleted;
|
||||
|
||||
_murdererTimer.Start();
|
||||
}
|
||||
|
||||
private static void OnPlayerDeleted(Mobile m)
|
||||
|
|
@ -182,6 +178,11 @@ public static class PlayerMurderSystem
|
|||
{
|
||||
}
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
new MurdererTimer().Start();
|
||||
}
|
||||
|
||||
protected override void OnTick()
|
||||
{
|
||||
if (_contextTerms.Count == 0)
|
||||
|
|
@ -208,5 +209,10 @@ public static class PlayerMurderSystem
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
~MurdererTimer()
|
||||
{
|
||||
PlayerMurderSystem.logger.Error($"{nameof(MurdererTimer)} is no longer running!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Server.Collections;
|
||||
using Server.Logging;
|
||||
using Server.Mobiles;
|
||||
|
||||
namespace Server.Engines.Virtues;
|
||||
|
|
@ -29,9 +30,9 @@ public enum VirtueName
|
|||
|
||||
public static class VirtueSystem
|
||||
{
|
||||
private static readonly Dictionary<PlayerMobile, VirtueContext> _playerVirtues = new();
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(VirtueSystem));
|
||||
|
||||
private static readonly Timer _virtueTimer = new VirtueTimer();
|
||||
private static readonly Dictionary<PlayerMobile, VirtueContext> _playerVirtues = new();
|
||||
|
||||
private static void FixVirtue(Mobile m, int[] virtueValues)
|
||||
{
|
||||
|
|
@ -66,8 +67,6 @@ public static class VirtueSystem
|
|||
FixVirtue(m, values);
|
||||
}
|
||||
}
|
||||
|
||||
_virtueTimer.Start();
|
||||
}
|
||||
|
||||
private static void Serialize(IGenericWriter writer)
|
||||
|
|
@ -346,6 +345,11 @@ public static class VirtueSystem
|
|||
{
|
||||
}
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
new VirtueTimer().Start();
|
||||
}
|
||||
|
||||
protected override void OnTick()
|
||||
{
|
||||
if (_playerVirtues.Count == 0)
|
||||
|
|
@ -371,5 +375,10 @@ public static class VirtueSystem
|
|||
_playerVirtues.Remove((PlayerMobile)queue.Dequeue());
|
||||
}
|
||||
}
|
||||
|
||||
~VirtueTimer()
|
||||
{
|
||||
VirtueSystem.logger.Error($"{nameof(VirtueTimer)} is no longer running!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"version": 3,
|
||||
"version": 4,
|
||||
"type": "Server.Mobiles.EtherealMount",
|
||||
"properties": [
|
||||
{
|
||||
"name": "IsDonationItem",
|
||||
"type": "bool",
|
||||
"usesSaveFlag": true,
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
{
|
||||
"name": "IsRewardItem",
|
||||
"type": "bool",
|
||||
"usesSaveFlag": true,
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
|
|
@ -37,7 +39,17 @@
|
|||
{
|
||||
"name": "Rider",
|
||||
"type": "Server.Mobile",
|
||||
"usesSaveFlag": true,
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "Steps",
|
||||
"type": "int",
|
||||
"usesSaveFlag": true,
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
11
Projects/UOContent/Migrations/Server.Mobiles.VirtualMountItem.v0.json
generated
Normal file
11
Projects/UOContent/Migrations/Server.Mobiles.VirtualMountItem.v0.json
generated
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Mobiles.VirtualMountItem",
|
||||
"properties": [
|
||||
{
|
||||
"name": "Rider",
|
||||
"type": "Server.Mobile",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Server.Collections;
|
||||
using Server.Logging;
|
||||
using Server.Mobiles;
|
||||
using Server.Spells.Ninjitsu;
|
||||
|
||||
|
|
@ -12,13 +17,263 @@ public enum DFAlgorithm
|
|||
|
||||
public static class StaminaSystem
|
||||
{
|
||||
public const int OverloadAllowance = 4; // We can be four stones overweight without getting fatigued
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(StaminaSystem));
|
||||
|
||||
private static TimeSpan ResetDuration = TimeSpan.FromHours(24);
|
||||
|
||||
private static readonly Dictionary<IHasSteps, StepsTaken> _stepsTaken = new();
|
||||
private static readonly OrderedHashSet<IHasSteps> _resetHash = new();
|
||||
|
||||
// TODO: This exploits single thread processing and is not thread safe!
|
||||
public static DFAlgorithm DFA { get; set; }
|
||||
|
||||
public static int StonesOverweightAllowance { get; set; }
|
||||
public static bool CannotMoveWhenFatigued { get; set; }
|
||||
public static int StonesPerOverweightLoss { get; set; }
|
||||
public static int BaseOverweightLoss { get; set; }
|
||||
public static double AdditionalLossWhenBelow { get; set; }
|
||||
public static bool EnableMountStamina { get; set; }
|
||||
public static bool UseMountStaminaWhenOverweight { get; set; }
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
CannotMoveWhenFatigued = ServerConfiguration.GetOrUpdateSetting("stamina.cannotMoveWhenFatigued", !Core.AOS);
|
||||
StonesPerOverweightLoss = ServerConfiguration.GetOrUpdateSetting("stamina.stonesPerOverweightLoss", 25);
|
||||
StonesOverweightAllowance = ServerConfiguration.GetOrUpdateSetting("stamina.stonesOverweightAllowance", 4);
|
||||
BaseOverweightLoss = ServerConfiguration.GetOrUpdateSetting("stamina.baseOverweightLoss", 5);
|
||||
AdditionalLossWhenBelow = ServerConfiguration.GetOrUpdateSetting("stamina.additionalLossWhenBelow", 0.10);
|
||||
EnableMountStamina = ServerConfiguration.GetOrUpdateSetting("stamina.enableMountStamina", true);
|
||||
UseMountStaminaWhenOverweight = ServerConfiguration.GetSetting("stamina.useStamina", Core.SA);
|
||||
|
||||
GenericPersistence.Register("StaminaSystem", Serialize, Deserialize);
|
||||
}
|
||||
|
||||
private static void Serialize(IGenericWriter writer)
|
||||
{
|
||||
writer.WriteEncodedInt(0); // version
|
||||
|
||||
writer.WriteEncodedInt(_stepsTaken.Count);
|
||||
foreach (var (m, stepsTaken) in _stepsTaken)
|
||||
{
|
||||
writer.Write(m as ISerializable); // To serialize all IHasSteps must be an ISerializable
|
||||
stepsTaken.Serialize(writer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Deserialize(IGenericReader reader)
|
||||
{
|
||||
var version = reader.ReadEncodedInt();
|
||||
|
||||
var count = reader.ReadEncodedInt();
|
||||
_stepsTaken.EnsureCapacity(count);
|
||||
|
||||
var now = Core.Now;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var m = reader.ReadEntity<ISerializable>() as IHasSteps;
|
||||
var stepsTaken = new StepsTaken();
|
||||
stepsTaken.Deserialize(reader);
|
||||
|
||||
if (m == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RegenSteps(m, ref stepsTaken, false);
|
||||
|
||||
if (stepsTaken.Steps > 0)
|
||||
{
|
||||
_stepsTaken.Add(m, stepsTaken);
|
||||
|
||||
if (m is IMount && now < stepsTaken.IdleStartTime + ResetDuration)
|
||||
{
|
||||
_resetHash.Add(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
EventSink.Movement += EventSink_Movement;
|
||||
EventSink.Login += Login;
|
||||
EventSink.Logout += Logout;
|
||||
EventSink.PlayerDeleted += OnPlayerDeleted;
|
||||
|
||||
// Credit idle time
|
||||
using var queue = PooledRefQueue<IHasSteps>.Create();
|
||||
foreach (var m in _stepsTaken.Keys)
|
||||
{
|
||||
ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, m);
|
||||
if (!Unsafe.IsNullRef(ref stepsTaken))
|
||||
{
|
||||
RegenSteps(m, ref stepsTaken, false);
|
||||
if (stepsTaken.Steps <= 0)
|
||||
{
|
||||
queue.Enqueue(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
_stepsTaken.Remove(queue.Dequeue());
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnPlayerDeleted(Mobile m)
|
||||
{
|
||||
RemoveEntry(m as IHasSteps);
|
||||
}
|
||||
|
||||
private static void Login(Mobile m)
|
||||
{
|
||||
if (EnableMountStamina)
|
||||
{
|
||||
// Start idle for mount
|
||||
ref var stepsTaken = ref GetMountStepsTaken(m.Mount, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
if (stepsTaken.Steps <= 0 || Core.Now >= stepsTaken.IdleStartTime + ResetDuration)
|
||||
{
|
||||
_stepsTaken.Remove(m.Mount);
|
||||
}
|
||||
else
|
||||
{
|
||||
stepsTaken.IdleStartTime = Core.Now;
|
||||
}
|
||||
}
|
||||
|
||||
_resetHash.Remove(m.Mount);
|
||||
}
|
||||
|
||||
if (m is PlayerMobile pm)
|
||||
{
|
||||
ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, pm);
|
||||
if (!Unsafe.IsNullRef(ref stepsTaken) && RegenSteps(pm, ref stepsTaken))
|
||||
{
|
||||
stepsTaken.IdleStartTime = Core.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Logout(Mobile m)
|
||||
{
|
||||
if (_stepsTaken == null || _stepsTaken.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EnableMountStamina)
|
||||
{
|
||||
// Regain mount idle time
|
||||
ref var stepsTaken = ref GetMountStepsTaken(m.Mount, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
if (RegenSteps(m.Mount, ref stepsTaken))
|
||||
{
|
||||
stepsTaken.IdleStartTime = Core.Now;
|
||||
_resetHash.Add(m.Mount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m is PlayerMobile pm)
|
||||
{
|
||||
ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, pm);
|
||||
|
||||
if (!Unsafe.IsNullRef(ref stepsTaken) && RegenSteps(pm, ref stepsTaken))
|
||||
{
|
||||
stepsTaken.IdleStartTime = Core.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveEntry(IHasSteps m)
|
||||
{
|
||||
if (m != null)
|
||||
{
|
||||
_stepsTaken.Remove(m);
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnDismount(IHasSteps mount)
|
||||
{
|
||||
if (!EnableMountStamina)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ref var stepsTaken = ref GetMountStepsTaken(mount, out var exists);
|
||||
if (exists && RegenSteps(mount, ref stepsTaken))
|
||||
{
|
||||
_resetHash.Add(mount);
|
||||
return;
|
||||
}
|
||||
|
||||
_resetHash.Remove(mount);
|
||||
}
|
||||
|
||||
private static ref StepsTaken GetMountStepsTaken(IHasSteps m, out bool exists)
|
||||
{
|
||||
if (m == null)
|
||||
{
|
||||
exists = false;
|
||||
return ref Unsafe.NullRef<StepsTaken>();
|
||||
}
|
||||
|
||||
ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrNullRef(_stepsTaken, m);
|
||||
exists = !Unsafe.IsNullRef(ref stepsTaken);
|
||||
return ref stepsTaken;
|
||||
}
|
||||
|
||||
private static ref StepsTaken GetOrCreateStepsTaken(IHasSteps m, out bool created)
|
||||
{
|
||||
ref var stepsTaken = ref CollectionsMarshal.GetValueRefOrAddDefault(_stepsTaken, m, out var exists);
|
||||
created = !exists;
|
||||
|
||||
return ref stepsTaken;
|
||||
}
|
||||
|
||||
public static void RegenSteps(IHasSteps m, int amount, bool removeOnInvalidation = true)
|
||||
{
|
||||
ref var stepsTaken = ref GetMountStepsTaken(m, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
RegenSteps(m, ref stepsTaken, removeOnInvalidation);
|
||||
}
|
||||
}
|
||||
|
||||
// Triggered on logout, dismount, and world load
|
||||
private static bool RegenSteps(IHasSteps m, ref StepsTaken stepsTaken, bool removeOnInvalidation = true)
|
||||
{
|
||||
var stepsGained = (int)((Core.Now - stepsTaken.IdleStartTime) / m.IdleTimePerStepsGain * m.StepsGainedPerIdleTime);
|
||||
return RegenSteps(m, stepsGained, ref stepsTaken, removeOnInvalidation);
|
||||
}
|
||||
|
||||
private static bool RegenSteps(IHasSteps m, int amount, ref StepsTaken stepsTaken, bool removeOnInvalidation = true)
|
||||
{
|
||||
if (m == null || Unsafe.IsNullRef(ref stepsTaken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stepsTaken.Steps -= amount;
|
||||
|
||||
if (stepsTaken.Steps <= 0)
|
||||
{
|
||||
if (removeOnInvalidation)
|
||||
{
|
||||
_stepsTaken.Remove(m);
|
||||
stepsTaken = ref Unsafe.NullRef<StepsTaken>();
|
||||
return false;
|
||||
}
|
||||
|
||||
stepsTaken.Steps = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void FatigueOnDamage(Mobile m, int damage)
|
||||
|
|
@ -42,63 +297,135 @@ public static class StaminaSystem
|
|||
{
|
||||
var from = e.Mobile;
|
||||
|
||||
if (!from.Alive || from.AccessLevel > AccessLevel.Player)
|
||||
if (!from.Player || !from.Alive || from.AccessLevel > AccessLevel.Player)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!from.Player)
|
||||
var maxWeight = GetMaxWeight(from) + StonesOverweightAllowance;
|
||||
var overweight = Mobile.BodyWeight + from.TotalWeight - maxWeight;
|
||||
|
||||
if (EnableMountStamina && from.Mount != null)
|
||||
{
|
||||
// Else it won't work on monsters.
|
||||
DeathStrike.AddStep(from);
|
||||
return;
|
||||
ProcessMountMovement(from.Mount, overweight, e);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessPlayerMovement(overweight, e);
|
||||
}
|
||||
|
||||
var maxWeight = GetMaxWeight(from) + OverloadAllowance;
|
||||
var overWeight = Mobile.BodyWeight + from.TotalWeight - maxWeight;
|
||||
DeathStrike.AddStep(from);
|
||||
}
|
||||
|
||||
if (overWeight > 0)
|
||||
private static void ProcessPlayerMovement(int overweight, MovementEventArgs e)
|
||||
{
|
||||
var from = e.Mobile;
|
||||
var running = (e.Direction & Direction.Running) != 0;
|
||||
|
||||
if (overweight > 0)
|
||||
{
|
||||
from.Stam -= GetStamLoss(from, overWeight, (e.Direction & Direction.Running) != 0);
|
||||
var stamLoss = GetStamLoss(from, overweight, running);
|
||||
|
||||
from.Stam -= stamLoss;
|
||||
|
||||
if (from.Stam == 0)
|
||||
{
|
||||
from.SendLocalizedMessage(
|
||||
500109
|
||||
); // You are too fatigued to move, because you are carrying too much weight!
|
||||
// You are too fatigued to move, because you are carrying too much weight!
|
||||
from.SendLocalizedMessage(500109);
|
||||
e.Blocked = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (from.Stam * 100 / Math.Max(from.StamMax, 1) < 10)
|
||||
if (AdditionalLossWhenBelow > 0 && from.Stam / Math.Max(from.StamMax, 1.0) < AdditionalLossWhenBelow)
|
||||
{
|
||||
--from.Stam;
|
||||
}
|
||||
|
||||
if (!Core.AOS && from.Stam == 0)
|
||||
if (CannotMoveWhenFatigued && from.Stam == 0)
|
||||
{
|
||||
from.SendLocalizedMessage(500110); // You are too fatigued to move.
|
||||
e.Blocked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (from is PlayerMobile pm)
|
||||
if (running && from is PlayerMobile pm)
|
||||
{
|
||||
var amt = pm.Mounted ? 48 : 16;
|
||||
ref StepsTaken stepsTaken = ref GetOrCreateStepsTaken(pm, out var created);
|
||||
if (!created)
|
||||
{
|
||||
RegenSteps(pm, ref stepsTaken, false);
|
||||
}
|
||||
|
||||
if (++pm.StepsTaken % amt == 0)
|
||||
var steps = ++stepsTaken.Steps;
|
||||
|
||||
// 3x steps if mounted is used when EnableMountStamina is false
|
||||
var maxSteps = pm.StepsMax * (from.Mount != null ? 3 : 1);
|
||||
|
||||
if (steps > maxSteps)
|
||||
{
|
||||
--pm.Stam;
|
||||
stepsTaken.Steps = 0;
|
||||
}
|
||||
|
||||
stepsTaken.IdleStartTime = Core.Now;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessMountMovement(IMount mount, int overweight, MovementEventArgs e)
|
||||
{
|
||||
var from = e.Mobile;
|
||||
|
||||
var running = (e.Direction & Direction.Running) != 0;
|
||||
var stamLoss = overweight > 0 ? GetStamLoss(from, overweight, running) : 0;
|
||||
|
||||
ref var stepsTaken = ref GetOrCreateStepsTaken(mount, out var created);
|
||||
|
||||
// Gain any idle steps
|
||||
if (!created)
|
||||
{
|
||||
RegenSteps(mount, ref stepsTaken, false); // Don't delete the entry if it's reset
|
||||
}
|
||||
|
||||
if (mount is Mobile m && AdditionalLossWhenBelow > 0 && m.Stam / Math.Max(m.StamMax, 1.0) < AdditionalLossWhenBelow)
|
||||
{
|
||||
stamLoss++;
|
||||
}
|
||||
|
||||
var maxSteps = mount.StepsMax;
|
||||
|
||||
if (stepsTaken.Steps <= maxSteps)
|
||||
{
|
||||
// Pre-SA mounts would lose stamina while running even when they were not overweight
|
||||
if (running && !UseMountStaminaWhenOverweight)
|
||||
{
|
||||
stamLoss++;
|
||||
}
|
||||
|
||||
if (stamLoss > 0)
|
||||
{
|
||||
stepsTaken.Steps += stamLoss;
|
||||
stepsTaken.IdleStartTime = Core.Now;
|
||||
|
||||
// This only executes when mounted, so we have the player say it since the actual mount is internalized
|
||||
if ((mount as BaseCreature)?.Debug == true && stepsTaken.Steps % 20 == 0)
|
||||
{
|
||||
from.PublicOverheadMessage(MessageType.Regular, 41, false, $"Steps {stepsTaken.Steps}/{mount.StepsMax}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeathStrike.AddStep(from);
|
||||
if (stepsTaken.Steps > maxSteps)
|
||||
{
|
||||
stepsTaken.Steps = maxSteps;
|
||||
from.SendLocalizedMessage(500108); // Your mount is too fatigued to move.
|
||||
e.Blocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetStamLoss(Mobile from, int overWeight, bool running)
|
||||
{
|
||||
var loss = 5 + overWeight / 25;
|
||||
var loss = BaseOverweightLoss + overWeight / StonesPerOverweightLoss;
|
||||
|
||||
if (from.Mounted)
|
||||
{
|
||||
|
|
@ -120,6 +447,81 @@ public static class StaminaSystem
|
|||
return false;
|
||||
}
|
||||
|
||||
return Mobile.BodyWeight + m.TotalWeight > GetMaxWeight(m) + OverloadAllowance;
|
||||
return Mobile.BodyWeight + m.TotalWeight > GetMaxWeight(m) + StonesOverweightAllowance;
|
||||
}
|
||||
|
||||
private struct StepsTaken
|
||||
{
|
||||
public int Steps;
|
||||
public DateTime IdleStartTime;
|
||||
|
||||
public void Serialize(IGenericWriter writer)
|
||||
{
|
||||
writer.WriteEncodedInt(0); // version
|
||||
|
||||
writer.WriteEncodedInt(Steps);
|
||||
writer.WriteDeltaTime(IdleStartTime);
|
||||
}
|
||||
|
||||
public void Deserialize(IGenericReader reader)
|
||||
{
|
||||
reader.ReadEncodedInt(); // version
|
||||
|
||||
Steps = reader.ReadEncodedInt();
|
||||
IdleStartTime = reader.ReadDeltaTime();
|
||||
}
|
||||
}
|
||||
|
||||
private class ResetTimer : Timer
|
||||
{
|
||||
private static TimeSpan CheckDuration = TimeSpan.FromHours(1);
|
||||
|
||||
private DateTime _nextCheck;
|
||||
|
||||
public ResetTimer() : base(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)) => _nextCheck = Core.Now;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
new ResetTimer().Start();
|
||||
}
|
||||
|
||||
~ResetTimer()
|
||||
{
|
||||
StaminaSystem.logger.Error($"{nameof(ResetTimer)} is no longer running!");
|
||||
}
|
||||
|
||||
protected override void OnTick()
|
||||
{
|
||||
if (Core.Now < _nextCheck)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var queue = PooledRefQueue<IHasSteps>.Create();
|
||||
|
||||
ref StepsTaken stepsTaken = ref Unsafe.NullRef<StepsTaken>();
|
||||
foreach (var m in _resetHash)
|
||||
{
|
||||
stepsTaken = ref GetMountStepsTaken(m, out var exists);
|
||||
if (!exists || Core.Now >= stepsTaken.IdleStartTime + ResetDuration)
|
||||
{
|
||||
queue.Enqueue(m);
|
||||
}
|
||||
}
|
||||
|
||||
if (_resetHash.Count == queue.Count)
|
||||
{
|
||||
_resetHash.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
_resetHash.Remove(queue.Dequeue());
|
||||
}
|
||||
}
|
||||
|
||||
_nextCheck = Core.Now + CheckDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,343 +1,352 @@
|
|||
using ModernUO.Serialization;
|
||||
using System;
|
||||
using Server.Items;
|
||||
using Server.Misc;
|
||||
using Server.Multis;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Mobiles
|
||||
namespace Server.Mobiles;
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public abstract partial class BaseMount : BaseCreature, IMount
|
||||
{
|
||||
[SerializationGenerator(0, false)]
|
||||
public abstract partial class BaseMount : BaseCreature, IMount
|
||||
public BaseMount(
|
||||
int bodyID,
|
||||
int itemID,
|
||||
AIType aiType,
|
||||
FightMode fightMode = FightMode.Closest,
|
||||
int rangePerception = 10,
|
||||
int rangeFight = 1
|
||||
) : base(aiType, fightMode, rangePerception, rangeFight)
|
||||
{
|
||||
public BaseMount(
|
||||
int bodyID, int itemID, AIType aiType, FightMode fightMode = FightMode.Closest,
|
||||
int rangePerception = 10, int rangeFight = 1
|
||||
) : base(aiType, fightMode, rangePerception, rangeFight)
|
||||
Body = bodyID;
|
||||
|
||||
InternalItem = new MountItem(this, itemID);
|
||||
}
|
||||
|
||||
public virtual TimeSpan MountAbilityDelay => TimeSpan.Zero;
|
||||
|
||||
[SerializableField(0)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
public DateTime _nextMountAbility;
|
||||
|
||||
[SerializableField(2)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
protected Item _internalItem;
|
||||
|
||||
public virtual bool AllowMaleRider => true;
|
||||
public virtual bool AllowFemaleRider => true;
|
||||
|
||||
// Stamina System - 1 step per 10 seconds and 3840 steps max = 9.667 hours
|
||||
public virtual int StepsMax => 3840;
|
||||
public virtual int StepsGainedPerIdleTime => 1;
|
||||
public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10);
|
||||
|
||||
[Hue]
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public override int Hue
|
||||
{
|
||||
get => base.Hue;
|
||||
set
|
||||
{
|
||||
Body = bodyID;
|
||||
base.Hue = value;
|
||||
|
||||
InternalItem = new MountItem(this, itemID);
|
||||
}
|
||||
|
||||
public virtual TimeSpan MountAbilityDelay => TimeSpan.Zero;
|
||||
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
[SerializableField(0)]
|
||||
public DateTime _nextMountAbility;
|
||||
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
[SerializableField(2)]
|
||||
protected Item _internalItem;
|
||||
|
||||
public virtual bool AllowMaleRider => true;
|
||||
public virtual bool AllowFemaleRider => true;
|
||||
|
||||
[Hue]
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public override int Hue
|
||||
{
|
||||
get => base.Hue;
|
||||
set
|
||||
if (InternalItem != null)
|
||||
{
|
||||
base.Hue = value;
|
||||
|
||||
if (InternalItem != null)
|
||||
{
|
||||
InternalItem.Hue = value;
|
||||
}
|
||||
InternalItem.Hue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int ItemID
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int ItemID
|
||||
{
|
||||
get => InternalItem?.ItemID ?? 0;
|
||||
set
|
||||
{
|
||||
get => InternalItem?.ItemID ?? 0;
|
||||
set
|
||||
if (InternalItem != null)
|
||||
{
|
||||
if (InternalItem != null)
|
||||
{
|
||||
InternalItem.ItemID = value;
|
||||
}
|
||||
InternalItem.ItemID = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
[SerializableProperty(1)]
|
||||
public Mobile Rider
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
[SerializableProperty(1)]
|
||||
public Mobile Rider
|
||||
{
|
||||
get => _rider;
|
||||
set
|
||||
{
|
||||
get => _rider;
|
||||
set
|
||||
{
|
||||
if (_rider != value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
var loc = _rider.Location;
|
||||
var map = _rider.Map;
|
||||
|
||||
if (map == null || map == Map.Internal)
|
||||
{
|
||||
loc = _rider.LogoutLocation;
|
||||
map = _rider.LogoutMap;
|
||||
}
|
||||
|
||||
Direction = _rider.Direction;
|
||||
Location = loc;
|
||||
Map = map;
|
||||
|
||||
InternalItem?.Internalize();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_rider != null)
|
||||
{
|
||||
Dismount(_rider);
|
||||
}
|
||||
|
||||
Dismount(value);
|
||||
|
||||
if (InternalItem != null)
|
||||
{
|
||||
value.AddItem(InternalItem);
|
||||
}
|
||||
|
||||
value.Direction = Direction;
|
||||
|
||||
Internalize();
|
||||
|
||||
if (value.Target is Bola.BolaTarget)
|
||||
{
|
||||
Target.Cancel(value);
|
||||
}
|
||||
}
|
||||
|
||||
_rider = value;
|
||||
this.MarkDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill)
|
||||
{
|
||||
if (_rider == null)
|
||||
if (_rider == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var attacker = from ?? _rider.FindMostRecentDamager(true);
|
||||
|
||||
if (!(attacker == this || attacker == _rider || willKill || Core.Now < NextMountAbility)
|
||||
&& DoMountAbility(amount, from))
|
||||
if (value == null)
|
||||
{
|
||||
NextMountAbility = Core.Now + MountAbilityDelay;
|
||||
}
|
||||
}
|
||||
var loc = _rider.Location;
|
||||
var map = _rider.Map;
|
||||
|
||||
public override bool OnBeforeDeath()
|
||||
{
|
||||
Rider = null;
|
||||
return base.OnBeforeDeath();
|
||||
}
|
||||
|
||||
public override void OnAfterDelete()
|
||||
{
|
||||
InternalItem?.Delete();
|
||||
InternalItem = null;
|
||||
|
||||
base.OnAfterDelete();
|
||||
}
|
||||
|
||||
public override void OnDelete()
|
||||
{
|
||||
Rider = null;
|
||||
|
||||
base.OnDelete();
|
||||
}
|
||||
|
||||
[AfterDeserialization(false)]
|
||||
private void AfterDeserialize()
|
||||
{
|
||||
if (InternalItem == null)
|
||||
{
|
||||
Delete();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnDisallowedRider(Mobile m)
|
||||
{
|
||||
m.SendMessage("You may not ride this creature.");
|
||||
}
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (IsDeadPet)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.IsBodyMod && !from.Body.IsHuman)
|
||||
{
|
||||
if (Core.AOS) // You cannot ride a mount in your current form.
|
||||
if (map == null || map == Map.Internal)
|
||||
{
|
||||
PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1062061, from.NetState);
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(1061628); // You can't do that while polymorphed.
|
||||
loc = _rider.LogoutLocation;
|
||||
map = _rider.LogoutMap;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
Direction = _rider.Direction;
|
||||
Location = loc;
|
||||
Map = map;
|
||||
|
||||
if (!CheckMountAllowed(from))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.Mounted)
|
||||
{
|
||||
from.SendLocalizedMessage(1005583); // Please dismount first.
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.Female ? !AllowFemaleRider : !AllowMaleRider)
|
||||
{
|
||||
OnDisallowedRider(from);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DesignContext.Check(from))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.HasTrade)
|
||||
{
|
||||
from.SendLocalizedMessage(1042317, "", 0x41); // You may not ride at this time
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.InRange(this, 1))
|
||||
{
|
||||
var canAccess = from.AccessLevel >= AccessLevel.GameMaster
|
||||
|| Controlled && ControlMaster == from
|
||||
|| Summoned && SummonMaster == from;
|
||||
|
||||
if (canAccess)
|
||||
{
|
||||
if (Poisoned)
|
||||
{
|
||||
PrivateOverheadMessage(
|
||||
MessageType.Regular,
|
||||
0x3B2,
|
||||
1049692,
|
||||
from.NetState
|
||||
); // This mount is too ill to ride.
|
||||
}
|
||||
else
|
||||
{
|
||||
Rider = from;
|
||||
}
|
||||
}
|
||||
else if (!Controlled && !Summoned)
|
||||
{
|
||||
// That mount does not look broken! You would have to tame it to ride it.
|
||||
PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501263, from.NetState);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This isn't your mount; it refuses to let you ride.
|
||||
PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501264, from.NetState);
|
||||
}
|
||||
InternalItem?.Internalize();
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(500206); // That is too far away to ride.
|
||||
}
|
||||
}
|
||||
|
||||
public static void Dismount(Mobile m)
|
||||
{
|
||||
var mount = m.Mount;
|
||||
|
||||
if (mount != null)
|
||||
{
|
||||
mount.Rider = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 1040024 You are still too dazed from being knocked off your mount to ride!
|
||||
// 1062910 You cannot mount while recovering from a bola throw.
|
||||
// 1070859 You cannot mount while recovering from a dismount special maneuver.
|
||||
|
||||
public static bool CheckMountAllowed(Mobile mob)
|
||||
{
|
||||
var result = true;
|
||||
|
||||
if (mob is PlayerMobile mobile)
|
||||
{
|
||||
if (mobile.MountBlockReason != BlockMountType.None)
|
||||
if (_rider != null)
|
||||
{
|
||||
mobile.SendLocalizedMessage((int)mobile.MountBlockReason);
|
||||
result = false;
|
||||
Dismount(_rider);
|
||||
}
|
||||
|
||||
if (mobile.Race == Race.Gargoyle)
|
||||
Dismount(value);
|
||||
|
||||
if (InternalItem != null)
|
||||
{
|
||||
mobile.PrivateOverheadMessage(MessageType.Regular, mobile.SpeechHue, 1112281, mobile.NetState); // Gargoyles are unable to ride animals.
|
||||
result = false;
|
||||
value.AddItem(InternalItem);
|
||||
}
|
||||
|
||||
value.Direction = Direction;
|
||||
|
||||
Internalize();
|
||||
|
||||
if (value.Target is Bola.BolaTarget)
|
||||
{
|
||||
Target.Cancel(value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
_rider = value;
|
||||
|
||||
public virtual bool DoMountAbility(int damage, Mobile attacker) => false;
|
||||
if (value == null)
|
||||
{
|
||||
StaminaSystem.OnDismount(this);
|
||||
}
|
||||
|
||||
this.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class MountItem : Item, IMountItem
|
||||
public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill)
|
||||
{
|
||||
private BaseMount _mount;
|
||||
|
||||
public MountItem(BaseMount mount, int itemID) : base(itemID)
|
||||
if (_rider == null)
|
||||
{
|
||||
Layer = Layer.Mount;
|
||||
Movable = false;
|
||||
|
||||
_mount = mount;
|
||||
return;
|
||||
}
|
||||
|
||||
public override double DefaultWeight => 0;
|
||||
var attacker = from ?? _rider.FindMostRecentDamager(true);
|
||||
|
||||
[SerializableProperty(0, useField: nameof(_mount))]
|
||||
public IMount Mount => _mount;
|
||||
|
||||
public override void OnAfterDelete()
|
||||
if (!(attacker == this || attacker == _rider || willKill || Core.Now < NextMountAbility)
|
||||
&& DoMountAbility(amount, from))
|
||||
{
|
||||
_mount?.Delete();
|
||||
_mount = null;
|
||||
NextMountAbility = Core.Now + MountAbilityDelay;
|
||||
}
|
||||
}
|
||||
|
||||
base.OnAfterDelete();
|
||||
public override bool OnBeforeDeath()
|
||||
{
|
||||
Rider = null;
|
||||
return base.OnBeforeDeath();
|
||||
}
|
||||
|
||||
public override void OnAfterDelete()
|
||||
{
|
||||
InternalItem?.Delete();
|
||||
InternalItem = null;
|
||||
|
||||
base.OnAfterDelete();
|
||||
}
|
||||
|
||||
public override void OnDelete()
|
||||
{
|
||||
Rider = null;
|
||||
|
||||
base.OnDelete();
|
||||
}
|
||||
|
||||
[AfterDeserialization(false)]
|
||||
private void AfterDeserialize()
|
||||
{
|
||||
if (InternalItem == null)
|
||||
{
|
||||
Delete();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnDisallowedRider(Mobile m)
|
||||
{
|
||||
m.SendLocalizedMessage(1042317); // You may not ride at this time
|
||||
}
|
||||
|
||||
public override void OnDoubleClick(Mobile from)
|
||||
{
|
||||
if (IsDeadPet)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public override DeathMoveResult OnParentDeath(Mobile parent)
|
||||
if (from.IsBodyMod && !from.Body.IsHuman)
|
||||
{
|
||||
if (_mount != null)
|
||||
if (Core.AOS) // You cannot ride a mount in your current form.
|
||||
{
|
||||
_mount.Rider = null;
|
||||
PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1062061, from.NetState);
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(1061628); // You can't do that while polymorphed.
|
||||
}
|
||||
|
||||
return DeathMoveResult.RemainEquipped;
|
||||
return;
|
||||
}
|
||||
|
||||
[AfterDeserialization(false)]
|
||||
private void AfterDeserialize()
|
||||
if (!CheckMountAllowed(from))
|
||||
{
|
||||
if (_mount == null)
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.Mounted)
|
||||
{
|
||||
from.SendLocalizedMessage(1005583); // Please dismount first.
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.Female ? !AllowFemaleRider : !AllowMaleRider)
|
||||
{
|
||||
OnDisallowedRider(from);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DesignContext.Check(from))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.HasTrade)
|
||||
{
|
||||
from.SendLocalizedMessage(1042317, "", 0x41); // You may not ride at this time
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.InRange(this, 1))
|
||||
{
|
||||
var canAccess = from.AccessLevel >= AccessLevel.GameMaster
|
||||
|| Controlled && ControlMaster == from
|
||||
|| Summoned && SummonMaster == from;
|
||||
|
||||
if (canAccess)
|
||||
{
|
||||
Delete();
|
||||
if (Poisoned)
|
||||
{
|
||||
// This mount is too ill to ride.
|
||||
PrivateOverheadMessage(MessageType.Regular,0x3B2,1049692,from.NetState);
|
||||
}
|
||||
else
|
||||
{
|
||||
Rider = from;
|
||||
}
|
||||
}
|
||||
else if (!Controlled && !Summoned)
|
||||
{
|
||||
// That mount does not look broken! You would have to tame it to ride it.
|
||||
PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501263, from.NetState);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This isn't your mount; it refuses to let you ride.
|
||||
PrivateOverheadMessage(MessageType.Regular, 0x3B2, 501264, from.NetState);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(500206); // That is too far away to ride.
|
||||
}
|
||||
}
|
||||
|
||||
public static void Dismount(Mobile m)
|
||||
{
|
||||
var mount = m.Mount;
|
||||
|
||||
if (mount != null)
|
||||
{
|
||||
mount.Rider = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool CheckMountAllowed(Mobile mob)
|
||||
{
|
||||
var result = true;
|
||||
|
||||
if (mob is PlayerMobile mobile)
|
||||
{
|
||||
if (mobile.MountBlockReason != BlockMountType.None)
|
||||
{
|
||||
mobile.SendLocalizedMessage((int)mobile.MountBlockReason);
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (mobile.Race == Race.Gargoyle)
|
||||
{
|
||||
mobile.PrivateOverheadMessage(MessageType.Regular, mobile.SpeechHue, 1112281, mobile.NetState); // Gargoyles are unable to ride animals.
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public virtual bool DoMountAbility(int damage, Mobile attacker) => false;
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class MountItem : Item, IMountItem
|
||||
{
|
||||
private BaseMount _mount;
|
||||
|
||||
public MountItem(BaseMount mount, int itemID) : base(itemID)
|
||||
{
|
||||
Layer = Layer.Mount;
|
||||
Movable = false;
|
||||
|
||||
_mount = mount;
|
||||
}
|
||||
|
||||
public override double DefaultWeight => 0;
|
||||
|
||||
[SerializableProperty(0, useField: nameof(_mount))]
|
||||
public IMount Mount => _mount;
|
||||
|
||||
public override void OnAfterDelete()
|
||||
{
|
||||
_mount?.Delete();
|
||||
_mount = null;
|
||||
|
||||
base.OnAfterDelete();
|
||||
}
|
||||
|
||||
public override DeathMoveResult OnParentDeath(Mobile parent)
|
||||
{
|
||||
if (_mount != null)
|
||||
{
|
||||
_mount.Rider = null;
|
||||
}
|
||||
|
||||
return DeathMoveResult.RemainEquipped;
|
||||
}
|
||||
|
||||
[AfterDeserialization(false)]
|
||||
private void AfterDeserialize()
|
||||
{
|
||||
if (_mount == null)
|
||||
{
|
||||
Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ namespace Server.Mobiles
|
|||
AddItem(pack);
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "a giant beetle corpse";
|
||||
public virtual double BoostedSpeed => 0.1;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,31 @@
|
|||
using ModernUO.Serialization;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Engines.VeteranRewards;
|
||||
using Server.Multis;
|
||||
using Server.Spells;
|
||||
|
||||
namespace Server.Mobiles
|
||||
{
|
||||
[SerializationGenerator(3, false)]
|
||||
[SerializationGenerator(4, false)]
|
||||
public partial class EtherealMount : Item, IMount, IMountItem, IRewardItem
|
||||
{
|
||||
[SerializableField(0)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
|
||||
public bool _isDonationItem;
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[SerializableFieldSaveFlag(0)]
|
||||
public bool ShouldSerializeIsDonationItem() => _isDonationItem;
|
||||
|
||||
[SerializableField(1)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
public bool _isRewardItem;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[SerializableFieldSaveFlag(1)]
|
||||
public bool ShouldSerializeIsRewardItem() => _isRewardItem;
|
||||
|
||||
[Constructible]
|
||||
public EtherealMount(int itemID, int mountID) : base(itemID)
|
||||
{
|
||||
|
|
@ -115,6 +124,29 @@ namespace Server.Mobiles
|
|||
}
|
||||
}
|
||||
|
||||
[SerializableFieldSaveFlag(4)]
|
||||
private bool ShouldSerializeRider() => _rider != null;
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
[SerializableProperty(5)]
|
||||
public int Steps
|
||||
{
|
||||
get => _steps;
|
||||
set
|
||||
{
|
||||
_steps = Math.Clamp(value, 0, StepsMax);
|
||||
this.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
[SerializableFieldSaveFlag(5)]
|
||||
private bool ShouldSerializeSteps() => _steps != StepsMax;
|
||||
|
||||
public virtual int StepsMax => 3840; // Should be same as horse
|
||||
|
||||
public virtual int StepsGainedPerIdleTime => 1;
|
||||
public virtual TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10);
|
||||
|
||||
public void OnRiderDamaged(int amount, Mobile from, bool willKill)
|
||||
{
|
||||
}
|
||||
|
|
@ -215,10 +247,8 @@ namespace Server.Mobiles
|
|||
_mountedID = reader.ReadInt();
|
||||
_regularID = reader.ReadInt();
|
||||
_rider = reader.ReadEntity<Mobile>();
|
||||
if (_mountedID == 0x3EA2)
|
||||
{
|
||||
_mountedID = 0x3EAA;
|
||||
}
|
||||
|
||||
_steps = StepsMax;
|
||||
}
|
||||
|
||||
[AfterDeserialization]
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ namespace Server.Mobiles
|
|||
MinTameSkill = 77.1;
|
||||
}
|
||||
|
||||
public override int StepsMax => 5120;
|
||||
public override string CorpseName => "an ostard corpse";
|
||||
|
||||
public override int Meat => 3;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Server.Mobiles
|
|||
SetStats(this);
|
||||
}
|
||||
|
||||
public override int StepsMax => 5120;
|
||||
public override string CorpseName => "a hellsteed corpse";
|
||||
public override Poison PoisonImmune => Poison.Lethal;
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ namespace Server.Mobiles
|
|||
}
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "a hiryu corpse";
|
||||
public override double WeaponAbilityChance => 0.07; /* 1 in 15 chance of using per landed hit */
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ namespace Server.Mobiles
|
|||
MinTameSkill = 95.1;
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "a ki-rin corpse";
|
||||
public override bool AllowFemaleRider => false;
|
||||
public override bool AllowFemaleTamer => false;
|
||||
|
|
@ -75,12 +76,12 @@ namespace Server.Mobiles
|
|||
return false;
|
||||
}
|
||||
|
||||
// Range and map checked here instead of other base fuction because of abiliites that don't need to check this
|
||||
// Range and map checked here instead of other base function because of abilities that don't need to check this
|
||||
if (Rider.Hits - damage < 30 && Rider.Map == attacker.Map && Rider.InRange(attacker, 18))
|
||||
{
|
||||
attacker.BoltEffect(0);
|
||||
// 35~100 damage, unresistable, by the Ki-rin.
|
||||
// Don't inform mount about this damage, Still unsure wether or not it's flagged as the mount doing damage or the player.
|
||||
// Don't inform mount about this damage, Still unsure whether or not it's flagged as the mount doing damage or the player.
|
||||
// If changed to player, without the extra bool it'd be an infinite loop
|
||||
attacker.Damage(Utility.RandomMinMax(35, 100), this, false);
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ namespace Server.Mobiles
|
|||
}
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "a hiryu corpse";
|
||||
public override double WeaponAbilityChance => 0.07; /* 1 in 15 chance of using; 1 in 5 chance of success */
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ namespace Server.Mobiles
|
|||
PackItem(new SulfurousAsh(Utility.RandomMinMax(3, 5)));
|
||||
}
|
||||
|
||||
public override int StepsMax => 6400;
|
||||
public override string CorpseName => "a nightmare corpse";
|
||||
public override int Meat => 5;
|
||||
public override int Hides => 10;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ namespace Server.Mobiles
|
|||
MinTameSkill = 29.1;
|
||||
}
|
||||
|
||||
public override int StepsMax => 2560;
|
||||
public override string CorpseName => "a llama corpse";
|
||||
|
||||
public override int Meat => 1;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ namespace Server.Mobiles
|
|||
MinTameSkill = 83.1;
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "a ridgeback corpse";
|
||||
|
||||
public override int Meat => 1;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ namespace Server.Mobiles
|
|||
MinTameSkill = 83.1;
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "a savage ridgeback corpse";
|
||||
|
||||
public override int Meat => 1;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ namespace Server.Mobiles
|
|||
MinTameSkill = 93.9;
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "a swamp dragon corpse";
|
||||
|
||||
public override bool AutoDispel => !Controlled;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ namespace Server.Mobiles
|
|||
MinTameSkill = 103.1;
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "a silver steed corpse";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ namespace Server.Mobiles
|
|||
Karma = 0;
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "an undead horse corpse";
|
||||
public override string DefaultName => "a skeletal steed";
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ namespace Server.Mobiles
|
|||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int BardingMaxHP => _bardingExceptional ? 2500 : 1000;
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override bool ReacquireOnMovement => true;
|
||||
public override bool AutoDispel => !Controlled;
|
||||
public override FoodType FavoriteFood => FoodType.Meat;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ namespace Server.Mobiles
|
|||
MinTameSkill = 95.1;
|
||||
}
|
||||
|
||||
public override int StepsMax => 4480;
|
||||
public override string CorpseName => "a unicorn corpse";
|
||||
public override bool AllowMaleRider => false;
|
||||
public override bool AllowMaleTamer => false;
|
||||
|
|
@ -89,12 +90,8 @@ namespace Server.Mobiles
|
|||
// TODO: Confirm if mount is the one flagged for curing it or the rider is
|
||||
if (Rider.CurePoison(this))
|
||||
{
|
||||
Rider.LocalOverheadMessage(
|
||||
MessageType.Regular,
|
||||
0x3B2,
|
||||
true,
|
||||
"Your mount senses you are in danger and aids you with magic."
|
||||
);
|
||||
// Your mount senses you are in danger and aids you with magic.
|
||||
Rider.LocalOverheadMessage(MessageType.Regular,0x3B2,1080039);
|
||||
Rider.FixedParticles(0x373A, 10, 15, 5012, EffectLayer.Waist);
|
||||
Rider.PlaySound(0x1E0); // Cure spell effect.
|
||||
Rider.PlaySound(0xA9); // Unicorn's whinny.
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ namespace Server.Mobiles
|
|||
MinTameSkill = 29.1;
|
||||
}
|
||||
|
||||
public override int StepsMax => 6400;
|
||||
public override string CorpseName => "a war horse corpse";
|
||||
|
||||
public override FoodType FavoriteFood => FoodType.FruitsAndVegies | FoodType.GrainsAndHay;
|
||||
|
|
|
|||
|
|
@ -2369,6 +2369,8 @@ namespace Server.Mobiles
|
|||
|
||||
UnsummonTimer.StopTimer(this);
|
||||
|
||||
StaminaSystem.RemoveEntry(this as IHasSteps);
|
||||
|
||||
base.OnAfterDelete();
|
||||
}
|
||||
|
||||
|
|
@ -4177,15 +4179,13 @@ namespace Server.Mobiles
|
|||
{
|
||||
if (!IsDeadPet && Controlled && (ControlMaster == from || IsPetFriend(from)))
|
||||
{
|
||||
var f = dropped;
|
||||
|
||||
if (CheckFoodPreference(f))
|
||||
if (CheckFoodPreference(dropped))
|
||||
{
|
||||
var amount = f.Amount;
|
||||
var amount = dropped.Amount;
|
||||
|
||||
if (amount > 0)
|
||||
{
|
||||
int stamGain = f switch
|
||||
int stamGain = dropped switch
|
||||
{
|
||||
Gold => amount - 50,
|
||||
_ => amount * 15 - 50
|
||||
|
|
@ -4194,6 +4194,8 @@ namespace Server.Mobiles
|
|||
if (stamGain > 0)
|
||||
{
|
||||
Stam += stamGain;
|
||||
// 64 food = 3,640 steps
|
||||
StaminaSystem.RegenSteps(this as IHasSteps, stamGain * 4);
|
||||
}
|
||||
|
||||
if (Core.SE)
|
||||
|
|
|
|||
|
|
@ -90,12 +90,12 @@ namespace Server.Mobiles
|
|||
public enum BlockMountType
|
||||
{
|
||||
None = -1,
|
||||
Dazed = 1040024,
|
||||
BolaRecovery = 1062910,
|
||||
DismountRecovery = 1070859
|
||||
Dazed = 1040024, // You are still too dazed from being knocked off your mount to ride!
|
||||
BolaRecovery = 1062910, // You cannot mount while recovering from a bola throw.
|
||||
DismountRecovery = 1070859 // You cannot mount while recovering from a dismount special maneuver.
|
||||
}
|
||||
|
||||
public class PlayerMobile : Mobile, IHonorTarget
|
||||
public class PlayerMobile : Mobile, IHonorTarget, IHasSteps
|
||||
{
|
||||
private static bool m_NoRecursion;
|
||||
|
||||
|
|
@ -214,6 +214,12 @@ namespace Server.Mobiles
|
|||
VisibilityList = new List<Mobile>();
|
||||
}
|
||||
|
||||
public int StepsMax => 16;
|
||||
|
||||
public int StepsGainedPerIdleTime => 1;
|
||||
|
||||
public TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(1);
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public DateTime AnkhNextUse { get; set; }
|
||||
|
||||
|
|
@ -399,8 +405,6 @@ namespace Server.Mobiles
|
|||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int Profession { get; set; }
|
||||
|
||||
public int StepsTaken { get; set; }
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public bool IsStealthing // IsStealthing should be moved to Server.Mobiles
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using Server.Items;
|
|||
|
||||
namespace Server.Mobiles;
|
||||
|
||||
[SerializationGenerator(0)]
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class ChaosGuard : BaseShieldGuard
|
||||
{
|
||||
[Constructible]
|
||||
|
|
|
|||
|
|
@ -201,65 +201,6 @@ public partial class Neira : BaseChampion
|
|||
CheckSpeedBoost();
|
||||
}
|
||||
|
||||
private class VirtualMount : IMount
|
||||
{
|
||||
private readonly VirtualMountItem _item;
|
||||
|
||||
public VirtualMount(VirtualMountItem item) => _item = item;
|
||||
|
||||
Mobile IMount.Rider
|
||||
{
|
||||
get => _item.Rider;
|
||||
set { }
|
||||
}
|
||||
|
||||
public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0)]
|
||||
private partial class VirtualMountItem : Item, IMountItem
|
||||
{
|
||||
private VirtualMount _mount;
|
||||
|
||||
[SerializableField(0, setter: "private")]
|
||||
private Mobile _rider;
|
||||
|
||||
public VirtualMountItem(Mobile mob) : base(0x3EBB)
|
||||
{
|
||||
Layer = Layer.Mount;
|
||||
|
||||
Movable = false;
|
||||
|
||||
_rider = mob;
|
||||
_mount = new VirtualMount(this);
|
||||
}
|
||||
|
||||
public IMount Mount => _mount;
|
||||
|
||||
[AfterDeserialization(false)]
|
||||
private void AfterDeserialization()
|
||||
{
|
||||
if (_rider?.Deleted != false)
|
||||
{
|
||||
Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
_mount = new VirtualMount(this);
|
||||
}
|
||||
}
|
||||
|
||||
public override DeathMoveResult OnParentDeath(Mobile parent)
|
||||
{
|
||||
_mount = null;
|
||||
Delete();
|
||||
|
||||
return DeathMoveResult.RemainEquipped;
|
||||
}
|
||||
}
|
||||
|
||||
private class DelayTimer : Timer
|
||||
{
|
||||
private readonly Mobile m_Mobile;
|
||||
|
|
|
|||
48
Projects/UOContent/Mobiles/VirtualMountItem.cs
Normal file
48
Projects/UOContent/Mobiles/VirtualMountItem.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
using ModernUO.Serialization;
|
||||
|
||||
namespace Server.Mobiles;
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class VirtualMountItem : Item, IMountItem, IMount
|
||||
{
|
||||
public VirtualMountItem(Mobile mob) : base(0x3EA0)
|
||||
{
|
||||
Layer = Layer.Mount;
|
||||
_rider = mob;
|
||||
}
|
||||
|
||||
public IMount Mount => this;
|
||||
|
||||
[SerializableProperty(0)]
|
||||
public Mobile Rider
|
||||
{
|
||||
get => _rider;
|
||||
set { }
|
||||
}
|
||||
|
||||
[AfterDeserialization(false)]
|
||||
private void AfterDeserialize()
|
||||
{
|
||||
if (_rider == null)
|
||||
{
|
||||
Delete();
|
||||
}
|
||||
}
|
||||
|
||||
public int Steps { get; set; }
|
||||
|
||||
public int StepsMax => 400;
|
||||
public int StepsGainedPerIdleTime => 1;
|
||||
public TimeSpan IdleTimePerStepsGain => TimeSpan.FromSeconds(10);
|
||||
|
||||
public void OnRiderDamaged(int amount, Mobile from, bool willKill)
|
||||
{
|
||||
}
|
||||
|
||||
public override DeathMoveResult OnParentDeath(Mobile parent)
|
||||
{
|
||||
Delete();
|
||||
return DeathMoveResult.RemainEquipped;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "0.9.8"
|
||||
"version": "0.9.9"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue