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:
2026-08-07 15:30:41 +02:00
parent 8dd5d02994
commit ef01651b1d
21 changed files with 770 additions and 63 deletions
+34 -5
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;
@@ -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.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 +153,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 +178,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());
@@ -210,6 +222,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
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 +238,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 +276,11 @@ 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 malus = wound + lourd;
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() {
@@ -365,9 +383,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 +411,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.