Skip to content

Commit

Permalink
Merge pull request #527 from FFXIV-CombatReborn/mergeWIP
Browse files Browse the repository at this point in the history
daivadipa refactor, ai movement delay setting
  • Loading branch information
CarnifexOptimus authored Dec 30, 2024
2 parents 92dd052 + 7809db9 commit 63b8961
Show file tree
Hide file tree
Showing 14 changed files with 181 additions and 188 deletions.
19 changes: 15 additions & 4 deletions BossMod/AI/AIBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ sealed class AIBehaviour(AIController ctrl, RotationModuleManager autorot, Prese
private WPos _masterPrevPos;
private WPos _masterMovementStart;
private DateTime _masterLastMoved;
private DateTime _navStartTime; // if current time is < this, navigation won't start

public void Dispose()
{
Expand All @@ -40,6 +41,7 @@ public async Task Execute(Actor player, Actor master)
var pyreticImminent = autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && autorot.Hints.ImminentSpecialMode.activation <= WorldState.FutureTime(1);
var misdirectionMode = autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Misdirection && autorot.Hints.ImminentSpecialMode.activation <= WorldState.CurrentTime;
var forbidActions = _config.ForbidActions || _afkMode || gazeImminent || pyreticImminent;
var hadNavi = _naviDecision.Destination != null;

Targeting target = new();
if (!forbidActions && (AIPreset != null || autorot.Preset != null))
Expand Down Expand Up @@ -76,6 +78,10 @@ public async Task Execute(Actor player, Actor master)
var masterIsMoving = TrackMasterMovement(master);
var moveWithMaster = masterIsMoving && (master == player || _followMaster);
ForceMovementIn = moveWithMaster || gazeImminent || pyreticImminent ? 0 : _naviDecision.LeewaySeconds;

if (_config.MoveDelay != 0 && !hadNavi && _naviDecision.Destination != null)
_navStartTime = WorldState.FutureTime(_config.MoveDelay);

if (!forbidActions)
{
autorot.Preset = target.Target != null ? AIPreset : null;
Expand Down Expand Up @@ -131,7 +137,7 @@ private void AdjustTargetPositional(Actor player, ref Targeting targeting)

// if target-of-target is player, don't try flanking, it's probably impossible... - unless target is currently casting (TODO: reconsider?)
// skip if targeting a dummy, they don't rotate
if (targeting.Target.Actor.TargetID == player.InstanceID && targeting.Target.Actor.CastInfo == null && targeting.Target.Actor.OID != 0x385)
if (targeting.Target.Actor.TargetID == player.InstanceID && targeting.Target.Actor.CastInfo == null && targeting.Target.Actor.NameID != 541)
targeting.PreferredPosition = Positional.Any;
}

Expand All @@ -150,7 +156,10 @@ private void AdjustTargetPositional(Actor player, ref Targeting targeting)
}
if (_config.FollowTarget && target != null)
{
var decision = await Task.Run(() => NavigationDecision.Build(_naviCtx, WorldState, autorot.Hints, player, target.Position, target.HitboxRadius + (_config.DesiredPositional != Positional.Any ? 2.6f : _config.MaxDistanceToTarget), target.Rotation, target != player ? _config.DesiredPositional : Positional.Any)).ConfigureAwait(true);
var positional = _config.DesiredPositional;
if (positional is not Positional.Any and not Positional.Front && target.TargetID == player.InstanceID && target.CastInfo == null && target.NameID != 541) // if player is target, rear/flank is usually impossible unless target is casting
positional = Positional.Any;
var decision = await Task.Run(() => NavigationDecision.Build(_naviCtx, WorldState, autorot.Hints, player, target.Position, target.HitboxRadius + (positional != Positional.Any ? 2.6f : _config.MaxDistanceToTarget), target.Rotation, positional)).ConfigureAwait(true);
return (decision, targeting);
}
}
Expand Down Expand Up @@ -243,7 +252,8 @@ private void UpdateMovement(Actor player, Actor master, Targeting target, bool g
{
var toDest = _naviDecision.Destination != null ? _naviDecision.Destination.Value - player.Position : new();
var distSq = toDest.LengthSq();
ctrl.NaviTargetPos = _naviDecision.Destination;

ctrl.NaviTargetPos = _config.MoveDelay == 0 ? _naviDecision.Destination : WorldState.CurrentTime >= _navStartTime ? _naviDecision.Destination : null;
ctrl.NaviTargetVertical = master != player ? master.PosRot.Y : null;
ctrl.AllowInterruptingCastByMovement = player.CastInfo != null && _naviDecision.LeewaySeconds <= player.CastInfo.RemainingTime - 0.5;
ctrl.ForceCancelCast = false;
Expand Down Expand Up @@ -290,7 +300,8 @@ public void DrawDebug()

var player = autorot.WorldState.Party.Player();
var dist = _naviDecision.Destination != null && player != null ? (_naviDecision.Destination.Value - player.Position).Length() : 0;
ImGui.TextUnformatted($"Max-cast={Math.Min(ForceMovementIn, 1000):f3}, afk={_afkMode}, follow={_followMaster}, algo={_naviDecision.DecisionType} {_naviDecision.Destination} (d={dist:f3}), master standing for {Math.Clamp((WorldState.CurrentTime - _masterLastMoved).TotalSeconds, 0, 1000):f1}");
var algo = WorldState.CurrentTime <= _navStartTime ? _naviDecision.DecisionType = NavigationDecision.Decision.Waiting : _naviDecision.DecisionType;
ImGui.TextUnformatted($"Max-cast={Math.Min(ForceMovementIn, 1000):f3}, afk={_afkMode}, follow={_followMaster}, algo={algo} {_naviDecision.Destination} (d={dist:f3}), master standing for {Math.Clamp((WorldState.CurrentTime - _masterLastMoved).TotalSeconds, 0, 1000):f1}");
}

private bool TargetIsForbidden(ulong actorId) => autorot.Hints.ForbiddenTargets.Any(e => e.Actor.InstanceID == actorId);
Expand Down
3 changes: 3 additions & 0 deletions BossMod/AI/AIConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,8 @@ sealed class AIConfig : ConfigNode
[PropertyDisplay("Allow AI to be out of pathfinding map bounds")]
public bool AllowAIToBeOutsideBounds = false;

[PropertyDisplay("Movement decision delay", tooltip: "Only change this at your own risk and keep this value low! Too high and it won't move in time for some mechanics. Make sure to readjust the value for different content.")]
public float MoveDelay = 0;

public string? AIAutorotPresetName;
}
15 changes: 15 additions & 0 deletions BossMod/AI/AIManagementWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ private Task UIAsync()
_config.Modified.Fire();
}
}

ImGui.Text("Movement decision delay");
ImGui.SameLine();
ImGui.SetNextItemWidth(100);
var movementDelayStr = _config.MoveDelay.ToString(CultureInfo.InvariantCulture);
if (ImGui.InputText("##MovementDelay", ref movementDelayStr, 64))
{
movementDelayStr = movementDelayStr.Replace(',', '.');
if (float.TryParse(movementDelayStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var delay))
{
_config.MoveDelay = delay;
_config.Modified.Fire();
}
}
ImGui.SameLine();
ImGui.Text("Autorotation AI preset");
ImGui.SameLine();
ImGui.SetNextItemWidth(250);
Expand Down
26 changes: 15 additions & 11 deletions BossMod/Components/GenericAOEs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ public override void DrawArenaBackground(int pcSlot, Actor pc)
}

// self-targeted aoe that happens at the end of the cast
public class SelfTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, int maxCasts = int.MaxValue, uint color = 0) : GenericAOEs(module, aid)
public class SelfTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, float riskyAfterSeconds = 0, int maxCasts = int.MaxValue, uint color = 0) : GenericAOEs(module, aid)
{
public readonly AOEShape Shape = shape;
public int MaxCasts = maxCasts; // used for staggered aoes, when showing all active would be pointless
public uint Color = color;
public bool Risky = true; // can be customized if needed
public float RiskyAfterSeconds = riskyAfterSeconds; // can be used to delay risky status of AOEs, so AI waits longer to dodge, if 0 it will just use the bool Risky
public readonly List<Actor> Casters = [];

public IEnumerable<Actor> ActiveCasters => Casters.Take(MaxCasts);
Expand All @@ -53,7 +54,7 @@ public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
for (var i = 0; i < min; ++i)
{
var caster = Casters[i];
AOEInstance aoeInstance = new(Shape, caster.Position, caster.CastInfo!.Rotation, Module.CastFinishAt(caster.CastInfo));
AOEInstance aoeInstance = new(Shape, caster.Position, caster.CastInfo!.Rotation, Module.CastFinishAt(caster.CastInfo), Color, RiskyAfterSeconds == 0 ? Risky : caster.CastInfo.ElapsedTime > RiskyAfterSeconds);
aoes.Add(aoeInstance);
}
return aoes;
Expand Down Expand Up @@ -97,14 +98,15 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell)
}

// location-targeted circle aoe that happens at the end of the cast
public class LocationTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, int maxCasts = int.MaxValue, bool targetIsLocation = false) : GenericAOEs(module, aid)
public class LocationTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, float riskyWithSecondsLeft = 0, int maxCasts = int.MaxValue, bool targetIsLocation = false) : GenericAOEs(module, aid)
{
public LocationTargetedAOEs(BossModule module, ActionID aid, float radius, int maxCasts = int.MaxValue, bool targetIsLocation = false) : this(module, aid, new AOEShapeCircle(radius), maxCasts, targetIsLocation) { }
public LocationTargetedAOEs(BossModule module, ActionID aid, float radius, float riskyWithSecondsLeft = 0, int maxCasts = int.MaxValue, bool targetIsLocation = false) : this(module, aid, new AOEShapeCircle(radius), riskyWithSecondsLeft, maxCasts, targetIsLocation) { }
public readonly AOEShape Shape = shape;
public readonly int MaxCasts = maxCasts; // used for staggered aoes, when showing all active would be pointless
public uint Color; // can be customized if needed
public bool Risky = true; // can be customized if needed
public readonly bool TargetIsLocation = targetIsLocation; // can be customized if needed
public float RiskyWithSecondsLeft = riskyWithSecondsLeft; // can be used to delay risky status of AOEs, so AI waits longer to dodge, if 0 it will just use the bool Risky

public readonly List<AOEInstance> Casters = [];
public IEnumerable<AOEInstance> ActiveCasters => Casters.Take(MaxCasts);
Expand All @@ -114,17 +116,18 @@ public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
var count = Casters.Count;
if (count == 0)
yield break;
var time = WorldState.CurrentTime;
for (var i = 0; i < (count > MaxCasts ? MaxCasts : count); ++i)
yield return Casters[i];
{
var caster = Casters[i];
yield return RiskyWithSecondsLeft == 0 ? caster : caster with { Risky = caster.Activation.AddSeconds(-RiskyWithSecondsLeft) <= time };
}
}

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if (spell.Action == WatchedAction)
{
var loc = TargetIsLocation ? WorldState.Actors.Find(caster.CastInfo!.TargetID)?.Position : spell.LocXZ;
Casters.Add(new(Shape, loc ?? default, spell.Rotation, Module.CastFinishAt(spell), Color, Risky));
}
Casters.Add(new(Shape, (TargetIsLocation ? WorldState.Actors.Find(caster.CastInfo!.TargetID)?.Position : spell.LocXZ) ?? default, spell.Rotation, Module.CastFinishAt(spell), Color, Risky));
}

public override void OnCastFinished(Actor caster, ActorCastInfo spell)
Expand All @@ -135,9 +138,10 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell)
}

// 'charge at location' aoes that happen at the end of the cast
public class ChargeAOEs(BossModule module, ActionID aid, float halfWidth) : GenericAOEs(module, aid)
public class ChargeAOEs(BossModule module, ActionID aid, float halfWidth, float riskyAfterSeconds = 0) : GenericAOEs(module, aid)
{
public readonly float HalfWidth = halfWidth;
public float RiskyAfterSeconds = riskyAfterSeconds; // can be used to delay risky status of AOEs, so AI waits longer to dodge, if 0 it will just use the bool Risky
public readonly List<(Actor caster, AOEShape shape, Angle direction)> Casters = [];

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
Expand All @@ -149,7 +153,7 @@ public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
for (var i = 0; i < count; ++i)
{
var csr = Casters[i];
AOEInstance aoeInstance = new(csr.shape, csr.caster.Position, csr.direction, Module.CastFinishAt(csr.caster.CastInfo));
AOEInstance aoeInstance = new(csr.shape, csr.caster.Position, csr.direction, Module.CastFinishAt(csr.caster.CastInfo), Risky: csr.caster.CastInfo?.ElapsedTime > RiskyAfterSeconds);
aoes.Add(aoeInstance);
}
return aoes;
Expand Down
2 changes: 1 addition & 1 deletion BossMod/Modules/Dawntrail/Alliance/A11Prishe/A11Prishe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Holy(BossModule module) : Components.SpreadFromCastTargets(module, ActionI
class BanishgaIV(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.BanishgaIV));
class Banishga(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Banishga));

[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13351, SortOrder = 2)]
[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13351, SortOrder = 2, PlanLevel = 100)]
public class A11Prishe(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, DefaultBounds)
{
public static readonly WPos ArenaCenter = new(800, 400);
Expand Down
2 changes: 1 addition & 1 deletion BossMod/Modules/Dawntrail/Alliance/A12Fafnir/A12Fafnir.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class BalefulBreath(BossModule module) : Components.LineStack(module, (uint)Icon
public override bool KeepOnPhaseChange => true;
}

[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13662, SortOrder = 4)]
[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13662, SortOrder = 4, PlanLevel = 100)]
public class A12Fafnir(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, new ArenaBoundsCircle(34.5f))
{
public static readonly WPos ArenaCenter = new(-500, 600);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class TachiGekko(BossModule module) : Components.CastGaze(module, ActionID.MakeS
class Raiton(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Raiton));
class Utsusemi(BossModule module) : Components.StretchTetherSingle(module, (uint)TetherID.Utsusemi, 10, needToKite: true);

[ModuleInfo(BossModuleInfo.Maturity.Verified, PrimaryActorOID = (uint)OID.BossGK, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13640, SortOrder = 7)]
[ModuleInfo(BossModuleInfo.Maturity.Verified, PrimaryActorOID = (uint)OID.BossGK, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13640, SortOrder = 7, PlanLevel = 100)]
public class A13ArkAngels(WorldState ws, Actor primary) : BossModule(ws, primary, new(865, -820), new ArenaBoundsCircle(34.5f))
{
public static readonly ArenaBoundsCircle DefaultBounds = new(25);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class DoomArc(BossModule module) : Components.RaidwideCast(module, ActionID.Make
class UnbridledRage(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeRect(100, 4), (uint)IconID.UnbridledRage, ActionID.MakeSpell(AID.UnbridledRageAOE), 5.9f);
class DarkNova(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.DarkNova), 6);

[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13653, SortOrder = 8)]
[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13653, SortOrder = 8, PlanLevel = 100)]
public class A14ShadowLord(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, DefaultBounds)
{
private const int RadiusSmall = 8;
Expand Down
16 changes: 9 additions & 7 deletions BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/BindingSigil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ class BindingSigil(BossModule module) : Components.GenericAOEs(module)
public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
{
var count = _aoes.Count;
if (count > 0)
for (var i = 0; i < count; ++i)
{
var aoe = _aoes[i];
if ((aoe.Activation - _aoes[0].Activation).TotalSeconds <= 1)
yield return aoe;
}
if (count == 0)
yield break;

for (var i = 0; i < count; ++i)
{
var aoe = _aoes[i];
if ((aoe.Activation - _aoes[0].Activation).TotalSeconds <= 1)
yield return aoe;
}
}

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
Expand Down
18 changes: 9 additions & 9 deletions BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/DarkNebula.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ class DarkNebula(BossModule module) : Components.Knockback(module)
public override IEnumerable<Source> Sources(int slot, Actor actor)
{
var count = Casters.Count;
if (count != 0)
if (count == 0)
yield break;

for (var i = 0; i < count; ++i)
{
for (var i = 0; i < count; ++i)
if (i < 2)
{
if (i < 2)
{
var caster = Casters[i];
var dir = caster.CastInfo?.Rotation ?? caster.Rotation;
var kind = dir.ToDirection().OrthoL().Dot(actor.Position - caster.Position) > 0 ? Kind.DirLeft : Kind.DirRight;
yield return new(caster.Position, 20, Module.CastFinishAt(caster.CastInfo), null, dir, kind);
}
var caster = Casters[i];
var dir = caster.CastInfo?.Rotation ?? caster.Rotation;
var kind = dir.ToDirection().OrthoL().Dot(actor.Position - caster.Position) > 0 ? Kind.DirLeft : Kind.DirRight;
yield return new(caster.Position, 20, Module.CastFinishAt(caster.CastInfo), null, dir, kind);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class GigaSlash(BossModule module) : Components.GenericAOEs(module)

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
{
if (AOEs.Count > 0)
if (AOEs.Count != 0)
yield return AOEs[0] with { Risky = Module.FindComponent<DarkNebula>()?.Casters.Count == 0 };
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class LoomingChaos(BossModule module) : Components.CastCounter(module, ActionID.
// TODO: tankswap hints component for phase1
// TODO: phase 2 squares, break timer, teleport zones

[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624)]
[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624, PlanLevel = 100)]
public class Ch01CloudOfDarkness(WorldState ws, Actor primary) : BossModule(ws, primary, DefaultCenter, DefaultArena)
{
public static readonly WPos DefaultCenter = new(100, 100);
Expand Down
Loading

0 comments on commit 63b8961

Please sign in to comment.