18 Commits
Author SHA1 Message Date
uberwald 0ef0de45ff fix(experience): phase ciblée (groupe/sans groupe/individuel) + dépense des Dés d'Évolution
Release Creation / build (release) Failing after 1m18s
Phase d'Expérience :
- sélecteur de groupe : « Tous les personnages », « Sans groupe » ou un groupe précis → seuls ses membres sont récompensés
- case « Inclure dans cette phase » par personnage pour exclure de la phase
- chaque ligne affiche son groupe ([Groupe] / [Sans groupe])
- les collectifs ne sont versés au groupe que si un vrai groupe est choisi

Dés d'Évolution :
- achat : ne consomme que le coût du niveau (le reste du stock est conservé) au lieu d'effacer tout
- total « après achat » recalculé en direct selon le niveau sélectionné
- libellés d'options nettoyés (1D au lieu de 1D — 1D)
2026-08-08 18:35:01 +02:00
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
787 changed files with 18545 additions and 7395 deletions
+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
+109 -1
View File
@@ -163,6 +163,35 @@
"pools": "Reserves",
"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",
"self_control": "Self-control",
"effort": "Effort",
@@ -211,6 +240,47 @@
"rarity_sm": "Rar.",
"reliability": "Reliability",
"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": {
"name": "Wounds",
"threshold": "Threshold",
@@ -263,6 +333,9 @@
"CharacterNamePlaceholder": "Character Name",
"ProfilePlaceholder": "Profile",
"none": "None",
"all_characters": "All characters",
"no_group": "No group",
"include_in_phase": "Include in this phase",
"bonuses": "Bonuses",
"choose_ability": "Choose Ability",
"choose_skill": "Choose Skill",
@@ -304,7 +377,20 @@
"die_successes": "successes",
"total": "Total",
"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",
"RollTool": "Roll Tool",
"Roll4Fight": "Roll for Fight",
@@ -374,6 +460,27 @@
"trauma_reward": "Chosen trauma",
"trauma_reward_5": "Trauma reward (+5D)",
"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 a number of Evolution dice equal to the chosen level (the remainder is kept). 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_learning": "Learning",
"validate_phase": "Validate phase",
@@ -508,6 +615,7 @@
},
"roll": "Roll",
"cancel": "Cancel",
"close": "Close",
"playMode": "Play Mode",
"editMode": "Edit Mode"
},
+108
View File
@@ -170,6 +170,35 @@
"reserves": "Réserves",
"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",
"self_control": "Sang-Froid",
"effort": "Effort",
@@ -292,9 +321,53 @@
"rarity_sm": "Rar.",
"reliability": "Fiabilité",
"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_skill": "Choisissez une compétence",
"none": "Aucun",
"all_characters": "Tous les personnages",
"no_group": "Sans groupe",
"include_in_phase": "Inclure dans cette phase",
"total": "Total",
"bonuses": "Bonus",
"handicap": "Handicap",
@@ -302,6 +375,19 @@
"totem_dice": "Dés de totem",
"keep_totem": "Garder le totem",
"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_contact": "Attaque au contact",
"attack_ranged": "Attaque à distance",
@@ -357,6 +443,27 @@
"trauma_reward": "Traumatisme choisi",
"trauma_reward_5": "Récompense de Traumatisme (+5D)",
"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 un nombre de Dés d'Évolution égal au niveau choisi (le reste est conservé). 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_learning": "Apprentissage",
"validate_phase": "Valider la phase",
@@ -516,6 +623,7 @@
"RollTool": "Outil de lancer de dés",
"roll": "Lancer",
"cancel": "Annuler",
"close": "Fermer",
"Sheet": {
"character": "Personnage",
"npc": "PNJ",
+80 -3
View File
@@ -21,7 +21,7 @@
input:not([type="radio"]):not([type="checkbox"]),
select, textarea {
color: @color-text-light-0;
color: #333;
line-height: 1.2;
padding: 1px 4px;
}
@@ -670,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 {
.item-name .item-image {
flex: 0 0 30px;
@@ -696,6 +752,27 @@
&: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 ─────────────────────────
@@ -706,7 +783,7 @@
margin: 0;
padding: 0;
background: @color-chitin-dark;
border-bottom: 2px solid @color-amber;
border-bottom: 2px solid @color-hemolymph;
min-height: 36px;
> a {
@@ -730,7 +807,7 @@
}
&.active {
background: linear-gradient(180deg, @color-amber 0%, @color-honeycomb 100%);
background: @color-honeycomb;
color: @color-chitin-dark;
&::after {
+9 -9
View File
@@ -69,14 +69,14 @@
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
color: @color-text-light-2;
}
}
.community-size-info {
margin: 6px 0 0;
font-size: @font-size-11;
color: @color-text-light-0;
color: @color-text-dark-secondary;
opacity: 0.85;
}
@@ -94,8 +94,8 @@
margin: 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-light-0;
opacity: 0.7;
color: @color-text-dark-secondary;
opacity: 0.8;
}
// Jauges de Domaines
@@ -111,7 +111,7 @@
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
color: @color-text-light-2;
}
input[type="number"] {
@@ -123,7 +123,7 @@
.gauge-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-16;
color: @color-text-light-0;
color: @color-text-dark-header;
}
.gauge-state {
@@ -134,7 +134,7 @@
}
&.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; } }
&.opulent { border-left-color: @theme-color-accent; .gauge-state { color: @theme-color-accent; } }
}
@@ -145,8 +145,8 @@
font-weight: 700;
&.high { color: @color-acid-green; }
&.normal { color: @color-text-light-0; }
&.low { color: @color-amber; }
&.normal { color: @color-text-dark-secondary; }
&.low { color: @color-honeycomb; }
&.crisis { color: @color-hemolymph; }
}
}
+1 -1
View File
@@ -188,7 +188,7 @@
.capacity-type {
flex: 0 0 auto;
font-size: @font-size-12;
color: @color-amber;
color: @color-text-light-2;
text-transform: uppercase;
}
+3 -3
View File
@@ -71,13 +71,13 @@
}
.npc-roll-btn {
color: @color-text-light-primary;
opacity: 0.7;
color: @color-text-dark-secondary;
opacity: 0.75;
transition: opacity 0.15s ease;
&:hover {
opacity: 1;
color: @color-text-light-1;
color: @theme-color-dark;
text-shadow: 0 0 6px @theme-color-highlight;
}
}
+32
View File
@@ -124,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;
}
}
}
}
}
+3 -3
View File
@@ -124,7 +124,7 @@
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
color: @color-text-light-2;
}
}
@@ -152,7 +152,7 @@
.unit {
font-size: @font-size-11;
color: @color-text-light-0;
color: @color-text-dark-secondary;
opacity: 0.8;
}
@@ -171,7 +171,7 @@
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
color: @color-text-light-2;
}
}
}
+295 -51
View File
@@ -12,29 +12,53 @@ body.system-vermine2047 {
color: @theme-color-secondary;
}
// Override Foundry Core default CSS custom properties
// for better readability on the dark Vermine2047 theme.
// AppV2 windows (sheets) use .themed.theme-dark on their content
.system-vermine2047 .themed.theme-dark {
--color-text-primary: #888888;
--color-text-emphatic: #999999;
--color-text-secondary: #777777;
--color-text-subtle: #666666;
--color-text-light-highlight: #888888;
--color-text-light-heading: #777777;
--color-text-light-primary: #666666;
// Les checkbox/radio du système ne s'étirent jamais dans les conteneurs
// flex : ils gardent leur taille hexagonale (1rem / 1.5rem). Les cases de
// blessure en `div.hexa` (position:absolute) ne sont pas affectées.
.system-vermine2047 {
input[type="checkbox"],
input[type="radio"] {
flex: 0 0 auto;
}
}
// 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 {
--color-text-primary: #888888;
--color-text-emphatic: #999999;
--color-text-secondary: #777777;
--color-text-subtle: #666666;
--color-text-light-highlight: #888888;
--color-text-light-heading: #777777;
--color-text-light-primary: #666666;
.window-content,
.roll-table-sheet,
.journal-sheet,
.document-sheet,
.settings-sheet {
color: @color-text-dark-primary;
--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,
@@ -44,31 +68,13 @@ body.system-vermine2047.theme-dark {
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 {
color: #777777;
color: @color-text-dark-secondary;
}
#settings-config p,
#settings-config .hint {
color: #777777;
color: @color-text-dark-secondary;
}
img {
@@ -732,7 +738,7 @@ ol.chat-log {
}
#required {
color: @color-amber;
color: @color-text-light-2;
}
}
@@ -837,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 ─────────────────────────────────────────────
@@ -860,6 +900,19 @@ ol.chat-log {
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 {
display: flex;
align-items: center;
@@ -894,7 +947,7 @@ ol.chat-log {
.exchange-passive {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @color-amber;
color: @color-text-light-2;
opacity: 0.85;
font-style: italic;
}
@@ -907,7 +960,7 @@ ol.chat-log {
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
color: @theme-color-dark;
background: fade(@color-hemolymph, 25%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
@@ -918,13 +971,38 @@ ol.chat-log {
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 {
margin: 4px 0 6px;
padding: 6px 8px;
border: 1px solid @color-amber;
background: fade(@color-amber, 8%);
border: 1px solid @color-hemolymph;
background: fade(@color-hemolymph, 8%);
.resolution-header {
display: flex;
@@ -937,7 +1015,7 @@ ol.chat-log {
font-weight: 900;
color: @theme-color-dark;
i { color: @color-amber; }
i { color: @color-hemolymph; }
}
.resolution-line {
@@ -962,7 +1040,7 @@ ol.chat-log {
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
color: @theme-color-dark;
background: fade(@color-hemolymph, 25%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
@@ -1063,7 +1141,7 @@ ol.chat-log {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
color: @color-text-light-2;
}
.exp-value {
@@ -1084,7 +1162,7 @@ ol.chat-log {
font-size: @font-size-12;
padding: 5px 14px;
cursor: pointer;
color: @color-text-light-0;
color: @theme-color-dark;
background: fade(@color-hemolymph, 20%);
border: 1px solid @color-hemolymph;
border-radius: 3px;
@@ -1111,7 +1189,7 @@ ol.chat-log {
margin: 0 0 6px;
.exp-total {
color: @color-amber;
color: @color-text-light-2;
}
}
@@ -1126,7 +1204,7 @@ ol.chat-log {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
color: @color-text-light-2;
}
}
@@ -1151,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%);
}
}
+147 -30
View File
@@ -148,7 +148,7 @@ iframe {
padding: 10px;
height: 100%;
overflow-y: auto;
color: @color-text-light-0;
color: @theme-color-dark;
.dice-pool {
flex: 1;
@@ -188,7 +188,7 @@ iframe {
> label {
font-size: @font-size-13;
color: @color-amber;
color: @color-text-light-2;
white-space: nowrap;
}
@@ -201,7 +201,7 @@ iframe {
.ability-score {
gap: 4px;
font-size: @font-size-12;
color: @color-text-light-0;
color: @color-text-dark-secondary;
opacity: 0.85;
#abilityScoreValue {
@@ -290,11 +290,11 @@ iframe {
font-size: @font-size-12;
font-family: "Roboto", sans-serif;
text-transform: none;
color: @color-text-light-0;
color: @color-text-dark-primary;
cursor: pointer;
&:hover {
color: @color-amber;
color: @color-acid-green;
}
}
@@ -317,7 +317,7 @@ iframe {
> label.label {
font-size: @font-size-11;
color: @color-text-light-0;
color: @color-text-dark-secondary;
white-space: nowrap;
}
@@ -331,7 +331,7 @@ iframe {
font-size: @font-size-12;
font-family: "Roboto", sans-serif;
text-transform: none;
color: @color-text-light-0;
color: @color-text-dark-primary;
}
.item-list {
@@ -343,13 +343,31 @@ iframe {
font-size: @font-size-11;
font-family: "Roboto", sans-serif;
text-transform: none;
color: @color-text-light-0;
color: @color-text-dark-primary;
cursor: pointer;
&: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 {
@@ -374,14 +392,14 @@ iframe {
display: flex;
align-items: center;
gap: 4px;
color: @color-text-light-0;
color: @color-text-dark-primary;
font-size: @font-size-12;
font-family: "Roboto", sans-serif;
text-transform: none;
cursor: pointer;
&:hover {
color: @color-amber;
color: @color-acid-green;
}
small {
@@ -392,8 +410,8 @@ iframe {
.totem-hint {
width: 100%;
font-size: @font-size-11;
color: @color-text-light-0;
opacity: 0.6;
color: @color-text-dark-secondary;
opacity: 0.8;
font-style: italic;
}
}
@@ -408,6 +426,16 @@ iframe {
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 {
flex: 1;
display: flex;
@@ -448,7 +476,7 @@ iframe {
.weapon-detail {
font-family: "Roboto", sans-serif;
font-size: @font-size-11;
color: @color-text-light-0;
color: @color-text-dark-primary;
strong { text-transform: uppercase; }
}
@@ -466,7 +494,7 @@ iframe {
font-family: "Roboto", sans-serif;
text-transform: none;
font-size: @font-size-12;
color: @color-text-light-0;
color: @color-text-dark-primary;
cursor: pointer;
input[type="radio"] { flex: none; }
@@ -487,14 +515,14 @@ iframe {
.label {
font-size: @font-size-11;
color: @color-amber;
color: @color-text-light-2;
}
.combat-target-tag {
padding: 2px 8px;
font-size: @font-size-11;
font-family: "Roboto", sans-serif;
color: @color-text-light-0;
color: @color-text-dark-header;
background: fade(@color-hemolymph, 20%);
border: 1px solid fade(@color-hemolymph, 50%);
border-radius: 3px;
@@ -527,7 +555,7 @@ iframe {
.label {
font-size: @font-size-12;
color: @color-amber;
color: @color-text-light-2;
}
.exp-value {
@@ -550,12 +578,12 @@ iframe {
font-family: "Roboto", sans-serif;
text-transform: none;
font-size: @font-size-12;
color: @color-text-light-0;
color: @color-text-dark-primary;
cursor: pointer;
input[type="radio"] { flex: none; }
&:hover { color: @color-amber; }
&:hover { color: @color-acid-green; }
}
}
@@ -571,8 +599,8 @@ iframe {
margin: 0;
font-size: @font-size-11;
font-style: italic;
color: @color-text-light-0;
opacity: 0.75;
color: @color-text-dark-secondary;
opacity: 0.8;
}
}
@@ -581,8 +609,8 @@ iframe {
flex-wrap: wrap;
gap: 8px 16px;
padding: 8px;
border: 1px solid @color-amber;
background: fade(@color-amber, 8%);
border: 1px solid @color-hemolymph;
background: fade(@color-hemolymph, 8%);
.context-item {
display: flex;
@@ -593,14 +621,14 @@ iframe {
font-family: "DistressBlack", sans-serif;
text-transform: uppercase;
font-size: @font-size-11;
color: @color-amber;
color: @color-text-light-2;
}
.context-value {
font-family: "Roboto", sans-serif;
font-size: @font-size-13;
font-weight: 700;
color: @color-text-light-0;
color: @color-text-dark-primary;
}
}
}
@@ -613,7 +641,7 @@ iframe {
.experience-group {
font-size: @font-size-11;
color: @color-text-light-0;
color: @color-text-dark-secondary;
opacity: 0.8;
#exp-group {
@@ -628,7 +656,7 @@ iframe {
font-size: @font-size-11;
padding: 3px 12px;
cursor: pointer;
color: @color-text-light-0;
color: @theme-color-dark;
background: fade(@color-acid-green, 15%);
border: 1px solid @color-acid-green;
border-radius: 3px;
@@ -666,8 +694,8 @@ iframe {
.total-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-28;
color: @color-acid-green;
text-shadow: 0 0 8px fade(@color-acid-green, 40%);
color: @color-text-light-0;
text-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
}
.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;
}
}
}
+1 -2
View File
@@ -66,11 +66,10 @@
select {
.custom-select-style();
color: @color-text-light-1;
color: @color-text-dark-primary;
font-family: "DistressBlack", sans-serif;
font-size: 0.875rem;
text-align: center;
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9);
option {
background: @color-bg-option;
+1
View File
@@ -204,6 +204,7 @@
cursor: pointer;
width: 1.5rem;
height: 1rem;
flex: 0 0 auto;
border-radius: 0.4rem;
.transition();
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
+1 -2
View File
@@ -20,7 +20,7 @@
// Niveaux de gris pour le texte clair
@color-text-light-0: #fff;
@color-text-light-1: #e0f0f0;
@color-text-light-2: #c9e0c0;
@color-text-light-2: #3f9b55;
@color-text-light-3: #90c4a4;
@color-text-light-4: #80c08b;
@color-text-light-5: #60b06b;
@@ -139,7 +139,6 @@
// Couleurs insecte / organique
@color-acid-green: #1e7d32;
@color-amber: #e8b84b;
@color-chitin-dark: #2a2520;
@color-hemolymph: #8b4513;
@color-membrane: rgba(106, 176, 76, 0.1);
+3
View File
@@ -31,6 +31,9 @@
// 7. Styles des dialogs
@import "dialogs";
// 8. Styles du combat et du combat tracker
@import "combat";
// ============================================
// Notes:
// - L'ordre des imports est important
+15 -1
View File
@@ -25,7 +25,9 @@ export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.app
editImage: VermineBaseItemSheet.#onEditImage,
toggleSheet: VermineBaseItemSheet.#onToggleSheet,
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")
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)
}
}
+58 -2
View File
@@ -10,7 +10,10 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
openLearning: VermineCharacterSheetV2.#onOpenLearning,
validatePhase: VermineCharacterSheetV2.#onValidatePhase
validatePhase: VermineCharacterSheetV2.#onValidatePhase,
buyEvolution: VermineCharacterSheetV2.#onBuyEvolution,
openTotemDice: VermineCharacterSheetV2.#onOpenTotemDice,
loseFetiche: VermineCharacterSheetV2.#onLoseFetiche
}
}
@@ -78,13 +81,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
case "stories":
context.tab = context.tabs.stories
break
case "combat":
case "combat": {
context.tab = context.tabs.combat
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
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
}
case "experience":
context.tab = context.tabs.experience
context.specialtyCount = doc.itemTypes.specialty.length
@@ -133,4 +144,49 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
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()
}
}
-1
View File
@@ -52,7 +52,6 @@ export default class VermineNpcSheetV2 extends VermineBaseActorSheet {
context.npcExperienceOptions = CONFIG.VERMINE.npcExperienceLevels
context.npcRoleOptions = CONFIG.VERMINE.npcRoleLevels
context.totemOptions = CONFIG.VERMINE.totems
context.originOptions = CONFIG.VERMINE.origins
break
case "traits":
context.tab = context.tabs.traits
+1 -1
View File
@@ -253,7 +253,7 @@ export function baseItemSchema() {
return {
description: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
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 }),
quantity: new fields.NumberField({ ...reqInt, initial: 1, min: 1 }),
weight: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
+14 -2
View File
@@ -11,6 +11,7 @@ import {
skillCategoriesSchema,
skillsSchema
} from "./_shared.mjs"
import { VermineTraits } from "../system/traits.mjs"
export default class VermineCharacterData extends foundry.abstract.TypeDataModel {
@@ -99,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 }),
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
learned: new fields.SchemaField({
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
@@ -157,6 +163,7 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
/**
* Calcule le max de sang-froid :
* somme des caractéristiques mentales + sociales + modificateur d'âge.
* Un Fétiche équipé ajoute +1D ; une perte de Fétiche soustrait 1D.
*/
_setSelfControlMax() {
const abilities = Object.values(this.abilities)
@@ -165,12 +172,15 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
.filter(a => a.category === "mental" || a.category === "social")
.reduce((acc, a) => acc + a.value, 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 :
* somme des caractéristiques physiques + manuelles + modificateur d'âge.
* Un Fétiche équipé ajoute +1D ; une perte de Fétiche soustrait 1D.
*/
_setEffortMax() {
const abilities = Object.values(this.abilities)
@@ -179,7 +189,9 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
.filter(a => a.category === "physical" || a.category === "manual")
.reduce((acc, a) => acc + a.value, 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))
}
/**
+2 -1
View File
@@ -13,7 +13,8 @@ export default class VermineEvolutionData extends foundry.abstract.TypeDataModel
const fields = foundry.data.fields
return {
...listItemSchema(),
level: levelSchema(1, 1, 4)
level: levelSchema(1, 1, 4),
mutation: new fields.BooleanField({ required: true, initial: false })
}
}
}
+6 -4
View File
@@ -129,12 +129,14 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
}
/**
* Calcule la réserve max en fonction du niveau du groupe.
* Règle simplifiée : niveau × 2, plafonné à 10.
* Calcule la réserve max selon le nombre de membres (personnages).
* Règles p. 68-69 : la Réserve débute à 3D et gagne 1D par personnage,
* avec un maximum de 10D.
*/
_calculateGroupReserve() {
const level = this.level?.value || 1
this.reserve.max = Math.min(10, level * 2)
const members = Array.isArray(this.members) ? this.members : []
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) {
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({
value: new fields.BooleanField({ required: true, initial: false }),
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 }),
max_range: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
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: "" }),
addVigor: new fields.BooleanField({ required: true, initial: false })
}),
+18 -1
View File
@@ -288,6 +288,23 @@ VERMINE.totems = {
"loner": "TOTEMS.loner.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 = {
"human": "TOTEMS.adapted.name",
"predator": "TOTEMS.scavenger.name",
@@ -345,7 +362,7 @@ VERMINE.traits = {
},
"fetiche": {
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
},
"incapacitant": {
@@ -0,0 +1,127 @@
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 consomme
* un nombre de Dés d'Évolution égal au niveau choisi.
*/
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 });
}
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] || ""
};
}
async _onRender() {
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
this.#updateUI();
}
/** Met à jour le total des niveaux « après achat » selon le niveau sélectionné. */
#updateUI() {
const cost = parseInt(this.element.querySelector("#evolution-level")?.value, 10) || 0;
const el = this.element.querySelector("#evolution-total-value");
if (el) el.textContent = `${VermineExperience.evolutionTotalLevels(this.#actor) + cost}D`;
}
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
}
}]);
// Ne consomme que le coût du niveau choisi (Règles p. 116-117 : coût = niveau).
if (!created || created.length === 0) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_create"));
return;
}
await actor.update({ "system.experience.evolution": Math.max(0, (actor.system?.experience?.evolution || 0) - cost) });
ui.notifications.info(game.i18n.localize("VERMINE.buy_evolution_done"));
this.close();
}
}
+42 -6
View File
@@ -1,5 +1,6 @@
import { VermineUtils } from "../roll.mjs";
import { VermineExchange } from "../exchange.mjs";
import { VermineTraits } from "../traits.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
@@ -50,12 +51,19 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
this.weapon = options.weapon ?? null;
this.mode = options.mode ?? "contact";
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.skillPool = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.dicePool || 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.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() {
@@ -147,7 +155,12 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
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("#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("#adapted-totem")?.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;
}
#getTemporaryMalus() {
return parseInt(this.#el.querySelector("#temporary-malus")?.value, 10) || 0;
}
#getUseAmmo() {
return this.#el.querySelector("#use-ammo")?.checked ?? false;
}
#getRafale() {
return this.#el.querySelector("#rafale")?.checked ?? false;
}
#getTotems() {
return {
human: this.#el.querySelector("#human-totem")?.checked ?? false,
@@ -222,7 +244,7 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
}
getDifficultySelect() {
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0);
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0) + (this.#getRafale() ? 2 : 0);
}
getSkillCategory() {
@@ -260,9 +282,12 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
const malus = VermineExchange.woundMalus(this.actor);
const wound = VermineExchange.woundMalus(this.actor);
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, malus, total };
return { ability, skill, selfControl, pools, specialty, help, tooling, group, wound, lourd, tempMalus, malus, total };
}
getDicePool() {
@@ -365,9 +390,13 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
targets,
weapon: this.weapon
? { 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
};
const rafale = this.#getRafale();
const rollParams = {
actor: this.actor,
NoD: this.getDicePool(),
@@ -389,6 +418,13 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
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.
+29 -13
View File
@@ -48,13 +48,18 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
if (!memberOf.has(id)) memberOf.set(id, g.id);
}
}
const groupById = new Map(groups.map(g => [g.id, g]));
return {
characters: characters.map(a => ({
id: a.id,
name: a.name,
dice: a.system?.experience?.dice || 0,
groupId: memberOf.get(a.id) || ""
})),
characters: characters.map(a => {
const groupId = memberOf.get(a.id) || "";
return {
id: a.id,
name: a.name,
dice: a.system?.experience?.dice || 0,
groupId,
groupName: groupId ? groupById.get(groupId)?.name : ""
};
}),
groups: groups.map(a => ({
id: a.id,
name: a.name,
@@ -72,6 +77,9 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
async _onRender(context, options) {
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
this.element.querySelector("#group-select")?.addEventListener("change", () => this.#updateGroupFilter());
this.element.querySelectorAll("[name^='include-']").forEach(cb =>
cb.addEventListener("click", event => event.stopPropagation())
);
this.#updateUI();
this.#updateGroupFilter();
}
@@ -84,11 +92,16 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
if (el) el.textContent = `${danger + discoveries + duration}D`;
}
/** Filtre les personnages selon le groupe sélectionné (vide = tous). */
/** Filtre les personnages selon le groupe sélectionné (vide = tous, "none" = sans groupe). */
#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;
const rowGroupId = row.dataset.groupId || "";
const show = groupId === ""
? true
: groupId === "none"
? rowGroupId === ""
: rowGroupId === groupId;
row.hidden = !show;
}
}
@@ -102,8 +115,8 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
+ (parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0)
+ (parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0);
const groupId = this.element.querySelector("#group-select")?.value || null;
if (groupId) {
const groupId = this.element.querySelector("#group-select")?.value || "";
if (groupId && groupId !== "none") {
const group = game.actors.get(groupId);
if (group && collective > 0) {
const current = group.system?.experience?.dice || 0;
@@ -121,8 +134,11 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
const results = [];
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;
// Filtre par groupe sélectionné (vide = tous, "none" = sans groupe)
const rowGroupId = row.dataset.groupId || "";
if (groupId === "none" ? rowGroupId !== "" : groupId !== "" && rowGroupId !== groupId) continue;
// Case « Inclure » décochée → personnage exclu de cette phase
if (!row.querySelector(`[name="include-${row.dataset.characterId}"]`)?.checked) continue;
const id = row.dataset.characterId;
const actor = game.actors.get(id);
if (!actor) continue;
@@ -164,7 +180,7 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
}
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
if (groupId) {
if (groupId && groupId !== "none") {
const group = game.actors.get(groupId);
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
}
+21 -2
View File
@@ -140,6 +140,21 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
inp.addEventListener("change", this.#onInputChange.bind(this))
}
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()
}
@@ -159,11 +174,15 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
#getGroup() { return parseInt(this.#el.querySelector("#group")?.value, 10) || 0 }
#getTemporaryMalus() { return parseInt(this.#el.querySelector("#temporary-malus")?.value, 10) || 0 }
getPoolBreakdown() {
const help = this.#getHelped()
const group = this.#getGroup()
const malus = VermineExchange.woundMalus(this.#actor)
return { pool: this.pool, help, group, malus, total: Math.max(0, this.pool + help + group - malus) }
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() {
+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();
}
}
+103 -16
View File
@@ -1,5 +1,6 @@
import { VermineUtils } from "../roll.mjs";
import { VermineExchange } from "../exchange.mjs";
import { VermineTraits } from "../traits.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
@@ -65,6 +66,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
const isCharacter = actor.type === "character";
const group = isCharacter ? RollDialog.#findGroup(actor) : null;
const groupPool = group?.system?.experience?.dice || 0;
const intimidant = isCharacter ? VermineTraits.equippedItems(actor, 'intimidant')[0] ?? null : null;
return {
actor,
system: actor.system,
@@ -78,6 +80,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
specialty: false,
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
availableItems: actor.items.filter(i => i.type === "item"),
hasIntimidant: Boolean(intimidant),
intimidantName: intimidant?.name ?? "",
locks: this.locks,
experience: {
hasExperience: isCharacter,
@@ -140,6 +144,50 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
#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.
* @param {Actor} actor
@@ -158,16 +206,17 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const specChecked = this.hasSpecialtySelected();
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 experience = this.#getExperiencePull();
const specialty = specChecked ? 1 : 0;
const help = helped ? 1 : 0;
const tooling = tools ? 1 : 0;
const malus = VermineExchange.woundMalus(this.#actor);
const tooling = this.#getTooling();
const wound = VermineExchange.woundMalus(this.#actor);
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, malus, total };
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, wound, tempMalus, psychology, malus, total };
}
getDicePool() {
@@ -188,12 +237,15 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
getReroll() {
const sel = this.#getSkill();
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() {
const sel = this.#getHandicap();
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
// Trait Ponctuel : l'outil sélectionné annule jusqu'à (n) Handicaps.
return this.#getBaseHandicap() - this.getPonctuelUsed();
}
getSkillCategory() {
@@ -221,14 +273,13 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
getLabel() {
const type = this.getRollType();
if (type === "skill") {
const sel = this.#getSkill();
const idx = sel?.selectedIndex ?? 0;
return sel?.options[idx]?.dataset?.label ?? "";
}
const sel = this.#getAbility();
const sel = type === "skill" ? this.#getSkill() : this.#getAbility();
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() {
@@ -285,6 +336,19 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
opt.selected = 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() {
@@ -342,7 +406,10 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
const handSel = this.#getHandicap();
const handEl = this.#el.querySelector("#current-handicap");
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();
@@ -450,6 +517,22 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
await group.update({ "system.experience.dice": groupPool - pulled });
}
// 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,
NoD: this.getDicePool(),
@@ -470,6 +553,10 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
targets: [...game.user.targets].map(t => t.name),
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.
+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();
}
}
+86 -9
View File
@@ -369,6 +369,12 @@ export class VermineExchange {
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
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) {
@@ -407,7 +413,7 @@ export class VermineExchange {
const dialog = await NpcRollDialog.create({
actor,
rolltype: 'action',
locks: { difficulty: exchange.difficulty },
locks: { difficulty: exchange.difficulty, handicap: exchange.rapideHandicap ?? null },
defense
})
if (dialog) dialog.render(true)
@@ -426,14 +432,16 @@ export class VermineExchange {
}
static #defenseLocks(actor, exchange, type) {
if (type === 'parade') {
return { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
}
return {
ability: 'reflexes',
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
difficulty: exchange.difficulty
}
const locks = type === 'parade'
? { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
: {
ability: 'reflexes',
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
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) {
@@ -479,6 +487,75 @@ export class VermineExchange {
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) {
ev.preventDefault()
const btn = ev.currentTarget
+58
View File
@@ -233,4 +233,62 @@ export class VermineExperience {
}
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
for (let c of combatants) {
let actor = game.actors.get(c.actorId)
actor.clearInitiative()
actor.displayRecuperation()
}
super._onDelete()*/
/**
* Nettoyage lors de la suppression du combat (bouton « Terminer » / suppression).
* Doit appeler le handler de base : c'est lui qui re-rend le CombatTracker
* (`ui.combat.render({combat: null})`) sans cela le tracker reste figé sur
* l'ancien ordre de tour jusqu'au rechargement de la page (F5).
* @param {object} options
* @param {string} userId
*/
_onDelete(options, userId) {
super._onDelete(options, userId);
}
}
export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
/** @override */
static DEFAULT_OPTIONS = {
actions: {
deleteCombatant: VermineCombatTracker.#onDeleteCombatant,
deleteCombat: VermineCombatTracker.#onDeleteCombat
}
};
/** @override */
static PARTS = {
main: {
@@ -248,13 +258,17 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
Object.assign(context, {
combat,
hasCombat,
combats: combats.map((c, i) => ({ id: c.id, name: c.name, label: i + 1, active: i === currentIdx })),
combatCount: combats.length,
currentIndex: currentIdx + 1,
previousId: combats[currentIdx - 1]?.id,
nextId: combats[currentIdx + 1]?.id,
round: combat?.round,
displayCycle: combats.length > 7,
control: isPlayerTurn && canControl,
linked: combat?.scene !== null,
initiativeIcon: CONFIG.Combat.initiativeIcon,
css: combats.length > 7 ? "cycle" : combats.length ? "tabbed" : "",
cssClass: combats.length > 7 ? "cycle" : combats.length ? "tabbed" : "",
cssId: "combat",
tabName: "combat",
@@ -264,12 +278,21 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
turns: []
});
// Tracker part : construit les tours + gestion des décimales d'initiative.
let hasDecimals = false;
if ( combat ) {
for ( const [i, combatant] of combat.turns.entries() ) {
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;
}
@@ -318,6 +341,31 @@ export class VermineCombatTracker extends foundry.applications.sidebar.tabs.Comb
let actor = await game.actors.get(combatant.actorId)
// 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 {
@@ -330,6 +378,18 @@ export class VermineCombatant extends Combatant {
}
}
_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)
}
}
+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 */
/* -------------------------------------------- */
+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 {
/**
* 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 {number} [options.skillLevel=null] - Skill level for auto-successes
* @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
*/
static async roll({
@@ -31,12 +37,17 @@ export class VermineUtils {
hasSpecialty = false,
handicap = 0,
bonusSuccesses = 0,
bonusOnSuccess = false,
poolBreakdown = null,
specialtyName = null,
weapon = null,
targets = [],
attack = null,
messageFlags = null
messageFlags = null,
effectiveRange = null,
mobilityHandicap = 0,
ponctuelUsed = 0,
intimidant = null
}) {
// Validate inputs
if (!actor) {
@@ -49,12 +60,12 @@ export class VermineUtils {
// Declare variables
let formula = "";
let modFormula = null;
let totemBonus = { human: 0, adapted: 0 };
// Calculate domain bonuses for totems
if (skillCategory) {
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor);
}
// Influence des Dés de Totems : Bonus/Malus de 1D selon le Totem dominant
// et le Domaine de la Compétence (règles p. 121). S'applique même sans
// utiliser un Dé de Totem.
const influenceMod = this._calculateTotemInfluence(skillCategory, actor);
NoD += influenceMod;
// Apply automatic successes from skill mastery
let adjustedDifficulty = difficulty;
@@ -65,11 +76,6 @@ export class VermineUtils {
const humanDifficulty = adjustedDifficulty;
const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`;
// Apply domain bonus/malus
if (totemBonus.human !== 0) {
NoD += totemBonus.human;
}
modFormula = humanFormula;
}
@@ -79,11 +85,6 @@ export class VermineUtils {
const adaptedDifficulty = adjustedDifficulty;
const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`;
// Apply domain bonus/malus
if (totemBonus.adapted !== 0) {
NoD += totemBonus.adapted;
}
// Build combined formula
if (modFormula !== null) {
modFormula = `${modFormula}+${adaptedFormula}`;
@@ -113,8 +114,8 @@ export class VermineUtils {
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
// Reconcile the breakdown displayed in chat with the dice actually rolled.
// Domain bonuses/malus are only applied when the corresponding totem die
// is used, and totem dice replace (not add to) base-pool dice.
// L'influence s'applique au pool ; les Dés de Totem remplacent (n'ajoutent
// pas) des dés du pool de base.
if (poolBreakdown) {
// Count totem dice present in the final modifier formula.
let totemDice = 0;
@@ -122,14 +123,9 @@ export class VermineUtils {
if (modFormula.includes("human_")) 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.domainBonus = domainBonus;
poolBreakdown.domainBonus = influenceMod;
// 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;
}
@@ -145,7 +141,7 @@ export class VermineUtils {
skillCategory: skillCategory,
skillLevel: skillLevel,
hasSpecialty: hasSpecialty,
totemBonuses: { ...totemBonus },
totemInfluence: influenceMod,
baseNoD: NoD,
rerolls: Reroll,
selfControl: self_control,
@@ -173,12 +169,17 @@ export class VermineUtils {
hasSpecialty,
handicap,
bonusSuccesses,
bonusOnSuccess,
poolBreakdown,
specialtyName,
weapon,
targets,
attack,
messageFlags
messageFlags,
effectiveRange,
mobilityHandicap,
ponctuelUsed,
intimidant
});
return roll;
@@ -188,47 +189,12 @@ export class VermineUtils {
* Calculates domain bonuses/penalties for totems.
* @param {string} skillCategory - The skill category
* @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) {
const bonuses = { human: 0, adapted: 0 };
// Validate inputs
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;
static _calculateTotemInfluence(skillCategory, actor) {
// Les Dés de Totems n'existent que sur les personnages.
if (actor?.type !== "character" || !skillCategory) return 0;
return VermineTotemDice.influenceModifier(actor, skillCategory);
}
/**
@@ -493,8 +459,12 @@ export class VermineUtils {
}
// Verdict
const required = 1 + (param.handicap ?? 0);
const bonusSuccesses = param.bonusSuccesses ?? 0;
const total = (roll._total ?? 0) + bonusSuccesses;
const rawTotal = roll._total ?? 0;
// 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 = {
success: total >= required,
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;
}
}
@@ -16,11 +16,11 @@ system:
value: 3
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -29,8 +29,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198965
modifiedTime: 1785775198965
createdTime: 1786119843343
modifiedTime: 1786119843357
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!vPA7gekUx8kUNOWB'
@@ -13,11 +13,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198966
modifiedTime: 1785775198966
createdTime: 1786119843344
modifiedTime: 1786119843357
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!DItkLLGSyjAGzruO'
@@ -13,11 +13,11 @@ system:
value: 3
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198966
modifiedTime: 1785775198966
createdTime: 1786119843345
modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!Cs6ibCrcPxim7QHR'
@@ -14,11 +14,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -27,8 +27,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198967
modifiedTime: 1785775198967
createdTime: 1786119843345
modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!pP9hfvk5JKsoFcnt'
@@ -12,11 +12,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198967
modifiedTime: 1785775198967
createdTime: 1786119843345
modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!h3Q6LwnnVuj6MH4Y'
@@ -13,11 +13,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198967
modifiedTime: 1785775198967
createdTime: 1786119843346
modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!JrtHAUqFgA75e6pq'
@@ -16,11 +16,11 @@ system:
value: 3
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -29,8 +29,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198967
modifiedTime: 1785775198967
createdTime: 1786119843346
modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!t4p4n73kwwVoeSMe'
@@ -12,11 +12,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198968
modifiedTime: 1785775198968
createdTime: 1786119843346
modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!2h0xUozy1xqLdKcE'
@@ -13,11 +13,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198968
modifiedTime: 1785775198968
createdTime: 1786119843347
modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!oNfjRGGRVFGtfbK0'
@@ -9,11 +9,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -22,8 +22,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198968
modifiedTime: 1785775198968
createdTime: 1786119843347
modifiedTime: 1786119843358
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!vgYLoiEqEOxrGdy6'
@@ -12,11 +12,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198968
modifiedTime: 1785775198968
createdTime: 1786119843347
modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!uGG0FrBn1QtMcxbz'
@@ -16,11 +16,11 @@ system:
value: 4
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -29,8 +29,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198968
modifiedTime: 1785775198968
createdTime: 1786119843347
modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!bV6V4i1v4ORWDJSQ'
@@ -13,11 +13,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198969
modifiedTime: 1785775198969
createdTime: 1786119843348
modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!S8AwTKDxIdez7vHB'
@@ -11,11 +11,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -24,8 +24,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198969
modifiedTime: 1785775198969
createdTime: 1786119843348
modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!RjdZB4Qd2V8mp44x'
@@ -17,11 +17,11 @@ system:
value: 3
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -30,8 +30,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198969
modifiedTime: 1785775198969
createdTime: 1786119843348
modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!kPoioCvlK7H0uJB1'
@@ -13,11 +13,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198969
modifiedTime: 1785775198969
createdTime: 1786119843349
modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!EqeONvUCK7OhJaNN'
@@ -19,11 +19,11 @@ system:
value: 3
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -32,8 +32,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198969
modifiedTime: 1785775198969
createdTime: 1786119843349
modifiedTime: 1786119843359
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!8ShwJQCWKHsfXLko'
@@ -13,11 +13,11 @@ system:
value: 4
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198970
modifiedTime: 1785775198970
createdTime: 1786119843349
modifiedTime: 1786119843360
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!ivx8gS0XBEcoqtdl'
@@ -15,11 +15,11 @@ system:
value: 4
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -28,8 +28,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198970
modifiedTime: 1785775198970
createdTime: 1786119843350
modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!HfC2oOARrpKnpoQf'
@@ -14,11 +14,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -27,8 +27,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198970
modifiedTime: 1785775198970
createdTime: 1786119843350
modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!Re0aeZzupnz3EL6E'
@@ -20,11 +20,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -33,8 +33,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198970
modifiedTime: 1785775198970
createdTime: 1786119843350
modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!UxNguZd8K62nt1GH'
@@ -17,11 +17,11 @@ system:
value: 3
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -30,8 +30,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198971
modifiedTime: 1785775198971
createdTime: 1786119843351
modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!AAw7U5tUxbUczorz'
@@ -11,11 +11,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -24,8 +24,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198971
modifiedTime: 1785775198971
createdTime: 1786119843351
modifiedTime: 1786119843362
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!guC1pOfmcmitRGDo'
@@ -16,11 +16,11 @@ system:
value: 4
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -29,8 +29,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198971
modifiedTime: 1785775198971
createdTime: 1786119843351
modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!kFak0PMoNAuPnSfX'
@@ -23,11 +23,11 @@ system:
value: 4
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -36,8 +36,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198971
modifiedTime: 1785775198971
createdTime: 1786119843352
modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!4jRJw5vuhZeqoKy9'
@@ -24,11 +24,11 @@ system:
value: 4
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -37,8 +37,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198972
modifiedTime: 1785775198972
createdTime: 1786119843352
modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!Y8UeTOYpGkM3nt9R'
@@ -11,11 +11,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -24,8 +24,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198972
modifiedTime: 1785775198972
createdTime: 1786119843352
modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!BNZeEd0CpTgYkEou'
@@ -15,11 +15,11 @@ system:
value: 3
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -28,8 +28,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198972
modifiedTime: 1785775198972
createdTime: 1786119843353
modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!wH5OPnv4cenzpgws'
@@ -14,11 +14,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -27,8 +27,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198973
modifiedTime: 1785775198973
createdTime: 1786119843354
modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!rCQOrAzaCgCF0Dri'
@@ -14,11 +14,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -27,8 +27,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198972
modifiedTime: 1785775198972
createdTime: 1786119843353
modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!DHv0PXsIgQ8rPPUY'
@@ -15,11 +15,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -28,8 +28,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198973
modifiedTime: 1785775198973
createdTime: 1786119843354
modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!IMXQRepTNtiHZcaf'
@@ -12,11 +12,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198972
modifiedTime: 1785775198972
createdTime: 1786119843353
modifiedTime: 1786119843363
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!nxy53EF4ZRSSwCvr'
@@ -13,11 +13,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198973
modifiedTime: 1785775198973
createdTime: 1786119843354
modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!uneyg1G0Gpbb7l5P'
@@ -12,11 +12,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198973
modifiedTime: 1785775198973
createdTime: 1786119843354
modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!mxzf2Od6SMlvhKon'
@@ -12,11 +12,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -25,8 +25,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198973
modifiedTime: 1785775198973
createdTime: 1786119843355
modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!71goz10chR7bPQZY'
@@ -13,11 +13,11 @@ system:
value: 4
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198974
modifiedTime: 1785775198974
createdTime: 1786119843355
modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!haijrxkf8tAxmWx5'
@@ -13,11 +13,11 @@ system:
value: 2
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198974
modifiedTime: 1785775198974
createdTime: 1786119843355
modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!qdtjHLeC7pKh57tP'
@@ -19,11 +19,11 @@ system:
value: 4
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -32,8 +32,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198974
modifiedTime: 1785775198974
createdTime: 1786119843356
modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!VIPHgGAqSklGFhFh'
@@ -13,11 +13,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -26,8 +26,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198974
modifiedTime: 1785775198974
createdTime: 1786119843356
modifiedTime: 1786119843364
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!ZcvIaqqfjvy67qWx'
@@ -18,11 +18,11 @@ system:
value: 3
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -31,8 +31,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198974
modifiedTime: 1785775198974
createdTime: 1786119843356
modifiedTime: 1786119843365
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!noIASNejaUkv0Qo3'
@@ -11,11 +11,11 @@ system:
value: 1
min: 1
max: 4
mutation: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -24,8 +24,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198975
modifiedTime: 1785775198975
createdTime: 1786119843357
modifiedTime: 1786119843365
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!flRHah4HO4JQ4DaM'
@@ -13,8 +13,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775198946
modifiedTime: 1785775198993
createdTime: 1786119843318
modifiedTime: 1786119843391
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!folders!WTyHT2h0fsSnUHFT'
@@ -0,0 +1,41 @@
folder: 1usUE4cvarmMlQdA
name: Adage
type: ability
_id: IQftageK6SlRVKhO
img: systems/vermine2047/assets/icons/items/ability.webp
system:
description: >-
<p>Le Prêcheur bénéficie dun Bonus de 1D aux jets de Volonté pour capter
lattention de plus dun individu (hors ceux de son Groupe actif). Qui plus
est, il peut décider dutiliser sa Volonté à la place de son Savoir lorsque
sa conviction est plus importante que ses connaissances (cependant le Bonus
de 1D nest accordé que lors de ses prises de parole).</p>
type: character
totem: human
learn:
threshold: 5
hindrance: 0
level:
value: 1
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1786119900165
modifiedTime: 1786119900165
lastModifiedBy: kb05teJOwuTQVHwZ
compendiumSource: null
duplicateSource: null
exportSource: null
_key: '!items!IQftageK6SlRVKhO'
@@ -18,11 +18,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -31,8 +31,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775498760
modifiedTime: 1785775498760
createdTime: 1786119917828
modifiedTime: 1786119917844
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!zfeNmyLcOyfD4AxI'
@@ -26,11 +26,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -39,8 +39,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775498762
modifiedTime: 1785775498762
createdTime: 1786119917831
modifiedTime: 1786119917846
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!CQeFYqMRYMFa6pAa'
@@ -11,7 +11,7 @@ system:
les autres protagonistes doivent assister à ce duel singulier sans
intervenir. Les créatures sans aucun intellect ne sont pas affectées. Cette
Capacité ne fonctionne quen présence dau moins la moitié du Groupe.</p>
type: group
type: totem
totem: predator
learn:
threshold: 5
@@ -21,11 +21,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -34,8 +34,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775496862
modifiedTime: 1785775496862
createdTime: 1786119905163
modifiedTime: 1786119905174
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!NOBtH3xQJPptBU6I'
@@ -21,11 +21,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -34,8 +34,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775498746
modifiedTime: 1785775498746
createdTime: 1786119917816
modifiedTime: 1786119917837
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!Vggeykg6wQGTaR2o'
@@ -32,11 +32,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -45,8 +45,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775498763
modifiedTime: 1785775498763
createdTime: 1786119917831
modifiedTime: 1786119917846
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!X1RckrwGCbG7IQVR'
@@ -12,7 +12,7 @@ system:
leurs réserves, y compris celle de Groupe. Sils trouvent un objet de Rareté
supérieure ou égale à 9, le gain est de 2D par Réserve, et le Groupe gagne
1D dexpérience de Découverte.</p>
type: group
type: totem
totem: scavenger
learn:
threshold: 5
@@ -22,11 +22,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -35,8 +35,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775496847
modifiedTime: 1785775496847
createdTime: 1786119905158
modifiedTime: 1786119905173
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!1UA2GqAD1OXwuAZx'
@@ -0,0 +1,38 @@
folder: XH0LM14mhjTAIOyv
name: Apprentissage
type: ability
_id: AfWXXhXHfbmqaeGT
img: systems/vermine2047/assets/icons/items/ability.webp
system:
description: >-
<p>Lorsquun Objectif est rempli, les membres dun Clan bénéficient de 2D
dExpérience supplémentaires.</p>
type: group
totem: human
learn:
threshold: 5
hindrance: 0
level:
value: 1
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1786119897586
modifiedTime: 1786119897586
lastModifiedBy: kb05teJOwuTQVHwZ
compendiumSource: null
duplicateSource: null
exportSource: null
_key: '!items!AfWXXhXHfbmqaeGT'
@@ -9,7 +9,7 @@ system:
dExpérience de Groupe peuvent bénéficier à tous les personnages pour leurs
jets individuels sur un apprentissage commun, moyennant la dépense de 1D de
Réserve de Groupe par personnage impliqué dans lapprentissage.</p>
type: group
type: totem
totem: symbiote
learn:
threshold: 5
@@ -19,11 +19,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -32,8 +32,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775496872
modifiedTime: 1785775496872
createdTime: 1786119905167
modifiedTime: 1786119905177
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!dDIHKXaimA3aaOdO'
@@ -14,7 +14,7 @@ system:
doivent dormir, ils le feront à même le sol ou dans les sacs de couchage
dont ils disposent dans leur équipement ou sur des couches quils auront
fabriquées par ailleurs.</p>
type: group
type: totem
totem: builder
learn:
threshold: 5
@@ -24,11 +24,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -37,8 +37,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775496840
modifiedTime: 1785775496840
createdTime: 1786119905155
modifiedTime: 1786119905172
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!vXAgLueVVeORjEzO'
@@ -0,0 +1,41 @@
folder: CN0O7riHof00DkiC
name: As
type: ability
_id: AR5snodS870lKJDV
img: systems/vermine2047/assets/icons/items/ability.webp
system:
description: >-
<p>Si lArtificier tente une Action pour laquelle une Compétence du Domaine
de la Machine quil ne possède pas serait nécessaire, il ignore les
Handicaps de Rareté de cette Compétences. De plus, sil tente une Action
pour laquelle il possède la Compétence du Domaine de la Machine, mais pas la
Spécialisation, il ignore les Handicaps de Spécialisation.</p>
type: character
totem: human
learn:
threshold: 5
hindrance: 0
level:
value: 1
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1786119900163
modifiedTime: 1786119900163
lastModifiedBy: kb05teJOwuTQVHwZ
compendiumSource: null
duplicateSource: null
exportSource: null
_key: '!items!AR5snodS870lKJDV'
@@ -12,7 +12,7 @@ system:
<p>En Mode <strong>Apocalypse</strong>, les événements négatifs font baisser
la Réserve de Groupe de 2D de moins que la normale.</p>
type: group
type: totem
totem: hive
learn:
threshold: 5
@@ -22,11 +22,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -35,8 +35,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775496825
modifiedTime: 1785775496825
createdTime: 1786119905150
modifiedTime: 1786119905171
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!37V8cS5XqgodzRYB'
@@ -20,11 +20,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -33,8 +33,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775498741
modifiedTime: 1785775498741
createdTime: 1786119917813
modifiedTime: 1786119917836
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!aHRsfu7k2kEQusni'
@@ -14,7 +14,7 @@ system:
<p>En Mode <strong>Apocalypse</strong>, il est possible de dépasser le
Niveau Maître, avec un total de 5 correspondant à un Bonus de 3D et 2
Relances, et un total de 6 à un Bonus de 3D et 3 Relances.</p>
type: group
type: totem
totem: scavenger
learn:
threshold: 5
@@ -24,11 +24,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -37,8 +37,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775496848
modifiedTime: 1785775496848
createdTime: 1786119905158
modifiedTime: 1786119905173
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!svA8aIY2ACliNr4f'
@@ -20,11 +20,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -33,8 +33,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775498763
modifiedTime: 1785775498763
createdTime: 1786119917832
modifiedTime: 1786119917846
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!uzUrffNWChsgFiEm'
@@ -26,11 +26,11 @@ system:
min: 1
max: 3
effects: []
learned: false
effects: []
sort: 0
ownership:
default: 0
GXSW26kNdNoaabl8: 3
kb05teJOwuTQVHwZ: 3
flags: {}
_stats:
@@ -39,8 +39,8 @@ _stats:
coreVersion: '14.365'
systemId: vermine2047
systemVersion: 0.1.13
createdTime: 1785775498746
modifiedTime: 1785775498746
createdTime: 1786119917816
modifiedTime: 1786119917837
lastModifiedBy: kb05teJOwuTQVHwZ
exportSource: null
_key: '!items!NBsKcx5rMfarenBE'

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