Basic Logic ā
Logic in ISDL lets you add calculations, conditions, and simple automation to your RPG systems. This page covers the fundamentals you'll need for most systems.
Variables ā
Variables store temporary values during calculations and actions.
Fleeting Variables ā
Fleeting variables can change their values - perfect for calculations and temporary storage:
action CalculateBonus {
fleeting baseAmount = 5
fleeting levelBonus = self.Level * 2
fleeting totalBonus = baseAmount + levelBonus
self.AttackBonus = totalBonus
}Eternal Variables ā
Eternal variables never change - great for constants and configuration:
action CheckDifficulty {
eternal easyDC = 10
eternal hardDC = 20
fleeting roll = roll(d20) + self.Skill
if (roll >= hardDC) {
// Critical success!
}
else if (roll >= easyDC) {
// Regular success
}
}Arrays ā
Store lists of values for lookups and iteration:
action GetLevelBonus {
eternal bonusTable = [ 0, 1, 2, 3, 5, 8, 13 ]
fleeting myBonus = bonusTable[self.Level] // Zero-indexed
self.CurrentBonus = myBonus
}Mathematical Operations ā
Basic Math ā
+Addition:5 + 3= 8-Subtraction:10 - 4= 6*Multiplication:6 * 2= 12/Division:15 / 3= 5
Assignment Shortcuts ā
| Shortcut | Meaning | Example |
|---|---|---|
+= | Add to variable | self.HP += 5 |
-= | Subtract from variable | self.HP -= damage |
*= | Multiply variable | self.Bonus *= 2 |
/= | Divide variable | self.Bonus /= 2 |
Tip: self.Level += 1 is the readable way to add one. ISDL also accepts self.Level++ as a shorthand if you prefer; both produce the same result.
Common Math Functions ā
Math.max(a, b)- Choose the larger valueMath.min(a, b)- Choose the smaller valueMath.floor(value)- Round down to a whole number. Use this for "lose half" / "half damage" patterns;Math.roundis rarely what you want for TTRPG math.Math.ceil(value)- Round up to a whole number.Math.round(value)- Round to the nearest whole number (5 rounds up).Math.abs(value)- Absolute value (drops the negative sign).
action CalculateDamage {
fleeting baseDamage = roll(2d6)
fleeting strengthBonus = self.STR
fleeting totalDamage = baseDamage + strengthBonus
// Ensure minimum 1 damage
totalDamage = Math.max(totalDamage, 1)
self.Target.HP -= totalDamage
}Comparisons and Conditions ā
Comparison Operators ā
ISDL provides word-style operators (preferred ā they read like English) and symbolic alternatives (familiar to programmers). Both work; use whichever you prefer, but the wiki examples use the word forms throughout.
| Comparison | Preferred (word form) | Alternative (symbol) |
|---|---|---|
| Equal to | equals | == |
| Not equal to | !equals | != |
| Less than | < | ā |
| Greater than | > | ā |
| Less than or equal | <= | ā |
| Greater than or equal | >= | ā |
| Contains | has | ā |
| Does not contain | excludes | ā |
| Starts with | startsWith | ā |
| Ends with | endsWith | ā |
| Is empty | isEmpty | ā |
| Has content | isNotEmpty | ā |
| Exists | exists | ā |
| Does not exist | !exists | ā |
Simple Conditions ā
action LevelUpCheck {
if (self.Experience >= 1000) {
self.Level += 1
self.Experience -= 1000
self.MaxHP += roll(1d10)
chat LevelUp {
self.Name + " levels up!"
"Now level " + self.Level
}
}
}Multiple Conditions ā
action UsePotion {
if (self.HP < self.HP.max and self.Potions > 0) {
self.HP += roll(2d4 + 2) // Resource auto-clamps to max on next derive
self.Potions -= 1
chat UsePotion {
"Used a healing potion!"
tag self.HP
}
}
}Shorthand Conditions (Ternary Operator) ā
The ternary operator is a compact way to choose between two values based on a condition. It's most useful inside value: blocks to derive a field from another:
readonly number Rating(value: {
return self.Score >= 10 ? 1 : 0
})Read it as: "if Score is 10 or higher, return 1; otherwise return 0."
The syntax is:
condition ? valueIfTrue : valueIfFalseIt works anywhere an expression is valid ā value: blocks, actions, and function bodies:
action Evaluate {
fleeting tier = self.Level >= 10 ? "Expert" : "Novice"
chat Result {
"Status: " + tier
}
}For more complex branching with multiple cases, use a full if / else if / else block instead.
Dice Rolling ā
Roll dice and use the results in your calculations:
action Attack {
fleeting attackRoll = roll(d20 + self.AttackBonus)
fleeting damage = roll(1d8 + self.STR)
if (attackRoll >= self.Target.AC) {
self.Target.HP -= damage
chat Attack {
"Hit for " + damage + " damage!"
tag attackRoll
tag damage
}
} else {
chat Attack {
"Attack missed!"
tag attackRoll
}
}
}Roll Properties ā
When you roll dice, the result is a roll object. In a numeric context (comparisons, math, assignments, string concatenation), ISDL automatically uses the roll's total ā you can write the variable bare:
action DetailedAttack {
fleeting attackRoll = roll(d20 + 5)
if (attackRoll >= self.Target.AC) { // Bare roll: ISDL substitutes the total here
self.Target.HP -= roll(1d8) // Same here
chat Attack {
"Attack roll: " + attackRoll // And here
"Hit for damage!"
}
}
}Note
roll variables auto-resolve to .total in numeric contexts. When you write if (attackRoll >= 15), ISDL knows you want the number and uses attackRoll.total for you. The same applies to math (damage * 2), assignments (self.HP -= damage), and string concatenation ("hit for " + damage). You only need to write .total explicitly if you want to be unambiguous (for example, when reading other people's code) ā both forms produce identical generated output.
The exception is chat blocks: when you write a roll variable on its own line inside chat { ... } (no math, no concatenation), ISDL renders the full roll object ā formula, total, and an expandable breakdown ā not just the number. That's by design.
Common Dice Patterns ā
roll(d20)- Twenty-sided dieroll(2d6)- Two six-sided diceroll(d8 + 3)- Eight-sided die plus 3roll(d20 + self.Skill)- Use character attributes
The expression inside roll(...) uses Foundry's dice expression syntax, so anything Foundry supports is valid here: keep-highest (4d6kh3), exploding dice (d6x6), reroll (d10rr1), and so on. ISDL passes the expression through to Foundry's dice parser, with one addition ā you can reference ISDL fields (self.Strength, parent.HP, etc.) inline and they'll be substituted before the roll is evaluated.
For the full list of supported dice modifiers, see Foundry's documentation: Foundry Dice Modifiers and Foundry Dice Advanced Usage.
Simple Actions ā
Actions create buttons on character sheets that run your logic:
action Rest {
self.HP = self.MaxHP
self.MP = self.MaxMP
chat Rest {
self.Name + " takes a rest and recovers!"
}
}
action UseSkill {
fleeting skillRoll = roll(d20 + self.SkillBonus)
eternal difficulty = 15
if (skillRoll >= difficulty) {
chat Success {
"Skill check succeeded!"
tag skillRoll
}
} else {
chat Failure {
"Skill check failed."
tag skillRoll
}
}
}Chat Cards ā
Chat cards display information and results in the game chat. They render in the order you write them, top to bottom ā the only structural rule is that each line must be one of the forms below.
chat AttackResult {
flavor "Rolling for attack!"
"Hit for " + damage + " damage!"
attackRoll
damage
tag self.WeaponType
tag self.Target.Name
}What can go inside a chat block ā
A chat block contains zero or more lines. Each line is one of:
| Line form | What it produces | Example |
|---|---|---|
| Plain expression | A line of body text. Strings are shown literally; expressions are evaluated and converted to text. | "Hit for " + damage + "!" |
| A roll variable | Renders the roll inline with an expandable breakdown. | attackRoll |
flavor <expression> | Renders as the chat card's flavor text at the top. Typically used once at the top of the block. | flavor "You strike!" |
tag <expression> | Renders as a small chip at the bottom of the card. Useful for damage type, weapon name, target name, etc. | tag self.DamageType |
wide <expression> | Renders the line full-width. Useful when an expression's result needs more horizontal room. | wide self.LongDescription |
You can write the lines in any order, but the conventional layout is: flavor at the top ā narrative strings ā roll variables ā tag lines at the bottom. ISDL doesn't enforce this; the chat card simply renders lines in source order.
Tip
flavor, tag, and wide accept any expression ā not just string literals. flavor "You hit " + target.Name + "!" is valid.
Accessing Character Data ā
Inside any action, three special words tell ISDL which document you mean. Pick the right one for what you're trying to do.
self ā the document the action lives on ā
self is the document the action is attached to. If the action is on an Actor, self is that Actor. If the action is on an Item, self is that Item.
actor PC {
health resource HP
action ShortRest {
self.HP = self.HP.max // Heal this character to full
}
}parent ā the owning Actor (only inside Items) ā
When an action is defined on an Item that is owned by an Actor, parent refers to the owning Actor. Use it when an Item action needs to read or change the wearer's stats.
Because an Item can be owned by any Actor type, you must tell ISDL which Actor you're working with before touching its properties. Wrap parent access in an if (parent is SomeActor) type check ā exactly like target. This both resolves the property names (parent.Mana is checked against SomeActor's fields) and safely skips the body when the Item is unowned or owned by a different type:
item Spell {
number Cost
action Cast {
if (parent is Hero) { // Required: narrows parent to a Hero
parent.Mana -= self.Cost // Spend caster's Mana
parent.XP += 1 // Caster gets a tick of XP
chat cast {
flavor "Cast " + self.Name + "!"
}
}
}
}Important
parent.Property only works inside an if (parent is SomeActor) block. Without the type check, ISDL can't know which Actor's fields you mean and will report "Could not resolve reference to Property". If the Item isn't owned by an Actor (for example, a Spell sitting in a Compendium with no owner), the if (parent is ā¦) check is simply false and the body is skipped.
target ā the Foundry-targeted Token, if any ā
target is the Token the user has currently targeted in Foundry (the orange-reticle target, set by clicking with T held or via the targeting tool). It can be empty.
Always guard target access with a type check so the code only runs when something useful is targeted:
action Strike {
fleeting damage = roll(1d8 + self.STR)
if (target is Monster) { // Skips the body if no target, or if the target is a different document type
target.HP -= damage
chat hit {
flavor "Hit " + target.Name + "!"
damage
}
}
}Warning
Modifying target bypasses Document permissions. If the current user doesn't have permission to edit the targeted document, ISDL will automatically route the update to a connected GM to apply on your behalf.
User ā the person clicking the button ā
User refers to the Foundry user who triggered the action. It exposes two properties:
| Expression | Returns |
|---|---|
User.isGM | true if the current user is a Gamemaster, false otherwise. |
User.name | The current user's display name as a string. |
Use User.isGM to gate logic that should only happen when a GM clicks (revealing a secret, applying damage automatically, advancing initiative):
action RevealSecret {
if (User.isGM) {
chat secret {
flavor "GM reveals: the door is trapped."
}
}
else {
chat blocked {
flavor "Only the GM can reveal this."
}
}
}Self Properties ā
Access the current character's data with self.:
action ShowInfo {
chat CharacterInfo {
"Character: " + self.Name
"Level: " + self.Level
"HP: " + self.HP + "/" + self.MaxHP
tag self.Class
}
}Common Self Properties ā
self.Name- Character's nameself.Level- Character's levelself.HP- Current hit pointsself.MaxHP- Maximum hit points- Any field you've defined on your actor
Practical Examples ā
Health Potion ā
action DrinkPotion {
if (self.HP < self.HP.max and self.Potions > 0) {
fleeting healing = roll(2d4 + 2)
self.HP += healing // Resource auto-clamps to max on next derive
self.Potions -= 1
chat Healing {
"Healed for " + healing + " HP!"
tag self.HP
}
}
}Skill Check with Degrees of Success ā
action PerformSkill {
fleeting roll = roll(d20 + self.SkillMod)
eternal easy = 10
eternal medium = 15
eternal hard = 20
if (roll >= hard) {
chat SkillResult {
"Exceptional success!"
tag roll
}
self.Experience += 100
}
else if (roll >= medium) {
chat SkillResult {
"Good success!"
tag roll
}
self.Experience += 50
}
else if (roll >= easy) {
chat SkillResult {
"Basic success."
tag roll
}
self.Experience += 25
}
else {
chat SkillResult {
"Failed attempt."
tag roll
}
}
}Level Up System ā
action CheckLevelUp {
eternal baseXP = 1000
fleeting requiredXP = baseXP * self.Level
if (self.Experience >= requiredXP and self.Level < 20) {
self.Experience -= requiredXP
self.Level += 1
fleeting hpGain = roll(1d8 + self.CON)
self.MaxHP += hpGain
self.HP = self.MaxHP
chat LevelUp {
"LEVEL UP!"
"Now level " + self.Level
"Gained " + hpGain + " max HP"
tag self.MaxHP
}
}
}Next Steps ā
Once you're comfortable with basic logic, you'll often want one of these next:
- Repeated logic across actions? See
functionin Advanced Logic. You can definefunction MyHelper(x) returns number { ... }once and call it asself.MyHelper(5)from any action ā perfect for "every miss, mark XP" or "every attack rolls 2d6 + chosen stat" patterns. - Need to ask the player or the GM something mid-action? See
promptin Interactivity. Prompts pop a small dialog, can be targeted at the user, the GM, or a specific target, and can return data your action then acts on. - Want a class/playbook to add fields to a character? Use the visibility system. See the class-injects-fields recipe for the pattern.
- Recipes has copy-paste solutions for typical RPG mechanics.
Ready for more? Move on to Advanced Logic to learn about functions and complex game mechanics!