Last active
May 18, 2017 21:24
-
-
Save ShadowKyogre/c21563441c1ad47cb32fd2477eae3512 to your computer and use it in GitHub Desktop.
DnD 5e Stat Sheet Processor created by ShadowKyogre - https://repl.it/Hxsr/70
This file contains hidden or 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
from ast import literal_eval | |
from collections import Counter, OrderedDict | |
from itertools import chain | |
import math | |
import yaml | |
## Constants | |
STATS = ('str', 'dex', 'con', 'int', 'wis', 'cha') | |
PB = {0:8, 1:9, 2:10, 3:11, 4:12, 5:13, 7:14, 9:15} | |
SKILLS = OrderedDict([ | |
('str', | |
['Athletics']), | |
('dex', | |
['Acrobatics', 'Sleight of hand', 'Stealth']), | |
('int', | |
['Arcana','History', 'Investigation', 'Nature', 'Religion']), | |
('wis', | |
['Animal Handling', 'Insight', 'Medicine', 'Perception', 'Survival']), | |
('cha', | |
['Deception', 'Intimidation', 'Performance']) | |
]) | |
## Util Funcs | |
def ordered_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict): | |
class OrderedLoader(Loader): | |
pass | |
def construct_mapping(loader, node): | |
loader.flatten_mapping(node) | |
return object_pairs_hook(loader.construct_pairs(node)) | |
OrderedLoader.add_constructor( | |
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, | |
construct_mapping) | |
return yaml.load(stream, OrderedLoader) | |
## Funcs | |
def proficiency_bonus(level): | |
return math.ceil(level/1E10) + math.ceil(level/4) | |
def stat_slash_fmt(stat_map): | |
return ' / '.join(' '.join((k, str(stat_map[k]))) for k in STATS) | |
def stat_modifier(stat): | |
return (stat - 10) // 2 | |
def cantrip_tier(level): | |
return round((level + 1) / 6 + 0.5) | |
## Input/Output | |
### Load sheet data | |
with open('stat-sheet.yaml', 'r') as f: | |
sheet_data = ordered_load(f) | |
### Calculate Proficiency Bonus | |
clevel = sum(v['Levels'] for v in sheet_data['Classes'].values()) | |
prof_bonus = proficiency_bonus(clevel) | |
### Verify Point Buy investments | |
pb_total = sum(sheet_data['Point Buy'].values()) | |
if pb_total < 27: | |
raise ValueError("Not all stat points are assigned!") | |
elif pb_total > 27: | |
raise ValueError("Too many points!") | |
### Add racials and species overlay | |
pb_base_stats = Counter({k: PB[v] for k, v in sheet_data['Point Buy'].items()}) | |
base_stats = pb_base_stats.copy() | |
base_stats.update(sheet_data['Racial Bonuses']) | |
dict.update(base_stats, sheet_data['Overlays']['Pre-ASI']) | |
final_stats = base_stats.copy() | |
### Update stats based on progression stages | |
progression_lines = [ | |
'=== Progression', | |
'[options="header"]', | |
'|===', | |
(''.join(chain("| ", (' ^| {0}'.format(k) for k in STATS)))) | |
] | |
for label, progress in sheet_data['Progression'].items(): | |
p_lock = progress.get('Lock', {}) | |
if p_lock: | |
is_p_locked = ( | |
p_lock['level'] > sheet_data['Classes'].get(p_lock['class'], {}).get('Levels', 0) | |
) | |
if is_p_locked: | |
continue | |
p_asi = progress.get('ASIs', {}) | |
final_stats.update(p_asi) | |
progression_lines.append(''.join( | |
chain('h| {0}'.format(label), | |
(' ^| {0}'.format(p_asi.get(k, '')) for k in STATS)) | |
)) | |
for feat, feat_data in progress.get('Feats', {}).items(): | |
progression_lines.append('| 6+h| {0}'.format(feat)) | |
if feat_data is not None: | |
f_asi = feat_data.get('ASIs', {}) | |
progression_lines.append(''.join( | |
chain('| ', | |
(' ^| {0}'.format(f_asi.get(k, '')) for k in STATS)) | |
)) | |
final_stats.update(f_asi) | |
progression_lines.append("|===") | |
### Update with overlays that should be applied after stats are calculated | |
dict.update(final_stats, sheet_data['Overlays']['Post-ASI']) | |
### Create breakdown of base stats | |
breakdown_lines = [ | |
'=== Base Stats', | |
'[options="header"]', | |
'|===', | |
(''.join(chain("| ", (' ^| {0}'.format(k) for k in STATS)))), | |
''.join(chain( | |
'h| Point Buy', | |
(' | {0}'.format(pb_base_stats.get(k, '')) for k in STATS) | |
)), | |
''.join(chain( | |
'h| Racial', | |
(' | {0}'.format(sheet_data['Racial Bonuses'].get(k, '')) for k in STATS) | |
)), | |
''.join(chain( | |
'h| Pre-ASI', | |
(' | {0}'.format(sheet_data['Overlays']['Pre-ASI'].get(k, '')) for k in STATS) | |
)), | |
''.join(chain( | |
'h| Post-ASI', | |
(' | {0}'.format(sheet_data['Overlays']['Post-ASI'].get(k, '')) for k in STATS) | |
)), | |
'|===', | |
] | |
### Calculate stat modifiers | |
fs_modifiers = Counter({k: stat_modifier(v) for k, v in final_stats.items()}) | |
### Calculate saving throw values | |
saves = fs_modifiers.copy() | |
saves.update({k: prof_bonus for k in sheet_data['Saves']['Proficiencies']}) | |
global_save_bonus = sheet_data['Saves']['Bonus']['Global'] | |
if isinstance(global_save_bonus, str): | |
global_save_bonus = literal_eval(global_save_bonus.format(**fs_modifiers)) | |
saves.update({k: global_save_bonus for k in saves.keys()}) | |
for k, v in sheet_data['Saves']['Bonus']['Specific'].items(): | |
if isinstance(v, str): | |
v = literal_eval(v.format(**fs_modifiers)) | |
saves[k] += v | |
### Calculate Max HP | |
if 'HDOverride' in sheet_data.keys(): | |
hd_size = sheet_data['HDOverride']['Size'] | |
hd_extras = sheet_data['HDOverride']['Extras'] | |
hd_avg = hd_size / 2 + 0.5 | |
hp = (hd_avg + fs_modifiers['con']) * hd_extras | |
hp += sum( | |
(hd_avg + fs_modifiers['con'] + v.get('HP Bonus', 0)) * v['Levels'] | |
for k, v in sheet_data['Classes'].items() | |
) | |
else: | |
first_value = True | |
hp = 0 | |
for k, v in sheet_data['Classes'].items(): | |
if first_value: | |
hp += ( | |
(v['HitDie'] + fs_modifiers['con']) | |
+ (v['HitDie']/2 + 1 + fs_modifiers['con'])*(v['Levels']-1) | |
) | |
first_value = False | |
else: | |
hp += (v['HitDie']/2 + 1 + fs_modifiers['con'])*(v['Levels']) | |
hp += v.get('HP Bonus', 0) * v['Levels'] | |
hp = int(hp) | |
skills_lines = [ | |
"== Skills", | |
'[cols="4"]', | |
"|===", | |
] | |
for ability, skills in SKILLS.items(): | |
skills_lines.append("4+h|{0}|".format(ability)) | |
skills_lines.append("h| Proficient h| Expert h| Passive Bonus") | |
for skill in skills: | |
skill_data = sheet_data['Skills'].get(skill, {}) | |
skills_lines.append(">h| {0} | {1} | {2} | {3}".format( | |
skill, | |
skill_data.get('Proficient', ''), | |
skill_data.get('Expert', ''), | |
sheet_data['Skills'].get(skill, {}).get('PBonus', '') | |
)) | |
skills_lines.append("|===") | |
print("== Stats") | |
print("[horizontal]") | |
print("HP::", hp) | |
print("AC::", | |
literal_eval(sheet_data['AC']['Formula'].format(**fs_modifiers)), | |
'({})'.format(sheet_data['AC']['Notes']) | |
) | |
print("Stats::", ' / '.join("{0} {1} ({2})".format(k, final_stats[k], fs_modifiers[k]) for k in STATS)) | |
print("Saving Throws::", ' / '.join( | |
"{0}{2} {1}".format( | |
k, | |
saves[k], | |
'*' if k in sheet_data['Saves']['Proficiencies'] else '' | |
) | |
for k in STATS | |
)) | |
print("Proficiency Bonus::", prof_bonus) | |
print("Cantrip Tier::", cantrip_tier(clevel)) | |
print() | |
print('\n'.join(skills_lines)) | |
print() | |
print("== Breakdown") | |
print('\n'.join(breakdown_lines)) | |
print() | |
print('\n'.join(progression_lines)) |
This file contains hidden or 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
Basics: | |
Name: Alicia Arsami | |
Gender: Female | |
Race: Devil's Tongue Tiefling (Peacock) | |
Background: Entertainer (Veteran) | |
Apperance: | |
Biography: | |
Inventory: | |
Point Buy: {str: 0, dex: 5, con: 7, int: 1, wis: 7, cha: 7} | |
Racial Bonuses: {int: 1, cha: 2} | |
HDOverride: | |
Size: 10 | |
Extras: 16 | |
Classes: | |
Warlock: | |
Subclass: [Undying Light, Tome] | |
Levels: 18 | |
HitDie: 8 | |
HP Bonus: 2 | |
Hierophant: | |
Subclass: [Arcana] | |
Levels: 2 | |
HitDie: 8 | |
HP Bonus: 2 | |
Overlays: | |
Pre-ASI: {str: 24, dex: 20, con: 24} | |
Post-ASI: {int: 19} | |
AC: | |
Formula: 14 + 2 + 1 + 5 + {dex} | |
Notes: Natural armor (14+Dex), Shield, Cloak of Protection, Boon of Star Scales | |
Saves: | |
Proficiencies: [dex, con, wis, cha] | |
Bonus: | |
Global: 1 | |
Specific: {dex: 5} | |
Skills: | |
Deception: | |
Proficient: Warlock | |
Intimidation: | |
Proficient: Warlock | |
Arcana: | |
Proficient: Arcana Hierophant | |
Acrobatics: | |
Proficient: Background | |
Performance: | |
Proficient: Background | |
Perception: | |
Proficient: Planetar overlay | |
Stealth: | |
Proficient: Planetar overlay | |
Progression: | |
Initial: | |
Feats: | |
Tough: | |
Actor: | |
ASIs: {cha: 1} | |
ASI1: | |
Lock: {class: Warlock, level: 4} | |
ASIs: {cha: 2} | |
Feats: | |
War Caster: | |
ASI2: | |
Lock: {class: Warlock, level: 8} | |
ASIs: {cha: 2} | |
Feats: | |
Spell Sniper: | |
ASI3: | |
Lock: {class: Warlock, level: 12} | |
ASIs: {dex: 2} | |
Feats: | |
Rune Knowledge - Naudiz, Sowilo: | |
ASI4: | |
Lock: {class: Warlock, level: 16} | |
ASIs: {wis: 2} | |
Feats: | |
Rune Mastery - Sowilo: | |
Demigod: | |
ASIs: {cha: 4} | |
Feats: | |
Resilient - Dexterity: | |
ASIs: {dex: 1} | |
Elemental Adept - Fire: | |
Star and Shadow Reader - Radiant: | |
Magic Initiate - Warlock: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment