diff --git a/royalnet/commands/royalgames/__init__.py b/royalnet/commands/royalgames/__init__.py index 5566826f..2ac2e64e 100644 --- a/royalnet/commands/royalgames/__init__.py +++ b/royalnet/commands/royalgames/__init__.py @@ -21,6 +21,7 @@ from .smecds import SmecdsCommand from .summon import SummonCommand from .videochannel import VideochannelCommand from .dnditem import DnditemCommand +from .dndspell import DndspellCommand __all__ = [ "CiaoruoziCommand", @@ -40,5 +41,6 @@ __all__ = [ "SmecdsCommand", "SummonCommand", "VideochannelCommand", - "DnditemCommand" + "DnditemCommand", + "DndspellCommand" ] diff --git a/royalnet/commands/royalgames/dnditem.py b/royalnet/commands/royalgames/dnditem.py index e88a6c49..d86c5875 100644 --- a/royalnet/commands/royalgames/dnditem.py +++ b/royalnet/commands/royalgames/dnditem.py @@ -5,6 +5,7 @@ from ..command import Command from ..commandargs import CommandArgs from ..commanddata import CommandData from ..commandinterface import CommandInterface +from ...utils import parse_5etools_entry class DnditemCommand(Command): @@ -36,32 +37,6 @@ class DnditemCommand(Command): for item in j["baseitem"]: self._dnddata.add(item) - def _parse_entry(self, entry): - if isinstance(entry, str): - return entry - elif isinstance(entry, dict): - string = "" - if entry["type"] == "entries": - string += f'[b]{entry.get("name", "")}[/b]\n' - for subentry in entry["entries"]: - string += self._parse_entry(subentry) - elif entry["type"] == "table": - string += "[i][table hidden][/i]" - # for label in entry["colLabels"]: - # string += f"| {label} " - # string += "|" - # for row in entry["rows"]: - # for column in row: - # string += f"| {self._parse_entry(column)} " - # string += "|\n" - elif entry["type"] == "cell": - return self._parse_entry(entry["entry"]) - else: - string += "[i][unknown type][/i]" - else: - return "[/i][unknown data][/i]" - return string - async def run(self, args: CommandArgs, data: CommandData) -> None: if self._dnddata is None: await data.reply("⚠️ Il database degli oggetti di D&D non è ancora stato scaricato.") @@ -78,6 +53,6 @@ class DnditemCommand(Command): f'Rarity: [b]{result["rarity"] if result.get("rarity", "None") != "None" else "Mundane"}[/b]\n' \ f'\n' for entry in result.get("entries", []): - string += self._parse_entry(entry) + string += parse_5etools_entry(entry) string += "\n\n" await data.reply(string) diff --git a/royalnet/commands/royalgames/dndspell.py b/royalnet/commands/royalgames/dndspell.py new file mode 100644 index 00000000..bee96cde --- /dev/null +++ b/royalnet/commands/royalgames/dndspell.py @@ -0,0 +1,111 @@ +import typing +import aiohttp +import sortedcontainers +from ..command import Command +from ..commandargs import CommandArgs +from ..commanddata import CommandData +from ..commandinterface import CommandInterface +from ...utils import parse_5etools_entry, ordinalformat + + +class DndspellCommand(Command): + name: str = "dndspell" + + description: str = "Ottieni informazioni su una magia di D&D5e." + + syntax = "(nomemagia)" + + _dnddata: sortedcontainers.SortedKeyList = None + + def __init__(self, interface: CommandInterface): + super().__init__(interface) + interface.loop.create_task(self._fetch_dnddata()) + + async def _fetch_dnddata(self): + self._dnddata = self._dnddata = sortedcontainers.SortedKeyList([], key=lambda i: i["name"].lower()) + async with aiohttp.ClientSession() as session: + for url in [ + "https://5e.tools/data/spells/spells-ai.json", + "https://5e.tools/data/spells/spells-ggr.json", + "https://5e.tools/data/spells/spells-llk.json", + "https://5e.tools/data/spells/spells-phb.json", + "https://5e.tools/data/spells/spells-scag.json", + "https://5e.tools/data/spells/spells-stream.json", + "https://5e.tools/data/spells/spells-ua-ar.json", + "https://5e.tools/data/spells/spells-ua-mm.json", + "https://5e.tools/data/spells/spells-ua-ss.json", + "https://5e.tools/data/spells/spells-ua-tobm.json", + "https://5e.tools/data/spells/spells-xge.json" + ]: + async with session.get(url) as response: + j = await response.json() + for spell in j["spell"]: + self._dnddata.add(spell) + + def _parse_spell(self, spell: dict) -> str: + string = f'✨ [b]{spell["name"]}[/b]\n' + if "source" in spell: + string += f'[i]{spell["source"]}, page {spell["page"]}[/i]\n' + string += "\n" + if spell["level"] == 0: + string += f'[b]Cantrip[/b] {spell["school"]}\n' + else: + string += f'[b]{ordinalformat(spell["level"])}[/b] level {spell["school"]}\n' + if "time" in spell: + for time in spell["time"]: + string += f'Cast time: ⌛️ [b]{time["number"]} {time["unit"]}[/b]\n' + if "range" in spell: + if spell["range"]["distance"]["type"] == "touch": + string += "Range: 👉 [b]Touch[/b]\n" + elif spell["range"]["distance"]["type"] == "self": + string += "Range: 👤 [b]Self[/b]\n" + else: + string += f'Range: 🏹 [b]{spell["range"]["distance"]["amount"]} {spell["range"]["distance"]["type"]}[/b] ({spell["range"]["type"]})\n' + if "components" in spell: + string += f'Components: ' + if spell["components"].get("v", False): + string += "👄 [b]Verbal[/b] | " + if spell["components"].get("s", False): + string += "🤙 [b]Somatic[/b] | " + if spell["components"].get("r", False): + # TODO: wtf is this + string += "❓ [b]R...?[/b] | " + if spell["components"].get("m", False): + if "text" in spell["components"]["m"]: + string += f'💎 [b]Material[/b] ([i]{spell["components"]["m"]["text"]}[/i]) | ' + else: + string += f'💎 [b]Material[/b] ([i]{spell["components"]["m"]}[/i]) | ' + string += "\n" + string += "\n" + if "duration" in spell: + for duration in spell["duration"]: + if duration["type"] == "timed": + string += f'Duration: 🕒 [b]{duration["duration"]["amount"]} {duration["duration"]["type"]}[/b]' + elif duration["type"] == "instant": + string += 'Duration: ☁️ [b]Instantaneous[/b]' + elif duration["type"] == "special": + string += 'Duration: ⭐️ [b]Special[/b]' + else: + string += f'Duration: ⚠️[b]UNKNOWN[/b]' + if duration.get("concentration", False): + string += " (requires concentration)" + string += "\n" + if "meta" in spell: + if spell["meta"].get("ritual", False): + string += "Can be casted as ritual\n" + string += "\n" + for entry in spell.get("entries", []): + string += parse_5etools_entry(entry) + string += "\n\n" + for entry in spell.get("entriesHigherLevel", []): + string += parse_5etools_entry(entry) + string += "\n\n" + return string + + async def run(self, args: CommandArgs, data: CommandData) -> None: + if self._dnddata is None: + await data.reply("⚠️ Il database degli oggetti di D&D non è ancora stato scaricato.") + return + search = args.joined().lower() + result = self._dnddata[self._dnddata.bisect_key_left(search)] + await data.reply(self._parse_spell(result)) diff --git a/royalnet/royalgames.py b/royalnet/royalgames.py index 2fb4a87c..1c4ec9a4 100644 --- a/royalnet/royalgames.py +++ b/royalnet/royalgames.py @@ -34,7 +34,8 @@ commands = [ SmecdsCommand, SummonCommand, VideochannelCommand, - DnditemCommand + DnditemCommand, + DndspellCommand ] # noinspection PyUnreachableCode diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py index 6f77b0ae..4a548832 100644 --- a/royalnet/utils/__init__.py +++ b/royalnet/utils/__init__.py @@ -6,8 +6,9 @@ from .safeformat import safeformat from .classdictjanitor import cdj from .sleepuntil import sleep_until from .networkhandler import NetworkHandler -from .formatters import andformat, plusformat, fileformat, ytdldateformat, numberemojiformat, splitstring +from .formatters import andformat, plusformat, fileformat, ytdldateformat, numberemojiformat, splitstring, ordinalformat +from .parse5etoolsentry import parse_5etools_entry __all__ = ["asyncify", "safeformat", "cdj", "sleep_until", "plusformat", "NetworkHandler", "andformat", "plusformat", "fileformat", "ytdldateformat", "numberemojiformat", - "telegram_escape", "discord_escape", "splitstring"] + "telegram_escape", "discord_escape", "splitstring", "parse_5etools_entry", "ordinalformat"] diff --git a/royalnet/utils/formatters.py b/royalnet/utils/formatters.py index 90695fc3..f2eba8c1 100644 --- a/royalnet/utils/formatters.py +++ b/royalnet/utils/formatters.py @@ -78,3 +78,15 @@ def splitstring(s: str, max: int) -> typing.List[str]: l.append(s[:max]) s = s[max:] return l + + +def ordinalformat(number: int): + if 10 <= number % 100 < 20: + return f"{number}th" + if number % 10 == 1: + return f"{number}st" + elif number % 10 == 2: + return f"{number}nd" + elif number % 10 == 3: + return f"{number}rd" + return f"{number}th" diff --git a/royalnet/utils/parse5etoolsentry.py b/royalnet/utils/parse5etoolsentry.py new file mode 100644 index 00000000..b027249c --- /dev/null +++ b/royalnet/utils/parse5etoolsentry.py @@ -0,0 +1,31 @@ +def parse_5etools_entry(entry) -> str: + if isinstance(entry, str): + return entry + elif isinstance(entry, dict): + string = "" + if entry["type"] == "entries": + string += f'[b]{entry.get("name", "")}[/b]\n' + for subentry in entry["entries"]: + string += parse_5etools_entry(subentry) + string += "\n\n" + elif entry["type"] == "table": + string += "[i][table hidden][/i]" + # for label in entry["colLabels"]: + # string += f"| {label} " + # string += "|" + # for row in entry["rows"]: + # for column in row: + # string += f"| {self._parse_entry(column)} " + # string += "|\n" + elif entry["type"] == "cell": + return parse_5etools_entry(entry["entry"]) + elif entry["type"] == "list": + string = "" + for item in entry["items"]: + string += f"- {parse_5etools_entry(item)}\n" + string.rstrip("\n") + else: + string += "[i]⚠️ [unknown type][/i]" + else: + return "[/i]⚠️ [unknown data][/i]" + return string