Last active
October 20, 2022 17:46
-
-
Save ThomasJunk/0c257b2992b1f5cd8868020e32c35120 to your computer and use it in GitHub Desktop.
Converts statblocks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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