-
Notifications
You must be signed in to change notification settings - Fork 1
Logic Reference
Complete syntax reference for ISDL logic constructs, functions, and expressions.
fleeting <name> = <value> // Mutable variable
eternal <name> = <value> // Immutable constantVariables can hold any value type:
fleeting number = 42
fleeting text = "Hello"
fleeting boolean = true
fleeting array = [1, 2, 3, 4]
fleeting roll = roll(2d6)fleeting value = array[index] // Get element at index
fleeting dynamic = array[variable] // Use variable as index| Operator | Description | Example |
|---|---|---|
+ |
Addition | 5 + 3 |
- |
Subtraction | 10 - 4 |
* |
Multiplication | 6 * 2 |
/ |
Division | 15 / 3 |
| Operator | Description | Example | Equivalent |
|---|---|---|---|
+= |
Add and assign | x += 5 |
x = x + 5 |
-= |
Subtract and assign | x -= 3 |
x = x - 3 |
*= |
Multiply and assign | x *= 2 |
x = x * 2 |
/= |
Divide and assign | x /= 4 |
x = x / 4 |
++ |
Increment | x++ |
x = x + 1 |
-- |
Decrement | x-- |
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.
| Function | Description | Example |
|---|---|---|
Math.abs(value) |
Absolute value |
Math.abs(-5) β 5
|
Math.ceil(value) |
Round up |
Math.ceil(4.3) β 5
|
Math.floor(value) |
Round down |
Math.floor(4.7) β 4
|
Math.round(value) |
Round to nearest |
Math.round(4.6) β 5
|
Math.max(a, b, ...) |
Maximum value |
Math.max(5, 10, 3) β 10
|
Math.min(a, b, ...) |
Minimum value |
Math.min(5, 10, 3) β 3
|
Math.random() |
Random 0-1 |
Math.random() β 0.747...
|
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.
| Operator | Description | Example |
|---|---|---|
equals |
Equal to (preferred) | x equals "text" |
!equals |
Not equal to (preferred) | x !equals "text" |
== |
Equal to (alternative) | x == 5 |
!= |
Not equal to (alternative) | x != 5 |
< |
Less than | x < 5 |
> |
Greater than | x > 10 |
<= |
Less than or equal | x <= 5 |
>= |
Greater than or equal | x >= 10 |
has |
Contains value | self.Tags has "magic" |
excludes |
Does not contain value | self.Tags excludes "cursed" |
startsWith |
Starts with text | self.Name startsWith "Sir" |
endsWith |
Ends with text | self.Title endsWith "III" |
| Operator | Description | Example |
|---|---|---|
and |
Logical AND | a > 5 and b < 10 |
or |
Logical OR | a == 1 or b == 2 |
! |
Logical NOT | !isAlive |
| Operator | Description | Example |
|---|---|---|
exists |
Value exists | self.OptionalField exists |
!exists |
Value doesn't exist | self.OptionalField !exists |
isEmpty |
String or array is empty | self.Notes isEmpty |
isNotEmpty |
String or array has content | self.Inventory isNotEmpty |
if (condition) {
// statements
}
else if (condition) {
// statements
}
else {
// statements
}A compact inline conditional β returns one of two values based on a condition. Works anywhere an expression is valid: value: blocks, actions, function bodies.
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.
function <name>(<parameters>) returns <returnType> {
// function body
return <value>
}function name(type paramName) returns returnType { } // Required parameter
function name(type paramName = defaultValue) returns type { } // Default parameter-
number- Numeric value -
boolean- True/false value -
string- Text value -
nothing- No return value (void)
self.functionName(parameters)self.PropertyName // Direct property access
self[self.dynamicProperty] // Dynamic property lookup
self.property.subProperty // Nested property accessRequires a type guard.
parent.*only resolves inside anif (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".
if (parent is Hero) {
parent.PropertyName // Parent document property
parent[self.dynamicProperty] // Dynamic parent lookup
parent.property.subProperty // Nested parent access
}target.PropertyName // Target document property
target.property.subProperty // Nested target accessself.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 effectsUser.isGM // Boolean: Is user a GM?
User.name // String: User's nameCombat.isMyTurn // Boolean: Is it this character's turn?
Combat.isNotMyTurn // Boolean: Is it NOT this character's turn?Combat.nextTurn() // Advance to next turn
Combat.end() // End combatroll(diceExpression)
roll(diceExpression, param: value, ...) // with detection params (below)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 countOptional 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.
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| Parameter | Meaning |
|---|---|
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) { ... }.
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).
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.
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 = falseThe 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.
// 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(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.
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 asroll(...)). -
type:- The damage type. Typically achoice<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 <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).
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.
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.
| Line form | Renders as | Example |
|---|---|---|
| Plain expression | A line of body text. Strings render literally; expressions evaluate and convert to text. | "Hit for " + damage + "!" |
| Roll variable | The 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 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
}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
}each <variable> in <collection> {
// loop body
}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[startNumber to endNumber] // Inclusive range
[1 to 10] // Numbers 1 through 10
[self.MinLevel to self.MaxLevel] // Dynamic range[modifiers] action <name>(<parameters>) {
// action body
}Prefix an action with a modifier to control where it appears and who can see it:
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.
| Parameter | Description | Example |
|---|---|---|
visibility: |
Control visibility | visibility: Visibility.gmOnly |
icon: |
Action icon | icon: "fa-solid fa-sword" |
color: |
Action color | color: "#FF0000" |
label: |
Action label | label: "Custom Name" |
| Value | Description |
|---|---|
Visibility.unlocked |
Fully visible and editable |
Visibility.default |
Standard visibility |
Visibility.secret |
Hidden from players, visible to GMs |
Visibility.edit |
Only visible in edit mode |
Visibility.play |
Only visible in play mode |
Visibility.gmEdit |
GMs can edit, players view |
Visibility.gmOnly |
Only visible to GMs |
Visibility.readonly |
Visible but not interactive |
Visibility.locked |
Locked from interaction |
Visibility.hidden |
Completely hidden |
on <eventName>(<parameters>) {
// event handling code
}| Event | Description | Parameters |
|---|---|---|
combatStart |
Combat begins | None |
combatEnd |
Combat ends | None |
turnStart |
Character's turn starts | None |
turnEnd |
Character's turn ends | None |
turnIsNext |
Character's turn is next | None |
roundStart |
Combat round starts | None |
roundEnd |
Combat round ends | None |
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.
| Event | Description | Parameters |
|---|---|---|
preApplyDamage |
Before damage is applied to this document | number amount, string damageType, object damageMetadata |
appliedDamage |
After damage is applied | number amount, string damageType, object damageMetadata |
preApplyHealing |
Before healing is applied | number amount, string damageType, object damageMetadata |
appliedHealing |
After healing is applied | number amount, string damageType, object damageMetadata |
preApplyTemp |
Before temp HP is applied | number amount, string damageType, object damageMetadata |
appliedTemp |
After temp HP is applied | number amount, string damageType, object damageMetadata |
death |
The document's health resource hit 0 | None |
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.
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(...).
fleeting result = prompt(<parameters>) {
<field definitions>
}| Parameter | Description | Example |
|---|---|---|
target: |
Who sees prompt | target: "user" |
label: |
Window title | label: "Choose Action" |
icon: |
Window icon | icon: "fa-solid fa-question" |
width: |
Window width | width: 400 |
height: |
Window height | height: 300 |
location: |
Window position | location: 100, 200 |
limit: |
Time limit | limit: 30 seconds |
-
"user"- Current user -
"gm"- Game master -
"target"- Targeted player
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.
-
ms- Milliseconds -
seconds- Seconds -
minutes- Minutes
wait <duration> <unit>play(file: "path/to/audio.wav", volume: 75)| Parameter | Description | Default |
|---|---|---|
file: |
Audio file path | Required |
volume: |
Volume 0-100 | System default |
target is Actor // Check if target is actor
target is Item // Check if target is item
parent is Actor // Check parent document typeself.update() // Commit pending changes to the document now
self.delete() // Delete the current documentself.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.
log(message1, message2, ...) // Output to consolelog("Debug message")
log("Value:", variable)
log("Multiple", "values", 123, true)@js{ JavaScript code here }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.
Operations are evaluated in this order (highest to lowest precedence):
-
Parentheses -
(expression) -
Negation -
!expression,-expression -
Multiplication/Division -
*,/ -
Addition/Subtraction -
+,- -
Comparisons -
<,>,<=,>=,==,!=,equals,!equals -
Logical AND -
and -
Logical OR -
or
fleeting result = 5 + 3 * 2 // = 11 (not 16)
fleeting result = (5 + 3) * 2 // = 16
fleeting result = x > 5 and y < 10 // Comparison before ANDif (self.OptionalProperty exists and self.OptionalProperty > 0) {
// Safe to use property
}if (index >= 0 and index < array.length) {
fleeting value = array[index]
}fleeting result = divisor != 0 ? dividend / divisor : 0if ((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.