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:
@@ -86,7 +86,7 @@ export class VermineItem extends Item {
|
||||
rollMode: rollMode,
|
||||
flavor: label,
|
||||
};
|
||||
mess.content = await renderTemplate(`systems/vermine2047/templates/item/chatCards/${this.type}.html`, { item: this, message: mess }) ?? null;
|
||||
mess.content = await renderTemplate(`systems/vermine2047/templates/item/chatCards/${this.type}.hbs`, { item: this, message: mess }) ?? null;
|
||||
ChatMessage.create(mess)
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,142 @@ VERMINE.PackLevels = {
|
||||
3: { "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Domains of influence for each totem
|
||||
* Each totem provides bonus to certain skill categories
|
||||
*/
|
||||
VERMINE.totemDomains = {
|
||||
"human": {
|
||||
"label": "TOTEMS.human.name",
|
||||
"domains": ["man", "world"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem humain favorise les compétences liées à l'humanité et au monde civilisé"
|
||||
},
|
||||
"predator": {
|
||||
"label": "TOTEMS.predator.name",
|
||||
"domains": ["animal", "survival"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem prédateur favorise la chasse et la survie"
|
||||
},
|
||||
"scavenger": {
|
||||
"label": "TOTEMS.scavenger.name",
|
||||
"domains": ["tool", "world"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem charognard favorise la récupération et l'utilisation d'outils"
|
||||
},
|
||||
"symbiote": {
|
||||
"label": "TOTEMS.symbiote.name",
|
||||
"domains": ["man", "social"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem symbiote favorise les interactions sociales"
|
||||
},
|
||||
"parasite": {
|
||||
"label": "TOTEMS.parasite.name",
|
||||
"domains": ["animal", "survival"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem parasite favorise la discrétion et la survie"
|
||||
},
|
||||
"builder": {
|
||||
"label": "TOTEMS.builder.name",
|
||||
"domains": ["tool", "world"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem bâtisseur favorise la construction et la manipulation"
|
||||
},
|
||||
"horde": {
|
||||
"label": "TOTEMS.horde.name",
|
||||
"domains": ["animal", "survival"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem horde favorise le combat en groupe"
|
||||
},
|
||||
"hive": {
|
||||
"label": "TOTEMS.hive.name",
|
||||
"domains": ["man", "social"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem ruche favorise l'organisation collective"
|
||||
},
|
||||
"loner": {
|
||||
"label": "TOTEMS.loner.name",
|
||||
"domains": ["survival", "world"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem solitaire favorise l'autonomie"
|
||||
},
|
||||
"adapted": {
|
||||
"label": "TOTEMS.adapted.name",
|
||||
"domains": ["animal", "survival"],
|
||||
"bonus": +1,
|
||||
"description": "Le totem adapté favorise l'adaptation à l'environnement"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NPC Threat Levels configuration
|
||||
*/
|
||||
VERMINE.npcThreatLevels = {
|
||||
1: { "label": "THREAT_LEVELS.minor", "attack": 3, "vigor": 1, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
2: { "label": "THREAT_LEVELS.serious", "attack": 4, "vigor": 2, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "THREAT_LEVELS.major", "attack": 5, "vigor": 3, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "THREAT_LEVELS.deadly", "attack": 6, "vigor": 4, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* NPC Experience Levels configuration
|
||||
*/
|
||||
VERMINE.npcExperienceLevels = {
|
||||
1: { "label": "SKILL_LEVELS.beginner", "action": 3, "specialties": 4, "rerolls": 0, "contact": "7" },
|
||||
2: { "label": "SKILL_LEVELS.proficient", "action": 3, "specialties": 5, "rerolls": 0, "contact": "5 ou 7" },
|
||||
3: { "label": "SKILL_LEVELS.expert", "action": 4, "specialties": 6, "rerolls": 1, "contact": "5,7 ou 9" },
|
||||
4: { "label": "SKILL_LEVELS.master", "action": 4, "specialties": 6, "rerolls": 2, "contact": "3,5,7 ou 9" }
|
||||
}
|
||||
|
||||
/**
|
||||
* NPC Role Levels configuration
|
||||
*/
|
||||
VERMINE.npcRoleLevels = {
|
||||
1: { "label": "ROLE_LEVELS.minor", "reaction": 3, "reaction_bonus": 0, "pools": 0, "gear": 9, "gear_hindrance": 0, "protection": 1 },
|
||||
2: { "label": "ROLE_LEVELS.secondary", "reaction": 3, "reaction_bonus": 1, "pools": 1, "gear": 9, "gear_hindrance": 1, "protection": 2 },
|
||||
3: { "label": "ROLE_LEVELS.important", "reaction": 3, "reaction_bonus": 2, "pools": 2, "gear": 9, "gear_hindrance": 2, "protection": 3 },
|
||||
4: { "label": "ROLE_LEVELS.major", "reaction": 4, "reaction_bonus": 2, "pools": 4, "gear": 10, "gear_hindrance": 2, "protection": 3 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creature Pattern Levels configuration
|
||||
*/
|
||||
VERMINE.creaturePatternLevels = {
|
||||
1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creature Size Levels configuration
|
||||
*/
|
||||
VERMINE.creatureSizeLevels = {
|
||||
1: { "attack": 2, "vigor": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "attack": 3, "vigor": 2, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "attack": 4, "vigor": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creature Pack Levels configuration
|
||||
*/
|
||||
VERMINE.creaturePackLevels = {
|
||||
0: { "attack": 0, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
|
||||
3: { "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creature Role Levels configuration (same as NPC roles)
|
||||
*/
|
||||
VERMINE.creatureRoleLevels = {
|
||||
1: { "label": "ROLE_LEVELS.minor", "reaction": 3, "reaction_bonus": 0, "pools": 0, "gear": 9, "gear_hindrance": 0, "protection": 1 },
|
||||
2: { "label": "ROLE_LEVELS.secondary", "reaction": 3, "reaction_bonus": 1, "pools": 1, "gear": 9, "gear_hindrance": 1, "protection": 2 },
|
||||
3: { "label": "ROLE_LEVELS.important", "reaction": 3, "reaction_bonus": 2, "pools": 2, "gear": 9, "gear_hindrance": 2, "protection": 3 },
|
||||
4: { "label": "ROLE_LEVELS.major", "reaction": 4, "reaction_bonus": 2, "pools": 4, "gear": 10, "gear_hindrance": 2, "protection": 3 }
|
||||
}
|
||||
|
||||
VERMINE.abilityCategories = {
|
||||
"physical": {
|
||||
"label": "VERMINE.ability_category.physical"
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* GroupLink - Gestion des liens entre acteurs et groupes
|
||||
*
|
||||
* Cette classe permet de gérer les relations bidirectionnelles entre :
|
||||
* - Les personnages (characters) et leurs groupes/rencontres
|
||||
* - Les groupes (groups) et leurs membres/rencontres
|
||||
*
|
||||
* @author Vermine2047 System
|
||||
*/
|
||||
|
||||
export class GroupLink {
|
||||
|
||||
/**
|
||||
* Met à jour les groupes dans tous les personnages membres
|
||||
* quand un groupe est modifié
|
||||
* @param {Actor} group - Le groupe modifié
|
||||
* @param {Object} changes - Les changements effectués
|
||||
*/
|
||||
static async updateActorsOnGroupChange(group, changes) {
|
||||
if (group.type !== 'group') return;
|
||||
|
||||
const groupData = group.system;
|
||||
const members = groupData.members || [];
|
||||
const encounters = groupData.encounters || [];
|
||||
|
||||
// Mettre à jour les membres du groupe
|
||||
if (changes.members !== undefined || changes.encounters !== undefined) {
|
||||
await this._updateMembersInGroup(group, members);
|
||||
await this._updateEncountersInGroup(group, encounters);
|
||||
}
|
||||
|
||||
// Synchroniser les données dans les acteurs membres
|
||||
await this._syncGroupToMembers(group, members);
|
||||
await this._syncGroupToEncounters(group, encounters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le groupe quand un personnage est modifié
|
||||
* @param {Actor} actor - L'acteur modifié
|
||||
* @param {Object} changes - Les changements effectués
|
||||
*/
|
||||
static async updateGroupsOnActorChange(actor, changes) {
|
||||
if (actor.type === 'group') return;
|
||||
|
||||
const actorData = actor.system;
|
||||
const encounters = actorData.encounters || [];
|
||||
|
||||
// Si les rencontres de l'acteur ont changé
|
||||
if (changes.encounters !== undefined) {
|
||||
// Pour chaque groupe dans les rencontres, mettre à jour les membres
|
||||
for (const groupId of encounters) {
|
||||
const group = game.actors.get(groupId);
|
||||
if (group && group.type === 'group') {
|
||||
await this._updateActorInGroupMembers(group, actor.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise les données du groupe vers les acteurs membres
|
||||
* @param {Actor} group - Le groupe
|
||||
* @param {Array} memberIds - Liste des IDs des membres
|
||||
*/
|
||||
static async _syncGroupToMembers(group, memberIds) {
|
||||
for (const memberId of memberIds) {
|
||||
const member = game.actors.get(memberId);
|
||||
if (member) {
|
||||
// Vérifier que le groupe est dans les rencontres du membre
|
||||
const memberEncounters = member.system.encounters || [];
|
||||
if (!memberEncounters.includes(group.id)) {
|
||||
// Ajouter le groupe aux rencontres du membre
|
||||
memberEncounters.push(group.id);
|
||||
await member.update({
|
||||
'system.encounters': memberEncounters
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise les données du groupe vers les acteurs rencontres
|
||||
* @param {Actor} group - Le groupe
|
||||
* @param {Array} encounterIds - Liste des IDs des rencontres
|
||||
*/
|
||||
static async _syncGroupToEncounters(group, encounterIds) {
|
||||
for (const encounterId of encounterIds) {
|
||||
const encounter = game.actors.get(encounterId);
|
||||
if (encounter) {
|
||||
// Vérifier que le groupe est dans les rencontres de l'acteur
|
||||
const encounterGroups = encounter.system.encounters || [];
|
||||
if (!encounterGroups.includes(group.id)) {
|
||||
encounterGroups.push(group.id);
|
||||
await encounter.update({
|
||||
'system.encounters': encounterGroups
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les membres dans un groupe
|
||||
* @param {Actor} group - Le groupe
|
||||
* @param {Array} memberIds - Liste des IDs des membres
|
||||
*/
|
||||
static async _updateMembersInGroup(group, memberIds) {
|
||||
const currentMembers = group.system.members || [];
|
||||
|
||||
// Retirer les membres qui ne sont plus dans la liste
|
||||
const membersToRemove = currentMembers.filter(id => !memberIds.includes(id));
|
||||
const membersToAdd = memberIds.filter(id => !currentMembers.includes(id));
|
||||
|
||||
// Mettre à jour les acteurs qui ont été retirés
|
||||
for (const memberId of membersToRemove) {
|
||||
const member = game.actors.get(memberId);
|
||||
if (member) {
|
||||
const memberEncounters = (member.system.encounters || []).filter(id => id !== group.id);
|
||||
await member.update({
|
||||
'system.encounters': memberEncounters
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour les nouveaux membres
|
||||
for (const memberId of membersToAdd) {
|
||||
const member = game.actors.get(memberId);
|
||||
if (member) {
|
||||
const memberEncounters = member.system.encounters || [];
|
||||
if (!memberEncounters.includes(group.id)) {
|
||||
memberEncounters.push(group.id);
|
||||
await member.update({
|
||||
'system.encounters': memberEncounters
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les rencontres dans un groupe
|
||||
* @param {Actor} group - Le groupe
|
||||
* @param {Array} encounterIds - Liste des IDs des rencontres
|
||||
*/
|
||||
static async _updateEncountersInGroup(group, encounterIds) {
|
||||
const currentEncounters = group.system.encounters || [];
|
||||
|
||||
// Retirer les rencontres qui ne sont plus dans la liste
|
||||
const encountersToRemove = currentEncounters.filter(id => !encounterIds.includes(id));
|
||||
const encountersToAdd = encounterIds.filter(id => !currentEncounters.includes(id));
|
||||
|
||||
// Mettre à jour les acteurs qui ont été retirés des rencontres
|
||||
for (const encounterId of encountersToRemove) {
|
||||
const encounter = game.actors.get(encounterId);
|
||||
if (encounter) {
|
||||
const encounterGroups = (encounter.system.encounters || []).filter(id => id !== group.id);
|
||||
await encounter.update({
|
||||
'system.encounters': encounterGroups
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour les nouvelles rencontres
|
||||
for (const encounterId of encountersToAdd) {
|
||||
const encounter = game.actors.get(encounterId);
|
||||
if (encounter) {
|
||||
const encounterGroups = encounter.system.encounters || [];
|
||||
if (!encounterGroups.includes(group.id)) {
|
||||
encounterGroups.push(group.id);
|
||||
await encounter.update({
|
||||
'system.encounters': encounterGroups
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un acteur dans les membres d'un groupe
|
||||
* @param {Actor} group - Le groupe
|
||||
* @param {string} actorId - L'ID de l'acteur
|
||||
*/
|
||||
static async _updateActorInGroupMembers(group, actorId) {
|
||||
const groupMembers = group.system.members || [];
|
||||
if (!groupMembers.includes(actorId)) {
|
||||
groupMembers.push(actorId);
|
||||
await group.update({
|
||||
'system.members': groupMembers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour un acteur dans les rencontres d'un groupe
|
||||
* @param {Actor} group - Le groupe
|
||||
* @param {string} actorId - L'ID de l'acteur
|
||||
*/
|
||||
static async _updateActorInGroupEncounters(group, actorId) {
|
||||
const groupEncounters = group.system.encounters || [];
|
||||
if (!groupEncounters.includes(actorId)) {
|
||||
groupEncounters.push(actorId);
|
||||
await group.update({
|
||||
'system.encounters': groupEncounters
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les objets Actor pour une liste d'IDs
|
||||
* @param {Array} actorIds - Liste d'IDs d'acteurs
|
||||
* @returns {Array} - Liste d'objets Actor
|
||||
*/
|
||||
static getActorObjects(actorIds) {
|
||||
return actorIds
|
||||
.map(id => game.actors.get(id))
|
||||
.filter(actor => actor !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les objets Actor pour les membres d'un groupe
|
||||
* @param {Actor} group - Le groupe
|
||||
* @returns {Array} - Liste d'objets Actor
|
||||
*/
|
||||
static getGroupMembers(group) {
|
||||
const memberIds = group.system.members || [];
|
||||
return this.getActorObjects(memberIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les objets Actor pour les rencontres d'un groupe
|
||||
* @param {Actor} group - Le groupe
|
||||
* @returns {Array} - Liste d'objets Actor
|
||||
*/
|
||||
static getGroupEncounters(group) {
|
||||
const encounterIds = group.system.encounters || [];
|
||||
return this.getActorObjects(encounterIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les groupes auxquels un acteur appartient
|
||||
* @param {Actor} actor - L'acteur
|
||||
* @returns {Array} - Liste d'objets Actor (groupes)
|
||||
*/
|
||||
static getActorGroups(actor) {
|
||||
const groupIds = actor.system.encounters || [];
|
||||
return this.getActorObjects(groupIds).filter(a => a.type === 'group');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les rencontres (PNJ/Créatures) d'un acteur
|
||||
* @param {Actor} actor - L'acteur
|
||||
* @returns {Array} - Liste d'objets Actor (PNJ/Créatures)
|
||||
*/
|
||||
static getActorEncounters(actor) {
|
||||
const encounterIds = actor.system.encounters || [];
|
||||
return this.getActorObjects(encounterIds).filter(a => a.type !== 'group');
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un acteur de tous ses groupes
|
||||
* @param {string} actorId - L'ID de l'acteur à supprimer
|
||||
*/
|
||||
static async removeActorFromAllGroups(actorId) {
|
||||
const allGroups = game.actors.filter(a => a.type === 'group');
|
||||
|
||||
for (const group of allGroups) {
|
||||
const members = group.system.members || [];
|
||||
const encounters = group.system.encounters || [];
|
||||
|
||||
let needsUpdate = false;
|
||||
const newMembers = members.filter(id => id !== actorId);
|
||||
const newEncounters = encounters.filter(id => id !== actorId);
|
||||
|
||||
if (newMembers.length !== members.length || newEncounters.length !== encounters.length) {
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
await group.update({
|
||||
'system.members': newMembers,
|
||||
'system.encounters': newEncounters
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer les groupes des rencontres de l'acteur
|
||||
const actor = game.actors.get(actorId);
|
||||
if (actor) {
|
||||
await actor.update({
|
||||
'system.encounters': []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute un acteur à un groupe
|
||||
* @param {string} actorId - L'ID de l'acteur
|
||||
* @param {string} groupId - L'ID du groupe
|
||||
*/
|
||||
static async addActorToGroup(actorId, groupId) {
|
||||
const actor = game.actors.get(actorId);
|
||||
const group = game.actors.get(groupId);
|
||||
|
||||
if (!actor || !group || group.type !== 'group') return;
|
||||
|
||||
// Ajouter l'acteur aux membres du groupe
|
||||
const groupMembers = group.system.members || [];
|
||||
if (!groupMembers.includes(actorId)) {
|
||||
groupMembers.push(actorId);
|
||||
await group.update({
|
||||
'system.members': groupMembers
|
||||
});
|
||||
}
|
||||
|
||||
// Ajouter le groupe aux rencontres de l'acteur
|
||||
const actorEncounters = actor.system.encounters || [];
|
||||
if (!actorEncounters.includes(groupId)) {
|
||||
actorEncounters.push(groupId);
|
||||
await actor.update({
|
||||
'system.encounters': actorEncounters
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire un acteur d'un groupe
|
||||
* @param {string} actorId - L'ID de l'acteur
|
||||
* @param {string} groupId - L'ID du groupe
|
||||
*/
|
||||
static async removeActorFromGroup(actorId, groupId) {
|
||||
const actor = game.actors.get(actorId);
|
||||
const group = game.actors.get(groupId);
|
||||
|
||||
if (!actor || !group || group.type !== 'group') return;
|
||||
|
||||
// Retirer l'acteur des membres du groupe
|
||||
const groupMembers = (group.system.members || []).filter(id => id !== actorId);
|
||||
await group.update({
|
||||
'system.members': groupMembers
|
||||
});
|
||||
|
||||
// Retirer le groupe des rencontres de l'acteur
|
||||
const actorEncounters = (actor.system.encounters || []).filter(id => id !== groupId);
|
||||
await actor.update({
|
||||
'system.encounters': actorEncounters
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise les hooks pour la synchronisation automatique
|
||||
*/
|
||||
static registerHooks() {
|
||||
// Hook sur la mise à jour d'un acteur
|
||||
Hooks.on('updateActor', async (actor, changes, options, userId) => {
|
||||
if (!game.user.isGM && userId !== game.userId) return;
|
||||
|
||||
// Si c'est un groupe qui est mis à jour
|
||||
if (actor.type === 'group') {
|
||||
await this.updateActorsOnGroupChange(actor, changes);
|
||||
}
|
||||
// Si c'est un autre acteur qui est mis à jour
|
||||
else {
|
||||
await this.updateGroupsOnActorChange(actor, changes);
|
||||
}
|
||||
});
|
||||
|
||||
// Hook sur la création d'un acteur
|
||||
Hooks.on('createActor', async (actor, options, userId) => {
|
||||
if (!game.user.isGM && userId !== game.userId) return;
|
||||
|
||||
// Si un personnage est créé, vérifier qu'il n'a pas de groupes invalides
|
||||
if (actor.type !== 'group') {
|
||||
const encounters = actor.system.encounters || [];
|
||||
for (const groupId of encounters) {
|
||||
const group = game.actors.get(groupId);
|
||||
if (!group) {
|
||||
// Nettoyer les références invalides
|
||||
await actor.update({
|
||||
'system.encounters': encounters.filter(id => game.actors.get(id))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hook sur la suppression d'un acteur
|
||||
Hooks.on('deleteActor', async (actor, options, userId) => {
|
||||
if (!game.user.isGM && userId !== game.userId) return;
|
||||
|
||||
if (actor.type === 'group') {
|
||||
// Si un groupe est supprimé, nettoyer les références dans les acteurs
|
||||
const memberIds = actor.system.members || [];
|
||||
const encounterIds = actor.system.encounters || [];
|
||||
|
||||
for (const id of [...memberIds, ...encounterIds]) {
|
||||
const a = game.actors.get(id);
|
||||
if (a) {
|
||||
const encounters = (a.system.encounters || []).filter(eid => eid !== actor.id);
|
||||
await a.update({
|
||||
'system.encounters': encounters
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Si un acteur est supprimé, le retirer de tous les groupes
|
||||
await this.removeActorFromAllGroups(actor.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Exporter pour utilisation globale
|
||||
export default GroupLink;
|
||||
@@ -204,6 +204,90 @@ export const registerHandlebarsHelpers = function () {
|
||||
}
|
||||
});
|
||||
|
||||
// return npc threat level information
|
||||
Handlebars.registerHelper('npcThreatLevel', function (property, level, options) {
|
||||
if (level < 1 || level > 4)
|
||||
return "";
|
||||
let levelData = CONFIG.VERMINE.npcThreatLevels[level];
|
||||
if (property == 'label') {
|
||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||
} else {
|
||||
return (levelData !== undefined) ? levelData[property] : "";
|
||||
}
|
||||
});
|
||||
|
||||
// return npc experience level information
|
||||
Handlebars.registerHelper('npcExperienceLevel', function (property, level, options) {
|
||||
if (level < 1 || level > 4)
|
||||
return "";
|
||||
let levelData = CONFIG.VERMINE.npcExperienceLevels[level];
|
||||
if (property == 'label') {
|
||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||
} else {
|
||||
return (levelData !== undefined) ? levelData[property] : "";
|
||||
}
|
||||
});
|
||||
|
||||
// return npc role level information
|
||||
Handlebars.registerHelper('npcRoleLevel', function (property, level, options) {
|
||||
if (level < 1 || level > 4)
|
||||
return "";
|
||||
let levelData = CONFIG.VERMINE.npcRoleLevels[level];
|
||||
if (property == 'label') {
|
||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||
} else {
|
||||
return (levelData !== undefined) ? levelData[property] : "";
|
||||
}
|
||||
});
|
||||
|
||||
// return creature pattern level information
|
||||
Handlebars.registerHelper('creaturePatternLevel', function (property, level, options) {
|
||||
if (level < 1 || level > 4)
|
||||
return "";
|
||||
let levelData = CONFIG.VERMINE.creaturePatternLevels[level];
|
||||
if (property == 'label') {
|
||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||
} else {
|
||||
return (levelData !== undefined) ? levelData[property] : "";
|
||||
}
|
||||
});
|
||||
|
||||
// return creature size level information
|
||||
Handlebars.registerHelper('creatureSizeLevel', function (property, level, options) {
|
||||
if (level < 1 || level > 3)
|
||||
return "";
|
||||
let levelData = CONFIG.VERMINE.creatureSizeLevels[level];
|
||||
if (property == 'label') {
|
||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||
} else {
|
||||
return (levelData !== undefined) ? levelData[property] : "";
|
||||
}
|
||||
});
|
||||
|
||||
// return creature role level information
|
||||
Handlebars.registerHelper('creatureRoleLevel', function (property, level, options) {
|
||||
if (level < 1 || level > 4)
|
||||
return "";
|
||||
let levelData = CONFIG.VERMINE.creatureRoleLevels[level];
|
||||
if (property == 'label') {
|
||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||
} else {
|
||||
return (levelData !== undefined) ? levelData[property] : "";
|
||||
}
|
||||
});
|
||||
|
||||
// return creature pack level information
|
||||
Handlebars.registerHelper('creaturePackLevel', function (property, level, options) {
|
||||
if (level < 0 || level > 3)
|
||||
return "";
|
||||
let levelData = CONFIG.VERMINE.creaturePackLevels[level];
|
||||
if (property == 'label') {
|
||||
return (levelData !== undefined) ? game.i18n.localize(levelData[property]) : "";
|
||||
} else {
|
||||
return (levelData !== undefined) ? levelData[property] : "";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// return skill level information
|
||||
Handlebars.registerHelper('skillLevel', function (property, level, options) {
|
||||
|
||||
+186
-8
@@ -2,34 +2,97 @@ export class VermineUtils {
|
||||
/**
|
||||
* Méthode pour effectuer un jet de dés avec différentes options
|
||||
* @param {Object} options - Les options du jet de dés
|
||||
* @param {Actor} options.actor - L'acteur qui lance les dés
|
||||
* @param {number} options.NoD - Nombre de dés de base
|
||||
* @param {number} [options.Reroll=0] - Nombre de relances autorisées
|
||||
* @param {number} [options.difficulty=7] - Difficulté du jet
|
||||
* @param {number} [options.self_control=0] - Sang-froid utilisé
|
||||
* @param {string} [options.rollLabel="jet custom"] - Libellé du jet
|
||||
* @param {Object} [options.totems={}] - Totems utilisés {human: false, adapted: false}
|
||||
* @param {number} [options.max_effort=0] - Effort maximum
|
||||
* @param {string} [options.skillCategory=null] - Catégorie de compétence pour les bonus de domaine
|
||||
* @param {string} [options.keepTotem=null] - Totem à garder ('human' ou 'adapted')
|
||||
* @param {number} [options.skillLevel=null] - Niveau de la compétence pour les réussites automatiques
|
||||
* @param {boolean} [options.hasSpecialty=false] - Si une spécialité est utilisée
|
||||
* @returns {Roll} - Le résultat du jet de dés
|
||||
*/
|
||||
static async roll({ actor, NoD, Reroll = 0, difficulty = 7, self_control = 0, rollLabel = "jet custom", totems = { human: false, adapted: false }, max_effort = 0 }) {
|
||||
static async roll({ actor, NoD, Reroll = 0, difficulty = 7, self_control = 0, rollLabel = "jet custom", totems = { human: false, adapted: false }, max_effort = 0, skillCategory = null, keepTotem = null, skillLevel = null, hasSpecialty = false }) {
|
||||
// Déclaration des variables
|
||||
let formula = "";
|
||||
let modFormula = null;
|
||||
let totemBonus = { human: 0, adapted: 0 };
|
||||
|
||||
// Calculer les bonus/malus par domaine de totem
|
||||
if (skillCategory) {
|
||||
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor);
|
||||
}
|
||||
|
||||
// Appliquer les réussites automatiques et seuils auto
|
||||
let autoSuccesses = 0;
|
||||
let adjustedDifficulty = difficulty;
|
||||
|
||||
if (skillLevel !== null && skillLevel !== undefined) {
|
||||
// Calculer les réussites automatiques
|
||||
autoSuccesses = this._calculateAutoSuccesses(skillLevel, hasSpecialty);
|
||||
|
||||
// Appliquer le seuil automatique si nécessaire
|
||||
const autoThreshold = this._getAutoThreshold(skillLevel);
|
||||
if (autoThreshold !== null) {
|
||||
adjustedDifficulty = autoThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérification des totems humains
|
||||
if (totems.human) {
|
||||
NoD--;
|
||||
modFormula = "(1D10cs>=" + difficulty + `[human_${game.user.name}]*2)`;
|
||||
|
||||
const humanDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty;
|
||||
const humanFormula = "(1D10cs>=" + humanDifficulty + `[human_${game.user.name}]*2)`;
|
||||
|
||||
// Appliquer bonus/malus de domaine
|
||||
if (totemBonus.human !== 0) {
|
||||
// Si bonus, ajouter un dé supplémentaire, si malus, réduire le pool
|
||||
NoD += totemBonus.human;
|
||||
}
|
||||
|
||||
modFormula = humanFormula;
|
||||
}
|
||||
|
||||
// Vérification des totems adaptés
|
||||
if (totems.adapted) {
|
||||
NoD--;
|
||||
const adaptedDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty;
|
||||
const adaptedFormula = "(1D10cs>=" + adaptedDifficulty + `[adapted_${game.user.name}]*2)`;
|
||||
|
||||
// Appliquer bonus/malus de domaine
|
||||
if (totemBonus.adapted !== 0) {
|
||||
NoD += totemBonus.adapted;
|
||||
}
|
||||
|
||||
// Construction de la formule modifiée
|
||||
if (modFormula != null) {
|
||||
modFormula = modFormula + "+(1D10cs>=" + difficulty + `[adapted_${game.user.name}]*2)`;
|
||||
modFormula = modFormula + "+" + adaptedFormula;
|
||||
} else {
|
||||
modFormula = "(1D10cs>=" + difficulty + `[adapted_${game.user.name}]*2)`;
|
||||
modFormula = adaptedFormula;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Gestion du choix de totem à garder (si les deux sont activés)
|
||||
if (totems.human && totems.adapted && keepTotem) {
|
||||
// Si on veut garder un seul totem, ne pas doubler le bonus
|
||||
if (keepTotem === 'human' && totems.adapted) {
|
||||
// Retirer le totem adapté du calcul
|
||||
modFormula = "(1D10cs>=" + adjustedDifficulty + `[human_${game.user.name}]*2)`;
|
||||
NoD++; // On avait décrémenté pour adapted, on annule
|
||||
} else if (keepTotem === 'adapted' && totems.human) {
|
||||
// Retirer le totem humain du calcul
|
||||
modFormula = "(1D10cs>=" + adjustedDifficulty + `[adapted_${game.user.name}]*2)`;
|
||||
NoD++; // On avait décrémenté pour human, on annule
|
||||
}
|
||||
}
|
||||
|
||||
// Construction de la formule de base
|
||||
let baseFormula = '' + NoD + "d10";
|
||||
baseFormula += (difficulty != undefined) ? "cs>=" + difficulty : "cs>=7";
|
||||
baseFormula += (adjustedDifficulty != undefined) ? "cs>=" + adjustedDifficulty : "cs>=7";
|
||||
baseFormula += `[regular_${game.user.name}]`
|
||||
|
||||
// Construction de la formule finale
|
||||
@@ -39,14 +102,129 @@ export class VermineUtils {
|
||||
|
||||
// Création du jet de dés
|
||||
let roll = new Roll(formula, actor.getRollData());
|
||||
|
||||
// Stocker les métadonnées du roll pour l'affichage
|
||||
roll.vermineData = {
|
||||
totemsUsed: { ...totems },
|
||||
keepTotem: keepTotem,
|
||||
difficulty: adjustedDifficulty,
|
||||
originalDifficulty: difficulty,
|
||||
skillCategory: skillCategory,
|
||||
skillLevel: skillLevel,
|
||||
hasSpecialty: hasSpecialty,
|
||||
autoSuccesses: autoSuccesses,
|
||||
totemBonuses: { ...totemBonus },
|
||||
baseNoD: NoD,
|
||||
rerolls: Reroll,
|
||||
selfControl: self_control
|
||||
};
|
||||
|
||||
//effectuer le lancé
|
||||
await roll.evaluate();
|
||||
//afficher le lancer 3d
|
||||
await VermineUtils.showDiceSoNice(roll);
|
||||
// afficher le résultat dans le chat
|
||||
VermineUtils.diplayChatRoll(roll, ...arguments);
|
||||
VermineUtils.diplayChatRoll(roll, { actor, NoD, Reroll, difficulty, self_control, rollLabel, totems, max_effort, skillCategory, keepTotem, skillLevel, hasSpecialty });
|
||||
return roll;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les bonus/malus par domaine de totem
|
||||
* @param {string} skillCategory - Catégorie de la compétence
|
||||
* @param {Actor} actor - L'acteur
|
||||
* @returns {Object} - Bonus pour chaque totem {human: number, adapted: number}
|
||||
*/
|
||||
static _calculateTotemDomainBonuses(skillCategory, actor) {
|
||||
const bonuses = { human: 0, adapted: 0 };
|
||||
|
||||
if (!CONFIG.VERMINE?.totemDomains || !actor?.system?.identity?.totem) {
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
const actorTotem = actor.system.identity.totem;
|
||||
const totemConfig = CONFIG.VERMINE.totemDomains[actorTotem];
|
||||
|
||||
if (!totemConfig || !totemConfig.domains) {
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
// Vérifier si la catégorie de compétence est dans les domaines du totem
|
||||
const preferredCategory = actor.system.skill_categories?.preferred;
|
||||
|
||||
// Bonus pour le totem de l'acteur
|
||||
if (preferredCategory && totemConfig.domains.includes(preferredCategory)) {
|
||||
// Le domaine de prédilection est dans les domaines du totem
|
||||
bonuses[actorTotem] = totemConfig.bonus || 1;
|
||||
}
|
||||
|
||||
// Malus pour le totem opposé
|
||||
const oppositeTotem = CONFIG.VERMINE.totem_opposites?.[actorTotem];
|
||||
if (oppositeTotem && preferredCategory) {
|
||||
const oppositeConfig = CONFIG.VERMINE.totemDomains[oppositeTotem];
|
||||
if (oppositeConfig?.domains?.includes(preferredCategory)) {
|
||||
bonuses[oppositeTotem] = -(oppositeConfig.bonus || 1);
|
||||
}
|
||||
}
|
||||
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les réussites automatiques basées sur la maîtrise de la compétence
|
||||
* @param {number} skillLevel - Niveau de la compétence (0-5)
|
||||
* @param {boolean} hasSpecialty - Si une spécialité est utilisée
|
||||
* @returns {number} - Nombre de réussites automatiques
|
||||
*/
|
||||
static _calculateAutoSuccesses(skillLevel, hasSpecialty = false) {
|
||||
// Selon les règles de Vermine2047, les réussites automatiques sont basées sur le niveau de maîtrise
|
||||
// Niveau 0 (Incompétent): 0 réussite automatique
|
||||
// Niveau 1 (Débutant): 0 réussite automatique
|
||||
// Niveau 2 (Compétent): 1 réussite automatique si spécialité utilisée
|
||||
// Niveau 3 (Expert): 1 réussite automatique
|
||||
// Niveau 4 (Maître): 1 réussite automatique + 1 si spécialité utilisée
|
||||
// Niveau 5 (Légende): 2 réussites automatiques
|
||||
|
||||
if (!skillLevel) return 0;
|
||||
|
||||
let autoSuccesses = 0;
|
||||
|
||||
switch (skillLevel) {
|
||||
case 2: // Compétent
|
||||
if (hasSpecialty) autoSuccesses = 1;
|
||||
break;
|
||||
case 3: // Expert
|
||||
autoSuccesses = 1;
|
||||
break;
|
||||
case 4: // Maître
|
||||
autoSuccesses = 1;
|
||||
if (hasSpecialty) autoSuccesses += 1;
|
||||
break;
|
||||
case 5: // Légende
|
||||
autoSuccesses = 2;
|
||||
break;
|
||||
default:
|
||||
autoSuccesses = 0;
|
||||
}
|
||||
|
||||
return autoSuccesses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine le seuil automatique si la compétence n'est pas maîtrisée
|
||||
* @param {number} skillLevel - Niveau de la compétence
|
||||
* @returns {number|null} - Seuil automatique ou null si la compétence est maîtrisée
|
||||
*/
|
||||
static _getAutoThreshold(skillLevel) {
|
||||
// Si la compétence n'est pas maîtrisée (niveau 0 ou 1), utiliser un seuil par défaut
|
||||
// Niveau 0 (Incompétent): seuil = 9 (très difficile)
|
||||
// Niveau 1 (Débutant): seuil = 7 (difficile)
|
||||
// Niveau >= 2: null (utiliser le seuil normal)
|
||||
|
||||
if (skillLevel === 0) return 9; // Très difficile
|
||||
if (skillLevel === 1) return 7; // Difficile
|
||||
|
||||
return null; // Utiliser le seuil normal
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode pour gérer les événements de relance de dés
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { registerHooks } from "./system/hooks.mjs";
|
||||
import { registerSettings } from "./system/settings.mjs";
|
||||
import { GroupLink } from "./system/group-link.mjs";
|
||||
|
||||
// Import document classes.
|
||||
import { VermineActor } from "./documents/actor.mjs";
|
||||
@@ -31,8 +32,12 @@ Hooks.once('init', async function () {
|
||||
VermineActor,
|
||||
VermineItem,
|
||||
VermineUtils,
|
||||
VermineCombat
|
||||
VermineCombat,
|
||||
GroupLink
|
||||
};
|
||||
|
||||
// Register GroupLink hooks for automatic synchronization
|
||||
GroupLink.registerHooks();
|
||||
|
||||
// Define custom Document classes
|
||||
CONFIG.Actor.documentClass = VermineActor;
|
||||
|
||||
Reference in New Issue
Block a user