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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user