ModernUO/Projects/UOContent/Engines/Virtues/VirtueSystem.cs
Kamron Batman 0f2870e94a
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.
2023-09-16 17:52:54 -07:00

384 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Server.Collections;
using Server.Logging;
using Server.Mobiles;
namespace Server.Engines.Virtues;
public enum VirtueLevel
{
None,
Seeker,
Follower,
Knight
}
[Flags]
public enum VirtueName
{
Humility,
Sacrifice,
Compassion,
Spirituality,
Valor,
Honor,
Justice,
Honesty
}
public static class VirtueSystem
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(VirtueSystem));
private static readonly Dictionary<PlayerMobile, VirtueContext> _playerVirtues = new();
private static void FixVirtue(Mobile m, int[] virtueValues)
{
if (m is not PlayerMobile pm)
{
return;
}
var virtues = pm.Virtues;
for (var i = 0; i < virtueValues.Length; i++)
{
var val = virtueValues[i];
if (val > 0)
{
virtues.SetValue(i, val);
}
}
}
public static void Configure()
{
GenericPersistence.Register("Virtues", Serialize, Deserialize);
}
public static void Initialize()
{
var migrations = Mobile.VirtueMigrations;
if (migrations?.Count > 0)
{
foreach (var (m, values) in migrations)
{
FixVirtue(m, values);
}
}
}
private static void Serialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt(_playerVirtues.Count);
foreach (var (pm, virtues) in _playerVirtues)
{
writer.Write(pm);
virtues.Serialize(writer);
}
}
private static void Deserialize(IGenericReader reader)
{
reader.ReadEncodedInt(); // version
var contextCount = reader.ReadEncodedInt();
for (var i = 0; i < contextCount; i++)
{
var player = reader.ReadEntity<PlayerMobile>();
var virtues = new VirtueContext();
virtues.Deserialize(reader);
if (virtues.IsUsed())
{
_playerVirtues.Add(player, virtues);
}
}
}
public static VirtueContext GetVirtues(this PlayerMobile from) =>
_playerVirtues.TryGetValue(from, out var context) ? context : null;
public static VirtueContext GetOrCreateVirtues(this PlayerMobile from)
{
ref VirtueContext context = ref CollectionsMarshal.GetValueRefOrAddDefault(_playerVirtues, from, out bool exists);
if (!exists)
{
context = new VirtueContext();
}
return context;
}
public static bool IsHighestPath(PlayerMobile from, VirtueName virtue) =>
from.GetVirtues()?.GetValue((int)virtue) >= GetMaxAmount(virtue);
public static VirtueLevel GetLevel(Mobile from, VirtueName virtue)
{
var v = (from as PlayerMobile)?.GetVirtues()?.GetValue((int)virtue) ?? 0;
int vl;
if (v < 4000)
{
vl = 0;
}
else if (v >= GetMaxAmount(virtue))
{
vl = 3;
}
else
{
vl = (v + 9999) / 10000;
}
return (VirtueLevel)vl;
}
public static string GetName(this VirtueName virtue) =>
virtue switch
{
VirtueName.Humility => "Humility",
VirtueName.Sacrifice => "Sacrifice",
VirtueName.Compassion => "Compassion",
VirtueName.Spirituality => "Spirituality",
VirtueName.Valor => "Valor",
VirtueName.Honor => "Honor",
VirtueName.Justice => "Justice",
VirtueName.Honesty => "Honesty",
_ => ""
};
public static string GetLowerCaseName(this VirtueName virtue) =>
virtue switch
{
VirtueName.Humility => "humility",
VirtueName.Sacrifice => "sacrifice",
VirtueName.Compassion => "compassion",
VirtueName.Spirituality => "spirituality",
VirtueName.Valor => "valor",
VirtueName.Honor => "honor",
VirtueName.Justice => "justice",
VirtueName.Honesty => "honesty",
_ => ""
};
public static int GetMaxAmount(VirtueName virtue) =>
virtue switch
{
VirtueName.Honor => 20000,
VirtueName.Sacrifice => 22000,
_ => 21000
};
public static int GetGainedLocalizedMessage(VirtueName virtue) =>
virtue switch
{
VirtueName.Sacrifice => 1054160, // You have gained in sacrifice.
VirtueName.Compassion => 1053002, // You have gained in compassion.
VirtueName.Spirituality => 1155832, // You have gained in Spirituality.
VirtueName.Valor => 1054030, // You have gained in Valor!
VirtueName.Honor => 1063225, // You have gained in Honor.
VirtueName.Justice => 1049363, // You have gained in Justice.
VirtueName.Humility => 1052070, // You have gained in Humility.
_ => 0
};
public static int GetGainedAPathLocalizedMessage(VirtueName virtue) =>
virtue switch
{
VirtueName.Sacrifice => 1052008, // You have gained a path in Sacrifice!
VirtueName.Spirituality => 1155833, // "You have gained a path in Spirituality!" (Why are there quotes?)
VirtueName.Valor => 1054032, // You have gained a path in Valor!
VirtueName.Honor => 1063226, // You have gained a path in Honor!
VirtueName.Justice => 1049367, // You have gained a path in Justice!
VirtueName.Humility => 1155811, // You have gained a path in Humility!
_ => 0
};
public static int GetHightestPathLocalizedMessage(VirtueName virtue) =>
virtue switch
{
VirtueName.Compassion => 1053003, // You have achieved the highest path of compassion and can no longer gain any further.
VirtueName.Spirituality => 1155831, // You cannot gain more Spirituality.
VirtueName.Valor => 1054031, // You have achieved the highest path in Valor and can no longer gain any further.
VirtueName.Honor => 1063228, // You cannot gain more Honor.
VirtueName.Justice => 1049534, // You cannot gain more Justice.
VirtueName.Humility => 1155808, // You cannot gain more Humility.
VirtueName.Honesty => 1153771, // You have achieved the highest path in Honesty and can no longer gain any further.
_ => 1052050, // You have achieved the highest path in this virtue.
};
public static bool Award(PlayerMobile from, VirtueName virtue, int amount, ref bool gainedPath)
{
var virtues = from.Virtues;
var current = virtues.GetValue((int)virtue);
var maxAmount = GetMaxAmount(virtue);
if (current >= maxAmount)
{
return false;
}
if (current + amount >= maxAmount)
{
amount = maxAmount - current;
}
var oldLevel = GetLevel(from, virtue);
virtues.SetValue((int)virtue, current + amount);
gainedPath = GetLevel(from, virtue) != oldLevel;
return true;
}
public static bool Atrophy(PlayerMobile from, VirtueName virtue, int amount = 1)
{
var virtues = from.GetVirtues();
if (virtues == null)
{
return false;
}
var current = virtues.GetValue((int)virtue);
if (current - amount >= 0)
{
virtues.SetValue((int)virtue, current - amount);
}
else
{
virtues.SetValue((int)virtue, 0);
}
return current > 0;
}
public static bool IsSeeker(PlayerMobile from, VirtueName virtue) => GetLevel(from, virtue) >= VirtueLevel.Seeker;
public static bool IsFollower(PlayerMobile from, VirtueName virtue) => GetLevel(from, virtue) >= VirtueLevel.Follower;
public static bool IsKnight(PlayerMobile from, VirtueName virtue) => GetLevel(from, virtue) >= VirtueLevel.Knight;
public static void AwardVirtue(PlayerMobile pm, VirtueName virtue, int amount)
{
var virtues = pm.GetOrCreateVirtues();
if (virtue == VirtueName.Compassion)
{
if (virtues.CompassionGains > 0 && Core.Now > virtues.NextCompassionDay)
{
virtues.NextCompassionDay = DateTime.MinValue;
virtues.CompassionGains = 0;
}
if (virtues.CompassionGains >= 5)
{
pm.SendLocalizedMessage(1053004); // You must wait about a day before you can gain in compassion again.
return;
}
}
var gainedPath = false;
var virtueName = virtue.GetName();
if (Award(pm, virtue, amount, ref gainedPath))
{
if (gainedPath)
{
var gainedPathMessage = GetGainedAPathLocalizedMessage(virtue);
if (gainedPathMessage != 0)
{
pm.SendLocalizedMessage(gainedPathMessage);
}
else
{
pm.SendMessage($"You have gained a path in {virtueName}!");
}
}
else
{
var gainMessage = GetGainedLocalizedMessage(virtue);
if (gainMessage != 0)
{
pm.SendLocalizedMessage(gainMessage);
}
else
{
pm.SendMessage($"You have gained in {virtueName}.");
}
}
if (virtue == VirtueName.Compassion)
{
virtues.NextCompassionDay = Core.Now + TimeSpan.FromDays(1.0);
if (++virtues.CompassionGains >= 5)
{
// You must wait about a day before you can gain in compassion again.
pm.SendLocalizedMessage(1053004);
}
}
}
else
{
pm.SendLocalizedMessage(GetHightestPathLocalizedMessage(virtue));
}
}
public static void CheckAtrophies(this PlayerMobile pm)
{
SacrificeVirtue.CheckAtrophy(pm);
JusticeVirtue.CheckAtrophy(pm);
CompassionVirtue.CheckAtrophy(pm);
ValorVirtue.CheckAtrophy(pm);
}
private class VirtueTimer : Timer
{
public VirtueTimer() : base(TimeSpan.FromMinutes(5.0), TimeSpan.FromMinutes(5.0))
{
}
public static void Initialize()
{
new VirtueTimer().Start();
}
protected override void OnTick()
{
if (_playerVirtues.Count == 0)
{
return;
}
using var queue = PooledRefQueue<Mobile>.Create();
// This is not particularly efficient. If it gets too slow, then use a different architecture.
foreach (var (player, virtues) in _playerVirtues)
{
CheckAtrophies(player);
if (!virtues.IsUsed())
{
queue.Enqueue(player);
}
}
while (queue.Count > 0)
{
_playerVirtues.Remove((PlayerMobile)queue.Dequeue());
}
}
~VirtueTimer()
{
VirtueSystem.logger.Error($"{nameof(VirtueTimer)} is no longer running!");
}
}
}