• Visit Rebornbuddy
  • Guide to Writing Combat Routines

    Discussion in 'Trinity' started by xzjv, Jan 20, 2017.

    1. xzjv

      xzjv Community Developer

      Joined:
      Mar 17, 2014
      Messages:
      1,243
      Likes Received:
      46
      Trophy Points:
      48
      This guide is for people wanting to create their own combat routines in Trinity. There is a lot of info that could be covered on this topic so ill do this first pass and then expand on it later if people are interested.


      What is a combat routine?

      Combat routines decide which powers should be cast, where they should be cast and how the bot moves around when fighting enemies. Trinity provides many routines for each class by default; and provides a way for coding-inclined users to make their own custom routines.


      How do i use them?

      Appropriate routines are selected Automatically - When you start DemonBuddy or open the trinity Settings window it scans your hero for equipped items and uses that information to automatically select the best routine available for you. Settings for each routine can be found in Trinity Settings/Config under the 'Routines' tab.

      Routine_Auto.png

      You can choose a specific routine - You have the option to force Trinity to use the routine you want. This is particularly useful if there are multiple versions of a build for your class, or if you've been customizing an existing routine to suit your needs, or maybe you want to try experimental/beta routines that aren't yet ready to be placed onto auto-detection.

      Routine_Force.png


      What does the routine code look like?


      The routines can be found in your Trinity folder: \Plugins\Trinity\Routines\

      A basic routine could look something like the code below. So lets break down the parts.

      .
      GetOffensivePower() is called when in combat. Trinity says 'hey routine guy, give me a power to attack this monster with' and your routine provides that information.

      GetDefensivePower() is called right before avoidance takes place. As an opportunity to cast life-saving type spells.

      GetBuffPower() is called always (except when dead etc). Asks routine for a buff spell to cast.

      GetDestructiblePower() is called when the current target is a destructible container/door/barricade.

      GetMovementPower() is called always for movement. Literally any DB plugin and DB itself will call to your routine for a power to move with. Making it very useful for skills like whirlwind that replace walking.

      Code:
          
          public class MyBasicMonkRoutine : MonkBase, IRoutine
          {
              public string DisplayName => "Example Monk Routine with no settings";
              public string Description => "A very simple example build ";
              public string Author => "xzjv";
              public string Version => "0.1";
              public string Url => string.Empty;
              public Build BuildRequirements => null;
              public IDynamicSetting RoutineSettings => null;
      
              public TrinityPower GetOffensivePower()
              {
                  TrinityPower power;
      
                  if (TrySpecialPower(out power))
                      return power;
      
                  if (TrySecondaryPower(out power))
                      return power;
      
                  if (TryPrimaryPower(out power))
                      return power;
      
                  if (IsNoPrimary)
                      return Walk(CurrentTarget);
      
                  return null;
              }
      
              public TrinityPower GetDefensivePower() => GetBuffPower();
      
              public TrinityPower GetBuffPower() => DefaultBuffPower();
      
              public TrinityPower GetDestructiblePower() => DefaultDestructiblePower();
      
              public TrinityPower GetMovementPower(Vector3 destination) => Walk(destination);
      
          }
      
      

      IRoutine

      At the heart of the system is the interface IRoutine - a contract that defines all the things a routine is expected to provide. The base classes make it so you don't really have to worry about these details.

      Any .cs file in the assembly that implements IRoutine will be found and shown in the Routine settings tab. It will have any .Xaml settings displayed and automatically have Save/Load/Import/Export handled.

      If you know what you're doing you can use some of the more advanced parts of your IRoutine implementation to take more control of the bot, such as changing weighting, avoidance, target selection. Here's the full spec:


      Code:
      using System;
      using System.Threading.Tasks;
      using Trinity.Components.Combat;
      using Trinity.Components.Combat.Resources;
      using Trinity.Framework.Actors.ActorTypes;
      using Trinity.Framework.Objects;
      using Trinity.Settings;
      using Zeta.Common;
      using Zeta.Game;
      
      namespace Trinity.Routines
      {
          public interface IRoutine
          {
              string DisplayName { get; }
              string Description { get; }
              string Author { get; }
              string Version { get; }
              ActorClass Class { get; }
              string Url { get; }
              Build BuildRequirements { get; }
              IDynamicSetting RoutineSettings { get; }
      
              // Kiting
              KiteMode KiteMode { get; }
              float KiteDistance { get; }
              int KiteStutterDuration { get; }
              int KiteStutterDelay { get; }
              int KiteHealthPct { get; }               
      
              // Range
              float TrashRange { get; }
              float EliteRange { get; }
              float HealthGlobeRange { get; }
              float ShrineRange { get; }        
      
              // Cluster
              float ClusterRadius { get; }
              int ClusterSize { get; }
      
              // Misc
              int PrimaryEnergyReserve { get; }
              int SecondaryEnergyReserve { get; }
      
              float EmergencyHealthPct { get; }
      
              // Power Selection
              TrinityPower GetOffensivePower();
              TrinityPower GetDefensivePower();
              TrinityPower GetBuffPower();
              TrinityPower GetDestructiblePower();
              TrinityPower GetMovementPower(Vector3 destination);
      
              // Hardcore Overrides        
              Task<bool> HandleKiting();
              Task<bool> HandleAvoiding();
              Task<bool> HandleTargetInRange();
              Task<bool> MoveToTarget();
              bool SetWeight(TrinityActor cacheObject);
      
              // Temporary Overrides        
              Func<bool> ShouldIgnoreNonUnits { get; }
              Func<bool> ShouldIgnorePackSize { get; }
              Func<bool> ShouldIgnoreAvoidance { get; }
              Func<bool> ShouldIgnoreKiting { get; }
              Func<bool> ShouldIgnoreFollowing { get; }
      
          }
      
      }


      Editing Tools

      In terms of productivity and avoiding basic syntax errors i strongly recommend you download VisualStudio Community edition.
      Free IDE and Tools | Visual Studio Community

      If you're familiar with VisualStudio and C# you should grab the solution from SVN unifiedtrinity.Master - Revision 290: / and fix the references to point to the files in your DB folder (Demonbuddy.exe, GreyMagic.dll, IronPython.dll, Microsoft.Dynamic.dll, Microsoft.Scripting.dll) and also check the post-build script which is currently set to copy files around and change/remove it as needed.


      Understanding the Base Classes

      Each class (monk/barb etc) has a 'base' that contains commonly used stuff like all the skills you can cast. This means your routine is able to focus just on what is different and special about the build without duplicating all the boring stuff.

      You do this by 'inheriting' from the base. e.g. In the previous example MyBasicMonkRoutine inherits from MonkBase.

      Code:
          public class MyBasicMonkRoutine : MonkBase, IRoutine
      
      inherit.jpg

      To find out what is in the base class just open the file MonkBase.cs located in the same routines folder. You'll see a lot of boilerplate code you can use or duplicate with changes in your routine. For example

      Code:
              protected static bool HasShenLongBuff
                  => Core.Buffs.HasBuff(SNOPower.P3_ItemPassive_Unique_Ring_026, 1);
      
      
              protected static bool HasRaimentDashBuff
                  => Core.Buffs.HasBuff(SNOPower.P2_ItemPassive_Unique_Ring_033, 2);
      
      
              protected static bool HasSpiritGuardsBuff
                  => Core.Buffs.HasBuff(SNOPower.P2_ItemPassive_Unique_Ring_034, 1);
      
      
              protected virtual TrinityPower FistsOfThunder(TrinityActor target)
                  => new TrinityPower(SNOPower.Monk_FistsofThunder, MeleeAttackRange, target.AcdId);
      
      
              protected virtual TrinityPower DeadlyReach(TrinityActor target)
                  => new TrinityPower(SNOPower.Monk_DeadlyReach, MeleeAttackRange, target.AcdId);
      
      Each base for a player class (barb/monk etc) itself inherits from another base, which contains more common resources that all routines may use. This one is called 'RoutineBase.cs' and located in Trinity\Routines\ folder.

      Some examples of the resources in RoutineBase:

      Code:
              protected static bool IsMultiSpender
                  => SkillUtils.Active.Count(s => s.IsAttackSpender) > 1;
      
      
              protected static bool IsNoPrimary
                  => SkillUtils.Active.Count(s => s.IsGeneratorOrPrimary) == 0;
      
      
              protected static bool ShouldRefreshBastiansGenerator
                  => Sets.BastionsOfWill.IsFullyEquipped && !Core.Buffs.HasBastiansWillGeneratorBuff
                  && SpellHistory.TimeSinceGeneratorCast >= 3750;
      
      
              protected static bool ShouldRefreshBastiansSpender
                  => Sets.BastionsOfWill.IsFullyEquipped && !Core.Buffs.HasBastiansWillGeneratorBuff
                  && SpellHistory.TimeSinceSpenderCast >= 3750;
      
      
              protected static int EndlessWalkOffensiveStacks
                  => Core.Buffs.GetBuffStacks(447541, 1);
      
      
              protected static int EndlessWalkDefensiveStacks
                  => Core.Buffs.GetBuffStacks(447541, 2);
      


      Overriding Cast Conditions

      Most of the default routines are leveraging the base methods:


      • TrySpecialPower
      • TrySecondaryPower
      • TryPrimaryPower

      Code:
              public TrinityPower GetOffensivePower()
              {
                  TrinityPower power;
      
      
                  if (TrySpecialPower(out power))
                      return power;
      
      
                  if (TrySecondaryPower(out power))
                      return power;
      
      
                  if (TryPrimaryPower(out power))
                      return power;
      
      
                  if (IsNoPrimary)
                      return Walk(CurrentTarget);
      
      
                  return null;
              }
      
      Alternatively, you could write this without the helper methods as:

      Code:
              public TrinityPower GetOffensivePower()
              {
                  TrinityActor target;
                  TrinityPower power;
                  Vector3 position;
      
                  if (ShouldCycloneStrike())
                      return CycloneStrike();
      
                  if (ShouldExplodingPalm(out target))
                      return ExplodingPalm(target);
      
                  if (ShouldTempestRush(out position))
                      return TempestRush(position);
      
                  if (ShouldDashingStrike(out position))
                      return DashingStrike(position);
      
                  if (ShouldSevenSidedStrike(out target))
                      return SevenSidedStrike(target);
      
                  if (ShouldWaveOfLight(out target))
                      return WaveOfLight(target);
      
                  if (ShouldLashingTailKick(out target))
                      return LashingTailKick(target);
      
                  if (ShouldFistsOfThunder(out target))
                      return FistsOfThunder(target);
      
                  if (ShouldDeadlyReach(out target))
                      return DeadlyReach(target);
      
                  if (ShouldCripplingWave(out target))
                      return CripplingWave(target);
      
                  if (ShouldWayOfTheHundredFists(out target))
                      return WayOfTheHundredFists(target);
      
                  if (IsNoPrimary)
                      return Walk(CurrentTarget);
      
                  return null;
              }
      
      How you structure your routine and how much of the base resources you leverage is entirely up to you. You could skip all the helper stuff and simply write old style routine code.

      Code:
              public TrinityPower GetOffensivePower()
              {
                  if (Skills.WitchDoctor.SpiritWalk.CanCast())
                  {
                      return SpiritWalk();
                  }
      
                   if (Player.CurrentHealthPct < EmergencyHealthPct)
                  {
                      return new TrinityPower(SNOPower.Walk, 7f, TargetUtil.BestWalkLocation(45f, true));
                  }
      
                  return null;
              }
      
      Lets assume that you want to use "TrySecondaryPower" but also change the condition for when SevenSidedStrike is cast, or who its cast on. How would that work?

      What you would do is 'override' the base method for "ShouldSevenSidedStrike" causing TrySecondaryPower to use your version instead of the default one.

      You can go and look at the default in MonkBase.cs and copy/paste it into your routine, and add the 'override' keyword.

      Code:
              protected override bool ShouldSevenSidedStrike(out TrinityActor target)
              {
                  target = null;
      
      
                  if (!Skills.Monk.SevenSidedStrike.CanCast())
                      return false;
      
      
                  if (!TargetUtil.AnyMobsInRange(45f) && !CurrentTarget.IsTreasureGoblin)
                      return false;
      
      
                  target = TargetUtil.GetBestClusterUnit() ?? CurrentTarget;
                  return target != null;       
              }
      
      This method does two things, it returns a true/false answer to the question, Should we cast SevenSidedStrike, and also specifies which target it should be cast on.



      How do i learn some basic C#?

      Take a look at this video tutorial series: https://app.pluralsight.com/player?...ive&clip=0&course=csharp-fundamentals-csharp5

      There is also this YouTube series that comes highly recommended How to program in C# - Beginner Course - YouTube

      Some free e-book/PDF by Rob Miles on c# http://www.robmiles.com/s/CSharp-Book-2016-Rob-Miles-82.pdf

      To be continued.
       
      Last edited: Feb 8, 2017
      Siriso and Sqnc like this.
    2. xzjv

      xzjv Community Developer

      Joined:
      Mar 17, 2014
      Messages:
      1,243
      Likes Received:
      46
      Trophy Points:
      48
      reserved
       
    3. Yndras

      Yndras New Member

      Joined:
      Oct 2, 2015
      Messages:
      6
      Likes Received:
      0
      Trophy Points:
      1
      Thanks for taking the time to write this :D
       
    4. muser11

      muser11 Member

      Joined:
      Sep 19, 2012
      Messages:
      55
      Likes Received:
      0
      Trophy Points:
      6
      Amazing TY !!
       
    5. Night Breeze

      Night Breeze Member

      Joined:
      Jan 28, 2017
      Messages:
      100
      Likes Received:
      14
      Trophy Points:
      18
      Thanks a lot.I have a problem, is routine auto load? For example, i write my wizard routine, if i move the file to Plugin\Trinity\Routines\Wizard\. The plugin will automatically load it?
       
    6. xzjv

      xzjv Community Developer

      Joined:
      Mar 17, 2014
      Messages:
      1,243
      Likes Received:
      46
      Trophy Points:
      48
      Yes it looks through all the classes in the assembly and finds everything that implements IRoutine, technically it doesn't matter where the files are located since they all get compiled into the trinity dll, but they're all in the routines folder for organizational reasons.
       
    7. Night Breeze

      Night Breeze Member

      Joined:
      Jan 28, 2017
      Messages:
      100
      Likes Received:
      14
      Trophy Points:
      18
      I copy files to Plugins folder, when i run db, i got an exception.System.Runtime.Serialization.InvalidDataContractException: Type cannot be serialized “Trin.Routines.Wizard.WizardParalysisArchonVyrTalObsidian".

      this is my code, where is wrong?

      Code:
      using System;
      using System.Collections.Generic;
      using System.ComponentModel;
      using System.Windows.Controls;
      using Trinity.Components.Combat.Resources;
      using Trinity.Framework.Helpers;
      using Trinity.Framework.Objects;
      using Trinity.Reference;
      using Trinity.Routines;
      using Trinity.Routines.Wizard;
      using Trinity.Settings;
      using Trinity.UI;
      using Zeta.Common;
      
      namespace Trin.Routines.Wizard
      {
          class WizardParalysisArchonVyrTalObsidian : WizardBase, IRoutine
          {
              #region About
              /// <summary>
              /// 显示在战斗策略中的选项名称
              /// </summary>
              public string DisplayName => "Vyr Tal 5+3 Obsidian Archon Paralysis";
      
              /// <summary>
              /// 策略的说明
              /// </summary>
              public string Description => "Need CDR 60+ and Ancient Weapon if you have. My QQ group num 572248802, email:59285120@qq.com ";
      
              /// <summary>
              /// 作者
              /// </summary>
              public string Author => "Night Breeze";
      
              /// <summary>
              /// 版本号
              /// </summary>
              public string Version => "1.0.5";
      
              /// <summary>
              /// 配装地址
              /// </summary>
              public string Url => "https://www.thebuddyforum.com/demonbuddy-forum/combat-routines/295084-paralysis-vyr-tal-5-3-obsidian-v1-0-4-a.html";
      
              #endregion
      
              #region Build Definition
              /// <summary>
              /// 配装需求,满足条件会自动切换策略
              /// </summary>
              public Build BuildRequirements => new Build
              {
                  // 套装检查
                  Sets = new Dictionary<Set, SetBonus>
                  {
                      // 塔拉夏的法理 3件套效果
                      { Sets.TalRashasElements, SetBonus.Third },
                      // 维尔的神装 2件套效果
                      { Sets.VyrsAmazingArcana, SetBonus.Second }
                  },
                  // 技能检查
                  Skills = new Dictionary<Skill, Rune>
                  {
                      // 御法者-烈焰爆发
                      { Skills.Wizard.Archon, Runes.Wizard.Combustion },
                      // 魔法武器-偏斜护盾
                      { Skills.Wizard.MagicWeapon, Runes.Wizard.Deflection },
                      // 黑洞-法术窃取
                      { Skills.Wizard.BlackHole, Runes.Wizard.Spellsteal },
                      // 能量护甲-原力护甲
                      { Skills.Wizard.EnergyArmor, Runes.Wizard.ForceArmor },
                      // 冰霜新星-极速冰冻
                      { Skills.Wizard.FrostNova, Runes.Wizard.ColdSnap},
                      // 奥术洪流-静电放射
                      { Skills.Wizard.ArcaneTorrent, Runes.Wizard.StaticDischarge}
                  },
                  // 物品检查
                  Items = new List<Item>
                  {
                      // 马纳德的治疗
                      Legendary.ManaldHeal,
                      // 黄道黑曜石之戒
                      Legendary.ObsidianRingOfTheZodiac,
                      // 皇家华戒
                      Legendary.RingOfRoyalGrandeur,
                      // 寅剑
                      Legendary.Ingeom
                  }
              };
              #endregion
      
              #region BuffPower
              /// <summary>
              /// 保持buff
              /// </summary>
              /// <returns></returns>
              public TrinityPower GetBuffPower()
              {
                  // 御法者未激活
                  if (!IsArchonActive)
                  {
                      // 使用三刀甲
                      if (!Skills.Wizard.EnergyArmor.IsActive && Skills.Wizard.EnergyArmor.CanCast())
                          return EnergyArmor();
      
                      // 使用魔法武器
                      if (!Skills.Wizard.MagicWeapon.IsActive && Skills.Wizard.MagicWeapon.CanCast())
                          return MagicWeapon();
      
                      // 使用御法者
                      if (!Skills.Wizard.Archon.IsActive && Skills.Wizard.Archon.CanCast() && !Player.IsInTown)
                          return Archon();
      
                      // 使用冰霜新星
                      if (Skills.Wizard.FrostNova.CanCast())
                          return FrostNova();
                  }
                  // 御法者已激活
                  else
                  {
                      // 使用时间延缓
                      if (!Skills.Wizard.ArchonSlowTime.IsActive && Skills.Wizard.ArchonSlowTime.CanCast())
                          return ArchonSlowTime();
      
                      // 如果移动中,使用闪电冲击
                      if (Player.IsMoving && Skills.Wizard.ArchonBlast.CanCast())
                          return ArchonBlast();
                  }
                  
                  return null;
              }
              #endregion
      
              #region DefensivePower
              public TrinityPower GetDefensivePower()
              {
                  return null;
              }
              #endregion
      
              #region DestructiblePower
              public TrinityPower GetDestructiblePower()
              {
                  
                  // 如果御法者已经激活
                  if (IsArchonActive)
                  {
                      //使用溃解光波
                      if (CurrentTarget.Distance > 25 && Skills.Wizard.ArchonDisintegrationWave.CanCast())
                          return ArchonDisintegrationWave(CurrentTarget);
                      // 使用
                      else if (Skills.Wizard.ArchonBlast.CanCast())
                          return ArchonBlast();
                  }
                  // 如果御法者未激活
                  else
                  {
                      // 使用奥术洪流
                      if (Skills.Wizard.ArcaneTorrent.CanCast())
                          return ArcaneTorrent(CurrentTarget);
                  }
      
                  return DefaultDestructiblePower();
              }
              #endregion
      
              #region MovementPower
              public TrinityPower GetMovementPower(Vector3 destination)
              {
                  if (CanTeleportTo(destination))
                      return Teleport(destination);
      
                  return Walk(destination);
              }
              #endregion
      
              #region OffensivePower
              public TrinityPower GetOffensivePower()
              {
                  if (IsArchonActive)
                  {
                      // 使用溃解光波
                      if (Skills.Wizard.ArchonDisintegrationWave.CanCast())
                      {
                          return ArchonDisintegrationWave(TargetUtil.GetBestClusterUnit() ?? CurrentTarget);
                      }
      
                      return null;
                  }
      
                  // 使用冰霜新星
                  if (Skills.Wizard.FrostNova.CanCast())
                      return FrostNova();
      
                  // 使用黑洞
                  if (Skills.Wizard.BlackHole.CanCast() && Player.PrimaryResourcePct > 0.25f)
                      return BlackHole(TargetUtil.GetBestClusterUnit() ?? CurrentTarget);
                  
                  // 使用奥术洪流
                  if (Skills.Wizard.ArcaneTorrent.CanCast())
                      return ArcaneTorrent(TargetUtil.GetBestClusterUnit() ?? CurrentTarget);
      
                  return Walk(CurrentTarget.Position);
              }
              #endregion
      
              #region Settings
              public override int ClusterSize => Settings.ClusterSize;
              public override float EmergencyHealthPct => Settings.EmergencyHealthPct;
              public IDynamicSetting RoutineSettings => Settings;
      
              public WizardParalysisArchonVyrTalObsidianSettings Settings { get; } = new WizardParalysisArchonVyrTalObsidianSettings();
      
              public sealed class WizardParalysisArchonVyrTalObsidianSettings : NotifyBase, IDynamicSetting
              {
                  private int _clusterSize;
                  private float _emergencyHealthPct;
      
                  [DefaultValue(8)]
                  public int ClusterSize
                  {
                      get { return _clusterSize; }
                      set { SetField(ref _clusterSize, value); }
                  }
      
                  [DefaultValue(0.4f)]
                  public float EmergencyHealthPct
                  {
                      get { return _emergencyHealthPct; }
                      set { SetField(ref _emergencyHealthPct, value); }
                  }
      
                  public override void LoadDefaults()
                  {
                      base.LoadDefaults();
                  }
      
                  #region IDynamicSetting
                  public string GetName() => GetType().Name;
                  public UserControl GetControl() => UILoader.LoadXamlByFileName<UserControl>(GetName() + ".xaml");
                  public object GetDataContext() => this;
                  public string GetCode() => JsonSerializer.Serialize(this);
                  public void ApplyCode(string code) => JsonSerializer.Deserialize(code, this, true);
                  public void Reset() => LoadDefaults();
                  public void Save() { }
                  #endregion
              }
              #endregion
          }
      }
      
      
       
      Last edited: Jan 31, 2017
    8. NoLongerAwayBoss

      NoLongerAwayBoss Member

      Joined:
      Feb 19, 2016
      Messages:
      175
      Likes Received:
      0
      Trophy Points:
      16
      Thanks for taking the time to share this xzjv. xD
       
    9. Geckoz

      Geckoz Member

      Joined:
      Aug 25, 2012
      Messages:
      157
      Likes Received:
      0
      Trophy Points:
      16
      Dear Xzjv

      Thanks for all your work.

      I have been modifying the barbariandefault profile along with the barbarianbase to make it run my IK/raekor charge/hota barb in a better way. Still some way to go, but one issue remains the biggest:

      In the log, it says, that power is missing for that target. The profile _wants_ a generator, but HOTA can sustain itself. How can I make the script accept HOTA the same way it does Bash or frenzy for those single targets?
       

    Share This Page