Files
dungeon-sheets/dungeonsheets/create_character.py
T

464 lines
18 KiB
Python
Executable File

#!/usr/bin/env python
"""Launch a system to interactively create a character."""
import logging
# logging.basicConfig(filename='character_creater.log', level=logging.DEBUG)
log = logging.getLogger(__name__)
import math
import os
from random import randint
import subprocess
import npyscreen
import jinja2
from dungeonsheets import character, race, dice, background, classes
def read_version():
version = open(os.path.join(os.path.dirname(__file__), '../VERSION')).read()
version = version.replace('\n', '')
return version
char_classes = {
'Barbarian': classes.Barbarian,
'Bard': classes.Bard,
'Cleric': classes.Cleric,
'Druid': classes.Druid,
'Fighter': classes.Fighter,
'Monk': classes.Monk,
'Paladin': classes.Paladin,
'Ranger': classes.Ranger,
'Rogue': classes.Rogue,
'Sorceror': classes.Sorceror,
'Warlock': classes.Warlock,
'Wizard': classes.Wizard
}
races = {
'Hill Dwarf': race.HillDwarf,
'Mountain Dwarf': race.MountainDwarf,
'High Elf': race.HighElf,
'Wood Elf': race.WoodElf,
'Dark Elf': race.DarkElf,
'Lightfoot Halfling': race.LightfootHalfling,
'Stout Halfling': race.StoutHalfling,
'Human': race.Human,
'Dragonborn': race.Dragonborn,
'Gnome': race.Gnome,
'Forest Gnome': race.ForestGnome,
'Rock Gnome': race.RockGnome,
'Half-Elf': race.HalfElf,
'Half-Orc': race.HalfOrc,
'Tiefling': race.Tiefling,
'Fallen Aasimar': race.FallenAasimar,
}
backgrounds = background.available_backgrounds
backgrounds = {bg.name: bg for bg in backgrounds}
class App(npyscreen.NPSAppManaged):
# STARTING_FORM = 'SKILLS'
character = None
def save_character(self):
# Save the file
filename = self.getForm("SAVE").filename.value
self.character.save(filename)
# Create the PDF character sheet
if self.getForm('SAVE').make_pdf.value:
log.debug("Creating PDF")
subprocess.call(['makesheets', filename])
def add_class(self, NewClass):
log.debug("Adding Class: {:s}".format(NewClass.class_name))
# basic_info = self.getForm('MAIN')
# self.character = NewClass(
# name=basic_info.name.value,
# player_name=basic_info.player_name.value,
# level=int(basic_info.level.value),
# strength=-1, dexterity=-1, constitution=-1,
# intelligence=-1, wisdom=-1, charisma=-1)
self.character.class_list.append(NewClass)
self.update_max_hp()
# Reset form widgets
log.debug("Resetting forms")
self.getForm('ABILITIES').reset()
# @property
# def character_class(self, *args, **kwargs):
# return self.character_class
# @character_class.setter
# def character_class(self, NewClass):
# log.debug("Adding Class: {:s}".format(NewClass.class_name))
# # basic_info = self.getForm('MAIN')
# # self.character = NewClass(
# # name=basic_info.name.value,
# # player_name=basic_info.player_name.value,
# # level=int(basic_info.level.value),
# # strength=-1, dexterity=-1, constitution=-1,
# # intelligence=-1, wisdom=-1, charisma=-1)
# self.character.class_list.append(NewClass)
# self.update_max_hp()
# # Reset form widgets
# log.debug("Resetting forms")
# self.getForm('ABILITIES').reset()
def update_max_hp(self):
# Update max HP based on the class
max_hp_fld = self.getForm('ABILITIES').max_hp
if max_hp_fld.value == '':
# Calculate the new value
hit_dice = [dice.read_dice_str(d)
for d in self.character.hit_dice.split(' + ')]
const = self.character.constitution.modifier
# Assume first hd given is from primary class
max_hp = math.floor(hit_dice[0].faces/2) + const
for hd in hit_dice:
for d in range(hd.num - 1):
max_hp += math.ceil(hd.faces/2) + const
log.debug("Updating max hp: %d", max_hp)
max_hp_fld.value = str(max_hp)
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("CLASS", CharacterClassForm, name="Select your character's primary class:")
self.addForm("SUBCLASS", SubclassForm, name="Select your subclass:")
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:")
class BasicInfoForm(npyscreen.ActionForm):
def create(self):
self.name = self.add(
npyscreen.TitleText, name="Character Name:", use_two_lines=False)
self.player_name = self.add(
npyscreen.TitleText, name="Player Name:", use_two_lines=False)
def on_ok(self):
# Update the default filename
name = self.name.value
if name == '':
filename = 'new_character.py'
else:
filename = f'{name.split(" ")[0].lower()}.py'
save_form = self.parentApp.getForm('SAVE')
save_form.filename.value = filename
# Move to the next form
self.parentApp.setNextForm('RACE')
def on_cancel(self):
raise KeyboardInterrupt
class RaceForm(npyscreen.ActionForm):
def create(self):
self.race = self.add(
npyscreen.TitleMultiLine, name="Race:", values=tuple(races.keys()))
def on_ok(self):
if self.race.value is not None:
selected_race = self.race.values[self.race.value]
SelectedRace = races[selected_race]
log.debug('Selected character race: %s', SelectedRace.name)
self.parentApp.character.race = SelectedRace()
self.parentApp.setNextForm('CLASS')
def on_cancel(self):
self.parentApp.setNextForm('MAIN')
class CharacterClassForm(npyscreen.ActionForm):
char_options = list(char_classes.keys())
def create(self):
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)
self.multiclass = self.add(npyscreen.Checkbox, name="Multiclass?", value=False)
self.character_class = self.add(
npyscreen.TitleMultiLine, name="Class:", values=tuple(self.char_options))
def setup_multiclass(self):
self.character_class.values = self.char_options
self.name = "Select your character's class #{:d}".format(1 + self.parentApp.character.num_classes)
self.multiclass.name = "Add another class?"
self.level.value = '1'
self.subclass.value = False
self.multiclass.value = False
self.character_class.value = None
def on_ok(self):
if self.character_class.value is not None:
# make sure this option can't be selected again
selected_class = self.character_class.values[self.character_class.value]
self.char_options.remove(selected_class)
selected_class = char_classes[selected_class]
log.debug('Selected character class %s', selected_class.class_name)
if self.subclass.value:
sc = self.parentApp.getForm('SUBCLASS')
sc.when_done = 'CLASS' if self.multiclass.value else 'BACKGROUND'
if self.multiclass:
self.setup_multiclass()
sc.prepare_for_class(selected_class, int(self.level.value))
self.parentApp.setNextForm('SUBCLASS')
else:
self.parentApp.add_class(selected_class(level=int(self.level.value),
subclass=None))
if self.multiclass.value:
self.setup_multiclass()
self.parentApp.setNextForm('CLASS')
else:
self.parentApp.setNextForm('BACKGROUND')
def on_cancel(self):
if self.parentApp.character.num_classes > 1:
self.parentApp.setNextForm('BACKGROUND')
self.parentApp.setNextForm('RACE')
class SubclassForm(npyscreen.ActionForm):
subclass_options = ('None',)
parent_class = None
level = 1
when_done = 'BACKGROUND'
name_formatter = "Select your {:s} subclass:"
def create(self):
self.subclass = self.add(
npyscreen.TitleMultiLine, name="Subclass:",
values=tuple(self.subclass_options))
def prepare_for_class(self, parent_class, level):
self.subclass_options = parent_class.subclasses_available or ('None',)
self.parent_class = parent_class
self.level = level
self.name = self.name_formatter.format(parent_class.class_name)
self._clear_all_widgets()
self.create()
def on_ok(self):
if self.subclass.value in [None, '', 'None']:
newclass = self.parent_class(level=self.level,
subclass=None)
else:
newclass = self.parent_class(level=self.level,
subclass=self.subclass.value)
self.parentApp.add_class(newclass)
self.parentApp.setNextForm(self.when_done)
def on_cancel(self):
self.parentApp.setNextForm('CLASS')
class BackgroundForm(npyscreen.ActionForm):
def create(self):
self.background = self.add(
npyscreen.TitleMultiLine,
name="Background:", values=tuple(backgrounds.keys()))
def on_ok(self):
if self.background.value is not None:
selected_bg = self.background.values[self.background.value]
Background = backgrounds[selected_bg]
self.parentApp.character.background = Background()
# Update the languages based on background and race
race_languages = self.parentApp.character.race.languages
languages = Background.languages + race_languages
self.parentApp.character.languages = ', '.join(languages)
log.debug("Selected character background: %s", Background.name)
self.parentApp.setNextForm('ALIGNMENT')
def on_cancel(self):
self.parentApp.setNextForm('CLASS')
class AlignmentForm(npyscreen.ActionForm):
"""Choose your character's alignment."""
alignments = ('Lawful good', 'Neutral good', 'Chaotic good',
'Lawful neutral', 'True neutral', 'Chaotic neutral',
'Lawful evil', 'Neutral evil', 'Chaotic evil', )
def create(self):
self.alignment = self.add(
npyscreen.TitleMultiLine, name="Alignment:", values=self.alignments)
def on_ok(self):
if self.alignment.value is not None:
selected_alignment = self.alignment.values[self.alignment.value]
log.debug('Selected character alignment %s', selected_alignment)
self.parentApp.character.alignment = selected_alignment
self.parentApp.setNextForm('ABILITIES')
def on_cancel(self):
self.parentApp.setNextForm('BACKGROUND')
class AbilityScoreForm(npyscreen.ActionForm):
def roll_dice(self):
"""Get six ability scores that can then be assigned to abilities."""
def roll_score():
# Roll 4 dice and add the 3 highest
rolls = (randint(1, 6) for i in range(4))
score = sum(sorted(rolls)[-3:])
return score
scores = (roll_score() for i in range(6))
return tuple(sorted(scores, reverse=True))
def reset(self):
# Update the character in real time
attrs = ('strength', 'dexterity', 'constitution',
'intelligence', 'wisdom', 'charisma')
for attr in attrs:
getattr(self, attr).value = ''
def while_editing(self):
# Update the character in real time
attrs = ('strength', 'dexterity', 'constitution',
'intelligence', 'wisdom', 'charisma')
for attr in attrs:
fld = getattr(self, attr)
try:
race_bonus = getattr(self.parentApp.character.race, f'{attr}_bonus')
val = int(float(fld.value))
except ValueError:
# Not an integer, so clear the field
fld.value = ''
else:
# Valid number, so process it
curr_val = getattr(self.parentApp.character, attr).value
if val != curr_val:
log.debug("Setting %s to %s", attr, str(val))
setattr(self.parentApp.character, attr, val)
# Update the "character" with new values
if attr == 'constitution':
self.parentApp.update_max_hp()
fld.value = str(val)
# Update the form display
self.display()
def create(self):
self.score_options = self.add(
npyscreen.TitleFixedText, name="Rolls:", editable=False,
value=str(self.roll_dice())[1:-1])
self.add(npyscreen.FixedText, editable=False,
value="Take the six rolls and assign each one to an ability.")
self.add(npyscreen.FixedText, editable=False,
value="Do not add racial bonuses, they will be added for you.")
self.strength = self.add(npyscreen.TitleText, name="Strength:")
self.dexterity = self.add(npyscreen.TitleText, name="Dexterity:")
self.constitution = self.add(npyscreen.TitleText, name="Constitution:")
self.intelligence = self.add(npyscreen.TitleText, name="Intelligence:")
self.wisdom = self.add(npyscreen.TitleText, name="Wisdom:")
self.charisma = self.add(npyscreen.TitleText, name="Charisma:")
self.add(npyscreen.FixedText, editable=False,
value="Maximum hit points initially determined by constitution.")
self.max_hp = self.add(npyscreen.TitleText, name="Max HP:")
def on_ok(self):
self.parentApp.setNextForm('SKILLS')
def on_cancel(self):
self.parentApp.setNextForm('ALIGNMENT')
class SkillForm(npyscreen.ActionForm):
def while_editing(self):
# Update the static skills for race and background
bg_skills = self.parentApp.character.background.skill_proficiencies
self.bg_skills.value = str(bg_skills)[1:-1].replace("'", "")
race_skills = self.parentApp.character.race.skill_proficiencies
self.race_skills.value = str(race_skills)[1:-1].replace("'", "")
# Now set the available discretionary choices
choices = (self.parentApp.character.primary_class.class_skill_choices +
self.parentApp.character.race.skill_choices +
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.update_remaining()
def update_remaining(self, widget=None):
num_choices = (self.parentApp.character.num_skill_choices +
self.parentApp.character.race.num_skill_choices +
self.parentApp.character.background.num_skill_choices)
num_selected = len(self.skill_proficiencies.value)
remaining = num_choices - num_selected
log.debug(f'Remaining: {remaining}')
self.remaining.value = str(remaining)
self.display()
def create(self):
self.bg_skills = self.add(
npyscreen.TitleText, name="Background:",
value="", editable=False)
self.race_skills = self.add(
npyscreen.TitleText, name="Racial:",
value="", editable=False)
self.remaining = self.add(
npyscreen.TitleText, name="Remaining:",
value=0, editable=False)
self.skill_proficiencies = self.add(
npyscreen.TitleMultiSelect, name="Skill Proficiencies:",
values=self.parentApp.character.class_skill_choices,
value_changed_callback=self.update_remaining)
def on_ok(self):
new_skills = self.skill_proficiencies.get_selected_objects()
if new_skills is not None:
new_skills = tuple(s.lower() for s in new_skills)
else:
new_skills = ()
bg_skills = tuple(self.parentApp.character.background.skill_proficiencies)
race_skills = tuple(self.parentApp.character.race.skill_proficiencies)
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('SAVE')
def on_cancel(self):
self.parentApp.setNextForm('ABILITIES')
class SaveForm(npyscreen.ActionForm):
def create(self):
self.filename = self.add(
npyscreen.TitleText, name='Filename:')
self.make_pdf = self.add(npyscreen.Checkbox, name="Create PDF:", value=True)
self.instructions = self.add(
npyscreen.FixedText, editbale=False,
value="After saving, edit this file to finish your personality, etc.")
def on_ok(self):
self.parentApp.setNextForm(None)
def on_cancel(self):
self.parentApp.setNextForm('SKILLS')
def main():
my_app = App()
try:
my_app.run()
except KeyboardInterrupt:
log.error("Aborted by user request")
else:
my_app.save_character()
if __name__ == '__main__':
main()