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
This commit is contained in:
parent
a28a32f46d
commit
cf3efaa2b7
26 changed files with 1175 additions and 9 deletions
|
|
@ -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<Cyclone>(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
|
||||
|
||||
/// <summary>At optimal range with no shield the chance should not change.</summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>At distance 0 with 120 skill and 120 dex, (120+120)/20 = 12 caps mitigation, no penalty.</summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Distance 2 is above melee range but below MinThrowRange of 4, so the flat -12% applies.</summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shield at 100 Parry: 1200/100 = 12% penalty, chance * 0.88.
|
||||
/// Layer must be set explicitly since tile data is not loaded in tests.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shield at 120 Parry: 1200/120 = 10% penalty, chance * 0.90.</summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shield at 0 Parry hits the 90% cap, chance * 0.10.
|
||||
/// Elf race prevents JOAT from raising Parry to 20.0.
|
||||
/// </summary>
|
||||
[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<SoulGlaive>(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<Cyclone>(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<Boomerang>(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<Boomerang>(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<Cyclone>(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<Cyclone>(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
|
||||
|
||||
/// <summary>BaseMana was raised from 15 to 20 to match the OSI reference.</summary>
|
||||
[Fact]
|
||||
public void MovingShot_BaseMana_Is20()
|
||||
{
|
||||
Assert.Equal(20, WeaponAbility.MovingShot.BaseMana);
|
||||
}
|
||||
|
||||
/// <summary>Pre-TOL accuracy penalty is -25; -35 applies only on TOL+ servers.</summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ namespace Server.Items
|
|||
/// </summary>
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
protected virtual double ModifyHitChance(Mobile attacker, Mobile defender, double chance) => chance;
|
||||
|
||||
public virtual TimeSpan GetDelay(Mobile m)
|
||||
{
|
||||
double speed = Speed;
|
||||
|
|
|
|||
17
Projects/UOContent/Items/Weapons/Throwing/AbyssReaver.cs
Normal file
17
Projects/UOContent/Items/Weapons/Throwing/AbyssReaver.cs
Normal file
|
|
@ -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
|
||||
}
|
||||
25
Projects/UOContent/Items/Weapons/Throwing/BansheesCall.cs
Normal file
25
Projects/UOContent/Items/Weapons/Throwing/BansheesCall.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<BaseShield>(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~
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
23
Projects/UOContent/Items/Weapons/Throwing/RaptorClaw.cs
Normal file
23
Projects/UOContent/Items/Weapons/Throwing/RaptorClaw.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
23
Projects/UOContent/Items/Weapons/Throwing/StoneSlithClaw.cs
Normal file
23
Projects/UOContent/Items/Weapons/Throwing/StoneSlithClaw.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
28
Projects/UOContent/Items/Weapons/Throwing/StormCaller.cs
Normal file
28
Projects/UOContent/Items/Weapons/Throwing/StormCaller.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
24
Projects/UOContent/Items/Weapons/Throwing/ValkyriesGlaive.cs
Normal file
24
Projects/UOContent/Items/Weapons/Throwing/ValkyriesGlaive.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Items.AbyssReaver.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Items.AbyssReaver.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Items.AbyssReaver"
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Items.BansheesCall.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Items.BansheesCall.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Items.BansheesCall"
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Items.RaptorClaw.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Items.RaptorClaw.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Items.RaptorClaw"
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Items.StoneSlithClaw.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Items.StoneSlithClaw.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Items.StoneSlithClaw"
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Items.StormCaller.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Items.StormCaller.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Items.StormCaller"
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Items.ValkyriesGlaive.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Items.ValkyriesGlaive.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Items.ValkyriesGlaive"
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Items.WindOfCorruption.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Items.WindOfCorruption.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Items.WindOfCorruption"
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Mobiles.Bladeweaver.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Mobiles.Bladeweaver.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Mobiles.Bladeweaver"
|
||||
}
|
||||
|
|
@ -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<BaseClothing>(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<BaseWeapon>(_saRangedWeaponTypes);
|
||||
}
|
||||
|
||||
if (Core.ML && isMondain)
|
||||
{
|
||||
return Construct<BaseWeapon>(_mlRangedWeaponTypes);
|
||||
|
|
@ -398,12 +415,18 @@ namespace Server
|
|||
return Construct<BaseWeapon>(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<BaseWeapon>(_saWeaponTypes);
|
||||
}
|
||||
|
||||
if (Core.ML && isMondain)
|
||||
{
|
||||
return Construct<BaseWeapon>(_mlWeaponTypes);
|
||||
|
|
@ -422,13 +445,19 @@ namespace Server
|
|||
return Construct<BaseWeapon>(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);
|
||||
|
|
|
|||
37
Projects/UOContent/Mobiles/Vendors/NPC/Bladeweaver.cs
Normal file
37
Projects/UOContent/Mobiles/Vendors/NPC/Bladeweaver.cs
Normal file
|
|
@ -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<SBInfo> 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<SBInfo> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<GenericBuyInfo> BuyInfo { get; } = new InternalBuyInfo();
|
||||
|
||||
public class InternalBuyInfo : List<GenericBuyInfo>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue