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:
actor PC {
++ health resource HP
}what you actually type into your .isdl file is:
actor PC {
health resource HP
}The bare minimum ā
The bare minimal required ISDL file is just the config:
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:
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.
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.
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:
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.
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:
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.
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
attribute Brain(min: 1, max: 10, roll: roll(d20 + self.Brain))Let's break down what we just did:
- Variables are how we store values temporarily.
fleetingvariables can have their values change, buteternalones won't accept new values:
fleeting myCoolValue = 0
myCoolValue = 2 // Now 2
eternal myOtherCoolValue = 0
myOtherCoolValue = 2 // Will error and still be 0We can reference values on the same Document via
self.Thing. This access is smart - forattributeit will grab themodvalue, while for other fields it will grab the current value.rollis how we build a Roll. You can do arbritary rolls:
fleeting rollResult = roll(d20 + 6)
log(rollResult.total) // Logs the resultIn this case, we build a roll referencing the attribute, which will substitute it in.
- Just rolling isn't super helpful - we also want to show it to the user. A
chatcard 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.
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.
item Skill {
parent<attribute> UsesAttribute
number Bonus
}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.
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.
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.
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.
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:
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:
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.
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
}
}
}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.
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:
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:
++ 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.Costfrom inside the spell Item ā see the Item Action Scope section on Basic Logic forself/parent/target/User. For a broader option that consumes Stamina or other resources, set up aparent<resource> Consumesfield 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
functionyou 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.