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:
Kamron Batman 2023-09-16 17:52:54 -07:00 committed by GitHub
parent 25eaff0b68
commit 0f2870e94a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 983 additions and 493 deletions

View file

@ -0,0 +1,12 @@
using System;
namespace Server.Mobiles;
public interface IHasSteps
{
int StepsMax { get; }
int StepsGainedPerIdleTime { get; }
TimeSpan IdleTimePerStepsGain { get; }
}

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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)]

View file

@ -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;
}
}
}

View file

@ -1,4 +1,5 @@
using Server.Items;
using Server.Mobiles;
namespace Server.Factions
{

View file

@ -1,4 +1,5 @@
using Server.Items;
using Server.Mobiles;
namespace Server.Factions
{

View file

@ -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!");
}
}
}

View file

@ -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!");
}
}
}

View file

@ -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": [
""
]
}
]
}

View file

@ -0,0 +1,11 @@
{
"version": 0,
"type": "Server.Mobiles.VirtualMountItem",
"properties": [
{
"name": "Rider",
"type": "Server.Mobile",
"rule": "SerializableInterfaceMigrationRule"
}
]
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}
}

View file

@ -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;

View file

@ -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]

View file

@ -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;

View file

@ -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;

View file

@ -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 */

View file

@ -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);

View file

@ -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 */

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -20,6 +20,7 @@ namespace Server.Mobiles
MinTameSkill = 103.1;
}
public override int StepsMax => 4480;
public override string CorpseName => "a silver steed corpse";
}
}

View file

@ -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";

View file

@ -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;

View file

@ -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.

View file

@ -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;

View file

@ -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)

View file

@ -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
{

View file

@ -4,7 +4,7 @@ using Server.Items;
namespace Server.Mobiles;
[SerializationGenerator(0)]
[SerializationGenerator(0, false)]
public partial class ChaosGuard : BaseShieldGuard
{
[Constructible]

View file

@ -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;

View 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;
}
}

View file

@ -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"
}