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:
@@ -64,6 +64,29 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
rarity: actor.system?.skills?.[key]?.rarity || 0,
|
||||
isDomain: VermineExperience.isPreferredDomain(actor, key)
|
||||
}));
|
||||
const totem = actor.system?.identity?.totem || "";
|
||||
const gameMode = this.#getGameMode();
|
||||
const groupLevel = this.#getGroupLevel();
|
||||
const totemAbilities = (actor.itemTypes?.ability || [])
|
||||
.filter(i =>
|
||||
i.system?.type === "character"
|
||||
&& !i.system?.learned
|
||||
&& i.system?.totem === totem
|
||||
&& (i.system?.level?.value || 0) <= gameMode
|
||||
&& groupLevel >= (i.system?.level?.value || 0)
|
||||
)
|
||||
.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
level: a.system.level.value,
|
||||
threshold: a.system.learn.threshold,
|
||||
hindrance: a.system.learn.hindrance
|
||||
}));
|
||||
const backgrounds = (actor.itemTypes?.background || [])
|
||||
.filter(i => !i.system?.learned)
|
||||
.map(b => ({ id: b.id, name: b.name, asterisk: b.system?.asterisk || false }));
|
||||
const human = actor.system?.adaptation?.totems?.human?.value || 0;
|
||||
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
|
||||
return {
|
||||
actor,
|
||||
system: actor.system,
|
||||
@@ -81,11 +104,44 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
],
|
||||
specialties: actor.itemTypes?.specialty || [],
|
||||
skills,
|
||||
specialtyCount: (actor.itemTypes?.specialty || []).length
|
||||
specialtyCount: (actor.itemTypes?.specialty || []).length,
|
||||
totem,
|
||||
gameMode,
|
||||
groupLevel,
|
||||
totemAbilities,
|
||||
backgrounds,
|
||||
reputation: actor.system?.attributes?.reputation?.value ?? 0,
|
||||
human,
|
||||
adapted,
|
||||
canUseAdaptedDie: VermineExperience.canUseAdaptedDie(human, adapted)
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
#getGameMode() {
|
||||
return parseInt(game.settings.get("vermine2047", "game-mode") || "1", 10) || 1;
|
||||
}
|
||||
|
||||
#getGroupLevel() {
|
||||
const groups = game.actors.filter(a => a.type === "group" && (a.system?.members || []).includes(this.#actor.id));
|
||||
return Math.max(0, ...groups.map(g => g.system?.level?.value || 0));
|
||||
}
|
||||
|
||||
#canUseAdaptedDie() {
|
||||
const a = this.#actor.system?.adaptation?.totems;
|
||||
return VermineExperience.canUseAdaptedDie(a?.human?.value || 0, a?.adapted?.value || 0);
|
||||
}
|
||||
|
||||
#totemCapacityCounts() {
|
||||
const items = (this.#actor.itemTypes?.ability || []).filter(i => i.system?.type === "character" && i.system?.learned);
|
||||
const byLevel = { 1: 0, 2: 0, 3: 0 };
|
||||
for (const i of items) {
|
||||
const level = i.system?.level?.value || 0;
|
||||
byLevel[level] = (byLevel[level] || 0) + 1;
|
||||
}
|
||||
return { total: items.length, byLevel };
|
||||
}
|
||||
|
||||
async _onRender() {
|
||||
this.element.dataset.actorId = this.#actor.id;
|
||||
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||
inp.addEventListener("change", () => this.#updateUI());
|
||||
@@ -104,9 +160,19 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
const type = this.#getType();
|
||||
if (type === "characteristic") return this.#el.querySelector("#learning-ability")?.value ?? "";
|
||||
if (type === "reserve") return this.#el.querySelector("#learning-reserve")?.value ?? "";
|
||||
if (type === "totem") return this.#el.querySelector("#learning-totem")?.value ?? "";
|
||||
if (type === "background") return this.#el.querySelector("#learning-background")?.value ?? "";
|
||||
return this.#el.querySelector("#learning-target")?.value ?? "";
|
||||
}
|
||||
|
||||
#getReputationDirection() {
|
||||
return this.#el.querySelector("#learning-reputation-direction")?.value ?? "reduce";
|
||||
}
|
||||
|
||||
#isAdaptedDieChecked() {
|
||||
return Boolean(this.#el.querySelector("#learning-adapted-die")?.checked);
|
||||
}
|
||||
|
||||
#getDiceSpent() {
|
||||
return Math.max(1, parseInt(this.#el.querySelector("#learning-dice")?.value, 10) || 1);
|
||||
}
|
||||
@@ -119,9 +185,6 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
const actor = this.#actor;
|
||||
const type = this.#getType();
|
||||
const key = this.#getTargetKey();
|
||||
const difficulty = 7;
|
||||
let handicap = 0;
|
||||
let poolModifier = 0;
|
||||
let label = "";
|
||||
let targetLevel = null;
|
||||
|
||||
@@ -182,10 +245,71 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
difficulty: VermineExperience.reserveLearningDifficulty(current),
|
||||
handicap: VermineExperience.ageHandicap(actor.system?.identity?.ageType || 2),
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label,
|
||||
targetLabel: key ? game.i18n.localize(key === "self_control" ? "VERMINE.self_control" : "VERMINE.effort") : ""
|
||||
};
|
||||
}
|
||||
if (type === "totem") {
|
||||
const item = actor.itemTypes?.ability?.find(i => i.id === key);
|
||||
const data = item?.system;
|
||||
label = game.i18n.localize("VERMINE.learn_totem_ability");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: data?.level?.value || 0,
|
||||
difficulty: VermineExperience.totemAbilityLearningDifficulty(data),
|
||||
handicap: VermineExperience.totemAbilityLearningHandicap(data),
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label,
|
||||
targetLabel: item?.name || ""
|
||||
};
|
||||
}
|
||||
if (type === "background") {
|
||||
const item = actor.itemTypes?.background?.find(i => i.id === key);
|
||||
const data = item?.system;
|
||||
label = game.i18n.localize("VERMINE.learn_background");
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
targetLevel: 0,
|
||||
difficulty: VermineExperience.backgroundLearningDifficulty(),
|
||||
handicap: VermineExperience.backgroundLearningHandicap(data?.asterisk),
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label,
|
||||
targetLabel: item?.name || ""
|
||||
};
|
||||
}
|
||||
if (type === "reputation") {
|
||||
const current = actor.system?.attributes?.reputation?.value ?? 0;
|
||||
const direction = this.#getReputationDirection();
|
||||
if (direction === "increase") {
|
||||
return {
|
||||
type,
|
||||
key: "increase",
|
||||
targetLevel: current,
|
||||
difficulty: VermineExperience.reputationIncreaseDifficulty(current),
|
||||
handicap: 0,
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label: game.i18n.localize("VERMINE.learn_reputation"),
|
||||
targetLabel: game.i18n.localize("VERMINE.reputation_increase")
|
||||
};
|
||||
}
|
||||
return {
|
||||
type,
|
||||
key: "reduce",
|
||||
targetLevel: Math.max(0, current - 1),
|
||||
difficulty: VermineExperience.reputationReduceDifficulty(current),
|
||||
handicap: 0,
|
||||
poolModifier: 0,
|
||||
adaptedDie: false,
|
||||
label: game.i18n.localize("VERMINE.learn_reputation"),
|
||||
targetLabel: game.i18n.localize("VERMINE.reputation_reduce")
|
||||
};
|
||||
}
|
||||
// evolution
|
||||
const human = actor.system?.adaptation?.totems?.human?.value || 0;
|
||||
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
|
||||
@@ -197,6 +321,7 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
difficulty: VermineExperience.evolutionDifficulty(actor.system?.identity?.ageType || 2),
|
||||
handicap: actor.system?.experience?.evolution || 0,
|
||||
poolModifier: VermineExperience.evolutionTotemModifier(human, adapted),
|
||||
adaptedDie: this.#canUseAdaptedDie() && this.#isAdaptedDieChecked(),
|
||||
label,
|
||||
targetLabel: ""
|
||||
};
|
||||
@@ -213,6 +338,10 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
show("#learning-ability-section", type === "characteristic");
|
||||
show("#learning-reserve-section", type === "reserve");
|
||||
show("#learning-evolution-section", type === "evolution");
|
||||
show("#learning-totem-section", type === "totem");
|
||||
show("#learning-background-section", type === "background");
|
||||
show("#learning-reputation-section", type === "reputation");
|
||||
show("#learning-adapted-die-row", type === "evolution" && this.#canUseAdaptedDie());
|
||||
|
||||
// Pour une Spécialité, seule une compétence au moins Confirmée (≥2) est valable.
|
||||
const skillSelect = this.#el.querySelector("#learning-target");
|
||||
@@ -229,16 +358,25 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
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)` : ""}`);
|
||||
let poolText = `${pool}D${c.poolModifier ? ` (${c.poolModifier >= 0 ? "+" : ""}${c.poolModifier}D)` : ""}`;
|
||||
if (c.adaptedDie) poolText += " — " + game.i18n.localize("VERMINE.adapted_die_x2");
|
||||
set("#pool-value", poolText);
|
||||
if (type === "reputation") {
|
||||
const current = this.#actor.system?.attributes?.reputation?.value ?? 0;
|
||||
const direction = this.#getReputationDirection();
|
||||
set("#reputation-target", direction === "increase"
|
||||
? `${current} → ${Math.min(10, current + 1)}+`
|
||||
: `${current} → ${Math.max(0, current - 1)}`);
|
||||
}
|
||||
const poolInput = this.#el.querySelector("#learning-dice");
|
||||
if (poolInput) poolInput.max = this.#actor.system?.experience?.dice || 0;
|
||||
}
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
static async #onCancel() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onRoll(event, target) {
|
||||
static async #onRoll() {
|
||||
const actor = this.#actor;
|
||||
const ex = actor.system?.experience;
|
||||
const dice = ex?.dice || 0;
|
||||
@@ -250,10 +388,11 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
|
||||
const c = this.#compute();
|
||||
|
||||
// Limite : une seule compétence, spécialité, réserve, caractéristique et
|
||||
// un seul Dé d'Évolution par phase.
|
||||
// Limite : une seule compétence, spécialité, réserve, caractéristique,
|
||||
// un seul Dé d'Évolution, Capacité de Totem, Historique ou jet de
|
||||
// Réputation par phase.
|
||||
if (ex?.learned?.phase === ex?.phase) {
|
||||
const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution" };
|
||||
const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution", totem: "ability", background: "background", reputation: "reputation" };
|
||||
if (ex.learned[flagMap[c.type]]) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_learning_limit"));
|
||||
return;
|
||||
@@ -275,6 +414,61 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_reserve_max"));
|
||||
return;
|
||||
}
|
||||
if (c.type === "totem") {
|
||||
const item = actor.itemTypes?.ability?.find(i => i.id === c.key);
|
||||
if (!item || item.system?.learned) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.no_totem_ability"));
|
||||
return;
|
||||
}
|
||||
const level = item.system?.level?.value || 0;
|
||||
if (level > this.#getGameMode()) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_mode"));
|
||||
return;
|
||||
}
|
||||
if (item.system?.totem !== actor.system?.identity?.totem) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_totem"));
|
||||
return;
|
||||
}
|
||||
if (this.#getGroupLevel() < level) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_group_level"));
|
||||
return;
|
||||
}
|
||||
const counts = this.#totemCapacityCounts();
|
||||
if (level === 1 && (counts.byLevel[1] || 0) >= 3) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
|
||||
return;
|
||||
}
|
||||
if (level >= 2 && (counts.byLevel[level] || 0) >= 1) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
|
||||
return;
|
||||
}
|
||||
if (counts.total >= 5) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_totem_limit"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "background") {
|
||||
const item = actor.itemTypes?.background?.find(i => i.id === c.key);
|
||||
if (!item || item.system?.learned) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.no_background"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "reputation") {
|
||||
const current = actor.system?.attributes?.reputation?.value ?? 0;
|
||||
if (c.key === "reduce" && current <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_reputation_min"));
|
||||
return;
|
||||
}
|
||||
if (c.key === "increase" && current >= 10) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_reputation_max"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (c.type === "evolution" && c.adaptedDie && !this.#canUseAdaptedDie()) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_evolution_no_adapted"));
|
||||
return;
|
||||
}
|
||||
|
||||
const pool = Math.max(0, spent + c.poolModifier);
|
||||
const rollLabel = `${game.i18n.localize("VERMINE.open_learning")} — ${c.label}${c.targetLabel ? " (" + c.targetLabel + ")" : ""}`;
|
||||
@@ -285,6 +479,7 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
Reroll: 0,
|
||||
difficulty: c.difficulty,
|
||||
handicap: c.handicap,
|
||||
totems: c.adaptedDie ? { adapted: true } : { human: false, adapted: false },
|
||||
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 } }
|
||||
@@ -293,14 +488,16 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
const required = 1 + c.handicap;
|
||||
const specialtyName = this.#getSpecialtyName();
|
||||
if ((roll._total ?? 0) >= required) {
|
||||
await this.#applySuccess(c, spent, specialtyName);
|
||||
await this.#applySuccess(c, specialtyName, roll._total ?? 0);
|
||||
}
|
||||
// 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, spent, specialtyName) {
|
||||
async #applySuccess(c, specialtyName, successes = 0) {
|
||||
const actor = this.#actor;
|
||||
const updates = { "system.experience.dice": (actor.system.experience.dice || 0) - spent };
|
||||
const updates = {};
|
||||
if (c.type === "skill") {
|
||||
const current = actor.system.skills[c.key].value || 0;
|
||||
updates["system.skills." + c.key + ".value"] = Math.min(5, current + 1);
|
||||
@@ -316,9 +513,23 @@ export default class LearningDialog extends HandlebarsApplicationMixin(foundry.a
|
||||
const bonus = actor.system.experience.reserveBonus?.[c.key] || 0;
|
||||
updates["system.experience.reserveBonus." + c.key] = bonus + 1;
|
||||
updates["system.experience.learned.reserve"] = true;
|
||||
} else {
|
||||
} else if (c.type === "evolution") {
|
||||
updates["system.experience.evolution"] = (actor.system.experience.evolution || 0) + 1;
|
||||
updates["system.experience.learned.evolution"] = true;
|
||||
} else if (c.type === "totem") {
|
||||
updates["system.experience.learned.ability"] = true;
|
||||
await actor.updateEmbeddedDocuments("Item", [{ _id: c.key, "system.learned": true }]);
|
||||
} else if (c.type === "background") {
|
||||
updates["system.experience.learned.background"] = true;
|
||||
await actor.updateEmbeddedDocuments("Item", [{ _id: c.key, "system.learned": true }]);
|
||||
} else if (c.type === "reputation") {
|
||||
const current = actor.system?.attributes?.reputation?.value ?? 0;
|
||||
if (c.key === "increase") {
|
||||
updates["system.attributes.reputation.value"] = Math.min(10, current + successes);
|
||||
} else {
|
||||
updates["system.attributes.reputation.value"] = Math.max(0, current - 1);
|
||||
}
|
||||
updates["system.experience.learned.reputation"] = true;
|
||||
}
|
||||
updates["system.experience.learned.phase"] = actor.system.experience.phase;
|
||||
await actor.update(updates);
|
||||
|
||||
Reference in New Issue
Block a user