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 vbm (FRU verified) #569

Merged
merged 6 commits into from
Jan 21, 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
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