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')); } }