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:
parent
d31b6e4100
commit
12f6967fa6
13 changed files with 288 additions and 16 deletions
|
@ -144,6 +144,8 @@ class DiscordBot(GenericBot):
|
|||
error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n"
|
||||
error_message += '\n'.join(e.args)
|
||||
await data.reply(error_message)
|
||||
# Close the data session
|
||||
await data.session_close()
|
||||
|
||||
async def on_connect(cli):
|
||||
log.debug("Connected to Discord")
|
||||
|
|
|
@ -178,8 +178,8 @@ class TelegramBot(GenericBot):
|
|||
error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n"
|
||||
error_message += '\n'.join(e.args)
|
||||
await data.reply(error_message)
|
||||
if __debug__:
|
||||
raise
|
||||
# Close the data session
|
||||
await data.session_close()
|
||||
|
||||
async def _handle_callback_query(self, update: telegram.Update):
|
||||
query: telegram.CallbackQuery = update.callback_query
|
||||
|
|
|
@ -26,5 +26,10 @@ class Command:
|
|||
def __init__(self, interface: CommandInterface):
|
||||
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:
|
||||
raise NotImplementedError()
|
||||
|
|
|
@ -2,6 +2,7 @@ import typing
|
|||
import warnings
|
||||
from .commanderrors import UnsupportedError
|
||||
from .commandinterface import CommandInterface
|
||||
from ..utils import asyncify
|
||||
|
||||
|
||||
class CommandData:
|
||||
|
@ -12,6 +13,18 @@ class CommandData:
|
|||
else:
|
||||
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:
|
||||
"""Send a text message to the channel where the call was made.
|
||||
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
# 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
|
||||
|
||||
# Enter the commands of your Pack here!
|
||||
available_commands = [
|
||||
RollCommand,
|
||||
DiceCommand,
|
||||
DndactiveCommand,
|
||||
DndinfoCommand,
|
||||
DndnewCommand,
|
||||
DndeditCommand,
|
||||
DndrollCommand,
|
||||
]
|
||||
|
||||
# Don't change this, it should automatically generate __all__
|
||||
|
|
|
@ -1,12 +1,52 @@
|
|||
from royalnet.commands import *
|
||||
from royalnet.utils import asyncify
|
||||
from ..tables import DndCharacter, DndActiveCharacter
|
||||
|
||||
|
||||
class DndactiveCommand(Command):
|
||||
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:
|
||||
...
|
||||
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]!")
|
||||
|
|
59
royalnet/packs/rpg/commands/dndedit.py
Normal file
59
royalnet/packs/rpg/commands/dndedit.py
Normal 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!")
|
19
royalnet/packs/rpg/commands/dndinfo.py
Normal file
19
royalnet/packs/rpg/commands/dndinfo.py
Normal 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())
|
|
@ -5,9 +5,9 @@ from ..tables import DndCharacter
|
|||
class DndnewCommand(Command):
|
||||
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" \
|
||||
"LV {level}\n" \
|
||||
|
@ -19,11 +19,41 @@ class DndnewCommand(Command):
|
|||
"WIS {wisdom}\n" \
|
||||
"CHA {charisma}\n" \
|
||||
"\n" \
|
||||
"MAXHP {maxhp}\n" \
|
||||
"AC {armorclass}"
|
||||
"MAXHP {max_hp}\n" \
|
||||
"AC {armor_class}"
|
||||
|
||||
tables = {}
|
||||
tables = {DndCharacter}
|
||||
|
||||
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!")
|
||||
|
|
88
royalnet/packs/rpg/commands/dndroll.py
Normal file
88
royalnet/packs/rpg/commands/dndroll.py
Normal 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]")
|
|
@ -16,11 +16,11 @@ class DndActiveCharacter:
|
|||
|
||||
@declared_attr
|
||||
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
|
||||
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):
|
||||
return f"<{self.__class__.__qualname__} for {self.user_id}: {self.character_id}>"
|
||||
|
|
|
@ -90,6 +90,9 @@ class DndCharacter:
|
|||
return f"<{self.__class__.__qualname__} {self.name}>"
|
||||
|
||||
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" \
|
||||
|
|
|
@ -22,15 +22,18 @@ def andformat(l: typing.List[str], middle=", ", final=" and ") -> str:
|
|||
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.
|
||||
|
||||
Parameters:
|
||||
i: the :py:class:`int` to convert.
|
||||
empty_if_zero: Return an empty string if ``i`` is zero.
|
||||
|
||||
Returns:
|
||||
The resulting :py:class:`str`."""
|
||||
if i >= 0:
|
||||
if i == 0 and empty_if_zero:
|
||||
return ""
|
||||
if i > 0:
|
||||
return f"+{i}"
|
||||
return str(i)
|
||||
|
||||
|
|
Loading…
Reference in a new issue