Skip to content

Making your first System ​

Important

About the ++ and -- in the examples below: as this guide builds up your system, code blocks marked ```diff use ++ to show lines being added and -- to show lines being removed — this is just a visual highlight of what changed since the previous step. Do not type the ++ / -- into your .isdl file. They are not ISDL syntax and will cause errors. Only copy the actual code, not the leading ++/--.

For example, when you see this:

diff
actor PC {
++  health resource HP
}

what you actually type into your .isdl file is:

kotlin
actor PC {
    health resource HP
}

The bare minimum ​

The bare minimal required ISDL file is just the config:

kotlin
config MyFirstSystem {
    label = "My First System"
    id = "my-first-system"
    description = "Hey look I'm a system developer!"
    author = "Cody Swendrowski"
}

Save this to myfirstsystem.isdl, hit Control + Shift + P in Visual Studio to bring up the command bar, and search ISDL: Generate.

This basic system should now allow you to create a new world based off the System in Foundry. You can't make any Actors nor Items, but it will load!

Set the stage with Actors ​

We'll want at least one Actor to have character sheets. Let's make one:

kotlin
actor PC {
    
}

This will let Actors have a Name, Image, Description, and Active Effects.

Let's also give it the special health resource so it has HP to track. All resources automatically wire up as available Token resource bars in Foundry, and the health tag will mark this one as the resource that damage gets applied against.

diff
actor PC {
++  health resource HP
}

Hit Control + Shift + P to bring up the command bar again, and this time choose ISDL: Regenerate - this will use the last generation options.

Refresh Foundry and you should now have the option to make new Actors, which will have HP bars. Surely enough to play a game now!

I want to collect things too ​

Fundamentally, all tabletop games are about being loot goblins right? So let's let them keep track of loot. We'll use a number and the optional min param so that weight can't be negative.

diff
actor PC {
    health resource HP
}

++ item Loot {
++   number Value(min: 0)
++ }

Regenerate and you can now make the Loot items, but we still need to let PC's own them:

diff
actor PC {
    health resource HP
++  table<Loot> MyLoot
}

item Loot {
    number Value(min: 0)
}

Regenerate and your PC sheet will now have a "My Loot" tab with a datatable that can display and control embedded Loot Items. You can create new Loot Items on the sheet directly or drag one in - all Items are "embedded" which means the Actor owns them, and if that Actor is deleted, those Items are gone forever.

Roll a Strength check ​

We can now have loot, but to earn it we should put some dice rolls into our tabletop game. attribute is the element intended to hold character attributes. We'll use a simplified Brain / Brawn system.

Our ISDL file is going to slowly get bigger, so from this point on we're only going to show the area we're changing.

diff
actor PC {
    health resource HP

    attribute Brain(min: 1, max: 10)
    attribute Brawn(min: 1, max: 10)

    table<Loot> MyLoot
}

Many F20 based systems use a derived mod value from the attribute value. If your system follows a model like that, you can configure the mod calculation like in this example:

kotlin
attribute Strength(min: 1, max: 30, mod: {
    return (self.Strength - 10) / 2
}

Regenerate and you should see our new fields. Let's add a way to roll them! actions render as a button on the sheet that execute logic. We'll use a simple d20 + Attribute model.

diff
actor PC {
    health resource HP

    attribute Brain(min: 1, max: 10)
    attribute Brawn(min: 1, max: 10)

++  action RollBrain {
++      fleeting brainRoll = roll(d20 + self.Brain)
++      chat brainResult {
++          brainRoll
++      }
++  }

++  action RollBrawn {
++      fleeting brawnRoll = roll(d20 + self.Brawn)
++      chat brawnResult {
++          brawnRoll
++      }
++  }

    table<Loot> MyLoot
}

Tip

As of a recent update, you can also embed the roll directly into the Attribute

kotlin
    attribute Brain(min: 1, max: 10, roll: roll(d20 + self.Brain))

Let's break down what we just did:

  1. Variables are how we store values temporarily. fleeting variables can have their values change, but eternal ones won't accept new values:
kotlin
fleeting myCoolValue = 0
myCoolValue = 2 // Now 2

eternal myOtherCoolValue = 0
myOtherCoolValue = 2 // Will error and still be 0
  1. We can reference values on the same Document via self.Thing. This access is smart - for attribute it will grab the mod value, while for other fields it will grab the current value.

  2. roll is how we build a Roll. You can do arbritary rolls:

kotlin
fleeting rollResult = roll(d20 + 6)
log(rollResult.total) // Logs the result

In this case, we build a roll referencing the attribute, which will substitute it in.

  1. Just rolling isn't super helpful - we also want to show it to the user. A chat card message is very helpful for this. Just passing in the roll to the card is enough, ISDL will render the total with an expandable breakdown showing what the result of the d20 and Brain / Brawn was. We'll show you how to do fancier chat cards later on.

Regenerate and you should now be able to roll attribute checks!

Cleaning up our Attribute section ​

UI wise, ISDL will lay out elements left to right, top to bottom. It's often useful to visually group elements, which is where sections come in - a section will render in the layout as a single element, with elements inside of it rendering top to bottom. Let's put our attributes in a section.

diff
actor PC {
    health resource HP

++  section Attributes {
        attribute Brain(min: 1, max: 10)
        attribute Brawn(min: 1, max: 10)

        action RollBrain {
            fleeting brainRoll = roll(d20 + self.Brain)
            chat brainResult {
                brainRoll
            }
        }

        action RollBrawn {
            fleeting brawnRoll = roll(d20 + self.Brawn)
            chat brawnResult {
                brawnRoll
            }
        }
++ }

    table<Loot> MyLoot
}

Regenerate and Attributes will now nicely render in their own column.

And now, roll for Perception ​

Most F20 systems also allow you to have Skills one is trained in that get added to your Attribute score.

If your system only has a handful of Skills that never change, you could manually create fields and actions for each Skill, like we did with our Attributes. It's more flexible, however, to instead represent Skills as Items, allowing arbritary Skills to be created and configured. This will also be cleaner for us to setup.

kotlin
item Skill {
    parent<attribute> UsesAttribute
    number Bonus
}
diff
actor PC {
    // . . . Rest of file

    table<Loot> MyLoot
++  table<Skill> Skills
}

parent<attribute> is a "reference" field, allowing our Skill to reference a value from the Parent. We want to only choose from attributes, so we pass it attribute as a type. If you regenerate and create a new Skill, you can see we can choose from "PC - Brain" or "PC - Brawn" as values. The current value will be resolved when something executes referencing this field.

There are two ways we could setup the roll.

Skills roll themselves ​

This is the "Foundry" way, where each Skill can roll itself. Choosing what to roll means finding the Skill in your list and either opening the sheet and clicking the Action, or using the Datatable inline actions to roll it. The drawback of this method is that sometimes finding the right table then the right thing to roll can take a bit.

diff
item Skill {
    parent<attribute> UsesAttribute
    number Bonus

++  action Roll {
++      fleeting skillRoll = roll(d20 + self.UsesAttribute + self.Bonus) // Will resolve to a roll such as d20 + 5 + 2
++
++      chat skillResult {
++          skillRoll
++      }
++  }
}

The Actor has a Skill roller ​

We can instead add a new field on the Actor to choose a Skill then roll it. This provides a single spot on the Actor sheet to roll, but means reselecting the Skill everytime it changes. This is also the first time where we need to access a "subproperty" - we want to get two different field values from the currently selected Skill. This model is how we will represent other documents like this in this guide, but you're free to choose either.

diff
actor PC {

++  section Roller {
++      choice<Skill> Skill
++
++      action Roll {
++          fleeting skillRoll = roll(d20 + self.Skill.UsesAttribute + self.Skill.Bonus) // Will resolve to a roll such as d20 + 5 + 2
++
++          chat skillResult {
++              skillRoll
++          }
++      }
++  }
    
    // . . . Rest of file
}

You might also choose this path if your system would benefit from a single generic roller, although this increases the amount of UI changes you have to make per-roll. choice supports having nothing selected, so you can roll just Brain or Brawn with no Skill and self.Skill.Bonus will resolve to 0. This neatly replaces our previous double action.

diff
actor PC {

  section Roller {
      self<attribute> Attribute
      choice<Skill> Skill

      action Roll {
++        fleeting rollResult = roll(d20 + self.Attribute + self.Skill.Bonus) // Will resolve to a roll such as d20 + 5 + 2

          chat rollResult {
              rollResult
          }
      }
  }
    
  // . . . Rest of file
}

item Skill {
--  parent<attribute> UsesAttribute
    number Bonus
}

My kingdom for a Sword ​

We can now dodge traps, but hitting things is a great and valid form of therapy, so let's do that next.

Let's first make something to hit - a Monster! Monsters only ever fight, so we'll give them a Fight attribute, and an Armor Class to represent how hard it is to land damage.

kotlin
actor Monster {
    health resource HP
    number ArmorClass(min: 0, max: 20)
    attribute Fight(min: 1, max: 10)
}

Wait Monsters don't have Skills ​

If you regenerate now and reopen a Skill, you will see that the UsesAttribute field now also lists Monster - Fight as an option. This is because Items can in theory be owned by any Actor. If this is valid for your system, great! For our sample system, we know that we are never going to have a Skill be owned by a Monster, so let's restrict it to PC options only via the choices field:

diff
item Skill {
--  parent<attribute> UsesAttribute
++  parent<attribute> UsesAttribute(choices: [PC])
    number Bonus
}

If we wanted to be even more restrictive or specific, we could pick individual fields as valid choices:

diff
item Skill {
--  parent<attribute> UsesAttribute
++  parent<attribute> UsesAttribute(choices: [PC.Brain, PC.Brawn])
    number Bonus
}

Ok but I still need a Sword here ​

Our weapons are going to look a lot like a Skill, but with damage. The die field is useful for this, and while damage types are not fully supported yet, we can visually represent them in the chat card.

kotlin
item Weapon {
    parent<attribute> UsesAttribute
    number AttackBonus
    die DamageDie
    number DamageBonus
    choice<string> DamageType(choices: ["Physical", "Mental"])

    action Roll {
        fleeting attackRoll = roll(d20 + self.UsesAttribute + self.AttackBonus)
        fleeting damageRoll = roll(self.DamageDie + self.DamageBonus)

        chat weaponResult {
            attackRoll
            damageRoll
            tag self.DamageType
        }
    }
}
diff
actor PC {
    // . . . Rest of file

    table<Loot> MyLoot
    table<Skill> Skills
++  table<Weapon> Weapons
}

Like Skill rolls, we output the attack roll to the chat. We also roll and attach a second damage roll, which a User can apply via a right click "apply damage" UX on the chat card.

A new feature we've used here is a tag, which attaches additional metadata at the bottom of the chat card for easy reference.

Automation! Automation! ​

Combat automation is a deep deep well. Adding AttackBonus and DamageBonus as fields already allows Active Effects to add bonuses, such as a buff giving a temporary +2 to hit and +1 to damage. We can also pretty easily add some basic "did I hit" detection as well to help make combat faster. We'll use the "Meet or beat" system against AC.

diff
item Weapon {
    parent<attribute> UsesAttribute
    number AttackBonus
    die DamageDie
    number DamageBonus
    choice<string> DamageType(choices: ["Physical", "Mental"])

    action Roll {
        fleeting attackRoll = roll(d20 + self.UsesAttribute + self.AttackBonus)
        fleeting damageRoll = roll(self.DamageDie + self.DamageBonus) // Rolls are only shown onscreen when sent to chat, so it's safe to roll this ahead of time

++      // This type check is required as it provides safe access to fields on the Monster. If no Monster is targeted, this code won't run.
++      if (target is Monster) { 
++          if (attackRoll < target.ArmorClass) {
++              chat miss {
++                  flavor "Sorry, you missed!"
++                  attackRoll // It's good practice to show them the result even if they miss
++              }
++          }
++          else {
++              chat hit {
++                  flavor "You hit!"
++                  attackRoll
++                  damageRoll
++                  tag self.DamageType
++              }
++          }
++      }
    }
}

We used another new chat feature - flavor adds descriptive text at the top.

Can I apply damage automatically too? ​

We generally recommend keeping a human in the loop - display the damage roll, and let a human manually apply it via the "Apply Damage" UX. This allows a GM to step in and handle things like rerolls, bonuses that are hard to automate, or apply percent damage due to resistances.

If you still want to adjust values on the target creature, you can do so:

diff
    action Roll {
        fleeting attackRoll = roll(d20 + self.UsesAttribute + self.AttackBonus)
        fleeting damageRoll = roll(self.DamageDie + self.DamageBonus) // Rolls are only shown onscreen when sent to chat, so it's safe to roll this ahead of time

        if (target is Monster) { 
            if (attackRoll < target.AC) {
                chat miss {
                    flavor "Sorry, you missed!"
                    attackRoll // It's good practice to show them the result even if they miss
                }
            }
            else {
                chat hit {
                    flavor "You hit!"
                    attackRoll
                    damageRoll
                    tag self.DamageType
                }
++              target.HP -= damageRoll.total  
            }
        }
    }

Warning

Modifying the target bypasses Document permissions - if the current user can't edit the target document, ISDL will automatically route the update to an available GM to execute instead.

I only want to let people Attack on their turn ​

One of the powerful features of ISDL is its Visibility system. Thus far, we've used default visibility for everything - actions are only usable in "Play" mode, fields are unlocked for change in "Edit" mode, and Active Effects can target any field.

If you want to learn more about this topic, you can check out the Fields and Visibility sections.

For now, we'll use a calculated visibility to disable the button when it's not that Character's turn:

diff
++  action Roll(visibility: {
++      if (Combat.isMyTurn) {
++         return Visibility.default
++      }
++      else {
++          return Visibility.locked
++      }
++  })
    {
        // Rest of the action here
    }

What's next? ​

You now have a system that allows you to roll up a character, beat up monsters, and earn some loot. There's a lot of good next steps:

  • Add Classes such as Tank, Mage, and Fighter. We recommend making these as items, and then using a choice<Class> Class(global: true) to allow linking to any defined Class in the World. To make a Class actually change the Hero (e.g. "Barbarian adds a Rage tracker"), use the visibility system — see the class-injects-fields recipe.
  • Monsters probably deserve the chance to attack back, you can give them Attacks like we gave Weapons to PCs, or give them a simple damage die on the Actor.
  • Magic is usually a bit more complicated than Weapons, and usually consumes a resource such as Mana. If it's always Mana, you could use parent.Mana -= self.Cost from inside the spell Item — see the Item Action Scope section on Basic Logic for self / parent / target / User. For a broader option that consumes Stamina or other resources, set up a parent<resource> Consumes field and subtract from that.
  • Skills often can only be used so often per battle / day. trackers are a good field type for this.
  • If you find yourself copy-pasting the same logic into multiple actions, Advanced Logic — Functions lets you define a reusable function you can call from anywhere.
  • Need to ask the player or GM something mid-action (pick a target, choose between two consequences, enter a number)? See Interactivity — Prompts.

The Recipes page has several useful snippets for common TTRPG concepts. The Examples section contains a few different ISDL files we publish for reference of how we implemented a few different systems.