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

@@ -71,13 +71,17 @@ namespace FFXIVClassic_Map_Server.Actors
public Group currentParty = null;
public ContentGroup currentContentGroup = null;
public DateTime lastAiUpdate;
//public DateTime lastAiUpdate;
public AIContainer aiContainer;
public StatusEffectContainer statusEffects;
public float meleeRange;
protected uint attackDelayMs;
public CharacterTargetingAllegiance allegiance;
public Pet pet;
public Character(uint actorID) : base(actorID)
{
//Init timer array to "notimer"
@@ -85,6 +89,11 @@ namespace FFXIVClassic_Map_Server.Actors
charaWork.statusShownTime[i] = 0xFFFFFFFF;
this.statusEffects = new StatusEffectContainer(this);
// todo: move this somewhere more appropriate
attackDelayMs = 4200;
meleeRange = 2.5f;
ResetMoveSpeeds();
}
public SubPacket CreateAppearancePacket()
@@ -153,45 +162,7 @@ namespace FFXIVClassic_Map_Server.Actors
public void PathTo(float x, float y, float z, float stepSize = 0.70f, int maxPath = 40, float polyRadius = 0.0f)
{
var pos = new Vector3(positionX, positionY, positionZ);
var dest = new Vector3(x, y, z);
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
var path = utils.NavmeshUtils.GetPath(((Zone)GetZone()), pos, dest, stepSize, maxPath, polyRadius);
if (path != null)
{
if (oldPositionX == 0.0f && oldPositionY == 0.0f && oldPositionZ == 0.0f)
{
oldPositionX = positionX;
oldPositionY = positionY;
oldPositionZ = positionZ;
}
// todo: something went wrong
if (path.Count == 0)
{
positionX = oldPositionX;
positionY = oldPositionY;
positionZ = oldPositionZ;
}
positionUpdates = path;
this.hasMoved = true;
this.isAtSpawn = false;
sw.Stop();
((Zone)zone).pathCalls++;
((Zone)zone).pathCallTime += sw.ElapsedMilliseconds;
if (path.Count == 1)
Program.Log.Info($"mypos: {positionX} {positionY} {positionZ} | targetPos: {x} {y} {z} | step {stepSize} | maxPath {maxPath} | polyRadius {polyRadius}");
Program.Log.Error("[{0}][{1}] Created {2} points in {3} milliseconds", actorId, actorName, path.Count, sw.ElapsedMilliseconds);
}
aiContainer?.pathFind?.PreparePath(x, y, z, stepSize, maxPath, polyRadius);
}
public void FollowTarget(Actor target, float stepSize = 1.2f, int maxPath = 25, float radius = 0.0f)
@@ -204,204 +175,118 @@ namespace FFXIVClassic_Map_Server.Actors
{
this.target = target;
}
this.moveState = player.moveState;
this.moveSpeeds = player.moveSpeeds;
// todo: move this to own function thing
this.oldMoveState = this.moveState;
this.moveState = 2;
updateFlags |= ActorUpdateFlags.Position | ActorUpdateFlags.Speed;
//this.moveSpeeds = player.moveSpeeds;
PathTo(player.positionX, player.positionY, player.positionZ, stepSize, maxPath, radius);
}
}
public void OnPath(Vector3 point)
public virtual void OnPath(Vector3 point)
{
if (positionUpdates != null && positionUpdates.Count > 0)
{
if (point == positionUpdates[positionUpdates.Count - 1])
{
var myPos = new Vector3(positionX, positionY, positionZ);
//point = NavmeshUtils.GetPath((Zone)zone, myPos, point, 0.35f, 1, 0.000001f, true)?[0];
}
}
lua.LuaEngine.CallLuaBattleAction(this, "onPath", this, point);
updateFlags |= ActorUpdateFlags.Position;
this.isAtSpawn = false;
}
public override void Update(DateTime tick)
{
// todo: actual ai controllers
// todo: mods to control different params instead of hardcode
// todo: other ai helpers
// time elapsed since last ai update
this.aiContainer?.Update(tick);
/*
var diffTime = (tick - lastAiUpdate);
if (this is Player)
{
// todo: handle player stuff here
}
else
{
// todo: handle mobs only?
//if (this.isStatic)
// return;
// todo: this too
if (diffTime.Milliseconds >= 10)
{
bool foundActor = false;
// leash back to spawn
if (!isMovingToSpawn && this.oldPositionX != 0.0f && this.oldPositionY != 0.0f && this.oldPositionZ != 0.0f)
{
//var spawnDistanceSq = Utils.DistanceSquared(positionX, positionY, positionZ, oldPositionX, oldPositionY, oldPositionZ);
// todo: actual spawn leash and modifiers read from table
// set a leash to path back to spawn even if have target
// (50 yalms)
if (Utils.DistanceSquared(positionX, positionY, positionZ, oldPositionX, oldPositionY, oldPositionZ) >= 3025)
{
this.isMovingToSpawn = true;
this.target = null;
this.lastMoveUpdate = this.lastMoveUpdate.AddSeconds(-5);
this.hasMoved = false;
ClearPositionUpdates();
}
}
// check if player
if (target != null && target is Player)
{
var player = target as Player;
// deaggro if zoning/logging
// todo: player.isZoning seems to be busted
if (player.playerSession.isUpdatesLocked)
{
target = null;
ClearPositionUpdates();
}
}
Player closestPlayer = null;
float closestPlayerDistanceSq = 1000.0f;
// dont bother checking for any in-range players if going back to spawn
if (!this.isMovingToSpawn)
{
foreach (var actor in zone.GetActorsAroundActor(this, 65))
{
if (actor is Player && actor != this)
{
var player = actor as Player;
// skip if zoning/logging
// todo: player.isZoning seems to be busted
if (player != null && player.playerSession.isUpdatesLocked)
continue;
// find distance between self and target
var distanceSq = Utils.DistanceSquared(positionX, positionY, positionZ, player.positionX, player.positionY, player.positionZ);
int maxDistanceSq = player == target ? 900 : 100;
// check target isnt too far
// todo: create cone thing for IsFacing
if (distanceSq <= maxDistanceSq && distanceSq <= closestPlayerDistanceSq && (IsFacing(player) || true))
{
closestPlayerDistanceSq = distanceSq;
closestPlayer = player;
foundActor = true;
}
}
}
// found a target
if (foundActor)
{
// make sure we're not already moving so we dont spam packets
if (!hasMoved)
{
// todo: include model size and mob specific distance checks
if (closestPlayerDistanceSq >= 9)
{
FollowTarget(closestPlayer, 2.5f, 4);
}
// too close, spread out
else if (closestPlayerDistanceSq <= 0.85f)
{
QueuePositionUpdate(target.FindRandomPointAroundActor(0.65f, 0.85f));
}
// we have a target, face them
if (target != null)
{
LookAt(target);
}
}
}
}
// time elapsed since last move update
var diffMove = (tick - lastMoveUpdate);
// todo: modifier for DelayBeforeRoamToSpawn
// player disappeared
if (!foundActor && diffMove.Seconds >= 5)
{
// dont path if havent moved before
if (!hasMoved && oldPositionX != 0.0f && oldPositionY != 0.0f && oldPositionZ != 0.0f)
{
// check within spawn radius
this.isAtSpawn = Utils.DistanceSquared(positionX, positionY, positionZ, oldPositionX, oldPositionY, oldPositionZ) <= 625.0f;
// make sure we have no target
if (this.target == null)
{
// path back to spawn
if (!this.isAtSpawn)
{
PathTo(oldPositionX, oldPositionY, oldPositionZ, 2.8f);
}
// within spawn range, find a random point
else if (diffMove.Seconds >= 15)
{
// todo: polyRadius isnt euclidean distance..
// pick a random point within 10 yalms of spawn
PathTo(oldPositionX, oldPositionY, oldPositionZ, 2.5f, 7, 2.5f);
// face destination
if (positionUpdates.Count > 0)
{
var destinationPos = positionUpdates[positionUpdates.Count - 1];
LookAt(destinationPos.X, destinationPos.Y);
}
if (this.isMovingToSpawn)
{
this.isMovingToSpawn = false;
this.ResetMoveSpeedsToDefault();
this.ChangeState(SetActorStatePacket.MAIN_STATE_DEAD2);
}
}
}
}
// todo: this is retarded. actually no it isnt, i didnt deaggro if out of range..
target = null;
}
// update last ai update time to now
lastAiUpdate = DateTime.Now;
}
}
*/
}
public override void PostUpdate(DateTime tick, List<SubPacket> packets = null)
{
if (updateFlags != ActorUpdateFlags.None)
{
packets = packets ?? new List<SubPacket>();
if ((updateFlags & ActorUpdateFlags.Appearance) != 0)
{
packets.Add(new SetActorAppearancePacket(modelId, appearanceIds).BuildPacket(actorId));
}
// todo: should probably add another flag for battleTemp since all this uses reflection
if ((updateFlags & ActorUpdateFlags.HpTpMp) != 0)
{
var propPacketUtil = new ActorPropertyPacketUtil("charaWork.parameterSave", this);
//Parameters
propPacketUtil.AddProperty("charaWork.parameterSave.hp[0]");
propPacketUtil.AddProperty("charaWork.parameterSave.hpMax[0]");
propPacketUtil.AddProperty("charaWork.parameterSave.mp");
propPacketUtil.AddProperty("charaWork.parameterSave.mpMax");
propPacketUtil.AddProperty("charaWork.parameterTemp.tp");
propPacketUtil.AddProperty("charaWork.parameterSave.state_mainSkill[0]");
propPacketUtil.AddProperty("charaWork.parameterSave.state_mainSkillLevel");
//General Parameters
for (int i = 3; i < charaWork.battleTemp.generalParameter.Length; i++)
{
if (charaWork.battleTemp.generalParameter[i] != 0)
propPacketUtil.AddProperty(String.Format("charaWork.battleTemp.generalParameter[{0}]", i));
}
propPacketUtil.AddProperty("charaWork.battleTemp.castGauge_speed[0]");
propPacketUtil.AddProperty("charaWork.battleTemp.castGauge_speed[1]");
packets.AddRange(propPacketUtil.Done());
}
base.PostUpdate(tick, packets);
}
}
public virtual bool CanAttack()
{
return false;
}
public virtual bool CanCast()
{
return false;
}
public virtual uint GetAttackDelayMs()
{
return attackDelayMs;
}
public bool Engage(uint targid = 0)
{
// todo: attack the things
targid = targid == 0 ? currentTarget: targid;
if (targid != 0)
{
var targ = Server.GetWorldManager().GetActorInWorld(targid);
if (targ is Character)
aiContainer.Engage((Character)targ);
}
return false;
}
public bool Disengage()
{
if (aiContainer != null)
{
aiContainer.Disengage();
return true;
}
return false;
}
public virtual void Spawn(DateTime tick)
{
// todo: reset hp/mp/tp etc here
RecalculateHpMpTp();
}
public virtual void Die(DateTime tick)
{
// todo: actual despawn timer
aiContainer.InternalDie(tick, 10);
}
protected virtual void Despawn(DateTime tick)
@@ -418,6 +303,54 @@ namespace FFXIVClassic_Map_Server.Actors
{
return !IsDead();
}
public virtual short GetHP()
{
// todo:
return charaWork.parameterSave.hp[0];
}
public virtual short GetMaxHP()
{
return charaWork.parameterSave.hpMax[0];
}
public virtual byte GetHPP()
{
return (byte)(charaWork.parameterSave.hp[0] / charaWork.parameterSave.hpMax[0]);
}
public virtual void AddHP(short hp)
{
// todo: +/- hp and die
// todo: battlenpcs probably have way more hp?
var addHp = charaWork.parameterSave.hp[0] + hp;
addHp = addHp.Clamp(short.MinValue, charaWork.parameterSave.hpMax[0]);
charaWork.parameterSave.hp[0] = (short)addHp;
if (charaWork.parameterSave.hp[0] < 1)
Die(Program.Tick);
updateFlags |= ActorUpdateFlags.HpTpMp;
}
public virtual void DelHP(short hp)
{
AddHP((short)-hp);
}
// todo: should this include stats too?
public virtual void RecalculateHpMpTp()
{
// todo: recalculate stats and crap
updateFlags |= ActorUpdateFlags.HpTpMp;
}
public virtual float GetSpeed()
{
// todo: for battlenpc/player calculate speed
return moveSpeeds[2];
}
}
}

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);
}
}
}
}

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.actors.chara.npc;
using FFXIVClassic_Map_Server.actors;
@@ -28,7 +29,11 @@ namespace FFXIVClassic_Map_Server.Actors
{
public HateContainer hateContainer;
public AggroType aggroType;
public bool neutral;
private uint despawnTime;
private uint spawnDistance;
private float spawnX, spawnY, spawnZ;
public BattleNpc(int actorNumber, ActorClass actorClass, string uniqueId, Area spawnedArea, float posX, float posY, float posZ, float rot,
ushort actorState, uint animationId, string customDisplayName)
: base(actorNumber, actorClass, uniqueId, spawnedArea, posX, posY, posZ, rot, actorState, animationId, customDisplayName)
@@ -43,14 +48,31 @@ namespace FFXIVClassic_Map_Server.Actors
this.hateContainer = new HateContainer(this);
this.allegiance = CharacterTargetingAllegiance.BattleNpcs;
spawnX = posX;
spawnY = posY;
spawnZ = posZ;
// todo: read this from db
aggroType = AggroType.Sight;
this.moveState = 2;
ResetMoveSpeeds();
this.meleeRange = 1.5f;
despawnTime = 10;
}
public override void Update(DateTime tick)
{
// todo:
this.aiContainer.Update(tick);
this.statusEffects.Update(tick);
}
public override bool CanAttack()
{
return true;
}
///<summary> // todo: create an action object? </summary>
public bool OnAttack(AttackState state)
{
@@ -60,16 +82,77 @@ namespace FFXIVClassic_Map_Server.Actors
public override void Spawn(DateTime tick)
{
base.Spawn(tick);
this.isMovingToSpawn = false;
this.ResetMoveSpeeds();
this.ChangeState(SetActorStatePacket.MAIN_STATE_PASSIVE);
}
public override void Die(DateTime tick)
{
base.Die(tick);
if (IsAlive())
{
aiContainer.InternalDie(tick, despawnTime);
this.ResetMoveSpeeds();
this.positionX = oldPositionX;
this.positionY = oldPositionY;
this.positionZ = oldPositionZ;
this.isAtSpawn = true;
}
else
{
var err = $"[{actorId}][{customDisplayName}] {positionX} {positionY} {positionZ} {GetZoneID()} tried to die ded";
Program.Log.Error(err);
//throw new Exception(err);
}
}
public void OnRoam(DateTime tick)
{
// todo: move this to battlenpccontroller..
bool foundActor = false;
// leash back to spawn
if (!IsCloseToSpawn())
{
isMovingToSpawn = true;
aiContainer.Reset();
}
else
{
this.isMovingToSpawn = false;
}
// dont bother checking for any in-range players if going back to spawn
if (!this.isMovingToSpawn && this.aggroType != AggroType.None)
{
foreach (var player in zone.GetActorsAroundActor<Player>(this, 50))
{
uint levelDifference = (uint)Math.Abs(this.charaWork.parameterSave.state_mainSkillLevel - player.charaWork.parameterSave.state_mainSkillLevel);
if (levelDifference < 10 && ((BattleNpcController)aiContainer.GetController()).CanAggroTarget(player))
hateContainer.AddBaseHate(player);
}
}
if (target == null)
aiContainer.pathFind.PathInRange(spawnX, spawnY, spawnZ, 1.0f, 35.0f);
}
public uint GetDespawnTime()
{
return despawnTime;
}
public void SetDespawnTime(uint seconds)
{
despawnTime = seconds;
}
public bool IsCloseToSpawn()
{
return this.isAtSpawn = Utils.DistanceSquared(positionX, positionY, positionZ, spawnX, spawnY, spawnZ) <= 2500.0f;
}
}
}

View File

@@ -396,7 +396,8 @@ namespace FFXIVClassic_Map_Server.Actors
public override void Update(DateTime tick)
{
// todo: can normal npcs have status effects?
aiContainer.Update(tick);
}
//A party member list packet came, set the party

View File

@@ -611,10 +611,12 @@ namespace FFXIVClassic_Map_Server.Actors
{
try
{
// BasePacket packet = new BasePacket(path);
BasePacket packet = new BasePacket(path);
//packet.ReplaceActorID(actorId);
//QueuePacket(packet);
packet.ReplaceActorID(actorId);
var packets = packet.GetSubpackets();
QueuePackets(packets);
}
catch (Exception e)
{
@@ -1687,6 +1689,64 @@ namespace FFXIVClassic_Map_Server.Actors
LuaEngine.GetInstance().CallLuaFunction(this, this, "OnUpdate", true, delta);
}
public override void Update(DateTime tick)
{
aiContainer.Update(tick);
statusEffects.Update(tick);
}
public override void PostUpdate(DateTime tick, List<SubPacket> packets = null)
{
base.PostUpdate(tick);
}
public override short GetHP()
{
return charaWork.parameterSave.hp[currentJob];
}
public override short GetMaxHP()
{
return charaWork.parameterSave.hpMax[currentJob];
}
public override byte GetHPP()
{
return (byte)(charaWork.parameterSave.hp[currentJob] / charaWork.parameterSave.hpMax[currentJob]);
}
public override void AddHP(short hp)
{
// todo: +/- hp and die
// todo: battlenpcs probably have way more hp?
var addHp = charaWork.parameterSave.hp[currentJob] + hp;
addHp = addHp.Clamp(short.MinValue, charaWork.parameterSave.hpMax[currentJob]);
charaWork.parameterSave.hp[currentJob] = (short)addHp;
if (charaWork.parameterSave.hp[0] < 1)
Die(Program.Tick);
updateFlags |= ActorUpdateFlags.HpTpMp;
}
public override void DelHP(short hp)
{
AddHP((short)-hp);
}
// todo: should this include stats too?
public override void RecalculateHpMpTp()
{
// todo: recalculate stats and crap
updateFlags |= ActorUpdateFlags.HpTpMp;
}
public override void Die(DateTime tick)
{
// todo: death timer
aiContainer.InternalDie(tick, 60);
}
//Update all the hotbar slots past the commandborder. Commands before the commandborder only need to be sent on init since they never change
public ActorPropertyPacketUtil GetUpdateHotbarPacket(uint playerActorId)
{
@@ -1837,6 +1897,5 @@ namespace FFXIVClassic_Map_Server.Actors
return firstSlot;
}
}
}