├── 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 | 
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 |
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 |
311 | {% endfor %}
312 | {% endif %}
313 | {% if actions %}
314 |
315 |
Actions
316 | {% for action in actions %}
317 |
320 | {% endfor %}
321 |
322 | {% endif %}
323 | {% if legendaryActions %}
324 |
325 |
Legendary Actions
326 | {% for action in legendaryActions %}
327 |
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 |
305 | {% endfor %}
306 | {% endif %}
307 | {% if monsterBlock['actions'] %}
308 |
309 |
Actions
310 | {% for action in monsterBlock['actions'] %}
311 |
314 | {% endfor %}
315 |
316 | {% endif %}
317 | {% if monsterBlock['legendaryActions'] %}
318 |
319 |
Legendary Actions
320 | {% for action in monsterBlock['legendaryActions'] %}
321 |
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 |
--------------------------------------------------------------------------------