class Reaction(base.TableBase): """Table de données des réactions d'IA connues du bot. Les instances sont enregistrées via :meth:`\!addIA <.IA.GestionIA.GestionIA.addIA.callback>` et supprimées via :meth:`\!modifIA <.IA.GestionIA.GestionIA.modifIA.callback>`. """ id = autodoc_Column(sqlalchemy.Integer(), primary_key=True, doc="Identifiant unique de la réaction, sans signification") reponse = autodoc_Column(sqlalchemy.String(2000), nullable=False, doc="Réponse, suivant le format (mini-langage) personnalisé " "(``\"txt <||> txt <&&> <##>react\"``)") # One-to-manys triggers = autodoc_OneToMany("Trigger", back_populates="reaction", doc="Déclencheurs associés") def __repr__(self): """Return repr(self).""" extract = self.reponse.replace('\n', ' ')[:15] + "..." return f"<Reaction #{self.id} ({extract})>"
class Joueur(base.TableBase): """Table de données des joueurs inscrits. Les instances de cette classe correspondent aux lignes du Tableau de bord ; elles sont crées par l'inscription (:func:`.inscription.main`) et synchronisées par :meth:`\!sync <.sync.Sync.Sync.sync.callback>`. """ discord_id = autodoc_Column(sqlalchemy.BigInteger(), primary_key=True, autoincrement=False, doc="ID Discord du joueur") chan_id_ = autodoc_Column(sqlalchemy.BigInteger(), nullable=False, doc="ID du chan privé Discord du joueur. *(le ``_`` final indique " "que ce champ n'est pas synchnisé avec le Tableau de bord)*") nom = autodoc_Column(sqlalchemy.String(32), nullable=False, doc="Nom du joueur (demandé à l'inscription)") chambre = autodoc_Column(sqlalchemy.String(200), doc="Emplacement du joueur (demandé à l'inscription)") statut = autodoc_Column(sqlalchemy.Enum(Statut), nullable=False, default=Statut.vivant, doc="Statut RP") _role_slug = sqlalchemy.Column(sqlalchemy.ForeignKey("roles.slug"), nullable=False, default=lambda: config.default_role_slug) role = autodoc_ManyToOne("Role", back_populates="joueurs", doc="Rôle du joueur (défaut :meth:`.bdd.Role.default`)") _camp_slug = sqlalchemy.Column(sqlalchemy.ForeignKey("camps.slug"), nullable=False, default=lambda: config.default_camp_slug) camp = autodoc_ManyToOne("Camp", back_populates="joueurs", doc="Camp du joueur (défaut :meth:`.bdd.Camp.default`)") votant_village = autodoc_Column(sqlalchemy.Boolean(), nullable=False, default=True, doc="Le joueur participe aux votes du village ?") votant_loups = autodoc_Column(sqlalchemy.Boolean(), nullable=False, default=False, doc="Le joueur participe au vote des loups ?") role_actif = autodoc_Column(sqlalchemy.Boolean(), nullable=False, default=True, doc="Le joueur peut agir ? (pas chatgarouté...)") # One-to-manys actions = autodoc_OneToMany("Action", back_populates="joueur", doc="Actions pour ce joueur") candidharos = autodoc_OneToMany("CandidHaro", back_populates="joueur", doc="Candidatures et haros de/contre ce joueur") bouderies = autodoc_OneToMany("Bouderie", back_populates="joueur", doc="Appartenances aux boudoirs de ce joueur") ciblages = autodoc_DynamicOneToMany("Ciblage", back_populates="joueur", doc="Ciblages prenant ce joueur pour cible") def __repr__(self): """Return repr(self).""" return f"<Joueur #{self.discord_id} ({self.nom})>" def __str__(self): """Return str(self).""" return str(self.nom) @property def member(self): """discord.Member: Membre Discord correspondant à ce Joueur. Raises: ValueError: pas de membre correspondant ~ready_check.NotReadyError: bot non connecté (:obj:`.config.guild` vaut ``None``) """ result = config.guild.get_member(self.discord_id) if not result: raise ValueError(f"Joueur.member : pas de membre pour `{self}` !") return result @property def private_chan(self): """discord.TextChannel: Channel privé (conversation bot) du joueur. Raises: ValueError: pas de channel correspondant ~ready_check.NotReadyError: bot non connecté (:obj:`.config.guild` vaut ``None``) """ result = config.guild.get_channel(self.chan_id_) if not result: raise ValueError("Joueur.private_chan : " f"pas de chan pour `{self}` !") return result @property def actions_actives(self): """Sequence[.bdd.Action]: Sous-ensemble de :attr:`actions` restreint aux actions actives. Élimine aussi les actions de vote (sans base). """ return [ac for ac in self.actions if ac.active and not ac.vote] @property def boudoirs(self): """Sequence[.Boudoir]: Boudoirs où est ce jour (read-only)""" return [boud.boudoir for boud in self.bouderies] @hybrid_property def est_vivant(self): """:class:`bool` (instance) / :class:`sqlalchemy.sql.selectable.Exists` (classe): Le joueur est en vie ou MVtisé ? Raccourci pour ``joueur.statut in {Statut.vivant, Statut.MV}`` Propriété hybride (voir :attr:`.Action.is_open` pour plus d'infos) """ return (self.statut in {Statut.vivant, Statut.MV}) @est_vivant.expression def est_vivant(cls): return cls.statut.in_({Statut.vivant, Statut.MV}) @hybrid_property def est_mort(self): """:class:`bool` (instance) / :class:`sqlalchemy.sql.selectable.Exists` (classe): Le joueur est mort ? Raccourci pour ``joueur.statut == Statut.mort`` Propriété hybride (voir :attr:`.Action.is_open` pour plus d'infos) """ return (self.statut == Statut.mort) @classmethod def from_member(cls, member): """Récupère le Joueur (instance de BDD) lié à un membre Discord. Args: member (discord.Member): le membre concerné Returns: Joueur: Le joueur correspondant. Raises: ValueError: membre introuvable en base ~ready_check.NotReadyError: session non initialisée (:obj:`.config.session` vaut ``None``) """ joueur = cls.query.get(member.id) if not joueur: raise ValueError("Joueur.from_member : " f"pas de joueur en base pour `{member}` !") return joueur def action_vote(self, vote): """Retourne l'"action de vote" voulue pour ce joueur. Args: vote (.bdd.Vote): vote pour lequel récupérer l'action Returns: :class:`~bdd.Action` Raises: RuntimeError: action non existante """ if not isinstance(vote, Vote): vote = Vote[vote] try: return next(act for act in self.actions if act.vote == vote) except StopIteration: raise RuntimeError(f"{self} : pas d'action de vote {vote.name} ! " "!cparti a bien été appelé ?") from None
class Boudoir(base.TableBase): """Table de données des boudoirs sur le serveur. Les instances de cette classe sont crées, modifiées et supprimées par :meth:`\!boudoir <.chans.GestionChans.GestionChans.boudoir.callback>`. """ chan_id = autodoc_Column(sqlalchemy.BigInteger(), primary_key=True, autoincrement=False, doc="ID Discord du salon") nom = autodoc_Column(sqlalchemy.String(32), nullable=False, doc="Nom du boudoir (demandé à la création)") ts_created = autodoc_Column(sqlalchemy.DateTime(), nullable=False, doc="Timestamp de la création") # One-to-manys bouderies = autodoc_OneToMany("Bouderie", back_populates="boudoir", doc="Appartenances à ce boudoir") def __repr__(self): """Return repr(self).""" return f"<Boudoir #{self.chan_id} ({self.nom})>" @property def chan(self): """discord.TextChannel: Salon Discord correspondant à ce boudoir. Raises: ValueError: pas de membre correspondant ~ready_check.NotReadyError: bot non connecté (:obj:`.config.guild` vaut ``None``) """ result = config.guild.get_channel(self.chan_id) if not result: raise ValueError(f"Boudoir.chan : pas de chan pour {self} !") return result @property def joueurs(self): """Sequence[.Joueur]: Joueurs présents dans ce boudoir (read-only)""" return [boud.joueur for boud in self.bouderies] @property def gerant(self): """.bdd.Joueur: Membre du boudoir ayant les droits de gestion. Raises: ValueError: pas de membre avec les droits de gestion """ try: return next(boud.joueur for boud in self.bouderies if boud.gerant) except StopIteration: raise ValueError(f"Pas de membre gérant le boudoir *{self.name}*") async def add_joueur(self, joueur, gerant=False): """Ajoute un joueur sur le boudoir. Crée la :class:`.Bouderie` correspondante et modifie les permissions du salon. Args: joueur (.Joueur): Le joueur à ajouter. gerant (bool): Si le joueur doit être ajouté avec les permissions de gérant. """ now = datetime.datetime.now() Bouderie(boudoir=self, joueur=joueur, gerant=gerant, ts_added=now, ts_promu=now if gerant else None).add() await self.chan.set_permissions(joueur.member, read_messages=True) async def remove_joueur(self, joueur): """Retire un joueur du boudoir. Supprime la :class:`.Bouderie` correspondante et modifie les permissions du salon. Args: joueur (.Joueur): Le joueur à ajouter. """ Bouderie.query.filter_by(boudoir=self, joueur=joueur).one().delete() await self.chan.set_permissions(joueur.member, overwrite=None) @classmethod def from_channel(cls, channel): """Récupère le Boudoir (instance de BDD) lié à un salon Discord. Args: channel (discord.TextChannel): le salon concerné Returns: .Boudoir: Le salon correspondant. Raises: ValueError: boudoir introuvable en base ~ready_check.NotReadyError: session non initialisée (:obj:`.config.session` vaut ``None``) """ boudoir = cls.query.get(channel.id) if not boudoir: raise ValueError("Boudoir.from_channel : " f"pas de boudoir en base pour `{channel.mention}` !") return boudoir
class Action(base.TableBase): """Table de données des actions attribuées (liées à un joueur). Les instances doivent être enregistrées via :func:`.gestion_actions.add_action` et supprimées via :func:`.gestion_actions.delete_action`. """ id = autodoc_Column( sqlalchemy.Integer(), primary_key=True, doc="Identifiant unique de l'action, sans signification") _joueur_id = sqlalchemy.Column(sqlalchemy.ForeignKey("joueurs.discord_id"), nullable=False) joueur = autodoc_ManyToOne("Joueur", back_populates="actions", doc="Joueur concerné") _base_slug = sqlalchemy.Column(sqlalchemy.ForeignKey("baseactions.slug")) base = autodoc_ManyToOne("BaseAction", back_populates="actions", nullable=True, doc="Action de base (``None`` si action de vote)") vote = autodoc_Column(sqlalchemy.Enum(Vote), doc="Si action de vote, vote concerné") active = autodoc_Column( sqlalchemy.Boolean(), nullable=False, default=True, doc="Si l'action est actuellement utilisable (False = archives)") cooldown = autodoc_Column( sqlalchemy.Integer(), nullable=False, default=0, doc="Nombre d'ouvertures avant disponiblité de l'action") charges = autodoc_Column( sqlalchemy.Integer(), doc="Nombre de charges restantes (``None`` si illimité)") # One-to-manys taches = autodoc_OneToMany("Tache", back_populates="action", doc="Tâches liées à cette action") utilisations = autodoc_DynamicOneToMany("Utilisation", back_populates="action", doc="Utilisations de cette action") def __init__(self, *args, **kwargs): """Initialize self.""" n_args = (("base" in kwargs) + ("_base_slug" in kwargs) + ("vote" in kwargs)) if not n_args: raise ValueError("bdd.Action: 'base'/'_base_slug' or 'vote' " "keyword-only argument must be specified") elif n_args > 1: raise ValueError("bdd.Action: 'base'/'_base_slug' and 'vote' " "keyword-only argument cannot both be specified") super().__init__(*args, **kwargs) def __repr__(self): """Return repr(self).""" return f"<Action #{self.id} ({self.base or self.vote}/{self.joueur})>" @property def utilisation_ouverte(self): """:class:`~bdd.Utilisation` | ``None``: Utilisation de l'action actuellement ouverte. Vaut ``None`` si aucune action n'a actuellement l'état :attr:`~bdd.UtilEtat.ouverte` ou :attr:`~bdd.UtilEtat.remplie`. Raises: RuntimeError: plus d'une action a actuellement l'état :attr:`~bdd.UtilEtat.ouverte` ou :attr:`~bdd.UtilEtat.remplie`. """ filtre = Utilisation.etat.in_({UtilEtat.ouverte, UtilEtat.remplie}) try: return self.utilisations.filter(filtre).one_or_none() except sqlalchemy.orm.exc.MultipleResultsFound: raise ValueError( f"Plusieurs utilisations ouvertes pour `{self}` !") @property def derniere_utilisation(self): """:class:`~bdd.Utilisation` | ``None``:: Dernière utilisation de cette action (temporellement). Considère l'utilisation ouverte le cas échéant, sinon la dernière utilisation par timestamp de fermeture descendant (quelque soit son état, y comprs :attr:`~.bdd.UtilEtat.contree`). Vaut ``None`` si l'action n'a jamais été utilisée. Raises: RuntimeError: plus d'une action a actuellement l'état :attr:`~bdd.UtilEtat.ouverte` ou :attr:`~bdd.UtilEtat.remplie`. """ return (self.utilisation_ouverte or self.utilisations.order_by( Utilisation.ts_close.desc()).first()) @property def decision(self): """str: Description de la décision de la dernière utilisation. Considère l'utilisation ouverte le cas échéant, sinon la dernière utilisation par timestamp de fermeture descendant. Vaut :attr:`.Utilisation.decision`, ou ``"<N/A>"`` si il n'y a aucune utilisation de cette action. Raises: RuntimeError: plus d'une action a actuellement l'état :attr:`~bdd.UtilEtat.ouverte` ou :attr:`~bdd.UtilEtat.remplie`. """ util = self.derniere_utilisation if util: return util.decision else: return "<N/A>" @hybrid_property def is_open(self): """:class:`bool` (instance) / :class:`sqlalchemy.sql.selectable.Exists` (classe): L'action est ouverte (l'utilisateur peut interagir) ? *I.e.* l'action a au moins une utilisation :attr:`~.bdd.UtilEtat.ouverte` ou :attr:`~.bdd.UtilEtat.remplie`. Propriété hybride (:class:`sqlalchemy.ext.hybrid.hybrid_property`) : - Sur l'instance, renvoie directement la valeur booléenne ; - Sur la classe, renvoie la clause permettant de déterminer si l'action est en attente. Examples:: action.is_open # bool Joueur.query.filter(Joueur.actions.any(Action.is_open)).all() """ return bool(self.utilisations.filter(Utilisation.is_open).all()) @is_open.expression def is_open(cls): return cls.utilisations.any(Utilisation.is_open) @hybrid_property def is_waiting(self): """:class:`bool` (instance) / :class:`sqlalchemy.sql.selectable.Exists` (classe): L'action est ouverte et aucune décision n'a été prise ? *I.e.* la clause a au moins une utilisation :attr:`~.bdd.UtilEtat.ouverte`. Propriété hybride (voir :attr:`.is_open` pour plus d'infos) """ return bool(self.utilisations.filter(Utilisation.is_waiting).all()) @is_waiting.expression def is_waiting(cls): return cls.utilisations.any(Utilisation.is_waiting)
class Utilisation(base.TableBase): """Table de données des utilisations des actions. Les instances sont enregistrées via :meth:`\!open <.open_close.OpenClose.OpenClose.open.callback>` ; elles n'ont pas vocation à être supprimées. """ id = autodoc_Column( sqlalchemy.BigInteger(), primary_key=True, doc="Identifiant unique de l'utilisation, sans signification") _action_id = sqlalchemy.Column(sqlalchemy.ForeignKey("actions.id"), nullable=False) action = autodoc_ManyToOne("Action", back_populates="utilisations", doc="Action utilisée") etat = autodoc_Column(sqlalchemy.Enum(UtilEtat), nullable=False, default=UtilEtat.ouverte, doc="État de l'utilisation") ts_open = autodoc_Column(sqlalchemy.DateTime(), doc="Timestamp d'ouverture de l'utilisation") ts_close = autodoc_Column(sqlalchemy.DateTime(), doc="Timestamp de fermeture de l'utilisation") ts_decision = autodoc_Column( sqlalchemy.DateTime(), doc="Timestamp du dernier remplissage de l'utilisation") # One-to-manys ciblages = autodoc_OneToMany("Ciblage", back_populates="utilisation", doc="Cibles désignées dans cette utilisation") def __repr__(self): """Return repr(self).""" return f"<Utilisation #{self.id} ({self.action}/{self.etat})>" def open(self): """Ouvre cette utilisation. Modifie son :attr:`etat`, définit :attr:`ts_open` au temps actuel, et update. """ self.etat = UtilEtat.ouverte self.ts_open = datetime.datetime.now() self.update() def close(self): """Clôture cette utilisation. Modifie son :attr:`etat`, définit :attr:`ts_close` au temps actuel, et update. """ if self.etat == UtilEtat.remplie: self.etat = UtilEtat.validee else: self.etat = UtilEtat.ignoree self.ts_close = datetime.datetime.now() self.update() def ciblage(self, slug): """Renvoie le ciblage de base de slug voulu. Args: slug (str): Doit correspondre à un des slugs des bases des :attr:`ciblages` de l'utilisation. Returns: :class:`.bdd.Ciblage` Raises: ValueError: slug non trouvé dans les :attr:`ciblages` """ try: return next(cib for cib in self.ciblages if cib.base.slug == slug) except StopIteration: raise ValueError( f"{self} : pas de ciblage de slug '{slug}'") from None @property def cible(self): """:class:`~bdd.Joueur` | ``None``: Joueur ciblé par l'utilisation, si applicable. Cet attribut n'est accessible que si l'utilisation est d'un vote ou d'une action définissant un et une seul ciblage de type :attr:`~bdd.CibleType.joueur`, :attr:`~bdd.CibleType.vivant` ou :attr:`~bdd.CibleType.mort`. Vaut ``None`` si l'utilisation a l'état :attr:`~bdd.UtilEtat.ouverte` ou :attr:`~bdd.UtilEtat.ignoree`. Raises: ValueError: l'action ne remplit pas les critères évoqués ci-dessus """ if self.action.vote: # vote : un BaseCiblage implicite de type CibleType.vivants return self.ciblages[0].joueur if self.ciblages else None else: base_ciblages = self.action.base.base_ciblages bc_joueurs = [ bc for bc in base_ciblages if bc.type in [CibleType.joueur, CibleType.vivant, CibleType.mort] ] if len(bc_joueurs) != 1: raise ValueError(f"L'utilisation {self} n'a pas une et " "une seule cible de type joueur") base_ciblage = bc_joueurs[0] try: ciblage = next(cib for cib in self.ciblages if cib.base == base_ciblage) except StopIteration: return None # Pas de ciblage fait return ciblage.joueur @property def decision(self): """str: Description de la décision de cette utilisation. Complète le template de :.bdd.BaseAction.decision_format` avec les valeurs des ciblages de l'utilisation. Vaut ``"Ne rien faire"`` si l'utilisation n'a pas de ciblages, et :attr:`.cible` dans le cas d'un vote. """ if not self.action.base: return str(self.cible) if not self.ciblages: return "Ne rien faire" template = self.action.base.decision_format data = { ciblage.base.slug: ciblage.valeur_descr for ciblage in self.ciblages } try: return template.format(**data) except KeyError: return template @hybrid_property def is_open(self): """:class:`bool` (instance) / :class:`sqlalchemy.sql.selectable.Exists` (classe): L'utilisation est ouverte (l'utilisateur peut interagir) ? Raccourci pour ``utilisation.etat in {UtilEtat.ouverte, UtilEtat.remplie}`` Propriété hybride (voir :attr:`.Action.is_open` pour plus d'infos) """ return (self.etat in {UtilEtat.ouverte, UtilEtat.remplie}) @is_open.expression def is_open(cls): return cls.etat.in_({UtilEtat.ouverte, UtilEtat.remplie}) @hybrid_property def is_waiting(self): """:class:`bool` (instance) / :class:`sqlalchemy.sql.selectable.Exists` (classe): L'utilisation est ouverte et aucune décision n'a été prise ? Raccourci pour ``utilisation.etat == UtilEtat.ouverte`` Propriété hybride (voir :attr:`.Action.is_open` pour plus d'infos) """ return (self.etat == UtilEtat.ouverte) @hybrid_property def is_filled(self): """:class:`bool` (instance) / :class:`sqlalchemy.sql.selectable.Exists` (classe): L'utilisation est remplie (l'utilisateur a interagi avec) ? Raccourci pour ``utilisation.etat in {UtilEtat.remplie, UtilEtat.validee, UtilEtat.contree}`` Propriété hybride (voir :attr:`.Action.is_open` pour plus d'infos) """ return (self.etat in {UtilEtat.remplie, UtilEtat.validee, UtilEtat.contree}) @is_filled.expression def is_filled(cls): return cls.etat.in_( {UtilEtat.remplie, UtilEtat.validee, UtilEtat.contree})
class Role(base.TableBase): """Table de données des rôles. Cette table est remplie automatiquement à partir du Google Sheet "Rôles et actions" par la commande :meth:`\!fillroles <.sync.Sync.Sync.fillroles.callback>`. """ slug = autodoc_Column(sqlalchemy.String(32), primary_key=True, doc="Identifiant unique du rôle") prefixe = autodoc_Column(sqlalchemy.String(8), nullable=False, default="", doc="Article du nom du rôle (``\"Le \"``, ``\"La \"``, ``\"L'\"``...)") nom = autodoc_Column(sqlalchemy.String(32), nullable=False, doc="Nom (avec casse et accents) du rôle") _camp_slug = sqlalchemy.Column(sqlalchemy.ForeignKey("camps.slug"), nullable=False) camp = autodoc_ManyToOne("Camp", back_populates="roles", doc="Camp auquel ce rôle est affilié à l'origine\n\n (On peut avoir " "``joueur.camp != joueur.role.camp`` si damnation, passage MV...)") actif = autodoc_Column(sqlalchemy.Boolean(), nullable=False, default=True, doc="Rôle actif ? (affiché dans la liste des rôles, etc)") description_courte = autodoc_Column(sqlalchemy.String(140), nullable=False, default="", doc="Description du rôle en une ligne") description_longue = autodoc_Column(sqlalchemy.String(1800), nullable=False, default="", doc="Règles et background complets du rôle") # to-manys joueurs = autodoc_OneToMany("Joueur", back_populates="role", doc="Joueurs ayant ce rôle") ciblages = autodoc_DynamicOneToMany("Ciblage", back_populates="role", doc="Ciblages prenant ce rôle pour cible") base_actions = autodoc_ManyToMany("BaseAction", secondary=_baseaction_role, back_populates="roles", doc="Modèles d'actions associées") def __repr__(self): """Return repr(self).""" return f"<Role '{self.slug}' ({self.prefixe}{self.nom})>" def __str__(self): """Return str(self).""" return self.nom_complet @property def nom_complet(self): """str: Préfixe + nom du rôle""" return f"{self.prefixe}{self.nom}" @property def embed(self): """discord.Embed: Embed Discord présentant le rôle et ses actions.""" emb = discord.Embed( title=f"**{self.nom_complet}** – {self.description_courte}", description=self.description_longue ) if (emoji := self.camp.discord_emoji_or_none): emb.set_thumbnail(url=emoji.url) for ba in self.base_actions: emb.add_field(name=f"{config.Emoji.action} Action : {ba.slug}", value=ba.temporalite) return emb
class BaseAction(base.TableBase): """Table de données des actions définies de base (non liées à un joueur). Cette table est remplie automatiquement à partir du Google Sheet "Rôles et actions" par la commande :meth:`\!fillroles <.remplissage_bdd.RemplissageBDD.RemplissageBDD.fillroles.callback>`. """ slug = autodoc_Column(sqlalchemy.String(32), primary_key=True, doc="Identifiant unique de l'action") trigger_debut = autodoc_Column(sqlalchemy.Enum(ActionTrigger), nullable=False, default=ActionTrigger.perma, doc="Mode de déclenchement de l'ouverture de l'action") trigger_fin = autodoc_Column(sqlalchemy.Enum(ActionTrigger), nullable=False, default=ActionTrigger.perma, doc="Mode de déclenchement de la clôture de l'action") instant = autodoc_Column(sqlalchemy.Boolean(), nullable=False, default=False, doc="L'action est instantannée (conséquence dès la prise de décision)" " ou non (conséquence à la fin du créneau d'action)") heure_debut = autodoc_Column(sqlalchemy.Time(), doc="Si :attr:`.trigger_debut` vaut " ":attr:`~ActionTrigger.temporel`, l'horaire associé") heure_fin = autodoc_Column(sqlalchemy.Time(), doc="Si :attr:`.trigger_fin` vaut\n" "- :attr:`~ActionTrigger.temporel` : l'horaire associé ;\n" "- :attr:`~ActionTrigger.delta`, l'intervalle associé") base_cooldown = autodoc_Column(sqlalchemy.Integer(), nullable=False, default=0, doc="Temps de rechargement entre deux utilisations du pouvoir " "(``0`` si pas de cooldown)") base_charges = autodoc_Column(sqlalchemy.Integer(), doc="Nombre de charges initiales du pouvoir (``None`` si illimité)") refill = autodoc_Column(sqlalchemy.String(32), nullable=False, default="", doc="Évènements pouvant recharger l'action, séparés par des virgules " "(``\"weekends\"``, ``\"forgeron\"``, ``\"rebouteux\"``...)") lieu = autodoc_Column(sqlalchemy.String(100), doc="*Attribut informatif, non exploité dans la version actuelle " "(Distance/Physique/Lieu/Contact/Conditionnel/None/Public)*") interaction_notaire = autodoc_Column(sqlalchemy.String(100), doc="*Attribut informatif, non exploité dans la version actuelle " "(Oui/Non/Conditionnel/Potion/Rapport ; None si récursif)*") interaction_gardien = autodoc_Column(sqlalchemy.String(100), doc="*Attribut informatif, non exploité dans la version actuelle " "(Oui/Non/Conditionnel/Taverne/Feu/MaisonClose/Précis/" "Cimetière/Loups ; None si récursif)*") mage = autodoc_Column(sqlalchemy.String(100), doc="*Attribut informatif, non exploité dans la version actuelle " "(Oui/Non/Changement de cible/...)*") decision_format = autodoc_Column(sqlalchemy.String(200), nullable=False, default="", doc="Description des utilisations de ces action, sous forme de " "texte formaté avec les noms des :attr:`.BaseCiblage.slug` " "entre accolades (exemple : ``Tuer {cible}``)") # -to-manys actions = autodoc_OneToMany("Action", back_populates="base", doc="Actions déroulant de cette base") base_ciblages = autodoc_OneToMany("BaseCiblage", back_populates="base_action", cascade="all, delete-orphan", order_by="BaseCiblage.prio", doc="Ciblages de ce modèle d'action (triés par priorité)") roles = autodoc_ManyToMany("Role", secondary=_baseaction_role, back_populates="base_actions", doc="Rôles ayant cette action de base") def __repr__(self): """Return repr(self).""" return f"<BaseAction '{self.slug}'>" def __str__(self): """Return str(self).""" return str(self.slug) @property def temporalite(self): """str: Phrase décrivant le mode d'utilisation / timing de l'action.""" def _time_to_heure(tps): if not tps: return "" if tps.hour == 0: return f"{tps.minute} min" if tps.minute > 0: return f"{tps.hour}h{tps.minute:02}" return f"{tps.hour}h" rep = "" # Périodicté if self.trigger_debut == ActionTrigger.perma: rep += "N'importe quand" elif self.trigger_debut == ActionTrigger.start: rep += "Au lancement de la partie" elif self.trigger_debut == ActionTrigger.mort: rep += "À la mort" else: if self.base_cooldown: rep += f"Tous les {self.base_cooldown + 1} jours " else: rep += "Tous les jours " # Fenêtre if self.trigger_debut == ActionTrigger.mot_mjs: rep += "à l'annonce des résultats du vote" elif self.trigger_debut == ActionTrigger.open_cond: rep += "pendant le vote condamné" elif self.trigger_debut == ActionTrigger.open_maire: rep += "pendant le vote pour le maire" elif self.trigger_debut == ActionTrigger.open_loups: rep += "pendant le vote des loups" elif self.trigger_debut == ActionTrigger.close_cond: rep += "à la fermeture du vote condamné" elif self.trigger_debut == ActionTrigger.close_maire: rep += "à la fermeture du vote pour le maire" elif self.trigger_debut == ActionTrigger.close_loups: rep += "à la fermeture du vote des loups" elif self.trigger_debut == ActionTrigger.temporel: if self.trigger_fin == ActionTrigger.temporel: rep += f"de {_time_to_heure(self.heure_debut)}" else: rep += f"à {_time_to_heure(self.heure_debut)}" # Fermeture if self.trigger_fin == ActionTrigger.delta: rep += f" – {_time_to_heure(self.heure_fin)} pour agir" elif self.trigger_fin == ActionTrigger.temporel: rep += f" à {_time_to_heure(self.heure_fin)}" # Autres caractères if self.instant: rep += f" (conséquence instantanée)" if self.base_charges: rep += f" – {self.base_charges} fois" if "weekends" in self.refill: rep += f" par semaine" return rep
class Camp(base.TableBase): """Table de données des camps, publics et secrets. Cette table est remplie automatiquement à partir du Google Sheet "Rôles et actions" par la commande :meth:`\!fillroles <.sync.Sync.Sync.fillroles.callback>`. """ slug = autodoc_Column(sqlalchemy.String(32), primary_key=True, doc="Identifiant unique du camp") nom = autodoc_Column(sqlalchemy.String(32), nullable=False, doc="Nom (affiché) du camp") description = autodoc_Column(sqlalchemy.String(1000), nullable=False, default="", doc="Description (courte) du camp") public = autodoc_Column(sqlalchemy.Boolean(), nullable=False, default=True, doc="L'existance du camp (et des rôles liés) est connue de tous ?") emoji = autodoc_Column(sqlalchemy.String(32), doc="Nom de l'emoji associé au camp (doit être le nom d'un " "emoji existant sur le serveur)") # One-to-manys joueurs = autodoc_OneToMany("Joueur", back_populates="camp", doc="Joueurs appartenant à ce camp") roles = autodoc_OneToMany("Role", back_populates="camp", doc="Rôles affiliés à ce camp de base") ciblages = autodoc_DynamicOneToMany("Ciblage", back_populates="camp", doc="Ciblages prenant ce camp pour cible") def __repr__(self): """Return repr(self).""" return f"<Camp '{self.slug}' ({self.nom})>" def __str__(self): """Return str(self).""" return str(self.nom) @property def discord_emoji(self): """discord.Emoji: Emoji Discord correspondant à ce camp Raises: ValueError: :attr:`.emoji` non défini ou manquant sur le serveur ~ready_check.NotReadyError: bot non connecté (:obj:`.config.guild` vaut ``None``) """ if not self.emoji: raise ValueError(f"{self}.emoji non défini !") try: return next(e for e in config.guild.emojis if e.name == self.emoji) except StopIteration: raise ValueError(f"Pas d'emoji :{self.emoji}: " "sur le serveur !") from None @property def discord_emoji_or_none(self): """:class:`discord.Emoji` | ``None``: :attr:`.discord_emoji` si défini Raises: ~ready_check.NotReadyError: bot non connecté (:obj:`.config.guild` vaut ``None``) """ try: return self.discord_emoji except ValueError: return None @classmethod def default(cls): """Retourne le camp par défaut (celui avant attribution). Warning: Un camp de :attr:`.slug` :obj:`.config.default_camp_slug` doit être défini en base. Returns: ~bdd.Camp Raises: ValueError: camp introuvable en base RuntimeError: session non initialisée (:obj:`.config.session` vaut ``None``) """ slug = config.default_camp_slug camp = cls.query.get(slug) if not camp: raise ValueError( "Camp par défaut (de slug " f"lgrez.config.default_camp_slug = \"{slug}\") non " "défini (dans le GSheet Rôles et actions) ou non " f"chargé (`!fillroles`) !" ) return camp