What's new
  • Visit Rebornbuddy
  • Visit Panda Profiles
  • Visit LLamamMagic
  • Visit Resources
  • Visit Downloads
  • Visit Portal

Guide to Writing Combat Routines

xzjv

Community Developer
Joined
Mar 17, 2014
Messages
1,243
Reaction score
46
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.webp

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.webp


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.webp

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:
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?
 
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?

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

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:[email protected] ";

        /// <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:
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?
 
Back
Top