From 0bfbdd0764fa3781531f52a6dd4f2af63f5fd8b6 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Thu, 2 Jul 2026 21:06:11 -0700
Subject: [PATCH] feat(throwing): core gargoyle Throwing skill (SA) (#2510)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Supersedes #2376 (@jwvalentine). This is a reviewed, corrected, and scoped-down **Phase 1** of Joe Valentine's throwing implementation — his original commits are cherry-picked here with authorship preserved, plus a fix/scoping pass. Phase 1 lands only the **core gargoyle Throwing skill**; the incomplete content is excised for follow-up PRs (see below).
## What's included (core skill)
- `BaseThrown` combat mechanics on top of the existing skeleton: close-quarters penalty, below-min-range penalty, shield penalty, STR-scaled range, overthrow damage penalty.
- Base weapons: **Boomerang, Cyclone, SoulGlaive** (gargoyle-only), + blacksmith crafting (SA-gated).
- Two symmetric hooks on `BaseWeapon` (`ModifyHitChance`, new `ModifyDamage`) that are inert no-ops for every other weapon.
## Fixes over the original
- **Overthrow damage**: was dead code (the swing gate already guarantees you're within `MaxRange`, so the old `ComputeDamage` check never fired). Reimplemented as `finalDamage × 0.53` applied *after* all offensive bonuses via a new `ModifyDamage` hook, firing at the outer range ring.
- **`DefMaxRange`**: clamped to `[MinThrowRange, MaxThrowRange]` (uncapped before → e.g. range 13 at 200 Str) and guarded against a latent divide-by-zero.
- **Close-quarters mitigation** now uses `RawDex` (matches ServUO/OSI; deterministic under stat mods).
- **Return-throw timer** guarded against a deleted/unmapped thrower/target.
- Reverted the `MovingShot` change (it rebalanced archery — belongs in a separate PR).
- Kept only complex-logic tests (hit-chance/range/damage math); dropped property-value assertions.
## Excised for follow-up PRs (Phase 2/3)
7 named artifacts, the Into-the-Void quest + Agralem, GargishOutcast, the Bladeweaver vendor, and the SA loot tables — these were unwired/non-functional (loot never triggered, quest/creature never spawned) and will return properly wired. `StormCaller` also needs its missing Battle Lust, and the quest its correct void-creature target.
## Verification
- Build: 0 warnings / 0 errors.
- `UOContent.Tests`: **501/501** passing (the `ModifyDamage` hook causes zero regressions across all weapons).
- Full whole-branch review completed: no must-fix defects.
---
.../Items/Weapons/Throwing/ThrowingTests.cs | 390 ++++++++++++++++++
.../UOContent/Engines/Craft/DefBlacksmithy.cs | 12 +
.../UOContent/Items/Weapons/BaseWeapon.cs | 16 +
.../Items/Weapons/Throwing/BaseThrown.cs | 81 +++-
.../Items/Weapons/Throwing/Boomerang.cs | 2 +
.../Items/Weapons/Throwing/Cyclone.cs | 2 +-
.../Items/Weapons/Throwing/SoulGlaive.cs | 2 +
7 files changed, 497 insertions(+), 8 deletions(-)
create mode 100644 Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.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..da896ba8b
--- /dev/null
+++ b/Projects/UOContent.Tests/Tests/Items/Weapons/Throwing/ThrowingTests.cs
@@ -0,0 +1,390 @@
+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);
+
+ public int TestModifyDamage(Mobile attacker, Mobile defender, int damage) =>
+ ModifyDamage(attacker, defender, damage);
+ }
+
+ // 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();
+ }
+ }
+
+ // DefMaxRange STR scaling (uses SoulGlaive: StrReq 60, Min 8, Max 11)
+
+ [Fact]
+ public void DefMaxRange_AtStrReq_EqualsMinThrowRange()
+ {
+ var weapon = new SoulGlaive();
+ var attacker = CreateMobile(Map.Felucca, new Point3D(5800, 500, 0));
+ try
+ {
+ attacker.RawStr = 60; // == AosStrengthReq
+ attacker.AddItem(weapon);
+ Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 8
+ }
+ finally
+ {
+ weapon.Delete();
+ attacker.Delete();
+ }
+ }
+
+ [Fact]
+ public void DefMaxRange_At140Str_EqualsMaxThrowRange()
+ {
+ var weapon = new SoulGlaive();
+ var attacker = CreateMobile(Map.Felucca, new Point3D(5810, 500, 0));
+ try
+ {
+ attacker.RawStr = 140;
+ attacker.AddItem(weapon);
+ Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 11
+ }
+ finally
+ {
+ weapon.Delete();
+ attacker.Delete();
+ }
+ }
+
+ [Fact]
+ public void DefMaxRange_AboveMaxStr_IsCappedAtMaxThrowRange()
+ {
+ var weapon = new SoulGlaive();
+ var attacker = CreateMobile(Map.Felucca, new Point3D(5820, 500, 0));
+ try
+ {
+ attacker.RawStr = 200; // uncapped formula would give 13
+ attacker.AddItem(weapon);
+ Assert.Equal(weapon.MaxThrowRange, weapon.DefMaxRange); // 11, not 13
+ }
+ finally
+ {
+ weapon.Delete();
+ attacker.Delete();
+ }
+ }
+
+ [Fact]
+ public void DefMaxRange_BelowStrReq_FlooredAtMinThrowRange()
+ {
+ var weapon = new SoulGlaive();
+ var attacker = CreateMobile(Map.Felucca, new Point3D(5830, 500, 0));
+ try
+ {
+ attacker.RawStr = 10; // below StrReq 60
+ attacker.AddItem(weapon);
+ Assert.Equal(weapon.MinThrowRange, weapon.DefMaxRange); // 8, not 6
+ }
+ finally
+ {
+ weapon.Delete();
+ attacker.Delete();
+ }
+ }
+
+ /// At the outermost admissible tile (dist == MaxRange) a throw loses 47% damage.
+ [Fact]
+ public void ModifyDamage_AtMaxRange_Reduces47Percent()
+ {
+ var map = Map.Felucca;
+ var attacker = CreateMobile(map, new Point3D(5900, 500, 0));
+ var weapon = new TestThrown(); // MinThrowRange 4 -> MaxThrowRange 7
+ try
+ {
+ attacker.RawStr = 140; // MaxRange == MaxThrowRange == 7
+ attacker.AddItem(weapon);
+ var defender = CreateMobile(map, new Point3D(5907, 500, 0)); // distance 7 == MaxRange
+ try
+ {
+ Assert.Equal(53, weapon.TestModifyDamage(attacker, defender, 100));
+ }
+ finally
+ {
+ defender.Delete();
+ }
+ }
+ finally
+ {
+ weapon.Delete();
+ attacker.Delete();
+ }
+ }
+
+ /// Inside max range, damage is unchanged.
+ [Fact]
+ public void ModifyDamage_WithinRange_NoChange()
+ {
+ var map = Map.Felucca;
+ var attacker = CreateMobile(map, new Point3D(5920, 500, 0));
+ var weapon = new TestThrown();
+ try
+ {
+ attacker.RawStr = 140; // MaxRange 7
+ attacker.AddItem(weapon);
+ var defender = CreateMobile(map, new Point3D(5925, 500, 0)); // distance 5 < 7
+ try
+ {
+ Assert.Equal(100, weapon.TestModifyDamage(attacker, defender, 100));
+ }
+ finally
+ {
+ defender.Delete();
+ }
+ }
+ finally
+ {
+ weapon.Delete();
+ attacker.Delete();
+ }
+ }
+
+ /// Close-quarters mitigation uses RawDex, so a Dex debuff does not change the penalty.
+ [Fact]
+ public void ModifyHitChance_CloseQuarters_UsesRawDex_IgnoresStatMods()
+ {
+ var map = Map.Felucca;
+ var attacker = CreateMobile(map, new Point3D(5940, 500, 0));
+ var defender = CreateMobile(map, new Point3D(5941, 500, 0)); // distance 1
+ var weapon = new TestThrown();
+ try
+ {
+ attacker.Race = Race.Elf; // avoid Human JOAT skill floor
+ attacker.Skills.Throwing.BaseFixedPoint = 0;
+ attacker.RawDex = 40;
+ // RawDex-based mitigation = (0 + 40)/20 = 2.0 -> penalty = (12 - 2)/100 = 0.10
+ var baseline = weapon.TestModifyHitChance(attacker, defender, 0.8); // 0.70
+
+ attacker.AddStatMod(new StatMod(StatType.Dex, "curse", -30, TimeSpan.Zero)); // effective Dex 10
+ var withMod = weapon.TestModifyHitChance(attacker, defender, 0.8);
+
+ Assert.Equal(0.70, baseline, 10);
+ Assert.Equal(baseline, withMod, 10); // unchanged because RawDex is used
+ }
+ finally
+ {
+ attacker.Delete();
+ defender.Delete();
+ weapon.Delete();
+ }
+ }
+
+ 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/BaseWeapon.cs b/Projects/UOContent/Items/Weapons/BaseWeapon.cs
index 4501c0ee3..b28f0d371 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,19 @@ 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;
+
+ ///
+ /// Allows subclasses to modify the final post-bonus damage (e.g. range-based penalties)
+ /// before defender mitigation is applied. Mirrors .
+ ///
+ protected virtual int ModifyDamage(Mobile attacker, Mobile defender, int damage) => damage;
+
public virtual TimeSpan GetDelay(Mobile m)
{
double speed = Speed;
@@ -1874,6 +1889,7 @@ public abstract partial class BaseWeapon
percentageBonus = Math.Min(percentageBonus, 300);
damage = AOS.Scale(damage, 100 + percentageBonus);
+ damage = ModifyDamage(attacker, defender, damage);
var defLoc = new WorldLocation(defender);
var bcAtt = attacker as BaseCreature;
diff --git a/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs b/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs
index 66821f385..3905aea14 100644
--- a/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs
+++ b/Projects/UOContent/Items/Weapons/Throwing/BaseThrown.cs
@@ -14,31 +14,92 @@ public abstract partial class BaseThrown : BaseRanged
public virtual int MaxThrowRange => MinThrowRange + 3;
+ // Dynamic max range scaled by attacker Strength, clamped to the weapon's throw band.
+ // At StrReq the effective range equals MinThrowRange; at 140 Str it reaches MaxThrowRange.
public override int DefMaxRange
{
get
{
- var baseRange = MaxThrowRange;
+ if (Parent is not Mobile attacker)
+ {
+ return MaxThrowRange;
+ }
- return Parent is Mobile attacker
- ? baseRange - 3 + (attacker.Str - AosStrengthReq) / ((140 - AosStrengthReq) / 3)
- : baseRange;
+ var divisor = (140 - AosStrengthReq) / 3;
+ if (divisor <= 0)
+ {
+ return MaxThrowRange;
+ }
+
+ // Scale up from MinThrowRange so the base matches the clamp floor even if a
+ // subclass overrides MaxThrowRange to something other than MinThrowRange + 3.
+ var scaled = MinThrowRange + (attacker.Str - AosStrengthReq) / divisor;
+ return Math.Clamp(scaled, MinThrowRange, MaxThrowRange);
}
}
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 + RawDex) / 20.
+ // At 240 combined (120 skill + 120 RawDex) the penalty is fully mitigated.
+ var throwSkill = attacker.Skills[SkillName.Throwing].Value;
+ var mitigation = Math.Min(12.0, (throwSkill + attacker.RawDex) / 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: a throw that reaches the edge of its (STR-scaled) range lands with 47% less
+ // damage, applied on top of all offensive bonuses. MaxRange is the dynamic DefMaxRange.
+ protected override int ModifyDamage(Mobile attacker, Mobile defender, int damage)
+ {
+ if (!attacker.InRange(defender.Location, MaxRange - 1))
+ {
+ damage = damage * 53 / 100;
+ }
+
+ return damage;
+ }
+
public override bool OnFired(Mobile attacker, Mobile defender)
{
if (!attacker.InRange(defender, 1))
@@ -73,12 +134,12 @@ public abstract partial class BaseThrown : BaseRanged
public virtual void Return(Mobile thrower, Mobile target, WorldLocation worldLocation)
{
- if (thrower == null)
+ if (thrower?.Deleted != false || thrower.Map == null || thrower.Map == Map.Internal)
{
return;
}
- if (target != null)
+ if (target?.Deleted == false)
{
target.MovingEffect(thrower, EffectID, 18, 1, false, false, Hue, 0);
}
@@ -102,4 +163,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/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;
}