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} 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} */ 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 }); } }