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 accessVisibility.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 ownersVisibility.edit- Only visible/usable in Edit modeVisibility.play- Only visible/usable in Play modeVisibility.gmEdit- GMs can edit, owners and viewers can only readVisibility.gmOnly- Only visible to GMsVisibility.readonly- Visible to all but never clickable/editableVisibility.locked- Read-only for everyone; equivalent to readonly for actionsVisibility.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 promptlabel: "text"- Prompt window titleicon: "icon-name"- Icon for the prompt windowlocation: x, y- Screen position of the promptwidth: number | "auto"- Width of the prompt windowheight: number | "auto"- Height of the prompt windowlimit: 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 ā
- Clear feedback - Always provide chat messages for user actions
- Appropriate visibility - Don't show actions users can't use
- Reasonable timeouts - Give users enough time for complex decisions
- Consistent styling - Use meaningful icons and colors
- 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!