using Yuanshen.Protocol; namespace Yuanshen.Gameserver.Game; internal sealed class WorldSpawner { private const float GroupLoadSq = 80f * 80f; private const float GroupUnloadSq = 110f * 110f; private const float LooseLoadSq = 80f * 80f; private const float WildlifeLoadSq = 50f * 50f; private const float LooseUnloadSq = 110f * 110f; private const float StreamStepSq = 20f * 20f; private const int MonsterCap = 60; private const int GadgetCap = 40; private const int LevelBand = 2; private const int LevelSpread = 6; private const int TopNodeLevel = 36; private const int MaxMonsterLevel = 90; private readonly Player _player; private readonly SceneManager _scene; private readonly MonsterSpawnTable _table; private readonly GadgetSpawnTable _gadgets; private readonly SpawnGroupTable _groups; private Vector? _lastStream; public WorldSpawner(Player player, SceneManager scene, MonsterSpawnTable table, GadgetSpawnTable gadgets, SpawnGroupTable groups) { _player = player; _scene = scene; _table = table; _gadgets = gadgets; _groups = groups; } public async ValueTask EnterSceneAsync(Vector pos) { await _scene.ClearMonstersAsync(); await _scene.ClearGadgetsAsync(); _lastStream = null; await StreamAsync(pos, force: true); } public async ValueTask StreamAsync(Vector pos) { await StreamAsync(pos, force: false); } public async ValueTask RespawnAsync(Vector pos) { _player.KilledNodeIds.Clear(); await EnterSceneAsync(pos); } private async ValueTask StreamAsync(Vector pos, bool force) { if (!force && _lastStream is { } last && HorizontalSq(pos, last) < StreamStepSq) return; _lastStream = pos; await DespawnFarAsync(pos); await SpawnNearAsync(pos); await StreamGadgetsAsync(pos); } private async ValueTask DespawnFarAsync(Vector pos) { foreach (GameMonster monster in _player.Monsters.Where(monster => monster.NodeId != 0 && IsOutOfRange(pos, monster)).ToList()) await _scene.DespawnMonsterAsync(monster); } private bool IsOutOfRange(Vector pos, GameMonster monster) { if (_groups.TryGet(monster.GroupId, out SpawnGroup group) && group.IsCompact) return AnchorSq(pos, group) > GroupUnloadSq; return HorizontalSq(pos, monster.Pos) > LooseUnloadSq; } private async ValueTask SpawnNearAsync(Vector pos) { HashSet active = new(); foreach (GameMonster monster in _player.Monsters) active.Add(monster.NodeId); foreach (SpawnNode node in _table.Nodes) { if (active.Count >= MonsterCap) break; if (active.Contains(node.NodeId) || _player.KilledNodeIds.Contains(node.NodeId)) continue; if (!ShouldLoad(pos, node)) continue; GameMonster monster = _player.SpawnMonster(node.MonsterId, node.Pos, ScaleLevel(node.Level), node.Hp, node.Attack, node.Defense, node.NodeId, node.ConfigId, node.GroupId); active.Add(node.NodeId); await _scene.AppearMonsterAsync(monster); } } private bool ShouldLoad(Vector pos, SpawnNode node) { if (_groups.TryGet(node.GroupId, out SpawnGroup group) && group.IsCompact) return AnchorSq(pos, group) <= GroupLoadSq; return HorizontalSq(pos, node.Pos) <= (node.IsWildlife ? WildlifeLoadSq : LooseLoadSq); } private async ValueTask StreamGadgetsAsync(Vector pos) { HashSet live = new(); foreach (GameGadget gadget in _player.Gadgets) live.Add(gadget.NodeId); foreach (GameGadget gadget in _player.Gadgets .Where(gadget => HorizontalSq(pos, gadget.Node.Pos) > LooseUnloadSq) .ToList()) { live.Remove(gadget.NodeId); await _scene.DespawnGadgetAsync(gadget, VisionType.VisionRemove); } foreach (GadgetNode node in _gadgets.Nodes) { if (live.Count >= GadgetCap) break; if (live.Contains(node.NodeId)) continue; if (node.IsChest && _player.OpenedGadgetIds.Contains(node.NodeId)) continue; if (HorizontalSq(pos, node.Pos) > LooseLoadSq) continue; GameGadget gadget = _player.SpawnGadget(node); live.Add(node.NodeId); await _scene.AppearGadgetAsync(gadget); } } private uint ScaleLevel(uint nodeLevel) { int reference = (int)(_player.ActiveAvatar?.Level ?? nodeLevel); if (reference <= (int)nodeLevel) return nodeLevel; int top = reference - LevelBand; int drop = (TopNodeLevel - Math.Min((int)nodeLevel, TopNodeLevel)) * LevelSpread / TopNodeLevel; return (uint)Math.Clamp(top - drop, 1, MaxMonsterLevel); } private static float AnchorSq(Vector pos, SpawnGroup group) { float dx = pos.X - group.X; float dz = pos.Z - group.Z; return dx * dx + dz * dz; } private static float HorizontalSq(Vector a, Vector b) { float dx = a.X - b.X; float dz = a.Z - b.Z; return dx * dx + dz * dz; } }