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
+80 -23
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}`);
await roll.evaluate();
await VermineUtils.showDiceSoNice(roll);
// 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 = '';
const results = (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;
// Trait Bien conçu (fortune) : la 1ʳᵉ réparation de fortune réussie rend
// 1 point de Fiabilité (compteur traits.bien_conçu.value).
const bienConcuUsed = bienConcu && mode === 'fortune' && (parseInt(traits.bien_conçu?.value ?? 0, 10) || 0) > 0;
const bienConcuBonus = bienConcu && mode === 'fortune' && !bienConcuUsed;
// Réparation de coque : pas de panne à réparer, Difficulté = max du Trait.
const required = 1 + (isCoque ? 0 : handicap) + (isCoque ? 0 : fortuneHandicap) + pratiqueHandicap;
const roll = autoSuccess ? null : new Roll(`${NoD + fortuneBonus}d10cs>=${difficulty}`);
if (roll) {
await roll.evaluate();
await VermineUtils.showDiceSoNice(roll);
}
const results = roll
? (roll.dice ?? []).flatMap((d) => (d.results ?? []).map((r) => ({
value: r.result ?? 0,
isSuccess: r.success,
})))
: [];
const successes = roll?._total ?? required;
const success = autoSuccess || successes >= required;
let gained = 0;
let fortuneFixed = false;
let coqueRestored = false;
let newReliability = null;
let resultLabel = '';
if (success) {
if (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,
},
);