mirror of
https://bitbucket.org/Ioncannon/project-meteor-server.git
synced 2025-05-20 08:26:59 -04:00
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:
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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)
|
||||
|
100
FFXIVClassic Map Server/actors/chara/ai/Ability.cs
Normal file
100
FFXIVClassic Map Server/actors/chara/ai/Ability.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
44
FFXIVClassic Map Server/actors/chara/ai/state/DeathState.cs
Normal file
44
FFXIVClassic Map Server/actors/chara/ai/state/DeathState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
114
FFXIVClassic Map Server/actors/chara/ai/state/MagicState.cs
Normal file
114
FFXIVClassic Map Server/actors/chara/ai/state/MagicState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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() { }
|
||||
|
@@ -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
|
||||
|
26
FFXIVClassic Map Server/actors/chara/ai/utils/BattleUtils.cs
Normal file
26
FFXIVClassic Map Server/actors/chara/ai/utils/BattleUtils.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user