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.
This commit is contained in:
parent
bde072f81c
commit
0b34cc4417
16 changed files with 502 additions and 196 deletions
|
|
@ -57,8 +57,8 @@ public interface ISpawner : IEntity
|
|||
{
|
||||
Guid Guid { get; }
|
||||
bool UnlinkOnTaming { get; }
|
||||
Point3D HomeLocation { get; }
|
||||
int HomeRange { get; }
|
||||
int WalkingRange { get; }
|
||||
Rectangle3D SpawnBounds { get; }
|
||||
Region Region { get; }
|
||||
bool ReturnOnDeactivate { get; }
|
||||
bool Running { get; }
|
||||
|
|
@ -66,6 +66,11 @@ public interface ISpawner : IEntity
|
|||
void Remove(ISpawnable spawn);
|
||||
Point3D GetSpawnPosition(ISpawnable spawned, Map map);
|
||||
void Respawn();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given location is within the spawn bounds.
|
||||
/// </summary>
|
||||
bool IsInSpawnBounds(IPoint3D location);
|
||||
}
|
||||
|
||||
public interface ISpawnable : IEntity
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Server.Gumps;
|
||||
using Server.Mobiles;
|
||||
using Server.Network;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Server.Factions;
|
||||
using Server.Gumps;
|
||||
using Server.Mobiles;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
using System.Collections.Generic;
|
||||
using Server.Items;
|
||||
|
||||
namespace Server.Engines.Spawners;
|
||||
|
||||
public abstract partial class BaseSpawner
|
||||
{
|
||||
// Static dictionary to track pending HomeRange migrations from v10
|
||||
// Keyed by spawner instance, stores the HomeRange value to convert to SpawnBounds
|
||||
private static readonly Dictionary<BaseSpawner, int> _pendingHomeRangeMigrations = new();
|
||||
|
||||
private void MigrateFrom(V10Content content)
|
||||
{
|
||||
_guid = content.Guid;
|
||||
_returnOnDeactivate = content.ReturnOnDeactivate;
|
||||
_entries = content.Entries;
|
||||
_walkingRange = content.WalkingRange;
|
||||
_wayPoint = content.WayPoint;
|
||||
_group = content.Group;
|
||||
_minDelay = content.MinDelay;
|
||||
_maxDelay = content.MaxDelay;
|
||||
_team = content.Team;
|
||||
_end = content.End;
|
||||
|
||||
// Defer SpawnBounds calculation to AfterDeserialization when Location is available
|
||||
if (content.HomeRange > 0)
|
||||
{
|
||||
_pendingHomeRangeMigrations[this] = content.HomeRange;
|
||||
}
|
||||
|
||||
_spawnLocationIsHome = false;
|
||||
}
|
||||
|
||||
private void Deserialize(IGenericReader reader, int version)
|
||||
{
|
||||
_guid = reader.ReadGuid();
|
||||
_returnOnDeactivate = reader.ReadBool();
|
||||
|
||||
var count = reader.ReadInt();
|
||||
_entries = new List<SpawnerEntry>(count);
|
||||
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var entry = new SpawnerEntry(this);
|
||||
entry.Deserialize(reader);
|
||||
_entries.Add(entry);
|
||||
}
|
||||
|
||||
_walkingRange = reader.ReadInt();
|
||||
_wayPoint = reader.ReadEntity<WayPoint>();
|
||||
_group = reader.ReadBool();
|
||||
_minDelay = reader.ReadTimeSpan();
|
||||
_maxDelay = reader.ReadTimeSpan();
|
||||
_count = reader.ReadInt();
|
||||
_team = reader.ReadInt();
|
||||
|
||||
// Store homeRange for migration in AfterDeserialization when Location is available
|
||||
var homeRange = reader.ReadInt();
|
||||
if (homeRange > 0)
|
||||
{
|
||||
_pendingHomeRangeMigrations[this] = homeRange;
|
||||
}
|
||||
|
||||
_running = reader.ReadBool();
|
||||
_end = _running ? reader.ReadDeltaTime() : Core.Now;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ using static Server.Attributes;
|
|||
|
||||
namespace Server.Engines.Spawners;
|
||||
|
||||
[SerializationGenerator(10, false)]
|
||||
[SerializationGenerator(11, false)]
|
||||
public abstract partial class BaseSpawner : Item, ISpawner
|
||||
{
|
||||
[SerializedIgnoreDupe]
|
||||
|
|
@ -28,9 +28,6 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
[SerializableField(2, setter: "private")]
|
||||
private List<SpawnerEntry> _entries;
|
||||
|
||||
[InvalidateProperties]
|
||||
[SerializableField(3)]
|
||||
[SerializedCommandProperty(AccessLevel.Developer)]
|
||||
private int _walkingRange = -1;
|
||||
|
||||
[SerializableField(4)]
|
||||
|
|
@ -60,15 +57,103 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
[InvalidateProperties]
|
||||
[SerializableField(10)]
|
||||
[SerializedCommandProperty(AccessLevel.Developer)]
|
||||
private int _homeRange;
|
||||
private Rectangle3D _spawnBounds;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the home location of the spawn is the location where it spawned
|
||||
/// If false, the home location of the spawn is the location of the spawner
|
||||
/// </summary>
|
||||
[InvalidateProperties]
|
||||
[SerializableField(12)]
|
||||
[SerializedCommandProperty(AccessLevel.Developer)]
|
||||
private bool _spawnLocationIsHome;
|
||||
|
||||
[SerializableField(13)]
|
||||
[SerializedCommandProperty(AccessLevel.Developer)]
|
||||
private DateTime _end;
|
||||
|
||||
private InternalTimer _timer;
|
||||
|
||||
public BaseSpawner() : this(1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10), 0, 4)
|
||||
/// <summary>
|
||||
/// Gets the distance from the spawner to the nearest edge of the spawn bounds.
|
||||
/// Setting this creates a square spawn area centered on the spawner location.
|
||||
/// </summary>
|
||||
[CommandProperty(AccessLevel.Developer)]
|
||||
public virtual int HomeRange
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_spawnBounds == default)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Distance from spawner location to nearest edge
|
||||
var distToMinX = Math.Abs(Location.X - _spawnBounds.Start.X);
|
||||
var distToMaxX = Math.Abs(_spawnBounds.End.X - Location.X);
|
||||
var distToMinY = Math.Abs(Location.Y - _spawnBounds.Start.Y);
|
||||
var distToMaxY = Math.Abs(_spawnBounds.End.Y - Location.Y);
|
||||
|
||||
// Return smallest distance to any edge
|
||||
return Math.Min(Math.Min(distToMinX, distToMaxX), Math.Min(distToMinY, distToMaxY));
|
||||
}
|
||||
set
|
||||
{
|
||||
// Create square bounds centered on spawner with full Z range
|
||||
_spawnBounds = new Rectangle3D(
|
||||
Location.X - value,
|
||||
Location.Y - value,
|
||||
sbyte.MinValue,
|
||||
value * 2 + 1,
|
||||
value * 2 + 1,
|
||||
256 // Full Z range: -128 to 127
|
||||
);
|
||||
this.MarkDirty();
|
||||
InvalidateProperties();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if SpawnBounds represents a HomeRange-style square centered on the spawner.
|
||||
/// </summary>
|
||||
public bool IsHomeRangeStyle
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_spawnBounds == default)
|
||||
{
|
||||
return true; // No bounds = default HomeRange behavior
|
||||
}
|
||||
|
||||
// Must be square
|
||||
if (_spawnBounds.Width != _spawnBounds.Height)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spawner must be at center
|
||||
var centerX = _spawnBounds.Start.X + _spawnBounds.Width / 2;
|
||||
var centerY = _spawnBounds.Start.Y + _spawnBounds.Height / 2;
|
||||
|
||||
return centerX == Location.X && centerY == Location.Y;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given location is within the spawn bounds.
|
||||
/// Virtual to allow RegionSpawner to override with region-based logic.
|
||||
/// </summary>
|
||||
public virtual bool IsInSpawnBounds(IPoint3D location)
|
||||
{
|
||||
if (_spawnBounds == default)
|
||||
{
|
||||
return true; // No bounds = always in bounds
|
||||
}
|
||||
|
||||
return _spawnBounds.Contains(location);
|
||||
}
|
||||
|
||||
public BaseSpawner() : this(1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10))
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -76,34 +161,22 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
1,
|
||||
TimeSpan.FromMinutes(5),
|
||||
TimeSpan.FromMinutes(10),
|
||||
0,
|
||||
4,
|
||||
spawnedName
|
||||
spawnedNames: spawnedName
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public BaseSpawner(
|
||||
int amount, int minDelay, int maxDelay, int team, int homeRange,
|
||||
params ReadOnlySpan<string> spawnedNames
|
||||
) : this(
|
||||
amount,
|
||||
TimeSpan.FromMinutes(minDelay),
|
||||
TimeSpan.FromMinutes(maxDelay),
|
||||
team,
|
||||
homeRange,
|
||||
spawnedNames
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public BaseSpawner(
|
||||
int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange,
|
||||
int amount,
|
||||
TimeSpan minDelay,
|
||||
TimeSpan maxDelay,
|
||||
int team = 0,
|
||||
Rectangle3D spawnBounds = default,
|
||||
params ReadOnlySpan<string> spawnedNames
|
||||
) : base(0x1f13)
|
||||
{
|
||||
_guid = Guid.NewGuid();
|
||||
InitSpawn(amount, minDelay, maxDelay, team, homeRange);
|
||||
InitSpawn(amount, minDelay, maxDelay, team, spawnBounds);
|
||||
for (var i = 0; i < spawnedNames.Length; i++)
|
||||
{
|
||||
AddEntry(spawnedNames[i], 100, amount, false);
|
||||
|
|
@ -130,7 +203,28 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
json.GetProperty("walkingRange", options, out int walkingRange);
|
||||
_walkingRange = walkingRange;
|
||||
|
||||
InitSpawn(amount, minDelay, maxDelay, team, homeRange);
|
||||
// Try new format first
|
||||
if (json.GetProperty("spawnBounds", options, out Rectangle3D spawnBounds))
|
||||
{
|
||||
_spawnBounds = spawnBounds;
|
||||
}
|
||||
// Fall back to homeRange with location for oldest format
|
||||
else if (homeRange > 0 && json.GetProperty("location", options, out Point3D location))
|
||||
{
|
||||
_spawnBounds = new Rectangle3D(
|
||||
location.X - homeRange,
|
||||
location.Y - homeRange,
|
||||
sbyte.MinValue,
|
||||
homeRange * 2 + 1,
|
||||
homeRange * 2 + 1,
|
||||
256
|
||||
);
|
||||
}
|
||||
|
||||
json.GetProperty("spawnLocationIsHome", options, out bool spawnLocationIsHome);
|
||||
_spawnLocationIsHome = spawnLocationIsHome;
|
||||
|
||||
InitSpawn(amount, minDelay, maxDelay, team, _spawnBounds);
|
||||
|
||||
json.GetProperty("entries", options, out List<SpawnerEntry> entries);
|
||||
|
||||
|
|
@ -147,6 +241,18 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
[IgnoreDupe]
|
||||
public Dictionary<ISpawnable, SpawnerEntry> Spawned { get; private set; }
|
||||
|
||||
[CommandProperty(AccessLevel.Developer)]
|
||||
[SerializableProperty(3, nameof(_walkingRange))]
|
||||
public int WalkingRange
|
||||
{
|
||||
get => _walkingRange > 0 ? _walkingRange : HomeRange;
|
||||
set
|
||||
{
|
||||
_walkingRange = value;
|
||||
InvalidateProperties();
|
||||
}
|
||||
}
|
||||
|
||||
[SerializableProperty(8)]
|
||||
[CommandProperty(AccessLevel.Developer)]
|
||||
public int Count
|
||||
|
|
@ -208,12 +314,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
public virtual Point3D HomeLocation => Location;
|
||||
public bool UnlinkOnTaming => true;
|
||||
|
||||
Region ISpawner.Region => Region.Find(Location, Map);
|
||||
|
||||
public void UpdateEntries(List<SpawnerEntry> entries)
|
||||
{
|
||||
Entries = entries;
|
||||
}
|
||||
public abstract Region Region { get; }
|
||||
|
||||
public void Remove(ISpawnable spawn)
|
||||
{
|
||||
|
|
@ -260,9 +361,10 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
json.SetProperty("minDelay", options, MinDelay);
|
||||
json.SetProperty("maxDelay", options, MaxDelay);
|
||||
json.SetProperty("team", options, Team);
|
||||
json.SetProperty("homeRange", options, HomeRange);
|
||||
json.SetProperty("walkingRange", options, WalkingRange);
|
||||
json.SetProperty("entries", options, Entries);
|
||||
json.SetProperty("spawnBounds", options, SpawnBounds);
|
||||
json.SetProperty("spawnLocationIsHome", options, SpawnLocationIsHome);
|
||||
}
|
||||
|
||||
public abstract Point3D GetSpawnPosition(ISpawnable spawned, Map map);
|
||||
|
|
@ -290,6 +392,45 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
}
|
||||
}
|
||||
|
||||
public override void OnLocationChange(Point3D oldLocation)
|
||||
{
|
||||
base.OnLocationChange(oldLocation);
|
||||
|
||||
// Only shift bounds if they represent a HomeRange-style square
|
||||
// (spawner was centered and bounds are square)
|
||||
if (_spawnBounds == default)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isSquare = _spawnBounds.Width == _spawnBounds.Height;
|
||||
if (!isSquare)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if spawner was at center of bounds
|
||||
var centerX = _spawnBounds.Start.X + _spawnBounds.Width / 2;
|
||||
var centerY = _spawnBounds.Start.Y + _spawnBounds.Height / 2;
|
||||
if (centerX != oldLocation.X || centerY != oldLocation.Y)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Shift bounds by the location delta
|
||||
var deltaX = Location.X - oldLocation.X;
|
||||
var deltaY = Location.Y - oldLocation.Y;
|
||||
|
||||
_spawnBounds = new Rectangle3D(
|
||||
_spawnBounds.Start.X + deltaX,
|
||||
_spawnBounds.Start.Y + deltaY,
|
||||
_spawnBounds.Start.Z,
|
||||
_spawnBounds.Width,
|
||||
_spawnBounds.Height,
|
||||
_spawnBounds.Depth
|
||||
);
|
||||
}
|
||||
|
||||
public SpawnerEntry AddEntry(
|
||||
string creaturename,
|
||||
int probability = 100,
|
||||
|
|
@ -309,7 +450,7 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
return entry;
|
||||
}
|
||||
|
||||
public void InitSpawn(int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange)
|
||||
public void InitSpawn(int amount, TimeSpan minDelay, TimeSpan maxDelay, int team = 0, Rectangle3D spawnBounds = default)
|
||||
{
|
||||
Visible = false;
|
||||
Movable = false;
|
||||
|
|
@ -319,7 +460,16 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
_maxDelay = maxDelay;
|
||||
_count = amount;
|
||||
_team = team;
|
||||
_homeRange = homeRange;
|
||||
|
||||
if (spawnBounds != default)
|
||||
{
|
||||
_spawnBounds = spawnBounds;
|
||||
}
|
||||
else
|
||||
{
|
||||
HomeRange = 4;
|
||||
}
|
||||
|
||||
Entries = [];
|
||||
Spawned = new Dictionary<ISpawnable, SpawnerEntry>();
|
||||
|
||||
|
|
@ -348,7 +498,10 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
list.Add(1060742); // active
|
||||
|
||||
list.Add(1060656, _count); // amount to make: ~1_val~
|
||||
list.Add(1061169, _homeRange); // range ~1_val~
|
||||
if (SpawnBounds != default)
|
||||
{
|
||||
list.Add(1061169, SpawnBounds.ToString()); // range ~1_val~
|
||||
}
|
||||
list.Add(1050039, $"{"walking range:"}\t{_walkingRange}"); // ~1_NUMBER~ ~2_ITEMNAME~
|
||||
list.Add(1053099, $"{"group:"}\t{_group}"); // ~1_oretype~: ~2_armortype~
|
||||
list.Add(1060847, $"{"team:"}\t{_team}"); // ~1_val~ ~2_val~
|
||||
|
|
@ -680,16 +833,14 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
Spawned.Add(m, entry);
|
||||
entry.AddToSpawned(m);
|
||||
|
||||
var loc = m is BaseVendor ? Location : GetSpawnPosition(m, map);
|
||||
var spawnLocation = m is BaseVendor ? Location : GetSpawnPosition(m, map);
|
||||
|
||||
m.OnBeforeSpawn(loc, map);
|
||||
m.MoveToWorld(loc, map);
|
||||
m.OnBeforeSpawn(spawnLocation, map);
|
||||
m.MoveToWorld(spawnLocation, map);
|
||||
|
||||
if (m is BaseCreature c)
|
||||
{
|
||||
var walkrange = GetWalkingRange();
|
||||
|
||||
c.RangeHome = walkrange >= 0 ? walkrange : _homeRange;
|
||||
c.RangeHome = WalkingRange;
|
||||
c.CurrentWayPoint = WayPoint;
|
||||
|
||||
if (_team > 0)
|
||||
|
|
@ -697,8 +848,18 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
c.Team = _team;
|
||||
}
|
||||
|
||||
c.Home = Location;
|
||||
c.HomeMap = Map;
|
||||
// If true, the home location of the mob is the location where it spawned
|
||||
// If false, the home location of the mob is the location of the spawner
|
||||
if (_spawnLocationIsHome)
|
||||
{
|
||||
c.Home = spawnLocation;
|
||||
c.HomeMap = map;
|
||||
}
|
||||
else
|
||||
{
|
||||
c.Home = Location;
|
||||
c.HomeMap = Map;
|
||||
}
|
||||
}
|
||||
|
||||
m.Spawner = this;
|
||||
|
|
@ -736,8 +897,6 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
return true;
|
||||
}
|
||||
|
||||
public virtual int GetWalkingRange() => _walkingRange;
|
||||
|
||||
public virtual Map GetSpawnMap() => Map;
|
||||
|
||||
public void DoTimer()
|
||||
|
|
@ -884,37 +1043,22 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
RemoveSpawns();
|
||||
}
|
||||
|
||||
private void Deserialize(IGenericReader reader, int version)
|
||||
{
|
||||
_guid = reader.ReadGuid();
|
||||
_returnOnDeactivate = reader.ReadBool();
|
||||
|
||||
var count = reader.ReadInt();
|
||||
_entries = new List<SpawnerEntry>(count);
|
||||
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var entry = new SpawnerEntry(this);
|
||||
entry.Deserialize(reader);
|
||||
_entries.Add(entry);
|
||||
}
|
||||
|
||||
_walkingRange = reader.ReadInt();
|
||||
_wayPoint = reader.ReadEntity<WayPoint>();
|
||||
_group = reader.ReadBool();
|
||||
_minDelay = reader.ReadTimeSpan();
|
||||
_maxDelay = reader.ReadTimeSpan();
|
||||
_count = reader.ReadInt();
|
||||
_team = reader.ReadInt();
|
||||
_homeRange = reader.ReadInt();
|
||||
_running = reader.ReadBool();
|
||||
|
||||
_end = _running ? reader.ReadDeltaTime() : Core.Now;
|
||||
}
|
||||
|
||||
[AfterDeserialization]
|
||||
private void AfterDeserialization()
|
||||
{
|
||||
// Handle v10 migration - convert HomeRange to SpawnBounds now that Location is available
|
||||
if (_pendingHomeRangeMigrations.Remove(this, out var homeRange))
|
||||
{
|
||||
_spawnBounds = new Rectangle3D(
|
||||
Location.X - homeRange,
|
||||
Location.Y - homeRange,
|
||||
sbyte.MinValue,
|
||||
homeRange * 2 + 1,
|
||||
homeRange * 2 + 1,
|
||||
256
|
||||
);
|
||||
}
|
||||
|
||||
Spawned = new Dictionary<ISpawnable, SpawnerEntry>();
|
||||
|
||||
foreach (var entry in Entries)
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ public static class ImportSpawnersCommand
|
|||
Point3D location = Point3D.Parse(Utility.GetText(node["location"], "Error"));
|
||||
Map map = Map.Parse(Utility.GetText(node["map"], "Error"));
|
||||
|
||||
Spawner spawner = new Spawner(count, minDelay, maxDelay, team, homeRange, creatureNames.ToArray());
|
||||
Spawner spawner = new Spawner(count, minDelay, maxDelay, team, spawnedNames: creatureNames.ToArray());
|
||||
if (walkingRange >= 0)
|
||||
{
|
||||
spawner.WalkingRange = walkingRange;
|
||||
|
|
@ -166,6 +166,7 @@ public static class ImportSpawnersCommand
|
|||
|
||||
spawner.Name = name;
|
||||
spawner.MoveToWorld(location, map);
|
||||
spawner.HomeRange = homeRange;
|
||||
if (spawner.Map == Map.Internal)
|
||||
{
|
||||
spawner.Delete();
|
||||
|
|
@ -347,7 +348,7 @@ public static class ImportSpawnersCommand
|
|||
var maxDelay = GetTimeSpan(maxTimeOverride, parts[12]);
|
||||
var homeRange = int.Parse(parts[14]);
|
||||
|
||||
var spawner = new Spawner(totalCount, minDelay, maxDelay, 0, homeRange)
|
||||
var spawner = new Spawner(totalCount, minDelay, maxDelay)
|
||||
{
|
||||
WalkingRange = int.Parse(parts[13]),
|
||||
Name = $"Spawner ({spawnerIdOverride ?? parts[15]})"
|
||||
|
|
@ -362,6 +363,7 @@ public static class ImportSpawnersCommand
|
|||
new Point3D(int.Parse(parts[7]), int.Parse(parts[8]), int.Parse(parts[9])),
|
||||
map
|
||||
);
|
||||
spawner.HomeRange = homeRange;
|
||||
|
||||
spawner.Respawn();
|
||||
allSpawners.Add(spawner.Guid, spawner);
|
||||
|
|
|
|||
|
|
@ -46,36 +46,18 @@ public partial class ProximitySpawner : Spawner
|
|||
{
|
||||
}
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
public ProximitySpawner(int amount, int minDelay, int maxDelay, int team, int homeRange, string spawnName)
|
||||
: base(amount, minDelay, maxDelay, team, homeRange, spawnName)
|
||||
{
|
||||
}
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
public ProximitySpawner(
|
||||
int amount, int minDelay, int maxDelay, int team, int homeRange, int triggerRange,
|
||||
string spawnMessage, bool instantFlag, string spawnName
|
||||
) : base(amount, minDelay, maxDelay, team, homeRange, spawnName)
|
||||
{
|
||||
TriggerRange = triggerRange;
|
||||
SpawnMessage = TextDefinition.Parse(spawnMessage);
|
||||
InstantFlag = instantFlag;
|
||||
}
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
public ProximitySpawner(
|
||||
int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange,
|
||||
int amount,
|
||||
TimeSpan minDelay,
|
||||
TimeSpan maxDelay,
|
||||
int team = 0,
|
||||
Rectangle3D spawnBounds = default,
|
||||
int triggerRange = 0,
|
||||
TextDefinition spawnMessage = null,
|
||||
bool instantFlag = false,
|
||||
params ReadOnlySpan<string> spawnedNames
|
||||
) : base(amount, minDelay, maxDelay, team, homeRange, spawnedNames)
|
||||
{
|
||||
}
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
public ProximitySpawner(
|
||||
int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange, int triggerRange,
|
||||
TextDefinition spawnMessage, bool instantFlag, params ReadOnlySpan<string> spawnedNames
|
||||
) : base(amount, minDelay, maxDelay, team, homeRange, spawnedNames)
|
||||
) : base(amount, minDelay, maxDelay, team, spawnBounds, spawnedNames)
|
||||
{
|
||||
TriggerRange = triggerRange;
|
||||
SpawnMessage = spawnMessage;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ public partial class RegionSpawner : Spawner
|
|||
|
||||
private BaseRegion _spawnRegion;
|
||||
|
||||
public override Region Region => _spawnRegion;
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
public RegionSpawner()
|
||||
{
|
||||
|
|
@ -41,24 +43,12 @@ public partial class RegionSpawner : Spawner
|
|||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
public RegionSpawner(
|
||||
int amount, int minDelay, int maxDelay, int team, int homeRange,
|
||||
int amount,
|
||||
TimeSpan minDelay,
|
||||
TimeSpan maxDelay,
|
||||
int team = 0,
|
||||
params ReadOnlySpan<string> spawnedNames
|
||||
) : this(
|
||||
amount,
|
||||
TimeSpan.FromMinutes(minDelay),
|
||||
TimeSpan.FromMinutes(maxDelay),
|
||||
team,
|
||||
homeRange,
|
||||
spawnedNames
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
public RegionSpawner(
|
||||
int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange,
|
||||
params ReadOnlySpan<string> spawnedNames
|
||||
) : base(amount, minDelay, maxDelay, team, homeRange, spawnedNames)
|
||||
) : base(amount, minDelay, maxDelay, team, spawnedNames: spawnedNames)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +165,7 @@ public partial class RegionSpawner : Spawner
|
|||
}
|
||||
}
|
||||
|
||||
return HomeLocation;
|
||||
return Location;
|
||||
}
|
||||
|
||||
[AfterDeserialization(false)]
|
||||
|
|
|
|||
|
|
@ -20,24 +20,13 @@ public partial class Spawner : BaseSpawner
|
|||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
public Spawner(
|
||||
int amount, int minDelay, int maxDelay, int team, int homeRange,
|
||||
int amount,
|
||||
TimeSpan minDelay,
|
||||
TimeSpan maxDelay,
|
||||
int team = 0,
|
||||
Rectangle3D spawnBounds = default,
|
||||
params ReadOnlySpan<string> spawnedNames
|
||||
) : this(
|
||||
amount,
|
||||
TimeSpan.FromMinutes(minDelay),
|
||||
TimeSpan.FromMinutes(maxDelay),
|
||||
team,
|
||||
homeRange,
|
||||
spawnedNames
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
public Spawner(
|
||||
int amount, TimeSpan minDelay, TimeSpan maxDelay, int team, int homeRange,
|
||||
params ReadOnlySpan<string> spawnedNames
|
||||
) : base(amount, minDelay, maxDelay, team, homeRange, spawnedNames)
|
||||
) : base(amount, minDelay, maxDelay, team, spawnBounds, spawnedNames)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +74,8 @@ public partial class Spawner : BaseSpawner
|
|||
}
|
||||
*/
|
||||
|
||||
public override Region Region => Region.Find(Location, Map);
|
||||
|
||||
public override Point3D GetSpawnPosition(ISpawnable spawned, Map map)
|
||||
{
|
||||
if (map == null || map == Map.Internal)
|
||||
|
|
@ -105,12 +96,29 @@ public partial class Spawner : BaseSpawner
|
|||
waterOnlyMob = false;
|
||||
}
|
||||
|
||||
var bounds = SpawnBounds;
|
||||
var hasBounds = bounds != default;
|
||||
|
||||
// Try 10 times to find a valid location.
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var x = Location.X + (Utility.Random(HomeRange * 2 + 1) - HomeRange);
|
||||
var y = Location.Y + (Utility.Random(HomeRange * 2 + 1) - HomeRange);
|
||||
int x, y;
|
||||
|
||||
if (hasBounds)
|
||||
{
|
||||
// Use SpawnBounds for X/Y selection
|
||||
x = Utility.RandomMinMax(bounds.Start.X, bounds.End.X - 1);
|
||||
y = Utility.RandomMinMax(bounds.Start.Y, bounds.End.Y - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No bounds set - spawn at spawner location
|
||||
x = Location.X;
|
||||
y = Location.Y;
|
||||
}
|
||||
|
||||
// Note: Z-level logic uses spawner Z and map average Z.
|
||||
// Multi-story building support (using bounds Z range) is planned for a future PR.
|
||||
var mapZ = map.GetAverageZ(x, y);
|
||||
|
||||
if (waterMob)
|
||||
|
|
@ -140,6 +148,6 @@ public partial class Spawner : BaseSpawner
|
|||
}
|
||||
}
|
||||
|
||||
return HomeLocation;
|
||||
return Location;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,15 +275,23 @@ public class SpawnerControllerGump : GumpGrid
|
|||
//entry
|
||||
var entry = i * EntryCount;
|
||||
|
||||
AddImageTiled(item.Cols[2].X - 2, list.Items[i].Y + 18, item.Cols[2].Width, 1, 9357);
|
||||
AddTextEntry(item.Cols[2].X, list.Items[i].Y + 2, list.Header.Cols[2].Width, 80, (int)GridHues.White, entry, spawner.Name, 20);
|
||||
AddImageTiled(item.Cols[2].X - 2, list.Items[i].Y + 18, item.Cols[2].Width - 8, 1, 9357);
|
||||
AddTextEntry(item.Cols[2].X, list.Items[i].Y + 2, list.Header.Cols[2].Width - 8, 80, (int)GridHues.White, entry, spawner.Name, 20);
|
||||
AddButton(item.Cols[2].X, list.Items[i].Y + vCenter + 9, 5837, 5838, GetButtonID(6, item.Index));
|
||||
|
||||
AddImageTiled(item.Cols[6].X + 8, list.Items[i].Y + 29, item.Cols[6].Width / 2, 1, 9357);
|
||||
AddTextEntry(item.Cols[6].X + 12, list.Items[i].Y + 13, list.Header.Cols[6].Width, 80, (int)GridHues.White, entry + 1, spawner.WalkingRange.ToString(), 2);
|
||||
|
||||
AddImageTiled(item.Cols[7].X + 8, list.Items[i].Y + 29, item.Cols[7].Width / 2, 1, 9357);
|
||||
AddTextEntry(item.Cols[7].X + 12, list.Items[i].Y + 13, list.Header.Cols[7].Width, 80, (int)GridHues.White, entry + 2, spawner.HomeRange.ToString(), 2);
|
||||
// Show HomeRange if bounds are HomeRange-style, otherwise show "Custom"
|
||||
if (spawner.IsHomeRangeStyle)
|
||||
{
|
||||
AddImageTiled(item.Cols[7].X + 8, list.Items[i].Y + 29, item.Cols[7].Width / 2, 1, 9357);
|
||||
AddTextEntry(item.Cols[7].X + 12, list.Items[i].Y + 13, list.Header.Cols[7].Width, 80, (int)GridHues.White, entry + 2, spawner.HomeRange.ToString(), 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddLabelHtml(item.Cols[7].X - 6, list.Items[i].Y + 13, list.Header.Cols[7].Width, 30, "Custom", GridColors.Yellow);
|
||||
}
|
||||
|
||||
AddImageTiled(item.Cols[8].X + 6, list.Items[i].Y + 29, item.Cols[8].Width - 20, 1, 9357);
|
||||
AddTextEntry(item.Cols[8].X + 8, list.Items[i].Y + 13, list.Header.Cols[8].Width, 80, (int)GridHues.White, entry + 3, spawner.MinDelay.ToString(), 8);
|
||||
|
|
@ -362,7 +370,8 @@ public class SpawnerControllerGump : GumpGrid
|
|||
target.MinDelay = spawner.MinDelay;
|
||||
target.MaxDelay = spawner.MaxDelay;
|
||||
target.WalkingRange = spawner.WalkingRange;
|
||||
target.HomeRange = spawner.HomeRange;
|
||||
target.SpawnBounds = spawner.SpawnBounds;
|
||||
target.SpawnLocationIsHome = spawner.SpawnLocationIsHome;
|
||||
target.Group = spawner.Group;
|
||||
target.Count = spawner.Count;
|
||||
}
|
||||
|
|
@ -510,9 +519,11 @@ public class SpawnerControllerGump : GumpGrid
|
|||
spawner.WalkingRange = walkRange;
|
||||
}
|
||||
|
||||
if (int.TryParse(info.GetTextEntry(indexEntry + 2), out var homeHange))
|
||||
// Only update HomeRange if text entry exists (HomeRange-style bounds)
|
||||
// Custom bounds show a label instead, so this will be null/empty and skip
|
||||
if (int.TryParse(info.GetTextEntry(indexEntry + 2), out var homeRange))
|
||||
{
|
||||
spawner.HomeRange = homeHange;
|
||||
spawner.HomeRange = homeRange;
|
||||
}
|
||||
|
||||
if (TimeSpan.TryParse(info.GetTextEntry(indexEntry + 3), out var minDelay))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using Server.Collections;
|
||||
using Server.Gumps;
|
||||
using Server.Network;
|
||||
|
|
@ -157,18 +158,16 @@ public class SpawnerGump : Gump
|
|||
}
|
||||
|
||||
var totalSpawned = 0;
|
||||
var totalSpawns = 0;
|
||||
var totalWeight = 0;
|
||||
|
||||
foreach (SpawnerEntry spawnerEntry in _spawner.Entries)
|
||||
{
|
||||
totalSpawns += spawnerEntry.SpawnedMaxCount;
|
||||
totalSpawned += spawner.CountSpawns(spawnerEntry);
|
||||
totalWeight += spawnerEntry.SpawnedProbability;
|
||||
}
|
||||
|
||||
AddHtml(270, 308 + offset, 35, 20, Html.Center($"{totalSpawns}", 0xF4F4F4));
|
||||
AddHtml(308, 308 + offset, 35, 20, Html.Center($"{totalWeight}",0xF4F4F4));
|
||||
AddHtml(232, 308 + offset, 35, 20, Html.Center($"{totalSpawned}", 0xF4F4F4));
|
||||
AddHtml(270, 308 + offset, 35, 20, Html.Center($"{totalWeight}", 0xF4F4F4));
|
||||
|
||||
AddHtml(5, 1, 161, 20, $"<BASEFONT COLOR=#FFEA00>{spawner.Name}</BASEFONT><BASEFONT COLOR={GetCountColor(totalSpawned, spawner.Count)}> ({totalSpawned}/{spawner.Count})</BASEFONT>");
|
||||
|
||||
|
|
@ -235,18 +234,18 @@ public class SpawnerGump : Gump
|
|||
var index = i * 5;
|
||||
var entryindex = _page * 13 + i;
|
||||
|
||||
var cte = info.GetTextEntry(index);
|
||||
var mte = info.GetTextEntry(index + 1);
|
||||
var poste = info.GetTextEntry(index + 2);
|
||||
var parmte = info.GetTextEntry(index + 3);
|
||||
var propte = info.GetTextEntry(index + 4);
|
||||
var typeEntry = info.GetTextEntry(index);
|
||||
var maxSpawns = info.GetTextEntry(index + 1);
|
||||
var probability = info.GetTextEntry(index + 2);
|
||||
var customParams = info.GetTextEntry(index + 3);
|
||||
var customPrompts = info.GetTextEntry(index + 4);
|
||||
|
||||
if (cte == null)
|
||||
if (typeEntry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var str = cte.Trim().ToLower();
|
||||
var str = typeEntry.Trim().ToLower();
|
||||
|
||||
if (str.Length > 0)
|
||||
{
|
||||
|
|
@ -265,42 +264,32 @@ public class SpawnerGump : Gump
|
|||
entry = spawner.Entries[entryindex];
|
||||
entry.SpawnedName = str;
|
||||
|
||||
if (mte != null)
|
||||
if (maxSpawns != null)
|
||||
{
|
||||
entry.SpawnedMaxCount = Utility.ToInt32(mte.Trim());
|
||||
entry.SpawnedMaxCount = Utility.ToInt32(maxSpawns.AsSpan().Trim());
|
||||
}
|
||||
|
||||
if (poste != null)
|
||||
if (probability != null)
|
||||
{
|
||||
entry.SpawnedProbability = Utility.ToInt32(poste.Trim());
|
||||
entry.SpawnedProbability = Utility.ToInt32(probability.AsSpan().Trim());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var maxcount = 1;
|
||||
var probcount = 100;
|
||||
|
||||
if (mte != null)
|
||||
{
|
||||
maxcount = Utility.ToInt32(mte.Trim());
|
||||
}
|
||||
|
||||
if (poste != null)
|
||||
{
|
||||
probcount = Utility.ToInt32(poste.Trim());
|
||||
}
|
||||
var maxcount = maxSpawns != null ? Utility.ToInt32(maxSpawns.AsSpan().Trim()) : 1;
|
||||
var probcount = probability != null ? Utility.ToInt32(probability.AsSpan().Trim()) : 100;
|
||||
|
||||
entry = spawner.AddEntry(str, probcount, maxcount);
|
||||
}
|
||||
|
||||
if (parmte != null)
|
||||
if (customParams != null)
|
||||
{
|
||||
entry.Parameters = parmte.Trim();
|
||||
entry.Parameters = customParams.Trim();
|
||||
}
|
||||
|
||||
if (propte != null)
|
||||
if (customPrompts != null)
|
||||
{
|
||||
entry.Properties = propte.Trim();
|
||||
entry.Properties = customPrompts.Trim();
|
||||
}
|
||||
}
|
||||
else if (entryindex < ocount && spawner.Entries[entryindex] != null)
|
||||
|
|
|
|||
108
Projects/UOContent/Migrations/Server.Engines.Spawners.BaseSpawner.v11.json
generated
Normal file
108
Projects/UOContent/Migrations/Server.Engines.Spawners.BaseSpawner.v11.json
generated
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
{
|
||||
"version": 11,
|
||||
"type": "Server.Engines.Spawners.BaseSpawner",
|
||||
"properties": [
|
||||
{
|
||||
"name": "Guid",
|
||||
"type": "System.Guid",
|
||||
"rule": "PrimitiveTypeMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "ReturnOnDeactivate",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Entries",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Engines.Spawners.SpawnerEntry\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Server.Engines.Spawners.SpawnerEntry",
|
||||
"RawSerializableMigrationRule",
|
||||
"DeserializationRequiresParent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "WalkingRange",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "WayPoint",
|
||||
"type": "Server.Items.WayPoint",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "Group",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MinDelay",
|
||||
"type": "System.TimeSpan",
|
||||
"rule": "PrimitiveTypeMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "MaxDelay",
|
||||
"type": "System.TimeSpan",
|
||||
"rule": "PrimitiveTypeMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "Count",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Team",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SpawnBounds",
|
||||
"type": "Server.Rectangle3D",
|
||||
"rule": "PrimitiveUOTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Rect3D"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Running",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SpawnLocationIsHome",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "End",
|
||||
"type": "System.DateTime",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -879,15 +879,15 @@ public abstract partial class BaseAI
|
|||
_timer.Stop();
|
||||
}
|
||||
|
||||
if (ShouldReturnToHome(Mobile.Spawner as Spawner))
|
||||
if (ShouldReturnToHome(Mobile.Spawner))
|
||||
{
|
||||
Timer.StartTimer(ReturnToHome);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldReturnToHome(Spawner spawner) =>
|
||||
private bool ShouldReturnToHome(ISpawner spawner) =>
|
||||
spawner?.ReturnOnDeactivate == true && !Mobile.Controlled &&
|
||||
(spawner.HomeLocation == Point3D.Zero || !Mobile.InRange(spawner.HomeLocation, spawner.HomeRange));
|
||||
!spawner.IsInSpawnBounds(Mobile.Location);
|
||||
|
||||
private void ReturnToHome()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
************************************************************************/
|
||||
|
||||
using Server.Engines.Spawners;
|
||||
|
||||
namespace Server.Mobiles;
|
||||
|
||||
public abstract partial class BaseAI
|
||||
|
|
@ -326,7 +328,7 @@ public abstract partial class BaseAI
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool IsInvalidControlTarget(Mobile target) => target?.Deleted != false || target.Map != Mobile.Map
|
||||
private bool IsInvalidControlTarget(Mobile target) => target?.Deleted != false || target.Map != Mobile.Map
|
||||
|| !target.Alive || target.IsDeadBondedPet;
|
||||
|
||||
private void HandleInvalidControlTarget()
|
||||
|
|
@ -344,7 +346,7 @@ public abstract partial class BaseAI
|
|||
private void FindCombatant()
|
||||
{
|
||||
var controlMaster = Mobile.ControlMaster;
|
||||
|
||||
|
||||
foreach (var aggr in Mobile.GetMobilesInRange(Mobile.RangePerception))
|
||||
{
|
||||
if (!Mobile.CanSee(aggr) || aggr.IsDeadBondedPet || !aggr.Alive)
|
||||
|
|
@ -354,7 +356,7 @@ public abstract partial class BaseAI
|
|||
|
||||
bool isAttackingPet = aggr.Combatant == Mobile;
|
||||
bool isAttackingMaster = controlMaster != null && aggr.Combatant == controlMaster;
|
||||
|
||||
|
||||
if (isAttackingPet || isAttackingMaster)
|
||||
{
|
||||
if (Mobile.InLOS(aggr))
|
||||
|
|
@ -377,12 +379,12 @@ public abstract partial class BaseAI
|
|||
for (var i = 0; i < controlMaster.Aggressors.Count; i++)
|
||||
{
|
||||
var aggressor = controlMaster.Aggressors[i].Attacker;
|
||||
|
||||
|
||||
if (aggressor?.Deleted != false || !aggressor.Alive || aggressor.IsDeadBondedPet)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (Mobile.InRange(aggressor, Mobile.RangePerception) && Mobile.CanSee(aggressor) && Mobile.InLOS(aggressor))
|
||||
{
|
||||
Mobile.ControlTarget = aggressor;
|
||||
|
|
@ -404,10 +406,10 @@ public abstract partial class BaseAI
|
|||
|
||||
var spawner = Mobile.Spawner;
|
||||
|
||||
if (spawner != null && spawner.HomeLocation != Point3D.Zero)
|
||||
if (spawner != null)
|
||||
{
|
||||
Mobile.Home = spawner.HomeLocation;
|
||||
Mobile.RangeHome = spawner.HomeRange;
|
||||
Mobile.Home = spawner.GetSpawnPosition(Mobile, spawner.Map);
|
||||
Mobile.RangeHome = spawner.WalkingRange;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3610,7 +3610,7 @@ namespace Server.Mobiles
|
|||
|
||||
if (ReturnsToHome && IsSpawnerBound() && !InRange(Home, RangeHome))
|
||||
{
|
||||
if (Combatant == null && Warmode == false && Utility.RandomDouble() < .10) /* some throttling */
|
||||
if (Combatant == null && !Warmode && Utility.RandomDouble() < .10) /* some throttling */
|
||||
{
|
||||
m_FailedReturnHome = !Move(GetDirectionTo(Home.X, Home.Y)) ? m_FailedReturnHome + 1 : 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ public class GuardIdleTimer : Timer
|
|||
}
|
||||
else
|
||||
{
|
||||
BaseGuard.TeleportTo(_owner, _owner.Spawner.HomeLocation);
|
||||
BaseGuard.TeleportTo(_owner, _owner.Spawner.Location);
|
||||
}
|
||||
|
||||
Stop();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue