Skip to content

Commit

Permalink
Merge pull request #569 from FFXIV-CombatReborn/mergeWIP
Browse files Browse the repository at this point in the history
merge vbm (FRU verified)
  • Loading branch information
CarnifexOptimus authored Jan 21, 2025
2 parents 95a34df + df3404e commit 2667b11
Show file tree
Hide file tree
Showing 19 changed files with 258 additions and 174 deletions.
3 changes: 2 additions & 1 deletion BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ class P3Junction(BossModule module) : Components.CastCounter(module, ActionID.Ma
abstract class P4HallowedWings(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(80, 20));
class P4HallowedWingsL(BossModule module) : P4HallowedWings(module, AID.HallowedWingsL);
class P4HallowedWingsR(BossModule module) : P4HallowedWings(module, AID.HallowedWingsR);
class P5ParadiseLost(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.ParadiseLostP5AOE));

[ModuleInfo(BossModuleInfo.Maturity.WIP, PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1006, NameID = 9707, PlanLevel = 100)]
[ModuleInfo(BossModuleInfo.Maturity.Verified, PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1006, NameID = 9707, PlanLevel = 100)]
public class FRU(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena with { IsCircle = true })
{
private static readonly ArenaBoundsComplex arena = new([new Polygon(new(100, 100), 20, 64)]);
Expand Down
2 changes: 1 addition & 1 deletion BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public class FRUConfig() : ConfigNode()
public GroupAssignmentUnique P2MirrorMirror1SpreadSpots = new() { Assignments = [0, 1, 4, 5, 2, 3, 6, 7] };

[PropertyDisplay("P2 Mirror Mirror: spread spots for second proteans (looking toward red mirror, if both red mirrors are symmetrical assume CW rotation)", tooltip: "Only used by AI")]
[GroupDetails(["Boss wall right", "Boss wall left", "Boss center", "Boss diagonal", "Mirror wall right", "Mirror wall left", "Mirror center", "Mirror diagonal"])]
[GroupDetails(["Boss wall opposite other", "Boss wall facing other", "Boss center", "Boss diagonal", "Mirror wall right", "Mirror wall left", "Mirror center right", "Mirror center left"])]
[GroupPreset("Default", [1, 0, 6, 7, 2, 3, 4, 5])]
public GroupAssignmentUnique P2MirrorMirror2SpreadSpots = new() { Assignments = [1, 0, 6, 7, 2, 3, 4, 5] };

Expand Down
4 changes: 3 additions & 1 deletion BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public enum AID : uint

MemorysEndP4 = 40305, // OracleOfDarknessP4->self, 10.0s cast, range 100 circle, enrage
AbsoluteZeroP4 = 40245, // UsurperOfFrostP4->self, 10.0s cast, range 100 circle, enrage
ParadiseLost = 40263, // Helper->self, no cast, range 100 circle, wipe on p5 failure state
ParadiseLostP4 = 40263, // Helper->self, no cast, range 100 circle, wipe on p5 failure state
IntermissionP5Visual = 40231, // UsurperOfFrostP4->self, no cast, single-target, visual (intermission start)
IntermissionP5Start = 40232, // Helper->self, no cast, range 60 circle, stun + move players to a specific spot

Expand Down Expand Up @@ -287,6 +287,8 @@ public enum AID : uint
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
ParadiseLostP5 = 40327, // BossP5->self, 12.0+9.5s cast, range 100 circle, visual (enrage)
ParadiseLostP5AOE = 40328, // Helper->self, 21.5s cast, range 100 circle, wipe
}

public enum SID : uint
Expand Down
26 changes: 16 additions & 10 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,10 @@ private void Phase5(uint id)
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, "???");
P5ParadiseRegained(id + 0x60000, 8.4f);
P5PolarizingStrikes(id + 0x70000, 2.3f);
P5FulgentBlade(id + 0x80000, 2.6f);
P5Enrage(id + 0x90000, 8.4f);
}

private void P1CyclonicBreakPowderMarkTrail(uint id, float delay)
Expand Down Expand Up @@ -316,7 +315,7 @@ private void P2MirrorMirror(uint id, float delay)
.DeactivateOnExit<P2MirrorMirrorHouseOfLight>();

ActorCastMulti(id + 0x100, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 0.5f, 5, true)
.ActivateOnEnter<P2Banish1>();
.ActivateOnEnter<P2MirrorMirrorBanish>();
ComponentCondition<P2Banish>(id + 0x102, 0.1f, comp => !comp.Active, "Spread/Stack")
.DeactivateOnExit<P2Banish>();
}
Expand Down Expand Up @@ -355,10 +354,8 @@ private void P2LightRampant(uint id, float delay)
.DeactivateOnExit<P2LightRampant>(); // tethers resolve right after first orbs

ActorCastStartMulti(id + 0x70, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 1.7f, true)
.ActivateOnEnter<P2LightRampantAIResolve>();
.ActivateOnEnter<P2LightRampantBanish>();
ComponentCondition<P2BrightHunger2>(id + 0x71, 1.9f, comp => comp.NumCasts > 0, "Central tower")
.ActivateOnEnter<P2Banish2>()
.DeactivateOnExit<P2LightRampantAIResolve>()
.DeactivateOnExit<P2BrightHunger2>()
.DeactivateOnExit<P2SinboundHolyVoidzone>();
ActorCastEnd(id + 0x72, _module.BossP2, 3.1f, true);
Expand Down Expand Up @@ -530,8 +527,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)
ActorCastStart(id + 0x10, _module.BossP4Usurper, AID.Materialization, 5.1f, true)
.ActivateOnEnter<P4AkhRhai>();
ActorCastEnd(id + 0x11, _module.BossP4Usurper, 3, true);
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");
Expand Down Expand Up @@ -722,4 +720,12 @@ private void P5PandorasBox(uint id, float delay)
ActorCast(id, _module.BossP5, AID.PandorasBox, delay, 12, true, "Tank LB")
.SetHint(StateMachine.StateHint.Raidwide);
}

private void P5Enrage(uint id, float delay)
{
ActorCast(id, _module.BossP5, AID.ParadiseLostP5, delay, 12, true);
ComponentCondition<P5ParadiseLost>(id + 2, 9.5f, comp => comp.NumCasts > 0, "Enrage")
.ActivateOnEnter<P5ParadiseLost>()
.DeactivateOnExit<P5ParadiseLost>();
}
}
89 changes: 0 additions & 89 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,92 +29,3 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell)
}
}
}

// this variant provides hints after mirrors
class P2Banish1(BossModule module) : P2Banish(module)
{
public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
var prepos = PrepositionLocation(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)
{
// 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;
}
}

// this variant provides hints after rampant
class P2Banish2(BossModule module) : P2Banish(module)
{
private readonly FRUConfig _config = Service.Config.Get<FRUConfig>();
private bool _allowHints;

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
if (!_allowHints)
return; // don't interfere with tower hints until it's done
var prepos = PrepositionLocation(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)
{
var clockspot = _config.P2Banish2SpreadSpots[assignment];
if (clockspot < 0)
return null; // no assignment

var assignedDirection = (180 - 45 * clockspot).Degrees();
if (Stacks.Count > 0 && Stacks[0].Activation > WorldState.FutureTime(1))
{
var isSupport = assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.OT or PartyRolesConfig.Assignment.H1 or PartyRolesConfig.Assignment.H2;
if (_config.P2Banish2SupportsMoveToStack == isSupport)
assignedDirection += (_config.P2Banish2MoveCCWToStack ? 45 : -45).Degrees();
return Module.Center + 10 * assignedDirection.ToDirection();
}
else if (Spreads.Count > 0 && Spreads[0].Activation > WorldState.FutureTime(1))
{
return Module.Center + 13 * assignedDirection.ToDirection();
}
return null;
}

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
base.OnEventCast(caster, spell);
if ((AID)spell.Action.ID == AID.BrightHunger)
_allowHints = true;
}
}
20 changes: 17 additions & 3 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ class P2TwinStillnessSilence(BossModule module) : Components.GenericAOEs(module)
public readonly List<AOEInstance> AOEs = [];
private readonly Actor? _source = module.Enemies(OID.OraclesReflection).FirstOrDefault();
private BitMask _thinIce;
private readonly WPos[] _slideBackPos = new WPos[PartyState.MaxPartySize]; // used for hints only
private P2SinboundHolyVoidzone? _voidzones; // used for hints only
private const float SlideDistance = 32;

Expand All @@ -378,6 +379,15 @@ public void EnableAIHints()

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

public override void Update()
{
if (AOEs.Count != 2)
return;
foreach (var (i, p) in Raid.WithSlot().IncludedInMask(_thinIce))
if (_slideBackPos[i] == default && p.LastFrameMovement != default)
_slideBackPos[i] = p.PrevPosition;
}

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
if (_voidzones == null || _source == null)
Expand Down Expand Up @@ -446,7 +456,6 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
{
// if we're behind boss, slide over
zoneList.ForbidInfiniteRect(Arena.Center, Angle.FromDirection(sourceOffset), Arena.Bounds.Radius);
//zoneList.ForbidCircle(_source.Position, 20);
}
else
{
Expand All @@ -455,12 +464,17 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
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)
// prefer to return to the starting spot, for more natural preposition for next mechanic
if (AOEs.Count == 1 && _slideBackPos[slot] != default && !zoneList.Forbidden.Contains(Angle.FromDirection(_slideBackPos[slot] - actor.Position).Rad))
{
hints.AddForbiddenZone(ShapeDistance.InvertedCircle(_slideBackPos[slot], 1), DateTime.MaxValue);
}
else if (zoneList.Allowed(1.Degrees()).MaxBy(r => (r.max - r.min).Rad) is var best && 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: no good direction can be found, wait for a bit, maybe voidzone will disappear
}
// else: we are already sliding, nothing to do...
}
Expand Down
20 changes: 20 additions & 0 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P2HallowedRay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

class P2HallowedRay(BossModule module) : Components.GenericWildCharge(module, 3, ActionID.MakeSpell(AID.HallowedRayAOE), 65)
{
public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
if (Source == null)
return;
if (PlayerRoles[slot] == PlayerRole.Target || Activation > WorldState.FutureTime(2.5f))
{
// stay at the average direction to the raid
// TODO: for melees, allow doing positionals...
WDir averageDir = default;
foreach (var p in Raid.WithoutSlot(false, true, true))
averageDir += (p.Position - Source.Position).Normalized();
hints.AddForbiddenZone(ShapeDistance.InvertedRect(Source.Position, Angle.FromDirection(averageDir), 20, -6, 2), Activation);
}
else
{
// default hints (go to target)
base.AddAIHints(slot, actor, assignment, hints);
}
}

public override void OnEventIcon(Actor actor, uint iconID, ulong targetID)
{
if (iconID == (uint)IconID.HallowedRay)
Expand Down
107 changes: 83 additions & 24 deletions BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,89 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell)
if (Towers.Count == 0 && (AID)spell.Action.ID == AID.HolyLightBurst)
Towers.Add(new(Module.Center, 4, 1, 8, _forbidden, WorldState.FutureTime(6.5f)));
}

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
if (spell.Action == WatchedAction)
{
Towers.Clear();
++NumCasts;
}
}
}

// note: this also moves to soak or avoid the central tower, because these mechanics overlap
class P2LightRampantBanish(BossModule module) : P2Banish(module)
{
private readonly FRUConfig _config = Service.Config.Get<FRUConfig>();
private readonly P2BrightHunger2? _tower = module.FindComponent<P2BrightHunger2>();

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
if (_tower?.Towers.Count > 0)
{
// first deal with towers
ref var t = ref _tower.Towers.Ref(0);
if (t.ForbiddenSoakers[slot])
{
// we should not be soaking a tower
hints.AddForbiddenZone(ShapeDistance.Circle(t.Position, t.Radius), t.Activation);
hints.AddForbiddenZone(ShapeDistance.Circle(t.Position, t.Radius + 2), WorldState.FutureTime(30));

var prepos = PrepositionLocation(assignment);
if (prepos != null)
{
// we know the mechanic, so preposition immediately
// there might be puddles covering prepos spot, so add extra rougher hint
hints.AddForbiddenZone(ShapeDistance.InvertedCircle(prepos.Value, 1), DateTime.MaxValue);
hints.AddForbiddenZone(ShapeDistance.InvertedCone(Module.Center, 50, Angle.FromDirection(prepos.Value - Module.Center), 15.Degrees()), WorldState.FutureTime(60));
}
}
else
{
// go soak the tower, somewhat offset to the assigned direction
hints.AddForbiddenZone(ShapeDistance.InvertedCircle(t.Position, t.Radius), t.Activation);
hints.AddForbiddenZone(ShapeDistance.InvertedCircle(t.Position, t.Radius - 2), DateTime.MaxValue);

var clockspot = _config.P2Banish2SpreadSpots[assignment];
if (clockspot >= 0)
{
var assignedDirection = (180 - 45 * clockspot).Degrees();
hints.AddForbiddenZone(ShapeDistance.InvertedCone(t.Position, 50, assignedDirection, 30.Degrees()), DateTime.MaxValue);
}
}
}
else
{
// now that towers are done, resolve the spread/stack
var prepos = PrepositionLocation(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)
{
var clockspot = _config.P2Banish2SpreadSpots[assignment];
if (clockspot < 0)
return null; // no assignment

var assignedDirection = (180 - 45 * clockspot).Degrees();
if (Stacks.Count > 0 && Stacks[0].Activation > WorldState.FutureTime(1))
{
var isSupport = assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.OT or PartyRolesConfig.Assignment.H1 or PartyRolesConfig.Assignment.H2;
if (_config.P2Banish2SupportsMoveToStack == isSupport)
assignedDirection += (_config.P2Banish2MoveCCWToStack ? 45 : -45).Degrees();
return Module.Center + 10 * assignedDirection.ToDirection();
}
else if (Spreads.Count > 0 && Spreads[0].Activation > WorldState.FutureTime(1))
{
return Module.Center + 13 * assignedDirection.ToDirection();
}
return null;
}
}

class P2HouseOfLightBoss(BossModule module) : Components.GenericBaitAway(module, ActionID.MakeSpell(AID.HouseOfLightBossAOE), false)
Expand Down Expand Up @@ -328,27 +411,3 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
}
}
}

// movement to soak central tower (if needed) and preposition for banish
class P2LightRampantAIResolve(BossModule module) : BossComponent(module)
{
private readonly FRUConfig _config = Service.Config.Get<FRUConfig>();
private readonly P2BrightHunger2? _tower = module.FindComponent<P2BrightHunger2>();

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
if (_tower == null || _tower.Towers.Count == 0)
return; // no towers

ref var t = ref _tower.Towers.Ref(0);
hints.AddForbiddenZone(t.ForbiddenSoakers[slot] ? ShapeDistance.Circle(t.Position, t.Radius) : ShapeDistance.InvertedCircle(t.Position, t.Radius), t.Activation);
hints.AddForbiddenZone(t.ForbiddenSoakers[slot] ? ShapeDistance.Circle(t.Position, t.Radius + 2) : ShapeDistance.InvertedCircle(t.Position, t.Radius - 2), DateTime.MaxValue);

var clockspot = _config.P2Banish2SpreadSpots[assignment];
if (clockspot >= 0)
{
var assignedDirection = (180 - 45 * clockspot).Degrees();
hints.AddForbiddenZone(ShapeDistance.InvertedCone(t.Position, 50, assignedDirection, 30.Degrees()), DateTime.MaxValue);
}
}
}
Loading

0 comments on commit 2667b11

Please sign in to comment.