ModernUO/Projects/UOContent/Mobiles/Guards/BaseGuard.cs
Quick 0b34cc4417
feat: Changes Spawner HomeRange to SpawnBounds (#2290)
### 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.
2025-12-26 18:07:26 -08:00

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);
}
}
}