feat: système d'expérience et transferts de dés d'expérience
Release Creation / build (release) Failing after 1m20s
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)
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# Combat — Échanges de coups (assisté)
|
||||
|
||||
## Principe
|
||||
|
||||
Chaque échange de coups se déroule dans le chat : un jet d'attaque, un éventuel
|
||||
jet de défense, puis une résolution assistée. Tous les calculs (réussites,
|
||||
dégâts, protection, blessure) sont effectués automatiquement ; la blessure
|
||||
s'applique ensuite d'un simple clic.
|
||||
|
||||
```
|
||||
Attaque → (Défense) → Résoudre → Appliquer
|
||||
```
|
||||
|
||||
## 1. L'attaque
|
||||
|
||||
Ouvrir le dialogue d'attaque :
|
||||
|
||||
- **Personnage / PNJ** : cliquer l'icône cible (`fa-crosshairs`) sur une arme
|
||||
dans l'onglet **Combat** (liste des armes équipées) ou dans l'onglet
|
||||
**Matériel**.
|
||||
- **Créature** : cliquer la valeur « Attaque » de l'onglet **Stats**.
|
||||
|
||||
Le dialogue est prérempli selon le type d'acteur :
|
||||
|
||||
| Type | Pool | Dégâts de base | Difficulté |
|
||||
|------|------|----------------|------------|
|
||||
| **Personnage** | Caractéristique + compétence (verrouillées) | Arme (+ Vigueur si « Vigueur+ ») | Selon maîtrise / portée |
|
||||
| **PNJ** | Pool de menace + réserves de rôle | Arme équipée, sinon `Vigueur` de menace | Selon niveau d'expérience |
|
||||
| **Créature** | `Attaque` calculée | `Dégâts` calculés | Son statut de combat (verrouillée) |
|
||||
|
||||
### Difficulté
|
||||
|
||||
- **Contact** : dépend du niveau de compétence de l'attaquant.
|
||||
- Incompétent / Débutant : `7`
|
||||
- Confirmé : `5 ou 7`
|
||||
- Expert : `5, 7 ou 9`
|
||||
- Maître / Légende : `3, 5, 7 ou 9`
|
||||
- **Distance** : par portée.
|
||||
- Courte : `5` — Moyenne : `7` — Longue : `9`
|
||||
- **Créature ciblée** : la difficulté est forcée au statut de combat de la
|
||||
créature (toute réussite ≥ 1 touche).
|
||||
|
||||
### Options du dialogue
|
||||
|
||||
- **Deuxième action** : +2 à la difficulté.
|
||||
- **Handicap** : augmente les succès requis.
|
||||
- **Sang-Froid** : dés bonus (débités du personnage).
|
||||
- **Totems** : dés de totem (+ garder un totem si les deux sont actifs).
|
||||
- **Consommation de munitions** : pour les armes à distance (coche cochée par
|
||||
défaut, la munition est décomptée au jet).
|
||||
- Les **cibles** sélectionnées sur la carte (`Ctrl` + clic / outil de ciblage)
|
||||
sont affichées et reprises dans l'échange.
|
||||
|
||||
### La carte de chat
|
||||
|
||||
Le message d'attaque affiche l'arme (dégâts, portées, munitions) puis le bloc
|
||||
**Échange de coups** avec, **par cible**, les boutons **Esquive** et **Parade**
|
||||
(contact) ou **Esquive** seule (distance), ainsi que le bouton **Résoudre**.
|
||||
|
||||
## 2. La défense
|
||||
|
||||
Seuls les acteurs **capables de se défendre** (personnages et PNJ) ont des
|
||||
boutons de défense — pas les créatures (défense passive).
|
||||
|
||||
- Cliquer **Esquive** ou **Parade** sur la carte d'attaque.
|
||||
- Le dialogue de défense est **verrouillé** :
|
||||
- **Esquive** : `Réflexes` + `Corps-à-corps` (attaque au contact) ou
|
||||
`Vigilance` (attaque à distance).
|
||||
- **Parade** : `Vigueur` + `Mêlée` (ou `Corps-à-corps`).
|
||||
- **Difficulté = celle de l'attaque** (non modifiable).
|
||||
- Le jet de défense est lié au message d'attaque (même échange).
|
||||
|
||||
> Permission : Esquive / Parade ne sont utilisables que par le **propriétaire
|
||||
> de la cible** ou le **MJ**.
|
||||
|
||||
## 3. La résolution
|
||||
|
||||
Cliquer **Résoudre** (auteur du message ou MJ). Le système compare les
|
||||
**réussites** des jets d'attaque et de défense :
|
||||
|
||||
- **Attaquant > Défenseur** → **Touché** (parade insuffisante si parade).
|
||||
- **Attaquant ≤ Défenseur** → **Esquivé / Paré** : aucun dégât.
|
||||
- **Sans défense** (abstention ou créature) : toute réussite ≥ 1 touche.
|
||||
|
||||
Les totaux sont lus en direct : une **relance** de dé effectuée avant la
|
||||
résolution est prise en compte.
|
||||
|
||||
### Dégâts
|
||||
|
||||
```
|
||||
Dégâts = dégâts de base de l'arme (+ Vigueur si « Vigueur+ ») + réussites d'attaque
|
||||
```
|
||||
|
||||
### Protection
|
||||
|
||||
- **Parade** : seule la protection utilisée pour la parade réduit.
|
||||
- **Sinon** : somme des **protections équipées** de la cible
|
||||
(créature : protection de rôle).
|
||||
|
||||
### Blessure
|
||||
|
||||
La blessure correspond au **seuil le plus élevé** inférieur ou égal aux dégâts
|
||||
nets : **Légère**, **Grave** ou **Mortelle**. Un bloc de résultat est affiché
|
||||
**par cible** (attaque multi-cibles = un bloc par cible).
|
||||
|
||||
## 4. L'application de la blessure
|
||||
|
||||
Cliquer **« Appliquer la blessure »** sur le bloc de résultat (propriétaire de
|
||||
la cible ou MJ) : le cercle de blessure correspondant est coché sur l'acteur.
|
||||
|
||||
## Rappels et permissions
|
||||
|
||||
| Action | Qui peut la faire |
|
||||
|--------|-------------------|
|
||||
| Esquive / Parade | Propriétaire de la cible ou MJ |
|
||||
| Résoudre | Auteur du message d'attaque ou MJ |
|
||||
| Appliquer la blessure | Propriétaire de la cible ou MJ |
|
||||
|
||||
- Les **créatures** ne lancent pas de défense : leur difficulté d'attaque et de
|
||||
défense est leur **statut de combat**.
|
||||
- Les **PNJ** lancent leur défense comme un personnage (capacités + compétences).
|
||||
- Les **réserves de rôle** des PNJ sont à dépenser à la main (gérées par le MJ).
|
||||
|
||||
---
|
||||
|
||||
*Guide utilisateur — Vermine2047 pour FoundryVTT.*
|
||||
@@ -7,18 +7,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
position: { width: 860, height: 720 },
|
||||
window: { contentClasses: ["character-content"] },
|
||||
actions: {
|
||||
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty
|
||||
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
|
||||
openExperiencePhase: VermineCharacterSheetV2.#onOpenExperiencePhase,
|
||||
openLearning: VermineCharacterSheetV2.#onOpenLearning
|
||||
}
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs" },
|
||||
main: { template: "systems/vermine2047/templates/actor/appv2/character-main.hbs", scrollable: [""] },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs" },
|
||||
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs" },
|
||||
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs" },
|
||||
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs" },
|
||||
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs" }
|
||||
abilities: { template: "systems/vermine2047/templates/actor/appv2/character-abilities.hbs", scrollable: [""] },
|
||||
totem: { template: "systems/vermine2047/templates/actor/appv2/character-totem.hbs", scrollable: [""] },
|
||||
equipment: { template: "systems/vermine2047/templates/actor/appv2/character-equipment.hbs", scrollable: [""] },
|
||||
stories: { template: "systems/vermine2047/templates/actor/appv2/character-stories.hbs", scrollable: [""] },
|
||||
combat: { template: "systems/vermine2047/templates/actor/appv2/character-combat.hbs", scrollable: [""] },
|
||||
experience: { template: "systems/vermine2047/templates/actor/appv2/character-experience.hbs", scrollable: [""] }
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "abilities" }
|
||||
@@ -29,7 +32,8 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
totem: { id: "totem", group: "sheet", icon: "fas fa-star", label: "VERMINE.tabs.totem" },
|
||||
equipment: { id: "equipment", group: "sheet", icon: "fas fa-hammer", label: "VERMINE.tabs.equipment" },
|
||||
stories: { id: "stories", group: "sheet", icon: "fas fa-book-open-reader", label: "VERMINE.tabs.stories" },
|
||||
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" }
|
||||
combat: { id: "combat", group: "sheet", icon: "fas fa-medal", label: "VERMINE.tabs.combat" },
|
||||
experience: { id: "experience", group: "sheet", icon: "fas fa-graduation-cap", label: "VERMINE.tabs.experience" }
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
@@ -74,9 +78,21 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
break
|
||||
case "combat":
|
||||
context.tab = context.tabs.combat
|
||||
context.equippedWeapons = doc.itemTypes.weapon.filter(i => i.system.equipped)
|
||||
context.equippedDefenses = doc.itemTypes.defense.filter(i => i.system.equipped)
|
||||
const { prepareActiveEffectCategories } = await import("../../system/effects.mjs")
|
||||
context.effects = prepareActiveEffectCategories(doc.effects)
|
||||
break
|
||||
case "experience":
|
||||
context.tab = context.tabs.experience
|
||||
context.specialtyCount = doc.itemTypes.specialty.length
|
||||
const ex = doc.system.experience
|
||||
context.experience = ex
|
||||
context.collectiveTotal = (ex.collective.danger + ex.collective.discoveries + ex.collective.duration)
|
||||
context.individualTotal = (ex.individual.implication + ex.individual.influence + ex.individual.interpretation)
|
||||
context.currentPhase = ex.phase
|
||||
context.learnedCurrent = ex.learned.phase === ex.phase
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -96,4 +112,16 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
if (skillKey) itemData.system = { skill: skillKey }
|
||||
await this.document.createEmbeddedDocuments("Item", [itemData])
|
||||
}
|
||||
|
||||
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: LearningDialog } = await import("../../system/dialogs/learningDialog.mjs")
|
||||
const dialog = await LearningDialog.create({ actorId: this.document.id })
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,40 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
||||
attributes: new fields.SchemaField({
|
||||
xp: attributeSchema(0, 0, 10),
|
||||
reputation: attributeSchema(0, 0, 10),
|
||||
self_control: attributeSchema(0, 0, 5),
|
||||
effort: attributeSchema(0, 0, 5)
|
||||
self_control: attributeSchema(0, 0, 10),
|
||||
effort: attributeSchema(0, 0, 10)
|
||||
}),
|
||||
|
||||
// Expérience (dés, récompenses, évolution, apprentissage)
|
||||
experience: new fields.SchemaField({
|
||||
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 }),
|
||||
collective: new fields.SchemaField({
|
||||
danger: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||
discoveries: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||
duration: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
|
||||
}),
|
||||
individual: new fields.SchemaField({
|
||||
implication: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||
influence: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
|
||||
interpretation: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
|
||||
}),
|
||||
evolution: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
traumaReward: new fields.BooleanField({ required: true, initial: false }),
|
||||
// Bonus de Réserve acquis par apprentissage (s'ajoute au max dérivé)
|
||||
reserveBonus: new fields.SchemaField({
|
||||
self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
|
||||
effort: 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 }),
|
||||
skill: new fields.BooleanField({ required: true, initial: false }),
|
||||
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 })
|
||||
})
|
||||
}),
|
||||
|
||||
// Rencontres
|
||||
@@ -127,7 +159,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
||||
const sum = abilities
|
||||
.filter(a => a.category === "mental" || a.category === "social")
|
||||
.reduce((acc, a) => acc + a.value, 0)
|
||||
this.attributes.self_control.max = sum + modFromAge
|
||||
const bonus = this.experience?.reserveBonus?.self_control || 0
|
||||
this.attributes.self_control.max = Math.min(10, sum + modFromAge + bonus)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +173,8 @@ export default class VermineCharacterData extends foundry.abstract.TypeDataModel
|
||||
const sum = abilities
|
||||
.filter(a => a.category === "physical" || a.category === "manual")
|
||||
.reduce((acc, a) => acc + a.value, 0)
|
||||
this.attributes.effort.max = sum + modFromAge
|
||||
const bonus = this.experience?.reserveBonus?.effort || 0
|
||||
this.attributes.effort.max = Math.min(10, sum + modFromAge + bonus)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,6 +75,11 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
|
||||
// Membres (IDs d'acteurs)
|
||||
members: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
|
||||
|
||||
// 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 })
|
||||
}),
|
||||
|
||||
// Rencontres
|
||||
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
/**
|
||||
* Dialogue du meneur pour ouvrir une phase d'Expérience.
|
||||
* Permet d'attribuer les Dés d'Expérience collectifs (Danger, Découvertes,
|
||||
* Durée) au Groupe et aux personnages, ainsi que les Dés individuels
|
||||
* (Implication, Influence, Interprétation), les récompenses d'Objectifs et
|
||||
* la récompense de Traumatisme choisi.
|
||||
*/
|
||||
export default class ExperiencePhaseDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["vermine-roll"],
|
||||
tag: "form",
|
||||
window: {
|
||||
icon: "fas fa-star",
|
||||
resizable: true
|
||||
},
|
||||
position: {
|
||||
width: 620,
|
||||
height: 700
|
||||
},
|
||||
actions: {
|
||||
apply: ExperiencePhaseDialog.#onApply,
|
||||
cancel: ExperiencePhaseDialog.#onCancel
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/vermine2047/templates/dialogs/experience-phase-dialog.hbs", scrollable: [""] }
|
||||
};
|
||||
|
||||
static async create() {
|
||||
return new ExperiencePhaseDialog();
|
||||
}
|
||||
|
||||
get title() {
|
||||
return game.i18n.localize("VERMINE.experience_phase");
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const characters = game.actors.filter(a => a.type === "character");
|
||||
const groups = game.actors.filter(a => a.type === "group");
|
||||
return {
|
||||
characters: characters.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0
|
||||
})),
|
||||
groups: groups.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
dice: a.system?.experience?.dice || 0
|
||||
})),
|
||||
rewards: [
|
||||
{ value: 0, label: game.i18n.localize("VERMINE.reward_anecdotic") },
|
||||
{ value: 1, label: game.i18n.localize("VERMINE.reward_minor") },
|
||||
{ value: 2, label: game.i18n.localize("VERMINE.reward_major") },
|
||||
{ value: 3, label: game.i18n.localize("VERMINE.reward_exceptionnal") }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
this.element.querySelectorAll("[data-roll]").forEach(inp => inp.addEventListener("change", () => this.#updateUI()));
|
||||
this.#updateUI();
|
||||
}
|
||||
|
||||
#updateUI() {
|
||||
const danger = parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0;
|
||||
const discoveries = parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0;
|
||||
const duration = parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0;
|
||||
const el = this.element.querySelector("#collective-total");
|
||||
if (el) el.textContent = `${danger + discoveries + duration}D`;
|
||||
}
|
||||
|
||||
static async #onCancel(event, target) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onApply(event, target) {
|
||||
const collective = (parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0)
|
||||
+ (parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0)
|
||||
+ (parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0);
|
||||
|
||||
const groupId = this.element.querySelector("#group-select")?.value || null;
|
||||
if (groupId) {
|
||||
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 results = [];
|
||||
for (const row of this.element.querySelectorAll("[data-character-id]")) {
|
||||
const id = row.dataset.characterId;
|
||||
const actor = game.actors.get(id);
|
||||
if (!actor) continue;
|
||||
const implication = parseInt(row.querySelector(`[name="implication-${id}"]`)?.value, 10) || 0;
|
||||
const influence = parseInt(row.querySelector(`[name="influence-${id}"]`)?.value, 10) || 0;
|
||||
const interpretation = parseInt(row.querySelector(`[name="interpretation-${id}"]`)?.value, 10) || 0;
|
||||
const objectiveMinor = row.querySelector(`[name="objective-minor-${id}"]`)?.checked ? 3 : 0;
|
||||
const objectiveMajor = row.querySelector(`[name="objective-major-${id}"]`)?.checked ? 5 : 0;
|
||||
const trauma = row.querySelector(`[name="trauma-${id}"]`)?.checked ? 5 : 0;
|
||||
|
||||
const individual = implication + influence + interpretation;
|
||||
const bonus = objectiveMinor + objectiveMajor + trauma;
|
||||
const gained = collective + individual + bonus;
|
||||
|
||||
const ex = actor.system?.experience;
|
||||
const nextPhase = (ex?.phase || 0) + 1;
|
||||
const update = {
|
||||
"system.experience.phase": nextPhase,
|
||||
"system.experience.dice": (ex?.dice || 0) + gained,
|
||||
"system.experience.collective.danger": parseInt(this.element.querySelector("#collective-danger")?.value, 10) || 0,
|
||||
"system.experience.collective.discoveries": parseInt(this.element.querySelector("#collective-discoveries")?.value, 10) || 0,
|
||||
"system.experience.collective.duration": parseInt(this.element.querySelector("#collective-duration")?.value, 10) || 0,
|
||||
"system.experience.individual.implication": implication,
|
||||
"system.experience.individual.influence": influence,
|
||||
"system.experience.individual.interpretation": interpretation,
|
||||
"system.experience.traumaReward": trauma > 0,
|
||||
"system.experience.learned.phase": nextPhase,
|
||||
"system.experience.learned.skill": false,
|
||||
"system.experience.learned.specialty": false,
|
||||
"system.experience.learned.reserve": false,
|
||||
"system.experience.learned.characteristic": false,
|
||||
"system.experience.learned.evolution": false
|
||||
};
|
||||
await actor.update(update);
|
||||
results.push({ name: actor.name, gained });
|
||||
}
|
||||
|
||||
const lines = [`${game.i18n.localize("VERMINE.experience_phase")} +1`];
|
||||
if (groupId) {
|
||||
const group = game.actors.get(groupId);
|
||||
lines.push(`${group?.name ?? ""} (${game.i18n.localize("VERMINE.collective")}): +${collective}D`);
|
||||
}
|
||||
for (const r of results) {
|
||||
lines.push(`${r.name}: +${r.gained}D`);
|
||||
}
|
||||
await ChatMessage.create({
|
||||
user: game.user?._id,
|
||||
speaker: ChatMessage.getSpeaker(),
|
||||
content: `<div class="vermine-roll-message"><h3>${game.i18n.localize("VERMINE.experience_rewards")}</h3><div class="roll-context flexrow flex-wrap">${lines.map(l => `<span class="context-item">${l}</span>`).join("")}</div></div>`
|
||||
});
|
||||
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
},
|
||||
actions: {
|
||||
roll: RollDialog.#onRoll,
|
||||
cancel: RollDialog.#onCancel
|
||||
cancel: RollDialog.#onCancel,
|
||||
giveToGroup: RollDialog.#onGiveToGroup
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,6 +33,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
};
|
||||
|
||||
static async create(data = {}) {
|
||||
if (data.actor instanceof Actor) {
|
||||
return new RollDialog({ actor: data.actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense });
|
||||
}
|
||||
const actorId = data.actorId ?? game.user.character?.id ?? canvas.tokens.controlled[0]?.actor?.id;
|
||||
if (!actorId || typeof actorId !== "string") {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||
@@ -42,7 +46,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"));
|
||||
return null;
|
||||
}
|
||||
return new RollDialog({ actor, label: data.label, rolltype: data.rolltype });
|
||||
return new RollDialog({ actor, label: data.label, rolltype: data.rolltype, weapon: data.weapon, locks: data.locks, defense: data.defense });
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
@@ -50,10 +54,16 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
this.#actor = options.actor;
|
||||
this.label = options.label ?? null;
|
||||
this.rolltype = options.rolltype ?? null;
|
||||
this.weapon = options.weapon ?? null;
|
||||
this.locks = options.locks ?? null;
|
||||
this.defense = options.defense ?? null;
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const actor = this.#actor;
|
||||
const isCharacter = actor.type === "character";
|
||||
const group = isCharacter ? RollDialog.#findGroup(actor) : null;
|
||||
const groupPool = group?.system?.experience?.dice || 0;
|
||||
return {
|
||||
actor,
|
||||
system: actor.system,
|
||||
@@ -66,13 +76,28 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
help: false,
|
||||
specialty: false,
|
||||
availableSpecialties: actor.items.filter(i => i.type === "specialty"),
|
||||
availableItems: actor.items.filter(i => i.type === "item")
|
||||
availableItems: actor.items.filter(i => i.type === "item"),
|
||||
locks: this.locks,
|
||||
experience: {
|
||||
hasExperience: isCharacter,
|
||||
individual: isCharacter ? actor.system?.experience?.dice || 0 : 0,
|
||||
groupPool,
|
||||
pullOptions: [0, 1, 2].filter(v => v <= Math.min(2, groupPool)),
|
||||
canPull: Boolean(group) && (game.user.isGM || group.isOwner),
|
||||
canGive: isCharacter && (game.user.isGM || actor.isOwner)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
this.element.dataset.actorId = this.#actor.id;
|
||||
|
||||
// Mode verrouillé (défense en échange de coups) : présélectionne et bloque
|
||||
// caractéristique + difficulté, présélectionne la compétence.
|
||||
if (this.locks) {
|
||||
this.#applyLocks(this.locks);
|
||||
}
|
||||
|
||||
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
||||
inp.addEventListener("change", this.#onInputChange.bind(this));
|
||||
}
|
||||
@@ -95,6 +120,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
this.element.querySelector("#adapted-totem")?.addEventListener("change", () => this.#updateUI());
|
||||
|
||||
this.#displaySpecialties();
|
||||
this.#refreshExperienceUI();
|
||||
this.#updateUI();
|
||||
|
||||
if (ability?.value !== "0") {
|
||||
@@ -111,6 +137,16 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
#getDifficulty() { return this.#el.querySelector("#difficulty"); }
|
||||
#getHandicap() { return this.#el.querySelector("#handicap"); }
|
||||
#getSelfCtrl() { return this.#el.querySelector("#self_control"); }
|
||||
#getExperiencePull() { return parseInt(this.#el.querySelector("#experience-pull")?.value, 10) || 0; }
|
||||
|
||||
/**
|
||||
* Trouve le premier Groupe dont le personnage est membre.
|
||||
* @param {Actor} actor
|
||||
* @returns {Actor|null}
|
||||
*/
|
||||
static #findGroup(actor) {
|
||||
return game.actors.filter(a => a.type === "group" && (a.system?.members ?? []).includes(actor.id))[0] ?? null;
|
||||
}
|
||||
|
||||
/** Décomposition du pool de dés pour affichage dans le message. */
|
||||
getPoolBreakdown() {
|
||||
@@ -124,11 +160,12 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
||||
const tools = toolsChecked && toolsChecked.value !== "0";
|
||||
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
||||
const experience = this.#getExperiencePull();
|
||||
const specialty = specChecked ? 1 : 0;
|
||||
const help = helped ? 1 : 0;
|
||||
const tooling = tools ? 1 : 0;
|
||||
const total = ability + skillPool + selfControl + specialty + help + tooling + group;
|
||||
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, total };
|
||||
const total = ability + skillPool + selfControl + specialty + help + tooling + group + experience;
|
||||
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, total };
|
||||
}
|
||||
|
||||
getDicePool() {
|
||||
@@ -214,6 +251,40 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
|
||||
// ── UI ───────────────────────────────────────────────────────────────
|
||||
|
||||
#applyLocks(locks) {
|
||||
const ability = this.#getAbility();
|
||||
if (ability && locks.ability) {
|
||||
const opt = [...ability.options].find(o => o.dataset.label === locks.ability);
|
||||
if (opt) {
|
||||
opt.selected = true;
|
||||
ability.disabled = true;
|
||||
const scoreEl = this.#el.querySelector("#abilityScore");
|
||||
if (scoreEl) scoreEl.value = opt.value;
|
||||
const sc = this.#getSelfCtrl();
|
||||
if (sc) sc.max = opt.value;
|
||||
}
|
||||
}
|
||||
const skill = this.#getSkill();
|
||||
if (skill && locks.skill) {
|
||||
const opt = [...skill.options].find(o => o.dataset.label === locks.skill);
|
||||
if (opt) opt.selected = true;
|
||||
}
|
||||
const difficulty = this.#getDifficulty();
|
||||
if (difficulty && locks.difficulty !== undefined && locks.difficulty !== null) {
|
||||
const locked = parseInt(locks.difficulty, 10);
|
||||
let opt = [...difficulty.options].find(o => parseInt(o.value, 10) === locked);
|
||||
if (!opt) {
|
||||
// Difficulté hors des options (ex: 9 + 2) : ajoute une option dédiée.
|
||||
opt = document.createElement("option");
|
||||
opt.value = String(locked);
|
||||
opt.textContent = `${game.i18n.localize("VERMINE.difficulty")} (${locked})`;
|
||||
difficulty.appendChild(opt);
|
||||
}
|
||||
opt.selected = true;
|
||||
difficulty.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
#displaySpecialties() {
|
||||
for (const el of this.#el.querySelectorAll("[data-spec-skill]")) {
|
||||
el.style.display = "inline";
|
||||
@@ -222,7 +293,31 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
|
||||
#calculateBonusCount() {
|
||||
const b = this.getPoolBreakdown();
|
||||
return b.specialty + b.help + b.tooling + b.group + b.selfControl;
|
||||
return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience;
|
||||
}
|
||||
|
||||
#refreshExperienceUI() {
|
||||
const actor = this.#actor;
|
||||
const group = RollDialog.#findGroup(actor);
|
||||
const individual = actor.system?.experience?.dice || 0;
|
||||
const groupPool = group?.system?.experience?.dice || 0;
|
||||
const indEl = this.#el.querySelector("#exp-individual");
|
||||
if (indEl) indEl.textContent = individual;
|
||||
const grpEl = this.#el.querySelector("#exp-group");
|
||||
if (grpEl) grpEl.textContent = groupPool;
|
||||
const pull = this.#el.querySelector("#experience-pull");
|
||||
if (pull) {
|
||||
const max = Math.min(2, groupPool);
|
||||
const current = Math.min(parseInt(pull.value, 10) || 0, max);
|
||||
pull.innerHTML = "";
|
||||
for (let i = 0; i <= max; i++) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(i);
|
||||
opt.textContent = `${i}D`;
|
||||
if (i === current) opt.selected = true;
|
||||
pull.appendChild(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#updateUI() {
|
||||
@@ -289,6 +384,29 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
this.close();
|
||||
}
|
||||
|
||||
static async #onGiveToGroup(event, target) {
|
||||
const actor = this.#actor;
|
||||
if (actor.type !== "character") return;
|
||||
const individual = actor.system?.experience?.dice || 0;
|
||||
if (individual < 1) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_experience_dice"));
|
||||
return;
|
||||
}
|
||||
if (!game.user.isGM && !actor.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
|
||||
return;
|
||||
}
|
||||
const group = RollDialog.#findGroup(actor);
|
||||
if (!group) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
|
||||
return;
|
||||
}
|
||||
const groupPool = group.system?.experience?.dice || 0;
|
||||
await actor.update({ "system.experience.dice": individual - 1 });
|
||||
await group.update({ "system.experience.dice": groupPool + 1 });
|
||||
this.#refreshExperienceUI();
|
||||
}
|
||||
|
||||
static async #onRoll(event, target) {
|
||||
const selfCtrl = this.getSelfControl();
|
||||
if (selfCtrl > 0) {
|
||||
@@ -310,6 +428,26 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
await this.#actor.update({ "system.attributes.self_control.value": newVal });
|
||||
}
|
||||
|
||||
// Transfert d'Expérience : puiser dans la Réserve de Groupe (1-2D).
|
||||
const pulled = this.#getExperiencePull();
|
||||
if (pulled > 0) {
|
||||
const group = RollDialog.#findGroup(this.#actor);
|
||||
if (!group) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group"));
|
||||
return;
|
||||
}
|
||||
if (!game.user.isGM && !group.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_permission"));
|
||||
return;
|
||||
}
|
||||
const groupPool = group.system?.experience?.dice || 0;
|
||||
if (groupPool < pulled) {
|
||||
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_group_dice"));
|
||||
return;
|
||||
}
|
||||
await group.update({ "system.experience.dice": groupPool - pulled });
|
||||
}
|
||||
|
||||
await VermineUtils.roll({
|
||||
actor: this.#actor,
|
||||
NoD: this.getDicePool(),
|
||||
@@ -325,7 +463,10 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
skillLevel: this.getSkillLevel(),
|
||||
hasSpecialty: this.hasSpecialtySelected(),
|
||||
poolBreakdown: this.getPoolBreakdown(),
|
||||
specialtyName: this.getSpecialtyName()
|
||||
specialtyName: this.getSpecialtyName(),
|
||||
weapon: this.weapon?.toObject() ?? null,
|
||||
targets: [...game.user.targets].map(t => t.name),
|
||||
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
|
||||
});
|
||||
|
||||
this.close();
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Règles d'apprentissage (phase d'Expérience).
|
||||
* Helpers purs de difficultés, handicaps et calculs de jets d'Expérience.
|
||||
*/
|
||||
|
||||
export class VermineExperience {
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Compétence selon le niveau visé.
|
||||
* @param {number} targetLevel niveau visé (1-5)
|
||||
* @returns {number}
|
||||
*/
|
||||
static skillLearningDifficulty(targetLevel) {
|
||||
switch (parseInt(targetLevel, 10)) {
|
||||
case 1: return 5 // Débutant
|
||||
case 2: return 7 // Confirmé
|
||||
case 3: return 9 // Expert
|
||||
case 4: return 10 // Maître
|
||||
case 5: return 10 // Légende
|
||||
default: return 7
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'apprentissage de Compétence.
|
||||
* Rareté (1-3) + handicap (I) supplémentaire pour le niveau Légende.
|
||||
* @param {number} targetLevel niveau visé (1-5)
|
||||
* @param {number} rarity rareté de la compétence (0-3)
|
||||
* @returns {number}
|
||||
*/
|
||||
static skillLearningHandicap(targetLevel, rarity) {
|
||||
let handicap = parseInt(rarity, 10) || 0
|
||||
if (parseInt(targetLevel, 10) === 5) handicap += 1 // Légende : (I)
|
||||
return handicap
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Spécialité selon le niveau actuel
|
||||
* de la compétence associée. Domaine de prédilection : -2.
|
||||
* @param {number} skillLevel niveau actuel de la compétence (2-5)
|
||||
* @param {boolean} isDomain la compétence est dans le domaine de prédilection
|
||||
* @returns {number}
|
||||
*/
|
||||
static specialtyLearningDifficulty(skillLevel, isDomain) {
|
||||
let difficulty
|
||||
switch (parseInt(skillLevel, 10)) {
|
||||
case 2: difficulty = 9 // Confirmé
|
||||
break
|
||||
case 3: difficulty = 7 // Expert
|
||||
break
|
||||
case 4: difficulty = 5 // Maître
|
||||
break
|
||||
case 5: difficulty = 5 // Légende (relance automatique)
|
||||
break
|
||||
default: difficulty = 9
|
||||
}
|
||||
return isDomain ? difficulty - 2 : difficulty
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'un jet d'apprentissage de Spécialité : nombre de spécialités
|
||||
* déjà possédées (requiert au moins Confirmé dans la compétence associée).
|
||||
* @param {number} specialtyCount
|
||||
* @returns {number}
|
||||
*/
|
||||
static specialtyLearningHandicap(specialtyCount) {
|
||||
return Math.max(0, parseInt(specialtyCount, 10) || 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Caractéristique.
|
||||
* @returns {number}
|
||||
*/
|
||||
static characteristicLearningDifficulty() {
|
||||
return 9
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'apprentissage de Réserve : valeur actuelle.
|
||||
* @param {number} currentValue
|
||||
* @returns {number}
|
||||
*/
|
||||
static reserveLearningDifficulty(currentValue) {
|
||||
return Math.max(0, parseInt(currentValue, 10) || 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handicap d'âge pour l'apprentissage des Réserves et autres jets liés à l'âge.
|
||||
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
|
||||
* @returns {number}
|
||||
*/
|
||||
static ageHandicap(ageType) {
|
||||
switch (parseInt(ageType, 10)) {
|
||||
case 1: return 1 // Jeune (I)
|
||||
case 3: return 3 // Ancien (III)
|
||||
default: return 2 // Adulte (II)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Difficulté d'un jet d'acquisition d'un Dé d'Évolution selon l'âge.
|
||||
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
|
||||
* @returns {number}
|
||||
*/
|
||||
static evolutionDifficulty(ageType) {
|
||||
switch (parseInt(ageType, 10)) {
|
||||
case 1: return 5
|
||||
case 3: return 9
|
||||
default: return 7
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bonus/Malus de Totem sur le jet de Dé d'Évolution.
|
||||
* Plus de Dés Adaptés que Humains : +1D. Inversement : -1D.
|
||||
* @param {number} human human totem dice
|
||||
* @param {number} adapted adapted totem dice
|
||||
* @returns {number}
|
||||
*/
|
||||
static evolutionTotemModifier(human, adapted) {
|
||||
if (adapted > human) return 1
|
||||
if (human > adapted) return -1
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Clé i18n du libellé du niveau de compétence visé.
|
||||
* @param {number} level
|
||||
* @returns {string}
|
||||
*/
|
||||
static skillLevelLabelKey(level) {
|
||||
return CONFIG.VERMINE.SkillLevels?.[level]?.label ?? "SKILL_LEVELS.incompetent"
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine si une compétence appartient au domaine de prédilection.
|
||||
* @param {Actor} actor
|
||||
* @param {string} skillKey
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isPreferredDomain(actor, skillKey) {
|
||||
const category = actor.system?.skills?.[skillKey]?.category
|
||||
const preferred = actor.system?.skill_categories?.preferred
|
||||
return Boolean(category && preferred && category === preferred)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<div class="tab experience sheet-part {{#if @root.tabs.experience.active}}active{{/if}}" data-group="sheet" data-tab="experience">
|
||||
<h3>{{ localize 'VERMINE.experience' }}</h3>
|
||||
|
||||
<div class="experience-summary">
|
||||
<div class="exp-stat">
|
||||
<label>{{ localize 'VERMINE.experience_dice' }}</label>
|
||||
<span class="exp-value">{{ system.experience.dice }}D</span>
|
||||
</div>
|
||||
<div class="exp-stat">
|
||||
<label>{{ localize 'VERMINE.experience_phase' }}</label>
|
||||
<span class="exp-value">{{ system.experience.phase }}</span>
|
||||
</div>
|
||||
<div class="exp-stat">
|
||||
<label>{{ localize 'VERMINE.evolution_dice' }}</label>
|
||||
<span class="exp-value">{{ system.experience.evolution }}D</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flexrow exp-actions">
|
||||
{{#if isGM}}
|
||||
<button type="button" data-action="openExperiencePhase">
|
||||
<i class="fas fa-star"></i> {{ localize 'VERMINE.open_experience_phase' }}
|
||||
</button>
|
||||
{{/if}}
|
||||
<button type="button" data-action="openLearning">
|
||||
<i class="fas fa-graduation-cap"></i> {{ localize 'VERMINE.open_learning' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="exp-rewards">
|
||||
<h4>{{ localize 'VERMINE.collective_rewards' }} <span class="exp-total">({{ collectiveTotal }}D)</span></h4>
|
||||
<div class="exp-reward-row">
|
||||
<label>{{ localize 'VERMINE.danger' }}</label>
|
||||
<span>{{ system.experience.collective.danger }}D</span>
|
||||
<label>{{ localize 'VERMINE.discoveries' }}</label>
|
||||
<span>{{ system.experience.collective.discoveries }}D</span>
|
||||
<label>{{ localize 'VERMINE.duration' }}</label>
|
||||
<span>{{ system.experience.collective.duration }}D</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="exp-rewards">
|
||||
<h4>{{ localize 'VERMINE.individual_rewards' }} <span class="exp-total">({{ individualTotal }}D)</span></h4>
|
||||
<div class="exp-reward-row">
|
||||
<label>{{ localize 'VERMINE.implication' }}</label>
|
||||
<span>{{ system.experience.individual.implication }}D</span>
|
||||
<label>{{ localize 'VERMINE.influence' }}</label>
|
||||
<span>{{ system.experience.individual.influence }}D</span>
|
||||
<label>{{ localize 'VERMINE.interpretation' }}</label>
|
||||
<span>{{ system.experience.individual.interpretation }}D</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="exp-rewards">
|
||||
<h4>{{ localize 'VERMINE.learning_limits' }}</h4>
|
||||
<div class="exp-learned">
|
||||
<span class="{{#if system.experience.learned.skill}}learned{{/if}}">{{ localize 'VERMINE.learned_skill' }}</span>
|
||||
<span class="{{#if system.experience.learned.specialty}}learned{{/if}}">{{ localize 'VERMINE.learned_specialty' }}</span>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="exp-rewards">
|
||||
<h4>{{ localize 'VERMINE.trauma_reward' }}</h4>
|
||||
<div class="exp-learned">
|
||||
<span class="{{#if system.experience.traumaReward}}learned{{/if}}">{{ localize 'VERMINE.trauma_reward_5' }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
<div class="roll-dialog-content">
|
||||
<h3 class="combat-mode-label">{{ localize 'VERMINE.experience_phase' }}</h3>
|
||||
|
||||
<details class="difficulty-section" open>
|
||||
<summary class="flexrow">
|
||||
<span class="label">{{ localize 'VERMINE.collective_rewards' }}</span>
|
||||
<span class="current-values"><span id="collective-total">0D</span></span>
|
||||
</summary>
|
||||
<div class="flexrow difficulty-controls">
|
||||
<div class="flexcol">
|
||||
<label class="label" for="collective-danger">{{ localize 'VERMINE.danger' }}</label>
|
||||
<select class="info-value" data-roll="true" id="collective-danger" name="collective-danger">
|
||||
{{#each rewards}}
|
||||
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flexcol">
|
||||
<label class="label" for="collective-discoveries">{{ localize 'VERMINE.discoveries' }}</label>
|
||||
<select class="info-value" data-roll="true" id="collective-discoveries" name="collective-discoveries">
|
||||
{{#each rewards}}
|
||||
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flexcol">
|
||||
<label class="label" for="collective-duration">{{ localize 'VERMINE.duration' }}</label>
|
||||
<select class="info-value" data-roll="true" id="collective-duration" name="collective-duration">
|
||||
{{#each rewards}}
|
||||
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{#if groups.length}}
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="group-select">{{ localize 'VERMINE.collective_group' }}</label>
|
||||
<select class="info-value" id="group-select" name="group-select">
|
||||
<option value="">{{ localize 'VERMINE.none' }}</option>
|
||||
{{#each groups}}
|
||||
<option value="{{ this.id }}">{{ this.name }} ({{ this.dice }}D)</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
{{/if}}
|
||||
</details>
|
||||
|
||||
<div class="exp-phase-characters">
|
||||
{{#each characters}}
|
||||
<details class="difficulty-section" data-character-id="{{ this.id }}" open>
|
||||
<summary class="flexrow">
|
||||
<span class="label">{{ this.name }}</span>
|
||||
<span class="current-values">{{ localize 'VERMINE.experience_dice' }}: {{ this.dice }}D</span>
|
||||
</summary>
|
||||
<div class="flexrow difficulty-controls">
|
||||
<div class="flexcol">
|
||||
<label class="label" for="implication-{{ this.id }}">{{ localize 'VERMINE.implication' }}</label>
|
||||
<select class="info-value" data-roll="true" id="implication-{{ this.id }}" name="implication-{{ this.id }}">
|
||||
{{#each @root.rewards}}
|
||||
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flexcol">
|
||||
<label class="label" for="influence-{{ this.id }}">{{ localize 'VERMINE.influence' }}</label>
|
||||
<select class="info-value" data-roll="true" id="influence-{{ this.id }}" name="influence-{{ this.id }}">
|
||||
{{#each @root.rewards}}
|
||||
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flexcol">
|
||||
<label class="label" for="interpretation-{{ this.id }}">{{ localize 'VERMINE.interpretation' }}</label>
|
||||
<select class="info-value" data-roll="true" id="interpretation-{{ this.id }}" name="interpretation-{{ this.id }}">
|
||||
{{#each @root.rewards}}
|
||||
<option value="{{ this.value }}">{{ this.label }} ({{ this.value }}D)</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="flexrow">
|
||||
<input type="checkbox" data-roll="true" id="objective-minor-{{ this.id }}" name="objective-minor-{{ this.id }}" />
|
||||
<span class="label">{{ localize 'VERMINE.objective_minor' }} (+3D)</span>
|
||||
</label>
|
||||
<label class="flexrow">
|
||||
<input type="checkbox" data-roll="true" id="objective-major-{{ this.id }}" name="objective-major-{{ this.id }}" />
|
||||
<span class="label">{{ localize 'VERMINE.objective_major' }} (+5D)</span>
|
||||
</label>
|
||||
<label class="flexrow">
|
||||
<input type="checkbox" data-roll="true" id="trauma-{{ this.id }}" name="trauma-{{ this.id }}" />
|
||||
<span class="label">{{ localize 'VERMINE.trauma_reward' }} (+5D)</span>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
{{else}}
|
||||
<p>{{ localize 'VERMINE.no_character' }}</p>
|
||||
{{/each}}
|
||||
</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="apply">
|
||||
<i class="fas fa-check"></i> {{localize "VERMINE.apply"}}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
<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="skill" checked /><span>{{ localize 'VERMINE.learn_skill' }}</span></label>
|
||||
<label><input type="radio" data-roll="true" name="learning-type" value="specialty" /><span>{{ localize 'VERMINE.learn_specialty' }}</span></label>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div id="learning-skill-section" class="learning-targets">
|
||||
<label class="label" for="learning-target">{{ localize 'VERMINE.learn_skill' }}</label>
|
||||
<select class="info-value" data-roll="true" id="learning-target" name="learning-target">
|
||||
{{#each skills}}
|
||||
<option value="{{ this.key }}" data-value="{{ this.value }}" data-rarity="{{ this.rarity }}" data-domain="{{ this.isDomain }}">
|
||||
{{ this.name }} ({{ this.value }} → {{ this.target }})
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="learning-specialty-section" class="learning-targets" style="display:none;">
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="specialty-name">{{ localize 'VERMINE.learn_specialty_name' }}</label>
|
||||
<input type="text" data-roll="true" id="specialty-name" name="specialty-name" value="{{ localize 'ITEMS.new_specialty' }}" />
|
||||
</div>
|
||||
<p class="learning-hint">{{ localize 'VERMINE.specialty_hint' }}</p>
|
||||
</div>
|
||||
|
||||
<div id="learning-ability-section" class="learning-targets" style="display:none;">
|
||||
<label class="label" for="learning-ability">{{ localize 'VERMINE.learn_characteristic' }}</label>
|
||||
<select class="info-value" data-roll="true" id="learning-ability" name="learning-ability">
|
||||
{{#each abilities}}
|
||||
<option value="{{ this.key }}">{{ this.label }} ({{ this.value }})</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<p class="learning-hint">{{ localize 'VERMINE.characteristic_hint' }}</p>
|
||||
</div>
|
||||
|
||||
<div id="learning-reserve-section" class="learning-targets" style="display:none;">
|
||||
<label class="label" for="learning-reserve">{{ localize 'VERMINE.learn_reserve' }}</label>
|
||||
<select class="info-value" data-roll="true" id="learning-reserve" name="learning-reserve">
|
||||
{{#each reserves}}
|
||||
<option value="{{ this.key }}">{{ this.label }} ({{ this.value }}/{{ this.max }})</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<p class="learning-hint">{{ localize 'VERMINE.reserve_hint' }}</p>
|
||||
</div>
|
||||
|
||||
<div id="learning-evolution-section" class="learning-targets" style="display:none;">
|
||||
<p class="learning-hint">{{ localize 'VERMINE.evolution_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>
|
||||
@@ -335,6 +335,33 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- EXPERIENCE DICE -->
|
||||
{{#if experience.hasExperience}}
|
||||
<details class="experience-section">
|
||||
<summary class="flexrow">
|
||||
<span class="label">{{localize 'VERMINE.experience_transfer'}}</span>
|
||||
<span class="current-values">{{localize 'VERMINE.experience_dice'}}: <span id="exp-individual">{{experience.individual}}</span>D</span>
|
||||
</summary>
|
||||
<div class="flexcol experience-controls">
|
||||
<div class="flexrow bonus-item">
|
||||
<label class="label" for="experience-pull">{{localize 'VERMINE.experience_pull'}}</label>
|
||||
<select class="info-value" data-roll="true" id="experience-pull" name="experience-pull"
|
||||
{{#unless experience.canPull}}disabled{{/unless}}>
|
||||
{{#each experience.pullOptions}}
|
||||
<option value="{{this}}">{{this}}D</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<span class="experience-group">{{localize 'VERMINE.collective_group'}}: <span id="exp-group">{{experience.groupPool}}</span>D</span>
|
||||
</div>
|
||||
<div class="flexrow bonus-item">
|
||||
<button type="button" data-action="giveToGroup" {{#unless experience.canGive}}disabled{{/unless}}>
|
||||
<i class="fas fa-share"></i> {{localize 'VERMINE.experience_give'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{{/if}}
|
||||
|
||||
<!-- TOTAL DICE POOL -->
|
||||
<div class="flexrow total-section">
|
||||
<label class="label">{{localize 'VERMINE.total'}}:</label>
|
||||
|
||||
Reference in New Issue
Block a user