Skip to content

Interactivity ​

ISDL provides powerful features for creating interactive experiences that respond to user actions, game events, and system state changes.

Action Visibility and Control ​

Visibility System ​

ISDL uses a comprehensive visibility system to control when actions appear and how they behave. The full table of what each visibility allows for GMs, owners, and viewers is on the Fields page; the short summary:

  • Visibility.unlocked - Always read/write for everyone with read access
  • Visibility.default - Read for everyone; write/click depends on Edit vs. Play mode (this is what you get when you don't specify anything)
  • Visibility.secret - Hidden from non-owner viewers, visible to GMs and owners
  • Visibility.edit - Only visible/usable in Edit mode
  • Visibility.play - Only visible/usable in Play mode
  • Visibility.gmEdit - GMs can edit, owners and viewers can only read
  • Visibility.gmOnly - Only visible to GMs
  • Visibility.readonly - Visible to all but never clickable/editable
  • Visibility.locked - Read-only for everyone; equivalent to readonly for actions
  • Visibility.hidden - Completely hidden from view

Conditional Visibility ​

To make visibility depend on document state, use a method block that returns a Visibility.X value. Falling through with no return keeps the default visibility:

kotlin
action LevelUp(visibility: {
    if (self.Experience < self.Level * 1000) return Visibility.hidden
    // Otherwise: default visibility
}) {
    self.Experience -= self.Level * 1000
    self.Level += 1
}

action CastSpell(visibility: {
    if (self.MP < 5 or self.IsStunned) return Visibility.readonly
}) {
    self.MP -= 5
    // Spell logic here
}

action AdminFunction(visibility: {
    if (!User.isGM) return Visibility.hidden
}) {
    // Only visible to game masters
    self.AdminPower = true
}

Advanced Visibility Examples ​

kotlin
// Complex conditional visibility
action ConditionalSpell(visibility: {
    if (self.Level < 5) {
        return Visibility.hidden
    }
    else if (self.MP < 10) {
        return Visibility.readonly
    }
    else {
        return Visibility.default
    }
}) {
    self.MP -= 10
    // Cast powerful spell
}

// Role-based interactions
action PlayerAction(visibility: {
    if (User.isGM) return Visibility.readonly
}) {
    // GMs can see but not click, players can use normally
    self.PlayerActions += 1
}

action SecretGMTool(visibility: Visibility.gmOnly) {
    // Always hidden from players, always visible to GMs
    self.GMNotes = "Secret information updated"
}

Action Styling ​

Actions support visual customization with icons and colors:

kotlin
action AttackWithFire(icon: "fa-solid fa-fire", color: "#FF4500") {
    fleeting damage = roll(2d6 + self.STR)
    self.Target.HP -= damage
}

action Heal(icon: "fa-solid fa-heart", color: "#32CD32") {
    fleeting healing = roll(1d8) + self.WIS
    self.HP += healing
}

action StealthMode(icon: "fa-solid fa-mask", color: "#4B0082") {
    self.IsHidden = true
    self.MovementSpeed *= 2
}

Macro Actions ​

The macro action modifier marks an action as eligible for the Foundry macro hotbar. Players can drag a macro action onto their hotbar to invoke it without opening the sheet.

kotlin
macro action QuickHeal {
    fleeting healing = roll(2d4 + self.WIS)
    self.HP += healing

    chat Healing {
        "Healed for " + healing + " HP"
    }
}

The action still appears as a button on the sheet too — macro is additive, not exclusive.

Note

The grammar also accepts quick action and secondary action modifiers, but these are not yet wired into the generated UI. Use plain action for now.

Interactive Prompts ​

Create dynamic prompts that collect user input during gameplay.

Basic Prompt Syntax ​

Syntax: prompt(parameters) { fields }

Parameters:

  • target: "user" | "gm" | "target" - Who should see the prompt
  • label: "text" - Prompt window title
  • icon: "icon-name" - Icon for the prompt window
  • location: x, y - Screen position of the prompt
  • width: number | "auto" - Width of the prompt window
  • height: number | "auto" - Height of the prompt window
  • limit: number unit - Time limit for response

Supported prompt fields ​

A prompt is a one-shot input dialog, so it only accepts fields that ask the user for a single value. Each field resolves to that value on the result object (e.g. userInput.ActionType is the chosen string):

FieldResult
stringthe entered text
numberthe entered number
booleantrue / false
choice<string>the chosen value
choices<string>array of chosen values
choice<damageType>the chosen value
choice<Document>the chosen document's UUID
choices<Document>array of UUIDs
parent<...> / self<...>reference to the chosen property (e.g. "which attribute to use")
diethe chosen die (e.g. "d8")
dicea dice pool object
date / time / datetimethe entered value

Other field types — attribute, resource, tracker, money, html, paperdoll, tables, inventories, and layout blocks (row/column/section) — are not allowed in a prompt (they're persistent or display widgets, not one-shot inputs). Using one is a validation error. To collect a number, use number; to pick from options, use a choice.

Simple Prompts ​

kotlin
action GetUserChoice {
    fleeting userInput = prompt(
        target: "user",
        label: "Choose Your Action",
        width: 400,
        height: 300
    ) {
        choice<string> ActionType(choices: ["Attack", "Defend", "Cast Spell"])
        number PowerLevel(min: 1, max: 10, value: 5)
        string TargetName
    }
    
    // Use the input
    if (userInput.ActionType equals "Attack") {
        fleeting damage = roll(userInput.PowerLevel + "d6")
        // Apply damage logic
    }
}

GM Decision Prompts ​

kotlin
action RequestGMDecision {
    fleeting gmDecision = prompt(
        target: "gm", 
        label: "GM Decision Required",
        limit: 30 seconds
    ) {
        boolean AllowAction(label: "Allow this action?")
        string Reasoning(label: "Reasoning:")
        number DifficultyModifier(min: -5, max: 5, label: "Difficulty modifier:")
    }
    
    if (gmDecision.AllowAction) {
        fleeting roll = roll(d20) + gmDecision.DifficultyModifier
        
        chat GMDecision {
            "GM Decision: " + gmDecision.Reasoning
            "Modified difficulty by: " + gmDecision.DifficultyModifier
            tag roll
        }
    } else {
        chat GMDecision {
            "Action denied by GM"
            "Reason: " + gmDecision.Reasoning
        }
    }
}

Complex Interactive Forms ​

kotlin
action CharacterCreationPrompt {
    fleeting newCharacter = prompt(
        target: "user",
        label: "Create New Character",
        width: 600,
        height: 500,
        icon: "fa-solid fa-user-plus"
    ) {
        string CharacterName(label: "Character Name:")
        choice<string> Class(
            choices: ["Fighter", "Wizard", "Rogue", "Cleric"],
            label: "Choose Class:"
        )
        choice<string> Background(
            choices: ["Noble", "Criminal", "Scholar", "Soldier"],
            label: "Background:"
        )
        number StartingGold(min: 100, max: 1000, value: 500, label: "Starting Gold:")
        boolean ExpertMode(label: "Enable expert rules?")
    }
    
    // Apply character creation choices
    self.Name = newCharacter.CharacterName
    self.Class = newCharacter.Class
    self.Background = newCharacter.Background
    self.Gold = newCharacter.StartingGold
    
    if (newCharacter.ExpertMode) {
        self.ExpertRules = true
        self.MaxHP += 10
    }
    
    chat CharacterCreated {
        "Character created: " + newCharacter.CharacterName
        "Class: " + newCharacter.Class
        "Background: " + newCharacter.Background
        tag newCharacter.StartingGold
    }
}

Event Handling (Hook Handlers) ​

Respond automatically to game events with hook handlers.

Combat Events ​

kotlin
on combatStart {
    self.Initiative = roll(d20) + self.DEX
    self.ActionsRemaining = 3
    
    chat CombatStart {
        self.Name + " enters combat!"
        tag self.Initiative
    }
}

on turnStart {
    // Refresh action economy
    self.ActionsRemaining = 3
    self.MovementRemaining = self.Speed
    
    // Apply ongoing effects
    if (self.IsOnFire) {
        self.HP -= roll(1d6)
        chat OngoingDamage {
            self.Name + " takes fire damage!"
        }
    }
}

on combatEnd {
    self.IsRaging = false
    self.TempHP = 0
    
    chat CombatEnd {
        self.Name + " exits combat."
    }
}

Health and Damage Events ​

kotlin
// React to damage with defensive abilities
on preApplyDamage(number amount, string damageType) {
    if (damageType equals "Fire" and self.FireResistance) {
        amount = Math.floor(amount / 2)
        
        chat Resistance {
            self.Name + " resists fire damage!"
            "Damage reduced from " + (amount * 2) + " to " + amount
        }
    }
}

on appliedDamage(number amount) {
    if (amount > 10) {
        // Heavy damage triggers defensive reaction
        if (self.DefensiveReflexes > 0) {
            self.DefensiveReflexes -= 1
            self.AC += 2
            
            chat DefensiveReaction {
                "Heavy damage triggers defensive reflexes!"
                "+2 AC until next turn"
            }
        }
    }
}

// Death saving throws
on death {
    if (self.Level >= 3 and self.DeathSaves > 0) {
        fleeting deathSave = roll(d20)
        self.DeathSaves -= 1
        
        if (deathSave >= 15) {
            self.HP = 1
            self.Status = "Unconscious"
            
            chat DeathSave {
                self.Name + " makes a miraculous recovery!"
                tag deathSave
            }
        } else {
            chat DeathSave {
                "Death save failed."
                tag deathSave
                "Remaining saves: " + self.DeathSaves
            }
        }
    }
}

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 (createActor, updateItem, chatMessage, renderChatLog, etc.), as well as hooks fired by modules.

kotlin
// Listen to Foundry's actor-creation hook
on createActor(actor, options, userId) {
    if (actor.uuid equals self.uuid) {
        chat WelcomeMessage {
            flavor self.Name + " has entered the world!"
        }
    }
}

Note

ISDL does not currently provide a way to fire custom hooks from your own code. You can listen to anything, but the events themselves must be emitted by Foundry, a module, or external code that calls Hooks.callAll(...). The ISDL helper hooks documented above (preApplyDamage, appliedDamage, death, etc.) are fired by ISDL's generated code on your behalf — those are the only ones you can rely on without external infrastructure.

System Integration ​

User Properties ​

Access information about the current user:

kotlin
action ShowWelcome {
    chat Welcome {
        "Welcome, " + User.name + "!"
        User.isGM ? "GM controls available." : "Player mode active."
    }
}

action AdminFunction {
    if (User.isGM) {
        // Only GMs can perform this action
        self.SpecialPower = true
        
        chat AdminAction {
            "GM has granted special power to " + self.Name
        }
    }
}

Combat Integration ​

kotlin
action EndTurn {
    if (Combat.isMyTurn) {
        self.ActionsUsed = 0
        Combat.nextTurn()
        
        chat TurnEnd {
            self.Name + " ends their turn."
        }
    }
}

action EmergencyRetreat(visibility: {
    if (!User.isGM) return Visibility.hidden
}) {
    Combat.end()
    chat Retreat {
        "Combat has been ended by the GM!"
    }
}

// Conditional actions based on combat state
action CombatAction(visibility: {
    if (Combat.isNotMyTurn) return Visibility.readonly
}) {
    fleeting damage = roll(1d8 + self.STR)
    self.Target.HP -= damage
}

Timing and Delays ​

Wait Functionality ​

Create timed sequences and delays:

kotlin
action DelayedEffect {
    chat Immediate {
        "Spell is charging..."
    }
    
    wait 3 seconds
    
    fleeting damage = roll(4d6)
    self.Target.HP -= damage
    
    chat Delayed {
        "Spell explodes for " + damage + " damage!"
    }
}

action CountdownSequence {
    chat Start { "Countdown starting..." }
    
    each second in [3 to 1] {
        wait 1 seconds
        chat Count { second + "..." }
    }
    
    wait 1 seconds
    chat Final { "GO!" }
}

Timed Prompts ​

kotlin
action TimedDecision {
    fleeting quickChoice = prompt(
        target: "user",
        label: "Quick Decision!",
        limit: 10 seconds
    ) {
        choice<string> Response(choices: ["Fight", "Flight", "Hide"])
    }
    
    if (quickChoice exists) {
        chat Decision {
            "Chose: " + quickChoice.Response
        }
    } else {
        chat Timeout {
            "No decision made - defaulting to confusion!"
        }
        self.IsConfused = true
    }
}

Audio Integration ​

Add sound effects and audio cues to enhance the experience:

kotlin
action PlaySwordStrike {
    play(file: "sounds/sword-hit.wav", volume: 75)
    fleeting damage = roll(1d8) + self.STR
    self.Target.HP -= damage
    
    chat Attack {
        "Sword strike hits!"
        tag damage
    }
}

action CastSpell {
    play(file: "sounds/magic-missile.mp3")
    wait 2 seconds
    
    fleeting damage = roll(3d4 + 1)
    self.Target.HP -= damage
    
    play(file: "sounds/explosion.wav", volume: 50)
    
    chat Spell {
        "Magic missile hits for " + damage + " damage!"
    }
}

action EnvironmentalEffect {
    play(file: "sounds/thunder.wav", volume: 100)
    
    each character in self.AllNearbyCharacters {
        if (character.ThunderResistance !exists) {
            character.IsStunned = true
        }
    }
    
    wait 5 seconds
    play(file: "sounds/rain-fade.wav", volume: 30)
}

Debug and Development Tools ​

Logging for Development ​

kotlin
action DebugCalculation {
    fleeting damage = roll(2d6) + self.STR
    log("Calculated damage:", damage)
    log("STR modifier:", self.STR)
    log("Character level:", self.Level)
    
    if (damage > 10) {
        log("High damage roll detected!")
        self.CriticalHits += 1
    }
    
    log("Final values - Damage:", damage, "Crits:", self.CriticalHits)
}

Update and Refresh ​

Inside an action, ISDL queues your assignments (self.HP -= 5, etc.) and applies them as a single document update at the end. If you need to force the update to apply before the action ends — for example, because subsequent code in the same action wants to read the updated value back — call self.update():

kotlin
action ModifyStats {
    self.STR += 2
    self.MaxHP = self.CON * 10

    // Commit pending changes now so subsequent reads see the new values
    self.update()

    chat StatChange {
        "Stats modified!"
        "New STR: " + self.STR
        "New Max HP: " + self.MaxHP
    }
}

You generally don't need this — the action's end-of-body flush handles the common case. Reach for self.update() only when in-action read-after-write ordering matters.

Best Practices for Interactivity ​

User Experience Guidelines ​

  1. Clear feedback - Always provide chat messages for user actions
  2. Appropriate visibility - Don't show actions users can't use
  3. Reasonable timeouts - Give users enough time for complex decisions
  4. Consistent styling - Use meaningful icons and colors
  5. Graceful failures - Handle edge cases and provide fallbacks

Performance Considerations ​

kotlin
// Good: Cache expensive calculations
action EfficientInteraction {
    eternal complexCalculation = self.calculateComplexValue()
    
    if (User.isGM and complexCalculation > 100) {
        // Use cached result multiple times
    }
}

// Avoid: Recalculating in visibility conditions
action InefficientInteraction(visibility: {
    if (self.expensiveFunction() <= 50) return Visibility.hidden
}) {
    // This recalculates expensiveFunction() every time visibility is checked
}

Accessibility ​

kotlin
action AccessibleAction(
    icon: "fa-solid fa-heal",
    color: "#32CD32"
) {
    fleeting healing = roll(2d4) + self.WIS
    self.HP += healing
    
    // Clear, descriptive feedback
    chat Healing {
        self.Name + " heals for " + healing + " hit points"
        "Current HP: " + self.HP + "/" + self.MaxHP
        tag healing
    }
    
    // Audio cue for screen readers
    play(file: "sounds/heal-chime.wav", volume: 50)
}

Next Steps ​

Master these interactive features and you'll be able to create rich, responsive RPG systems:

  • Recipes - Copy-paste solutions for typical interactive scenarios and RPG mechanics
  • Logic Reference - Complete reference for all interactive syntax

Your systems can now respond intelligently to player actions and game state changes!