stubbed some more states

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

View File

@ -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)

View File

@ -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();

View File

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

View File

@ -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" />

View File

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

View File

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

View File

@ -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()

View File

@ -71,13 +71,17 @@ namespace FFXIVClassic_Map_Server.Actors
public Group currentParty = null;
public ContentGroup currentContentGroup = null;
public DateTime lastAiUpdate;
//public DateTime lastAiUpdate;
public AIContainer aiContainer;
public StatusEffectContainer statusEffects;
public float meleeRange;
protected uint attackDelayMs;
public CharacterTargetingAllegiance allegiance;
public Pet pet;
public Character(uint actorID) : base(actorID)
{
//Init timer array to "notimer"
@ -85,6 +89,11 @@ namespace FFXIVClassic_Map_Server.Actors
charaWork.statusShownTime[i] = 0xFFFFFFFF;
this.statusEffects = new StatusEffectContainer(this);
// todo: move this somewhere more appropriate
attackDelayMs = 4200;
meleeRange = 2.5f;
ResetMoveSpeeds();
}
public SubPacket CreateAppearancePacket()
@ -153,45 +162,7 @@ namespace FFXIVClassic_Map_Server.Actors
public void PathTo(float x, float y, float z, float stepSize = 0.70f, int maxPath = 40, float polyRadius = 0.0f)
{
var pos = new Vector3(positionX, positionY, positionZ);
var dest = new Vector3(x, y, z);
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
var path = utils.NavmeshUtils.GetPath(((Zone)GetZone()), pos, dest, stepSize, maxPath, polyRadius);
if (path != null)
{
if (oldPositionX == 0.0f && oldPositionY == 0.0f && oldPositionZ == 0.0f)
{
oldPositionX = positionX;
oldPositionY = positionY;
oldPositionZ = positionZ;
}
// todo: something went wrong
if (path.Count == 0)
{
positionX = oldPositionX;
positionY = oldPositionY;
positionZ = oldPositionZ;
}
positionUpdates = path;
this.hasMoved = true;
this.isAtSpawn = false;
sw.Stop();
((Zone)zone).pathCalls++;
((Zone)zone).pathCallTime += sw.ElapsedMilliseconds;
if (path.Count == 1)
Program.Log.Info($"mypos: {positionX} {positionY} {positionZ} | targetPos: {x} {y} {z} | step {stepSize} | maxPath {maxPath} | polyRadius {polyRadius}");
Program.Log.Error("[{0}][{1}] Created {2} points in {3} milliseconds", actorId, actorName, path.Count, sw.ElapsedMilliseconds);
}
aiContainer?.pathFind?.PreparePath(x, y, z, stepSize, maxPath, polyRadius);
}
public void FollowTarget(Actor target, float stepSize = 1.2f, int maxPath = 25, float radius = 0.0f)
@ -204,204 +175,118 @@ namespace FFXIVClassic_Map_Server.Actors
{
this.target = target;
}
this.moveState = player.moveState;
this.moveSpeeds = player.moveSpeeds;
// todo: move this to own function thing
this.oldMoveState = this.moveState;
this.moveState = 2;
updateFlags |= ActorUpdateFlags.Position | ActorUpdateFlags.Speed;
//this.moveSpeeds = player.moveSpeeds;
PathTo(player.positionX, player.positionY, player.positionZ, stepSize, maxPath, radius);
}
}
public void OnPath(Vector3 point)
public virtual void OnPath(Vector3 point)
{
if (positionUpdates != null && positionUpdates.Count > 0)
{
if (point == positionUpdates[positionUpdates.Count - 1])
{
var myPos = new Vector3(positionX, positionY, positionZ);
//point = NavmeshUtils.GetPath((Zone)zone, myPos, point, 0.35f, 1, 0.000001f, true)?[0];
}
}
lua.LuaEngine.CallLuaBattleAction(this, "onPath", this, point);
updateFlags |= ActorUpdateFlags.Position;
this.isAtSpawn = false;
}
public override void Update(DateTime tick)
{
// todo: actual ai controllers
// todo: mods to control different params instead of hardcode
// todo: other ai helpers
// time elapsed since last ai update
this.aiContainer?.Update(tick);
/*
var diffTime = (tick - lastAiUpdate);
if (this is Player)
{
// todo: handle player stuff here
}
else
{
// todo: handle mobs only?
//if (this.isStatic)
// return;
// todo: this too
if (diffTime.Milliseconds >= 10)
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];
}
}
}

View File

@ -20,7 +20,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
private Stack<State> states;
private DateTime latestUpdate;
private DateTime prevUpdate;
private PathFind pathFind;
public readonly PathFind pathFind;
private TargetFind targetFind;
private ActionQueue actionQueue;
@ -43,16 +43,24 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
// todo: trigger listeners
// todo: action queues
controller?.Update(tick);
State currState;
while (states.Count > 0 && (currState = states.Peek()).Update(tick))
{
if (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)

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ using FFXIVClassic_Map_Server.actors.area;
using FFXIVClassic_Map_Server.packets.send;
using FFXIVClassic_Map_Server.packets.send.actor;
using System.Collections.ObjectModel;
using FFXIVClassic_Map_Server.utils;
namespace FFXIVClassic_Map_Server.actors.chara.ai
{
@ -19,6 +20,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
private readonly Dictionary<uint, StatusEffect> effects;
public static readonly int MAX_EFFECTS = 20;
private bool sendUpdate = false;
public StatusEffectContainer(Character owner)
{
this.owner = owner;
@ -44,10 +46,30 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (sendUpdate)
{
owner.zone.BroadcastPacketsAroundActor(owner, owner.GetActorStatusPackets());
var propPacketUtil = new ActorPropertyPacketUtil("charaWork.status", owner);
//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)

View File

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

View File

@ -3,7 +3,11 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIVClassic.Common;
using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor;
using FFXIVClassic_Map_Server.actors.area;
using FFXIVClassic_Map_Server.utils;
namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
{
@ -20,29 +24,39 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
private bool firstSpell = true;
private DateTime lastRoamScript; // todo: what even is this used as
public BattleNpcController(Character owner)
private new BattleNpc owner;
public BattleNpcController(BattleNpc owner) :
base(owner)
{
this.owner = owner;
this.lastUpdate = DateTime.Now;
this.waitTime = lastUpdate.AddSeconds(5);
}
public override void Update(DateTime tick)
{
var battleNpc = this.owner as BattleNpc;
if (battleNpc != null)
{
// todo: handle aggro/deaggro and other shit here
if (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());
}
}
}

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIVClassic.Common;
using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor;
using FFXIVClassic_Map_Server.packets.send.actor.battle;
@ -10,20 +11,31 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
{
class AttackState : State
{
private int damage = 0;
private bool tooFar = false;
private DateTime attackTime;
public AttackState(Character owner, Character target) :
base(owner, target)
{
owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE);
owner.aiContainer.ChangeTarget(target);
this.startTime = DateTime.Now;
attackTime = startTime;
owner.aiContainer.pathFind?.Clear();
// todo: should handle everything here instead of on next tick..
}
public override void OnStart()
{
// todo: check within attack range
owner.LookAt(target);
}
public override bool Update(DateTime tick)
{
/*
TryInterrupt();
if (interrupt)
@ -31,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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
{

View File

@ -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());

View File

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

View File

@ -58,4 +58,7 @@ Global
GlobalSection(Performance) = preSolution
HasPerformanceSessions = true
EndGlobalSection
GlobalSection(Performance) = preSolution
HasPerformanceSessions = true
EndGlobalSection
EndGlobal

View File

@ -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