Skip to content

Commit

Permalink
Merge pull request #577 from FFXIV-CombatReborn/mergeWIP
Browse files Browse the repository at this point in the history
fixed enemy priorities
  • Loading branch information
CarnifexOptimus authored Jan 24, 2025
2 parents 01a13ef + 9531212 commit f734cf1
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 50 deletions.
90 changes: 55 additions & 35 deletions BossMod/BossModule/AIHintsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public sealed class AIHintsBuilder : IDisposable
private readonly Dictionary<ulong, (Actor Caster, Actor? Target, AOEShape Shape, bool IsCharge)> _activeAOEs = [];
private ArenaBoundsCircle? _activeFateBounds;
private static readonly HashSet<uint> ignore = [27503, 33626]; // action IDs that the AI should ignore
private static readonly PartyRolesConfig _config = Service.Config.Get<PartyRolesConfig>();
private static readonly Dictionary<uint, Lumina.Excel.Sheets.Fate> _fateCache = [];
private static readonly Dictionary<uint, Lumina.Excel.Sheets.Action> _spellCache = [];

public AIHintsBuilder(WorldState ws, BossModuleManager bmm, ZoneModuleManager zmm)
{
Expand Down Expand Up @@ -42,9 +45,9 @@ public void Update(AIHints hints, int playerSlot, float maxCastTime)
var player = _ws.Party[playerSlot];
if (player != null)
{
var playerAssignment = Service.Config.Get<PartyRolesConfig>()[_ws.Party.Members[playerSlot].ContentId];
var playerAssignment = _config[_ws.Party.Members[playerSlot].ContentId];
var activeModule = _bmm.ActiveModule?.StateMachine.ActivePhase != null ? _bmm.ActiveModule : null;
FillEnemies(hints, playerAssignment == PartyRolesConfig.Assignment.MT || playerAssignment == PartyRolesConfig.Assignment.OT && !_ws.Party.WithoutSlot().Any(p => p != player && p.Role == Role.Tank));
FillEnemies(hints, playerAssignment == PartyRolesConfig.Assignment.MT || playerAssignment == PartyRolesConfig.Assignment.OT && !_ws.Party.WithoutSlot(false, false, true).Any(p => p != player && p.Role == Role.Tank));
if (activeModule != null)
{
activeModule.CalculateAIHints(playerSlot, player, playerAssignment, hints);
Expand All @@ -58,24 +61,41 @@ public void Update(AIHints hints, int playerSlot, float maxCastTime)
hints.Normalize();
}

// fill list of potential targets from world state
// Fill list of potential targets from world state
private void FillEnemies(AIHints hints, bool playerIsDefaultTank)
{
var playerInFate = _ws.Client.ActiveFate.ID != 0 && (_ws.Party.Player()?.Level <= Service.LuminaRow<Lumina.Excel.Sheets.Fate>(_ws.Client.ActiveFate.ID)?.ClassJobLevelMax
|| Service.LuminaRow<Lumina.Excel.Sheets.Fate>(_ws.Client.ActiveFate.ID)?.EurekaFate == 1); // TODO: find out how to get the current player elemental level
var allowedFateID = playerInFate ? _ws.Client.ActiveFate.ID : 0;
foreach (var actor in _ws.Actors.Where(a => a.IsTargetable && !a.IsAlly && !a.IsDead))
uint allowedFateID = 0;
var activeFateID = _ws.Client.ActiveFate.ID;
if (activeFateID != 0)
{
var activeFateRow = GetFateRow(activeFateID);
var playerInFate = activeFateRow != null && (_ws.Party.Player()?.Level <= activeFateRow.Value.ClassJobLevelMax || activeFateRow.Value.EurekaFate == 1);
allowedFateID = playerInFate ? activeFateID : 0;
}
foreach (var actor in _ws.Actors.Actors.Values)
{
var index = actor.CharacterSpawnIndex;
if (index < 0 || index >= hints.Enemies.Length)
continue;
if (!actor.IsTargetable || actor.IsAlly || actor.IsDead)
continue;

// determine default priority for the enemy
var priority = actor.FateID > 0 && actor.FateID != allowedFateID ? AIHints.Enemy.PriorityInvincible // fate mob in fate we are NOT a part of can't be damaged at all
: actor.PredictedDead ? AIHints.Enemy.PriorityPointless // this mob is about to be dead, any attacks will likely ghost
: actor.AggroPlayer ? 0 // enemies in our enmity list can be attacked, regardless of who they are targeting (since they are keeping us in combat)
: actor.InCombat && _ws.Party.FindSlot(actor.TargetID) >= 0 ? 0 // we generally want to assist our party members (note that it includes allied npcs in duties)
: AIHints.Enemy.PriorityUndesirable; // this enemy is either not pulled yet or fighting someone we don't care about - try not to aggro it by default
int priority;
if (actor.FateID != 0)
{
if (actor.FateID != allowedFateID)
priority = AIHints.Enemy.PriorityInvincible; // Fate mob in an irrelevant fate
else
priority = 0; // Relevant fate mob
}
else if (actor.PredictedDead)
priority = AIHints.Enemy.PriorityPointless; // Mob is about to die
else if (actor.AggroPlayer)
priority = 0; // Aggroed player
else if (actor.InCombat && _ws.Party.FindSlot(actor.TargetID) >= 0)
priority = 0; // Assisting party members
else
priority = AIHints.Enemy.PriorityUndesirable; // Default undesirable

var enemy = hints.Enemies[index] = new(actor, priority, playerIsDefaultTank);
hints.PotentialTargets.Add(enemy);
Expand All @@ -84,8 +104,15 @@ private void FillEnemies(AIHints hints, bool playerIsDefaultTank)

private void CalculateAutoHints(AIHints hints, Actor player)
{
var inFate = _ws.Client.ActiveFate.ID != 0 && (_ws.Party.Player()?.Level <= Service.LuminaRow<Lumina.Excel.Sheets.Fate>(_ws.Client.ActiveFate.ID)?.ClassJobLevelMax
|| Service.LuminaRow<Lumina.Excel.Sheets.Fate>(_ws.Client.ActiveFate.ID)?.EurekaFate == 1); // TODO: find out how to get the current player elemental level
var inFate = false;
var activeFateID = _ws.Client.ActiveFate.ID;
if (activeFateID != 0)
{
var activeFateRow = GetFateRow(activeFateID);
var playerInFate = activeFateRow != null && (_ws.Party.Player()?.Level <= activeFateRow.Value.ClassJobLevelMax || activeFateRow.Value.EurekaFate == 1);
inFate = playerInFate;
}

var center = inFate ? _ws.Client.ActiveFate.Center : player.PosRot.XYZ();
var (e, bitmap) = Obstacles.Find(center);
var resolution = bitmap?.PixelSize ?? 0.5f;
Expand Down Expand Up @@ -153,7 +180,7 @@ private void OnCastStarted(Actor actor)
{
if (actor.Type is not ActorType.Enemy and not ActorType.Helper || actor.IsAlly)
return;
var data = actor.CastInfo!.IsSpell() ? Service.LuminaRow<Lumina.Excel.Sheets.Action>(actor.CastInfo.Action.ID) : null;
var data = actor.CastInfo!.IsSpell() ? GetSpellRow(actor.CastInfo.Action.ID) : null;
var dat = data!.Value;
if (data == null || dat.CastType == 1)
return;
Expand Down Expand Up @@ -206,24 +233,17 @@ private Angle DetermineConeAngle(Lumina.Excel.Sheets.Action data)
return angle.Degrees();
}

// private float DetermineDonutInner(Lumina.Excel.Sheets.Action data)
// {
// var omen = data.Omen.ValueNullable;
// if (omen == null)
// {
// Service.Log($"[AutoHints] No omen data for {data.RowId} '{data.Name}'...");
// return 0;
// }
// var path = omen.Value.Path.ToString();
// var pos = path.IndexOf("sircle_", StringComparison.Ordinal);
// if (pos >= 0 && pos + 11 <= path.Length && int.TryParse(path.AsSpan(pos + 9, 2), out var inner))
// return inner;

// pos = path.IndexOf("circle", StringComparison.Ordinal);
// if (pos >= 0 && pos + 10 <= path.Length && int.TryParse(path.AsSpan(pos + 8, 2), out inner))
// return inner;
private Lumina.Excel.Sheets.Fate? GetFateRow(uint fateID)
{
if (_fateCache.TryGetValue(fateID, out var fateRow))
return fateRow;
return _fateCache[fateID] = Service.LuminaRow<Lumina.Excel.Sheets.Fate>(fateID) ?? new();
}

// Service.Log($"[AutoHints] Can't determine inner radius from omen ({path}/{omen.Value.PathAlly}) for {data.RowId} '{data.Name}'...");
// return 0;
// }
private Lumina.Excel.Sheets.Action? GetSpellRow(uint actionID)
{
if (_spellCache.TryGetValue(actionID, out var actionRow))
return actionRow;
return _spellCache[actionID] = Service.LuminaRow<Lumina.Excel.Sheets.Action>(actionID) ?? new();
}
}
7 changes: 4 additions & 3 deletions BossMod/Config/ConfigNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,18 @@ private static FieldInfo[] GetSerializableFields(Type t)

var fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
var len = fields.Length;
var discoveredFields = new List<FieldInfo>(len);
var discoveredFields = new FieldInfo[len];
var index = 0;
for (var i = 0; i < len; ++i)
{
var field = fields[i];
if (!field.IsStatic && !field.IsDefined(typeof(JsonIgnoreAttribute), false))
{
discoveredFields.Add(field);
discoveredFields[index++] = field;
}
}

return _fieldsCache[t] = [.. discoveredFields];
return _fieldsCache[t] = discoveredFields[..index];
}

// deserialize fields from json; default implementation should work fine for most cases
Expand Down
3 changes: 3 additions & 0 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public class FRUConfig() : ConfigNode()
[PropertyDisplay("P3 Apocalypse: uptime swaps (only consider swaps within prio 1/2 and 3/4, assuming these are melee and ranged)")]
public bool P3ApocalypseUptime;

[PropertyDisplay("P3 Apocalypse: ignore swaps and use initial static positions for spreads")]
public bool P3ApocalypseStaticSpreads;

[PropertyDisplay("P4 Darklit Dragonsong: assignments (lower prio stays more clockwise, lowest prio support takes N tower)")]
[GroupDetails(["Support prio1", "Support prio2", "Support prio3", "Support prio4", "DD prio1", "DD prio2", "DD prio3", "DD prio4"])]
[GroupPreset("Default (healer N)", [2, 3, 0, 1, 4, 5, 6, 7])]
Expand Down
19 changes: 12 additions & 7 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class P3ApocalypseDarkWater(BossModule module) : Components.UniformStackSpread(m
public struct State
{
public int Order;
public int InitialGroup;
public int InitialPosition;
public int AssignedGroup;
public int AssignedPosition;
public DateTime Expiration;
Expand Down Expand Up @@ -143,8 +145,9 @@ private void InitAssignments()
Span<int> slotPerAssignment = [-1, -1, -1, -1, -1, -1, -1, -1];
foreach (var (slot, group) in _config.P3ApocalypseAssignments.Resolve(Raid))
{
States[slot].AssignedGroup = group < 4 ? 1 : 2;
States[slot].AssignedPosition = group & 3;
ref var state = ref States[slot];
state.InitialGroup = state.AssignedGroup = group < 4 ? 1 : 2;
state.InitialPosition = state.AssignedPosition = group & 3;
slotPerAssignment[group] = slot;
}

Expand Down Expand Up @@ -259,6 +262,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme

class P3ApocalypseDarkEruption(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.DarkEruption, ActionID.MakeSpell(AID.DarkEruption), 6, 5.1f)
{
private readonly FRUConfig _config = Service.Config.Get<FRUConfig>();
private readonly P3Apocalypse? _apoc = module.FindComponent<P3Apocalypse>();
private readonly P3ApocalypseDarkWater? _water = module.FindComponent<P3ApocalypseDarkWater>();

Expand Down Expand Up @@ -302,27 +306,28 @@ private WDir SafeOffset(int slot, out WDir reference)
reference = 10 * midDir.ToDirection();

ref var state = ref _water.States[slot];
if (state.AssignedGroup == 0)
var (group, pos) = _config.P3ApocalypseStaticSpreads ? (state.InitialGroup, state.InitialPosition) : (state.AssignedGroup, state.AssignedPosition);
if (group == 0)
return default; // no assignments - oh well, at least we know reference directions

// G1 takes dir CCW from N, G2 takes 0/45/90/135
var midIsForG2 = midDir.Deg is >= -20 and < 160;
if (midIsForG2 != (state.AssignedGroup == 2))
if (midIsForG2 != (group == 2))
{
midDir += 180.Degrees();
reference = -reference;
}

if ((state.AssignedPosition & 2) == 0)
if ((pos & 2) == 0)
{
// melee spot; note that non-reference melee goes in right after second apoc (max range is 14-9)
var altPos = _apoc.Rotation.Rad < 0 ? 1 : 0;
return state.AssignedPosition == altPos ? (_apoc.NumCasts > 4 ? 4.5f : 10) * (midDir - _apoc.Rotation).ToDirection() : reference;
return pos == altPos ? (_apoc.NumCasts > 4 ? 4.5f : 10) * (midDir - _apoc.Rotation).ToDirection() : reference;
}
else
{
// ranged spot
var offset = (state.AssignedPosition == 2 ? -15 : +15).Degrees();
var offset = (pos == 2 ? -15 : +15).Degrees();
return 19 * (midDir + offset).ToDirection();
}
}
Expand Down
8 changes: 4 additions & 4 deletions BossMod/Modules/Shadowbringers/Hunt/RankA/Baal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
var count = _aoes.Count;
if (count == 0)
return [];
List<AOEInstance> aoes = new(count);
var aoes = new AOEInstance[count];
for (var i = 0; i < count; ++i)
{
var aoe = _aoes[i];
if (i == 0)
aoes.Add(count > 1 ? aoe with { Color = Colors.Danger } : aoe);
aoes[i] = count > 1 ? aoe with { Color = Colors.Danger } : aoe;
else if (i == 1)
aoes.Add(aoe with { Risky = false });
aoes[i] = aoe with { Risky = false };
}
return aoes;
}
Expand Down Expand Up @@ -71,7 +71,7 @@ public BaalStates(BossModule module) : base(module)
{
TrivialPhase()
.ActivateOnEnter<SewageWave>()
.ActivateOnEnter<SewerWater>()
.ActivateOnEnter<SewerWater1>()
.ActivateOnEnter<SewerWater2>();
}
}
Expand Down
2 changes: 1 addition & 1 deletion UIDev/IntersectionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ public override void Draw()
else
_arena.AddCone(default, _shapeExtentPrimary, _shapeDirDeg.Degrees(), _shapeExtentSecondary.Degrees(), Colors.Safe);
_arena.AddCircle(circleCenter.ToWPos(), _circleRadius, Colors.Danger);
_arena.End();
MiniArena.End();
}
}

0 comments on commit f734cf1

Please sign in to comment.