Merge branch 'main' into Refactor/MagerySpell.cs

This commit is contained in:
Bohica 2025-07-02 22:53:42 -07:00 committed by GitHub
commit 4648947f38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1519 additions and 1665 deletions

View file

@ -1,24 +0,0 @@
name: Qodana
on:
workflow_dispatch:
pull_request:
push:
branches: # Specify your branches here
- main # The 'main' branch
jobs:
qodana:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
checks: write
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
fetch-depth: 0 # a full history is required for pull request analysis
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2025.1
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}

View file

@ -294,13 +294,17 @@ public ref struct SpanReader
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Read(Span<byte> bytes)
public int Read(Span<byte> bytes)
{
if (bytes.Length < Length)
if (bytes.Length == 0)
{
throw new ArgumentOutOfRangeException(nameof(bytes));
return 0;
}
return _buffer.TryCopyTo(bytes);
var bytesWritten = Math.Min(bytes.Length, Remaining);
_buffer.Slice(Position, bytesWritten).CopyTo(bytes);
Position += bytesWritten;
return bytesWritten;
}
}

View file

@ -792,6 +792,7 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
public virtual void GetProperties(IPropertyList list)
{
AddNameProperties(list);
AppendChildNameProperties(list);
}
[IgnoreDupe]
@ -1943,8 +1944,6 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
{
AddQuestItemProperty(list);
}
AppendChildNameProperties(list);
}
/// <summary>

View file

@ -661,6 +661,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
}
}
[CommandProperty(AccessLevel.Administrator)]
public long NextActionTime { get; set; }
public long NextActionMessage { get; set; }
@ -675,6 +676,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
public virtual bool CanRegenStam => Alive;
public virtual bool CanRegenMana => Alive;
[CommandProperty(AccessLevel.Administrator)]
public long NextSkillTime { get; set; }
public List<AggressorInfo> Aggressors { get; private set; }

View file

@ -343,7 +343,28 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
return null;
}
public SecureTradeContainer FindTradeContainer(Mobile m) => FindTrade(m)?.From.Container;
public SecureTradeContainer FindTradeContainer(Mobile m)
{
for (var i = 0; i < Trades.Count; ++i)
{
var trade = Trades[i];
var from = trade.From;
var to = trade.To;
if (from.Mobile == Mobile && to.Mobile == m)
{
return from.Container;
}
if (from.Mobile == m && to.Mobile == Mobile)
{
return to.Container;
}
}
return null;
}
public SecureTradeContainer AddTrade(NetState state)
{

View file

@ -119,7 +119,7 @@ public class NameVerificationTests
public void Validate_TooManyExceptions_ReturnsFalse()
{
var exceptions = SearchValues.Create(' ', '-', '.');
Assert.False(NameVerification.Validate("A-B.C D-E", 2, 16, true, false, false, 3, exceptions));
Assert.False(NameVerification.Validate("A-B.C D-E", 2, 16, true, false, false, 1, exceptions));
}
[Fact]

View file

@ -279,53 +279,25 @@ namespace Server.Commands.Generic
}
case StringOperator.Equal:
{
if (m_IgnoreCase)
{
methodName = "InsensitiveEquals";
}
else
{
methodName = "EqualsOrdinal";
}
methodName = m_IgnoreCase ? "InsensitiveEquals" : "EqualsOrdinal";
break;
}
case StringOperator.Contains:
{
if (m_IgnoreCase)
{
methodName = "InsensitiveContains";
}
else
{
methodName = "ContainsOrdinal";
}
methodName = m_IgnoreCase ? "InsensitiveContains" : "ContainsOrdinal";
break;
}
case StringOperator.StartsWith:
{
if (m_IgnoreCase)
{
methodName = "InsensitiveStartsWith";
}
else
{
methodName = "StartsWithOrdinal";
}
methodName = m_IgnoreCase ? "InsensitiveStartsWith" : "StartsWithOrdinal";
break;
}
case StringOperator.EndsWith:
{
if (m_IgnoreCase)
{
methodName = "InsensitiveEndsWith";
}
else
{
methodName = "EndsWithOrdinal";
}
methodName = m_IgnoreCase ? "InsensitiveEndsWith" : "EndsWithOrdinal";
break;
}
@ -342,11 +314,7 @@ namespace Server.Commands.Generic
methodName,
BindingFlags.Public | BindingFlags.Static,
null,
new[]
{
typeof(string),
typeof(string)
},
[typeof(string), typeof(string)],
null
)
);
@ -380,12 +348,9 @@ namespace Server.Commands.Generic
emitter.BeginCall(
type.GetMethod(
methodName,
BindingFlags.Public | BindingFlags.Instance,
BindingFlags.Public | BindingFlags.Static,
null,
new[]
{
typeof(string)
},
[typeof(string), typeof(string)],
null
)
);

View file

@ -15,6 +15,7 @@
using System;
using System.Collections.Generic;
using Server.Collections;
using Server.Misc;
namespace Server.Engines.CannedEvil
@ -29,8 +30,8 @@ namespace Server.Engines.CannedEvil
Instance.OnTick();
}
private static readonly HashSet<DungeonChampionSpawn> _dungeonSpawns = new();
private static readonly HashSet<LLChampionSpawn> _lostLandsSpawns = new();
private static readonly HashSet<ChampionSpawn> _dungeonSpawns = new();
private static readonly HashSet<ChampionSpawn> _lostLandsSpawns = new();
private static DateTime _sliceTime;
public static CannedEvilTimer Instance { get; private set; }
@ -38,25 +39,25 @@ namespace Server.Engines.CannedEvil
public static void AddSpawn(DungeonChampionSpawn spawn)
{
_dungeonSpawns.Add(spawn);
Instance?.OnSlice(_dungeonSpawns, false);
OnSlice(_dungeonSpawns, false);
}
public static void AddSpawn(LLChampionSpawn spawn)
{
_lostLandsSpawns.Add(spawn);
Instance?.OnSlice(_lostLandsSpawns, false);
OnSlice(_lostLandsSpawns, false);
}
public static void RemoveSpawn(DungeonChampionSpawn spawn)
{
_dungeonSpawns.Remove(spawn);
Instance?.OnSlice(_dungeonSpawns, false);
OnSlice(_dungeonSpawns, false);
}
public static void RemoveSpawn(LLChampionSpawn spawn)
{
_lostLandsSpawns.Remove(spawn);
Instance?.OnSlice(_lostLandsSpawns, false);
OnSlice(_lostLandsSpawns, false);
}
public CannedEvilTimer() : base(TimeSpan.Zero, TimeSpan.FromMinutes(1.0))
@ -64,32 +65,34 @@ namespace Server.Engines.CannedEvil
_sliceTime = Core.Now;
}
public void OnSlice<T>(ICollection<T> list, bool rotate = true) where T : ChampionSpawn
public static void OnSlice(HashSet<ChampionSpawn> spawns, bool rotate = true)
{
if (list.Count > 0)
if (spawns.Count <= 0)
{
List<T> valid = new List<T>();
return;
}
foreach (T spawn in list)
using var queue = rotate ? PooledRefQueue<Item>.Create() : default;
foreach (var spawn in spawns)
{
if (spawn.AlwaysActive && !spawn.Active)
{
if (spawn.AlwaysActive && !spawn.Active)
{
spawn.ReadyToActivate = true;
}
else if (rotate && (!spawn.Active || spawn.Kills == 0 && spawn.Level == 0))
{
spawn.Active = false;
spawn.ReadyToActivate = false;
valid.Add(spawn);
}
spawn.ReadyToActivate = true;
}
if (valid.Count > 0)
else if (rotate && (!spawn.Active || spawn.Kills == 0 && spawn.Level == 0))
{
valid[Utility.Random(valid.Count)].ReadyToActivate = true;
spawn.Active = false;
spawn.ReadyToActivate = false;
queue.Enqueue(spawn);
}
}
if (rotate && queue.Count > 0)
{
((ChampionSpawn)queue.PeekRandom()).ReadyToActivate = true;
}
}
protected override void OnTick()

View file

@ -26,11 +26,6 @@ public partial class DungeonChampionSpawn : ChampionSpawn
CannedEvilTimer.AddSpawn(this);
}
public DungeonChampionSpawn(Serial serial) : base(serial)
{
CannedEvilTimer.AddSpawn(this);
}
public override bool ProximitySpawn => true;
public override bool AlwaysActive => false;
@ -39,4 +34,7 @@ public partial class DungeonChampionSpawn : ChampionSpawn
base.OnAfterDelete();
CannedEvilTimer.RemoveSpawn(this);
}
[AfterDeserialization]
private void AfterDeserialization() => CannedEvilTimer.AddSpawn(this);
}

View file

@ -15,35 +15,34 @@
using System;
namespace Server.Engines.CannedEvil
namespace Server.Engines.CannedEvil;
public class ChampionEntry
{
public record ChampionEntry
public readonly bool _randomizeType;
public readonly ChampionSpawnType _type;
public readonly Point3D _signLocation;
public readonly Type _champType;
public readonly Map _map;
public readonly Point3D _ejectLocation;
public readonly Map _ejectMap;
public ChampionEntry(Type champtype, Point3D signloc, Map map, Point3D ejectloc, Map ejectmap) :
this(champtype, ChampionSpawnType.Abyss, signloc, map, ejectloc, ejectmap, true)
{
public readonly bool m_RandomizeType;
public readonly ChampionSpawnType m_Type;
public readonly Point3D m_SignLocation;
public readonly Type m_ChampType;
public readonly Map m_Map;
public readonly Point3D m_EjectLocation;
public readonly Map m_EjectMap;
}
public ChampionEntry(Type champtype, Point3D signloc, Map map, Point3D ejectloc, Map ejectmap) :
this(champtype, ChampionSpawnType.Abyss, signloc, map, ejectloc, ejectmap, true)
{
}
public ChampionEntry(
Type champtype, ChampionSpawnType type, Point3D signloc, Map map, Point3D ejectloc, Map ejectmap,
bool randomizetype = false
)
{
m_ChampType = champtype;
m_RandomizeType = randomizetype;
m_Type = type;
m_SignLocation = signloc;
m_Map = map;
m_EjectLocation = ejectloc;
m_EjectMap = ejectmap;
}
public ChampionEntry(
Type champtype, ChampionSpawnType type, Point3D signloc, Map map, Point3D ejectloc, Map ejectmap,
bool randomizetype = false
)
{
_champType = champtype;
_randomizeType = randomizetype;
_type = type;
_signLocation = signloc;
_map = map;
_ejectLocation = ejectloc;
_ejectMap = ejectmap;
}
}

View file

@ -17,105 +17,106 @@ using System;
using System.Collections.Generic;
using Server.Logging;
namespace Server.Engines.CannedEvil
namespace Server.Engines.CannedEvil;
public static class ChampionGenerator
{
public static class ChampionGenerator
private static readonly ILogger logger = LogFactory.GetLogger(typeof(ChampionGenerator));
public static void Configure()
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(ChampionGenerator));
CommandSystem.Register("GenChamps", AccessLevel.Developer, ChampGen_OnCommand);
}
public static void Configure()
private static readonly ChampionEntry[] LLLocations =
[
new(typeof(LLChampionSpawn), new Point3D(5511, 2360, 42), Map.Felucca, new Point3D(5439, 2323, 26), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(6038, 2401, 47), Map.Felucca, new Point3D(5988, 2340, 24), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5549, 2640, 16), Map.Felucca, new Point3D(5645, 2696, -8), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5636, 2916, 37), Map.Felucca, new Point3D(5721, 2949, 28), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(6035, 2943, 50), Map.Felucca, new Point3D(6098, 2997, 17), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5265, 3171, 105), Map.Felucca, new Point3D(5314, 3232, 2), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5282, 3368, 50), Map.Felucca, new Point3D(5215, 3318, 3), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5207, 3637, 20), Map.Felucca, new Point3D(5263, 3687, 0), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5954, 3475, 25), Map.Felucca, new Point3D(6013, 3529, 0), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5982, 3882, 20), Map.Felucca, new Point3D(5929, 3820, -1), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5724, 3991, 41), Map.Felucca, new Point3D(5774, 4041, 26), Map.Felucca),
new(typeof(LLChampionSpawn), ChampionSpawnType.ForestLord, new Point3D(5559, 3757, 21), Map.Felucca, new Point3D(5513, 3878, 3), Map.Felucca)
];
private static readonly ChampionEntry[] DungeonLocations =
[
new(typeof(DungeonChampionSpawn), ChampionSpawnType.UnholyTerror, new Point3D(5179, 709, 20), Map.Felucca, new Point3D(4111, 432, 5), Map.Felucca),
new(typeof(DungeonChampionSpawn), ChampionSpawnType.VerminHorde, new Point3D(5557, 827, 65), Map.Felucca, new Point3D(5580, 632, 30), Map.Felucca),
new(typeof(DungeonChampionSpawn), ChampionSpawnType.ColdBlood, new Point3D(5259, 837, 64), Map.Felucca, new Point3D(1176, 2637, 0), Map.Felucca),
new(typeof(DungeonChampionSpawn), ChampionSpawnType.Abyss, new Point3D(5815, 1352, 5), Map.Felucca, new Point3D(2923, 3406, 8), Map.Felucca),
new(typeof(DungeonChampionSpawn), ChampionSpawnType.Arachnid, new Point3D(5190, 1607, 20), Map.Felucca, new Point3D(5482, 3161, -54), Map.Felucca)
];
[Usage("GenChamps")]
[Description("Generates champions for Felucca Dungeons & Lost Lands.")]
private static void ChampGen_OnCommand(CommandEventArgs e)
{
/*
//We take the assumption that we are spawning managed champions
for (int i = CannedEvilTimer.DungeonSpawns.Count - 1; i >= 0; i--)
CannedEvilTimer.DungeonSpawns[i].Delete();
for (int i = CannedEvilTimer.LLSpawns.Count - 1; i >= 0; i--)
CannedEvilTimer.LLSpawns[i].Delete();
*/
//We assume that all champion spawns are generated here.
List<ChampionSpawn> spawns = [];
foreach (Item item in World.Items.Values)
{
CommandSystem.Register("GenChamps", AccessLevel.Developer, ChampGen_OnCommand);
if (item is ChampionSpawn spawn)
{
spawns.Add(spawn);
}
}
private static readonly ChampionEntry[] LLLocations = {
new(typeof(LLChampionSpawn), new Point3D(5511, 2360, 42), Map.Felucca, new Point3D(5439, 2323, 26 ), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(6038, 2401, 47), Map.Felucca, new Point3D(5988, 2340, 24), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5549, 2640, 16), Map.Felucca, new Point3D(5645, 2696, -8), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5636, 2916, 37), Map.Felucca, new Point3D(5721, 2949, 28), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(6035, 2943, 50), Map.Felucca, new Point3D(6098, 2997, 17), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5265, 3171, 105), Map.Felucca, new Point3D(5314, 3232, 2), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5282, 3368, 50), Map.Felucca, new Point3D(5215, 3318, 3), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5207, 3637, 20), Map.Felucca, new Point3D(5263, 3687, 0), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5954, 3475, 25), Map.Felucca, new Point3D(6013, 3529, 0), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5982, 3882, 20), Map.Felucca, new Point3D(5929, 3820, -1), Map.Felucca),
new(typeof(LLChampionSpawn), new Point3D(5724, 3991, 41), Map.Felucca, new Point3D(5774, 4041, 26), Map.Felucca),
new(typeof(LLChampionSpawn), ChampionSpawnType.ForestLord, new Point3D(5559, 3757, 21), Map.Felucca, new Point3D(5513, 3878, 3), Map.Felucca),
};
private static readonly ChampionEntry[] DungeonLocations = {
new(typeof(DungeonChampionSpawn), ChampionSpawnType.UnholyTerror, new Point3D(5179, 709, 20), Map.Felucca, new Point3D(4111, 432, 5), Map.Felucca),
new(typeof(DungeonChampionSpawn), ChampionSpawnType.VerminHorde, new Point3D(5557, 827, 65), Map.Felucca, new Point3D(5580, 632, 30), Map.Felucca),
new(typeof(DungeonChampionSpawn), ChampionSpawnType.ColdBlood, new Point3D(5259, 837, 64), Map.Felucca, new Point3D(1176, 2637, 0), Map.Felucca),
new(typeof(DungeonChampionSpawn), ChampionSpawnType.Abyss, new Point3D(5815, 1352, 5), Map.Felucca, new Point3D(2923, 3406, 8), Map.Felucca),
new(typeof(DungeonChampionSpawn), ChampionSpawnType.Arachnid, new Point3D(5190, 1607, 20), Map.Felucca, new Point3D(5482, 3161, -54), Map.Felucca),
};
[Usage("GenChamps")]
[Description("Generates champions for Felucca Dungeons & Lost Lands.")]
private static void ChampGen_OnCommand(CommandEventArgs e)
for (int i = spawns.Count - 1; i >= 0; i--)
{
/*
//We take the assumption that we are spawning managed champions
for (int i = CannedEvilTimer.DungeonSpawns.Count - 1; i >= 0; i--)
CannedEvilTimer.DungeonSpawns[i].Delete();
for (int i = CannedEvilTimer.LLSpawns.Count - 1; i >= 0; i--)
CannedEvilTimer.LLSpawns[i].Delete();
*/
//We assume that all champion spawns are generated here.
List<ChampionSpawn> spawns = new List<ChampionSpawn>();
foreach (Item item in World.Items.Values)
{
if (item is ChampionSpawn spawn)
{
spawns.Add(spawn);
}
}
for (int i = spawns.Count - 1; i >= 0; i--)
{
spawns[i].Delete();
}
Process(DungeonLocations);
Process(LLLocations);
//ProcessIlshenar();
//ProcessTokuno();
spawns[i].Delete();
}
private static void Process(ChampionEntry[] entries)
{
for (int i = 0; i < entries.Length; i++)
{
ChampionEntry entry = entries[i];
Process(DungeonLocations);
Process(LLLocations);
//ProcessIlshenar();
//ProcessTokuno();
}
try
private static void Process(ChampionEntry[] entries)
{
for (int i = 0; i < entries.Length; i++)
{
ChampionEntry entry = entries[i];
try
{
if (Activator.CreateInstance(entry._champType) is ChampionSpawn spawn)
{
if (Activator.CreateInstance(entry.m_ChampType) is ChampionSpawn spawn)
spawn.RandomizeType = entry._randomizeType;
spawn.Type = entry._type;
spawn.MoveToWorld(entry._signLocation, entry._map);
spawn.EjectLocation = entry._ejectLocation;
spawn.EjectMap = entry._ejectMap;
if (spawn.AlwaysActive)
{
spawn.RandomizeType = entry.m_RandomizeType;
spawn.Type = entry.m_Type;
spawn.MoveToWorld(entry.m_SignLocation, entry.m_Map);
spawn.EjectLocation = entry.m_EjectLocation;
spawn.EjectMap = entry.m_EjectMap;
if (spawn.AlwaysActive)
{
spawn.ReadyToActivate = true;
}
spawn.ReadyToActivate = true;
}
}
catch (Exception e)
{
logger.Error(
e,
"Failed to generate champion \"{Type}\" at {Location} ({Map}).",
entry.m_ChampType.FullName,
entry.m_SignLocation,
entry.m_Map
);
}
}
catch (Exception e)
{
logger.Error(
e,
"Failed to generate champion \"{Type}\" at {Location} ({Map}).",
entry._champType.FullName,
entry._signLocation,
entry._map
);
}
}
}

View file

@ -28,11 +28,6 @@ public partial class LLChampionSpawn : ChampionSpawn
CannedEvilTimer.AddSpawn(this);
}
public LLChampionSpawn(Serial serial) : base(serial)
{
CannedEvilTimer.AddSpawn(this);
}
public override bool AlwaysActive => false;
public override void OnAfterDelete()
@ -40,4 +35,7 @@ public partial class LLChampionSpawn : ChampionSpawn
base.OnAfterDelete();
CannedEvilTimer.RemoveSpawn(this);
}
[AfterDeserialization]
private void AfterDeserialization() => CannedEvilTimer.AddSpawn(this);
}

View file

@ -258,5 +258,15 @@ public class DefBowFletching : CraftSystem
MarkOption = true;
Repair = Core.AOS;
SetSubRes(typeof(Log), 1072643);
AddSubRes(typeof(Log), 1072643, 0.0, 1044041, 1072652);
AddSubRes(typeof(OakLog), 1072644, 65.0, 1044041, 1072652);
AddSubRes(typeof(AshLog), 1072645, 80.0, 1044041, 1072652);
AddSubRes(typeof(YewLog), 1072646, 95.0, 1044041, 1072652);
AddSubRes(typeof(HeartwoodLog), 1072647, 100.0, 1044041, 1072652);
AddSubRes(typeof(BloodwoodLog), 1072648, 100.0, 1044041, 1072652);
AddSubRes(typeof(FrostwoodLog), 1072649, 100.0, 1044041, 1072652);
}
}

View file

@ -79,11 +79,7 @@ public partial class MurderContext
if (_player.Kills > 0)
{
var timeUntilLong = now + (LongTermElapse - gameTime);
if (_nextElapse > timeUntilLong)
{
_nextElapse = timeUntilLong;
}
_nextElapse = Utility.Min(_nextElapse, now + (LongTermElapse - gameTime));
}
return _nextElapse != DateTime.MaxValue;

View file

@ -101,9 +101,17 @@ public class PlayerMurderSystem : GenericPersistence
private static void OnDisconnected(Mobile m)
{
if (m is PlayerMobile pm && _murderContexts.Remove(pm, out var context))
if (m is not PlayerMobile pm || !_murderContexts.TryGetValue(pm, out var context))
{
_contextTerms.Remove(context);
return;
}
context.DecayKills();
_contextTerms.Remove(context);
if (pm.Kills <= 0 && context.ShortTermMurders <= 0)
{
_murderContexts.Remove(pm);
}
}
@ -164,6 +172,8 @@ public class PlayerMurderSystem : GenericPersistence
{
var context = GetOrCreateMurderContext(player);
context.ShortTermMurders = shortTermMurders;
context.ResetKillTime();
UpdateMurderContext(context);
}

View file

@ -104,7 +104,7 @@ namespace Server.Items
}
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
[SerializedCommandProperty(AccessLevel.GameMaster, readOnly: true)]
public BaseAddon _addon;
[SerializableField(1)]

View file

@ -270,7 +270,8 @@ namespace Server.Items
foreach (var c in Components)
{
c.Delete();
// Component can become null if the Addon property is somehow deleted, then the component itself is deleted.
c?.Delete();
}
}
@ -283,5 +284,12 @@ namespace Server.Items
_resource = (CraftResource)reader.ReadEncodedInt();
}
}
[AfterDeserialization]
private void AfterDeserialization()
{
// We have had issues in the past, so let's tidy it up.
_components?.Tidy();
}
}
}

View file

@ -10,7 +10,7 @@ public partial class FarmableCabbage : FarmableCrop
{
}
public static int GetCropID() => 3254;
public static int GetCropID() => 0x0C7B;
public override Item GetCropObject() =>
new Cabbage

View file

@ -88,7 +88,7 @@ public abstract partial class BaseWeapon
[SerializableFieldSaveFlag(7)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ShouldSerializePoison() => _poison?.Level > 0;
private bool ShouldSerializePoison() => _poison != null;
[InvalidateProperties]
[SerializableField(8)]
@ -890,6 +890,11 @@ public abstract partial class BaseWeapon
if (attacker is BaseCreature bc)
{
if (bc.TriggerAbility(MonsterAbilityTrigger.CombatAction, defender))
{
return GetDelay(attacker);
}
// Only change direction if they are not a player.
attacker.Direction = attacker.GetDirectionTo(defender);
var ab = bc.GetWeaponAbility();
@ -1350,7 +1355,7 @@ public abstract partial class BaseWeapon
theirValue = Math.Max(0.1, defValue + 50.0);
}
var chance = ourValue / (theirValue * 2.0) * 1.0 + (double)bonus / 100;;
var chance = ourValue / (theirValue * 2.0) * 1.0 + (double)bonus / 100;
if (Core.AOS && chance < 0.02)
{

View file

@ -50,7 +50,7 @@ namespace Server.Items
return wresValue > incrValue ? wresValue : incrValue;
}
private void CheckPreAOSMoves(Mobile attacker, Mobile defender)
private static void CheckPreAOSMoves(Mobile attacker, Mobile defender)
{
if (!attacker.CanBeginAction<Fists>())
{
@ -90,60 +90,67 @@ namespace Server.Items
attacker.SendLocalizedMessage(1004010); // You failed in your attempt to stun.
defender.SendLocalizedMessage(1004011); // Your opponent tried to stun you and failed.
}
return;
}
else if (attacker.DisarmReady)
if (!attacker.DisarmReady)
{
if (!defender.Player && !defender.Body.IsHuman)
{
attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent.
return;
}
return;
}
if (attacker.Skills.ArmsLore.Value < 80.0 || attacker.Skills.Wrestling.Value < 80.0)
{
attacker.SendLocalizedMessage(1004002); // You are not skilled enough to disarm your opponent.
attacker.DisarmReady = false;
return;
}
if (!defender.Player && !defender.Body.IsHuman)
{
attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent.
return;
}
if (attacker.Stam < 15)
{
attacker.SendLocalizedMessage(1004003); // You are too fatigued to attempt anything.
return;
}
if (attacker.Skills.ArmsLore.Value < 80.0 || attacker.Skills.Wrestling.Value < 80.0)
{
attacker.SendLocalizedMessage(1004002); // You are not skilled enough to disarm your opponent.
attacker.DisarmReady = false;
return;
}
var toDisarm = defender.FindItemOnLayer(Layer.OneHanded);
if (attacker.Stam < 15)
{
attacker.SendLocalizedMessage(1004003); // You are too fatigued to attempt anything.
return;
}
if (toDisarm?.Movable == false)
{
toDisarm = defender.FindItemOnLayer(Layer.TwoHanded);
}
var toDisarm = defender.FindItemOnLayer(Layer.OneHanded);
var pack = defender.Backpack;
if (toDisarm?.Movable != true)
{
toDisarm = defender.FindItemOnLayer(Layer.TwoHanded);
}
if (pack == null || toDisarm?.Movable == false)
{
attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent.
}
else if (CheckMove(attacker, SkillName.ArmsLore))
{
StartMoveDelay(attacker);
var pack = defender.Backpack;
attacker.Stam -= 15;
attacker.DisarmReady = false;
if (pack == null || toDisarm?.Movable != true)
{
attacker.SendLocalizedMessage(1004001); // You cannot disarm your opponent.
return;
}
attacker.SendLocalizedMessage(1004006); // You successfully disarm your opponent!
defender.SendLocalizedMessage(1004007); // You have been disarmed!
if (CheckMove(attacker, SkillName.ArmsLore))
{
StartMoveDelay(attacker);
pack.DropItem(toDisarm);
}
else
{
attacker.Stam -= 15;
attacker.Stam -= 15;
attacker.DisarmReady = false;
attacker.SendLocalizedMessage(1004004); // You failed in your attempt to disarm.
defender.SendLocalizedMessage(1004005); // Your opponent tried to disarm you but failed.
}
attacker.SendLocalizedMessage(1004006); // You successfully disarm your opponent!
defender.SendLocalizedMessage(1004007); // You have been disarmed!
pack.DropItem(toDisarm);
}
else
{
attacker.Stam -= 15;
attacker.SendLocalizedMessage(1004004); // You failed in your attempt to disarm.
defender.SendLocalizedMessage(1004005); // Your opponent tried to disarm you but failed.
}
}

View file

@ -70,6 +70,11 @@ namespace Server.Items
attacker.DisruptiveAction();
attacker.NetState.SendSwing(attacker.Serial, defender.Serial);
if (attacker is BaseCreature bc && bc.TriggerAbility(MonsterAbilityTrigger.CombatAction, defender))
{
return GetDelay(attacker);
}
if (OnFired(attacker, defender))
{
if (CheckHit(attacker, defender))

View file

@ -216,6 +216,7 @@ public static class NameVerification
}
var index = name.IndexOfAny(exceptions);
var exceptionCount = 0;
while (index != -1)
{
@ -229,7 +230,7 @@ public static class NameVerification
noExceptionsAtStart = false;
}
if (maxExceptions-- <= 0)
if (exceptionCount++ >= maxExceptions)
{
return true;
}
@ -238,6 +239,10 @@ public static class NameVerification
{
name = name[(index + 1)..];
index = name.IndexOfAny(exceptions);
if (index != 0)
{
exceptionCount = 0;
}
}
else
{

View file

@ -626,15 +626,13 @@ public abstract class BaseAI
{
if (m_Mobile.Summoned || m_Mobile is GrizzledMare)
{
e.Mobile.SendLocalizedMessage(
1005481
); // Summoned creatures are loyal only to their summoners.
// Summoned creatures are loyal only to their summoners.
e.Mobile.SendLocalizedMessage(1005481);
}
else if (e.Mobile.HasTrade)
{
e.Mobile.SendLocalizedMessage(
1070947
); // You cannot friend a pet with a trade pending
// You cannot friend a pet with a trade pending
e.Mobile.SendLocalizedMessage(1070947);
}
else
{
@ -742,15 +740,13 @@ public abstract class BaseAI
{
if (m_Mobile.Summoned || m_Mobile is GrizzledMare)
{
e.Mobile.SendLocalizedMessage(
1005487
); // You cannot transfer ownership of a summoned creature.
// You cannot transfer ownership of a summoned creature.
e.Mobile.SendLocalizedMessage(1005487);
}
else if (e.Mobile.HasTrade)
{
e.Mobile.SendLocalizedMessage(
1010507
); // You cannot transfer a pet with a trade pending
// You cannot transfer a pet with a trade pending
e.Mobile.SendLocalizedMessage(1010507);
}
else
{
@ -2975,6 +2971,7 @@ public abstract class BaseAI
{
if (bc.CheckControlChance(from))
{
bc.ControlTarget = null;
bc.ControlOrder = _order;
}

View file

@ -5,8 +5,23 @@ namespace Server.Mobiles;
public class FanningFire : MonsterAbilitySingleTargetDoT
{
public override MonsterAbilityType AbilityType => MonsterAbilityType.FanningFire;
public override MonsterAbilityTrigger AbilityTrigger => MonsterAbilityTrigger.GiveDamage;
public override double ChanceToTrigger => 0.05;
public override MonsterAbilityTrigger AbilityTrigger => MonsterAbilityTrigger.CombatAction;
public FanningFire(double chanceToTrigger, int fireResistMod, int minDamage, int maxDamage)
{
ChanceToTrigger = chanceToTrigger;
FireResistMod = fireResistMod;
MinDamage = minDamage;
MaxDamage = maxDamage;
}
public sealed override double ChanceToTrigger { get; }
public int FireResistMod { get; }
public int MinDamage { get; }
public int MaxDamage { get; }
public const string Name = "FanningFire";
@ -52,16 +67,12 @@ public class FanningFire : MonsterAbilitySingleTargetDoT
*/
source.DoHarmful(defender);
var effect = -(defender.FireResistance / 10);
defender.AddResistanceMod(new ResistanceMod(ResistanceType.Fire, Name, FireResistMod));
var mod = new ResistanceMod(ResistanceType.Fire, Name, effect);
defender.AddResistanceMod(mod);
defender.FixedParticles(0x37B9, 10, 30, 0x34, EffectLayer.RightFoot);
defender.FixedParticles(0x3709, 10, 30, 0x34, EffectLayer.RightFoot);
defender.PlaySound(0x208);
// TODO: Trigger replaces a normal attack.
AOS.Damage(defender, source, Utility.RandomMinMax(35, 45), 0, 100, 0, 0, 0);
AOS.Damage(defender, source, Utility.RandomMinMax(MinDamage, MaxDamage), 0, 100, 0, 0, 0);
}
protected override void EffectTick(BaseCreature source, Mobile defender, ref TimeSpan nextDelay)

View file

@ -14,7 +14,7 @@ public static class MonsterAbilities
// Resistance Debuffs
public static GraspingClaw GraspingClaw => new();
public static RuneCorruption RuneCorruption => new();
public static FanningFire FanningFire => new();
public static FanningFire FanningFire => new(0.05, -10, 35, 45);
// Summon Undead
public static SummonSkeletonsCounter SummonSkeletonsCounter => new();

View file

@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
using ModernUO.CodeGeneratedEvents;
namespace Server.Mobiles;
/// <summary>
/// Abstract class used to build singletons for managing a specific monster ability.
/// </summary>
public abstract partial class MonsterAbility
public abstract class MonsterAbility
{
private Dictionary<BaseCreature, long> _nextTriggerTicks;
@ -18,39 +19,34 @@ public abstract partial class MonsterAbility
public virtual TimeSpan MinTriggerCooldown => TimeSpan.Zero;
public virtual TimeSpan MaxTriggerCooldown => TimeSpan.Zero;
public bool WillTrigger(MonsterAbilityTrigger trigger) => (AbilityTrigger & trigger) != 0;
/// <summary>
/// Returns true if ability is not on cooldown, and the chance to trigger succeeds.
/// </summary>
/// <returns>Boolean indicating the ability can trigger.</returns>
public virtual bool CanTrigger(BaseCreature source, MonsterAbilityTrigger trigger)
{
if (source is not { Alive: true, Deleted: false })
if ((AbilityTrigger & trigger) == 0 || source is not { Alive: true, Deleted: false })
{
return false;
}
if (_nextTriggerTicks?.TryGetValue(source, out var nextTrigger) == true && nextTrigger - Core.TickCount > 0)
if (_nextTriggerTicks?.TryGetValue(source, out var nextTrigger) == true)
{
return false;
if (nextTrigger - Core.TickCount > 0)
{
return false;
}
_nextTriggerTicks.Remove(source);
if (_nextTriggerTicks.Count == 0)
{
_nextTriggerTicks = null;
}
}
var c = ChanceToTrigger;
if (c >= 1)
{
return true;
}
if (c <= 0)
{
return false;
}
var rnd = Utility.RandomDouble();
return c > rnd;
return c >= 1 || c > 0 && c > Utility.RandomDouble();
}
/// <summary>
@ -99,7 +95,23 @@ public abstract partial class MonsterAbility
{
}
public virtual void Move(BaseCreature creature, Direction d)
public virtual void Move(BaseCreature source, Direction d)
{
}
[OnEvent(nameof(BaseCreature.CreatureDeathEvent))]
[OnEvent(nameof(BaseCreature.CreatureDeletedEvent))]
public static void InvalidateNextAbilityTriggers(BaseCreature source)
{
var abilities = source.GetMonsterAbilities();
if (abilities == null || abilities.Length == 0)
{
return;
}
for (var i = 0; i < abilities.Length; i++)
{
abilities[i]._nextTriggerTicks?.Remove(source);
}
}
}

View file

@ -1,15 +1,14 @@
using System;
using Server.Random;
using WeightedMonsterAbility = Server.Random.WeightedValue<Server.Mobiles.MonsterAbility>;
namespace Server.Mobiles;
public class MonsterAbilityGroup : MonsterAbility
{
private WeightedMonsterAbility[] _weightedAbilities;
private WeightedMonsterAbility[] _availableToTrigger;
private readonly WeightedMonsterAbility[] _weightedAbilities;
private readonly WeightedMonsterAbility[] _availableToTrigger;
private readonly MonsterAbilityTrigger _triggers;
private int _availableToTriggerCount;
private MonsterAbilityTrigger _triggers;
public MonsterAbilityGroup(params WeightedMonsterAbility[] weightedAbilities)
{
@ -18,8 +17,7 @@ public class MonsterAbilityGroup : MonsterAbility
for (var i = 0; i < _weightedAbilities.Length; i++)
{
var weightedAbility = _weightedAbilities[i];
_triggers |= weightedAbility.Value.AbilityTrigger;
_triggers |= _weightedAbilities[i].Value.AbilityTrigger;
}
}
@ -66,7 +64,7 @@ public class MonsterAbilityGroup : MonsterAbility
for (var i = 0; i < _weightedAbilities.Length; i++)
{
var weightedAbility = _weightedAbilities[i];
if (weightedAbility.Value.WillTrigger(trigger) && weightedAbility.Value.CanTrigger(source, trigger))
if (weightedAbility.Value.CanTrigger(source, trigger))
{
_availableToTrigger[_availableToTriggerCount++] = weightedAbility;
}
@ -83,7 +81,7 @@ public class MonsterAbilityGroup : MonsterAbility
return;
}
var slice = new ReadOnlySpan<WeightedValue<MonsterAbility>>(_availableToTrigger, 0, _availableToTriggerCount);
var slice = new ReadOnlySpan<WeightedMonsterAbility>(_availableToTrigger, 0, _availableToTriggerCount);
var chosenAbility = slice.RandomWeightedElement().Value;
// Just in case?

View file

@ -8,4 +8,8 @@ public class ReflectPhysicalDamage : MonsterAbility
public override MonsterAbilityType AbilityType => MonsterAbilityType.ReflectPhysicalDamage;
public virtual int PercentReflected => 10;
public override void Trigger(MonsterAbilityTrigger trigger, BaseCreature source, Mobile target)
{
}
}

View file

@ -1216,7 +1216,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
if (ability.WillTrigger(trigger) && ability.CanTrigger(this, trigger))
if (ability.CanTrigger(this, trigger))
{
ability.Trigger(trigger, this, defender);
triggered = true;
@ -1238,7 +1238,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
if (ability.WillTrigger(trigger) && ability.CanTrigger(this, trigger))
if (ability.CanTrigger(this, trigger))
{
ability.Move(this, d);
}
@ -1257,7 +1257,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
if (ability.WillTrigger(trigger) && ability.CanTrigger(this, trigger))
if (ability.CanTrigger(this, trigger))
{
if ((trigger & MonsterAbilityTrigger.GiveMeleeDamage) != 0)
{
@ -1298,7 +1298,7 @@ namespace Server.Mobiles
for (var i = 0; i < abilities.Length; i++)
{
var ability = abilities[i];
if (ability.WillTrigger(trigger) && ability.CanTrigger(this, trigger))
if (ability.CanTrigger(this, trigger))
{
if ((trigger & MonsterAbilityTrigger.GiveSpellDamage) != 0)
{
@ -3263,7 +3263,7 @@ namespace Server.Mobiles
}
[GeneratedEvent(nameof(CreatureDeathEvent))]
public static partial void CreatureDeathEvent(Mobile m);
public static partial void CreatureDeathEvent(BaseCreature bc);
public override void OnDeath(Container c)
{
@ -3454,8 +3454,13 @@ namespace Server.Mobiles
CreatureDeathEvent(this);
}
[GeneratedEvent(nameof(CreatureDeletedEvent))]
public static partial void CreatureDeletedEvent(BaseCreature bc);
public override void OnDelete()
{
CreatureDeletedEvent(this);
var m = m_ControlMaster;
SetControlMaster(null);

View file

@ -228,11 +228,6 @@ public partial class BaseHire : BaseCreature
if (!Controlled)
{
if (CanPaperdollBeOpenedBy(from))
{
list.Add(new PaperdollEntry());
}
list.Add(new HireEntry());
}
else

View file

@ -1,122 +1,71 @@
using ModernUO.Serialization;
using System;
using System.Collections.Generic;
namespace Server.Mobiles
namespace Server.Mobiles;
[SerializationGenerator(0, false)]
public partial class LadyJennifyr : SkeletalKnight
{
[SerializationGenerator(0, false)]
public partial class LadyJennifyr : SkeletalKnight
[Constructible]
public LadyJennifyr()
{
private static readonly Dictionary<Mobile, ExpireTimer> m_Table = new();
IsParagon = true;
[Constructible]
public LadyJennifyr()
{
IsParagon = true;
Hue = 0x76D;
Hue = 0x76D;
SetStr(208, 309);
SetDex(91, 118);
SetInt(44, 101);
SetStr(208, 309);
SetDex(91, 118);
SetInt(44, 101);
SetHits(1113, 1285);
SetHits(1113, 1285);
SetDamage(15, 25);
SetDamage(15, 25);
SetDamageType(ResistanceType.Physical, 40);
SetDamageType(ResistanceType.Cold, 60);
SetDamageType(ResistanceType.Physical, 40);
SetDamageType(ResistanceType.Cold, 60);
SetResistance(ResistanceType.Physical, 56, 65);
SetResistance(ResistanceType.Fire, 41, 49);
SetResistance(ResistanceType.Cold, 71, 80);
SetResistance(ResistanceType.Poison, 41, 50);
SetResistance(ResistanceType.Energy, 50, 58);
SetResistance(ResistanceType.Physical, 56, 65);
SetResistance(ResistanceType.Fire, 41, 49);
SetResistance(ResistanceType.Cold, 71, 80);
SetResistance(ResistanceType.Poison, 41, 50);
SetResistance(ResistanceType.Energy, 50, 58);
SetSkill(SkillName.Wrestling, 127.9, 137.1);
SetSkill(SkillName.Tactics, 128.4, 141.9);
SetSkill(SkillName.MagicResist, 102.1, 119.5);
SetSkill(SkillName.Anatomy, 129.0, 137.5);
SetSkill(SkillName.Wrestling, 127.9, 137.1);
SetSkill(SkillName.Tactics, 128.4, 141.9);
SetSkill(SkillName.MagicResist, 102.1, 119.5);
SetSkill(SkillName.Anatomy, 129.0, 137.5);
Fame = 18000;
Karma = -18000;
}
public override string CorpseName => "a Lady Jennifyr corpse";
public override string DefaultName => "Lady Jennifyr";
/*
// TODO: Uncomment once added
public override void OnDeath( Container c )
{
base.OnDeath( c );
if (Utility.RandomDouble() < 0.15)
c.DropItem( new DisintegratingThesisNotes() );
if (Utility.RandomDouble() < 0.1)
c.DropItem( new ParrotItem() );
}
*/
public override bool GivesMLMinorArtifact => true;
public override void GenerateLoot()
{
AddLoot(LootPack.UltraRich, 3);
}
public override void OnGaveMeleeAttack(Mobile defender, int damage)
{
base.OnGaveMeleeAttack(defender, damage);
if (Utility.RandomDouble() < 0.9)
{
return;
}
if (m_Table.Remove(defender, out var timer))
{
timer.DoExpire();
}
defender.FixedParticles(0x3709, 10, 30, 5052, EffectLayer.LeftFoot);
defender.PlaySound(0x208);
// The creature fans you with fire, reducing your resistance to fire attacks.
defender.SendLocalizedMessage(1070833);
var mod = new ResistanceMod(ResistanceType.Fire, "FireResistFanningFire", -10);
defender.AddResistanceMod(mod);
m_Table[defender] = timer = new ExpireTimer(defender, mod);
timer.Start();
}
private class ExpireTimer : Timer
{
private readonly Mobile m_Mobile;
private readonly ResistanceMod m_Mod;
public ExpireTimer(Mobile m, ResistanceMod mod)
: base(TimeSpan.FromSeconds(10))
{
m_Mobile = m;
m_Mod = mod;
}
public void DoExpire()
{
m_Mobile.RemoveResistanceMod(m_Mod);
Stop();
}
protected override void OnTick()
{
m_Mobile.SendLocalizedMessage(1070834); // Your resistance to fire attacks has returned.
DoExpire();
m_Table.Remove(m_Mobile);
}
}
Fame = 18000;
Karma = -18000;
}
public override string CorpseName => "a Lady Jennifyr corpse";
public override string DefaultName => "Lady Jennifyr";
/*
// TODO: Uncomment once added
public override void OnDeath( Container c )
{
base.OnDeath( c );
if (Utility.RandomDouble() < 0.15)
c.DropItem( new DisintegratingThesisNotes() );
if (Utility.RandomDouble() < 0.1)
c.DropItem( new ParrotItem() );
}
*/
public override bool GivesMLMinorArtifact => true;
public override void GenerateLoot()
{
AddLoot(LootPack.UltraRich, 3);
}
private static readonly MonsterAbility[] _abilities =
[
new FanningFire(0.10, -10, 35, 45)
];
public override MonsterAbility[] GetMonsterAbilities() => _abilities;
}

View file

@ -12,7 +12,7 @@ namespace Server.Mobiles;
[SerializationGenerator(0, false)]
public partial class Banker : BaseVendor
{
private readonly List<SBInfo> m_SBInfos = new();
private readonly List<SBInfo> m_SBInfos = [];
[Constructible]
public Banker() : base("the banker")
@ -41,7 +41,7 @@ public partial class Banker : BaseVendor
}
}
Container bank = m.FindBankNoCreate();
var bank = m.FindBankNoCreate();
if (bank != null)
{
@ -80,11 +80,11 @@ public partial class Banker : BaseVendor
}
}
Container bank = m.FindBankNoCreate();
var bank = m.FindBankNoCreate();
if (bank != null)
{
gold = new List<Gold>();
gold = [];
foreach (var g in bank.FindItemsByType<Gold>())
{
@ -97,7 +97,7 @@ public partial class Banker : BaseVendor
return int.MaxValue;
}
checks = new List<BankCheck>();
checks = [];
foreach (var bc in bank.FindItemsByType<BankCheck>())
{
@ -111,7 +111,7 @@ public partial class Banker : BaseVendor
private static bool HasRequiredBalance(int requiredBalance, Mobile m, out PooledRefList<Gold> gold, out PooledRefList<BankCheck> checks)
{
Container bank = m.FindBankNoCreate();
var bank = m.FindBankNoCreate();
if (bank == null)
{
@ -382,9 +382,7 @@ public partial class Banker : BaseVendor
}
else if (amount > 0)
{
var box = e.Mobile.FindBankNoCreate();
if (box == null || !Withdraw(e.Mobile, amount))
if (!Withdraw(e.Mobile, amount))
{
Say(500384); // Ah, art thou trying to fool me? Thou hast not so much gold!
}

View file

@ -118,7 +118,7 @@ public class TownCrierDurationPrompt : Prompt
{
if (!TimeSpan.TryParse(text, out var ts))
{
from.SendMessage("Value was not properly formatted. Use: <hours:minutes:seconds>");
from.SendMessage("Value was not properly formatted. Use: <hours:minutes:seconds, 00:00:00>");
from.SendGump(new TownCrierGump(from, m_Owner));
return;
}
@ -272,7 +272,7 @@ public class TownCrierGump : Gump
{
if (info.ButtonID == 1)
{
m_From.SendMessage("Enter the duration for the new message. Format: <hours:minutes:seconds>");
m_From.SendMessage("Enter the duration for the new message. Format: <hours:minutes:seconds, 00:00:00>");
m_From.Prompt = new TownCrierDurationPrompt(m_Owner);
}
else if (info.ButtonID > 1)
@ -433,7 +433,7 @@ public partial class TownCrier : Mobile, ITownCrierEntryList
if (tce == null)
{
_autoShoutTimer.Stop();
_autoShoutTimer?.Stop();
_autoShoutTimer = null;
}
else if (_newsTimer == null)
@ -454,7 +454,7 @@ public partial class TownCrier : Mobile, ITownCrierEntryList
var index = _newsTimer.Index;
if (index >= tce.Lines.Length)
{
_newsTimer.Stop();
_newsTimer?.Stop();
_newsTimer = null;
}
else

File diff suppressed because it is too large Load diff

View file

@ -171,7 +171,7 @@ public partial class PlayerVendor : Mobile
reader.ReadBool(); // New vendor system?
_shopName = reader.ReadString();
_nextPayTime = reader.ReadDeltaTime();
_house = reader.ReadEntity<BaseHouse>();
House = reader.ReadEntity<BaseHouse>();
_owner = reader.ReadEntity<Mobile>();
_bankAccount = reader.ReadInt();
_holdGold = reader.ReadInt();
@ -201,6 +201,34 @@ public partial class PlayerVendor : Mobile
{
NameHue = -1;
}
// Do we have a vendor that may have been orphaned? Let's try to recover them and attach them to their house.
if (_house == null)
{
if (_owner == null)
{
Timer.DelayCall(Delete); // Don't try to dismiss, no owner.
}
Timer.DelayCall(() =>
{
var house = BaseHouse.FindHouseAt(this);
if (house != null)
{
House = house;
}
else if (_owner.AccessLevel == AccessLevel.Player)
{
// If we can't find a house, dismiss the vendor.
Dismiss(_owner);
}
}
);
}
else
{
_house.PlayerVendors.Add(this);
}
}
public void InitBody()

View file

@ -175,40 +175,16 @@ namespace Server.Multis
[CommandProperty(AccessLevel.GameMaster)]
public BoatOrder Order { get; set; }
public int Status
{
get
public int Status =>
(Core.Now - (TimeOfDecay - BoatDecayDelay)) switch
{
var start = Core.Now - TimeOfDecay - BoatDecayDelay;
if (start < TimeSpan.FromHours(1.0))
{
return 1043010; // This structure is like new.
}
if (start < TimeSpan.FromDays(2.0))
{
return 1043011; // This structure is slightly worn.
}
if (start < TimeSpan.FromDays(3.0))
{
return 1043012; // This structure is somewhat worn.
}
if (start < TimeSpan.FromDays(4.0))
{
return 1043013; // This structure is fairly worn.
}
if (start < TimeSpan.FromDays(5.0))
{
return 1043014; // This structure is greatly worn.
}
return 1043015; // This structure is in danger of collapsing.
}
}
var start when start < TimeSpan.FromHours(1) => 1043010, // This structure is like new.
var start when start < TimeSpan.FromDays(2) => 1043011, // This structure is slightly worn.
var start when start < TimeSpan.FromDays(3) => 1043012, // This structure is somewhat worn.
var start when start < TimeSpan.FromDays(4) => 1043013, // This structure is fairly worn.
var start when start < TimeSpan.FromDays(5) => 1043014, // This structure is greatly worn.
_ => 1043015 // This structure is in danger of collapsing.
};
public virtual int NorthID => 0;
public virtual int EastID => 0;

View file

@ -71,6 +71,8 @@ namespace Server.Multis.Deeds
[CommandProperty(AccessLevel.GameMaster)]
public Point3D Offset { get; set; }
public virtual Direction HouseDirection => Direction.South;
public abstract Rectangle2D[] Area { get; }
public override void Serialize(IGenericWriter writer)
@ -153,7 +155,7 @@ namespace Server.Multis.Deeds
else
{
var center = new Point3D(p.X - Offset.X, p.Y - Offset.Y, p.Z - Offset.Z);
var res = HousePlacement.Check(from, MultiID, center, out var toMove);
var res = HousePlacement.Check(from, MultiID, center, out var toMove, HouseDirection);
switch (res)
{

View file

@ -100,6 +100,8 @@ namespace Server.Multis
[CommandProperty(AccessLevel.GameMaster)]
public bool RestrictDecay { get; set; }
public virtual Direction HouseDirection => Direction.South;
public virtual TimeSpan DecayPeriod => TimeSpan.FromDays(5.0);
public virtual DecayType DecayType

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Server.Collections;
using Server.Regions;
@ -37,7 +38,9 @@ namespace Server.Multis
0x0150, 0x015C // Furrows
};
public static HousePlacementResult Check(Mobile from, int multiID, Point3D center, out List<IEntity> toMove)
public static HousePlacementResult Check(
Mobile from, int multiID, Point3D center, out List<IEntity> toMove, Direction houseFacing = Direction.South
)
{
// If this spot is considered valid, every item and mobile in this list will be moved under the house sign
toMove = new List<IEntity>();
@ -77,7 +80,7 @@ namespace Server.Multis
HouseFoundation.AddStairsTo(ref mcl); // this is a AOS house, add the stairs
}
// Location of the nortwest-most corner of the house
// Location of the northwest-most corner of the house
var start = new Point3D(center.X + mcl.Min.X, center.Y + mcl.Min.Y, center.Z);
// These are storage lists. They hold items and mobiles found in the map for further processing
@ -85,7 +88,7 @@ namespace Server.Multis
var mobiles = new List<Mobile>();
// These are also storage lists. They hold location values indicating the yard and border locations.
List<Point2D> yard = new(), borders = new();
List<Point2D> borders = [];
/* RULES:
*
@ -121,7 +124,7 @@ namespace Server.Multis
return HousePlacementResult.BadRegionTemp;
}
if (reg.IsPartOf<TreasureRegion>() || reg.IsPartOf<HouseRegion>())
if (reg.IsPartOf<TreasureRegion, HouseRegion>())
{
return HousePlacementResult.BadRegionHidden;
}
@ -218,16 +221,18 @@ namespace Server.Multis
{
var id = item.ItemData;
if (addTileTop > item.Z && item.Z + id.CalcHeight > addTileZ)
if (addTileTop <= item.Z || item.Z + id.CalcHeight <= addTileZ)
{
if (item.Movable)
{
toMove.Add(item);
}
else if (id.Impassable || id.Surface && !id.Background)
{
return HousePlacementResult.BadItem; // Broke rule #2
}
continue;
}
if (item.Movable)
{
toMove.Add(item);
}
else if (id.Impassable || id.Surface && !id.Background)
{
return HousePlacementResult.BadItem; // Broke rule #2
}
}
@ -257,17 +262,9 @@ namespace Server.Multis
if (hasFoundation)
{
for (var xOffset = -1; xOffset <= 1; ++xOffset)
if (!CheckYard(map, tileX, tileY, YardSize, houseFacing))
{
for (var yOffset = -YardSize; yOffset <= YardSize; ++yOffset)
{
var yardPoint = new Point2D(tileX + xOffset, tileY + yOffset);
if (!yard.Contains(yardPoint))
{
yard.Add(yardPoint);
}
}
return HousePlacementResult.BadStatic; // Broke rule #3
}
for (var xOffset = -1; xOffset <= 1; ++xOffset)
@ -364,37 +361,73 @@ namespace Server.Multis
}
}
for (var i = 0; i < yard.Count; i++)
{
var yardPoint = yard[i];
return HousePlacementResult.Valid;
}
foreach (var house in map.GetMultisInSector<BaseHouse>(yardPoint))
private static bool CheckYard(Map map, int tileX, int tileY, int yardSize, Direction houseFacing)
{
var isSouthFacing = (houseFacing & Direction.South) != 0;
var isEastFacing = (houseFacing & Direction.East) != 0;
for (var xOffset = -yardSize; xOffset <= yardSize; ++xOffset)
{
var absXOffset = Math.Abs(xOffset);
for (var yOffset = -yardSize; yOffset <= yardSize; ++yOffset)
{
if (house.Contains(yard[i]))
var absYOffset = Math.Abs(yOffset);
var yardPoint = new Point2D(tileX + xOffset, tileY + yOffset);
bool inSouthYard = yOffset > 0 && yOffset <= yardSize && absXOffset <= 1;
bool inEastYard = xOffset > 0 && xOffset <= yardSize && absYOffset <= 1;
bool inNorthYard = yOffset < 0 && yOffset >= -yardSize && absXOffset <= 1;
bool inWestYard = xOffset < 0 && xOffset >= -yardSize && absYOffset <= 1;
// Check each house at this point
foreach (var house in map.GetMultisInSector<BaseHouse>(yardPoint))
{
return HousePlacementResult.BadStatic; // Broke rule #3
if (!house.Contains(yardPoint))
{
continue;
}
var existingHouseFacing = house.HouseDirection;
var existingHouseIsSouthFacing = (existingHouseFacing & Direction.South) != 0;
var existingHouseIsEastFacing = (existingHouseFacing & Direction.East) != 0;
// Sub-Rule 1: No houses within immediate proximity (1 tile radius)
if (absXOffset <= 1 && absYOffset <= 1)
{
return false;
}
// Sub-Rule 2: If we're south facing, protect our south yard
if (isSouthFacing && inSouthYard)
{
return false;
}
// Sub-Rule 3: If we're east facing, protect our east yard
if (isEastFacing && inEastYard)
{
return false;
}
// Sub-Rule 4: If there's a south-facing house to our north, respect its yard
if (inNorthYard && existingHouseIsSouthFacing)
{
return false;
}
// Sub-Rule 5: If there's an east-facing house to our west, respect its yard
if (inWestYard && existingHouseIsEastFacing)
{
return false;
}
}
}
}
// TODO: Should we check for MultiTilesAt each yard point?
// for (var i = 0; i < yard.Count; i++)
// {
// var yardPoint = yard[i];
//
// foreach (var tiles in map.GetMultiTilesAt(yardPoint))
// {
// for (int j = 0; j < tiles.Length; ++j)
// {
// if ((TileData.ItemTable[tiles[j].ID & TileData.MaxItemValue].Flags & (TileFlag.Impassable | TileFlag.Surface)) != 0)
// {
// return HousePlacementResult.BadStatic; // Broke rule #3
// }
// }
// }
// }
return HousePlacementResult.Valid;
return true;
}
}
}

View file

@ -303,7 +303,7 @@ public class HousePlacementEntry
public HousePlacementEntry(
Type type, int description, int storage, int lockdowns, int newStorage, int newLockdowns,
int vendors, int cost, int xOffset, int yOffset, int zOffset, int multiID
int vendors, int cost, int xOffset, int yOffset, int zOffset, int multiID, Direction direction = Direction.South
)
{
Type = type;
@ -318,6 +318,7 @@ public class HousePlacementEntry
Offset = new Point3D(xOffset, yOffset, zOffset);
MultiID = multiID;
HouseDirection = direction;
}
public Type Type { get; }
@ -334,6 +335,8 @@ public class HousePlacementEntry
public Point3D Offset { get; }
public Direction HouseDirection { get; }
public static HousePlacementEntry[] ClassicHouses { get; } =
{
new(typeof(SmallOldHouse), 1011303, 425, 212, 489, 244, 10, 37000, 0, 4, 0, 0x0064),
@ -1981,7 +1984,7 @@ public class HousePlacementEntry
prevHouse.Delete();
var res = HousePlacement.Check(from, MultiID, center, out var toMove);
var res = HousePlacement.Check(from, MultiID, center, out var toMove, HouseDirection);
switch (res)
{
@ -2008,21 +2011,18 @@ public class HousePlacementEntry
$"{Cost} gold would have been withdrawn from your bank if you were not a GM."
);
}
else if (Banker.Withdraw(from, Cost))
{
// ~1_AMOUNT~ gold has been withdrawn from your bank box.
from.SendLocalizedMessage(1060398, Cost.ToString());
}
else
{
if (Banker.Withdraw(from, Cost))
{
// ~1_AMOUNT~ gold has been withdrawn from your bank box.
from.SendLocalizedMessage(1060398, Cost.ToString());
}
else
{
house.RemoveKeys(from);
house.Delete();
// You do not have the funds available in your bank box to purchase this house. Try placing a smaller house, or adding gold or checks to your bank box.
from.SendLocalizedMessage(1060646);
return;
}
house.RemoveKeys(from);
house.Delete();
// You do not have the funds available in your bank box to purchase this house. Try placing a smaller house, or adding gold or checks to your bank box.
from.SendLocalizedMessage(1060646);
return;
}
house.MoveToWorld(center, from.Map);
@ -2087,7 +2087,7 @@ public class HousePlacementEntry
}
var center = new Point3D(p.X - Offset.X, p.Y - Offset.Y, p.Z - Offset.Z);
var res = HousePlacement.Check(from, MultiID, center, out var toMove);
var res = HousePlacement.Check(from, MultiID, center, out var toMove, HouseDirection);
switch (res)
{

View file

@ -158,6 +158,8 @@ public static class IncomingMobilePackets
trade.To.Plat = plat;
trade.UpdateToCurrency();
}
cont.ClearChecks();
}
}
}

View file

@ -21,23 +21,18 @@ namespace Server.SkillHandlers
m.SendLocalizedMessage(500397); // To whom do you wish to grovel?
return TimeSpan.FromHours(6.0);
return TimeSpan.FromSeconds(30.0);
}
private class InternalTarget : Target
{
private bool m_SetSkillTime = true;
public InternalTarget() : base(12, false, TargetFlags.None)
{
}
protected override void OnTargetFinish(Mobile from)
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
{
if (m_SetSkillTime)
{
from.NextSkillTime = Core.TickCount;
}
from.NextSkillTime = Core.TickCount;
}
protected override void OnTarget(Mobile from, object targeted)
@ -81,8 +76,6 @@ namespace Server.SkillHandlers
from.Animate(32, 5, 1, true, false, 0); // Bow
new InternalTimer(from, targ).Start();
m_SetSkillTime = false;
}
}
else // Not a Mobile
@ -125,16 +118,7 @@ namespace Server.SkillHandlers
else if (m_From.CheckTargetSkill(SkillName.Begging, m_Target, 0.0, 100.0))
{
var toConsume = theirPack.GetAmount(typeof(Gold)) / 10;
var max = 10 + m_From.Fame / 2500;
if (max > 14)
{
max = 14;
}
else if (max < 10)
{
max = 10;
}
var max = Math.Clamp(10 + m_From.Fame / 2500, 10, 14);
if (toConsume > max)
{

View file

@ -18,7 +18,7 @@ namespace Server.SkillHandlers
src.SendLocalizedMessage(500819); // Where will you search?
src.Target = new InternalTarget();
return TimeSpan.FromSeconds(6.0);
return TimeSpan.FromSeconds(30.0);
}
private class InternalTarget : Target
@ -27,6 +27,11 @@ namespace Server.SkillHandlers
{
}
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
{
from.NextSkillTime = Core.TickCount;
}
protected override void OnTarget(Mobile src, object targ)
{
var foundAnyone = false;
@ -108,6 +113,8 @@ namespace Server.SkillHandlers
{
src.SendLocalizedMessage(500817); // You can see nothing hidden there.
}
src.NextSkillTime = Core.TickCount + 6000; // 6 seconds cooldown
}
}
}

View file

@ -27,27 +27,22 @@ namespace Server.SkillHandlers
from.RevealingAction();
from.SendLocalizedMessage(1049525); // Whom do you wish to calm?
from.Target = new InternalTarget(from, instrument);
from.NextSkillTime = Core.TickCount + 21600000;
from.NextSkillTime = Core.TickCount + 30000; // 30s timeout on the targeter
}
private class InternalTarget : Target
{
private readonly BaseInstrument m_Instrument;
private bool m_SetSkillTime = true;
public InternalTarget(Mobile from, BaseInstrument instrument) : base(
BaseInstrument.GetBardRange(from, SkillName.Peacemaking),
false,
TargetFlags.None
) =>
m_Instrument = instrument;
) => m_Instrument = instrument;
protected override void OnTargetFinish(Mobile from)
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
{
if (m_SetSkillTime)
{
from.NextSkillTime = Core.TickCount;
}
from.NextSkillTime = Core.TickCount;
}
protected override void OnTarget(Mobile from, object targeted)
@ -60,21 +55,19 @@ namespace Server.SkillHandlers
}
else if (from.Region.IsPartOf<SafeZone>())
{
from.SendMessage("You may not peacemake in this area.");
from.SendMessage("You may not use peacemaking in this area.");
}
else if (targ.Region.IsPartOf<SafeZone>())
{
from.SendMessage("You may not peacemake there.");
from.SendMessage("You may not use peacemaking there.");
}
else if (!m_Instrument.IsChildOf(from.Backpack))
{
from.SendLocalizedMessage(
1062488
); // The instrument you are trying to play is no longer in your backpack!
// The instrument you are trying to play is no longer in your backpack!
from.SendLocalizedMessage(1062488);
}
else
{
m_SetSkillTime = false;
from.NextSkillTime = Core.TickCount + 10000;
if (targeted == from)
@ -149,17 +142,14 @@ namespace Server.SkillHandlers
if (!from.CanBeHarmful(targ, false))
{
from.SendLocalizedMessage(1049528);
m_SetSkillTime = true;
}
else if (bc?.Uncalmable == true)
{
from.SendLocalizedMessage(1049526); // You have no chance of calming that creature.
m_SetSkillTime = true;
}
else if (bc?.BardPacified == true)
{
from.SendLocalizedMessage(1049527); // That creature is already being calmed.
m_SetSkillTime = true;
}
else if (!BaseInstrument.CheckMusicianship(from))
{
@ -193,10 +183,10 @@ namespace Server.SkillHandlers
targ.Combatant = null;
targ.Warmode = false;
from.SendLocalizedMessage(1049532); // You play hypnotic music, calming your target.
if (bc != null)
{
from.SendLocalizedMessage(1049532); // You play hypnotic music, calming your target.
// You play hypnotic music, calming your target.
var seconds = 100 - diff / 1.5;
if (seconds > 120)
@ -212,8 +202,7 @@ namespace Server.SkillHandlers
}
else
{
from.SendLocalizedMessage(1049532); // You play hypnotic music, calming your target.
// You play hypnotic music, calming your target.
// You hear lovely music, and forget to continue battling!
targ.SendLocalizedMessage(500616);
}

View file

@ -12,510 +12,503 @@ using Server.Spells.Ninjitsu;
using Server.Spells.Seventh;
using Server.Targeting;
namespace Server.SkillHandlers
namespace Server.SkillHandlers;
public static class Stealing
{
public static class Stealing
public static bool ClassicMode { get; private set; }
public static bool SuspendOnMurder { get; private set; }
public static int MaxWeightToSteal { get; private set; }
public static bool CanStealContainers { get; private set; }
public static void Configure()
{
public static readonly bool ClassicMode = false;
public static readonly bool SuspendOnMurder = false;
private const int MaxWeightToSteal = 10;
ClassicMode = ServerConfiguration.GetSetting("stealing.classicMode", !Core.AOS);
SuspendOnMurder = ServerConfiguration.GetSetting("stealing.suspendOnMurder", !Core.AOS);
CanStealContainers = ServerConfiguration.GetSetting("stealing.canStealContainers", !Core.AOS);
MaxWeightToSteal = ServerConfiguration.GetSetting("stealing.maxWeightToSteal", 10);
}
public static void Initialize()
public static void Initialize()
{
SkillInfo.Table[33].Callback = OnUse;
}
public static bool IsInGuild(Mobile m) => m is PlayerMobile mobile && mobile.NpcGuild == NpcGuild.ThievesGuild;
public static bool IsInnocentTo(Mobile from, Mobile to) => Notoriety.Compute(from, to) == Notoriety.Innocent;
public static bool IsEmptyHanded(Mobile from) =>
from.FindItemOnLayer(Layer.OneHanded) == null && from.FindItemOnLayer(Layer.TwoHanded) == null;
public static TimeSpan OnUse(Mobile m)
{
if (!IsEmptyHanded(m))
{
SkillInfo.Table[33].Callback = OnUse;
m.SendLocalizedMessage(1005584); // Both hands must be free to steal.
}
else if (m.Region.IsPartOf<SafeZone>())
{
m.SendMessage("You may not steal in this area.");
}
else
{
m.Target = new StealingTarget(m);
m.RevealingAction();
m.SendLocalizedMessage(502698); // Which item do you want to steal?
}
public static bool IsInGuild(Mobile m) => m is PlayerMobile mobile && mobile.NpcGuild == NpcGuild.ThievesGuild;
return TimeSpan.FromSeconds(30.0);
}
public static bool IsInnocentTo(Mobile from, Mobile to) => Notoriety.Compute(from, to) == Notoriety.Innocent;
private class StealingTarget : Target
{
private readonly Mobile _thief;
public static bool IsEmptyHanded(Mobile from)
public StealingTarget(Mobile thief) : base(1, false, TargetFlags.None)
{
if (from.FindItemOnLayer(Layer.OneHanded) != null)
{
return false;
}
if (from.FindItemOnLayer(Layer.TwoHanded) != null)
{
return false;
}
return true;
_thief = thief;
AllowNonlocal = true;
}
public static TimeSpan OnUse(Mobile m)
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
{
if (!IsEmptyHanded(m))
{
m.SendLocalizedMessage(1005584); // Both hands must be free to steal.
}
else if (m.Region.IsPartOf<SafeZone>())
{
m.SendMessage("You may not steal in this area.");
}
else
{
m.Target = new StealingTarget(m);
m.RevealingAction();
m.SendLocalizedMessage(502698); // Which item do you want to steal?
}
return TimeSpan.FromSeconds(10.0);
from.NextSkillTime = Core.TickCount;
}
private class StealingTarget : Target
private Item TryStealItem(Item toSteal, ref bool caught)
{
private readonly Mobile m_Thief;
Item stolen = null;
public StealingTarget(Mobile thief) : base(1, false, TargetFlags.None)
var root = toSteal.RootParent;
var mobRoot = root as Mobile;
var rootIsPlayer = mobRoot?.Player == true;
var si = toSteal.Parent == null || !toSteal.Movable
? StealableArtifacts.GetStealableInstance(toSteal)
: null;
if (!IsEmptyHanded(_thief))
{
m_Thief = thief;
AllowNonlocal = true;
_thief.SendLocalizedMessage(1005584); // Both hands must be free to steal.
}
private Item TryStealItem(Item toSteal, ref bool caught)
else if (_thief.Region.IsPartOf<SafeZone>())
{
Item stolen = null;
_thief.SendMessage("You may not steal in this area.");
}
else if ((_thief as PlayerMobile)?.Young == true && (rootIsPlayer || mobRoot is BaseCreature))
{
_thief.SendLocalizedMessage(502700); // You cannot steal from people or monsters right now. Practice on chests and barrels.
}
else if (rootIsPlayer && !IsInGuild(_thief))
{
_thief.SendLocalizedMessage(1005596); // You must be in the thieves guild to steal from other players.
}
else if (SuspendOnMurder && rootIsPlayer && IsInGuild(_thief) && _thief.Kills > 0)
{
_thief.SendLocalizedMessage(502706); // You are currently suspended from the thieves guild.
}
else if ((mobRoot as PlayerMobile)?.Young == true)
{
_thief.SendLocalizedMessage(502699); // You cannot steal from the Young.
}
else if ((root as BaseVendor)?.IsInvulnerable == true)
{
_thief.SendLocalizedMessage(1005598); // You can't steal from shopkeepers.
}
else if (root is PlayerVendor)
{
_thief.SendLocalizedMessage(502709); // You can't steal from vendors.
}
else if (!_thief.CanSee(toSteal))
{
_thief.SendLocalizedMessage(500237); // Target can not be seen.
}
else if (toSteal is Sigil sig)
{
var pl = PlayerState.Find(_thief);
var faction = pl?.Faction;
var root = toSteal.RootParent;
var mobRoot = root as Mobile;
var rootIsPlayer = mobRoot?.Player == true;
StealableArtifacts.StealableInstance si = toSteal.Parent == null || !toSteal.Movable
? StealableArtifacts.GetStealableInstance(toSteal)
: null;
if (!IsEmptyHanded(m_Thief))
if (!_thief.InRange(sig.GetWorldLocation(), 1))
{
m_Thief.SendLocalizedMessage(1005584); // Both hands must be free to steal.
_thief.SendLocalizedMessage(502703); // You must be standing next to an item to steal it.
}
else if (m_Thief.Region.IsPartOf<SafeZone>())
else if (root != null) // not on the ground
{
m_Thief.SendMessage("You may not steal in this area.");
_thief.SendLocalizedMessage(502710); // You can't steal that!
}
else if ((m_Thief as PlayerMobile)?.Young == true && (rootIsPlayer || mobRoot is BaseCreature))
else if (faction == null)
{
m_Thief.SendLocalizedMessage(502700); // You cannot steal from people or monsters right now. Practice on chests and barrels.
_thief.SendLocalizedMessage(1005588); // You must join a faction to do that
}
else if (rootIsPlayer && !IsInGuild(m_Thief))
else if (!_thief.CanBeginAction<IncognitoSpell>())
{
m_Thief.SendLocalizedMessage(1005596); // You must be in the thieves guild to steal from other players.
_thief.SendLocalizedMessage(1010581); // You cannot steal the sigil when you are incognito
}
else if (SuspendOnMurder && rootIsPlayer && IsInGuild(m_Thief) && m_Thief.Kills > 0)
else if (DisguisePersistence.IsDisguised(_thief))
{
m_Thief.SendLocalizedMessage(502706); // You are currently suspended from the thieves guild.
_thief.SendLocalizedMessage(1010583); // You cannot steal the sigil while disguised
}
else if ((mobRoot as PlayerMobile)?.Young == true)
else if (!_thief.CanBeginAction<PolymorphSpell>())
{
m_Thief.SendLocalizedMessage(502699); // You cannot steal from the Young.
_thief.SendLocalizedMessage(1010582); // You cannot steal the sigil while polymorphed
}
else if ((root as BaseVendor)?.IsInvulnerable == true)
else if (TransformationSpellHelper.UnderTransformation(_thief))
{
m_Thief.SendLocalizedMessage(1005598); // You can't steal from shopkeepers.
_thief.SendLocalizedMessage(1061622); // You cannot steal the sigil while in that form.
}
else if (root is PlayerVendor)
else if (AnimalForm.UnderTransformation(_thief))
{
m_Thief.SendLocalizedMessage(502709); // You can't steal from vendors.
_thief.SendLocalizedMessage(1063222); // You cannot steal the sigil while mimicking an animal.
}
else if (!m_Thief.CanSee(toSteal))
else if (pl.IsLeaving)
{
m_Thief.SendLocalizedMessage(500237); // Target can not be seen.
// You are currently quitting a faction and cannot steal the town sigil
_thief.SendLocalizedMessage(1005589);
}
else if (toSteal is Sigil sig)
else if (sig.IsBeingCorrupted && sig.LastMonolith.Faction == faction)
{
var pl = PlayerState.Find(m_Thief);
var faction = pl?.Faction;
if (!m_Thief.InRange(sig.GetWorldLocation(), 1))
{
m_Thief.SendLocalizedMessage(502703); // You must be standing next to an item to steal it.
}
else if (root != null) // not on the ground
{
m_Thief.SendLocalizedMessage(502710); // You can't steal that!
}
else if (faction != null)
{
if (!m_Thief.CanBeginAction<IncognitoSpell>())
{
m_Thief.SendLocalizedMessage(1010581); // You cannot steal the sigil when you are incognito
}
else if (DisguisePersistence.IsDisguised(m_Thief))
{
m_Thief.SendLocalizedMessage(1010583); // You cannot steal the sigil while disguised
}
else if (!m_Thief.CanBeginAction<PolymorphSpell>())
{
m_Thief.SendLocalizedMessage(1010582); // You cannot steal the sigil while polymorphed
}
else if (TransformationSpellHelper.UnderTransformation(m_Thief))
{
m_Thief.SendLocalizedMessage(1061622); // You cannot steal the sigil while in that form.
}
else if (AnimalForm.UnderTransformation(m_Thief))
{
m_Thief.SendLocalizedMessage(1063222); // You cannot steal the sigil while mimicking an animal.
}
else if (pl.IsLeaving)
{
// You are currently quitting a faction and cannot steal the town sigil
m_Thief.SendLocalizedMessage(1005589);
}
else if (sig.IsBeingCorrupted && sig.LastMonolith.Faction == faction)
{
m_Thief.SendLocalizedMessage(1005590); // You cannot steal your own sigil
}
else if (sig.IsPurifying)
{
m_Thief.SendLocalizedMessage(1005592); // You cannot steal this sigil until it has been purified
}
else if (m_Thief.CheckTargetSkill(SkillName.Stealing, toSteal, 80.0, 80.0))
{
if (Sigil.ExistsOn(m_Thief))
{
// The sigil has gone back to its home location because you already have a sigil.
m_Thief.SendLocalizedMessage(1010258);
}
else if (m_Thief.Backpack?.CheckHold(m_Thief, sig, false, true) != true)
{
// The sigil has gone home because your backpack is full
m_Thief.SendLocalizedMessage(1010259);
}
else
{
if (sig.IsBeingCorrupted)
{
sig.GraceStart = Core.Now; // begin grace period
}
m_Thief.SendLocalizedMessage(1010586); // YOU STOLE THE SIGIL!!! (woah, calm down now)
if (sig.LastMonolith?.Sigil != null)
{
sig.LastMonolith.Sigil = null;
sig.LastStolen = Core.Now;
}
return sig;
}
}
else
{
m_Thief.SendLocalizedMessage(1005594); // You do not have enough skill to steal the sigil
}
}
else
{
m_Thief.SendLocalizedMessage(1005588); // You must join a faction to do that
}
_thief.SendLocalizedMessage(1005590); // You cannot steal your own sigil
}
else if (m_Thief.Backpack?.CheckHold(m_Thief, toSteal, false, true) != true)
else if (sig.IsPurifying)
{
m_Thief.SendLocalizedMessage(1048147); // Your backpack can't hold anything else.
_thief.SendLocalizedMessage(1005592); // You cannot steal this sigil until it has been purified
}
else if (si == null && (toSteal.Parent == null || !toSteal.Movable))
else if (!_thief.CheckTargetSkill(SkillName.Stealing, toSteal, 80.0, 80.0))
{
m_Thief.SendLocalizedMessage(502710); // You can't steal that!
_thief.SendLocalizedMessage(1005594); // You do not have enough skill to steal the sigil
}
else if (toSteal.LootType == LootType.Newbied || toSteal.CheckBlessed(mobRoot))
else if (Sigil.ExistsOn(_thief))
{
m_Thief.SendLocalizedMessage(502710); // You can't steal that!
// The sigil has gone back to its home location because you already have a sigil.
_thief.SendLocalizedMessage(1010258);
}
else if (Core.AOS && si == null && toSteal is Container)
else if (_thief.Backpack?.CheckHold(_thief, sig, false, true) != true)
{
m_Thief.SendLocalizedMessage(502710); // You can't steal that!
}
else if (!m_Thief.InRange(toSteal.GetWorldLocation(), 1))
{
m_Thief.SendLocalizedMessage(502703); // You must be standing next to an item to steal it.
}
else if (si != null && m_Thief.Skills.Stealing.Value < 100.0)
{
// You're not skilled enough to attempt the theft of this item.
m_Thief.SendLocalizedMessage(1060025, "", 0x66D);
}
else if (toSteal.Parent is Mobile)
{
m_Thief.SendLocalizedMessage(1005585); // You cannot steal items which are equipped.
}
else if (root == m_Thief)
{
m_Thief.SendLocalizedMessage(502704); // You catch yourself red-handed.
}
else if (mobRoot?.AccessLevel > AccessLevel.Player)
{
m_Thief.SendLocalizedMessage(502710); // You can't steal that!
}
else if (mobRoot != null && !m_Thief.CanBeHarmful(mobRoot))
{
}
else if (root is Corpse)
{
m_Thief.SendLocalizedMessage(502710); // You can't steal that!
// The sigil has gone home because your backpack is full
_thief.SendLocalizedMessage(1010259);
}
else
{
var w = toSteal.Weight + toSteal.TotalWeight;
if (w > MaxWeightToSteal)
if (sig.IsBeingCorrupted)
{
// This item is too heavy to steal from someone's backpack.
m_Thief.SendLocalizedMessage(502722);
sig.GraceStart = Core.Now; // begin grace period
}
else
_thief.SendLocalizedMessage(1010586); // YOU STOLE THE SIGIL!!! (woah, calm down now)
if (sig.LastMonolith?.Sigil != null)
{
if (toSteal.Stackable && toSteal.Amount > 1)
sig.LastMonolith.Sigil = null;
sig.LastStolen = Core.Now;
}
return sig;
}
}
else if (_thief.Backpack?.CheckHold(_thief, toSteal, false, true) != true)
{
_thief.SendLocalizedMessage(1048147); // Your backpack can't hold anything else.
}
else if (si == null && (toSteal.Parent == null || !toSteal.Movable) || toSteal.LootType == LootType.Newbied ||
toSteal.CheckBlessed(mobRoot) || !CanStealContainers && si == null && toSteal is Container)
{
_thief.SendLocalizedMessage(502710); // You can't steal that!
}
else if (!_thief.InRange(toSteal.GetWorldLocation(), 1))
{
_thief.SendLocalizedMessage(502703); // You must be standing next to an item to steal it.
}
else if (si != null && _thief.Skills.Stealing.Value < 100.0)
{
// You're not skilled enough to attempt the theft of this item.
_thief.SendLocalizedMessage(1060025, "", 0x66D);
}
else if (toSteal.Parent is Mobile)
{
_thief.SendLocalizedMessage(1005585); // You cannot steal items which are equipped.
}
else if (root == _thief)
{
_thief.SendLocalizedMessage(502704); // You catch yourself red-handed.
}
else if (mobRoot?.AccessLevel > AccessLevel.Player)
{
_thief.SendLocalizedMessage(502710); // You can't steal that!
}
else if (mobRoot != null && !_thief.CanBeHarmful(mobRoot))
{
}
else if (root is Corpse)
{
_thief.SendLocalizedMessage(502710); // You can't steal that!
}
else
{
var w = toSteal.Weight + toSteal.TotalWeight;
if (w > MaxWeightToSteal)
{
// This item is too heavy to steal from someone's backpack.
_thief.SendLocalizedMessage(502722);
}
else
{
if (toSteal.Stackable && toSteal.Amount > 1)
{
var maxAmount = Math.Clamp(
(int)(_thief.Skills.Stealing.Value / 10.0 / toSteal.Weight),
1,
toSteal.Amount
);
var amount = Utility.RandomMinMax(1, maxAmount);
if (amount >= toSteal.Amount)
{
var maxAmount = Math.Clamp(
(int)(m_Thief.Skills.Stealing.Value / 10.0 / toSteal.Weight),
1,
toSteal.Amount
);
var pileWeight = (int)Math.Ceiling(toSteal.Weight * toSteal.Amount);
pileWeight *= 10;
var amount = Utility.RandomMinMax(1, maxAmount);
if (amount >= toSteal.Amount)
{
var pileWeight = (int)Math.Ceiling(toSteal.Weight * toSteal.Amount);
pileWeight *= 10;
if (m_Thief.CheckTargetSkill(
if (_thief.CheckTargetSkill(
SkillName.Stealing,
toSteal,
pileWeight - 22.5,
pileWeight + 27.5
))
{
stolen = toSteal;
}
}
else
{
var pileWeight = (int)Math.Ceiling(toSteal.Weight * amount);
pileWeight *= 10;
if (m_Thief.CheckTargetSkill(
SkillName.Stealing,
toSteal,
pileWeight - 22.5,
pileWeight + 27.5
))
{
stolen = Mobile.LiftItemDupe(toSteal, toSteal.Amount - amount) ?? toSteal;
}
}
}
else
{
var iw = (int)Math.Ceiling(w);
iw *= 10;
if (m_Thief.CheckTargetSkill(SkillName.Stealing, toSteal, iw - 22.5, iw + 27.5))
{
stolen = toSteal;
}
}
if (stolen != null)
{
m_Thief.SendLocalizedMessage(502724); // You successfully steal the item.
if (si != null)
{
toSteal.Movable = true;
si.Item = null;
}
}
else
{
m_Thief.SendLocalizedMessage(502723); // You fail to steal the item.
}
var pileWeight = (int)Math.Ceiling(toSteal.Weight * amount);
pileWeight *= 10;
caught = m_Thief.Skills.Stealing.Value < Utility.Random(150);
}
}
return stolen;
}
protected override void OnTarget(Mobile from, object target)
{
from.RevealingAction();
Item stolen = null;
IEntity root = null;
var caught = false;
if (target is Item item)
{
root = item.RootParent;
stolen = TryStealItem(item, ref caught);
}
else if (target is Mobile mobile)
{
var pack = mobile.Backpack;
if (pack?.Items.Count > 0)
{
root = mobile;
stolen = TryStealItem(pack.Items.RandomElement(), ref caught);
}
}
else
{
m_Thief.SendLocalizedMessage(502710); // You can't steal that!
}
var mobRoot = root as Mobile;
if (stolen != null)
{
from.AddToBackpack(stolen);
if (!(stolen is Container || stolen.Stackable))
{
StolenItem.Add(stolen, m_Thief, mobRoot);
}
}
var corpse = root as Corpse;
if (caught)
{
if (root == null || corpse?.IsCriminalAction(m_Thief) == true)
{
m_Thief.CriminalAction(false);
}
else if (mobRoot != null)
{
if (!IsInGuild(mobRoot) && IsInnocentTo(m_Thief, mobRoot))
{
m_Thief.CriminalAction(false);
}
var message = $"You notice {m_Thief.Name} trying to steal from {mobRoot.Name}.";
foreach (var ns in m_Thief.GetClientsInRange(8))
{
if (ns.Mobile != m_Thief)
if (_thief.CheckTargetSkill(
SkillName.Stealing,
toSteal,
pileWeight - 22.5,
pileWeight + 27.5
))
{
ns.Mobile.SendMessage(message);
stolen = Mobile.LiftItemDupe(toSteal, toSteal.Amount - amount) ?? toSteal;
}
}
}
}
else if (corpse?.IsCriminalAction(m_Thief) == true)
{
m_Thief.CriminalAction(false);
}
if (mobRoot?.Player == true && m_Thief is PlayerMobile pm &&
IsInnocentTo(pm, mobRoot) && !IsInGuild(mobRoot))
{
pm.PermaFlags.Add(mobRoot);
pm.Delta(MobileDelta.Noto);
}
}
}
}
public class StolenItem
{
public static readonly TimeSpan StealTime = TimeSpan.FromMinutes(2.0);
private static readonly Queue<StolenItem> m_Queue = new();
public StolenItem(Item stolen, Mobile thief, Mobile victim)
{
Stolen = stolen;
Thief = thief;
Victim = victim;
Expires = Core.Now + StealTime;
}
public Item Stolen { get; }
public Mobile Thief { get; }
public Mobile Victim { get; }
public DateTime Expires { get; private set; }
public bool IsExpired => Core.Now >= Expires;
public static void Add(Item item, Mobile thief, Mobile victim)
{
Clean();
m_Queue.Enqueue(new StolenItem(item, thief, victim));
}
public static bool IsStolen(Item item)
{
Mobile victim = null;
return IsStolen(item, ref victim);
}
public static bool IsStolen(Item item, ref Mobile victim)
{
Clean();
foreach (var si in m_Queue)
{
if (si.Stolen == item && !si.IsExpired)
{
victim = si.Victim;
return true;
}
}
return false;
}
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
public static void ReturnOnDeath(Mobile killed)
{
Clean();
var corpse = killed.Corpse;
foreach (var si in m_Queue)
{
if (si.Stolen.RootParent == corpse && si.Victim != null && !si.IsExpired)
{
if (si.Victim.AddToBackpack(si.Stolen))
{
si.Victim.SendLocalizedMessage(1010464); // the item that was stolen is returned to you.
}
else
{
si.Victim.SendLocalizedMessage(1010463); // the item that was stolen from you falls to the ground.
var iw = (int)Math.Ceiling(w);
iw *= 10;
if (_thief.CheckTargetSkill(SkillName.Stealing, toSteal, iw - 22.5, iw + 27.5))
{
stolen = toSteal;
}
}
si.Expires = Core.Now; // such a hack
if (stolen != null)
{
_thief.SendLocalizedMessage(502724); // You successfully steal the item.
if (si != null)
{
toSteal.Movable = true;
si.Item = null;
}
}
else
{
_thief.SendLocalizedMessage(502723); // You fail to steal the item.
}
caught = _thief.Skills.Stealing.Value < Utility.Random(150);
}
}
return stolen;
}
public static void Clean()
protected override void OnTarget(Mobile from, object target)
{
while (m_Queue.Count > 0)
{
var si = m_Queue.Peek();
from.RevealingAction();
if (si.IsExpired)
Item stolen = null;
IEntity root = null;
var caught = false;
if (target is Item item)
{
root = item.RootParent;
stolen = TryStealItem(item, ref caught);
}
else if (target is Mobile mobile)
{
var pack = mobile.Backpack;
if (pack?.Items.Count > 0)
{
m_Queue.Dequeue();
}
else
{
break;
root = mobile;
stolen = TryStealItem(pack.Items.RandomElement(), ref caught);
}
}
else
{
_thief.SendLocalizedMessage(502710); // You can't steal that!
}
var mobRoot = root as Mobile;
if (stolen != null)
{
from.AddToBackpack(stolen);
if (!(stolen is Container || stolen.Stackable))
{
StolenItem.Add(stolen, _thief, mobRoot);
}
}
var corpse = root as Corpse;
if (caught)
{
if (root == null || corpse?.IsCriminalAction(_thief) == true)
{
_thief.CriminalAction(false);
}
else if (mobRoot != null)
{
if (!IsInGuild(mobRoot) && IsInnocentTo(_thief, mobRoot))
{
_thief.CriminalAction(false);
}
var message = $"You notice {_thief.Name} trying to steal from {mobRoot.Name}.";
foreach (var ns in _thief.GetClientsInRange(8))
{
if (ns.Mobile != _thief)
{
ns.Mobile.SendMessage(message);
}
}
}
}
else if (corpse?.IsCriminalAction(_thief) == true)
{
_thief.CriminalAction(false);
}
if (mobRoot?.Player == true && _thief is PlayerMobile pm &&
IsInnocentTo(pm, mobRoot) && !IsInGuild(mobRoot))
{
pm.PermaFlags.Add(mobRoot);
pm.Delta(MobileDelta.Noto);
}
from.NextSkillTime = Core.TickCount + 10000; // 10 seconds cooldown
}
}
}
public class StolenItem
{
public static readonly TimeSpan StealTime = TimeSpan.FromMinutes(2.0);
private static readonly Queue<StolenItem> _queue = [];
public StolenItem(Item stolen, Mobile thief, Mobile victim)
{
Stolen = stolen;
Thief = thief;
Victim = victim;
Expires = Core.Now + StealTime;
}
public Item Stolen { get; }
public Mobile Thief { get; }
public Mobile Victim { get; }
public DateTime Expires { get; private set; }
public bool IsExpired => Core.Now >= Expires;
public static void Add(Item item, Mobile thief, Mobile victim)
{
Clean();
_queue.Enqueue(new StolenItem(item, thief, victim));
}
public static bool IsStolen(Item item)
{
Mobile victim = null;
return IsStolen(item, ref victim);
}
public static bool IsStolen(Item item, ref Mobile victim)
{
Clean();
foreach (var si in _queue)
{
if (si.Stolen == item && !si.IsExpired)
{
victim = si.Victim;
return true;
}
}
return false;
}
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
public static void ReturnOnDeath(Mobile killed)
{
Clean();
var corpse = killed.Corpse;
foreach (var si in _queue)
{
if (si.Stolen.RootParent != corpse || si.Victim == null || si.IsExpired)
{
continue;
}
if (si.Victim.AddToBackpack(si.Stolen))
{
si.Victim.SendLocalizedMessage(1010464); // the item that was stolen is returned to you.
}
else
{
si.Victim.SendLocalizedMessage(1010463); // the item that was stolen from you falls to the ground.
}
si.Expires = Core.Now; // such a hack
}
}
public static void Clean()
{
while (_queue.Count > 0)
{
var si = _queue.Peek();
if (!si.IsExpired)
{
break;
}
_queue.Dequeue();
}
}
}

View file

@ -54,7 +54,7 @@ namespace Server.Spells.Fourth
var oldDexOffset = SpellHelper.GetCurse(caster, m, StatType.Dex);
var oldIntOffset = SpellHelper.GetCurse(caster, m, StatType.Int);
if (oldStrOffset <= newStrOffset && oldDexOffset <= newDexOffset && oldIntOffset <= newIntOffset)
if (oldStrOffset > newStrOffset && oldDexOffset > newDexOffset && oldIntOffset > newIntOffset)
{
return false;
}

View file

@ -108,6 +108,6 @@ Thank you for supporting us! You can find out how by visiting the [sponsors](./S
- [Karasho](https://github.com/andreakarasho), [Jaedan](https://github.com/jaedan) and the ClassicUO Community
</br></br>
<p align=center>Development Tools & Plugins provided with &hearts; by <br /><a href="https://www.jetbrains.com/?from=ModernUO"><img align=middle src="https://user-images.githubusercontent.com/3953314/86882249-cfb2ea00-c0a4-11ea-9cec-bf3f3bcc6f28.png" height="64px" alt="JetBrains" title="JetBrains" /></a>
<p align=center>Development Tools & Plugins provided with &hearts; by <br /><a href="https://www.jetbrains.com/?from=ModernUO"><img align=middle src="https://github.com/user-attachments/assets/07b12bd0-ca00-472f-8a47-ab48461f3f1d" height="64px" alt="JetBrains" title="JetBrains" /></a><br />
<a href="https://material-theme.com/"><img align=center src="https://material-theme.com/img/logo/material-oceanic.svg" width="64px" alt="Material Theme" title="Material Theme"></a>
</p>

View file

@ -3,6 +3,7 @@
Thank you to all of our generous sponsors that make ModernUO possible.
**A special thank you to the following sponsors for their considerable contributions:**
* [Age of Shadows](https://ageofshadows.gg)
* [UO Outlands](https://uooutlands.com)
* Prayer ([MagnUm-Opus](https://discord.gg/CzDEq3vv2N))
@ -10,7 +11,7 @@ Thank you to all of our generous sponsors that make ModernUO possible.
We greatly appreciate the support! Use one of the following platforms below:
#### GitHub
[<img alt="Github Sponsors | ModernUO" data-canonical-src="https://camo.githubusercontent.com/a44b124b41d702cb4a66abb48cf746ffb7aabcc96d0c72cdb34e0bf8f761ff49/68747470733a2f2f6769746875622e6769746875626173736574732e636f6d2f696d616765732f6d6f64756c65732f736974652f73706f6e736f72732f6c6f676f2d6d6f6e612e737667" src="https://camo.githubusercontent.com/a44b124b41d702cb4a66abb48cf746ffb7aabcc96d0c72cdb34e0bf8f761ff49/68747470733a2f2f6769746875622e6769746875626173736574732e636f6d2f696d616765732f6d6f64756c65732f736974652f73706f6e736f72732f6c6f676f2d6d6f6e612e737667" width=128px />](https://github.com/sponsors/modernuo)
[<img alt="Github Sponsors | ModernUO" data-canonical-src="https://camo.githubusercontent.com/6e0df12df1fdf5c39e8afea60fda5e925322922f07e59e54f7af0f0d9166e771/68747470733a2f2f6769746875622e6769746875626173736574732e636f6d2f696d616765732f6d6f64756c65732f736974652f73706f6e736f72732f6c6f676f2d6d6f6e612e737667" src="https://camo.githubusercontent.com/6e0df12df1fdf5c39e8afea60fda5e925322922f07e59e54f7af0f0d9166e771/68747470733a2f2f6769746875622e6769746875626173736574732e636f6d2f696d616765732f6d6f64756c65732f736974652f73706f6e736f72732f6c6f676f2d6d6f6e612e737667" width=128px />](https://github.com/sponsors/modernuo)
#### Patreon
[<img alt="Patreon | ModernUO" data-canonical-src="https://user-images.githubusercontent.com/3953314/104968719-7c96e980-599b-11eb-839b-28745496b1a4.png" src="https://user-images.githubusercontent.com/3953314/104968719-7c96e980-599b-11eb-839b-28745496b1a4.png" width=128px />](https://muo.gg/patreon)