2 Commits
Author SHA1 Message Date
uberwald 0ef0de45ff fix(experience): phase ciblée (groupe/sans groupe/individuel) + dépense des Dés d'Évolution
Release Creation / build (release) Failing after 1m18s
Phase d'Expérience :
- sélecteur de groupe : « Tous les personnages », « Sans groupe » ou un groupe précis → seuls ses membres sont récompensés
- case « Inclure dans cette phase » par personnage pour exclure de la phase
- chaque ligne affiche son groupe ([Groupe] / [Sans groupe])
- les collectifs ne sont versés au groupe que si un vrai groupe est choisi

Dés d'Évolution :
- achat : ne consomme que le coût du niveau (le reste du stock est conservé) au lieu d'effacer tout
- total « après achat » recalculé en direct selon le niveau sélectionné
- libellés d'options nettoyés (1D au lieu de 1D — 1D)
2026-08-08 18:35:01 +02:00
uberwald c7e6660d9d fix(combat): re-render tracker après suppression + initiative par Réaction + lisibilité + malus PNJ
Release Creation / build (release) Failing after 1m41s
- fight.mjs : _onDelete appelle super._onDelete() → le tracker se vide immédiatement après « Terminer » (plus de F5)
- fight.mjs : _getInitiativeFormula type-dépendante — PNJ/Créature roulent leur pool de Réaction (computed.reaction + reactionBonus) au lieu de 0D → classement par Réaction correct
- dialogs.less : total des dés (.total-value) en blanc au lieu du vert sombre (illisible sur fond foncé)
- npc-roll-dialog : champ « Malus temporaires » + logique pool (dés malus dans le dialogue PNJ/Créature)
2026-08-07 21:04:24 +02:00
10 changed files with 101 additions and 35 deletions
+4 -1
View File
@@ -333,6 +333,9 @@
"CharacterNamePlaceholder": "Character Name",
"ProfilePlaceholder": "Profile",
"none": "None",
"all_characters": "All characters",
"no_group": "No group",
"include_in_phase": "Include in this phase",
"bonuses": "Bonuses",
"choose_ability": "Choose Ability",
"choose_skill": "Choose Skill",
@@ -469,7 +472,7 @@
"mode_survie": "Survival",
"mode_cauchemar": "Nightmare",
"mode_apocalypse": "Apocalypse",
"evolution_buy_hint": "The purchase consumes and erases all accumulated Evolution dice. Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
"evolution_buy_hint": "The purchase consumes a number of Evolution dice equal to the chosen level (the remainder is kept). Limits: Survival 5D, Nightmare 6D, Apocalypse 7D total. Level 3/4 Mutations are forbidden in Survival; only one level 4 Mutation.",
"buy_evolution_done": "Adaptation/Mutation acquired. Evolution dice erased.",
"error_evolution_no_name": "Provide a name for the Adaptation/Mutation.",
"error_evolution_invalid_level": "Invalid level (1 to 4).",
+4 -1
View File
@@ -365,6 +365,9 @@
"choose_ability": "Choisissez une caractéristique",
"choose_skill": "Choisissez une compétence",
"none": "Aucun",
"all_characters": "Tous les personnages",
"no_group": "Sans groupe",
"include_in_phase": "Inclure dans cette phase",
"total": "Total",
"bonuses": "Bonus",
"handicap": "Handicap",
@@ -452,7 +455,7 @@
"mode_survie": "Survie",
"mode_cauchemar": "Cauchemar",
"mode_apocalypse": "Apocalypse",
"evolution_buy_hint": "L'achat consomme et efface tous les Dés d'Évolution accumulés. Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
"evolution_buy_hint": "L'achat consomme un nombre de Dés d'Évolution égal au niveau choisi (le reste est conservé). Limites : Survie 5D, Cauchemar 6D, Apocalypse 7D au total. Mutations 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.",
"buy_evolution_done": "Adaptation/Mutation acquise. Dés d'Évolution effacés.",
"error_evolution_no_name": "Indiquez un nom pour l'Adaptation/Mutation.",
"error_evolution_invalid_level": "Niveau invalide (1 à 4).",
+2 -2
View File
@@ -694,8 +694,8 @@ iframe {
.total-value {
font-family: "DistressBlack", sans-serif;
font-size: @font-size-28;
color: @color-acid-green;
text-shadow: 0 0 8px fade(@color-acid-green, 40%);
color: @color-text-light-0;
text-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
}
.totem-selector {
+17 -5
View File
@@ -5,8 +5,8 @@ const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'achat d'une Adaptation/Mutation avec les Dés d'Évolution
* accumulés. Vérifie le coût (niveau 1-4), la limite du Mode de jeu et les
* restrictions sur les Mutations ; en cas de succès, crée l'item et efface
* les Dés d'Évolution accumulés.
* restrictions sur les Mutations ; en cas de succès, crée l'item et consomme
* un nombre de Dés d'Évolution égal au niveau choisi.
*/
export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
@@ -61,7 +61,7 @@ export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(found
};
const levels = [];
for (let value = 1; value <= 4; value++) {
levels.push({ value, cost: value });
levels.push({ value });
}
return {
evolutionDice: actor.system?.experience?.evolution || 0,
@@ -73,6 +73,18 @@ export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(found
};
}
async _onRender() {
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
this.#updateUI();
}
/** Met à jour le total des niveaux « après achat » selon le niveau sélectionné. */
#updateUI() {
const cost = parseInt(this.element.querySelector("#evolution-level")?.value, 10) || 0;
const el = this.element.querySelector("#evolution-total-value");
if (el) el.textContent = `${VermineExperience.evolutionTotalLevels(this.#actor) + cost}D`;
}
static async #onCancel(event, target) {
this.close();
}
@@ -102,12 +114,12 @@ export default class BuyEvolutionDialog extends HandlebarsApplicationMixin(found
mutation
}
}]);
// N'efface les Dés d'Évolution que si l'item a bien été créé.
// Ne consomme que le coût du niveau choisi (Règles p. 116-117 : coût = niveau).
if (!created || created.length === 0) {
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_create"));
return;
}
await actor.update({ "system.experience.evolution": 0 });
await actor.update({ "system.experience.evolution": Math.max(0, (actor.system?.experience?.evolution || 0) - cost) });
ui.notifications.info(game.i18n.localize("VERMINE.buy_evolution_done"));
this.close();
+29 -13
View File
@@ -48,13 +48,18 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
if (!memberOf.has(id)) memberOf.set(id, g.id);
}
}
const groupById = new Map(groups.map(g => [g.id, g]));
return {
characters: characters.map(a => ({
id: a.id,
name: a.name,
dice: a.system?.experience?.dice || 0,
groupId: memberOf.get(a.id) || ""
})),
characters: characters.map(a => {
const groupId = memberOf.get(a.id) || "";
return {
id: a.id,
name: a.name,
dice: a.system?.experience?.dice || 0,
groupId,
groupName: groupId ? groupById.get(groupId)?.name : ""
};
}),
groups: groups.map(a => ({
id: a.id,
name: a.name,
@@ -72,6 +77,9 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
async _onRender(context, options) {
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
this.element.querySelector("#group-select")?.addEventListener("change", () => this.#updateGroupFilter());
this.element.querySelectorAll("[name^='include-']").forEach(cb =>
cb.addEventListener("click", event => event.stopPropagation())
);
this.#updateUI();
this.#updateGroupFilter();
}
@@ -84,11 +92,16 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
if (el) el.textContent = `${danger + discoveries + duration}D`;
}
/** Filtre les personnages selon le groupe sélectionné (vide = tous). */
/** Filtre les personnages selon le groupe sélectionné (vide = tous, "none" = sans groupe). */
#updateGroupFilter() {
const groupId = this.element.querySelector("#group-select")?.value || "";
for (const row of this.element.querySelectorAll("[data-character-id]")) {
const show = !groupId || row.dataset.groupId === groupId;
const rowGroupId = row.dataset.groupId || "";
const show = groupId === ""
? true
: groupId === "none"
? rowGroupId === ""
: rowGroupId === groupId;
row.hidden = !show;
}
}
@@ -102,8 +115,8 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
+ (parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0)
+ (parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0);
const groupId = this.element.querySelector("#group-select")?.value || null;
if (groupId) {
const groupId = this.element.querySelector("#group-select")?.value || "";
if (groupId && groupId !== "none") {
const group = game.actors.get(groupId);
if (group && collective > 0) {
const current = group.system?.experience?.dice || 0;
@@ -121,8 +134,11 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
const results = [];
for (const row of this.element.querySelectorAll("[data-character-id]")) {
// Quand un groupe précis est choisi, ne récompenser que ses membres.
if (groupId && row.dataset.groupId !== groupId) continue;
// Filtre par groupe sélectionné (vide = tous, "none" = sans groupe)
const rowGroupId = row.dataset.groupId || "";
if (groupId === "none" ? rowGroupId !== "" : groupId !== "" && rowGroupId !== groupId) continue;
// Case « Inclure » décochée → personnage exclu de cette phase
if (!row.querySelector(`[name="include-${row.dataset.characterId}"]`)?.checked) continue;
const id = row.dataset.characterId;
const actor = game.actors.get(id);
if (!actor) continue;
@@ -164,7 +180,7 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
}
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
if (groupId) {
if (groupId && groupId !== "none") {
const group = game.actors.get(groupId);
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
}
+6 -2
View File
@@ -174,11 +174,15 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
#getGroup() { return parseInt(this.#el.querySelector("#group")?.value, 10) || 0 }
#getTemporaryMalus() { return parseInt(this.#el.querySelector("#temporary-malus")?.value, 10) || 0 }
getPoolBreakdown() {
const help = this.#getHelped()
const group = this.#getGroup()
const malus = VermineExchange.woundMalus(this.#actor)
return { pool: this.pool, help, group, malus, total: Math.max(0, this.pool + help + group - malus) }
const wound = VermineExchange.woundMalus(this.#actor)
const tempMalus = this.#getTemporaryMalus()
const malus = wound + tempMalus
return { pool: this.pool, help, group, wound, tempMalus, malus, total: Math.max(0, this.pool + help + group - malus) }
}
#updateUI() {
+22 -8
View File
@@ -210,14 +210,16 @@ export class VermineCombat extends Combat {
}
/************************************************************************************/
_onDelete() {
/*let combatants = this.combatants.contents
for (let c of combatants) {
let actor = game.actors.get(c.actorId)
actor.clearInitiative()
actor.displayRecuperation()
}
super._onDelete()*/
/**
* Nettoyage lors de la suppression du combat (bouton « Terminer » / suppression).
* Doit appeler le handler de base : c'est lui qui re-rend le CombatTracker
* (`ui.combat.render({combat: null})`) — sans cela le tracker reste figé sur
* l'ancien ordre de tour jusqu'au rechargement de la page (F5).
* @param {object} options
* @param {string} userId
*/
_onDelete(options, userId) {
super._onDelete(options, userId);
}
}
@@ -376,6 +378,18 @@ export class VermineCombatant extends Combatant {
}
}
_getInitiativeFormula() {
// Personnage : jet de Réaction (Réflexes + Vigilance) contre la
// Difficulté du statut de combat — les Réussites = valeur de Réaction.
// PNJ et Créature : n'ont pas de caractéristiques/compétences — ils
// roulent leur pool de Réaction (computed.reaction) contre la même
// Difficulté, majoré de leur bonus de Réaction.
const actor = this.actor
if ( actor?.type === 'npc' || actor?.type === 'creature' ) {
const reaction = parseInt(actor.system?.computed?.reaction, 10) || 0
const difficulty = parseInt(actor.system?.combatStatus?.difficulty, 10) || 7
const bonus = parseInt(actor.system?.computed?.reactionBonus, 10) || 0
return `${reaction}d10cs>=${difficulty}+${bonus}`
}
return String(CONFIG.Combat.initiative.formula || game.system.initiative)
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
<label class="label" for="evolution-level">{{ localize 'VERMINE.evolution_level' }}</label>
<select class="info-value" data-roll="true" id="evolution-level" name="evolution-level">
{{#each levels}}
<option value="{{ this.value }}">{{ this.value }}D{{ this.cost }}D</option>
<option value="{{ this.value }}">{{ this.value }}D</option>
{{/each}}
</select>
</div>
@@ -36,7 +36,8 @@
<div class="flexrow bonus-item">
<label class="label" for="group-select">{{ localize 'VERMINE.collective_group' }}</label>
<select class="info-value" id="group-select" name="group-select">
<option value="">{{ localize 'VERMINE.none' }}</option>
<option value="">{{ localize 'VERMINE.all_characters' }}</option>
<option value="none">{{ localize 'VERMINE.no_group' }}</option>
{{#each groups}}
<option value="{{ this.id }}">{{ this.name }} ({{ this.dice }}D)</option>
{{/each}}
@@ -49,8 +50,14 @@
{{#each characters}}
<details class="difficulty-section" data-character-id="{{ this.id }}" data-group-id="{{ this.groupId }}" open>
<summary class="flexrow">
<label class="flexrow exp-include-label" title="{{ localize 'VERMINE.include_in_phase' }}">
<input type="checkbox" data-roll="true" id="include-{{ this.id }}" name="include-{{ this.id }}" checked />
</label>
<span class="label">{{ this.name }}</span>
<span class="current-values">{{ localize 'VERMINE.experience_dice' }}: {{ this.dice }}D</span>
<span class="current-values">
{{#if this.groupName}}[{{ this.groupName }}]{{ else }}[{{ localize 'VERMINE.no_group' }}]{{/if}}
{{ localize 'VERMINE.experience_dice' }}: {{ this.dice }}D
</span>
</summary>
<div class="flexrow difficulty-controls">
<div class="flexcol">
+7
View File
@@ -79,6 +79,13 @@
<span>D</span>
</div>
<div class="flexrow bonus-item full-width">
<label class="label" for="temporary-malus">{{localize 'VERMINE.temporary_malus'}}</label>
<input type="number" data-roll="true" class="numeric-entry" style="text-align: center;"
name="temporary-malus" id="temporary-malus" min="0" max="5" value="0" />
<span>D</span>
</div>
</div>
</details>