modernuo/Projects/Server/Skills.cs
Vitalii Zurian f9517854d0
feat: Adds configurable skill and stat gain (#1489)
### Summary

- Adds the following configurations:
```json
"stats.statMax": "125",
"stats.gainChanceMultiplier": 1.0,
"stats.primaryStatGainChance": 0.75,
"stats.gainDelay": "00:10:00",
"stats.petGainDelay": "00:05:00",
"stats.usePub45StatGain": "False"
```

- Adds Pub 45 stat gain rate for UOML+.
- Reduces the gain delay for legacy stat gain from 15 minutes to 10 minutes.
- Legacy gain no longer requires a gain in skill to gain in a stat.
2023-10-14 12:02:36 -07:00

896 lines
23 KiB
C#

using System;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Server.Network;
namespace Server;
public delegate TimeSpan SkillUseCallback(Mobile user);
public enum SkillLock : byte
{
Up = 0,
Down = 1,
Locked = 2
}
public enum SkillName
{
Alchemy = 0,
Anatomy = 1,
AnimalLore = 2,
ItemID = 3,
ArmsLore = 4,
Parry = 5,
Begging = 6,
Blacksmith = 7,
Fletching = 8,
Peacemaking = 9,
Camping = 10,
Carpentry = 11,
Cartography = 12,
Cooking = 13,
DetectHidden = 14,
Discordance = 15,
EvalInt = 16,
Healing = 17,
Fishing = 18,
Forensics = 19,
Herding = 20,
Hiding = 21,
Provocation = 22,
Inscribe = 23,
Lockpicking = 24,
Magery = 25,
MagicResist = 26,
Tactics = 27,
Snooping = 28,
Musicianship = 29,
Poisoning = 30,
Archery = 31,
SpiritSpeak = 32,
Stealing = 33,
Tailoring = 34,
AnimalTaming = 35,
TasteID = 36,
Tinkering = 37,
Tracking = 38,
Veterinary = 39,
Swords = 40,
Macing = 41,
Fencing = 42,
Wrestling = 43,
Lumberjacking = 44,
Mining = 45,
Meditation = 46,
Stealth = 47,
RemoveTrap = 48,
Necromancy = 49,
Focus = 50,
Chivalry = 51,
Bushido = 52,
Ninjitsu = 53,
Spellweaving = 54,
Mysticism = 55,
Imbuing = 56,
Throwing = 57
}
public enum Stat
{
Str,
Dex,
Int
}
[PropertyObject]
public class Skill
{
private ushort m_Base;
private ushort m_Cap;
public Skill(Skills owner, SkillInfo info, IGenericReader reader)
{
Owner = owner;
Info = info;
int version = reader.ReadByte();
switch (version)
{
case 0:
{
m_Base = reader.ReadUShort();
m_Cap = reader.ReadUShort();
Lock = (SkillLock)reader.ReadByte();
break;
}
case 0xFF:
{
m_Base = 0;
m_Cap = 1000;
Lock = SkillLock.Up;
break;
}
default:
{
if ((version & 0xC0) == 0x00)
{
if ((version & 0x1) != 0)
{
m_Base = reader.ReadUShort();
}
if ((version & 0x2) != 0)
{
m_Cap = reader.ReadUShort();
}
else
{
m_Cap = 1000;
}
if ((version & 0x4) != 0)
{
Lock = (SkillLock)reader.ReadByte();
}
}
break;
}
}
if (Lock > SkillLock.Locked)
{
Lock = SkillLock.Up;
}
}
public Skill(Skills owner, SkillInfo info, int baseValue, int cap, SkillLock skillLock)
{
Owner = owner;
Info = info;
m_Base = (ushort)baseValue;
m_Cap = (ushort)cap;
Lock = skillLock;
}
public Skills Owner { get; }
public SkillName SkillName => (SkillName)Info.SkillID;
public int SkillID => Info.SkillID;
[CommandProperty(AccessLevel.Counselor)]
public string Name => Info.Name;
public SkillInfo Info { get; }
[CommandProperty(AccessLevel.Counselor)]
public SkillLock Lock { get; private set; }
public int BaseFixedPoint
{
get => m_Base;
set
{
var sv = (ushort)Math.Clamp(value, 0, 0xFFFF);
int oldBase = m_Base;
if (m_Base != sv)
{
Owner.Total = Owner.Total - m_Base + sv;
m_Base = sv;
Owner.OnSkillChange(this);
var m = Owner.Owner;
m?.OnSkillChange(SkillName, (double)oldBase / 10);
}
}
}
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public double Base
{
get => m_Base / 10.0;
set => BaseFixedPoint = (int)(value * 10.0);
}
public int CapFixedPoint
{
get => m_Cap;
set
{
var sv = (ushort)Math.Clamp(value, 0, 0xFFFF);
if (m_Cap != sv)
{
m_Cap = sv;
Owner.OnSkillChange(this);
}
}
}
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public double Cap
{
get => m_Cap / 10.0;
set => CapFixedPoint = (int)(value * 10.0);
}
public int Fixed => (int)(Value * 10);
[CommandProperty(AccessLevel.Counselor)]
public double Value
{
get
{
// There has to be this distinction between the racial values and not to account for gaining skills and these skills aren't displayed nor Totaled up.
var value = NonRacialValue;
var raceBonus = Owner.Owner.RacialSkillBonus;
if (raceBonus > value)
{
value = raceBonus;
}
return value;
}
}
[CommandProperty(AccessLevel.Counselor)]
public double NonRacialValue
{
get
{
var baseValue = Base;
var statsOffset = Owner.Owner.RawStr * Info.StrScale +
Owner.Owner.RawDex * Info.DexScale +
Owner.Owner.RawInt * Info.IntScale;
var inv = 100.0 - baseValue;
if (inv <= 0.0)
{
statsOffset = 0.0;
}
else
{
statsOffset *= inv;
if (Info.StatTotal > 0)
{
var statTotal = Info.StatTotal * inv;
if (statsOffset > statTotal)
{
statsOffset = statTotal;
}
}
}
var value = baseValue + statsOffset;
Owner.Owner.ValidateSkillMods();
double bonusObey = 0.0, bonusNotObey = 0.0;
var mods = Owner.Owner.SkillMods;
if (mods != null)
{
foreach (var mod in mods)
{
if (mod.Skill != (SkillName)Info.SkillID)
{
continue;
}
if (mod.Relative)
{
if (mod.ObeyCap)
{
bonusObey += mod.Value;
}
else
{
bonusNotObey += mod.Value;
}
}
else
{
bonusObey = 0.0;
bonusNotObey = 0.0;
value = mod.Value;
}
}
}
value += bonusNotObey;
if (value < Cap)
{
value += bonusObey;
if (value > Cap)
{
value = Cap;
}
}
return value;
}
}
public override string ToString() => $"[{Name}: {Base}]";
public void SetLockNoRelay(SkillLock skillLock)
{
if (skillLock > SkillLock.Locked)
{
return;
}
Lock = skillLock;
}
public void Serialize(IGenericWriter writer)
{
if (m_Base == 0 && m_Cap == 1000 && Lock == SkillLock.Up)
{
writer.Write((byte)0xFF); // default
}
else
{
var flags = 0x0;
if (m_Base != 0)
{
flags |= 0x1;
}
if (m_Cap != 1000)
{
flags |= 0x2;
}
if (Lock != SkillLock.Up)
{
flags |= 0x4;
}
writer.Write((byte)flags); // version
if (m_Base != 0)
{
writer.Write((short)m_Base);
}
if (m_Cap != 1000)
{
writer.Write((short)m_Cap);
}
if (Lock != SkillLock.Up)
{
writer.Write((byte)Lock);
}
}
}
public void Update()
{
Owner.OnSkillChange(this);
}
}
public class SkillInfo
{
[JsonConstructor]
public SkillInfo(
int skillID, string name, double strScale, double dexScale, double intScale, string title,
SkillUseCallback callback, double strGain, double dexGain, double intGain, double gainFactor,
string professionSkillName, Stat primaryStat, Stat secondaryStat
)
{
Name = name;
Title = title;
SkillID = skillID;
StrScale = strScale / 100.0;
DexScale = dexScale / 100.0;
IntScale = intScale / 100.0;
Callback = callback;
StrGain = strGain;
DexGain = dexGain;
IntGain = intGain;
GainFactor = gainFactor;
ProfessionSkillName = professionSkillName ?? Name.RemoveOrdinal(" ");
StatTotal = strScale + dexScale + intScale;
PrimaryStat = primaryStat;
SecondaryStat = secondaryStat;
}
public SkillUseCallback Callback { get; set; }
public int SkillID { get; }
public string Name { get; set; }
public string Title { get; set; }
public double StrScale { get; set; }
public double DexScale { get; set; }
public double IntScale { get; set; }
public double StatTotal { get; set; }
public double StrGain { get; set; }
public double DexGain { get; set; }
public double IntGain { get; set; }
public double GainFactor { get; set; }
public string ProfessionSkillName { get; set; }
public Stat PrimaryStat { get; set; }
public Stat SecondaryStat { get; set; }
public static SkillInfo[] Table { get; set; } = Array.Empty<SkillInfo>();
}
[PropertyObject]
public class Skills
{
private readonly Skill[] m_Skills;
private Skill m_Highest;
public Skills(Mobile owner)
{
Owner = owner;
Cap = 7000;
var info = SkillInfo.Table;
m_Skills = new Skill[info.Length];
}
public Skills(Mobile owner, IGenericReader reader)
{
Owner = owner;
var version = reader.ReadInt();
switch (version)
{
case 3:
case 2:
{
Cap = reader.ReadInt();
goto case 1;
}
case 1:
{
if (version < 2)
{
Cap = 7000;
}
/*m_Total =*/
if (version < 3)
{
reader.ReadInt();
}
var info = SkillInfo.Table;
m_Skills = new Skill[info.Length];
var count = reader.ReadInt();
for (var i = 0; i < count; ++i)
{
if (i < info.Length)
{
var sk = new Skill(this, info[i], reader);
if (sk.BaseFixedPoint != 0 || sk.CapFixedPoint != 1000 || sk.Lock != SkillLock.Up)
{
m_Skills[i] = sk;
Total += sk.BaseFixedPoint;
}
}
else
{
// Will be discarded
_ = new Skill(this, null, reader);
}
}
// for ( int i = count; i < info.Length; ++i )
// m_Skills[i] = new Skill( this, info[i], 0, 1000, SkillLock.Up );
break;
}
case 0:
{
reader.ReadInt();
goto case 1;
}
}
}
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
public int Cap { get; set; }
public int Total { get; set; }
public Mobile Owner { get; }
public int Length => m_Skills.Length;
public Skill this[SkillName name] => this[(int)name];
public Skill this[int skillID]
{
get
{
if (skillID < 0 || skillID >= m_Skills.Length)
{
return null;
}
var sk = m_Skills[skillID];
if (sk == null)
{
m_Skills[skillID] = sk = new Skill(this, SkillInfo.Table[skillID], 0, 1000, SkillLock.Up);
}
return sk;
}
}
public Skill Highest
{
get
{
if (m_Highest == null)
{
Skill highest = null;
var value = int.MinValue;
for (var i = 0; i < m_Skills.Length; ++i)
{
var sk = m_Skills[i];
if (sk?.BaseFixedPoint > value)
{
value = sk.BaseFixedPoint;
highest = sk;
}
}
m_Highest = highest == null && m_Skills.Length > 0 ? this[0] : highest;
}
return m_Highest;
}
}
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Alchemy => this[SkillName.Alchemy];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Anatomy => this[SkillName.Anatomy];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill AnimalLore => this[SkillName.AnimalLore];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill ItemID => this[SkillName.ItemID];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill ArmsLore => this[SkillName.ArmsLore];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Parry => this[SkillName.Parry];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Begging => this[SkillName.Begging];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Blacksmith => this[SkillName.Blacksmith];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Fletching => this[SkillName.Fletching];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Peacemaking => this[SkillName.Peacemaking];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Camping => this[SkillName.Camping];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Carpentry => this[SkillName.Carpentry];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Cartography => this[SkillName.Cartography];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Cooking => this[SkillName.Cooking];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill DetectHidden => this[SkillName.DetectHidden];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Discordance => this[SkillName.Discordance];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill EvalInt => this[SkillName.EvalInt];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Healing => this[SkillName.Healing];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Fishing => this[SkillName.Fishing];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Forensics => this[SkillName.Forensics];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Herding => this[SkillName.Herding];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Hiding => this[SkillName.Hiding];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Provocation => this[SkillName.Provocation];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Inscribe => this[SkillName.Inscribe];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Lockpicking => this[SkillName.Lockpicking];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Magery => this[SkillName.Magery];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill MagicResist => this[SkillName.MagicResist];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Tactics => this[SkillName.Tactics];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Snooping => this[SkillName.Snooping];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Musicianship => this[SkillName.Musicianship];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Poisoning => this[SkillName.Poisoning];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Archery => this[SkillName.Archery];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill SpiritSpeak => this[SkillName.SpiritSpeak];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Stealing => this[SkillName.Stealing];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Tailoring => this[SkillName.Tailoring];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill AnimalTaming => this[SkillName.AnimalTaming];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill TasteID => this[SkillName.TasteID];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Tinkering => this[SkillName.Tinkering];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Tracking => this[SkillName.Tracking];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Veterinary => this[SkillName.Veterinary];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Swords => this[SkillName.Swords];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Macing => this[SkillName.Macing];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Fencing => this[SkillName.Fencing];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Wrestling => this[SkillName.Wrestling];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Lumberjacking => this[SkillName.Lumberjacking];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Mining => this[SkillName.Mining];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Meditation => this[SkillName.Meditation];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Stealth => this[SkillName.Stealth];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill RemoveTrap => this[SkillName.RemoveTrap];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Necromancy => this[SkillName.Necromancy];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Focus => this[SkillName.Focus];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Chivalry => this[SkillName.Chivalry];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Bushido => this[SkillName.Bushido];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Ninjitsu => this[SkillName.Ninjitsu];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Spellweaving => this[SkillName.Spellweaving];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Mysticism => this[SkillName.Mysticism];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Imbuing => this[SkillName.Imbuing];
[CommandProperty(AccessLevel.Counselor, canModify: true)]
public Skill Throwing => this[SkillName.Throwing];
public override string ToString() => "...";
public static bool UseSkill(Mobile from, SkillName name) => UseSkill(from, (int)name);
public static bool UseSkill(Mobile from, int skillID)
{
if (!from.CheckAlive())
{
return false;
}
if (!from.Region.OnSkillUse(from, skillID))
{
return false;
}
if (!from.AllowSkillUse((SkillName)skillID))
{
return false;
}
if (skillID >= 0 && skillID < SkillInfo.Table.Length)
{
var info = SkillInfo.Table[skillID];
if (info.Callback != null)
{
if (Core.TickCount - from.NextSkillTime >= 0 && from.Spell == null)
{
from.DisruptiveAction();
from.NextSkillTime = Core.TickCount + (int)info.Callback(from).TotalMilliseconds;
return true;
}
from.SendSkillMessage();
}
else
{
from.SendLocalizedMessage(500014); // That skill cannot be used directly.
}
}
return false;
}
public void Serialize(IGenericWriter writer)
{
Total = 0;
writer.Write(3); // version
writer.Write(Cap);
writer.Write(m_Skills.Length);
for (var i = 0; i < m_Skills.Length; ++i)
{
var sk = m_Skills[i];
if (sk == null)
{
writer.Write((byte)0xFF);
}
else
{
sk.Serialize(writer);
Total += sk.BaseFixedPoint;
}
}
}
public void OnSkillChange(Skill skill)
{
if (skill == m_Highest) // could be downgrading the skill, force a recalc
{
m_Highest = null;
}
else if (skill.BaseFixedPoint > m_Highest?.BaseFixedPoint)
{
m_Highest = skill;
}
Owner.OnSkillInvalidated(skill);
Owner.NetState.SendSkillChange(skill);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator() => new(m_Skills);
public ref struct Enumerator
{
private readonly Skill[] _skills;
private int _index;
private Skill _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Enumerator(Skill[] skills)
{
_skills = skills;
_index = 0;
_current = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
Skill[] localList = _skills;
while ((uint)_index < (uint)localList.Length)
{
_current = localList[_index++];
if (_current != null)
{
return true;
}
}
return false;
}
public Skill Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _current;
}
}
}