Skip to content

Basic Logic

Cody Swendrowski edited this page Jun 6, 2026 · 10 revisions

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 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).
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 : valueIfFalse

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

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

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 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!

Clone this wiki locally