Files
vermine2047/module/system/dialogs/groupLearningDialog.mjs
T
uberwald 18180a8445
Release Creation / build (release) Failing after 1m40s
fix: apprentissage de Groupe - dés dépensés en cas d'échec et gardes
- Les Dés d'Expérience du Groupe sont désormais dépensés que le jet
  d'apprentissage soit réussi ou raté (cohérent avec l'apprentissage
  de personnage et les règles ; le décompte est sorti de #applySuccess).
- Garde sur le type Capacité : si aucune Capacité de Groupe n'est
  sélectionnée, le jet est bloqué avec un avertissement (avant, il
  marquait learned.ability et dépensait les dés sans rien apprendre).
- Limites de Capacités de Totem (personnage) : seules les Capacités du
  Totem courant du personnage comptent dans les compteurs
  3×N1 / 1×N2-N3 / max 5.
2026-08-05 14:15:11 +02:00

255 lines
8.1 KiB
JavaScript

import { VermineUtils } from '../roll.mjs';
const { HandlebarsApplicationMixin } = foundry.applications.api;
/**
* Dialogue d'apprentissage de Groupe (phase d'Expérience).
* Permet de dépenser des Dés d'Expérience du Groupe pour tenter d'améliorer
* le Niveau de Groupe, la Réserve de Groupe ou de développer une Capacité
* collective. La progression est appliquée automatiquement en cas de réussite.
*/
export default class GroupLearningDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
#actor;
static DEFAULT_OPTIONS = {
classes: ['vermine-roll'],
tag: 'form',
window: {
icon: 'fas fa-graduation-cap',
resizable: false,
},
position: {
width: 560,
height: 560,
},
actions: {
roll: GroupLearningDialog.#onRoll,
cancel: GroupLearningDialog.#onCancel,
},
};
static PARTS = {
main: { template: 'systems/vermine2047/templates/dialogs/group-learning-dialog.hbs', scrollable: [''] },
};
static async create({ actorId }) {
const actor = await game.actors.get(actorId);
if (!actor) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'));
return null;
}
return new GroupLearningDialog({ actor });
}
constructor(options = {}) {
super(options);
this.#actor = options.actor;
}
get title() {
return game.i18n.localize('VERMINE.open_learning');
}
_prepareContext() {
const actor = this.#actor;
return {
actor,
system: actor.system,
dice: actor.system?.experience?.dice || 0,
abilities: actor.itemTypes.ability
.filter((i) => i.system.type !== 'totem' && !i.system.learned)
.map((a) => ({
id: a.id,
name: a.name,
threshold: a.system?.learn?.threshold ?? 5,
level: a.system?.level?.value ?? 1,
})),
};
}
_onRender() {
this.element.dataset.actorId = this.#actor.id;
for (const inp of this.element.querySelectorAll('[data-roll]')) {
inp.addEventListener('change', () => this.#updateUI());
}
this.element.querySelector('#learning-dice')?.addEventListener('input', () => this.#updateUI());
this.#updateUI();
}
get #el() { return this.element; }
#getType() {
return this.#el.querySelector('input[name="learning-type"]:checked')?.value ?? 'level';
}
#getAbilityId() {
return this.#el.querySelector('#learning-ability')?.value ?? '';
}
#getDiceSpent() {
return Math.max(1, parseInt(this.#el.querySelector('#learning-dice')?.value, 10) || 1);
}
#compute() {
const actor = this.#actor;
const type = this.#getType();
const key = type === 'ability' ? this.#getAbilityId() : '';
let difficulty = 7;
let handicap = 0;
let targetLevel = null;
let targetLabel = '';
if (type === 'level') {
const current = actor.system?.level?.value || 1;
difficulty = actor.system?.members?.length || 0;
handicap = current;
targetLevel = current + 1;
} else if (type === 'reserve') {
const current = actor.system?.reserve?.value || 0;
difficulty = current;
handicap = 2;
targetLevel = current + 1;
} else {
const ability = key ? actor.items.get(key) : null;
if (ability) {
difficulty = ability.system?.learn?.threshold ?? 5;
handicap = ability.system?.level?.value ?? 1;
targetLabel = ability.name;
}
}
const labels = {
level: 'VERMINE.learn_level',
reserve: 'VERMINE.learn_group_reserve',
ability: 'VERMINE.learn_ability',
};
return {
type,
key,
targetLevel,
difficulty,
handicap,
poolModifier: 0,
label: game.i18n.localize(labels[type] || labels.level),
targetLabel,
};
}
#updateUI() {
const type = this.#getType();
const show = (id, visible) => {
const el = this.#el.querySelector(id);
if (el) el.style.display = visible ? '' : 'none';
};
show('#learning-level-section', type === 'level');
show('#learning-reserve-section', type === 'reserve');
show('#learning-ability-section', type === 'ability');
const c = this.#compute();
const set = (sel, txt) => {
const el = this.#el.querySelector(sel);
if (el) el.textContent = txt;
};
set('#diff-value', c.difficulty);
set('#handicap-value', c.handicap);
set('#required-value', 1 + c.handicap);
const pool = Math.max(0, this.#getDiceSpent() + c.poolModifier);
set('#pool-value', `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? '+' : ''}${c.poolModifier}D)` : ''}`);
const poolInput = this.#el.querySelector('#learning-dice');
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
}
static #onCancel() {
this.close();
}
/** Renvoie la clé i18n d'erreur si le jet est impossible, sinon null. */
#getError(c) {
const actor = this.#actor;
const ex = actor.system?.experience;
// Limite : un seul Niveau, une seule Réserve et une seule Capacité par phase.
if (ex?.learned?.phase === ex?.phase && ex.learned[c.type]) {
return 'VERMINE.error_learning_limit';
}
if (c.type === 'level' && c.targetLevel !== null && (actor.system?.level?.value || 0) >= 10) {
return 'VERMINE.error_group_level_max';
}
if (c.type === 'reserve' && c.targetLevel !== null &&
(actor.system?.reserve?.value || 0) >= (actor.system?.reserve?.max || 10)) {
return 'VERMINE.error_group_reserve_max';
}
if (c.type === 'ability') {
if (!c.key) {
return 'VERMINE.no_group_ability';
}
const ability = c.key ? actor.items.get(c.key) : null;
const requiredLevel = ability?.system?.level?.value ?? 1;
if (ability && (actor.system?.level?.value || 1) < requiredLevel) {
return 'VERMINE.error_group_level_requirement';
}
}
return null;
}
static async #onRoll() {
const actor = this.#actor;
const ex = actor.system?.experience;
const spent = Math.min(this.#getDiceSpent(), ex?.dice || 0);
if (spent < 1) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_experience_dice'));
return;
}
const c = this.#compute();
const errorKey = this.#getError(c);
if (errorKey) {
ui.notifications.warn(game.i18n.localize(errorKey));
return;
}
const pool = Math.max(0, spent + c.poolModifier);
const rollLabel = `${game.i18n.localize('VERMINE.open_learning')}${c.label}${c.targetLabel ? ` (${c.targetLabel})` : ''}`;
const roll = await VermineUtils.roll({
actor,
NoD: pool,
Reroll: 0,
difficulty: c.difficulty,
handicap: c.handicap,
rollLabel,
poolBreakdown: { ability: 0, skill: 0, selfControl: 0, specialty: 0, help: 0, tooling: 0, group: 0, total: pool },
messageFlags: { 'vermine-learning': { type: c.type, key: c.key, spent, success: false } },
});
const required = 1 + c.handicap;
if ((roll._total ?? 0) >= required) {
await this.#applySuccess(c);
}
// Les Dés d'Expérience sont perdus que le jet soit réussi ou raté.
await actor.update({ "system.experience.dice": Math.max(0, (actor.system.experience.dice || 0) - spent) });
this.close();
}
async #applySuccess(c) {
const actor = this.#actor;
const updates = {};
if (c.type === 'level') {
const current = actor.system.level.value || 1;
updates['system.level.value'] = Math.min(10, current + 1);
updates['system.experience.learned.level'] = true;
} else if (c.type === 'reserve') {
const current = actor.system.reserve.value || 0;
updates['system.reserve.value'] = Math.min(actor.system.reserve.max || 10, current + 1);
updates['system.experience.learned.reserve'] = true;
} else {
if (c.key) {
await actor.updateEmbeddedDocuments('Item', [{ _id: c.key, 'system.learned': true }]);
}
updates['system.experience.learned.ability'] = true;
}
updates['system.experience.learned.phase'] = actor.system.experience.phase;
await actor.update(updates);
ui.notifications.info(game.i18n.localize('VERMINE.learning_success'));
}
}