diff --git a/BossMod/AI/AIBehaviour.cs b/BossMod/AI/AIBehaviour.cs index f3143e49f6..781ede8d18 100644 --- a/BossMod/AI/AIBehaviour.cs +++ b/BossMod/AI/AIBehaviour.cs @@ -36,6 +36,7 @@ public void Execute(Actor player, Actor master) _afkMode = master != player && !master.InCombat && (WorldState.CurrentTime - _masterLastMoved).TotalSeconds > 10; bool gazeImminent = autorot.Hints.ForbiddenDirections.Count > 0 && autorot.Hints.ForbiddenDirections[0].activation <= WorldState.FutureTime(0.5f); bool pyreticImminent = autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && autorot.Hints.ImminentSpecialMode.activation <= WorldState.FutureTime(1); + bool misdirectionMode = autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Misdirection && autorot.Hints.ImminentSpecialMode.activation <= WorldState.CurrentTime; bool forbidTargeting = _config.ForbidActions || _afkMode || gazeImminent || pyreticImminent; Targeting target = new(); @@ -61,7 +62,7 @@ public void Execute(Actor player, Actor master) bool moveWithMaster = masterIsMoving && _followMaster && master != player; ForceMovementIn = moveWithMaster || gazeImminent || pyreticImminent ? 0 : _naviDecision.LeewaySeconds; - UpdateMovement(player, master, target, gazeImminent || pyreticImminent, !forbidTargeting ? autorot.Hints.ActionsToExecute : null); + UpdateMovement(player, master, target, gazeImminent || pyreticImminent, misdirectionMode ? autorot.Hints.MisdirectionThreshold : default, !forbidTargeting ? autorot.Hints.ActionsToExecute : null); } // returns null if we're to be idle, otherwise target to attack @@ -172,7 +173,7 @@ private bool TrackMasterMovement(Actor master) return masterIsMoving; } - private void UpdateMovement(Actor player, Actor master, Targeting target, bool gazeOrPyreticImminent, ActionQueue? queueForSprint) + private void UpdateMovement(Actor player, Actor master, Targeting target, bool gazeOrPyreticImminent, Angle misdirectionAngle, ActionQueue? queueForSprint) { if (gazeOrPyreticImminent) { @@ -181,6 +182,19 @@ private void UpdateMovement(Actor player, Actor master, Targeting target, bool g ctrl.NaviTargetVertical = null; ctrl.ForceCancelCast = true; } + else if (misdirectionAngle != default && _naviDecision.Destination != null) + { + ctrl.NaviTargetPos = _naviDecision.NextTurn == 0 ? _naviDecision.Destination + : player.Position + (_naviDecision.Destination.Value - player.Position).Rotate(_naviDecision.NextTurn > 0 ? -misdirectionAngle : misdirectionAngle); + ctrl.AllowInterruptingCastByMovement = true; + + // debug + //void drawLine(WPos from, WPos to, uint color) => Camera.Instance!.DrawWorldLine(new(from.X, player.PosRot.Y, from.Z), new(to.X, player.PosRot.Y, to.Z), color); + //var toDest = _naviDecision.Destination.Value - player.Position; + //drawLine(player.Position, _naviDecision.Destination.Value, 0xff00ff00); + //drawLine(_naviDecision.Destination.Value, _naviDecision.Destination.Value + toDest.Normalized().OrthoL(), 0xff00ff00); + //drawLine(player.Position, ctrl.NaviTargetPos.Value, 0xff00ffff); + } else { var toDest = _naviDecision.Destination != null ? _naviDecision.Destination.Value - player.Position : new(); diff --git a/BossMod/BossModule/AIHints.cs b/BossMod/BossModule/AIHints.cs index 926a8c4224..fcc3065c0b 100644 --- a/BossMod/BossModule/AIHints.cs +++ b/BossMod/BossModule/AIHints.cs @@ -29,7 +29,7 @@ public enum SpecialMode Normal, Pyretic, // pyretic/acceleration bomb type of effects - no movement, no actions, no casting allowed at activation time Freezing, // should be moving at activation time - // TODO: misdirection, etc + Misdirection, // temporary misdirection - if current time is greater than activation, use special pathfinding codepath } public static readonly ArenaBounds DefaultBounds = new ArenaBoundsSquare(30); @@ -73,6 +73,9 @@ public enum SpecialMode // closest special movement/targeting/action mode, if any public (SpecialMode mode, DateTime activation) ImminentSpecialMode; + // for misdirection: if forced movement is set, make real direction be within this angle + public Angle MisdirectionThreshold; + // predicted incoming damage (raidwides, tankbusters, etc.) // AI will attempt to shield & mitigate public List<(BitMask players, DateTime activation)> PredictedDamage = []; @@ -106,6 +109,7 @@ public void Clear() RecommendedPositional = default; ForbiddenDirections.Clear(); ImminentSpecialMode = default; + MisdirectionThreshold = 15.Degrees(); PredictedDamage.Clear(); MaxCastTimeEstimate = float.MaxValue; ActionsToExecute.Clear(); diff --git a/BossMod/BossModule/AIHintsVisualizer.cs b/BossMod/BossModule/AIHintsVisualizer.cs index 4119437872..cf8328471a 100644 --- a/BossMod/BossModule/AIHintsVisualizer.cs +++ b/BossMod/BossModule/AIHintsVisualizer.cs @@ -17,7 +17,7 @@ public void Draw(UITree tree) tree.LeafNodes(hints.PotentialTargets, e => $"[{e.Priority}] {e.Actor} (str={e.AttackStrength:f2}), dist={(e.Actor.Position - player.Position).Length():f2}, tank={e.ShouldBeTanked}/{e.PreferProvoking}/{e.DesiredPosition}/{e.DesiredRotation}"); } tree.LeafNode($"Forced target: {hints.ForcedTarget}"); - tree.LeafNode($"Forced movement: {hints.ForcedMovement}"); + tree.LeafNode($"Forced movement: {hints.ForcedMovement} (misdirection threshold={hints.MisdirectionThreshold})"); tree.LeafNode($"Special movement: {hints.ImminentSpecialMode.mode} in {Math.Max(0, (hints.ImminentSpecialMode.activation - ws.CurrentTime).TotalSeconds):f3}s"); foreach (var _1 in tree.Node("Forbidden zones", hints.ForbiddenZones.Count == 0)) { diff --git a/BossMod/Components/StackSpread.cs b/BossMod/Components/StackSpread.cs index 5f729db93d..3bfb868ea4 100644 --- a/BossMod/Components/StackSpread.cs +++ b/BossMod/Components/StackSpread.cs @@ -92,6 +92,12 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme foreach (var spreadFrom in ActiveSpreads.Where(s => s.Target != actor)) hints.AddForbiddenZone(ShapeDistance.Circle(spreadFrom.Target.Position, spreadFrom.Radius + ExtraAISpreadThreshold), spreadFrom.Activation); + // if player has spread himself, stay away from everyone else + var actorSpread = Spreads.FirstOrDefault(s => s.Target == actor); + if (actorSpread.Target != null) + foreach (var p in Raid.WithoutSlot().Exclude(actor)) + hints.AddForbiddenZone(ShapeDistance.Circle(p.Position, actorSpread.Radius), actorSpread.Activation); + foreach (var avoid in ActiveStacks.Where(s => s.Target != actor && s.ForbiddenPlayers[slot])) hints.AddForbiddenZone(ShapeDistance.Circle(avoid.Target.Position, avoid.Radius), avoid.Activation); @@ -105,7 +111,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (closest != null) hints.AddForbiddenZone(ShapeDistance.InvertedCircle(closest.Position, actorStack.Radius * 0.5f), actorStack.Activation); } - else if (!IsSpreadTarget(actor)) + else if (actorSpread.Target == null) { // TODO: handle multi stacks better... var closestStack = ActiveStacks.Where(s => !s.ForbiddenPlayers[slot]).MinBy(s => (s.Target.Position - actor.Position).LengthSq()); diff --git a/BossMod/Debug/DebugInput.cs b/BossMod/Debug/DebugInput.cs index 90137443dc..7ade1f1192 100644 --- a/BossMod/Debug/DebugInput.cs +++ b/BossMod/Debug/DebugInput.cs @@ -150,6 +150,7 @@ public void Draw() _prevPos = curPos; _prevSpeed = speedAbs; ImGui.TextUnformatted($"Speed={speedAbs:f3}, SpeedH={speed.XZ().Length():f3}, SpeedV={speed.Y:f3}, Accel={accel:f3}, Azimuth={Angle.FromDirection(new(speed.XZ()))}, Altitude={Angle.FromDirection(new(speed.Y, speed.XZ().Length()))}"); + ImGui.TextUnformatted($"MO: desired={_move.DesiredDirection}, user={_move.UserMove}, actual={_move.ActualMove}"); //Service.Log($"Speed: {speedAbs:f3}, accel: {accel:f3}"); ImGui.SliderFloat("Move direction", ref _moveDir, -180, 180); diff --git a/BossMod/Framework/MovementOverride.cs b/BossMod/Framework/MovementOverride.cs index 0c00dc34ec..7e87913ccf 100644 --- a/BossMod/Framework/MovementOverride.cs +++ b/BossMod/Framework/MovementOverride.cs @@ -23,19 +23,18 @@ public unsafe struct PlayerMoveControllerFlyInput public sealed unsafe class MovementOverride : IDisposable { public Vector3? DesiredDirection; + public Angle MisdirectionThreshold; - private float UserMoveLeft; - private float UserMoveUp; - private float ActualMoveLeft; - private float ActualMoveUp; + public WDir UserMove { get; private set; } // unfiltered movement direction, as read from input + public WDir ActualMove { get; private set; } // actual movement direction, as of last input read private readonly ActionTweaksConfig _tweaksConfig = Service.Config.Get(); private bool _movementBlocked; private bool? _forcedControlState; private bool _legacyMode; - public bool IsMoving() => ActualMoveLeft != 0 || ActualMoveUp != 0; - public bool IsMoveRequested() => UserMoveLeft != 0 || UserMoveUp != 0; + public bool IsMoving() => ActualMove != default; + public bool IsMoveRequested() => UserMove != default; public bool IsForceUnblocked() => _tweaksConfig.MoveEscapeHatch switch { @@ -89,10 +88,6 @@ public void Dispose() { Service.GameConfig.UiControlChanged -= OnConfigChanged; _movementBlocked = false; - UserMoveLeft = 0; - UserMoveUp = 0; - ActualMoveLeft = 0; - ActualMoveUp = 0; _mcIsInputActiveHook.Dispose(); _rmiWalkHook.Dispose(); _rmiFlyHook.Dispose(); @@ -102,54 +97,54 @@ private void RMIWalkDetour(void* self, float* sumLeft, float* sumForward, float* { _forcedControlState = null; _rmiWalkHook.Original(self, sumLeft, sumForward, sumTurnLeft, haveBackwardOrStrafe, a6, bAdditiveUnk); - UserMoveLeft = *sumLeft; - UserMoveUp = *sumForward; - // TODO: this allows AI mode to move even if movement is "blocked", is this the right behavior? AI mode should try to avoid moving while casting anyway... - if (MovementBlocked) + // TODO: we really need to introduce some extra checks that PlayerMoveController::readInput does - sometimes it skips reading input, and returning something non-zero breaks stuff... + var movementAllowed = bAdditiveUnk == 0 && _rmiWalkIsInputEnabled1(self) && _rmiWalkIsInputEnabled2(self); + var misdirectionMode = PlayerHasMisdirection(); + if (!movementAllowed && misdirectionMode) { - *sumLeft = 0; - *sumForward = 0; + // in misdirection mode, when we are already moving, the 'original' call will not actually sample input and just return immediately + // we actually want to know the direction, in case user changes input mid movement - so force sample raw input + float realTurn = 0; + byte realStrafe = 0, realUnk = 0; + _rmiWalkHook.Original(self, sumLeft, sumForward, &realTurn, &realStrafe, &realUnk, 1); } - // TODO: we really need to introduce some extra checks that PlayerMoveController::readInput does - sometimes it skips reading input, and returning something non-zero breaks stuff... - var movementAllowed = bAdditiveUnk == 0 && _rmiWalkIsInputEnabled1(self) && _rmiWalkIsInputEnabled2(self); //&& !_movementBlocked - if (movementAllowed && *sumLeft == 0 && *sumForward == 0 && DirectionToDestination(false) is var relDir && relDir != null) + // at this point, UserMove contains true user input + UserMove = new(*sumLeft, *sumForward); + + // apply movement block logic + // note: currently movement block is ignored in misdirection mode + // the assumption is that, with misdirection active, it's not safe to block movement just because player is casting or doing something else (as arrow will rotate away) + ActualMove = !MovementBlocked || misdirectionMode ? UserMove : default; + + // movement override logic + // note: currently we follow desired direction, only if user does not have any input _or_ if manual movement is blocked + // this allows AI mode to move even if movement is blocked (TODO: is this the right behavior? AI mode should try to avoid moving while casting anyway...) + if ((movementAllowed || misdirectionMode) && ActualMove == default && DirectionToDestination(false) is var relDir && relDir != null) { - var dir = relDir.Value.h.ToDirection(); - *sumLeft = dir.X; - *sumForward = dir.Z; + ActualMove = relDir.Value.h.ToDirection(); } - if (_tweaksConfig.MisdirectionThreshold < 180 && PlayerHasMisdirection()) + // misdirection override logic + if (misdirectionMode) { - if (!movementAllowed) - { - // we are already moving, see whether we need to force stop it - // unfortunately, the base implementation would not sample the input if movement is disabled - force it - float realLeft = 0, realForward = 0, realTurn = 0; - byte realStrafe = 0, realUnk = 0; - if (!MovementBlocked) - _rmiWalkHook.Original(self, &realLeft, &realForward, &realTurn, &realStrafe, &realUnk, 1); - var desiredRelDir = realLeft != 0 || realForward != 0 ? Angle.FromDirection(new(realLeft, realForward)) : DirectionToDestination(false)?.h; - _forcedControlState = desiredRelDir != null && (desiredRelDir.Value + ForwardMovementDirection() - ForcedMovementDirection->Radians()).Normalized().Abs().Deg <= _tweaksConfig.MisdirectionThreshold; - } - else if (*sumLeft != 0 || *sumForward != 0) + var thresholdDeg = UserMove != default ? _tweaksConfig.MisdirectionThreshold : MisdirectionThreshold.Deg; + if (thresholdDeg < 180) { - var currentDir = Angle.FromDirection(new(*sumLeft, *sumForward)) + ForwardMovementDirection(); - var dirDelta = currentDir - ForcedMovementDirection->Radians(); - _forcedControlState = dirDelta.Normalized().Abs().Deg <= _tweaksConfig.MisdirectionThreshold; - if (!_forcedControlState.Value) - { - // forbid movement for now - *sumLeft = *sumForward = 0; - } + // note: if we are already moving, it doesn't matter what we do here, only whether 'is input active' function returns true or false + _forcedControlState = ActualMove != default && (Angle.FromDirection(ActualMove) + ForwardMovementDirection() - ForcedMovementDirection->Radians()).Normalized().Abs().Deg <= thresholdDeg; } - // else: movement is allowed (so we're not already moving), but we don't want to move anywhere, do nothing } - ActualMoveLeft = *sumLeft; - ActualMoveUp = *sumForward; + // finally, update output + var output = !misdirectionMode ? ActualMove // standard mode - just return desired movement + : !movementAllowed ? default // misdirection and already moving - always return 0, as game does + : _forcedControlState == null ? ActualMove // misdirection mode, but we're not trying to help user + : _forcedControlState.Value ? ActualMove // misdirection mode, not moving yet, but want to start - can return anything really + : default; // misdirection mode, not moving yet and don't want to + *sumLeft = output.X; + *sumForward = output.Z; } private void RMIFlyDetour(void* self, PlayerMoveControllerFlyInput* result) diff --git a/BossMod/Framework/Plugin.cs b/BossMod/Framework/Plugin.cs index 89a0013dd5..182ae116a0 100644 --- a/BossMod/Framework/Plugin.cs +++ b/BossMod/Framework/Plugin.cs @@ -290,6 +290,7 @@ private unsafe bool QuestUnlocked(uint link) private unsafe void ExecuteHints() { _movementOverride.DesiredDirection = _hints.ForcedMovement; + _movementOverride.MisdirectionThreshold = _hints.MisdirectionThreshold; // update forced target, if needed (TODO: move outside maybe?) if (_hints.ForcedTarget != null) { diff --git a/BossMod/Modules/Dawntrail/Dungeon/D09Yuweyawata/D092OverseerKanilokka.cs b/BossMod/Modules/Dawntrail/Dungeon/D09Yuweyawata/D092OverseerKanilokka.cs index 629bd020b3..838d4d91d2 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D09Yuweyawata/D092OverseerKanilokka.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D09Yuweyawata/D092OverseerKanilokka.cs @@ -113,7 +113,16 @@ public TelltaleTears(BossModule module) : base(module, ActionID.MakeSpell(AID.Te } } -class Necrohazard(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Necrohazard), new AOEShapeCircle(18)); // TODO: verify falloff +class Necrohazard(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Necrohazard), new AOEShapeCircle(18)) // TODO: verify falloff +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + if (Casters.Count > 0) + hints.AddSpecialMode(AIHints.SpecialMode.Misdirection, default); + } +} + class Bloodburst(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Bloodburst)); class SoulDouse(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.SoulDouse), 6, 4); @@ -144,7 +153,7 @@ public D092OverseerKanilokkaStates(BossModule module) : base(module) public static readonly ArenaBoundsCustom ComplexBounds1 = BuildComplexBounds1(); public static readonly ArenaBoundsCustom ComplexBounds2 = BuildComplexBounds2(); - private static ArenaBoundsCustom BuildCircularBounds(float radius) => new(InitialBounds.Radius, InitialBounds.Clipper.Simplify(new(CurveApprox.Circle(radius, 0.1f)))); + private static ArenaBoundsCustom BuildCircularBounds(float radius) => new(InitialBounds.Radius, InitialBounds.Clipper.Simplify(new(CurveApprox.Circle(radius, 0.05f)))); private static ArenaBoundsCustom BuildComplexBounds1() { @@ -157,7 +166,7 @@ private static ArenaBoundsCustom BuildComplexBounds1() new(-4.9f, +0.2f), new(-7.1f, -0.5f), new(-9.3f, -3.9f), new(-11.4f, -3.1f), new(-14.2f, -1.0f), new(-17.5f, -1.5f), new(-19.6f, -3.9f), new(-25, +25)]); remove.AddContour([new(-18.8f, -6.6f), new(-17.4f, -4.5f), new(-15.1f, -4.1f), new(-12.7f, -6.9f), new(-8.0f, -7.0f), new(-5.4f, -4.0f), new(-3.2f, -3.7f), new(-2.0f, -4.4f), new(0, -6.8f), new(-3.0f, -11.6f), new(-2.3f, -14.9f), new(+1.2f, -17.9f), new(+4.0f, -19.6f), new(-25, -25)]); - return new(InitialBounds.Radius, InitialBounds.Clipper.Difference(new(CurveApprox.Circle(19.5f, 0.1f)), remove)); + return new(InitialBounds.Radius, InitialBounds.Clipper.Difference(new(CurveApprox.Circle(19.5f, 0.05f)), remove)); } private static ArenaBoundsCustom BuildComplexBounds2() @@ -172,6 +181,6 @@ private static ArenaBoundsCustom BuildComplexBounds2() remove.AddContour([new(-25, +8.7f), new(-17.9f, +8.7f), new(-15.9f, +7.6f), new(-13.2f, +8.9f), new(-12.0f, +8.1f), new(-12.1f, +6.8f), new(-12.8f, +5.6f), new(-12.3f, +3.6f), new(-11.3f, +1.6f), new(-10.4f, +0.8f), new(-8.8f, +0.5f), new(-5.3f, +0.6f), new(-5.0f, +0.3f), new(-2.7f, -4.1f), new(-2.6f, -6.4f), new(-5.1f, -8.8f), new(-5.1f, -10.0f), new(-4.8f, -11.5f), new(-3.9f, -12.9f), new(-1.3f, -13.7f), new(0, -13.8f), new(+1.1f, -14.1f), new(+1.5f, -14.9f), new(+1.7f, -16.3f), new(-1.1f, -18.1f), new(-1.4f, -19.9f), new(-1.4f, -25), new(-25, -25)]); - return new(InitialBounds.Radius, InitialBounds.Clipper.Union(new(InitialBounds.Clipper.Difference(new(CurveApprox.Circle(19.5f, 0.1f)), remove)), new(CurveApprox.Circle(5, 0.1f)))); + return new(InitialBounds.Radius, InitialBounds.Clipper.Union(new(InitialBounds.Clipper.Difference(new(CurveApprox.Circle(19.5f, 0.05f)), remove)), new(CurveApprox.Circle(5, 0.05f)))); } } diff --git a/BossMod/Modules/Dawntrail/Dungeon/D09Yuweyawata/D093Lunipyati.cs b/BossMod/Modules/Dawntrail/Dungeon/D09Yuweyawata/D093Lunipyati.cs index 4fb2e8448a..173d408ac6 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D09Yuweyawata/D093Lunipyati.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D09Yuweyawata/D093Lunipyati.cs @@ -166,7 +166,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) if ((AID)spell.Action.ID == AID.LeapingEarthSpiral) { NumCasts = 0; - _direction = spell.Rotation.ToDirection(); + _direction = (-spell.Rotation).ToDirection(); _firstActivation = WorldState.FutureTime(4.5f); } } @@ -208,7 +208,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) if (spell.Action == WatchedAction && _aoes.Count == 0) { var toCaster = caster.Position - Module.Center; - var cw = toCaster.OrthoL().Dot(spell.Rotation.ToDirection()) > 0; + var cw = toCaster.OrthoL().Dot(spell.Rotation.ToDirection()) < 0; var delta = (cw ? -22.5f : 22.5f).Degrees().ToDirection(); for (int i = 0; i < 15; ++i) { @@ -261,8 +261,8 @@ public D093LunipyatiStates(BossModule module) : base(module) private static ArenaBoundsCustom BuildHoleBounds() { - var poly = new RelPolygonWithHoles([.. CurveApprox.Circle(15, 0.1f)]); - poly.AddHole(CurveApprox.Circle(11, 0.1f)); + var poly = new RelPolygonWithHoles([.. CurveApprox.Circle(15, 0.05f)]); + poly.AddHole(CurveApprox.Circle(11, 0.05f)); return new(NormalBounds.Radius, new([poly])); } } diff --git a/BossMod/Modules/Endwalker/Alliance/A31Thaliak/Hieroglyphika.cs b/BossMod/Modules/Endwalker/Alliance/A31Thaliak/Hieroglyphika.cs index cb8fa8decb..9225150860 100644 --- a/BossMod/Modules/Endwalker/Alliance/A31Thaliak/Hieroglyphika.cs +++ b/BossMod/Modules/Endwalker/Alliance/A31Thaliak/Hieroglyphika.cs @@ -14,7 +14,7 @@ class Hieroglyphika(BossModule module) : Components.GenericAOEs(module, ActionID public readonly List AOEs = []; private static readonly AOEShapeRect _shape = new(6, 6, 6); - private static readonly WDir[] _canonicalSafespots = [new(-6, 18), new(18, -18)]; + private static readonly WDir[] _canonicalSafespots = [new(6, -18), new(-18, 18)]; public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs; @@ -31,7 +31,7 @@ public override void OnEventEnvControl(byte index, uint state) WDir dir = index switch { - 0x17 => new(1, 0), + 0x17 => new(-1, 0), 0x4A => new(0, 1), _ => default }; diff --git a/BossMod/Modules/Endwalker/Alliance/A34Eulogia/Hieroglyphika.cs b/BossMod/Modules/Endwalker/Alliance/A34Eulogia/Hieroglyphika.cs index 75c6052c06..2ebdecf9b7 100644 --- a/BossMod/Modules/Endwalker/Alliance/A34Eulogia/Hieroglyphika.cs +++ b/BossMod/Modules/Endwalker/Alliance/A34Eulogia/Hieroglyphika.cs @@ -22,8 +22,8 @@ public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) { WDir dir = (IconID)iconID switch { - IconID.HieroglyphikaCW => new(1, 0), - IconID.HieroglyphikaCCW => new(-1, 0), + IconID.HieroglyphikaCW => new(-1, 0), + IconID.HieroglyphikaCCW => new(1, 0), _ => default }; if (dir == default) diff --git a/BossMod/Pathfinding/MapVisualizer.cs b/BossMod/Pathfinding/MapVisualizer.cs index 3639b88c1e..4cd223c83f 100644 --- a/BossMod/Pathfinding/MapVisualizer.cs +++ b/BossMod/Pathfinding/MapVisualizer.cs @@ -284,17 +284,19 @@ private void DrawWaypoints(int startingIndex) var nextIndex = startingNode.ParentIndex; var (x1, y1) = Map.IndexToGrid(startingIndex); var (x2, y2) = Map.IndexToGrid(nextIndex); + var futureCenter = _pathfind.CellCenter(startingIndex); while (x1 != x2 || y1 != y2) { ref var node = ref _pathfind.NodeByIndex(startingIndex); - var off1 = node.EnterOffset; - using var n = ImRaii.TreeNode($"Waypoint: {x1}x{y1} ({Map.GridToWorld(x1, y1, off1.X + 0.5f, off1.Y + 0.5f)}), minG={node.PathMinG}, leeway={node.PathLeeway}", ImGuiTreeNodeFlags.Leaf); - x1 = x2; - y1 = y2; + var curCenter = _pathfind.CellCenter(startingIndex); + var prevCenter = _pathfind.CellCenter(node.ParentIndex); + var turn = (curCenter - prevCenter).OrthoL().Dot(futureCenter - curCenter); + using var n = ImRaii.TreeNode($"Waypoint: {x1}x{y1} ({Map.GridToWorld(x1, y1, node.EnterOffset.X + 0.5f, node.EnterOffset.Y + 0.5f)}), minG={node.PathMinG}, leeway={node.PathLeeway}, turn={turn}", ImGuiTreeNodeFlags.Leaf); startingIndex = nextIndex; - nextIndex = node.ParentIndex; + nextIndex = _pathfind.NodeByIndex(node.ParentIndex).ParentIndex; (x1, y1) = (x2, y2); (x2, y2) = Map.IndexToGrid(nextIndex); + futureCenter = curCenter; } } diff --git a/BossMod/Pathfinding/NavigationDecision.cs b/BossMod/Pathfinding/NavigationDecision.cs index ac8f9ca711..504267c548 100644 --- a/BossMod/Pathfinding/NavigationDecision.cs +++ b/BossMod/Pathfinding/NavigationDecision.cs @@ -17,6 +17,7 @@ public class Context } public WPos? Destination; + public float NextTurn; // > 0 if we turn left after reaching first waypoint, < 0 if we turn right, 0 otherwise (no more waypoints) public float LeewaySeconds; // can be used for finishing casts / slidecasting etc. public float TimeToGoal; public Map? Map; @@ -37,7 +38,8 @@ public static NavigationDecision Build(Context ctx, WorldState ws, AIHints hints ctx.ThetaStar.Start(ctx.Map, player.Position, 1.0f / playerSpeed); var bestNodeIndex = ctx.ThetaStar.Execute(); ref var bestNode = ref ctx.ThetaStar.NodeByIndex(bestNodeIndex); - return new() { Destination = GetFirstWaypoint(ctx.ThetaStar, ctx.Map, bestNodeIndex), LeewaySeconds = bestNode.PathLeeway, TimeToGoal = bestNode.GScore, Map = ctx.Map }; + var (destination, turn) = GetFirstWaypoint(ctx.ThetaStar, ctx.Map, bestNodeIndex, player.Position); + return new() { Destination = destination, NextTurn = turn, LeewaySeconds = bestNode.PathLeeway, TimeToGoal = bestNode.GScore, Map = ctx.Map }; } public static void RasterizeForbiddenZones(Map map, List<(Func shapeDistance, DateTime activation)> zones, DateTime current, ref float[] scratch) @@ -183,17 +185,23 @@ private static float CalculateMaxG(Span<(Func shapeDistance, float return float.MaxValue; } - private static WPos? GetFirstWaypoint(ThetaStar pf, Map map, int cell) + private static (WPos? destination, float turn) GetFirstWaypoint(ThetaStar pf, Map map, int cell, WPos startingPos) { ref var startingNode = ref pf.NodeByIndex(cell); if (startingNode.GScore == 0 && startingNode.PathMinG == float.MaxValue) - return null; // we're already in safe zone + return (null, 0); // we're already in safe zone + var nextCell = cell; do { ref var node = ref pf.NodeByIndex(cell); if (pf.NodeByIndex(node.ParentIndex).GScore == 0) - return pf.CellCenter(cell); + { + var dest = pf.CellCenter(cell); + var next = pf.CellCenter(nextCell); + return (dest, (dest - startingPos).OrthoL().Dot(next - dest)); + } + nextCell = cell; cell = node.ParentIndex; } while (true); diff --git a/BossMod/Util/WPosDir.cs b/BossMod/Util/WPosDir.cs index 87ca97442f..3d2ebb0ea7 100644 --- a/BossMod/Util/WPosDir.cs +++ b/BossMod/Util/WPosDir.cs @@ -23,7 +23,7 @@ public WDir(Vector2 v) : this(v.X, v.Y) { } public readonly float Dot(WDir a) => X * a.X + Z * a.Z; public static float Cross(WDir a, WDir b) => a.X * b.Z - a.Z * b.X; public readonly float Cross(WDir b) => Cross(this, b); - public readonly WDir Rotate(WDir dir) => new(Dot(dir.OrthoL()), Dot(dir)); + public readonly WDir Rotate(WDir dir) => new(X * dir.Z + Z * dir.X, Z * dir.Z - X * dir.X); public readonly WDir Rotate(Angle dir) => Rotate(dir.ToDirection()); public readonly float LengthSq() => X * X + Z * Z; public readonly float Length() => MathF.Sqrt(LengthSq()); diff --git a/TODO b/TODO index 12c3be3f5f..c0427973b9 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,5 @@ immediate plans -- new dungeon +- nechuciho - more omen statuses - freeze - rotation modules in preset/plan to be ordered (incl ui) - get rid of legacyxxx @@ -43,7 +43,6 @@ general: - zone modules: replay visualization - zone modules: module info ui - refactor ipc/dtr -- nechuciho - seen incorrect rotations... - questbattles - collisions for pathfinding -- embedded mode @@ -54,6 +53,7 @@ boss modules: - fail log - boss module config presets/profiles - jeuno +-- review recent logs -- a11: --- mechanic repeats --- ai hints for spikes+uppercut