Skip to content

Commit

Permalink
Merge pull request #547 from FFXIV-CombatReborn/mergeWIP
Browse files Browse the repository at this point in the history
BA Art module
  • Loading branch information
CarnifexOptimus authored Jan 6, 2025
2 parents 63ac51f + 30aeb0b commit 8c9f276
Show file tree
Hide file tree
Showing 17 changed files with 482 additions and 118 deletions.
2 changes: 1 addition & 1 deletion BossMod/AI/AIBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private void AdjustTargetPositional(Actor player, ref Targeting targeting)

private async Task<(NavigationDecision decision, Targeting updatedTargeting)> BuildNavigationDecision(Actor player, Actor master, Targeting targeting)
{
if (_config.ForbidMovement || _config.ForbidAIMovementMounted && player.MountId != 0)
if (_config.ForbidMovement || _config.ForbidAIMovementMounted && player.MountId != 0 || autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.NoMovement && autorot.Hints.ImminentSpecialMode.activation <= WorldState.FutureTime(1))
return (new NavigationDecision { LeewaySeconds = float.MaxValue }, targeting);

if (_followMaster && (AIPreset == null || _config.OverrideAutorotation))
Expand Down
1 change: 1 addition & 0 deletions BossMod/BossModule/AIHints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public enum SpecialMode
{
Normal,
Pyretic, // pyretic/acceleration bomb type of effects - no movement, no actions, no casting allowed at activation time
NoMovement, // no movement allowed
Freezing, // should be moving at activation time
Misdirection, // temporary misdirection - if current time is greater than activation, use special pathfinding codepath
}
Expand Down
16 changes: 6 additions & 10 deletions BossMod/BossModule/ArenaBounds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ public ArenaBoundsCustom(float Radius, RelSimplifiedComplexPolygon Poly, float M
{
var part = Poly.Parts[i];
edgeList.AddRange(part.ExteriorEdges);
for (var j = 0; j < part.Holes.Count(); ++j)
for (var j = 0; j < part.Holes.Length; ++j)
{
edgeList.AddRange(part.InteriorEdges(j));
}
Expand Down Expand Up @@ -258,7 +258,10 @@ public override WDir ClampToBounds(WDir offset)
for (var i = 0; i < edges.Length; ++i)
{
var edge = edges[i];
var nearest = NearestPointOnSegment(offset, edge.Item1, edge.Item2);
var segmentVector = edge.Item2 - edge.Item1;
var segmentLengthSq = segmentVector.LengthSq();
var t = Math.Max(0, Math.Min(1, (offset - edge.Item1).Dot(segmentVector) / segmentLengthSq));
var nearest = edge.Item1 + t * segmentVector;
var distance = (nearest - offset).LengthSq();

if (distance < minDistance)
Expand All @@ -267,18 +270,11 @@ public override WDir ClampToBounds(WDir offset)
nearestPoint = nearest;
}
}

AddToInstanceCache(cacheKey, nearestPoint);
return nearestPoint;
}

private static WDir NearestPointOnSegment(WDir point, WDir segmentStart, WDir segmentEnd)
{
var segmentVector = segmentEnd - segmentStart;
var segmentLengthSq = segmentVector.LengthSq();
var t = Math.Max(0, Math.Min(1, (point - segmentStart).Dot(segmentVector) / segmentLengthSq));
return segmentStart + t * segmentVector;
}

private Pathfinding.Map BuildMap()
{
// faster than using the polygonwithholes distance method directly
Expand Down
12 changes: 6 additions & 6 deletions BossMod/BossModule/StateMachineBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ public Phase DeathPhase(uint seqID, Action<uint> buildState)
// create a simple state without any actions
public State SimpleState(uint id, float duration, string name)
{
if (_states.ContainsKey(id))
throw new InvalidOperationException($"Duplicate state id {id:X}");
// if (_states.ContainsKey(id))
// throw new InvalidOperationException($"Duplicate state id {id:X}");

var state = _states[id] = new() { ID = id, Duration = duration, Name = name };
if (_lastState != null)
Expand Down Expand Up @@ -297,7 +297,7 @@ public State CastStart<AID>(uint id, AID aid, float delay, string name = "") whe
=> ActorCastStart(id, () => Module.PrimaryActor, aid, delay, true, name);

// create a state triggered by one of a set of expected casts by arbitrary actor; unexpected casts still trigger a transition, but log error
public State ActorCastStartMulti<AID>(uint id, Func<Actor?> actorAcc, IEnumerable<AID> aids, float delay, bool isBoss = false, string name = "")
public State ActorCastStartMulti<AID>(uint id, Func<Actor?> actorAcc, AID[] aids, float delay, bool isBoss = false, string name = "")
where AID : Enum
{
var state = SimpleState(id, delay, name).SetHint(StateMachine.StateHint.BossCastStart, isBoss);
Expand All @@ -315,7 +315,7 @@ public State ActorCastStartMulti<AID>(uint id, Func<Actor?> actorAcc, IEnumerabl
}

// create a state triggered by one of a set of expected casts by a primary actor; unexpected casts still trigger a transition, but log error
public State CastStartMulti<AID>(uint id, IEnumerable<AID> aids, float delay, string name = "") where AID : Enum
public State CastStartMulti<AID>(uint id, AID[] aids, float delay, string name = "") where AID : Enum
=> ActorCastStartMulti(id, () => Module.PrimaryActor, aids, delay, true, name);

// create a state triggered by one of a set of expected casts by arbitrary actor, each of which forking to a separate subsequence
Expand Down Expand Up @@ -360,15 +360,15 @@ public State Cast<AID>(uint id, AID aid, float delay, float castTime, string nam
}

// create a chain of states: ActorCastStartMulti -> ActorCastEnd; second state uses id+1
public State ActorCastMulti<AID>(uint id, Func<Actor?> actorAcc, IEnumerable<AID> aids, float delay, float castTime, bool isBoss = false, string name = "", bool interruptible = false)
public State ActorCastMulti<AID>(uint id, Func<Actor?> actorAcc, AID[] aids, float delay, float castTime, bool isBoss = false, string name = "", bool interruptible = false)
where AID : Enum
{
ActorCastStartMulti(id, actorAcc, aids, delay, isBoss, "");
return ActorCastEnd(id + 1, actorAcc, castTime, isBoss, name, interruptible);
}

// create a chain of states: CastStartMulti -> CastEnd; second state uses id+1
public State CastMulti<AID>(uint id, IEnumerable<AID> aids, float delay, float castTime, string name = "", bool interruptible = false)
public State CastMulti<AID>(uint id, AID[] aids, float delay, float castTime, string name = "", bool interruptible = false)
where AID : Enum
{
CastStartMulti(id, aids, delay, "");
Expand Down
49 changes: 49 additions & 0 deletions BossMod/Components/ChasingAOEs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,52 @@ public override void OnEventIcon(Actor actor, uint iconID, ulong targetID)
}
}
}

// since open world players don't count towards party, we need to make a new component
public abstract class OpenWorldChasingAOEs(BossModule module, AOEShape shape, ActionID actionFirst, ActionID actionRest, float moveDistance, float secondsBetweenActivations, int maxCasts, bool resetExcludedTargets = false, uint icon = default, float activationDelay = 5.1f) : StandardChasingAOEs(module, shape, actionFirst, actionRest, moveDistance, secondsBetweenActivations, maxCasts, resetExcludedTargets, icon, activationDelay)
{
public new HashSet<Actor> ExcludedTargets = []; // any targets in this hashset aren't considered to be possible targets

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if (spell.Action == ActionFirst)
{
var pos = spell.TargetID == caster.InstanceID ? caster.Position : WorldState.Actors.Find(spell.TargetID)?.Position ?? spell.LocXZ;
Actor? target = null;
var minDistanceSq = float.MaxValue;

foreach (var actor in WorldState.Actors)
{
if (actor.OID == 0 && !ExcludedTargets.Contains(actor))
{
var distanceSq = (actor.Position - pos).LengthSq();
if (distanceSq < minDistanceSq)
{
minDistanceSq = distanceSq;
target = actor;
}
}
}
if (target != null)
{
Actors.Remove(target);
Chasers.Add(new(Shape, target, pos, 0, MaxCasts, Module.CastFinishAt(spell), SecondsBetweenActivations)); // initial cast does not move anywhere
ExcludedTargets.Add(target);
}
}
}

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
if (spell.Action == ActionFirst || spell.Action == ActionRest)
{
var pos = spell.MainTargetID == caster.InstanceID ? caster.Position : WorldState.Actors.Find(spell.MainTargetID)?.Position ?? spell.TargetXZ;
Advance(pos, MoveDistance, WorldState.CurrentTime);
if (Chasers.Count == 0 && ResetExcludedTargets)
{
ExcludedTargets.Clear();
NumCasts = 0;
}
}
}
}
9 changes: 8 additions & 1 deletion BossMod/Components/StayMove.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// priorities can be used to simplify implementation when e.g. status changes at different stages of the mechanic (eg if prep status is replaced with pyretic, we want to allow them to happen in any sequence)
public class StayMove(BossModule module, float maxTimeToShowHint = float.PositiveInfinity) : BossComponent(module)
{
public enum Requirement { None, Stay, Move }
public enum Requirement { None, Stay, Stay2, Move }
public record struct PlayerState(Requirement Requirement, DateTime Activation, int Priority = 0);

public readonly PlayerState[] PlayerStates = new PlayerState[PartyState.MaxAllies];
Expand All @@ -18,6 +18,10 @@ public override void AddHints(int slot, Actor actor, TextHints hints)
if (float.IsInfinity(MaxTimeToShowHint) || PlayerStates[slot].Activation <= WorldState.FutureTime(MaxTimeToShowHint))
hints.Add("Stop everything!", actor.PrevPosition != actor.PrevPosition || actor.CastInfo != null || actor.TargetID != 0); // note: assume if target is selected, we might autoattack...
break;
case Requirement.Stay2:
if (float.IsInfinity(MaxTimeToShowHint) || PlayerStates[slot].Activation <= WorldState.FutureTime(MaxTimeToShowHint))
hints.Add("Don't move!", actor.PrevPosition != actor.PrevPosition); // you are allowed to attack here, only moving is forbidden
break;
case Requirement.Move:
if (float.IsInfinity(MaxTimeToShowHint) || PlayerStates[slot].Activation <= WorldState.FutureTime(MaxTimeToShowHint))
hints.Add("Move!", actor.PrevPosition == actor.PrevPosition);
Expand All @@ -36,6 +40,9 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
case Requirement.Stay:
hints.AddSpecialMode(AIHints.SpecialMode.Pyretic, PlayerStates[slot].Activation);
break;
case Requirement.Stay2:
hints.AddSpecialMode(AIHints.SpecialMode.NoMovement, PlayerStates[slot].Activation);
break;
case Requirement.Move:
hints.AddSpecialMode(AIHints.SpecialMode.Freezing, PlayerStates[slot].Activation);
break;
Expand Down
7 changes: 4 additions & 3 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public enum AID : uint
AkhMornAOEOracle = 40303, // Helper->players, no cast, range 4 circle, 4-hit 4-man stack
MornAfahUsurper = 40249, // UsurperOfFrostP4->self, 6.0s cast, single-target, visual (full raid stack, lethal if hp difference is large)
MornAfahOracle = 40304, // OracleOfDarknessP4->self, 6.0s cast, single-target, visual (full raid stack, lethal if hp difference is large)
MornAfahAOE = 40250, // Helper->players, no cast, range 4 circle, wipe if hp difference check fails ?
MornAfahAOE = 40250, // Helper->players, no cast, range 4 circle, 8-man stack on usurper target, wipe if hp difference check fails

CrystallizeTimeUsurper = 40240, // UsurperOfFrostP4->self, 10.0s cast, single-target, visual
CrystallizeTimeOracle = 40298, // OracleOfDarknessP4->self, 10.0s cast, range 100 circle, raidwide
Expand All @@ -240,8 +240,9 @@ public enum AID : uint
LongingOfTheLost = 40241, // Helper->location, no cast, range 12 circle, aoe when head is touched
DrachenWandererDisappear = 40244, // DrachenWanderer->self, no cast, single-target, visual (disappear)
JoylessDragonsong = 40242, // Helper->self, no cast, range 40 circle, wipe if ???
CrystallizeTimeHallowedWings = 40229, // UsurperOfFrostP4->self, 4.7+1.3s cast, single-target, visual (??? knockbacks?)
//_Weaponskill_HallowedWings = 40332, // UsurperOfFrostP4->self, 0.5s cast, range 40 width 50 rect
CrystallizeTimeHallowedWings1 = 40229, // UsurperOfFrostP4->self, 4.7+1.3s cast, single-target, visual (first knockback)
CrystallizeTimeHallowedWings2 = 40230, // UsurperOfFrostP4->self, 0.5+1.3s cast, single-target, visual (second knockback)
CrystallizeTimeHallowedWingsAOE = 40332, // UsurperOfFrostP4->self, 0.5s cast, range 40 width 50 rect, knockback 20, heavy damage on first target, vuln on first 4 targets
}

public enum SID : uint
Expand Down
16 changes: 11 additions & 5 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ private void Phase34(uint id)
P4DarklitDragonsong(id + 0x110000, 1.9f);
P4AkhMornMornAfah(id + 0x120000, 5.8f);
P4CrystallizeTime(id + 0x130000, 4.6f);
P4AkhMornMornAfah(id + 0x140000, 0.1f);

SimpleState(id + 0xFF0000, 100, "???");
}
Expand Down Expand Up @@ -595,18 +596,23 @@ private void P4CrystallizeTime(uint id, float delay)
.ActivateOnEnter<P4CrystallizeTimeQuietus>()
.DeactivateOnExit<P4CrystallizeTimeQuietus>()
.SetHint(StateMachine.StateHint.Raidwide);
ComponentCondition<P4CrystallizeTimeRewind>(id + 0x80, 1.9f, comp => comp.Done, "Rewind")
.DeactivateOnExit<P4CrystallizeTimeRewind>()
ComponentCondition<P4CrystallizeTimeRewind>(id + 0x80, 1.9f, comp => comp.RewindDone, "Rewind place")
.DeactivateOnExit<P4CrystallizeTimeTidalLight>()
.DeactivateOnExit<P4CrystallizeTimeDragonHead>()
.DeactivateOnExit<P4CrystallizeTime>();
ActorCastStart(id + 0x90, _module.BossP4Oracle, AID.SpiritTaker, 0.4f);
ActorCastStart(id + 0x91, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings, 2.2f)
ActorCastStart(id + 0x91, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings1, 2.2f)
.ActivateOnEnter<P3SpiritTaker>();
ActorCastEnd(id + 0x92, _module.BossP4Oracle, 0.8f);
ComponentCondition<P3SpiritTaker>(id + 0x93, 0.3f, comp => comp.Spreads.Count == 0, "Jump")
.DeactivateOnExit<P3SpiritTaker>();
ActorCastEnd(id + 0x94, _module.BossP4Usurper, 3.6f);
// TODO: knockbacks resolve, downtime end
ComponentCondition<P4CrystallizeTimeRewind>(id + 0x94, 3.3f, comp => comp.ReturnDone, "Rewind return")
.DeactivateOnExit<P4CrystallizeTimeRewind>();
ActorCastEnd(id + 0x95, _module.BossP4Usurper, 0.3f);
ActorCast(id + 0xA0, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWingsAOE, 1.4f, 0.5f, true);
ActorCast(id + 0xB0, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings2, 2.1f, 0.5f, true);
ActorCast(id + 0xC0, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWingsAOE, 1.4f, 0.5f, true);
ActorTargetable(id + 0xD0, _module.BossP4Usurper, true, 5.3f, "Bosses reappear")
.SetHint(StateMachine.StateHint.DowntimeEnd);
}
}
5 changes: 3 additions & 2 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
namespace BossMod.Dawntrail.Ultimate.FRU;

// TODO: can target change if boss is provoked mid cast?
class P4AkhMorn(BossModule module) : Components.UniformStackSpread(module, 4, 0, 4)
{
public int NumCasts;

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.AkhMornOracle)
AddStacks(Raid.WithoutSlot(true).Where(p => p.Role == Role.Tank), Module.CastFinishAt(spell, 0.9f));
if ((AID)spell.Action.ID is AID.AkhMornOracle or AID.AkhMornUsurper && WorldState.Actors.Find(caster.TargetID) is var target && target != null)
AddStack(target, Module.CastFinishAt(spell, 0.9f));
}

public override void OnEventCast(Actor caster, ActorCastEvent spell)
Expand Down
66 changes: 61 additions & 5 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -424,21 +424,77 @@ private WDir SafeOffsetFangOther(int numHourglassesDone, float northSlowX)
private WDir SafeOffsetFinalNonAir(float northSlowX) => 6 * (northSlowX > 0 ? -150 : 150).Degrees().ToDirection();
}

// TODO: better positioning hints
class P4CrystallizeTimeRewind(BossModule module) : BossComponent(module)
{
public bool Done;
public bool RewindDone;
public bool ReturnDone;
private readonly P4CrystallizeTime? _ct = module.FindComponent<P4CrystallizeTime>();
private readonly P4CrystallizeTimeTidalLight? _exalines = module.FindComponent<P4CrystallizeTimeTidalLight>();

public override void AddHints(int slot, Actor actor, TextHints hints)
{
if (!RewindDone && _ct != null && _exalines != null && _ct.Cleansed[slot])
{
var players = Raid.WithoutSlot(excludeNPCs: true).ToList();
players.SortBy(p => p.Position.X);
var xOrder = players.IndexOf(actor);
players.SortBy(p => p.Position.Z);
var zOrder = players.IndexOf(actor);
if (xOrder >= 0 && zOrder >= 0)
{
if (_exalines.StartingOffset.X > 0)
xOrder = players.Count - 1 - xOrder;
if (_exalines.StartingOffset.Z > 0)
zOrder = players.Count - 1 - zOrder;

var isFirst = xOrder == 0 || zOrder == 0;
var isTank = actor.Role == Role.Tank;
if (isFirst != isTank)
hints.Add(isTank ? "Stay in front of the group!" : "Hide behind tank!");
var isFirstX = xOrder < 4;
var isFirstZ = zOrder < 4;
if (isFirstX == isFirstZ)
hints.Add("Position in group properly!");
}

if (KnockbackSpots(actor.Position).Any(p => !Module.Bounds.Contains(p - Module.Center)))
hints.Add("About to be knocked into wall!");
}
}

public override void DrawArenaForeground(int pcSlot, Actor pc)
{
if (_ct != null && _exalines != null && _ct.Cleansed[pcSlot])
Arena.AddCircle(Arena.Center + 0.5f * _exalines.StartingOffset, 1, Colors.Safe); // TODO: better hints...
if (!RewindDone && _ct != null && _exalines != null && _ct.Cleansed[pcSlot])
{
var vertices = KnockbackSpots(pc.Position).ToList();
Arena.AddQuad(pc.Position, vertices[0], vertices[2], vertices[1], Colors.Danger);
Arena.AddCircle(Arena.Center + 0.5f * _exalines.StartingOffset, 1, Colors.Safe);
}
}

public override void OnStatusGain(Actor actor, ActorStatus status)
{
if ((SID)status.ID == SID.Return)
Done = true;
switch ((SID)status.ID)
{
case SID.Return:
RewindDone = true;
break;
case SID.Stun:
ReturnDone = true;
break;
}
}

private IEnumerable<WPos> KnockbackSpots(WPos starting)
{
if (_exalines != null)
{
var dx = _exalines.StartingOffset.X > 0 ? -20 : +20;
var dz = _exalines.StartingOffset.Z > 0 ? -20 : +20;
yield return starting + new WDir(dx, 0);
yield return starting + new WDir(0, dz);
yield return starting + new WDir(dx, dz);
}
}
}
5 changes: 2 additions & 3 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ class P4MornAfah(BossModule module) : Components.UniformStackSpread(module, 4, 0
{
public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.MornAfahOracle)
if ((AID)spell.Action.ID == AID.MornAfahUsurper)
{
// note: target is random?..
var target = WorldState.Actors.Find(caster.TargetID);
if (target != null)
AddStack(target, Module.CastFinishAt(spell, 0.9f));
Expand All @@ -15,7 +14,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell)

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
if ((AID)spell.Action.ID == AID.MornAfahAOE) // TODO: proper spell...
if ((AID)spell.Action.ID == AID.MornAfahAOE)
Stacks.Clear();
}
}
Expand Down
Loading

0 comments on commit 8c9f276

Please sign in to comment.