-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(chargen): major overhaul of
/character create
(#78)
* feat: add `ghoul` and `changeling` to character classes * fix(utils): function to select random vampire clan * fix: update constants * refactor(chargen): use new rng chargen engine * fix(chargen): rebuild the chargen wizard * refactor(constants): use `TraitCategories` enum * build(deps): update dependencies * feat(chargen): rename characters * feat(chargen): reallocate dots * build(deps): update dependencies * docs: improve comments and docstrings * test: combine character tests * fix(character): use enum for character sheet sections * refactor: add traits by class to constants * feat(chargen): virtues and backgrounds * feat(chargen): apply concept special abilities * feat(chargen): spend freebie points * build(deps): update dependencies * fix(chargen): improve character sheet displays * fix(chargen): improve willpower, humanity, conviction * build(deps): bump deps * refactor(database): refactor db migrations * feat(chargen): create hunters according to book
- Loading branch information
1 parent
6cd48f5
commit 1349817
Showing
42 changed files
with
5,014 additions
and
1,822 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,24 @@ | ||
# Todo List | ||
|
||
- [ ] Character: Add full character generator | ||
- [ ] Character: Add class/concept generator | ||
- [ ] Campaign: Rework campaigns to be the backbone of gameplay | ||
- [ ] Campaign: Renumber chapters | ||
- [ ] Campaign: View any campaign, not just active one | ||
- [ ] Campaign: Associate characters with campaigns | ||
- [ ] Campaign: If only one campaign, always set it as active | ||
- [ ] Campaign: Improve campaign paginator view | ||
- [ ] Campaign: Renumber chapters | ||
- [ ] Campaign: Rework campaigns to be the backbone of gameplay | ||
- [ ] Campaign: View any campaign, not just active one | ||
- [ ] CharGen: Add backgrounds to freebie point picker | ||
- [ ] CharGen: Add edges for hunters | ||
- [ ] CharGen: Add merits/flaws to freebie point picker | ||
- [ ] CharGen: Add mages | ||
- [ ] CharGen: Add werewolfs | ||
- [ ] CharGen: Add ghouls | ||
- [ ] CharGen: Add changelings | ||
- [ ] Database: Move classes and clans to constants | ||
- [ ] Gameplay: Button to create macro from rolling traits | ||
- [ ] Statistics: Pull stats based on timeframe | ||
- [ ] Storyteller: Add notes | ||
- [ ] CharGen: Adjust hunter virtues according to this: Virtues are awarded to or imposed upon your character when she is imbued. You have three starting points to spend in any of the Virtues. However, your rating in your character's "creed Virtue" - her primary Virtue - cannot be exceeded by her score in any other Virtue. Thus, a Defender's Zeal rating cannot be exceeded by her Vision or Mercy ratings - she could have 3 Zeal; 2 Zeal, 1 Vision and 0 Mercy; or one point in each. Likewise, a Redeemer's Mercy cannot be exceeded by her Zeal or Vision ratings. | ||
- [ ] Refactor: Centralize pagination for long responses | ||
- [ ] Refactor: Move testable logic out of cogs and to services or db models | ||
- [ ] Tests: Add tests for converters | ||
- [ ] Tests: Add tests for cogs | ||
- [ ] Statistics: Pull stats based on timeframe | ||
- [ ] Storyteller: Add notes | ||
- [ ] Tests: Increase coverage | ||
- [ ] Users: Add notes per user |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
"""Models for working with characters.""" | ||
from .add_from_sheet import AddFromSheetWizard | ||
from .chargen import CharGenWizard | ||
from .reallocate_dots import DotsReallocationWizard | ||
|
||
__all__ = ["AddFromSheetWizard", "CharGenWizard", "DotsReallocationWizard"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
"""A wizard that walks the user through the character creation process.""" | ||
import asyncio | ||
import uuid | ||
from typing import Any | ||
|
||
import discord | ||
from discord.ui import Button | ||
from loguru import logger | ||
|
||
from valentina.constants import MAX_BUTTONS_PER_ROW, EmbedColor | ||
from valentina.models.db_tables import Trait | ||
from valentina.utils.helpers import get_max_trait_value | ||
|
||
|
||
class RatingView(discord.ui.View): | ||
"""A View that lets the user select a rating.""" | ||
|
||
def __init__( # type: ignore [no-untyped-def] | ||
self, | ||
trait: Trait, | ||
callback, | ||
failback, | ||
) -> None: | ||
"""Initialize the view.""" | ||
super().__init__(timeout=300) | ||
self.callback = callback | ||
self.failback = failback | ||
|
||
self.trait_id = trait.id | ||
self.trait_name = trait.name | ||
self.trait_category = trait.category.name | ||
self.trait_max_value = get_max_trait_value(self.trait_name, self.trait_category) | ||
self.ratings: dict[str, int] = {} | ||
self.response: int = None | ||
self.last_interaction = None | ||
|
||
for rating in range(1, self.trait_max_value + 1): | ||
button_id = str(uuid.uuid4()) | ||
self.ratings[button_id] = rating | ||
|
||
# Calculate the row number for the button | ||
row = 1 if rating <= MAX_BUTTONS_PER_ROW else 0 | ||
|
||
button: Button = Button( | ||
label=str(rating), custom_id=button_id, style=discord.ButtonStyle.primary, row=row | ||
) | ||
button.callback = self.button_pressed # type: ignore [method-assign] | ||
self.add_item(button) | ||
|
||
# Add the 0 button at the end, so it appears at the bottom | ||
zero_button_id = str(uuid.uuid4()) | ||
self.ratings[zero_button_id] = 0 | ||
zero_button: Button = Button( | ||
label="0", custom_id=zero_button_id, style=discord.ButtonStyle.secondary, row=2 | ||
) | ||
zero_button.callback = self.button_pressed # type: ignore [method-assign] | ||
self.add_item(zero_button) | ||
|
||
async def button_pressed(self, interaction) -> None: # type: ignore [no-untyped-def] | ||
"""Respond to the button.""" | ||
button_id = interaction.data["custom_id"] | ||
rating = self.ratings.get(button_id, 0) | ||
self.last_interaction = interaction | ||
|
||
await self.callback(rating, interaction) | ||
|
||
|
||
class AddFromSheetWizard: | ||
"""A character generation wizard that walks the user through setting a value for each trait. This is used for entering a character that has already been created from a physical character sheet.""" | ||
|
||
def __init__( | ||
self, | ||
ctx: discord.ApplicationContext, | ||
all_traits: list[Trait], | ||
first_name: str | None = None, | ||
last_name: str | None = None, | ||
nickname: str | None = None, | ||
) -> None: | ||
self.ctx = ctx | ||
self.msg = None | ||
self.all_traits = all_traits | ||
self.assigned_traits: list[tuple[Trait, int]] = [] | ||
self.view: discord.ui.View = None | ||
|
||
self.name = first_name.title() | ||
self.name += f" ({nickname.title()})" if nickname else "" | ||
self.name += f" {last_name.title() }" if last_name else "" | ||
|
||
async def begin_chargen(self) -> None: | ||
"""Start the chargen wizard.""" | ||
await self.__send_messages() | ||
|
||
async def wait_until_done(self) -> list[tuple[Trait, int]]: | ||
"""Wait until the wizard is done.""" | ||
while self.all_traits: | ||
await asyncio.sleep(1) # Wait a bit then check again | ||
|
||
return self.assigned_traits | ||
|
||
async def __view_callback(self, rating: int, interaction: discord.Interaction) -> None: | ||
"""Assign the next trait. | ||
Assign a value to the previously rated trait and display the next trait or finish creating the character if finished. | ||
Args: | ||
rating (int): The value for the next rating in the list. | ||
interaction (discord.Interaction): The interaction that triggered | ||
""" | ||
# Remove the first trait from the list and assign it | ||
previously_rated_trait = self.all_traits.pop(0) | ||
self.assigned_traits.append((previously_rated_trait, rating)) | ||
|
||
if not self.all_traits: | ||
# We're finished; create the character | ||
await self.__finalize_character() | ||
else: | ||
await self.__send_messages( | ||
message=f"`{previously_rated_trait.name} set to {rating}`", | ||
interaction=interaction, | ||
) | ||
|
||
async def __finalize_character( | ||
self, | ||
) -> None: | ||
"""Add the character to the database and inform the user they are done.""" | ||
embed = discord.Embed( | ||
title="Success!", | ||
description=f"{self.name} has been created", | ||
color=EmbedColor.INFO.value, | ||
) | ||
embed.set_author( | ||
name=f"Valentina on {self.ctx.guild.name}", icon_url=self.ctx.guild.icon or "" | ||
) | ||
embed.add_field(name="Make a mistake?", value="Use `/character update trait`", inline=False) | ||
embed.add_field( | ||
name="Need to add a trait?", value="Use `/character add trait`", inline=False | ||
) | ||
|
||
embed.set_footer(text="See /help for further details") | ||
|
||
button: discord.ui.Button = Button( | ||
label=f"Back to {self.ctx.guild.name}", url=self.ctx.guild.jump_url | ||
) | ||
|
||
self.view.stop() | ||
await self.edit_message(embed=embed, view=discord.ui.View(button)) | ||
|
||
async def __send_messages( | ||
self, *, interaction: discord.Interaction | None = None, message: str | None = None | ||
) -> None: | ||
"""Query a trait.""" | ||
trait_to_be_rated = self.all_traits[0] | ||
|
||
description = "This wizard will guide you through the character creation process.\n\n" | ||
|
||
if message is not None: | ||
description = message | ||
|
||
embed = discord.Embed( | ||
title=f"Select the rating for: {trait_to_be_rated.name}", | ||
description=description, | ||
color=0x7777FF, | ||
) | ||
embed.set_author( | ||
name=f"Creating {self.name}", | ||
icon_url=self.ctx.guild.icon or "", | ||
) | ||
embed.set_footer(text="Your character will not be saved until you have entered all traits.") | ||
|
||
# Build the view with the first trait in the list. (Note, it is removed from the list in the callback) | ||
|
||
self.view = RatingView(trait_to_be_rated, self.__view_callback, self.__timeout) | ||
|
||
if self.msg is None: | ||
# Send DM with the character generation wizard as a DM. This is the first message. | ||
self.msg = await self.ctx.author.send(embed=embed, view=self.view) | ||
|
||
# Respond in-channel to check DM | ||
await self.ctx.respond( | ||
"Please check your DMs! I hope you have your character sheet ready.", | ||
ephemeral=True, | ||
) | ||
else: | ||
# Subsequent sends, edit the interaction of the DM | ||
await interaction.response.edit_message(embed=embed, view=self.view) # type: ignore [unreachable] | ||
|
||
async def __timeout(self) -> None: | ||
"""Inform the user they took too long.""" | ||
errmsg = f"Due to inactivity, your character generation on **{self.ctx.guild.name}** has been canceled." | ||
await self.edit_message(content=errmsg, embed=None, view=None) | ||
logger.info("CHARGEN: Timed out") | ||
|
||
@property | ||
def edit_message(self) -> Any: | ||
"""Get the proper edit method for editing our message outside of an interaction.""" | ||
if self.msg: | ||
return self.msg.edit # type: ignore [unreachable] | ||
return self.ctx.respond |
Oops, something went wrong.