From 1bd40ef9cdcfc8947f3f00d0d291ca383c287c26 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2019 10:34:05 +0100 Subject: [PATCH] Second commit --- requirements.txt | 4 + {{packname} => rpgpack}/__init__.py | 4 +- rpgpack/commands/__init__.py | 26 ++ rpgpack/commands/dice.py | 40 +++ rpgpack/commands/dndactive.py | 56 ++++ rpgpack/commands/dndedit.py | 35 +++ rpgpack/commands/dndinfo.py | 19 ++ rpgpack/commands/dnditem.py | 56 ++++ rpgpack/commands/dndnew.py | 71 +++++ rpgpack/commands/dndroll.py | 146 ++++++++++ rpgpack/commands/dndspell.py | 114 ++++++++ rpgpack/commands/roll.py | 28 ++ {{packname} => rpgpack}/stars/__init__.py | 5 +- {{packname} => rpgpack}/tables/__init__.py | 6 +- rpgpack/tables/dndactivecharacters.py | 26 ++ rpgpack/tables/dndcharacters.py | 301 +++++++++++++++++++++ rpgpack/utils/__init__.py | 4 + rpgpack/utils/dndproficiencytype.py | 11 + rpgpack/utils/parse5etoolsentry.py | 31 +++ rpgpack/version.py | 4 + setup.py | 13 +- {packname}/commands/__init__.py | 10 - 22 files changed, 987 insertions(+), 23 deletions(-) rename {{packname} => rpgpack}/__init__.py (87%) create mode 100644 rpgpack/commands/__init__.py create mode 100644 rpgpack/commands/dice.py create mode 100644 rpgpack/commands/dndactive.py create mode 100644 rpgpack/commands/dndedit.py create mode 100644 rpgpack/commands/dndinfo.py create mode 100644 rpgpack/commands/dnditem.py create mode 100644 rpgpack/commands/dndnew.py create mode 100644 rpgpack/commands/dndroll.py create mode 100644 rpgpack/commands/dndspell.py create mode 100644 rpgpack/commands/roll.py rename {{packname} => rpgpack}/stars/__init__.py (70%) rename {{packname} => rpgpack}/tables/__init__.py (60%) create mode 100644 rpgpack/tables/dndactivecharacters.py create mode 100644 rpgpack/tables/dndcharacters.py create mode 100644 rpgpack/utils/__init__.py create mode 100644 rpgpack/utils/dndproficiencytype.py create mode 100644 rpgpack/utils/parse5etoolsentry.py create mode 100644 rpgpack/version.py delete mode 100644 {packname}/commands/__init__.py diff --git a/requirements.txt b/requirements.txt index e69de29b..cd3f3a4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,4 @@ +royalnet>=5.0a1 +aiohttp>=3.6.2 +dice>=2.4.2 +sortedcontainers>=2.1.0 diff --git a/{packname}/__init__.py b/rpgpack/__init__.py similarity index 87% rename from {packname}/__init__.py rename to rpgpack/__init__.py index 8d53497f..aa4b1d96 100644 --- a/{packname}/__init__.py +++ b/rpgpack/__init__.py @@ -1,6 +1,6 @@ # This is a template Pack __init__. You can use this without changing anything in other packages too! -from . import commands, tables, stars +from . import commands, tables, stars, version from .commands import available_commands from .tables import available_tables from .stars import available_page_stars, available_exception_stars @@ -9,9 +9,9 @@ __all__ = [ "commands", "tables", "stars", + "version", "available_commands", "available_tables", "available_page_stars", "available_exception_stars", ] - diff --git a/rpgpack/commands/__init__.py b/rpgpack/commands/__init__.py new file mode 100644 index 00000000..aa045b90 --- /dev/null +++ b/rpgpack/commands/__init__.py @@ -0,0 +1,26 @@ +# Imports go here! +from .roll import RollCommand +from .dice import DiceCommand +from .dndactive import DndactiveCommand +from .dndinfo import DndinfoCommand +from .dndnew import DndnewCommand +from .dndedit import DndeditCommand +from .dndroll import DndrollCommand +from .dnditem import DnditemCommand +from .dndspell import DndspellCommand + +# Enter the commands of your Pack here! +available_commands = [ + RollCommand, + DiceCommand, + DndactiveCommand, + DndinfoCommand, + DndnewCommand, + DndeditCommand, + DndrollCommand, + DnditemCommand, + DndspellCommand, +] + +# Don't change this, it should automatically generate __all__ +__all__ = [command.__name__ for command in available_commands] diff --git a/rpgpack/commands/dice.py b/rpgpack/commands/dice.py new file mode 100644 index 00000000..f75d8388 --- /dev/null +++ b/rpgpack/commands/dice.py @@ -0,0 +1,40 @@ +import dice +from royalnet.commands import * + + +class DiceCommand(Command): + name: str = "dice" + + description: str = "Roll a dice, using 'dice'." + + syntax = "{dice}" + + aliases = ["d"] + + async def run(self, args: CommandArgs, data: CommandData) -> None: + dice_str = args.joined(require_at_least=1) + try: + roll = dice.roll(dice_str) + except dice.DiceFatalException as e: + raise CommandError(e.msg) + except dice.DiceException as e: + raise CommandError(e.msg) + except dice.DiceBaseException as e: + raise CommandError(str(e)) + try: + result = list(roll) + except TypeError: + result = [roll] + message = f"๐ŸŽฒ {dice_str}" + total = 0 + if len(result) > 1: + message += f" = " + for index, die in enumerate(result): + message += f"{die}" + total += int(die) + if (index + 1) < len(result): + message += "+" + else: + total += int(result[0]) + message += f" = [b]{total}[/b]" + await data.reply(message) diff --git a/rpgpack/commands/dndactive.py b/rpgpack/commands/dndactive.py new file mode 100644 index 00000000..876b1369 --- /dev/null +++ b/rpgpack/commands/dndactive.py @@ -0,0 +1,56 @@ +from royalnet.commands import * +from royalnet.utils import asyncify +from ..tables import DndCharacter, DndActiveCharacter + + +class DndactiveCommand(Command): + name: str = "dndactive" + + description: str = "Set a DnD character as active." + + aliases = ["da", "dnda", "active", "dactive"] + + syntax = "{name|id}" + + tables = {DndCharacter, DndActiveCharacter} + + async def run(self, args: CommandArgs, data: CommandData) -> None: + identifier = args.optional(0) + author = await data.get_author(error_if_none=True) + if identifier is None: + # Display the active character + if author.dnd_active_character is None: + await data.reply("โ„น๏ธ You have no active characters.") + else: + await data.reply(f"โ„น๏ธ You currently active character is [b]{author.dnd_active_character}[/b].") + return + try: + identifier = int(identifier) + except ValueError: + # Find the character by name + chars = await asyncify(data.session.query(self.alchemy.DndCharacter).filter_by(name=identifier).all) + if len(chars) >= 2: + char_string = "\n".join([f"[c]{char.character_id}[/c] (LV {char.level}) by {char.creator})" for char in chars]) + raise CommandError(f"Multiple characters share the name {identifier}, " + f"please activate them using their id:\n{char_string}") + elif len(chars) == 1: + char = chars[0] + else: + char = None + else: + # Find the character by id + char = await asyncify(data.session.query(self.alchemy.DndCharacter) + .filter_by(character_id=identifier) + .one_or_none) + if char is None: + raise CommandError("No character found.") + # Check if the player already has an active character + if author.dnd_active_character is None: + # Create a new active character + achar = self.alchemy.DndActiveCharacter(character=char, user=author) + data.session.add(achar) + else: + # Change the active character + author.dnd_active_character.character = char + await data.session_commit() + await data.reply(f"โœ… Active character set to [b]{char}[/b]!") diff --git a/rpgpack/commands/dndedit.py b/rpgpack/commands/dndedit.py new file mode 100644 index 00000000..918e431d --- /dev/null +++ b/rpgpack/commands/dndedit.py @@ -0,0 +1,35 @@ +import re +from royalnet.commands import * +from .dndnew import DndnewCommand +from ..tables import DndCharacter, DndActiveCharacter + + +class DndeditCommand(DndnewCommand): + name: str = "dndedit" + + description: str = "Edit the active DnD character." + + aliases = ["de", "dnde", "edit", "dedit"] + + tables = {DndCharacter, DndActiveCharacter} + + async def run(self, args: CommandArgs, data: CommandData) -> None: + character_sheet = args.joined() + + if character_sheet == "": + await data.reply(self._syntax()) + return + + author = await data.get_author(error_if_none=True) + if author.dnd_active_character is None: + raise CommandError("You don't have an active character.") + + char: DndCharacter = author.dnd_active_character.character + + arguments = self._parse(character_sheet) + for key in arguments: + char.__setattr__(key, arguments[key]) + + await data.session_commit() + + await data.reply(f"โœ… Edit successful!") diff --git a/rpgpack/commands/dndinfo.py b/rpgpack/commands/dndinfo.py new file mode 100644 index 00000000..865955db --- /dev/null +++ b/rpgpack/commands/dndinfo.py @@ -0,0 +1,19 @@ +from royalnet.commands import * +from royalnet.utils import asyncify +from ..tables import DndCharacter, DndActiveCharacter + + +class DndinfoCommand(Command): + name: str = "dndinfo" + + description: str = "Display the character sheet of the active DnD character." + + aliases = ["di", "dndi", "info", "dinfo"] + + tables = {DndCharacter, DndActiveCharacter} + + async def run(self, args: CommandArgs, data: CommandData) -> None: + author = await data.get_author(error_if_none=True) + if author.dnd_active_character is None: + raise CommandError("You don't have an active character.") + await data.reply(author.dnd_active_character.character.character_sheet()) diff --git a/rpgpack/commands/dnditem.py b/rpgpack/commands/dnditem.py new file mode 100644 index 00000000..3aa470b3 --- /dev/null +++ b/rpgpack/commands/dnditem.py @@ -0,0 +1,56 @@ +import aiohttp +import sortedcontainers +from royalnet.commands import * +from ..utils import parse_5etools_entry + + +class DnditemCommand(Command): + name: str = "dnditem" + + aliases = ["item"] + + description: str = "Ottieni informazioni su un oggetto di D&D5e." + + syntax = "{nomeoggetto}" + + _dnddata: sortedcontainers.SortedKeyList = None + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + interface.loop.create_task(self._fetch_dnddata()) + + async def _fetch_dnddata(self): + self._dnddata = self._dnddata = sortedcontainers.SortedKeyList([], key=lambda i: i["name"].lower()) + async with aiohttp.ClientSession() as session: + async with session.get("https://5e.tools/data/items.json") as response: + j = await response.json() + for item in j["item"]: + self._dnddata.add(item) + async with session.get("https://5e.tools/data/fluff-items.json") as response: + j = await response.json() + for item in j["item"]: + self._dnddata.add(item) + async with session.get("https://5e.tools/data/items-base.json") as response: + j = await response.json() + for item in j["baseitem"]: + self._dnddata.add(item) + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if self._dnddata is None: + await data.reply("โš ๏ธ Il database degli oggetti di D&D non รจ ancora stato scaricato.") + return + search = args.joined().lower() + result = self._dnddata[self._dnddata.bisect_key_left(search)] + string = f'๐Ÿ“ฆ [b]{result["name"]}[/b]\n' + if "source" in result: + string += f'[i]{result["source"]}, page {result["page"]}[/i]\n' + string += f'\n' \ + f'Type: [b]{result.get("type", "None")}[/b]\n' \ + f'Value: [b]{result.get("value", "-")}[/b]\n' \ + f'Weight: [b]{result.get("weight", "0")} lb[/b]\n' \ + f'Rarity: [b]{result["rarity"] if result.get("rarity", "None") != "None" else "Mundane"}[/b]\n' \ + f'\n' + for entry in result.get("entries", []): + string += parse_5etools_entry(entry) + string += "\n\n" + await data.reply(string) diff --git a/rpgpack/commands/dndnew.py b/rpgpack/commands/dndnew.py new file mode 100644 index 00000000..28754404 --- /dev/null +++ b/rpgpack/commands/dndnew.py @@ -0,0 +1,71 @@ +import re +# noinspection PyUnresolvedReferences +from royalnet.commands import * +from ..tables import DndCharacter +from ..utils import DndProficiencyType + + +class DndnewCommand(Command): + name: str = "dndnew" + + description: str = "Create a new DnD character." + + aliases = ["dn", "dndn", "new", "dnew"] + + syntax = "{name}\n{character_sheet}" + + tables = {DndCharacter} + + def _search_value(self, name: str, string: str): + return re.search(r"\s*" + name + r"\s*([0-9]+)\s*", string, re.IGNORECASE) + + def _parse(self, character_sheet: str) -> dict: + columns = list(self.alchemy.DndCharacter.__table__.columns) + column_names = [column.name for column in columns if (not column.primary_key and + not column.foreign_keys and + column.name != "name")] + arguments = {} + for column_name in column_names: + match = self._search_value(column_name, character_sheet) + if match: + if column_name.endswith("_proficiency"): + arguments[column_name] = DndProficiencyType(float(match.group(1))) + else: + arguments[column_name] = match.group(1) + return arguments + + def _syntax(self) -> str: + columns = list(self.alchemy.DndCharacter.__table__.columns) + column_names = [column.name for column in columns if (not column.primary_key and + not column.foreign_keys and + column.name != "name")] + message = "โ„น๏ธ How to create a new character:\n[p]/dndnew YOUR_CHARACTER_NAME\n" + for column_name in column_names: + message += f"{column_name} _\n" + message += "[/p]" + return message + + async def run(self, args: CommandArgs, data: CommandData) -> None: + character_sheet = args.joined() + + if character_sheet == "": + await data.reply(self._syntax()) + return + + creator = await data.get_author() + + name, rest = character_sheet.split("\n", 1) + + character = self.alchemy.DndCharacter(name=name, creator=creator, **self._parse(rest)) + data.session.add(character) + + try: + await data.session_commit() + except Exception as err: + # THIS IS INTENDED + if err.__class__.__name__ == "IntegrityError": + param_name = re.search(r'in column "(\S+)"', err.args[0]).group(1) + raise CommandError(f"Mandatory parameter '{param_name}' is missing.") + raise + + await data.reply(f"โœ… Character [b]{character.name}[/b] created!") diff --git a/rpgpack/commands/dndroll.py b/rpgpack/commands/dndroll.py new file mode 100644 index 00000000..2daf07f1 --- /dev/null +++ b/rpgpack/commands/dndroll.py @@ -0,0 +1,146 @@ +import re +import random +from royalnet.commands import * +from ..tables import DndCharacter, DndActiveCharacter +from royalnet.utils import plusformat + + +class DndrollCommand(Command): + name: str = "dndroll" + + description: str = "Roll dice as the active DnD character." + + aliases = ["dr", "dndr", "roll", "droll"] + + tables = {DndCharacter, DndActiveCharacter} + + _skill_names = { + "str": "strength", + "for": "strength", + "dex": "dexterity", + "des": "dexterity", + "con": "constitution", + "cos": "constitution", + "inte": "intelligence", + "wis": "wisdom", + "sag": "wisdom", + "cha": "charisma", + "car": "charisma", + + "ststr": "strength_save", + "stfor": "strength_save", + "stdex": "dexterity_save", + "stdes": "dexterity_save", + "stcon": "constitution_save", + "stcos": "constitution_save", + "stint": "intelligence_save", + "stwis": "wisdom_save", + "stsag": "wisdom_save", + "stcha": "charisma_save", + "stcar": "charisma_save", + + "tsstr": "strength_save", + "tsfor": "strength_save", + "tsdex": "dexterity_save", + "tsdes": "dexterity_save", + "tscon": "constitution_save", + "tscos": "constitution_save", + "tsint": "intelligence_save", + "tswis": "wisdom_save", + "tssag": "wisdom_save", + "tscha": "charisma_save", + "tscar": "charisma_save", + + "acr": "acrobatics", + "add": "animal_handling", + "ani": "animal_handling", + "arc": "arcana", + "ath": "athletics", + "dec": "deception", + "ing": "deception", + "his": "history", + "sto": "history", + "ins": "insight", + "intu": "insight", + "inti": "intimidation", + "inv": "investigation", + "med": "medicine", + "nat": "nature", + "perc": "perception", + "perf": "performance", + "pers": "persuasion", + "rel": "religion", + "sle": "sleight_of_hand", + "soh": "sleight_of_hand", + "rap": "sleight_of_hand", + "ste": "stealth", + "nas": "stealth", + "sur": "survival", + "sop": "sopravvivenza", + } + + async def run(self, args: CommandArgs, data: CommandData) -> None: + author = await data.get_author(error_if_none=True) + if author.dnd_active_character is None: + raise CommandError("You don't have an active character.") + char: DndCharacter = author.dnd_active_character.character + + first = args[0] + second = args.optional(1) + third = args.optional(2) + + advantage = False + disadvantage = False + extra_modifier = 0 + + if third: + try: + extra_modifier = int(third) + except ValueError: + raise InvalidInputError("Invalid modifier value (third parameter).") + if second.startswith("a") or second.startswith("v"): + advantage = True + elif second.startswith("d") or second.startswith("d"): + disadvantage = True + else: + raise InvalidInputError("Invalid advantage string (second parameter).") + + elif second: + try: + extra_modifier = int(second) + except ValueError: + if second.startswith("a") or second.startswith("v"): + advantage = True + elif second.startswith("d") or second.startswith("d"): + disadvantage = True + else: + raise InvalidInputError("Invalid modifier value or advantage string (second parameter).") + + skill_short_name = first.lower() + for root in self._skill_names: + if skill_short_name.startswith(root): + skill_name = self._skill_names[root] + break + else: + raise CommandError("Invalid skill name (first parameter).") + + skill_modifier = char.__getattribute__(skill_name) + modifier = skill_modifier + extra_modifier + modifier_str = plusformat(modifier, empty_if_zero=True) + + if advantage: + roll_a = random.randrange(1, 21) + roll_b = random.randrange(1, 21) + roll = max([roll_a, roll_b]) + total = roll + modifier + await data.reply(f"๐ŸŽฒ 2d20h1{modifier_str} = ({roll_a}|{roll_b}){modifier_str} = [b]{total}[/b]") + elif disadvantage: + roll_a = random.randrange(1, 21) + roll_b = random.randrange(1, 21) + roll = min([roll_a, roll_b]) + total = roll + modifier + await data.reply(f"๐ŸŽฒ 2d20l1{modifier_str} = ({roll_a}|{roll_b}){modifier_str} = [b]{total}[/b]") + else: + roll = random.randrange(1, 21) + total = roll + modifier + await data.reply(f"๐ŸŽฒ 1d20{modifier_str} = {roll}{modifier_str} = [b]{total}[/b]") diff --git a/rpgpack/commands/dndspell.py b/rpgpack/commands/dndspell.py new file mode 100644 index 00000000..6d645f06 --- /dev/null +++ b/rpgpack/commands/dndspell.py @@ -0,0 +1,114 @@ +import aiohttp +import sortedcontainers +from royalnet.commands import * +from royalnet.utils import ordinalformat, andformat +from ..utils import parse_5etools_entry + + +class DndspellCommand(Command): + name: str = "dndspell" + + aliases = ["spell"] + + description: str = "Ottieni informazioni su una magia di D&D5e." + + syntax = "{nomemagia}" + + _dnddata: sortedcontainers.SortedKeyList = None + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + interface.loop.create_task(self._fetch_dnddata()) + + async def _fetch_dnddata(self): + self._dnddata = self._dnddata = sortedcontainers.SortedKeyList([], key=lambda i: i["name"].lower()) + async with aiohttp.ClientSession() as session: + for url in [ + "https://5e.tools/data/spells/spells-ai.json", + "https://5e.tools/data/spells/spells-ggr.json", + "https://5e.tools/data/spells/spells-llk.json", + "https://5e.tools/data/spells/spells-phb.json", + "https://5e.tools/data/spells/spells-scag.json", + "https://5e.tools/data/spells/spells-stream.json", + "https://5e.tools/data/spells/spells-ua-ar.json", + "https://5e.tools/data/spells/spells-ua-mm.json", + "https://5e.tools/data/spells/spells-ua-ss.json", + "https://5e.tools/data/spells/spells-ua-tobm.json", + "https://5e.tools/data/spells/spells-xge.json" + ]: + async with session.get(url) as response: + j = await response.json() + for spell in j["spell"]: + self._dnddata.add(spell) + + @staticmethod + def _parse_spell(spell: dict) -> str: + string = f'โœจ [b]{spell["name"]}[/b]\n' + if "source" in spell: + string += f'[i]{spell["source"]}, page {spell["page"]}[/i]\n' + string += "\n" + if spell["level"] == 0: + string += f'[b]Cantrip[/b] {spell["school"]}\n' + else: + string += f'[b]{ordinalformat(spell["level"])}[/b] level {spell["school"]}\n' + if "time" in spell: + for time in spell["time"]: + string += f'Cast time: โŒ›๏ธ [b]{time["number"]} {time["unit"]}[/b]\n' + if "range" in spell: + if spell["range"]["distance"]["type"] == "touch": + string += "Range: ๐Ÿ‘‰ [b]Touch[/b]\n" + elif spell["range"]["distance"]["type"] == "self": + string += "Range: ๐Ÿ‘ค [b]Self[/b]\n" + else: + string += f'Range: ๐Ÿน [b]{spell["range"]["distance"]["amount"]} {spell["range"]["distance"]["type"]}[/b] ({spell["range"]["type"]})\n' + if "components" in spell: + string += f'Components: ' + if spell["components"].get("v", False): + string += "๐Ÿ‘„ [b]Verbal[/b] | " + if spell["components"].get("s", False): + string += "๐Ÿค™ [b]Somatic[/b] | " + if spell["components"].get("r", False): + # TODO: wtf is this + string += "โ“ [b]R...?[/b] | " + if spell["components"].get("m", False): + if "text" in spell["components"]["m"]: + string += f'๐Ÿ’Ž [b]Material[/b] ([i]{spell["components"]["m"]["text"]}[/i]) | ' + else: + string += f'๐Ÿ’Ž [b]Material[/b] ([i]{spell["components"]["m"]}[/i]) | ' + string = string.rstrip(" ").rstrip("|") + string += "\n" + string += "\n" + if "duration" in spell: + for duration in spell["duration"]: + if duration["type"] == "timed": + string += f'Duration: ๐Ÿ•’ [b]{duration["duration"]["amount"]} {duration["duration"]["type"]}[/b]' + elif duration["type"] == "instant": + string += 'Duration: โ˜๏ธ [b]Instantaneous[/b]' + elif duration["type"] == "special": + string += 'Duration: โญ๏ธ [b]Special[/b]' + elif duration["type"] == "permanent": + string += f"Duration: โ™พ [b]Permanent[/b] (ends on {andformat(duration['ends'], final=' or ')})" + else: + string += f'Duration: โš ๏ธ[b]UNKNOWN[/b]' + if duration.get("concentration", False): + string += " (requires concentration)" + string += "\n" + if "meta" in spell: + if spell["meta"].get("ritual", False): + string += "๐Ÿ”ฎ Can be casted as ritual\n" + string += "\n" + for entry in spell.get("entries", []): + string += parse_5etools_entry(entry) + string += "\n\n" + for entry in spell.get("entriesHigherLevel", []): + string += parse_5etools_entry(entry) + string += "\n\n" + return string + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if self._dnddata is None: + await data.reply("โš ๏ธ Il database degli oggetti di D&D non รจ ancora stato scaricato.") + return + search = args.joined().lower() + result = self._dnddata[self._dnddata.bisect_key_left(search)] + await data.reply(self._parse_spell(result)) diff --git a/rpgpack/commands/roll.py b/rpgpack/commands/roll.py new file mode 100644 index 00000000..bb69dd8f --- /dev/null +++ b/rpgpack/commands/roll.py @@ -0,0 +1,28 @@ +import typing +import random +from royalnet.commands import * + + +class RollCommand(Command): + name: str = "roll" + + description: str = "Roll a dice, from N to M (defaults to 1-100)." + + syntax = "[min] [max]" + + aliases = ["r", "random"] + + async def run(self, args: CommandArgs, data: CommandData) -> None: + first: typing.Optional[str] = args.optional(0) + second: typing.Optional[str] = args.optional(1) + if second: + minimum = int(first) + maximum = int(second) + elif first: + minimum = 1 + maximum = int(first) + else: + minimum = 1 + maximum = 100 + result = random.randrange(minimum, maximum+1) + await data.reply(f"๐ŸŽฒ Dice roll [{minimum}-{maximum}]: [b]{result}[/b]") diff --git a/{packname}/stars/__init__.py b/rpgpack/stars/__init__.py similarity index 70% rename from {packname}/stars/__init__.py rename to rpgpack/stars/__init__.py index dcfea74b..3aa1c42f 100644 --- a/{packname}/stars/__init__.py +++ b/rpgpack/stars/__init__.py @@ -3,7 +3,7 @@ # Enter the PageStars of your Pack here! available_page_stars = [ - + ] # Enter the ExceptionStars of your Pack here! @@ -12,5 +12,4 @@ available_exception_stars = [ ] # Don't change this, it should automatically generate __all__ -__all__ = [command.__name__ for command in [*available_page_stars, *available_exception_stars]] - +__all__ = [star.__name__ for star in [*available_page_stars, *available_exception_stars]] diff --git a/{packname}/tables/__init__.py b/rpgpack/tables/__init__.py similarity index 60% rename from {packname}/tables/__init__.py rename to rpgpack/tables/__init__.py index 961c34f9..70dc4390 100644 --- a/{packname}/tables/__init__.py +++ b/rpgpack/tables/__init__.py @@ -1,9 +1,11 @@ # Imports go here! - +from .dndactivecharacters import DndActiveCharacter +from .dndcharacters import DndCharacter # Enter the tables of your Pack here! available_tables = [ - + DndActiveCharacter, + DndCharacter, ] # Don't change this, it should automatically generate __all__ diff --git a/rpgpack/tables/dndactivecharacters.py b/rpgpack/tables/dndactivecharacters.py new file mode 100644 index 00000000..36693add --- /dev/null +++ b/rpgpack/tables/dndactivecharacters.py @@ -0,0 +1,26 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import * + + +class DndActiveCharacter: + __tablename__ = "dndactivecharacters" + + @declared_attr + def character_id(self): + return Column(Integer, ForeignKey("dndcharacters.character_id"), primary_key=True) + + @declared_attr + def user_id(self): + return Column(Integer, ForeignKey("users.uid"), primary_key=True) + + @declared_attr + def character(self): + return relationship("DndCharacter", foreign_keys=self.character_id, backref="activated_by") + + @declared_attr + def user(self): + return relationship("User", foreign_keys=self.user_id, backref=backref("dnd_active_character", uselist=False)) + + def __repr__(self): + return f"<{self.__class__.__qualname__} for {self.user_id}: {self.character_id}>" diff --git a/rpgpack/tables/dndcharacters.py b/rpgpack/tables/dndcharacters.py new file mode 100644 index 00000000..7699d1ef --- /dev/null +++ b/rpgpack/tables/dndcharacters.py @@ -0,0 +1,301 @@ +from sqlalchemy import * +from sqlalchemy.orm import * +from sqlalchemy.ext.declarative import * +from ..utils import DndProficiencyType + + +class DndCharacter: + __tablename__ = "dndcharacters" + + @declared_attr + def character_id(self): + return Column(Integer, primary_key=True) + + @declared_attr + def creator_id(self): + return Column(Integer, ForeignKey("users.uid")) + + @declared_attr + def creator(self): + return relationship("User", foreign_keys=self.creator_id, backref="dndcharacters_created") + + @declared_attr + def name(self): + return Column(String, nullable=False) + + @declared_attr + def strength_score(self): + return Column(Integer, nullable=False) + + @property + def strength(self): + return (self.strength_score - 10) // 2 + + @declared_attr + def dexterity_score(self): + return Column(Integer, nullable=False) + + @property + def dexterity(self): + return (self.dexterity_score - 10) // 2 + + @declared_attr + def constitution_score(self): + return Column(Integer, nullable=False) + + @property + def constitution(self): + return (self.constitution_score - 10) // 2 + + @declared_attr + def intelligence_score(self): + return Column(Integer, nullable=False) + + @property + def intelligence(self): + return (self.intelligence_score - 10) // 2 + + @declared_attr + def wisdom_score(self): + return Column(Integer, nullable=False) + + @property + def wisdom(self): + return (self.wisdom_score - 10) // 2 + + @declared_attr + def charisma_score(self): + return Column(Integer, nullable=False) + + @property + def charisma(self): + return (self.charisma_score - 10) // 2 + + @declared_attr + def level(self): + return Column(Integer, nullable=False) + + @property + def proficiency_bonus(self): + return ((self.level - 1) // 4) + 2 + + @declared_attr + def current_hp(self): + return Column(Integer, nullable=False) + + @declared_attr + def max_hp(self): + return Column(Integer, nullable=False) + + @declared_attr + def armor_class(self): + return Column(Integer, nullable=False) + + @declared_attr + def strength_save_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def dexterity_save_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def constitution_save_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def intelligence_save_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def wisdom_save_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def charisma_save_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def acrobatics_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def animal_handling_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def arcana_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def athletics_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def deception_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def history_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def insight_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def intimidation_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def investigation_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def medicine_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def nature_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def perception_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def performance_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def persuasion_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def religion_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def sleight_of_hand_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def stealth_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def survival_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @property + def strength_save(self): + return self.strength + self.proficiency_bonus * self.strength_save_proficiency.value + + @property + def dexterity_save(self): + return self.dexterity + self.proficiency_bonus * self.dexterity_save_proficiency.value + + @property + def constitution_save(self): + return self.constitution + self.proficiency_bonus * self.constitution_save_proficiency.value + + @property + def intelligence_save(self): + return self.intelligence + self.proficiency_bonus * self.intelligence_save_proficiency.value + + @property + def wisdom_save(self): + return self.wisdom + self.proficiency_bonus * self.wisdom_save_proficiency.value + + @property + def charisma_save(self): + return self.charisma + self.proficiency_bonus * self.charisma_save_proficiency.value + + @property + def acrobatics(self): + return self.dexterity + self.proficiency_bonus * self.acrobatics_proficiency.value + + @property + def animal_handling(self): + return self.wisdom + self.proficiency_bonus * self.animal_handling_proficiency.value + + @property + def arcana(self): + return self.intelligence + self.proficiency_bonus * self.arcana_proficiency.value + + @property + def athletics(self): + return self.strength + self.proficiency_bonus * self.athletics_proficiency.value + + @property + def deception(self): + return self.charisma + self.proficiency_bonus * self.deception_proficiency.value + + @property + def history(self): + return self.intelligence + self.proficiency_bonus * self.history_proficiency.value + + @property + def insight(self): + return self.wisdom + self.proficiency_bonus * self.insight_proficiency.value + + @property + def intimidation(self): + return self.charisma + self.proficiency_bonus * self.intimidation_proficiency.value + + @property + def investigation(self): + return self.intelligence + self.proficiency_bonus * self.investigation_proficiency.value + + @property + def medicine(self): + return self.wisdom + self.proficiency_bonus * self.medicine_proficiency.value + + @property + def nature(self): + return self.intelligence + self.proficiency_bonus * self.nature_proficiency.value + + @property + def perception(self): + return self.wisdom + self.proficiency_bonus * self.perception_proficiency.value + + @property + def performance(self): + return self.charisma + self.proficiency_bonus * self.performance_proficiency.value + + @property + def persuasion(self): + return self.charisma + self.proficiency_bonus * self.persuasion_proficiency.value + + @property + def religion(self): + return self.intelligence + self.proficiency_bonus * self.religion_proficiency.value + + @property + def sleight_of_hand(self): + return self.dexterity + self.proficiency_bonus * self.sleight_of_hand_proficiency.value + + @property + def stealth(self): + return self.dexterity + self.proficiency_bonus * self.stealth_proficiency.value + + @property + def survival(self): + return self.wisdom + self.proficiency_bonus * self.survival_proficiency.value + + def __repr__(self): + return f"<{self.__class__.__qualname__} {self.name}>" + + def __str__(self): + return f"{self.name}" + + def character_sheet(self) -> str: + columns = list(self.__class__.__table__.columns) + column_names = [column.name for column in columns if (not column.primary_key and + not column.foreign_keys and + column.name != "name")] + message = f"[b]{self.name}[/b]\n" + for column_name in column_names: + value = self.__getattribute__(column_name) + message += f"{column_name} {value}\n" + return message diff --git a/rpgpack/utils/__init__.py b/rpgpack/utils/__init__.py new file mode 100644 index 00000000..affccc25 --- /dev/null +++ b/rpgpack/utils/__init__.py @@ -0,0 +1,4 @@ +from .dndproficiencytype import DndProficiencyType +from .parse5etoolsentry import parse_5etools_entry + +__all__ = ["DndProficiencyType", "parse_5etools_entry"] diff --git a/rpgpack/utils/dndproficiencytype.py b/rpgpack/utils/dndproficiencytype.py new file mode 100644 index 00000000..d7ba925b --- /dev/null +++ b/rpgpack/utils/dndproficiencytype.py @@ -0,0 +1,11 @@ +import enum + + +class DndProficiencyType(enum.Enum): + NONE = 0 + HALF_PROFICIENCY = 0.5 + FULL_PROFICIENCY = 1 + EXPERTISE = 2 + + def __str__(self): + return str(self.value) \ No newline at end of file diff --git a/rpgpack/utils/parse5etoolsentry.py b/rpgpack/utils/parse5etoolsentry.py new file mode 100644 index 00000000..b027249c --- /dev/null +++ b/rpgpack/utils/parse5etoolsentry.py @@ -0,0 +1,31 @@ +def parse_5etools_entry(entry) -> str: + if isinstance(entry, str): + return entry + elif isinstance(entry, dict): + string = "" + if entry["type"] == "entries": + string += f'[b]{entry.get("name", "")}[/b]\n' + for subentry in entry["entries"]: + string += parse_5etools_entry(subentry) + string += "\n\n" + elif entry["type"] == "table": + string += "[i][table hidden][/i]" + # for label in entry["colLabels"]: + # string += f"| {label} " + # string += "|" + # for row in entry["rows"]: + # for column in row: + # string += f"| {self._parse_entry(column)} " + # string += "|\n" + elif entry["type"] == "cell": + return parse_5etools_entry(entry["entry"]) + elif entry["type"] == "list": + string = "" + for item in entry["items"]: + string += f"- {parse_5etools_entry(item)}\n" + string.rstrip("\n") + else: + string += "[i]โš ๏ธ [unknown type][/i]" + else: + return "[/i]โš ๏ธ [unknown data][/i]" + return string diff --git a/rpgpack/version.py b/rpgpack/version.py new file mode 100644 index 00000000..37d8d6a0 --- /dev/null +++ b/rpgpack/version.py @@ -0,0 +1,4 @@ +semantic = "5.0a93" + +if __name__ == "__main__": + print(semantic) diff --git a/setup.py b/setup.py index e625dbff..e093307e 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ import setuptools +import rpgpack.version with open("README.md", "r") as f: long_description = f.read() @@ -7,14 +8,14 @@ with open("requirements.txt", "r") as f: install_requires = f.readlines() setuptools.setup( - name="{packname}", - version="0.1", - author="{packauthorname}", - author_email="{packauthoremail}", - description="{packdescription}", + name="rpgpack", + version=rpgpack.version.semantic, + author="Stefano Pigozzi", + author_email="ste.pigozzi@gmail.com", + description="A Royalnet pack to play D&D by-chat", long_description=long_description, long_description_content_type="text/markdown", - url="{packgithublink}", + url="https://github.com/Steffo99/rpgpack", packages=setuptools.find_packages(), install_requires=install_requires, python_requires=">=3.7", diff --git a/{packname}/commands/__init__.py b/{packname}/commands/__init__.py deleted file mode 100644 index bf5c2b43..00000000 --- a/{packname}/commands/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Imports go here! - - -# Enter the commands of your Pack here! -available_commands = [ - -] - -# Don't change this, it should automatically generate __all__ -__all__ = [command.__name__ for command in available_commands]