feat: améliorer les fiches (tests PNJ/créature, message de jet, compétences, créatures)
Release Creation / build (release) Failing after 1m23s

This commit is contained in:
2026-08-04 00:49:37 +02:00
parent 9c7ebc6df9
commit 31f2f4530c
28 changed files with 736 additions and 224 deletions
+4 -3
View File
@@ -139,9 +139,10 @@ VERMINE.creaturePatternLevels = {
* Creature Size Levels configuration
*/
VERMINE.creatureSizeLevels = {
1: { "label": "SIZE_LEVELS.small", "attack": 2, "vigor": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
2: { "label": "SIZE_LEVELS.medium", "attack": 3, "vigor": 2, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
3: { "label": "SIZE_LEVELS.large", "attack": 4, "vigor": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
}
/**
+26 -16
View File
@@ -112,18 +112,32 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
#getHandicap() { return this.#el.querySelector("#handicap"); }
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
getDicePool() {
/** Décomposition du pool de dés pour affichage dans le message. */
getPoolBreakdown() {
const abil = this.#getAbility();
const abilVal = parseInt(abil?.options[abil?.selectedIndex]?.value, 10) || 0;
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 sc = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const specChecked = this.#el.querySelector("#usingSpecialization")?.checked;
const selfControl = parseInt(this.#getSelfCtrl()?.value, 10) || 0;
const specChecked = this.hasSpecialtySelected();
const helped = this.#el.querySelector("#helped")?.checked;
const tools = this.#el.querySelector("input[name='usingTools']:checked")?.value !== "0";
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 bonuses = (specChecked ? 1 : 0) + (helped ? 1 : 0) + (tools ? 1 : 0) + group;
return (abilVal + sc + skillPool + bonuses) || 0;
const specialty = specChecked ? 1 : 0;
const help = helped ? 1 : 0;
const tooling = tools ? 1 : 0;
const total = ability + skillPool + selfControl + specialty + help + tooling + group;
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, 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() {
@@ -207,14 +221,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
}
#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;
if (this.hasSpecialtySelected()) b += 1;
return b;
const b = this.getPoolBreakdown();
return b.specialty + b.help + b.tooling + b.group + b.selfControl;
}
#updateUI() {
@@ -315,7 +323,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
keepTotem: this.getKeepTotem(),
skillCategory: this.getSkillCategory(),
skillLevel: this.getSkillLevel(),
hasSpecialty: this.hasSpecialtySelected()
hasSpecialty: this.hasSpecialtySelected(),
poolBreakdown: this.getPoolBreakdown(),
specialtyName: this.getSpecialtyName()
});
this.close();
+19
View File
@@ -32,6 +32,7 @@ export const preloadHandlebarsTemplates = async function () {
// npc partials
"systems/vermine2047/templates/actor/npc/npc-combat.hbs",
"systems/vermine2047/templates/actor/parts/npc-skill-category.hbs",
"systems/vermine2047/templates/actor/parts/npc-skill-item.hbs",
// creature partials
"systems/vermine2047/templates/actor/creature/creature-combat.hbs",
@@ -100,6 +101,21 @@ export const registerHandlebarsHelpers = function () {
return game.i18n.localize(arrayLabel + "." + objectLabel + ".name");
});
// Return an object of skills ordered alphabetically by their localized
// display name (easier to find a skill quickly). Use with `{{#each}}`.
Handlebars.registerHelper('sortedSkills', function (skills) {
if (!skills || typeof skills !== 'object') return skills;
const sorted = {};
Object.keys(skills)
.sort((a, b) => {
const na = game.i18n.localize(`SKILLS.${a}.name`).toLocaleLowerCase();
const nb = game.i18n.localize(`SKILLS.${b}.name`).toLocaleLowerCase();
return na.localeCompare(nb, game.i18n.lang);
})
.forEach(k => { sorted[k] = skills[k]; });
return sorted;
});
Handlebars.registerHelper('smarttlk', function (arrayLabel, objectLabel, key) {
return game.i18n.localize(arrayLabel + "." + objectLabel + "." + key);
});
@@ -347,6 +363,9 @@ export const registerHandlebarsHelpers = function () {
Handlebars.registerHelper('ifincludes', function (arg1, arg2, options) {
return (arg1.includes(arg2)) ? options.fn(this) : options.inverse(this);
});
Handlebars.registerHelper('ifStartsWith', function (arg1, arg2, options) {
return (typeof arg1 === 'string' && arg1.startsWith(arg2)) ? options.fn(this) : options.inverse(this);
});
//math operations
+114 -28
View File
@@ -29,7 +29,9 @@ export class VermineUtils {
keepTotem = null,
skillLevel = null,
hasSpecialty = false,
handicap = 0
handicap = 0,
poolBreakdown = null,
specialtyName = null
}) {
// Validate inputs
if (!actor) {
@@ -105,6 +107,27 @@ export class VermineUtils {
// Build final formula
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
// Reconcile the breakdown displayed in chat with the dice actually rolled.
// Domain bonuses/malus are only applied when the corresponding totem die
// is used, and totem dice replace (not add to) base-pool dice.
if (poolBreakdown) {
// Count totem dice present in the final modifier formula.
let totemDice = 0;
if (modFormula) {
if (modFormula.includes("human_")) totemDice += 1;
if (modFormula.includes("adapted_")) totemDice += 1;
}
// Only count the domain bonus of totems actually activated in the roll.
const domainBonus = (totems.human ? totemBonus.human : 0)
+ (totems.adapted ? totemBonus.adapted : 0);
const basePool = poolBreakdown.total;
poolBreakdown.totemDice = totemDice;
poolBreakdown.domainBonus = domainBonus;
// Total = nombre réel de dés lancés (NoD réguliers + dés de totem).
// Équivaut à basePool + domainBonus sauf si le pool régulier clampe à 0.
poolBreakdown.total = NoD + totemDice;
}
// Create the roll
const roll = new Roll(formula, actor.getRollData());
@@ -143,7 +166,9 @@ export class VermineUtils {
keepTotem,
skillLevel,
hasSpecialty,
handicap
handicap,
poolBreakdown,
specialtyName
});
return roll;
@@ -199,10 +224,10 @@ export class VermineUtils {
/**
* Handles reroll events on dice in chat messages.
* @param {Object} message - The chat message containing the reroll event
* @param {Object} ev - The reroll event
* @param {HTMLElement} target - The clicked die element
* @returns {Promise<boolean>} Whether the reroll was successful
*/
static async onReroll(message, ev) {
static async onReroll(message, target) {
// Verify user permissions
const msgUserId = message.user?.id ?? message.user;
if (msgUserId !== game.user?.id && !game.user?.isGM) {
@@ -211,7 +236,7 @@ export class VermineUtils {
}
// Get reroll count
const rollMessage = ev.currentTarget.closest('div.vermine-roll-message');
const rollMessage = target.closest('div.vermine-roll-message');
if (!rollMessage) {
return false;
}
@@ -221,22 +246,22 @@ export class VermineUtils {
// Check if rerolls are available
if (!rerollCount || parseInt(rerollCount, 10) < 1) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left'));
const rerollables = ev.currentTarget.closest('ul')?.querySelectorAll('.rerollable');
const rerollables = target.closest('ul')?.querySelectorAll('.rerollable');
if (rerollables) {
rerollables.forEach(el => el.classList.remove('rerollable'));
}
return false;
}
ev.currentTarget.classList.add('rerolled');
target.classList.add('rerolled');
// Set reroll flag
await message.setFlag("world", "reroll", true);
// Get difficulty and dice type
const ulElement = ev.currentTarget.closest('ul');
const ulElement = target.closest('ul');
const difficulty = ulElement?.dataset.difficulty ?? 7;
let diceType = ev.currentTarget.dataset.diceType;
let diceType = target.dataset.diceType;
// Sanitize user name
const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');
@@ -265,30 +290,68 @@ export class VermineUtils {
// Update die display
const result = reroll.dice[0]?.results[0]?.result ?? 0;
const dieSpan = ev.currentTarget.querySelector('span');
const dieSpan = target.querySelector('span');
if (dieSpan) {
dieSpan.innerText = result;
}
// Update the face used for the CSS background image.
target.dataset.result = result;
// Update total if successful
// Set/clear the success class from the rerolled die (a previous success
// must be removed if the new result fails).
const success = reroll.dice[0]?.results[0]?.success;
if (success) {
ev.currentTarget.classList.add('success');
const totalElement = rollMessage.querySelector('#total');
if (totalElement) {
const currentTotal = parseInt(totalElement.innerText, 10) || 0;
totalElement.innerText = currentTotal + reroll.total;
}
target.classList.toggle('success', !!success);
target.classList.remove("rerollable");
// Recompute the total from the live dice in the DOM. This avoids the
// cumulative-increment drift of the old logic (which never removed a
// removed success from #total). Totem dice (human/adapted) count double.
const totalEl = rollMessage.querySelector('#total');
if (totalEl) {
let total = 0;
rollMessage.querySelectorAll('li.die.success').forEach(die => {
const isTotem = die.classList.contains('human') || die.classList.contains('adapted');
total += isTotem ? 2 : 1;
});
totalEl.innerText = total;
}
// Update message content
ev.currentTarget.classList.remove("rerollable");
const messageContent = ev.currentTarget.closest('div.message-content');
const messageContent = target.closest('div.message-content');
if (messageContent) {
const newRerollCount = parseInt(rerollCount, 10) - 1;
rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount;
// Recompute the verdict banner from the recomputed total.
const verdictEl = rollMessage.querySelector('.roll-verdict');
const requiredEl = rollMessage.querySelector('#required');
if (verdictEl && totalEl && requiredEl) {
const total = parseInt(totalEl.innerText, 10) || 0;
const required = parseInt(requiredEl.innerText, 10) || 1;
const won = total >= required;
verdictEl.classList.toggle('success', won);
verdictEl.classList.toggle('failure', !won);
const label = won
? game.i18n.localize('VERMINE.roll_success')
: game.i18n.localize('VERMINE.roll_failure');
verdictEl.innerHTML = `${won ? '<i class="fas fa-check-circle"></i>' : '<i class="fas fa-times-circle"></i>'}<span>${label}</span>`;
}
// Recompute the dice summary from the current die list.
const summaryEl = rollMessage.querySelector('.roll-dice-summary');
if (summaryEl) {
const stats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
rollMessage.querySelectorAll('li.die').forEach(die => {
const type = die.classList.contains('human') ? 'human' : die.classList.contains('adapted') ? 'adapted' : 'regular';
stats[type].count += 1;
if (die.classList.contains('success')) stats[type].success += 1;
});
const mk = (key, count, s) => count ? `<span class="die-summary ${key}">${count} ${game.i18n.localize(`VERMINE.die_${key}`)} · ${s} ${game.i18n.localize('VERMINE.die_successes')}</span>` : '';
summaryEl.innerHTML = mk('regular', stats.regular.count, stats.regular.success)
+ mk('human', stats.human.count, stats.human.success)
+ mk('adapted', stats.adapted.count, stats.adapted.success);
}
await message.update({
content: messageContent.outerHTML
});
@@ -314,10 +377,11 @@ export class VermineUtils {
html.querySelectorAll('.rerollable').forEach(el => {
el.addEventListener('click', async (ev) => {
ev.preventDefault();
const msgId = ev.currentTarget.closest("li.message")?.dataset?.messageId;
const dieEl = ev.currentTarget;
const msgId = dieEl.closest("li.message")?.dataset?.messageId;
if (msgId) {
const message = await game.messages.get(msgId);
await VermineUtils.onReroll(message, ev);
await VermineUtils.onReroll(message, dieEl);
}
});
});
@@ -336,6 +400,7 @@ export class VermineUtils {
// Add click event for granting rerolls
html.querySelectorAll("button.grant-reroll").forEach(el => {
el.addEventListener('click', async (ev) => {
const btn = ev.currentTarget;
const grantedRerollElement = html.querySelector('#granted-reroll');
const allowedRerollElement = html.querySelector("#allowed_reroll");
@@ -343,13 +408,13 @@ export class VermineUtils {
allowedRerollElement.innerText = grantedRerollElement.innerText;
}
const mesEl = ev.currentTarget.closest('[data-message-id]');
const mesEl = btn.closest('[data-message-id]');
const messageId = mesEl?.dataset?.messageId;
if (messageId) {
ev.currentTarget.closest('.reroll-from-effort').style.display = "none";
btn.closest('.reroll-from-effort').style.display = "none";
const rollMessage = ev.currentTarget.closest(".vermine-roll-message");
const rollMessage = btn.closest(".vermine-roll-message");
if (rollMessage) {
const content = rollMessage.outerHTML;
const message = await game.messages.get(messageId);
@@ -401,7 +466,28 @@ export class VermineUtils {
* @returns {Promise<ChatMessage>} The created chat message
*/
static async diplayChatRoll(roll, param) {
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param });
// Aggregate dice stats per type (regular / human / adapted)
const diceStats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
for (const die of roll.dice ?? []) {
// Flavors look like "<type>_<user>"; classify from the leading token.
const flavor = die.options?.flavor ?? "";
const prefix = flavor.split("_")[0];
const type = prefix === "human" ? "human" : prefix === "adapted" ? "adapted" : "regular";
for (const r of die.results ?? []) {
diceStats[type].count += 1;
if (r.success) diceStats[type].success += 1;
}
}
// Verdict
const required = 1 + (param.handicap ?? 0);
const total = roll._total ?? 0;
const verdict = {
success: total >= required,
total,
required
};
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param, diceStats, verdict });
const chatData = {
user: game.user?._id,
speaker: ChatMessage.getSpeaker(),