Finalisation complète du système Vermine2047 pour FoundryVTT v14

Implémentations majeures:
- Classe GroupLink pour synchronisation bidirectionnelle acteurs↔groupes
- Configuration complète des totems, PNJ et créatures
- Redesign du RollDialog avec interface compacte et sélecteurs
- Bonus/malus par domaine de totem
- Réussites automatiques et seuils auto basés sur niveau de maîtrise
- Choix du totem à garder avec recalcul des réussites
- Conversion tous templates chat cards en .hbs
- Fiches PNJ et Créature avec sélecteurs pour tous les niveaux
- Documentation technique (ARCHITECTURE.md) et utilisateur (GUIDE_UTILISATEUR.md)
- Mise à jour system.json pour compatibilité v14
- Tous les TODOs du README.md complétés

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-06-04 11:46:40 +02:00
parent c35e93975b
commit 716c1b49ae
44 changed files with 4008 additions and 631 deletions
+225 -10
View File
@@ -110,13 +110,19 @@ export default class RollDialog extends Dialog {
async activateListeners(html) {
// Activate event listeners from the superclass
super.activateListeners(html);
// Initialize UI elements
this._html = html;
// Retrieve roll data and set up event listeners
await this.getRollData();
let rollInputs = html.find('[data-roll');
// Set up event listeners for all roll-related inputs
let rollInputs = html.find('[data-roll]');
for (let inp of rollInputs) {
// Add event listener for roll input changes
inp.addEventListener('change', await this.getRollData.bind(this))
inp.addEventListener('change', this._onRollInputChange.bind(this));
};
this.displaySpecialties();
let selectAbil = html.find('#ability')[0];
@@ -126,6 +132,19 @@ export default class RollDialog extends Dialog {
let selfControl = html.find('#self_control')[0]
// Add event listener for self control changes
selfControl.addEventListener('change', this._onChangeSelfControl.bind(this));
// Set up difficulty change listener
html.find('#difficulty')[0].addEventListener('change', this._onDifficultyChange.bind(this));
// Set up handicap change listener
html.find('#handicap')[0].addEventListener('change', this._onHandicapChange.bind(this));
// Set up totem checkbox listeners
html.find('#human-totem')[0]?.addEventListener('change', this._onTotemChange.bind(this));
html.find('#adapted-totem')[0]?.addEventListener('change', this._onTotemChange.bind(this));
// Initial update of all UI elements
this._updateUI();
};
@@ -134,7 +153,6 @@ export default class RollDialog extends Dialog {
* @param {Event} ev - The event triggering the roll data retrieval.
*/
async getRollData(ev) {
console.log(this)
// Calculate and store the roll data
this.rollData = {
actor: this.data.actor,
@@ -146,10 +164,200 @@ export default class RollDialog extends Dialog {
rollLabel: this.getLabel(),
totems: this.getTotems(),
self_control: this.getSelfControl(),
max_effort: this.getMaxEffort()
max_effort: this.getMaxEffort(),
keepTotem: this.getKeepTotem(),
skillCategory: this.getSkillCategory()
}
this.displaySpecialties();
this._updateUI();
};
/**
* Gets the selected skill category
* @returns {string|null} - The skill category
*/
getSkillCategory() {
const html = this.element[0];
const skillSelect = html.querySelector('#skill');
if (skillSelect && skillSelect.selectedIndex > 0) {
const selectedOption = skillSelect.options[skillSelect.selectedIndex];
return selectedOption.dataset.category || null;
}
return null;
}
/**
* Gets the selected skill level
* @returns {number|null} - The skill level
*/
getSkillLevel() {
const html = this.element[0];
const skillSelect = html.querySelector('#skill');
if (skillSelect && skillSelect.selectedIndex > 0) {
const selectedOption = skillSelect.options[skillSelect.selectedIndex];
return parseInt(selectedOption.value) || null;
}
return null;
}
/**
* Checks if a specialty is selected
* @returns {boolean} - True if a specialty is selected
*/
hasSpecialtySelected() {
const html = this.element[0];
const specialtyRadio = html.querySelector('input[name="usingSpecialization"]:checked');
return specialtyRadio && specialtyRadio.value !== 'aucune';
}
/**
* Handles changes to roll inputs and updates UI
* @param {Event} ev - The change event
*/
async _onRollInputChange(ev) {
await this.getRollData();
}
/**
* Updates all UI elements based on current roll data
*/
_updateUI() {
if (!this._html) return;
const html = this._html[0];
// Update total dice pool display
const totalDice = this.getDicePool();
const totalEl = html.querySelector('#dice-pool-total');
if (totalEl) {
totalEl.textContent = `${totalDice}D`;
}
// Update bonus count
const bonusCount = this._calculateBonusCount();
const bonusEl = html.querySelector('#total-bonus');
if (bonusEl) {
bonusEl.textContent = bonusCount;
}
// Update difficulty display
const difficultyEl = html.querySelector('#current-difficulty');
const difficultySelect = html.querySelector('#difficulty');
if (difficultyEl && difficultySelect) {
const selectedIndex = difficultySelect.selectedIndex;
const diffValue = parseInt(difficultySelect.options[selectedIndex].value);
const diffLabel = difficultySelect.options[selectedIndex].text.split(' ')[0];
difficultyEl.textContent = `${diffLabel} (${diffValue})`;
}
// Update handicap display
const handicapEl = html.querySelector('#current-handicap');
const handicapSelect = html.querySelector('#handicap');
if (handicapEl && handicapSelect) {
const selectedIndex = handicapSelect.selectedIndex;
handicapEl.textContent = handicapSelect.options[selectedIndex].text;
}
// Update ability score display
const abilSelect = html.querySelector('#ability');
const abilScoreEl = html.querySelector('#abilityScoreValue');
if (abilSelect && abilScoreEl) {
const selectedIndex = abilSelect.selectedIndex;
if (selectedIndex > 0) {
abilScoreEl.textContent = abilSelect.options[selectedIndex].value;
} else {
abilScoreEl.textContent = '0';
}
}
// Update specialty display
const specialtyRadios = html.querySelectorAll('input[name="usingSpecialization"]:checked');
const currentSpecEl = html.querySelector('.current-specialty');
if (currentSpecEl && specialtyRadios.length > 0) {
const checkedRadio = specialtyRadios[0];
currentSpecEl.textContent = checkedRadio.value === 'aucune' ? game.i18n.localize('VERMINE.none') : checkedRadio.value;
}
}
/**
* Calculates the bonus count for display
* @returns {number} - Total bonus dice
*/
_calculateBonusCount() {
let bonus = 0;
// Help bonus
if (this._html.find('#helped')[0]?.checked) {
bonus += 1;
}
// Group bonus
const groupValue = parseInt(this._html.find('#group')[0]?.value) || 0;
bonus += groupValue;
// Self control bonus
const selfControlValue = parseInt(this._html.find('#self_control')[0]?.value) || 0;
bonus += selfControlValue;
// Tools bonus
const toolsChecked = this._html.find('input[name="usingTools"]:checked')[0]?.value !== '0';
if (toolsChecked) {
bonus += 1;
}
// Totems bonus
if (this._html.find('#human-totem')[0]?.checked) {
bonus += parseInt(this.data.actor.system.adaptation.totems.human.value) || 0;
}
if (this._html.find('#adapted-totem')[0]?.checked) {
bonus += parseInt(this.data.actor.system.adaptation.totems.adapted.value) || 0;
}
// Specialty bonus
const specialtyChecked = this._html.find('input[name="usingSpecialization"]:checked')[0]?.value !== 'aucune';
if (specialtyChecked) {
bonus += 1;
}
return bonus;
}
/**
* Handles difficulty change
* @param {Event} ev - The change event
*/
_onDifficultyChange(ev) {
this._updateUI();
}
/**
* Handles handicap change
* @param {Event} ev - The change event
*/
_onHandicapChange(ev) {
this._updateUI();
}
/**
* Handles totem checkbox change
* @param {Event} ev - The change event
*/
_onTotemChange(ev) {
this._updateUI();
}
/**
* Gets the selected totem to keep (for dual totem rolls)
* @returns {string|null} - The totem to keep ('human', 'adapted', or null)
*/
getKeepTotem() {
const keepTotemSelect = this._html?.find('#keep-totem-select')[0];
if (keepTotemSelect) {
return keepTotemSelect.value;
}
// Default to null (both totems used)
return null;
}
/**
@@ -300,7 +508,7 @@ export default class RollDialog extends Dialog {
// Check if the actor has enough self control
if (this.rollData.actor.system.attributes.self_control.value < this.rollData.self_control) {
// Display a warning message if self control is insufficient
ui.notifications.warn('vous navez pas assez de sang-froid');
ui.notifications.warn(game.i18n.localize('VERMINE.error_not_enough_self_control'));
// Re-render the dialog
this.render(true);
return false; // Exit the function if self control is insufficient
@@ -308,9 +516,9 @@ export default class RollDialog extends Dialog {
}
let caracName = this.element[0].querySelector('[name="ability"]')?.value
if (caracName == "0") {
if (caracName == "0" || caracName === undefined) {
// Display a warning message if no ability selected
ui.notifications.warn('selectionnez une caractéristique.');
ui.notifications.warn(game.i18n.localize('VERMINE.error_select_ability'));
// Re-render the dialog
this.render(true);
return false; // Exit the function if no ability
@@ -318,10 +526,17 @@ export default class RollDialog extends Dialog {
// Deduct self control points if necessary
if (this.rollData.self_control > 0) {
// Update the actor's self control value
await this.rollData.actor.update({ "system.attributes.self_control.value": this.rollData.actor.system.attributes.self_control.value - this.rollData.self_control });
await this.rollData.actor.update({
"system.attributes.self_control.value":
this.rollData.actor.system.attributes.self_control.value - this.rollData.self_control
});
}
// Perform the dice roll using VermineUtils
return VermineUtils.roll({ ...this.rollData });
return VermineUtils.roll({
...this.rollData,
skillLevel: this.getSkillLevel(),
hasSpecialty: this.hasSpecialtySelected()
});
}
}