ModernUO/Projects/UOContent/Engines/Spawners/Spawner.cs
Kamron Batman 6d51b33cf8
feat: Add CanSpawnMobile overload with props Z-range support. (#2293)
### Summary

- Adds CanSpawnMobile(x, y, minZ, maxZ, canSwim, cantWalk, out spawnZ) overload for finding spawn surfaces within a Z range
- Adds CanSpawnItem(x, y, minZ, maxZ, out spawnZ) for item spawning with Surface+Impassable support (tables, furniture)
- Uses bitmask optimization inspired by Item.DropToWorld's m_OpenSlots pattern for O(1) surface/blocker checks
- HomeRange spawners now use surface detection to set proper Z bounds

### Key Changes

Map.cs:
- CanSpawnMobile with Z-range finds lowest valid surface for mobiles
- CanSpawnItem with Z-range finds lowest valid surface for items (including tables)
- CanFitItem for point-check item placement on Surface+Impassable tiles
- Bitmask approach eliminates nested loops and stackalloc arrays

Spawners:
- Simplified GetSpawnPosition using new Z-range methods
- HomeRange setter detects surface below spawner for proper Z bounds
- Consistent handling for mobiles and items

### Bug Fixes

- Water tiles (Impassable | Wet) no longer block swimming mobs
- Items can now spawn on tables/furniture (Surface+Impassable)

### Test Plan

- Run dotnet test - 631 tests pass
- Manual testing: multi-story spawning, water mobs, item spawning on tables
- Verify HomeRange spawner movement shifts bounds correctly
2025-12-27 17:01:15 -08:00

88 lines
2.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 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;
}
var bounds = SpawnBounds;
// No bounds = HomeRange of 0, spawn at spawner location
if (bounds == default)
{
return Location;
}
// Z range from SpawnBounds (supports multi-story buildings)
var minZ = bounds.Start.Z;
var maxZ = bounds.End.Z - 1;
// Try 10 times to find a valid location.
for (var i = 0; i < 10; i++)
{
var x = Utility.RandomMinMax(bounds.Start.X, bounds.End.X - 1);
var y = Utility.RandomMinMax(bounds.Start.Y, bounds.End.Y - 1);
if (spawned is Mobile mob && map.CanSpawnMobile(x, y, minZ, maxZ, mob.CanSwim, mob.CantWalk, out var spawnZ)
|| spawned is Item && map.CanSpawnItem(x, y, minZ, maxZ, out spawnZ))
{
return new Point3D(x, y, spawnZ);
}
}
return Location;
}
}