stubbed some more states

- stubbed some ability stuff
- moved packet things to loop instead of session only
- added mob roaming and aggro
- todo: fix target find/detection/pathfinding speed/line of sight/line aoe length etc
- todo: see "// todo:" in code
This commit is contained in:
Tahir Akhlaq
2017-08-02 23:06:11 +01:00
parent c7b87c0d89
commit 68657e1edc
33 changed files with 1459 additions and 444 deletions

View File

@@ -20,7 +20,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
private Stack<State> states;
private DateTime latestUpdate;
private DateTime prevUpdate;
private PathFind pathFind;
public readonly PathFind pathFind;
private TargetFind targetFind;
private ActionQueue actionQueue;
@@ -43,16 +43,24 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
// todo: trigger listeners
// todo: action queues
controller?.Update(tick);
State currState;
while (states.Count > 0 && (currState = states.Peek()).Update(tick))
if (controller == null && pathFind != null)
{
if (currState == GetCurrentState())
{
pathFind.FollowPath();
}
// todo: action queues
if (controller != null && controller.canUpdate)
controller.Update(tick);
State top;
while (states.Count > 0 && (top = states.Peek()).Update(tick))
{
if (top == GetCurrentState())
{
states.Pop().Cleanup();
}
}
owner.PostUpdate(tick);
}
public void CheckCompletedStates()
@@ -93,6 +101,16 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return controller;
}
public TargetFind GetTargetFind()
{
return targetFind;
}
public bool CanFollowPath()
{
return pathFind != null && (GetCurrentState() != null || GetCurrentState().CanChangeState());
}
public bool CanChangeState()
{
return states.Count == 0 || states.Peek().CanInterrupt();
@@ -135,9 +153,14 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
}
}
public bool IsCurrentState<T>() where T : State
{
return GetCurrentState() is T;
}
public State GetCurrentState()
{
return states.Peek() ?? null;
return states.Count > 0 ? states.Peek() : null;
}
public DateTime GetLatestUpdate()
@@ -145,10 +168,19 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return latestUpdate;
}
public void Reset()
{
// todo: reset cooldowns and stuff here too?
targetFind?.Reset();
pathFind?.Clear();
ClearStates();
InternalDisengage();
}
public bool IsSpawned()
{
// todo: set a flag when finished spawning
return true;
return !IsDead();
}
public bool IsEngaged()
@@ -211,7 +243,20 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public void InternalChangeTarget(Character target)
{
// todo: use invalid target id
// todo: this is retarded, call entity's changetarget function
owner.target = target;
owner.currentLockedTarget = target != null ? target.actorId : 0xC0000000;
owner.currentTarget = target != null ? target.actorId : 0xC0000000;
if (IsEngaged() || target == null)
{
}
else
{
Engage(target);
}
}
public bool InternalEngage(Character target)
@@ -236,7 +281,15 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public void InternalDisengage()
{
pathFind?.Clear();
GetTargetFind()?.Reset();
owner.updateFlags |= (ActorUpdateFlags.State | ActorUpdateFlags.HpTpMp);
// todo: use the update flags
owner.ChangeState(SetActorStatePacket.MAIN_STATE_PASSIVE);
ChangeTarget(null);
}
public void InternalCast(Character target, uint spellId)
@@ -256,7 +309,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public void InternalDie(DateTime tick, uint timeToFadeout)
{
ClearStates();
Disengage();
ForceChangeState(new DeathState(owner, tick, timeToFadeout));
}
public void InternalRaise(Character target)

View File

@@ -0,0 +1,100 @@
using FFXIVClassic_Map_Server.Actors;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FFXIVClassic_Map_Server.actors.chara.ai
{
public enum AbilityRequirements : ushort
{
None,
DiscipleOfWar = 0x01,
DiscipeOfMagic = 0x02,
HandToHand = 0x04,
Sword = 0x08,
Shield = 0x10,
Axe = 0x20,
Archery = 0x40,
Polearm = 0x80,
Thaumaturgy = 0x100,
Conjury = 0x200
}
public enum AbilityPositionBonus : byte
{
None,
Front = 0x01,
Rear = 0x02,
Flank = 0x04
}
public enum AbilityProcRequirement : byte
{
None,
Evade = 0x01,
Block = 0x02,
Parry = 0x04,
Miss = 0x08
}
class Ability
{
public ushort abilityId;
public string name;
public byte job;
public byte level;
public AbilityRequirements requirements;
public TargetFindFlags validTarget;
public TargetFindAOETarget aoeTarget;
public TargetFindAOEType aoeType;
public int range;
public TargetFindCharacterType characterFind;
public uint statusDurationSeconds;
public uint castTimeSeconds;
public uint recastTimeSeconds;
public ushort mpCost;
public ushort tpCost;
public byte animationType;
public ushort effectAnimation;
public ushort modelAnimation;
public ushort animationDurationSeconds;
public AbilityPositionBonus positionBonus;
public AbilityProcRequirement procRequirement;
public TargetFind targetFind;
public Ability(ushort id, string name)
{
this.abilityId = id;
this.name = name;
this.range = -1;
}
public Ability Clone()
{
return (Ability)MemberwiseClone();
}
public bool IsSpell()
{
return mpCost != 0 || castTimeSeconds != 0;
}
public bool IsInstantCast()
{
return castTimeSeconds == 0;
}
public bool CanPlayerUse(Character user, Character target)
{
// todo: set box length..
targetFind = new TargetFind(user);
targetFind.SetAOEType(aoeTarget, aoeType, aoeType == TargetFindAOEType.Box ? range / 2 : range, 40);
return false;
}
}
}

View File

@@ -38,11 +38,20 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public void AddBaseHate(Character target)
{
if (!HasHateForTarget(target))
hateList.Add(target, new HateEntry(target, 0, 0, true));
hateList.Add(target, new HateEntry(target, 1, 0, true));
else
Program.Log.Error($"{target.actorName} is already on [{owner.actorId}]{owner.actorName}'s hate list!");
}
public void UpdateHate(Character target, int damage)
{
if (HasHateForTarget(target))
{
//hateList[target].volatileEnmity += (uint)damage;
hateList[target].cumulativeEnmity += (uint)damage;
}
}
public void ClearHate(Character target = null)
{
if (target != null)

View File

@@ -8,20 +8,37 @@ using FFXIVClassic_Map_Server;
using FFXIVClassic_Map_Server.utils;
using FFXIVClassic.Common;
using FFXIVClassic_Map_Server.actors.area;
using FFXIVClassic_Map_Server.packets.send.actor;
namespace FFXIVClassic_Map_Server.actors.chara.ai
{
// todo: path flags, check for obstacles etc
public enum PathFindFlags
{
None,
Scripted = 0x01,
IgnoreNav = 0x02,
}
class PathFind
{
private Character owner;
private List<Vector3> path;
private bool canFollowPath;
private PathFindFlags pathFlags;
public PathFind(Character owner)
{
this.owner = owner;
}
public void PreparePath(Vector3 dest, float stepSize = 0.70f, int maxPath = 40, float polyRadius = 0.0f)
{
PreparePath(dest.X, dest.Y, dest.Z, stepSize, maxPath, polyRadius);
}
// todo: is this class even needed?
public void PathTo(float x, float y, float z, float stepSize = 0.70f, int maxPath = 40, float polyRadius = 0.0f)
public void PreparePath(float x, float y, float z, float stepSize = 0.70f, int maxPath = 40, float polyRadius = 0.0f)
{
var pos = new Vector3(owner.positionX, owner.positionY, owner.positionZ);
var dest = new Vector3(x, y, z);
@@ -29,7 +46,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
var path = NavmeshUtils.GetPath(zone, pos, dest, stepSize, maxPath, polyRadius);
if ((pathFlags & PathFindFlags.IgnoreNav) != 0)
path = new List<Vector3>(1) { new Vector3(x, y, z) };
else
path = NavmeshUtils.GetPath(zone, pos, dest, stepSize, maxPath, polyRadius);
if (path != null)
{
@@ -48,11 +68,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
owner.positionZ = owner.oldPositionZ;
}
owner.positionUpdates = path;
owner.hasMoved = true;
owner.isAtSpawn = false;
sw.Stop();
zone.pathCalls++;
zone.pathCallTime += sw.ElapsedMilliseconds;
@@ -63,5 +78,70 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
Program.Log.Error("[{0}][{1}] Created {2} points in {3} milliseconds", owner.actorId, owner.actorName, path.Count, sw.ElapsedMilliseconds);
}
}
public void PathInRange(Vector3 dest, float minRange, float maxRange)
{
PathInRange(dest.X, dest.Y, dest.Z, minRange, maxRange);
}
public void PathInRange(float x, float y, float z, float minRange, float maxRange = 5.0f)
{
var dest = owner.FindRandomPoint(x, y, z, minRange, maxRange);
PreparePath(dest.X, dest.Y, dest.Z);
}
public void SetPathFlags(PathFindFlags flags)
{
this.pathFlags = flags;
}
public bool IsFollowingPath()
{
return path.Count > 0;
}
public bool IsFollowingScriptedPath()
{
return (pathFlags & PathFindFlags.Scripted) != 0;
}
public void FollowPath()
{
if (path?.Count > 0)
{
var point = path[0];
owner.OnPath(point);
owner.QueuePositionUpdate(point);
path.Remove(point);
if (path.Count == 0)
owner.LookAt(point.X, point.Y);
}
}
public void Clear()
{
// todo:
path?.Clear();
pathFlags = PathFindFlags.None;
}
private float GetSpeed()
{
float baseSpeed = owner.GetSpeed();
// todo: get actual speed crap
if (owner.currentSubState != SetActorStatePacket.SUB_STATE_NONE)
{
if (owner.currentSubState == SetActorStatePacket.SUB_STATE_MONSTER)
{
owner.ChangeSpeed(0.0f, SetActorSpeedPacket.DEFAULT_WALK - 2.0f, SetActorSpeedPacket.DEFAULT_RUN - 2.0f, SetActorSpeedPacket.DEFAULT_ACTIVE - 2.0f);
}
// baseSpeed += ConfigConstants.SPEED_MOD;
}
return baseSpeed;
}
}
}

View File

@@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace FFXIVClassic_Map_Server.actors.chara.ai
{
enum StatusEffectId
enum StatusEffectId : uint
{
RageofHalone = 221021,
@@ -324,7 +324,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
}
[Flags]
enum StatusEffectFlags
enum StatusEffectFlags : uint
{
None = 0x00,
Silent = 0x01, // dont display effect loss message
@@ -338,6 +338,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
LoseOnDamageTaken = 0x100, // effects removed when owner takes damage
PreventAction = 0x200, // effects which prevent actions such as sleep/paralyze/petrify
Stealth = 0x400, // sneak/invis
}
enum StatusEffectOverwrite : byte
@@ -370,6 +371,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public StatusEffect(Character owner, uint id, UInt64 magnitude, uint tickMs, uint durationMs, byte tier = 0)
{
this.owner = owner;
this.source = owner;
this.id = (StatusEffectId)id;
this.magnitude = magnitude;
this.tickMs = tickMs;
@@ -390,6 +392,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public StatusEffect(Character owner, StatusEffect effect)
{
this.owner = owner;
this.source = owner;
this.id = effect.id;
this.magnitude = effect.magnitude;
this.tickMs = effect.tickMs;

View File

@@ -10,6 +10,7 @@ using FFXIVClassic_Map_Server.actors.area;
using FFXIVClassic_Map_Server.packets.send;
using FFXIVClassic_Map_Server.packets.send.actor;
using System.Collections.ObjectModel;
using FFXIVClassic_Map_Server.utils;
namespace FFXIVClassic_Map_Server.actors.chara.ai
{
@@ -19,6 +20,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
private readonly Dictionary<uint, StatusEffect> effects;
public static readonly int MAX_EFFECTS = 20;
private bool sendUpdate = false;
public StatusEffectContainer(Character owner)
{
this.owner = owner;
@@ -44,10 +46,30 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (sendUpdate)
{
owner.zone.BroadcastPacketsAroundActor(owner, owner.GetActorStatusPackets());
}
var propPacketUtil = new ActorPropertyPacketUtil("charaWork.status", owner);
sendUpdate = false;
//Status Times
for (int i = 0; i < owner.charaWork.statusShownTime.Length; i++)
{
if (owner.charaWork.status[i] != 0xFFFF && owner.charaWork.status[i] != 0)
propPacketUtil.AddProperty(String.Format("charaWork.status[{0}]", i));
if (owner.charaWork.statusShownTime[i] != 0xFFFFFFFF)
propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i));
}
owner.zone.BroadcastPacketsAroundActor(owner, propPacketUtil.Done());
sendUpdate = false;
}
}
public bool HasStatusEffect(uint id)
{
return effects.ContainsKey(id);
}
public bool HasStatusEffect(StatusEffectId id)
{
return effects.ContainsKey((uint)id);
}
public bool AddStatusEffect(uint id, UInt64 magnitude, double tickMs, double durationMs, byte tier = 0)
@@ -74,8 +96,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (!silent || !effect.GetSilent() || (effect.GetFlags() & (uint)StatusEffectFlags.Silent) == 0)
{
// todo: send packet to client with effect added message
//foreach (var player in owner.zone.GetActorsAroundActor<Player>(owner, 50))
// player.QueuePacket(packets.send.actor.battle.BattleActionX01Packet.BuildPacket(player.actorId, effect.GetSource().actorId, owner.actorId, 0, effect.GetStatusEffectId(), 0, effect.GetStatusId(), 0, 0));
foreach (var player in owner.zone.GetActorsAroundActor<Player>(owner, 50))
player.QueuePacket(packets.send.actor.battle.BattleActionX01Packet.BuildPacket(player.actorId, newEffect.GetSource().actorId, owner.actorId, 0, newEffect.GetStatusEffectId(), 0, newEffect.GetStatusId(), 0, 0));
}
// wont send a message about losing effect here
@@ -93,6 +115,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
owner.charaWork.statusShownTime[index] = Utils.UnixTimeStampUTC() + (newEffect.GetDurationMs() / 1000);
this.owner.zone.BroadcastPacketAroundActor(this.owner, SetActorStatusPacket.BuildPacket(this.owner.actorId, (ushort)index, (ushort)newEffect.GetStatusId()));
}
owner.RecalculateHpMpTp();
sendUpdate = true;
}
return true;
@@ -122,6 +145,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
// function onLose(actor, effect)
LuaEngine.CallLuaStatusEffectFunction(this.owner, effect, "onLose", this.owner, effect);
effects.Remove(effect.GetStatusEffectId());
owner.RecalculateHpMpTp();
sendUpdate = true;
}
}
@@ -185,6 +209,12 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return list;
}
// todo: why the fuck cant c# convert enums/
public bool HasStatusEffectsByFlag(StatusEffectFlags flags)
{
return HasStatusEffectsByFlag((uint)flags);
}
public bool HasStatusEffectsByFlag(uint flag)
{
foreach (var effect in effects.Values)

View File

@@ -7,30 +7,30 @@ 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.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
{
/// <summary> todo: what even do i summarise this as? </summary>
[Flags]
enum TargetFindFlags
enum TargetFindFlags : byte
{
None,
None = 0x00,
/// <summary> Able to target <see cref="Player"/>s even if not in target's party </summary>
HitAll,
HitAll = 0x01,
/// <summary> Able to target all <see cref="Player"/>s in target's party/alliance </summary>
Alliance,
Alliance = 0x02,
/// <summary> Able to target any <see cref="Pet"/> in target's party/alliance </summary>
Pets,
Pets = 0x04,
/// <summary> Target all in zone, regardless of distance </summary>
ZoneWide,
ZoneWide = 0x08,
/// <summary> Able to target dead <see cref="Player"/>s </summary>
Dead,
Dead = 0x10,
}
/// <summary> Targeting from/to different entity types </summary>
enum TargetFindCharacterType
enum TargetFindCharacterType : byte
{
None,
/// <summary> Player can target all <see cref="Player">s in party </summary>
@@ -44,7 +44,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
}
/// <summary> Type of AOE region to create </summary>
enum TargetFindAOEType
enum TargetFindAOEType : byte
{
None,
/// <summary> Really a cylinder, uses extents parameter in SetAOEType </summary>
@@ -56,7 +56,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
}
/// <summary> Set AOE around self or target </summary>
enum TargetFindAOERadiusType
enum TargetFindAOETarget : byte
{
/// <summary> Set AOE's origin at target's position </summary>
Target,
@@ -73,7 +73,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
private TargetFindCharacterType findType;
private TargetFindFlags findFlags;
private TargetFindAOEType aoeType;
private TargetFindAOERadiusType radiusType;
private TargetFindAOETarget aoeTarget;
private Vector3 targetPosition;
private float extents;
private float angle;
@@ -91,7 +91,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.findType = TargetFindCharacterType.None;
this.findFlags = TargetFindFlags.None;
this.aoeType = TargetFindAOEType.None;
this.radiusType = TargetFindAOERadiusType.Self;
this.aoeTarget = TargetFindAOETarget.Self;
this.targetPosition = null;
this.extents = 0.0f;
this.angle = 0.0f;
@@ -114,12 +114,12 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
/// <param name="extents">
/// <see cref="TargetFindAOEType.Circle"/> - radius of circle <para/>
/// <see cref="TargetFindAOEType.Cone"/> - height of cone <para/>
/// <see cref="TargetFindAOEType.Box"/> - width of box / 2
/// <see cref="TargetFindAOEType.Box"/> - width of box / 2 (todo: set box length not just between user and target)
/// </param>
/// <param name="angle"> Angle in radians of cone </param>
public void SetAOEType(TargetFindAOERadiusType radiusType, TargetFindAOEType aoeType, float extents = -1.0f, float angle = -1.0f)
public void SetAOEType(TargetFindAOETarget aoeTarget, TargetFindAOEType aoeType, float extents = -1.0f, float angle = -1.0f)
{
this.radiusType = TargetFindAOERadiusType.Target;
this.aoeTarget = TargetFindAOETarget.Target;
this.aoeType = aoeType;
this.extents = extents != -1.0f ? extents : 0.0f;
this.angle = angle != -1.0f ? angle : 0.0f;
@@ -146,7 +146,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
findFlags = flags;
// todo: maybe we should keep a snapshot which is only updated on each tick for consistency
// are we creating aoe circles around target or self
if ((aoeType & TargetFindAOEType.Circle) != 0 && radiusType != TargetFindAOERadiusType.Self)
if ((aoeType & TargetFindAOEType.Circle) != 0 && aoeTarget != TargetFindAOETarget.Self)
this.targetPosition = owner.GetPosAsVector3();
else
this.targetPosition = target.GetPosAsVector3();
@@ -306,10 +306,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
}
}
public bool CanTarget(Character target, bool withPet = false)
public bool CanTarget(Character target, bool withPet = false, bool retarget = false)
{
// already targeted, dont target again
if (targets.Contains(target))
if (target == null || !retarget && targets.Contains(target))
return false;
// cant target dead
@@ -318,8 +318,12 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
bool targetingPlayer = target is Player;
// todo: why is player always zoning?
// cant target if zoning
if (target.isZoning || owner.isZoning || target.zone != owner.zone || targetingPlayer && ((Player)target).playerSession.isUpdatesLocked)
if (/*target.isZoning || owner.isZoning || */target.zone != owner.zone || targetingPlayer && ((Player)target).playerSession.isUpdatesLocked)
return false;
if (aoeTarget == TargetFindAOETarget.Self && aoeType != TargetFindAOEType.None && owner != target)
return false;
// hit everything within zone or within aoe region
@@ -332,7 +336,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (aoeType == TargetFindAOEType.Box && IsWithinBox(target, withPet))
return true;
return false;
return true;
}
private bool IsPlayer(Character target)
@@ -359,12 +363,38 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
private bool IsBattleNpcOwner(Character target)
{
// i know i copied this from dsp but what even
if (!(owner is Player) || target is Player)
if (owner.currentSubState != SetActorStatePacket.SUB_STATE_PLAYER || target.currentSubState == SetActorStatePacket.SUB_STATE_PLAYER)
return true;
// todo: check hate list
if (owner.currentSubState == SetActorStatePacket.SUB_STATE_MONSTER && ((BattleNpc)owner).hateContainer.GetMostHatedTarget() != target)
{
return false;
}
return false;
}
public Character GetValidTarget(Character target, TargetFindFlags findFlags)
{
if (target == null || target.currentSubState == SetActorStatePacket.SUB_STATE_PLAYER && ((Player)target).playerSession.isUpdatesLocked)
return null;
if ((findFlags & TargetFindFlags.Pets) != 0)
{
return owner.pet;
}
// todo: this is beyond retarded
var oldFlags = this.findFlags;
this.findFlags = findFlags;
if (CanTarget(target, false, true))
{
this.findFlags = oldFlags;
return target;
}
this.findFlags = oldFlags;
return null;
}
}
}

View File

@@ -3,7 +3,11 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIVClassic.Common;
using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor;
using FFXIVClassic_Map_Server.actors.area;
using FFXIVClassic_Map_Server.utils;
namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
{
@@ -20,29 +24,39 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
private bool firstSpell = true;
private DateTime lastRoamScript; // todo: what even is this used as
public BattleNpcController(Character owner)
private new BattleNpc owner;
public BattleNpcController(BattleNpc owner) :
base(owner)
{
this.owner = owner;
this.lastUpdate = DateTime.Now;
this.waitTime = lastUpdate.AddSeconds(5);
}
public override void Update(DateTime tick)
{
var battleNpc = this.owner as BattleNpc;
if (battleNpc != null)
// todo: handle aggro/deaggro and other shit here
if (owner.aiContainer.IsEngaged())
{
// todo: handle aggro/deaggro and other shit here
if (battleNpc.aiContainer.IsEngaged())
{
DoCombatTick(tick);
}
else if (!battleNpc.IsDead())
{
DoRoamTick(tick);
}
battleNpc.Update(tick);
DoCombatTick(tick);
}
else if (!owner.IsDead())
{
DoRoamTick(tick);
}
}
public bool TryDeaggro()
{
if (owner.hateContainer.GetMostHatedTarget() == null || !owner.aiContainer.GetTargetFind().CanTarget(owner.target as Character))
{
return true;
}
else if (!owner.IsCloseToSpawn())
{
return true;
}
return false;
}
public override bool Engage(Character target)
@@ -53,7 +67,27 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
{
// reset casting
firstSpell = true;
// todo: find a better place to put this?
if (owner.GetState() != SetActorStatePacket.MAIN_STATE_ACTIVE)
owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE);
// 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);
owner.aiContainer.pathFind.PreparePath(target.positionX, target.positionY, target.positionZ);
ChangeTarget(target);
return false;
}
}
lastActionTime = DateTime.Now;
// todo: adjust cooldowns with modifiers
}
return canEngage;
@@ -65,10 +99,17 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
return true;
}
public override bool Disengage()
public override void Disengage()
{
var target = owner.target;
base.Disengage();
// todo:
return true;
lastActionTime = lastUpdate;
owner.isMovingToSpawn = true;
neutralTime = lastUpdate;
owner.hateContainer.ClearHate();
owner.moveState = 1;
lua.LuaEngine.CallLuaBattleAction(owner, "onDisengage", owner, target);
}
public override void Cast(Character target, uint spellId)
@@ -93,25 +134,185 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
private void DoRoamTick(DateTime tick)
{
var battleNpc = owner as BattleNpc;
if (battleNpc != null)
if (owner.hateContainer.GetHateList().Count > 0)
{
if (battleNpc.hateContainer.GetHateList().Count > 0)
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())
{
Engage(battleNpc.hateContainer.GetMostHatedTarget());
return;
owner.aiContainer.pathFind.FollowPath();
lastActionTime = tick.AddSeconds(-5);
}
else if (battleNpc.currentLockedTarget != 0)
else
{
if (tick >= lastActionTime)
{
}
}
// todo:
waitTime = tick.AddSeconds(10);
owner.OnRoam(tick);
}
}
private void DoCombatTick(DateTime tick)
{
HandleHate();
// 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;
}
var targetPos = owner.target.GetPosAsVector3();
var distance = Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, targetPos.X, targetPos.Y, targetPos.Z);
if (distance > owner.meleeRange - 0.2f || owner.aiContainer.CanFollowPath())
{
if (CanMoveForward(distance))
{
if (!owner.aiContainer.pathFind.IsFollowingPath() && distance > 3)
{
// pathfind if too far otherwise jump to target
owner.aiContainer.pathFind.SetPathFlags(distance > 3 ? PathFindFlags.None : PathFindFlags.IgnoreNav );
owner.aiContainer.pathFind.PreparePath(targetPos, 0.7f, 5);
}
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))
{
battlenpc.aiContainer.pathFind.PathInRange(targetPos, 1.5f, 1.5f);
}
}
}
}
}
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)
{
// 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;
var distance = Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, target.positionX, target.positionY, target.positionZ);
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;
}
if (detectSight && !hasInvisible && owner.IsFacing(target))
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());
}
}
}

View File

@@ -12,16 +12,20 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
protected Character owner;
protected DateTime lastUpdate;
protected bool canUpdate = true;
public bool canUpdate = true;
protected bool autoAttackEnabled = true;
protected bool castingEnabled = true;
protected bool weaponSkillEnabled = true;
protected PathFind pathFind;
protected TargetFind targetFind;
public Controller(Character owner)
{
this.owner = owner;
}
public abstract void Update(DateTime tick);
public abstract bool Engage(Character target);
public abstract bool Disengage();
public abstract void Cast(Character target, uint spellId);
public virtual void WeaponSkill(Character target, uint weaponSkillId) { }
public virtual void MonsterSkill(Character target, uint mobSkillId) { }
@@ -31,6 +35,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
public virtual void Despawn() { }
public virtual void Disengage()
{
owner.aiContainer.InternalDisengage();
}
public virtual void ChangeTarget(Character target)
{
owner.aiContainer.InternalChangeTarget(target);

View File

@@ -11,9 +11,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
{
private Character petMaster;
public PetController(Character owner)
public PetController(Character owner) :
base(owner)
{
this.owner = owner;
this.lastUpdate = Program.Tick;
}
@@ -33,10 +33,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
return true;
}
public override bool Disengage()
public override void Disengage()
{
// todo:
return true;
return;
}
public override void Cast(Character target, uint spellId)

View File

@@ -4,22 +4,22 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor;
using FFXIVClassic.Common;
namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
{
class PlayerController : Controller
{
public PlayerController(Character owner)
public PlayerController(Character owner) :
base(owner)
{
this.owner = owner;
this.lastUpdate = DateTime.Now;
}
public override void Update(DateTime tick)
{
// todo: handle player stuff on tick
((Player)this.owner).statusEffects.Update(tick);
}
public override void ChangeTarget(Character target)
@@ -29,14 +29,33 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
public override bool Engage(Character target)
{
// todo: check distance, last swing time, status effects
return true;
var canEngage = this.owner.aiContainer.InternalEngage(target);
if (canEngage)
{
// todo: find a better place to put this?
if (owner.GetState() != SetActorStatePacket.MAIN_STATE_ACTIVE)
owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE);
// todo: check speed/is able to move
// todo: too far, path to player if mob, message if player
// todo: actual stat based range
if (Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, target.positionX, target.positionY, target.positionZ) > 10)
{
owner.aiContainer.pathFind.PreparePath(target.positionX, target.positionY, target.positionZ);
ChangeTarget(target);
return false;
}
// todo: adjust cooldowns with modifiers
}
return canEngage;
}
public override bool Disengage()
public override void Disengage()
{
// todo:
return true;
return;
}
public override void Cast(Character target, uint spellId)

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIVClassic.Common;
using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor;
using FFXIVClassic_Map_Server.packets.send.actor.battle;
@@ -10,20 +11,31 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
{
class AttackState : State
{
private int damage = 0;
private bool tooFar = false;
private DateTime attackTime;
public AttackState(Character owner, Character target) :
base(owner, target)
{
owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE);
owner.aiContainer.ChangeTarget(target);
this.startTime = DateTime.Now;
attackTime = startTime;
owner.aiContainer.pathFind?.Clear();
// todo: should handle everything here instead of on next tick..
}
public override void OnStart()
{
// todo: check within attack range
owner.LookAt(target);
}
public override bool Update(DateTime tick)
{
/*
TryInterrupt();
if (interrupt)
@@ -31,13 +43,33 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
OnInterrupt();
return true;
}
// todo: check weapon delay/haste etc and use that
if ((tick - startTime).TotalMilliseconds >= 0)
*/
if (owner.target == null || target.IsDead())
{
OnComplete();
return true;
}
if (IsAttackReady())
{
if (CanAttack())
{
TryInterrupt();
// todo: check weapon delay/haste etc and use that
if (!interrupt)
{
OnComplete();
}
else
{
}
SetInterrupted(false);
}
else
{
// todo: handle interrupt/paralyze etc
}
}
return false;
}
@@ -48,18 +80,20 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public override void OnComplete()
{
var damage = utils.AttackUtils.CalculateDamage(owner, target);
damage = utils.AttackUtils.CalculateDamage(owner, target);
// onAttack(actor, target, damage)
utils.BattleUtils.DamageTarget(owner, target, damage);
lua.LuaEngine.CallLuaBattleAction(owner, "onAttack", false, owner, target, damage);
//var packet = BattleAction1Packet.BuildPacket(owner.actorId, target.actorId);
foreach (var player in owner.zone.GetActorsAroundActor<Player>(owner, 50))
player.QueuePacket(BattleActionX01Packet.BuildPacket(player.actorId, owner.actorId, target.actorId, 223001, 18, 0, (ushort)BattleActionX01PacketCommand.Attack, (ushort)damage, 0));
if (target is Player)
((Player)target).SendPacket("139.bin");
// todo: find a better place to put this?
if (owner.GetState() != SetActorStatePacket.MAIN_STATE_ACTIVE)
owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE);
isCompleted = true;
target.AddHP((short)damage);
attackTime = attackTime.AddMilliseconds(owner.GetAttackDelayMs());
//this.errorPacket = BattleActionX01Packet.BuildPacket(target.actorId, owner.actorId, target.actorId, 0, effectId, 0, (ushort)BattleActionX01PacketCommand.Attack, (ushort)damage, 0);
}
public override void TryInterrupt()
@@ -75,7 +109,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
effectId = list[0].GetStatusEffectId();
}
// todo: which is actually the swing packet
//this.errorPacket = BattleActionX01Packet.BuildPacket(target.actorId, owner.actorId, target.actorId, 0, effectId, 0, (ushort)BattleActionX01PacketCommand.Attack, 0, 0);
//this.errorPacket = BattleActionX01Packet.BuildPacket(target.actorId, owner.actorId, target.actorId, 0, effectId, 0, (ushort)BattleActionX01PacketCommand.Attack, (ushort)damage, 0);
//owner.zone.BroadcastPacketAroundActor(owner, errorPacket);
//errorPacket = null;
interrupt = true;
@@ -85,22 +119,43 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
interrupt = !CanAttack();
}
private bool IsAttackReady()
{
return Program.Tick >= attackTime;
}
private bool CanAttack()
{
if (target == null)
{
return false;
}
// todo: shouldnt need to check if owner is dead since all states would be cleared
if (owner.aiContainer.IsDead() || target.aiContainer.IsDead())
{
return false;
}
else if (target.zone != owner.zone)
else if (!owner.aiContainer.GetTargetFind().CanTarget(target, false, true))
{
return false;
}
else if (target is Player && ((Player)target).playerSession.isUpdatesLocked)
else if (Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, target.positionX, target.positionY, target.positionZ) >= 7.5f)
{
//owner.aiContainer.GetpathFind?.PreparePath(target.positionX, target.positionY, target.positionZ, 2.5f, 4);
return false;
}
return true;
}
public override void Cleanup()
{
if (owner.IsDead())
owner.Disengage();
}
public override bool CanChangeState()
{
return true;
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor;
namespace FFXIVClassic_Map_Server.actors.chara.ai.state
{
class DeathState : State
{
DateTime despawnTime;
public DeathState(Character owner, DateTime tick, uint timeToFadeOut)
: base(owner, null)
{
owner.ChangeState(SetActorStatePacket.MAIN_STATE_DEAD);
canInterrupt = false;
startTime = tick;
despawnTime = startTime.AddSeconds(timeToFadeOut);
}
public override bool Update(DateTime tick)
{
// todo: handle raise etc
if (tick >= despawnTime)
{
if (owner.currentSubState == SetActorStatePacket.SUB_STATE_PLAYER)
{
owner.ChangeState(SetActorStatePacket.MAIN_STATE_PASSIVE);
Server.GetWorldManager().DoZoneChange(((Player)owner), 244, null, 0, 15, -160.048f, 0, -165.737f, 0.0f);
}
else
{
owner.ChangeState(SetActorStatePacket.MAIN_STATE_PASSIVE);
// todo: fadeout animation and crap
//owner.zone.DespawnActor(owner);
}
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIVClassic.Common;
using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor;
using FFXIVClassic_Map_Server.packets.send.actor.battle;
namespace FFXIVClassic_Map_Server.actors.chara.ai.state
{
class MagicState : State
{
private Ability spell;
public MagicState(Character owner, Character target, ushort spellId) :
base(owner, target)
{
this.startTime = DateTime.Now;
// todo: lookup spell from global table
this.spell = Server.GetWorldManager().GetAbility(spellId);
if (spell != null)
{
if (spell.CanPlayerUse(owner, target))
OnStart();
}
}
public override void OnStart()
{
// todo: check within attack range
owner.LookAt(target);
}
public override bool Update(DateTime tick)
{
TryInterrupt();
if (interrupt)
{
OnInterrupt();
return true;
}
// todo: check weapon delay/haste etc and use that
if ((tick - startTime).TotalMilliseconds >= 0)
{
OnComplete();
return true;
}
return false;
}
public override void OnInterrupt()
{
// todo: send paralyzed/sleep message etc.
}
public override void OnComplete()
{
//this.errorPacket = BattleActionX01Packet.BuildPacket(target.actorId, owner.actorId, target.actorId, 0, effectId, 0, (ushort)BattleActionX01PacketCommand.Attack, (ushort)damage, 0);
isCompleted = true;
}
public override void TryInterrupt()
{
if (owner.statusEffects.HasStatusEffectsByFlag((uint)StatusEffectFlags.PreventAction))
{
// todo: sometimes paralyze can let you attack, get random percentage of actually letting you attack
var list = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.PreventAction);
uint effectId = 0;
if (list.Count > 0)
{
// todo: actually check proc rate/random chance of whatever effect
effectId = list[0].GetStatusEffectId();
}
// todo: which is actually the swing packet
//this.errorPacket = BattleActionX01Packet.BuildPacket(target.actorId, owner.actorId, target.actorId, 0, effectId, 0, (ushort)BattleActionX01PacketCommand.Attack, (ushort)damage, 0);
//owner.zone.BroadcastPacketAroundActor(owner, errorPacket);
//errorPacket = null;
interrupt = true;
return;
}
interrupt = !CanAttack();
}
private bool CanAttack()
{
if (target == null)
{
return false;
}
// todo: shouldnt need to check if owner is dead since all states would be cleared
if (owner.aiContainer.IsDead() || target.aiContainer.IsDead())
{
return false;
}
else if (!owner.aiContainer.GetTargetFind().CanTarget(target, false, true))
{
return false;
}
else if (Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, target.positionX, target.positionY, target.positionZ) >= 7.5f)
{
owner.aiContainer.pathFind?.PreparePath(target.positionX, target.positionY, target.positionZ, 2.5f, 4);
return false;
}
return true;
}
}
}

View File

@@ -34,7 +34,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public virtual void OnStart() { }
public virtual void OnInterrupt() { }
public virtual void OnComplete() { isCompleted = true; }
public virtual bool CanChangeState() { return false; }
public virtual void TryInterrupt() { }
public virtual void Cleanup() { }

View File

@@ -14,6 +14,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
return dmg;
}
public static int CalculateBaseDamage(Character attacker, Character defender)
{
// todo: actually calculate damage

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIVClassic_Map_Server.Actors;
namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
{
static class BattleUtils
{
public static void DamageTarget(Character attacker, Character defender, int damage)
{
// todo: other stuff too
if (defender is BattleNpc)
{
if (!((BattleNpc)defender).hateContainer.HasHateForTarget(attacker))
{
((BattleNpc)defender).hateContainer.AddBaseHate(attacker);
}
((BattleNpc)defender).hateContainer.UpdateHate(attacker, damage);
}
}
}
}