Skip to content

Logic Reference ​

Complete syntax reference for ISDL logic constructs, functions, and expressions.

Variables ​

Variable Declaration ​

kotlin
fleeting <name> = <value>  // Mutable variable
eternal <name> = <value>   // Immutable constant

Variable Types ​

Variables can hold any value type:

kotlin
fleeting number = 42
fleeting text = "Hello"
fleeting boolean = true
fleeting array = [1, 2, 3, 4]
fleeting roll = roll(2d6)

Array Access ​

kotlin
fleeting value = array[index]      // Get element at index
fleeting dynamic = array[variable] // Use variable as index

Mathematical Operations ​

Arithmetic Operators ​

OperatorDescriptionExample
+Addition5 + 3
-Subtraction10 - 4
*Multiplication6 * 2
/Division15 / 3

Assignment Operators ​

OperatorDescriptionExampleEquivalent
+=Add and assignx += 5x = x + 5
-=Subtract and assignx -= 3x = x - 3
*=Multiply and assignx *= 2x = x * 2
/=Divide and assignx /= 4x = x / 4
++Incrementx++x = x + 1
--Decrementx--x = x - 1

The wiki examples prefer x += 1 and x -= 1 over x++ and x-- because they read more clearly for newer devs. Both forms produce identical generated output.

Mathematical Functions ​

FunctionDescriptionExample
Math.abs(value)Absolute valueMath.abs(-5) β†’ 5
Math.ceil(value)Round upMath.ceil(4.3) β†’ 5
Math.floor(value)Round downMath.floor(4.7) β†’ 4
Math.round(value)Round to nearestMath.round(4.6) β†’ 5
Math.max(a, b, ...)Maximum valueMath.max(5, 10, 3) β†’ 10
Math.min(a, b, ...)Minimum valueMath.min(5, 10, 3) β†’ 3
Math.random()Random 0-1Math.random() β†’ 0.747...

Comparison Operators ​

ISDL provides word-style operators (preferred β€” they read like English) and symbolic alternatives (familiar to programmers). Both work; the wiki examples use the word forms.

OperatorDescriptionExample
equalsEqual to (preferred)x equals "text"
!equalsNot equal to (preferred)x !equals "text"
==Equal to (alternative)x == 5
!=Not equal to (alternative)x != 5
<Less thanx < 5
>Greater thanx > 10
<=Less than or equalx <= 5
>=Greater than or equalx >= 10
hasContains valueself.Tags has "magic"
excludesDoes not contain valueself.Tags excludes "cursed"
startsWithStarts with textself.Name startsWith "Sir"
endsWithEnds with textself.Title endsWith "III"

Logical Operators ​

OperatorDescriptionExample
andLogical ANDa > 5 and b < 10
orLogical ORa == 1 or b == 2
!Logical NOT!isAlive

Existence & Content Checks ​

OperatorDescriptionExample
existsValue existsself.OptionalField exists
!existsValue doesn't existself.OptionalField !exists
isEmptyString or array is emptyself.Notes isEmpty
isNotEmptyString or array has contentself.Inventory isNotEmpty

Conditional Statements ​

If Statement Syntax ​

kotlin
if (condition) {
    // statements
}
else if (condition) {
    // statements  
}
else {
    // statements
}

Ternary Operator ​

A compact inline conditional β€” returns one of two values based on a condition. Works anywhere an expression is valid: value: blocks, actions, function bodies.

kotlin
fleeting result = condition ? valueIfTrue : valueIfFalse

// Inside a value: block
readonly number Tier(value: { return self.Level >= 10 ? 2 : 1 })

// Inside an action
fleeting label = self.HP > 0 ? "Alive" : "Defeated"

The full condition is evaluated first, so a + b > c ? x : y parses as (a + b > c) ? x : y.

Functions ​

Function Definition ​

kotlin
function <name>(<parameters>) returns <returnType> {
    // function body
    return <value>
}

Parameter Syntax ​

kotlin
function name(type paramName) returns returnType { }           // Required parameter
function name(type paramName = defaultValue) returns type { }  // Default parameter

Return Types ​

  • number - Numeric value
  • boolean - True/false value
  • string - Text value
  • nothing - No return value (void)

Function Call ​

kotlin
self.functionName(parameters)

Property Access ​

Self Properties ​

kotlin
self.PropertyName              // Direct property access
self[self.dynamicProperty]     // Dynamic property lookup
self.property.subProperty      // Nested property access

Parent Properties ​

Requires a type guard. parent.* only resolves inside an if (parent is SomeActor) { … } block, since an Item can be owned by any Actor type. Without it you'll get "Could not resolve reference to Property".

kotlin
if (parent is Hero) {
    parent.PropertyName                    // Parent document property
    parent[self.dynamicProperty]           // Dynamic parent lookup
    parent.property.subProperty            // Nested parent access
}

Target Properties ​

kotlin
target.PropertyName            // Target document property
target.property.subProperty    // Nested target access

Special Self Properties ​

kotlin
self.Name                      // Document name
self.Description               // Document description
self.Image                     // Document image
self.DocumentType             // Document type (actor/item)
self.EditMode                 // Current edit mode
self.Effects                  // Active effects

System Properties ​

User Properties ​

kotlin
User.isGM                     // Boolean: Is user a GM?
User.name                     // String: User's name

Combat Properties ​

kotlin
Combat.isMyTurn               // Boolean: Is it this character's turn?
Combat.isNotMyTurn           // Boolean: Is it NOT this character's turn?

Combat Methods ​

kotlin
Combat.nextTurn()             // Advance to next turn
Combat.end()                  // End combat

Dice Rolling ​

Roll Syntax ​

kotlin
roll(diceExpression)
roll(diceExpression, param: value, ...)   // with detection params (below)

Dice Expression Examples ​

kotlin
roll(d20)                     // Single d20
roll(2d6)                     // Two d6 dice
roll(3d8 + 5)                 // Three d8 plus 5
roll(d20 + self.Modifier)     // d20 plus character modifier
roll(self.DiceCount + "d6")   // Dynamic dice count

Detection Parameters ​

Optional parameters configure crit/fumble flagging and success counting at roll time. They are opt-in β€” if you don't add the parameter, the matching accessor isn't available.

kotlin
roll(d20 + self.STR, crit: 20, fumble: 1)   // natural 20 crits, natural 1 fumbles
roll(d20 + self.STR, crit: >= 19)           // crit range (19–20)
roll(5d6, success: >= 5)                     // count dice β‰₯ 5 as successes
roll(5d6, success: >= 5, failure: 1)         // 1s subtract from the success count
ParameterMeaning
crit: / fumble:Compare the natural face of the first die. Bare value (crit: 20) means equality; an operator form (crit: >= 19) uses that operator.
success: / failure:Per-die comparison for success counting. failure: subtracts from the success total.

For total-based crits (compare the modified total, not the die face), branch manually: if (myRoll.total >= 25) { ... }.

Roll Properties ​

kotlin
fleeting attack = roll(d20 + self.STR, crit: 20, fumble: 1)
attack.total                  // Total result (also the value of a bare `attack`)
attack.crit                   // true if the crit: condition triggered (read/write β€” see below)
attack.fumble                 // true if the fumble: condition triggered (read/write β€” see below)

fleeting pool = roll(5d6, success: >= 5, failure: 1)
pool.successes                // number of successes (requires success:)
pool.highest                  // highest face rolled
pool.lowest                   // lowest face rolled
pool.dice                     // array of face values β€” iterable with `each`
pool.count(6)                 // how many dice show a 6
pool.count(face => face >= 5) // how many dice satisfy a predicate
pool.contains(1)              // true if any die shows a 1
pool.contains(face => face >= 5)

count/contains accept either a face value or a single-parameter predicate (face => ...). The predicate variable can be any name except die/dice, which are reserved field-type keywords β€” use face, d, etc.

pool.dice, count, contains, highest, and lowest see every standing die, including dice dropped by keep/drop modifiers β€” so roll((n)d6 kh1) plus .contains(1) still detects a 1 on a discarded die (the EZD6 "spell fizzles" pattern).

Marking Crit/Fumble Manually ​

When a rule is too complex for a crit:/fumble: threshold, set the flags yourself β€” crit/fumble are read/write. A manual value wins over the parameter, and you don't need a crit:/fumble: parameter to set them.

kotlin
fleeting wild = roll(2d6)
if (wild.contains(6)) { wild.crit = true }   // crit on any 6
if (wild.contains(1)) { wild.fumble = true } // fumble on any 1
// "nothing" / reset:  wild.crit = false

The chat card auto-highlights from the flags either way. If a roll ends up both crit and fumble (only really possible via manual marking), it gets a distinct rare "crit-fumble" treatment (a green↔red shimmer). .successes is the exception β€” it's computed from success: and can't be set manually.

The chat card automatically highlights a crit (green) or fumble (red) when a crit:/fumble: parameter is present and triggers.

kotlin
// EZD6-style fizzle:
fleeting cast = roll((self.Dice)d6 kh1)
if (cast.contains(1)) {
    chat Spell { "The spell fizzles!" }
}

// Walk every die:
each face in cast.dice {
    log("Rolled a " + face)
}

Damage Rolls ​

damage(roll: <expression>, type: <damage type>) produces a typed damage roll. The result is a roll-like object that carries both the rolled total and the damage type metadata (color, icon, custom flags from choice<damageType>). Use this when you want a single value that knows what kind of damage it represents β€” useful for chat-card display, resistance lookups, and Active Effect bonuses keyed to damage types.

kotlin
fleeting hit = damage(roll: 2d8 + self.STR, type: self.SpellDamage)

if (target.hasResistance(hit.type)) {
    hit = hit / 2
}

chat AttackResult {
    "Deals " + hit + " " + hit.type + " damage!"
    tag hit.icon       // metadata from the choice<damageType> entry
}

Parameters:

  • roll: - The dice expression for the damage amount (same syntax as roll(...)).
  • type: - The damage type. Typically a choice<damageType> field reference (e.g. self.SpellDamage), but a literal string ("Fire") is also accepted.

Properties on the returned object include the standard roll properties (.total, .dice) plus all metadata attached to the chosen damage-type choice (.color, .icon, plus any custom keys you defined on the choice<damageType>).

Chat Cards ​

Chat Card Syntax ​

kotlin
chat <name> {
    <line>
    <line>
    ...
}

// Or with a custom template:
chat <name>(template: "path/to/template.hbs") {
    <line>
    ...
}

A chat block is zero or more lines. Lines render in source order; ISDL does not enforce ordering between flavor, tag, body, and roll lines (the conventional layout is flavor at the top, body and roll variables in the middle, tag at the bottom).

Custom Card Templates ​

The optional template: parameter overrides the default chat-card HTML with a Handlebars template path of your choosing. Use this when you need card layouts that the line-based grammar can't express (multi-column tables, custom buttons, etc.). The path is resolved relative to your system's root.

kotlin
chat AttackResult(template: "templates/chat/attack-card.hbs") {
    flavor "You strike!"
    attackRoll
    damage
}

When you provide a custom template, the chat-block lines are still evaluated and made available to the template under the same names you'd use in any chat block β€” but how they render is up to your Handlebars file.

Valid Line Forms ​

Line formRenders asExample
Plain expressionA line of body text. Strings render literally; expressions evaluate and convert to text."Hit for " + damage + "!"
Roll variableThe full roll inline with an expandable formula breakdown. (Only place where the roll object renders, not its total.)attackRoll
flavor <expression>Flavor text at the top of the card.flavor "You strike!"
tag <expression>A small chip at the bottom of the card.tag self.DamageType
wide <expression>Renders the line full-width.wide self.LongDescription

flavor, tag, and wide accept any expression β€” not just string literals. flavor "You hit " + target.Name + "!" is valid.

Chat Card Content Types ​

kotlin
chat Example {
    "Literal text"             // Plain text
    variableName               // Variable value
    self.PropertyName          // Property value
    tag self.Property          // Tagged property (small chip)
    flavor "Description"       // Flavor text at the top
    wide self.LongDescription  // Full-width line
}

Advanced Chat Card ​

kotlin
chat AttackResult {
    flavor success ? "Hit!" : "Miss!"   // Conditional flavor text
    "Damage: " + damage                 // String concatenation
    attackRoll                          // Roll object: renders with breakdown
    damage                              // Roll object: renders with breakdown
    tag self.WeaponType                 // Tagged weapon
    tag target.Name                     // Tagged target
}

Loops and Iteration ​

Each Loop Syntax ​

kotlin
each <variable> in <collection> {
    // loop body
}

Collection Types ​

kotlin
each item in self.Equipment { }        // Property collection
each skill in self.Skills { }         // Document array
each bonus in bonusArray { }          // Variable array
each level in [1 to self.Level] { }   // Number range

Number Range Syntax ​

kotlin
[startNumber to endNumber]            // Inclusive range
[1 to 10]                            // Numbers 1 through 10
[self.MinLevel to self.MaxLevel]     // Dynamic range

Actions ​

Action Syntax ​

kotlin
[modifiers] action <name>(<parameters>) {
    // action body
}

Action Modifiers ​

Prefix an action with a modifier to control where it appears and who can see it:

kotlin
macro action Name { }      // Eligible for the Foundry macro hotbar
gmOnly action Name { }     // Only shown to GMs; hidden from players in item table buttons
secret action Name { }     // Shown to GMs and the document's owner; hidden from others
hidden action Name { }     // Never shown in the UI (useful for macro-only actions)

Note

gmOnly, secret, and hidden apply to action buttons rendered in item table columns. They work alongside the visibility: parameter β€” visibility: controls when a button is enabled or locked on actor sheets; prefix modifiers control who can see the button at all.

Action Parameters ​

ParameterDescriptionExample
visibility:Control visibilityvisibility: Visibility.gmOnly
icon:Action iconicon: "fa-solid fa-sword"
color:Action colorcolor: "#FF0000"
label:Action labellabel: "Custom Name"

Visibility Values ​

ValueDescription
Visibility.unlockedFully visible and editable
Visibility.defaultStandard visibility
Visibility.secretHidden from players, visible to GMs
Visibility.editOnly visible in edit mode
Visibility.playOnly visible in play mode
Visibility.gmEditGMs can edit, players view
Visibility.gmOnlyOnly visible to GMs
Visibility.readonlyVisible but not interactive
Visibility.lockedLocked from interaction
Visibility.hiddenCompletely hidden

Event Handlers ​

Hook Handler Syntax ​

kotlin
on <eventName>(<parameters>) {
    // event handling code
}

Combat Events ​

EventDescriptionParameters
combatStartCombat beginsNone
combatEndCombat endsNone
turnStartCharacter's turn startsNone
turnEndCharacter's turn endsNone
turnIsNextCharacter's turn is nextNone
roundStartCombat round startsNone
roundEndCombat round endsNone

Health Events ​

ISDL fires these hooks on its own when the damage applicator runs. The pre-apply hooks let you mutate the incoming amount before it lands.

EventDescriptionParameters
preApplyDamageBefore damage is applied to this documentnumber amount, string damageType, object damageMetadata
appliedDamageAfter damage is appliednumber amount, string damageType, object damageMetadata
preApplyHealingBefore healing is appliednumber amount, string damageType, object damageMetadata
appliedHealingAfter healing is appliednumber amount, string damageType, object damageMetadata
preApplyTempBefore temp HP is appliednumber amount, string damageType, object damageMetadata
appliedTempAfter temp HP is appliednumber amount, string damageType, object damageMetadata
deathThe document's health resource hit 0None

Listening to Foundry Hooks ​

Any identifier you put after on becomes a Hooks.on(<name>, ...) listener for a Foundry hook of that name. This means you can listen to any hook Foundry itself fires (e.g. createActor, updateItem, chatMessage, renderChatLog) or hooks fired by modules.

kotlin
on createActor(actor, options, userId) {
    // runs whenever a new actor is created
}

ISDL does not currently provide a way to fire custom hooks from your own code. The combat, health, and death events listed above are the only hooks ISDL emits on your behalf; everything else must be fired by Foundry, a module, or external code calling Hooks.callAll(...).

Interactive Prompts ​

Prompt Syntax ​

kotlin
fleeting result = prompt(<parameters>) {
    <field definitions>
}

Prompt Parameters ​

ParameterDescriptionExample
target:Who sees prompttarget: "user"
label:Window titlelabel: "Choose Action"
icon:Window iconicon: "fa-solid fa-question"
width:Window widthwidth: 400
height:Window heightheight: 300
location:Window positionlocation: 100, 200
limit:Time limitlimit: 30 seconds

Prompt Target Values ​

  • "user" - Current user
  • "gm" - Game master
  • "target" - Targeted player

Allowed Prompt Fields ​

Prompts only accept one-shot input fields: string, number, boolean, choice<string>, choices<string>, choice<damageType>, choice<Document>, choices<Document>, parent<...>/self<...> references, die, dice, date, time, datetime. Persistent or display widgets (attribute, resource, tracker, money, html, paperdoll), embedded collections (tables/inventories), and layout blocks are not allowed and produce a validation error. See Interactivity.

Time Units ​

  • ms - Milliseconds
  • seconds - Seconds
  • minutes - Minutes

Timing and Audio ​

Wait Syntax ​

kotlin
wait <duration> <unit>

Audio Playback ​

kotlin
play(file: "path/to/audio.wav", volume: 75)

Audio Parameters ​

ParameterDescriptionDefault
file:Audio file pathRequired
volume:Volume 0-100System default

Type Checking ​

Document Type Checks ​

kotlin
target is Actor               // Check if target is actor
target is Item               // Check if target is item
parent is Actor              // Check parent document type

Update Methods ​

Document Updates ​

kotlin
self.update()                // Commit pending changes to the document now
self.delete()                // Delete the current document

self.update() is the canonical form. It commits whatever assignments the action has queued so that subsequent code in the same action can read the updated values. The end of an action body implicitly flushes pending updates, so you only need self.update() when in-action read-after-write ordering matters.

Debug and Logging ​

Log Function ​

kotlin
log(message1, message2, ...)  // Output to console

Log Examples ​

kotlin
log("Debug message")
log("Value:", variable)
log("Multiple", "values", 123, true)

JavaScript Escape Hatch ​

JavaScript Block Syntax ​

kotlin
@js{ JavaScript code here }

JavaScript Examples ​

kotlin
action JSExample {
    @js{ const value = Math.sqrt(25); }
    fleeting result = self.BaseValue + @js{ value }

    @js{ console.log("Custom JavaScript executed"); }
}

Warning

@js{...} is an escape hatch, not a supported feature surface. The block executes inside whatever method ISDL is generating, with access to whatever variables the surrounding code has in scope (typically this, document, context, update, system, etc. β€” but the exact set depends on where in the action body you use it). There is no stable contract for what's available; if ISDL's codegen changes, your @js{...} blocks may break. Use it for genuine gaps in the language, and prefer ISDL constructs when they exist. For larger or system-wide native code (settings, extra sheets, UI hooks, module integration), use the stable, regeneration-safe Custom Code & Styles files instead.

Expression Precedence ​

Operations are evaluated in this order (highest to lowest precedence):

  1. Parentheses - (expression)
  2. Negation - !expression, -expression
  3. Multiplication/Division - *, /
  4. Addition/Subtraction - +, -
  5. Comparisons - <, >, <=, >=, ==, !=, equals, !equals
  6. Logical AND - and
  7. Logical OR - or

Precedence Examples ​

kotlin
fleeting result = 5 + 3 * 2        // = 11 (not 16)
fleeting result = (5 + 3) * 2      // = 16
fleeting result = x > 5 and y < 10  // Comparison before AND

Common Patterns ​

Null-Safe Operations ​

kotlin
if (self.OptionalProperty exists and self.OptionalProperty > 0) {
    // Safe to use property
}

Array Bounds Checking ​

kotlin
if (index >= 0 and index < array.length) {
    fleeting value = array[index]
}

Safe Division ​

kotlin
fleeting result = divisor != 0 ? dividend / divisor : 0

Complex Conditionals ​

kotlin
if ((self.Level >= 5 and self.Class equals "Mage") or 
    (self.Level >= 8 and self.HasSpecialTraining)) {
    // Complex condition logic
}

This reference covers all ISDL logic syntax. For practical examples, see Basic Logic, Advanced Logic, and Interactivity.