Skip to content

Recipes

Cody Swendrowski edited this page Jun 12, 2026 · 14 revisions

Recipes & Common Patterns

A collection of helpful snippets and copy-paste solutions for frequently used RPG mechanics. These patterns work out-of-the-box and can be customized for your system.

New to ISDL? The concept pages explain the building blocks these recipes use: Fields covers every field type, Basic Logic covers actions and dice, Advanced Logic covers functions and loops, and Interactivity covers prompts and events. If you're looking for something specific, the How Do I…? page routes you directly to the right answer.

Rolling a standard D20 attack roll

β†’ Related: attribute, parent<type>, roll(), chat

Many D20 systems follow the form of d20 + Attribute mod for their attack. We can calculate the roll by storing per-equipment what Attribute it uses. Referencing this value will automatically get the parent's value and grab the mod by default for Attributes.

This example takes advantage of the powerful HTML editor to display rolls and other effects that occur on a hit, but more data modeling could be done to replace that with properties instead.

actor PC {
  attribute Strength(min: 1, max: 30, mod: {
    return (self.Strength - 10) / 2
  }
  attribute Dexterity(min: 1, max: 30, mod: {
    return (self.Dexterity- 10) / 2
  }
  table<Equipment> Equipment
}

item Equipment {
  parent<Attribute> Attribute
  html OnHit
  action Roll(icon: "fa-solid fa-dice-d20") {
        fleeting attack = roll(d20 + self.Attribute)
        chat AttackRoll {
          attack
          self.OnHit
        }
  }
}

Rolling with Advantage / Bonuses

β†’ Related: prompt(), roll()

Often players get granted boons / banes which modify the roll. We can prompt either the current User (no target)

item Equipment {
  parent<Attribute> Attribute
  html OnHit
  action Roll(icon: "fa-solid fa-dice-d20") {
        fleeting attack = roll(d20 + self.Attribute)
        fleeting userAnswer = prompt {
          boolean HasAdvantage
          number Bonus
        }
        if (userAnswer.HasAdvantage) {
          attack = roll(2d20kh + self.Attribute + userAnswer.Bonus)
        }
        else {
          attack = roll(d20 + self.Attribute + userAnswer.Bonus)
        }
        chat AttackRoll {
          attack
          self.OnHit
        }
  }
}

or the GM (target: GM)

item Equipment {
  parent<Attribute> Attribute
  html OnHit
  action Roll(icon: "fa-solid fa-dice-d20") {
        fleeting attack = roll(d20 + self.Attribute)
        fleeting gmAnswer = prompt(target: gm) {
          boolean HasAdvantage
          number Bonus
        }
        if (userAnswer.HasAdvantage) {
          attack = roll(2d20kh + self.Attribute + gmAnswer.Bonus)
        }
        else {
          attack = roll(d20 + self.Attribute + gmAnswer.Bonus)
        }
        chat AttackRoll {
          attack
          self.OnHit
        }
  }
}

Detecting if an attack hits

β†’ Related: target, roll(), chat

Reading target data is available to non-GM's, and allows you to calculate if an attack should hit. target will resolve to the first target a user has, and if none are selected the code inside the target check won't run. Checking the document type of the target is a required step, as it helps to ensure safe access to the data - a Monster and a Structure might have very different data, for instance.

actor NPC {
  number ArmorClass
}

item Equipment {
  parent<Attribute> Attribute
  html OnHit
  action Roll(icon: "fa-solid fa-dice-d20") {
        fleeting attack = roll(d20 + self.Attribute)
        if (target is NPC) {
          if (attack.total >= target.ArmorClass) {
            chat AttackRoll {
              attack
              self.OnHit
            }
          }
        }
        else { // Do something else }
  }
}

Tracking Uses Left

β†’ Related: tracker, visibility:, Visibility.locked

Many spells and talents can only be used so often, making it helpful to keep track of uses left automatically and block usage if no uses are left. tracker is a good element for this as it allows the max to be controlled by the user, or optionally a max can be set in the syntax. Trackers also have built-in UI to refill / empty them. number can also be used, although it doesn't allow a user editable max.

item Skill {
  tracker UsesLeft(style: slashes)
  
  action Use(visibility: {
    if (self.UsesLeft <= 0) return Visibility.locked // Locked or readonly will disable the Action
    // If uses are left, the default visibility will be used
  })
  {
    self.UsesLeft -= 1
    // Do something
  }
}

Tracking actions and ending turn automatically

β†’ Related: tracker, on turnStart, Combat.nextTurn()

Many systems track Action Points (AP), and when you're out, your turn is up. Like skill uses, a tracker is a good element to use for this. This example takes advantage of Combat.isMyTurn to lock the Action to only be available when it's that character's turn

actor PC {
  tracker AP(style: slashes, max: 3)
  
  on turnStart {
    self.AP = 3
  }

  table<Skill> Skills
}

item Skill {
  tracker UsesLeft(style: slashes)
  number ApCost
  
  action Use(visibility: {
    if (self.UsesLeft <= 0) return Visibility.locked
    else if (Combat.isMyTurn) {
      return Visibility.default
    }
    else {
      return Visibility.locked
    }
  })
  {
    self.UsesLeft -= 1
    // Do something
    if (parent is PC) {
      fleeting remainingAp = parent.AP - self.ApCost
      parent.AP = remainingAp
      if (remainingAp <= 0) {
        Combat.nextTurn()
      }
    }
  }
}

Handling shields / armor / resistances / etc

β†’ Related: resource, on preApplyDamage, tracker

A health resource already comes out of the box with support for temporary values, useful for things like basic shields or overflow health. Damage is applied to a sheet via a chat card driven UX.

For more advanced setups, you can use the on preApplyDamage to adjust the amount of damage about to be applied.

Shields prevent all damage

actor PC {
    tracker Shields(style: icons, icon: "fa-shield-halved")
    on preApplyDamage (damage) {
        if (self.Shields > 0) {
            self.Shields -= 1
            damage = 0
            
            chat ShieldAbsorb {
                flavor "Shield Absorbed"
                "The shield absorbed the damage."
            }
        }
    }
}

Armor is removed first

actor PC {
    tracker Armor(style: slashes)
    on preApplyDamage (damage) {
        if (self.Armor > 0) {
            // Order is important
            fleeting remainingArmor = self.Armor - damage
            damage -= self.Armor
            self.Armor = remainingArmor 
        }
    }
}

Damage resistance

ISDL doesn't currently support attaching damage types to damage, but for now you can do basic flat damage resistances

actor PC {
    number DamageResistance(min: 0, max: 100) // 0 would resist no damage, 100 would resist all damage
    on preApplyDamage (damage) {
        damage *= 1 - (self.DamageResistance / 100)
    }
}

Poison / Burn / etc

β†’ Related: tracker, on turnStart, hidden

This example takes advantage of ISDL's custom Active Effect mode "Add Once". Hidden numbers aren't rendered on the sheet, but can be impacted by Active Effects. Setting up an effect with 3 current Burn AddOnce would add to the Burn when the Effect is added to the sheet.

actor PC {
  hidden tracker BurnAmount
  on combatStart {
    self.BurnAmount = 0
  }

  on turnStart {
        if (self.BurnAmount > 0) {
            self.Health -= self.BurnAmount
            self.BurnAmount -= 1
            
            chat Burn {
                flavor "Hot hot hot"
                "You take " + self.BurnAmount + " πŸ”₯ burn damage."
            }
        }
    }
}

Item Weight

β†’ Related: each loops, value: (calculated), tracker

If your system cares about things like "how much can I carry", you can use a calculated value to help show that.

actor PC {

  tracker WeightLimit(style: bar, min: 0, max: 100, value: {
    fleeting total = 0
    each loot in self.Loot {
        total += loot.Weight
    }
    return total
  })

  table<Loot> Loot
}

item Loot {
  number Weight(min: 0)
}

Class adds fields (Rage, Bloodlust, etc.)

β†’ Related: choice<string>, visibility:, Visibility.hidden

A common pattern in class-based RPGs: picking a class (Barbarian, Mage, Rogue) should change the character β€” adding stats, resources, or actions that other classes don't have. ISDL doesn't have a class-as-mixin construct, but you can get the same effect with the visibility system: define every possible field on the actor, and use visibility: to hide the ones that don't apply.

actor Hero {
    choice<string> Class(choices: ["Barbarian", "Mage", "Rogue"])

    health resource HP

    // Barbarian-only: rage tracker shown only when Class is Barbarian
    tracker Rage(max: 6, style: segmented, visibility: {
        if (self.Class equals "Barbarian") return Visibility.default
        return Visibility.hidden
    })

    // Mage-only: spell points
    resource Mana(max: { return self.Level * 5 }, visibility: {
        if (self.Class equals "Mage") return Visibility.default
        return Visibility.hidden
    })

    // Rogue-only: sneak-attack die
    die SneakAttackDie(initial: "d6", visibility: {
        if (self.Class equals "Rogue") return Visibility.default
        return Visibility.hidden
    })
}

When the player picks "Barbarian", they see HP and Rage. Picking "Mage" hides Rage and shows Mana. Hidden fields don't render on the sheet at all, so the UI stays clean.

The same pattern works for actions:

action Rage(visibility: {
    if (self.Class equals "Barbarian" and self.Rage > 0) return Visibility.default
    return Visibility.hidden
}) {
    self.Rage -= 1
    chat rage { flavor "RAAAAAH!" }
}

For larger systems, consider modeling each class as a Class item with a choice<Class> Class(global: true) field on the actor. Class items can carry their own data (description, level table, etc.), and your visibility checks become if (self.Class.Name equals "Barbarian"). See the tutorial's "What's next?" for the Class-as-item pattern.

Character Creation and Progression

β†’ Related: number, resource, action, visibility:

Level-Up System with Experience Points

actor Hero {
    number Level(min: 1, max: 20, value: 1)
    number Experience
    resource HP(max: { return self.Level * 10 + self.CON })
    
    action LevelUp(visibility: self.Experience >= (self.Level * 1000) ? Visibility.default : Visibility.hidden) {
        eternal costPerLevel = 1000
        fleeting requiredXP = self.Level * costPerLevel
        
        self.Experience -= requiredXP
        self.Level += 1
        
        fleeting hpGain = roll(1d8) + self.CON
        
        chat LevelUp {
            "LEVEL UP! Now level " + self.Level
            "Gained " + hpGain + " maximum HP"
            tag self.Level
            tag self.HP.max
        }
    }
}

Point-Buy Attribute System

actor Character {
    number AttributePoints(value: 27)
    
    number Strength(min: 8, max: 15, value: 8)
    number Dexterity(min: 8, max: 15, value: 8)  
    number Constitution(min: 8, max: 15, value: 8)
    number Intelligence(min: 8, max: 15, value: 8)
    number Wisdom(min: 8, max: 15, value: 8)
    number Charisma(min: 8, max: 15, value: 8)
    
    readonly number UsedPoints(value: {
        fleeting total = 0
        fleeting attributes = [self.Strength, self.Dexterity, self.Constitution, self.Intelligence, self.Wisdom, self.Charisma]
        
        each attr in attributes {
            if (attr <= 13) {
                total += attr - 8
            } else {
                total += (attr - 8) + (attr - 13)  // Higher costs for 14-15
            }
        }
        
        return total
    })
    
    readonly number RemainingPoints(value: {
        return self.AttributePoints - self.UsedPoints
    })
}

Advanced Combat Mechanics

β†’ Related: roll(), value: (calculated), on preApplyDamage

Armor Class Calculation

actor Character {
    number BaseAC(value: 10)
    attribute Dexterity(mod: { return Math.floor((self.Dexterity - 10) / 2) })
    boolean WearingArmor
    number ArmorBonus
    boolean HasShield
    
    readonly number AC(value: {
        fleeting total = self.BaseAC + self.Dexterity.mod
        
        if (self.WearingArmor) {
            total += self.ArmorBonus
        }
        
        if (self.HasShield) {
            total += 2
        }
        
        return total
    })
}

Attack with Critical Hits

The crit: parameter checks the natural die face, so a crit is a real natural 20 β€” not just a 20+ total after bonuses. The chat card also auto-highlights green on a crit (red on a fumble).

action Attack {
    fleeting attackRoll = roll(d20 + self.AttackBonus, crit: 20, fumble: 1)

    if (attackRoll.fumble) {
        chat AttackResult {
            "Critical miss!"
            tag attackRoll
        }
    }
    else if (attackRoll >= self.Target.AC) {
        fleeting damage = roll(self.WeaponDamage + self.STR)

        if (attackRoll.crit) {
            damage = damage * 2
            self.CriticalHits += 1
        }

        self.Target.HP -= damage

        chat AttackResult {
            attackRoll.crit ? "CRITICAL HIT!" : "Attack hits!"
            "Damage: " + damage
            tag attackRoll
            tag self.Target.HP
        }
    } else {
        chat AttackResult {
            "Attack misses!"
            tag attackRoll
            tag self.Target.AC
        }
    }
}

Dice Pool: Counting Successes

World of Darkness / Storyteller style β€” roll a pool, count dice at or above a target, with 1s subtracting.

action ChannelMagic {
    fleeting pool = roll((self.Willpower)d10, success: >= 8, failure: 1)

    chat MagicResult {
        "Rolled " + pool.successes + " success(es)"
        if (pool.successes <= 0) { "The ritual backfires!" }
        tag pool
    }
}

Dice Pool: Fail if Any Die Shows a 1 (EZD6)

Keep-highest pools drop dice, but contains/count still see the dropped faces β€” so a 1 on a discarded die still fizzles the spell.

action CastSpell {
    fleeting cast = roll((self.SpellDice)d6 kh1)

    if (cast.contains(1)) {
        chat SpellResult { "The spell fizzles β€” a 1 was rolled!" }
    }
    else if (cast >= self.Difficulty) {
        chat SpellResult { "Success! (" + cast.total + ")" }
    }
    else {
        chat SpellResult { "The spell fails to take hold." }
    }
}

Damage Type Resistance System

on preApplyDamage(number amount, string damageType) {
    fleeting finalDamage = amount
    
    // Check resistances
    if (damageType equals "Fire" and self.FireResistance) {
        finalDamage = Math.floor(amount / 2)
        chat Resistance {
            "Fire resistance reduces damage!"
            "Damage reduced from " + amount + " to " + finalDamage
        }
    }
    else if (damageType equals "Cold" and self.ColdImmunity) {
        finalDamage = 0
        chat Immunity {
            "Cold immunity negates all damage!"
        }
    }
    else if (damageType equals "Poison" and self.PoisonVulnerability) {
        finalDamage = amount * 2
        chat Vulnerability {
            "Poison vulnerability doubles damage!"
            "Damage increased from " + amount + " to " + finalDamage
        }
    }
    
    // Apply the modified damage
    amount = finalDamage
}

Skill Systems

β†’ Related: function, roll(), prompt()

Skill Checks with Degrees of Success

function makeSkillCheck(string skillName, number difficulty) returns boolean {
    fleeting skillBonus = parent[skillName].mod
    fleeting roll = roll(d20) + skillBonus + self.Level
    
    fleeting success = roll >= difficulty
    
    chat SkillCheck {
        success ? "Skill check succeeded!" : "Skill check failed."
        skillName + " check: " + roll + " vs DC " + difficulty
        tag roll
    }
    
    return success
}

action PerformSkill {
    fleeting roll = roll(d20) + self.SkillMod
    eternal easy = 10
    eternal medium = 15
    eternal hard = 20
    
    if (roll >= hard) {
        chat SkillResult {
            "Exceptional success!"
            tag roll
        }
        self.Experience += 100
    }
    else if (roll >= medium) {
        chat SkillResult {
            "Good success!"
            tag roll
        }
        self.Experience += 50
    }
    else if (roll >= easy) {
        chat SkillResult {
            "Basic success."
            tag roll
        }
        self.Experience += 25
    }
    else {
        chat SkillResult {
            "Failed attempt."
            tag roll
        }
    }
}

Advantage/Disadvantage System

function rollWithAdvantage(string diceExpression, boolean hasAdvantage, boolean hasDisadvantage) returns number {
    if (hasAdvantage and !hasDisadvantage) {
        fleeting roll1 = roll(diceExpression)
        fleeting roll2 = roll(diceExpression)
        fleeting result = Math.max(roll1, roll2)
        
        chat AdvantageRoll {
            "Rolling with advantage"
            "Rolled: " + roll1 + " and " + roll2
            "Using: " + result
        }
        
        return result
    }
    else if (hasDisadvantage and !hasAdvantage) {
        fleeting roll1 = roll(diceExpression)
        fleeting roll2 = roll(diceExpression)
        fleeting result = Math.min(roll1, roll2)
        
        chat DisadvantageRoll {
            "Rolling with disadvantage"  
            "Rolled: " + roll1 + " and " + roll2
            "Using: " + result
        }
        
        return result
    }
    else {
        return roll(diceExpression)
    }
}

Resource Management

β†’ Related: tracker, function, each loops

Spell Slots System

actor Spellcaster {
    section SpellSlots {
        tracker Level1Slots(max: { return Math.max(1, Math.floor(self.Level / 2)) })
        tracker Level2Slots(max: { return Math.max(0, Math.floor(self.Level / 4)) })
        tracker Level3Slots(max: { return Math.max(0, Math.floor(self.Level / 6)) })
    }
    
    function canCastSpell(number spellLevel) returns boolean {
        if (spellLevel equals 1) {
            return self.Level1Slots > 0
        }
        else if (spellLevel equals 2) {
            return self.Level2Slots > 0
        }
        else if (spellLevel equals 3) {
            return self.Level3Slots > 0
        }
        return false
    }

    function castSpell(number spellLevel) returns nothing {
        if (spellLevel equals 1) {
            self.Level1Slots -= 1
        }
        else if (spellLevel equals 2) {
            self.Level2Slots -= 1
        }
        else if (spellLevel equals 3) {
            self.Level3Slots -= 1
        }
    }
    
    action RestoreSpellSlots {
        self.Level1Slots = self.Level1Slots.max
        self.Level2Slots = self.Level2Slots.max
        self.Level3Slots = self.Level3Slots.max
        
        chat SpellSlotsRestored {
            "All spell slots restored!"
        }
    }
}

Advanced Inventory Weight System

actor Character {
    number CarryingCapacity(value: { return self.STR * 15 })
    
    readonly number TotalWeight(value: {
        fleeting weight = 0
        
        each item in self.Equipment {
            if (item.Weight exists) {
                weight += item.Weight * item.Quantity
            }
        }
        
        return weight
    })
    
    readonly string EncumbranceStatus(value: {
        fleeting ratio = self.TotalWeight / self.CarryingCapacity
        
        if (ratio <= 0.33) {
            return "Light Load"
        }
        else if (ratio <= 0.66) {
            return "Medium Load"
        }
        else if (ratio <= 1.0) {
            return "Heavy Load"
        }
        else {
            return "Overloaded"
        }
    })
    
    readonly boolean IsOverloaded(value: {
        return self.TotalWeight > self.CarryingCapacity
    })
}

Status Effects and Conditions

β†’ Related: boolean, on turnStart, status

Temporary Conditions with Duration

actor Character {
    section StatusEffects {
        boolean IsStunned
        boolean IsBlinded  
        boolean IsCharmed
        boolean IsPoisoned
        number PoisonDuration
        number StunDuration
    }
    
    action ApplyStun(number duration) {
        self.IsStunned = true
        self.StunDuration = duration
        
        chat ConditionApplied {
            self.Name + " is stunned for " + duration + " rounds!"
        }
    }
    
    on turnStart {
        // Process ongoing conditions
        if (self.IsStunned and self.StunDuration > 0) {
            self.StunDuration -= 1
            
            if (self.StunDuration <= 0) {
                self.IsStunned = false
                chat ConditionEnded {
                    self.Name + " is no longer stunned."
                }
            }
        }
        
        if (self.IsPoisoned and self.PoisonDuration > 0) {
            self.HP -= roll(1d4)
            self.PoisonDuration -= 1
            
            chat PoisonDamage {
                self.Name + " takes poison damage!"
            }
            
            if (self.PoisonDuration <= 0) {
                self.IsPoisoned = false
                chat ConditionEnded {
                    self.Name + " recovers from poison."
                }
            }
        }
    }
}

Buff/Debuff System with Timed Effects

actor Character {
    section TemporaryEffects {
        number StrengthBonus
        number DexterityBonus
        number ConstitutionBonus
        number TemporaryHP
    }
    
    function applyBuff(string statName, number bonus, number duration) returns nothing {
        if (statName equals "Strength") {
            self.StrengthBonus += bonus
        }
        else if (statName equals "Dexterity") {
            self.DexterityBonus += bonus
        }
        else if (statName equals "Constitution") {
            self.ConstitutionBonus += bonus
        }
        
        chat BuffApplied {
            statName + " increased by " + bonus + " for " + duration + " minutes!"
        }
        
        // Schedule buff removal
        wait duration minutes
        
        if (statName equals "Strength") {
            self.StrengthBonus -= bonus
        }
        else if (statName equals "Dexterity") {
            self.DexterityBonus -= bonus
        }
        else if (statName equals "Constitution") {
            self.ConstitutionBonus -= bonus
        }
        
        chat BuffExpired {
            statName + " boost expires."
        }
    }
    
    action CastBullsStrength {
        if (self.canCastSpell(2)) {
            self.castSpell(2)
            self.applyBuff("Strength", 4, 10)
        }
    }
}

Economy and Trading

β†’ Related: money, function, number

Multi-Currency System

actor Character {
    section Currency {
        number Copper
        number Silver  
        number Gold
        number Platinum
    }
    
    readonly number TotalCurrency(value: {
        return self.Copper + (self.Silver * 10) + (self.Gold * 100) + (self.Platinum * 1000)
    })
    
    function payAmount(number copperAmount) returns boolean {
        if (self.TotalCurrency >= copperAmount) {
            // Convert and pay from largest denominations first
            fleeting remaining = copperAmount
            
            fleeting platinumNeeded = Math.min(Math.floor(remaining / 1000), self.Platinum)
            self.Platinum -= platinumNeeded
            remaining -= platinumNeeded * 1000
            
            fleeting goldNeeded = Math.min(Math.floor(remaining / 100), self.Gold)
            self.Gold -= goldNeeded
            remaining -= goldNeeded * 100
            
            fleeting silverNeeded = Math.min(Math.floor(remaining / 10), self.Silver)
            self.Silver -= silverNeeded
            remaining -= silverNeeded * 10
            
            self.Copper -= remaining
            
            return true
        }
        
        return false
    }
    
    action BuyItem(number cost) {
        if (self.payAmount(cost)) {
            chat Purchase {
                "Purchase successful!"
                "Paid: " + cost + " copper"
            }
        } else {
            chat InsufficientFunds {
                "Not enough money!"
                "Need: " + cost + " copper"
                "Have: " + self.TotalCurrency + " copper"
            }
        }
    }
}

User Interface Patterns

β†’ Related: value: (calculated), string, boolean

Progress Tracking

actor Character {
    number QuestsCompleted
    number TotalQuests(value: 20)
    
    readonly number QuestProgress(value: {
        return Math.min(100, Math.floor((self.QuestsCompleted / self.TotalQuests) * 100))
    })
    
    readonly string ProgressDescription(value: {
        fleeting percent = self.QuestProgress
        
        if (percent >= 100) {
            return "All quests complete!"
        }
        else if (percent >= 75) {
            return "Nearly finished"
        }
        else if (percent >= 50) {
            return "Halfway there"  
        }
        else if (percent >= 25) {
            return "Making progress"
        }
        else {
            return "Just getting started"
        }
    })
    
    action CompleteQuest {
        self.QuestsCompleted += 1
        
        chat QuestComplete {
            "Quest completed!"
            "Progress: " + self.QuestProgress + "%"
            self.ProgressDescription
        }
    }
}

Dice and Random Generation

β†’ Related: roll(), function, each loops

Exploding Dice

function explodingRoll(string baseDice, number explodeOn) returns number {
    fleeting total = 0
    fleeting keepRolling = true
    
    while (keepRolling) {
        fleeting thisRoll = roll(baseDice)
        total += thisRoll
        
        if (thisRoll >= explodeOn) {
            chat ExplodingDie {
                "Die exploded! Rolled " + thisRoll + ", rolling again..."
            }
        } else {
            keepRolling = false
        }
    }
    
    return total
}

action ExplodingAttack {
    fleeting damage = self.explodingRoll("d8", 8) + self.STR
    
    self.Target.HP -= damage
    
    chat ExplodingAttack {
        "Exploding damage roll: " + damage
    }
}

Random Tables

action RandomEncounter {
    eternal encounters = [
        "Bandits attack!",
        "Friendly merchant offers trade",
        "Ancient ruins discovered",
        "Wild animals block the path", 
        "Mystical fog rolls in",
        "Treasure chest found!"
    ]
    
    fleeting roll = roll(d6) - 1  // Convert to 0-based index
    fleeting encounter = encounters[roll]
    
    chat RandomEncounter {
        "Random Encounter:"
        encounter
        tag roll + 1
    }
}

Clone this wiki locally