feat(traits): phase 1 — réparations (Bien conçu, Pratique, Coque réparable)

- Coque : épuisée stockée {value:0, max:n} au lieu de null → réparable ; nouveau mode « Réparer la coque » (difficulté = coque.max) dans le dialogue ; affichage (0/n) sur la fiche
- Bien conçu : Réussite automatique en réparation classique (restauration complète) ; +1 Fiabilité à la 1ère réparation de fortune réussie (compteur traits.bien_conçu.value)
- Pratique : Handicap +I sur toutes les réparations (required +1, note sur la carte)
- i18n fr/en (9 clés)
- TRAITS_PLAN.md : suivi du plan d'automatisation des Traits
This commit is contained in:
2026-08-07 14:40:42 +02:00
parent ef86b2637c
commit 8dd5d02994
8 changed files with 169 additions and 31 deletions
+43
View File
@@ -0,0 +1,43 @@
# Plan d'automatisation des Traits — Suivi
Base : `regles/regles_traits.txt` (et KitDeSurvie p.70-71, 39).
Implémenté déjà : `cassant`, `coque` (absorption), `etanche` (immersion) dans `module/system/reliability.mjs`.
Décisions utilisateur : Fétiche +1D sur les **deux** Réserves · Malus/Durée = **saisie manuelle** · Handicap de Mobilité = **note de rappel** · Items génériques = **flag équipé OK**.
## Légende
- [x] fait
- [ ] à faire
- [~] en cours
## Tâche 0 — Prérequis communs
- [ ] `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`…)
## Phase 1 — Réparations (reliability.mjs + dialogue)
- [x] **Bien conçu** : succès auto en réparation classique (restauration complète) + 1ʳᵉ réparation de fortune rend +1 Fiabilité (compteur `traits.bien_conçu.value`)
- [x] **Pratique** : Handicap +I aux réparations (required +1, note sur la carte)
- [x] **Coque réparable** : épuisement → `{ value: 0, max: n }` ; mode « Réparer la coque » dans le dialogue (jet vs `coque.max`) ; affichage `(0/n)` sur la fiche
- [x] **Étanche** : rien de plus (immersion gérée ; gaz/maladie hors scope)
## Phase 2 — Combat (combatDialog.mjs, exchange.mjs)
- [ ] **Lourd** : malus auto (`lourdMalus()`) dans dialogue combat
- [ ] **Rapide (n)** : handicap (n) défenseur pré-rempli + note carte d'échange
- [ ] **Rafale (n)** : case « Tir en rafale » → difficulté +2, +n réussites si réussi
- [ ] **Portée** : portée effective × Vigueur (archery/throwing) affichée
- [ ] **Maniable / Mobilité** : note de rappel dialogue combat + jets athlétiques
- [ ] **Incapacitant (n)** : bouton « Jet de Santé vs (n) » sur carte d'échange
## Phase 3 — Jets d'action (rollDialog.mjs + roll-dialog.hbs)
- [ ] **Ponctuel (n)** : select outil → annule n Handicaps, décrémente, supprime à 0
- [ ] **Pratique (jets)** : select d'outil → +1D / +2D si Pratique (remplace checkbox `usingTools`)
- [ ] **Intimidant** : case « Brandit » → malus 1D Psychologie ou note relance
- [ ] **Malus (n)** : champ « Malus temporaires » (saisie manuelle) déduit du pool
- [ ] **Durée (n)** : note « effet pendant n tours » sur carte de jet d'arme
## 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)
- [ ] **Zone (n)** : note carte d'échange + bouton « Appliquer aux cibles de la zone »
## Transverse
- [x] i18n fr/en pour la Phase 1
- [x] ESLint (0 erreur sur les fichiers modifiés) + tests navigateur Phase 1 (coque, réparation coque, Bien conçu classique/fortune, Pratique, dialogue, affichage fiche)
+8 -1
View File
@@ -234,7 +234,14 @@
"reliability_need_actor": "Repair impossible: the object must belong to an actor",
"reliability_already_destroyed": "Object already destroyed or unusable",
"reliability_coque_absorbed": "Hull absorbs {absorbed} damage(s), {remaining} left",
"reliability_coque_lost": "Hull: trait exhausted and lost",
"reliability_coque_lost": "Hull: trait exhausted — repairable (Difficulty {max})",
"reliability_coque_restored": "Hull repaired: {max} points",
"reliability_repair_coque": "Repair the hull (Difficulty {max})",
"reliability_repair_coque_label": "Hull",
"reliability_bien_concu": "Well-made: automatic success — reliability fully restored",
"reliability_bien_concu_fortune": "Well-made: +1 reliability point (first field repair)",
"reliability_auto_success": "Automatic success (Well-made)",
"reliability_pratique": "Pratique: +I Handicap",
"reliability_cassant": "Fragile: reliability losses increased by 1 point",
"reliability_etanche": "Watertight: immersion damage ignored",
"reliability_fortune_skill_bonus": "+1D — the character has the required skill ({skill})",
+8 -1
View File
@@ -315,7 +315,14 @@
"reliability_need_actor": "Réparation impossible : l'objet doit appartenir à un acteur",
"reliability_already_destroyed": "Objet déjà détruit ou inutilisable",
"reliability_coque_absorbed": "Coque absorbe {absorbed} Dommage(s), {remaining} restant(s)",
"reliability_coque_lost": "Coque : Trait épuisé et perdu",
"reliability_coque_lost": "Coque : Trait épuisé — réparable (Difficulté {max})",
"reliability_coque_restored": "Coque restaurée : {max} points",
"reliability_repair_coque": "Réparer la coque (Difficulté {max})",
"reliability_repair_coque_label": "Coque",
"reliability_bien_concu": "Bien conçu : Réussite automatique — Fiabilité entièrement restaurée",
"reliability_bien_concu_fortune": "Bien conçu : +1 point de Fiabilité (1ʳᵉ réparation de fortune)",
"reliability_auto_success": "Réussite automatique (Bien conçu)",
"reliability_pratique": "Pratique : Handicap +I",
"reliability_cassant": "Cassant : pertes de Fiabilité majorées d'1 point",
"reliability_etanche": "Étanche : Dommages d'immersion ignorés",
"reliability_fortune_skill_bonus": "+1D — le personnage possède la Compétence requise ({skill})",
@@ -50,6 +50,8 @@ export default class ReliabilityDialog extends HandlebarsApplicationMixin(foundr
async _prepareContext() {
const hasEtanche = !!this.#item.system?.traits?.etanche;
const hasActor = !!this.#item.actor;
const coque = this.#item.system?.traits?.coque ?? {};
const hasBrokenCoque = this.#mode === 'repair' && (parseInt(coque?.value ?? 0, 10) || 0) === 0 && (parseInt(coque?.max ?? 0, 10) || 0) > 0;
const storedSkill = (this.#item.system?.needSkill?.value && this.#item.system?.needSkill?.skill)
|| this.#item.system?.skill || '';
const severities = ['mineurs', 'graves', 'critiques'].map((key) => ({
@@ -68,6 +70,8 @@ export default class ReliabilityDialog extends HandlebarsApplicationMixin(foundr
isRepair: this.#mode === 'repair',
hasEtanche,
hasActor,
hasBrokenCoque,
coqueMax: parseInt(coque?.max ?? 0, 10) || 0,
severities,
skills,
};
+76 -19
View File
@@ -81,8 +81,10 @@ export class VermineReliability {
traitsNotes.push(game.i18n.localize('VERMINE.reliability_etanche'));
}
// Trait Coque (n) : ignore les (n) premiers Dommages puis se perd.
// Trait Coque (n) : ignore les (n) premiers Dommages puis s'épuise.
// La coque épuisée est stockée { value: 0, max: n } : réparable (Phase 1).
let coqueValue = parseInt(traits.coque?.value ?? 0, 10) || 0;
const coqueInit = parseInt(traits.coque?.max ?? traits.coque?.value ?? 0, 10) || 0;
const updates = {};
if (coqueValue > 0 && dommages > 0) {
const absorbed = Math.min(coqueValue, dommages);
@@ -92,8 +94,8 @@ export class VermineReliability {
updates['system.traits.coque.value'] = coqueValue;
traitsNotes.push(game.i18n.format('VERMINE.reliability_coque_absorbed', { absorbed, remaining: coqueValue }));
} else {
updates['system.traits.coque'] = null;
traitsNotes.push(game.i18n.localize('VERMINE.reliability_coque_lost'));
updates['system.traits.coque'] = { value: 0, max: coqueInit };
traitsNotes.push(game.i18n.format('VERMINE.reliability_coque_lost', { max: coqueInit }));
}
}
@@ -154,10 +156,10 @@ export class VermineReliability {
}
/**
* Réparation classique ou de fortune.
* Réparation classique, de fortune ou de coque.
* @param {Item} item
* @param {Object} opts
* @param {string} opts.mode "classique" | "fortune"
* @param {string} opts.mode "classique" | "fortune" | "coque"
* @param {string} opts.skill Compétence de réparation classique (clé de skills)
* @returns {Promise<Object|null>}
*/
@@ -168,13 +170,25 @@ export class VermineReliability {
return null;
}
const traits = item.system?.traits ?? {};
const current = parseInt(item.system?.reliability ?? 0, 10) || 0;
const damagesValue = parseInt(item.system?.damages?.value ?? 0, 10) || 0;
const handicap = this.panneHandicap(item);
// Trait Pratique : +1 Handicap sur toutes les réparations.
const pratique = !!traits.pratique;
const pratiqueHandicap = pratique ? 1 : 0;
const pratiqueNote = pratique ? game.i18n.localize('VERMINE.reliability_pratique') : '';
const isCoque = mode === 'coque';
const coqueMax = isCoque ? parseInt(traits.coque?.max ?? 0, 10) || 0 : 0;
// Difficulté : Fiabilité d'origine (≈ actuelle + Dommages subis) pour la
// réparation classique ; Fiabilité actuelle pour la réparation de fortune.
const difficulty = mode === 'classique' ? Math.min(10, current + damagesValue) : current;
// réparation classique ; Fiabilité actuelle pour la réparation de fortune ;
// valeur maximale du Trait pour la réparation de coque.
let difficulty = current;
if (isCoque) difficulty = coqueMax;
else if (mode === 'classique') difficulty = Math.min(10, current + damagesValue);
const precision = parseInt(actor.system?.abilities?.precision?.value ?? 0, 10) || 0;
const skillValue = mode === 'fortune'
@@ -198,27 +212,57 @@ export class VermineReliability {
}
}
const roll = new Roll(`${NoD + fortuneBonus}d10cs>=${difficulty}`);
// Trait Bien conçu : Réussite automatique sur les Réparations classiques.
const bienConcu = !!traits.bien_conçu;
const autoSuccess = bienConcu && mode === 'classique';
let bienConcuNote = '';
// Trait Bien conçu (fortune) : la 1ʳᵉ réparation de fortune réussie rend
// 1 point de Fiabilité (compteur traits.bien_conçu.value).
const bienConcuUsed = bienConcu && mode === 'fortune' && (parseInt(traits.bien_conçu?.value ?? 0, 10) || 0) > 0;
const bienConcuBonus = bienConcu && mode === 'fortune' && !bienConcuUsed;
// Réparation de coque : pas de panne à réparer, Difficulté = max du Trait.
const required = 1 + (isCoque ? 0 : handicap) + (isCoque ? 0 : fortuneHandicap) + pratiqueHandicap;
const roll = autoSuccess ? null : new Roll(`${NoD + fortuneBonus}d10cs>=${difficulty}`);
if (roll) {
await roll.evaluate();
await VermineUtils.showDiceSoNice(roll);
}
const results = (roll.dice ?? []).flatMap((d) => (d.results ?? []).map((r) => ({
const results = roll
? (roll.dice ?? []).flatMap((d) => (d.results ?? []).map((r) => ({
value: r.result ?? 0,
isSuccess: r.success,
})));
const successes = roll._total ?? 0;
const required = 1 + handicap + fortuneHandicap;
const success = successes >= required;
})))
: [];
const successes = roll?._total ?? required;
const success = autoSuccess || successes >= required;
let gained = 0;
let fortuneFixed = false;
let coqueRestored = false;
let newReliability = null;
let resultLabel = '';
if (success) {
if (mode === 'classique') {
gained = successes;
if (isCoque) {
coqueRestored = true;
resultLabel = game.i18n.format('VERMINE.reliability_coque_restored', { max: coqueMax });
} else if (mode === 'classique') {
gained = autoSuccess ? damagesValue : successes;
newReliability = Math.min(10, current + gained);
resultLabel = game.i18n.format('VERMINE.reliability_gained', { gained });
if (autoSuccess) {
bienConcuNote = game.i18n.localize('VERMINE.reliability_bien_concu');
}
} else {
fortuneFixed = true;
if (bienConcuBonus) {
gained = 1;
newReliability = Math.min(10, current + 1);
bienConcuNote = game.i18n.localize('VERMINE.reliability_bien_concu_fortune');
}
resultLabel = game.i18n.localize('VERMINE.reliability_fixed');
}
} else {
@@ -226,31 +270,44 @@ export class VermineReliability {
}
const updates = {};
if (mode === 'classique' && gained > 0) {
updates['system.reliability'] = Math.min(10, current + gained);
if (isCoque && coqueRestored) {
updates['system.traits.coque'] = { value: coqueMax, max: coqueMax };
} else if (mode === 'classique' && gained > 0) {
updates['system.reliability'] = newReliability;
updates['system.damages.value'] = Math.max(0, damagesValue - gained);
} else if (mode === 'fortune' && fortuneFixed) {
updates['system.damages.value'] = 0;
if (bienConcuBonus) {
updates['system.reliability'] = newReliability;
updates['system.traits.bien_conçu'] = { value: 1 };
}
}
if (Object.keys(updates).length) await item.update(updates);
const skillLabel = mode === 'fortune'
? game.i18n.localize('SKILLS.diy.name')
: game.i18n.localize(`SKILLS.${skill}.name`);
const modeLabel = isCoque
? game.i18n.localize('VERMINE.reliability_repair_coque_label')
: game.i18n.localize(mode === 'classique' ? 'VERMINE.reliability_repair_classic' : 'VERMINE.reliability_repair_fortune');
const content = await foundry.applications.handlebars.renderTemplate(
'systems/vermine2047/templates/reliability-repair.hbs',
{
item: { name: item.name, img: item.img },
modeLabel: game.i18n.localize(mode === 'classique' ? 'VERMINE.reliability_repair_classic' : 'VERMINE.reliability_repair_fortune'),
modeLabel,
skillLabel,
difficulty,
handicap,
handicap: isCoque ? 0 : handicap,
pratiqueHandicap,
results,
successes,
required,
resultLabel,
autoSuccess,
fortuneNote,
bienConcuNote,
pratiqueNote,
},
);
+6
View File
@@ -31,6 +31,12 @@
<input type="radio" name="repairMode" value="fortune">
<span>{{ localize "VERMINE.reliability_repair_fortune" }}</span>
</label>
{{#if hasBrokenCoque}}
<label class="reliability-option">
<input type="radio" name="repairMode" value="coque">
<span>{{ localize "VERMINE.reliability_repair_coque" max=coqueMax }}</span>
</label>
{{/if}}
</div>
</div>
<div class="form-group">
+10 -3
View File
@@ -6,12 +6,19 @@
</span>
</h3>
<h4>
{{#each item.system.traits as | trait index|}}
<span data-tooltip="{{trait.description}}">{{trait.name}}
{{#each item.system.traits as |trait key|}}
{{#if trait}}
{{#with (lookup ../config.traits key)}}
<span data-tooltip="{{this.description}}">{{this.name}}
{{#if trait.max}}
<em class="trait-count">({{trait.value}}/{{trait.max}})</em>
{{/if}}
{{#if trait.value}}
<input type="number" class="hexa" value="{{trait.value}}" name="system.traits.{{index}}.value">
<input type="number" class="hexa" value="{{trait.value}}" name="system.traits.{{key}}.value">
{{/if}}
</span>
{{/with}}
{{/if}}
{{/each}}
</h4>
</div>
+7
View File
@@ -8,15 +8,22 @@
<span class="reliability-meta-item"><strong>{{ localize "VERMINE.reliability_skill" }}:</strong> {{skillLabel}}</span>
<span class="reliability-meta-item"><strong>{{ localize "VERMINE.difficulty" }}:</strong> {{difficulty}}</span>
{{#if handicap}}<span class="reliability-meta-item"><strong>{{ localize "VERMINE.handicap" }}:</strong> +{{handicap}}</span>{{/if}}
{{#if pratiqueHandicap}}<span class="reliability-meta-item"><strong>{{ localize "VERMINE.handicap" }}:</strong> +{{pratiqueHandicap}}</span>{{/if}}
</div>
{{#if autoSuccess}}
<p class="reliability-note">{{ localize "VERMINE.reliability_auto_success" }}</p>
{{else}}
<ul class="flexrow reliability-results">
{{#each results}}
<li class="reliability-die {{#if this.isSuccess}}success{{/if}}">{{this.value}}</li>
{{/each}}
</ul>
{{/if}}
<div class="reliability-summary flexrow flex-wrap">
<span><strong>{{ localize "VERMINE.reliability_successes" }}:</strong> {{successes}} / {{required}}</span>
<span><strong>{{ localize "VERMINE.reliability_result" }}:</strong> {{resultLabel}}</span>
</div>
{{#if fortuneNote}}<p class="reliability-note">{{fortuneNote}}</p>{{/if}}
{{#if bienConcuNote}}<p class="reliability-note">{{bienConcuNote}}</p>{{/if}}
{{#if pratiqueNote}}<p class="reliability-note">{{pratiqueNote}}</p>{{/if}}
</div>