Skip to content

Interactivity

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

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:

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

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

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.

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

Field Result
string the entered text
number the entered number
boolean true / 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")
die the chosen die (e.g. "d8")
dice a dice pool object
date / time / datetime the 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

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

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

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

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

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

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

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

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:

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

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:

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

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():

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

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

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!

Clone this wiki locally