Release Creation / build (release) Failing after 1m20s
- phase d'expérience (récompenses collectives et individuelles, objectifs, traumatisme) - jets d'apprentissage (compétences, spécialités, caractéristiques, réserves, évolution) - bonus de réserve acquis par apprentissage intégré au max dérivé - transferts de dés individuels <-> groupe dans le dialogue de jet - onglet expérience sur la fiche personnage - guide utilisateur combat (docs/user/COMBAT.md)
328 lines
12 KiB
JavaScript
328 lines
12 KiB
JavaScript
import { VermineUtils } from "../roll.mjs";
|
|
import { VermineExperience } from "../experience.mjs";
|
|
|
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
|
|
|
/**
|
|
* Dialogue d'apprentissage (phase d'Expérience).
|
|
* Permet de dépenser des Dés d'Expérience pour tenter de développer une
|
|
* Compétence, une Spécialité, une Caractéristique, une Réserve ou un
|
|
* Dé d'Évolution. La progression est appliquée automatiquement en cas de
|
|
* réussite.
|
|
*/
|
|
export default class LearningDialog 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: 640
|
|
},
|
|
actions: {
|
|
roll: LearningDialog.#onRoll,
|
|
cancel: LearningDialog.#onCancel
|
|
}
|
|
};
|
|
|
|
static PARTS = {
|
|
main: { template: "systems/vermine2047/templates/dialogs/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 LearningDialog({ actor });
|
|
}
|
|
|
|
constructor(options = {}) {
|
|
super(options);
|
|
this.#actor = options.actor;
|
|
}
|
|
|
|
get title() {
|
|
return game.i18n.localize("VERMINE.open_learning");
|
|
}
|
|
|
|
async _prepareContext() {
|
|
const actor = this.#actor;
|
|
const skills = Object.entries(CONFIG.VERMINE.skills).map(([key, cfg]) => ({
|
|
key,
|
|
name: game.i18n.localize("SKILLS." + key + ".name"),
|
|
category: cfg.category,
|
|
value: actor.system?.skills?.[key]?.value || 0,
|
|
target: Math.min(5, (actor.system?.skills?.[key]?.value || 0) + 1),
|
|
rarity: actor.system?.skills?.[key]?.rarity || 0,
|
|
isDomain: VermineExperience.isPreferredDomain(actor, key)
|
|
}));
|
|
return {
|
|
actor,
|
|
system: actor.system,
|
|
config: CONFIG.VERMINE,
|
|
dice: actor.system?.experience?.dice || 0,
|
|
ageType: actor.system?.identity?.ageType || 2,
|
|
abilities: Object.entries(CONFIG.VERMINE.abilities).map(([key, label]) => ({
|
|
key,
|
|
label: game.i18n.localize(label),
|
|
value: actor.system?.abilities?.[key]?.value || 0
|
|
})),
|
|
reserves: [
|
|
{ key: "self_control", label: game.i18n.localize("VERMINE.self_control"), value: actor.system?.attributes?.self_control?.value || 0, max: actor.system?.attributes?.self_control?.max || 0 },
|
|
{ key: "effort", label: game.i18n.localize("VERMINE.effort"), value: actor.system?.attributes?.effort?.value || 0, max: actor.system?.attributes?.effort?.max || 0 }
|
|
],
|
|
specialties: actor.itemTypes?.specialty || [],
|
|
skills,
|
|
specialtyCount: (actor.itemTypes?.specialty || []).length
|
|
};
|
|
}
|
|
|
|
async _onRender(context, options) {
|
|
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 ?? "skill";
|
|
}
|
|
|
|
#getTargetKey() {
|
|
const type = this.#getType();
|
|
if (type === "characteristic") return this.#el.querySelector("#learning-ability")?.value ?? "";
|
|
if (type === "reserve") return this.#el.querySelector("#learning-reserve")?.value ?? "";
|
|
return this.#el.querySelector("#learning-target")?.value ?? "";
|
|
}
|
|
|
|
#getDiceSpent() {
|
|
return Math.max(1, parseInt(this.#el.querySelector("#learning-dice")?.value, 10) || 1);
|
|
}
|
|
|
|
#getSpecialtyName() {
|
|
return this.#el.querySelector("#specialty-name")?.value?.trim() || game.i18n.localize("ITEMS.new_specialty");
|
|
}
|
|
|
|
#compute() {
|
|
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;
|
|
|
|
if (type === "skill") {
|
|
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
|
|
targetLevel = Math.min(5, current + 1);
|
|
const rarity = key ? actor.system?.skills?.[key]?.rarity || 0 : 0;
|
|
label = game.i18n.localize(VermineExperience.skillLevelLabelKey(targetLevel));
|
|
const diff = VermineExperience.skillLearningDifficulty(targetLevel);
|
|
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
|
|
return {
|
|
type,
|
|
key,
|
|
targetLevel,
|
|
difficulty: domain ? Math.max(3, diff - 2) : diff,
|
|
handicap: VermineExperience.skillLearningHandicap(targetLevel, rarity),
|
|
poolModifier: 0,
|
|
label,
|
|
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
|
|
};
|
|
}
|
|
if (type === "specialty") {
|
|
const current = key ? actor.system?.skills?.[key]?.value || 0 : 0;
|
|
const domain = key ? VermineExperience.isPreferredDomain(actor, key) : false;
|
|
label = game.i18n.localize("VERMINE.specialty");
|
|
return {
|
|
type,
|
|
key,
|
|
targetLevel: current,
|
|
difficulty: VermineExperience.specialtyLearningDifficulty(current, domain),
|
|
handicap: VermineExperience.specialtyLearningHandicap(actor.itemTypes?.specialty?.length || 0),
|
|
poolModifier: 0,
|
|
label,
|
|
targetLabel: key ? game.i18n.localize("SKILLS." + key + ".name") : ""
|
|
};
|
|
}
|
|
if (type === "characteristic") {
|
|
const current = key ? actor.system?.abilities?.[key]?.value || 0 : 0;
|
|
label = game.i18n.localize("VERMINE.characteristic");
|
|
return {
|
|
type,
|
|
key,
|
|
targetLevel: current,
|
|
difficulty: VermineExperience.characteristicLearningDifficulty(),
|
|
handicap: current,
|
|
poolModifier: 0,
|
|
label,
|
|
targetLabel: key ? game.i18n.localize(CONFIG.VERMINE.abilities[key]) : ""
|
|
};
|
|
}
|
|
if (type === "reserve") {
|
|
const current = key ? actor.system?.attributes?.[key]?.value || 0 : 0;
|
|
label = game.i18n.localize("VERMINE.reserve");
|
|
return {
|
|
type,
|
|
key,
|
|
targetLevel: current,
|
|
difficulty: VermineExperience.reserveLearningDifficulty(current),
|
|
handicap: VermineExperience.ageHandicap(actor.system?.identity?.ageType || 2),
|
|
poolModifier: 0,
|
|
label,
|
|
targetLabel: key ? game.i18n.localize(key === "self_control" ? "VERMINE.self_control" : "VERMINE.effort") : ""
|
|
};
|
|
}
|
|
// evolution
|
|
const human = actor.system?.adaptation?.totems?.human?.value || 0;
|
|
const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0;
|
|
label = game.i18n.localize("VERMINE.evolution_dice");
|
|
return {
|
|
type,
|
|
key,
|
|
targetLevel: actor.system?.experience?.evolution || 0,
|
|
difficulty: VermineExperience.evolutionDifficulty(actor.system?.identity?.ageType || 2),
|
|
handicap: actor.system?.experience?.evolution || 0,
|
|
poolModifier: VermineExperience.evolutionTotemModifier(human, adapted),
|
|
label,
|
|
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-skill-section", type === "skill" || type === "specialty");
|
|
show("#learning-specialty-section", type === "specialty");
|
|
show("#learning-ability-section", type === "characteristic");
|
|
show("#learning-reserve-section", type === "reserve");
|
|
show("#learning-evolution-section", type === "evolution");
|
|
|
|
// Pour une Spécialité, seule une compétence au moins Confirmée (≥2) est valable.
|
|
const skillSelect = this.#el.querySelector("#learning-target");
|
|
if (skillSelect) {
|
|
for (const opt of skillSelect.options) {
|
|
const value = parseInt(opt.dataset.value, 10) || 0;
|
|
opt.hidden = (type === "specialty") ? value < 2 : false;
|
|
}
|
|
}
|
|
|
|
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 async #onCancel(event, target) {
|
|
this.close();
|
|
}
|
|
|
|
static async #onRoll(event, target) {
|
|
const actor = this.#actor;
|
|
const ex = actor.system?.experience;
|
|
const dice = ex?.dice || 0;
|
|
const spent = Math.min(this.#getDiceSpent(), dice);
|
|
if (spent < 1) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
|
|
return;
|
|
}
|
|
|
|
const c = this.#compute();
|
|
|
|
// Limite : une seule compétence, spécialité, réserve, caractéristique et
|
|
// un seul Dé d'Évolution par phase.
|
|
if (ex?.learned?.phase === ex?.phase) {
|
|
const flagMap = { skill: "skill", specialty: "specialty", reserve: "reserve", characteristic: "characteristic", evolution: "evolution" };
|
|
if (ex.learned[flagMap[c.type]]) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_learning_limit"));
|
|
return;
|
|
}
|
|
}
|
|
if (c.type === "skill" && c.targetLevel !== null && (actor.system?.skills?.[c.key]?.value || 0) >= 5) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_skill_max"));
|
|
return;
|
|
}
|
|
if (c.type === "characteristic" && c.targetLevel !== null && (actor.system?.abilities?.[c.key]?.value || 0) >= 3) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_characteristic_max"));
|
|
return;
|
|
}
|
|
if (c.type === "specialty" && c.targetLevel !== null && c.targetLevel < 2) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_specialty_requirement"));
|
|
return;
|
|
}
|
|
if (c.type === "reserve" && c.targetLevel !== null && (actor.system?.attributes?.[c.key]?.max || 0) >= 10) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_reserve_max"));
|
|
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;
|
|
const specialtyName = this.#getSpecialtyName();
|
|
if ((roll._total ?? 0) >= required) {
|
|
await this.#applySuccess(c, spent, specialtyName);
|
|
}
|
|
this.close();
|
|
}
|
|
|
|
async #applySuccess(c, spent, specialtyName) {
|
|
const actor = this.#actor;
|
|
const updates = { "system.experience.dice": (actor.system.experience.dice || 0) - spent };
|
|
if (c.type === "skill") {
|
|
const current = actor.system.skills[c.key].value || 0;
|
|
updates["system.skills." + c.key + ".value"] = Math.min(5, current + 1);
|
|
updates["system.experience.learned.skill"] = true;
|
|
} else if (c.type === "specialty") {
|
|
await actor.createEmbeddedDocuments("Item", [{ name: specialtyName, type: "specialty", system: { skill: c.key } }]);
|
|
updates["system.experience.learned.specialty"] = true;
|
|
} else if (c.type === "characteristic") {
|
|
const current = actor.system.abilities[c.key].value || 0;
|
|
updates["system.abilities." + c.key + ".value"] = Math.min(3, current + 1);
|
|
updates["system.experience.learned.characteristic"] = true;
|
|
} else if (c.type === "reserve") {
|
|
const bonus = actor.system.experience.reserveBonus?.[c.key] || 0;
|
|
updates["system.experience.reserveBonus." + c.key] = bonus + 1;
|
|
updates["system.experience.learned.reserve"] = true;
|
|
} else {
|
|
updates["system.experience.evolution"] = (actor.system.experience.evolution || 0) + 1;
|
|
updates["system.experience.learned.evolution"] = true;
|
|
}
|
|
updates["system.experience.learned.phase"] = actor.system.experience.phase;
|
|
await actor.update(updates);
|
|
ui.notifications.info(game.i18n.localize("VERMINE.learning_success"));
|
|
}
|
|
}
|