Skip to content

Commit

Permalink
Merge pull request #445 from xanunderscore/ai-interact
Browse files Browse the repository at this point in the history
"One Life, One World" solo duty
  • Loading branch information
awgil authored Aug 26, 2024
2 parents 0a4d397 + 7ddb8db commit e08749a
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 0 deletions.
3 changes: 3 additions & 0 deletions BossMod/AI/AIBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ public void Execute(Actor player, Actor master)
// returns null if we're to be idle, otherwise target to attack
private Targeting SelectPrimaryTarget(Actor player, Actor master)
{
if (autorot.Hints.InteractWithTarget is Actor interact)
return new Targeting(new AIHints.Enemy(interact, false), 3);

// we prefer not to switch targets unnecessarily, so start with current target - it could've been selected manually or by AI on previous frames
// if current target is not among valid targets, clear it - this opens way for future target selection heuristics
var targetId = autorot.Hints.ForcedTarget?.InstanceID ?? player.TargetID;
Expand Down
18 changes: 18 additions & 0 deletions BossMod/AI/AIController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Game.Control;

namespace BossMod.AI;

Expand All @@ -17,6 +19,7 @@ sealed class AIController(ActionManagerEx amex, MovementOverride movement)

private readonly ActionManagerEx _amex = amex;
private readonly MovementOverride _movement = movement;
private DateTime _nextInteract;
private DateTime _nextJump;

public bool InCutscene => Service.Condition[ConditionFlag.OccupiedInCutSceneEvent] || Service.Condition[ConditionFlag.WatchingCutscene78] || Service.Condition[ConditionFlag.Occupied33] || Service.Condition[ConditionFlag.BetweenAreas] || Service.Condition[ConditionFlag.OccupiedInQuestEvent];
Expand Down Expand Up @@ -73,6 +76,21 @@ public void Update(Actor? player, AIHints hints)

if (hints.ForcedMovement == null && desiredPosition != null)
hints.ForcedMovement = desiredPosition.Value - player.PosRot.XYZ();

if (hints.InteractWithTarget is Actor tar && Service.TargetManager.Target is IGameObject obj && obj.EntityId == tar.InstanceID && player.DistanceToHitbox(tar) <= 3)
ExecuteInteract(obj);
}

private unsafe void ExecuteInteract(IGameObject obj)
{
if (_amex.EffectiveAnimationLock > 0)
return;

if (DateTime.Now >= _nextInteract)
{
TargetSystem.Instance()->OpenObjectInteraction((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address);
_nextInteract = DateTime.Now.AddMilliseconds(100);
}
}

private unsafe void ExecuteJump()
Expand Down
4 changes: 4 additions & 0 deletions BossMod/BossModule/AIHints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public class Enemy(Actor actor, bool shouldBeTanked)
// low-level forced movement - if set, character will move in specified direction (ignoring casts, uptime, forbidden zones, etc), or stay in place if set to default
public Vector3? ForcedMovement;

// indicates to AI mode that it should try to interact with some object
public Actor? InteractWithTarget;

// positioning: list of shapes that are either forbidden to stand in now or will be in near future
// 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<WPos, float> shapeDistance, DateTime activation)> ForbiddenZones = [];
Expand Down Expand Up @@ -72,6 +75,7 @@ public void Clear()
PotentialTargets.Clear();
ForcedTarget = null;
ForcedMovement = null;
InteractWithTarget = null;
ForbiddenZones.Clear();
RecommendedPositional = default;
RecommendedRangeToTarget = 0;
Expand Down
124 changes: 124 additions & 0 deletions BossMod/Modules/Heavensward/Quest/OneLifeOneWorld.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
namespace BossMod.Heavensward.Quest.OneLifeForOneWorld;

public enum OID : uint
{
Boss = 0x17CD,
KnightOfDarkness = 0x17CE, // R0.500, x1
BladeOfLight = 0x1EA19E,
}

public enum AID : uint
{
Overpower = 6683, // Boss->self, 2.5s cast, range 6+R 90-degree cone
UnlitCyclone = 6684, // Boss->self, 4.0s cast, range 5+R circle
UnlitCycloneAdds = 6685, // 18D6->location, 4.0s cast, range 9 circle
Skydrive = 6686, // Boss->player, 5.0s cast, single-target
UtterDestruction = 6690, // _Gen_FirstWard->self, 3.0s cast, range 20+R circle
RollingBladeCircle = 6691, // Boss->self, 3.0s cast, range 7 circle
RollingBladeCone = 6692, // _Gen_FirstWard->self, 3.0s cast, range 60+R 30-degree cone
}

public enum SID : uint
{
Invincibility = 325, // _Gen_KnightOfDarkness->Boss/_Gen_FirstWard, extra=0x0
}

class Overpower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(7, 45.Degrees()));
class UnlitCyclone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.UnlitCyclone), new AOEShapeCircle(6));
class UnlitCycloneAdds(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.UnlitCycloneAdds), 9);

class Skydrive(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(5), 23, ActionID.MakeSpell(AID.Skydrive), centerAtTarget: true);
class SkydrivePuddle(BossModule module) : Components.PersistentVoidzone(module, 5, m => m.Enemies(0x1EA19C).Where(x => x.EventState != 7));
class RollingBlade(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RollingBladeCircle), new AOEShapeCircle(7));
class RollingBladeCone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RollingBladeCone), new AOEShapeCone(60, 15.Degrees()));

class BladeOfLight(BossModule module) : BossComponent(module)
{
public Actor? Blade => WorldState.Actors.FirstOrDefault(x => x.OID == 0x1EA19E && x.IsTargetable);

public override void DrawArenaForeground(int pcSlot, Actor pc)
{
if (Blade != null)
Arena.Actor(Blade, ArenaColor.Vulnerable);
}
}

class Adds(BossModule module) : Components.AddsMulti(module, [0x17CE, 0x17CF, 0x17D0, 0x17D1]);
class TargetPriorityHandler(BossModule module) : BossComponent(module)
{
private Actor? Knight => Module.Enemies(OID.KnightOfDarkness).FirstOrDefault();
private Actor? Covered => WorldState.Actors.FirstOrDefault(s => s.FindStatus(SID.Invincibility) != null);
private Actor? BladeOfLight => WorldState.Actors.FirstOrDefault(s => (OID)s.OID == OID.BladeOfLight && s.IsTargetable);

public override void DrawArenaBackground(int pcSlot, Actor pc)
{
if (Knight != null && Covered != null)
Arena.AddLine(Knight.Position, Covered.Position, ArenaColor.Danger, 1);
}

public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
if (BladeOfLight != null)
{
var playerIsAttacked = false;

foreach (var e in hints.PotentialTargets)
{
if (e.Actor.TargetID == actor.InstanceID)
{
playerIsAttacked = true;
e.Priority = 0;
}
else
{
e.Priority = -1;
}
}

if (!playerIsAttacked)
{
if (actor.DistanceToHitbox(BladeOfLight) > 5.5f)
hints.ForcedMovement = (BladeOfLight!.Position - actor.Position).ToVec3();
else
hints.InteractWithTarget = BladeOfLight;
}
}
else
{
foreach (var e in hints.PotentialTargets)
{
if (e.Actor == Knight)
e.Priority = 2;
else if (e.Actor == Covered)
e.Priority = 0;
else
e.Priority = 1;
}
}
}
}

class UtterDestruction(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.UtterDestruction), new AOEShapeDonut(10, 20));

class WarriorOfDarknessStates : StateMachineBuilder
{
public WarriorOfDarknessStates(BossModule module) : base(module)
{
TrivialPhase()
.ActivateOnEnter<Overpower>()
.ActivateOnEnter<Adds>()
.ActivateOnEnter<TargetPriorityHandler>()
.ActivateOnEnter<UnlitCyclone>()
.ActivateOnEnter<UnlitCycloneAdds>()
.ActivateOnEnter<Skydrive>()
.ActivateOnEnter<SkydrivePuddle>()
.ActivateOnEnter<BladeOfLight>()
.ActivateOnEnter<RollingBlade>()
.ActivateOnEnter<RollingBladeCone>()
.ActivateOnEnter<UtterDestruction>()
;
}
}

[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 194, NameID = 5240)]
public class WarriorOfDarkness(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsCircle(20));

0 comments on commit e08749a

Please sign in to comment.