diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index bbd8a3e9..89b3d89f 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Numerics; +using DotRecast.Core.Numerics; using DotRecast.Detour.Crowd; using Maple2.Database.Storage; using Maple2.Model.Common; @@ -16,6 +17,7 @@ using Maple2.Server.Game.Util; using Maple2.Tools; using Maple2.Tools.Collision; +using Maple2.Tools.DotRecast; using Maple2.Tools.Extensions; using Maple2.Tools.VectorMath; using Serilog; @@ -103,10 +105,33 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId } public FieldNpc? SpawnNpc(NpcMetadata npc, Vector3 position, Vector3 rotation, FieldMobSpawn? owner = null, SpawnPointNPC? spawnPointNpc = null, string spawnAnimation = "") { - DtCrowdAgent agent = Navigation.AddAgent(npc, position); + // Apply random offset if SpawnRadius is set + Vector3 spawnPosition = position; + if (spawnPointNpc?.SpawnRadius > 0) { + float angle = Random.Shared.NextSingle() * MathF.PI * 2; + float distance = Random.Shared.NextSingle() * spawnPointNpc.SpawnRadius; + Vector3 offset = new Vector3( + MathF.Cos(angle) * distance, + MathF.Sin(angle) * distance, + 0 + ); + Vector3 randomizedPosition = position + offset; + + // Validate and snap to nearest valid navmesh position + if (FindNearestPoly(randomizedPosition, out long nearestRef, out RcVec3f validPosition) && nearestRef != 0) { + spawnPosition = DotRecastHelper.FromNavMeshSpace(validPosition); + logger.Debug("[SpawnNpc] Applied spawn radius {Radius} to NPC {NpcId} at SpawnPoint {SpawnPointId}, offset: {Offset}, snapped from {Original} to {Final}", + spawnPointNpc.SpawnRadius, npc.Id, spawnPointNpc.SpawnPointId, offset, randomizedPosition, spawnPosition); + } else { + logger.Warning("[SpawnNpc] Randomized position {Position} invalid for NPC {NpcId}, using original spawn point {Original}", + randomizedPosition, npc.Id, position); + spawnPosition = position; + } + } + + DtCrowdAgent agent = Navigation.AddAgent(npc, spawnPosition); AnimationMetadata? animation = NpcMetadata.GetAnimation(npc.Model.Name); - Vector3 spawnPosition = position; var fieldNpc = new FieldNpc(this, NextLocalId(), agent, new Npc(npc, animation), npc.AiPath, patrolDataUUID: spawnPointNpc?.PatrolData, spawnAnimation: spawnAnimation) { Owner = owner, Position = spawnPosition, diff --git a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs index 3786f7e6..3faacf12 100644 --- a/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs +++ b/Maple2.Server.Game/Trigger/TriggerContext.Npc.cs @@ -287,30 +287,42 @@ private void SpawnNpc(int spawnId, bool useSpawnAnimation = false) { return; } - List npcList = spawn.NpcList.OrderBy(_ => Random.Shared.Next()).Take(spawn.NpcCount).ToList(); + DebugLog("[SpawnNpc] spawnId:{SpawnId}, SpawnRadius:{SpawnRadius}, NpcCount:{NpcCount}, Position:{Position}", + spawnId, spawn.SpawnRadius, spawn.NpcCount, spawn.Position); + + // NpcCount is the total number of NPCs that can exist at this spawn point. + // Each NpcList entry's Count is how many of that type can spawn. + // Build a pool of individual NPC ids from the list, shuffle, and take up to NpcCount. + List npcPool = []; + foreach (SpawnPointNPCListEntry entry in spawn.NpcList) { + for (int j = 0; j < entry.Count; j++) { + npcPool.Add(entry.NpcId); + } + } + + // Shuffle and cap at NpcCount + IEnumerable npcIdsToSpawn = npcPool.OrderBy(_ => Random.Shared.Next()).Take(spawn.NpcCount); - foreach (SpawnPointNPCListEntry entry in npcList) { - if (!Field.NpcMetadata.TryGet(entry.NpcId, out NpcMetadata? npc)) { - logger.Error("[SpawnNpc] Invalid npcId:{NpcId}", entry.NpcId); + foreach (int npcId in npcIdsToSpawn) { + if (!Field.NpcMetadata.TryGet(npcId, out NpcMetadata? npc)) { + logger.Error("[SpawnNpc] Invalid npcId:{NpcId}", npcId); continue; } - for (int i = 0; i < entry.Count; i++) { - string spawnAnimationString = string.Empty; - if (!string.IsNullOrEmpty(spawn.SpawnAnimation) && useSpawnAnimation) { - spawnAnimationString = spawn.SpawnAnimation; - } - FieldNpc? fieldNpc = Field.SpawnNpc(npc, spawn.Position, spawn.Rotation, spawnAnimation: spawnAnimationString); - if (fieldNpc == null) { - logger.Error("[SpawnNpc] Failed to spawn npcId:{NpcId}", entry.NpcId); - continue; - } + string spawnAnimationString = string.Empty; + if (!string.IsNullOrEmpty(spawn.SpawnAnimation) && useSpawnAnimation) { + spawnAnimationString = spawn.SpawnAnimation; + } + FieldNpc? fieldNpc = Field.SpawnNpc(npc, spawn.Position, spawn.Rotation, spawnPointNpc: spawn, spawnAnimation: spawnAnimationString); + if (fieldNpc == null) { + logger.Error("[SpawnNpc] Failed to spawn npcId:{NpcId}", npcId); + continue; + } - fieldNpc.SpawnPointId = spawnId; + fieldNpc.SpawnPointId = spawnId; - Field.Broadcast(FieldPacket.AddNpc(fieldNpc)); - Field.Broadcast(ProxyObjectPacket.AddNpc(fieldNpc)); - } + Field.Broadcast(FieldPacket.AddNpc(fieldNpc)); + Field.Broadcast(ProxyObjectPacket.AddNpc(fieldNpc)); } } }