Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
48 changes: 30 additions & 18 deletions Maple2.Server.Game/Trigger/TriggerContext.Npc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,30 +287,42 @@ private void SpawnNpc(int spawnId, bool useSpawnAnimation = false) {
return;
}

List<SpawnPointNPCListEntry> 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<int> 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<int> 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));
}
}
}
Loading