diff --git a/Source/Game/Movement/Generators/PathGenerator.cs b/Source/Game/Movement/Generators/PathGenerator.cs index 565eddc44..71808f27f 100644 --- a/Source/Game/Movement/Generators/PathGenerator.cs +++ b/Source/Game/Movement/Generators/PathGenerator.cs @@ -920,6 +920,15 @@ namespace Game.Movement return (p1 - p2).LengthSquared(); } + public float GetPathLength() + { + float length = 0.0f; + for (var i = 0; i < _pathPoints.Length - 1; ++i) + length += (_pathPoints[i + 1] - _pathPoints[i]).Length(); + + return length; + } + public void ShortenPathUntilDist(Position pos, float dist) { ShortenPathUntilDist(new Vector3(pos.posX, pos.posY, pos.posZ), dist); } public void ShortenPathUntilDist(Vector3 target, float dist) diff --git a/Source/Game/Movement/Generators/PointMovement.cs b/Source/Game/Movement/Generators/PointMovement.cs index 3cb345e26..09021fabe 100644 --- a/Source/Game/Movement/Generators/PointMovement.cs +++ b/Source/Game/Movement/Generators/PointMovement.cs @@ -192,7 +192,9 @@ namespace Game.Movement public void MovementInform(Unit owner) { - owner.ToCreature()?.GetAI()?.MovementInform(MovementGeneratorType.Point, _movementId); + // deliver EVENT_CHARGE to scripts, EVENT_CHARGE_PREPATH is just internal implementation detail of this movement generator + uint movementId = _movementId == EventId.ChargePrepath ? EventId.Charge : _movementId; + owner.ToCreature()?.GetAI()?.MovementInform(MovementGeneratorType.Point, movementId); } public override void UnitSpeedChanged() diff --git a/Source/Game/Movement/MotionMaster.cs b/Source/Game/Movement/MotionMaster.cs index baf7208d7..370a82b00 100644 --- a/Source/Game/Movement/MotionMaster.cs +++ b/Source/Game/Movement/MotionMaster.cs @@ -702,6 +702,8 @@ namespace Game.Movement MoveCharge(dest.X, dest.Y, dest.Z, SPEED_CHARGE, EventId.ChargePrepath); + // If this is ever changed to not happen immediately then all spell effect handlers that use this must be updated + // Charge movement is not started when using EVENT_CHARGE_PREPATH MoveSplineInit init = new(_owner); init.MovebyPath(path.GetPath()); diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index 78b20c26c..d976af0f0 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -337,8 +337,36 @@ namespace Game.Spells public void RecalculateDelayMomentForDst() { - m_delayMoment = CalculateDelayMomentForDst(0.0f); - m_caster.m_Events.ModifyEventTime(_spellEvent, TimeSpan.FromMilliseconds(GetDelayStart() + m_delayMoment)); + UpdateDelayMomentForDst(CalculateDelayMomentForDst(0.0f)); + } + + void UpdateDelayMomentForDst(ulong hitDelay) + { + m_delayMoment = hitDelay; + + if (GetDelayStart() != 0) + m_caster.m_Events.ModifyEventTime(_spellEvent, TimeSpan.FromMilliseconds(GetDelayStart() + m_delayMoment)); + } + + void UpdateDelayMomentForUnitTarget(Unit unit, ulong hitDelay) + { + var itr = m_UniqueTargetInfo.Find(targetInfo => targetInfo.TargetGUID == unit.GetGUID()); + + ulong oldDelay = itr.TimeDelay; + itr.TimeDelay = hitDelay; + + if (hitDelay != 0 && (m_delayMoment == 0 || m_delayMoment > hitDelay)) + m_delayMoment = hitDelay; + else if (m_delayMoment != 0 && oldDelay < hitDelay) + { + // if new hit delay is greater than old delay for this target we must check all other spell targets to see if m_delayMoment can be increased + var minDelay = m_UniqueTargetInfo.Min(targetInfo => targetInfo.TimeDelay); + + m_delayMoment = minDelay; + } + + if (GetDelayStart() != 0) + m_caster.m_Events.ModifyEventTime(_spellEvent, TimeSpan.FromMilliseconds(GetDelayStart() + m_delayMoment)); } void SelectEffectImplicitTargets(SpellEffectInfo spellEffectInfo, SpellImplicitTargetInfo targetType, ref uint processedEffectMask) @@ -3054,7 +3082,9 @@ namespace Game.Spells return 0; } + // when spell has a single missile we hit all targets (except caster) at the same time bool single_missile = m_targets.HasDst(); + bool ignoreTargetInfoTimeDelay = single_missile; ulong next_time = 0; if (!m_launchHandled) @@ -3065,21 +3095,13 @@ namespace Game.Spells HandleLaunchPhase(); m_launchHandled = true; - if (m_delayMoment > offset) - { - if (single_missile) - return m_delayMoment; - - next_time = m_delayMoment; - if ((m_UniqueTargetInfo.Count > 2 || (m_UniqueTargetInfo.Count == 1 && m_UniqueTargetInfo[0].TargetGUID == m_caster.GetGUID())) || !m_UniqueGOTargetInfo.Empty()) - { - offset = 0; // if LaunchDelay was present then the only target that has timeDelay = 0 is m_caster - and that is the only target we want to process now - } - } } - if (single_missile && offset == 0) - return m_delayMoment; + if (m_delayMoment > offset) + { + ignoreTargetInfoTimeDelay = false; + next_time = m_delayMoment; + } Player modOwner = m_caster.GetSpellModOwner(); if (modOwner != null) @@ -3087,7 +3109,7 @@ namespace Game.Spells PrepareTargetProcessing(); - if (!m_immediateHandled && offset != 0) + if (!m_immediateHandled && m_delayMoment <= offset) { _handle_immediate_phase(); m_immediateHandled = true; @@ -3098,13 +3120,13 @@ namespace Game.Spells List delayedTargets = new(); m_UniqueTargetInfo.RemoveAll(target => { - if (single_missile || target.TimeDelay <= offset) + if (ignoreTargetInfoTimeDelay || target.TimeDelay <= offset) { target.TimeDelay = offset; delayedTargets.Add(target); return true; } - else if (next_time == 0 || target.TimeDelay < next_time) + else if (!single_missile && (next_time == 0 || target.TimeDelay < next_time)) next_time = target.TimeDelay; return false; }); @@ -3117,13 +3139,13 @@ namespace Game.Spells List delayedGOTargets = new(); m_UniqueGOTargetInfo.RemoveAll(goTarget => { - if (single_missile || goTarget.TimeDelay <= offset) + if (ignoreTargetInfoTimeDelay || goTarget.TimeDelay <= offset) { goTarget.TimeDelay = offset; delayedGOTargets.Add(goTarget); return true; } - else if (next_time == 0 || goTarget.TimeDelay < next_time) + else if (!single_missile && (next_time == 0 || goTarget.TimeDelay < next_time)) next_time = goTarget.TimeDelay; return false; }); @@ -9070,7 +9092,7 @@ namespace Game.Spells if (m_Spell.m_spellInfo.LaunchDelay != 0) Cypher.Assert(n_offset == (ulong)Math.Floor(m_Spell.m_spellInfo.LaunchDelay * 1000.0f)); else - Cypher.Assert(n_offset == m_Spell.GetDelayMoment()); + Cypher.Assert(n_offset == m_Spell.GetDelayMoment(), $"{n_offset} == {m_Spell.GetDelayMoment()}"); // re-plan the event for the delay moment m_Spell.GetCaster().m_Events.AddEvent(this, TimeSpan.FromMilliseconds(e_time + n_offset), false); diff --git a/Source/Game/Spells/SpellEffects.cs b/Source/Game/Spells/SpellEffects.cs index 9d0f08d91..f190103ed 100644 --- a/Source/Game/Spells/SpellEffects.cs +++ b/Source/Game/Spells/SpellEffects.cs @@ -3528,21 +3528,17 @@ namespace Game.Spells if (m_preGeneratedPath == null) { Position pos = unitTarget.GetFirstCollisionPosition(unitTarget.GetCombatReach(), unitTarget.GetRelativeAngle(m_caster.GetPosition())); - if (MathFunctions.fuzzyGt(m_spellInfo.Speed, 0.0f) && m_spellInfo.HasAttribute(SpellAttr9.SpecialDelayCalculation)) - speed = pos.GetExactDist(m_caster) / speed; - - unitCaster.GetMotionMaster().MoveCharge(pos.posX, pos.posY, pos.posZ, speed, EventId.Charge, false, unitTarget, spellEffectExtraData); + m_preGeneratedPath = new PathGenerator(unitCaster); + m_preGeneratedPath.CalculatePath(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), false); } - else - { - if (MathFunctions.fuzzyGt(m_spellInfo.Speed, 0.0f) && m_spellInfo.HasAttribute(SpellAttr9.SpecialDelayCalculation)) - { - Vector3 pos = m_preGeneratedPath.GetActualEndPosition(); - speed = new Position(pos.X, pos.Y, pos.Z).GetExactDist(m_caster) / speed; - } - unitCaster.GetMotionMaster().MoveCharge(m_preGeneratedPath, speed, unitTarget, spellEffectExtraData); - } + if (MathFunctions.fuzzyGt(m_spellInfo.Speed, 0.0f) && m_spellInfo.HasAttribute(SpellAttr9.SpecialDelayCalculation)) + speed = m_preGeneratedPath.GetPathLength() / speed; + + unitCaster.GetMotionMaster().MoveCharge(m_preGeneratedPath, speed, unitTarget, spellEffectExtraData); + + // abuse implementation detail of MoveCharge accepting PathGenerator argument (instantly started spline) + UpdateDelayMomentForUnitTarget(unitTarget, (uint)unitCaster.MoveSpline.Duration()); } if (effectHandleMode == SpellEffectHandleMode.HitTarget) @@ -3578,7 +3574,18 @@ namespace Game.Spells pos = unitCaster.GetFirstCollisionPosition(dist, angle); } - unitCaster.GetMotionMaster().MoveCharge(pos.posX, pos.posY, pos.posZ); + PathGenerator path = new(unitCaster); + path.CalculatePath(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ(), false); + + float speed = MathFunctions.fuzzyGt(m_spellInfo.Speed, 0.0f) ? m_spellInfo.Speed : MotionMaster.SPEED_CHARGE; + + if (MathFunctions.fuzzyGt(m_spellInfo.Speed, 0.0f) && m_spellInfo.HasAttribute(SpellAttr9.SpecialDelayCalculation)) + speed = path.GetPathLength() / speed; + + unitCaster.GetMotionMaster().MoveCharge(path, speed); + + // abuse implementation detail of MoveCharge accepting PathGenerator argument (instantly started spline) + UpdateDelayMomentForDst((uint)unitCaster.MoveSpline.Duration()); } else if (effectHandleMode == SpellEffectHandleMode.Hit) {