gdritter repos dndchar / 002d42f
Added HTML generation script as well as Leb charsheet Getty Ritter 7 years ago
2 changed file(s) with 303 addition(s) and 0 deletion(s). Collapse all Expand all
1 #!/usr/bin/env python3
2
3 import sys
4 import yaml
5 import yattag
6
7 TEMPLATE = '''<!DOCTYPE html>
8 <html>
9 <head>
10 <meta http-equiv="Content-type"
11 content="application/html; charset=utf-8"/>
12 <style type="text/css">
13 body {{ font-family: Fira Sans; }}
14 ul {{ list-style-type: '—'; margin-top: 2px; margin-bottom: 2px; }}
15 .main {{ width: 600px; margin-left: auto; margin-right: auto; }}
16 .key {{ color: #888; }}
17 .key:after {{ content: ": " }}
18 .val {{ font-weight: bold; }}
19 .section {{ padding-left: 30px; padding-right: 30px;
20 padding-top: 15px; padding-bottom: 15px;
21 margin: 20px; border: 1px solid; }}
22 .note {{ color: #888; }}
23 li {{ padding-left: 10px; margin-left: 5px; }}
24 .subhed {{ font-weight: bold; }}
25 </style>
26 </head>
27 <body><div class="main">{0}</div></body>
28 </html>
29 '''
30
31 def main(source):
32 '''Load a character YAML file and then HTMLify it'''
33 if source == '-':
34 char = yaml.load(sys.stdin)
35 else:
36 with open(source) as f:
37 char = yaml.load(f)
38 print(TEMPLATE.format(render(char)))
39
40 def stat_mod(n):
41 '''Get the modifier corresponding to a stat'''
42 mod = (n // 2) - 5
43 if mod < 0:
44 return str(mod)
45 else:
46 return '+' + str(mod)
47
48 STATS = [
49 'strength',
50 'dexterity',
51 'constitution',
52 'intelligence',
53 'wisdom',
54 'charisma',
55 'sanity'
56 ]
57
58 def render(char):
59 '''
60 Turn a character into a nice HTML page. This uses a lot of
61 helper functions for common structures.
62 '''
63 doc, tag, text, line = yattag.Doc().ttl()
64
65 def field(name, source=char):
66 '''
67 Look up a field in `source` and print it nicely as a key/value
68 pair of some kind. This does a bit of inspection of the value,
69 and in the case of a list of dictionary, tries to do the right
70 thing and lay it out as an HTML unordered list.
71 '''
72 if isinstance(source[name], list):
73 line('span', name.title(), klass='subhed')
74 with tag('ul', id=name):
75 for elem in source[name]:
76 maybe_li(elem)
77 elif isinstance(source[name], dict):
78 line('span', name.title(), klass='subhed')
79 with tag('ul', id=name):
80 for k, elem in source[name].items():
81 maybe_li('{0}: {1}'.format(k, elem).capitalize())
82 else:
83 print_kv(name, source[name])
84 doc.stag('br')
85
86 def print_kv(name, val):
87 '''
88 Print a simple key/value pair nicely.
89 '''
90 with tag('span', id=name):
91 line('span', name.capitalize(), klass='key')
92 line('span', val, klass='val')
93
94 def stat_field(name, source=char['stats']):
95 '''
96 For fields representing a D&D stat, we also want to print the
97 modifier afterwards, for convenience.
98 '''
99 with tag('span', id=name):
100 line('span', name.title(), klass='key')
101 line('span', source[name], klass='val')
102 mod = stat_mod(source[name])
103 line('span', ' ({0})'.format(mod), klass='note')
104 doc.stag('br')
105
106 def maybe_li(val):
107 '''
108 Sometimes a list item is going to be just a string, but it
109 might have more structure than that, in which case we want
110 to print the tags on the structure, as well.
111 '''
112 if isinstance(val, str):
113 line('li', val.capitalize())
114 else:
115 print_li_with_note(val)
116
117 def print_li_with_note(val):
118 '''
119 Right now, I'm assuming all "rich" fields are dictionaries,
120 but this might not turn out to be true eventually. This
121 handles fields of the form `"item": {"note": "blah"}` by
122 formatting the note separately, and fields of the form
123 "foo": "bar" by printing them normally, because that lets
124 us pretend in the source text that we can just have single-line
125 fields with colons in them. (YAML doesn't interpret it like that!)
126 '''
127 for k, v in val.items():
128 with tag('li'):
129 text(k.capitalize())
130 if isinstance(v, dict):
131 for cl, tx in v.items():
132 doc.stag('br')
133 line('span', cl+': '+tx, klass='note')
134 else:
135 text(': ' + v)
136
137 # Now that we have that our of the way, everything else
138 # is just printing out the right pieces of data in order!
139
140 # basic character data
141 with tag('div', id='chardata', klass='section'):
142 field('name')
143 field('class')
144 field('level')
145 field('background')
146 field('race')
147 field('alignment')
148
149 # stat block
150 with tag('div', id='stats', klass='section'):
151 for stat in STATS:
152 stat_field(stat)
153
154 # saving throw proficiencies
155 with tag('div', id='saving_throws', klass='section'):
156 field('saving throws')
157
158 # combat stuff
159 with tag('div', id='combat', klass='section'):
160 field('ac')
161 field('speed')
162 field('hit point maximum')
163 field('hit die')
164
165 # spells and spell slots
166 with tag('div', id='spells', klass='section'):
167 field('spell slots')
168 field('spells')
169
170 # skill proficiencies
171 with tag('div', id='skills', klass='section'):
172 field('skills')
173
174 # equipment section
175 with tag('div', id='equipment', klass='section'):
176 field('equipment')
177
178 # personality section
179 with tag('div', id='personality', klass='section'):
180 field('personality')
181 field('ideals')
182 field('bonds')
183 field('flaw')
184
185 # features and traits
186 with tag('div', id='features_traits', klass='section'):
187 field('features & traits')
188
189 # other proficiencies
190 with tag('div', id='other_proficiencies', klass='section'):
191 field('languages')
192 field('proficiencies')
193
194 # and we're done!
195 return doc.getvalue()
196
197 if __name__ == '__main__':
198 main(sys.argv[1])
1 name: Lebethron of Angtaur (or "Leb")
2 class: Ranger
3 level: 2
4 background: Outlander
5 race: Firbolg
6 alignment: Chaotic Good
7
8 stats:
9 strength: 15
10 dexterity: 17
11 constitution: 12
12 intelligence: 8
13 wisdom: 17
14 charisma: 10
15 sanity: 12
16
17 speed: 30 feet
18 size: medium
19
20 proficiency bonus: +2
21 hit die: d10
22 hit point maximum: 15
23 ac: 14 (11 [leather armor] + 3 [dex modifier])
24
25 personality:
26 - I'm always picking things up, absently fiddling with them, and sometimes accidentally breaking them.
27 ideals:
28 - Life is in constant change, and we must change with it.
29 bonds:
30 - I am the last of my tribe.
31 flaw:
32 - Too enamored of ale, wine, and other intoxicants.
33
34 saving throws:
35 - strength
36 - dexterity
37
38 languages:
39 - common
40 - elvish
41 - giant
42 - orkish
43 - sylvan
44
45 proficiencies:
46 - musical instrument: flute
47
48 skills:
49 - animal handling
50 - athletics
51 - perception
52 - stealth
53 - survival
54
55 spell slots:
56 1st level: 2
57 spells:
58 - detect magic:
59 note: firbolg ability, can use once per rest
60 - disguise self:
61 note: firbolg ability, can use once per rest
62 - cure wounds
63 - hail of thorns
64
65 equipment:
66 - leather armor
67 - 1× shortswords (1d6 piercing):
68 note: another shortsword is being loaned to Gal
69 - longbow and 20 arrows (1d8 piercing)
70 - backpack
71 - bedroll
72 - mess kit
73 - tinderbox
74 - 10 torches
75 - 10 days rations
76 - waterskin
77 - 2× 50ft hempen rope
78 - staff
79 - hunting trap
80 - set of traveler's clothes
81 - flute
82 - pen & bottle of ink
83 - journal
84 - fishing tackle
85 - 50ft hempen rope
86 - silver candlestick:
87 cost: 20 gp
88 - scroll of minor restoration
89 - silver bowl & healing herbs
90 - 9× amulet:
91 note: taken from dead zombies
92 money:
93 - 525 cp
94 - 136 sp
95 - 90 gp
96
97 features & traits:
98 - firbolg magic
99 - hidden step
100 - powerful build
101 - speech of beast and leaf
102 - favored enemy: orcs & drow
103 - natural explorer: underdark
104 - wanderer
105 - fighting style: archery