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; } &.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 ──────────────────────────────────────────────── // ── Dice results ────────────────────────────────────────────────
ul.roll-results { ul.roll-results {
list-style: none; list-style: none;
+4 -2
View File
@@ -268,8 +268,10 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
} }
getMaxEffort() { getMaxEffort() {
if (this.actor.type === "character") return this.actor.system?.abilities?.[this.abilityKey]?.value || 0; if (this.actor.type !== "character") return 0;
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() { getPoolBreakdown() {
+3 -1
View File
@@ -288,7 +288,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
getMaxEffort() { getMaxEffort() {
const sel = this.#getAbility(); 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() { 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 => { html.querySelectorAll("button.grant-reroll").forEach(el => {
el.addEventListener('click', async (ev) => { el.addEventListener('click', async (ev) => {
const btn = ev.currentTarget; const btn = ev.currentTarget;
const rollMessage = btn.closest(".vermine-roll-message");
const grantedRerollElement = html.querySelector('#granted-reroll'); const grantedRerollElement = html.querySelector('#granted-reroll');
const allowedRerollElement = html.querySelector("#allowed_reroll"); const allowedRerollElement = html.querySelector("#allowed_reroll");
const requested = parseInt(grantedRerollElement?.innerText, 10) || 0;
if (grantedRerollElement && allowedRerollElement) { // Déduit les points d'Effort de l'acteur (max : ce qu'il lui reste).
allowedRerollElement.innerText = grantedRerollElement.innerText; 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 mesEl = btn.closest('[data-message-id]');
const messageId = mesEl?.dataset?.messageId; const messageId = mesEl?.dataset?.messageId;
if (messageId) { if (messageId) {
btn.closest('.reroll-from-effort').style.display = "none"; const content = rollMessage.outerHTML;
// GM applies the update directly ; players relay through the
const rollMessage = btn.closest(".vermine-roll-message"); // system socket (Foundry v14 ChatMessage updates are GM-only).
if (rollMessage) { if (game.user?.isGM) {
const content = rollMessage.outerHTML; const message = await game.messages.get(messageId);
// GM applies the update directly ; players relay through the await message.update({ content: content });
// system socket (Foundry v14 ChatMessage updates are GM-only). } else {
if (game.user?.isGM) { game.socket?.emit(`system.${game.system.id}`, {
const message = await game.messages.get(messageId); type: "reroll",
await message.update({ content: content }); messageId: messageId,
} else { requestId: foundry.utils.randomID(8),
game.socket?.emit(`system.${game.system.id}`, { content: content
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. * Displays dice rolls in 3D if available.
* @param {Roll} roll - The roll to display * @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 --}} {{!-- Verdict --}}
<div class="roll-verdict {{#if verdict.success}}success{{else}}failure{{/if}}"> <div class="roll-verdict {{#if verdict.success}}success{{else}}failure{{/if}}">
{{#if verdict.success}} {{#if verdict.success}}
@@ -226,9 +226,11 @@
<div class="reroll-fromroll"> <div class="reroll-fromroll">
<h4>{{localize "VERMINE.rerolls_possible"}} : <span id="allowed_reroll">{{param.Reroll}}</span></h4> <h4>{{localize "VERMINE.rerolls_possible"}} : <span id="allowed_reroll">{{param.Reroll}}</span></h4>
<div class="reroll flexrow"> <div class="reroll flexrow">
{{#ifgt param.actor.system.attributes.effort.value 0}}
<div class="reroll-from-effort flexrow"> <div class="reroll-from-effort flexrow">
<h4 class="flexcol"> <h4 class="flexcol">
<span>{{localize "VERMINE.effort"}}</span> <span>{{localize "VERMINE.effort"}}</span>
<span class="effort-pool">{{param.actor.system.attributes.effort.value}}/{{param.actor.system.attributes.effort.max}}</span>
</h4> </h4>
<input type="range" <input type="range"
min="0" min="0"
@@ -240,6 +242,7 @@
<span id="granted-reroll">0</span> <span id="granted-reroll">0</span>
</button> </button>
</div> </div>
{{/ifgt}}
</div> </div>
</div> </div>