import random import json # TODO AC? Or Gold Start no AC? # TODO Feats? VOWELS = ["A", "E", "I", "O", "U"] SKILL_LIST = ["acrobatics", "animal_handling", "arcana", "athletics", "deception", "history", "insight", "intimidation", "investigation", "medicine", "nature", "perception", "performance", "persuasion", "religion", "sleight_of_hand", "stealth", "survival"] ATTRIBUTE_LIST = ["strength", "dexterity", "wisdom", "constitution", "intelligence", "charisma"] CLASS_LIST = ["Artificer", "Barbarian", "Bard", "Cleric", "Druid", "Fighter", "Monk", "Paladin", "Ranger", "Rogue", "Sorcerer", "Warlock", "Wizard", "Blood Hunter"] BACKGROUND_LIST = ["Acolyte", "Charlatan", "Criminal / Spy", "Entertainer", "Folk Hero", "Gladiator", "Guild Artisan / Guild Merchant", "Hermit", "Knight", "Noble", "Outlander", "Pirate", "Sage", "Sailor", "Soldier", "Urchin", ] instrument_list = ["Bagpipes", "Birdpipes", "Clarinet", "Drum", "Dulcimer", "Fiddle", "Flute", "Glaur", "Hand Drum", "Harp", "Horn", "Longhorn", "Lute", "Lyre", "Pan Flute", "Shawm", "Songhorn", "Tantan", "Thelarr", "Tocken", "Trumpet", "Viol", "Wargong", "Yarting", "Zulkoon"] languages = ["Aarakocra", "Abyssal", "Aquan", "Auran", "Celestial", "Deep Speech", "Draconic", "Dwarvish", "Elvish", "Giant", "Gith", "Gnomish", "Goblin", "Halfling", "Infernal", "Orc", "Primordial", "Sylvan", "Undercommon", "Vedalken"] known_language_list = ["Common"] GAMING_SETS = ["Bowls", "Darts", "Dice Set", "Dragonchess Set", "Playing Card Set", "Quoits", "Three-Dragon Ante Set"] ARTISANS_TOOLS = ["Alchemist's Supplies", "Brewer's Supplies", "Calligrapher's Supplies", "Carpenter's Tools", "Cartographer's Tools", "Cobbler's Tools", "Cook's Utensils", "Glassblower's Tools", "Jeweler's Tools", "Leatherworker's Tools", "Mason's Tools", "Painter's Supplies", "Potter's Tools", "Smith's Tools", "Tinker's Tools", "Weaver's Tools", "Woodcarver's Tools"] CLERIC_SUBCLASSES = ["Life Domain", "Knowledge Domain", "Light Domain", "Nature Domain", "Tempest Domain", "Trickery Domain", "War Domain", "Death Domain", "Arcana Domain", "Forge Domain", "Grave Domain", "Twilight Domain", "Peace Domain", "Order Domain"] SORCERER_SUBCLASSES = ["Aberrant Mind", "Clockwork Soul", "Divine Soul", "Draconic Bloodline", "Lunar Sorcery", "Shadow Magic", "Storm Sorcery", "Wild Magic"] WIZARD_SUBCLASSES = ["Bladesinging", "Chronurgy Magic", "Graviturgy Magic", "Order of Scribes", "School of Abjuration", "School of Conjuration", "School of Divination", "School of Enchantment", "School of Evocation", "School of Illusion", "School of Necromancy", "School of Transmutation", "War Magic"] class HeroCreation: def __init__(self): self.name_generation() self.race_selection() self.race = self.race_string["Race"] # 'job' is used in place of 'class' to avoid a function conflict self.job = random.choice(CLASS_LIST) self.job_characteristics() self.background = random.choice(BACKGROUND_LIST) self.stat_rolls() self.racial_bonuses() self.speed = self.race_string["Speed"] self.article = self.grammar() self.skill_generation() self.skill_proficiency() self.instruments = "None" self.artisan_tools = "None" self.additional_tools = "None" self.armor_proficiency = "None" self.race_proficiencies() self.job_proficiencies() self.background_proficiencies() self.finalize_languages() self.apply_skill_modifiers("dexterity", "initiative") self.whoami() self.stat_block = (f"Stat Block\n----------\nCharisma: {self.charisma}\nConstitution: {self.constitution}\n" f"Dexterity: {self.dexterity}\nIntelligence: {self.intelligence}\n" f"Strength: {self.strength}\nWisdom: {self.wisdom}\n") self.saves = (f"Saving Throws\n----------\nCharisma Save: {self.charisma_save}\n" f"Constitution Save: {self.constitution_save}\n" f"Dexterity Save: {self.dexterity_save}\n" f"Intelligence Save: {self.intelligence_save}\n" f"Strength Save: {self.strength_save}\n" f"Wisdom Save: {self.wisdom_save}\n") self.additional_stats = ( f"Additional Stats\n----------\nStarting HP: {self.starting_hp}\nHit Die: {self.hit_die}\n" f"Initiative Bonus: {self.initiative}\nSpeed: {self.speed}\n") self.skill_bonuses = ( f"Skill Bonuses\n----------\nAcrobatics: {self.acrobatics}\nAnimal Handling: {self.animal_handling}" f"\nArcana: {self.arcana}\nAthletics: {self.athletics}\nDeception: {self.deception}" f"\nHistory: {self.history}\nInsight: {self.insight}\nIntimidation: {self.intimidation}" f"\nInvestigation: {self.investigation}\nMedicine: {self.medicine}\nNature: {self.nature}" f"\nPerception: {self.perception}\nPerformance: {self.performance}\nPersuasion: {self.persuasion}" f"\nReligion: {self.religion}\nSleight of Hand: {self.sleight_of_hand}" f"\nStealth: {self.stealth}\nSurvival: {self.survival}\n") self.other_proficiencies = (f"Additional Proficiencies\n----------\n" f"Languages: {self.languages}\n" f"Instruments: {self.instruments}\n" f"Artisan's Tools: {self.artisan_tools}\nAdditional Tools: " f"{self.additional_tools}\n" f"Weapon Proficiencies: {self.weapon_proficiency}\n" f"Armor Proficiencies: {self.armor_proficiency}\n") # Assign the correct a/an article in the 'whoami' attr def grammar(self): if self.race[0] in VOWELS: return "an" else: return "a" def race_selection(self): with open('races.json') as json_file: race_data = json.load(json_file) chosen_race = random.choice(race_data) setattr(self, "race_string", chosen_race) def stat_rolls(self): # Configured to "Re-roll" 1s # To include 1s, increase the randint range to 1,6 for attribute in ATTRIBUTE_LIST: roll_list = [] for each in range(4): roll_list.append(random.randint(2, 6)) roll_list.remove(min(roll_list)) stat_total = sum(roll_list) setattr(self, f"{attribute}", stat_total) # Assign racial stat bonuses def racial_bonuses(self): self.charisma += self.race_string["Charisma"] self.constitution += self.race_string["Constitution"] self.dexterity += self.race_string["Dexterity"] self.intelligence += self.race_string["Intelligence"] self.strength += self.race_string["Strength"] self.wisdom += self.race_string["Wisdom"] def name_generation(self): with open("names.txt", "r") as f: name_list = f.read().splitlines() first_name = random.choice(name_list) sur_name = random.choice(name_list) combined_name = f"{first_name} {sur_name}" setattr(self, "name", combined_name) # Created skill attributes and save attributes with a base value of +0 to the roll def skill_generation(self): for skill in SKILL_LIST: setattr(self, skill, 0) if skill == "athletics": self.apply_skill_modifiers("strength", skill) elif skill in ["acrobatics", "sleight_of_hand", "stealth"]: self.apply_skill_modifiers("dexterity", skill) elif skill in ["arcana", "history", "investigation", "nature", "religion"]: self.apply_skill_modifiers("intelligence", skill) elif skill in ["animal_handling", "insight", "medicine", "perception", "survival"]: self.apply_skill_modifiers("wisdom", skill) elif skill in ["deception", "intimidation", "performance", "persuasion"]: self.apply_skill_modifiers("charisma", skill) # Initialization of saving throw modifiers for attribute in ATTRIBUTE_LIST: save_name = f"{attribute}_save" setattr(self, save_name, 0) self.apply_skill_modifiers(attribute, save_name) def apply_skill_modifiers(self, attribute_name, skill_name): attribute = getattr(self, attribute_name) if attribute == 1: modifier = -5 elif attribute in [2, 3]: modifier = -4 elif attribute in [4, 5]: modifier = -3 elif attribute in [6, 7]: modifier = -2 elif attribute in [8, 9]: modifier = -1 elif attribute in [10, 11]: modifier = 0 elif attribute in [12, 13]: modifier = 1 elif attribute in [14, 15]: modifier = 2 elif attribute in [16, 17]: modifier = 3 elif attribute in [18, 19]: modifier = 4 elif attribute in [20, 21]: modifier = 5 else: modifier = 0 setattr(self, skill_name, modifier) def skill_proficiency(self): with open('backgrounds.json') as json_file: background_data = json.load(json_file) # Is this actually a dict? background_dict = background_data[self.background]["Skills"] for skill in background_dict: current_score = getattr(self, skill) new_score = str(current_score + 2) setattr(self, skill, f"{new_score} (Prof)") with open('jobs.json') as json_file: job_data = json.load(json_file) # Is this actually a dict? job_dict = job_data[self.job] proficiencies = job_dict["Proficiencies"] job_skill_list = job_dict["Skills"] for skill in background_dict: if skill in job_skill_list: job_skill_list.remove(skill) # rogue list for expertise calculation rogue_proficient_list = [] for each in range(proficiencies): proficient_skill = random.choice(job_skill_list) if self.job == "Rogue": rogue_proficient_list.append(proficient_skill) job_skill_list.remove(proficient_skill) current_score = getattr(self, proficient_skill) setattr(self, f"{proficient_skill}_base_value", current_score) new_score = str(current_score + 2) setattr(self, proficient_skill, f"{new_score} (Prof)") if self.job == "Rogue": expertise_skill = random.choice(rogue_proficient_list) score_value = getattr(self, f"{expertise_skill}_base_value") new_score = score_value + 4 setattr(self, expertise_skill, f"{new_score} (Expertise)") def job_characteristics(self): with open('jobs.json') as json_file: job_data = json.load(json_file) hit_die_value = job_data[self.job]["Hit Die"] setattr(self, "hit_die", hit_die_value) starting_hp_value = job_data[self.job]["Starting HP"] setattr(self, "starting_hp", starting_hp_value) def job_proficiencies(self): with open('jobs.json') as json_file: job_data = json.load(json_file) if self.job == "Monk": tool_choice = random.randint(0, 1) if tool_choice == 0: instrument_value = 1 else: artisan_tool_value = 1 else: try: instrument_value = job_data[self.job]["Instruments"] except KeyError: pass try: artisan_tool_value = job_data[self.job]["Artisan's Tools"] except KeyError: pass instrument_prof_list = [] artisan_prof_list = [] try: for value in range(0, instrument_value): new_instrument = random.choice(instrument_list) instrument_list.remove(new_instrument) instrument_prof_list.append(new_instrument) instrument_string = ", ".join(instrument_prof_list) setattr(self, "instruments", str(instrument_string)) except UnboundLocalError: pass try: for value in range(0, artisan_tool_value): new_tool = random.choice(ARTISANS_TOOLS) ARTISANS_TOOLS.remove(new_tool) artisan_prof_list.append(new_tool) artisan_tool_string = ", ".join(artisan_prof_list) setattr(self, "artisan_tools", str(artisan_tool_string)) except UnboundLocalError: pass try: additional_tool_list = job_data[self.job]["Additional Tools"] additional_tool_string = ", ".join(additional_tool_list) setattr(self, "additional_tools", str(additional_tool_string)) except KeyError: pass armor_prof_list = job_data[self.job]["Armor Proficiency"] if not armor_prof_list: setattr(self, "armor_proficiency", "None") else: armor_prof_string = ", ".join(armor_prof_list) setattr(self, "armor_proficiency", armor_prof_string) weapon_prof_list = job_data[self.job]["Weapon Proficiency"] weapon_prof_string = ", ".join(weapon_prof_list) setattr(self, "weapon_proficiency", weapon_prof_string) for attribute in job_data[self.job]["Saving Throws"]: current_score = getattr(self, f"{attribute}_save") new_score = str(current_score + 2) setattr(self, f"{attribute}_save", f"{new_score} (Prof)") def background_proficiencies(self): global known_language_list with open('backgrounds.json') as json_file: background_data = json.load(json_file) # background instrument proficiencies try: instrument_value = background_data[self.background]["Additional Proficiencies"]["Instrument"] except KeyError or UnboundLocalError: pass for value in range(0, instrument_value): new_instrument = random.choice(instrument_list) current_instruments = getattr(self, "instruments") if current_instruments == "None": setattr(self, "instruments", f"{new_instrument}") else: setattr(self, "instruments", current_instruments + f", {new_instrument}") # background artisan's tools proficiencies try: tool_value = background_data[self.background]["Additional Proficiencies"]["Artisan's Tool"] except KeyError or UnboundLocalError: pass for value in range(0, tool_value): new_tool = random.choice(ARTISANS_TOOLS) current_tools = getattr(self, "artisan_tools") if current_tools == "None": setattr(self, "artisan_tools", f"{new_tool}") else: setattr(self, "artisan_tools", current_tools + f", {new_tool}") # language proficiencies try: language_value = background_data[self.background]["Additional Proficiencies"]["Languages"] except UnboundLocalError: pass for value in range(0, language_value): try: new_language = random.choice(languages) known_language_list.append(new_language) languages.remove(new_language) except IndexError: pass # additional tool proficiencies additional_tool_list = background_data[self.background]["Additional Proficiencies"]["Additional Tools"] if not additional_tool_list: pass else: current_tools = getattr(self, "additional_tools") if current_tools == "None": setattr(self, "additional_tools", ", ".join(additional_tool_list)) else: for tool in additional_tool_list: if tool in current_tools: pass else: new_tool_list = getattr(self, "additional_tools") + f", {tool}" setattr(self, "additional_tools", new_tool_list) def race_proficiencies(self): global known_language_list try: known_languages = self.race_string["Languages"] for language in known_languages: known_language_list.append(language) print(language) languages.remove(language) except KeyError: pass for value in range(0, self.race_string["AdditionalLanguages"]): try: new_language = random.choice(languages) known_language_list.append(new_language) languages.remove(new_language) except IndexError: pass def finalize_languages(self): global known_language_list language_string = ", ".join(known_language_list) setattr(self, "languages", language_string) def whoami(self): if self.job == "Cleric": subclass = random.choice(CLERIC_SUBCLASSES) setattr(self,"subclass",subclass) print(f"Your new character is {self.name}, {self.article} {self.race} {self.job}, with the " f"{self.background} background and the {self.subclass} subclass.\n") elif self.job == "Sorcerer": subclass = random.choice(SORCERER_SUBCLASSES) setattr(self, "subclass", subclass) print(f"Your new character is {self.name}, {self.article} {self.race} {self.job}, with the " f"{self.background} background and the {self.subclass} subclass.\n") elif self.job == "Wizard": subclass = random.choice(WIZARD_SUBCLASSES) setattr(self, "subclass", subclass) print(f"Your new character is {self.name}, {self.article} {self.race} {self.job}, with the " f"{self.background} background and the {self.subclass} subclass.\n") else: print(f"Your new character is {self.name}, {self.article} {self.race} {self.job}, with the " f"{self.background} background.\n") # Used for interactively moving around stats in case of a bad allocation. # Disabling for now, as this project is intended to be one-command and self-contained # def stat_moves(self): # finished = False # while not finished: # move_request = input("Would you like to move any of these stats around? (y/n)").lower() # if move_request == "y": # stat_1 = input("What is the first stat you would like to move? Please enter the full stat # name.").lower() # stat_2 = input("What is the second stat you would like to move? Please enter the full stat # name.").lower() # if hasattr(self, stat_1) and hasattr(self, stat_2): # stat_1_value = getattr(self, stat_1) # stat_2_value = getattr(self, stat_2) # setattr(self, stat_1, stat_2_value) # setattr(self, stat_2, stat_1_value) # self.update_stat_block() # print(self.whoami) # print(self.stat_block) # elif move_request == "n": # finished = True # Currently only used for the stat_moves functionality. Disabling for now # def update_stat_block(self): # self.stat_block = (f"Stat Block\n---------\nCharisma: {self.charisma}\nConstitution: {self.constitution}\n" # f"Dexterity: {self.dexterity}\nIntelligence: {self.intelligence}\n" # f"Strength: {self.strength}\nWisdom: {self.wisdom}")