diff --git a/BossMod/Framework/ActionManagerEx.cs b/BossMod/Framework/ActionManagerEx.cs index 1c5276d508..83bbbe39c9 100644 --- a/BossMod/Framework/ActionManagerEx.cs +++ b/BossMod/Framework/ActionManagerEx.cs @@ -339,7 +339,7 @@ private void UpdateDetour(ActionManager* self) var desiredRotation = CalculateDesiredOrientation(actionImminent); // execute rotation, if needed - var autoRotateConfig = fwk->SystemConfig.GetConfigOption(/*(uint)ConfigOption.AutoFaceTargetOnAction*/225); // TODO: 7.1: update config option + var autoRotateConfig = fwk->SystemConfig.GetConfigOption((uint)ConfigOption.AutoFaceTargetOnAction); var autoRotateOriginal = autoRotateConfig->Value.UInt; if (desiredRotation != null) { diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternal.cs b/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternal.cs index 5be14eb56f..fca44a7e0d 100644 --- a/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternal.cs +++ b/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternal.cs @@ -4,7 +4,7 @@ class ProsecutionOfWar(BossModule module) : Components.TankSwap(module, ActionID class DyingMemory(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.DyingMemory)); class DyingMemoryLast(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.DyingMemoryLast)); -[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "veyn, Malediktus", PlanLevel = 100, PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1017, NameID = 13029)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1017, NameID = 13029, PlanLevel = 100)] public class Ex3QueenEternal(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, NormalBounds) { public static readonly WPos ArenaCenter = Trial.T03QueenEternal.T03QueenEternal.ArenaCenter; diff --git a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/AratamaPuddle.cs b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/AratamaPuddle.cs new file mode 100644 index 0000000000..470e520e4c --- /dev/null +++ b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/AratamaPuddle.cs @@ -0,0 +1,19 @@ +namespace BossMod.Dawntrail.Unreal.Un1Byakko; + +class AratamaPuddleBait(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.AratamaPuddle, ActionID.MakeSpell(AID.AratamaPuddle), 4, 5.1f) +{ + private DateTime _nextSpread; + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action == SpreadAction && WorldState.CurrentTime > _nextSpread) + { + if (++NumFinishedSpreads >= 3) + Spreads.Clear(); + else + _nextSpread = WorldState.FutureTime(0.5f); // protection in case one target dies + } + } +} + +class AratamaPuddleVoidzone(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.AratamaPuddle).Where(z => z.EventState != 7)); diff --git a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/HundredfoldHavoc.cs b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/HundredfoldHavoc.cs new file mode 100644 index 0000000000..e0a7d765ca --- /dev/null +++ b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/HundredfoldHavoc.cs @@ -0,0 +1,30 @@ +namespace BossMod.Dawntrail.Unreal.Un1Byakko; + +class HundredfoldHavoc(BossModule module) : Components.Exaflare(module, 5) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.HundredfoldHavocFirst) + { + Lines.Add(new() { Next = caster.Position, Advance = 5 * spell.Rotation.ToDirection(), NextExplosion = Module.CastFinishAt(spell), TimeToMove = 1.1f, ExplosionsLeft = 4, MaxShownExplosions = 2 }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.HundredfoldHavocFirst or AID.HundredfoldHavocRest) + { + ++NumCasts; + int index = Lines.FindIndex(item => item.Next.AlmostEqual(caster.Position, 1)); + if (index == -1) + { + ReportError($"Failed to find entry for {caster.InstanceID:X}"); + return; + } + + AdvanceLine(Lines[index], caster.Position); + if (Lines[index].ExplosionsLeft == 0) + Lines.RemoveAt(index); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Intermission.cs b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Intermission.cs new file mode 100644 index 0000000000..3fdbb0a5d7 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Intermission.cs @@ -0,0 +1,42 @@ +namespace BossMod.Dawntrail.Unreal.Un1Byakko; + +class VoiceOfThunder : Components.PersistentInvertibleVoidzone +{ + public VoiceOfThunder(BossModule module) : base(module, 2, m => m.Enemies(OID.AramitamaSoul).Where(x => !x.IsDead)) + { + InvertResolveAt = WorldState.CurrentTime; + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Sources(Module).Any(x => !Shape.Check(actor.Position, x))) + hints.Add("Touch the orbs!"); + } +} + +class IntermissionOrbAratama(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.IntermissionOrbAratama), "GTFO from puddle!") +{ + public readonly List AOEs = []; + + private static readonly AOEShapeCircle _shape = new(2); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs; + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.IntermissionOrbSpawn: + AOEs.Add(new(_shape, spell.TargetXZ, default, WorldState.FutureTime(5.1f))); + break; + case AID.IntermissionOrbAratama: + ++NumCasts; + AOEs.RemoveAll(aoe => aoe.Origin.AlmostEqual(spell.TargetXZ, 1)); + break; + } + } +} + +class IntermissionSweepTheLeg(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IntermissionSweepTheLeg), new AOEShapeDonut(5, 25)); +class ImperialGuard(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ImperialGuard), new AOEShapeRect(44.75f, 2.5f)); +class FellSwoop(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.FellSwoop)); diff --git a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/StateOfShock.cs b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/StateOfShock.cs new file mode 100644 index 0000000000..7e06794944 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/StateOfShock.cs @@ -0,0 +1,42 @@ +namespace BossMod.Dawntrail.Unreal.Un1Byakko; + +class StateOfShock(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.StateOfShockSecond)) +{ + public int NumStuns; + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.Stun) + ++NumStuns; + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.Stun) + --NumStuns; + } +} + +class HighestStakes(BossModule module) : Components.GenericTowers(module, ActionID.MakeSpell(AID.HighestStakesAOE)) +{ + private BitMask _forbidden; + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + if (iconID == (uint)IconID.HighestStakes) + { + Towers.Add(new(actor.Position, 6, 3, 3, _forbidden, WorldState.FutureTime(6.1f))); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action == WatchedAction) + { + ++NumCasts; + Towers.Clear(); + foreach (var t in spell.Targets) + _forbidden.Set(Raid.FindSlot(t.ID)); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1Byakko.cs b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1Byakko.cs new file mode 100644 index 0000000000..67ce2bf71f --- /dev/null +++ b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1Byakko.cs @@ -0,0 +1,31 @@ +namespace BossMod.Dawntrail.Unreal.Un1Byakko; + +class StormPulseRepeat(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.StormPulseRepeat)); +class HeavenlyStrike(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.HeavenlyStrike), new AOEShapeCircle(3), true); +class FireAndLightningBoss(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FireAndLightningBoss), new AOEShapeRect(54.3f, 10)); +class FireAndLightningAdd(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FireAndLightningAdd), new AOEShapeRect(54.75f, 10)); +class SteelClaw(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.SteelClaw), new AOEShapeCone(17.75f, 30.Degrees()), (uint)OID.Hakutei); // TODO: verify angle +class WhiteHerald(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.WhiteHerald, ActionID.MakeSpell(AID.WhiteHerald), 15, 5.1f); // TODO: verify falloff +class DistantClap(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DistantClap), new AOEShapeDonut(4, 25)); +class SweepTheLegBoss(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SweepTheLegBoss), new AOEShapeCone(28.3f, 135.Degrees())); + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1007, NameID = 7092, PlanLevel = 100)] +public class Un1Byakko(WorldState ws, Actor primary) : BossModule(ws, primary, default, new ArenaBoundsCircle(20)) +{ + private Actor? _hakutei; + public Actor? Boss() => PrimaryActor; + public Actor? Hakutei() => _hakutei; + + protected override void UpdateModule() + { + // TODO: this is an ugly hack, think how multi-actor fights can be implemented without it... + // the problem is that on wipe, any actor can be deleted and recreated in the same frame + _hakutei ??= StateMachine.ActivePhaseIndex >= 0 ? Enemies(OID.Hakutei).FirstOrDefault() : null; + } + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actor(PrimaryActor); + Arena.Actor(_hakutei); + } +} diff --git a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1ByakkoEnums.cs b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1ByakkoEnums.cs new file mode 100644 index 0000000000..fd54d1cd8d --- /dev/null +++ b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1ByakkoEnums.cs @@ -0,0 +1,84 @@ +namespace BossMod.Dawntrail.Unreal.Un1Byakko; + +public enum OID : uint +{ + Boss = 0x4579, // R4.300, x1 + Hakutei = 0x4581, // R4.750, x1 - tiger + Helper = 0x464D, // R0.500, x29, mixed types + AratamaForce = 0x4589, // R0.700, x0 (spawn during fight) - orbs from center + IntermissionHakutei = 0x458B, // R4.750, x0 (spawn during fight) + AramitamaSoul = 0x458A, // R1.000, x0 (spawn during fight) - orbs from edge + AratamaPuddle = 0x1E8EA9, // R0.500, x0 (spawn during fight), EventObj type + IntermissionHelper = 0x1EA87E, // R0.500, x0 (spawn during fight), EventObj type + VacuumClaw = 0x1EA957, // R0.500, x0 (spawn during fight), EventObj type +} + +public enum AID : uint +{ + AutoAttackBoss = 39987, // Boss->player, no cast, single-target + AutoAttackAdd = 39988, // Hakutei->player, no cast, single-target + TeleportBoss = 39929, // Boss->location, no cast, single-target + TeleportAdd = 39927, // Hakutei->location, no cast, single-target + StormPulse = 39933, // Boss->self, 4.0s cast, range 100 circle, raidwide + StormPulseRepeat = 39964, // Boss->self, no cast, range 100 circle, raidwide + HeavenlyStrike = 39931, // Boss->players, 4.0s cast, range 3 circle tankbuster + + StateOfShock = 39937, // Boss->player, 4.0s cast, single-target, stun target to grab & throw it + StateOfShockSecond = 39928, // Boss->player, no cast, single-target, stun second target to grab & throw it + Clutch = 39938, // Boss->player, no cast, single-target, grab target + HighestStakes = 39939, // Boss->location, 5.0s cast, single-target, jump + HighestStakesAOE = 39940, // Helper->location, no cast, range 6 circle tower + + UnrelentingAnguish = 39950, // Boss->self, 3.0s cast, single-target, visual (orbs) + UnrelentingAnguishAratama = 39958, // AratamaForce->self, no cast, range 2 circle, orb explosion + OminousWind = 39948, // Boss->self, no cast, single-target, apply bubbles + OminousWindAOE = 39949, // Helper->self, no cast, range 6 circle + FireAndLightningBoss = 39930, // Boss->self, 4.0s cast, range 50+R width 20 rect + + DanceOfTheIncomplete = 39924, // Boss->self, no cast, single-target, visual (split off tiger) + AddAppear = 39923, // Hakutei->self, no cast, single-target, visual (start appear animation) + AratamaPuddle = 39926, // Helper->location, no cast, range 4 circle, puddle drop + SteelClaw = 39936, // Hakutei->self, no cast, range 13+R ?-degree cone, cleave + WhiteHerald = 39962, // Hakutei->self, no cast, range 50 circle with ? falloff + DistantClap = 39934, // Boss->self, 5.0s cast, range 4-25 donut + FireAndLightningAdd = 39935, // Hakutei->self, 4.0s cast, range 50+R width 20 rect + + VoiceOfThunder = 39959, // Hakutei->self, no cast, single-target, visual (physical damage down orbs) + VoiceOfThunderAratama = 39965, // AramitamaSoul->self, no cast, range 2 circle, orb explosion if soaked + VoiceOfThunderAratamaFail = 39960, // AramitamaSoul->Hakutei, no cast, single-target, orb explosion if it reaches the tiger, heals + RoarOfThunder = 39961, // Hakutei->self, 20.0s cast, range 100 circle, raidwide scaled by remaining hp + RoarOfThunderEnd1 = 39968, // Hakutei->Boss, no cast, single-target, visual (???) + RoarOfThunderEnd2 = 39967, // Boss->self, no cast, single-target, visual (???) + IntermissionOrbVisual = 39951, // Boss->self, no cast, single-target, visual (start next set of orbs) + IntermissionOrbSpawn = 39952, // Helper->location, no cast, single-target, visual (location of next orb) + IntermissionOrbAratama = 39953, // Helper->location, no cast, range 2 circle + ImperialGuard = 39954, // IntermissionHakutei->self, 3.0s cast, range 40+R width 5 rect + IntermissionSweepTheLegVisual = 39956, // Boss->self, no cast, single-target, visual (start donut) + IntermissionSweepTheLeg = 39957, // Helper->self, 5.1s cast, range 5-25 donut + IntermissionEnd = 39970, // Boss->self, no cast, single-target, visual (intermission end) + FellSwoop = 39963, // Helper->self, no cast, range 100 circle, raidwide + + AnswerOnHigh = 39941, // Boss->self, no cast, single-target, visual (exaflare start) + HundredfoldHavocFirst = 39942, // Helper->self, 5.0s cast, range 5 circle exaflare + HundredfoldHavocRest = 39943, // Helper->self, no cast, range 5 circle exaflare + SweepTheLegBoss = 39932, // Boss->self, 4.0s cast, range 24+R 270-degree cone + + Bombogenesis = 39944, // Boss->self, no cast, single-target, visual (3 baited puddle icons) + GaleForce = 39945, // Helper->self, no cast, range 6 circle, baited puddle + VacuumClaw = 39946, // Helper->self, no cast, range ? circle, growing voidzone aoe + VacuumBlade = 39947, // Helper->self, no cast, range 100 circle, raidwide +} + +public enum SID : uint +{ + Stun = 201, // Boss->player, extra=0x0 + OminousWind = 1481, // none->player, extra=0x0 +} + +public enum IconID : uint +{ + HighestStakes = 62, // Helper->self + AratamaPuddle = 4, // player->self + WhiteHerald = 87, // player->self + Bombogenesis = 101, // player->self +} diff --git a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1ByakkoStates.cs b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1ByakkoStates.cs new file mode 100644 index 0000000000..6401963f60 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1ByakkoStates.cs @@ -0,0 +1,301 @@ +namespace BossMod.Dawntrail.Unreal.Un1Byakko; + +class Un1ByakkoStates : StateMachineBuilder +{ + private readonly Un1Byakko _module; + + public Un1ByakkoStates(Un1Byakko module) : base(module) + { + _module = module; + DeathPhase(0, SinglePhase) + .ActivateOnEnter(); // these orbs linger after mechanic ends + } + + private void SinglePhase(uint id) + { + StormPulse(id, 6.2f); + HeavenlyStrike(id + 0x10000, 2.1f); + StateOfShock(id + 0x20000, 6.1f); + UnrelentingAnguish1(id + 0x30000, 11.1f); + Hakutei1(id + 0x40000, 13.4f); + Intermission(id + 0x50000, 2.1f); + HeavenlyStrike(id + 0x60000, 7.2f); + HundredfoldHavoc1(id + 0x70000, 6.2f); + UnrelentingAnguish2(id + 0x80000, 11.6f); + Hakutei2(id + 0x90000, 8.9f); // TODO: variance is quite large? + StormPulseDouble(id + 0xA0000, 11.2f); + HundredfoldHavoc2(id + 0xB0000, 8); + HeavenlyStrike(id + 0xC0000, 10.2f); + StormPulseDouble(id + 0xD0000, 6.2f); + DistantClap(id + 0xE0000, 6.1f); + HeavenlyStrike(id + 0xF0000, 2.1f); + UnrelentingAnguish2(id + 0x100000, 9.5f); + StormPulseDouble(id + 0x110000, 10.5f); + HundredfoldHavoc1(id + 0x120000, 6.1f); + // TODO: ??? + SimpleState(id + 0xFF0000, 10000, "???"); + } + + private State StormPulse(uint id, float delay, string name = "Raidwide") + { + return Cast(id, AID.StormPulse, delay, 4, name) + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void StormPulseDouble(uint id, float delay) + { + StormPulse(id, delay, "Raidwide 1"); + ComponentCondition(id + 2, 2.2f, comp => comp.NumCasts > 0, "Raidwide 2") + .ActivateOnEnter() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + } + + private State HeavenlyStrike(uint id, float delay) + { + return Cast(id, AID.HeavenlyStrike, delay, 4, "Tankbuster") + .ActivateOnEnter() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Tankbuster); + } + + private void DistantClap(uint id, float delay) + { + Cast(id, AID.DistantClap, delay, 5, "Donut") + .ActivateOnEnter() + .DeactivateOnExit(); + } + + private void StateOfShock(uint id, float delay) + { + Cast(id, AID.StateOfShock, delay, 4); + ComponentCondition(id + 0x10, 0.9f, comp => comp.NumStuns > 0, "Grab tank") + .ActivateOnEnter() + .ActivateOnEnter(); + Cast(id + 0x20, AID.HighestStakes, 1.5f, 5); + ComponentCondition(id + 0x30, 0.8f, comp => comp.NumCasts > 0, "Tower 1") + .DeactivateOnExit(); + ComponentCondition(id + 0x40, 2, comp => comp.NumCasts > 0) + .ActivateOnEnter(); + ComponentCondition(id + 0x41, 0.9f, comp => comp.NumStuns > 0, "Grab tank"); + Cast(id + 0x50, AID.HighestStakes, 1.2f, 5); + ComponentCondition(id + 0x60, 0.8f, comp => comp.NumCasts > 1, "Tower 2") + .DeactivateOnExit() + .DeactivateOnExit(); + } + + private void UnrelentingAnguish1(uint id, float delay) + { + Cast(id, AID.UnrelentingAnguish, delay, 3, "Orbs start"); + StormPulse(id + 0x10, 2.2f); + ComponentCondition(id + 0x20, 2.9f, comp => comp.Targets.Any()) + .ActivateOnEnter(); + Cast(id + 0x30, AID.FireAndLightningBoss, 4.5f, 4, "Line") + .ActivateOnEnter() + .DeactivateOnExit(); + ComponentCondition(id + 0x40, 1.5f, comp => comp.Targets.None(), "Orbs end") + .DeactivateOnExit(); + } + + private void Hakutei1(uint id, float delay) + { + ActorTargetable(id, _module.Hakutei, true, delay, "Tiger appears") + .ActivateOnEnter(); // icons appear ~2s before tiger + ComponentCondition(id + 0x10, 3.1f, comp => comp.NumFinishedSpreads > 0, "Puddle baits start") + .ActivateOnEnter(); + StormPulse(id + 0x20, 4) + .ActivateOnEnter() + .DeactivateOnExit(); + ComponentCondition(id + 0x30, 0.1f, comp => comp.NumCasts > 0, "Cleave 1"); + CastStart(id + 0x40, AID.HeavenlyStrike, 6); + ComponentCondition(id + 0x41, 0.1f, comp => comp.NumCasts > 1, "Cleave 2") + .ActivateOnEnter() + .DeactivateOnExit(); + CastEnd(id + 0x42, 3.9f, "Tankbuster") + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Tankbuster); + ActorTargetable(id + 0x50, _module.Hakutei, false, 3.1f, "Tiger disappears"); + + ComponentCondition(id + 0x60, 0.7f, comp => comp.Active) + .ActivateOnEnter(); + CastStart(id + 0x61, AID.DistantClap, 2.4f); + ComponentCondition(id + 0x62, 2.7f, comp => comp.NumFinishedSpreads > 0, "Flare") + .ActivateOnEnter() + .DeactivateOnExit(); + ActorTargetable(id + 0x63, _module.Hakutei, true, 2, "Tiger reappears"); + CastEnd(id + 0x64, 0.3f, "Donut") + .DeactivateOnExit(); + ComponentCondition(id + 0x65, 3.9f, comp => comp.NumCasts > 0, "Line") + .ActivateOnEnter() + .DeactivateOnExit(); + StormPulse(id + 0x70, 0.2f) + .DeactivateOnExit(); + } + + private void Intermission(uint id, float delay) + { + ActorTargetable(id, _module.Boss, false, delay, "Boss disappears"); + ActorCast(id + 0x10, _module.Hakutei, AID.RoarOfThunder, 4.4f, 20, true, "Add enrage") + .ActivateOnEnter() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide | StateMachine.StateHint.DowntimeStart); + ComponentCondition(id + 0x20, 42.2f, comp => comp.NumCasts > 0, "Donut 1") + .ActivateOnEnter() + .ActivateOnEnter(); + ComponentCondition(id + 0x21, 5.7f, comp => comp.NumCasts > 0, "Line 1") + .ActivateOnEnter(); + ComponentCondition(id + 0x22, 12, comp => comp.NumCasts > 1, "Line 2"); + ComponentCondition(id + 0x23, 13.6f, comp => comp.NumCasts > 1, "Donut 2") + .DeactivateOnExit() + .DeactivateOnExit(); + ComponentCondition(id + 0x24, 3.4f, comp => comp.NumCasts > 2, "Line 3") + .DeactivateOnExit(); + ComponentCondition(id + 0x25, 27.7f, comp => comp.NumCasts > 0, "Raidwide") + .ActivateOnEnter() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + ActorTargetable(id + 0x30, _module.Boss, true, 7.9f, "Boss reappears") + .SetHint(StateMachine.StateHint.DowntimeEnd); + } + + private void HundredfoldHavoc1(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Active) + .ActivateOnEnter(); + CastStart(id + 1, AID.StateOfShock, 2.1f); + ComponentCondition(id + 2, 2.9f, comp => comp.NumCasts > 0, "Exaflares start"); + CastEnd(id + 3, 1.1f); + + ComponentCondition(id + 0x10, 0.9f, comp => comp.NumStuns > 0, "Grab tank") + .ActivateOnEnter() + .ActivateOnEnter(); + Cast(id + 0x20, AID.HighestStakes, 1.5f, 5) + .DeactivateOnExit(); + ComponentCondition(id + 0x30, 0.8f, comp => comp.NumCasts > 0, "Tower 1") + .DeactivateOnExit(); + ComponentCondition(id + 0x40, 2, comp => comp.NumCasts > 0) + .ActivateOnEnter(); + ComponentCondition(id + 0x41, 0.9f, comp => comp.NumStuns > 0, "Grab tank"); + Cast(id + 0x50, AID.HighestStakes, 1.2f, 5); + ComponentCondition(id + 0x60, 0.8f, comp => comp.NumCasts > 1, "Tower 2") + .DeactivateOnExit() + .DeactivateOnExit(); + + Cast(id + 0x1000, AID.SweepTheLegBoss, 1.9f, 4, "Wide cone") + .ActivateOnEnter() + .DeactivateOnExit(); + } + + private void UnrelentingAnguish2(uint id, float delay) + { + Cast(id, AID.UnrelentingAnguish, delay, 3, "Orbs start"); + StormPulseDouble(id + 0x10, 2.1f); + + ComponentCondition(id + 0x20, 2, comp => comp.CurrentBaits.Count > 0) + .ActivateOnEnter(); + ComponentCondition(id + 0x21, 6.9f, comp => comp.Targets.Any()) + .ActivateOnEnter() + .ActivateOnEnter(); + ComponentCondition(id + 0x22, 1.2f, comp => comp.NumCasts > 0, "Baits") + .DeactivateOnExit(); + + Cast(id + 0x30, AID.FireAndLightningBoss, 3.4f, 4, "Line") + .ActivateOnEnter() + .DeactivateOnExit(); + ComponentCondition(id + 0x40, 1.5f, comp => comp.Targets.None(), "Orbs end") + .DeactivateOnExit(); + + Cast(id + 0x50, AID.FireAndLightningBoss, 3.4f, 4, "Line") + .ActivateOnEnter() + .DeactivateOnExit(); + ComponentCondition(id + 0x60, 0.7f, comp => comp.NumCasts >= 39, "Voidzones end") + .DeactivateOnExit(); + } + + private void Hakutei2(uint id, float delay) + { + ActorTargetable(id, _module.Hakutei, true, delay, "Tiger appears") + .ActivateOnEnter(); // icons appear ~2s before tiger + + ComponentCondition(id + 0x10, 3.1f, comp => comp.NumFinishedSpreads > 0, "Puddle baits start") + .ActivateOnEnter(); + ActorTargetable(id + 0x20, _module.Hakutei, false, 6, "Tiger disappears"); + + ComponentCondition(id + 0x30, 0.7f, comp => comp.Active) + .ActivateOnEnter(); + CastStart(id + 0x31, AID.DistantClap, 2.4f); + ComponentCondition(id + 0x32, 2.7f, comp => comp.NumFinishedSpreads > 0, "Flare") + .ActivateOnEnter() + .DeactivateOnExit(); + ActorTargetable(id + 0x33, _module.Hakutei, true, 2, "Tiger reappears"); + CastEnd(id + 0x34, 0.3f, "Donut 1") + .DeactivateOnExit(); + + CastStart(id + 0x40, AID.DistantClap, 3.2f) + .ActivateOnEnter(); + ComponentCondition(id + 0x41, 0.8f, comp => comp.NumCasts > 0, "Line 1") + .ActivateOnEnter() + .DeactivateOnExit(); + CastEnd(id + 0x42, 4.2f, "Donut 2") + .DeactivateOnExit(); + ComponentCondition(id + 0x50, 4, comp => comp.NumCasts > 0, "Line 2") + .ActivateOnEnter() + .DeactivateOnExit(); + + HeavenlyStrike(id + 0x100, 3.2f) + .ActivateOnEnter() + .DeactivateOnExit(); + + ActorCastStart(id + 0x200, _module.Hakutei, AID.RoarOfThunder, 10.6f, false) + .ActivateOnEnter() + .DeactivateOnExit(); + StormPulseDouble(id + 0x210, 5.7f); + ActorCastEnd(id + 0x220, _module.Hakutei, 8.1f, false, "Add enrage") + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + + Cast(id + 0x300, AID.FireAndLightningBoss, 8.3f, 4, "Line") + .ActivateOnEnter() + .DeactivateOnExit(); + } + + private void HundredfoldHavoc2(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.CurrentBaits.Count > 0) + .ActivateOnEnter(); + + ComponentCondition(id + 0x10, 2.2f, comp => comp.Active) + .ActivateOnEnter(); + CastStart(id + 0x11, AID.StateOfShock, 4.6f); + ComponentCondition(id + 0x12, 0.4f, comp => comp.NumCasts > 0, "Exaflares start"); + ComponentCondition(id + 0x13, 0.9f, comp => comp.NumCasts > 0, "Baits") + .ActivateOnEnter() + .DeactivateOnExit(); + CastEnd(id + 0x14, 2.7f); + + ComponentCondition(id + 0x20, 0.9f, comp => comp.NumStuns > 0, "Grab tank") + .ActivateOnEnter() + .ActivateOnEnter(); + Cast(id + 0x30, AID.HighestStakes, 1.3f, 5) + .DeactivateOnExit(); + ComponentCondition(id + 0x40, 0.8f, comp => comp.NumCasts > 0, "Tower 1") + .DeactivateOnExit(); + ComponentCondition(id + 0x50, 2, comp => comp.NumCasts > 0) + .ActivateOnEnter(); + ComponentCondition(id + 0x51, 0.9f, comp => comp.NumStuns > 0, "Grab tank"); + CastStart(id + 0x60, AID.HighestStakes, 1.2f); + ComponentCondition(id + 0x61, 3, comp => comp.NumCasts > 0, "Raidwide") + .ActivateOnEnter() + .DeactivateOnExit() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + CastEnd(id + 0x62, 2); + ComponentCondition(id + 0x70, 0.8f, comp => comp.NumCasts > 1, "Tower 2") + .DeactivateOnExit() + .DeactivateOnExit(); + + Cast(id + 0x1000, AID.SweepTheLegBoss, 1.9f, 4, "Wide cone") + .ActivateOnEnter() + .DeactivateOnExit(); + } +} diff --git a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/UnrelentingAnguish.cs b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/UnrelentingAnguish.cs new file mode 100644 index 0000000000..93552400fa --- /dev/null +++ b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/UnrelentingAnguish.cs @@ -0,0 +1,34 @@ +namespace BossMod.Dawntrail.Unreal.Un1Byakko; + +class UnrelentingAnguish(BossModule module) : Components.PersistentVoidzone(module, 2, m => m.Enemies(OID.AratamaForce).Where(z => !z.IsDead), 2); + +// TODO: show something +class OminousWind(BossModule module) : BossComponent(module) +{ + public BitMask Targets; + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.OminousWind) + Targets.Set(Raid.FindSlot(actor.InstanceID)); + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.OminousWind) + Targets.Clear(Raid.FindSlot(actor.InstanceID)); + } +} + +class GaleForce(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(6), (uint)IconID.Bombogenesis, ActionID.MakeSpell(AID.GaleForce), 8.1f, true); + +class VacuumClaw(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.VacuumClaw), "GTFO from voidzone!") +{ + private readonly IReadOnlyList _sources = module.Enemies(OID.VacuumClaw); + + private static readonly AOEShapeCircle _shape = new(10); // TODO: verify radius; initial voidzone is 6, but spell radius is 1 in sheets; after 5th hit we get 4 stacks of area-of-influence increase + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _sources.Select(s => new AOEInstance(_shape, s.Position)); +} + +class VacuumBlade(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.VacuumBlade)); diff --git a/BossMod/Replay/ReplayParserLog.cs b/BossMod/Replay/ReplayParserLog.cs index 2d6b22c7c8..0179cbf930 100644 --- a/BossMod/Replay/ReplayParserLog.cs +++ b/BossMod/Replay/ReplayParserLog.cs @@ -299,7 +299,6 @@ public static Replay Parse(string path, ref float progress, CancellationToken ca private ulong _qpcStart; private uint _legacyFrameIndex; private DateTime _legacyPrevTS; - private readonly Dictionary _legacyIcons = []; // TODO: remove private double _invQPF = 1.0 / TimeSpan.TicksPerSecond; private ReplayParserLog(Input input, ReplayBuilder builder) @@ -615,11 +614,7 @@ private ActorState.OpCastEvent ParseActorCastEvent() private ActorState.OpEffectResult ParseActorEffectResult() => new(_input.ReadActorID(), _input.ReadUInt(false), _input.ReadInt()); private ActorState.OpStatus ParseActorStatus(bool gainOrUpdate) => new(_input.ReadActorID(), _input.ReadInt(), gainOrUpdate ? _input.ReadStatus() : default); - private ActorState.OpIcon ParseActorIcon() - { - var source = _input.ReadActorID(); - return new(source, _input.ReadUInt(false), _version >= 22 ? _input.ReadActorID() : _legacyIcons.Remove(source, out var target) ? target : 0); - } + private ActorState.OpIcon ParseActorIcon() => new(_input.ReadActorID(), _input.ReadUInt(false), _version >= 22 ? _input.ReadActorID() : 0); private ActorState.OpEventObjectStateChange ParseActorEventObjectStateChange() => new(_input.ReadActorID(), _input.ReadUShort(true)); private ActorState.OpEventObjectAnimation ParseActorEventObjectAnimation() => new(_input.ReadActorID(), _input.ReadUShort(true), _input.ReadUShort(true)); private ActorState.OpPlayActionTimelineEvent ParseActorPlayActionTimelineEvent() => new(_input.ReadActorID(), _input.ReadUShort(true)); @@ -711,28 +706,7 @@ private ClientState.OpClassJobLevelsChange ParseClientClassJobLevels() private ClientState.OpFocusTargetChange ParseClientFocusTarget() => new(_input.ReadULong(true)); private NetworkState.OpIDScramble ParseNetworkIDScramble() => new(_input.ReadUInt(false)); - private NetworkState.OpServerIPC ParseNetworkServerIPC() - { - var packet = new NetworkState.ServerIPC((Network.ServerIPC.PacketID)_input.ReadInt(), _input.ReadUShort(false), _input.ReadUInt(false), _input.ReadUInt(true), new(_input.ReadLong()), _input.ReadBytes()); - - if (_version < 22 && packet.ID == Network.ServerIPC.PacketID.ActorControl) - { - if (packet.Payload.Length > 0 && packet.Payload[0] == (byte)Network.ServerIPC.ActorControlCategory.TargetIcon) - { - if (packet.Payload.Length >= 12) - { - unsafe - { - fixed (byte* p = &packet.Payload[8]) - { - _legacyIcons[packet.SourceServerActor] = *(uint*)p; - } - } - } - } - } - return new(packet); - } + private NetworkState.OpServerIPC ParseNetworkServerIPC() => new(new((Network.ServerIPC.PacketID)_input.ReadInt(), _input.ReadUShort(false), _input.ReadUInt(false), _input.ReadUInt(true), new(_input.ReadLong()), _input.ReadBytes())); private ActorHPMP ReadActorHPMP() { diff --git a/FFXIVClientStructs b/FFXIVClientStructs index 028dd87ddb..31a434115b 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 028dd87ddbbc5c7851db8202b9d41676fa11d92f +Subproject commit 31a434115b617d94b239979f71fc48a60af68a43 diff --git a/TODO b/TODO index 5626bf276b..56c2f6786e 100644 --- a/TODO +++ b/TODO @@ -1,8 +1,4 @@ immediate plans -- 7.1 --- revert csproj changes --- bump api version in workflow --- remove icontarget hack - nechuciho - seen incorrect rotations... - questbattles - collisions for pathfinding