1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +00:00

Add more dnd commands

This commit is contained in:
Steffo 2019-10-22 15:29:23 +02:00
parent d31b6e4100
commit 12f6967fa6
13 changed files with 288 additions and 16 deletions

View file

@ -144,6 +144,8 @@ class DiscordBot(GenericBot):
error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n" error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n"
error_message += '\n'.join(e.args) error_message += '\n'.join(e.args)
await data.reply(error_message) await data.reply(error_message)
# Close the data session
await data.session_close()
async def on_connect(cli): async def on_connect(cli):
log.debug("Connected to Discord") log.debug("Connected to Discord")

View file

@ -178,8 +178,8 @@ class TelegramBot(GenericBot):
error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n" error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n"
error_message += '\n'.join(e.args) error_message += '\n'.join(e.args)
await data.reply(error_message) await data.reply(error_message)
if __debug__: # Close the data session
raise await data.session_close()
async def _handle_callback_query(self, update: telegram.Update): async def _handle_callback_query(self, update: telegram.Update):
query: telegram.CallbackQuery = update.callback_query query: telegram.CallbackQuery = update.callback_query

View file

@ -26,5 +26,10 @@ class Command:
def __init__(self, interface: CommandInterface): def __init__(self, interface: CommandInterface):
self.interface = interface self.interface = interface
@property
def alchemy(self):
"""A shortcut to ``self.interface.alchemy``"""
return self.interface.alchemy
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:
raise NotImplementedError() raise NotImplementedError()

View file

@ -2,6 +2,7 @@ import typing
import warnings import warnings
from .commanderrors import UnsupportedError from .commanderrors import UnsupportedError
from .commandinterface import CommandInterface from .commandinterface import CommandInterface
from ..utils import asyncify
class CommandData: class CommandData:
@ -12,6 +13,18 @@ class CommandData:
else: else:
self.session = None self.session = None
async def session_commit(self):
"""Commit the changes to the session."""
await asyncify(self.session.commit)
async def session_close(self):
"""Close the opened session.
Remember to call this when the data is disposed of!"""
if self.session:
await asyncify(self.session.close)
self.session = None
async def reply(self, text: str) -> None: async def reply(self, text: str) -> None:
"""Send a text message to the channel where the call was made. """Send a text message to the channel where the call was made.

View file

@ -1,11 +1,21 @@
# Imports go here! # Imports go here!
from .roll import RollCommand from .roll import RollCommand
from .dice import DiceCommand from .dice import DiceCommand
from .dndactive import DndactiveCommand
from .dndinfo import DndinfoCommand
from .dndnew import DndnewCommand
from .dndedit import DndeditCommand
from .dndroll import DndrollCommand
# Enter the commands of your Pack here! # Enter the commands of your Pack here!
available_commands = [ available_commands = [
RollCommand, RollCommand,
DiceCommand, DiceCommand,
DndactiveCommand,
DndinfoCommand,
DndnewCommand,
DndeditCommand,
DndrollCommand,
] ]
# Don't change this, it should automatically generate __all__ # Don't change this, it should automatically generate __all__

View file

@ -1,12 +1,52 @@
from royalnet.commands import * from royalnet.commands import *
from royalnet.utils import asyncify
from ..tables import DndCharacter, DndActiveCharacter
class DndactiveCommand(Command): class DndactiveCommand(Command):
name: str = "dndactive" name: str = "dndactive"
description: str = "Set the active D&D character." description: str = "Set a DnD character as active."
aliases = ["da", "dnda", "active"] aliases = ["da", "dnda", "active", "dactive"]
syntax = "{name|id}"
tables = {DndCharacter, DndActiveCharacter}
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:
... identifier = args.optional(0)
if identifier is None:
# Display the active character identifiers
...
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
author = await data.get_author(error_if_none=True)
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]!")

View file

@ -0,0 +1,59 @@
from royalnet.commands import *
from ..tables import DndCharacter, DndActiveCharacter
class DndeditCommand(Command):
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+)")
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 = 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)
await data.session_commit()
await data.reply(f"✅ Edit successful!")

View file

@ -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())

View file

@ -5,9 +5,9 @@ from ..tables import DndCharacter
class DndnewCommand(Command): class DndnewCommand(Command):
name: str = "dndnew" name: str = "dndnew"
description: str = "Create a new D&D character." description: str = "Create a new DnD character."
aliases = ["dn", "dndn", "new"] aliases = ["dn", "dndn", "new", "dnew"]
syntax = "{name}\n" \ syntax = "{name}\n" \
"LV {level}\n" \ "LV {level}\n" \
@ -19,11 +19,41 @@ class DndnewCommand(Command):
"WIS {wisdom}\n" \ "WIS {wisdom}\n" \
"CHA {charisma}\n" \ "CHA {charisma}\n" \
"\n" \ "\n" \
"MAXHP {maxhp}\n" \ "MAXHP {max_hp}\n" \
"AC {armorclass}" "AC {armor_class}"
tables = {} tables = {DndCharacter}
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:
name = args[0] 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+)")
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!")

View file

@ -0,0 +1,88 @@
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}
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 = random.randrange(1, 21)
result = roll + total_mod
await data.reply(f"🎲 Rolling {stat_name}{proficiency_name}{plusformat(extra_mod, empty_if_zero=True)}:\n"
f"1d20"
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]")

View file

@ -16,11 +16,11 @@ class DndActiveCharacter:
@declared_attr @declared_attr
def character(self): def character(self):
return relationship("DndCharacter", foreign_keys=self.character_id, backref="activations", use_scalar=True) return relationship("DndCharacter", foreign_keys=self.character_id, backref="activated_by")
@declared_attr @declared_attr
def user(self): def user(self):
return relationship("User", foreign_keys=self.user_id, backref="active_dnd_character", use_scalar=True) return relationship("User", foreign_keys=self.user_id, backref=backref("dnd_active_character", uselist=False))
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__qualname__} for {self.user_id}: {self.character_id}>" return f"<{self.__class__.__qualname__} for {self.user_id}: {self.character_id}>"

View file

@ -90,6 +90,9 @@ class DndCharacter:
return f"<{self.__class__.__qualname__} {self.name}>" return f"<{self.__class__.__qualname__} {self.name}>"
def __str__(self): def __str__(self):
return f"{self.name}"
def character_sheet(self):
return f"{self.name}\n" \ return f"{self.name}\n" \
f"LV {self.level}\n\n" \ f"LV {self.level}\n\n" \
f"STR {self.strength}\n" \ f"STR {self.strength}\n" \

View file

@ -22,15 +22,18 @@ def andformat(l: typing.List[str], middle=", ", final=" and ") -> str:
return result return result
def plusformat(i: int) -> str: def plusformat(i: int, empty_if_zero: bool = False) -> str:
"""Convert an :py:class:`int` to a :py:class:`str`, prepending a ``+`` if it's greater than 0. """Convert an :py:class:`int` to a :py:class:`str`, prepending a ``+`` if it's greater than 0.
Parameters: Parameters:
i: the :py:class:`int` to convert. i: the :py:class:`int` to convert.
empty_if_zero: Return an empty string if ``i`` is zero.
Returns: Returns:
The resulting :py:class:`str`.""" The resulting :py:class:`str`."""
if i >= 0: if i == 0 and empty_if_zero:
return ""
if i > 0:
return f"+{i}" return f"+{i}"
return str(i) return str(i)