diff --git a/ben.pdf b/ben.pdf index ea608bd..7b4b29e 100644 Binary files a/ben.pdf and b/ben.pdf differ diff --git a/ben.py b/ben.py index 1aab057..827fcce 100644 --- a/ben.py +++ b/ben.py @@ -8,31 +8,34 @@ sheet by running ``makesheets`` from the command line. """ -dungeonsheets_version = "0.8.3" +dungeonsheets_version = "0.9.0" name = "Ben" -classes_levels = ['paladin 1'] -subclasses = ["Oath of The Ancients"] player_name = "Ben" -background = "Charlatan" -race = "Hill Dwarf" + +# Be sure to list Primary class first +classes = ['Bard', 'Paladin'] # ex: ['Wizard'] or ['Rogue', 'Fighter'] +levels = [10, 2] # ex: [10] or [3, 2] +subclasses = ['', ''] # ex: ['Necromacy'] or ['Thief', None] +background = "Sailor" +race = "Half-Orc" alignment = "Neutral good" xp = 0 hp_max = 10 # Ability Scores -strength = 15 -dexterity = 14 -constitution = 15 +strength = 20 +dexterity = 13 +constitution = 14 intelligence = 12 -wisdom = 11 -charisma = 8 +wisdom = 10 +charisma = 9 # Select what skills you're proficient with -skill_proficiencies = ('intimidation', 'athletics', 'deception', 'sleight of hand') +# ex: skill_proficiencies = ('athletics', 'acrobatics', 'arcana') +skill_proficiencies = ('arcana', 'medicine', 'athletics', 'perception', 'intimidation') -# Named features / feats that aren't part of your classes, -# race, or background. +# Named features / feats that aren't part of your classes, race, or background. # Example: # features = ('Tavern Brawler',) # take the optional Feat from PHB features = () @@ -43,7 +46,7 @@ features = () feature_choices = () # Proficiencies and languages -languages = """Common, Dwarvish""" +languages = """Common, Orc""" # Inventory # TODO: Get yourself some money @@ -55,6 +58,7 @@ pp = 0 # TODO: Put your equipped weapons and armor here weapons = () # Example: ('shortsword', 'longsword') +magic_items = () # Example: ('ring of protection',) armor = "" # Eg "light leather armor" shield = "" # Eg "shield" diff --git a/dungeonsheets/background.py b/dungeonsheets/background.py index 8cb70b3..d703fec 100644 --- a/dungeonsheets/background.py +++ b/dungeonsheets/background.py @@ -12,7 +12,7 @@ class Background(): features = () languages = () - def __init__(self, owner): + def __init__(self, owner=None): self.owner = owner cls = type(self) self.features = tuple([f(owner=self.owner) for f in cls.features]) diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 2c367e2..2b80fef 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -143,7 +143,7 @@ class Character(): my_levels = [attrs.pop('level', 1)] my_subclasses = [attrs.pop('subclass', None)] # Generate the list of class objects - self.class_list = parse_classes( + self.add_classes( my_classes, my_levels, my_subclasses, feature_choices=attrs.get('feature_choices', [])) # parse race and background @@ -158,6 +158,47 @@ class Character(): def __repr__(self): return f"<{self.class_name}: {self.name}>" + def add_class(self, cls: (classes.CharClass, type, str), level: (int, str), + subclass=None, feature_choices=[]): + if isinstance(cls, str): + cls = cls.strip().title().replace(' ', '') + try: + cls = getattr(classes, cls) + except AttributeError: + raise AttributeError( + 'class was not recognized from classes.py: {:s}'.format(c)) + if isinstance(level, str): + level = int(level) + params = {} + params['feature_choices'] = feature_choices + self.class_list.append(cls(level, owner=self, + subclass=subclass, **params)) + + def add_classes(self, classes_list=[], levels=[], subclasses=[], + feature_choices=[]): + if isinstance(classes_list, str): + classes_list = [classes_list] + if isinstance(levels, int) or isinstance(levels, float) or isinstance(levels, str): + levels = [levels] + if len(levels) == 0: + levels = [1]*len(classes_list) + if isinstance(subclasses, str): + subclasses = [subclasses] + if len(subclasses) == 0: + subclasses = [None]*len(classes_list) + assert len(classes_list) == len(levels), ( + 'the length of classes {:d} does not match length of ' + 'levels {:d}'.format(len(classes), len(levels))) + assert len(classes_list) == len(subclasses), ( + 'the length of classes {:d} does not match length of ' + 'subclasses {:d}'.format(len(classes_list), len(subclasses))) + class_list = [] + for cls, lvl, sub in zip(classes_list, levels, subclasses): + params = {} + params['feature_choices'] = feature_choices + self.add_class(cls=cls, level=lvl, subclass=sub, + **params) + @property def race(self): return self._race @@ -632,7 +673,7 @@ class Character(): for c in self.class_list: if isinstance(c, classes.Druid): c.wild_shapes = new_shapes - + @classmethod def load(cls, character_file): # Create a character from the character definition @@ -668,42 +709,6 @@ class Character(): flatten=kwargs.get('flatten', True)) -def parse_classes(classes_list=[], levels=[], subclasses=[], - feature_choices=[]): - if isinstance(classes_list, str): - classes_list = [classes_list] - if isinstance(levels, int) or isinstance(levels, float) or isinstance(levels, str): - levels = [levels] - if len(levels) == 0: - levels = [1]*len(classes_list) - if isinstance(subclasses, str): - subclasses = [subclasses] - if len(subclasses) == 0: - subclasses = [None]*len(classes_list) - assert len(classes_list) == len(levels), ( - 'the length of classes {:d} does not match length of ' - 'levels {:d}'.format(len(classes), len(levels))) - assert len(classes_list) == len(subclasses), ( - 'the length of classes {:d} does not match length of ' - 'subclasses {:d}'.format(len(classes_list), len(subclasses))) - class_list = [] - for cls, lvl, sub in zip(classes_list, levels, subclasses): - if isinstance(cls, str): - cls = cls.strip().title().replace(' ', '') - try: - this_class = getattr(classes, cls) - this_level = int(lvl) - except AttributeError: - raise AttributeError( - 'class was not recognized from classes.py: {:s}'.format(c)) - except ValueError: - raise ValueError( - 'level was not recognizable as an int: {:s}'.format(str(lvl))) - params = {} - params['feature_choices'] = feature_choices - class_list += [this_class(this_level, subclass=sub, **params)] - return class_list - def read_character_file(filename): """Create a character object from the given definition file. diff --git a/dungeonsheets/classes/barbarian.py b/dungeonsheets/classes/barbarian.py index 196c997..a306498 100644 --- a/dungeonsheets/classes/barbarian.py +++ b/dungeonsheets/classes/barbarian.py @@ -99,13 +99,15 @@ class ZealotPath(SubClass): class Barbarian(CharClass): name = 'Barbarian' hit_dice_faces = 12 + subclass_select_level = 3 saving_throw_proficiencies = ('strength', 'constitution') primary_abilities = ('strength',) - weapon_proficiencies = (weapons.SimpleWeapon + weapons.MartialWearpon) + weapon_proficiencies = (weapons.SimpleWeapon, weapons.MartialWeapon) _proficiencies_text = ('light armor', 'medium armor', 'shields', 'simple weapons', 'martial weapons') multiclass_weapon_proficiencies = weapon_proficiencies - _multiclass_proficiencies_text = ('shields', 'simple weapons', 'martial weapons') + _multiclass_proficiencies_text = ('shields', 'simple weapons', + 'martial weapons') class_skill_choices = ('Animal Handling', 'Athletics', 'Intimidation', 'Nature', 'Perception', 'Survival') subclasses_available = (BerserkerPath, TotemWarriorPath, BattleragerPath, diff --git a/dungeonsheets/classes/bard.py b/dungeonsheets/classes/bard.py index 699006c..06bce9f 100644 --- a/dungeonsheets/classes/bard.py +++ b/dungeonsheets/classes/bard.py @@ -116,13 +116,14 @@ class CollegeOfWhispers(SubClass): class Bard(CharClass): name = 'Bard' hit_dice_faces = 8 + subclass_select_level = 3 saving_throw_proficiencies = ('dexterity', 'charisma') primary_abilities = ('charisma',) _proficiencies_text = ( 'Light armor', 'simple weapons', 'hand crossbows', 'longswords', 'rapiers', 'shortswords', 'three musical instruments of your choice') - weapon_proficiencies = ((weapons.HandCrossbow, weapons.Longsword, - weapons.Rapier, weapons.Shortsword) + + weapon_proficiencies = (weapons.HandCrossbow, weapons.Longsword, + weapons.Rapier, weapons.Shortsword, weapons.SimpleWeapon) class_skill_choices = ('Acrobatics', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', diff --git a/dungeonsheets/classes/classes.py b/dungeonsheets/classes/classes.py index 0fc9dfa..71c5910 100644 --- a/dungeonsheets/classes/classes.py +++ b/dungeonsheets/classes/classes.py @@ -9,6 +9,7 @@ class CharClass(): name = "Default" level = 1 hit_dice_faces = 2 + subclass_select_level = 3 weapon_proficiencies = () _proficiencies_text = () multiclass_weapon_proficiencies = () @@ -26,9 +27,12 @@ class CharClass(): subclasses_available = () features_by_level = defaultdict(list) - def __init__(self, level, subclass=None, feature_choices=[], + def __init__(self, level, owner=None, subclass=None, feature_choices=[], **params): self.level = level + self.owner = owner + # For ex: add "char.Monk" attribute + setattr(self.owner, self.name, self) # Instantiate the features self.features_by_level = defaultdict(list) cls = type(self) diff --git a/dungeonsheets/classes/cleric.py b/dungeonsheets/classes/cleric.py index 21ca7d0..9dcff8f 100644 --- a/dungeonsheets/classes/cleric.py +++ b/dungeonsheets/classes/cleric.py @@ -183,6 +183,7 @@ class GraveDomain(SubClass): class Cleric(CharClass): name = 'Cleric' hit_dice_faces = 8 + subclass_select_level = 1 saving_throw_proficiencies = ('wisdom', 'charisma') primary_abilities = ('wisdom',) _proficiencies_text = ('light armor', 'medium armor', 'shields', diff --git a/dungeonsheets/classes/druid.py b/dungeonsheets/classes/druid.py index 893f526..9490458 100644 --- a/dungeonsheets/classes/druid.py +++ b/dungeonsheets/classes/druid.py @@ -86,6 +86,7 @@ class Druid(CharClass): _wild_shapes = () _circle = '' hit_dice_faces = 8 + subclass_select_level = 2 saving_throw_proficiencies = ('intelligence', 'wisdom') primary_abilities = ('wisdom',) languages = 'Druidic' diff --git a/dungeonsheets/classes/fighter.py b/dungeonsheets/classes/fighter.py index f7c3f5c..0c914d9 100644 --- a/dungeonsheets/classes/fighter.py +++ b/dungeonsheets/classes/fighter.py @@ -165,6 +165,7 @@ class Gunslinger(SubClass): class Fighter(CharClass): name = 'Fighter' hit_dice_faces = 10 + subclass_select_level = 3 saving_throw_proficiencies = ('strength', 'constitution') primary_abilities = ('strength', 'dexterity',) _proficiencies_text = ('All armor', 'shields', 'simple weapons', diff --git a/dungeonsheets/classes/monk.py b/dungeonsheets/classes/monk.py index 0bd4984..c442f84 100644 --- a/dungeonsheets/classes/monk.py +++ b/dungeonsheets/classes/monk.py @@ -110,6 +110,7 @@ class KenseiWay(SubClass): class Monk(CharClass): name = 'Monk' hit_dice_faces = 8 + subclass_select_level = 3 saving_throw_proficiencies = ('strength', 'dexterity') primary_abilities = ('dexterity', 'wisdom') _proficiencies_text = ( diff --git a/dungeonsheets/classes/paladin.py b/dungeonsheets/classes/paladin.py index adeab7a..298b90c 100644 --- a/dungeonsheets/classes/paladin.py +++ b/dungeonsheets/classes/paladin.py @@ -222,6 +222,7 @@ class OathOfRedemption(SubClass): class Paladin(CharClass): name = 'Paladin' hit_dice_faces = 10 + subclass_select_level = 3 saving_throw_proficiencies = ('wisdom', 'charisma') primary_abilities = ('strength', 'charisma') _proficiencies_text = ('All armor', 'shields', 'simple weapons', diff --git a/dungeonsheets/classes/rogue.py b/dungeonsheets/classes/rogue.py index d197230..8bf8b62 100644 --- a/dungeonsheets/classes/rogue.py +++ b/dungeonsheets/classes/rogue.py @@ -118,12 +118,13 @@ class Swashbuckler(SubClass): class Rogue(CharClass): name = 'Rogue' hit_dice_faces = 8 + subclass_select_level = 3 saving_throw_proficiencies = ('dexterity', 'intelligence') primary_abilities = ('dexterity',) _proficiencies_text = ( 'light armor', 'simple weapons', 'hand crossbows', 'longswords', 'rapiers', 'shortswords', "thieves' tools") - weapon_proficiencies = (weapons,SimpleWeapon, weapons.HandCrossbow, + weapon_proficiencies = (weapons.SimpleWeapon, weapons.HandCrossbow, weapons.Longsword, weapons.Rapier, weapons.Shortsword) multiclass_weapon_proficiencies = () diff --git a/dungeonsheets/classes/sorceror.py b/dungeonsheets/classes/sorceror.py index baf1cdb..1317505 100644 --- a/dungeonsheets/classes/sorceror.py +++ b/dungeonsheets/classes/sorceror.py @@ -94,6 +94,7 @@ class StormSorcery(SubClass): class Sorceror(CharClass): name = 'Sorceror' hit_dice_faces = 6 + subclass_select_level = 1 saving_throw_proficiencies = ('constitution', 'charisma') primary_abilities = ('charisma',) _proficiencies_text = ('daggers', 'darts', 'slings', diff --git a/dungeonsheets/classes/warlock.py b/dungeonsheets/classes/warlock.py index 86a7428..01c8470 100644 --- a/dungeonsheets/classes/warlock.py +++ b/dungeonsheets/classes/warlock.py @@ -115,6 +115,7 @@ class Hexblade(SubClass): class Warlock(CharClass): name = 'Warlock' hit_dice_faces = 8 + subclass_select_level = 1 saving_throw_proficiencies = ('wisdom', 'charisma') primary_abilities = ('charisma',) _proficiencies_text = ("light Armor", "simple weapons") diff --git a/dungeonsheets/classes/wizard.py b/dungeonsheets/classes/wizard.py index a07cae3..0db124e 100644 --- a/dungeonsheets/classes/wizard.py +++ b/dungeonsheets/classes/wizard.py @@ -159,6 +159,7 @@ class WarMagic(SubClass): class Wizard(CharClass): name = 'Wizard' hit_dice_faces = 6 + subclass_select_level = 2 saving_throw_proficiencies = ('intelligence', 'wisdom') primary_abilities = ('intelligence',) _proficiencies_text = ('daggers', 'darts', 'slings', diff --git a/dungeonsheets/create_character.py b/dungeonsheets/create_character.py index c43a4aa..0c639c3 100755 --- a/dungeonsheets/create_character.py +++ b/dungeonsheets/create_character.py @@ -30,6 +30,49 @@ races = {r.name: r for r in race.available_races} backgrounds = {b.name: b for b in background.available_backgrounds} +class LinkedListForm(npyscreen.ActionForm): + prev_page = None + this_page = None + next_page = None + + def __init__(self, formid, *args, **kwargs): + self.this_page = formid + super().__init__(*args, **kwargs) + + def to_next(self): + self.parentApp.setNextForm(self.next_page) + + def to_prev(self): + self.parentApp.setNextForm(self.prev_page) + + def add_next(self, next_name): + new_next = self.parentApp.getForm(next_name) + if self.next_page: + current_next = self.parentApp.getForm(self.next_page) + current_next.prev_page = next_name + new_next.next_page = self.next_page + new_next.prev_page = self.this_page + self.next_page = next_name + + def add_prev(self, prev_name): + new_prev = self.parentApp.getForm(prev_name) + if self.prev_page: + current_prev = self.parentApp.getForm(self.prev_page) + current_prev.next_page = prev_name + new_prev.prev_page = self.prev_page + new_prev.next_page = self.this_page + self.prev_page = prev_name + + def prune(self): + if self.next_page: + next_form = self.parentApp.getForm(self.next_page) + next_form.prev_page = self.prev_page + if self.prev_page: + prev_form = self.parentApp.getForm(self.prev_page) + prev_form.next_page = self.next_page + self.parentApp.removeForm(self.this_page) + + class App(npyscreen.NPSAppManaged): # STARTING_FORM = 'SKILLS' character = None @@ -63,17 +106,31 @@ class App(npyscreen.NPSAppManaged): def onStart(self): self.character = character.Character() - self.addForm("MAIN", BasicInfoForm, name="Basic Info:") - self.addForm("RACE", RaceForm, name="Select your character's race:") - self.addForm("CLASS1", CharacterClassForm, name="Select your character's primary class:") - self.addForm("BACKGROUND", BackgroundForm, name="Choose background:") - self.addForm("ALIGNMENT", AlignmentForm, name="Select your character's alignment:") - self.addForm("ABILITIES", AbilityScoreForm, name="Choose ability scores:") - self.addForm("SKILLS", SkillForm, name="Choose skill proficiencies") - self.addForm("SAVE", SaveForm, name="Save character:") + self.addForm("MAIN", BasicInfoForm, name="Basic Info:", formid='MAIN') + self.addForm("RACE", RaceForm, name="Select your character's race:", + formid='RACE') + self.addForm("CLASS1", CharacterClassForm, name="Select your character's primary class:", + formid='CLASS1') + self.addForm("BACKGROUND", BackgroundForm, name="Choose background:", + formid='BACKGROUND') + self.addForm("ALIGNMENT", AlignmentForm, + name="Select your character's alignment:", + formid='ALIGNMENT') + self.addForm("ABILITIES", AbilityScoreForm, + name="Choose ability scores:", formid='ABILITIES') + self.addForm("SKILLS", SkillForm, name="Choose skill proficiencies", + formid='SKILLS') + self.addForm("SAVE", SaveForm, name="Save character:", formid='SAVE') + + # Initialized the DoublyLinkedList + forms = ['MAIN', 'RACE', 'CLASS1', 'BACKGROUND', + 'ALIGNMENT', 'ABILITIES', 'SKILLS', 'SAVE'] + for i in range(len(forms)-1): + form = self.getForm(forms[i]) + form.add_next(forms[i+1]) -class BasicInfoForm(npyscreen.ActionForm): +class BasicInfoForm(LinkedListForm): def create(self): self.name = self.add( npyscreen.TitleText, name="Character Name:", use_two_lines=False) @@ -91,18 +148,13 @@ class BasicInfoForm(npyscreen.ActionForm): save_form.filename.value = filename self.parentApp.character.name = self.name.value self.parentApp.character.player_name = self.player_name.value - # Move to the next form - self.parentApp.setNextForm('RACE') + super().to_next() def on_cancel(self): raise KeyboardInterrupt -class RaceForm(npyscreen.ActionForm): - prev_page = 'MAIN' - this_page = 'RACE' - next_page = 'CLASS1' - +class RaceForm(LinkedListForm): def create(self): self.race = self.add( npyscreen.TitleSelectOne, name="Race:", values=tuple(races.keys())) @@ -112,17 +164,14 @@ class RaceForm(npyscreen.ActionForm): selected_race = self.race.get_selected_objects()[0] SelectedRace = races[selected_race] log.debug('Selected character race: %s', SelectedRace.name) - self.parentApp.character.race = SelectedRace() - self.parentApp.setNextForm(self.next_page) - + self.parentApp.character.race = SelectedRace + super().to_next() + def on_cancel(self): - self.parentApp.setNextForm(self.prev_page) + super().to_prev() -class CharacterClassForm(npyscreen.ActionForm): - prev_page = 'RACE' - this_page = 'CLASS1' - next_page = 'BACKGROUND' +class CharacterClassForm(LinkedListForm): class_num = 1 def __init__(self, num=1, **kwargs): @@ -153,7 +202,7 @@ class CharacterClassForm(npyscreen.ActionForm): self.class_options = list(char_classes.keys()) for c in self.parentApp.character.class_list[:self.class_num-1]: self.class_options.remove(c.name) - self.character_class.values = tuple(self.class_options) + self.character_class.values = sorted(tuple(self.class_options)) self.character_class.update() def create(self): @@ -167,14 +216,12 @@ class CharacterClassForm(npyscreen.ActionForm): t = 'Class #{:d}:'.format(self.class_num) for c in self.parentApp.character.class_list: self.class_options.remove(c.name) - self.level = self.add( - npyscreen.TitleText, name='Level:', value="1", use_two_lines=False) - self.subclass = self.add(npyscreen.Checkbox, name="Choose a Subclass?", value=False) if self.class_num == 1: self.multiclass = self.add(npyscreen.Checkbox, name="Add Multiclass?".format(self.class_num + 1), value=False) else: self.multiclass = self.add(npyscreen.Checkbox, name="Add Class #{:d}?".format(self.class_num + 1), value=False) - self.this_page = 'CLASS{:d}'.format(self.class_num) + self.level = self.add( + npyscreen.TitleText, name='Level:', value="1", use_two_lines=False) self.character_class = self.add( npyscreen.TitleSelectOne, name=t, values=tuple(self.class_options)) @@ -183,11 +230,9 @@ class CharacterClassForm(npyscreen.ActionForm): new_form = self.parentApp.addForm(new_name, CharacterClassForm, name="Select your character's Class #{:d}:".format(self.class_num + 1), - num=self.class_num+1) - self.parentApp.getForm(self.next_page).prev_page = new_name - new_form.next_page = self.next_page - new_form.prev_page = self.this_page - self.next_page = new_name + num=self.class_num+1, + formid=new_name) + self.add_next(new_name) return new_form def add_subclass_page(self, newclass, level): @@ -197,11 +242,9 @@ class CharacterClassForm(npyscreen.ActionForm): name="Select your {:s} Subclass".format(newclass.name), newclass=newclass, level=level, - num=self.class_num) - self.parentApp.getForm(self.next_page).prev_page = new_name - new_form.next_page = self.next_page - new_form.prev_page = self.this_page - self.next_page = new_name + num=self.class_num, + formid=new_name) + self.add_next(new_name) return new_form def on_ok(self): @@ -209,13 +252,11 @@ class CharacterClassForm(npyscreen.ActionForm): selected_class = self.character_class.get_selected_objects()[0] selected_class = char_classes[selected_class] log.debug('Selected character class %s', selected_class.name) - new_class = selected_class(level=int(self.level.value), - subclass=None) - if len(self.parentApp.character.class_list) < self.class_num: - self.parentApp.character.class_list.append(new_class) - else: - # replace existing character if we've backed up - self.parentApp.character.class_list[self.class_num-1] = new_class + # replace later classes if we've backed up + self.parentApp.character.class_list = self.parentApp.character.class_list[:self.class_num-1] + self.parentApp.character.add_class(cls=selected_class, + level=int(self.level.value), + subclass=None) # add multiclass page if not exists yet if self.multiclass.value: if self.next_multiclass_page is None: @@ -224,22 +265,24 @@ class CharacterClassForm(npyscreen.ActionForm): self.next_multiclass_page.update_options() else: # in case returned a page, prune any future multiclasses - self.next_page = "BACKGROUND" - self.parentApp.getForm("BACKGROUND").prev_page = self.this_page - self.parentApp.character.class_list = self.parentApp.character.class_list[:self.class_num] - if self.subclass.value: - self.add_subclass_page(newclass=selected_class, - level=int(self.level.value)) - self.parentApp.setNextForm(self.next_page) + while self.next_page != 'BACKGROUND': + f = self.parentApp.getForm(self.next_page) + f.prune() + if int(self.level.value) >= selected_class.subclass_select_level: + if not self.subclass_page: + self.add_subclass_page(newclass=selected_class, + level=int(self.level.value)) + else: + if self.subclass_page is not None: + f = self.parentApp.getForm(self.next_page) + f.prune() + super().to_next() def on_cancel(self): - self.parentApp.setNextForm(self.prev_page) + super().to_prev() -class SubclassForm(npyscreen.ActionForm): - prev_page = 'CLASS1' - next_page = 'BACKGROUND' - +class SubclassForm(LinkedListForm): def __init__(self, newclass, level, num=1, **kwargs): self.class_num = num self.parent_class = newclass @@ -256,25 +299,21 @@ class SubclassForm(npyscreen.ActionForm): values=tuple(self.subclass_options)) def on_ok(self): - sc = self.subclass.get_selected_objects()[0] - if sc in [None, '', 'None']: - newclass = self.parent_class(level=self.level, - subclass=None) - else: - newclass = self.parent_class(level=self.level, - subclass=sc) - self.parentApp.character.class_list[self.class_num-1] = newclass - self.parentApp.setNextForm(self.next_page) - + if self.subclass.value is not None: + sc = self.subclass.get_selected_objects()[0] + if sc in [None, '', 'None']: + sc = None + self.parentApp.character.class_list = self.parentApp.character.class_list[:self.class_num-1] + self.parentApp.character.add_class(cls=self.parent_class, + level=self.level, + subclass=sc) + super().to_next() + def on_cancel(self): - self.parentApp.setNextForm(self.prev_page) + super().to_prev() -class BackgroundForm(npyscreen.ActionForm): - prev_page = 'CLASS1' - this_page = 'BACKGROUND' - next_page = 'ALIGNMENT' - +class BackgroundForm(LinkedListForm): def create(self): self.background = self.add( npyscreen.TitleSelectOne, @@ -290,21 +329,17 @@ class BackgroundForm(npyscreen.ActionForm): languages = Background.languages + race_languages self.parentApp.character.languages = ', '.join(languages) log.debug("Selected character background: %s", Background.name) - self.parentApp.setNextForm(self.next_page) + super().to_next() def on_cancel(self): - self.parentApp.setNextForm(self.prev_page) + super().to_prev() -class AlignmentForm(npyscreen.ActionForm): +class AlignmentForm(LinkedListForm): """Choose your character's alignment.""" alignments = ('Lawful good', 'Neutral good', 'Chaotic good', 'Lawful neutral', 'True neutral', 'Chaotic neutral', 'Lawful evil', 'Neutral evil', 'Chaotic evil', ) - prev_page = 'BACKGROUND' - this_page = 'ALIGNMENT' - next_page = 'ABILITIES' - def create(self): self.alignment = self.add( npyscreen.TitleSelectOne, name="Alignment:", values=self.alignments) @@ -317,16 +352,13 @@ class AlignmentForm(npyscreen.ActionForm): # prep additions to abilities page abils = self.parentApp.getForm('ABILITIES') abils.prep() - self.parentApp.setNextForm(self.next_page) + super().to_next() def on_cancel(self): - self.parentApp.setNextForm(self.prev_page) + super().to_prev() -class AbilityScoreForm(npyscreen.ActionForm): - prev_page = 'ALIGNMENT' - this_page = 'ABILITIES' - next_page = 'SKILLS' +class AbilityScoreForm(LinkedListForm): num_rolls = 0 def roll_dice(self): @@ -348,7 +380,7 @@ class AbilityScoreForm(npyscreen.ActionForm): self.score_options.value = str(new_scores)[1:-1] self.score_options.update() self.reroll_button.value = False - self.reroll_button.name = 'Reroll ({:d}x):'.format(self.num_rolls) + self.reroll_button.name = 'Reroll' self.reroll_button.update() self.default_button.value = False self.default_button.update() @@ -403,7 +435,7 @@ class AbilityScoreForm(npyscreen.ActionForm): name="Use Default Rolls", when_pressed_function=self.set_default) self.reroll_button = self.add(npyscreen.MiniButtonPress, - name="Reroll (0x)", + name="Reroll", when_pressed_function=self.reroll) def prep(self): @@ -414,7 +446,7 @@ class AbilityScoreForm(npyscreen.ActionForm): self.race_text = self.add(npyscreen.FixedText, editable=False, value="Do not add racial bonuses, they will be added for you as listed.") for attr in attrs: - if attr in self.parentApp.character.saving_throw_proficiencies: + if attr in self.parentApp.character.primary_class.primary_abilities: name = '**' + attr else: name = '' + attr @@ -431,17 +463,13 @@ class AbilityScoreForm(npyscreen.ActionForm): self.max_hp = self.add(npyscreen.TitleText, name="Max HP:") def on_ok(self): - self.parentApp.setNextForm(self.next_page) + super().to_next() def on_cancel(self): - self.parentApp.setNextForm(self.prev_page) + super().to_prev() -class SkillForm(npyscreen.ActionForm): - prev_page = 'ABILITIES' - this_page = 'SKILLS' - next_page = 'SAVE' - +class SkillForm(LinkedListForm): def while_editing(self): # Update the static skills for race and background bg_skills = self.parentApp.character.background.skill_proficiencies @@ -454,7 +482,7 @@ class SkillForm(npyscreen.ActionForm): self.parentApp.character.background.skill_choices) static_skills = bg_skills + race_skills choices = set([c for c in choices if c.lower() not in static_skills]) - self.skill_proficiencies.set_values(tuple(choices)) + self.skill_proficiencies.set_values(sorted(tuple(choices))) self.update_remaining() def update_remaining(self, widget=None): @@ -493,17 +521,13 @@ class SkillForm(npyscreen.ActionForm): all_skills = new_skills + bg_skills + race_skills self.parentApp.character.skill_proficiencies = all_skills log.debug(f"Skill proficiencies: {all_skills}") - self.parentApp.setNextForm(self.next_page) + super().to_next() def on_cancel(self): - self.parentApp.setNextForm(self.prev_page) + super().to_prev() -class SaveForm(npyscreen.ActionForm): - prev_page = 'SKILLS' - this_page = 'SAVE' - next_page = None - +class SaveForm(LinkedListForm): def create(self): self.filename = self.add( npyscreen.TitleText, name='Filename:') @@ -513,10 +537,10 @@ class SaveForm(npyscreen.ActionForm): value="After saving, edit this file to finish your personality, etc.") def on_ok(self): - self.parentApp.setNextForm(self.next_page) + super().to_next() def on_cancel(self): - self.parentApp.setNextForm(self.prev_page) + super().to_prev() def main(): diff --git a/dungeonsheets/features/features.py b/dungeonsheets/features/features.py index e5fc6f5..e22f3ea 100644 --- a/dungeonsheets/features/features.py +++ b/dungeonsheets/features/features.py @@ -32,7 +32,7 @@ class Feature(): spells_prepared = () needs_implementation = False # Set to True if need to find way to compute stats - def __init__(self, owner): + def __init__(self, owner=None): self.owner = owner def __eq__(self, other): diff --git a/dungeonsheets/features/monk.py b/dungeonsheets/features/monk.py index 547ec4e..bb52382 100644 --- a/dungeonsheets/features/monk.py +++ b/dungeonsheets/features/monk.py @@ -46,12 +46,7 @@ class MartialArts(Feature): def __init__(self, owner): self.owner = owner - if self.owner.level >= 5: - self.die = 'd6' - if self.owner.level >= 11: - self.die = 'd8' - if self.owner.level >= 17: - self.die = 'd10' + self.level = owner.Monk.level def weapon_func(self, weapon: weapons.Weapon, char=None, **kwargs): """ @@ -63,6 +58,13 @@ class MartialArts(Feature): return weapon if char is None: return weapon + self.die = 'd4' + if self.level >= 5: + self.die = 'd6' + if self.level >= 11: + self.die = 'd8' + if self.level >= 17: + self.die = 'd10' # check if new damage is better than default if self.die > int(weapon.base_damage.split('d')[-1]): weapon.base_damage = '1d' + str(self.die) @@ -81,15 +83,20 @@ class UnarmoredMovement(Feature): """ name = "Unarmored Movement" source = "Monk" - speed_bonus = 10 def __init__(self, owner): self.owner = owner - if self.owner.level >= 6: - self.speed_bonus = 15 - if self.owner.level >= 10: - self.speed_bonus = 20 - if self.owner.level >= 14: - self.speed_bonus = 25 - if self.owner.level >= 18: - self.speed_bonus = 30 + self.level = owner.Monk.level + + @property + def speed_bonus(self): + _speed_bonus = 10 + if self.level >= 6: + _speed_bonus = 15 + if self.level >= 10: + _speed_bonus = 20 + if self.level >= 14: + _speed_bonus = 25 + if self.level >= 18: + _speed_bonus = 30 + return _speed_bonus diff --git a/dungeonsheets/features/ranger.py b/dungeonsheets/features/ranger.py index 2593eba..69c3f26 100644 --- a/dungeonsheets/features/ranger.py +++ b/dungeonsheets/features/ranger.py @@ -2,18 +2,18 @@ from .features import Feature, FeatureSelector from .. import (weapons, armor) -def select_ranger_fighting_style(feature_choices=[]): +def select_ranger_fighting_style(char=None, feature_choices=[]): lower_choices = [fc for fc in map(str.lower, feature_choices)] if 'archery' in lower_choices: - return Archery() + return Archery(owner=char) elif 'defense' in lower_choices: - return Defense() + return Defense(owner=char) elif 'dueling' in lower_choices: - return Dueling() + return Dueling(owner=char) elif 'two-weapon fighting' in lower_choices: - return TwoWeaponFighting() + return TwoWeaponFighting(owner=char) else: - return RangerFightingStyle() + return RangerFightingStyle(owner=char) class Archery(Feature): diff --git a/dungeonsheets/forms/features_template.tex b/dungeonsheets/forms/features_template.tex index 5869438..9450273 100644 --- a/dungeonsheets/forms/features_template.tex +++ b/dungeonsheets/forms/features_template.tex @@ -5,7 +5,7 @@ \usepackage[dvipsnames]{color} \definecolor{mygrey}{gray}{0.7} -\title{Features and Subclass} +\title{Features and Magic Items} \author{[[ character.name ]]} \date{} @@ -36,4 +36,20 @@ [% endfor %] +[% for mitem in character.magic_items %] + + \section*{[[ mitem.name ]]} + + \noindent + \textbf{Requires Attunement:} [[ mitem.requires_attunement ]] \\ + \textbf{Rarity:} [[ mitem.rarity ]] \\ + + [% if mitem.needs_implementation %] % + \textbf{**Not included in stats on Character Sheet} % + [% endif %] % + + [[ mitem.__doc__|rst_to_latex ]] + + [% endfor %] + \end{document} diff --git a/dungeonsheets/magic_items.py b/dungeonsheets/magic_items.py index 9226e97..d4e00c4 100644 --- a/dungeonsheets/magic_items.py +++ b/dungeonsheets/magic_items.py @@ -6,6 +6,7 @@ class MagicItem(): name = '' ac_bonus = 0 requires_attunement = False + needs_implementation = False rarity = '' diff --git a/dungeonsheets/race.py b/dungeonsheets/race.py index b9a93f6..0bda2cf 100644 --- a/dungeonsheets/race.py +++ b/dungeonsheets/race.py @@ -26,7 +26,7 @@ class Race(): spells_known = () spells_prepared = () - def __init__(self, owner): + def __init__(self, owner=None): self.owner = owner cls = type(self) # Instantiate the features diff --git a/dungeonsheets/stats.py b/dungeonsheets/stats.py index 165df26..7c966ce 100644 --- a/dungeonsheets/stats.py +++ b/dungeonsheets/stats.py @@ -1,7 +1,6 @@ import math from collections import namedtuple from .armor import NoArmor, NoShield, HeavyArmor -from . import (weapons) from .features import (UnarmoredDefenseMonk, UnarmoredDefenseBarbarian, DraconicResilience, Defense, FastMovement, UnarmoredMovement) diff --git a/dungeonsheets/weapons.py b/dungeonsheets/weapons.py index c5f689c..7111370 100644 --- a/dungeonsheets/weapons.py +++ b/dungeonsheets/weapons.py @@ -1,6 +1,3 @@ -from .stats import mod_str - - class Weapon(): name = "" cost = "0 gp" @@ -17,7 +14,7 @@ class Weapon(): def damage(self): dam_str = str(self.base_damage) if self.bonus_damage != 0: - dam_str += mod_str(self.bonus_damage) + dam_str += '{:+d}'.format(self.bonus_damage) return dam_str def __str__(self):