Make content

Custom blocks & scripts

When the built-in blocks can't do something, write a small C# class. It appears in the menus, explains itself in plain English, and runs in battles and simulations like any other block.

Its own script

The quickest way in: give one hero, enemy, item or status its own script. In its Inspector, the Files box has Add a script.

  1. Press Add a script

    The kit writes DireWolf.cs into the entity's folder: a custom effect named after it, with one number you can tune and an example to replace.

  2. Let Unity compile

    The kit adds the script to the entity as an ability named after it: Battle start for heroes, enemies and items, Turn start for statuses. Change the trigger or add conditions in the Inspector like any other ability.

  3. Write what it does

    Open script in the Files box. Replace the example in Run and the sentence in Describe.

[Serializable, TypeMenu("Scripts/Dire Wolf")]
public class DireWolf : Effect
{
    public int amount = 1;   // shows in the Inspector

    public override void Run(EffectContext c)
    {
        // an example to replace: gain Attack
        c.Engine.ChangeStat(c.Owner, StatType.Attack, amount, c.Label, c.Depth);
    }

    public override string Describe() { return "gain " + amount + " attack"; }
}

Good to knowWhy isn't every entity a script? A script per enemy means a recompile for every new enemy, content nobody can make or mod without code, and hundreds of files to keep in step. Assets keep content data-only; scripts are there for the few that need them.

The rest of this page is about writing blocks you reuse across many items.

Every ability is a trigger, conditions and effects. When the built-in blocks don't cover a mechanic, write your own: a small C# class that shows up in the inspector's pickers, describes itself in plain English, works in simulations and saves with your items. The 09 Custom Blocks demo has four complete examples.

The shortest effect

using System;
using AutoBattle;

namespace MyGame.Blocks
{
    [Serializable, TypeMenu("Custom/Heal Per Item")]
    public class HealPerItem : Effect
    {
        public int perItem = 1;

        public override void Run(EffectContext c)
        {
            c.Engine.Heal(c.Owner, perItem * c.Owner.Items.Count, c.Label, c.Depth);
        }

        public override string Describe() { return "heal " + perItem + " per item you own"; }
    }
}

Save it, let Unity compile, then open any item: Abilities > + > Effects > + > Custom › Heal Per Item. Its fields appear in the inspector, and the header reads "Battle start: heal 1 per item you own."

The shortest condition

[Serializable, TypeMenu("Custom/Behind On Health")]
public class BehindOnHealth : Condition
{
    public override bool Check(EffectContext c)
    {
        return c.Owner.Get(StatType.Health) < c.Opponent.Get(StatType.Health);
    }

    public override string Describe() { return "if you have less health than the enemy"; }
}

All of an ability's conditions must pass before its effects run. OnPass(c) runs after they all pass, if you need to record something (see "Keeping state").

Where the code goes

Put blocks in your own code, never inside the kit's folder (updates replace it).

  • For one entity: the Add a script button in any class, enemy, boss, item or status Inspector writes a block named after it into its folder (Enemies/Dire Wolf/DireWolf.cs) and adds it to that entity as an ability once Unity compiles. See Its own script above.

  • Simplest: any folder under Assets (Unity compiles it into Assembly-CSharp). Add using AutoBattle;.

  • Recommended: your own assembly. Create an Assembly Definition in your blocks folder and add AutoBattleKit.Runtime to its references, as Demos/09 Custom Blocks/AutoBattleKit.Samples.CustomBlocks.asmdef does. This compiles faster and keeps your battle code free of editor-only code.

Items save each block's class, namespace and assembly name. Renaming any of them breaks saved items unless you add Unity's [MovedFrom] attribute:

[Serializable, TypeMenu("Custom/Heal Per Item"), UnityEngine.Scripting.APIUpdating.MovedFrom(false, "MyGame.OldNamespace", null, "HealEachItem")]
public class HealPerItem : Effect { ... }

What a block can see: EffectContext

Member What it is
c.Engine The battle. Change it only through its actions (below). Also Rng, Round, Counters.
c.Owner Who owns the ability (the item's holder, the status's holder, the passive's fighter).
c.Opponent The enemy in front of the owner.
c.Event What triggered it: Type, Subject, Source (the attacker), Target, Amount (damage, heal or stacks), Kind (Strike, Poison...), StatusId.
c.Targets(t) / c.Release(list) Everyone a TargetRef means (All Enemies, Lowest Health Ally...). Give the list back when done. c.Resolve(t) returns just one.
c.Stacks For a status's own abilities: how many stacks the holder has.
c.Item, c.Tier, c.ItemPower The item the ability is on (null for passives and statuses), its tier and rarity-tier power.
c.Key A key unique to this ability on this fighter in this battle. Use it for state.
c.Label, c.Depth Pass them to engine actions so the log names the source and indents reactions.

A fighter (Combatant) has Get(StatType), Stacks(id), Items, Passives, Team, Dead, Wounded, Exposed and ItemTagCount(tag).

Changing the battle: engine actions

Always go through these, never set stats directly. They log, trigger reactions (thorns, lifesteal, "when healed"), play animations and sounds, and keep simulations and replays identical.

Action Does
c.Engine.DealDamage(source, target, amount, DamageKind, c.Label, c.Depth) Damage through armor and "before damaged" reactions.
c.Engine.Heal(target, amount, c.Label, c.Depth) Heal up to max health.
c.Engine.ChangeStat(target, StatType, delta, c.Label, c.Depth) Attack, armor, speed, max health.
c.Engine.AddStatus(target, statusId, stacks, c.Label, c.Depth) Add stacks; a negative number removes them.
c.Engine.Summon(owner, template, c.Label, c.Depth) A new unit at the back of the owner's side.
c.Event.Amount = ... / c.Event.Cancelled = true On "Before" triggers only (Before Strike, Before Struck, Before Damaged, Before Death): change or cancel what's about to happen.

For numbers designers tune, use an Amount field instead of int. It lets them pick a flat number, per stack, per tier, a % of a stat or a % of the event, and amount.Resolve(c) gives the value.

Plain-English text

Describe() is the phrase inspectors, tooltips and generated item text use. The kit builds sentences as "Trigger (conditions): effect, then effect."

  • Effects: an imperative verb phrase, lowercase, no period: "deal 3 damage to the enemy", "gain 2 armor".
  • Conditions: a clause: "if you have 3+ poison", "once per battle", "25% chance".
  • Use TextTemplates.TargetText(t), TextTemplates.SubjectText(t), TextTemplates.StatusName(id) and TextTemplates.AmountText(amount) so wording matches the built-in blocks and follows any renaming or translation you do in TextTemplates.

[TypeMenu("Group/Name")] sets where a block appears in the picker. The first part is its group. A Ruleset's Hidden Block Groups hide whole groups for simpler games, so pick a group name designers can hide (the sample uses "Custom"). [HideInTypeMenu] hides a block completely, for example an old block kept only so saved items still load.

Catching mistakes

Three ways, all shown in Hub › Content Check for problems and the database inspector:

  • [RequiresTrigger(Trigger.BeforeDamaged)]: the block only makes sense on those triggers. Anywhere else is an error.
  • [StatusId] on a string field: a status dropdown in the inspector, and an error if the id isn't in the database (or is turned off).
  • IValidatedBlock: your own rules.
public class Detonate : Effect, IValidatedBlock
{
    [StatusId] public string statusId = "poison";
    public int damagePerStack = 2;
    ...
    public void Validate(BlockCheck check)
    {
        if (damagePerStack <= 0) check.Error("deals no damage (Damage Per Stack is " + damagePerStack + ").");
        if (check.Trigger == Trigger.BattleStart) check.Warning("runs at battle start, before anyone has stacks, so it does nothing.");
    }
}

check.Ability is the ability the block is in, and check.StatusExists(id) checks the database's statuses. For content built in code, ContentValidator.ValidateAbilities(abilities, "Item 'bomb'", statusIds) runs every check without a database.

Keeping state

One block instance is shared by every battle that uses the item, including thousands of parallel simulation battles. Never store battle state in a field. Keep it in c.Engine.Counters, keyed by c.Key:

[Serializable, TypeMenu("Custom/Once Per Round")]
public class OncePerRound : Condition
{
    public override bool Check(EffectContext c)
    {
        int last;
        return !c.Engine.Counters.TryGetValue(c.Key + "#round", out last) || last != c.Engine.Round;
    }
    public override void OnPass(EffectContext c) { c.Engine.Counters[c.Key + "#round"] = c.Engine.Round; }
    public override string Describe() { return "once per round"; }
}

Staying deterministic

The same content, rules and seed must always give the same battle; saves, replays and the Balance Lab rely on it.

  • Random numbers only from c.Engine.Rng (Range(n), Next()), never UnityEngine.Random or System.Random.
  • No static fields, time, frame counts or dictionaries iterated in hash order.
  • Loops over fighters use c.Targets, which returns them in a fixed order.

Testing a block

Build two fighters in code, give one the block, run a short battle, check the numbers. Scripts/Tests/Editor/CustomBlockTests.cs does this for every sample block:

[Test] public void ReflectDamage_SplitsAStrike()
{
    var hero = new CombatantTemplate { displayName = "Hero", health = 100 };
    hero.passives.Add(new Ability("", Trigger.BeforeDamaged, new ReflectDamage { percent = 50 }));
    var brute = new CombatantTemplate { displayName = "Brute", health = 100, attack = 10, speed = 9 };
    var r = new BattleEngine(hero, brute, new Dictionary<string, StatusDef>(), new BattleRules { maxRounds = 1 }, 1).Run();
    Assert.AreEqual(95, r.PlayerTeam.Units[0].Get(StatType.Health));
    Assert.AreEqual(95, r.EnemyTeam.Units[0].Get(StatType.Health));
}

Then try it in Hub > Balance Lab against your real content, and regenerate the block list with Tools > AutoBattleKit > Docs > Generate Block Reference. Your blocks are included automatically.