From 82618b0299ebf1024a72f19eb46dc2bd9051a965 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Thu, 26 Dec 2024 16:39:02 +0000 Subject: [PATCH 1/3] FRU & chaotic WIP --- .../ActivePivotParticleBeam.cs | 24 ++ .../Ch01CloudOfDarkness/BladeOfDarkness.cs | 29 +++ .../Chaotic/Ch01CloudOfDarkness/Break.cs | 20 ++ .../Ch01CloudOfDarkness.cs | 189 ++++++++++++++ .../Ch01CloudOfDarknessEnums.cs | 199 +++++++++++++++ .../ChaosCondensedParticleBeam.cs | 21 ++ .../Ch01CloudOfDarkness/CurseOfDarkness.cs | 31 +++ .../DiffusiveForceParticleBeam.cs | 18 ++ .../Chaotic/Ch01CloudOfDarkness/Enaero.cs | 109 ++++++++ .../Chaotic/Ch01CloudOfDarkness/Endeath.cs | 116 +++++++++ .../Ch01CloudOfDarkness/GrimEmbrace.cs | 68 +++++ .../Chaotic/Ch01CloudOfDarkness/Phaser.cs | 15 ++ .../RapidSequenceParticleBeam.cs | 35 +++ .../RazingVolleyParticleBeam.cs | 16 ++ .../Ch01CloudOfDarkness/ThirdArtOfDarkness.cs | 105 ++++++++ .../Ch01CloudOfDarkness/UnholyDarkness.cs | 14 + BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs | 2 + .../Dawntrail/Ultimate/FRU/FRUConfig.cs | 5 + .../Dawntrail/Ultimate/FRU/FRUEnums.cs | 31 ++- .../Dawntrail/Ultimate/FRU/FRUStates.cs | 78 +++++- .../Dawntrail/Ultimate/FRU/P1Blastburn.cs | 7 + .../Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs | 9 +- .../Dawntrail/Ultimate/FRU/P4AkhMorn.cs | 18 ++ .../FRU/{P4AhkRhai.cs => P4AkhRhai.cs} | 2 +- .../Ultimate/FRU/P4CrystallizeTime.cs | 6 + .../Ultimate/FRU/P4DarklitDragonsong.cs | 241 ++++++++++++++++++ .../Dawntrail/Ultimate/FRU/P4MornAfah.cs | 35 +++ 27 files changed, 1432 insertions(+), 11 deletions(-) create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/BladeOfDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ChaosCondensedParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/GrimEmbrace.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Phaser.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RapidSequenceParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RazingVolleyParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ThirdArtOfDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/UnholyDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs rename BossMod/Modules/Dawntrail/Ultimate/FRU/{P4AhkRhai.cs => P4AkhRhai.cs} (88%) create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs new file mode 100644 index 000000000..6052f981b --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs @@ -0,0 +1,24 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class ActivePivotParticleBeam(BossModule module) : Components.GenericRotatingAOE(module) +{ + private static readonly AOEShapeRect _shape = new(40, 9, 40); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + var rotation = (AID)spell.Action.ID switch + { + AID.ActivePivotParticleBeamCW => -22.5f.Degrees(), + AID.ActivePivotParticleBeamCCW => 22.5f.Degrees(), + _ => default + }; + if (rotation != default) + Sequences.Add(new(_shape, caster.Position, spell.Rotation, rotation, Module.CastFinishAt(spell, 0.6f), 1.6f, 5)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.ActivePivotParticleBeamAOE && Sequences.Count > 0) + AdvanceSequence(0, WorldState.CurrentTime); + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/BladeOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/BladeOfDarkness.cs new file mode 100644 index 000000000..7f651ce03 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/BladeOfDarkness.cs @@ -0,0 +1,29 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class BladeOfDarkness(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + + private static readonly AOEShapeDonutSector _shapeIn = new(12, 60, 75.Degrees()); + private static readonly AOEShapeCone _shapeOut = new(30, 90.Degrees()); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + AOEShape? shape = (AID)spell.Action.ID switch + { + AID.BladeOfDarknessLAOE or AID.BladeOfDarknessRAOE => _shapeIn, + AID.BladeOfDarknessCAOE => _shapeOut, + _ => null + }; + if (shape != null) + _aoe = new(shape, spell.LocXZ, spell.Rotation, Module.CastFinishAt(spell)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.BladeOfDarknessLAOE or AID.BladeOfDarknessRAOE or AID.BladeOfDarknessCAOE) + _aoe = null; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs new file mode 100644 index 000000000..0de99df6c --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs @@ -0,0 +1,20 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class Break(BossModule module) : Components.GenericGaze(module) +{ + private readonly List _eyes = []; + + public override IEnumerable ActiveEyes(int slot, Actor actor) => _eyes;//_casters.Where(c => c.CastInfo?.TargetID != actor.InstanceID).Select(c => new Eye(EyePosition(c), Module.CastFinishAt(c.CastInfo), Range: range)); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.BreakBoss or AID.BreakEye) + _eyes.Add(new(caster.Position, Module.CastFinishAt(spell, 0.9f))); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.BreakBossAOE or AID.BreakEyeAOE) + _eyes.RemoveAll(eye => eye.Position.AlmostEqual(caster.Position, 1)); + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs new file mode 100644 index 000000000..1b8a0cfc1 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs @@ -0,0 +1,189 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class DelugeOfDarkness1(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DelugeOfDarkness1)); +class Flare(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(25), (uint)IconID.Flare, ActionID.MakeSpell(AID.FlareAOE), 8.1f, true); +class FloodOfDarkness(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.FloodOfDarkness)); +class DelugeOfDarkness2(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DelugeOfDarkness2)); +class StygianShadow(BossModule module) : Components.Adds(module, (uint)OID.StygianShadow); +class Atomos(BossModule module) : Components.Adds(module, (uint)OID.Atomos); +class DarkDominion(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DarkDominion)); +class GhastlyGloomCross(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GhastlyGloomCross), new AOEShapeCross(40, 15)); +class GhastlyGloomDonut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GhastlyGloomDonut), new AOEShapeDonut(21, 40)); +class FloodOfDarknessAdd(BossModule module) : Components.CastInterruptHint(module, ActionID.MakeSpell(AID.FloodOfDarknessAdd)); // TODO: only if add is player's?.. +class Excruciate(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.Excruciate), new AOEShapeCircle(4), true); +class LoomingChaos(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.LoomingChaosAOE), "Raidwide + swap positions"); + +class Ch01CloudOfDarknessStates : StateMachineBuilder +{ + public Ch01CloudOfDarknessStates(BossModule module) : base(module) + { + DeathPhase(0, SinglePhase); + } + + private void SinglePhase(uint id) + { + SimpleState(id + 0xFF0000, 10000, "???") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +// TODO: mechanic phase bounds +// TODO: flood bounds & squares +// TODO: particle concentration towers +// TODO: evil seed +// TODO: chaser beam +// TODO: tankswap hints? +[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624)] +public class Ch01CloudOfDarkness(WorldState ws, Actor primary) : BossModule(ws, primary, DefaultCenter, InitialBounds) +{ + public static readonly WPos DefaultCenter = new(100, 100); + public static readonly ArenaBoundsCircle InitialBounds = new(40); + public static readonly ArenaBoundsCustom Phase1Bounds = new(InitialBounds.Radius, new(BuildPhase1BoundsContour())); + public static readonly ArenaBoundsCustom Phase2Bounds = new(InitialBounds.Radius, BuildPhase2BoundsPoly()); + public static readonly WPos Phase1Midpoint = DefaultCenter + Phase1Bounds.Poly.Parts[0].Vertices[1] + Phase1Bounds.Poly.Parts[0].Vertices[3]; + + public static List BuildPhase1BoundsContour() + { + // north 'diagonal' is at [+/-15, -37] (it almost intersects the initial circle - at x=15 z is ~37.08) + // the main diagonal is 20, rotated by 45 degrees, which means that side corners are at x=+/- 40/sqrt(2), z = -37 + 40/sqrt(2) - 15 + var nz = -37; + var nx = 15; + var halfDiag = 40 / MathF.Sqrt(2); + var cz = nz + halfDiag - nx; + return [new(nx, nz), new(halfDiag, cz), new(0, cz + halfDiag), new(-halfDiag, cz), new(-nx, nz)]; + } + + public static RelSimplifiedComplexPolygon BuildPhase2BoundsPoly() + { + // mid is union of 4 rects + var midHalfWidth = 3; + var midHalfLength = 24; + var midOffset = 15; + var op1 = new PolygonClipper.Operand(); + var op2 = new PolygonClipper.Operand(); + op1.AddContour(CurveApprox.Rect(new WDir(0, +midOffset), new(1, 0), midHalfWidth, midHalfLength)); + op1.AddContour(CurveApprox.Rect(new WDir(0, -midOffset), new(1, 0), midHalfWidth, midHalfLength)); + op2.AddContour(CurveApprox.Rect(new WDir(+midOffset, 0), new(0, 1), midHalfWidth, midHalfLength)); + op2.AddContour(CurveApprox.Rect(new WDir(-midOffset, 0), new(0, 1), midHalfWidth, midHalfLength)); + var mid = InitialBounds.Clipper.Union(op1, op2); + + // sides is union of two platforms and the outside ring + var sideHalfWidth = 7.5f; + var sideHalfLength = 10; + var sideOffset = 19 + sideHalfLength; + var sideRingWidth = 6; + op1.Clear(); + op2.Clear(); + op1.AddContour(CurveApprox.Rect(new WDir(+sideOffset, 0), new(1, 0), sideHalfWidth, sideHalfLength)); + op1.AddContour(CurveApprox.Rect(new WDir(-sideOffset, 0), new(1, 0), sideHalfWidth, sideHalfLength)); + op2.AddContour(CurveApprox.Circle(InitialBounds.Radius, 0.1f)); + op2.AddContour(CurveApprox.Circle(InitialBounds.Radius - sideRingWidth, 0.1f)); + var side = InitialBounds.Clipper.Union(op1, op2); + + op1.Clear(); + op2.Clear(); + op1.AddPolygon(mid); + op2.AddPolygon(side); + return InitialBounds.Clipper.Union(op1, op2); + } +} + +// envcontrols: +// 00 = main bounds telegraph +// - 00200010 - phase 1 +// - 00020001 - phase 2 +// - 00040004 - remove telegraph (note that actual bounds are controlled by something else!) +// 02 = outer ring +// - 00020001 - become dangerous +// - 00080004 - restore to normal +// 03-1E = mid squares +// - 08000001 - init +// - 00200010 - become occupied +// - 02000001 - become free +// - 00800040 - player is standing for too long, will break soon +// - 00080004 - break +// - 00020001 - repair +// - arrangement: +// 04 0B +// 03 05 06 07 0E 0D 0C 0A +// 08 0F +// 09 10 +// 17 1E +// 16 1D +// 11 13 14 15 1C 1B 1A 18 +// 12 19 +// 1F-2E = 1-man towers +// - 00020001 - appear +// - 00200010 - occupied +// - 00080004 - disappear +// - 08000001 - ? (spot animation) +// - arrangement: +// 25 26 +// 21 xx 1F xx xx 20 xx 22 +// 23 24 +// xx xx +// xx xx +// 2B 2C +// 29 xx 27 xx xx 28 xx 2A +// 2D 2E +// 2F-3E = 2-man towers +// - 00020001 - appear +// - 00200010 - occupied by 1 +// - 00800040 - occupied by 2 +// - 00080004 - disappear +// - 08000001 - ? (spot animations) +// - arrangement (also covers intersecting square): +// 35 36 +// 31 xx 2F xx xx 30 xx 32 +// 33 34 +// xx xx +// xx xx +// 3B 3C +// 39 xx 37 xx xx 38 xx 3A +// 3D 3E +// 3F-46 = 3-man towers +// - 00020001 - appear +// - 00200010 - occupied by 1 +// - 00800040 - occupied by 2 +// - 02000100 - occupied by 3 +// - 00080004 - disappear +// - 08000001 - ? (spot animations) +// - arrangement: +// 3F 43 +// 42 40 44 46 +// 41 45 +// 47-56 = 1-man tower falling orb +// 57-66 = 2-man tower falling orb +// 67-6E = 3-man tower falling orb + diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs new file mode 100644 index 000000000..617023a81 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs @@ -0,0 +1,199 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +public enum OID : uint +{ + Boss = 0x461E, // R23.000, x1 + Helper = 0x233C, // R0.500, x24, Helper type + StygianShadow = 0x461F, // R4.000, x0 (spawn during fight), big add + Atomos = 0x4620, // R2.800, x0 (spawn during fight), small add + DeathsHand = 0x4621, // R2.000, x6, grim embrace hand + StygianTendrils = 0x4622, // R1.200, x0 (spawn during fight), evil seed + CloudletOfDarkness = 0x4623, // R3.000, x0 (spawn during fight), criss-cross source + BallOfNaught = 0x4624, // R1.500, x1, (en)death sphere + //_Gen_DreadGale = 0x4625, // R1.200, x1 + SinisterEye = 0x4626, // R2.800, x2, break gaze source + AtomosSpawnPoint = 0x1EBD7B, // R0.500, x0 (spawn during fight), EventObj type + EvilSeed = 0x1E9B3B, // R0.500, x0 (spawn during fight), EventObj type + //_Gen_Actor1e8536 = 0x1E8536, // R2.000, x1, EventObj type + //_Gen_Exit = 0x1E850B, // R0.500, x1, EventObj type +} + +public enum AID : uint +{ + AutoAttackNormalPrimary = 40441, // Boss->player, no cast, single-target + AutoAttackNormalSecondary = 40442, // Helper->player, no cast, single-target + AutoAttackVulnPrimary = 41348, // Boss->player, no cast, single-target + AutoAttackVulnSecondary = 41349, // Helper->player, no cast, single-target + Teleport1 = 40534, // Boss->location, no cast, single-target + BladeOfDarknessL = 40443, // Boss->self, 7.0+0.7s cast, single-target, visual (left hand safe) + BladeOfDarknessLAOE = 40444, // Helper->self, 7.7s cast, range 12-60 150-degree cone donut + BladeOfDarknessR = 40445, // Boss->self, 7.0+0.7s cast, single-target, visual (right hand safe) + BladeOfDarknessRAOE = 40446, // Helper->self, 7.7s cast, range 12-60 150-degree cone donut + BladeOfDarknessC = 40447, // Boss->self, 7.0+0.7s cast, single-target, visual (out safe) + BladeOfDarknessCAOE = 40448, // Helper->self, 7.7s cast, range 30 180-degree cone + + DelugeOfDarkness1 = 40509, // Boss->location, 8.0s cast, range 60 circle, raidwide + arena transition + GrimEmbraceForward = 40505, // Boss->self, 5.0s cast, tethered players spawn aoe forward + GrimEmbraceBackward = 40506, // Boss->self, 5.0s cast, tethered players spawn aoe backward + GrimEmbraceVisual = 40507, // DeathsHand->self, no cast, single-target, visual (hand spawn) + GrimEmbraceAOE = 40508, // Helper->self, 1.0s cast, range 8 width 8 rect + RazingVolleyParticleBeam = 40511, // CloudletOfDarkness->self, 8.0s cast, range 45 width 8 rect, criss-cross + + RapidSequenceParticleBeam = 40512, // Boss->self, 7.0+0.7s cast, single-target, visual (party line stacks) + RapidSequenceParticleBeamRepeat = 40513, // Boss->self, no cast, single-target, visual (repeat) + RapidSequenceParticleBeamAOE = 40514, // Helper->self, no cast, range 50 width 6 rect + Death = 40515, // Boss->self, 5.6+0.7s cast, single-target, visual (attract to out/in) + DeathVortex = 40516, // Helper->self, 2.0s cast, range 40 circle, attract 15 + DeathAOE1 = 40517, // Helper->self, 4.0s cast, range 6 circle + DeathAOE2 = 40518, // BallOfNaught->self, 6.0s cast, range 6-40 donut + Endeath = 40531, // Boss->self, 5.0s cast, single-target, visual (delayed attract) + EndeathVortex = 40519, // Helper->self, 1.0s cast, range 40 circle, attract 15 + EndeathAOE1 = 40520, // Helper->self, 3.0s cast, range 6 circle + EndeathAOE2 = 40521, // BallOfNaught->self, 5.0s cast, range 6-40 donut + Aero = 40524, // Boss->self, 5.6+0.7s cast, single-target, visual (knockback) + AeroKnockback = 40522, // Helper->self, 2.0s cast, range 40 circle, knockback 15 + AeroAOE = 40523, // 4625->self, 2.0s cast, range 8 circle + Enaero = 40532, // Boss->self, 5.0s cast, single-target, visual (delayed knockback) + EnaeroKnockback = 40525, // Helper->self, 1.0s cast, range 40 circle, knockback 15 + EnaeroAOE = 40526, // 4625->self, 1.0s cast, range 8 circle + BreakBoss = 40527, // Boss->self, 5.0+1.0s cast, single-target, visual (gazes) + BreakBossAOE = 40528, // Helper->self, 6.0s cast, range 60 circle, gaze + BreakEye = 40529, // SinisterEye->self, 3.0+1.0s cast, single-target, visual (gaze) + BreakEyeAOE = 40530, // Helper->self, 4.0s cast, range 60 circle, gaze + Flare = 40536, // Boss->self, 4.0s cast, single-target, visual (flares) + FlareAOE = 40537, // Helper->players, no cast, range 60 circle with 25 falloff + UnholyDarkness = 41261, // Boss->self, 5.0s cast, single-target, visual (4-man stacks on healers) + UnholyDarknessAOE = 41262, // Helper->players, no cast, range 6 circle, 4-man stack + + FloodOfDarkness = 40510, // Boss->location, 7.0s cast, range 60 circle, raidwide + arena transition to normal + DelugeOfDarkness2 = 40449, // Boss->location, 8.0s cast, range 60 circle, raidwide + arena transition + Teleport2 = 40450, // Boss->location, no cast, single-target + AutoAttackAdd = 40501, // StygianShadow->player, no cast, single-target + DarkDominion = 40456, // Boss->self, 5.0s cast, range 60 circle, raidwide + TeleportAdd = 40494, // StygianShadow->location, no cast, single-target + FloodOfDarknessAdd = 40503, // StygianShadow->self, 6.0s cast, range 40 circle, interruptible raidwide + + ThirdArtOfDarknessR = 40480, // StygianShadow->self, 10.0+0.4s cast, single-target, visual (right first) + ThirdArtOfDarknessL = 40483, // StygianShadow->self, 10.0+0.4s cast, single-target, visual (left first) + ArtOfDarknessNextR = 40481, // StygianShadow->self, no cast, single-target, visual (next right) + ArtOfDarknessAOER = 40482, // Helper->self, no cast, range 15 180-degree cone + ArtOfDarknessNextL = 40484, // StygianShadow->self, no cast, single-target, visual (next left) + ArtOfDarknessAOEL = 40485, // Helper->self, no cast, range 15 ?-degree cone + HyperFocusedParticleBeamNext = 40486, // StygianShadow->self, no cast, single-target, visual (next spread) + HyperFocusedParticleBeamAOE = 40487, // Helper->self, no cast, range 22 width 5 rect protean + MultiProngedParticleBeamNext = 40488, // StygianShadow->self, no cast, single-target, visual (next pairs) + MultiProngedParticleBeamAOE = 40489, // Helper->players, no cast, range 3 circle, 2-man stack + + ParticleConcentration = 40472, // Boss->self, 6.0s cast, single-target, visual (towers) + ParticleBeam1 = 40474, // Helper->location, no cast, range 3 circle, 1-man tower + ParticleBeam2 = 40475, // Helper->location, no cast, range 3 circle, 2-man tower + ParticleBeam3 = 40476, // Helper->location, no cast, range 3 circle, 3-man tower + ParticleBeam1Fail = 40473, // Helper->self, no cast, range 80 circle, raidwide if 1-man tower is not soaked + ParticleBeam2Fail = 41346, // Helper->self, no cast, range 80 circle, raidwide if 2-man tower is not soaked + ParticleBeam3Fail = 41347, // Helper->self, no cast, range 80 circle, raidwide if 3-man tower is not soaked + + GhastlyGloomCross = 40457, // Boss->self, 7.8+0.7s cast, single-target, visual (cross) + GhastlyGloomCrossAOE = 40458, // Helper->self, 8.5s cast, range 40 width 30 cross + GhastlyGloomDonut = 40459, // Boss->self, 7.8+0.7s cast, single-target, visual (donut) + GhastlyGloomDonutAOE = 40460, // Helper->self, 8.5s cast, range 21-40 donut + + CurseOfDarkness = 40498, // StygianShadow->self, 2.0s cast, single-target, visual (raidwide with debuff that causes dark energy particle beam on expire) + CurseOfDarknessAOE = 40499, // Helper->self, 3.0s cast, range 40 circle, raidwide + DarkEnergyParticleBeam = 40500, // Helper->self, no cast, range 25 15?-degree cone + + EvilSeed = 40490, // StygianShadow->self, 7.0s cast, single-target, visual (seeds plant) + EvilSeedAOE = 40491, // Helper->location, 5.0s cast, range 5 circle, puddle when seed is planted + ThornyVine = 40492, // StygianShadow->self, 8.0s cast, single-target, visual (seeds tether) + ThornyVineAOE = 40493, // Helper->self, no cast, ??? (if tethers weren't broken?) + + ChaosCondensedParticleBeam = 40461, // Boss->self, 8.0+0.7s cast, single-target, visual (wild charges) + ChaosCondensedParticleBeamAOE1 = 40462, // Helper->self, no cast, range 50 width 6 rect, 6-man wild charge on platforms + ChaosCondensedParticleBeamAOE2 = 40463, // Helper->self, no cast, range 50 width 6 rect, 3-man wild charge on mid + DiffusiveForceParticleBeam = 40464, // Boss->self, 8.0+0.7s cast, single-target, visual (spread) + DiffusiveForceParticleBeamAOE1 = 40465, // Helper->players, no cast, range 7 circle, first wave (any specific targeting?) + DiffusiveForceParticleBeamAOE2 = 40466, // Helper->players, no cast, range 5 circle, second wave + + LateralCorePhaser = 40495, // StygianShadow->self, 6.0+2.0s cast, single-target, visual (sides > front) + CoreLateralPhaser = 40496, // StygianShadow->self, 6.0+2.0s cast, single-target, visual (front > sides) + Phaser = 40497, // Helper->self, 8.0s cast, range 23 ?-degree cone + + ActivePivotParticleBeamCW = 40467, // Boss->self, 14.0+0.5s cast, single-target, visual (cw rotation) + ActivePivotParticleBeamCWRepeat = 40468, // Boss->self, no cast, single-target + ActivePivotParticleBeamCCW = 40469, // Boss->self, 14.0+0.5s cast, single-target, visual (ccw rotation) + ActivePivotParticleBeamCCWRepeat = 40470, // Boss->self, no cast, single-target + ActivePivotParticleBeamAOE = 40471, // Helper->self, no cast, range 80 width 18 rect + + Excruciate = 40502, // StygianShadow->player, 5.0s cast, range 4 circle, tankbuster + LoomingChaosAdd = 41673, // StygianShadow->self, 7.0s cast, single-target, visual (position swaps) + LoomingChaosBoss = 41674, // Boss->self, 7.0s cast, single-target, visual (position swaps) + LoomingChaosAOE = 41675, // Helper->self, 7.7s cast, range 50 circle, raidwide + position swaps + + //_Weaponskill_FeintParticleBeam = 40477, // Boss->self, 6.0+0.7s cast, single-target +} + +public enum SID : uint +{ + //_Gen_ArcaneDesign = 4180, // Boss->Boss, extra=0x0 + //_Gen_LightningResistanceDown = 4386, // Helper/Boss->player, extra=0x1/0x2/0x3/0x4/0x5/0x6/0x7/0x8/0x9/0xA/0xB/0xC/0xD/0xE/0xF/0x10 + DeadlyEmbrace = 4181, // none->player, extra=0x0 + //_Gen_ = 2970, // none->player, extra=0x358 + //_Gen_Heavy = 1595, // none->player, extra=0x4B + AbyssalEdge = 4182, // Boss->Boss, extra=0x0 (endeath/enaero stored) + //_Gen_Doom = 3364, // Helper/BallOfNaught->player, extra=0x0 + //_Gen_MagicVulnerabilityUp = 2941, // Helper->player, extra=0x0 + //_Gen_BrinkOfDeath = 44, // none->player, extra=0x0 + //_Gen_Petrification = 3007, // Helper->player, extra=0x0 + //_Gen_VeilOfDarkness = 4179, // Boss->Boss, extra=0x0 + //_Gen_CloyingCondensation = 2532, // none->player, extra=0x0 + //_Gen_ = 4388, // none->StygianShadow, extra=0x1052 + //_Gen_ = 4387, // none->Boss, extra=0x1051 + //_Gen_InnerDarkness = 4177, // none->player, extra=0x0 + //_Gen_OuterDarkness = 4178, // none->player, extra=0x0 + //_Gen_VulnerabilityDown = 2198, // none->StygianShadow/Boss, extra=0x0 + //_Gen_DamageUp = 3129, // none->StygianShadow/Boss, extra=0x0 + //_Gen_Rehabilitation = 4191, // none->Boss, extra=0x1/0x4/0x3/0x2 + //_Gen_LifeDrain = 1377, // none->player, extra=0x0 + //_Gen_CraftersGrace = 45, // player->player, extra=0x50 + //_Gen_VulnerabilityUp = 4375, // none->player, extra=0x10/0x2/0xA/0x7/0x6/0x1/0x3/0x5/0x4/0x9/0x8/0xF + //_Gen_Terror = 66, // Helper->player, extra=0x0 + CurseOfDarkness = 2387, // none->player, extra=0x0 + //_Gen_SustainedDamage = 4149, // Helper/StygianTendrils/StygianShadow->player, extra=0x1/0x2/0x3 + //_Gen_StabWound = 3061, // none->player, extra=0x0 + //_Gen_StabWound = 3062, // none->player, extra=0x0 + //_Gen_ThornyVine = 445, // none->player, extra=0x0 + //_Gen_ForwardWithThee = 2240, // none->player, extra=0x33F + //_Gen_Stun = 149, // none->player, extra=0x0 + //_Gen_BackWithThee = 2241, // none->player, extra=0x340 + //_Gen_LeftWithThee = 2242, // none->player, extra=0x341 + //_Gen_Stun = 2656, // none->player, extra=0x0 + //_Gen_RightWithThee = 2243, // none->player, extra=0x342 + //_Gen_Bleeding = 3077, // none->player, extra=0x0 +} + +public enum IconID : uint +{ + GrimEmbraceCountdown = 552, // player->self + Flare = 346, // player->self + UnholyDarkness = 100, // player->self + ThirdArtOfDarknessLeft = 239, // StygianShadow->self + ThirdArtOfDarknessRight = 240, // StygianShadow->self + ThirdArtOfDarknessStack = 241, // StygianShadow->self + ThirdArtOfDarknessSpread = 242, // StygianShadow->self + EvilSeed = 551, // player->self + ThornyVine = 569, // StygianTendrils->self + //_Gen_Icon_12 = 12, // player->self + RotateCW = 564, // Boss->self + RotateCCW = 565, // Boss->self + Excruciate = 342, // player->self +} + +public enum TetherID : uint +{ + GrimEmbraceForward = 300, // player->Boss + GrimEmbraceBackward = 301, // player->Boss + //_Gen_Tether_14 = 14, // Boss/StygianShadow->StygianShadow/Boss + //_Gen_Tether_165 = 165, // 4620->player + ThornyVine = 18, // StygianTendrils/player->player + //_Gen_Tether_38 = 38, // player->player + //_Gen_Tether_1 = 1, // StygianTendrils->StygianTendrils +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ChaosCondensedParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ChaosCondensedParticleBeam.cs new file mode 100644 index 000000000..e5b3611d9 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ChaosCondensedParticleBeam.cs @@ -0,0 +1,21 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class ChaosCondensedParticleBeam(BossModule module) : Components.GenericWildCharge(module, 3, default, 50) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.ChaosCondensedParticleBeam) + { + Source = caster; + Activation = Module.CastFinishAt(spell, 0.7f); + foreach (var (i, p) in Raid.WithSlot(true)) + PlayerRoles[i] = p.Role == Role.Tank ? PlayerRole.Target : PlayerRole.ShareNotFirst; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.ChaosCondensedParticleBeamAOE1 or AID.ChaosCondensedParticleBeamAOE2) + Source = null; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs new file mode 100644 index 000000000..c1073a13d --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs @@ -0,0 +1,31 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class CurseOfDarkness(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.CurseOfDarknessAOE), "Raidwide + bait debuffs"); + +class DarkEnergyParticleBeam(BossModule module) : Components.GenericBaitAway(module, ActionID.MakeSpell(AID.DarkEnergyParticleBeam)) +{ + private readonly DateTime[] _activation = new DateTime[PartyState.MaxAllianceSize]; + + private static readonly AOEShapeCone _shape = new(25, 7.5f.Degrees()); + + public override void Update() + { + CurrentBaits.Clear(); + var deadline = WorldState.FutureTime(7); + foreach (var (i, p) in Raid.WithSlot()) + if (_activation[i] != default && _activation[i] < deadline) + CurrentBaits.Add(new(p, p, _shape, _activation[i])); + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.CurseOfDarkness && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + _activation[slot] = status.ExpireAt; + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.CurseOfDarkness && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + _activation[slot] = default; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs new file mode 100644 index 000000000..9e92c4d42 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs @@ -0,0 +1,18 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +// TODO: who gets radius 7 and who gets radius 5? +// TODO: show for second wave too... +class DiffusiveForceParticleBeam(BossModule module) : Components.UniformStackSpread(module, 0, 7) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.DiffusiveForceParticleBeam) + AddSpreads(Raid.WithoutSlot(), Module.CastFinishAt(spell, 0.7f)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.DiffusiveForceParticleBeamAOE1 or AID.DiffusiveForceParticleBeamAOE2) + Spreads.Clear(); + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs new file mode 100644 index 000000000..7828a9c80 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs @@ -0,0 +1,109 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class EnaeroKnockback(BossModule module) : Components.Knockback(module) +{ + private Source? _source; + private bool _delayed; + + public override IEnumerable Sources(int slot, Actor actor) => Utils.ZeroOrOne(_source); + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!_delayed) + base.AddHints(slot, actor, hints); + } + + public override void AddGlobalHints(GlobalHints hints) + { + if (_delayed) + hints.Add("Delayed knockback"); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Aero: + Start(Module.CastFinishAt(spell, 0.5f)); + break; + case AID.Enaero: + _delayed = true; + break; + case AID.AeroKnockback: + case AID.EnaeroKnockback: + if (_source == null || !_source.Value.Origin.AlmostEqual(caster.Position, 1)) + ReportError("Aero knockback mispredicted"); + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + if (_delayed) + Start(Module.CastFinishAt(spell, 2.2f)); + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.AeroKnockback: + case AID.EnaeroKnockback: + _source = null; + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + _delayed = false; + break; + } + } + + private void Start(DateTime activation) => _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation); +} + +class EnaeroAOE(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + private bool _delayed; + + private static readonly AOEShapeCircle _shape = new(8); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Aero: + Start(Module.CastFinishAt(spell, 0.5f)); + break; + case AID.Enaero: + _delayed = true; + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.AeroAOE: + case AID.EnaeroAOE: + _aoe = null; + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + if (_delayed) + Start(WorldState.FutureTime(2.2f)); + break; + } + } + + private void Start(DateTime activation) + { + _aoe = new(_shape, Ch01CloudOfDarkness.Phase1Midpoint, default, activation); + _delayed = false; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs new file mode 100644 index 000000000..35f468ddf --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs @@ -0,0 +1,116 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class EndeathVortex(BossModule module) : Components.Knockback(module) +{ + private Source? _source; + private bool _delayed; + + public override IEnumerable Sources(int slot, Actor actor) => Utils.ZeroOrOne(_source); + + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => (pos - Ch01CloudOfDarkness.Phase1Midpoint).LengthSq() <= 36; + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!_delayed) + base.AddHints(slot, actor, hints); + } + + public override void AddGlobalHints(GlobalHints hints) + { + if (_delayed) + hints.Add("Delayed attract"); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Death: + Start(Module.CastFinishAt(spell, 0.5f)); + break; + case AID.Endeath: + _delayed = true; + break; + case AID.DeathVortex: + case AID.EndeathVortex: + if (_source == null || !_source.Value.Origin.AlmostEqual(caster.Position, 1)) + ReportError("Death vortex mispredicted"); + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + if (_delayed) + Start(Module.CastFinishAt(spell, 2.2f)); + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.DeathVortex: + case AID.EndeathVortex: + _source = null; + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + _delayed = false; + break; + } + } + + private void Start(DateTime activation) => _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation, Kind: Kind.TowardsOrigin); +} + +class EndeathAOE(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = []; + private bool _delayed; + + private static readonly AOEShapeCircle _shapeOut = new(6); + private static readonly AOEShapeDonut _shapeIn = new(6, 40); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes.Take(1); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Death: + Start(Module.CastFinishAt(spell, 0.5f)); + break; + case AID.Endeath: + _delayed = true; + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.DeathAOE1: + case AID.DeathAOE2: + case AID.EndeathAOE1: + case AID.EndeathAOE2: + if (_aoes.Count > 0) + _aoes.RemoveAt(0); + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + if (_delayed) + Start(WorldState.FutureTime(2.2f)); + break; + } + } + + private void Start(DateTime activation) + { + _aoes.Add(new(_shapeOut, Ch01CloudOfDarkness.Phase1Midpoint, default, activation.AddSeconds(2))); + _aoes.Add(new(_shapeIn, Ch01CloudOfDarkness.Phase1Midpoint, default, activation.AddSeconds(4))); + _delayed = false; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/GrimEmbrace.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/GrimEmbrace.cs new file mode 100644 index 000000000..ce186ca23 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/GrimEmbrace.cs @@ -0,0 +1,68 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class GrimEmbraceBait(BossModule module) : Components.GenericBaitAway(module) +{ + public struct PlayerState + { + public AOEShapeRect? Shape; + public DateTime Activation; + } + + private readonly PlayerState[] _states = new PlayerState[PartyState.MaxAllianceSize]; + + private static readonly AOEShapeRect _shapeForward = new(8, 4); + private static readonly AOEShapeRect _shapeBackward = new(0, 4, 8); + + public override void Update() + { + CurrentBaits.Clear(); + var deadline = WorldState.FutureTime(7); + foreach (var (i, p) in Raid.WithSlot()) + { + ref var s = ref _states[i]; + if (s.Shape != null && s.Activation != default && s.Activation < deadline) + CurrentBaits.Add(new(p, p, s.Shape, s.Activation)); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + ref var s = ref _states[slot]; + if (s.Shape != null && s.Activation != default) + hints.Add($"Dodge {(s.Shape == _shapeForward ? "backward" : "forward")} in {Math.Max(0, (s.Activation - WorldState.CurrentTime).TotalSeconds):f1}s", false); + base.AddHints(slot, actor, hints); + } + + //public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + //{ + // var s = _states[slot]; + // if (s.Shape != null) + // hints.AddSpecialMode(AIHints.SpecialMode.Pyretic, s.Activation); // TODO: reconsider? i want to ensure character won't turn last moment... + //} + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + var shape = (TetherID)tether.ID switch + { + TetherID.GrimEmbraceForward => _shapeForward, + TetherID.GrimEmbraceBackward => _shapeBackward, + _ => null + }; + if (shape != null && Raid.FindSlot(source.InstanceID) is var slot && slot >= 0 && slot < _states.Length) + _states[slot].Shape = shape; + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.DeadlyEmbrace && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0 && slot < _states.Length) + _states[slot].Activation = status.ExpireAt; + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.DeadlyEmbrace && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0 && slot < _states.Length) + _states[slot] = default; + } +} + +class GrimEmbraceAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GrimEmbraceAOE), new AOEShapeRect(8, 4)); diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Phaser.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Phaser.cs new file mode 100644 index 000000000..2eb927dc2 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Phaser.cs @@ -0,0 +1,15 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class Phaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Phaser), new AOEShapeCone(23, 30.Degrees())) // TODO: verify angle +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var deadline = Module.CastFinishAt(Casters.Count > 0 ? Casters[0].CastInfo : null, 1); + foreach (var c in Casters) + { + var activation = Module.CastFinishAt(c.CastInfo); + if (activation < deadline) + yield return new(Shape, c.Position, c.CastInfo?.Rotation ?? c.Rotation, activation); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RapidSequenceParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RapidSequenceParticleBeam.cs new file mode 100644 index 000000000..e6d28c736 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RapidSequenceParticleBeam.cs @@ -0,0 +1,35 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class RapidSequenceParticleBeam(BossModule module) : Components.GenericWildCharge(module, 3, ActionID.MakeSpell(AID.RapidSequenceParticleBeamAOE), 50) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + Source = null; // just in case, if mechanic was not finished properly, reset on next cast start + if ((AID)spell.Action.ID == AID.RapidSequenceParticleBeam) + { + NumCasts = 0; + Source = caster; + Activation = Module.CastFinishAt(spell, 0.8f); + // TODO: not sure how targets are selected, assume it's first healer of each alliance + BitMask selectedTargetsInAlliance = default; + foreach (var (i, p) in Raid.WithSlot()) + { + if (p.Role == Role.Healer && !selectedTargetsInAlliance[i >> 3]) + { + PlayerRoles[i] = PlayerRole.TargetNotFirst; + selectedTargetsInAlliance.Set(i >> 3); + } + else + { + PlayerRoles[i] = p.Role == Role.Tank ? PlayerRole.Share : PlayerRole.ShareNotFirst; + } + } + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.RapidSequenceParticleBeamAOE && ++NumCasts >= 12) + Source = null; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RazingVolleyParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RazingVolleyParticleBeam.cs new file mode 100644 index 000000000..0f9cf5ddb --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RazingVolleyParticleBeam.cs @@ -0,0 +1,16 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class RazingVolleyParticleBeam(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RazingVolleyParticleBeam), new AOEShapeRect(45, 4)) +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var deadline = Module.CastFinishAt(Casters.Count > 0 ? Casters[0].CastInfo : null, 3); + foreach (var c in Casters) + { + var activation = Module.CastFinishAt(c.CastInfo); + if (activation > deadline) + break; + yield return new(Shape, c.Position, c.CastInfo?.Rotation ?? c.Rotation, activation); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ThirdArtOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ThirdArtOfDarkness.cs new file mode 100644 index 000000000..d97daec19 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ThirdArtOfDarkness.cs @@ -0,0 +1,105 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class ThirdArtOfDarknessCleave(BossModule module) : Components.GenericAOEs(module) +{ + public enum Mechanic { None, Left, Right, Stack, Spread } + + public readonly Dictionary> Mechanics = []; + + private static readonly AOEShapeCone _shape = new(15, 90.Degrees()); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var (caster, m) in Mechanics) + { + var dir = m.Count == 0 ? default : m[0].mechanic switch + { + Mechanic.Left => 90.Degrees(), + Mechanic.Right => -90.Degrees(), + _ => default + }; + if (dir != default) + yield return new(_shape, caster.Position, caster.Rotation + dir, m[0].activation); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + var (a, m) = Mechanics.FirstOrDefault(kv => kv.Key.InstanceID == actor.TargetID); + if (a != null && m.Count > 0) + hints.Add($"Order: {string.Join(" > ", m.Select(m => m.mechanic))}", false); + base.AddHints(slot, actor, hints); + } + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + if ((OID)actor.OID == OID.StygianShadow) + { + var mechanic = (IconID)iconID switch + { + IconID.ThirdArtOfDarknessLeft => Mechanic.Left, + IconID.ThirdArtOfDarknessRight => Mechanic.Right, + IconID.ThirdArtOfDarknessStack => Mechanic.Stack, + IconID.ThirdArtOfDarknessSpread => Mechanic.Spread, + _ => Mechanic.None + }; + if (mechanic != Mechanic.None) + Mechanics.GetOrAdd(actor).Add((mechanic, WorldState.FutureTime(9.5f))); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + var mechanic = (AID)spell.Action.ID switch + { + AID.ArtOfDarknessAOEL => Mechanic.Left, + AID.ArtOfDarknessAOER => Mechanic.Right, + AID.HyperFocusedParticleBeamAOE => Mechanic.Spread, + AID.MultiProngedParticleBeamAOE => Mechanic.Stack, + _ => Mechanic.None + }; + if (mechanic != Mechanic.None) + { + var (a, m) = Mechanics.FirstOrDefault(kv => kv.Key.Position.AlmostEqual(caster.Position, 1) && kv.Value.Count > 0 && kv.Value[0].mechanic == mechanic); + if (a != null) + { + m.RemoveAt(0); + if (m.Count == 0) + Mechanics.Remove(a); + } + } + } +} + +class ThirdArtOfDarknessHyperFocusedParticleBeam(BossModule module) : Components.GenericBaitAway(module) +{ + private readonly ThirdArtOfDarknessCleave? _main = module.FindComponent(); + + private static readonly AOEShapeRect _shape = new(22, 2.5f); + + public override void Update() + { + CurrentBaits.Clear(); + if (_main != null) + foreach (var (a, m) in _main.Mechanics) + if (m.Count > 0 && m[0].mechanic == ThirdArtOfDarknessCleave.Mechanic.Spread) + foreach (var p in Raid.WithoutSlot().SortedByRange(a.Position).Take(6)) + CurrentBaits.Add(new(a, p, _shape, m[0].activation)); + } +} + +class ThirdArtOfDarknessMultiProngedParticleBeam(BossModule module) : Components.UniformStackSpread(module, 3, 0, 2) +{ + private readonly ThirdArtOfDarknessCleave? _main = module.FindComponent(); + + public override void Update() + { + Stacks.Clear(); + if (_main != null) + foreach (var (a, m) in _main.Mechanics) + if (m.Count > 0 && m[0].mechanic == ThirdArtOfDarknessCleave.Mechanic.Stack) + foreach (var p in Raid.WithoutSlot().SortedByRange(a.Position).Take(3)) + AddStack(p, m[0].activation); + base.Update(); + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/UnholyDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/UnholyDarkness.cs new file mode 100644 index 000000000..0d2cdfa9d --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/UnholyDarkness.cs @@ -0,0 +1,14 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class UnholyDarkness(BossModule module) : Components.StackWithIcon(module, (uint)IconID.UnholyDarkness, ActionID.MakeSpell(AID.UnholyDarknessAOE), 6, 8.1f, 4) +{ + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action == StackAction) + { + Stacks.Clear(); // if one of the target dies, it won't get hit + ++NumFinishedStacks; + } + } +} + diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs index 8f4170b33..82c98a5bd 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs @@ -5,6 +5,8 @@ class P2CrystalOfLight(BossModule module) : Components.Adds(module, (uint)OID.Cr class P3Junction(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Junction)); class P3BlackHalo(BossModule module) : Components.CastSharedTankbuster(module, ActionID.MakeSpell(AID.BlackHalo), new AOEShapeCone(60, 45.Degrees())); // TODO: verify angle class P4EdgeOfOblivion(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.EdgeOfOblivion)); +class P4HallowedWingsL(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HallowedWingsL), new AOEShapeRect(80, 20, 0, 90.Degrees())); +class P4HallowedWingsR(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HallowedWingsR), new AOEShapeRect(80, 20, 0, -90.Degrees())); [ModuleInfo(BossModuleInfo.Maturity.WIP, PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1006, NameID = 9707, PlanLevel = 100)] public class FRU(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs index 3f6d56358..14d0779a5 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs @@ -57,6 +57,11 @@ public class FRUConfig() : ConfigNode() [PropertyDisplay("P3 Apocalypse: uptime swaps (only consider swaps within prio 1/2 and 3/4, assuming these are melee and ranged)", separator: true)] public bool P3ApocalypseUptime; + [PropertyDisplay("P4 Darklit Dragonsong: assignments (lower prio stays more clockwise, lowest prio support takes N tower)")] + [GroupDetails(["Support prio1", "Support prio2", "Support prio3", "Support prio4", "DD prio1", "DD prio2", "DD prio3", "DD prio4"])] + [GroupPreset("Default (healer N)", [2, 3, 0, 1, 4, 5, 6, 7])] + public GroupAssignmentUnique P4DarklitDragonsongAssignments = new() { Assignments = [2, 3, 0, 1, 4, 5, 6, 7] }; + // ai-only settings [PropertyDisplay("P1 Cyclonic Break (proteans): bait clock spots (supports should be near dd to resolve pairs)", tooltip: "Only used by AI")] [GroupDetails(["N", "NE", "E", "SE", "S", "SW", "W", "NW"])] diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs index aa011be6f..c2a466f76 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs @@ -28,11 +28,12 @@ public enum OID : uint ApocalypseLight = 0x1EB0FF, // R0.500, x0 (spawn during fight), EventObj type UsurperOfFrostP4 = 0x45A9, // R6.125, x0 (spawn during fight) + GreatWyrm = 0x45AA, // R3.500, x0 (spawn during fight), Part type OracleOfDarknessP4 = 0x45AB, // R7.040, x0 (spawn during fight) + SorrowsHourglass = 0x45AD, // R1.000, x0 (spawn during fight) + FragmentOfFate = 0x45B1, // R3.500, x0 (spawn during fight) VisionOfRyne = 0x45B4, // R0.750, x0 (spawn during fight) VisionOfGaia = 0x45B5, // R1.500, x0 (spawn during fight) - FragmentOfFate = 0x45B1, // R3.500, x0 (spawn during fight) - GreatWyrm = 0x45AA, // R3.500, x0 (spawn during fight), Part type } public enum AID : uint @@ -211,7 +212,23 @@ public enum AID : uint DarklitDragonsongUsurper = 40239, // UsurperOfFrostP4->self, 5.0s cast, range 100 circle, raidwide DarklitDragonsongOracle = 40301, // OracleOfDarknessP4->self, 5.0s cast, single-target, visual - //_Weaponskill_ThePathOfLight = 40187, // UsurperOfFrostP4->self, 8.0s cast, single-target + PathOfLight = 40187, // UsurperOfFrostP4->self, 8.0s cast, single-target, visual (towers + proteans) + PathOfLightAOE = 40190, // Helper->self, no cast, range 60 60?-degree cone protean on 4 closest targets + HallowedWingsL = 40227, // UsurperOfFrostP4->self, 5.0s cast, range 80 width 40 rect, side cleave + HallowedWingsR = 40228, // UsurperOfFrostP4->self, 5.0s cast, range 80 width 40 rect, side cleave + SomberDance = 40283, // OracleOfDarknessP4->self, 5.0s cast, single-target, visual (baited tankbusters) + SomberDanceAOE1 = 40284, // OracleOfDarknessP4->player, no cast, range 8 circle, tankbuster on farthest + SomberDanceAOE2 = 40285, // OracleOfDarknessP4->players, no cast, range 8 circle, tankbuster on closest + AkhMornUsurper = 40247, // UsurperOfFrostP4->self, 4.0s cast, single-target, visual (4-hit party stacks) + AkhMornOracle = 40302, // OracleOfDarknessP4->self, 4.0s cast, single-target, visual (4-hit party stacks) + AkhMornAOEUsurper = 40248, // Helper->players, no cast, range 4 circle, 4-hit 4-man stack + 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 ? + + CrystallizeTimeUsurper = 40240, // UsurperOfFrostP4->self, 10.0s cast, single-target, visual + CrystallizeTimeOracle = 40298, // OracleOfDarknessP4->self, 10.0s cast, range 100 circle, raidwide } public enum SID : uint @@ -227,6 +244,7 @@ public enum SID : uint CurseOfEverlastingLight = 4158, // none->player, extra=0x0, light rampant second tether WeightOfLight = 4159, // none->player, extra=0x0, light rampant stack Lightsteeped = 2257, // Helper/HolyLight->player, extra=0x1/0x2/0x3/0x4/0x5 + Invincibility = 775, // none->IceVeil, extra=0x0 SpellInWaitingUnholyDarkness = 2454, // none->player, extra=0x0, stack SpellInWaitingDarkFire = 2455, // none->player, extra=0x0, large spread SpellInWaitingShadoweye = 2456, // none->player, extra=0x0, delayed gaze @@ -237,6 +255,12 @@ public enum SID : uint DelightsHourglassRotation = 2970, // none->DelightsHourglass, extra=0x10D (ccw)/0x15C (cw) Return = 2452, // none->player, extra=0x0 Stun = 4163, // none->player, extra=0x0 + Wyrmclaw = 3263, // none->player, extra=0x0 + Wyrmfang = 3264, // none->player, extra=0x0 + SpellInWaitingQuietus = 4174, // none->player, extra=0x0 + SpellInWaitingDarkAero = 2463, // none->player, extra=0x0 + //SpellInWaitingReturn = 4208, // none->player, extra=0x0 + //SpellInWaitingReturnII = 4171, // Helper->UsurperOfFrostP4, extra=0x0 } public enum IconID : uint @@ -263,4 +287,5 @@ public enum TetherID : uint HiemalRay = 84, // CrystalOfLight->player UltimateRelativitySlow = 133, // DelightsHourglass->BossP3 UltimateRelativityQuicken = 134, // DelightsHourglass->BossP3 + MornAfahHPCheck = 1, // UsurperOfFrostP4->OracleOfDarknessP4 } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index 3bd601b68..8e02df855 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -52,6 +52,8 @@ private void Phase34(uint id) P4AkhRhai(id + 0x100000, 5.5f); P4DarklitDragonsong(id + 0x110000, 1.9f); + P4AkhMornMornAfah(id + 0x120000, 2.4f); + P4CrystallizeTime(id + 0x130000, 4.6f); SimpleState(id + 0xFF0000, 100, "???"); } @@ -147,6 +149,7 @@ private void P1TurnOfTheHeavensBoundOfFaith(uint id, float delay) ComponentCondition(id + 0x20, 2.1f, comp => comp.NumCasts > 0, "Circles") .ActivateOnEnter() .ExecOnEnter(comp => comp.Risky = true) + .ExecOnEnter(comp => comp.EnableHints = true) .DeactivateOnExit() .DeactivateOnExit(); ComponentCondition(id + 0x30, 4, comp => !comp.Active, "Stacks") // note: won't happen if both targets die, but that's a wipe anyway @@ -486,22 +489,85 @@ private void P4AkhRhai(uint id, float delay) ActorTargetable(id, _module.BossP4Usurper, true, delay, "Usurper appears") .SetHint(StateMachine.StateHint.DowntimeEnd); ActorCast(id + 0x10, _module.BossP4Usurper, AID.Materialization, 5.1f, 3, true); - ComponentCondition(id + 0x20, 11.2f, comp => comp.AOEs.Count > 0, "Puddle baits") - .ActivateOnEnter(); - ComponentCondition(id + 0x30, 2.6f, comp => comp.NumCasts > 0); + ComponentCondition(id + 0x20, 11.2f, comp => comp.AOEs.Count > 0, "Puddle baits") + .ActivateOnEnter(); + ComponentCondition(id + 0x30, 2.6f, comp => comp.NumCasts > 0); ComponentCondition(id + 0x40, 2.4f, comp => comp.NumCasts > 0, "Raidwide") .ActivateOnEnter() .DeactivateOnExit() .SetHint(StateMachine.StateHint.Raidwide); ActorTargetable(id + 0x50, _module.BossP4Oracle, true, 1.2f, "Oracle appears"); - ComponentCondition(id + 0x60, 1.6f, comp => comp.NumCasts >= 10 * comp.AOEs.Count, "Puddle resolve") - .DeactivateOnExit(); + ComponentCondition(id + 0x60, 1.6f, comp => comp.NumCasts >= 10 * comp.AOEs.Count, "Puddle resolve") + .ActivateOnEnter() + .DeactivateOnExit(); } private void P4DarklitDragonsong(uint id, float delay) { ActorCast(id, _module.BossP4Usurper, AID.DarklitDragonsongUsurper, delay, 5, true, "Raidwide (darklit)") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .SetHint(StateMachine.StateHint.Raidwide); - // TODO: more... + ActorCast(id + 0x10, _module.BossP4Usurper, AID.PathOfLight, 3.2f, 8); + ActorCastStart(id + 0x20, _module.BossP4Oracle, AID.SpiritTaker, 0.1f, true, "Towers") // towers resolve right as cast starts + .DeactivateOnExit(); + ComponentCondition(id + 0x21, 0.8f, comp => comp.NumCasts > 0, "Proteans") + .DeactivateOnExit(); + ActorCastEnd(id + 0x22, _module.BossP4Oracle, 2.2f, true) + .ActivateOnEnter(); + ActorCastStartMulti(id + 0x23, _module.BossP4Usurper, [AID.HallowedWingsL, AID.HallowedWingsR], 0.1f, true); + ComponentCondition(id + 0x24, 0.3f, comp => comp.Spreads.Count == 0, "Jump") + .DeactivateOnExit(); + ActorCastStart(id + 0x25, _module.BossP4Oracle, AID.SomberDance, 2.8f) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + ComponentCondition(id + 0x26, 1.7f, comp => comp.Stacks.Count == 0, "Stacks") + .DeactivateOnExit(); + ActorCastEnd(id + 0x27, _module.BossP4Usurper, 0.2f, false, "Side cleave") + .ActivateOnEnter() + .DeactivateOnExit() + .DeactivateOnExit() + .DeactivateOnExit(); + ActorCastEnd(id + 0x28, _module.BossP4Oracle, 3.1f, true); + ComponentCondition(id + 0x29, 0.4f, comp => comp.NumCasts > 0, "Tankbuster 1") + .SetHint(StateMachine.StateHint.Tankbuster); + ComponentCondition(id + 0x2A, 3.2f, comp => comp.NumCasts > 1, "Tankbuster 2") + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Tankbuster); + + ComponentCondition(id + 0x30, 3.4f, comp => comp.NumCasts > 0, "Raidwide") + .ActivateOnEnter() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void P4AkhMornMornAfah(uint id, float delay) + { + ActorCast(id, _module.BossP4Usurper, AID.AkhMornUsurper, delay, 4, true) + .ActivateOnEnter(); + ComponentCondition(id + 0x10, 0.9f, comp => comp.NumCasts >= 1, "Party stack 1"); + ComponentCondition(id + 0x11, 1.1f, comp => comp.NumCasts >= 2, "Party stack 2"); + ComponentCondition(id + 0x12, 1.1f, comp => comp.NumCasts >= 3, "Party stack 3"); + ComponentCondition(id + 0x13, 1.1f, comp => comp.NumCasts >= 4, "Party stack 4") + .DeactivateOnExit(); + + ActorCast(id + 0x1000, _module.BossP4Usurper, AID.MornAfahUsurper, 0.1f, 6, true) + .ActivateOnEnter(); + ComponentCondition(id + 0x1010, 0.9f, comp => comp.Stacks.Count == 0, "Raid stack + HP check") + .DeactivateOnExit(); + } + + private void P4CrystallizeTime(uint id, float delay) + { + ActorCast(id, _module.BossP4Oracle, AID.CrystallizeTimeOracle, delay, 10, true, "Raidwide (crystallize)") + .SetHint(StateMachine.StateHint.Raidwide); + ActorTargetable(id + 0x10, _module.BossP4Usurper, false, 3.1f, "Usurper disappears") + .ActivateOnEnter(); + ActorTargetable(id + 0x11, _module.BossP4Oracle, false, 1.1f, "Oracle disappears") + .SetHint(StateMachine.StateHint.DowntimeStart); + ActorCast(id + 0x20, _module.BossP4Oracle, AID.UltimateRelativitySpeed, 0.1f, 5.5f, true); + // TODO: ... } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs index 3b3d9afa1..411aadc9b 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs @@ -15,6 +15,13 @@ public override IEnumerable Sources(int slot, Actor actor) } } + public override void AddHints(int slot, Actor actor, TextHints hints) + { + // don't show kb hints until aoe is done + if (_aoeDone) + base.AddHints(slot, actor, hints); + } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { if (_caster != null) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs index 9c96c8d56..7103790f5 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs @@ -9,6 +9,7 @@ // TODO: fixed tethers strat variant (tether target with clone on safe side goes S, other goes N, if any group has 5 players prio1 adjusts) class P1BoundOfFaith(BossModule module) : Components.UniformStackSpread(module, 6, 0, 4, 4) { + public bool EnableHints; public WDir SafeSide; public DateTime Activation; public readonly int[] AssignedGroups = new int[PartyState.MaxPartySize]; @@ -17,6 +18,12 @@ class P1BoundOfFaith(BossModule module) : Components.UniformStackSpread(module, public WDir AssignedLane(int slot) => new(0, AssignedGroups[slot] * 5.4f); + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (EnableHints) + base.AddHints(slot, actor, hints); + } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // we have dedicated components for this public override void DrawArenaForeground(int pcSlot, Actor pc) @@ -140,7 +147,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme var targetSlot = Raid.FindSlot(s.Target.InstanceID); var targetGroup = targetSlot >= 0 ? _comp.AssignedGroups[targetSlot] : 0; if (targetGroup == _comp.AssignedGroups[slot]) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(s.Target.Position, 6), _comp.Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(s.Target.Position, 4), _comp.Activation); // stay a bit closer to the target to avoid spooking people else hints.AddForbiddenZone(ShapeDistance.Circle(s.Target.Position, 6), _comp.Activation); } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs new file mode 100644 index 000000000..feea44955 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs @@ -0,0 +1,18 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +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)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.AkhMornAOEOracle) + ++NumCasts; + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhRhai.cs similarity index 88% rename from BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs rename to BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhRhai.cs index a03aed8ee..ab80db268 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhRhai.cs @@ -1,6 +1,6 @@ namespace BossMod.Dawntrail.Ultimate.FRU; -class P4AhkRhai(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.AkhRhaiAOE)) +class P4AkhRhai(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.AkhRhaiAOE)) { public readonly List AOEs = []; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs new file mode 100644 index 000000000..74c525329 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs @@ -0,0 +1,6 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P4CrystallizeTime(BossModule module) : BossComponent(module) +{ + +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs new file mode 100644 index 000000000..9114fb9f4 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs @@ -0,0 +1,241 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +// tethers & general assignments +class P4DarklitDragonsong(BossModule module) : BossComponent(module) +{ + public BitMask Stacks; + public BitMask TowerSoakers; + public BitMask AssignS; + public BitMask AssignE; + private readonly List<(Actor from, Actor to)> _tethers = []; + + public override void AddGlobalHints(GlobalHints hints) + { + var southTower = TowerSoakers & AssignS; + if (southTower.Any()) + hints.Add($"Water in {((southTower & Stacks).Any() ? "S" : "N")} tower"); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var t in _tethers) + Arena.AddLine(t.from.Position, t.to.Position, ArenaColor.Safe); // TODO: min/max break distance + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.SpellInWaitingDarkWater) + Stacks.Set(Raid.FindSlot(actor.InstanceID)); + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.LightRampantChains) + { + TowerSoakers.Set(Raid.FindSlot(source.InstanceID)); + var target = WorldState.Actors.Find(tether.Target); + if (target != null) + _tethers.Add((source, target)); + if (_tethers.Count == 4) + InitAssignments(); + } + } + + private void InitAssignments() + { + Span ccwOrderSlots = [-1, -1, -1, -1, -1, -1, -1, -1]; + int[] playerPrios = [-1, -1, -1, -1, -1, -1, -1, -1]; + foreach (var (slot, group) in Service.Config.Get().P4DarklitDragonsongAssignments.Resolve(Raid)) + { + ccwOrderSlots[group] = slot; + playerPrios[slot] = group; + } + if (ccwOrderSlots.Contains(-1)) + return; // assignments are not valid, bail + + // find the anchor (tethered player with lowest prio), players tethered to him on both sides will take S tower + var anchorSlot = TowerSoakers.SetBits().MinBy(i => playerPrios[i]); + var anchorPlayer = Raid[anchorSlot]; + foreach (var t in _tethers) + { + if (t.from == anchorPlayer) + AssignS.Set(Raid.FindSlot(t.to.InstanceID)); + else if (t.to == anchorPlayer) + AssignS.Set(Raid.FindSlot(t.from.InstanceID)); + } + + // remaining assignments (N/S for baits, E/W for everyone) in prio order + int numAssignedSoakers = 0, numAssignedBaits = 0; + foreach (var slot in ccwOrderSlots) + { + if (TowerSoakers[slot]) + { + // first and last prio go E + AssignE[slot] = numAssignedSoakers++ is 0 or 3; + } + else + { + // first and last go N, last two go E + AssignS[slot] = numAssignedBaits is 1 or 2; + AssignE[slot] = numAssignedBaits >= 2; + ++numAssignedBaits; + } + } + + // finally, if both stacks are on the same N/S side, bait needs to swap with other bait on same E/W side + if ((AssignS & Stacks).NumSetBits() != 1) + { + var flexStack = Stacks & ~TowerSoakers; // mask containing one set bit, corresponding to non-soaker stack - he will need to flex + var flexE = AssignE[flexStack.LowestSetBit()]; + var flexMask = (AssignE ^ new BitMask(flexE ? 0u : 0xFF)) & ~TowerSoakers; // mask containing both flexers + if (flexStack.NumSetBits() != 1 || flexMask.NumSetBits() != 2 || (flexStack & flexMask) != flexStack) + ReportError("Some error with swap logic, investigate"); + AssignS ^= flexMask; + } + } +} + +class P4DarklitDragonsongBrightHunger(BossModule module) : Components.GenericTowers(module, ActionID.MakeSpell(AID.BrightHunger)) +{ + private int _numTethers; + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.LightRampantChains && ++_numTethers == 4) + { + var assignments = Module.FindComponent(); + if (assignments != null) + { + var allowedN = assignments.TowerSoakers & ~assignments.AssignS; + var allowedS = assignments.TowerSoakers & assignments.AssignS; + if (assignments.AssignS.None()) + allowedN = allowedS = assignments.TowerSoakers; // no assignments, just mark both towers as good + + var towerOffset = new WDir(0, 8); + Towers.Add(new(Module.Center - towerOffset, 4, 4, 4, new BitMask(0xFF) ^ allowedN, WorldState.FutureTime(10.4f))); + Towers.Add(new(Module.Center + towerOffset, 4, 4, 4, new BitMask(0xFF) ^ allowedS, WorldState.FutureTime(10.4f))); + } + } + } +} + +class P4DarklitDragonsongPathOfLight(BossModule module) : Components.GenericBaitAway(module, ActionID.MakeSpell(AID.PathOfLightAOE)) +{ + private Actor? _source; + private DateTime _activation; + + private static readonly AOEShapeCone _shape = new(60, 30.Degrees()); + + public override void Update() + { + CurrentBaits.Clear(); + if (_source != null && ForbiddenPlayers.Any()) + foreach (var p in Raid.WithoutSlot().SortedByRange(_source.Position).Take(4)) + CurrentBaits.Add(new(_source, p, _shape, _activation)); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (CurrentBaits.Count == 0) + return; + + var baitIndex = CurrentBaits.FindIndex(b => b.Target == actor); + if (ForbiddenPlayers[slot]) + { + if (baitIndex >= 0) + hints.Add("Stay farther away!"); + } + else + { + if (baitIndex < 0) + hints.Add("Stay closer to bait!"); + else if (PlayersClippedBy(CurrentBaits[baitIndex]).Any()) + hints.Add("Bait cone away from raid!"); + } + + if (ActiveBaitsNotOn(actor).Any(b => IsClippedBy(actor, b))) + hints.Add("GTFO from baited cone!"); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.DarklitDragonsongUsurper) + { + _source = caster; + _activation = Module.CastFinishAt(spell, 12); + } + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.LightRampantChains) + ForbiddenPlayers.Set(Raid.FindSlot(source.InstanceID)); + } +} + +class P4DarklitDragonsongDarkWater(BossModule module) : Components.UniformStackSpread(module, 6, 0, 4, includeDeadTargets: true) +{ + private readonly P4DarklitDragonsong? _assignments = module.FindComponent(); + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.SpellInWaitingDarkWater) + { + BitMask forbidden = default; + if (_assignments != null && _assignments.AssignS.Any()) + { + var isSouth = _assignments.AssignS[Raid.FindSlot(actor.InstanceID)]; + forbidden = _assignments.AssignS ^ new BitMask(isSouth ? 0xFF : 0u); + } + AddStack(actor, status.ExpireAt, forbidden); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.DarkWater) + Stacks.Clear(); + } +} + +class P4SomberDance(BossModule module) : Components.GenericBaitAway(module, centerAtTarget: true) +{ + private Actor? _source; + private DateTime _activation; + + private static readonly AOEShapeCircle _shape = new(8); + + public override void Update() + { + CurrentBaits.Clear(); + if (_source == null) + return; + var targets = Raid.WithoutSlot(excludeNPCs: true); + var target = NumCasts == 0 ? targets.Farthest(_source.Position) : targets.Closest(_source.Position); + if (target != null) + CurrentBaits.Add(new(_source, target, _shape, _activation)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.SomberDance) + { + ForbiddenPlayers = Raid.WithSlot(true).WhereActor(p => p.Role != Role.Tank).Mask(); + _source = caster; + _activation = Module.CastFinishAt(spell, 0.4f); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.SomberDanceAOE1 or AID.SomberDanceAOE2) + { + ++NumCasts; + _activation = WorldState.FutureTime(3.2f); + foreach (var t in spell.Targets) + ForbiddenPlayers.Set(Raid.FindSlot(t.ID)); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs new file mode 100644 index 000000000..1ec3b55c6 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs @@ -0,0 +1,35 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P4MornAfah(BossModule module) : Components.UniformStackSpread(module, 4, 0, 8) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.MornAfahOracle) + { + // note: target is random?.. + var target = WorldState.Actors.Find(caster.TargetID); + if (target != null) + AddStack(target, Module.CastFinishAt(spell, 0.9f)); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.MornAfahAOE) // TODO: proper spell... + Stacks.Clear(); + } +} + +class P4MornAfahHPCheck(BossModule module) : BossComponent(module) +{ + public override void AddGlobalHints(GlobalHints hints) + { + var usurper = Module.Enemies(OID.UsurperOfFrostP4).FirstOrDefault(); + var oracle = Module.Enemies(OID.OracleOfDarknessP4).FirstOrDefault(); + if (usurper != null && oracle != null) + { + var diff = (int)(usurper.HPMP.CurHP - oracle.HPMP.CurHP) * 100.0f / usurper.HPMP.MaxHP; + hints.Add($"Usurper HP: {(diff > 0 ? "+" : "")}{diff:f1}%"); + } + } +} From 81a4118f349378c953a4caed9dfe2426ced612fa Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Fri, 27 Dec 2024 13:25:02 +0000 Subject: [PATCH 2/3] Some fixes. --- BossMod/AI/AIRotationModule.cs | 7 +++++-- BossMod/Debug/DebugAutorotation.cs | 4 ++++ BossMod/Debug/DebugInput.cs | 13 ++++++++----- .../Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs | 2 +- BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs | 4 ++-- .../Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs | 13 +++++++------ .../Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs | 4 ++-- BossMod/Replay/ReplayBuilder.cs | 8 ++++---- 8 files changed, 33 insertions(+), 22 deletions(-) diff --git a/BossMod/AI/AIRotationModule.cs b/BossMod/AI/AIRotationModule.cs index 4c703f7a7..0f2cc76e0 100644 --- a/BossMod/AI/AIRotationModule.cs +++ b/BossMod/AI/AIRotationModule.cs @@ -20,8 +20,11 @@ protected bool InMeleeRange(Actor target, WPos position) protected void SetForcedMovement(WPos? pos, float tolerance = 0.1f) { - var dir = (pos ?? Player.Position) - Player.Position; - Hints.ForcedMovement = dir.LengthSq() > tolerance * tolerance ? dir.ToVec3(Player.PosRot.Y) : default; + if (pos != null) + { + var dir = pos.Value - Player.Position; + Hints.ForcedMovement = dir.LengthSq() > tolerance * tolerance ? dir.ToVec3(Player.PosRot.Y) : default; + } } protected WPos ClosestInRange(WPos pos, WPos target, float maxRange) diff --git a/BossMod/Debug/DebugAutorotation.cs b/BossMod/Debug/DebugAutorotation.cs index 317bc5ca2..c98b61454 100644 --- a/BossMod/Debug/DebugAutorotation.cs +++ b/BossMod/Debug/DebugAutorotation.cs @@ -1,4 +1,5 @@ using BossMod.Autorotation; +using ImGuiNET; namespace BossMod; @@ -12,5 +13,8 @@ public void Draw() if (player == null) return; new AIHintsVisualizer(autorot.Hints, autorot.Bossmods.WorldState, player, 3).Draw(_tree); + + if (ImGui.Button("Gaze!")) + autorot.Hints.ForbiddenDirections.Add((player.Rotation, 45.Degrees(), default)); } } diff --git a/BossMod/Debug/DebugInput.cs b/BossMod/Debug/DebugInput.cs index 91aba5a44..7dffdaa73 100644 --- a/BossMod/Debug/DebugInput.cs +++ b/BossMod/Debug/DebugInput.cs @@ -90,7 +90,7 @@ internal sealed unsafe class DebugInput : IDisposable private readonly AIHints _hints; private readonly MovementOverride _move; //private readonly AI.AIController _navi; - private Vector3 _prevPos; + private Vector4 _prevPosRot; private float _prevSpeed; private bool _wannaMove; private float _moveDir; @@ -143,13 +143,16 @@ public void Draw() var dt = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->FrameDeltaTime; var player = _ws.Party.Player(); - var curPos = player?.PosRot.XYZ() ?? new(); - var speed = (curPos - _prevPos) / dt; + var curPosRot = player?.PosRot ?? new(); + var speed = (curPosRot.XYZ() - _prevPosRot.XYZ()) / dt; var speedAbs = speed.Length(); var accel = (speedAbs - _prevSpeed) / dt; - _prevPos = curPos; + var rotSpeed = (curPosRot.W - _prevPosRot.W).Radians().Normalized() / dt; + //if (curPosRot.W != _prevPosRot.W) + // Service.Log($"ROT: {_prevPosRot.W.Radians()} -> {curPosRot.W.Radians()} over {dt} (s={rotSpeed})"); + _prevPosRot = curPosRot; _prevSpeed = speedAbs; - ImGui.TextUnformatted($"Speed={speedAbs:f3}, SpeedH={speed.XZ().Length():f3}, SpeedV={speed.Y:f3}, Accel={accel:f3}, Azimuth={Angle.FromDirection(new(speed.XZ()))}, Altitude={Angle.FromDirection(new(speed.Y, speed.XZ().Length()))}"); + ImGui.TextUnformatted($"Speed={speedAbs:f3}, SpeedH={speed.XZ().Length():f3}, SpeedV={speed.Y:f3}, RSpeed={rotSpeed}, Accel={accel:f3}, Azimuth={Angle.FromDirection(new(speed.XZ()))}, Altitude={Angle.FromDirection(new(speed.Y, speed.XZ().Length()))}"); ImGui.TextUnformatted($"MO: desired={_move.DesiredDirection}, user={_move.UserMove}, actual={_move.ActualMove}"); //Service.Log($"Speed: {speedAbs:f3}, accel: {accel:f3}"); diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs index 1b8a0cfc1..a0f30020a 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs @@ -71,7 +71,7 @@ public class Ch01CloudOfDarkness(WorldState ws, Actor primary) : BossModule(ws, public static readonly ArenaBoundsCircle InitialBounds = new(40); public static readonly ArenaBoundsCustom Phase1Bounds = new(InitialBounds.Radius, new(BuildPhase1BoundsContour())); public static readonly ArenaBoundsCustom Phase2Bounds = new(InitialBounds.Radius, BuildPhase2BoundsPoly()); - public static readonly WPos Phase1Midpoint = DefaultCenter + Phase1Bounds.Poly.Parts[0].Vertices[1] + Phase1Bounds.Poly.Parts[0].Vertices[3]; + public static readonly WPos Phase1Midpoint = DefaultCenter + (Phase1Bounds.Poly.Parts[0].Vertices[1] + Phase1Bounds.Poly.Parts[0].Vertices[3]) * 0.5f; public static List BuildPhase1BoundsContour() { diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs index a18d0dd4f..665137769 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs @@ -33,14 +33,14 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa } } - private WPos CalculateDestination(FRU module, Actor? primaryTarget, StrategyValues.OptionRef strategy, PartyRolesConfig.Assignment assignment) => strategy.As() switch + private WPos? CalculateDestination(FRU module, Actor? primaryTarget, StrategyValues.OptionRef strategy, PartyRolesConfig.Assignment assignment) => strategy.As() switch { MovementStrategy.Pathfind => PathfindPosition(null), MovementStrategy.PathfindMeleeGreed => PathfindPosition(ResolveTargetOverride(strategy.Value) ?? primaryTarget), MovementStrategy.Explicit => ResolveTargetLocation(strategy.Value), MovementStrategy.Prepull => PrepullPosition(module, assignment), MovementStrategy.DragToCenter => DragToCenterPosition(module), - _ => Player.Position + _ => null }; // TODO: account for leeway for casters diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs index 7103790f5..b54ce94ca 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs @@ -142,14 +142,15 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (_haveFetters) { + // stack with closest (note: we could also stack with assigned, but that won't work well if people swap and assignments end up wrong) + //var stackWith = _comp.Stacks.FirstOrDefault(s => _comp.AssignedGroups[Raid.FindSlot(s.Target.InstanceID)] == _comp.AssignedGroups[slot]); + var stackWith = _comp.Stacks.MinBy(s => (s.Target.Position - actor.Position).LengthSq()); foreach (var s in _comp.Stacks) { - var targetSlot = Raid.FindSlot(s.Target.InstanceID); - var targetGroup = targetSlot >= 0 ? _comp.AssignedGroups[targetSlot] : 0; - if (targetGroup == _comp.AssignedGroups[slot]) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(s.Target.Position, 4), _comp.Activation); // stay a bit closer to the target to avoid spooking people - else - hints.AddForbiddenZone(ShapeDistance.Circle(s.Target.Position, 6), _comp.Activation); + var zone = s.Target == stackWith.Target + ? ShapeDistance.InvertedCircle(s.Target.Position, 4) // stay a bit closer to the target to avoid spooking people + : ShapeDistance.Circle(s.Target.Position, 6); + hints.AddForbiddenZone(zone, _comp.Activation); } // all else being equal, try staying closer to center diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs index 6dc8a6b9e..379dadb65 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs @@ -134,7 +134,7 @@ public override void Update() return; var folded = _aoes.DangerousSpots | (_aoes.DangerousSpots >> 4); - if (WorldState.FutureTime(5) > _aoes.Activation) + if (WorldState.FutureTime(6) > _aoes.Activation) { // can't wait any longer for people to think... _seenDangerSpot = folded & new BitMask(0xF); @@ -144,7 +144,7 @@ public override void Update() foreach (var (slot, group) in _config.P1UtopianSkyInitialSpots.Resolve(Raid)) { var spot = group & 3; - if (folded[spot] && !_seenDangerSpot[spot] && Raid[slot] is var p && p != null && p.Position.InCircle(Module.Center, 12)) + if (folded[spot] && !_seenDangerSpot[spot] && Raid[slot] is var p && p != null && !p.Position.InDonutCone(Module.Center, 12, 20, (180 - 45 * group).Degrees(), 30.Degrees())) _seenDangerSpot.Set(spot); } } diff --git a/BossMod/Replay/ReplayBuilder.cs b/BossMod/Replay/ReplayBuilder.cs index a02ae4f11..5bb5a39d4 100644 --- a/BossMod/Replay/ReplayBuilder.cs +++ b/BossMod/Replay/ReplayBuilder.cs @@ -214,11 +214,11 @@ private void ActorAdded(Actor actor) p.EffectiveExistence.End = DateTime.MaxValue; // until it is destroyed p.WorldExistence.Add(new(_ws.CurrentTime)); if (p.NameHistory.Count == 0 ? (actor.Name.Length > 0 || actor.NameID != 0) : p.NameHistory.Values[^1] != (actor.Name, actor.NameID)) - p.NameHistory.Add(_ws.CurrentTime, (actor.Name, actor.NameID)); + p.NameHistory[_ws.CurrentTime] = (actor.Name, actor.NameID); if (actor.IsTargetable) - p.TargetableHistory.Add(_ws.CurrentTime, true); - p.PosRotHistory.Add(_ws.CurrentTime, actor.PosRot); - p.HPMPHistory.Add(_ws.CurrentTime, actor.HPMP); + p.TargetableHistory[_ws.CurrentTime] = true; + p.PosRotHistory[_ws.CurrentTime] = actor.PosRot; + p.HPMPHistory[_ws.CurrentTime] = actor.HPMP; p.MinRadius = Math.Min(p.MinRadius, actor.HitboxRadius); p.MaxRadius = Math.Max(p.MaxRadius, actor.HitboxRadius); From c0148eef76541853dec0708b7114374e8308736b Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Fri, 27 Dec 2024 13:57:12 +0000 Subject: [PATCH 3/3] TODO update --- TODO | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO b/TODO index 5c5907301..3ae396629 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,6 @@ immediate plans - freeze +- gaze avoidance + forced movement fail - rotation modules in preset/plan to be ordered (incl ui) - get rid of legacyxxx - ex3 p2 ice bridges