### 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.
153 lines
4 KiB
C#
153 lines
4 KiB
C#
using System;
|
|
using System.Text.Json;
|
|
using ModernUO.Serialization;
|
|
using Server.Json;
|
|
|
|
namespace Server.Engines.Spawners;
|
|
|
|
[SerializationGenerator(0)]
|
|
public partial class Spawner : BaseSpawner
|
|
{
|
|
[Constructible(AccessLevel.Developer)]
|
|
public Spawner()
|
|
{
|
|
}
|
|
|
|
[Constructible(AccessLevel.Developer)]
|
|
public Spawner(string spawnedName) : base(spawnedName)
|
|
{
|
|
}
|
|
|
|
[Constructible(AccessLevel.Developer)]
|
|
public Spawner(
|
|
int amount,
|
|
TimeSpan minDelay,
|
|
TimeSpan maxDelay,
|
|
int team = 0,
|
|
Rectangle3D spawnBounds = default,
|
|
params ReadOnlySpan<string> spawnedNames
|
|
) : base(amount, minDelay, maxDelay, team, spawnBounds, spawnedNames)
|
|
{
|
|
}
|
|
|
|
public Spawner(DynamicJson json, JsonSerializerOptions options) : base(json, options)
|
|
{
|
|
}
|
|
|
|
public static bool IsValidWater(Map map, int x, int y, int z)
|
|
{
|
|
if (!Region.Find(new Point3D(x, y, z), map).AllowSpawn() || !map.CanFit(x, y, z, 16, false, true, false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var landTile = map.Tiles.GetLandTile(x, y);
|
|
|
|
if (landTile.Z == z && (TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags & TileFlag.Wet) != 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
foreach (var staticTile in map.Tiles.GetStaticAndMultiTiles(x, y))
|
|
{
|
|
if (staticTile.Z == z && TileData.ItemTable[staticTile.ID & TileData.MaxItemValue].Wet)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
public override bool OnDefragSpawn(ISpawnable spawned, bool remove)
|
|
{
|
|
// To despawn a mob that was lured 4x away from its spawner
|
|
// TODO: Move this to a config
|
|
if (spawned is BaseCreature c && c.Combatant == null && c.GetDistanceToSqrt( Location ) > c.RangeHome * 4)
|
|
{
|
|
c.Delete();
|
|
remove = true;
|
|
}
|
|
|
|
return base.OnDefragSpawn(entry, spawned, remove);
|
|
}
|
|
*/
|
|
|
|
public override Region Region => Region.Find(Location, Map);
|
|
|
|
public override Point3D GetSpawnPosition(ISpawnable spawned, Map map)
|
|
{
|
|
if (map == null || map == Map.Internal)
|
|
{
|
|
return Location;
|
|
}
|
|
|
|
bool waterMob, waterOnlyMob;
|
|
|
|
if (spawned is Mobile mob)
|
|
{
|
|
waterMob = mob.CanSwim;
|
|
waterOnlyMob = mob.CanSwim && mob.CantWalk;
|
|
}
|
|
else
|
|
{
|
|
waterMob = false;
|
|
waterOnlyMob = false;
|
|
}
|
|
|
|
var bounds = SpawnBounds;
|
|
var hasBounds = bounds != default;
|
|
|
|
// Try 10 times to find a valid location.
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
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)
|
|
{
|
|
if (IsValidWater(map, x, y, Z))
|
|
{
|
|
return new Point3D(x, y, Z);
|
|
}
|
|
|
|
if (IsValidWater(map, x, y, mapZ))
|
|
{
|
|
return new Point3D(x, y, mapZ);
|
|
}
|
|
}
|
|
|
|
if (!waterOnlyMob)
|
|
{
|
|
if (map.CanSpawnMobile(x, y, Z))
|
|
{
|
|
return new Point3D(x, y, Z);
|
|
}
|
|
|
|
if (map.CanSpawnMobile(x, y, mapZ))
|
|
{
|
|
return new Point3D(x, y, mapZ);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Location;
|
|
}
|
|
}
|