diff --git a/TODO.md b/TODO.md index 4522cb6a..dd95640b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,6 @@ # Todo List - [ ] Campaign: add notes in channel without needing a command -- [ ] Campaign: Delete book channel when book is deleted -- [ ] Campaign: View any campaign, not just active one - [ ] CharGen: Add backgrounds to freebie point picker - [ ] CharGen: Add changelings - [ ] CharGen: Add ghouls @@ -14,16 +12,19 @@ - [ ] Gameplay: Button to create macro from rolling traits - [ ] Refactor: Move testable logic out of cogs and to services or db models - [ ] Statistics: Pull stats based on timeframe +- [ ] Statistics: Track per book stats - [ ] Storyteller: Add notes - [ ] Tests: Increase coverage - [x] Campaign: Add books to campaigns. Books contain chapters. - [x] Campaign: Associate characters with campaigns - [x] Campaign: create channels for each character +- [x] Campaign: Delete book channel when book is deleted - [x] Campaign: Delete channels when campaign is deleted - [x] Campaign: If only one campaign, always set it as active - [x] Campaign: Improve campaign paginator view - [x] Campaign: Renumber chapters - [x] Campaign: Rework campaigns to be the backbone of gameplay +- [x] Campaign: View any campaign, not just active one - [x] Character: assign to other campaign - [x] Character: rethink inventory - [x] CharGen: Add edges for hunters diff --git a/poetry.lock b/poetry.lock index 846f388e..01a460a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -208,17 +208,17 @@ test = ["asgi-lifespan (>=1.0.1)", "dnspython (>=2.1.0)", "fastapi (>=0.100)", " [[package]] name = "boto3" -version = "1.34.127" +version = "1.34.128" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.127-py3-none-any.whl", hash = "sha256:d370befe4fb7aea5bc383057d7dad18dda5d0cf3cd3295915bcc8c8c4191905c"}, - {file = "boto3-1.34.127.tar.gz", hash = "sha256:58ccdeae3a96811ecc9d5d866d8226faadbd0ee1891756e4a04d5186e9a57a64"}, + {file = "boto3-1.34.128-py3-none-any.whl", hash = "sha256:a048ff980a81cd652724a73bc496c519b336fabe19cc8bfc6c53b2ff6eb22c7b"}, + {file = "boto3-1.34.128.tar.gz", hash = "sha256:43a6e99f53a8d34b3b4dbe424dbcc6b894350dc41a85b0af7c7bc24a7ec2cead"}, ] [package.dependencies] -botocore = ">=1.34.127,<1.35.0" +botocore = ">=1.34.128,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -227,13 +227,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.127" +version = "1.34.128" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.127-py3-none-any.whl", hash = "sha256:e14fa28c8bb141de965e700f88b196d17c67a703c7f0f5c7e14f7dd1cf636011"}, - {file = "botocore-1.34.127.tar.gz", hash = "sha256:a377871742c40603d559103f19acb7bc93cfaf285e68f21b81637ec396099877"}, + {file = "botocore-1.34.128-py3-none-any.whl", hash = "sha256:db67fda136c372ab3fa432580c819c89ba18d28a6152a4d2a7ea40d44082892e"}, + {file = "botocore-1.34.128.tar.gz", hash = "sha256:8d8e03f7c8c080ecafda72036eb3b482d649f8417c90b5dca33b7c2c47adb0c9"}, ] [package.dependencies] diff --git a/src/valentina/cogs/campaign.py b/src/valentina/cogs/campaign.py index ff0f8f61..31a6b27d 100644 --- a/src/valentina/cogs/campaign.py +++ b/src/valentina/cogs/campaign.py @@ -34,7 +34,7 @@ ValidChapterNumber, ValidYYYYMMDD, ) -from valentina.utils.discord_utils import book_from_channel +from valentina.utils.discord_utils import book_from_channel, campaign_from_channel from valentina.utils.helpers import truncate_string from valentina.views import ( BookModal, @@ -193,7 +193,7 @@ async def current_date( ), ) -> None: """Set current date of a campaign.""" - campaign = await ctx.fetch_active_campaign() + campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() campaign.date_in_game = date await campaign.save() @@ -242,7 +242,7 @@ async def delete_campaign( @campaign.command(name="view", description="View a campaign") async def view_campaign(self, ctx: ValentinaContext) -> None: """View a campaign.""" - campaign = await ctx.fetch_active_campaign() + campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() cv = CampaignViewer(ctx, campaign, max_chars=1000) paginator = await cv.display() @@ -374,7 +374,7 @@ async def create_npc( ), ) -> None: """Create a new NPC.""" - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() modal = NPCModal(title=truncate_string("Create new NPC", 45)) await ctx.send_modal(modal) @@ -419,7 +419,7 @@ async def list_npcs( ), ) -> None: """List all NPCs.""" - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() if len(active_campaign.npcs) == 0: await present_embed( @@ -465,7 +465,7 @@ async def edit_npc( if not await self.check_permissions(ctx): return - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() try: npc = active_campaign.npcs[index] except IndexError: @@ -528,7 +528,7 @@ async def delete_npc( if not await self.check_permissions(ctx): return - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() try: npc = active_campaign.npcs[index] except IndexError: @@ -570,7 +570,7 @@ async def create_book( if not await self.check_permissions(ctx): return - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() modal = BookModal(title=truncate_string("Create new book", 45)) await ctx.send_modal(modal) @@ -619,7 +619,7 @@ async def list_books( ), ) -> None: """List all books.""" - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() all_books = await active_campaign.fetch_books() if len(all_books) == 0: @@ -669,7 +669,7 @@ async def edit_book( if not await self.check_permissions(ctx): return - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() original_name = book.name modal = BookModal(title=truncate_string(f"Edit book {book.name}", 45), book=book) @@ -721,7 +721,7 @@ async def delete_book( if not await self.check_permissions(ctx): return - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() title = f"Delete book `{book.number}. {book.name}` from `{active_campaign.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( @@ -774,7 +774,7 @@ async def renumber_books( ) return - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() original_number = book.number title = ( @@ -1120,7 +1120,7 @@ async def create_note( ), ) -> None: """Create a new note.""" - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() modal = NoteModal(title=truncate_string("Create new note", 45)) await ctx.send_modal(modal) @@ -1161,8 +1161,7 @@ async def list_notes( ), ) -> None: """List all notes.""" - guild = await Guild.get(ctx.guild.id, fetch_links=True) - active_campaign = await guild.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() if len(active_campaign.notes) == 0: await present_embed( @@ -1204,7 +1203,7 @@ async def edit_note( ), ) -> None: """Edit a note.""" - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() note = active_campaign.notes[index] modal = NoteModal(title=truncate_string("Edit note", 45), note=note) @@ -1253,7 +1252,7 @@ async def delete_note( if not await self.check_permissions(ctx): return - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() note = active_campaign.notes[index] title = f"Delete note: `{note.name}` from `{active_campaign.name}`" diff --git a/src/valentina/cogs/experience.py b/src/valentina/cogs/experience.py index ac18df19..256532ea 100644 --- a/src/valentina/cogs/experience.py +++ b/src/valentina/cogs/experience.py @@ -14,6 +14,7 @@ ValidCharacterObject, ValidCharTrait, ) +from valentina.utils.discord_utils import campaign_from_channel from valentina.utils.helpers import get_trait_multiplier, get_trait_new_value from valentina.views import confirm_action, present_embed @@ -61,7 +62,7 @@ async def xp_add( ) return - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() title = f"Add `{amount}` xp to `{user.name}`" description = "View experience with `/user_info`" @@ -110,7 +111,7 @@ async def cp_add( ) return - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() title = f"Add `{amount}` cool {p.plural_noun('point', amount)} to `{user.name}`" description = "View cool points with `/user_info`" @@ -189,7 +190,7 @@ async def xp_spend( # Make the updates user = await User.get(ctx.author.id) - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() await user.spend_campaign_xp(active_campaign, upgrade_cost) trait.value = new_trait_value diff --git a/src/valentina/cogs/gameplay.py b/src/valentina/cogs/gameplay.py index 750aaef5..dcab327c 100644 --- a/src/valentina/cogs/gameplay.py +++ b/src/valentina/cogs/gameplay.py @@ -18,7 +18,7 @@ select_macro, ) from valentina.utils.converters import ValidTraitFromID -from valentina.utils.discord_utils import character_from_channel +from valentina.utils.discord_utils import campaign_from_channel, character_from_channel from valentina.utils.perform_roll import perform_roll from valentina.views import present_embed @@ -65,7 +65,7 @@ async def throw( character = await character_from_channel(ctx) or await ctx.fetch_active_character() if desperation > 0: - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() if active_campaign.desperation == 0 or desperation > 5: # noqa: PLR2004 await present_embed( ctx, @@ -130,7 +130,7 @@ async def traits( ) if desperation > 0: - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() if active_campaign.desperation == 0 or desperation > 5: # noqa: PLR2004 await present_embed( ctx, @@ -210,7 +210,7 @@ async def roll_macro( raise commands.BadArgument(msg) if desperation > 0: - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() if active_campaign.desperation == 0 or desperation > 5: # noqa: PLR2004 await present_embed( ctx, diff --git a/src/valentina/cogs/storyteller.py b/src/valentina/cogs/storyteller.py index d8144c15..1e0be42e 100644 --- a/src/valentina/cogs/storyteller.py +++ b/src/valentina/cogs/storyteller.py @@ -43,6 +43,7 @@ ValidImageURL, ValidTraitCategory, ) +from valentina.utils.discord_utils import campaign_from_channel from valentina.utils.helpers import ( fetch_data_from_url, ) @@ -92,7 +93,7 @@ def __init__(self, bot: Valentina) -> None: @storyteller.command(name="set_danger", description="Set the danger level") async def set_danger(self, ctx: ValentinaContext, danger: int) -> None: """Set the danger level for a campaign.""" - campaign = await ctx.fetch_active_campaign() + campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() title = f"Set danger level to {danger}" is_confirmed, interaction, confirmation_embed = await confirm_action(ctx, title, audit=True) @@ -108,7 +109,7 @@ async def set_danger(self, ctx: ValentinaContext, danger: int) -> None: @storyteller.command(name="set_desperation", description="Set the desperation level") async def set_desperation(self, ctx: ValentinaContext, desperation: int) -> None: """Set the desperation level for a campaign.""" - campaign = await ctx.fetch_active_campaign() + campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() title = f"Set desperation level to {desperation}" is_confirmed, interaction, confirmation_embed = await confirm_action(ctx, title, audit=True) diff --git a/src/valentina/utils/autocomplete.py b/src/valentina/utils/autocomplete.py index d7fdbfe3..c64e0e19 100644 --- a/src/valentina/utils/autocomplete.py +++ b/src/valentina/utils/autocomplete.py @@ -18,7 +18,11 @@ ) from valentina.models import AWSService, Campaign, ChangelogParser, Character, Guild, User from valentina.models.bot import Valentina -from valentina.utils.discord_utils import book_from_channel, character_from_channel +from valentina.utils.discord_utils import ( + book_from_channel, + campaign_from_channel, + character_from_channel, +) from valentina.utils.helpers import truncate_string MAX_OPTION_LENGTH = 99 @@ -117,7 +121,7 @@ async def select_book(ctx: discord.AutocompleteContext) -> list[OptionChoice]: """ # Fetch the active campaign guild = await Guild.get(ctx.interaction.guild.id, fetch_links=True) - active_campaign = await guild.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await guild.fetch_active_campaign() books = await active_campaign.fetch_books() if not active_campaign: @@ -214,7 +218,7 @@ async def select_chapter_old( """ # Fetch the active campaign guild = await Guild.get(ctx.interaction.guild.id, fetch_links=True) - active_campaign = await guild.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await guild.fetch_active_campaign() if not active_campaign: return [OptionChoice("No active campaign", 1000)] @@ -508,7 +512,7 @@ async def select_desperation_dice( # Fetch the active campaign guild = await Guild.get(ctx.interaction.guild.id, fetch_links=True) - active_campaign = await guild.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await guild.fetch_active_campaign() desperation_dice = active_campaign.desperation if not active_campaign: @@ -568,7 +572,7 @@ async def select_note(ctx: discord.AutocompleteContext) -> list[OptionChoice]: """ # Fetch the active campaign guild = await Guild.get(ctx.interaction.guild.id, fetch_links=True) - active_campaign = await guild.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await guild.fetch_active_campaign() if not active_campaign: return [OptionChoice("No active campaign", 1000)] @@ -596,7 +600,7 @@ async def select_npc(ctx: discord.AutocompleteContext) -> list[OptionChoice]: """ # Fetch the active campaign guild = await Guild.get(ctx.interaction.guild.id, fetch_links=True) - active_campaign = await guild.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await guild.fetch_active_campaign() if not active_campaign: return [OptionChoice("No active campaign", 1000)] diff --git a/src/valentina/utils/converters.py b/src/valentina/utils/converters.py index 2ef5e0df..af035270 100644 --- a/src/valentina/utils/converters.py +++ b/src/valentina/utils/converters.py @@ -25,7 +25,7 @@ Guild, InventoryItem, ) -from valentina.utils.discord_utils import book_from_channel +from valentina.utils.discord_utils import book_from_channel, campaign_from_channel class CampaignChapterConverter(Converter): @@ -37,7 +37,7 @@ class CampaignChapterConverter(Converter): async def convert(self, ctx: commands.Context, argument: str) -> CampaignChapter: """Validate and normalize traits.""" guild = await Guild.get(ctx.guild.id, fetch_links=True) - active_campaign = await guild.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await guild.fetch_active_campaign() try: chapter_number = int(argument) @@ -100,7 +100,7 @@ class ValidBookNumber(Converter): async def convert(self, ctx: commands.Context, argument: str) -> int: """Validate and normalize book numbers.""" guild = await Guild.get(ctx.guild.id, fetch_links=True) - active_campaign = await guild.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await guild.fetch_active_campaign() campaign_book_numbers = [x.number for x in await active_campaign.fetch_books()] try: diff --git a/src/valentina/utils/discord_utils.py b/src/valentina/utils/discord_utils.py index 915c97ba..3cf30581 100644 --- a/src/valentina/utils/discord_utils.py +++ b/src/valentina/utils/discord_utils.py @@ -7,7 +7,7 @@ from loguru import logger from valentina.constants import ChannelPermission -from valentina.models import CampaignBook, Character +from valentina.models import Campaign, CampaignBook, Character from .errors import BotMissingPermissionsError @@ -217,3 +217,24 @@ async def book_from_channel( ) return await CampaignBook.find_one(CampaignBook.channel == discord_channel.id, fetch_links=True) + + +async def campaign_from_channel( + ctx: discord.ApplicationContext | discord.AutocompleteContext | commands.Context, +) -> Campaign | None: + """Get the campaign from a campaign channel. + + Args: + ctx (discord.ApplicationContext|discord.AutocompleteContext): The context containing the channel object. + + Returns: + CampaignBook|None: The CampaignBook object if found; otherwise, None. + """ + discord_channel = ( + ctx.interaction.channel if isinstance(ctx, discord.AutocompleteContext) else ctx.channel + ) + category = discord_channel.category + + return await Campaign.find_one( + Campaign.channel_campaign_category == category.id, fetch_links=True + ) diff --git a/src/valentina/utils/perform_roll.py b/src/valentina/utils/perform_roll.py index 3c7a2f36..594b9aa9 100644 --- a/src/valentina/utils/perform_roll.py +++ b/src/valentina/utils/perform_roll.py @@ -5,6 +5,7 @@ from valentina.constants import EmbedColor, Emoji from valentina.models import Character, CharacterTrait, DiceRoll from valentina.models.bot import ValentinaContext +from valentina.utils.discord_utils import campaign_from_channel from valentina.views import ReRollButton, RollDisplay @@ -71,7 +72,7 @@ async def perform_roll( # pragma: no cover await view.wait() if view.overreach: - active_campaign = await ctx.fetch_active_campaign() + active_campaign = await campaign_from_channel(ctx) or await ctx.fetch_active_campaign() if active_campaign.danger < 5: # noqa: PLR2004 active_campaign.danger += 1 await active_campaign.save()