Skip to content

Instantly share code, notes, and snippets.

@ThomasJunk
Last active October 20, 2022 17:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ThomasJunk/0c257b2992b1f5cd8868020e32c35120 to your computer and use it in GitHub Desktop.
Save ThomasJunk/0c257b2992b1f5cd8868020e32c35120 to your computer and use it in GitHub Desktop.
Converts statblocks
#!/bin/env python3
import json
# MONSTERS = "test_conversion.json"
MONSTERS = "monstersCompendium.json"
"""
Example Statblock
```statblock
image: [[Ancient Black Dragon.jpg]]
name: Ancient Black Dragon
size: Gargantuan
type: dragon
subtype:
alignment: chaotic evil
ac: 22
hp: 367
hit_dice: 21d20
speed: 40 ft., fly 80 ft., swim 40 ft.
stats: [27, 14, 25, 16, 15, 19]
saves:
- dexterity: 9
- constitution: 14
- wisdom: 9
- charisma: 11
skillsaves:
- perception: 16
- stealth: 9
senses: blindsight 60 ft., darkvision 120 ft., passive Perception 26
languages: Common, Draconic
damage_resistances: bludgeoning, piercing, and slashing from nonmagical attacks
damage_immunities: fire, poison
condition_immunities: charmed, frightened, grappled, paralyzed, petrified, poisoned, prone, restrained
cr: 21
traits:
- name: Amphibious
desc: The dragon can breathe air and water
- name: Legendary Resistance (3/Day)
desc: If the dragon fails a saving throw, it can choose to succeed instead.
actions:
- name: Multiattack
desc: "The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws."
- name: Bite
desc: "Melee Weapon Attack: +15 to hit, reach 15 ft., one target. Hit: 19 (2d10 + 8) piercing damage plus 9 (2d8) acid damage."
- name: Claw
desc: "Melee Weapon Attack: +15 to hit, reach 10 ft., one target. Hit: 15 (2d6 + 8) slashing damage."
- name: Tail
desc: "Melee Weapon Attack: +15 to hit, reach 20 ft ., one target. Hit: 17 (2d8 + 8) bludgeoning damage."
- name: Frightful Presence
desc: "Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 19 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours."
- name: Acid Breath (Recharge 5-6)
desc: The dragon exhales acid in a 90-foot line that is 10 feet wide. Each creature in that line must make a DC 22 Dexterity saving throw, taking 67 (15d8) acid damage on a failed save, or half as much damage on a successful one.
reactions:
- name: Amphibious
desc: The dragon can breathe air and water.
- name: Legendary Resistance (3/Day)
desc: If the dragon fails a saving throw, it can choose to succeed instead.
legendary_actions:
- name: Detect
desc: The dragon makes a Wisdom (Perception) check.
- name: Tail Attack
desc: The dragon makes a tail attack.
- name: Wing Attack (Costs 2 Actions),
desc: The dragon beats its wings. Each creature within 15 ft. of the dragon must succeed on a DC 23 Dexterity saving throw or take 15 (2d6 + 8) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed.
spells:
- The archmage is an 18th-level spellcaster. Its spellcasting ability is Intelligence (spell save DC 17, +9 to hit with spell attacks). The archmage can cast disguise self and invisibility at will and has the following wizard spells prepared
- Cantrips (at will): fire bolt, light, mage hand, prestidigitation, shocking grasp
- 1st level (4 slots): detect magic, identify, mage armor*, magic missile
- 2nd level (3 slots): detect thoughts, mirror image, misty step
- 3rd level (3 slots): counterspell, fly, lightning bolt
- 4th level (3 slots): banishment, fire shield, stoneskin*
- 5th level (3 slots): cone of cold, scrying, wall of force
- 6th level (1 slot): globe of invulnerability
- 7th level (1 slot): teleport
- 8th level (1 slot): mind blank*
- 9th level (1 slot): time stop
- "* The archmage casts these spells on itself before combat."
```
"""
def generate_statblock(monster):
statblock = []
statblock.append("```statblock")
# statblock.append("image: [[]]")
statblock.append(f"name: {monster.get('Name')}")
size_type, *alignment = monster.get("Type").rpartition(",")
size, *type_ = size_type.split(" ")
statblock.append(f"size: {size}")
statblock.append(f"type: {' '.join(type_)}")
statblock.append(f"alignment:{''.join(alignment).replace(',','')}")
ac = "{} {}".format(monster.get("AC")["Value"], monster.get("AC")["Notes"])
statblock.append(f"ac: {ac}")
statblock.append(f"hp: {monster.get('HP')['Value']}")
statblock.append(f"hit_dice: {monster.get('HP')['Notes']}")
statblock.append(f"speed: {' '.join(monster.get('Speed'))}")
stats = ", ".join(
[
monster.get("Abilities")["Str"],
monster.get("Abilities")["Dex"],
monster.get("Abilities")["Con"],
monster.get("Abilities")["Int"],
monster.get("Abilities")["Wis"],
monster.get("Abilities")["Cha"],
]
)
statblock.append(f"stats: [{stats}]")
saves = monster.get("Saves")
if saves:
statblock.append("saves:")
for save in saves:
statblock.append(f"- {save['Name']}: {save['Modifier']}")
skills = monster.get("Skills")
if skills:
statblock.append("skill_saves:")
for skill in skills:
statblock.append(f"- {skill['Name']}: {skill['Modifier']}")
if monster.get("Senses"):
statblock.append(f"senses: {', '.join(monster.get('Senses'))}")
statblock.append(f"languages: {', '.join(monster.get('Languages'))}")
damage_resistances = monster.get("DamageResistances")
if damage_resistances:
statblock.append(
'damage_resistances: "{}"'.format(", ".join(damage_resistances))
)
damage_vulerabilities = monster.get("DamageVulerabilities")
if damage_vulerabilities:
statblock.append(
'damage_vulnerabilities: "{}"'.format(
", ".join(damage_vulerabilities)
)
)
damage_immunities = monster.get("DamageImmunities")
if damage_immunities:
statblock.append(
'damage_immunities: "{}"'.format(", ".join(damage_immunities))
)
condition_immunities = monster.get("ConditionImmunities")
if condition_immunities:
statblock.append(
'condition_immunities: "{}"'.format(
", ".join(condition_immunities)
)
)
statblock.append(f"cr: {monster.get('Challenge')}")
traits = monster.get("Traits")
if traits:
statblock.append("traits:")
for trait in traits:
statblock.append('- name: "{}"'.format(trait["Name"]))
statblock.append(
' desc: "{} {}"'.format(
trait.get("Content")
.replace("\t", "")
.replace("\n", "")
.replace('"', "'"),
trait.get("Usage")
.replace("\t", "")
.replace("\n", "")
.replace('"', "'"),
)
)
actions = monster.get("Actions")
if actions:
statblock.append("actions:")
for action in actions:
statblock.append('- name: "{}"'.format(action["Name"]))
statblock.append(
' desc: "{} {}"'.format(
action.get("Content")
.replace("\t", "")
.replace("\n", "")
.replace('"', "'"),
action.get("Usage")
.replace("\t", "")
.replace("\n", "")
.replace('"', "'"),
)
)
reactions = monster.get("Reactions")
if reactions:
statblock.append("reactions:")
for reaction in reactions:
statblock.append('- name: "{}"'.format(action["Name"]))
statblock.append(
' desc: "{} {}"'.format(
reaction.get("Content")
.replace("\t", "")
.replace("\n", "")
.replace('"', "'"),
reaction.get("Usage")
.replace("\t", "")
.replace("\n", "")
.replace('"', "'"),
)
)
legendary_actions = monster.get("Actions")
if legendary_actions:
statblock.append("legendary_actions:")
for legendary_action in legendary_actions:
statblock.append('- name: "{}"'.format(action["Name"]))
statblock.append(
' desc: "{} {}"'.format(
legendary_action.get("Content")
.replace("\t", "")
.replace("\n", "")
.replace('"', "'"),
legendary_action.get("Usage")
.replace("\t", "")
.replace("\n", "")
.replace('"', "'"),
)
)
statblock.append("```")
return statblock
def main():
with open(MONSTERS) as monster_file:
monsters = json.load(monster_file)
for monster in monsters:
file_name = f"{monster.get('Name')}.md"
file_name = file_name.replace("/", " or ")
file_name = file_name.replace(" ", "_")
statblock = generate_statblock(monster)
with open(file_name, "w+") as out:
for line in statblock:
out.write(line)
out.write("\n")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment