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
2 changes: 1 addition & 1 deletion Maple2.File.Ingest/Mapper/MapEntityMapper.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,7 +225,7 @@ private IEnumerable<MapEntity> ParseMap(string xblock, IEnumerable<IMapEntity> e
patrolData.ApproachAnims.TryGetValue(wayPointDict.Key, out string? approachAnimation);
patrolData.ArriveAnims.TryGetValue(wayPointDict.Key, out string? arriveAnimation);
patrolData.ArriveAnimsTime.TryGetValue(wayPointDict.Key, out uint arriveAnimationTime);
wayPoints.Add(new MS2WayPoint(wayPoint.EntityId, wayPoint.IsVisible, wayPoint.Position, wayPoint.Rotation, approachAnimation ?? "", arriveAnimation ?? "", (int) arriveAnimationTime));
wayPoints.Add(new MS2WayPoint(wayPoint.EntityId, wayPoint.IsVisible, wayPoint.Position, wayPoint.Rotation, approachAnimation ?? "", arriveAnimation ?? "", (int) arriveAnimationTime, patrolData.IsAirWayPoint));
}


Expand Down
3 changes: 2 additions & 1 deletion Maple2.Model/Metadata/MapEntity/PatrolData.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,5 +19,6 @@ public record MS2WayPoint(
Vector3 Rotation,
string ApproachAnimation,
string ArriveAnimation,
int ArriveAnimationTime
int ArriveAnimationTime,
bool AirWayPoint
);
23 changes: 22 additions & 1 deletion Maple2.Server.Game/Manager/Field/AgentNavigation.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,7 +72,7 @@ public AgentNavigation(FieldNpc fieldNpc, DtCrowdAgent dtAgent, DtCrowd dtCrowd)
while (pathIterPolysCount > 0 && smoothPath.Count < MAX_SMOOTH) {
// Find location to steer towards.
if (!DtPathUtils.GetSteerTarget(navMeshQuery, iterPos, endPos, DotRecastHelper.MIN_TARGET_DIST,
pathIterPolys, pathIterPolysCount, out var steerPos, out int steerPosFlag, out long steerPosRef)) {
pathIterPolys, pathIterPolysCount, out var steerPos, out int steerPosFlag, out long steerPosRef)) {
break;
}

Expand DownExpand Up@@ -221,6 +221,27 @@ public Vector3 GetAgentPosition() {
return (start, DotRecastHelper.FromNavMeshSpace(end));
}

/// <summary>
/// Advances the actor in a straight line towards the target position, ignoring navmesh.
/// </summary>
/// <param name="start">Current position</param>
/// <param name="target">Target position</param>
/// <param name="speed">Movement speed</param>
/// <param name="deltaTime">Time in seconds</param>
/// <returns>Tuple of (new position, reachedTarget)</returns>
public (Vector3 newPosition, bool reachedTarget) FlyAdvance(Vector3 start, Vector3 target, float speed, float deltaTime) {
Vector3 direction = target - start;
float distance = direction.Length();
if (distance == 0) return (target, true);

float moveDist = speed * deltaTime;
if (moveDist >= distance) {
return (target, true);
}
Vector3 step = direction / distance * moveDist;
return (start + step, false);
}

public Vector3 GetRandomPatrolPoint() {
if (!field.FindNearestPoly(npc.Origin, out long startRef, out RcVec3f startVec)) {
return npc.Position;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,19 @@ public NpcTask TryMoveTo(Vector3 position, bool isBattle, string sequence = "",
};
}

public NpcTask TryFlyTo(Vector3 position, bool isBattle, string sequence = "", float speed = 0, bool lookAt = false) {
walkTask?.Cancel();

NpcTaskPriority priority = isBattle ? NpcTaskPriority.BattleWalk : NpcTaskPriority.IdleAction;

return new NpcFlyToTask(actor.TaskState, priority, this) {
Position = position,
Sequence = sequence,
Speed = speed,
LookAt = lookAt,
};
}

public NpcTask TryMoveTargetDistance(IActor target, float distance, bool isBattle, string sequence = "", float speed = 0) {
walkTask?.Cancel();

Expand DownExpand Up@@ -193,7 +206,7 @@ public void Update(long tickCount) {
return;
}

long tickDelta = Math.Min(lastTick == 0 ? 0 : tickCount - lastTick, 20);
long tickDelta = Math.Min(lastTick == 0 ? 0 : tickCount - lastTick, 200);

RemoveDebugMarker(debugNpc, tickCount);
RemoveDebugMarker(debugTarget, tickCount);
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
using Maple2.Model.Enum;
using Maple2.Model.Enum;
using Maple2.Model.Metadata;
using Maple2.Server.Game.Model.Enum;
using System.Numerics;
Expand All@@ -15,6 +14,7 @@ private enum WalkType {
ToTarget,
FromTarget
}

private Vector3 walkDirection;
private Vector3 walkTargetPosition;
private float walkTargetDistance;
Expand All@@ -25,6 +25,7 @@ private enum WalkType {
private AnimationSequenceMetadata? walkSequence = null;
private float walkSpeed;
private NpcTask? walkTask = null;
private bool isFlying;

private void UpdateMoveSpeed(float speed) {
Stat moveSpeed = actor.Stats.Values[BasicAttribute.MovementSpeed];
Expand DownExpand Up@@ -86,10 +87,28 @@ private void StateWalkUpdate(long tickCount, long tickDelta) {
UpdateMoveSpeed(speedOverride);

float delta = (float) tickDelta / 1000;

if (walkType == WalkType.Direction) {
StateWalkDirectionUpdate(tickCount, tickDelta, delta);
return;
}

// --- FLYING ADVANCE LOGIC ---
// If isFlying, use direct advance
if (walkSequence != null && isFlying) {
Vector3 target = walkTargetPosition;
(Vector3 newPos, bool reachedFlying) = actor.Navigation.FlyAdvance(actor.Position, target, Speed, delta);

Velocity = (newPos - actor.Position) / delta;
actor.Position = newPos;

if (walkLookWhenDone && (target - actor.Position).LengthSquared() > 0) {
actor.Transform.LookTo(Vector3.Normalize(target - actor.Position));
}

if (reachedFlying) {
Velocity = Vector3.Zero;
walkTask?.Completed();
}
return;
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
using static Maple2.Server.Game.Model.ActorStateComponent.TaskState;

namespace Maple2.Server.Game.Model.ActorStateComponent;

public partial class MovementState {

public class NpcMoveDirectionTask : NpcTask {
Expand DownExpand Up@@ -75,6 +76,33 @@ protected override void TaskFinished(bool isCompleted) {
}
}

public class NpcFlyToTask : NpcTask {
private readonly MovementState movement;
public Vector3 Position { get; init; }
public string Sequence { get; init; } = "";
public float Speed { get; init; }
public bool LookAt { get; init; }
public override bool CancelOnInterrupt => Priority == NpcTaskPriority.IdleAction;

public NpcFlyToTask(TaskState taskState, NpcTaskPriority priority, MovementState movement) : base(taskState, priority) {
this.movement = movement;
}

protected override void TaskResumed() {
movement.FlyTo(this, Position, Sequence, Speed, LookAt);
}

protected override void TaskPaused() {
movement.Idle();
}

protected override void TaskFinished(bool isCompleted) {
movement.walkTask = null;
movement.isFlying = false;
movement.Idle();
}
}

private void MoveTo(NpcTask task, Vector3 position, string sequence, float speed, bool lookAt) {
if (!CanTransitionToState(ActorState.Walk)) {
task.Cancel();
Expand DownExpand Up@@ -107,6 +135,30 @@ private void MoveTo(NpcTask task, Vector3 position, string sequence, float speed
StartWalking(sequence, task);
}

private void FlyTo(NpcTask task, Vector3 position, string sequence, float speed, bool lookAt) {
if (!CanTransitionToState(ActorState.Walk)) {
task.Cancel();
return;
}

if (actor.Navigation is null) {
task.Cancel();

return;
}

actor.AppendDebugMessage($"> Flying to position\n");

walkTargetPosition = position;
walkType = WalkType.MoveTo;
walkLookWhenDone = lookAt;
walkTask = task;
isFlying = true;

UpdateMoveSpeed(speed);
StartWalking(sequence, task);
}

public class NpcMoveTargetDistanceTask : NpcTask {
private readonly MovementState movement;
public IActor Target { get; init; }
Expand Down
32 changes: 25 additions & 7 deletions Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,7 @@ public short SequenceId {
public readonly AnimationSequenceMetadata IdleSequenceMetadata;
public readonly AnimationSequenceMetadata? JumpSequence;
public readonly AnimationSequenceMetadata? WalkSequence;
public readonly AnimationSequenceMetadata? FlySequence;
public readonly AnimationSequenceMetadata? SpawnSequence;
private readonly WeightedSet<string> defaultRoutines;
public readonly AiState AiState;
Expand All@@ -98,6 +99,7 @@ public FieldNpc(FieldManager field, int objectId, DtCrowdAgent? agent, Npc npc,
IdleSequenceMetadata = npc.Animations.GetValueOrDefault("Idle_A") ?? new AnimationSequenceMetadata(string.Empty, -1, 1f, null);
JumpSequence = npc.Animations.GetValueOrDefault("Jump_A") ?? npc.Animations.GetValueOrDefault("Jump_B");
WalkSequence = npc.Animations.GetValueOrDefault("Walk_A");
FlySequence = npc.Animations.GetValueOrDefault("Fly_A");
SpawnSequence = npc.Animations.GetValueOrDefault(spawnAnimation);
defaultRoutines = new WeightedSet<string>();
foreach (NpcAction action in Value.Metadata.Action.Actions) {
Expand DownExpand Up@@ -258,25 +260,41 @@ public override void KeyframeEvent(string keyName) {

private NpcTask? NextWaypoint() {
MS2WayPoint currentWaypoint = Patrol!.WayPoints[currentWaypointIndex];
MS2WayPoint? waypointBefore = null;
if (Patrol.IsLoop) {
waypointBefore = Patrol.WayPoints[(currentWaypointIndex - 1 + Patrol.WayPoints.Count) % Patrol.WayPoints.Count];
} else if (currentWaypointIndex != 0) {
waypointBefore = Patrol.WayPoints[currentWaypointIndex - 1];
}

if (!string.IsNullOrEmpty(currentWaypoint.ArriveAnimation) && idleTask is not MovementState.NpcEmoteTask) {
if (Value.Animations.TryGetValue(currentWaypoint.ArriveAnimation, out AnimationSequenceMetadata? arriveSequence)) {
if (waypointBefore is not null && !string.IsNullOrEmpty(waypointBefore.ArriveAnimation) && idleTask is not (MovementState.NpcEmoteTask or null)) {
if (Value.Animations.TryGetValue(waypointBefore.ArriveAnimation, out AnimationSequenceMetadata? arriveSequence)) {
return MovementState.TryEmote(arriveSequence.Name, false);
}
}

NpcTask? approachTask = null;

if (Navigation!.PathTo(currentWaypoint.Position)) {
if (currentWaypoint.AirWayPoint) {
if (Value.Animations.TryGetValue(currentWaypoint.ApproachAnimation, out AnimationSequenceMetadata? patrolSequence)) {
approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, sequence: patrolSequence.Name);
} else if (WalkSequence is not null) {
approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, WalkSequence.Name);
approachTask = MovementState.TryFlyTo(currentWaypoint.Position, false, sequence: patrolSequence.Name, speed: Patrol.PatrolSpeed / 2, lookAt: true);
} else if (FlySequence is not null) {
approachTask = MovementState.TryFlyTo(currentWaypoint.Position, false, sequence: FlySequence.Name, speed: Patrol.PatrolSpeed / 2, lookAt: true);
} else {
Logger.Warning("No walk sequence found for npc {NpcId} in patrol {PatrolId}", Value.Metadata.Id, Patrol.Uuid);
}
} else {
Logger.Warning("Failed to path to waypoint id({Id}) coord {Coord} for npc {NpcName} - {NpcId} in patrol {PatrolId}", currentWaypoint.Id, currentWaypoint.Position, Value.Metadata.Name, Value.Metadata.Id, Patrol.Uuid);
if (Navigation!.PathTo(currentWaypoint.Position)) {
if (Value.Animations.TryGetValue(currentWaypoint.ApproachAnimation, out AnimationSequenceMetadata? patrolSequence)) {
approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, sequence: patrolSequence.Name, speed: 1);
} else if (WalkSequence is not null) {
approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, sequence: WalkSequence.Name, speed: 1);
} else {
Logger.Warning("No walk sequence found for npc {NpcId} in patrol {PatrolId}", Value.Metadata.Id, Patrol.Uuid);
}
} else {
Logger.Warning("Failed to path to waypoint id({Id}) coord {Coord} for npc {NpcName} - {NpcId} in patrol {PatrolId}", currentWaypoint.Id, currentWaypoint.Position, Value.Metadata.Name, Value.Metadata.Id, Patrol.Uuid);
}
}

MS2WayPoint lastWaypoint = Patrol.WayPoints.Last();
Expand Down