From bdf8177cc9f93559d06d06d24d5802bc84e0d49b Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 4 Mar 2020 19:29:33 +0100 Subject: [PATCH] Complete initiative tracker --- rpgpack/commands/__init__.py | 14 ++++- rpgpack/commands/dndaddunit.py | 20 ++++++-- rpgpack/commands/dnddamage.py | 34 +++++++++++++ rpgpack/commands/dnddeathsave.py | 40 +++++++++++++++ rpgpack/commands/dndextra.py | 33 ++++++++++++ rpgpack/commands/dndheal.py | 34 +++++++++++++ rpgpack/commands/dndjoinbattle.py | 65 ++++++++++++++++++++++++ rpgpack/commands/dndstatus.py | 34 +++++++++++++ rpgpack/tables/dndbattle.py | 2 +- rpgpack/tables/dndbattleunit.py | 8 +++ rpgpack/tables/dndcharacters.py | 8 +++ rpgpack/types/health.py | 14 +++-- rpgpack/utils/__init__.py | 2 + rpgpack/utils/findunitincurrentbattle.py | 39 ++++++++++++++ 14 files changed, 336 insertions(+), 11 deletions(-) create mode 100644 rpgpack/commands/dnddamage.py create mode 100644 rpgpack/commands/dnddeathsave.py create mode 100644 rpgpack/commands/dndextra.py create mode 100644 rpgpack/commands/dndheal.py create mode 100644 rpgpack/commands/dndjoinbattle.py create mode 100644 rpgpack/commands/dndstatus.py create mode 100644 rpgpack/utils/findunitincurrentbattle.py diff --git a/rpgpack/commands/__init__.py b/rpgpack/commands/__init__.py index 3cd5ed96..be8f093d 100644 --- a/rpgpack/commands/__init__.py +++ b/rpgpack/commands/__init__.py @@ -13,6 +13,12 @@ from .testfaction import TestfactionCommand from .dndnewbattle import DndnewbattleCommand from .dndactivebattle import DndactivebattleCommand from .dndaddunit import DndaddunitCommand +from .dnddamage import DnddamageCommand +from .dndheal import DndhealCommand +from .dndstatus import DndstatusCommand +from .dndextra import DndextraCommand +from .dnddeathsave import DnddeathsaveCommand +from .dndjoinbattle import DndjoinbattleCommand # Enter the commands of your Pack here! available_commands = [ @@ -29,7 +35,13 @@ available_commands = [ TestfactionCommand, DndnewbattleCommand, DndactivebattleCommand, - DndaddunitCommand + DndaddunitCommand, + DnddamageCommand, + DndhealCommand, + DndstatusCommand, + DndextraCommand, + DnddeathsaveCommand, + DndjoinbattleCommand, ] # Don't change this, it should automatically generate __all__ diff --git a/rpgpack/commands/dndaddunit.py b/rpgpack/commands/dndaddunit.py index 36d7a2a9..56f6d720 100644 --- a/rpgpack/commands/dndaddunit.py +++ b/rpgpack/commands/dndaddunit.py @@ -2,7 +2,7 @@ from typing import * import royalnet import royalnet.commands as rc import royalnet.utils as ru -from ..types import Faction, Health +from ..types import Faction from ..tables import DndBattleUnit from ..utils import get_active_battle @@ -12,7 +12,7 @@ class DndaddunitCommand(rc.Command): description: str = "Add an Unit to a Battle." - aliases = ["dau", "dndau", "daddunit", ""] + aliases = ["dau", "dndau", "addunit", "daddunit"] syntax: str = "{faction} {name} {initiative} {health} {armorclass}" @@ -29,7 +29,16 @@ class DndaddunitCommand(rc.Command): if active_battle is None: raise rc.CommandError("No battle is active in this chat.") + units_with_same_name = await ru.asyncify(data.session.query(DndBattleUnitT).filter_by( + name=name, + battle=active_battle.battle + ).all) + + if len(units_with_same_name) != 0: + raise rc.InvalidInputError("A unit with the same name already exists.") + dbu = DndBattleUnitT( + linked_character_id=None, initiative=initiative, faction=faction, name=name, @@ -41,5 +50,8 @@ class DndaddunitCommand(rc.Command): data.session.add(dbu) await data.session_commit() - await data.reply(f"✅ [b]{dbu.name}[/b] joined the battle!\n" - f"{dbu}") + await data.reply(f"{dbu}\n" + f"joins the battle!") + + if dbu.health.hidden: + await data.delete_invoking() \ No newline at end of file diff --git a/rpgpack/commands/dnddamage.py b/rpgpack/commands/dnddamage.py new file mode 100644 index 00000000..2c3e44bb --- /dev/null +++ b/rpgpack/commands/dnddamage.py @@ -0,0 +1,34 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import royalnet.utils as ru +from ..tables import DndBattleUnit +from ..utils import find_unit_in_current_battle + + +class DnddamageCommand(rc.Command): + name: str = "dnddamage" + + description: str = "Damage a unit in the currently active battle." + + syntax: str = "[name] {damage}" + + aliases = ["dmg", "ddmg", "dnddmg", "damage", "ddamage"] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + if len(args) > 1: + name = args[0] + damage = int(args[1]) + else: + name = None + damage = int(args[0]) + + unit = await find_unit_in_current_battle(data, name) + if unit is None: + raise rc.InvalidInputError("No such unit is fighting in the currently active battle.") + + health = unit.health + health.change(-damage) + unit.health = health + await data.session_commit() + await data.reply(f"{unit}") diff --git a/rpgpack/commands/dnddeathsave.py b/rpgpack/commands/dnddeathsave.py new file mode 100644 index 00000000..a46b27b7 --- /dev/null +++ b/rpgpack/commands/dnddeathsave.py @@ -0,0 +1,40 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import royalnet.utils as ru +from ..tables import DndBattleUnit +from ..utils import find_unit_in_current_battle + + +class DnddeathsaveCommand(rc.Command): + name: str = "dnddeathsave" + + description: str = "Add a death save result to a unit in the currently active battle." + + syntax: str = "[name] {s|f}" + + aliases = ["deathsave", "ddeathsave", "ds", "dds", "dndds"] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + if len(args) > 1: + name = args[0] + result = args[1].lower() + else: + name = None + result = args[0].lower() + + unit = await find_unit_in_current_battle(data, name) + if unit is None: + raise rc.InvalidInputError("No such unit is fighting in the currently active battle.") + + health = unit.health + if result[0] == "s": + health.deathsave_success() + elif result[0] == "f": + health.deathsave_failure() + else: + raise rc.InvalidInputError("Unknown result type") + unit.health = health + + await data.session_commit() + await data.reply(f"{unit}") diff --git a/rpgpack/commands/dndextra.py b/rpgpack/commands/dndextra.py new file mode 100644 index 00000000..38d37e91 --- /dev/null +++ b/rpgpack/commands/dndextra.py @@ -0,0 +1,33 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import royalnet.utils as ru +from ..tables import DndBattleUnit +from ..utils import find_unit_in_current_battle + + +class DndextraCommand(rc.Command): + name: str = "dndextra" + + description: str = "Change the extras for a unit in the current battle." + + syntax: str = "[name] {extra}" + + aliases = ["extra", "dextra"] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + name = args.optional(0) + extra = " ".join(args[1:]) + + if name is not None: + unit: Optional[DndBattleUnit] = await find_unit_in_current_battle(data, name) + else: + unit = None + + if unit is None: + extra = " ".join(args) + unit: Optional[DndBattleUnit] = await find_unit_in_current_battle(data, None) + + unit.extra = extra + await data.session_commit() + await data.reply(f"{unit}") diff --git a/rpgpack/commands/dndheal.py b/rpgpack/commands/dndheal.py new file mode 100644 index 00000000..84aec16b --- /dev/null +++ b/rpgpack/commands/dndheal.py @@ -0,0 +1,34 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import royalnet.utils as ru +from ..tables import DndBattleUnit +from ..utils import find_unit_in_current_battle + + +class DndhealCommand(rc.Command): + name: str = "dndheal" + + description: str = "Heal a unit in the currently active battle." + + syntax: str = "[name] {heal}" + + aliases = ["heal", "dheal"] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + if len(args) > 1: + name = args[0] + heal = int(args[1]) + else: + name = None + heal = int(args[0]) + + unit = await find_unit_in_current_battle(data, name) + if unit is None: + raise rc.InvalidInputError("No such unit is fighting in the currently active battle.") + + health = unit.health + health.change(heal) + unit.health = health + await data.session_commit() + await data.reply(f"{unit}") diff --git a/rpgpack/commands/dndjoinbattle.py b/rpgpack/commands/dndjoinbattle.py new file mode 100644 index 00000000..8cecb442 --- /dev/null +++ b/rpgpack/commands/dndjoinbattle.py @@ -0,0 +1,65 @@ +from typing import * +import random +import royalnet +import royalnet.commands as rc +import royalnet.utils as ru +from ..types import Faction +from ..tables import DndBattleUnit, DndCharacter +from ..utils import get_active_battle, get_active_character + + +class DndjoinbattleCommand(rc.Command): + name: str = "dndjoinbattle" + + description: str = "Add your currently active character to the currently active battle." + + aliases = ["joinbattle", "djoinbattle"] + + syntax: str = "{faction} {initiative_mod}" + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + faction = Faction[args[0].upper()] + initiative_mod = int(args.optional(1, default="0")) + + DndBattleUnitT = self.alchemy.get(DndBattleUnit) + + active_battle = await get_active_battle(data) + if active_battle is None: + raise rc.CommandError("No battle is active in this chat.") + + active_character = await get_active_character(data) + if active_character is None: + raise rc.CommandError("You don't have an active character.") + + char: DndCharacter = active_character.character + + units_with_same_name = await ru.asyncify(data.session.query(DndBattleUnitT).filter_by( + name=char.name, + battle=active_battle.battle + ).all) + + if len(units_with_same_name) != 0: + raise rc.InvalidInputError("A unit with the same name already exists.") + + roll = random.randrange(1, 21) + modifier = char.initiative + initiative_mod + modifier_str = f"{modifier:+d}" if modifier != 0 else "" + initiative = roll + modifier + + dbu = DndBattleUnitT( + linked_character=char, + initiative=initiative, + faction=faction, + name=char.name, + health_string=f"{char.current_hp}/{char.max_hp}", + armor_class=char.armor_class, + battle=active_battle.battle + ) + + data.session.add(dbu) + await data.session_commit() + + await data.reply(f"{dbu}\n" + f"joins the battle!\n" + f"\n" + f"🎲 1d20{modifier_str} = {roll}{modifier_str} = {initiative}") diff --git a/rpgpack/commands/dndstatus.py b/rpgpack/commands/dndstatus.py new file mode 100644 index 00000000..a7a6c89f --- /dev/null +++ b/rpgpack/commands/dndstatus.py @@ -0,0 +1,34 @@ +from typing import * +import royalnet +import royalnet.commands as rc +import royalnet.utils as ru +from ..tables import DndBattleUnit +from ..utils import find_unit_in_current_battle + + +class DndstatusCommand(rc.Command): + name: str = "dndstatus" + + description: str = "Change the status for a unit in the current battle." + + syntax: str = "[name] {status}" + + aliases = ["status", "dstatus"] + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + name = args.optional(0) + status = " ".join(args[1:]) + + if name is not None: + unit: Optional[DndBattleUnit] = await find_unit_in_current_battle(data, name) + else: + unit = None + + if unit is None: + status = " ".join(args) + unit: Optional[DndBattleUnit] = await find_unit_in_current_battle(data, None) + + + unit.status = status + await data.session_commit() + await data.reply(f"{unit}") diff --git a/rpgpack/tables/dndbattle.py b/rpgpack/tables/dndbattle.py index cabb64bf..b5fcb891 100644 --- a/rpgpack/tables/dndbattle.py +++ b/rpgpack/tables/dndbattle.py @@ -30,5 +30,5 @@ class DndBattle: string.append(f"{self.description}\n") string.append("\n") for unit in sorted(self.units, key=lambda u: -u.initiative): - string.append(f"{unit}\n") + string.append(f"{unit}\n\n") return "".join(string) diff --git a/rpgpack/tables/dndbattleunit.py b/rpgpack/tables/dndbattleunit.py index cd997f61..65159739 100644 --- a/rpgpack/tables/dndbattleunit.py +++ b/rpgpack/tables/dndbattleunit.py @@ -11,6 +11,14 @@ class DndBattleUnit: def id(self): return Column(Integer, primary_key=True) + @declared_attr + def linked_character_id(self): + return Column(Integer, ForeignKey("dndcharacters.character_id")) + + @declared_attr + def linked_character(self): + return relationship("DndCharacter", foreign_keys=self.linked_character_id, backref="as_battle_unit") + @declared_attr def battle_id(self): return Column(Integer, ForeignKey("dndbattle.id")) diff --git a/rpgpack/tables/dndcharacters.py b/rpgpack/tables/dndcharacters.py index 6ce52dec..04931a66 100644 --- a/rpgpack/tables/dndcharacters.py +++ b/rpgpack/tables/dndcharacters.py @@ -188,6 +188,10 @@ class DndCharacter: def survival_proficiency(self): return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + @declared_attr + def initiative_proficiency(self): + return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE) + @property def strength_save(self): return self.strength + math.floor(self.proficiency_bonus * self.strength_save_proficiency.value) @@ -284,6 +288,10 @@ class DndCharacter: def survival(self): return self.wisdom + math.floor(self.proficiency_bonus * self.survival_proficiency.value) + @property + def initiative(self): + return self.dexterity + math.floor(self.proficiency_bonus * self.initiative_proficiency.value) + def __repr__(self): return f"<{self.__class__.__qualname__} {self.name}>" diff --git a/rpgpack/types/health.py b/rpgpack/types/health.py index dfdb3ac1..3af42081 100644 --- a/rpgpack/types/health.py +++ b/rpgpack/types/health.py @@ -39,7 +39,7 @@ class Health: if self.temp_value > 0: string.append(f"{self.temp_value:+}") string.append("/") - string.append(f"{self.max_value}̋") + string.append(f"{self.max_value}") string.append("s" * self.deathsave_successes) string.append("f" * self.deathsave_failures) return "".join(string) @@ -112,15 +112,19 @@ class Health: self.value = 0 def deathsave_success(self) -> None: - if self.dying: + if not self.dying: raise ValueError("Can't roll death saves while alive") if self.stable: - raise ValueError("Successful death saves are capped at 3") + raise ValueError("Can't roll death saves while stable") + if self.dead: + raise ValueError("Can't roll death saves while dead") self.deathsave_successes += 1 def deathsave_failure(self) -> None: - if self.dying: + if not self.dying: raise ValueError("Can't roll death saves while alive") + if self.stable: + raise ValueError("Can't roll death saves while stable") if self.dead: - raise ValueError("Failing death saves are capped at 3") + raise ValueError("Can't roll death saves while dead") self.deathsave_failures += 1 diff --git a/rpgpack/utils/__init__.py b/rpgpack/utils/__init__.py index b965057a..a1149e4a 100644 --- a/rpgpack/utils/__init__.py +++ b/rpgpack/utils/__init__.py @@ -2,10 +2,12 @@ from .parse5etoolsentry import parse_5etools_entry from .getinterfacedata import get_interface_data from .getactivechar import get_active_character from .getactivebattle import get_active_battle +from .findunitincurrentbattle import find_unit_in_current_battle __all__ = [ "parse_5etools_entry", "get_interface_data", "get_active_character", "get_active_battle", + "find_unit_in_current_battle" ] diff --git a/rpgpack/utils/findunitincurrentbattle.py b/rpgpack/utils/findunitincurrentbattle.py new file mode 100644 index 00000000..90de921d --- /dev/null +++ b/rpgpack/utils/findunitincurrentbattle.py @@ -0,0 +1,39 @@ +from typing import * +import royalnet.commands as rc +import royalnet.utils as ru + +from ..tables import DndBattleUnit +from .getactivebattle import get_active_battle +from .getactivechar import get_active_character + +from sqlalchemy import func, and_ + + +async def find_unit_in_current_battle(data: rc.CommandData, name: Optional[str]) -> Optional[DndBattleUnit]: + DndBattleUnitT = data._interface.alchemy.get(DndBattleUnit) + + active_battle = await get_active_battle(data) + if active_battle is None: + raise rc.CommandError("No battle is active in this chat.") + + if name is None: + active_character = await get_active_character(data) + if active_character is None: + raise rc.InvalidInputError("You currently have no active character.") + + unit = await ru.asyncify(data.session.query(DndBattleUnitT).filter_by( + linked_character=active_character.character, + battle=active_battle.battle + ).one_or_none) + if unit is None: + raise rc.InvalidInputError("Your active character is not fighting in this battle.") + + else: + unit = await ru.asyncify(data.session.query(DndBattleUnitT).filter(and_( + func.lower(DndBattleUnitT.name) == func.lower(name), + DndBattleUnitT.battle == active_battle.battle + )).one_or_none) + if unit is None: + return None + + return unit