mirror of
https://bitbucket.org/Ioncannon/project-meteor-server.git
synced 2025-04-02 19:42:05 -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:
parent
c7b87c0d89
commit
68657e1edc
@ -73,8 +73,13 @@ namespace FFXIVClassic.Common
|
||||
|
||||
public static float GetAngle(Vector3 lhs, Vector3 rhs)
|
||||
{
|
||||
var angle = (float)Math.Atan((rhs.Z - lhs.Z) / (rhs.X - lhs.X));
|
||||
return lhs.X > rhs.X ? angle + (float)Math.PI : angle;
|
||||
return GetAngle(lhs.X, lhs.Z, rhs.X, rhs.Z);
|
||||
}
|
||||
|
||||
public static float GetAngle(float x, float z, float x2, float z2)
|
||||
{
|
||||
var angle = (float)Math.Atan((z2 - z) / (x2 - x));
|
||||
return x > x2 ? angle + (float)Math.PI : angle;
|
||||
}
|
||||
|
||||
public Vector3 NewHorizontalVector(float angle, float extents)
|
||||
|
@ -32,7 +32,7 @@ namespace FFXIVClassic_Map_Server
|
||||
|
||||
internal bool DoCommand(string input, Session session)
|
||||
{
|
||||
if (!input.Any() || input.Equals(""))
|
||||
if (!input.Any() || input.Equals("") || input.Length == 1)
|
||||
return false;
|
||||
|
||||
input.Trim();
|
||||
|
@ -1833,6 +1833,66 @@ namespace FFXIVClassic_Map_Server
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<ushort, Ability> LoadGlobalAbilityList()
|
||||
{
|
||||
var abilities = new Dictionary<ushort, Ability>();
|
||||
|
||||
using (MySqlConnection conn = new MySqlConnection(String.Format("Server={0}; Port={1}; Database={2}; UID={3}; Password={4}", ConfigConstants.DATABASE_HOST, ConfigConstants.DATABASE_PORT, ConfigConstants.DATABASE_NAME, ConfigConstants.DATABASE_USERNAME, ConfigConstants.DATABASE_PASSWORD)))
|
||||
{
|
||||
try
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var query = ("SELECT id, name, classJob, level, requirements, validTarget, aoeTarget, aoeType, `range`, characterFind, statusDuration, " +
|
||||
"castTime, recastTime, mpCost, tpCost, animationType, effectAnimation, modelAnimation, animationDuration, positionBonus, procRequirement FROM abilities;");
|
||||
|
||||
MySqlCommand cmd = new MySqlCommand(query, conn);
|
||||
|
||||
using (MySqlDataReader reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetUInt16(0);
|
||||
var name = reader.GetString(1);
|
||||
var ability = new Ability(id, name);
|
||||
|
||||
ability.job = reader.GetByte(2);
|
||||
ability.level = reader.GetByte(3);
|
||||
ability.requirements = (AbilityRequirements)reader.GetUInt16(4);
|
||||
ability.validTarget = (TargetFindFlags)reader.GetByte(5);
|
||||
ability.aoeTarget = (TargetFindAOETarget)reader.GetByte(6);
|
||||
ability.aoeType = (TargetFindAOEType)reader.GetByte(7);
|
||||
ability.range = reader.GetInt32(8);
|
||||
ability.characterFind = (TargetFindCharacterType)reader.GetByte(9);
|
||||
ability.statusDurationSeconds = reader.GetUInt32(10);
|
||||
ability.castTimeSeconds = reader.GetUInt32(11);
|
||||
ability.recastTimeSeconds = reader.GetUInt32(12);
|
||||
ability.mpCost = reader.GetUInt16(13);
|
||||
ability.tpCost = reader.GetUInt16(14);
|
||||
ability.animationType = reader.GetByte(15);
|
||||
ability.effectAnimation = reader.GetUInt16(16);
|
||||
ability.modelAnimation = reader.GetUInt16(17);
|
||||
ability.animationDurationSeconds = reader.GetUInt16(18);
|
||||
ability.positionBonus = (AbilityPositionBonus)reader.GetByte(19);
|
||||
ability.procRequirement = (AbilityProcRequirement)reader.GetByte(20);
|
||||
|
||||
abilities.Add(id, ability);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (MySqlException e)
|
||||
{
|
||||
Program.Log.Error(e.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
conn.Dispose();
|
||||
}
|
||||
}
|
||||
return abilities;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -93,12 +93,16 @@
|
||||
<Compile Include="actors\chara\ai\controllers\PlayerController.cs" />
|
||||
<Compile Include="actors\chara\ai\HateContainer.cs" />
|
||||
<Compile Include="actors\chara\ai\PathFind.cs" />
|
||||
<Compile Include="actors\chara\ai\Ability.cs" />
|
||||
<Compile Include="actors\chara\ai\state\AttackState.cs" />
|
||||
<Compile Include="actors\chara\ai\state\DeathState.cs" />
|
||||
<Compile Include="actors\chara\ai\state\MagicState.cs" />
|
||||
<Compile Include="actors\chara\ai\state\State.cs" />
|
||||
<Compile Include="actors\chara\ai\StatusEffect.cs" />
|
||||
<Compile Include="actors\chara\ai\StatusEffectContainer.cs" />
|
||||
<Compile Include="actors\chara\ai\TargetFind.cs" />
|
||||
<Compile Include="actors\chara\ai\utils\AttackUtils.cs" />
|
||||
<Compile Include="actors\chara\ai\utils\BattleUtils.cs" />
|
||||
<Compile Include="actors\chara\npc\ActorClass.cs" />
|
||||
<Compile Include="actors\chara\npc\BattleNpc.cs" />
|
||||
<Compile Include="actors\chara\npc\NpcWork.cs" />
|
||||
|
@ -55,6 +55,7 @@ namespace FFXIVClassic_Map_Server
|
||||
mWorldManager.LoadSpawnLocations();
|
||||
mWorldManager.SpawnAllActors();
|
||||
mWorldManager.LoadStatusEffects();
|
||||
mWorldManager.LoadAbilities();
|
||||
mWorldManager.StartZoneThread();
|
||||
|
||||
IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(ConfigConstants.OPTIONS_BINDIP), int.Parse(ConfigConstants.OPTIONS_PORT));
|
||||
|
@ -36,7 +36,8 @@ namespace FFXIVClassic_Map_Server
|
||||
private Dictionary<uint, ZoneEntrance> zoneEntranceList;
|
||||
private Dictionary<uint, ActorClass> actorClasses = new Dictionary<uint,ActorClass>();
|
||||
private Dictionary<ulong, Party> currentPlayerParties = new Dictionary<ulong, Party>(); //GroupId, Party object
|
||||
private Dictionary<uint, StatusEffect> effectList = new Dictionary<uint, StatusEffect>(); // todo: load these in from a db table
|
||||
private Dictionary<uint, StatusEffect> effectList = new Dictionary<uint, StatusEffect>();
|
||||
private Dictionary<ushort, Ability> abilityList = new Dictionary<ushort, Ability>();
|
||||
|
||||
private Server mServer;
|
||||
|
||||
@ -586,7 +587,6 @@ namespace FFXIVClassic_Map_Server
|
||||
{
|
||||
oldZone.RemoveActorFromZone(player);
|
||||
}
|
||||
|
||||
newArea.AddActorToZone(player);
|
||||
|
||||
//Update player actor's properties
|
||||
@ -761,6 +761,8 @@ namespace FFXIVClassic_Map_Server
|
||||
}
|
||||
|
||||
public void ReloadZone(uint zoneId)
|
||||
{
|
||||
lock (zoneList)
|
||||
{
|
||||
if (!zoneList.ContainsKey(zoneId))
|
||||
return;
|
||||
@ -768,7 +770,7 @@ namespace FFXIVClassic_Map_Server
|
||||
Zone zone = zoneList[zoneId];
|
||||
//zone.clear();
|
||||
//LoadNPCs(zone.actorId);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public ContentGroup CreateContentGroup(Director director, params Actor[] actors)
|
||||
@ -1038,6 +1040,8 @@ namespace FFXIVClassic_Map_Server
|
||||
}
|
||||
|
||||
public Actor GetActorInWorld(uint charId)
|
||||
{
|
||||
lock (zoneList)
|
||||
{
|
||||
foreach (Zone zone in zoneList.Values)
|
||||
{
|
||||
@ -1045,10 +1049,13 @@ namespace FFXIVClassic_Map_Server
|
||||
if (a != null)
|
||||
return a;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Actor GetActorInWorldByUniqueId(string uid)
|
||||
{
|
||||
lock (zoneList)
|
||||
{
|
||||
foreach (Zone zone in zoneList.Values)
|
||||
{
|
||||
@ -1056,23 +1063,31 @@ namespace FFXIVClassic_Map_Server
|
||||
if (a != null)
|
||||
return a;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Zone GetZone(uint zoneId)
|
||||
{
|
||||
lock (zoneList)
|
||||
{
|
||||
if (!zoneList.ContainsKey(zoneId))
|
||||
return null;
|
||||
|
||||
return zoneList[zoneId];
|
||||
}
|
||||
}
|
||||
|
||||
public PrivateArea GetPrivateArea(uint zoneId, string privateArea, uint privateAreaType)
|
||||
{
|
||||
lock (zoneList)
|
||||
{
|
||||
if (!zoneList.ContainsKey(zoneId))
|
||||
return null;
|
||||
|
||||
return zoneList[zoneId].GetPrivateArea(privateArea, privateAreaType);
|
||||
}
|
||||
}
|
||||
|
||||
public WorldMaster GetActor()
|
||||
{
|
||||
@ -1135,6 +1150,17 @@ namespace FFXIVClassic_Map_Server
|
||||
|
||||
return effectList.TryGetValue(id, out effect) ? new StatusEffect(null, effect) : null;
|
||||
}
|
||||
|
||||
public void LoadAbilities()
|
||||
{
|
||||
abilityList = Database.LoadGlobalAbilityList();
|
||||
}
|
||||
|
||||
public Ability GetAbility(ushort id)
|
||||
{
|
||||
Ability ability;
|
||||
return abilityList.TryGetValue(id, out ability) ? ability.Clone() : null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,6 +13,22 @@ using FFXIVClassic_Map_Server.packets.send.actor.battle;
|
||||
|
||||
namespace FFXIVClassic_Map_Server.Actors
|
||||
{
|
||||
[Flags]
|
||||
enum ActorUpdateFlags
|
||||
{
|
||||
None = 0x00,
|
||||
Position = 0x01,
|
||||
HpTpMp = 0x02,
|
||||
State = 0x04,
|
||||
Combat = 0x07,
|
||||
Name = 0x08,
|
||||
Appearance = 0x10,
|
||||
Speed = 0x20,
|
||||
|
||||
AllNpc = 0x2F,
|
||||
AllPlayer = 0x3F
|
||||
}
|
||||
|
||||
class Actor
|
||||
{
|
||||
public uint actorId;
|
||||
@ -41,7 +57,7 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
public string className;
|
||||
public List<LuaParam> classParams;
|
||||
|
||||
public List<Vector3> positionUpdates = new List<Vector3>();
|
||||
public List<Vector3> positionUpdates;
|
||||
public DateTime lastMoveUpdate;
|
||||
protected DateTime lastUpdate;
|
||||
public Actor target;
|
||||
@ -49,6 +65,8 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
public bool hasMoved = false;
|
||||
public bool isAtSpawn = true;
|
||||
|
||||
public ActorUpdateFlags updateFlags;
|
||||
|
||||
public EventList eventConditions;
|
||||
|
||||
public Actor(uint actorId)
|
||||
@ -84,7 +102,7 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetMoveSpeedsToDefault()
|
||||
public virtual void ResetMoveSpeeds()
|
||||
{
|
||||
this.moveSpeeds[0] = SetActorSpeedPacket.DEFAULT_STOP;
|
||||
this.moveSpeeds[1] = SetActorSpeedPacket.DEFAULT_WALK;
|
||||
@ -93,7 +111,7 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
|
||||
// todo: make this halal
|
||||
this.moveState = this.oldMoveState;
|
||||
hasMoved = true;
|
||||
this.updateFlags |= ActorUpdateFlags.Speed;
|
||||
}
|
||||
|
||||
public SubPacket CreateAddActorPacket(byte val)
|
||||
@ -152,38 +170,10 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
return spawnPacket;
|
||||
}
|
||||
|
||||
public SubPacket CreatePositionUpdatePacket(bool forceUpdate = false)
|
||||
public SubPacket CreatePositionUpdatePacket()
|
||||
{
|
||||
int updateMs = 300;
|
||||
var diffTime = (DateTime.Now - lastMoveUpdate);
|
||||
|
||||
if (this.target != null)
|
||||
{
|
||||
updateMs = 150;
|
||||
}
|
||||
|
||||
if (forceUpdate || (hasMoved && ((this is Player) || diffTime.TotalMilliseconds >= updateMs)))
|
||||
{
|
||||
hasMoved = (this.positionUpdates != null && this.positionUpdates.Count > 0);
|
||||
if (hasMoved)
|
||||
{
|
||||
var pos = positionUpdates[0];
|
||||
|
||||
if (this is Character)
|
||||
((Character)this).OnPath(pos);
|
||||
|
||||
positionX = pos.X;
|
||||
positionY = pos.Y;
|
||||
positionZ = pos.Z;
|
||||
//Program.Server.GetInstance().mLuaEngine.OnPath(actor, position, positionUpdates)
|
||||
|
||||
positionUpdates.RemoveAt(0);
|
||||
}
|
||||
lastMoveUpdate = DateTime.Now;
|
||||
return MoveActorToPositionPacket.BuildPacket(actorId, positionX, positionY, positionZ, rotation, moveState);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public SubPacket CreateStatePacket()
|
||||
{
|
||||
@ -374,19 +364,18 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
}
|
||||
|
||||
public void ChangeState(ushort newState)
|
||||
{
|
||||
if (newState != currentMainState)
|
||||
{
|
||||
currentMainState = newState;
|
||||
SubPacket ChangeStatePacket = SetActorStatePacket.BuildPacket(actorId, newState, currentSubState);
|
||||
SubPacket battleActionPacket = BattleActionX01Packet.BuildPacket(actorId, actorId, actorId, 0x72000062, 1, 0, 0x05209, 0, 0);
|
||||
zone.BroadcastPacketAroundActor(this, ChangeStatePacket);
|
||||
zone.BroadcastPacketAroundActor(this, battleActionPacket);
|
||||
updateFlags |= ActorUpdateFlags.State;
|
||||
}
|
||||
}
|
||||
|
||||
public void ChangeSpeed(int type, float value)
|
||||
{
|
||||
moveSpeeds[type] = value;
|
||||
SubPacket ChangeSpeedPacket = SetActorSpeedPacket.BuildPacket(actorId, moveSpeeds[0], moveSpeeds[1], moveSpeeds[2], moveSpeeds[3]);
|
||||
zone.BroadcastPacketAroundActor(this, ChangeSpeedPacket);
|
||||
updateFlags |= ActorUpdateFlags.Speed;
|
||||
}
|
||||
|
||||
public void ChangeSpeed(float speedStop, float speedWalk, float speedRun, float speedActive)
|
||||
@ -395,8 +384,7 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
moveSpeeds[1] = speedWalk;
|
||||
moveSpeeds[2] = speedRun;
|
||||
moveSpeeds[3] = speedActive;
|
||||
SubPacket ChangeSpeedPacket = SetActorSpeedPacket.BuildPacket(actorId, moveSpeeds[0], moveSpeeds[1], moveSpeeds[2], moveSpeeds[3]);
|
||||
zone.BroadcastPacketAroundActor(this, ChangeSpeedPacket);
|
||||
updateFlags |= ActorUpdateFlags.Speed;
|
||||
}
|
||||
|
||||
public virtual void Update(DateTime tick)
|
||||
@ -404,6 +392,48 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
|
||||
}
|
||||
|
||||
public virtual void PostUpdate(DateTime tick, List<SubPacket> packets = null)
|
||||
{
|
||||
if (updateFlags != ActorUpdateFlags.None)
|
||||
{
|
||||
packets = packets ?? new List<SubPacket>();
|
||||
if ((updateFlags & ActorUpdateFlags.Position) != 0)
|
||||
{
|
||||
if (positionUpdates != null && positionUpdates.Count > 0)
|
||||
{
|
||||
// push latest for player
|
||||
var pos = positionUpdates?[currentSubState == SetActorStatePacket.SUB_STATE_PLAYER ? positionUpdates.Count - 1 : 0];
|
||||
|
||||
positionX = pos.X;
|
||||
positionY = pos.Y;
|
||||
positionZ = pos.Z;
|
||||
//Program.Server.GetInstance().mLuaEngine.OnPath(actor, position, positionUpdates)
|
||||
|
||||
positionUpdates.RemoveAt(0);
|
||||
}
|
||||
lastMoveUpdate = DateTime.Now;
|
||||
packets.Add(CreatePositionUpdatePacket());
|
||||
}
|
||||
|
||||
if ((updateFlags & ActorUpdateFlags.Speed) != 0)
|
||||
{
|
||||
packets.Add(SetActorSpeedPacket.BuildPacket(actorId, moveSpeeds[0], moveSpeeds[1], moveSpeeds[2], moveSpeeds[3]));
|
||||
}
|
||||
|
||||
if ((updateFlags & ActorUpdateFlags.Name) != 0)
|
||||
{
|
||||
packets.Add(SetActorNamePacket.BuildPacket(actorId, displayNameId, customDisplayName));
|
||||
}
|
||||
|
||||
if ((updateFlags & ActorUpdateFlags.State) != 0)
|
||||
{
|
||||
packets.Add(SetActorStatePacket.BuildPacket(actorId, currentMainState, currentSubState));
|
||||
packets.Add(BattleActionX01Packet.BuildPacket(actorId, actorId, actorId, 0x72000062, 1, 0, 0x05209, 0, 0));
|
||||
}
|
||||
updateFlags = ActorUpdateFlags.None;
|
||||
zone.BroadcastPacketsAroundActor(this, packets);
|
||||
}
|
||||
}
|
||||
|
||||
public void GenerateActorName(int actorNumber)
|
||||
{
|
||||
@ -518,7 +548,7 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
if (value.GetType() == curObj.GetType())
|
||||
parentObj.GetType().GetField(split[split.Length - 1]).SetValue(parentObj, value);
|
||||
else
|
||||
parentObj.GetType().GetField(split[split.Length-1]).SetValue(parentObj, TypeDescriptor.GetConverter(value.GetType()).ConvertTo(value, curObj.GetType()));
|
||||
parentObj.GetType().GetField(split[split.Length - 1]).SetValue(parentObj, TypeDescriptor.GetConverter(value.GetType()).ConvertTo(value, curObj.GetType()));
|
||||
|
||||
SetActorPropetyPacket changeProperty = new SetActorPropetyPacket(uiFunc);
|
||||
changeProperty.AddProperty(this, name);
|
||||
@ -592,27 +622,6 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
return rot1 == (float)dRot;
|
||||
}
|
||||
|
||||
// todo: do this properly
|
||||
public bool IsFacing(Actor target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
Program.Log.Error("[{0}][{1}] IsFacing no target!", actorId, actorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
var rot1 = this.rotation;
|
||||
|
||||
var dX = this.positionX - target.positionX;
|
||||
var dY = this.positionY - target.positionY;
|
||||
|
||||
var rot2 = Math.Atan2(dY, dX);
|
||||
|
||||
var dRot = Math.PI - rot2 + Math.PI / 2;
|
||||
|
||||
return rot1 == (float)dRot;
|
||||
}
|
||||
|
||||
public void LookAt(Actor actor)
|
||||
{
|
||||
if (actor != null)
|
||||
@ -637,19 +646,34 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
var dRot = Math.PI - rot2 + Math.PI / 2;
|
||||
|
||||
// pending move, dont need to unset it
|
||||
if (!hasMoved)
|
||||
hasMoved = rot1 != (float)dRot;
|
||||
|
||||
this.updateFlags = (rotation != (float)dRot) ? updateFlags |= ActorUpdateFlags.Position : updateFlags;
|
||||
rotation = (float)dRot;
|
||||
}
|
||||
|
||||
public bool IsFacing(float x, float z, float angle = 40.0f)
|
||||
{
|
||||
return Vector3.GetAngle(positionX, positionZ, x, z) < angle;
|
||||
}
|
||||
|
||||
// todo: is this legit?
|
||||
public bool IsFacing(Actor target, float angle = 40.0f)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
Program.Log.Error("[{0}][{1}] IsFacing no target!", actorId, actorName);
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsFacing(target.positionX, target.positionY, angle);
|
||||
}
|
||||
|
||||
public void QueuePositionUpdate(Vector3 pos)
|
||||
{
|
||||
if (positionUpdates == null)
|
||||
positionUpdates = new List<Vector3>();
|
||||
|
||||
positionUpdates.Add(pos);
|
||||
this.hasMoved = true;
|
||||
this.updateFlags |= ActorUpdateFlags.Position;
|
||||
}
|
||||
|
||||
public void QueuePositionUpdate(float x, float y, float z)
|
||||
@ -662,15 +686,27 @@ namespace FFXIVClassic_Map_Server.Actors
|
||||
positionUpdates.Clear();
|
||||
}
|
||||
|
||||
public Vector3 FindRandomPointAroundActor(float minRadius, float maxRadius)
|
||||
public Vector3 FindRandomPoint(float x, float y, float z, float minRadius, float maxRadius)
|
||||
{
|
||||
var angle = Program.Random.NextDouble() * Math.PI * 2;
|
||||
var radius = Math.Sqrt(Program.Random.NextDouble() * (maxRadius - minRadius)) + minRadius;
|
||||
|
||||
float x = (float)(radius * Math.Cos(angle));
|
||||
float z = (float)(radius * Math.Sin(angle));
|
||||
return new Vector3(x + (float)(radius * Math.Cos(angle)), y, z + (float)(radius * Math.Sin(angle)));
|
||||
}
|
||||
|
||||
return new Vector3(positionX + x, positionY, positionZ + z);
|
||||
public Vector3 FindRandomPointAroundTarget(Actor target, float minRadius, float maxRadius)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
Program.Log.Error($"[{this.actorId}][{this.customDisplayName}] FindRandomPointAroundTarget: no target found!");
|
||||
return GetPosAsVector3();
|
||||
}
|
||||
return FindRandomPoint(target.positionX, target.positionY, target.positionZ, minRadius, maxRadius);
|
||||
}
|
||||
|
||||
public Vector3 FindRandomPointAroundActor(float minRadius, float maxRadius)
|
||||
{
|
||||
return FindRandomPoint(positionX, positionY, positionZ, minRadius, maxRadius);
|
||||
}
|
||||
|
||||
public Player GetAsPlayer()
|
||||
|
@ -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)
|
||||
public override void PostUpdate(DateTime tick, List<SubPacket> packets = null)
|
||||
{
|
||||
bool foundActor = false;
|
||||
if (updateFlags != ActorUpdateFlags.None)
|
||||
{
|
||||
packets = packets ?? new List<SubPacket>();
|
||||
|
||||
// leash back to spawn
|
||||
if (!isMovingToSpawn && this.oldPositionX != 0.0f && this.oldPositionY != 0.0f && this.oldPositionZ != 0.0f)
|
||||
if ((updateFlags & ActorUpdateFlags.Appearance) != 0)
|
||||
{
|
||||
//var spawnDistanceSq = Utils.DistanceSquared(positionX, positionY, positionZ, oldPositionX, oldPositionY, oldPositionZ);
|
||||
packets.Add(new SetActorAppearancePacket(modelId, appearanceIds).BuildPacket(actorId));
|
||||
}
|
||||
|
||||
// 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)
|
||||
// todo: should probably add another flag for battleTemp since all this uses reflection
|
||||
if ((updateFlags & ActorUpdateFlags.HpTpMp) != 0)
|
||||
{
|
||||
this.isMovingToSpawn = true;
|
||||
this.target = null;
|
||||
this.lastMoveUpdate = this.lastMoveUpdate.AddSeconds(-5);
|
||||
this.hasMoved = false;
|
||||
ClearPositionUpdates();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// check if player
|
||||
if (target != null && target is Player)
|
||||
public virtual bool CanAttack()
|
||||
{
|
||||
var player = target as Player;
|
||||
|
||||
// deaggro if zoning/logging
|
||||
// todo: player.isZoning seems to be busted
|
||||
if (player.playerSession.isUpdatesLocked)
|
||||
{
|
||||
target = null;
|
||||
ClearPositionUpdates();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Player closestPlayer = null;
|
||||
float closestPlayerDistanceSq = 1000.0f;
|
||||
|
||||
// dont bother checking for any in-range players if going back to spawn
|
||||
if (!this.isMovingToSpawn)
|
||||
public virtual bool CanCast()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// found a target
|
||||
if (foundActor)
|
||||
public virtual uint GetAttackDelayMs()
|
||||
{
|
||||
// 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));
|
||||
return attackDelayMs;
|
||||
}
|
||||
|
||||
// we have a target, face them
|
||||
if (target != null)
|
||||
public bool Engage(uint targid = 0)
|
||||
{
|
||||
LookAt(target);
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
// time elapsed since last move update
|
||||
var diffMove = (tick - lastMoveUpdate);
|
||||
public bool Disengage()
|
||||
{
|
||||
if (aiContainer != null)
|
||||
{
|
||||
aiContainer.Disengage();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 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 (currState == GetCurrentState())
|
||||
if (controller == null && pathFind != null)
|
||||
{
|
||||
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);
|
||||
|
||||
//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;
|
||||
}
|
||||
}
|
||||
|
||||
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 (battleNpc.aiContainer.IsEngaged())
|
||||
if (owner.aiContainer.IsEngaged())
|
||||
{
|
||||
DoCombatTick(tick);
|
||||
}
|
||||
else if (!battleNpc.IsDead())
|
||||
else if (!owner.IsDead())
|
||||
{
|
||||
DoRoamTick(tick);
|
||||
}
|
||||
battleNpc.Update(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(battleNpc.hateContainer.GetMostHatedTarget());
|
||||
Engage(owner.hateContainer.GetMostHatedTarget());
|
||||
return;
|
||||
}
|
||||
else if (battleNpc.currentLockedTarget != 0)
|
||||
//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())
|
||||
{
|
||||
owner.aiContainer.pathFind.FollowPath();
|
||||
lastActionTime = tick.AddSeconds(-5);
|
||||
}
|
||||
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,12 +43,32 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
|
||||
OnInterrupt();
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
if (owner.target == null || target.IsDead())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (IsAttackReady())
|
||||
{
|
||||
if (CanAttack())
|
||||
{
|
||||
TryInterrupt();
|
||||
|
||||
// todo: check weapon delay/haste etc and use that
|
||||
if ((tick - startTime).TotalMilliseconds >= 0)
|
||||
if (!interrupt)
|
||||
{
|
||||
OnComplete();
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ namespace FFXIVClassic_Map_Server.dataobjects
|
||||
public void QueuePacket(SubPacket subPacket)
|
||||
{
|
||||
subPacket.SetTargetId(id);
|
||||
Server.GetWorldConnection().QueuePacket(subPacket);
|
||||
Server.GetWorldConnection()?.QueuePacket(subPacket);
|
||||
}
|
||||
|
||||
public Player GetActor()
|
||||
@ -119,12 +119,12 @@ namespace FFXIVClassic_Map_Server.dataobjects
|
||||
{
|
||||
//Don't send for static characters (npcs)
|
||||
// todo: this is retarded, need actual mob class
|
||||
if (actor is Character && ((Character)actor).isStatic)
|
||||
continue;
|
||||
//if (actor is Character && ((Character)actor).isStatic)
|
||||
// continue;
|
||||
|
||||
var packet = actor.CreatePositionUpdatePacket();
|
||||
if (packet != null)
|
||||
QueuePacket(packet);
|
||||
//var packet = actor.CreatePositionUpdatePacket();
|
||||
//if (packet != null)
|
||||
// QueuePacket(packet);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -491,9 +491,13 @@ namespace FFXIVClassic_Map_Server.lua
|
||||
public static void RunGMCommand(Player player, String cmd, string[] param, bool help = false)
|
||||
{
|
||||
bool playerNull = player == null;
|
||||
if (playerNull && param.Length >= 2)
|
||||
player = Server.GetWorldManager().GetPCInWorld(param[1].Contains("\"") ? param[1] : param[1] + " " + param[2]);
|
||||
|
||||
if (playerNull)
|
||||
{
|
||||
if (param.Length >= 2 && param[1].Contains("\""))
|
||||
player = Server.GetWorldManager().GetPCInWorld(param[1]);
|
||||
else if (param.Length > 2)
|
||||
player = Server.GetWorldManager().GetPCInWorld(param[1] + param[2]);
|
||||
}
|
||||
// load from scripts/commands/gm/ directory
|
||||
var path = String.Format("./scripts/commands/gm/{0}.lua", cmd.ToLower());
|
||||
|
||||
|
@ -16,11 +16,35 @@ namespace FFXIVClassic_Map_Server.utils
|
||||
{
|
||||
|
||||
// navmesh
|
||||
public static bool CanSee(float x1, float y1, float z1, float x2, float y2, float z2)
|
||||
public static bool CanSee(actors.area.Zone zone, float x1, float y1, float z1, float x2, float y2, float z2)
|
||||
{
|
||||
// todo: prolly shouldnt raycast
|
||||
var navMesh = zone.tiledNavMesh;
|
||||
if (navMesh != null)
|
||||
{
|
||||
var navMeshQuery = zone.navMeshQuery;
|
||||
|
||||
NavPoint startPt, endPt;
|
||||
SharpNav.Pathfinding.Path path = new SharpNav.Pathfinding.Path();
|
||||
|
||||
RaycastHit hit = new RaycastHit();
|
||||
|
||||
SharpNav.Geometry.Vector3 c = new SharpNav.Geometry.Vector3(x1, y1, z1);
|
||||
SharpNav.Geometry.Vector3 ep = new SharpNav.Geometry.Vector3(x2, y2, z2);
|
||||
|
||||
SharpNav.Geometry.Vector3 e = new SharpNav.Geometry.Vector3(5, 5, 5);
|
||||
navMeshQuery.FindNearestPoly(ref c, ref e, out startPt);
|
||||
navMeshQuery.FindNearestPoly(ref ep, ref e, out endPt);
|
||||
|
||||
|
||||
if (navMeshQuery.Raycast(ref startPt, ref ep, RaycastOptions.None, out hit, path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static SharpNav.TiledNavMesh LoadNavmesh(TiledNavMesh navmesh, string filePath)
|
||||
{
|
||||
@ -61,7 +85,7 @@ namespace FFXIVClassic_Map_Server.utils
|
||||
// no point pathing if in range
|
||||
if (distanceSquared < 4 && Math.Abs(startVec.Y - endVec.Y) < 1.1f)
|
||||
{
|
||||
return null;
|
||||
return new List<Vector3>() { endVec };
|
||||
}
|
||||
|
||||
var smoothPath = new List<Vector3>(pathSize) { };
|
||||
|
@ -58,4 +58,7 @@ Global
|
||||
GlobalSection(Performance) = preSolution
|
||||
HasPerformanceSessions = true
|
||||
EndGlobalSection
|
||||
GlobalSection(Performance) = preSolution
|
||||
HasPerformanceSessions = true
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
@ -11,12 +11,12 @@ Switches between active and passive mode states
|
||||
function onEventStarted(player, command, triggerName)
|
||||
|
||||
if (player:GetState() == 0) then
|
||||
player:ChangeState(2);
|
||||
player.Engage();
|
||||
elseif (player:GetState() == 2) then
|
||||
player:ChangeState(0);
|
||||
player.Disengage();
|
||||
end
|
||||
player:endEvent();
|
||||
|
||||
sendSignal("playerActive");
|
||||
|
||||
end
|
Loading…
Reference in New Issue
Block a user