From adaebcd3da23d969471d7518308654c0c30a8021 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Tue, 6 Aug 2024 00:43:00 +0100 Subject: [PATCH] M2S --- BossMod/Components/SharedTankbuster.cs | 25 ++ .../RM02SHoneyBLovely/DropSplashOfVenom.cs | 66 +++++ .../RM02SHoneyBLovely/HoneyBLiveBeat.cs | 187 +++++++++++++ .../RM02SHoneyBLovely/RM02SHoneyBLovely.cs | 12 + .../RM02SHoneyBLovelyEnums.cs | 123 +++++++++ .../RM02SHoneyBLovelyStates.cs | 255 ++++++++++++++++++ .../Savage/RM02SHoneyBLovely/RottenHeart.cs | 65 +++++ .../Savage/RM02SHoneyBLovely/StageCombo.cs | 63 +++++ BossMod/Network/PacketDecoder.cs | 2 + BossMod/Network/ServerIPC.cs | 2 + 10 files changed, 800 insertions(+) create mode 100644 BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/DropSplashOfVenom.cs create mode 100644 BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/HoneyBLiveBeat.cs create mode 100644 BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovely.cs create mode 100644 BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovelyEnums.cs create mode 100644 BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovelyStates.cs create mode 100644 BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RottenHeart.cs create mode 100644 BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/StageCombo.cs diff --git a/BossMod/Components/SharedTankbuster.cs b/BossMod/Components/SharedTankbuster.cs index 73c97af71..5aefc5d6a 100644 --- a/BossMod/Components/SharedTankbuster.cs +++ b/BossMod/Components/SharedTankbuster.cs @@ -82,3 +82,28 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) Source = Target = null; } } + +// shared tankbuster at icon +public class IconSharedTankbuster(BossModule module, uint iconId, ActionID aid, AOEShape shape, float activationDelay = 5.1f, bool originAtTarget = false) : GenericSharedTankbuster(module, aid, shape, originAtTarget) +{ + public IconSharedTankbuster(BossModule module, uint iconId, ActionID aid, float radius, float activationDelay = 5.1f) : this(module, iconId, aid, new AOEShapeCircle(radius), activationDelay, true) { } + + public virtual Actor? BaitSource(Actor target) => Module.PrimaryActor; + + public override void OnEventIcon(Actor actor, uint iconID) + { + if (iconID == iconId) + { + Source = BaitSource(actor); + Target = actor; + Activation = WorldState.FutureTime(activationDelay); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + base.OnEventCast(caster, spell); + if (spell.Action == WatchedAction) + Source = Target = null; + } +} diff --git a/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/DropSplashOfVenom.cs b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/DropSplashOfVenom.cs new file mode 100644 index 000000000..deadf28bd --- /dev/null +++ b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/DropSplashOfVenom.cs @@ -0,0 +1,66 @@ +namespace BossMod.Dawntrail.Savage.RM02SHoneyBLovely; + +class DropSplashOfVenom(BossModule module) : Components.UniformStackSpread(module, 6, 6, 2, 2, alwaysShowSpreads: true) +{ + public enum Mechanic { None, Pairs, Spread } + + private Mechanic NextMechanic; + + public override void AddGlobalHints(GlobalHints hints) + { + if (NextMechanic != Mechanic.None) + hints.Add(NextMechanic.ToString()); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.SplashOfVenom: + case AID.SpreadLove: + NextMechanic = Mechanic.Spread; + break; + case AID.DropOfVenom: + case AID.DropOfLove: + NextMechanic = Mechanic.Pairs; + break; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.TemptingTwistAOE: + case AID.HoneyBeelineAOE: + case AID.TemptingTwistBeatAOE: + case AID.HoneyBeelineBeatAOE: + switch (NextMechanic) + { + case Mechanic.Pairs: + // note: it's random whether dd or supports are hit, select supports arbitrarily + AddStacks(Raid.WithoutSlot(true).Where(p => p.Class.IsSupport()), WorldState.FutureTime(4.5f)); + break; + case Mechanic.Spread: + AddSpreads(Raid.WithoutSlot(true), WorldState.FutureTime(4.5f)); + break; + } + break; + case AID.SplashOfVenomAOE: + case AID.DropOfVenomAOE: + case AID.SpreadLoveAOE: + case AID.DropOfLoveAOE: + Spreads.Clear(); + Stacks.Clear(); + NextMechanic = Mechanic.None; + break; + } + } +} + +class TemptingTwist(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TemptingTwistAOE), new AOEShapeDonut(6, 30)); // TODO: verify inner radius +class TemptingTwistBeat(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TemptingTwistBeatAOE), new AOEShapeDonut(6, 30)); // TODO: verify inner radius +class HoneyBeeline(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HoneyBeelineAOE), new AOEShapeRect(30, 7, 30)); +class HoneyBeelineBeat(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HoneyBeelineBeatAOE), new AOEShapeRect(30, 7, 30)); +class PoisonCloudSplinter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PoisonCloudSplinter), new AOEShapeCircle(8)); +class SweetheartSplinter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SweetheartSplinter), new AOEShapeCircle(8)); diff --git a/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/HoneyBLiveBeat.cs b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/HoneyBLiveBeat.cs new file mode 100644 index 000000000..6707c9dfd --- /dev/null +++ b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/HoneyBLiveBeat.cs @@ -0,0 +1,187 @@ +namespace BossMod.Dawntrail.Savage.RM02SHoneyBLovely; + +class HoneyBLiveBeat1(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.HoneyBLiveBeat1AOE)); +class HoneyBLiveBeat2(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.HoneyBLiveBeat2AOE)); +class HoneyBLiveBeat3(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.HoneyBLiveBeat3AOE)); + +class HoneyBLiveHearts(BossModule module) : BossComponent(module) +{ + public int[] Hearts = new int[PartyState.MaxPartySize]; + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + var hearts = NumHearts((SID)status.ID); + if (hearts >= 0 && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + Hearts[slot] = hearts; + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + var hearts = NumHearts((SID)status.ID); + if (hearts >= 0 && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0 && Hearts[slot] == hearts) + Hearts[slot] = 0; + } + + private int NumHearts(SID sid) => sid switch + { + SID.Hearts0 => 0, + SID.Hearts1 => 1, + SID.Hearts2 => 2, + SID.Hearts3 => 3, + SID.Hearts4 => 4, + _ => -1 + }; +} + +abstract class Fracture(BossModule module) : Components.CastTowers(module, ActionID.MakeSpell(AID.Fracture), 4) +{ + protected abstract BitMask UpdateForbidden(); + + public override void Update() + { + var forbidden = UpdateForbidden(); + foreach (ref var t in Towers.AsSpan()) + t.ForbiddenSoakers = forbidden; + } +} + +class Fracture1(BossModule module) : Fracture(module) +{ + private readonly HoneyBLiveHearts? _hearts = module.FindComponent(); + + protected override BitMask UpdateForbidden() + { + var forbidden = new BitMask(); + if (_hearts != null) + for (int i = 0; i < _hearts.Hearts.Length; ++i) + if (_hearts.Hearts[i] == 3) + forbidden.Set(i); + return forbidden; + } +} + +class Fracture2(BossModule module) : Fracture(module) +{ + private BitMask _spreads; + private readonly HoneyBLiveHearts? _hearts = module.FindComponent(); + + protected override BitMask UpdateForbidden() + { + var forbidden = _spreads; + if (_hearts != null) + for (int i = 0; i < _hearts.Hearts.Length; ++i) + if (_hearts.Hearts[i] > 1) + forbidden.Set(i); + return forbidden; + } + + public override void OnEventIcon(Actor actor, uint iconID) + { + // spread targets should never take towers + if (iconID == (uint)IconID.Heartsore) + _spreads.Set(Raid.FindSlot(actor.InstanceID)); + } +} + +class Fracture3 : Fracture +{ + private BitMask _defamations; + + public Fracture3(BossModule module) : base(module) + { + var bigBurst = module.FindComponent(); + if (bigBurst != null) + { + var order = bigBurst.NumCasts == 0 ? 1 : 2; + _defamations = Raid.WithSlot(true).WhereSlot(i => bigBurst.Order[i] == order).Mask(); + } + } + + protected override BitMask UpdateForbidden() => _defamations; +} + +class Loveseeker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LoveseekerAOE), new AOEShapeCircle(10)); +class HeartStruck(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HeartStruck), 6); +class Heartsore(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.Heartsore, ActionID.MakeSpell(AID.Heartsore), 6, 7.1f); + +class Sweetheart(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _adds = []; + + private static readonly AOEShapeCircle _shape = new(2); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _adds.Where(a => !a.IsDestroyed).Select(a => new AOEInstance(_shape, a.Position)); + + public override void OnActorPlayActionTimelineEvent(Actor actor, ushort id) + { + if ((OID)actor.OID == OID.Sweetheart && id == 0x11D3) + _adds.Add(actor); + } +} + +abstract class Heartsick(BossModule module, bool roles) : Components.StackWithIcon(module, (uint)IconID.Heartsick, ActionID.MakeSpell(AID.Heartsick), 6, 7, roles ? 2 : 4) +{ + private readonly HoneyBLiveHearts? _hearts = module.FindComponent(); + + public override void Update() + { + if (_hearts != null) + { + foreach (ref var stack in Stacks.AsSpan()) + { + for (int i = 0; i < _hearts.Hearts.Length; ++i) + { + stack.ForbiddenPlayers[i] = roles + ? (_hearts.Hearts[i] > 0 || stack.Target.Class.IsSupport() != Raid[i]?.Class.IsSupport()) + : _hearts.Hearts[i] == 3; + } + } + } + } +} +class Heartsick1(BossModule module) : Heartsick(module, false); +class Heartsick2(BossModule module) : Heartsick(module, true); + +class HoneyBLiveBeat3BigBurst(BossModule module) : Components.UniformStackSpread(module, 0, 14, alwaysShowSpreads: true) +{ + public int NumCasts; + public int[] Order = new int[PartyState.MaxPartySize]; + private readonly DateTime[] _activation = new DateTime[2]; + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Order[slot] != 0) + hints.Add($"Order: {Order[slot]}", false); + base.AddHints(slot, actor, hints); + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.PoisonNPop) + { + var order = (status.ExpireAt - WorldState.CurrentTime).TotalSeconds > 30 ? 1 : 0; + _activation[order] = status.ExpireAt; + var slot = Raid.FindSlot(actor.InstanceID); + if (slot >= 0) + Order[slot] = order + 1; + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Fracture && Spreads.Count == 0) + { + var order = NumCasts == 0 ? 1 : 2; + AddSpreads(Raid.WithSlot(true).WhereSlot(i => Order[i] == order).Actors(), _activation[order - 1]); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.HoneyBLiveBeat3BigBurst) + { + ++NumCasts; + Spreads.Clear(); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovely.cs b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovely.cs new file mode 100644 index 000000000..3ebe716ea --- /dev/null +++ b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovely.cs @@ -0,0 +1,12 @@ +namespace BossMod.Dawntrail.Savage.RM02SHoneyBLovely; + +class StingingSlash(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCone(50, 45.Degrees()), (uint)IconID.StingingSlash, ActionID.MakeSpell(AID.StingingSlashAOE)); +class KillerSting(BossModule module) : Components.IconSharedTankbuster(module, (uint)IconID.KillerSting, ActionID.MakeSpell(AID.KillerStingAOE), 6); +class BlindingLoveBait(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BlindingLoveBaitAOE), new AOEShapeRect(50, 4)); +class BlindingLoveCharge1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BlindingLoveCharge1AOE), new AOEShapeRect(45, 5)); +class BlindingLoveCharge2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BlindingLoveCharge2AOE), new AOEShapeRect(45, 5)); +class PoisonStingBait(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.PoisonStingAOE), new AOEShapeCircle(6), true); +class PoisonStingVoidzone(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.PoisonStingVoidzone).Where(z => z.EventState != 7)); +class BeeSting(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.BeeStingAOE), 6, 4); + +public class RM02SHoneyBLovely(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovelyEnums.cs b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovelyEnums.cs new file mode 100644 index 000000000..c7d01b7d0 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovelyEnums.cs @@ -0,0 +1,123 @@ +namespace BossMod.Dawntrail.Savage.RM02SHoneyBLovely; + +public enum OID : uint +{ + Boss = 0x422D, // R5.004, x1 + Helper = 0x233C, // R0.500, x28, Helper type + Groupbee = 0x422E, // R1.500, x0 (spawn during fight) - charging bee + Sweetheart = 0x422F, // R1.000, x0 (spawn during fight) - small heart moving in straight line + PoisonCloud = 0x4231, // R1.000, x0 (spawn during fight) + PoisonStingVoidzone = 0x1EBAA1, // R0.500, x0 (spawn during fight), EventObj type + LoveMeTenderTower1 = 0x1EBAA2, // R0.500, x0 (spawn during fight), EventObj type + LoveMeTenderTower2 = 0x1EBAA3, // R0.500, x0 (spawn during fight), EventObj type (this one plays eobjanim on enter/exit) +} + +public enum AID : uint +{ + AutoAttack = 37320, // Boss->player, no cast, single-target + Teleport = 37219, // Boss->location, no cast, single-target + CallMeHoney = 37251, // Boss->self, 5.0s cast, range 60 circle, raidwide + StingingSlash = 37275, // Boss->self, 4.0+1.0s cast, single-target, visual (tankbusters on two tanks) + StingingSlashAOE = 37277, // Helper->self, no cast, range 50 90-degree cone + KillerSting = 37276, // Boss->self, 4.0+1.0s cast, single-target, visual (shared tankbuster) + KillerStingAOE = 37278, // Helper->players, no cast, range 6 circle + + SplashOfVenom = 37252, // Boss->self, 5.0s cast, single-target, visual (prepare spreads) + SplashOfVenomAOE = 37257, // Helper->player, no cast, range 6 circle spread + DropOfVenom = 37253, // Boss->self, 5.0s cast, single-target, visual (prepare pairs) + DropOfVenomAOE = 37258, // Helper->players, no cast, range 6 circle 2-man stack + HoneyBeeline = 37254, // Boss->self, 5.5+0.7s cast, single-target + HoneyBeelineAOE = 39625, // Helper->self, 6.2s cast, range 60 width 14 rect + TemptingTwist = 37255, // Boss->self, 5.5+0.7s cast, single-target + TemptingTwistAOE = 39626, // Helper->self, 6.2s cast, range ?-30 donut + PoisonCloudAppear = 37229, // PoisonCloud->location, no cast, single-target + PoisonCloudSplinter = 37256, // PoisonCloud->self, 3.3s cast, range 8 circle + + HoneyBLiveBeat1 = 39972, // Boss->self, 2.0+6.3s cast, single-target, visual (mechanic start + raidwide) + HoneyBLiveBeat1AOE = 39551, // Helper->self, no cast, range 60 circle, raidwide + HoneyBLiveFail = 37262, // Helper->Boss, no cast, single-target, heal + damage up if someone gets 4 hearts + CenterstageCombo = 37292, // Boss->self, 5.0+1.0s cast, single-target, visual (in->cross->out) + OuterstageCombo = 37293, // Boss->self, 5.0+1.0s cast, single-target, visual (out->cross->in) + StageComboIn = 37294, // Boss->self, no cast, single-target, visual + StageComboCross = 37295, // Boss->self, no cast, single-target, visual + StageComboOut = 37296, // Boss->self, no cast, single-target, visual + LacerationOut = 37297, // Helper->self, no cast, range 7 circle + LacerationCross = 37298, // Helper->self, no cast, range 30 width 14 cross + LacerationCone = 37299, // Helper->self, no cast, range 30 45-degree cone + LacerationIn = 37300, // Helper->self, no cast, range 7-30 donut + SweetheartTouch = 37285, // Sweetheart->player, no cast, single-target, visual (extra heart if touched) + LoveMeTender = 37279, // Boss->self, 4.0s cast, single-target, visual (heart towers) + Fracture = 37283, // Helper->self, 8.0s cast, range 4 circle tower + FractureBigBurst = 37284, // Helper->self, no cast, range 60 circle, tower fail + Loveseeker = 39805, // Boss->self, 3.0+1.0s cast, single-target, visual (out) + LoveseekerAOE = 39806, // Helper->self, 4.0s cast, range 10 circle + Heartsick = 37280, // Helper->players, no cast, range 6 circle, stack that gives out 4 hearts + HoneyBFinale = 37263, // Boss->self, 5.0s cast, range 60 circle, raidwide + beat end + + AlarmPheromones = 37245, // Boss->self, 3.0s cast, single-target, visual (mechanic start) + BlindingLoveBait = 37272, // Groupbee->self, 6.3+0.7s cast, single-target + BlindingLoveBaitAOE = 39629, // Helper->self, 7.0s cast, range 50 width 8 rect, damage down + knockback 15 + BlindingLoveCharge1 = 37264, // Groupbee->location, 5.3+0.7s cast, width 10 rect charge, visual (? difference between 1 and 2) + BlindingLoveCharge1AOE = 39627, // Helper->self, 6.0s cast, range 45 width 10 rect + BlindingLoveCharge2 = 37265, // Groupbee->location, 5.3+0.7s cast, width 10 rect charge, visual + BlindingLoveCharge2AOE = 39628, // Helper->self, 6.0s cast, range 45 width 10 rect + PoisonSting = 37268, // Boss->self, 4.7+1.6s cast, single-target, visual (dropped voidzone) + PoisonStingRest = 37269, // Boss->self, no cast, single-target, visual (second+) + PoisonStingAOE = 37267, // Helper->player, 6.0s cast, range 6 circle, drops voidzone + BeeSting = 37288, // Boss->self, 4.0+1.0s cast, single-target, visual (stack) + BeeStingAOE = 37289, // Helper->players, 5.0s cast, range 6 circle 4-man stack + + HoneyBLiveBeat2 = 39973, // Boss->self, 2.0+6.3s cast, single-target, visual (mechanic start + raidwide) + HoneyBLiveBeat2AOE = 39975, // Helper->self, no cast, range 60 circle, raidwide + SpreadLove = 39688, // Boss->self, 5.0s cast, single-target, visual (prepare spreads) + SpreadLoveAOE = 39694, // Helper->player, no cast, range 6 circle + DropOfLove = 39689, // Boss->self, 5.0s cast, single-target, visual (prepare pairs) + DropOfLoveAOE = 39695, // Helper->players, no cast, range 6 circle + HeartStruck = 37287, // Helper->location, 3.0s cast, range 6 circle puddle + Heartsore = 37281, // Helper->player, no cast, range 6 circle spread + HoneyBeelineBeat = 39692, // Boss->self, 5.5+0.7s cast, single-target + HoneyBeelineBeatAOE = 39696, // Helper->self, 6.2s cast, range 60 width 14 rect + TemptingTwistBeat = 39693, // Boss->self, 5.5+0.7s cast, single-target + TemptingTwistBeatAOE = 39697, // Helper->self, 6.2s cast, range ?-30 donut + SweetheartAppear = 39690, // Sweetheart->location, no cast, single-target + SweetheartSplinter = 39691, // Sweetheart->self, 3.3s cast, range 8 circle + + HoneyBLiveBeat3 = 39974, // Boss->self, 2.0+6.3s cast, single-target, visual (mechanic start + raidwide) + HoneyBLiveBeat3AOE = 39976, // Helper->self, no cast, range 60 circle, raidwide + HoneyBLiveBeat3BigBurst = 37302, // Helper->player, no cast, range 14 circle spread + + RottenHeart = 37290, // Boss->self, 1.0+4.0s cast, single-target, visual (mechanic start) + RottenHeartAOE = 37330, // Helper->self, no cast, range 60 circle, raidwide + RottenHeartBigBurst = 37291, // Helper->self, no cast, range 60 circle, raidwide on touch + + SheerHeartAttack = 37303, // Boss->self, 10.0s cast, range 60 circle, visual (enrage) + SheerHeartAttackHeal = 38659, // Helper->Boss, no cast, single-target, heal for gaining 4 hearts due to enrage + SheerHeartAttackHealHoneyBFinale = 37304, // Boss->self, no cast, range 60 circle, enrage +} + +public enum SID : uint +{ + Hearts0 = 3922, // none->player, extra=0x2DA + Hearts1 = 3923, // none->player, extra=0x2DB + Hearts2 = 3924, // none->player, extra=0x2DC + Hearts3 = 3925, // none->player, extra=0x2DD + Hearts4 = 3926, // none->player, extra=0x2DE + PoisonNPop = 3934, // none->player, extra=0x0 + BeelovedVenomA = 3932, // none->player, extra=0x0 + BeelovedVenomB = 3933, // none->player, extra=0x0 +} + +public enum IconID : uint +{ + StingingSlash = 471, // player + KillerSting = 259, // player + Heartsick = 517, // player + Heartsore = 515, // player + PoisonSting = 234, // player + BeeSting = 161, // player +} + +public enum TetherID : uint +{ + RottenHeart = 224, // player->player +} diff --git a/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovelyStates.cs b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovelyStates.cs new file mode 100644 index 000000000..7e73a46ac --- /dev/null +++ b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RM02SHoneyBLovelyStates.cs @@ -0,0 +1,255 @@ +namespace BossMod.Dawntrail.Savage.RM02SHoneyBLovely; + +class RM02SHoneyBLovelyStates : StateMachineBuilder +{ + public RM02SHoneyBLovelyStates(BossModule module) : base(module) + { + DeathPhase(0, SinglePhase); + } + + private void SinglePhase(uint id) + { + CallMeHoney(id, 5.2f); + DropSplashOfVenomHoneyBeelineTemptingTwist(id + 0x10000, 2.1f, false); + DropSplashOfVenomHoneyBeelineTemptingTwist(id + 0x20000, 1.0f, true); + StingingSlashKillerSting(id + 0x30000, 3.0f); + Beat1(id + 0x40000, 10.5f); + StingingSlashKillerSting(id + 0x50000, 10.2f); + AlarmPheromones1(id + 0x60000, 4.2f); + Beat2(id + 0x70000, 7.8f); + AlarmPheromones2(id + 0x80000, 13.5f); + Beat3(id + 0x90000, 11.3f); + StingingSlashKillerSting(id + 0xA0000, 9.2f); + RottenHeart(id + 0xB0000, 11.4f); + Cast(id + 0xC0000, AID.SheerHeartAttack, 12.2f, 10, "Enrage"); + } + + private void CallMeHoney(uint id, float delay, string name = "Raidwide") + { + Cast(id, AID.CallMeHoney, delay, 5, name) + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void HoneyBeelineTemptingTwistResolve(uint id, float delay) + { + CastMulti(id, [AID.HoneyBeeline, AID.TemptingTwist], delay, 5.5f) + .ActivateOnEnter() + .ActivateOnEnter(); + Condition(id + 2, 0.7f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "In/out") + .DeactivateOnExit() + .DeactivateOnExit(); + ComponentCondition(id + 3, 4.5f, comp => !comp.Active, "Pairs/spread") + .ActivateOnEnter() + .DeactivateOnExit() + .DeactivateOnExit(); // resolves at the same time + } + + private void HoneyBeelineTemptingTwistBeatResolve(uint id, float delay) + { + CastMulti(id, [AID.HoneyBeelineBeat, AID.TemptingTwistBeat], delay, 5.5f) + .ActivateOnEnter() + .ActivateOnEnter(); + Condition(id + 2, 0.7f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "In/out") + .DeactivateOnExit() + .DeactivateOnExit(); + ComponentCondition(id + 3, 4.5f, comp => !comp.Active, "Pairs/spread") + .ActivateOnEnter() + .DeactivateOnExit() + .DeactivateOnExit(); // resolves at the same time + } + + private void DropSplashOfVenomHoneyBeelineTemptingTwist(uint id, float delay, bool shortInOutDelay) + { + CastMulti(id, [AID.SplashOfVenom, AID.DropOfVenom], delay, 5) + .ActivateOnEnter(); + HoneyBeelineTemptingTwistResolve(id + 0x10, shortInOutDelay ? 2.1f : 5.5f); + } + + private State StingingSlashKillerSting(uint id, float delay) + { + CastStartMulti(id, [AID.StingingSlash, AID.KillerSting], delay) // icons appear right before cast start + .ActivateOnEnter() + .ActivateOnEnter(); + CastEnd(id + 1, 4); + return Condition(id + 2, 1, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "Tankbuster") + .DeactivateOnExit() + .DeactivateOnExit() // note that killer sting happens ~0.3s later, not a huge deal + .SetHint(StateMachine.StateHint.Tankbuster); + } + + private void CenterOuterStageComboResolve(uint id, float delay, bool activateComponent) + { + CastEnd(id, delay) + .ActivateOnEnter(activateComponent); + ComponentCondition(id + 1, 1.2f, comp => comp.NumCasts > 0, "In/out"); + ComponentCondition(id + 2, 3.0f, comp => comp.NumCasts > 5, "Cross"); + ComponentCondition(id + 3, 3.3f, comp => comp.NumCasts > 6, "Out/in") + .DeactivateOnExit(); + } + + private void CenterOuterStageCombo(uint id, float delay) + { + CastStartMulti(id, [AID.CenterstageCombo, AID.OuterstageCombo], delay); + CenterOuterStageComboResolve(id + 1, 5, true); + } + + private void Beat1(uint id, float delay) + { + Cast(id, AID.HoneyBLiveBeat1, delay, 2); + ComponentCondition(id + 2, 6.3f, comp => comp.NumCasts > 0, "Beat 1 raidwide") + .ActivateOnEnter() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + + CenterOuterStageCombo(id + 0x100, 2.9f); + + Cast(id + 0x200, AID.LoveMeTender, 4.2f, 4); + ComponentCondition(id + 0x210, 2.2f, comp => comp.Towers.Count > 0) + .ActivateOnEnter() + .ActivateOnEnter(); + ComponentCondition(id + 0x220, 8, comp => comp.NumCasts > 0, "First tower"); // one or two towers every 4s, 11 towers total + ComponentCondition(id + 0x230, 20, comp => comp.NumCasts >= 11, "Last tower") + .DeactivateOnExit(); + + Cast(id + 0x300, AID.Loveseeker, 0.1f, 3) + .ActivateOnEnter(); + ComponentCondition(id + 0x302, 1, comp => comp.NumCasts > 0, "Out") + .ActivateOnEnter() // adds activate right after resolve + .DeactivateOnExit(); + + Cast(id + 0x400, AID.LoveMeTender, 9.2f, 4); + ComponentCondition(id + 0x410, 1.1f, comp => comp.Stacks.Count > 0) + .ActivateOnEnter(); + ComponentCondition(id + 0x420, 7.1f, comp => comp.NumFinishedStacks > 0, "Stack") + .DeactivateOnExit() + .DeactivateOnExit(); + + CenterOuterStageCombo(id + 0x500, 5.2f); + Cast(id + 0x600, AID.HoneyBFinale, 6.2f, 5, "Beat 1 end raidwide") + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void AlarmPheromones1(uint id, float delay) + { + Cast(id, AID.AlarmPheromones, delay, 3); + ComponentCondition(id + 0x10, 3.6f, comp => comp.Casters.Count > 0) // every 1.2s, 16 total + .ActivateOnEnter(); + ComponentCondition(id + 0x20, 7, comp => comp.NumCasts > 0, "Baited charges start"); + ComponentCondition(id + 0x30, 18, comp => comp.NumCasts >= 16, "Baited charges end") + .DeactivateOnExit(); + } + + private void Beat2(uint id, float delay) + { + Cast(id, AID.HoneyBLiveBeat2, delay, 2); + ComponentCondition(id + 2, 6.3f, comp => comp.NumCasts > 0, "Beat 2 raidwide") + .ActivateOnEnter() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + CastMulti(id + 0x10, [AID.SpreadLove, AID.DropOfLove], 2.8f, 5) + .ActivateOnEnter(); + + Cast(id + 0x100, AID.LoveMeTender, 6.2f, 4); + ComponentCondition(id + 0x110, 1.1f, comp => comp.Stacks.Count > 0, "Puddles bait start") + .ActivateOnEnter() + .ActivateOnEnter(); + ComponentCondition(id + 0x120, 6.1f, comp => comp.Towers.Count > 0) + .ActivateOnEnter() // first puddles appear together with stack icons + .ActivateOnEnter() // spread markers appear just before towers + .ActivateOnEnter(); + ComponentCondition(id + 0x130, 1, comp => comp.NumFinishedStacks > 0, "Stacks") + .DeactivateOnExit(); + ComponentCondition(id + 0x140, 6, comp => comp.NumFinishedSpreads > 0, "Spreads") + .DeactivateOnExit() // last puddle resolves ~2s before spreads + .DeactivateOnExit(); + ComponentCondition(id + 0x150, 1, comp => comp.NumCasts > 0, "Towers") + .DeactivateOnExit() + .DeactivateOnExit(); + + HoneyBeelineTemptingTwistBeatResolve(id + 0x200, 3.7f); + + Cast(id + 0x300, AID.HoneyBFinale, 4, 5, "Beat 2 end raidwide") + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void AlarmPheromones2(uint id, float delay) + { + CastMulti(id, [AID.SplashOfVenom, AID.DropOfVenom], delay, 5) + .ActivateOnEnter(); + + Cast(id + 0x100, AID.AlarmPheromones, 2.1f, 3); + Cast(id + 0x110, AID.PoisonSting, 3.2f, 4.7f) + .ActivateOnEnter() + .ActivateOnEnter() // voidzones drop ~0.8s after puddle cast + .ActivateOnEnter() // first charges begin ~0.4s into cast + .ActivateOnEnter(); + ComponentCondition(id + 0x120, 1.3f, comp => comp.NumCasts > 0, "Puddle drop 1"); + // +0.4s: charges 1 + ComponentCondition(id + 0x130, 5.0f, comp => comp.NumCasts > 2, "Puddle drop 2"); + // +0.4s: charges 2 + ComponentCondition(id + 0x140, 5.0f, comp => comp.NumCasts > 4, "Puddle drop 3"); + // +0.4s: charges 3 + ComponentCondition(id + 0x150, 5.0f, comp => comp.NumCasts > 6, "Puddle drop 4") + .DeactivateOnExit(); + // +0.4s: charges 4 + + Cast(id + 0x200, AID.BeeSting, 5, 4) + .ActivateOnEnter(); + ComponentCondition(id + 0x210, 1, comp => comp.NumFinishedStacks > 0, "Role stacks") + .DeactivateOnExit(); + // +0.2s: last charge + + StingingSlashKillerSting(id + 0x300, 3.1f) + .DeactivateOnExit() + .DeactivateOnExit(); + + HoneyBeelineTemptingTwistResolve(id + 0x400, 6.8f); + } + + private void Beat3(uint id, float delay) + { + Cast(id, AID.HoneyBLiveBeat3, delay, 2); + ComponentCondition(id + 2, 6.3f, comp => comp.NumCasts > 0, "Beat 3 raidwide") + .ActivateOnEnter() + .ActivateOnEnter() // statuses appear right before raidwide + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + CastMulti(id + 0x10, [AID.SpreadLove, AID.DropOfLove], 1.9f, 5) + .ActivateOnEnter(); + + CenterOuterStageCombo(id + 0x100, 2.1f); + ComponentCondition(id + 0x110, 4.6f, comp => comp.NumCasts > 0, "Defamations 1") + .ActivateOnEnter(); + CastStartMulti(id + 0x120, [AID.CenterstageCombo, AID.OuterstageCombo], 2.6f); + ComponentCondition(id + 0x121, 0.8f, comp => comp.NumCasts > 0, "Towers 1") + .ActivateOnEnter() + .DeactivateOnExit(); + CenterOuterStageComboResolve(id + 0x122, 4.2f, false); + ComponentCondition(id + 0x130, 5.0f, comp => comp.NumCasts > 4, "Defamations 2") + .ActivateOnEnter(); + ComponentCondition(id + 0x140, 3.0f, comp => comp.NumCasts > 0, "Towers 2") + .DeactivateOnExit() + .DeactivateOnExit(); + + HoneyBeelineTemptingTwistBeatResolve(id + 0x200, 4.2f); + + Cast(id + 0x300, AID.HoneyBFinale, 3, 5, "Beat 3 end raidwide") + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void RottenHeart(uint id, float delay) + { + Cast(id, AID.RottenHeart, delay, 1); + ComponentCondition(id + 2, 3.6f, comp => comp.NumCasts > 0, "Nisi start raidwide") + .ActivateOnEnter() + .ActivateOnEnter() // statuses appear right before raidwide + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + + CallMeHoney(id + 0x10, 11.5f, "Raidwide 1"); + CallMeHoney(id + 0x20, 12.2f, "Raidwide 2"); + CallMeHoney(id + 0x30, 12.2f, "Raidwide 3"); + CallMeHoney(id + 0x40, 12.2f, "Raidwide 4"); + } +} diff --git a/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RottenHeart.cs b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RottenHeart.cs new file mode 100644 index 000000000..56db957f6 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/RottenHeart.cs @@ -0,0 +1,65 @@ +namespace BossMod.Dawntrail.Savage.RM02SHoneyBLovely; + +class RottenHeart(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.RottenHeartAOE)); + +class RottenHeartBigBurst(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.RottenHeartBigBurst)) +{ + private int _numRaidwides; + private readonly int[] _order = new int[PartyState.MaxPartySize]; + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (_order[slot] > NumCasts) + hints.Add($"Order: {_order[slot]}", ResolveImminent(slot)); + } + + public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot, Actor player, ref uint customColor) + => _order[pcSlot] != 0 && _order[pcSlot] == _order[playerSlot] ? PlayerPriority.Danger : PlayerPriority.Irrelevant; + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + var partner = _order[pcSlot] > NumCasts ? FindPartner(pcSlot) : null; + if (partner != null) + Arena.AddLine(pc.Position, partner.Position, ResolveImminent(pcSlot) ? ArenaColor.Danger : ArenaColor.Safe); + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID is SID.BeelovedVenomA or SID.BeelovedVenomB && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + { + _order[slot] = (status.ExpireAt - WorldState.CurrentTime).TotalSeconds switch + { + < 15 => 1, + < 30 => 2, + < 45 => 3, + _ => 4 + }; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.RottenHeartBigBurst: + ++NumCasts; + break; + case AID.CallMeHoney: + ++_numRaidwides; + break; + } + } + + private Actor? FindPartner(int slot) + { + var order = _order[slot]; + if (order == 0) + return null; + for (int i = 0; i < _order.Length; ++i) + if (i != slot && _order[i] == order) + return Raid[i]; + return null; + } + + private bool ResolveImminent(int slot) => _order[slot] == _numRaidwides + 1; +} diff --git a/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/StageCombo.cs b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/StageCombo.cs new file mode 100644 index 000000000..11c1346c4 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/StageCombo.cs @@ -0,0 +1,63 @@ +namespace BossMod.Dawntrail.Savage.RM02SHoneyBLovely; + +class StageCombo(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = []; + + private static readonly AOEShapeCircle _shapeOut = new(7); + private static readonly AOEShapeDonut _shapeIn = new(7, 30); + private static readonly AOEShapeCross _shapeCross = new(30, 7); + private static readonly AOEShapeCone _shapeCone = new(30, 22.5f.Degrees()); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var firstActivation = _aoes.Count > 0 ? _aoes[0].Activation : default; + return _aoes.TakeWhile(aoe => aoe.Activation == firstActivation); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + var (first, last, firstRot) = (AID)spell.Action.ID switch + { + AID.CenterstageCombo => (_shapeIn, _shapeOut, 0.Degrees()), + AID.OuterstageCombo => (_shapeOut, _shapeIn, 45.Degrees()), + _ => ((AOEShape?)null, (AOEShape?)null, 0.Degrees()) + }; + if (first != null && last != null) + { + var firstActivation = Module.CastFinishAt(spell, 1.2f); + var lastActivation = Module.CastFinishAt(spell, 7.5f); + _aoes.Add(new(first, Module.Center, 180.Degrees(), firstActivation)); + _aoes.Add(new(_shapeCone, Module.Center, firstRot, firstActivation)); + _aoes.Add(new(_shapeCone, Module.Center, firstRot + 90.Degrees(), firstActivation)); + _aoes.Add(new(_shapeCone, Module.Center, firstRot + 180.Degrees(), firstActivation)); + _aoes.Add(new(_shapeCone, Module.Center, firstRot - 90.Degrees(), firstActivation)); + _aoes.Add(new(_shapeCross, Module.Center, 180.Degrees(), Module.CastFinishAt(spell, 4.2f))); + _aoes.Add(new(last, Module.Center, 180.Degrees(), lastActivation)); + _aoes.Add(new(_shapeCone, Module.Center, firstRot + 45.Degrees(), lastActivation)); + _aoes.Add(new(_shapeCone, Module.Center, firstRot + 135.Degrees(), lastActivation)); + _aoes.Add(new(_shapeCone, Module.Center, firstRot - 45.Degrees(), lastActivation)); + _aoes.Add(new(_shapeCone, Module.Center, firstRot - 135.Degrees(), lastActivation)); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + AOEShape? shape = (AID)spell.Action.ID switch + { + AID.LacerationOut => _shapeOut, + AID.LacerationCross => _shapeCross, + AID.LacerationCone => _shapeCone, + AID.LacerationIn => _shapeIn, + _ => null + }; + if (shape != null) + { + ++NumCasts; + var firstActivation = _aoes.Count > 0 ? _aoes[0].Activation : default; + var count = _aoes.RemoveAll(aoe => aoe.Shape == shape && aoe.Rotation.AlmostEqual(caster.Rotation, 0.1f) && aoe.Activation == firstActivation); + if (count != 1) + ReportError($"Unexpected aoe: {spell.Action} @ {caster.Rotation}deg"); + } + } +} diff --git a/BossMod/Network/PacketDecoder.cs b/BossMod/Network/PacketDecoder.cs index 35f289d94..d8e7e1de6 100644 --- a/BossMod/Network/PacketDecoder.cs +++ b/BossMod/Network/PacketDecoder.cs @@ -188,6 +188,8 @@ private TextNode DecodeActorControl(ActorControlCategory category, uint p1, uint ActorControlCategory.SetTarget => $"{DecodeActor(targetID)}", ActorControlCategory.SetAnimationState => $"#{p1} = {p2}", ActorControlCategory.SetModelState => $"{p1}", + ActorControlCategory.SetName => $"'{Service.LuminaRow(p1)?.Singular}' ({p1})", + ActorControlCategory.SetCompanionOwnerId => $"{DecodeActor(p1)}", ActorControlCategory.ForcedMovement => $"dest={Utils.Vec3String(IntToFloatCoords((ushort)p1, (ushort)p2, (ushort)p3))}, rot={IntToFloatAngle((ushort)p4)}deg over {p5 * 0.0001:f4}s, type={p6}", ActorControlCategory.PlayActionTimeline => $"{p1:X4}", ActorControlCategory.EObjSetState => $"{p1:X4}, housing={(p3 != 0 ? p4 : null)}", diff --git a/BossMod/Network/ServerIPC.cs b/BossMod/Network/ServerIPC.cs index e056c13f6..64e3e9631 100644 --- a/BossMod/Network/ServerIPC.cs +++ b/BossMod/Network/ServerIPC.cs @@ -421,6 +421,7 @@ public enum ActorControlCategory : ushort TreasureScreenMsg = 87, // from dissector SetOwnerId = 89, // from dissector ItemRepairMsg = 92, // from dissector + SetName = 98, BluActionLearn = 99, // from dissector DirectorInit = 100, // from dissector DirectorClear = 101, // from dissector @@ -497,6 +498,7 @@ public enum ActorControlCategory : ushort EObjSetState = 409, // from dissector Unk6 = 412, // from dissector EObjAnimation = 413, // from dissector + SetCompanionOwnerId = 417, SetTitle = 500, // from dissector SetTargetSign = 502, SetStatusIcon = 504, // from dissector