From cf3efaa2b76bfd8f04b5b97f0bc53ee28b43c0c6 Mon Sep 17 00:00:00 2001 From: Joe Valentine Date: Sun, 15 Mar 2026 20:04:24 -0400 Subject: [PATCH] feat: Implements Throwing skill, weapons, artifacts, and crafting (SA) Interim Commit. TODO: NPC/creature work. - Adds BaseThrown with close-quarters/range/shield hit chance modifiers, returning weapon effect, STR-scaled max range, and overthrow penalty - Adds Boomerang, Cyclone, and SoulGlaive (Gargoyle-only throwing weapons) - Adds Bladeweaver NPC vendor selling throwing weapons and ammo - Adds DefBlacksmithy recipes for all three throwing weapons (Core.SA) - Adds SA weapon type arrays to Loot (SAWeaponTypes, SARangedWeaponTypes) with isStygian parameter on random weapon/ranged/jewelry helpers - Adds AbyssReaver, BansheesCall, RaptorClaw, StoneSlithClaw, StormCaller, ValkyriesGlaive, WindOfCorruption throwing weapon artifacts - Fixes MovingShot BaseMana (15 -> 20) and TOL-conditional accuracy penalty - Adds 58 unit tests covering weapon properties, race restrictions, hit chance math, artifact properties, and loot table contents --- .../Items/Weapons/Throwing/ThrowingTests.cs | 767 ++++++++++++++++++ .../UOContent/Engines/Craft/DefBlacksmithy.cs | 12 + .../Items/Weapons/Abilities/MovingShot.cs | 4 +- .../UOContent/Items/Weapons/BaseWeapon.cs | 9 + .../Items/Weapons/Throwing/AbyssReaver.cs | 17 + .../Items/Weapons/Throwing/BansheesCall.cs | 25 + .../Items/Weapons/Throwing/BaseThrown.cs | 61 +- .../Items/Weapons/Throwing/Boomerang.cs | 2 + .../Items/Weapons/Throwing/Cyclone.cs | 2 +- .../Items/Weapons/Throwing/RaptorClaw.cs | 23 + .../Items/Weapons/Throwing/SoulGlaive.cs | 2 + .../Items/Weapons/Throwing/StoneSlithClaw.cs | 23 + .../Items/Weapons/Throwing/StormCaller.cs | 28 + .../Items/Weapons/Throwing/ValkyriesGlaive.cs | 24 + .../Weapons/Throwing/WindOfCorruption.cs | 24 + .../Server.Items.AbyssReaver.v0.json | 4 + .../Server.Items.BansheesCall.v0.json | 4 + .../Server.Items.RaptorClaw.v0.json | 4 + .../Server.Items.StoneSlithClaw.v0.json | 4 + .../Server.Items.StormCaller.v0.json | 4 + .../Server.Items.ValkyriesGlaive.v0.json | 4 + .../Server.Items.WindOfCorruption.v0.json | 4 + .../Server.Mobiles.Bladeweaver.v0.json | 4 + Projects/UOContent/Misc/Loot.cs | 61 +- .../Mobiles/Vendors/NPC/Bladeweaver.cs | 37 + .../SBInfo/Weapons/SBBladeweaverWeapon.cs | 31 + 26 files changed, 1175 insertions(+), 9 deletions(-) create mode 100644 Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs create mode 100644 Projects/UOContent/Items/Weapons/Throwing/AbyssReaver.cs create mode 100644 Projects/UOContent/Items/Weapons/Throwing/BansheesCall.cs create mode 100644 Projects/UOContent/Items/Weapons/Throwing/RaptorClaw.cs create mode 100644 Projects/UOContent/Items/Weapons/Throwing/StoneSlithClaw.cs create mode 100644 Projects/UOContent/Items/Weapons/Throwing/StormCaller.cs create mode 100644 Projects/UOContent/Items/Weapons/Throwing/ValkyriesGlaive.cs create mode 100644 Projects/UOContent/Items/Weapons/Throwing/WindOfCorruption.cs create mode 100644 Projects/UOContent/Migrations/Server.Items.AbyssReaver.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Items.BansheesCall.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Items.RaptorClaw.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Items.StoneSlithClaw.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Items.StormCaller.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Items.ValkyriesGlaive.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Items.WindOfCorruption.v0.json create mode 100644 Projects/UOContent/Migrations/Server.Mobiles.Bladeweaver.v0.json create mode 100644 Projects/UOContent/Mobiles/Vendors/NPC/Bladeweaver.cs create mode 100644 Projects/UOContent/Mobiles/Vendors/SBInfo/Weapons/SBBladeweaverWeapon.cs diff --git a/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs b/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs new file mode 100644 index 000000000..682e271ef --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs @@ -0,0 +1,767 @@ +using System; +using Server; +using Server.Items; +using Server.Mobiles; +using Server.Tests; +using Xunit; + +namespace UOContent.Tests; + +[Collection("Sequential UOContent Tests")] +public class ThrowingTests +{ + // Exposes protected ModifyHitChance so the math can be verified directly. + private class TestThrown : BaseThrown + { + public TestThrown() : base(0x8FF) { } + + public override int MinThrowRange => 4; + + public double TestModifyHitChance(Mobile attacker, Mobile defender, double chance) => + ModifyHitChance(attacker, defender, chance); + } + + // Weapon properties + + [Theory] + [InlineData(typeof(Boomerang))] + [InlineData(typeof(Cyclone))] + [InlineData(typeof(SoulGlaive))] + public void ThrowingWeapon_AccuracySkill_IsThrowingSkill(Type weaponType) + { + var weapon = (BaseThrown)Activator.CreateInstance(weaponType); + try + { + Assert.Equal(SkillName.Throwing, weapon.AccuracySkill); + } + finally + { + weapon.Delete(); + } + } + + [Theory] + [InlineData(typeof(Boomerang))] + [InlineData(typeof(Cyclone))] + [InlineData(typeof(SoulGlaive))] + public void ThrowingWeapon_DefSkill_IsThrowingSkill(Type weaponType) + { + var weapon = (BaseThrown)Activator.CreateInstance(weaponType); + try + { + Assert.Equal(SkillName.Throwing, weapon.DefSkill); + } + finally + { + weapon.Delete(); + } + } + + [Theory] + [InlineData(typeof(Boomerang), 4, 7)] + [InlineData(typeof(Cyclone), 6, 9)] + [InlineData(typeof(SoulGlaive), 8, 11)] + public void ThrowingWeapon_ThrowRange_IsCorrect(Type weaponType, int expectedMin, int expectedMax) + { + var weapon = (BaseThrown)Activator.CreateInstance(weaponType); + try + { + Assert.Equal(expectedMin, weapon.MinThrowRange); + Assert.Equal(expectedMax, weapon.MaxThrowRange); + } + finally + { + weapon.Delete(); + } + } + + // Race restriction + + [Theory] + [InlineData(typeof(Boomerang))] + [InlineData(typeof(Cyclone))] + [InlineData(typeof(SoulGlaive))] + public void ThrowingWeapon_RequiredRaces_IsGargoylesOnly(Type weaponType) + { + var weapon = (BaseThrown)Activator.CreateInstance(weaponType); + try + { + Assert.Equal(Race.AllowGargoylesOnly, weapon.RequiredRaces); + } + finally + { + weapon.Delete(); + } + } + + [Theory] + [InlineData(typeof(Boomerang))] + [InlineData(typeof(Cyclone))] + [InlineData(typeof(SoulGlaive))] + public void ThrowingWeapon_CheckRace_AllowsGargoyle(Type weaponType) + { + var weapon = (BaseThrown)Activator.CreateInstance(weaponType); + var mobile = CreateMobile(Map.Felucca, new Point3D(5700, 500, 0)); + try + { + mobile.Race = Race.Gargoyle; + Assert.True(weapon.CheckRace(mobile, message: false)); + } + finally + { + weapon.Delete(); + mobile.Delete(); + } + } + + [Theory] + [InlineData(typeof(Boomerang))] + [InlineData(typeof(Cyclone))] + [InlineData(typeof(SoulGlaive))] + public void ThrowingWeapon_CheckRace_BlocksHuman(Type weaponType) + { + var weapon = (BaseThrown)Activator.CreateInstance(weaponType); + var mobile = CreateMobile(Map.Felucca, new Point3D(5720, 500, 0)); + try + { + mobile.Race = Race.Human; + Assert.False(weapon.CheckRace(mobile, message: false)); + } + finally + { + weapon.Delete(); + mobile.Delete(); + } + } + + [Theory] + [InlineData(typeof(Boomerang))] + [InlineData(typeof(Cyclone))] + [InlineData(typeof(SoulGlaive))] + public void ThrowingWeapon_CheckRace_BlocksElf(Type weaponType) + { + var weapon = (BaseThrown)Activator.CreateInstance(weaponType); + var mobile = CreateMobile(Map.Felucca, new Point3D(5740, 500, 0)); + try + { + mobile.Race = Race.Elf; + Assert.False(weapon.CheckRace(mobile, message: false)); + } + finally + { + weapon.Delete(); + mobile.Delete(); + } + } + + // AbyssReaver + + [Fact] + public void AbyssReaver_IsInstanceOfCyclone() + { + var weapon = new AbyssReaver(); + try + { + Assert.IsType(weapon, exactMatch: false); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void AbyssReaver_LabelNumber_IsAbyssReaver() + { + var weapon = new AbyssReaver(); + try + { + Assert.Equal(1112694, weapon.LabelNumber); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void AbyssReaver_Slayer_IsExorcism() + { + var weapon = new AbyssReaver(); + try + { + Assert.Equal(SlayerName.Exorcism, weapon.Slayer); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void AbyssReaver_WeaponDamage_IsInRange() + { + var weapon = new AbyssReaver(); + try + { + Assert.InRange(weapon.Attributes.WeaponDamage, 25, 35); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void AbyssReaver_ThrowingSkillBonus_IsInRange() + { + var weapon = new AbyssReaver(); + try + { + weapon.SkillBonuses.GetValues(0, out var skill, out var bonus); + Assert.Equal(SkillName.Throwing, skill); + Assert.InRange(bonus, 5.0, 10.0); + } + finally + { + weapon.Delete(); + } + } + + // Hit chance modifiers + + /// At optimal range with no shield the chance should not change. + [Fact] + public void ModifyHitChance_OptimalRange_NoShield_NoChange() + { + var map = Map.Felucca; + var attacker = CreateMobile(map, new Point3D(5000, 500, 0)); + var defender = CreateMobile(map, new Point3D(5005, 500, 0)); // distance 5, within range 4..7 + var weapon = new TestThrown(); + + try + { + attacker.Skills.Throwing.BaseFixedPoint = 0; + attacker.RawDex = 10; + + var result = weapon.TestModifyHitChance(attacker, defender, 0.8); + + Assert.Equal(0.8, result, 10); + } + finally + { + attacker.Delete(); + defender.Delete(); + weapon.Delete(); + } + } + + /// + /// At distance 1 with no throwing skill and minimum dex, close to the full -12% applies. + /// Elf race avoids the Human Jack-of-All-Trades 20.0 skill floor. + /// RawDex clamps to 1 minimum, so mitigation = (0+1)/20 = 0.05 -> penalty = 0.1195. + /// + [Fact] + public void ModifyHitChance_CloseQuarters_MinimumSkill_AppliesNearFullPenalty() + { + var map = Map.Felucca; + var attacker = CreateMobile(map, new Point3D(5100, 500, 0)); + var defender = CreateMobile(map, new Point3D(5101, 500, 0)); // distance 1 + var weapon = new TestThrown(); + + try + { + attacker.Race = Race.Elf; // humans have Jack-of-All-Trades (+20.0 floor on all skills) + attacker.Skills.Throwing.BaseFixedPoint = 0; + attacker.RawDex = 1; // minimum + + var result = weapon.TestModifyHitChance(attacker, defender, 0.8); + + Assert.Equal(0.6805, result, 10); + } + finally + { + attacker.Delete(); + defender.Delete(); + weapon.Delete(); + } + } + + /// At distance 0 with 120 skill and 120 dex, (120+120)/20 = 12 caps mitigation, no penalty. + [Fact] + public void ModifyHitChance_CloseQuarters_MaxSkillAndDex_NoChange() + { + var map = Map.Felucca; + var attacker = CreateMobile(map, new Point3D(5200, 500, 0)); + var defender = CreateMobile(map, new Point3D(5200, 500, 0)); // same tile, distance 0 + var weapon = new TestThrown(); + + try + { + attacker.Skills.Throwing.BaseFixedPoint = 1200; // 120.0 + attacker.RawDex = 120; + + var result = weapon.TestModifyHitChance(attacker, defender, 0.8); + + Assert.Equal(0.8, result, 10); + } + finally + { + attacker.Delete(); + defender.Delete(); + weapon.Delete(); + } + } + + /// Distance 2 is above melee range but below MinThrowRange of 4, so the flat -12% applies. + [Fact] + public void ModifyHitChance_BelowMinRange_AppliesFlatPenalty() + { + var map = Map.Felucca; + var attacker = CreateMobile(map, new Point3D(5300, 500, 0)); + var defender = CreateMobile(map, new Point3D(5302, 500, 0)); // distance 2 + var weapon = new TestThrown(); + + try + { + var result = weapon.TestModifyHitChance(attacker, defender, 0.8); + + Assert.Equal(0.68, result, 10); + } + finally + { + attacker.Delete(); + defender.Delete(); + weapon.Delete(); + } + } + + /// + /// Shield at 100 Parry: 1200/100 = 12% penalty, chance * 0.88. + /// Layer must be set explicitly since tile data is not loaded in tests. + /// + [Fact] + public void ModifyHitChance_Shield_100Parry_AppliesCorrectPenalty() + { + var map = Map.Felucca; + var attacker = CreateMobile(map, new Point3D(5400, 500, 0)); + var defender = CreateMobile(map, new Point3D(5405, 500, 0)); // distance 5, optimal + var weapon = new TestThrown(); + var shield = new Buckler { Layer = Layer.TwoHanded }; // tile data not loaded in tests + + try + { + attacker.Skills.Parry.BaseFixedPoint = 1000; // 100.0 + attacker.AddItem(shield); + + var result = weapon.TestModifyHitChance(attacker, defender, 0.8); + + Assert.Equal(0.704, result, 10); + } + finally + { + attacker.Delete(); + defender.Delete(); + weapon.Delete(); + } + } + + /// Shield at 120 Parry: 1200/120 = 10% penalty, chance * 0.90. + [Fact] + public void ModifyHitChance_Shield_120Parry_AppliesCorrectPenalty() + { + var map = Map.Felucca; + var attacker = CreateMobile(map, new Point3D(5500, 500, 0)); + var defender = CreateMobile(map, new Point3D(5505, 500, 0)); // distance 5, optimal + var weapon = new TestThrown(); + var shield = new Buckler { Layer = Layer.TwoHanded }; + + try + { + attacker.Skills.Parry.BaseFixedPoint = 1200; // 120.0 + attacker.AddItem(shield); + + var result = weapon.TestModifyHitChance(attacker, defender, 0.8); + + Assert.Equal(0.72, result, 10); + } + finally + { + attacker.Delete(); + defender.Delete(); + weapon.Delete(); + } + } + + /// + /// Shield at 0 Parry hits the 90% cap, chance * 0.10. + /// Elf race prevents JOAT from raising Parry to 20.0. + /// + [Fact] + public void ModifyHitChance_Shield_ZeroParry_CapsAt90Percent() + { + var map = Map.Felucca; + var attacker = CreateMobile(map, new Point3D(5600, 500, 0)); + var defender = CreateMobile(map, new Point3D(5605, 500, 0)); // distance 5, optimal + var weapon = new TestThrown(); + var shield = new Buckler { Layer = Layer.TwoHanded }; + + try + { + attacker.Race = Race.Elf; // humans have Jack-of-All-Trades (+20.0 floor on all skills) + attacker.Skills.Parry.BaseFixedPoint = 0; + attacker.AddItem(shield); + + var result = weapon.TestModifyHitChance(attacker, defender, 0.8); + + Assert.Equal(0.08, result, 10); + } + finally + { + attacker.Delete(); + defender.Delete(); + weapon.Delete(); + } + } + + // Throwing artifacts + + [Fact] + public void ValkyriesGlaive_IsInstanceOfSoulGlaive() + { + var weapon = new ValkyriesGlaive(); + try + { + Assert.IsType(weapon, exactMatch: false); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void ValkyriesGlaive_LabelNumber_IsCorrect() + { + var weapon = new ValkyriesGlaive(); + try + { + Assert.Equal(1113531, weapon.LabelNumber); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void ValkyriesGlaive_Slayer_IsSilver() + { + var weapon = new ValkyriesGlaive(); + try + { + Assert.Equal(SlayerName.Silver, weapon.Slayer); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void ValkyriesGlaive_InitHits_Is255() + { + var weapon = new ValkyriesGlaive(); + try + { + Assert.Equal(255, weapon.InitMinHits); + Assert.Equal(255, weapon.InitMaxHits); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void BansheesCall_IsInstanceOfCyclone() + { + var weapon = new BansheesCall(); + try + { + Assert.IsType(weapon, exactMatch: false); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void BansheesCall_LabelNumber_IsCorrect() + { + var weapon = new BansheesCall(); + try + { + Assert.Equal(1113529, weapon.LabelNumber); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void BansheesCall_ElementDamage_IsColdOnly() + { + var weapon = new BansheesCall(); + try + { + Assert.Equal(100, weapon.AosElementDamages.Cold); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void StormCaller_IsInstanceOfBoomerang() + { + var weapon = new StormCaller(); + try + { + Assert.IsType(weapon, exactMatch: false); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void StormCaller_LabelNumber_IsCorrect() + { + var weapon = new StormCaller(); + try + { + Assert.Equal(1113530, weapon.LabelNumber); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void StormCaller_ElementDamages_AreEqualFiveSplit() + { + var weapon = new StormCaller(); + try + { + Assert.Equal(20, weapon.AosElementDamages.Physical); + Assert.Equal(20, weapon.AosElementDamages.Fire); + Assert.Equal(20, weapon.AosElementDamages.Cold); + Assert.Equal(20, weapon.AosElementDamages.Poison); + Assert.Equal(20, weapon.AosElementDamages.Energy); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void RaptorClaw_IsInstanceOfBoomerang() + { + var weapon = new RaptorClaw(); + try + { + Assert.IsType(weapon, exactMatch: false); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void RaptorClaw_LabelNumber_IsCorrect() + { + var weapon = new RaptorClaw(); + try + { + Assert.Equal(1112394, weapon.LabelNumber); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void RaptorClaw_Slayer_IsSilver() + { + var weapon = new RaptorClaw(); + try + { + Assert.Equal(SlayerName.Silver, weapon.Slayer); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void StoneSlithClaw_IsInstanceOfCyclone() + { + var weapon = new StoneSlithClaw(); + try + { + Assert.IsType(weapon, exactMatch: false); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void StoneSlithClaw_LabelNumber_IsCorrect() + { + var weapon = new StoneSlithClaw(); + try + { + Assert.Equal(1112393, weapon.LabelNumber); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void StoneSlithClaw_Slayer_IsDaemonDismissal() + { + var weapon = new StoneSlithClaw(); + try + { + Assert.Equal(SlayerName.DaemonDismissal, weapon.Slayer); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void WindOfCorruption_IsInstanceOfCyclone() + { + var weapon = new WindOfCorruption(); + try + { + Assert.IsType(weapon, exactMatch: false); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void WindOfCorruption_LabelNumber_IsCorrect() + { + var weapon = new WindOfCorruption(); + try + { + Assert.Equal(1150358, weapon.LabelNumber); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void WindOfCorruption_Slayer_IsFey() + { + var weapon = new WindOfCorruption(); + try + { + Assert.Equal(SlayerName.Fey, weapon.Slayer); + } + finally + { + weapon.Delete(); + } + } + + [Fact] + public void WindOfCorruption_ElementDamage_IsChaosOnly() + { + var weapon = new WindOfCorruption(); + try + { + Assert.Equal(100, weapon.AosElementDamages.Chaos); + } + finally + { + weapon.Delete(); + } + } + + // MovingShot + + /// BaseMana was raised from 15 to 20 to match the OSI reference. + [Fact] + public void MovingShot_BaseMana_Is20() + { + Assert.Equal(20, WeaponAbility.MovingShot.BaseMana); + } + + /// Pre-TOL accuracy penalty is -25; -35 applies only on TOL+ servers. + [Fact] + public void MovingShot_AccuracyBonus_IsNegative25_PreTOL() + { + if (!Core.TOL) + { + Assert.Equal(-25, WeaponAbility.MovingShot.AccuracyBonus); + } + } + + // Loot tables + + [Fact] + public void SAWeaponTypes_ContainsAllExpectedTypes() + { + Assert.Contains(typeof(DiscMace), Loot.SAWeaponTypes); + Assert.Contains(typeof(GargishTalwar), Loot.SAWeaponTypes); + Assert.Contains(typeof(DualPointedSpear), Loot.SAWeaponTypes); + Assert.Contains(typeof(GlassStaff), Loot.SAWeaponTypes); + Assert.Contains(typeof(DualShortAxes), Loot.SAWeaponTypes); + Assert.Contains(typeof(GlassSword), Loot.SAWeaponTypes); + } + + [Fact] + public void SARangedWeaponTypes_ContainsAllThrowingWeapons() + { + Assert.Contains(typeof(Boomerang), Loot.SARangedWeaponTypes); + Assert.Contains(typeof(Cyclone), Loot.SARangedWeaponTypes); + Assert.Contains(typeof(SoulGlaive), Loot.SARangedWeaponTypes); + } + + [Fact] + public void SARangedWeaponTypes_ContainsExactlyThreeEntries() + { + Assert.Equal(3, Loot.SARangedWeaponTypes.Length); + } + + private static PlayerMobile CreateMobile(Map map, Point3D location) + { + var mobile = new PlayerMobile(World.NewMobile); + mobile.DefaultMobileInit(); + mobile.MoveToWorld(location, map); + return mobile; + } +} diff --git a/Projects/UOContent/Engines/Craft/DefBlacksmithy.cs b/Projects/UOContent/Engines/Craft/DefBlacksmithy.cs index 58001d606..51b26876a 100644 --- a/Projects/UOContent/Engines/Craft/DefBlacksmithy.cs +++ b/Projects/UOContent/Engines/Craft/DefBlacksmithy.cs @@ -667,6 +667,18 @@ public class DefBlacksmithy : CraftSystem index = AddCraft(typeof(DragonChest), 1053114, 1029793, 85.0, 135.0, typeof(RedScales), 1060883, 36, 1060884); SetUseSubRes2(index, true); + if (Core.SA) + { + index = AddCraft(typeof(Boomerang), 1079508, 1095359, 75.0, 125.0, typeof(IronIngot), 1044036, 5, 1044037); + SetNeededExpansion(index, Expansion.SA); + + index = AddCraft(typeof(Cyclone), 1079508, 1095364, 75.0, 125.0, typeof(IronIngot), 1044036, 9, 1044037); + SetNeededExpansion(index, Expansion.SA); + + index = AddCraft(typeof(SoulGlaive), 1079508, 1095363, 75.0, 125.0, typeof(IronIngot), 1044036, 9, 1044037); + SetNeededExpansion(index, Expansion.SA); + } + // Set the overridable material SetSubRes(typeof(IronIngot), 1044022); diff --git a/Projects/UOContent/Items/Weapons/Abilities/MovingShot.cs b/Projects/UOContent/Items/Weapons/Abilities/MovingShot.cs index e0b543cfd..4f17dbd6f 100644 --- a/Projects/UOContent/Items/Weapons/Abilities/MovingShot.cs +++ b/Projects/UOContent/Items/Weapons/Abilities/MovingShot.cs @@ -6,8 +6,8 @@ namespace Server.Items /// public class MovingShot : WeaponAbility { - public override int BaseMana => 15; - public override int AccuracyBonus => -25; + public override int BaseMana => 20; + public override int AccuracyBonus => Core.TOL ? -35 : -25; public override bool ValidatesDuringHit => false; diff --git a/Projects/UOContent/Items/Weapons/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs index c746a83a2..3611d55cb 100644 --- a/Projects/UOContent/Items/Weapons/BaseWeapon.cs +++ b/Projects/UOContent/Items/Weapons/BaseWeapon.cs @@ -1357,6 +1357,8 @@ public abstract partial class BaseWeapon var chance = ourValue / (theirValue * 2.0) * 1.0 + (double)bonus / 100; + chance = ModifyHitChance(attacker, defender, chance); + if (Core.AOS && chance < 0.02) { chance = 0.02; @@ -1365,6 +1367,13 @@ public abstract partial class BaseWeapon return attacker.CheckSkill(atkSkill.SkillName, chance); } + /// + /// Allows subclasses to modify the final hit chance before the dice roll. + /// Called after all standard AOS bonuses are applied but before the 2% floor. + /// Supports both additive adjustments (chance -= 0.12) and multiplicative ones (chance *= scalar). + /// + protected virtual double ModifyHitChance(Mobile attacker, Mobile defender, double chance) => chance; + public virtual TimeSpan GetDelay(Mobile m) { double speed = Speed; diff --git a/Projects/UOContent/Items/Weapons/Throwing/AbyssReaver.cs b/Projects/UOContent/Items/Weapons/Throwing/AbyssReaver.cs new file mode 100644 index 000000000..1b88d6898 --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/AbyssReaver.cs @@ -0,0 +1,17 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class AbyssReaver : Cyclone +{ + [Constructible] + public AbyssReaver() + { + SkillBonuses.SetValues(0, SkillName.Throwing, Utility.RandomMinMax(5, 10)); + Attributes.WeaponDamage = Utility.RandomMinMax(25, 35); + Slayer = SlayerName.Exorcism; + } + + public override int LabelNumber => 1112694; // Abyss Reaver +} diff --git a/Projects/UOContent/Items/Weapons/Throwing/BansheesCall.cs b/Projects/UOContent/Items/Weapons/Throwing/BansheesCall.cs new file mode 100644 index 000000000..7c94abf53 --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/BansheesCall.cs @@ -0,0 +1,25 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class BansheesCall : Cyclone +{ + [Constructible] + public BansheesCall() + { + Hue = 1266; + Attributes.BonusStr = 5; + Attributes.WeaponSpeed = 30; + Attributes.WeaponDamage = 50; + WeaponAttributes.HitHarm = 40; + WeaponAttributes.HitLeechHits = 45; + Velocity = 35; + AosElementDamages.Cold = 100; + } + + public override int LabelNumber => 1113529; // Banshee's Call + + public override int InitMinHits => 255; + public override int InitMaxHits => 255; +} diff --git a/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs b/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs index 66821f385..d881bbdc9 100644 --- a/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs +++ b/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs @@ -14,6 +14,8 @@ public abstract partial class BaseThrown : BaseRanged public virtual int MaxThrowRange => MinThrowRange + 3; + // Dynamic max range scaled by attacker Strength. + // At StrReq the effective range equals MinThrowRange; at 140 Str it reaches MaxThrowRange. public override int DefMaxRange { get @@ -28,17 +30,68 @@ public abstract partial class BaseThrown : BaseRanged public override int EffectID => ItemID; + // Throwing weapons require no ammo — the weapon itself is the projectile. public override Type AmmoType => null; - public override Item Ammo => null; public override int DefHitSound => 0x5D3; public override int DefMissSound => 0x5D4; public override SkillName DefSkill => SkillName.Throwing; + public override SkillName AccuracySkill => SkillName.Throwing; public override WeaponAnimation DefAnimation => WeaponAnimation.Throwing; + // Throwing-specific hit chance modifiers (applied via BaseWeapon.ModifyHitChance hook). + protected override double ModifyHitChance(Mobile attacker, Mobile defender, double chance) + { + // Use Chebyshev distance — consistent with all UO range mechanics. + var distance = Math.Max( + Math.Abs(attacker.X - defender.X), + Math.Abs(attacker.Y - defender.Y) + ); + + if (distance <= 1) + { + // Close-quarters penalty: up to -12%, mitigated by (Throwing + Dex) / 20. + // At 240 combined (120 skill + 120 dex) the penalty is fully mitigated. + var throwSkill = attacker.Skills[SkillName.Throwing].Value; + var mitigation = Math.Min(12.0, (throwSkill + attacker.Dex) / 20.0); + chance -= (12.0 - mitigation) / 100.0; + } + else if (distance < MinThrowRange) + { + // Below minimum throw range (but not melee): flat -12% hit chance. + chance -= 0.12; + } + + // Shield penalty: equipping a shield while throwing reduces hit chance. + // Penalty = 1200 / Parry (capped at 90%), applied multiplicatively. + // High Parrying skill reduces the penalty significantly. + if (attacker.FindItemOnLayer(Layer.TwoHanded) != null) + { + var parry = attacker.Skills[SkillName.Parry].Value; + var penalty = parry > 0.0 ? 1200.0 / parry : 90.0; + chance *= 1.0 - Math.Min(90.0, penalty) / 100.0; + } + + return chance; + } + + // Overthrow penalty: -47% damage when target is beyond the attacker's current max range. + // MaxRange returns DefMaxRange (STR-scaled), so this reflects the dynamic per-attack value. + public override int ComputeDamage(Mobile attacker, Mobile defender) + { + var damage = base.ComputeDamage(attacker, defender); + + if (!attacker.InRange(defender.Location, MaxRange)) + { + damage = (int)(damage * 0.53); + } + + return damage; + } + public override bool OnFired(Mobile attacker, Mobile defender) { if (!attacker.InRange(defender, 1)) @@ -102,4 +155,10 @@ public abstract partial class BaseThrown : BaseRanged ); } } + + public override void GetProperties(IPropertyList list) + { + base.GetProperties(list); + list.Add(1149791, MinThrowRange); // Min Throw Range: ~1_val~ + } } diff --git a/Projects/UOContent/Items/Weapons/Throwing/Boomerang.cs b/Projects/UOContent/Items/Weapons/Throwing/Boomerang.cs index bae88c893..34d802cf7 100644 --- a/Projects/UOContent/Items/Weapons/Throwing/Boomerang.cs +++ b/Projects/UOContent/Items/Weapons/Throwing/Boomerang.cs @@ -19,4 +19,6 @@ public partial class Boomerang : BaseThrown public override float MlSpeed => 2.75f; public override int InitMinHits => 31; public override int InitMaxHits => 60; + + public override int RequiredRaces => Race.AllowGargoylesOnly; } diff --git a/Projects/UOContent/Items/Weapons/Throwing/Cyclone.cs b/Projects/UOContent/Items/Weapons/Throwing/Cyclone.cs index f2b6812c1..db9a14ec1 100644 --- a/Projects/UOContent/Items/Weapons/Throwing/Cyclone.cs +++ b/Projects/UOContent/Items/Weapons/Throwing/Cyclone.cs @@ -20,5 +20,5 @@ public partial class Cyclone : BaseThrown public override int InitMinHits => 31; public override int InitMaxHits => 60; - + public override int RequiredRaces => Race.AllowGargoylesOnly; } diff --git a/Projects/UOContent/Items/Weapons/Throwing/RaptorClaw.cs b/Projects/UOContent/Items/Weapons/Throwing/RaptorClaw.cs new file mode 100644 index 000000000..9b3ed6a10 --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/RaptorClaw.cs @@ -0,0 +1,23 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class RaptorClaw : Boomerang +{ + [Constructible] + public RaptorClaw() + { + Hue = 53; + Attributes.AttackChance = 12; + Attributes.WeaponSpeed = 30; + Attributes.WeaponDamage = 35; + Slayer = SlayerName.Silver; + WeaponAttributes.HitLeechStam = 40; + } + + public override int LabelNumber => 1112394; // Raptor Claw + + public override int InitMinHits => 255; + public override int InitMaxHits => 255; +} diff --git a/Projects/UOContent/Items/Weapons/Throwing/SoulGlaive.cs b/Projects/UOContent/Items/Weapons/Throwing/SoulGlaive.cs index 434d0c166..6a14de3c8 100644 --- a/Projects/UOContent/Items/Weapons/Throwing/SoulGlaive.cs +++ b/Projects/UOContent/Items/Weapons/Throwing/SoulGlaive.cs @@ -20,4 +20,6 @@ public partial class SoulGlaive : BaseThrown public override int InitMinHits => 31; public override int InitMaxHits => 65; + + public override int RequiredRaces => Race.AllowGargoylesOnly; } diff --git a/Projects/UOContent/Items/Weapons/Throwing/StoneSlithClaw.cs b/Projects/UOContent/Items/Weapons/Throwing/StoneSlithClaw.cs new file mode 100644 index 000000000..028253e0a --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/StoneSlithClaw.cs @@ -0,0 +1,23 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class StoneSlithClaw : Cyclone +{ + [Constructible] + public StoneSlithClaw() + { + Hue = 1150; + Attributes.WeaponSpeed = 25; + Attributes.WeaponDamage = 45; + Slayer = SlayerName.DaemonDismissal; + WeaponAttributes.HitHarm = 40; + WeaponAttributes.HitLowerDefend = 40; + } + + public override int LabelNumber => 1112393; // Stone Slith Claw + + public override int InitMinHits => 255; + public override int InitMaxHits => 255; +} diff --git a/Projects/UOContent/Items/Weapons/Throwing/StormCaller.cs b/Projects/UOContent/Items/Weapons/Throwing/StormCaller.cs new file mode 100644 index 000000000..6b729e6b5 --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/StormCaller.cs @@ -0,0 +1,28 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class StormCaller : Boomerang +{ + [Constructible] + public StormCaller() + { + Hue = 456; + Attributes.BonusStr = 5; + Attributes.WeaponSpeed = 30; + Attributes.WeaponDamage = 40; + WeaponAttributes.HitLightning = 40; + WeaponAttributes.HitLowerDefend = 30; + AosElementDamages.Physical = 20; + AosElementDamages.Fire = 20; + AosElementDamages.Cold = 20; + AosElementDamages.Poison = 20; + AosElementDamages.Energy = 20; + } + + public override int LabelNumber => 1113530; // Storm Caller + + public override int InitMinHits => 255; + public override int InitMaxHits => 255; +} diff --git a/Projects/UOContent/Items/Weapons/Throwing/ValkyriesGlaive.cs b/Projects/UOContent/Items/Weapons/Throwing/ValkyriesGlaive.cs new file mode 100644 index 000000000..41faf84e7 --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/ValkyriesGlaive.cs @@ -0,0 +1,24 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class ValkyriesGlaive : SoulGlaive +{ + [Constructible] + public ValkyriesGlaive() + { + Hue = 1651; + Attributes.SpellChanneling = 1; + Attributes.BonusStr = 5; + Attributes.WeaponSpeed = 20; + Attributes.WeaponDamage = 20; + Slayer = SlayerName.Silver; + WeaponAttributes.HitFireball = 40; + } + + public override int LabelNumber => 1113531; // Valkyrie's Glaive + + public override int InitMinHits => 255; + public override int InitMaxHits => 255; +} diff --git a/Projects/UOContent/Items/Weapons/Throwing/WindOfCorruption.cs b/Projects/UOContent/Items/Weapons/Throwing/WindOfCorruption.cs new file mode 100644 index 000000000..a7e235df2 --- /dev/null +++ b/Projects/UOContent/Items/Weapons/Throwing/WindOfCorruption.cs @@ -0,0 +1,24 @@ +using ModernUO.Serialization; + +namespace Server.Items; + +[SerializationGenerator(0)] +public partial class WindOfCorruption : Cyclone +{ + [Constructible] + public WindOfCorruption() + { + Hue = 1171; + Attributes.WeaponSpeed = 30; + Attributes.WeaponDamage = 50; + Slayer = SlayerName.Fey; + WeaponAttributes.HitLeechStam = 40; + WeaponAttributes.HitLowerDefend = 40; + AosElementDamages.Chaos = 100; + } + + public override int LabelNumber => 1150358; // Wind of Corruption + + public override int InitMinHits => 255; + public override int InitMaxHits => 255; +} diff --git a/Projects/UOContent/Migrations/Server.Items.AbyssReaver.v0.json b/Projects/UOContent/Migrations/Server.Items.AbyssReaver.v0.json new file mode 100644 index 000000000..7af2fdda6 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.AbyssReaver.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.AbyssReaver" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.BansheesCall.v0.json b/Projects/UOContent/Migrations/Server.Items.BansheesCall.v0.json new file mode 100644 index 000000000..1a3d9c627 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.BansheesCall.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.BansheesCall" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.RaptorClaw.v0.json b/Projects/UOContent/Migrations/Server.Items.RaptorClaw.v0.json new file mode 100644 index 000000000..8165371d4 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.RaptorClaw.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.RaptorClaw" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.StoneSlithClaw.v0.json b/Projects/UOContent/Migrations/Server.Items.StoneSlithClaw.v0.json new file mode 100644 index 000000000..f01d64da8 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.StoneSlithClaw.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.StoneSlithClaw" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.StormCaller.v0.json b/Projects/UOContent/Migrations/Server.Items.StormCaller.v0.json new file mode 100644 index 000000000..d69235cd9 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.StormCaller.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.StormCaller" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.ValkyriesGlaive.v0.json b/Projects/UOContent/Migrations/Server.Items.ValkyriesGlaive.v0.json new file mode 100644 index 000000000..227fe419a --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.ValkyriesGlaive.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.ValkyriesGlaive" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.WindOfCorruption.v0.json b/Projects/UOContent/Migrations/Server.Items.WindOfCorruption.v0.json new file mode 100644 index 000000000..37df005be --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.WindOfCorruption.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Items.WindOfCorruption" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Mobiles.Bladeweaver.v0.json b/Projects/UOContent/Migrations/Server.Mobiles.Bladeweaver.v0.json new file mode 100644 index 000000000..ac914418a --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Mobiles.Bladeweaver.v0.json @@ -0,0 +1,4 @@ +{ + "version": 0, + "type": "Server.Mobiles.Bladeweaver" +} \ No newline at end of file diff --git a/Projects/UOContent/Misc/Loot.cs b/Projects/UOContent/Misc/Loot.cs index b641400a2..3a43818f9 100644 --- a/Projects/UOContent/Misc/Loot.cs +++ b/Projects/UOContent/Misc/Loot.cs @@ -6,6 +6,17 @@ namespace Server { public static class Loot { + public static Type[] SAWeaponTypes { get; } = + { + typeof(DiscMace), typeof(GargishTalwar), typeof(DualPointedSpear), + typeof(GlassStaff), typeof(DualShortAxes), typeof(GlassSword) + }; + + public static Type[] SARangedWeaponTypes { get; } = + { + typeof(Boomerang), typeof(Cyclone), typeof(SoulGlaive) + }; + public static Type[] MLWeaponTypes { get; } = { typeof(AssassinSpike), typeof(DiamondMace), typeof(ElvenMachete), @@ -374,12 +385,18 @@ namespace Server return Construct(ClothingTypes); } + private static readonly Type[][] _saRangedWeaponTypes = [SARangedWeaponTypes, AosRangedWeaponTypes, RangedWeaponTypes]; private static readonly Type[][] _mlRangedWeaponTypes = [MLRangedWeaponTypes, AosRangedWeaponTypes, RangedWeaponTypes]; private static readonly Type[][] _seRangedWeaponTypes = [SERangedWeaponTypes, AosRangedWeaponTypes, RangedWeaponTypes]; private static readonly Type[][] _aosRangedWeaponTypes = [AosRangedWeaponTypes, RangedWeaponTypes]; - public static BaseWeapon RandomRangedWeapon(bool inTokuno = false, bool isMondain = false) + public static BaseWeapon RandomRangedWeapon(bool inTokuno = false, bool isMondain = false, bool isStygian = false) { + if (Core.SA && isStygian) + { + return Construct(_saRangedWeaponTypes); + } + if (Core.ML && isMondain) { return Construct(_mlRangedWeaponTypes); @@ -398,12 +415,18 @@ namespace Server return Construct(RangedWeaponTypes); } + private static readonly Type[][] _saWeaponTypes = [SAWeaponTypes, AosWeaponTypes, WeaponTypes]; private static readonly Type[][] _mlWeaponTypes = [MLWeaponTypes, AosWeaponTypes, WeaponTypes]; private static readonly Type[][] _seWeaponTypes = [SEWeaponTypes, AosWeaponTypes, WeaponTypes]; private static readonly Type[][] _aosWeaponTypes = [AosWeaponTypes, WeaponTypes]; - public static BaseWeapon RandomWeapon(bool inTokuno = false, bool isMondain = false) + public static BaseWeapon RandomWeapon(bool inTokuno = false, bool isMondain = false, bool isStygian = false) { + if (Core.SA && isStygian) + { + return Construct(_saWeaponTypes); + } + if (Core.ML && isMondain) { return Construct(_mlWeaponTypes); @@ -422,13 +445,19 @@ namespace Server return Construct(WeaponTypes); } + private static readonly Type[][] _saWeaponOrJewelryTypes = [SAWeaponTypes, AosWeaponTypes, WeaponTypes, JewelryTypes]; private static readonly Type[][] _mlWeaponOrJewelryTypes = [MLWeaponTypes, AosWeaponTypes, WeaponTypes, JewelryTypes]; private static readonly Type[][] _seWeaponOrJewelryTypes = [SEWeaponTypes, AosWeaponTypes, WeaponTypes, JewelryTypes]; private static readonly Type[][] _aosWeaponOrJewelryTypes = [AosWeaponTypes, WeaponTypes, JewelryTypes]; private static readonly Type[][] _oldWeaponOrJewelryTypes = [WeaponTypes, JewelryTypes]; - public static Item RandomWeaponOrJewelry(bool inTokuno = false, bool isMondain = false) + public static Item RandomWeaponOrJewelry(bool inTokuno = false, bool isMondain = false, bool isStygian = false) { + if (Core.SA && isStygian) + { + return Construct(_saWeaponOrJewelryTypes); + } + if (Core.ML && isMondain) { return Construct(_mlWeaponOrJewelryTypes); @@ -576,6 +605,12 @@ namespace Server return Construct(_oldArmorOrHatOrShieldOrJewelryTypes); } + private static readonly Type[][] _saWeaponOrRangedOrArmorOrHatOrShieldTypes = + [ + SAWeaponTypes, AosWeaponTypes, WeaponTypes, SARangedWeaponTypes, AosRangedWeaponTypes, RangedWeaponTypes, + ArmorTypes, AosHatTypes, HatTypes, AosShieldTypes, ShieldTypes + ]; + private static readonly Type[][] _mlWeaponOrRangedOrArmorOrHatOrShieldTypes = [ MLWeaponTypes, AosWeaponTypes, WeaponTypes, MLRangedWeaponTypes, AosRangedWeaponTypes, RangedWeaponTypes, @@ -599,8 +634,13 @@ namespace Server WeaponTypes, RangedWeaponTypes, ArmorTypes, HatTypes, ShieldTypes ]; - public static Item RandomArmorOrShieldOrWeapon(bool inTokuno = false, bool isMondain = false) + public static Item RandomArmorOrShieldOrWeapon(bool inTokuno = false, bool isMondain = false, bool isStygian = false) { + if (Core.SA && isStygian) + { + return Construct(_saWeaponOrRangedOrArmorOrHatOrShieldTypes); + } + if (Core.ML && isMondain) { return Construct(_mlWeaponOrRangedOrArmorOrHatOrShieldTypes); @@ -619,6 +659,12 @@ namespace Server return Construct(_oldWeaponOrRangedOrArmorOrHatOrShieldTypes); } + private static readonly Type[][] _saWeaponOrRangedOrArmorOrHatOrShieldOrJewelryTypes = + [ + SAWeaponTypes, AosWeaponTypes, WeaponTypes, SARangedWeaponTypes, AosRangedWeaponTypes, RangedWeaponTypes, + ArmorTypes, AosHatTypes, HatTypes, AosShieldTypes, ShieldTypes, JewelryTypes + ]; + private static readonly Type[][] _mlWeaponOrRangedOrArmorOrHatOrShieldOrJewelryTypes = [ MLWeaponTypes, AosWeaponTypes, WeaponTypes, MLRangedWeaponTypes, AosRangedWeaponTypes, RangedWeaponTypes, @@ -642,8 +688,13 @@ namespace Server WeaponTypes, RangedWeaponTypes, ArmorTypes, HatTypes, ShieldTypes, JewelryTypes ]; - public static Item RandomArmorOrShieldOrWeaponOrJewelry(bool inTokuno = false, bool isMondain = false) + public static Item RandomArmorOrShieldOrWeaponOrJewelry(bool inTokuno = false, bool isMondain = false, bool isStygian = false) { + if (Core.SA && isStygian) + { + return Construct(_saWeaponOrRangedOrArmorOrHatOrShieldOrJewelryTypes); + } + if (Core.ML && isMondain) { return Construct(_mlWeaponOrRangedOrArmorOrHatOrShieldOrJewelryTypes); diff --git a/Projects/UOContent/Mobiles/Vendors/NPC/Bladeweaver.cs b/Projects/UOContent/Mobiles/Vendors/NPC/Bladeweaver.cs new file mode 100644 index 000000000..5783c387b --- /dev/null +++ b/Projects/UOContent/Mobiles/Vendors/NPC/Bladeweaver.cs @@ -0,0 +1,37 @@ +using ModernUO.Serialization; +using System.Collections.Generic; +using Server.Items; + +namespace Server.Mobiles; + +[SerializationGenerator(0, false)] +public partial class Bladeweaver : BaseVendor +{ + private readonly List m_SBInfos = new(); + + [Constructible] + public Bladeweaver() : base("the bladeweaver") + { + SetSkill(SkillName.Throwing, 85.0, 100.0); + SetSkill(SkillName.Tactics, 85.0, 100.0); + } + + protected override List SBInfos => m_SBInfos; + + public override NpcGuild NpcGuild => NpcGuild.WarriorsGuild; + + public override void InitSBInfo() + { + m_SBInfos.Add(new SBBladeweaverWeapon()); + } + + public override void InitOutfit() + { + AddItem(new GargishLeatherArmsType1()); + AddItem(new GargishLeatherChestType1()); + AddItem(new GargishLeatherLegsType1()); + AddItem(new Boomerang()); + + PackGold(100, 200); + } +} diff --git a/Projects/UOContent/Mobiles/Vendors/SBInfo/Weapons/SBBladeweaverWeapon.cs b/Projects/UOContent/Mobiles/Vendors/SBInfo/Weapons/SBBladeweaverWeapon.cs new file mode 100644 index 000000000..6abd14d65 --- /dev/null +++ b/Projects/UOContent/Mobiles/Vendors/SBInfo/Weapons/SBBladeweaverWeapon.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Server.Items; + +namespace Server.Mobiles; + +public class SBBladeweaverWeapon : SBInfo +{ + public override IShopSellInfo SellInfo { get; } = new InternalSellInfo(); + + public override List BuyInfo { get; } = new InternalBuyInfo(); + + public class InternalBuyInfo : List + { + public InternalBuyInfo() + { + Add(new GenericBuyInfo(typeof(Boomerang), 250, 20, 0x8FF, 0)); + Add(new GenericBuyInfo(typeof(Cyclone), 350, 20, 0x901, 0)); + Add(new GenericBuyInfo(typeof(SoulGlaive), 500, 20, 0x090A, 0)); + } + } + + public class InternalSellInfo : GenericSellInfo + { + public InternalSellInfo() + { + Add(typeof(Boomerang), 125); + Add(typeof(Cyclone), 175); + Add(typeof(SoulGlaive), 250); + } + } +}