Skip to content

Commit

Permalink
A bunch of FRU improvements.
Browse files Browse the repository at this point in the history
  • Loading branch information
awgil committed Jan 21, 2025
1 parent 10dec46 commit 45f6801
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 97 deletions.
6 changes: 3 additions & 3 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public class FRUConfig() : ConfigNode()

[PropertyDisplay("P1 Fall of Faith (cone tethers): conga priority (two people without tethers with lower priorities join odd group)")]
[GroupDetails(["1", "2", "3", "4", "5", "6", "7", "8"])]
[GroupPreset("TTHHMMRR", [0, 1, 2, 3, 4, 5, 6, 7])]
[GroupPreset("RHMTTMHR", [3, 4, 1, 6, 2, 5, 0, 7])]
public GroupAssignmentUnique P1FallOfFaithAssignment = GroupAssignmentUnique.DefaultRoles();
[GroupPreset("HHTTMMRR", [2, 3, 0, 1, 4, 5, 6, 7])]
[GroupPreset("HRMTTMRH", [3, 4, 0, 7, 2, 5, 1, 6])]
public GroupAssignmentUnique P1FallOfFaithAssignment = new() { Assignments = [2, 3, 0, 1, 4, 5, 6, 7] };

[PropertyDisplay("P1 Fall of Faith (cone tethers): odd groups go W (rather than N)")]
public bool P1FallOfFaithEW = false;
Expand Down
7 changes: 4 additions & 3 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,11 @@ private void P2MirrorMirror(uint id, float delay)
ComponentCondition<P2MirrorMirrorReflectedScytheKickRed>(id + 0x20, 9.3f, comp => comp.NumCasts > 0)
.ActivateOnEnter<P2MirrorMirrorReflectedScytheKickRed>()
.DeactivateOnExit<P2MirrorMirrorReflectedScytheKickRed>();
ComponentCondition<P2MirrorMirrorHouseOfLight>(id + 0x21, 0.6f, comp => comp.NumCasts > 1, "Mirror 2")
ComponentCondition<P2MirrorMirrorHouseOfLight>(id + 0x21, 0.6f, comp => comp.NumCasts > 8, "Mirror 2")
.ActivateOnEnter<P2MirrorMirrorBanish>() // activate a bit early, so that it can read state gathered by house of light component
.DeactivateOnExit<P2MirrorMirrorHouseOfLight>();

ActorCastMulti(id + 0x100, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 0.5f, 5, true)
.ActivateOnEnter<P2MirrorMirrorBanish>();
ActorCastMulti(id + 0x100, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 0.5f, 5, true);
ComponentCondition<P2Banish>(id + 0x102, 0.1f, comp => !comp.Active, "Spread/Stack")
.DeactivateOnExit<P2Banish>();
}
Expand Down Expand Up @@ -523,6 +523,7 @@ private void P4AkhRhai(uint id, float delay)
{
ActorTargetable(id, _module.BossP4Usurper, true, delay, "Usurper appears")
.ActivateOnEnter<P4Preposition>()
.ActivateOnEnter<P4FragmentOfFate>()
.DeactivateOnExit<P4Preposition>()
.SetHint(StateMachine.StateHint.DowntimeEnd);
ActorCastStart(id + 0x10, _module.BossP4Usurper, AID.Materialization, 5.1f, true)
Expand Down
153 changes: 81 additions & 72 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P2MirrorMirror.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
namespace BossMod.Dawntrail.Ultimate.FRU;

class P2MirrorMirrorReflectedScytheKickBlue(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.ReflectedScytheKickBlue))
class P2MirrorMirrorReflectedScytheKickBlue : Components.GenericAOEs
{
private WDir _blueMirror;
private BitMask _rangedSpots;
private AOEInstance? _aoe;

private static readonly AOEShapeDonut _shape = new(4, 20);

public P2MirrorMirrorReflectedScytheKickBlue(BossModule module) : base(module, ActionID.MakeSpell(AID.ReflectedScytheKickBlue))
{
foreach (var (slot, group) in Service.Config.Get<FRUConfig>().P2MirrorMirror1SpreadSpots.Resolve(Raid))
_rangedSpots[slot] = group >= 4;
}

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe);

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
Expand All @@ -28,8 +35,9 @@ public override void DrawArenaForeground(int pcSlot, Actor pc)
Arena.Actor(Module.Center + 20 * _blueMirror, Angle.FromDirection(-_blueMirror), ArenaColor.Object);
if (_aoe == null)
{
// draw hint for melees
Arena.AddCircle(Module.Center - 11 * _blueMirror, 1, ArenaColor.Safe);
// draw preposition hint
var distance = _rangedSpots[pcSlot] ? 19 : -11;
Arena.AddCircle(Module.Center + distance * _blueMirror, 1, ArenaColor.Safe);
}
}
}
Expand Down Expand Up @@ -57,28 +65,34 @@ public override void DrawArenaForeground(int pcSlot, Actor pc)

class P2MirrorMirrorHouseOfLight(BossModule module) : Components.GenericBaitAway(module, ActionID.MakeSpell(AID.HouseOfLight))
{
public readonly record struct Source(Actor Actor, DateTime Activation);

public bool RedRangedLeftOfMelee;
public readonly List<Source> FirstSources = []; // [boss, blue mirror]
public readonly List<Source> SecondSources = []; // [melee red mirror, ranged red mirror]
private readonly FRUConfig _config = Service.Config.Get<FRUConfig>();
private Angle? _blueMirror;
private int _numRedMirrors;
private readonly List<(Actor source, DateTime activation)> _sources = [];

private List<Source> CurrentSources => NumCasts == 0 ? FirstSources : SecondSources;

private static readonly AOEShapeCone _shape = new(60, 15.Degrees());

public override void Update()
{
CurrentBaits.Clear();
foreach (var s in _sources.Take(2))
foreach (var p in Raid.WithoutSlot().SortedByRange(s.source.Position).Take(4))
CurrentBaits.Add(new(s.source, p, _shape, s.activation));
foreach (var s in CurrentSources)
foreach (var p in Raid.WithoutSlot().SortedByRange(s.Actor.Position).Take(4))
CurrentBaits.Add(new(s.Actor, p, _shape, s.Activation));
}

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
var group = (NumCasts == 0 ? _config.P2MirrorMirror1SpreadSpots : _config.P2MirrorMirror2SpreadSpots)[assignment];
if (_sources.Count < 2 || _blueMirror == null || group < 0)
var sources = CurrentSources;
if (sources.Count < 2 || _blueMirror == null || group < 0)
return; // inactive or no valid assignments

var origin = _sources[group < 4 ? 0 : 1];
var origin = sources[group < 4 ? 0 : 1];
Angle dir;
if (NumCasts == 0)
{
Expand All @@ -93,31 +107,20 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
}
else
{
var offset = origin.source.Position - Module.Center;
var altSourceToTheRight = offset.OrthoL().Dot(_sources[group < 4 ? 1 : 0].source.Position - Module.Center) < 0;
dir = Angle.FromDirection(offset) + group switch
dir = Angle.FromDirection(origin.Actor.Position - Module.Center) + group switch
{
0 => (altSourceToTheRight ? 90 : -90).Degrees(),
1 => (altSourceToTheRight ? -90 : 90).Degrees(),
0 => (RedRangedLeftOfMelee ? -90 : 90).Degrees(),
1 => (RedRangedLeftOfMelee ? 90 : -90).Degrees(),
2 => 180.Degrees(),
3 => (altSourceToTheRight ? 135 : -135).Degrees(),
3 => (RedRangedLeftOfMelee ? -135 : 135).Degrees(),
4 => -90.Degrees(),
5 => 90.Degrees(),
6 => (altSourceToTheRight ? 180 : -135).Degrees(),
7 => (altSourceToTheRight ? 135 : 180).Degrees(),
6 => (RedRangedLeftOfMelee ? 180 : -135).Degrees(),
7 => (RedRangedLeftOfMelee ? 135 : 180).Degrees(),
_ => default
};

// special logic for current tank: if mechanic will take a while to resolve, and boss is far enough away from the destination, and normal destination is on the same side as the boss, drag towards other side first
// this guarantees uptime for OT
//if (origin.activation > WorldState.FutureTime(3) && Module.Enemies(OID.BossP2).FirstOrDefault() is var boss && boss != null && boss.TargetID == actor.InstanceID)
//{
// var dirVec = dir.ToDirection();
// if (dirVec.Dot(boss.Position - origin.source.Position) > 2.5f && (origin.source.Position - 3 * dirVec - boss.Position).Length() > boss.HitboxRadius + 3.5f)
// dir += 180.Degrees();
//}
}
hints.AddForbiddenZone(ShapeDistance.InvertedCone(origin.source.Position, 4, dir, 15.Degrees()), origin.activation);
hints.AddForbiddenZone(ShapeDistance.InvertedCone(origin.Actor.Position, 4, dir, 15.Degrees()), origin.Activation);
}

public override void OnEventEnvControl(byte index, uint state)
Expand All @@ -134,80 +137,86 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
case AID.ScytheKick:
var activation = Module.CastFinishAt(spell, 0.7f);
_sources.Add((caster, activation));
FirstSources.Add(new(caster, activation));
var mirror = _blueMirror != null ? Module.Enemies(OID.FrozenMirror).Closest(Module.Center + 20 * _blueMirror.Value.ToDirection()) : null;
if (mirror != null)
_sources.Add((mirror, activation));
FirstSources.Add(new(mirror, activation));
break;
case AID.ReflectedScytheKickRed:
_sources.Add((caster, Module.CastFinishAt(spell, 0.6f)));
if (++_numRedMirrors == 2 && _blueMirror != null && _sources.Count >= 2)
SecondSources.Add(new(caster, Module.CastFinishAt(spell, 0.6f)));
if (SecondSources.Count == 2 && _blueMirror != null)
{
// order two red mirrors so that first one is closer to boss and second one closer to blue mirror; if both are same distance, select CW ones (arbitrary)
var d1 = (Angle.FromDirection(_sources[^2].source.Position - Module.Center) - _blueMirror.Value).Normalized();
var d2 = (Angle.FromDirection(_sources[^1].source.Position - Module.Center) - _blueMirror.Value).Normalized();
var d1 = (Angle.FromDirection(SecondSources[0].Actor.Position - Module.Center) - _blueMirror.Value).Normalized();
var d2 = (Angle.FromDirection(SecondSources[1].Actor.Position - Module.Center) - _blueMirror.Value).Normalized();
var d1abs = d1.Abs();
var d2abs = d2.Abs();
var swap = d2abs.AlmostEqual(d1abs, 0.1f)
? d2.Rad > 0 // swap if currently second one is CCW from blue mirror
: d2abs.Rad > d1abs.Rad; // swap if currently second one is further from the blue mirror
if (swap)
(_sources[^1], _sources[^2]) = (_sources[^2], _sources[^1]);
(SecondSources[1], SecondSources[0]) = (SecondSources[0], SecondSources[1]);

RedRangedLeftOfMelee = (SecondSources[0].Actor.Position - Module.Center).OrthoL().Dot(SecondSources[1].Actor.Position - Module.Center) > 0;
}
break;
}
}
}

class P2MirrorMirrorBanish : P2Banish
{
private WPos _anchorMelee;
private WPos _anchorRanged;
private BitMask _aroundRanged;
private BitMask _closerToCenter;
private BitMask _leftSide;

public override void OnEventCast(Actor caster, ActorCastEvent spell)
public P2MirrorMirrorBanish(BossModule module) : base(module)
{
if (spell.Action == WatchedAction)
var proteans = module.FindComponent<P2MirrorMirrorHouseOfLight>();
if (proteans != null && proteans.FirstSources.Count == 2 && proteans.SecondSources.Count == 2)
{
var deadline = WorldState.FutureTime(2);
var firstInBunch = _sources.RemoveAll(s => s.activation < deadline) > 0;
if (firstInBunch)
++NumCasts;
_anchorMelee = proteans.FirstSources[0].Actor.Position;
_anchorRanged = module.Center + 0.5f * (proteans.SecondSources[1].Actor.Position - module.Center);
foreach (var (slot, group) in Service.Config.Get<FRUConfig>().P2MirrorMirror2SpreadSpots.Resolve(Raid))
{
_aroundRanged[slot] = group >= 4;
_closerToCenter[slot] = (group & 2) != 0;
_leftSide[slot] = group switch
{
0 => !proteans.RedRangedLeftOfMelee,
1 => proteans.RedRangedLeftOfMelee,
2 => proteans.RedRangedLeftOfMelee,
3 => !proteans.RedRangedLeftOfMelee,
4 => false,
5 => true,
6 => false,
7 => true,
_ => false
};
}
}
}
}

class P2MirrorMirrorBanish(BossModule module) : P2Banish(module)
{
public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
var prepos = PrepositionLocation(assignment);
var prepos = PrepositionLocation(slot, assignment);
if (prepos != null)
hints.AddForbiddenZone(ShapeDistance.InvertedCircle(prepos.Value, 1), DateTime.MaxValue);
else
base.AddAIHints(slot, actor, assignment, hints);
}

private WPos? PrepositionLocation(PartyRolesConfig.Assignment assignment)
private WPos? PrepositionLocation(int slot, PartyRolesConfig.Assignment assignment)
=> Stacks.Count > 0 && Stacks[0].Activation > WorldState.FutureTime(2.5f) ? CalculatePrepositionLocation(_aroundRanged[slot], _leftSide[slot], 90.Degrees())
: Spreads.Count > 0 && Spreads[0].Activation > WorldState.FutureTime(2.5f) ? CalculatePrepositionLocation(_aroundRanged[slot], _leftSide[slot], (_closerToCenter[slot] ? 135 : 45).Degrees())
: null;

private WPos CalculatePrepositionLocation(bool aroundRanged, bool leftSide, Angle angle)
{
// TODO: consider a different strategy for melee (left if more left)
if (Stacks.Count > 0 && Stacks[0].Activation > WorldState.FutureTime(2.5f))
{
// preposition for stacks
var boss = Module.Enemies(OID.BossP2).FirstOrDefault();
return assignment switch
{
PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.M1 => boss != null ? boss.Position + 6 * boss.Rotation.ToDirection().OrthoL() : null,
PartyRolesConfig.Assignment.OT or PartyRolesConfig.Assignment.M2 => boss != null ? boss.Position + 6 * boss.Rotation.ToDirection().OrthoR() : null,
_ => null // TODO: implement positioning for ranged
};
}
else if (Spreads.Count > 0 && Spreads[0].Activation > WorldState.FutureTime(2.5f))
{
// preposition for spreads
var boss = Module.Enemies(OID.BossP2).FirstOrDefault();
return assignment switch
{
PartyRolesConfig.Assignment.MT => boss != null ? boss.Position + 6 * (boss.Rotation + 45.Degrees()).ToDirection() : null,
PartyRolesConfig.Assignment.OT => boss != null ? boss.Position + 6 * (boss.Rotation - 45.Degrees()).ToDirection() : null,
PartyRolesConfig.Assignment.M1 => boss != null ? boss.Position + 6 * (boss.Rotation + 135.Degrees()).ToDirection() : null,
PartyRolesConfig.Assignment.M2 => boss != null ? boss.Position + 6 * (boss.Rotation - 135.Degrees()).ToDirection() : null,
_ => null // TODO: implement positioning for ranged
};
}
return null;
var anchor = aroundRanged ? _anchorRanged : _anchorMelee;
var offset = Angle.FromDirection(anchor - Module.Center) + (leftSide ? angle : -angle);
return anchor + 6 * offset.ToDirection();
}
}
24 changes: 17 additions & 7 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,16 +342,26 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme

public override void DrawArenaForeground(int pcSlot, Actor pc)
{
base.DrawArenaForeground(pcSlot, pc);

foreach (ref var b in CurrentBaits.AsSpan())
foreach (var bait in ActiveBaitsOn(pc))
{
if (b.Target == pc && b.Source.Position.AlmostEqual(AssignedHourglass(pcSlot), 1) && _rel != null)
if (bait.Source.Position.AlmostEqual(AssignedHourglass(pcSlot), 1) && _rel != null)
{
// draw extra rotation hints for correctly baited hourglass
var rot = _rel.LaserRotationAt(b.Source.Position);
for (int i = 1; i < 10; ++i)
_shape.Outline(Arena, b.Source.Position, b.Rotation + i * rot);
// note: we don't want to draw 'short' edges of the rectangle (farther one is far outside arena bounds anyway, and closer one messes visualization up too much
var rot = _rel.LaserRotationAt(bait.Source.Position);
for (int i = 0; i < 10; ++i)
{
var dir = (bait.Rotation + i * rot).ToDirection();
var side = _shape.HalfWidth * dir.OrthoR();
var end = bait.Source.Position + _shape.LengthFront * dir;
Arena.AddLine(bait.Source.Position + side, end + side, ArenaColor.Danger);
Arena.AddLine(bait.Source.Position - side, end - side, ArenaColor.Danger);
}
}
else
{
// just draw default hint
bait.Shape.Outline(Arena, bait.Source.Position, bait.Rotation);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ private WDir AssignedPositionOffset(Actor actor, PartyRolesConfig.Assignment ass
var isLeft = assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.H1 or PartyRolesConfig.Assignment.M1 or PartyRolesConfig.Assignment.R1;
var offDir = (Angle.FromDirection(_exalines.StartingOffsetSum) + (isLeft ? 45 : -45).Degrees()).ToDirection();
var normDir = isLeft ? offDir.OrthoL() : offDir.OrthoR();
var (offX, offY) = actor.Role == Role.Tank ? (4, 1) : (2, 3);
var (offX, offY) = actor.Role == Role.Tank ? (4, 1) : (1, 2);
return offX * offDir + offY * normDir;
}
}
Expand Down
22 changes: 22 additions & 0 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P4Preposition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,25 @@ public override void DrawArenaForeground(int pcSlot, Actor pc)
Arena.AddCircle(b.Position, 1, ArenaColor.Safe);
}
}

// utility to draw hitbox around crystal, so that it's easier not to clip
class P4FragmentOfFate(BossModule module) : BossComponent(module)
{
private readonly IReadOnlyList<Actor> _fragment = module.Enemies(OID.FragmentOfFate);

public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot, Actor player, ref uint customColor)
{
if (playerSlot >= PartyState.MaxPartySize)
{
customColor = ArenaColor.Object;
return PlayerPriority.Danger;
}
return PlayerPriority.Irrelevant;
}

public override void DrawArenaForeground(int pcSlot, Actor pc)
{
foreach (var f in _fragment)
Arena.AddCircle(f.Position, f.HitboxRadius, ArenaColor.Object);
}
}
13 changes: 3 additions & 10 deletions TODO
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
immediate plans
- circle vs rect/cone intersection
- fru
-- banish 2 ai
-- proper circle bounds -> revise everything
-- p1 fall of faith presets for hhttmmrr / hrmttmrh
-- mirror 1 hints for ranged
-- ur laser circle hint on top of baits
-- apoc revise default prio for healers
-- p4 crystal hitbox
-- ct revise visual pos hints for eruptions
-- ct revise visual pos hints for rewind
- proper circle bounds -> revise everything in fru
- ai refactoring
-- high-level ai modules
--- ordered before standard rotation modules
Expand All @@ -35,6 +26,8 @@ immediate plans
- ishape

general:
- pending effects
-- there's no effectresult for attract if it won't move (within mindistance)
- horizontal timeline / cooldown planner
- cdplanner should use real cds
- assignments per ui order rather than per player + class-specific assignments
Expand Down
2 changes: 1 addition & 1 deletion UIDev/UIDev.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@
</ItemGroup>

<Target Name="PostBuildScript" AfterTargets="Build">
<Copy SourceFiles="$(DalamudLibPath)\runtimes\win-x64\native\cimgui.dll" DestinationFolder="$(OutputPath)"/>
<Copy SourceFiles="$(DalamudLibPath)cimgui.dll" DestinationFolder="$(OutputPath)"/>
</Target>
</Project>

0 comments on commit 45f6801

Please sign in to comment.