feat: gestion de la Fiabilité du matériel (test de Fiabilité + réparations)
- 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
This commit is contained in:
@@ -211,6 +211,32 @@
|
|||||||
"rarity_sm": "Rar.",
|
"rarity_sm": "Rar.",
|
||||||
"reliability": "Reliability",
|
"reliability": "Reliability",
|
||||||
"reliability_sm": "Fiab.",
|
"reliability_sm": "Fiab.",
|
||||||
|
"reliability_test": "Reliability test",
|
||||||
|
"reliability_repair": "Repair",
|
||||||
|
"reliability_severity": "Damage severity",
|
||||||
|
"reliability_severity_mineurs": "Minor (1D)",
|
||||||
|
"reliability_severity_graves": "Grave (3D)",
|
||||||
|
"reliability_severity_critiques": "Critical (5D)",
|
||||||
|
"reliability_immersion": "Prolonged immersion",
|
||||||
|
"reliability_damages": "Damage taken",
|
||||||
|
"reliability_state": "State",
|
||||||
|
"reliability_destroyed": "OBJECT DESTROYED / UNUSABLE",
|
||||||
|
"reliability_repair_mode": "Repair type",
|
||||||
|
"reliability_repair_classic": "Classic",
|
||||||
|
"reliability_repair_fortune": "Field repair",
|
||||||
|
"reliability_skill": "Skill",
|
||||||
|
"reliability_run": "Roll",
|
||||||
|
"reliability_successes": "Successes",
|
||||||
|
"reliability_result": "Result",
|
||||||
|
"reliability_gained": "Reliability restored: +{gained}",
|
||||||
|
"reliability_fixed": "Breakdown fixed (object functional)",
|
||||||
|
"reliability_failed": "Failure — no reliability point restored",
|
||||||
|
"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_cassant": "Fragile: reliability losses increased by 1 point",
|
||||||
|
"reliability_etanche": "Watertight: immersion damage ignored",
|
||||||
"wounds": {
|
"wounds": {
|
||||||
"name": "Wounds",
|
"name": "Wounds",
|
||||||
"threshold": "Threshold",
|
"threshold": "Threshold",
|
||||||
|
|||||||
@@ -292,6 +292,32 @@
|
|||||||
"rarity_sm": "Rar.",
|
"rarity_sm": "Rar.",
|
||||||
"reliability": "Fiabilité",
|
"reliability": "Fiabilité",
|
||||||
"reliability_sm": "Fiab.",
|
"reliability_sm": "Fiab.",
|
||||||
|
"reliability_test": "Test de Fiabilité",
|
||||||
|
"reliability_repair": "Réparer",
|
||||||
|
"reliability_severity": "Gravité des Dommages",
|
||||||
|
"reliability_severity_mineurs": "Mineurs (1D)",
|
||||||
|
"reliability_severity_graves": "Graves (3D)",
|
||||||
|
"reliability_severity_critiques": "Critiques (5D)",
|
||||||
|
"reliability_immersion": "Immersion prolongée",
|
||||||
|
"reliability_damages": "Dommages subis",
|
||||||
|
"reliability_state": "État",
|
||||||
|
"reliability_destroyed": "OBJET DÉTRUIT / INUTILISABLE",
|
||||||
|
"reliability_repair_mode": "Type de réparation",
|
||||||
|
"reliability_repair_classic": "Classique",
|
||||||
|
"reliability_repair_fortune": "De fortune",
|
||||||
|
"reliability_skill": "Compétence",
|
||||||
|
"reliability_run": "Lancer",
|
||||||
|
"reliability_successes": "Réussites",
|
||||||
|
"reliability_result": "Résultat",
|
||||||
|
"reliability_gained": "Fiabilité restaurée : +{gained}",
|
||||||
|
"reliability_fixed": "Panne réparée (objet fonctionnel)",
|
||||||
|
"reliability_failed": "Échec — aucun point de Fiabilité rendu",
|
||||||
|
"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_cassant": "Cassant : pertes de Fiabilité majorées d'1 point",
|
||||||
|
"reliability_etanche": "Étanche : Dommages d'immersion ignorés",
|
||||||
"choose_ability": "Choisissez une caractéristique",
|
"choose_ability": "Choisissez une caractéristique",
|
||||||
"choose_skill": "Choisissez une compétence",
|
"choose_skill": "Choisissez une compétence",
|
||||||
"none": "Aucun",
|
"none": "Aucun",
|
||||||
|
|||||||
+161
@@ -1171,3 +1171,164 @@ ol.chat-log {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Test de Fiabilité / Réparations ──────────────────────────────────
|
||||||
|
.vermine-reliability-message {
|
||||||
|
padding: 4px 10px 10px;
|
||||||
|
|
||||||
|
.reliability-header {
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-13;
|
||||||
|
margin: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
color: @theme-color-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reliability-meta {
|
||||||
|
gap: 4px 12px;
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
.reliability-meta-item {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
color: @color-text-dark-primary;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reliability-results {
|
||||||
|
gap: 6px;
|
||||||
|
margin: 6px 0;
|
||||||
|
|
||||||
|
.reliability-die {
|
||||||
|
flex: none;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
font-size: @font-size-16;
|
||||||
|
border: 1px solid @color-border-dark-4;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
color: @color-text-dark-primary;
|
||||||
|
|
||||||
|
&.damage {
|
||||||
|
border-color: @color-hemolymph;
|
||||||
|
background: fade(@color-hemolymph, 15%);
|
||||||
|
color: @color-hemolymph;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
border-color: @color-acid-green;
|
||||||
|
background: fade(@color-acid-green, 15%);
|
||||||
|
color: @color-acid-green;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reliability-summary {
|
||||||
|
gap: 4px 16px;
|
||||||
|
margin: 4px 0;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
color: @color-text-dark-primary;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reliability-note {
|
||||||
|
margin: 2px 0;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-style: italic;
|
||||||
|
color: @color-text-dark-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reliability-destroyed {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-family: "DistressBlack";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
text-align: center;
|
||||||
|
color: @color-hemolymph;
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: fade(@color-hemolymph, 12%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item .reliability-actions {
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 4px 0 8px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: "DistressBlack", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
padding: 3px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: @theme-color-dark;
|
||||||
|
background: fade(@color-hemolymph, 15%);
|
||||||
|
border: 1px solid @color-hemolymph;
|
||||||
|
border-radius: 3px;
|
||||||
|
.transition();
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @theme-color-dark;
|
||||||
|
background: @color-hemolymph;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reliability-severities,
|
||||||
|
.reliability-modes {
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.reliability-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: @font-size-12;
|
||||||
|
color: @color-text-dark-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reliability-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: @font-size-11;
|
||||||
|
font-style: italic;
|
||||||
|
color: @color-text-dark-secondary;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.app
|
|||||||
editImage: VermineBaseItemSheet.#onEditImage,
|
editImage: VermineBaseItemSheet.#onEditImage,
|
||||||
toggleSheet: VermineBaseItemSheet.#onToggleSheet,
|
toggleSheet: VermineBaseItemSheet.#onToggleSheet,
|
||||||
clickDamage: VermineBaseItemSheet.#onClickDamage,
|
clickDamage: VermineBaseItemSheet.#onClickDamage,
|
||||||
openTraits: VermineBaseItemSheet.#onOpenTraits
|
openTraits: VermineBaseItemSheet.#onOpenTraits,
|
||||||
|
reliabilityTest: VermineBaseItemSheet.#onReliabilityTest,
|
||||||
|
reliabilityRepair: VermineBaseItemSheet.#onReliabilityRepair
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,4 +122,16 @@ export class VermineBaseItemSheet extends HandlebarsApplicationMixin(foundry.app
|
|||||||
const { TraitSelector } = await import("../../system/applications.mjs")
|
const { TraitSelector } = await import("../../system/applications.mjs")
|
||||||
new TraitSelector(this.document).render(true)
|
new TraitSelector(this.document).render(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onReliabilityTest() {
|
||||||
|
const { ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||||
|
const dialog = new ReliabilityDialog({ item: this.document, mode: "test" })
|
||||||
|
await dialog.render(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onReliabilityRepair() {
|
||||||
|
const { ReliabilityDialog } = await import("../../system/dialogs/reliabilityDialog.mjs")
|
||||||
|
const dialog = new ReliabilityDialog({ item: this.document, mode: "repair" })
|
||||||
|
await dialog.render(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { VermineReliability } from '../reliability.mjs';
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialogue de Test de Fiabilité / Réparation d'un item.
|
||||||
|
* Mode "test" : choix de la gravité des Dommages matériels (Mineurs/Graves/Critiques)
|
||||||
|
* + case Immersion (Trait Étanche). Mode "repair" : type de réparation
|
||||||
|
* (classique/de fortune) + compétence utilisée.
|
||||||
|
*/
|
||||||
|
export default class ReliabilityDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||||
|
#item;
|
||||||
|
#mode;
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: ['vermine-roll', 'reliability'],
|
||||||
|
tag: 'form',
|
||||||
|
window: {
|
||||||
|
icon: 'fas fa-tools',
|
||||||
|
resizable: false,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 460,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
run: ReliabilityDialog.#onRun,
|
||||||
|
cancel: ReliabilityDialog.#onCancel,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: { template: 'systems/vermine2047/templates/dialogs/reliability-dialog.hbs', scrollable: [''] },
|
||||||
|
};
|
||||||
|
|
||||||
|
static async create({ item, mode = 'test' }) {
|
||||||
|
return new ReliabilityDialog({ item, mode });
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options = {}) {
|
||||||
|
super(options);
|
||||||
|
this.#item = options.item;
|
||||||
|
this.#mode = options.mode ?? 'test';
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return game.i18n.localize(this.#mode === 'repair' ? 'VERMINE.reliability_repair' : 'VERMINE.reliability_test');
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareContext() {
|
||||||
|
const hasEtanche = !!this.#item.system?.traits?.etanche;
|
||||||
|
const hasActor = !!this.#item.actor;
|
||||||
|
const severities = ['mineurs', 'graves', 'critiques'].map((key) => ({
|
||||||
|
key,
|
||||||
|
label: game.i18n.localize(`VERMINE.reliability_severity_${key}`),
|
||||||
|
selected: key === 'graves',
|
||||||
|
}));
|
||||||
|
const skills = VermineReliability.REPAIR_SKILLS.map((key) => ({
|
||||||
|
key,
|
||||||
|
label: game.i18n.localize(`SKILLS.${key}.name`),
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
mode: this.#mode,
|
||||||
|
isTest: this.#mode === 'test',
|
||||||
|
isRepair: this.#mode === 'repair',
|
||||||
|
hasEtanche,
|
||||||
|
hasActor,
|
||||||
|
severities,
|
||||||
|
skills,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRender(context, options) {
|
||||||
|
super._onRender(context, options);
|
||||||
|
if (this.#mode !== 'repair') return;
|
||||||
|
const select = this.element.querySelector('#reliability-skill');
|
||||||
|
const radios = this.element.querySelectorAll('input[name="repairMode"]');
|
||||||
|
if (!select || !radios.length) return;
|
||||||
|
const refresh = () => {
|
||||||
|
const fortune = this.element.querySelector('input[name="repairMode"]:checked')?.value === 'fortune';
|
||||||
|
select.closest('.form-group')?.classList.toggle('hidden', fortune);
|
||||||
|
};
|
||||||
|
radios.forEach((r) => r.addEventListener('change', refresh));
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
#getOptions() {
|
||||||
|
const el = this.element;
|
||||||
|
if (this.#mode === 'test') {
|
||||||
|
return {
|
||||||
|
severity: el.querySelector('input[name="severity"]:checked')?.value ?? 'graves',
|
||||||
|
immersion: !!el.querySelector('#reliability-immersion')?.checked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mode: el.querySelector('input[name="repairMode"]:checked')?.value ?? 'classique',
|
||||||
|
skill: el.querySelector('#reliability-skill')?.value ?? 'technology',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onRun() {
|
||||||
|
const options = this.#getOptions();
|
||||||
|
if (this.#mode === 'test') {
|
||||||
|
await VermineReliability.test(this.#item, options);
|
||||||
|
} else {
|
||||||
|
await VermineReliability.repair(this.#item, options);
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static #onCancel() {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<div class="roll-dialog-content">
|
||||||
|
{{#if isTest}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{ localize "VERMINE.reliability_severity" }}</label>
|
||||||
|
<div class="flexrow flex-wrap reliability-severities">
|
||||||
|
{{#each severities}}
|
||||||
|
<label class="reliability-option">
|
||||||
|
<input type="radio" name="severity" value="{{this.key}}" {{#if this.selected}}checked{{/if}}>
|
||||||
|
<span>{{this.label}}</span>
|
||||||
|
</label>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{#if hasEtanche}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="flexrow reliability-option">
|
||||||
|
<input type="checkbox" id="reliability-immersion" name="immersion">
|
||||||
|
<span>{{ localize "VERMINE.reliability_immersion" }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{ localize "VERMINE.reliability_repair_mode" }}</label>
|
||||||
|
<div class="flexrow flex-wrap reliability-modes">
|
||||||
|
<label class="reliability-option">
|
||||||
|
<input type="radio" name="repairMode" value="classique" checked>
|
||||||
|
<span>{{ localize "VERMINE.reliability_repair_classic" }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="reliability-option">
|
||||||
|
<input type="radio" name="repairMode" value="fortune">
|
||||||
|
<span>{{ localize "VERMINE.reliability_repair_fortune" }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reliability-skill">{{ localize "VERMINE.reliability_skill" }}</label>
|
||||||
|
<select id="reliability-skill" name="skill">
|
||||||
|
{{#each skills}}
|
||||||
|
<option value="{{this.key}}">{{this.label}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{{#unless hasActor}}
|
||||||
|
<p class="reliability-hint">{{ localize "VERMINE.reliability_need_actor" }}</p>
|
||||||
|
{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<footer class="sheet-footer flexrow">
|
||||||
|
<button type="button" data-action="run">{{ localize "VERMINE.reliability_run" }}</button>
|
||||||
|
<button type="button" data-action="cancel">{{ localize "VERMINE.cancel" }}</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
@@ -38,6 +38,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</h4>
|
</h4>
|
||||||
|
<div class="flexrow reliability-actions">
|
||||||
|
<button type="button" data-action="reliabilityTest" title="{{ localize 'VERMINE.reliability_test' }}"><i class="fas fa-flask"></i> {{ localize 'VERMINE.reliability_test' }}</button>
|
||||||
|
<button type="button" data-action="reliabilityRepair" title="{{ localize 'VERMINE.reliability_repair' }}"><i class="fas fa-hammer"></i> {{ localize 'VERMINE.reliability_repair' }}</button>
|
||||||
|
</div>
|
||||||
<h2>description</h2>
|
<h2>description</h2>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{localize "VERMINE.sheet.description"}}</legend>
|
<legend>{{localize "VERMINE.sheet.description"}}</legend>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="vermine-reliability-message">
|
||||||
|
<div class="reliability-header flexrow">
|
||||||
|
{{#if item.img}}<img src="{{item.img}}" alt="{{item.name}}">{{/if}}
|
||||||
|
<h3>{{item.name}} — {{ localize "VERMINE.reliability_repair" }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="reliability-meta flexrow flex-wrap">
|
||||||
|
<span class="reliability-meta-item"><strong>{{ localize "VERMINE.reliability_repair_mode" }}:</strong> {{modeLabel}}</span>
|
||||||
|
<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}}
|
||||||
|
</div>
|
||||||
|
<ul class="flexrow reliability-results">
|
||||||
|
{{#each results}}
|
||||||
|
<li class="reliability-die {{#if this.isSuccess}}success{{/if}}">{{this.value}}</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="vermine-reliability-message">
|
||||||
|
<div class="reliability-header flexrow">
|
||||||
|
{{#if item.img}}<img src="{{item.img}}" alt="{{item.name}}">{{/if}}
|
||||||
|
<h3>{{item.name}} — {{ localize "VERMINE.reliability_test" }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="reliability-meta flexrow flex-wrap">
|
||||||
|
<span class="reliability-meta-item"><strong>{{ localize "VERMINE.reliability_severity" }}:</strong> {{severityLabel}}</span>
|
||||||
|
<span class="reliability-meta-item"><strong>{{ localize "VERMINE.reliability" }}:</strong> {{oldReliability}}</span>
|
||||||
|
</div>
|
||||||
|
<ul class="flexrow reliability-results">
|
||||||
|
{{#each results}}
|
||||||
|
<li class="reliability-die {{#if this.isDamage}}damage{{/if}}">{{this.value}}</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<div class="reliability-summary flexrow flex-wrap">
|
||||||
|
<span><strong>{{ localize "VERMINE.reliability_damages" }}:</strong> {{dommages}}</span>
|
||||||
|
<span><strong>{{ localize "VERMINE.reliability" }}:</strong> {{oldReliability}} → {{newReliability}}</span>
|
||||||
|
{{#if state}}<span><strong>{{ localize "VERMINE.reliability_state" }}:</strong> {{state}}</span>{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#each traitsNotes}}<p class="reliability-note">{{this}}</p>{{/each}}
|
||||||
|
{{#if destroyed}}<p class="reliability-destroyed">{{ localize "VERMINE.reliability_destroyed" }}</p>{{/if}}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user