diff --git a/rpgpack/commands/__init__.py b/rpgpack/commands/__init__.py index aa045b90..b64224a3 100644 --- a/rpgpack/commands/__init__.py +++ b/rpgpack/commands/__init__.py @@ -8,6 +8,8 @@ from .dndedit import DndeditCommand from .dndroll import DndrollCommand from .dnditem import DnditemCommand from .dndspell import DndspellCommand +from .testhealth import TesthealthCommand +from .testfaction import TestfactionCommand # Enter the commands of your Pack here! available_commands = [ @@ -20,6 +22,8 @@ available_commands = [ DndrollCommand, DnditemCommand, DndspellCommand, + TesthealthCommand, + TestfactionCommand, ] # Don't change this, it should automatically generate __all__ diff --git a/rpgpack/commands/testfaction.py b/rpgpack/commands/testfaction.py new file mode 100644 index 00000000..e3ebb10c --- /dev/null +++ b/rpgpack/commands/testfaction.py @@ -0,0 +1,15 @@ +from typing import * +import royalnet +import royalnet.commands as rc +from ..utils import FactionColor + + +class TestfactionCommand(rc.Command): + name: str = "testfaction" + + description: str = "Test a faction string." + + syntax: str = "{factionstring}" + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + await data.reply(FactionColor[args[0].upper()].value) diff --git a/rpgpack/commands/testhealth.py b/rpgpack/commands/testhealth.py new file mode 100644 index 00000000..3acf0518 --- /dev/null +++ b/rpgpack/commands/testhealth.py @@ -0,0 +1,15 @@ +from typing import * +import royalnet +import royalnet.commands as rc +from ..utils import Health + + +class TesthealthCommand(rc.Command): + name: str = "testhealth" + + description: str = "Test a health string." + + syntax: str = "{healthstring}" + + async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None: + await data.reply(str(Health.from_text(args[0]))) diff --git a/rpgpack/utils/__init__.py b/rpgpack/utils/__init__.py index 6e271b11..36b5786a 100644 --- a/rpgpack/utils/__init__.py +++ b/rpgpack/utils/__init__.py @@ -1,5 +1,14 @@ from .dndproficiencytype import DndProficiencyType from .parse5etoolsentry import parse_5etools_entry from .getactivechar import get_active_character, get_interface_data +from .factioncolors import FactionColor +from .health import Health -__all__ = ["DndProficiencyType", "parse_5etools_entry", "get_active_character", "get_interface_data"] +__all__ = [ + "DndProficiencyType", + "parse_5etools_entry", + "get_active_character", + "get_interface_data", + "FactionColor", + "Health", +] diff --git a/rpgpack/utils/factioncolors.py b/rpgpack/utils/factioncolors.py new file mode 100644 index 00000000..b3120d47 --- /dev/null +++ b/rpgpack/utils/factioncolors.py @@ -0,0 +1,14 @@ +from typing import * +import enum + + +class FactionColor(enum.Enum): + RED = "🔴" + ORANGE = "🟠" + YELLOW = "🟡" + GREEN = "🟢" + BLUE = "🔵" + PURPLE = "🟣" + BLACK = "⚫️" + WHITE = "⚪️" + BROWN = "🟤" diff --git a/rpgpack/utils/health.py b/rpgpack/utils/health.py new file mode 100644 index 00000000..dfdb3ac1 --- /dev/null +++ b/rpgpack/utils/health.py @@ -0,0 +1,126 @@ +from typing import * +import re + + +class Health: + def __init__(self, + initial_value: int, + max_value: int, + hidden: Optional[bool] = None, + temp_value: Optional[int] = None, + deathsave_successes: Optional[int] = None, + deathsave_failures: Optional[int] = None): + self.value: int = 0 + self.max_value: int = max_value + self.hidden: bool = hidden if hidden else False + self.temp_value: int = temp_value if temp_value else 0 + self.deathsave_successes: int = deathsave_successes if deathsave_successes else 0 + self.deathsave_failures: int = deathsave_failures if deathsave_failures else 0 + self.change(initial_value) + + @classmethod + def from_text(cls, text: str) -> "Health": + match = re.match(r"(h)?([0-9]+)(?:\+([0-9]+))?/([0-9]+)(s{0,3})(f{0,3})", text) + if not match: + raise ValueError("Could not parse passed string.") + hidden, value, temp_value, max_value, ds_successes, ds_failures = match.groups() + return cls(initial_value=int(value), + max_value=int(max_value), + hidden=bool(hidden), + temp_value=int(temp_value) if temp_value else None, + deathsave_successes=len(ds_successes) if ds_successes else None, + deathsave_failures=len(ds_failures) if ds_failures else None) + + def to_text(self) -> str: + string = [] + if self.hidden: + string.append("h") + string.append(f"{self.value}") + if self.temp_value > 0: + string.append(f"{self.temp_value:+}") + string.append("/") + string.append(f"{self.max_value}̋") + string.append("s" * self.deathsave_successes) + string.append("f" * self.deathsave_failures) + return "".join(string) + + @property + def dead(self) -> bool: + return self.deathsave_failures >= 3 + + @property + def stable(self) -> bool: + return self.deathsave_successes >= 3 + + @property + def dying(self) -> bool: + return self.value <= 0 + + @property + def total_value(self) -> int: + return self.value + self.temp_value + + def __str__(self): + # Dead + if self.dead: + return "💀" + # Stable + if self.stable: + return f"💔 {self.value}/{self.max_value} " + # Dying + if self.value <= 0: + return "".join([ + f"💔 {self.value}/{self.max_value} ", + "🔷" * self.deathsave_successes, + "🔹" * (3 - self.deathsave_successes), + " " + "🔶" * self.deathsave_failures, + "🔸" * (3 - self.deathsave_failures), + ]) + # Hidden + if self.hidden: + return f"🖤 {self.value - self.max_value}" + # Temporary HP + if self.temp_value > 0: + return f"💙 {self.value}+{self.temp_value}/{self.max_value}" + # Default + return f"❤️ {self.value}/{self.max_value}" + + def __repr__(self): + return f"{self.__class__.__qualname__}.from_text({self.to_text()})" + + def change(self, amount) -> None: + # Heal + if amount > 0: + self.value += amount + # Cap at maximum + if self.value >= self.max_value: + self.value = self.max_value + # Restore death saves + self.deathsave_successes = 0 + self.deathsave_failures = 0 + # Damage + else: + # First remove temporary health + self.temp_value += amount + # Then damage health based on how much damage wasn't absorbed + if self.temp_value < 0: + self.value += self.temp_value + self.temp_value = 0 + # Cap health at 0 + if self.value < 0: + self.value = 0 + + def deathsave_success(self) -> None: + if self.dying: + raise ValueError("Can't roll death saves while alive") + if self.stable: + raise ValueError("Successful death saves are capped at 3") + self.deathsave_successes += 1 + + def deathsave_failure(self) -> None: + if self.dying: + raise ValueError("Can't roll death saves while alive") + if self.dead: + raise ValueError("Failing death saves are capped at 3") + self.deathsave_failures += 1