feat: nouveaux apprentissages (Capacités de Totem, Historique, Réputation) et expérience de Groupe
- Dialogue d'apprentissage personnage : 4 nouveaux types d'achat.
- Capacité de Totem : options filtrées (mode de jeu, niveau de
groupe, totem, non-apprises), difficulté = valeur d'Apprentissage,
limites 3×N1 / 1×N2-N3 / max 5, marque system.learned de l'item
(convention type character) et learned.ability du personnage.
- Élément d'Historique : difficulté 9, handicap 1 si astérisque
(background.asterisk), marque learned.background + item.
- Test de Réputation (réduire/augmenter) : réduire = difficulté
10 − score visé (−1 pt), augmenter = difficulté = réputation
actuelle (+1 par succès), bornes 0-10, marque learned.reputation.
- Évolution : option Dé de Totem Adapté ×2 quand Adapté > Humain
(1D remplacé par un dé adapté compté ×2 via VermineUtils.roll).
- Correction règles : les Dés d'Expérience sont dépensés même en cas
d'échec du jet d'apprentissage (décompte hors #applySuccess).
- Expérience de Groupe : nouveau dialogue d'apprentissage de Groupe
(niveau, réserve, capacité de totem), réserve d'expérience et phase
dans system.experience, boutons sur la fiche Groupe, reset des
limites par phase (level/reserve/ability).
- Phase d'expérience : filtre par groupe sélectionné, associe chaque
personnage à son premier groupe, reset des limites de tous les
nouveaux types d'apprentissage.
- Corrections : l'attaque de créature n'ajoute plus la Réaction du Rôle
(regles_creatures.txt), suppression de la case XP des en-têtes PJ
(legacy et AppV2).
- Divers : .eslintrc.js (règles inexistantes/obsolètes retirées),
libellé jaune de dialogue en couleur chitine, i18n FR/EN, chips
« appris » sur l'onglet expérience du personnage.
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
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') {
|
||||
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, spent);
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
|
||||
async #applySuccess(c, spent) {
|
||||
const actor = this.#actor;
|
||||
const updates = { 'system.experience.dice': (actor.system.experience.dice || 0) - spent };
|
||||
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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user