Skip to content

Commit

Permalink
feat(chargen): major overhaul of /character create (#78)
Browse files Browse the repository at this point in the history
* 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
natelandau authored Oct 16, 2023
1 parent 6cd48f5 commit 1349817
Show file tree
Hide file tree
Showing 42 changed files with 5,014 additions and 1,822 deletions.
1 change: 1 addition & 0 deletions .github/workflows/automated-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
install.python-poetry.org:443
pypi.org:443
python-poetry.org:443
randomuser.me:443
storage.googleapis.com:443
uploader.codecov.io:443
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ default_stages: [commit, manual]
fail_fast: true
repos:
- repo: "https://github.com/commitizen-tools/commitizen"
rev: 3.10.0
rev: 3.10.1
hooks:
- id: commitizen
# - id: commitizen-branch
Expand All @@ -25,7 +25,7 @@ repos:
- id: text-unicode-replacement-char

- repo: "https://github.com/pre-commit/pre-commit-hooks"
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-ast
Expand Down Expand Up @@ -66,12 +66,12 @@ repos:
exclude: tests/

- repo: "https://github.com/jendrikseipp/vulture"
rev: "v2.9.1"
rev: "v2.10"
hooks:
- id: vulture

- repo: "https://github.com/crate-ci/typos"
rev: v1.16.15
rev: v1.16.19
hooks:
- id: typos

Expand Down
24 changes: 15 additions & 9 deletions TODO.md
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
431 changes: 216 additions & 215 deletions poetry.lock

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@
[tool.poetry.dependencies]
aiofiles = "^23.2.1"
arrow = "^1.3.0"
boto3 = "^1.28.49"
boto3 = "^1.28.63"
inflect = "^7.0.0"
loguru = "^0.7.2"
numpy = "^1.25.2"
peewee = "^3.16.3"
peewee = "^3.17.0"
py-cord = "^2.4.1"
python = "^3.11"
python-dotenv = "^1.0.0"
rich = "^13.6.0"
semver = "^3.0.1"
semver = "^3.0.2"
typer = { extras = ["all"], version = "^0.9.0" }

[tool.poetry.group.test.dependencies]
Expand All @@ -42,17 +42,17 @@

[tool.poetry.group.dev.dependencies]
black = "^23.9.1"
commitizen = "^3.9.0"
commitizen = "^3.10.1"
coverage = "^7.3.1"
mypy = "^1.5.0"
mypy = "^1.6.0"
pdoc = "^14.0.0"
poethepoet = "^0.22.1"
pre-commit = "^3.4.0"
pre-commit = "^3.5.0"
ruff = "^0.0.292"
shellcheck-py = "^0.9.0.5"
types-aiofiles = "^23.2.0.0"
typos = "^1.16.11"
vulture = "^2.9.1"
typos = "^1.16.19"
vulture = "^2.10"

[tool.black]
line-length = 100
Expand Down
6 changes: 6 additions & 0 deletions src/valentina/characters/__init__.py
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"]
198 changes: 198 additions & 0 deletions src/valentina/characters/add_from_sheet.py
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
Loading

0 comments on commit 1349817

Please sign in to comment.