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:
+3
-7
@@ -86,7 +86,6 @@ module.exports = {
|
||||
'default-case': 'warn',
|
||||
'default-case-last': 'warn',
|
||||
'default-param-last': 'warn',
|
||||
'dot-locale-compare': 'warn',
|
||||
'dot-notation': ['warn', { allowKeywords: true }],
|
||||
'eqeqeq': ['error', 'always', { null: 'ignore' }],
|
||||
'func-name-matching': 'warn',
|
||||
@@ -123,7 +122,6 @@ module.exports = {
|
||||
'no-delete-var': 'error',
|
||||
'no-div-regex': 'warn',
|
||||
'no-else-return': ['warn', { allowElseIf: true }],
|
||||
'no-empty-destructuring': 'warn',
|
||||
'no-empty-function': ['warn', { allow: ['constructors'] }],
|
||||
'no-empty-pattern': 'warn',
|
||||
'no-eq-null': 'off',
|
||||
@@ -153,7 +151,6 @@ module.exports = {
|
||||
'no-new-func': 'warn',
|
||||
'no-new-wrappers': 'error',
|
||||
'no-nonoctal-decimal-escape': 'error',
|
||||
'no-object-multi-space': 'warn',
|
||||
'no-octal-escape': 'error',
|
||||
'no-param-reassign': ['warn', { props: false }],
|
||||
'no-plusplus': 'off',
|
||||
@@ -267,10 +264,9 @@ module.exports = {
|
||||
VariableDeclarator: { var: 2, let: 2, const: 3 },
|
||||
outerIIFEBody: 1,
|
||||
MemberExpression: 'off',
|
||||
FunctionDeclaration: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } },
|
||||
FunctionExpression: { body: 1, parameters: 1, parameters: { var: 2, let: 2, const: 3 } },
|
||||
StaticBlock: { body: 1 },
|
||||
ClassBody: 1
|
||||
FunctionDeclaration: { body: 1, parameters: 1 },
|
||||
FunctionExpression: { body: 1, parameters: 1 },
|
||||
StaticBlock: { body: 1 }
|
||||
}],
|
||||
'jsx-quotes': 'off',
|
||||
'key-spacing': ['warn', { beforeColon: false, afterColon: true, mode: 'strict' }],
|
||||
|
||||
@@ -381,6 +381,9 @@
|
||||
"learned_reserve": "Reserve",
|
||||
"learned_characteristic": "Characteristic",
|
||||
"learned_evolution": "Evolution",
|
||||
"learned_ability": "Totem capacity",
|
||||
"learned_background": "Background",
|
||||
"learned_reputation": "Reputation",
|
||||
"no_character": "No character in this world.",
|
||||
"apply": "Apply",
|
||||
"characteristic": "Characteristic",
|
||||
@@ -389,11 +392,22 @@
|
||||
"learn_characteristic": "Characteristic",
|
||||
"learn_reserve": "Reserve",
|
||||
"learn_evolution": "Evolution die",
|
||||
"learn_totem_ability": "Totem capacity",
|
||||
"learn_background": "Background element",
|
||||
"learn_reputation": "Reputation",
|
||||
"learn_adapted_die": "Replace 1 Experience die with an Adapted totem die (×2)",
|
||||
"adapted_die_x2": "1 Adapted totem die ×2",
|
||||
"learn_specialty_name": "Specialty name",
|
||||
"specialty_hint": "Prerequisite: skill at least Proficient (≥2).",
|
||||
"characteristic_hint": "Difficulty 9 + Handicap = current value. Max 3 (except Superhuman).",
|
||||
"reserve_hint": "Difficulty = current value + age Handicap. Max 10.",
|
||||
"evolution_hint": "Difficulty by age + Handicap = accumulated Evolution dice. Adapted totem +1D, Human -1D.",
|
||||
"totem_ability_hint": "Prerequisites: same Totem as the character, Group level ≥ Capacity level, game mode compatible (Survival N1, Nightmare N2, Apocalypse N3).",
|
||||
"background_hint": "Difficulty 9. Handicap (I) if the Background is marked with an asterisk (*). GM authorization required.",
|
||||
"reputation_hint": "Reduce = more famous (Difficulty 10 − target score, 1 pt max per phase). Increase = fade into obscurity (requires a major Objective, Difficulty = current Reputation, +1 per Success).",
|
||||
"reputation_reduce": "Become more famous (reduce)",
|
||||
"reputation_increase": "Fade into obscurity (increase)",
|
||||
"reputation_current": "Current Reputation",
|
||||
"dice_spent": "Dice spent",
|
||||
"learning_success": "Learning successful!",
|
||||
"error_no_experience_dice": "No Experience dice available.",
|
||||
@@ -402,6 +416,25 @@
|
||||
"error_characteristic_max": "Characteristic already at maximum (3).",
|
||||
"error_specialty_requirement": "Specialty requires: skill at least Proficient (≥2).",
|
||||
"error_reserve_max": "Reserve already at maximum (10).",
|
||||
"no_totem_ability": "No Totem capacity available to learn.",
|
||||
"no_background": "No background element to acquire.",
|
||||
"error_totem_mode": "Capacity level incompatible with the game mode.",
|
||||
"error_totem_totem": "The character must share the Totem of the Capacity.",
|
||||
"error_totem_group_level": "Group level insufficient for this Capacity.",
|
||||
"error_totem_limit": "Totem capacity limit reached (3× Level 1, 1× Levels 2 and 3, max 5).",
|
||||
"error_reputation_min": "Reputation already at minimum (0).",
|
||||
"error_reputation_max": "Reputation already at maximum (10).",
|
||||
"error_evolution_no_adapted": "The character must have strictly more Adapted than Human totem dice.",
|
||||
"learn_level": "Group level",
|
||||
"learn_group_reserve": "Group reserve",
|
||||
"learn_ability": "Group capacity",
|
||||
"group_level_hint": "Difficulty = number of members. Handicap = current Group level. Max 10.",
|
||||
"group_reserve_hint": "Difficulty = current Reserve value. Handicap II. Max 10.",
|
||||
"group_ability_hint": "Difficulty = Learning. Handicap = Capacity level. Requires Group level ≥ Capacity level.",
|
||||
"no_group_ability": "No Group capacity available to develop.",
|
||||
"error_group_level_max": "Group level already at maximum (10).",
|
||||
"error_group_reserve_max": "Group reserve already at maximum.",
|
||||
"error_group_level_requirement": "Group level insufficient for this Capacity.",
|
||||
"experience_transfer": "Experience dice",
|
||||
"experience_pull": "Pull from group",
|
||||
"experience_give": "Give 1D to group",
|
||||
|
||||
@@ -364,6 +364,9 @@
|
||||
"learned_reserve": "Réserve",
|
||||
"learned_characteristic": "Caractéristique",
|
||||
"learned_evolution": "Évolution",
|
||||
"learned_ability": "Capacité de Totem",
|
||||
"learned_background": "Historique",
|
||||
"learned_reputation": "Réputation",
|
||||
"no_character": "Aucun personnage dans ce monde.",
|
||||
"apply": "Appliquer",
|
||||
"characteristic": "Caractéristique",
|
||||
@@ -372,11 +375,22 @@
|
||||
"learn_characteristic": "Caractéristique",
|
||||
"learn_reserve": "Réserve",
|
||||
"learn_evolution": "Dé d'Évolution",
|
||||
"learn_totem_ability": "Capacité de Totem",
|
||||
"learn_background": "Élément d'Historique",
|
||||
"learn_reputation": "Réputation",
|
||||
"learn_adapted_die": "Remplacer 1D d'Expérience par un Dé de Totem Adapté (×2)",
|
||||
"adapted_die_x2": "1 Dé de Totem Adapté ×2",
|
||||
"learn_specialty_name": "Nom de la spécialité",
|
||||
"specialty_hint": "Prérequis : compétence au moins Confirmée (≥2).",
|
||||
"characteristic_hint": "Difficulté 9 + Handicap = valeur actuelle. Max 3 (Surhumain excepté).",
|
||||
"reserve_hint": "Difficulté = valeur actuelle + Handicap d'âge. Max 10.",
|
||||
"evolution_hint": "Difficulté selon l'âge + Handicap = Dés d'Évolution accumulés. Totem Adapté +1D, Humain -1D.",
|
||||
"totem_ability_hint": "Prérequis : même Totem que le personnage, Niveau de Groupe ≥ Niveau de la Capacité, Mode de jeu compatible (Survie N1, Cauchemar N2, Apocalypse N3).",
|
||||
"background_hint": "Difficulté 9. Handicap (I) si l'Historique est marqué d'un astérisque (*). Autorisation du MJ requise.",
|
||||
"reputation_hint": "Réduire = plus célèbre (Difficulté 10 − score visé, 1 pt max/phase). Augmenter = se faire oublier (exige un Objectif majeur, Difficulté = Réputation actuelle, +1 par Réussite).",
|
||||
"reputation_reduce": "Devenir plus célèbre (réduire)",
|
||||
"reputation_increase": "Se faire oublier (augmenter)",
|
||||
"reputation_current": "Réputation actuelle",
|
||||
"dice_spent": "Dés dépensés",
|
||||
"learning_success": "Apprentissage réussi !",
|
||||
"error_no_experience_dice": "Pas de Dés d'Expérience disponibles.",
|
||||
@@ -385,6 +399,25 @@
|
||||
"error_characteristic_max": "Caractéristique déjà au maximum (3).",
|
||||
"error_specialty_requirement": "Spécialité requise : compétence au moins Confirmée (≥2).",
|
||||
"error_reserve_max": "Réserve déjà au maximum (10).",
|
||||
"no_totem_ability": "Aucune Capacité de Totem disponible à apprendre.",
|
||||
"no_background": "Aucun élément d'Historique à acquérir.",
|
||||
"error_totem_mode": "Niveau de Capacité incompatible avec le Mode de jeu.",
|
||||
"error_totem_totem": "Le personnage doit être du même Totem que la Capacité.",
|
||||
"error_totem_group_level": "Niveau de Groupe insuffisant pour cette Capacité.",
|
||||
"error_totem_limit": "Limite de Capacités de Totem atteinte (3× Niveau 1, 1× Niveaux 2 et 3, max 5).",
|
||||
"error_reputation_min": "Réputation déjà au minimum (0).",
|
||||
"error_reputation_max": "Réputation déjà au maximum (10).",
|
||||
"error_evolution_no_adapted": "Il faut strictement plus de Dés du Totem Adapté que Humains.",
|
||||
"learn_level": "Niveau de Groupe",
|
||||
"learn_group_reserve": "Réserve de Groupe",
|
||||
"learn_ability": "Capacité de Groupe",
|
||||
"group_level_hint": "Difficulté = nombre de membres. Handicap = Niveau actuel du Groupe. Max 10.",
|
||||
"group_reserve_hint": "Difficulté = valeur actuelle de la Réserve. Handicap II. Max 10.",
|
||||
"group_ability_hint": "Difficulté = Apprentissage. Handicap = Niveau de la Capacité. Nécessite un Niveau de Groupe ≥ Niveau de la Capacité.",
|
||||
"no_group_ability": "Aucune Capacité de Groupe disponible à développer.",
|
||||
"error_group_level_max": "Niveau de Groupe déjà au maximum (10).",
|
||||
"error_group_reserve_max": "Réserve de Groupe déjà au maximum.",
|
||||
"error_group_level_requirement": "Niveau de Groupe insuffisant pour cette Capacité.",
|
||||
"experience_transfer": "Dés d'Expérience",
|
||||
"experience_pull": "Puiser du Groupe",
|
||||
"experience_give": "Donner 1D au Groupe",
|
||||
|
||||
+1
-1
@@ -273,7 +273,7 @@ iframe {
|
||||
|
||||
label {
|
||||
font-size: @font-size-11;
|
||||
color: @color-amber;
|
||||
color: @color-chitin-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
|
||||
deleteObjective: VermineGroupSheetV2.#onDeleteObjective,
|
||||
addObjective: VermineGroupSheetV2.#onAddObjective,
|
||||
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility
|
||||
createTotemAbility: VermineGroupSheetV2.#onCreateTotemAbility,
|
||||
openLearning: VermineGroupSheetV2.#onOpenLearning,
|
||||
openExperiencePhase: VermineGroupSheetV2.#onOpenExperiencePhase
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,4 +160,16 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
const name = game.i18n.localize("ITEMS.new_ability")
|
||||
await this.document.createEmbeddedDocuments("Item", [{ name, type: "ability", system: { type: "totem" } }])
|
||||
}
|
||||
|
||||
static async #onOpenExperiencePhase(event, target) {
|
||||
const { default: ExperiencePhaseDialog } = await import("../../system/dialogs/experiencePhaseDialog.mjs")
|
||||
const dialog = await ExperiencePhaseDialog.create()
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
static async #onOpenLearning(event, target) {
|
||||
const { default: GroupLearningDialog } = await import("../../system/dialogs/groupLearningDialog.mjs")
|
||||
const dialog = await GroupLearningDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class VermineAbilityData extends foundry.abstract.TypeDataModel {
|
||||
totem: new fields.StringField({ required: true, initial: "" }),
|
||||
learn: learnSchema(),
|
||||
level: levelSchema(1, 1, 3),
|
||||
learned: new fields.BooleanField({ required: true, initial: false }),
|
||||
effects: new fields.ArrayField(new fields.StringField({ required: true, initial: "" }), {
|
||||
initial: []
|
||||
})
|
||||
|
||||
@@ -14,7 +14,9 @@ export default class VermineBackgroundData extends foundry.abstract.TypeDataMode
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
return {
|
||||
...listItemSchema(),
|
||||
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 })
|
||||
cost: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
|
||||
asterisk: new fields.BooleanField({ required: true, initial: false }),
|
||||
learned: new fields.BooleanField({ required: true, initial: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,10 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
||||
specialty: new fields.BooleanField({ required: true, initial: false }),
|
||||
reserve: new fields.BooleanField({ required: true, initial: false }),
|
||||
characteristic: new fields.BooleanField({ required: true, initial: false }),
|
||||
evolution: new fields.BooleanField({ required: true, initial: false })
|
||||
evolution: new fields.BooleanField({ required: true, initial: false }),
|
||||
ability: new fields.BooleanField({ required: true, initial: false }),
|
||||
background: new fields.BooleanField({ required: true, initial: false }),
|
||||
reputation: new fields.BooleanField({ required: true, initial: false })
|
||||
})
|
||||
}),
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
* .creatureRoleLevels, .creaturePackLevels.
|
||||
*
|
||||
* Règles :
|
||||
* - Attaque = pattern.attack + size.attack + pack.attack + role.reaction
|
||||
* - Attaque = pattern.attack + size.attack + pack.attack
|
||||
* - Dégâts = pattern.damage + size.vigor + pack.damage
|
||||
* - Vigueur = size.vigor + pack.damage
|
||||
* - Réaction = role.reaction + role.reaction_bonus
|
||||
@@ -116,11 +116,12 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
sizeVigor += cfg.vigor || 0
|
||||
}
|
||||
|
||||
// Attaque : patron + taille + meute + réaction du rôle
|
||||
// Attaque : patron + taille + meute.
|
||||
// NB : le Rôle ne donne que la Réaction (dés + réussites automatiques),
|
||||
// il n'ajoute pas de dés d'attaque (regles_creatures.txt).
|
||||
this.computed.attack = (patternConfig.attack || 0)
|
||||
+ sizeAttack
|
||||
+ (packConfig.attack || 0)
|
||||
+ (roleConfig.reaction || 0)
|
||||
|
||||
// Dégâts : patron + vigueur de taille + meute
|
||||
this.computed.damage = (patternConfig.damage || 0)
|
||||
|
||||
@@ -80,7 +80,15 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
|
||||
|
||||
// Dés d'Expérience collectifs (réserve de Groupe)
|
||||
experience: new fields.SchemaField({
|
||||
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
|
||||
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
// Limites d'apprentissage par phase
|
||||
learned: new fields.SchemaField({
|
||||
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
level: new fields.BooleanField({ required: true, initial: false }),
|
||||
reserve: new fields.BooleanField({ required: true, initial: false }),
|
||||
ability: new fields.BooleanField({ required: true, initial: false })
|
||||
})
|
||||
}),
|
||||
|
||||
// Rencontres
|
||||
|
||||
@@ -41,11 +41,19 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
async _prepareContext() {
|
||||
const characters = game.actors.filter(a => a.type === "character");
|
||||
const groups = game.actors.filter(a => a.type === "group");
|
||||
// Associe chaque personnage à son premier groupe (même logique que RollDialog.#findGroup)
|
||||
const memberOf = new Map();
|
||||
for (const g of groups) {
|
||||
for (const id of g.system?.members || []) {
|
||||
if (!memberOf.has(id)) memberOf.set(id, g.id);
|
||||
}
|
||||
}
|
||||
return {
|
||||
characters: characters.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0
|
||||
dice: a.system?.experience?.dice || 0,
|
||||
groupId: memberOf.get(a.id) || ""
|
||||
})),
|
||||
groups: groups.map(a => ({
|
||||
id: a.id,
|
||||
@@ -63,7 +71,9 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
|
||||
async _onRender(context, options) {
|
||||
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
|
||||
this.element.querySelector("#group-select")?.addEventListener("change", () => this.#updateGroupFilter());
|
||||
this.#updateUI();
|
||||
this.#updateGroupFilter();
|
||||
}
|
||||
|
||||
#updateUI() {
|
||||
@@ -74,6 +84,15 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
if (el) el.textContent = `${danger + discoveries + duration}D`;
|
||||
}
|
||||
|
||||
/** Filtre les personnages selon le groupe sélectionné (vide = tous). */
|
||||
#updateGroupFilter() {
|
||||
const groupId = this.element.querySelector("#group-select")?.value || "";
|
||||
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||
const show = !groupId || row.dataset.groupId === groupId;
|
||||
row.hidden = !show;
|
||||
}
|
||||
}
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
this.close();
|
||||
}
|
||||
@@ -88,12 +107,22 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
const group = game.actors.get(groupId);
|
||||
if (group && collective > 0) {
|
||||
const current = group.system?.experience?.dice || 0;
|
||||
await group.update({ "system.experience.dice": current + collective });
|
||||
const phase = (group.system?.experience?.phase || 0) + 1;
|
||||
await group.update({
|
||||
"system.experience.dice": current + collective,
|
||||
"system.experience.phase": phase,
|
||||
"system.experience.learned.phase": phase,
|
||||
"system.experience.learned.level": false,
|
||||
"system.experience.learned.reserve": false,
|
||||
"system.experience.learned.ability": false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||
// Quand un groupe précis est choisi, ne récompenser que ses membres.
|
||||
if (groupId && row.dataset.groupId !== groupId) continue;
|
||||
const id = row.dataset.characterId;
|
||||
const actor = game.actors.get(id);
|
||||
if (!actor) continue;
|
||||
@@ -125,7 +154,10 @@ export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(fo
|
||||
"system.experience.learned.specialty": false,
|
||||
"system.experience.learned.reserve": false,
|
||||
"system.experience.learned.characteristic": false,
|
||||
"system.experience.learned.evolution": false
|
||||
"system.experience.learned.evolution": false,
|
||||
"system.experience.learned.ability": false,
|
||||
"system.experience.learned.background": false,
|
||||
"system.experience.learned.reputation": false
|
||||
};
|
||||
await actor.update(update);
|
||||
results.push({ name: actor.name, gained });
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -123,6 +123,76 @@ export class VermineExperience {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Un Dé de Totem Adapté peut remplacer un Dé d'Expérience (compte ×2)
|
||||
* uniquement si le personnage possède strictement plus de Dés Adaptés
|
||||
* que Humains.
|
||||
* @param {number} human human totem dice
|
||||
* @param {number} adapted adapted totem dice
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static canUseAdaptedDie(human, adapted) {
|
||||
return adapted > human
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Capacité de Totem : valeur
|
||||
* d'Apprentissage de la capacité.
|
||||
* @param {object} abilityData system data de l'item ability
|
||||
* @returns {number}
|
||||
*/
|
||||
static totemAbilityLearningDifficulty(abilityData) {
|
||||
return parseInt(abilityData?.learn?.threshold, 10) || 5
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'apprentissage de Capacité de Totem : valeur
|
||||
* d'Apprentissage de la capacité.
|
||||
* @param {object} abilityData system data de l'item ability
|
||||
* @returns {number}
|
||||
*/
|
||||
static totemAbilityLearningHandicap(abilityData) {
|
||||
return parseInt(abilityData?.learn?.hindrance, 10) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'obtention d'un Élément d'Historique.
|
||||
* @returns {number}
|
||||
*/
|
||||
static backgroundLearningDifficulty() {
|
||||
return 9
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'obtention d'un Élément d'Historique : (I) si
|
||||
* l'Historique est listé avec un astérisque.
|
||||
* @param {boolean} asterisk
|
||||
* @returns {number}
|
||||
*/
|
||||
static backgroundLearningHandicap(asterisk) {
|
||||
return asterisk ? 1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet visant à réduire le score de Réputation (devenir
|
||||
* plus célèbre) : 10 moins le score que le personnage souhaite atteindre.
|
||||
* @param {number} current réputation actuelle
|
||||
* @returns {number}
|
||||
*/
|
||||
static reputationReduceDifficulty(current) {
|
||||
return 10 - Math.max(0, (parseInt(current, 10) || 0) - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet visant à augmenter le score de Réputation (se faire
|
||||
* oublier) : égale à la Réputation actuelle.
|
||||
* @param {number} current réputation actuelle
|
||||
* @returns {number}
|
||||
*/
|
||||
static reputationIncreaseDifficulty(current) {
|
||||
return parseInt(current, 10) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Clé i18n du libellé du niveau de compétence visé.
|
||||
* @param {number} level
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
<span class="{{#if system.experience.learned.reserve}}learned{{/if}}">{{ localize 'VERMINE.learned_reserve' }}</span>
|
||||
<span class="{{#if system.experience.learned.characteristic}}learned{{/if}}">{{ localize 'VERMINE.learned_characteristic' }}</span>
|
||||
<span class="{{#if system.experience.learned.evolution}}learned{{/if}}">{{ localize 'VERMINE.learned_evolution' }}</span>
|
||||
<span class="{{#if system.experience.learned.ability}}learned{{/if}}">{{ localize 'VERMINE.learned_ability' }}</span>
|
||||
<span class="{{#if system.experience.learned.background}}learned{{/if}}">{{ localize 'VERMINE.learned_background' }}</span>
|
||||
<span class="{{#if system.experience.learned.reputation}}learned{{/if}}">{{ localize 'VERMINE.learned_reputation' }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -47,14 +47,6 @@
|
||||
{{else}}
|
||||
<span>{{system.attributes.reputation.value}}</span>
|
||||
{{/if}}
|
||||
|
||||
<label>{{ localize 'VERMINE.experience' }}</label>
|
||||
{{#if isEditMode}}
|
||||
<input name="system.attributes.xp.value" data-type="Number" type="number"
|
||||
value="{{system.attributes.xp.value}}" />
|
||||
{{else}}
|
||||
<span>{{system.attributes.xp.value}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
@@ -33,6 +33,20 @@
|
||||
{{/if}}
|
||||
<span>/ {{system.morale.max}}D</span>
|
||||
</div>
|
||||
<div class="experience-control flexrow flex-group-center">
|
||||
<label>{{ localize 'VERMINE.experience_dice' }}:</label>
|
||||
{{#if isEditMode}}
|
||||
<input type="number" name="system.experience.dice" value="{{system.experience.dice}}"
|
||||
data-dtype="Number" min="0" />
|
||||
{{else}}
|
||||
<span>{{system.experience.dice}}</span>
|
||||
{{/if}}
|
||||
<span>D</span>
|
||||
</div>
|
||||
<div class="group-experience-actions flexrow flex-group-center">
|
||||
<button type="button" data-action="openLearning"><i class="fas fa-graduation-cap"></i> {{ localize 'VERMINE.open_learning' }}</button>
|
||||
<button type="button" data-action="openExperiencePhase"><i class="fas fa-star"></i> {{ localize 'VERMINE.experience_phase' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Membres --}}
|
||||
|
||||
@@ -35,10 +35,6 @@
|
||||
<input name="system.attributes.reputation.value" type="text"
|
||||
value="{{system.attributes.reputation.value }}"
|
||||
data-dtype="Number" />
|
||||
|
||||
<label>{{ localize 'VERMINE.experience' }}</label>
|
||||
<input name="system.attributes.xp.value" type="text"
|
||||
value="{{system.attributes.xp.value}}" data-dtype="Number" />
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<div class="exp-phase-characters">
|
||||
{{#each characters}}
|
||||
<details class="difficulty-section" data-character-id="{{ this.id }}" open>
|
||||
<details class="difficulty-section" data-character-id="{{ this.id }}" data-group-id="{{ this.groupId }}" open>
|
||||
<summary class="flexrow">
|
||||
<span class="label">{{ this.name }}</span>
|
||||
<span class="current-values">{{ localize 'VERMINE.experience_dice' }}: {{ this.dice }}D</span>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<div class="roll-dialog-content">
|
||||
<h3 class="combat-mode-label">{{ localize 'VERMINE.open_learning' }}</h3>
|
||||
|
||||
<div class="learning-summary">
|
||||
<span class="label">{{ localize 'VERMINE.experience_dice' }}:</span>
|
||||
<span class="exp-value">{{ dice }}D</span>
|
||||
</div>
|
||||
|
||||
<div class="learning-type">
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="level" checked /><span>{{ localize 'VERMINE.learn_level' }}</span></label>
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="reserve" /><span>{{ localize 'VERMINE.learn_group_reserve' }}</span></label>
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="ability" /><span>{{ localize 'VERMINE.learn_ability' }}</span></label>
|
||||
</div>
|
||||
|
||||
<div id="learning-level-section" class="learning-targets">
|
||||
<p class="learning-hint">{{ localize 'VERMINE.group_level_hint' }}</p>
|
||||
</div>
|
||||
|
||||
<div id="learning-reserve-section" class="learning-targets" style="display:none;">
|
||||
<p class="learning-hint">{{ localize 'VERMINE.group_reserve_hint' }}</p>
|
||||
</div>
|
||||
|
||||
<div id="learning-ability-section" class="learning-targets" style="display:none;">
|
||||
<label class="label" for="learning-ability">{{ localize 'VERMINE.learn_ability' }}</label>
|
||||
<select class="info-value" data-roll="true" id="learning-ability" name="learning-ability">
|
||||
{{#each abilities}}
|
||||
<option value="{{ this.id }}" data-threshold="{{ this.threshold }}" data-level="{{ this.level }}">
|
||||
{{ this.name }} ({{ localize 'VERMINE.learn' }} {{ this.threshold }} / {{ localize 'VERMINE.level' }} {{ this.level }})
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{#unless abilities.length}}
|
||||
<p class="learning-hint">{{ localize 'VERMINE.no_group_ability' }}</p>
|
||||
{{/unless}}
|
||||
<p class="learning-hint">{{ localize 'VERMINE.group_ability_hint' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flexrow bonus-item full-width">
|
||||
<label class="label" for="learning-dice">{{ localize 'VERMINE.dice_spent' }}</label>
|
||||
<input type="number" class="numeric-entry" style="text-align:center;" data-roll="true"
|
||||
id="learning-dice" name="learning-dice" min="1" max="{{ dice }}" value="1" />
|
||||
<span>D</span>
|
||||
</div>
|
||||
|
||||
<div class="learning-result">
|
||||
<div class="context-item">
|
||||
<span class="context-label">{{ localize 'VERMINE.difficulty' }}</span>
|
||||
<span class="context-value" id="diff-value">5</span>
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">{{ localize 'VERMINE.handicap' }}</span>
|
||||
<span class="context-value" id="handicap-value">0</span>
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">{{ localize 'VERMINE.success_required' }}</span>
|
||||
<span class="context-value" id="required-value">1</span>
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">{{ localize 'VERMINE.pool_total' }}</span>
|
||||
<span class="context-value" id="pool-value">1D</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sheet-footer flexrow">
|
||||
<button type="button" data-action="cancel">
|
||||
<i class="fas fa-times"></i> {{localize "VERMINE.cancel"}}
|
||||
</button>
|
||||
<button type="button" data-action="roll">
|
||||
<i class="fas fa-graduation-cap"></i> {{localize "VERMINE.roll"}}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -12,6 +12,9 @@
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="characteristic" /><span>{{ localize 'VERMINE.learn_characteristic' }}</span></label>
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="reserve" /><span>{{ localize 'VERMINE.learn_reserve' }}</span></label>
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="evolution" /><span>{{ localize 'VERMINE.learn_evolution' }}</span></label>
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="totem" /><span>{{ localize 'VERMINE.learn_totem_ability' }}</span></label>
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="background" /><span>{{ localize 'VERMINE.learn_background' }}</span></label>
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="reputation" /><span>{{ localize 'VERMINE.learn_reputation' }}</span></label>
|
||||
</div>
|
||||
|
||||
<div id="learning-skill-section" class="learning-targets">
|
||||
@@ -55,6 +58,54 @@
|
||||
|
||||
<div id="learning-evolution-section" class="learning-targets" style="display:none;">
|
||||
<p class="learning-hint">{{ localize 'VERMINE.evolution_hint' }}</p>
|
||||
{{#if canUseAdaptedDie}}
|
||||
<div class="flexrow bonus-item" id="learning-adapted-die-row" style="display:none;">
|
||||
<label class="label" for="learning-adapted-die">{{ localize 'VERMINE.learn_adapted_die' }}</label>
|
||||
<input type="checkbox" data-roll="true" id="learning-adapted-die" name="learning-adapted-die" />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div id="learning-totem-section" class="learning-targets" style="display:none;">
|
||||
{{#if totemAbilities.length}}
|
||||
<label class="label" for="learning-totem">{{ localize 'VERMINE.learn_totem_ability' }}</label>
|
||||
<select class="info-value" data-roll="true" id="learning-totem" name="learning-totem">
|
||||
{{#each totemAbilities}}
|
||||
<option value="{{ this.id }}" data-level="{{ this.level }}" data-threshold="{{ this.threshold }}" data-hindrance="{{ this.hindrance }}">
|
||||
{{ this.name }} (N{{ this.level }} — {{ this.threshold }}{{#ifgt this.hindrance 0}}/{{ roman this.hindrance }}{{/ifgt}})
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<p class="learning-hint">{{ localize 'VERMINE.no_totem_ability' }}</p>
|
||||
{{/if}}
|
||||
<p class="learning-hint">{{ localize 'VERMINE.totem_ability_hint' }}</p>
|
||||
</div>
|
||||
|
||||
<div id="learning-background-section" class="learning-targets" style="display:none;">
|
||||
{{#if backgrounds.length}}
|
||||
<label class="label" for="learning-background">{{ localize 'VERMINE.learn_background' }}</label>
|
||||
<select class="info-value" data-roll="true" id="learning-background" name="learning-background">
|
||||
{{#each backgrounds}}
|
||||
<option value="{{ this.id }}" data-asterisk="{{ this.asterisk }}">
|
||||
{{ this.name }}{{#if this.asterisk}} *{{/if}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{else}}
|
||||
<p class="learning-hint">{{ localize 'VERMINE.no_background' }}</p>
|
||||
{{/if}}
|
||||
<p class="learning-hint">{{ localize 'VERMINE.background_hint' }}</p>
|
||||
</div>
|
||||
|
||||
<div id="learning-reputation-section" class="learning-targets" style="display:none;">
|
||||
<label class="label" for="learning-reputation-direction">{{ localize 'VERMINE.learn_reputation' }}</label>
|
||||
<select class="info-value" data-roll="true" id="learning-reputation-direction" name="learning-reputation-direction">
|
||||
<option value="reduce">{{ localize 'VERMINE.reputation_reduce' }}</option>
|
||||
<option value="increase">{{ localize 'VERMINE.reputation_increase' }}</option>
|
||||
</select>
|
||||
<p class="learning-hint">{{ localize 'VERMINE.reputation_hint' }}</p>
|
||||
<p class="learning-hint"><strong>{{ localize 'VERMINE.reputation_current' }}: <span id="reputation-target">{{ reputation }}</span></strong></p>
|
||||
</div>
|
||||
|
||||
<div class="flexrow bonus-item full-width">
|
||||
|
||||
Reference in New Issue
Block a user