- module/system/reliability.mjs : VermineReliability
- Test de Fiabilité : jet Xd10 (Mineurs 1D / Graves 3D / Critiques 5D),
chaque dé >= Fiabilité inflige 1 Dommage ; réduit Fiabilité et dommages
cumulés (panne/état/effet) ; destruction à 0
- Traits appliqués : Coque (absorbe n puis se perd), Cassant (+1 perte),
Étanche (Dommages d'immersion ignorés si Fiabilité au max)
- Réparations : classique (Précision + Artisanat/Mécanique/Technologie/
Armurerie vs Fiabilité d'origine + Handicap de panne, +1 Fiabilité/Réussite)
et de fortune (Précision + Bricolage, panne réparée sans gain de Fiabilité)
- ReliabilityDialog (test/réparation) + cartes de chat dédiées
- Boutons Test de Fiabilité / Réparer sur les fiches d'items (weapon/defense/item)
- i18n fr/en + styles des cartes et boutons
247 lines
8.8 KiB
JavaScript
247 lines
8.8 KiB
JavaScript
import { VermineUtils } from './roll.mjs';
|
|
|
|
/**
|
|
* Gestion de la Fiabilité du matériel (regles/regles_materiel.txt).
|
|
*
|
|
* Test de Fiabilité (Dommages matériels) : selon la gravité (Mineurs 1D,
|
|
* Graves 3D, Critiques 5D), on lance les dés sans Relance ni Réserve ;
|
|
* chaque dé >= Fiabilité inflige 1 Dommage (réduit la Fiabilité de 1).
|
|
* À 0, l'équipement est détruit ou inutilisable.
|
|
*
|
|
* Réparations : jet de Précision + Compétence contre la Fiabilité d'origine
|
|
* (classique) ou de Précision + Bricolage contre la Fiabilité actuelle
|
|
* (de fortune). Chaque Réussite rend 1 point de Fiabilité (classique) ;
|
|
* la réparation de fortune rend l'objet fonctionnel sans rendre de point.
|
|
*/
|
|
export class VermineReliability {
|
|
/** Gravités et nombre de dés de Dommages (p. 37). */
|
|
static SEVERITIES = { mineurs: 1, graves: 3, critiques: 5 };
|
|
|
|
/** Handicap de réparation selon les Dommages subis (Mineure I, Grave II, Critique III). */
|
|
static #PANNE_HANDICAP = { 1: 1, 2: 1, 3: 2, 4: 2, 5: 3 };
|
|
|
|
/** Compétences de réparation classique (regles_materiel p. 39). */
|
|
static REPAIR_SKILLS = ['crafting', 'mecanical', 'technology', 'armory'];
|
|
|
|
/**
|
|
* Handicap de la panne courante (0 si intact).
|
|
* @param {Item} item
|
|
* @returns {number}
|
|
*/
|
|
static panneHandicap(item) {
|
|
const value = parseInt(item.system?.damages?.value ?? 0, 10) || 0;
|
|
return this.#PANNE_HANDICAP[value] || 0;
|
|
}
|
|
|
|
/**
|
|
* État courant dérivé des Dommages subis (tableau `damages.state`).
|
|
* @param {Item} item
|
|
* @returns {string}
|
|
*/
|
|
static panneState(item) {
|
|
const damages = item.system?.damages;
|
|
const idx = (parseInt(damages?.value ?? 0, 10) || 0) - 1;
|
|
return idx >= 0 ? damages.state?.[idx] ?? '' : '';
|
|
}
|
|
|
|
/**
|
|
* Test de Fiabilité : jet de Dommages matériels.
|
|
* @param {Item} item
|
|
* @param {Object} opts
|
|
* @param {string} opts.severity "mineurs" | "graves" | "critiques"
|
|
* @param {boolean} opts.immersion si vrai, le Trait Étanche peut annuler les Dommages
|
|
* @returns {Promise<Object|null>} Résultat (null si impossible)
|
|
*/
|
|
static async test(item, { severity = 'graves', immersion = false } = {}) {
|
|
if (!item) return null;
|
|
const diceCount = this.SEVERITIES[severity] ?? 3;
|
|
const current = parseInt(item.system?.reliability ?? 0, 10) || 0;
|
|
const traits = item.system?.traits ?? {};
|
|
const damagesValue = parseInt(item.system?.damages?.value ?? 0, 10) || 0;
|
|
|
|
if (current <= 0) {
|
|
ui.notifications.warn(game.i18n.localize('VERMINE.reliability_already_destroyed'));
|
|
return { destroyed: true, dommages: 0 };
|
|
}
|
|
|
|
// Jet des dés de Dommages (sans Relance ni Réserve)
|
|
const roll = new Roll(`${diceCount}d10`);
|
|
await roll.evaluate();
|
|
await VermineUtils.showDiceSoNice(roll);
|
|
|
|
const results = (roll.dice ?? []).flatMap((d) => (d.results ?? []).map((r) => r.result ?? 0));
|
|
const traitsNotes = [];
|
|
|
|
// Trait Étanche : ignore les Dommages d'immersion tant que la Fiabilité
|
|
// est à son maximum (objet sans Dommage cumulé).
|
|
let dommages = results.filter((r) => r >= current).length;
|
|
if (immersion && traits.etanche && damagesValue === 0) {
|
|
dommages = 0;
|
|
traitsNotes.push(game.i18n.localize('VERMINE.reliability_etanche'));
|
|
}
|
|
|
|
// Trait Coque (n) : ignore les (n) premiers Dommages puis se perd.
|
|
let coqueValue = parseInt(traits.coque?.value ?? 0, 10) || 0;
|
|
const updates = {};
|
|
if (coqueValue > 0 && dommages > 0) {
|
|
const absorbed = Math.min(coqueValue, dommages);
|
|
dommages -= absorbed;
|
|
coqueValue -= absorbed;
|
|
if (coqueValue > 0) {
|
|
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'));
|
|
}
|
|
}
|
|
|
|
// Trait Cassant : toutes les pertes de Fiabilité sont majorées d'1 point.
|
|
const cassant = !!traits.cassant;
|
|
if (cassant && dommages > 0) {
|
|
dommages += 1;
|
|
traitsNotes.push(game.i18n.localize('VERMINE.reliability_cassant'));
|
|
}
|
|
|
|
const newReliability = Math.max(0, current - dommages);
|
|
const newDamagesValue = Math.min(5, damagesValue + dommages);
|
|
const destroyed = newReliability === 0;
|
|
|
|
updates['system.reliability'] = newReliability;
|
|
updates['system.damages.value'] = destroyed ? 5 : newDamagesValue;
|
|
await item.update(updates);
|
|
|
|
// État final affiché sur la carte.
|
|
let state = '';
|
|
if (destroyed) {
|
|
state = game.i18n.localize('VERMINE.reliability_destroyed');
|
|
} else if (newDamagesValue > 0) {
|
|
const idx = newDamagesValue - 1;
|
|
state = item.system.damages.state?.[idx] ?? '';
|
|
} else {
|
|
state = '';
|
|
}
|
|
|
|
const content = await foundry.applications.handlebars.renderTemplate(
|
|
'systems/vermine2047/templates/reliability-test.hbs',
|
|
{
|
|
item: { name: item.name, img: item.img },
|
|
severityLabel: game.i18n.localize(`VERMINE.reliability_severity_${severity}`),
|
|
diceCount,
|
|
oldReliability: current,
|
|
newReliability,
|
|
dommages,
|
|
results: results.map((v) => ({ value: v, isDamage: v >= current })),
|
|
state,
|
|
destroyed,
|
|
traitsNotes,
|
|
},
|
|
);
|
|
|
|
await ChatMessage.create({
|
|
user: game.user?._id,
|
|
speaker: this.#getSpeaker(item),
|
|
content,
|
|
});
|
|
|
|
return { dommages, newReliability, destroyed };
|
|
}
|
|
|
|
/**
|
|
* Réparation classique ou de fortune.
|
|
* @param {Item} item
|
|
* @param {Object} opts
|
|
* @param {string} opts.mode "classique" | "fortune"
|
|
* @param {string} opts.skill Compétence de réparation classique (clé de skills)
|
|
* @returns {Promise<Object|null>}
|
|
*/
|
|
static async repair(item, { mode = 'classique', skill = 'technology' } = {}) {
|
|
const actor = item?.actor;
|
|
if (!item || !actor) {
|
|
ui.notifications.warn(game.i18n.localize('VERMINE.reliability_need_actor'));
|
|
return null;
|
|
}
|
|
|
|
const current = parseInt(item.system?.reliability ?? 0, 10) || 0;
|
|
const damagesValue = parseInt(item.system?.damages?.value ?? 0, 10) || 0;
|
|
const handicap = this.panneHandicap(item);
|
|
|
|
// 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;
|
|
|
|
const precision = parseInt(actor.system?.abilities?.precision?.value ?? 0, 10) || 0;
|
|
const skillValue = mode === 'fortune'
|
|
? parseInt(actor.system?.skills?.diy?.value ?? 0, 10) || 0
|
|
: parseInt(actor.system?.skills?.[skill]?.value ?? 0, 10) || 0;
|
|
const NoD = Math.max(0, precision + skillValue);
|
|
|
|
const roll = new Roll(`${NoD}d10cs>=${difficulty}`);
|
|
await roll.evaluate();
|
|
await VermineUtils.showDiceSoNice(roll);
|
|
|
|
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;
|
|
const success = successes >= required;
|
|
|
|
let gained = 0;
|
|
let fortuneFixed = false;
|
|
let resultLabel = '';
|
|
if (success) {
|
|
if (mode === 'classique') {
|
|
gained = successes;
|
|
resultLabel = game.i18n.format('VERMINE.reliability_gained', { gained });
|
|
} else {
|
|
fortuneFixed = true;
|
|
resultLabel = game.i18n.localize('VERMINE.reliability_fixed');
|
|
}
|
|
} else {
|
|
resultLabel = game.i18n.localize('VERMINE.reliability_failed');
|
|
}
|
|
|
|
const updates = {};
|
|
if (mode === 'classique' && gained > 0) {
|
|
updates['system.reliability'] = Math.min(10, current + gained);
|
|
updates['system.damages.value'] = Math.max(0, damagesValue - gained);
|
|
} else if (mode === 'fortune' && fortuneFixed) {
|
|
updates['system.damages.value'] = 0;
|
|
}
|
|
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 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'),
|
|
skillLabel,
|
|
difficulty,
|
|
handicap,
|
|
results,
|
|
successes,
|
|
required,
|
|
resultLabel,
|
|
},
|
|
);
|
|
|
|
await ChatMessage.create({
|
|
user: game.user?._id,
|
|
speaker: this.#getSpeaker(item),
|
|
content,
|
|
});
|
|
|
|
return { success, gained, successes, required };
|
|
}
|
|
|
|
static #getSpeaker(item) {
|
|
return ChatMessage.getSpeaker({ actor: item?.actor ?? null });
|
|
}
|
|
}
|