├── requirements.txt ├── icon.png ├── Examples ├── Preview.png └── ExampleReport.html ├── README.md ├── mainWindow.ui ├── mainWindow.py ├── wideTemplate.html ├── reportTemplate.html └── D&DPrinter.py /requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2 2 | unidecode 3 | pyyaml 4 | pyside -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/5eStatblockPrinter/master/icon.png -------------------------------------------------------------------------------- /Examples/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blakebjorn/5eStatblockPrinter/master/Examples/Preview.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 5eStatblockPrinter 2 | Print statblocks from the basic rules in pretty formats. 3 | Python 2 compatible only, due to move from QtWebKit to QtWebEngine in PyQt5 and questionable QPrinter functionality at the time being. 4 | 5 | # Installation 6 | Download the DM rules (web format) from the WOTC site and save the html page as "DM_Rules.html" in the program directory 7 | > python D&DPrinter.py 8 | 9 | User will be prompted to install requirements.txt on startup if they aren't present. 10 | 11 | # Usage 12 | DM rulebook will be crawled on application startup and monster stat blocks will be extracted. All monsters will be listed in the left pane. Right/double click on a monster to add it to the current active list. Active list can be previewed in the center pane, or saved to PDF for printing. 13 | 14 | ![alt text](Examples/Preview.png "Preview") 15 | -------------------------------------------------------------------------------- /mainWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 800 10 | 600 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | Qt::Horizontal 22 | 23 | 24 | 25 | QFrame::StyledPanel 26 | 27 | 28 | QFrame::Raised 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 0 41 | 0 42 | 43 | 44 | 45 | Filter: 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | false 55 | 56 | 57 | 58 | 1 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | QFrame::StyledPanel 68 | 69 | 70 | QFrame::Raised 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 16777215 80 | 225 81 | 82 | 83 | 84 | false 85 | 86 | 87 | 88 | 1 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Move Up 99 | 100 | 101 | 102 | 103 | 104 | 105 | true 106 | 107 | 108 | Move Down 109 | 110 | 111 | 112 | 113 | 114 | 115 | Preview 116 | 117 | 118 | 119 | 120 | 121 | 122 | Save 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 0 140 | 0 141 | 800 142 | 20 143 | 144 | 145 | 146 | 147 | 148 | 149 | Create Report 150 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /mainWindow.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'mainWindow.ui' 4 | # 5 | # Created: Sun Mar 5 13:51:14 2017 6 | # by: pyside-uic 0.2.15 running on PySide 1.2.2 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from PySide import QtCore, QtGui 11 | 12 | class Ui_MainWindow(object): 13 | def setupUi(self, MainWindow): 14 | MainWindow.setObjectName("MainWindow") 15 | MainWindow.resize(800, 600) 16 | self.centralwidget = QtGui.QWidget(MainWindow) 17 | self.centralwidget.setObjectName("centralwidget") 18 | self.verticalLayout = QtGui.QVBoxLayout(self.centralwidget) 19 | self.verticalLayout.setObjectName("verticalLayout") 20 | self.splitter = QtGui.QSplitter(self.centralwidget) 21 | self.splitter.setOrientation(QtCore.Qt.Horizontal) 22 | self.splitter.setObjectName("splitter") 23 | self.frame = QtGui.QFrame(self.splitter) 24 | self.frame.setFrameShape(QtGui.QFrame.StyledPanel) 25 | self.frame.setFrameShadow(QtGui.QFrame.Raised) 26 | self.frame.setObjectName("frame") 27 | self.verticalLayout_2 = QtGui.QVBoxLayout(self.frame) 28 | self.verticalLayout_2.setObjectName("verticalLayout_2") 29 | self.gridLayout = QtGui.QGridLayout() 30 | self.gridLayout.setObjectName("gridLayout") 31 | self.filterRulesLineEdit = QtGui.QLineEdit(self.frame) 32 | self.filterRulesLineEdit.setObjectName("filterRulesLineEdit") 33 | self.gridLayout.addWidget(self.filterRulesLineEdit, 0, 1, 1, 1) 34 | self.label = QtGui.QLabel(self.frame) 35 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) 36 | sizePolicy.setHorizontalStretch(0) 37 | sizePolicy.setVerticalStretch(0) 38 | sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) 39 | self.label.setSizePolicy(sizePolicy) 40 | self.label.setObjectName("label") 41 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 42 | self.verticalLayout_2.addLayout(self.gridLayout) 43 | self.ruleBookTreeWidget = QtGui.QTreeWidget(self.frame) 44 | self.ruleBookTreeWidget.setObjectName("ruleBookTreeWidget") 45 | self.ruleBookTreeWidget.headerItem().setText(0, "1") 46 | self.ruleBookTreeWidget.header().setVisible(False) 47 | self.verticalLayout_2.addWidget(self.ruleBookTreeWidget) 48 | self.frame_2 = QtGui.QFrame(self.splitter) 49 | self.frame_2.setFrameShape(QtGui.QFrame.StyledPanel) 50 | self.frame_2.setFrameShadow(QtGui.QFrame.Raised) 51 | self.frame_2.setObjectName("frame_2") 52 | self.rightFrameLayout = QtGui.QVBoxLayout(self.frame_2) 53 | self.rightFrameLayout.setObjectName("rightFrameLayout") 54 | self.horizontalLayout = QtGui.QHBoxLayout() 55 | self.horizontalLayout.setObjectName("horizontalLayout") 56 | self.selectedItemsTreeWidget = QtGui.QTreeWidget(self.frame_2) 57 | self.selectedItemsTreeWidget.setMaximumSize(QtCore.QSize(16777215, 225)) 58 | self.selectedItemsTreeWidget.setObjectName("selectedItemsTreeWidget") 59 | self.selectedItemsTreeWidget.headerItem().setText(0, "1") 60 | self.selectedItemsTreeWidget.header().setVisible(False) 61 | self.horizontalLayout.addWidget(self.selectedItemsTreeWidget) 62 | self.verticalLayout_3 = QtGui.QVBoxLayout() 63 | self.verticalLayout_3.setObjectName("verticalLayout_3") 64 | self.moveUpInTreeButton = QtGui.QPushButton(self.frame_2) 65 | self.moveUpInTreeButton.setObjectName("moveUpInTreeButton") 66 | self.verticalLayout_3.addWidget(self.moveUpInTreeButton) 67 | self.moveDownInTreeButton = QtGui.QPushButton(self.frame_2) 68 | self.moveDownInTreeButton.setEnabled(True) 69 | self.moveDownInTreeButton.setObjectName("moveDownInTreeButton") 70 | self.verticalLayout_3.addWidget(self.moveDownInTreeButton) 71 | self.previewPushButton = QtGui.QPushButton(self.frame_2) 72 | self.previewPushButton.setObjectName("previewPushButton") 73 | self.verticalLayout_3.addWidget(self.previewPushButton) 74 | self.savePushButton = QtGui.QPushButton(self.frame_2) 75 | self.savePushButton.setObjectName("savePushButton") 76 | self.verticalLayout_3.addWidget(self.savePushButton) 77 | self.horizontalLayout.addLayout(self.verticalLayout_3) 78 | self.rightFrameLayout.addLayout(self.horizontalLayout) 79 | self.verticalLayout.addWidget(self.splitter) 80 | MainWindow.setCentralWidget(self.centralwidget) 81 | self.menubar = QtGui.QMenuBar(MainWindow) 82 | self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 20)) 83 | self.menubar.setObjectName("menubar") 84 | MainWindow.setMenuBar(self.menubar) 85 | self.statusbar = QtGui.QStatusBar(MainWindow) 86 | self.statusbar.setObjectName("statusbar") 87 | MainWindow.setStatusBar(self.statusbar) 88 | self.actionCreate_Report = QtGui.QAction(MainWindow) 89 | self.actionCreate_Report.setObjectName("actionCreate_Report") 90 | 91 | self.retranslateUi(MainWindow) 92 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 93 | 94 | def retranslateUi(self, MainWindow): 95 | MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) 96 | self.label.setText(QtGui.QApplication.translate("MainWindow", "Filter:", None, QtGui.QApplication.UnicodeUTF8)) 97 | self.moveUpInTreeButton.setText(QtGui.QApplication.translate("MainWindow", "Move Up", None, QtGui.QApplication.UnicodeUTF8)) 98 | self.moveDownInTreeButton.setText(QtGui.QApplication.translate("MainWindow", "Move Down", None, QtGui.QApplication.UnicodeUTF8)) 99 | self.previewPushButton.setText(QtGui.QApplication.translate("MainWindow", "Preview", None, QtGui.QApplication.UnicodeUTF8)) 100 | self.savePushButton.setText(QtGui.QApplication.translate("MainWindow", "Save", None, QtGui.QApplication.UnicodeUTF8)) 101 | self.actionCreate_Report.setText(QtGui.QApplication.translate("MainWindow", "Create Report", None, QtGui.QApplication.UnicodeUTF8)) 102 | 103 | -------------------------------------------------------------------------------- /wideTemplate.html: -------------------------------------------------------------------------------- 1 | 210 | 211 |
212 |
213 |
214 |
215 |

{{monsterName}}

216 |

{{monsterType}}

217 |
218 | 219 | 220 | 221 |
222 |
223 |

Armor Class

224 |

{{armorClass}}

225 |
226 |
227 |

Hit Points

228 |

{{hitPoints}}

229 |
230 |
231 |

Speed

232 |

{{speed}}

233 |
234 | 235 | 236 | 237 |
238 |
239 |

STR

240 |

{{STR}}

241 |
242 |
243 |

DEX

244 |

{{DEX}}

245 |
246 |
247 |

CON

248 |

{{CON}}

249 |
250 |
251 |

INT

252 |

{{INT}}

253 |
254 |
255 |

WIS

256 |

{{WIS}}

257 |
258 |
259 |

CHA

260 |

{{CHA}}

261 |
262 |
263 | 264 | 265 | 266 | 267 | {% if damageImmunities %} 268 |
269 |

Damage Immunities

270 |

{{damageImmunities}}

271 |
272 | {% endif %} 273 | 274 | {% if conditionImmunities %} 275 |
276 |

Condition Immunities

277 |

{{conditionImmunities}}

278 |
279 | {% endif %} 280 | 281 | {% if senses %} 282 |
283 |

Senses

284 |

{{senses}}

285 |
286 | {% endif %} 287 | 288 | {% if languages %} 289 |
290 |

Languages

291 |

{{languages}}

292 |
293 | {% endif %} 294 | 295 |
296 |

Challenge

297 |

{{challenge}}

298 |
299 |
300 | 301 | 302 | 303 | 304 |
305 |
306 | {% if specialTraits %} 307 | {% for trait in specialTraits %} 308 |
309 |

{{trait}}

310 |
311 | {% endfor %} 312 | {% endif %} 313 | {% if actions %} 314 |
315 |

Actions

316 | {% for action in actions %} 317 |
318 |

{{action}}

319 |
320 | {% endfor %} 321 |
322 | {% endif %} 323 | {% if legendaryActions %} 324 |
325 |

Legendary Actions

326 | {% for action in legendaryActions %} 327 |
328 |

{{action}}

329 |
330 | {% endfor %} 331 |
332 | {% endif %} 333 |
334 |
335 |
336 | 337 | -------------------------------------------------------------------------------- /reportTemplate.html: -------------------------------------------------------------------------------- 1 | 203 | 204 | {% for monsterBlock in outputList %} 205 |
206 |
207 |
208 |
209 |

{{monsterBlock['monsterName']}}

210 |

{{monsterBlock['monsterType']}}

211 |
212 | 213 | 214 | 215 |
216 |
217 |

Armor Class

218 |

{{monsterBlock['armorClass']}}

219 |
220 |
221 |

Hit Points

222 |

{{monsterBlock['hitPoints']}}

223 |
224 |
225 |

Speed

226 |

{{monsterBlock['speed']}}

227 |
228 | 229 | 230 | 231 |
232 |
233 |

STR

234 |

{{monsterBlock['STR']}}

235 |
236 |
237 |

DEX

238 |

{{monsterBlock['DEX']}}

239 |
240 |
241 |

CON

242 |

{{monsterBlock['CON']}}

243 |
244 |
245 |

INT

246 |

{{monsterBlock['INT']}}

247 |
248 |
249 |

WIS

250 |

{{monsterBlock['WIS']}}

251 |
252 |
253 |

CHA

254 |

{{monsterBlock['CHA']}}

255 |
256 |
257 | 258 | 259 | 260 | 261 | {% if monsterBlock['damageImmunities'] %} 262 |
263 |

Damage Immunities

264 |

{{monsterBlock['damageImmunities']}}

265 |
266 | {% endif %} 267 | 268 | {% if monsterBlock['conditionImmunities'] %} 269 |
270 |

Condition Immunities

271 |

{{monsterBlock['conditionImmunities']}}

272 |
273 | {% endif %} 274 | 275 | {% if monsterBlock['senses'] %} 276 |
277 |

Senses

278 |

{{monsterBlock['senses']}}

279 |
280 | {% endif %} 281 | 282 | {% if monsterBlock['languages'] %} 283 |
284 |

Languages

285 |

{{monsterBlock['languages']}}

286 |
287 | {% endif %} 288 | 289 |
290 |

Challenge

291 |

{{monsterBlock['challenge']}}

292 |
293 |
294 | 295 | 296 | 297 | 298 |
299 |
300 | {% if monsterBlock['specialTraits'] %} 301 | {% for trait in monsterBlock['specialTraits'] %} 302 |
303 |

{{trait}}

304 |
305 | {% endfor %} 306 | {% endif %} 307 | {% if monsterBlock['actions'] %} 308 |
309 |

Actions

310 | {% for action in monsterBlock['actions'] %} 311 |
312 |

{{action}}

313 |
314 | {% endfor %} 315 |
316 | {% endif %} 317 | {% if monsterBlock['legendaryActions'] %} 318 |
319 |

Legendary Actions

320 | {% for action in monsterBlock['legendaryActions'] %} 321 |
322 |

{{action}}

323 |
324 | {% endfor %} 325 |
326 | {% endif %} 327 |
328 |
329 |
330 | {% endfor %} 331 | -------------------------------------------------------------------------------- /D&DPrinter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | reload(sys) 3 | sys.setdefaultencoding("UTF8") 4 | import re 5 | import os 6 | import time 7 | import hashlib 8 | import webbrowser 9 | from functools import partial 10 | 11 | try: 12 | import yaml 13 | import jinja2 14 | from unidecode import unidecode 15 | from PySide import QtGui, QtCore, QtWebKit 16 | except ImportError: 17 | import pip 18 | proc = raw_input("Modules need to be installed to continue. Download and Install:\n"+"\n".join(("PyYaml","Jinja2","Unidecode","PySide","\n(Y/N)?"))) 19 | if proc.upper() in ("YES","Y"): 20 | pip.main(['install','-r','requirements.txt']) 21 | import yaml 22 | import jinja2 23 | from unidecode import unidecode 24 | from PySide import QtGui, QtCore, QtWebKit 25 | else: 26 | print "Closing in 5 Seconds." 27 | time.sleep(5) 28 | sys.exit() 29 | 30 | 31 | def compile_ui_files(): 32 | """ 33 | Stores a list of all the .ui files in the current directory. 34 | If the UI changes since last startup, re-compile with pyside-uic before importing. 35 | """ 36 | 37 | if os.path.isfile("uiHashDigests.yml"): 38 | with open("uiHashDigests.yml",'rb') as file_: 39 | uiDigests = yaml.load(file_) 40 | else: 41 | uiDigests = {} 42 | 43 | for fileName in (x for x in os.listdir(os.curdir) if x.endswith(".ui")): 44 | with open(fileName, 'rb') as file_: 45 | digest = hashlib.md5(file_.read()).hexdigest() 46 | print digest 47 | if fileName in uiDigests.keys() and digest == uiDigests[fileName]: 48 | continue 49 | else: 50 | print "Reloading UI" 51 | command = "pyside-uic "+fileName+" -o "+os.path.splitext(fileName)[0]+".py" 52 | print command 53 | os.system(command) 54 | uiDigests[fileName] = digest 55 | with open("uiHashDigests.yml", 'wb') as file_: 56 | yaml.safe_dump(uiDigests,file_) 57 | time.sleep(0.5) 58 | if "--dev" in sys.argv: 59 | compile_ui_files() 60 | 61 | import mainWindow 62 | 63 | class MainWindow(QtGui.QMainWindow, mainWindow.Ui_MainWindow): 64 | def __init__(self, parent=None): 65 | super(MainWindow, self).__init__(parent) 66 | self.setupUi(self) 67 | self.setWindowIcon(QtGui.QIcon("icon.png")) 68 | self.setWindowTitle("Statblock Printer") 69 | self.reportHTML = "" 70 | 71 | ## Load monster manual 72 | if not os.path.isfile("DM_Rules.html"): 73 | QtGui.QMessageBox.warning(self,'Error',"""No rules file is present. Save the DM rules site HTML in this directory as "DM_Rules.html" and restart the program.""") 74 | webbrowser.open("http://dnd.wizards.com/products/tabletop/dm-basic-rules") 75 | with open("DM_Rules.html",'r') as file_: 76 | monsterList = re.findall("""
.*?""", file_.read().split("List of Monsters")[-1], re.DOTALL) 77 | 78 | self.monsterTreeParent = QtGui.QTreeWidgetItem(["Monsters"]) 79 | self.monsterDict = self.parse_monsters(monsterList) 80 | self.ruleBookTreeWidget.addTopLevelItem(self.monsterTreeParent) 81 | self.monsterTreeParent.setExpanded(True) 82 | 83 | self.ruleBookTreeWidget.setSelectionMode(QtGui.QTreeWidget.ExtendedSelection) 84 | self.ruleBookTreeWidget.itemSelectionChanged.connect(self.preview_rule_selection) 85 | self.ruleBookTreeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 86 | self.ruleBookTreeWidget.customContextMenuRequested.connect(self.prepare_rule_menu) 87 | self.ruleBookTreeWidget.doubleClicked.connect(self.add_rule_double_click) 88 | 89 | self.webView = QtWebKit.QWebView() 90 | self.settings = self.webView.settings() 91 | self.settings.setAttribute(QtWebKit.QWebSettings.LocalContentCanAccessRemoteUrls, True) 92 | 93 | self.rightFrameLayout.insertWidget(0, self.webView) 94 | self.jinjaTemplate = jinja2.Environment(loader=jinja2.FileSystemLoader(os.curdir)).get_template("wideTemplate.html") 95 | self.reportTemplate = jinja2.Environment(loader=jinja2.FileSystemLoader(os.curdir)).get_template("reportTemplate.html") 96 | 97 | self.previewPushButton.clicked.connect(self.create_html_view) 98 | self.savePushButton.clicked.connect(self.save_html_view) 99 | self.moveDownInTreeButton.clicked.connect(self.move_item_down) 100 | self.moveUpInTreeButton.clicked.connect(self.move_item_up) 101 | self.filterRulesLineEdit.textChanged.connect(self.filter_rules) 102 | self.show() 103 | 104 | def parse_monsters(self, monster_list): 105 | outputDict = {} 106 | for monster in monster_list: 107 | monsterName = re.search("()(.*?)()", monster).group(2).strip() 108 | type = re.search("""(

)(.*?)(

)""", monster, re.DOTALL).group(2).strip() 109 | ac = re.search("""(Armor Class)(.*?)(

)""", monster, re.DOTALL).group(2).strip() 110 | hp = re.search("""(Hit Points)(.*?)(

)""", monster, re.DOTALL).group(2).strip() 111 | speed = re.search("""(Speed)(.*?)(

)""", monster, re.DOTALL).group(2).strip() 112 | 113 | statTable = re.search("""(.*?.*?)(.*?)(.*?)(.*?)(.*?)(.*?)(.*?)(.*?)(.*?)(.*?)(.*?)(.*?)()""", monster, re.DOTALL) 114 | STR = statTable.group(2).strip() 115 | DEX = statTable.group(4).strip() 116 | CON = statTable.group(6).strip() 117 | INT = statTable.group(8).strip() 118 | WIS = statTable.group(10).strip() 119 | CHA = statTable.group(12).strip() 120 | 121 | damResistances = re.search("""(Damage Resistances)(.*?)(

)""", monster, re.DOTALL) 122 | damImmunities = re.search("""(Damage Immunities)(.*?)(

)""", monster, re.DOTALL) 123 | conditionImmunities = re.search("""(Condition Immunities)(.*?)(

)""", monster, re.DOTALL) 124 | savingThrows = re.search("""(Saving Throws)(.*?)(

)""", monster, re.DOTALL) 125 | skills = re.search("""(Skills)(.*?)(

)""", monster, re.DOTALL) 126 | senses = re.search("""(Senses)(.*?)(

)""", monster, re.DOTALL) 127 | languages = re.search("""(Languages)(.*?)(

)""", monster, re.DOTALL) 128 | challenge = re.search("""(Challenge)(.*?)(

)""", monster, re.DOTALL) 129 | actionBlock = re.search("""
Actions
.*?(

|

Legendary Actions)""", monster, re.DOTALL) 130 | legendaryActionBlock = re.search("""
Legendary Actions
.*?(

)""", monster, re.DOTALL) 131 | 132 | specialTraitList = [x[1] for x in re.findall("""(

)(.*?)(

)""", monster, re.DOTALL) if len(x)==3] 133 | actionList = [x[1] for x in re.findall("""(

)(.*?)(

)""", actionBlock.group(0) if actionBlock is not None else "", re.DOTALL) if len(x) == 3] 134 | legendaryActionList = [x[1] for x in re.findall("""(

)(.*?)(

)""", legendaryActionBlock.group(0) if legendaryActionBlock is not None else "", re.DOTALL) if len(x) == 3] 135 | lore = re.search("""(

)(.*?)(

)""", monster, re.DOTALL) 136 | 137 | outputDict[monsterName] = { 138 | "monsterName":monsterName, 139 | "monsterType":type, 140 | "armorClass":ac, 141 | "hitPoints":hp, 142 | "speed":speed, 143 | "STR":STR, 144 | "DEX": DEX, 145 | "CON": CON, 146 | "INT": INT, 147 | "WIS": WIS, 148 | "CHA": CHA, 149 | "damageResistances":damResistances.group(2).strip() if damResistances is not None else "", 150 | "damageImmunities": damImmunities.group(2).strip() if damImmunities is not None else "", 151 | "conditionImmunities": conditionImmunities.group(2).strip() if conditionImmunities is not None else "", 152 | "savingThrows": savingThrows.group(2).strip() if savingThrows is not None else "", 153 | "skills": skills.group(2).strip() if skills is not None else "", 154 | "senses": senses.group(2).strip() if senses is not None else "", 155 | "languages": (languages.group(2).strip() if languages.group(2).strip() != '\xe2\x80\x94' else "") if languages is not None else "", 156 | "challenge": challenge.group(2).strip() if challenge is not None else "", 157 | "specialTraits" : specialTraitList, 158 | "actions": actionList, 159 | "legendaryActions":legendaryActionList, 160 | "lore" : lore 161 | } 162 | monsterTreeItem = QtGui.QTreeWidgetItem([monsterName]) 163 | self.monsterTreeParent.addChild(monsterTreeItem) 164 | return outputDict 165 | 166 | def filter_rules(self, inputText): 167 | inputText = inputText.upper() 168 | for parent in range(self.ruleBookTreeWidget.topLevelItemCount()): 169 | parentWidget = self.ruleBookTreeWidget.topLevelItem(parent) 170 | for childIndex in range(parentWidget.childCount()): 171 | if inputText in parentWidget.child(childIndex).text(0).upper(): 172 | parentWidget.child(childIndex).setHidden(False) 173 | else: 174 | parentWidget.child(childIndex).setHidden(True) 175 | 176 | 177 | def move_item_down(self): 178 | selectedItem = self.selectedItemsTreeWidget.selectedItems()[0] 179 | if selectedItem.parent(): 180 | parent = selectedItem.parent() 181 | index = selectedItem.parent().indexOfChild(selectedItem) 182 | if index+1 < selectedItem.parent().childCount(): 183 | item = selectedItem.parent().takeChild(index) 184 | parent.insertChild(index+1, item) 185 | self.selectedItemsTreeWidget.clearSelection() 186 | item.setSelected(True) 187 | 188 | def move_item_up(self): 189 | selectedItem = self.selectedItemsTreeWidget.selectedItems()[0] 190 | if selectedItem.parent(): 191 | parent = selectedItem.parent() 192 | index = selectedItem.parent().indexOfChild(selectedItem) 193 | if index-1 >= 0: 194 | item = selectedItem.parent().takeChild(index) 195 | parent.insertChild(index-1, item) 196 | self.selectedItemsTreeWidget.clearSelection() 197 | item.setSelected(True) 198 | 199 | def add_rule_double_click(self, index): 200 | widget = self.ruleBookTreeWidget.itemFromIndex(index) 201 | self.pin_item([(widget.parent().text(0),widget.text(0))]) 202 | 203 | def create_html_view(self): 204 | # build list of lists of items to include. 205 | self.reportTemplate = jinja2.Environment(loader=jinja2.FileSystemLoader(os.curdir)).get_template("reportTemplate.html") 206 | 207 | outputList = [] 208 | for topLevelIndex in range(self.selectedItemsTreeWidget.topLevelItemCount()): 209 | topLevelItem = self.selectedItemsTreeWidget.topLevelItem(topLevelIndex) 210 | for childIndex in range(topLevelItem.childCount()): 211 | outputList.append(self.build_monster_dict(topLevelItem.child(childIndex).text(0))) 212 | 213 | 214 | print outputList 215 | self.reportHTML = self.reportTemplate.render(outputList=outputList) 216 | try: 217 | self.webView.setHtml(self.reportHTML) 218 | except: 219 | raise 220 | 221 | def save_html_view(self): 222 | self.create_html_view() 223 | saveFileName = QtGui.QFileDialog.getSaveFileName(self,"Save File", "My Report","HTML File (*.html);;PDF (*.pdf)") 224 | if saveFileName[0] and self.reportHTML: 225 | fileName = saveFileName[0] 226 | extension = saveFileName[1] 227 | if ".html" in extension and not os.path.splitext(fileName)[-1].lower().endswith(".html"): 228 | fileName += ".html" 229 | if ".pdf" in extension and not os.path.splitext(fileName)[-1].lower().endswith(".pdf"): 230 | fileName += ".pdf" 231 | 232 | if os.path.splitext(fileName)[-1].lower() == ".html": 233 | with open(fileName,'w') as file_: 234 | file_.write(self.reportHTML) 235 | 236 | elif os.path.splitext(fileName)[-1].lower() == ".pdf": 237 | printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) 238 | printer.setOutputFileName(fileName) 239 | self.webView.print_(printer) 240 | 241 | def prepare_rule_menu(self, pos): 242 | items = self.ruleBookTreeWidget.selectedItems() 243 | print (item.text(0) for item in items) 244 | menu = QtGui.QMenu() 245 | action = QtGui.QAction("Add", menu) 246 | action.triggered.connect(partial(self.pin_item, [(item.parent().text(0), item.text(0)) for item in items])) 247 | menu.addAction(action) 248 | menu.exec_(self.ruleBookTreeWidget.mapToGlobal(pos)) 249 | 250 | def pin_item(self, inputList): 251 | for inputTuple in inputList: 252 | # Creates top level tree widget if it doesn't exist yet 253 | for topLevelIndex in range(self.selectedItemsTreeWidget.topLevelItemCount()): 254 | if inputTuple[0] in self.selectedItemsTreeWidget.topLevelItem(topLevelIndex).text(0): 255 | currentParent = self.selectedItemsTreeWidget.topLevelItem(topLevelIndex) 256 | break 257 | else: 258 | currentParent = QtGui.QTreeWidgetItem([inputTuple[0]]) 259 | self.selectedItemsTreeWidget.addTopLevelItem(currentParent) 260 | currentParent.setExpanded(True) 261 | 262 | for childIndex in range(currentParent.childCount()): 263 | if inputTuple[1] in currentParent.child(childIndex).text(0): 264 | self.statusBar().showMessage("Already added.",2000) 265 | break 266 | else: 267 | childItem = QtGui.QTreeWidgetItem([inputTuple[1]]) 268 | currentParent.addChild(childItem) 269 | 270 | def build_monster_dict(self,name): 271 | d = self.monsterDict[name] 272 | for key in d.keys(): 273 | if isinstance(d[key], list): 274 | for i in range(len(d[key])): 275 | try: 276 | d[key][i] = unidecode(unicode(d[key][i])) 277 | except: 278 | d[key][i] = unicode(d[key][i]).decode('latin1') 279 | else: 280 | try: 281 | d[key] = unidecode(unicode(d[key])) 282 | except: 283 | d[key] = unicode(d[key]).decode('latin1') 284 | return d 285 | 286 | def preview_rule_selection(self): 287 | selection = self.ruleBookTreeWidget.selectedItems() 288 | if selection and selection[0].parent(): 289 | d = self.build_monster_dict(selection[0].text(0)) 290 | self.webView.setHtml(self.jinjaTemplate.render(d)) 291 | 292 | 293 | 294 | if __name__ == "__main__": 295 | app = QtGui.QApplication(sys.argv) 296 | mw = MainWindow() 297 | sys.exit(app.exec_()) 298 | 299 | -------------------------------------------------------------------------------- /Examples/ExampleReport.html: -------------------------------------------------------------------------------- 1 | 203 | 204 | 205 |
206 |
207 |
208 |
209 |

Animated Armor

210 |

Medium construct, unaligned

211 |
212 | 213 | 214 | 215 |
216 |
217 |

Armor Class

218 |

18 (natural armor)

219 |
220 |
221 |

Hit Points

222 |

33 (6d8 + 6)

223 |
224 |
225 |

Speed

226 |

25 ft.

227 |
228 | 229 | 230 | 231 |
232 |
233 |

STR

234 |

14 (+2)

235 |
236 |
237 |

DEX

238 |

11 (+0)

239 |
240 |
241 |

CON

242 |

13 (+1)

243 |
244 |
245 |

INT

246 |

1 (-5)

247 |
248 |
249 |

WIS

250 |

3 (-4)

251 |
252 |
253 |

CHA

254 |

1 (-5)

255 |
256 |
257 | 258 | 259 | 260 | 261 | 262 |
263 |

Damage Immunities

264 |

poison, psychic

265 |
266 | 267 | 268 | 269 |
270 |

Condition Immunities

271 |

blinded, charmed, deafened, exhaustion, frightened, paralyzed, petrified, poisoned

272 |
273 | 274 | 275 | 276 |
277 |

Senses

278 |

blindsight 60 ft. (blind beyond this radius), passive Perception 6

279 |
280 | 281 | 282 | 283 | 284 |
285 |

Challenge

286 |

1 (200 XP)

287 |
288 |
289 | 290 | 291 | 292 | 293 |
294 |
295 | 296 | 297 |
298 |

Antimagic Susceptibility. The armor is incapacitated while in the area of an antimagic field. If targeted by dispel magic, the armor must succeed on a Constitution saving throw against the caster's spell save DC or fall unconscious for 1 minute.

299 |
300 | 301 |
302 |

False Appearance. While the armor remains motionless, it is indistinguishable from a normal suit of armor.

303 |
304 | 305 | 306 | 307 |
308 |

Actions

309 | 310 |
311 |

Multiattack. The armor makes two melee attacks.

312 |
313 | 314 |
315 |

Slam. Melee Weapon Attack: +4 to hit, reach 5 ft., one target. Hit: 5 (1d6 + 2) bludgeoning damage.

316 |
317 | 318 |
319 | 320 | 321 |
322 |
323 |
324 | 325 |
326 |
327 |
328 |
329 |

Ankylosaurus

330 |

Huge beast, unaligned

331 |
332 | 333 | 334 | 335 |
336 |
337 |

Armor Class

338 |

15 (natural armor)

339 |
340 |
341 |

Hit Points

342 |

68 (8d12 + 16)

343 |
344 |
345 |

Speed

346 |

30 ft.

347 |
348 | 349 | 350 | 351 |
352 |
353 |

STR

354 |

19 (+4)

355 |
356 |
357 |

DEX

358 |

11 (+0)

359 |
360 |
361 |

CON

362 |

15 (+2)

363 |
364 |
365 |

INT

366 |

2 (-4)

367 |
368 |
369 |

WIS

370 |

12 (+1)

371 |
372 |
373 |

CHA

374 |

5 (-3)

375 |
376 |
377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 |
387 |

Senses

388 |

passive Perception 11

389 |
390 | 391 | 392 | 393 | 394 |
395 |

Challenge

396 |

3 (700 XP)

397 |
398 |
399 | 400 | 401 | 402 | 403 |
404 |
405 | 406 | 407 |
408 |

Actions

409 | 410 |
411 |

Tail. Melee Weapon Attack: +7 to hit, reach 10 ft., one target.
412 | Hit: 18 (4d6 + 4) bludgeoning damage. If the target is a creature, it must succeed on a DC 14 Strength saving throw or be knocked prone.

413 |
414 | 415 |
416 | 417 | 418 |
419 |
420 |
421 | 422 |
423 |
424 |
425 |
426 |

Ape

427 |

Medium beast, unaligned

428 |
429 | 430 | 431 | 432 |
433 |
434 |

Armor Class

435 |

12

436 |
437 |
438 |

Hit Points

439 |

19 (3d8 + 6)

440 |
441 |
442 |

Speed

443 |

30 ft., climb 30 ft.

444 |
445 | 446 | 447 | 448 |
449 |
450 |

STR

451 |

16 (+3)

452 |
453 |
454 |

DEX

455 |

14 (+2)

456 |
457 |
458 |

CON

459 |

14 (+2)

460 |
461 |
462 |

INT

463 |

6 (-2)

464 |
465 |
466 |

WIS

467 |

12 (+1)

468 |
469 |
470 |

CHA

471 |

7 (-2)

472 |
473 |
474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 |
484 |

Senses

485 |

passive Perception 13

486 |
487 | 488 | 489 | 490 | 491 |
492 |

Challenge

493 |

1/2 (100 XP)

494 |
495 |
496 | 497 | 498 | 499 | 500 |
501 |
502 | 503 | 504 |
505 |

Actions

506 | 507 |
508 |

Multiattack. The ape makes two fist attacks.

509 |
510 | 511 |
512 |

Fist. Melee Weapon Attack: +5 to hit, reach 5 ft., one target.
513 | Hit: 6 (1d6 + 3) bludgeoning damage.

514 |
515 | 516 |
517 |

Rock. Ranged Weapon Attack: +5 to hit, range 25/50 ft., one target.
518 | Hit: 6 (1d6 + 3) bludgeoning damage.

519 |
520 | 521 |
522 | 523 | 524 |
525 |
526 |
527 | 528 |
529 |
530 |
531 |
532 |

Awakened Shrub

533 |

Small plant, unaligned

534 |
535 | 536 | 537 | 538 |
539 |
540 |

Armor Class

541 |

9

542 |
543 |
544 |

Hit Points

545 |

10 (3d6)

546 |
547 |
548 |

Speed

549 |

20 ft.

550 |
551 | 552 | 553 | 554 |
555 |
556 |

STR

557 |

3 (-4)

558 |
559 |
560 |

DEX

561 |

8 (-1)

562 |
563 |
564 |

CON

565 |

11 (+0)

566 |
567 |
568 |

INT

569 |

10 (+0)

570 |
571 |
572 |

WIS

573 |

10 (+0)

574 |
575 |
576 |

CHA

577 |

6 (-2)

578 |
579 |
580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 |
590 |

Senses

591 |

passive Perception 10

592 |
593 | 594 | 595 | 596 |
597 |

Languages

598 |

one language known by its creator

599 |
600 | 601 | 602 |
603 |

Challenge

604 |

0 (10 XP)

605 |
606 |
607 | 608 | 609 | 610 | 611 |
612 |
613 | 614 | 615 |
616 |

False Appearance. While the shrub remains motionless, it is indistinguishable from a normal shrub.

617 |
618 | 619 | 620 | 621 |
622 |

Actions

623 | 624 |
625 |

Rake. Melee Weapon Attack: +1 to hit, reach 5 ft., one target.
626 | Hit: 1 (1d4 - 1) slashing damage.

627 |
628 | 629 |
630 | 631 | 632 |
633 |
634 |
635 | --------------------------------------------------------------------------------