Skip to content

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:

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

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

kotlin
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 ​

ShortcutMeaningExample
+=Add to variableself.HP += 5
-=Subtract from variableself.HP -= damage
*=Multiply variableself.Bonus *= 2
/=Divide variableself.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 value
  • Math.min(a, b) - Choose the smaller value
  • Math.floor(value) - Round down to a whole number. Use this for "lose half" / "half damage" patterns; Math.round is 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).
kotlin
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.

ComparisonPreferred (word form)Alternative (symbol)
Equal toequals==
Not equal to!equals!=
Less than<—
Greater than>—
Less than or equal<=—
Greater than or equal>=—
Containshas—
Does not containexcludes—
Starts withstartsWith—
Ends withendsWith—
Is emptyisEmpty—
Has contentisNotEmpty—
Existsexists—
Does not exist!exists—

Simple Conditions ​

kotlin
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 ​

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

isdl
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 : valueIfFalse

It works anywhere an expression is valid — value: blocks, actions, and function bodies:

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

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

kotlin
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 die
  • roll(2d6) - Two six-sided dice
  • roll(d8 + 3) - Eight-sided die plus 3
  • roll(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:

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

kotlin
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 formWhat it producesExample
Plain expressionA line of body text. Strings are shown literally; expressions are evaluated and converted to text."Hit for " + damage + "!"
A roll variableRenders 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.

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

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

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

ExpressionReturns
User.isGMtrue if the current user is a Gamemaster, false otherwise.
User.nameThe 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):

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

kotlin
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 name
  • self.Level - Character's level
  • self.HP - Current hit points
  • self.MaxHP - Maximum hit points
  • Any field you've defined on your actor

Practical Examples ​

Health Potion ​

kotlin
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 ​

kotlin
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 ​

kotlin
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 function in Advanced Logic. You can define function MyHelper(x) returns number { ... } once and call it as self.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 prompt in 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!