fix: migrate combat system from d6 to d10 to match official rules

- Change all dice rolls in fight.mjs from d6 to d10
- Implement success counting system (dice >= difficulty) as per Vermine2047 rules
- Add d10 success class to dice that meet or exceed difficulty
- Display success count in confrontation UI
- Update chat message handler to count successes instead of summing dice
- Add comprehensive JSDoc documentation to performTest method
- Add missing French translations for fight tool terms

This corrects a critical inconsistency where fight.mjs was using d6
while the official Vermine2047 rules and the rest of the system (roll.mjs)
use d10 with success counting.

Compatibility: FoundryVTT v11-v14

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-06-04 16:48:57 +02:00
parent 386d80639c
commit f9f07cbc7e
2 changed files with 78 additions and 18 deletions
+8
View File
@@ -130,6 +130,14 @@
"error_no_rerolls_left": "Plus de relances possibles",
"error_select_ability": "Veuillez sélectionner une caractéristique",
"error_not_enough_self_control": "Pas assez de points de sang-froid",
"FightTool": "Outil de combat",
"Roll4Fight": "Lancer pour le combat",
"Selected": "Sélectionné",
"Achievement": "Accomplissement",
"Conservation": "Conservation",
"ConfrontationHint": "Résolvez la confrontation en sélectionnant des dés",
"PurposeTrait": "Trait de but",
"SpleenTrait": "Trait de rate",
"group": "Groupe",
"abilities": "Caractéristiques",
"ability": "Caractéristique",
+69 -17
View File
@@ -1,11 +1,36 @@
import { VERMINE } from "./config.mjs";
import { getActorSkillScore, updateActorSkillScore } from "./functions.mjs";
/**
* Handles combat-related dice rolls for Vermine2047.
* Uses d10-based system with success counting as per official rules.
*/
export class VermineFight {
/**
* Performs a d10-based test according to Vermine2047 official rules.
* Each die result is compared to a difficulty threshold.
* Each result >= difficulty counts as 1 Success.
*
* @param {number} enemyAchievement - Opponent's achievement score
* @param {number} enemyConservation - Opponent's conservation score
* @param {string} skillKey - The skill key being tested
* @param {number} skill - The skill value
* @param {Object} params - Additional test parameters
* @param {number} [params.difficulty=7] - Difficulty threshold (3-10)
* @param {boolean} [params.spleen] - Whether to use spleen rule
* @param {boolean} [params.purpose] - Whether to use purpose rule
* @param {number} [params.usure] - Wear/usage modifier
* @param {number} [params.trait] - Trait bonus
* @param {boolean} [params.specialization] - Whether specialization applies
* @param {Actor} actor - The actor performing the test
* @returns {Promise<Object>} Roll data including successes count
*/
async performTest(enemyAchievement, enemyConservation, skillKey, skill, params, actor) {
// Use d10 as per Vermine2047 official rules
const dicePool = (params.spleen != undefined || params.purpose != undefined) ? '5' : '4';
const r = new Roll(dicePool + `d6`);
const difficulty = params.difficulty || 7; // Default difficulty
const r = new Roll(dicePool + `d10`);
let diceString = '';
let dicePoolHint = '';
let discardedRoll = false;
@@ -47,9 +72,14 @@ export class VermineFight {
}
const discardedRollText = (discardedRoll.result != undefined) ? '<div class="discarded-roll">' + discardedRoll.result + '</div>' : "";
// Count successes (dice >= difficulty) as per Vermine2047 rules
let successes = 0;
for (let i = 0; i < r.terms[0].results.length; i++) {
let result = r.terms[0].results[i].result;
diceString += '<li class="roll die d6 die-' + i + '">' + result + '</li>';
if (result >= difficulty) {
successes++;
}
diceString += '<li class="roll die d10 die-' + i + (result >= difficulty ? ' success' : '') + '">' + result + '</li>';
}
let hintText = game.i18n.format('VERMINE.ConfrontationHint');
@@ -60,7 +90,7 @@ export class VermineFight {
<div class="dice-roll">
<div class="dice-result">
<div class="dice-formula">
` + dicePool + `d6 ` + dicePoolHint + `
` + dicePool + `d10 ` + dicePoolHint + `
</div>
<div class="dice-tooltip expanded">
<section class="tooltip-part">
@@ -80,6 +110,9 @@ export class VermineFight {
<a class="inline-block button reset"><i class="fa-solid fa-rotate-right"></i></a>
<a class="inline-block button resolve"><i class="fa-solid fa-check"></i></a>
</div>
<div class="success-count">
<strong>` + game.i18n.format('VERMINE.success_count') + `:</strong> <span class="success-value">` + successes + `</span>
</div>
</div>
</div>
</div>
@@ -95,7 +128,14 @@ export class VermineFight {
this.sendToChat(html, r, actor);
};
// on fait les comptes
// Return roll data for further processing with d10 success counting
return {
roll: r,
successes: successes,
difficulty: difficulty,
dicePool: parseInt(dicePool, 10),
total: r.total
};
}
@@ -141,42 +181,54 @@ export class VermineFight {
// console.log("accès au fin du fin", message._id);
// sélection du dé actif
html.on("click", '.confrontation .die.d6', event => {
html.on("click", '.confrontation .die.d10', event => {
const diceResult = parseInt($(event.target).html(), 10);
html.find('.confrontation .die.d6').removeClass('active');
html.find('.confrontation .die.d10').removeClass('active');
$(event.target).addClass('active');
});
// sélection des dés d'accomplissement
// sélection des dés d'accomplissement (achievement = successes)
html.on("click", '.confrontation .add-to-achievement', event => {
const diceResult = parseInt(html.find('.confrontation .die.d6.active').html(), 10);
html.find('.confrontation .die.d6.active').removeClass('min').addClass('max');
const diceResult = parseInt(html.find('.confrontation .die.d10.active').html(), 10);
html.find('.confrontation .die.d10.active').removeClass('min').addClass('max');
});
// sélection des dés de conservation
html.on("click", '.confrontation .add-to-conservation', event => {
const diceResult = parseInt(html.find('.confrontation .die.d6.active').html(), 10);
html.find('.confrontation .die.d6.active').removeClass('max').addClass('min');
const diceResult = parseInt(html.find('.confrontation .die.d10.active').html(), 10);
html.find('.confrontation .die.d10.active').removeClass('max').addClass('min');
});
// reset de la sélection des pools
html.on("click", '.confrontation .reset', event => {
html.find('.confrontation .die.d6')
html.find('.confrontation .die.d10')
.removeClass('max')
.removeClass('min');
});
// résolution de la confrontation
// Note: With d10 system, we count successes (dice >= difficulty) instead of summing
html.on("click", '.confrontation .resolve', async event => {
let achievementDice = 0;
let conservationDice = 0;
let achievementBasis = 0;
let conservationBasis = 0;
html.find('.confrontation .die.d6.max').each(function (index) {
achievementDice += parseInt($(this).html(), 10);
const difficulty = 7; // Default difficulty for legacy compatibility
// Count successes (dice >= difficulty) for achievement
html.find('.confrontation .die.d10.max').each(function (index) {
const value = parseInt($(this).html(), 10);
if (value >= difficulty) {
achievementDice++;
}
});
html.find('.confrontation .die.d6.min').each(function (index) {
conservationDice += parseInt($(this).html(), 10);
// Count successes (dice >= difficulty) for conservation
html.find('.confrontation .die.d10.min').each(function (index) {
const value = parseInt($(this).html(), 10);
if (value >= difficulty) {
conservationDice++;
}
});
// saisie des résultats
@@ -188,7 +240,7 @@ export class VermineFight {
html.find('td.conservation-result').data('conservation-value', conservationDice);
html.find('td.conservation-result').html(conservationBasis + conservationDice);
// calcul des marges
// calcul des marges (now based on successes, not sum)
const achievementMargin = achievementBasis + achievementDice - parseInt(html.find('td.adv-achievement-result').html(), 10);
const conservationMargin = conservationBasis + conservationDice - parseInt(html.find('td.adv-conservation-result').html(), 10);
html.find('td.achievement-margin').html(achievementMargin);