using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using FFXIVClassic_Map_Server.Actors; using FFXIVClassic.Common; using FFXIVClassic_Map_Server.actors.chara.ai; using FFXIVClassic_Map_Server.actors.chara.ai.controllers; using FFXIVClassic_Map_Server.actors.group; using FFXIVClassic_Map_Server.packets.send.actor; // port of dsp's ai code https://github.com/DarkstarProject/darkstar/blob/master/src/map/ai/ namespace FFXIVClassic_Map_Server.actors.chara.ai { // https://github.com/Windower/POLUtils/blob/master/PlayOnline.FFXI/Enums.cs [Flags] public enum ValidTarget : ushort { None = 0x00, Self = 0x01, Player = 0x02, PartyMember = 0x04, Ally = 0x08, NPC = 0x10, Enemy = 0x20, Unknown = 0x40, Object = 0x60, CorpseOnly = 0x80, Corpse = 0x9D // CorpseOnly + NPC + Ally + Partymember + Self } /// Targeting from/to different entity types enum TargetFindCharacterType : byte { None, /// Player can target all s in party PlayerToPlayer, /// Player can target all s (excluding player owned s) PlayerToBattleNpc, /// BattleNpc can target other s BattleNpcToBattleNpc, /// BattleNpc can target s and their s BattleNpcToPlayer, } /// Type of AOE region to create enum TargetFindAOEType : byte { None, /// Really a cylinder, uses maxDistance parameter in SetAOEType Circle, /// Create a cone with param in radians Cone, /// Box using self/target coords and Box } /// Set AOE around self or target enum TargetFindAOETarget : byte { /// Set AOE's origin at target's position Target, /// Set AOE's origin to own position. Self } /// Target finding helper class class TargetFind { private Character owner; private Character masterTarget; // if target is a pet, this is the owner private TargetFindCharacterType findType; private ValidTarget validTarget; private TargetFindAOETarget aoeTarget; private TargetFindAOEType aoeType; private Vector3 targetPosition; private float maxDistance; private float param; private List targets; public TargetFind(Character owner) { this.owner = owner; Reset(); } public void Reset() { this.findType = TargetFindCharacterType.None; this.validTarget = ValidTarget.Enemy; this.aoeType = TargetFindAOEType.None; this.aoeTarget = TargetFindAOETarget.Target; this.targetPosition = null; this.maxDistance = 0.0f; this.param = 0.0f; this.targets = new List(); } public List GetTargets() where T : Character { return new List(targets.OfType()); } public List GetTargets() { return targets; } /// /// Call this before /// /// /// - radius of circle /// - height of cone /// - width of box / 2 (todo: set box length not just between user and target) /// /// param in degrees of cone (todo: probably use radians and forget converting at runtime) public void SetAOEType(ValidTarget validTarget, TargetFindAOEType aoeType, TargetFindAOETarget aoeTarget, float maxDistance = -1.0f, float param = -1.0f) { this.validTarget = validTarget; this.aoeType = aoeType; this.maxDistance = maxDistance != -1.0f ? maxDistance : 0.0f; this.param = param != -1.0f ? param : 0.0f; } /// /// Call this to prepare Box AOE /// /// /// /// /// public void SetAOEBox(ValidTarget validTarget, TargetFindAOETarget aoeTarget, float length, float width) { this.validTarget = validTarget; this.aoeType = TargetFindAOEType.Box; this.aoeTarget = aoeTarget; float x = owner.positionX - (float)Math.Cos(owner.rotation + (float)(Math.PI / 2)) * (length); float z = owner.positionZ + (float)Math.Sin(owner.rotation + (float)(Math.PI / 2)) * (length); this.targetPosition = new Vector3(x, owner.positionY, z); this.maxDistance = width; } /// /// Find and try to add a single target to target list /// public void FindTarget(Character target, ValidTarget flags) { validTarget = flags; // todo: maybe this should only be set if successfully added? AddTarget(target, false); } /// /// Call SetAOEType before calling this /// Find targets within area set by /// public void FindWithinArea(Character target, ValidTarget flags, TargetFindAOETarget aoeTarget) { targets.Clear(); validTarget = flags; // are we creating aoe circles around target or self if (aoeTarget == TargetFindAOETarget.Self) this.targetPosition = owner.GetPosAsVector3(); else this.targetPosition = target.GetPosAsVector3(); masterTarget = TryGetMasterTarget(target) ?? target; // todo: this is stupid bool withPet = (flags & ValidTarget.Ally) != 0 || masterTarget.allegiance != owner.allegiance; if (masterTarget != null) targets.Add(masterTarget); if (aoeType != TargetFindAOEType.None) { if (IsPlayer(owner)) { if (masterTarget is Player) { findType = TargetFindCharacterType.PlayerToPlayer; if (masterTarget.currentParty != null) { if ((validTarget & (ValidTarget.Ally | ValidTarget.PartyMember)) != 0) AddAllInAlliance(masterTarget, withPet); else AddAllInParty(masterTarget, withPet); } else { AddTarget(masterTarget, withPet); } } else { findType = TargetFindCharacterType.PlayerToBattleNpc; AddAllBattleNpcs(masterTarget, false); } } else { // todo: this needs checking.. if (masterTarget is Player || owner.allegiance == CharacterTargetingAllegiance.Player) findType = TargetFindCharacterType.BattleNpcToPlayer; else findType = TargetFindCharacterType.BattleNpcToBattleNpc; // todo: configurable pet aoe buff if (findType == TargetFindCharacterType.BattleNpcToBattleNpc && TryGetMasterTarget(target) != null) withPet = true; // todo: does ffxiv have call for help flag? //if ((findFlags & ValidTarget.HitAll) != 0) //{ // AddAllInZone(masterTarget, withPet); //} AddAllInAlliance(target, withPet); if (findType == TargetFindCharacterType.BattleNpcToPlayer) { if (owner.allegiance == CharacterTargetingAllegiance.Player) AddAllInZone(masterTarget, withPet); else AddAllInHateList(); } } } if(targets.Count > 16) targets.RemoveRange(16, targets.Count - 16); } /// /// Find targets within a box using owner's coordinates and target's coordinates as length /// with corners being `maxDistance` yalms to either side of self and target /// private bool IsWithinBox(Character target, bool withPet) { if (aoeTarget == TargetFindAOETarget.Self) targetPosition = owner.GetPosAsVector3(); else targetPosition = target.GetPosAsVector3(); var myPos = owner.GetPosAsVector3(); var angle = Vector3.GetAngle(myPos, targetPosition); // todo: actually check this works.. var myCorner = myPos.NewHorizontalVector(angle, maxDistance); var myCorner2 = myPos.NewHorizontalVector(angle, -maxDistance); var targetCorner = targetPosition.NewHorizontalVector(angle, maxDistance); var targetCorner2 = targetPosition.NewHorizontalVector(angle, -maxDistance); return target.GetPosAsVector3().IsWithinBox(targetCorner2, myCorner); } private bool IsWithinCone(Character target, bool withPet) { // todo: make this actual cone return owner.IsFacing(target, param) && Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, target.positionX, target.positionY, target.positionZ) < maxDistance; } private void AddTarget(Character target, bool withPet) { if (CanTarget(target)) { // todo: add pets too targets.Add(target); } } private void AddAllInParty(Character target, bool withPet) { var party = target.currentParty as Party; if (party != null) { foreach (var actorId in party.members) { AddTarget(owner.zone.FindActorInArea(actorId), withPet); } } } private void AddAllInAlliance(Character target, bool withPet) { // todo: AddAllInParty(target, withPet); } private void AddAllBattleNpcs(Character target, bool withPet) { int dist = (int)maxDistance; var actors = owner.zone.GetActorsAroundActor(target, dist); foreach (BattleNpc actor in actors) { AddTarget(actor, false); } } private void AddAllInZone(Character target, bool withPet) { var actors = owner.zone.GetAllActors(); foreach (Character actor in actors) { AddTarget(actor, withPet); } } private void AddAllInHateList() { if (!(owner is BattleNpc)) { Program.Log.Error($"TargetFind.AddAllInHateList() owner [{owner.actorId}] {owner.customDisplayName} {owner.actorName} is not a BattleNpc"); } else { foreach (var hateEntry in ((BattleNpc)owner).hateContainer.GetHateList()) { AddTarget(hateEntry.Value.actor, false); } } } public bool CanTarget(Character target, bool withPet = false, bool retarget = false, bool ignoreAOE = false) { // already targeted, dont target again if (target == null || !retarget && targets.Contains(target)) return false; // cant target dead if ((validTarget & ValidTarget.Corpse) == 0 && target.IsDead()) return false; if ((validTarget & ValidTarget.Ally) != 0 && target.allegiance != owner.allegiance) return false; if ((validTarget & ValidTarget.Enemy) != 0 && target.allegiance == owner.allegiance) return false; if ((validTarget & ValidTarget.NPC) != 0 && target.isStatic) return false; // todo: why is player always zoning? // cant target if zoning if (target is Player && ((Player)target).playerSession.isUpdatesLocked) { owner.aiContainer.ChangeTarget(null); return false; } if (/*target.isZoning || owner.isZoning || */target.zone != owner.zone) return false; if (validTarget == ValidTarget.Self && aoeType == TargetFindAOEType.None && owner != target) return false; // this is fuckin retarded, think of a better way l8r if (!ignoreAOE) { // hit everything within zone or within aoe region if (maxDistance == -1.0f || aoeType == TargetFindAOEType.Circle && !IsWithinCircle(target, maxDistance)) return false; if (aoeType == TargetFindAOEType.Cone && !IsWithinCone(target, withPet)) return false; if (aoeType == TargetFindAOEType.Box && !IsWithinBox(target, withPet)) return false; if (aoeType == TargetFindAOEType.None && targets.Count != 0) return false; } return true; } private bool IsWithinCircle(Character target, float maxDistance) { // todo: make y diff modifiable? //if (Math.Abs(owner.positionX - target.positionY) > 6.0f) // return false; if (this.targetPosition == null) this.targetPosition = aoeTarget == TargetFindAOETarget.Self ? owner.GetPosAsVector3() : masterTarget.GetPosAsVector3(); return target.GetPosAsVector3().IsWithinCircle(targetPosition, maxDistance); } private bool IsPlayer(Character target) { if (target is Player) return true; // treat player owned pets as players too return TryGetMasterTarget(target) is Player; } private Character TryGetMasterTarget(Character target) { // if character is a player owned pet, treat as a player if (target.aiContainer != null) { var controller = target.aiContainer.GetController(); if (controller != null) return controller.GetPetMaster(); } return null; } private bool IsBattleNpcOwner(Character target) { // i know i copied this from dsp but what even if (!(owner is Player) || target is Player) return true; // todo: check hate list if (owner is BattleNpc && ((BattleNpc)owner).hateContainer.GetMostHatedTarget() != target) { return false; } return false; } public Character GetValidTarget(Character target, ValidTarget findFlags) { if (target == null || target is Player && ((Player)target).playerSession.isUpdatesLocked) return null; if ((findFlags & ValidTarget.Ally) != 0) { return owner.pet; } // todo: this is beyond retarded var oldFlags = this.validTarget; this.validTarget = findFlags; if (CanTarget(target, false, true)) { this.validTarget = oldFlags; return target; } this.validTarget = oldFlags; return null; } } }