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

merge fru improvements #560

Merged
merged 8 commits into from
Jan 17, 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
12 changes: 12 additions & 0 deletions BossMod/BossModule/AIHints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,5 +330,17 @@ public Func<WPos, float> GoalCombined(Func<WPos, float> singleTarget, Func<WPos,
};
}

// goal zone that returns a value between 0 and weight depending on distance to point; useful for downtime movement targets
public Func<WPos, float> GoalProximity(WPos destination, float maxDistance, float maxWeight)
{
var invDist = 1.0f / maxDistance;
return p =>
{
var dist = (p - destination).Length();
var weight = 1 - Math.Clamp(invDist * dist, 0, 1);
return maxWeight * weight;
};
}

public WPos ClampToBounds(WPos position) => PathfindMapCenter + PathfindMapBounds.ClampToBounds(position - PathfindMapCenter);
}
2 changes: 1 addition & 1 deletion BossMod/Debug/MainDebugWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ private unsafe void DrawStatuses()
if (ImGui.Button("Add thin ice"))
{
var player = (Character*)GameObjectManager.Instance()->Objects.IndexSorted[0].Value;
player->GetStatusManager()->SetStatus(20, 911, 20.0f, 320, 0xE0000000, true); // param = distance * 10
player->GetStatusManager()->SetStatus(20, 911, 20.0f, 50, 0xE0000000, true); // param = distance * 10
}

foreach (var elem in ws.Actors)
Expand Down
15 changes: 14 additions & 1 deletion BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,20 @@ public class FRUConfig() : ConfigNode()
[GroupPreset("Default", [0, 2, 5, 3, 4, 6, 7, 1])]
public GroupAssignmentUnique P2IntermissionClockSpots = new() { Assignments = [0, 2, 5, 3, 4, 6, 7, 1] };

[PropertyDisplay("P3 Darkest Dance: baiter")]
[PropertyDisplay("P3 Darkest Dance: baiter", tooltip: "Only used by AI")]
[PropertyCombo("MT", "OT")]
public bool P3DarkestDanceOTBait;

[PropertyDisplay("P4 Somber Dance: baiter", tooltip: "Only used by AI")]
[PropertyCombo("MT", "OT")]
public bool P4SomberDanceOTBait = true;

[PropertyDisplay("P5 Akh Morn: side assignments", tooltip: "Only used by AI")]
[GroupDetails(["Left (looking at boss)", "Right (looking at boss)"])]
public GroupAssignmentLightParties P5AkhMornAssignments = GroupAssignmentLightParties.DefaultLightParties();

[PropertyDisplay("P5 Polarizing Strikes: bait order", tooltip: "Only used by AI")]
[GroupDetails(["Left 1", "Left 2", "Left 3", "Left 4", "Right 1", "Right 2", "Right 3", "Right 4"])]
[GroupPreset("TMRH", [0, 4, 3, 7, 1, 5, 2, 6])]
public GroupAssignmentUnique P5PolarizingStrikesAssignments = new() { Assignments = [0, 4, 3, 7, 1, 5, 2, 6] };
}
11 changes: 11 additions & 0 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ public enum AID : uint
WingsDarkAndLightCleaveDark = 40315, // BossP5->self, no cast, range 100 240?-degree cone on target
WingsDarkAndLightTetherLight = 39879, // Helper->players, no cast, range 4 circle on farthest
WingsDarkAndLightTetherDark = 39880, // Helper->player, no cast, range 4 circle on closest

PolarizingStrikes = 40316, // BossP5->self, 6.5+0.5s cast, single-target, visual (line stacks)
CruelPathOfLightBait = 40317, // Helper->self, 0.5s cast, range 100 width 6 rect (left side)
CruelPathOfDarknessBait = 40318, // Helper->self, 0.5s cast, range 100 width 6 rect (right side)
CruelPathOfLightAOE = 40119, // Helper->self, no cast, range 100 width 6 rect (repeated hit)
CruelPathOfDarknessAOE = 40120, // Helper->self, no cast, range 100 width 6 rect (repeated hit)
PolarizingPaths = 40234, // BossP5->self, 2.5+0.5s cast, single-target, visual (second+ hit)

PandorasBox = 40326, // BossP5->self, 12.0s cast, range 100 circle, raidwide requiring tank LB
}

public enum SID : uint
Expand Down Expand Up @@ -310,6 +319,8 @@ public enum SID : uint
SpellInWaitingDarkAero = 2463, // none->player, extra=0x0
//SpellInWaitingReturn = 4208, // none->player, extra=0x0
//SpellInWaitingReturnII = 4171, // Helper->UsurperOfFrostP4, extra=0x0
LightResistanceDown = 4164, // Helper->player, extra=0x0
DarkResistanceDown = 3323, // Helper->player, extra=0x0
}

public enum IconID : uint
Expand Down
60 changes: 46 additions & 14 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ private void Phase5(uint id)
P5Start(id, 77);
P5FulgentBlade(id + 0x10000, 5.3f);
P5ParadiseRegained(id + 0x20000, 4.2f);
P5PolarizingStrikes(id + 0x30000, 7.6f);
P5PandorasBox(id + 0x40000, 5.8f);
P5FulgentBlade(id + 0x50000, 6.2f);
P5ParadiseRegained(id + 0x60000, 8.2f); // TODO: timing...
P5PolarizingStrikes(id + 0x70000, 8); // TODO: timing...
P5FulgentBlade(id + 0x80000, 8); // TODO: timing...

SimpleState(id + 0xFF0000, 100, "???");
}
Expand Down Expand Up @@ -524,9 +530,9 @@ private void P4AkhRhai(uint id, float delay)
.ActivateOnEnter<P4Preposition>()
.DeactivateOnExit<P4Preposition>()
.SetHint(StateMachine.StateHint.DowntimeEnd);
ActorCast(id + 0x10, _module.BossP4Usurper, AID.Materialization, 5.1f, 3, true);
ComponentCondition<P4AkhRhai>(id + 0x20, 11.2f, comp => comp.AOEs.Count > 0, "Puddle baits")
ActorCast(id + 0x10, _module.BossP4Usurper, AID.Materialization, 5.1f, 3, true)
.ActivateOnEnter<P4AkhRhai>();
ComponentCondition<P4AkhRhai>(id + 0x20, 11.2f, comp => comp.AOEs.Count > 0, "Puddle baits");
ComponentCondition<P4AkhRhai>(id + 0x30, 2.6f, comp => comp.NumCasts > 0);
ActorTargetable(id + 0x50, _module.BossP4Oracle, true, 3.6f, "Oracle appears");
ComponentCondition<P4AkhRhai>(id + 0x60, 1.6f, comp => comp.NumCasts >= 10 * comp.AOEs.Count, "Puddle resolve")
Expand All @@ -548,22 +554,22 @@ private void P4DarklitDragonsong(uint id, float delay)
ComponentCondition<P4DarklitDragonsongPathOfLight>(id + 0x21, 0.8f, comp => comp.NumCasts > 0, "Proteans")
.DeactivateOnExit<P4DarklitDragonsongPathOfLight>();
ActorCastEnd(id + 0x22, _module.BossP4Oracle, 2.2f, true)
.ActivateOnEnter<DefaultSpiritTaker>();
.ActivateOnEnter<P4DarklitDragonsongSpiritTaker>();
ActorCastStartMulti(id + 0x23, _module.BossP4Usurper, [AID.HallowedWingsL, AID.HallowedWingsR], 0.1f, true);
ComponentCondition<SpiritTaker>(id + 0x24, 0.3f, comp => comp.Spreads.Count == 0, "Jump")
.DeactivateOnExit<SpiritTaker>();
ActorCastStart(id + 0x25, _module.BossP4Oracle, AID.SomberDance, 2.8f)
.ActivateOnEnter<P4HallowedWingsL>()
.ActivateOnEnter<P4HallowedWingsR>()
.ExecOnEnter<P4DarklitDragonsongDarkWater>(comp => comp.ResolveImminent = true);
.ExecOnEnter<P4DarklitDragonsongDarkWater>(comp => comp.Show());
ComponentCondition<P4DarklitDragonsongDarkWater>(id + 0x26, 1.7f, comp => comp.Stacks.Count == 0, "Stacks")
.DeactivateOnExit<P4DarklitDragonsongDarkWater>();
ActorCastEnd(id + 0x27, _module.BossP4Usurper, 0.2f, false, "Side cleave")
.ActivateOnEnter<P4SomberDance>()
.DeactivateOnExit<P4HallowedWingsL>()
.DeactivateOnExit<P4HallowedWingsR>()
.DeactivateOnExit<P4DarklitDragonsong>();
ActorCastEnd(id + 0x28, _module.BossP4Oracle, 3.1f, true);
.DeactivateOnExit<P4HallowedWingsR>();
ActorCastEnd(id + 0x28, _module.BossP4Oracle, 3.1f, true)
.DeactivateOnExit<P4DarklitDragonsong>(); // tethers deactivate ~0.5s before cast end
ComponentCondition<P4SomberDance>(id + 0x29, 0.4f, comp => comp.NumCasts > 0, "Tankbuster 1")
.SetHint(StateMachine.StateHint.Tankbuster);
ComponentCondition<P4SomberDance>(id + 0x2A, 3.2f, comp => comp.NumCasts > 1, "Tankbuster 2")
Expand Down Expand Up @@ -618,8 +624,7 @@ private void P4CrystallizeTime(uint id, float delay)
.ActivateOnEnter<P4CrystallizeTimeTidalLight>();
ComponentCondition<P4CrystallizeTimeMaelstrom>(id + 0x50, 1.1f, comp => comp.NumCasts > 4, "Hourglass 3")
.DeactivateOnExit<P4CrystallizeTimeMaelstrom>()
.DeactivateOnExit<P4CrystallizeTimeHints>()
.ExecOnExit<P4CrystallizeTimeDragonHead>(comp => comp.ShowPuddles = true);
.DeactivateOnExit<P4CrystallizeTimeHints>();
ActorCast(id + 0x60, _module.BossP4Usurper, AID.TidalLight, 2.3f, 3, true, "Exaline NS start")
.ActivateOnEnter<P4CrystallizeTimeRewind>();
ComponentCondition<P4CrystallizeTimeQuietus>(id + 0x70, 4.1f, comp => comp.NumCasts > 0)
Expand All @@ -632,7 +637,7 @@ private void P4CrystallizeTime(uint id, float delay)
.DeactivateOnExit<P4CrystallizeTime>();
ActorCastStart(id + 0x90, _module.BossP4Oracle, AID.SpiritTaker, 0.4f);
ActorCastStart(id + 0x91, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings1, 2.2f)
.ActivateOnEnter<DefaultSpiritTaker>();
.ActivateOnEnter<P4CrystallizeTimeSpiritTaker>();
ActorCastEnd(id + 0x92, _module.BossP4Oracle, 0.8f);
ComponentCondition<SpiritTaker>(id + 0x93, 0.3f, comp => comp.Spreads.Count == 0, "Jump")
.DeactivateOnExit<SpiritTaker>();
Expand Down Expand Up @@ -683,11 +688,38 @@ private void P5ParadiseRegained(uint id, float delay)
.ActivateOnEnter<P5ParadiseRegainedTowers>(); // first tower appears ~0.9s after cast end, then every 3.5s
ActorCastMulti(id + 0x10, _module.BossP5, [AID.WingsDarkAndLightDL, AID.WingsDarkAndLightLD], 3.2f, 6.9f, true)
.ActivateOnEnter<P5ParadiseRegainedBaits>();
ComponentCondition<P5ParadiseRegainedBaits>(id + 0x20, 0.5f, comp => comp.NumCasts > 0, "Light/dark"); // first tower resolve ~0.1s earlier
ComponentCondition<P5ParadiseRegainedBaits>(id + 0x30, 3.7f, comp => comp.NumCasts > 1, "Dark/light") // second tower resolves ~1s earlier
ComponentCondition<P5ParadiseRegainedBaits>(id + 0x20, 0.3f, comp => comp.NumCasts > 0, "Light/dark"); // first tower resolves at the same time
ComponentCondition<P5ParadiseRegainedBaits>(id + 0x30, 3.7f, comp => comp.NumCasts > 1, "Dark/light") // second tower resolves at the same time
.DeactivateOnExit<P5ParadiseRegainedBaits>();
// note: tethers resolve ~0.7s after cleave, but they won't happen if tether target dies to cleave
ComponentCondition<P5ParadiseRegainedTowers>(id + 0x40, 2.4f, comp => comp.NumCasts > 2, "Towers resolve")
// note: tethers resolve ~0.8s after cleave, but they won't happen if tether target dies to cleave
ComponentCondition<P5ParadiseRegainedTowers>(id + 0x40, 3.4f, comp => comp.NumCasts > 2, "Towers resolve")
.DeactivateOnExit<P5ParadiseRegainedTowers>();
}

private void P5PolarizingStrikes(uint id, float delay)
{
ActorCast(id, _module.BossP5, AID.PolarizingStrikes, delay, 6.5f, true)
.ActivateOnEnter<P5PolarizingStrikes>();
ComponentCondition<P5PolarizingStrikes>(id + 0x10, 0.6f, comp => comp.NumCasts > 0, "Polarizing bait 1");
ActorCastStart(id + 0x20, _module.BossP5, AID.PolarizingPaths, 1.5f, true);
ComponentCondition<P5PolarizingStrikes>(id + 0x21, 0.5f, comp => comp.NumCasts > 2, "Polarizing AOE 1");
ActorCastEnd(id + 0x22, _module.BossP5, 2, true);
ComponentCondition<P5PolarizingStrikes>(id + 0x30, 0.6f, comp => comp.NumCasts > 4, "Polarizing bait 2");
ActorCastStart(id + 0x40, _module.BossP5, AID.PolarizingPaths, 1.5f, true);
ComponentCondition<P5PolarizingStrikes>(id + 0x41, 0.5f, comp => comp.NumCasts > 6, "Polarizing AOE 2");
ActorCastEnd(id + 0x42, _module.BossP5, 2, true);
ComponentCondition<P5PolarizingStrikes>(id + 0x50, 0.6f, comp => comp.NumCasts > 8, "Polarizing bait 3");
ActorCastStart(id + 0x60, _module.BossP5, AID.PolarizingPaths, 1.5f, true);
ComponentCondition<P5PolarizingStrikes>(id + 0x61, 0.5f, comp => comp.NumCasts > 10, "Polarizing AOE 3");
ActorCastEnd(id + 0x62, _module.BossP5, 2, true);
ComponentCondition<P5PolarizingStrikes>(id + 0x70, 0.6f, comp => comp.NumCasts > 12, "Polarizing bait 4");
ComponentCondition<P5PolarizingStrikes>(id + 0x80, 2.0f, comp => comp.NumCasts > 14, "Polarizing AOE 4")
.DeactivateOnExit<P5PolarizingStrikes>();
}

private void P5PandorasBox(uint id, float delay)
{
ActorCast(id, _module.BossP5, AID.PandorasBox, delay, 12, true, "Tank LB")
.SetHint(StateMachine.StateHint.Raidwide);
}
}
12 changes: 11 additions & 1 deletion BossMod/Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@ public override IEnumerable<Source> Sources(int slot, Actor actor)

class P2HiemalStorm(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HiemalStormAOE), 7)
{
private bool _slowDodges;

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
// storms are cast every 3s, ray voidzones appear every 2s; to place voidzones more tightly, we pretend radius is smaller during first half of cast
var deadline = WorldState.FutureTime(1.5f);
// there's no point doing it before first voidzone appears, however
var deadline = _slowDodges ? WorldState.FutureTime(1.5f) : DateTime.MaxValue;
foreach (var c in Casters)
{
var activation = c.Activation;
hints.AddForbiddenZone(ShapeDistance.Circle(c.Origin, activation > deadline ? 4 : 7), activation);
}
}

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
base.OnEventCast(caster, spell);
if ((AID)spell.Action.ID == AID.HiemalRay)
_slowDodges = true;
}
}

class P2HiemalRay(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 4, ActionID.MakeSpell(AID.HiemalRay), module => module.Enemies(OID.HiemalRayVoidzone).Where(z => z.EventState != 7), 0.7f);
Expand Down
68 changes: 48 additions & 20 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ class P2TwinStillnessSilence(BossModule module) : Components.GenericAOEs(module)
private readonly Actor? _source = module.Enemies(OID.OraclesReflection).FirstOrDefault();
private BitMask _thinIce;
private P2SinboundHolyVoidzone? _voidzones; // used for hints only
private const float SlideDistance = 32;

private readonly AOEShapeCone _shapeFront = new(30, 135.Degrees());
private readonly AOEShapeCone _shapeBack = new(30, 45.Degrees());
Expand Down Expand Up @@ -412,29 +413,56 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
hints.AddForbiddenZone(ShapeDistance.Circle(Arena.Center, 16), WorldState.FutureTime(50));
hints.AddForbiddenZone(ShapeDistance.InvertedCone(Arena.Center, 100, desiredDir, halfWidth), DateTime.MaxValue);
}
return;
}

if (AOEs.Count == 0)
{
// if we're behind boss, slide over
hints.AddForbiddenZone(ShapeDistance.Rect(_source.Position, _source.Rotation, 20, 20, 20), DateTime.MaxValue);
}
else
else if (actor.LastFrameMovement == default)
{
// otherwise just dodge next aoe
ref var nextAOE = ref AOEs.Ref(0);
hints.AddForbiddenZone(nextAOE.Shape.Distance(nextAOE.Origin, nextAOE.Rotation), nextAOE.Activation);
}
// at this point, we have thin ice, so we can either stay or move fixed distance
var sourceOffset = _source.Position - Arena.Center;
var needToMove = AOEs.Count > 0 ? AOEs[0].Check(actor.Position) : NumCasts == 0 && sourceOffset.Dot(actor.Position - Arena.Center) > 0;
if (!needToMove)
return;

// ensure we don't slide over voidzones
foreach (var z in _voidzones.Sources(Module))
{
var offset = z.Position - actor.Position;
var dist = offset.Length();
if (dist > 6)
hints.AddForbiddenZone(ShapeDistance.Cone(actor.Position, 100, Angle.FromDirection(offset), Angle.Asin(dist / 6)));
var zoneList = new ArcList(actor.Position, SlideDistance);
zoneList.ForbidInverseCircle(Arena.Center, Arena.Bounds.Radius);

foreach (var z in _voidzones.Sources(Module))
{
var offset = z.Position - actor.Position;
var dist = offset.Length();
if (dist >= SlideDistance)
{
// voidzone center is outside slide distance => forbid voidzone itself
zoneList.ForbidCircle(z.Position, 6);
}
else if (dist >= 6)
{
// forbid the voidzone's shadow
zoneList.ForbidArcByLength(Angle.FromDirection(offset), Angle.Asin(6 / dist));
}
// else: we're already in voidzone, oh well
}

if (AOEs.Count == 0)
{
// if we're behind boss, slide over
zoneList.ForbidInfiniteRect(Arena.Center, Angle.FromDirection(sourceOffset), Arena.Bounds.Radius);
//zoneList.ForbidCircle(_source.Position, 20);
}
else
{
// dodge next aoe
ref var nextAOE = ref AOEs.Ref(0);
zoneList.ForbidInfiniteCone(nextAOE.Origin, nextAOE.Rotation, ((AOEShapeCone)nextAOE.Shape).HalfAngle);
}

var best = zoneList.Allowed(1.Degrees()).MaxBy(r => (r.max - r.min).Rad);
if (best.max.Rad > best.min.Rad)
{
var dir = 0.5f * (best.min + best.max);
hints.AddForbiddenZone(ShapeDistance.InvertedCircle(actor.Position + SlideDistance * dir.ToDirection(), 1), DateTime.MaxValue);
}
}
// else: we are already sliding, nothing to do...
}

public override void DrawArenaForeground(int pcSlot, Actor pc)
Expand Down Expand Up @@ -474,7 +502,7 @@ public override void OnStatusGain(Actor actor, ActorStatus status)
}
}

class P2ThinIce(BossModule module) : Components.ThinIce(module, 32, true)
class P2ThinIce(BossModule module) : Components.ThinIce(module, 32)
{
public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => (Module.FindComponent<P2TwinStillnessSilence>()?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false) ||
!Module.InBounds(pos);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
foreach (var (i, p) in Raid.WithSlot(false, true, true).Exclude(slot))
{
var avoidRadius = avoidBlizzard && States[i].HaveDarkBlizzard ? 12 : 8;
hints.AddForbiddenZone(ShapeDistance.Circle(p.Position, avoidRadius + 1));
hints.AddForbiddenZone(ShapeDistance.Circle(p.Position, avoidRadius));
}
var lasers = Module.FindComponent<P3UltimateRelativitySinboundMeltdownAOE>();
if (lasers != null)
Expand Down Expand Up @@ -401,7 +401,7 @@ class P3UltimateRelativityDarkBlizzard(BossModule module) : Components.GenericAO
private readonly List<Actor> _sources = [];
private DateTime _activation;

private static readonly AOEShapeDonut _shape = new(4, 12); // TODO: verify inner radius
private static readonly AOEShapeDonut _shape = new(3, 12); // TODO: verify inner radius

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor) => _sources.Select(s => new AOEInstance(_shape, s.Position, default, _activation));

Expand Down
13 changes: 13 additions & 0 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhRhai.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ class P4AkhRhai(BossModule module) : Components.GenericAOEs(module, ActionID.Mak

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor) => AOEs;

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
base.AddAIHints(slot, actor, assignment, hints);
if (AOEs.Count == 0)
{
// preposition for baits - note that this is very arbitrary...
var off = 10 * 45.Degrees().ToDirection();
var p1 = ShapeDistance.Circle(Arena.Center + off, 1);
var p2 = ShapeDistance.Circle(Arena.Center - off, 1);
hints.AddForbiddenZone(p => -Math.Min(p1(p), p2(p)), DateTime.MaxValue);
}
}

public override void OnCastStarted(Actor caster, ActorCastInfo spell)
{
if ((AID)spell.Action.ID == AID.AkhRhai)
Expand Down
Loading