## 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.
352 lines
8.3 KiB
C#
352 lines
8.3 KiB
C#
using ModernUO.Serialization;
|
|
using System;
|
|
using Server.Items;
|
|
using Server.Misc;
|
|
using Server.Multis;
|
|
using Server.Targeting;
|
|
|
|
namespace Server.Mobiles;
|
|
|
|
[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)
|
|
{
|
|
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
|
|
{
|
|
base.Hue = value;
|
|
|
|
if (InternalItem != null)
|
|
{
|
|
InternalItem.Hue = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int ItemID
|
|
{
|
|
get => InternalItem?.ItemID ?? 0;
|
|
set
|
|
{
|
|
if (InternalItem != null)
|
|
{
|
|
InternalItem.ItemID = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
[SerializableProperty(1)]
|
|
public Mobile Rider
|
|
{
|
|
get => _rider;
|
|
set
|
|
{
|
|
if (_rider == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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;
|
|
|
|
if (value == null)
|
|
{
|
|
StaminaSystem.OnDismount(this);
|
|
}
|
|
|
|
this.MarkDirty();
|
|
}
|
|
}
|
|
|
|
public virtual void OnRiderDamaged(int amount, Mobile from, bool willKill)
|
|
{
|
|
if (_rider == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var attacker = from ?? _rider.FindMostRecentDamager(true);
|
|
|
|
if (!(attacker == this || attacker == _rider || willKill || Core.Now < NextMountAbility)
|
|
&& DoMountAbility(amount, from))
|
|
{
|
|
NextMountAbility = Core.Now + MountAbilityDelay;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (from.IsBodyMod && !from.Body.IsHuman)
|
|
{
|
|
if (Core.AOS) // You cannot ride a mount in your current form.
|
|
{
|
|
PrivateOverheadMessage(MessageType.Regular, 0x3B2, 1062061, from.NetState);
|
|
}
|
|
else
|
|
{
|
|
from.SendLocalizedMessage(1061628); // You can't do that while polymorphed.
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
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)
|
|
{
|
|
// 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();
|
|
}
|
|
}
|
|
}
|