feat(effort): dépense des points d'Effort sur un test (règles p.35)

Le bouton d'Effort du message de jet déduit désormais les points de la
Réserve d'Effort de l'acteur (1 point = 1 relance), s'ajoute aux relances
de Compétence au lieu de les remplacer, et affiche le pool restant. Le
curseur est plafonné à min(caractéristique, effort disponible) et la
section est masquée pour les acteurs sans Réserve d'Effort (PNJ,
créatures, effort vide).
This commit is contained in:
2026-08-09 09:32:59 +02:00
parent 15f8e927f0
commit 89413c17a4
5 changed files with 74 additions and 25 deletions
+9
View File
@@ -627,6 +627,15 @@ ol.chat-log {
&.visible { max-height: 15rem; }
}
.reroll-from-effort {
h4.flexcol { gap: 2px; }
.effort-pool {
font-size: @font-size-12;
opacity: 0.8;
text-align: center;
}
}
// ── Dice results ────────────────────────────────────────────────
ul.roll-results {
list-style: none;
+4 -2
View File
@@ -268,8 +268,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
}
getMaxEffort() {
if (this.actor.type === "character") return this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
return 0;
if (this.actor.type !== "character") return 0;
const ability = this.actor.system?.abilities?.[this.abilityKey]?.value || 0;
const effort = this.actor.system?.attributes?.effort?.value || 0;
return Math.min(ability, effort);
}
getPoolBreakdown() {
+3 -1
View File
@@ -288,7 +288,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
getMaxEffort() {
const sel = this.#getAbility();
return parseInt(sel?.value, 10) || 0;
const ability = parseInt(sel?.value, 10) || 0;
const effort = this.#actor?.system?.attributes?.effort?.value || 0;
return Math.min(ability, effort);
}
getTotems() {
+54 -21
View File
@@ -415,45 +415,78 @@ export class VermineUtils {
});
}
// Add click event for granting rerolls
// Add click event for spending Effort points as rerolls (règles p.35) :
// 1 point d'Effort = 1 relance, ajouté aux relances de Compétence restantes.
html.querySelectorAll("button.grant-reroll").forEach(el => {
el.addEventListener('click', async (ev) => {
const btn = ev.currentTarget;
const rollMessage = btn.closest(".vermine-roll-message");
const grantedRerollElement = html.querySelector('#granted-reroll');
const allowedRerollElement = html.querySelector("#allowed_reroll");
const requested = parseInt(grantedRerollElement?.innerText, 10) || 0;
if (grantedRerollElement && allowedRerollElement) {
allowedRerollElement.innerText = grantedRerollElement.innerText;
// Déduit les points d'Effort de l'acteur (max : ce qu'il lui reste).
const actor = await VermineUtils.#resolveMessageActor(rollMessage);
const effort = actor?.system?.attributes?.effort;
let spent = 0;
if (effort && requested > 0) {
spent = Math.min(requested, parseInt(effort.value, 10) || 0);
if (spent > 0) {
const remaining = (parseInt(effort.value, 10) || 0) - spent;
await actor.update({ 'system.attributes.effort.value': remaining });
const poolEl = rollMessage?.querySelector('.effort-pool');
if (poolEl) poolEl.innerText = `${remaining}/${effort.max || 0}`;
}
}
// Les relances d'Effort s'ajoutent aux relances de Compétence restantes.
if (allowedRerollElement) {
const current = parseInt(allowedRerollElement.innerText, 10) || 0;
allowedRerollElement.innerText = current + spent;
}
const effortSection = btn.closest('.reroll-from-effort');
if (effortSection) effortSection.style.display = "none";
const mesEl = btn.closest('[data-message-id]');
const messageId = mesEl?.dataset?.messageId;
if (messageId) {
btn.closest('.reroll-from-effort').style.display = "none";
const rollMessage = btn.closest(".vermine-roll-message");
if (rollMessage) {
const content = rollMessage.outerHTML;
// GM applies the update directly ; players relay through the
// system socket (Foundry v14 ChatMessage updates are GM-only).
if (game.user?.isGM) {
const message = await game.messages.get(messageId);
await message.update({ content: content });
} else {
game.socket?.emit(`system.${game.system.id}`, {
type: "reroll",
messageId: messageId,
requestId: foundry.utils.randomID(8),
content: content
});
}
const content = rollMessage.outerHTML;
// GM applies the update directly ; players relay through the
// system socket (Foundry v14 ChatMessage updates are GM-only).
if (game.user?.isGM) {
const message = await game.messages.get(messageId);
await message.update({ content: content });
} else {
game.socket?.emit(`system.${game.system.id}`, {
type: "reroll",
messageId: messageId,
requestId: foundry.utils.randomID(8),
content: content
});
}
}
});
});
}
/**
* Résout l'acteur à l'origine d'un message de jet depuis les attributs
* data-actor-id / data-actor-uuid posés sur le message. Le uuid est
* privilégié : il distingue les tokens non liés (PNJ mineurs) issus de la
* même fiche, qui partagent le même id d'acteur monde.
* @param {HTMLElement} rollMessage
* @returns {Promise<Actor|null>}
*/
static async #resolveMessageActor(rollMessage) {
if (!rollMessage) return null;
const id = rollMessage.dataset.actorId ?? null;
const uuid = rollMessage.dataset.actorUuid ?? null;
if (!id && !uuid) return null;
const { VermineExchange } = await import('./exchange.mjs');
return VermineExchange.resolveDefender({ id, uuid });
}
/**
* Displays dice rolls in 3D if available.
* @param {Roll} roll - The roll to display
+4 -1
View File
@@ -1,4 +1,4 @@
<div class="vermine-roll-message" data-free-successes="{{verdict.bonusSuccesses}}">
<div class="vermine-roll-message" data-free-successes="{{verdict.bonusSuccesses}}" data-actor-id="{{param.actor.id}}" data-actor-uuid="{{param.actor.uuid}}">
{{!-- Verdict --}}
<div class="roll-verdict {{#if verdict.success}}success{{else}}failure{{/if}}">
{{#if verdict.success}}
@@ -226,9 +226,11 @@
<div class="reroll-fromroll">
<h4>{{localize "VERMINE.rerolls_possible"}} : <span id="allowed_reroll">{{param.Reroll}}</span></h4>
<div class="reroll flexrow">
{{#ifgt param.actor.system.attributes.effort.value 0}}
<div class="reroll-from-effort flexrow">
<h4 class="flexcol">
<span>{{localize "VERMINE.effort"}}</span>
<span class="effort-pool">{{param.actor.system.attributes.effort.value}}/{{param.actor.system.attributes.effort.max}}</span>
</h4>
<input type="range"
min="0"
@@ -240,6 +242,7 @@
<span id="granted-reroll">0</span>
</button>
</div>
{{/ifgt}}
</div>
</div>