Release Creation / build (release) Failing after 1m20s
- phase d'expérience (récompenses collectives et individuelles, objectifs, traumatisme) - jets d'apprentissage (compétences, spécialités, caractéristiques, réserves, évolution) - bonus de réserve acquis par apprentissage intégré au max dérivé - transferts de dés individuels <-> groupe dans le dialogue de jet - onglet expérience sur la fiche personnage - guide utilisateur combat (docs/user/COMBAT.md)
476 lines
16 KiB
JavaScript
476 lines
16 KiB
JavaScript
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,
|
|
giveToGroup: RollDialog.#onGiveToGroup
|
|
}
|
|
};
|
|
|
|
static PARTS = {
|
|
main: { template: "systems/vermine2047/templates/dialogs/roll-dialog.hbs" }
|
|
};
|
|
|
|
static async create(data = {}) {
|
|
if (data.actor instanceof Actor) {
|
|
return new RollDialog({ actor: data.actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense });
|
|
}
|
|
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, weapon: data.weapon, locks: data.locks, defense: data.defense });
|
|
}
|
|
|
|
constructor(options = {}) {
|
|
super(options);
|
|
this.#actor = options.actor;
|
|
this.label = options.label ?? null;
|
|
this.rolltype = options.rolltype ?? null;
|
|
this.weapon = options.weapon ?? null;
|
|
this.locks = options.locks ?? null;
|
|
this.defense = options.defense ?? null;
|
|
}
|
|
|
|
async _prepareContext() {
|
|
const actor = this.#actor;
|
|
const isCharacter = actor.type === "character";
|
|
const group = isCharacter ? RollDialog.#findGroup(actor) : null;
|
|
const groupPool = group?.system?.experience?.dice || 0;
|
|
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"),
|
|
locks: this.locks,
|
|
experience: {
|
|
hasExperience: isCharacter,
|
|
individual: isCharacter ? actor.system?.experience?.dice || 0 : 0,
|
|
groupPool,
|
|
pullOptions: [0, 1, 2].filter(v => v <= Math.min(2, groupPool)),
|
|
canPull: Boolean(group) && (game.user.isGM || group.isOwner),
|
|
canGive: isCharacter && (game.user.isGM || actor.isOwner)
|
|
}
|
|
};
|
|
}
|
|
|
|
async _onRender(context, options) {
|
|
this.element.dataset.actorId = this.#actor.id;
|
|
|
|
// Mode verrouillé (défense en échange de coups) : présélectionne et bloque
|
|
// caractéristique + difficulté, présélectionne la compétence.
|
|
if (this.locks) {
|
|
this.#applyLocks(this.locks);
|
|
}
|
|
|
|
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.#refreshExperienceUI();
|
|
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"); }
|
|
#getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; }
|
|
|
|
/**
|
|
* Trouve le premier Groupe dont le personnage est membre.
|
|
* @param {Actor} actor
|
|
* @returns {Actor|null}
|
|
*/
|
|
static #findGroup(actor) {
|
|
return game.actors.filter(a => a.type === "group" && (a.system?.members ?? []).includes(actor.id))[0] ?? null;
|
|
}
|
|
|
|
/** Décomposition du pool de dés pour affichage dans le message. */
|
|
getPoolBreakdown() {
|
|
const abil = this.#getAbility();
|
|
const ability = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
|
|
const skill = this.#getSkill();
|
|
const skillPool = parseInt(skill?.options[skill?.selectedIndex]?.dataset?.pool, 10) || 0;
|
|
const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
|
|
const specChecked = this.hasSpecialtySelected();
|
|
const helped = this.#el.querySelector("#helped")?.checked;
|
|
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
|
const tools = toolsChecked && toolsChecked.value !== "0";
|
|
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
|
const experience = this.#getExperiencePull();
|
|
const specialty = specChecked ? 1 : 0;
|
|
const help = helped ? 1 : 0;
|
|
const tooling = tools ? 1 : 0;
|
|
const total = ability + skillPool + selfControl + specialty + help + tooling + group + experience;
|
|
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, total };
|
|
}
|
|
|
|
getDicePool() {
|
|
return this.getPoolBreakdown().total;
|
|
}
|
|
|
|
getSpecialtyName() {
|
|
const checked = this.#el.querySelector("input[name='usingSpecialization']:checked");
|
|
return checked && checked.value !== "aucune" ? checked.value : null;
|
|
}
|
|
|
|
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 Math.max(0, (parseInt(sel?.value, 10) || 1) - 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 ───────────────────────────────────────────────────────────────
|
|
|
|
#applyLocks(locks) {
|
|
const ability = this.#getAbility();
|
|
if (ability && locks.ability) {
|
|
const opt = [...ability.options].find(o => o.dataset.label === locks.ability);
|
|
if (opt) {
|
|
opt.selected = true;
|
|
ability.disabled = true;
|
|
const scoreEl = this.#el.querySelector("#abilityScore");
|
|
if (scoreEl) scoreEl.value = opt.value;
|
|
const sc = this.#getSelfCtrl();
|
|
if (sc) sc.max = opt.value;
|
|
}
|
|
}
|
|
const skill = this.#getSkill();
|
|
if (skill && locks.skill) {
|
|
const opt = [...skill.options].find(o => o.dataset.label === locks.skill);
|
|
if (opt) opt.selected = true;
|
|
}
|
|
const difficulty = this.#getDifficulty();
|
|
if (difficulty && locks.difficulty !== undefined && locks.difficulty !== null) {
|
|
const locked = parseInt(locks.difficulty, 10);
|
|
let opt = [...difficulty.options].find(o => parseInt(o.value, 10) === locked);
|
|
if (!opt) {
|
|
// Difficulté hors des options (ex: 9 + 2) : ajoute une option dédiée.
|
|
opt = document.createElement("option");
|
|
opt.value = String(locked);
|
|
opt.textContent = `${game.i18n.localize("VERMINE.difficulty")} (${locked})`;
|
|
difficulty.appendChild(opt);
|
|
}
|
|
opt.selected = true;
|
|
difficulty.disabled = true;
|
|
}
|
|
}
|
|
|
|
#displaySpecialties() {
|
|
for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
|
|
el.style.display = "inline";
|
|
}
|
|
}
|
|
|
|
#calculateBonusCount() {
|
|
const b = this.getPoolBreakdown();
|
|
return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience;
|
|
}
|
|
|
|
#refreshExperienceUI() {
|
|
const actor = this.#actor;
|
|
const group = RollDialog.#findGroup(actor);
|
|
const individual = actor.system?.experience?.dice || 0;
|
|
const groupPool = group?.system?.experience?.dice || 0;
|
|
const indEl = this.#el.querySelector("#exp-individual");
|
|
if (indEl) indEl.textContent = individual;
|
|
const grpEl = this.#el.querySelector("#exp-group");
|
|
if (grpEl) grpEl.textContent = groupPool;
|
|
const pull = this.#el.querySelector("#experience-pull");
|
|
if (pull) {
|
|
const max = Math.min(2, groupPool);
|
|
const current = Math.min(parseInt(pull.value, 10) || 0, max);
|
|
pull.innerHTML = "";
|
|
for (let i = 0; i <= max; i++) {
|
|
const opt = document.createElement("option");
|
|
opt.value = String(i);
|
|
opt.textContent = `${i}D`;
|
|
if (i === current) opt.selected = true;
|
|
pull.appendChild(opt);
|
|
}
|
|
}
|
|
}
|
|
|
|
#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 #onGiveToGroup(event, target) {
|
|
const actor = this.#actor;
|
|
if (actor.type !== "character") return;
|
|
const individual = actor.system?.experience?.dice || 0;
|
|
if (individual < 1) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
|
|
return;
|
|
}
|
|
if (!game.user.isGM && !actor.isOwner) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
|
|
return;
|
|
}
|
|
const group = RollDialog.#findGroup(actor);
|
|
if (!group) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
|
|
return;
|
|
}
|
|
const groupPool = group.system?.experience?.dice || 0;
|
|
await actor.update({ "system.experience.dice": individual - 1 });
|
|
await group.update({ "system.experience.dice": groupPool + 1 });
|
|
this.#refreshExperienceUI();
|
|
}
|
|
|
|
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 });
|
|
}
|
|
|
|
// Transfert d'Expérience : puiser dans la Réserve de Groupe (1-2D).
|
|
const pulled = this.#getExperiencePull();
|
|
if (pulled > 0) {
|
|
const group = RollDialog.#findGroup(this.#actor);
|
|
if (!group) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
|
|
return;
|
|
}
|
|
if (!game.user.isGM && !group.isOwner) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
|
|
return;
|
|
}
|
|
const groupPool = group.system?.experience?.dice || 0;
|
|
if (groupPool < pulled) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group_dice"));
|
|
return;
|
|
}
|
|
await group.update({ "system.experience.dice": groupPool - pulled });
|
|
}
|
|
|
|
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(),
|
|
poolBreakdown: this.getPoolBreakdown(),
|
|
specialtyName: this.getSpecialtyName(),
|
|
weapon: this.weapon?.toObject() ?? null,
|
|
targets: [...game.user.targets].map(t => t.name),
|
|
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
|
|
});
|
|
|
|
this.close();
|
|
}
|
|
|
|
}
|