31 Commits
Author SHA1 Message Date
uberwald c7e6660d9d fix(combat): re-render tracker après suppression + initiative par Réaction + lisibilité + malus PNJ
Release Creation / build (release) Failing after 1m41s
- fight.mjs : _onDelete appelle super._onDelete() → le tracker se vide immédiatement après « Terminer » (plus de F5)
- fight.mjs : _getInitiativeFormula type-dépendante — PNJ/Créature roulent leur pool de Réaction (computed.reaction + reactionBonus) au lieu de 0D → classement par Réaction correct
- dialogs.less : total des dés (.total-value) en blanc au lieu du vert sombre (illisible sur fond foncé)
- npc-roll-dialog : champ « Malus temporaires » + logique pool (dés malus dans le dialogue PNJ/Créature)
2026-08-07 21:04:24 +02:00
uberwald 9f88a66b72 feat(combat): refonte UI — tracker v14, handicap III, malus d'attaque, checkbox
Release Creation / build (release) Failing after 1m28s
- combat-tracker.hbs + fight.mjs : alignés sur le CSS core v14 (header/tracker/footer, boutons <button>), attitudes/couleurs PJ-PNJ conservées, menu contextuel combatant réparé
- less/combat.less (nouveau) : styles custom tracker + footer sticky des dialogues
- handicap des 3 dialogues de jet étendu jusqu'à III
- dialogue d'attaque : champ « Malus temporaires » (malus sur les jets)
- checkbox/radio : flex: 0 0 auto global (fin des icônes étirées — ex. « Deuxième action » 228px → 24px)
- TEST_COMBAT.md : plan de test combat rejouable (S1-S8, tout PASS)
2026-08-07 18:31:04 +02:00
uberwald 61b7815381 chore: régénérer les compendiums YAML depuis les packs (mise à jour en jeu)
- materiel : +25 docs (boucliers, munitions, objets) + dossiers Électronique/Munitions
- tables : 17 → 54 docs
- adaptationsmutations : champ system.mutation ajouté, ownership nettoyé
- supprime le doublon typo 'Anti_inflflammatoire' (→ Anti_inflammatoire)
2026-08-07 18:30:57 +02:00
uberwald 44fbc592ad fix: Origine des PNJ en texte libre (remplace la dropdown) 2026-08-07 15:32:09 +02:00
uberwald ef01651b1d feat(traits): tâche 0 + phases 2-4 — automatisation complète des Traits
Tâche 0 :
- item.mjs : champ equipped sur items génériques + toggle fiche
- nouveau module/system/traits.mjs (helpers partagés : equippedItems, lourdMalus, mobilityHandicap, feticheItem…)

Phase 2 — Combat :
- Lourd : malus cumulés auto (Vigueur < n) dans le dialogue + pool
- Rapide : handicap (n) pré-rempli dans la défense (character + PNJ) + note carte
- Rafale : case tir en rafale (+2 difficulté), +n réussites si réussi (bonusOnSuccess)
- Portée : portée effective n × Vigueur affichée
- Maniable/Mobilité : note de rappel (dialogue + carte)
- Incapacitant : bouton « Jet de Santé (n) » sur la carte d'échange
- getLabel() : libellés caractéristiques/compétences localisés

Phase 3 — Jets d'action :
- Ponctuel : annule jusqu'à n Handicaps, consommé au jet (Traits supprimé à 0)
- Pratique : outil sélectionné +2D au lieu de +1D
- Intimidant : case « Brandit » → -1D Psychologie ou Relance 1D
- Malus : champ « Malus temporaires » manuel déduit du pool
- Durée : note « n tours » sur la carte de jet d'arme
- roll() : transmission effectiveRange/mobilityHandicap/ponctuelUsed/intimidant (corrige l'affichage Portée/Mobilité)

Phase 4 — Acteur :
- Fétiche : +1D Effort ET Sang-Froid (max dérivé, config corrigé), alerte 2+ fétiches, action « Perdre le fétiche » (-2D, max -1D persistant feticheLoss)
- Zone : note + bouton « Appliquer aux cibles de la zone » (#onApplyZone)

CSS : styles LESS pour les nouvelles classes (fetiche-block, exchange-reminder/zone, traits-reminder, tool-bonus/ponctuel, duree-note)
i18n : 29 clés fr/en
TRAITS_PLAN.md : plan complet cochée
2026-08-07 15:30:41 +02:00
uberwald 8dd5d02994 feat(traits): phase 1 — réparations (Bien conçu, Pratique, Coque réparable)
- Coque : épuisée stockée {value:0, max:n} au lieu de null → réparable ; nouveau mode « Réparer la coque » (difficulté = coque.max) dans le dialogue ; affichage (0/n) sur la fiche
- Bien conçu : Réussite automatique en réparation classique (restauration complète) ; +1 Fiabilité à la 1ère réparation de fortune réussie (compteur traits.bien_conçu.value)
- Pratique : Handicap +I sur toutes les réparations (required +1, note sur la carte)
- i18n fr/en (9 clés)
- TRAITS_PLAN.md : suivi du plan d'automatisation des Traits
2026-08-07 14:40:42 +02:00
uberwald ef86b2637c chore: régénérer les compendiums depuis le monde
Ajoute les 242 nouveaux items du compendium Historiques
(capacités) au format YAML (src/packs).
2026-08-07 13:53:43 +02:00
uberwald 2f78d1a1ea fix(reliability): corrections suite au review
- min: 0 sur reliability (l'update à 0 était clippé à 1 par le schéma —
  l'état 'détruit' était inatteignable)
- réparation de fortune (règles p. 39) : +1D si le personnage possède la
  Compétence requise, sinon Handicap de Rareté de cette Compétence
  (rarity du skill de l'acteur) ; select toujours visible (compétence requise)
  et pré-rempli depuis needSkill/skill de l'item
- dés marqués 'dommage' uniquement pour les Dommages réellement comptés
  (Étanche/Coque absorbe → dés non marqués)
- destruction : damages.value réel conservé (difficulté de réparation
  honnête au lieu de 5 forcé)
- états de panne localisés (VERMINE.damages_state_N), aussi dans panneState()
- ReliabilityDialog invoqué via .create() (convention du codebase)
2026-08-07 13:48:13 +02:00
uberwald 7ec3d73915 fix: clé i18n manquante VERMINE.close (boutons affichaient 'VERMINE.CLOSE')
La clé 'close' du fichier lang était en fait la compétence 'Corps-à-corps'
(SKILLS.close) : les dialogues totem et Fiabilité affichaient la clé brute
'VERMINE.CLOSE'. Ajout de VERMINE.close = 'Fermer'/'Close', utilisé par les
deux dialogues.
2026-08-07 09:13:14 +02:00
uberwald 88e2902068 fix: checkbox/radio du dialogue Fiabilité étirés (flexrow) — flex:none sur les inputs
La classe .flexrow du label étirait le checkbox Immersion prolongée (et les
radios de gravité) sur toute la largeur du dialogue (~205px). .reliability-option
impose désormais flex:none aux inputs : taille naturelle (24px).
2026-08-07 09:12:08 +02:00
uberwald 70a97796a3 feat: gestion de la Fiabilité du matériel (test de Fiabilité + réparations)
- module/system/reliability.mjs : VermineReliability
  - Test de Fiabilité : jet Xd10 (Mineurs 1D / Graves 3D / Critiques 5D),
    chaque dé >= Fiabilité inflige 1 Dommage ; réduit Fiabilité et dommages
    cumulés (panne/état/effet) ; destruction à 0
  - Traits appliqués : Coque (absorbe n puis se perd), Cassant (+1 perte),
    Étanche (Dommages d'immersion ignorés si Fiabilité au max)
  - Réparations : classique (Précision + Artisanat/Mécanique/Technologie/
    Armurerie vs Fiabilité d'origine + Handicap de panne, +1 Fiabilité/Réussite)
    et de fortune (Précision + Bricolage, panne réparée sans gain de Fiabilité)
- ReliabilityDialog (test/réparation) + cartes de chat dédiées
- Boutons Test de Fiabilité / Réparer sur les fiches d'items (weapon/defense/item)
- i18n fr/en + styles des cartes et boutons
2026-08-07 08:57:35 +02:00
uberwald d499f60386 fix: dégâts d'arme négatifs autorisés (ex. Pierre Vigueur -1)
Release Creation / build (release) Failing after 1m30s
damage.value était plafonné à 0 dans le DataModel des armes,
empêchant la saisie de valeurs négatives (règles : Pierre = Vigueur -1).
min passe de 0 à -10.
2026-08-06 23:25:03 +02:00
uberwald 1c7e05bae6 fix: Fiabilité des objets modifiable au-delà de 5 (max 5 → 10)
Comme la Rareté, la Fiabilité était plafonnée à 5 dans le DataModel
(baseItemSchema), empêchant de saisir des valeurs supérieures sur les
fiches d'items. Alignée sur les véhicules (max 10).
2026-08-06 23:10:50 +02:00
uberwald 06603ceff7 feat: affichage des Traits des armes dans le message de jet en tchat
- templates/roll-message.hbs : ligne Traits dans le bloc arme (nom + valeur)
  avec description en infobulle au survol
- less/base.less : style .weapon-traits / .weapon-trait (badges lisibles)
2026-08-06 23:08:54 +02:00
uberwald ca74ff4a88 fix: lisibilité des textes - blancs/gris illisibles passés en sombre
Release Creation / build (release) Failing after 1m23s
- Textes blancs sur fond clair (fiches, dialogues, cartes de chat) → couleurs sombres
  - dialogs.less : conteneur roll-dialog-content + labels, options, boutons, valeurs
  - actor.less : inputs/select/textarea des fiches + badges Adaptation/Mutation
  - community.less : info taille, hint, valeur de jauge, niveau de moral
  - vehicle.less : unités ; npc.less : bouton de jet ; items.less : select
- Textes gris des dialogues core Foundry (FilePicker, compendiums, tables, journaux)
  → noirs : suppression des overrides gris theme-dark, texte sombre sur .window-content,
  sidebar sombre conservée en texte clair, messages de tchat en sombre
- CSS : moins/actor/totem.less (ligne influence + bouton en sombre)
2026-08-06 22:26:36 +02:00
uberwald b0a3aff648 feat: gestion des Dés de Totems (influence, instincts, interdits)
Release Creation / build (release) Failing after 1m32s
- VermineTotemDice : influence Humain/Adapté (+1D/-1D par domaine, règles
  p. 121), gains/pertes via Instincts/Interdits avec limites (3D/totem,
  5D total) et règles d'échange.
- roll.mjs applique l'influence selon le Totem dominant (au lieu de
  l'ancienne logique basée sur identity.totem).
- Dialogue TotemDiceDialog (instinct Humain/autre totem, interdit Humain,
  acte grave ±2D) + bouton et ligne d'influence sur la fiche personnage.
- Dés de Totem gagnés en cas d'échec à l'apprentissage Dé d'Évolution
  (dés dépensés, garde capacité vide).
- Couleur de texte @color-text-light-2 assombrie (#c9e0c0 → #3f9b55)
  pour la lisibilité dans tous les dialogues.

fix: robustesse et visibilité des évolutions

- buyEvolutionDialog : n'efface les Dés d'Évolution que si l'item est créé.
- Flag mutation éditable sur la fiche item evolution + badge
  Adaptation/Mutation dans la liste du personnage.
- loseDie : applique aussi la limite totale de 5 dés au gain Adapté.
- Case "acte grave" conservée entre les rendus du dialogue.
2026-08-06 15:32:55 +02:00
uberwald 3cb96410b9 fix: réserve de Groupe calculée selon les membres (3D + 1D/personnage)
- _calculateGroupReserve utilisait une règle simplifiée (niveau × 2) au
  lieu du nombre de personnages membres. Désormais max = 3 + membres
  (personnages uniquement), plafonné à 10D, conforme aux règles p. 68-69.
- Un Groupe nouvellement créé démarre avec sa Réserve à son maximum.
2026-08-06 09:37:52 +02:00
uberwald ad4daf0b00 fix: exception si le dialogue d'apprentissage est fermé pendant le jet
Release Creation / build (release) Failing after 1m38s
- #getSpecialtyName accédait à this.#el sans garde : fermer le dialogue
  pendant l'animation du jet levait "Uncaught (in promise)". Chaînage
  optionnel sur l'élément (le jet se résout quand même et dépense le dé).
2026-08-06 08:14:06 +02:00
uberwald c96e8d69d7 fix: retours review - validation de phase, drop de véhicule et dés de Réserve
- resetPhaseLimits ne touche plus au compteur de phase (seul le
  dialogue d'attribution du MJ l'avance) : évite le double-incrément
  quand joueur et MJ valident la même phase.
- _onDropActor : import d'une copie monde pour un véhicule déposé
  depuis un compendium, confirmation avant de transférer un véhicule
  déjà rattaché à un autre Groupe, try/catch avec retour utilisateur.
- #onClickRadioHexa : garde isEditable (pas d'erreur de permission
  pour un utilisateur non éditeur).
2026-08-05 21:24:33 +02:00
uberwald 282fb2794a feat: validation de phase d'Expérience (réinitialisation des limites)
- Nouveau bouton « Valider la phase » sur les fiches Personnage et
  Groupe : incrémente experience.phase et réinitialise les limites
  d'apprentissage (learned.*) pour recommencer une nouvelle phase,
  sans repasser par le dialogue d'attribution du MJ.
- Helper VermineExperience.completePhase(actor) mutualisé
  (personnage ou Groupe) ; i18n FR/EN.
2026-08-05 20:58:46 +02:00
uberwald 46b9512256 fix: sous-titres de la fiche de Groupe illisibles (jaune sur fond beige)
- Les h5 (Objectifs majeurs/mineurs) passent du jaune @color-amber à
  @color-chitin-dark, lisible sur le fond box_background.webp.
2026-08-05 18:56:04 +02:00
uberwald 8316112741 fix: toggle Chaman de l'onglet Totem étiré sur toute la largeur
- Style compact pour .chaman-toggle (width fit-content, centré, label
  DistressBlack, checkbox de taille fixe) dans less/actor/totem.less.
2026-08-05 18:52:41 +02:00
uberwald 75f39551f8 feat: fiche de Groupe - drag&drop de véhicules et dés de Réserve cliquables
- Dépôt d'un acteur Véhicule sur la fiche de Groupe : le véhicule est
  lié via system.ownerId (pas de duplication), apparaît dans la liste
  Véhicules de l'onglet Matériel et s'ouvre au clic (_onDropActor).
- Onglet Réserve : dés de Réserve hexagonaux cliquables (1..max),
  même comportement que les réserves du personnage (cocher/décocher).
- ESLint : fromUuid ajouté aux globals (corrige aussi 3 erreurs
  préexistantes dans base-actor-sheet.mjs).
2026-08-05 18:05:13 +02:00
uberwald ef76ae1116 feat: fiche de Groupe - ordre des sections Matériel
- Onglet Matériel : les Objets passent sous les Armes et les
  Protections (ordre aligné sur la fiche PJ : Armes, Protections,
  Objets, Véhicules, Notes).
2026-08-05 17:49:22 +02:00
uberwald 18180a8445 fix: apprentissage de Groupe - dés dépensés en cas d'échec et gardes
Release Creation / build (release) Failing after 1m40s
- Les Dés d'Expérience du Groupe sont désormais dépensés que le jet
  d'apprentissage soit réussi ou raté (cohérent avec l'apprentissage
  de personnage et les règles ; le décompte est sorti de #applySuccess).
- Garde sur le type Capacité : si aucune Capacité de Groupe n'est
  sélectionnée, le jet est bloqué avec un avertissement (avant, il
  marquait learned.ability et dépensait les dés sans rien apprendre).
- Limites de Capacités de Totem (personnage) : seules les Capacités du
  Totem courant du personnage comptent dans les compteurs
  3×N1 / 1×N2-N3 / max 5.
2026-08-05 14:15:11 +02:00
uberwald 3235017a4c fix: fiche Communauté - doublons de champs (Origine, Effectif)
- Origine était rendue à la fois dans l'en-tête (main) et l'onglet
  Identité : deux inputs du même nom, soumis en tableau puis concaténés
  avec une virgule à chaque sortie de champ.
- Effectif (population) était aussi dupliqué entre l'en-tête et
  l'onglet Domaines.
- Garde l'Origine dans l'en-tête, l'Effectif dans l'onglet Domaines
  (regroupé avec la Taille) ; plus aucun champ texte/nombre dupliqué.
2026-08-05 13:58:37 +02:00
uberwald b2a044640c feat: Instincts et Interdits du Totem sur la fiche de Groupe
- Onglet Info du Groupe : affichage des Instincts/Interdits de base du
  Totem (TOTEMS.*.instincts / bans), comme sur la fiche de personnage.
2026-08-05 13:54:00 +02:00
uberwald a13b0fc2d6 fix: affichage des Instincts et Interdits du Totem sur la fiche PJ
- Helper smarttl : honore la clé passée en 3e argument (défaut "name") ;
  il renvoyait systématiquement le nom du Totem, donc Instincts et
  Interdits n'apparaissaient pas (description et autres clés aussi).
- Onglet Totem (AppV2 et legacy) : libellés localisés
  IDENTITY.instincts / IDENTITY.prohibits au lieu de textes codés en dur.
2026-08-05 13:06:57 +02:00
uberwald 0ad240bd03 feat: section Rites chamaniques sur la fiche de personnage
- Nouveau booléen system.identity.chaman (statut de Chaman).
- Onglet Totem : case à cocher « Chaman » toujours visible ; quand
  elle est cochée, la section « Rites chamaniques » apparaît avec la
  liste des items de type rite maîtrisés (création, édition,
  suppression en mode édition).
- i18n FR/EN (VERMINE.chaman).
2026-08-05 12:05:31 +02:00
uberwald 9d36b92fa9 feat: nouveaux apprentissages (Capacités de Totem, Historique, Réputation) et expérience de Groupe
- Dialogue d'apprentissage personnage : 4 nouveaux types d'achat.
  - Capacité de Totem : options filtrées (mode de jeu, niveau de
    groupe, totem, non-apprises), difficulté = valeur d'Apprentissage,
    limites 3×N1 / 1×N2-N3 / max 5, marque system.learned de l'item
    (convention type character) et learned.ability du personnage.
  - Élément d'Historique : difficulté 9, handicap 1 si astérisque
    (background.asterisk), marque learned.background + item.
  - Test de Réputation (réduire/augmenter) : réduire = difficulté
    10 − score visé (−1 pt), augmenter = difficulté = réputation
    actuelle (+1 par succès), bornes 0-10, marque learned.reputation.
  - Évolution : option Dé de Totem Adapté ×2 quand Adapté > Humain
    (1D remplacé par un dé adapté compté ×2 via VermineUtils.roll).
- Correction règles : les Dés d'Expérience sont dépensés même en cas
  d'échec du jet d'apprentissage (décompte hors #applySuccess).
- Expérience de Groupe : nouveau dialogue d'apprentissage de Groupe
  (niveau, réserve, capacité de totem), réserve d'expérience et phase
  dans system.experience, boutons sur la fiche Groupe, reset des
  limites par phase (level/reserve/ability).
- Phase d'expérience : filtre par groupe sélectionné, associe chaque
  personnage à son premier groupe, reset des limites de tous les
  nouveaux types d'apprentissage.
- Corrections : l'attaque de créature n'ajoute plus la Réaction du Rôle
  (regles_creatures.txt), suppression de la case XP des en-têtes PJ
  (legacy et AppV2).
- Divers : .eslintrc.js (règles inexistantes/obsolètes retirées),
  libellé jaune de dialogue en couleur chitine, i18n FR/EN, chips
  « appris » sur l'onglet expérience du personnage.
2026-08-05 11:56:59 +02:00
uberwald d0373fab5a feat: capacités de créature, blessures séquentielles et refonte fiche Groupe
- Nouvel item Capacité de créature (creaturecapacity) : DataModel, fiche
  AppV2, chat card, types Base/Survie/Cauchemar/Apocalypse, liste sur
  l'onglet Informations de la créature avec drag&drop et post au chat.
- Blessures : coches séquentielles respectant la gravité (onClickWound),
  malus de blessure -1D/-2D/-3D appliqué aux jets (Règles p.42),
  montée en gravité automatique si les cercles sont pleins.
- Fiche Groupe : objectifs majeurs/mineurs éditables, liste de membres
  simplifiée, notes de voyage (roadNotes), totem qui remplit
  instinct/interdits, refonte visuelle des onglets.
- Créature : Réaction lançable, seuils/cercles de blessures corrigés
  (Taille cumulée, Groupe sans seuil), libellés Taille/Meute.
- Corrections : icône de carte de chat plafonnée à 64px, slash orphelin
  d'entrave à 0, notes de matériel du Groupe éditable (formInput),
  bonus de Réaction non double-compté.
- Divers : suppression des captures de débogage, licence README.
2026-08-05 09:38:36 +02:00
839 changed files with 20566 additions and 8688 deletions
+5 -8
View File
@@ -34,7 +34,8 @@ module.exports = {
Dialog: 'readonly', Dialog: 'readonly',
foundry: 'readonly', foundry: 'readonly',
Handlebars: 'readonly', Handlebars: 'readonly',
renderTemplate: 'readonly' renderTemplate: 'readonly',
fromUuid: 'readonly'
}, },
rules: { rules: {
// Possible Problems // Possible Problems
@@ -86,7 +87,6 @@ module.exports = {
'default-case': 'warn', 'default-case': 'warn',
'default-case-last': 'warn', 'default-case-last': 'warn',
'default-param-last': 'warn', 'default-param-last': 'warn',
'dot-locale-compare': 'warn',
'dot-notation': ['warn', { allowKeywords: true }], 'dot-notation': ['warn', { allowKeywords: true }],
'eqeqeq': ['error', 'always', { null: 'ignore' }], 'eqeqeq': ['error', 'always', { null: 'ignore' }],
'func-name-matching': 'warn', 'func-name-matching': 'warn',
@@ -123,7 +123,6 @@ module.exports = {
'no-delete-var': 'error', 'no-delete-var': 'error',
'no-div-regex': 'warn', 'no-div-regex': 'warn',
'no-else-return': ['warn', { allowElseIf: true }], 'no-else-return': ['warn', { allowElseIf: true }],
'no-empty-destructuring': 'warn',
'no-empty-function': ['warn', { allow: ['constructors'] }], 'no-empty-function': ['warn', { allow: ['constructors'] }],
'no-empty-pattern': 'warn', 'no-empty-pattern': 'warn',
'no-eq-null': 'off', 'no-eq-null': 'off',
@@ -153,7 +152,6 @@ module.exports = {
'no-new-func': 'warn', 'no-new-func': 'warn',
'no-new-wrappers': 'error', 'no-new-wrappers': 'error',
'no-nonoctal-decimal-escape': 'error', 'no-nonoctal-decimal-escape': 'error',
'no-object-multi-space': 'warn',
'no-octal-escape': 'error', 'no-octal-escape': 'error',
'no-param-reassign': ['warn', { props: false }], 'no-param-reassign': ['warn', { props: false }],
'no-plusplus': 'off', 'no-plusplus': 'off',
@@ -267,10 +265,9 @@ module.exports = {
VariableDeclarator: { var: 2, let: 2, const: 3 }, VariableDeclarator: { var: 2, let: 2, const: 3 },
outerIIFEBody: 1, outerIIFEBody: 1,
MemberExpression: 'off', MemberExpression: 'off',
FunctionDeclaration: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } }, FunctionDeclaration: { body: 1, parameters: 1 },
FunctionExpression: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } }, FunctionExpression: { body: 1, parameters: 1 },
StaticBlock: { body: 1 }, StaticBlock: { body: 1 }
ClassBody: 1
}], }],
'jsx-quotes': 'off', 'jsx-quotes': 'off',
'key-spacing': ['warn', { beforeColon: false, afterColon: true, mode: 'strict' }], 'key-spacing': ['warn', { beforeColon: false, afterColon: true, mode: 'strict' }],
+7 -1
View File
@@ -133,4 +133,10 @@ make build|watch|lint # Raccourcis Makefile
## Licence ## Licence
Projet fan non-officiel. L'univers et les marques Vermine 2047 appartiennent aux Éditions John Doe. VERMINE 2047, Un jeu de Julien Blondel
© 2023 Studio Agate
TOTEM System
© 2023 Julien Blondel
Tous droits réservés
+145
View File
@@ -0,0 +1,145 @@
# Plan de test — Combat Vermine2047 (rejouable)
Base : règles de combat (Kit de Survie p.44-52, `regles/2047_KitDeSurvie-DEF.txt`) et moteur `VermineExchange`/`CombatDialog`/`VermineCombatTracker`.
Ce plan décrit le **setup reproductible**, les **scénarios**, les **valeurs attendues** (déterministes : pools, difficultés, formules) et la **procédure** de validation. Les jets de dés restent aléatoires ; on valide la mécanique, pas un résultat chiffré précis.
## Prérequis
- Monde Vermine2047 sur `https://localhost:31000`, connecté en **Gamemaster**.
- Scène « AA » (`49tnX0ynTzztVkxy`) avec tokens PJ / PNJ (+ token Créature ajouté).
- Dés en main : chaque jet via l'UI crée une carte de chat (animations 3D possibles).
## Acteurs (setup reproductible)
| Acteur | Type | ID | Réglages |
|--------|------|----|----------|
| **Personnage** | PJ (character) | `f6qo5ubEnrSrtcfM` | Vigueur 3, Précision 2, Réflexes 2 · Mêlée 3, Corps-à-corps 2, Armes à feu 3 · Equipé : TEST Epée (lame 3), TEST Fusil (balle 4, mun. 10, 5/50 m), TEST Gilet (Protection 2, Mobilité 1) |
| **PNJ** | PNJ (npc) | `EsksoLF7SV6yx7ZM` | Menace 2 (Attaque 4), Expérience 3 (contact « 5,7 ou 9 », action 4), Rôle 3 (Protection 3) · Equipé : TEST Hache (lame 3), TEST Armure (Protection 3) |
| **Créature** | Créature | `4CCdIWBHSqgApmlR` | Attaque 7, Dommages 5, Vigueur 4, Protection 2, Statut combat Difficulté 7 · Equipée : TEST Morsure (choc 4 + Vigueur) |
### Pools de dés attendus (SkillLevels : lvl2→pool1/reroll1, lvl3→pool2/reroll1)
| Jet | Pool | Difficulté |
|-----|------|-----------|
| PJ contact (Épée, Mêlée 3) | 3 + 2 = **5D** | 5 / 7 / 9 |
| PJ distance (Fusil, AF 3) | 2 + 2 = **4D** | Portée : courte 5 / moyenne 7 / longue 9 |
| PJ esquive | 2 + 1 = **3D** | = difficulté attaque |
| PJ parade | 3 + 2 = **5D** | = difficulté attaque |
| PNJ attaque (Menace 2) | **4D** | 5 / 7 / 9 |
| PNJ défense | **4D** (action) | = difficulté attaque |
| Créature attaque (Morsure) | **7D** | = statut combat (7) |
| Créature défense | **passive** (pas de jet, difficulté verrouillée 7) | — |
### Seuils de blessure (netDamage)
| Acteur | Légère | Grave | Mortelle |
|--------|--------|-------|----------|
| PJ | ≥ 1 | ≥ 4 | ≥ 8 |
| PNJ | ≥ 2 | ≥ 5 | ≥ 8 |
| Créature | ≥ 2 | ≥ 4 | ≥ 6 |
### Formules de résolution (vérifiées par le moteur)
- Dégâts bruts = **Dommages de base + réussites de l'attaquant**.
- Dégâts nets = max(0, bruts Protection du défenseur). La Protection est la somme des **défenses équipées** (PNJ : TEST Armure 3 ; la « Protection » de rôle n'est PAS une défense équipée). Parade : seule la protection utilisée compte.
- Blessure : catégorie dont le seuil est ≤ dégâts nets (la plus haute).
- Défense : ≥ réussites attaquant → paré/esquivé (0 dégât) ; 1+ de plus → touché.
---
## Scénarios
### S1 — Création du combat et initiative
1. Créer un combat « TEST COMBAT » ; ajouter les combatants PJ + PNJ + Créature.
2. `rollAll()` : toutes les initiatives sont jetées (formule `Xd10cs>=7`).
- **Attendu** : combatantCount = 3 ; chaque combatant a une initiative non nulle ; `turn`/`round` cohérents après `startCombat`.
- **Validation** : ⬜
### S2 — Attaque PJ → PNJ au contact (Épée)
1. Cibler le token PNJ. Ouvrir l'attaque via l'icône « Attaque » de la fiche PJ (équipement → TEST Epée).
2. Vérifier le pool **5D** (Vigueur 3 + Mêlée) et les difficultés **5/7/9**.
3. Lancer (difficulté 7). Défense PNJ : **Esquive** (4D).
4. « Résoudre » : dégâts bruts = 3 + réussites PJ ; nets = bruts 3 (Protection PNJ).
- **Attendu** : pool affiché 5D, difficulté 7 ; résolution = formules ci-dessus ; si touché → blessure Légère (≥2) / Grave (≥5) / Mortelle (≥8).
- **Validation** : ⬜
### S3 — Attaque PJ → Créature à distance (Fusil)
1. Cibler le token Créature. Attaque via TEST Fusil.
2. Vérifier le pool **4D**, difficulté de Portée (choisir Moyenne = 7).
3. Lancer. La créature **ne peut pas défendre** (défense passive ; pas de bouton Esquive/Parade sur sa ligne).
- **Attendu** : pool 4D, difficulté 7 ; carte créature = « Défense passive » ; résolution = 4 + réussites 2 (Protection créature) ; seuils créature 2/4/6.
- **Validation** : ⬜
### S4 — Attaque PNJ → PJ (Hache)
1. Cibler le token PJ. Attaque via la Hache du PNJ.
2. Vérifier le pool **4D** (Menace 2) et les difficultés **5/7/9**.
3. Défense PJ : **Parade** (5D, Vigueur + Mêlée) — choisir Parade.
- **Attendu** : pool 4D ; parade 5D ; si touché → 3 + réussites 2 (TEST Gilet) ; seuils PJ 1/4/8.
- **Validation** : ⬜
### S5 — Attaque Créature → PJ (Morsure)
1. Cibler le token PJ. Attaque via TEST Morsure de la créature.
2. Vérifier le pool **7D**, difficulté **verrouillée 7** (statut combat).
3. Défense PJ : **Esquive** (3D).
- **Attendu** : pool 7D, difficulté 7 verrouillée ; dégâts de base = 4 + Vigueur 4 = **8** ; nets = 8 + réussites 2 ; seuils PJ 1/4/8.
- **Validation** : ⬜
### S6 — Traits de combat
- **Rafale (3)** : case « Tir en rafale » sur le Fusil → difficulté +2 (7→9) ; si jet réussi, +3 réussites affichées (« réussites automatiques ») ; si échec, pas de bonus.
- **Rapide (2)** : arme avec Rapide → note « le défenseur subit 2 Handicap(s) » ; la défense du PNJ s'ouvre avec handicap **II** pré-rempli.
- **Lourd (4)** : équiper un objet Lourd 4 au PJ (Vigueur 3 < 4) → malus **-1D** dans le pool (dialogue + ligne « Malus Lourd »).
- **Portée (8)** : arme Portée 8, Vigueur 4 → « Portée effective : 32 m » affichée.
- **Incapacitant (7)** : sur la carte d'échange, bouton « Jet de Santé (7) » → ouvre un jet de Santé vs 7.
- **Zone (6)** : note « Zone (6 m) » + bouton « Appliquer aux cibles de la zone ».
- **Validation** : ⬜
### S7 — Tracker (gestion)
1. Bouton **+** (createCombat) : crée un nouveau combat.
2. Clic droit sur un combatant → menu contextuel (Mettre à jour / Effacer / Relancer / **Supprimer**) ; le bouton poubelle par combatant supprime aussi.
3. Poubelle d'en-tête (deleteCombat) : supprime le combat.
4. Footer : **Commencer** (startCombat), tours (←/→), rounds (←/→), **Terminer** (endCombat).
- **Validation** : ⬜
### S8 — Nettoyage
- Supprimer les combats de test, les items « TEST * », restaurer les compétences PJ (0), retirer le token Créature.
---
## Rapport d'exécution
Chaque scénario : note **PASS** si les valeurs attendues sont vérifiées, **FAIL** sinon (avec l'écart). Les chiffres aléatoires (dés) sont relevés tels quels.
### S1 — Combat & initiative — **PASS**
3 combatants ajoutés ; initiatives jetées (PJ 1, PNJ 0, Créature 0) ; `startCombat` → round 1 ; ordre du tour = initiative décroissante (Personnage, PNJ, Créature).
### S2 — PJ→PNJ contact (Épée) — **PASS**
- Dialogue : pool **5D** (Vigueur 3 + Mêlée 3), difficultés **[5,7,9]**, dégâts de base 3.
- Attaque (difficulté 7) → 2 réussites. Défense PNJ **Esquive 4D** (action) → 0 réussite.
- Résolution : **Touché**, bruts 3+2=5, nets 5 (PNJ sans défense équipée → 0 protection) → **Blessure grave** (seuil 5).
- Application : `majorWound` 0→1, « Blessure appliquée ».
- Note : la « Protection 3 » du rôle PNJ n'est pas une défense équipée → non déduite.
### S3 — PJ→Créature distance (Fusil) — **PASS**
- Dialogue : pool **4D** (Précision 2 + AF 3), difficulté **verrouillée 7** (cible créature).
- Créature : pas de bouton défense → « Défense passive ».
- Résolution : attaque 3 réussites → **Touché**, bruts 4+3=7 protection 2 = **5****Blessure grave** (seuil créature 4).
### S4 — PNJ→PJ (Hache) — **PASS**
- Dialogue PNJ : pool **4D** (Menace 2), difficultés **[5,7,9]**.
- Défense PJ **Parade** : pool **5D** (Vigueur 3 + Mêlée 3), difficulté 7, compétence Mêlée.
- Résolution : attaque 2, défense 2 → **Paré, aucun dégât** (réussites défense ≥ attaque).
### S5 — Créature→PJ (Morsure) — **PASS**
- Dialogue : pool **7D** (attaque), difficulté **verrouillée 7** (statut combat), dégâts de base **8** (4 + Vigueur 4).
- Défense PJ **Esquive** : pool **3D** (Réflexes 2 + Corps-à-corps 2), difficulté 7.
- Résolution (défense 0) : **Touché**, bruts 8+4=12 protection 2 = **10****Blessure mortelle** (seuil 8).
- Bonus : le **malus de blessure** d'une créature blessée est bien appliqué (deadlyWound=1 → 3D, pool 7→4).
### S6 — Traits de combat — **PASS**
- **Rafale (3)** : case → difficulté +2 (7→9) ; jet raté → **aucun bonus** (total 0) ; jet réussi → **+3 réussites** (total = brut + 3, ligne « réussites automatiques »).
- **Rapide (2)** : note sur la carte ; dialogue de défense → handicap **II** pré-rempli.
- **Lourd (4)** : Vigueur 3 < 4 → malus **1D** (pool breakdown « Malus Lourd »).
- **Portée (8)** : portée effective = 8 × Vigueur 3 = **24 m**.
- **Incapacitant (7)** / **Zone (6)** : boutons « Jet de Santé » / « Appliquer aux cibles de la zone » présents sur la carte.
### S7 — Tracker — **PASS** (onglet combat actif requis)
Bouton **+** → nouveau combat ; suppression combatant (bouton poubelle + menu contextuel clic-droit) ; footer **Commencer** → round 1 ; boutons tours/rounds ; **Terminer** → round 0 ; poubelle d'en-tête → suppression du combat.
### S8 — Nettoyage — **PASS**
Combats de test supprimés, items « TEST * » retirés, compétences PJ restaurées, token Créature retiré.
+48
View File
@@ -0,0 +1,48 @@
# Plan d'automatisation des Traits — Suivi
Base : `regles/regles_traits.txt` (et KitDeSurvie p.70-71, 39).
Implémenté déjà : `cassant`, `coque` (absorption), `etanche` (immersion) dans `module/system/reliability.mjs`.
Décisions utilisateur : Fétiche +1D sur les **deux** Réserves · Malus/Durée = **saisie manuelle** · Handicap de Mobilité = **note de rappel** · Items génériques = **flag équipé OK**.
## Légende
- [x] fait
- [ ] à faire
- [~] en cours
## Tâche 0 — Prérequis communs
- [x] `item.mjs` : champ `equipped` sur items génériques + toggle fiche `item-item-sheet.hbs`
- [x] nouveau `module/system/traits.mjs` : helpers partagés (`equippedItems`, `lourdMalus`, `mobilityNote`, `feticheItem`…)
## Phase 1 — Réparations (reliability.mjs + dialogue)
- [x] **Bien conçu** : succès auto en réparation classique (restauration complète) + 1ʳᵉ réparation de fortune rend +1 Fiabilité (compteur `traits.bien_conçu.value`)
- [x] **Pratique** : Handicap +I aux réparations (required +1, note sur la carte)
- [x] **Coque réparable** : épuisement → `{ value: 0, max: n }` ; mode « Réparer la coque » dans le dialogue (jet vs `coque.max`) ; affichage `(0/n)` sur la fiche
- [x] **Étanche** : rien de plus (immersion gérée ; gaz/maladie hors scope)
## Phase 2 — Combat (combatDialog.mjs, exchange.mjs)
- [x] **Lourd** : malus auto (`lourdMalus()`) dans dialogue combat + pool breakdown (ligne « Malus Lourd »)
- [x] **Rapide (n)** : handicap (n) défenseur pré-rempli (locks.handicap) + note sur carte d'échange
- [x] **Rafale (n)** : case « Tir en rafale » → difficulté +2, +n réussites si réussi (`bonusOnSuccess`)
- [x] **Portée** : portée effective × Vigueur (n × vigor) affichée (dialogue + carte)
- [x] **Maniable / Mobilité** : note de rappel dialogue combat + carte (handicap de mobilité cumulé, Maniable l'annule)
- [x] **Incapacitant (n)** : bouton « Jet de Santé (n) » sur carte d'échange (RollDialog santé vs n)
- [x] `getLabel()` : libellés caractéristiques/compétences localisés dans le tchat (bonus du jet de Santé)
## Phase 3 — Jets d'action (rollDialog.mjs + roll-dialog.hbs)
- [x] **Ponctuel (n)** : outil sélectionné → annule jusqu'à n Handicaps ; consommé au jet (décrément, Trait supprimé à 0) ; note carte
- [x] **Pratique (jets)** : outil sélectionné → +2D au lieu de +1D (badge « +2D » dans le dialogue)
- [x] **Intimidant** : case « Brandit (X) » → 1D Psychologie si compétence psychologie, sinon Relance 1D ; note carte
- [x] **Malus (n)** : champ « Malus temporaires » (saisie manuelle) déduit du pool + ligne carte
- [x] **Durée (n)** : note « effet pendant n tours de combat » sur carte de jet d'arme
- [x] Fix `roll()` : transmission des paramètres étendus (`ponctuelUsed`, `intimidant`, `effectiveRange`, `mobilityHandicap`) au message — corrige aussi l'affichage Portée/Mobilité en Phase 2
## Phase 4 — Acteur (character.mjs + fiche)
- [x] **Fétiche** : config « ou »→« et » ; +1D Effort ET Sang-Froid (max dérivé) ; alerte 2+ fétiches (bandeau fiche) ; action « Perdre le fétiche » (2D valeurs, +1 perte max persistant `experience.feticheLoss`, retire le Trait)
- [x] **Zone (n)** : note carte d'échange + bouton « Appliquer aux cibles de la zone » (#onApplyZone : résout et applique la blessure à toutes les cibles)
## Transverse
- [x] i18n fr/en pour la Phase 1
- [x] ESLint (0 erreur sur les fichiers modifiés) + tests navigateur Phase 1 (coque, réparation coque, Bien conçu classique/fortune, Pratique, dialogue, affichage fiche)
- [x] ESLint (0 erreur sur les fichiers modifiés) + tests navigateur Phase 2 (Lourd, Rapide+défense, Rafale conditionnel, Portée, Mobilité, Incapacitant, toggle equip)
- [x] ESLint (0 erreur sur les fichiers modifiés) + tests navigateur Phase 3 (Pratique +2D, Ponctuel annulation+consommation, Intimidant psychologie/relance, Malus temporaires, Durée, transmission roll())
- [x] ESLint (0 erreur sur les fichiers modifiés) + tests navigateur Phase 4 (Fétiche helpers + UI, Zone note/bouton + #onApplyZone) — modèle réserves + champ feticheLoss effectifs au reload
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 855 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 KiB

-680
View File
@@ -1,680 +0,0 @@
uid=78_0 RootWebArea "Foundry Virtual Tabletop" url="https://localhost:31000/game"
uid=78_1 ignored
uid=78_2 generic
uid=78_3 list
uid=78_4 generic
uid=78_5 generic
uid=78_6 generic
uid=78_7 generic
uid=78_8 generic
uid=78_9 generic
uid=78_10 list
uid=78_11 listitem level="1"
uid=78_12 tab "Outils de token" selectable
uid=78_13 listitem level="1"
uid=78_14 tab "Outils de tuile" selectable
uid=78_15 listitem level="1"
uid=78_16 tab "Outils de dessin" selectable
uid=78_17 listitem level="1"
uid=78_18 tab "Outils de mur" selectable
uid=78_19 listitem level="1"
uid=78_20 tab "Outils de lumière" selectable
uid=78_21 listitem level="1"
uid=78_22 tab "Outils de son dambiance" selectable
uid=78_23 listitem level="1"
uid=78_24 tab "Outils de région" selectable
uid=78_25 listitem level="1"
uid=78_26 tab "Notes" selectable
uid=78_27 list
uid=78_28 listitem level="1"
uid=78_29 button "Sélection de tokens" pressed
uid=78_30 listitem level="1"
uid=78_31 button "Sélection de cibles"
uid=78_32 listitem level="1"
uid=78_33 button "Règle"
uid=78_34 listitem level="1"
uid=78_35 button "Déplacement sans contrainte"
uid=78_36 generic
uid=78_37 generic
uid=78_38 list
uid=78_39 listitem level="1"
uid=78_40 ignored
uid=78_41 StaticText "Gamemaster [MJ]"
uid=78_42 InlineTextBox "Gamemaster [MJ]"
uid=78_43 generic
uid=78_44 generic
uid=78_45 LabelText
uid=78_46 StaticText "Latence"
uid=78_42 InlineTextBox "Latence"
uid=78_47 StaticText " "
uid=78_42 InlineTextBox " "
uid=78_48 StaticText "1ms"
uid=78_42 InlineTextBox "1ms"
uid=78_49 generic
uid=78_50 LabelText
uid=78_51 StaticText "IPS"
uid=78_42 InlineTextBox "IPS"
uid=78_52 StaticText " "
uid=78_42 InlineTextBox " "
uid=78_53 StaticText "--"
uid=78_42 InlineTextBox "--"
uid=78_54 button
uid=78_55 generic
uid=78_56 navigation
uid=78_57 list
uid=78_58 generic
uid=78_59 sectionheader
uid=78_60 generic
uid=78_61 generic
uid=78_62 LabelText
uid=78_63 LabelText
uid=78_64 sectionfooter
uid=78_65 generic roledescription="Barre de raccourcis"
uid=78_66 generic
uid=78_67 button "Couper le son"
uid=78_68 button "Menu principal"
uid=78_69 list
uid=78_70 button "Emplacement vide"
uid=78_71 ignored
uid=78_72 StaticText "1"
uid=78_42 InlineTextBox "1"
uid=78_73 button "Emplacement vide"
uid=78_74 ignored
uid=78_75 StaticText "2"
uid=78_42 InlineTextBox "2"
uid=78_76 button "Emplacement vide"
uid=78_77 ignored
uid=78_78 StaticText "3"
uid=78_42 InlineTextBox "3"
uid=78_79 button "Emplacement vide"
uid=78_80 ignored
uid=78_81 StaticText "4"
uid=78_42 InlineTextBox "4"
uid=78_82 button "Emplacement vide"
uid=78_83 ignored
uid=78_84 StaticText "5"
uid=78_42 InlineTextBox "5"
uid=78_85 button "Emplacement vide"
uid=78_86 ignored
uid=78_87 StaticText "6"
uid=78_42 InlineTextBox "6"
uid=78_88 button "Emplacement vide"
uid=78_89 ignored
uid=78_90 StaticText "7"
uid=78_42 InlineTextBox "7"
uid=78_91 button "Emplacement vide"
uid=78_92 ignored
uid=78_93 StaticText "8"
uid=78_42 InlineTextBox "8"
uid=78_94 button "Emplacement vide"
uid=78_95 ignored
uid=78_96 StaticText "9"
uid=78_42 InlineTextBox "9"
uid=78_97 button "Emplacement vide"
uid=78_98 ignored
uid=78_99 StaticText "0"
uid=78_42 InlineTextBox "0"
uid=78_100 generic
uid=78_101 navigation
uid=78_102 button "Page suivante"
uid=78_103 ignored
uid=78_104 StaticText "1"
uid=78_42 InlineTextBox "1"
uid=78_105 button "Page précédente"
uid=78_106 ignored
uid=78_107 button "Verrouiller la barre de raccourcis"
uid=78_108 button "Effacer la barre de raccourcis"
uid=78_109 generic
uid=78_110 generic
uid=78_111 generic
uid=78_112 ignored
uid=78_113 list
uid=78_114 generic "Tchat"
uid=78_115 generic
uid=78_116 generic value="
"
uid=78_117 paragraph
uid=78_118 LineBreak "
"
uid=78_42 InlineTextBox "
"
uid=78_119 ignored
uid=78_42 generic
uid=78_42 StaticText "Entrer un message"
uid=78_42 InlineTextBox "Entrer un message"
uid=78_120 generic
uid=78_121 generic
uid=78_122 button "Public en tant qu'utilisateur" pressed
uid=78_123 button "Privé pour les maîtres de jeu"
uid=78_124 button "Aveugle pour les maîtres de jeu"
uid=78_125 button "Seulement pour soi-même"
uid=78_126 button "Public en tant que personnage"
uid=78_127 generic
uid=78_128 tablist orientation="horizontal"
uid=78_129 list
uid=78_130 listitem level="1"
uid=78_131 tab "Messages du tchat" selectable
uid=78_132 listitem level="1"
uid=78_133 tab "Rencontres de combat" selectable
uid=78_134 listitem level="1"
uid=78_135 tab "Scènes" selectable
uid=78_136 listitem level="1"
uid=78_137 tab "Objets plaçables" selectable
uid=78_138 listitem level="1"
uid=78_139 tab "Acteurs" selectable
uid=78_140 listitem level="1"
uid=78_141 tab "Objets" selectable
uid=78_142 listitem level="1"
uid=78_143 tab "Journaux" selectable
uid=78_144 listitem level="1"
uid=78_145 tab "Tables aléatoires" selectable
uid=78_146 listitem level="1"
uid=78_147 tab "Jeux de cartes" selectable
uid=78_148 listitem level="1"
uid=78_149 tab "Macros" selectable
uid=78_150 listitem level="1"
uid=78_151 tab "Playlists" selectable
uid=78_152 listitem level="1"
uid=78_153 tab "Compendiums" selectable
uid=78_154 listitem level="1"
uid=78_155 tab "Paramètres" selectable
uid=78_156 listitem level="1"
uid=78_157 button "Réduire"
uid=78_158 generic
uid=78_159 ignored
uid=78_160 ignored
uid=78_161 ignored
uid=78_162 ignored
uid=78_163 generic
uid=78_164 sectionheader
uid=78_165 ignored
uid=78_166 button "Créer un acteur"
uid=78_167 ignored
uid=78_168 ignored
uid=78_169 StaticText "Créer un acteur"
uid=78_42 InlineTextBox "Créer un acteur"
uid=78_170 button "Créer un dossier"
uid=78_171 ignored
uid=78_172 ignored
uid=78_173 StaticText "Créer un dossier"
uid=78_42 InlineTextBox "Créer un dossier"
uid=78_174 search
uid=78_175 button "Recherche par nom uniquement"
uid=78_176 searchbox "Chercher dans les Acteurs"
uid=78_177 ignored
uid=78_178 ignored
uid=78_179 generic
uid=78_180 button "Trier par ordre alphabétique"
uid=78_181 button "Réduire tous les dossiers"
uid=78_182 list
uid=78_183 listitem level="1"
uid=78_184 image "Acteur" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/creature.webp"
uid=78_185 generic
uid=78_186 StaticText "Acteur"
uid=78_42 InlineTextBox "Acteur"
uid=78_187 listitem level="1"
uid=78_188 image "Acteur (2)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/group.webp"
uid=78_189 generic
uid=78_190 StaticText "Acteur (2)"
uid=78_42 InlineTextBox "Acteur (2)"
uid=78_191 listitem level="1"
uid=78_192 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
uid=78_193 generic
uid=78_194 StaticText "Acteur (3)"
uid=78_42 InlineTextBox "Acteur (3)"
uid=78_195 sectionfooter
uid=78_196 ignored
uid=78_197 ignored
uid=78_198 ignored
uid=78_199 ignored
uid=78_200 ignored
uid=78_201 ignored
uid=78_202 ignored
uid=78_203 ignored
uid=78_204 ignored
uid=78_205 ignored
uid=78_206 ignored
uid=78_207 ignored
uid=78_208 ignored
uid=78_209 ignored
uid=78_210 ignored
uid=78_211 ignored
uid=78_212 ignored
uid=78_213 ignored
uid=78_214 ignored
uid=78_215 ignored
uid=78_216 ignored
uid=78_217 ignored
uid=78_218 ignored
uid=78_219 figure
uid=78_220 image url="https://localhost:31000/ui/pause.svg"
uid=78_221 Figcaption
uid=78_222 StaticText "JEU EN PAUSE"
uid=78_42 InlineTextBox "JEU EN PAUSE"
uid=78_223 form
uid=78_224 banner
uid=78_225 heading "Personnage: Acteur (3)" level="1"
uid=78_226 StaticText "Personnage: Acteur (3)"
uid=78_42 InlineTextBox "Personnage: Acteur (3)"
uid=78_227 button "Basculer les contrôles"
uid=78_228 button "Copier l'UUID du document"
uid=78_229 button "Fermer la fenêtre"
uid=78_230 generic
uid=78_231 ignored
uid=78_232 ignored
uid=78_233 button "MODE ÉDITION"
uid=78_234 StaticText "MODE ÉDITION"
uid=78_42 InlineTextBox "MODE ÉDITION"
uid=78_235 image "logo Vermine" url="https://localhost:31000/systems/vermine2047/assets/images/ui/logo.webp"
uid=78_236 ignored
uid=78_237 image "Acteur (3)" url="https://localhost:31000/systems/vermine2047/assets/icons/actors/character.webp"
uid=78_238 generic
uid=78_239 heading "ADAPTATION" level="3"
uid=78_240 StaticText "ADAPTATION"
uid=78_42 InlineTextBox "ADAPTATION"
uid=78_241 heading "L'Humain" level="5"
uid=78_242 StaticText "L'Humain"
uid=78_42 InlineTextBox "L'Humain"
uid=78_243 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/human.webp"
uid=78_244 heading "L'adapté" level="5"
uid=78_245 StaticText "L'adapté"
uid=78_42 InlineTextBox "L'adapté"
uid=78_246 image url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/adapted.webp"
uid=78_247 ignored
uid=78_248 ignored
uid=78_249 ignored
uid=78_250 generic
uid=78_251 generic
uid=78_252 ignored
uid=78_253 ignored
uid=78_254 ignored
uid=78_255 ignored
uid=78_256 generic
uid=78_257 generic
uid=78_258 ignored
uid=78_259 ignored
uid=78_260 list
uid=78_261 listitem level="1"
uid=78_262 ignored
uid=78_263 heading "LA HORDE" level="4"
uid=78_264 StaticText "LA HORDE"
uid=78_42 InlineTextBox "LA HORDE"
uid=78_265 image "La Horde" url="https://localhost:31000/systems/vermine2047/assets/images/ui/totems/horde.webp"
uid=78_266 navigation roledescription="Navigation dans l’onglet des feuilles de personnages"
uid=78_267 generic
uid=78_268 ignored
uid=78_269 StaticText "Caractéristiques et compétences"
uid=78_42 InlineTextBox "Caractéristiques "
uid=78_42 InlineTextBox "et compétences"
uid=78_270 generic
uid=78_271 ignored
uid=78_272 StaticText "Totem et ajustements"
uid=78_42 InlineTextBox "Totem et "
uid=78_42 InlineTextBox "ajustements"
uid=78_273 generic
uid=78_274 ignored
uid=78_275 StaticText "Matériel"
uid=78_42 InlineTextBox "Matériel"
uid=78_276 generic
uid=78_277 ignored
uid=78_278 StaticText "Histoire"
uid=78_42 InlineTextBox "Histoire"
uid=78_279 generic
uid=78_280 ignored
uid=78_281 StaticText "Combat et reserves"
uid=78_42 InlineTextBox "Combat "
uid=78_42 InlineTextBox "et "
uid=78_42 InlineTextBox "reserves"
uid=78_282 generic
uid=78_283 sectionheader
uid=78_284 generic
uid=78_285 heading "Nom Acteur (3)" level="1"
uid=78_286 LabelText
uid=78_287 StaticText "Nom"
uid=78_42 InlineTextBox "Nom"
uid=78_288 ignored
uid=78_289 StaticText "Acteur (3)"
uid=78_42 InlineTextBox "Acteur (3)"
uid=78_290 ignored
uid=78_291 LabelText
uid=78_292 StaticText "Profil"
uid=78_42 InlineTextBox "Profil"
uid=78_293 ignored
uid=78_294 ignored
uid=78_295 LabelText
uid=78_296 StaticText "Age"
uid=78_42 InlineTextBox "Age"
uid=78_297 ignored
uid=78_298 ignored
uid=78_299 StaticText "15"
uid=78_42 InlineTextBox "15"
uid=78_300 generic
uid=78_301 StaticText "(Jeune)"
uid=78_42 InlineTextBox "(Jeune)"
uid=78_302 generic
uid=78_303 heading "Totem La Horde" level="1"
uid=78_304 LabelText
uid=78_305 StaticText "Totem"
uid=78_42 InlineTextBox "Totem"
uid=78_306 generic
uid=78_307 StaticText "La Horde"
uid=78_42 InlineTextBox "La Horde"
uid=78_308 ignored
uid=78_309 LabelText
uid=78_310 StaticText "Réputation"
uid=78_42 InlineTextBox "Réputation"
uid=78_311 ignored
uid=78_312 StaticText "0"
uid=78_42 InlineTextBox "0"
uid=78_313 LabelText
uid=78_314 StaticText "Expérience"
uid=78_42 InlineTextBox "Expérience"
uid=78_315 ignored
uid=78_316 StaticText "0"
uid=78_42 InlineTextBox "0"
uid=78_317 heading "CARACTÉRISTIQUES" level="3"
uid=78_318 StaticText "CARACTÉRISTIQUES"
uid=78_42 InlineTextBox "CARACTÉRISTIQUES"
uid=78_319 ignored
uid=78_320 ignored
uid=78_321 heading "PHYSIQUE" level="4"
uid=78_322 StaticText "PHYSIQUE"
uid=78_42 InlineTextBox "PHYSIQUE"
uid=78_323 generic
uid=78_324 LabelText
uid=78_325 StaticText "Vigueur"
uid=78_42 InlineTextBox "Vigueur"
uid=78_326 generic
uid=78_327 LabelText
uid=78_328 StaticText "Santé"
uid=78_42 InlineTextBox "Santé"
uid=78_329 ignored
uid=78_330 heading "MANUEL" level="4"
uid=78_331 StaticText "MANUEL"
uid=78_42 InlineTextBox "MANUEL"
uid=78_332 generic
uid=78_333 LabelText
uid=78_334 StaticText "Précision"
uid=78_42 InlineTextBox "Précision"
uid=78_335 generic
uid=78_336 LabelText
uid=78_337 StaticText "Réflexes"
uid=78_42 InlineTextBox "Réflexes"
uid=78_338 ignored
uid=78_339 heading "MENTAL" level="4"
uid=78_340 StaticText "MENTAL"
uid=78_42 InlineTextBox "MENTAL"
uid=78_341 generic
uid=78_342 LabelText
uid=78_343 StaticText "Savoir"
uid=78_42 InlineTextBox "Savoir"
uid=78_344 generic
uid=78_345 LabelText
uid=78_346 StaticText "Perception"
uid=78_42 InlineTextBox "Perception"
uid=78_347 ignored
uid=78_348 heading "SOCIAL" level="4"
uid=78_349 StaticText "SOCIAL"
uid=78_42 InlineTextBox "SOCIAL"
uid=78_350 generic
uid=78_351 LabelText
uid=78_352 StaticText "Volonté"
uid=78_42 InlineTextBox "Volonté"
uid=78_353 generic
uid=78_354 LabelText
uid=78_355 StaticText "Empathie"
uid=78_42 InlineTextBox "Empathie"
uid=78_356 heading "COMPÉTENCES" level="3"
uid=78_357 StaticText "COMPÉTENCES"
uid=78_42 InlineTextBox "COMPÉTENCES"
uid=78_358 ignored
uid=78_359 ignored
uid=78_360 heading "L'HOMME" level="4"
uid=78_361 StaticText "L'HOMME"
uid=78_42 InlineTextBox "L'HOMME"
uid=78_362 generic
uid=78_363 LabelText
uid=78_364 StaticText "Arts "
uid=78_42 InlineTextBox "Arts "
uid=78_365 superscript
uid=78_366 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_367 generic
uid=78_368 LabelText
uid=78_369 StaticText "Civilisation "
uid=78_42 InlineTextBox "Civilisation "
uid=78_370 superscript
uid=78_371 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_372 generic
uid=78_373 LabelText
uid=78_374 StaticText "Psychologie "
uid=78_42 InlineTextBox "Psychologie "
uid=78_375 superscript
uid=78_376 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_377 generic
uid=78_378 LabelText
uid=78_379 StaticText "Rumeurs"
uid=78_42 InlineTextBox "Rumeurs"
uid=78_380 generic
uid=78_381 LabelText
uid=78_382 StaticText "Soins "
uid=78_42 InlineTextBox "Soins "
uid=78_383 superscript
uid=78_384 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_385 ignored
uid=78_386 heading "L'ANIMAL" level="4"
uid=78_387 StaticText "L'ANIMAL"
uid=78_42 InlineTextBox "L'ANIMAL"
uid=78_388 generic
uid=78_389 LabelText
uid=78_390 StaticText "Animalisme "
uid=78_42 InlineTextBox "Animalisme "
uid=78_391 superscript
uid=78_392 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_393 generic
uid=78_394 LabelText
uid=78_395 StaticText "Dissection "
uid=78_42 InlineTextBox "Dissection "
uid=78_396 superscript
uid=78_397 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_398 generic
uid=78_399 LabelText
uid=78_400 StaticText "Faune "
uid=78_42 InlineTextBox "Faune "
uid=78_401 superscript
uid=78_402 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_403 generic
uid=78_404 LabelText
uid=78_405 StaticText "Répulsion"
uid=78_42 InlineTextBox "Répulsion"
uid=78_406 generic
uid=78_407 LabelText
uid=78_408 StaticText "Pistage"
uid=78_42 InlineTextBox "Pistage"
uid=78_409 ignored
uid=78_410 heading "LA MACHINE" level="4"
uid=78_411 StaticText "LA MACHINE"
uid=78_42 InlineTextBox "LA MACHINE"
uid=78_412 generic
uid=78_413 LabelText
uid=78_414 StaticText "Artisanat "
uid=78_42 InlineTextBox "Artisanat "
uid=78_415 superscript
uid=78_416 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_417 generic
uid=78_418 LabelText
uid=78_419 StaticText "Bricolage"
uid=78_42 InlineTextBox "Bricolage"
uid=78_420 generic
uid=78_421 LabelText
uid=78_422 StaticText "Mécanique "
uid=78_42 InlineTextBox "Mécanique "
uid=78_423 superscript
uid=78_424 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_425 generic
uid=78_426 LabelText
uid=78_427 StaticText "Pilotage "
uid=78_42 InlineTextBox "Pilotage "
uid=78_428 superscript
uid=78_429 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_430 generic
uid=78_431 LabelText
uid=78_432 StaticText "Technologie "
uid=78_42 InlineTextBox "Technologie "
uid=78_433 superscript
uid=78_434 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_435 ignored
uid=78_436 heading "L'ARME" level="4"
uid=78_437 StaticText "L'ARME"
uid=78_42 InlineTextBox "L'ARME"
uid=78_438 generic
uid=78_439 LabelText
uid=78_440 StaticText "Armes à feu "
uid=78_42 InlineTextBox "Armes à feu "
uid=78_441 superscript
uid=78_442 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_443 generic
uid=78_444 LabelText
uid=78_445 StaticText "Tir à l'arc"
uid=78_42 InlineTextBox "Tir à l'arc"
uid=78_446 generic
uid=78_447 LabelText
uid=78_448 StaticText "Armurerie "
uid=78_42 InlineTextBox "Armurerie "
uid=78_449 superscript
uid=78_450 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_451 generic
uid=78_452 LabelText
uid=78_453 StaticText "Lancer"
uid=78_42 InlineTextBox "Lancer"
uid=78_454 generic
uid=78_455 LabelText
uid=78_456 StaticText "Mêlée"
uid=78_42 InlineTextBox "Mêlée"
uid=78_457 ignored
uid=78_458 heading "LA SURVIE" level="4"
uid=78_459 StaticText "LA SURVIE"
uid=78_42 InlineTextBox "LA SURVIE"
uid=78_460 generic
uid=78_461 LabelText
uid=78_462 StaticText "Vigilance"
uid=78_42 InlineTextBox "Vigilance"
uid=78_463 generic
uid=78_464 LabelText
uid=78_465 StaticText "Athlétisme"
uid=78_42 InlineTextBox "Athlétisme"
uid=78_466 generic
uid=78_467 LabelText
uid=78_468 StaticText "Alimentation"
uid=78_42 InlineTextBox "Alimentation"
uid=78_469 generic
uid=78_470 LabelText
uid=78_471 StaticText "Discrétion"
uid=78_42 InlineTextBox "Discrétion"
uid=78_472 generic
uid=78_473 LabelText
uid=78_474 StaticText "Corps-à-corps"
uid=78_42 InlineTextBox "Corps-à-corps"
uid=78_475 ignored
uid=78_476 heading "LA TERRE" level="4"
uid=78_477 StaticText "LA TERRE"
uid=78_42 InlineTextBox "LA TERRE"
uid=78_478 generic
uid=78_479 LabelText
uid=78_480 StaticText "Environnement "
uid=78_42 InlineTextBox "Environnement "
uid=78_481 superscript
uid=78_482 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_483 generic
uid=78_484 LabelText
uid=78_485 StaticText "Flore "
uid=78_42 InlineTextBox "Flore "
uid=78_486 superscript
uid=78_487 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_488 generic
uid=78_489 LabelText
uid=78_490 StaticText "Route"
uid=78_42 InlineTextBox "Route"
uid=78_491 generic
uid=78_492 LabelText
uid=78_493 StaticText "Toxiques "
uid=78_42 InlineTextBox "Toxiques "
uid=78_494 superscript
uid=78_495 StaticText "(II)"
uid=78_42 InlineTextBox "(II)"
uid=78_496 generic
uid=78_497 LabelText
uid=78_498 StaticText "Vestiges "
uid=78_42 InlineTextBox "Vestiges "
uid=78_499 superscript
uid=78_500 StaticText "(I)"
uid=78_42 InlineTextBox "(I)"
uid=78_501 ignored
uid=78_502 ignored
uid=78_503 ignored
uid=78_504 ignored
uid=78_505 ignored
uid=78_506 ignored
uid=78_507 ignored
uid=78_508 ignored
uid=78_509 ignored
uid=78_510 ignored
uid=78_511 ignored
uid=78_512 ignored
uid=78_513 ignored
uid=78_514 ignored
uid=78_515 ignored
uid=78_516 ignored
uid=78_517 ignored
uid=78_518 ignored
uid=78_519 ignored
uid=78_520 ignored
uid=78_521 ignored
uid=78_522 ignored
uid=78_523 ignored
uid=78_524 ignored
uid=78_525 ignored
uid=78_526 ignored
uid=78_527 ignored
uid=78_528 ignored
uid=78_529 ignored
uid=78_530 ignored
uid=78_531 ignored
uid=78_532 ignored
uid=78_533 ignored
uid=78_534 ignored
uid=78_535 ignored
uid=78_536 ignored
uid=78_537 ignored
uid=78_538 ignored
uid=78_539 ignored
uid=78_540 ignored
uid=78_541 ignored
uid=78_542 ignored
uid=78_543 ignored
uid=78_544 ignored
uid=78_545 ignored
uid=78_546 ignored
uid=78_547 ignored
uid=78_548 ignored
uid=78_549 ignored
uid=78_550 ignored
uid=78_551 ignored
uid=78_552 ignored
uid=78_553 ignored
uid=78_554 ignored
uid=78_555 generic
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 KiB

+177 -1
View File
@@ -18,6 +18,7 @@
"TYPES.Item.defense": "Defense", "TYPES.Item.defense": "Defense",
"TYPES.Actor.vehicle": "Vehicle", "TYPES.Actor.vehicle": "Vehicle",
"TYPES.Item.ability": "Ability", "TYPES.Item.ability": "Ability",
"TYPES.Item.creaturecapacity": "Creature capacity",
"TYPES.Item.specialty": "Specialty", "TYPES.Item.specialty": "Specialty",
"TYPES.Item.background": "Background", "TYPES.Item.background": "Background",
"TYPES.Item.trauma": "Trauma", "TYPES.Item.trauma": "Trauma",
@@ -43,6 +44,20 @@
"nightmare": "Nightmare", "nightmare": "Nightmare",
"apocalypse": "Apocalypse" "apocalypse": "Apocalypse"
}, },
"CREATURE_CAPACITY_TYPES": {
"base": {
"name": "Base"
},
"survival": {
"name": "Survival"
},
"nightmare": {
"name": "Nightmare"
},
"apocalypse": {
"name": "Apocalypse"
}
},
"ROLLS": { "ROLLS": {
"tool": "Dice Roller", "tool": "Dice Roller",
"roll_dice": "Roll the dice" "roll_dice": "Roll the dice"
@@ -140,12 +155,43 @@
"level": "Level", "level": "Level",
"experience": "Experience", "experience": "Experience",
"reputation": "Reputation", "reputation": "Reputation",
"chaman": "Shaman",
"sheet": { "sheet": {
"description": "Description" "description": "Description"
}, },
"pool": "Reserve", "pool": "Reserve",
"pools": "Reserves", "pools": "Reserves",
"dice_pool": "Dice pool", "dice_pool": "Dice pool",
"pool_malus": "Wound penalty",
"pool_wound": "Wound penalty",
"pool_lourd": "Heavy penalty",
"lourd_malus_note": "Heavy penalty: -{malus}D (Vigor too low for equipped Heavy items)",
"effective_range": "Effective range",
"traits_mobility_note": "Mobility Handicap: +{handicap} (equipped non-Maniable protection)",
"rafale_check": "Burst fire (Difficulty +2, +{rafaleValue} successes if successful)",
"rapide_note": "Fast ({handicap}): the defender suffers {handicap} Handicap(s)",
"incapacitant_roll": "Health roll",
"intimidant": "Intimidating",
"intimidant_check": "Wield ({intimidantName}) — Intimidating",
"intimidant_psychology": "1D penalty (Psychology)",
"intimidant_reroll": "1D reroll",
"temporary_malus": "Temporary penalties",
"pool_temp_malus": "Temporary penalties",
"pool_psychology": "Psychology penalty",
"ponctuel": "One-shot",
"ponctuel_used": "cancels {value} Handicap(s)",
"duree": "Duration",
"duree_note": "effect for {value} combat turns",
"fetiche": "Fetish",
"fetiche_bonus": "Effort and Composure reserves +1D",
"fetiche_multiple": "A character can only possess a single Fetish object",
"fetiche_none": "No equipped Fetish object",
"fetiche_lost": "Fetish lost: reserves 2D, max 1D",
"fetiche_lost_penalty": "Fetish lost: reserves reduced by 1D",
"lose_fetiche": "Lose the fetish",
"zone_note": "Area ({value} m): everyone in the area suffers the damage",
"zone_apply": "Apply to targets in the area",
"zone_applied": "Area: {count} wound(s) applied",
"auto_successes": "automatic successes", "auto_successes": "automatic successes",
"self_control": "Self-control", "self_control": "Self-control",
"effort": "Effort", "effort": "Effort",
@@ -188,10 +234,53 @@
"maintenance": "Maintenance", "maintenance": "Maintenance",
"owner": "Owner", "owner": "Owner",
"no_vehicles": "No vehicles", "no_vehicles": "No vehicles",
"no_encounters": "No encounters",
"travel_notes": "Travel notes",
"rarity": "Rarity", "rarity": "Rarity",
"rarity_sm": "Rar.", "rarity_sm": "Rar.",
"reliability": "Reliability", "reliability": "Reliability",
"reliability_sm": "Fiab.", "reliability_sm": "Fiab.",
"reliability_test": "Reliability test",
"reliability_repair": "Repair",
"reliability_severity": "Damage severity",
"reliability_severity_mineurs": "Minor (1D)",
"reliability_severity_graves": "Grave (3D)",
"reliability_severity_critiques": "Critical (5D)",
"reliability_immersion": "Prolonged immersion",
"reliability_damages": "Damage taken",
"reliability_state": "State",
"reliability_destroyed": "OBJECT DESTROYED / UNUSABLE",
"reliability_repair_mode": "Repair type",
"reliability_repair_classic": "Classic",
"reliability_repair_fortune": "Field repair",
"reliability_skill": "Skill",
"reliability_run": "Roll",
"reliability_successes": "Successes",
"reliability_result": "Result",
"reliability_gained": "Reliability restored: +{gained}",
"reliability_fixed": "Breakdown fixed (object functional)",
"reliability_failed": "Failure — no reliability point restored",
"reliability_need_actor": "Repair impossible: the object must belong to an actor",
"reliability_already_destroyed": "Object already destroyed or unusable",
"reliability_coque_absorbed": "Hull absorbs {absorbed} damage(s), {remaining} left",
"reliability_coque_lost": "Hull: trait exhausted — repairable (Difficulty {max})",
"reliability_coque_restored": "Hull repaired: {max} points",
"reliability_repair_coque": "Repair the hull (Difficulty {max})",
"reliability_repair_coque_label": "Hull",
"reliability_bien_concu": "Well-made: automatic success — reliability fully restored",
"reliability_bien_concu_fortune": "Well-made: +1 reliability point (first field repair)",
"reliability_auto_success": "Automatic success (Well-made)",
"reliability_pratique": "Pratique: +I Handicap",
"reliability_cassant": "Fragile: reliability losses increased by 1 point",
"reliability_etanche": "Watertight: immersion damage ignored",
"reliability_fortune_skill_bonus": "+1D — the character has the required skill ({skill})",
"reliability_fortune_skill_missing": "Required skill ({skill}) not mastered: rarity Handicap +{rarity}",
"reliability_fortune_hint": "Field repair: +1D if the character has the required skill, otherwise the rarity Handicap of that skill applies.",
"damages_state_1": "Damaged",
"damages_state_2": "Damaged",
"damages_state_3": "Faulty",
"damages_state_4": "Faulty",
"damages_state_5": "Out of service",
"wounds": { "wounds": {
"name": "Wounds", "name": "Wounds",
"threshold": "Threshold", "threshold": "Threshold",
@@ -214,6 +303,7 @@
"boost": "Boost", "boost": "Boost",
"group_abilities": "Group abilities", "group_abilities": "Group abilities",
"totem_abilities": "Totem abilities", "totem_abilities": "Totem abilities",
"creature_capacities": "Creature capacities",
"type_group": "Group", "type_group": "Group",
"type_character": "Character", "type_character": "Character",
"type_npc": "NPC", "type_npc": "NPC",
@@ -284,7 +374,20 @@
"die_successes": "successes", "die_successes": "successes",
"total": "Total", "total": "Total",
"totem_dice": "Totem Dice", "totem_dice": "Totem Dice",
"totem_hint": "Totem Hint", "totem_hint": "Check to use totem dice (double success possible)",
"totem_neutral": "Neutral",
"totem_manage": "Manage Totem Dice (Instincts / Interdits)",
"totem_serious": "Serious act (2 dice)",
"totem_manage_hint": "Instincts: obeying the Human's Instincts gains 1 Human die; another Totem's Instincts gain 1 Adapted die. Interdit: breaking a Human Interdit loses 1 Human die (or gains 1 Adapted die if none). Max 3 dice of the same Totem and 5 dice total.",
"totem_instinct_human": "Human Instinct (+1 Human die)",
"totem_instinct_adapted": "Another Totem's Instinct (+1 Adapted die)",
"totem_interdit_human": "Human Interdit (-1 Human die)",
"totem_gained": "{totem} +1D",
"totem_gained_adapted": "+1D Adapted",
"totem_exchanged": "{totem} gained in exchange (1 opposite die removed)",
"totem_replaced": "Opposite die removed instead (max 3D reached)",
"totem_lost": "-1 Human die",
"totem_limit_reached": "Totem dice limit reached.",
"FightTool": "Fight Tool", "FightTool": "Fight Tool",
"RollTool": "Roll Tool", "RollTool": "Roll Tool",
"Roll4Fight": "Roll for Fight", "Roll4Fight": "Roll for Fight",
@@ -327,6 +430,8 @@
"no_wound": "No wound", "no_wound": "No wound",
"apply_wound": "Apply wound", "apply_wound": "Apply wound",
"wound_applied": "Wound applied", "wound_applied": "Wound applied",
"wound_escalated": "{to} wound (escalated from {from})",
"wound_death": "All wounds checked — death",
"error_creature_no_defense": "Creatures cannot defend (passive defense)", "error_creature_no_defense": "Creatures cannot defend (passive defense)",
"error_no_permission": "Insufficient permissions", "error_no_permission": "Insufficient permissions",
"error_no_target": "No target selected for this exchange", "error_no_target": "No target selected for this exchange",
@@ -352,14 +457,45 @@
"trauma_reward": "Chosen trauma", "trauma_reward": "Chosen trauma",
"trauma_reward_5": "Trauma reward (+5D)", "trauma_reward_5": "Trauma reward (+5D)",
"evolution_dice": "Evolution dice", "evolution_dice": "Evolution dice",
"buy_evolution": "Buy an Adaptation/Mutation",
"evolution_name": "Name",
"evolution_level": "Level (cost in Evolution dice)",
"evolution_mutation": "Mutation",
"evolution_adaptation": "Adaptation",
"evolution_available_dice": "Accumulated Evolution dice",
"evolution_total": "Total levels (after purchase)",
"evolution_mode_max": "Game mode maximum",
"game_mode": "Game mode",
"mode_survie": "Survival",
"mode_cauchemar": "Nightmare",
"mode_apocalypse": "Apocalypse",
"evolution_buy_hint": "The purchase consumes and erases all accumulated Evolution dice. Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
"buy_evolution_done": "Adaptation/Mutation acquired. Evolution dice erased.",
"error_evolution_no_name": "Provide a name for the Adaptation/Mutation.",
"error_evolution_invalid_level": "Invalid level (1 to 4).",
"error_evolution_not_enough_dice": "Not enough Evolution dice for this level.",
"error_evolution_mode_limit": "Game mode maximum Evolution dice reached.",
"error_evolution_mutation_mode": "Level 3/4 Mutations require Nightmare or Apocalypse modes.",
"error_evolution_mutation_4d": "The character already has a level 4 Mutation.",
"error_evolution_create": "Could not create the Adaptation/Mutation.",
"open_experience_phase": "Experience phase", "open_experience_phase": "Experience phase",
"open_learning": "Learning", "open_learning": "Learning",
"validate_phase": "Validate phase",
"validate_phase_done": "Experience phase validated: learning limits reset.",
"error_not_editable": "You do not have permission to modify this element.",
"vehicle_assign": "Attach vehicle",
"vehicle_assign_confirm": "This vehicle already belongs to \"{group}\". Attach it to this group?",
"vehicle_assign_failed": "Could not attach this vehicle to the group.",
"vehicle_import_failed": "Could not import this vehicle from the compendium.",
"learning_limits": "Learning limits (per phase)", "learning_limits": "Learning limits (per phase)",
"learned_skill": "Skill", "learned_skill": "Skill",
"learned_specialty": "Specialty", "learned_specialty": "Specialty",
"learned_reserve": "Reserve", "learned_reserve": "Reserve",
"learned_characteristic": "Characteristic", "learned_characteristic": "Characteristic",
"learned_evolution": "Evolution", "learned_evolution": "Evolution",
"learned_ability": "Totem capacity",
"learned_background": "Background",
"learned_reputation": "Reputation",
"no_character": "No character in this world.", "no_character": "No character in this world.",
"apply": "Apply", "apply": "Apply",
"characteristic": "Characteristic", "characteristic": "Characteristic",
@@ -368,11 +504,22 @@
"learn_characteristic": "Characteristic", "learn_characteristic": "Characteristic",
"learn_reserve": "Reserve", "learn_reserve": "Reserve",
"learn_evolution": "Evolution die", "learn_evolution": "Evolution die",
"learn_totem_ability": "Totem capacity",
"learn_background": "Background element",
"learn_reputation": "Reputation",
"learn_adapted_die": "Replace 1 Experience die with an Adapted totem die (×2)",
"adapted_die_x2": "1 Adapted totem die ×2",
"learn_specialty_name": "Specialty name", "learn_specialty_name": "Specialty name",
"specialty_hint": "Prerequisite: skill at least Proficient (≥2).", "specialty_hint": "Prerequisite: skill at least Proficient (≥2).",
"characteristic_hint": "Difficulty 9 + Handicap = current value. Max 3 (except Superhuman).", "characteristic_hint": "Difficulty 9 + Handicap = current value. Max 3 (except Superhuman).",
"reserve_hint": "Difficulty = current value + age Handicap. Max 10.", "reserve_hint": "Difficulty = current value + age Handicap. Max 10.",
"evolution_hint": "Difficulty by age + Handicap = accumulated Evolution dice. Adapted totem +1D, Human -1D.", "evolution_hint": "Difficulty by age + Handicap = accumulated Evolution dice. Adapted totem +1D, Human -1D.",
"totem_ability_hint": "Prerequisites: same Totem as the character, Group level ≥ Capacity level, game mode compatible (Survival N1, Nightmare N2, Apocalypse N3).",
"background_hint": "Difficulty 9. Handicap (I) if the Background is marked with an asterisk (*). GM authorization required.",
"reputation_hint": "Reduce = more famous (Difficulty 10 target score, 1 pt max per phase). Increase = fade into obscurity (requires a major Objective, Difficulty = current Reputation, +1 per Success).",
"reputation_reduce": "Become more famous (reduce)",
"reputation_increase": "Fade into obscurity (increase)",
"reputation_current": "Current Reputation",
"dice_spent": "Dice spent", "dice_spent": "Dice spent",
"learning_success": "Learning successful!", "learning_success": "Learning successful!",
"error_no_experience_dice": "No Experience dice available.", "error_no_experience_dice": "No Experience dice available.",
@@ -381,6 +528,25 @@
"error_characteristic_max": "Characteristic already at maximum (3).", "error_characteristic_max": "Characteristic already at maximum (3).",
"error_specialty_requirement": "Specialty requires: skill at least Proficient (≥2).", "error_specialty_requirement": "Specialty requires: skill at least Proficient (≥2).",
"error_reserve_max": "Reserve already at maximum (10).", "error_reserve_max": "Reserve already at maximum (10).",
"no_totem_ability": "No Totem capacity available to learn.",
"no_background": "No background element to acquire.",
"error_totem_mode": "Capacity level incompatible with the game mode.",
"error_totem_totem": "The character must share the Totem of the Capacity.",
"error_totem_group_level": "Group level insufficient for this Capacity.",
"error_totem_limit": "Totem capacity limit reached (3× Level 1, 1× Levels 2 and 3, max 5).",
"error_reputation_min": "Reputation already at minimum (0).",
"error_reputation_max": "Reputation already at maximum (10).",
"error_evolution_no_adapted": "The character must have strictly more Adapted than Human totem dice.",
"learn_level": "Group level",
"learn_group_reserve": "Group reserve",
"learn_ability": "Group capacity",
"group_level_hint": "Difficulty = number of members. Handicap = current Group level. Max 10.",
"group_reserve_hint": "Difficulty = current Reserve value. Handicap II. Max 10.",
"group_ability_hint": "Difficulty = Learning. Handicap = Capacity level. Requires Group level ≥ Capacity level.",
"no_group_ability": "No Group capacity available to develop.",
"error_group_level_max": "Group level already at maximum (10).",
"error_group_reserve_max": "Group reserve already at maximum.",
"error_group_level_requirement": "Group level insufficient for this Capacity.",
"experience_transfer": "Experience dice", "experience_transfer": "Experience dice",
"experience_pull": "Pull from group", "experience_pull": "Pull from group",
"experience_give": "Give 1D to group", "experience_give": "Give 1D to group",
@@ -418,6 +584,8 @@
"morale": "Morale", "morale": "Morale",
"needed": "Needed", "needed": "Needed",
"objective_placeholder": "eg: Survive, Explore", "objective_placeholder": "eg: Survive, Explore",
"major_objective_placeholder": "eg: Reach a distant goal",
"minor_objective_placeholder": "eg: Find a medic, scavenge",
"objectives": "Objectives", "objectives": "Objectives",
"pack": "Pack", "pack": "Pack",
"pattern": "Pattern", "pattern": "Pattern",
@@ -444,6 +612,7 @@
}, },
"roll": "Roll", "roll": "Roll",
"cancel": "Cancel", "cancel": "Cancel",
"close": "Close",
"playMode": "Play Mode", "playMode": "Play Mode",
"editMode": "Edit Mode" "editMode": "Edit Mode"
}, },
@@ -570,6 +739,8 @@
"ability": "Ability", "ability": "Ability",
"new_ability": "New ability", "new_ability": "New ability",
"abilities": "Abilities", "abilities": "Abilities",
"creaturecapacity": "Creature capacity",
"new_creaturecapacity": "New creature capacity",
"specialties": "Specialties", "specialties": "Specialties",
"new_specialty": "New specialty", "new_specialty": "New specialty",
"target": "Target", "target": "Target",
@@ -644,6 +815,11 @@
"dog": "Dog", "dog": "Dog",
"bear": "Bear" "bear": "Bear"
}, },
"PACK_LEVELS": {
"small": "Small",
"large": "Large",
"giant": "Giant"
},
"TOTEMS": { "TOTEMS": {
"human": { "human": {
"name": "Human", "name": "Human",
+176
View File
@@ -10,6 +10,7 @@
"TYPES.Item.defense": "Protection", "TYPES.Item.defense": "Protection",
"TYPES.Actor.vehicle": "Véhicule", "TYPES.Actor.vehicle": "Véhicule",
"TYPES.Item.ability": "Capacité", "TYPES.Item.ability": "Capacité",
"TYPES.Item.creaturecapacity": "Capacité de créature",
"TYPES.Item.specialty": "Spécialité", "TYPES.Item.specialty": "Spécialité",
"TYPES.Item.background": "Historique", "TYPES.Item.background": "Historique",
"TYPES.Item.trauma": "Traumatisme", "TYPES.Item.trauma": "Traumatisme",
@@ -49,6 +50,20 @@
"nightmare": "Cauchemar", "nightmare": "Cauchemar",
"apocalypse": "Apocalypse" "apocalypse": "Apocalypse"
}, },
"CREATURE_CAPACITY_TYPES": {
"base": {
"name": "Base"
},
"survival": {
"name": "Survie"
},
"nightmare": {
"name": "Cauchemar"
},
"apocalypse": {
"name": "Apocalypse"
}
},
"ROLLS": { "ROLLS": {
"tool": "Lanceur de dés", "tool": "Lanceur de dés",
"roll_dice": "Jeter les dés" "roll_dice": "Jeter les dés"
@@ -146,6 +161,7 @@
"level": "Niveau", "level": "Niveau",
"experience": "Expérience", "experience": "Expérience",
"reputation": "Réputation", "reputation": "Réputation",
"chaman": "Chaman",
"sheet": { "sheet": {
"description": "Description" "description": "Description"
}, },
@@ -153,6 +169,36 @@
"pools": "Réserves", "pools": "Réserves",
"reserves": "Réserves", "reserves": "Réserves",
"dice_pool": "Pool de dés", "dice_pool": "Pool de dés",
"pool_malus": "Malus Blessure",
"pool_wound": "Malus Blessure",
"pool_lourd": "Malus Lourd",
"lourd_malus_note": "Malus Lourd : -{malus}D (Vigueur insuffisante pour les objets Lourds équipés)",
"effective_range": "Portée effective",
"traits_mobility_note": "Handicap de Mobilité : +{handicap} (protections équipées non Maniables)",
"rafale_check": "Tir en rafale (Difficulté +2, +{rafaleValue} Réussites si réussi)",
"rapide_note": "Rapide ({handicap}) : le défenseur subit {handicap} Handicap(s)",
"incapacitant_roll": "Jet de Santé",
"intimidant": "Intimidant",
"intimidant_check": "Brandit ({intimidantName}) — Intimidant",
"intimidant_psychology": "Malus 1D (Psychologie)",
"intimidant_reroll": "Relance 1D",
"temporary_malus": "Malus temporaires",
"pool_temp_malus": "Malus temporaires",
"pool_psychology": "Malus Psychologie",
"ponctuel": "Ponctuel",
"ponctuel_used": "annule {value} Handicap(s)",
"duree": "Durée",
"duree_note": "effet pendant {value} tours de combat",
"fetiche": "Fétiche",
"fetiche_bonus": "Réserves d'Effort et de Sang-Froid +1D",
"fetiche_multiple": "Un même personnage ne peut posséder qu'un seul objet Fétiche",
"fetiche_none": "Aucun objet Fétiche équipé",
"fetiche_lost": "Fétiche perdu : Réserves 2D, max 1D",
"fetiche_lost_penalty": "Fétiche perdu : Réserves réduites de 1D",
"lose_fetiche": "Perdre le fétiche",
"zone_note": "Zone ({value} m) : tous les présents dans la zone subissent les Dommages",
"zone_apply": "Appliquer aux cibles de la zone",
"zone_applied": "Zone : {count} blessure(s) appliquée(s)",
"auto_successes": "réussites automatiques", "auto_successes": "réussites automatiques",
"self_control": "Sang-Froid", "self_control": "Sang-Froid",
"effort": "Effort", "effort": "Effort",
@@ -269,10 +315,53 @@
"maintenance": "Entretien", "maintenance": "Entretien",
"owner": "Propriétaire", "owner": "Propriétaire",
"no_vehicles": "Aucun véhicule", "no_vehicles": "Aucun véhicule",
"no_encounters": "Aucune rencontre",
"travel_notes": "Notes de voyage",
"rarity": "Rareté", "rarity": "Rareté",
"rarity_sm": "Rar.", "rarity_sm": "Rar.",
"reliability": "Fiabilité", "reliability": "Fiabilité",
"reliability_sm": "Fiab.", "reliability_sm": "Fiab.",
"reliability_test": "Test de Fiabilité",
"reliability_repair": "Réparer",
"reliability_severity": "Gravité des Dommages",
"reliability_severity_mineurs": "Mineurs (1D)",
"reliability_severity_graves": "Graves (3D)",
"reliability_severity_critiques": "Critiques (5D)",
"reliability_immersion": "Immersion prolongée",
"reliability_damages": "Dommages subis",
"reliability_state": "État",
"reliability_destroyed": "OBJET DÉTRUIT / INUTILISABLE",
"reliability_repair_mode": "Type de réparation",
"reliability_repair_classic": "Classique",
"reliability_repair_fortune": "De fortune",
"reliability_skill": "Compétence",
"reliability_run": "Lancer",
"reliability_successes": "Réussites",
"reliability_result": "Résultat",
"reliability_gained": "Fiabilité restaurée : +{gained}",
"reliability_fixed": "Panne réparée (objet fonctionnel)",
"reliability_failed": "Échec — aucun point de Fiabilité rendu",
"reliability_need_actor": "Réparation impossible : l'objet doit appartenir à un acteur",
"reliability_already_destroyed": "Objet déjà détruit ou inutilisable",
"reliability_coque_absorbed": "Coque absorbe {absorbed} Dommage(s), {remaining} restant(s)",
"reliability_coque_lost": "Coque : Trait épuisé — réparable (Difficulté {max})",
"reliability_coque_restored": "Coque restaurée : {max} points",
"reliability_repair_coque": "Réparer la coque (Difficulté {max})",
"reliability_repair_coque_label": "Coque",
"reliability_bien_concu": "Bien conçu : Réussite automatique — Fiabilité entièrement restaurée",
"reliability_bien_concu_fortune": "Bien conçu : +1 point de Fiabilité (1ʳᵉ réparation de fortune)",
"reliability_auto_success": "Réussite automatique (Bien conçu)",
"reliability_pratique": "Pratique : Handicap +I",
"reliability_cassant": "Cassant : pertes de Fiabilité majorées d'1 point",
"reliability_etanche": "Étanche : Dommages d'immersion ignorés",
"reliability_fortune_skill_bonus": "+1D — le personnage possède la Compétence requise ({skill})",
"reliability_fortune_skill_missing": "Compétence requise ({skill}) non maîtrisée : Handicap de Rareté +{rarity}",
"reliability_fortune_hint": "Réparation de fortune : +1D si le personnage possède la Compétence requise, sinon Handicap de Rareté de cette Compétence.",
"damages_state_1": "Endommagé",
"damages_state_2": "Endommagé",
"damages_state_3": "Défectueux",
"damages_state_4": "Défectueux",
"damages_state_5": "Hors d'usage",
"choose_ability": "Choisissez une caractéristique", "choose_ability": "Choisissez une caractéristique",
"choose_skill": "Choisissez une compétence", "choose_skill": "Choisissez une compétence",
"none": "Aucun", "none": "Aucun",
@@ -283,6 +372,19 @@
"totem_dice": "Dés de totem", "totem_dice": "Dés de totem",
"keep_totem": "Garder le totem", "keep_totem": "Garder le totem",
"totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)", "totem_hint": "Cochez pour utiliser les dés de totem (double réussite possible)",
"totem_neutral": "Neutre",
"totem_manage": "Gérer les Dés de Totems (Instincts / Interdits)",
"totem_serious": "Acte grave (2 dés)",
"totem_manage_hint": "Instincts : obéir aux Instincts de l'Humain gagne 1D Humain ; ceux d'un autre Totem gagnent 1D Adapté. Interdit : briser un Interdit de l'Humain fait perdre 1D Humain (ou gagner 1D Adapté si aucun). Max 3 Dés du même Totem et 5 Dés au total.",
"totem_instinct_human": "Instinct de l'Humain (+1D Humain)",
"totem_instinct_adapted": "Instinct d'un autre Totem (+1D Adapté)",
"totem_interdit_human": "Interdit de l'Humain (-1D Humain)",
"totem_gained": "{totem} +1D",
"totem_gained_adapted": "+1D Adapté",
"totem_exchanged": "{totem} gagné en échange (1D opposé retiré)",
"totem_replaced": "Dé opposé retiré à la place (max 3D atteint)",
"totem_lost": "-1D Humain",
"totem_limit_reached": "Limite de Dés de Totems atteinte.",
"attack": "Attaque", "attack": "Attaque",
"attack_contact": "Attaque au contact", "attack_contact": "Attaque au contact",
"attack_ranged": "Attaque à distance", "attack_ranged": "Attaque à distance",
@@ -311,6 +413,8 @@
"no_wound": "Aucune blessure", "no_wound": "Aucune blessure",
"apply_wound": "Appliquer la blessure", "apply_wound": "Appliquer la blessure",
"wound_applied": "Blessure appliquée", "wound_applied": "Blessure appliquée",
"wound_escalated": "Blessure {to} (montée depuis {from})",
"wound_death": "Toutes les blessures sont cochées — mort",
"error_creature_no_defense": "Les créatures ne peuvent pas se défendre (défense passive)", "error_creature_no_defense": "Les créatures ne peuvent pas se défendre (défense passive)",
"error_no_permission": "Permissions insuffisantes", "error_no_permission": "Permissions insuffisantes",
"error_no_target": "Aucune cible sélectionnée pour cet échange", "error_no_target": "Aucune cible sélectionnée pour cet échange",
@@ -336,14 +440,45 @@
"trauma_reward": "Traumatisme choisi", "trauma_reward": "Traumatisme choisi",
"trauma_reward_5": "Récompense de Traumatisme (+5D)", "trauma_reward_5": "Récompense de Traumatisme (+5D)",
"evolution_dice": "Dés d'Évolution", "evolution_dice": "Dés d'Évolution",
"buy_evolution": "Acheter une Adaptation/Mutation",
"evolution_name": "Nom",
"evolution_level": "Niveau (coût en Dés d'Évolution)",
"evolution_mutation": "Mutation",
"evolution_adaptation": "Adaptation",
"evolution_available_dice": "Dés d'Évolution accumulés",
"evolution_total": "Total des niveaux (après achat)",
"evolution_mode_max": "Maximum du Mode de jeu",
"game_mode": "Mode de jeu",
"mode_survie": "Survie",
"mode_cauchemar": "Cauchemar",
"mode_apocalypse": "Apocalypse",
"evolution_buy_hint": "L'achat consomme et efface tous les Dés d'Évolution accumulés. Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
"buy_evolution_done": "Adaptation/Mutation acquise. Dés d'Évolution effacés.",
"error_evolution_no_name": "Indiquez un nom pour l'Adaptation/Mutation.",
"error_evolution_invalid_level": "Niveau invalide (1 à 4).",
"error_evolution_not_enough_dice": "Dés d'Évolution insuffisants pour ce niveau.",
"error_evolution_mode_limit": "Maximum de Dés d'Évolution du Mode de jeu atteint.",
"error_evolution_mutation_mode": "Les Mutations de Niveau 3/4 nécessitent les Modes Cauchemar ou Apocalypse.",
"error_evolution_mutation_4d": "Le personnage possède déjà une Mutation à 4D.",
"error_evolution_create": "Impossible de créer l'Adaptation/Mutation.",
"open_experience_phase": "Phase d'Expérience", "open_experience_phase": "Phase d'Expérience",
"open_learning": "Apprentissage", "open_learning": "Apprentissage",
"validate_phase": "Valider la phase",
"validate_phase_done": "Phase d'Expérience validée : limites d'apprentissage réinitialisées.",
"error_not_editable": "Vous n'avez pas la permission de modifier cet élément.",
"vehicle_assign": "Attacher le véhicule",
"vehicle_assign_confirm": "Ce véhicule appartient déjà à « {group} ». L'attacher à ce Groupe ?",
"vehicle_assign_failed": "Impossible d'attacher ce véhicule au Groupe.",
"vehicle_import_failed": "Impossible d'importer ce véhicule depuis le compendium.",
"learning_limits": "Limites d'apprentissage (par phase)", "learning_limits": "Limites d'apprentissage (par phase)",
"learned_skill": "Compétence", "learned_skill": "Compétence",
"learned_specialty": "Spécialité", "learned_specialty": "Spécialité",
"learned_reserve": "Réserve", "learned_reserve": "Réserve",
"learned_characteristic": "Caractéristique", "learned_characteristic": "Caractéristique",
"learned_evolution": "Évolution", "learned_evolution": "Évolution",
"learned_ability": "Capacité de Totem",
"learned_background": "Historique",
"learned_reputation": "Réputation",
"no_character": "Aucun personnage dans ce monde.", "no_character": "Aucun personnage dans ce monde.",
"apply": "Appliquer", "apply": "Appliquer",
"characteristic": "Caractéristique", "characteristic": "Caractéristique",
@@ -352,11 +487,22 @@
"learn_characteristic": "Caractéristique", "learn_characteristic": "Caractéristique",
"learn_reserve": "Réserve", "learn_reserve": "Réserve",
"learn_evolution": "Dé d'Évolution", "learn_evolution": "Dé d'Évolution",
"learn_totem_ability": "Capacité de Totem",
"learn_background": "Élément d'Historique",
"learn_reputation": "Réputation",
"learn_adapted_die": "Remplacer 1D d'Expérience par un Dé de Totem Adapté (×2)",
"adapted_die_x2": "1 Dé de Totem Adapté ×2",
"learn_specialty_name": "Nom de la spécialité", "learn_specialty_name": "Nom de la spécialité",
"specialty_hint": "Prérequis : compétence au moins Confirmée (≥2).", "specialty_hint": "Prérequis : compétence au moins Confirmée (≥2).",
"characteristic_hint": "Difficulté 9 + Handicap = valeur actuelle. Max 3 (Surhumain excepté).", "characteristic_hint": "Difficulté 9 + Handicap = valeur actuelle. Max 3 (Surhumain excepté).",
"reserve_hint": "Difficulté = valeur actuelle + Handicap d'âge. Max 10.", "reserve_hint": "Difficulté = valeur actuelle + Handicap d'âge. Max 10.",
"evolution_hint": "Difficulté selon l'âge + Handicap = Dés d'Évolution accumulés. Totem Adapté +1D, Humain -1D.", "evolution_hint": "Difficulté selon l'âge + Handicap = Dés d'Évolution accumulés. Totem Adapté +1D, Humain -1D.",
"totem_ability_hint": "Prérequis : même Totem que le personnage, Niveau de Groupe ≥ Niveau de la Capacité, Mode de jeu compatible (Survie N1, Cauchemar N2, Apocalypse N3).",
"background_hint": "Difficulté 9. Handicap (I) si l'Historique est marqué d'un astérisque (*). Autorisation du MJ requise.",
"reputation_hint": "Réduire = plus célèbre (Difficulté 10 score visé, 1 pt max/phase). Augmenter = se faire oublier (exige un Objectif majeur, Difficulté = Réputation actuelle, +1 par Réussite).",
"reputation_reduce": "Devenir plus célèbre (réduire)",
"reputation_increase": "Se faire oublier (augmenter)",
"reputation_current": "Réputation actuelle",
"dice_spent": "Dés dépensés", "dice_spent": "Dés dépensés",
"learning_success": "Apprentissage réussi !", "learning_success": "Apprentissage réussi !",
"error_no_experience_dice": "Pas de Dés d'Expérience disponibles.", "error_no_experience_dice": "Pas de Dés d'Expérience disponibles.",
@@ -365,6 +511,25 @@
"error_characteristic_max": "Caractéristique déjà au maximum (3).", "error_characteristic_max": "Caractéristique déjà au maximum (3).",
"error_specialty_requirement": "Spécialité requise : compétence au moins Confirmée (≥2).", "error_specialty_requirement": "Spécialité requise : compétence au moins Confirmée (≥2).",
"error_reserve_max": "Réserve déjà au maximum (10).", "error_reserve_max": "Réserve déjà au maximum (10).",
"no_totem_ability": "Aucune Capacité de Totem disponible à apprendre.",
"no_background": "Aucun élément d'Historique à acquérir.",
"error_totem_mode": "Niveau de Capacité incompatible avec le Mode de jeu.",
"error_totem_totem": "Le personnage doit être du même Totem que la Capacité.",
"error_totem_group_level": "Niveau de Groupe insuffisant pour cette Capacité.",
"error_totem_limit": "Limite de Capacités de Totem atteinte (3× Niveau 1, 1× Niveaux 2 et 3, max 5).",
"error_reputation_min": "Réputation déjà au minimum (0).",
"error_reputation_max": "Réputation déjà au maximum (10).",
"error_evolution_no_adapted": "Il faut strictement plus de Dés du Totem Adapté que Humains.",
"learn_level": "Niveau de Groupe",
"learn_group_reserve": "Réserve de Groupe",
"learn_ability": "Capacité de Groupe",
"group_level_hint": "Difficulté = nombre de membres. Handicap = Niveau actuel du Groupe. Max 10.",
"group_reserve_hint": "Difficulté = valeur actuelle de la Réserve. Handicap II. Max 10.",
"group_ability_hint": "Difficulté = Apprentissage. Handicap = Niveau de la Capacité. Nécessite un Niveau de Groupe ≥ Niveau de la Capacité.",
"no_group_ability": "Aucune Capacité de Groupe disponible à développer.",
"error_group_level_max": "Niveau de Groupe déjà au maximum (10).",
"error_group_reserve_max": "Réserve de Groupe déjà au maximum.",
"error_group_level_requirement": "Niveau de Groupe insuffisant pour cette Capacité.",
"experience_transfer": "Dés d'Expérience", "experience_transfer": "Dés d'Expérience",
"experience_pull": "Puiser du Groupe", "experience_pull": "Puiser du Groupe",
"experience_give": "Donner 1D au Groupe", "experience_give": "Donner 1D au Groupe",
@@ -405,6 +570,7 @@
"boost": "boost", "boost": "boost",
"group_abilities": "Capacités de groupe", "group_abilities": "Capacités de groupe",
"totem_abilities": "Capacités de totem", "totem_abilities": "Capacités de totem",
"creature_capacities": "Capacités de créature",
"type_group": "Groupe", "type_group": "Groupe",
"type_character": "Personnage", "type_character": "Personnage",
"type_npc": "PNJ", "type_npc": "PNJ",
@@ -431,6 +597,8 @@
"morale_low": "Bas", "morale_low": "Bas",
"morale_crisis": "Crise", "morale_crisis": "Crise",
"objective_placeholder": "Objectif...", "objective_placeholder": "Objectif...",
"major_objective_placeholder": "Objectif majeur...",
"minor_objective_placeholder": "Objectif mineur...",
"instincts_placeholder": "Ex: Triompher, relever un défi...", "instincts_placeholder": "Ex: Triompher, relever un défi...",
"prohibits_placeholder": "Ex: Fuir, abandonner...", "prohibits_placeholder": "Ex: Fuir, abandonner...",
"error_unknown_actor": "Acteur inconnu", "error_unknown_actor": "Acteur inconnu",
@@ -452,6 +620,7 @@
"RollTool": "Outil de lancer de dés", "RollTool": "Outil de lancer de dés",
"roll": "Lancer", "roll": "Lancer",
"cancel": "Annuler", "cancel": "Annuler",
"close": "Fermer",
"Sheet": { "Sheet": {
"character": "Personnage", "character": "Personnage",
"npc": "PNJ", "npc": "PNJ",
@@ -581,6 +750,8 @@
"ability": "Capacité", "ability": "Capacité",
"new_ability": "Nouvelle capacité", "new_ability": "Nouvelle capacité",
"abilities": "Capacités", "abilities": "Capacités",
"creaturecapacity": "Capacité de créature",
"new_creaturecapacity": "Nouvelle capacité de créature",
"specialties": "Spécialités", "specialties": "Spécialités",
"new_specialty": "Nouvelle spécialité", "new_specialty": "Nouvelle spécialité",
"target": "Cible", "target": "Cible",
@@ -661,6 +832,11 @@
"dog": "Chien", "dog": "Chien",
"bear": "Ours" "bear": "Ours"
}, },
"PACK_LEVELS": {
"small": "Petit",
"large": "Grand",
"giant": "Géant"
},
"TOTEMS": { "TOTEMS": {
"human": { "human": {
"name": "L'Humain", "name": "L'Humain",
+171 -3
View File
@@ -21,7 +21,7 @@
input:not([type="radio"]):not([type="checkbox"]), input:not([type="radio"]):not([type="checkbox"]),
select, textarea { select, textarea {
color: @color-text-light-0; color: #333;
line-height: 1.2; line-height: 1.2;
padding: 1px 4px; padding: 1px 4px;
} }
@@ -188,6 +188,42 @@
} }
} }
// ── Stacked layout for creature sheets (header + tabs + content) ───
&.creature .window-content {
display: grid;
grid-template-rows: auto auto 1fr;
grid-template-columns: 1fr;
overflow: hidden;
> .tab.main {
grid-column: 1;
grid-row: 1;
display: flex !important;
flex-direction: column;
overflow-y: auto;
padding: 8px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
> nav.tabs {
grid-column: 1;
grid-row: 2;
}
> .tab:not(.main) {
grid-column: 1;
grid-row: 3;
display: none;
overflow-y: auto;
padding: 8px;
&.active {
display: flex;
flex-direction: column;
}
}
}
form { form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -304,6 +340,61 @@
} }
} }
// Fiche personnage (ApplicationV2 : l'élément fiche EST le <form>, le
// sélecteur "form div.hexa" ci-dessus ne s'y applique donc pas).
&.character div.hexa {
.hexa-style();
.transition();
margin: 0.2rem;
position: relative;
&:hover {
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
}
input {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
z-index: 1;
margin: 0;
cursor: pointer;
}
&.checked {
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
}
}
// Fiche de Groupe : mêmes dés de Réserve cliquables (AppV2).
&.group div.hexa {
.hexa-style();
.transition();
margin: 0.2rem;
position: relative;
&:hover {
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
}
input {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
z-index: 1;
margin: 0;
cursor: pointer;
}
&.checked {
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
}
}
.sheet-header-toggle { .sheet-header-toggle {
text-align: right; text-align: right;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@@ -579,6 +670,62 @@
} }
} }
.fetiche-block {
gap: 12px;
margin: 6px 0;
padding: 6px 8px;
border: 1px dashed @color-hemolymph;
background: fade(@color-hemolymph, 8%);
border-radius: 3px;
.fetiche-info {
gap: 2px;
flex: 1;
h4 {
margin: 0;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
color: @theme-color-dark;
}
.fetiche-bonus {
margin: 0;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @color-acid-green;
}
.fetiche-warning {
margin: 0;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
font-weight: 700;
color: @color-hemolymph;
}
}
button {
flex: none;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-11;
padding: 4px 10px;
cursor: pointer;
color: @theme-color-dark;
background: fade(@color-hemolymph, 20%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-hemolymph;
}
}
}
.items-list { .items-list {
.item-name .item-image { .item-name .item-image {
flex: 0 0 30px; flex: 0 0 30px;
@@ -605,6 +752,27 @@
&:last-child { border-bottom: none; } &:last-child { border-bottom: none; }
} }
.evolution-badge {
margin-left: 6px;
padding: 1px 6px;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
text-transform: uppercase;
border-radius: 3px;
&.adaptation {
color: @color-text-dark-header;
background: fade(@color-acid-green, 25%);
border: 1px solid @color-acid-green;
}
&.mutation {
color: @color-text-dark-header;
background: fade(@color-hemolymph, 30%);
border: 1px solid @color-hemolymph;
}
}
} }
// ── V2 tab navigation — chitin-plate style ───────────────────────── // ── V2 tab navigation — chitin-plate style ─────────────────────────
@@ -615,7 +783,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
background: @color-chitin-dark; background: @color-chitin-dark;
border-bottom: 2px solid @color-amber; border-bottom: 2px solid @color-hemolymph;
min-height: 36px; min-height: 36px;
> a { > a {
@@ -639,7 +807,7 @@
} }
&.active { &.active {
background: linear-gradient(180deg, @color-amber 0%, @color-honeycomb 100%); background: @color-honeycomb;
color: @color-chitin-dark; color: @color-chitin-dark;
&::after { &::after {
+9 -9
View File
@@ -69,14 +69,14 @@
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
} }
.community-size-info { .community-size-info {
margin: 6px 0 0; margin: 6px 0 0;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-text-light-0; color: @color-text-dark-secondary;
opacity: 0.85; opacity: 0.85;
} }
@@ -94,8 +94,8 @@
margin: 0; margin: 0;
font-size: @font-size-11; font-size: @font-size-11;
font-style: italic; font-style: italic;
color: @color-text-light-0; color: @color-text-dark-secondary;
opacity: 0.7; opacity: 0.8;
} }
// Jauges de Domaines // Jauges de Domaines
@@ -111,7 +111,7 @@
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
input[type="number"] { input[type="number"] {
@@ -123,7 +123,7 @@
.gauge-value { .gauge-value {
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
font-size: @font-size-16; font-size: @font-size-16;
color: @color-text-light-0; color: @color-text-dark-header;
} }
.gauge-state { .gauge-state {
@@ -134,7 +134,7 @@
} }
&.crisis { border-left-color: @color-hemolymph; .gauge-state { color: @color-hemolymph; } } &.crisis { border-left-color: @color-hemolymph; .gauge-state { color: @color-hemolymph; } }
&.low { border-left-color: @color-amber; .gauge-state { color: @color-amber; } } &.low { border-left-color: @color-honeycomb; .gauge-state { color: @color-honeycomb; } }
&.stable { border-left-color: @color-acid-green; .gauge-state { color: @color-acid-green; } } &.stable { border-left-color: @color-acid-green; .gauge-state { color: @color-acid-green; } }
&.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } } &.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } }
} }
@@ -145,8 +145,8 @@
font-weight: 700; font-weight: 700;
&.high { color: @color-acid-green; } &.high { color: @color-acid-green; }
&.normal { color: @color-text-light-0; } &.normal { color: @color-text-dark-secondary; }
&.low { color: @color-amber; } &.low { color: @color-honeycomb; }
&.crisis { color: @color-hemolymph; } &.crisis { color: @color-hemolymph; }
} }
} }
+94 -2
View File
@@ -5,7 +5,7 @@
.sheet-header { .sheet-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top); .background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
padding: 10px; padding: 10px;
max-height: 110px; overflow: visible;
} }
.header-fields { flex: 1; } .header-fields { flex: 1; }
@@ -13,6 +13,10 @@
.resource { .resource {
.card-style(); .card-style();
// Le mixin card-style ajoute un margin-bottom (15px) : inutile dans le
// header, on le neutralise pour garder une hauteur compacte.
margin-bottom: 8px;
} }
.resource-label { .resource-label {
@@ -34,13 +38,20 @@
.charname { .charname {
margin: 0; margin: 0;
height: auto;
font-size: @font-size-18;
line-height: 1.2;
span,
input { input {
width: 100%;
font-size: @font-size-18; font-size: @font-size-18;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
} }
input {
width: 100%;
}
} }
.stats-grid { .stats-grid {
@@ -116,4 +127,85 @@
gap: 8px; gap: 8px;
} }
// ── Cases de blessures (hexagones) ──────────────────────────────────
// Le radio est masqué et recouvre l'hexagone : cliquer sur la case la coche.
div.hexa {
.hexa-style();
.transition();
margin: 0.2rem;
position: relative;
&:hover {
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
}
input {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
z-index: 1;
margin: 0;
cursor: pointer;
}
&.checked {
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
}
}
// ── Capacités de créature ───────────────────────────────────────────
.creature-list {
list-style: none;
margin: 0;
padding: 0;
> li {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
&:hover { box-shadow: 0 0 10px @theme-color-highlight inset; }
}
.item-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
gap: 8px;
.item-roll { cursor: pointer; }
}
.capacity-type {
flex: 0 0 auto;
font-size: @font-size-12;
color: @color-text-light-2;
text-transform: uppercase;
}
.item-controls {
flex: 0 0 auto;
display: flex;
gap: 6px;
}
}
h4 {
a.item-control {
color: inherit;
opacity: 0.85;
&:hover { opacity: 1; }
}
}
} }
+101 -46
View File
@@ -15,13 +15,113 @@
} }
} }
// ── Titres de section : uniforme (centré, scotch, majuscules) ───────
h4 { h4 {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-14; font-size: @font-size-14;
text-align: center;
padding: 2px 8px;
margin: 0 0 8px;
background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp"); background: 50% 0% / cover no-repeat url("@{ui-path}/scotch.webp");
a {
color: inherit;
opacity: 0.85;
&:hover { opacity: 1; }
}
} }
// ── Sous-titres (ex. Objectifs majeurs / mineurs) ───────────────────
h5 {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
// Brun foncé lisible sur le fond beige de la fiche (le jaune était illisible).
color: @color-chitin-dark;
margin: 8px 0 4px;
text-align: left;
border-bottom: none;
}
// ── Liste uniforme (objectifs, capacités, membres, rencontres) ──────
.group-list {
list-style: none;
margin: 0;
padding: 0;
> li {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
&:hover { box-shadow: 0 0 10px @theme-color-highlight inset; }
}
> li.group-list-header {
font-weight: bold;
text-transform: uppercase;
font-size: @font-size-12;
border-bottom: 2px solid @color-border-dark-3;
box-shadow: none;
> div { text-align: center; }
}
.item-name, .actor-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-controls {
flex: 0 0 auto;
display: flex;
gap: 6px;
}
input[type="text"] {
flex: 1;
min-width: 0;
}
}
// ── Champs (Origine, Concept, Instincts, Interdits…) ────────────────
.group-field {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
label {
flex: 0 0 auto;
font-weight: bold;
white-space: nowrap;
}
> :last-child {
flex: 1;
min-width: 0;
}
input[type="text"] { width: 100%; }
}
// ── Colonnes des tableaux (Rencontres) ──────────────────────────────
.gcol-name { flex: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gcol-profile { flex: 3; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gcol-totem { flex: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reserve-control, .reserve-control,
.morale-control { .morale-control {
.card-style(); .card-style();
@@ -39,58 +139,13 @@
} }
} }
.actor-list {
list-style: none;
padding: 0;
margin: 0;
li.actor {
padding: 5px;
border-bottom: 1px solid @color-border-dark-3;
&:last-child { border-bottom: none; }
&:hover {
box-shadow: 0 0 10px @theme-color-highlight inset;
}
}
.actor-name, .item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.objective-item {
padding: 5px;
border-bottom: 1px solid @color-border-dark-4;
input {
flex: 1;
width: 100%;
}
}
.totem-display { .totem-display {
.card-style(); .card-style();
flex: 1;
span { font-weight: bold; } span { font-weight: bold; }
} }
.identity-field {
margin-bottom: 8px;
label {
font-weight: bold;
display: block;
margin-bottom: 3px;
}
input { width: 100%; }
}
.level input, .level input,
.reputation input { .reputation input {
width: 60px; width: 60px;
+49 -4
View File
@@ -5,7 +5,25 @@
.sheet-header { .sheet-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top); .background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
padding: 0.5rem; padding: 0.5rem;
max-height: 150px; overflow: visible;
}
.charname {
margin: 0;
height: auto;
font-size: @font-size-18;
line-height: 1.2;
span,
input {
font-size: @font-size-18;
font-weight: bold;
text-align: center;
}
input {
width: 100%;
}
} }
.ability-value, .ability-value,
@@ -53,25 +71,32 @@
} }
.npc-roll-btn { .npc-roll-btn {
color: @color-text-light-primary; color: @color-text-dark-secondary;
opacity: 0.7; opacity: 0.75;
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
&:hover { &:hover {
opacity: 1; opacity: 1;
color: @color-text-light-1; color: @theme-color-dark;
text-shadow: 0 0 6px @theme-color-highlight; text-shadow: 0 0 6px @theme-color-highlight;
} }
} }
.npc-wounds { .npc-wounds {
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px 16px;
margin-bottom: 8px;
.wound-cat { .wound-cat {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
font-weight: bold; font-weight: bold;
.wound-label {
font-size: @font-size-12;
white-space: nowrap;
}
} }
.wound-boxes { .wound-boxes {
@@ -82,6 +107,10 @@
font-size: 8px; font-size: 8px;
color: @color-border-dark-2; color: @color-border-dark-2;
} }
.hexa {
margin: 0;
}
} }
} }
@@ -199,9 +228,25 @@
.hexa { .hexa {
.hexa-style(); .hexa-style();
.transition(); .transition();
position: relative;
&:hover { &:hover {
.hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288)); .hexa-style(rgba(255, 255, 255, 0.425), rgba(0, 0, 0, 0.288));
} }
input {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
z-index: 1;
margin: 0;
cursor: pointer;
}
&.checked {
.hexa-style(rgb(0, 0, 0), rgba(0, 0, 0, 0.288));
&:hover { .hexa-style(rgb(43, 43, 43), rgba(0, 0, 0, 0.288)); }
}
} }
} }
+55
View File
@@ -18,6 +18,29 @@
} }
} }
// Toggle « Chaman » de l'onglet Totem : ne pas étirer sur toute la largeur.
.chaman-toggle {
width: fit-content;
margin: 0.5rem auto;
gap: 0.5rem;
label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
color: @theme-color-dark;
cursor: pointer;
margin: 0;
}
input[type="checkbox"] {
width: 1rem;
height: 1rem;
margin: 0;
cursor: pointer;
}
}
div.minor-totems { div.minor-totems {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -101,5 +124,37 @@
} }
} }
} }
.totem-influence-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin-top: 2px;
font-size: 0.75rem;
.totem-influence-label {
color: @color-text-dark-header;
strong.totem-human { color: @totem-human-color; }
strong.totem-adapted { color: @totem-adapted-color; }
}
.totem-manage-btn {
flex: none;
padding: 2px 8px;
cursor: pointer;
color: @color-text-dark-header;
background: fade(@color-text-light-0, 30%);
border: 1px solid @color-text-dark-secondary;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-hemolymph;
}
}
}
} }
} }
+16 -5
View File
@@ -59,13 +59,17 @@
.sheet-header { .sheet-header {
.background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top); .background-image(url("@{ui-path}/barre_haut.webp"), no-repeat, 100% 100%, right top);
padding: 10px; padding: 10px;
max-height: 130px; overflow: visible;
} }
.header-fields { flex: 1; } .header-fields { flex: 1; }
.resource { .resource {
.card-style(); .card-style();
// Le mixin card-style ajoute un margin-bottom (15px) : inutile dans le
// header, on le neutralise pour garder une hauteur compacte.
margin-bottom: 8px;
} }
.resource-label { .resource-label {
@@ -92,13 +96,20 @@
.charname { .charname {
margin: 0; margin: 0;
height: auto;
font-size: @font-size-18;
line-height: 1.2;
span,
input { input {
width: 100%;
font-size: @font-size-18; font-size: @font-size-18;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
} }
input {
width: 100%;
}
} }
.vehicle-owner { .vehicle-owner {
@@ -113,7 +124,7 @@
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
} }
@@ -141,7 +152,7 @@
.unit { .unit {
font-size: @font-size-11; font-size: @font-size-11;
color: @color-text-light-0; color: @color-text-dark-secondary;
opacity: 0.8; opacity: 0.8;
} }
@@ -160,7 +171,7 @@
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
} }
} }
+300 -52
View File
@@ -12,29 +12,53 @@ body.system-vermine2047 {
color: @theme-color-secondary; color: @theme-color-secondary;
} }
// Override Foundry Core default CSS custom properties // Les checkbox/radio du système ne s'étirent jamais dans les conteneurs
// for better readability on the dark Vermine2047 theme. // flex : ils gardent leur taille hexagonale (1rem / 1.5rem). Les cases de
// blessure en `div.hexa` (position:absolute) ne sont pas affectées.
// AppV2 windows (sheets) use .themed.theme-dark on their content .system-vermine2047 {
.system-vermine2047 .themed.theme-dark { input[type="checkbox"],
--color-text-primary: #888888; input[type="radio"] {
--color-text-emphatic: #999999; flex: 0 0 auto;
--color-text-secondary: #777777; }
--color-text-subtle: #666666;
--color-text-light-highlight: #888888;
--color-text-light-heading: #777777;
--color-text-light-primary: #666666;
} }
// Old-style Application windows (settings-config, dialogs) inherit from body.theme-dark // Override Foundry Core default CSS custom properties
// for better readability on the light Vermine2047 theme.
// Core windows (FilePicker, dialogs, compendium, RollTable, Journal, …) and chat
// messages sit on the light box_background texture: force dark readable text.
// The dark sidebar keeps Foundry's default light-on-dark text colors.
body.system-vermine2047.theme-dark { body.system-vermine2047.theme-dark {
--color-text-primary: #888888; .window-content,
--color-text-emphatic: #999999; .roll-table-sheet,
--color-text-secondary: #777777; .journal-sheet,
--color-text-subtle: #666666; .document-sheet,
--color-text-light-highlight: #888888; .settings-sheet {
--color-text-light-heading: #777777; color: @color-text-dark-primary;
--color-text-light-primary: #666666; --color-text-primary: @color-text-dark-primary;
--color-text-emphatic: @color-text-dark-header;
--color-text-secondary: @color-text-dark-secondary;
--color-text-subtle: #666666;
--color-text-light-highlight: @color-acid-green;
--color-text-light-heading: @color-text-dark-header;
--color-text-light-primary: @color-text-dark-secondary;
--color-text-dark-primary: @color-text-dark-primary;
--color-text-dark-secondary: @color-text-dark-secondary;
--color-text-dark-header: @color-text-dark-header;
--text-color: @color-text-dark-primary;
}
}
#chat-log .message,
.chat-log .message {
.message-content {
color: @color-text-dark-primary;
}
header.message-header {
color: #fff;
text-shadow: 0 0 5px #000;
}
} }
#settings-config button, #settings-config button,
@@ -44,31 +68,13 @@ body.system-vermine2047.theme-dark {
color: #000000; color: #000000;
} }
// Core sheets the system doesn't restyle (RollTable, Journal, …) inherit the
// gray text overrides above, which are unreadable on their dark backgrounds.
// Restore Foundry's default light-on-dark text colors for those apps (dark theme only).
body.system-vermine2047.theme-dark {
.roll-table-sheet,
.journal-sheet,
.document-sheet,
.settings-sheet {
--color-text-primary: var(--color-light-2);
--color-text-emphatic: var(--color-light-1);
--color-text-secondary: var(--color-light-3);
--color-text-subtle: var(--color-light-5);
--color-text-light-highlight: #f0f0e0;
--color-text-light-heading: #c9c7b8;
--color-text-light-primary: #b5b3a4;
}
}
body.system-vermine2047.theme-dark .form-group > label { body.system-vermine2047.theme-dark .form-group > label {
color: #777777; color: @color-text-dark-secondary;
} }
#settings-config p, #settings-config p,
#settings-config .hint { #settings-config .hint {
color: #777777; color: @color-text-dark-secondary;
} }
img { img {
@@ -665,7 +671,11 @@ ol.chat-log {
cursor: pointer; cursor: pointer;
&:hover { transform: translateY(0.5rem); } &:hover { transform: translateY(0.5rem); }
} }
&.success { border-bottom-color: @color-acid-green; } &.success {
border-bottom-color: @color-acid-green;
border-bottom-width: 6px;
box-shadow: 0 0 8px fade(@color-acid-green, 55%);
}
&.adapted::after { content: "adapté"; } &.adapted::after { content: "adapté"; }
&.human::after { content: "humain"; } &.human::after { content: "humain"; }
&.rerolled { transform: translateY(0rem); } &.rerolled { transform: translateY(0rem); }
@@ -728,7 +738,7 @@ ol.chat-log {
} }
#required { #required {
color: @color-amber; color: @color-text-light-2;
} }
} }
@@ -833,6 +843,40 @@ ol.chat-log {
} }
} }
} }
.weapon-traits {
gap: 4px 6px;
margin-top: 4px;
padding-top: 4px;
border-top: 1px dashed fade(@color-hemolymph, 35%);
align-items: center;
.weapon-traits-label {
flex: none;
}
.weapon-trait {
flex: none;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
font-weight: 600;
padding: 1px 6px;
border: 1px solid @color-border-dark-4;
border-radius: 3px;
background: fade(@color-acid-green, 10%);
cursor: help;
}
}
.duree-note {
margin-top: 4px;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
font-style: italic;
color: @theme-color-accent;
i { color: @color-hemolymph; }
}
} }
// ── Échange de coups ───────────────────────────────────────────── // ── Échange de coups ─────────────────────────────────────────────
@@ -856,6 +900,19 @@ ol.chat-log {
i { color: @color-hemolymph; } i { color: @color-hemolymph; }
} }
.exchange-reminder {
display: flex;
align-items: center;
gap: 6px;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
font-style: italic;
color: @theme-color-accent;
padding: 2px 0;
i { color: @color-hemolymph; }
}
.exchange-target { .exchange-target {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -890,7 +947,7 @@ ol.chat-log {
.exchange-passive { .exchange-passive {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
opacity: 0.85; opacity: 0.85;
font-style: italic; font-style: italic;
} }
@@ -903,7 +960,7 @@ ol.chat-log {
font-size: @font-size-11; font-size: @font-size-11;
padding: 3px 12px; padding: 3px 12px;
cursor: pointer; cursor: pointer;
color: @color-text-light-0; color: @theme-color-dark;
background: fade(@color-hemolymph, 25%); background: fade(@color-hemolymph, 25%);
border: 1px solid @color-hemolymph; border: 1px solid @color-hemolymph;
border-radius: 3px; border-radius: 3px;
@@ -914,13 +971,38 @@ ol.chat-log {
background: @color-hemolymph; background: @color-hemolymph;
} }
} }
button.exchange-incapacitant,
button.exchange-zone {
margin-top: 4px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-11;
padding: 3px 10px;
cursor: pointer;
color: @theme-color-dark;
background: fade(@color-acid-green, 20%);
border: 1px solid @color-acid-green;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-acid-green;
}
&:disabled {
opacity: 0.6;
cursor: default;
}
}
} }
div.exchange-resolution { div.exchange-resolution {
margin: 4px 0 6px; margin: 4px 0 6px;
padding: 6px 8px; padding: 6px 8px;
border: 1px solid @color-amber; border: 1px solid @color-hemolymph;
background: fade(@color-amber, 8%); background: fade(@color-hemolymph, 8%);
.resolution-header { .resolution-header {
display: flex; display: flex;
@@ -933,7 +1015,7 @@ ol.chat-log {
font-weight: 900; font-weight: 900;
color: @theme-color-dark; color: @theme-color-dark;
i { color: @color-amber; } i { color: @color-hemolymph; }
} }
.resolution-line { .resolution-line {
@@ -958,7 +1040,7 @@ ol.chat-log {
font-size: @font-size-11; font-size: @font-size-11;
padding: 3px 12px; padding: 3px 12px;
cursor: pointer; cursor: pointer;
color: @color-text-light-0; color: @theme-color-dark;
background: fade(@color-hemolymph, 25%); background: fade(@color-hemolymph, 25%);
border: 1px solid @color-hemolymph; border: 1px solid @color-hemolymph;
border-radius: 3px; border-radius: 3px;
@@ -1059,7 +1141,7 @@ ol.chat-log {
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
.exp-value { .exp-value {
@@ -1080,7 +1162,7 @@ ol.chat-log {
font-size: @font-size-12; font-size: @font-size-12;
padding: 5px 14px; padding: 5px 14px;
cursor: pointer; cursor: pointer;
color: @color-text-light-0; color: @theme-color-dark;
background: fade(@color-hemolymph, 20%); background: fade(@color-hemolymph, 20%);
border: 1px solid @color-hemolymph; border: 1px solid @color-hemolymph;
border-radius: 3px; border-radius: 3px;
@@ -1107,7 +1189,7 @@ ol.chat-log {
margin: 0 0 6px; margin: 0 0 6px;
.exp-total { .exp-total {
color: @color-amber; color: @color-text-light-2;
} }
} }
@@ -1122,7 +1204,7 @@ ol.chat-log {
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
} }
@@ -1147,3 +1229,169 @@ ol.chat-log {
} }
} }
} }
// ── Test de Fiabilité / Réparations ──────────────────────────────────
.vermine-reliability-message {
padding: 4px 10px 10px;
.reliability-header {
align-items: center;
gap: 8px;
img {
flex: 0 0 auto;
width: 36px;
height: 36px;
object-fit: cover;
border-radius: 3px;
border: 1px solid @color-border-dark-4;
}
h3 {
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-13;
margin: 0;
border-bottom: none;
color: @theme-color-dark;
}
}
.reliability-meta {
gap: 4px 12px;
margin: 4px 0 6px;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.06);
border-radius: 3px;
.reliability-meta-item {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @color-text-dark-primary;
strong {
text-transform: uppercase;
}
}
}
.reliability-results {
gap: 6px;
margin: 6px 0;
.reliability-die {
flex: none;
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
font-family: "DistressBlack";
font-size: @font-size-16;
border: 1px solid @color-border-dark-4;
border-radius: 50%;
background: rgba(0, 0, 0, 0.08);
color: @color-text-dark-primary;
&.damage {
border-color: @color-hemolymph;
background: fade(@color-hemolymph, 15%);
color: @color-hemolymph;
font-weight: 900;
}
&.success {
border-color: @color-acid-green;
background: fade(@color-acid-green, 15%);
color: @color-acid-green;
font-weight: 900;
}
}
}
.reliability-summary {
gap: 4px 16px;
margin: 4px 0;
font-family: "Roboto", sans-serif;
font-size: @font-size-12;
color: @color-text-dark-primary;
strong {
text-transform: uppercase;
}
}
.reliability-note {
margin: 2px 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-dark-secondary;
}
.reliability-destroyed {
margin: 6px 0 0;
padding: 4px 8px;
font-family: "DistressBlack";
text-transform: uppercase;
font-size: @font-size-12;
text-align: center;
color: @color-hemolymph;
border: 1px solid @color-hemolymph;
border-radius: 3px;
background: fade(@color-hemolymph, 12%);
}
}
.item .reliability-actions {
gap: 6px;
justify-content: center;
margin: 4px 0 8px;
button {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @theme-color-dark;
background: fade(@color-hemolymph, 15%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-hemolymph;
}
i {
margin-right: 4px;
}
}
}
.reliability-option {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
font-size: @font-size-12;
color: @color-text-dark-primary;
input[type="checkbox"],
input[type="radio"] {
flex: none;
}
}
.reliability-severities,
.reliability-modes {
gap: 8px;
}
.reliability-hint {
margin: 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-dark-secondary;
}
+45
View File
@@ -0,0 +1,45 @@
/* ============================================
Combat & Combat Tracker (Vermine2047)
Styles custom par-dessus le CSS core Foundry v14
============================================ */
// ── Combat tracker sidebar ──────────────────────────────────────────
.combat-sidebar {
// Nom des combatants coloré : PJ (vert acide) / PNJ (hémolymphe)
.combat-tracker .name.player {
color: @color-acid-green;
}
.combat-tracker .name.npc {
color: @color-hemolymph;
}
// Boutons d'attitude (offensif / actif / passif)
.combatant-attitudes {
display: flex;
align-items: center;
gap: var(--element-spacing-half);
.status {
color: var(--color-text-subtle);
}
.status.active {
color: var(--color-warm-2);
}
}
}
// ── Footer des dialogues de jet (combat & action) ───────────────────
// Pin le footer en bas de la fenêtre : visible même si le contenu dépasse.
.application.vermine-roll {
.roll-dialog-content .sheet-footer {
position: sticky;
bottom: 0;
margin-top: auto;
padding-top: 8px;
background: @theme-color-dark;
border-top: 1px solid fade(@color-hemolymph, 50%);
}
}
+148 -31
View File
@@ -148,7 +148,7 @@ iframe {
padding: 10px; padding: 10px;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
color: @color-text-light-0; color: @theme-color-dark;
.dice-pool { .dice-pool {
flex: 1; flex: 1;
@@ -188,7 +188,7 @@ iframe {
> label { > label {
font-size: @font-size-13; font-size: @font-size-13;
color: @color-amber; color: @color-text-light-2;
white-space: nowrap; white-space: nowrap;
} }
@@ -201,7 +201,7 @@ iframe {
.ability-score { .ability-score {
gap: 4px; gap: 4px;
font-size: @font-size-12; font-size: @font-size-12;
color: @color-text-light-0; color: @color-text-dark-secondary;
opacity: 0.85; opacity: 0.85;
#abilityScoreValue { #abilityScoreValue {
@@ -273,7 +273,7 @@ iframe {
label { label {
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-chitin-dark;
} }
} }
} }
@@ -290,11 +290,11 @@ iframe {
font-size: @font-size-12; font-size: @font-size-12;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
text-transform: none; text-transform: none;
color: @color-text-light-0; color: @color-text-dark-primary;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: @color-amber; color: @color-acid-green;
} }
} }
@@ -317,7 +317,7 @@ iframe {
> label.label { > label.label {
font-size: @font-size-11; font-size: @font-size-11;
color: @color-text-light-0; color: @color-text-dark-secondary;
white-space: nowrap; white-space: nowrap;
} }
@@ -331,7 +331,7 @@ iframe {
font-size: @font-size-12; font-size: @font-size-12;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
text-transform: none; text-transform: none;
color: @color-text-light-0; color: @color-text-dark-primary;
} }
.item-list { .item-list {
@@ -343,13 +343,31 @@ iframe {
font-size: @font-size-11; font-size: @font-size-11;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
text-transform: none; text-transform: none;
color: @color-text-light-0; color: @color-text-dark-primary;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: @color-amber; color: @color-acid-green;
} }
} }
.tool-bonus,
.tool-ponctuel {
font-style: normal;
font-weight: 700;
padding: 0 4px;
border-radius: 3px;
}
.tool-bonus {
color: @color-acid-green;
background: fade(@color-acid-green, 12%);
}
.tool-ponctuel {
color: @theme-color-dark;
background: fade(@color-hemolymph, 18%);
}
} }
&.full-width { &.full-width {
@@ -374,14 +392,14 @@ iframe {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
color: @color-text-light-0; color: @color-text-dark-primary;
font-size: @font-size-12; font-size: @font-size-12;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
text-transform: none; text-transform: none;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: @color-amber; color: @color-acid-green;
} }
small { small {
@@ -392,8 +410,8 @@ iframe {
.totem-hint { .totem-hint {
width: 100%; width: 100%;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-text-light-0; color: @color-text-dark-secondary;
opacity: 0.6; opacity: 0.8;
font-style: italic; font-style: italic;
} }
} }
@@ -408,6 +426,16 @@ iframe {
text-transform: uppercase; text-transform: uppercase;
} }
.traits-reminder {
margin: 4px 0 0;
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
font-style: italic;
color: @theme-color-accent;
i { color: @color-hemolymph; }
}
.locked-value { .locked-value {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -448,7 +476,7 @@ iframe {
.weapon-detail { .weapon-detail {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-text-light-0; color: @color-text-dark-primary;
strong { text-transform: uppercase; } strong { text-transform: uppercase; }
} }
@@ -466,7 +494,7 @@ iframe {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
text-transform: none; text-transform: none;
font-size: @font-size-12; font-size: @font-size-12;
color: @color-text-light-0; color: @color-text-dark-primary;
cursor: pointer; cursor: pointer;
input[type="radio"] { flex: none; } input[type="radio"] { flex: none; }
@@ -487,14 +515,14 @@ iframe {
.label { .label {
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
.combat-target-tag { .combat-target-tag {
padding: 2px 8px; padding: 2px 8px;
font-size: @font-size-11; font-size: @font-size-11;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
color: @color-text-light-0; color: @color-text-dark-header;
background: fade(@color-hemolymph, 20%); background: fade(@color-hemolymph, 20%);
border: 1px solid fade(@color-hemolymph, 50%); border: 1px solid fade(@color-hemolymph, 50%);
border-radius: 3px; border-radius: 3px;
@@ -527,7 +555,7 @@ iframe {
.label { .label {
font-size: @font-size-12; font-size: @font-size-12;
color: @color-amber; color: @color-text-light-2;
} }
.exp-value { .exp-value {
@@ -550,12 +578,12 @@ iframe {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
text-transform: none; text-transform: none;
font-size: @font-size-12; font-size: @font-size-12;
color: @color-text-light-0; color: @color-text-dark-primary;
cursor: pointer; cursor: pointer;
input[type="radio"] { flex: none; } input[type="radio"] { flex: none; }
&:hover { color: @color-amber; } &:hover { color: @color-acid-green; }
} }
} }
@@ -571,8 +599,8 @@ iframe {
margin: 0; margin: 0;
font-size: @font-size-11; font-size: @font-size-11;
font-style: italic; font-style: italic;
color: @color-text-light-0; color: @color-text-dark-secondary;
opacity: 0.75; opacity: 0.8;
} }
} }
@@ -581,8 +609,8 @@ iframe {
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px 16px; gap: 8px 16px;
padding: 8px; padding: 8px;
border: 1px solid @color-amber; border: 1px solid @color-hemolymph;
background: fade(@color-amber, 8%); background: fade(@color-hemolymph, 8%);
.context-item { .context-item {
display: flex; display: flex;
@@ -593,14 +621,14 @@ iframe {
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: @font-size-11; font-size: @font-size-11;
color: @color-amber; color: @color-text-light-2;
} }
.context-value { .context-value {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-size: @font-size-13; font-size: @font-size-13;
font-weight: 700; font-weight: 700;
color: @color-text-light-0; color: @color-text-dark-primary;
} }
} }
} }
@@ -613,7 +641,7 @@ iframe {
.experience-group { .experience-group {
font-size: @font-size-11; font-size: @font-size-11;
color: @color-text-light-0; color: @color-text-dark-secondary;
opacity: 0.8; opacity: 0.8;
#exp-group { #exp-group {
@@ -628,7 +656,7 @@ iframe {
font-size: @font-size-11; font-size: @font-size-11;
padding: 3px 12px; padding: 3px 12px;
cursor: pointer; cursor: pointer;
color: @color-text-light-0; color: @theme-color-dark;
background: fade(@color-acid-green, 15%); background: fade(@color-acid-green, 15%);
border: 1px solid @color-acid-green; border: 1px solid @color-acid-green;
border-radius: 3px; border-radius: 3px;
@@ -666,8 +694,8 @@ iframe {
.total-value { .total-value {
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
font-size: @font-size-28; font-size: @font-size-28;
color: @color-acid-green; color: @color-text-light-0;
text-shadow: 0 0 8px fade(@color-acid-green, 40%); text-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
} }
.totem-selector { .totem-selector {
@@ -721,3 +749,92 @@ iframe {
} }
} }
} }
// ── Achat d'Adaptation/Mutation ───────────────────────────────────────
.application.buy-evolution .bonus-item {
align-items: center;
gap: 8px;
> label.label {
flex: none;
font-size: @font-size-12;
color: @color-text-light-2;
white-space: nowrap;
}
input[type="text"] {
flex: 1 1 auto;
min-width: 0;
}
select {
flex: 1 1 auto;
min-width: 0;
width: auto;
}
input[type="checkbox"] {
flex: none;
width: 18px;
height: 18px;
}
}
// ── Gestion des Dés de Totems ─────────────────────────────────────────
.application.totem-dice .totem-dice-state {
gap: 12px;
padding: 6px 8px;
border: 1px solid @color-hemolymph;
background: fade(@color-hemolymph, 8%);
.totem-state {
display: flex;
align-items: center;
gap: 6px;
.label {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
color: @color-text-light-2;
white-space: nowrap;
}
&.totem-human .label { color: @color-text-light-2; }
&.totem-adapted .label { color: @color-text-light-2; }
.value {
font-family: "Roboto", sans-serif;
font-size: @font-size-14;
font-weight: 700;
color: @color-text-dark-header;
}
}
}
.application.totem-dice .totem-influence {
font-size: @font-size-16;
}
.application.totem-dice .totem-actions {
gap: 8px;
button {
flex: 1 1 100%;
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-12;
padding: 6px 10px;
cursor: pointer;
color: @theme-color-dark;
background: fade(@color-hemolymph, 15%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
.transition();
&:hover {
color: @theme-color-dark;
background: @color-hemolymph;
}
}
}
+12 -2
View File
@@ -66,11 +66,10 @@
select { select {
.custom-select-style(); .custom-select-style();
color: @color-text-light-1; color: @color-text-dark-primary;
font-family: "DistressBlack", sans-serif; font-family: "DistressBlack", sans-serif;
font-size: 0.875rem; font-size: 0.875rem;
text-align: center; text-align: center;
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9);
option { option {
background: @color-bg-option; background: @color-bg-option;
@@ -98,3 +97,14 @@ ol#chat-log div.item-card header img,
ol.chat-log div.item-card header img { ol.chat-log div.item-card header img {
max-width: 30%; max-width: 30%;
} }
// Icônes des cartes d'item envoyées au chat (ApplicationV2).
// Le template des cartes (templates/item/chatCards/parts/base.hbs) rend
// une <img> pleine taille : on la plafonne à 64×64.
ol#chat-log .chat-card-img,
ol.chat-log .chat-card-img {
max-width: 64px;
max-height: 64px;
object-fit: contain;
flex: 0 0 auto;
}
+1
View File
@@ -204,6 +204,7 @@
cursor: pointer; cursor: pointer;
width: 1.5rem; width: 1.5rem;
height: 1rem; height: 1rem;
flex: 0 0 auto;
border-radius: 0.4rem; border-radius: 0.4rem;
.transition(); .transition();
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
+2 -3
View File
@@ -20,7 +20,7 @@
// Niveaux de gris pour le texte clair // Niveaux de gris pour le texte clair
@color-text-light-0: #fff; @color-text-light-0: #fff;
@color-text-light-1: #e0f0f0; @color-text-light-1: #e0f0f0;
@color-text-light-2: #c9e0c0; @color-text-light-2: #3f9b55;
@color-text-light-3: #90c4a4; @color-text-light-3: #90c4a4;
@color-text-light-4: #80c08b; @color-text-light-4: #80c08b;
@color-text-light-5: #60b06b; @color-text-light-5: #60b06b;
@@ -138,8 +138,7 @@
@theme-color-highlight: #005a3c; @theme-color-highlight: #005a3c;
// Couleurs insecte / organique // Couleurs insecte / organique
@color-acid-green: #6ab04c; @color-acid-green: #1e7d32;
@color-amber: #e8b84b;
@color-chitin-dark: #2a2520; @color-chitin-dark: #2a2520;
@color-hemolymph: #8b4513; @color-hemolymph: #8b4513;
@color-membrane: rgba(106, 176, 76, 0.1); @color-membrane: rgba(106, 176, 76, 0.1);
+3
View File
@@ -31,6 +31,9 @@
// 7. Styles des dialogs // 7. Styles des dialogs
@import "dialogs"; @import "dialogs";
// 8. Styles du combat et du combat tracker
@import "combat";
// ============================================ // ============================================
// Notes: // Notes:
// - L'ordre des imports est important // - L'ordre des imports est important
+1
View File
@@ -11,6 +11,7 @@ export {
VermineWeaponSheetV2, VermineWeaponSheetV2,
VermineDefenseSheetV2, VermineDefenseSheetV2,
VermineAbilitySheetV2, VermineAbilitySheetV2,
VermineCreatureCapacitySheetV2,
VermineSpecialtySheetV2, VermineSpecialtySheetV2,
VermineBackgroundSheetV2, VermineBackgroundSheetV2,
VermineTraumaSheetV2, VermineTraumaSheetV2,
@@ -38,6 +38,7 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
roll: VermineBaseActorSheet.#onRollItem, roll: VermineBaseActorSheet.#onRollItem,
attack: VermineBaseActorSheet.#onAttack, attack: VermineBaseActorSheet.#onAttack,
clickRadio: VermineBaseActorSheet.#onClickRadioHexa, clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
clickWound: VermineBaseActorSheet.#onClickWound,
effectControl: VermineBaseActorSheet.#onEffectControl, effectControl: VermineBaseActorSheet.#onEffectControl,
chooseTotem: VermineBaseActorSheet.#onChooseTotem chooseTotem: VermineBaseActorSheet.#onChooseTotem
} }
@@ -264,9 +265,10 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
if (!id) return if (!id) return
const item = this.document.items.get(id) const item = this.document.items.get(id)
if (!item) return if (!item) return
// Armes : PNJ → dialogue de combat (valeur d'Attaque) ; personnage → // Armes : PNJ et créatures → dialogue de combat (valeur d'Attaque) ;
// dialogue de compétence. Les PNJ n'ont ni caractéristiques ni compétences. // personnage → dialogue de compétence. PNJ/créatures n'ont ni
if (item.type === "weapon" && this.document.type === 'npc') { // caractéristiques ni compétences.
if (item.type === "weapon" && (this.document.type === 'npc' || this.document.type === 'creature')) {
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs") const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
const dialog = await CombatDialog.create({ actorId: this.document.id, weapon: item }) const dialog = await CombatDialog.create({ actorId: this.document.id, weapon: item })
if (dialog) dialog.render(true) if (dialog) dialog.render(true)
@@ -297,6 +299,8 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
} }
static async #onToggleEquip(event, target) { static async #onToggleEquip(event, target) {
event.preventDefault()
event.stopPropagation()
const id = target.closest("[data-item-id]")?.dataset?.itemId const id = target.closest("[data-item-id]")?.dataset?.itemId
if (!id) return if (!id) return
const item = this.document.items.get(id) const item = this.document.items.get(id)
@@ -307,6 +311,11 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
static #onClickRadioHexa(event, target) { static #onClickRadioHexa(event, target) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
// Évite une erreur de permission pour un utilisateur non éditeur.
if (!this.isEditable) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_editable"))
return
}
const input = target const input = target
const update = {} const update = {}
let current = this.document let current = this.document
@@ -322,6 +331,36 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
this.document.update(update) this.document.update(update)
} }
/**
* Coche/décoche les cases de Blessure en respectant l'ordre de gravité
* (règles p.39) : on ne peut cocher que la case suivante dans l'ordre
* (une blessure s'applique toujours au Seuil égal ou inférieur le plus
* élevé), et on ne peut pas en sauter. Décocher une case retire toutes
* celles situées après elle.
*/
static #onClickWound(event, target) {
event.preventDefault()
event.stopPropagation()
const input = target
const box = parseInt(input.value, 10) || 0 // numéro de case (1-based)
let current = this.document
const propTree = input.name.split(".")
for (const prop of propTree) {
current = current[prop]
}
const currentValue = parseInt(current, 10) || 0
let next = currentValue
if (box === currentValue + 1) {
// Cocher la case suivante dans l'ordre.
next = box
} else if (box <= currentValue) {
// Décocher : retire la case cliquée et toutes celles après.
next = box - 1
}
// Si box > currentValue + 1 : on ne permet pas de sauter des cases.
this.document.update({ [input.name]: next })
}
static #onEffectControl(event, target) { static #onEffectControl(event, target) {
onManageActiveEffect(event, this.document) onManageActiveEffect(event, this.document)
} }
+15 -1
View File
@@ -25,7 +25,9 @@ export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.app
editImage: VermineBaseItemSheet.#onEditImage, editImage: VermineBaseItemSheet.#onEditImage,
toggleSheet: VermineBaseItemSheet.#onToggleSheet, toggleSheet: VermineBaseItemSheet.#onToggleSheet,
clickDamage: VermineBaseItemSheet.#onClickDamage, clickDamage: VermineBaseItemSheet.#onClickDamage,
openTraits: VermineBaseItemSheet.#onOpenTraits openTraits: VermineBaseItemSheet.#onOpenTraits,
reliabilityTest: VermineBaseItemSheet.#onReliabilityTest,
reliabilityRepair: VermineBaseItemSheet.#onReliabilityRepair
} }
} }
@@ -120,4 +122,16 @@ export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.app
const { TraitSelector } = await import("../../system/applications.mjs") const { TraitSelector } = await import("../../system/applications.mjs")
new TraitSelector(this.document).render(true) new TraitSelector(this.document).render(true)
} }
static async #onReliabilityTest() {
const { ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
const dialog = await ReliabilityDialog.create({ item: this.document, mode: "test" })
await dialog.render(true)
}
static async #onReliabilityRepair() {
const { ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
const dialog = await ReliabilityDialog.create({ item: this.document, mode: "repair" })
await dialog.render(true)
}
} }
+68 -3
View File
@@ -4,12 +4,16 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ["character"], classes: ["character"],
position: { width: 860, height: 720 }, position: { width: 920, height: 720 },
window: { contentClasses: ["character-content"] }, window: { contentClasses: ["character-content"] },
actions: { actions: {
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty, addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase, openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
openLearning: VermineCharacterSheetV2.#onOpenLearning openLearning: VermineCharacterSheetV2.#onOpenLearning,
validatePhase: VermineCharacterSheetV2.#onValidatePhase,
buyEvolution: VermineCharacterSheetV2.#onBuyEvolution,
openTotemDice: VermineCharacterSheetV2.#onOpenTotemDice,
loseFetiche: VermineCharacterSheetV2.#onLoseFetiche
} }
} }
@@ -65,6 +69,7 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
context.backgrounds = doc.itemTypes.background context.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma context.traumas = doc.itemTypes.trauma
context.evolutions = doc.itemTypes.evolution context.evolutions = doc.itemTypes.evolution
context.rites = doc.itemTypes.rite
break break
case "equipment": case "equipment":
context.tab = context.tabs.equipment context.tab = context.tabs.equipment
@@ -76,13 +81,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
case "stories": case "stories":
context.tab = context.tabs.stories context.tab = context.tabs.stories
break break
case "combat": case "combat": {
context.tab = context.tabs.combat context.tab = context.tabs.combat
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped) context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped) context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs") const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
context.effects = prepareActiveEffectCategories(doc.effects) context.effects = prepareActiveEffectCategories(doc.effects)
const { VermineTraits } = await import("../../system/traits.mjs")
const feticheItems = VermineTraits.equippedItems(doc, 'fetiche')
context.feticheItems = feticheItems
context.fetiche = feticheItems[0] ?? null
context.feticheMultiple = feticheItems.length > 1
const loss = doc.system.experience?.feticheLoss
context.feticheLost = Boolean(loss?.self_control || loss?.effort)
break break
}
case "experience": case "experience":
context.tab = context.tabs.experience context.tab = context.tabs.experience
context.specialtyCount = doc.itemTypes.specialty.length context.specialtyCount = doc.itemTypes.specialty.length
@@ -124,4 +137,56 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
const dialog = await LearningDialog.create({ actorId: this.document.id }) const dialog = await LearningDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true) if (dialog) dialog.render(true)
} }
/** Valide la phase d'Expérience courante et réinitialise les limites. */
static async #onValidatePhase() {
const { VermineExperience } = await import("../../system/experience.mjs")
await VermineExperience.resetPhaseLimits(this.document)
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
}
/** Ouvre le dialogue d'achat d'une Adaptation/Mutation (Dés d'Évolution). */
static async #onBuyEvolution(event, target) {
const { default: BuyEvolutionDialog } = await import("../../system/dialogs/buyEvolutionDialog.mjs")
const dialog = await BuyEvolutionDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
/** Ouvre le dialogue de gestion des Dés de Totems. */
static async #onOpenTotemDice(event, target) {
const { default: TotemDiceDialog } = await import("../../system/dialogs/totemDiceDialog.mjs")
const dialog = await TotemDiceDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
/**
* Action « Perdre le fétiche » : applique -2D dans chaque Réserve et
* -1D de max persistant (expérience.feticheLoss), puis retire le Trait
* Fétiche de l'objet équipé.
*/
static async #onLoseFetiche() {
const doc = this.document
if (!game.user.isGM && !doc.isOwner) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"))
return
}
const { VermineTraits } = await import("../../system/traits.mjs")
const items = VermineTraits.equippedItems(doc, 'fetiche')
if (!items.length) {
ui.notifications.warn(game.i18n.localize("VERMINE.fetiche_none"))
return
}
const loss = doc.system.experience?.feticheLoss
const sc = Math.max(0, (doc.system.attributes.self_control.value || 0) - 2)
const ef = Math.max(0, (doc.system.attributes.effort.value || 0) - 2)
await doc.update({
"system.attributes.self_control.value": sc,
"system.attributes.effort.value": ef,
"system.experience.feticheLoss.self_control": (loss?.self_control || 0) + 1,
"system.experience.feticheLoss.effort": (loss?.effort || 0) + 1
})
await items[0].update({ "system.traits.fetiche": null })
ui.notifications.info(game.i18n.localize("VERMINE.fetiche_lost"))
this.render()
}
} }
+17 -2
View File
@@ -59,16 +59,25 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
context.roleOptions = CONFIG.VERMINE.creatureRoleLevels context.roleOptions = CONFIG.VERMINE.creatureRoleLevels
context.sizeOptions = CONFIG.VERMINE.creatureSizeLevels context.sizeOptions = CONFIG.VERMINE.creatureSizeLevels
context.packOptions = CONFIG.VERMINE.creaturePackLevels context.packOptions = CONFIG.VERMINE.creaturePackLevels
context.sizeLabel = doc.system.size?.value !== undefined
? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "")
: ""
context.packLabel = doc.system.pack?.value
? game.i18n.localize(CONFIG.VERMINE.creaturePackLevels[doc.system.pack.value]?.label ?? "")
: ""
break break
case "info": case "info":
context.tab = context.tabs.info context.tab = context.tabs.info
context.capacities = doc.itemTypes.creaturecapacity
break break
case "stats": case "stats":
context.tab = context.tabs.stats context.tab = context.tabs.stats
context.patternLabel = doc.system.pattern?.value ? game.i18n.localize(CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label ?? "") : "" context.patternLabel = doc.system.pattern?.value ? game.i18n.localize(CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label ?? "") : ""
context.sizeLabel = doc.system.size?.value !== undefined ? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "") : "" context.sizeLabel = doc.system.size?.value !== undefined ? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "") : ""
context.roleLabel = doc.system.role?.value ? game.i18n.localize(CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label ?? "") : "" context.roleLabel = doc.system.role?.value ? game.i18n.localize(CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label ?? "") : ""
context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none") context.packLabel = doc.system.pack?.value
? `${doc.system.pack.value} (${game.i18n.localize(CONFIG.VERMINE.creaturePackLevels[doc.system.pack.value]?.label ?? "")})`
: game.i18n.localize("VERMINE.none")
// Contribution cumulée de la Taille (les modificateurs se cumulent : Taille N = somme 1..N) // Contribution cumulée de la Taille (les modificateurs se cumulent : Taille N = somme 1..N)
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {} const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
const sizeLevel = doc.system.size?.value ?? 0 const sizeLevel = doc.system.size?.value ?? 0
@@ -97,11 +106,17 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
return context return context
} }
/** @override - Les créatures n'ont pas de caractéristiques/compétences : on ouvre le dialogue d'attaque (pool calculé). */ /** @override - Les créatures n'ont pas de caractéristiques/compétences : on ouvre le dialogue d'attaque (pool calculé) ou de Réaction. */
async _onRoll(event) { async _onRoll(event) {
event.preventDefault() event.preventDefault()
const el = event.currentTarget const el = event.currentTarget
const type = el.dataset.type const type = el.dataset.type
if (type === "creature-reaction") {
const { default: NpcRollDialog } = await import("../../system/dialogs/npcRollDialog.mjs")
const dialog = await NpcRollDialog.create({ actorId: this.document.id, rolltype: "reaction" })
if (dialog) dialog.render(true)
return
}
if (type !== "creature-attack") return super._onRoll(event) if (type !== "creature-attack") return super._onRoll(event)
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs") const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
const dialog = await CombatDialog.create({ actorId: this.document.id }) const dialog = await CombatDialog.create({ actorId: this.document.id })
+84 -8
View File
@@ -7,12 +7,16 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
position: { width: 700, height: 600 }, position: { width: 700, height: 600 },
window: { contentClasses: ["group-content"] }, window: { contentClasses: ["group-content"] },
actions: { actions: {
chooseTotem: VermineGroupSheetV2.#onChooseTotem,
chooseActor: VermineGroupSheetV2.#onChooseActor, chooseActor: VermineGroupSheetV2.#onChooseActor,
deleteMember: VermineGroupSheetV2.#onDeleteMember, deleteMember: VermineGroupSheetV2.#onDeleteMember,
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter, deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
deleteObjective: VermineGroupSheetV2.#onDeleteObjective, deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
addObjective: VermineGroupSheetV2.#onAddObjective, addObjective: VermineGroupSheetV2.#onAddObjective,
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility,
openLearning: VermineGroupSheetV2.#onOpenLearning,
openExperiencePhase: VermineGroupSheetV2.#onOpenExperiencePhase,
validatePhase: VermineGroupSheetV2.#onValidatePhase
} }
} }
@@ -56,9 +60,22 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
if (this.document.system.encounters?.length > 0) { if (this.document.system.encounters?.length > 0) {
for (const encId of this.document.system.encounters) { for (const encId of this.document.system.encounters) {
const a = game.actors.get(encId) const a = game.actors.get(encId)
if (a) context.resolvedEncounters[encId] = { name: a.name, id: a.id } if (a) {
context.resolvedEncounters[encId] = {
name: a.name,
id: a.id,
type: a.type,
profile: a.system?.identity?.profile || "",
totem: a.system?.identity?.totem || ""
}
}
} }
} }
// Notes de voyage (riche texte)
const roadNotes = this.document.system?.roadNotes
context.enrichedRoadNotes = roadNotes
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(roadNotes, { async: true })
: ""
return context return context
} }
@@ -70,10 +87,6 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
context.tab = context.tabs.info context.tab = context.tabs.info
context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem") context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem") context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
context.specialties = doc.itemTypes.specialty
context.backgrounds = doc.itemTypes.background
context.traumas = doc.itemTypes.trauma
context.evolutions = doc.itemTypes.evolution
break break
case "gear": case "gear":
context.tab = context.tabs.gear context.tab = context.tabs.gear
@@ -84,7 +97,6 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
break break
case "road": case "road":
context.tab = context.tabs.road context.tab = context.tabs.road
context.vehicles = this.#getOwnedVehicles()
break break
case "reserve": case "reserve":
context.tab = context.tabs.reserve context.tab = context.tabs.reserve
@@ -98,10 +110,55 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
return game.actors.filter(a => a.type === "vehicle" && a.system.ownerId === this.document.id) return game.actors.filter(a => a.type === "vehicle" && a.system.ownerId === this.document.id)
} }
/**
* Dépôt d'un acteur Véhicule sur la fiche : lie le véhicule au Groupe
* (system.ownerId) au lieu de le dupliquer, puis affiche la liste.
* Gère les dépôts depuis un compendium (copie importée dans le monde)
* et demande confirmation si le véhicule appartient déjà à un autre Groupe.
* @override
*/
async _onDropActor(event, data) {
let actor = data instanceof foundry.abstract.Document ? data : await fromUuid(data.uuid)
if (actor?.type !== "vehicle") {
return super._onDropActor?.(event, data)
}
// Véhicule issu d'un compendium : importer une copie dans le monde.
if (actor.compendium) {
try {
const copy = await game.actors.importFromCompendium(actor.compendium, actor._id, { renderSheet: false })
if (!copy) return
actor = copy
} catch {
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_import_failed"))
return
}
}
// Véhicule déjà rattaché à un autre Groupe : confirmation avant transfert.
const currentOwner = actor.system?.ownerId
if (currentOwner && currentOwner !== this.document.id) {
const other = game.actors.get(currentOwner)
const content = game.i18n.localize("VERMINE.vehicle_assign_confirm").replace("{group}", other?.name ?? currentOwner)
const confirmed = await Dialog.confirm({
title: game.i18n.localize("VERMINE.vehicle_assign"),
content,
defaultYes: false
})
if (!confirmed) return
}
try {
await actor.update({ "system.ownerId": this.document.id })
} catch {
ui.notifications.warn(game.i18n.localize("VERMINE.vehicle_assign_failed"))
return
}
this.render()
}
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker // Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
static async #onChooseTotem(event, target) { static async #onChooseTotem(event, target) {
const { TotemPicker } = await import("../../system/applications.mjs") const { TotemPicker } = await import("../../system/applications.mjs")
new TotemPicker(target, this.document).render(true) // Le Totem du Groupe remplit automatiquement Instincts/Interdits.
new TotemPicker(target, this.document, { fillInstincts: true }).render(true)
} }
static async #onChooseActor(event, target) { static async #onChooseActor(event, target) {
const { ActorPicker } = await import("../../system/applications.mjs") const { ActorPicker } = await import("../../system/applications.mjs")
@@ -148,4 +205,23 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
const name = game.i18n.localize("ITEMS.new_ability") const name = game.i18n.localize("ITEMS.new_ability")
await this.document.createEmbeddedDocuments("Item", [{ name, type: "ability", system: { type: "totem" } }]) await this.document.createEmbeddedDocuments("Item", [{ name, type: "ability", system: { type: "totem" } }])
} }
static async #onOpenExperiencePhase(event, target) {
const { default: ExperiencePhaseDialog } = await import("../../system/dialogs/experiencePhaseDialog.mjs")
const dialog = await ExperiencePhaseDialog.create()
if (dialog) dialog.render(true)
}
static async #onOpenLearning(event, target) {
const { default: GroupLearningDialog } = await import("../../system/dialogs/groupLearningDialog.mjs")
const dialog = await GroupLearningDialog.create({ actorId: this.document.id })
if (dialog) dialog.render(true)
}
/** Valide la phase d'Expérience courante du Groupe et réinitialise les limites. */
static async #onValidatePhase() {
const { VermineExperience } = await import("../../system/experience.mjs")
await VermineExperience.resetPhaseLimits(this.document)
ui.notifications.info(game.i18n.localize("VERMINE.validate_phase_done"))
}
} }
@@ -24,6 +24,12 @@ export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs", scrollable: [""] } } static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs", scrollable: [""] } }
} }
// ── Capacité de créature ────────────────────────────────────────────────
export class VermineCreatureCapacitySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["creaturecapacity"], position: { width: 560 } }
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-creaturecapacity-sheet.hbs", scrollable: [""] } }
}
// ── Spécialité ──────────────────────────────────────────────────────── // ── Spécialité ────────────────────────────────────────────────────────
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet { export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } } static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
-1
View File
@@ -52,7 +52,6 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
context.npcExperienceOptions = CONFIG.VERMINE.npcExperienceLevels context.npcExperienceOptions = CONFIG.VERMINE.npcExperienceLevels
context.npcRoleOptions = CONFIG.VERMINE.npcRoleLevels context.npcRoleOptions = CONFIG.VERMINE.npcRoleLevels
context.totemOptions = CONFIG.VERMINE.totems context.totemOptions = CONFIG.VERMINE.totems
context.originOptions = CONFIG.VERMINE.origins
break break
case "traits": case "traits":
context.tab = context.tabs.traits context.tab = context.tabs.traits
+1
View File
@@ -8,6 +8,7 @@ export { default as VermineWeaponData } from "./weapon.mjs"
export { default as VermineDefenseData } from "./defense.mjs" export { default as VermineDefenseData } from "./defense.mjs"
export { default as VermineVehicleData } from "./vehicle.mjs" export { default as VermineVehicleData } from "./vehicle.mjs"
export { default as VermineAbilityData } from "./ability.mjs" export { default as VermineAbilityData } from "./ability.mjs"
export { default as VermineCreatureCapacityData } from "./creaturecapacity.mjs"
export { default as VermineSpecialtyData } from "./specialty.mjs" export { default as VermineSpecialtyData } from "./specialty.mjs"
export { default as VermineBackgroundData } from "./background.mjs" export { default as VermineBackgroundData } from "./background.mjs"
export { default as VermineTraumaData } from "./trauma.mjs" export { default as VermineTraumaData } from "./trauma.mjs"
+1 -1
View File
@@ -253,7 +253,7 @@ export function baseItemSchema() {
return { return {
description: new fields.HTMLField({ required: true, initial: "", textSearch: true }), description: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
rarity: raritySchema(), rarity: raritySchema(),
reliability: new fields.NumberField({ ...reqInt, initial: 3, min: 1, max: 5 }), reliability: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 }),
handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), handicap: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
quantity: new fields.NumberField({ ...reqInt, initial: 1, min: 1 }), quantity: new fields.NumberField({ ...reqInt, initial: 1, min: 1 }),
weight: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), weight: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
+1
View File
@@ -17,6 +17,7 @@ export default class VermineAbilityData extends foundry.abstract.TypeDataModel {
totem: new fields.StringField({ required: true, initial: "" }), totem: new fields.StringField({ required: true, initial: "" }),
learn: learnSchema(), learn: learnSchema(),
level: levelSchema(1, 1, 3), level: levelSchema(1, 1, 3),
learned: new fields.BooleanField({ required: true, initial: false }),
effects: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), { effects: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
initial: [] initial: []
}) })
+3 -1
View File
@@ -14,7 +14,9 @@ export default class VermineBackgroundData extends foundry.abstract.TypeDataMode
const reqInt = { required: true, nullable: false, integer: true } const reqInt = { required: true, nullable: false, integer: true }
return { return {
...listItemSchema(), ...listItemSchema(),
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }) cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
asterisk: new fields.BooleanField({ required: true, initial: false }),
learned: new fields.BooleanField({ required: true, initial: false })
} }
} }
} }
+20 -3
View File
@@ -11,6 +11,7 @@ import {
skillCategoriesSchema, skillCategoriesSchema,
skillsSchema skillsSchema
} from "./_shared.mjs" } from "./_shared.mjs"
import { VermineTraits } from "../system/traits.mjs"
export default class VermineCharacterData extends foundry.abstract.TypeDataModel { export default class VermineCharacterData extends foundry.abstract.TypeDataModel {
@@ -54,6 +55,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
height: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), height: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
weight: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), weight: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }), totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Statut de Chaman : débloque l'affichage des Rites chamaniques maîtrisés.
chaman: new fields.BooleanField({ required: true, initial: false }),
age: new fields.StringField({ required: true, nullable: false, initial: "15" }), age: new fields.StringField({ required: true, nullable: false, initial: "15" }),
ageType: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2 }), ageType: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2 }),
profile: new fields.StringField({ required: true, nullable: false, initial: "" }), profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
@@ -97,6 +100,11 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }), self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }) effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
}), }),
// Perte de Réserves après perte du Fétiche : -1D de max persistant par Réserve
feticheLoss: new fields.SchemaField({
self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
}),
// Limites d'apprentissage par phase // Limites d'apprentissage par phase
learned: new fields.SchemaField({ learned: new fields.SchemaField({
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }), phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
@@ -104,7 +112,10 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
specialty: new fields.BooleanField({ required: true, initial: false }), specialty: new fields.BooleanField({ required: true, initial: false }),
reserve: new fields.BooleanField({ required: true, initial: false }), reserve: new fields.BooleanField({ required: true, initial: false }),
characteristic: new fields.BooleanField({ required: true, initial: false }), characteristic: new fields.BooleanField({ required: true, initial: false }),
evolution: new fields.BooleanField({ required: true, initial: false }) evolution: new fields.BooleanField({ required: true, initial: false }),
ability: new fields.BooleanField({ required: true, initial: false }),
background: new fields.BooleanField({ required: true, initial: false }),
reputation: new fields.BooleanField({ required: true, initial: false })
}) })
}), }),
@@ -152,6 +163,7 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
/** /**
* Calcule le max de sang-froid : * Calcule le max de sang-froid :
* somme des caractéristiques mentales + sociales + modificateur d'âge. * somme des caractéristiques mentales + sociales + modificateur d'âge.
* Un Fétiche équipé ajoute +1D ; une perte de Fétiche soustrait 1D.
*/ */
_setSelfControlMax() { _setSelfControlMax() {
const abilities = Object.values(this.abilities) const abilities = Object.values(this.abilities)
@@ -160,12 +172,15 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
.filter(a => a.category === "mental" || a.category === "social") .filter(a => a.category === "mental" || a.category === "social")
.reduce((acc, a) => acc + a.value, 0) .reduce((acc, a) => acc + a.value, 0)
const bonus = this.experience?.reserveBonus?.self_control || 0 const bonus = this.experience?.reserveBonus?.self_control || 0
this.attributes.self_control.max = Math.min(10, sum + modFromAge + bonus) const loss = this.experience?.feticheLoss?.self_control || 0
const fetiche = VermineTraits.feticheItem(this.parent) ? 1 : 0
this.attributes.self_control.max = Math.min(10, Math.max(0, sum + modFromAge + bonus + fetiche - loss))
} }
/** /**
* Calcule le max d'effort : * Calcule le max d'effort :
* somme des caractéristiques physiques + manuelles + modificateur d'âge. * somme des caractéristiques physiques + manuelles + modificateur d'âge.
* Un Fétiche équipé ajoute +1D ; une perte de Fétiche soustrait 1D.
*/ */
_setEffortMax() { _setEffortMax() {
const abilities = Object.values(this.abilities) const abilities = Object.values(this.abilities)
@@ -174,7 +189,9 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
.filter(a => a.category === "physical" || a.category === "manual") .filter(a => a.category === "physical" || a.category === "manual")
.reduce((acc, a) => acc + a.value, 0) .reduce((acc, a) => acc + a.value, 0)
const bonus = this.experience?.reserveBonus?.effort || 0 const bonus = this.experience?.reserveBonus?.effort || 0
this.attributes.effort.max = Math.min(10, sum + modFromAge + bonus) const loss = this.experience?.feticheLoss?.effort || 0
const fetiche = VermineTraits.feticheItem(this.parent) ? 1 : 0
this.attributes.effort.max = Math.min(10, Math.max(0, sum + modFromAge + bonus + fetiche - loss))
} }
/** /**
+30 -26
View File
@@ -90,7 +90,7 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
* .creatureRoleLevels, .creaturePackLevels. * .creatureRoleLevels, .creaturePackLevels.
* *
* Règles : * Règles :
* - Attaque = pattern.attack + size.attack + pack.attack + role.reaction * - Attaque = pattern.attack + size.attack + pack.attack
* - Dégâts = pattern.damage + size.vigor + pack.damage * - Dégâts = pattern.damage + size.vigor + pack.damage
* - Vigueur = size.vigor + pack.damage * - Vigueur = size.vigor + pack.damage
* - Réaction = role.reaction + role.reaction_bonus * - Réaction = role.reaction + role.reaction_bonus
@@ -116,11 +116,12 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
sizeVigor += cfg.vigor || 0 sizeVigor += cfg.vigor || 0
} }
// Attaque : patron + taille + meute + réaction du rôle // Attaque : patron + taille + meute.
// NB : le Rôle ne donne que la Réaction (dés + réussites automatiques),
// il n'ajoute pas de dés d'attaque (regles_creatures.txt).
this.computed.attack = (patternConfig.attack || 0) this.computed.attack = (patternConfig.attack || 0)
+ sizeAttack + sizeAttack
+ (packConfig.attack || 0) + (packConfig.attack || 0)
+ (roleConfig.reaction || 0)
// Dégâts : patron + vigueur de taille + meute // Dégâts : patron + vigueur de taille + meute
this.computed.damage = (patternConfig.damage || 0) this.computed.damage = (patternConfig.damage || 0)
@@ -130,8 +131,9 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
// Vigueur : taille + meute // Vigueur : taille + meute
this.computed.vigor = sizeVigor + (packConfig.damage || 0) this.computed.vigor = sizeVigor + (packConfig.damage || 0)
// Réaction : rôle // Réaction : rôle (le bonus de rôle est compté séparément et ajouté
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0) // comme réussites automatiques par le NpcRollDialog — cf. modèle PNJ).
this.computed.reaction = roleConfig.reaction || 0
this.computed.reactionBonus = roleConfig.reaction_bonus || 0 this.computed.reactionBonus = roleConfig.reaction_bonus || 0
// Réserves // Réserves
@@ -146,8 +148,13 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
} }
/** /**
* Calcule les seuils de blessures à partir du patron, de la taille et de la meute. * Calcule les seuils et les cercles de blessures à partir du patron, de la
* Les seuils sont la somme des valeurs correspondantes des trois sources. * taille et de la meute.
*
* Règles (regles_creatures.txt) :
* - Seuil = contribution du Gabarit + contributions de Taille (cumulées).
* Le Groupe n'ajoute PAS de seuil (tableau « Groupe » : cercles seuls).
* - Cercles = cercles du Gabarit + cercles de Taille (cumulés) + cercles du Groupe.
*/ */
_calculateCreatureWoundThresholds() { _calculateCreatureWoundThresholds() {
const patternLevel = this.pattern?.value || 1 const patternLevel = this.pattern?.value || 1
@@ -159,30 +166,27 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
// Les modificateurs de Taille se cumulent (Taille N = somme des tailles 1..N). // Les modificateurs de Taille se cumulent (Taille N = somme des tailles 1..N).
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {} const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
let sizeMinor = 0 const sizeThresholds = { minor: 0, major: 0, deadly: 0 }
let sizeMajor = 0 const sizeWounds = { minor: 0, major: 0, deadly: 0 }
let sizeDeadly = 0
for (let i = 1; i <= sizeLevel; i++) { for (let i = 1; i <= sizeLevel; i++) {
const cfg = sizeLevels[i] || {} const cfg = sizeLevels[i] || {}
sizeMinor += cfg.minorWound || 0 sizeThresholds.minor += cfg.minorThreshold || 0
sizeMajor += cfg.majorWound || 0 sizeThresholds.major += cfg.majorThreshold || 0
sizeDeadly += cfg.deadlyWound || 0 sizeThresholds.deadly += cfg.deadlyThreshold || 0
sizeWounds.minor += cfg.minorWound || 0
sizeWounds.major += cfg.majorWound || 0
sizeWounds.deadly += cfg.deadlyWound || 0
} }
this.minorWound.threshold = (patternConfig.minorWound || 0) // Seuils : gabarit + taille cumulée (pas de contribution du groupe).
+ sizeMinor this.minorWound.threshold = (patternConfig.minorThreshold ?? 0) + sizeThresholds.minor
+ (packConfig.minorWound || 0) this.majorWound.threshold = (patternConfig.majorThreshold ?? 0) + sizeThresholds.major
this.majorWound.threshold = (patternConfig.majorWound || 0) this.deadlyWound.threshold = (patternConfig.deadlyThreshold ?? 0) + sizeThresholds.deadly
+ sizeMajor
+ (packConfig.majorWound || 0)
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
+ sizeDeadly
+ (packConfig.deadlyWound || 0)
// Max de blessures // Cercles : gabarit + taille cumulée + groupe.
this.minorWound.max = Math.min(5, this.minorWound.threshold + 2) this.minorWound.max = (patternConfig.minorWound ?? 0) + sizeWounds.minor + (packConfig.minorWound ?? 0)
this.majorWound.max = Math.min(4, this.majorWound.threshold + 1) this.majorWound.max = (patternConfig.majorWound ?? 0) + sizeWounds.major + (packConfig.majorWound ?? 0)
this.deadlyWound.max = Math.min(2, this.deadlyWound.threshold) this.deadlyWound.max = (patternConfig.deadlyWound ?? 0) + sizeWounds.deadly + (packConfig.deadlyWound ?? 0)
} }
/** /**
+19
View File
@@ -0,0 +1,19 @@
import { listItemSchema } from "./_shared.mjs"
/**
* DataModel pour les items de type "creaturecapacity" (capacités de créature).
* @augments {foundry.abstract.TypeDataModel}
*/
export default class VermineCreatureCapacityData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.item.creaturecapacity"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
return {
...listItemSchema(),
type: new fields.StringField({ required: true, initial: "base" })
}
}
}
+2 -1
View File
@@ -13,7 +13,8 @@ export default class VermineEvolutionData extends foundry.abstract.TypeDataModel
const fields = foundry.data.fields const fields = foundry.data.fields
return { return {
...listItemSchema(), ...listItemSchema(),
level: levelSchema(1, 1, 4) level: levelSchema(1, 1, 4),
mutation: new fields.BooleanField({ required: true, initial: false })
} }
} }
} }
+18 -5
View File
@@ -42,6 +42,9 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
// Équipement // Équipement
equipment: equipmentSchema(), equipment: equipmentSchema(),
// Notes de voyage (lieux, dangers, routes…)
roadNotes: new fields.HTMLField({ required: true, initial: "" }),
// Niveau du groupe (1-10) // Niveau du groupe (1-10)
level: levelSchema(1, 1, 10), level: levelSchema(1, 1, 10),
@@ -77,7 +80,15 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
// Dés d'Expérience collectifs (réserve de Groupe) // Dés d'Expérience collectifs (réserve de Groupe)
experience: new fields.SchemaField({ experience: new fields.SchemaField({
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }) dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
// Limites d'apprentissage par phase
learned: new fields.SchemaField({
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
level: new fields.BooleanField({ required: true, initial: false }),
reserve: new fields.BooleanField({ required: true, initial: false }),
ability: new fields.BooleanField({ required: true, initial: false })
})
}), }),
// Rencontres // Rencontres
@@ -118,12 +129,14 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
} }
/** /**
* Calcule la réserve max en fonction du niveau du groupe. * Calcule la réserve max selon le nombre de membres (personnages).
* Règle simplifiée : niveau × 2, plafonné à 10. * Règles p. 68-69 : la Réserve débute à 3D et gagne 1D par personnage,
* avec un maximum de 10D.
*/ */
_calculateGroupReserve() { _calculateGroupReserve() {
const level = this.level?.value || 1 const members = Array.isArray(this.members) ? this.members : []
this.reserve.max = Math.min(10, level * 2) const characterCount = members.filter((id) => game.actors?.get(id)?.type === "character").length
this.reserve.max = Math.min(10, 3 + characterCount)
if (this.reserve.value > this.reserve.max) { if (this.reserve.value > this.reserve.max) {
this.reserve.value = this.reserve.max this.reserve.value = this.reserve.max
+2 -1
View File
@@ -14,7 +14,8 @@ export default class VermineItemData extends foundry.abstract.TypeDataModel {
needSkill: new fields.SchemaField({ needSkill: new fields.SchemaField({
value: new fields.BooleanField({ required: true, initial: false }), value: new fields.BooleanField({ required: true, initial: false }),
skill: new fields.StringField({ required: true, initial: "" }) skill: new fields.StringField({ required: true, initial: "" })
}) }),
equipped: new fields.BooleanField({ required: true, initial: false })
} }
} }
} }
+1 -1
View File
@@ -19,7 +19,7 @@ export default class VermineWeaponData extends foundry.abstract.TypeDataModel {
min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), min_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
damage: new fields.SchemaField({ damage: new fields.SchemaField({
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), value: new fields.NumberField({ ...reqInt, initial: 0, min: -10 }),
type: new fields.StringField({ required: true, initial: "" }), type: new fields.StringField({ required: true, initial: "" }),
addVigor: new fields.BooleanField({ required: true, initial: false }) addVigor: new fields.BooleanField({ required: true, initial: false })
}), }),
+13 -2
View File
@@ -2,10 +2,11 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) { export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(linkEl, actor) { constructor(linkEl, actor, options = {}) {
super(); super();
this.linkEl = linkEl; this.linkEl = linkEl;
this.actor = actor; this.actor = actor;
this.fillInstincts = options.fillInstincts === true;
} }
static get DEFAULT_OPTIONS() { static get DEFAULT_OPTIONS() {
@@ -32,7 +33,17 @@ export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
el.addEventListener('click', event => { el.addEventListener('click', event => {
const link = event.target.closest('a'); const link = event.target.closest('a');
if (!link?.dataset.totem) return; if (!link?.dataset.totem) return;
this.actor.update({ 'system.identity.totem': link.dataset.totem }); const update = { 'system.identity.totem': link.dataset.totem };
// Remplit automatiquement Instincts/Interdits depuis la config du totem
// (utilisé pour le Totem du Groupe).
if (this.fillInstincts && this.actor.system?.identity) {
const totemKey = link.dataset.totem;
const instincts = game.i18n.localize(`TOTEMS.${totemKey}.instincts`);
const bans = game.i18n.localize(`TOTEMS.${totemKey}.bans`);
if (!instincts.startsWith("TOTEMS.")) update['system.identity.instincts'] = instincts;
if (!bans.startsWith("TOTEMS.")) update['system.identity.prohibits'] = bans;
}
this.actor.update(update);
this.close(); this.close();
}); });
}); });
+49 -13
View File
@@ -129,30 +129,30 @@ VERMINE.npcRoleLevels = {
* Creature Pattern Levels configuration * Creature Pattern Levels configuration
*/ */
VERMINE.creaturePatternLevels = { VERMINE.creaturePatternLevels = {
1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 }, 1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorThreshold": 0, "majorThreshold": 0, "deadlyThreshold": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 }, 2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorThreshold": 0, "majorThreshold": 1, "deadlyThreshold": 3, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }, 3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorThreshold": 1, "majorThreshold": 3, "deadlyThreshold": 5, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 } 4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorThreshold": 3, "majorThreshold": 5, "deadlyThreshold": 7, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
} }
/** /**
* Creature Size Levels configuration * Creature Size Levels configuration
*/ */
VERMINE.creatureSizeLevels = { VERMINE.creatureSizeLevels = {
0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 }, 0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorThreshold": 0, "majorThreshold": 0, "deadlyThreshold": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 }, 1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorThreshold": 0, "majorThreshold": 1, "deadlyThreshold": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }, 2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorThreshold": 2, "majorThreshold": 2, "deadlyThreshold": 2, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 } 3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorThreshold": 3, "majorThreshold": 3, "deadlyThreshold": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
} }
/** /**
* Creature Pack Levels configuration * Creature Pack Levels configuration
*/ */
VERMINE.creaturePackLevels = { VERMINE.creaturePackLevels = {
0: { "attack": 0, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 }, 0: { "label": "VERMINE.none", "attack": 0, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
1: { "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 }, 1: { "label": "PACK_LEVELS.small", "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
2: { "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }, 2: { "label": "PACK_LEVELS.large", "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
3: { "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 } 3: { "label": "PACK_LEVELS.giant", "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
} }
/** /**
@@ -180,6 +180,25 @@ VERMINE.abilityCategories = {
} }
} }
/**
* Types de Capacités de créature (mode d'activation/disponibilité).
* Correspond aux Modes de jeu + une catégorie "base".
*/
VERMINE.creatureCapacityTypes = {
"base": {
"label": "CREATURE_CAPACITY_TYPES.base"
},
"survival": {
"label": "CREATURE_CAPACITY_TYPES.survival"
},
"nightmare": {
"label": "CREATURE_CAPACITY_TYPES.nightmare"
},
"apocalypse": {
"label": "CREATURE_CAPACITY_TYPES.apocalypse"
}
}
VERMINE.abilities = { VERMINE.abilities = {
"vigor": "ABILITIES.vigor.name", "vigor": "ABILITIES.vigor.name",
"health": "ABILITIES.health.name", "health": "ABILITIES.health.name",
@@ -269,6 +288,23 @@ VERMINE.totems = {
"loner": "TOTEMS.loner.name", "loner": "TOTEMS.loner.name",
"adapted": "TOTEMS.adapted.name" "adapted": "TOTEMS.adapted.name"
} }
/**
* Influence des Dés de Totems (règles p. 121).
* Selon le Totem dominant (strictement plus de Dés Humains ou Adaptés),
* le personnage subit un Bonus/Malus de 1D sur les Domaines de Compétences.
* Neutre : aucun modificateur.
*/
VERMINE.totemInfluence = {
"human": {
"bonus": ["man", "tool", "world"],
"malus": ["animal", "survival", "weapon"]
},
"adapted": {
"bonus": ["animal", "survival", "weapon"],
"malus": ["man", "tool", "world"]
}
}
VERMINE.totem_opposites = { VERMINE.totem_opposites = {
"human": "TOTEMS.adapted.name", "human": "TOTEMS.adapted.name",
"predator": "TOTEMS.scavenger.name", "predator": "TOTEMS.scavenger.name",
@@ -326,7 +362,7 @@ VERMINE.traits = {
}, },
"fetiche": { "fetiche": {
name: "Fétiche", name: "Fétiche",
description: "Le personnage possède un objet fétiche auquel il tient.Tant quil le possède, sa Réserve dEffort ou de Sang- Froid est augmentée de 1D, et il ne subit pas les Malus d’Épuisement liés à la valeur de ses Réserves, même si elles sont réduites à 0. Si le personnage perd ou est privé de cet objet, les règles d’Épuisement sappliquent de nouveau et il perd immédiatement 2D dans chacune de ses Réserves, dont les valeurs maximales sont réduites de 1D.Un même personnage ne peut posséder quun seul objet avec le Trait Fétiche.", description: "Le personnage possède un objet fétiche auquel il tient.Tant quil le possède, ses Réserves dEffort et de Sang-Froid sont augmentées de 1D, et il ne subit pas les Malus d’Épuisement liés à la valeur de ses Réserves, même si elles sont réduites à 0. Si le personnage perd ou est privé de cet objet, les règles d’Épuisement sappliquent de nouveau et il perd immédiatement 2D dans chacune de ses Réserves, dont les valeurs maximales sont réduites de 1D.Un même personnage ne peut posséder quun seul objet avec le Trait Fétiche.",
value: 0 value: 0
}, },
"incapacitant": { "incapacitant": {
@@ -0,0 +1,115 @@
import { VermineExperience } from "../experience.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'achat d'une Adaptation/Mutation avec les Dés d'Évolution
* accumulés. Vérifie le coût (niveau 1-4), la limite du Mode de jeu et les
* restrictions sur les Mutations ; en cas de succès, crée l'item et efface
* les Dés d'Évolution accumulés.
*/
export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#actor;
static DEFAULT_OPTIONS = {
classes: ["vermine-roll", "buy-evolution"],
tag: "form",
window: {
icon: "fas fa-cart-shopping",
resizable: false
},
position: {
width: 480,
height: 460
},
actions: {
buy: BuyEvolutionDialog.#onBuy,
cancel: BuyEvolutionDialog.#onCancel
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/buy-evolution-dialog.hbs", scrollable: [""] }
};
static async create({ actorId }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
return new BuyEvolutionDialog({ actor });
}
constructor(options = {}) {
super(options);
this.#actor = options.actor;
}
get title() {
return game.i18n.localize("VERMINE.buy_evolution");
}
async _prepareContext() {
const actor = this.#actor;
const mode = VermineExperience.gameMode();
const modeLabels = {
1: game.i18n.localize("VERMINE.mode_survie"),
2: game.i18n.localize("VERMINE.mode_cauchemar"),
3: game.i18n.localize("VERMINE.mode_apocalypse")
};
const levels = [];
for (let value = 1; value <= 4; value++) {
levels.push({ value, cost: value });
}
return {
evolutionDice: actor.system?.experience?.evolution || 0,
defaultName: game.i18n.localize("ITEMS.new_evolution"),
levels,
totalLevels: VermineExperience.evolutionTotalLevels(actor),
modeMax: VermineExperience.evolutionMaxForMode(mode),
modeLabel: modeLabels[mode] || ""
};
}
static async #onCancel(event, target) {
this.close();
}
static async #onBuy(event, target) {
const actor = this.#actor;
const name = this.element.querySelector("#evolution-name")?.value?.trim() || "";
const cost = parseInt(this.element.querySelector("#evolution-level")?.value, 10) || 0;
const mutation = this.element.querySelector("#evolution-mutation")?.checked || false;
if (!name) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_no_name"));
return;
}
const error = VermineExperience.validateEvolutionPurchase(actor, cost, mutation);
if (error) {
ui.notifications.warn(game.i18n.localize(error));
return;
}
const created = await actor.createEmbeddedDocuments("Item", [{
name,
type: "evolution",
system: {
level: { value: cost },
mutation
}
}]);
// N'efface les Dés d'Évolution que si l'item a bien été créé.
if (!created || created.length === 0) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_create"));
return;
}
await actor.update({ "system.experience.evolution": 0 });
ui.notifications.info(game.i18n.localize("VERMINE.buy_evolution_done"));
this.close();
}
}
+50 -9
View File
@@ -1,5 +1,6 @@
import { VermineUtils } from "../roll.mjs"; import { VermineUtils } from "../roll.mjs";
import { VermineExchange } from "../exchange.mjs"; import { VermineExchange } from "../exchange.mjs";
import { VermineTraits } from "../traits.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
@@ -50,12 +51,19 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
this.weapon = options.weapon ?? null; this.weapon = options.weapon ?? null;
this.mode = options.mode ?? "contact"; this.mode = options.mode ?? "contact";
this.abilityKey = VermineExchange.attackAbility(this.mode); this.abilityKey = VermineExchange.attackAbility(this.mode);
this.skillKey = this.weapon?.system?.skill || null; // Une arme sans compétence définie est traitée comme une attaque
// improvisée au contact : Corps-à-corps (Vigueur + pool Corps-à-corps).
this.skillKey = this.weapon?.system?.skill || 'close';
this.skillValue = this.skillKey ? (this.actor.system?.skills?.[this.skillKey]?.value || 0) : 0; this.skillValue = this.skillKey ? (this.actor.system?.skills?.[this.skillKey]?.value || 0) : 0;
this.skillPool = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.dicePool || 0) : 0; this.skillPool = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.dicePool || 0) : 0;
this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0; this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0;
this.basePool = VermineExchange.baseAttackPool(this.actor, this.weapon, this.mode); this.basePool = VermineExchange.baseAttackPool(this.actor, this.weapon, this.mode);
this.baseDamage = VermineExchange.baseDamage(this.actor, this.weapon); this.baseDamage = VermineExchange.baseDamage(this.actor, this.weapon);
this.rafaleValue = this.mode === "ranged" ? VermineTraits.traitValue(this.weapon, "rafale") : 0;
this.rapideHandicap = VermineTraits.traitValue(this.weapon, "rapide");
this.zoneValue = VermineTraits.traitValue(this.weapon, "zone");
const portee = VermineTraits.traitValue(this.weapon, "portee");
this.effectiveRange = this.mode === "ranged" && portee > 0 ? portee * VermineTraits.vigor(this.actor) : null;
} }
get title() { get title() {
@@ -147,7 +155,12 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0, hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
availableSpecialties: actor.items.filter(i => i.type === "specialty"), availableSpecialties: actor.items.filter(i => i.type === "specialty"),
availableItems: actor.items.filter(i => i.type === "item"), availableItems: actor.items.filter(i => i.type === "item"),
targets targets,
lourdMalus: VermineTraits.lourdMalus(actor),
mobilityNote: VermineTraits.mobilityNote(actor),
hasRafale: this.rafaleValue > 0,
rafaleValue: this.rafaleValue,
effectiveRange: this.effectiveRange
}; };
} }
@@ -167,6 +180,7 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
this.element.querySelector("#second-action")?.addEventListener("change", () => this.#updateUI()); this.element.querySelector("#second-action")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI()); this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#rafale")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI()); this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI()); this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
this.element.querySelector("#pools-spent")?.addEventListener("change", () => this.#updateUI()); this.element.querySelector("#pools-spent")?.addEventListener("change", () => this.#updateUI());
@@ -206,10 +220,18 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
return parseInt(this.#el.querySelector("#pools-spent")?.value, 10) || 0; return parseInt(this.#el.querySelector("#pools-spent")?.value, 10) || 0;
} }
#getTemporaryMalus() {
return parseInt(this.#el.querySelector("#temporary-malus")?.value, 10) || 0;
}
#getUseAmmo() { #getUseAmmo() {
return this.#el.querySelector("#use-ammo")?.checked ?? false; return this.#el.querySelector("#use-ammo")?.checked ?? false;
} }
#getRafale() {
return this.#el.querySelector("#rafale")?.checked ?? false;
}
#getTotems() { #getTotems() {
return { return {
human: this.#el.querySelector("#human-totem")?.checked ?? false, human: this.#el.querySelector("#human-totem")?.checked ?? false,
@@ -222,7 +244,7 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
} }
getDifficultySelect() { getDifficultySelect() {
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0); return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0) + (this.#getRafale() ? 2 : 0);
} }
getSkillCategory() { getSkillCategory() {
@@ -260,8 +282,12 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked"); const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0; const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0; const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
const total = this.basePool + selfControl + pools + specialty + help + tooling + group; const wound = VermineExchange.woundMalus(this.actor);
return { ability, skill, selfControl, pools, specialty, help, tooling, group, total }; const lourd = VermineTraits.lourdMalus(this.actor);
const tempMalus = this.#getTemporaryMalus();
const malus = wound + lourd + tempMalus;
const total = Math.max(0, this.basePool + selfControl + pools + specialty + help + tooling + group - malus);
return { ability, skill, selfControl, pools, specialty, help, tooling, group, wound, lourd, tempMalus, malus, total };
} }
getDicePool() { getDicePool() {
@@ -297,7 +323,7 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
const b = this.getPoolBreakdown(); const b = this.getPoolBreakdown();
const bonusEl = this.#el.querySelector("#total-bonus"); const bonusEl = this.#el.querySelector("#total-bonus");
if (bonusEl) bonusEl.textContent = b.selfControl + b.pools + b.specialty + b.help + b.tooling + b.group; if (bonusEl) bonusEl.textContent = b.selfControl + b.pools + b.specialty + b.help + b.tooling + b.group - b.malus;
const handSel = this.#el.querySelector("#handicap"); const handSel = this.#el.querySelector("#handicap");
const handEl = this.#el.querySelector("#current-handicap"); const handEl = this.#el.querySelector("#current-handicap");
@@ -364,10 +390,14 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
targets, targets,
weapon: this.weapon weapon: this.weapon
? { name: this.weapon.name, img: this.weapon.img, damage: foundry.utils.duplicate(this.weapon.system.damage) } ? { name: this.weapon.name, img: this.weapon.img, damage: foundry.utils.duplicate(this.weapon.system.damage) }
: null : null,
rapideHandicap: this.rapideHandicap || null,
zoneValue: this.zoneValue || null
}; };
await VermineUtils.roll({ const rafale = this.#getRafale();
const rollParams = {
actor: this.actor, actor: this.actor,
NoD: this.getDicePool(), NoD: this.getDicePool(),
Reroll: this.getReroll(), Reroll: this.getReroll(),
@@ -387,9 +417,20 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
targets: targets.map(t => t.name), targets: targets.map(t => t.name),
attack, attack,
messageFlags: { "vermine-exchange": attack } messageFlags: { "vermine-exchange": attack }
}); };
if (rafale && this.rafaleValue > 0) {
rollParams.bonusSuccesses = this.rafaleValue;
rollParams.bonusOnSuccess = true;
}
if (this.effectiveRange !== null) rollParams.effectiveRange = this.effectiveRange;
const mobilityHandicap = VermineTraits.mobilityHandicap(this.actor);
if (mobilityHandicap > 0) rollParams.mobilityHandicap = mobilityHandicap;
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
// en arrière-plan sans bloquer la fermeture.
this.close(); this.close();
await VermineUtils.roll(rollParams);
} }
#getRangeKey() { #getRangeKey() {
@@ -41,11 +41,19 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
async _prepareContext() { async _prepareContext() {
const characters = game.actors.filter(a => a.type === "character"); const characters = game.actors.filter(a => a.type === "character");
const groups = game.actors.filter(a => a.type === "group"); const groups = game.actors.filter(a => a.type === "group");
// Associe chaque personnage à son premier groupe (même logique que RollDialog.#findGroup)
const memberOf = new Map();
for (const g of groups) {
for (const id of g.system?.members || []) {
if (!memberOf.has(id)) memberOf.set(id, g.id);
}
}
return { return {
characters: characters.map(a => ({ characters: characters.map(a => ({
id: a.id, id: a.id,
name: a.name, name: a.name,
dice: a.system?.experience?.dice || 0 dice: a.system?.experience?.dice || 0,
groupId: memberOf.get(a.id) || ""
})), })),
groups: groups.map(a => ({ groups: groups.map(a => ({
id: a.id, id: a.id,
@@ -63,7 +71,9 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
async _onRender(context, options) { async _onRender(context, options) {
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI())); this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
this.element.querySelector("#group-select")?.addEventListener("change", () => this.#updateGroupFilter());
this.#updateUI(); this.#updateUI();
this.#updateGroupFilter();
} }
#updateUI() { #updateUI() {
@@ -74,6 +84,15 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
if (el) el.textContent = `${danger + discoveries + duration}D`; if (el) el.textContent = `${danger + discoveries + duration}D`;
} }
/** Filtre les personnages selon le groupe sélectionné (vide = tous). */
#updateGroupFilter() {
const groupId = this.element.querySelector("#group-select")?.value || "";
for (const row of this.element.querySelectorAll("[data-character-id]")) {
const show = !groupId || row.dataset.groupId === groupId;
row.hidden = !show;
}
}
static async #onCancel(event, target) { static async #onCancel(event, target) {
this.close(); this.close();
} }
@@ -88,12 +107,22 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
const group = game.actors.get(groupId); const group = game.actors.get(groupId);
if (group && collective > 0) { if (group && collective > 0) {
const current = group.system?.experience?.dice || 0; const current = group.system?.experience?.dice || 0;
await group.update({ "system.experience.dice": current + collective }); const phase = (group.system?.experience?.phase || 0) + 1;
await group.update({
"system.experience.dice": current + collective,
"system.experience.phase": phase,
"system.experience.learned.phase": phase,
"system.experience.learned.level": false,
"system.experience.learned.reserve": false,
"system.experience.learned.ability": false
});
} }
} }
const results = []; const results = [];
for (const row of this.element.querySelectorAll("[data-character-id]")) { for (const row of this.element.querySelectorAll("[data-character-id]")) {
// Quand un groupe précis est choisi, ne récompenser que ses membres.
if (groupId && row.dataset.groupId !== groupId) continue;
const id = row.dataset.characterId; const id = row.dataset.characterId;
const actor = game.actors.get(id); const actor = game.actors.get(id);
if (!actor) continue; if (!actor) continue;
@@ -125,7 +154,10 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
"system.experience.learned.specialty": false, "system.experience.learned.specialty": false,
"system.experience.learned.reserve": false, "system.experience.learned.reserve": false,
"system.experience.learned.characteristic": false, "system.experience.learned.characteristic": false,
"system.experience.learned.evolution": false "system.experience.learned.evolution": false,
"system.experience.learned.ability": false,
"system.experience.learned.background": false,
"system.experience.learned.reputation": false
}; };
await actor.update(update); await actor.update(update);
results.push({ name: actor.name, gained }); results.push({ name: actor.name, gained });
@@ -0,0 +1,254 @@
import { VermineUtils } from '../roll.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'apprentissage de Groupe (phase d'Expérience).
* Permet de dépenser des Dés d'Expérience du Groupe pour tenter d'améliorer
* le Niveau de Groupe, la Réserve de Groupe ou de développer une Capacité
* collective. La progression est appliquée automatiquement en cas de réussite.
*/
export default class GroupLearningDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#actor;
static DEFAULT_OPTIONS = {
classes: ['vermine-roll'],
tag: 'form',
window: {
icon: 'fas fa-graduation-cap',
resizable: false,
},
position: {
width: 560,
height: 560,
},
actions: {
roll: GroupLearningDialog.#onRoll,
cancel: GroupLearningDialog.#onCancel,
},
};
static PARTS = {
main: { template: 'systems/vermine2047/templates/dialogs/group-learning-dialog.hbs', scrollable: [''] },
};
static async create({ actorId }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'));
return null;
}
return new GroupLearningDialog({ actor });
}
constructor(options = {}) {
super(options);
this.#actor = options.actor;
}
get title() {
return game.i18n.localize('VERMINE.open_learning');
}
_prepareContext() {
const actor = this.#actor;
return {
actor,
system: actor.system,
dice: actor.system?.experience?.dice || 0,
abilities: actor.itemTypes.ability
.filter((i) => i.system.type !== 'totem' && !i.system.learned)
.map((a) => ({
id: a.id,
name: a.name,
threshold: a.system?.learn?.threshold ?? 5,
level: a.system?.level?.value ?? 1,
})),
};
}
_onRender() {
this.element.dataset.actorId = this.#actor.id;
for (const inp of this.element.querySelectorAll('[data-roll]')) {
inp.addEventListener('change', () => this.#updateUI());
}
this.element.querySelector('#learning-dice')?.addEventListener('input', () => this.#updateUI());
this.#updateUI();
}
get #el() { return this.element; }
#getType() {
return this.#el.querySelector('input[name="learning-type"]:checked')?.value ?? 'level';
}
#getAbilityId() {
return this.#el.querySelector('#learning-ability')?.value ?? '';
}
#getDiceSpent() {
return Math.max(1, parseInt(this.#el.querySelector('#learning-dice')?.value, 10) || 1);
}
#compute() {
const actor = this.#actor;
const type = this.#getType();
const key = type === 'ability' ? this.#getAbilityId() : '';
let difficulty = 7;
let handicap = 0;
let targetLevel = null;
let targetLabel = '';
if (type === 'level') {
const current = actor.system?.level?.value || 1;
difficulty = actor.system?.members?.length || 0;
handicap = current;
targetLevel = current + 1;
} else if (type === 'reserve') {
const current = actor.system?.reserve?.value || 0;
difficulty = current;
handicap = 2;
targetLevel = current + 1;
} else {
const ability = key ? actor.items.get(key) : null;
if (ability) {
difficulty = ability.system?.learn?.threshold ?? 5;
handicap = ability.system?.level?.value ?? 1;
targetLabel = ability.name;
}
}
const labels = {
level: 'VERMINE.learn_level',
reserve: 'VERMINE.learn_group_reserve',
ability: 'VERMINE.learn_ability',
};
return {
type,
key,
targetLevel,
difficulty,
handicap,
poolModifier: 0,
label: game.i18n.localize(labels[type] || labels.level),
targetLabel,
};
}
#updateUI() {
const type = this.#getType();
const show = (id, visible) => {
const el = this.#el.querySelector(id);
if (el) el.style.display = visible ? '' : 'none';
};
show('#learning-level-section', type === 'level');
show('#learning-reserve-section', type === 'reserve');
show('#learning-ability-section', type === 'ability');
const c = this.#compute();
const set = (sel, txt) => {
const el = this.#el.querySelector(sel);
if (el) el.textContent = txt;
};
set('#diff-value', c.difficulty);
set('#handicap-value', c.handicap);
set('#required-value', 1 + c.handicap);
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
set('#pool-value', `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? '+' : ''}${c.poolModifier}D)` : ''}`);
const poolInput = this.#el.querySelector('#learning-dice');
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
}
static #onCancel() {
this.close();
}
/** Renvoie la clé i18n d'erreur si le jet est impossible, sinon null. */
#getError(c) {
const actor = this.#actor;
const ex = actor.system?.experience;
// Limite : un seul Niveau, une seule Réserve et une seule Capacité par phase.
if (ex?.learned?.phase === ex?.phase && ex.learned[c.type]) {
return 'VERMINE.error_learning_limit';
}
if (c.type === 'level' && c.targetLevel !== null && (actor.system?.level?.value || 0) >= 10) {
return 'VERMINE.error_group_level_max';
}
if (c.type === 'reserve' && c.targetLevel !== null &&
(actor.system?.reserve?.value || 0) >= (actor.system?.reserve?.max || 10)) {
return 'VERMINE.error_group_reserve_max';
}
if (c.type === 'ability') {
if (!c.key) {
return 'VERMINE.no_group_ability';
}
const ability = c.key ? actor.items.get(c.key) : null;
const requiredLevel = ability?.system?.level?.value ?? 1;
if (ability && (actor.system?.level?.value || 1) < requiredLevel) {
return 'VERMINE.error_group_level_requirement';
}
}
return null;
}
static async #onRoll() {
const actor = this.#actor;
const ex = actor.system?.experience;
const spent = Math.min(this.#getDiceSpent(), ex?.dice || 0);
if (spent < 1) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_experience_dice'));
return;
}
const c = this.#compute();
const errorKey = this.#getError(c);
if (errorKey) {
ui.notifications.warn(game.i18n.localize(errorKey));
return;
}
const pool = Math.max(0, spent + c.poolModifier);
const rollLabel = `${game.i18n.localize('VERMINE.open_learning')}${c.label}${c.targetLabel ? ` (${c.targetLabel})` : ''}`;
const roll = await VermineUtils.roll({
actor,
NoD: pool,
Reroll: 0,
difficulty: c.difficulty,
handicap: c.handicap,
rollLabel,
poolBreakdown: { ability: 0, skill: 0, selfControl: 0, specialty: 0, help: 0, tooling: 0, group: 0, total: pool },
messageFlags: { 'vermine-learning': { type: c.type, key: c.key, spent, success: false } },
});
const required = 1 + c.handicap;
if ((roll._total ?? 0) >= required) {
await this.#applySuccess(c);
}
// Les Dés d'Expérience sont perdus que le jet soit réussi ou raté.
await actor.update({ "system.experience.dice": Math.max(0, (actor.system.experience.dice || 0) - spent) });
this.close();
}
async #applySuccess(c) {
const actor = this.#actor;
const updates = {};
if (c.type === 'level') {
const current = actor.system.level.value || 1;
updates['system.level.value'] = Math.min(10, current + 1);
updates['system.experience.learned.level'] = true;
} else if (c.type === 'reserve') {
const current = actor.system.reserve.value || 0;
updates['system.reserve.value'] = Math.min(actor.system.reserve.max || 10, current + 1);
updates['system.experience.learned.reserve'] = true;
} else {
if (c.key) {
await actor.updateEmbeddedDocuments('Item', [{ _id: c.key, 'system.learned': true }]);
}
updates['system.experience.learned.ability'] = true;
}
updates['system.experience.learned.phase'] = actor.system.experience.phase;
await actor.update(updates);
ui.notifications.info(game.i18n.localize('VERMINE.learning_success'));
}
}
+233 -16
View File
@@ -64,6 +64,29 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
rarity: actor.system?.skills?.[key]?.rarity || 0, rarity: actor.system?.skills?.[key]?.rarity || 0,
isDomain: VermineExperience.isPreferredDomain(actor, key) isDomain: VermineExperience.isPreferredDomain(actor, key)
})); }));
const totem = actor.system?.identity?.totem || "";
const gameMode = this.#getGameMode();
const groupLevel = this.#getGroupLevel();
const totemAbilities = (actor.itemTypes?.ability || [])
.filter(i =>
i.system?.type === "character"
&& !i.system?.learned
&& i.system?.totem === totem
&& (i.system?.level?.value || 0) <= gameMode
&& groupLevel >= (i.system?.level?.value || 0)
)
.map(a => ({
id: a.id,
name: a.name,
level: a.system.level.value,
threshold: a.system.learn.threshold,
hindrance: a.system.learn.hindrance
}));
const backgrounds = (actor.itemTypes?.background || [])
.filter(i => !i.system?.learned)
.map(b => ({ id: b.id, name: b.name, asterisk: b.system?.asterisk || false }));
const human = actor.system?.adaptation?.totems?.human?.value || 0;
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
return { return {
actor, actor,
system: actor.system, system: actor.system,
@@ -81,11 +104,48 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
], ],
specialties: actor.itemTypes?.specialty || [], specialties: actor.itemTypes?.specialty || [],
skills, skills,
specialtyCount: (actor.itemTypes?.specialty || []).length specialtyCount: (actor.itemTypes?.specialty || []).length,
totem,
gameMode,
groupLevel,
totemAbilities,
backgrounds,
reputation: actor.system?.attributes?.reputation?.value ?? 0,
human,
adapted,
canUseAdaptedDie: VermineExperience.canUseAdaptedDie(human, adapted)
}; };
} }
async _onRender(context, options) { #getGameMode() {
return parseInt(game.settings.get("vermine2047", "game-mode") || "1", 10) || 1;
}
#getGroupLevel() {
const groups = game.actors.filter(a => a.type === "group" && (a.system?.members || []).includes(this.#actor.id));
return Math.max(0, ...groups.map(g => g.system?.level?.value || 0));
}
#canUseAdaptedDie() {
const a = this.#actor.system?.adaptation?.totems;
return VermineExperience.canUseAdaptedDie(a?.human?.value || 0, a?.adapted?.value || 0);
}
#totemCapacityCounts() {
// Ne comptabilise que les Capacités du Totem courant du personnage
// (les Capacités liées à un autre Totem ne bloquent pas les limites).
const totem = this.#actor.system?.identity?.totem;
const items = (this.#actor.itemTypes?.ability || []).filter(i =>
i.system?.type === "character" && i.system?.learned && i.system?.totem === totem);
const byLevel = { 1: 0, 2: 0, 3: 0 };
for (const i of items) {
const level = i.system?.level?.value || 0;
byLevel[level] = (byLevel[level] || 0) + 1;
}
return { total: items.length, byLevel };
}
async _onRender() {
this.element.dataset.actorId = this.#actor.id; this.element.dataset.actorId = this.#actor.id;
for (const inp of this.element.querySelectorAll("[data-roll]")) { for (const inp of this.element.querySelectorAll("[data-roll]")) {
inp.addEventListener("change", () => this.#updateUI()); inp.addEventListener("change", () => this.#updateUI());
@@ -104,24 +164,33 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
const type = this.#getType(); const type = this.#getType();
if (type === "characteristic") return this.#el.querySelector("#learning-ability")?.value ?? ""; if (type === "characteristic") return this.#el.querySelector("#learning-ability")?.value ?? "";
if (type === "reserve") return this.#el.querySelector("#learning-reserve")?.value ?? ""; if (type === "reserve") return this.#el.querySelector("#learning-reserve")?.value ?? "";
if (type === "totem") return this.#el.querySelector("#learning-totem")?.value ?? "";
if (type === "background") return this.#el.querySelector("#learning-background")?.value ?? "";
return this.#el.querySelector("#learning-target")?.value ?? ""; return this.#el.querySelector("#learning-target")?.value ?? "";
} }
#getReputationDirection() {
return this.#el.querySelector("#learning-reputation-direction")?.value ?? "reduce";
}
#isAdaptedDieChecked() {
return Boolean(this.#el.querySelector("#learning-adapted-die")?.checked);
}
#getDiceSpent() { #getDiceSpent() {
return Math.max(1, parseInt(this.#el.querySelector("#learning-dice")?.value, 10) || 1); return Math.max(1, parseInt(this.#el.querySelector("#learning-dice")?.value, 10) || 1);
} }
#getSpecialtyName() { #getSpecialtyName() {
return this.#el.querySelector("#specialty-name")?.value?.trim() || game.i18n.localize("ITEMS.new_specialty"); // Le dialogue peut avoir été fermé pendant le jet (this.#el null).
const el = this.#el?.querySelector("#specialty-name");
return el?.value?.trim() || game.i18n.localize("ITEMS.new_specialty");
} }
#compute() { #compute() {
const actor = this.#actor; const actor = this.#actor;
const type = this.#getType(); const type = this.#getType();
const key = this.#getTargetKey(); const key = this.#getTargetKey();
const difficulty = 7;
let handicap = 0;
let poolModifier = 0;
let label = ""; let label = "";
let targetLevel = null; let targetLevel = null;
@@ -182,10 +251,71 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
difficulty: VermineExperience.reserveLearningDifficulty(current), difficulty: VermineExperience.reserveLearningDifficulty(current),
handicap: VermineExperience.ageHandicap(actor.system?.identity?.ageType || 2), handicap: VermineExperience.ageHandicap(actor.system?.identity?.ageType || 2),
poolModifier: 0, poolModifier: 0,
adaptedDie: false,
label, label,
targetLabel: key ? game.i18n.localize(key === "self_control" ? "VERMINE.self_control" : "VERMINE.effort") : "" targetLabel: key ? game.i18n.localize(key === "self_control" ? "VERMINE.self_control" : "VERMINE.effort") : ""
}; };
} }
if (type === "totem") {
const item = actor.itemTypes?.ability?.find(i => i.id === key);
const data = item?.system;
label = game.i18n.localize("VERMINE.learn_totem_ability");
return {
type,
key,
targetLevel: data?.level?.value || 0,
difficulty: VermineExperience.totemAbilityLearningDifficulty(data),
handicap: VermineExperience.totemAbilityLearningHandicap(data),
poolModifier: 0,
adaptedDie: false,
label,
targetLabel: item?.name || ""
};
}
if (type === "background") {
const item = actor.itemTypes?.background?.find(i => i.id === key);
const data = item?.system;
label = game.i18n.localize("VERMINE.learn_background");
return {
type,
key,
targetLevel: 0,
difficulty: VermineExperience.backgroundLearningDifficulty(),
handicap: VermineExperience.backgroundLearningHandicap(data?.asterisk),
poolModifier: 0,
adaptedDie: false,
label,
targetLabel: item?.name || ""
};
}
if (type === "reputation") {
const current = actor.system?.attributes?.reputation?.value ?? 0;
const direction = this.#getReputationDirection();
if (direction === "increase") {
return {
type,
key: "increase",
targetLevel: current,
difficulty: VermineExperience.reputationIncreaseDifficulty(current),
handicap: 0,
poolModifier: 0,
adaptedDie: false,
label: game.i18n.localize("VERMINE.learn_reputation"),
targetLabel: game.i18n.localize("VERMINE.reputation_increase")
};
}
return {
type,
key: "reduce",
targetLevel: Math.max(0, current - 1),
difficulty: VermineExperience.reputationReduceDifficulty(current),
handicap: 0,
poolModifier: 0,
adaptedDie: false,
label: game.i18n.localize("VERMINE.learn_reputation"),
targetLabel: game.i18n.localize("VERMINE.reputation_reduce")
};
}
// evolution // evolution
const human = actor.system?.adaptation?.totems?.human?.value || 0; const human = actor.system?.adaptation?.totems?.human?.value || 0;
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0; const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
@@ -197,6 +327,7 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
difficulty: VermineExperience.evolutionDifficulty(actor.system?.identity?.ageType || 2), difficulty: VermineExperience.evolutionDifficulty(actor.system?.identity?.ageType || 2),
handicap: actor.system?.experience?.evolution || 0, handicap: actor.system?.experience?.evolution || 0,
poolModifier: VermineExperience.evolutionTotemModifier(human, adapted), poolModifier: VermineExperience.evolutionTotemModifier(human, adapted),
adaptedDie: this.#canUseAdaptedDie() && this.#isAdaptedDieChecked(),
label, label,
targetLabel: "" targetLabel: ""
}; };
@@ -213,6 +344,10 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
show("#learning-ability-section", type === "characteristic"); show("#learning-ability-section", type === "characteristic");
show("#learning-reserve-section", type === "reserve"); show("#learning-reserve-section", type === "reserve");
show("#learning-evolution-section", type === "evolution"); show("#learning-evolution-section", type === "evolution");
show("#learning-totem-section", type === "totem");
show("#learning-background-section", type === "background");
show("#learning-reputation-section", type === "reputation");
show("#learning-adapted-die-row", type === "evolution" && this.#canUseAdaptedDie());
// Pour une Spécialité, seule une compétence au moins Confirmée (≥2) est valable. // Pour une Spécialité, seule une compétence au moins Confirmée (≥2) est valable.
const skillSelect = this.#el.querySelector("#learning-target"); const skillSelect = this.#el.querySelector("#learning-target");
@@ -229,16 +364,25 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
set("#handicap-value", c.handicap); set("#handicap-value", c.handicap);
set("#required-value", 1 + c.handicap); set("#required-value", 1 + c.handicap);
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier); const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
set("#pool-value", `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`); let poolText = `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`;
if (c.adaptedDie) poolText += " — " + game.i18n.localize("VERMINE.adapted_die_x2");
set("#pool-value", poolText);
if (type === "reputation") {
const current = this.#actor.system?.attributes?.reputation?.value ?? 0;
const direction = this.#getReputationDirection();
set("#reputation-target", direction === "increase"
? `${current}${Math.min(10, current + 1)}+`
: `${current}${Math.max(0, current - 1)}`);
}
const poolInput = this.#el.querySelector("#learning-dice"); const poolInput = this.#el.querySelector("#learning-dice");
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0; if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
} }
static async #onCancel(event, target) { static async #onCancel() {
this.close(); this.close();
} }
static async #onRoll(event, target) { static async #onRoll() {
const actor = this.#actor; const actor = this.#actor;
const ex = actor.system?.experience; const ex = actor.system?.experience;
const dice = ex?.dice || 0; const dice = ex?.dice || 0;
@@ -250,10 +394,11 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
const c = this.#compute(); const c = this.#compute();
// Limite : une seule compétence, spécialité, réserve, caractéristique et // Limite : une seule compétence, spécialité, réserve, caractéristique,
// un seul Dé d'Évolution par phase. // un seul Dé d'Évolution, Capacité de Totem, Historique ou jet de
// Réputation par phase.
if (ex?.learned?.phase === ex?.phase) { if (ex?.learned?.phase === ex?.phase) {
const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution" }; const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution", totem: "ability", background: "background", reputation: "reputation" };
if (ex.learned[flagMap[c.type]]) { if (ex.learned[flagMap[c.type]]) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_learning_limit")); ui.notifications.warn(game.i18n.localize("VERMINE.error_learning_limit"));
return; return;
@@ -275,6 +420,61 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
ui.notifications.warn(game.i18n.localize("VERMINE.error_reserve_max")); ui.notifications.warn(game.i18n.localize("VERMINE.error_reserve_max"));
return; return;
} }
if (c.type === "totem") {
const item = actor.itemTypes?.ability?.find(i => i.id === c.key);
if (!item || item.system?.learned) {
ui.notifications.warn(game.i18n.localize("VERMINE.no_totem_ability"));
return;
}
const level = item.system?.level?.value || 0;
if (level > this.#getGameMode()) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_mode"));
return;
}
if (item.system?.totem !== actor.system?.identity?.totem) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_totem"));
return;
}
if (this.#getGroupLevel() < level) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_group_level"));
return;
}
const counts = this.#totemCapacityCounts();
if (level === 1 && (counts.byLevel[1] || 0) >= 3) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
return;
}
if (level >= 2 && (counts.byLevel[level] || 0) >= 1) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
return;
}
if (counts.total >= 5) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
return;
}
}
if (c.type === "background") {
const item = actor.itemTypes?.background?.find(i => i.id === c.key);
if (!item || item.system?.learned) {
ui.notifications.warn(game.i18n.localize("VERMINE.no_background"));
return;
}
}
if (c.type === "reputation") {
const current = actor.system?.attributes?.reputation?.value ?? 0;
if (c.key === "reduce" && current <= 0) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_reputation_min"));
return;
}
if (c.key === "increase" && current >= 10) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_reputation_max"));
return;
}
}
if (c.type === "evolution" && c.adaptedDie && !this.#canUseAdaptedDie()) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_no_adapted"));
return;
}
const pool = Math.max(0, spent + c.poolModifier); const pool = Math.max(0, spent + c.poolModifier);
const rollLabel = `${game.i18n.localize("VERMINE.open_learning")}${c.label}${c.targetLabel ? " (" + c.targetLabel + ")" : ""}`; const rollLabel = `${game.i18n.localize("VERMINE.open_learning")}${c.label}${c.targetLabel ? " (" + c.targetLabel + ")" : ""}`;
@@ -285,6 +485,7 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
Reroll: 0, Reroll: 0,
difficulty: c.difficulty, difficulty: c.difficulty,
handicap: c.handicap, handicap: c.handicap,
totems: c.adaptedDie ? { adapted: true } : { human: false, adapted: false },
rollLabel, rollLabel,
poolBreakdown: { ability: 0, skill: 0, selfControl: 0, specialty: 0, help: 0, tooling: 0, group: 0, total: pool }, poolBreakdown: { ability: 0, skill: 0, selfControl: 0, specialty: 0, help: 0, tooling: 0, group: 0, total: pool },
messageFlags: { "vermine-learning": { type: c.type, key: c.key, spent, success: false } } messageFlags: { "vermine-learning": { type: c.type, key: c.key, spent, success: false } }
@@ -293,14 +494,16 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
const required = 1 + c.handicap; const required = 1 + c.handicap;
const specialtyName = this.#getSpecialtyName(); const specialtyName = this.#getSpecialtyName();
if ((roll._total ?? 0) >= required) { if ((roll._total ?? 0) >= required) {
await this.#applySuccess(c, spent, specialtyName); await this.#applySuccess(c, specialtyName, roll._total ?? 0);
} }
// Les Dés d'Expérience sont perdus que le jet soit réussi ou raté.
await actor.update({ "system.experience.dice": Math.max(0, (actor.system.experience.dice || 0) - spent) });
this.close(); this.close();
} }
async #applySuccess(c, spent, specialtyName) { async #applySuccess(c, specialtyName, successes = 0) {
const actor = this.#actor; const actor = this.#actor;
const updates = { "system.experience.dice": (actor.system.experience.dice || 0) - spent }; const updates = {};
if (c.type === "skill") { if (c.type === "skill") {
const current = actor.system.skills[c.key].value || 0; const current = actor.system.skills[c.key].value || 0;
updates["system.skills." + c.key + ".value"] = Math.min(5, current + 1); updates["system.skills." + c.key + ".value"] = Math.min(5, current + 1);
@@ -316,9 +519,23 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
const bonus = actor.system.experience.reserveBonus?.[c.key] || 0; const bonus = actor.system.experience.reserveBonus?.[c.key] || 0;
updates["system.experience.reserveBonus." + c.key] = bonus + 1; updates["system.experience.reserveBonus." + c.key] = bonus + 1;
updates["system.experience.learned.reserve"] = true; updates["system.experience.learned.reserve"] = true;
} else { } else if (c.type === "evolution") {
updates["system.experience.evolution"] = (actor.system.experience.evolution || 0) + 1; updates["system.experience.evolution"] = (actor.system.experience.evolution || 0) + 1;
updates["system.experience.learned.evolution"] = true; updates["system.experience.learned.evolution"] = true;
} else if (c.type === "totem") {
updates["system.experience.learned.ability"] = true;
await actor.updateEmbeddedDocuments("Item", [{ _id: c.key, "system.learned": true }]);
} else if (c.type === "background") {
updates["system.experience.learned.background"] = true;
await actor.updateEmbeddedDocuments("Item", [{ _id: c.key, "system.learned": true }]);
} else if (c.type === "reputation") {
const current = actor.system?.attributes?.reputation?.value ?? 0;
if (c.key === "increase") {
updates["system.attributes.reputation.value"] = Math.min(10, current + successes);
} else {
updates["system.attributes.reputation.value"] = Math.max(0, current - 1);
}
updates["system.experience.learned.reputation"] = true;
} }
updates["system.experience.learned.phase"] = actor.system.experience.phase; updates["system.experience.learned.phase"] = actor.system.experience.phase;
await actor.update(updates); await actor.update(updates);
+29 -4
View File
@@ -1,4 +1,5 @@
import { VermineUtils } from "../roll.mjs" import { VermineUtils } from "../roll.mjs"
import { VermineExchange } from "../exchange.mjs"
const { HandlebarsApplicationMixin } = foundry.applications.api const { HandlebarsApplicationMixin } = foundry.applications.api
@@ -139,6 +140,21 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
inp.addEventListener("change", this.#onInputChange.bind(this)) inp.addEventListener("change", this.#onInputChange.bind(this))
} }
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI()) this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI())
// Trait Rapide (n) : le défenseur subit (n) Handicaps sur sa défense.
if (this.locks?.handicap !== undefined && this.locks?.handicap !== null) {
const sel = this.#el.querySelector("#handicap")
if (sel) {
const locked = parseInt(this.locks.handicap, 10)
let opt = [...sel.options].find(o => parseInt(o.value, 10) === locked + 1)
if (!opt) {
opt = document.createElement("option")
opt.value = String(locked + 1)
opt.textContent = game.i18n.localize("VERMINE.handicap") + ` (${locked})`
sel.appendChild(opt)
}
opt.selected = true
}
}
this.#updateUI() this.#updateUI()
} }
@@ -158,10 +174,15 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
#getGroup() { return parseInt(this.#el.querySelector("#group")?.value, 10) || 0 } #getGroup() { return parseInt(this.#el.querySelector("#group")?.value, 10) || 0 }
#getTemporaryMalus() { return parseInt(this.#el.querySelector("#temporary-malus")?.value, 10) || 0 }
getPoolBreakdown() { getPoolBreakdown() {
const help = this.#getHelped() const help = this.#getHelped()
const group = this.#getGroup() const group = this.#getGroup()
return { pool: this.pool, help, group, total: this.pool + help + group } const wound = VermineExchange.woundMalus(this.#actor)
const tempMalus = this.#getTemporaryMalus()
const malus = wound + tempMalus
return { pool: this.pool, help, group, wound, tempMalus, malus, total: Math.max(0, this.pool + help + group - malus) }
} }
#updateUI() { #updateUI() {
@@ -169,7 +190,7 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
const totalEl = this.#el.querySelector("#dice-pool-total") const totalEl = this.#el.querySelector("#dice-pool-total")
if (totalEl) totalEl.textContent = `${b.total}D` if (totalEl) totalEl.textContent = `${b.total}D`
const bonusEl = this.#el.querySelector("#total-bonus") const bonusEl = this.#el.querySelector("#total-bonus")
if (bonusEl) bonusEl.textContent = b.help + b.group if (bonusEl) bonusEl.textContent = b.help + b.group - b.malus
const freeEl = this.#el.querySelector("#free-successes") const freeEl = this.#el.querySelector("#free-successes")
if (freeEl) freeEl.textContent = String(this.freeSuccesses) if (freeEl) freeEl.textContent = String(this.freeSuccesses)
} }
@@ -191,7 +212,7 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
: game.i18n.localize("VERMINE.defense_esquive")) : game.i18n.localize("VERMINE.defense_esquive"))
: this.label : this.label
await VermineUtils.roll({ const rollParams = {
actor: this.#actor, actor: this.#actor,
NoD: this.getPoolBreakdown().total, NoD: this.getPoolBreakdown().total,
Reroll: this.rerolls, Reroll: this.rerolls,
@@ -201,8 +222,12 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
bonusSuccesses: this.freeSuccesses, bonusSuccesses: this.freeSuccesses,
poolBreakdown: this.getPoolBreakdown(), poolBreakdown: this.getPoolBreakdown(),
messageFlags: this.defense ? { "vermine-defense": this.defense } : null messageFlags: this.defense ? { "vermine-defense": this.defense } : null
}) }
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
// en arrière-plan sans bloquer la fermeture.
this.close() this.close()
await VermineUtils.roll(rollParams)
} }
} }
+107
View File
@@ -0,0 +1,107 @@
import { VermineReliability } from '../reliability.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue de Test de Fiabilité / Réparation d'un item.
* Mode "test" : choix de la gravité des Dommages matériels (Mineurs/Graves/Critiques)
* + case Immersion (Trait Étanche). Mode "repair" : type de réparation
* (classique/de fortune) + compétence utilisée.
*/
export default class ReliabilityDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#item;
#mode;
static DEFAULT_OPTIONS = {
classes: ['vermine-roll', 'reliability'],
tag: 'form',
window: {
icon: 'fas fa-tools',
resizable: false,
},
position: {
width: 460,
height: 'auto',
},
actions: {
run: ReliabilityDialog.#onRun,
cancel: ReliabilityDialog.#onCancel,
},
};
static PARTS = {
main: { template: 'systems/vermine2047/templates/dialogs/reliability-dialog.hbs', scrollable: [''] },
};
static async create({ item, mode = 'test' }) {
return new ReliabilityDialog({ item, mode });
}
constructor(options = {}) {
super(options);
this.#item = options.item;
this.#mode = options.mode ?? 'test';
}
get title() {
return game.i18n.localize(this.#mode === 'repair' ? 'VERMINE.reliability_repair' : 'VERMINE.reliability_test');
}
async _prepareContext() {
const hasEtanche = !!this.#item.system?.traits?.etanche;
const hasActor = !!this.#item.actor;
const coque = this.#item.system?.traits?.coque ?? {};
const hasBrokenCoque = this.#mode === 'repair' && (parseInt(coque?.value ?? 0, 10) || 0) === 0 && (parseInt(coque?.max ?? 0, 10) || 0) > 0;
const storedSkill = (this.#item.system?.needSkill?.value && this.#item.system?.needSkill?.skill)
|| this.#item.system?.skill || '';
const severities = ['mineurs', 'graves', 'critiques'].map((key) => ({
key,
label: game.i18n.localize(`VERMINE.reliability_severity_${key}`),
selected: key === 'graves',
}));
const skills = VermineReliability.REPAIR_SKILLS.map((key) => ({
key,
label: game.i18n.localize(`SKILLS.${key}.name`),
selected: key === storedSkill && VermineReliability.REPAIR_SKILLS.includes(storedSkill),
}));
return {
mode: this.#mode,
isTest: this.#mode === 'test',
isRepair: this.#mode === 'repair',
hasEtanche,
hasActor,
hasBrokenCoque,
coqueMax: parseInt(coque?.max ?? 0, 10) || 0,
severities,
skills,
};
}
#getOptions() {
const el = this.element;
if (this.#mode === 'test') {
return {
severity: el.querySelector('input[name="severity"]:checked')?.value ?? 'graves',
immersion: !!el.querySelector('#reliability-immersion')?.checked,
};
}
return {
mode: el.querySelector('input[name="repairMode"]:checked')?.value ?? 'classique',
skill: el.querySelector('#reliability-skill')?.value ?? 'technology',
};
}
static async #onRun() {
const options = this.#getOptions();
if (this.#mode === 'test') {
await VermineReliability.test(this.#item, options);
} else {
await VermineReliability.repair(this.#item, options);
}
this.close();
}
static #onCancel() {
this.close();
}
}
+112 -19
View File
@@ -1,4 +1,6 @@
import { VermineUtils } from "../roll.mjs"; import { VermineUtils } from "../roll.mjs";
import { VermineExchange } from "../exchange.mjs";
import { VermineTraits } from "../traits.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
@@ -64,6 +66,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
const isCharacter = actor.type === "character"; const isCharacter = actor.type === "character";
const group = isCharacter ? RollDialog.#findGroup(actor) : null; const group = isCharacter ? RollDialog.#findGroup(actor) : null;
const groupPool = group?.system?.experience?.dice || 0; const groupPool = group?.system?.experience?.dice || 0;
const intimidant = isCharacter ? VermineTraits.equippedItems(actor, 'intimidant')[0] ?? null : null;
return { return {
actor, actor,
system: actor.system, system: actor.system,
@@ -77,6 +80,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
specialty: false, specialty: false,
availableSpecialties: actor.items.filter(i => i.type === "specialty"), availableSpecialties: actor.items.filter(i => i.type === "specialty"),
availableItems: actor.items.filter(i => i.type === "item"), availableItems: actor.items.filter(i => i.type === "item"),
hasIntimidant: Boolean(intimidant),
intimidantName: intimidant?.name ?? "",
locks: this.locks, locks: this.locks,
experience: { experience: {
hasExperience: isCharacter, hasExperience: isCharacter,
@@ -139,6 +144,50 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
#getSelfCtrl() { return this.#el.querySelector("#self_control"); } #getSelfCtrl() { return this.#el.querySelector("#self_control"); }
#getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; } #getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; }
/** Clé de la compétence sélectionnée (data-label) ou null. */
getSelectedSkillKey() {
const sel = this.#getSkill();
return sel?.options[sel.selectedIndex]?.dataset?.label ?? null;
}
/** Outil générique sélectionné (radio usingTools) ou null. */
getSelectedTool() {
const checked = this.#el.querySelector("input[name='usingTools']:checked");
if (!checked || checked.value === "0") return null;
return this.#actor.items.find(i => i.name === checked.value) ?? null;
}
/** Bonus d'outillage : +1D, ou +2D si l'outil possède le Trait Pratique. */
#getTooling() {
const tool = this.getSelectedTool();
if (!tool) return 0;
return VermineTraits.hasTrait(tool, 'pratique') ? 2 : 1;
}
/** Malus temporaires saisis manuellement (Trait Malus). */
#getTemporaryMalus() {
return parseInt(this.#el.querySelector("#temporary-malus")?.value, 10) || 0;
}
/** Case « Brandit » (Trait Intimidant). */
getIntimidant() {
return this.#el.querySelector("#intimidant")?.checked ?? false;
}
/** Valeur de Handicap brute du sélecteur (0-2). */
#getBaseHandicap() {
const sel = this.#getHandicap();
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
}
/** Nombre de Handicaps annulés par l'outil Ponctuel sélectionné. */
getPonctuelUsed() {
const tool = this.getSelectedTool();
const ponctuel = tool ? VermineTraits.traitValue(tool, 'ponctuel') : 0;
if (ponctuel <= 0) return 0;
return Math.min(ponctuel, this.#getBaseHandicap());
}
/** /**
* Trouve le premier Groupe dont le personnage est membre. * Trouve le premier Groupe dont le personnage est membre.
* @param {Actor} actor * @param {Actor} actor
@@ -157,15 +206,17 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0; const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const specChecked = this.hasSpecialtySelected(); const specChecked = this.hasSpecialtySelected();
const helped = this.#el.querySelector("#helped")?.checked; const helped = this.#el.querySelector("#helped")?.checked;
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
const tools = toolsChecked && toolsChecked.value !== "0";
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0; const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
const experience = this.#getExperiencePull(); const experience = this.#getExperiencePull();
const specialty = specChecked ? 1 : 0; const specialty = specChecked ? 1 : 0;
const help = helped ? 1 : 0; const help = helped ? 1 : 0;
const tooling = tools ? 1 : 0; const tooling = this.#getTooling();
const total = ability + skillPool + selfControl + specialty + help + tooling + group + experience; const wound = VermineExchange.woundMalus(this.#actor);
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, total }; const tempMalus = this.#getTemporaryMalus();
const psychology = this.getIntimidant() && this.getSelectedSkillKey() === 'psychology' ? 1 : 0;
const malus = wound + tempMalus + psychology;
const total = Math.max(0, ability + skillPool + selfControl + specialty + help + tooling + group + experience - malus);
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, wound, tempMalus, psychology, malus, total };
} }
getDicePool() { getDicePool() {
@@ -186,12 +237,15 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
getReroll() { getReroll() {
const sel = this.#getSkill(); const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0; const idx = sel?.selectedIndex ?? 0;
return parseInt(sel?.options[idx]?.dataset?.reroll, 10) || 0; const base = parseInt(sel?.options[idx]?.dataset?.reroll, 10) || 0;
// Trait Intimidant (hors Psychologie) : Relance 1D tant que l'objet est brandi.
if (this.getIntimidant() && this.getSelectedSkillKey() !== 'psychology') return base + 1;
return base;
} }
getHandicapSelect() { getHandicapSelect() {
const sel = this.#getHandicap(); // Trait Ponctuel : l'outil sélectionné annule jusqu'à (n) Handicaps.
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1); return this.#getBaseHandicap() - this.getPonctuelUsed();
} }
getSkillCategory() { getSkillCategory() {
@@ -219,14 +273,13 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
getLabel() { getLabel() {
const type = this.getRollType(); const type = this.getRollType();
if (type === "skill") { const sel = type === "skill" ? this.#getSkill() : this.#getAbility();
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
return sel?.options[idx]?.dataset?.label ?? "";
}
const sel = this.#getAbility();
const idx = sel?.selectedIndex ?? 0; const idx = sel?.selectedIndex ?? 0;
return sel?.options[idx]?.dataset?.label ?? ""; const raw = sel?.options[idx]?.dataset?.label ?? "";
if (!raw) return "";
const base = type === "skill" ? "SKILLS" : "ABILITIES";
const localized = game.i18n.localize(`${base}.${raw}.name`);
return localized === `${base}.${raw}.name` ? raw : localized;
} }
getSelfControl() { getSelfControl() {
@@ -283,6 +336,19 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
opt.selected = true; opt.selected = true;
difficulty.disabled = true; difficulty.disabled = true;
} }
const handicap = this.#getHandicap();
if (handicap && locks.handicap !== undefined && locks.handicap !== null) {
const locked = parseInt(locks.handicap, 10);
let opt = [...handicap.options].find(o => parseInt(o.value, 10) === locked + 1);
if (!opt) {
// Handicap supérieur aux options (ex: Rapide 3) : option dédiée.
opt = document.createElement("option");
opt.value = String(locked + 1);
opt.textContent = game.i18n.localize("VERMINE.handicap") + ` (${locked})`;
handicap.appendChild(opt);
}
opt.selected = true;
}
} }
#displaySpecialties() { #displaySpecialties() {
@@ -293,7 +359,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
#calculateBonusCount() { #calculateBonusCount() {
const b = this.getPoolBreakdown(); const b = this.getPoolBreakdown();
return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience; return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience - b.malus;
} }
#refreshExperienceUI() { #refreshExperienceUI() {
@@ -340,7 +406,10 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
const handSel = this.#getHandicap(); const handSel = this.#getHandicap();
const handEl = this.#el.querySelector("#current-handicap"); const handEl = this.#el.querySelector("#current-handicap");
if (handEl && handSel) { if (handEl && handSel) {
handEl.textContent = handSel.options[handSel.selectedIndex].text; let text = handSel.options[handSel.selectedIndex].text;
const used = this.getPonctuelUsed();
if (used > 0) text += ` (${game.i18n.localize("VERMINE.ponctuel")} -${used})`;
handEl.textContent = text;
} }
const abilSel = this.#getAbility(); const abilSel = this.#getAbility();
@@ -448,7 +517,23 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
await group.update({ "system.experience.dice": groupPool - pulled }); await group.update({ "system.experience.dice": groupPool - pulled });
} }
await VermineUtils.roll({ // Trait Ponctuel : consomme (n) charges de l'outil sélectionné pour annuler
// les Handicaps ; l'objet perd définitivement le Trait si la valeur tombe à 0.
const ponctuelUsed = this.getPonctuelUsed();
const tool = this.getSelectedTool();
if (tool && ponctuelUsed > 0) {
const current = VermineTraits.traitValue(tool, 'ponctuel');
const remaining = current - ponctuelUsed;
if (remaining <= 0) {
await tool.update({ "system.traits.ponctuel": null });
} else {
await tool.update({ "system.traits.ponctuel.value": remaining });
}
}
const intimidant = this.getIntimidant();
const rollParams = {
actor: this.#actor, actor: this.#actor,
NoD: this.getDicePool(), NoD: this.getDicePool(),
Reroll: this.getReroll(), Reroll: this.getReroll(),
@@ -467,9 +552,17 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
weapon: this.weapon?.toObject() ?? null, weapon: this.weapon?.toObject() ?? null,
targets: [...game.user.targets].map(t => t.name), targets: [...game.user.targets].map(t => t.name),
messageFlags: this.defense ? { "vermine-defense": this.defense } : null messageFlags: this.defense ? { "vermine-defense": this.defense } : null
}); };
if (intimidant) {
rollParams.intimidant = { used: true, psychology: this.getSelectedSkillKey() === 'psychology' };
}
if (ponctuelUsed > 0) rollParams.ponctuelUsed = ponctuelUsed;
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
// en arrière-plan sans bloquer la fermeture.
this.close(); this.close();
await VermineUtils.roll(rollParams);
} }
} }
+125
View File
@@ -0,0 +1,125 @@
import { VermineTotemDice } from "../totem-dice.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue de gestion des Dés de Totems (Humain / Adapté).
* Permet d'appliquer les gains et pertes liés aux Instincts et Interdits
* (1D ou 2D selon la gravité de l'acte), en respectant les limites
* (3 Dés du même Totem, 5 Dés au maximum) et les règles d'échange.
*/
export default class TotemDiceDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#actor;
#serious = false;
static DEFAULT_OPTIONS = {
classes: ["vermine-roll", "totem-dice"],
tag: "form",
window: {
icon: "fas fa-dice-d10",
resizable: false
},
position: {
width: 480,
height: 420
},
actions: {
gainHuman: TotemDiceDialog.#onGainHuman,
gainAdapted: TotemDiceDialog.#onGainAdapted,
loseHuman: TotemDiceDialog.#onLoseHuman,
close: TotemDiceDialog.#onClose
}
};
static PARTS = {
main: { template: "systems/vermine2047/templates/dialogs/totem-dice-dialog.hbs", scrollable: [""] }
};
static async create({ actorId }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
return null;
}
return new TotemDiceDialog({ actor });
}
constructor(options = {}) {
super(options);
this.#actor = options.actor;
}
get title() {
return game.i18n.localize("VERMINE.totem_dice");
}
async _prepareContext() {
const actor = this.#actor;
const influence = VermineTotemDice.getInfluence(actor);
const influenceLabels = {
human: game.i18n.localize("TOTEMS.human.name"),
adapted: game.i18n.localize("TOTEMS.adapted.name"),
neutral: game.i18n.localize("VERMINE.totem_neutral")
};
return {
humanValue: actor.system?.adaptation?.totems?.human?.value ?? 0,
humanMax: actor.system?.adaptation?.totems?.human?.max ?? 3,
adaptedValue: actor.system?.adaptation?.totems?.adapted?.value ?? 0,
adaptedMax: actor.system?.adaptation?.totems?.adapted?.max ?? 3,
influenceLabel: influenceLabels[influence] || "",
serious: this.#serious
};
}
_onRender(context, options) {
super._onRender(context, options);
const checkbox = this.element.querySelector("#totem-serious");
if (checkbox) {
checkbox.checked = this.#serious;
checkbox.addEventListener("change", () => { this.#serious = checkbox.checked; });
}
}
#getCount() {
return this.element.querySelector("#totem-serious")?.checked ? 2 : 1;
}
#notify(result) {
const key = result.totem === "human" ? "TOTEMS.human.name" : "TOTEMS.adapted.name";
const totemName = game.i18n.localize(key);
const parts = [];
if (result.gained) parts.push(game.i18n.localize("VERMINE.totem_gained").replace("{totem}", totemName));
if (result.exchanged) parts.push(game.i18n.localize("VERMINE.totem_exchanged").replace("{totem}", totemName));
if (result.replaced) parts.push(game.i18n.localize("VERMINE.totem_replaced").replace("{totem}", totemName));
if (result.lost) parts.push(game.i18n.localize("VERMINE.totem_lost"));
if (result.gainedAdapted) parts.push(game.i18n.localize("VERMINE.totem_gained_adapted"));
if (result.refused) parts.push(game.i18n.localize("VERMINE.totem_limit_reached"));
const text = parts.join(" ");
if (result.refused && !result.gained && !result.lost && !result.exchanged && !result.replaced) {
ui.notifications.warn(text || game.i18n.localize("VERMINE.totem_limit_reached"));
} else if (text) {
ui.notifications.info(text);
}
this.render();
}
static async #onGainHuman() {
const result = await VermineTotemDice.gainDie(this.#actor, "human", this.#getCount());
this.#notify(result);
}
static async #onGainAdapted() {
const result = await VermineTotemDice.gainDie(this.#actor, "adapted", this.#getCount());
this.#notify(result);
}
static async #onLoseHuman() {
const result = await VermineTotemDice.loseDie(this.#actor, "human", this.#getCount());
this.#notify(result);
}
static #onClose() {
this.close();
}
}
+136 -19
View File
@@ -204,20 +204,52 @@ export class VermineExchange {
return Boolean(actor) && (game.user.isGM || actor.isOwner) return Boolean(actor) && (game.user.isGM || actor.isOwner)
} }
/** Ordre de gravité des blessures (croissant). */
static WOUND_ORDER = ['minorWound', 'majorWound', 'deadlyWound']
/** Malus de la pire blessure (règles p.42) : -1D Légère, -2D Grave, -3D Mortelle. */
static WOUND_MALUS = { minorWound: 1, majorWound: 2, deadlyWound: 3 }
/** /**
* Applique +1 dans la catégorie de blessure indiquée (plafonné au max). * Malus de blessure applicable à toutes les actions : seul le Malus de la
* Blessure la plus grave compte (non cumulatif).
* @param {Actor} actor
* @returns {number} 0, 1, 2 ou 3
*/
static woundMalus(actor) {
if (!actor?.system) return 0
if (actor.system.deadlyWound?.value > 0) return 3
if (actor.system.majorWound?.value > 0) return 2
if (actor.system.minorWound?.value > 0) return 1
return 0
}
/**
* Applique +1 cercle de blessure, en montant en gravité si les cercles du
* niveau correspondant sont déjà pleins (règles p.42 : si tous les cercles
* d'une Blessure sont cochés, la suivante est de gravité supérieure).
* @param {Actor} actor * @param {Actor} actor
* @param {string} woundCategory 'minorWound' | 'majorWound' | 'deadlyWound' * @param {string} woundCategory 'minorWound' | 'majorWound' | 'deadlyWound'
* @returns {Promise<number|boolean>} nouvelle valeur, ou false si refusé * @returns {Promise<Object|boolean|null>}
* - false si refusé (permissions) ou null si invalide
* - { category, value, escalated, death } si appliqué (death si tout est plein)
*/ */
static async applyWound(actor, woundCategory) { static async applyWound(actor, woundCategory) {
if (!actor || !woundCategory) return null if (!actor || !woundCategory) return null
if (!this.canApplyWound(actor)) return false if (!this.canApplyWound(actor)) return false
const current = actor.system?.[woundCategory]?.value || 0 const startIdx = this.WOUND_ORDER.indexOf(woundCategory)
const max = actor.system?.[woundCategory]?.max ?? Number.POSITIVE_INFINITY if (startIdx === -1) return null
const next = Math.min(current + 1, max) for (let i = startIdx; i < this.WOUND_ORDER.length; i++) {
await actor.update({ [`system.${woundCategory}.value`]: next }) const cat = this.WOUND_ORDER[i]
return next const sys = actor.system?.[cat]
const max = sys?.max ?? Number.POSITIVE_INFINITY
const value = sys?.value || 0
if (value < max) {
await actor.update({ [`system.${cat}.value`]: value + 1 })
return { category: cat, value: value + 1, escalated: cat !== woundCategory, death: false }
}
}
return { category: woundCategory, value: null, escalated: false, death: true }
} }
// ── Résolution ────────────────────────────────────────────────────── // ── Résolution ──────────────────────────────────────────────────────
@@ -337,6 +369,12 @@ export class VermineExchange {
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => { html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
btn.addEventListener('click', ev => this.#onApply(ev, message)) btn.addEventListener('click', ev => this.#onApply(ev, message))
}) })
html.querySelectorAll('.exchange-incapacitant').forEach(btn => {
btn.addEventListener('click', ev => this.#onIncapacitant(ev, message))
})
html.querySelectorAll('.exchange-zone').forEach(btn => {
btn.addEventListener('click', ev => this.#onApplyZone(ev, message))
})
} }
static async #onDefense(ev, attackMessage) { static async #onDefense(ev, attackMessage) {
@@ -375,7 +413,7 @@ export class VermineExchange {
const dialog = await NpcRollDialog.create({ const dialog = await NpcRollDialog.create({
actor, actor,
rolltype: 'action', rolltype: 'action',
locks: { difficulty: exchange.difficulty }, locks: { difficulty: exchange.difficulty, handicap: exchange.rapideHandicap ?? null },
defense defense
}) })
if (dialog) dialog.render(true) if (dialog) dialog.render(true)
@@ -394,14 +432,16 @@ export class VermineExchange {
} }
static #defenseLocks(actor, exchange, type) { static #defenseLocks(actor, exchange, type) {
if (type === 'parade') { const locks = type === 'parade'
return { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty } ? { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
} : {
return { ability: 'reflexes',
ability: 'reflexes', skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
skill: exchange.mode === 'ranged' ? 'alertness' : 'close', difficulty: exchange.difficulty
difficulty: exchange.difficulty }
} // Trait Rapide (n) : le défenseur subit (n) Handicaps sur sa défense.
if (exchange.rapideHandicap) locks.handicap = exchange.rapideHandicap
return locks
} }
static #parrySkill(actor) { static #parrySkill(actor) {
@@ -447,6 +487,75 @@ export class VermineExchange {
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML }) if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
} }
static async #onIncapacitant(ev) {
ev.preventDefault()
ev.stopPropagation()
const btn = ev.currentTarget
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
if (!actor) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
return
}
if (!game.user.isGM && !actor.isOwner) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return
}
const value = parseInt(btn.dataset.value, 10) || 7
if (actor.type === 'character') {
const { default: RollDialog } = await import('./dialogs/rollDialog.mjs')
const dialog = await RollDialog.create({
actor,
rolltype: 'ability',
label: 'health',
locks: { ability: 'health', difficulty: value }
})
if (dialog) dialog.render(true)
return
}
const { default: NpcRollDialog } = await import('./dialogs/npcRollDialog.mjs')
const dialog = await NpcRollDialog.create({
actor,
rolltype: 'action',
locks: { difficulty: value }
})
if (dialog) dialog.render(true)
}
/**
* Trait Zone (n) : applique la blessure résolue à toutes les cibles de
* l'attaque présentes dans la zone (confort pour le MJ).
*/
static async #onApplyZone(ev, attackMessage) {
ev.preventDefault()
ev.stopPropagation()
const btn = ev.currentTarget
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
if (!exchange) return
if (!game.user.isGM && !attackMessage.isOwner) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return
}
if (!exchange.targets?.length) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_target'))
return
}
let applied = 0
for (const target of exchange.targets) {
const r = await this.resolve(attackMessage, target?.id ?? null)
if (!r?.hit || !r.woundCategory) continue
const defender = await this.#resolveDefender({ id: r.defenderId, uuid: r.defenderUuid })
if (!defender || !this.canApplyWound(defender)) continue
const result = await this.applyWound(defender, r.woundCategory)
if (result && result !== false) applied += 1
}
btn.disabled = true
btn.innerHTML = `<i class="fas fa-check"></i> ${game.i18n.format('VERMINE.zone_applied', { count: applied })}`
if (game.user.isGM || attackMessage.isOwner) {
const msgEl = btn.closest('.vermine-roll-message')
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
}
}
static async #onApply(ev, attackMessage) { static async #onApply(ev, attackMessage) {
ev.preventDefault() ev.preventDefault()
const btn = ev.currentTarget const btn = ev.currentTarget
@@ -457,10 +566,18 @@ export class VermineExchange {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission')) ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
return return
} }
const next = await this.applyWound(defender, wound) const result = await this.applyWound(defender, wound)
if (next === false) return if (result === false) return
btn.disabled = true btn.disabled = true
btn.innerHTML = `<i class="fas fa-check"></i> ${game.i18n.localize('VERMINE.wound_applied')}` let label = game.i18n.localize('VERMINE.wound_applied')
if (result?.death) {
label = game.i18n.localize('VERMINE.wound_death')
} else if (result?.escalated) {
const from = game.i18n.localize(this.woundLabelKey(wound))
const to = game.i18n.localize(this.woundLabelKey(result.category))
label = game.i18n.format('VERMINE.wound_escalated', { from, to })
}
btn.innerHTML = `<i class="fas fa-check"></i> ${label}`
// Persiste l'état du bouton seulement si l'utilisateur peut modifier le message // Persiste l'état du bouton seulement si l'utilisateur peut modifier le message
if (game.user.isGM || attackMessage.isOwner) { if (game.user.isGM || attackMessage.isOwner) {
const msgEl = btn.closest('.vermine-roll-message') const msgEl = btn.closest('.vermine-roll-message')
+148
View File
@@ -123,6 +123,76 @@ export class VermineExperience {
return 0 return 0
} }
/**
* Un de Totem Adapté peut remplacer un d'Expérience (compte ×2)
* uniquement si le personnage possède strictement plus de Dés Adaptés
* que Humains.
* @param {number} human human totem dice
* @param {number} adapted adapted totem dice
* @returns {boolean}
*/
static canUseAdaptedDie(human, adapted) {
return adapted > human
}
/**
* Difficulté d'un jet d'apprentissage de Capacité de Totem : valeur
* d'Apprentissage de la capacité.
* @param {object} abilityData system data de l'item ability
* @returns {number}
*/
static totemAbilityLearningDifficulty(abilityData) {
return parseInt(abilityData?.learn?.threshold, 10) || 5
}
/**
* Handicap d'un jet d'apprentissage de Capacité de Totem : valeur
* d'Apprentissage de la capacité.
* @param {object} abilityData system data de l'item ability
* @returns {number}
*/
static totemAbilityLearningHandicap(abilityData) {
return parseInt(abilityData?.learn?.hindrance, 10) || 0
}
/**
* Difficulté d'un jet d'obtention d'un Élément d'Historique.
* @returns {number}
*/
static backgroundLearningDifficulty() {
return 9
}
/**
* Handicap d'un jet d'obtention d'un Élément d'Historique : (I) si
* l'Historique est listé avec un astérisque.
* @param {boolean} asterisk
* @returns {number}
*/
static backgroundLearningHandicap(asterisk) {
return asterisk ? 1 : 0
}
/**
* Difficulté d'un jet visant à réduire le score de Réputation (devenir
* plus célèbre) : 10 moins le score que le personnage souhaite atteindre.
* @param {number} current réputation actuelle
* @returns {number}
*/
static reputationReduceDifficulty(current) {
return 10 - Math.max(0, (parseInt(current, 10) || 0) - 1)
}
/**
* Difficulté d'un jet visant à augmenter le score de Réputation (se faire
* oublier) : égale à la Réputation actuelle.
* @param {number} current réputation actuelle
* @returns {number}
*/
static reputationIncreaseDifficulty(current) {
return parseInt(current, 10) || 0
}
/** /**
* Clé i18n du libellé du niveau de compétence visé. * Clé i18n du libellé du niveau de compétence visé.
* @param {number} level * @param {number} level
@@ -143,4 +213,82 @@ export class VermineExperience {
const preferred = actor.system?.skill_categories?.preferred const preferred = actor.system?.skill_categories?.preferred
return Boolean(category && preferred && category === preferred) return Boolean(category && preferred && category === preferred)
} }
/**
* Réinitialise les limites d'apprentissage (learned.*) de la phase
* courante, pour un personnage ou un Groupe, sans toucher au compteur
* de phase (avancé uniquement par le dialogue d'attribution du MJ).
* @param {foundry.abstract.Document} actor personnage ou Groupe
* @returns {Promise<void>}
*/
static async resetPhaseLimits(actor) {
const ex = actor.system?.experience
if (!ex) return
const update = {}
if (ex.learned && typeof ex.learned === "object") {
for (const key of Object.keys(ex.learned)) {
if (key === "phase") continue
update[`system.experience.learned.${key}`] = false
}
}
if (Object.keys(update).length) await actor.update(update)
}
/**
* Mode de jeu actif (1=Survie, 2=Cauchemar, 3=Apocalypse).
* @returns {number}
*/
static gameMode() {
return parseInt(game.settings.get("vermine2047", "game-mode"), 10) || 1
}
/**
* Total de Dés d'Évolution maximum selon le Mode de jeu.
* @param {number} mode 1=Survie, 2=Cauchemar, 3=Apocalypse
* @returns {number}
*/
static evolutionMaxForMode(mode) {
switch (parseInt(mode, 10)) {
case 2: return 6
case 3: return 7
default: return 5
}
}
/**
* Somme des niveaux des Adaptations/Mutations déjà possédées.
* @param {foundry.abstract.Document} actor personnage
* @returns {number}
*/
static evolutionTotalLevels(actor) {
return (actor?.itemTypes?.evolution || []).reduce((sum, item) => sum + (item.system?.level?.value || 0), 0)
}
/**
* Valide l'achat d'une Adaptation/Mutation contre les Dés d'Évolution
* accumulés. Retourne une clé i18n d'erreur, ou null si l'achat est permis.
* Règles p. 116-117 : coût = niveau ; total limité par le Mode ; Mutations
* 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.
* @param {foundry.abstract.Document} actor personnage
* @param {number} cost niveau de l'Adaptation/Mutation (1-4)
* @param {boolean} mutation vrai si c'est une Mutation
* @returns {string|null}
*/
static validateEvolutionPurchase(actor, cost, mutation) {
const level = parseInt(cost, 10) || 0
const accumulated = actor.system?.experience?.evolution || 0
const mode = this.gameMode()
if (accumulated < level) return "VERMINE.error_evolution_not_enough_dice"
if (level < 1 || level > 4) return "VERMINE.error_evolution_invalid_level"
const total = this.evolutionTotalLevels(actor) + level
if (total > this.evolutionMaxForMode(mode)) return "VERMINE.error_evolution_mode_limit"
if (mutation && level >= 3 && mode < 2) return "VERMINE.error_evolution_mutation_mode"
if (mutation && level === 4) {
const hasFourDMutation = (actor?.itemTypes?.evolution || []).some(
(item) => item.system?.mutation && (item.system?.level?.value || 0) === 4
)
if (hasFourDMutation) return "VERMINE.error_evolution_mutation_4d"
}
return null
}
} }
+69 -9
View File
@@ -210,19 +210,29 @@ export class VermineCombat extends Combat {
} }
/************************************************************************************/ /************************************************************************************/
_onDelete() { /**
/*let combatants = this.combatants.contents * Nettoyage lors de la suppression du combat (bouton « Terminer » / suppression).
for (let c of combatants) { * Doit appeler le handler de base : c'est lui qui re-rend le CombatTracker
let actor = game.actors.get(c.actorId) * (`ui.combat.render({combat: null})`) sans cela le tracker reste figé sur
actor.clearInitiative() * l'ancien ordre de tour jusqu'au rechargement de la page (F5).
actor.displayRecuperation() * @param {object} options
} * @param {string} userId
super._onDelete()*/ */
_onDelete(options, userId) {
super._onDelete(options, userId);
} }
} }
export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
/** @override */
static DEFAULT_OPTIONS = {
actions: {
deleteCombatant: VermineCombatTracker.#onDeleteCombatant,
deleteCombat: VermineCombatTracker.#onDeleteCombat
}
};
/** @override */ /** @override */
static PARTS = { static PARTS = {
main: { main: {
@@ -248,13 +258,17 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
Object.assign(context, { Object.assign(context, {
combat, combat,
hasCombat, hasCombat,
combats: combats.map((c, i) => ({ id: c.id, name: c.name, label: i + 1, active: i === currentIdx })),
combatCount: combats.length, combatCount: combats.length,
currentIndex: currentIdx + 1, currentIndex: currentIdx + 1,
previousId: combats[currentIdx - 1]?.id, previousId: combats[currentIdx - 1]?.id,
nextId: combats[currentIdx + 1]?.id, nextId: combats[currentIdx + 1]?.id,
round: combat?.round, round: combat?.round,
displayCycle: combats.length > 7,
control: isPlayerTurn && canControl, control: isPlayerTurn && canControl,
linked: combat?.scene !== null, linked: combat?.scene !== null,
initiativeIcon: CONFIG.Combat.initiativeIcon,
css: combats.length > 7 ? "cycle" : combats.length ? "tabbed" : "",
cssClass: combats.length > 7 ? "cycle" : combats.length ? "tabbed" : "", cssClass: combats.length > 7 ? "cycle" : combats.length ? "tabbed" : "",
cssId: "combat", cssId: "combat",
tabName: "combat", tabName: "combat",
@@ -264,12 +278,21 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
turns: [] turns: []
}); });
// Tracker part : construit les tours + gestion des décimales d'initiative.
let hasDecimals = false;
if ( combat ) { if ( combat ) {
for ( const [i, combatant] of combat.turns.entries() ) { for ( const [i, combatant] of combat.turns.entries() ) {
if ( !combatant.visible ) continue; if ( !combatant.visible ) continue;
context.turns.push(await this._prepareTurnContext(combat, combatant, i)); const turn = await this._prepareTurnContext(combat, combatant, i);
if ( turn.hasDecimals ) hasDecimals = true;
context.turns.push(turn);
} }
} }
const precision = CONFIG.Combat.initiative.decimals;
context.turns.forEach(t => {
if ( Number.isFinite(t.initiative) ) t.initiative = t.initiative.toFixed(hasDecimals ? precision : 0);
});
context.hasDecimals = hasDecimals;
return context; return context;
} }
@@ -318,6 +341,31 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
let actor = await game.actors.get(combatant.actorId) let actor = await game.actors.get(combatant.actorId)
// combatant attitude set // combatant attitude set
} }
/**
* Supprime un combatant du combat en cours.
* @param {PointerEvent} event
* @param {HTMLElement} target
*/
static async #onDeleteCombatant(event, target) {
const combatantId = target.closest("[data-combatant-id]")?.dataset?.combatantId
const combatant = this.viewed?.combatants.get(combatantId)
if (!combatant) return
return combatant.delete()
}
/**
* Supprime le combat affiché (bouton poubelle de l'en-tête).
* @param {PointerEvent} event
* @param {HTMLElement} target
*/
static async #onDeleteCombat() {
const combat = this.viewed
if (!combat) return
const deleted = await combat.delete()
this.viewed = null
return deleted
}
} }
export class VermineCombatant extends Combatant { export class VermineCombatant extends Combatant {
@@ -330,6 +378,18 @@ export class VermineCombatant extends Combatant {
} }
} }
_getInitiativeFormula() { _getInitiativeFormula() {
// Personnage : jet de Réaction (Réflexes + Vigilance) contre la
// Difficulté du statut de combat — les Réussites = valeur de Réaction.
// PNJ et Créature : n'ont pas de caractéristiques/compétences — ils
// roulent leur pool de Réaction (computed.reaction) contre la même
// Difficulté, majoré de leur bonus de Réaction.
const actor = this.actor
if ( actor?.type === 'npc' || actor?.type === 'creature' ) {
const reaction = parseInt(actor.system?.computed?.reaction, 10) || 0
const difficulty = parseInt(actor.system?.combatStatus?.difficulty, 10) || 7
const bonus = parseInt(actor.system?.computed?.reactionBonus, 10) || 0
return `${reaction}d10cs>=${difficulty}+${bonus}`
}
return String(CONFIG.Combat.initiative.formula || game.system.initiative) return String(CONFIG.Combat.initiative.formula || game.system.initiative)
} }
} }
+14 -2
View File
@@ -98,8 +98,9 @@ export const registerHandlebarsHelpers = function () {
}); });
// search translation with variables // search translation with variables
Handlebars.registerHelper('smarttl', function (arrayLabel, objectLabel, options) { Handlebars.registerHelper('smarttl', function (arrayLabel, objectLabel, key) {
return game.i18n.localize(arrayLabel + "." + objectLabel + ".name"); if (!key || typeof key !== 'string') key = 'name';
return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key);
}); });
// Return an object of skills ordered alphabetically by their localized // Return an object of skills ordered alphabetically by their localized
@@ -367,6 +368,17 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('ifStartsWith', function (arg1, arg2, options) { Handlebars.registerHelper('ifStartsWith', function (arg1, arg2, options) {
return (typeof arg1 === 'string' && arg1.startsWith(arg2)) ? options.fn(this) : options.inverse(this); return (typeof arg1 === 'string' && arg1.startsWith(arg2)) ? options.fn(this) : options.inverse(this);
}); });
// Chiffre → chiffre romain (handicaps, entraves). 0 → chaîne vide.
Handlebars.registerHelper('roman', function (n) {
const table = [[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']]
let num = Number(n)
if (!Number.isFinite(num) || num <= 0) return ''
let out = ''
for (const [v, s] of table) {
while (num >= v) { out += s; num -= v }
}
return out
});
//math operations //math operations
+9
View File
@@ -131,6 +131,15 @@ export const registerHooks = function () {
} }
}); });
// Un Groupe démarre avec une Réserve à son maximum (3D + 1D par membre).
Hooks.on("createActor", async function (actor) {
if (actor.type !== "group") return;
const reserve = actor.system?.reserve;
if (reserve && reserve.value === 0 && reserve.max > 0) {
await actor.update({ "system.reserve.value": reserve.max });
}
});
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Combat Hooks */ /* Combat Hooks */
/* -------------------------------------------- */ /* -------------------------------------------- */
+326
View File
@@ -0,0 +1,326 @@
import { VermineUtils } from './roll.mjs';
/**
* Gestion de la Fiabilité du matériel (regles/regles_materiel.txt).
*
* Test de Fiabilité (Dommages matériels) : selon la gravité (Mineurs 1D,
* Graves 3D, Critiques 5D), on lance les dés sans Relance ni Réserve ;
* chaque >= Fiabilité inflige 1 Dommage (réduit la Fiabilité de 1).
* À 0, l'équipement est détruit ou inutilisable.
*
* Réparations : jet de Précision + Compétence contre la Fiabilité d'origine
* (classique) ou de Précision + Bricolage contre la Fiabilité actuelle
* (de fortune). Chaque Réussite rend 1 point de Fiabilité (classique) ;
* la réparation de fortune rend l'objet fonctionnel sans rendre de point.
*/
export class VermineReliability {
/** Gravités et nombre de dés de Dommages (p. 37). */
static SEVERITIES = { mineurs: 1, graves: 3, critiques: 5 };
/** Handicap de réparation selon les Dommages subis (Mineure I, Grave II, Critique III). */
static #PANNE_HANDICAP = { 1: 1, 2: 1, 3: 2, 4: 2, 5: 3 };
/** Compétences de réparation classique (regles_materiel p. 39). */
static REPAIR_SKILLS = ['crafting', 'mecanical', 'technology', 'armory'];
/**
* Handicap de la panne courante (0 si intact).
* @param {Item} item
* @returns {number}
*/
static panneHandicap(item) {
const value = parseInt(item.system?.damages?.value ?? 0, 10) || 0;
return this.#PANNE_HANDICAP[value] || 0;
}
/**
* État courant dérivé des Dommages subis (tableau `damages.state`).
* @param {Item} item
* @returns {string}
*/
static panneState(item) {
const damages = item.system?.damages;
const value = parseInt(damages?.value ?? 0, 10) || 0;
if (value <= 0 || !damages?.state?.[value - 1]) return '';
return game.i18n.localize(`VERMINE.damages_state_${value}`);
}
/**
* Test de Fiabilité : jet de Dommages matériels.
* @param {Item} item
* @param {Object} opts
* @param {string} opts.severity "mineurs" | "graves" | "critiques"
* @param {boolean} opts.immersion si vrai, le Trait Étanche peut annuler les Dommages
* @returns {Promise<Object|null>} Résultat (null si impossible)
*/
static async test(item, { severity = 'graves', immersion = false } = {}) {
if (!item) return null;
const diceCount = this.SEVERITIES[severity] ?? 3;
const current = parseInt(item.system?.reliability ?? 0, 10) || 0;
const traits = item.system?.traits ?? {};
const damagesValue = parseInt(item.system?.damages?.value ?? 0, 10) || 0;
if (current <= 0) {
ui.notifications.warn(game.i18n.localize('VERMINE.reliability_already_destroyed'));
return { destroyed: true, dommages: 0 };
}
// Jet des dés de Dommages (sans Relance ni Réserve)
const roll = new Roll(`${diceCount}d10`);
await roll.evaluate();
await VermineUtils.showDiceSoNice(roll);
const results = (roll.dice ?? []).flatMap((d) => (d.results ?? []).map((r) => r.result ?? 0));
const traitsNotes = [];
// Trait Étanche : ignore les Dommages d'immersion tant que la Fiabilité
// est à son maximum (objet sans Dommage cumulé).
let dommages = results.filter((r) => r >= current).length;
if (immersion && traits.etanche && damagesValue === 0) {
dommages = 0;
traitsNotes.push(game.i18n.localize('VERMINE.reliability_etanche'));
}
// Trait Coque (n) : ignore les (n) premiers Dommages puis s'épuise.
// La coque épuisée est stockée { value: 0, max: n } : réparable (Phase 1).
let coqueValue = parseInt(traits.coque?.value ?? 0, 10) || 0;
const coqueInit = parseInt(traits.coque?.max ?? traits.coque?.value ?? 0, 10) || 0;
const updates = {};
if (coqueValue > 0 && dommages > 0) {
const absorbed = Math.min(coqueValue, dommages);
dommages -= absorbed;
coqueValue -= absorbed;
if (coqueValue > 0) {
updates['system.traits.coque.value'] = coqueValue;
traitsNotes.push(game.i18n.format('VERMINE.reliability_coque_absorbed', { absorbed, remaining: coqueValue }));
} else {
updates['system.traits.coque'] = { value: 0, max: coqueInit };
traitsNotes.push(game.i18n.format('VERMINE.reliability_coque_lost', { max: coqueInit }));
}
}
// Trait Cassant : toutes les pertes de Fiabilité sont majorées d'1 point.
const cassant = !!traits.cassant;
if (cassant && dommages > 0) {
dommages += 1;
traitsNotes.push(game.i18n.localize('VERMINE.reliability_cassant'));
}
const newReliability = Math.max(0, current - dommages);
const newDamagesValue = Math.min(5, damagesValue + dommages);
const destroyed = newReliability === 0;
updates['system.reliability'] = newReliability;
updates['system.damages.value'] = newDamagesValue;
await item.update(updates);
// État final affiché sur la carte.
let state = '';
if (destroyed) {
state = game.i18n.localize('VERMINE.reliability_destroyed');
} else if (newDamagesValue > 0) {
state = game.i18n.localize(`VERMINE.damages_state_${newDamagesValue}`);
} else {
state = '';
}
// Ne marque comme Dommage que les dés réellement comptés (après
// absorption Étanche/Coque) : les dés absorbés restent affichés mais
// sans le style "damage".
const candidates = results.map((v, i) => ({ v, i })).filter((o) => o.v >= current);
const damageIdx = new Set(candidates.slice(0, Math.max(0, dommages)).map((o) => o.i));
const content = await foundry.applications.handlebars.renderTemplate(
'systems/vermine2047/templates/reliability-test.hbs',
{
item: { name: item.name, img: item.img },
severityLabel: game.i18n.localize(`VERMINE.reliability_severity_${severity}`),
diceCount,
oldReliability: current,
newReliability,
dommages,
results: results.map((v, i) => ({ value: v, isDamage: damageIdx.has(i) })),
state,
destroyed,
traitsNotes,
},
);
await ChatMessage.create({
user: game.user?._id,
speaker: this.#getSpeaker(item),
content,
});
return { dommages, newReliability, destroyed };
}
/**
* Réparation classique, de fortune ou de coque.
* @param {Item} item
* @param {Object} opts
* @param {string} opts.mode "classique" | "fortune" | "coque"
* @param {string} opts.skill Compétence de réparation classique (clé de skills)
* @returns {Promise<Object|null>}
*/
static async repair(item, { mode = 'classique', skill = 'technology' } = {}) {
const actor = item?.actor;
if (!item || !actor) {
ui.notifications.warn(game.i18n.localize('VERMINE.reliability_need_actor'));
return null;
}
const traits = item.system?.traits ?? {};
const current = parseInt(item.system?.reliability ?? 0, 10) || 0;
const damagesValue = parseInt(item.system?.damages?.value ?? 0, 10) || 0;
const handicap = this.panneHandicap(item);
// Trait Pratique : +1 Handicap sur toutes les réparations.
const pratique = !!traits.pratique;
const pratiqueHandicap = pratique ? 1 : 0;
const pratiqueNote = pratique ? game.i18n.localize('VERMINE.reliability_pratique') : '';
const isCoque = mode === 'coque';
const coqueMax = isCoque ? parseInt(traits.coque?.max ?? 0, 10) || 0 : 0;
// Difficulté : Fiabilité d'origine (≈ actuelle + Dommages subis) pour la
// réparation classique ; Fiabilité actuelle pour la réparation de fortune ;
// valeur maximale du Trait pour la réparation de coque.
let difficulty = current;
if (isCoque) difficulty = coqueMax;
else if (mode === 'classique') difficulty = Math.min(10, current + damagesValue);
const precision = parseInt(actor.system?.abilities?.precision?.value ?? 0, 10) || 0;
const skillValue = mode === 'fortune'
? parseInt(actor.system?.skills?.diy?.value ?? 0, 10) || 0
: parseInt(actor.system?.skills?.[skill]?.value ?? 0, 10) || 0;
const NoD = Math.max(0, precision + skillValue);
// Réparation de fortune (p. 39) : si le personnage possède la Compétence
// requise, +1D sur le jet ; sinon, Handicap de Rareté de cette Compétence
// (le Bonus de Bricolage s'applique dans les deux cas).
const skillRarity = parseInt(actor.system?.skills?.[skill]?.rarity ?? 0, 10) || 0;
const hasSkill = (parseInt(actor.system?.skills?.[skill]?.value ?? 0, 10) || 0) > 0;
const fortuneBonus = mode === 'fortune' && hasSkill ? 1 : 0;
const fortuneHandicap = mode === 'fortune' && !hasSkill ? skillRarity : 0;
let fortuneNote = '';
if (mode === 'fortune') {
if (hasSkill) {
fortuneNote = game.i18n.format('VERMINE.reliability_fortune_skill_bonus', { skill: game.i18n.localize(`SKILLS.${skill}.name`) });
} else if (skillRarity > 0) {
fortuneNote = game.i18n.format('VERMINE.reliability_fortune_skill_missing', { skill: game.i18n.localize(`SKILLS.${skill}.name`), rarity: skillRarity });
}
}
// Trait Bien conçu : Réussite automatique sur les Réparations classiques.
const bienConcu = !!traits.bien_conçu;
const autoSuccess = bienConcu && mode === 'classique';
let bienConcuNote = '';
// Trait Bien conçu (fortune) : la 1ʳᵉ réparation de fortune réussie rend
// 1 point de Fiabilité (compteur traits.bien_conçu.value).
const bienConcuUsed = bienConcu && mode === 'fortune' && (parseInt(traits.bien_conçu?.value ?? 0, 10) || 0) > 0;
const bienConcuBonus = bienConcu && mode === 'fortune' && !bienConcuUsed;
// Réparation de coque : pas de panne à réparer, Difficulté = max du Trait.
const required = 1 + (isCoque ? 0 : handicap) + (isCoque ? 0 : fortuneHandicap) + pratiqueHandicap;
const roll = autoSuccess ? null : new Roll(`${NoD + fortuneBonus}d10cs>=${difficulty}`);
if (roll) {
await roll.evaluate();
await VermineUtils.showDiceSoNice(roll);
}
const results = roll
? (roll.dice ?? []).flatMap((d) => (d.results ?? []).map((r) => ({
value: r.result ?? 0,
isSuccess: r.success,
})))
: [];
const successes = roll?._total ?? required;
const success = autoSuccess || successes >= required;
let gained = 0;
let fortuneFixed = false;
let coqueRestored = false;
let newReliability = null;
let resultLabel = '';
if (success) {
if (isCoque) {
coqueRestored = true;
resultLabel = game.i18n.format('VERMINE.reliability_coque_restored', { max: coqueMax });
} else if (mode === 'classique') {
gained = autoSuccess ? damagesValue : successes;
newReliability = Math.min(10, current + gained);
resultLabel = game.i18n.format('VERMINE.reliability_gained', { gained });
if (autoSuccess) {
bienConcuNote = game.i18n.localize('VERMINE.reliability_bien_concu');
}
} else {
fortuneFixed = true;
if (bienConcuBonus) {
gained = 1;
newReliability = Math.min(10, current + 1);
bienConcuNote = game.i18n.localize('VERMINE.reliability_bien_concu_fortune');
}
resultLabel = game.i18n.localize('VERMINE.reliability_fixed');
}
} else {
resultLabel = game.i18n.localize('VERMINE.reliability_failed');
}
const updates = {};
if (isCoque && coqueRestored) {
updates['system.traits.coque'] = { value: coqueMax, max: coqueMax };
} else if (mode === 'classique' && gained > 0) {
updates['system.reliability'] = newReliability;
updates['system.damages.value'] = Math.max(0, damagesValue - gained);
} else if (mode === 'fortune' && fortuneFixed) {
updates['system.damages.value'] = 0;
if (bienConcuBonus) {
updates['system.reliability'] = newReliability;
updates['system.traits.bien_conçu'] = { value: 1 };
}
}
if (Object.keys(updates).length) await item.update(updates);
const skillLabel = mode === 'fortune'
? game.i18n.localize('SKILLS.diy.name')
: game.i18n.localize(`SKILLS.${skill}.name`);
const modeLabel = isCoque
? game.i18n.localize('VERMINE.reliability_repair_coque_label')
: game.i18n.localize(mode === 'classique' ? 'VERMINE.reliability_repair_classic' : 'VERMINE.reliability_repair_fortune');
const content = await foundry.applications.handlebars.renderTemplate(
'systems/vermine2047/templates/reliability-repair.hbs',
{
item: { name: item.name, img: item.img },
modeLabel,
skillLabel,
difficulty,
handicap: isCoque ? 0 : handicap,
pratiqueHandicap,
results,
successes,
required,
resultLabel,
autoSuccess,
fortuneNote,
bienConcuNote,
pratiqueNote,
},
);
await ChatMessage.create({
user: game.user?._id,
speaker: this.#getSpeaker(item),
content,
});
return { success, gained, successes, required };
}
static #getSpeaker(item) {
return ChatMessage.getSpeaker({ actor: item?.actor ?? null });
}
}
+38 -68
View File
@@ -1,3 +1,5 @@
import { VermineTotemDice } from "./totem-dice.mjs";
export class VermineUtils { export class VermineUtils {
/** /**
* Rolls dice with Vermine2047-specific rules. * Rolls dice with Vermine2047-specific rules.
@@ -14,6 +16,10 @@ export class VermineUtils {
* @param {string} [options.keepTotem=null] - Totem to keep ('human' or 'adapted') * @param {string} [options.keepTotem=null] - Totem to keep ('human' or 'adapted')
* @param {number} [options.skillLevel=null] - Skill level for auto-successes * @param {number} [options.skillLevel=null] - Skill level for auto-successes
* @param {boolean} [options.hasSpecialty=false] - Whether a specialty is used * @param {boolean} [options.hasSpecialty=false] - Whether a specialty is used
* @param {number} [options.effectiveRange=null] - Portée effective (Trait Portée)
* @param {number} [options.mobilityHandicap=0] - Handicap de Mobilité (note)
* @param {number} [options.ponctuelUsed=0] - Handicaps annulés (Trait Ponctuel)
* @param {Object|null} [options.intimidant=null] - Trait Intimidant utilisé
* @returns {Promise<Roll>} The roll result * @returns {Promise<Roll>} The roll result
*/ */
static async roll({ static async roll({
@@ -31,12 +37,17 @@ export class VermineUtils {
hasSpecialty = false, hasSpecialty = false,
handicap = 0, handicap = 0,
bonusSuccesses = 0, bonusSuccesses = 0,
bonusOnSuccess = false,
poolBreakdown = null, poolBreakdown = null,
specialtyName = null, specialtyName = null,
weapon = null, weapon = null,
targets = [], targets = [],
attack = null, attack = null,
messageFlags = null messageFlags = null,
effectiveRange = null,
mobilityHandicap = 0,
ponctuelUsed = 0,
intimidant = null
}) { }) {
// Validate inputs // Validate inputs
if (!actor) { if (!actor) {
@@ -49,12 +60,12 @@ export class VermineUtils {
// Declare variables // Declare variables
let formula = ""; let formula = "";
let modFormula = null; let modFormula = null;
let totemBonus = { human: 0, adapted: 0 };
// Calculate domain bonuses for totems // Influence des Dés de Totems : Bonus/Malus de 1D selon le Totem dominant
if (skillCategory) { // et le Domaine de la Compétence (règles p. 121). S'applique même sans
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor); // utiliser un Dé de Totem.
} const influenceMod = this._calculateTotemInfluence(skillCategory, actor);
NoD += influenceMod;
// Apply automatic successes from skill mastery // Apply automatic successes from skill mastery
let adjustedDifficulty = difficulty; let adjustedDifficulty = difficulty;
@@ -65,11 +76,6 @@ export class VermineUtils {
const humanDifficulty = adjustedDifficulty; const humanDifficulty = adjustedDifficulty;
const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`; const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`;
// Apply domain bonus/malus
if (totemBonus.human !== 0) {
NoD += totemBonus.human;
}
modFormula = humanFormula; modFormula = humanFormula;
} }
@@ -79,11 +85,6 @@ export class VermineUtils {
const adaptedDifficulty = adjustedDifficulty; const adaptedDifficulty = adjustedDifficulty;
const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`; const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`;
// Apply domain bonus/malus
if (totemBonus.adapted !== 0) {
NoD += totemBonus.adapted;
}
// Build combined formula // Build combined formula
if (modFormula !== null) { if (modFormula !== null) {
modFormula = `${modFormula}+${adaptedFormula}`; modFormula = `${modFormula}+${adaptedFormula}`;
@@ -113,8 +114,8 @@ export class VermineUtils {
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula; formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
// Reconcile the breakdown displayed in chat with the dice actually rolled. // Reconcile the breakdown displayed in chat with the dice actually rolled.
// Domain bonuses/malus are only applied when the corresponding totem die // L'influence s'applique au pool ; les Dés de Totem remplacent (n'ajoutent
// is used, and totem dice replace (not add to) base-pool dice. // pas) des dés du pool de base.
if (poolBreakdown) { if (poolBreakdown) {
// Count totem dice present in the final modifier formula. // Count totem dice present in the final modifier formula.
let totemDice = 0; let totemDice = 0;
@@ -122,14 +123,9 @@ export class VermineUtils {
if (modFormula.includes("human_")) totemDice += 1; if (modFormula.includes("human_")) totemDice += 1;
if (modFormula.includes("adapted_")) totemDice += 1; if (modFormula.includes("adapted_")) totemDice += 1;
} }
// Only count the domain bonus of totems actually activated in the roll.
const domainBonus = (totems.human ? totemBonus.human : 0)
+ (totems.adapted ? totemBonus.adapted : 0);
const basePool = poolBreakdown.total;
poolBreakdown.totemDice = totemDice; poolBreakdown.totemDice = totemDice;
poolBreakdown.domainBonus = domainBonus; poolBreakdown.domainBonus = influenceMod;
// Total = nombre réel de dés lancés (NoD réguliers + dés de totem). // Total = nombre réel de dés lancés (NoD réguliers + dés de totem).
// Équivaut à basePool + domainBonus sauf si le pool régulier clampe à 0.
poolBreakdown.total = NoD + totemDice; poolBreakdown.total = NoD + totemDice;
} }
@@ -145,7 +141,7 @@ export class VermineUtils {
skillCategory: skillCategory, skillCategory: skillCategory,
skillLevel: skillLevel, skillLevel: skillLevel,
hasSpecialty: hasSpecialty, hasSpecialty: hasSpecialty,
totemBonuses: { ...totemBonus }, totemInfluence: influenceMod,
baseNoD: NoD, baseNoD: NoD,
rerolls: Reroll, rerolls: Reroll,
selfControl: self_control, selfControl: self_control,
@@ -173,12 +169,17 @@ export class VermineUtils {
hasSpecialty, hasSpecialty,
handicap, handicap,
bonusSuccesses, bonusSuccesses,
bonusOnSuccess,
poolBreakdown, poolBreakdown,
specialtyName, specialtyName,
weapon, weapon,
targets, targets,
attack, attack,
messageFlags messageFlags,
effectiveRange,
mobilityHandicap,
ponctuelUsed,
intimidant
}); });
return roll; return roll;
@@ -188,47 +189,12 @@ export class VermineUtils {
* Calculates domain bonuses/penalties for totems. * Calculates domain bonuses/penalties for totems.
* @param {string} skillCategory - The skill category * @param {string} skillCategory - The skill category
* @param {Actor} actor - The actor * @param {Actor} actor - The actor
* @returns {Object} Bonuses for each totem {human: number, adapted: number} * @returns {number} Modificateur d'influence (-1, 0 ou +1)
*/ */
static _calculateTotemDomainBonuses(skillCategory, actor) { static _calculateTotemInfluence(skillCategory, actor) {
const bonuses = { human: 0, adapted: 0 }; // Les Dés de Totems n'existent que sur les personnages.
if (actor?.type !== "character" || !skillCategory) return 0;
// Validate inputs return VermineTotemDice.influenceModifier(actor, skillCategory);
if (!CONFIG.VERMINE?.totemDomains || !actor?.system?.identity?.totem) {
return bonuses;
}
const actorTotem = actor.system.identity.totem;
// Check if actor's totem exists in configuration
if (!CONFIG.VERMINE.totemDomains[actorTotem]) {
return bonuses;
}
const totemConfig = CONFIG.VERMINE.totemDomains[actorTotem];
if (!totemConfig?.domains) {
return bonuses;
}
// Get actor's preferred skill category
const preferredCategory = actor.system.skill_categories?.preferred;
// Bonus for actor's totem if preferred category is in its domains
if (preferredCategory && totemConfig.domains.includes(preferredCategory)) {
bonuses[actorTotem] = totemConfig.bonus || 1;
}
// Penalty for opposite totem if preferred category is in its domains
const oppositeTotem = CONFIG.VERMINE.totem_opposites?.[actorTotem];
if (oppositeTotem && CONFIG.VERMINE.totemDomains[oppositeTotem]) {
const oppositeConfig = CONFIG.VERMINE.totemDomains[oppositeTotem];
if (preferredCategory && oppositeConfig?.domains?.includes(preferredCategory)) {
bonuses[oppositeTotem] = -(oppositeConfig.bonus || 1);
}
}
return bonuses;
} }
/** /**
@@ -493,8 +459,12 @@ export class VermineUtils {
} }
// Verdict // Verdict
const required = 1 + (param.handicap ?? 0); const required = 1 + (param.handicap ?? 0);
const bonusSuccesses = param.bonusSuccesses ?? 0; const rawTotal = roll._total ?? 0;
const total = (roll._total ?? 0) + bonusSuccesses; // Réussites automatiques : appliquées inconditionnellement, ou uniquement
// si le jet est réussi (Trait Rafale — bonusOnSuccess).
const requested = param.bonusSuccesses ?? 0;
const bonusSuccesses = requested > 0 && (!param.bonusOnSuccess || rawTotal >= required) ? requested : 0;
const total = rawTotal + bonusSuccesses;
const verdict = { const verdict = {
success: total >= required, success: total >= required,
total, total,
+171
View File
@@ -0,0 +1,171 @@
/**
* VermineTotemDice - Gestion des Dés de Totems (Humain / Adapté).
*
* Règles p. 121 :
* - Influence : si un joueur possède strictement plus de Dés d'un Totem que
* de l'autre, ce Totem prend l'ascendant (+1D/-1D sur certains Domaines).
* - Gains et pertes via les Instincts et les Interdits (1D ou 2D selon la
* gravité), avec des limites (3 Dés du même Totem, 5 Dés au maximum).
*/
export class VermineTotemDice {
/**
* Totem dominant d'un personnage.
* @param {foundry.abstract.Document} actor
* @returns {'human'|'adapted'|'neutral'}
*/
static getInfluence(actor) {
const human = actor.system?.adaptation?.totems?.human?.value || 0;
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
if (human > adapted) return "human";
if (adapted > human) return "adapted";
return "neutral";
}
/**
* Modificateur (Bonus/Malus) d'influence pour une catégorie de compétence.
* @param {foundry.abstract.Document} actor
* @param {string} skillCategory catégorie (man, animal, tool, weapon, survival, world)
* @returns {number} -1, 0 ou +1
*/
static influenceModifier(actor, skillCategory) {
const influence = this.getInfluence(actor);
if (influence === "neutral" || !skillCategory) return 0;
const cfg = CONFIG.VERMINE?.totemInfluence?.[influence];
if (!cfg) return 0;
if ((cfg.bonus || []).includes(skillCategory)) return 1;
if ((cfg.malus || []).includes(skillCategory)) return -1;
return 0;
}
/**
* Applique un gain d'un Dé de Totem selon les règles d'échange et de limites.
* Retourne un objet décrivant ce qui s'est passé (pour notification).
* @param {foundry.abstract.Document} actor
* @param {'human'|'adapted'} totem
* @param {number} [count=1] 1 ou 2 dés (acte grave)
* @returns {Promise<Object>}
*/
static async gainDie(actor, totem, count = 1) {
const human = actor.system.adaptation.totems.human;
const adapted = actor.system.adaptation.totems.adapted;
const actions = [];
for (let i = 0; i < count; i++) {
const step = this._gainOneDie(human.value, adapted.value, totem);
if (step.unchanged) {
actions.push({ ok: false, reason: "max" });
break;
}
human.value = step.human;
adapted.value = step.adapted;
actions.push({ ok: true, exchanged: step.exchanged, replaced: step.replaced });
}
await actor.update({
"system.adaptation.totems.human.value": human.value,
"system.adaptation.totems.adapted.value": adapted.value
});
return this._summarize(actions, totem);
}
/**
* Applique une perte d'un de Totem.
* Règle : seul l'Humain a des Interdits. Briser un Interdit Humain fait
* perdre 1D Humain ; si le personnage n'en possède pas, il gagne 1D Adapté.
* @param {foundry.abstract.Document} actor
* @param {'human'} totem
* @param {number} [count=1] 1 ou 2 dés (acte grave)
* @returns {Promise<Object>}
*/
static async loseDie(actor, totem, count = 1) {
const human = actor.system.adaptation.totems.human;
const adapted = actor.system.adaptation.totems.adapted;
const actions = [];
for (let i = 0; i < count; i++) {
if (human.value > 0) {
human.value -= 1;
actions.push({ ok: true, lost: true });
} else if (adapted.value < adapted.max && (human.value + adapted.value) < 5) {
// Pas de Dé Humain : gagne un Dé Adapté à la place (limites appliquées).
adapted.value += 1;
actions.push({ ok: true, gainedAdapted: true });
} else {
actions.push({ ok: false, reason: "max" });
break;
}
}
await actor.update({
"system.adaptation.totems.human.value": human.value,
"system.adaptation.totems.adapted.value": adapted.value
});
return this._summarize(actions, totem);
}
/**
* Ajoute un du Totem donné en respectant les limites et les échanges.
* Règles :
* - 3 Dés max du même Totem, 5 Dés max au total.
* - Si le joueur gagne un qu'il ne peut conserver, il peut supprimer un
* du Totem opposé à la place ; sinon le gain est annulé.
* - Gagner un 4e Adapté est impossible, mais supprime un Humain.
* @param {number} human
* @param {number} adapted
* @param {'human'|'adapted'} totem
* @returns {Object}
*/
static _gainOneDie(human, adapted, totem) {
const limitTotem = 3;
const opposite = totem === "human" ? "adapted" : "human";
const total = human + adapted;
const current = totem === "human" ? human : adapted;
const oppositeVal = totem === "human" ? adapted : human;
// Le Totem cible est déjà au maximum (3) : impossible d'empiler.
if (current >= limitTotem) {
if (oppositeVal > 0) {
// Supprime un Dé du Totem opposé à la place (le gain n'augmente pas).
const result = { human, adapted, unchanged: false, replaced: true };
result[opposite] -= 1;
return result;
}
return { human, adapted, unchanged: true };
}
// Limite totale atteinte (5) : peut remplacer un Dé opposé.
if (total >= 5) {
if (oppositeVal > 0) {
const result = { human, adapted, unchanged: false, exchanged: true };
result[totem] += 1;
result[opposite] -= 1;
return result;
}
return { human, adapted, unchanged: true };
}
// Gain normal.
const result = { human, adapted, unchanged: false };
result[totem] += 1;
return result;
}
/**
* Résume les actions appliquées en un libellé localisé.
* @param {Array} actions
* @param {'human'|'adapted'} totem
* @returns {Object}
*/
static _summarize(actions, totem) {
const gained = actions.filter(a => a.ok && !a.exchanged && !a.replaced && !a.lost && !a.gainedAdapted).length;
const exchanged = actions.filter(a => a.exchanged).length;
const replaced = actions.filter(a => a.replaced).length;
const lost = actions.filter(a => a.lost).length;
const gainedAdapted = actions.filter(a => a.gainedAdapted).length;
const refused = actions.some(a => !a.ok);
return { totem, gained, exchanged, replaced, lost, gainedAdapted, refused };
}
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Helpers partagés pour les Traits (regles/regles_traits.txt).
*
* Centralise la lecture des Traits sur les items (présence, valeur) et les
* calculs liés aux objets équipés d'un acteur (Lourd, Mobilité, Fétiche).
* Consommé par les dialogues de jet (combat, action) et les cartes de chat.
*/
export class VermineTraits {
/**
* Présence d'un Trait sur un item.
* @param {Item|null} item
* @param {string} key clé du Trait (ex. "lourd")
* @returns {boolean}
*/
static hasTrait(item, key) {
return Boolean(item?.system?.traits?.[key]);
}
/**
* Valeur (n) d'un Trait sur un item (0 si absent ou sans valeur).
* @param {Item|null} item
* @param {string} key clé du Trait
* @returns {number}
*/
static traitValue(item, key) {
if (!item?.system?.traits?.[key]) return 0;
return parseInt(item.system.traits[key]?.value ?? 0, 10) || 0;
}
/**
* Items équipés d'un acteur (armes + protections + objets génériques)
* possédant un Trait donné.
* @param {Actor} actor
* @param {string} traitKey
* @returns {Item[]}
*/
static equippedItems(actor, traitKey) {
if (!actor?.itemTypes) return [];
return [...actor.itemTypes.weapon || [], ...actor.itemTypes.defense || [], ...actor.itemTypes.item || []]
.filter(i => i.system?.equipped && this.hasTrait(i, traitKey));
}
/**
* Vigueur de l'acteur : caractéristique pour le personnage, tables pour
* PNJ (Menace) et créature (computed).
* @param {Actor} actor
* @returns {number}
*/
static vigor(actor) {
if (!actor?.system) return 0;
if (actor.type === 'npc') {
const threat = actor.system?.threat?.value || 1;
return CONFIG.VERMINE.npcThreatLevels?.[threat]?.vigor || 0;
}
if (actor.type === 'creature') return actor.system?.computed?.vigor || 0;
return actor.system?.abilities?.vigor?.value || 0;
}
/**
* Malus cumulés des objets Lourd (n) équipés dont la Vigueur du porteur
* est inférieure à (n). Les Malus se cumulent entre objets (règles p. ).
* @param {Actor} actor
* @returns {number}
*/
static lourdMalus(actor) {
const vigor = this.vigor(actor);
return this.equippedItems(actor, 'lourd').reduce((sum, item) => {
const n = this.traitValue(item, 'lourd');
return sum + (n > 0 && vigor < n ? 1 : 0);
}, 0);
}
/**
* Handicap de Mobilité total : somme des handicaps des protections équipées
* qui ne sont pas Maniables (le Trait Maniable annule le Handicap).
* @param {Actor} actor
* @returns {number}
*/
static mobilityHandicap(actor) {
const defenses = (actor?.itemTypes?.defense || []).filter(d => d.system?.equipped);
return defenses.reduce((sum, d) => sum + (this.hasTrait(d, 'maniable') ? 0 : (parseInt(d.system?.mobility ?? 0, 10) || 0)), 0);
}
/**
* Note de rappel sur la Mobilité (protections équipées non Maniables),
* ou null si aucun Handicap de Mobilité.
* @param {Actor} actor
* @returns {string|null}
*/
static mobilityNote(actor) {
const handicap = this.mobilityHandicap(actor);
if (handicap <= 0) return null;
return game.i18n.format('VERMINE.traits_mobility_note', { handicap });
}
/**
* Premier item équipé possédant le Trait Fétiche, ou null.
* @param {Actor} actor
* @returns {Item|null}
*/
static feticheItem(actor) {
return this.equippedItems(actor, 'fetiche')[0] ?? null;
}
/**
* Nombre d'items équipés avec le Trait Fétiche.
* @param {Actor} actor
* @returns {number}
*/
static feticheCount(actor) {
return this.equippedItems(actor, 'fetiche').length;
}
}
+4
View File
@@ -53,6 +53,7 @@ Hooks.once('init', async function () {
CONFIG.Item.dataModels.weapon = models.VermineWeaponData; CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
CONFIG.Item.dataModels.defense = models.VermineDefenseData; CONFIG.Item.dataModels.defense = models.VermineDefenseData;
CONFIG.Item.dataModels.ability = models.VermineAbilityData; CONFIG.Item.dataModels.ability = models.VermineAbilityData;
CONFIG.Item.dataModels.creaturecapacity = models.VermineCreatureCapacityData;
CONFIG.Item.dataModels.specialty = models.VermineSpecialtyData; CONFIG.Item.dataModels.specialty = models.VermineSpecialtyData;
CONFIG.Item.dataModels.background = models.VermineBackgroundData; CONFIG.Item.dataModels.background = models.VermineBackgroundData;
CONFIG.Item.dataModels.trauma = models.VermineTraumaData; CONFIG.Item.dataModels.trauma = models.VermineTraumaData;
@@ -119,6 +120,9 @@ Hooks.once('init', async function () {
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineAbilitySheetV2, { foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineAbilitySheetV2, {
types: ["ability"], makeDefault: true types: ["ability"], makeDefault: true
}) })
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineCreatureCapacitySheetV2, {
types: ["creaturecapacity"], makeDefault: true
})
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineSpecialtySheetV2, { foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineSpecialtySheetV2, {
types: ["specialty"], makeDefault: true types: ["specialty"], makeDefault: true
}) })
Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 KiB

@@ -16,11 +16,11 @@ system:
value: 3 value: 3
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -29,8 +29,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198965 createdTime: 1786119843343
modifiedTime: 1785775198965 modifiedTime: 1786119843357
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!vPA7gekUx8kUNOWB' _key: '!items!vPA7gekUx8kUNOWB'
@@ -13,11 +13,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198966 createdTime: 1786119843344
modifiedTime: 1785775198966 modifiedTime: 1786119843357
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!DItkLLGSyjAGzruO' _key: '!items!DItkLLGSyjAGzruO'
@@ -13,11 +13,11 @@ system:
value: 3 value: 3
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198966 createdTime: 1786119843345
modifiedTime: 1785775198966 modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!Cs6ibCrcPxim7QHR' _key: '!items!Cs6ibCrcPxim7QHR'
@@ -14,11 +14,11 @@ system:
value: 1 value: 1
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -27,8 +27,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198967 createdTime: 1786119843345
modifiedTime: 1785775198967 modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!pP9hfvk5JKsoFcnt' _key: '!items!pP9hfvk5JKsoFcnt'
@@ -12,11 +12,11 @@ system:
value: 1 value: 1
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198967 createdTime: 1786119843345
modifiedTime: 1785775198967 modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!h3Q6LwnnVuj6MH4Y' _key: '!items!h3Q6LwnnVuj6MH4Y'
@@ -13,11 +13,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198967 createdTime: 1786119843346
modifiedTime: 1785775198967 modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!JrtHAUqFgA75e6pq' _key: '!items!JrtHAUqFgA75e6pq'
@@ -16,11 +16,11 @@ system:
value: 3 value: 3
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -29,8 +29,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198967 createdTime: 1786119843346
modifiedTime: 1785775198967 modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!t4p4n73kwwVoeSMe' _key: '!items!t4p4n73kwwVoeSMe'
@@ -12,11 +12,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198968 createdTime: 1786119843346
modifiedTime: 1785775198968 modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!2h0xUozy1xqLdKcE' _key: '!items!2h0xUozy1xqLdKcE'
@@ -13,11 +13,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198968 createdTime: 1786119843347
modifiedTime: 1785775198968 modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!oNfjRGGRVFGtfbK0' _key: '!items!oNfjRGGRVFGtfbK0'
@@ -9,11 +9,11 @@ system:
value: 1 value: 1
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -22,8 +22,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198968 createdTime: 1786119843347
modifiedTime: 1785775198968 modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!vgYLoiEqEOxrGdy6' _key: '!items!vgYLoiEqEOxrGdy6'
@@ -12,11 +12,11 @@ system:
value: 1 value: 1
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198968 createdTime: 1786119843347
modifiedTime: 1785775198968 modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!uGG0FrBn1QtMcxbz' _key: '!items!uGG0FrBn1QtMcxbz'
@@ -16,11 +16,11 @@ system:
value: 4 value: 4
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -29,8 +29,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198968 createdTime: 1786119843347
modifiedTime: 1785775198968 modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!bV6V4i1v4ORWDJSQ' _key: '!items!bV6V4i1v4ORWDJSQ'
@@ -13,11 +13,11 @@ system:
value: 1 value: 1
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198969 createdTime: 1786119843348
modifiedTime: 1785775198969 modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!S8AwTKDxIdez7vHB' _key: '!items!S8AwTKDxIdez7vHB'
@@ -11,11 +11,11 @@ system:
value: 1 value: 1
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -24,8 +24,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198969 createdTime: 1786119843348
modifiedTime: 1785775198969 modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!RjdZB4Qd2V8mp44x' _key: '!items!RjdZB4Qd2V8mp44x'
@@ -17,11 +17,11 @@ system:
value: 3 value: 3
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -30,8 +30,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198969 createdTime: 1786119843348
modifiedTime: 1785775198969 modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!kPoioCvlK7H0uJB1' _key: '!items!kPoioCvlK7H0uJB1'
@@ -13,11 +13,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198969 createdTime: 1786119843349
modifiedTime: 1785775198969 modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!EqeONvUCK7OhJaNN' _key: '!items!EqeONvUCK7OhJaNN'
@@ -19,11 +19,11 @@ system:
value: 3 value: 3
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -32,8 +32,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198969 createdTime: 1786119843349
modifiedTime: 1785775198969 modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!8ShwJQCWKHsfXLko' _key: '!items!8ShwJQCWKHsfXLko'
@@ -13,11 +13,11 @@ system:
value: 4 value: 4
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198970 createdTime: 1786119843349
modifiedTime: 1785775198970 modifiedTime: 1786119843360
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!ivx8gS0XBEcoqtdl' _key: '!items!ivx8gS0XBEcoqtdl'
@@ -15,11 +15,11 @@ system:
value: 4 value: 4
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -28,8 +28,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198970 createdTime: 1786119843350
modifiedTime: 1785775198970 modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!HfC2oOARrpKnpoQf' _key: '!items!HfC2oOARrpKnpoQf'
@@ -14,11 +14,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -27,8 +27,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198970 createdTime: 1786119843350
modifiedTime: 1785775198970 modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!Re0aeZzupnz3EL6E' _key: '!items!Re0aeZzupnz3EL6E'
@@ -20,11 +20,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -33,8 +33,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198970 createdTime: 1786119843350
modifiedTime: 1785775198970 modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!UxNguZd8K62nt1GH' _key: '!items!UxNguZd8K62nt1GH'
@@ -17,11 +17,11 @@ system:
value: 3 value: 3
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -30,8 +30,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198971 createdTime: 1786119843351
modifiedTime: 1785775198971 modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!AAw7U5tUxbUczorz' _key: '!items!AAw7U5tUxbUczorz'
@@ -11,11 +11,11 @@ system:
value: 1 value: 1
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -24,8 +24,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198971 createdTime: 1786119843351
modifiedTime: 1785775198971 modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!guC1pOfmcmitRGDo' _key: '!items!guC1pOfmcmitRGDo'
@@ -16,11 +16,11 @@ system:
value: 4 value: 4
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -29,8 +29,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198971 createdTime: 1786119843351
modifiedTime: 1785775198971 modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!kFak0PMoNAuPnSfX' _key: '!items!kFak0PMoNAuPnSfX'
@@ -23,11 +23,11 @@ system:
value: 4 value: 4
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -36,8 +36,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198971 createdTime: 1786119843352
modifiedTime: 1785775198971 modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!4jRJw5vuhZeqoKy9' _key: '!items!4jRJw5vuhZeqoKy9'
@@ -24,11 +24,11 @@ system:
value: 4 value: 4
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -37,8 +37,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198972 createdTime: 1786119843352
modifiedTime: 1785775198972 modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!Y8UeTOYpGkM3nt9R' _key: '!items!Y8UeTOYpGkM3nt9R'
@@ -11,11 +11,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -24,8 +24,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198972 createdTime: 1786119843352
modifiedTime: 1785775198972 modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!BNZeEd0CpTgYkEou' _key: '!items!BNZeEd0CpTgYkEou'
@@ -15,11 +15,11 @@ system:
value: 3 value: 3
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -28,8 +28,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198972 createdTime: 1786119843353
modifiedTime: 1785775198972 modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!wH5OPnv4cenzpgws' _key: '!items!wH5OPnv4cenzpgws'
@@ -14,11 +14,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -27,8 +27,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198973 createdTime: 1786119843354
modifiedTime: 1785775198973 modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!rCQOrAzaCgCF0Dri' _key: '!items!rCQOrAzaCgCF0Dri'
@@ -14,11 +14,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -27,8 +27,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198972 createdTime: 1786119843353
modifiedTime: 1785775198972 modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!DHv0PXsIgQ8rPPUY' _key: '!items!DHv0PXsIgQ8rPPUY'
@@ -15,11 +15,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -28,8 +28,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198973 createdTime: 1786119843354
modifiedTime: 1785775198973 modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!IMXQRepTNtiHZcaf' _key: '!items!IMXQRepTNtiHZcaf'
@@ -12,11 +12,11 @@ system:
value: 1 value: 1
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198972 createdTime: 1786119843353
modifiedTime: 1785775198972 modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!nxy53EF4ZRSSwCvr' _key: '!items!nxy53EF4ZRSSwCvr'
@@ -13,11 +13,11 @@ system:
value: 2 value: 2
min: 1 min: 1
max: 4 max: 4
mutation: false
effects: [] effects: []
sort: 0 sort: 0
ownership: ownership:
default: 0 default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3 kb05teJOwuTQVHwZ: 3
flags: {} flags: {}
_stats: _stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365' coreVersion: '14.365'
systemId: vermine2047 systemId: vermine2047
systemVersion: 0.1.13 systemVersion: 0.1.13
createdTime: 1785775198973 createdTime: 1786119843354
modifiedTime: 1785775198973 modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null exportSource: null
_key: '!items!uneyg1G0Gpbb7l5P' _key: '!items!uneyg1G0Gpbb7l5P'

Some files were not shown because too many files have changed in this diff Show More