diff --git a/FFXIVClassic Common Class Lib/Vector3.cs b/FFXIVClassic Common Class Lib/Vector3.cs index c6cc3d07..17dec44b 100644 --- a/FFXIVClassic Common Class Lib/Vector3.cs +++ b/FFXIVClassic Common Class Lib/Vector3.cs @@ -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) diff --git a/FFXIVClassic Map Server/CommandProcessor.cs b/FFXIVClassic Map Server/CommandProcessor.cs index 283c78c5..2ad53257 100644 --- a/FFXIVClassic Map Server/CommandProcessor.cs +++ b/FFXIVClassic Map Server/CommandProcessor.cs @@ -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(); diff --git a/FFXIVClassic Map Server/Database.cs b/FFXIVClassic Map Server/Database.cs index 034c8928..41a4afb4 100644 --- a/FFXIVClassic Map Server/Database.cs +++ b/FFXIVClassic Map Server/Database.cs @@ -1833,6 +1833,66 @@ namespace FFXIVClassic_Map_Server } } } + + public static Dictionary LoadGlobalAbilityList() + { + var abilities = new Dictionary(); + + 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; + } + } } diff --git a/FFXIVClassic Map Server/FFXIVClassic Map Server.csproj b/FFXIVClassic Map Server/FFXIVClassic Map Server.csproj index 0ebd9e8b..992b0837 100644 --- a/FFXIVClassic Map Server/FFXIVClassic Map Server.csproj +++ b/FFXIVClassic Map Server/FFXIVClassic Map Server.csproj @@ -93,12 +93,16 @@ + + + + diff --git a/FFXIVClassic Map Server/Server.cs b/FFXIVClassic Map Server/Server.cs index 9cd1550d..cc01b26e 100644 --- a/FFXIVClassic Map Server/Server.cs +++ b/FFXIVClassic Map Server/Server.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)); diff --git a/FFXIVClassic Map Server/WorldManager.cs b/FFXIVClassic Map Server/WorldManager.cs index a552b8a6..52cbee0d 100644 --- a/FFXIVClassic Map Server/WorldManager.cs +++ b/FFXIVClassic Map Server/WorldManager.cs @@ -36,7 +36,8 @@ namespace FFXIVClassic_Map_Server private Dictionary zoneEntranceList; private Dictionary actorClasses = new Dictionary(); private Dictionary currentPlayerParties = new Dictionary(); //GroupId, Party object - private Dictionary effectList = new Dictionary(); // todo: load these in from a db table + private Dictionary effectList = new Dictionary(); + private Dictionary abilityList = new Dictionary(); private Server mServer; @@ -586,7 +587,6 @@ namespace FFXIVClassic_Map_Server { oldZone.RemoveActorFromZone(player); } - newArea.AddActorToZone(player); //Update player actor's properties @@ -762,13 +762,15 @@ namespace FFXIVClassic_Map_Server public void ReloadZone(uint zoneId) { - if (!zoneList.ContainsKey(zoneId)) - return; - - Zone zone = zoneList[zoneId]; - //zone.clear(); - //LoadNPCs(zone.actorId); + lock (zoneList) + { + if (!zoneList.ContainsKey(zoneId)) + return; + Zone zone = zoneList[zoneId]; + //zone.clear(); + //LoadNPCs(zone.actorId); + } } public ContentGroup CreateContentGroup(Director director, params Actor[] actors) @@ -1038,40 +1040,53 @@ namespace FFXIVClassic_Map_Server } public Actor GetActorInWorld(uint charId) - { - foreach (Zone zone in zoneList.Values) + { + lock (zoneList) { - Actor a = zone.FindActorInZone(charId); - if (a != null) - return a; + foreach (Zone zone in zoneList.Values) + { + Actor a = zone.FindActorInZone(charId); + if (a != null) + return a; + } } return null; } public Actor GetActorInWorldByUniqueId(string uid) { - foreach (Zone zone in zoneList.Values) + lock (zoneList) { - Actor a = zone.FindActorInZoneByUniqueID(uid); - if (a != null) - return a; + foreach (Zone zone in zoneList.Values) + { + Actor a = zone.FindActorInZoneByUniqueID(uid); + if (a != null) + return a; + } } return null; } public Zone GetZone(uint zoneId) { - if (!zoneList.ContainsKey(zoneId)) - return null; - return zoneList[zoneId]; + lock (zoneList) + { + if (!zoneList.ContainsKey(zoneId)) + return null; + + return zoneList[zoneId]; + } } public PrivateArea GetPrivateArea(uint zoneId, string privateArea, uint privateAreaType) { - if (!zoneList.ContainsKey(zoneId)) - return null; + lock (zoneList) + { + if (!zoneList.ContainsKey(zoneId)) + return null; - return zoneList[zoneId].GetPrivateArea(privateArea, privateAreaType); + 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; + } } } diff --git a/FFXIVClassic Map Server/actors/Actor.cs b/FFXIVClassic Map Server/actors/Actor.cs index d45a0242..53b5be94 100644 --- a/FFXIVClassic Map Server/actors/Actor.cs +++ b/FFXIVClassic Map Server/actors/Actor.cs @@ -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 classParams; - public List positionUpdates = new List(); + public List 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,37 +170,9 @@ 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; + return MoveActorToPositionPacket.BuildPacket(actorId, positionX, positionY, positionZ, rotation, moveState); } public SubPacket CreateStatePacket() @@ -375,18 +365,17 @@ namespace FFXIVClassic_Map_Server.Actors public void ChangeState(ushort newState) { - 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); + if (newState != currentMainState) + { + currentMainState = newState; + 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,15 +384,56 @@ 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) { } - + + public virtual void PostUpdate(DateTime tick, List packets = null) + { + if (updateFlags != ActorUpdateFlags.None) + { + packets = packets ?? new List(); + 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); @@ -531,7 +561,7 @@ namespace FFXIVClassic_Map_Server.Actors } return false; } - } + } public List GetPos() { @@ -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(); 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() diff --git a/FFXIVClassic Map Server/actors/chara/Character.cs b/FFXIVClassic Map Server/actors/chara/Character.cs index 720277cc..b88ba1f5 100644 --- a/FFXIVClassic Map Server/actors/chara/Character.cs +++ b/FFXIVClassic Map Server/actors/chara/Character.cs @@ -71,13 +71,17 @@ namespace FFXIVClassic_Map_Server.Actors public Group currentParty = null; public ContentGroup currentContentGroup = null; - public DateTime lastAiUpdate; + //public DateTime lastAiUpdate; public AIContainer aiContainer; public StatusEffectContainer statusEffects; + public float meleeRange; + protected uint attackDelayMs; public CharacterTargetingAllegiance allegiance; + public Pet pet; + public Character(uint actorID) : base(actorID) { //Init timer array to "notimer" @@ -85,6 +89,11 @@ namespace FFXIVClassic_Map_Server.Actors charaWork.statusShownTime[i] = 0xFFFFFFFF; this.statusEffects = new StatusEffectContainer(this); + + // todo: move this somewhere more appropriate + attackDelayMs = 4200; + meleeRange = 2.5f; + ResetMoveSpeeds(); } public SubPacket CreateAppearancePacket() @@ -153,45 +162,7 @@ namespace FFXIVClassic_Map_Server.Actors public void PathTo(float x, float y, float z, float stepSize = 0.70f, int maxPath = 40, float polyRadius = 0.0f) { - var pos = new Vector3(positionX, positionY, positionZ); - var dest = new Vector3(x, y, z); - - var sw = new System.Diagnostics.Stopwatch(); - sw.Start(); - - var path = utils.NavmeshUtils.GetPath(((Zone)GetZone()), pos, dest, stepSize, maxPath, polyRadius); - - if (path != null) - { - if (oldPositionX == 0.0f && oldPositionY == 0.0f && oldPositionZ == 0.0f) - { - oldPositionX = positionX; - oldPositionY = positionY; - oldPositionZ = positionZ; - } - - // todo: something went wrong - if (path.Count == 0) - { - positionX = oldPositionX; - positionY = oldPositionY; - positionZ = oldPositionZ; - } - - positionUpdates = path; - - this.hasMoved = true; - this.isAtSpawn = false; - - sw.Stop(); - ((Zone)zone).pathCalls++; - ((Zone)zone).pathCallTime += sw.ElapsedMilliseconds; - - if (path.Count == 1) - Program.Log.Info($"mypos: {positionX} {positionY} {positionZ} | targetPos: {x} {y} {z} | step {stepSize} | maxPath {maxPath} | polyRadius {polyRadius}"); - - Program.Log.Error("[{0}][{1}] Created {2} points in {3} milliseconds", actorId, actorName, path.Count, sw.ElapsedMilliseconds); - } + aiContainer?.pathFind?.PreparePath(x, y, z, stepSize, maxPath, polyRadius); } public void FollowTarget(Actor target, float stepSize = 1.2f, int maxPath = 25, float radius = 0.0f) @@ -204,204 +175,118 @@ namespace FFXIVClassic_Map_Server.Actors { this.target = target; } - this.moveState = player.moveState; - this.moveSpeeds = player.moveSpeeds; + // todo: move this to own function thing + this.oldMoveState = this.moveState; + this.moveState = 2; + updateFlags |= ActorUpdateFlags.Position | ActorUpdateFlags.Speed; + //this.moveSpeeds = player.moveSpeeds; PathTo(player.positionX, player.positionY, player.positionZ, stepSize, maxPath, radius); } } - public void OnPath(Vector3 point) + public virtual void OnPath(Vector3 point) { - if (positionUpdates != null && positionUpdates.Count > 0) - { - if (point == positionUpdates[positionUpdates.Count - 1]) - { - var myPos = new Vector3(positionX, positionY, positionZ); - //point = NavmeshUtils.GetPath((Zone)zone, myPos, point, 0.35f, 1, 0.000001f, true)?[0]; - } - } + lua.LuaEngine.CallLuaBattleAction(this, "onPath", this, point); + + updateFlags |= ActorUpdateFlags.Position; + this.isAtSpawn = false; } public override void Update(DateTime tick) { - // todo: actual ai controllers - // todo: mods to control different params instead of hardcode - // todo: other ai helpers - // time elapsed since last ai update - - this.aiContainer?.Update(tick); - - /* - var diffTime = (tick - lastAiUpdate); - - if (this is Player) - { - // todo: handle player stuff here - } - else - { - // todo: handle mobs only? - //if (this.isStatic) - // return; - - // todo: this too - if (diffTime.Milliseconds >= 10) - { - bool foundActor = false; - - // leash back to spawn - if (!isMovingToSpawn && this.oldPositionX != 0.0f && this.oldPositionY != 0.0f && this.oldPositionZ != 0.0f) - { - //var spawnDistanceSq = Utils.DistanceSquared(positionX, positionY, positionZ, oldPositionX, oldPositionY, oldPositionZ); - - // todo: actual spawn leash and modifiers read from table - // set a leash to path back to spawn even if have target - // (50 yalms) - if (Utils.DistanceSquared(positionX, positionY, positionZ, oldPositionX, oldPositionY, oldPositionZ) >= 3025) - { - this.isMovingToSpawn = true; - this.target = null; - this.lastMoveUpdate = this.lastMoveUpdate.AddSeconds(-5); - this.hasMoved = false; - ClearPositionUpdates(); - } - } - - // check if player - if (target != null && target is Player) - { - var player = target as Player; - - // deaggro if zoning/logging - // todo: player.isZoning seems to be busted - if (player.playerSession.isUpdatesLocked) - { - target = null; - ClearPositionUpdates(); - } - } - - Player closestPlayer = null; - float closestPlayerDistanceSq = 1000.0f; - - // dont bother checking for any in-range players if going back to spawn - if (!this.isMovingToSpawn) - { - foreach (var actor in zone.GetActorsAroundActor(this, 65)) - { - if (actor is Player && actor != this) - { - var player = actor as Player; - - // skip if zoning/logging - // todo: player.isZoning seems to be busted - if (player != null && player.playerSession.isUpdatesLocked) - continue; - - // find distance between self and target - var distanceSq = Utils.DistanceSquared(positionX, positionY, positionZ, player.positionX, player.positionY, player.positionZ); - - int maxDistanceSq = player == target ? 900 : 100; - - // check target isnt too far - // todo: create cone thing for IsFacing - if (distanceSq <= maxDistanceSq && distanceSq <= closestPlayerDistanceSq && (IsFacing(player) || true)) - { - closestPlayerDistanceSq = distanceSq; - closestPlayer = player; - foundActor = true; - } - } - } - - // found a target - if (foundActor) - { - // make sure we're not already moving so we dont spam packets - if (!hasMoved) - { - // todo: include model size and mob specific distance checks - if (closestPlayerDistanceSq >= 9) - { - FollowTarget(closestPlayer, 2.5f, 4); - } - // too close, spread out - else if (closestPlayerDistanceSq <= 0.85f) - { - QueuePositionUpdate(target.FindRandomPointAroundActor(0.65f, 0.85f)); - } - - // we have a target, face them - if (target != null) - { - LookAt(target); - } - } - } - } - - // time elapsed since last move update - var diffMove = (tick - lastMoveUpdate); - - // todo: modifier for DelayBeforeRoamToSpawn - // player disappeared - if (!foundActor && diffMove.Seconds >= 5) - { - // dont path if havent moved before - if (!hasMoved && oldPositionX != 0.0f && oldPositionY != 0.0f && oldPositionZ != 0.0f) - { - // check within spawn radius - this.isAtSpawn = Utils.DistanceSquared(positionX, positionY, positionZ, oldPositionX, oldPositionY, oldPositionZ) <= 625.0f; - - // make sure we have no target - if (this.target == null) - { - // path back to spawn - if (!this.isAtSpawn) - { - PathTo(oldPositionX, oldPositionY, oldPositionZ, 2.8f); - } - // within spawn range, find a random point - else if (diffMove.Seconds >= 15) - { - // todo: polyRadius isnt euclidean distance.. - // pick a random point within 10 yalms of spawn - PathTo(oldPositionX, oldPositionY, oldPositionZ, 2.5f, 7, 2.5f); - - // face destination - if (positionUpdates.Count > 0) - { - var destinationPos = positionUpdates[positionUpdates.Count - 1]; - LookAt(destinationPos.X, destinationPos.Y); - } - if (this.isMovingToSpawn) - { - this.isMovingToSpawn = false; - this.ResetMoveSpeedsToDefault(); - this.ChangeState(SetActorStatePacket.MAIN_STATE_DEAD2); - } - } - } - } - // todo: this is retarded. actually no it isnt, i didnt deaggro if out of range.. - target = null; - } - // update last ai update time to now - lastAiUpdate = DateTime.Now; - } - } - */ } + + public override void PostUpdate(DateTime tick, List packets = null) + { + if (updateFlags != ActorUpdateFlags.None) + { + packets = packets ?? new List(); + + if ((updateFlags & ActorUpdateFlags.Appearance) != 0) + { + packets.Add(new SetActorAppearancePacket(modelId, appearanceIds).BuildPacket(actorId)); + } + + // todo: should probably add another flag for battleTemp since all this uses reflection + if ((updateFlags & ActorUpdateFlags.HpTpMp) != 0) + { + var propPacketUtil = new ActorPropertyPacketUtil("charaWork.parameterSave", this); + + //Parameters + + propPacketUtil.AddProperty("charaWork.parameterSave.hp[0]"); + propPacketUtil.AddProperty("charaWork.parameterSave.hpMax[0]"); + propPacketUtil.AddProperty("charaWork.parameterSave.mp"); + propPacketUtil.AddProperty("charaWork.parameterSave.mpMax"); + propPacketUtil.AddProperty("charaWork.parameterTemp.tp"); + propPacketUtil.AddProperty("charaWork.parameterSave.state_mainSkill[0]"); + propPacketUtil.AddProperty("charaWork.parameterSave.state_mainSkillLevel"); + + //General Parameters + for (int i = 3; i < charaWork.battleTemp.generalParameter.Length; i++) + { + if (charaWork.battleTemp.generalParameter[i] != 0) + propPacketUtil.AddProperty(String.Format("charaWork.battleTemp.generalParameter[{0}]", i)); + } + + propPacketUtil.AddProperty("charaWork.battleTemp.castGauge_speed[0]"); + propPacketUtil.AddProperty("charaWork.battleTemp.castGauge_speed[1]"); + packets.AddRange(propPacketUtil.Done()); + } + base.PostUpdate(tick, packets); + } + } + + public virtual bool CanAttack() + { + return false; + } + + public virtual bool CanCast() + { + return false; + } + + public virtual uint GetAttackDelayMs() + { + return attackDelayMs; + } + + public bool Engage(uint targid = 0) + { + // todo: attack the things + targid = targid == 0 ? currentTarget: targid; + if (targid != 0) + { + var targ = Server.GetWorldManager().GetActorInWorld(targid); + if (targ is Character) + aiContainer.Engage((Character)targ); + } + return false; + } + + public bool Disengage() + { + if (aiContainer != null) + { + aiContainer.Disengage(); + return true; + } + return false; + } + public virtual void Spawn(DateTime tick) { - + // todo: reset hp/mp/tp etc here + RecalculateHpMpTp(); } public virtual void Die(DateTime tick) { - + // todo: actual despawn timer + aiContainer.InternalDie(tick, 10); } protected virtual void Despawn(DateTime tick) @@ -418,6 +303,54 @@ namespace FFXIVClassic_Map_Server.Actors { return !IsDead(); } + + public virtual short GetHP() + { + // todo: + return charaWork.parameterSave.hp[0]; + } + + public virtual short GetMaxHP() + { + return charaWork.parameterSave.hpMax[0]; + } + + public virtual byte GetHPP() + { + return (byte)(charaWork.parameterSave.hp[0] / charaWork.parameterSave.hpMax[0]); + } + + public virtual void AddHP(short hp) + { + // todo: +/- hp and die + // todo: battlenpcs probably have way more hp? + var addHp = charaWork.parameterSave.hp[0] + hp; + addHp = addHp.Clamp(short.MinValue, charaWork.parameterSave.hpMax[0]); + charaWork.parameterSave.hp[0] = (short)addHp; + + if (charaWork.parameterSave.hp[0] < 1) + Die(Program.Tick); + + updateFlags |= ActorUpdateFlags.HpTpMp; + } + + public virtual void DelHP(short hp) + { + AddHP((short)-hp); + } + + // todo: should this include stats too? + public virtual void RecalculateHpMpTp() + { + // todo: recalculate stats and crap + updateFlags |= ActorUpdateFlags.HpTpMp; + } + + public virtual float GetSpeed() + { + // todo: for battlenpc/player calculate speed + return moveSpeeds[2]; + } } } diff --git a/FFXIVClassic Map Server/actors/chara/ai/AIContainer.cs b/FFXIVClassic Map Server/actors/chara/ai/AIContainer.cs index a7e3bce7..03f3c229 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/AIContainer.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/AIContainer.cs @@ -20,7 +20,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai private Stack states; private DateTime latestUpdate; private DateTime prevUpdate; - private PathFind pathFind; + public readonly PathFind pathFind; private TargetFind targetFind; private ActionQueue actionQueue; @@ -43,16 +43,24 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai // todo: trigger listeners - // todo: action queues - controller?.Update(tick); - State currState; - while (states.Count > 0 && (currState = states.Peek()).Update(tick)) + if (controller == null && pathFind != null) { - if (currState == GetCurrentState()) - { + pathFind.FollowPath(); + } + // todo: action queues + if (controller != null && controller.canUpdate) + controller.Update(tick); + + State top; + while (states.Count > 0 && (top = states.Peek()).Update(tick)) + { + if (top == GetCurrentState()) + { + states.Pop().Cleanup(); } } + owner.PostUpdate(tick); } public void CheckCompletedStates() @@ -93,6 +101,16 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai return controller; } + public TargetFind GetTargetFind() + { + return targetFind; + } + + public bool CanFollowPath() + { + return pathFind != null && (GetCurrentState() != null || GetCurrentState().CanChangeState()); + } + public bool CanChangeState() { return states.Count == 0 || states.Peek().CanInterrupt(); @@ -135,9 +153,14 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai } } + public bool IsCurrentState() 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) diff --git a/FFXIVClassic Map Server/actors/chara/ai/Ability.cs b/FFXIVClassic Map Server/actors/chara/ai/Ability.cs new file mode 100644 index 00000000..5137ed15 --- /dev/null +++ b/FFXIVClassic Map Server/actors/chara/ai/Ability.cs @@ -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; + } + } +} diff --git a/FFXIVClassic Map Server/actors/chara/ai/HateContainer.cs b/FFXIVClassic Map Server/actors/chara/ai/HateContainer.cs index e4274bcf..b696944f 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/HateContainer.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/HateContainer.cs @@ -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) diff --git a/FFXIVClassic Map Server/actors/chara/ai/PathFind.cs b/FFXIVClassic Map Server/actors/chara/ai/PathFind.cs index f77e9df2..baa6616a 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/PathFind.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/PathFind.cs @@ -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 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(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; + } } } diff --git a/FFXIVClassic Map Server/actors/chara/ai/StatusEffect.cs b/FFXIVClassic Map Server/actors/chara/ai/StatusEffect.cs index 57e538ae..bdb3baf6 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/StatusEffect.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/StatusEffect.cs @@ -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; diff --git a/FFXIVClassic Map Server/actors/chara/ai/StatusEffectContainer.cs b/FFXIVClassic Map Server/actors/chara/ai/StatusEffectContainer.cs index 4cc2d202..66dc9793 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/StatusEffectContainer.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/StatusEffectContainer.cs @@ -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 effects; public static readonly int MAX_EFFECTS = 20; private bool sendUpdate = false; + public StatusEffectContainer(Character owner) { this.owner = owner; @@ -44,10 +46,30 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai if (sendUpdate) { - owner.zone.BroadcastPacketsAroundActor(owner, owner.GetActorStatusPackets()); - } + var propPacketUtil = new ActorPropertyPacketUtil("charaWork.status", owner); - sendUpdate = false; + //Status Times + for (int i = 0; i < owner.charaWork.statusShownTime.Length; i++) + { + if (owner.charaWork.status[i] != 0xFFFF && owner.charaWork.status[i] != 0) + propPacketUtil.AddProperty(String.Format("charaWork.status[{0}]", i)); + + if (owner.charaWork.statusShownTime[i] != 0xFFFFFFFF) + propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i)); + } + owner.zone.BroadcastPacketsAroundActor(owner, propPacketUtil.Done()); + sendUpdate = false; + } + } + + public bool HasStatusEffect(uint id) + { + return effects.ContainsKey(id); + } + + public bool HasStatusEffect(StatusEffectId id) + { + return effects.ContainsKey((uint)id); } public bool AddStatusEffect(uint id, UInt64 magnitude, double tickMs, double durationMs, byte tier = 0) @@ -74,8 +96,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai if (!silent || !effect.GetSilent() || (effect.GetFlags() & (uint)StatusEffectFlags.Silent) == 0) { // todo: send packet to client with effect added message - //foreach (var player in owner.zone.GetActorsAroundActor(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(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) diff --git a/FFXIVClassic Map Server/actors/chara/ai/TargetFind.cs b/FFXIVClassic Map Server/actors/chara/ai/TargetFind.cs index 49af0b78..b63b0603 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/TargetFind.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/TargetFind.cs @@ -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 { /// todo: what even do i summarise this as? - [Flags] - enum TargetFindFlags + enum TargetFindFlags : byte { - None, + None = 0x00, /// Able to target s even if not in target's party - HitAll, + HitAll = 0x01, /// Able to target all s in target's party/alliance - Alliance, + Alliance = 0x02, /// Able to target any in target's party/alliance - Pets, + Pets = 0x04, /// Target all in zone, regardless of distance - ZoneWide, + ZoneWide = 0x08, /// Able to target dead s - Dead, + Dead = 0x10, } /// Targeting from/to different entity types - enum TargetFindCharacterType + enum TargetFindCharacterType : byte { None, /// Player can target all s in party @@ -44,7 +44,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai } /// Type of AOE region to create - enum TargetFindAOEType + enum TargetFindAOEType : byte { None, /// Really a cylinder, uses extents parameter in SetAOEType @@ -56,7 +56,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai } /// Set AOE around self or target - enum TargetFindAOERadiusType + enum TargetFindAOETarget : byte { /// Set AOE's origin at target's position 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 /// /// - radius of circle /// - height of cone - /// - width of box / 2 + /// - width of box / 2 (todo: set box length not just between user and target) /// /// Angle in radians of cone - 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; + } } } diff --git a/FFXIVClassic Map Server/actors/chara/ai/controllers/BattleNpcController.cs b/FFXIVClassic Map Server/actors/chara/ai/controllers/BattleNpcController.cs index 2e283f4b..759ab393 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/controllers/BattleNpcController.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/controllers/BattleNpcController.cs @@ -3,7 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using FFXIVClassic.Common; using FFXIVClassic_Map_Server.Actors; +using FFXIVClassic_Map_Server.packets.send.actor; +using FFXIVClassic_Map_Server.actors.area; +using FFXIVClassic_Map_Server.utils; namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers { @@ -20,29 +24,39 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers private bool firstSpell = true; private DateTime lastRoamScript; // todo: what even is this used as - public BattleNpcController(Character owner) + private new BattleNpc owner; + public BattleNpcController(BattleNpc owner) : + base(owner) { this.owner = owner; this.lastUpdate = DateTime.Now; + this.waitTime = lastUpdate.AddSeconds(5); } public override void Update(DateTime tick) { - var battleNpc = this.owner as BattleNpc; - - if (battleNpc != null) + // todo: handle aggro/deaggro and other shit here + if (owner.aiContainer.IsEngaged()) { - // todo: handle aggro/deaggro and other shit here - if (battleNpc.aiContainer.IsEngaged()) - { - DoCombatTick(tick); - } - else if (!battleNpc.IsDead()) - { - DoRoamTick(tick); - } - battleNpc.Update(tick); + DoCombatTick(tick); } + else if (!owner.IsDead()) + { + DoRoamTick(tick); + } + } + + public bool TryDeaggro() + { + if (owner.hateContainer.GetMostHatedTarget() == null || !owner.aiContainer.GetTargetFind().CanTarget(owner.target as Character)) + { + return true; + } + else if (!owner.IsCloseToSpawn()) + { + return true; + } + return false; } public override bool Engage(Character target) @@ -53,7 +67,27 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers { // reset casting firstSpell = true; + // todo: find a better place to put this? + if (owner.GetState() != SetActorStatePacket.MAIN_STATE_ACTIVE) + owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE); + + // todo: check speed/is able to move + // todo: too far, path to player if mob, message if player + // owner.ResetMoveSpeeds(); + owner.moveState = 2; + if (owner.currentSubState == SetActorStatePacket.SUB_STATE_MONSTER && owner.moveSpeeds[1] != 0) + { + // todo: actual stat based range + if (Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, target.positionX, target.positionY, target.positionZ) > 10) + { + owner.aiContainer.pathFind.SetPathFlags(PathFindFlags.None); + owner.aiContainer.pathFind.PreparePath(target.positionX, target.positionY, target.positionZ); + ChangeTarget(target); + return false; + } + } + lastActionTime = DateTime.Now; // todo: adjust cooldowns with modifiers } return canEngage; @@ -65,10 +99,17 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers return true; } - public override bool Disengage() + public override void Disengage() { + var target = owner.target; + base.Disengage(); // todo: - return true; + lastActionTime = lastUpdate; + owner.isMovingToSpawn = true; + neutralTime = lastUpdate; + owner.hateContainer.ClearHate(); + owner.moveState = 1; + lua.LuaEngine.CallLuaBattleAction(owner, "onDisengage", owner, target); } public override void Cast(Character target, uint spellId) @@ -93,25 +134,185 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers private void DoRoamTick(DateTime tick) { - var battleNpc = owner as BattleNpc; - - if (battleNpc != null) + if (owner.hateContainer.GetHateList().Count > 0) { - if (battleNpc.hateContainer.GetHateList().Count > 0) + Engage(owner.hateContainer.GetMostHatedTarget()); + return; + } + //else if (owner.currentLockedTarget != 0) + //{ + // ChangeTarget(Server.GetWorldManager().GetActorInWorld(owner.currentLockedTarget).GetAsCharacter()); + //} + + if (tick >= waitTime) + { + // todo: aggro cooldown + neutralTime = tick.AddSeconds(5); + if (owner.aiContainer.pathFind.IsFollowingPath()) { - Engage(battleNpc.hateContainer.GetMostHatedTarget()); - return; + owner.aiContainer.pathFind.FollowPath(); + lastActionTime = tick.AddSeconds(-5); } - else if (battleNpc.currentLockedTarget != 0) + else { - + if (tick >= lastActionTime) + { + + } } + // todo: + waitTime = tick.AddSeconds(10); + owner.OnRoam(tick); } } private void DoCombatTick(DateTime tick) { + HandleHate(); + // todo: magic/attack/ws cooldowns etc + if (TryDeaggro()) + { + Disengage(); + return; + } + + Move(); + } + + private void Move() + { + if (!owner.aiContainer.CanFollowPath()) + { + return; + } + + if (owner.aiContainer.pathFind.IsFollowingScriptedPath()) + { + owner.aiContainer.pathFind.FollowPath(); + return; + } + + var targetPos = owner.target.GetPosAsVector3(); + var distance = Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, targetPos.X, targetPos.Y, targetPos.Z); + + if (distance > owner.meleeRange - 0.2f || owner.aiContainer.CanFollowPath()) + { + if (CanMoveForward(distance)) + { + if (!owner.aiContainer.pathFind.IsFollowingPath() && distance > 3) + { + // pathfind if too far otherwise jump to target + owner.aiContainer.pathFind.SetPathFlags(distance > 3 ? PathFindFlags.None : PathFindFlags.IgnoreNav ); + owner.aiContainer.pathFind.PreparePath(targetPos, 0.7f, 5); + } + owner.aiContainer.pathFind.FollowPath(); + if (!owner.aiContainer.pathFind.IsFollowingPath()) + { + if (owner.target.currentSubState == SetActorStatePacket.SUB_STATE_PLAYER) + { + foreach (var battlenpc in owner.zone.GetActorsAroundActor(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()); } } } diff --git a/FFXIVClassic Map Server/actors/chara/ai/controllers/Controller.cs b/FFXIVClassic Map Server/actors/chara/ai/controllers/Controller.cs index 9ad00f8b..a60cc332 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/controllers/Controller.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/controllers/Controller.cs @@ -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); diff --git a/FFXIVClassic Map Server/actors/chara/ai/controllers/PetController.cs b/FFXIVClassic Map Server/actors/chara/ai/controllers/PetController.cs index 2ffa3336..dff7bfae 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/controllers/PetController.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/controllers/PetController.cs @@ -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) diff --git a/FFXIVClassic Map Server/actors/chara/ai/controllers/PlayerController.cs b/FFXIVClassic Map Server/actors/chara/ai/controllers/PlayerController.cs index bffacc54..9059f63c 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/controllers/PlayerController.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/controllers/PlayerController.cs @@ -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) diff --git a/FFXIVClassic Map Server/actors/chara/ai/state/AttackState.cs b/FFXIVClassic Map Server/actors/chara/ai/state/AttackState.cs index 609bb25e..ce62aeca 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/state/AttackState.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/state/AttackState.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using FFXIVClassic.Common; using FFXIVClassic_Map_Server.Actors; using FFXIVClassic_Map_Server.packets.send.actor; using FFXIVClassic_Map_Server.packets.send.actor.battle; @@ -10,20 +11,31 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state { class AttackState : State { + private int damage = 0; + private bool tooFar = false; + private DateTime attackTime; + public AttackState(Character owner, Character target) : base(owner, target) { + owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE); + owner.aiContainer.ChangeTarget(target); this.startTime = DateTime.Now; + attackTime = startTime; + owner.aiContainer.pathFind?.Clear(); // todo: should handle everything here instead of on next tick.. } public override void OnStart() { - + // todo: check within attack range + + owner.LookAt(target); } public override bool Update(DateTime tick) { + /* TryInterrupt(); if (interrupt) @@ -31,13 +43,33 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state OnInterrupt(); return true; } - - // todo: check weapon delay/haste etc and use that - if ((tick - startTime).TotalMilliseconds >= 0) + */ + if (owner.target == null || target.IsDead()) { - OnComplete(); return true; } + if (IsAttackReady()) + { + if (CanAttack()) + { + TryInterrupt(); + + // todo: check weapon delay/haste etc and use that + if (!interrupt) + { + OnComplete(); + } + else + { + + } + SetInterrupted(false); + } + else + { + // todo: handle interrupt/paralyze etc + } + } return false; } @@ -48,18 +80,20 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state public override void OnComplete() { - var damage = utils.AttackUtils.CalculateDamage(owner, target); + damage = utils.AttackUtils.CalculateDamage(owner, target); // onAttack(actor, target, damage) + utils.BattleUtils.DamageTarget(owner, target, damage); lua.LuaEngine.CallLuaBattleAction(owner, "onAttack", false, owner, target, damage); - //var packet = BattleAction1Packet.BuildPacket(owner.actorId, target.actorId); + foreach (var player in owner.zone.GetActorsAroundActor(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; + } } } diff --git a/FFXIVClassic Map Server/actors/chara/ai/state/DeathState.cs b/FFXIVClassic Map Server/actors/chara/ai/state/DeathState.cs new file mode 100644 index 00000000..9d9f7ac3 --- /dev/null +++ b/FFXIVClassic Map Server/actors/chara/ai/state/DeathState.cs @@ -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; + } + } +} diff --git a/FFXIVClassic Map Server/actors/chara/ai/state/MagicState.cs b/FFXIVClassic Map Server/actors/chara/ai/state/MagicState.cs new file mode 100644 index 00000000..5c407dd1 --- /dev/null +++ b/FFXIVClassic Map Server/actors/chara/ai/state/MagicState.cs @@ -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; + } + } +} diff --git a/FFXIVClassic Map Server/actors/chara/ai/state/State.cs b/FFXIVClassic Map Server/actors/chara/ai/state/State.cs index 14cd49c8..dc5da1d6 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/state/State.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/state/State.cs @@ -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() { } diff --git a/FFXIVClassic Map Server/actors/chara/ai/utils/AttackUtils.cs b/FFXIVClassic Map Server/actors/chara/ai/utils/AttackUtils.cs index 36d5bcde..cdd7865d 100644 --- a/FFXIVClassic Map Server/actors/chara/ai/utils/AttackUtils.cs +++ b/FFXIVClassic Map Server/actors/chara/ai/utils/AttackUtils.cs @@ -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 diff --git a/FFXIVClassic Map Server/actors/chara/ai/utils/BattleUtils.cs b/FFXIVClassic Map Server/actors/chara/ai/utils/BattleUtils.cs new file mode 100644 index 00000000..fed7d4d6 --- /dev/null +++ b/FFXIVClassic Map Server/actors/chara/ai/utils/BattleUtils.cs @@ -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); + } + } + } +} diff --git a/FFXIVClassic Map Server/actors/chara/npc/BattleNpc.cs b/FFXIVClassic Map Server/actors/chara/npc/BattleNpc.cs index 808238c6..cdacd61c 100644 --- a/FFXIVClassic Map Server/actors/chara/npc/BattleNpc.cs +++ b/FFXIVClassic Map Server/actors/chara/npc/BattleNpc.cs @@ -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; + } + /// // todo: create an action object? 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(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; } } } diff --git a/FFXIVClassic Map Server/actors/chara/npc/Npc.cs b/FFXIVClassic Map Server/actors/chara/npc/Npc.cs index 80324c88..cccd394c 100644 --- a/FFXIVClassic Map Server/actors/chara/npc/Npc.cs +++ b/FFXIVClassic Map Server/actors/chara/npc/Npc.cs @@ -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 diff --git a/FFXIVClassic Map Server/actors/chara/player/Player.cs b/FFXIVClassic Map Server/actors/chara/player/Player.cs index 97e9a765..b8b5cfe7 100644 --- a/FFXIVClassic Map Server/actors/chara/player/Player.cs +++ b/FFXIVClassic Map Server/actors/chara/player/Player.cs @@ -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 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; } - } } diff --git a/FFXIVClassic Map Server/dataobjects/Session.cs b/FFXIVClassic Map Server/dataobjects/Session.cs index 027223b6..73732a3e 100644 --- a/FFXIVClassic Map Server/dataobjects/Session.cs +++ b/FFXIVClassic Map Server/dataobjects/Session.cs @@ -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 { diff --git a/FFXIVClassic Map Server/lua/LuaEngine.cs b/FFXIVClassic Map Server/lua/LuaEngine.cs index e37acd88..2e68c63e 100644 --- a/FFXIVClassic Map Server/lua/LuaEngine.cs +++ b/FFXIVClassic Map Server/lua/LuaEngine.cs @@ -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()); diff --git a/FFXIVClassic Map Server/utils/NavmeshUtils.cs b/FFXIVClassic Map Server/utils/NavmeshUtils.cs index 54c6e433..0ee151fe 100644 --- a/FFXIVClassic Map Server/utils/NavmeshUtils.cs +++ b/FFXIVClassic Map Server/utils/NavmeshUtils.cs @@ -16,10 +16,34 @@ 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; - return false; + 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() { endVec }; } var smoothPath = new List(pathSize) { }; diff --git a/FFXIVClassic.sln b/FFXIVClassic.sln index c38a65cf..340c36b6 100644 --- a/FFXIVClassic.sln +++ b/FFXIVClassic.sln @@ -58,4 +58,7 @@ Global GlobalSection(Performance) = preSolution HasPerformanceSessions = true EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection EndGlobal diff --git a/data/scripts/commands/ActivateCommand.lua b/data/scripts/commands/ActivateCommand.lua index 71c84183..3e5a3be9 100644 --- a/data/scripts/commands/ActivateCommand.lua +++ b/data/scripts/commands/ActivateCommand.lua @@ -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:ChangeState(0); + player.Disengage(); end player:endEvent(); - sendSignal("playerActive"); end \ No newline at end of file