Release Creation / build (release) Failing after 1m40s
- 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.
255 lines
8.1 KiB
JavaScript
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'));
|
|
}
|
|
}
|