2017-07-08 00:20:55 +01:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading.Tasks;
|
2017-08-02 23:06:11 +01:00
|
|
|
|
using FFXIVClassic.Common;
|
2017-07-08 00:20:55 +01:00
|
|
|
|
using FFXIVClassic_Map_Server.Actors;
|
2017-08-02 23:06:11 +01:00
|
|
|
|
using FFXIVClassic_Map_Server.packets.send.actor;
|
|
|
|
|
using FFXIVClassic_Map_Server.actors.area;
|
|
|
|
|
using FFXIVClassic_Map_Server.utils;
|
2017-07-08 00:20:55 +01:00
|
|
|
|
|
|
|
|
|
namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
|
|
|
|
|
{
|
|
|
|
|
class BattleNpcController : Controller
|
|
|
|
|
{
|
2017-07-11 20:49:38 +01:00
|
|
|
|
private DateTime lastActionTime;
|
|
|
|
|
private DateTime lastSpellCastTime;
|
|
|
|
|
private DateTime lastSkillTime;
|
|
|
|
|
private DateTime lastSpecialSkillTime; // todo: i dont think monsters have "2hr" cooldowns like ffxi
|
|
|
|
|
private DateTime deaggroTime;
|
|
|
|
|
private DateTime neutralTime;
|
|
|
|
|
private DateTime waitTime;
|
|
|
|
|
|
|
|
|
|
private bool firstSpell = true;
|
|
|
|
|
private DateTime lastRoamScript; // todo: what even is this used as
|
|
|
|
|
|
2017-08-02 23:06:11 +01:00
|
|
|
|
private new BattleNpc owner;
|
|
|
|
|
public BattleNpcController(BattleNpc owner) :
|
|
|
|
|
base(owner)
|
2017-07-08 00:20:55 +01:00
|
|
|
|
{
|
|
|
|
|
this.owner = owner;
|
|
|
|
|
this.lastUpdate = DateTime.Now;
|
2017-08-02 23:06:11 +01:00
|
|
|
|
this.waitTime = lastUpdate.AddSeconds(5);
|
2017-07-08 00:20:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void Update(DateTime tick)
|
|
|
|
|
{
|
2017-08-02 23:06:11 +01:00
|
|
|
|
// todo: handle aggro/deaggro and other shit here
|
|
|
|
|
if (owner.aiContainer.IsEngaged())
|
|
|
|
|
{
|
|
|
|
|
DoCombatTick(tick);
|
|
|
|
|
}
|
|
|
|
|
else if (!owner.IsDead())
|
|
|
|
|
{
|
|
|
|
|
DoRoamTick(tick);
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-11 20:49:38 +01:00
|
|
|
|
|
2017-08-02 23:06:11 +01:00
|
|
|
|
public bool TryDeaggro()
|
|
|
|
|
{
|
|
|
|
|
if (owner.hateContainer.GetMostHatedTarget() == null || !owner.aiContainer.GetTargetFind().CanTarget(owner.target as Character))
|
2017-07-11 20:49:38 +01:00
|
|
|
|
{
|
2017-08-02 23:06:11 +01:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
else if (!owner.IsCloseToSpawn())
|
|
|
|
|
{
|
|
|
|
|
return true;
|
2017-07-11 20:49:38 +01:00
|
|
|
|
}
|
2017-08-02 23:06:11 +01:00
|
|
|
|
return false;
|
2017-07-08 00:20:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override bool Engage(Character target)
|
|
|
|
|
{
|
|
|
|
|
// todo: check distance, last swing time, status effects
|
2017-07-11 20:49:38 +01:00
|
|
|
|
var canEngage = this.owner.aiContainer.InternalEngage(target);
|
|
|
|
|
if (canEngage)
|
|
|
|
|
{
|
|
|
|
|
// reset casting
|
|
|
|
|
firstSpell = true;
|
2017-08-02 23:06:11 +01:00
|
|
|
|
// todo: find a better place to put this?
|
|
|
|
|
if (owner.GetState() != SetActorStatePacket.MAIN_STATE_ACTIVE)
|
|
|
|
|
owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE);
|
|
|
|
|
|
2017-07-11 20:49:38 +01:00
|
|
|
|
|
2017-08-02 23:06:11 +01:00
|
|
|
|
// todo: check speed/is able to move
|
|
|
|
|
// todo: too far, path to player if mob, message if player
|
|
|
|
|
// owner.ResetMoveSpeeds();
|
|
|
|
|
owner.moveState = 2;
|
|
|
|
|
if (owner.currentSubState == SetActorStatePacket.SUB_STATE_MONSTER && owner.moveSpeeds[1] != 0)
|
|
|
|
|
{
|
|
|
|
|
// todo: actual stat based range
|
|
|
|
|
if (Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, target.positionX, target.positionY, target.positionZ) > 10)
|
|
|
|
|
{
|
|
|
|
|
owner.aiContainer.pathFind.SetPathFlags(PathFindFlags.None);
|
2017-08-25 03:52:43 +01:00
|
|
|
|
owner.aiContainer.pathFind.PathInRange(target.positionX, target.positionY, target.positionZ, 1.5f, owner.GetAttackRange());
|
2017-08-02 23:06:11 +01:00
|
|
|
|
ChangeTarget(target);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lastActionTime = DateTime.Now;
|
2017-07-11 20:49:38 +01:00
|
|
|
|
// todo: adjust cooldowns with modifiers
|
|
|
|
|
}
|
|
|
|
|
return canEngage;
|
2017-07-08 00:20:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryEngage(Character target)
|
|
|
|
|
{
|
|
|
|
|
// todo:
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-02 23:06:11 +01:00
|
|
|
|
public override void Disengage()
|
2017-07-08 00:20:55 +01:00
|
|
|
|
{
|
2017-08-02 23:06:11 +01:00
|
|
|
|
var target = owner.target;
|
|
|
|
|
base.Disengage();
|
2017-07-08 00:20:55 +01:00
|
|
|
|
// todo:
|
2017-08-21 00:40:41 +01:00
|
|
|
|
lastActionTime = lastUpdate.AddSeconds(5);
|
2017-08-02 23:06:11 +01:00
|
|
|
|
owner.isMovingToSpawn = true;
|
2017-08-21 00:40:41 +01:00
|
|
|
|
neutralTime = lastActionTime;
|
2017-08-02 23:06:11 +01:00
|
|
|
|
owner.hateContainer.ClearHate();
|
|
|
|
|
owner.moveState = 1;
|
|
|
|
|
lua.LuaEngine.CallLuaBattleAction(owner, "onDisengage", owner, target);
|
2017-07-08 00:20:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void Cast(Character target, uint spellId)
|
|
|
|
|
{
|
2017-08-25 03:52:43 +01:00
|
|
|
|
// todo:
|
2017-07-08 00:20:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void Ability(Character target, uint abilityId)
|
|
|
|
|
{
|
2017-08-25 03:52:43 +01:00
|
|
|
|
// todo:
|
2017-07-08 00:20:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void RangedAttack(Character target)
|
|
|
|
|
{
|
2017-08-25 03:52:43 +01:00
|
|
|
|
// todo:
|
2017-07-08 00:20:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-11 20:49:38 +01:00
|
|
|
|
public override void MonsterSkill(Character target, uint mobSkillId)
|
2017-07-08 00:20:55 +01:00
|
|
|
|
{
|
2017-08-25 03:52:43 +01:00
|
|
|
|
// todo:
|
2017-07-08 00:20:55 +01:00
|
|
|
|
}
|
2017-07-11 20:49:38 +01:00
|
|
|
|
|
|
|
|
|
private void DoRoamTick(DateTime tick)
|
|
|
|
|
{
|
2017-08-02 23:06:11 +01:00
|
|
|
|
if (owner.hateContainer.GetHateList().Count > 0)
|
2017-07-27 22:19:20 +01:00
|
|
|
|
{
|
2017-08-02 23:06:11 +01:00
|
|
|
|
Engage(owner.hateContainer.GetMostHatedTarget());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
//else if (owner.currentLockedTarget != 0)
|
|
|
|
|
//{
|
|
|
|
|
// ChangeTarget(Server.GetWorldManager().GetActorInWorld(owner.currentLockedTarget).GetAsCharacter());
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
|
|
if (tick >= waitTime)
|
|
|
|
|
{
|
|
|
|
|
// todo: aggro cooldown
|
|
|
|
|
neutralTime = tick.AddSeconds(5);
|
|
|
|
|
if (owner.aiContainer.pathFind.IsFollowingPath())
|
2017-07-27 22:19:20 +01:00
|
|
|
|
{
|
2017-08-02 23:06:11 +01:00
|
|
|
|
owner.aiContainer.pathFind.FollowPath();
|
|
|
|
|
lastActionTime = tick.AddSeconds(-5);
|
2017-07-27 22:19:20 +01:00
|
|
|
|
}
|
2017-08-02 23:06:11 +01:00
|
|
|
|
else
|
2017-07-27 22:19:20 +01:00
|
|
|
|
{
|
2017-08-02 23:06:11 +01:00
|
|
|
|
if (tick >= lastActionTime)
|
|
|
|
|
{
|
2017-08-23 19:31:03 +01:00
|
|
|
|
var battlenpc = owner as BattleNpc;
|
|
|
|
|
owner.aiContainer.pathFind.PathInRange(battlenpc.spawnX, battlenpc.spawnY, battlenpc.spawnZ, 1.5f, 15.0f);
|
2017-08-02 23:06:11 +01:00
|
|
|
|
}
|
2017-07-27 22:19:20 +01:00
|
|
|
|
}
|
2017-08-02 23:06:11 +01:00
|
|
|
|
// todo:
|
|
|
|
|
waitTime = tick.AddSeconds(10);
|
|
|
|
|
owner.OnRoam(tick);
|
2017-07-27 22:19:20 +01:00
|
|
|
|
}
|
2017-08-21 00:40:41 +01:00
|
|
|
|
|
|
|
|
|
if (owner.aiContainer.pathFind.IsFollowingPath())
|
|
|
|
|
{
|
|
|
|
|
owner.aiContainer.pathFind.FollowPath();
|
|
|
|
|
}
|
2017-07-11 20:49:38 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DoCombatTick(DateTime tick)
|
|
|
|
|
{
|
2017-08-02 23:06:11 +01:00
|
|
|
|
HandleHate();
|
2017-07-11 20:49:38 +01:00
|
|
|
|
|
2017-08-02 23:06:11 +01:00
|
|
|
|
// todo: magic/attack/ws cooldowns etc
|
|
|
|
|
if (TryDeaggro())
|
|
|
|
|
{
|
|
|
|
|
Disengage();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Move();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Move()
|
|
|
|
|
{
|
|
|
|
|
if (!owner.aiContainer.CanFollowPath())
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner.aiContainer.pathFind.IsFollowingScriptedPath())
|
|
|
|
|
{
|
|
|
|
|
owner.aiContainer.pathFind.FollowPath();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-16 17:25:32 +01:00
|
|
|
|
var targetPos = new Vector3(owner.target.positionX, owner.target.positionY, owner.target.positionZ);
|
2017-08-02 23:06:11 +01:00
|
|
|
|
var distance = Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, targetPos.X, targetPos.Y, targetPos.Z);
|
|
|
|
|
|
2017-08-25 03:52:43 +01:00
|
|
|
|
if (distance > owner.GetAttackRange() - 0.2f || owner.aiContainer.CanFollowPath())
|
2017-08-02 23:06:11 +01:00
|
|
|
|
{
|
|
|
|
|
if (CanMoveForward(distance))
|
|
|
|
|
{
|
|
|
|
|
if (!owner.aiContainer.pathFind.IsFollowingPath() && distance > 3)
|
|
|
|
|
{
|
|
|
|
|
// pathfind if too far otherwise jump to target
|
2017-08-23 19:31:03 +01:00
|
|
|
|
owner.aiContainer.pathFind.SetPathFlags(PathFindFlags.None);
|
2017-08-22 19:47:54 +01:00
|
|
|
|
owner.aiContainer.pathFind.PreparePath(targetPos, 1.5f, 5);
|
2017-08-02 23:06:11 +01:00
|
|
|
|
}
|
|
|
|
|
owner.aiContainer.pathFind.FollowPath();
|
|
|
|
|
if (!owner.aiContainer.pathFind.IsFollowingPath())
|
|
|
|
|
{
|
|
|
|
|
if (owner.target.currentSubState == SetActorStatePacket.SUB_STATE_PLAYER)
|
|
|
|
|
{
|
|
|
|
|
foreach (var battlenpc in owner.zone.GetActorsAroundActor<BattleNpc>(owner, 1))
|
|
|
|
|
{
|
2017-08-16 17:25:32 +01:00
|
|
|
|
if (battlenpc == owner)
|
|
|
|
|
continue;
|
|
|
|
|
float mobDistance = Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, battlenpc.positionX, battlenpc.positionY, battlenpc.positionZ);
|
2017-08-23 19:31:03 +01:00
|
|
|
|
if (mobDistance < 0.25f && (battlenpc.updateFlags & ActorUpdateFlags.Position) == 0)
|
|
|
|
|
battlenpc.aiContainer.pathFind.PathInRange(targetPos, 1.3f, 1.8f);
|
2017-08-02 23:06:11 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
FaceTarget();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void FaceTarget()
|
|
|
|
|
{
|
|
|
|
|
// todo: check if stunned etc
|
|
|
|
|
if (owner.statusEffects.HasStatusEffectsByFlag(StatusEffectFlags.PreventAction))
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
owner.LookAt(owner.target);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool CanMoveForward(float distance)
|
|
|
|
|
{
|
|
|
|
|
// todo: check spawn leash and stuff
|
|
|
|
|
if (!owner.IsCloseToSpawn())
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool CanAggroTarget(Character target)
|
|
|
|
|
{
|
|
|
|
|
if (owner.neutral || owner.aggroType == AggroType.None || owner.IsDead())
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// todo: can mobs aggro mounted targets?
|
|
|
|
|
if (target.IsDead() || target.currentMainState == SetActorStatePacket.MAIN_STATE_MOUNTED)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (owner.aiContainer.IsSpawned() && !owner.aiContainer.IsEngaged() && CanDetectTarget(target))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool CanDetectTarget(Character target, bool forceSight = false)
|
|
|
|
|
{
|
2017-08-23 19:31:03 +01:00
|
|
|
|
// todo: this should probably be changed to only allow detection at end of path?
|
|
|
|
|
if (owner.aiContainer.pathFind.IsFollowingScriptedPath() || owner.aiContainer.pathFind.IsFollowingPath() && !owner.aiContainer.pathFind.AtPoint())
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-02 23:06:11 +01:00
|
|
|
|
// todo: handle sight/scent/hp etc
|
|
|
|
|
if (target.IsDead() || target.currentMainState == SetActorStatePacket.MAIN_STATE_MOUNTED)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
float verticalDistance = Math.Abs(target.positionY - owner.positionY);
|
|
|
|
|
if (verticalDistance > 8)
|
|
|
|
|
return false;
|
|
|
|
|
|
2017-08-21 00:40:41 +01:00
|
|
|
|
var distance = Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, target.positionX, target.positionY, target.positionZ);
|
2017-08-02 23:06:11 +01:00
|
|
|
|
|
|
|
|
|
bool detectSight = forceSight || (owner.aggroType & AggroType.Sight) != 0;
|
|
|
|
|
bool hasSneak = false;
|
|
|
|
|
bool hasInvisible = false;
|
|
|
|
|
bool isFacing = owner.IsFacing(target);
|
|
|
|
|
|
|
|
|
|
// todo: check line of sight and aggroTypes
|
|
|
|
|
if (distance > 20)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// todo: seems ffxiv doesnt even differentiate between sneak/invis?
|
|
|
|
|
{
|
|
|
|
|
hasSneak = target.statusEffects.HasStatusEffectsByFlag((uint)StatusEffectFlags.Stealth);
|
|
|
|
|
hasInvisible = hasSneak;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-21 00:40:41 +01:00
|
|
|
|
if (detectSight && !hasInvisible && isFacing)
|
2017-08-02 23:06:11 +01:00
|
|
|
|
return CanSeePoint(target.positionX, target.positionY, target.positionZ);
|
|
|
|
|
|
|
|
|
|
if ((owner.aggroType & AggroType.LowHp) != 0 && target.GetHPP() < 75)
|
|
|
|
|
return CanSeePoint(target.positionX, target.positionY, target.positionZ);
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool CanSeePoint(float x, float y, float z)
|
|
|
|
|
{
|
|
|
|
|
return NavmeshUtils.CanSee((Zone)owner.zone, owner.positionX, owner.positionY, owner.positionZ, x, y, z);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleHate()
|
|
|
|
|
{
|
|
|
|
|
ChangeTarget(owner.hateContainer.GetMostHatedTarget());
|
2017-07-11 20:49:38 +01:00
|
|
|
|
}
|
2017-07-08 00:20:55 +01:00
|
|
|
|
}
|
|
|
|
|
}
|