## 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.
471 lines
14 KiB
C#
471 lines
14 KiB
C#
using System;
|
|
using ModernUO.Serialization;
|
|
using Server.Factions.AI;
|
|
using Server.Items;
|
|
using Server.Mobiles;
|
|
|
|
namespace Server.Factions
|
|
{
|
|
public abstract class BaseFactionGuard : BaseCreature
|
|
{
|
|
private const int ListenRange = 12;
|
|
|
|
private static readonly Type[] m_StrongPotions =
|
|
{
|
|
typeof(GreaterHealPotion), typeof(GreaterHealPotion), typeof(GreaterHealPotion),
|
|
typeof(GreaterCurePotion), typeof(GreaterCurePotion), typeof(GreaterCurePotion),
|
|
typeof(GreaterStrengthPotion), typeof(GreaterStrengthPotion),
|
|
typeof(GreaterAgilityPotion), typeof(GreaterAgilityPotion),
|
|
typeof(TotalRefreshPotion), typeof(TotalRefreshPotion),
|
|
typeof(GreaterExplosionPotion)
|
|
};
|
|
|
|
private static readonly Type[] m_WeakPotions =
|
|
{
|
|
typeof(HealPotion), typeof(HealPotion), typeof(HealPotion),
|
|
typeof(CurePotion), typeof(CurePotion), typeof(CurePotion),
|
|
typeof(StrengthPotion), typeof(StrengthPotion),
|
|
typeof(AgilityPotion), typeof(AgilityPotion),
|
|
typeof(RefreshPotion), typeof(RefreshPotion),
|
|
typeof(ExplosionPotion)
|
|
};
|
|
|
|
private Faction m_Faction;
|
|
|
|
private DateTime m_OrdersEnd;
|
|
private Town m_Town;
|
|
|
|
public BaseFactionGuard(string title) : base(AIType.AI_Melee)
|
|
{
|
|
Orders = new Orders(this);
|
|
Title = title;
|
|
|
|
RangeHome = 6;
|
|
}
|
|
|
|
public BaseFactionGuard(Serial serial) : base(serial)
|
|
{
|
|
}
|
|
|
|
public override bool BardImmune => true;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
|
|
public Faction Faction
|
|
{
|
|
get => m_Faction;
|
|
set
|
|
{
|
|
Unregister();
|
|
m_Faction = value;
|
|
Register();
|
|
}
|
|
}
|
|
|
|
public Orders Orders { get; private set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
|
|
public Town Town
|
|
{
|
|
get => m_Town;
|
|
set
|
|
{
|
|
Unregister();
|
|
m_Town = value;
|
|
Register();
|
|
}
|
|
}
|
|
|
|
public abstract GuardAI GuardAI { get; }
|
|
|
|
protected override BaseAI ForcedAI => new FactionGuardAI(this);
|
|
|
|
public override TimeSpan ReacquireDelay => TimeSpan.FromSeconds(2.0);
|
|
|
|
public override bool ClickTitle => false;
|
|
|
|
public void Register()
|
|
{
|
|
if (m_Town != null && m_Faction != null)
|
|
{
|
|
m_Town.RegisterGuard(this);
|
|
}
|
|
}
|
|
|
|
public void Unregister()
|
|
{
|
|
m_Town?.UnregisterGuard(this);
|
|
}
|
|
|
|
public override bool IsEnemy(Mobile m)
|
|
{
|
|
var ourFaction = m_Faction;
|
|
var theirFaction = Faction.Find(m);
|
|
|
|
if (theirFaction == null && m is BaseFactionGuard guard)
|
|
{
|
|
theirFaction = guard.Faction;
|
|
}
|
|
|
|
if (ourFaction != null && theirFaction != null && ourFaction != theirFaction)
|
|
{
|
|
var reactionType = Orders.GetReaction(theirFaction).Type;
|
|
|
|
if (reactionType == ReactionType.Attack)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var list = m.Aggressed;
|
|
|
|
for (var i = 0; i < list.Count; ++i)
|
|
{
|
|
var ai = list[i];
|
|
|
|
if (ai.Defender is BaseFactionGuard bf && bf.Faction == ourFaction)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public override void OnMovement(Mobile m, Point3D oldLocation)
|
|
{
|
|
if (m.Player && m.Alive && InRange(m, 10) && !InRange(oldLocation, 10) && InLOS(m) &&
|
|
Orders.GetReaction(Faction.Find(m)).Type == ReactionType.Warn)
|
|
{
|
|
Direction = GetDirectionTo(m);
|
|
|
|
var warning = Utility.Random(6) switch
|
|
{
|
|
0 => $"I warn you, {m.Name}, you would do well to leave this area before someone shows you the world of gray.",
|
|
1 => $"It would be wise to leave this area, {m.Name}, lest your head become my commanders' trophy.",
|
|
2 => $"You are bold, {m.Name}, for one of the meager {Faction.Find(m)?.Definition.FriendlyName ?? "civilians"}. Leave now, lest you be taught the taste of dirt.",
|
|
3 => $"Your presence here is an insult, {m.Name}. Be gone now, knave.",
|
|
4 => $"Dost thou wish to be hung by your toes, {m.Name}? Nay? Then come no closer.",
|
|
_ => $"Hey, {m.Name}. Yeah, you. Get out of here before I beat you with a stick." // 5
|
|
};
|
|
|
|
Say(warning);
|
|
}
|
|
}
|
|
|
|
public override bool HandlesOnSpeech(Mobile from) => InRange(from, ListenRange) || base.HandlesOnSpeech(from);
|
|
|
|
private void ChangeReaction(Faction faction, ReactionType type)
|
|
{
|
|
if (faction == null)
|
|
{
|
|
switch (type)
|
|
{
|
|
case ReactionType.Ignore:
|
|
{
|
|
Say(1005179);
|
|
break; // Civilians will now be ignored.
|
|
}
|
|
case ReactionType.Warn:
|
|
{
|
|
Say(1005180);
|
|
break; // Civilians will now be warned of their impending deaths.
|
|
}
|
|
case ReactionType.Attack:
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var def = type switch
|
|
{
|
|
ReactionType.Ignore => faction.Definition.GuardIgnore,
|
|
ReactionType.Warn => faction.Definition.GuardWarn,
|
|
_ => faction.Definition.GuardAttack // ReactionType.Attack
|
|
};
|
|
|
|
if (def?.Number > 0)
|
|
{
|
|
Say(def.Number);
|
|
}
|
|
else if (def?.String != null)
|
|
{
|
|
Say(def.String);
|
|
}
|
|
}
|
|
|
|
Orders.SetReaction(faction, type);
|
|
}
|
|
|
|
private bool WasNamed(string speech)
|
|
{
|
|
var name = Name;
|
|
|
|
return name != null && speech.InsensitiveStartsWith(name);
|
|
}
|
|
|
|
public override void OnSpeech(SpeechEventArgs e)
|
|
{
|
|
base.OnSpeech(e);
|
|
|
|
var from = e.Mobile;
|
|
|
|
if (!e.Handled && InRange(from, ListenRange) && from.Alive)
|
|
{
|
|
if (e.HasKeyword(0xE6) && (e.Speech.InsensitiveEquals("orders") || WasNamed(e.Speech))) // *orders*
|
|
{
|
|
if (m_Town?.IsSheriff(from) != true)
|
|
{
|
|
Say(1042189); // I don't work for you!
|
|
}
|
|
else if (Town.FromRegion(Region) == m_Town)
|
|
{
|
|
Say(1042180); // Your orders, sire?
|
|
m_OrdersEnd = Core.Now + TimeSpan.FromSeconds(10.0);
|
|
}
|
|
}
|
|
else if (Core.Now < m_OrdersEnd)
|
|
{
|
|
if (m_Town?.IsSheriff(from) != true || Town.FromRegion(Region) != m_Town)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_OrdersEnd = Core.Now + TimeSpan.FromSeconds(10.0);
|
|
|
|
var understood = true;
|
|
ReactionType newType = 0;
|
|
|
|
if (e.Speech.InsensitiveContains("attack"))
|
|
{
|
|
newType = ReactionType.Attack;
|
|
}
|
|
else if (e.Speech.InsensitiveContains("warn"))
|
|
{
|
|
newType = ReactionType.Warn;
|
|
}
|
|
else if (e.Speech.InsensitiveContains("ignore"))
|
|
{
|
|
newType = ReactionType.Ignore;
|
|
}
|
|
else
|
|
{
|
|
understood = false;
|
|
}
|
|
|
|
if (understood)
|
|
{
|
|
understood = false;
|
|
|
|
if (e.Speech.InsensitiveContains("civil"))
|
|
{
|
|
ChangeReaction(null, newType);
|
|
understood = true;
|
|
}
|
|
|
|
var factions = Faction.Factions;
|
|
|
|
for (var i = 0; i < factions.Count; ++i)
|
|
{
|
|
var faction = factions[i];
|
|
|
|
if (faction != m_Faction && e.Speech.InsensitiveContains(faction.Definition.Keyword))
|
|
{
|
|
ChangeReaction(faction, newType);
|
|
understood = true;
|
|
}
|
|
}
|
|
}
|
|
else if (e.Speech.InsensitiveContains("patrol"))
|
|
{
|
|
Home = Location;
|
|
RangeHome = 6;
|
|
Combatant = null;
|
|
Orders.Movement = MovementType.Patrol;
|
|
Say(1005146); // This spot looks like it needs protection! I shall guard it with my life.
|
|
understood = true;
|
|
}
|
|
else if (e.Speech.InsensitiveContains("follow"))
|
|
{
|
|
Home = Location;
|
|
RangeHome = 6;
|
|
Combatant = null;
|
|
Orders.Follow = from;
|
|
Orders.Movement = MovementType.Follow;
|
|
Say(1005144); // Yes, Sire.
|
|
understood = true;
|
|
}
|
|
|
|
if (!understood)
|
|
{
|
|
Say(1042183); // I'm sorry, I don't understand your orders...
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void GetProperties(IPropertyList list)
|
|
{
|
|
base.GetProperties(list);
|
|
|
|
if (m_Faction != null && Map == Faction.Facet)
|
|
{
|
|
list.Add(1060846, $"{m_Faction.Definition.PropName}"); // Guard: ~1_val~
|
|
}
|
|
}
|
|
|
|
public override void OnSingleClick(Mobile from)
|
|
{
|
|
if (m_Faction != null && Map == Faction.Facet)
|
|
{
|
|
var text = $"(Guard, {m_Faction.Definition.FriendlyName})";
|
|
|
|
var hue = Faction.Find(from) == m_Faction ? 98 : 38;
|
|
|
|
PrivateOverheadMessage(MessageType.Label, hue, true, text, from.NetState);
|
|
}
|
|
|
|
base.OnSingleClick(from);
|
|
}
|
|
|
|
public virtual void GenerateRandomHair()
|
|
{
|
|
Utility.AssignRandomHair(this);
|
|
Utility.AssignRandomFacialHair(this, HairHue);
|
|
}
|
|
|
|
public void PackStrongPotions(int min, int max)
|
|
{
|
|
PackStrongPotions(Utility.RandomMinMax(min, max));
|
|
}
|
|
|
|
public void PackStrongPotions(int count)
|
|
{
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
PackStrongPotion();
|
|
}
|
|
}
|
|
|
|
public void PackStrongPotion()
|
|
{
|
|
PackItem(Loot.Construct(m_StrongPotions));
|
|
}
|
|
|
|
public void PackWeakPotions(int min, int max)
|
|
{
|
|
PackWeakPotions(Utility.RandomMinMax(min, max));
|
|
}
|
|
|
|
public void PackWeakPotions(int count)
|
|
{
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
PackWeakPotion();
|
|
}
|
|
}
|
|
|
|
public void PackWeakPotion()
|
|
{
|
|
PackItem(Loot.Construct(m_WeakPotions));
|
|
}
|
|
|
|
public Item Immovable(Item item)
|
|
{
|
|
item.Movable = false;
|
|
return item;
|
|
}
|
|
|
|
public Item Newbied(Item item)
|
|
{
|
|
item.LootType = LootType.Newbied;
|
|
return item;
|
|
}
|
|
|
|
public Item Rehued(Item item, int hue)
|
|
{
|
|
item.Hue = hue;
|
|
return item;
|
|
}
|
|
|
|
public Item Layered(Item item, Layer layer)
|
|
{
|
|
item.Layer = layer;
|
|
return item;
|
|
}
|
|
|
|
public Item Resourced(BaseWeapon weapon, CraftResource resource)
|
|
{
|
|
weapon.Resource = resource;
|
|
return weapon;
|
|
}
|
|
|
|
public Item Resourced(BaseArmor armor, CraftResource resource)
|
|
{
|
|
armor.Resource = resource;
|
|
return armor;
|
|
}
|
|
|
|
public override void OnAfterDelete()
|
|
{
|
|
base.OnAfterDelete();
|
|
Unregister();
|
|
}
|
|
|
|
public override void OnDeath(Container c)
|
|
{
|
|
base.OnDeath(c);
|
|
|
|
c.Delete();
|
|
}
|
|
|
|
public virtual void GenerateBody(bool isFemale, bool randomHair)
|
|
{
|
|
Hue = Race.Human.RandomSkinHue();
|
|
|
|
if (isFemale)
|
|
{
|
|
Female = true;
|
|
Body = 401;
|
|
Name = NameList.RandomName("female");
|
|
}
|
|
else
|
|
{
|
|
Female = false;
|
|
Body = 400;
|
|
Name = NameList.RandomName("male");
|
|
}
|
|
|
|
if (randomHair)
|
|
{
|
|
GenerateRandomHair();
|
|
}
|
|
}
|
|
|
|
public override void Serialize(IGenericWriter writer)
|
|
{
|
|
base.Serialize(writer);
|
|
|
|
writer.Write(0); // version
|
|
|
|
Faction.WriteReference(writer, m_Faction);
|
|
Town.WriteReference(writer, m_Town);
|
|
|
|
Orders.Serialize(writer);
|
|
}
|
|
|
|
public override void Deserialize(IGenericReader reader)
|
|
{
|
|
base.Deserialize(reader);
|
|
|
|
var version = reader.ReadInt();
|
|
|
|
m_Faction = Faction.ReadReference(reader);
|
|
m_Town = Town.ReadReference(reader);
|
|
Orders = new Orders(this, reader);
|
|
|
|
Timer.StartTimer(Register);
|
|
}
|
|
}
|
|
}
|