Skip to content

Commit 22fb850

Browse files
Add spawn radius and pool-based NPC spawning (#632)
Apply random offset within a SpawnRadius when spawning NPCs and snap the resulting position to the navmesh using DotRecast (with logging). Refactor TriggerContext spawn logic to build a per-spawn pool from NpcList, shuffle and cap to NpcCount, validate metadata, pass the SpawnPointNPC to SpawnNpc, and broadcast spawned NPCs. Also add required using imports and minor cleanup around spawn position handling and logging.
1 parent 8920a65 commit 22fb850

2 files changed

Lines changed: 57 additions & 20 deletions

File tree

Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Diagnostics;
33
using System.Diagnostics.CodeAnalysis;
44
using System.Numerics;
5+
using DotRecast.Core.Numerics;
56
using DotRecast.Detour.Crowd;
67
using Maple2.Database.Storage;
78
using Maple2.Model.Common;
@@ -16,6 +17,7 @@
1617
using Maple2.Server.Game.Util;
1718
using Maple2.Tools;
1819
using Maple2.Tools.Collision;
20+
using Maple2.Tools.DotRecast;
1921
using Maple2.Tools.Extensions;
2022
using Maple2.Tools.VectorMath;
2123
using Serilog;
@@ -103,10 +105,33 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId
103105
}
104106

105107
public FieldNpc? SpawnNpc(NpcMetadata npc, Vector3 position, Vector3 rotation, FieldMobSpawn? owner = null, SpawnPointNPC? spawnPointNpc = null, string spawnAnimation = "") {
106-
DtCrowdAgent agent = Navigation.AddAgent(npc, position);
108+
// Apply random offset if SpawnRadius is set
109+
Vector3 spawnPosition = position;
110+
if (spawnPointNpc?.SpawnRadius > 0) {
111+
float angle = Random.Shared.NextSingle() * MathF.PI * 2;
112+
float distance = Random.Shared.NextSingle() * spawnPointNpc.SpawnRadius;
113+
Vector3 offset = new Vector3(
114+
MathF.Cos(angle) * distance,
115+
MathF.Sin(angle) * distance,
116+
0
117+
);
118+
Vector3 randomizedPosition = position + offset;
119+
120+
// Validate and snap to nearest valid navmesh position
121+
if (FindNearestPoly(randomizedPosition, out long nearestRef, out RcVec3f validPosition) && nearestRef != 0) {
122+
spawnPosition = DotRecastHelper.FromNavMeshSpace(validPosition);
123+
logger.Debug("[SpawnNpc] Applied spawn radius {Radius} to NPC {NpcId} at SpawnPoint {SpawnPointId}, offset: {Offset}, snapped from {Original} to {Final}",
124+
spawnPointNpc.SpawnRadius, npc.Id, spawnPointNpc.SpawnPointId, offset, randomizedPosition, spawnPosition);
125+
} else {
126+
logger.Warning("[SpawnNpc] Randomized position {Position} invalid for NPC {NpcId}, using original spawn point {Original}",
127+
randomizedPosition, npc.Id, position);
128+
spawnPosition = position;
129+
}
130+
}
131+
132+
DtCrowdAgent agent = Navigation.AddAgent(npc, spawnPosition);
107133

108134
AnimationMetadata? animation = NpcMetadata.GetAnimation(npc.Model.Name);
109-
Vector3 spawnPosition = position;
110135
var fieldNpc = new FieldNpc(this, NextLocalId(), agent, new Npc(npc, animation), npc.AiPath, patrolDataUUID: spawnPointNpc?.PatrolData, spawnAnimation: spawnAnimation) {
111136
Owner = owner,
112137
Position = spawnPosition,

Maple2.Server.Game/Trigger/TriggerContext.Npc.cs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -287,30 +287,42 @@ private void SpawnNpc(int spawnId, bool useSpawnAnimation = false) {
287287
return;
288288
}
289289

290-
List<SpawnPointNPCListEntry> npcList = spawn.NpcList.OrderBy(_ => Random.Shared.Next()).Take(spawn.NpcCount).ToList();
290+
DebugLog("[SpawnNpc] spawnId:{SpawnId}, SpawnRadius:{SpawnRadius}, NpcCount:{NpcCount}, Position:{Position}",
291+
spawnId, spawn.SpawnRadius, spawn.NpcCount, spawn.Position);
292+
293+
// NpcCount is the total number of NPCs that can exist at this spawn point.
294+
// Each NpcList entry's Count is how many of that type can spawn.
295+
// Build a pool of individual NPC ids from the list, shuffle, and take up to NpcCount.
296+
List<int> npcPool = [];
297+
foreach (SpawnPointNPCListEntry entry in spawn.NpcList) {
298+
for (int j = 0; j < entry.Count; j++) {
299+
npcPool.Add(entry.NpcId);
300+
}
301+
}
302+
303+
// Shuffle and cap at NpcCount
304+
IEnumerable<int> npcIdsToSpawn = npcPool.OrderBy(_ => Random.Shared.Next()).Take(spawn.NpcCount);
291305

292-
foreach (SpawnPointNPCListEntry entry in npcList) {
293-
if (!Field.NpcMetadata.TryGet(entry.NpcId, out NpcMetadata? npc)) {
294-
logger.Error("[SpawnNpc] Invalid npcId:{NpcId}", entry.NpcId);
306+
foreach (int npcId in npcIdsToSpawn) {
307+
if (!Field.NpcMetadata.TryGet(npcId, out NpcMetadata? npc)) {
308+
logger.Error("[SpawnNpc] Invalid npcId:{NpcId}", npcId);
295309
continue;
296310
}
297311

298-
for (int i = 0; i < entry.Count; i++) {
299-
string spawnAnimationString = string.Empty;
300-
if (!string.IsNullOrEmpty(spawn.SpawnAnimation) && useSpawnAnimation) {
301-
spawnAnimationString = spawn.SpawnAnimation;
302-
}
303-
FieldNpc? fieldNpc = Field.SpawnNpc(npc, spawn.Position, spawn.Rotation, spawnAnimation: spawnAnimationString);
304-
if (fieldNpc == null) {
305-
logger.Error("[SpawnNpc] Failed to spawn npcId:{NpcId}", entry.NpcId);
306-
continue;
307-
}
312+
string spawnAnimationString = string.Empty;
313+
if (!string.IsNullOrEmpty(spawn.SpawnAnimation) && useSpawnAnimation) {
314+
spawnAnimationString = spawn.SpawnAnimation;
315+
}
316+
FieldNpc? fieldNpc = Field.SpawnNpc(npc, spawn.Position, spawn.Rotation, spawnPointNpc: spawn, spawnAnimation: spawnAnimationString);
317+
if (fieldNpc == null) {
318+
logger.Error("[SpawnNpc] Failed to spawn npcId:{NpcId}", npcId);
319+
continue;
320+
}
308321

309-
fieldNpc.SpawnPointId = spawnId;
322+
fieldNpc.SpawnPointId = spawnId;
310323

311-
Field.Broadcast(FieldPacket.AddNpc(fieldNpc));
312-
Field.Broadcast(ProxyObjectPacket.AddNpc(fieldNpc));
313-
}
324+
Field.Broadcast(FieldPacket.AddNpc(fieldNpc));
325+
Field.Broadcast(ProxyObjectPacket.AddNpc(fieldNpc));
314326
}
315327
}
316328
}

0 commit comments

Comments
 (0)