/* =========================================================================== Copyright (C) 2015-2019 Project Meteor Dev Team This file is part of Project Meteor Server. Project Meteor Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Project Meteor Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Project Meteor Server. If not, see . =========================================================================== */ using System; using System.Collections.Generic; using System.Linq; using Meteor.Map.Actors; using Meteor.Common; using Meteor.Map.actors.chara.ai.controllers; using Meteor.Map.actors.group; // port of dsp's ai code https://github.com/DarkstarProject/darkstar/blob/master/src/map/ai/ namespace Meteor.Map.actors.chara.ai { [Flags] public enum ValidTarget : ushort { None = 0x00, Self = 0x01, //Can be used on self (if this flag isn't set and target is self, return false) SelfOnly = 0x02, //Must be used on self (if this flag is set and target isn't self, return false) Party = 0x4, //Can be used on party members PartyOnly = 0x8, //Must be used on party members Ally = 0x10, //Can be used on allies AllyOnly = 0x20, //Must be used on allies NPC = 0x40, //Can be used on static NPCs NPCOnly = 0x80, //Must be used on static NPCs Enemy = 0x100, //Can be used on enemies EnemyOnly = 0x200, //Must be used on enemies Object = 0x400, //Can be used on objects ObjectOnly = 0x800, //Must be used on objects Corpse = 0x1000, //Can be used on corpses CorpseOnly = 0x2000, //Must be used on corpses //These are only used for ValidTarget, not MainTarget MainTargetParty = 0x4000, //Can be used on main target's party (This will basically always be true.) MainTargetPartyOnly = 0x8000, //Must be used on main target's party (This is for Protect basically.) } /// 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 mainTarget; //This is the target that the skill is being used on private Character masterTarget; //If mainTarget is a pet, this is the owner private TargetFindCharacterType findType; private ValidTarget validTarget; private TargetFindAOETarget aoeTarget; private TargetFindAOEType aoeType; private Vector3 aoeTargetPosition; //This is the center of circle of cone AOEs and the position where line aoes come out. If we have mainTarget this might not be needed? private float aoeTargetRotation; //This is the direction the aoe target is facing private float maxDistance; //Radius for circle and cone AOEs, length for line AOEs private float minDistance; //Minimum distance to that target must be to be able to be hit private float width; //Width of line AOEs private float height; //All AoEs are boxes or cylinders. Height is usually 10y regardless of maxDistance, but some commands have different values. Height is total height, so targets can be at most half this distance away on Y axis private float aoeRotateAngle; //This is the angle that cones and line aoes are rotated about aoeTargetPosition for skills that come out of a side other than the front private float coneAngle; //The angle of the cone itself in Pi Radians private float param; private List targets; public TargetFind(Character owner, Character mainTarget = null) { Reset(); this.owner = owner; this.mainTarget = mainTarget == null ? owner : mainTarget; } public void Reset() { this.mainTarget = owner; this.findType = TargetFindCharacterType.None; this.validTarget = ValidTarget.Enemy; this.aoeType = TargetFindAOEType.None; this.aoeTarget = TargetFindAOETarget.Target; this.aoeTargetPosition = null; this.aoeTargetRotation = 0; this.maxDistance = 0.0f; this.minDistance = 0.0f; this.width = 0.0f; this.height = 0.0f; this.aoeRotateAngle = 0.0f; this.coneAngle = 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, float minDistance, float height, float aoeRotate, float coneAngle, float param = 0.0f) { this.validTarget = validTarget; this.aoeType = aoeType; this.maxDistance = maxDistance; this.minDistance = minDistance; this.param = param; this.height = height; this.aoeRotateAngle = aoeRotate; this.coneAngle = coneAngle; } /// /// Call this to prepare Box AOE /// /// /// /// /// public void SetAOEBox(ValidTarget validTarget, TargetFindAOETarget aoeTarget, float length, float width, float aoeRotateAngle) { this.validTarget = validTarget; this.aoeType = TargetFindAOEType.Box; this.aoeTarget = aoeTarget; this.aoeRotateAngle = aoeRotateAngle; 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.maxDistance = length; this.width = 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.aoeTargetPosition = owner.GetPosAsVector3(); this.aoeTargetRotation = owner.rotation + (float) (aoeRotateAngle * Math.PI); } else { this.aoeTargetPosition = target.GetPosAsVector3(); this.aoeTargetRotation = target.rotation + (float) (aoeRotateAngle * Math.PI); } masterTarget = TryGetMasterTarget(target) ?? target; // todo: this is stupid bool withPet = (flags & ValidTarget.Ally) != 0 || masterTarget.allegiance != owner.allegiance; if (masterTarget != null && CanTarget(masterTarget)) targets.Add(masterTarget); if (aoeType != TargetFindAOEType.None) { AddAllInRange(target, withPet); } //if (targets.Count > 8) //targets.RemoveRange(8, targets.Count - 8); } /// /// 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) { Vector3 vec = target.GetPosAsVector3() - aoeTargetPosition; Vector3 relativePos = new Vector3(); //Get target's position relative to owner's position where owner's front is facing positive z axis relativePos.X = (float)(vec.X * Math.Cos(aoeTargetRotation) - vec.Z * Math.Sin(aoeTargetRotation)); relativePos.Z = (float)(vec.X * Math.Sin(aoeTargetRotation) + vec.Z * Math.Cos(aoeTargetRotation)); float halfHeight = height / 2; float halfWidth = width / 2; Vector3 closeBottomLeft = new Vector3(-halfWidth, -halfHeight, minDistance); Vector3 farTopRight = new Vector3(halfWidth, halfHeight, maxDistance); return relativePos.IsWithinBox(closeBottomLeft, farTopRight); } private bool IsWithinCone(Character target, bool withPet) { double distance = Utils.XZDistance(aoeTargetPosition, target.GetPosAsVector3()); //Make sure target is within the correct range first if (!IsWithinCircle(target)) return false; //This might not be 100% right or the most optimal way to do this //Get between taget's position and our position return target.GetPosAsVector3().IsWithinCone(aoeTargetPosition, aoeTargetRotation, coneAngle); } 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.CurrentArea.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.CurrentArea.GetActorsAroundActor(target, dist); foreach (BattleNpc actor in actors) { AddTarget(actor, false); } } private void AddAllInZone(Character target, bool withPet) { var actors = owner.CurrentArea.GetAllActors(); foreach (Character actor in actors) { AddTarget(actor, withPet); } } private void AddAllInRange(Character target, bool withPet) { int dist = (int)maxDistance; var actors = owner.CurrentArea.GetActorsAroundActor(target, dist); foreach (Character actor in actors) { AddTarget(actor, false); } } private void AddAllInHateList() { if (!(owner is BattleNpc)) { Program.Log.Error($"TargetFind.AddAllInHateList() owner [{owner.Id}] {owner.DisplayName} {owner.Name} 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; if (target == null) return false; //This skill can't be used on a corpse and target is dead if ((validTarget & ValidTarget.Corpse) == 0 && target.IsDead()) return false; //This skill must be used on a corpse and target is alive if ((validTarget & ValidTarget.CorpseOnly) != 0 && target.IsAlive()) return false; //This skill can't be used on self and target is self if ((validTarget & ValidTarget.Self) == 0 && target == owner) return false; //This skill must be used on self and target isn't self if ((validTarget & ValidTarget.SelfOnly) != 0 && target != owner) return false; //This skill can't be used on an ally and target is an ally if ((validTarget & ValidTarget.Ally) == 0 && target.allegiance == owner.allegiance) return false; //This skill must be used on an ally and target is not an ally if ((validTarget & ValidTarget.AllyOnly) != 0 && target.allegiance != owner.allegiance) return false; //This skill can't be used on an enemu and target is an enemy if ((validTarget & ValidTarget.Enemy) == 0 && target.allegiance != owner.allegiance) return false; //This skill must be used on an enemy and target is an ally if ((validTarget & ValidTarget.EnemyOnly) != 0 && target.allegiance == owner.allegiance) return false; //This skill can't be used on party members and target is a party member if ((validTarget & ValidTarget.Party) == 0 && target.currentParty == owner.currentParty) return false; //This skill must be used on party members and target is not a party member if ((validTarget & ValidTarget.PartyOnly) != 0 && target.currentParty != owner.currentParty) return false; //This skill can't be used on NPCs and target is an npc if ((validTarget & ValidTarget.NPC) == 0 && target.isStatic) return false; //This skill must be used on NPCs and target is not an npc if ((validTarget & ValidTarget.NPCOnly) != 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.CurrentArea != owner.CurrentArea) return false; if (validTarget == ValidTarget.Self && aoeType == TargetFindAOEType.None && owner != target) return false; //This skill can't be used on main target's party members and target is a party member of main target if ((validTarget & ValidTarget.MainTargetParty) == 0 && target.currentParty == mainTarget.currentParty) return false; //This skill must be used on main target's party members and target is not a party member of main target if ((validTarget & ValidTarget.MainTargetPartyOnly) != 0 && target.currentParty != mainTarget.currentParty) return false; // this is fuckin retarded, think of a better way l8r if (!ignoreAOE) { // hit everything within zone or within aoe region if (param == -1.0f || aoeType == TargetFindAOEType.Circle && !IsWithinCircle(target)) 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) { //Check if XZ is in circle and that y difference isn't larger than half height return target.GetPosAsVector3().IsWithinCircle(aoeTargetPosition, maxDistance, minDistance) && Math.Abs(owner.positionY - target.positionY) <= (height / 2); } 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; } } }