Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixed enemy priorities #577

Merged
merged 4 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}