diff --git a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs index 1f9f481a4..11bcaddee 100644 --- a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs @@ -225,7 +225,7 @@ private IEnumerable ParseMap(string xblock, IEnumerable 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)); } diff --git a/Maple2.Model/Metadata/MapEntity/PatrolData.cs b/Maple2.Model/Metadata/MapEntity/PatrolData.cs index bb1f7e951..12fad9bce 100644 --- a/Maple2.Model/Metadata/MapEntity/PatrolData.cs +++ b/Maple2.Model/Metadata/MapEntity/PatrolData.cs @@ -19,5 +19,6 @@ public record MS2WayPoint( Vector3 Rotation, string ApproachAnimation, string ArriveAnimation, - int ArriveAnimationTime + int ArriveAnimationTime, + bool AirWayPoint ); diff --git a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs index 12b536879..4e32ef499 100644 --- a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs +++ b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs @@ -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; } @@ -221,6 +221,27 @@ public Vector3 GetAgentPosition() { return (start, DotRecastHelper.FromNavMeshSpace(end)); } + /// + /// Advances the actor in a straight line towards the target position, ignoring navmesh. + /// + /// Current position + /// Target position + /// Movement speed + /// Time in seconds + /// Tuple of (new position, reachedTarget) + 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; diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs index 04678a7d7..5b2af2aa3 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementState.cs @@ -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(); @@ -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); diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs index 0bf75bf25..e2090e22c 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateStates/MovementState.Walk.cs @@ -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; @@ -15,6 +14,7 @@ private enum WalkType { ToTarget, FromTarget } + private Vector3 walkDirection; private Vector3 walkTargetPosition; private float walkTargetDistance; @@ -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]; @@ -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; } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.WalkTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.WalkTask.cs index 82cd81126..084b08368 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.WalkTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.WalkTask.cs @@ -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 { @@ -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(); @@ -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; } diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index f64b11ae8..49e2ab508 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -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 defaultRoutines; public readonly AiState AiState; @@ -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(); foreach (NpcAction action in Value.Metadata.Action.Actions) { @@ -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();