diff --git a/BossMod/AI/AIBehaviour.cs b/BossMod/AI/AIBehaviour.cs index 863a224b1a..ba67b2f338 100644 --- a/BossMod/AI/AIBehaviour.cs +++ b/BossMod/AI/AIBehaviour.cs @@ -95,7 +95,7 @@ private Targeting SelectPrimaryTarget(Actor player, Actor master) // now give class module a chance to improve targeting // typically it would switch targets for multidotting, or to hit more targets with AOE // in case of ties, it should prefer to return original target - this would prevent useless switches - var targeting = new Targeting(target!, autorot.Hints.RecommendedRangeToTarget - 0.1f); + var targeting = new Targeting(target!, player.Role is Role.Melee or Role.Tank ? 2.9f : 24.5f); var pos = autorot.Hints.RecommendedPositional; if (pos.Target != null && targeting.Target.Actor == pos.Target) diff --git a/BossMod/AI/AIRotationModule.cs b/BossMod/AI/AIRotationModule.cs index ccf460806a..551541137c 100644 --- a/BossMod/AI/AIRotationModule.cs +++ b/BossMod/AI/AIRotationModule.cs @@ -4,7 +4,7 @@ namespace BossMod.AI; public abstract class AIRotationModule(RotationModuleManager manager, Actor player) : RotationModule(manager, player) { - protected float Deadline(DateTime deadline) => Math.Max(0, (float)(deadline - Manager.WorldState.CurrentTime).TotalSeconds); + protected float Deadline(DateTime deadline) => Math.Max(0, (float)(deadline - World.CurrentTime).TotalSeconds); protected float Speed() => Player.FindStatus(50) != null ? 7.8f : 6; protected bool InMeleeRange(Actor target) @@ -47,7 +47,7 @@ protected WPos MoveTarget(Actor target, WPos desired, float nextAction, float ta var playerDotTargetMove = targetMoveDir.Dot(ideal - Player.Position); if (playerDotTargetMove < 0) ideal -= playerDotTargetMove * targetMoveDir; // don't move towards boss, though - var targetRemaining = (ideal - target.Position).Length() - target.HitboxRadius - targetMeleeRange - (target.Position - target.PrevPosition).Length() / Manager.WorldState.Frame.Duration * nextAction - Speed() * nextAction; + var targetRemaining = (ideal - target.Position).Length() - target.HitboxRadius - targetMeleeRange - (target.Position - target.PrevPosition).Length() / World.Frame.Duration * nextAction - Speed() * nextAction; if (targetRemaining > 0) ideal += targetRemaining * (target.Position - ideal).Normalized(); return ideal; diff --git a/BossMod/ActionTweaks/ActionTweaksConfig.cs b/BossMod/ActionTweaks/ActionTweaksConfig.cs index 8c17437221..fe89f52e43 100644 --- a/BossMod/ActionTweaks/ActionTweaksConfig.cs +++ b/BossMod/ActionTweaks/ActionTweaksConfig.cs @@ -53,6 +53,9 @@ public enum ModifierKey [PropertyDisplay("Use custom queueing for manually pressed actions", tooltip: "This setting allows better integration with autorotations and will prevent you from triple-weaving or drifting GCDs if you press a healing ability while autorotation is going on")] public bool UseManualQueue = false; + [PropertyDisplay("Automatically manage auto attacks", tooltip: "This setting prevents starting autos early during countdown, starts them automatically at pull, when switching targets and when using any actions that don't explicitly cancel autos.")] + public bool AutoAutos = false; + [PropertyDisplay("Automatically dismount to execute actions")] public bool AutoDismount = true; diff --git a/BossMod/ActionTweaks/AutoAutosTweak.cs b/BossMod/ActionTweaks/AutoAutosTweak.cs new file mode 100644 index 0000000000..472c82d45a --- /dev/null +++ b/BossMod/ActionTweaks/AutoAutosTweak.cs @@ -0,0 +1,38 @@ +namespace BossMod; + +// This tweak controls auto attacks to prevent early pulls and to enable them asap when pulling, changing targets or starting casts. +public sealed class AutoAutosTweak(WorldState ws, AIHints hints) +{ + private readonly ActionTweaksConfig _config = Service.Config.Get(); + private bool _lastActionDisabledAutos; + + public const float PrePullThreshold = 0.5f; // effect result delay for autos + + public bool Enabled => _config.AutoAutos; + + public bool ShouldPreventAutoActivation(uint spellId) + { + var actionData = Service.LuminaRow(spellId); + _lastActionDisabledAutos = actionData?.Unknown50 is 3 or 6 or 7; + return Enabled && ws.Client.CountdownRemaining > PrePullThreshold && !(ws.Party.Player()?.InCombat ?? false); + } + + public bool GetDesiredState(bool currentState) + { + if (!Enabled || _lastActionDisabledAutos) + return currentState; + + var player = ws.Party.Player(); + if (player == null || player.Statuses.Any(s => s.ID is 418 or 2648)) // transcendent + return currentState; + + var target = ws.Actors.Find(player.TargetID); + if (target == null || target.IsAlly) + return currentState; + + if (_config.PyreticThreshold > 0 && hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && hints.ImminentSpecialMode.activation < ws.FutureTime(_config.PyreticThreshold)) + return false; // pyretic => disable autos + + return player.InCombat || ws.Client.CountdownRemaining <= PrePullThreshold; // no reason not to enable autos! + } +} diff --git a/BossMod/ActionTweaks/OutOfCombatActionsTweak.cs b/BossMod/ActionTweaks/OutOfCombatActionsTweak.cs index a5cb620f28..9366c04e97 100644 --- a/BossMod/ActionTweaks/OutOfCombatActionsTweak.cs +++ b/BossMod/ActionTweaks/OutOfCombatActionsTweak.cs @@ -1,6 +1,6 @@ namespace BossMod; -[ConfigDisplay(Name = "Automatic out-of-combat utility actions", Parent = typeof(ActionTweaksConfig))] +[ConfigDisplay(Name = "Automatic out-of-combat utility actions", Parent = typeof(ActionTweaksConfig), Since = "7.2.0.146")] class OutOfCombatActionsConfig : ConfigNode { [PropertyDisplay("Enable the feature")] diff --git a/BossMod/ActionTweaks/SmartRotationTweak.cs b/BossMod/ActionTweaks/SmartRotationTweak.cs index b01373d409..ee9a52b89a 100644 --- a/BossMod/ActionTweaks/SmartRotationTweak.cs +++ b/BossMod/ActionTweaks/SmartRotationTweak.cs @@ -1,6 +1,6 @@ namespace BossMod; -[ConfigDisplay(Name = "Smart character orientation", Parent = typeof(ActionTweaksConfig))] +[ConfigDisplay(Name = "Smart character orientation", Parent = typeof(ActionTweaksConfig), Since = "7.2.0.146")] class SmartRotationConfig : ConfigNode { [PropertyDisplay("Enable the feature", tooltip: "Replace in-game 'auto face target' option with a smarter alternative.\nWhen using an action, changes direction only if target is not in frontal cone.\nDuring cast, keep character facing the target.")] diff --git a/BossMod/Autorotation/RotationModule.cs b/BossMod/Autorotation/RotationModule.cs index 4f90d44a68..91b3bed4a7 100644 --- a/BossMod/Autorotation/RotationModule.cs +++ b/BossMod/Autorotation/RotationModule.cs @@ -80,7 +80,6 @@ public ConfigRef AddAssociatedActions(params AID[] aids) where AID : // base class for rotation modules // each rotation module should contain a `public static RotationModuleDefinition Definition()` function -// TODO: i don't think it should know about manager, rework this... public abstract class RotationModule(RotationModuleManager manager, Actor player) { public readonly RotationModuleManager Manager = manager; diff --git a/BossMod/Autorotation/Standard/StandardWAR.cs b/BossMod/Autorotation/Standard/StandardWAR.cs index 15e4578a3b..59d776f79b 100644 --- a/BossMod/Autorotation/Standard/StandardWAR.cs +++ b/BossMod/Autorotation/Standard/StandardWAR.cs @@ -334,7 +334,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa // special case for use as gapcloser - it has to be very high priority var (prio, basePrio) = stratOnsOpt == OnslaughtStrategy.GapClose ? (OGCDPriority.GapcloseOnslaught, ActionQueue.Priority.High) : LostBloodRageStacks is > 0 and < 4 ? (OGCDPriority.LostBanner, ActionQueue.Priority.Medium) - : (OGCDPriority.Onslaught, OnslaughtCD < GCDLength ? ActionQueue.Priority.VeryLow : ActionQueue.Priority.Low); + : (OGCDPriority.Onslaught, OnslaughtCapIn < GCDLength ? ActionQueue.Priority.Low : ActionQueue.Priority.VeryLow); QueueOGCD(WAR.AID.Onslaught, target, stratOns.Value.PriorityOverride, prio, basePrio); } } @@ -350,6 +350,18 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa Hints.ActionsToExecute.Push(BozjaActionID.GetNormal(BozjaHolsterID.LostFontOfPower), Player, ActionQueue.Priority.Low + (int)OGCDPriority.LostFont); if (ShouldUseLostBuff(LostBannerCD, 90)) Hints.ActionsToExecute.Push(BozjaActionID.GetNormal(BozjaHolsterID.BannerHonoredSacrifice), Player, ActionQueue.Priority.Low + (int)OGCDPriority.LostBanner); + + // ai hints for positioning + var goalST = primaryTarget != null ? Hints.GoalSingleTarget(primaryTarget, 3) : null; + var goalAOE = Hints.GoalAOECircle(3); + var goal = aoeStrategy switch + { + AOEStrategy.SingleTarget => goalST, + AOEStrategy.ForceAOE => goalAOE, + _ => goalST != null ? Hints.GoalCombined(goalST, goalAOE, 3) : goalAOE + }; + if (goal != null) + Hints.GoalZones.Add(goal); } private void QueueGCD(WAR.AID aid, Actor? target, GCDPriority prio) @@ -712,7 +724,9 @@ private GCDPriority FellCleavePriorityBerserk() } } var nextAction = wantAOEAction ? NextComboAOE(comboStepsRemaining == 0) : NextComboSingleTarget(wantSERoute, comboStepsRemaining == 0); - var riskOvercappingGauge = Gauge + GaugeGainedFromAction(nextAction) > 100; + + var needInfuriateSoon = Unlocked(WAR.AID.Infuriate) && !CanFitGCD(InfuriateCD - InfuriateCDReduction - InfuriateCDLeeway, 1); + var riskOvercappingGauge = Gauge + GaugeGainedFromAction(nextAction) > (needInfuriateSoon ? 50 : 100); // first deal with forced combo; for ST extension, we generally want to minimize overcap by using combo finisher as late as possible // TODO: reconsider what to do if we can't fit in combo - do we still want to do partial combo? especially if it would cause gauge overcap diff --git a/BossMod/Autorotation/Utility/ClassDRKUtility.cs b/BossMod/Autorotation/Utility/ClassDRKUtility.cs index b779414c94..4f21fb6ce3 100644 --- a/BossMod/Autorotation/Utility/ClassDRKUtility.cs +++ b/BossMod/Autorotation/Utility/ClassDRKUtility.cs @@ -74,7 +74,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa var dashStrategy = strategy.Option(Track.Shadowstride).As(); if (ShouldUseDash(dashStrategy, primaryTarget)) - Hints.ActionsToExecute.Push(ActionID.MakeSpell(DRK.AID.Shadowstride), Player, obl.Priority()); + Hints.ActionsToExecute.Push(ActionID.MakeSpell(DRK.AID.Shadowstride), primaryTarget, obl.Priority()); } private bool ShouldUseDash(DashStrategy strategy, Actor? primaryTarget) => strategy switch { diff --git a/BossMod/Autorotation/Utility/ClassGNBUtility.cs b/BossMod/Autorotation/Utility/ClassGNBUtility.cs index 65fe382039..bfcb691369 100644 --- a/BossMod/Autorotation/Utility/ClassGNBUtility.cs +++ b/BossMod/Autorotation/Utility/ClassGNBUtility.cs @@ -73,7 +73,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa var dashStrategy = strategy.Option(Track.Trajectory).As(); if (ShouldUseDash(dashStrategy, primaryTarget)) - Hints.ActionsToExecute.Push(ActionID.MakeSpell(GNB.AID.Trajectory), Player, hoc.Priority()); + Hints.ActionsToExecute.Push(ActionID.MakeSpell(GNB.AID.Trajectory), primaryTarget, hoc.Priority()); } private bool ShouldUseDash(DashStrategy strategy, Actor? primaryTarget) => strategy switch { diff --git a/BossMod/Autorotation/Utility/ClassSCHUtility.cs b/BossMod/Autorotation/Utility/ClassSCHUtility.cs index f455180ffc..3b53bc16ce 100644 --- a/BossMod/Autorotation/Utility/ClassSCHUtility.cs +++ b/BossMod/Autorotation/Utility/ClassSCHUtility.cs @@ -2,11 +2,7 @@ public sealed class ClassSCHUtility(RotationModuleManager manager, Actor player) : RoleHealerUtility(manager, player) { - public enum Track - { - WhisperingDawn = SharedTrack.Count, Adloquium, Succor, FeyIllumination, Lustrate, SacredSoil, Indomitability, DeploymentTactics, - EmergencyTactics, Dissipation, Excogitation, Aetherpact, Recitation, FeyBlessing, Consolation, Protraction, Expedient, Seraphism, Resurrection, PetActions - } + public enum Track { WhisperingDawn = SharedTrack.Count, Adloquium, Succor, FeyIllumination, Lustrate, SacredSoil, Indomitability, DeploymentTactics, EmergencyTactics, Dissipation, Excogitation, Aetherpact, Recitation, FeyBlessing, Consolation, Protraction, Expedient, Seraphism, Resurrection, Summons } public enum SuccorOption { None, Succor, Concitation } public enum DeployOption { None, Use, UseEx } public enum AetherpactOption { None, Use, End } @@ -64,7 +60,7 @@ public static RotationModuleDefinition Definition() DefineSimpleConfig(res, Track.Resurrection, "Resurrection", "Raise", 10, SCH.AID.Resurrection); // Pet Summons - res.Define(Track.PetActions).As("Pet", "", 180) + res.Define(Track.Summons).As("Pet", "", 180) .AddOption(PetOption.None, "None", "Do not use automatically") .AddOption(PetOption.Eos, "Eos", "Summon Eos", 2, 0, ActionTargets.Self, 4) .AddOption(PetOption.Seraph, "Seraph", "Summon Seraph", 120, 22, ActionTargets.Self, 80) @@ -120,14 +116,15 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa if (recit.As() != RecitationOption.None) Hints.ActionsToExecute.Push(ActionID.MakeSpell(SCH.AID.Recitation), Player, recit.Priority(), recit.Value.ExpireIn); - var pet = strategy.Option(Track.PetActions); - var petAction = pet.As() switch + var pet = strategy.Option(Track.Summons); + var petSummons = pet.As() switch { PetOption.Eos => SCH.AID.SummonEos, PetOption.Seraph => SCH.AID.SummonSeraph, _ => default }; - if (petAction != default) - Hints.ActionsToExecute.Push(ActionID.MakeSpell(petAction), Player, pet.Priority(), pet.Value.ExpireIn); + if (petSummons != default) + Hints.ActionsToExecute.Push(ActionID.MakeSpell(petSummons), Player, pet.Priority(), pet.Value.ExpireIn); + } } diff --git a/BossMod/Autorotation/xan/Casters/RDM.cs b/BossMod/Autorotation/xan/Casters/RDM.cs index b19bb7431f..a4b54e0226 100644 --- a/BossMod/Autorotation/xan/Casters/RDM.cs +++ b/BossMod/Autorotation/xan/Casters/RDM.cs @@ -107,8 +107,9 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) (BestLineTarget, NumLineTargets) = SelectTarget(strategy, primaryTarget, 25, Is25yRectTarget); (BestConeTarget, NumConeTargets) = SelectTarget(strategy, primaryTarget, 8, (primary, other) => Hints.TargetInAOECone(other, Player.Position, 8, Player.DirectionTo(primary), 60.Degrees())); - if (Swordplay > 0 || LowestMana >= 50 || InCombo) - Hints.RecommendedRangeToTarget = 3; + // TODO: fixme xan! + //if (Swordplay > 0 || LowestMana >= 50 || InCombo) + // Hints.RecommendedRangeToTarget = 3; if (CountdownRemaining > 0) { @@ -118,8 +119,9 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) return; } - if (Swordplay > 0 || LowestMana >= 50 || InCombo) - Hints.RecommendedRangeToTarget = 3f; + // TODO: fixme xan! + //if (Swordplay > 0 || LowestMana >= 50 || InCombo) + // Hints.RecommendedRangeToTarget = 3f; OGCD(strategy, primaryTarget); diff --git a/BossMod/Autorotation/xan/Healers/SCH.cs b/BossMod/Autorotation/xan/Healers/SCH.cs index 99554e4af2..7da877bdfb 100644 --- a/BossMod/Autorotation/xan/Healers/SCH.cs +++ b/BossMod/Autorotation/xan/Healers/SCH.cs @@ -71,8 +71,9 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (NumAOETargets >= needAOETargets) { - if (needAOETargets == 1) - Hints.RecommendedRangeToTarget = 5f; + // TODO: fixme xan! + //if (needAOETargets == 1) + // Hints.RecommendedRangeToTarget = 5f; PushGCD(AID.ArtOfWar1, Player); } diff --git a/BossMod/Autorotation/xan/Melee/MNK.cs b/BossMod/Autorotation/xan/Melee/MNK.cs index 6191ecfe9b..0d217440d7 100644 --- a/BossMod/Autorotation/xan/Melee/MNK.cs +++ b/BossMod/Autorotation/xan/Melee/MNK.cs @@ -416,7 +416,7 @@ private void WindsReply() } private float DesiredFireWindow => GCDLength * 10; - private float EarliestRoF(float estimatedDelay) => MathF.Max(estimatedDelay + 0.6f, 20.6f - DesiredFireWindow); + private float EarliestRoF(float estimatedDelay) => MathF.Max(estimatedDelay + 0.8f, 20.6f - DesiredFireWindow); private void Potion() => Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionStr, Player, ActionQueue.Priority.Low + 100 + (float)OGCDPriority.Potion); diff --git a/BossMod/BossModule/AIHints.cs b/BossMod/BossModule/AIHints.cs index 21a5c30506..d3e707d15c 100644 --- a/BossMod/BossModule/AIHints.cs +++ b/BossMod/BossModule/AIHints.cs @@ -56,17 +56,15 @@ public enum SpecialMode // AI will try to move in such a way to avoid standing in any forbidden zone after its activation or outside of some restricted zone after its activation, even at the cost of uptime public List<(Func shapeDistance, DateTime activation)> ForbiddenZones = []; - // positioning: rough target & radius of the movement; if not set, uses either target's position or module center instead - // used to somewhat prioritize movement direction and optimize pathfinding - public WPos? PathfindingHintDestination; - public float? PathfindingHintRadius; + // positioning: list of goal functions + // AI will try to move to reach non-forbidden point with highest goal value (sum of values returned by all functions) + // guideline: rotation modules should return 1 if it would use single-target action from that spot, 2 if it is also a positional, 3 if it would use aoe that would hit minimal viable number of targets, +1 for each extra target + // other parts of the code can return small (e.g. 0.01) values to slightly (de)prioritize some positions, or large (e.g. 1000) values to effectively soft-override target position (but still utilize pathfinding) + public List> GoalZones = []; // positioning: next positional hint (TODO: reconsider, maybe it should be a list prioritized by in-gcds, and imminent should be in-gcds instead? or maybe it should be property of an enemy? do we need correct?) public (Actor? Target, Positional Pos, bool Imminent, bool Correct) RecommendedPositional; - // positioning: recommended range to target (TODO: reconsider?) - public float RecommendedRangeToTarget; - // orientation restrictions (e.g. for gaze attacks): a list of forbidden orientation ranges, now or in near future // AI will rotate to face allowed orientation at last possible moment, potentially losing uptime public List<(Angle center, Angle halfWidth, DateTime activation)> ForbiddenDirections = []; @@ -94,10 +92,8 @@ public void Clear() ForcedMovement = null; InteractWithTarget = null; ForbiddenZones.Clear(); - PathfindingHintDestination = null; - PathfindingHintRadius = null; + GoalZones.Clear(); RecommendedPositional = default; - RecommendedRangeToTarget = 0; ForbiddenDirections.Clear(); ImminentSpecialMode = default; PredictedDamage.Clear(); @@ -170,5 +166,95 @@ public void Normalize() public bool TargetInAOECone(Actor target, WPos origin, float radius, WDir direction, Angle halfAngle) => target.Position.InCircleCone(origin, radius + target.HitboxRadius, direction, halfAngle); public bool TargetInAOERect(Actor target, WPos origin, WDir direction, float lenFront, float halfWidth, float lenBack = 0) => target.Position.InRect(origin, direction, lenFront + target.HitboxRadius, lenBack, halfWidth); + // goal zones + // simple goal zone that returns 1 if target is in range, useful for single-target actions + public Func GoalSingleTarget(WPos target, float radius) + { + var effRsq = radius * radius; + return p => (p - target).LengthSq() <= effRsq ? 1 : 0; + } + public Func GoalSingleTarget(Actor target, float range) => GoalSingleTarget(target.Position, range + target.HitboxRadius + 0.5f); + + // simple goal zone that returns 1 if target is in range (usually melee), 2 if it's also in correct positional + public Func GoalSingleTarget(WPos target, Angle rotation, Positional positional, float radius) + { + if (positional == Positional.Any) + return GoalSingleTarget(target, radius); // more efficient implementation + var effRsq = radius * radius; + var targetDir = rotation.ToDirection(); + return p => + { + var offset = p - target; + var lsq = offset.LengthSq(); + if (lsq > effRsq) + return 0; // out of range + // note: this assumes that extra dot is cheaper than sqrt?.. + var front = targetDir.Dot(offset); + var side = Math.Abs(targetDir.Dot(offset.OrthoL())); + var inPositional = positional switch + { + Positional.Flank => side > Math.Abs(front), + Positional.Rear => -front > side, + Positional.Front => front > side, // TODO: reconsider this, it's not a real positional?.. + _ => false + }; + return inPositional ? 2 : 1; + }; + } + public Func GoalSingleTarget(Actor target, Positional positional, float range = 3) => GoalSingleTarget(target.Position, target.Rotation, positional, range + target.HitboxRadius); + + // simple goal zone that returns number of targets in aoes; note that performance is a concern for these functions, and perfection isn't required, so eg they ignore forbidden targets, etc + public Func GoalAOECircle(float radius) + { + List<(WPos pos, float radius)> targets = [.. PriorityTargets.Select(e => (e.Actor.Position, e.Actor.HitboxRadius))]; + return p => targets.Count(t => t.pos.InCircle(p, radius + t.radius)); + } + + public Func GoalAOECone(Actor primaryTarget, float radius, Angle halfAngle) + { + List<(WPos pos, float radius)> targets = [.. PriorityTargets.Select(e => (e.Actor.Position, e.Actor.HitboxRadius))]; + var aimPoint = primaryTarget.Position; + var effRange = radius + primaryTarget.HitboxRadius; + var effRsq = effRange * effRange; + return p => + { + var toTarget = aimPoint - p; + var lenSq = toTarget.LengthSq(); + if (lenSq > effRsq) + return 0; + var dir = toTarget / MathF.Sqrt(lenSq); + return targets.Count(t => t.pos.InCircleCone(p, radius + t.radius, dir, halfAngle)); + }; + } + + public Func GoalAOERect(Actor primaryTarget, float lenFront, float halfWidth, float lenBack = 0) + { + List<(WPos pos, float radius)> targets = [.. PriorityTargets.Select(e => (e.Actor.Position, e.Actor.HitboxRadius))]; + var aimPoint = primaryTarget.Position; + var effRange = lenFront + primaryTarget.HitboxRadius; + var effRsq = effRange * effRange; + return p => + { + var toTarget = aimPoint - p; + var lenSq = toTarget.LengthSq(); + if (lenSq > effRsq) + return 0; + var dir = toTarget / MathF.Sqrt(lenSq); + return targets.Count(t => t.pos.InRect(p, dir, lenFront, lenBack, halfWidth)); + }; + } + + // combined goal zone: returns 'aoe' priority if targets hit are at or above minimum, otherwise returns 'single-target' priority + public Func GoalCombined(Func singleTarget, Func aoe, int minAOETargets) + { + if (minAOETargets >= 50) + return singleTarget; // assume aoe is never efficient, so don't bother + return p => + { + var aoeTargets = aoe(p) - minAOETargets; + return aoeTargets >= 0 ? 3 + aoeTargets : singleTarget(p); + }; + } + public WPos ClampToBounds(WPos position) => Center + Bounds.ClampToBounds(position - Center); } diff --git a/BossMod/BossModule/AIHintsBuilder.cs b/BossMod/BossModule/AIHintsBuilder.cs index 8c89ecafb0..ba4d5c7265 100644 --- a/BossMod/BossModule/AIHintsBuilder.cs +++ b/BossMod/BossModule/AIHintsBuilder.cs @@ -36,7 +36,6 @@ public void Update(AIHints hints, int playerSlot) var playerAssignment = Service.Config.Get()[_ws.Party.Members[playerSlot].ContentId]; var activeModule = _bmm.ActiveModule?.StateMachine.ActivePhase != null ? _bmm.ActiveModule : null; hints.FillPotentialTargets(_ws, playerAssignment == PartyRolesConfig.Assignment.MT || playerAssignment == PartyRolesConfig.Assignment.OT && !_ws.Party.WithoutSlot().Any(p => p != player && p.Role == Role.Tank)); - hints.RecommendedRangeToTarget = player.Role is Role.Melee or Role.Tank ? 3 : 25; if (activeModule != null) activeModule.CalculateAIHints(playerSlot, player, playerAssignment, hints); else diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index ac5cbb44de..3d9f9bd3ba 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -124,6 +124,7 @@ public override WDir ClampToBounds(WDir offset) private Pathfinding.Map BuildMap() { + // circle is convex, and pathfinding always aims to cell centers, so we can only block pixels which centers are out of bounds var map = new Pathfinding.Map(MapResolution, default, Radius, Radius); map.BlockPixelsInsideArenaBounds(ShapeDistance.InvertedCircle(default, Radius), 0, 0); return map; diff --git a/BossMod/BossModule/BossModule.cs b/BossMod/BossModule/BossModule.cs index 76affc9182..94c2f6b54a 100644 --- a/BossMod/BossModule/BossModule.cs +++ b/BossMod/BossModule/BossModule.cs @@ -248,6 +248,10 @@ public void ReportError(BossComponent? comp, string message) // default implementation activates if primary target is both targetable and in combat protected virtual bool CheckPull() { return PrimaryActor.IsTargetable && PrimaryActor.InCombat; } + // called during update if module is active; should return true if module is to be reset (i.e. deleted and new instance recreated for same actor) + // default implementation never resets, but it's useful for outdoor bosses that can be leashed + public virtual bool CheckReset() => false; + protected virtual void UpdateModule() { } protected virtual void DrawArenaBackground(int pcSlot, Actor pc) { } // before modules background protected virtual void DrawArenaForeground(int pcSlot, Actor pc) { } // after border, before modules foreground diff --git a/BossMod/BossModule/BossModuleManager.cs b/BossMod/BossModule/BossModuleManager.cs index 8e330f19fb..c6bae7d368 100644 --- a/BossMod/BossModule/BossModuleManager.cs +++ b/BossMod/BossModule/BossModuleManager.cs @@ -86,6 +86,16 @@ public void Update() continue; } + // if module is active and wants to be reset, oblige + if (isActive && m.CheckReset()) + { + var actor = m.PrimaryActor; + UnloadModule(i--); + if (!actor.IsDestroyed) + ActorAdded(actor); + continue; + } + // module remains loaded var priority = ModuleDisplayPriority(m); if (priority > bestPriority) diff --git a/BossMod/BossModule/SimpleBossModule.cs b/BossMod/BossModule/SimpleBossModule.cs index df56aab6ee..aa490c627d 100644 --- a/BossMod/BossModule/SimpleBossModule.cs +++ b/BossMod/BossModule/SimpleBossModule.cs @@ -4,5 +4,6 @@ // these always center map around PC public abstract class SimpleBossModule(WorldState ws, Actor primary) : BossModule(ws, primary, primary.Position, new ArenaBoundsCircle(30)) { + public override bool CheckReset() => !PrimaryActor.InCombat; protected override void UpdateModule() => Arena.Center = WorldState.Party.Player()?.Position ?? default; } diff --git a/BossMod/Config/ConfigChangelog.cs b/BossMod/Config/ConfigChangelog.cs new file mode 100644 index 0000000000..bf55a6d6ad --- /dev/null +++ b/BossMod/Config/ConfigChangelog.cs @@ -0,0 +1,121 @@ +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using System.Reflection; + +namespace BossMod; + +public sealed record class VersionedField(ConfigNode Node, FieldInfo FieldInfo, Version AddedVersion) +{ + public string FieldKey => $"{Node}.{FieldInfo.Name}"; +} + +public class ConfigChangelogWindow : UIWindow +{ + private readonly Version PreviousVersion; + private readonly List Fields; + + public ConfigChangelogWindow() : base("BMR Changelog", true, new(400, 300)) + { + PreviousVersion = GetPreviousPluginVersion(); + Service.Config.AssemblyVersion = GetCurrentPluginVersion(); + if (Service.Config.AssemblyVersion != PreviousVersion) + { + Service.Config.Modified.Fire(); + Fields = GetAllFields().Where(f => f.AddedVersion > PreviousVersion).ToList(); + } + else + { + Fields = []; + } + + if (Fields.Count == 0) + { + // nothing interesting to show... + IsOpen = false; + Dispose(); + } + } + + public override void Draw() + { + ImGui.TextUnformatted($"The following config options have been added since version {PreviousVersion}:"); + + ImGui.Separator(); + + Action? postIteration = null; + foreach (var group in Fields.GroupBy(f => f.Node.GetType())) + { + ImGui.TextUnformatted(group.Key.GetCustomAttribute()?.Name ?? ""); + foreach (var f in group) + { + using var id = ImRaii.PushId($"changelog{f.FieldKey}"); + + var disp = f.FieldInfo.GetCustomAttribute(); + + ImGui.Bullet(); + if (!string.IsNullOrEmpty(disp?.Tooltip)) + UIMisc.HelpMarker(disp!.Tooltip); + ImGui.SameLine(); + ImGui.TextUnformatted(disp?.Label ?? "unknown"); + ImGui.SameLine(); + if (ImGui.Button("Enable")) + postIteration += () => SetOption(f, true); + ImGui.SameLine(); + if (ImGui.Button("Disable")) + postIteration += () => SetOption(f, false); + } + } + postIteration?.Invoke(); + } + + private void SetOption(VersionedField field, bool value) + { + field.FieldInfo.SetValue(field.Node, value); + Service.Config.Modified.Fire(); + + Fields.Remove(field); + if (Fields.Count == 0) + IsOpen = false; + } + + private static IEnumerable GetAllFields() + { + foreach (var n in Service.Config.Nodes) + { + var sinceNode = n.GetType().GetCustomAttribute()?.Since; + + foreach (var f in n.GetType().GetFields()) + { + // i don't feel like supporting non bool fields + if (f.FieldType != typeof(bool)) + continue; + + if (sinceNode != null) + yield return new(n, f, Version.Parse(sinceNode)); + else if (f.GetCustomAttribute()?.Since is string sinceVersion) + yield return new(n, f, Version.Parse(sinceVersion)); + } + } + } + + private static Version GetCurrentPluginVersion() + { +#if DEBUG + // version is always 0.0.0.0 in debug, making it useless for testing + return new(0, 0, 0, 999); +#else + return Assembly.GetExecutingAssembly().GetName().Version!; +#endif + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate")] + private static Version GetPreviousPluginVersion() + { +#if DEBUG + // change value to something sensible if you want to test the changelog stuff + return new(0, 0, 0, 999); +#else + return Service.Config.AssemblyVersion; +#endif + } +} diff --git a/BossMod/Config/ConfigNode.cs b/BossMod/Config/ConfigNode.cs index 97c772736e..237a3b874f 100644 --- a/BossMod/Config/ConfigNode.cs +++ b/BossMod/Config/ConfigNode.cs @@ -10,16 +10,18 @@ public sealed class ConfigDisplayAttribute : Attribute public string? Name { get; set; } public int Order { get; set; } public Type? Parent { get; set; } + public string? Since { get; set; } } // attribute that specifies how config node field or enumeration value is shown in the UI [AttributeUsage(AttributeTargets.Field)] -public sealed class PropertyDisplayAttribute(string label, uint color = 0, string tooltip = "", bool separator = false) : Attribute +public sealed class PropertyDisplayAttribute(string label, uint color = 0, string tooltip = "", bool separator = false, string? since = null) : Attribute { public string Label { get; } = label; public uint Color { get; } = color == 0 ? Colors.TextColor1 : color; public string Tooltip { get; } = tooltip; public bool Separator { get; } = separator; + public string? Since { get; } = since; } // attribute that specifies combobox should be used for displaying int/bool property diff --git a/BossMod/Config/ConfigRoot.cs b/BossMod/Config/ConfigRoot.cs index 647c27e902..efdcecc7b8 100644 --- a/BossMod/Config/ConfigRoot.cs +++ b/BossMod/Config/ConfigRoot.cs @@ -10,6 +10,7 @@ public class ConfigRoot private const int _version = 10; public Event Modified = new(); + public Version AssemblyVersion = new(); // we use this to show newly added config options private readonly Dictionary _nodes = []; public IEnumerable Nodes => _nodes.Values; @@ -45,6 +46,7 @@ public void LoadFromFile(FileInfo file) var node = type != null ? _nodes.GetValueOrDefault(type) : null; node?.Deserialize(jconfig.Value, ser); } + AssemblyVersion = json.RootElement.TryGetProperty(nameof(AssemblyVersion), out var jver) ? new(jver.GetString() ?? "") : new(); } catch (Exception e) { @@ -66,6 +68,7 @@ public void SaveToFile(FileInfo file) n.Serialize(jwriter, ser); } jwriter.WriteEndObject(); + jwriter.WriteString(nameof(AssemblyVersion), AssemblyVersion.ToString()); }); } catch (Exception e) diff --git a/BossMod/Debug/DebugAction.cs b/BossMod/Debug/DebugAction.cs index 46c02841d3..e31100a802 100644 --- a/BossMod/Debug/DebugAction.cs +++ b/BossMod/Debug/DebugAction.cs @@ -1,6 +1,7 @@ using Dalamud.Game.Gui; using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Event; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; namespace BossMod; @@ -12,20 +13,21 @@ sealed unsafe class DebugAction : IDisposable private readonly WorldState _ws; private readonly ActionManagerEx _amex; - //private delegate uint GetActionStatusBallistaDelegate(Vector3* source, float radius, float minRange, float maxRange, Vector3* target, Vector3* aimPoint, uint* outOptExtraInfo); - //private readonly HookAddress _gasHook; + private bool _autoAttack; + //private delegate byte SetAutoAttackDelegate(byte* self, byte value, byte sendPacket, byte isInstant); + //private readonly HookAddress _hook; public DebugAction(WorldState ws, ActionManagerEx amex) { _ws = ws; _amex = amex; - //_gasHook = new(Service.SigScanner.Module.BaseAddress + 0xB5F300, GetActionStatusBallistaDetour); + //_hook = new(Service.SigScanner.Module.BaseAddress + 0xAD3740, SetAutoAttackDetour); Service.Log("---"); } public void Dispose() { - //_gasHook.Dispose(); + //_hook.Dispose(); } public void DrawActionManagerExtensions() @@ -192,6 +194,15 @@ public void DrawDutyActions() } } + public void DrawAutoAttack() + { + var aa = UIState.Instance()->WeaponState.IsAutoAttacking; + if (_autoAttack != aa) + Service.Log($"AA state changed: {_autoAttack} -> {aa}"); + _autoAttack = aa; + ImGui.TextUnformatted($"Auto-attack: {aa}"); + } + private void DrawStatus(string prompt, ActionID action, bool checkRecast, bool checkCasting) { uint extra; @@ -210,10 +221,10 @@ private void DrawFilteredActions(string tag, Func {res} ({*outOptExtraInfo})"); - // return res; + // if (*self != 0 || value != 0) + // Service.Log($"SAA: {*self} -> {value} ({sendPacket}, {isInstant})"); + // return _hook.Original(self, value, sendPacket, isInstant); //} } diff --git a/BossMod/Debug/DebugCollision.cs b/BossMod/Debug/DebugCollision.cs index 012d7b5d27..b403408caa 100644 --- a/BossMod/Debug/DebugCollision.cs +++ b/BossMod/Debug/DebugCollision.cs @@ -528,8 +528,9 @@ // private void VisualizeOBB(Vector3 min, Vector3 max, CollisionObjectShape* obj) => Camera.Instance?.DrawWorldOBB(min, max, obj->World.M, Colors.Safe); // private void VisualizeVertex(Vector3 v, CollisionObjectShape* obj) => Camera.Instance?.DrawWorldSphere(SharpDX.Vector3.TransformCoordinate(new(v.X, v.Y, v.Z), obj->World.M).ToSystem(), 0.1f, Colors.Danger); -// private void VisualizePrimitive(CollisionShapePCBData* data, int iPrim, CollisionObjectShape* obj, uint color = Colors.Danger) +// private void VisualizePrimitive(CollisionShapePCBData* data, int iPrim, CollisionObjectShape* obj, uint color = 0) // { +// var colors = color == 0 ? Colors.Danger : color; // var pRaw = (float*)(data + 1); // var pCompr = (ushort*)(pRaw + 3 * data->NumVertsRaw); // var pPrim = (CollisionShapePrimitive*)(pCompr + 3 * data->NumVertsCompressed); @@ -541,19 +542,20 @@ // var w1 = SharpDX.Vector3.TransformCoordinate(new(v1.X, v1.Y, v1.Z), w).ToSystem(); // var w2 = SharpDX.Vector3.TransformCoordinate(new(v2.X, v2.Y, v2.Z), w).ToSystem(); // var w3 = SharpDX.Vector3.TransformCoordinate(new(v3.X, v3.Y, v3.Z), w).ToSystem(); -// Camera.Instance?.DrawWorldLine(w1, w2, color); -// Camera.Instance?.DrawWorldLine(w2, w3, color); -// Camera.Instance?.DrawWorldLine(w3, w1, color); +// Camera.Instance?.DrawWorldLine(w1, w2, colors); +// Camera.Instance?.DrawWorldLine(w2, w3, colors); +// Camera.Instance?.DrawWorldLine(w3, w1, colors); // } -// private void VisualizeShape(CollisionShapePCBData* data, CollisionObjectShape* obj, uint color = Colors.Danger) +// private void VisualizeShape(CollisionShapePCBData* data, CollisionObjectShape* obj, uint color = 0) // { +// var colors = color == 0 ? Colors.Danger : color; // for (int i = 0; i < data->NumPrims; ++i) // VisualizePrimitive(data, i, obj, color); // if (data->Child1Offset != 0) -// VisualizeShape((CollisionShapePCBData*)((byte*)data + data->Child1Offset), obj, color); +// VisualizeShape((CollisionShapePCBData*)((byte*)data + data->Child1Offset), obj, colors); // if (data->Child2Offset != 0) -// VisualizeShape((CollisionShapePCBData*)((byte*)data + data->Child2Offset), obj, color); +// VisualizeShape((CollisionShapePCBData*)((byte*)data + data->Child2Offset), obj, colors); // } // private void VisualizeObject(CollisionObjectBase* obj) diff --git a/BossMod/Debug/MainDebugWindow.cs b/BossMod/Debug/MainDebugWindow.cs index be81913f22..156cae22ae 100644 --- a/BossMod/Debug/MainDebugWindow.cs +++ b/BossMod/Debug/MainDebugWindow.cs @@ -29,7 +29,7 @@ protected override void Dispose(bool disposing) _debugClassDefinitions.Dispose(); _debugAddon.Dispose(); // _debugCollision.Dispose(); - //_debugVfx.Dispose(); + // _debugVfx.Dispose(); base.Dispose(disposing); } @@ -111,6 +111,10 @@ public override unsafe void Draw() { _debugAction.DrawDutyActions(); } + if (ImGui.CollapsingHeader("Auto attacks")) + { + _debugAction.DrawAutoAttack(); + } if (ImGui.CollapsingHeader("Hate")) { _debugHate.Draw(); diff --git a/BossMod/Framework/ActionManagerEx.cs b/BossMod/Framework/ActionManagerEx.cs index 5d2587bc85..278e153544 100644 --- a/BossMod/Framework/ActionManagerEx.cs +++ b/BossMod/Framework/ActionManagerEx.cs @@ -59,6 +59,7 @@ public sealed unsafe class ActionManagerEx : IDisposable private readonly RestoreRotationTweak _restoreRotTweak = new(); private readonly SmartRotationTweak _smartRotationTweak; private readonly OutOfCombatActionsTweak _oocActionsTweak; + private readonly AutoAutosTweak _autoAutosTweak; private readonly HookAddress _updateHook; private readonly HookAddress _useActionHook; @@ -80,6 +81,7 @@ public ActionManagerEx(WorldState ws, AIHints hints, MovementOverride movement) _dismountTweak = new(ws); _smartRotationTweak = new(ws, hints); _oocActionsTweak = new(ws); + _autoAutosTweak = new(ws, hints); Service.Log($"[AMEx] ActionManager singleton address = 0x{(ulong)_inst:X}"); _updateHook = new(ActionManager.Addresses.Update, UpdateDetour); @@ -374,6 +376,10 @@ private void UpdateDetour(ActionManager* self) if (_ws.Party.Player()?.CastInfo != null && _cancelCastTweak.ShouldCancel(_ws.CurrentTime, ForceCancelCastNextFrame)) UIState.Instance()->Hotbar.CancelCast(); ForceCancelCastNextFrame = false; + + var autosEnabled = UIState.Instance()->WeaponState.IsAutoAttacking; + if (_autoAutosTweak.GetDesiredState(autosEnabled) != autosEnabled) + _inst->UseAction(CSActionType.GeneralAction, 1); } // note: targetId is usually your current primary target (or 0xE0000000 if you don't target anyone), unless you do something like /ac XXX etc @@ -418,10 +424,17 @@ private bool UseActionDetour(ActionManager* self, CSActionType actionType, uint private bool UseActionLocationDetour(ActionManager* self, CSActionType actionType, uint actionId, ulong targetId, Vector3* location, uint extraParam) { + var targetSystem = TargetSystem.Instance(); var player = GameObjectManager.Instance()->Objects.IndexSorted[0].Value; var prevSeq = _inst->LastUsedActionSequence; var prevRot = player != null ? player->Rotation.Radians() : default; + var hardTarget = targetSystem->Target; + var preventAutos = _autoAutosTweak.ShouldPreventAutoActivation(ActionManager.GetSpellIdForAction(actionType, actionId)); + if (preventAutos) + targetSystem->Target = null; bool ret = _useActionLocationHook.Original(self, actionType, actionId, targetId, location, extraParam); + if (preventAutos) + targetSystem->Target = hardTarget; var currSeq = _inst->LastUsedActionSequence; var currRot = player != null ? player->Rotation.Radians() : default; if (currSeq != prevSeq) diff --git a/BossMod/Framework/Plugin.cs b/BossMod/Framework/Plugin.cs index b92d39ee64..574bbbb761 100644 --- a/BossMod/Framework/Plugin.cs +++ b/BossMod/Framework/Plugin.cs @@ -99,6 +99,8 @@ public unsafe Plugin(IDalamudPluginInterface dalamud, ICommandManager commandMan dalamud.UiBuilder.DisableAutomaticUiHide = true; dalamud.UiBuilder.Draw += DrawUI; dalamud.UiBuilder.OpenConfigUi += () => OpenConfigUI(); + + _ = new ConfigChangelogWindow(); } public void Dispose() diff --git a/BossMod/Modules/Dawntrail/Savage/M02SHoneyBLovely/AI/AIExperiment.cs b/BossMod/Modules/Dawntrail/Savage/M02SHoneyBLovely/AI/AIExperiment.cs index 619c458dad..3510e9b126 100644 --- a/BossMod/Modules/Dawntrail/Savage/M02SHoneyBLovely/AI/AIExperiment.cs +++ b/BossMod/Modules/Dawntrail/Savage/M02SHoneyBLovely/AI/AIExperiment.cs @@ -38,7 +38,7 @@ public static RotationModuleDefinition Definition() public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, float forceMovementIn, bool isMoving) { - if (Manager.Bossmods.ActiveModule is not M02SHoneyBLovely module) + if (Bossmods.ActiveModule is not M02SHoneyBLovely module) return; var drag = strategy.Option(Track.DragBoss).As(); diff --git a/BossMod/Modules/Dawntrail/Savage/M04SWickedThunder/AI/AIExperiment.cs b/BossMod/Modules/Dawntrail/Savage/M04SWickedThunder/AI/AIExperiment.cs index bdf8b975fa..761c3fdd8a 100644 --- a/BossMod/Modules/Dawntrail/Savage/M04SWickedThunder/AI/AIExperiment.cs +++ b/BossMod/Modules/Dawntrail/Savage/M04SWickedThunder/AI/AIExperiment.cs @@ -29,7 +29,7 @@ public static RotationModuleDefinition Definition() public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, float forceMovementIn, bool isMoving) { - if (Manager.Bossmods.ActiveModule is not M04SWickedThunder module) + if (Bossmods.ActiveModule is not M04SWickedThunder module) return; var ewh = strategy.Option(Track.ElectrifyingWitchHunt).As(); diff --git a/BossMod/Modules/Endwalker/Criterion/C01ASS/C011Silkie/C011Silkie.cs b/BossMod/Modules/Endwalker/Criterion/C01ASS/C011Silkie/C011Silkie.cs index 35a01e3266..91581bab65 100644 --- a/BossMod/Modules/Endwalker/Criterion/C01ASS/C011Silkie/C011Silkie.cs +++ b/BossMod/Modules/Endwalker/Criterion/C01ASS/C011Silkie/C011Silkie.cs @@ -35,8 +35,8 @@ class SFizzlingDusterPuff(BossModule module) : FizzlingDusterPuff(module, AID.SF public static readonly AOEShapeCone ShapeYellow = new(60, 22.5f.Degrees()); } -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 878, NameID = 11369, SortOrder = 5)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 878, NameID = 11369, SortOrder = 5, PlanLevel = 90)] public class C011NSilkie(WorldState ws, Actor primary) : C011Silkie(ws, primary); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 879, NameID = 11369, SortOrder = 5)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 879, NameID = 11369, SortOrder = 5, PlanLevel = 90)] public class C011SSilkie(WorldState ws, Actor primary) : C011Silkie(ws, primary); diff --git a/BossMod/Modules/Endwalker/Criterion/C01ASS/C012Gladiator/C012Gladiator.cs b/BossMod/Modules/Endwalker/Criterion/C01ASS/C012Gladiator/C012Gladiator.cs index 49a84ed34d..11ec521860 100644 --- a/BossMod/Modules/Endwalker/Criterion/C01ASS/C012Gladiator/C012Gladiator.cs +++ b/BossMod/Modules/Endwalker/Criterion/C01ASS/C012Gladiator/C012Gladiator.cs @@ -10,8 +10,8 @@ class SRushOfMightBack(BossModule module) : RushOfMightBack(module, AID.SRushOfM public abstract class C012Gladiator(WorldState ws, Actor primary) : BossModule(ws, primary, new(-35, -271), new ArenaBoundsSquare(20)); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 878, NameID = 11387, SortOrder = 8)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 878, NameID = 11387, SortOrder = 8, PlanLevel = 90)] public class C012NGladiator(WorldState ws, Actor primary) : C012Gladiator(ws, primary); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 879, NameID = 11387, SortOrder = 8)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 879, NameID = 11387, SortOrder = 8, PlanLevel = 90)] public class C012SGladiator(WorldState ws, Actor primary) : C012Gladiator(ws, primary); diff --git a/BossMod/Modules/Endwalker/Criterion/C01ASS/C013Shadowcaster/C013Shadowcaster.cs b/BossMod/Modules/Endwalker/Criterion/C01ASS/C013Shadowcaster/C013Shadowcaster.cs index 9676eed1fe..24dbcd3795 100644 --- a/BossMod/Modules/Endwalker/Criterion/C01ASS/C013Shadowcaster/C013Shadowcaster.cs +++ b/BossMod/Modules/Endwalker/Criterion/C01ASS/C013Shadowcaster/C013Shadowcaster.cs @@ -10,8 +10,8 @@ class SPureFire(BossModule module) : PureFire(module, AID.SPureFireAOE); public abstract class C013Shadowcaster(WorldState ws, Actor primary) : BossModule(ws, primary, new(289, -105), new ArenaBoundsRect(15, 20)); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 878, NameID = 11393, SortOrder = 9)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 878, NameID = 11393, SortOrder = 9, PlanLevel = 90)] public class C013NShadowcaster(WorldState ws, Actor primary) : C013Shadowcaster(ws, primary); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 879, NameID = 11393, SortOrder = 9)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 879, NameID = 11393, SortOrder = 9, PlanLevel = 90)] public class C013SShadowcaster(WorldState ws, Actor primary) : C013Shadowcaster(ws, primary); diff --git a/BossMod/Modules/Endwalker/Criterion/C02AMR/C021Shishio/C021Shishio.cs b/BossMod/Modules/Endwalker/Criterion/C02AMR/C021Shishio/C021Shishio.cs index 9d43dc9e5c..b53b941290 100644 --- a/BossMod/Modules/Endwalker/Criterion/C02AMR/C021Shishio/C021Shishio.cs +++ b/BossMod/Modules/Endwalker/Criterion/C02AMR/C021Shishio/C021Shishio.cs @@ -16,8 +16,8 @@ public abstract class C021Shishio(WorldState ws, Actor primary) : BossModule(ws, public static readonly ArenaBoundsCircle CircleBounds = new(20); } -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 946, NameID = 12428, SortOrder = 4)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 946, NameID = 12428, SortOrder = 4, PlanLevel = 90)] public class C021NShishio(WorldState ws, Actor primary) : C021Shishio(ws, primary); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 947, NameID = 12428, SortOrder = 4)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 947, NameID = 12428, SortOrder = 4, PlanLevel = 90)] public class C021SShishio(WorldState ws, Actor primary) : C021Shishio(ws, primary); diff --git a/BossMod/Modules/Endwalker/Criterion/C02AMR/C022Gorai/C022Gorai.cs b/BossMod/Modules/Endwalker/Criterion/C02AMR/C022Gorai/C022Gorai.cs index 954ad50ab4..e038b73192 100644 --- a/BossMod/Modules/Endwalker/Criterion/C02AMR/C022Gorai/C022Gorai.cs +++ b/BossMod/Modules/Endwalker/Criterion/C02AMR/C022Gorai/C022Gorai.cs @@ -11,8 +11,8 @@ public abstract class C022Gorai(WorldState ws, Actor primary) : BossModule(ws, p public static readonly ArenaBoundsSquare DefaultBounds = new(20); } -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 946, NameID = 12373, SortOrder = 7)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 946, NameID = 12373, SortOrder = 7, PlanLevel = 90)] public class C022NGorai(WorldState ws, Actor primary) : C022Gorai(ws, primary); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 947, NameID = 12373, SortOrder = 7)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 947, NameID = 12373, SortOrder = 7, PlanLevel = 90)] public class C022SGorai(WorldState ws, Actor primary) : C022Gorai(ws, primary); diff --git a/BossMod/Modules/Endwalker/Criterion/C02AMR/C023Moko/C023Moko.cs b/BossMod/Modules/Endwalker/Criterion/C02AMR/C023Moko/C023Moko.cs index 6d3645abd8..7fdae545e3 100644 --- a/BossMod/Modules/Endwalker/Criterion/C02AMR/C023Moko/C023Moko.cs +++ b/BossMod/Modules/Endwalker/Criterion/C02AMR/C023Moko/C023Moko.cs @@ -11,8 +11,8 @@ public abstract class C023Moko(WorldState ws, Actor primary) : BossModule(ws, pr public static readonly ArenaBoundsSquare DefaultBounds = new(20); } -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 946, NameID = 12357, SortOrder = 8)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 946, NameID = 12357, SortOrder = 8, PlanLevel = 90)] public class C023NMoko(WorldState ws, Actor primary) : C023Moko(ws, primary); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 947, NameID = 12357, SortOrder = 8)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 947, NameID = 12357, SortOrder = 8, PlanLevel = 90)] public class C023SMoko(WorldState ws, Actor primary) : C023Moko(ws, primary); diff --git a/BossMod/Modules/Endwalker/Criterion/C03AAI/C031Ketuduke/C031Ketuduke.cs b/BossMod/Modules/Endwalker/Criterion/C03AAI/C031Ketuduke/C031Ketuduke.cs index a3f0cdf08e..8ee8630323 100644 --- a/BossMod/Modules/Endwalker/Criterion/C03AAI/C031Ketuduke/C031Ketuduke.cs +++ b/BossMod/Modules/Endwalker/Criterion/C03AAI/C031Ketuduke/C031Ketuduke.cs @@ -16,8 +16,8 @@ class SHydrobomb(BossModule module) : Hydrobomb(module, AID.SHydrobombAOE); public abstract class C031Ketuduke(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsSquare(20)); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 979, NameID = 12605, SortOrder = 5)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 979, NameID = 12605, SortOrder = 5, PlanLevel = 90)] public class C031NKetuduke(WorldState ws, Actor primary) : C031Ketuduke(ws, primary); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 980, NameID = 12605, SortOrder = 5)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 980, NameID = 12605, SortOrder = 5, PlanLevel = 90)] public class C031SKetuduke(WorldState ws, Actor primary) : C031Ketuduke(ws, primary); diff --git a/BossMod/Modules/Endwalker/Criterion/C03AAI/C032Lala/C032Lala.cs b/BossMod/Modules/Endwalker/Criterion/C03AAI/C032Lala/C032Lala.cs index 3a94cc4535..84281579e2 100644 --- a/BossMod/Modules/Endwalker/Criterion/C03AAI/C032Lala/C032Lala.cs +++ b/BossMod/Modules/Endwalker/Criterion/C03AAI/C032Lala/C032Lala.cs @@ -6,8 +6,8 @@ class SArcaneBlight(BossModule module) : ArcaneBlight(module, AID.SArcaneBlightA public abstract class C032Lala(WorldState ws, Actor primary) : BossModule(ws, primary, new(200, 0), new ArenaBoundsSquare(20)); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 979, NameID = 12639, SortOrder = 8)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 979, NameID = 12639, SortOrder = 8, PlanLevel = 90)] public class C032NLala(WorldState ws, Actor primary) : C032Lala(ws, primary); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 980, NameID = 12639, SortOrder = 8)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 980, NameID = 12639, SortOrder = 8, PlanLevel = 90)] public class C032SLala(WorldState ws, Actor primary) : C032Lala(ws, primary); diff --git a/BossMod/Modules/Endwalker/Criterion/C03AAI/C033Statice/C033Statice.cs b/BossMod/Modules/Endwalker/Criterion/C03AAI/C033Statice/C033Statice.cs index 9fceed3b6a..9815a8e185 100644 --- a/BossMod/Modules/Endwalker/Criterion/C03AAI/C033Statice/C033Statice.cs +++ b/BossMod/Modules/Endwalker/Criterion/C03AAI/C033Statice/C033Statice.cs @@ -12,8 +12,8 @@ class SFaerieRing(BossModule module) : FaerieRing(module, AID.SFaerieRing); public abstract class C033Statice(WorldState ws, Actor primary) : BossModule(ws, primary, new(-200, 0), new ArenaBoundsCircle(20)); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 979, NameID = 12506, SortOrder = 9)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.NBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 979, NameID = 12506, SortOrder = 9, PlanLevel = 90)] public class C033NStatice(WorldState ws, Actor primary) : C033Statice(ws, primary); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 980, NameID = 12506, SortOrder = 9)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn", PrimaryActorOID = (uint)OID.SBoss, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 980, NameID = 12506, SortOrder = 9, PlanLevel = 90)] public class C033SStatice(WorldState ws, Actor primary) : C033Statice(ws, primary); diff --git a/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD20CloningNode.cs b/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD20CloningNode.cs index a66c22a529..78cf6b5109 100644 --- a/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD20CloningNode.cs +++ b/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD20CloningNode.cs @@ -70,7 +70,7 @@ private static bool IsCasterIntercardinal(Actor caster) return false; } - private static WPos RoundPosition(WPos position) => new(MathF.Round(position.X * 2) / 2, MathF.Round(position.Z * 2) / 2); + private static WPos RoundPosition(WPos position) => new(MathF.Round(position.X * 2) * 0.5f, MathF.Round(position.Z * 2) * 0.5f); } class PiercingLaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PiercingLaser), new AOEShapeRect(40, 2.5f)); @@ -86,4 +86,4 @@ public DD20CloningNodeStates(BossModule module) : base(module) } [ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 898, NameID = 12261)] -public class DD20CloningNode(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, -300), new ArenaBoundsCircle(19.5f)); \ No newline at end of file +public class DD20CloningNode(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, -300), new ArenaBoundsCircle(19.5f)); diff --git a/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD40TwintaniasClone.cs b/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD40TwintaniasClone.cs index babb6d87bf..27b990e044 100644 --- a/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD40TwintaniasClone.cs +++ b/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD40TwintaniasClone.cs @@ -95,4 +95,4 @@ public DD40TwintaniasCloneStates(BossModule module) : base(module) } [ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 900, NameID = 12263)] -public class DD40TwintaniasClone(WorldState ws, Actor primary) : BossModule(ws, primary, new(-600, -300), new ArenaBoundsCircle(20)); \ No newline at end of file +public class DD40TwintaniasClone(WorldState ws, Actor primary) : BossModule(ws, primary, new(-600, -300), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D100NybethObdilord.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D100NybethObdilord.cs index b435862728..db10763967 100644 --- a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D100NybethObdilord.cs +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D100NybethObdilord.cs @@ -15,10 +15,10 @@ public enum AID : uint Abyss = 6872, // Boss->player, 2.0s cast, range 6 circle // kinda like a tankbuster? It's a circle on the player ButterflyFloat = 6879, // IronCorse->player, 3.0s cast, single-target Catapult = 6878, // BicephalicCorse->location, 3.0s cast, range 6 circle - Doom = 6875, // Boss->self, 5.0s cast, range 45+R 120-degree cone, feels like this is wrong, + Doom = 6875, // Boss->self, 5.0s cast, range 45+R 120-degree cone, feels like this is wrong, GlassPunch = 6877, // GiantCorse/BicephalicCorse->self, no cast, range 6+R ?-degree cone Shackle = 6874, // Boss->self, 3.0s cast, range 50+R width 8 rect - SummonDarkness = 6876, // Boss->self, 3.0s cast, ???, Summons Corse's, + SummonDarkness = 6876, // Boss->self, 3.0s cast, ???, Summons Corse's, WordOfPain = 6873, // Boss->self, no cast, range 40+R circle } @@ -27,7 +27,15 @@ class Catapult(BossModule module) : Components.LocationTargetedAOEs(module, Acti class CorseAdds(BossModule module) : Components.AddsMulti(module, [(uint)OID.BicephalicCorse, (uint)OID.GiantCorse, (uint)OID.IronCorse]); class Doom(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Doom), new AOEShapeCone(47.4f, 60.Degrees())); class Shackle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Shackle), new AOEShapeRect(52.4f, 4, 0)); -class SummonDarkness(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.SummonDarkness), "Summoning the corses, use Resolution if you want them permanently dead"); +class SummonDarkness(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.SummonDarkness), "Summoning the corse, incoming Adds! \nRemember to use a resolution to make them permanently disappear"); + +class EncounterHints(BossModule module) : BossComponent(module) +{ + public override void AddGlobalHints(GlobalHints hints) + { + hints.Add($"There is 3 sets of adds that spawn at HP %'s -> (90%, 65%, 40%) \nA resolution can make the adds permanently disappear once they are at 0% HP/the corpse are just laying on the floor.\nResolution is also does high damage to the adds + 0.3% to the Boss\nSolo tip: Either pop a resolution on all add packs, or pop lust -> resolution on 2nd ad pack. Make sure to keep regen up!"); + } +} class D100NybethObdilordStates : StateMachineBuilder { @@ -39,9 +47,16 @@ public D100NybethObdilordStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter(); + .ActivateOnEnter() + .DeactivateOnEnter(); } } [ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "LegendofIceman", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 208, NameID = 5356)] -public class D100NybethObdilord(WorldState ws, Actor primary) : BossModule(ws, primary, new(300, 300), new ArenaBoundsCircle(24)); +public class D100NybethObdilord : BossModule +{ + public D100NybethObdilord(WorldState ws, Actor primary) : base(ws, primary, new(300, 300), new ArenaBoundsCircle(24)) + { + ActivateComponent(); + } +} diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D110Alicanto.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D110Alicanto.cs index 86c40231a0..72d074d7ae 100644 --- a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D110Alicanto.cs +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D110Alicanto.cs @@ -32,5 +32,5 @@ public D110AlicantoStates(BossModule module) : base(module) } } -[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 209, NameID = 5371)] +[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "LegendofIceman", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 209, NameID = 5371)] public class D110Alicanto(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, -235), new ArenaBoundsCircle(25)); diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D120Kirtimukha.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D120Kirtimukha.cs new file mode 100644 index 0000000000..4f53e73f89 --- /dev/null +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D120Kirtimukha.cs @@ -0,0 +1,57 @@ +namespace BossMod.Modules.Heavensward.DeepDungeon.PalaceOfTheDead.D120Kirtimukha; + +public enum OID : uint +{ + Boss = 0x1819, // R3.600, x1 + DeepPalaceHornet = 0x1905, // R0.400, x0 (spawn during fight) +} + +public enum AID : uint +{ + AutoAttack = 6499, // Boss->player, no cast, single-target + AutoAttackAdds = 6498, // DeepPalaceHornet->player, no cast, single-target + AcidMist = 7134, // Boss->self, 3.0s cast, range 6+R circle + BloodyCaress = 7133, // Boss->self, no cast, range 8+R 120-degree cone + FinalSting = 919, // DeepPalaceHornet->player, 3.0s cast, single-target + GoldDust = 7135, // Boss->location, 3.0s cast, range 8 circle + Leafstorm = 7136, // Boss->self, 3.0s cast, range 50 circle + RottenStench = 7137, // Boss->self, 3.0s cast, range 45+R width 12 rect +} + +class AcidMist(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AcidMist), new AOEShapeCircle(9.6f)); +class BossAdds(BossModule module) : Components.Adds(module, (uint)OID.DeepPalaceHornet) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = (OID)e.Actor.OID switch + { + OID.DeepPalaceHornet => 2, + OID.Boss => 1, + _ => 0 + }; + } +} +class BloodyCaress(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.BloodyCaress), new AOEShapeCone(11.6f, 60.Degrees()), activeWhileCasting: false); +class FinalSting(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.FinalSting), "Final sting is being cast! \nKill the add or take 98% of your hp!"); +class GoldDust(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.GoldDust), 8); +class Leafstorm(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Leafstorm)); +class RottenStench(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RottenStench), new AOEShapeRect(47.6f, 6)); + +class D120KirtimukhaStates : StateMachineBuilder +{ + public D120KirtimukhaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "LegendofIceman", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 210, NameID = 5384)] +public class D120Kirtimukha(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, -235), new ArenaBoundsCircle(24)); diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D130Alfard.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D130Alfard.cs new file mode 100644 index 0000000000..26a0ed7126 --- /dev/null +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D130Alfard.cs @@ -0,0 +1,64 @@ +using BossMod.Heavensward.Dungeon.D06AetherochemicalResearchFacility.D062Harmachis; + +namespace BossMod.Modules.Heavensward.DeepDungeon.PalaceOfTheDead.D130Alfard; + +public enum OID : uint +{ + Boss = 0x181A, // R4.800, x1 + FireVoidPuddle = 0x1E8D9B, // R0.500, x0 (spawn during fight), EventObj type + IceVoidPuddle = 0x1E8D9C, // R0.500, x0 (spawn during fight), EventObj type +} + +public enum AID : uint +{ + AutoAttack = 6501, // Boss->players, no cast, range 6+R ?-degree cone + BallOfFire = 7139, // Boss->location, no cast, range 6 circle + BallOfIce = 7140, // Boss->location, no cast, range 6 circle + Dissever = 7138, // Boss->self, no cast, range 6+R 90-degree cone + FearItself = 7141, // Boss->self, 2.0s cast, range 54+R circle +} + +class Dissever(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Dissever), new AOEShapeCone(10.8f, 45.Degrees()), activeWhileCasting: false); +class BallofFire(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.FireVoidPuddle).Where(z => z.EventState != 7)); +class BallofIce(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.IceVoidPuddle).Where(z => z.EventState != 7)); +class FearItself(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FearItself), new AOEShapeDonut(5, 50)); + +class Hints(BossModule module) : BossComponent(module) +{ + public int NumCasts { get; private set; } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.BallOfFire or AID.BallOfIce or AID.FearItself) + ++NumCasts; + + if (NumCasts >= 5) + { + NumCasts = 0; + } + } + + public override void AddGlobalHints(GlobalHints hints) + { + if (NumCasts < 4) + hints.Add($"Bait the boss away from the middle of the arena. \n{Module.PrimaryActor.Name} will cast x2 Fire Puddles & x2 Ice Puddles. \nAfter the 4th puddle is dropped, run to the middle."); + if (NumCasts >= 4) + hints.Add($"Run to the middle of the arena! \n{Module.PrimaryActor.Name} is about to cast a donut AOE!"); + } +} + +class D130AlfardStates : StateMachineBuilder +{ + public D130AlfardStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "LegendofIceman", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 211, NameID = 5397)] +public class D130Alfard(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, -235), new ArenaBoundsCircle(25)); diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D140AhPuch.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D140AhPuch.cs new file mode 100644 index 0000000000..1cee369dbb --- /dev/null +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D140AhPuch.cs @@ -0,0 +1,57 @@ +namespace BossMod.Modules.Heavensward.DeepDungeon.PalaceOfTheDead.D140AhPuch; + +public enum OID : uint +{ + Boss = 0x181B, // R3.800, x1 + DeepPalaceFollower = 0x1906, // R1.800, x0 (spawn during fight) + AccursedPoxVoidZone = 0x1E8EA9, // R0.500, x0 (spawn during fight), EventObj type +} + +public enum AID : uint +{ + AutoAttack = 6498, // Boss->player, no cast, single-target + AccursedPox = 7146, // Boss->location, 3.0s cast, range 8 circle + AncientEruption = 7142, // Boss->location, 2.5s cast, range 4 circle + Blizzard = 967, // DeepPalaceFollower->player, 1.0s cast, single-target + EntropicFlame = 7143, // Boss->self, 3.0s cast, range 50+R width 8 rect + Scream = 7145, // Boss->self, 3.0s cast, range 30 circle + ShadowFlare = 7144, // Boss->self, 3.0s cast, range 25+R circle +} + +class Adds(BossModule module) : Components.Adds(module, (uint)OID.DeepPalaceFollower) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = (OID)e.Actor.OID switch + { + OID.DeepPalaceFollower => 2, + OID.Boss => 1, + _ => 0 + }; + } +} +class AccursedPox(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AccursedPox), 8); +class AncientEruption(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AncientEruption), 4); +class AncientEruptionZone(BossModule module) : Components.PersistentInvertibleVoidzone(module, 4, m => m.Enemies(OID.AccursedPoxVoidZone).Where(z => z.EventState != 7)); +class EntropicFlame(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EntropicFlame), new AOEShapeRect(53.8f, 4)); +class Scream(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Scream), "Raidwide + Fear, Adds need to be dead by now"); +class ShadowFlare(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ShadowFlare)); + +class D140AhPuchStates : StateMachineBuilder +{ + public D140AhPuchStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "LegendofIceman", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 212, NameID = 5410)] +public class D140AhPuch(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, -237), new ArenaBoundsCircle(25)); diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D150Tisiphone.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D150Tisiphone.cs new file mode 100644 index 0000000000..f8b38828cb --- /dev/null +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D150Tisiphone.cs @@ -0,0 +1,74 @@ +namespace BossMod.Modules.Heavensward.DeepDungeon.PalaceOfTheDead.D150Tisiphone; + +public enum OID : uint +{ + Boss = 0x181C, // R2.000, x1 + FanaticGargoyle = 0x18EB, // R2.300, x0 (spawn during fight) + FanaticSuccubus = 0x18EE, // R1.000, x0 (spawn during fight) + FanaticVodoriga = 0x18EC, // R1.200, x0 (spawn during fight) + FanaticZombie = 0x18ED, // R0.500, x0 (spawn during fight) +} + +public enum AID : uint +{ + AutoAttack = 6497, // Boss/FanaticSuccubus->player, no cast, single-target + BloodRain = 7153, // Boss->location, 5.0s cast, range 100 circle + BloodSword = 7111, // Boss->FanaticSuccubus, no cast, single-target + DarkMist = 7108, // Boss->self, 3.0s cast, range 8+R circle + Desolation = 7112, // FanaticGargoyle->self, 4.0s cast, range 55+R(57.3) width 6 rect + FatalAllure = 7110, // Boss->FanaticSuccubus, 2.0s cast, single-target, sucks the HP that remained off the FanaticSuccubus and transfers it to boss + SummonDarkness = 7107, // Boss->self, no cast, single-target + SweetSteel = 7148, // FanaticSuccubus->self, no cast, range 6+R(7) 90?-degree cone, currently a safe bet on the cone angel, needs to be confirmed + TerrorEye = 7113, // FanaticVodoriga->location, 4.0s cast, range 6 circle + VoidAero = 7177, // Boss->self, 3.0s cast, range 40+R(42.3) width 8 rect + VoidFireII = 7150, // FanaticSuccubus->location, 3.0s cast, range 5 circle + VoidFireIV = 7109, // Boss->location, 3.5s cast, range 10 circle +} + +class BloodRain(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.BloodRain), "Heavy Raidwide damage! Also killing any add that is currently up"); +class BossAdds(BossModule module) : Components.AddsMulti(module, [(uint)OID.FanaticZombie, (uint)OID.FanaticSuccubus]); +class DarkMist(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DarkMist), new AOEShapeCircle(10)); +class Desolation(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Desolation), new AOEShapeRect(57.3f, 3)); +class FatalAllure(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.FatalAllure), "Boss is life stealing from the succubus"); +class SweetSteel(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SweetSteel), new AOEShapeCone(7, 45.Degrees())); +class TerrorEye(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.TerrorEye), 6); +class VoidFireII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.VoidFireII), 5); +class VoidFireIV(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.VoidFireIV), 10); +class ZombieGrab(BossModule module) : Components.PersistentVoidzone(module, 2, m => m.Enemies(OID.FanaticZombie)); // Future note to Ice(self): Not entirely sure if I'm happy with this per se? It shows to essentially stay away from the zombies but, maybe a better hint when I can think of one + +class EncounterHints(BossModule module) : BossComponent(module) +{ + public override void AddGlobalHints(GlobalHints hints) + { + hints.Add($"{Module.PrimaryActor.Name} will spawn 4 zombies, you can either kite them or kill them. The BloodRain raidwide will also kill them if they're still alive. \nThe boss will also life-steal however much HP is left of the Succubus, you're choice if you want to kill it or not."); + } +} + +class D150TisiphoneStates : StateMachineBuilder +{ + public D150TisiphoneStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .DeactivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "LegendofIceman", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 213, NameID = 5424)] +//public class D150Tisiphone(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, -237.17f), new ArenaBoundsCircle(24)); +public class D150Tisiphone : BossModule +{ + public D150Tisiphone(WorldState ws, Actor primary) : base(ws, primary, new(-300, -237.17f), new ArenaBoundsCircle(24)) + { + ActivateComponent(); + } +} diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D20Spurge.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D20Spurge.cs index f715f22ee2..b3ac72354c 100644 --- a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D20Spurge.cs +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D20Spurge.cs @@ -19,6 +19,19 @@ public enum AID : uint RottenStench = 6425, // Boss->self, 3.0s cast, range 45+R width 12 rect } +class BossAdds(BossModule module) : Components.Adds(module, (uint)OID.PalaceHornet) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = (OID)e.Actor.OID switch + { + OID.PalaceHornet => 2, + OID.Boss => 1, + _ => 0 + }; + } +} class AcidMist(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AcidMist), new AOEShapeCircle(9.6f)); class BloodyCaress(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.BloodyCaress), new AOEShapeCone(11.6f, 60.Degrees()), activeWhileCasting: false); class GoldDust(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.GoldDust), 8); @@ -30,6 +43,7 @@ class D20SpurgeStates : StateMachineBuilder public D20SpurgeStates(BossModule module) : base(module) { TrivialPhase() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D30Ningishzida.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D30Ningishzida.cs index 18393b767b..d79713361c 100644 --- a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D30Ningishzida.cs +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D30Ningishzida.cs @@ -3,8 +3,8 @@ namespace BossMod.Heavensward.DeepDungeon.PalaceoftheDead.D30Ningishzida; public enum OID : uint { Boss = 0x16AC, // R4.800, x1 - BallofFirePuddle = 0x1E8D9B, // R0.500, x0 (spawn during fight), EventObj type - BallofIcePuddle = 0x1E8D9C, // R0.500, x0 (spawn during fight), EventObj type + FireVoidPuddle = 0x1E8D9B, // R0.500, x0 (spawn during fight), EventObj type + IceVoidPuddle = 0x1E8D9C, // R0.500, x0 (spawn during fight), EventObj type } public enum AID : uint @@ -17,15 +17,31 @@ public enum AID : uint } class Dissever(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Dissever), new AOEShapeCone(10.8f, 45.Degrees()), activeWhileCasting: false); -class BallofFire(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.BallofFirePuddle).Where(z => z.EventState != 7)); -class BallofIce(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.BallofIcePuddle).Where(z => z.EventState != 7)); +class BallofFire(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.FireVoidPuddle).Where(z => z.EventState != 7)); +class BallofIce(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.IceVoidPuddle).Where(z => z.EventState != 7)); class FearItself(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FearItself), new AOEShapeDonut(5, 50)); class Hints(BossModule module) : BossComponent(module) { + public int NumCasts { get; private set; } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.BallOfFire or AID.BallOfIce or AID.FearItself) + ++NumCasts; + + if (NumCasts >= 5) + { + NumCasts = 0; + } + } + public override void AddGlobalHints(GlobalHints hints) { - hints.Add($" Bait the boss away from the middle of the arena. \n {Module.PrimaryActor.Name} will cast x2 Fire Puddles & x2 Ice Puddles, after the 4th puddle is dropped, run to the middle."); + if (NumCasts < 4) + hints.Add($"Bait the boss away from the middle of the arena. \n{Module.PrimaryActor.Name} will cast x2 Fire Puddles & x2 Ice Puddles. \nAfter the 4th puddle is dropped, run to the middle."); + if (NumCasts >= 4) + hints.Add($"Run to the middle of the arena! \n{Module.PrimaryActor.Name} is about to cast a donut AOE!"); } } diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D40Ixtab.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D40Ixtab.cs index 8d9cb95382..8a5370dbdd 100644 --- a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D40Ixtab.cs +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D40Ixtab.cs @@ -19,7 +19,19 @@ public enum AID : uint ShadowFlare = 6432, // Boss->self, 3.0s cast, range 25+R circle } -class Adds(BossModule module) : Components.Adds(module, (uint)OID.NightmareBhoot); +class Adds(BossModule module) : Components.Adds(module, (uint)OID.NightmareBhoot) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = (OID)e.Actor.OID switch + { + OID.NightmareBhoot => 2, + OID.Boss => 1, + _ => 0 + }; + } +} class AccursedPox(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AccursedPox), 8); class AncientEruption(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AncientEruption), 4); class AncientEruptionZone(BossModule module) : Components.PersistentInvertibleVoidzone(module, 4, m => m.Enemies(OID.AccursedPoxVoidZone).Where(z => z.EventState != 7)); diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D80Gudanna.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D80Gudanna.cs index 3f083137d0..42c8968652 100644 --- a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D80Gudanna.cs +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/D80Gudanna.cs @@ -10,7 +10,7 @@ public enum OID : uint public enum AID : uint { Attack = 6497, // Boss->player, no cast, single-target - Charybdis = 7096, // Boss->location, 3.0s cast, range 6 circle // Cast that sets the tornado location + Charybdis = 7096, // Boss->location, 3.0s cast, range 6 circle // Cast that sets the tornado location TornadoVoidzone = 7164, // 18F0->self, no cast, range 6 circle EclipticMeteor = 7099, // Boss->self, 20.0s cast, range 50 circle Maelstrom = 7167, // 18F0->self, 1.3s cast, range 10 circle @@ -24,11 +24,14 @@ class Maelstrom(BossModule module) : Components.PersistentVoidzone(module, 10, m class EclipticMeteor(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.EclipticMeteor), "Kill him before he kills you! 80% max HP damage incoming!"); class Thunderbolt(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Thunderbolt), new AOEShapeCone(16.6f, 60.Degrees())); +// things to do: +// add a hint counter to tell the player where the boss is going to cast Trounce next +// It's always East wall -> West wall -> East... ect class Hints(BossModule module) : BossComponent(module) { public override void AddGlobalHints(GlobalHints hints) { - hints.Add($"{Module.PrimaryActor.Name} will use Ecliptic Meteor.\nYou must either kill him before he cast it multiple times, or heal through it."); + hints.Add($"{Module.PrimaryActor.Name} will cast Trounce (Cone AOE) from the East and West wall. \nMake sure to stay near him to dodge the AOE. \n{Module.PrimaryActor.Name} will also cast Ecliptic Meteor at 10% HP, plan accordingly!"); } } diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs index 83a3723045..3aad5940e7 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Dungeon.D02DohnMheg.D031AencThon; +namespace BossMod.Shadowbringers.Dungeon.D02DohnMheg.D031AencThon; public enum OID : uint { @@ -106,7 +106,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } } -class Finale(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.Finale), $"Enrage, destroy the Liar's Lyre!", true); +class Finale(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.Finale), "Enrage, destroy the Liar's Lyre!", true); class CorrosiveBile(BossModule module) : Components.GenericAOEs(module) { diff --git a/BossMod/Modules/Stormblood/Ultimate/UWU/UWUEnums.cs b/BossMod/Modules/Stormblood/Ultimate/UWU/UWUEnums.cs index 3026ba5ba3..9312db4c73 100644 --- a/BossMod/Modules/Stormblood/Ultimate/UWU/UWUEnums.cs +++ b/BossMod/Modules/Stormblood/Ultimate/UWU/UWUEnums.cs @@ -43,6 +43,7 @@ public enum AID : uint Friction = 11080, // Garuda->players, 2.0s cast, range 5 circle SuperCyclone1 = 11079, // Helper->location, no cast, raidwide on thermal low 1 cleanse SuperCyclone2 = 11189, // Helper->location, no cast, raidwide on thermal low 2 cleanse + SuperCyclone3 = 11190, // Helper->location, no cast, deadly raidwide on thermal low 3 cleanse EyeOfTheStorm = 11090, // Helper->self, 3.0s cast, range 12-25 donut WickedWheel = 11086, // Garuda->self, 3.0s cast, range 7+R circle aoe WickedTornado = 11087, // Helper->self, no cast, range ?-20 donut @@ -53,7 +54,7 @@ public enum AID : uint Gigastorm = 11078, // SpinyPlume->self, 3.0s cast, range 6+R circle aoe on death AutoAttackIfrit = 11089, // Ifrit->player, no cast, single-target - //??? = 461, // Ifrit->self, no cast, single-target, visual ??? + SpawnNails = 461, // Ifrit->self, no cast, single-target, visual ??? CrimsonCyclone = 11103, // Ifrit->self, 3.0s cast, range 44+R width 18 rect aoe CrimsonCycloneCross = 11104, // Helper->self, no cast, range 44+R width 10 rect aoe RadiantPlumeAOE = 11105, // Helper->location, 4.0s cast, range 8 circle aoe diff --git a/BossMod/Modules/Stormblood/Ultimate/UWU/UWUStates.cs b/BossMod/Modules/Stormblood/Ultimate/UWU/UWUStates.cs index fb91543d64..25773275ec 100644 --- a/BossMod/Modules/Stormblood/Ultimate/UWU/UWUStates.cs +++ b/BossMod/Modules/Stormblood/Ultimate/UWU/UWUStates.cs @@ -418,9 +418,7 @@ private void Phase4LahabreaUltima(uint id) P4BeforeAnnihilation(id + 0x30000, 2.0f); P4Annihilation(id + 0x40000, 2.3f); P4BeforeSuppression(id + 0x50000, 0.7f); - - SimpleState(id + 0xFF0000, 100, "???") - .ActivateOnEnter(); + SimpleState(id + 0x60000, 3.1f, "Suppression start"); } private void P4Lahabrea(uint id, float delay) @@ -623,8 +621,19 @@ private void P4BeforeSuppression(uint id, float delay) ComponentCondition(id + 0x302, 2.0f, comp => comp.NumCasts > 0) .DeactivateOnExit(); - // TODO: homing lasers > eots + knockback > diffractive ? - //P4HomingLasers(id + 0x400, 10); + P4HomingLasers(id + 0x400, 2.8f); + + ComponentCondition(id + 0x500, 3.1f, comp => comp.Casters.Count > 0) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); // show hint early... + ComponentCondition(id + 0x501, 1.0f, comp => comp.NumCasts > 0, "Knockback") + .DeactivateOnExit(); + ComponentCondition(id + 0x502, 2.0f, comp => comp.NumCasts > 0) + .DeactivateOnExit(); + + ComponentCondition(id + 0x600, 1.5f, comp => comp.NumCasts > 0, "Tankbuster") + .DeactivateOnExit(); } private void P4HomingLasers(uint id, float delay) diff --git a/BossMod/Replay/Visualization/OpList.cs b/BossMod/Replay/Visualization/OpList.cs index 54a552afee..155869e580 100644 --- a/BossMod/Replay/Visualization/OpList.cs +++ b/BossMod/Replay/Visualization/OpList.cs @@ -109,7 +109,7 @@ private bool FilterOp(WorldState.Operation o) ActorState.OpHPMP => false, ActorState.OpTargetable op => FilterInterestingActor(op.InstanceID, op.Timestamp, false), ActorState.OpDead op => FilterInterestingActor(op.InstanceID, op.Timestamp, true), - ActorState.OpCombat => false, + ActorState.OpCombat op => FilterInterestingActor(op.InstanceID, op.Timestamp, false), ActorState.OpEventState op => FilterInterestingActor(op.InstanceID, op.Timestamp, false), ActorState.OpTarget op => FilterInterestingActor(op.InstanceID, op.Timestamp, false), ActorState.OpCastInfo op => FilterInterestingActor(op.InstanceID, op.Timestamp, false) && !_filteredActions.Contains(FindCast(replay.FindParticipant(op.InstanceID, op.Timestamp), op.Timestamp, op.Value != null)?.ID ?? new()), diff --git a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs index 346c4474d2..54729002f5 100644 --- a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs +++ b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs @@ -67,6 +67,8 @@ public override void Draw() MoveTo(_curTime + (curFrame - _prevFrame) * _playSpeed); _prevFrame = curFrame; + var resetPF = false; + DrawControlRow(); DrawTimelineRow(); ImGui.TextUnformatted($"Num loaded modules: {_mgr.LoadedModules.Count}, num active modules: {_mgr.LoadedModules.Count(m => m.StateMachine.ActiveState != null)}, active module: {_mgr.ActiveModule?.GetType()}"); @@ -96,6 +98,8 @@ public override void Draw() if (ImGui.CollapsingHeader("Plan execution")) { + resetPF |= UIRotationWindow.DrawRotationSelector(_rmm); + if (ImGui.Button("Timeline")) { _ = new StateMachineWindow(_mgr.ActiveModule); @@ -110,6 +114,7 @@ public override void Draw() { plans.SelectedIndex = newSel; _rotationDB.Plans.ModifyManifest(_mgr.ActiveModule.GetType(), _mgr.WorldState.Party.Player()?.Class ?? Class.None); + resetPF = true; } ImGui.SameLine(); @@ -146,7 +151,7 @@ public override void Draw() } } - DrawPartyTable(); + resetPF |= DrawPartyTable(); DrawEnemyTables(); DrawAllActorsTable(); DrawAI(); @@ -155,6 +160,9 @@ public override void Draw() _events.Draw(); if (ImGui.CollapsingHeader("Analysis")) _analysis.Draw(); + + if (resetPF) + ResetPF(); } private void DrawControlRow() @@ -287,11 +295,12 @@ private void DrawCommonColumns(Actor actor) } } - private void DrawPartyTable() + private bool DrawPartyTable() { if (!ImGui.CollapsingHeader("Party")) - return; + return false; + var resetPF = false; ImGui.BeginTable("party", 11, ImGuiTableFlags.Resizable); ImGui.TableSetupColumn("POV", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize, 25); ImGui.TableSetupColumn("Class", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize, 30); @@ -315,7 +324,7 @@ private void DrawPartyTable() if (ImGui.Checkbox("###POV", ref isPOV) && isPOV) { _povSlot = slot; - ResetPF(); + resetPF = true; } ImGui.TableNextColumn(); @@ -339,6 +348,7 @@ private void DrawPartyTable() ImGui.PopID(); } ImGui.EndTable(); + return resetPF; } private void DrawEnemyTables() diff --git a/BossMod/ThirdParty/Clipper2/Clipper.Engine.cs b/BossMod/ThirdParty/Clipper2/Clipper.Engine.cs index 0fc5de82bd..91a0d9920b 100644 --- a/BossMod/ThirdParty/Clipper2/Clipper.Engine.cs +++ b/BossMod/ThirdParty/Clipper2/Clipper.Engine.cs @@ -887,7 +887,7 @@ protected void AddReuseableData(ReuseableDataContainer64 reuseableData) } } - // VBM/BMR edit: a version of AddReusableData that forces polytype; useful if same data is to be reused as subject or clip + // BMR edit: a version of AddReusableData that forces polytype; useful if same data is to be reused as subject or clip [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void AddReuseableData(ReuseableDataContainer64 reuseableData, PathType typeOverride) { @@ -3163,7 +3163,7 @@ public class Clipper64 : ClipperBase base.AddReuseableData(reuseableData); } - // VBM/BMR edit: a version of AddReusableData that forces polytype; useful if same data is to be reused as subject or clip + // BMR edit: a version of AddReusableData that forces polytype; useful if same data is to be reused as subject or clip [MethodImpl(MethodImplOptions.AggressiveInlining)] public new void AddReuseableData(ReuseableDataContainer64 reuseableData, PathType typeOverride) { diff --git a/BossMod/Util/Bitmap.cs b/BossMod/Util/Bitmap.cs new file mode 100644 index 0000000000..a4a8b90435 --- /dev/null +++ b/BossMod/Util/Bitmap.cs @@ -0,0 +1,148 @@ +namespace BossMod; + +// utility for working with 2d 1bpp bitmaps +// for easier compatibility with .bmp format, this is stored row-by-row (without any swizzle) with stride rounded up to 32 pixels (ie packing bits into dwords and requiring rows to start on dword boundary) +public sealed class Bitmap +{ + public readonly int Width; + public readonly int Height; + public readonly int WordsPerRow; + public readonly uint[] Pixels; + + public int BytesPerRow => WordsPerRow << 2; + public int CoordToIndex(int x, int y) => y * WordsPerRow + (x >> 5); + public uint CoordToMask(int x) => 1u << (x & 31); + public ref uint WordAt(int x, int y) => ref Pixels[CoordToIndex(x, y)]; + + public bool this[int x, int y] + { + get => (WordAt(x, y) & CoordToMask(x)) != 0; + set + { + if (value) + WordAt(x, y) |= CoordToMask(x); + else + WordAt(x, y) &= ~CoordToMask(x); + } + } + + public Bitmap(int width, int height) + { + Width = width; + Height = height; + WordsPerRow = (width + 31) >> 5; + Pixels = new uint[height * WordsPerRow]; + } + + public Bitmap Clone() + { + var res = new Bitmap(Width, Height); + Array.Copy(Pixels, res.Pixels, Pixels.Length); + return res; + } + + // copy a rectangle from source bitmap into current bitmap + // any out-of-range accesses are ignored: actual copied region is intersection of source data, copy rect and dest rect + public void CopyRegion(Bitmap source, int sourceX, int sourceY, int sourceWidth, int sourceHeight, int destX, int destY) + { + var offX = destX - sourceX; + var offY = destY - sourceY; + ClampRect(ref sourceX, ref sourceY, ref sourceWidth, ref sourceHeight, 0, 0, source.Width, source.Height, 0, 0); + ClampRect(ref sourceX, ref sourceY, ref sourceWidth, ref sourceHeight, 0, 0, Width, Height, offX, offY); + if (sourceWidth <= 0) + return; // nothing to copy + + // note: this could be optimized if needed... + var maxX = sourceX + sourceWidth; + var maxY = sourceY + sourceHeight; + for (int y = sourceY; y < maxY; ++y) + { + for (int x = sourceX; x < maxX; ++x) + { + this[x + offX, y + offY] = source[x, y]; + } + } + } + + // upsample a region from source bitmap into current bitmap: each source pixel is converted into 2x2 destination pixels of same value + // note: if dest x/y are odd, they are rounded down + public void UpsampleRegion(Bitmap source, int sourceX, int sourceY, int sourceWidth, int sourceHeight, int destX, int destY) + { + // work in lower-resolution coordinates + destX >>= 1; + destY >>= 1; + var offX = destX - sourceX; + var offY = destY - sourceY; + ClampRect(ref sourceX, ref sourceY, ref sourceWidth, ref sourceHeight, 0, 0, source.Width, source.Height, 0, 0); + ClampRect(ref sourceX, ref sourceY, ref sourceWidth, ref sourceHeight, 0, 0, Width >> 1, Height >> 1, offX, offY); + + if (sourceWidth <= 0) + return; // nothing to copy + + // note: this could be optimized if needed... + var maxX = sourceX + sourceWidth; + var maxY = sourceY + sourceHeight; + for (int y = sourceY; y < maxY; ++y) + { + for (int x = sourceX; x < maxX; ++x) + { + this[(x + offX) << 1, (y + offY) << 1] = this[((x + offX) << 1) + 1, (y + offY) << 1] = this[(x + offX) << 1, ((y + offY) << 1) + 1] = this[((x + offX) << 1) + 1, ((y + offY) << 1) + 1] = source[x, y]; + } + } + } + + // downsample a region from source bitmap into current bitmap; each 2x2 group of source pixels are copied into destination pixel if equal, otherwise destination is set to fallback value + // note: if source x/y/w/h are odd, they are rounded down + public void DownsampleRegion(Bitmap source, int sourceX, int sourceY, int sourceWidth, int sourceHeight, int destX, int destY, bool fallback) + { + // work in lower-resolution coordinates + sourceX >>= 1; + sourceY >>= 1; + sourceWidth >>= 1; + sourceHeight >>= 1; + var offX = destX - sourceX; + var offY = destY - sourceY; + ClampRect(ref sourceX, ref sourceY, ref sourceWidth, ref sourceHeight, 0, 0, source.Width >> 1, source.Height >> 1, 0, 0); + ClampRect(ref sourceX, ref sourceY, ref sourceWidth, ref sourceHeight, 0, 0, Width, Height, offX, offY); + + if (sourceWidth <= 0) + return; // nothing to copy + + // note: this could be optimized if needed... + var maxX = sourceX + sourceWidth; + var maxY = sourceY + sourceHeight; + for (int y = sourceY; y < maxY; ++y) + { + for (int x = sourceX; x < maxX; ++x) + { + var v = source[x << 1, y << 1]; + if (v != source[(x << 1) + 1, y << 1] || v != source[x << 1, (y << 1) + 1] || v != source[(x << 1) + 1, (y << 1) + 1]) + v = fallback; + this[x, y] = v; + } + } + } + + // modify rect (x,y,w,h) so that, if offset by (dx,dy), it is clamped to rect (cx,cy,cw,ch) + private static void ClampRect(ref int x, ref int y, ref int w, ref int h, int cx, int cy, int cw, int ch, int dx, int dy) + { + var offX = x + dx - cx; // offset of left border from clip rect + if (offX < 0) + { + x -= offX; + w += offX; + } + var offY = y + dy - cy; + if (offY < 0) + { + y -= offY; + h += offY; + } + var maxW = cx + cw - (x + dx); // maximal width that rect can have so that when offset it doesn't extend past right border + if (w > maxW) + w = maxW; + var maxH = cy + ch - (y + dy); + if (h > maxH) + h = maxH; + } +} diff --git a/BossMod/Util/UIBitmapEditor.cs b/BossMod/Util/UIBitmapEditor.cs new file mode 100644 index 0000000000..293991a6d9 --- /dev/null +++ b/BossMod/Util/UIBitmapEditor.cs @@ -0,0 +1,335 @@ +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility.Numerics; +using ImGuiNET; + +namespace BossMod; + +// a 'simple' bitmap editor utility +public class UIBitmapEditor +{ + private readonly List _bitmaps; // undo-redo stack; this is not terribly efficient, but oh well + private int _curUndoPos; + private bool _paintInProgress; + + private readonly List _modeNames = []; + + public readonly int PanModeId; + public readonly int BrushModeId; + public readonly int EraseModeId; + + public Vector2 ScreenSize = new(600, 600); // size of the 'viewport' in screen space + public Vector2 ScreenOffset; // top-level coordinate of the virtual 'viewport' in screen space coordinates (scaled by zoom level compared to bitmap coordinates) + public int ZoomLevel = 4; // 0 is 1:1 screen to bitmap, positive if bitmap pixel is bigger than screen pixel (upscaled bitmap), negative otherwise + public int CurrentMode; + public float BrushRadius = 1; + + public Bitmap Bitmap => _bitmaps[_curUndoPos]; + + public UIBitmapEditor(Bitmap initial) + { + _bitmaps = [initial.Clone()]; + PanModeId = RegisterMode("Pan"); + BrushModeId = RegisterMode("Brush"); + EraseModeId = RegisterMode("Erase"); + CurrentMode = PanModeId; + } + + public void Draw() + { + using var table = ImRaii.Table("table", 2); + if (!table) + return; + + ImGui.TableSetupColumn("Map", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoClip, ScreenSize.X); + ImGui.TableSetupColumn("Control"); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + DrawGrid(); + ImGui.TableNextColumn(); + DrawSidebar(); + } + + public bool CanUndo() => _curUndoPos > 0; + public bool CanRedo() => _curUndoPos < _bitmaps.Count - 1; + public void Undo() => _curUndoPos = Math.Max(0, _curUndoPos - 1); + public void Redo() => _curUndoPos = Math.Min(_bitmaps.Count - 1, _curUndoPos + 1); + public void Checkpoint() => CheckpointImpl(_bitmaps[_curUndoPos].Clone()); + + protected int RegisterMode(string name) + { + _modeNames.Add(name); + return _modeNames.Count; + } + + protected virtual void DrawSidebar() + { + DrawModeButtons(); + DrawUndoRedoButtons(); + + // debug stuff + ImGui.TextUnformatted($"Size: {Bitmap.Width}x{Bitmap.Height}, Brush radius: {BrushRadius}"); + + if (ImGui.Button("+1 on left")) + { + var newBitmap = new Bitmap(Bitmap.Width + 1, Bitmap.Height); + newBitmap.CopyRegion(Bitmap, 0, 0, Bitmap.Width, Bitmap.Height, 1, 0); + CheckpointImpl(newBitmap); + } + + if (ImGui.Button("upscale")) + { + var newBitmap = new Bitmap(Bitmap.Width * 2, Bitmap.Height * 2); + newBitmap.UpsampleRegion(Bitmap, 0, 0, Bitmap.Width, Bitmap.Height, 0, 0); + CheckpointImpl(newBitmap); + } + + if (ImGui.Button("downscale")) + { + var newBitmap = new Bitmap(Bitmap.Width / 2, Bitmap.Height / 2); + newBitmap.DownsampleRegion(Bitmap, 0, 0, Bitmap.Width, Bitmap.Height, 0, 0, true); + CheckpointImpl(newBitmap); + } + } + + protected void DrawModeButtons() + { + for (int i = 0; i < _modeNames.Count; ++i) + { + var active = CurrentMode == i + 1; + using var color = ImRaii.PushColor(ImGuiCol.Button, 0xff008080, active); + if (ImGui.Button(_modeNames[i])) + CurrentMode = active ? 0 : i + 1; + ImGui.SameLine(); + } + ImGui.NewLine(); + } + + protected void DrawUndoRedoButtons() + { + using (ImRaii.Disabled(!CanUndo())) + if (ImGui.Button("Undo")) + Undo(); + ImGui.SameLine(); + using (ImRaii.Disabled(!CanRedo())) + if (ImGui.Button("Redo")) + Redo(); + } + + private void DrawGrid() + { + var tl = ImGui.GetCursorScreenPos(); + var br = tl + ScreenSize; + var dl = ImGui.GetWindowDrawList(); + var mouseOffset = ImGui.GetIO().MousePos - tl; + + ImGui.InvisibleButton("###grid", ScreenSize); + if (ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left, 0)) + HandleDrag(mouseOffset, ImGui.GetIO().MouseDelta); + else + _paintInProgress = false; + + if (ImGui.IsItemHovered() && ImGui.GetIO().MouseWheel is var wheel && wheel != 0) + HandleWheel(wheel, mouseOffset); + + ImGui.PushClipRect(tl, br, false); + + var bitmapToScreenScale = MathF.Pow(2, ZoomLevel); + var screenToBitmapScale = 1.0f / bitmapToScreenScale; + var bitmapTL = ScreenOffset * screenToBitmapScale; + var bitmapBR = (ScreenOffset + ScreenSize) * screenToBitmapScale; + var x0 = Math.Max(0, (int)bitmapTL.X); + var y0 = Math.Max(0, (int)bitmapTL.Y); + var x1 = Math.Min(Bitmap.Width, (int)MathF.Ceiling(bitmapBR.X) + 1); + var y1 = Math.Min(Bitmap.Height, (int)MathF.Ceiling(bitmapBR.Y) + 1); + var numBitmapPixelsPerScreenPixel = Math.Max(1, (int)screenToBitmapScale); + var numScreenPixelsPerBitmapPixel = Math.Max(1, (int)bitmapToScreenScale); + var screenX0 = x0 * bitmapToScreenScale - ScreenOffset.X; + var screenY0 = y0 * bitmapToScreenScale - ScreenOffset.Y; + var pixelWeight = 1.0f / (numBitmapPixelsPerScreenPixel * numBitmapPixelsPerScreenPixel); + + for (int y = y0; y < y1; y += numBitmapPixelsPerScreenPixel) + { + var corner = tl + new Vector2(screenX0, screenY0); + for (int x = x0; x < x1; x += numBitmapPixelsPerScreenPixel) + { + var cornerEnd = corner + new Vector2(numScreenPixelsPerBitmapPixel); + var cellTL = Vector2.Max(tl, corner); + var cellBR = Vector2.Min(br, cornerEnd); + if (cellTL.X < cellBR.X && cellTL.Y < cellBR.Y) + { + float opacity = 0; + var subXMax = Math.Min(x1, x + numBitmapPixelsPerScreenPixel); + var subYMax = Math.Min(y1, y + numBitmapPixelsPerScreenPixel); + for (int sy = y; sy < subYMax; ++sy) + for (int sx = x; sx < subXMax; ++sx) + if (Bitmap[sx, sy]) + opacity += pixelWeight; + var color = new Vector4(1, 0.5f, 0, 1) * opacity; + dl.AddRectFilled(cellTL, cellBR, Color.FromFloat4(color).ABGR); + } + corner.X += numScreenPixelsPerBitmapPixel; + } + screenY0 += numScreenPixelsPerBitmapPixel; + } + + void drawLine(Vector2 a, Vector2 b, uint color, int thickness) + { + if (a.X < tl.X && b.X < tl.X || a.Y < tl.Y && b.Y < tl.Y || a.X > br.X && b.X > br.X || a.Y > br.Y && b.Y > br.Y) + return; + a.X = Math.Clamp(a.X, tl.X, br.X); + a.Y = Math.Clamp(a.Y, tl.Y, br.Y); + b.X = Math.Clamp(b.X, tl.X, br.X); + b.Y = Math.Clamp(b.Y, tl.Y, br.Y); + dl.AddLine(a, b, color, thickness); + } + + // border + var borderA = tl - ScreenOffset; + var borderB = new Vector2(borderA.X + bitmapToScreenScale * Bitmap.Width, borderA.Y); + var borderC = new Vector2(borderA.X + bitmapToScreenScale * Bitmap.Width, borderA.Y + bitmapToScreenScale * Bitmap.Height); + var borderD = new Vector2(borderA.X, borderA.Y + bitmapToScreenScale * Bitmap.Height); + drawLine(borderA, borderB, 0xffffffff, 2); + drawLine(borderB, borderC, 0xffffffff, 2); + drawLine(borderC, borderD, 0xffffffff, 2); + drawLine(borderD, borderA, 0xffffffff, 2); + + // grid + if (ZoomLevel > 1) + { + for (int x = x0 + 1; x < x1; ++x) + { + var off = new Vector2(x * numScreenPixelsPerBitmapPixel, 0); + drawLine(borderA + off, borderD + off, 0xffffffff, 1); + } + for (int y = y0 + 1; y < y1; ++y) + { + var off = new Vector2(0, y * numScreenPixelsPerBitmapPixel); + drawLine(borderA + off, borderB + off, 0xffffffff, 1); + } + } + + // brush + if ((CurrentMode == BrushModeId || CurrentMode == EraseModeId) && ImGui.IsItemHovered()) + { + dl.AddCircle(tl + mouseOffset, BrushRadius * bitmapToScreenScale, 0xffff00ff); + } + + ImGui.PopClipRect(); + } + + private void HandleDrag(Vector2 end, Vector2 delta) + { + if (CurrentMode == PanModeId) + { + ScreenOffset -= delta; + } + else if (CurrentMode == BrushModeId || CurrentMode == EraseModeId) + { + if (!_paintInProgress) + { + Checkpoint(); + _paintInProgress = true; + } + var scale = MathF.Pow(0.5f, ZoomLevel); + var virtualEnd = ScreenOffset + end + new Vector2(0.5f); // assume we're starting at clicked pixel center + var p1 = (virtualEnd - delta) * scale; + var p2 = virtualEnd * scale; + var d = Vector2.Normalize(delta); + var n = new Vector2(d.Y, -d.X); + var brushHalfWidth = n * BrushRadius; + var brushA = p1 + brushHalfWidth; + var brushB = p2 + brushHalfWidth; + var brushC = p2 - brushHalfWidth; + var brushD = p2 - brushHalfWidth; + // SAT test for rect/rect portion: + // for X/Y axes, each pixel's projection is (x, x+1) / (y, y+1) + // for D/N axes, each pixel's projection is (x,y) dot axis + (0,0,1,1) projection + // brush projection is obviously constant + var brushProjX = ProjectRectOnAxis(new(1, 0), brushA, brushB, brushC, brushD); + var brushProjY = ProjectRectOnAxis(new(0, 1), brushA, brushB, brushC, brushD); + var brushProjD = ProjectRectOnAxis(d, brushA, brushB, brushC, brushD); + var brushProjN = ProjectRectOnAxis(n, brushA, brushB, brushC, brushD); + var unitProjD = ProjectRectOnAxis(d, new(0, 0), new(0, 1), new(1, 0), new(1, 1)); + var unitProjN = ProjectRectOnAxis(n, new(0, 0), new(0, 1), new(1, 0), new(1, 1)); + + // check whether brush rect intersects pixel (x to x+1), (y to y+1) (capsule excluding caps) + bool intersectBrushRectPixel(int x, int y) + { + if (RangesDisjoint(brushProjX, (x, x + 1)) || RangesDisjoint(brushProjY, (y, y + 1))) + return false; // either X or Y is a separating axis + var offD = Vector2.Dot(d, new(x, y)); + if (RangesDisjoint(brushProjD, (unitProjD.min + offD, unitProjD.max + offD))) + return false; // d is a separating axis + var offN = Vector2.Dot(n, new(x, y)); + if (RangesDisjoint(brushProjN, (unitProjN.min + offN, unitProjN.max + offN))) + return false; // n is a separating axis + return true; // no separating axis found + } + + var x0 = Math.Max(0, (int)(Math.Min(p1.X, p2.X) - BrushRadius)); + var x1 = Math.Min(Bitmap.Width, (int)(Math.Max(p1.X, p2.X) + BrushRadius) + 1); + var y0 = Math.Max(0, (int)(Math.Min(p1.Y, p2.Y) - BrushRadius)); + var y1 = Math.Min(Bitmap.Height, (int)(Math.Max(p1.Y, p2.Y) + BrushRadius) + 1); + var value = CurrentMode == BrushModeId; + for (int y = y0; y < y1; ++y) + for (int x = x0; x < x1; ++x) + if (intersectBrushRectPixel(x, y) || IntersectCirclePixel(p1, BrushRadius, x, y) || IntersectCirclePixel(p2, BrushRadius, x, y)) + Bitmap[x, y] = value; + } + } + + private void HandleWheel(float delta, Vector2 offset) + { + if (ImGui.IsKeyDown(ImGuiKey.ModShift)) + { + BrushRadius *= MathF.Pow(1.5f, delta); + } + else + { + // zoom, keeping pixel under cursor in place + var pivot = (ScreenOffset + offset) * MathF.Pow(0.5f, ZoomLevel); + ZoomLevel += (int)delta; + ScreenOffset = pivot * MathF.Pow(2, ZoomLevel) - offset; + ScreenOffset.X = MathF.Round(ScreenOffset.X); + ScreenOffset.Y = MathF.Round(ScreenOffset.Y); + } + } + + // note: assumes newState has no other references + private void CheckpointImpl(Bitmap newState) + { + if (_curUndoPos < _bitmaps.Count - 1) + _bitmaps.RemoveRange(_curUndoPos + 1, _bitmaps.Count - _curUndoPos - 1); + _bitmaps.Add(newState); + ++_curUndoPos; + } + + private static bool IntersectCirclePixel(Vector2 center, float radius, int x, int y) + { + center.X -= x + 0.5f; + center.Y -= y + 0.5f; + center = Vector2.Abs(center); + // at this point, center is relative to pixel center, plus we've mirrored it to always be in positive quadrant + center.X -= 0.5f; + center.Y -= 0.5f; + // now center is relative to corner + return center.X <= 0 ? center.Y <= radius // closest point is on +Y border or center is inside + : center.Y <= 0 ? center.X <= radius // closest point is on +X border + : center.X * center.X + center.Y * center.Y <= radius * radius; // closest point is corner + } + + // note: scaled by dir's length, doesn't matter for SAT test + private static (float min, float max) ProjectRectOnAxis(Vector2 dir, Vector2 a, Vector2 b, Vector2 c, Vector2 d) + { + var pa = Vector2.Dot(dir, a); + var pb = Vector2.Dot(dir, b); + var pc = Vector2.Dot(dir, c); + var pd = Vector2.Dot(dir, d); + var min = Math.Min(Math.Min(pa, pb), Math.Min(pc, pd)); + var max = Math.Max(Math.Max(pa, pb), Math.Max(pc, pd)); + return (min, max); + } + + private static bool RangesDisjoint((float min, float max) a, (float min, float max) b) => a.max < b.min || a.min > b.max; +} diff --git a/TODO b/TODO index a9801d1602..d409fd0348 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,6 @@ immediate plans +- nechuciho - seen incorrect rotations... - merge prs! -- auto control -- goal priority for aoes - collisions for pathfinding - brd rotation @@ -35,6 +34,7 @@ general: --- target is not a priority, but damaging it is a nice bonus (lower priority than max target) -- use that for targeting utils in aihints - debug utility to play action animations, spawn actors, play vfx, etc... +- encounter hints (to show before pull) boss modules: - timers diff --git a/UIDev/BitmapEditorTest.cs b/UIDev/BitmapEditorTest.cs new file mode 100644 index 0000000000..bf46dd65f3 --- /dev/null +++ b/UIDev/BitmapEditorTest.cs @@ -0,0 +1,23 @@ +using BossMod; +using ImGuiNET; + +namespace UIDev; + +class BitmapEditorTest : TestWindow +{ + private readonly UIBitmapEditor _editor; + + public BitmapEditorTest() : base("Bitmap editor test", new(1500, 1500), ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) + { + var init = new Bitmap(16, 16); + for (int y = 0; y < init.Height; ++y) + for (int x = 0; x < init.Width; ++x) + init[x, y] = (x >> 1) == 1 || (y >> 1) == 1; + _editor = new(init); + } + + public override void Draw() + { + _editor.Draw(); + } +}