This commit is contained in:
Bohica 2026-08-24 08:13:30 -05:00 committed by GitHub
commit 0a4d92402b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 69 additions and 37 deletions

View file

@ -0,0 +1,9 @@
{
"MagerySpell": {
"manaPerCircle": [4, 6, 9, 11, 14, 20, 40, 50],
"skillTable": [-50.0, -30.0, 0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0],
"skillTableML": [-46.0, -32.0, -18.0, -4.0, 10.0, 24.0, 38.0, 52.0, 66.0, 80.0],
"skillCheckWindow": 40.0,
"castDelay": 1.0
}
}

View file

@ -1,53 +1,50 @@
using System; using System;
using System.IO;
using Server.Items; using Server.Items;
using Server.Json;
namespace Server.Spells namespace Server.Spells
{ {
public abstract class MagerySpell : Spell public abstract class MagerySpell : Spell
{ {
private static readonly int[] _manaTable = { 4, 6, 9, 11, 14, 20, 40, 50 }; // Mana costs per spell circle
public static int[] ManaPerCircle { get; set; } = { 4, 6, 9, 11, 14, 20, 40, 50 };
/* // Minimum skill required per circle (scrolls use Circle+2) for non-ML core (10 entries)
* Starts at Circle -2 to account for scrolls public static double[] SkillTable { get; set; } = { -50.0, -30.0, 0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0 };
* Mana requirements formula: (14 * (circle - 1)) + 2 = 50% probability public static double[] SkillTableML { get; set; } = { -46.0, -32.0, -18.0, -4.0, 10.0, 24.0, 38.0, 52.0, 66.0, 80.0 };
* Add or subtract 20 for max or min limits
*/
private static readonly double[] _requiredSkill = Core.ML ?
new[] { -46.0, -32.0, -18.0, -4.0, 10.0, 24.0, 38.0, 52.0, 66.0, 80.0 } :
new[] { -50.0, -30.0, 0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0 };
public MagerySpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) public static double[] RequiredSkill => Core.ML ? SkillTableML : SkillTable;
{
} // Skill check window for casting a spell successfully
public static double SkillCheckWindow { get; set; } = 40.0;
// Cast delay per tick (seconds)
public static double CastDelay { get; set; } = 1.0;
public MagerySpell(Mobile caster, Item scroll, SpellInfo info) : base(caster, scroll, info) { }
public abstract SpellCircle Circle { get; } public abstract SpellCircle Circle { get; }
public override TimeSpan CastDelayBase => TimeSpan.FromSeconds((3 + (int)Circle) * CastDelaySecondsPerTick); public override TimeSpan CastDelayBase =>
TimeSpan.FromSeconds((3 + (int)Circle) * CastDelay);
public override bool ConsumeReagents() => public override bool ConsumeReagents() =>
base.ConsumeReagents() || ArcaneGem.ConsumeCharges(Caster, Core.SE ? 1 : 1 + (int)Circle); base.ConsumeReagents() || ArcaneGem.ConsumeCharges(Caster, Core.SE ? 1 : 1 + (int)Circle);
public override void GetCastSkills(out double min, out double max) public override void GetCastSkills(out double min, out double max)
{ {
// Original RunUO algorithm for required skill // Uses scrolls if present, otherwise use spellbook
// const double chanceOffset = 20.0 var skillIndex = (int)(Scroll == null ? Circle + 2 : Circle);
// const double chanceLength = 100.0 / 7.0 min = RequiredSkill[skillIndex];
// var avg = chanceLength * circle; max = min + SkillCheckWindow;
// min = avg - chanceOffset;
// max = avg + chanceOffset;
// Correct algorithm according to OSI for UOR/UOML
// TODO: Verify this algorithm on OSI for latest expansion.
min = _requiredSkill[(int)(Scroll == null ? Circle + 2 : Circle)];
max = min + 40;
} }
public override int GetMana() => Scroll is BaseWand ? 0 : _manaTable[(int)Circle]; public override int GetMana() => Scroll is BaseWand ? 0 : ManaPerCircle[(int)Circle];
public override double GetResistSkill(Mobile m) public override double GetResistSkill(Mobile m)
{ {
var circle = (int)Circle; var circle = (int)Circle;
var maxSkill = 1 + circle * 10 + (1 + circle / 6) * 25; var maxSkill = 1 + circle * 10 + (1 + circle / 6) * 25;
if (m.Skills.MagicResist.Value < maxSkill) if (m.Skills.MagicResist.Value < maxSkill)
@ -60,20 +57,18 @@ namespace Server.Spells
public virtual bool CheckResisted(Mobile target) public virtual bool CheckResisted(Mobile target)
{ {
var n = GetResistPercent(target) / 100.0; var resistChance = GetResistPercent(target) / 100.0;
if (n <= 0.0) if (resistChance <= 0.0)
{ {
return false; return false;
} }
if (n >= 1.0) if (resistChance >= 1.0)
{ {
return true; return true;
} }
// Even though this calculation matches AOS+, we don't combine with GetResistSkills because of an assumption
// about how it is used.
var circle = (int)Circle; var circle = (int)Circle;
var maxSkill = (1 + circle) * 10 + (1 + circle / 6) * 25; var maxSkill = (1 + circle) * 10 + (1 + circle / 6) * 25;
@ -82,18 +77,19 @@ namespace Server.Spells
target.CheckSkill(SkillName.MagicResist, 0.0, target.Skills.MagicResist.Cap); target.CheckSkill(SkillName.MagicResist, 0.0, target.Skills.MagicResist.Cap);
} }
return n >= Utility.RandomDouble(); return resistChance >= Utility.RandomDouble();
} }
public virtual double GetResistPercentForCircle(Mobile target, SpellCircle circle) public virtual double GetResistPercentForCircle(Mobile target, SpellCircle circle)
{ {
var magicResist = target.Skills.MagicResist.Value; var magicResist = target.Skills.MagicResist.Value;
var firstPercent = magicResist / 5.0; var casterSkill = Caster.Skills[CastSkill].Value;
var secondPercent = magicResist -
((Caster.Skills[CastSkill].Value - 20.0) / 5.0 + (1 + (int)circle) * 5.0);
// Seems should be about half of what stratics says. var firstPercent = magicResist / 5.0;
return (firstPercent > secondPercent ? firstPercent : secondPercent) / 2.0; var secondPercent = magicResist - ((casterSkill - 20.0) / 5.0 + (1 + (int)circle) * 5.0);
// Uses the higher of the two, then halves it
return Math.Max(firstPercent, secondPercent) / 2.0;
} }
public virtual double GetResistPercent(Mobile target) => GetResistPercentForCircle(target, Circle); public virtual double GetResistPercent(Mobile target) => GetResistPercentForCircle(target, Circle);
@ -112,5 +108,32 @@ namespace Server.Spells
return base.GetCastDelay(); return base.GetCastDelay();
} }
public class MagerySpellConfig
{
public int[] ManaPerCircle { get; set; }
public double[] SkillTable { get; set; }
public double[] SkillTableML { get; set; }
public double? SkillCheckWindow { get; set; }
public double? CastDelay { get; set; }
}
private const string _configPath = "Data/magery-spell.json";
public static void Configure()
{
var path = Path.Combine(Core.BaseDirectory, _configPath);
if (File.Exists(path))
{
var config = JsonConfig.Deserialize<MagerySpellConfig>(path);
if (config.ManaPerCircle != null) { ManaPerCircle = config.ManaPerCircle; }
if (config.SkillTable != null) { SkillTable = config.SkillTable; }
if (config.SkillTableML != null) { SkillTableML = config.SkillTableML; }
if (config.SkillCheckWindow.HasValue) { SkillCheckWindow = config.SkillCheckWindow.Value; }
if (config.CastDelay.HasValue) { CastDelay = config.CastDelay.Value; }
}
}
} }
} }