Skip to content

Commit

Permalink
A bunch of fixes.
Browse files Browse the repository at this point in the history
- better misdirection for AI movement (always active, and adjust cone to avoid steering into walls)
- spread target now tries to spread from everyone else
- reworked movement override logic a bit again
- more detailed custom bounds
- fixed rotation direction
  • Loading branch information
awgil committed Nov 25, 2024
1 parent 1b3eb7c commit 5524fac
Show file tree
Hide file tree
Showing 15 changed files with 115 additions and 75 deletions.
18 changes: 16 additions & 2 deletions BossMod/AI/AIBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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)
{
Expand All @@ -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();
Expand Down
6 changes: 5 additions & 1 deletion BossMod/BossModule/AIHints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -106,6 +109,7 @@ public void Clear()
RecommendedPositional = default;
ForbiddenDirections.Clear();
ImminentSpecialMode = default;
MisdirectionThreshold = 15.Degrees();
PredictedDamage.Clear();
MaxCastTimeEstimate = float.MaxValue;
ActionsToExecute.Clear();
Expand Down
2 changes: 1 addition & 1 deletion BossMod/BossModule/AIHintsVisualizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down
8 changes: 7 additions & 1 deletion BossMod/Components/StackSpread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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());
Expand Down
1 change: 1 addition & 0 deletions BossMod/Debug/DebugInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
87 changes: 41 additions & 46 deletions BossMod/Framework/MovementOverride.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ActionTweaksConfig>();
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
{
Expand Down Expand Up @@ -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();
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions BossMod/Framework/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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()
{
Expand All @@ -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()
Expand All @@ -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))));
}
}
Loading

0 comments on commit 5524fac

Please sign in to comment.