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