MapTool geek-out update: Flexible monster creation

Update: The new campaign file now has a cool Edit Monster macro that lets you tweak things on the fly, including Solo/Elite/Minion status. Thank you to JonathanTheBlack over on the MapTool forums!

After I put my initial pass at new damage tables and MapTool monster templates out there for the world to see, I saw some niggling problems that I wanted to correct.

First, my original damage tables had a couple of flat spots where the damage expression stayed the same from one level to the next. Since average damage is supposed to increase by 1 per level, this bugged me and I wanted to fix it.

Second, I knew I should be able to make monsters tweakable more easily with some additional properties.

Third, I wanted to handle limited damage expressions more elegantly.

Updated damage tables

My damage table now increases at every level. It also has a slightly wider range than the official tables, even at low level, but I’m okay with that. My crits will hurt a little more and my lousy damage rolls will hurt a little less than with official monsters. At the lowest levels, my expressions still have less overall variance because I’m rolling two dice right from the start (the official expressions only roll one for level 1-3). And at the highest levels I have some more variance because I’m only rolling two dice instead of four; that’s a good feature in my opinion.

I also fleshed out the table to include damage expressions for multi-target attacks (about 25% less than standard attacks) as well as limited damage expressions (25% more and 50% more than standard attacks). These took some fiddling to get right, but I’m pretty happy with where they ended up.

Tweakable monsters

I added a property for the attack bonus versus non-AC defenses (NADs); it’s just the normal attack bonus minus 2, but it makes it easier to tweak attack macros en masse. If you want a particular monster to have an easier time hitting NADs (as Soldiers do), just tweak the one property rather than editing every attack macro.

The same goes for having the 125% and 150% damage expressions; it’s easier to do something like tweak the Brute by changing the normal damage to the 125% numbers and the 125% numbers to the 150% numbers in the monster’s properties rather than in each individual attack macro.

Limited damage expressions

High-damage attacks are not perfectly formulaic; the official guidelines say to increase the damage for encounter or recharge powers by 25 to 50 percent.

I decided to add a formula. My generic monsters’ recharge powers do 25% extra damage, while their encounter powers do 50% extra damage. If it’s a multi-target recharge attack I use the standard single-target damage expressions; a multi-target encounter attack gets the 125% damage expressions.

A word on artillery monsters

The official guidelines say that artillery monsters should have +1 or +2 to hit for ranged or area attacks. This is entirely too vague for me, and messy to implement. So, I just gave them +2 to hit versus both AC and NADs. Sure, maybe they’re a little more accurate than they “should” be; I can live with that.

Putting it all together

If you want to download a template campaign file with the sample monsters and properties, you can download it here. The file was created in version 1.3.b66 of MapTool. The properties themselves have been pasted below.

#---StatsToSetManually-----
#Level:1
#Role:Skirmisher
#HPModifier:8
#Spd:6
#NativeSize:Medium
#ActPts:0
#SaveBonus:0
#HeShe:It
#ArmorClassRoleMod:0
#InitiativeRoleMod:0
#Leader:0
#MinionHP:0
#SubType:Standard
#SubTypeHPMod:1
#----StatsThatCanBeDefaulted---------
#MaxHP:{((HPModifier + Constitution + (Level * HPModifier)) * SubTypeHPMod) + MinionHP}
#ArmorClass:{Level+14+ArmorClassRoleMod+ACAdj}
#Fortitude:{Level+12+FortAdj}
#Reflex:{Level+12+RefAdj}
#Will:{Level+12+WillAdj}
#Init:{HalfLevel+DexMod-ArmorPenalty+InitAdj}
#Strength:{10+HalfLevel+StrAdj}
#Constitution:{10+HalfLevel+ConAdj}
#Dexterity:{10+HalfLevel+DexAdj}
#Intelligence:{10+HalfLevel+IntAdj}
#Wisdom:{10+HalfLevel+WisAdj}
#Charisma:{10+HalfLevel+ChaAdj}
#DefaultAttackBonusVsAC:{Level+5}
#DefaultAttackBonusVsNAD:{Level+3}
#SingleTargetDamageBonus:{DamageBonus100}
#SingleTargetDamageDie:{DamageDie100}
#MultiTargetDamageDie:{DamageDie75}
#MultiTargetDamageBonus:{DamageBonus75}
#DamageBonus100:{1+CEILING(Level*2/3)}
#DamageDie100:{6+FLOOR(Level/3)}
#DamageDie75:{SingleTargetDamageDie-2-FLOOR((Level+3)/9)}
#DamageBonus75:{SingleTargetDamageBonus-1-FLOOR((Level+1)/9)}
#DamageDie125:{8+FLOOR(Level/3)}
#DamageBonus125:{1+CEILING(Level*2/3)+FLOOR((Level+1)/4)}
#DamageDie150:{9+FLOOR(Level/3)+FLOOR((Level+3)/6)}
#DamageBonus150:{2+CEILING(Level*2/3)+FLOOR((Level+1)/3)}
#MinionDamage:{4+FLOOR(Level/2)}
#-----CalculatedOrStaticStats-----
#HitPoints:{MaxHP}
#TempHP:0
#BloodiedHP:{FLOOR(MaxHP/2)}
#DeathFails:0
*#HP:{Hitpoints}/{MaxHP} + {TempHP}
*#AC/Fort/Ref/Will:{ArmorClass} / {Fortitude} / {Reflex} / {Will}
*#Type:Level {Level} {Role}
*#Speed:{Spd}
*#Initiative:{Init}
*#ActionPoints:{ActPts}
*#Str/Con/Dex:{Strength} / {Constitution} / {Dexterity}
*#Int/Wis/Cha:{Intelligence} / {Wisdom} / {Charisma}
#E1:1
#E2:1
#E3:1
#E4:1
#E5:1
#R1:1
#R2:1
#R3:1
#R4:1
#R5:1
#PowerCharged:1
---------------------------Skills-------------------------------------
#ArmorPenalty:0
#Acrobatics:{HalfLevel+DexMod-ArmorPenalty+5*AcrTrained}
#Arcana:{HalfLevel+IntMod+5*ArcTrained}
#Athletics:{HalfLevel+StrMod-ArmorPenalty+5*AthTrained}
#Bluff:{HalfLevel+ChaMod+5*BlfTrained}
#Diplomacy:{HalfLevel+ChaMod+5*DipTrained}
#Dungeoneering:{HalfLevel+WisMod+5*DunTrained}
#Endurance:{HalfLevel+ConMod-ArmorPenalty+5*EndTrained}
#Heal:{HalfLevel+WisMod+5*HeaTrained}
#History:{HalfLevel+IntMod+5*HisTrained}
#Insight:{HalfLevel+WisMod+5*InsTrained}
#Intimidate:{HalfLevel+ChaMod+5*IntTrained}
#Nature:{HalfLevel+WisMod+5*NatTrained}
#Perception:{HalfLevel+WisMod+5*PerTrained}
#Religion:{HalfLevel+IntMod+5*RelTrained}
#Stealth:{HalfLevel+DexMod-ArmorPenalty+5*StlTrained}
#Streetwise:{HalfLevel+ChaMod+5*StrTrained}
#Thievery:{HalfLevel+DexMod-ArmorPenalty+5*ThvTrained}
#AcrTrained:0
#ArcTrained:0
#AthTrained:0
#BlfTrained:0
#DipTrained:0
#DunTrained:0
#EndTrained:0
#HeaTrained:0
#HisTrained:0
#InsTrained:0
#IntTrained:0
#NatTrained:0
#PerTrained:0
#RelTrained:0
#StlTrained:0
#StrTrained:0
#ThvTrained:0
------------------AbilityMods-------------------------------
#StrMod:{FLOOR((Strength-10)/2)}
#ConMod:{FLOOR((Constitution-10)/2)}
#DexMod:{FLOOR((Dexterity-10)/2)}
#IntMod:{FLOOR((Intelligence-10)/2)}
#WisMod:{FLOOR((Wisdom-10)/2)}
#ChaMod:{FLOOR((Charisma-10)/2)}
#HalfLevel:{FLOOR(Level/2)}
-----------------Adjustments-------------------
#StrAdj:0
#ConAdj:0
#DexAdj:0
#IntAdj:0
#WisAdj:0
#ChaAdj:0
#ACAdj:0
#FortAdj:0
#RefAdj:0
#WillAdj:0
#InitAdj:0
---------------Other-----------------------------------------
Elevation:0
AttackState:0
DefenseState:0

3 thoughts on “MapTool geek-out update: Flexible monster creation

  1. I realize this article is 6 months old at this point, but I thought you might be interested in 2 alternatives I have come up with. I haven’t tested these yet, but I did geek-out myself a little and crunch some stats to see how the results would be.

    I see the value in fixing the number of dice. If you fix one thing it make it much easier to mathematically calculate the sides on the die and the bonus. One interesting alternative to fixing the number of dice is fixing the number sides of the die. If you always use a d4, you can express the other 2 items like so:

    sides = 4
    dice = 3 + floor((level-1)/5)
    bonus = level – 2*floor((level-1)/5)-floor((level-1)/5)
    [actually, if you want to be really clever, the bonus in any case is always: bonus = floor(level+8-(sides+sides*dice)/2). This will work for your formula above as well.]

    It sticks to the linear damage progression nicely throughout level increases, and you get a nicer bell curve for damage output. Interestingly, compared to the official rolls this method does give you a slightly higher spread for damage at low levels and a lower spread at higher levels. At epic tier the chances of rolling max damage are damn near impossible (1/65536), but the trade off is that rolling min damage is just as difficult.

    The second alternative is far easier than both of our methods. 🙂

    Build a table in maptool (I named mine Monster_Damage) numbered 1 to 30 with the values of the official chart as comma separated values sides,die,bonus (ie. for level 1 use 1,8,4; for level 30 use 4,8,20; etc).
    Then in your macro:

    [h:md = json.fromList(table(“Monster_Damage”,Level))]
    [h:dice = json.get(md,0)]
    [h:sides = json.get(md,1)]
    [h:bonus = json.get(md,2)]
    [r: roll(dice,sides)+bonus]

    If you still really want it as a property, (in your framework) set DamageDie100 to [json.get(json.fromList(table(“Monster_Damage”,Level)),1)] (no braces, this is a string). and then in the macro you can just use [h: sides = evalMacro(DamageDie100)].

    So now a question for you. Why did you come up with the different die rolls for higher/lower damage? Statistically the official multi-target damage is about 25% less than the normal damage. (They were probably aiming for 25%, but using normal dice makes this difficult.) Since you are using a fancy tool and not real dice, it’s actually much easier and just as reliable to just multiply your roll by the change.

    Multi-target: [r: (roll(dice,sides)+bonus)*0.75]
    Recharging: [r: (roll(dice,sides)+bonus)*1.25]
    Encounter: [r: (roll(dice,sides)+bonus)*1.5]

    I have a cool follow-up idea to this, but it is getting late. I’ll try to tackle it tomorrow, if I have time.

    • The article is a little old, but I still use this stuff pretty much every day (well, every day that I’m putting monsters into MapTool). So, it’s relevant!

      Your suggestion about fixing the number of sides and altering the number of dice can work, yes, but it’s not what I’m looking for. The more dice you roll, the less likely you are to get meaningful variation. If you roll 4d10 (expected value of 22) versus 9d4 (expected value of 22.5), you are MUCH less likely to get, say, a result below 18 or above 27 with the 9d4 than you are with the 4d10. More dice = lower standard deviation. I don’t like that. I like the bell curve from two dice of varying sides, but that’s just personal preference. Like I said in the post, I like that my high-level damage expressions involve rolling two dice instead of four; I prefer the flatter bell curve.

      As for the suggestion about using a table, you’re dead-on. I wrote this post before I delved into JSON stuff, and yep, it would be cleaner to implement it using a table or a property. But since it works for me now, I haven’t bothered to go back and re-do it with tables. Thank you for doing so!

      As for the high/low damage, you’re right that I could just multiply the result by 0.75 or 1.25 and round to the nearest whole number, but that takes it a little too far away from the reality of rolling dice for my taste. I know, I’m fine with rolling 2d13 but I draw the line at rolling 2d10 and multiplying by 1.25. Go figure. Again, just personal preference.

      Thanks for the thoughtful follow-up!

      • I agree with your assessment of using the d4; I don’t really like it either. The table is much more solid idea. i sat down and wrote the follow-up idea I had in a macro, so i thought i would share. It uses the Level, Role, and SubType properties you define in your monsters and the Monster_Damage table I mentioned above. It’s a campaign level macro, so it can be used on any monster selected.

        I’m still very new to macros and maptool, so take it easy on me. 🙂

        [h:inputStr = “[]”]
        [h:monsterLevel = Level]
        [h:monsterRole = Role]
        [h:monsterSubType = SubType]

        [h:inputStr = json.append(inputStr,”dummy|”+monsterLevel+”|Monster Level|LABEL”)]
        [h:inputStr = json.append(inputStr,”dummy|”+monsterRole+”|Monster Role|LABEL”)]
        [h:inputStr = json.append(inputStr,”dummy|”+monsterSubType+”|Monster SubType|LABEL”)]
        [h,if(monsterRole == “Artillery”),code: { [inputStr = json.append(inputStr,”areaAttack|0|Ranged/Area Attack Bonus|CHECK”)] }; { [areaAttack = 0] }]
        [h:inputStr = json.append(inputStr,”targetDefense|AC,Fortitude,Reflex,Will|Targeted Defense|LIST|VALUE=STRING”)]
        [h:inputStr = json.append(inputStr,”multiTarget|Single,Multiple|Number of Targets|RADIO|SELECT=0 ORIENT=H”)]
        [h,if(monsterSubType != “Minion”),code: { [inputStr = json.append(inputStr,”powerType|At-Will,Recharge,Encounter|Type of Power|LIST”)] }; { [powerType = 0] }]

        [h:hasInput= input(json.toList(inputStr,”##”))]
        [h:abort(hasInput)]

        [h:attackBonus = if(targetDefense == “AC”, 5, 3)]
        [h,if(areaAttack): attackBonus = attackBonus+2]
        [h:d20roll=d20]
        [h:attackRoll = d20roll + attackBonus]

        [h:roleDamageBonus = if(monsterRole == “Brute”,1,0)]
        [h,if(monsterSubType != “Minion”),code: {
        [h:md = table(“Monster_Damage”,monsterLevel)]
        [h:dice = listGet(md,0)]
        [h:sides = listGet(md,1)]
        [h:bonus = listGet(md,2)]
        [h:damageFactor = 1 + 0.25*(roleDamageBonus + powerType – multiTarget)]
        [h:damage = round((roll(dice,sides)+bonus)*damageFactor)]
        [h:maxDamage = round((dice*sides+bonus)*damageFactor)]
        };
        {
        [h:damageFactor = 1 + 0.25*(roleDamageBonus – multiTarget)]
        [h:damage = round((4 + floor(monsterLevel/2))*damageFactor)]
        [h:maxDamage = round((damage +roll(1,damage))*damageFactor)]
        }]

        [listGet(“At-Will,Recharage,Encounter”,powerType)] Attack
        Attack: [d20roll] + [attackBonus] = [attackRoll] versus [targetDefense]
        [if(d20roll==20), CODE:
        {
        –CRITICAL HIT–
        Hit: [maxDamage] damage.
        };
        {
        Hit: [damage] damage.
        }]

Leave a Reply