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
This commit is contained in:
+20
-15
@@ -10,8 +10,8 @@ Décisions utilisateur : Fétiche +1D sur les **deux** Réserves · Malus/Durée
|
|||||||
- [~] en cours
|
- [~] en cours
|
||||||
|
|
||||||
## Tâche 0 — Prérequis communs
|
## Tâche 0 — Prérequis communs
|
||||||
- [ ] `item.mjs` : champ `equipped` sur items génériques + toggle fiche `item-item-sheet.hbs`
|
- [x] `item.mjs` : champ `equipped` sur items génériques + toggle fiche `item-item-sheet.hbs`
|
||||||
- [ ] nouveau `module/system/traits.mjs` : helpers partagés (`equippedItems`, `lourdMalus`, `mobilityNote`, `feticheItem`…)
|
- [x] nouveau `module/system/traits.mjs` : helpers partagés (`equippedItems`, `lourdMalus`, `mobilityNote`, `feticheItem`…)
|
||||||
|
|
||||||
## Phase 1 — Réparations (reliability.mjs + dialogue)
|
## 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] **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`)
|
||||||
@@ -20,24 +20,29 @@ Décisions utilisateur : Fétiche +1D sur les **deux** Réserves · Malus/Durée
|
|||||||
- [x] **Étanche** : rien de plus (immersion gérée ; gaz/maladie hors scope)
|
- [x] **Étanche** : rien de plus (immersion gérée ; gaz/maladie hors scope)
|
||||||
|
|
||||||
## Phase 2 — Combat (combatDialog.mjs, exchange.mjs)
|
## Phase 2 — Combat (combatDialog.mjs, exchange.mjs)
|
||||||
- [ ] **Lourd** : malus auto (`lourdMalus()`) dans dialogue combat
|
- [x] **Lourd** : malus auto (`lourdMalus()`) dans dialogue combat + pool breakdown (ligne « Malus Lourd »)
|
||||||
- [ ] **Rapide (n)** : handicap (n) défenseur pré-rempli + note carte d'échange
|
- [x] **Rapide (n)** : handicap (n) défenseur pré-rempli (locks.handicap) + note sur carte d'échange
|
||||||
- [ ] **Rafale (n)** : case « Tir en rafale » → difficulté +2, +n réussites si réussi
|
- [x] **Rafale (n)** : case « Tir en rafale » → difficulté +2, +n réussites si réussi (`bonusOnSuccess`)
|
||||||
- [ ] **Portée** : portée effective × Vigueur (archery/throwing) affichée
|
- [x] **Portée** : portée effective × Vigueur (n × vigor) affichée (dialogue + carte)
|
||||||
- [ ] **Maniable / Mobilité** : note de rappel dialogue combat + jets athlétiques
|
- [x] **Maniable / Mobilité** : note de rappel dialogue combat + carte (handicap de mobilité cumulé, Maniable l'annule)
|
||||||
- [ ] **Incapacitant (n)** : bouton « Jet de Santé vs (n) » sur carte d'échange
|
- [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)
|
## Phase 3 — Jets d'action (rollDialog.mjs + roll-dialog.hbs)
|
||||||
- [ ] **Ponctuel (n)** : select outil → annule n Handicaps, décrémente, supprime à 0
|
- [x] **Ponctuel (n)** : outil sélectionné → annule jusqu'à n Handicaps ; consommé au jet (décrément, Trait supprimé à 0) ; note carte
|
||||||
- [ ] **Pratique (jets)** : select d'outil → +1D / +2D si Pratique (remplace checkbox `usingTools`)
|
- [x] **Pratique (jets)** : outil sélectionné → +2D au lieu de +1D (badge « +2D » dans le dialogue)
|
||||||
- [ ] **Intimidant** : case « Brandit » → malus −1D Psychologie ou note relance
|
- [x] **Intimidant** : case « Brandit (X) » → −1D Psychologie si compétence psychologie, sinon Relance 1D ; note carte
|
||||||
- [ ] **Malus (n)** : champ « Malus temporaires » (saisie manuelle) déduit du pool
|
- [x] **Malus (n)** : champ « Malus temporaires » (saisie manuelle) déduit du pool + ligne carte
|
||||||
- [ ] **Durée (n)** : note « effet pendant n tours » sur carte de jet d'arme
|
- [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)
|
## Phase 4 — Acteur (character.mjs + fiche)
|
||||||
- [ ] **Fétiche** : +1D Effort ET Sang-Froid (corriger config « ou » → « et ») ; alerte 2+ fétiches ; action « Perdre le fétiche » (−2D, max −1D)
|
- [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)
|
||||||
- [ ] **Zone (n)** : note carte d'échange + bouton « Appliquer aux cibles de la zone »
|
- [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
|
## Transverse
|
||||||
- [x] i18n fr/en pour la Phase 1
|
- [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 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
|
||||||
|
|||||||
@@ -163,6 +163,35 @@
|
|||||||
"pools": "Reserves",
|
"pools": "Reserves",
|
||||||
"dice_pool": "Dice pool",
|
"dice_pool": "Dice pool",
|
||||||
"pool_malus": "Wound penalty",
|
"pool_malus": "Wound penalty",
|
||||||
|
"pool_wound": "Wound penalty",
|
||||||
|
"pool_lourd": "Heavy penalty",
|
||||||
|
"lourd_malus_note": "Heavy penalty: -{malus}D (Vigor too low for equipped Heavy items)",
|
||||||
|
"effective_range": "Effective range",
|
||||||
|
"traits_mobility_note": "Mobility Handicap: +{handicap} (equipped non-Maniable protection)",
|
||||||
|
"rafale_check": "Burst fire (Difficulty +2, +{rafaleValue} successes if successful)",
|
||||||
|
"rapide_note": "Fast ({handicap}): the defender suffers {handicap} Handicap(s)",
|
||||||
|
"incapacitant_roll": "Health roll",
|
||||||
|
"intimidant": "Intimidating",
|
||||||
|
"intimidant_check": "Wield ({intimidantName}) — Intimidating",
|
||||||
|
"intimidant_psychology": "1D penalty (Psychology)",
|
||||||
|
"intimidant_reroll": "1D reroll",
|
||||||
|
"temporary_malus": "Temporary penalties",
|
||||||
|
"pool_temp_malus": "Temporary penalties",
|
||||||
|
"pool_psychology": "Psychology penalty",
|
||||||
|
"ponctuel": "One-shot",
|
||||||
|
"ponctuel_used": "cancels {value} Handicap(s)",
|
||||||
|
"duree": "Duration",
|
||||||
|
"duree_note": "effect for {value} combat turns",
|
||||||
|
"fetiche": "Fetish",
|
||||||
|
"fetiche_bonus": "Effort and Composure reserves +1D",
|
||||||
|
"fetiche_multiple": "A character can only possess a single Fetish object",
|
||||||
|
"fetiche_none": "No equipped Fetish object",
|
||||||
|
"fetiche_lost": "Fetish lost: reserves −2D, max −1D",
|
||||||
|
"fetiche_lost_penalty": "Fetish lost: reserves reduced by 1D",
|
||||||
|
"lose_fetiche": "Lose the fetish",
|
||||||
|
"zone_note": "Area ({value} m): everyone in the area suffers the damage",
|
||||||
|
"zone_apply": "Apply to targets in the area",
|
||||||
|
"zone_applied": "Area: {count} wound(s) applied",
|
||||||
"auto_successes": "automatic successes",
|
"auto_successes": "automatic successes",
|
||||||
"self_control": "Self-control",
|
"self_control": "Self-control",
|
||||||
"effort": "Effort",
|
"effort": "Effort",
|
||||||
|
|||||||
@@ -170,6 +170,35 @@
|
|||||||
"reserves": "Réserves",
|
"reserves": "Réserves",
|
||||||
"dice_pool": "Pool de dés",
|
"dice_pool": "Pool de dés",
|
||||||
"pool_malus": "Malus Blessure",
|
"pool_malus": "Malus Blessure",
|
||||||
|
"pool_wound": "Malus Blessure",
|
||||||
|
"pool_lourd": "Malus Lourd",
|
||||||
|
"lourd_malus_note": "Malus Lourd : -{malus}D (Vigueur insuffisante pour les objets Lourds équipés)",
|
||||||
|
"effective_range": "Portée effective",
|
||||||
|
"traits_mobility_note": "Handicap de Mobilité : +{handicap} (protections équipées non Maniables)",
|
||||||
|
"rafale_check": "Tir en rafale (Difficulté +2, +{rafaleValue} Réussites si réussi)",
|
||||||
|
"rapide_note": "Rapide ({handicap}) : le défenseur subit {handicap} Handicap(s)",
|
||||||
|
"incapacitant_roll": "Jet de Santé",
|
||||||
|
"intimidant": "Intimidant",
|
||||||
|
"intimidant_check": "Brandit ({intimidantName}) — Intimidant",
|
||||||
|
"intimidant_psychology": "Malus 1D (Psychologie)",
|
||||||
|
"intimidant_reroll": "Relance 1D",
|
||||||
|
"temporary_malus": "Malus temporaires",
|
||||||
|
"pool_temp_malus": "Malus temporaires",
|
||||||
|
"pool_psychology": "Malus Psychologie",
|
||||||
|
"ponctuel": "Ponctuel",
|
||||||
|
"ponctuel_used": "annule {value} Handicap(s)",
|
||||||
|
"duree": "Durée",
|
||||||
|
"duree_note": "effet pendant {value} tours de combat",
|
||||||
|
"fetiche": "Fétiche",
|
||||||
|
"fetiche_bonus": "Réserves d'Effort et de Sang-Froid +1D",
|
||||||
|
"fetiche_multiple": "Un même personnage ne peut posséder qu'un seul objet Fétiche",
|
||||||
|
"fetiche_none": "Aucun objet Fétiche équipé",
|
||||||
|
"fetiche_lost": "Fétiche perdu : Réserves −2D, max −1D",
|
||||||
|
"fetiche_lost_penalty": "Fétiche perdu : Réserves réduites de 1D",
|
||||||
|
"lose_fetiche": "Perdre le fétiche",
|
||||||
|
"zone_note": "Zone ({value} m) : tous les présents dans la zone subissent les Dommages",
|
||||||
|
"zone_apply": "Appliquer aux cibles de la zone",
|
||||||
|
"zone_applied": "Zone : {count} blessure(s) appliquée(s)",
|
||||||
"auto_successes": "réussites automatiques",
|
"auto_successes": "réussites automatiques",
|
||||||
"self_control": "Sang-Froid",
|
"self_control": "Sang-Froid",
|
||||||
"effort": "Effort",
|
"effort": "Effort",
|
||||||
|
|||||||
@@ -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 {
|
.items-list {
|
||||||
.item-name .item-image {
|
.item-name .item-image {
|
||||||
flex: 0 0 30px;
|
flex: 0 0 30px;
|
||||||
|
|||||||
@@ -857,6 +857,16 @@ ol.chat-log {
|
|||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.duree-note {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-style: italic;
|
||||||
|
color: @theme-color-accent;
|
||||||
|
|
||||||
|
i { color: @color-hemolymph; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Échange de coups ─────────────────────────────────────────────
|
// ── Échange de coups ─────────────────────────────────────────────
|
||||||
@@ -880,6 +890,19 @@ ol.chat-log {
|
|||||||
i { color: @color-hemolymph; }
|
i { color: @color-hemolymph; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.exchange-reminder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-style: italic;
|
||||||
|
color: @theme-color-accent;
|
||||||
|
padding: 2px 0;
|
||||||
|
|
||||||
|
i { color: @color-hemolymph; }
|
||||||
|
}
|
||||||
|
|
||||||
.exchange-target {
|
.exchange-target {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -938,6 +961,31 @@ ol.chat-log {
|
|||||||
background: @color-hemolymph;
|
background: @color-hemolymph;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.exchange-incapacitant,
|
||||||
|
button.exchange-zone {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
padding: 3px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: @theme-color-dark;
|
||||||
|
background: fade(@color-acid-green, 20%);
|
||||||
|
border: 1px solid @color-acid-green;
|
||||||
|
border-radius: 3px;
|
||||||
|
.transition();
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @theme-color-dark;
|
||||||
|
background: @color-acid-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.exchange-resolution {
|
div.exchange-resolution {
|
||||||
|
|||||||
@@ -350,6 +350,24 @@ iframe {
|
|||||||
color: @color-acid-green;
|
color: @color-acid-green;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-bonus,
|
||||||
|
.tool-ponctuel {
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-bonus {
|
||||||
|
color: @color-acid-green;
|
||||||
|
background: fade(@color-acid-green, 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-ponctuel {
|
||||||
|
color: @theme-color-dark;
|
||||||
|
background: fade(@color-hemolymph, 18%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.full-width {
|
&.full-width {
|
||||||
@@ -408,6 +426,16 @@ iframe {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.traits-reminder {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-style: italic;
|
||||||
|
color: @theme-color-accent;
|
||||||
|
|
||||||
|
i { color: @color-hemolymph; }
|
||||||
|
}
|
||||||
|
|
||||||
.locked-value {
|
.locked-value {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
|||||||
openLearning: VermineCharacterSheetV2.#onOpenLearning,
|
openLearning: VermineCharacterSheetV2.#onOpenLearning,
|
||||||
validatePhase: VermineCharacterSheetV2.#onValidatePhase,
|
validatePhase: VermineCharacterSheetV2.#onValidatePhase,
|
||||||
buyEvolution: VermineCharacterSheetV2.#onBuyEvolution,
|
buyEvolution: VermineCharacterSheetV2.#onBuyEvolution,
|
||||||
openTotemDice: VermineCharacterSheetV2.#onOpenTotemDice
|
openTotemDice: VermineCharacterSheetV2.#onOpenTotemDice,
|
||||||
|
loseFetiche: VermineCharacterSheetV2.#onLoseFetiche
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +81,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
|||||||
case "stories":
|
case "stories":
|
||||||
context.tab = context.tabs.stories
|
context.tab = context.tabs.stories
|
||||||
break
|
break
|
||||||
case "combat":
|
case "combat": {
|
||||||
context.tab = context.tabs.combat
|
context.tab = context.tabs.combat
|
||||||
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
|
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
|
||||||
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
|
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
|
||||||
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
||||||
context.effects = prepareActiveEffectCategories(doc.effects)
|
context.effects = prepareActiveEffectCategories(doc.effects)
|
||||||
|
const { VermineTraits } = await import("../../system/traits.mjs")
|
||||||
|
const feticheItems = VermineTraits.equippedItems(doc, 'fetiche')
|
||||||
|
context.feticheItems = feticheItems
|
||||||
|
context.fetiche = feticheItems[0] ?? null
|
||||||
|
context.feticheMultiple = feticheItems.length > 1
|
||||||
|
const loss = doc.system.experience?.feticheLoss
|
||||||
|
context.feticheLost = Boolean(loss?.self_control || loss?.effort)
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case "experience":
|
case "experience":
|
||||||
context.tab = context.tabs.experience
|
context.tab = context.tabs.experience
|
||||||
context.specialtyCount = doc.itemTypes.specialty.length
|
context.specialtyCount = doc.itemTypes.specialty.length
|
||||||
@@ -149,4 +158,35 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
|||||||
const dialog = await TotemDiceDialog.create({ actorId: this.document.id })
|
const dialog = await TotemDiceDialog.create({ actorId: this.document.id })
|
||||||
if (dialog) dialog.render(true)
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
skillCategoriesSchema,
|
skillCategoriesSchema,
|
||||||
skillsSchema
|
skillsSchema
|
||||||
} from "./_shared.mjs"
|
} from "./_shared.mjs"
|
||||||
|
import { VermineTraits } from "../system/traits.mjs"
|
||||||
|
|
||||||
export default class VermineCharacterData extends foundry.abstract.TypeDataModel {
|
export default class VermineCharacterData extends foundry.abstract.TypeDataModel {
|
||||||
|
|
||||||
@@ -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 }),
|
self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||||
effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
|
effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
|
||||||
}),
|
}),
|
||||||
|
// Perte de Réserves après perte du Fétiche : -1D de max persistant par Réserve
|
||||||
|
feticheLoss: new fields.SchemaField({
|
||||||
|
self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||||
|
effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
|
||||||
|
}),
|
||||||
// Limites d'apprentissage par phase
|
// Limites d'apprentissage par phase
|
||||||
learned: new fields.SchemaField({
|
learned: new fields.SchemaField({
|
||||||
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||||
@@ -157,6 +163,7 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
|||||||
/**
|
/**
|
||||||
* Calcule le max de sang-froid :
|
* Calcule le max de sang-froid :
|
||||||
* somme des caractéristiques mentales + sociales + modificateur d'âge.
|
* somme des caractéristiques mentales + sociales + modificateur d'âge.
|
||||||
|
* Un Fétiche équipé ajoute +1D ; une perte de Fétiche soustrait 1D.
|
||||||
*/
|
*/
|
||||||
_setSelfControlMax() {
|
_setSelfControlMax() {
|
||||||
const abilities = Object.values(this.abilities)
|
const abilities = Object.values(this.abilities)
|
||||||
@@ -165,12 +172,15 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
|||||||
.filter(a => a.category === "mental" || a.category === "social")
|
.filter(a => a.category === "mental" || a.category === "social")
|
||||||
.reduce((acc, a) => acc + a.value, 0)
|
.reduce((acc, a) => acc + a.value, 0)
|
||||||
const bonus = this.experience?.reserveBonus?.self_control || 0
|
const bonus = this.experience?.reserveBonus?.self_control || 0
|
||||||
this.attributes.self_control.max = Math.min(10, sum + modFromAge + bonus)
|
const loss = this.experience?.feticheLoss?.self_control || 0
|
||||||
|
const fetiche = VermineTraits.feticheItem(this.parent) ? 1 : 0
|
||||||
|
this.attributes.self_control.max = Math.min(10, Math.max(0, sum + modFromAge + bonus + fetiche - loss))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcule le max d'effort :
|
* Calcule le max d'effort :
|
||||||
* somme des caractéristiques physiques + manuelles + modificateur d'âge.
|
* somme des caractéristiques physiques + manuelles + modificateur d'âge.
|
||||||
|
* Un Fétiche équipé ajoute +1D ; une perte de Fétiche soustrait 1D.
|
||||||
*/
|
*/
|
||||||
_setEffortMax() {
|
_setEffortMax() {
|
||||||
const abilities = Object.values(this.abilities)
|
const abilities = Object.values(this.abilities)
|
||||||
@@ -179,7 +189,9 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
|||||||
.filter(a => a.category === "physical" || a.category === "manual")
|
.filter(a => a.category === "physical" || a.category === "manual")
|
||||||
.reduce((acc, a) => acc + a.value, 0)
|
.reduce((acc, a) => acc + a.value, 0)
|
||||||
const bonus = this.experience?.reserveBonus?.effort || 0
|
const bonus = this.experience?.reserveBonus?.effort || 0
|
||||||
this.attributes.effort.max = Math.min(10, sum + modFromAge + bonus)
|
const loss = this.experience?.feticheLoss?.effort || 0
|
||||||
|
const fetiche = VermineTraits.feticheItem(this.parent) ? 1 : 0
|
||||||
|
this.attributes.effort.max = Math.min(10, Math.max(0, sum + modFromAge + bonus + fetiche - loss))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export default class VermineItemData extends foundry.abstract.TypeDataModel {
|
|||||||
needSkill: new fields.SchemaField({
|
needSkill: new fields.SchemaField({
|
||||||
value: new fields.BooleanField({ required: true, initial: false }),
|
value: new fields.BooleanField({ required: true, initial: false }),
|
||||||
skill: new fields.StringField({ required: true, initial: "" })
|
skill: new fields.StringField({ required: true, initial: "" })
|
||||||
})
|
}),
|
||||||
|
equipped: new fields.BooleanField({ required: true, initial: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ VERMINE.traits = {
|
|||||||
},
|
},
|
||||||
"fetiche": {
|
"fetiche": {
|
||||||
name: "Fétiche",
|
name: "Fétiche",
|
||||||
description: "Le personnage possède un objet fétiche auquel il tient.Tant qu’il le possède, sa Réserve d’Effort 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 s’appliquent 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 qu’un seul objet avec le Trait Fétiche.",
|
description: "Le personnage possède un objet fétiche auquel il tient.Tant qu’il le possède, ses Réserves d’Effort 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 s’appliquent 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 qu’un seul objet avec le Trait Fétiche.",
|
||||||
value: 0
|
value: 0
|
||||||
},
|
},
|
||||||
"incapacitant": {
|
"incapacitant": {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { VermineUtils } from "../roll.mjs";
|
import { VermineUtils } from "../roll.mjs";
|
||||||
import { VermineExchange } from "../exchange.mjs";
|
import { VermineExchange } from "../exchange.mjs";
|
||||||
|
import { VermineTraits } from "../traits.mjs";
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
@@ -56,6 +57,11 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
|||||||
this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0;
|
this.skillReroll = this.skillValue ? (CONFIG.VERMINE.SkillLevels?.[this.skillValue]?.reroll || 0) : 0;
|
||||||
this.basePool = VermineExchange.baseAttackPool(this.actor, this.weapon, this.mode);
|
this.basePool = VermineExchange.baseAttackPool(this.actor, this.weapon, this.mode);
|
||||||
this.baseDamage = VermineExchange.baseDamage(this.actor, this.weapon);
|
this.baseDamage = VermineExchange.baseDamage(this.actor, this.weapon);
|
||||||
|
this.rafaleValue = this.mode === "ranged" ? VermineTraits.traitValue(this.weapon, "rafale") : 0;
|
||||||
|
this.rapideHandicap = VermineTraits.traitValue(this.weapon, "rapide");
|
||||||
|
this.zoneValue = VermineTraits.traitValue(this.weapon, "zone");
|
||||||
|
const portee = VermineTraits.traitValue(this.weapon, "portee");
|
||||||
|
this.effectiveRange = this.mode === "ranged" && portee > 0 ? portee * VermineTraits.vigor(this.actor) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
@@ -147,7 +153,12 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
|||||||
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
|
hasAmmo: this.mode === "ranged" && (this.weapon?.system?.ammo ?? 0) > 0,
|
||||||
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
||||||
availableItems: actor.items.filter(i => i.type === "item"),
|
availableItems: actor.items.filter(i => i.type === "item"),
|
||||||
targets
|
targets,
|
||||||
|
lourdMalus: VermineTraits.lourdMalus(actor),
|
||||||
|
mobilityNote: VermineTraits.mobilityNote(actor),
|
||||||
|
hasRafale: this.rafaleValue > 0,
|
||||||
|
rafaleValue: this.rafaleValue,
|
||||||
|
effectiveRange: this.effectiveRange
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +178,7 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
|||||||
|
|
||||||
this.element.querySelector("#second-action")?.addEventListener("change", () => this.#updateUI());
|
this.element.querySelector("#second-action")?.addEventListener("change", () => this.#updateUI());
|
||||||
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
|
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
|
||||||
|
this.element.querySelector("#rafale")?.addEventListener("change", () => this.#updateUI());
|
||||||
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
|
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
|
||||||
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
|
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
|
||||||
this.element.querySelector("#pools-spent")?.addEventListener("change", () => this.#updateUI());
|
this.element.querySelector("#pools-spent")?.addEventListener("change", () => this.#updateUI());
|
||||||
@@ -210,6 +222,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
|||||||
return this.#el.querySelector("#use-ammo")?.checked ?? false;
|
return this.#el.querySelector("#use-ammo")?.checked ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#getRafale() {
|
||||||
|
return this.#el.querySelector("#rafale")?.checked ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
#getTotems() {
|
#getTotems() {
|
||||||
return {
|
return {
|
||||||
human: this.#el.querySelector("#human-totem")?.checked ?? false,
|
human: this.#el.querySelector("#human-totem")?.checked ?? false,
|
||||||
@@ -222,7 +238,7 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDifficultySelect() {
|
getDifficultySelect() {
|
||||||
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0);
|
return this.#getDifficultyBase() + (this.#getSecondAction() ? 2 : 0) + (this.#getRafale() ? 2 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSkillCategory() {
|
getSkillCategory() {
|
||||||
@@ -260,9 +276,11 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
|||||||
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
||||||
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
|
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
|
||||||
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
||||||
const malus = VermineExchange.woundMalus(this.actor);
|
const wound = VermineExchange.woundMalus(this.actor);
|
||||||
|
const lourd = VermineTraits.lourdMalus(this.actor);
|
||||||
|
const malus = wound + lourd;
|
||||||
const total = Math.max(0, this.basePool + selfControl + pools + specialty + help + tooling + group - malus);
|
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, malus, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
getDicePool() {
|
getDicePool() {
|
||||||
@@ -365,9 +383,13 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
|||||||
targets,
|
targets,
|
||||||
weapon: this.weapon
|
weapon: this.weapon
|
||||||
? { name: this.weapon.name, img: this.weapon.img, damage: foundry.utils.duplicate(this.weapon.system.damage) }
|
? { name: this.weapon.name, img: this.weapon.img, damage: foundry.utils.duplicate(this.weapon.system.damage) }
|
||||||
: null
|
: null,
|
||||||
|
rapideHandicap: this.rapideHandicap || null,
|
||||||
|
zoneValue: this.zoneValue || null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rafale = this.#getRafale();
|
||||||
|
|
||||||
const rollParams = {
|
const rollParams = {
|
||||||
actor: this.actor,
|
actor: this.actor,
|
||||||
NoD: this.getDicePool(),
|
NoD: this.getDicePool(),
|
||||||
@@ -389,6 +411,13 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
|||||||
attack,
|
attack,
|
||||||
messageFlags: { "vermine-exchange": attack }
|
messageFlags: { "vermine-exchange": attack }
|
||||||
};
|
};
|
||||||
|
if (rafale && this.rafaleValue > 0) {
|
||||||
|
rollParams.bonusSuccesses = this.rafaleValue;
|
||||||
|
rollParams.bonusOnSuccess = true;
|
||||||
|
}
|
||||||
|
if (this.effectiveRange !== null) rollParams.effectiveRange = this.effectiveRange;
|
||||||
|
const mobilityHandicap = VermineTraits.mobilityHandicap(this.actor);
|
||||||
|
if (mobilityHandicap > 0) rollParams.mobilityHandicap = mobilityHandicap;
|
||||||
|
|
||||||
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
||||||
// en arrière-plan sans bloquer la fermeture.
|
// en arrière-plan sans bloquer la fermeture.
|
||||||
|
|||||||
@@ -140,6 +140,21 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
|
|||||||
inp.addEventListener("change", this.#onInputChange.bind(this))
|
inp.addEventListener("change", this.#onInputChange.bind(this))
|
||||||
}
|
}
|
||||||
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI())
|
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI())
|
||||||
|
// Trait Rapide (n) : le défenseur subit (n) Handicaps sur sa défense.
|
||||||
|
if (this.locks?.handicap !== undefined && this.locks?.handicap !== null) {
|
||||||
|
const sel = this.#el.querySelector("#handicap")
|
||||||
|
if (sel) {
|
||||||
|
const locked = parseInt(this.locks.handicap, 10)
|
||||||
|
let opt = [...sel.options].find(o => parseInt(o.value, 10) === locked + 1)
|
||||||
|
if (!opt) {
|
||||||
|
opt = document.createElement("option")
|
||||||
|
opt.value = String(locked + 1)
|
||||||
|
opt.textContent = game.i18n.localize("VERMINE.handicap") + ` (${locked})`
|
||||||
|
sel.appendChild(opt)
|
||||||
|
}
|
||||||
|
opt.selected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
this.#updateUI()
|
this.#updateUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { VermineUtils } from "../roll.mjs";
|
import { VermineUtils } from "../roll.mjs";
|
||||||
import { VermineExchange } from "../exchange.mjs";
|
import { VermineExchange } from "../exchange.mjs";
|
||||||
|
import { VermineTraits } from "../traits.mjs";
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
const isCharacter = actor.type === "character";
|
const isCharacter = actor.type === "character";
|
||||||
const group = isCharacter ? RollDialog.#findGroup(actor) : null;
|
const group = isCharacter ? RollDialog.#findGroup(actor) : null;
|
||||||
const groupPool = group?.system?.experience?.dice || 0;
|
const groupPool = group?.system?.experience?.dice || 0;
|
||||||
|
const intimidant = isCharacter ? VermineTraits.equippedItems(actor, 'intimidant')[0] ?? null : null;
|
||||||
return {
|
return {
|
||||||
actor,
|
actor,
|
||||||
system: actor.system,
|
system: actor.system,
|
||||||
@@ -78,6 +80,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
specialty: false,
|
specialty: false,
|
||||||
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
||||||
availableItems: actor.items.filter(i => i.type === "item"),
|
availableItems: actor.items.filter(i => i.type === "item"),
|
||||||
|
hasIntimidant: Boolean(intimidant),
|
||||||
|
intimidantName: intimidant?.name ?? "",
|
||||||
locks: this.locks,
|
locks: this.locks,
|
||||||
experience: {
|
experience: {
|
||||||
hasExperience: isCharacter,
|
hasExperience: isCharacter,
|
||||||
@@ -140,6 +144,50 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
|
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
|
||||||
#getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; }
|
#getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; }
|
||||||
|
|
||||||
|
/** Clé de la compétence sélectionnée (data-label) ou null. */
|
||||||
|
getSelectedSkillKey() {
|
||||||
|
const sel = this.#getSkill();
|
||||||
|
return sel?.options[sel.selectedIndex]?.dataset?.label ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Outil générique sélectionné (radio usingTools) ou null. */
|
||||||
|
getSelectedTool() {
|
||||||
|
const checked = this.#el.querySelector("input[name='usingTools']:checked");
|
||||||
|
if (!checked || checked.value === "0") return null;
|
||||||
|
return this.#actor.items.find(i => i.name === checked.value) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bonus d'outillage : +1D, ou +2D si l'outil possède le Trait Pratique. */
|
||||||
|
#getTooling() {
|
||||||
|
const tool = this.getSelectedTool();
|
||||||
|
if (!tool) return 0;
|
||||||
|
return VermineTraits.hasTrait(tool, 'pratique') ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Malus temporaires saisis manuellement (Trait Malus). */
|
||||||
|
#getTemporaryMalus() {
|
||||||
|
return parseInt(this.#el.querySelector("#temporary-malus")?.value, 10) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Case « Brandit » (Trait Intimidant). */
|
||||||
|
getIntimidant() {
|
||||||
|
return this.#el.querySelector("#intimidant")?.checked ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Valeur de Handicap brute du sélecteur (0-2). */
|
||||||
|
#getBaseHandicap() {
|
||||||
|
const sel = this.#getHandicap();
|
||||||
|
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nombre de Handicaps annulés par l'outil Ponctuel sélectionné. */
|
||||||
|
getPonctuelUsed() {
|
||||||
|
const tool = this.getSelectedTool();
|
||||||
|
const ponctuel = tool ? VermineTraits.traitValue(tool, 'ponctuel') : 0;
|
||||||
|
if (ponctuel <= 0) return 0;
|
||||||
|
return Math.min(ponctuel, this.#getBaseHandicap());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trouve le premier Groupe dont le personnage est membre.
|
* Trouve le premier Groupe dont le personnage est membre.
|
||||||
* @param {Actor} actor
|
* @param {Actor} actor
|
||||||
@@ -158,16 +206,17 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
||||||
const specChecked = this.hasSpecialtySelected();
|
const specChecked = this.hasSpecialtySelected();
|
||||||
const helped = this.#el.querySelector("#helped")?.checked;
|
const helped = this.#el.querySelector("#helped")?.checked;
|
||||||
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
|
||||||
const tools = toolsChecked && toolsChecked.value !== "0";
|
|
||||||
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
||||||
const experience = this.#getExperiencePull();
|
const experience = this.#getExperiencePull();
|
||||||
const specialty = specChecked ? 1 : 0;
|
const specialty = specChecked ? 1 : 0;
|
||||||
const help = helped ? 1 : 0;
|
const help = helped ? 1 : 0;
|
||||||
const tooling = tools ? 1 : 0;
|
const tooling = this.#getTooling();
|
||||||
const malus = VermineExchange.woundMalus(this.#actor);
|
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);
|
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() {
|
getDicePool() {
|
||||||
@@ -188,12 +237,15 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
getReroll() {
|
getReroll() {
|
||||||
const sel = this.#getSkill();
|
const sel = this.#getSkill();
|
||||||
const idx = sel?.selectedIndex ?? 0;
|
const idx = sel?.selectedIndex ?? 0;
|
||||||
return parseInt(sel?.options[idx]?.dataset?.reroll, 10) || 0;
|
const base = parseInt(sel?.options[idx]?.dataset?.reroll, 10) || 0;
|
||||||
|
// Trait Intimidant (hors Psychologie) : Relance 1D tant que l'objet est brandi.
|
||||||
|
if (this.getIntimidant() && this.getSelectedSkillKey() !== 'psychology') return base + 1;
|
||||||
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
getHandicapSelect() {
|
getHandicapSelect() {
|
||||||
const sel = this.#getHandicap();
|
// Trait Ponctuel : l'outil sélectionné annule jusqu'à (n) Handicaps.
|
||||||
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1);
|
return this.#getBaseHandicap() - this.getPonctuelUsed();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSkillCategory() {
|
getSkillCategory() {
|
||||||
@@ -221,14 +273,13 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
|
|
||||||
getLabel() {
|
getLabel() {
|
||||||
const type = this.getRollType();
|
const type = this.getRollType();
|
||||||
if (type === "skill") {
|
const sel = type === "skill" ? this.#getSkill() : this.#getAbility();
|
||||||
const sel = this.#getSkill();
|
|
||||||
const idx = sel?.selectedIndex ?? 0;
|
const idx = sel?.selectedIndex ?? 0;
|
||||||
return sel?.options[idx]?.dataset?.label ?? "";
|
const raw = sel?.options[idx]?.dataset?.label ?? "";
|
||||||
}
|
if (!raw) return "";
|
||||||
const sel = this.#getAbility();
|
const base = type === "skill" ? "SKILLS" : "ABILITIES";
|
||||||
const idx = sel?.selectedIndex ?? 0;
|
const localized = game.i18n.localize(`${base}.${raw}.name`);
|
||||||
return sel?.options[idx]?.dataset?.label ?? "";
|
return localized === `${base}.${raw}.name` ? raw : localized;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelfControl() {
|
getSelfControl() {
|
||||||
@@ -285,6 +336,19 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
opt.selected = true;
|
opt.selected = true;
|
||||||
difficulty.disabled = true;
|
difficulty.disabled = true;
|
||||||
}
|
}
|
||||||
|
const handicap = this.#getHandicap();
|
||||||
|
if (handicap && locks.handicap !== undefined && locks.handicap !== null) {
|
||||||
|
const locked = parseInt(locks.handicap, 10);
|
||||||
|
let opt = [...handicap.options].find(o => parseInt(o.value, 10) === locked + 1);
|
||||||
|
if (!opt) {
|
||||||
|
// Handicap supérieur aux options (ex: Rapide 3) : option dédiée.
|
||||||
|
opt = document.createElement("option");
|
||||||
|
opt.value = String(locked + 1);
|
||||||
|
opt.textContent = game.i18n.localize("VERMINE.handicap") + ` (${locked})`;
|
||||||
|
handicap.appendChild(opt);
|
||||||
|
}
|
||||||
|
opt.selected = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#displaySpecialties() {
|
#displaySpecialties() {
|
||||||
@@ -342,7 +406,10 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
const handSel = this.#getHandicap();
|
const handSel = this.#getHandicap();
|
||||||
const handEl = this.#el.querySelector("#current-handicap");
|
const handEl = this.#el.querySelector("#current-handicap");
|
||||||
if (handEl && handSel) {
|
if (handEl && handSel) {
|
||||||
handEl.textContent = handSel.options[handSel.selectedIndex].text;
|
let text = handSel.options[handSel.selectedIndex].text;
|
||||||
|
const used = this.getPonctuelUsed();
|
||||||
|
if (used > 0) text += ` (${game.i18n.localize("VERMINE.ponctuel")} -${used})`;
|
||||||
|
handEl.textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const abilSel = this.#getAbility();
|
const abilSel = this.#getAbility();
|
||||||
@@ -450,6 +517,22 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
await group.update({ "system.experience.dice": groupPool - pulled });
|
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 = {
|
const rollParams = {
|
||||||
actor: this.#actor,
|
actor: this.#actor,
|
||||||
NoD: this.getDicePool(),
|
NoD: this.getDicePool(),
|
||||||
@@ -470,6 +553,10 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
|||||||
targets: [...game.user.targets].map(t => t.name),
|
targets: [...game.user.targets].map(t => t.name),
|
||||||
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
|
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
|
||||||
};
|
};
|
||||||
|
if (intimidant) {
|
||||||
|
rollParams.intimidant = { used: true, psychology: this.getSelectedSkillKey() === 'psychology' };
|
||||||
|
}
|
||||||
|
if (ponctuelUsed > 0) rollParams.ponctuelUsed = ponctuelUsed;
|
||||||
|
|
||||||
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
||||||
// en arrière-plan sans bloquer la fermeture.
|
// en arrière-plan sans bloquer la fermeture.
|
||||||
|
|||||||
@@ -369,6 +369,12 @@ export class VermineExchange {
|
|||||||
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
|
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
|
||||||
btn.addEventListener('click', ev => this.#onApply(ev, message))
|
btn.addEventListener('click', ev => this.#onApply(ev, message))
|
||||||
})
|
})
|
||||||
|
html.querySelectorAll('.exchange-incapacitant').forEach(btn => {
|
||||||
|
btn.addEventListener('click', ev => this.#onIncapacitant(ev, message))
|
||||||
|
})
|
||||||
|
html.querySelectorAll('.exchange-zone').forEach(btn => {
|
||||||
|
btn.addEventListener('click', ev => this.#onApplyZone(ev, message))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #onDefense(ev, attackMessage) {
|
static async #onDefense(ev, attackMessage) {
|
||||||
@@ -407,7 +413,7 @@ export class VermineExchange {
|
|||||||
const dialog = await NpcRollDialog.create({
|
const dialog = await NpcRollDialog.create({
|
||||||
actor,
|
actor,
|
||||||
rolltype: 'action',
|
rolltype: 'action',
|
||||||
locks: { difficulty: exchange.difficulty },
|
locks: { difficulty: exchange.difficulty, handicap: exchange.rapideHandicap ?? null },
|
||||||
defense
|
defense
|
||||||
})
|
})
|
||||||
if (dialog) dialog.render(true)
|
if (dialog) dialog.render(true)
|
||||||
@@ -426,14 +432,16 @@ export class VermineExchange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static #defenseLocks(actor, exchange, type) {
|
static #defenseLocks(actor, exchange, type) {
|
||||||
if (type === 'parade') {
|
const locks = type === 'parade'
|
||||||
return { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
|
? { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
|
||||||
}
|
: {
|
||||||
return {
|
|
||||||
ability: 'reflexes',
|
ability: 'reflexes',
|
||||||
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
|
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
|
||||||
difficulty: exchange.difficulty
|
difficulty: exchange.difficulty
|
||||||
}
|
}
|
||||||
|
// Trait Rapide (n) : le défenseur subit (n) Handicaps sur sa défense.
|
||||||
|
if (exchange.rapideHandicap) locks.handicap = exchange.rapideHandicap
|
||||||
|
return locks
|
||||||
}
|
}
|
||||||
|
|
||||||
static #parrySkill(actor) {
|
static #parrySkill(actor) {
|
||||||
@@ -479,6 +487,75 @@ export class VermineExchange {
|
|||||||
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onIncapacitant(ev) {
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
const btn = ev.currentTarget
|
||||||
|
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||||
|
if (!actor) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!game.user.isGM && !actor.isOwner) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const value = parseInt(btn.dataset.value, 10) || 7
|
||||||
|
if (actor.type === 'character') {
|
||||||
|
const { default: RollDialog } = await import('./dialogs/rollDialog.mjs')
|
||||||
|
const dialog = await RollDialog.create({
|
||||||
|
actor,
|
||||||
|
rolltype: 'ability',
|
||||||
|
label: 'health',
|
||||||
|
locks: { ability: 'health', difficulty: value }
|
||||||
|
})
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { default: NpcRollDialog } = await import('./dialogs/npcRollDialog.mjs')
|
||||||
|
const dialog = await NpcRollDialog.create({
|
||||||
|
actor,
|
||||||
|
rolltype: 'action',
|
||||||
|
locks: { difficulty: value }
|
||||||
|
})
|
||||||
|
if (dialog) dialog.render(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trait Zone (n) : applique la blessure résolue à toutes les cibles de
|
||||||
|
* l'attaque présentes dans la zone (confort pour le MJ).
|
||||||
|
*/
|
||||||
|
static async #onApplyZone(ev, attackMessage) {
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
const btn = ev.currentTarget
|
||||||
|
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||||
|
if (!exchange) return
|
||||||
|
if (!game.user.isGM && !attackMessage.isOwner) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!exchange.targets?.length) {
|
||||||
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_target'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let applied = 0
|
||||||
|
for (const target of exchange.targets) {
|
||||||
|
const r = await this.resolve(attackMessage, target?.id ?? null)
|
||||||
|
if (!r?.hit || !r.woundCategory) continue
|
||||||
|
const defender = await this.#resolveDefender({ id: r.defenderId, uuid: r.defenderUuid })
|
||||||
|
if (!defender || !this.canApplyWound(defender)) continue
|
||||||
|
const result = await this.applyWound(defender, r.woundCategory)
|
||||||
|
if (result && result !== false) applied += 1
|
||||||
|
}
|
||||||
|
btn.disabled = true
|
||||||
|
btn.innerHTML = `<i class="fas fa-check"></i> ${game.i18n.format('VERMINE.zone_applied', { count: applied })}`
|
||||||
|
if (game.user.isGM || attackMessage.isOwner) {
|
||||||
|
const msgEl = btn.closest('.vermine-roll-message')
|
||||||
|
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async #onApply(ev, attackMessage) {
|
static async #onApply(ev, attackMessage) {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
const btn = ev.currentTarget
|
const btn = ev.currentTarget
|
||||||
|
|||||||
+22
-4
@@ -16,6 +16,10 @@ export class VermineUtils {
|
|||||||
* @param {string} [options.keepTotem=null] - Totem to keep ('human' or 'adapted')
|
* @param {string} [options.keepTotem=null] - Totem to keep ('human' or 'adapted')
|
||||||
* @param {number} [options.skillLevel=null] - Skill level for auto-successes
|
* @param {number} [options.skillLevel=null] - Skill level for auto-successes
|
||||||
* @param {boolean} [options.hasSpecialty=false] - Whether a specialty is used
|
* @param {boolean} [options.hasSpecialty=false] - Whether a specialty is used
|
||||||
|
* @param {number} [options.effectiveRange=null] - Portée effective (Trait Portée)
|
||||||
|
* @param {number} [options.mobilityHandicap=0] - Handicap de Mobilité (note)
|
||||||
|
* @param {number} [options.ponctuelUsed=0] - Handicaps annulés (Trait Ponctuel)
|
||||||
|
* @param {Object|null} [options.intimidant=null] - Trait Intimidant utilisé
|
||||||
* @returns {Promise<Roll>} The roll result
|
* @returns {Promise<Roll>} The roll result
|
||||||
*/
|
*/
|
||||||
static async roll({
|
static async roll({
|
||||||
@@ -33,12 +37,17 @@ export class VermineUtils {
|
|||||||
hasSpecialty = false,
|
hasSpecialty = false,
|
||||||
handicap = 0,
|
handicap = 0,
|
||||||
bonusSuccesses = 0,
|
bonusSuccesses = 0,
|
||||||
|
bonusOnSuccess = false,
|
||||||
poolBreakdown = null,
|
poolBreakdown = null,
|
||||||
specialtyName = null,
|
specialtyName = null,
|
||||||
weapon = null,
|
weapon = null,
|
||||||
targets = [],
|
targets = [],
|
||||||
attack = null,
|
attack = null,
|
||||||
messageFlags = null
|
messageFlags = null,
|
||||||
|
effectiveRange = null,
|
||||||
|
mobilityHandicap = 0,
|
||||||
|
ponctuelUsed = 0,
|
||||||
|
intimidant = null
|
||||||
}) {
|
}) {
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
@@ -160,12 +169,17 @@ export class VermineUtils {
|
|||||||
hasSpecialty,
|
hasSpecialty,
|
||||||
handicap,
|
handicap,
|
||||||
bonusSuccesses,
|
bonusSuccesses,
|
||||||
|
bonusOnSuccess,
|
||||||
poolBreakdown,
|
poolBreakdown,
|
||||||
specialtyName,
|
specialtyName,
|
||||||
weapon,
|
weapon,
|
||||||
targets,
|
targets,
|
||||||
attack,
|
attack,
|
||||||
messageFlags
|
messageFlags,
|
||||||
|
effectiveRange,
|
||||||
|
mobilityHandicap,
|
||||||
|
ponctuelUsed,
|
||||||
|
intimidant
|
||||||
});
|
});
|
||||||
|
|
||||||
return roll;
|
return roll;
|
||||||
@@ -445,8 +459,12 @@ export class VermineUtils {
|
|||||||
}
|
}
|
||||||
// Verdict
|
// Verdict
|
||||||
const required = 1 + (param.handicap ?? 0);
|
const required = 1 + (param.handicap ?? 0);
|
||||||
const bonusSuccesses = param.bonusSuccesses ?? 0;
|
const rawTotal = roll._total ?? 0;
|
||||||
const total = (roll._total ?? 0) + bonusSuccesses;
|
// Réussites automatiques : appliquées inconditionnellement, ou uniquement
|
||||||
|
// si le jet est réussi (Trait Rafale — bonusOnSuccess).
|
||||||
|
const requested = param.bonusSuccesses ?? 0;
|
||||||
|
const bonusSuccesses = requested > 0 && (!param.bonusOnSuccess || rawTotal >= required) ? requested : 0;
|
||||||
|
const total = rawTotal + bonusSuccesses;
|
||||||
const verdict = {
|
const verdict = {
|
||||||
success: total >= required,
|
success: total >= required,
|
||||||
total,
|
total,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
|
{{> "systems/vermine2047/templates/actor/parts/actor-equipped-gear.hbs"}}
|
||||||
|
{{#if fetiche}}
|
||||||
|
<div class="fetiche-block flexrow align-center">
|
||||||
|
<div class="fetiche-info flexcol">
|
||||||
|
<h4 class="align-center"><i class="fas fa-heart"></i> {{ localize "VERMINE.fetiche" }} : {{fetiche.name}}</h4>
|
||||||
|
<p class="fetiche-bonus">{{ localize "VERMINE.fetiche_bonus" }}</p>
|
||||||
|
{{#if feticheMultiple}}
|
||||||
|
<p class="fetiche-warning"><i class="fas fa-triangle-exclamation"></i> {{ localize "VERMINE.fetiche_multiple" }}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if feticheLost}}
|
||||||
|
<p class="fetiche-warning"><i class="fas fa-triangle-exclamation"></i> {{ localize "VERMINE.fetiche_lost_penalty" }}</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<button type="button" data-action="loseFetiche" title="{{ localize 'VERMINE.lose_fetiche' }}"><i class="fas fa-unlink"></i> {{ localize "VERMINE.lose_fetiche" }}</button>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
<div class="flexrow">
|
<div class="flexrow">
|
||||||
<div class="self-control-container flexcol align-center">
|
<div class="self-control-container flexcol align-center">
|
||||||
<h4 class="align-center">
|
<h4 class="align-center">
|
||||||
|
|||||||
@@ -19,6 +19,12 @@
|
|||||||
<strong>{{localize "VERMINE.ranges"}}:</strong>
|
<strong>{{localize "VERMINE.ranges"}}:</strong>
|
||||||
{{ weapon.system.min_range }} / {{ weapon.system.max_range }}
|
{{ weapon.system.min_range }} / {{ weapon.system.max_range }}
|
||||||
</span>
|
</span>
|
||||||
|
{{#if effectiveRange}}
|
||||||
|
<span class="weapon-detail">
|
||||||
|
<strong>{{localize "VERMINE.effective_range"}}:</strong>
|
||||||
|
{{ effectiveRange }} m
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
<span class="weapon-detail">
|
<span class="weapon-detail">
|
||||||
<strong>{{localize "VERMINE.ammo"}}:</strong>
|
<strong>{{localize "VERMINE.ammo"}}:</strong>
|
||||||
{{ weapon.system.ammo }}
|
{{ weapon.system.ammo }}
|
||||||
@@ -29,6 +35,13 @@
|
|||||||
|
|
||||||
<h4 class="combat-mode-label">{{#ife mode "ranged"}}{{localize 'VERMINE.attack_ranged'}}{{else}}{{localize 'VERMINE.attack_contact'}}{{/ife}}</h4>
|
<h4 class="combat-mode-label">{{#ife mode "ranged"}}{{localize 'VERMINE.attack_ranged'}}{{else}}{{localize 'VERMINE.attack_contact'}}{{/ife}}</h4>
|
||||||
|
|
||||||
|
{{#if lourdMalus}}
|
||||||
|
<p class="traits-reminder"><i class="fas fa-weight-hanging"></i> {{ localize 'VERMINE.lourd_malus_note' malus=lourdMalus }}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if mobilityNote}}
|
||||||
|
<p class="traits-reminder"><i class="fas fa-shoe-prints"></i> {{ mobilityNote }}</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<div class="dice-pool">
|
<div class="dice-pool">
|
||||||
|
|
||||||
<!-- ATTACKER POOL -->
|
<!-- ATTACKER POOL -->
|
||||||
@@ -90,6 +103,12 @@
|
|||||||
<span class="label">{{localize 'VERMINE.consume_ammo'}}</span>
|
<span class="label">{{localize 'VERMINE.consume_ammo'}}</span>
|
||||||
</label>
|
</label>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if hasRafale}}
|
||||||
|
<label class="flexrow bonus-item">
|
||||||
|
<input type="checkbox" data-roll="true" name="rafale" id="rafale" value="1" />
|
||||||
|
<span class="label">{{ localize 'VERMINE.rafale_check' rafaleValue=rafaleValue }}</span>
|
||||||
|
</label>
|
||||||
|
{{/if}}
|
||||||
<div class="flexcol handicap-section">
|
<div class="flexcol handicap-section">
|
||||||
<label class="label" for="handicap">{{localize 'VERMINE.handicap'}}</label>
|
<label class="label" for="handicap">{{localize 'VERMINE.handicap'}}</label>
|
||||||
<select class="info-value" data-roll="true" data-dtype="String" type="number"
|
<select class="info-value" data-roll="true" data-dtype="String" type="number"
|
||||||
|
|||||||
@@ -236,7 +236,7 @@
|
|||||||
|
|
||||||
<!-- TOOLS -->
|
<!-- TOOLS -->
|
||||||
<div class="flexrow bonus-item full-width">
|
<div class="flexrow bonus-item full-width">
|
||||||
<label class="label">{{localize 'VERMINE.tooling'}} (+1D)</label>
|
<label class="label">{{localize 'VERMINE.tooling'}} (+1D / +2D)</label>
|
||||||
<div class="item-list grid grid-4col">
|
<div class="item-list grid grid-4col">
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
@@ -258,12 +258,37 @@
|
|||||||
id="usingTools"
|
id="usingTools"
|
||||||
value="{{item.name}}"
|
value="{{item.name}}"
|
||||||
/>
|
/>
|
||||||
<i>{{item.name}}</i>
|
<i>{{item.name}}{{#if item.system.traits.pratique}} <em class="tool-bonus">+2D</em>{{/if}}{{#if item.system.traits.ponctuel}} <em class="tool-ponctuel">P{{item.system.traits.ponctuel.value}}</em>{{/if}}</i>
|
||||||
</label>
|
</label>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- INTIMIDANT -->
|
||||||
|
{{#if hasIntimidant}}
|
||||||
|
<div class="flexrow bonus-item full-width">
|
||||||
|
<input type="checkbox" data-roll="true" name="intimidant" id="intimidant" value="1" />
|
||||||
|
<label class="label" for="intimidant">{{ localize 'VERMINE.intimidant_check' intimidantName=intimidantName }}</label>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<!-- TEMPORARY MALUS -->
|
||||||
|
<div class="flexrow bonus-item full-width">
|
||||||
|
<label class="label" for="temporary-malus">{{localize 'VERMINE.temporary_malus'}}</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
data-roll="true"
|
||||||
|
class="numeric-entry"
|
||||||
|
style="text-align: center;"
|
||||||
|
name="temporary-malus"
|
||||||
|
id="temporary-malus"
|
||||||
|
min="0"
|
||||||
|
max="5"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
<span>D</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- TOTEMS -->
|
<!-- TOTEMS -->
|
||||||
{{#ifgt @root.actor.system.adaptation.totems.human.value 0}}
|
{{#ifgt @root.actor.system.adaptation.totems.human.value 0}}
|
||||||
{{#ifgt @root.actor.system.adaptation.totems.adapted.value 0}}
|
{{#ifgt @root.actor.system.adaptation.totems.adapted.value 0}}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
<section class="sheet-body ">
|
<section class="sheet-body ">
|
||||||
|
|
||||||
{{> "systems/vermine2047/templates/item/partials/traits.hbs"}}
|
{{> "systems/vermine2047/templates/item/partials/traits.hbs"}}
|
||||||
|
<div class="resource align-center flexcol">
|
||||||
|
<label class="resource-label">{{ localize "VERMINE.equip" }}
|
||||||
|
<input type="checkbox" name="system.equipped" {{#if system.equipped}}checked{{/if}} data-dtype="Boolean" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="resource align-center flexcol">
|
<div class="resource align-center flexcol">
|
||||||
<label class="resource-label">compétence necessaire
|
<label class="resource-label">compétence necessaire
|
||||||
<input type="checkbox" data-tooltip="ajouter la vigueur" name="system.needSkill.value" {{#if system.needSkill.value}} checked {{/if}}>
|
<input type="checkbox" data-tooltip="ajouter la vigueur" name="system.needSkill.value" {{#if system.needSkill.value}} checked {{/if}}>
|
||||||
|
|||||||
@@ -31,6 +31,12 @@
|
|||||||
<strong>{{localize "VERMINE.ranges"}}:</strong>
|
<strong>{{localize "VERMINE.ranges"}}:</strong>
|
||||||
{{param.weapon.system.min_range}} / {{param.weapon.system.max_range}}
|
{{param.weapon.system.min_range}} / {{param.weapon.system.max_range}}
|
||||||
</span>
|
</span>
|
||||||
|
{{#if param.effectiveRange}}
|
||||||
|
<span class="weapon-detail">
|
||||||
|
<strong>{{localize "VERMINE.effective_range"}}:</strong>
|
||||||
|
{{param.effectiveRange}} m
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
<span class="weapon-detail">
|
<span class="weapon-detail">
|
||||||
<strong>{{localize "VERMINE.ammo"}}:</strong>
|
<strong>{{localize "VERMINE.ammo"}}:</strong>
|
||||||
{{param.weapon.system.ammo}}
|
{{param.weapon.system.ammo}}
|
||||||
@@ -48,12 +54,21 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if param.weapon.system.traits.duree}}
|
||||||
|
<div class="weapon-detail duree-note"><i class="fas fa-hourglass-half"></i> {{ localize 'VERMINE.duree_note' value=param.weapon.system.traits.duree.value }}</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if param.attack}}
|
{{#if param.attack}}
|
||||||
<div class="exchange-actions" data-exchange-id="{{param.attack.id}}">
|
<div class="exchange-actions" data-exchange-id="{{param.attack.id}}">
|
||||||
<div class="exchange-header"><i class="fas fa-exchange-alt"></i> {{localize "VERMINE.exchange"}}</div>
|
<div class="exchange-header"><i class="fas fa-exchange-alt"></i> {{localize "VERMINE.exchange"}}</div>
|
||||||
|
{{#if param.attack.rapideHandicap}}
|
||||||
|
<div class="exchange-reminder"><i class="fas fa-bolt"></i> {{ localize 'VERMINE.rapide_note' handicap=param.attack.rapideHandicap }}</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if param.attack.zoneValue}}
|
||||||
|
<div class="exchange-reminder"><i class="fas fa-burst"></i> {{ localize 'VERMINE.zone_note' value=param.attack.zoneValue }}</div>
|
||||||
|
{{/if}}
|
||||||
{{#each param.attack.targets}}
|
{{#each param.attack.targets}}
|
||||||
<div class="exchange-target">
|
<div class="exchange-target">
|
||||||
<span class="exchange-target-name"><i class="fas fa-bullseye"></i> {{this.name}}</span>
|
<span class="exchange-target-name"><i class="fas fa-bullseye"></i> {{this.name}}</span>
|
||||||
@@ -65,9 +80,15 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<span class="exchange-passive"><i class="fas fa-shield-virus"></i> {{localize "VERMINE.passive_defense"}}</span>
|
<span class="exchange-passive"><i class="fas fa-shield-virus"></i> {{localize "VERMINE.passive_defense"}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if @root.param.weapon.system.traits.incapacitant}}
|
||||||
|
<button type="button" class="exchange-incapacitant" data-target-id="{{this.id}}" data-target-uuid="{{this.uuid}}" data-value="{{@root.param.weapon.system.traits.incapacitant.value}}" title="{{localize 'VERMINE.incapacitant_roll'}}"><i class="fas fa-bolt"></i> {{localize "VERMINE.incapacitant_roll"}} ({{@root.param.weapon.system.traits.incapacitant.value}})</button>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<button type="button" class="exchange-resolve" data-exchange-id="{{param.attack.id}}"><i class="fas fa-balance-scale"></i> {{localize "VERMINE.resolve"}}</button>
|
<button type="button" class="exchange-resolve" data-exchange-id="{{param.attack.id}}"><i class="fas fa-balance-scale"></i> {{localize "VERMINE.resolve"}}</button>
|
||||||
|
{{#if param.attack.zoneValue}}
|
||||||
|
<button type="button" class="exchange-zone" data-exchange-id="{{param.attack.id}}"><i class="fas fa-burst"></i> {{ localize "VERMINE.zone_apply" }}</button>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@@ -87,12 +108,30 @@
|
|||||||
<span class="context-value">+{{param.handicap}}</span>
|
<span class="context-value">+{{param.handicap}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if param.mobilityHandicap}}
|
||||||
|
<div class="context-item">
|
||||||
|
<span class="context-label">{{localize "VERMINE.mobility"}}</span>
|
||||||
|
<span class="context-value">{{ localize 'VERMINE.traits_mobility_note' handicap=param.mobilityHandicap }}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
{{#if verdict.bonusSuccesses}}
|
{{#if verdict.bonusSuccesses}}
|
||||||
<div class="context-item">
|
<div class="context-item">
|
||||||
<span class="context-label">{{localize "VERMINE.auto_successes"}}</span>
|
<span class="context-label">{{localize "VERMINE.auto_successes"}}</span>
|
||||||
<span class="context-value">+{{verdict.bonusSuccesses}}</span>
|
<span class="context-value">+{{verdict.bonusSuccesses}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if param.intimidant}}
|
||||||
|
<div class="context-item">
|
||||||
|
<span class="context-label">{{localize "VERMINE.intimidant"}}</span>
|
||||||
|
<span class="context-value">{{#if param.intimidant.psychology}}{{localize "VERMINE.intimidant_psychology"}}{{else}}{{localize "VERMINE.intimidant_reroll"}}{{/if}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if param.ponctuelUsed}}
|
||||||
|
<div class="context-item">
|
||||||
|
<span class="context-label">{{localize "VERMINE.ponctuel"}}</span>
|
||||||
|
<span class="context-value">{{ localize 'VERMINE.ponctuel_used' value=param.ponctuelUsed }}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
{{#if param.specialtyName}}
|
{{#if param.specialtyName}}
|
||||||
<div class="context-item">
|
<div class="context-item">
|
||||||
<span class="context-label">{{localize "VERMINE.specialty"}}</span>
|
<span class="context-label">{{localize "VERMINE.specialty"}}</span>
|
||||||
@@ -144,6 +183,12 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if param.poolBreakdown.tooling}}
|
{{#if param.poolBreakdown.tooling}}
|
||||||
<span class="pool-item">{{localize "VERMINE.pool_tooling"}} +{{param.poolBreakdown.tooling}}D</span>
|
<span class="pool-item">{{localize "VERMINE.pool_tooling"}} +{{param.poolBreakdown.tooling}}D</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if param.poolBreakdown.tempMalus}}
|
||||||
|
<span class="pool-item pool-malus">{{localize "VERMINE.pool_temp_malus"}} -{{param.poolBreakdown.tempMalus}}D</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if param.poolBreakdown.psychology}}
|
||||||
|
<span class="pool-item pool-malus">{{localize "VERMINE.pool_psychology"}} -{{param.poolBreakdown.psychology}}D</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if param.poolBreakdown.group}}
|
{{#if param.poolBreakdown.group}}
|
||||||
<span class="pool-item">{{localize "VERMINE.pool_group"}} +{{param.poolBreakdown.group}}D</span>
|
<span class="pool-item">{{localize "VERMINE.pool_group"}} +{{param.poolBreakdown.group}}D</span>
|
||||||
@@ -160,8 +205,18 @@
|
|||||||
{{#if param.poolBreakdown.domainBonus}}
|
{{#if param.poolBreakdown.domainBonus}}
|
||||||
<span class="pool-item">{{#ifgt param.poolBreakdown.domainBonus 0}}{{localize "VERMINE.domain_bonus"}}{{else}}{{localize "VERMINE.domain_penalty"}}{{/ifgt}} {{param.poolBreakdown.domainBonus}}D</span>
|
<span class="pool-item">{{#ifgt param.poolBreakdown.domainBonus 0}}{{localize "VERMINE.domain_bonus"}}{{else}}{{localize "VERMINE.domain_penalty"}}{{/ifgt}} {{param.poolBreakdown.domainBonus}}D</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#unless param.poolBreakdown.wound}}
|
||||||
|
{{#unless param.poolBreakdown.lourd}}
|
||||||
{{#if param.poolBreakdown.malus}}
|
{{#if param.poolBreakdown.malus}}
|
||||||
<span class="pool-item pool-malus">{{localize "VERMINE.pool_malus"}} -{{param.poolBreakdown.malus}}D</span>
|
<span class="pool-item pool-malus">{{localize "VERMINE.pool_malus"}} -{{param.poolBreakdown.malus}}D</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
{{/unless}}
|
||||||
|
{{#if param.poolBreakdown.wound}}
|
||||||
|
<span class="pool-item pool-malus">{{localize "VERMINE.pool_wound"}} -{{param.poolBreakdown.wound}}D</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if param.poolBreakdown.lourd}}
|
||||||
|
<span class="pool-item pool-malus">{{localize "VERMINE.pool_lourd"}} -{{param.poolBreakdown.lourd}}D</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<span class="pool-item pool-total">{{localize "VERMINE.pool_total"}} {{param.poolBreakdown.total}}D</span>
|
<span class="pool-item pool-total">{{localize "VERMINE.pool_total"}} {{param.poolBreakdown.total}}D</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user