### Summary This PR transitions the spawner system from a simple radius-based model to a flexible 3D boundary system. ### Core Changes * **Replaced `HomeRange` with `SpawnBounds`**: Spawners now use a `Rectangle3D` to define spawn areas instead of a circular integer range. * **Backward Compatibility**: * The `HomeRange` property remains as a helper that generates square `SpawnBounds` centered on the spawner. * Included a migration path (v10 to v11) that automatically converts old range data into new bounds during deserialization. * **Dynamic Bounds Shifting**: If a spawner is moved, its `SpawnBounds` will automatically shift with it, provided the bounds are currently configured as a centered square. * **New Spawn Logic**: Added `SpawnLocationIsHome` toggle. If enabled, spawned mobiles treat their exact spawn coordinates as their "Home" rather than the spawner's location. ### Implementation Details * **Interface Updates**: Updated `ISpawner` to include `WalkingRange`, `SpawnBounds`, and `IsInSpawnBounds()`. * **UI Enhancements**: The Spawner Controller Gump now displays "Custom" for complex bounds and allows copying of the new boundary properties between spawners. * **Refactored Constructors**: Streamlined `BaseSpawner`, `ProximitySpawner`, and `RegionSpawner` constructors to support the new data types.
282 lines
6.4 KiB
C#
282 lines
6.4 KiB
C#
using System;
|
|
using System.Runtime.CompilerServices;
|
|
using ModernUO.Serialization;
|
|
using Server.Items;
|
|
|
|
namespace Server.Mobiles;
|
|
|
|
[SerializationGenerator(0)]
|
|
public abstract partial class BaseGuard : Mobile
|
|
{
|
|
public static bool GuardsInstantKill { get; private set; }
|
|
public static void Configure()
|
|
{
|
|
GuardsInstantKill = ServerConfiguration.GetSetting("guards.instantKill", true);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Spawn(Mobile caller, Mobile target, int amount = 1, bool onlyAdditional = false) =>
|
|
Spawn(caller.Region, target, amount, onlyAdditional);
|
|
|
|
public static void Spawn(Region region, Mobile target, int amount = 1, bool onlyAdditional = false)
|
|
{
|
|
if (target?.Deleted != false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var g in target.GetMobilesInRange<BaseGuard>(15))
|
|
{
|
|
if (g.Focus == null) // idling
|
|
{
|
|
g.Focus = target;
|
|
|
|
--amount;
|
|
}
|
|
else if (g.Focus == target && !onlyAdditional)
|
|
{
|
|
--amount;
|
|
}
|
|
}
|
|
|
|
while (amount-- > 0)
|
|
{
|
|
region.MakeGuard(target);
|
|
}
|
|
}
|
|
|
|
public static void TeleportTo(Mobile source, Point3D to)
|
|
{
|
|
Effects.SendLocationParticles(
|
|
EffectItem.Create(source.Location, source.Map, EffectItem.DefaultDuration),
|
|
0x3728,
|
|
10,
|
|
10,
|
|
2023
|
|
);
|
|
|
|
source.Location = to;
|
|
|
|
Effects.SendLocationParticles(
|
|
EffectItem.Create(to, source.Map, EffectItem.DefaultDuration),
|
|
0x3728,
|
|
10,
|
|
10,
|
|
5023
|
|
);
|
|
|
|
source.PlaySound(0x1FE);
|
|
}
|
|
|
|
|
|
private GuardIdleTimer _idleTimer;
|
|
private GuardAttackTimer _attackTimer;
|
|
|
|
public BaseGuard(Mobile target)
|
|
{
|
|
Title = "the guard";
|
|
|
|
if (target != null)
|
|
{
|
|
Location = target.Location;
|
|
Map = target.Map;
|
|
|
|
Effects.SendLocationParticles(
|
|
EffectItem.Create(Location, Map, EffectItem.DefaultDuration),
|
|
0x3728,
|
|
10,
|
|
10,
|
|
5023
|
|
);
|
|
|
|
Focus = target;
|
|
}
|
|
}
|
|
|
|
protected GuardAttackTimer AttackTimer
|
|
{
|
|
get => _attackTimer;
|
|
set
|
|
{
|
|
_attackTimer?.Stop();
|
|
_attackTimer = value;
|
|
_attackTimer?.Start();
|
|
}
|
|
}
|
|
|
|
protected GuardIdleTimer IdleTimer
|
|
{
|
|
get => _idleTimer;
|
|
set
|
|
{
|
|
_idleTimer?.Stop();
|
|
_idleTimer = value;
|
|
_idleTimer?.Start();
|
|
}
|
|
}
|
|
|
|
public abstract Mobile Focus { get; set; }
|
|
|
|
public override void OnAfterDelete()
|
|
{
|
|
AttackTimer = null;
|
|
IdleTimer = null;
|
|
base.OnAfterDelete();
|
|
}
|
|
|
|
public override bool OnBeforeDeath()
|
|
{
|
|
Effects.SendLocationParticles(
|
|
EffectItem.Create(Location, Map, EffectItem.DefaultDuration),
|
|
0x3728,
|
|
10,
|
|
10,
|
|
2023
|
|
);
|
|
|
|
PlaySound(0x1FE);
|
|
Delete();
|
|
return false;
|
|
}
|
|
|
|
public abstract void NonLethalAttack(Mobile target);
|
|
|
|
[AfterDeserialization]
|
|
private void AfterDeserialization()
|
|
{
|
|
if (Focus != null)
|
|
{
|
|
AttackTimer = new GuardAttackTimer(this);
|
|
}
|
|
else
|
|
{
|
|
IdleTimer = new GuardIdleTimer(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class GuardAvengeTimer : Timer
|
|
{
|
|
private readonly Mobile _focus;
|
|
|
|
public GuardAvengeTimer(Mobile focus) : base(TimeSpan.FromSeconds(2.5), TimeSpan.FromSeconds(1.0), 3) =>
|
|
_focus = focus;
|
|
|
|
protected override void OnTick() => BaseGuard.Spawn(_focus, _focus, 1, true);
|
|
}
|
|
|
|
public class GuardIdleTimer : Timer
|
|
{
|
|
private readonly BaseGuard _owner;
|
|
private int m_Stage;
|
|
|
|
public GuardIdleTimer(BaseGuard owner) : base(TimeSpan.FromSeconds(2.0), TimeSpan.FromSeconds(2.5)) =>
|
|
_owner = owner;
|
|
|
|
protected override void OnTick()
|
|
{
|
|
if (_owner.Deleted)
|
|
{
|
|
Stop();
|
|
return;
|
|
}
|
|
|
|
if (m_Stage++ % 4 == 0 || !_owner.Move(_owner.Direction))
|
|
{
|
|
_owner.Direction = (Direction)Utility.Random(8);
|
|
}
|
|
|
|
if (m_Stage > 16)
|
|
{
|
|
Effects.SendLocationParticles(
|
|
EffectItem.Create(_owner.Location, _owner.Map, EffectItem.DefaultDuration),
|
|
0x3728,
|
|
10,
|
|
10,
|
|
2023
|
|
);
|
|
_owner.PlaySound(0x1FE);
|
|
|
|
if (_owner.Spawner == null)
|
|
{
|
|
_owner.Delete();
|
|
}
|
|
else
|
|
{
|
|
BaseGuard.TeleportTo(_owner, _owner.Spawner.Location);
|
|
}
|
|
|
|
Stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
public class GuardAttackTimer : Timer
|
|
{
|
|
private readonly BaseGuard _owner;
|
|
|
|
public GuardAttackTimer(BaseGuard owner) : base(TimeSpan.FromSeconds(0.25), TimeSpan.FromSeconds(0.1)) =>
|
|
_owner = owner;
|
|
|
|
public void DoOnTick()
|
|
{
|
|
OnTick();
|
|
}
|
|
|
|
protected override void OnTick()
|
|
{
|
|
if (_owner.Deleted)
|
|
{
|
|
Stop();
|
|
return;
|
|
}
|
|
|
|
_owner.Criminal = false;
|
|
_owner.Kills = 0;
|
|
_owner.Stam = _owner.StamMax;
|
|
|
|
var target = _owner.Focus;
|
|
|
|
if (target != null && (target.Deleted || !target.Alive || !_owner.CanBeHarmful(target)))
|
|
{
|
|
_owner.Focus = null;
|
|
Stop();
|
|
return;
|
|
}
|
|
|
|
if (target != null && _owner.Combatant != target)
|
|
{
|
|
_owner.Combatant = target;
|
|
}
|
|
|
|
if (target == null)
|
|
{
|
|
Stop();
|
|
}
|
|
else if (BaseGuard.GuardsInstantKill)
|
|
{
|
|
BaseGuard.TeleportTo(_owner, target.Location);
|
|
target.BoltEffect(0);
|
|
|
|
if (target is BaseCreature creature)
|
|
{
|
|
creature.NoKillAwards = true;
|
|
}
|
|
|
|
target.Damage(target.HitsMax, _owner);
|
|
target.Kill(); // just in case, maybe Damage is overridden on some shard
|
|
|
|
if (target.Corpse != null && !target.Player)
|
|
{
|
|
target.Corpse.Delete();
|
|
}
|
|
|
|
_owner.Focus = null;
|
|
Stop();
|
|
}
|
|
else
|
|
{
|
|
_owner.NonLethalAttack(target);
|
|
}
|
|
}
|
|
}
|