From 50700c7b6318eec1e864afe1f47f75f5ff92395a Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Thu, 24 Oct 2019 01:18:00 +0200 Subject: [PATCH] Change dnd commands --- royalnet/packs/rpg/commands/__init__.py | 6 - royalnet/packs/rpg/commands/dndedit.py | 61 ++-- royalnet/packs/rpg/commands/dndnew.py | 96 +++--- royalnet/packs/rpg/commands/dndroll.py | 94 ------ royalnet/packs/rpg/commands/dndrolladv.py | 26 -- royalnet/packs/rpg/commands/dndrolldis.py | 26 -- royalnet/packs/rpg/tables/dndcharacters.py | 284 +++++++++++++++--- royalnet/packs/rpg/utils/__init__.py | 3 + .../packs/rpg/utils/dndproficiencytype.py | 11 + 9 files changed, 325 insertions(+), 282 deletions(-) delete mode 100644 royalnet/packs/rpg/commands/dndroll.py delete mode 100644 royalnet/packs/rpg/commands/dndrolladv.py delete mode 100644 royalnet/packs/rpg/commands/dndrolldis.py create mode 100644 royalnet/packs/rpg/utils/__init__.py create mode 100644 royalnet/packs/rpg/utils/dndproficiencytype.py diff --git a/royalnet/packs/rpg/commands/__init__.py b/royalnet/packs/rpg/commands/__init__.py index 569bae5e..4f481b9b 100644 --- a/royalnet/packs/rpg/commands/__init__.py +++ b/royalnet/packs/rpg/commands/__init__.py @@ -5,9 +5,6 @@ from .dndactive import DndactiveCommand from .dndinfo import DndinfoCommand from .dndnew import DndnewCommand from .dndedit import DndeditCommand -from .dndroll import DndrollCommand -from .dndrolladv import DndrolladvCommand -from .dndrolldis import DndrolldisCommand # Enter the commands of your Pack here! available_commands = [ @@ -17,9 +14,6 @@ available_commands = [ DndinfoCommand, DndnewCommand, DndeditCommand, - DndrollCommand, - DndrolladvCommand, - DndrolldisCommand, ] # Don't change this, it should automatically generate __all__ diff --git a/royalnet/packs/rpg/commands/dndedit.py b/royalnet/packs/rpg/commands/dndedit.py index 12881cc9..918e431d 100644 --- a/royalnet/packs/rpg/commands/dndedit.py +++ b/royalnet/packs/rpg/commands/dndedit.py @@ -1,60 +1,35 @@ import re from royalnet.commands import * +from .dndnew import DndnewCommand from ..tables import DndCharacter, DndActiveCharacter -class DndeditCommand(Command): +class DndeditCommand(DndnewCommand): name: str = "dndedit" description: str = "Edit the active DnD character." aliases = ["de", "dnde", "edit", "dedit"] - syntax = "{name}\n" \ - "LV {level}\n" \ - "\n" \ - "STR {strength}\n" \ - "DEX {dexterity}\n" \ - "CON {constitution}\n" \ - "INT {intelligence}\n" \ - "WIS {wisdom}\n" \ - "CHA {charisma}\n" \ - "\n" \ - "MAXHP {max_hp}\n" \ - "AC {armor_class}" - tables = {DndCharacter, DndActiveCharacter} async def run(self, args: CommandArgs, data: CommandData) -> None: - name, level, strength, dexterity, constitution, intelligence, wisdom, charisma, max_hp, armor_class = \ - args.match(r"([\w ]+\w)\s*" - r"LV\s+(\d+)\s+" - r"STR\s+(\d+)\s+" - r"DEX\s+(\d+)\s+" - r"CON\s+(\d+)\s+" - r"INT\s+(\d+)\s+" - r"WIS\s+(\d+)\s+" - r"CHA\s+(\d+)\s+" - r"MAXHP\s+(\d+)\s+" - r"AC\s+(\d+)", re.IGNORECASE) - try: - int(name) - except ValueError: - pass - else: - raise CommandError("Character names cannot be composed of only a number.") + character_sheet = args.joined() + + if character_sheet == "": + await data.reply(self._syntax()) + return + author = await data.get_author(error_if_none=True) - char = author.dnd_active_character.character - char.name = name - char.level = level - char.strength = strength - char.dexterity = dexterity - char.constitution = constitution - char.intelligence = intelligence - char.wisdom = wisdom - char.charisma = charisma - char.max_hp = max_hp - char.armor_class = armor_class - data.session.add(char) + 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/royalnet/packs/rpg/commands/dndnew.py b/royalnet/packs/rpg/commands/dndnew.py index 4c8af8f9..1f17d157 100644 --- a/royalnet/packs/rpg/commands/dndnew.py +++ b/royalnet/packs/rpg/commands/dndnew.py @@ -1,6 +1,8 @@ import re +# noinspection PyUnresolvedReferences from royalnet.commands import * from ..tables import DndCharacter +from ..utils import DndProficiencyType class DndnewCommand(Command): @@ -10,51 +12,59 @@ class DndnewCommand(Command): aliases = ["dn", "dndn", "new", "dnew"] - syntax = "{name}\n" \ - "LV {level}\n" \ - "\n" \ - "STR {strength}\n" \ - "DEX {dexterity}\n" \ - "CON {constitution}\n" \ - "INT {intelligence}\n" \ - "WIS {wisdom}\n" \ - "CHA {charisma}\n" \ - "\n" \ - "MAXHP {max_hp}\n" \ - "AC {armor_class}" + 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 = "ℹ️ [b]Character sheet format:[/b]" + for column_name in column_names: + message += f"{column_name} _\n" + return message + async def run(self, args: CommandArgs, data: CommandData) -> None: - name, level, strength, dexterity, constitution, intelligence, wisdom, charisma, max_hp, armor_class = \ - args.match(r"([\w ]+\w)\s*" - r"LV\s+(\d+)\s+" - r"STR\s+(\d+)\s+" - r"DEX\s+(\d+)\s+" - r"COS\s+(\d+)\s+" - r"INT\s+(\d+)\s+" - r"WIS\s+(\d+)\s+" - r"CHA\s+(\d+)\s+" - r"MAXHP\s+(\d+)\s+" - r"AC\s+(\d+)", re.IGNORECASE) + 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: - int(name) - except ValueError: - pass - else: - raise CommandError("Character names cannot be composed of only a number.") - author = await data.get_author(error_if_none=True) - char = self.alchemy.DndCharacter(name=name, - level=level, - strength=strength, - dexterity=dexterity, - constitution=constitution, - intelligence=intelligence, - wisdom=wisdom, - charisma=charisma, - max_hp=max_hp, - armor_class=armor_class, - creator=author) - data.session.add(char) - await data.session_commit() - await data.reply(f"✅ Character [b]{char.name}[/b] ([c]{char.character_id}[/c]) was created!") + 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/royalnet/packs/rpg/commands/dndroll.py b/royalnet/packs/rpg/commands/dndroll.py deleted file mode 100644 index 348b34a4..00000000 --- a/royalnet/packs/rpg/commands/dndroll.py +++ /dev/null @@ -1,94 +0,0 @@ -import typing -import random -from royalnet.commands import * -from royalnet.utils import plusformat -from ..tables import DndCharacter, DndActiveCharacter - - -class DndrollCommand(Command): - name: str = "dndroll" - - description: str = "Roll as the active DnD character." - - aliases = ["dr", "dndr", "droll"] - - syntax = "{stat} [proficiency] [modifier]" - - tables = {DndCharacter, DndActiveCharacter} - - @staticmethod - def _roll(): - return random.randrange(1, 21) - - _roll_string = "1d20" - - 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 - stat: str = args[0] - second: typing.Optional[str] = args.optional(1) - third: typing.Optional[str] = args.optional(2) - - if third: - extra_mod: int = int(third) - else: - extra_mod: int = 0 - - if second: - if second.startswith("e") or second.startswith("x"): - proficiency_mul: float = 2.0 - proficiency_name: str = " with Expertise" - elif second.startswith("f") or second.startswith("n") or second.startswith("c"): - proficiency_mul: float = 1.0 - proficiency_name: str = " with Proficiency" - elif second.startswith("h") or second == "/" or second.startswith("m"): - proficiency_mul: float = 0.5 - proficiency_name: str = " with Half Proficiency" - elif second.startswith("h") or second == "/" or second.startswith("m"): - proficiency_mul: float = 0.0 - proficiency_name: str = " [i]without Proficiency[/i]" - else: - raise CommandError(f"Unknown proficiency type '{second}'") - proficiency_mod: int = int(char.proficiency_bonus * proficiency_mul) - else: - proficiency_name: str = "" - proficiency_mod: int = 0 - - if stat.startswith("st") or stat.startswith("fo"): - stat_mod: int = char.strength_mod - stat_name: str = "[i]STR[/i]" - elif stat.startswith("de"): - stat_mod: int = char.dexterity_mod - stat_name: str = "[i]DEX[/i]" - elif stat.startswith("co"): - stat_mod: int = char.constitution_mod - stat_name: str = "[i]CON[/i]" - elif stat.startswith("in"): - stat_mod: int = char.intelligence_mod - stat_name: str = "[i]INT[/i]" - elif stat.startswith("wi") or stat.startswith("sa"): - stat_mod: int = char.wisdom_mod - stat_name: str = "[i]WIS[/i]" - elif stat.startswith("ch") or stat.startswith("ca"): - stat_mod: int = char.charisma_mod - stat_name: str = "[i]CHA[/i]" - else: - raise CommandError(f"Unknown stat '{stat}'") - - total_mod = stat_mod + proficiency_mod + extra_mod - - roll = self._roll() - - result = roll + total_mod - - await data.reply(f"🎲 Rolling {stat_name}{proficiency_name}{plusformat(extra_mod, empty_if_zero=True)}:\n" - f"{self._roll_string}" - f"{plusformat(stat_mod, empty_if_zero=True)}" - f"{plusformat(proficiency_mod, empty_if_zero=True)}" - f"{plusformat(extra_mod, empty_if_zero=True)}" - f" = " - f"{roll}{plusformat(total_mod, empty_if_zero=True)}" - f" = " - f"[b]{result}[/b]") diff --git a/royalnet/packs/rpg/commands/dndrolladv.py b/royalnet/packs/rpg/commands/dndrolladv.py deleted file mode 100644 index 9d918597..00000000 --- a/royalnet/packs/rpg/commands/dndrolladv.py +++ /dev/null @@ -1,26 +0,0 @@ -import typing -import random -from royalnet.commands import * -from royalnet.utils import plusformat -from .dndroll import DndrollCommand -from ..tables import DndCharacter, DndActiveCharacter - - -class DndrolladvCommand(DndrollCommand): - name: str = "dndrolladv" - - description: str = "Roll with advantage as the active DnD character." - - aliases = ["dra", "dndra", "drolladv"] - - syntax = "{stat} [proficiency] [modifier]" - - tables = {DndCharacter, DndActiveCharacter} - - @staticmethod - def _roll(): - first = random.randrange(1, 21) - second = random.randrange(1, 21) - return max(first, second) - - _roll_string = "2d20h1" diff --git a/royalnet/packs/rpg/commands/dndrolldis.py b/royalnet/packs/rpg/commands/dndrolldis.py deleted file mode 100644 index e195c34d..00000000 --- a/royalnet/packs/rpg/commands/dndrolldis.py +++ /dev/null @@ -1,26 +0,0 @@ -import typing -import random -from royalnet.commands import * -from royalnet.utils import plusformat -from .dndroll import DndrollCommand -from ..tables import DndCharacter, DndActiveCharacter - - -class DndrolldisCommand(DndrollCommand): - name: str = "dndrolldis" - - description: str = "Roll with disadvantage as the active DnD character." - - aliases = ["drd", "dndrd", "drolldis"] - - syntax = "{stat} [proficiency] [modifier]" - - tables = {DndCharacter, DndActiveCharacter} - - @staticmethod - def _roll(): - first = random.randrange(1, 21) - second = random.randrange(1, 21) - return min(first, second) - - _roll_string = "2d20l1" diff --git a/royalnet/packs/rpg/tables/dndcharacters.py b/royalnet/packs/rpg/tables/dndcharacters.py index e3c4cb35..6bd346c6 100644 --- a/royalnet/packs/rpg/tables/dndcharacters.py +++ b/royalnet/packs/rpg/tables/dndcharacters.py @@ -1,6 +1,7 @@ from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import * +from ..utils import DndProficiencyType class DndCharacter: @@ -20,71 +21,267 @@ class DndCharacter: @declared_attr def name(self): - return Column(String) + return Column(String, nullable=False) @declared_attr + def strength_score(self): + return Column(Integer, nullable=False) + + @property def strength(self): - return Column(Integer) - - @property - def strength_mod(self): - return (self.strength - 10) // 2 + return (self.strength_score - 10) // 2 @declared_attr + def dexterity_score(self): + return Column(Integer, nullable=False) + + @property def dexterity(self): - return Column(Integer) - - @property - def dexterity_mod(self): - return (self.dexterity - 10) // 2 + return (self.dexterity_score - 10) // 2 @declared_attr + def constitution_score(self): + return Column(Integer, nullable=False) + + @property def constitution(self): - return Column(Integer) - - @property - def constitution_mod(self): - return (self.constitution - 10) // 2 + return (self.constitution_score - 10) // 2 @declared_attr + def intelligence_score(self): + return Column(Integer, nullable=False) + + @property def intelligence(self): - return Column(Integer) - - @property - def intelligence_mod(self): - return (self.intelligence - 10) // 2 + return (self.intelligence_score - 10) // 2 @declared_attr + def wisdom_score(self): + return Column(Integer, nullable=False) + + @property def wisdom(self): - return Column(Integer) - - @property - def wisdom_mod(self): - return (self.wisdom - 10) // 2 + return (self.wisdom_score - 10) // 2 @declared_attr - def charisma(self): - return Column(Integer) + def charisma_score(self): + return Column(Integer, nullable=False) @property - def charisma_mod(self): - return (self.charisma - 10) // 2 + def charisma(self): + return (self.charisma_score - 10) // 2 @declared_attr def level(self): - return Column(Integer) + 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) + return Column(Integer, nullable=False) @declared_attr def armor_class(self): - return Column(Integer) + return Column(Integer, nullable=False) + + @declared_attr + def strength_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def dexterity_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def constitution_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def intelligence_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def wisdom_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + + @declared_attr + def charisma_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_proficiency.value + + @property + def dexterity_save(self): + return self.dexterity + self.proficiency_bonus * self.dexterity_proficiency.value + + @property + def constitution_save(self): + return self.constitution + self.proficiency_bonus * self.constitution_proficiency.value + + @property + def intelligence_save(self): + return self.intelligence + self.proficiency_bonus * self.intelligence_proficiency.value + + @property + def wisdom_save(self): + return self.wisdom + self.proficiency_bonus * self.wisdom_proficiency.value + + @property + def charisma_save(self): + return self.charisma + self.proficiency_bonus * self.charisma_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}>" @@ -92,14 +289,13 @@ class DndCharacter: def __str__(self): return f"{self.name}" - def character_sheet(self): - return f"{self.name}\n" \ - f"LV {self.level}\n\n" \ - f"STR {self.strength}\n" \ - f"DEX {self.dexterity}\n" \ - f"CON {self.constitution}\n" \ - f"INT {self.intelligence}\n" \ - f"WIS {self.wisdom}\n" \ - f"CHA {self.charisma}\n\n" \ - f"MAXHP {self.max_hp}\n" \ - f"AC {self.armor_class}\n" \ + 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/royalnet/packs/rpg/utils/__init__.py b/royalnet/packs/rpg/utils/__init__.py new file mode 100644 index 00000000..2d229e83 --- /dev/null +++ b/royalnet/packs/rpg/utils/__init__.py @@ -0,0 +1,3 @@ +from .dndproficiencytype import DndProficiencyType + +__all__ = ["DndProficiencyType"] diff --git a/royalnet/packs/rpg/utils/dndproficiencytype.py b/royalnet/packs/rpg/utils/dndproficiencytype.py new file mode 100644 index 00000000..d7ba925b --- /dev/null +++ b/royalnet/packs/rpg/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