From 5b44134a2c1c4ce6f07cc968ca7f01c132fb4293 Mon Sep 17 00:00:00 2001 From: Brandon Woolworth Date: Fri, 11 Aug 2017 14:29:02 -0500 Subject: [PATCH 1/2] Updated combat Updated combat to be singleton-use only. --- Albion/Merlin/Merlin.csproj | 3 +- Albion/Merlin/Profiles/Combat.cs | 385 +++++++++++++++---------------- 2 files changed, 189 insertions(+), 199 deletions(-) diff --git a/Albion/Merlin/Merlin.csproj b/Albion/Merlin/Merlin.csproj index 6c2e9b7..5d1aee7 100644 --- a/Albion/Merlin/Merlin.csproj +++ b/Albion/Merlin/Merlin.csproj @@ -174,6 +174,7 @@ + @@ -246,4 +247,4 @@ - + \ No newline at end of file diff --git a/Albion/Merlin/Profiles/Combat.cs b/Albion/Merlin/Profiles/Combat.cs index 30cc72a..7f59123 100644 --- a/Albion/Merlin/Profiles/Combat.cs +++ b/Albion/Merlin/Profiles/Combat.cs @@ -2,94 +2,71 @@ using System.Diagnostics; using System.Linq; using Merlin.API; -using Stateless; using UnityEngine; +using Player = LocalPlayerCharacterView; using SpellCategory = gz.SpellCategory; using SpellTarget = gz.SpellTarget; namespace Merlin.Profiles { - class Combat { - private readonly LocalPlayerCharacterView _player; - private readonly List _mobList; + public class Combat { - private StateMachine _state; - - private float elapsedSeconds; - private float secondCount; - - public Combat(LocalPlayerCharacterView player) { - _player = player; - _mobList = new List(); - - _state = new StateMachine(State.Idle); - _state.Configure(State.Idle) - .Permit(Trigger.Died, State.Respawn) - .Permit(Trigger.Recover, State.Recover) - .Permit(Trigger.EncounteredAttacker, State.Combat); - - _state.Configure(State.Combat) - .Permit(Trigger.Finished, State.Idle) - // .Permit(Trigger.LowHealth, State.Flee) Not working for now. - .Permit(Trigger.Died, State.Respawn); - - _state.Configure(State.Respawn) - .Permit(Trigger.Finished, State.Idle); - - _state.Configure(State.Flee) - .Permit(Trigger.Died, State.Respawn) - .Permit(Trigger.Finished, State.Recover); - - _state.Configure(State.Recover) - .Permit(Trigger.EncounteredAttacker, State.Combat) - .Permit(Trigger.Died, State.Respawn) - .Permit(Trigger.Finished, State.Idle); + public static Combat Instance; + private static Player Player => Client.Instance.LocalPlayerCharacter; + static Combat() { + Instance = new Combat(); } - public void AddMob(string name) { - _mobList.Add(new SpecialMob(name)); - } + public delegate bool InterruptDelegate(FightingObjectView target); + public delegate bool DodgeDelegate(FightingObjectView target, out Evade evade); - private void AddSpellToMob(SpecialMob mob, DangerousSpell spell) => mob.AddSpell(spell); + private InterruptDelegate ShouldInterrupt; + private DodgeDelegate ShouldDodge; - private SpecialMob GetMobByName(string mobName) { - return _mobList.FirstOrDefault(m => m.GetName().Equals(mobName)); - } + private State state; - public void AddSpellToMob(string mobName, SpellCategory category, SpellTarget target, Evade evade, string spellName = null) { - DangerousSpell spell = new DangerousSpell(target, category, evade, spellName); - var mob = GetMobByName(mobName); - if (mob != null) - AddSpellToMob(mob, spell); - } + private float elapsedSeconds; + private float secondCount; - private bool IsSpecialMob(FightingObjectView target, out Evade evade) { - evade = Evade.Tank; - if (!target.IsCasting()) return false; + private bool evading; - var targetName = target.name; - var spellName = target.GetSpellCasted().d6; - var spellTarget = target.GetSpellCasted().d1; - var spellCategory = target.GetSpellCasted().d4; + public void SetState(State newState) => state = newState; + public bool IsState(State state) => this.state == state; + public State GetState() => state; - var mob = _mobList.Find(s => s.GetName().Equals(targetName)); - var mobSpell = mob.GetSpell(spellName, spellCategory, spellTarget); - if (mobSpell == null) return false; - evade = mobSpell.GetEvadeMethod(); - return true; + private Combat() { + SetState(State.Idle); + ResetDelegates(); } - public State GetState() => _state.State; + public void Debug() { + var target = Player.GetAttackTarget(); + var guiX = 1373; + var guiY = 182; + var guiW = 304; + var guiH = 206; // 9 label lines. + var boxGui = new Rect(guiX, guiY, guiW, guiH); + GUI.Box(boxGui, ""); + + var useSpells = GetUsableSpells(); + var spellStr = ""; + for (int i = useSpells.Length - 1; i >= 0; i--) { + if (string.IsNullOrEmpty(useSpells[i].Name)) continue; + spellStr += useSpells[i].Name + "\n\t"; + } + var dbgStr = $"Target: {target.PrefabName}\nSpells: {spellStr}\n"; + GUI.Label(new Rect(guiX + 4, guiY + 4, guiW - 8, guiH - 8), dbgStr); + } public void Update() { var curTime = Stopwatch.GetTimestamp() / Stopwatch.Frequency; // Gets seconds - elapsedSeconds = secondCount - curTime; + elapsedSeconds += curTime - secondCount; secondCount = curTime; // Update based on state // Do nothing while idle. - switch (_state.State) { + switch (state) { case State.Idle: Idle(); break; @@ -108,73 +85,73 @@ public void Update() { } } - public bool HandleAttackers() { - if (_player.IsUnderAttack(out FightingObjectView attacker)) { - _player.CreateTextEffect("[Attacked]"); - _state.Fire(Trigger.EncounteredAttacker); - return true; - } - - return false; - } - private void Idle() { - if (_player.IsUnderAttack(out FightingObjectView attacker)) { + if (Player.IsUnderAttack(out FightingObjectView attacker)) { Core.Log("[Combat] Attacked"); elapsedSeconds = 0; - _state.Fire(Trigger.EncounteredAttacker); + SetState(State.Combat); return; } - if (_player.GetHealth() <= 0) { + if (Player.GetHealth() <= 0) { Core.Log("[Combat] Player died"); - _state.Fire(Trigger.Died); + SetState(State.Respawn); return; } - if (_player.GetHealth() <= _player.GetMaxHealth() * 0.5f) { + if (Player.GetHealth() <= Player.GetMaxHealth() * 0.5f) { Core.Log("[Combat] Recovering"); - _state.Fire(Trigger.Recover); + SetState(State.Recover); return; } } private void Fight() { - var attackTimer = _player.GetAttackDelay().p(); - var spells = _player.GetSpells().Ready() - .Ignore("ESCAPE_DUNGEON").Ignore("PLAYER_COUPDEGRACE") - .Ignore("AMBUSH").Ignore("OUTOFCOMBATHEAL"); - - var attackTarget = _player.GetAttackTarget(); - - Evade evade; - if (attackTarget != null && IsSpecialMob(attackTarget, out evade)) { - _player.CreateTextEffect("[Dodging]"); - Dodge(attackTarget, evade); + var target = Player.GetAttackTarget(); + if (ShouldDodge(target, out Evade evade)) { + Dodge(target, evade, ShouldInterrupt(target)); return; } - if (attackTarget != null && elapsedSeconds > attackTimer / 1000f) { - var selfBuffSpells = spells.Target(SpellTarget.Self).Category(SpellCategory.Damage); - if (selfBuffSpells.Any() && !_player.IsCastingSpell()) { - _player.CreateTextEffect("[Casting Buff Spell]"); - _player.CastOnSelf(selfBuffSpells.FirstOrDefault().SpellSlot); + if (!evading) + Attack(target); + } + + private void Attack(FightingObjectView target) { + var attackTimer = Player.GetAttackDelay().p(); + var spells = GetUsableSpells(); + Player.CreateTextEffect("[Attacking]"); + + // enemy not null and player has finished autoAttacking + if (target != null && elapsedSeconds > (attackTimer / 1000f) * 2) { + var selfBuffSpells = spells.Target(SpellTarget.Self).Category(SpellCategory.Buff); + if (selfBuffSpells.Any() && !Player.IsCastingSpell()) { + Player.CreateTextEffect("[Casting Buff Spell]"); + Player.CastOnSelf(selfBuffSpells.FirstOrDefault().SpellSlot); + elapsedSeconds = 0; + return; + } + + var selfDamage = spells.Target(SpellTarget.Self).Category(SpellCategory.Damage); + if (selfDamage.Any() && !Player.IsCastingSpell()) { + Player.CreateTextEffect("[Casting Buff Spell]"); + Player.CastOnSelf(selfDamage.FirstOrDefault().SpellSlot); elapsedSeconds = 0; return; } var enemyBuffSpells = spells.Target(SpellTarget.Enemy).Category(SpellCategory.Buff); - if (enemyBuffSpells.Any() && !_player.IsCastingSpell()) { - _player.CreateTextEffect("[Casting Damage Spell]"); - _player.CastOn(enemyBuffSpells.FirstOrDefault().SpellSlot, attackTarget); + if (enemyBuffSpells.Any() && !Player.IsCastingSpell()) { + Player.CreateTextEffect("[Casting Damage Spell]"); + Player.CastOn(enemyBuffSpells.FirstOrDefault().SpellSlot, target); elapsedSeconds = 0; return; } var enemyCCSpells = spells.Target(SpellTarget.Enemy).Category(SpellCategory.CrowdControl); - if (enemyCCSpells.Any() && !_player.IsCastingSpell()) { - _player.CreateTextEffect("[Casting CrowdControl Spell]"); - _player.CastOn(enemyCCSpells.FirstOrDefault().SpellSlot, attackTarget); + if (enemyCCSpells.Any() && !Player.IsCastingSpell()) { + Player.CreateTextEffect("[Casting CrowdControl Spell]"); + Player.CastOn(enemyCCSpells.FirstOrDefault().SpellSlot, target); elapsedSeconds = 0; return; } @@ -188,143 +165,162 @@ private void Fight() { } */ } - - if (_player.IsUnderAttack(out FightingObjectView attacker)) { - _player.SetSelectedObject(attacker); - _player.AttackSelectedObject(); + if (Player.IsUnderAttack(out FightingObjectView attacker)) { + Player.SetSelectedObject(attacker); + Player.AttackSelectedObject(); return; } - if (_player.GetHealth() <= (_player.GetMaxHealth() * 0.1f)) { - _state.Fire(Trigger.LowHealth); + if (Player.GetHealth() <= (Player.GetMaxHealth() * 0.1f)) { + SetState(State.Flee); return; } - if (_player.IsCasting()) + if (Player.IsCasting()) return; - Core.Log("[Expedition] Continuing."); - _state.Fire(Trigger.Finished); + Core.Log("Continuing."); + SetState(State.Idle); } - private void Dodge(FightingObjectView attackTarget, Evade evadeMethod) { - Vector3 movePosition; - switch (evadeMethod) { + private void Dodge(FightingObjectView target, Evade evade, bool shouldInterrupt = true) { + Player.CreateTextEffect("[Dodging]"); + var spells = GetUsableSpells(); + var defensive = spells.FirstOrDefault(s => s.Category.Equals(SpellCategory.Buff_Damageshield)); + var interrupt = spells.FirstOrDefault(s => s.Category.Equals(SpellCategory.CrowdControl) && !s.Target.Equals(SpellTarget.Ground)); + + if (interrupt != null && shouldInterrupt) { + Cast(interrupt); + Core.Log("Evading by Interrupt"); + return; + } + Vector3 movePos; + switch (evade) { case Evade.Behind: - movePosition = (attackTarget.transform.position - attackTarget.transform.forward * 3); - _player.RequestMove(movePosition); + movePos = target.transform.position - target.transform.forward * 20f; + Player.RequestMove(movePos); + evading = true; + Core.Log("Evading behind"); break; case Evade.Left: - movePosition = (attackTarget.transform.position - attackTarget.transform.right * 3); - _player.RequestMove(movePosition); + movePos = target.transform.position - target.transform.right * 20f; + Player.RequestMove(movePos); + evading = true; + Core.Log("Evading left"); break; - case Evade.Defensive: - // Not implemented yet. + case Evade.Away: + movePos = target.transform.position - target.transform.forward * 20f; + Player.RequestMove(movePos); + evading = true; + Core.Log("Evading away"); break; - case Evade.Tank: - default: + case Evade.Defensive: + Cast(defensive); + Core.Log("Evading by defensive"); break; } } private void Recover() { - var recoverySpell = _player.GetSpells().Slot(SpellSlotIndex.Armor).FirstOrDefault(); + var recoverySpell = Player.GetSpells().Slot(SpellSlotIndex.Armor).FirstOrDefault(); - if (_player.IsUnderAttack(out FightingObjectView attacker)) { + if (Player.IsUnderAttack(out FightingObjectView attacker)) { Core.Log("Attacked"); - _state.Fire(Trigger.EncounteredAttacker); + SetState(State.Combat); return; } - if (recoverySpell != null && recoverySpell.Name.Equals("OUTOFCOMBATHEAL") && recoverySpell.IsReady) { - _player.CastOnSelf(SpellSlotIndex.Armor); + if (recoverySpell != null && !Player.IsGettingUpFromKnockDown() && + recoverySpell.Name.Equals("OUTOFCOMBATHEAL") && recoverySpell.IsReady) { + Player.CastOnSelf(SpellSlotIndex.Armor); } - if (_player.GetHealth() <= 0) - _state.Fire(Trigger.Died); + if (Player.GetHealth() <= 0) + SetState(State.Respawn); - if (_player.GetHealth() > _player.GetMaxHealth() * 0.75f) - _state.Fire(Trigger.Finished); + if (Player.GetHealth() > Player.GetMaxHealth() * 0.75f) { + SetState(State.Idle); + } } - private void Flee() { - if (_player.GetHealth() <= 0) { - _state.Fire(Trigger.Died); - return; - } + /** + * Flee is a virtual, but protected, method so it *can* be overridden. + * This can be used to implement your own methods in separate classes. + */ + protected virtual void Flee() { + if (Player.GetHealth() <= 0) + SetState(State.Respawn); + if (Player.GetHealth() == Player.GetMaxHealth()) + SetState(State.Idle); - /* Not Yet Implemented - if (_player.IsInCombat()) - path.Flee(); - else - _state.Fire(Trigger.Finished); - */ } private void Respawn() { var isRespawnShowing = GameGui.Instance.RespawnGui.ExpeditionStart.isActiveAndEnabled; - if (isRespawnShowing) - _player.OnRespawn(); + if (isRespawnShowing) { + Player.OnRespawn(); + SetState(State.Idle); + } } - private class DangerousSpell { - private readonly string name; - private readonly Evade evadeMethod; - private readonly SpellTarget target; - private readonly SpellCategory category; - - // Name should be the FightingObjectView.GetSpellCasted().d6 - public DangerousSpell(SpellTarget target, SpellCategory category, Evade evadeMethod, string spellName) { - this.name = spellName; - this.target = target; - this.category = category; - this.evadeMethod = evadeMethod; + private Spell[] GetUsableSpells() { + List returnSpells = new List(); + var spells = Player.GetSpells(); + for (int i = spells.Length - 1; i > 0; i--) { + if (!spells[i].IsReady) continue; + if (spells[i].Target == SpellTarget.Ground) continue; // Not quite working. + if (spells[i].SpellSlot == SpellSlotIndex.Potion || spells[i].SpellSlot == SpellSlotIndex.Food) + continue; + returnSpells.Add(spells[i]); } - public Evade GetEvadeMethod() => evadeMethod; - public bool IsNamed() => !string.IsNullOrEmpty(name); - public string GetName() => name; - public SpellTarget GetTarget() => target; - public SpellCategory GetCategory() => category; + return returnSpells.ToArray(); } - private class SpecialMob { - - private readonly string name; - public string GetName() => name; + private void Cast(Spell spell, FightingObjectView target = null) { + switch (spell.Target) { + case SpellTarget.Self: + Player.CastOnSelf(spell.SpellSlot); + return; + case SpellTarget.Ground: + Player.CastAt(spell.SpellSlot, target == null ? Player.GetPosition() : target.GetPosition()); + return; + case SpellTarget.Enemy: + if (target != null) + Player.CastOn(spell.SpellSlot, target); + return; + } + } - private List dangerousSpells; + public void ResetDelegates() { + ResetInterruptDelegate(); + ResetDodgeDelegate(); + } - public SpecialMob(string name, params DangerousSpell[] dSpells) { - this.name = name; - this.dangerousSpells = new List(); + public void SetInterruptDelegate(InterruptDelegate shouldInterrupt) => ShouldInterrupt = shouldInterrupt; + public void SetDodgeDelegate(DodgeDelegate dodgeDelegate) => ShouldDodge = dodgeDelegate; + public void ResetInterruptDelegate() => ShouldInterrupt = DefaultShouldInterrupt; + public void ResetDodgeDelegate() => ShouldDodge = DefaultShouldDodge; - foreach (var spell in dSpells) - dangerousSpells.Add(spell); - } + private bool DefaultShouldDodge(FightingObjectView target, out Evade evade) { + evade = Evade.Tank; + if (target.IsCasting()) return false; - public List GetDangerousSpells() => dangerousSpells; + var spellCategory = target.GetSpellCasted().d4; + var spellTarget = target.GetSpellCasted().d1; - public DangerousSpell GetSpell(string spellName, SpellCategory category, SpellTarget target) { - foreach (var spell in dangerousSpells) { - if (!spell.IsNamed()) continue; - if (spell.GetCategory().Equals(category) && spell.GetTarget().Equals(target) && spell.GetName().Equals(spellName)) - return spell; - } + if (spellCategory != SpellCategory.Damage) return true; - return null; - } - public DangerousSpell GetSpell(SpellCategory category, SpellTarget target) { - foreach (var spell in dangerousSpells) { - if (spell.IsNamed()) continue; - if (spell.GetCategory().Equals(category) && spell.GetTarget().Equals(target)) - return spell; - } + if (spellTarget == SpellTarget.Enemy) + evade = Evade.Defensive; + if (spellTarget == SpellTarget.Ground) + evade = Evade.Left; - return null; - } + return true; + } - internal void AddSpell(DangerousSpell spell) => dangerousSpells.Add(spell); + private bool DefaultShouldInterrupt(FightingObjectView target) { + return true; } public enum State { @@ -340,15 +336,8 @@ public enum Evade { Behind, Left, Defensive, - Tank - } - - public enum Trigger { - EncounteredAttacker, - LowHealth, - Recover, - Died, - Finished, + Tank, + Away } public static string GetStateString(State state) { From 474fb5892e8d2c70ef8c97377f0fd62d29c4c9c9 Mon Sep 17 00:00:00 2001 From: Brandon Woolworth Date: Fri, 11 Aug 2017 14:42:59 -0500 Subject: [PATCH 2/2] Expedition profile Semi-working expedition profile. Does not flee. --- Albion/Merlin/API/Game/Spells.cs | 22 +-- Albion/Merlin/API/Game/Spells.tt | 22 +-- Albion/Merlin/Merlin.csproj | 12 +- .../Profiles/Expedition/Expedition.Combat.cs | 25 ++++ .../Expedition/Expedition.Navigation.cs | 54 +++++++ .../Profiles/Expedition/Expedition.Restart.cs | 114 +++++++++++++++ .../Merlin/Profiles/Expedition/Expedition.cs | 132 ++++++++++++++++++ .../Expedition/ExpeditionPathingRequest.cs | 112 +++++++++++++++ Albion/Merlin/Profiles/Expedition/Mob.cs | 58 ++++++++ .../Expedition/Mobs/HereticArcherMob.cs | 8 ++ .../Expedition/Mobs/HereticMageMob.cs | 9 ++ .../Expedition/Mobs/HereticMinibossMob.cs | 9 ++ .../Expedition/Mobs/HereticTankMob.cs | 8 ++ .../Profiles/Expedition/Mobs/OverseerMob.cs | 13 ++ 14 files changed, 577 insertions(+), 21 deletions(-) create mode 100644 Albion/Merlin/Profiles/Expedition/Expedition.Combat.cs create mode 100644 Albion/Merlin/Profiles/Expedition/Expedition.Navigation.cs create mode 100644 Albion/Merlin/Profiles/Expedition/Expedition.Restart.cs create mode 100644 Albion/Merlin/Profiles/Expedition/Expedition.cs create mode 100644 Albion/Merlin/Profiles/Expedition/ExpeditionPathingRequest.cs create mode 100644 Albion/Merlin/Profiles/Expedition/Mob.cs create mode 100644 Albion/Merlin/Profiles/Expedition/Mobs/HereticArcherMob.cs create mode 100644 Albion/Merlin/Profiles/Expedition/Mobs/HereticMageMob.cs create mode 100644 Albion/Merlin/Profiles/Expedition/Mobs/HereticMinibossMob.cs create mode 100644 Albion/Merlin/Profiles/Expedition/Mobs/HereticTankMob.cs create mode 100644 Albion/Merlin/Profiles/Expedition/Mobs/OverseerMob.cs diff --git a/Albion/Merlin/API/Game/Spells.cs b/Albion/Merlin/API/Game/Spells.cs index f82c650..a337abe 100644 --- a/Albion/Merlin/API/Game/Spells.cs +++ b/Albion/Merlin/API/Game/Spells.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using SpellCategory = gz.SpellCategory; +using SpellTarget = gz.SpellTarget; using UnityEngine; @@ -50,7 +52,7 @@ public string Name } } - public gz.SpellCategory Category + public SpellCategory Category { get { @@ -59,11 +61,11 @@ public gz.SpellCategory Category if (configuration != null) return configuration.Category; - return gz.SpellCategory.None; + return SpellCategory.None; } } - public gz.SpellTarget Target + public SpellTarget Target { get { @@ -72,7 +74,7 @@ public gz.SpellTarget Target if (configuration != null) return configuration.Target; - return gz.SpellTarget.None; + return SpellTarget.None; } } @@ -134,25 +136,25 @@ public string Name } } - public gz.SpellCategory Category + public SpellCategory Category { get { if (_internalConfiguration != null) return _internalConfiguration.d4; - return gz.SpellCategory.None; + return SpellCategory.None; } } - public gz.SpellTarget Target + public SpellTarget Target { get { if (_internalConfiguration != null) return _internalConfiguration.d1; - return gz.SpellTarget.None; + return SpellTarget.None; } } @@ -190,12 +192,12 @@ public static IEnumerable Slot(this IEnumerable spells, SpellSlotI return spells.Where(spell => spell.SpellSlot == spellSlot); } - public static IEnumerable Category(this IEnumerable spells, gz.SpellCategory category) + public static IEnumerable Category(this IEnumerable spells, SpellCategory category) { return spells.Where(spell => spell.Category == category); } - public static IEnumerable Target(this IEnumerable spells, gz.SpellTarget target) + public static IEnumerable Target(this IEnumerable spells, SpellTarget target) { return spells.Where(spell => spell.Target == target); } diff --git a/Albion/Merlin/API/Game/Spells.tt b/Albion/Merlin/API/Game/Spells.tt index 3281612..88ad21b 100644 --- a/Albion/Merlin/API/Game/Spells.tt +++ b/Albion/Merlin/API/Game/Spells.tt @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; +using SpellCategory = gz.SpellCategory; +using SpellTarget = gz.SpellTarget; using UnityEngine; @@ -51,7 +53,7 @@ namespace Merlin.API } } - public gz.SpellCategory Category + public SpellCategory Category { get { @@ -60,11 +62,11 @@ namespace Merlin.API if (configuration != null) return configuration.Category; - return gz.SpellCategory.None; + return SpellCategory.None; } } - public gz.SpellTarget Target + public SpellTarget Target { get { @@ -73,7 +75,7 @@ namespace Merlin.API if (configuration != null) return configuration.Target; - return gz.SpellTarget.None; + return SpellTarget.None; } } @@ -135,25 +137,25 @@ namespace Merlin.API } } - public gz.SpellCategory Category + public SpellCategory Category { get { if (_internalConfiguration != null) return _internalConfiguration.d4; - return gz.SpellCategory.None; + return SpellCategory.None; } } - public gz.SpellTarget Target + public SpellTarget Target { get { if (_internalConfiguration != null) return _internalConfiguration.d1; - return gz.SpellTarget.None; + return SpellTarget.None; } } @@ -191,12 +193,12 @@ namespace Merlin.API return spells.Where(spell => spell.SpellSlot == spellSlot); } - public static IEnumerable Category(this IEnumerable spells, gz.SpellCategory category) + public static IEnumerable Category(this IEnumerable spells, SpellCategory category) { return spells.Where(spell => spell.Category == category); } - public static IEnumerable Target(this IEnumerable spells, gz.SpellTarget target) + public static IEnumerable Target(this IEnumerable spells, SpellTarget target) { return spells.Where(spell => spell.Target == target); } diff --git a/Albion/Merlin/Merlin.csproj b/Albion/Merlin/Merlin.csproj index 5d1aee7..103915e 100644 --- a/Albion/Merlin/Merlin.csproj +++ b/Albion/Merlin/Merlin.csproj @@ -159,6 +159,17 @@ + + + + + + + + + + + @@ -174,7 +185,6 @@ - diff --git a/Albion/Merlin/Profiles/Expedition/Expedition.Combat.cs b/Albion/Merlin/Profiles/Expedition/Expedition.Combat.cs new file mode 100644 index 0000000..d467d06 --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Expedition.Combat.cs @@ -0,0 +1,25 @@ +// ReSharper disable All + +namespace Merlin.Profiles.Expedition { + partial class Expedition { + private Mob LookupTarget(FightingObjectView target) { + for (int i = mobs.Length - 1; i >= 0; i--) + if (mobs[i].Equals(target)) + return mobs[i]; + return null; + } + + private bool ShouldInterrupt(FightingObjectView target) { + var mob = LookupTarget(target); + return mob != null && mob.ShouldInterrupt(target.GetSpellCasted()); + } + + private bool ShouldDodge(FightingObjectView target, out Profiles.Combat.Evade evade) { + evade = Combat.Evade.Tank; // Default + if (target == null) return false; + var mob = LookupTarget(target); + if (mob == null) target.CreateTextEffect("Mob not found."); + return mob != null && mob.ShouldDodge(target.GetSpellCasted(), out evade); + } + } +} diff --git a/Albion/Merlin/Profiles/Expedition/Expedition.Navigation.cs b/Albion/Merlin/Profiles/Expedition/Expedition.Navigation.cs new file mode 100644 index 0000000..69502ce --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Expedition.Navigation.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using YinYang.CodeProject.Projects.SimplePathfinding.PathFinders.AStar; + +namespace Merlin.Profiles.Expedition { + partial class Expedition { + + ExpeditionAgentObjectView GetEntrancePortal() { + List expeditionAgentList = _client.GetEntities(IsValidAgent); + ExpeditionAgentObjectView agent = expeditionAgentList.FirstOrDefault(); + return agent; + } + + ExpeditionExitObjectView GetExitPortal() { + List portalList = _client.GetEntities(IsValidPortal); + ExpeditionExitObjectView agent = portalList.FirstOrDefault(); + return agent; + } + + bool IsValidAgent(ExpeditionAgentObjectView agent) { + return agent != null; + } + + bool IsValidPortal(ExpeditionExitObjectView portal) { + return portal != null; + } + + Vector3 GetDungeonEndPoint() { + return T3_EXPEDITION_ENDING_POINT; + } + + void FindPathToEnd() { + targetPoint = GetDungeonEndPoint(); + _localPlayerCharacterView.TryFindPath(new ClusterPathfinder(), targetPoint, IsBlocked, out targetPoints); + path = new ExpeditionPathingRequest(_localPlayerCharacterView, targetPoint, targetPoints); + pathFound = true; + } + + public bool IsBlocked(Vector2 location) { + var vector = new Vector3(location.x, 0, location.y); + + if (_localPlayerCharacterView != null) { + var playerLocation = new Vector2(_localPlayerCharacterView.transform.position.x, _localPlayerCharacterView.transform.position.z); + var distance = (playerLocation - location).magnitude; + + if (distance < 2f) + return false; + } + + return (_client.Collision.GetFlag(vector, 1.0f) > 0); + } + } +} diff --git a/Albion/Merlin/Profiles/Expedition/Expedition.Restart.cs b/Albion/Merlin/Profiles/Expedition/Expedition.Restart.cs new file mode 100644 index 0000000..19cc50c --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Expedition.Restart.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.Linq; +using Merlin.API; + +namespace Merlin.Profiles.Expedition { + partial class Expedition { + + private bool pathFound; + + void Restart() { + if (NeedRepair()) + SetState(State.Repair); + + if (!IsInExpedition()) + EnterDungeon(); + else { + if (_client.State != GameState.Playing) return; + if (!pathFound && elapsedSeconds > 5) { + // 5 sec to load exped + FindPathToEnd(); + SetState(State.Continue); + } + } + } + + bool IsInExpedition() { + if (_world.CurrentCluster == null) + return true; + return false; + } + + void EnterDungeon() { + var player = _localPlayerCharacterView; + Core.Log("[Expedition] Entering Dungeon."); + + var agentDialogOpened = GameGui.Instance.ExpeditionAgentGui.isActiveAndEnabled; + var entryDialogOpened = GameGui.Instance.ExpeditionAgentGui.ExpeditionAgentDetailsGui.isActiveAndEnabled; + + var agent = GetEntrancePortal(); + + // Walk to agent. + if (!agentDialogOpened) { + player.Interact(agent); + elapsedSeconds = 0; + } + + if (agentDialogOpened && !entryDialogOpened && elapsedSeconds > 0.2f) { + var entries = GameGui.Instance.ExpeditionAgentGui.ExpeditionSelectionList; + entries[0].OnClicked(); + elapsedSeconds = 0; + } + + if (agentDialogOpened && entryDialogOpened && elapsedSeconds > 0.2f) { + GameGui.Instance.ExpeditionAgentGui.ExpeditionAgentDetailsGui?.OnRegistrationClicked(); + elapsedSeconds = 0; + } + } + + bool NeedRepair() { + Core.Log("[Expedition] Checking if player needs to repair."); + return false; + } + + RepairBuildingView GetRepairBuilding() { + List repairBuildingList = _client.GetEntities(IsValidRepair); + RepairBuildingView repairBuilding = repairBuildingList.FirstOrDefault(); + return repairBuilding; + } + + bool IsValidRepair(RepairBuildingView view) { + return view != null; + } + + private bool hasRepaired = false; + + void Repair() { + var player = _localPlayerCharacterView; + + var repairDialogOpened = GameGui.Instance.BuildingUsageAndManagementGui.BuildingUsage.RepairItemView.isActiveAndEnabled; + var payDialogOpened = GameGui.Instance.PaySilverDetailGui.isActiveAndEnabled; + + RepairBuildingView rbv = GetRepairBuilding(); + var minimumDistance = rbv.GetColliderExtents() + player.GetColliderExtents(); + + var directionToPlayer = (player.transform.position - rbv.transform.position).normalized; + var bufferDistance = directionToPlayer * minimumDistance; + + var currentNode = rbv.transform.position + bufferDistance; + + if (!repairDialogOpened) { + Core.Log("Moving to RBV"); + player.Interact(rbv); + } + + if (repairDialogOpened && !hasRepaired) { + Core.Log("Using RBV"); + GameGui.Instance.BuildingUsageAndManagementGui.BuildingUsage.RepairItemView.OnClickRepairAllButton(); + } + + if (payDialogOpened && !hasRepaired) { + Core.Log("Paying..."); + GameGui.Instance.PaySilverDetailGui.OnPay(); + hasRepaired = true; + elapsedSeconds = 0; + } + + if (hasRepaired && elapsedSeconds > 5) { + Core.Log("Finished Repairing"); + hasRepaired = false; + SetState(State.Restart); + } + } + } +} diff --git a/Albion/Merlin/Profiles/Expedition/Expedition.cs b/Albion/Merlin/Profiles/Expedition/Expedition.cs new file mode 100644 index 0000000..2f32d38 --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Expedition.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using Merlin.Profiles.Expedition.Mobs; +using UnityEngine; + +namespace Merlin.Profiles.Expedition { + public partial class Expedition : Profile { + + public enum State { + Continue, + Repair, + Restart + } + + public override string Name => "Expedition"; + private readonly Vector3 T3_EXPEDITION_ENDING_POINT = new Vector3(-182.0243f, 0f, 115.7252f); + private const float GUI_X = 70; + private const float GUI_Y = 135; + private const float GUI_W = 296; + + private Mob[] mobs = { + new OverseerMob(), + new MageMinibossMob(), + new HereticTankMob(), + new HereticMageMob(), + new HereticArcherMob() + }; + + private static State state = State.Restart; + public static void SetState(State newState) => state = newState; + public static State GetState() => state; + + private static Combat Combat => Combat.Instance; + + private List targetPoints; + private ExpeditionPathingRequest path; + private Vector3 targetPoint; + private Vector3 stillPos; + + private bool guiEnabled = true; + + private float stillTimer; + private float lastStill; + + private float secondCount; + private float elapsedSeconds; + + protected override void OnStart() { + targetPoints = new List(); + SetState(State.Restart); + Combat.SetDodgeDelegate(ShouldDodge); + Combat.SetInterruptDelegate(ShouldInterrupt); + } + + protected override void OnStop() { + path = null; + targetPoints = null; + Combat.ResetDelegates(); + } + + protected override void OnUpdate() { + var curSeconds = Stopwatch.GetTimestamp() / Stopwatch.Frequency; // Gets seconds + elapsedSeconds += curSeconds - secondCount; + secondCount = curSeconds; + + if (Input.GetKeyDown(KeyCode.F2)) + guiEnabled = !guiEnabled; + + // Will handle fighting. + Combat.Update(); + + if (!Combat.IsState(Combat.State.Idle)) return; + + switch (state) { + case State.Continue: + Continue(); + break; + case State.Repair: + Repair(); + break; + case State.Restart: + Restart(); + break; + } + } + + void Continue() { + var player = _localPlayerCharacterView; + + if (!IsInExpedition()) + SetState(State.Restart); + + // Standstill timer + var pos = player.GetPosition(); + var curSeconds = Stopwatch.GetTimestamp() / Stopwatch.Frequency; // Gets seconds + if (pos == stillPos) { + stillTimer += curSeconds - lastStill; + if (stillTimer > 30) // We have stood still for 30 seconds. + path = null; + } else { + stillTimer = 0; + } + + if (path != null) { + if (path.IsRunning) { + path.Continue(); + } else { + path = null; + player.Interact(GetExitPortal()); + } + } + + // Always update the timer, so we don't get an infinity-second "boost" accidentally + lastStill = curSeconds; + stillPos = player.GetPosition(); + } + + void OnGUI() { + if (guiEnabled) { + var playerPos = _localPlayerCharacterView.GetPosition(); + GUI.Label(new Rect(GUI_X + 8, GUI_Y, GUI_W, 18), "Position: " + playerPos.x + ", " + playerPos.y + ", " + playerPos.z); + GUI.Label(new Rect(GUI_X + 8, GUI_Y + 18, GUI_W, 18), "Target: " + targetPoint.x + ", " + targetPoint.y + ", " + targetPoint.z); + GUI.Label(new Rect(GUI_X + 8, GUI_Y + 36, GUI_W, 18), "State: " + Combat.GetStateString(Combat.GetState())); + GUI.Label(new Rect(GUI_X + 8, GUI_Y + 54, GUI_W, 18), "Time: " + Convert.ToString(elapsedSeconds, CultureInfo.InvariantCulture)); + + Combat.Debug(); + } + } + } +} diff --git a/Albion/Merlin/Profiles/Expedition/ExpeditionPathingRequest.cs b/Albion/Merlin/Profiles/Expedition/ExpeditionPathingRequest.cs new file mode 100644 index 0000000..41bc8b1 --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/ExpeditionPathingRequest.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Stateless; +using UnityEngine; + +namespace Merlin.Profiles.Expedition { + class ExpeditionPathingRequest { + + private bool _useCollider; + + private LocalPlayerCharacterView _player; + + private List _path; + private List _fleePoints; + + private StateMachine _state; + private Vector3 _target; + + public bool IsRunning => _state.State != State.Finish; + + public ExpeditionPathingRequest(LocalPlayerCharacterView player, Vector3 target, List path, bool useCollider = true) { + _player = player; + _target = target; + _path = path; + _useCollider = useCollider; + + _fleePoints = new List(); + + _state = new StateMachine(State.Start); + _state.Configure(State.Start) + .Permit(Trigger.ApprachTarget, State.Running) + .Permit(Trigger.ReachedTarget, State.Finish); + _state.Configure(State.Running) + .Permit(Trigger.ReachedTarget, State.Start); + } + + public void Continue() { + switch (_state.State) { + case State.Start: + if (_path.Count > 0) + _state.Fire(Trigger.ApprachTarget); + else + _state.Fire(Trigger.ReachedTarget); + + break; + + case State.Running: + var currentNode = _path[0]; + var minimumDistance = 3f; + + if (_path.Count < 2 && _useCollider) { + minimumDistance = 1.0f + _player.GetColliderExtents(); + + var directionToPlayer = (_player.transform.position - _target).normalized; + var bufferDistance = directionToPlayer * minimumDistance; + + currentNode = _target + bufferDistance; + } + + var distanceToNode = (_player.transform.position - currentNode).sqrMagnitude; + + if (distanceToNode < minimumDistance) { + _fleePoints.Add(currentNode); + _path.RemoveAt(0); + } else { + _player.RequestMove(currentNode); + } + + _state.Fire(Trigger.ReachedTarget); + break; + } + } + + // Reverses Continue, to follow the points backwards + public void Flee() { + var lastInd = _fleePoints.Count - 1; + var currentNode = _fleePoints[lastInd]; + var minimumDistance = 3f; + + if (_fleePoints.Count < 2 && _useCollider) { + minimumDistance = 1.0f + _player.GetColliderExtents(); + + var directionToPlayer = (_player.transform.position - _target).normalized; + var bufferDistance = directionToPlayer * minimumDistance; + + currentNode = _target + bufferDistance; + } + + var distanceToNode = (_player.transform.position - currentNode).sqrMagnitude; + + if (distanceToNode < minimumDistance) { + _path.Insert(0, currentNode); + _fleePoints.RemoveAt(lastInd); + } else { + _player.RequestMove(currentNode); + } + } + + private enum Trigger { + ApprachTarget, + ReachedTarget, + } + + private enum State { + Start, + Running, + Finish + } + } +} diff --git a/Albion/Merlin/Profiles/Expedition/Mob.cs b/Albion/Merlin/Profiles/Expedition/Mob.cs new file mode 100644 index 0000000..4432d32 --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Mob.cs @@ -0,0 +1,58 @@ +using SpellCategory = gz.SpellCategory; +using SpellTarget = gz.SpellTarget; +using Evade = Merlin.Profiles.Combat.Evade; +// ReSharper disable All + +namespace Merlin.Profiles.Expedition { + abstract class Mob { + + public class Spell { + public SpellCategory Category { get; } + public SpellTarget Target { get; } + public Evade Evade { get; } + public string Name { get; } + + public bool Interruptable { get; set; } + + public Spell(SpellCategory category, SpellTarget target, Evade evade, string name = null) { + Category = category; + Target = target; + Evade = evade; + Name = name; + Interruptable = true; + } + + public bool Equals(gz spell) { + return Category == spell.d4 && Target == spell.d1 && string.Equals(Name, spell.d6); + } + } + + public abstract string Name { get; } + public abstract Spell[] Spells { get; } + + public Spell LookupSpell(gz spell) { + if (spell == null) return null; + for (int i = Spells.Length - 1; i >= 0; i--) { + if (Spells[i].Equals(spell)) + return Spells[i]; + } + + return null; + } + + public bool Equals(FightingObjectView view) => view.PrefabName.Equals(Name); + + public bool ShouldDodge(gz spell, out Evade evade) { + evade = Evade.Tank; + var mobSpell = LookupSpell(spell); + if (mobSpell == null) return false; + evade = mobSpell.Evade; + return true; + } + + public bool ShouldInterrupt(gz spell) { + var mobSpell = LookupSpell(spell); + return mobSpell != null && mobSpell.Interruptable; + } + } +} diff --git a/Albion/Merlin/Profiles/Expedition/Mobs/HereticArcherMob.cs b/Albion/Merlin/Profiles/Expedition/Mobs/HereticArcherMob.cs new file mode 100644 index 0000000..1f80426 --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Mobs/HereticArcherMob.cs @@ -0,0 +1,8 @@ +namespace Merlin.Profiles.Expedition.Mobs { + class HereticArcherMob : Mob { + public override string Name => "MOB_HERETIC_ARCHER_01"; + public override Spell[] Spells => new Spell[1] { + new Spell(gz.SpellCategory.Damage, gz.SpellTarget.Ground, Combat.Evade.Left) + }; + } +} diff --git a/Albion/Merlin/Profiles/Expedition/Mobs/HereticMageMob.cs b/Albion/Merlin/Profiles/Expedition/Mobs/HereticMageMob.cs new file mode 100644 index 0000000..30e4af3 --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Mobs/HereticMageMob.cs @@ -0,0 +1,9 @@ +namespace Merlin.Profiles.Expedition.Mobs { + class HereticMageMob : Mob { + public override string Name => "MOB_HERETIC_MAGE_01"; + + public override Spell[] Spells => new Spell[1] { + new Spell(gz.SpellCategory.Damage, gz.SpellTarget.Ground, Combat.Evade.Left, "melee_areadamage") + }; + } +} diff --git a/Albion/Merlin/Profiles/Expedition/Mobs/HereticMinibossMob.cs b/Albion/Merlin/Profiles/Expedition/Mobs/HereticMinibossMob.cs new file mode 100644 index 0000000..b360d1e --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Mobs/HereticMinibossMob.cs @@ -0,0 +1,9 @@ +namespace Merlin.Profiles.Expedition.Mobs { + class MageMinibossMob : Mob { + public override string Name => "MOB_HERETIC_MAGE_MINIBOSS_01"; + + public override Spell[] Spells => new Spell[1] { + new Spell(gz.SpellCategory.Damage, gz.SpellTarget.Ground, Combat.Evade.Behind) + }; + } +} diff --git a/Albion/Merlin/Profiles/Expedition/Mobs/HereticTankMob.cs b/Albion/Merlin/Profiles/Expedition/Mobs/HereticTankMob.cs new file mode 100644 index 0000000..344fd24 --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Mobs/HereticTankMob.cs @@ -0,0 +1,8 @@ +namespace Merlin.Profiles.Expedition.Mobs { + class HereticTankMob : Mob { + public override string Name => "MOB_HERETIC_TANK_01"; + public override Spell[] Spells => new Spell[1] { + new Spell(gz.SpellCategory.CrowdControl, gz.SpellTarget.Ground, Combat.Evade.Behind, "melee_shieldslam") + }; + } +} diff --git a/Albion/Merlin/Profiles/Expedition/Mobs/OverseerMob.cs b/Albion/Merlin/Profiles/Expedition/Mobs/OverseerMob.cs new file mode 100644 index 0000000..bd6af18 --- /dev/null +++ b/Albion/Merlin/Profiles/Expedition/Mobs/OverseerMob.cs @@ -0,0 +1,13 @@ +namespace Merlin.Profiles.Expedition.Mobs { + class OverseerMob : Mob { + public override string Name => "MOB_HERETIC_OVERSEER_BOSS_01"; + public sealed override Spell[] Spells => new Spell[2] { + new Spell(gz.SpellCategory.Damage, gz.SpellTarget.Ground, Combat.Evade.Away), + new Spell(gz.SpellCategory.Damage, gz.SpellTarget.Ground, Combat.Evade.Behind, "melee_standardstrike") + }; + + public OverseerMob() { + Spells[1].Interruptable = false; + } + } +}