Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
158 changes: 158 additions & 0 deletions 5e_monsters/convert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import re
import json
from bs4 import BeautifulSoup

book_regex = re.compile('(?<=-).*')

def doc_shorthand(document_name):
if document_name == 'Tome of Beasts 2023':
return 'tob2023'
elif document_name == 'Tome of Beasts 2':
return 'tob2'
elif document_name == 'Tome of Beasts 3':
return 'tob3'
elif document_name == 'A5e Monstrous Menagerie':
return 'a5emm'
elif document_name == 'Creature Codex':
return 'cc'
elif document_name == 'Black Flag':
return 'bf'
else:
ValueError

def decode_ascii(str):
if not str.isascii():
return str.replace('\n', '\\n')
return bytes(str, 'utf-8').decode('unicode_escape').replace('\n', '\\n')


def rewriteA5eSpellcasting(str):
# Rewrite newlines for a5e content, kind of borked in the json
return re.sub(r'(?<=[0-9a-zA-Z])\n(?=\s[a-zA-Z\+])', ',', str).replace('\n', '\\n')


def parse_abilities(md_file, ability_json, name, isA5emm=False):
md_file.write(f'"{name}":\n')
abilities = json.loads(ability_json)
for ability in abilities:
if ability['name'] == 'Spellcasting' and isA5emm:
description = rewriteA5eSpellcasting(ability['desc'])
else:
description = decode_ascii(ability['description']) if 'description' in ability else decode_ascii(ability['desc'])
md_file.write(f'- "desc": "{description}"\n')
md_file.write(f' "name": "{ability['name']}"\n')

def parse_cr(cr):
if cr >= 1 or cr == 0:
return str(int(cr))
elif cr == 0.5:
return '1/2'
elif cr == 0.25:
return '1/4'
elif cr == 0.125:
return '1/8'
else:
ValueError

def add_link(slug, url):
with open(f'monsters_html/{slug}.html', "r", encoding='utf-8') as file:
contents = file.read()
soup = BeautifulSoup(contents, 'html.parser')
paragraph = soup.find_all('p')[-1]
paragraph.append(' - ')
link_tag = soup.new_tag(name='a', attrs=dict(href=url))
link_tag.string = 'Obsidian statblock'
paragraph.append(link_tag)
with open(f'monsters_html/{slug}.html', "w", encoding='utf-8') as file:
file.write(str(soup))

def generate_mdfile(monster):
if monster['document__title'] == 'Black Flag':
return
name_doc = f'{monster["name"]}-{doc_shorthand(monster['document__title'])}'
filename = f'obsidian_statblock/{re.sub(r' ','-', name_doc).lower()}.md'
with open(filename, 'w', encoding="utf-8") as md_file:
md_file.write('---\n')
md_file.write('obsidianUIMode: preview\n')
md_file.write('cssclasses: json5e-monster\n')
md_file.write('tags:\n')
md_file.write(f'- compendium/src/5e/{doc_shorthand(monster['document__title'])}\n')
md_file.write(f'- monster/cr/{monster["challenge_rating"]}\n')
md_file.write(f"- monster/size/{monster['size']}\n")
md_file.write(f"- monster/type/{monster['type']}\n")
md_file.write('statblock: inline\n')
md_file.write(f'aliases: ["{monster['slug']}", "{name_doc}"]\n')
md_file.write('---\n')
md_file.write(f"# {name_doc.replace('-',' ').title()}\n")
md_file.write(f'*Source: {monster['document__title']} Page {monster['page_no']}*\n')
md_file.write('\n')
md_file.write('```statblock\n')
md_file.write(f'"dice": false\n')
md_file.write(f'"name": "{name_doc.replace('-',' ').title()}"\n')
md_file.write(f'"size": "{monster['size']}"\n')
md_file.write(f'"type": "{monster['type']}"\n')
md_file.write(f'"ac": !!int "{monster['armor_class']}"\n')
md_file.write(f'"ac_class": "{monster['armor_desc']}"\n')
md_file.write(f'"hp": !!int "{monster['hit_points']}"\n')
md_file.write(f'"hit_dice": "{monster['hit_dice']}"\n')
md_file.write(f'"stats":\n')
# stats in order: str, dex, con, int, wis, cha
md_file.write(f'- !!int "{monster['strength']}"\n')
md_file.write(f'- !!int "{monster['dexterity']}"\n')
md_file.write(f'- !!int "{monster['constitution']}"\n')
md_file.write(f'- !!int "{monster['intelligence']}"\n')
md_file.write(f'- !!int "{monster['wisdom']}"\n')
md_file.write(f'- !!int "{monster['charisma']}"\n')

speed = json.loads(monster['speed_json'])
md_file.write(f'"speed": "{' '.join(list(map(lambda x: f'{x} {speed[x]} ft.', speed)))}"\n')

if monster["strength_save"] or monster["strength_save"] or monster["strength_save"] or monster["strength_save"] or monster["strength_save"] or monster["strength_save"]:
md_file.write('"saves":\n')
if monster['strength_save']:
md_file.write(f' "Strength": !!int "{monster["strength_save"]}"\n')
if monster['dexterity_save']:
md_file.write(f' "Dexterity": !!int "{monster["dexterity_save"]}"\n')
if monster['constitution_save']:
md_file.write(f' "Constitution": !!int "{monster["constitution_save"]}"\n')
if monster['intelligence_save']:
md_file.write(f' "Intelligence": !!int "{monster["intelligence_save"]}"\n')
if monster['wisdom_save']:
md_file.write(f' "Wisdom": !!int "{monster["wisdom_save"]}"\n')
if monster['charisma_save']:
md_file.write(f' "Charisma": !!int "{monster["charisma_save"]}"\n')
if monster["senses"]:
md_file.write(f'"senses": {monster["senses"]}\n')
if monster["skills_json"]:
md_file.write('"skillsaves":\n')
skills = json.loads(monster['skills_json'])
for skill in skills.keys():
md_file.write(f' "{skill}": !!int "{skills[skill]}"\n')
if monster["languages"]:
md_file.write(f'"languages": "{decode_ascii(monster['languages'])}"\n')
md_file.write(f'"cr": "{parse_cr(monster['cr'])}"\n')
if monster['damage_immunities']:
md_file.write(f'"damage_immunities": "{monster['damage_immunities']}"\n')
if monster['damage_resistances']:
md_file.write(f'"damage_resistances": "{monster['damage_resistances']}"\n')
if monster['special_abilities_json'] and monster['special_abilities_json'] != 'null':
parse_abilities(md_file, monster['special_abilities_json'], 'traits', monster['document__title'] == 'A5e Monstrous Menagerie')
if monster['actions_json'] and monster['actions_json'] != 'null':
parse_abilities(md_file, monster['actions_json'], 'actions')
if monster['reactions_json'] and monster['reactions_json'] != 'null':
parse_abilities(md_file, monster['reactions_json'], 'reactions')
if monster['bonus_actions_json'] and monster['bonus_actions_json'] != 'null':
parse_abilities(md_file, monster['bonus_actions_json'], 'bonus_actions')
if monster['legendary_actions_json'] and monster['legendary_actions_json'] != 'null':
parse_abilities(md_file, monster['legendary_actions_json'], 'legendary_actions')
md_file.write('source:\n')
md_file.write(f'- [{monster["document__title"]}]({monster["document__url"]})\n')
md_file.write('```\n')
# add_link(monster['slug'], f'../{filename}')

with open("monsters.json", 'r', encoding='utf-8',
errors='ignore') as f:
monsters = json.load(f)
print(f'Starting Monster statblock generation')
for monster in monsters:
generate_mdfile(monster)
1 change: 1 addition & 0 deletions 5e_monsters/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ <h1>5e Artisanal Monster Database</h1>
<li>Link to monster pages or copy and paste HTML or markdown versions into your notes.</li>
<li>Includes <a href="./licensing.html">open-licensed</a> monsters from the <a href="https://enpublishingrpg.com/products/level-up-monstrous-menagerie-a5e">A5E Monstrous Menagerie</a>, <a href="https://koboldpress.com/kpstore/product/tome-of-beasts-1-2023-edition/">Tome of Beasts 1</a>, <a href="https://koboldpress.com/tome-of-beasts-2/">2</a>, <a href="https://koboldpress.com/kpstore/product/tome-of-beasts-3-for-5th-edition/">3</a>, the <a href="https://koboldpress.com/kpstore/product/creature-codex-for-5th-edition-dnd/">Creature Codex</a>, and the <a href="https://koboldpress.com/black-flag-roleplaying/">Kobold Press Black Flag SRD</a>.</li>
<li><a href="./5e_monsters.zip">Download the database</a> and run the local copy in a browser or host on your own website.</li>
<li>Using the Obsidian statblock markdown files requires <a href="https://obsidian.md/">Obsidian</a> and the <a href="https://github.com/javalent/fantasy-statblocks">Fantasy Statblock plugin</a></li>
</ul>
<table id="monsterTable" class="display">
<thead>
Expand Down
9 changes: 5 additions & 4 deletions 5e_monsters/monsters_html/a-mi-kuk.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

<!DOCTYPE html>

<head>
<meta name="viewport" content="user-scalable=yes, width=device-width">
<meta charset="UTF-8">
<meta content="user-scalable=yes, width=device-width" name="viewport"/>
<meta charset="utf-8"/>
<style>
body {
max-width: 700px;
Expand Down Expand Up @@ -71,6 +72,6 @@ <h3>Actions</h3>
<li><strong>Grasping Claw:</strong> Melee Weapon Attack: +8 to hit, reach 10 ft., one target. Hit: 18 (3d8 + 5) bludgeoning damage, and the target is grappled (escape DC 16). The a-mi-kuk has two grasping claws, each of which can grapple only one target at a time.</li>
<li><strong>Strangle:</strong> The a-mi-kuk strangles one creature grappled by it. The target must make a DC 16 Strength saving throw. On a failure, the target takes 27 (6d8) bludgeoning damage, can’t breathe, speak, or cast spells, and begins suffocating. On a success, the target takes half the bludgeoning damage and is no longer grappled. Until this strangling grapple ends (escape DC 16), the target takes 13 (3d8) bludgeoning damage at the start of each of its turns. The a-mi-kuk can strangle up to two Medium or smaller targets or one Large target at a time.</li>
</ul>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/a-mi-kuk.md">Markdown Format</a></p>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/a-mi-kuk.md">Markdown Format</a> - <a href="../obsidian_statblock/a-mi-kuk-tob2.md">Obsidian statblock</a></p>
</body>
</html>

9 changes: 5 additions & 4 deletions 5e_monsters/monsters_html/aalpamac.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

<!DOCTYPE html>

<head>
<meta name="viewport" content="user-scalable=yes, width=device-width">
<meta charset="UTF-8">
<meta content="user-scalable=yes, width=device-width" name="viewport"/>
<meta charset="utf-8"/>
<style>
body {
max-width: 700px;
Expand Down Expand Up @@ -68,6 +69,6 @@ <h3>Actions</h3>
<li><strong>Bite:</strong> Melee Weapon Attack: +8 to hit, reach 5 ft., one target. Hit: 16 (2d10 + 5) piercing damage.</li>
<li><strong>Claws:</strong> Melee Weapon Attack: +8 to hit, reach 5 ft., one target. Hit: 12 (2d6 + 5) slashing damage.</li>
</ul>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/aalpamac.md">Markdown Format</a></p>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/aalpamac.md">Markdown Format</a> - <a href="../obsidian_statblock/aalpamac-tob2.md">Obsidian statblock</a></p>
</body>
</html>

9 changes: 5 additions & 4 deletions 5e_monsters/monsters_html/aatxe.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

<!DOCTYPE html>

<head>
<meta name="viewport" content="user-scalable=yes, width=device-width">
<meta charset="UTF-8">
<meta content="user-scalable=yes, width=device-width" name="viewport"/>
<meta charset="utf-8"/>
<style>
body {
max-width: 700px;
Expand Down Expand Up @@ -75,6 +76,6 @@ <h3>Legendary Actions</h3>
<li><strong>Gore (Costs 2 Actions):</strong> The aatxe makes one gore attack.</li>
<li><strong>Bulwark (Costs 3 Actions):</strong> The aatxe flares crimson with celestial power, protecting those nearby. The next attack that would hit an ally within 5 feet of the aatxe hits the aatxe instead.</li>
</ul>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/aatxe.md">Markdown Format</a></p>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/aatxe.md">Markdown Format</a> - <a href="../obsidian_statblock/aatxe-cc.md">Obsidian statblock</a></p>
</body>
</html>

9 changes: 5 additions & 4 deletions 5e_monsters/monsters_html/abaasy.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

<!DOCTYPE html>

<head>
<meta name="viewport" content="user-scalable=yes, width=device-width">
<meta charset="UTF-8">
<meta content="user-scalable=yes, width=device-width" name="viewport"/>
<meta charset="utf-8"/>
<style>
body {
max-width: 700px;
Expand Down Expand Up @@ -70,6 +71,6 @@ <h3>Actions</h3>
<li><strong>Spear:</strong> Melee or Ranged Weapon Attack: +8 to hit 15 ft. or range 20/60' one target 15 (3d6+5) piercing damage or 18 (3d8+5) piercing damage if used with two hands to make a melee attack.</li>
<li><strong>Eyebeam (Recharge 5–6):</strong> Fires a beam of oscillating energy from its eye in a 90' line that is 5 ft. wide. Each creature in the line: 27 (5d10) radiant (DC 16 Dex half).</li>
</ul>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/abaasy.md">Markdown Format</a></p>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/abaasy.md">Markdown Format</a> - <a href="../obsidian_statblock/abaasy-tob3.md">Obsidian statblock</a></p>
</body>
</html>

9 changes: 5 additions & 4 deletions 5e_monsters/monsters_html/abbanith-giant.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

<!DOCTYPE html>

<head>
<meta name="viewport" content="user-scalable=yes, width=device-width">
<meta charset="UTF-8">
<meta content="user-scalable=yes, width=device-width" name="viewport"/>
<meta charset="utf-8"/>
<style>
body {
max-width: 700px;
Expand Down Expand Up @@ -70,6 +71,6 @@ <h3>Reactions</h3>
<ul>
<li><strong>Earth Counter (Recharge 6):</strong> When a creature the abbanith can see within 30 feet of it casts a spell, the abbanith counters it. This reaction works like the counterspell spell, except the abbanith can only counter spells that directly affect or create earth or stone, such as stone shape, wall of stone, or move earth, and it doesn’t need to make a spellcasting ability check, regardless of the spell’s level.</li>
</ul>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/abbanith-giant.md">Markdown Format</a></p>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/abbanith-giant.md">Markdown Format</a> - <a href="../obsidian_statblock/abbanith-giant-tob2.md">Obsidian statblock</a></p>
</body>
</html>

9 changes: 5 additions & 4 deletions 5e_monsters/monsters_html/aboleth-a5e.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

<!DOCTYPE html>

<head>
<meta name="viewport" content="user-scalable=yes, width=device-width">
<meta charset="UTF-8">
<meta content="user-scalable=yes, width=device-width" name="viewport"/>
<meta charset="utf-8"/>
<style>
body {
max-width: 700px;
Expand Down Expand Up @@ -75,6 +76,6 @@ <h3>Legendary Actions</h3>
<li><strong>Baleful Charm (Costs 2 Actions):</strong> The aboleth targets one creature within 60 feet that has contracted Sea Change. The target makes a DC 16 Wisdom saving throw. On a failure, it is magically charmed by the aboleth until the aboleth dies. The target can repeat this saving throw every 24 hours and when it takes damage from the aboleth or the aboleths allies. While charmed in this way, the target can communicate telepathically with the aboleth over any distance and it follows the aboleths orders.</li>
<li><strong>Soul Drain (Costs 2 Actions):</strong> One creature charmed by the aboleth takes 22 (4d10) psychic damage, and the aboleth regains hit points equal to the damage dealt.</li>
</ul>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/aboleth-a5e.md">Markdown Format</a></p>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/aboleth-a5e.md">Markdown Format</a> - <a href="../obsidian_statblock/aboleth-a5emm.md">Obsidian statblock</a></p>
</body>
</html>

9 changes: 5 additions & 4 deletions 5e_monsters/monsters_html/aboleth-thrall-a5e.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

<!DOCTYPE html>

<head>
<meta name="viewport" content="user-scalable=yes, width=device-width">
<meta charset="UTF-8">
<meta content="user-scalable=yes, width=device-width" name="viewport"/>
<meta charset="utf-8"/>
<style>
body {
max-width: 700px;
Expand Down Expand Up @@ -67,6 +68,6 @@ <h3>Reactions</h3>
<ul>
<li><strong>Self-Sacrifice:</strong> When a creature within 5 feet of the thrall that the thrall can see hits an aboleth with an attack, the thrall can make itself the target of the attack instead.</li>
</ul>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/aboleth-thrall-a5e.md">Markdown Format</a></p>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/aboleth-thrall-a5e.md">Markdown Format</a> - <a href="../obsidian_statblock/aboleth-thrall-a5emm.md">Obsidian statblock</a></p>
</body>
</html>

9 changes: 5 additions & 4 deletions 5e_monsters/monsters_html/abominable-snowman-a5e.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

<!DOCTYPE html>

<head>
<meta name="viewport" content="user-scalable=yes, width=device-width">
<meta charset="UTF-8">
<meta content="user-scalable=yes, width=device-width" name="viewport"/>
<meta charset="utf-8"/>
<style>
body {
max-width: 700px;
Expand Down Expand Up @@ -68,6 +69,6 @@ <h3>Actions</h3>
<li><strong>Claw:</strong> Melee Weapon Attack: +6 to hit reach 10 ft. one target. Hit: 9 (2d4 + 4) slashing damage.</li>
<li><strong>Chilling Gaze (Gaze):</strong> One creature within 30 feet that is not immune to cold damage makes a DC 13 Constitution saving throw. On a failure the creature takes 10 (3d6) cold damage and is paralyzed for 1 minute. It repeats the saving throw at the end of each of its turns ending the effect on a success. If a creatures saving throw is successful or the effect ends for it it is immune to any Chilling Gaze for 24 hours.</li>
</ul>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/abominable-snowman-a5e.md">Markdown Format</a></p>
<p><a href="https://slyflourish.com">SlyFlourish.com</a> – <a href="../licensing.html">Licensing</a> – <a href="../monsters_md/abominable-snowman-a5e.md">Markdown Format</a> - <a href="../obsidian_statblock/abominable-snowman-a5emm.md">Obsidian statblock</a></p>
</body>
</html>

Loading