2022-10-30 16:33:28 +01:00
|
|
|
import collections
|
2022-11-02 22:02:48 +01:00
|
|
|
from collections.abc import Iterable
|
2022-11-09 20:49:57 +01:00
|
|
|
from dataclasses import dataclass
|
2022-12-09 19:00:20 +01:00
|
|
|
from functools import partial
|
2022-10-30 16:33:28 +01:00
|
|
|
|
2022-10-31 00:42:26 +01:00
|
|
|
import markdown
|
2022-12-09 19:00:20 +01:00
|
|
|
from django.core.exceptions import ValidationError
|
2022-10-29 00:32:18 +02:00
|
|
|
from django.db import models
|
2022-11-03 00:02:28 +01:00
|
|
|
from django.db.models import Q
|
2022-10-29 00:32:18 +02:00
|
|
|
from django.db.models.functions import Lower
|
2022-10-30 10:12:49 +01:00
|
|
|
from django.urls import reverse
|
2022-10-29 00:32:18 +02:00
|
|
|
from django_extensions.db.models import TimeStampedModel
|
|
|
|
|
2022-10-30 16:33:28 +01:00
|
|
|
from character.models import Capability, Path
|
2022-10-29 00:32:18 +02:00
|
|
|
from character.models.dice import Dice
|
2022-10-31 00:20:55 +01:00
|
|
|
from character.models.equipment import Weapon
|
2022-10-30 09:44:28 +01:00
|
|
|
from common.models import DocumentedModel, UniquelyNamedModel
|
2022-10-29 00:32:18 +02:00
|
|
|
|
|
|
|
|
2022-10-30 09:44:28 +01:00
|
|
|
class Profile(DocumentedModel, UniquelyNamedModel, TimeStampedModel, models.Model):
|
2022-10-29 00:32:18 +02:00
|
|
|
class MagicalStrength(models.TextChoices):
|
2022-10-30 11:09:46 +01:00
|
|
|
NONE = "NON", "Aucun"
|
2022-10-29 00:32:18 +02:00
|
|
|
INTELLIGENCE = "INT", "Intelligence"
|
2022-10-30 11:09:46 +01:00
|
|
|
WISDOM = "SAG", "Sagesse"
|
|
|
|
CHARISMA = "CHA", "Charisme"
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-10-30 22:18:42 +01:00
|
|
|
class ManaMax(models.IntegerChoices):
|
|
|
|
NO_MANA = 0, "Pas de mana"
|
|
|
|
LEVEL = 1, "1 x niveau + mod. magique"
|
|
|
|
DOUBLE_LEVEL = 2, "2 x niveau + mod. magique"
|
|
|
|
|
2022-10-29 00:32:18 +02:00
|
|
|
magical_strength = models.CharField(
|
2022-10-30 11:09:46 +01:00
|
|
|
max_length=3,
|
|
|
|
choices=MagicalStrength.choices,
|
|
|
|
default=MagicalStrength.NONE,
|
|
|
|
verbose_name="force magique",
|
2022-10-29 00:32:18 +02:00
|
|
|
)
|
2022-10-30 11:09:46 +01:00
|
|
|
life_dice = models.PositiveSmallIntegerField(
|
|
|
|
choices=Dice.choices, verbose_name="dé de vie"
|
|
|
|
)
|
2022-10-30 22:18:42 +01:00
|
|
|
mana_max_compute = models.PositiveSmallIntegerField(
|
|
|
|
choices=ManaMax.choices, verbose_name="calcul mana max", default=ManaMax.NO_MANA
|
|
|
|
)
|
2022-10-30 11:09:46 +01:00
|
|
|
notes = models.TextField(blank=True, verbose_name="notes")
|
|
|
|
|
2022-11-02 23:10:48 +01:00
|
|
|
class Meta(UniquelyNamedModel.Meta, TimeStampedModel.Meta):
|
2022-10-30 11:09:46 +01:00
|
|
|
verbose_name = "Profil"
|
|
|
|
verbose_name_plural = "Profils"
|
2022-10-29 00:32:18 +02:00
|
|
|
|
|
|
|
|
2022-10-30 09:44:28 +01:00
|
|
|
class Race(DocumentedModel, UniquelyNamedModel, TimeStampedModel, models.Model):
|
2022-11-02 23:10:48 +01:00
|
|
|
class Meta(UniquelyNamedModel.Meta, TimeStampedModel.Meta):
|
2022-10-30 11:09:46 +01:00
|
|
|
verbose_name = "Race"
|
|
|
|
verbose_name_plural = "Races"
|
2022-10-29 00:32:18 +02:00
|
|
|
|
|
|
|
|
2022-11-02 21:41:06 +01:00
|
|
|
class HarmfulState(DocumentedModel, UniquelyNamedModel, TimeStampedModel, models.Model):
|
|
|
|
description = models.TextField()
|
|
|
|
icon_url = models.URLField()
|
|
|
|
|
2022-11-02 23:10:48 +01:00
|
|
|
class Meta(UniquelyNamedModel.Meta, TimeStampedModel.Meta):
|
2022-11-02 21:41:06 +01:00
|
|
|
verbose_name = "État préjudiciable"
|
|
|
|
verbose_name_plural = "États préjudiciables"
|
|
|
|
|
|
|
|
|
2022-10-29 00:32:18 +02:00
|
|
|
def modifier(value: int) -> int:
|
2022-10-31 00:42:26 +01:00
|
|
|
if not value:
|
|
|
|
return 0
|
2023-01-29 10:38:41 +01:00
|
|
|
if 1 < value < 10: # noqa: PLR2004
|
2022-10-29 00:32:18 +02:00
|
|
|
value -= 1
|
|
|
|
value -= 10
|
|
|
|
return int(value / 2)
|
|
|
|
|
|
|
|
|
|
|
|
class CharacterManager(models.Manager):
|
|
|
|
def get_by_natural_key(self, name: str, player_id: int):
|
|
|
|
return self.get(name=name, player_id=player_id)
|
|
|
|
|
|
|
|
|
2022-11-02 23:10:48 +01:00
|
|
|
class CharacterQuerySet(models.QuerySet):
|
|
|
|
def managed_by(self, user):
|
2022-12-28 09:27:27 +01:00
|
|
|
"""
|
|
|
|
Return characters managed by the given user.
|
|
|
|
|
|
|
|
Characters are managed by a user if they own the character
|
|
|
|
or if they are the game master for a group in which the character plays.
|
|
|
|
"""
|
2022-11-03 00:02:28 +01:00
|
|
|
from party.models import Party
|
|
|
|
|
|
|
|
return self.filter(
|
|
|
|
Q(player=user) | Q(parties__in=Party.objects.managed_by(user))
|
|
|
|
)
|
2022-11-02 23:10:48 +01:00
|
|
|
|
2022-12-28 09:27:27 +01:00
|
|
|
def mastered_by(self, user):
|
|
|
|
"""Return characters in groups where the given user is the game master."""
|
|
|
|
from party.models import Party
|
|
|
|
|
|
|
|
return self.filter(parties__in=Party.objects.managed_by(user))
|
|
|
|
|
2022-11-02 23:10:48 +01:00
|
|
|
def owned_by(self, user):
|
2022-12-28 09:27:27 +01:00
|
|
|
"""Return characters either owned by the given user."""
|
2022-11-02 23:10:48 +01:00
|
|
|
return self.filter(player=user)
|
|
|
|
|
2022-11-11 08:59:18 +01:00
|
|
|
def friendly_to(self, user):
|
2022-12-28 09:27:27 +01:00
|
|
|
"""
|
|
|
|
Return characters friendly to the given users.
|
|
|
|
|
|
|
|
Friendly characters are either owned by the given user
|
|
|
|
or in a party related to the given user.
|
|
|
|
"""
|
2022-11-11 08:59:18 +01:00
|
|
|
from party.models import Party
|
|
|
|
|
|
|
|
return self.filter(
|
2022-11-16 12:47:56 +01:00
|
|
|
Q(player=user)
|
|
|
|
| Q(parties__in=Party.objects.related_to(user))
|
|
|
|
| Q(invites__in=Party.objects.related_to(user))
|
2022-11-16 15:21:29 +01:00
|
|
|
).distinct()
|
2022-11-11 08:59:18 +01:00
|
|
|
|
2022-11-02 23:10:48 +01:00
|
|
|
|
2022-10-31 00:42:26 +01:00
|
|
|
DEFAULT_NOTES = """
|
|
|
|
#### Traits personnalisés
|
|
|
|
|
|
|
|
#### Objectifs
|
|
|
|
|
|
|
|
#### Langages
|
|
|
|
|
|
|
|
#### Historique
|
|
|
|
|
|
|
|
#### Handicaps
|
|
|
|
|
|
|
|
#### Alignement
|
|
|
|
|
|
|
|
#### Relations
|
|
|
|
|
|
|
|
#### Religion
|
|
|
|
""".lstrip()
|
|
|
|
|
|
|
|
|
2022-11-09 20:49:57 +01:00
|
|
|
@dataclass
|
|
|
|
class CharacterCapability:
|
|
|
|
capability: Capability
|
|
|
|
known: bool = False
|
|
|
|
|
|
|
|
|
2022-12-09 19:00:20 +01:00
|
|
|
def validate_image(fieldfile_obj, megabytes_limit: float):
|
|
|
|
filesize = fieldfile_obj.file.size
|
|
|
|
if filesize > megabytes_limit * 1024 * 1024:
|
|
|
|
raise ValidationError("La taille maximale est de %sMo" % str(megabytes_limit))
|
|
|
|
|
|
|
|
|
2022-10-29 00:32:18 +02:00
|
|
|
class Character(models.Model):
|
|
|
|
class Gender(models.TextChoices):
|
2022-10-30 11:09:46 +01:00
|
|
|
MALE = "M", "Mâle"
|
|
|
|
FEMALE = "F", "Femelle"
|
|
|
|
OTHER = "O", "Autre"
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-10-30 11:09:46 +01:00
|
|
|
name = models.CharField(max_length=100, verbose_name="nom")
|
2022-10-29 00:32:18 +02:00
|
|
|
player = models.ForeignKey(
|
2022-10-30 11:09:46 +01:00
|
|
|
"common.User",
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name="characters",
|
|
|
|
verbose_name="joueur",
|
2022-10-29 00:32:18 +02:00
|
|
|
)
|
2022-12-09 18:38:24 +01:00
|
|
|
profile_picture = models.ImageField(
|
|
|
|
verbose_name="image de profil",
|
|
|
|
upload_to="profile_pictures",
|
|
|
|
blank=True,
|
|
|
|
null=True,
|
2022-12-09 19:00:20 +01:00
|
|
|
validators=[partial(validate_image, megabytes_limit=2)],
|
2022-12-09 18:38:24 +01:00
|
|
|
)
|
2022-10-29 00:32:18 +02:00
|
|
|
|
|
|
|
race = models.ForeignKey(
|
|
|
|
"character.Race",
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
related_name="characters",
|
2022-10-30 11:09:46 +01:00
|
|
|
verbose_name="race",
|
2022-10-29 00:32:18 +02:00
|
|
|
)
|
|
|
|
profile = models.ForeignKey(
|
|
|
|
"character.Profile",
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
related_name="characters",
|
2022-10-30 11:09:46 +01:00
|
|
|
verbose_name="profil",
|
2022-10-29 00:32:18 +02:00
|
|
|
)
|
2022-10-31 00:42:26 +01:00
|
|
|
level = models.PositiveSmallIntegerField(verbose_name="niveau", default=1)
|
2022-10-29 00:32:18 +02:00
|
|
|
|
|
|
|
gender = models.CharField(
|
2022-10-30 11:09:46 +01:00
|
|
|
max_length=1, choices=Gender.choices, default=Gender.OTHER, verbose_name="genre"
|
2022-10-29 00:32:18 +02:00
|
|
|
)
|
2022-10-30 11:09:46 +01:00
|
|
|
age = models.PositiveSmallIntegerField(verbose_name="âge")
|
|
|
|
height = models.PositiveSmallIntegerField(verbose_name="taille")
|
|
|
|
weight = models.PositiveSmallIntegerField(verbose_name="poids")
|
|
|
|
|
|
|
|
value_strength = models.PositiveSmallIntegerField(verbose_name="valeur force")
|
|
|
|
value_dexterity = models.PositiveSmallIntegerField(verbose_name="valeur dextérité")
|
|
|
|
value_constitution = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name="valeur constitution"
|
|
|
|
)
|
|
|
|
value_intelligence = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name="valeur intelligence"
|
|
|
|
)
|
|
|
|
value_wisdom = models.PositiveSmallIntegerField(verbose_name="valeur sagesse")
|
|
|
|
value_charisma = models.PositiveSmallIntegerField(verbose_name="valeur charisme")
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-10-30 11:09:46 +01:00
|
|
|
health_max = models.PositiveSmallIntegerField(verbose_name="points de vie max")
|
|
|
|
health_remaining = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name="points de vie restants"
|
|
|
|
)
|
2022-10-29 00:32:18 +02:00
|
|
|
|
|
|
|
racial_capability = models.ForeignKey(
|
|
|
|
"character.RacialCapability",
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
related_name="characters",
|
2022-10-30 11:09:46 +01:00
|
|
|
verbose_name="capacité raciale",
|
2022-10-29 00:32:18 +02:00
|
|
|
)
|
|
|
|
|
2022-10-30 11:09:46 +01:00
|
|
|
weapons = models.ManyToManyField(
|
|
|
|
"character.Weapon", blank=True, verbose_name="armes"
|
|
|
|
)
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-10-31 00:42:26 +01:00
|
|
|
armor = models.PositiveSmallIntegerField(verbose_name="armure", default=0)
|
|
|
|
shield = models.PositiveSmallIntegerField(verbose_name="bouclier", default=0)
|
|
|
|
defense_misc = models.SmallIntegerField(verbose_name="divers défense", default=0)
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-11-02 13:03:20 +01:00
|
|
|
initiative_misc = models.SmallIntegerField(
|
|
|
|
verbose_name="divers initiative", default=0
|
|
|
|
)
|
|
|
|
|
2022-10-30 11:09:46 +01:00
|
|
|
capabilities = models.ManyToManyField(
|
|
|
|
"character.Capability", blank=True, verbose_name="capacités"
|
|
|
|
)
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-10-30 11:09:46 +01:00
|
|
|
equipment = models.TextField(blank=True, verbose_name="équipement")
|
|
|
|
luck_points_remaining = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name="points de chance restants"
|
|
|
|
)
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-10-30 17:47:42 +01:00
|
|
|
mana_remaining = models.PositiveSmallIntegerField(
|
|
|
|
default=0, verbose_name="mana restant"
|
2022-10-30 11:09:46 +01:00
|
|
|
)
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-10-30 16:51:17 +01:00
|
|
|
money_pp = models.PositiveSmallIntegerField(default=0, verbose_name="PP")
|
|
|
|
money_po = models.PositiveSmallIntegerField(default=0, verbose_name="PO")
|
|
|
|
money_pa = models.PositiveSmallIntegerField(default=0, verbose_name="PA")
|
|
|
|
money_pc = models.PositiveSmallIntegerField(default=0, verbose_name="PC")
|
|
|
|
|
2022-10-30 21:55:05 +01:00
|
|
|
recovery_points_remaining = models.PositiveSmallIntegerField(
|
|
|
|
default=5, verbose_name="points de récupération restants"
|
|
|
|
)
|
|
|
|
|
2022-10-31 00:42:26 +01:00
|
|
|
notes = models.TextField(blank=True, verbose_name="notes", default=DEFAULT_NOTES)
|
2022-12-28 09:27:27 +01:00
|
|
|
gm_notes = models.TextField(blank=True, verbose_name="notes MJ")
|
2022-10-30 23:14:27 +01:00
|
|
|
damage_reduction = models.TextField(blank=True, verbose_name="réduction de dégâts")
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-11-02 21:41:06 +01:00
|
|
|
states = models.ManyToManyField(HarmfulState, blank=True, related_name="characters")
|
|
|
|
|
2022-11-09 18:19:08 +01:00
|
|
|
private = models.BooleanField(
|
|
|
|
"privé",
|
|
|
|
help_text="Empêche toute invitation dans un groupe.",
|
|
|
|
default=False,
|
|
|
|
blank=True,
|
|
|
|
)
|
|
|
|
|
2022-11-02 23:10:48 +01:00
|
|
|
objects = CharacterManager.from_queryset(CharacterQuerySet)()
|
2022-10-29 00:32:18 +02:00
|
|
|
|
|
|
|
class Meta:
|
2022-10-30 11:09:46 +01:00
|
|
|
verbose_name = "Personnage"
|
|
|
|
verbose_name_plural = "Personnages"
|
2022-10-29 00:32:18 +02:00
|
|
|
constraints = [
|
|
|
|
models.UniqueConstraint(
|
|
|
|
Lower("name"), "player", name="unique_character_player"
|
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
def natural_key(self):
|
|
|
|
return (self.name, self.player_id)
|
|
|
|
|
2022-10-30 10:12:49 +01:00
|
|
|
def get_absolute_url(self):
|
|
|
|
return reverse("character:view", kwargs={"pk": self.pk})
|
|
|
|
|
2022-10-29 00:32:18 +02:00
|
|
|
@property
|
|
|
|
def modifier_strength(self) -> int:
|
|
|
|
return modifier(self.value_strength)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def modifier_dexterity(self) -> int:
|
|
|
|
return modifier(self.value_dexterity)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def modifier_constitution(self) -> int:
|
|
|
|
return modifier(self.value_constitution)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def modifier_intelligence(self) -> int:
|
|
|
|
return modifier(self.value_intelligence)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def modifier_wisdom(self) -> int:
|
|
|
|
return modifier(self.value_wisdom)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def modifier_charisma(self) -> int:
|
|
|
|
return modifier(self.value_charisma)
|
|
|
|
|
|
|
|
@property
|
2022-11-02 13:03:20 +01:00
|
|
|
def modifier_initiative(self) -> int:
|
|
|
|
return self.modifier_dexterity + self.initiative_misc
|
2022-10-29 00:32:18 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def attack_melee(self) -> int:
|
|
|
|
return self.level + self.modifier_strength
|
|
|
|
|
|
|
|
@property
|
|
|
|
def attack_range(self) -> int:
|
|
|
|
return self.level + self.modifier_dexterity
|
|
|
|
|
|
|
|
@property
|
|
|
|
def attack_magic(self) -> int:
|
2022-10-30 22:18:42 +01:00
|
|
|
return self.level + self.modifier_magic
|
|
|
|
|
|
|
|
@property
|
|
|
|
def modifier_magic(self) -> int:
|
2022-10-29 00:32:18 +02:00
|
|
|
modifier_map = {
|
|
|
|
Profile.MagicalStrength.INTELLIGENCE: self.modifier_intelligence,
|
|
|
|
Profile.MagicalStrength.WISDOM: self.modifier_wisdom,
|
|
|
|
Profile.MagicalStrength.CHARISMA: self.modifier_charisma,
|
2022-10-30 09:30:54 +01:00
|
|
|
Profile.MagicalStrength.NONE: 0,
|
2022-10-29 00:32:18 +02:00
|
|
|
}
|
2022-10-31 00:20:55 +01:00
|
|
|
return modifier_map.get(
|
|
|
|
Profile.MagicalStrength(self.profile.magical_strength), 0
|
|
|
|
)
|
2022-10-29 00:32:18 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def defense(self) -> int:
|
|
|
|
return (
|
|
|
|
10 + self.armor + self.shield + self.modifier_dexterity + self.defense_misc
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mana_max(self) -> int:
|
2022-10-30 22:18:42 +01:00
|
|
|
mana_max_compute = self.profile.mana_max_compute
|
|
|
|
if mana_max_compute == Profile.ManaMax.NO_MANA:
|
|
|
|
return 0
|
2023-01-29 10:38:41 +01:00
|
|
|
if mana_max_compute == Profile.ManaMax.LEVEL:
|
2022-10-31 09:25:38 +01:00
|
|
|
return self.level + self.modifier_magic
|
2023-01-29 10:38:41 +01:00
|
|
|
return 2 * self.level + self.modifier_magic
|
2022-10-29 00:32:18 +02:00
|
|
|
|
2022-10-30 10:54:28 +01:00
|
|
|
@property
|
|
|
|
def height_m(self) -> float:
|
|
|
|
return round(self.height / 100, 2)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def imc(self) -> float:
|
|
|
|
return self.weight / (self.height_m**2)
|
2022-10-30 16:33:28 +01:00
|
|
|
|
2022-10-30 21:55:05 +01:00
|
|
|
@property
|
|
|
|
def recovery_points_max(self) -> int:
|
|
|
|
return 5
|
|
|
|
|
2022-10-30 22:43:50 +01:00
|
|
|
@property
|
|
|
|
def luck_points_max(self) -> int:
|
2022-11-10 11:40:51 +01:00
|
|
|
return max([3 + self.modifier_charisma, 0])
|
2022-10-30 22:43:50 +01:00
|
|
|
|
2022-11-06 16:09:41 +01:00
|
|
|
@property
|
|
|
|
def health_remaining_percent(self) -> float:
|
|
|
|
if self.health_max == 0:
|
|
|
|
return 0
|
|
|
|
return self.health_remaining / self.health_max * 100
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mana_remaining_percent(self) -> float:
|
|
|
|
if self.mana_max == 0:
|
|
|
|
return 0
|
|
|
|
return self.mana_remaining / self.mana_max * 100
|
|
|
|
|
2022-11-10 22:44:53 +01:00
|
|
|
@property
|
|
|
|
def capability_points_max(self) -> int:
|
|
|
|
return 2 * self.level
|
|
|
|
|
|
|
|
@property
|
|
|
|
def capability_points_used(self) -> int:
|
|
|
|
return sum(cap.capability_points_cost for cap in self.capabilities.only("rank"))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def capability_points_remaining(self) -> int:
|
|
|
|
return self.capability_points_max - self.capability_points_used
|
|
|
|
|
2022-10-31 00:20:55 +01:00
|
|
|
def get_modifier_for_weapon(self, weapon: Weapon) -> int:
|
|
|
|
modifier_map = {
|
|
|
|
Weapon.Category.RANGE: self.modifier_dexterity,
|
|
|
|
Weapon.Category.MELEE: self.modifier_strength,
|
|
|
|
Weapon.Category.NONE: 0,
|
|
|
|
}
|
2022-11-02 12:57:03 +01:00
|
|
|
return modifier_map.get(Weapon.Category(weapon.category), 0) + self.level
|
2022-10-31 00:20:55 +01:00
|
|
|
|
2022-11-09 20:49:57 +01:00
|
|
|
def get_capabilities_by_path(self) -> dict[Path, list[CharacterCapability]]:
|
2022-10-30 16:33:28 +01:00
|
|
|
capabilities_by_path = collections.defaultdict(list)
|
2022-11-09 20:49:57 +01:00
|
|
|
character_capabilities = self.capabilities.all()
|
|
|
|
character_paths = {capability.path for capability in character_capabilities}
|
|
|
|
for path in character_paths:
|
|
|
|
for capability in path.capabilities.all():
|
|
|
|
capabilities_by_path[capability.path].append(
|
|
|
|
CharacterCapability(
|
|
|
|
capability, known=capability in character_capabilities
|
|
|
|
)
|
|
|
|
)
|
2022-10-30 16:33:28 +01:00
|
|
|
|
|
|
|
return dict(
|
|
|
|
sorted(
|
|
|
|
(
|
2022-11-09 20:49:57 +01:00
|
|
|
(path, sorted(capabilities, key=lambda x: x.capability.rank))
|
2022-10-30 16:33:28 +01:00
|
|
|
for path, capabilities in capabilities_by_path.items()
|
|
|
|
),
|
|
|
|
key=lambda x: x[0].name,
|
|
|
|
)
|
|
|
|
)
|
2022-10-31 00:42:26 +01:00
|
|
|
|
|
|
|
def get_formatted_notes(self) -> str:
|
|
|
|
md = markdown.Markdown(extensions=["extra", "nl2br"])
|
|
|
|
return md.convert(self.notes)
|
2022-11-02 22:02:48 +01:00
|
|
|
|
2022-12-28 09:27:27 +01:00
|
|
|
def get_formatted_gm_notes(self) -> str:
|
|
|
|
md = markdown.Markdown(extensions=["extra", "nl2br"])
|
|
|
|
return md.convert(self.gm_notes)
|
|
|
|
|
2022-11-02 22:02:48 +01:00
|
|
|
def get_missing_states(self) -> Iterable[HarmfulState]:
|
|
|
|
return HarmfulState.objects.exclude(
|
|
|
|
pk__in=self.states.all().values_list("pk", flat=True)
|
|
|
|
)
|
2022-11-06 15:06:29 +01:00
|
|
|
|
|
|
|
def managed_by(self, user):
|
|
|
|
return self in Character.objects.managed_by(user)
|
2022-11-19 10:00:48 +01:00
|
|
|
|
2022-12-28 09:27:27 +01:00
|
|
|
def mastered_by(self, user):
|
|
|
|
return self in Character.objects.mastered_by(user)
|
|
|
|
|
2022-11-19 10:00:48 +01:00
|
|
|
def reset_stats(self):
|
|
|
|
self.health_remaining = self.health_max
|
|
|
|
self.mana_remaining = self.mana_max
|
|
|
|
self.luck_points_remaining = self.luck_points_max
|
|
|
|
self.recovery_points_remaining = self.recovery_points_max
|
|
|
|
self.save()
|