|
|
|
@@ -1,596 +1,327 @@
|
|
|
|
|
import { VermineUtils } from "../roll.mjs";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Dialog for rolling dice in Vermine2047.
|
|
|
|
|
* Handles dice pool calculation, modifiers, and roll execution.
|
|
|
|
|
*/
|
|
|
|
|
export default class RollDialog extends Dialog {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new RollDialog instance.
|
|
|
|
|
* @param {Object} data - The data for the dialog
|
|
|
|
|
* @param {HTMLElement} html - The HTML content of the dialog
|
|
|
|
|
* @param {Object} options - The options for the dialog
|
|
|
|
|
* @param {Function} [close] - The callback function for closing the dialog
|
|
|
|
|
*/
|
|
|
|
|
constructor(data, html, options, close = undefined) {
|
|
|
|
|
const conf = {
|
|
|
|
|
title: "jet de dés",
|
|
|
|
|
content: html,
|
|
|
|
|
buttons: {
|
|
|
|
|
roll: {
|
|
|
|
|
icon: '<i class="fas fa-check"></i>',
|
|
|
|
|
label: "Lancer !",
|
|
|
|
|
callback: () => this._onRoll()
|
|
|
|
|
},
|
|
|
|
|
cancel: {
|
|
|
|
|
icon: '<i class="fas fa-times"></i>',
|
|
|
|
|
label: "Annuler",
|
|
|
|
|
callback: () => this.close()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
close: close
|
|
|
|
|
};
|
|
|
|
|
super({ ...conf, ...data }, options);
|
|
|
|
|
// Store reference to close callback
|
|
|
|
|
this._closeCallback = close;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new RollDialog instance.
|
|
|
|
|
* @param {Object} [data] - The data for the dialog
|
|
|
|
|
* @param {string} [data.label] - Roll label
|
|
|
|
|
* @param {string} [data.rolltype] - Roll type
|
|
|
|
|
* @param {number} [data.NoD=1] - Number of dice
|
|
|
|
|
* @param {boolean} [data.Reroll=false] - Allow rerolls
|
|
|
|
|
* @param {string} [data.actorId] - Actor ID for the roll
|
|
|
|
|
* @returns {Promise<RollDialog|null>} The RollDialog instance or null if creation failed
|
|
|
|
|
*/
|
|
|
|
|
static async create(data = {
|
|
|
|
|
label: null,
|
|
|
|
|
rolltype: null,
|
|
|
|
|
NoD: 1,
|
|
|
|
|
Reroll: false,
|
|
|
|
|
actorId: game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id
|
|
|
|
|
}) {
|
|
|
|
|
// Validate actorId
|
|
|
|
|
const actorId = data.actorId;
|
|
|
|
|
if (!actorId || typeof actorId !== 'string') {
|
|
|
|
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Retrieve the actor data based on the actorId
|
|
|
|
|
data.actor = await game.actors.get(actorId);
|
|
|
|
|
if (!data.actor) {
|
|
|
|
|
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data.availableSpecialties = data.actor.items.filter(item => item.type === "specialty");
|
|
|
|
|
data.availableItems = data.actor.items.filter(item => item.type === "item");
|
|
|
|
|
data.config = CONFIG.VERMINE;
|
|
|
|
|
|
|
|
|
|
// Define options for the dialog
|
|
|
|
|
const options = {
|
|
|
|
|
classes: ["vermineDialog"],
|
|
|
|
|
width: "fit-content",
|
|
|
|
|
height: 'fit-content',
|
|
|
|
|
zIndex: 99999
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Render the HTML template for the dialog
|
|
|
|
|
const html = await renderTemplate('systems/vermine2047/templates/dialogs/roll-dialog.hbs', data);
|
|
|
|
|
|
|
|
|
|
// Return a new RollDialog instance with the provided data, HTML, and options
|
|
|
|
|
return new RollDialog(data, html, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the default options for the RollDialog.
|
|
|
|
|
*/
|
|
|
|
|
static get defaultOptions() {
|
|
|
|
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
|
|
|
|
focus: true,
|
|
|
|
|
classes: ["dialog vermine-roll"],
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the data for the dialog.
|
|
|
|
|
* @returns {Object} The context data for the dialog
|
|
|
|
|
*/
|
|
|
|
|
getData() {
|
|
|
|
|
// Get the context data from the superclass
|
|
|
|
|
const context = super.getData();
|
|
|
|
|
context.data = this.data;
|
|
|
|
|
context.config = CONFIG.VERMINE;
|
|
|
|
|
|
|
|
|
|
return context;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prepares items for display.
|
|
|
|
|
* @returns {Array} Filtered list of items
|
|
|
|
|
*/
|
|
|
|
|
prepareItems() {
|
|
|
|
|
return this.data.actor.items.filter(it => it.type === "item");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prepares specialties for display.
|
|
|
|
|
* @returns {Array} Filtered list of specialties
|
|
|
|
|
*/
|
|
|
|
|
prepareSpecialties() {
|
|
|
|
|
return this.data.actor.items.filter(it => it.type === "specialty");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Activates event listeners for the dialog.
|
|
|
|
|
* @param {HTMLElement} html - The HTML element of the 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();
|
|
|
|
|
|
|
|
|
|
// Set up event listeners for all roll-related inputs
|
|
|
|
|
const rollInputs = html.find('[data-roll]');
|
|
|
|
|
for (const inp of rollInputs) {
|
|
|
|
|
inp.addEventListener('change', this._onRollInputChange.bind(this));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.displaySpecialties();
|
|
|
|
|
|
|
|
|
|
const selectAbil = html.find('#ability')[0];
|
|
|
|
|
// Set the maximum value for self control based on ability value
|
|
|
|
|
html.find("#self_control")[0].max = selectAbil.value;
|
|
|
|
|
selectAbil.addEventListener('change', this._onChangeAbility.bind(this));
|
|
|
|
|
const 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();
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the roll data for the dialog.
|
|
|
|
|
* @param {Event} _ev - The event triggering the roll data retrieval (unused).
|
|
|
|
|
*/
|
|
|
|
|
getRollData(_ev) {
|
|
|
|
|
// Calculate and store the roll data
|
|
|
|
|
this.rollData = {
|
|
|
|
|
actor: this.data.actor,
|
|
|
|
|
NoD: this.getDicePool(),
|
|
|
|
|
Reroll: this.getReroll(),
|
|
|
|
|
difficulty: this.getDifficulty(),
|
|
|
|
|
handicap: this.getHandicap(),
|
|
|
|
|
rollType: this.getRollType(),
|
|
|
|
|
rollLabel: this.getLabel(),
|
|
|
|
|
totems: this.getTotems(),
|
|
|
|
|
self_control: this.getSelfControl(),
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
_onRollInputChange(ev) {
|
|
|
|
|
this.getRollData(ev);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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, 10) || 0;
|
|
|
|
|
bonus += groupValue;
|
|
|
|
|
|
|
|
|
|
// Self control bonus
|
|
|
|
|
const selfControlValue = parseInt(this._html?.find('#self_control')[0]?.value, 10) || 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, 10) || 0;
|
|
|
|
|
}
|
|
|
|
|
if (this._html?.find('#adapted-totem')[0]?.checked) {
|
|
|
|
|
bonus += parseInt(this.data.actor?.system?.adaptation?.totems?.adapted?.value, 10) || 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles the change in self control value.
|
|
|
|
|
* @param {Event} ev - The event triggering the change in self control value.
|
|
|
|
|
*/
|
|
|
|
|
_onChangeSelfControl(ev) {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
const selfControlValueElement = html.querySelector('#self_control_value');
|
|
|
|
|
if (selfControlValueElement) {
|
|
|
|
|
selfControlValueElement.innerText = ev.currentTarget.value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the handicap value from the HTML element.
|
|
|
|
|
* @returns {number} The handicap value.
|
|
|
|
|
*/
|
|
|
|
|
getHandicap() {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
const handicapValue = html.querySelector('#handicap')?.value ?? '1';
|
|
|
|
|
return parseInt(handicapValue, 10);
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Gets the roll type (ability or skill).
|
|
|
|
|
* @returns {string} The roll type: 'skill' or 'ability'.
|
|
|
|
|
*/
|
|
|
|
|
getRollType() {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
return html.querySelector('select#skill')?.value ? "skill" : "ability";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets the label for the roll.
|
|
|
|
|
* @returns {string} The roll label.
|
|
|
|
|
*/
|
|
|
|
|
getLabel() {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
const rollType = this.getRollType();
|
|
|
|
|
|
|
|
|
|
if (rollType === "skill") {
|
|
|
|
|
const skillSelect = html.querySelector('select#skill');
|
|
|
|
|
const selectedIndex = skillSelect?.selectedIndex ?? 0;
|
|
|
|
|
return skillSelect?.options[selectedIndex]?.dataset?.label ?? "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const abilitySelect = html.querySelector('select#ability');
|
|
|
|
|
const selectedIndex = abilitySelect?.selectedIndex ?? 0;
|
|
|
|
|
return abilitySelect?.options[selectedIndex]?.dataset?.label ?? "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Displays specialties related to the selected skill.
|
|
|
|
|
*/
|
|
|
|
|
displaySpecialties() {
|
|
|
|
|
const specialties = this.element[0]?.querySelectorAll('[data-spec-skill]');
|
|
|
|
|
if (specialties) {
|
|
|
|
|
specialties.forEach(specEl => {
|
|
|
|
|
specEl.style.display = "inline";
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the self control value from the HTML element.
|
|
|
|
|
* @returns {number} The self control value.
|
|
|
|
|
*/
|
|
|
|
|
getSelfControl() {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
const selfControlValue = html.querySelector('#self_control')?.value ?? '0';
|
|
|
|
|
return parseInt(selfControlValue, 10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the maximum effort value from the HTML element.
|
|
|
|
|
* @returns {number} The maximum effort value.
|
|
|
|
|
*/
|
|
|
|
|
getMaxEffort() {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
const abilityValue = html.querySelector('#ability')?.value ?? '0';
|
|
|
|
|
return parseInt(abilityValue, 10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the selected totems from the HTML element.
|
|
|
|
|
* @returns {Object} An object containing the selected totems {human: boolean, adapted: boolean}.
|
|
|
|
|
*/
|
|
|
|
|
getTotems() {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
return {
|
|
|
|
|
human: html.querySelector('#human-totem')?.checked ?? false,
|
|
|
|
|
adapted: html.querySelector('#adapted-totem')?.checked ?? false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles the change in ability value.
|
|
|
|
|
* @param {Event} ev - The event triggering the change in ability value.
|
|
|
|
|
*/
|
|
|
|
|
_onChangeAbility(ev) {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
const abilitySelect = html.querySelector('#ability');
|
|
|
|
|
const selectedIndex = abilitySelect?.selectedIndex ?? 0;
|
|
|
|
|
const score = abilitySelect?.options[selectedIndex]?.value ?? '0';
|
|
|
|
|
|
|
|
|
|
const scoreElement = html.querySelector('#abilityScore');
|
|
|
|
|
if (scoreElement) {
|
|
|
|
|
scoreElement.value = score;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selfControlElement = html.querySelector('#self_control');
|
|
|
|
|
if (selfControlElement) {
|
|
|
|
|
selfControlElement.max = score;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the total dice pool based on various factors.
|
|
|
|
|
* @returns {number} The total dice pool value.
|
|
|
|
|
*/
|
|
|
|
|
getDicePool() {
|
|
|
|
|
// Retrieve the HTML element
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
|
|
|
|
|
// Safely get ability value
|
|
|
|
|
const abilitySelect = html.querySelector('#ability');
|
|
|
|
|
const abilValue = abilitySelect?.options[abilitySelect?.selectedIndex]?.value ?? 0;
|
|
|
|
|
|
|
|
|
|
// Safely get skill value and pool
|
|
|
|
|
const skillSelect = html.querySelector('#skill');
|
|
|
|
|
const skillOption = skillSelect?.options[skillSelect?.selectedIndex];
|
|
|
|
|
const skillValue = skillOption?.dataset?.pool ?? 0;
|
|
|
|
|
|
|
|
|
|
// Get the self control value
|
|
|
|
|
const selfControl = html.querySelector('#self_control')?.value ?? 0;
|
|
|
|
|
|
|
|
|
|
// Calculate bonuses based on certain conditions
|
|
|
|
|
const bonuses =
|
|
|
|
|
(html.querySelector('#usingSpecialization')?.checked ? 1 : 0) +
|
|
|
|
|
(html.querySelector('#helped')?.checked ? 1 : 0) +
|
|
|
|
|
(html.querySelector('#usingTools')?.checked ? 1 : 0);
|
|
|
|
|
|
|
|
|
|
// Calculate the total dice pool
|
|
|
|
|
const total = parseInt(abilValue, 10) + parseInt(selfControl, 10) + parseInt(skillValue, 10) + bonuses;
|
|
|
|
|
return total || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the reroll value based on selected skill.
|
|
|
|
|
* @returns {number} The reroll value.
|
|
|
|
|
*/
|
|
|
|
|
getReroll() {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
const skillSelect = html.querySelector('#skill');
|
|
|
|
|
const selectedIndex = skillSelect?.selectedIndex ?? 0;
|
|
|
|
|
const rerollValue = skillSelect?.options[selectedIndex]?.dataset?.reroll ?? '0';
|
|
|
|
|
return parseInt(rerollValue, 10) || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the difficulty value based on selected option.
|
|
|
|
|
* @returns {number} The difficulty value.
|
|
|
|
|
*/
|
|
|
|
|
getDifficulty() {
|
|
|
|
|
const html = this.element[0];
|
|
|
|
|
const difficultySelect = html.querySelector('#difficulty');
|
|
|
|
|
const selectedIndex = difficultySelect?.selectedIndex ?? 0;
|
|
|
|
|
const diffValue = difficultySelect?.options[selectedIndex]?.value ?? '0';
|
|
|
|
|
return parseInt(diffValue, 10) || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Performs a dice roll based on the roll data and handles self control checks.
|
|
|
|
|
* @returns {Promise<Roll|false>} A promise that resolves with the Roll result or false if cancelled.
|
|
|
|
|
*/
|
|
|
|
|
async _onRoll() {
|
|
|
|
|
// Check if self control is required for the roll
|
|
|
|
|
if (this.rollData.self_control > 0) {
|
|
|
|
|
// Check if the actor has enough self control
|
|
|
|
|
const currentSelfControl = this.rollData.actor?.system?.attributes?.self_control?.value ?? 0;
|
|
|
|
|
if (currentSelfControl < this.rollData.self_control) {
|
|
|
|
|
// Display a warning message if self control is insufficient
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const caracName = this.element[0]?.querySelector('[name="ability"]')?.value;
|
|
|
|
|
if (caracName === "0" || caracName === undefined) {
|
|
|
|
|
// Display a warning message if no ability selected
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Deduct self control points if necessary
|
|
|
|
|
if (this.rollData.self_control > 0) {
|
|
|
|
|
const newSelfControl = this.rollData.actor.system.attributes.self_control.value - this.rollData.self_control;
|
|
|
|
|
// Update the actor's self control value
|
|
|
|
|
await this.rollData.actor.update({
|
|
|
|
|
"system.attributes.self_control.value": newSelfControl
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Perform the dice roll using VermineUtils
|
|
|
|
|
return VermineUtils.roll({
|
|
|
|
|
...this.rollData,
|
|
|
|
|
skillLevel: this.getSkillLevel(),
|
|
|
|
|
hasSpecialty: this.hasSpecialtySelected()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
import { VermineUtils } from "../roll.mjs";
|
|
|
|
|
|
|
|
|
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
|
|
|
|
|
|
|
|
|
export default class RollDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
|
|
|
|
|
|
|
|
|
#actor;
|
|
|
|
|
|
|
|
|
|
get title() {
|
|
|
|
|
return game.i18n.localize("VERMINE.roll");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DEFAULT_OPTIONS = {
|
|
|
|
|
classes: ["vermine-roll"],
|
|
|
|
|
tag: "form",
|
|
|
|
|
window: {
|
|
|
|
|
icon: "fas fa-dice-d10",
|
|
|
|
|
resizable: false
|
|
|
|
|
},
|
|
|
|
|
position: {
|
|
|
|
|
width: 520,
|
|
|
|
|
height: 600
|
|
|
|
|
},
|
|
|
|
|
actions: {
|
|
|
|
|
roll: RollDialog.#onRoll,
|
|
|
|
|
cancel: RollDialog.#onCancel
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static PARTS = {
|
|
|
|
|
main: { template: "systems/vermine2047/templates/dialogs/roll-dialog.hbs" }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static async create(data = {}) {
|
|
|
|
|
const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id;
|
|
|
|
|
if (!actorId || typeof actorId !== "string") {
|
|
|
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const actor = await game.actors.get(actorId);
|
|
|
|
|
if (!actor) {
|
|
|
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return new RollDialog({ actor, label: data.label, rolltype: data.rolltype });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constructor(options = {}) {
|
|
|
|
|
super(options);
|
|
|
|
|
this.#actor = options.actor;
|
|
|
|
|
this.label = options.label ?? null;
|
|
|
|
|
this.rolltype = options.rolltype ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async _prepareContext() {
|
|
|
|
|
const actor = this.#actor;
|
|
|
|
|
return {
|
|
|
|
|
actor,
|
|
|
|
|
system: actor.system,
|
|
|
|
|
config: CONFIG.VERMINE,
|
|
|
|
|
label: this.label,
|
|
|
|
|
rollType: this.rolltype,
|
|
|
|
|
labelKey: this.label,
|
|
|
|
|
speakerId: actor.id,
|
|
|
|
|
ability: null,
|
|
|
|
|
help: false,
|
|
|
|
|
specialty: false,
|
|
|
|
|
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
|
|
|
|
availableItems: actor.items.filter(i => i.type === "item")
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async _onRender(context, options) {
|
|
|
|
|
this.element.dataset.actorId = this.#actor.id;
|
|
|
|
|
|
|
|
|
|
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
|
|
|
|
inp.addEventListener("change", this.#onInputChange.bind(this));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ability = this.element.querySelector("#ability");
|
|
|
|
|
if (ability) {
|
|
|
|
|
ability.addEventListener("change", this.#onChangeAbility.bind(this));
|
|
|
|
|
const selfControl = this.element.querySelector("#self_control");
|
|
|
|
|
if (selfControl) selfControl.max = ability.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selfControl = this.element.querySelector("#self_control");
|
|
|
|
|
if (selfControl) {
|
|
|
|
|
selfControl.addEventListener("change", this.#onChangeSelfControl.bind(this));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.element.querySelector("#difficulty")?.addEventListener("change", () => this.#updateUI());
|
|
|
|
|
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI());
|
|
|
|
|
this.element.querySelector("#human-totem")?.addEventListener("change", () => this.#updateUI());
|
|
|
|
|
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
|
|
|
|
|
|
|
|
|
|
this.#displaySpecialties();
|
|
|
|
|
this.#updateUI();
|
|
|
|
|
|
|
|
|
|
if (ability?.value !== "0") {
|
|
|
|
|
this.element.querySelector("#self_control")?.dispatchEvent(new Event("change"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Getters ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
get #el() { return this.element; }
|
|
|
|
|
|
|
|
|
|
#getAbility() { return this.#el.querySelector("#ability"); }
|
|
|
|
|
#getSkill() { return this.#el.querySelector("#skill"); }
|
|
|
|
|
#getDifficulty() { return this.#el.querySelector("#difficulty"); }
|
|
|
|
|
#getHandicap() { return this.#el.querySelector("#handicap"); }
|
|
|
|
|
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
|
|
|
|
|
|
|
|
|
|
getDicePool() {
|
|
|
|
|
const abil = this.#getAbility();
|
|
|
|
|
const abilVal = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
|
|
|
|
|
const skill = this.#getSkill();
|
|
|
|
|
const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0;
|
|
|
|
|
const sc = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
|
|
|
|
const specChecked = this.#el.querySelector("#usingSpecialization")?.checked;
|
|
|
|
|
const helped = this.#el.querySelector("#helped")?.checked;
|
|
|
|
|
const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0";
|
|
|
|
|
const bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0);
|
|
|
|
|
return (abilVal + sc + skillPool + bonuses) || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getDifficultySelect() {
|
|
|
|
|
const sel = this.#getDifficulty();
|
|
|
|
|
const idx = sel?.selectedIndex ?? 0;
|
|
|
|
|
return parseInt(sel?.options[idx]?.value, 10) || 7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getReroll() {
|
|
|
|
|
const sel = this.#getSkill();
|
|
|
|
|
const idx = sel?.selectedIndex ?? 0;
|
|
|
|
|
return parseInt(sel?.options[idx]?.dataset?.reroll, 10) || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getHandicapSelect() {
|
|
|
|
|
const sel = this.#getHandicap();
|
|
|
|
|
return parseInt(sel?.value, 10) || 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSkillCategory() {
|
|
|
|
|
const sel = this.#getSkill();
|
|
|
|
|
const idx = sel?.selectedIndex ?? 0;
|
|
|
|
|
return sel?.options[idx]?.dataset?.category ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSkillLevel() {
|
|
|
|
|
const sel = this.#getSkill();
|
|
|
|
|
const idx = sel?.selectedIndex ?? 0;
|
|
|
|
|
const val = sel?.options[idx]?.value;
|
|
|
|
|
return val ? parseInt(val, 10) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasSpecialtySelected() {
|
|
|
|
|
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
|
|
|
|
return checked && checked.value !== "aucune";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getRollType() {
|
|
|
|
|
const sel = this.#getSkill();
|
|
|
|
|
return sel?.value ? "skill" : "ability";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getLabel() {
|
|
|
|
|
const type = this.getRollType();
|
|
|
|
|
if (type === "skill") {
|
|
|
|
|
const sel = this.#getSkill();
|
|
|
|
|
const idx = sel?.selectedIndex ?? 0;
|
|
|
|
|
return sel?.options[idx]?.dataset?.label ?? "";
|
|
|
|
|
}
|
|
|
|
|
const sel = this.#getAbility();
|
|
|
|
|
const idx = sel?.selectedIndex ?? 0;
|
|
|
|
|
return sel?.options[idx]?.dataset?.label ?? "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSelfControl() {
|
|
|
|
|
return parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMaxEffort() {
|
|
|
|
|
const sel = this.#getAbility();
|
|
|
|
|
return parseInt(sel?.value, 10) || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getTotems() {
|
|
|
|
|
return {
|
|
|
|
|
human: this.#el.querySelector("#human-totem")?.checked ?? false,
|
|
|
|
|
adapted: this.#el.querySelector("#adapted-totem")?.checked ?? false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getKeepTotem() {
|
|
|
|
|
return this.#el.querySelector("#keep-totem-select")?.value ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── UI ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#displaySpecialties() {
|
|
|
|
|
for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
|
|
|
|
|
el.style.display = "inline";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#calculateBonusCount() {
|
|
|
|
|
let b = 0;
|
|
|
|
|
if (this.#el.querySelector("#helped")?.checked) b += 1;
|
|
|
|
|
b += parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
|
|
|
|
b += parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
|
|
|
|
const tools = this.#el.querySelector("input[name='usingTools']:checked");
|
|
|
|
|
if (tools && tools.value !== "0") b += 1;
|
|
|
|
|
const human = this.#el.querySelector("#human-totem");
|
|
|
|
|
if (human?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.human?.value, 10) || 0;
|
|
|
|
|
const adapted = this.#el.querySelector("#adapted-totem");
|
|
|
|
|
if (adapted?.checked) b += parseInt(this.#actor?.system?.adaptation?.totems?.adapted?.value, 10) || 0;
|
|
|
|
|
if (this.hasSpecialtySelected()) b += 1;
|
|
|
|
|
return b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#updateUI() {
|
|
|
|
|
const total = this.getDicePool();
|
|
|
|
|
const totalEl = this.#el.querySelector("#dice-pool-total");
|
|
|
|
|
if (totalEl) totalEl.textContent = `${total}D`;
|
|
|
|
|
|
|
|
|
|
const bonusEl = this.#el.querySelector("#total-bonus");
|
|
|
|
|
if (bonusEl) bonusEl.textContent = this.#calculateBonusCount();
|
|
|
|
|
|
|
|
|
|
const diffSel = this.#getDifficulty();
|
|
|
|
|
const diffEl = this.#el.querySelector("#current-difficulty");
|
|
|
|
|
if (diffEl && diffSel) {
|
|
|
|
|
const idx = diffSel.selectedIndex;
|
|
|
|
|
const val = diffSel.options[idx].value;
|
|
|
|
|
const lbl = diffSel.options[idx].text.split(" ")[0];
|
|
|
|
|
diffEl.textContent = `${lbl} (${val})`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handSel = this.#getHandicap();
|
|
|
|
|
const handEl = this.#el.querySelector("#current-handicap");
|
|
|
|
|
if (handEl && handSel) {
|
|
|
|
|
handEl.textContent = handSel.options[handSel.selectedIndex].text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const abilSel = this.#getAbility();
|
|
|
|
|
const abilValEl = this.#el.querySelector("#abilityScoreValue");
|
|
|
|
|
if (abilSel && abilValEl) {
|
|
|
|
|
const idx = abilSel.selectedIndex;
|
|
|
|
|
abilValEl.textContent = idx > 0 ? abilSel.options[idx].value : "0";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const specChecked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
|
|
|
|
const specEl = this.#el.querySelector(".current-specialty");
|
|
|
|
|
if (specEl && specChecked) {
|
|
|
|
|
specEl.textContent = specChecked.value === "aucune"
|
|
|
|
|
? game.i18n.localize("VERMINE.none")
|
|
|
|
|
: specChecked.value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Event handlers ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#onInputChange() {
|
|
|
|
|
this.#updateUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#onChangeAbility(ev) {
|
|
|
|
|
const sel = ev.currentTarget;
|
|
|
|
|
const score = sel.options[sel.selectedIndex]?.value ?? "0";
|
|
|
|
|
const scoreEl = this.#el.querySelector("#abilityScore");
|
|
|
|
|
if (scoreEl) scoreEl.value = score;
|
|
|
|
|
const sc = this.#getSelfCtrl();
|
|
|
|
|
if (sc) sc.max = score;
|
|
|
|
|
this.#updateUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#onChangeSelfControl(ev) {
|
|
|
|
|
const valEl = this.#el.querySelector("#self_control_value");
|
|
|
|
|
if (valEl) valEl.textContent = ev.currentTarget.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async #onCancel(event, target) {
|
|
|
|
|
this.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async #onRoll(event, target) {
|
|
|
|
|
const selfCtrl = this.getSelfControl();
|
|
|
|
|
if (selfCtrl > 0) {
|
|
|
|
|
const current = this.#actor?.system?.attributes?.self_control?.value ?? 0;
|
|
|
|
|
if (current < selfCtrl) {
|
|
|
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_not_enough_self_control"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const abilityVal = this.#el.querySelector('[name="ability"]')?.value;
|
|
|
|
|
if (!abilityVal || abilityVal === "0") {
|
|
|
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_select_ability"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selfCtrl > 0) {
|
|
|
|
|
const newVal = this.#actor.system.attributes.self_control.value - selfCtrl;
|
|
|
|
|
await this.#actor.update({ "system.attributes.self_control.value": newVal });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await VermineUtils.roll({
|
|
|
|
|
actor: this.#actor,
|
|
|
|
|
NoD: this.getDicePool(),
|
|
|
|
|
Reroll: this.getReroll(),
|
|
|
|
|
difficulty: this.getDifficultySelect(),
|
|
|
|
|
handicap: this.getHandicapSelect(),
|
|
|
|
|
rollLabel: this.getLabel(),
|
|
|
|
|
totems: this.getTotems(),
|
|
|
|
|
self_control: selfCtrl,
|
|
|
|
|
max_effort: this.getMaxEffort(),
|
|
|
|
|
keepTotem: this.getKeepTotem(),
|
|
|
|
|
skillCategory: this.getSkillCategory(),
|
|
|
|
|
skillLevel: this.getSkillLevel(),
|
|
|
|
|
hasSpecialty: this.hasSpecialtySelected()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|